Skip to main content

setback/
lib.rs

1#![no_std]
2#![allow(unsafe_op_in_unsafe_fn)]
3
4/*!
5# `setback`: setjmp/longjmp failure recovery, confined to C
6
7[`protect`] runs a closure and returns `Ok(value)` on normal completion, or
8`Err(RecoveryError)` if a `longjmp` - triggered by a stack-overflow fault
9handler, an out-of-memory handler, or explicit user code via [`recover`] -
10abandons the closure's stack. Everything on the abandoned stack is leaked: no
11`Drop` runs. See [`protect`] for the full safety contract.
12
13## How it works
14
15All `setjmp`/`longjmp` lives in a tiny C file (`setback.c`): rustc does not support
16`setjmp`/`longjmp`, so calling `setjmp` from Rust risks miscompilation. Rust hands
17C a data pointer and an `extern "C"` trampoline, C arms the mark and calls the
18trampoline, which runs the closure. A `longjmp` resets the stack pointer to
19that `setjmp`, jumping over every live Rust frame above it - the trampoline, the
20closure, and its whole call tree - and abandons them where they sit. The jump
21stops at the C frame, and [`protect`] returns `Err(RecoveryError)`.
22
23An uncaught panic crossing the `extern "C"` trampoline aborts (Rust 1.81+)
24rather than entering C.
25
26## One global registry, keyed by thread id
27
28The crate owns a single `static` intrusive doubly-linked list of active marks.
29Each [`protect`] call links one node, tagged with the caller's [`ThreadId`], and
30unlinks it on exit. One shared fault handler, given the *faulting* thread's id,
31calls [`recover`] to find that thread's innermost active mark and jump into it,
32or [`can_recover`] to ask whether such a mark exists without jumping.
33The link/unlink runs inside a [`critical_section`], the protected closure runs
34outside it. You supply the [`critical-section`] impl in the final binary.
35
36[`critical-section`]: https://docs.rs/critical-section/latest/critical_section/
37
38*/
39
40#[cfg(target_family = "wasm")]
41compile_error!("`setback` does not support wasm targets");
42
43use core::cell::UnsafeCell;
44use core::convert::Infallible;
45use core::error::Error;
46use core::ffi::c_void;
47use core::mem::{ManuallyDrop, MaybeUninit};
48use core::panic::UnwindSafe;
49use core::ptr;
50use core::sync::atomic::{AtomicPtr, AtomicU8, Ordering};
51
52/// Identifier the caller uses to tag a `protect` scope and that the fault
53/// handler uses to find it again. Cast your RTOS task handle / index to `usize`.
54pub type ThreadId = usize;
55
56/// Wrap a capture (or a whole closure) to assert it is unwind-safe if needed,
57/// satisfying the [`UnwindSafe`] bound on [`protect`]. Safe in itself, you
58/// should still fulfill the safety contract of [`protect`] when the closure runs.
59pub use core::panic::AssertUnwindSafe;
60
61/// Returned by [`protect`] when the closure's stack was abandoned by a `longjmp`.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct RecoveryError {
64    /// The code the caller of [`recover`] chose for this abandonment.
65    /// `setback` assigns it no meaning. You decide what each value stands for.
66    pub cause: i32,
67}
68
69/// Returned by [`recover`] when the given `tid` has no active [`protect`] scope.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct RecoveryFailure;
72
73unsafe extern "C" {
74    fn setback_jmpbuf_size() -> usize;
75    fn setback_jmpbuf_align() -> usize;
76    fn setback_call(
77        jb: *mut c_void,
78        armed: *mut u8,
79        tramp: unsafe extern "C" fn(*mut c_void),
80        data: *mut c_void,
81    ) -> i32;
82    fn setback_longjmp(jb: *mut c_void) -> !;
83}
84
85const SETBACK_OK: i32 = 0;
86
87/// Bytes of stack that [`protect`] reserves below the recovery mark before it
88/// runs the closure - the gap a fault handler may rely on when choosing where
89/// to run [`recover`]. See the "Recovery-stack guarantee" on [`protect`].
90//
91// Must stay equal to `SETBACK_RECOVERY_GAP_BYTES` in `setback.c`.
92pub const RECOVERY_GAP_BYTES: usize = 64;
93
94/// Backing storage for one C `jmp_buf`. 512 bytes / 16-byte alignment covers
95/// every mainstream target. The constructor asserts it.
96#[repr(C, align(16))]
97struct JmpBufStorage {
98    bytes: UnsafeCell<MaybeUninit<[u8; 512]>>,
99}
100
101struct Mark {
102    tid: ThreadId,
103    accepts: Option<i32>,
104    armed: AtomicU8,
105    jmpbuf: JmpBufStorage,
106    prev: *mut Mark,
107    /// Atomic because a walk from a fault handler may run concurrently with a
108    /// link/unlink: a critical section cannot exclude a context that preempts
109    /// it, such as an NMI. `prev` stays plain - only the mutators read it, and
110    /// they exclude each other.
111    next: AtomicPtr<Mark>,
112    cause: MaybeUninit<i32>,
113}
114
115struct CallPayload<F, R> {
116    func: ManuallyDrop<F>,
117    result: MaybeUninit<R>,
118}
119
120/// Head of the intrusive list of active marks, most recently linked first.
121static REGISTRY_HEAD: AtomicPtr<Mark> = AtomicPtr::new(ptr::null_mut());
122
123/// Run `f` under recovery protection, tagging this scope with `tid`. Catches
124/// any cause; see [`protect_cause`] to recover from a single cause only.
125///
126/// Returns `Ok(value)` on normal completion, or `Err(RecoveryError)` if
127/// [`recover`] (from the fault/OOM handler) jumped into this scope. On the
128/// `Err` path everything `f` had on the stack is leaked: no destructors run.
129/// Nesting is supported (the handler resolves to the innermost scope for `tid`).
130/// Note that nesting different `tid`s will lead to UB.
131///
132/// ## The [`UnwindSafe`] bound
133///
134/// `protect` requires `F: UnwindSafe` for the reason `std::panic::catch_unwind`
135/// does: a closure abandoned mid-mutation can leave a value torn, so the bound
136/// makes the usual offenders (`&mut T` captures, `Cell`/`RefCell`/`Mutex`) fail
137/// at the call site instead of passing silently. It is advisory -
138/// [`AssertUnwindSafe`] satisfies it unconditionally and safely. The obligations
139/// the type system cannot express are in `# Safety` below, which is why
140/// `protect` is `unsafe`.
141///
142/// ## Recovery-stack guarantee
143///
144/// Before calling `f`, `protect` reserves at least [`RECOVERY_GAP_BYTES`] of
145/// stack between the closure and the recovery mark (the `setjmp` point) and
146/// holds it reserved for the whole run, so `f` never touches it. This gives a
147/// fault handler somewhere to stand: to turn a fault into an `Err`, the handler
148/// resumes the faulting thread and calls [`recover`], which must not overwrite
149/// the mark, the saved `jmp_buf`, or any frame at or before the `protect` call.
150/// Those all sit at or before the mark, and the reserved gap guarantees room
151/// below it - so a handler may land `recover` at the bottom of the thread's
152/// stack and run entirely on abandoned frames.
153///
154/// Gap isn't designed to always be a place to run the handler, but it gives you
155/// a guarantee the you can go off [`RECOVERY_GAP_BYTES`] bytes before the stack
156/// bottom.
157///
158/// # Safety
159///
160/// Recovery rewinds the stack pointer and runs no destructors: every frame `f`
161/// pushed is leaked in place and its storage is reused by later calls. The
162/// caller must ensure nothing depends on those frames living on, or on their
163/// `Drop` running. This is non-exhaustive - among the things it breaks:
164///
165/// - `Pin`'s drop guarantee for stack-pinned `!Unpin` values
166///   (`core::pin::pin!`, an on-stack address-sensitive future, an intrusive
167///   node): the storage is invalidated and reused with no `Drop`. (`Pin<Box<T>>`
168///   is safe - heap storage is only leaked.)
169/// - Raw pointers into the frames dangle after `Err`: fine to hold, UB to
170///   dereference.
171/// - References into the frames dangle too, and a reference can be UB just by
172///   staying live across recovery (using it retags it), not only when read.
173/// - Scope-based APIs (such as `thread::scope`) are bypassed.
174/// - `Drop`-based invariants (lock guards, `RAII cleanup) do not run.
175/// - Interior-mutable state shared outward can be left torn if `f` was
176///   abandoned mid-mutation.
177///
178/// ...and anything else that assumed the stack above the mark stayed valid.
179pub unsafe fn protect<F, R>(tid: ThreadId, f: F) -> Result<R, RecoveryError>
180where
181    F: FnOnce() -> R + UnwindSafe,
182{
183    protect_inner(tid, None, f)
184}
185
186/// Like [`protect`], but only recovers when [`recover`]'s `cause` equals
187/// `cause`; any other cause skips this scope. See [`protect`] for the full
188/// contract.
189pub unsafe fn protect_cause<F, R>(
190    tid: ThreadId,
191    cause: i32,
192    f: F,
193) -> Result<R, RecoveryError>
194where
195    F: FnOnce() -> R + UnwindSafe,
196{
197    protect_inner(tid, Some(cause), f)
198}
199
200// SAFETY: the contract is `protect`'s; the wrappers only pick `accepts`.
201unsafe fn protect_inner<F, R>(
202    tid: ThreadId,
203    accepts: Option<i32>,
204    f: F,
205) -> Result<R, RecoveryError>
206where
207    F: FnOnce() -> R + UnwindSafe,
208{
209    let mut payload = CallPayload::<F, R> {
210        func: ManuallyDrop::new(f),
211        result: MaybeUninit::uninit(),
212    };
213    let mut mark = Mark {
214        tid,
215        accepts,
216        armed: AtomicU8::new(0),
217        jmpbuf: JmpBufStorage::new(),
218        prev: ptr::null_mut(),
219        next: AtomicPtr::new(ptr::null_mut()),
220        cause: MaybeUninit::uninit(),
221    };
222    let mark_ptr: *mut Mark = &mut mark;
223    let jb = JmpBufStorage::raw(&raw const (*mark_ptr).jmpbuf);
224    let armed = (&raw mut (*mark_ptr).armed).cast::<u8>();
225
226    critical_section::with(|_cs| registry_push(mark_ptr));
227
228    let outcome = setback_call(
229        jb,
230        armed,
231        trampoline::<F, R>,
232        &mut payload as *mut CallPayload<F, R> as *mut c_void,
233    );
234
235    critical_section::with(|_cs| registry_unlink(mark_ptr));
236
237    if outcome == SETBACK_OK {
238        // SAFETY: success path wrote the result.
239        Ok(payload.result.assume_init())
240    } else {
241        // SAFETY: a nonzero outcome means `recover` longjmp'd back here, and it
242        // wrote `cause` into this mark before jumping.
243        Err(RecoveryError {
244            cause: (*mark_ptr).cause.assume_init(),
245        })
246    }
247}
248
249unsafe extern "C" fn trampoline<F, R>(data: *mut c_void)
250where
251    F: FnOnce() -> R,
252{
253    // SAFETY: `data` is the &mut CallPayload<F,R> passed into setback_call.
254    let payload = unsafe { &mut *(data as *mut CallPayload<F, R>) };
255    // SAFETY: `payload.func` is a live closure, and we are calling it exactly once.
256    let f = unsafe { ManuallyDrop::take(&mut payload.func) };
257    payload.result.write(f());
258}
259
260/// From the shared fault/OOM handler: recover the thread identified by `tid` by
261/// jumping into its innermost active scope that accepts `cause`, reporting it.
262/// [`protect`] scopes accept any cause; [`protect_cause`] scopes accept one.
263///
264/// Diverges on success: the matching [`protect`] returns
265/// `Err(RecoveryError { cause })`. Returns `Err(RecoveryFailure)` if no active
266/// scope for `tid` accepts `cause`, so the caller can halt or escalate, leaving
267/// every scope live.
268///
269/// Scopes for `tid` nested inside the one it jumps into never return: the jump
270/// abandons their frames and drops their marks from the registry.
271///
272/// # Safety
273/// - `tid` must identify the thread on whose stack the matching `protect` is
274///   still live.
275/// - Must be called from the same thread as `tid`, not from the other thread,
276///   context, or the fault handler.
277/// - All leak / `protect` `# Safety` obligations apply to everything between
278///   the fault point and the mark.
279pub unsafe fn recover(tid: ThreadId, cause: i32) -> Result<Infallible, RecoveryFailure> {
280    let jb = critical_section::with(|_cs| {
281        let mark = registry_find(tid, cause);
282        if mark.is_null() {
283            return ptr::null_mut();
284        }
285        // The jump abandons every scope for `tid` nested inside `mark`; their
286        // marks leave the list here, while it can still be walked safely.
287        registry_unlink_nested(tid, mark);
288        // Stash the cause while the node is locked-live, the matching `protect`
289        // reads it back after the jump. `recover` runs on the faulting thread
290        // and `protect` resumes on it, so the write and read do not race.
291        (*mark).cause = MaybeUninit::new(cause);
292        JmpBufStorage::raw(&raw const (*mark).jmpbuf)
293    });
294    if jb.is_null() {
295        return Err(RecoveryFailure);
296    }
297    setback_longjmp(jb)
298}
299
300/// Whether [`recover`] would find a scope: `true` when `tid` has an active
301/// [`protect`] scope that accepts `cause`.
302///
303/// For a fault handler that must decide *before* it commits to recovery. 
304///
305/// Safe to call from a fault handler, including one that preempts a critical
306/// section.
307pub fn can_recover(tid: ThreadId, cause: i32) -> bool {
308    // SAFETY: `registry_find` needs every node it walks to stay alive, and the
309    // critical section keeps every mutator out for the duration. A caller that
310    // preempts the critical section instead of taking it - a fault handler -
311    // has the mutator stopped mid-`protect`, so its mark cannot go away either.
312    critical_section::with(|_cs| unsafe { !registry_find(tid, cause).is_null() })
313}
314
315unsafe fn registry_push(node: *mut Mark) {
316    let head = REGISTRY_HEAD.load(Ordering::Relaxed);
317    (*node).next.store(head, Ordering::Relaxed);
318    (*node).prev = ptr::null_mut();
319    if !head.is_null() {
320        (*head).prev = node;
321    }
322    // Release, paired with the load in `registry_find`: the node becomes
323    // reachable only once its `tid`, `accepts` and `next` are visible, so a walk
324    // that reaches it never reads them half-written or follows a stale `next`.
325    REGISTRY_HEAD.store(node, Ordering::Release);
326}
327
328unsafe fn registry_unlink(node: *mut Mark) {
329    let prev = (*node).prev;
330    let next = (*node).next.load(Ordering::Relaxed);
331    if prev.is_null() {
332        REGISTRY_HEAD.store(next, Ordering::Release);
333    } else {
334        (*prev).next.store(next, Ordering::Relaxed);
335    }
336    if !next.is_null() {
337        (*next).prev = prev;
338    }
339}
340
341/// Unlink every mark for `tid` that sits ahead of `target` in the list.
342///
343/// A `longjmp` into `target` abandons those scopes' frames without returning
344/// through their `protect`, so nothing else would ever unlink them. For one
345/// `tid` the marks form a LIFO sub-stack, so every mark ahead of `target` is a
346/// scope nested inside it - armed or still arming, both are abandoned by the
347/// jump. Marks for other `tid`s live on other stacks and are left alone.
348///
349/// # Safety
350///
351/// `target` must be a node in the list, and every node this walks must stay
352/// alive for the walk, so the caller must hold the critical section - it keeps
353/// the other mutators out, and this one splices nodes rather than only reading
354/// them, so it cannot run from a context that merely preempts them.
355unsafe fn registry_unlink_nested(tid: ThreadId, target: *mut Mark) {
356    let mut p = REGISTRY_HEAD.load(Ordering::Acquire);
357    while !p.is_null() && p != target {
358        // Read `next` before the splice, so the walk does not rest on what
359        // `registry_unlink` leaves behind in the node it removes.
360        let next = (*p).next.load(Ordering::Relaxed);
361        if (*p).tid == tid {
362            registry_unlink(p);
363        }
364        p = next;
365    }
366}
367
368/// Innermost mark for `tid` that accepts `cause`, or null.
369///
370/// # Safety
371///
372/// Every node this walks must stay alive for the walk. Marks live on the
373/// protected thread's stack, so the caller must either hold the critical
374/// section, which keeps every mutator out, or run in a context that cannot be
375/// preempted by one - a fault handler, whose interrupted mutator is stopped
376/// mid-list and cannot return out of its `protect` frame.
377unsafe fn registry_find(tid: ThreadId, cause: i32) -> *mut Mark {
378    // Acquire, paired with the stores in `registry_push` / `registry_unlink`.
379    // The links are read atomically because this may interrupt a mutator: a
380    // critical section does not exclude the contexts that call `recover` and
381    // `can_recover`. Walking head -> tail keeps that sound. `registry_push`
382    // publishes the head last, and `registry_unlink` only re-points its
383    // neighbours, so a walk in progress sees either list, never a dangling link.
384    let mut p = REGISTRY_HEAD.load(Ordering::Acquire);
385    while !p.is_null() {
386        if (*p).armed.load(Ordering::Acquire) != 0
387            && (*p).tid == tid
388            && (*p).accepts.is_none_or(|c| c == cause)
389        {
390            return p;
391        }
392        p = (*p).next.load(Ordering::Relaxed);
393    }
394    ptr::null_mut()
395}
396
397impl JmpBufStorage {
398    #[inline]
399    fn new() -> Self {
400        let need = unsafe { setback_jmpbuf_size() };
401        let align = unsafe { setback_jmpbuf_align() };
402        assert!(need <= 512, "setback: jmp_buf larger than reserved storage");
403        assert!(
404            align <= 16,
405            "setback: jmp_buf alignment exceeds storage alignment"
406        );
407        JmpBufStorage {
408            bytes: UnsafeCell::new(MaybeUninit::uninit()),
409        }
410    }
411
412    #[inline]
413    unsafe fn raw(this: *const JmpBufStorage) -> *mut c_void {
414        UnsafeCell::raw_get(&raw const (*this).bytes) as *mut c_void
415    }
416}
417
418impl Error for RecoveryFailure {}
419impl core::fmt::Display for RecoveryFailure {
420    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
421        write!(f, "setback recovery failure (no active scope)")
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn a_linked_mark_is_ignored_until_it_is_armed() {
431        const TID: ThreadId = 1234;
432        const CAUSE: i32 = 9;
433
434        let mut mark = Mark {
435            tid: TID,
436            accepts: None,
437            armed: AtomicU8::new(0),
438            jmpbuf: JmpBufStorage::new(),
439            prev: ptr::null_mut(),
440            next: AtomicPtr::new(ptr::null_mut()),
441            cause: MaybeUninit::uninit(),
442        };
443        let mark_ptr: *mut Mark = &mut mark;
444
445        let found = || critical_section::with(|_cs| !unsafe { registry_find(TID, CAUSE) }.is_null());
446
447        unsafe {
448            critical_section::with(|_cs| registry_push(mark_ptr));
449            assert!(!found());
450
451            (*mark_ptr).armed.store(1, Ordering::Release);
452            assert!(found());
453
454            critical_section::with(|_cs| registry_unlink(mark_ptr));
455            assert!(!found());
456        }
457    }
458}