Case 92: Bundle — Symbol Provider Migration¶
| Field | Value |
|---|---|
| Verdict | 🟡 COMPATIBLE_WITH_RISK |
| Category | Bundle (Multi-library) |
| Classification | Scenario — Multi-library project topology |
| Platforms | Linux |
| Flags | — |
Detected ChangeKinds |
bundle_provider_changed |
| Source files | catalog/cases/case92_bundle_provider_changed/ |
| Related rules | compat-addition, exported-function-removed, symbol-source-owner-changed |
| Subject | Multi-library bundle topology |
Category: Bundle / cross-library | Verdict: ⚠️ COMPATIBLE_WITH_RISK
(bundle-level finding) — combined abicheck compare verdict is 🔴 BREAKING
(exit 4), the worst-of the per-library BREAKING result for libcore.so.
Verdict and consumer impact¶
shared_util moves from libcore.so to libutil.so between releases. The
bundle's total exported-symbol set is unchanged — some library in the
release still exports shared_util — but which library provides it
changed. A consumer whose binary has DT_NEEDED libcore.so only (it linked
-lcore and never touched -lutil) loses the symbol at runtime unless
something else in its dependency chain happens to pull in libutil.so. A
consumer that links both, or already depends on libutil.so transitively,
is unaffected. abicheck's bundle layer recognizes the migration and reports
bundle_provider_changed / COMPATIBLE_WITH_RISK instead of a flat
removal — but libcore.so compared on its own is still BREAKING, and the
combined abicheck compare verdict (worst-of across libraries and bundle)
reflects that.
Old/new diff¶
| Library | v1 exports | v2 exports |
|---|---|---|
libcore.so |
core_add, shared_util |
core_add |
libutil.so |
util_double_add |
util_double_add, shared_util |
abicheck command¶
g++ -shared -fPIC -g old/libcore.cpp -o old/libcore.so
g++ -shared -fPIC -g old/libutil.cpp -o old/libutil.so
g++ -shared -fPIC -g new/libcore.cpp -o new/libcore.so
g++ -shared -fPIC -g new/libutil.cpp -o new/libutil.so
abicheck compare old/ new/ -o markdown=-
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
Bundle: COMPATIBLE_WITH_RISK (1 cross-library finding)
libcore.so -> BREAKING
- func_removed: Public function removed: shared_util
libutil.so -> COMPATIBLE
- (func_added: shared_util)
## 🔗 Bundle (Cross-Library) Findings
- bundle_provider_changed: shared_util (provider: libutil.so)
> Symbol shared_util moved from libcore.so to libutil.so within the
bundle. Downstream consumers with DT_NEEDED on libcore.so only resolve
transitively if their dependency chain reaches libutil.so.
Minimum evidence¶
min_evidence: L0 — each library's exported-symbol table (.dynsym) is
enough for both the per-library func_removed/func_added pair and the
bundle-level bundle_provider_changed correlation. No debug info or
headers needed; -g above is only there so the Runtime failure
demonstration below can build a matching app.
Why abicheck catches it¶
The bundle layer (ADR-023) builds a symbol→provider map across every
library in a release from each .dynsym, then diffs that map between
releases: a symbol whose provider library changed (rather than
disappearing from the release entirely) is bundle_provider_changed —
distinct from the per-library func_removed/func_added pair a single-pair
compare would report on libcore.so/libutil.so in isolation, with no
way to correlate the two.
Runtime failure demonstration¶
Severity: RISK (linkage-dependent)
Scenario: an app links only -lcore (the pre-migration provider) and is
not recompiled against the new release.
# Build old libcore.so + app linked only against -lcore
g++ -shared -fPIC -g old/libcore.cpp -o libcore.so
gcc app.c -L. -lcore -Wl,-rpath,. -o app
./app
# → core_add(2,3) = 5
# → shared_util(5) = 10
# Swap in new libcore.so (shared_util moved out, app not recompiled)
g++ -shared -fPIC -g new/libcore.cpp -o libcore.so
./app
# → ./app: symbol lookup error: ./app: undefined symbol: shared_util
Why RISK, not always BREAKING: the same app relinked against both
-lcore -lutil (or one that already picks up libutil.so transitively)
keeps working unmodified — the symbol still exists somewhere in the
release, just from a different .so. That link-configuration dependency is
exactly what downgrades this from a flat "symbol removed" BREAKING to the
bundle layer's COMPATIBLE_WITH_RISK.
Safe redesign¶
Don't move a public symbol between libraries within one release without keeping a forwarding re-export in the old location (or documenting the move loudly enough that every narrowly-linked consumer gets recompiled). If consolidation across libraries is unavoidable, ship a transition release where the symbol is present in both the old and new provider before removing it from the old one.
Real-world example: oneDAL reorganizes its internal libraries between
major releases (e.g. detail symbols moving from libonedal_core to a new
libonedal_parameters). The exported symbol set is preserved at the bundle
level but DT_NEEDED contracts of narrowly-linked consumers change.
Cross-tool comparison¶
abidiff and abi-compliance-checker compare one library pair at a time —
neither has a bundle-aware, cross-library notion of "symbol moved to a
sibling library in the same release." Run per-library, either would report
exactly the per-library split shown above (func_removed: shared_util for
libcore.so, func_added: shared_util for libutil.so) with no way to
correlate the two into a single provider-migration finding, and no basis
for downgrading the verdict from a flat removal.
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 |
libutil.so |
🟢 COMPATIBLE | func_added |
Source files¶
new/old/CMakeLists.txt
See also: Compatibility Catalog · All COMPATIBLE_WITH_RISK cases · Category: Bundle (Multi-library) · Subject: Multi-library bundle topology.