Skip to main content

logicaffeine_system/
stack.rs

1//! The stack a compiled program's `Main` runs on.
2//!
3//! A LOGOS program recurses as deep as its data: a tree of a million nodes is built and walked by recursion. A
4//! compiled program's `Main` therefore runs on a stack of [`MAIN_STACK_BYTES`] mapped for it at start, on the thread
5//! that started it. Switching stacks rather than starting a thread keeps the process single-threaded, and that is
6//! worth having: once a second thread exists, even one only waiting for another to finish, the C library takes its
7//! locked, atomic path through every `malloc` and `free`, which doubled the time of an allocation-heavy program.
8//!
9//! Recursion that runs past the end of the stack reaches an inaccessible guard page. The program then reports
10//! [`logicaffeine_data::failure::CALL_DEPTH_EXCEEDED`], the sentence every tier reports for it, after writing out what
11//! it had shown, and ends with exit status 1. A native target whose stack the switch does not support runs `Main` on a
12//! thread of its own instead, and WebAssembly, which has one thread and a stack its module declares, runs it in place.
13
14/// The room a compiled program's `Main` has for its calls.
15pub const MAIN_STACK_BYTES: usize = 64 * 1024 * 1024;
16
17/// Run `main`, a compiled program's `Main`, on a stack of [`MAIN_STACK_BYTES`].
18///
19/// A failure no boundary catches ends the process in the program's failure hook, so nothing unwinds out of `main`.
20/// Generic, so `Main` is compiled into the call that switches to its stack, as it was into a thread's start.
21#[inline]
22pub fn run_main<F: FnOnce() + Send + 'static>(main: F) {
23    switch::run_main(main);
24}
25
26/// `main` on a thread of its own with a stack of [`MAIN_STACK_BYTES`]; a failure that reaches the thread's end ends
27/// the program with exit status 1.
28#[allow(dead_code)]
29fn on_a_thread<F: FnOnce() + Send + 'static>(main: F) {
30    let outcome = std::thread::Builder::new()
31        .stack_size(MAIN_STACK_BYTES)
32        .spawn(main)
33        .expect("the program's thread starts")
34        .join();
35    if outcome.is_err() {
36        std::process::exit(1);
37    }
38}
39
40#[cfg(unix)]
41mod switch {
42    psm::psm_stack_manipulation! {
43        yes {
44            #[inline]
45            pub(super) fn run_main<F: FnOnce() + Send + 'static>(main: F) {
46                let Some(stack) = super::mapped::Stack::map(super::MAIN_STACK_BYTES) else {
47                    return super::on_a_thread(main);
48                };
49                super::overflow::watch(stack.guards());
50                /// Ends the process if a panic ever unwinds to the switch: unwinding across it is undefined.
51                struct NoUnwind;
52                impl Drop for NoUnwind {
53                    fn drop(&mut self) {
54                        std::process::abort();
55                    }
56                }
57                // SAFETY: `stack` maps `size` readable, writable bytes from `base`, between two inaccessible
58                // pages, and outlives the call; `NoUnwind` keeps an unwind from crossing the switch.
59                unsafe {
60                    psm::on_stack(stack.base(), stack.size(), || {
61                        let no_unwind = NoUnwind;
62                        main();
63                        std::mem::forget(no_unwind);
64                    });
65                }
66            }
67        }
68        no {
69            pub(super) fn run_main<F: FnOnce() + Send + 'static>(main: F) {
70                super::on_a_thread(main);
71            }
72        }
73    }
74}
75
76#[cfg(all(not(unix), not(target_arch = "wasm32")))]
77mod switch {
78    pub(super) fn run_main<F: FnOnce() + Send + 'static>(main: F) {
79        super::on_a_thread(main);
80    }
81}
82
83#[cfg(target_arch = "wasm32")]
84mod switch {
85    pub(super) fn run_main<F: FnOnce() + Send + 'static>(main: F) {
86        main();
87    }
88}
89
90#[cfg(unix)]
91mod mapped {
92    /// An anonymous mapping of `size` readable, writable bytes between two inaccessible guard pages, whichever way
93    /// the target's stack grows.
94    pub(super) struct Stack {
95        mapping: *mut u8,
96        total: usize,
97        page: usize,
98        size: usize,
99    }
100
101    impl Stack {
102        /// The mapping, or `None` when the system will not map it.
103        pub(super) fn map(size: usize) -> Option<Stack> {
104            // SAFETY: `sysconf` reads a constant of the system.
105            let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
106            let page = usize::try_from(page).ok().filter(|p| p.is_power_of_two())?;
107            let size = size.checked_add(page - 1)? & !(page - 1);
108            let total = size.checked_add(2 * page)?;
109            #[cfg(target_os = "linux")]
110            let flags = libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE | libc::MAP_STACK;
111            #[cfg(not(target_os = "linux"))]
112            let flags = libc::MAP_PRIVATE | libc::MAP_ANON;
113            // SAFETY: a fresh anonymous mapping, inaccessible until the stack between the guards is opened below.
114            let mapping = unsafe { libc::mmap(std::ptr::null_mut(), total, libc::PROT_NONE, flags, -1, 0) };
115            if mapping == libc::MAP_FAILED {
116                return None;
117            }
118            let stack = Stack {
119                mapping: mapping.cast(),
120                total,
121                page,
122                size,
123            };
124            // SAFETY: the range lies inside the mapping just made.
125            let opened = unsafe {
126                libc::mprotect(
127                    stack.mapping.add(page).cast(),
128                    size,
129                    libc::PROT_READ | libc::PROT_WRITE,
130                )
131            };
132            (opened == 0).then_some(stack)
133        }
134
135        /// The lowest address of the usable stack.
136        pub(super) fn base(&self) -> *mut u8 {
137            // SAFETY: one page into the mapping.
138            unsafe { self.mapping.add(self.page) }
139        }
140
141        /// The usable stack's length.
142        pub(super) fn size(&self) -> usize {
143            self.size
144        }
145
146        /// The two guard pages, as `[start, end)` address ranges.
147        pub(super) fn guards(&self) -> [(usize, usize); 2] {
148            let start = self.mapping as usize;
149            [
150                (start, start + self.page),
151                (start + self.page + self.size, start + self.total),
152            ]
153        }
154    }
155
156    impl Drop for Stack {
157        fn drop(&mut self) {
158            // SAFETY: the mapping this value made, unmapped once.
159            unsafe { libc::munmap(self.mapping.cast(), self.total) };
160        }
161    }
162}
163
164/// The report a run past the end of the stack makes. A signal handler, run on an alternate stack, recognizes a fault
165/// in a guard page of the stack [`run_main`] mapped; any other fault goes to the handler that was there before.
166#[cfg(unix)]
167mod overflow {
168    use std::sync::atomic::{AtomicUsize, Ordering};
169    use std::sync::Once;
170
171    static GUARDS: [(AtomicUsize, AtomicUsize); 2] = [
172        (AtomicUsize::new(0), AtomicUsize::new(0)),
173        (AtomicUsize::new(0), AtomicUsize::new(0)),
174    ];
175    static mut PREVIOUS: [std::mem::MaybeUninit<libc::sigaction>; 2] =
176        [std::mem::MaybeUninit::uninit(), std::mem::MaybeUninit::uninit()];
177    const SIGNALS: [libc::c_int; 2] = [libc::SIGSEGV, libc::SIGBUS];
178    /// The alternate stack the handler runs on: room for writing out what the program had shown.
179    const SIGNAL_STACK_BYTES: usize = 64 * 1024;
180
181    /// Report a fault inside `guards` as a stack overflow, from now on.
182    pub(super) fn watch(guards: [(usize, usize); 2]) {
183        for (slot, (start, end)) in GUARDS.iter().zip(guards) {
184            slot.0.store(start, Ordering::SeqCst);
185            slot.1.store(end, Ordering::SeqCst);
186        }
187        ensure_signal_stack();
188        static INSTALL: Once = Once::new();
189        INSTALL.call_once(|| {
190            for (i, signal) in SIGNALS.into_iter().enumerate() {
191                // SAFETY: `PREVIOUS` is written here, once, before the handler that reads it is installed.
192                unsafe {
193                    let mut action: libc::sigaction = std::mem::zeroed();
194                    action.sa_sigaction = on_fault as *const () as usize;
195                    action.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK;
196                    libc::sigemptyset(&mut action.sa_mask);
197                    let previous = std::ptr::addr_of_mut!(PREVIOUS[i]).cast::<libc::sigaction>();
198                    libc::sigaction(signal, &action, previous);
199                }
200            }
201        });
202    }
203
204    /// An alternate signal stack on this thread of at least [`SIGNAL_STACK_BYTES`]: the handler cannot run on the
205    /// stack it reports full.
206    fn ensure_signal_stack() {
207        // SAFETY: reads this thread's alternate stack, then installs a fresh mapping in its place when it is missing
208        // or small; the mapping lives as long as the process.
209        unsafe {
210            let mut current: libc::stack_t = std::mem::zeroed();
211            if libc::sigaltstack(std::ptr::null(), &mut current) != 0 {
212                return;
213            }
214            if current.ss_flags & libc::SS_DISABLE == 0 && current.ss_size >= SIGNAL_STACK_BYTES {
215                return;
216            }
217            let Some(stack) = super::mapped::Stack::map(SIGNAL_STACK_BYTES) else {
218                return;
219            };
220            let installed = libc::stack_t {
221                ss_sp: stack.base().cast(),
222                ss_flags: 0,
223                ss_size: stack.size(),
224            };
225            if libc::sigaltstack(&installed, std::ptr::null_mut()) == 0 {
226                std::mem::forget(stack);
227            }
228        }
229    }
230
231    fn in_a_guard(address: usize) -> bool {
232        GUARDS.iter().any(|(start, end)| {
233            let (start, end) = (start.load(Ordering::Relaxed), end.load(Ordering::Relaxed));
234            start <= address && address < end
235        })
236    }
237
238    extern "C" fn on_fault(signal: libc::c_int, info: *mut libc::siginfo_t, context: *mut libc::c_void) {
239        // SAFETY: the kernel hands a valid `siginfo_t` to an `SA_SIGINFO` handler.
240        let address = unsafe { (*info).si_addr() } as usize;
241        if in_a_guard(address) {
242            crate::io::flush_stdout_if_free();
243            let report = logicaffeine_data::failure::CALL_DEPTH_EXCEEDED;
244            // SAFETY: `write` and `_exit` are async-signal-safe.
245            unsafe {
246                libc::write(2, report.as_ptr().cast(), report.len());
247                libc::write(2, b"\n".as_ptr().cast(), 1);
248                libc::_exit(1);
249            }
250        }
251        let i = SIGNALS.iter().position(|s| *s == signal).unwrap_or(0);
252        // SAFETY: `PREVIOUS[i]` was written before this handler was installed.
253        unsafe {
254            let previous = &*std::ptr::addr_of!(PREVIOUS[i]).cast::<libc::sigaction>();
255            if previous.sa_flags & libc::SA_SIGINFO != 0 && previous.sa_sigaction > libc::SIG_IGN {
256                let handler: extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void) =
257                    std::mem::transmute(previous.sa_sigaction);
258                handler(signal, info, context);
259            } else {
260                // Put the previous disposition back and return: the faulting instruction runs again under it.
261                libc::sigaction(signal, previous, std::ptr::null_mut());
262            }
263        }
264    }
265}
266
267#[cfg(all(test, unix))]
268#[path = "../../logicaffeine_tests/unit/logicaffeine_system/stack/tests.rs"]
269mod tests;