Skip to content

Commit 3f02985

Browse files
committed
[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 (cherry picked from commit c2c6fed)
1 parent 3487553 commit 3f02985

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
@@ -98,6 +98,10 @@ extern "C" {
9898
mach_msg_type_number_t *infoCnt);
9999
}
100100

101+
// Weak symbol no-op when TSan is not linked
102+
SANITIZER_WEAK_ATTRIBUTE extern void __tsan_set_in_internal_write_call(
103+
bool value) {}
104+
101105
namespace __sanitizer {
102106

103107
#include "sanitizer_syscall_generic.inc"
@@ -168,7 +172,11 @@ uptr internal_read(fd_t fd, void *buf, uptr count) {
168172
}
169173

170174
uptr internal_write(fd_t fd, const void *buf, uptr count) {
171-
return write(fd, buf, count);
175+
// We need to disable interceptors when writing in TSan
176+
__tsan_set_in_internal_write_call(true);
177+
uptr res = write(fd, buf, count);
178+
__tsan_set_in_internal_write_call(false);
179+
return res;
172180
}
173181

174182
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)