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¶
References¶
Source files¶
CMakeLists.txtapp.c
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.