Skip to content

Case 44: Cyclic Type Member Added

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds โ€”
Source files browse on GitHub

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

What breaks

A long priority field is added to a self-referential linked-list Node struct. On 64-bit systems, the existing padding is consumed and sizeof(Node) grows from 16 โ†’ 24 bytes. Because node_sum() accepts Node by value, the size is directly part of the ABI calling convention โ€” callers compiled against v1 pass a 16-byte struct on the stack while the v2 function expects 24 bytes.

Why abidiff catches it

abidiff reports Added_Non_Virtual_Member_Variable and exits 4. abicheck detects: TYPE_SIZE_CHANGED (Node: 16โ†’24 bytes).

Code diff

v1.h v2.h
struct Node { int data; int flags; struct Node *next; } struct Node { int data; int flags; long priority; struct Node *next; }
int node_sum(Node n); โ€” by value, 16 bytes int node_sum(Node n); โ€” by value, 24 bytes

Real Failure Demo

Severity: ๐Ÿ”ด CRITICAL

Scenario: compile app against v1, run with v2 .so โ€” stack corruption due to by-value size mismatch.

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)

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

Avoid by-value passing of structs in public API: 1. Pass by pointer โ€” int node_sum(const Node *n) โ€” pointer size is stable. 2. Opaque handle โ€” hide Node behind a typedef to an incomplete struct. 3. Version bump โ€” if by-value semantics are required, bump SONAME.


Source files

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