Skip to main content

rivet/preempt/
mutex.rs

1//! Priority-inheritance mutex for the preemptive tier.
2//!
3//! Classic priority inversion: a low-priority task holds a resource; a
4//! high-priority task blocks waiting for it; a *medium*-priority task
5//! (uninvolved with the resource) preempts the low-priority holder and
6//! runs indefinitely, indirectly blocking the high-priority task for far
7//! longer than the critical section itself would ever take.
8//!
9//! Priority inheritance fixes this: while a higher-priority task is
10//! blocked on a mutex, the current holder's *effective* priority is
11//! boosted to match, so it can't be preempted by anything the waiter
12//! itself couldn't preempt. The boost is undone on unlock — but see [B11]:
13//! with *nested* mutexes, unlocking one must recompute the boost from the
14//! remaining held mutexes rather than blindly restoring the base priority.
15
16use core::cell::UnsafeCell;
17use core::ops::{Deref, DerefMut};
18
19use super::sched;
20use super::tcb::{self, MAX_PTASKS, NO_TASK};
21use crate::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
22
23/// Error returned by [`PriorityMutex::lock_timeout`].
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum LockError {
26    /// The calling task already holds this mutex (re-entrancy — a
27    /// self-deadlock).
28    Recursive,
29    /// `lock_timeout`'s deadline passed before the mutex was acquired.
30    Timeout,
31    /// The calling task already holds [`tcb::MAX_HELD`] mutexes.
32    TooManyHeldMutexes,
33    /// Called outside of a preemptive task context.
34    NotInTask,
35    /// The mutex was poisoned: its previous holder faulted while holding
36    /// it (plan.md §3.4). The data may be inconsistent.
37    Poisoned,
38}
39
40/// A mutex that applies priority inheritance to its holder while
41/// higher-priority tasks are waiting on it.
42///
43/// `#[repr(C)]`: the fault-isolation path (`poison_mutex`) casts a
44/// type-erased pointer to `PriorityMutex<()>`, so the leading-field layout
45/// must be identical across monomorphizations.
46#[repr(C)]
47pub struct PriorityMutex<T> {
48    locked: AtomicBool,
49    owner: AtomicUsize,
50    waiters: [AtomicUsize; MAX_PTASKS],
51    /// Set when a faulting holder was isolated while holding this mutex
52    /// (plan.md §3.4). `lock()`/`try_lock()` fail with
53    /// [`LockError::Poisoned`] once set.
54    poisoned: AtomicBool,
55    data: UnsafeCell<T>,
56}
57
58// Safety: access to `data` is only granted through `PriorityMutexGuard`,
59// obtained while `locked` is held — standard mutex safety argument.
60unsafe impl<T: Send> Sync for PriorityMutex<T> {}
61
62impl<T> PriorityMutex<T> {
63    #[cfg(not(loom))]
64    pub const fn new(value: T) -> Self {
65        Self {
66            locked: AtomicBool::new(false),
67            owner: AtomicUsize::new(NO_TASK),
68            // Inline const avoids a named `const` item with interior
69            // mutability (clippy::declare_interior_mutable_const).
70            waiters: [const { AtomicUsize::new(NO_TASK) }; MAX_PTASKS],
71            poisoned: AtomicBool::new(false),
72            data: UnsafeCell::new(value),
73        }
74    }
75
76    /// Loom's atomics are not const-constructible; runtime constructor.
77    #[cfg(loom)]
78    pub fn new(value: T) -> Self {
79        Self {
80            locked: AtomicBool::new(false),
81            owner: AtomicUsize::new(NO_TASK),
82            waiters: core::array::from_fn(|_| AtomicUsize::new(NO_TASK)),
83            poisoned: AtomicBool::new(false),
84            data: UnsafeCell::new(value),
85        }
86    }
87
88    /// Whether this mutex has been poisoned by a faulting holder.
89    pub fn is_poisoned(&self) -> bool {
90        self.poisoned.load(Ordering::Acquire)
91    }
92
93    /// Non-blocking acquire. Returns `None` if the mutex is held (by
94    /// anyone, including this task).
95    pub fn try_lock(&self) -> Option<PriorityMutexGuard<'_, T>> {
96        let me = sched::current()?;
97        self.try_acquire_guarded(me)
98    }
99
100    /// Acquire the mutex, blocking (yielding the CPU to other preemptive
101    /// tasks — not busy-spinning) while it's held elsewhere. Applies
102    /// priority inheritance to the current holder for as long as this
103    /// task waits.
104    ///
105    /// # Panics
106    /// Panics if called outside of a preemptive task context, if the
107    /// calling task already holds this mutex (recursive lock), or if the
108    /// task already holds [`tcb::MAX_HELD`] mutexes.
109    pub fn lock(&self) -> PriorityMutexGuard<'_, T> {
110        match self.lock_timeout(None) {
111            Ok(g) => g,
112            Err(LockError::Recursive) => panic!(
113                "PriorityMutex::lock: recursive lock from the same task \
114                 (self-deadlock; use try_lock or restructure)"
115            ),
116            Err(LockError::TooManyHeldMutexes) => panic!(
117                "PriorityMutex::lock: task already holds MAX_HELD={} mutexes",
118                tcb::MAX_HELD
119            ),
120            Err(LockError::NotInTask) => {
121                panic!("PriorityMutex::lock() outside preemptive task context")
122            }
123            Err(LockError::Poisoned) => panic!(
124                "PriorityMutex::lock: mutex poisoned by a faulting holder                  (data may be inconsistent; use lock_timeout/try_lock to recover)"
125            ),
126            Err(LockError::Timeout) => unreachable!("lock() has no timeout"),
127        }
128    }
129
130    /// Acquire the mutex with a deadline. Returns
131    /// [`LockError::Timeout`] if the mutex is not acquired within
132    /// `timeout`; `None` waits forever.
133    pub fn lock_timeout(
134        &self,
135        timeout: Option<crate::time::Duration>,
136    ) -> Result<PriorityMutexGuard<'_, T>, LockError> {
137        let me = sched::current().ok_or(LockError::NotInTask)?;
138        let deadline = timeout.map(|d| crate::port::board::now_us().wrapping_add(d.as_micros()));
139
140        loop {
141            // Fast path (lock free).
142            if self.poisoned.load(Ordering::Acquire) {
143                return Err(LockError::Poisoned);
144            }
145            if let Some(g) = self.try_acquire_guarded(me) {
146                return Ok(g);
147            }
148
149            // Re-entrancy: only this task could have failed the CAS while
150            // being the owner.
151            if self.owner.load(Ordering::Acquire) == me {
152                return Err(LockError::Recursive);
153            }
154
155            if let Some(d) = deadline {
156                if crate::port::board::now_us() >= d {
157                    // Deregister so a later unlock can't spuriously wake a
158                    // task that is no longer waiting.
159                    self.remove_waiter(me);
160                    crate::timer::cancel_ptask_deadline(me);
161                    return Err(LockError::Timeout);
162                }
163            }
164
165            // Slow path — the whole check/register/block sequence runs
166            // with interrupts disabled (plan.md [B1]): the CAS is
167            // *re-tested* inside the critical section, so a tick that
168            // lands between the failed fast-path CAS and add_waiter can
169            // never let the holder run to completion and release with
170            // nobody registered — the re-test catches the release.
171            let outcome = crate::critical::enter(|| {
172                if let Some(g) = self.try_acquire_guarded(me) {
173                    Ok(g)
174                } else {
175                    self.boost_holder(me);
176                    self.add_waiter(me);
177                    if let Some(d) = deadline {
178                        let _ = crate::timer::register_ptask_deadline(d, me);
179                    }
180                    sched::block_current();
181                    Err(LockError::Timeout) // placeholder; only used if the
182                                            // caller falls through
183                }
184            });
185            match outcome {
186                Ok(g) => return Ok(g),
187                Err(_) => {
188                    // Registered as a waiter and blocked; actually give up
189                    // the CPU (outside the critical section; the
190                    // software-interrupt/PendSV path handles the switch).
191                    // Woken spuriously or by unlock/timeout — loop back
192                    // and re-test.
193                    crate::port::arch::request_reschedule();
194                }
195            }
196        }
197    }
198
199    /// CAS + owner-store + held-list registration in one step. On held-list
200    /// overflow the acquire is rolled back (the lock must never be left
201    /// held with no guard to release it).
202    fn try_acquire_guarded(&self, me: usize) -> Option<PriorityMutexGuard<'_, T>> {
203        if self.poisoned.load(Ordering::Acquire) {
204            return None;
205        }
206        if !self.try_acquire(me) {
207            return None;
208        }
209        match self.push_held(me) {
210            Ok(()) => {
211                #[cfg(feature = "trace")]
212                crate::trace::mutex_lock_acquired(me as u16, self as *const _ as usize as u32);
213                Some(PriorityMutexGuard { mutex: self })
214            }
215            Err(_) => {
216                // Roll back the acquire we just made so the lock is never
217                // left held without a guard.
218                self.owner.store(NO_TASK, Ordering::Release);
219                self.locked.store(false, Ordering::Release);
220                None
221            }
222        }
223    }
224
225    /// Attempt the CAS + owner-store. No blocking.
226    fn try_acquire(&self, me: usize) -> bool {
227        if self
228            .locked
229            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
230            .is_ok()
231        {
232            self.owner.store(me, Ordering::Release);
233            true
234        } else {
235            false
236        }
237    }
238
239    /// Record this mutex in the calling task's held list ([B11]).
240    fn push_held(&self, me: usize) -> Result<(), LockError> {
241        let tcb = tcb::get(me).ok_or(LockError::NotInTask)?;
242        if tcb.push_held(
243            self as *const _ as *const (),
244            Self::highest_waiter_priority_erased,
245        ) {
246            Ok(())
247        } else {
248            Err(LockError::TooManyHeldMutexes)
249        }
250    }
251
252    /// Type-erased `highest_waiter_priority` accessor for [`HeldMutex`].
253    fn highest_waiter_priority_erased(ptr: *const ()) -> u8 {
254        // SAFETY: `ptr` was registered by `push_held` as `self as *const _`
255        // for a `PriorityMutex<T>` of this exact type, and the mutex is
256        // still alive (it is being held).
257        unsafe { (&*(ptr as *const PriorityMutex<T>)).highest_waiter_priority() }
258    }
259
260    /// Highest *base* priority among the tasks currently waiting on this
261    /// mutex (0 if none).
262    fn highest_waiter_priority(&self) -> u8 {
263        let mut max = 0u8;
264        for slot in &self.waiters {
265            let id = slot.load(Ordering::Acquire);
266            if id != NO_TASK {
267                if let Some(w) = tcb::get(id) {
268                    let b = w.base_priority.load(Ordering::Acquire);
269                    if b > max {
270                        max = b;
271                    }
272                }
273            }
274        }
275        max
276    }
277
278    /// Boost the current holder's effective priority to at least ours.
279    fn boost_holder(&self, me: usize) {
280        let owner_id = self.owner.load(Ordering::Acquire);
281        if owner_id != NO_TASK {
282            if let (Some(me_tcb), Some(owner_tcb)) = (tcb::get(me), tcb::get(owner_id)) {
283                let my_base = me_tcb.base_priority.load(Ordering::Acquire);
284                let owner_eff = owner_tcb.effective_priority.load(Ordering::Acquire);
285                if my_base > owner_eff {
286                    owner_tcb.set_effective_priority(owner_id, my_base);
287                    #[cfg(feature = "trace")]
288                    crate::trace::priority_inherit(
289                        owner_id as u16,
290                        self as *const _ as usize as u32,
291                    );
292                }
293            }
294        }
295    }
296
297    fn add_waiter(&self, id: usize) {
298        for slot in &self.waiters {
299            if slot
300                .compare_exchange(NO_TASK, id, Ordering::AcqRel, Ordering::Acquire)
301                .is_ok()
302            {
303                return;
304            }
305        }
306        // Waiter list full (more concurrent waiters than MAX_PTASKS, which
307        // is impossible since MAX_PTASKS bounds total task count) — unreachable.
308    }
309
310    /// Deregister a waiter (e.g. it timed out and is no longer waiting).
311    fn remove_waiter(&self, id: usize) {
312        for slot in &self.waiters {
313            let _ = slot.compare_exchange(id, NO_TASK, Ordering::AcqRel, Ordering::Acquire);
314        }
315    }
316
317    fn wake_all_waiters(&self) {
318        for slot in &self.waiters {
319            let id = slot.swap(NO_TASK, Ordering::AcqRel);
320            if id != NO_TASK {
321                sched::unblock(id);
322                crate::timer::cancel_ptask_deadline(id);
323            }
324        }
325    }
326}
327
328/// Mark a type-erased `PriorityMutex` as poisoned and wake its waiters so
329/// they observe the poison (called by the fault-isolation path, plan.md
330/// §3.4).
331///
332/// # Safety
333/// `ptr` must be a live `PriorityMutex<T>` address that was registered in
334/// some task's held list.
335pub unsafe fn poison_mutex(ptr: *const ()) {
336    // SAFETY: contract above.
337    unsafe {
338        let m = &*(ptr as *const PriorityMutex<()>);
339        m.poisoned.store(true, Ordering::Release);
340        m.wake_all_waiters();
341    }
342}
343
344/// RAII guard returned by [`PriorityMutex::lock`]. On drop: releases the
345/// mutex, recomputes the holder's effective priority from its *remaining*
346/// held mutexes ([B11] — nested inheritance), wakes waiters, and requests
347/// a reschedule.
348pub struct PriorityMutexGuard<'a, T> {
349    mutex: &'a PriorityMutex<T>,
350}
351
352impl<'a, T> Deref for PriorityMutexGuard<'a, T> {
353    type Target = T;
354    fn deref(&self) -> &T {
355        // SAFETY: access to `data` is only reachable through a
356        // `PriorityMutexGuard`, which exists only while `locked` is held;
357        // exclusive ownership of `data` is guaranteed by the mutex protocol.
358        unsafe { &*self.mutex.data.get() }
359    }
360}
361
362impl<'a, T> DerefMut for PriorityMutexGuard<'a, T> {
363    fn deref_mut(&mut self) -> &mut T {
364        // SAFETY: same argument as `Deref::deref` — the guard holds the
365        // mutex exclusively, and `&mut self` proves no other reference to
366        // the data is live.
367        unsafe { &mut *self.mutex.data.get() }
368    }
369}
370
371impl<'a, T> Drop for PriorityMutexGuard<'a, T> {
372    fn drop(&mut self) {
373        // The whole unlock — held-list update, effective-priority
374        // recompute, and waking every waiter — must commit as one step.
375        // `ready_add`/`ready_remove` (reached via `set_effective_priority`
376        // and `wake_all_waiters`'s `unblock`) touch `READY_BITMAP` and
377        // `QUEUES` as two separate atomics; without a critical section
378        // here, a tick landing mid-unlock can observe that torn state —
379        // e.g. a waiter's `Ready` transition half-applied — and the
380        // scheduler's bitmap/queue pair can end up permanently
381        // inconsistent (a stale `READY_BITMAP` bit with an empty queue
382        // word). Matches the pattern already used for the equivalent
383        // lock-side sequence (`lock_timeout`'s own `critical::enter`
384        // around boost/add_waiter/register/block) and the join path.
385        crate::critical::enter(|| {
386            let owner = self.mutex.owner.swap(NO_TASK, Ordering::AcqRel);
387            #[cfg(feature = "trace")]
388            if owner != NO_TASK {
389                crate::trace::mutex_unlock(
390                    owner as u16,
391                    self.mutex as *const _ as usize as u32,
392                );
393            }
394            if owner != NO_TASK {
395                // Remove this mutex from the holder's held list, then
396                // recompute the holder's effective priority from what
397                // remains (plan.md [B11]: unlocking one mutex must not
398                // drop the boost held for another).
399                if let Some(t) = tcb::get(owner) {
400                    t.remove_held(self.mutex as *const _ as *const ());
401                    let base = t.base_priority.load(Ordering::Acquire);
402                    let mut eff = base;
403                    for slot in &t.held {
404                        let ptr = slot.ptr.load(Ordering::Acquire);
405                        if !ptr.is_null() {
406                            // SAFETY: the hwp fn pointer was registered by
407                            // the matching `push_held` for a live mutex;
408                            // loading ptr (Acquire) orders the hwp read.
409                            let hwp = unsafe {
410                                let f: fn(*const ()) -> u8 =
411                                    core::mem::transmute(slot.hwp.load(Ordering::Acquire));
412                                f(ptr)
413                            };
414                            if hwp > eff {
415                                eff = hwp;
416                            }
417                        }
418                    }
419                    // `set_effective_priority` (not a plain store): the
420                    // unlocking task is normally `Running` (unqueued), so
421                    // this is usually a no-op queue-wise — but going
422                    // through the real API keeps that true by
423                    // construction instead of by the caller happening to
424                    // always be the running task, and costs nothing extra
425                    // when it is.
426                    t.set_effective_priority(owner, eff);
427                }
428            }
429            self.mutex.locked.store(false, Ordering::Release);
430            self.mutex.wake_all_waiters();
431        });
432        // Give a higher-priority waiter (now Ready) an immediate chance to
433        // preempt us rather than waiting for the next tick.
434        crate::port::arch::request_reschedule();
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use crate::preempt::tcb as tcbmod;
442
443    #[test]
444    fn lock_unlock_basic() {
445        crate::kernel_test! {
446            let a = tcbmod::register(0x1000, 1).unwrap();
447            sched::set_current(a);
448
449            let m: PriorityMutex<u32> = PriorityMutex::new(0);
450            {
451                let mut guard = m.lock();
452                *guard = 42;
453            }
454            assert_eq!(*m.lock(), 42);
455        }
456    }
457
458    #[test]
459    fn try_lock_behavior() {
460        crate::kernel_test! {
461            let a = tcbmod::register(0x1000, 1).unwrap();
462            sched::set_current(a);
463
464            let m: PriorityMutex<u32> = PriorityMutex::new(0);
465            let g = m.try_lock().expect("free mutex must lock");
466            assert!(m.try_lock().is_none(), "held mutex must not lock");
467            drop(g);
468            assert!(m.try_lock().is_some(), "released mutex must lock");
469        }
470    }
471
472    #[test]
473    #[should_panic(expected = "recursive lock")]
474    fn recursive_lock_panics() {
475        crate::kernel_test! {
476            let a = tcbmod::register(0x1000, 1).unwrap();
477            sched::set_current(a);
478
479            let m: PriorityMutex<u32> = PriorityMutex::new(0);
480            let _g = m.lock();
481            let _ = m.lock(); // must panic
482        }
483    }
484
485    #[test]
486    fn priority_inheritance_boosts_holder() {
487        crate::kernel_test! {
488            let low = tcbmod::register(0x1000, 1).unwrap();
489            let high = tcbmod::register(0x2000, 5).unwrap();
490
491            let m: PriorityMutex<u32> = PriorityMutex::new(0);
492
493            // `low` acquires the lock.
494            sched::set_current(low);
495            let guard = m.lock();
496            assert_eq!(
497                tcbmod::get(low).unwrap().effective_priority.load(Ordering::Acquire),
498                1
499            );
500
501            // `high` contends for the lock (would block — but that calls
502            // port::arch::request_reschedule(), which is a no-op on the dummy/host arch).
503            // Directly exercise the boost logic used inside lock()'s slow path
504            // by simulating one contention iteration.
505            sched::set_current(high);
506            let owner_id = low;
507            let my_base = tcbmod::get(high).unwrap().base_priority.load(Ordering::Acquire);
508            let owner_tcb = tcbmod::get(owner_id).unwrap();
509            if my_base > owner_tcb.effective_priority.load(Ordering::Acquire) {
510                owner_tcb.effective_priority.store(my_base, Ordering::Release);
511            }
512            assert_eq!(
513                tcbmod::get(low).unwrap().effective_priority.load(Ordering::Acquire),
514                5
515            );
516
517            drop(guard);
518            // Unlock restores the holder's base priority (no other held
519            // mutexes, no remaining waiters).
520            assert_eq!(
521                tcbmod::get(low).unwrap().effective_priority.load(Ordering::Acquire),
522                1
523            );
524        }
525    }
526
527    #[test]
528    fn b11_nested_unlock_keeps_boost_from_other_mutex() {
529        crate::kernel_test! {
530            // Statics, not locals: the held-list stores a raw mutex pointer
531            // that outlives any single scope (miri: 'static provenance).
532            static A: PriorityMutex<u32> = PriorityMutex::new(0);
533            static B: PriorityMutex<u32> = PriorityMutex::new(0);
534
535            let holder = tcbmod::register(0x1000, 1).unwrap();
536            sched::set_current(holder);
537
538            let ga = A.lock();
539            let gb = B.lock();
540
541            // A waiter (priority 8) registers on mutex A's waiters array,
542            // simulating the state after its slow path ran: boost the
543            // holder to 8 and record the waiter.
544            let waiter = tcbmod::register(0x3000, 8).unwrap();
545            sched::set_current(waiter);
546            // Simulate the contender's slow-path registration on A.
547            A.waiters[0].store(waiter, Ordering::Release);
548            let holder_tcb = tcbmod::get(holder).unwrap();
549            holder_tcb.effective_priority.store(8, Ordering::Release);
550            // (waiter would also have called add_waiter + block_current,
551            // but for the [B11] unit check the boost + waiter entry are
552            // what matters.)
553
554            sched::set_current(holder);
555            // Unlock B. Old behavior: effective_priority := base (1),
556            // clobbering the boost held for A. New behavior: recompute
557            // from remaining held mutexes -> A's highest waiter = 8.
558            drop(gb);
559            assert_eq!(
560                holder_tcb.effective_priority.load(Ordering::Acquire),
561                8,
562                "[B11] unlocking B must not drop the boost held for A"
563            );
564
565            // Unlock A: no waiters remain -> back to base.
566            drop(ga);
567            assert_eq!(
568                holder_tcb.effective_priority.load(Ordering::Acquire),
569                1,
570                "[B11] after unlocking both, effective priority = base"
571            );
572        }
573    }
574
575    #[test]
576    fn b1_retest_inside_critical_section_catches_release() {
577        crate::kernel_test! {
578            let a = tcbmod::register(0x1000, 1).unwrap();
579            let b = tcbmod::register(0x2000, 5).unwrap();
580            let m: PriorityMutex<u32> = PriorityMutex::new(0);
581
582            // A holds the mutex.
583            sched::set_current(a);
584            let guard_a = m.lock();
585
586            // B's fast-path CAS fails.
587            sched::set_current(b);
588            assert!(!m.try_acquire(b), "A holds the mutex");
589
590            // [B1] interleaving: a tick lands between B's failed CAS and
591            // B's add_waiter; the holder runs to completion and releases,
592            // and wake_all_waiters() scans an empty list.
593            sched::set_current(a);
594            drop(guard_a);
595
596            // B resumes: the critical-section *re-test* must observe the
597            // release and acquire — with the old code, B would register a
598            // waiter nobody will ever wake and block forever.
599            sched::set_current(b);
600            let guard_b = m.lock();
601            assert!(guard_b.mutex.locked.load(Ordering::Acquire));
602            drop(guard_b);
603        }
604    }
605}