Skip to content

Commit 9abd6f2

Browse files
authored
[TSan] Add lock_during_write flag on Apple platforms to avoid deadlock (#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
1 parent 0c2e900 commit 9abd6f2

File tree

9 files changed

+154
-2
lines changed

9 files changed

+154
-2
lines changed

compiler-rt/lib/sanitizer_common/sanitizer_mac.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ extern "C" {
105105
mach_msg_type_number_t *infoCnt);
106106
}
107107

108+
// Weak symbol no-op when TSan is not linked
109+
SANITIZER_WEAK_ATTRIBUTE extern void __tsan_set_in_internal_write_call(
110+
bool value) {}
111+
108112
namespace __sanitizer {
109113

110114
#include "sanitizer_syscall_generic.inc"
@@ -175,7 +179,11 @@ uptr internal_read(fd_t fd, void *buf, uptr count) {
175179
}
176180

177181
uptr internal_write(fd_t fd, const void *buf, uptr count) {
178-
return write(fd, buf, count);
182+
// We need to disable interceptors when writing in TSan
183+
__tsan_set_in_internal_write_call(true);
184+
uptr res = write(fd, buf, count);
185+
__tsan_set_in_internal_write_call(false);
186+
return res;
179187
}
180188

181189
uptr internal_stat(const char *path, void *buf) {

compiler-rt/lib/tsan/rtl/tsan_flags.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,43 @@
2020
#include "tsan_rtl.h"
2121
#include "ubsan/ubsan_flags.h"
2222

23+
#if SANITIZER_APPLE
24+
namespace __sanitizer {
25+
26+
template <>
27+
inline bool FlagHandler<LockDuringWriteSetting>::Parse(const char *value) {
28+
if (internal_strcmp(value, "on") == 0) {
29+
*t_ = kLockDuringAllWrites;
30+
return true;
31+
}
32+
if (internal_strcmp(value, "disable_for_current_process") == 0) {
33+
*t_ = kNoLockDuringWritesCurrentProcess;
34+
return true;
35+
}
36+
if (internal_strcmp(value, "disable_for_all_processes") == 0) {
37+
*t_ = kNoLockDuringWritesAllProcesses;
38+
return true;
39+
}
40+
Printf("ERROR: Invalid value for signal handler option: '%s'\n", value);
41+
return false;
42+
}
43+
44+
template <>
45+
inline bool FlagHandler<LockDuringWriteSetting>::Format(char *buffer,
46+
uptr size) {
47+
switch (*t_) {
48+
case kLockDuringAllWrites:
49+
return FormatString(buffer, size, "on");
50+
case kNoLockDuringWritesCurrentProcess:
51+
return FormatString(buffer, size, "disable_for_current_process");
52+
case kNoLockDuringWritesAllProcesses:
53+
return FormatString(buffer, size, "disable_for_all_processes");
54+
}
55+
}
56+
57+
} // namespace __sanitizer
58+
#endif
59+
2360
namespace __tsan {
2461

2562
// Can be overriden in frontend.

compiler-rt/lib/tsan/rtl/tsan_flags.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
#include "sanitizer_common/sanitizer_flags.h"
1717
#include "sanitizer_common/sanitizer_deadlock_detector_interface.h"
1818

19+
#if SANITIZER_APPLE
20+
enum LockDuringWriteSetting {
21+
kLockDuringAllWrites,
22+
kNoLockDuringWritesCurrentProcess,
23+
kNoLockDuringWritesAllProcesses,
24+
};
25+
#endif
26+
1927
namespace __tsan {
2028

2129
struct Flags : DDFlags {

compiler-rt/lib/tsan/rtl/tsan_flags.inc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,15 @@ TSAN_FLAG(bool, shared_ptr_interceptor, true,
8080
TSAN_FLAG(bool, print_full_thread_history, false,
8181
"If set, prints thread creation stacks for the threads involved in "
8282
"the report and their ancestors up to the main thread.")
83+
84+
#if SANITIZER_APPLE
85+
TSAN_FLAG(LockDuringWriteSetting, lock_during_write, kLockDuringAllWrites,
86+
"Determines whether to obtain a lock while writing logs or error "
87+
"reports. "
88+
"\"on\" - [default] lock during all writes. "
89+
"\"disable_for_current_process\" - don't lock during all writes in "
90+
"the current process, but do lock for all writes in child "
91+
"processes."
92+
"\"disable_for_all_processes\" - don't lock during all writes in "
93+
"the current process and it's children processes.")
94+
#endif

compiler-rt/lib/tsan/rtl/tsan_interceptors.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#ifndef TSAN_INTERCEPTORS_H
22
#define TSAN_INTERCEPTORS_H
33

4+
#if SANITIZER_APPLE
5+
# include "sanitizer_common/sanitizer_mac.h"
6+
#endif
47
#include "sanitizer_common/sanitizer_stacktrace.h"
58
#include "tsan_rtl.h"
69

@@ -43,7 +46,12 @@ inline bool in_symbolizer() {
4346
#endif
4447

4548
inline bool MustIgnoreInterceptor(ThreadState *thr) {
46-
return !thr->is_inited || thr->ignore_interceptors || thr->in_ignored_lib;
49+
return !thr->is_inited || thr->ignore_interceptors || thr->in_ignored_lib
50+
#if SANITIZER_APPLE
51+
|| (flags()->lock_during_write != kLockDuringAllWrites &&
52+
thr->in_internal_write_call)
53+
#endif
54+
;
4755
}
4856

4957
} // namespace __tsan

compiler-rt/lib/tsan/rtl/tsan_interceptors_posix.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
#include "sanitizer_common/sanitizer_tls_get_addr.h"
3232
#include "sanitizer_common/sanitizer_vector.h"
3333
#include "tsan_fd.h"
34+
#if SANITIZER_APPLE
35+
# include "tsan_flags.h"
36+
#endif
3437
#include "tsan_interceptors.h"
3538
#include "tsan_interface.h"
3639
#include "tsan_mman.h"
@@ -1665,6 +1668,14 @@ TSAN_INTERCEPTOR(int, pthread_barrier_wait, void *b) {
16651668

16661669
TSAN_INTERCEPTOR(int, pthread_once, void *o, void (*f)()) {
16671670
SCOPED_INTERCEPTOR_RAW(pthread_once, o, f);
1671+
#if SANITIZER_APPLE
1672+
if (flags()->lock_during_write != kLockDuringAllWrites &&
1673+
cur_thread_init()->in_internal_write_call) {
1674+
// This is needed to make it through process launch without hanging
1675+
f();
1676+
return 0;
1677+
}
1678+
#endif
16681679
if (o == 0 || f == 0)
16691680
return errno_EINVAL;
16701681
atomic_uint32_t *a;

compiler-rt/lib/tsan/rtl/tsan_rtl.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ SANITIZER_WEAK_DEFAULT_IMPL
4040
void __tsan_test_only_on_fork() {}
4141
#endif
4242

43+
#if SANITIZER_APPLE
44+
// Override weak symbol from sanitizer_common
45+
extern void __tsan_set_in_internal_write_call(bool value) {
46+
__tsan::cur_thread_init()->in_internal_write_call = value;
47+
}
48+
#endif
49+
4350
namespace __tsan {
4451

4552
#if !SANITIZER_GO
@@ -893,6 +900,13 @@ void ForkChildAfter(ThreadState* thr, uptr pc, bool start_thread) {
893900
ThreadIgnoreBegin(thr, pc);
894901
ThreadIgnoreSyncBegin(thr, pc);
895902
}
903+
904+
# if SANITIZER_APPLE
905+
// This flag can have inheritance disabled - we are the child so act
906+
// accordingly
907+
if (flags()->lock_during_write == kNoLockDuringWritesCurrentProcess)
908+
flags()->lock_during_write = kLockDuringAllWrites;
909+
# endif
896910
}
897911
#endif
898912

compiler-rt/lib/tsan/rtl/tsan_rtl.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ struct alignas(SANITIZER_CACHE_LINE_SIZE) ThreadState {
236236

237237
const ReportDesc *current_report;
238238

239+
#if SANITIZER_APPLE
240+
bool in_internal_write_call;
241+
#endif
242+
239243
explicit ThreadState(Tid tid);
240244
};
241245

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Test that dylibs interposing write, and then calling functions intercepted
2+
// by TSan don't deadlock (self-lock)
3+
4+
// RUN: %clang_tsan %s -o %t
5+
// RUN: %clang_tsan %s -o %t.dylib -fno-sanitize=thread -dynamiclib -DSHARED_LIB
6+
7+
// Note that running the below command with out `lock_during_write` should
8+
// deadlock (self-lock)
9+
// RUN: env DYLD_INSERT_LIBRARIES=%t.dylib TSAN_OPTIONS=verbosity=2:lock_during_write=disable_for_current_process %run %t 2>&1 | FileCheck %s
10+
11+
#include <stdio.h>
12+
13+
#if defined(SHARED_LIB)
14+
15+
// dylib implementation - interposes write() calls
16+
# include <os/lock.h>
17+
# include <unistd.h>
18+
19+
struct interpose_substitution {
20+
const void *replacement;
21+
const void *original;
22+
};
23+
24+
# define INTERPOSE(replacement, original) \
25+
__attribute__((used)) static const struct interpose_substitution \
26+
substitution_##original[] \
27+
__attribute__((section("__DATA, __interpose"))) = { \
28+
{(const void *)(replacement), (const void *)(original)}}
29+
30+
static ssize_t my_write(int fd, const void *buf, size_t count) {
31+
struct os_unfair_lock_s lock = OS_UNFAIR_LOCK_INIT;
32+
os_unfair_lock_lock(&lock);
33+
printf("Interposed write called: fd=%d, count=%zu\n", fd, count);
34+
ssize_t res = write(fd, buf, count);
35+
os_unfair_lock_unlock(&lock);
36+
return res;
37+
}
38+
INTERPOSE(my_write, write);
39+
40+
#else // defined(SHARED_LIB)
41+
42+
int main() {
43+
printf("Write test completed\n");
44+
return 0;
45+
}
46+
47+
#endif // defined(SHARED_LIB)
48+
49+
// CHECK: Interposed write called: fd={{[0-9]+}}, count={{[0-9]+}}
50+
// CHECK: Write test completed

0 commit comments

Comments
 (0)