llvm / llvm/llvm-project

[tsan] False positive race with libc++ std::call_once on Linux

Open
#216,066 0 comments 1 reaction 0 assignees View on GitHub
compiler-rt:tsan false-positive
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.