https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125659
Bug ID: 125659
Summary: ThreadSanitizer reports a false-positive data race for
code synchronized with std::atomic::wait
Product: gcc
Version: 16.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: libstdc++
Assignee: unassigned at gcc dot gnu.org
Reporter: kyusic at gmail dot com
Target Milestone: ---
The program below is data-race-free. The only synchronization between the two
conflicting accesses to the non-atomic object data is std::atomic<int>::wait /
notify_one, which establishes a happens-before relationship:
```
#include <atomic>
#include <cassert>
#include <cstdio>
#include <thread>
int main() {
std::atomic<int> flag{0};
int data = 0; // plain (non-atomic) shared
object
std::thread worker([&] {
flag.wait(0, std::memory_order_acquire); // (B) returns after observing
the store below
int local = data; // read, happens-after (A)
assert(local == 42);
std::printf("data=%d\n", local);
});
data = 42; // (A)
flag.store(1, std::memory_order_release);
flag.notify_one();
worker.join();
}
```
'data = 42' is sequenced before the release store; worker reads data only after
'wait' returns, having observed 'flag != 0' with an acquire load. So (A)
happens-before the read. The program always prints 'data=42' and the assertion
never fires.
Built with -fsanitize=thread on GCC 16, ThreadSanitizer reports a data race on
data. GCC 15 does not.
## Reproducing reliably
A single run only triggers intermittently — the report appears only when the
worker's wait actually races with the notifying store (skewing the timing in
either direction hides it). Wrapping the (still race-free) body in a loop
reproduces it on every run on GCC 16 and never on GCC 15:
```
#include <atomic>
#include <cstdio>
#include <thread>
int main() {
for (int i = 0; i < 2000; ++i) {
std::atomic<int> flag{0};
int data = 0;
std::thread w([&] {
flag.wait(0, std::memory_order_acquire);
volatile int l = data; (void)l;
});
data = 42;
flag.store(1, std::memory_order_release);
flag.notify_one();
w.join();
}
std::printf("done\n");
}
```
```
$ g++-16 -std=c++20 -O1 -g -fsanitize=thread -pthread loop.cc -o loop && ./loop
```
## Expected
No ThreadSanitizer diagnostic (the program is race-free; it prints done /
data=42).
## Actual (GCC 16)
The program still prints done / data=42 (so the happens-before holds at
runtime), but ThreadSanitizer reports:
```
WARNING: ThreadSanitizer: data race
Read of size 4 at 0x... by thread T1:
#0 operator() loop.cc:11
...
#6 (libstdc++.so.6+0x...)
Previous write of size 4 at 0x... by main thread:
#0 main loop.cc:12
Location is stack of main thread.
Thread T1 ... created by main thread at:
#0 pthread_create
#1 std::thread::_M_start_thread(...) (libstdc++.so.6+0x...)
...
SUMMARY: ThreadSanitizer: data race loop.cc:11 in operator()
```
## Scope
- GCC 15: no warning. GCC 16: warning (10/10 runs of the loop reproducer here).
- Deterministic enough via the loop; a single iteration is ~2/3 of runs.
- The same wait/notify mechanism backs std::latch, std::barrier,
std::counting_semaphore and std::atomic_flag::wait, which appear to be affected
as well.
## Note
This looks like ThreadSanitizer losing the happens-before edge carried by the
library's wait/notify; it appears to be new in GCC 16.
## Environment
- gcc version 16.0.1 20260322 (experimental) [trunk r16-8246-g569ace1fa50]
(Ubuntu 16-20260322-1ubuntu1)
- Target: x86_64-linux-gnu
- Compile: g++ -std=c++20 -O1 -g -fsanitize=thread -pthread <file>.cc
- TSAN_OPTIONS unset (defaults).
g++ -v:
```
Using built-in specs.
COLLECT_GCC=g++-16
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-linux-gnu/16/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu
16-20260322-1ubuntu1' --with-bugurl=file:///usr/share/doc/gcc-16/README.Bugs
--enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2,rust,cobol,algol68
--prefix=/usr --with-gcc-major-version-only --program-suffix=-16
--program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id
--libexecdir=/usr/libexec --without-included-gettext --enable-threads=posix
--libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu
--enable-libstdcxx-debug --enable-libstdcxx-time=yes
--with-default-libstdcxx-abi=new --enable-libstdcxx-backtrace
--enable-gnu-unique-object --disable-vtable-verify --enable-plugin
--enable-default-pie --with-system-zlib --enable-libphobos-checking=release
--with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch
--disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64
--with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic
--enable-offload-targets=nvptx-none=/build/gcc-16-UBzkgO/gcc-16-16-20260322/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-16-UBzkgO/gcc-16-16-20260322/debian/tmp-gcn/usr
--enable-offload-defaulted --without-cuda-driver --enable-checking=release
--build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
--with-build-config=bootstrap-lto-lean --enable-link-serialization=2
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 16.0.1 20260322 (experimental) [trunk r16-8246-g569ace1fa50]
(Ubuntu 16-20260322-1ubuntu1)
```