Skip to main content

rivet/preempt/
stack_pool.rs

1//! Preemptive task stack pool (plan.md §3).
2//!
3//! All preemptive task stacks live in one contiguous, power-of-two-aligned
4//! `.task_stacks` section. Stacks are carved from it at spawn time with
5//! size-aligned addresses (sizes must be powers of two), which is exactly
6//! what the CM3 MPU (per-switch current-stack region) and the RISC-V PMP
7//! (NAPOT guard bands at the low end of each stack) require.
8//!
9//! `__task_stacks_start`/`__task_stacks_end` are part of the linker
10//! contract every board's linker script provides (documented in
11//! `docs/porting.md`) — not an arch/board API call, just fixed symbol
12//! names, the same on every real target. The host test backend has no
13//! linker-provided pool at all (gated on the `host-port` feature, not
14//! `target_arch`: the distinction is "is there a real linker script",
15//! which is unrelated to which arch a real target happens to be); there,
16//! [`alloc_stack`] always returns `None` and callers fall back to their
17//! own static stacks.
18
19#[cfg(not(feature = "host-port"))]
20use crate::sync::atomic::{AtomicUsize, Ordering};
21
22#[cfg(not(feature = "host-port"))]
23extern "C" {
24    static __task_stacks_start: u8;
25    static __task_stacks_end: u8;
26}
27
28/// Next free offset into the pool. Single writer (spawn happens on one
29/// context at a time: boot or a running task); readers are the fault
30/// handler and watermarking, which only need the base.
31#[cfg(not(feature = "host-port"))]
32static NEXT: AtomicUsize = AtomicUsize::new(0);
33
34/// Number of stacks allocated so far (used as the guard-registration
35/// index — meaningful on arches with a limited number of hardware guard
36/// slots, e.g. RISC-V PMP; `port::arch::guard_register` is a no-op on
37/// arches without that limit, e.g. Cortex-M's two-region MPU design).
38#[cfg(not(feature = "host-port"))]
39static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
40
41/// Guard band size at the low end of every stack (plan.md §3.2): an
42/// aligned region the RISC-V PMP denies, so a stack overflow faults
43/// instead of corrupting silently. The CM3 MPU denies the whole pool so
44/// the guard is redundant there (but harmless). `64` bytes on every arch
45/// this has ever been measured on, except the ESP32-C6 (plan.md Phase
46/// 26): its PMP grain forces a larger minimum NAPOT region, so this is a
47/// runtime query — [`crate::port::arch::min_guard_size`] — not a
48/// hardcoded constant, to guarantee the reservation here and what
49/// [`crate::port::arch::guard_register`] actually denies always agree.
50#[cfg(not(feature = "host-port"))]
51fn guard_size() -> usize {
52    crate::port::arch::min_guard_size()
53}
54
55/// Pool base address on embedded targets.
56#[cfg(not(feature = "host-port"))]
57fn pool_base() -> usize {
58    // (Linker symbol; addr_of! is not unsafe on an extern static.)
59    core::ptr::addr_of!(__task_stacks_start) as usize
60}
61
62#[cfg(not(feature = "host-port"))]
63fn pool_len() -> usize {
64    core::ptr::addr_of!(__task_stacks_end) as usize
65        - core::ptr::addr_of!(__task_stacks_start) as usize
66}
67
68/// Allocate a size-aligned stack of `size` bytes from the pool, with a
69/// guard band (usually 64 bytes — see [`guard_size`]) at its low end
70/// (plan.md §3.2). `size` must be a power of two (the MPU/PMP region
71/// alignment requires it) and at least as large as the guard band.
72///
73/// Each stack's guard band is registered via
74/// [`crate::port::arch::guard_register`] (a no-op on arches whose memory
75/// guard doesn't need one, e.g. Cortex-M). Returns `None` when the pool
76/// is exhausted or on the host test backend (no linker-provided pool).
77pub fn alloc_stack(size: usize) -> Option<&'static mut [u8]> {
78    debug_assert!(
79        size.is_power_of_two(),
80        "rivet: task stack size {size} must be a power of two (MPU/PMP region alignment)"
81    );
82    #[cfg(not(feature = "host-port"))]
83    {
84        let guard_size = guard_size();
85        debug_assert!(
86            size >= guard_size,
87            "rivet: task stack size {size} is smaller than this hardware's minimum PMP/MPU \
88             guard band ({guard_size} bytes) — the guard alignment math below assumes a stack \
89             is always at least as large as its own guard band"
90        );
91        let base = pool_base();
92        let len = pool_len();
93        // Prefer a released stack (LIFO) — the guard band is still
94        // registered for it, so reuse is free.
95        if let Some((off, sz)) = FREE_LIST.pop() {
96            if sz == size {
97                // `off` is the *slice's* offset from the pool base (already
98                // past the guard band); adding GUARD_SIZE again would
99                // double-count it and shift the recycled stack into the
100                // next region (plan.md §5.4 — caught by the CM3 MPU as a
101                // switch-time MemManage with base off by 64 bytes).
102                let stack_base = base + off;
103                // SAFETY: this slice was released by `release_stack` (still
104                // within the 'static pool) and is not handed out elsewhere.
105                return Some(unsafe {
106                    core::slice::from_raw_parts_mut(stack_base as *mut u8, size)
107                });
108            }
109            // Size mismatch: put it back; the caller requested a different
110            // size than the last released stack.
111            FREE_LIST.push(off, sz);
112        }
113        let next = NEXT.load(Ordering::Relaxed);
114        // Stack base size-aligned; guard = `guard_size` bytes immediately
115        // below (`size >= guard_size`, asserted above, guarantees a
116        // `size`-aligned address is also `guard_size`-aligned, since both
117        // are powers of two — required for the NAPOT guard encoding).
118        let stack_base = (base + next + guard_size + size - 1) & !(size - 1);
119        let guard_base = stack_base - guard_size;
120        let offset = guard_base - base;
121        if offset + guard_size + size > len {
122            return None;
123        }
124        NEXT.store(offset + guard_size + size, Ordering::Relaxed);
125        let entry = ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
126        crate::port::arch::guard_register(guard_base, entry);
127        // SAFETY: the slice is within the 'static pool, never handed out
128        // twice (NEXT only moves forward), and aligned to `size`.
129        Some(unsafe { core::slice::from_raw_parts_mut(stack_base as *mut u8, size) })
130    }
131    #[cfg(feature = "host-port")]
132    {
133        let _ = size;
134        None
135    }
136}
137
138/// Max free-list entries (a released stack per TCB slot).
139#[cfg(not(feature = "host-port"))]
140const FREE_LIST_CAP: usize = 16;
141
142/// LIFO free list of released stacks (plan.md §5.4 respawn): pairs of
143/// (offset, size) into the pool. Released stacks are refilled with 0xAA so
144/// watermark detection stays meaningful across respawns. The list is only
145/// touched by `alloc_stack`/`release_stack`, which run on one context at a
146/// time (boot or a running task with interrupts logically disabled around
147/// spawn/despawn).
148#[cfg(not(feature = "host-port"))]
149struct FreeList {
150    count: AtomicUsize,
151    entries: [AtomicUsize; FREE_LIST_CAP * 2],
152}
153
154#[cfg(not(feature = "host-port"))]
155impl FreeList {
156    const fn new() -> Self {
157        Self {
158            count: AtomicUsize::new(0),
159            entries: [const { AtomicUsize::new(0) }; FREE_LIST_CAP * 2],
160        }
161    }
162    fn push(&self, offset: usize, size: usize) {
163        let n = self.count.load(Ordering::Relaxed);
164        if n < FREE_LIST_CAP {
165            self.entries[n * 2].store(offset, Ordering::Relaxed);
166            self.entries[n * 2 + 1].store(size, Ordering::Relaxed);
167            self.count.store(n + 1, Ordering::Relaxed);
168        }
169    }
170    fn pop(&self) -> Option<(usize, usize)> {
171        let n = self.count.load(Ordering::Relaxed);
172        if n == 0 {
173            return None;
174        }
175        let i = n - 1;
176        let off = self.entries[i * 2].load(Ordering::Relaxed);
177        let sz = self.entries[i * 2 + 1].load(Ordering::Relaxed);
178        self.count.store(i, Ordering::Relaxed);
179        Some((off, sz))
180    }
181}
182
183#[cfg(not(feature = "host-port"))]
184static FREE_LIST: FreeList = FreeList::new();
185
186/// Release a stack back to the pool (plan.md §5.4 despawn/respawn). The
187/// slice is refilled with `0xAA` so watermarking keeps working after the
188/// stack is reused. No-op if the slice did not come from the pool.
189pub fn release_stack(stack: &'static mut [u8]) {
190    #[cfg(not(feature = "host-port"))]
191    {
192        let base = pool_base();
193        let offset = stack.as_mut_ptr() as usize - base;
194        let size = stack.len();
195        if offset + size > pool_len() {
196            return; // not from the pool; ignore
197        }
198        // Refill for watermark detection (0xAA = untouched). The pool is
199        // MPU/PMP-denied to thread-mode code, so the refill runs inside
200        // the same scratch window (and critical section) the spawn path
201        // uses; the svc-based frame init is not involved here (plan.md §5.4).
202        crate::critical::enter(|| {
203            crate::port::arch::scratch_open(stack.as_ptr() as usize, size);
204            for b in stack.iter_mut() {
205                *b = 0xAA;
206            }
207            crate::port::arch::scratch_close();
208        });
209        FREE_LIST.push(offset, size);
210    }
211    #[cfg(feature = "host-port")]
212    {
213        let _ = stack;
214    }
215}
216
217/// Pool bounds `(base, len)` on embedded targets; `(0, 0)` on the host
218/// test backend.
219pub fn pool_bounds() -> (usize, usize) {
220    #[cfg(not(feature = "host-port"))]
221    {
222        (pool_base(), pool_len())
223    }
224    #[cfg(feature = "host-port")]
225    {
226        (0, 0)
227    }
228}
229
230/// Is address `addr` inside the task-stack pool?
231pub fn contains(addr: usize) -> bool {
232    #[cfg(not(feature = "host-port"))]
233    {
234        addr >= pool_base() && addr < pool_base() + pool_len()
235    }
236    #[cfg(feature = "host-port")]
237    {
238        let _ = addr;
239        false
240    }
241}
242
243/// Test-only: reset the allocation cursor (host tests).
244#[cfg(feature = "test-support")]
245pub(crate) fn reset_for_test() {
246    #[cfg(not(feature = "host-port"))]
247    {
248        NEXT.store(0, Ordering::Relaxed);
249        ALLOC_COUNT.store(0, Ordering::Relaxed);
250    }
251}