Skip to content

Commit 3059251

Browse files
authored
Fixing thread exit and thread cancellation (#12985) (#10524)
This patch fixes thread cancellation/exit method and also unifies why how thread structures are updated when thread is canceled or exits. Following problems are addressed: 1. heap-use-after-free on std::thread #12985 Function `cleanupThread()` from `src/library_pthread.js` unconditionally set `pthread.self` member of pthread structure to 0. Problem was that `cleanupThread()` function might be called after that pthread structure has been deleted. To fix this problem, setting `pthread.self` field is now guarded by check if thread data hasn't been already freed. 2. pthread_cond_wait/2-3 test hang - Disabling recursive thread cancellation. - Allowing __timedwait_cp to be true cancellation point as _cp suffix suggests 3. pthread_getschedparam/1-3 test hangs sometimes: In pthread_barrier_wait adding check if lock is held by main thread and waiting on futex in small slices of time there, to check if there is some work to do on behalf of Worker Threads. Signed-off-by: Adam Bujalski <[email protected]>
1 parent ce48de8 commit 3059251

File tree

7 files changed

+158
-34
lines changed

7 files changed

+158
-34
lines changed

src/library_pthread.js

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,30 @@ var LibraryPThread = {
173173
if (ENVIRONMENT_IS_PTHREAD && _pthread_self()) ___pthread_tsd_run_dtors();
174174
},
175175

176+
runExitHandlersAndDeinitThread: function(tb, exitCode) {
177+
#if PTHREADS_PROFILING
178+
var profilerBlock = Atomics.load(HEAPU32, (tb + {{{ C_STRUCTS.pthread.profilerBlock }}} ) >> 2);
179+
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.profilerBlock }}} ) >> 2, 0);
180+
_free(profilerBlock);
181+
#endif
182+
183+
// Disable all cancellation so that executing the cleanup handlers won't trigger another JS
184+
// canceled exception to be thrown.
185+
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.canceldisable }}} ) >> 2, 1/*PTHREAD_CANCEL_DISABLE*/);
186+
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.cancelasync }}} ) >> 2, 0/*PTHREAD_CANCEL_DEFERRED*/);
187+
PThread.runExitHandlers();
188+
189+
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.threadExitCode }}} ) >> 2, exitCode);
190+
// When we publish this, the main thread is free to deallocate the thread object and we are done.
191+
// Therefore set _pthread_self = 0; above to 'release' the object in this worker thread.
192+
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.threadStatus }}} ) >> 2, 1); // Mark the thread as no longer running.
193+
194+
_emscripten_futex_wake(tb + {{{ C_STRUCTS.pthread.threadStatus }}}, {{{ cDefine('INT_MAX') }}}); // wake all threads
195+
196+
// Not hosting a pthread anymore in this worker, reset the info structures to null.
197+
__emscripten_thread_init(0, 0, 0); // Unregister the thread block also inside the asm.js scope.
198+
},
199+
176200
// Called when we are performing a pthread_exit(), either explicitly called
177201
// by programmer, or implicitly when leaving the thread main function.
178202
threadExit: function(exitCode) {
@@ -181,24 +205,8 @@ var LibraryPThread = {
181205
#if ASSERTIONS
182206
err('Pthread 0x' + tb.toString(16) + ' exited.');
183207
#endif
184-
#if PTHREADS_PROFILING
185-
var profilerBlock = Atomics.load(HEAPU32, (tb + {{{ C_STRUCTS.pthread.profilerBlock }}} ) >> 2);
186-
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.profilerBlock }}} ) >> 2, 0);
187-
_free(profilerBlock);
188-
#endif
189-
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.threadExitCode }}} ) >> 2, exitCode);
190-
// When we publish this, the main thread is free to deallocate the thread object and we are done.
191-
// Therefore set _pthread_self = 0; above to 'release' the object in this worker thread.
192-
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.threadStatus }}} ) >> 2, 1);
193-
194-
// Disable all cancellation so that executing the cleanup handlers won't trigger another JS
195-
// canceled exception to be thrown.
196-
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.canceldisable }}} ) >> 2, 1/*PTHREAD_CANCEL_DISABLE*/);
197-
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.cancelasync }}} ) >> 2, 0/*PTHREAD_CANCEL_DEFERRED*/);
198-
PThread.runExitHandlers();
199-
200-
_emscripten_futex_wake(tb + {{{ C_STRUCTS.pthread.threadStatus }}}, {{{ cDefine('INT_MAX') }}});
201-
__emscripten_thread_init(0, 0, 0); // Unregister the thread block also inside the asm.js scope.
208+
PThread.runExitHandlersAndDeinitThread(tb, exitCode);
209+
202210
if (ENVIRONMENT_IS_PTHREAD) {
203211
// Note: in theory we would like to return any offscreen canvases back to the main thread,
204212
// but if we ever fetched a rendering context for them that would not be valid, so we don't try.
@@ -208,13 +216,7 @@ var LibraryPThread = {
208216
},
209217

210218
threadCancel: function() {
211-
PThread.runExitHandlers();
212-
var tb = _pthread_self();
213-
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.threadExitCode }}} ) >> 2, -1/*PTHREAD_CANCELED*/);
214-
Atomics.store(HEAPU32, (tb + {{{ C_STRUCTS.pthread.threadStatus }}} ) >> 2, 1); // Mark the thread as no longer running.
215-
_emscripten_futex_wake(tb + {{{ C_STRUCTS.pthread.threadStatus }}}, {{{ cDefine('INT_MAX') }}}); // wake all threads
216-
// Not hosting a pthread anymore in this worker, reset the info structures to null.
217-
__emscripten_thread_init(0, 0, 0); // Unregister the thread block also inside the asm.js scope.
219+
PThread.runExitHandlersAndDeinitThread(_pthread_self(), -1/*PTHREAD_CANCELED*/);
218220
postMessage({ 'cmd': 'cancelDone' });
219221
},
220222

@@ -494,9 +496,23 @@ var LibraryPThread = {
494496
$cleanupThread: function(pthread_ptr) {
495497
if (ENVIRONMENT_IS_PTHREAD) throw 'Internal Error! cleanupThread() can only ever be called from main application thread!';
496498
if (!pthread_ptr) throw 'Internal Error! Null pthread_ptr in cleanupThread!';
497-
{{{ makeSetValue('pthread_ptr', C_STRUCTS.pthread.self, 0, 'i32') }}};
498499
var pthread = PThread.pthreads[pthread_ptr];
500+
// If pthread has been removed from this map this also means that pthread_ptr points
501+
// to already freed data. Such situation may occur in following circumstances:
502+
// 1. Joining cancelled thread - in such situation it may happen that pthread data will
503+
// already be removed by handling 'cancelDone' message.
504+
// 2. Joining thread from non-main browser thread (this also includes thread running main()
505+
// when compiled with `PROXY_TO_PTHREAD`) - in such situation it may happen that following
506+
// code flow occur (MB - Main Browser Thread, S1, S2 - Worker Threads):
507+
// S2: thread ends, 'exit' message is sent to MB
508+
// S1: calls pthread_join(S2), this causes:
509+
// a. S2 is marked as detached,
510+
// b. 'cleanupThread' message is sent to MB.
511+
// MB: handles 'exit' message, as thread is detached, so returnWorkerToPool()
512+
// is called and all thread related structs are freed/released.
513+
// MB: handles 'cleanupThread' message which calls this function.
499514
if (pthread) {
515+
{{{ makeSetValue('pthread_ptr', C_STRUCTS.pthread.self, 0, 'i32') }}};
500516
var worker = pthread.worker;
501517
PThread.returnWorkerToPool(worker);
502518
}

system/lib/libc/musl/src/thread/__timedwait.c

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ int __timedwait_cp(volatile int *addr, int val,
4242
#ifdef __EMSCRIPTEN__
4343
double msecsToSleep = top ? (top->tv_sec * 1000 + top->tv_nsec / 1000000.0) : INFINITY;
4444
int is_main_thread = emscripten_is_main_browser_thread();
45-
if (is_main_thread || pthread_self()->cancelasync == PTHREAD_CANCEL_ASYNCHRONOUS) {
45+
// cp suffix in the function name means "cancellation point", so this wait can be cancelled
46+
// by the users unless current threads cancelability is set to PTHREAD_CANCEL_DISABLE
47+
// which may be either done by the user of __timedwait() function.
48+
if (is_main_thread ||
49+
pthread_self()->canceldisable != PTHREAD_CANCEL_DISABLE ||
50+
pthread_self()->cancelasync == PTHREAD_CANCEL_ASYNCHRONOUS) {
4651
double sleepUntilTime = emscripten_get_now() + msecsToSleep;
4752
do {
4853
if (_pthread_isduecanceled(pthread_self())) {

system/lib/libc/musl/src/thread/pthread_barrier_wait.c

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,29 @@ int pthread_barrier_wait(pthread_barrier_t *b)
8787
while (spins-- && !inst->finished)
8888
a_spin();
8989
a_inc(&inst->finished);
90-
while (inst->finished == 1) {
9190
#ifdef __EMSCRIPTEN__
92-
emscripten_futex_wait(&inst->finished, 1, INFINITY);
91+
int is_main_thread = emscripten_is_main_browser_thread();
92+
while (inst->finished == 1) {
93+
if (is_main_thread) {
94+
int e;
95+
do {
96+
// Main thread waits in _very_ small slices so that it stays responsive to assist proxied
97+
// pthread calls.
98+
e = emscripten_futex_wait(&inst->finished, 1, 1);
99+
// Assist other threads by executing proxied operations that are effectively singlethreaded.
100+
emscripten_main_thread_process_queued_calls();
101+
} while(e == -ETIMEDOUT);
102+
} else {
103+
// Can wait in one go.
104+
emscripten_futex_wait(&inst->finished, 1, INFINITY);
105+
}
106+
}
93107
#else
108+
while (inst->finished == 1) {
94109
__syscall(SYS_futex,&inst->finished,FUTEX_WAIT|128,1,0) != -ENOSYS
95110
|| __syscall(SYS_futex,&inst->finished,FUTEX_WAIT,1,0);
96-
#endif
97111
}
112+
#endif
98113
return PTHREAD_BARRIER_SERIAL_THREAD;
99114
}
100115

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2021 The Emscripten Authors. All rights reserved.
2+
// Emscripten is available under two separate licenses, the MIT license and the
3+
// University of Illinois/NCSA Open Source License. Both these licenses can be
4+
// found in the LICENSE file.
5+
6+
#include <pthread.h>
7+
#include <sys/types.h>
8+
#include <stdio.h>
9+
#include <stdlib.h>
10+
#include <assert.h>
11+
#include <unistd.h>
12+
#include <errno.h>
13+
#include <emscripten.h>
14+
#include <emscripten/threading.h>
15+
16+
pthread_barrier_t barrier;
17+
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
18+
pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;
19+
20+
int th_cancelled = 0;
21+
22+
volatile int res = 43;
23+
24+
static void cleanup_handler(void *arg) {
25+
emscripten_log(EM_LOG_CONSOLE, "Called clean-up handler with arg %p", arg);
26+
int a = reinterpret_cast<int>(arg);
27+
res -= a;
28+
29+
pthread_mutex_unlock(&mutex);
30+
pthread_barrier_wait(&barrier);
31+
}
32+
33+
static void *thread_start(void *arg) {
34+
pthread_cleanup_push(cleanup_handler, (void*)42);
35+
emscripten_log(EM_LOG_CONSOLE, "Thread started!");
36+
pthread_mutex_lock(&mutex);
37+
pthread_barrier_wait(&barrier);
38+
39+
int ret = 0;
40+
do {
41+
emscripten_log(EM_LOG_CONSOLE, "Waiting on conditional variable");
42+
ret = pthread_cond_wait(&condvar, &mutex);
43+
} while (ret == 0 && th_cancelled == 0);
44+
45+
if (ret != 0) {
46+
emscripten_log(EM_LOG_CONSOLE, "Cond wait failed ret: %d", ret);
47+
}
48+
49+
res = 1000; // Shouldn't ever reach here.
50+
pthread_cleanup_pop(0);
51+
52+
pthread_mutex_unlock(&mutex);
53+
pthread_barrier_wait(&barrier);
54+
return NULL;
55+
}
56+
57+
int main() {
58+
if (!emscripten_has_threading_support()) {
59+
printf("Skipped: Threading is not supported.\n");
60+
return 1;
61+
}
62+
63+
pthread_barrier_init(&barrier, nullptr, 2);
64+
65+
pthread_t thr;
66+
int s = pthread_create(&thr, NULL, thread_start, (void*)0);
67+
assert(s == 0);
68+
emscripten_log(EM_LOG_CONSOLE, "Thread created");
69+
70+
pthread_barrier_wait(&barrier);
71+
72+
// Lock mutex to ensure that thread is waiting
73+
pthread_mutex_lock(&mutex);
74+
75+
emscripten_log(EM_LOG_CONSOLE, "Canceling thread..");
76+
s = pthread_cancel(thr);
77+
assert(s == 0);
78+
th_cancelled = 1;
79+
pthread_mutex_unlock(&mutex);
80+
81+
emscripten_log(EM_LOG_CONSOLE, "Main thread waitnig for side-thread");
82+
pthread_barrier_wait(&barrier);
83+
pthread_barrier_destroy(&barrier);
84+
85+
emscripten_log(EM_LOG_CONSOLE, "Test finished result: %d", res);
86+
return res;
87+
}

tests/test_browser.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3771,6 +3771,11 @@ def test_std_thread_detach(self):
37713771
def test_pthread_cancel(self):
37723772
self.btest(path_from_root('tests', 'pthread', 'test_pthread_cancel.cpp'), expected='1', args=['-O3', '-s', 'USE_PTHREADS', '-s', 'PTHREAD_POOL_SIZE=8'])
37733773

3774+
# Test that pthread_cancel() cancels pthread_cond_wait() operation
3775+
@requires_threads
3776+
def test_pthread_cancel_cond_wait(self):
3777+
self.btest_exit(path_from_root('tests', 'pthread', 'test_pthread_cancel_cond_wait.cpp'), expected='1', args=['-O3', '-s', 'USE_PTHREADS=1', '-s', 'PTHREAD_POOL_SIZE=8'])
3778+
37743779
# Test pthread_kill() operation
37753780
@no_chrome('pthread_kill hangs chrome renderer, and keep subsequent tests from passing')
37763781
@requires_threads

tests/test_core.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8148,7 +8148,6 @@ def test_pthread_c11_threads(self):
81488148
self.set_setting('INITIAL_MEMORY', '64mb')
81498149
self.do_run_in_out_file_test('tests', 'pthread', 'test_pthread_c11_threads.c')
81508150

8151-
@no_asan('flakey errors that must be fixed, https://github.com/emscripten-core/emscripten/issues/12985')
81528151
@node_pthreads
81538152
def test_pthread_cxx_threads(self):
81548153
self.set_setting('PROXY_TO_PTHREAD')

tests/test_posixtest.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,7 @@ def get_pthread_tests():
147147
**flaky,
148148
'test_pthread_create_11_1': 'never returns',
149149
'test_pthread_barrier_wait_2_1': 'never returns',
150-
'test_pthread_cond_timedwait_2_6': 'never returns',
151-
'test_pthread_cond_timedwait_4_3': 'never returns',
152150
'test_pthread_attr_setscope_5_1': 'internally skipped (PTS_UNTESTED)',
153-
'test_pthread_cond_wait_2_3': 'never returns',
154151
'test_pthread_create_5_1': 'never returns',
155152
'test_pthread_exit_1_2': 'never returns',
156153
'test_pthread_exit_2_2': 'never returns',

0 commit comments

Comments
 (0)