Skip to content

Case 65: Symbol Version Removed

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux
Flags ABI break
Detected ChangeKinds symbol_version_defined_removed
Source files browse on GitHub

Category: Symbol Versioning | Verdict: BREAKING

What breaks

The CRYPTO_1.0 symbol version node is removed from v2 of the library. Old binaries that were linked against crypto_hash@CRYPTO_1.0 record that version requirement in their .gnu.version_r section. When the dynamic linker tries to load v2 (which only provides CRYPTO_2.0), it cannot satisfy the CRYPTO_1.0 requirement and refuses to start the process.

Why this matters

ELF symbol versioning (via .gnu.version_d / .gnu.version_r sections and version scripts) is the primary mechanism for maintaining backward compatibility in shared libraries across major-version boundaries. It allows a single .so to provide multiple implementations of the same symbol for different ABI generations.

Removing a version node is equivalent to removing symbols โ€” but worse, because: - The version was an explicit promise of backward compatibility - Tools like nm or readelf -s still show crypto_hash in the symbol table, so the breakage is invisible to naive checks - The failure happens at load time, not link time โ€” binaries that appeared to link correctly fail only when deployed against the new library

Code diff

v1.c:  .symver crypto_hash_v1,crypto_hash@CRYPTO_1.0   โ† compat version
       .symver crypto_hash_v2,crypto_hash@@CRYPTO_2.0   โ† default version
v1.map: CRYPTO_1.0 { crypto_hash; };
        CRYPTO_2.0 { crypto_hash; crypto_verify; } CRYPTO_1.0;

v2.c:  (no .symver โ€” plain crypto_hash() definition)    โ† CRYPTO_1.0 gone!
v2.map: CRYPTO_2.0 { crypto_hash; crypto_verify; };

Real Failure Demo

Severity: CRITICAL

Scenario: compile app against v1 (links to crypto_hash@CRYPTO_1.0), swap in v2 .so which removed the CRYPTO_1.0 version node.

# Build v1 library with both version nodes
gcc -shared -fPIC -g v1.c -Wl,--version-script=v1.map -o libcrypto.so
gcc -g app.c -L. -lcrypto -Wl,-rpath,. -o app
./app
# โ†’ hash("hello") = 99162322
# โ†’ OK: crypto_hash@CRYPTO_1.0 resolved successfully

# Check version requirement recorded in the binary
readelf -V app | grep CRYPTO
# โ†’ Name: CRYPTO_1.0  Flags: none  Version: 4

# Swap in v2 library (CRYPTO_1.0 removed)
gcc -shared -fPIC -g v2.c -Wl,--version-script=v2.map -o libcrypto.so
./app
# โ†’ ./app: ./libcrypto.so: version `CRYPTO_1.0' not found (required by ./app)

Why CRITICAL: The dynamic linker's version check is strict โ€” if the required version node doesn't exist in the loaded library, the process is killed immediately. This is a hard failure with a clear error message, but it still catches many library maintainers by surprise when they "clean up" old version nodes.

How to fix

Never remove a symbol version node from a shared library. Instead:

  1. Keep the old version node: even if the implementation is identical, retain the .symver alias so old binaries still resolve
  2. Forward old versions: __asm__(".symver old_impl,func@OLD_VER") can point the old version to the new implementation
  3. SONAME bump: if you must drop old versions, increment the SONAME major version to force all consumers to re-link

Real-world example

glibc maintains symbol versions going back to GLIBC_2.0 (1997). Removing any version node would break every binary linked against that version โ€” potentially millions of executables across the entire Linux ecosystem. This is why glibc's version script is append-only.

OpenSSL 3.0 removed the OPENSSL_1.0.0 and OPENSSL_1.1.0 version nodes, which is why it required a SONAME change from libssl.so.1.1 to libssl.so.3 โ€” all consumers had to be rebuilt.

abicheck detection

abicheck detects this as symbol_version_defined_removed (BREAKING) by comparing the .gnu.version_d sections of the two library versions.

References


Source files

See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.