Skip to content

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.txt
  • app.c
  • v1.c
  • v1.h
  • v2.c
  • v2.h

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