Skip to content

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.txt
  • manifest.yaml

See also: Compatibility Catalog · All BREAKING cases · Category: Bundle (Multi-library) · Subject: Multi-library bundle topology.