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) 
```

Reply via email to