Skip to content

Function.elf_binding/Variable.elf_binding (and the pre-existing elf_visibility it mirrors) collapse mixed bindings across symbol-versioned aliases sharing one bare name — fixed 2026-10-12 (Codex review, fresh evidence)

Index: Known gaps

Status (closed 2026-10-12): CLOSED. Every ELF reader that compared a binding, visibility, size, COMMON or UNIQUE state, origin, or presence per bare name now pairs symbol-versioned aliases. Nothing under abicheck/ reads ElfMetadata.symbol_map anymore, and tests/test_elf_version_alias_binding.py::test_no_new_code_reader_of_the_collapsed_symbol_map keeps it that way.

What changed. Reproduced first: a two-version library built with .symver (foo@VER1 WEAK, foo@@VER2 GLOBAL in .dynsym order, so the changed alias is the non-last entry) went GLOBAL→WEAK on foo@@VER2, and abicheck compare exited 0 with no finding for foo. - model/elf_facts.py: ElfMetadata.symbol_aliases keeps every versioned entry per bare name. unambiguous_symbol(name) returns the symbol only when all aliases agree on binding and visibility, otherwise None. symbol_map is unchanged, for external callers that want one entry per name. - compare/elf_alias_pairs.py (new): pair_elf_aliases pairs old and new aliases by version tag. One entry per side pairs as before. A 1:1 version rename pairs only when both aliases share is_default. Any other unmatched alias is recorded with record_declined, so it appears in the report's declined list and never vanishes. - Symbol diff (diff_platform_elf_symbols.py): _diff_elf_symbol_metadata pairs aliases for the per-symbol checks; the COMMON check and the gained-GNU_UNIQUE check look at every alias. - Other readers converted (diff_platform.py): _diff_tls_symbols (TLS_VAR_SIZE_CHANGED), _diff_protected_visibility (PROTECTED_VISIBILITY_CHANGED), and the RTTI visibility/version loop in _diff_vtable_identity (VTABLE_SYMBOL_IDENTITY_CHANGED). - Leaked-dependency check (diff_platform_elf_dynamic.py): a removed or added alias is matched by version tag, so a surviving bare name no longer hides a removed SYMBOL_LEAKED_FROM_DEPENDENCY_CHANGED. - Layout thunk check (diff_elf_layout.py): name presence now reads symbol_aliases; the result is identical, and the last symbol_map read in the code is gone. - Declarations: _populate_elf_visibility and backfill_missing_elf_binding take values from unambiguous_symbol. On load, a declaration whose aliases disagree has its binding and visibility cleared, so a snapshot written by an older last-wins writer does not keep the wrong value. - workflows/dump/cache/salt.py bumped to v36, since declaration output changed. A warm cache would otherwise replay the last-alias values.

Verified. tests/test_elf_version_alias_binding.py (38 tests): - A table covers each converted reader (TLS size, protected visibility, COMMON, UNIQUE, RTTI visibility, leaked removal) with the change on the non-last and on the last alias. On the first-fix commit (98314ad) the non-last cases for TLS, protected, COMMON, RTTI and leaked fail; the last-alias controls pass. - Declined leftovers, the is_default rule for renames, and the reader guard fail on 98314ad and pass now. - A real gcc-built library is dumped, saved, loaded through the storage codec, and compared. Alias versions survive the round trip, and foo is reported. - A stored snapshot with a disagreeing declaration comes back with binding and visibility unset, while an agreeing one keeps its value. - Single-version libraries are unchanged, including a version-tag bump on a leaked dependency symbol (name presence still decides); the existing ELF, binding, visibility and diff-platform test files pass (1,813 tests before the table and round-trip additions).

Residual by design. - symbol_map remains for external callers and is documented as one entry per bare name. - A finding still names the bare symbol, so foo: global → weak does not say which alias changed. Carrying the version in a finding is a report-field change and needs its own ADR. - Declines are accounting only and never change a finding. An ambiguous name is declined only by a detector that judged it. - Verdict impact: TLS_VAR_SIZE_CHANGED defaults to BREAKING, so a size change on a hidden alias of a multi-alias library now fails where it was hidden. The other newly reachable kinds (PROTECTED_VISIBILITY_CHANGED, VTABLE_SYMBOL_IDENTITY_CHANGED, SYMBOL_BINDING_BECAME_UNIQUE, SYMBOL_LEAKED_FROM_DEPENDENCY_CHANGED) default to COMPATIBLE_WITH_RISK; COMMON_SYMBOL_RISK, SYMBOL_BINDING_CHANGED and SYMBOL_ELF_VISIBILITY_CHANGED default to COMPATIBLE.

Historical analysis (the state before the fix, kept for context). ElfMetadata.symbol_map was a name -> ElfSymbol dict built from elf.symbols, last-entry-wins; elf_metadata.py's own parser deliberately never embeds a version suffix in ElfSymbol.name (pyelftools doesn't decode @@/@ into the name either — see the _pyelftools_exported_symbols comment), so two legally distinct versioned definitions of one exported name — e.g. a GLOBAL foo@GLIBC_2.2 and a WEAK foo@@GLIBC_2.14 — collapse to a single dict entry, and _populate_elf_visibility (which both fields share) reads only that survivor. A Function/Variable whose mangled matches such a bare name therefore reports whichever binding happened to parse last, not "the" binding — and if that happens to be WEAK while an older, still-live version was GLOBAL, a binding: weak suppression rule (policy/suppression/rules.py) could match a removal that, from the GLOBAL version's perspective, is a real break. The first pass did not fix this: a correct fix needs symbol_map (or a sibling index) to preserve every versioned entry per bare name rather than collapsing to one — a change with several other consumers beyond elf_visibility/elf_binding (dumper_elf_symbols.py's own callers, diff_versioning.py's existing version-aware machinery, which already correlates symbols against versions_defined/versions_required through a different path) that deserves its own scoped design rather than a drive-by change to a cached property several modules depend on today. Per this file's own "known gaps over risky reactive patches" convention.