Case 36: Anonymous Struct/Union Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_size_changed |
| Source files | examples/case36_anon_struct/ |
Category: Type Layout | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Variant's anonymous union member changes from float f to double d,
growing the union from 4 to 8 bytes and the struct from 8 to 16 bytes (with
alignment padding). Code compiled against v1 allocates and copies Variant
at the old 8-byte size; any binary passing Variant by value or embedding it
in a larger structure reads/writes at stale offsets once the new library is
loaded. Recompilation is mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
struct Variant { int tag; union { int i; float f; }; }; |
struct Variant { int tag; union { int i; double d; }; }; |
abicheck command¶
gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_size_changed: Size changed: Variant (64 -> 128 bits)
> Old code allocates or copies the type with the old size;
heap/stack corruption, out-of-bounds access.
Affected symbols: variant_get_int
- type_field_offset_changed: Field offset changed: Variant::i (32 -> 64 bits)
> Old code reads/writes fields at stale offsets; silent data corruption.
Affected symbols: variant_get_int
- type_field_removed: Field removed: Variant::f
> Old code accesses a field that no longer exists at the expected offset;
reads garbage or writes out of bounds.
Affected symbols: variant_get_int
Additions:
- type_field_added_compatible: Field added: Variant::d
> Field appended without changing existing offsets; old code works
but won't initialize the new field.
Minimum evidence¶
min_evidence: L1 โ DWARF's struct-layout info (DW_TAG_structure_type
size + member offsets, including the anonymous union's members) is enough to
detect the size and offset change; no public headers required.
Why abicheck catches it¶
DWARF flattens anonymous union members into the enclosing struct's member list with their own byte offsets; abicheck compares each named member's offset and the overall type size directly from debug info in both versions.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: app allocates Variant with v1's 8-byte layout, calls
variant_get_int() from v2 which reads i at offset 8 (shifted by the
union's new double alignment) instead of offset 4.
# Build old library + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ sizeof(Variant) = 8 (compiled against v1)
# โ tag = 1, i = 42
# โ variant_get_int() = 42
# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ sizeof(Variant) = 8 (compiled against v1)
# โ tag = 1, i = 42
# โ variant_get_int() = -1431655766
# โ ERROR: expected 42, got -1431655766 โ ABI layout mismatch!
Why CRITICAL: the struct size changed from 8 to 16 bytes and i's
offset shifted from 4 to 8 because double d forces 8-byte union alignment.
variant_get_int() reads past the app's 8-byte allocation, returning
sentinel bytes instead of the real value โ silent, deterministic data
corruption with no crash to signal it.
Safe redesign¶
Never widen a member of a public anonymous union โ it silently changes the enclosing struct's size and every sibling member's offset. Add a new, separately-named field instead, or move variant-sized payloads behind an opaque pointer so the union's internal layout isn't part of the ABI.
Real-world example: tagged-union APIs (e.g. JSON_Value-style variant
types) commonly hit this when a new numeric type is added to the union โ
widening any member without bumping the SONAME breaks every consumer that
allocated the smaller layout.
Cross-tool comparison¶
abidiff/abi-compliance-checker also detect this (a struct-layout change
from an anonymous-union member type swap) via their own DWARF-based layout
comparison:
References¶
Source files¶
CMakeLists.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.