Case 149: ODR Type Variant (Cross-Source, L4 Layout ↔ Layout)¶
| Field | Value |
|---|---|
| Verdict | 🟠 API_BREAK |
| Category | API Break |
| Classification | Scenario — Capability / evidence demonstration · audit |
| Platforms | Linux |
| Flags | API break |
Detected ChangeKinds |
odr_type_variant |
| Source files | catalog/cases/case149_xcheck_odr_variant/ |
| Related rules | odr-type-variant |
| Subject | Export/declaration mismatches |
Category: API Break (Audit) | Verdict: 🟠 API_BREAK
Verdict and consumer impact¶
Single-release audit: one build's evidence checked against itself, no
baseline comparison. This finding's own kind is classified API_BREAK (the severity
ground-truth row above); the audit itself reports no compatibility
verdict and, by default, exits 0 — an audit has no baseline to break
against, so it never emits 2/4, the compatibility family's
own break codes. Gating on a hygiene finding like this one is opt-in via the
orthogonal audit-gate axis
:
adding --severity-preset default (or strict) reproduces legacy scan's
gating decision on this exact finding, but through its own exit code 3,
never 2, so a hygiene gate can't be mistaken for a real compatibility
break — measured live: abicheck compare --no-baseline snapshot.abi.json
exits 0, abicheck compare --no-baseline snapshot.abi.json
--severity-preset default exits 3. The finding: two translation
units materialize one public type, geometry::Vec3, with different
layouts (one TU's definition carries an extra member behind a macro the
other TU does not see). This is an ODR violation: the linker picks one
definition arbitrarily, and call sites compiled against the other TU's
layout read the wrong bytes at runtime. The shipped binary contains exactly
one layout, so nothing about the binary itself looks wrong — the conflict
only exists in the relationship between the two TUs' own source-replayed
definitions.
What this snapshot contains¶
snapshot.abi.json is a single, hand-built AbiSnapshot carrying the L4
per-TU source-ABI replay for two translation units that both define
geometry::Vec3:
| Source in the snapshot | What it records |
|---|---|
Per-TU source-ABI surface (L4, build_source.source_abi per TU) |
TU-A's replayed geometry::Vec3 layout hash and TU-B's replayed geometry::Vec3 layout hash, and they differ |
abicheck command¶
compare --no-baseline, not scan
0.6 makes this the declared spelling for a single-build audit, and
retires scan. This case was blocked on that migration until
2026-09-09; the audit now reports the finding below directly, and
tests/parity/test_no_baseline_audit_corpus_parity.py pins that it
reports at least every check scan does, counted per finding kind,
while manufacturing no comparison of its own (no verdict, no
changes[] entry).
Expected abicheck finding¶
# ABI audit: libdemo.so (no baseline)
OLD side: **declared absent** (`--no-baseline`) -- this is an audit of the candidate build alone, not a compatibility comparison. No additions, removals, or compatibility verdict are reported.
- Candidate version: `1.0`
- Acquisition state (OLD): `declared_absent`
- Evidence tiers: header
## Candidate-side findings
| Finding | Symbol | Severity | State | Detail |
| --- | --- | --- | --- | --- |
| `odr_type_variant` | `geometry::Vec3` | potential_breaking | present in this build | Type 'geometry::Vec3' has divergent per-translation-unit definitions in 'include/geometry/vec3.h': the source-replay surface recorded different layouts for the same type. Linking code that mixes them is undefined behavior — a consumer compiled against one layout silently reads the other. Reconcile the definitions (usually a macro/flag that changes the type per TU). |
Minimum evidence¶
min_evidence: L4 — the shipped binary (L0/L1) and the header AST (L2)
each see exactly one geometry::Vec3 definition and look internally
consistent; only replaying each translation unit's own source (L4) and
comparing the per-TU layouts against each other exposes that two TUs
disagree about the same type's layout.
Why abicheck catches it¶
| Source | What it sees alone |
|---|---|
| Binary (L0/L1) | one geometry::Vec3 layout — looks self-consistent |
| Header AST (L2) | one declaration — looks self-consistent |
| Per-TU source-ABI replay (L4) | TU-A's Vec3 hash ≠ TU-B's Vec3 hash |
| Combination | the L4 surface records the per-TU conflict → odr_type_variant (API_BREAK) |
odr_type_variant reads the L4 source-replay surface's recorded per-TU
type hashes, supplied by the source_index provider — no single artifact
layer can see it, because the binary contains only the one layout the
linker happened to pick.
Why this matters for a real release¶
Whichever TU's definition the linker keeps, every call site compiled
against the other TU's assumed layout reads or writes the wrong offsets
for geometry::Vec3 — silent corruption or misreads with no crash to flag
it. This is exactly the class of bug that is expensive to diagnose after
the fact (it reproduces only with specific link orders or optimization
levels) and cheap to catch here, before the build ships.
Safe redesign¶
Make the type's definition identical in every TU: guard the divergent member with the same macro everywhere and compile every TU with that macro consistently, or move the type to a single header all TUs include unconditionally so there is only ever one definition to replay.
Cross-tool comparison¶
odr_type_variant is a cross-source check unique to abicheck's audit mode
— it compares two translation units' own replayed layouts for the same
type against each other, which isn't something abidiff/abi-compliance-checker
do (they diff two whole-binary ABI dumps against each other, not two TUs'
source-replayed definitions within one build).
Source files¶
snapshot.abi.json
See also: Compatibility Catalog · All API_BREAK cases · Category: API Break · Subject: Export/declaration mismatches.