Skip to content

How System Libraries Stay Compatible

glibc has shipped one SONAME, libc.so.6, since 1997. libstdc++ has shipped libstdc++.so.6 since 2004, through a change to the layout of std::string that would have been a break for any other library. Neither achieved this by never changing; both did it by adopting a discipline — a strategy for evolving the contract that a library of any size can copy. This page is that strategy, placed on a ladder with the others.

The ladder of strategies

Tier of the stack Example Strategy Check it with
kernel ↔ user space syscalls, kABI never break; exported-symbol namespaces and CRCs Kernel BTF/kABI; case175, case176
C runtime glibc one SONAME for decades; every ABI change is a new version node, the old node kept as a compat symbol; append-only --policy glibc_symbol_versioned; case13, case65, case139, case141, case183
C++ runtime libstdc++ same SONAME since 2004; GLIBCXX_3.4.x nodes; the dual ABI as a parallel namespace, not a break case104; Modern C/C++ and Toolchain Hazards
system tooling binutils / ld linker defaults drift between releases and move a library's contract without a source change Environment & Toolchain Drift § binutils; Security Hardening
vendor SDK / product oneDAL, TBB, OpenSSL SONAME bump per major; inline-namespace generations; explicit-instantiation matrix; experimental namespaces Part 7; case99case101; Template- and Header-Heavy Libraries
application plugin host ↔ dlopen required-symbol contract, direction reversed --policy plugin_abi, --required-symbol; Consumer Models

Find your own library's row. Most libraries are the fifth; the discipline of the second and third rows is what the rest of this page explains, because it is the one that makes "one SONAME for decades" possible.

glibc: one SONAME, append-only version nodes

A version script groups exported symbols into named nodes, recorded in the library and referenced by every consumer's own binary (Part 5 § Symbol versioning). glibc's rule is that a node, once shipped, is never removed and never changes: when realpath needed a different behaviour, glibc added realpath@@GLIBC_2.3 and kept realpath@GLIBC_2.0 as a compat symbol that old binaries keep resolving. The whole ABI history is additive, and the SONAME never has to move.

What a consumer records is the highest node it needed: a binary linked against glibc 2.28 carries GLIBC_2.28 in its version requirements, and that requirement is the deployment floor — it loads on any glibc that has the node and refuses on any that does not. The floor is a property of the consumer's build, not its source, which is why a mere relink on a newer host can raise it (Dependency & Runtime Floors).

The cases mark the rules' edges: adding a version script to an unversioned library is compatible, because old binaries carry no requirement (case13); removing a node strands every binary that named it (case65, case139); renaming the scheme wholesale is the same break for the whole library (case141); and churn in a node that only internal symbols use is risk, not a break, because no consumer could have recorded it (case183).

libstdc++: the dual ABI as a parallel namespace

libstdc++ applied the same append-only rule to C++, where the hard case is not a function but a type. C++11 required std::string and std::list to change layout. Instead of a new SONAME, GCC 5 put the new types in an inline namespace, std::__cxx11, so their mangled names differ from the old ones and both sets of symbols coexist in one library; a translation unit chooses with _GLIBCXX_USE_CXX11_ABI. It is Part 7 Pattern 5 at the scale of a whole standard library.

The cost is that the choice became part of every library's contract: a library rebuilt with the other setting exports different mangled names for every function that takes a std::string, and its consumers stop linking. That flip is what the report shows for case104, as a single root-cause finding rather than hundreds of removals; the wider family of toolchain-level hazards is Modern C/C++ and Toolchain Hazards.

binutils and the linker: defaults move the contract

The fourth row is the one nobody designs. A newer binutils flips a linker default — packed relative relocations that need a newer loader, DT_RPATH versus DT_RUNPATH, the hash-table style, CET or static-TLS markings, a distribution's RELRO default — and a library rebuilt with no source change ships a different contract. The detector family and each flag's fix are in Environment & Toolchain Drift § binutils; the hardening flags in Security Hardening. The discipline here is reproducibility: pin the toolchain that builds a release, and compare the release against the previous one rather than against a rebuild.

Adopting the discipline yourself

  • One version node per release that changes the ABI, in the version script (Part 7 Pattern 4): new symbols go in a new node that inherits the old; old nodes are never edited.
  • Change a function's contract by adding a versioned alias, not by editing it: the new implementation becomes the default version and the old one stays exported under its old node (.symver in GNU assembler syntax, or the compiler's symver attribute).
  • Remove only on a SONAME bump. A symbol or node is deleted only when the library's identity changes, so that no binary linked against the old identity can reach the new one. OpenSSL 3.0's node removals forced exactly that bump.
  • For C++, use inline-namespace generations (Part 7 Pattern 5): a new generation is added beside the old, and bumping the generation is itself a deliberate break (case101).
  • Keep an experimental namespace for surface that has not earned a promise: graduating from it is compatible (case99), and removing an experimental declaration without a replacement is still a break for anyone who used it (case100).

Checking it

The built-in policy encodes the glibc rules: a version-node removal is pinned to a break under any base policy, a consumer's added compat version requirement is accepted as the way a rebuilt consumer is expected to evolve, and a dropped DT_NEEDED is surfaced as deployment risk:

abicheck compare libfoo.so.1.old libfoo.so.1.new --policy glibc_symbol_versioned

The one-build audit checks the discipline's own hygiene: an export with no version node in a library that otherwise versions everything is a symbol no consumer can pin (case145):

abicheck compare --no-baseline libfoo.so.1 -H include/

Profile contents and the other ecosystem profiles are owned by Policy Profiles.


Ladder:Template- and Header-Heavy Libraries · Step 8 · At Scale · Dependency & Runtime Floors