Skip to main content

rivet/
fault.rs

1//! Fault policy (plan.md §3.4).
2//!
3//! When the arch layer catches a hardware fault — a MemManage fault on
4//! Cortex-M, an access fault on RISC-V (mcause 1/5/7, typically from a PMP
5//! guard), or a detected stack overflow — it builds a [`FaultInfo`] and
6//! dispatches it here. Two explicit policies:
7//!
8//! - [`FaultPolicy::Panic`] (default): dump a diagnosis (kind, address,
9//!   PC, faulting task) and reset the system.
10//! - [`FaultPolicy::IsolateTask`]: mark the faulting task `Faulted`,
11//!   poison every [`PriorityMutex`](crate::preempt::PriorityMutex) it
12//!   holds, invoke the user `on_task_fault` hook, and context-switch to
13//!   the next ready task — genuine per-task fault containment, using only
14//!   the scheduler's existing "resume an arbitrary sp" primitive.
15
16use core::sync::atomic::{AtomicU8, Ordering};
17
18/// What kind of fault occurred.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum FaultKind {
21    /// RISC-V instruction access fault (mcause 1).
22    InstructionAccess,
23    /// RISC-V load access fault (mcause 5).
24    LoadAccess,
25    /// RISC-V store access fault (mcause 7).
26    StoreAccess,
27    /// Cortex-M MemManage fault; payload is the CFSR.
28    MemManage(u32),
29    /// Detected by stack watermarking at a context switch.
30    StackOverflow,
31    /// A task with a configured `budget_us` (plan.md Phase 11) ran longer
32    /// than its budget within one period, detected at tick time.
33    BudgetExceeded,
34}
35
36/// Everything needed to attribute and report a fault.
37#[derive(Debug, Clone, Copy)]
38pub struct FaultInfo {
39    /// The preemptive task that was running, if any.
40    pub task_id: Option<usize>,
41    pub kind: FaultKind,
42    /// Faulting address (mtval / MMFAR); 0 when not applicable.
43    pub address: usize,
44    /// PC at the fault (mepc / stacked PC); 0 when not applicable.
45    pub pc: usize,
46}
47
48/// Fault handling policy.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum FaultPolicy {
51    /// Dump and reset (default).
52    Panic,
53    /// Mark faulted, poison held mutexes, hook, and switch to another task.
54    IsolateTask,
55}
56
57const POLICY_PANIC: u8 = 0;
58const POLICY_ISOLATE: u8 = 1;
59static POLICY: AtomicU8 = AtomicU8::new(POLICY_PANIC);
60
61/// User hook invoked (under `IsolateTask`) with the faulting task id and
62/// the fault info, before the scheduler switches away.
63pub type OnTaskFault = fn(usize, &FaultInfo);
64static HOOK: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
65
66/// Set the fault policy.
67pub fn set_policy(policy: FaultPolicy) {
68    POLICY.store(
69        match policy {
70            FaultPolicy::Panic => POLICY_PANIC,
71            FaultPolicy::IsolateTask => POLICY_ISOLATE,
72        },
73        Ordering::Release,
74    );
75}
76
77/// Register the `IsolateTask` hook (called with the faulting task id).
78pub fn set_on_task_fault(hook: OnTaskFault) {
79    HOOK.store(hook as usize, Ordering::Release);
80}
81
82/// Handle a fault. Under [`FaultPolicy::Panic`] this diverges (dumps and
83/// resets). Under [`FaultPolicy::IsolateTask`] it returns the stack pointer
84/// the trap handler should resume — the next ready task's.
85pub fn on_fault(info: &FaultInfo) -> usize {
86    #[cfg(feature = "trace")]
87    {
88        let reason = match info.kind {
89            FaultKind::InstructionAccess => 0,
90            FaultKind::LoadAccess => 1,
91            FaultKind::StoreAccess => 2,
92            FaultKind::MemManage(_) => 3,
93            FaultKind::StackOverflow => 4,
94            FaultKind::BudgetExceeded => 5,
95        };
96        crate::trace::fault(info.task_id.map(|id| id as u16).unwrap_or(0xffff), reason, info.pc as u32);
97    }
98    if POLICY.load(Ordering::Acquire) == POLICY_ISOLATE {
99        isolate(info)
100    } else {
101        panic_policy(info)
102    }
103}
104
105fn dump(info: &FaultInfo) {
106    crate::console::write_str("\nRIVET FAULT: ");
107    match info.kind {
108        FaultKind::InstructionAccess => crate::console::write_str("instruction-access"),
109        FaultKind::LoadAccess => crate::console::write_str("load-access"),
110        FaultKind::StoreAccess => crate::console::write_str("store-access"),
111        FaultKind::MemManage(_) => crate::console::write_str("memmanage"),
112        FaultKind::StackOverflow => crate::console::write_str("stack-overflow"),
113        FaultKind::BudgetExceeded => crate::console::write_str("budget-exceeded"),
114    }
115    crate::console::write_str(" addr=0x");
116    print_hex(info.address);
117    crate::console::write_str(" pc=0x");
118    print_hex(info.pc);
119    if let Some(id) = info.task_id {
120        crate::console::write_str(" task=");
121        print_dec(id);
122    }
123    crate::console::write_str("\n");
124}
125
126fn panic_policy(info: &FaultInfo) -> ! {
127    dump(info);
128    // Dump stack watermarks for all tasks (helps right-size stacks).
129    for (id, t) in crate::preempt::tcb::TASKS.iter().enumerate() {
130        if t.used.load(Ordering::Acquire) {
131            let base = t.stack_base.load(Ordering::Acquire);
132            let size = t.stack_size.load(Ordering::Acquire);
133            if base != 0 && size != 0 {
134                // SAFETY: reading a static task stack is safe (kernel
135                // context, the faulting task is frozen).
136                let used = crate::preempt::stack_usage(unsafe {
137                    core::slice::from_raw_parts(base as *const u8, size)
138                });
139                crate::console::write_str("  task ");
140                print_dec(id);
141                crate::console::write_str(" stack ");
142                print_dec(used);
143                crate::console::write_str("/");
144                print_dec(size);
145                crate::console::write_str("\n");
146            }
147        }
148    }
149    // Halt with a distinguishable code (reset is reserved for the
150    // watchdog path, which the fault tests would otherwise loop on).
151    crate::port::board::exit(0xFA)
152}
153
154fn isolate(info: &FaultInfo) -> usize {
155    dump(info);
156
157    // 1. Mark the faulting task Faulted (Blocked so the scheduler skips it)
158    //    and free its slot's scheduling participation.
159    // No task context to isolate — fall back to panic semantics.
160    let faulting = info.task_id.unwrap_or_else(|| panic_policy(info));
161
162    // 2. Poison every mutex the task held (plan.md §3.4: held list from
163    //    Phase 2.3) and wake its waiters so they observe the poison.
164    if let Some(t) = crate::preempt::tcb::get(faulting) {
165        for slot in &t.held {
166            let ptr = slot.ptr.load(Ordering::Acquire);
167            if !ptr.is_null() {
168                // SAFETY: the held list only ever contains live
169                // `PriorityMutex` addresses registered by push_held.
170                unsafe {
171                    crate::preempt::mutex::poison_mutex(ptr);
172                }
173            }
174        }
175        t.set_state(faulting, crate::preempt::tcb::TaskState::Blocked);
176    }
177
178    // 3. User hook.
179    let hook = HOOK.load(Ordering::Acquire);
180    if hook != 0 {
181        // SAFETY: the hook is a function pointer set via set_on_task_fault.
182        unsafe {
183            let f: OnTaskFault = core::mem::transmute(hook);
184            f(faulting, info);
185        }
186    }
187
188    // 4. Switch to the next ready task.
189    match crate::preempt::sched::schedule() {
190        Some(next) => {
191            if let Some(nt) = crate::preempt::tcb::get(next) {
192                nt.set_state(next, crate::preempt::tcb::TaskState::Running);
193                crate::preempt::sched::set_current(next);
194                crate::port::arch::on_switch_to(
195                    nt.stack_base.load(Ordering::Acquire),
196                    nt.stack_size.load(Ordering::Acquire),
197                );
198            }
199            nt_sp(next)
200        }
201        None => panic_policy(info),
202    }
203}
204
205fn nt_sp(id: usize) -> usize {
206    crate::preempt::tcb::get(id)
207        .map(|t| t.sp.load(Ordering::Acquire))
208        .unwrap_or(0)
209}
210
211fn print_hex(mut n: usize) {
212    let mut buf = [0u8; 8];
213    for i in (0..8).rev() {
214        let d = (n & 0xF) as u8;
215        buf[i] = if d < 10 { b'0' + d } else { b'a' + d - 10 };
216        n >>= 4;
217    }
218    if let Ok(s) = core::str::from_utf8(&buf) {
219        crate::console::write_str(s);
220    }
221}
222
223fn print_dec(mut n: usize) {
224    if n == 0 {
225        crate::console::write_str("0");
226        return;
227    }
228    let mut digits = [0u8; 10];
229    let mut i = 0;
230    while n > 0 {
231        digits[i] = b'0' + (n % 10) as u8;
232        n /= 10;
233        i += 1;
234    }
235    let mut out = [0u8; 10];
236    for j in 0..i {
237        out[j] = digits[i - 1 - j];
238    }
239    if let Ok(s) = core::str::from_utf8(&out[..i]) {
240        crate::console::write_str(s);
241    }
242}