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}