Skip to content

Case 133: TLS Model Flip

Field Value
Verdict ๐ŸŸก COMPATIBLE_WITH_RISK
Category Risk
Platforms Linux
Flags โ€”
Detected ChangeKinds tls_model_changed
Source files examples/case133_tls_model_flip/

Category: Build mode | Verdict: ๐ŸŸก COMPATIBLE_WITH_RISK

Verdict and consumer impact

The public symbol and its source are byte-for-byte identical between v1 and v2 โ€” only the thread-local-storage access model changed. v1 is built with -ftls-model=global-dynamic (safe for a library that may be dlopened after program start); v2 is built with -ftls-model=initial-exec, which emits a cheaper TLS access sequence but assumes the library is present at program-load time. A consumer that later dlopens v2 (rather than linking it directly at startup) can crash or misbehave accessing thread-local data, since initial-exec is not valid for TLS blocks that show up after process start. Nothing observable changes for a normally-linked-at-startup consumer, which is why this is a risk finding rather than a hard break.

Old/new diff

v1.cpp v2.cpp
int compute(int x) { return x + 1; } (compiled -ftls-model=global-dynamic) int compute(int x) { return x + 1; } (compiled -ftls-model=initial-exec)

abicheck command

g++ -shared -fPIC -g -std=gnu++17 -ftls-model=global-dynamic v1.cpp -o libv1.so
g++ -shared -fPIC -g -std=gnu++17 -ftls-model=initial-exec v2.cpp -o libv2.so

# abicheck needs the compile flags, not just the binaries, to see this
# change โ€” a compile_commands.json per side supplies the L3 build context.
cat > v1_compile_commands.json <<EOF
[{"directory": "$PWD", "command": "g++ -std=gnu++17 -fPIC -g -ftls-model=global-dynamic -c v1.cpp -o v1.o", "file": "$PWD/v1.cpp"}]
EOF
cat > v2_compile_commands.json <<EOF
[{"directory": "$PWD", "command": "g++ -std=gnu++17 -fPIC -g -ftls-model=initial-exec -c v2.cpp -o v2.o", "file": "$PWD/v2.cpp"}]
EOF

abicheck dump libv1.so --build-info v1_compile_commands.json -o v1.abi.json
abicheck dump libv2.so --build-info v2_compile_commands.json -o v2.abi.json
abicheck compare v1.abi.json v2.abi.json

Expected abicheck finding

Verdict: COMPATIBLE_WITH_RISK (exit 0)

- tls_model_changed: Runtime-model option 'tls_model' changed:
  'global-dynamic' -> 'initial-exec'.
  > May not be link- or runtime-compatible across consumers; the artifact
    diff confirms any concrete break.

Minimum evidence

min_evidence: L3 โ€” the symbols, DWARF, and binary layout are all identical between v1 and v2; there is nothing to see at L0/L1/L2. Only the build system's compile flags (captured in a compile_commands.json / build context) reveal that the two sides used different TLS models.

Why abicheck catches it

abicheck's L3 build-context diff reads each side's compiler invocation from the supplied build info, normalizes -ftls-model=... to the canonical tls_model runtime-model option, and reports the switch when the two sides disagree.

Runtime failure demonstration

Severity: RISK (not a proven break)

This is a build-mode signal, not an artifact-proven binary break (ADR-028 D3). compute() has no thread_local data, so nothing observably fails in this minimal case either way; the real risk applies to a library exporting thread_local state that gets dlopened after program start with initial-exec. No swap-in-place crash demo is included for that reason.

Safe redesign

Choose a TLS model compatible with how the library is actually loaded โ€” global-dynamic for anything that may be dlopened after program start โ€” and keep it stable across releases; don't let it silently vary between build configurations.

Cross-tool comparison

abidiff/ABICC compare pre-built binaries or headers; neither reads compiler flags from a build system, so this build-mode-only change is invisible to both โ€” there's no symbol, type, or layout delta for them to diff. Detecting it is specific to abicheck's L3 build-context evidence layer.


Source files

  • CMakeLists.txt
  • app.cpp
  • v1.cpp
  • v2.cpp

See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.