Skip to content

[browser][coreCLR] corerun: exit Node naturally after main returns - #136897

Merged
lewing merged 1 commit into
mainfrom
lewing-fix-issue-135042
Nov 10, 2026
Merged

lewing merged 1 commit into
mainfrom
lewing-fix-issue-135042

Conversation

@lewing

@lewing lewing commented Nov 3, 2026

Copy link
Copy Markdown
Member

Problem

JIT/jit64/regress/vsw/539509/test1_539509 intermittently times out on the R2R-CG2 browser wasm Checked leg. It prints Passed. and then node never exits (#135042, about 25 hits a month). Any runtime test run under corerun.js on Node can hit this; this test hits it most often.

Root cause

This is a Node/V8 shutdown deadlock, nodejs/node#64274. The upstream fix, nodejs/node#66171, is not merged yet, so the Helix image's node v24.22.2 is affected.

corerun's _proc_exit override (#132078) calls process.exit(). That joins V8's platform worker threads without disposing the isolate. If a background compile job is parked in CollectionBarrier::AwaitCollectionBackground, waiting for a GC that only the main thread can run, the join never returns.

Evidence, using the exact payloads from the failing Helix job (crossgen2 + corerun.js + node v24.22.2) on Linux x64:

  • Repro: the test hangs after Passed. in about 1 of every 1,000–2,000 runs.
  • Hung process: it has only MainThread, one V8Worker and SignalInspector left, all in futex_wait_queue with no CPU time. It doesn't answer --report-on-signal, which fits a main thread blocked in a join.
  • Stress: with --stress-concurrent-allocation the hang rate rises to 18 of 170 runs. --single-threaded (0 of 60) and natural exit both remove it.

Fix

In libCorerun.js, on Node:

  • Keep the hard process.exit() only when the exit is requested from inside wasm, i.e. a wasm frame is on the JS stack (for example Environment.Exit). That case must not unwind ExitStatus through live CLR frames ([ci-scan] Test failure: JIT/Regression/CLR-x86-JIT/V1-M12-Beta2/b65423 crashes on browser-wasm with strerror called after runt [Content truncated due to length] #131937).
  • Otherwise, when main has returned or a timer tick has finished, fall through to Emscripten's own _proc_exit. It sets process.exitCode and lets Node exit when 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 already exited. Previously it never ran because process.exit() ended the process first. Without the guard it would trip Emscripten's exitRuntime assertion for exit code 0.

Validation

  • Stress: test1_539509 with the CI payloads under --stress-concurrent-allocation had 0 hangs in 180 runs with the fix vs 3 in 60 with the current host. Without stress, the average run time is unchanged (656 vs 652 ms).
  • Exit-mode matrix: I compared the current and patched hosts with a test app covering return 0, return 100, Environment.Exit(42), Environment.Exit(0), async Main returning 3 and 0, an unhandled exception, and GC with finalizers. Exit codes and output were identical.
  • Real Emscripten link: I linked the changed node block with Emscripten 6.0.2 at -O0 and -O2. Main returning exits naturally (beforeExit fires), exit() from wasm still hard-exits, and a late _exit after runtime exit is silent.
  • Not done: I didn't rebuild the full browser corerun locally. The runs above used the same code patched into the CI-generated corerun.js, so this PR's browser legs provide the full build validation.

The Windows x86 gcstress timeouts added to #135042 are a different mechanism (native corerun, no Node), and this PR does not address them.

Fixes #135042

Note

This PR description was generated with GitHub Copilot.

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>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Source-only reviews could not verify shutdown safety across V8 stack formatting and generated Emscripten exit paths, requiring browser-wasm maintainer validation of fully built Checked/Release hosts for exit-code preservation and safe synchronous and asynchronous exits.

No open issues 🎉

What changed in this PR

This PR adjusts CoreCLR’s WebAssembly corerun host to avoid the Node/V8 shutdown deadlock reported in #135042 while preserving immediate termination for exits requested from live WebAssembly frames.

Changes:

  • Detects WebAssembly frames on the JavaScript stack to retain hard exits for calls such as Environment.Exit.
  • Uses Emscripten’s normal exit path otherwise, allowing Node to exit naturally.
  • Guards against a queued exit running after the runtime has already shut down.
File Description
src/​coreclr/​hosts/​corerun/​wasm/​libCorerun.js Refines Node exit handling and guards against repeated runtime shutdown.

Review effort: Balanced

@lewing
lewing merged commit 6c64c0b into main Nov 10, 2026
116 checks passed
@lewing
lewing deleted the lewing-fix-issue-135042 branch November 10, 2026 01:28
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ci-scan] Hang: JIT/jit64/regress/vsw/539509/test1_539509 never exits after Passed on browser-wasm R2R-CG2

3 participants