[libcxx-commits] [libcxx] [libc++] Make std::call_once synchronization visible to TSan (PR #220160)

Jeewoong Kim via libcxx-commits libcxx-commits at lists.llvm.org
Mon Aug 31 23:14:18 PDT 2026


https://github.com/JeewoongKim created https://github.com/llvm/llvm-project/pull/220160

Fixes #216066 

The false positive happens when libc++ itself is not built with TSan.
The acquire is in the header, while the corresponding release is in the libc++ implementation
and is not visible to TSan.

I first tried a Darwin-style `__call_once` interceptor in `compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp`. 
It fixed the shared libc++ case, but not the static one. This change instead adds the TSan release
to the existing callback wrapper in the header, so it works for both static and shared libc++.

The regression test now waits for the first `call_once` to complete before making the second call
so it exercises the failing path deterministically.

>From ad6ad358eff0de7b76f6e330315a00f4cf79c121 Mon Sep 17 00:00:00 2001
From: Jeewoong Kim <dnd414 at gmail.com>
Date: Tue, 1 Sep 2026 13:43:20 +0900
Subject: [PATCH] [libc++] Make std::call_once synchronization visible to TSan

---
 libcxx/include/__debug_utils/sanitizers.h     | 20 +++++++++++++++++++
 libcxx/include/__mutex/once_flag.h            | 16 +++++++++------
 .../thread.once.callonce/race.pass.cpp        | 13 +++++++++++-
 3 files changed, 42 insertions(+), 7 deletions(-)

diff --git a/libcxx/include/__debug_utils/sanitizers.h b/libcxx/include/__debug_utils/sanitizers.h
index e35812cc440d7..70538691f723e 100644
--- a/libcxx/include/__debug_utils/sanitizers.h
+++ b/libcxx/include/__debug_utils/sanitizers.h
@@ -38,6 +38,12 @@
 #  define _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS 0
 #endif
 
+#if __has_feature(thread_sanitizer) || defined(__SANITIZE_THREAD__)
+#  define _LIBCPP_ENABLE_TSAN_ANNOTATIONS 1
+#else
+#  define _LIBCPP_ENABLE_TSAN_ANNOTATIONS 0
+#endif
+
 #if _LIBCPP_INSTRUMENTED_WITH_ASAN && !_LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
 #  error "We can't disable ASAN container checks when libc++ has been built with ASAN container checks enabled"
 #endif
@@ -55,8 +61,22 @@ __sanitizer_verify_double_ended_contiguous_container(const void*, const void*, c
 
 #endif // _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
 
+#if _LIBCPP_ENABLE_TSAN_ANNOTATIONS
+extern "C" {
+_LIBCPP_EXPORTED_FROM_ABI void __tsan_release(void*);
+}
+#endif
+
 _LIBCPP_BEGIN_NAMESPACE_STD
 
+inline _LIBCPP_HIDE_FROM_ABI void __libcpp_tsan_release(void* __addr) {
+#if _LIBCPP_ENABLE_TSAN_ANNOTATIONS
+  ::__tsan_release(__addr);
+#else
+  (void)__addr;
+#endif
+}
+
 // ASan choices
 #if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
 #  define _LIBCPP_HAS_ASAN_CONTAINER_ANNOTATIONS_FOR_ALL_ALLOCATORS 1
diff --git a/libcxx/include/__mutex/once_flag.h b/libcxx/include/__mutex/once_flag.h
index 63c97a2921412..f93817f9edd67 100644
--- a/libcxx/include/__mutex/once_flag.h
+++ b/libcxx/include/__mutex/once_flag.h
@@ -10,6 +10,7 @@
 #define _LIBCPP___MUTEX_ONCE_FLAG_H
 
 #include <__config>
+#include <__debug_utils/sanitizers.h>
 #include <__memory/addressof.h>
 #include <__tuple/tuple_size.h>
 #include <__type_traits/invoke.h>
@@ -82,14 +83,16 @@ struct once_flag {
 template <class _Fp>
 class __call_once_param {
   _Fp& __f_;
+  void* __flag_;
 
 public:
-  _LIBCPP_HIDE_FROM_ABI explicit __call_once_param(_Fp& __f) : __f_(__f) {}
+  _LIBCPP_HIDE_FROM_ABI explicit __call_once_param(_Fp& __f, void* __flag) : __f_(__f), __flag_(__flag) {}
 
   _LIBCPP_HIDE_FROM_ABI void operator()() {
     [&]<size_t... _Indices>(__index_sequence<_Indices...>) -> void {
       std::__invoke(std::get<_Indices>(std::move(__f_))...);
     }(__make_index_sequence<tuple_size<_Fp>::value>());
+    std::__libcpp_tsan_release(__flag_);
   }
 };
 
@@ -98,11 +101,12 @@ class __call_once_param {
 template <class _Fp>
 class __call_once_param {
   _Fp& __f_;
+  void* __flag_;
 
 public:
-  _LIBCPP_HIDE_FROM_ABI explicit __call_once_param(_Fp& __f) : __f_(__f) {}
+  _LIBCPP_HIDE_FROM_ABI explicit __call_once_param(_Fp& __f, void* __flag) : __f_(__f), __flag_(__flag) {}
 
-  _LIBCPP_HIDE_FROM_ABI void operator()() { __f_(); }
+  _LIBCPP_HIDE_FROM_ABI void operator()() { __f_(); std::__libcpp_tsan_release(__flag_); }
 };
 
 #endif
@@ -133,7 +137,7 @@ inline _LIBCPP_HIDE_FROM_ABI void call_once(once_flag& __flag, _Callable&& __fun
   if (__libcpp_acquire_load(&__flag.__state_) != once_flag::_Complete) {
     typedef tuple<_Callable&&, _Args&&...> _Gp;
     _Gp __f(std::forward<_Callable>(__func), std::forward<_Args>(__args)...);
-    __call_once_param<_Gp> __p(__f);
+    __call_once_param<_Gp> __p(__f, std::addressof(__flag.__state_));
     std::__call_once(__flag.__state_, std::addressof(__p), std::addressof(__call_once_proxy<_Gp>));
   }
 }
@@ -143,7 +147,7 @@ inline _LIBCPP_HIDE_FROM_ABI void call_once(once_flag& __flag, _Callable&& __fun
 template <class _Callable>
 inline _LIBCPP_HIDE_FROM_ABI void call_once(once_flag& __flag, _Callable& __func) {
   if (__libcpp_acquire_load(&__flag.__state_) != once_flag::_Complete) {
-    __call_once_param<_Callable> __p(__func);
+    __call_once_param<_Callable> __p(__func, std::addressof(__flag.__state_));
     std::__call_once(__flag.__state_, std::addressof(__p), std::addressof(__call_once_proxy<_Callable>));
   }
 }
@@ -151,7 +155,7 @@ inline _LIBCPP_HIDE_FROM_ABI void call_once(once_flag& __flag, _Callable& __func
 template <class _Callable>
 inline _LIBCPP_HIDE_FROM_ABI void call_once(once_flag& __flag, const _Callable& __func) {
   if (__libcpp_acquire_load(&__flag.__state_) != once_flag::_Complete) {
-    __call_once_param<const _Callable> __p(__func);
+    __call_once_param<const _Callable> __p(__func, std::addressof(__flag.__state_));
     std::__call_once(__flag.__state_, std::addressof(__p), std::addressof(__call_once_proxy<const _Callable>));
   }
 }
diff --git a/libcxx/test/std/thread/thread.mutex/thread.once/thread.once.callonce/race.pass.cpp b/libcxx/test/std/thread/thread.mutex/thread.once/thread.once.callonce/race.pass.cpp
index 1bc608afef353..bff47b3c00886 100644
--- a/libcxx/test/std/thread/thread.mutex/thread.once/thread.once.callonce/race.pass.cpp
+++ b/libcxx/test/std/thread/thread.mutex/thread.once/thread.once.callonce/race.pass.cpp
@@ -19,6 +19,7 @@
 // call_once properly synchronizes user state, a data race that was fixed
 // in r280621.
 
+#include <atomic>
 #include <mutex>
 #include <thread>
 #include <cassert>
@@ -27,6 +28,7 @@
 #include "test_macros.h"
 
 std::once_flag flg0;
+std::atomic<bool> initialized(false);
 long global = 0;
 
 void init0()
@@ -36,6 +38,15 @@ void init0()
 
 void f0()
 {
+    std::call_once(flg0, init0);
+    initialized.store(true, std::memory_order_relaxed);
+}
+
+void f1()
+{
+    while (!initialized.load(std::memory_order_relaxed))
+        std::this_thread::yield();
+
     std::call_once(flg0, init0);
     assert(global == 1);
 }
@@ -43,7 +54,7 @@ void f0()
 int main(int, char**)
 {
     std::thread t0 = support::make_test_thread(f0);
-    std::thread t1 = support::make_test_thread(f0);
+    std::thread t1 = support::make_test_thread(f1);
     t0.join();
     t1.join();
     assert(global == 1);



More information about the libcxx-commits mailing list