Case 39: Variable Const Change¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS, Windows |
| Flags | ABI break, API break |
Detected ChangeKinds |
var_became_const |
| Source files | examples/case39_var_const/ |
Category: Global Variable Qualifiers | Verdict: ๐ด BREAKING
Verdict and consumer impact¶
Three independent global-variable changes land in one release:
g_buffer_size becomes const (moves to .rodata), g_max_retries loses
const (moves out of .rodata), and g_legacy_flag is removed entirely.
Old binaries that write to g_buffer_size (legal against v1) get a SIGSEGV
once the page is read-only; binaries that inlined g_max_retries's old
constant value now silently disagree with the library; and anything
referencing g_legacy_flag fails to load at all. Recompilation is
mandatory.
Old/new diff¶
| v1.h | v2.h |
|---|---|
extern int g_buffer_size; |
extern const int g_buffer_size; (became const) |
extern const int g_max_retries; |
extern int g_max_retries; (lost const) |
extern int g_legacy_flag; |
(removed) |
abicheck command¶
gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abicheck compare libfoo_v1.so libfoo_v2.so
Expected abicheck finding¶
Verdict: BREAKING (exit 4)
- var_became_const: Variable became const-qualified: g_buffer_size
(writes now -> SIGSEGV) (non-const -> const)
> Variable moved to read-only section; old code writing to it gets SIGSEGV.
- var_lost_const: Variable lost const qualifier: g_max_retries
(ODR / inlining break) (const -> non-const)
> Variable no longer const; ODR violations possible if old code
inlined the value.
- var_removed: Public variable removed: g_legacy_flag
> Old binaries reference a global variable that no longer exists;
link or load failure.
Deployment Risk:
- exported_object_alignment_reduced: Exported object alignment reduced:
g_max_retries (4096 -> 8 bytes)
Minimum evidence¶
min_evidence: L1 โ DWARF's variable type entries carry the const
qualifier for each global, and the exported-symbol table carries
existence/removal; both are enough to detect all three changes directly
from debug info. No public headers required โ the diff canonicalizes each
side's type string with the const token stripped before comparing, so a
pure const-qualifier flip is caught instead of being read as an unrelated
base-type change.
Why abicheck catches it¶
DWARF's DW_TAG_variable entries carry each global's type, including a
DW_TAG_const_type wrapper when present; abicheck compares the
const-qualification of each global directly from debug info in both
versions, and cross-checks variable existence against the exported-symbol
table for removal.
Runtime failure demonstration¶
Severity: HIGH
Scenario: compile app against v1, swap in v2 .so without recompile.
# Build old library + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ g_buffer_size = 4096
# โ g_max_retries = 3
# โ g_legacy_flag = 1
# โ get_config() = 4096
# โ g_buffer_size after write = 2048
# โ exit 0
# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ ./app: symbol lookup error: ./app: undefined symbol: g_legacy_flag
# โ exit 127
Why HIGH: the removed g_legacy_flag symbol fails resolution before
the app can even reach the g_buffer_size write, so this run demonstrates
immediate load failure. Had g_legacy_flag stayed in place, the app would
have loaded and then SIGSEGV'd on g_buffer_size = 2048; once that global
moved to the now-read-only .rodata section โ both are real ABI breaks
from this one release.
Safe redesign¶
Never change const-qualification on an exported global variable, and never
remove one without a SONAME bump. If a variable needs to become read-only,
add an accessor function (int get_buffer_size(void)) instead and
deprecate the raw global.
Real-world example: libraries that "harden" their public globals by
adding const after the fact (a seemingly safe tightening) break any
consumer that legitimately wrote to the old mutable variable โ the fix
always needs a major version bump, not a patch release.
Cross-tool comparison¶
abidw --out-file v1.xml libv1.so
abidw --out-file v2.xml libv2.so
abidiff v1.xml v2.xml
echo "exit: $?" # โ 12 (detected at binary level)
References¶
Source files¶
CMakeLists.txtapp.cv1.cv1.hv2.cv2.h
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.