Skip to main content

praxis_runtime/
shadow_stack.rs

1//! The compiler-managed shadow stack (§12.3, ADR-019, ADR-101).
2//!
3//! §12.3 offers "compiler-managed shadow-stack frames **or** explicit root
4//! frames," and the runtime has both: explicit root frames
5//! ([`RootScope`](crate::RootScope), ADR-012) for the host, and this
6//! compiler-managed shadow stack that JIT-generated code spills into. At every
7//! GC safepoint (allocation / call that may allocate), the Cranelift backend
8//! stores the live `GcRef` locals into this function's slots *before* the
9//! safepoint and reloads them after.
10//!
11//! A **frame is not an object.** The runtime owns one contiguous region of
12//! slots for the whole program ([`SlotStack`]); a function's frame is the run
13//! of slots between the `top` it found on entry and the `top` it left behind.
14//! Generated code claims a run by bump-allocating inline — load `top`, zero
15//! `slot_count` slots, store `top + slot_count*8` back — and reclaims it by
16//! storing the saved base into `top` again. No allocation, no free, no call
17//! (ADR-101).
18//!
19//! Slots are raw `*mut GcHeader` (not [`GcRef`]) because a slot is *null until
20//! the backend writes a value into it*: a local may be live across a safepoint
21//! (so it must be in the root set) before it has ever been assigned at runtime.
22//! `GcRef` is `NonNull` by construction, so it cannot represent that state; the
23//! raw pointer can, and `push_roots` skips nulls.
24//!
25//! The collector reaches the whole stack through one [`RootSet`] impl on
26//! [`SlotStackHeader`], which scans `[base, top)` in a single linear pass. That
27//! is exactly every live frame's roots, because each frame occupies exactly its
28//! own slot run and the runs partition `[base, top)`.
29//!
30//! The header is `#[repr(C)]` and publishes [`SlotStackHeader::TOP_OFFSET`] so
31//! the backend emits a compile-time-derived displacement rather than a literal
32//! (Appendix B).
33
34use crate::gc::{GcHeader, GcRef};
35use crate::roots::RootSet;
36use crate::{
37    FRAME_BYTES_BASE, FRAME_BYTES_PER_SLOT, MAX_RECURSION_DEPTH, REFERENCE_FRAME_SLOTS,
38    STACK_BUDGET_BYTES,
39};
40
41/// The maximum `Gc` roots a single JIT'd function may spill. The backend
42/// rejects (at compile time) any function exceeding this, through
43/// [`SlotCount`]; real Praxis functions have small root sets.
44///
45/// **It is not what bounds the stack, and it is not a performance dial**
46/// (ADR-101). The budget guard bounds every claimed slot on its own — see
47/// [`SHADOW_STACK_SLOTS`] — and this constant contributes only that
48/// reservation's headroom term for Rust-side pushes. It appears in exactly two
49/// places that survive to run time: the size of the reservation, and
50/// [`SlotCount::new`], which is a compile-time check. **It appears in no
51/// generated code.** Raising it changes the cost of no program that compiles
52/// today; it changes only which programs compile.
53///
54/// A frame's root width is the number of *colors* its co-live root sets need,
55/// not its count of `Gc` locals (ADR-128), and over all 71 functions of
56/// `tests/aoc-corpus` the largest co-live root set is 11 — which is
57/// [`REFERENCE_FRAME_SLOTS`]. So this cap is in practice unreachable. What a
58/// programmer can still exhaust is [`MAX_DEBUG_VALUE_SLOTS`], which bounds the
59/// thing they can see: how many `Gc` locals one function may have.
60///
61/// This is part of the contract between the backend and the runtime; bumping it
62/// is an ABI-affecting change caught by the ABI version check (§11.6) only in
63/// that the two are rebuilt together.
64pub const MAX_SHADOW_SLOTS: usize = 192;
65
66/// The maximum `Gc` locals a single JIT'd function may have — the bound on the
67/// *dense* index space the crash debugger reads (ADR-128 decision 3).
68///
69/// This is a **different index space** from the shadow slots, so it has its own
70/// bound: root slots are as many as a function's co-live root sets need, debug
71/// value slots are one per `Gc` local, in MIR local order, so that the debugger
72/// can render a local the program has finished with.
73///
74/// It is sized for the thing it limits: how many `Gc` locals a function may
75/// have, which is a property of the source text a programmer can see and can
76/// act on. A bound of 192 would bite — `bfs`'s and `vm`'s entry points are
77/// already at 178 and 185 locals, and a 40-line function of twenty
78/// `var v = [1, 2, 3]` / `out(v.len())` pairs reaches it while its largest
79/// co-live root set is **2**.
80///
81/// The cost of the headroom is address space and nothing else, and it is
82/// **one** reservation's, not two: only
83/// [`DEBUG_VALUE_STACK_SLOTS`](crate::debug::DEBUG_VALUE_STACK_SLOTS) carries
84/// it, at `(4096 − 192) × 8` = **30.5 KiB** over [`SHADOW_STACK_SLOTS`].
85/// [`SlotStack::new`] allocates zeroed, which for the shadow stack's raw
86/// pointers is an `mmap` of untouched zero pages, so resident memory tracks how
87/// wide programs actually are.
88pub const MAX_DEBUG_VALUE_SLOTS: usize = 4096;
89
90/// A frame width the shadow stack can actually hold: a `u32` proven `<=`
91/// [`MAX_SHADOW_SLOTS`] at construction.
92///
93/// An over-wide frame is *unconstructible* rather than rejected at run time:
94/// [`SlotCount::new`] is the only way to make one, the backend turns the `None`
95/// into a compile diagnostic naming the function, and every consumer of a
96/// `SlotCount` may assume the bound without re-checking it. That assumption is
97/// load-bearing — it is one of the two premises of [`SHADOW_STACK_SLOTS`].
98#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub struct SlotCount(u32);
100
101impl SlotCount {
102    /// `Some` iff `n` slots fit in one frame. `const` so a caller can prove a
103    /// literal width at compile time.
104    #[must_use]
105    pub const fn new(n: u32) -> Option<SlotCount> {
106        if n as usize <= MAX_SHADOW_SLOTS {
107            Some(SlotCount(n))
108        } else {
109            None
110        }
111    }
112
113    /// The width, which is `<= MAX_SHADOW_SLOTS` by construction.
114    #[must_use]
115    pub const fn get(self) -> u32 {
116        self.0
117    }
118}
119
120/// A count of debug value slots the debug value stack can actually hold: a `u32`
121/// proven `<=` [`MAX_DEBUG_VALUE_SLOTS`] at construction (ADR-128 decision 3).
122///
123/// **A distinct type from [`SlotCount`], and that is the point.** The claim on
124/// the shadow stack and the claim on the debug value stack are different
125/// numbers with different bounds over different index spaces, and the failure
126/// mode of confusing them is silent: claiming the *colored* width on the debug
127/// stack would give the debugger a frame too short for its own metadata to
128/// index, and every local past the end would render another frame's value.
129/// Making them two types means that mix-up does not compile.
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub struct DebugSlotCount(u32);
132
133impl DebugSlotCount {
134    /// `Some` iff `n` `Gc` locals fit in one function's debug frame.
135    #[must_use]
136    pub const fn new(n: u32) -> Option<DebugSlotCount> {
137        if n as usize <= MAX_DEBUG_VALUE_SLOTS {
138            Some(DebugSlotCount(n))
139        } else {
140            None
141        }
142    }
143
144    /// The width, which is `<= MAX_DEBUG_VALUE_SLOTS` by construction.
145    #[must_use]
146    pub const fn get(self) -> u32 {
147        self.0
148    }
149}
150
151/// The size of the one shadow-stack reservation, in slots.
152///
153/// **Exhaustion is unrepresentable, not handled.** One fact bounds the stack,
154/// and since ADR-105 it bounds it *exactly* rather than through a product of two
155/// independent worst cases:
156///
157/// - every generated prologue rejects `stack_left < frame_cost(slots)` *before*
158///   it pushes anything. A context starts with at most [`STACK_BUDGET_BYTES`] —
159///   [`StackBudget`] refuses to make a larger one — and a frame spends at least
160///   [`FRAME_BYTES_BASE`], so there are at most [`MAX_RECURSION_DEPTH`] live
161///   frames; and it spends [`FRAME_BYTES_PER_SLOT`] on every slot past
162///   [`REFERENCE_FRAME_SLOTS`], so those slots number at most
163///   `STACK_BUDGET_BYTES / FRAME_BYTES_PER_SLOT`. Adding the two: live slots are
164///   bounded by `budget / FRAME_BYTES_PER_SLOT + MAX_RECURSION_DEPTH ×
165///   REFERENCE_FRAME_SLOTS`.
166///
167/// Reserving that — plus one frame of headroom for the Rust-side [`push_frame`]
168/// callers, which spend no budget and so are not covered by the argument —
169/// means there is no inline bounds check in the prologue, because there is
170/// nothing left to check. That is one branch removed from the hottest path in
171/// the language.
172///
173/// Sizing this as `MAX_RECURSION_DEPTH * MAX_SHADOW_SLOTS` would multiply "the
174/// deepest recursion" by "the widest frame" as if a program could have both at
175/// once. It cannot, and the guard is what says so: a maximum-width frame spends
176/// `FRAME_BYTES_BASE + 2 × (MAX_SHADOW_SLOTS − REFERENCE_FRAME_SLOTS)` bytes, so
177/// a stack of them runs out of budget at 2452 frames, not 8000. The reservation
178/// is therefore 5.56 MiB of *virtual address space* per
179/// [`Runtime`](crate::Runtime). [`SlotStack::new`] allocates it zeroed, which is
180/// an `mmap` of fresh zero pages: resident memory tracks how deep the program
181/// actually recurses, not how deep it is allowed to.
182pub const SHADOW_STACK_SLOTS: usize = (STACK_BUDGET_BYTES / FRAME_BYTES_PER_SLOT) as usize
183    + (MAX_RECURSION_DEPTH * REFERENCE_FRAME_SLOTS) as usize
184    + MAX_SHADOW_SLOTS;
185
186/// The bound the reservation above covers, spelled once so the `const` block and
187/// the test can both read it rather than restating the arithmetic.
188///
189/// `pub(crate)` because it bounds the *debug value* stack too, and for the
190/// same reason rather than by coincidence. [`crate::frame_cost`] charges the
191/// dense count of `Gc` locals (decision 4), which is exactly that stack's width,
192/// so the budget argument that bounds the claimed shadow slots bounds the claimed
193/// debug value slots by the identical arithmetic. The shadow stack's own claim is
194/// no wider — a colored width is at most the dense one — so this covers both.
195pub(crate) const MAX_LIVE_SLOTS: usize = (STACK_BUDGET_BYTES / FRAME_BYTES_PER_SLOT) as usize
196    + (MAX_RECURSION_DEPTH * REFERENCE_FRAME_SLOTS) as usize;
197
198// The capacity identity, restated so a build cannot disagree with it. It is
199// deliberately spelled without reference to how `SHADOW_STACK_SLOTS` is
200// computed: the hazard is not someone raising the budget (the reservation
201// follows it, and `StackBudget` refuses anyway), it is someone deciding the
202// address space is too much and writing a smaller number. That edit makes
203// shadow-stack overflow reachable from generated code — silently, because
204// generated code does not check the limit — and this fails the *build* instead.
205// Same discipline as ADR-040: a sizing error is a compile error, not a test run.
206const _: () = assert!(
207    SHADOW_STACK_SLOTS > MAX_LIVE_SLOTS,
208    "the shadow stack must cover every slot the budget can buy, plus one frame \
209     of headroom for Rust-side pushes"
210);
211
212// The two premises that argument rests on, stated where the reservation is
213// sized rather than left to the reader of `frame_cost`: a frame must spend
214// something (or there is no bound on how many are live), and a slot past the
215// reference width must spend something (or there is no bound on how many are
216// claimed).
217const _: () = assert!(
218    FRAME_BYTES_BASE > 0 && FRAME_BYTES_PER_SLOT > 0,
219    "a frame and a slot must each spend budget, or the reservation argument in \
220     SHADOW_STACK_SLOTS does not close"
221);
222
223/// The three-word header generated code bump-allocates against.
224///
225/// `#[repr(C)]` and pointer-shaped rather than index-shaped: the prologue's
226/// whole job is `top` → zero → `top + n*8`, and holding raw addresses makes
227/// that three instructions with no base-plus-scaled-index arithmetic.
228///
229/// The fields stay private and the backend reaches `top` through
230/// [`Self::TOP_OFFSET`]. That is strictly better than making the field `pub` so
231/// the backend can `offset_of!` it: the displacement is still derived from the
232/// `#[repr(C)]` layout at compile time, but no other crate can write the field.
233///
234/// Generic in the slot type because the mechanism is not specific to GC roots;
235/// see [`SlotStack`].
236#[repr(C)]
237pub struct SlotStackHeader<T: Copy> {
238    /// One past the last claimed slot — where the next frame starts.
239    top: *mut T,
240    /// The first slot of the reservation. Never moves.
241    base: *mut T,
242    /// One past the last slot of the reservation. Never moves. Read only by
243    /// [`push_frame`]; generated code does not check it (see
244    /// [`SHADOW_STACK_SLOTS`]).
245    limit: *mut T,
246}
247
248impl<T: Copy> SlotStackHeader<T> {
249    /// The byte offset of `top` within the header, for the backend to emit as a
250    /// load/store displacement. Computed from the `#[repr(C)]` layout so it
251    /// stays correct if the struct evolves.
252    pub const TOP_OFFSET: i32 = core::mem::offset_of!(Self, top) as i32;
253
254    /// Claim `n` slots, all set to `zero`, and answer the base of the run: the
255    /// Rust-side form of the bump a generated prologue emits inline.
256    ///
257    /// Crate-private, and deliberately so — the public doors are
258    /// [`push_frame`] and [`crate::debug::push_frame`], which hand back a guard
259    /// that restores `top` on drop. A caller holding a bare base could forget.
260    ///
261    /// # Safety
262    /// The [`SlotStack`] this header belongs to must be live.
263    ///
264    /// # Panics
265    /// If the run does not fit. This is the one place the reservation's limit is
266    /// checked at runtime: Rust callers do not pass the prologue's depth guard,
267    /// so the argument in [`SHADOW_STACK_SLOTS`] does not cover them.
268    pub(crate) unsafe fn claim(&mut self, n: usize, zero: T) -> *mut T {
269        let base = self.top;
270        // SAFETY: `top` is inside the reservation and `n` slots past it is at
271        // worst one-past-the-end once the assertion below passes.
272        let new_top = unsafe { base.add(n) };
273        assert!(
274            new_top <= self.limit,
275            "slot stack exhausted: {n} more slots do not fit"
276        );
277        // SAFETY: `[base, new_top)` is inside the live reservation.
278        unsafe { std::slice::from_raw_parts_mut(base, n) }.fill(zero);
279        self.top = new_top;
280        base
281    }
282
283    /// Restore `top` to `base`, releasing everything claimed since.
284    ///
285    /// An absolute, not a subtraction: it cannot underflow, and an imbalance
286    /// introduced below this frame is corrected here rather than propagated.
287    pub(crate) fn restore(&mut self, base: *mut T) {
288        self.top = base;
289    }
290
291    /// Every frame currently on the stack, concatenated — the collector's door
292    /// for the shadow instantiation, and the crash snapshot's for the debug
293    /// ones.
294    #[must_use]
295    pub fn claimed(&self) -> &[T] {
296        self.live_slots()
297    }
298
299    /// The slots between `base` and `top`: every frame currently on the stack,
300    /// concatenated.
301    fn live_slots(&self) -> &[T] {
302        debug_assert!(self.base <= self.top && self.top <= self.limit);
303        // SAFETY: `base` heads a `Box<[T]>` owned by the `SlotStack` that also
304        // owns this header, and `top` never leaves `[base, limit]` — generated
305        // code only ever stores back a value it loaded from `top`, or that
306        // value plus a `SlotCount`-bounded bump, and `SHADOW_STACK_SLOTS` is
307        // sized so the bump cannot pass `limit`.
308        unsafe {
309            let len = self.top.offset_from(self.base) as usize;
310            std::slice::from_raw_parts(self.base, len)
311        }
312    }
313
314    /// How many slots are currently claimed. Zero between runs, if every
315    /// prologue was balanced by an epilogue.
316    #[must_use]
317    pub fn len(&self) -> usize {
318        self.live_slots().len()
319    }
320
321    /// True iff no frame is on the stack.
322    #[must_use]
323    pub fn is_empty(&self) -> bool {
324        self.len() == 0
325    }
326}
327
328/// The owner of one slot reservation and its header.
329///
330/// Two allocations, both made once and never resized. **Not a `Vec`**: a `Vec`
331/// that reallocated would invalidate the base pointer generated code holds in a
332/// Cranelift `Variable` for the duration of a call — a use-after-free reachable
333/// from any Praxis program deep enough to trigger the growth. A `Box<[T]>`
334/// allocated at its final size cannot move.
335///
336/// The header is separately boxed so [`Self::header_ptr`] survives a `Runtime`
337/// move: `Runtime::new` returns by value, and generated code holds the address
338/// this hands out for the whole program.
339///
340/// Generic in `T` because the mechanism is not specific to GC roots. The same
341/// shape serves any per-frame array of `Copy` slots whose zero value means
342/// "nothing here yet" — the crash debugger's per-frame locals
343/// (`SlotStack<Option<GcRef>>`, whose zero *is* `None` by the `NonNull` niche)
344/// being the instantiation this shape was built with in view. The root stack
345/// and the debug value stack index *different* spaces: root slots are colored
346/// by live range, debug value slots are dense, one per `Gc` local (ADR-128).
347pub struct SlotStack<T: Copy> {
348    header: Box<SlotStackHeader<T>>,
349    slots: Box<[T]>,
350}
351
352impl<T: Copy> SlotStack<T> {
353    /// Reserve `capacity` slots, all set to `zero`, and point a header at them.
354    ///
355    /// `zero` is a parameter rather than a `Default` bound so the caller names
356    /// the all-zero value — and because that is what lets this lower to a
357    /// single `alloc_zeroed`. `vec![zero; n]` hits std's `IsZero`
358    /// specialization for raw pointers, so the 5.56 MiB shadow reservation is
359    /// an `mmap` of untouched zero pages rather than a memset at every
360    /// `Runtime::new()`. An instantiation whose `zero` std does not recognise
361    /// as all-zero-bytes still works; it pays that memset.
362    #[must_use]
363    pub fn new(capacity: usize, zero: T) -> Self {
364        // Build the storage first: `base`/`limit` must be addresses inside the
365        // final allocation, so nothing may move after they are taken.
366        let mut slots: Box<[T]> = vec![zero; capacity].into_boxed_slice();
367        let base = slots.as_mut_ptr();
368        // SAFETY: `capacity` elements were just allocated at `base`, so
369        // one-past-the-end is a valid pointer to form.
370        let limit = unsafe { base.add(capacity) };
371        SlotStack {
372            header: Box::new(SlotStackHeader {
373                top: base,
374                base,
375                limit,
376            }),
377            slots,
378        }
379    }
380
381    /// The address generated code bump-allocates against. Stable for the life
382    /// of this `SlotStack`, including across moves of whatever owns it.
383    pub fn header_ptr(&mut self) -> *mut SlotStackHeader<T> {
384        &mut *self.header
385    }
386
387    /// Borrow the header — the collector's door, and the tests'.
388    #[must_use]
389    pub fn header(&self) -> &SlotStackHeader<T> {
390        &self.header
391    }
392
393    /// Drop every frame. Only correct between runs: a generated epilogue that
394    /// later restored its saved base would undo this.
395    pub fn reset(&mut self) {
396        self.header.top = self.header.base;
397    }
398
399    /// How many slots are currently claimed.
400    #[must_use]
401    pub fn len(&self) -> usize {
402        self.header.len()
403    }
404
405    /// True iff no frame is on the stack.
406    #[must_use]
407    pub fn is_empty(&self) -> bool {
408        self.header.is_empty()
409    }
410
411    /// The reservation's capacity in slots.
412    #[must_use]
413    pub fn capacity(&self) -> usize {
414        self.slots.len()
415    }
416}
417
418/// The whole shadow stack, as the runtime owns it.
419pub type ShadowStack = SlotStack<*mut GcHeader>;
420/// The header generated code bump-allocates against.
421pub type ShadowStackHeader = SlotStackHeader<*mut GcHeader>;
422
423// Nothing *depends* on `top` being first — the backend emits `TOP_OFFSET`,
424// whatever it is. But a header whose hottest field is not at displacement zero
425// is a layout mistake worth noticing at build time rather than in a profile.
426const _: () = assert!(ShadowStackHeader::TOP_OFFSET == 0);
427
428impl RootSet for ShadowStackHeader {
429    /// One linear pass over every claimed slot.
430    ///
431    /// This yields *exactly* every live frame's roots: each frame occupies
432    /// exactly its own `slot_count` slots and the frames partition
433    /// `[base, top)`, so the concatenation is the union of every live frame's
434    /// `slots[..slot_count]`. Walking a parent-pointer chain instead would cost
435    /// a level of native recursion and an allocation per frame *inside*
436    /// `Heap::mark`, 8000 of each at the deepest legal recursion.
437    ///
438    /// Slots *above* `top` may still hold pointers a popped frame wrote. That is
439    /// harmless, and needs no invariant to make it so: they are never scanned,
440    /// and the next push zeroes exactly the run it claims before any safepoint
441    /// can read it.
442    fn push_roots(&self, out: &mut Vec<GcRef>) {
443        out.extend(self.live_slots().iter().copied().filter_map(|p| {
444            // SAFETY: a non-null slot was written by generated code (or by
445            // `ShadowFrameGuard::set`) with a live allocation's header pointer.
446            std::ptr::NonNull::new(p).map(|nn| unsafe { GcRef::from_non_null(nn) })
447        }));
448    }
449}
450
451// ---------------------------------------------------------------------------
452// The Rust-side push. Generated code does this inline; this is for the
453// runtime's own tests and for any host that wants to root as a prologue does.
454// ---------------------------------------------------------------------------
455
456/// A frame claimed from Rust, released when dropped.
457///
458/// The RAII shape is what makes "write past your frame" unrepresentable from
459/// Rust: [`Self::set`] and [`Self::clear`] are bounds-checked against the width
460/// the frame was pushed with, and the only way to restore `top` is to drop the
461/// guard — so a frame cannot outlive its slots or be popped twice.
462pub struct ShadowFrameGuard {
463    header: *mut ShadowStackHeader,
464    /// This frame's first slot, and the `top` the drop restores.
465    base: *mut *mut GcHeader,
466    count: u32,
467}
468
469impl ShadowFrameGuard {
470    /// Root `r` in slot `index`.
471    ///
472    /// # Panics
473    /// If `index` is outside the frame. Writing another frame's slot would make
474    /// the collector's view of *that* frame wrong, which is not a condition the
475    /// caller could detect afterwards.
476    pub fn set(&mut self, index: usize, r: GcRef) {
477        assert!(
478            index < self.count as usize,
479            "shadow slot {index} is outside a {}-slot frame",
480            self.count
481        );
482        // SAFETY: `index` is inside the run claimed by `push_frame`, which is
483        // live until this guard drops.
484        unsafe { *self.base.add(index) = r.as_ptr() };
485    }
486
487    /// Un-root slot `index`. A dead slot must not keep its object reachable,
488    /// and since swept storage is reusable a stale slot could name a live
489    /// object of an entirely different type.
490    ///
491    /// # Panics
492    /// If `index` is outside the frame.
493    pub fn clear(&mut self, index: usize) {
494        assert!(
495            index < self.count as usize,
496            "shadow slot {index} is outside a {}-slot frame",
497            self.count
498        );
499        // SAFETY: as `set`.
500        unsafe { *self.base.add(index) = std::ptr::null_mut() };
501    }
502
503    /// This frame's first slot, for a caller that wants to observe the slot
504    /// memory the way generated code addresses it.
505    #[must_use]
506    pub fn base_ptr(&self) -> *mut *mut GcHeader {
507        self.base
508    }
509}
510
511impl Drop for ShadowFrameGuard {
512    fn drop(&mut self) {
513        // Restore the absolute base rather than subtracting `count`, for the
514        // same reason the generated epilogue does: an imbalance introduced by
515        // anything that ran inside this frame cannot leak past it, and there is
516        // no subtraction to underflow.
517        // SAFETY: `header` was non-null when the guard was made, and belongs to
518        // a runtime the caller guaranteed outlives it.
519        unsafe { (*self.header).restore(self.base) };
520    }
521}
522
523/// Claim `count` zeroed slots on `ctx`'s shadow stack, the way a generated
524/// prologue does.
525///
526/// This is the one place the reservation's limit is checked at runtime: Rust
527/// callers do not go through the prologue's depth guard, so the argument in
528/// [`SHADOW_STACK_SLOTS`] does not cover them.
529///
530/// # Safety
531/// `ctx` must point at a live context wired by
532/// [`Runtime::context`](crate::Runtime::context), and the runtime that owns the
533/// stack must outlive the returned guard.
534///
535/// # Panics
536/// If `ctx` is null, its `shadow` header is null, or the frame would not fit.
537#[must_use]
538pub unsafe fn push_frame(ctx: *mut crate::RuntimeContext, count: SlotCount) -> ShadowFrameGuard {
539    assert!(!ctx.is_null(), "push_frame needs a wired context");
540    // SAFETY: the caller guarantees `ctx` is live.
541    let header = unsafe { (*ctx).shadow };
542    assert!(
543        !header.is_null(),
544        "push_frame needs a context from `Runtime::context`, not a placeholder"
545    );
546    let n = count.get() as usize;
547    // SAFETY: `header` is non-null and owned by a live `SlotStack`, so `claim`
548    // may bump it; it checks the reservation's limit itself.
549    let base = unsafe { (*header).claim(n, std::ptr::null_mut()) };
550    ShadowFrameGuard {
551        header,
552        base,
553        count: count.get(),
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use std::ptr::NonNull;
561
562    fn dummy_ref() -> GcRef {
563        // A leaked header; only its non-null address matters for root-walking
564        // tests. The collector is never run against these.
565        let header = Box::leak(Box::new(GcHeader::detached()));
566        // SAFETY: `header` is leaked, aligned, and live for the process.
567        unsafe { GcRef::from_non_null(NonNull::from(header)) }
568    }
569
570    /// A stack plus a context wired to it, the way `Runtime` wires one.
571    struct Fixture {
572        stack: ShadowStack,
573        ctx: Box<crate::RuntimeContext>,
574    }
575
576    impl Fixture {
577        fn new() -> Fixture {
578            let mut stack = ShadowStack::new(SHADOW_STACK_SLOTS, std::ptr::null_mut());
579            // SAFETY: no generated code runs against this context and nothing
580            // dereferences `input_source`; only `shadow` is read.
581            let mut ctx = Box::new(unsafe { crate::RuntimeContext::placeholder(dummy_ref()) });
582            ctx.shadow = stack.header_ptr();
583            Fixture { stack, ctx }
584        }
585
586        fn ctx_ptr(&mut self) -> *mut crate::RuntimeContext {
587            &mut *self.ctx
588        }
589
590        fn roots(&self) -> Vec<GcRef> {
591            let mut out = Vec::new();
592            self.stack.header().push_roots(&mut out);
593            out
594        }
595    }
596
597    #[test]
598    fn an_empty_stack_roots_nothing() {
599        let f = Fixture::new();
600        assert!(f.roots().is_empty());
601        assert!(f.stack.is_empty());
602    }
603
604    #[test]
605    fn a_frame_yields_written_slots_only() {
606        // The test that pins "null means not-yet-written" — the reason a slot
607        // is a raw `*mut GcHeader` and not a `GcRef`.
608        let mut f = Fixture::new();
609        let a = dummy_ref();
610        let b = dummy_ref();
611        let ctx = f.ctx_ptr();
612        // SAFETY: `ctx` is wired to `f.stack`, which outlives the guard.
613        let mut guard = unsafe { push_frame(ctx, SlotCount::new(3).unwrap()) };
614        guard.set(0, a);
615        guard.set(2, b); // slot 1 left null
616        let out = f.roots();
617        assert_eq!(out.len(), 2);
618        assert_eq!(out[0].as_ptr(), a.as_ptr());
619        assert_eq!(out[1].as_ptr(), b.as_ptr());
620        drop(guard);
621    }
622
623    #[test]
624    fn nested_frames_are_one_contiguous_scan() {
625        // The collector walks no chain: it reads `[base, top)` once and gets
626        // every live frame's roots.
627        let mut f = Fixture::new();
628        let a = dummy_ref();
629        let b = dummy_ref();
630        let ctx = f.ctx_ptr();
631        // SAFETY: `ctx` is wired to `f.stack`, which outlives both guards.
632        let (outer, inner) = unsafe {
633            let mut outer = push_frame(ctx, SlotCount::new(1).unwrap());
634            outer.set(0, a);
635            let mut inner = push_frame(ctx, SlotCount::new(1).unwrap());
636            inner.set(0, b);
637            (outer, inner)
638        };
639        let out = f.roots();
640        assert_eq!(out.len(), 2);
641        assert!(out.iter().any(|r| r.as_ptr() == a.as_ptr()));
642        assert!(out.iter().any(|r| r.as_ptr() == b.as_ptr()));
643        drop(inner);
644        drop(outer);
645    }
646
647    #[test]
648    fn a_popped_frames_slots_are_not_scanned() {
649        // Slots above `top` still hold what the popped frame wrote. Nothing
650        // clears them, and nothing needs to: they are outside `[base, top)`,
651        // and the next push zeroes what it claims.
652        let mut f = Fixture::new();
653        let a = dummy_ref();
654        let ctx = f.ctx_ptr();
655        // SAFETY: `ctx` is wired to `f.stack`, which outlives the guard.
656        let base = unsafe {
657            let mut guard = push_frame(ctx, SlotCount::new(1).unwrap());
658            guard.set(0, a);
659            let base = guard.base_ptr();
660            drop(guard);
661            base
662        };
663        // SAFETY: `base` is inside the live reservation; that reading a stale
664        // slot is harmless is exactly what this test asserts.
665        assert_eq!(unsafe { *base }, a.as_ptr(), "the slot memory is untouched");
666        assert!(f.roots().is_empty(), "but it is outside [base, top)");
667    }
668
669    #[test]
670    fn pushing_and_popping_restores_the_top() {
671        let mut f = Fixture::new();
672        let ctx = f.ctx_ptr();
673        // SAFETY: `ctx` is wired to `f.stack`, which outlives the guards.
674        unsafe {
675            let outer = push_frame(ctx, SlotCount::new(4).unwrap());
676            assert_eq!(f.stack.len(), 4);
677            {
678                let inner = push_frame(ctx, SlotCount::new(7).unwrap());
679                assert_eq!(f.stack.len(), 11);
680                drop(inner);
681            }
682            assert_eq!(f.stack.len(), 4, "an inner pop restores the outer extent");
683            drop(outer);
684        }
685        assert!(f.stack.is_empty(), "every push is balanced by a pop");
686    }
687
688    #[test]
689    fn a_zero_slot_frame_moves_nothing() {
690        // The counterexample that keeps the prologue guard alive: a function
691        // with no `Gc` locals consumes no slots, so it can recurse without limit
692        // while `top` never moves. A full shadow stack is therefore *not* a
693        // cleaner encoding of stack overflow — the guard's budget bounds the
694        // native stack, which the shadow stack knows nothing about. It is also
695        // why `frame_cost` has a base at all: a frame charged only for its slots
696        // would charge this one nothing.
697        let mut f = Fixture::new();
698        let ctx = f.ctx_ptr();
699        // SAFETY: `ctx` is wired to `f.stack`, which outlives the guards.
700        unsafe {
701            let guards: Vec<ShadowFrameGuard> = (0..1000)
702                .map(|_| push_frame(ctx, SlotCount::new(0).unwrap()))
703                .collect();
704            assert!(f.stack.is_empty(), "1000 frames claimed nothing");
705            drop(guards);
706        }
707        assert!(f.stack.is_empty());
708    }
709
710    #[test]
711    fn rejects_an_oversized_frame() {
712        // An over-wide frame is unconstructible, not rejected at run time:
713        // there is no panic to provoke, only a `None`.
714        assert!(SlotCount::new(MAX_SHADOW_SLOTS as u32).is_some());
715        assert!(SlotCount::new((MAX_SHADOW_SLOTS + 1) as u32).is_none());
716    }
717
718    #[test]
719    fn the_reservation_covers_every_slot_the_budget_can_buy() {
720        // The readable form of the `const` block above, and the arithmetic that
721        // makes it exact rather than a product of two worst cases. Whatever mix
722        // of widths a program recurses through, every slot it claims was paid
723        // for out of one budget, so the slots alone can never outrun it.
724        for width in [0u32, 1, 7, 64, MAX_SHADOW_SLOTS as u32] {
725            let per_frame = crate::frame_cost(width);
726            let frames = STACK_BUDGET_BYTES / per_frame;
727            let slots = frames as usize * width as usize;
728            assert!(
729                slots <= MAX_LIVE_SLOTS,
730                "a stack of {frames} frames {width} slots wide claims {slots} \
731                 slots, past the {MAX_LIVE_SLOTS}-slot bound"
732            );
733        }
734        let stack = ShadowStack::new(SHADOW_STACK_SLOTS, std::ptr::null_mut());
735        assert_eq!(stack.capacity(), SHADOW_STACK_SLOTS);
736    }
737
738    #[test]
739    fn a_wide_frame_spends_more_budget_than_a_narrow_one() {
740        // The whole content of ADR-105, as arithmetic: the guard's addend
741        // varies with the frame, so the deepest recursion a program reaches
742        // falls as its frames widen. A plain call count cannot express that,
743        // and the factor it misses is the factor by which the measured native
744        // frames actually differ.
745        let reference = STACK_BUDGET_BYTES / crate::frame_cost(REFERENCE_FRAME_SLOTS);
746        let widest = STACK_BUDGET_BYTES / crate::frame_cost(MAX_SHADOW_SLOTS as u32);
747        assert_eq!(
748            STACK_BUDGET_BYTES / crate::frame_cost(0),
749            MAX_RECURSION_DEPTH,
750            "the cheapest frame there is must not buy more calls than the debug \
751             frame stack has entries — that stack is sized MAX_RECURSION_DEPTH + 1"
752        );
753        assert_eq!(
754            reference, MAX_RECURSION_DEPTH,
755            "a reference-width frame reaches exactly the depth the old call \
756             count allowed, so an ordinary recursive program is unaffected"
757        );
758        assert!(
759            widest * 3 < reference,
760            "the widest legal frame must be several times dearer than the \
761             reference one: {widest} vs {reference}"
762        );
763    }
764
765    #[test]
766    fn a_budget_larger_than_the_reservation_cannot_be_built() {
767        // The seal. `SHADOW_STACK_SLOTS` is sized from `STACK_BUDGET_BYTES`, so
768        // a host that could install a bigger budget would make shadow-stack
769        // overflow reachable from generated code, which does not check.
770        assert!(crate::StackBudget::new(STACK_BUDGET_BYTES).is_some());
771        assert!(crate::StackBudget::new(STACK_BUDGET_BYTES + 1).is_none());
772        assert_eq!(crate::StackBudget::DEFAULT.get(), STACK_BUDGET_BYTES);
773    }
774}