Case 147: Depth Ladder — the Same Input Answered at Increasing Depth¶
| Field | Value |
|---|---|
| Verdict | 🟢 COMPATIBLE |
| Category | Quality (Compatible) |
| Classification | Scenario — Capability / evidence demonstration · audit |
| Platforms | Linux |
| Flags | Bad practice |
Detected ChangeKinds |
private_header_leak |
| Source files | catalog/cases/case147_scan_depth_ladder/ |
| Related rules | audit-private-header-leak |
| Subject | Export/declaration mismatches |
Category: Quality (Audit) | Verdict: 🟢 COMPATIBLE (bad practice)
Verdict and consumer impact¶
Single-release audit: one build's evidence checked against itself, no
baseline. abicheck reports no verdict at all ("verdict": null): a single build has nothing to be compatible with. (The catalog's 🟢
COMPATIBLE classification above describes the case, not the command's output.)
The audit flags an advisory finding: connect() is a public function that
takes detail::SessionState&, a type declared only in a private header — the
same private_header_leak shape as case144, but this case exists to
demonstrate how much evidence abicheck needed to prove it. abicheck's honest-coverage promise is that a scan says exactly what each depth proved and
what it could not, rather than silently upgrading a hint into a confirmed
finding. This case is the legibility anchor for that promise: the same input,
read at increasing evidence depth.
What this snapshot contains¶
snapshot.abi.json is a single, hand-built AbiSnapshot for one build of
libdemo.so. Unlike case144/146, it carries both the L2 header
provenance and a baked-in L5 source graph, so it represents what a live
--depth source scan would have already collected:
| Source in the snapshot | What it records |
|---|---|
| Binary export table (L0) | _Z7connectv (connect) exported |
| Public-header AST (L2) | connect(detail::SessionState &s); the detail::SessionState struct carries origin: private_header |
| Source graph (L5) | corroborates the reference, adding source_index as a second provider alongside public_header_ast |
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: elf, header
## Candidate-side findings
| Finding | Symbol | Severity | State | Detail |
| --- | --- | --- | --- | --- |
| `private_header_leak` | `_Z7connectv` | potential_breaking | present in this build | Public API 'connect' exposes type 'detail::SessionState', which is declared only in a private (non-installed) header. Consumers including the public header pull in an unshipped declaration. Make the header self-contained or install the leaked header. |
private_header_leak's provider list on this snapshot is
["public_header_ast", "source_index"] (checked with the same
crosscheck_surface() helper tests/test_g20_catalog.py asserts against) —
the source_index entry is what marks this as the L5-corroborated case,
distinct from case144's ["public_header_ast"]-only leak.
Minimum evidence¶
min_evidence: L2 — a public-header AST reference to a private-header type
is already enough to raise the finding; the L5 source graph baked into this
fixture corroborates it with a resolved call/reference edge, it isn't
required to produce the finding in the first place. That's the point of the
ladder: L2 alone already proves the leak here (unlike a case where only a
lexical pattern hints at it without AST confirmation).
Why abicheck catches it — and what depth actually changes here¶
Because this is a committed snapshot fixture, not a live binary/source
tree, the --depth flag controls how much new evidence abicheck would
collect from --sources/--build-info — evidence this fixture doesn't need
collected because it's already baked in. Verified directly against this
file:
abicheck compare --no-baseline snapshot.abi.json --depth headers # exit 0, same finding
abicheck compare --no-baseline snapshot.abi.json --depth binary # exit 0, same finding
abicheck compare --no-baseline snapshot.abi.json # exit 0, same finding
abicheck compare --no-baseline snapshot.abi.json --depth source # exit 0, same finding
All four report the identical private_header_leak row — because the L2
header AST alone already carries the detail::SessionState → private-header
fact, pinning a shallower --depth doesn't hide it here. --depth source
passes too, and that is deliberate rather than an unenforced pin: the
evidence-contract floor (policy/depth_evidence_contract.py) applies to
live extraction only, and this fixture is an already-serialized snapshot
this run never extracted, so there is no "reached a shallower depth than
requested" failure to report for it. Against a live binary the same pin
with no --sources/--build-info exits 7. (Legacy scan errored on this
fixture instead, because it drew no live/stored distinction.)
Against a live binary + real source tree (not a committed fixture), the
ladder plays out as designed: --depth headers gives only the AST-level
hint, and --depth source is what adds the resolved source-graph
corroboration that upgrades it to a confirmed cross-check.
Why this matters for a real release¶
A scan that silently reports "no leak found" because it only had shallow
evidence would be worse than one that says "not checked at this depth" —
the honest-coverage contract is what lets a CI policy decide how much
evidence to require before trusting a COMPATIBLE result. Here, L2 already
proves the leak; a project could still choose to require L5 corroboration
before gating on it, and the coverage report is what makes that choice
possible instead of guesswork.
Safe redesign¶
Same fix as a private-header leak generally: give detail::SessionState an
opaque handle so the public signature no longer names a private-header type,
or install the header that defines it.
Cross-tool comparison¶
private_header_leak is a cross-source check unique to abicheck's audit
mode — it reconciles a public declaration against the provenance of the
type it references, optionally corroborated by a source graph, within the
same build. This depth-ladder framing (evidence tiers, --depth dial,
honest per-layer coverage reporting) has no equivalent in
abidiff/abi-compliance-checker, which only diff two ABI dumps against
each other at a single, fixed evidence level.
Source files¶
snapshot.abi.json
See also: Compatibility Catalog · All COMPATIBLE cases · Category: Quality (Compatible) · Subject: Export/declaration mismatches.