Skip to content

Commit f6612c0

Browse files
committed
[pthread] Don't let a stale "finished" message mark a reused thread in dlopen sync
With dynamic linking, a joinable thread posts CMD_MARK_AS_FINISHED to the main thread as it exits, so that dlopen()'s sync skips it. A join cleans the thread up as soon as it returns, possibly before that message is handled, and a new thread created in the meantime can get the same pthread_t. The late message then marked the new, running thread as finished, so every later dlopen() skipped it and its calls into newly loaded modules trapped. Only mark a thread if it is still known and has actually exited. Fixes: #27841
1 parent f9ce3a1 commit f6612c0

6 files changed

Lines changed: 97 additions & 0 deletions

File tree

‎src/lib/libpthread.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,12 @@ var LibraryPThread = {
12171217
},
12181218
12191219
$markAsFinished: (pthread_ptr) => {
1220+
// When this is reached via a message from the exiting thread, the thread
1221+
// may already have been joined and cleaned up, and its pthread_t reused by
1222+
// a new thread that is still running, which must not be marked.
1223+
if (!PThread.pthreads[pthread_ptr]) return;
1224+
var state = {{{ makeGetValue('pthread_ptr', C_STRUCTS.pthread.detach_state, 'i32') }}};
1225+
if (state != {{{ cDefs.DT_EXITING }}} && state != {{{ cDefs.DT_EXITED }}}) return;
12201226
#if PTHREADS_DEBUG
12211227
dbg(`markAsFinished: ${ptrToString(pthread_ptr)}`);
12221228
#endif

‎src/struct_info_generated.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
"CSIZE": 48,
5656
"DT_CHR": 2,
5757
"DT_DIR": 4,
58+
"DT_EXITED": 0,
59+
"DT_EXITING": 1,
5860
"DT_LNK": 10,
5961
"DT_REG": 8,
6062
"E2BIG": 1,
@@ -1072,6 +1074,7 @@
10721074
},
10731075
"pthread": {
10741076
"__size__": 120,
1077+
"detach_state": 28,
10751078
"profilerBlock": 96,
10761079
"stack": 48,
10771080
"stack_size": 52,

‎src/struct_info_generated_wasm64.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
"CSIZE": 48,
5656
"DT_CHR": 2,
5757
"DT_DIR": 4,
58+
"DT_EXITED": 0,
59+
"DT_EXITING": 1,
5860
"DT_LNK": 10,
5961
"DT_REG": 8,
6062
"E2BIG": 1,
@@ -1072,6 +1074,7 @@
10721074
},
10731075
"pthread": {
10741076
"__size__": 216,
1077+
"detach_state": 52,
10751078
"profilerBlock": 176,
10761079
"stack": 80,
10771080
"stack_size": 88,

‎src/struct_info_internal.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
// ===========================================
55
{
66
"file": "pthread_impl.h",
7+
"defines": [
8+
"DT_EXITED",
9+
"DT_EXITING"
10+
],
711
"structs": {
812
"pthread": [
13+
"detach_state",
914
"profilerBlock",
1015
"stack",
1116
"stack_size",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// A joinable thread's "finished" message only reaches the main thread once it
2+
// returns to the event loop. By then the main thread has joined that thread
3+
// and created a new one that reuses its pthread_t. The late message must not
4+
// stop the new, still running thread from taking part in dlopen()'s sync.
5+
6+
#include <assert.h>
7+
#include <dlfcn.h>
8+
#include <emscripten/eventloop.h>
9+
#include <emscripten/threading.h>
10+
#include <limits.h>
11+
#include <math.h>
12+
#include <pthread.h>
13+
#include <stdio.h>
14+
#include <stdlib.h>
15+
16+
typedef int (*func_t)();
17+
18+
static pthread_t waiter;
19+
static _Atomic int go;
20+
static func_t (*side_func_address)();
21+
static int result;
22+
23+
static void* noop(void* arg) { return NULL; }
24+
25+
static void* wait_then_call(void* arg) {
26+
while (!go) {
27+
emscripten_futex_wait(&go, 0, INFINITY);
28+
}
29+
if (side_func_address) {
30+
result = side_func_address()();
31+
}
32+
return NULL;
33+
}
34+
35+
static void release_waiter() {
36+
go = 1;
37+
emscripten_futex_wake(&go, INT_MAX);
38+
pthread_join(waiter, NULL);
39+
go = 0;
40+
}
41+
42+
static void load_and_call(void* arg) {
43+
void* handle = dlopen("libside.so", RTLD_NOW);
44+
assert(handle);
45+
side_func_address = dlsym(handle, "side_func_address");
46+
assert(side_func_address);
47+
// The waiter traps here if dlopen() skipped it.
48+
release_waiter();
49+
assert(result == 43);
50+
printf("done\n");
51+
exit(0);
52+
}
53+
54+
int main() {
55+
// The new thread almost always gets the joined thread's block back, but
56+
// retry rather than depend on it.
57+
for (int i = 0; i < 10; i++) {
58+
pthread_t joined;
59+
pthread_create(&joined, NULL, noop, NULL);
60+
pthread_join(joined, NULL);
61+
pthread_create(&waiter, NULL, wait_then_call, NULL);
62+
if (pthread_equal(waiter, joined)) {
63+
emscripten_set_timeout(load_and_call, 100, NULL);
64+
return 0;
65+
}
66+
release_waiter();
67+
}
68+
printf("no thread reused a joined thread's pthread_t\n");
69+
return 1;
70+
}

‎test/test_core.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9403,6 +9403,16 @@ def test_pthread_dlopen(self, args):
94039403
['side module ctor', 'done\n', 'side module atexit'],
94049404
assert_all=True, cflags=args)
94059405

9406+
@needs_dylink
9407+
@requires_pthreads
9408+
def test_pthread_dlsync_reused(self):
9409+
self.cflags += ['-Wno-experimental', '-pthread']
9410+
self.build_dlfcn_lib(test_file('core/pthread/test_pthread_dlopen_side.c'))
9411+
self.cflags += ['--embed-file', 'libside.so@libside.so']
9412+
self.prep_dlfcn_main()
9413+
self.set_setting('EXIT_RUNTIME')
9414+
self.do_runf('core/pthread/test_pthread_dlsync_reused.c', 'done\n')
9415+
94069416
@needs_dylink
94079417
@requires_pthreads
94089418
@flaky('https://github.com/emscripten-core/emscripten/issues/18887')

0 commit comments

Comments
 (0)