Skip to content

Commit 6823b69

Browse files
authored
zephyr: sync: Disallow Mutex reentrancy to prevent unsound mutable aliasing (#181)
## Problem `zephyr::sync::Mutex` wraps Zephyr's underlying `k_mutex`. In Zephyr, `k_mutex` is recursive by design, allowing the owning thread to lock it multiple times without blocking. Consequently, `Mutex::lock()` and `Mutex::try_lock()` allowed reentrant acquisition from the same thread. Because `MutexGuard` implements `DerefMut`, allowing reentrancy permitted safe Rust code to obtain multiple concurrent mutable references (`&mut T`) to the inner data: ```rust let mutex = Mutex::new(0); let mut g1 = mutex.lock().unwrap(); let mut g2 = mutex.lock().unwrap(); // Succeeded because k_mutex is recursive! let r1: &mut i32 = &mut *g1; let r2: &mut i32 = &mut *g2; // Undefined Behavior: concurrent mutable references to the same data ``` This violates Rust's fundamental aliasing rules ("aliasing XOR mutability") and introduces undefined behavior in safe code. ## Solution Track whether the mutex is currently locked by a guard using an `AtomicBool`: 1. In `lock()`, acquire the underlying `sys::Mutex`. If `locked` was already true (indicating recursive acquisition by the current thread), immediately unlock the `sys::Mutex` to restore its lock count and panic with `"cannot recursively lock Mutex"`. 2. In `try_lock()`, acquire the underlying `sys::Mutex`. If `locked` was already true, immediately unlock and return `Err(TryLockError::WouldBlock)`. 3. In `MutexGuard::drop()`, clear `locked` before unlocking `sys::Mutex`. 4. In `Condvar::wait()`, clear `locked` before calling `sys::Condvar::wait()` and re-assert `locked` upon re-acquiring the mutex. ## Prior Issues / PRs Checked the repository issues and pull requests; there are currently no open or linked pull requests or issues addressing mutex reentrancy soundness.
2 parents 5fcbb8e + a8901a0 commit 6823b69

1 file changed

Lines changed: 24 additions & 5 deletions

File tree

‎zephyr/src/sync/mutex.rs‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use core::{
1212
ops::{Deref, DerefMut},
1313
};
1414

15+
use crate::sync::atomic::{AtomicBool, Ordering};
1516
use crate::sys::sync as sys;
1617
use crate::time::{Forever, NoWait};
1718

@@ -46,6 +47,7 @@ pub enum TryLockError {
4647
pub struct Mutex<T: ?Sized> {
4748
inner: sys::Mutex,
4849
// poison: ...
50+
locked: AtomicBool,
4951
data: UnsafeCell<T>,
5052
}
5153

@@ -93,6 +95,7 @@ impl<T> Mutex<T> {
9395
pub const fn new_from(t: T, raw_mutex: sys::Mutex) -> Mutex<T> {
9496
Mutex {
9597
inner: raw_mutex,
98+
locked: AtomicBool::new(false),
9699
data: UnsafeCell::new(t),
97100
}
98101
}
@@ -111,12 +114,17 @@ impl<T: ?Sized> Mutex<T> {
111114
/// allow scoped unlock of the lock. When the guard goes out of scope, the mutex will be
112115
/// unlocked.
113116
///
114-
/// In `std`, an attempt to lock a mutex by a thread that already holds the mutex is
115-
/// unspecified. Zephyr explicitly supports this behavior, by simply incrementing a lock
116-
/// count.
117+
/// Attempting to lock a mutex that is already held by the current thread is not permitted and
118+
/// will panic. Because [`MutexGuard`] provides mutable access to the protected data via
119+
/// [`DerefMut`], allowing reentrancy would produce multiple mutable references to the same
120+
/// data, which is undefined behavior in Rust.
117121
pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
118122
// With `Forever`, should never return an error.
119123
self.inner.lock(Forever).unwrap();
124+
if self.locked.swap(true, Ordering::Acquire) {
125+
self.inner.unlock().unwrap();
126+
panic!("cannot recursively lock Mutex");
127+
}
120128
unsafe { Ok(MutexGuard::new(self)) }
121129
}
122130

@@ -125,10 +133,18 @@ impl<T: ?Sized> Mutex<T> {
125133
/// If the lock could not be acquired at this time, then [`Err`] is returned. Otherwise, an RAII
126134
/// guard is returned. The lock will be unlocked when the guard is dropped.
127135
///
128-
/// This function does not block.
136+
/// This function does not block. Attempting to lock a mutex that is already held by the current
137+
/// thread will return `Err(TryLockError::WouldBlock)`.
129138
pub fn try_lock(&self) -> TryLockResult<MutexGuard<'_, T>> {
130139
match self.inner.lock(NoWait) {
131-
Ok(()) => unsafe { Ok(MutexGuard::new(self)) },
140+
Ok(()) => {
141+
if self.locked.swap(true, Ordering::Acquire) {
142+
self.inner.unlock().unwrap();
143+
Err(TryLockError::WouldBlock)
144+
} else {
145+
unsafe { Ok(MutexGuard::new(self)) }
146+
}
147+
}
132148
// TODO: It might be better to distinguish these errors, and only return the WouldBlock
133149
// if that is the corresponding error. But, the lock shouldn't fail in Zephyr.
134150
Err(_) => Err(TryLockError::WouldBlock),
@@ -163,6 +179,7 @@ impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
163179
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
164180
#[inline]
165181
fn drop(&mut self) {
182+
self.lock.locked.store(false, Ordering::Release);
166183
if let Err(e) = self.lock.inner.unlock() {
167184
panic!("Problem unlocking MutexGuard in drop: {:?}", e);
168185
}
@@ -210,7 +227,9 @@ impl Condvar {
210227
/// have a boolean predicate associated with them, and the predicate must always be checked
211228
/// each time this function returns to protect against spurious wakeups.
212229
pub fn wait<'a, T>(&self, guard: MutexGuard<'a, T>) -> LockResult<MutexGuard<'a, T>> {
230+
guard.lock.locked.store(false, Ordering::Release);
213231
self.inner.wait(&guard.lock.inner);
232+
guard.lock.locked.store(true, Ordering::Release);
214233
Ok(guard)
215234
}
216235

0 commit comments

Comments
 (0)