Skip to content

Bun.openInEditor: throw when no editor is detected instead of spawning an empty command - #31310

Closed
robobun wants to merge 2 commits into
mainfrom
farm/ed5420ac/openineditor-no-editor-spawn
Closed

Bun.openInEditor: throw when no editor is detected instead of spawning an empty command#31310
robobun wants to merge 2 commits into
mainfrom
farm/ed5420ac/openineditor-no-editor-spawn

Conversation

@robobun

@robobun robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a Fuzzilli-found flaky crash (fingerprint ad4892a7a5610032, TERMSIG 30 / "Power failure" on Linux) in the Bun.openInEditor path — the same family as #31183.

Root cause

EditorContext::detect_editor caches "nothing found" as the sentinel Editor::None (with an empty binary path), but Bun.openInEditor only treated Option::None as "no editor". So when no editor is detectable (no bunfig editor, no EDITOR/VISUAL, nothing in PATH — i.e. the fuzzing container), the intended "Failed to auto-detect editor" error never fired. Instead, every call silently spawned a detached "Open Editor" thread plus a forked child with an empty argv[0] that can never exec.

The fuzzer sample exploits this: it recurses until stack overflow and calls Bun.openInEditor once per unwound frame, so a single script run creates tens of thousands of detached threads, each running the synchronous spawn machinery (process-global signal-forwarding register/unregister, vfork + wait) concurrently, followed by Bun.gc(true). On Linux, JSC and the libpas scavenger suspend threads with SIGPWR during exactly this kind of thread/allocator churn (verified: forcing SIGPWR to SIG_DFL and running this workload dies immediately with "Power failure"), and the storm of concurrent, main-thread-only spawnSync signal bookkeeping is what lets the flaky SIGPWR termination show up. #31183 removed SIGPWR from the forwarding list; this removes the pointless spawn storm that hammers it.

Fix

Treat Editor::None as "not found" in Bun.openInEditor:

  • no detectable editor → throws "Failed to auto-detect editor" instead of spawning an empty command per call
  • { editor: "code" } (or another known name) that isn't installed → throws "Could not find editor \"code\"" instead of falling through to the empty spawn
  • explicit absolute paths and real detected editors are unchanged

Related (not closed by this PR): #31194 tracks making the detached editor helper skip the foreground spawnSync signal-forwarding entirely (via a forward_signals spawn option). This PR only eliminates the no-editor spawn storm; when a real editor is configured, the helper thread still uses sync::spawn as before, so that issue remains open.

How did you verify your code works?

  • New test in test/js/bun/util/open-in-editor-gc.test.ts runs Bun.openInEditor 50× in a child bun with editor detection guaranteed to fail and asserts every call throws (fails on the previous behavior: threw: 0, silent: 50; passes with this change).
  • Existing Bun.openInEditor does not break GC signal handling test (from Don't forward SIGPWR in spawnSync signal handling #31183, uses an explicit editor path) still passes.
  • The fuzzer's crash body now exits with a proper JS error (Failed to auto-detect editor) and no longer spawns any threads/processes; previously it spawned one detached thread + doomed child per unwound frame.

…g an empty command

detect_editor caches "nothing found" as Editor::None, but open_in_editor only
treated Option::None as failure, so with no detectable editor every call
silently spawned a detached "Open Editor" thread plus a child process with an
empty argv[0] that could never exec. Treat Editor::None as "not found" so the
existing "Failed to auto-detect editor" / "Could not find editor" errors fire
instead.
@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

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 19 minutes and 31 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: 5219a524-16b3-4559-92b4-92fa74b27348

📥 Commits

Reviewing files that changed from the base of the PR and between 295026b and b09c365.

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

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

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:00 PM PT - May 23rd, 2026

@robobun, your commit 32febea has 1 failures in Build #57463 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31310

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

bun-31310 --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 issue documents how Bun.openInEditor() spawns helper threads via sync::spawn() that install process-wide signal forwarding handlers; this PR prevents the empty spawns entirely by checking for Editor::None before spawning.

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

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Spawn the editor from Bun.openInEditor without spawnSync's signal forwarding #31299 - Also fixes the fuzzilli SIGPWR crash in Bun.openInEditor (via signal forwarding changes instead of throwing on no editor)
  2. openInEditor: spawn the editor without spawnSync's signal forwarding #31297 - Also fixes openInEditor's SIGPWR crash by disabling signal forwarding on detached editor threads
  3. fix(openInEditor): disable spawnSync signal forwarding for editor helper #31195 - Earlier attempt at the same openInEditor signal-forwarding fix

🤖 Generated with Claude 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.

LGTM — correctly maps the Some(Editor::None) sentinel to the existing "no editor" error paths, with a regression test.

Extended reasoning...

Overview

This PR touches two files: a 3-line logic change in src/runtime/api/BunObject.rs (open_in_editor) and a new regression test in test/js/bun/util/open-in-editor-gc.test.ts. The change adds .filter(|e| *e != Editor::None) at three points where an Option<Editor> is consumed, so that the cached "nothing found" sentinel is treated the same as Option::None and routes into the pre-existing Could not find editor … / Failed to auto-detect editor throws instead of falling through to spawn an empty command.

I verified the root cause in the codebase: EditorContext::detect_editor ends with self.editor = Some(Editor::None); (src/runtime/cli/open.rs:646) when no editor is resolvable, and auto_detect_editor only re-detects when self.editor.is_none(). So the prior code's match edit.editor { Some(e) => e, None => throw } could never reach the throw once detection had run — it would proceed with Editor::None and an empty edit.path, spawning a doomed child per call. The filter is the minimal, obvious fix and matches the PR description exactly.

Security risks

None. Bun.openInEditor is a developer-convenience API; this change strictly reduces process spawning by replacing an empty-argv fork with a thrown JS error. No new inputs are accepted, no auth/crypto/permissions code is touched.

Level of scrutiny

Low. The diff is mechanical (three identical .filter insertions on Option<Editor>), the sentinel semantics are confirmed in open.rs, and the only behavioral change is that previously-silent failure cases now throw the error message the code already intended to throw. The accompanying test pins the behavior (50/50 throws, exact error message, clean exit) in an isolated child process with an empty PATH and no EDITOR/VISUAL.

Other factors

No CODEOWNERS cover these paths, no bugs were flagged by the bug-hunting system, and there are no outstanding human review comments. The existing Bun.openInEditor does not break GC signal handling test (explicit absolute editor path) is unaffected since absolute paths resolve to Editor::Other/a real variant, never Editor::None.

@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 — minimal fix that makes the existing-but-unreachable "Failed to auto-detect editor" error actually fire instead of spawning an empty command.

Extended reasoning...

Overview

This PR adds .filter(|e| *e != Editor::None) at three points in open_in_editor (src/runtime/api/BunObject.rs) so that the Editor::None sentinel — which EditorContext::detect_editor writes at src/runtime/cli/open.rs:646 when nothing is found — is treated the same as Option::None. Previously the "Could not find editor …" and "Failed to auto-detect editor" branches were dead code because detect_editor always returns Some(_), so the function fell through to editor.open() with an empty edit.path and spawned a doomed child per call. A new Linux-only test spawns a child bun with an empty PATH and no EDITOR/VISUAL, calls Bun.openInEditor 50× and asserts every call throws.

Security risks

None. The change strictly removes a process spawn (empty argv[0]) in favor of throwing a JS error. No new inputs are interpreted, no paths or commands are constructed differently for the success cases (absolute paths and real detected editors are unchanged).

Level of scrutiny

Low-to-medium. The diff is three one-line filters plus a test; I traced both the first-call path (edit.editor == Noneauto_detect_editor runs → sets Some(Editor::None) → filtered → throw) and the cached path (edit.editor == Some(Editor::None) → filtered → auto_detect_editor is a no-op via its is_none() guard → filtered again → throw), and the explicit-{ editor: "…" } path now correctly restores prev and throws "Could not find editor" when detection yields Editor::None. Editor::Other (used for unrecognized absolute paths) is untouched, so user-supplied editor binaries still work.

Other factors

The single CI failure (webview-chrome.test.ts core dump) is unrelated to this change. The bot-flagged "duplicate" PRs (#31299/#31297/#31195) target the orthogonal signal-forwarding side of #31194; this PR is complementary and the description explicitly leaves #31194 open. No CODEOWNERS cover these files, no human review comments are outstanding, and the bug-hunting system found nothing.

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the code change is green on every job that exercised it; the two failing runs are unrelated, pre-existing flakes:

  • Build 57463: Alpine 3.23 x64 — test/js/bun/webview/webview-chrome.test.ts failed because the spawned chromium process segfaulted (core dump chromium-11538.core). The new open-in-editor test wasn't in that shard.
  • Build 57470 (retriggered): Windows 2019 x64 (no-AVX) — test/napi/napi.test.ts segfaulted in a NAPI finalizer test (Segmentation fault at address 0xFFFFFFFFFFFFFFFF, bun.report link in the job log).

Neither suite touches Bun.openInEditor; the diff here is three expressions in editor detection plus a Linux-only regression test, which passes locally and in CI where it ran. Already retriggered once, so leaving CI as-is rather than churning further — happy to rebase/retry if a maintainer wants another run.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: the same fix landed in #37210.

#37210 (merged 2026-08-09) treats Editor::None as "not found" in Bun.openInEditor via a new EditorContext::found() helper (src/runtime/cli/open.rs), used at the same three checks this PR changed in src/runtime/api/BunObject.rs, so a missing editor throws Could not find editor "..." / Failed to auto-detect editor instead of spawning an empty command. It also updated test/js/bun/util/open-in-editor-gc.test.ts to assert that every call throws in that situation.

Verified on current main (bdb7382): test/js/bun/util/open-in-editor-gc.test.ts from this branch, run unmodified against a debug build of main, passes (2 pass, including throws when no editor can be found) on two consecutive runs.

@robobun robobun closed this Aug 13, 2026
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.

1 participant