Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4413,6 +4413,8 @@ pub fn exec_self(args: &[&[u8]], keep_open: Option<Fd>) -> i32 {
let mut pointers: Vec<*const core::ffi::c_char> = argv.iter().map(|z| z.as_ptr()).collect();
pointers.push(core::ptr::null());
let exec_path = self_exe_path().expect("unreachable").as_ptr();
// The new image snapshots the stdio flags at its start, so put them back first.
crate::output::stdio::restore();
on_before_reload_process_posix(false);
// SAFETY: `pointers` is a null-terminated array of NUL-terminated strings that outlive the call, `environ` is the
// process's own, and `fcntl` takes no pointer.
Expand Down
47 changes: 35 additions & 12 deletions src/crash_handler/stopped_builds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,32 +135,55 @@ fn raise_with_default_action(signal: c_int) {
}
}

#[cfg(unix)]
unsafe extern "C" {
/// Bun's SIGINT and SIGTERM handler (c-bindings.cpp): it restores stdio and re-raises with the default action.
fn onExitSignal(signal: c_int);
}

/// Signals that had `onExitSignal` before the reporter. Relaxed: the SeqCst `BUILDS_UNDER_WAY` counter orders it.
#[cfg(unix)]
static REPLACED_EXIT_HANDLER: [AtomicBool; INTERRUPTIONS.len()] =
[const { AtomicBool::new(false) }; INTERRUPTIONS.len()];

#[cfg(unix)]
extern "C" fn handle_interruption(signal: c_int) {
write_what_threads_are_compiling();
raise_with_default_action(signal);
bun_core::output::source::stdio::restore();
// Ends the process when the kernel drops the re-raised signal too, as it does for PID 1.
bun_core::Global::raise_default_action(signal);
}

/// A signal that the program handles or ignores stays as it is.
/// A signal that the program handles or ignores stays as it is. Bun's own exit handler is replaced and put back.
#[cfg(unix)]
fn set_reports_interruptions(reports: bool) {
let ours = handle_interruption as extern "C" fn(c_int) as usize;
let (from, to) = if reports {
(libc::SIG_DFL, ours)
} else {
(ours, libc::SIG_DFL)
};
for signal in INTERRUPTIONS {
let exit_handler = onExitSignal as unsafe extern "C" fn(c_int) as usize;
for (index, signal) in INTERRUPTIONS.into_iter().enumerate() {
let mut current: libc::sigaction = bun_core::ffi::zeroed();
// SAFETY: a null new action only reads the current one into valid memory.
if unsafe { libc::sigaction(signal, core::ptr::null(), &raw mut current) } != 0
|| current.sa_sigaction != from
{
if unsafe { libc::sigaction(signal, core::ptr::null(), &raw mut current) } != 0 {
continue;
}
// One sigaction call each way, so no moment has SIG_DFL installed in between.
let (to, flags) = if reports {
let replaces_exit_handler = current.sa_sigaction == exit_handler;
if current.sa_sigaction != libc::SIG_DFL && !replaces_exit_handler {
continue;
}
REPLACED_EXIT_HANDLER[index].store(replaces_exit_handler, Ordering::Relaxed);
(ours, libc::SA_RESTART)
} else if current.sa_sigaction != ours {
continue;
} else if REPLACED_EXIT_HANDLER[index].load(Ordering::Relaxed) {
Comment thread
robobun marked this conversation as resolved.
Resolved
// The flags bun_install_exit_signal_handler uses.
(exit_handler, libc::SA_RESETHAND)
Comment thread
robobun marked this conversation as resolved.
Resolved
} else {
(libc::SIG_DFL, 0)
};
let mut action: libc::sigaction = bun_core::ffi::zeroed();
action.sa_sigaction = to;
action.sa_flags = libc::SA_RESTART;
action.sa_flags = flags;
// SAFETY: both point to valid memory, and the handler is never unloaded.
unsafe {
libc::sigemptyset(&raw mut action.sa_mask);
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/PosixSignalHandle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ extern "C" fn Bun__onPosixSignal(number: i32) {
&& WATCH_MODE_KILL_SIGNAL.load(Ordering::Relaxed) != 0
&& WATCH_SIGINT_LISTENERS.load(Ordering::Acquire) == 0
{
// Puts the stdio flags and termios back; fcntl, fstat and tcsetattr are async-signal-safe.
bun_core::output::source::stdio::restore();
// SAFETY: `_exit(2)` is async-signal-safe and takes no pointers.
unsafe { libc::_exit(0) };
}
Expand Down
44 changes: 42 additions & 2 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ extern "C" bool Bun__Node__ProcessPendingDeprecation;
extern "C" void Bun__writeProfilesBeforeSelfKill(bool signalEndsProcess);
#if !OS(WINDOWS)
extern "C" void onExitSignal(int);
extern "C" void bun_restore_stdio();
extern "C" void bun_install_exit_signal_handler(int);
#endif
extern "C" int32_t bun_stdio_tty[3];

Expand Down Expand Up @@ -1623,6 +1625,8 @@ static void uninstallForwardSignalHandler(int signalNumber)
if (void (*oldHandler)(int) = Bun::setSignalHandler(signalNumber, SIG_DFL); oldHandler != forwardSignal) {
// Don't uninstall the old handler if it's not the one we installed.
Bun::setSignalHandler(signalNumber, oldHandler);
} else if (signalNumber == SIGINT || signalNumber == SIGTERM) {
bun_install_exit_signal_handler(signalNumber);
}
}

Expand Down Expand Up @@ -2111,6 +2115,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionAbort, (JSGlobalObject * globalObject,
#else
// process.abort() is user-requested; bypass the crash handler so it does
// not print "Bun has crashed" or upload a report.
bun_restore_stdio();
restoreDefaultSignalDisposition(SIGABRT);
abort();
#endif
Expand All @@ -2124,6 +2129,36 @@ extern "C" ssize_t bun_close_range(unsigned int start, unsigned int end, unsigne
// Records the stdio fds whose FD_CLOEXEC bit execve() clears so a failed
// execve(2) can put them back: the failure now throws back to JS, and Node
// likewise restores exactly the flags it changed.
// What bun_restore_stdio() changes on fds 0-2, so a failed execve(2) can put it back for the script that goes on.
struct ExecveStdioRestorer {
int flags[3] = { -1, -1, -1 };
termios tio[3];
bool hasTio[3] = { false, false, false };

void save()
{
for (int fd = 0; fd < 3; fd++) {
flags[fd] = fcntl(fd, F_GETFL);
hasTio[fd] = bun_stdio_tty[fd] && tcgetattr(fd, &tio[fd]) == 0;
}
}
void restore()
{
// A background job gets SIGTTOU from tcsetattr, which would stop it.
sigset_t ttou;
sigemptyset(&ttou);
sigaddset(&ttou, SIGTTOU);
pthread_sigmask(SIG_BLOCK, &ttou, nullptr);
for (int fd = 0; fd < 3; fd++) {
if (flags[fd] != -1)
fcntl(fd, F_SETFL, flags[fd]);
if (hasTio[fd])
tcsetattr(fd, TCSANOW, &tio[fd]);
}
pthread_sigmask(SIG_UNBLOCK, &ttou, nullptr);
}
Comment thread
robobun marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
};

struct ExecveCloexecRestorer {
WTF::Vector<std::pair<int, int>, 8> changed; // (fd, original flags)

Expand Down Expand Up @@ -2319,6 +2354,11 @@ JSC_DEFINE_HOST_FUNCTION_WITH_ATTRIBUTES(Process_functionExecve, __attribute__((
}
}

// The replacement program inherits fds 0-2: put their startup flags back first.
ExecveStdioRestorer stdioRestorer;
stdioRestorer.save();
bun_restore_stdio();
Comment thread
robobun marked this conversation as resolved.
Resolved

int savedErrno;

#if OS(DARWIN)
Expand Down Expand Up @@ -2390,6 +2430,7 @@ JSC_DEFINE_HOST_FUNCTION_WITH_ATTRIBUTES(Process_functionExecve, __attribute__((
// execve(2) failed and the original image is still running. Restore the
// FD_CLOEXEC flags we cleared on stdio, then throw back to JS so the
// caller can handle it and recover. Node does the same (nodejs/node#62878).
stdioRestorer.restore();
cloexecRestorer.restore();

// Match Node's ErrnoException: a plain Error with errno/code/syscall/path.
Expand Down Expand Up @@ -5927,8 +5968,7 @@ static bool selfSentSignalEndsProcess(int pid, int ownPid, int signal)
sigset_t blocked;
if (pthread_sigmask(SIG_SETMASK, nullptr, &blocked) || sigismember(&blocked, signal))
return false;
// The default action, or Bun's own handler for SIGINT and SIGTERM when stdio is a terminal, which restores the
// terminal and raises the signal again with the default action.
// The default action, or onExitSignal, which restores stdio and re-raises with the default action.
struct sigaction current;
if (sigaction(signal, nullptr, &current) || (current.sa_flags & SA_SIGINFO))
return false;
Expand Down
72 changes: 55 additions & 17 deletions src/jsc/bindings/c-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,9 @@ extern "C" void Bun__onExit();
extern "C" int32_t bun_stdio_tty[3];
#if !OS(WINDOWS)
static termios termios_to_restore_later[3];
// Startup F_GETFL (-1 = not captured) and identity of each stdio fd, for the O_NONBLOCK restore at exit.
static int stdio_flags_to_restore[3] = { -1, -1, -1 };
static struct stat stdio_stat_at_start[3];
// Whether Bun itself has modified the termios of each stdio fd during this
// process's lifetime (e.g. via process.stdin.setRawMode). Used to decide
// whether the exit-time termios restore should fire when Bun is acting as a
Expand Down Expand Up @@ -933,6 +936,33 @@ extern "C" void bun_restore_stdio()
// the state the user expects back.
const bool pipeline_producer = bun_stdio_tty[1] == 0;

// Only the O_NONBLOCK bit. fcntl and fstat are async-signal-safe.
for (int32_t fd = 0; fd < 3; fd++) {
if (stdio_flags_to_restore[fd] == -1)
continue;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

int flags;
do
flags = fcntl(fd, F_GETFL);
while (flags == -1 && errno == EINTR);
if (flags == -1)
continue;

const int nonblock = stdio_flags_to_restore[fd] & O_NONBLOCK;
if ((flags & O_NONBLOCK) == nonblock)
continue;

// Skip a fd the script reopened onto another file.
struct stat now;
if (fstat(fd, &now) != 0 || now.st_dev != stdio_stat_at_start[fd].st_dev || now.st_ino != stdio_stat_at_start[fd].st_ino)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
continue;

int err;
do
err = fcntl(fd, F_SETFL, (flags & ~O_NONBLOCK) | nonblock);
while (err == -1 && errno == EINTR);
}

// restore stdio
for (int32_t fd = 0; fd < 3; fd++) {
if (!bun_stdio_tty[fd])
Expand Down Expand Up @@ -966,6 +996,23 @@ extern "C" void onExitSignal(int sig)
bun_restore_stdio();
Bun__raiseDefaultAction(sig);
}

// With a TTY this replaces an inherited SIG_IGN too, as before, so a `&` job in a package script dies on Ctrl-C.
extern "C" void bun_install_exit_signal_handler(int sig)
{
const bool anyTTY = bun_stdio_tty[0] || bun_stdio_tty[1] || bun_stdio_tty[2];
struct sigaction sa;
if (sigaction(sig, nullptr, &sa) != 0)
return;
if (sa.sa_handler != SIG_DFL && !(anyTTY && sa.sa_handler == SIG_IGN))
return;

memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESETHAND;
sa.sa_handler = onExitSignal;
sigaction(sig, &sa, nullptr);
}
#endif

#if OS(WINDOWS)
Expand Down Expand Up @@ -1064,8 +1111,6 @@ extern "C" void bun_initialize_process()

#if OS(LINUX) || OS(DARWIN) || OS(FREEBSD)

bool anyTTYs = false;

for (int fd = 0; fd < 3; fd++) {
int result = isatty(fd);
if (result == 0) {
Expand All @@ -1079,25 +1124,18 @@ extern "C" void bun_initialize_process()
do {
err = tcgetattr(fd, &termios_to_restore_later[fd]);
} while (err == -1 && errno == EINTR);
}

if (err == 0) [[likely]] {
anyTTYs = true;
}
if (fstat(fd, &stdio_stat_at_start[fd]) == 0) {
do {
stdio_flags_to_restore[fd] = fcntl(fd, F_GETFL);
} while (stdio_flags_to_restore[fd] == -1 && errno == EINTR);
}
}

// Restore TTY state on exit
if (anyTTYs) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);

sa.sa_flags = SA_RESETHAND;
sa.sa_handler = onExitSignal;

sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
}
// Restore stdio when SIGINT or SIGTERM kills the process.
bun_install_exit_signal_handler(SIGTERM);
bun_install_exit_signal_handler(SIGINT);
Comment thread
robobun marked this conversation as resolved.
Resolved
#elif OS(WINDOWS)
for (int fd = 0; fd <= 2; ++fd) {
auto handle = reinterpret_cast<HANDLE>(uv_get_osfhandle(fd));
Expand Down
Loading
Loading