Case 139: Symbol Version Node Removed¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux |
| Flags | ABI break |
Detected ChangeKinds |
symbol_version_node_removed |
| Source files | examples/case139_symbol_version_node_removed/ |
Category: Symbol API | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Any downstream binary that links against beta records a dependency on
beta@LIBX_2.0 (the version node it was defined under in v1). v2 deletes
the LIBX_2.0 node entirely and folds beta into LIBX_1.0 โ the symbol
name is still exported, but the version node the old binary's dynamic
loader looks for no longer exists. The process fails at load time with
version 'LIBX_2.0' not found, before main() ever runs. This is the
classic glibc-style versioned-symbol ABI break: removing or renaming a
version node breaks every binary that recorded a dependency on it, even
though nothing about the symbol's name or signature changed.
Old/new diff¶
v1 exports two symbols bound to two version nodes via a linker version script:
v2 deletes the LIBX_2.0 node and folds beta into LIBX_1.0:
abicheck command¶
gcc -shared -fPIC -g v1.c -o libfoo_v1.so -Wl,--version-script=v1.map
gcc -shared -fPIC -g v2.c -o libfoo_v2.so -Wl,--version-script=v2.map
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- symbol_version_node_removed: Version node LIBX_2.0 was entirely removed
from the version script. Symbols previously under this node: beta.
Applications linked against LIBX_2.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.
Also reported (risk):
- symbol_moved_version_node: Symbol beta moved from version node LIBX_2.0
to LIBX_1.0.
Minimum evidence¶
min_evidence: L0 โ the .gnu.version_d/.gnu.version_r ELF sections
carry every version node's name and its member symbols directly; no debug
info or headers needed.
Why abicheck catches it¶
abicheck parses each binary's ELF symbol-version definitions
(.gnu.version_d) into the full set of version nodes and their member
symbols, then diffs the two sets directly โ a node present in v1 and
absent in v2 is reported as removed, independent of whether the symbol
names themselves changed.
Runtime failure demonstration¶
Severity: CRITICAL
Scenario: compile app against v1 (recording beta@LIBX_2.0), swap in
v2 .so without recompile.
# Build old library + app
gcc -shared -fPIC -g v1.c -o libfoo.so -Wl,--version-script=v1.map
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# โ alpha(1) = 2
# โ beta(1) = 3
# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so -Wl,--version-script=v2.map
./app
# โ ./app: ./libfoo.so: version `LIBX_2.0' not found (required by ./app)
Why CRITICAL: the dynamic loader checks every recorded version
dependency before running any application code; LIBX_2.0 no longer
exists in v2, so the process refuses to start at all.
Safe redesign¶
Never remove or rename a published version node. Add new symbols under a
new node (LIBX_3.0) while keeping all existing nodes intact, so old
verneed records keep resolving.
Real-world example: glibc itself follows this discipline strictly โ
GLIBC_2.x version nodes are added, never removed or renamed, precisely so
that decades-old binaries keep resolving their versioned symbols.
Cross-tool comparison¶
abidiff also models symbol-version information (via
elf-symbol::version) and would flag beta's version change; the version
node deletion itself is the sharper signal abicheck reports directly, and
is what actually explains the load-time failure above.
References¶
Source files¶
CMakeLists.txtapp.cv1.cv1.mapv2.cv2.map
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.