case217 — WEAK export loss demoted to a risk by a binding: weak rule¶
| Field | Value |
|---|---|
| Verdict | 🔴 BREAKING |
| Category | Breaking |
| Classification | Rule |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
func_removed_elf_only |
| Source files | catalog/cases/case217_export_loss_weak_binding_demoted/ |
| Rule family | export-loss-weak-binding-demoted |
| Subject | ELF export and linker metadata |
Verdict and consumer impact¶
NEW drops three WEAK exports that OLD had: a header-declared inline function
(ns::hdr_inline), a WEAK export no public header declares
(ns::weak_undeclared), and the ns::detail::P<float> constructor from an
internal template namespace. Without a policy, abicheck reports them as
BREAKING (func_visibility_changed, func_removed_elf_only) and exits 4.
These are copies the consumer's own translation units emit as well, so the
loss is a risk, not a hard break. The policy in policy.yaml says so with a
binding: weak selector scoped to the library's namespace. With that policy
abicheck reports COMPATIBLE_WITH_RISK and exits 0.
This is the case that the binding: selector did not reach before PRs #1629
and #1630: the findings carried no symbol_binding, so the rule never matched.
Old/new diff¶
| v1 (OLD) | v2 (NEW) | |
|---|---|---|
hdr_inline() |
called from Pub::g(), emitted WEAK |
Pub::g() no longer calls it, not emitted |
weak_undeclared() |
__attribute__((weak)), not in any header |
not defined |
detail::P<float> ctor |
instantiated by Pub::q() |
Pub::q() returns nullptr, not instantiated |
strong_fn() |
GLOBAL, declared | GLOBAL, unchanged |
abicheck command¶
Dump both sides from headers (L2), delete the OLD binary, then compare with the policy:
abicheck dump libv1.so -H v1.h -o v1.json
abicheck dump libv2.so -H v2.h -o v2.json
rm libv1.so
abicheck compare v1.json v2.json --policy policy.yaml
Expected abicheck finding¶
- Verdict:
COMPATIBLE_WITH_RISK, exit code0. - Without
--policy:BREAKING, exit code4. - Kinds (both runs):
func_visibility_changedonhdr_inline,func_removed_elf_onlyonweak_undeclaredand theP<float>constructor. Each carriessymbol_binding: "weak". Under the policy each is reclassified torisk.
Minimum evidence¶
min_evidence: L0: the expected kind (func_removed_elf_only) is detectable
from the exported symbol table alone, and the gate derives the floor from it.
func_visibility_changed on hdr_inline appears only when headers are
supplied; the binary-only run reports the same symbol as func_removed_elf_only.
The walkthrough above still passes -H, because the verdict needs
headers. Without -H the run also reports visibility_leak and
public_surface_shrank, which the binding rule does not cover, so the
verdict stays BREAKING there.
Why abicheck catches it¶
The ELF .dynsym tables of both sides give the removals and their WEAK
binding (elf_metadata.py). The header AST (castxml) tells the detector that
hdr_inline is still declared, so the removal is classified as a visibility
change instead of an API removal. compare/undeclared_exports.py and the
template detectors stamp the old-side binding on each finding
(Change.symbol_binding); the binding: selector matches on that field
(policy/selectors.py, EXPORT_LOSS_KINDS).
Sibling¶
case218_export_loss_strong_binding_stays_breaking uses the same policy.yaml
and the same WEAK losses, plus the removal of a GLOBAL export. Its verdict stays
BREAKING, which shows the selector does not demote strong exports.
Source files¶
policy.yamlv1.cppv1.hv2.cppv2.h
See also: Compatibility Catalog · All BREAKING cases · Category: Breaking · Rule: WEAK export loss demoted by a binding rule · Subject: ELF export and linker metadata.