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 |
โ |
| Source files | browse on GitHub |
Category: Breaking | Verdict: ๐ด BREAKING
What breaks¶
A nested "leaf" struct Leaf gains a new int z field, growing from 4 โ 8 bytes.
Leaf is embedded (not pointed to) inside Container, so Container::flags shifts
from offset 8 to offset 16. The public API only takes Container* โ no by-value
Leaf in function signatures โ but the size change propagates through the embedding.
Callers compiled against v1 allocate Container with the old layout: passing such
a struct to a v2 function causes flags to be read at the wrong offset.
Why abidiff catches it¶
abidiff reports Leaf_Type_Change / TYPE_SIZE_CHANGED and exits 4.
abicheck detects: TYPE_SIZE_CHANGED on Leaf (4โ8 bytes) propagates to Container.
Code 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 |
Real Failure Demo¶
Severity: ๐ด CRITICAL โ silent wrong-field reads
gcc -shared -fPIC -g v1.c -o libv1.so
gcc -shared -fPIC -g v2.c -o libv2.so
abidw --out-file v1.abi libv1.so
abidw --out-file v2.abi libv2.so
abidiff v1.abi v2.abi
echo "exit: $?" # โ 4 (TYPE_SIZE_CHANGED on Leaf โ Container)
Reproduce manually¶
gcc -shared -fPIC -g v1.c -o libv1.so
gcc -shared -fPIC -g v2.c -o libv2.so
abidw --headers-dir . --out-file v1.abi libv1.so
abidw --headers-dir . --out-file v2.abi libv2.so
abidiff v1.abi v2.abi
How to fix¶
- Pimpl on Leaf โ replace embedded
LeafwithLeaf*(pointer to incomplete type); size is pointer-stable. - Add field without breaking: only possible if there is tail padding in the struct โ verify with
pahole. - SONAME bump if embedding is required and the field is necessary.
Real-world pattern¶
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 were broken โ same
propagation mechanism as this case. See case18_dependency_leak for the external-library variant.
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.