From 9d120b0428b4d6b2bbea34d6f04124b22051f97f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Tue, 28 Oct 2025 16:07:12 +0200 Subject: [PATCH 1/3] Fix a deadlock issue with emscripten_lock_async_acquire() if user attempted to synchronously acquire the lock right after asynchronously acquiring it. https://github.com/emscripten-core/emscripten/commit/b25abd56ca6cd1947b5013a759064634bb0917ff#r168975511 --- src/lib/libwasm_worker.js | 13 ++++----- test/test_browser.py | 6 ++++ .../wasm_worker/lock_async_and_sync_acquire.c | 29 +++++++++++++++++++ 3 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 test/wasm_worker/lock_async_and_sync_acquire.c diff --git a/src/lib/libwasm_worker.js b/src/lib/libwasm_worker.js index 8e9162ecd3aca..162164072b98b 100644 --- a/src/lib/libwasm_worker.js +++ b/src/lib/libwasm_worker.js @@ -300,24 +300,21 @@ if (ENVIRONMENT_IS_WASM_WORKER emscripten_lock_async_acquire__deps: ['$polyfillWaitAsync'], emscripten_lock_async_acquire: (lock, asyncWaitFinished, userData, maxWaitMilliseconds) => { - let dispatch = (val, ret) => { - setTimeout(() => { - {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(lock, val, /*waitResult=*/ret, userData); - }, 0); - }; let tryAcquireLock = () => { do { var val = Atomics.compareExchange(HEAP32, {{{ getHeapOffset('lock', 'i32') }}}, 0/*zero represents lock being free*/, 1/*one represents lock being acquired*/); - if (!val) return dispatch(0, 0/*'ok'*/); + if (!val) return {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(lock, 0, 0/*'ok'*/, userData); var wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('lock', 'i32') }}}, val, maxWaitMilliseconds); } while (wait.value === 'not-equal'); #if ASSERTIONS assert(wait.async || wait.value === 'timed-out'); #endif if (wait.async) wait.value.then(tryAcquireLock); - else dispatch(val, 2/*'timed-out'*/); + else return {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(lock, val, 2/*'timed-out'*/, userData); }; - tryAcquireLock(); + // Asynchronously dispatch acquiring the lock so that we have uniform control flow in both + // cases when the lock is acquired, and when it needs to wait. + setTimeout(tryAcquireLock); }, emscripten_semaphore_async_acquire__deps: ['$polyfillWaitAsync'], diff --git a/test/test_browser.py b/test/test_browser.py index 224fd4af8976b..10945ad891164 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5329,6 +5329,12 @@ def test_wasm_worker_lock_wait2(self): def test_wasm_worker_lock_async_acquire(self): self.btest_exit('wasm_worker/lock_async_acquire.c', cflags=['--closure=1', '-sWASM_WORKERS']) + # Tests emscripten_lock_async_acquire() function when lock is acquired both synchronously and asynchronously. + @also_with_minimal_runtime + @flaky('https://github.com/emscripten-core/emscripten/issues/25270') + def test_wasm_worker_lock_async_and_sync_acquire(self): + self.btest('wasm_worker/lock_async_and_sync_acquire.c', expected='1', cflags=['--closure=1', '-sWASM_WORKERS']) + # Tests emscripten_lock_busyspin_wait_acquire() in Worker and main thread. @also_with_minimal_runtime def test_wasm_worker_lock_busyspin_wait(self): diff --git a/test/wasm_worker/lock_async_and_sync_acquire.c b/test/wasm_worker/lock_async_and_sync_acquire.c new file mode 100644 index 0000000000000..7bcf7882b2a14 --- /dev/null +++ b/test/wasm_worker/lock_async_and_sync_acquire.c @@ -0,0 +1,29 @@ +#include +#include +#include + +emscripten_lock_t lock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; + +int result = 0; + +void on_acquire(volatile void* address, uint32_t value, + ATOMICS_WAIT_RESULT_T waitResult, void* userData) { + printf("on_acquire: releasing lock.\n"); + emscripten_lock_release(&lock); + printf("on_acquire: released lock.\n"); +#ifdef REPORT_RESULT + REPORT_RESULT(result); +#endif +} + +int main() { + printf("main: async acquiring lock.\n"); + emscripten_lock_async_acquire(&lock, on_acquire, 0, 100); + printf("main: busy-spin acquiring lock.\n"); + emscripten_lock_busyspin_waitinf_acquire(&lock); + printf("main: lock acquired.\n"); + emscripten_lock_release(&lock); + printf("main: lock released.\n"); + result += 1; + emscripten_exit_with_live_runtime(); +} From 5325528a8b3bdb7f004b05e58e2615ed7be74d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Tue, 28 Oct 2025 16:11:29 +0200 Subject: [PATCH 2/3] Add comment --- system/include/emscripten/wasm_worker.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/system/include/emscripten/wasm_worker.h b/system/include/emscripten/wasm_worker.h index e34ec99f1803d..07715c1c14755 100644 --- a/system/include/emscripten/wasm_worker.h +++ b/system/include/emscripten/wasm_worker.h @@ -188,6 +188,10 @@ void emscripten_lock_busyspin_waitinf_acquire(emscripten_lock_t *lock __attribut // NOTE: This function can be called in both main thread and in Workers. If you // use this API in Worker, you cannot utilise an infinite loop programming // model. +// NOTE 2: This function will always acquire the lock asynchronously. That is, +// the lock will only be attempted to acquire after current control flow +// yields back to the browser, so that the Wasm call stack is empty. +// This is to guarantee an uniform control flow. void emscripten_lock_async_acquire(emscripten_lock_t *lock __attribute__((nonnull)), emscripten_async_wait_volatile_callback_t asyncWaitFinished __attribute__((nonnull)), void *userData, From aceaccaa65433cb8e776492fac0067670d7cce05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jukka=20Jyl=C3=A4nki?= Date: Wed, 5 Nov 2025 15:56:06 +0200 Subject: [PATCH 3/3] Review --- system/include/emscripten/wasm_worker.h | 7 +++---- test/test_browser.py | 3 +-- test/wasm_worker/lock_async_and_sync_acquire.c | 8 ++------ 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/system/include/emscripten/wasm_worker.h b/system/include/emscripten/wasm_worker.h index 07715c1c14755..c983e705615eb 100644 --- a/system/include/emscripten/wasm_worker.h +++ b/system/include/emscripten/wasm_worker.h @@ -185,13 +185,12 @@ void emscripten_lock_busyspin_waitinf_acquire(emscripten_lock_t *lock __attribut // timeout parameter as int64 nanosecond units, this function takes in the wait // timeout parameter as double millisecond units. See // https://github.com/WebAssembly/threads/issues/175 for more information. -// NOTE: This function can be called in both main thread and in Workers. If you -// use this API in Worker, you cannot utilise an infinite loop programming -// model. +// NOTE: This function can be called in both main thread and in Workers. // NOTE 2: This function will always acquire the lock asynchronously. That is, // the lock will only be attempted to acquire after current control flow // yields back to the browser, so that the Wasm call stack is empty. -// This is to guarantee an uniform control flow. +// This is to guarantee an uniform control flow. If you use this API in +// a Worker, you cannot utilise an infinite loop programming model. void emscripten_lock_async_acquire(emscripten_lock_t *lock __attribute__((nonnull)), emscripten_async_wait_volatile_callback_t asyncWaitFinished __attribute__((nonnull)), void *userData, diff --git a/test/test_browser.py b/test/test_browser.py index 10945ad891164..a552d30f9e037 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -5331,9 +5331,8 @@ def test_wasm_worker_lock_async_acquire(self): # Tests emscripten_lock_async_acquire() function when lock is acquired both synchronously and asynchronously. @also_with_minimal_runtime - @flaky('https://github.com/emscripten-core/emscripten/issues/25270') def test_wasm_worker_lock_async_and_sync_acquire(self): - self.btest('wasm_worker/lock_async_and_sync_acquire.c', expected='1', cflags=['--closure=1', '-sWASM_WORKERS']) + self.btest_exit('wasm_worker/lock_async_and_sync_acquire.c', cflags=['-sWASM_WORKERS']) # Tests emscripten_lock_busyspin_wait_acquire() in Worker and main thread. @also_with_minimal_runtime diff --git a/test/wasm_worker/lock_async_and_sync_acquire.c b/test/wasm_worker/lock_async_and_sync_acquire.c index 7bcf7882b2a14..5b7985c0fb203 100644 --- a/test/wasm_worker/lock_async_and_sync_acquire.c +++ b/test/wasm_worker/lock_async_and_sync_acquire.c @@ -4,16 +4,12 @@ emscripten_lock_t lock = EMSCRIPTEN_LOCK_T_STATIC_INITIALIZER; -int result = 0; - void on_acquire(volatile void* address, uint32_t value, ATOMICS_WAIT_RESULT_T waitResult, void* userData) { printf("on_acquire: releasing lock.\n"); emscripten_lock_release(&lock); printf("on_acquire: released lock.\n"); -#ifdef REPORT_RESULT - REPORT_RESULT(result); -#endif + exit(0); } int main() { @@ -24,6 +20,6 @@ int main() { printf("main: lock acquired.\n"); emscripten_lock_release(&lock); printf("main: lock released.\n"); - result += 1; emscripten_exit_with_live_runtime(); + return 1; }