[compiler-rt] [TSan] Add lock_during_write flag on Apple platforms to avoid deadlock (PR #157928)
Dan Blackwell via llvm-commits
llvm-commits at lists.llvm.org
Wed Sep 10 11:46:28 PDT 2025
https://github.com/DanBlackwell created https://github.com/llvm/llvm-project/pull/157928
There is a commonly used library that interposes the write call, and is interposed itself. When running with `TSAN_OPTIONS=verbosity=(1|2)`, this leads to a thread locking itself, resulting in the program hanging.
This patch adds a new flag `lock_during_write` to TSan (on Apple platforms only) that, when set, allows interceptors to be bypassed during calls to write. The flag can be inherited by children, or not, depending on the value.
rdar://157565672
>From a9724c84d2cd5f39249695b1398de98978b9b485 Mon Sep 17 00:00:00 2001
From: Dan Blackwell <dan_blackwell at apple.com>
Date: Tue, 26 Aug 2025 12:08:29 +0100
Subject: [PATCH 1/2] [TSan] Add in lock_during_write flag on Apple platforms
to avoid deadlock
There is a common library that interposes the write call, and is interposed itself. When running with TSAN_OPTIONS=verbosity=(1|2), this leads to a thread locking itself, resulting in a hang.
This patch adds a new flag `lock_during_write` to TSan that allows interceptors to be bypassed during calls to write when set. The flag can be inherited by children, or not, depending on the value.
rdar://157565672
---
.../lib/sanitizer_common/sanitizer_mac.cpp | 13 ++++++-
compiler-rt/lib/tsan/rtl/tsan_flags.cpp | 37 ++++++++++++++++++
compiler-rt/lib/tsan/rtl/tsan_flags.h | 8 ++++
compiler-rt/lib/tsan/rtl/tsan_flags.inc | 12 ++++++
compiler-rt/lib/tsan/rtl/tsan_interceptors.h | 10 ++++-
.../lib/tsan/rtl/tsan_interceptors_posix.cpp | 10 +++++
compiler-rt/lib/tsan/rtl/tsan_rtl.cpp | 14 +++++++
.../test/tsan/Darwin/write-interpose.c | 39 +++++++++++++++++++
8 files changed, 141 insertions(+), 2 deletions(-)
create mode 100644 compiler-rt/test/tsan/Darwin/write-interpose.c
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_mac.cpp b/compiler-rt/lib/sanitizer_common/sanitizer_mac.cpp
index d4811ff4ed217..dfc7562f66848 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_mac.cpp
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_mac.cpp
@@ -167,8 +167,19 @@ uptr internal_read(fd_t fd, void *buf, uptr count) {
return read(fd, buf, count);
}
+} // namespace __sanitizer
+
+// Weak symbol no-op when TSan is not linked
+SANITIZER_WEAK_ATTRIBUTE void __tsan_set_in_internal_write_call(bool value) {}
+
+namespace __sanitizer {
+
uptr internal_write(fd_t fd, const void *buf, uptr count) {
- return write(fd, buf, count);
+ // We need to disable interceptors when writing in TSan
+ __tsan_set_in_internal_write_call(true);
+ uptr res = write(fd, buf, count);
+ __tsan_set_in_internal_write_call(false);
+ return res;
}
uptr internal_stat(const char *path, void *buf) {
diff --git a/compiler-rt/lib/tsan/rtl/tsan_flags.cpp b/compiler-rt/lib/tsan/rtl/tsan_flags.cpp
index 3fd58f46983fd..50632d2016376 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_flags.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_flags.cpp
@@ -20,6 +20,43 @@
#include "tsan_rtl.h"
#include "ubsan/ubsan_flags.h"
+#if SANITIZER_APPLE
+namespace __sanitizer {
+
+template <>
+inline bool FlagHandler<LockDuringWriteSetting>::Parse(const char *value) {
+ if (internal_strcmp(value, "on") == 0) {
+ *t_ = kLockDuringAllWrites;
+ return true;
+ }
+ if (internal_strcmp(value, "disable_for_current_process") == 0) {
+ *t_ = kNoLockDuringWritesCurrentProcess;
+ return true;
+ }
+ if (internal_strcmp(value, "disable_for_all_processes") == 0) {
+ *t_ = kNoLockDuringWritesAllProcesses;
+ return true;
+ }
+ Printf("ERROR: Invalid value for signal handler option: '%s'\n", value);
+ return false;
+}
+
+template <>
+inline bool FlagHandler<LockDuringWriteSetting>::Format(char *buffer,
+ uptr size) {
+ switch (*t_) {
+ case kLockDuringAllWrites:
+ return FormatString(buffer, size, "on");
+ case kNoLockDuringWritesCurrentProcess:
+ return FormatString(buffer, size, "disable_for_current_process");
+ case kNoLockDuringWritesAllProcesses:
+ return FormatString(buffer, size, "disable_for_all_processes");
+ }
+}
+
+} // namespace __sanitizer
+#endif
+
namespace __tsan {
// Can be overriden in frontend.
diff --git a/compiler-rt/lib/tsan/rtl/tsan_flags.h b/compiler-rt/lib/tsan/rtl/tsan_flags.h
index da27d5b992bcb..477d08d334605 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_flags.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_flags.h
@@ -16,6 +16,14 @@
#include "sanitizer_common/sanitizer_flags.h"
#include "sanitizer_common/sanitizer_deadlock_detector_interface.h"
+#if SANITIZER_APPLE
+enum LockDuringWriteSetting {
+ kLockDuringAllWrites,
+ kNoLockDuringWritesCurrentProcess,
+ kNoLockDuringWritesAllProcesses,
+};
+#endif
+
namespace __tsan {
struct Flags : DDFlags {
diff --git a/compiler-rt/lib/tsan/rtl/tsan_flags.inc b/compiler-rt/lib/tsan/rtl/tsan_flags.inc
index 731d776cc893e..64cc0919c0090 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_flags.inc
+++ b/compiler-rt/lib/tsan/rtl/tsan_flags.inc
@@ -80,3 +80,15 @@ TSAN_FLAG(bool, shared_ptr_interceptor, true,
TSAN_FLAG(bool, print_full_thread_history, false,
"If set, prints thread creation stacks for the threads involved in "
"the report and their ancestors up to the main thread.")
+
+#if SANITIZER_APPLE
+TSAN_FLAG(LockDuringWriteSetting, lock_during_write, kLockDuringAllWrites,
+ "Determines whether to obtain a lock while writing logs or error "
+ "reports. "
+ "\"on\" - [default] lock during all writes. "
+ "\"disable_for_current_process\" - don't lock during all writes in "
+ "the current process, but do lock for all writes in child "
+ "processes."
+ "\"disable_for_all_processes\" - don't lock during all writes in "
+ "the current process and it's children processes.")
+#endif
diff --git a/compiler-rt/lib/tsan/rtl/tsan_interceptors.h b/compiler-rt/lib/tsan/rtl/tsan_interceptors.h
index a357a870fdf8e..d4b65ab1aaa6a 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_interceptors.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_interceptors.h
@@ -1,6 +1,9 @@
#ifndef TSAN_INTERCEPTORS_H
#define TSAN_INTERCEPTORS_H
+#if SANITIZER_APPLE
+# include "sanitizer_common/sanitizer_mac.h"
+#endif
#include "sanitizer_common/sanitizer_stacktrace.h"
#include "tsan_rtl.h"
@@ -43,7 +46,12 @@ inline bool in_symbolizer() {
#endif
inline bool MustIgnoreInterceptor(ThreadState *thr) {
- return !thr->is_inited || thr->ignore_interceptors || thr->in_ignored_lib;
+ return !thr->is_inited || thr->ignore_interceptors || thr->in_ignored_lib
+#if SANITIZER_APPLE
+ || (flags()->lock_during_write != kLockDuringAllWrites &&
+ thr->in_internal_write_call)
+#endif
+ ;
}
} // namespace __tsan
diff --git a/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp b/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
index b46a81031258c..5de97ff549209 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp
@@ -31,6 +31,9 @@
#include "sanitizer_common/sanitizer_tls_get_addr.h"
#include "sanitizer_common/sanitizer_vector.h"
#include "tsan_fd.h"
+#if SANITIZER_APPLE
+# include "tsan_flags.h"
+#endif
#include "tsan_interceptors.h"
#include "tsan_interface.h"
#include "tsan_mman.h"
@@ -1649,6 +1652,13 @@ TSAN_INTERCEPTOR(int, pthread_barrier_wait, void *b) {
TSAN_INTERCEPTOR(int, pthread_once, void *o, void (*f)()) {
SCOPED_INTERCEPTOR_RAW(pthread_once, o, f);
+#if SANITIZER_APPLE
+ if (flags()->lock_during_write != kLockDuringAllWrites &&
+ cur_thread_init()->in_internal_write_call) {
+ f();
+ return 0;
+ }
+#endif
if (o == 0 || f == 0)
return errno_EINVAL;
atomic_uint32_t *a;
diff --git a/compiler-rt/lib/tsan/rtl/tsan_rtl.cpp b/compiler-rt/lib/tsan/rtl/tsan_rtl.cpp
index 0d7247a56a4c2..b8041d724d342 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_rtl.cpp
+++ b/compiler-rt/lib/tsan/rtl/tsan_rtl.cpp
@@ -40,6 +40,13 @@ SANITIZER_WEAK_DEFAULT_IMPL
void __tsan_test_only_on_fork() {}
#endif
+#if SANITIZER_APPLE
+// Override weak symbol from sanitizer_common
+extern void __tsan_set_in_internal_write_call(bool value) {
+ __tsan::cur_thread_init()->in_internal_write_call = value;
+}
+#endif
+
namespace __tsan {
#if !SANITIZER_GO
@@ -893,6 +900,13 @@ void ForkChildAfter(ThreadState* thr, uptr pc, bool start_thread) {
ThreadIgnoreBegin(thr, pc);
ThreadIgnoreSyncBegin(thr, pc);
}
+
+# if SANITIZER_APPLE
+ // This flag can have inheritance disabled - we are the child so act
+ // accordingly
+ if (flags()->lock_during_write == kNoLockDuringWritesCurrentProcess)
+ flags()->lock_during_write = kLockDuringAllWrites;
+# endif
}
#endif
diff --git a/compiler-rt/test/tsan/Darwin/write-interpose.c b/compiler-rt/test/tsan/Darwin/write-interpose.c
new file mode 100644
index 0000000000000..d103c1dce0a0d
--- /dev/null
+++ b/compiler-rt/test/tsan/Darwin/write-interpose.c
@@ -0,0 +1,39 @@
+// Test that dylibs interposing write, and then calling functions intercepted
+// by TSan don't deadlock (self-lock)
+
+// RUN: %clang_tsan %s -o %t
+// RUN: %clang_tsan %s -o %t.dylib -fno-sanitize=thread -dynamiclib -DSHARED_LIB
+
+// RUN: env DYLD_INSERT_LIBRARIES=%t.dylib TSAN_OPTIONS=verbosity=2:lock_during_write=disable_for_current_process %run %t 2>&1 | FileCheck %s
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#if defined(SHARED_LIB)
+
+// dylib implementation - interposes write() calls
+# include <mach-o/dyld-interposing.h>
+# include <os/lock.h>
+
+static ssize_t my_write(int fd, const void *buf, size_t count) {
+ struct os_unfair_lock_s lock = OS_UNFAIR_LOCK_INIT;
+ os_unfair_lock_lock(&lock);
+ printf("Interposed write called: fd=%d, count=%zu\n", fd, count);
+ ssize_t res = write(fd, buf, count);
+ os_unfair_lock_unlock(&lock);
+ return res;
+}
+DYLD_INTERPOSE(my_write, write);
+
+#else // defined(SHARED_LIB)
+
+int main() {
+ printf("Write test completed\n");
+ return 0;
+}
+
+#endif // defined(SHARED_LIB)
+
+// CHECK: Interposed write called: fd={{[0-9]+}}, count={{[0-9]+}}
+// CHECK: Write test completed
>From 82db9fc2cbbfcfd837ed13201cafa230d785e9d0 Mon Sep 17 00:00:00 2001
From: Dan Blackwell <dan_blackwell at apple.com>
Date: Fri, 5 Sep 2025 17:47:30 +0100
Subject: [PATCH 2/2] Fixup
---
compiler-rt/lib/tsan/rtl/tsan_rtl.h | 4 ++++
compiler-rt/test/tsan/Darwin/write-interpose.c | 5 +++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/compiler-rt/lib/tsan/rtl/tsan_rtl.h b/compiler-rt/lib/tsan/rtl/tsan_rtl.h
index 0b6d5f088b142..77390f090f8af 100644
--- a/compiler-rt/lib/tsan/rtl/tsan_rtl.h
+++ b/compiler-rt/lib/tsan/rtl/tsan_rtl.h
@@ -236,6 +236,10 @@ struct alignas(SANITIZER_CACHE_LINE_SIZE) ThreadState {
const ReportDesc *current_report;
+#if SANITIZER_APPLE
+ bool in_internal_write_call;
+#endif
+
explicit ThreadState(Tid tid);
};
diff --git a/compiler-rt/test/tsan/Darwin/write-interpose.c b/compiler-rt/test/tsan/Darwin/write-interpose.c
index d103c1dce0a0d..7e4d50694bdb4 100644
--- a/compiler-rt/test/tsan/Darwin/write-interpose.c
+++ b/compiler-rt/test/tsan/Darwin/write-interpose.c
@@ -4,17 +4,18 @@
// RUN: %clang_tsan %s -o %t
// RUN: %clang_tsan %s -o %t.dylib -fno-sanitize=thread -dynamiclib -DSHARED_LIB
+// Note that running the below command with out `lock_during_write` should
+// deadlock (self-lock)
// RUN: env DYLD_INSERT_LIBRARIES=%t.dylib TSAN_OPTIONS=verbosity=2:lock_during_write=disable_for_current_process %run %t 2>&1 | FileCheck %s
#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
#if defined(SHARED_LIB)
// dylib implementation - interposes write() calls
# include <mach-o/dyld-interposing.h>
# include <os/lock.h>
+# include <unistd.h>
static ssize_t my_write(int fd, const void *buf, size_t count) {
struct os_unfair_lock_s lock = OS_UNFAIR_LOCK_INIT;
More information about the llvm-commits
mailing list