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: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "f20ce7744553c910bcf16a33faf976af208de091";
export const WEBKIT_VERSION = "autobuild-preview-pr-741-242085de";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
36 changes: 36 additions & 0 deletions src/bun_core/Global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,42 @@ pub fn set_thread_name(name: &ZStr) {
}
}

/// The calling thread's name, whoever set it (bun, WTF, `std::thread`), or empty.
/// Called from the crash handler: a syscall on Linux, elsewhere a libpthread
/// read of the calling thread's own record.
Comment thread
robobun marked this conversation as resolved.
#[cfg(unix)]
pub fn current_thread_name(buf: &mut [u8; 64]) -> &[u8] {
buf.fill(0);
#[cfg(any(target_os = "linux", target_os = "android"))]
{
// SAFETY: PR_GET_NAME writes at most 16 bytes (TASK_COMM_LEN), NUL
// included, into `buf`.
if unsafe { libc::prctl(libc::PR_GET_NAME, buf.as_mut_ptr() as usize) } != 0 {
return &[];
}
}
#[cfg(target_os = "macos")]
{
// SAFETY: `buf` is writable for `buf.len()` bytes; the call writes a
// NUL-terminated name that fits.
if unsafe {
libc::pthread_getname_np(libc::pthread_self(), buf.as_mut_ptr().cast(), buf.len())
} != 0
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
return &[];
}
}
#[cfg(target_os = "freebsd")]
{
// SAFETY: `buf` is writable for `buf.len()` bytes; the call writes a
// NUL-terminated name that fits.
unsafe {
libc::pthread_get_name_np(libc::pthread_self(), buf.as_mut_ptr().cast(), buf.len());
}
}
crate::slice_to_nul(buf)
}
Comment thread
robobun marked this conversation as resolved.

// ──────────────────────────────────────────────────────────────────────────
// Exit callbacks
// ──────────────────────────────────────────────────────────────────────────
Expand Down
5 changes: 5 additions & 0 deletions src/bun_core/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub mod comptime_string_map;
pub mod error;
pub mod hint;
pub mod result;
#[cfg(unix)]
pub mod signal_stack;
pub mod thread_id;
pub mod tty;
pub mod util;
Expand Down Expand Up @@ -2604,6 +2606,9 @@ pub mod ffi {
// SAFETY: integer-array struct on the gated targets; all-zero is valid.
#[cfg(all(unix, not(target_vendor = "apple")))]
unsafe impl Zeroable for libc::sigset_t {}
// SAFETY: C POD (raw pointer + integer fields); all-zero is valid.
#[cfg(unix)]
unsafe impl Zeroable for libc::stack_t {}
// SAFETY: C POD (integer/array/raw-pointer fields only); all-zero is valid.
#[cfg(unix)]
unsafe impl Zeroable for libc::utsname {}
Expand Down
4 changes: 4 additions & 0 deletions src/bun_core/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@ impl Source {
SOURCE.with_borrow_mut(|s| unsafe {
Source::init(s, STDOUT_STREAM.read(), STDERR_STREAM.read())
});
#[cfg(unix)]
crate::signal_stack::install_for_current_thread();
crate::StackCheck::configure_thread();
}

Expand Down Expand Up @@ -380,6 +382,8 @@ impl Source {
SOURCE.with_borrow_mut(|s| unsafe {
Source::init(s, STDOUT_STREAM.read(), STDERR_STREAM.read())
});
#[cfg(unix)]
crate::signal_stack::install_for_current_thread();
// Intentionally NOT calling `crate::StackCheck::configure_thread()`.
}

Expand Down
87 changes: 87 additions & 0 deletions src/bun_core/signal_stack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Per-thread alternate signal stack for the crash handler. A stack overflow
//! faults on the guard page, so the kernel can deliver the signal only onto an
//! alternate stack (`SA_ONSTACK` handler + `sigaltstack(2)` on the thread).
//! `Output::Source::configure_thread` installs one on every bun thread; the
//! main thread uses the crash handler's static buffer.
Comment thread
robobun marked this conversation as resolved.

use core::cell::Cell;

/// Excludes the guard page below it. The crash handler runs its report on it.
pub const ALT_STACK_SIZE: usize = 512 * 1024;

/// This thread's alternate stack mapping; dropped by the thread-local destructor.
struct Mapping {
base: *mut libc::c_void,
len: usize,
}

impl Drop for Mapping {
fn drop(&mut self) {
let mut disable: libc::stack_t = crate::ffi::zeroed();
disable.ss_flags = libc::SS_DISABLE;
// Darwin's libc rejects a size below MINSIGSTKSZ, also for SS_DISABLE.
disable.ss_size = ALT_STACK_SIZE;
// SAFETY: `disable` is a valid `stack_t`; a null `old_ss` is permitted.
if unsafe { libc::sigaltstack(&raw const disable, core::ptr::null_mut()) } != 0 {
// Still registered: the kernel can write a signal frame to it.
return;
}
// SAFETY: `base`/`len` describe the mapping `install_for_current_thread`
// created, and the kernel no longer uses it as a signal stack.
unsafe { libc::munmap(self.base, self.len) };
}
}

thread_local! {
static MAPPING: Cell<Option<Mapping>> = const { Cell::new(None) };
}

/// Register an alternate signal stack for the calling thread. No-op when one is
/// already active (main thread, a repeat call, or ASAN's). Failure is silent.
Comment thread
robobun marked this conversation as resolved.
pub fn install_for_current_thread() {
let mut current: libc::stack_t = crate::ffi::zeroed();
// SAFETY: a null `ss` only queries; `current` is a valid out-pointer.
if unsafe { libc::sigaltstack(core::ptr::null(), &raw mut current) } != 0
|| current.ss_flags & libc::SS_DISABLE == 0
{
return;
}

// SAFETY: `sysconf` has no preconditions.
let page = match usize::try_from(unsafe { libc::sysconf(libc::_SC_PAGESIZE) }) {
Ok(page) if page > 0 => page,
_ => 4096,
};
let len = ALT_STACK_SIZE + page;
// SAFETY: anonymous private mapping; no file, no fixed address.
let base = unsafe {
libc::mmap(
core::ptr::null_mut(),
len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
if base == libc::MAP_FAILED {
return;
}
// Guard page: an overflow of the handler itself faults instead of writing
// into the mapping below.
// SAFETY: `base` is page-aligned and the first page belongs to the mapping.
unsafe { libc::mprotect(base, page, libc::PROT_NONE) };

let mut stack: libc::stack_t = crate::ffi::zeroed();
// SAFETY: `page` is within the mapping of `len` bytes.
stack.ss_sp = unsafe { base.byte_add(page) };
stack.ss_size = ALT_STACK_SIZE;
// SAFETY: `stack` describes a live, writable mapping; a null `old_ss` is
// permitted.
if unsafe { libc::sigaltstack(&raw const stack, core::ptr::null_mut()) } != 0 {
// SAFETY: the mapping was never registered; nothing else references it.
unsafe { libc::munmap(base, len) };
return;
}
MAPPING.with(|slot| slot.set(Some(Mapping { base, len })));
Comment thread
robobun marked this conversation as resolved.
}
1 change: 1 addition & 0 deletions src/bundler/BundleThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl<C: CompletionStruct> BundleThread<C> {
let ptr = SendPtr(instance);
let thread = std::thread::Builder::new()
.name("Bundler".into())
.stack_size(bun_threading::thread_pool::DEFAULT_THREAD_STACK_SIZE as usize)
.spawn(move || {
let ptr = ptr;
// SAFETY: caller guarantees `instance` is valid for 'static; `thread_main`
Expand Down
107 changes: 82 additions & 25 deletions src/crash_handler/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ mod draft {
Trap(usize),
/// Windows-only
DatatypeMisalignment,
/// Windows-only
/// Windows: `EXCEPTION_STACK_OVERFLOW`. POSIX: a guard-page SIGSEGV/SIGBUS.
StackOverflow,

/// Either `main` returned an error, or somewhere else in the code a trace string is printed.
Expand All @@ -648,11 +648,12 @@ mod draft {
/// been printed. Signal-originated crashes re-raise the original
/// fault so the parent process (and core-dump analyzers) see the
/// real cause instead of a misleading SIGILL/SIGTRAP from a trap
/// instruction; everything else (panics, OOM) uses SIGABRT.
/// instruction; everything else (panics, OOM) uses SIGABRT. A POSIX
/// `StackOverflow` is a classified SIGSEGV.
Comment thread
robobun marked this conversation as resolved.
#[cfg(unix)]
fn terminal_signal(&self) -> c_int {
match self {
CrashReason::SegmentationFault(_) => libc::SIGSEGV,
CrashReason::SegmentationFault(_) | CrashReason::StackOverflow => libc::SIGSEGV,
CrashReason::IllegalInstruction(_) => libc::SIGILL,
CrashReason::BusError(_) => libc::SIGBUS,
CrashReason::FloatingPointError(_) => libc::SIGFPE,
Expand All @@ -661,7 +662,6 @@ mod draft {
| CrashReason::Panic(_)
| CrashReason::Unreachable
| CrashReason::DatatypeMisalignment
| CrashReason::StackOverflow
| CrashReason::ZigError(_)
| CrashReason::OutOfMemory => libc::SIGABRT,
}
Expand Down Expand Up @@ -989,13 +989,23 @@ mod draft {
}
}
}
#[cfg(any(
target_os = "macos",
target_os = "linux",
target_os = "android",
target_os = "freebsd"
))]
{ /* no-op */ }
#[cfg(unix)]
{
let mut name_buf = [0u8; 64];
let name = bun_core::current_thread_name(&mut name_buf);
let written = if name.is_empty() {
write!(
writer,
"(thread {})",
bun_threading::current_thread_id()
)
} else {
write!(writer, "({})", bstr::BStr::new(name))
};
if written.is_err() {
abort();
}
}
}

if writer.write_all(b": ").is_err() {
Expand Down Expand Up @@ -1491,27 +1501,57 @@ mod draft {
ARCH_DISPLAY_STRING,
);

/// Extract `(pc, fp)` from the `ucontext_t` the kernel hands the signal
/// handler. Seeds the frame-pointer walk from the faulting frame. Returns
/// `None` on arch/OS combos we don't have register offsets for (the caller
/// then falls back to a current-stack capture).
/// Registers of the faulting frame, from the signal handler's `ucontext_t`.
#[cfg(unix)]
fn fault_context_from_ucontext(ctx: *mut c_void) -> Option<(usize, usize)> {
#[derive(Clone, Copy)]
struct FaultRegisters {
pc: usize,
fp: usize,
sp: usize,
}

#[cfg(unix)]
impl FaultRegisters {
/// An overflow is a data access in the frame being entered: just below
/// `sp` (`push`/`call`, red zone) or above it in a frame allocated in one
/// step. Never an instruction fetch, never above the frame pointer.
Comment thread
robobun marked this conversation as resolved.
fn is_stack_overflow(self, fault_addr: usize) -> bool {
const BELOW: usize = 4096;
const ABOVE: usize = 256 * 1024;
let mut end = self.sp.saturating_add(ABOVE);
if self.fp >= self.sp {
end = end.min(self.fp);
}
fault_addr != self.pc && fault_addr >= self.sp.saturating_sub(BELOW) && fault_addr < end
}
}

/// `pc`/`fp` seed the frame-pointer walk from the faulting frame. `None` on
/// arch/OS combos without register offsets (the caller then captures the
/// current stack).
Comment thread
robobun marked this conversation as resolved.
#[cfg(unix)]
fn fault_context_from_ucontext(ctx: *mut c_void) -> Option<FaultRegisters> {
debug_assert!(!ctx.is_null());
let uc = ctx.cast::<libc::ucontext_t>().cast_const();
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
// SAFETY: the kernel passes a valid ucontext_t as the handler's 3rd arg.
unsafe {
let mc = &(*uc).uc_mcontext;
let pc = mc.gregs[libc::REG_RIP as usize] as usize;
let fp = mc.gregs[libc::REG_RBP as usize] as usize;
Some((pc, fp))
Some(FaultRegisters {
pc: mc.gregs[libc::REG_RIP as usize] as usize,
fp: mc.gregs[libc::REG_RBP as usize] as usize,
sp: mc.gregs[libc::REG_RSP as usize] as usize,
})
}
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
// SAFETY: the kernel passes a valid ucontext_t as the handler's 3rd arg.
unsafe {
let mc = &(*uc).uc_mcontext;
Some((mc.pc as usize, mc.regs[29] as usize))
Some(FaultRegisters {
pc: mc.pc as usize,
fp: mc.regs[29] as usize,
sp: mc.sp as usize,
})
}
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
// SAFETY: the kernel passes a valid ucontext_t as the handler's 3rd arg.
Expand All @@ -1520,7 +1560,11 @@ mod draft {
if mc.is_null() {
return None;
}
Some(((*mc).__ss.__rip as usize, (*mc).__ss.__rbp as usize))
Some(FaultRegisters {
pc: (*mc).__ss.__rip as usize,
fp: (*mc).__ss.__rbp as usize,
sp: (*mc).__ss.__rsp as usize,
})
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
// SAFETY: the kernel passes a valid ucontext_t as the handler's 3rd arg.
Expand All @@ -1529,7 +1573,11 @@ mod draft {
if mc.is_null() {
return None;
}
Some(((*mc).__ss.__pc as usize, (*mc).__ss.__fp as usize))
Some(FaultRegisters {
pc: (*mc).__ss.__pc as usize,
fp: (*mc).__ss.__fp as usize,
sp: (*mc).__ss.__sp as usize,
})
}
#[cfg(not(any(
all(target_os = "linux", target_arch = "x86_64"),
Expand All @@ -1548,9 +1596,15 @@ mod draft {
// SAFETY: kernel provides a valid siginfo_t; `si_addr` reads the per-platform
// sigfault address field.
let addr: usize = unsafe { (*info).si_addr() as usize };
let registers = fault_context_from_ucontext(ctx);

crash_handler(
match sig {
libc::SIGSEGV | libc::SIGBUS
if registers.is_some_and(|r| r.is_stack_overflow(addr)) =>
{
CrashReason::StackOverflow
}
libc::SIGSEGV => CrashReason::SegmentationFault(addr),
libc::SIGILL => CrashReason::IllegalInstruction(addr),
libc::SIGBUS => CrashReason::BusError(addr),
Expand All @@ -1560,8 +1614,8 @@ mod draft {
// we do not register this handler for other signals
_ => unreachable!(),
},
match fault_context_from_ucontext(ctx) {
Some((pc, fp)) => TraceSeed::Fault { pc, fp },
match registers {
Some(FaultRegisters { pc, fp, .. }) => TraceSeed::Fault { pc, fp },
None => TraceSeed::None,
},
);
Expand Down Expand Up @@ -1597,11 +1651,14 @@ mod draft {

// SAFETY: stack points to a valid static buffer
if unsafe { libc::sigaltstack(&raw const stack, core::ptr::null_mut()) } == 0 {
act_.sa_flags |= libc::SA_ONSTACK;
// SAFETY: single global; only mutated during signal-handler setup
DID_REGISTER_SIGALTSTACK.store(true, Ordering::Relaxed);
}
}
// Keep the flag on every re-install, not only the first.
if DID_REGISTER_SIGALTSTACK.load(Ordering::Relaxed) {
act_.sa_flags |= libc::SA_ONSTACK;
}
}

let act_ptr: *const libc::sigaction = act
Expand Down
1 change: 1 addition & 0 deletions src/jsc/NodeCompileCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,7 @@ fn generate_bytecode(format: Format, code: &[u8], url: &[u8]) -> Option<Box<[u8]
// JSC parsing of large modules needs a deep stack.
.stack_size(16 * 1024 * 1024)
.spawn(move || {
bun_core::Output::Source::configure_thread();
for job in rx {
let url = BunString::clone_utf8(&job.url);
let result = crate::cached_bytecode::__bun_jsc_generate_cached_bytecode(
Expand Down
Loading
Loading