Skip to content

Case 13: Symbol Versioning Script

Field Value
Verdict ๐ŸŸข COMPATIBLE
Category Quality (Compatible)
Platforms Linux
Flags โ€”
Detected ChangeKinds โ€”
Source files browse on GitHub

Category: ELF/Linker
Verdict: ๐ŸŸข COMPATIBLE โ€” symbol_version_defined_added: LIBFOO_1.0
Direction tested: unversioned (v1) โ†’ versioned (v2)


What changes

Version Build flags Symbol in .dynsym
v1 (old) gcc -shared -fPIC bad.c (no version script) foo, bar
v2 (new) gcc -shared -fPIC good.c -Wl,--version-script=libfoo.map foo@@LIBFOO_1.0, bar@@LIBFOO_1.0

bad.c and good.c are identical source โ€” the only difference is the linker script.

libfoo.map:

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


What abicheck detects

Running abicheck dump + abicheck compare on the compiled .so files:

verdict: COMPATIBLE
changes:
  - symbol_version_defined_added: LIBFOO_1.0

The ELF detector sees LIBFOO_1.0 in v2's .gnu.version_d section but not in v1 โ†’ version definition added. This is a compatible_addition, not a break.


Why it is COMPATIBLE (unversioned โ†’ versioned)

When an existing binary was linked against unversioned foo (v1), its ELF DT_NEEDED has no version requirement (no DT_VERNEED entry for LIBFOO_1.0). Loading such a binary against versioned v2 works normally: ld.so resolves foo โ†’ foo@@LIBFOO_1.0 without complaint.

Runtime demo:

make clean && make

# app linked against v1 (unversioned)
gcc -g app.c -L. -lv1 -Wl,-rpath,. -o app_v1
./app_v1
# foo() = 0
# bar() = 1

# Swap in v2 (versioned) โ€” ld.so resolves transparently
cp libv1.so libv1.so.bak
cp libv2.so libv1.so
./app_v1
# foo() = 0       โ† still works, no warnings
# bar() = 1
mv libv1.so.bak libv1.so

The opposite direction IS breaking

If you go versioned โ†’ unversioned (v1 has @@LIBFOO_1.0, v2 drops the version script), binaries compiled against the versioned v1 embed a DT_VERNEED entry LIBFOO_1.0. Running against unversioned v2:

  • ld.so prints: no version information available (required by ./app)
  • Basic symbol lookup still works (soft match), but:
  • dlvsym(handle, "foo", "LIBFOO_1.0") returns NULL โ€” hard failure
  • Any future LIBFOO_2.0 block becomes impossible to add alongside v1 symbols
  • abicheck reports: symbol_version_defined_removed: LIBFOO_1.0 โ†’ BREAKING

That reverse scenario is a separate test case.


Why two test suites report different verdicts

Test suite How it builds v1 v2 Verdict
test_abi_examples.py Builds via CMake (applies --version-script) bad.c (unversioned) good.c + libfoo.map (versioned) COMPATIBLE
test_example_autodiscovery.py Compiles source files directly without CMake linker flags bad.c good.c (no --version-script!) NO_CHANGE (both lack version sections)

The autodiscovery test fallback compilation does not re-apply linker flags from CMakeLists.txt, so both .so files it compiles lack .gnu.version_d โ†’ no version change detected โ†’ NO_CHANGE. This is a known gap, listed in KNOWN_GAPS in test_example_autodiscovery.py.


ELF inspection

# v1: bare symbols
nm -D libv1.so | grep -E 'foo|bar'
# 0000000000001108 T bar
# 00000000000010f9 T foo

# v2: versioned symbols
nm -D libv2.so | grep -E 'foo|bar|LIBFOO'
# 0000000000000000 A LIBFOO_1.0      โ† version-def aux symbol (SHN_ABS, filtered by abicheck)
# 0000000000001108 T bar@@LIBFOO_1.0
# 00000000000010f9 T foo@@LIBFOO_1.0

# v2: version definition section
readelf --version-info libv2.so
# Version definition section '.gnu.version_d':
#   000000: Rev: 1  Flags: BASE  Index: 1  Cnt: 1  Name: libv2.so   โ† skipped (VER_FLG_BASE)
#   0x001c:  Rev: 1  Flags: none  Index: 2  Cnt: 1  Name: LIBFOO_1.0 โ† recorded as versions_defined

abicheck filters out the LIBFOO_1.0 symbol from .dynsym (it's an SHN_ABS entry with size=0), but correctly captures LIBFOO_1.0 as a version definition from .gnu.version_d (skipping the VER_FLG_BASE entry for libv2.so itself).

References


Source files

See also: Examples overview ยท All COMPATIBLE cases ยท Category: Quality (Compatible).