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 browse on GitHub

Category: Type Layout | Verdict: ๐ŸŸก ABI CHANGE (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.

What breaks

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.

Why abidiff catches it

Reports type size changed from 64 to 96 (in bits) and 1 data member insertion.

Code diff

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

Reproduce manually

gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?"   # โ†’ 4

How to fix

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.

Real Failure Demo

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.

References


Source files

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