From 0638ddf4b185ba6e6f5e8d4a2678f67232ab610a Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Tue, 3 Nov 2026 00:04:42 -0600 Subject: [PATCH] [browser][coreCLR] corerun: exit Node naturally after main returns process.exit() joins V8's platform worker threads without disposing the isolate. When a background compile job is parked waiting for a main-thread GC, that join never returns and Node hangs after the test has finished (nodejs/node#64274). This intermittently times out runtime tests on the browser-wasm legs, for example JIT/jit64/regress/vsw/539509 after it prints "Passed.". Keep the hard process.exit() only for exits requested from inside wasm, such as Environment.Exit, which must not unwind ExitStatus through live CLR frames. Otherwise let Node exit once the event loop drains, which disposes the isolate and releases the parked worker. Ignore the late exit that SystemJS_ResolveMainPromise queues for corerun once the runtime has exited. Fixes #135042 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/hosts/corerun/wasm/libCorerun.js | 28 +++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/coreclr/hosts/corerun/wasm/libCorerun.js b/src/coreclr/hosts/corerun/wasm/libCorerun.js index c2d8598af73c9a..59218d8b64dc38 100644 --- a/src/coreclr/hosts/corerun/wasm/libCorerun.js +++ b/src/coreclr/hosts/corerun/wasm/libCorerun.js @@ -34,13 +34,39 @@ function libCoreRunFactory() { ENV["DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"] = "true"; if (ENVIRONMENT_IS_NODE) { + // V8 formats every wasm frame as ":wasm-function[]:". + const isWasmOnStack = () => { + const savedStackTraceLimit = Error.stackTraceLimit; + try { + Error.stackTraceLimit = Infinity; + return /wasm-function\[/.test(new Error().stack || ""); + } finally { + Error.stackTraceLimit = savedStackTraceLimit; + } + }; + + // An exit requested from inside wasm (for example Environment.Exit) must not unwind + // the ExitStatus exception through live CLR frames, so end the process immediately. + // Otherwise let Node.js exit on its own once the event loop drains: process.exit() + // joins V8's worker threads without disposing the isolate, which can deadlock when + // a background compile job is waiting for a main thread GC (nodejs/node#64274). const original_proc_exit = _proc_exit; _proc_exit = (code) => { - if (!keepRuntimeAlive()) { + if (!keepRuntimeAlive() && isWasmOnStack()) { process.exit(code); } return original_proc_exit(code); }; + + // SystemJS_ResolveMainPromise queues an exit that can run after the runtime already + // exited, now that the process outlives that exit until the event loop drains. + const original_exit = _exit; + _exit = (status, implicit) => { + if (runtimeExited) { + throw new ExitStatus(EXITSTATUS); + } + return original_exit(status, implicit); + }; } }, },