Skip to content
Open
7 changes: 4 additions & 3 deletions src/jsc/bindings/bun-spawn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ extern "C" ssize_t posix_spawn_bun(
char* const argv[],
char* const envp[])
{
sigset_t blockall, oldmask;
sigset_t blockall, oldmask, childmask;
int res = 0, cs = 0;

#if OS(DARWIN) || OS(FREEBSD)
Expand Down Expand Up @@ -260,6 +260,9 @@ extern "C" ssize_t posix_spawn_bun(
#endif

sigfillset(&blockall);
// The child execs with an empty mask, like libuv's uv__process_child_init:
// https://github.com/libuv/libuv/blob/b00d1bd225b602570baee82a6152eaa823a84fa6/src/unix/process.c#L374-L377
Comment thread
robobun marked this conversation as resolved.
Resolved
sigemptyset(&childmask);
sigprocmask(SIG_SETMASK, &blockall, &oldmask);
#if !OS(ANDROID)
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs);
Expand Down Expand Up @@ -303,8 +306,6 @@ extern "C" ssize_t posix_spawn_bun(
#endif

const auto startChild = [&]() -> ssize_t {
sigset_t childmask = oldmask;

// Reset signals
struct sigaction sa = { 0 };
sa.sa_handler = SIG_DFL;
Expand Down
43 changes: 21 additions & 22 deletions src/jsc/bindings/c-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1018,9 +1018,11 @@ extern "C" int ffi_fileno(FILE* file)
#include <signal.h>
#include <pthread.h>

// Note: We only ever use bun.spawnSync on the main thread.
extern "C" int64_t Bun__currentSyncPID = 0;
static int Bun__pendingSignalToSend = 0;
// The handler below can run on any thread that does not block the signal,
// concurrently with the spawning thread's store of the pid.
Comment thread
robobun marked this conversation as resolved.
Resolved
extern "C" std::atomic<int64_t> Bun__currentSyncPID { 0 }; // `AtomicI64` on the Rust side.
static_assert(std::atomic<int64_t>::is_always_lock_free);
static std::atomic<int> Bun__pendingSignalToSend { 0 };
static struct sigaction previous_actions[NSIG];

// npm's signal list minus SIGIOT/SIGPOLL (aliases of SIGABRT/SIGIO; listing both would overwrite previous_actions[N]).
Expand Down Expand Up @@ -1058,45 +1060,42 @@ static struct sigaction previous_actions[NSIG];
FOR_EACH_LINUX_ONLY_SIGNAL(M)
#endif

static void Bun__forwardSignalFromParentToChildAndRestorePreviousAction(pid_t pid, int sig)
// `pid` is the child or (negative) the group it leads, never bun itself.
static void Bun__forwardSignalToChild(pid_t pid, int sig)
{
sigset_t restore_mask;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, sig);
sigemptyset(&restore_mask);
sigaddset(&restore_mask, sig);
pthread_sigmask(SIG_BLOCK, &mask, &restore_mask);
kill(pid, sig);
pthread_sigmask(SIG_UNBLOCK, &restore_mask, nullptr);
}

// Store pid, then exchange the slot; the handler stores the slot, then loads
// the pid. One side sees the other's store, and the exchange picks the sender.
Comment thread
robobun marked this conversation as resolved.
Resolved
extern "C" void Bun__sendPendingSignalIfNecessary()
{
int sig = Bun__pendingSignalToSend;
Bun__pendingSignalToSend = 0;
int pid = Bun__currentSyncPID;
int sig = Bun__pendingSignalToSend.exchange(0);
int64_t pid = Bun__currentSyncPID.load();
if (sig == 0 || pid == 0)
return;

Bun__forwardSignalFromParentToChildAndRestorePreviousAction(pid, sig);
Bun__forwardSignalToChild(pid, sig);
}

extern "C" void Bun__registerSignalsForForwarding()
{
Bun__pendingSignalToSend = 0;
Bun__pendingSignalToSend.store(0);
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
// Not SA_RESETHAND: a nested runner sees the signal more than once and must not die on a repeat delivery.
sa.sa_flags = 0;
sa.sa_handler = [](int sig) {
if (Bun__currentSyncPID == 0) {
Bun__pendingSignalToSend = sig;
return;
int64_t pid = Bun__currentSyncPID.load();
if (pid == 0) {
Bun__pendingSignalToSend.store(sig);
pid = Bun__currentSyncPID.load();
if (pid == 0 || Bun__pendingSignalToSend.exchange(0) == 0)
return;
}

Bun__forwardSignalFromParentToChildAndRestorePreviousAction(Bun__currentSyncPID, sig);
Bun__forwardSignalToChild(pid, sig);
};

#define REGISTER_SIGNAL(SIG) \
Expand All @@ -1110,7 +1109,7 @@ extern "C" void Bun__registerSignalsForForwarding()

extern "C" void Bun__unregisterSignalsForForwarding()
{
Bun__currentSyncPID = 0;
Bun__currentSyncPID.store(0);

#define UNREGISTER_SIGNAL(SIG) \
if (sigaction(SIG, &previous_actions[SIG], NULL) == -1) { \
Expand Down
15 changes: 15 additions & 0 deletions src/runtime/bin_entry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,21 @@ pub(crate) unsafe extern "C" fn main(argc: c_int, argv: *const *const c_char) ->
libc::signal(libc::SIGXFSZ, libc::SIG_IGN);
}

// The blocked-signal mask survives execve, so a launcher that had (say)
// SIGTERM blocked starts bun with it blocked, and a blocked signal never
// reaches our handlers, JS `process.on(...)` listeners, or its default
// action. Clear it like Node's PlatformInit does; threads created from
// here on inherit the empty mask.
// https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/src/node.cc#L549-L555
// SAFETY: `set` is a valid out-pointer for `sigemptyset` and fully
// initialized before `pthread_sigmask` reads it; a null old-set is allowed.
#[cfg(unix)]
unsafe {
let mut set: libc::sigset_t = bun_core::ffi::zeroed();
libc::sigemptyset(&raw mut set);
libc::pthread_sigmask(libc::SIG_SETMASK, &raw const set, core::ptr::null_mut());
}

// Windows-only startup. Must run before anything reads
// `Bun.env`/`process.env` (env conversion).
#[cfg(windows)]
Expand Down
6 changes: 4 additions & 2 deletions src/spawn/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3264,7 +3264,7 @@ mod spawn_process_body {
}
}

Bun__currentSyncPID.store(0, core::sync::atomic::Ordering::Relaxed);
Bun__currentSyncPID.store(0, core::sync::atomic::Ordering::SeqCst);
let _signals = SignalForwarding::register();

// SAFETY: caller-built argv/envp are null-terminated C-string
Expand All @@ -3278,13 +3278,15 @@ mod spawn_process_body {
// Negative → kill() in the C++ signal forwarder targets the pgroup, so
// a SIGTERM/SIGINT delivered to `bun run` reaches every descendant
// that hasn't `setsid()`-escaped.
// SeqCst: ordered against the handler's store-then-load, see
// `Bun__sendPendingSignalIfNecessary`.
Comment thread
robobun marked this conversation as resolved.
Resolved
Bun__currentSyncPID.store(
if no_orphans {
-i64::from(process.pid)
} else {
i64::from(process.pid)
},
core::sync::atomic::Ordering::Relaxed,
core::sync::atomic::Ordering::SeqCst,
);

let mut jc = JobControl {
Expand Down
11 changes: 4 additions & 7 deletions src/spawn_sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,12 @@ pub mod ffi {
pub safe fn Bun__noOrphans_onFork();
pub safe fn Bun__noOrphans_onExit(pid: pid_t);

/// The PID to forward signals to. Set to 0 when unregistering.
///
/// C++ declares this as plain `int64_t`; `AtomicI64` is `#[repr(C)]`
/// with the same size/align, and the C side only does word-sized
/// loads/stores from the signal handler, so `Relaxed` here matches.
/// The PID to forward signals to (`std::atomic<int64_t>` in C++).
/// Set to 0 when unregistering.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub safe static Bun__currentSyncPID: core::sync::atomic::AtomicI64;

/// Race condition: a signal could be sent before `spawn_process_posix`
/// returns. Call after the child PID is known.
/// Forwards a signal that arrived before the child PID was known.
/// Call after a `SeqCst` store of the PID.
Comment thread
robobun marked this conversation as resolved.
Resolved
pub safe fn Bun__sendPendingSignalIfNecessary();
}
}
Expand Down
Loading
Loading