[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