Skip to content

Case 24: Union Field Removed

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds union_field_removed
Source files examples/case24_union_field_removed/

Category: Type Layout | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

union Data loses its float f member in v2 โ€” only int i remains. Any caller compiled against v1 that reads or writes d.f is now interacting with a member the library no longer maintains: the library's init_data() stores an int bit pattern, but the caller reinterprets those same bits as a float. The result is silent data corruption, not a crash โ€” the removed alternative was part of the union's public contract.

Old/new diff

old/lib.h new/lib.h
union Data { int i; float f; }; union Data { int i; };
init_data() writes d->f = 3.14f; init_data() writes d->i = 42;

abicheck command

gcc -shared -fPIC -g old/lib.c -Iold -o libdata_v1.so
gcc -shared -fPIC -g new/lib.c -Inew -o libdata_v2.so
abicheck compare libdata_v1.so libdata_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- union_field_removed: Union field removed: Data::f (float)
  > Old code accessing removed alternative reads uninitialized memory.
  Affected symbols: init_data
- struct_field_removed: Field removed from struct; old code accessing it
  reads/writes garbage.
  Affected symbols: init_data

Minimum evidence

min_evidence: L1 โ€” DWARF's member list for Data (DW_TAG_union_type's DW_TAG_member children) records each variant's name and type for both versions; abicheck diffs the two member sets directly from debug info, no public headers required.

Why abicheck catches it

DWARF encodes union members the same way it encodes struct members โ€” a list of DW_TAG_member children, all sharing offset 0; abicheck compares the old and new member lists and reports any variant present in v1 but absent in v2 as union_field_removed.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app writes/reads d.f after init_data(). v2's init_data() writes an int (42) into the same storage; the app still reinterprets it as a float.

# Build old library + app
gcc -shared -fPIC -g old/lib.c -Iold -o libdata.so
gcc -g app.c -Iold -L. -ldata -Wl,-rpath,. -o app
./app
# โ†’ d.f = 3.140000e+00 (expected ~3.14)

# Swap in new library (no recompile)
gcc -shared -fPIC -g new/lib.c -Inew -o libdata.so
./app
# โ†’ d.f = 5.885454e-44 (expected ~3.14)
# โ†’ UNION_MISMATCH: removed float field changed interpretation

Why CRITICAL: the library now writes integer bits where the caller still reads float bits from the same storage. There is no crash or diagnostic โ€” just a silently wrong value, here 5.885454e-44 instead of ~3.14.

Safe redesign

Never remove a union variant that's part of the public contract. If a variant is truly obsolete, keep it declared (even if the library stops writing it) for at least one deprecation cycle, or move to a tagged union with an explicit discriminator so callers can detect which variant is currently valid.

Real-world example: POSIX unions such as union sigval keep every historical variant declared indefinitely โ€” removing one would silently break any consumer still reading it.

Cross-tool comparison

abidw --out-file v1.xml libdata_v1.so
abidw --out-file v2.xml libdata_v2.so
abidiff v1.xml v2.xml

References


Source files

  • CMakeLists.txt
  • app.c

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