331 lines
12 KiB
Rust
331 lines
12 KiB
Rust
//! 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<libc::sigaction> = MaybeUninit::uninit();
|
||
|
||
thread_local! {
|
||
/// Whether this OS thread has registered its altstack.
|
||
static ALTSTACK_SET: Cell<bool> = 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).
|
||
}
|
||
}
|