Case 26: Union Field Added (Size Grows)¶
| Field | Value |
|---|---|
| Verdict | 🔴 BREAKING |
| Category | Breaking |
| Classification | Rule |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break |
Detected ChangeKinds |
union_field_added, type_size_changed |
| Source files | catalog/cases/case26_union_field_added/ |
| Rule family | union-field-added |
| Subject | Struct and type layout changes |
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¶
new/old/CMakeLists.txtapp.c
See also: Compatibility Catalog · All BREAKING cases · Category: Breaking · Rule: Union gained a member that grows it · Subject: Struct and type layout changes.