Skip to content

Commit e202bb9

Browse files
quantum5belraquib
authored andcommitted
Add multithread support to ASan (emscripten-core#9076)
1 parent 3298e36 commit e202bb9

15 files changed

+167
-16
lines changed

emcc.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,9 +1544,6 @@ def check(input_file):
15441544
# by SAFE_HEAP as a null pointer dereference.
15451545
exit_with_error('ASan does not work with SAFE_HEAP')
15461546

1547-
if shared.Settings.USE_PTHREADS:
1548-
exit_with_error('ASan currently does not support threads')
1549-
15501547
if sanitize and '-g4' in args:
15511548
shared.Settings.LOAD_SOURCE_MAP = 1
15521549

src/library_pthread.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,8 @@ var LibraryPThread = {
507507
{{{ makeSetValue('__num_logical_cores', 0, 'cores', 'i32') }}};
508508
},
509509

510-
{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_create__deps: ['_spawn_thread', 'pthread_getschedparam', 'pthread_self', 'memalign'],
511-
{{{ USE_LSAN ? 'emscripten_builtin_' : '' }}}pthread_create: function(pthread_ptr, attr, start_routine, arg) {
510+
{{{ USE_LSAN || USE_ASAN ? 'emscripten_builtin_' : '' }}}pthread_create__deps: ['_spawn_thread', 'pthread_getschedparam', 'pthread_self', 'memalign'],
511+
{{{ USE_LSAN || USE_ASAN ? 'emscripten_builtin_' : '' }}}pthread_create: function(pthread_ptr, attr, start_routine, arg) {
512512
if (typeof SharedArrayBuffer === 'undefined') {
513513
err('Current environment does not support SharedArrayBuffer, pthreads are not available!');
514514
return {{{ cDefine('EAGAIN') }}};

system/lib/compiler-rt/lib/asan/asan_emscripten.cc

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
#include "asan_interceptors.h"
12
#include "asan_internal.h"
23
#include "asan_mapping.h"
34
#include "asan_poisoning.h"
5+
#include "asan_stack.h"
46
#include "asan_thread.h"
57

68
#if SANITIZER_EMSCRIPTEN
@@ -29,6 +31,74 @@ void *AsanDoesNotSupportStaticLinkage() {
2931

3032
void InitializeAsanInterceptors() {}
3133

34+
// We can use a plain thread_local variable for TSD.
35+
static thread_local void *per_thread;
36+
37+
void *AsanTSDGet() { return per_thread; }
38+
39+
void AsanTSDSet(void *tsd) { per_thread = tsd; }
40+
41+
// There's no initialization needed, and the passed-in destructor
42+
// will never be called. Instead, our own thread destruction hook
43+
// (below) will call AsanThread::TSDDtor directly.
44+
void AsanTSDInit(void (*destructor)(void *tsd)) {
45+
DCHECK(destructor == &PlatformTSDDtor);
46+
}
47+
48+
void PlatformTSDDtor(void *tsd) { UNREACHABLE(__func__); }
49+
50+
extern "C" {
51+
void *emscripten_builtin_malloc(size_t size);
52+
void emscripten_builtin_free(void *memory);
53+
int emscripten_builtin_pthread_create(void *thread, void *attr,
54+
void *(*callback)(void *), void *arg);
55+
int pthread_attr_getdetachstate(void *attr, int *detachstate);
56+
}
57+
58+
static thread_return_t THREAD_CALLING_CONV asan_thread_start(void *arg) {
59+
atomic_uintptr_t *param = reinterpret_cast<atomic_uintptr_t *>(arg);
60+
AsanThread *t = nullptr;
61+
while ((t = reinterpret_cast<AsanThread *>(
62+
atomic_load(param, memory_order_acquire))) == nullptr)
63+
internal_sched_yield();
64+
emscripten_builtin_free(param);
65+
SetCurrentThread(t);
66+
return t->ThreadStart(GetTid(), nullptr);
67+
}
68+
69+
INTERCEPTOR(int, pthread_create, void *thread,
70+
void *attr, void *(*start_routine)(void*), void *arg) {
71+
EnsureMainThreadIDIsCorrect();
72+
// Strict init-order checking is thread-hostile.
73+
if (flags()->strict_init_order)
74+
StopInitOrderChecking();
75+
GET_STACK_TRACE_THREAD;
76+
int detached = 0;
77+
if (attr)
78+
pthread_attr_getdetachstate(attr, &detached);
79+
atomic_uintptr_t *param = (atomic_uintptr_t *)
80+
emscripten_builtin_malloc(sizeof(atomic_uintptr_t));
81+
atomic_store(param, 0, memory_order_relaxed);
82+
int result;
83+
{
84+
// Ignore all allocations made by pthread_create: thread stack/TLS may be
85+
// stored by pthread for future reuse even after thread destruction, and
86+
// the linked list it's stored in doesn't even hold valid pointers to the
87+
// objects, the latter are calculated by obscure pointer arithmetic.
88+
#if CAN_SANITIZE_LEAKS
89+
__lsan::ScopedInterceptorDisabler disabler;
90+
#endif
91+
result = REAL(pthread_create)(thread, attr, asan_thread_start, param);
92+
}
93+
if (result == 0) {
94+
u32 current_tid = GetCurrentTidOrInvalid();
95+
AsanThread *t =
96+
AsanThread::Create(start_routine, arg, current_tid, &stack, detached);
97+
atomic_store(param, reinterpret_cast<uptr>(t), memory_order_release);
98+
}
99+
return result;
100+
}
101+
32102
} // namespace __asan
33103

34104
namespace __lsan {

system/lib/compiler-rt/lib/asan/asan_posix.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ void PlatformTSDDtor(void *tsd) {
8484
atomic_signal_fence(memory_order_seq_cst);
8585
AsanThread::TSDDtor(tsd);
8686
}
87-
#else
87+
#elif !SANITIZER_EMSCRIPTEN
8888
static pthread_key_t tsd_key;
8989
static bool tsd_key_inited = false;
9090
void AsanTSDInit(void (*destructor)(void *tsd)) {

system/lib/compiler-rt/lib/asan/asan_thread.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ struct DTLS;
3030
namespace __asan {
3131

3232
const u32 kInvalidTid = 0xffffff; // Must fit into 24 bits.
33-
#if SANITIZER_EMSCRIPTEN
33+
#if SANITIZER_EMSCRIPTEN && !defined(USE_THREADS)
3434
const u32 kMaxNumberOfThreads = 1;
35+
#elif SANITIZER_EMSCRIPTEN
36+
const u32 kMaxNumberOfThreads = 128;
3537
#else
3638
const u32 kMaxNumberOfThreads = (1 << 22); // 4M
3739
#endif

system/lib/compiler-rt/lib/lsan/lsan_common_emscripten.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,11 @@ void ProcessThreads(SuspendedThreadsList const &suspended_threads,
172172

173173
} // namespace __lsan
174174

175+
extern "C" void __lsan_disable_in_this_thread() {
176+
__lsan::DisableInThisThread();
177+
}
178+
179+
extern "C" void __lsan_enable_in_this_thread() {
180+
__lsan::EnableInThisThread();
181+
}
175182
#endif // CAN_SANITIZE_LEAKS && SANITIZER_EMSCRIPTEN

system/lib/compiler-rt/lib/sanitizer_common/sanitizer_linux.cc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,6 @@ uptr internal_rename(const char *oldpath, const char *newpath) {
434434

435435
uptr internal_sched_yield() {
436436
#if SANITIZER_EMSCRIPTEN
437-
Report("WARNING: sched_yield doesn't do anything on emscripten");
438437
return 0;
439438
#else
440439
return internal_syscall(SYSCALL(sched_yield));

system/lib/libc/musl/src/misc/emscripten_pthread.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ EM_JS(void, initPthreadsJS, (void), {
2424
PThread.initRuntime();
2525
})
2626

27+
// This must run before any userland ctors
28+
// Note that ASan constructor priority is 50, and we must be higher.
2729
EMSCRIPTEN_KEEPALIVE
28-
__attribute__((constructor(99))) // This must run before any userland ctors
30+
__attribute__((constructor(48)))
2931
void __emscripten_pthread_data_constructor(void) {
3032
initPthreadsJS();
3133
pthread_self()->locale = &libc.global_locale;

system/lib/pthread/library_pthread.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,37 @@ void emscripten_async_waitable_close(em_queued_call* call) { em_queued_call_free
188188

189189
extern double emscripten_receive_on_main_thread_js(int, int, double*);
190190

191+
#if defined(__has_feature)
192+
#if __has_feature(address_sanitizer)
193+
#define HAS_ASAN
194+
void __lsan_disable_in_this_thread(void);
195+
void __lsan_enable_in_this_thread(void);
196+
int emscripten_builtin_pthread_create(void *thread, void *attr,
197+
void *(*callback)(void *), void *arg);
198+
#endif
199+
#endif
200+
191201
static void _do_call(em_queued_call* q) {
192202
if (!q->js) {
193203
// C function pointer
194204
assert(EM_FUNC_SIG_NUM_FUNC_ARGUMENTS(q->functionEnum) <= EM_QUEUED_CALL_MAX_ARGS);
195205
switch (q->functionEnum) {
196206
case EM_PROXIED_PTHREAD_CREATE:
207+
#ifdef HAS_ASAN
208+
// ASan wraps the emscripten_builtin_pthread_create call in __lsan::ScopedInterceptorDisabler.
209+
// Unfortunately, that only disables it on the thread that made the call.
210+
// This is sufficient on the main thread.
211+
// On non-main threads, pthread_create gets proxied to the main thread, where LSan is not
212+
// disabled. This makes it necessary for us to disable LSan here, so that it does not detect
213+
// pthread's internal allocations as leaks.
214+
__lsan_disable_in_this_thread();
215+
q->returnValue.i =
216+
emscripten_builtin_pthread_create(q->args[0].vp, q->args[1].vp, q->args[2].vp, q->args[3].vp);
217+
__lsan_enable_in_this_thread();
218+
#else
197219
q->returnValue.i =
198220
pthread_create(q->args[0].vp, q->args[1].vp, q->args[2].vp, q->args[3].vp);
221+
#endif
199222
break;
200223
case EM_PROXIED_SYSCALL:
201224
q->returnValue.i = emscripten_syscall(q->args[0].i, q->args[1].vp);

system/lib/pthread/library_pthread_wasm.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ extern void __wasm_init_tls(void *memory);
173173
void *emscripten_builtin_memalign(size_t align, size_t size);
174174
void emscripten_builtin_free(void *memory);
175175

176-
__attribute__((constructor(100)))
176+
// Note that ASan constructor priority is 50, and we must be higher.
177+
__attribute__((constructor(49)))
177178
void EMSCRIPTEN_KEEPALIVE emscripten_tls_init(void) {
178179
size_t tls_size = __builtin_wasm_tls_size();
179180
size_t tls_align = __builtin_wasm_tls_align();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#include <cstdio>
2+
#include <thread>
3+
4+
std::atomic<bool> thread_done;
5+
6+
void f(int *a) {
7+
delete [] a;
8+
a[0] = 1;
9+
std::atomic_store(&thread_done, true);
10+
}
11+
12+
int main(int argc, char **argv) {
13+
std::thread t(f, new int[10]);
14+
t.detach();
15+
while (!std::atomic_load(&thread_done));
16+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
(function () {
2+
var output = [];
3+
Module['printErr'] = function (text) {
4+
if (text == '==42==ABORTING') {
5+
var result = output.join('\n');
6+
var passed = [
7+
'ERROR: AddressSanitizer: heap-use-after-free on address',
8+
'WRITE of size 4',
9+
'is located 0 bytes inside of 40-byte region',
10+
'freed by thread T2 here:',
11+
'previously allocated by thread T1 here:',
12+
'Thread T2 created by T1 here:',
13+
'SUMMARY: AddressSanitizer: heap-use-after-free',
14+
'Shadow bytes around the buggy address:',
15+
'Shadow byte legend (one shadow byte represents 8 application bytes):',
16+
].every(function (snippet) {
17+
return result.indexOf(snippet) >= 0;
18+
});
19+
reportResultToServer(passed ? 1 : 0);
20+
return;
21+
}
22+
output.push(text);
23+
console.log(text);
24+
};
25+
})();

tests/pthread/test_pthread_lsan_leak.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
'Direct leak of 42 byte(s) in 1 object(s) allocated from',
1818
'test_pthread_lsan_leak.cpp:12:21',
1919
'test_pthread_lsan_leak.cpp:38:3',
20-
'SUMMARY: LeakSanitizer: 8513 byte(s) leaked in 6 allocation(s).',
20+
'8513 byte(s) leaked in 6 allocation(s).',
2121
''
2222
].every(function (snippet) {
2323
return result.indexOf(snippet) >= 0;

tests/test_browser.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3916,6 +3916,20 @@ def test_pthread_safe_stack(self):
39163916
def test_pthread_lsan(self, name, args=[]):
39173917
self.btest(path_from_root('tests', 'pthread', name + '.cpp'), expected='1', args=['-fsanitize=leak', '-s', 'TOTAL_MEMORY=256MB', '-s', 'USE_PTHREADS', '-s', 'PROXY_TO_PTHREAD', '-std=c++11', '--pre-js', path_from_root('tests', 'pthread', name + '.js')] + args)
39183918

3919+
@parameterized({
3920+
# Reusing the LSan test files for ASan.
3921+
'leak': ['test_pthread_lsan_leak', ['-g4']],
3922+
'no_leak': ['test_pthread_lsan_no_leak'],
3923+
})
3924+
@no_fastcomp('ASan is only supported on WASM backend')
3925+
@requires_threads
3926+
def test_pthread_asan(self, name, args=[]):
3927+
self.btest(path_from_root('tests', 'pthread', name + '.cpp'), expected='1', args=['-fsanitize=address', '-s', 'TOTAL_MEMORY=256MB', '-s', 'USE_PTHREADS', '-s', 'PROXY_TO_PTHREAD', '-std=c++11', '--pre-js', path_from_root('tests', 'pthread', name + '.js')] + args)
3928+
3929+
@no_fastcomp('ASan is only supported on WASM backend')
3930+
def test_pthread_asan_use_after_free(self):
3931+
self.btest(path_from_root('tests', 'pthread', 'test_pthread_asan_use_after_free.cpp'), expected='1', args=['-fsanitize=address', '-s', 'TOTAL_MEMORY=256MB', '-s', 'USE_PTHREADS', '-s', 'PROXY_TO_PTHREAD', '-std=c++11', '--pre-js', path_from_root('tests', 'pthread', 'test_pthread_asan_use_after_free.js')])
3932+
39193933
# Tests MAIN_THREAD_EM_ASM_INT() function call signatures.
39203934
@no_wasm_backend('MAIN_THREAD_EM_ASM() not yet implemented in Wasm backend')
39213935
def test_main_thread_em_asm_signatures(self):

tools/system_libs.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -528,11 +528,6 @@ def vary_on(cls):
528528
vary_on += ['is_asan']
529529
return vary_on
530530

531-
@classmethod
532-
def variations(cls):
533-
return [variation for variation in super(AsanInstrumentedLibrary, cls).variations()
534-
if not shared.Settings.WASM_BACKEND or not variation['is_asan'] or not variation.get('is_mt')]
535-
536531
@classmethod
537532
def get_default_variation(cls, **kwargs):
538533
return super(AsanInstrumentedLibrary, cls).get_default_variation(is_asan=shared.Settings.USE_ASAN, **kwargs)

0 commit comments

Comments
 (0)