diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index a20749bef69..ee96df62986 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 to suspend/resume +// threads (see WTF/wtf/posix/ThreadingPOSIX.cpp). Overriding it with an +// SA_RESETHAND forwarder resets the disposition to SIG_DFL after the first +// GC suspend, and the next GC SIGPWR terminates the process. #define FOR_EACH_LINUX_ONLY_SIGNAL(M) \ M(SIGPOLL); \ - M(SIGPWR); \ M(SIGSTKFLT); #endif diff --git a/test/js/bun/spawn/spawnSync-sigpwr-gc.test.ts b/test/js/bun/spawn/spawnSync-sigpwr-gc.test.ts new file mode 100644 index 00000000000..31ad34459e4 --- /dev/null +++ b/test/js/bun/spawn/spawnSync-sigpwr-gc.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// On Linux, JSC's GC uses SIGPWR to suspend/resume threads for conservative +// stack scanning. Bun.openInEditor spawns a detached thread that runs the +// internal sync-spawn path, which installs a temporary signal-forwarding +// handler for the signals it knows about. If SIGPWR is in that set, +// concurrent register/unregister races on the shared previous_actions[] +// array leave the SIGPWR disposition as SIG_DFL, and the next GC suspend +// terminates the process with SIGPWR. +test.skipIf(process.platform !== "linux")( + "sync-spawn signal forwarding does not override the GC's SIGPWR handler", + async () => { + const script = ` + for (let i = 0; i < 100; i++) { + try { Bun.openInEditor("/tmp/__nonexistent__"); } catch {} + } + await Bun.sleep(200); + for (let g = 0; g < 100; g++) { + new Array(10000).fill({ x: g }); + Bun.gc(true); + } + process.exit(0); + `; + + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + // Empty PATH / no EDITOR so auto-detect picks Editor::None and the + // background thread's posix_spawn fails immediately instead of + // launching a real editor. + env: { ...bunEnv, PATH: "", EDITOR: "", VISUAL: "" }, + stdio: ["ignore", "ignore", "pipe"], + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect(proc.signalCode).not.toBe("SIGPWR"); + expect(proc.signalCode).toBeNull(); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + }, +);