Skip to content

Case 85: Internal Template Signature Changed

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS
Flags ABI break
Detected ChangeKinds internal_template_leaks_via_public_api, func_removed
Source files examples/case85_internal_template_signature_changed/

Category: Template ABI | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

lib::__detail::walk<T> looks private (it lives in a __detail namespace), but the public inline algorithm lib::sum_range<T> dispatches through it โ€” so every consumer that ever instantiated sum_range<float> has __detail::walk<float> baked into its own symbol table via extern template. In v2 the library's explicit-instantiation set shifts (walk<float> dropped, walk<double> added). A binary built against v1 that calls sum_range<float>(...) fails to resolve walk<float> the moment v2's library is loaded โ€” recompilation is mandatory even though the caller never named __detail::walk directly.

Old/new diff

v1.h v2.h
extern template int walk<int>(int*, int*); extern template int walk<int>(int*, int*);
extern template float walk<float>(float*, float*); extern template double walk<double>(double*, double*);

abicheck command

g++ -shared -fPIC -g -std=c++17 -I. v1.cpp -o libfoo_v1.so
g++ -shared -fPIC -g -std=c++17 -I. v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so \
  --ast-frontend clang -H old=v1.h -H new=v2.h --lang c++

Expected abicheck finding

Verdict: BREAKING (exit 4)

- func_removed: Public function removed: walk
  > Old binaries call a symbol that no longer exists; dynamic linker
    will refuse to load or crash at call site.

- internal_template_leaks_via_public_api: Internal-namespace function
  template 'float lib::__detail::walk(float*, float*)' has changed
  instantiations: removed=['float lib::__detail::walk<float>(float*, float*)'], added=[]
  > An internal-namespace function template changed signature, and its
    instantiations appear in consumer symbol tables because public
    algorithms inline-dispatch through it. Every consumer must rebuild.

Additions:
- func_added: New public function: walk (double lib::__detail::walk<double>(double*, double*))

Minimum evidence

min_evidence: L2 โ€” the exported symbols alone show a plain add/remove pair (walk<float> gone, walk<double> new); distinguishing "an internal template's instantiation set shifted, and it leaks through a public inline algorithm" from an unrelated add+remove needs the header AST to see that __detail::walk is dispatched to from sum_range's inline body. castxml is the documented default header backend; a clang-based AST frontend is a supported alternative that reaches the same evidence.

Why abicheck catches it

The header AST resolves sum_range's inline call graph and confirms it reaches __detail::walk<T>, so a change to __detail's explicit instantiation set is correlated with the public entry point that pulls it into every consumer's symbol table โ€” the function-template analogue of internal_type_leaks_via_public_api.

Runtime failure demonstration

Severity: CRITICAL

Scenario: app instantiates sum_range<float>, which pulls __detail::walk<float> into its own undefined-symbol table via extern template. Swap in v2's library without recompiling.

# Build old library + app
g++ -shared -fPIC -g -std=c++17 -I. v1.cpp -o libfoo.so
g++ -g -std=c++17 -I. app.cpp -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ exit 0 (sum_range<int> and sum_range<float> both resolve)

# Swap in new library (no recompile)
g++ -shared -fPIC -g -std=c++17 -I. v2.cpp -o libfoo.so
./app
# โ†’ ./app: symbol lookup error: ./app: undefined symbol: _ZN3lib8__detail4walkIfEET_PS2_S3_

Why CRITICAL: the app never spelled __detail::walk in its own source โ€” the symbol was pulled in transitively via sum_range<float>'s inline body โ€” so the failure is invisible from reading the consumer's code, only from its compiled symbol table.

Safe redesign

Never let a public inline/template API dispatch to an internal helper via extern template with a instantiation set that can shift between releases. Either instantiate every type the public API supports and freeze that set, or move the helper out-of-line behind a stable, non-template public entry point so its symbol doesn't depend on caller-side instantiation.

Real-world example: the function-template counterpart of PR #238's internal type leak detection โ€” the same failure mode libstdc++/libc++ avoid by keeping internal helper templates either fully static/anonymous or free of caller-visible instantiation churn across releases.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml

Not independently re-verified in this environment (abidiff unavailable here) โ€” see case02's parameter-type-change case for a documented abidiff exit-code comparison.


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v1.h
  • v2.cpp
  • v2.h

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.