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}