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.jsonold.json
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.