setback
no_std failure recovery for Rust built on C setjmp/longjmp, with all of
the jump confined to C frames. Set a mark, run a closure, and get Ok(value)
back - or, if a stack overflow, OOM, or explicit trigger fires while it runs,
get Err(RecoveryError) with the abandoned stack leaked (no destructors run).
It targets mostly-pure computations - parsers, interpreters, deserializers - that might recurse too deep or exhaust the heap on hostile input, where you want a clean error on the faulting task instead of a system reset. Built for bare-metal / RTOS targets, runs on the host too, though modern machines with MMUs should not need this crate.
Capabilities
- One call protects a scope.
protect(tid, f)runsfand returnsResult<R, RecoveryError>. Nesting works and recovery resolves to the innermost scope for a thread. - One shared fault handler recovers any task by id:
recover(tid, cause)jumps into that thread's innermost active scope.causeis anyi32whose meaning you choose. It resurfaces asRecoveryError::cause. - A fault handler can look before it leaps:
can_recover(tid, cause)reports whetherrecoverwould find a scope, for handlers that pay something irreversible to reachrecoverat all. no_stdandno_alloc. A singlestaticintrusive list keyed by thread id holds the active marks, based oncritical-section.- A recovery-stack gap (
RECOVERY_GAP_BYTES) is reserved below each mark so a fault handler always has stack to runrecoveron.
How it works
All setjmp/longjmp lives in src/setback.c. A longjmp unwinds that one C
frame back to its setjmp, abandoning (and leaking) the Rust frames above it.
The full rationale, contract, and recovery-stack guarantee are in the
API docs - see protect and recover.
Usage
use ;
// Config is large and deeply nested. Hostile JSON can recurse serde past the
// stack limit or exhaust the heap.
Caveats
recovermust be called on (or for) the faulting thread, whoseprotectframe must still be live.- On embedded targets, recovering from a fault handler into thread-mode code needs
glue to resume at a thunk that runs the jump in thread mode (
longjmpis a plain branch, not an exception return).recoverfinds the mark, the mode transition is yours.
Simplified wiring example (you provide the fault handler, stack guard, and CS impl)
use ;
const STACK_OVERFLOW: i32 = 1;
const OOM: i32 = 2;
// 1. cortex-m provides the critical-section impl (single-core):
// cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
// 2. Put an MPU guard region (or PSPLIM on Armv8-M) below each task stack so an
// overflow faults instead of corrupting RAM.
// OOM fires in THREAD mode: call recover directly.
!
// Stack overflow fires in HANDLER MODE on a broken stack. The handler only
// redirects: rewrite the stacked PSP frame so the exception return resumes at
// overflow_trampoline in thread mode (see `recover` Safety for the full recipe).
unsafe
// Reset SP (the overflowed stack is unusable) then enter thread-mode Rust.
extern "C" !
extern "C" !
Testing
- Host tests:
cargo test --features std(pulls in critical-section's std-backed impl so the registry locking links).