Case 48: Leaf Struct Change Propagated Through Pointer¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_size_changed |
| Source files | examples/case48_leaf_struct_through_pointer/ |
Category: Breaking | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
The nested "leaf" struct Leaf gains a new int z field, growing from 4 to
8 bytes. Leaf is embedded (not pointed to) inside Container, so
Container::flags shifts from offset 8 to offset 16. The public API only
ever takes Container* โ no by-value Leaf appears in any signature โ but
the size change still propagates through the embedding. A caller compiled
against v1 allocates Container at the old size and offsets; passing that
struct into a v2 build reads flags from the wrong bytes. Recompilation
against v2 is mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
typedef struct Leaf { short x; short y; } Leaf; โ 4 bytes |
typedef struct Leaf { short x; short y; int z; } Leaf; โ 8 bytes |
Container { int id; Leaf position; int flags; } โ flags at offset 8 |
Container { int id; Leaf position; int flags; } โ flags at offset 16 |
abicheck command¶
gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- type_size_changed: Size changed: Leaf (32 -> 64 bits)
> Old code allocates or copies the type with the old size; heap/stack
corruption, out-of-bounds access.
Affected symbols: container_flags, container_get_pos, container_init
- type_size_changed: Size changed: Container (96 -> 128 bits)
Affected symbols: container_flags, container_get_pos, container_init
- type_field_offset_changed: Field offset changed: Container::flags (64 -> 96 bits)
> Old code reads/writes fields at stale offsets; silent data corruption.
Additions:
- type_field_added_compatible: Field added: Leaf::z
Minimum evidence¶
min_evidence: L1 โ DWARF's struct-layout info (DW_TAG_structure_type
size and member offsets) for both Leaf and Container is enough to see the
size grow and flags shift; no public headers required.
Why abicheck catches it¶
abicheck reads the DWARF member list and byte offsets of every struct in
both snapshots, including structs only ever reached indirectly through
embedding in another type. It flags Leaf's own size change and then, from
Container's member offsets, the resulting shift in Container::flags โ the
propagation falls straight out of comparing offsets, no special-casing needed.
Runtime failure demonstration¶
Severity: CRITICAL โ silent wrong-field reads
Scenario: app allocates a Wrapped { Container c; int guard; } with v1
layout; a v2 container_init() writes past the old Container boundary into
guard.
# Build old library + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ pos=(11,22) flags=0 guard=0x12345678
# โ expected: pos=(11,22) flags=0 guard=0x12345678
# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ pos=(11,22) flags=0 guard=0x0
# โ expected: pos=(11,22) flags=0 guard=0x12345678
# โ CORRUPTION: nested leaf layout changed and overwrote caller memory
Why CRITICAL: v2's container_init() writes Leaf::z and flags using
the new (larger) Container layout, spilling past the 12-byte allocation the
app reserved for Container under v1 and zeroing the adjacent guard field
โ a classic silent struct-embedding overflow.
Safe redesign¶
Replace the embedded Leaf with a pointer to an incomplete type (Pimpl) so
Container's size stays pointer-stable regardless of Leaf's internals; if
embedding is required, only add fields into pre-existing tail padding
(verify with pahole) or bump the SONAME.
Real-world example: TBB's tbb::task_arena was embedded in some
library public headers. When TBB changed task_arena's internal layout, all
consumers of those libraries broke โ the same propagation-through-embedding
mechanism as this case. See case18_dependency_leak for the
external-library variant.
Cross-tool comparison¶
abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 4
Note on abidiff: libabigail reports this as a
Leaf_Type_Changethat propagates intoContainer's size, exit code 4 โ the same conclusion abicheck reaches from the same DWARF layout evidence.
Source files¶
CMakeLists.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.