Skip to content

Case 58: Global Variable Removed

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS
Flags ABI break, API break
Detected ChangeKinds var_removed
Source files examples/case58_var_removed/

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

Verdict and consumer impact

v1 exports two global variables, lib_version and lib_debug_level. v2 makes lib_debug_level static (renamed internally to debug_level), removing it from the export table while get_debug() keeps working through the now-private variable. Any downstream binary that references lib_debug_level directly fails to resolve the symbol at load time โ€” undefined symbol, before main() even runs. Recompilation cannot fix an already-deployed binary.

Old/new diff

bad.c (v1) good.c (v2)
int lib_debug_level = 0; (exported) static int debug_level = 0; (not exported)
int get_debug(void) { return lib_debug_level; } int get_debug(void) { return debug_level; }

abicheck command

gcc -shared -fPIC -g bad.c  -o libfoo_v1.so
gcc -shared -fPIC -g good.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so

Expected abicheck finding

Verdict: BREAKING (exit 4)

- var_removed: Public variable removed: lib_debug_level
  > Old binaries reference a global variable that no longer exists;
    link or load failure.

Minimum evidence

min_evidence: L0 โ€” the exported-symbol table alone is enough: lib_debug_level is a D/B-type symbol in v1's .dynsym and absent from v2's. No debug info or headers needed; -g above is only there so the Runtime failure demonstration below can build a matching app.

Why abicheck catches it

The dynamic symbol table is authoritative L0 evidence for exported data symbols the same way it is for functions โ€” abicheck diffs the exported variable sets directly from .dynsym, no debug info or headers required.

Runtime failure demonstration

Severity: CRITICAL

Scenario: compile app against v1, swap in v2 .so without recompile.

# Build old library + app
gcc -shared -fPIC -g bad.c -o libfoo.so
gcc -g app.c -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ version = 1
# โ†’ debug = 3

# Swap in new library (no recompile)
gcc -shared -fPIC -g good.c -o libfoo.so
./app
# โ†’ ./app: symbol lookup error: ./app: undefined symbol: lib_debug_level

Why CRITICAL: lib_debug_level is removed from the dynamic symbol table in v2; the runtime linker cannot resolve the app's reference to it and the process is killed immediately on startup, before get_debug() is ever called.

Safe redesign

Never make a previously-exported global variable static (or otherwise remove it from the export table) in a minor/patch release. Deprecate it with __attribute__((deprecated)) while still exporting it, or route external access through an accessor function (get_debug_level()) from the start so the underlying storage can move freely without touching the ABI.

Real-world example: libraries that expose mutable global state directly (rather than through getters/setters) routinely get bitten by this when refactoring internal implementation details โ€” the fix upstream is almost always "wrap it in a function and keep the old symbol as a deprecated alias for one release cycle."

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
  • bad.c
  • good.c

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