[compiler-rt] TSan: Support relaxed accesses and fences (PR #142579)

via llvm-commits llvm-commits at lists.llvm.org
Sat Jun 13 12:18:35 PDT 2026


https://github.com/rymrg updated https://github.com/llvm/llvm-project/pull/142579

>From a93a4203e942b154a12eeaff9c460033eba75389 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       |   3 +
 .../lib/tsan/rtl/tsan_interface_atomic.cpp    | 109 ++++++++++++++----
 compiler-rt/lib/tsan/rtl/tsan_rtl.h           |   2 +
 compiler-rt/test/tsan/atomic_relaxed.cpp      |  84 ++++++++++++++
 4 files changed, 174 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..e44e55f4ebfc9 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_flags.inc
+++ b/compiler-rt/lib/tsan/rtl/tsan_flags.inc
@@ -119,3 +119,6 @@ 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..1dc26b50099f6
--- /dev/null
+++ b/compiler-rt/test/tsan/atomic_relaxed.cpp
@@ -0,0 +1,84 @@
+// RUN: %clangxx_tsan -O1 %s -o %t
+
+// RUN: %env_tsan_opts="relaxed_support=0" not %run %t 0 2>&1 | FileCheck %s --check-prefix=CHECK-OFF
+// RUN: %env_tsan_opts="relaxed_support=0" not %run %t 1 2>&1 | FileCheck %s --check-prefix=CHECK-OFF
+// RUN: %env_tsan_opts="relaxed_support=0" not %run %t 2 2>&1 | FileCheck %s --check-prefix=CHECK-OFF
+// RUN: %env_tsan_opts="relaxed_support=0" not %run %t 3 2>&1 | FileCheck %s --check-prefix=CHECK-OFF
+// RUN: %env_tsan_opts="relaxed_support=0" not %run %t 4 2>&1 | FileCheck %s --check-prefix=CHECK-OFF
+// RUN: %env_tsan_opts="relaxed_support=0" not %run %t 5 2>&1 | FileCheck %s --check-prefix=CHECK-OFF
+// RUN: %env_tsan_opts="relaxed_support=0" not %run %t 6 2>&1 | FileCheck %s --check-prefix=CHECK-OFF
+// RUN: %env_tsan_opts="relaxed_support=0" not %run %t 7 2>&1 | FileCheck %s --check-prefix=CHECK-OFF
+// RUN: %env_tsan_opts="relaxed_support=1"     %run %t 0 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: %env_tsan_opts="relaxed_support=1"     %run %t 1 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: %env_tsan_opts="relaxed_support=1"     %run %t 2 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: %env_tsan_opts="relaxed_support=1"     %run %t 3 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: %env_tsan_opts="relaxed_support=1"     %run %t 4 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: %env_tsan_opts="relaxed_support=1"     %run %t 5 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: %env_tsan_opts="relaxed_support=1"     %run %t 6 2>&1 | FileCheck %s --check-prefix=CHECK-ON
+// RUN: %env_tsan_opts="relaxed_support=1"     %run %t 7 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: ThreadSanitizer: data race
+// CHECK-OFF: ThreadSanitizer: reported
+
+// CHECK-ON-NOT: WARNING: ThreadSanitizer: data race
+// CHECK-ON-NOT: ThreadSanitizer: reported



More information about the llvm-commits mailing list