Case 91: Bundle — Intra-Bundle extern-C Signature Drift¶
| Field | Value |
|---|---|
| Verdict | 🔴 BREAKING |
| Category | Bundle (Multi-library) |
| Classification | Scenario — Multi-library project topology |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
bundle_intra_dep_signature_changed |
| Source files | catalog/cases/case91_bundle_intra_signature_drift/ |
| Related rules | param-type-change, return-type |
| 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 changes core_add(int, int) to core_add(long, long).
Because core_add has C linkage, the mangled symbol name is identical on
both sides — the dynamic linker resolves it without complaint. libalgo.so
is byte-identical between versions: it was compiled once against the v1
declaration and its call sites still push two 32-bit ints. The new
libcore.so's core_add reads 64-bit rdi/rsi, picking up garbage in
the high halves of the registers it wasn't given. There is no load-time
error — only silently wrong results or undefined behaviour at the first
cross-library call, depending on what garbage lands in the unused register
bits.
Old/new diff¶
| Library | v1 | v2 |
|---|---|---|
libcore.so |
extern "C" int core_add(int a, int b) |
extern "C" long core_add(long a, long b) — same mangled name |
libalgo.so |
declares/calls core_add(int,int) |
unchanged — still declares/calls core_add(int,int) |
abicheck command¶
cmake -S examples -B /tmp/abicheck-examples-build -DCMAKE_BUILD_TYPE=Debug
cmake --build /tmp/abicheck-examples-build \
--target case91_bundle_intra_signature_drift_old_libcore \
case91_bundle_intra_signature_drift_old_libalgo \
case91_bundle_intra_signature_drift_new_libcore \
case91_bundle_intra_signature_drift_new_libalgo
abicheck compare \
/tmp/abicheck-examples-build/case91_bundle_intra_signature_drift/old \
/tmp/abicheck-examples-build/case91_bundle_intra_signature_drift/new \
-o json=-
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
Per-library:
- libalgo.so -> COMPATIBLE (no findings — the file is byte-identical)
- libcore.so -> BREAKING
- func_return_changed: Return type changed: core_add
- func_params_changed: Parameters changed: core_add
Bundle (cross-library) findings:
- bundle_intra_dep_signature_changed: libalgo.so calls core_add (mangled
name unchanged) but libcore.so altered its DWARF signature (int -> long int).
> Calling convention is now mismatched.
Minimum evidence¶
ground_truth.json records min_evidence: L0 for this case's bundle
category. In this specific instance the DT_NEEDED/.dynsym import graph
that the bundle layer walks (libalgo.so imports core_add, libcore.so
still exports a symbol of that exact mangled name) is genuinely L0
evidence. But confirming that the signature itself changed needs DWARF:
extern "C" keeps the mangled name identical on both sides, so a
symbols-only comparison sees the same exported name before and after and
has no type information to compare. abicheck/bundle.py's
_detect_intra_dep_signature_changed reads this fact off the underlying
func_params_changed/func_return_changed findings, both of which need
DWARF's DW_TAG_formal_parameter/return-type entries — this repro's
-DCMAKE_BUILD_TYPE=Debug build supplies that (L1). Stripping debug info
from both sides before comparing (verified locally) drops the verdict to
NO_CHANGE: the bundle finding disappears along with the per-library
signature-change findings it's built from.
Why abicheck catches it¶
A pairwise compare of libalgo.so old vs. new alone reports
COMPATIBLE — the file is genuinely unchanged. The bundle layer instead
diffs libcore.so on its own (finding func_params_changed /
func_return_changed on core_add via DWARF) and then cross-references
that against every sibling's imports: libalgo.so imports the same
mangled core_add, so the provider-side signature change is re-emitted as
a bundle-level bundle_intra_dep_signature_changed finding attributing the
break to the consumer (libalgo.so) even though libalgo.so's own binary
never changed.
Runtime failure demonstration¶
Severity: BREAKING (cross-DSO calling-convention mismatch)
There's no single app.c — the mismatch is between two library binaries,
not between an app and one library, and it produces silently wrong values
rather than a crash the OS reports. What actually happens when both new
libraries are loaded together: libalgo.so's call sites for core_add
still emit 32-bit movs into edi/esi (or push 32-bit values); the new
libcore.so's core_add reads the full 64-bit rdi/rsi. Since the
caller only ever wrote the low 32 bits, the upper 32 bits of each register
hold whatever was left over from prior code — core_add's result depends
on that leftover garbage. Unlike case90's undefined-symbol failure, the
dynamic linker is completely satisfied here (same mangled name resolves
fine); the bug only shows up as wrong numbers coming out of algo_sum().
Safe redesign¶
Never change a public extern "C" function's parameter or return types in
place — the mangled name gives callers no signal that anything changed.
Introduce a new symbol name (core_add_l) for the new signature, or bump
the library's SONAME and keep the old core_add(int,int) available under
it, so old callers keep resolving to a function with the calling
convention they were built for.
Real-world example: oneDAL's internal extern "C" shims between
threading and algorithm-kernel layers. A core type-width change (e.g. row
count int32_t → int64_t) silently breaks every algorithm .so compiled
against the old header, exactly this pattern at production scale.
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 byte-identical — which is exactly why
this class of break (the change lives entirely in a sibling library, only
visible by cross-referencing the DT_NEEDED graph) needs bundle-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_params_changed, func_return_changed |
Source files¶
new/old/CMakeLists.txt
See also: Compatibility Catalog · All BREAKING cases · Category: Bundle (Multi-library) · Subject: Multi-library bundle topology.