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
imember 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.