Case 93: Bundle — Instantiation Manifest Drift¶
| Field | Value |
|---|---|
| Verdict | 🔴 BREAKING |
| Category | Bundle (Multi-library) |
| Classification | Scenario — Multi-library project topology |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
bundle_manifest_instantiation_removed |
| Source files | catalog/cases/case93_bundle_manifest_drift/ |
| Related rules | exported-function-removed, missing-template-instantiation |
| Subject | Multi-library bundle topology |
Category: Bundle / manifest | Verdict: 🔴 BREAKING (both with and
without --instantiation-manifest; the manifest upgrades why it's breaking)
Verdict and consumer impact¶
The release ships a single library, libcore.so, with four explicit
template instantiations. train_double_sparse silently disappears between
v1 and v2 (in real oneDAL these would be mangled C++ symbols for
train_ops<Float, Method, Task> triples; plain extern "C" names are used
here so the example is reproducible without a specific demangler).
Downstream code that instantiated the dropped triple fails to link or
resolve the symbol at runtime — recompilation cannot fix a binary that
already calls it. Per-library func_removed already flags the missing
symbol as BREAKING with no extra input. What only the bundle layer's
--instantiation-manifest can add is why it matters: distinguishing "a documented
public promise was broken" from "an internal helper happened to be
visible and got cleaned up," which a bare removed-symbol diff can't tell
apart on its own.
Old/new diff¶
| Symbol | v1 | v2 |
|---|---|---|
train_float_dense |
present | present |
train_float_sparse |
present | present |
train_double_dense |
present | present |
train_double_sparse |
present | (removed) |
manifest.yaml declares all four as promised public instantiations
(docs/contribute/adr/023-bundle-aware-multi-binary-analysis.md).
abicheck command¶
g++ -shared -fPIC -g old/libcore.cpp -o old/libcore.so
g++ -shared -fPIC -g new/libcore.cpp -o new/libcore.so
abicheck compare old/ new/ --instantiation-manifest manifest.yaml -o markdown=-
Expected abicheck finding¶
Without --instantiation-manifest — the per-library break is still caught, just not
attributed to a broken promise:
Verdict: BREAKING (exit 4)
Bundle: NO_CHANGE (0 cross-library findings)
libcore.so -> BREAKING
- func_removed: Public function removed: train_double_sparse
With --instantiation-manifest manifest.yaml:
Verdict: BREAKING (exit 4)
Bundle: BREAKING (1 cross-library finding)
libcore.so -> BREAKING
- func_removed: Public function removed: train_double_sparse
## 🔗 Bundle (Cross-Library) Findings
- bundle_manifest_instantiation_removed: train_double_sparse (provider: libcore.so)
> Manifest promises symbol 'train_double_sparse' but no exported symbol
in the new bundle matches it.
Minimum evidence¶
min_evidence: L0 — the exported-symbol table alone tells abicheck
train_double_sparse is gone from libcore.so's .dynsym; cross-checking
it against manifest.yaml's promised-symbol list needs no debug info or
headers either. -g above is only there so the Runtime failure
demonstration below can build a matching app.
Why abicheck catches it¶
Per-library func_removed detection already flags the missing symbol from
plain .dynsym diffing. The --instantiation-manifest input externalizes the contract:
it lists exactly the symbols the release promises to keep. The bundle
layer then enforces "every manifest entry must be exported by some library
in the new bundle" and emits bundle_manifest_instantiation_removed for
any promised symbol that isn't — the same underlying fact
(func_removed), but classified against a documented public-API contract
instead of inferred from symbol visibility alone.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against v1, swap in v2 libcore.so without
recompile.
# Build old library + app
g++ -shared -fPIC -g old/libcore.cpp -o libcore.so
gcc app.c -L. -lcore -Wl,-rpath,. -o app
./app
# → train_float_dense() = 1
# → train_double_sparse() = 4
# Swap in new library (no recompile)
g++ -shared -fPIC -g new/libcore.cpp -o libcore.so
./app
# → ./app: symbol lookup error: ./app: undefined symbol: train_double_sparse
Why CRITICAL: train_double_sparse is removed from the dynamic symbol
table in v2; the runtime linker cannot resolve the symbol and the process
is killed immediately on startup.
Safe redesign¶
Never drop a documented instantiation from a template-explosion library
without a deprecation cycle — apply the same discipline as removing any
other public symbol. Keep a manifest.yaml-style promised-symbol list in
CI (abicheck compare --instantiation-manifest) so a build-system regression that
silently drops an instantiation fails the release gate instead of shipping
quietly.
Real-world example: oneDAL maintains explicit instantiation lists for
its algorithms (the build-system file enumerates which (Float, Method,
Task) triples are instantiated). Refactors that change these lists are
real ABI changes — but distinguishing "promised" from "incidental" removal
needs the manifest, not just the symbol diff.
Cross-tool comparison¶
abidiff and abi-compliance-checker have no manifest/promised-symbol
concept — run against libcore.so old vs. new, either would report the
same train_double_sparse removal as a plain symbol change, with no way
to distinguish "a documented public promise was broken" from "an internal
helper was cleaned up." That distinction is exactly what
--instantiation-manifest-driven bundle_manifest_instantiation_removed adds on top of
the per-library removal both tools already see.
Per-library expectations¶
The bundle-level verdict above is the cohort's. Each library in the cohort is separately expected to report:
| Library | Verdict | Detected ChangeKinds |
|---|---|---|
libcore.so |
🔴 BREAKING | func_removed |
Source files¶
new/old/CMakeLists.txtmanifest.yaml
See also: Compatibility Catalog · All BREAKING cases · Category: Bundle (Multi-library) · Subject: Multi-library bundle topology.