Skip to content

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_Change that propagates into Container's size, exit code 4 โ€” the same conclusion abicheck reaches from the same DWARF layout evidence.


Source files

  • CMakeLists.txt
  • app.c
  • v1.c
  • v1.h
  • v2.c
  • v2.h

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.