llvm / llvm/llvm-project

MemorySanitizer reports use-of-uninitialized-value when a DSO destructor accesses TLS after `dlclose`

Open
#215,047 1 comment 0 reactions 0 assignees View on GitHub
compiler-rt:msan
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Summary

MemorySanitizer incorrectly reports a `use-of-uninitialized-value` when a global/static destructor in a dynamically loaded shared object accesses a TLS variable after the DSO has been unloaded with `dlclose()`.

The reproducer does not depend on RocksDB and uses only `dlopen`/`dlclose`, ELF TLS, and a destructor in the shared object.

The failure appears to involve MemorySanitizer's handling of `__tls_get_addr` during destruction of a DSO. The same scenario executes normally without MemorySanitizer.

## Environment

* Target: `x86_64-pc-linux-gnu`
* OS: Debian
* Clang:

```text
Debian clang version 24.0.0 (++20260808083100+1e11d9113548-1~exp1~20260808203255.691)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/lib/llvm-24/bin
```

* MemorySanitizer: enabled with `-fsanitize=memory`
* Optimization: `-O0`
* TLS model: `global-dynamic`
* DSO compiled with `-fPIC -shared`

## Reproducer

### `libtls.cc`

```cpp
#include

thread_local int tls_value = 42;

struct TLSObject {
~TLSObject() {
printf("TLSObject::~TLSObject() value=%d\n", tls_value);
}
};

TLSObject tls_object;

struct StaticMeta {
~StaticMeta() {
printf("StaticMeta::~StaticMeta(): accessing TLS\n");
printf("TLS value = %d\n", tls_value);
}
};

StaticMeta static_meta;

extern "C" void touch_tls() {
printf("touch_tls()\n");
printf("TLS value = %d\n", tls_value);
}
```

### `main.cc`

```cpp
#include
#include
#include

int main() {
void *handle = dlopen("./libtls.so", RTLD_NOW);
if (!handle) {
std::fprintf(stderr, "dlopen failed: %s\n", dlerror());
return 1;
}

using touch_tls_t = void (*)();
auto touch_tls =
reinterpret_cast(dlsym(handle, "touch_tls"));

if (!touch_tls) {
std::fprintf(stderr, "dlsym failed: %s\n", dlerror());
return 1;
}

touch_tls();

printf("calling dlclose()\n");
dlclose(handle);

printf("done\n");
return 0;
}
```

## Build

```sh
clang++ -g -O0 \
-fsanitize=memory \
-fno-omit-frame-pointer \
-fPIC \
-ftls-model=global-dynamic \
-shared \
-o libtls.so libtls.cc

clang++ -g -O0 \
-fsanitize=memory \
-fno-omit-frame-pointer \
-o main main.cc -ldl
```

## Run

```sh
MSAN_OPTIONS=verbosity=1 ./main
```

## Observed output

The program reaches the DSO destructor and MemorySanitizer reports an uninitialized read:

```text
touch_tls()
TLS value = 42
calling dlclose()
done
TLSObject::~TLSObject() value=42
StaticMeta::~StaticMeta(): accessing TLS

==6370==WARNING: MemorySanitizer: use-of-uninitialized-value
#0 0x7fde9adc8610 in StaticMeta::~StaticMeta() /source/libtls.cc:25:5
#1 0x5605f813013f in MSanCxaAtExitWrapper(void*) (/build/main+0x7313f)
#2 0x7fde9b1ca2b0 (/lib/x86_64-linux-gnu/libc.so.6+0x422b0)
#3 0x7fde9b1ca379 in exit (/lib/x86_64-linux-gnu/libc.so.6+0x42379)
#4 0x7fde9b1b1cae (/lib/x86_64-linux-gnu/libc.so.6+0x29cae)
#5 0x7fde9b1b1d64 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29d64)
#6 0x5605f80f02f0 in _start (/build/main+0x332f0)

SUMMARY: MemorySanitizer: use-of-uninitialized-value
/source/libtls.cc:25:5 in StaticMeta::~StaticMeta()
```

The process exits because of the MSan report.

## Expected behaviour

The TLS variable was initialised to `42` and was successfully accessed before `dlclose()`.

Accessing the TLS variable from the DSO's destructor should not produce an MSan `use-of-uninitialized-value` report merely because the destructor is being executed as part of DSO teardown.

In particular, the same reproducer without MemorySanitizer completes normally:

```text
touch_tls()
TLS value = 42
calling dlclose()
done
TLSObject::~TLSObject() value=42
StaticMeta::~StaticMeta(): accessing TLS
TLS value = 42
```

## Additional debugging

The problematic access goes through `__tls_get_addr`.

The destructor contains a TLS access:

```asm
data16 lea ...,%rdi
data16 data16 rex.W call ... <__tls_get_addr@plt>
```

With the MSan runtime, the call reaches:

```text
__interceptor___tls_get_addr
-> __sanitizer::DTLS_on_tls_get_addr(...)
```

The relevant call stack observed under rr was:

```text
#0 __sanitizer::DTLS_on_tls_get_addr(...)
#1 __interceptor___tls_get_addr
#2 StaticMeta::~StaticMeta()
#3 MSanCxaAtExitWrapper(void*)
#4 __cxa_finalize
#5 __do_global_dtors_aux
#6 ...
#13 dlclose
```

This suggests the issue is related to MSan's dynamic TLS bookkeeping/DTLS handling during DSO destruction.

In particular, the TLS access from the destructor reaches `__tls_get_addr` after `dlclose()` has initiated DSO teardown, and MSan appears to associate the resulting TLS state with uninitialised shadow/origin state.

## Notes

The reproducer intentionally uses:

* a shared object;
* `thread_local` storage;
* `-ftls-model=global-dynamic`;
* a global object's destructor;
* `dlopen()` followed by `dlclose()`.

No third-party library is required.

The issue was originally observed in a RocksDB-based application, where a static destructor in a DSO similarly accessed TLS during DSO teardown. This reproducer demonstrates that RocksDB is not required to trigger the behaviour.

The problem reproduces with the Debian Clang 24 build shown above.

Also observed on:
* Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77)
* Debian clang version 20.1.8 (++20250708063551+0c9f909b7976-1~exp1~20250708183702.136)

ref: https://jira.mariadb.org/browse/MDEV-38979

Contributor guide

Open the contributing guide

Research direction

Start with the provided libtls.cc and main.cc reproducer, built with MemorySanitizer and global-dynamic TLS. Trace the path from __interceptor___tls_get_addr to DTLS_on_tls_get_addr during dlclose() and DSO destruction. Done means the destructor's initialized TLS access no longer produces a false use-of-uninitialized-value report, with a regression test covering this scenario.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, linux
Domain
compilers, operating-systems, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.