Case 84: Multi-Library Bundle SONAME Skew¶
| Field | Value |
|---|---|
| Verdict | 🔴 BREAKING |
| Category | Bundle (Multi-library) |
| Classification | Scenario — Multi-library project topology |
| Platforms | Linux |
| Flags | ABI break, Bad practice |
Detected ChangeKinds |
bundle_soname_skew |
| Source files | catalog/cases/case84_bundle_soname_skew/ |
| Related rules | soname-inconsistent |
| Subject | Multi-library bundle topology |
Category: Bundle / cross-artifact ABI | Verdict: 🔴 BREAKING
Verdict and consumer impact¶
oneDAL-style toolkits ship several .sos that are meant to move in lockstep —
one release, one SONAME bump across the whole set. case84 encodes a release
where libonedal_core and libonedal_dpc bump .so.1 → .so.2 but
libonedal_thread is accidentally left at .so.1. Each of the three
libraries passes an individual ABI check — nothing inside any one file is
wrong. The cohort as a whole is broken: a distro package built from this
release records Depends: libonedal-core2, libonedal-thread1, an internally
inconsistent dependency set, and a process that links the new core/dpc
against the still-.so.1 thread mixes binaries built against different
internal contracts.
Old/new diff¶
No source changes — only the linked SONAME differs per library:
| Library | v1 SONAME | v2 SONAME |
|---|---|---|
libonedal_core.so |
.so.1 |
.so.2 |
libonedal_dpc.so |
.so.1 |
.so.2 |
libonedal_thread.so |
.so.1 |
.so.1 (unbumped — the skew) |
abicheck command¶
bash examples/case84_bundle_soname_skew/gen_bundle.sh
printf 'bundle:\n cohorts: ["libonedal_"]\n' > /tmp/case84.abicheck.yml
abicheck compare examples/case84_bundle_soname_skew/v1 examples/case84_bundle_soname_skew/v2 \
--config /tmp/case84.abicheck.yml -o json=-
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
Per-library (each passes in isolation):
- libonedal_core.so.1 -> COMPATIBLE_WITH_RISK (soname_changed: .so.1 -> .so.2)
- libonedal_dpc.so.1 -> COMPATIBLE_WITH_RISK (soname_changed: .so.1 -> .so.2)
- libonedal_thread.so.1 -> NO_CHANGE
Bundle (cross-library) findings:
- bundle_soname_skew: Bundle SONAME skew: 2 of 3 cohort members bumped
major SONAME but 1 did not. Bumped: libonedal_core.so.1 -> libonedal_core.so.2,
libonedal_dpc.so.1 -> libonedal_dpc.so.2. Lagging: libonedal_thread.so.1.
> Distro packages built on this set carry inconsistent dependency
metadata; mixed loads can corrupt internal cross-library state.
.abicheck.yml's bundle.cohorts: ["libonedal_"] is required:
SONAME-skew detection is opt-in. You declare which libraries are
co-versioned by name prefix; without it abicheck never infers a lockstep
invariant from filenames, so an ordinary release that bumps one
independent library while another (unrelated) one stays put is not
flagged.
Minimum evidence¶
min_evidence: L0 — each library's SONAME (DT_SONAME in the ELF dynamic
section) is authoritative binary-only evidence. No debug info or headers are
needed to read it; gen_bundle.sh doesn't even pass -g.
Why abicheck catches it¶
No individual artifact's DT_SONAME is "wrong" — libonedal_thread.so.1
is a perfectly valid SONAME on its own, and a plain pairwise compare of
libonedal_thread.so.1 old vs. new reports NO_CHANGE because the file is
untouched. The skew is a property of the set. abicheck compare on two
directories builds a bundle snapshot of the whole release and, once a
cohort prefix is declared, abicheck/bundle.py's _detect_soname_skew
extracts every declared member's major SONAME from both releases and emits
bundle_soname_skew when the cohort has mixed deltas — some bumped, one
not.
Runtime failure demonstration¶
Severity: BREAKING (release-engineering / packaging failure)
This is a directory-level failure, not a single-process crash, so there is
no app.c to swap a library under. The real-world failure mode: a
packaging script matches installed files by name, not by declared cohort.
It ships libonedal_core.so.2 and libonedal_dpc.so.2 from the new
release but leaves the previously-installed libonedal_thread.so.1 in
place (nothing told it to remove or replace a file whose name didn't
change). A process now loads core/dpc built against the v2 internal
contract alongside a thread still built against v1's. The dynamic linker
resolves every symbol it's asked for — there is no undefined symbol
error to catch the mistake at load time. The mismatch only surfaces as
corrupted shared state deep inside a compute kernel, far from the
packaging bug that caused it.
Safe redesign¶
Treat a declared cohort's SONAME bump as one atomic release action: a
packaging/CI gate should refuse to publish if any member of a declared
cohort is missing the version bump the rest of the set received.
abicheck compare --config .abicheck.yml with a declared bundle.cohorts:
is exactly that gate — wire it into the release pipeline so a skew like
this fails CI instead of shipping.
Real-world example: projects that ship several co-versioned .sos from
one build (oneDAL's libonedal_* set, ffmpeg's libavcodec/libavutil/
libavformat) rely on every member bumping together; a partial bump in a
downstream repackage is a recurring distro-packaging bug class.
Cross-tool comparison¶
abidiff/abi-compliance-checker compare one library pair at a time —
neither tool has a cohort concept. Run against libonedal_thread.so.1 old
vs. new alone, either would correctly report "no ABI change" (the file is
byte-for-byte identical here), which is right for that one artifact and
is exactly why this class of bug — every artifact individually clean,
cohort mismatched — is structurally invisible to per-library ABI diffing.
Catching it requires directory/cohort-aware tooling, not a stronger
per-file diff.
Source files¶
gen_bundle.shonedal_core.conedal_dpc.conedal_thread.c
See also: Compatibility Catalog · All BREAKING cases · Category: Bundle (Multi-library) · Subject: Multi-library bundle topology.