Skip to content

Case 180: Symbol Binding Lost GNU_UNIQUE

Field Value
Verdict ๐ŸŸก COMPATIBLE_WITH_RISK
Category Risk
Platforms Linux
Flags Bad practice
Detected ChangeKinds symbol_binding_lost_unique
Source files examples/case180_symbol_binding_lost_unique/

Category: Deployment Risk (ELF/Loader) | Verdict: โš ๏ธ COMPATIBLE_WITH_RISK

Verdict and consumer impact

v1 and v2 share byte-identical source โ€” a Meyers-singleton Registry<Widget>::instance() function-local static inside a class template. Only the build flag differs: v2 adds -fno-gnu-unique. By default GCC marks this vague-linkage guard variable STB_GNU_UNIQUE, a GNU ELF extension where the dynamic linker guarantees exactly one definition is used process-wide, no matter how many DSOs independently instantiate Registry<Widget>. Dropping it falls back to ordinary weak-symbol binding: get_widget_id() still returns the same value from a single DSO, but if the same template is independently instantiated by a second DSO loaded into the same process, each can end up with its own, diverging "singleton" instead of sharing one.

Old/new diff

Source is byte-identical between v1 and v2; only the compile flags differ:

v1 v2
g++ -shared -fPIC v1.cpp -o libv1.so (GNU_UNIQUE, the default) g++ -shared -fPIC -fno-gnu-unique v2.cpp -o libv2.so (ordinary weak binding)

abicheck command

g++ -shared -fPIC -g -O2 v1.cpp -o libfoo_v1.so
g++ -shared -fPIC -g -O2 -fno-gnu-unique v2.cpp -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: COMPATIBLE_WITH_RISK (exit 0)

Deployment Risk Changes:
- symbol_binding_lost_unique: Symbol binding lost GNU_UNIQUE:
  Registry<Widget>::instance()::value โ€” process-wide uniqueness guarantee
  removed

Minimum evidence

min_evidence: L0 โ€” read directly from the ELF symbol table's st_info binding field; no DWARF, no headers.

Toolchain note: clang never emits STB_GNU_UNIQUE for vague-linkage template statics (a GCC ELF extension), so this finding only exists to detect under a GCC-built pair like the one below.

Why abicheck catches it

abicheck reads each defined symbol's ELF binding directly. In v1, Registry<Widget>::instance()::value's guard variable carries STB_GNU_UNIQUE binding; in v2 (built with -fno-gnu-unique) the same symbol carries an ordinary weak/global binding instead. The detector reports the transition without needing to understand what the symbol's type or template instantiation actually is.

Runtime failure demonstration

Severity: informational โ€” no observable effect in this single-DSO demo.

g++ -shared -fPIC -g v1.cpp -o libfoo.so
g++ -g app.cpp -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ get_widget_id() = 42 (expected 42)

The single-DSO demo always prints the same value โ€” that's expected; STB_GNU_UNIQUE's guarantee only becomes observable in a multi-DSO process. To see the actual divergence, build the same v1.cpp twice under two different SONAMEs, dlopen() both into one process, and compare the address of Registry<Widget>::instance() as seen from each: under GNU_UNIQUE binding both DSOs resolve to the same address; under -fno-gnu-unique, glibc version and load order determine whether they coincide or diverge. The artifact fact above โ€” the binding itself changing โ€” is the deterministic, CI-graded proof; the multi-DSO divergence is illustrative, not something this catalog gates CI on.

Safe redesign

Do not build parts of a project with -fno-gnu-unique and other parts without it โ€” a template-heavy library and its plugins should agree on this flag, or singleton-shaped vague-linkage statics can silently diverge across the boundary. If a true process-wide singleton is required regardless of DSO topology, consider an explicit, named exported symbol instead of relying on vague-linkage template statics.

Cross-tool comparison

readelf -s libfoo_v1.so | grep -i UNIQUE
readelf -s libfoo_v2.so | grep -i UNIQUE
libfoo_v1.so:
     5: 0000000000004008     4 OBJECT  UNIQUE DEFAULT   19 _ZZN8RegistryI6W[...]
libfoo_v2.so:
(no matches โ€” ordinary weak binding, no UNIQUE entries)

abidiff/ABICC do not model ELF symbol-binding transitions at all, so this class of finding is abicheck-specific; readelf confirms the raw difference abicheck's symbol_binding_lost_unique detector reads.

References


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v1.hpp
  • v2.cpp
  • v2.hpp

See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.