[compiler-rt] TSan: Support relaxed accesses and fences (PR #142579)
via llvm-commits
llvm-commits at lists.llvm.org
Sat Jun 13 11:36:51 PDT 2026
https://github.com/rymrg updated https://github.com/llvm/llvm-project/pull/142579
>From d15f1aba244b0d8b10fcfd5df2afa7dfb86689fb Mon Sep 17 00:00:00 2001
From: rymrg <54061433+rymrg at users.noreply.github.com>
Date: Tue, 3 Jun 2025 13:33:18 +0300
Subject: [PATCH] [TSan] Support relaxed accesses and fences
---
compiler-rt/lib/tsan/rtl/tsan_flags.inc | 4 +
.../lib/tsan/rtl/tsan_interface_atomic.cpp | 109 ++++++++++++++----
compiler-rt/lib/tsan/rtl/tsan_rtl.h | 2 +
compiler-rt/test/tsan/atomic_relaxed.cpp | 107 +++++++++++++++++
4 files changed, 198 insertions(+), 24 deletions(-)
create mode 100644 compiler-rt/test/tsan/atomic_relaxed.cpp
diff --git a/compiler-rt/lib/tsan/rtl/tsan_flags.inc b/compiler-rt/lib/tsan/rtl/tsan_flags.inc
index 68d4ba660debb..8d91d38efe51f 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_flags.inc
+++ b/compiler-rt/lib/tsan/rtl/tsan_flags.inc
@@ -119,3 +119,7 @@ TSAN_FLAG(const char*, adaptive_delay_max_atomic, "sleep_us=50",
TSAN_FLAG(const char*, adaptive_delay_max_sync, "sleep_us=500",
"Delay for sync operations: 'spin=N' (max N spins), 'yield', or "
"'sleep_us=N' (max N>0 us sleep)")
+TSAN_FLAG(
+ bool, relaxed_support, false,
+ "If set, slows relaxed access fast path and support fences and release"
+ "sequence in race detection.")
diff --git a/compiler-rt/lib/tsan/rtl/tsan_interface_atomic.cpp b/compiler-rt/lib/tsan/rtl/tsan_interface_atomic.cpp
index 5c2461634d2d4..a0fa7d914f7aa 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_interface_atomic.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_interface_atomic.cpp
@@ -28,6 +28,8 @@
using namespace __tsan;
+static bool relaxedSupport() { return flags()->relaxed_support; }
+
#if !SANITIZER_GO && __TSAN_HAS_INT128
// Protects emulation of 128-bit atomic operations.
static StaticSpinMutex mutex128;
@@ -228,18 +230,37 @@ namespace {
template <typename T, T (*F)(volatile T *v, T op)>
static T AtomicRMW(ThreadState *thr, uptr pc, volatile T *a, T v, morder mo) {
MemoryAccess(thr, pc, (uptr)a, AccessSize<T>(), kAccessWrite | kAccessAtomic);
- if (LIKELY(mo == mo_relaxed))
- return F(a, v);
+ const bool trackRelaxedAccess = relaxedSupport();
+ if (LIKELY(!trackRelaxedAccess)) {
+ if (LIKELY(mo == mo_relaxed))
+ return F(a, v);
+ }
SlotLocker locker(thr);
{
auto s = ctx->metamap.GetSyncOrCreate(thr, pc, (uptr)a, false);
- RWLock lock(&s->mtx, IsReleaseOrder(mo));
- if (IsAcqRelOrder(mo))
- thr->clock.ReleaseAcquire(&s->clock);
- else if (IsReleaseOrder(mo))
- thr->clock.Release(&s->clock);
- else if (IsAcquireOrder(mo))
- thr->clock.Acquire(s->clock);
+ bool fullLock = trackRelaxedAccess || IsReleaseOrder(mo);
+ RWLock lock(&s->mtx, fullLock);
+ if (!trackRelaxedAccess) {
+ if (IsAcqRelOrder(mo))
+ thr->clock.ReleaseAcquire(&s->clock);
+ else if (IsReleaseOrder(mo))
+ thr->clock.Release(&s->clock);
+ else if (IsAcquireOrder(mo))
+ thr->clock.Acquire(s->clock);
+ } else {
+ if (mo == mo_relaxed){
+ thr->clockA.Acquire(s->clock);
+ thr->clockR.Release(&s->clock);
+ } else if (IsAcqRelOrder(mo)) {
+ thr->clock.ReleaseAcquire(&s->clock);
+ } else if (IsReleaseOrder(mo)) {
+ thr->clockA.Acquire(s->clock);
+ thr->clock.Release(&s->clock);
+ } else if (IsAcquireOrder(mo)) {
+ thr->clock.Acquire(s->clock);
+ thr->clockR.Release(&s->clock);
+ }
+ }
v = F(a, v);
}
if (IsReleaseOrder(mo))
@@ -265,7 +286,8 @@ struct OpLoad {
DCHECK(IsLoadOrder(mo));
// This fast-path is critical for performance.
// Assume the access is atomic.
- if (!IsAcquireOrder(mo)) {
+ const bool trackRelaxedAccess = relaxedSupport();
+ if (!trackRelaxedAccess && !IsAcquireOrder(mo)) {
MemoryAccess(thr, pc, (uptr)a, AccessSize<T>(),
kAccessRead | kAccessAtomic);
return NoTsanAtomic(mo, a);
@@ -277,7 +299,11 @@ struct OpLoad {
if (s) {
SlotLocker locker(thr);
ReadLock lock(&s->mtx);
- thr->clock.Acquire(s->clock);
+ if (IsAcquireOrder(mo)) {
+ thr->clock.Acquire(s->clock);
+ } else if (trackRelaxedAccess) {
+ thr->clockA.Acquire(s->clock);
+ }
// Re-read under sync mutex because we need a consistent snapshot
// of the value and the clock we acquire.
v = NoTsanAtomic(mo, a);
@@ -310,7 +336,8 @@ struct OpStore {
// Assume the access is atomic.
// Strictly saying even relaxed store cuts off release sequence,
// so must reset the clock.
- if (!IsReleaseOrder(mo)) {
+ const bool trackRelaxedAccess = relaxedSupport();
+ if (!trackRelaxedAccess && !IsReleaseOrder(mo)) {
NoTsanAtomic(mo, a, v);
return;
}
@@ -318,10 +345,14 @@ struct OpStore {
{
auto s = ctx->metamap.GetSyncOrCreate(thr, pc, (uptr)a, false);
Lock lock(&s->mtx);
- thr->clock.ReleaseStore(&s->clock);
- NoTsanAtomic(mo, a, v);
+ if (IsReleaseOrder(mo))
+ thr->clock.ReleaseStore(&s->clock);
+ else if (trackRelaxedAccess)
+ thr->clockR.ReleaseStore(&s->clock);
+ NoTsanAtomic(mo, a, v);
}
- IncrementEpoch(thr);
+ if (IsReleaseOrder(mo))
+ IncrementEpoch(thr);
}
};
@@ -442,7 +473,8 @@ struct OpCAS {
MemoryAccess(thr, pc, (uptr)a, AccessSize<T>(),
kAccessWrite | kAccessAtomic);
- if (LIKELY(mo == mo_relaxed && fmo == mo_relaxed)) {
+ const bool trackRelaxedAccess = relaxedSupport();
+ if (LIKELY(!trackRelaxedAccess && mo == mo_relaxed && fmo == mo_relaxed)) {
T cc = *c;
T pr = func_cas(a, cc, v);
if (pr == cc)
@@ -455,7 +487,8 @@ struct OpCAS {
bool success;
{
auto s = ctx->metamap.GetSyncOrCreate(thr, pc, (uptr)a, false);
- RWLock lock(&s->mtx, release);
+ bool fullLock = trackRelaxedAccess || release;
+ RWLock lock(&s->mtx, fullLock);
T cc = *c;
T pr = func_cas(a, cc, v);
success = pr == cc;
@@ -463,12 +496,27 @@ struct OpCAS {
*c = pr;
mo = fmo;
}
- if (success && IsAcqRelOrder(mo))
- thr->clock.ReleaseAcquire(&s->clock);
- else if (success && IsReleaseOrder(mo))
- thr->clock.Release(&s->clock);
- else if (IsAcquireOrder(mo))
- thr->clock.Acquire(s->clock);
+ if (!trackRelaxedAccess) {
+ if (success && IsAcqRelOrder(mo))
+ thr->clock.ReleaseAcquire(&s->clock);
+ else if (success && IsReleaseOrder(mo))
+ thr->clock.Release(&s->clock);
+ else if (IsAcquireOrder(mo))
+ thr->clock.Acquire(s->clock);
+ } else {
+ if (!IsAcquireOrder(mo)) {
+ thr->clockA.Acquire(s->clock);
+ } else {
+ thr->clock.Acquire(s->clock);
+ }
+ if (success) {
+ if (!IsReleaseOrder(mo)) {
+ thr->clockR.Release(&s->clock);
+ } else {
+ thr->clock.Release(&s->clock);
+ }
+ }
+ }
}
if (success && release)
IncrementEpoch(thr);
@@ -488,7 +536,20 @@ struct OpFence {
static void NoTsanAtomic(morder mo) { __sync_synchronize(); }
static void Atomic(ThreadState *thr, uptr pc, morder mo) {
- // FIXME(dvyukov): not implemented.
+ const bool trackRelaxedAccess = relaxedSupport();
+ if (UNLIKELY(trackRelaxedAccess)) {
+ SlotLocker locker(thr);
+ if (IsAcquireOrder(mo))
+ thr->clock.Acquire(&thr->clockA);
+ if (mo == mo_seq_cst) {
+ auto s = ctx->metamap.GetSyncOrCreate(thr, pc, 0, false);
+ thr->clock.ReleaseAcquire(&s->clock);
+ }
+ if (IsReleaseOrder(mo)) {
+ thr->clockR.Acquire(&thr->clock);
+ IncrementEpoch(thr);
+ }
+ }
__sync_synchronize();
}
};
diff --git a/compiler-rt/lib/tsan/rtl/tsan_rtl.h b/compiler-rt/lib/tsan/rtl/tsan_rtl.h
index 3d1018accafc4..01c2b58c6d8ff 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_rtl.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_rtl.h
@@ -180,6 +180,8 @@ struct alignas(SANITIZER_CACHE_LINE_SIZE) ThreadState {
atomic_sint32_t pending_signals;
VectorClock clock;
+ VectorClock clockR;
+ VectorClock clockA;
// This is a slow path flag. On fast path, fast_state.GetIgnoreBit() is read.
// We do not distinguish beteween ignoring reads and writes
diff --git a/compiler-rt/test/tsan/atomic_relaxed.cpp b/compiler-rt/test/tsan/atomic_relaxed.cpp
new file mode 100644
index 0000000000000..b7b085a873d95
--- /dev/null
+++ b/compiler-rt/test/tsan/atomic_relaxed.cpp
@@ -0,0 +1,107 @@
+// RUN: %clangxx_tsan -O1 %s -o %t
+
+// RUN: TSAN_OPTIONS="relaxed_support=0" not %run %t 0 2>&1 | FileCheck %s --check-prefix=CHECK-OFF-0
+// RUN: TSAN_OPTIONS="relaxed_support=0" not %run %t 1 2>&1 | FileCheck %s --check-prefix=CHECK-OFF-1
+// RUN: TSAN_OPTIONS="relaxed_support=0" not %run %t 2 2>&1 | FileCheck %s --check-prefix=CHECK-OFF-2
+// RUN: TSAN_OPTIONS="relaxed_support=0" not %run %t 3 2>&1 | FileCheck %s --check-prefix=CHECK-OFF-3
+// RUN: TSAN_OPTIONS="relaxed_support=0" not %run %t 4 2>&1 | FileCheck %s --check-prefix=CHECK-OFF-4
+// RUN: TSAN_OPTIONS="relaxed_support=0" not %run %t 5 2>&1 | FileCheck %s --check-prefix=CHECK-OFF-5
+// RUN: TSAN_OPTIONS="relaxed_support=0" not %run %t 6 2>&1 | FileCheck %s --check-prefix=CHECK-OFF-6
+// RUN: TSAN_OPTIONS="relaxed_support=0" not %run %t 7 2>&1 | FileCheck %s --check-prefix=CHECK-OFF-7
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 0 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 1 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 2 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 3 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 4 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 5 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 6 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 7 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: TSAN_OPTIONS="relaxed_support=1" %run %t 8 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+#include "test.h"
+
+typedef long long T;
+T atomic;
+T data;
+
+void Reset() {
+ __atomic_store_n(&atomic, 1, __ATOMIC_RELEASE);
+}
+
+void* Reader(void* arg) {
+ uintptr_t test = (uintptr_t) arg;
+ volatile T sink = 0;
+ if (test == 0) {
+ do {
+ sink = __atomic_load_n(&atomic, __ATOMIC_RELAXED);
+ } while (sink == 0);
+ __atomic_thread_fence(__ATOMIC_SEQ_CST);
+ } else if (test == 1) {
+ do {
+ sink = __atomic_load_n(&atomic, __ATOMIC_RELAXED);
+ } while (sink == 0);
+ __atomic_thread_fence(__ATOMIC_ACQUIRE);
+ } else if (test == 2) {
+ do {
+ sink = __atomic_load_n(&atomic, __ATOMIC_ACQUIRE);
+ } while (sink == 0);
+ }
+ sink = data;
+ return nullptr;
+}
+
+void* Writer(void* arg) {
+ uintptr_t test = (uintptr_t) arg;
+ data = 1;
+ if (test == 0) {
+ __atomic_thread_fence(__ATOMIC_SEQ_CST);
+ __atomic_store_n(&atomic, 1, __ATOMIC_RELAXED);
+ } else if (test == 1) {
+ __atomic_thread_fence(__ATOMIC_RELEASE);
+ __atomic_store_n(&atomic, 1, __ATOMIC_RELAXED);
+ } else if (test == 2) {
+ __atomic_store_n(&atomic, 1, __ATOMIC_RELEASE);
+ }
+ return nullptr;
+}
+
+int main(int argc, char *argv[]) {
+ if (argc < 2) return 1;
+ int test = atoi(argv[1]);
+ uintptr_t writer = test / 3;
+ uintptr_t reader = test % 3;
+ pthread_t tr, tw;
+ Reset();
+ fprintf(stderr, "Test writer %zd reader %zd\n", writer, reader);
+ pthread_create(&tw, nullptr, Writer, (void*) writer);
+ pthread_create(&tr, nullptr, Reader, (void*) reader);
+ pthread_join(tr, 0);
+ pthread_join(tw, 0);
+}
+
+// CHECK-OFF-0: Test writer 0 reader 0
+// CHECK-OFF-0: ThreadSanitizer: data race
+// CHECK-OFF-0: ThreadSanitizer: reported
+// CHECK-OFF-1: Test writer 0 reader 1
+// CHECK-OFF-1: ThreadSanitizer: data race
+// CHECK-OFF-1: ThreadSanitizer: reported
+// CHECK-OFF-2: Test writer 0 reader 2
+// CHECK-OFF-2: ThreadSanitizer: data race
+// CHECK-OFF-2: ThreadSanitizer: reported
+// CHECK-OFF-3: Test writer 1 reader 0
+// CHECK-OFF-3: ThreadSanitizer: data race
+// CHECK-OFF-3: ThreadSanitizer: reported
+// CHECK-OFF-4: Test writer 1 reader 1
+// CHECK-OFF-4: ThreadSanitizer: data race
+// CHECK-OFF-4: ThreadSanitizer: reported
+// CHECK-OFF-5: Test writer 1 reader 2
+// CHECK-OFF-5: ThreadSanitizer: data race
+// CHECK-OFF-5: ThreadSanitizer: reported
+// CHECK-OFF-6: Test writer 2 reader 0
+// CHECK-OFF-6: ThreadSanitizer: data race
+// CHECK-OFF-6: ThreadSanitizer: reported
+// CHECK-OFF-7: Test writer 2 reader 1
+// CHECK-OFF-7: ThreadSanitizer: data race
+// CHECK-OFF-7: ThreadSanitizer: reported
+//
+// CHECK-ON-NOT: WARNING: ThreadSanitizer: data race
+// CHECK-ON-NOT: ThreadSanitizer: reported
More information about the llvm-commits
mailing list