Case 18 โ Dependency ABI Leak¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break, Bad practice |
Detected ChangeKinds |
โ |
| Source files | browse on GitHub |
Verdict: ๐ด BREAKING (project policy + binary layout risk)
Compatibility classification¶
- Binary ABI impact: BREAKING when consumers and library are compiled against different third-party layout versions.
- Source compatibility impact: May compile, but behavior is unsafe if transitive type size changed.
- Runtime behavior impact: High risk of OOB reads/writes or corrupted interpretation.
- Policy severity: BREAKING in
ground_truth.jsonbecause public ABI leaks third-party layout.
What changes¶
libfoo's exported symbol interface is identical between v1 and v2 โ same function
names, same signatures. The breaking change is in ThirdPartyHandle, a type from a third-party library
that libfoo exposes in its public header.
| Version | ThirdPartyHandle layout |
|---|---|
| v1 | { int x; } โ sizeof = 4 bytes |
| v2 | { int x; int y; } โ sizeof = 8 bytes |
What breaks at binary level¶
A caller compiled with v1 headers allocates ThirdPartyHandle h = {42} โ 4 bytes.
It passes &h to process(). The v2 .so was compiled with the new layout and
may access h->y (at offset 4) โ reading garbage or unmapped memory.
Even if libfoo itself doesn't access y, the caller's sizeof(ThirdPartyHandle)
mismatch means array indexing is wrong:
libfoo's exported symbol table looks identical in both scenarios. nm, readelf,
and naive abidiff see the same function names and signatures. Only the headers changed.
nm, readelf, and naive abidiff see no difference in the .so.
Why abidiff may catch it (with DWARF)¶
If libfoo is compiled with -g, DWARF records ThirdPartyHandle's layout.
abidiff comparing v1 and v2 .so files would detect the type size change โ but
only if the third-party type is transitively included in the DWARF of libfoo.so.
Many distributions strip debug info, making this invisible to abidiff.
Why ABICC catches it¶
ABICC processes all headers transitively, including thirdparty.h. It computes AST
diffs across the entire header graph. When ThirdPartyHandle changes size, ABICC
reports it as a type layout change that affects the ABI of libfoo's exported functions.
Real-world example¶
Some large libraries include tbb::task_arena (a TBB type) in their public headers.
When TBB changed task_arena's internal layout in TBB 2021.3, those libraries' ABI
broke for users who had TBB 2021.2 installed. The .so files hadn't changed.
Protocol Buffers (protobuf): Several gRPC components include grpc::Status which
internally holds std::string. When libstdc++ changed std::string ABI (GCC 5.x,
CXX11 ABI), all libraries exposing grpc::Status in their public headers broke
silently. This is why Abseil and gRPC now use opaque handle types.
Best practice¶
Never expose implementation-detail types in your public API headers.
Use the Pimpl idiom or opaque handles:
/* BAD: ThirdPartyHandle leaks into public API */
void process(ThirdPartyHandle* h);
/* GOOD: opaque handle โ caller never sees ThirdPartyHandle */
typedef struct foo_handle foo_handle_t;
foo_handle_t* foo_create(int x);
void foo_process(foo_handle_t* h);
void foo_destroy(foo_handle_t* h);
See also the Dependency ABI Leaks section in examples/README.md.
Code diff¶
-/* thirdparty.h v1 */
-typedef struct { int x; } ThirdPartyHandle;
+/* thirdparty.h v2 */
+typedef struct { int x; int y; } ThirdPartyHandle; /* struct grew */
/* foo.h โ unchanged source */
void process(ThirdPartyHandle* h);
Reproduce steps¶
cd examples/case18_dependency_leak
# Build libfoo v1 and v2 (exported ABI/symbol surface is unchanged; only thirdparty headers differ)
gcc -shared -fPIC -g libfoo_v1.c -I. -o libfoo_v1.so
gcc -shared -fPIC -g libfoo_v2.c -I. -o libfoo_v2.so
# Compare the .so files โ they look identical at symbol level
nm --dynamic libfoo_v1.so
nm --dynamic libfoo_v2.so
diff <(nm --dynamic libfoo_v1.so) <(nm --dynamic libfoo_v2.so) || true
# abidiff WITH DWARF may catch ThirdPartyHandle layout change
abidw --out-file foo_v1.xml libfoo_v1.so
abidw --out-file foo_v2.xml libfoo_v2.so
abidiff foo_v1.xml foo_v2.xml || true
# ABICC catches via transitive header AST diff
abi-compliance-checker -lib libfoo -v1 1.0 -v2 2.0 \
-header foo_v1.h -header foo_v2.h \
-include-path . \
-gcc-options "-I."
Real Failure Demo¶
Severity: CRITICAL
Scenario: app allocates ThirdPartyHandle with v1 layout (4 bytes), calls process() from v2 which reads h->y at offset 4 โ uninitialized memory.
# Build libfoo v1 + app
gcc -shared -fPIC -g libfoo_v1.c -I. -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ sizeof(ThirdPartyHandle) = 4
# โ before process: h.x=42 canary=0x5AFE5AFE
# โ process: x=42
# โ after process: h.x=42 canary=0x5AFE5AFE
# Swap in libfoo v2 (writes to h->y = offset 4, which is canary's location)
gcc -shared -fPIC -g libfoo_v2.c -I. -o libfoo.so
./app
# โ sizeof(ThirdPartyHandle) = 4
# โ before process: h.x=42 canary=0x5AFE5AFE
# โ process: x=42
# โ process: wrote y=0xBADC0DE at offset 4
# โ after process: h.x=42 canary=0x0BADC0DE โ CORRUPTED by v2!
# โ CORRUPTION: v2 read/wrote past ThirdPartyHandle boundary!
Why CRITICAL: The library's exported symbol table looks identical in both scenarios โ
nm and readelf show the same function names. Yet the v2 library reads 4 bytes past the
caller's struct allocation because the third-party type it exposes in its public header
grew silently. Heap corruption or information leakage follows.
Why runtime result may differ from verdict¶
Header dependency leak: binary compat, recompile fails
References¶
- libstdc++ dual ABI notes (
std::stringABI split) - Protobuf compatibility guidance (wire/source evolution pitfalls)
- Pimpl / compilation firewall rationale (opaque ABI boundary)
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.