llvm / llvm/llvm-project

ThreadSanitizer reports a data race when stack storage containing std::latch is reused after wait()

Open
#212,705 0 comments 0 reactions 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

## 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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.