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 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:

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml

References


Source files

  • CMakeLists.txt
  • app.c
  • v1.c
  • v1.h
  • v2.c
  • v2.h

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