Skip to content

Case 13: Symbol Versioning Script

Field Value
Verdict 🟢 COMPATIBLE
Category Quality (Compatible)
Classification Rule
Platforms Linux
Flags —
Detected ChangeKinds symbol_version_defined_added
Source files catalog/cases/case13_symbol_versioning/
Rule family symbol-versioning
Subject Symbol versioning and kABI

Category: ELF/Linker | Verdict: ✅ COMPATIBLE

Verdict and consumer impact

Nothing breaks: a binary already linked against unversioned foo/bar (old) keeps running unmodified when the library adds GNU symbol versioning (new). ld.so resolves the old, unversioned symbol references against the new foo@@LIBFOO_1.0/bar@@LIBFOO_1.0 definitions transparently — no DT_VERNEED entry means the loader doesn't require a specific version, so it accepts the newest one. This is the safe direction to add symbol versioning to a previously-unversioned library.

Old/new diff

old/lib.c new/lib.c
int foo(void) { return 0; } int foo(void) { return 0; }
int bar(void) { return 1; } int bar(void) { return 1; }

Identical source. The only difference is new's link step adds a version script, libfoo.map:

LIBFOO_1.0 {
  global: foo; bar;
  local: *;
};

abicheck command

gcc -shared -fPIC -g old/lib.c -o libfoo_v1.so
gcc -shared -fPIC -g new/lib.c -o libfoo_v2.so -Wl,--version-script=libfoo.map
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: COMPATIBLE (exit 0)

Quality Issues:
- symbol_version_defined_added: Symbol version definition added: LIBFOO_1.0

Minimum evidence

min_evidence: L0 — version definitions live in the ELF .gnu.version_d section, readable straight from the binary alongside the dynamic symbol table. No debug info or headers needed.

Why abicheck catches it

abicheck reads each library's .gnu.version_d section as part of its L0 ELF metadata pass (skipping the VER_FLG_BASE self-entry) and diffs the sets of version names between old and new. LIBFOO_1.0 present only on the new side is classified symbol_version_defined_added and placed in the compatible/quality bucket, not the breaking one — going the other direction (a version definition removed, which breaks any consumer whose DT_VERNEED requires it) is a separate, breaking ChangeKind.

Runtime failure demonstration

Severity: INFORMATIONAL — no observable effect on existing binaries.

Scenario: compile app against old (unversioned), swap in new (versioned) .so without recompile.

# Build old library + app
gcc -shared -fPIC -g old/lib.c -o libfoo.so
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# → foo() = 0
# → bar() = 1

# Swap in new library (no recompile)
gcc -shared -fPIC -g new/lib.c -o libfoo.so -Wl,--version-script=libfoo.map
./app
# → foo() = 0   (same — no breakage)
# → bar() = 1

nm -D libfoo.so | grep -E 'foo|bar'
# → 0000000000001108 T bar@@LIBFOO_1.0
# → 00000000000010f9 T foo@@LIBFOO_1.0

Why INFORMATIONAL: app was linked against unversioned symbols, so its ELF has no DT_VERNEED entry requiring LIBFOO_1.0; the dynamic linker resolves foo/bar against the new library's default (@@) versioned definitions without complaint. The opposite direction — a library going from versioned to unversioned, dropping a version consumers already require — is breaking (symbol_version_defined_removed), because dlvsym() lookups pinned to that version would then fail.

Safe redesign

No fix needed — adding a version script to a previously-unversioned library is the correct way to start managing symbol versions going forward. Keep existing symbols in the first version node (as done here) so already-built consumers keep resolving against them; only later API changes need a new LIBFOO_2.0 node.

Real-world example: glibc and most core system libraries ship symbol versioning so multiple ABI generations of the same function (e.g. realpath@GLIBC_2.2.5 vs. realpath@GLIBC_2.3) can coexist in one .so.

Cross-tool comparison

readelf --version-info is the lower-level way to inspect the same fact:

readelf --version-info libfoo_v1.so
# → No version information found in this file.

readelf --version-info libfoo_v2.so
# → Version definition section '.gnu.version_d' contains 2 entries:
#     Index: 1  Cnt: 1  Name: libfoo_v2.so     ← skipped (VER_FLG_BASE)
#     Index: 2  Cnt: 1  Name: LIBFOO_1.0       ← recorded as versions_defined

References


Source files

  • new/
  • old/
  • CMakeLists.txt
  • app.c
  • libfoo.map

See also: Compatibility Catalog · All COMPATIBLE cases · Category: Quality (Compatible) · Rule: Symbol versioning introduced · Subject: Symbol versioning and kABI.