Case 26: Union Field Added (Size Grows)¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break |
Detected ChangeKinds |
union_field_added, type_size_changed |
| Source files | examples/case26_union_field_added/ |
Category: Type Layout | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
union Value gains a double d member. double (8 bytes) is larger than
either existing member (int i/float f, both 4 bytes), so
sizeof(union Value) grows from 4 to 8 bytes. Every existing field stays
at offset 0, but any caller that stack- or heap-allocated the old
4-byte size and passes it to the v2 library's fill() โ which now writes
a full 8-byte double โ writes past the end of its own allocation.
Old/new diff¶
| old/lib.h | new/lib.h |
|---|---|
union Value { int i; float f; }; |
union Value { int i; float f; double d; }; |
fill() writes v->i = 42; |
fill() writes v->d = 3.1415926535; |
abicheck command¶
gcc -shared -fPIC -g old/lib.c -Iold -o libval_v1.so
gcc -shared -fPIC -g new/lib.c -Inew -o libval_v2.so
abicheck compare libval_v1.so libval_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_size_changed: Size changed: Value (32 -> 64 bits)
> Old code allocates or copies the type with the old size; heap/stack
corruption, out-of-bounds access.
Affected symbols: fill
Additions:
- union_field_added: Union field added: Value::d (double)
> Union size may grow; old code allocating with old sizeof gets
truncated data.
Affected symbols: fill
Minimum evidence¶
min_evidence: L1 โ DWARF's DW_TAG_union_type carries the union's total
byte size directly (the max of its members' sizes), so -g alone is
enough to see the 32โ64 bit size change; no public headers required.
Why abicheck catches it¶
DWARF records each union's total byte size for both versions; abicheck
compares the sizes directly from debug info and reports a mismatch as
type_size_changed, alongside the new member itself as
union_field_added.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: app stack-allocates a 4-byte union Value (the v1 layout)
and calls fill(). Against v2, fill() writes a full 8-byte double
into that 4-byte allocation.
# Build old library + app
gcc -shared -fPIC -g old/lib.c -Iold -o libval.so
gcc -g app.c -Iold -L. -lval -Wl,-rpath,. -o app
./app
# โ after fill: v.i = 42
# โ unexpected old-compatible value
# (exit 0)
# Swap in new library (no recompile)
gcc -shared -fPIC -g new/lib.c -Inew -o libval.so
./app
# โ *** stack smashing detected ***: terminated
# (exit 134 โ aborted by __stack_chk_fail)
Why CRITICAL: sizeof(union Value) grows from 4 to 8 bytes because of
the new double d member, but the app still only reserves 4 bytes on the
stack for it. v2's fill() writes 8 bytes into that 4-byte slot,
overwriting the stack canary; glibc's stack protector detects the
corruption and aborts the process.
Safe redesign¶
Never add a union member whose size exceeds the current largest member in
a binary-compatible release. If a wider variant is genuinely needed,
introduce a new type (ValueV2) or a tagged union with an explicit
discriminator so callers opt in to the larger size deliberately, rather
than being silently under-allocated by an old header.
Real-world example: kernel UAPI unions (e.g. union bpf_attr) are
deliberately padded to a fixed maximum size up front, precisely so that
new members can be added later without ever changing sizeof().
Cross-tool comparison¶
References¶
Source files¶
CMakeLists.txtapp.c
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.