Skip to content

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.txt
  • app.c
  • v1.c
  • v1.h
  • v2.c
  • v2.h

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