Skip to content

Commit d175d3e

Browse files
committed
fix(wasi): avoid deadlock caused by child thread abort when the main thread is in Atomics.wait (#160)
1 parent 94e9198 commit d175d3e

File tree

28 files changed

+414
-87
lines changed

28 files changed

+414
-87
lines changed

.github/workflows/main.yml

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

2020
env:
21-
WASI_VERSION: '22'
22-
WASI_VERSION_FULL: '22.0'
23-
WASI_SDK_PATH: './wasi-sdk-22.0'
24-
EM_VERSION: '3.1.52'
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.16.0'
2627

2728
jobs:
2829
build:
@@ -76,7 +77,7 @@ jobs:
7677
7778
- uses: actions/setup-node@v3
7879
with:
79-
node-version: '20.9.0'
80+
node-version: ${{ matrix.target == 'wasm64-unknown-emscripten' && '24.5.0' || env.NODE_VERSION }}
8081
registry-url: 'https://registry.npmjs.org'
8182
env:
8283
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
@@ -44,6 +44,7 @@ set(ENAPI_BASIC_SRC
4444
"${CMAKE_CURRENT_SOURCE_DIR}/src/node_api.c"
4545
"${CMAKE_CURRENT_SOURCE_DIR}/src/async_cleanup_hook.c"
4646
"${CMAKE_CURRENT_SOURCE_DIR}/src/async_context.c"
47+
"${CMAKE_CURRENT_SOURCE_DIR}/src/wasi_wait.c"
4748
)
4849
set(EMNAPI_THREADS_SRC
4950
"${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
@@ -308,6 +308,7 @@
308308
'src/async_cleanup_hook.c',
309309
'src/async_context.c',
310310
'src/async_work.c',
311+
'src/wasi_wait.c',
311312
'src/threadsafe_function.c',
312313
'src/uv/uv-common.c',
313314
'src/uv/threadpool.c',
@@ -370,6 +371,16 @@
370371
]
371372
},
372373
}],
374+
['OS == "wasi"', {
375+
'ldflags': [
376+
'-Wl,--export=emnapi_thread_crashed',
377+
],
378+
'xcode_settings': {
379+
'OTHER_LDFLAGS': [
380+
'-Wl,--export=emnapi_thread_crashed',
381+
],
382+
},
383+
}],
373384
['OS != "wasi"', {
374385
'defines': [
375386
'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
@@ -69,11 +69,6 @@ var uvThreadpoolReady: Promise<void> & { ready: boolean } = new Promise<void>((r
6969
}) as any
7070
uvThreadpoolReady.ready = false
7171

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

packages/emnapi/src/core/init.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ export var napiModule: INapiModule = {
123123
const napiValue = napi_register_wasm_v1(to64('_envObject.id'), to64('exportsHandle.id'))
124124
napiModule.exports = (!napiValue) ? exports : emnapiCtx.handleStore.get(napiValue)!.value
125125
})
126+
} catch (e) {
127+
if (e !== 'unwind') {
128+
throw e
129+
}
126130
} finally {
127131
emnapiCtx.closeScope(envObject, scope)
128132
}

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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,12 @@ export function emnapiInit (options: InitOptions): any {
9898
emnapiModule.exports = (!napiValue) ? exports : emnapiCtx.handleStore.get(napiValue)!.value
9999
})
100100
} catch (err) {
101+
if (err !== 'unwind') {
102+
throw err
103+
}
104+
} finally {
101105
emnapiCtx.closeScope(envObject, scope)
102-
throw err
103106
}
104-
emnapiCtx.closeScope(envObject, scope)
105107
emnapiModule.loaded = true
106108
delete emnapiModule.envObject
107109
return emnapiModule.exports

0 commit comments

Comments
 (0)