Skip to content

Exit cleanly with Node loaded and keep nested entries on Node's stack (NI-7c, NI-3b) - #1905

Merged
ctate merged 28 commits into
mainfrom
ni/i8-exit
Oct 29, 2026
Merged

ctate merged 28 commits into
mainfrom
ni/i8-exit

Conversation

@cramforce

@cramforce cramforce commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1904. Eighth of nine Node islands PRs. This PR has no ABI change (still 9).

What it does

Exit. node:tls starts threads that parse root certificates into function-local statics. Only Node's own exit sequence joins them. In a scriptc program, exit() ran those statics' destructors and OPENSSL_cleanup while a thread was still parsing. The result was heap corruption, NULL rwlocks, SIGSEGV or SIGABRT, and hangs in the sanitized lane.

  • Node's per-process teardown is left to process exit.
  • When Node's loop finishes, the bridge builds a root store if node:tls was loaded. That blocks until the certificate threads finish, which costs no more than Node's own exit.
  • OPENSSL_init_crypto(OPENSSL_INIT_NO_ATEXIT) runs before Node initializes OpenSSL.
  • uv_library_shutdown() joins libuv's pool.
  • tests/harness/lsan.supp gains Node's process-lifetime certificate allocations.

Nested entries (NI-3b). A server whose async handler awaits island promises segfaulted. The handler's continuations run as runtime jobs that Node's loop dispatches, possibly on an async fiber's stack. Nested entries into Node now run below every live frame of Node's stack.

bridge.test.ts reads the expected ABI from scr_node_bridge.h.

Evidence

  • tests/harness/node-island-exit.test.ts with exit-tls.{mjs,cjs} runs eight modes: early, load, exit, keys and return, plus the early, load and keys modes ending by return instead of process.exit. It sets NODE_USE_SYSTEM_CA=1 and NODE_EXTRA_CA_CERTS and does 40 runs per mode (10 sanitized). Before the fix, load-return crashed in 11 of 30 runs. In the earlier measurement, a five-package program loaded with TLS ran 200 times per lane with 0 failures.
  • Corpus 6870-node-island-server-fibers: an HTTP server with an async handler over island promises, an island subclass and timers, driven by an island client while the island allocates enough to collect garbage. Byte-identical with Node in both lanes.

The nested entry's context now has its ASan fiber annotations (start/finish on both switches).

Fixed: 6870-node-island-server-fibers used to hit an ASan SEGV in nh_keep_done in the sanitized dev-mode llvm-differential lane. Runtime code called back from Node inside an async fiber's nested call still ran as that fiber (current fiber, loop resume context and ASan fake stack). scr_loop_nested_enter/scr_loop_nested_leave now save and restore that state around nested entries (3b2d5f2).

Shortcuts

  • OpenSSL's state and Node's per-process state are left to process exit, as Node's skipped teardown would otherwise free them. LSan suppressions cover those allocations.

Gate results

  • Island set in a Vercel Sandbox, with the libnode dev pack rebuilt from this branch's bridge (node-island*.test.ts, npm.test.ts, packages/compiler/src/node-islands, differential 68xx): plain 231/231 + diff 69/69; sanitized 231/231 + 69/69.
  • pnpm lint, pnpm format:check and pnpm test:ts7 --baselines-only pass.
  • The self-hosted compiler (packages/compiler/src/native/cli.ts --emit=llvm) builds.
  • Zero-cost gate: 2837 programs identical (benchmarks, self-hosted compiler, TypeScript corpus) against the base branch. The only DIFFERS is corpus/fs-extra-package (%m0.settleVoid). It differs in every gate of the stack, including Bound V8's stack scan by Node's own stack in Node islands #1898, which does not touch the compiler, so it is nondeterministic on the base. both-fail covers only the island programs, because the gate host has no libnode pack. Compile time stays within tolerance.

CI on the first push found failures outside the focused island set: the ABI in bridge.test.ts, the runtime ABI header declarations, the effect gate on the ts7 allowlist, weak collections in the compiler, the pipeline scout and the Node version line. The fixes are folded into the PRs that introduce each piece, and they are re-checked in a Sandbox on the final tree.

Stack

#1898
#1899
#1900
#1901
#1902
#1903
#1904
→ #1905
#1906
#1907
#1908

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 28, 2026 6:58pm UTC

… (NI-7c, NI-3b)

Node islands stack, part 8 of 11 (scriptc-perf/results/islands-prs/PLAN.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A call into Node from an async fiber runs in a context of its own on Node's
stack while the calling fiber stays suspended in it, but runtime code that
Node called back there still ran as that fiber: scr_current named it, and
a fiber resumed from there saved its resumer in scr_loop_ctx and, under
ASan, its fake stack in the calling fiber's slot. The calling fiber's next
park then returned to a context that no longer existed, or under ASan
resumed with a fake stack the nested context had destroyed
(6870-node-island-server-fibers in dev mode, sanitized: SEGV in
nh_keep_done). The nested entry now runs host callbacks as the loop's code
does (no current fiber, the main exception cell and ALS slot) and puts the
calling fiber's state and the loop context back when it returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/libnode-common/bridge/scr_node_bridge.cc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	packages/runtime/src/scr_node_island.c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	packages/runtime/src/scr_node_island.c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Review pass on this PR.

No review findings: this PR has no inline threads, no review bodies, and no human comments, and Vercel Agent's review of the current head (a11b82d) reports 0 suggestions.

CI is green (run 40505964101): every check passes, the rest are skipped by CI scope.

Stack note: ni/i5-shared-modules (#1902) is still at 3a8aee7, which this branch already contains.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cramforce and others added 5 commits October 28, 2026 08:47
Resolve tests/harness/npm.test.ts by keeping main's npm-suite split and
porting this branch's island KNOWN entry and stderr frame normalisation
into tests/harness/npm-suite.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	tests/harness/npm-suite.ts
@cramforce

Copy link
Copy Markdown
Contributor Author

Stack maintenance round; head is now 54ca2938f.

CI root cause. The failing test (Linux, plain|san, 10/20) lane was unit-journal.test.ts, fixed two branches down in 126154edc (the island collections were unclassified). Carried here by 1fd7d94cd, which merged the current ni/i7-routing head; 54ca2938f brings the lazy-traps divergence classification from the same line.

Sandbox validation with a libnode pack built from the top branch's bridge (bridgeAbi 10): unit-journal, the eight node-island*.test.ts files (100 tests), the 64 island corpus programs, and the npm suite all pass.

No compiler behavior changes on this branch; the journal classification is read only by unit-journal.test.ts.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cramforce

Copy link
Copy Markdown
Contributor Author

Restacked after main moved. #1902 was merged at 17:41Z (merge commit 2f3b926de), so this branch now merges origin/ni/i7-routing and the merge was clean (no conflicts, no hand edits; pnpm manifest reproduces the merged surface manifest byte for byte). Head is now c3e4f2ced and a fresh CI run is in progress on it.

🤖 Generated with Claude Code

Base automatically changed from ni/i7-routing to main October 29, 2026 02:53
@ctate
ctate merged commit 5f27c4d into main Oct 29, 2026
136 of 138 checks passed
@ctate
ctate deleted the ni/i8-exit branch October 29, 2026 04:43

This branch was successfully deployed

1 active deployment
Preview — c3e4f2ce Deployed Oct 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants