Skip to content

Case 07: Struct Layout Change

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux
Flags ABI break, API break
Detected ChangeKinds type_size_changed
Source files examples/case07_struct_layout/

Category: Type Layout | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

Code compiled against v1 allocates sizeof(Point) = 8 bytes. v2's Point is 12 bytes. Stack/heap allocations are undersized; the z field reads/writes outside the allocated region. Any binary passing Point by value is broken without recompilation.

Old/new diff

v1.c v2.c
struct Point { int x; int y; }; struct Point { int x; int y; int z; };

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: Point (64 -> 96 bits)
  > Old code allocates or copies the type with the old size;
    heap/stack corruption, out-of-bounds access.
  Affected symbols: get_x, init_point

Additions:
- type_field_added_compatible: Field added: Point::z
  > Field appended without changing existing offsets; old code works
    but won't initialize the new field.

Minimum evidence

min_evidence: L1 โ€” DWARF's struct-layout info (DW_TAG_structure_type size + member offsets) is enough to detect the size change; no public headers required.

Why abicheck catches it

DWARF records each struct's total byte size and every member's offset for both versions; abicheck compares them directly from debug info.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app allocates Point with v1 layout (8 bytes), calls init_point() from v2 which writes a z field at offset 8 โ€” past the allocation.

# Build v1 + app (use -O0 to ensure predictable stack layout for canary demo)
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g -O0 app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ before: p={0,0} canary=0xDEADBEEF
# โ†’ after:  p={1,2} canary=0xDEADBEEF

# Swap in v2 (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ†’ before: p={0,0} canary=0xDEADBEEF
# โ†’ after:  p={1,2} canary=0x00000003   โ† CORRUPTED
# โ†’ CORRUPTION detected! (v2 wrote past end of struct)

Why CRITICAL: The v2 library writes a z field at byte offset 8, but the app only allocated 8 bytes for the struct. The canary variable on the stack is overwritten โ€” a classic stack corruption that can corrupt control flow or cause silent data loss.

Safe redesign

Never add fields to public structs. Use the opaque-pointer (PIMPL) idiom: expose struct Point* and allocate/free through library functions, so the struct layout is hidden from callers.

Real-world example: the C standard library's FILE* is a classic opaque handle โ€” callers never see the struct layout; all access is through fopen/fread/fclose. This pattern keeps the ABI stable across libc versions even as the internal FILE struct changes.

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 2.4.0: Struct layout changes return exit 4 (not 12), but the change is semantically breaking โ€” all callers allocate the old size and pass wrong-length data.

References


Source files

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

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