[libc++] ThreadSanitizer data race between `packaged_task::operator()` and `get_future()`
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
### Summary
Calling `packaged_task::operator()` on one thread concurrently with `get_future()` on the same task — for example, handing the task to a worker thread and then retrieving its future — is reported as a data race by ThreadSanitizer under libc++.
If I'm reading the synchronization clause in [futures.task.members] correctly (quoted below), these two calls are required to be race-free, so I suspect this may be a conformance issue.
### Reproducer
```cpp
#include
#include
#include
int main() {
std::packaged_task task([] {
// widen the race window
std::this_thread::sleep_for(std::chrono::microseconds(50));
return 42;
});
std::thread worker([&task] { task(); });
std::future fut = task.get_future();
int v = fut.get();
worker.join();
return v == 42 ? 0 : 1;
}
```
```
clang++ -std=c++20 -stdlib=libc++ -fsanitize=thread -g -O1 repro.cpp -o repro && ./repro
```
Reproduces on essentially every run.
ThreadSanitizer report (clang 21.1.8 / libc++ 21.1.8)
```
==================
WARNING: ThreadSanitizer: data race (pid=3370)
Write of size 4 at 0xfffff4a01ef8 by main thread (mutexes: write M0):
#0 std::__1::__assoc_sub_state::__attach_future[abi:ne210108]() /usr/lib/llvm-21/bin/../include/c++/v1/future:544:14 (repro+0x101e88) (BuildId: 65c2523c0556f87867deabcca06ca3e3e043cf08)
#1 std::__1::future::future(std::__1::__assoc_state*) /usr/lib/llvm-21/bin/../include/c++/v1/future:969:13 (repro+0x101e88)
#2 std::__1::promise::get_future() /usr/lib/llvm-21/bin/../include/c++/v1/future:1183:10 (repro+0x101e88)
#3 std::__1::packaged_task::get_future[abi:ne210108]() /usr/lib/llvm-21/bin/../include/c++/v1/future:1645:64 (repro+0x101e88)
#4 main /work/repro.cpp:13:31 (repro+0x101e88)
Previous read of size 4 at 0xfffff4a01ef8 by thread T1:
#0 std::__1::__assoc_sub_state::__has_value[abi:ne210108]() const /usr/lib/llvm-21/bin/../include/c++/v1/future:536:60 (repro+0x102700) (BuildId: 65c2523c0556f87867deabcca06ca3e3e043cf08)
#1 std::__1::packaged_task::operator()() /usr/lib/llvm-21/bin/../include/c++/v1/future:1658:22 (repro+0x102700)
#2 main::$_1::operator()() const /work/repro.cpp:12:32 (repro+0x102188) (BuildId: 65c2523c0556f87867deabcca06ca3e3e043cf08)
#3 std::__1::__invoke_result_impl::type std::__1::__invoke[abi:ne210108](main::$_1&&) /usr/lib/llvm-21/bin/../include/c++/v1/__type_traits/invoke.h:87:27 (repro+0x102188)
#4 void std::__1::__thread_execute[abi:ne210108]>, main::$_1>(std::__1::tuple>, main::$_1>&, std::__1::__tuple_indices<...>) /usr/lib/llvm-21/bin/../include/c++/v1/__thread/thread.h:159:3 (repro+0x102188)
#5 void* std::__1::__thread_proxy[abi:ne210108]>, main::$_1>>(void*) /usr/lib/llvm-21/bin/../include/c++/v1/__thread/thread.h:168:3 (repro+0x102188)
Location is heap block of size 128 at 0xfffff4a01e80 allocated by main thread:
#0 operator new(unsigned long) (repro+0x100ae4) (BuildId: 65c2523c0556f87867deabcca06ca3e3e043cf08)
#1 std::__1::promise::promise() /usr/lib/llvm-21/bin/../include/c++/v1/future:1156:36 (repro+0x101d64) (BuildId: 65c2523c0556f87867deabcca06ca3e3e043cf08)
#2 std::__1::packaged_task::packaged_task[abi:ne210108](main::$_0&&) /usr/lib/llvm-21/bin/../include/c++/v1/future:1615:34 (repro+0x101d64)
#3 main /work/repro.cpp:6:29 (repro+0x101d64)
Mutex M0 (0xfffff4a01e98) created at:
#0 pthread_mutex_lock (repro+0x71518) (BuildId: 65c2523c0556f87867deabcca06ca3e3e043cf08)
#1 std::__1::mutex::lock() (libc++.so.1+0xc0988) (BuildId: 5d4478f8c2dfa03da8f707efe0527ee66e107ef4)
#2 (libc.so.6+0x22f18) (BuildId: b50ceafbd17dc6bceee344a66671c7eaa152bef4)
Thread T1 (tid=3372, running) created by main thread at:
#0 pthread_create (repro+0x6f208) (BuildId: 65c2523c0556f87867deabcca06ca3e3e043cf08)
#1 std::__1::__libcpp_thread_create[abi:ne210108](unsigned long*, void* (*)(void*), void*) /usr/lib/llvm-21/bin/../include/c++/v1/__thread/support/pthread.h:182:10 (repro+0x101e18) (BuildId: 65c2523c0556f87867deabcca06ca3e3e043cf08)
#2 std::__1::thread::thread[abi:ne210108](main::$_1&&) /usr/lib/llvm-21/bin/../include/c++/v1/__thread/thread.h:213:16 (repro+0x101e18)
#3 main /work/repro.cpp:12:15 (repro+0x101e18)
SUMMARY: ThreadSanitizer: data race /work/repro.cpp:13:31 in main
==================
ThreadSanitizer: reported 1 warnings
```
### Relevant standard guarantee
`packaged_task::get_future()` is required not to introduce a data race with `operator()` — C++20, [[futures.task.members] p15](https://timsong-cpp.github.io/cppwp/n4861/futures.task.members#15):
> *Synchronization*: Calls to this function do not introduce data races ([intro.races]) with calls to `operator()` or `make_ready_at_thread_exit`.
> [*Note*: Such calls need not synchronize with each other. — *end note*]
libc++ nonetheless races on the non-atomic `__state_`: `operator()` reads it through an unsynchronized [`__has_value()`](https://github.com/llvm/llvm-project/blob/llvmorg-21.1.8/libcxx/include/future#L536) at its entry ([`future:1658`](https://github.com/llvm/llvm-project/blob/llvmorg-21.1.8/libcxx/include/future#L1658)), before taking any lock, while `get_future()` writes it under a lock in [`__attach_future()`](https://github.com/llvm/llvm-project/blob/llvmorg-21.1.8/libcxx/include/future#L538-L545). Both are in `` (links pinned to `llvmorg-21.1.8`).
### Environment
- libc++ / clang 21.1.8 (Ubuntu 26.04, aarch64).
- C++20, `-fsanitize=thread`.
- The code is unchanged on `main` (`ce5b2e8`, 2026-06): `__has_value()` is still an unlocked read and `operator()` still calls it before taking a lock — i.e. not already fixed on trunk.
Thanks for taking a look.
Contributor guide
Research direction
Start with libcxx/include/future, especially __has_value(), __attach_future(), packaged_task::get_future(), and operator(), then run the supplied C++20 reproducer under ThreadSanitizer. Confirm the relevant standard guarantee and existing locking behavior; done means the concurrent calls no longer produce a data-race report while preserving packaged_task behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100