Skip to content

Commit f4acc12

Browse files
authored
fix(wasi): avoid deadlock caused by child thread abort when the main thread is in Atomics.wait (#160)
1 parent 06e2a4f commit f4acc12

File tree

30 files changed

+437
-95
lines changed

30 files changed

+437
-95
lines changed

.github/workflows/main.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ on:
1818
workflow_dispatch:
1919

2020
env:
21-
WASI_VERSION: '25'
22-
WASI_VERSION_FULL: '25.0'
23-
WASI_SDK_PATH: './wasi-sdk-25.0'
24-
EM_VERSION: '3.1.64'
21+
WASI_VERSION: '27'
22+
WASI_VERSION_FULL: '27.0'
23+
WASI_SDK_PATH: './wasi-sdk-27.0'
24+
EM_VERSION: '4.0.1'
2525
EM_CACHE_FOLDER: 'emsdk-cache'
26-
NODE_VERSION: '22.15.0'
26+
NODE_VERSION: '22.16.0'
2727

2828
jobs:
2929
build:
@@ -77,7 +77,7 @@ jobs:
7777
7878
- uses: actions/setup-node@v3
7979
with:
80-
node-version: ${{ env.NODE_VERSION }}
80+
node-version: ${{ matrix.target == 'wasm64-unknown-emscripten' && '24.5.0' || env.NODE_VERSION }}
8181
registry-url: 'https://registry.npmjs.org'
8282
env:
8383
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

packages/core/src/worker.ts

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {
22
ThreadMessageHandler,
33
type ThreadMessageHandlerOptions,
4-
type LoadPayload
4+
type LoadPayload,
5+
type WorkerMessageEvent
56
} from '@emnapi/wasi-threads'
67
import type { NapiModule } from './emnapi/index'
78
import type { InstantiatedSource } from './load'
@@ -21,7 +22,24 @@ export class MessageHandler extends ThreadMessageHandler {
2122
if (typeof options.onLoad !== 'function') {
2223
throw new TypeError('options.onLoad is not a function')
2324
}
24-
super(options)
25+
const userOnError = options.onError
26+
super({
27+
...options,
28+
onError: (err, type) => {
29+
const emnapi_thread_crashed = this.instance?.exports.emnapi_thread_crashed as () => void
30+
if (typeof emnapi_thread_crashed === 'function') {
31+
emnapi_thread_crashed()
32+
} /* else {
33+
tryWakeUpPthreadJoin(this.instance!)
34+
} */
35+
36+
if (typeof userOnError === 'function') {
37+
userOnError(err, type)
38+
} else {
39+
throw err
40+
}
41+
}
42+
})
2543
this.napiModule = undefined
2644
}
2745

@@ -38,21 +56,39 @@ export class MessageHandler extends ThreadMessageHandler {
3856
return source
3957
}
4058

41-
public override handle (e: any): void {
59+
public override handle (e: WorkerMessageEvent): void {
4260
super.handle(e)
4361
if (e?.data?.__emnapi__) {
4462
const type = e.data.__emnapi__.type
4563
const payload = e.data.__emnapi__.payload
46-
47-
if (type === 'async-worker-init') {
48-
this.handleAfterLoad(e, () => {
49-
this.napiModule!.initWorker(payload.arg)
50-
})
51-
} else if (type === 'async-work-execute') {
52-
this.handleAfterLoad(e, () => {
53-
this.napiModule!.executeAsyncWork(payload.work)
54-
})
64+
try {
65+
if (type === 'async-worker-init') {
66+
this.handleAfterLoad(e, () => {
67+
this.napiModule!.initWorker(payload.arg)
68+
})
69+
} else if (type === 'async-work-execute') {
70+
this.handleAfterLoad(e, () => {
71+
this.napiModule!.executeAsyncWork(payload.work)
72+
})
73+
}
74+
} catch (err) {
75+
this.onError(err, type)
5576
}
5677
}
5778
}
5879
}
80+
81+
// function tryWakeUpPthreadJoin (instance: WebAssembly.Instance): void {
82+
// // https://github.com/WebAssembly/wasi-libc/blob/574b88da481569b65a237cb80daf9a2d5aeaf82d/libc-top-half/musl/src/thread/pthread_join.c#L18-L21
83+
// const pthread_self = instance.exports.pthread_self as () => number
84+
// const memory = instance.exports.memory as WebAssembly.Memory
85+
// if (typeof pthread_self === 'function') {
86+
// const selfThread = pthread_self()
87+
// if (selfThread && memory) {
88+
// // https://github.com/WebAssembly/wasi-libc/blob/574b88da481569b65a237cb80daf9a2d5aeaf82d/libc-top-half/musl/src/internal/pthread_impl.h#L45
89+
// const detatchState = new Int32Array(memory.buffer, selfThread + 7 * 4 /** detach_state */, 1)
90+
// Atomics.store(detatchState, 0, 0)
91+
// Atomics.notify(detatchState, 0, Infinity)
92+
// }
93+
// }
94+
// }

packages/emnapi/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ set(ENAPI_BASIC_SRC
6161
"${CMAKE_CURRENT_SOURCE_DIR}/src/node_api.c"
6262
"${CMAKE_CURRENT_SOURCE_DIR}/src/async_cleanup_hook.c"
6363
"${CMAKE_CURRENT_SOURCE_DIR}/src/async_context.c"
64+
"${CMAKE_CURRENT_SOURCE_DIR}/src/wasi_wait.c"
6465
)
6566
set(EMNAPI_THREADS_SRC
6667
"${CMAKE_CURRENT_SOURCE_DIR}/src/async_work.c"

packages/emnapi/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -816,8 +816,9 @@ Now emnapi has 3 implementations of async work and 2 implementations of TSFN:
816816
There are some limitations on browser about wasi-libc's pthread implementation, for example
817817
`pthread_mutex_lock` may call `__builtin_wasm_memory_atomic_wait32`(`memory.atomic.wait32`)
818818
which is disallowed in browser JS main thread. While Emscripten's pthread implementation
819-
has considered usage in browser. If you need to run your addon with multithreaded features on browser,
820-
we recommend you use Emscripten A & D, or bare wasm32 C & E.
819+
has considered usage in browser. This issue can be solved by upgrading `wasi-sdk` to v26+
820+
and emnapi v1.5.0+ then pass `--export=emnapi_thread_crashed` to the linker. If you need to
821+
run your addon with multithreaded features, we recommend you use A & D or C & E.
821822
822823
Note: For browsers, all the multithreaded features relying on Web Workers (Emscripten pthread also relying on Web Workers)
823824
require cross-origin isolation to enable `SharedArrayBuffer`. You can make a page cross-origin isolated
@@ -879,6 +880,7 @@ elseif(CMAKE_C_COMPILER_TARGET STREQUAL "wasm32-wasi-threads")
879880
"-Wl,--export-if-defined=node_api_module_get_api_version_v1"
880881
"-Wl,--export=malloc"
881882
"-Wl,--export=free"
883+
"-Wl,--export=emnapi_thread_crashed"
882884
"-Wl,--import-undefined"
883885
"-Wl,--export-table"
884886
)

packages/emnapi/common.gypi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@
310310
'src/async_cleanup_hook.c',
311311
'src/async_context.c',
312312
'src/async_work.c',
313+
'src/wasi_wait.c',
313314
'src/threadsafe_function.c',
314315
'src/uv/uv-common.c',
315316
'src/uv/threadpool.c',
@@ -372,6 +373,16 @@
372373
]
373374
},
374375
}],
376+
['OS == "wasi"', {
377+
'ldflags': [
378+
'-Wl,--export=emnapi_thread_crashed',
379+
],
380+
'xcode_settings': {
381+
'OTHER_LDFLAGS': [
382+
'-Wl,--export=emnapi_thread_crashed',
383+
],
384+
},
385+
}],
375386
['OS != "wasi"', {
376387
'defines': [
377388
'PAGESIZE=65536'

packages/emnapi/emnapi.gyp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
'src/node_api.c',
5656
'src/async_cleanup_hook.c',
5757
'src/async_context.c',
58+
'src/wasi_wait.c',
5859
],
5960
'link_settings': {
6061
'target_conditions': [
@@ -88,6 +89,22 @@
8889
]
8990
},
9091
}],
92+
['OS == "wasi"', {
93+
'link_settings': {
94+
'target_conditions': [
95+
['_type == "executable"', {
96+
'ldflags': [
97+
'-Wl,--export=emnapi_thread_crashed',
98+
],
99+
'xcode_settings': {
100+
'OTHER_LDFLAGS': [
101+
'-Wl,--export=emnapi_thread_crashed',
102+
],
103+
},
104+
}],
105+
]
106+
},
107+
}],
91108
]
92109
},
93110
{
@@ -98,6 +115,7 @@
98115
'src/node_api.c',
99116
'src/async_cleanup_hook.c',
100117
'src/async_context.c',
118+
'src/wasi_wait.c',
101119

102120
'src/uv/uv-common.c',
103121
'src/uv/threadpool.c',

packages/emnapi/src/core/async.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,6 @@ var uvThreadpoolReady: Promise<void> & { ready: boolean } = new Promise<void>((r
6767
}) as any
6868
uvThreadpoolReady.ready = false
6969

70-
/** @__sig i */
71-
export function _emnapi_is_main_browser_thread (): number {
72-
return (typeof window !== 'undefined' && typeof document !== 'undefined' && !ENVIRONMENT_IS_NODE) ? 1 : 0
73-
}
74-
7570
/** @__sig vppi */
7671
export function _emnapi_after_uvthreadpool_ready (callback: number, q: number, type: number): void {
7772
if (uvThreadpoolReady.ready) {

packages/emnapi/src/core/init.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,12 @@ export var napiModule: INapiModule = {
109109
const moduleHandle = scope.add(napiModule)
110110
instance.exports[nodeRegisterModuleSymbol](to64('exportsHandle'), to64('moduleHandle'), to64('5'))
111111
} catch (err) {
112+
if (err !== 'unwind') {
113+
throw err
114+
}
115+
} finally {
112116
emnapiCtx.isolate.closeScope(scope)
113-
throw err
114117
}
115-
emnapiCtx.isolate.closeScope(scope)
116118
napiModule.loaded = true
117119
delete napiModule.envObject
118120
return napiModule.exports
@@ -150,6 +152,10 @@ export var napiModule: INapiModule = {
150152
const napiValue = napi_register_wasm_v1(to64('_envObject.id'), to64('exportsHandle'))
151153
napiModule.exports = (!napiValue) ? exports : emnapiCtx.jsValueFromNapiValue(napiValue)!
152154
})
155+
} catch (e) {
156+
if (e !== 'unwind') {
157+
throw e
158+
}
153159
} finally {
154160
emnapiCtx.closeScope(envObject, scope)
155161
}

packages/emnapi/src/emnapi_internal.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ EMNAPI_INTERNAL_EXTERN void _emnapi_env_unref(napi_env env);
105105
EMNAPI_INTERNAL_EXTERN void _emnapi_ctx_increase_waiting_request_counter();
106106
EMNAPI_INTERNAL_EXTERN void _emnapi_ctx_decrease_waiting_request_counter();
107107

108+
EMNAPI_INTERNAL_EXTERN int _emnapi_is_main_browser_thread();
109+
EMNAPI_INTERNAL_EXTERN int _emnapi_is_main_runtime_thread();
110+
EMNAPI_INTERNAL_EXTERN double _emnapi_get_now();
111+
EMNAPI_INTERNAL_EXTERN void _emnapi_unwind();
112+
108113
#if defined(__EMSCRIPTEN_PTHREADS__) || defined(_REENTRANT)
109114
#define EMNAPI_HAVE_THREADS 1
110115
#else

packages/emnapi/src/emscripten/init.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,12 @@ export function emnapiInit (options: InitOptions): any {
8080
const moduleHandle = scope.add(emnapiModule)
8181
Module[emscriptenExportedSymbol](to64('exportsHandle'), to64('moduleHandle'), to64('5'))
8282
} catch (err) {
83+
if (err !== 'unwind') {
84+
throw err
85+
}
86+
} finally {
8387
emnapiCtx.isolate.closeScope(scope)
84-
throw err
8588
}
86-
emnapiCtx.isolate.closeScope(scope)
8789
emnapiModule.loaded = true
8890
delete emnapiModule.envObject
8991
return emnapiModule.exports
@@ -118,10 +120,12 @@ NODE_MODULE_VERSION ${NODE_MODULE_VERSION}.`)
118120
emnapiModule.exports = (!napiValue) ? exports : emnapiCtx.jsValueFromNapiValue(napiValue)!
119121
})
120122
} catch (err) {
123+
if (err !== 'unwind') {
124+
throw err
125+
}
126+
} finally {
121127
emnapiCtx.closeScope(envObject, scope)
122-
throw err
123128
}
124-
emnapiCtx.closeScope(envObject, scope)
125129
emnapiModule.loaded = true
126130
delete emnapiModule.envObject
127131
return emnapiModule.exports

0 commit comments

Comments
 (0)