From 15ed2e2ac80a9a7d31a0bf772a87480cee755d2d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 19 May 2026 23:05:44 +0000 Subject: [PATCH] Don't forward SIGPWR in spawnSync signal handling On Linux, JSC uses SIGPWR for GC thread suspend/resume (see WTF ThreadingPOSIX.cpp). The spawnSync signal-forwarding list was copied from npm and included SIGPWR, so Bun__registerSignalsForForwarding would replace JSC's handler with a forwarding handler marked SA_RESETHAND. Bun.openInEditor() calls sync::spawn from a detached background thread, which meant multiple editor threads could race on the global previous_actions[] array and leave SIGPWR at SIG_DFL after the last unregister. The next GC-driven SIGPWR then terminated the process with signal 30. --- src/jsc/bindings/c-bindings.cpp | 4 ++- test/js/bun/util/openInEditor-gc.test.ts | 33 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 test/js/bun/util/openInEditor-gc.test.ts diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index a20749bef69..51ffe14a342 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 ThreadingPOSIX.cpp), so replacing its handler here would +// break the collector and can terminate the process with signal 30. #define FOR_EACH_LINUX_ONLY_SIGNAL(M) \ M(SIGPOLL); \ - M(SIGPWR); \ M(SIGSTKFLT); #endif diff --git a/test/js/bun/util/openInEditor-gc.test.ts b/test/js/bun/util/openInEditor-gc.test.ts new file mode 100644 index 00000000000..0c293458d59 --- /dev/null +++ b/test/js/bun/util/openInEditor-gc.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; + +// Bun.openInEditor spawns a detached thread that runs sync::spawn, which +// installs signal-forwarding handlers. On Linux the forwarding list used to +// include SIGPWR, which JSC uses for GC thread suspend/resume. Overlapping +// register/unregister calls from multiple editor threads could leave the +// SIGPWR disposition at SIG_DFL, so the next GC-driven SIGPWR killed the +// process with signal 30. +test.skipIf(process.platform !== "linux")( + "Bun.openInEditor does not clobber the GC thread-suspend signal handler", + async () => { + const script = ` + for (let i = 0; i < 30; i++) { + try { Bun.openInEditor("/tmp/bun-open-in-editor-gc-" + i); } catch {} + } + await Bun.sleep(50); + for (let i = 0; i < 100; i++) { + new Uint8Array(10000); + Bun.gc(true); + } + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: { ...bunEnv, PATH: "/nonexistent" }, + stdout: "ignore", + stderr: "ignore", + }); + const exitCode = await proc.exited; + expect(proc.signalCode).toBeNull(); + expect(exitCode).toBe(0); + }, +);