Skip to content

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.sh
  • onedal_core.c
  • onedal_dpc.c
  • onedal_thread.c

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