ThreadSanitizer reports a data race when stack storage containing std::latch is reused after wait()
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Environment
```text
OS:
Ubuntu 26.04 LTS (x86_64)
Compiler:
Clang 22.1.2
Comparison Compiler:
GCC 16.0.1 (Ubuntu 16-20260322-1ubuntu1, trunk r16-8246-g569ace1fa50)
Standard Library:
libstdc++ from GCC 16 (Ubuntu package 16-20260322-1ubuntu1)
Language:
C++23
Build Flags:
-O1
-g3
-fno-omit-frame-pointer
-fno-optimize-sibling-calls
-fsanitize=thread
```
---
## Description
ThreadSanitizer reports a data race involving a stack-allocated `std::latch`.
The code starts a worker thread, waits on a stack-allocated `std::latch`, returns after `wait()` completes, and then immediately constructs a `std::function` in the caller.
TSan reports a race between:
* an atomic write performed by `std::latch::count_down()`, and
* a non-atomic write constructing the `std::function`.
The reported address belongs to the main thread's stack.
Since `wait()` is specified to synchronize with the corresponding `count_down()` that releases it, I expected the former stack storage to be safely reusable after `wait()` returned. Therefore I am unsure whether this warning indicates a real issue in my code or a limitation/false positive in ThreadSanitizer.
The `std::latch` object remains alive until after `wait()` returns, and the worker thread does not access it after `count_down()` completes.
---
## Relevant Code
```cpp
void EventLoopThread::start() {
std::latch startup_ready{1};
thread_ = std::thread([this, &startup_ready] {
EventLoop loop;
loop_ptr_.store(&loop, std::memory_order_release);
startup_ready.count_down();
loop.loop();
loop_ptr_.store(nullptr, std::memory_order_release);
});
startup_ready.wait();
}
```
Later:
```cpp
TEST(EventLoopAttorneyTest, ExecutesCallbackInLoopThread) {
EventLoopThread thread;
thread.start();
auto& loop = EventLoopThreadAttorney::loop(thread);
std::promise promise;
auto future = promise.get_future();
loop.runInLoop([&promise] {
promise.set_value(true);
});
ASSERT_EQ(future.wait_for(std::chrono::seconds(1)),
std::future_status::ready);
EXPECT_TRUE(future.get());
}
```
The warning is reproducible with both `std::function` and `std::move_only_function`.
---
## ThreadSanitizer Report
```text
startup_ready address: 0x7fffffffd484
[INFO] [3855685] src/io/event_loop.cpp:121 [void raycpp::io::EventLoop::loop()]: EventLoop 0x7ffff3dfd510 start looping
==================
WARNING: ThreadSanitizer: data race (pid=3855680)
Write of size 8 at 0x7fffffffd480 by main thread:
#0 std::function::function(std::function&&) /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/bits/std_function.h:395:27 (raycpp_unit_tests+0x1fea4b) (BuildId: 9e9771779a9a37a7ebdbb5bd86a30ff1424f97af)
#1 raycpp::io::EventLoop::runInLoop(std::function) /home/ray/raycpp/src/io/event_loop.cpp:209:21 (raycpp_unit_tests+0x1fea4b)
#2 EventLoopAttorneyTest_ExecutesCallbackInLoopThread_Test::TestBody() /home/ray/raycpp/tests/io/detail/event_loop_thread_attorney_test.cpp:38:10 (raycpp_unit_tests+0x1c47da) (BuildId: 9e9771779a9a37a7ebdbb5bd86a30ff1424f97af)
#3 void testing::internal::HandleSehExceptionsInMethodIfSupported(testing::Test*, void (testing::Test::*)(), char const*) /home/ray/raycpp/local_deps/googletest/googletest/src/gtest.cc:2664:10 (raycpp_unit_tests+0x2429ac) (BuildId: 9e9771779a9a37a7ebdbb5bd86a30ff1424f97af)
#4 void testing::internal::HandleExceptionsInMethodIfSupported(testing::Test*, void (testing::Test::*)(), char const*) /home/ray/raycpp/local_deps/googletest/googletest/src/gtest.cc:2700:14 (raycpp_unit_tests+0x2429ac)
#5 __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:59:16 (libc.so.6+0x2a600) (BuildId: 240c8909736b31f963346aca80667fd00c551e32)
Previous atomic write of size 4 at 0x7fffffffd484 by thread T1:
#0 int std::__atomic_impl::fetch_sub(int*, std::__conditional>::type::type>, std::memory_order) /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/bits/atomic_base.h:1226:16 (raycpp_unit_tests+0x2012b1) (BuildId: 9e9771779a9a37a7ebdbb5bd86a30ff1424f97af)
#1 std::latch::count_down(long) /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/latch:77:26 (raycpp_unit_tests+0x2012b1)
#2 raycpp::io::EventLoopThread::start()::$_0::operator()() /home/ray/raycpp/src/io/event_loop_thread.cpp:36:23 (raycpp_unit_tests+0x2012b1)
#3 void std::__invoke_impl(std::__invoke_other, raycpp::io::EventLoopThread::start()::$_0&&) /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/bits/invoke.h:63:14 (raycpp_unit_tests+0x2012b1)
#4 std::__invoke_result::type std::__invoke(raycpp::io::EventLoopThread::start()::$_0&&) /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/bits/invoke.h:98:14 (raycpp_unit_tests+0x2012b1)
#5 void std::thread::_Invoker>::_M_invoke<0ul>(std::_Index_tuple<0ul>) /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/bits/std_thread.h:303:13 (raycpp_unit_tests+0x2012b1)
#6 std::thread::_Invoker>::operator()() /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/bits/std_thread.h:310:11 (raycpp_unit_tests+0x2012b1)
#7 std::thread::_State_impl>>::_M_run() /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/bits/std_thread.h:255:13 (raycpp_unit_tests+0x2012b1)
#8 (libstdc++.so.6+0xf5ef8) (BuildId: 3a7a8c6bd922e6314edb7a710fdc0293265c2c42)
Location is stack of main thread.
Thread T1 (tid=3855685, running) created by main thread at:
#0 pthread_create (raycpp_unit_tests+0xbd25e) (BuildId: 9e9771779a9a37a7ebdbb5bd86a30ff1424f97af)
#1 std::thread::_M_start_thread(std::unique_ptr>, void (*)()) (libstdc++.so.6+0xf5ff0) (BuildId: 3a7a8c6bd922e6314edb7a710fdc0293265c2c42)
#2 EventLoopAttorneyTest_ExecutesCallbackInLoopThread_Test::TestBody() /home/ray/raycpp/tests/io/detail/event_loop_thread_attorney_test.cpp:31:12 (raycpp_unit_tests+0x1c458a) (BuildId: 9e9771779a9a37a7ebdbb5bd86a30ff1424f97af)
#3 void testing::internal::HandleSehExceptionsInMethodIfSupported(testing::Test*, void (testing::Test::*)(), char const*) /home/ray/raycpp/local_deps/googletest/googletest/src/gtest.cc:2664:10 (raycpp_unit_tests+0x2429ac) (BuildId: 9e9771779a9a37a7ebdbb5bd86a30ff1424f97af)
#4 void testing::internal::HandleExceptionsInMethodIfSupported(testing::Test*, void (testing::Test::*)(), char const*) /home/ray/raycpp/local_deps/googletest/googletest/src/gtest.cc:2700:14 (raycpp_unit_tests+0x2429ac)
#5 __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:59:16 (libc.so.6+0x2a600) (BuildId: 240c8909736b31f963346aca80667fd00c551e32)
SUMMARY: ThreadSanitizer: data race /home/ray/raycpp/src/io/event_loop.cpp:209:21 in raycpp::io::EventLoop::runInLoop(std::function)
==================
[INFO] [3855685] src/io/event_loop.cpp:132 [void raycpp::io::EventLoop::loop()]: EventLoop 0x7ffff3dfd510 stop looping
```
---
## Additional Testing
I tested the same code under the following configurations.
The warning only appears when using LLVM's ThreadSanitizer runtime together with libstdc++.
The same code does not trigger a warning when compiled with GCC 16 and GCC's ThreadSanitizer.
| Compiler | TSan Runtime | Standard Library | Result |
|----------|--------------|------------------|--------|
| Clang 22.1.2 | LLVM TSan | GCC 16 libstdc++ | Warning |
| GCC 16.0.1 | GCC TSan | GCC 16 libstdc++ | No warning |
The warning is reproducible across multiple runs.
I have not yet been able to test against libc++, because the libc++ version available in my environment does not currently provide `std::move_only_function`.
---
## Additional Observations
In my testing, replacing the stack-allocated `std::latch` with a separately allocated one (using `std::shared_ptr`) consistently eliminates the warning.
```cpp
auto startup_ready = std::make_shared(1);
```
It also disappears if the latch is given sufficiently large alignment, for example:
```cpp
alignas(64) std::latch startup_ready{1};
```
---
## Notes
I attempted to reduce this to a minimal standalone reproducer, but so far I have not been able to reproduce the warning outside of this test case.
The attached test reproduces the warning reliably, while the two changes described above consistently eliminate it.
---
## Questions
Could someone clarify whether this behavior is expected, or whether it indicates a limitation in ThreadSanitizer's handling of `std::latch` synchronization and stack storage reuse?
In particular:
- Is this a known limitation or expected behavior of LLVM ThreadSanitizer?
- Should `std::latch::wait()` synchronization be sufficient for TSan to recognize that the stack storage can be safely reused after `wait()` returns?
- If this is a TSan instrumentation/modeling issue, would a reduced reproducer be useful for further investigation?
I would be happy to provide any additional information or help create a smaller reproducer if needed.
Contributor guide
Research direction
Start with the reproducing code in src/io/event_loop_thread.cpp and src/io/event_loop.cpp, then inspect tests/io/detail/event_loop_thread_attorney_test.cpp and rerun it with Clang's ThreadSanitizer and GCC's libstdc++. Compare the LLVM and GCC TSan results and reduce the case if possible; done means establishing whether this is expected behavior or a TSan modeling issue, with a focused reproducer if it is a bug.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers, testing-qa
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100