An exported instantiation of a public class template is not in the public contract¶
Status (2026-11-03): OPEN. Needs a contract-semantics ruling (ADR), not a detector fix.
Trigger: catalog case180_symbol_binding_lost_unique compared under the
default --contract public (live or stored): NO_CHANGE, with
symbol_binding_lost_unique recorded and excluded as out of contract. The
same snapshots under --contract all give COMPATIBLE_WITH_RISK. The catalog
runner passes the case only because it compares every case unscoped
(scope.public: false, tests/_legacy_scope.py) unless a case opts in.
The chain, link by link:
- Fixed (2026-11-03). The finding's symbol is a function-local static
(
_ZZN8RegistryI6WidgetE8instanceEvE5value). It is not a declaration, so it is never in a header-derived public symbol set, and the public-surface filter excluded it with the reasonnot-exportedalthough it is in the dynamic symbol table. It is now judged by its enclosing function (model/owner_recovery.local_entity_owner): for a public inline function the local static's binding loss is reported (tests/test_local_static_unique_binding_scope.py; a private-header control stays out). - Open. The enclosing function here is
Registry<Widget>::instance(), an implicit instantiation made in the library's.cpp. Neither header frontend emits an uninstantiated class template, so the public surface holds noRegistry<Widget>::instanceand the owner is itself classifiedexported_not_public, described as an instantiation of an external template althoughRegistryis declared in the library's own public header.
What a fix has to decide: whether an exported instantiation of a class
template declared in a public header, over template arguments that are
themselves public, belongs to the public contract — and with what evidence
(the v55 public_header_identifiers index spells both names). That changes
contract membership for every such instantiation, so it needs an ADR-049
amendment and a measurement against the catalog's deliberately internal cases
(case119/case184 must stay out). Globally switching to --contract all is not
a fix: on the catalog it also moves 118, 119 and 184.
Applicability: Clang never emits STB_GNU_UNIQUE, so the transition does not
exist there (not_applicable_toolchains in ground truth).