Skip to content

Case 36 -- Anonymous Struct/Union Change

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds โ€”
Source files browse on GitHub

Verdict: ๐Ÿ”ด BREAKING abicheck verdict: BREAKING

What changes

Version Definition
v1 struct Variant { int tag; union { int i; float f; }; }; (size 8)
v2 struct Variant { int tag; union { int i; double d; }; }; (size 16)

Why this is a binary ABI break

Changing float f to double d inside the anonymous union increases the union's size from 4 bytes to 8 bytes. This changes the overall struct size from 8 to 16 bytes (with alignment padding). When the app allocates a Variant on the stack using v1's sizeof (8 bytes), but the v2 library's functions expect 16-byte objects, the mismatch can cause:

  • Stack corruption if the library writes beyond the 8-byte allocation
  • Wrong field offsets if the i member moves due to alignment changes
  • Subtle data corruption in arrays of Variant

The demo makes this offset mismatch deterministic by embedding the Variant in a buffer filled with sentinel bytes (0xAA). When v2's variant_get_int() reads v->i at offset 8 instead of offset 4, it reads the sentinel bytes instead of the actual value 42, producing a visibly wrong result every run.

Code diff

 struct Variant {
     int tag;
     union {
         int    i;
-        float  f;    /* 4 bytes -> union size = 4 */
+        double d;    /* 8 bytes -> union size = 8, struct gains padding */
     };
 };

Real Failure Demo

Severity: CRITICAL

# Build v1 lib + app
gcc -shared -fPIC -g v1.c -o libv1.so
gcc -g app.c -I. -L. -lv1 -Wl,-rpath,. -o app
./app
# -> sizeof(Variant) = 8 (compiled against v1)
# -> tag = 1, i = 42
# -> variant_get_int() = 42

# Swap to v2
gcc -shared -fPIC -g v2.c -o libv1.so
./app
# -> sizeof(Variant) = 8 (compiled against v1)    <-- app still uses v1's size!
# -> tag = 1, i = 42
# -> variant_get_int() = -1431655766               <-- WRONG! (0xAAAAAAAA sentinel)
# -> ERROR: expected 42, got -1431655766 โ€” ABI layout mismatch!
#      v2's variant_get_int() read 'i' at offset 8 instead of 4
#      (sentinel bytes 0xAA were read instead of the actual value)

Why CRITICAL: The struct size changed from 8 to 16 bytes. The double member in v2's anonymous union forces 8-byte alignment, shifting v->i from offset 4 to offset 8. Reading v->i via variant_get_int() is already undefined behavior due to the layout change โ€” the demo makes this visible by using sentinel-filled buffers so the wrong offset read produces a deterministic incorrect value.

Why runtime result may differ from verdict

Anon struct layout change: field offsets shift without DWARF, silent corruption

Runtime note

App returns non-zero on detected offset mismatch.

References


Source files

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.