Case 171: Static TLS Introduced¶
| Field | Value |
|---|---|
| Verdict | ๐ก COMPATIBLE_WITH_RISK |
| Category | Risk |
| Platforms | Linux |
| Flags | โ |
Detected ChangeKinds |
static_tls_introduced |
| Source files | examples/case171_static_tls_introduced/ |
Category: Deployment Risk (ELF/Loader) | Verdict: โ ๏ธ COMPATIBLE_WITH_RISK
Verdict and consumer impact¶
The source is byte-identical between v1 and v2 โ only the TLS access
model selected at compile time differs (-ftls-model=initial-exec on v2
vs. the default global-dynamic on v1). Under global-dynamic, a thread's
storage for counter is allocated lazily, so the library can be loaded at
any point in a process's life โ including via dlopen() long after
startup. Under initial-exec, the dynamic linker must instead assign
counter a fixed offset in a per-thread block sized once, at process
startup, from the libraries present at that time (the "static TLS
surplus"). The exported ABI (symbol names, signatures) is identical, so
this is not a hard break โ but the library's runtime loading contract
silently narrowed: it may no longer be reliably dlopen()-able once the
static-TLS surplus is exhausted.
Old/new diff¶
| v1.c | v2.c |
|---|---|
__thread int counter = 0; int bump(void) { return ++counter; } |
(identical source) |
built: gcc -shared -fPIC v1.c (global-dynamic TLS) |
built: gcc -shared -fPIC -ftls-model=initial-exec v2.c (initial-exec TLS) |
abicheck command¶
gcc -shared -fPIC -g v1.c -o libv1.so
gcc -shared -fPIC -g -ftls-model=initial-exec v2.c -o libv2.so
abicheck compare libv1.so libv2.so
Expected abicheck finding¶
Verdict: COMPATIBLE_WITH_RISK (exit 0)
Deployment Risk Changes:
- static_tls_introduced: Static-TLS model introduced (DF_STATIC_TLS set):
the library may no longer be reliably dlopen()ed
Quality Issues:
- needed_removed: Dependency removed: ld-linux-x86-64.so.2
- symbol_version_required_removed: Symbol version requirement removed:
GLIBC_2.3 (from ld-linux-x86-64.so.2)
- imported_symbol_removed: Imported symbol no longer required:
__tls_get_addr@GLIBC_2.3
The dependency/symbol-version churn is a build-environment side effect of
dropping the __tls_get_addr call (global-dynamic needs it, initial-exec
doesn't); static_tls_introduced is the finding that names the actual
deployment-contract change.
Minimum evidence¶
min_evidence: L0 โ the ELF dynamic section alone is enough: abicheck
reads DF_STATIC_TLS straight from DT_FLAGS and checks the library
genuinely participates in TLS (it exports or imports at least one
STT_TLS symbol, so a spurious flag on a TLS-free library never fires).
No DWARF, no headers, no build metadata required โ this is the
binary-only counterpart to case133_tls_model_flip's L3 build-flag view
of the same transition.
Why abicheck catches it¶
The linker stamps DF_STATIC_TLS into a library's DT_FLAGS whenever any
input object used the initial-exec/local-exec TLS model โ a fact visible
even on a distributed binary with no build logs or compile database:
readelf -d libv1.so | grep FLAGS # (nothing โ no DF_STATIC_TLS)
readelf -d libv2.so | grep FLAGS
# 0x000000000000001e (FLAGS) STATIC_TLS
abicheck reads this flag directly from each side's dynamic section and
reports its introduction as static_tls_introduced.
Runtime failure demonstration¶
Severity: INFORMATIONAL (host-dependent)
Scenario: dlopen() a plugin after the process has already started.
gcc -g app.c -o app -ldl
./app
# โ dlopen succeeded; bump() = 1
cp libv2.so libv1.so
./app
# โ dlopen succeeded; bump() = 1 (on this host โ surplus absorbed it)
Why this demo isn't the CI-graded proof: whether the second run fails
depends on how much static-TLS "surplus" the libc reserved at process
startup for later dlopen()s โ a budget that varies by glibc version,
LD_DEBUG=statistics tuning, and how many other initial-exec libraries
are already loaded. On many systems (including the sandbox this was run
in) it simply works, since the surplus absorbs one more consumer; on a
host where the surplus is exhausted, dlopen() fails outright with
glibc's "cannot allocate memory in static TLS block" (or an analogous
diagnostic on other libcs) โ a hard load failure with no exported-symbol
change to explain it. The CI-graded proof is the DF_STATIC_TLS artifact
fact above, not whether this particular run happens to fail.
Safe redesign¶
- Avoid
-ftls-model=initial-exec/local-exec(or__attribute__((tls_model(...)))on individual variables) in any library that may bedlopen()'d after startup โ plugins, optional codecs, lazily-loaded backends. - If the static model is required for performance, document that the
library must be linked at startup (
DT_NEEDED), notdlopen()'d later.
References¶
- DF_STATIC_TLS โ System V ABI / glibc TLS internals
- Related cases:
case133_tls_model_flip,case67_tls_var_size_changed
Source files¶
CMakeLists.txtapp.cv1.cv2.c
See also: Examples overview ยท All COMPATIBLE_WITH_RISK cases ยท Category: Risk.