Skip to content

Case 30 โ€” Field Qualifier Changes (const, volatile)

Field Value
Verdict ๐Ÿ”ด BREAKING
Category Breaking
Platforms Linux, macOS, Windows
Flags ABI break, API break
Detected ChangeKinds โ€”
Source files browse on GitHub

Category: Type Qualifiers | Verdict: ๐Ÿ”ด BREAKING (policy escalated source break)

Compatibility classification

  • Binary ABI impact: Usually layout-compatible (no size/offset change), but stale optimization assumptions can still break behavior.
  • Source compatibility impact: BREAKING (const write errors, volatile contract changes).
  • Runtime behavior impact: Semantic divergence (stale reads / UB writes) without linker errors.
  • Policy severity: BREAKING in ground_truth.json (source_break category escalated by policy).

What changes

Field v1 v2 Effect
sample_rate int sample_rate const int sample_rate Writing becomes UB
raw_value int raw_value volatile int raw_value Compiler must not cache reads
cache_hits int cache_hits int cache_hits Unchanged

Why this IS a (semantic) ABI break

The binary layout of struct SensorConfig is unchanged โ€” const and volatile do not affect size, alignment, or field offsets. An existing binary will link and run against the v2 library without error.

However, the API contract has changed:

  1. const int sample_rate: Code compiled against v1 freely writes to sample_rate. The v2 header declares this field const, meaning the library now considers it immutable after initialization. Writing to a const-qualified field through a non-const pointer is undefined behavior in C. Compilers recompiling against v2 will reject the write at compile time.

  2. volatile int raw_value: Code compiled against v1 may have the compiler optimize away redundant reads of raw_value. The v2 header marks it volatile, indicating it may change asynchronously (e.g., hardware-mapped). Binaries compiled without volatile may return stale cached values.

Code diff

 struct SensorConfig {
-    int   sample_rate;
-    int   raw_value;
+    const int    sample_rate;
+    volatile int raw_value;
     int   cache_hits;
 };

Real Failure Demo

Severity: MODERATE (semantic break, not crash)

Scenario: Compile app against v1 headers, swap in v2 .so.

# Build v1 library + app
gcc -shared -fPIC -g v1.c -o libfoo.so
gcc -g app.c -I. -L. -lfoo -Wl,-rpath,. -o app
./app
# โ†’ Field qualifier change demo (compiled against v1.h):
# โ†’
# โ†’ Initial state:
# โ†’   sample_rate = 1000
# โ†’   raw_value   = 42
# โ†’   cache_hits  = 0
# โ†’
# โ†’ sensor_read(&cfg) = 42
# โ†’
# โ†’ After setting sample_rate = 2000:
# โ†’   sample_rate = 2000
# โ†’
# โ†’ raw_value read twice: r1=99 r2=99 (should be equal)
# โ†’ ...
# โ†’ sensor_read(&cfg) after modifications = 99

# Swap in v2 (no recompile)
gcc -shared -fPIC -g v2.c -o libfoo.so
./app
# โ†’ Output is identical โ€” binary layout unchanged.
# โ†’ But the semantic contract is now violated: the app writes
# โ†’ to sample_rate which v2 declares const.

Source break verification (recompilation against v2 will warn/error):

# Create a temporary source that includes v2.h instead of v1.h
sed 's/#include "v1.h"/#include "v2.h"/' app.c > /tmp/app_v2_test.c
gcc -g /tmp/app_v2_test.c -I. -L. -lfoo -Wl,-rpath,. -o app_v2 2>&1
# โ†’ error: assignment of read-only member 'sample_rate'
#   (because sample_rate is const in v2.h)
rm -f /tmp/app_v2_test.c

Reproduce with abicheck

gcc -shared -fPIC -g v1.c -o libfoo_v1.so
gcc -shared -fPIC -g v2.c -o libfoo_v2.so
abidw --out-file v1.xml libfoo_v1.so
abidw --out-file v2.xml libfoo_v2.so
abidiff v1.xml v2.xml
echo "exit: $?"

How to fix

  • Do not add const to fields of public structs unless the field was always documented as read-only.
  • If a field must become immutable, provide setter/getter functions instead of direct field access, and hide the struct behind an opaque pointer.
  • Adding volatile should be done only in a new struct or with a major version bump.

References


Source files

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