Skip to content

case153_struct_packing_flip โ€” Struct-packing mode flip (-fpack-struct)

Field Value
Verdict ๐ŸŸก COMPATIBLE_WITH_RISK
Category Risk
Platforms Linux
Flags Bad practice
Detected ChangeKinds struct_packing_mode_changed
Source files examples/case153_struct_packing_flip/

Category: Risk | Verdict: ๐ŸŸก COMPATIBLE_WITH_RISK

Verdict and consumer impact

v1 and v2 have identical source and identical exported symbols โ€” only the captured build flag differs: v1 was built with -fpack-struct=8, v2 with -fpack-struct=1. Reduced packing removes inter-member padding, so every member offset and the type's sizeof can change with no source or symbol change at all. A consumer compiled against the old packing reads struct fields at stale offsets after linking against the new library โ€” silent corruption, not a link error.

Old/new diff

Build flags v1 v2
struct packing -fpack-struct=8 -fpack-struct=1

Source and exported symbols are byte-for-byte identical between v1 and v2; only this compile option differs.

abicheck command

The case ships old.json/new.json as hand-built BuildEvidence fixtures (the normalized L3 model dump --build-info/--sources would produce from a real build) rather than compiled binaries, so reproducing the finding means embedding each side's fixture into a snapshot's build_source field โ€” exactly what dump --build-info does internally โ€” and comparing the two:

python3 - <<'PY'
import json
from pathlib import Path
from abicheck.model import AbiSnapshot
from abicheck.buildsource.pack import BuildSourcePack
from abicheck.buildsource.build_evidence import BuildEvidence
from abicheck.serialization import save_snapshot

for side in ("old", "new"):
    d = json.loads(Path(f"{side}.json").read_text())
    snap = AbiSnapshot(library="libdemo.so", version="1")
    snap.build_source = BuildSourcePack(root=Path(""), build_evidence=BuildEvidence.from_dict(d))
    save_snapshot(snap, f"{side}.abi.json")
PY

abicheck compare old.abi.json new.abi.json

Expected abicheck finding

Verdict: COMPATIBLE_WITH_RISK (exit 0)

Deployment Risk Changes:
- struct_packing_mode_changed: Runtime-model option 'struct_packing' changed: '8' -> '1'.
  > May not be link- or runtime-compatible across consumers; the artifact
    diff confirms any concrete break.

Minimum evidence

min_evidence: L3 โ€” the compile options themselves carry the fact. Nothing in the exported symbol table records the packing policy; a single build looks internally consistent, so only the L3 build option (or an artifact diff of two actual builds) exposes the mismatch.

Why abicheck catches it

abicheck compare reads each side's normalized BuildEvidence.build_options (as embedded by dump --build-info/--sources, or supplied out-of-band via --old/new-build-info) and diffs the struct_packing option directly โ€” the same diff_build_evidence() routine tests/test_l3l4l5_examples.py exercises against the committed fixtures. Struct packing's compiler default is target-dependent (GCC/Clang natural packing vs. MSVC's /Zp8//Zp16), so abicheck reports a flip only when both sides state the packing explicitly, avoiding a false finding on a Visual Studio project that merely records its platform default. Per ADR-028 D3 this build-evidence finding never decides a shipped-ABI break on its own โ€” it flags the elevated risk and localizes the cause; an artifact diff of the actual built layout is what would confirm a concrete break.

Runtime failure demonstration

There's no compiled app.c consumer for this case โ€” the failure mode is a build-configuration one, not a single process crash. Picture a project where one CMake target sets add_compile_options(-fpack-struct=1) for a size-constrained embedded build while a shared library it links stays on the default packing: every consumer that passes a public struct by value or by pointer across that boundary reads fields at the wrong offset the moment the two are linked together. A CI job that diffs captured build options (compile_commands.json or an equivalent record) across build configurations, not just across releases, is exactly what would catch this.

Safe redesign

Use one packing policy across the library and its consumers. Prefer explicit #pragma pack / alignas on the specific types that need tight packing over a global -fpack-struct flag that silently affects every struct in the translation unit.

Cross-tool comparison

abidiff/abi-compliance-checker compare built binaries (symbols + DWARF); neither reads compile options, so two builds from identical source under different -fpack-struct settings produce no diff for either tool unless the flag actually changed a concrete struct's offsets in the compiled output (at which point it's the resulting layout change they'd catch, not the flag itself). Only the L3 build-evidence layer that abicheck reads directly localizes the cause to the packing flag flip.


Source files

  • new.json
  • old.json

See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.