diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 97ba56e934c0..f2ea806035a3 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -1041,7 +1041,7 @@ pub fn open_in_editor(global_this: &JSGlobalObject, callframe: &CallFrame) -> Js edit.name = unsafe { bun_ptr::detach_lifetime(slot.name_storage.as_slice()) }; edit.detect_editor(env); - editor_choice = edit.editor; + editor_choice = edit.editor.filter(|e| *e != Editor::None); if editor_choice.is_none() { slot.name_storage = prev_storage; *edit = prev; @@ -1074,11 +1074,11 @@ pub fn open_in_editor(global_this: &JSGlobalObject, callframe: &CallFrame) -> Js } } - let editor = match editor_choice.or(edit.editor) { + let editor = match editor_choice.or(edit.editor).filter(|e| *e != Editor::None) { Some(e) => e, None => { edit.auto_detect_editor(env); - match edit.editor { + match edit.editor.filter(|e| *e != Editor::None) { Some(e) => e, None => { return Err(global_this.throw(format_args!("Failed to auto-detect editor"))); diff --git a/test/js/bun/util/open-in-editor-gc.test.ts b/test/js/bun/util/open-in-editor-gc.test.ts index d63a41ee700b..d3c323763b8a 100644 --- a/test/js/bun/util/open-in-editor-gc.test.ts +++ b/test/js/bun/util/open-in-editor-gc.test.ts @@ -3,6 +3,59 @@ import { bunEnv, bunExe, isLinux, tempDir } from "harness"; import { existsSync, symlinkSync } from "node:fs"; import { join } from "node:path"; +// When no editor can be detected (no bunfig editor, no EDITOR/VISUAL, nothing +// in PATH), Bun.openInEditor must throw instead of spawning a detached editor +// thread with an empty argv[0]. Repeated calls used to fork a doomed child per +// call, which let fuzzed scripts create thousands of threads/processes and +// stress the GC/scavenger thread-suspension signal until the process died. +test.skipIf(!isLinux)("Bun.openInEditor throws when no editor can be found", async () => { + using dir = tempDir("open-in-editor-none", { + "empty-path/.keep": "", + }); + + const env: Record = { + ...bunEnv, + PATH: join(String(dir), "empty-path"), + }; + delete env.EDITOR; + delete env.VISUAL; + + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + function f0(a1, a2, a3) {} + let threw = 0; + let silent = 0; + let lastError = ""; + for (let i = 0; i < 50; i++) { + try { + Bun.openInEditor(i % 2 ? "foo.js" : f0); + silent++; + } catch (e) { + threw++; + lastError = e.message; + } + } + Bun.gc(true); + console.log(JSON.stringify({ threw, silent, lastError })); + `, + ], + env, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ threw: 50, silent: 0, lastError: "Failed to auto-detect editor" }); + expect(proc.signalCode).toBeNull(); + expect(exitCode).toBe(0); +}); + // On Linux, JSC uses SIGPWR to suspend/resume threads for GC and the libpas // scavenger. Bun.openInEditor spawns a detached thread that goes through // bun.spawnSync, whose signal-forwarding setup must not touch SIGPWR or the