Case 67: TLS Variable Size Changed¶
| Field | Value |
|---|---|
| Verdict | ๐ด BREAKING |
| Category | Breaking |
| Platforms | Linux, macOS |
| Flags | ABI break, API break |
Detected ChangeKinds |
tls_var_size_changed |
| Source files | browse on GitHub |
Category: Variable ABI | Verdict: BREAKING
What breaks¶
The thread-local ErrorCtx struct grows from 68 bytes (v1) to 72 bytes (v2)
because a new severity field is inserted between code and message. This
shifts message from offset 4 to offset 8.
A consumer compiled against v1 accesses tls_error.message at offset 4, but
v2 wrote the severity integer there. The app reads the integer bytes as a
string โ getting garbage or an empty string instead of the error message.
Why this matters¶
Thread-local variables (__thread / thread_local) are commonly used for
per-thread error state, logging context, and locale data. When exported as part
of a library's public ABI, the struct layout of TLS variables becomes a
binary contract:
- Consumers that access struct fields directly (not through accessor functions) embed the field offsets at compile time
- Changing the struct layout changes the ELF symbol size (
st_sizein.dynsym) which abicheck tracks - The corruption is per-thread and hard to reproduce in testing because each thread gets its own TLS copy
This break is particularly dangerous because: - TLS variables are often accessed on hot paths (error checking, logging) - The struct may be large (message buffers, context data) - Inserting a field is a natural "improvement" that seems harmless
Code diff¶
// v1: message at offset 4
typedef struct ErrorCtx {
int code; /* offset 0 */
char message[64]; /* offset 4 */
} ErrorCtx; /* sizeof = 68 */
// v2: severity inserted, message shifts to offset 8
typedef struct ErrorCtx {
int code; /* offset 0 (unchanged) */
int severity; /* offset 4 (NEW!) */
char message[64]; /* offset 8 (was 4 โ shifted!) */
} ErrorCtx; /* sizeof = 72 */
Real Failure Demo¶
Severity: CRITICAL
Scenario: compile app against v1, swap in v2 .so without recompile.
# Build old library + app
gcc -shared -fPIC -g v1.c -o liblogger.so
gcc -g app.c -L. -llogger -Wl,-rpath,. -o app
./app
# โ error code = 404 (expected 404)
# โ message = "not found" (expected "not found")
# Swap in new library (no recompile)
gcc -shared -fPIC -g v2.c -o liblogger.so
./app
# โ error code = 404 (expected 404)
# โ message = "\x03" (expected "not found") โ reads severity=3 as a char!
# โ CORRUPTION: TLS struct layout changed
Why CRITICAL: The app reads tls_error.message at v1's offset 4, but v2
placed the severity integer (value 3) there. On little-endian x86, the app
interprets bytes 0x03 0x00 0x00 0x00 as a 1-character string "\x03" (a
non-printable control character). The actual message "not found" is at
offset 8, which the app never reads. No crash occurs โ just silently wrong
error messages, making debugging extremely difficult.
How to fix¶
- Use accessor functions: don't export TLS variables directly; provide
logger_get_message()/logger_set_message()instead - Append-only layout: only add new fields at the end of the struct, never insert between existing fields
- Use opaque pointers:
extern __thread void *tls_error_ctx;with accessor functions that cast internally - Freeze exported struct layout: treat the
sizeofand field offsets of any exported TLS variable as part of the ABI contract - Add reserved space: include padding fields for future expansion
Real-world example¶
glibc's errno is a TLS variable (__thread int errno). Its size (4 bytes)
has been frozen since glibc 2.0 โ changing it would break every C program in
existence. Similarly, __thread locale_t __locale in glibc has a fixed layout
that cannot change without breaking the ABI.
OpenSSL's per-thread error queue used to be a public TLS struct; OpenSSL 3.0 moved to an opaque handle specifically to avoid this class of break when the error context needed to grow.
abicheck detection¶
abicheck detects this as tls_var_size_changed (BREAKING) by comparing the
st_size field in the .dynsym entry for TLS symbols (those with STT_TLS
type) between the two library versions.
References¶
Source files¶
See also: Examples overview ยท All BREAKING cases ยท Category: Breaking.