Skip to content

unversioned_exported_symbol false-positives on base-version (VER_FLG_BASE) symbols of versioned libraries: the model cannot tell base-bound from unversioned

Index: Known gaps — closed and moot

Status (2026-10-12): CLOSED. Fixed by recording the binding as its own fact, ElfSymbol.version_binding (snapshot schema v58), the "next step" this entry named; the check flags only unversioned. See the closed entry for what landed and the residual it leaves. The text below is the original write-up.

Found once ADR-068 §3 row 3's first slice made checker.compare() run this check automatically: self-comparing a real, canonically GNU-symbol-versioned C library (zlib1g, exercised by .github/workflows/realworld- validation.yml's "Ubuntu package and bundle smoke" job) reports COMPATIBLE_WITH_RISK with one unversioned_exported_symbol finding per base-API function — readelf -V/objdump -T confirm every one of them is legitimately bound to the anonymous/default GNU version node (Base, i.e. the library's own SONAME), the standard, permanent convention for a versioned library's original API: symbols present before the library adopted symbol versioning stay on the base version forever, and retroactively assigning one a named version tag would itself be an ABI break. _check_unversioned_exported_symbol (buildsource/cross_source_checks.py) cannot currently distinguish "bound to the base version" from "genuinely absent from .gnu.version" — both leave ElfSymbol.version == "" (elf_metadata.py's _apply_version_to_symbol intentionally never populates version/is_default for ver_idx < 2, and _build_verdef_index intentionally excludes the base/VER_FLG_BASE entry from ver_index_map), so nothing in the persisted model lets the check tell them apart.

Why not fixed here. A sound fix needs a real distinguishing fact (e.g. a new ElfSymbol field recording "explicitly bound to the base version"), which is a model/+storage/ schema change (AbiSnapshot. SCHEMA_VERSION bump, elf_from_dict/elf_to_dict wiring in snapshot_platform_blocks.py) — real, scoped work, but a materially larger and riskier change than "make an already-migrated check reachable," and touching the shared ElfSymbol.version field's value for the base case (the cheaper-looking alternative) risks silently changing every other detector that already reads version == "" as "unversioned" (symbol-version add/remove detection, OLD/NEW symbol matching by (name, version), and others) — an unbounded blast radius for a targeted fix.

Why it stands regardless. buildsource/cross_source_checks.py's own module docstring states the design tolerance this falls squarely inside: these findings "are never BREAKING on their own... default to RISK or API_BREAK and are advisory/suppressible until a check earns its FP-rate-gate corpus and is promoted" (ADR-035 D4). The smoke-test assertions were widened to accept COMPATIBLE_WITH_RISK alongside NO_CHANGE/COMPATIBLE rather than silently suppressing or re-gating the check — per this repository's standing rule against trading a flag for a real capability (ADR-068 D5). Next step, if picked up: the model change above, or narrowing the check to require some other corroborating evidence of genuine omission (unresearched; a naive ratio/threshold heuristic is explicitly not the fix — see this file's own "attempted twice, reverted twice" discipline for why an unvalidated heuristic is worse than a documented gap).