Skip to content

openInEditor: don't run spawnSync's process-wide signal forwarding on detached editor threads#31297

Open
robobun wants to merge 4 commits into
mainfrom
farm/6d382bbf/openineditor-plain-spawn
Open

openInEditor: don't run spawnSync's process-wide signal forwarding on detached editor threads#31297
robobun wants to merge 4 commits into
mainfrom
farm/6d382bbf/openineditor-plain-spawn

Conversation

@robobun

@robobun robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Fuzzer fingerprint: 8723bd9fcfb9e59f

What the fuzzer found

A flaky crash (process terminated by signal 30 / SIGPWR, Linux) on a script whose core is:

function F1() {
  if (!new.target) { throw 'must be called with new'; }
  const v7 = this.constructor;
  try { new v7(); } catch (e) {}
  Bun.openInEditor(Buffer);
}
new F1();
Bun.gc(true);

The recursion unwinds through tens of thousands of frames and calls Bun.openInEditor() at every depth, so the process ends up running tens of thousands of short-lived "Open Editor" threads while the GC is active. SIGPWR is the signal JSC uses for GC/scavenger thread suspend/resume on Linux, so dying with it means the process-wide signal state was corrupted while a suspend was in flight.

This is the third fuzzer fingerprint in this family (#31183 fixed 4e72ca85863f92fa by dropping SIGPWR from the spawnSync forwarding list; #30956 is open for 11551e051c08db2c, hardening the register/unregister pair itself).

Root cause

Editor::open() runs the actual editor spawn on a detached std::thread (auto_close). The Zig original used std.process.Child.spawn() + wait() there — a plain spawn with inherited stdio. The Rust port routed it through sync::spawn (the full bun.spawnSync machinery), which:

  • installs and restores process-wide signal-forwarding handlers (Bun__registerSignalsForForwarding / Bun__unregisterSignalsForForwarding, an unsynchronized process-global previous_actions[] table with SA_RESETHAND handlers), and
  • mutates other process globals (Bun__currentSyncPID, crash-handler reset, job-control/no-orphans state)

all of which are documented as main-thread-only (c-bindings.cpp: "We only ever use bun.spawnSync on the main thread."). With Bun.openInEditor that machinery runs concurrently on thousands of detached threads, racing each other and the GC's signal-based thread suspension.

Fix

Make auto_close use bun_core::spawn_sync_inherit — a minimal spawn + waitpid with inherited stdio — instead of sync::spawn. That restores the original std.process.Child semantics: the detached editor threads no longer touch any process-wide signal state, so there is nothing left for them to race against GC suspension.

Side effects of the switch:

  • The FIXME(windows-leak) in the removed block is resolved: the Windows path of spawn_sync_inherit uses CreateProcessW via std::process::Command, so no MiniEventLoop is leaked per editor open.
  • The xdg-open opener (used for vim/emacs/neovim on Linux) is now resolved via PATH like the Zig original, instead of being exec'd as a cwd-relative path.

Test

Added a Linux-only case to test/js/bun/util/open-in-editor-gc.test.ts that mirrors the fuzzer scenario: 1500 Bun.openInEditor() calls with no detectable editor, interleaved with allocation and GC, asserting the child exits cleanly with no signal.

Caveat: the underlying failure is a timing-dependent race that I could not reproduce deterministically on an unpatched build (the fuzzer itself marked the crash flaky), so the test exercises the crash scenario rather than failing reliably before the fix. The existing test in that file (from #31183) has the same characteristic and still passes.

Fixes #31194

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Bun.openInEditor can install spawnSync signal forwarding from editor helper thread #31194 - Describes the exact bug: Bun.openInEditor() spawns a detached helper thread that calls sync::spawn, installing process-wide signal-forwarding handlers that race with the main thread and GC

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #31194

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 30 minutes and 47 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b6984e5a-d97b-48c6-9c49-692643f753ee

📥 Commits

Reviewing files that changed from the base of the PR and between a42408c and 39c2537.

📒 Files selected for processing (2)
  • src/runtime/cli/open.rs
  • test/js/bun/util/open-in-editor-gc.test.ts

Walkthrough

The PR refactors Bun.openInEditor's internal editor spawning mechanism to use bun_core::spawn_sync_inherit instead of a prior sync::spawn approach with Windows-specific setup. A new regression test validates that the change safely handles repeated editor invocations under GC pressure on Linux.

Changes

openInEditor Process Spawning Refactor

Layer / File(s) Summary
Refactored editor process spawning
src/runtime/cli/open.rs
Removed the sync module import and simplified auto_close's editor process launch by replacing sync::spawn with owned argv construction and Windows loop initialization to a direct bun_core::spawn_sync_inherit call using reconstructed argv slices.
Regression test for openInEditor under GC stress
test/js/bun/util/open-in-editor-gc.test.ts
Added Linux-only test that stress-calls Bun.openInEditor with PATH invalidated and no editor detected, triggering GC pressure and validating the subprocess survives without stderr, exits cleanly with code 0, and is not killed by a signal.

Possibly related issues

  • oven-sh/bun#31194: openInEditor no longer calls sync::spawn (which installed spawnSync signal-forwarding), replacing it with bun_core::spawn_sync_inherit to avoid signal-forwarding side effects.

Possibly related PRs

  • oven-sh/bun#31183: Refactors openInEditor editor auto-close spawning from sync::spawn to bun_core::spawn_sync_inherit and adds an openInEditor/GC regression test scenario.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: removing process-wide signal forwarding from detached editor threads in openInEditor.
Description check ✅ Passed The description is comprehensive with required sections (What the fuzzer found, Root cause, Fix, Test), clearly explaining the problem, root cause, solution, and testing approach.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/util/open-in-editor-gc.test.ts`:
- Around line 76-79: The test is using Buffer inside a try/catch which hides
failures from the initial coercion step in open_in_editor; change the call in
the loop to pass a guaranteed-valid string path (e.g., "/dev/null" or
"C:\\Windows\\System32\\drivers\\etc\\hosts" depending on platform) to
open_in_editor/openInEditor so the pre-spawn coercion runs and any unexpected
throws surface, and remove the empty catch so that failures that occur before
Editor::open() are not swallowed; update the loop that calls
Bun.openInEditor(Buffer) to call Bun.openInEditor(validPathString) and allow
exceptions to propagate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 257e542e-e569-4089-a22b-d030b0995d1c

📥 Commits

Reviewing files that changed from the base of the PR and between f161e03 and a42408c.

📒 Files selected for processing (2)
  • src/runtime/cli/open.rs
  • test/js/bun/util/open-in-editor-gc.test.ts

Comment thread test/js/bun/util/open-in-editor-gc.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(openInEditor): disable spawnSync signal forwarding for editor helper #31195 - Both fix the same SIGPWR crash by removing spawnSync signal forwarding from the detached editor thread in open.rs

🤖 Generated with Claude Code

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

On the possible-duplicate note: #31195 (open) targets the same underlying problem reported in #31194, via a different approach — it adds a forward_signals option to the sync-spawn options and disables it for the editor helper, keeping the helper on sync::spawn.

This PR instead takes the detached editor helper off the bun.spawnSync machinery entirely (bun_core::spawn_sync_inherit, matching the original Zig std.process.Child behaviour), so the helper no longer touches any of the process-global sync-spawn state (previous_actions[], Bun__currentSyncPID, crash-handler reset, job-control/no-orphans paths), and the per-call MiniEventLoop leak on Windows noted in the removed FIXME goes away as well. Leaving it to maintainers to pick whichever direction they prefer.

Comment thread src/runtime/cli/open.rs

@claude claude Bot 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.

Thanks for addressing the comment cleanup — no further issues from my side. Not auto-approving since this and #31195 take different approaches to the same fix and the author flagged that as a maintainer call; the spawn/signal-handling path change is also worth a human sign-off.

Extended reasoning...

Overview

This PR changes auto_close in src/runtime/cli/open.rs — the function that runs on a detached std::thread per Bun.openInEditor() call — to use bun_core::spawn_sync_inherit (a minimal posix_spawn + waitpid / std::process::Command helper) instead of sync::spawn (the full bun.spawnSync machinery). The net diff is ~28 lines removed and ~6 added in the Rust source, plus a new Linux-only stress test in test/js/bun/util/open-in-editor-gc.test.ts and a few comment updates. The change restores parity with the original Zig std.process.Child.spawn() + wait() and, as a side effect, removes a per-call MiniEventLoop leak on Windows that was documented in the deleted FIXME.

Security risks

None identified. The change removes process-global signal-handler manipulation from a detached thread rather than adding any. The argv passed to the new helper is constructed exactly as before (reconstructed from SpawnedEditorContext), and the editor binary path still comes from the same detection logic; no new external input is introduced. The xdg-open opener is now resolved via PATH (matching the Zig original) rather than as a cwd-relative path, which is if anything a minor improvement.

Level of scrutiny

Medium. The diff is small and is a simplification, but it sits at the intersection of process spawning, detached threads, and process-wide signal state — an area where this repo has already seen three related fuzzer fingerprints (#31183, #30956, this one). More importantly, there is an open competing PR (#31195) that fixes the same issue by adding a forward_signals option to sync::spawn and keeping the editor helper on the spawnSync path. The author explicitly noted in the thread that the choice between the two approaches is a maintainer decision. That is a design call I should not make on their behalf.

Other factors

All prior review feedback has been addressed: the CodeRabbit suggestion to pass a plain string path in the new test was applied (commit 318f5cb / "/dev/null"), and my earlier nit about three stale comments pointing back at bun.spawnSync was cleaned up (commit 6d35553). The bug-hunting system found no issues. The new test is acknowledged by the author as exercising the crash scenario rather than failing deterministically pre-fix (the underlying race is timing-dependent), which is consistent with the existing test in the same file but means CI coverage is best-effort. I verified bun_core::spawn_sync_inherit exists and is a thin posix_spawn + waitpid wrapper with no signal-handler side effects. Given the competing-approach decision and the signal-handling subject matter, deferring to a human reviewer.

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI triage for build 60248: the only failure was test/cli/install/bunx.test.ts (code 1) — identically on all 16 lanes, including platforms where nothing in this PR is exercised. The failing cases there ("should work for github repository"/"with committish", "should handle package that requires node 24") all fetch from live github/npm, and they fail the same way locally with the stock release bun that contains none of this PR's changes — this diff only touches Bun.openInEditor's detached helper spawn and a Linux-only test, and never goes near bunx. Retriggered CI once.

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

Status update: the retriggered build (#60262) is failing again while still in progress. The previous build's only failure was test/cli/install/bunx.test.ts — uniformly on all 16 platforms, in tests that fetch from live github/npm ("should work for github repository", "…with committish", "should handle package that requires node 24") — and those same tests fail locally with the stock release bun that contains none of this PR's changes. This PR's diff is confined to Bun.openInEditor's detached helper spawn plus a Linux-only test and cannot affect bunx.

I've used my one CI retrigger, so I'll leave this as-is rather than pushing more empty commits. The branch is rebased on current main, conflict-free, and all review feedback is addressed — once the bunx upstream breakage clears (or the test is fixed on main), a fresh build should go green.

robobun added 4 commits June 7, 2026 13:36
Bun.openInEditor spawns its editor on a detached thread. Routing that
spawn through bun.spawnSync installed process-wide signal-forwarding
handlers (Bun__registerSignalsForForwarding) and mutated process globals
that are only safe on the main thread, so concurrent openInEditor calls
raced that state against JSC's SIGPWR-based GC thread suspension on
Linux. Switch the detached thread to bun_core::spawn_sync_inherit — a
plain spawn + waitpid with inherited stdio, matching the original
std.process.Child behaviour — so no process-global signal state is
touched off the main thread.
@robobun robobun force-pushed the farm/6d382bbf/openineditor-plain-spawn branch from 4ed3d07 to e07dfde Compare June 7, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun.openInEditor can install spawnSync signal forwarding from editor helper thread

1 participant