Skip to content

Case 56: Struct Packing Changed (pragma pack)

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS
Flags ABI break, API break
Detected ChangeKinds struct_packing_changed
Source files examples/case56_struct_packing_changed/

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

Verdict and consumer impact

v1's Record uses natural alignment (sizeof(Record) = 12, value at offset 4). v2 wraps the same struct in #pragma pack(push, 1), eliminating all padding (sizeof(Record) = 6, value at offset 1). Any binary compiled against v1 that allocates, copies, or indexes into Record reads/writes the wrong bytes once v2's library is loaded โ€” a silent field-offset break, not a crash at load time. Recompilation against v2 is mandatory.

Old/new diff

bad.h (v1) good.h (v2)
typedef struct { char tag; int value; char status; } Record; (natural alignment) #pragma pack(push, 1) around the same fields
sizeof(Record) = 12, value at offset 4 sizeof(Record) = 6, value at offset 1

abicheck command

gcc -shared -fPIC -g -include bad.h  bad.c  -o libfoo_v1.so
gcc -shared -fPIC -g -include good.h good.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: Record (96 -> 48 bits)
  > Old code allocates or copies the type with the old size;
    heap/stack corruption, out-of-bounds access.
  Affected symbols: record_create, record_destroy, record_get_value
- type_field_offset_changed: Field offset changed: Record::value (32 -> 8 bits)
  > Old code reads/writes fields at stale offsets; silent data corruption.
- type_field_offset_changed: Field offset changed: Record::status (64 -> 40 bits)
  > Old code reads/writes fields at stale offsets; silent data corruption.
- struct_packing_changed: Struct packing added: Record is now __attribute__((packed))
  > Packing attribute changed; field offsets differ from what old code expects.

Minimum evidence

min_evidence: L1 โ€” DWARF's struct-layout info (DW_AT_byte_size and each member's DW_AT_data_member_location) is enough to detect both the size shrink and every field's offset move; no public headers required, even though #pragma pack is a header-level attribute โ€” its effect is fully visible in the compiled layout DWARF records.

Why abicheck catches it

DWARF records each struct's total byte size and every member's byte offset for both versions; abicheck diffs those directly and additionally recognizes the packed-vs-natural alignment pattern across the two field-offset deltas as a struct_packing_changed finding.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app compiled against v1's natural-alignment Record (12 bytes, value at offset 4), library swapped for v2's packed Record (6 bytes, value at offset 1) without recompiling.

# Build old library + app
gcc -shared -fPIC -g -include bad.h bad.c -o libfoo.so
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ value = 42

# Swap in new library (no recompile)
gcc -shared -fPIC -g -include good.h good.c -o libfoo.so
./app
# โ†’ value = 22528
# โ†’ WRONG RESULT: struct packing/layout changed

Why CRITICAL: the app's own Record layout (compiled from bad.h) still expects value at offset 4, but v2's record_create writes value at offset 1 into a 6-byte allocation the app never resized. The read at offset 4 lands on bytes that used to be padding/status, producing a silently wrong value instead of a crash.

Safe redesign

Never change a public struct's packing/alignment after release โ€” it's an ABI-visible layout change even though the struct definition still "looks the same" at the field-list level. Use the opaque-pointer (PIMPL) idiom instead: expose Record* and let the library allocate/free it, so callers never embed the struct's size or layout in their own code.

Real-world example: Windows API headers use #pragma pack extensively; mixing packed and unpacked struct definitions across DLL boundaries (e.g. a stale cached header on one side of the link) is a recurring source of silent data corruption in interop code.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml

References


Source files

  • CMakeLists.txt
  • app.c
  • bad.c
  • bad.h
  • good.c
  • good.h

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