diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index a20749bef690..50794b9781be 100644 --- a/src/jsc/bindings/c-bindings.cpp +++ b/src/jsc/bindings/c-bindings.cpp @@ -933,9 +933,12 @@ static struct sigaction previous_actions[NSIG]; M(SIGIO); #if OS(LINUX) +// SIGPWR is intentionally excluded: JSC's GC uses it (see wtf/posix/ThreadingPOSIX.cpp, +// g_wtfConfig.sigThreadSuspendResume) to suspend/resume threads for conservative stack +// scanning. Replacing that handler here would break the suspend protocol and, via +// SA_RESETHAND, revert SIGPWR to SIG_DFL so the next GC suspend terminates the process. #define FOR_EACH_LINUX_ONLY_SIGNAL(M) \ M(SIGPOLL); \ - M(SIGPWR); \ M(SIGSTKFLT); #endif diff --git a/test/js/bun/util/openInEditor.test.ts b/test/js/bun/util/openInEditor.test.ts new file mode 100644 index 000000000000..bb69634e0175 --- /dev/null +++ b/test/js/bun/util/openInEditor.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, isLinux, tempDirWithFiles } from "harness"; +import { chmodSync } from "node:fs"; +import { join } from "node:path"; + +// On Linux, JSC's GC uses SIGPWR to suspend/resume threads for conservative +// stack scanning. Bun.openInEditor spawns the editor via the sync spawn path, +// which installs signal-forwarding handlers for the duration of the spawn. If +// SIGPWR is in that set, a concurrent GC's pthread_kill(SIGPWR) goes to the +// forwarding handler instead of the suspend/resume handler, which either +// deadlocks the GC (the semaphore is never posted) or, because the forwarding +// handler uses SA_RESETHAND, resets SIGPWR to SIG_DFL so the next GC suspend +// terminates the process with signal 30. +test.skipIf(!isLinux)("Bun.openInEditor does not hijack the GC suspend/resume signal", async () => { + const dir = tempDirWithFiles("open-in-editor-gc", { + // The short sleep keeps the signal-forwarding window open long enough to + // overlap a GC on the main thread. + "code": "#!/bin/sh\nsleep 0.01\nexit 0\n", + "repro.js": ` + for (let i = 0; i < 200; i++) { + try { Bun.openInEditor("/tmp/whatever", 1); } catch {} + Bun.gc(true); + } + console.log("survived"); + `, + }); + chmodSync(join(dir, "code"), 0o755); + + await using proc = Bun.spawn({ + cmd: [bunExe(), join(dir, "repro.js")], + env: { + ...bunEnv, + PATH: `${dir}:${bunEnv.PATH ?? process.env.PATH}`, + EDITOR: undefined, + VISUAL: undefined, + }, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(proc.signalCode).toBeNull(); + expect(stderr).toBe(""); + expect(stdout.trim()).toBe("survived"); + expect(exitCode).toBe(0); +});