-
Notifications
You must be signed in to change notification settings - Fork 5k
spawnSync: make signal-forwarding register/unregister safe for concurrent callers #30956
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
base: main
Are you sure you want to change the base?
Changes from 12 commits
5da5c9b
4022a11
77baf7e
06e9176
ed97b02
79f6fd1
572391f
da8ae77
b4b612b
63eb355
c1d46cb
5f1f951
289bb83
be93485
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,8 +26,8 @@ | |
| // birth records its uniqueid — is narrowed by the freeze-then-rescan loop in | ||
| // `killTracked()` but cannot be fully closed from userspace. | ||
| // | ||
| // All state is process-global; spawnSync is single-threaded by design (see | ||
| // Bun__currentSyncPID), so no locking. | ||
| // All state is process-global; this is only reached from the thread that | ||
| // armed the parent-death watchdog (pdeathsig::is_arming_thread), so no locking. | ||
|
|
||
| #include "root.h" | ||
|
|
||
|
|
@@ -65,9 +65,8 @@ static_assert(sizeof(ProcUniqIdentifierInfo) == 56, "xnu ABI"); | |
| class NoOrphansTracker { | ||
| public: | ||
| // Function-local static: lazy first-use construction, no global ctor, | ||
| // thread-safe per C++11 [stmt.dcl]. spawnSync is single-threaded anyway | ||
| // (see Bun__currentSyncPID), but this keeps the binary's static-init | ||
| // section clean. | ||
| // thread-safe per C++11 [stmt.dcl]. Only the arming thread gets here | ||
| // anyway, but this keeps the binary's static-init section clean. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trimmed to one line in 289bb83; the threading clause was redundant with the file header. |
||
| static NoOrphansTracker& get() | ||
| { | ||
| static NoOrphansTracker instance; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -937,11 +937,14 @@ extern "C" int ffi_fileno(FILE* file) | |
| #if OS(LINUX) || OS(DARWIN) || OS(FREEBSD) | ||
| #include <signal.h> | ||
| #include <pthread.h> | ||
| #include <wtf/Lock.h> | ||
|
|
||
| // Note: We only ever use bun.spawnSync on the main thread. | ||
| // Bun.openInEditor runs spawnSync on detached threads, so register/unregister can overlap. | ||
|
claude[bot] marked this conversation as resolved.
|
||
| extern "C" int64_t Bun__currentSyncPID = 0; | ||
| static int Bun__pendingSignalToSend = 0; | ||
| static struct sigaction previous_actions[NSIG]; | ||
| static WTF::Lock signalForwardingLock; | ||
| static int signalForwardingDepth = 0; | ||
|
|
||
| // npm's signal list minus SIGIOT/SIGPOLL (aliases of SIGABRT/SIGIO; listing both would overwrite previous_actions[N]). | ||
| // https://github.com/npm/cli/blob/fefd509992a05c2dfddbe7bc46931c42f1da69d7/workspaces/arborist/lib/signals.js#L26-L57 | ||
|
|
@@ -1004,6 +1007,10 @@ extern "C" void Bun__sendPendingSignalIfNecessary() | |
|
|
||
| extern "C" void Bun__registerSignalsForForwarding() | ||
| { | ||
| WTF::Locker<WTF::Lock> locker(signalForwardingLock); | ||
| if (signalForwardingDepth++ != 0) | ||
| return; | ||
|
|
||
| Bun__pendingSignalToSend = 0; | ||
| struct sigaction sa; | ||
| memset(&sa, 0, sizeof(sa)); | ||
|
|
@@ -1032,6 +1039,10 @@ extern "C" void Bun__unregisterSignalsForForwarding() | |
| { | ||
| Bun__currentSyncPID = 0; | ||
|
|
||
| WTF::Locker<WTF::Lock> locker(signalForwardingLock); | ||
| if (--signalForwardingDepth != 0) | ||
| return; | ||
|
Comment on lines
1040
to
+1044
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Minor: Extended reasoning...What this is
Bun__currentSyncPID = 0; // line 1019 — unconditional
std::lock_guard<std::mutex> lock(signalForwardingLock);
if (--signalForwardingDepth != 0)
return; // inner caller: no-opThe depth counter added in db5ce58 is meant to make nested register/unregister pairs no-ops so only the outermost pair touches process-wide state. But the PID reset on line 1019 sits above both the lock and the depth check, so every caller — inner or outer — zeroes Concrete walk-through (the ordering where this bites)
If line 1019 were below the depth check, step 3 would leave Why this isn't already preventedThe new comment at lines 912-916 says "only the outermost pair touches process-wide signal dispositions" — and that's accurate for the Addressing the counter-argumentThere's a reasonable case that the current placement is intentional. In the opposite interleaving (main-thread So neither placement is correct for all interleavings — that's the acknowledged singleton limitation, and a real fix needs per-caller PID tracking (out of scope here). The point of this comment is narrower: the placement looks like an oversight relative to the depth guard added two lines below it, and there is at least one realistic ordering (steps 1-4 above) where it discards a still-valid PID that moving it would preserve. Suggested actionGiven the stale-PID tradeoff, I'd lean toward leaving the code as-is and adding a one-line comment above line 1019 noting that the reset is deliberately outside the depth guard (zero is safer than a possibly-reaped inner PID). Alternatively, drop line 1019 entirely — every caller already does
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes — the reset is intentionally outside the depth guard. With overlapping callers the PID slot is already a last-writer-wins singleton, so by the time an inner unregister runs, the slot usually holds the inner (now-reaped) child anyway. Zeroing it makes the handler fall back to the pending-signal path instead of kill()ing a possibly-recycled PID, which is the safer failure mode. A real fix is per-caller PID tracking, which is out of scope here. Leaving the code as-is; happy to add the one-line comment if a maintainer prefers it inline. |
||
|
|
||
| #define UNREGISTER_SIGNAL(SIG) \ | ||
| if (sigaction(SIG, &previous_actions[SIG], NULL) == -1) { \ | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,8 +9,8 @@ | |
| //! `bun_runtime` re-exports them. The only non-leaf dependencies are | ||
| //! `bun_io` (`FilePoll`/`KeepAlive`/`EventLoopCtx`), `bun_ptr` | ||
| //! (`ThreadSafeRefCount`), `bun_io` (`BufferedWriter`), `bun_event_loop`, | ||
| //! `bun_threading`, and `bun_crash_handler` — none of which depend back on | ||
| //! this crate, so no cycle. | ||
| //! and `bun_threading` — none of which depend back on this crate, so no | ||
| //! cycle. | ||
|
Comment on lines
+12
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the pre-existing crate doc listing |
||
|
|
||
| use core::ffi::c_char; | ||
|
|
||
|
|
||
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.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
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 header comment; the edit here only corrects a cross-reference that this PR made stale (it pointed at the Bun__currentSyncPID note, which now says the opposite). It documents why the file has no locking, which matters more now that the sibling signal-forwarding code does.