2026-09-08
Semantic versioning is a promise. Shared-library ABIs are the reality. If you've ever bumped a package from 1.4.2 to 1.4.3, run ldconfig, and watched half of userspace segfault, you already know the mainstream answer — "compare headers, cross your fingers, run the tests" — misses the vast space of breakages that don't touch a single line of source: struct padding shifts because someone added a member in the middle, enum values renumber because a new constant landed at the top, a virtual method reorders and vtables slide by eight bytes.
libabigail is Red Hat's toolkit for this. Its flagship tool abidiff reads DWARF debug info from two ELF binaries and reports, at the actual ABI level, what changed and whether it's a compatibility break. You do not need source. You need debuginfo.
$ abidiff libfoo.so.1.4.2 libfoo.so.1.4.3
Functions changes summary: 0 Removed, 1 Changed (2 filtered out), 3 Added
Variables changes summary: 0 Removed, 0 Changed, 0 Added
1 function with some indirect sub-type change:
[C] 'function int foo_process(struct foo_ctx*, size_t)' at foo.c:142:1:
parameter 1 of type 'struct foo_ctx*' has sub-type changes:
in referenced type 'struct foo_ctx':
type size changed from 384 to 448 (in bits)
1 data member insertion:
'uint64_t retry_count', at offset 320 (in bits) at foo.h:87:1
That size change means every binary that stack-allocated a foo_ctx will now corrupt its stack. No compiler warning. No test failure. Just weird crashes six months later.
Package-level comparison. abipkgdiff eats two RPMs plus their debuginfo packages and reports every ABI change across every shared library in the package:
$ abipkgdiff --d1 glibc-2.38-1.debuginfo.rpm --d2 glibc-2.39-1.debuginfo.rpm \
glibc-2.38-1.rpm glibc-2.39-1.rpm
This is how Fedora and openSUSE catch accidental ABI breaks before the update lands in a repo.
Suppressions. Not every "change" is a break. A struct with a void *_reserved[4] that gets replaced by real members was intentional padding. libabigail reads INI-format suppression files:
[suppress_type]
name = foo_internal_ctx
; opaque handle — never inspected by callers
Serialize ABIs to XML. abidw writes a full ABI description as XML, so you can commit a reference ABI to git and have CI run abidiff against it on every PR. Now your ABI is code-reviewed like everything else:
$ abidw --out-file libfoo-abi.xml libfoo.so.1
$ abidiff libfoo-abi.xml build/libfoo.so.1
abicompat: given an application binary and two versions of a library, will the app still work against the new one? It reads the app's undefined symbols and struct usage, then checks if the new library still satisfies them:
$ abicompat myapp libfoo.so.1.4.2 libfoo.so.1.4.3
ELF file 'myapp' might not be ABI compatible with 'libfoo.so.1.4.3'...
Compare that to ldd-plus-launch-and-pray. abidiff makes the invisible visible: struct layout, enum values, vtable order, symbol versions, function signature drift, all read straight out of the DWARF the compiler already emitted for you.
abidiff reads DWARF from two ELF binaries and tells you what actually changed at the ABI level, before your users' segfaults do it for you.
