Case 40: Field Layout Changes¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
type_size_changed |
| Source files | examples/case40_field_layout/ |
Category: Struct Field Layout | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Packet changes on four axes in one release: version widens from int
to long, sequence is removed, the flags bitfield grows from 4 to 8
bits, and a new priority field is appended. Together these push
payload_size and flags to different byte offsets and grow the struct
from 16 to 24 bytes. Any binary that allocates, copies, or reads/writes a
Packet compiled against v1 is working with the wrong offsets against v2 โ
silently, with no crash to flag it. Recompilation is mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
int version; |
long version; (type widened) |
int sequence; |
(removed) |
int payload_size; |
int payload_size; (offset shifts) |
unsigned flags : 4; |
unsigned flags : 8; (bitfield widened) |
| โ | int priority; (appended) |
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: Packet (128 -> 192 bits)
> Old code allocates or copies the type with the old size;
heap/stack corruption, out-of-bounds access.
Affected symbols: packet_send
- type_field_type_changed: Field type changed: Packet::version (int -> long int)
> Field has different size or representation; old code misinterprets the data.
Affected symbols: packet_send
- type_field_removed: Field removed: Packet::sequence
> Old code accesses a field that no longer exists at the expected offset;
reads garbage or writes out of bounds.
Affected symbols: packet_send
- field_bitfield_changed: Bitfield layout changed: Packet::flags (bits=4 -> bits=8)
> Bit-field width or offset changed; old code reads/writes wrong bits.
Additions:
- type_field_added_compatible: Field added: Packet::priority
> 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, per-member type and offset, plus DW_AT_bit_size/DW_AT_data_bit_offset
for the bitfield) is enough to detect all four changes; no public headers
required.
Why abicheck catches it¶
DWARF records each struct member's type, byte offset, and (for bitfields) bit-size and bit-offset for both versions; abicheck compares the member list, per-member type/offset, and bitfield width directly from debug info, alongside the type's overall byte size.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: app builds a Packet with v1's layout and computes an
expected checksum from its own field reads, then calls packet_send()
which recomputes the same checksum from inside the library โ first against
v1's layout, then against v2's after the library is swapped.
# 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
# โ sizeof(Packet) = 16
# โ expected checksum (v1 layout) = 1082
# โ packet_send() = 1082
# โ exit 0
# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ sizeof(Packet) = 16
# โ expected checksum (v1 layout) = 1082
# โ packet_send() = -263838592
# โ LAYOUT_MISMATCH: library interpreted struct with incompatible field layout
# โ exit 2
Why CRITICAL: the app's Packet is still only 16 bytes (built against
v1), but v2's packet_send() reads version as an 8-byte long,
payload_size/flags at their new offsets, and priority at an offset
past the end of the app's allocation. There is no crash โ every field read
just returns different, wrong data, which is exactly the class of ABI break
that's hardest to notice in production.
Safe redesign¶
Never change field types, remove fields, reorder fields, or resize
bitfields in a public struct. Use an opaque pointer (struct Packet *)
with accessor functions so internal layout can evolve freely, and reserve
appending fields for the rare case where every consumer allocates through a
library-provided constructor (never sizeof/stack allocation directly).
Real-world example: wire-protocol/packet structs shared across a library boundary are a classic source of this bug โ the C struct doubles as the serialization format, so any "just add one field" change silently reinterprets the whole struct the moment offsets shift.
Cross-tool comparison¶
abidw --out-file v1.xml libv1.so
abidw --out-file v2.xml libv2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 12 (ABI change + breaking)
References¶
Source files¶
CMakeLists.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.