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}