Case 90: Bundle — Intra-Bundle Removed Symbol¶
| Field | Value |
|---|---|
| Verdict | 🔴 BREAKING |
| Category | Bundle (Multi-library) |
| Classification | Scenario — Multi-library project topology |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
bundle_intra_dep_removed |
| Source files | catalog/cases/case90_bundle_intra_dep_removed/ |
| Related rules | exported-function-removed |
| Subject | Multi-library bundle topology |
Category: Bundle / cross-library | Verdict: 🔴 BREAKING
(per-library: libalgo.so COMPATIBLE, libcore.so BREAKING)
Verdict and consumer impact¶
libcore.so removes core_mul(). libalgo.so is unchanged — it still
imports core_mul (DT_NEEDED libcore.so.1, undefined core_mul in its
.dynsym). The new bundle no longer exports core_mul from anywhere, so
dlopen("libalgo.so") (or any process that loads it) fails at load time
with undefined symbol: core_mul. A consumer who only re-links against the
new libcore.so and never touches libalgo.so still breaks, because the
break lives in the relationship between the two libraries, not in either
one's own public surface change.
Old/new diff¶
| Library | v1 | v2 |
|---|---|---|
libcore.so |
int core_add(int,int), int core_mul(int,int) |
int core_add(int,int) — core_mul (removed) |
libalgo.so |
calls core_add, core_mul |
unchanged — still calls core_mul |
abicheck command¶
cmake -S examples -B /tmp/abicheck-examples-build -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/abicheck-examples-build \
--target case90_bundle_intra_dep_removed_old_libcore \
case90_bundle_intra_dep_removed_old_libalgo \
case90_bundle_intra_dep_removed_new_libcore \
case90_bundle_intra_dep_removed_new_libalgo
abicheck compare \
/tmp/abicheck-examples-build/case90_bundle_intra_dep_removed/old \
/tmp/abicheck-examples-build/case90_bundle_intra_dep_removed/new \
-o json=-
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
Per-library:
- libalgo.so -> COMPATIBLE (no findings — the file is unchanged)
- libcore.so -> BREAKING
- func_removed: Public function removed: core_mul
Bundle (cross-library) findings:
- bundle_intra_dep_removed: libalgo.so imports core_mul, but no library
in the new bundle exports it.
> Runtime load of libalgo.so will fail with undefined symbol.
Minimum evidence¶
min_evidence: L0 — both halves of this finding are binary-only facts:
core_mul disappearing from libcore.so's exported .dynsym is a plain
symbol-table diff (the same evidence case01's func_removed uses), and
libalgo.so's undefined core_mul import plus its DT_NEEDED
libcore.so.1 entry are read straight from its dynamic section. No debug
info or headers are required — stripping -g from both builds still
produces the removal (as func_removed_elf_only instead of func_removed)
and the same bundle_intra_dep_removed bundle finding.
Why abicheck catches it¶
A pairwise compare of libalgo.so old vs. new alone reports
COMPATIBLE — the file's own exported surface and imports are unchanged.
The break only exists between libraries: libalgo.so imports a symbol that
the sibling providing it just dropped. abicheck compare on two
directories builds a bundle snapshot of the whole release and cross-checks
every library's undefined imports against every other library's
exported symbols in the new release; an import with no remaining
provider anywhere in the bundle is bundle_intra_dep_removed. Per-library
compare has no visibility into that relationship — it only ever sees one
file at a time.
Runtime failure demonstration¶
Severity: BREAKING (cross-library load failure)
There's no single app.c here — the failure is between two library
artifacts, not between an app and one library. The real failure mode:
libalgo.so's own binary never changed and its own ABI check passes
cleanly — the break is entirely a consequence of what libcore.so stopped
providing. Any process that loads both new libraries together, or any
symbol resolution that reaches algo_square() (the function that calls
core_mul), fails at that point.
Safe redesign¶
Never remove a symbol one sibling library still imports without either
removing that sibling too or providing a compatibility shim. Treat the
bundle's internal DT_NEEDED graph as part of the public contract — a
release gate should refuse to publish a bundle where any member's imports
resolve to nothing.
Real-world example: oneDAL's libonedal_thread.so imports symbols
from libonedal_core.so. If a refactor moves or deletes an internal core
symbol that thread still needs, per-library diffing says "thread is
fine, core changed" — exactly the false-negative this bundle layer exists
to close.
Cross-tool comparison¶
abidiff/abi-compliance-checker compare one library pair at a time. Run
against libalgo.so old vs. new alone, either would correctly report "no
ABI change" — the file really is unchanged — which is exactly why this
class of break (nothing in the changed file, everything in what a sibling
stopped exporting) needs bundle/cohort-aware tooling rather than a
stronger per-file diff.
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 |
|---|---|---|
libalgo.so |
🟢 COMPATIBLE | — (no findings) |
libcore.so |
🔴 BREAKING | func_removed |
Source files¶
include/new/old/CMakeLists.txt
See also: Compatibility Catalog · All BREAKING cases · Category: Bundle (Multi-library) · Subject: Multi-library bundle topology.