From 0e310b448c6b3bd9f66b089cdd0863cda2d2fd20 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 20 May 2026 14:57:00 +0000 Subject: [PATCH] Don't forward SIGPWR in sync-spawn signal forwarding On Linux, JSC's GC uses SIGPWR (via g_wtfConfig.sigThreadSuspendResume) to suspend and resume threads for conservative stack scanning. The sync-spawn signal-forwarding path (Bun__registerSignalsForForwarding) was installing an SA_RESETHAND forwarder for SIGPWR, which both: - resets the disposition to SIG_DFL after the first GC suspend, and - races on the shared previous_actions[] array when called from the detached background thread spawned by Bun.openInEditor, leaving the disposition as SIG_DFL once the last unregister runs after a memset. Either way, the next GC suspend terminates the process with SIGPWR. SIGPWR is already excluded from process.on() for the same reason; exclude it from the forwarding set too. --- src/jsc/bindings/c-bindings.cpp | 5 ++- test/js/bun/spawn/spawnSync-sigpwr-gc.test.ts | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 test/js/bun/spawn/spawnSync-sigpwr-gc.test.ts diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index a20749bef690..ee96df629867 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 000000000000..31ad34459e4f --- /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); + }, +);