//! RFC 019 §7 — overflow diagnostics. //! //! One process-global SIGSEGV handler, installed once at [`crate::runtime::init`] //! (before any scheduler thread exists, so the PRIOR save is unracing), plus a //! per-scheduler-thread `sigaltstack` registered at `schedule_loop` entry — a //! guard hit means the faulting stack has no room to run anything, so the //! altstack is not optional. //! //! The handler classifies `si_addr` against the *current* actor only, reached //! through `preempt::CURRENT_SLOT` — a const-initialized `Cell<*const Slot>` //! whose access is a plain TLS load (no lazy init, no allocation, no dtor //! registration), and which every scheduler thread has materialized before an //! actor can run on it. The slot's diag atomics (`diag_stack_top` & co) are //! written in `install_actor` before the Release publish and are only consulted //! here while the actor is on-CPU, so they cannot be stale. //! //! Two classification tiers: //! - **In-guard**: definitive. Rust frames probe pages in order //! (`__rust_probestack`), so Rust overflow always lands here; so does any C //! built with `-fstack-clash-protection` (distro-packaged libraries), and — //! with the 1 MiB default guard — nearly every unprobed frame too. //! - **Overshoot**: within [`OVERSHOOT_SLOP`] *below* the guard. An unprobed //! frame (cargo-built C via `cc` almost never enables clash protection) //! large enough to step over the guard in one `sub rsp`. Attribution is //! "probable": the address is in unmapped VA that nothing else owns, an //! actor was on-CPU, and the distance fits a frame — the diagnostic says so. //! //! Classified faults print one line (async-signal-safe: stack buffer + //! `write(2)`, no fmt, no alloc, no locks) and re-raise with default //! disposition — no unwind, no resume, no fail-soft (jarred; UB-adjacent from //! a handler). Unclassified faults reinstate the PRIOR handler and refault, so //! std's own "thread ... has overflowed its stack" diagnostics for OS-thread //! stacks survive our presence. Reinstating deregisters us for good, which is //! fine: the process is dying either way. use std::cell::Cell; use std::mem::MaybeUninit; use std::sync::atomic::Ordering; use std::sync::Once; /// Tier-2 window below the guard. Matches the guard default (and the kernel's /// `stack_guard_gap`): a frame that out-jumps both the guard and this window /// in one displacement is past what a diagnostic can honestly attribute. pub(crate) const OVERSHOOT_SLOP: usize = 1024 * 1024; /// Per-scheduler-thread signal stack. MINSIGSTKSZ is ~11 KiB on AVX-512 /// hardware; 64 KiB leaves the formatter room without mattering to anyone. /// One per OS thread, never freed: scheduler threads live for the process in /// practice, and repeated `run()`s on reused threads re-use the registration /// (the TLS flag), so the leak is bounded by the OS thread count. const ALTSTACK_SIZE: usize = 64 * 1024; static INSTALL: Once = Once::new(); /// The handler that was installed before ours (std's, typically). Written /// exactly once inside INSTALL — which completes in `runtime::init` before /// any scheduler thread (and thus any classifiable fault) can exist — and /// only read from the handler afterwards. static mut PRIOR: MaybeUninit = MaybeUninit::uninit(); thread_local! { /// Whether this OS thread has registered its altstack. static ALTSTACK_SET: Cell = const { Cell::new(false) }; } /// Where a fault landed relative to the current actor's stack. #[derive(Debug, PartialEq, Eq)] pub(crate) enum FaultClass { /// Inside `[top − reserve − guard, top − reserve)`: the guard region. Guard, /// Within `OVERSHOOT_SLOP` below the guard: stepped over it. Payload is /// the distance below `guard_lo`. Overshoot(usize), /// Not ours to explain. Foreign, } /// Pure classifier — all edges unit-tested below. `top` is the stack's usable /// top, `reserve`/`guard` its shape; both page-rounded by `Stack::new`. pub(crate) fn classify(addr: usize, top: usize, reserve: usize, guard: usize) -> FaultClass { let guard_hi = top.wrapping_sub(reserve); let guard_lo = guard_hi.wrapping_sub(guard); if addr >= guard_lo && addr < guard_hi { FaultClass::Guard } else if addr < guard_lo && addr >= guard_lo.saturating_sub(OVERSHOOT_SLOP) { FaultClass::Overshoot(guard_lo - addr) } else { FaultClass::Foreign } } /// Install the process-global handler. Idempotent; called from /// `runtime::init`. pub(crate) fn install_once() { INSTALL.call_once(|| unsafe { let mut sa: libc::sigaction = std::mem::zeroed(); sa.sa_sigaction = handler as *const () as usize; sa.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK; libc::sigemptyset(&mut sa.sa_mask); let prior = &mut *std::ptr::addr_of_mut!(PRIOR); libc::sigaction(libc::SIGSEGV, &sa, prior.as_mut_ptr()); }); } /// Register this OS thread's altstack (idempotent per thread). Called at /// `schedule_loop` entry, so every thread that can run an actor has one. pub(crate) fn register_altstack() { ALTSTACK_SET.with(|set| { if set.get() { return; } unsafe { let sp = libc::mmap( std::ptr::null_mut(), ALTSTACK_SIZE, libc::PROT_READ | libc::PROT_WRITE, libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, -1, 0, ); if sp == libc::MAP_FAILED { // Degrade: no altstack means a guard hit dies without the // message (handler can't run) — the pre-RFC behavior, never // incorrectness. return; } let ss = libc::stack_t { ss_sp: sp, ss_flags: 0, ss_size: ALTSTACK_SIZE, }; libc::sigaltstack(&ss, std::ptr::null_mut()); } set.set(true); }); } // --------------------------------------------------------------------------- // The handler // --------------------------------------------------------------------------- unsafe extern "C" fn handler( _sig: libc::c_int, info: *mut libc::siginfo_t, _ctx: *mut libc::c_void, ) { let slot_ptr = crate::preempt::current_slot_ptr(); if !slot_ptr.is_null() { let slot = &*slot_ptr; let top = slot.diag_stack_top.load(Ordering::Relaxed); if top != 0 { let reserve = slot.diag_stack_reserve.load(Ordering::Relaxed); let guard = slot.diag_stack_guard.load(Ordering::Relaxed); let pid = slot.diag_pid.load(Ordering::Relaxed); let addr = (*info).si_addr() as usize; match classify(addr, top, reserve, guard) { FaultClass::Guard => { let mut b = Buf::new(); b.s("smarm: actor "); b.pid(pid); b.s(" overflowed its stack: fault in the guard region, depth-at-fault="); b.u(top - addr); b.s(" bytes (reserve="); b.u(reserve); b.s(", guard="); b.u(guard); b.s("). Raise stack_reserve (SpawnOpts or Config).\n"); b.emit(); die_by_default(); return; } FaultClass::Overshoot(below) => { let mut b = Buf::new(); b.s("smarm: actor "); b.pid(pid); b.s(" probably overflowed its stack: fault "); b.u(below); b.s(" bytes below the guard - an unprobed (FFI?) frame stepped over it (reserve="); b.u(reserve); b.s(", guard="); b.u(guard); b.s("). Raise stack_guard or stack_reserve.\n"); b.emit(); die_by_default(); return; } FaultClass::Foreign => {} } } } // Not ours: put back whoever was there before us and refault into them. let prior = &*std::ptr::addr_of!(PRIOR); libc::sigaction(libc::SIGSEGV, prior.as_ptr(), std::ptr::null_mut()); } /// Reset SIGSEGV to default disposition; returning from the handler then /// refaults at the same instruction and the process dies the normal death /// (core-dumpable, correct wait status), exactly as if we were never here — /// but with the message already on stderr. unsafe fn die_by_default() { let mut dfl: libc::sigaction = std::mem::zeroed(); dfl.sa_sigaction = libc::SIG_DFL; libc::sigemptyset(&mut dfl.sa_mask); libc::sigaction(libc::SIGSEGV, &dfl, std::ptr::null_mut()); } // --------------------------------------------------------------------------- // Async-signal-safe formatting: fixed buffer, decimal itoa, one write(2). // --------------------------------------------------------------------------- struct Buf { b: [u8; 320], len: usize, } impl Buf { fn new() -> Self { Buf { b: [0; 320], len: 0, } } fn s(&mut self, s: &str) { for &c in s.as_bytes() { if self.len < self.b.len() { self.b[self.len] = c; self.len += 1; } } } fn u(&mut self, mut n: usize) { let mut tmp = [0u8; 20]; let mut i = tmp.len(); loop { i -= 1; tmp[i] = b'0' + (n % 10) as u8; n /= 10; if n == 0 { break; } } for &c in &tmp[i..] { if self.len < self.b.len() { self.b[self.len] = c; self.len += 1; } } } /// `idx.gen`, unpacked from the install-time packing. fn pid(&mut self, packed: u64) { self.u((packed >> 32) as usize); self.s("."); self.u((packed & 0xffff_ffff) as usize); } fn emit(&self) { unsafe { libc::write(2, self.b.as_ptr() as *const libc::c_void, self.len); } } } // --------------------------------------------------------------------------- // Classifier units — the arithmetic edges, before anything integrates. // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::{classify, FaultClass, OVERSHOOT_SLOP}; const PG: usize = 4096; // A synthetic stack far from address-space edges: top at 1 GiB. const TOP: usize = 1 << 30; const RESERVE: usize = 16 * PG; const GUARD: usize = 4 * PG; const GUARD_HI: usize = TOP - RESERVE; const GUARD_LO: usize = GUARD_HI - GUARD; #[test] fn inside_guard_both_edges() { assert_eq!(classify(GUARD_LO, TOP, RESERVE, GUARD), FaultClass::Guard); assert_eq!( classify(GUARD_HI - 1, TOP, RESERVE, GUARD), FaultClass::Guard ); assert_eq!( classify(GUARD_LO + GUARD / 2, TOP, RESERVE, GUARD), FaultClass::Guard ); } #[test] fn usable_region_is_foreign() { // A fault inside the RW stack itself isn't a guard hit and must not // be explained as one. assert_eq!(classify(GUARD_HI, TOP, RESERVE, GUARD), FaultClass::Foreign); assert_eq!(classify(TOP - 1, TOP, RESERVE, GUARD), FaultClass::Foreign); } #[test] fn above_top_is_foreign() { assert_eq!(classify(TOP, TOP, RESERVE, GUARD), FaultClass::Foreign); assert_eq!(classify(TOP + PG, TOP, RESERVE, GUARD), FaultClass::Foreign); } #[test] fn overshoot_window_edges() { assert_eq!( classify(GUARD_LO - 1, TOP, RESERVE, GUARD), FaultClass::Overshoot(1) ); assert_eq!( classify(GUARD_LO - OVERSHOOT_SLOP, TOP, RESERVE, GUARD), FaultClass::Overshoot(OVERSHOOT_SLOP) ); assert_eq!( classify(GUARD_LO - OVERSHOOT_SLOP - 1, TOP, RESERVE, GUARD), FaultClass::Foreign ); } #[test] fn low_address_stack_saturates_not_wraps() { // A stack mapped so low that the slop window would underflow: the // window clips to 0 instead of wrapping around the address space. let top = RESERVE + GUARD + PG; // guard_lo == PG assert_eq!(classify(0, top, RESERVE, GUARD), FaultClass::Overshoot(PG)); // Null-page fault still classified only because it IS within slop // here; with a normal-height stack it is Foreign (covered above by // the window-edge test at realistic addresses). } }