[tsan] False positive race with libc++ std::call_once on Linux
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
ThreadSanitizer reports a data race between the initializing thread inside `std::call_once` (writing via `optional::emplace` / `unordered_map` move) and another thread that has already returned from `call_once` and is reading the object. Per [cppreference / [thread.once.callonce]](https://eel.is/c++draft/thread.once.callonce), the returning call synchronizes-with all passive calls, so this should be impossible.
This looks like the known libc++ issue: `__call_once` lives in (non-instrumented) libc++, so its atomic release is invisible to TSan. Darwin already has an interceptor for this (`compiler-rt/lib/tsan/rtl/tsan_interceptors_mac.cpp`, `_ZNSt3__111__call_onceERVmPvPFvS2_E`). I do not see an equivalent on Linux.
## Environment
- OS: Linux x86_64 (Ubuntu)
- Clang: Ubuntu clang version 21.1.8 (2078da43e25a)
- libc++: 21.1.8 (`libc++-21-dev` / `libc++1`)
- Flags: `-stdlib=libc++ -static-libstdc++ -fsanitize=thread`
- `nm` shows `__call_once` linked into the binary:
`T _ZNSt3__111__call_onceERVmPvPFvS2_E`
With **libstdc++**, the same test is clean under TSan.
## Minimal reproducer
```cpp
#include
#include
#include
#include
#include
int main() {
for (int i = 0; i < 200000; ++i) {
std::once_flag flag;
std::optional> m;
auto f = [&] {
std::call_once(flag, [&] {
m.emplace(std::unordered_map{{"foo", 42}});
});
(void)m->contains("foo");
};
std::thread a(f);
std::thread b(f);
a.join();
b.join();
}
}
```
```
clang++-21 -std=c++20 -O3 -g -fsanitize=thread \
-stdlib=libc++ -static-libstdc++ -pthread \
repro.cpp -o repro
./repro
```
## Actual (flaky)
```
WARNING: ThreadSanitizer: data race
Read of size 8 ... unordered_map::contains
Previous write of size 8 ... optional::emplace / unordered_map move
... inside std::__1::__call_once ...
Location is stack of main thread.
SUMMARY: ThreadSanitizer: data race ... contains
```
## Expected
No report. Passive B must observe all side effects of the active call before call_once returns.
## Notes / possible fix
Port the Darwin __call_once interceptor (callback wrapper + explicit __tsan_release on the flag after user code) to Linux, and/or document that Linux + non-instrumented/static libc++ needs that interceptor or a TSan-instrumented libc++.
## Workaround
TSan suppression: `race:std::__1::__call_once`
Contributor guide
Research direction
Start with compiler-rt/lib/tsan/rtl/tsan_interceptors_mac.cpp and its __call_once interceptor, then locate the corresponding Linux TSan interceptor entry points. Build and run the supplied libc++ reproducer with Clang and -fsanitize=thread. Done means the valid std::call_once synchronization no longer produces the reported race, with the existing workaround and libstdc++ comparison understood.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, linux
- Domain
- devtools, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100