-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Don't forward SIGPWR in sync-spawn signal forwarding #31131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }, | ||
| ); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟣 Pre-existing, but since you're already pruning this list:
SIGIOTis an alias forSIGABRT(both 6) andSIGPOLLis an alias forSIGIO(both 29 on Linux), soREGISTER_SIGNALruns twice for the sameprevious_actions[N]slot — the secondsigaction()overwrites the saved original handler with the forwarder we just installed, andBun__unregisterSignalsForForwardingthen "restores" the forwarder instead of the user's handler. DroppingSIGIOTfromFOR_EACH_POSIX_SIGNALandSIGPOLLhere would fix it with the same rationale as the SIGPWR removal.Extended reasoning...
What the bug is
FOR_EACH_POSIX_SIGNALlists bothSIGABRTandSIGIOT, and on LinuxFOR_EACH_SIGNALaddsSIGPOLLon top of theSIGIOalready in the POSIX list. On Linux these are aliases for the same signal number —SIGABRT == SIGIOT == 6andSIGIO == SIGPOLL == 29(verified against/usr/include/asm-generic/signal.hand by compiling a test program). The npm list this was copied from uses string signal names so duplicates were harmless there, but here they're integer macros that collide on the sameprevious_actions[]index.The code path that triggers it
REGISTER_SIGNAL(SIG)expands tosigaction(SIG, &sa, &previous_actions[SIG]). SoBun__registerSignalsForForwarding()does, in order:sigaction(SIGABRT /*6*/, &sa, &previous_actions[6])— installs the forwarder, saves the original SIGABRT handler intoprevious_actions[6].sigaction(SIGIOT /*6*/, &sa, &previous_actions[6])— installs the forwarder again (no-op), but the old action returned is now the forwarder that step 1 just installed, which overwrites the original handler saved inprevious_actions[6].The same thing happens for
SIGIO(step N) followed bySIGPOLL(step N+k) at index 29.Then in
Bun__unregisterSignalsForForwarding(),UNREGISTER_SIGNAL(SIGABRT)andUNREGISTER_SIGNAL(SIGIOT)both writeprevious_actions[6]— the forwarder — back as the active handler. The original handler is gone.Why nothing repairs it
The only thing that runs after unregister is
crash_handler.resetOnPosix()(process.zig:2400), which only re-installs handlers forSIGSEGV/SIGILL/SIGBUS/SIGFPE. It does not touchSIGABRTorSIGIO, so the corruption persists.Step-by-step proof of impact
process.on('SIGABRT', handler)→ libuv installs a SIGABRT handler.Bun.spawnSync(...)→Bun__registerSignalsForForwarding()runs. After theSIGIOTiteration,previous_actions[6]holds the forwarder lambda withSA_RESETHAND, not libuv's handler.Bun__unregisterSignalsForForwarding()"restores"previous_actions[6], i.e. re-installs the forwarder.crash_handler.resetOnPosix()doesn't touch SIGABRT.SIGABRTto the process. The forwarder runs, seesBun__currentSyncPID == 0, stashes the signal inBun__pendingSignalToSend, and returns — the signal is silently swallowed.SA_RESETHANDthen resets SIGABRT toSIG_DFL.process.on('SIGABRT')handler never fires, and the next SIGABRT core-dumps the process viaSIG_DFL.Same story for
SIGIO/SIGPOLLat index 29.How to fix
Drop the redundant alias entries: remove
M(SIGIOT);fromFOR_EACH_POSIX_SIGNAL(it's identical toSIGABRTon every platform we support) and removeM(SIGPOLL);fromFOR_EACH_LINUX_ONLY_SIGNAL(it's identical toSIGIOon Linux). This is the same kind of one-line list cleanup as theSIGPWRremoval in this PR.This is pre-existing — not introduced by this PR — but the PR edits the line directly adjacent to
SIGPOLLinFOR_EACH_LINUX_ONLY_SIGNALfor the same class of "this signal shouldn't be in the forwarding list" reason, so it seemed worth flagging while you're here. Not blocking.