Skip to content

Case 65: Symbol Version Removed

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux
Flags ABI break
Detected ChangeKinds symbol_version_node_removed
Source files examples/case65_symbol_version_removed/

Category: Symbol Versioning | Verdict: ๐Ÿ”ด BREAKING

Verdict and consumer impact

The CRYPTO_1.0 symbol-version node is removed from v2 of the library. Old binaries linked against crypto_hash@CRYPTO_1.0 record that version requirement in their .gnu.version_r section. When the dynamic linker loads v2 โ€” which only provides CRYPTO_2.0 โ€” it cannot satisfy the CRYPTO_1.0 requirement and refuses to start the process. Tools like nm or readelf -s still show crypto_hash in the symbol table, so the breakage is invisible to a naive symbol-name check; only the version node itself is gone. Recompilation (and re-linking against whatever version node now provides crypto_hash) is mandatory for affected binaries.

Old/new 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; };

abicheck command

gcc -shared -fPIC -g v1.c -Wl,--version-script=v1.map -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -Wl,--version-script=v2.map -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- symbol_version_node_removed: Version node CRYPTO_1.0 was entirely removed
  from the version script. Symbols previously under this node: crypto_hash.
  Applications linked against CRYPTO_1.0 will get unresolved symbol errors.
  > A version node (e.g. LIBFOO_1.0) was entirely removed from the version
    script. Applications linked against symbols under that version node
    will get unresolved symbol errors at load time.

Additions:
- func_added: New public function: crypto_hash
  > New function available; existing binaries are unaffected.

The func_added finding is an artifact of comparing the plain (unversioned) export name once the version wrapper is gone โ€” the symbol_version_node_removed finding above is the one that carries the real break, and both SYMBOL_VERSION_DEFINED_REMOVED and SYMBOL_VERSION_NODE_REMOVED detect this same removal; abicheck's cross-detector deduplication keeps the more specific symbol_version_node_removed.

Minimum evidence

min_evidence: L0 โ€” the ELF .gnu.version_d section alone records which version nodes a library defines and which symbols belong to each; comparing that section between v1 and v2 is enough, no debug info or headers needed.

Why abicheck catches it

abicheck parses the .gnu.version_d (version definitions) section of both binaries into a node โ†’ symbol-set mapping and diffs the node sets directly โ€” the same class of authoritative, always-present ELF metadata that a plain symbol removal (case01) is detected from, just one section over.

Runtime failure demonstration

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, and app
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.

Safe redesign

Never remove a symbol version node from a shared library. Keep the old .symver alias even once the implementation is identical (__asm__(".symver old_impl,func@OLD_VER") can point the old version at the new implementation), and only drop old version nodes alongside a SONAME major-version bump that forces all consumers to re-link.

Real-world example: glibc maintains symbol versions going back to GLIBC_2.0 (1997) โ€” its version script is append-only, because removing any node would break every binary linked against that version across the entire Linux ecosystem. 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.

Cross-tool comparison

abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml

References


Source files

  • CMakeLists.txt
  • app.c
  • v1.c
  • v1.h
  • v1.map
  • v2.c
  • v2.h
  • v2.map

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