Skip to main content

catallaxy_runtime/
stack.rs

1//! The native stack guard.
2//!
3//! Calls between bytecode functions do not use the native stack, but natives that call back into
4//! the VM (`Mutex#lock`, `Atomic#update`, `par_map`, a user `==` called by a dict lookup) and the
5//! structural operations in [`crate::ops`] do. [`check`] compares the stack pointer with a limit
6//! kept per thread, and turns a native recursion that would run off the end of the stack into a
7//! `SystemStackError` panic, which the program can catch, instead of a crash.
8//!
9//! The limit sits [`margin`] above the lowest usable address of the running stack. The margin
10//! covers what native code may use below the last check: one interpreter frame and the deepest
11//! native that does not check (formatting, JSON, regex, a collection), with room to spare.
12//!
13//! A thread's bounds are read from the OS the first time it checks (or when it starts, through
14//! [`init_thread`]). A fiber runs on its own stack: the scheduler swaps the limit with
15//! [`set_limit`] around every switch to it. Where the bounds are unknown (other platforms, Miri)
16//! the check never fires.
17
18use std::cell::Cell;
19
20use crate::mutator::{RtResult, Trap};
21
22thread_local! {
23    /// The lowest stack address a check accepts. 0: not read yet; 1: unknown (never fires).
24    static LIMIT: Cell<usize> = const { Cell::new(0) };
25}
26
27/// The panic raised when the stack runs out.
28pub const OVERFLOW: &str = "SystemStackError: stack level too deep";
29
30/// How much of a stack of `size` bytes is kept back below the limit: a quarter, at most
31/// 256 KB (64 KB of a fiber's 256 KB stack; 256 KB of a thread's).
32pub fn margin(size: usize) -> usize {
33    (size / 4).min(256 << 10)
34}
35
36/// The limit for a stack spanning `lo..hi` (its lowest address may include a guard page).
37pub fn limit_for(lo: usize, hi: usize) -> usize {
38    lo + margin(hi.saturating_sub(lo))
39}
40
41/// An address in the current stack frame.
42#[inline(always)]
43fn sp() -> usize {
44    let probe = 0u8;
45    std::hint::black_box(&probe) as *const u8 as usize
46}
47
48/// Raises `SystemStackError` if the native stack is within the margin of its end.
49#[inline]
50pub fn check() -> RtResult<()> {
51    let lim = LIMIT.with(|l| l.get());
52    if sp() < lim || lim == 0 {
53        return check_slow();
54    }
55    Ok(())
56}
57
58/// As [`check`], for a native about to use up to `extra` bytes of stack beyond the margin
59/// without checking (a bounded recursion such as parsing JSON).
60pub fn check_extra(extra: usize) -> RtResult<()> {
61    let mut lim = LIMIT.with(|l| l.get());
62    if lim == 0 {
63        lim = init_thread();
64    }
65    if lim > 1 && sp() < lim.saturating_add(extra) {
66        return Err(Trap::Panic(OVERFLOW.into()));
67    }
68    Ok(())
69}
70
71#[cold]
72#[inline(never)]
73fn check_slow() -> RtResult<()> {
74    let mut lim = LIMIT.with(|l| l.get());
75    if lim == 0 {
76        lim = init_thread();
77    }
78    if sp() < lim {
79        return Err(Trap::Panic(OVERFLOW.into()));
80    }
81    Ok(())
82}
83
84/// Reads the current thread's stack bounds from the OS and sets its limit, which it returns.
85pub fn init_thread() -> usize {
86    let lim = match thread_bounds() {
87        Some((lo, hi)) if lo < hi => limit_for(lo, hi),
88        _ => 1,
89    };
90    LIMIT.with(|l| l.set(lim));
91    lim
92}
93
94/// Replaces the current limit, returning the previous one (for switching to a fiber's stack
95/// and back).
96pub fn set_limit(lim: usize) -> usize {
97    LIMIT.with(|l| l.replace(lim))
98}
99
100/// The current thread's stack as `(lowest, highest)` address.
101#[cfg(all(not(miri), any(target_os = "linux", target_os = "android")))]
102fn thread_bounds() -> Option<(usize, usize)> {
103    // SAFETY: plain queries about the calling thread; the attribute object is initialized by
104    // `pthread_getattr_np` and destroyed here.
105    unsafe {
106        let mut attr: libc::pthread_attr_t = std::mem::zeroed();
107        if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
108            return None;
109        }
110        let mut addr: *mut libc::c_void = std::ptr::null_mut();
111        let mut size: libc::size_t = 0;
112        let r = libc::pthread_attr_getstack(&attr, &mut addr, &mut size);
113        libc::pthread_attr_destroy(&mut attr);
114        (r == 0 && !addr.is_null()).then(|| (addr as usize, addr as usize + size))
115    }
116}
117
118#[cfg(all(not(miri), any(target_os = "macos", target_os = "ios")))]
119fn thread_bounds() -> Option<(usize, usize)> {
120    // SAFETY: plain queries about the calling thread.
121    unsafe {
122        let t = libc::pthread_self();
123        let hi = libc::pthread_get_stackaddr_np(t) as usize;
124        let size = libc::pthread_get_stacksize_np(t);
125        (hi > size).then(|| (hi - size, hi))
126    }
127}
128
129#[cfg(all(not(miri), windows))]
130fn thread_bounds() -> Option<(usize, usize)> {
131    #[link(name = "kernel32")]
132    extern "system" {
133        fn GetCurrentThreadStackLimits(low: *mut usize, high: *mut usize);
134    }
135    let (mut lo, mut hi) = (0usize, 0usize);
136    // SAFETY: writes the calling thread's stack bounds to the two locals.
137    unsafe { GetCurrentThreadStackLimits(&mut lo, &mut hi) };
138    Some((lo, hi))
139}
140
141#[cfg(any(
142    miri,
143    not(any(target_os = "linux", target_os = "android", target_os = "macos", target_os = "ios", windows))
144))]
145fn thread_bounds() -> Option<(usize, usize)> {
146    None
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn shallow_check_passes() {
155        assert!(check().is_ok());
156    }
157
158    #[test]
159    fn limit_above_the_stack_fires() {
160        let prev = set_limit(usize::MAX);
161        let r = check();
162        set_limit(prev);
163        assert!(matches!(r, Err(Trap::Panic(m)) if m == OVERFLOW));
164    }
165
166    #[cfg(not(miri))]
167    #[test]
168    fn deep_native_recursion_is_caught() {
169        fn down(n: u64) -> RtResult<u64> {
170            check()?;
171            let pad = std::hint::black_box([n; 32]);
172            Ok(down(n + 1)? + pad[0])
173        }
174        // Run on a small thread so the test is quick.
175        let r = std::thread::Builder::new().stack_size(1 << 20).spawn(|| down(0)).unwrap().join().unwrap();
176        assert!(r.is_err());
177    }
178}