diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index a20749bef69..571d4ed8909 100644 --- a/src/jsc/bindings/c-bindings.cpp +++ b/src/jsc/bindings/c-bindings.cpp @@ -908,14 +908,23 @@ extern "C" int ffi_fileno(FILE* file) #if OS(LINUX) || OS(DARWIN) || OS(FREEBSD) #include #include +#include -// Note: We only ever use bun.spawnSync on the main thread. +// bun.spawnSync is primarily used from the main thread (e.g. `bun run`), but +// Bun.openInEditor spawns detached threads that also go through this path. +// The depth counter + lock below keep previous_actions[] from being corrupted +// by overlapping register/unregister calls; only the outermost pair touches +// process-wide signal dispositions. extern "C" int64_t Bun__currentSyncPID = 0; static int Bun__pendingSignalToSend = 0; static struct sigaction previous_actions[NSIG]; +static std::mutex signalForwardingLock; +static int signalForwardingDepth = 0; // This list of signals is copied from npm. // https://github.com/npm/cli/blob/fefd509992a05c2dfddbe7bc46931c42f1da69d7/workspaces/arborist/lib/signals.js#L26-L57 +// SIGIOT is omitted because it aliases SIGABRT; registering both would +// overwrite previous_actions[SIGABRT] with our own handler. #define FOR_EACH_POSIX_SIGNAL(M) \ M(SIGABRT); \ M(SIGALRM); \ @@ -929,13 +938,14 @@ static struct sigaction previous_actions[NSIG]; M(SIGTRAP); \ M(SIGSYS); \ M(SIGQUIT); \ - M(SIGIOT); \ M(SIGIO); #if OS(LINUX) +// SIGPWR is intentionally excluded: JSC uses it for GC thread suspend/resume +// (see WTF/wtf/posix/ThreadingPOSIX.cpp). Replacing its handler can terminate +// the process when the GC next signals a thread. +// SIGPOLL is omitted because it aliases SIGIO (see SIGIOT note above). #define FOR_EACH_LINUX_ONLY_SIGNAL(M) \ - M(SIGPOLL); \ - M(SIGPWR); \ M(SIGSTKFLT); #endif @@ -976,6 +986,10 @@ extern "C" void Bun__sendPendingSignalIfNecessary() extern "C" void Bun__registerSignalsForForwarding() { + std::lock_guard lock(signalForwardingLock); + if (signalForwardingDepth++ != 0) + return; + Bun__pendingSignalToSend = 0; struct sigaction sa; memset(&sa, 0, sizeof(sa)); @@ -1003,6 +1017,10 @@ extern "C" void Bun__unregisterSignalsForForwarding() { Bun__currentSyncPID = 0; + std::lock_guard lock(signalForwardingLock); + if (--signalForwardingDepth != 0) + return; + #define UNREGISTER_SIGNAL(SIG) \ if (sigaction(SIG, &previous_actions[SIG], NULL) == -1) { \ } 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..ba19544d191 --- /dev/null +++ b/test/js/bun/spawn/spawnSync-sigpwr-gc.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from "bun:test"; +import { symlinkSync } from "fs"; +import { bunEnv, bunExe, isLinux, tempDir } from "harness"; +import { join } from "path"; + +// Regression: spawnSync's signal-forwarding list included SIGPWR on Linux. +// JSC uses SIGPWR to suspend/resume threads for GC. Bun.openInEditor spawns a +// detached thread per call that runs spawnSync; concurrent calls race on the +// process-wide previous-handler table and can leave SIGPWR at SIG_DFL. The +// next GC suspend then terminates the process with signal 30. +test.skipIf(!isLinux)("spawnSync signal forwarding does not clobber JSC's SIGPWR handler", async () => { + const sleepBin = Bun.which("sleep"); + expect(sleepBin).toBeTruthy(); + + using dir = tempDir("spawnSync-sigpwr", { + "run.js": ` +for (let i = 0; i < 64; i++) { + try { Bun.openInEditor("0.2"); } catch {} +} +let junk = []; +for (let i = 0; i < 2000; i++) { + junk.push({ a: new Uint8Array(4096).fill(i), b: { c: i } }); +} +for (let i = 0; i < 30; i++) Bun.gc(true); +console.log("ok"); +`, + }); + // Make `code` (first in the editor preference list) resolve to `sleep`, so + // each background spawnSync holds its signal-forwarding window open long + // enough for threads to overlap. + symlinkSync(sleepBin!, join(String(dir), "code")); + + await using proc = Bun.spawn({ + cmd: [bunExe(), join(String(dir), "run.js")], + env: { + ...bunEnv, + PATH: String(dir), + EDITOR: "", + VISUAL: "", + }, + stdout: "pipe", + stderr: "inherit", + }); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + + expect(proc.signalCode).toBeNull(); + expect(stdout.trim()).toBe("ok"); + expect(exitCode).toBe(0); +});