Skip to content

Spawn the editor from Bun.openInEditor without spawnSync's signal forwarding#31299

Open
robobun wants to merge 4 commits into
mainfrom
farm/f739c887/openineditor-detached-spawn-signals
Open

Spawn the editor from Bun.openInEditor without spawnSync's signal forwarding#31299
robobun wants to merge 4 commits into
mainfrom
farm/f739c887/openineditor-detached-spawn-signals

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented May 24, 2026

What does this PR do?

Fixes #31194

Fixes a Fuzzilli-found crash (fingerprint 1f89a105f06c0e7e): a script that calls Bun.openInEditor() tens of thousands of times (via a recursive constructor that swallows the stack-overflow error and calls it on every unwound frame) gets the process killed by signal 30 (SIGPWR — the signal JSC uses on Linux to suspend/resume threads for GC) with no output.

Each Bun.openInEditor() call spawns a detached editor thread, and that thread ran the full sync::spawn (bun.spawnSync) machinery. spawnSync's signal-forwarding setup is process-global and written for main-thread-only use (c-bindings.cpp: "Note: We only ever use bun.spawnSync on the main thread"): Bun__registerSignalsForForwarding() / Bun__unregisterSignalsForForwarding() install a one-shot (SA_RESETHAND) forwarding handler over ~14 signals and save/restore the previous dispositions through a shared, unsynchronized previous_actions[] array that gets memset to zero after each restore, plus the shared Bun__currentSyncPID.

With many concurrent openInEditor calls, those register/unregister cycles race each other: restores frequently re-install from the zeroed array, so the process-wide dispositions of SIGHUP, SIGINT, SIGQUIT, SIGTRAP, SIGABRT, SIGUSR2, SIGALRM, SIGTERM, SIGSTKFLT, SIGXCPU, SIGXFSZ, SIGVTALRM, SIGIO and SIGSYS flip between "one-shot forwarding handler" and SIG_DFL for the duration of the storm and can be left corrupted afterwards. Tracing a release build during a 256-call storm shows ~250 sigaction(SIGHUP, forwarding handler w/ SA_RESETHAND) installs and ~247 restores of a zeroed sigaction (i.e. SIG_DFL), ending with the handler gone. That is the same failure class as #31183 (which dropped SIGPWR itself from the forwarding list after an earlier fuzzer crash): process-wide signal handling gets scrambled by the editor threads while GC's SIGPWR suspend/resume traffic is in flight.

The fix makes the detached editor thread spawn the editor with the minimal bun_core::util::spawn_sync_inherit (plain spawn + waitpid, inherited stdio) instead of sync::spawn. That matches what the original implementation did (std.process.Child.spawn() + wait() on the detached thread — no signal forwarding, no job control, no Bun__currentSyncPID), so nothing in the openInEditor path touches process-wide signal state anymore. It also removes the per-call MiniEventLoop leak on Windows that the old code's FIXME described, since spawn_sync_inherit uses CreateProcessW directly there.

Note: I could not reproduce the exact SIGPWR termination locally (it needs the fuzzer machine's timing/parallelism — the crash is deterministic there), but the process-wide disposition corruption from this code path reproduces reliably on current canary and is what this PR removes.

How did you verify your code works?

  • New Linux regression test in test/js/bun/util/open-in-editor-gc.test.ts: it hammers Bun.openInEditor with an absolute editor path pointing at sleep so dozens of editor threads overlap, samples the process's caught-signal mask (SigCgt in /proc/self/status) the whole time, and asserts it never changes, then forces GC and asserts the process is still alive.
    • USE_SYSTEM_BUN=1 bun test test/js/bun/util/open-in-editor-gc.test.ts (bun 1.4.0-canary f161e03) fails: the mask changes for signals [1,2,3,5,6,12,14,15,16,24,25,26,29,31].
    • bun bd test test/js/bun/util/open-in-editor-gc.test.ts passes repeatedly with this change; the pre-existing SIGPWR test in the same file still passes.
  • The fuzzer's crash script (sloppy-mode) and a heavier variant (~44k Bun.openInEditor(Bun) calls interleaved with Bun.gc) run to completion with exit 0 on the debug build with this change.
  • cargo check -p bun_runtime / cargo clippy -p bun_runtime are clean for the touched code.

…warding

The detached editor thread went through sync::spawn (bun.spawnSync), whose
signal-forwarding setup (Bun__registerSignalsForForwarding / previous_actions /
Bun__currentSyncPID) is process-global and only safe on the main thread.
Concurrent Bun.openInEditor calls raced on it and flipped the dispositions of
unrelated signals process-wide. Use the minimal spawn+wait helper instead,
matching the original std.process.Child behavior.
@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented May 24, 2026

Updated 9:49 PM PT - May 23rd, 2026

@robobun, your commit a1fb56409932f38ecef81f48d5e6fb6622b1415e passed in Build #57421! 🎉


🧪   To try this PR locally:

bunx bun-pr 31299

That installs a local version of the PR into your bun-31299 executable, so you can run:

bun-31299 --bun

@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 - This is the exact bug: Bun.openInEditor calling sync::spawn on a detached thread installs process-wide signal forwarding that corrupts signal dispositions

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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 24, 2026

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 59 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: 8ee239ad-2abf-49b5-9ba0-fce1e422fb81

📥 Commits

Reviewing files that changed from the base of the PR and between 6bb1ac6 and a1fb564.

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

Walkthrough

This PR consolidates editor process spawning in openInEditor by replacing a local sync::spawn call with the shared bun_core::util::spawn_sync_inherit utility, eliminating unnecessary platform-specific setup from a detached thread. A new Linux concurrency regression test validates that signal mask state is stable when many openInEditor calls run in parallel, with forced GC to exercise SIGPWR suspend/resume behavior.

Changes

Editor spawn refactoring and signal handling validation

Layer / File(s) Summary
Editor spawn implementation refactoring
src/runtime/cli/open.rs
The sync import is removed and auto_close now calls bun_core::util::spawn_sync_inherit instead of manually managing owned argv and spawning a sync child with Windows-specific event-loop setup.
Signal handling concurrency regression test
test/js/bun/util/open-in-editor-gc.test.ts
A new Linux-only test spawns a helper script that repeatedly snapshots the caught-signal mask from /proc/self/status while launching many concurrent openInEditor calls, then forces Bun.gc(true) and asserts the mask never changes, validating no signal-state corruption under parallel load. Comment context before the existing GC test is adjusted.

Possibly related issues

  • oven-sh/bun#31194: This PR directly addresses the issue by removing use of crate::api::bun::process::sync::spawn in open.rs and replacing it with bun_core::util::spawn_sync_inherit, eliminating process-wide signal-forwarding installation from the editor helper thread.

Possibly related PRs

  • oven-sh/bun#31183: This PR's switch to spawn_sync_inherit for openInEditor and the added SIGPWR concurrency regression test align directly with the target PR's Linux change to stop forwarding SIGPWR in sync/spawnSync signal handling.
  • oven-sh/bun#31196: This PR switches openInEditor to use bun_core::util::spawn_sync_inherit, which overlaps directly with the target PR's changes to spawn_sync_inherit's posix_spawn_bun file-action and stdio handling.

Suggested reviewers

  • Jarred-Sumner
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: replacing spawnSync's signal forwarding with a simpler spawn mechanism for the editor process in Bun.openInEditor.
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.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering both required sections with technical depth and verification details.

✏️ 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 65-67: The test uses stdout.match(/^CHANGED:(.*)$/m) with a
non-null assertion which throws an unhelpful error if the fixture never prints
CHANGED:; replace the assertion with an explicit match variable (e.g. const
match = stdout.match(/^CHANGED:(.*)$/m)), assert match is truthy with a clear
error message mentioning “missing CHANGED: output” (or fail the test), then
parse JSON.parse(match[1]) into changed and continue the existing expect on
changed; update references to stdout, match, changed, and the regex
/^CHANGED:(.*)$/m in open-in-editor-gc.test.ts accordingly.
🪄 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: aed9a9c5-f556-4d56-9ba9-4e7b43139b49

📥 Commits

Reviewing files that changed from the base of the PR and between a207a77 and 6bb1ac6.

📒 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. openInEditor: don't run spawnSync's process-wide signal forwarding on detached editor threads #31297 - Same fix: bypasses spawnSync signal forwarding for openInEditor by modifying the same files (open.rs, open-in-editor-gc.test.ts)
  2. fix(openInEditor): disable spawnSync signal forwarding for editor helper #31195 - Same fix: disables spawnSync signal forwarding for the editor helper, modifying the same core files

🤖 Generated with Claude Code

Comment thread src/runtime/cli/open.rs
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — thanks for cleaning up the stale porting TODOs as well.

Extended reasoning...

Overview

This PR fixes a Fuzzilli-found crash (#31194) where concurrent Bun.openInEditor() calls from detached threads raced on bun.spawnSync's process-global signal-forwarding state (Bun__registerSignalsForForwarding / previous_actions[]), corrupting signal dispositions and getting the process killed by SIGPWR during GC. The fix replaces ~30 lines of sync::spawn setup (including a Windows MiniEventLoop leak workaround) in auto_close() with a one-line call to the existing bun_core::util::spawn_sync_inherit helper — a plain posix_spawn + waitpid (CreateProcessW on Windows) that touches no process-wide signal state. This matches what the original Zig code did (std.process.Child.spawn() + .wait()). A new Linux regression test samples SigCgt in /proc/self/status while hammering openInEditor and asserts the caught-signal mask never changes.

Security risks

None. The change narrows the spawn surface: it removes signal-handler manipulation and a per-call event-loop allocation, replacing them with a minimal spawn+wait that's already used by bun init and the crash handler. No new inputs, no privilege changes, no new code paths.

Level of scrutiny

Moderate — process spawning is sensitive, but this is a strict simplification that swaps a misused heavyweight primitive for an established lightweight one (spawn_sync_inherit is already exercised by init_command.rs and crash_handler/lib.rs). I read spawn_sync_inherit end-to-end: on Linux it's posix_spawn_bun + an EINTR-safe waitpid loop with no sigaction calls; on Windows it's std::process::Command (CreateProcessW) with inherited stdio and no event loop, which also eliminates the documented MiniEventLoop leak. Argv handling is unchanged (same (ptr, len) reconstruction into &[u8] slices, which spawn_sync_inherit accepts via impl AsRef<[u8]>).

Other factors

Both prior review nits have been addressed: my comment about the stale TODO(port) recommending bun.spawnSync was fixed in d68be24 (the Editor::open() comment now points at spawn_sync_inherit, and the obsolete SpawnedEditorContext TODO was removed), and CodeRabbit's regex-guard nit was fixed in a479191. The new test is well-constructed (warm-up before baselining, overlapping editor threads via sleep 0.15, forced GC, exit/signal assertions) and the PR description confirms it fails on current canary with the expected 14-signal delta. The musl CI entries are scripts/build/ci.ts build-step failures on a479191, not test failures, and the only commit since (d68be24) is comment-only. No CODEOWNERS cover these files.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — focused fix that swaps the main-thread-only sync::spawn for the lightweight spawn_sync_inherit helper on the detached editor thread, restoring parity with the original Zig behavior; both earlier review nits have been addressed.

Extended reasoning...

Overview

This PR changes src/runtime/cli/open.rs to stop calling sync::spawn (bun.spawnSync) from the detached editor thread spawned by Bun.openInEditor, replacing it with bun_core::util::spawn_sync_inherit — a plain posix_spawn/CreateProcessW + wait helper with inherited stdio. It also removes the now-unneeded sync import, the Windows MiniEventLoop::init_global workaround (and its FIXME describing the per-call leak), and updates two stale TODO(port) comments that previously recommended the unsafe approach. A new Linux-only regression test in test/js/bun/util/open-in-editor-gc.test.ts hammers openInEditor concurrently while sampling SigCgt from /proc/self/status and asserts the caught-signal mask never changes, then forces GC to confirm the process survives SIGPWR traffic. Net diff in open.rs is roughly −30 lines.

Security risks

None identified. The change removes process-global signal-handler mutation from a non-main thread (a thread-safety hazard) rather than adding any new attack surface. spawn_sync_inherit is an existing, established helper already used elsewhere (e.g., crash_handler's llvm-symbolizer spawn); on Linux/FreeBSD it routes through posix_spawn_bun with explicit dup2 actions for fds 0–2, on macOS it uses posix_spawnp, and on Windows it uses CreateProcessW directly. The argv passed in is the same editor-binary + file-path argv that was previously handed to sync::spawn, so there's no new injection vector. The editor binary is whatever the user configured (or auto-detected) — unchanged from before.

Level of scrutiny

Medium. Process spawning and signal handling are subtle areas, but this is a simplification that restores parity with the original Zig implementation (std.process.Child.spawn() + .wait() on a detached thread, no signal forwarding). I verified spawn_sync_inherit in src/bun_core/util.rs:5088 and confirmed it does no signal-forwarding registration and touches no process-global mutable state — it just builds a NUL-terminated argv, spawns, and waitpids. The PR description's diagnosis of the race on previous_actions[] / Bun__currentSyncPID is accurate and matches the "main thread only" comment in c-bindings.cpp. The behavior difference (no Ctrl-C forwarding to the editor child) matches the pre-port Zig behavior, so this is not a regression.

Other factors

  • Both prior review nits were addressed in follow-up commits: my comment about the stale TODO(port): … calls bun.spawnSync guidance in Editor::open() was fixed in d68be24, and CodeRabbit's nit about guarding the CHANGED: regex match was fixed in a479191.
  • The new test is well-designed: it uses an absolute path to sleep as the editor so dozens of editor threads overlap, includes a warm-up call before baselining to avoid false positives from lazy one-time handler installation, and asserts on the precise observable (SigCgt mask) rather than just "didn't crash". The PR author confirmed it fails on canary and passes with the fix.
  • The Windows side benefit (eliminating the per-call MiniEventLoop + uv_loop_t leak documented in the removed FIXME) falls out for free since spawn_sync_inherit uses CreateProcessW directly.
  • A bot flagged potential duplicate PRs (#31297, #31195) — that's a merge-coordination question for maintainers, not a correctness concern with this PR.

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