Skip to content

openInEditor: spawn the editor without spawnSync's signal forwarding - #31297

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

openInEditor: spawn the editor without spawnSync's signal forwarding#31297
robobun wants to merge 3 commits into
mainfrom
farm/6d382bbf/openineditor-plain-spawn

Conversation

@robobun

@robobun robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.openInEditor() installs spawnSync's signal forwarding for as long as the editor runs (Bun.openInEditor can install spawnSync signal forwarding from editor helper thread #31194). While an editor is open, SIGINT/SIGTERM/SIGHUP/SIGUSR2/... sent to the process are redirected to the editor instead of reaching the program's own handlers: a process.on("SIGUSR2") listener never runs, the editor gets the signal. Two overlapping opens additionally race on the shared previous_actions[] table: the second registration saves the forwarding handler as the "previous" disposition, so after both editors exit the forwarded signals are left at SIG_DFL, wiping the program's handlers.
  • Cause: auto_close() in src/runtime/cli/open.rs, which runs on a detached thread per open, called process::sync::spawn(). That is the foreground spawnSync behind bun run; spawn_posix() (src/spawn/process.rs) unconditionally registers the forwarding handlers (Bun__registerSignalsForForwarding, c-bindings.cpp, whose comment says it is only ever used on the main thread) and restores them when the child exits. The Zig implementation used a plain std.process.Child spawn here; the Rust port switched it to sync::spawn.
  • Same call also exec'd the xdg-open opener used for vim/emacs/neovim on Linux relative to cwd (the sync spawn path does no PATH lookup), so that spawn silently failed, and on Windows it allocated a MiniEventLoop per open that was never torn down.

Fix

  • auto_close() uses bun_core::spawn_sync_inherit(): spawn with inherited stdio, wait, nothing else. It touches no process-wide state, resolves a bare argv[0] on PATH, and needs no event loop on Windows, so the FIXME block goes away with the sync::spawn call.
  • This is the right layer: the forwarding exists so a foreground bun run child sees the terminal's signals. An editor launched from inside a running program is not that case, so the editor thread should not be on that code path at all, rather than that path growing an opt-out for one caller (the approach in fix(openInEditor): disable spawnSync signal forwarding for editor helper #31195).
  • spawn_sync_inherit()'s macOS arm (src/bun_core/util.rs) was a bare posix_spawnp with no attributes, so unlike its Linux arm (which goes through posix_spawn_bun and scrubs the child) it handed the editor every descriptor Bun had opened without CLOEXEC (fs.openSync fds, for example) and Bun's ignored signals. Switching the editor onto the helper would have regressed that on macOS relative to the sync::spawn path, so the helper now sets POSIX_SPAWN_CLOEXEC_DEFAULT and the same signal reset bun_spawn_sys uses, inheriting only fds 0-2 (or /dev/null on 0). This also applies to the helper's existing callers (bun init, bun publish, the URL opener). Cross-checked with cargo check -p bun_core --target aarch64-apple-darwin; the runtime behavior is covered by the fd test below on the macOS lanes.
  • Verified with bun bd test test/js/bun/util/open-in-editor-gc.test.ts (6 tests, repeated runs clean). The three forwarding tests keep a fake editor alive while asserting; on unmodified main all three fail (SigCgt gains the 14 forwarded signals [1,2,3,5,6,12,14,15,16,24,25,26,29,31], the SIGUSR2 listener is bypassed and the editor dies from the forwarded signal, and the opener is never spawned); with the fix everything passes.
    • Bun.openInEditor does not change the process's signal dispositions (Linux): SigCgt from /proc/self/status is identical before and while the editor is up.
    • a process.on signal handler still runs while an editor ... is up (POSIX): the listener runs; the alternative outcome (editor killed by the forwarded signal) is detected explicitly so the failure is immediate rather than a timeout.
    • Bun.openInEditor finds the xdg-open opener on PATH (Linux): a fake xdg-open on PATH records that it was invoked.
    • Bun.openInEditor does not pass the process's file descriptors to the editor (POSIX): bun itself is used as the editor and checks whether the parent's fs.openSync descriptor number still refers to the sentinel file. Passes on Linux with or without this PR (the Linux spawner always scrubbed); on macOS it fails without the util.rs change and passes with it.
    • alternating editors on live editor threads, then GC (POSIX) replaces the old timed "does not break GC signal handling" test. That test was written for Don't forward SIGPWR in spawnSync signal handling #31183's SIGPWR exclusion in the forwarding list, which the editor path no longer reaches, so it passed both with and without this change and its 1 s sleep no longer had a reason; the replacement keeps what it still covered (the editor name being replaced while earlier threads are live, then a forced GC) and waits for every editor to have run instead of sleeping. The SIGPWR exclusion itself is unchanged and still applies to the remaining sync::spawn callers; it just has no test driving it from openInEditor any more.
  • The no-editor call storm test from the first version of this PR was dropped: since Bun.openInEditor: throw when no editor is found instead of spawning "" #37210 those calls throw before spawning, so it no longer exercised anything.

Background

  • spawnSync signal forwarding: when bun run/bunx run a script as a child, Bun installs handlers for npm's signal list that kill() the current child (Bun__currentSyncPID) and restores the previous dispositions afterwards. The saved dispositions live in one global array, so it is only correct for one foreground child at a time on the main thread.
  • Bun.openInEditor() builds the editor argv and hands it to a detached thread (auto_close) that spawns the editor and waits for it to exit, while JS continues on the main thread.
  • bun_core::spawn_sync_inherit() is the minimal spawn+wait helper already used by bun init, bun publish and the URL opener in cli/mod.rs (posix_spawn with PATH lookup on POSIX, CreateProcessW via std::process::Command on Windows).
  • SigCgt in /proc/<pid>/status is the bitmask of signals the process currently has a handler installed for, which makes "did anything touch the dispositions" directly observable.

Related

Fixes #31194


no test proof · iteration 10 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/util/open-in-editor-gc.test.ts

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:06 AM PT - Aug 16th, 2026

@robobun, your commit 1e7db03967b64508bf3b703a5222bc8ac5951d71 passed in Build #99355! 🎉


🧪   To try this PR locally:

bunx bun-pr 31297

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

bun-31297 --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 - 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, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Limit details: You’ve used all 5 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7cfb7312-a921-409c-8dfd-3acd1fc83cf2

📥 Commits

Reviewing files that changed from the base of the PR and between aec33f5 and 1e7db03.

📒 Files selected for processing (3)
  • src/bun_core/util.rs
  • 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
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.
Title check ✅ Passed The title clearly summarizes the primary change: openInEditor no longer uses spawnSync signal forwarding.
Description check ✅ Passed The description explains the problem, fix, side effects, related issues, and verification results, despite using different section headings than the template.

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

@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 Outdated
@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 Outdated

@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
robobun force-pushed the farm/6d382bbf/openineditor-plain-spawn branch from 4ed3d07 to e07dfde Compare June 7, 2026 13:39
@robobun
robobun force-pushed the farm/6d382bbf/openineditor-plain-spawn branch from e07dfde to 901d01f Compare August 13, 2026 01:43
Comment thread src/runtime/cli/open.rs Outdated
Comment on lines +402 to +406
// Not `process::sync::spawn`: that is the foreground spawnSync used by `bun run`,
// and it installs process-wide signal-forwarding handlers
// (`Bun__registerSignalsForForwarding`) that are only valid on the main thread.
// This runs on a detached thread per editor open while the user's JS keeps
// running, so it must not touch process signal state.

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

@robobun robobun changed the title openInEditor: don't run spawnSync's process-wide signal forwarding on detached editor threads openInEditor: spawn the editor without spawnSync's signal forwarding Aug 13, 2026
Comment thread src/runtime/cli/open.rs Outdated
Comment on lines +402 to +403
// Not `process::sync::spawn`: it installs `bun run`'s process-wide signal
// forwarding, and the user's JS is still running while this thread waits.

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Removed the comment above the editor spawn in f155233 rather than trimming it again. The reason for not using sync::spawn there is now enforced by the tests in this PR ("does not change the process's signal dispositions" and the process.on handler test both fail if the helper goes back through spawnSync), so the prose was redundant.

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

LGTM — one-line swap to the established bun_core::spawn_sync_inherit helper, restoring the original Zig std.process.Child behavior.

What was reviewed:

  • Confirmed spawn_sync_inherit (util.rs:4654) does PATH lookup for bare argv[0] on Linux via which, uses posix_spawnp on macOS, and installs no signal handlers — matches all three PR claims.
  • The helper is already exercised on all platforms by init_command, publish_command, crash_handler, and cli/mod.rs; the removed Windows MiniEventLoop setup is no longer needed.
  • Three new tests await observable conditions (pid file, marker file) rather than sleeping, drain both pipes, and the macOS-reachable SIGUSR2 test uses the Editor::Other argv shape ([binary, file]) which works there.
  • All prior feedback addressed: stale TODOs removed, test-header comment reworded, and the paragraph comment above the spawn dropped per comment-cop.
Extended reasoning...

Overview

The PR replaces the sync::spawn call in auto_close() (src/runtime/cli/open.rs) with bun_core::spawn_sync_inherit. This is a net -26 line change on the Rust side: the use crate::api::bun::process::sync import, the FIXME(windows-leak) block, the owned_argv boxing, and the sync::Options struct with its Windows MiniEventLoop::init_global setup all collapse to a single line. The test file gains three new tests (SigCgt bitmask unchanged while an editor is up, process.on("SIGUSR2") still fires, and xdg-open is resolved on PATH) plus a rewording of the existing GC-test header comment.

Security risks

None. This is a spawn-mechanism swap for a helper thread that launches a user-specified editor binary. No new inputs are accepted, no privilege boundaries are crossed, and the replacement helper is strictly less capable (no signal-handler installation, no event-loop allocation) than what it replaces.

Level of scrutiny

Low-to-medium. The Rust change is mechanical: it swaps to a helper that src/CLAUDE.md documents as the canonical "simple inherit-stdio CLI helper" and that four other call sites (init_command, publish_command, crash_handler, cli/mod.rs) already exercise across all platforms. I read spawn_sync_inherit_impl in src/bun_core/util.rs and confirmed the PR's three specific claims: it resolves a bare argv[0] via which() on Linux/FreeBSD (util.rs:4690-4700, so the xdg-open opener now works), it uses posix_spawnp on macOS (util.rs:4773), and it never touches Bun__registerSignalsForForwarding or any process-global state. The signature &[impl AsRef<[u8]>] accepts the &[&[u8]] slice the call site passes.

Other factors

The tests are well-constructed per REVIEW.md guidance: they await the actual observable condition (pid file written, marker file written) via bounded polling rather than fixed sleeps; they drain stdout/stderr/exited concurrently; they assert stderr/stdout before exitCode; the SIGUSR2 test explicitly races the two possible outcomes so a regression fails immediately with "signal was forwarded to the editor" rather than timing out. The macOS-reachable test (skipIf(isWindows)) uses an absolute-path fake-editor.sh whose basename maps to Editor::Other, so the argv shape is [binary, file] on both Linux and macOS — no OPENER prefix, so it does not depend on open/xdg-open being present.

All prior review feedback has been addressed across the PR's iterations: my earlier nit about three stale TODO comments (fixed in 39c2537), CodeRabbit's note about the Buffer argument in a since-dropped test, and two comment-cop flags about the paragraph-length comment above the spawn (removed in f155233 — the invariant is now enforced by the SigCgt and signal-handler tests instead of prose). The PR description documents that all five tests in the file pass with the fix and the three new ones fail on unmodified main.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Heads up: #38032 removes the xdg-open/start prefix from the vim/nvim/emacs argv in Editor::open (xdg-open rejects the three arguments, so that shape never launched anything on Linux; the opener is now macOS-only). After that change every argv[0] produced by Editor::open outside macOS is an absolute editor path, so the "finds the xdg-open opener on PATH" test here would wait forever for an xdg-open that is no longer spawned. Whichever of the two lands second needs to drop or rework that test; the other two tests here are unaffected.

@robobun
robobun force-pushed the farm/6d382bbf/openineditor-plain-spawn branch from f155233 to 0eafef1 Compare August 13, 2026 08:20
Comment thread src/bun_core/util.rs Outdated
Comment on lines +4650 to +4651
/// `<spawn.h>` extension the `libc` crate does not bind: keeps `fd` open in
/// the child when `POSIX_SPAWN_CLOEXEC_DEFAULT` closes everything else.

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/bun_core/util.rs Outdated
Comment on lines +4656 to +4657
/// src/jsc/bindings/spawn.cpp: default dispositions for every signal and an
/// empty signal mask, the same reset `bun_spawn_sys` applies.

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/bun_core/util.rs Outdated
Comment on lines +4661 to +4662
/// Owns an initialized `posix_spawnattr_t` + `posix_spawn_file_actions_t`
/// pair and destroys both on every exit path of the macOS spawn arm.

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/bun_core/util.rs Outdated
Comment on lines +4724 to +4727
// `posix_spawnp`. On Darwin the attr/file-actions objects handed to
// `posix_spawnp` and its `add*`/`setflags` helpers stay initialized inside
// `DarwinSpawnSetup` until after the spawn call. `waitpid` is passed a
// valid `&mut c_int` out-param.

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

Comment thread src/bun_core/util.rs Outdated
Comment on lines +4804 to +4808
// Same child setup as the Linux arm gets from posix_spawn_bun: every
// signal back to its default disposition (Bun ignores SIGPIPE
// process-wide), and only the stdio named in the file actions
// survives the exec. Without POSIX_SPAWN_CLOEXEC_DEFAULT the child
// would inherit every descriptor Bun opened without CLOEXEC.

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.

If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code

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

I reviewed this and didn't find any bugs. The open.rs change is a clean simplification, but the newer spawn_sync_inherit_impl macOS rework in util.rs (CLOEXEC_DEFAULT + signal reset via the new DarwinSpawnSetup) is a behavior change to a shared helper that also backs bun init, bun publish, the URL opener, and the crash handler — worth a human look to confirm that scope is intended. There are also five open comment-cop flags on the new util.rs comments and the noted interaction with #38032's xdg-open removal.

What was reviewed:

  • auto_close now calls spawn_sync_inherit with the reconstructed argv slice; no process-global state touched, PATH lookup restored, Windows MiniEventLoop path removed.
  • macOS posix_spawnp arm: DarwinSpawnSetup RAII pairs init/destroy on all exit paths including the mid-init failure; addinherit_np for fds 0/1/2 under POSIX_SPAWN_CLOEXEC_DEFAULT; posix_spawnattr_reset_signals matches the existing helper in spawn.cpp and the flags set (SETSIGDEF|SETSIGMASK).
  • New tests poll observable conditions (pid file, marker files, /proc/self/status SigCgt) rather than sleeping; each drains stdout/stderr/exited concurrently and asserts exit code last.
Extended reasoning...

Overview

Three files. src/runtime/cli/open.rs replaces the 27-line sync::spawn block (with its Windows MiniEventLoop FIXME) in auto_close with a single bun_core::spawn_sync_inherit(&argv[..]) call and drops the process::sync import — this is the core fix for #31194. test/js/bun/util/open-in-editor-gc.test.ts reworks the existing GC test to assert every editor actually ran (marker files) and adds four new tests: SigCgt-unchanged (Linux), SIGUSR2-handler-still-runs (POSIX), xdg-open PATH lookup (Linux), and fd-not-inherited (POSIX). src/bun_core/util.rs reworks the macOS arm of spawn_sync_inherit_impl: adds a DarwinSpawnSetup RAII wrapper, binds posix_spawn_file_actions_addinherit_np and posix_spawnattr_reset_signals, and switches from a bare posix_spawnp (only file_actions when stdin=Ignore) to one that always sets POSIX_SPAWN_CLOEXEC_DEFAULT | SETSIGDEF | SETSIGMASK and explicitly inherits fds 0/1/2.

Security risks

None identified. The change removes a path where a background thread mutated process-wide signal handlers, and hardens the macOS child (no leaked fds, default signal dispositions) to match what the Linux posix_spawn_bun arm already does. No new attack surface.

Level of scrutiny

Medium-high. The open.rs change alone would be low-risk (net deletion, restores Zig-era behavior). The util.rs change raises it: spawn_sync_inherit is documented in src/CLAUDE.md as the canonical CLI spawn helper and is called from init_command.rs (×2), publish_command.rs, cli/mod.rs, crash_handler/lib.rs, and now open.rs. On macOS all of those now get fd scrubbing and signal reset where they previously inherited everything. That is almost certainly correct (it brings macOS to parity with Linux), but it's a cross-cutting behavior change to a shared primitive that the PR title/description frames as an openInEditor fix, so a maintainer should confirm the scope is intentional.

Other factors

  • Five unresolved comment-cop inline comments on util.rs (posted 2026-08-13, after the last author response) flag the new multi-line comments; the author previously addressed identical flags on open.rs by deleting the comment, so these look like they're still pending.
  • The author's own note about #38032 removing the Linux xdg-open prefix means the "finds the xdg-open opener on PATH" test here becomes a hang if that PR lands first — merge order needs coordination.
  • The tests are well-constructed (poll for observable conditions, wire the failing outcome to a distinct message, drain pipes concurrently, exit code asserted last, test.concurrent for independent subprocess tests). The DarwinSpawnSetup RAII correctly destroys attr on the actions init-failure path and both on drop.
  • My prior review comment (stale TODOs) was addressed; the current util.rs rework and the fd/signal tests are new since then.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Trimmed the flagged comments in util.rs in 8a14996: the two extern declarations now carry only the one-line pointer to spawn.cpp that spawn_sys uses for the same symbol, the DarwinSpawnSetup doc comment is gone (the Drop impl says it), the SAFETY comment on the spawn block is back to its pre-PR wording, and the macOS arm keeps a single line naming what it mirrors. The remaining multi-line comments in that diff are the SAFETY comments on the individual unsafe calls, which stay because clippy::undocumented_unsafe_blocks is denied in this workspace.

On the scope point from the review: the spawn_sync_inherit change is intentionally shared, and the PR description lists the other callers it reaches (bun init, bun publish, the URL opener, the crash handler's symbolizer). Nothing in this container runs macOS, so beyond the fd test on the darwin lanes, the macOS arm is covered here by cargo check and cargo clippy for bun_core on both aarch64-apple-darwin and x86_64-apple-darwin, both clean. All six tests in open-in-editor-gc.test.ts pass on the Linux debug build at this head.

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

I reviewed this PR and didn't find any bugs. Because it changes the macOS spawn_sync_inherit path (adding POSIX_SPAWN_CLOEXEC_DEFAULT + signal reset) which is shared by bun init, bun publish, the crash-handler symbolizer spawn, and the URL opener — and the author flagged a landing-order dependency with #38032 for the xdg-open test — a human look would still be worthwhile.

What was reviewed:

  • auto_close swap to spawn_sync_inherit — argv slice lifetimes (backed by the boxed SpawnedEditorContext that outlives the call), Windows path (goes through std::process::Command, no MiniEventLoop needed).
  • macOS DarwinSpawnSetup: RAII pairing of posix_spawnattr_init/file_actions_init with _destroy on all paths, including the early-return when file_actions_init fails after attr is up.
  • Checked that posix_spawnattr_reset_signals (spawn.cpp) fills sigdefault and empties sigmask, matching the flags set; addinherit_np for 0-2 combined with CLOEXEC_DEFAULT is the standard Darwin inherit-only-stdio pattern.
  • New tests: no unbounded sleeps (all poll on observable markers/pids), pipes drained concurrently, exit code asserted last, hermetic (fake editors in tempDir, no network).
Extended reasoning...

Overview

Three files. src/runtime/cli/open.rs replaces a ~27-line sync::spawn block (with a FIXME'd per-call MiniEventLoop leak on Windows) in the detached editor thread with one call to bun_core::spawn_sync_inherit, so Bun.openInEditor no longer installs bun run's process-wide signal-forwarding handlers from a background thread. src/bun_core/util.rs reworks the macOS arm of spawn_sync_inherit_impl to set POSIX_SPAWN_CLOEXEC_DEFAULT | SETSIGDEF | SETSIGMASK, reset signals via the existing posix_spawnattr_reset_signals helper in spawn.cpp, and explicitly inherit fds 0-2 via addinherit_np — wrapped in a new DarwinSpawnSetup RAII struct so both the attr and file-actions objects are destroyed on every exit path. The test file replaces one timed test and adds four new ones covering SigCgt stability, process.on handler delivery, xdg-open PATH lookup, and fd inheritance.

Security risks

None identified. The change narrows what the spawned editor inherits (fewer fds, defaulted signals) rather than broadening it. No user-controlled input reaches new parsing.

Level of scrutiny

Moderate-to-high. The open.rs change itself is a clear simplification back toward the original Zig behavior. The util.rs change is where the review weight is: it alters macOS spawn semantics for every spawn_sync_inherit caller — crash_handler (llvm-symbolizer), bun init (self-spawn for install), bun publish (opening the auth URL), and cli/mod.rs (URL opener). Bringing macOS in line with what posix_spawn_bun already does on Linux is defensible, but it's a behavior change on a shared helper across callers that aren't exercised by this PR's tests, and it's macOS-only so it can't be verified on the Linux dev machine. The RAII wiring looks correct (init failures clean up the partially-initialized state; Drop handles the rest), and the fd test is designed to catch regressions on the macOS lanes.

Other factors

  • The author explicitly noted that #38032 removes the xdg-open prefix from the vim/nvim/emacs argv on Linux, which would make the new "finds the xdg-open opener on PATH" test wait forever. Whichever lands second needs to adjust that test — a maintainer should sequence these.
  • This is one of three overlapping fixes for #31194 (also #31195, #31299) with different approaches; a maintainer already needs to pick one.
  • My prior inline nit (stale TODO comments pointing back at spawnSync) was addressed. The subsequent comment-cop flags on util.rs appear addressed by 8a14996 (comments in the current diff are one-liners plus required SAFETY blocks).
  • CI on 0eafef1 showed one unrelated failure (test-cluster-shared-leak.js timeout on Windows x64); the latest commit only trims comments.

robobun and others added 3 commits August 16, 2026 08:41
Bun.openInEditor waits for the editor on a detached thread. That thread went
through process::sync::spawn, the foreground spawnSync used by bun run, which
installs process-wide signal-forwarding handlers for the lifetime of the
child. With the user's program still running on the main thread, signals such
as SIGUSR2 or SIGINT were redirected to the editor instead of reaching the
process's own handlers, and overlapping editor opens raced on the shared
previous_actions table. Use bun_core::spawn_sync_inherit, a plain spawn and
wait with inherited stdio, which also resolves the opener on PATH and removes
the per-open MiniEventLoop allocation the sync::spawn path needed on Windows.

Co-authored-by: Minh Vu <38443830+fallintoplace@users.noreply.github.com>
…ild too

The Linux arm goes through posix_spawn_bun, which resets every signal to its
default and closes everything above the stdio it sets up. The macOS arm was a
bare posix_spawnp with no attributes, so the child inherited every descriptor
Bun had opened without CLOEXEC and Bun's ignored signals. Set
POSIX_SPAWN_CLOEXEC_DEFAULT plus the signal reset and inherit only fds 0-2
(or /dev/null on 0), matching what bun_spawn_sys does for its spawns.

The editor tests now verify this with bun itself as the editor, replace the
timed GC test with one that waits for every editor to have run, and no longer
describe the spawnSync SIGPWR history the editor path no longer touches.
@robobun
robobun force-pushed the farm/6d382bbf/openineditor-plain-spawn branch from 8a14996 to 1e7db03 Compare August 16, 2026 08:44

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

I reviewed this PR and didn't find any bugs. Because it changes the shared spawn_sync_inherit macOS path (fd scrubbing + signal reset) for every caller — bun init, bun publish, the URL opener, the crash-handler symbolizer — and the author has flagged both a competing approach (#31195) and a known test interaction with #38032 for a maintainer to decide, a human look would still be worthwhile.

What was reviewed:

  • auto_close now uses spawn_sync_inherit (no signal forwarding, no MiniEventLoop leak) — matches the original Zig std.process.Child shape.
  • DarwinSpawnSetup RAII: attr/actions destroyed on every exit path; posix_spawnattr_reset_signals extern matches spawn.cpp; addinherit_np signature matches the one already in spawn_sys/posix_spawn.rs.
  • New tests poll observable conditions (pid file, marker files, SigCgt) rather than sleeping, and gate per-platform correctly.
Extended reasoning...

Overview

Three files: src/runtime/cli/open.rs swaps the detached editor thread's spawn from process::sync::spawn (the bun run foreground path with process-wide signal forwarding) to bun_core::spawn_sync_inherit, deleting the Windows MiniEventLoop FIXME block. src/bun_core/util.rs hardens the macOS arm of spawn_sync_inherit_impl to set POSIX_SPAWN_CLOEXEC_DEFAULT + SETSIGDEF/SETSIGMASK, explicitly inherit only fds 0–2, and wraps the attr/actions pair in a DarwinSpawnSetup struct with a Drop impl. test/js/bun/util/open-in-editor-gc.test.ts rewrites one test and adds four new ones covering signal dispositions, process.on handler delivery, xdg-open PATH lookup, and fd inheritance.

Security risks

None identified. The change tightens child-process isolation on macOS (fewer inherited fds, reset signal mask/dispositions). No user-controlled input reaches new parsing.

Level of scrutiny

Medium-high. The open.rs change itself is small and clearly right, but the util.rs change is a behavioral change to a shared helper on macOS that reaches four other call sites (init_command.rs, publish_command.rs, cli/mod.rs URL opener, crash_handler/lib.rs symbolizer). The author only compile-checked it on darwin targets and deferred runtime verification to CI's macOS lanes. Spawn attribute setup on Darwin (addinherit_np, CLOEXEC_DEFAULT, the c_short flags cast) is subtle enough that a maintainer familiar with the macOS spawn path should confirm it matches spawn_sys's established pattern.

Other factors

  • The author explicitly left it to maintainers to choose between this PR and #31195 (which instead adds a forward_signals opt-out to sync::spawn). That is a design decision a human should make.
  • The author flagged that #38032 removes the non-macOS xdg-open prefix, which will break the new "finds the xdg-open opener on PATH" test if #38032 lands first — a merge-order coordination point.
  • My earlier nit (stale TODO comments pointing back at bun.spawnSync) was addressed in 39c2537; the comment-cop bot's verbose-comment flags were addressed in f155233 and 8a14996 / 1e7db03.
  • Test quality is good: conditions are awaited via marker/pid files rather than sleeps, subprocess pipes are drained concurrently, exit codes asserted last, platform gating uses isLinux/isWindows, and each test has a clear failure mode on the unfixed build per the PR description.

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