From 80b86f4cc51c092fdd8df38451bca16177dd4ae5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 18 May 2026 14:09:57 +0000 Subject: [PATCH] spawn: don't forward SIGPWR on Linux On Linux, JSC uses SIGPWR to suspend/resume mutator threads for GC (WTF/wtf/posix/ThreadingPOSIX.cpp). Bun__registerSignalsForForwarding() was installing a SA_RESETHAND forwarding handler for SIGPWR, replacing JSC's signalHandlerSuspendResume process-wide for the duration of each sync::spawn(). When multiple threads call sync::spawn() concurrently (e.g. via Bun.openInEditor, which spawns a detached thread per call), the register/unregister pair races on the global previous_actions[] array: one thread's unregister memsets the saved handler to zero, so the next unregister restores SIG_DFL for SIGPWR. The next GC-sent SIGPWR then terminates the process with signal 30. The signal list was copied from npm, which runs on Node.js and has no special use for SIGPWR. Drop SIGPWR from the Linux forwarding list. Also replace a stray lowercase assert() in wtf-bindings.cpp with WTF's ASSERT so the file compiles standalone in a unified build. --- src/jsc/bindings/c-bindings.cpp | 4 +++- src/jsc/bindings/wtf-bindings.cpp | 2 +- test/js/bun/spawn/spawnSync.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index a20749bef69..1b42f7dc364 100644 --- a/src/jsc/bindings/c-bindings.cpp +++ b/src/jsc/bindings/c-bindings.cpp @@ -933,9 +933,11 @@ static struct sigaction previous_actions[NSIG]; M(SIGIO); #if OS(LINUX) +// SIGPWR is intentionally excluded: JSC uses it for GC thread suspend/resume +// on Linux (see WTF/wtf/posix/ThreadingPOSIX.cpp). Replacing that handler here +// makes the next GC-sent SIGPWR terminate the process. #define FOR_EACH_LINUX_ONLY_SIGNAL(M) \ M(SIGPOLL); \ - M(SIGPWR); \ M(SIGSTKFLT); #endif diff --git a/src/jsc/bindings/wtf-bindings.cpp b/src/jsc/bindings/wtf-bindings.cpp index 0d4968f805f..8b09b5a8665 100644 --- a/src/jsc/bindings/wtf-bindings.cpp +++ b/src/jsc/bindings/wtf-bindings.cpp @@ -61,7 +61,7 @@ extern "C" int uv_tty_reset_mode(void) static void uv__tty_make_raw(struct termios* tio) { - assert(tio != NULL); + ASSERT(tio != NULL); #if defined __sun || defined __MVS__ /* diff --git a/test/js/bun/spawn/spawnSync.test.ts b/test/js/bun/spawn/spawnSync.test.ts index bb60b9cad9e..701413ba817 100644 --- a/test/js/bun/spawn/spawnSync.test.ts +++ b/test/js/bun/spawn/spawnSync.test.ts @@ -40,4 +40,31 @@ describe("spawnSync", () => { it.skipIf(!isPosix)("should use spawnSync optimizations when possible", () => { expect([join(import.meta.dir, "spawnSync-counters-fixture.ts")]).toRun(); }); + + // On Linux, JSC uses SIGPWR to suspend/resume threads for GC. The spawnSync + // signal-forwarding table used to include SIGPWR, so a GC that fired while + // (or after) spawnSync ran would terminate the process with signal 30. + it.skipIf(process.platform !== "linux")("does not clobber the GC thread-suspend signal handler", () => { + const result = Bun.spawnSync({ + cmd: [ + bunExe(), + "-e", + ` + for (let i = 0; i < 50; i++) { + Bun.spawnSync({ cmd: ["true"] }); + Bun.gc(true); + } + for (let i = 0; i < 50; i++) { + Bun.spawnSync({ cmd: ["true"] }); + } + Bun.gc(true); + `, + ], + env: bunEnv, + stdout: "inherit", + stderr: "inherit", + }); + expect(result.signalCode).toBeFalsy(); + expect(result.exitCode).toBe(0); + }); });