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¶
References¶
Source files¶
CMakeLists.txtapp.cbad.cgood.c
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.