case152_enum_size_flag_flip โ Enum-size flag flip (-fshort-enums)¶
| Field | Value |
|---|---|
| Verdict | ๐ก COMPATIBLE_WITH_RISK |
| Category | Risk |
| Platforms | Linux |
| Flags | Bad practice |
Detected ChangeKinds |
enum_size_flag_changed |
| Source files | examples/case152_enum_size_flag_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: v2 was built with -fshort-enums. That flag
makes the compiler pick the smallest integer type that holds an enum's
range, so any enum member of a public struct, enum-typed parameter, or
enum return value can change size โ and as a struct member it shifts every
field that follows it. Nothing in the binary or the header text records
this; it is purely a property of how the library was built, so consumers
built against the old flag setting are at risk of silent layout drift the
moment both sides stop agreeing on it.
Old/new diff¶
| Build flags | v1 | v2 |
|---|---|---|
| enum sizing | default (int-sized) | -fshort-enums |
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:
- enum_size_flag_changed: Runtime-model option 'enum_size' changed: 'int' -> 'short'.
> 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. A
symbol-only (L0) or even DWARF-only (L1) check of one build sees a
self-consistent binary; the incompatibility only exists between two
builds made under different enum-size assumptions, which lives in the L3
build option, not in either binary alone.
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 enum_size option directly โ the same
diff_build_evidence() routine tests/test_l3l4l5_examples.py exercises
against the committed fixtures. Per ADR-028 D3 a build-evidence finding never
decides a shipped-ABI break on its own; it flags the elevated risk and
localizes the cause, and an artifact (L0/L1) diff is what would confirm any
concrete layout break.
Runtime failure demonstration¶
There's no compiled app.c consumer for this case โ the failure mode is a
CI/release-engineering one, not a single process crash. Picture a packaging
pipeline that rebuilds libdemo.so for a new distro target and flips
-fshort-enums along the way (a common ARM/embedded toolchain default) while
downstream consumers keep linking against headers built for the old
int-sized enums: any consumer that embeds an enum-typed field in a public
struct silently reads/writes it at the wrong offset after the next rebuild.
A CI job that diffs compile_commands.json (or an equivalent captured
build-options record) across releases is exactly what would catch this
before it reaches a consumer.
Safe redesign¶
Build the library and all its consumers with the same -fshort-enums
setting, or avoid exposing bare enums in the ABI โ use fixed-width
underlying types (enum E : int { ... }) so the size is pinned regardless
of compiler flags.
Cross-tool comparison¶
abidiff/abi-compliance-checker compare built binaries (symbols + DWARF);
neither reads compile options, so a rebuild with identical source and
identical exports under a different -fshort-enums setting produces no
diff for either tool unless the flag actually changed a concrete struct's
size (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 flag flip.
Source files¶
new.jsonold.json
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.