Case 145: Unversioned Export Under a Versioning Scheme (Audit, Pure L0)¶
| Field | Value |
|---|---|
| Verdict | 🟢 COMPATIBLE |
| Category | Quality (Compatible) |
| Classification | Rule · audit |
| Platforms | Linux |
| Flags | Bad practice |
Detected ChangeKinds |
unversioned_exported_symbol |
| Source files | catalog/cases/case145_audit_unversioned_export/ |
| Rule family | audit-unversioned-export |
| Subject | Symbol versioning and kABI |
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: the library defines a symbol-versioning
scheme (DEMO_1.0 in .gnu.version_d) for demo_init/demo_run, yet a
third export, demo_experimental, ships with no version node at all. Once
consumers link against the bare (unversioned) demo_experimental symbol, the
library has no way to later ship an incompatible demo_experimental under a
new version node the way it can for the versioned symbols — the usual "add
DEMO_2.0, keep DEMO_1.0 for old binaries" escape hatch doesn't exist for a
symbol that was never versioned in the first place.
What this snapshot contains¶
snapshot.abi.json is a single, hand-built AbiSnapshot for one build of
libdemo.so. This case is pure L0 — everything the finding needs is in the
ELF export table's own version metadata, nothing from DWARF or headers:
| Source in the snapshot | What it records |
|---|---|
Binary export table (L0, elf.symbols[].version) |
demo_init → "DEMO_1.0", demo_run → "DEMO_1.0", demo_experimental → null (no version) |
Version-definition scheme (L0, elf.versions_defined) |
["DEMO_1.0"] — the library does define a scheme |
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 |
| --- | --- | --- | --- | --- |
| `unversioned_exported_symbol` | `demo_experimental` | potential_breaking | present in this build | Symbol 'demo_experimental' is exported with no version node even though the library defines a versioning scheme (1 version(s)). Add it to the version script so it can be evolved compatibly — or hide it if it is not public API. |
Minimum evidence¶
min_evidence: L0 — both facts the check needs (the export table and its
per-symbol version strings, plus the set of defined version nodes) live
entirely in the ELF .dynsym/.gnu.version_d data. No DWARF, no headers,
no previous release required.
Why abicheck catches it¶
unversioned_exported_symbol is a cross-source check within a single
artifact: abicheck compares the exported-symbol set against the
version-definition scheme recorded in the same binary (binary_exports is
the sole provider — no second artifact needed, per provider_assertions).
A library that defines any version scheme is expected to version every
export consistently; a symbol sitting outside that scheme is what the check
flags. Reading the export table alone (without also reading the version
metadata attached to each symbol) can't tell "no versioning used" from
"versioning used inconsistently" — the check has to look at both.
Why this matters for a real release¶
Symbol versioning exists precisely so a library can ship an ABI-incompatible
change to a symbol while keeping old binaries working against the old
version node. An export that slips through unversioned trades away that
option the moment a consumer links against it — the next incompatible
change to demo_experimental has no compatible path back to old callers,
unlike demo_init/demo_run, which do.
Safe redesign¶
Bring the export into the existing scheme before it ships: add
demo_experimental to the version script under a version node (e.g.
DEMO_1.1 { global: demo_experimental; } DEMO_1.0;), or, if it isn't meant
to be public API yet, move it to the script's local: block so it isn't
exported at all.
Cross-tool comparison¶
unversioned_exported_symbol is a cross-source check unique to abicheck's
audit mode — it reconciles two views of the same binary (its export table
and its version-definition scheme) rather than diffing two releases, which
isn't something abidiff/abi-compliance-checker do.
Source files¶
snapshot.abi.json
See also: Compatibility Catalog · All COMPATIBLE cases · Category: Quality (Compatible) · Rule: Exported symbol carries no version · Subject: Symbol versioning and kABI.