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 | browse on GitHub |
Verdict: ๐ด BREAKING abicheck verdict: BREAKING
What changes¶
| Version | Definition |
|---|---|
| v1 | union Data { int i; float f; }; |
| v2 | union Data { int i; }; |
What breaks at binary level¶
Removing a union field removes a supported representation of the shared storage.
While the remaining field (int i) is still at offset 0 and accessible, any code
that was compiled to use the float f variant is now accessing an undefined member.
If the removal also reduces the union's size (e.g., removing a double field from
a union of int and double), existing code that allocates sizeof(union Data) with
the old size will over-allocate (harmless) or under-allocate if the field was the
largest member (harmful โ but this is caught by TYPE_SIZE_CHANGED).
The semantic break is the main concern: the removed field was part of the type's public contract, and consumers relied on it as a valid interpretation.
Note: Adding a union field is classified as COMPATIBLE because all fields share offset 0 and existing fields are unaffected. Size increases are caught separately.
Consumer impact¶
/* consumer compiled against v1 */
union Data d;
d.f = 3.14f; /* valid in v1 */
lib_consume(d); /* library no longer expects float variant */
/* v2: 'f' field doesn't exist โ semantic mismatch */
Mitigation¶
- Keep union variants stable across ABI-compatible releases.
- Introduce versioned replacement types (e.g.,
DataV2) when the union contract must change. - Use tagged unions with explicit discriminators to manage variant evolution.
Code diff¶
Real Failure Demo¶
Severity: CRITICAL
Scenario: app reads d.f as float after init_data(). With v2 the function writes d.i = 42 โ reading as float gives garbage.
# Build old lib + 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.140000
# Swap in new lib (writes int 42 instead of float 3.14)
gcc -shared -fPIC -g new/lib.c -Inew -o libdata.so
./app
# โ d.f = 0.000000 (expected 3.14, got wrong value with v2)
# (integer 42 reinterpreted as IEEE 754 float = ~5.88e-44)
Why CRITICAL: The library writes integer bits, the caller reads float bits from the same storage. Silent wrong value โ no crash, no error, just completely wrong data.
Why runtime result may differ from verdict¶
Union field removed: accessing removed field reads undefined memory
Runtime note¶
Runtime check now fails explicitly when removed union member changes data interpretation.
References¶
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.