Repository navigation
Release the task queue lock while running cancel handlers - #28047
clementperon wants to merge 3 commits into
Conversation
em_task_queue_cancel ran cancel handlers with the queue lock held. A handler can block, and a blocked thread processes its dlopen queue, which can be the queue being cancelled: a thread exiting during dlopen() then deadlocks on its own lock. em_task_queue_execute already unlocks around tasks. See emscripten-core#18887.
aeedef9 to
8b60e22
Compare
There was a problem hiding this comment.
Code Review
This pull request updates em_task_queue_cancel in system/lib/pthread/em_task_queue.c to unlock the queue's mutex before executing the task's cancel handler and lock it again afterward, preventing potential deadlocks if the handler blocks. There are no review comments, so I have no feedback to provide.
|
@sbc100 Release build:
Build with -fsanitize=address -sASSERTIONS=2
|
|
Thanks @clementperon for tracking down this flakiness and fixing it. @tlively does the fix looks reasonable? |
|
Seems good directionally, but Gemini found a problem. If there is a re-entrant Gemini suggests this additional test for that problem (edit: new test that does not depend on proxying queue internals): #include <assert.h>
#include <dlfcn.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#define NUM_ITERATIONS 100
static pthread_barrier_t barrier;
static _Atomic bool stop = false;
// Worker thread that repeatedly starts and immediately exits while dlopen()s occur
static void* exiting_worker(void* arg) {
pthread_barrier_wait(&barrier);
// Thread exits immediately while dynamic link synchronization calls are in-flight
return NULL;
}
// Loader thread that continuously dlopen()s libraries to generate pending dlsync tasks
static void* loader_thread(void* arg) {
pthread_barrier_wait(&barrier);
while (!stop) {
void* handle = dlopen("libside.so", RTLD_NOW | RTLD_GLOBAL);
assert(handle && "dlopen failed");
dlclose(handle);
}
return NULL;
}
int main() {
pthread_barrier_init(&barrier, NULL, 2);
pthread_t loader;
pthread_create(&loader, NULL, loader_thread, NULL);
for (int i = 0; i < NUM_ITERATIONS; ++i) {
pthread_t worker;
pthread_create(&worker, NULL, exiting_worker, NULL);
pthread_join(worker, NULL);
}
stop = true;
pthread_join(loader, NULL);
pthread_barrier_destroy(&barrier);
printf("PASSED\n");
return 0;
}Side module (libside.c): int side_func() {
return 42;
}AI explanation of the test (I haven't vetted it):
|
|
I deep dive into this and indeed it can happen, seems that a blocking cancellation handler can re-enter the dlopen queue and execute the remaining task. |
tlively
left a comment
There was a problem hiding this comment.
Can you add the test as well?
em_task_queue_cancelruns cancel handlers with the queue lock held. A handler can block (cancel_ctxproxies back to the sender), and a blocked thread processes its dlopen queue from_emscripten_yield. When that is the queue being cancelled, a thread exiting duringdlopen()deadlocks on its own lock:This is the assertion
test_pthread_dlopen_manyhits in #18887.em_task_queue_executealready releases the lock around each task; this does the same for cancel handlers.Stress test with threads created and joined while another thread
dlopen()s side modules (Node, 6.0.9): 15/300 runs hang or assert without this change, 0/600 with it.🤖 Generated with Claude Code