From 693e629b7b6444d9f42493be793f3dbb1b621417 Mon Sep 17 00:00:00 2001 From: Nick Santana Date: Wed, 25 Jan 2023 07:44:27 -0800 Subject: [PATCH] Add `RwLock` to `mc-sgx-sync` --- sync/src/lib.rs | 2 + sync/src/poison.rs | 1 + sync/src/rwlock.rs | 530 +++++++++++++++++++++++++++++++++++ sync/src/sys/locks/mod.rs | 1 + sync/src/sys/locks/rwlock.rs | 2 - 5 files changed, 534 insertions(+), 2 deletions(-) create mode 100644 sync/src/rwlock.rs diff --git a/sync/src/lib.rs b/sync/src/lib.rs index aa133ce..bf7efc6 100644 --- a/sync/src/lib.rs +++ b/sync/src/lib.rs @@ -11,4 +11,6 @@ mod poison; pub use condvar::Condvar; pub use mutex::{Mutex, MutexGuard}; pub use poison::{LockResult, PoisonError, TryLockError, TryLockResult}; +pub use rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +mod rwlock; mod sys; diff --git a/sync/src/poison.rs b/sync/src/poison.rs index 2f827ee..9654e8b 100644 --- a/sync/src/poison.rs +++ b/sync/src/poison.rs @@ -91,6 +91,7 @@ pub(crate) struct Guard { /// is held. The precise semantics for when a lock is poisoned is documented on /// each lock, but once a lock is poisoned then all future acquisitions will /// return this error. +/// /// [`Mutex`]: crate::Mutex /// [`RwLock`]: crate::RwLock pub struct PoisonError { diff --git a/sync/src/rwlock.rs b/sync/src/rwlock.rs new file mode 100644 index 0000000..45a73ed --- /dev/null +++ b/sync/src/rwlock.rs @@ -0,0 +1,530 @@ +// Copyright (c) The Rust Foundation +// Copyright (c) 2023 The MobileCoin Foundation + +//! rwlock.rs implementation more or less copied from +//! [rust source](https://github.com/rust-lang/rust.git) at +//! [606c3907](https://github.com/rust-lang/rust/commit/606c3907251397a42e23d3e60de31be9d32525d5) +//! +//! Differences: +//! - The imports were changed to work with the `mc-sgx` crates. +//! - The stable attributes have been removed +//! - The unstable attributes have been removed +//! - Removed examples that were not possible in an SGX enclave have been omitted +//! - Ran `cargo fmt` +//! - Removed unnecessary unsafe blocks + +use crate::sys::locks as sys; +use crate::{poison, LockResult, TryLockError, TryLockResult}; +use core::cell::UnsafeCell; +use core::fmt; +use core::ops::{Deref, DerefMut}; +use core::ptr::NonNull; + +/// A reader-writer lock +/// +/// This type of lock allows a number of readers or at most one writer at any +/// point in time. The write portion of this lock typically allows modification +/// of the underlying data (exclusive access) and the read portion of this lock +/// typically allows for read-only access (shared access). +/// +/// In comparison, a [`Mutex`] does not distinguish between readers or writers +/// that acquire the lock, therefore blocking any threads waiting for the lock to +/// become available. An `RwLock` will allow any number of readers to acquire the +/// lock as long as a writer is not holding the lock. +/// +/// The priority policy of the lock is dependent on the underlying operating +/// system's implementation, and this type does not guarantee that any +/// particular policy will be used. In particular, a writer which is waiting to +/// acquire the lock in `write` might or might not block concurrent calls to +/// `read`, e.g.: +/// +///
Potential deadlock example +/// +/// ```text +/// // Thread 1 | // Thread 2 +/// let _rg = lock.read(); | +/// | // will block +/// | let _wg = lock.write(); +/// // may deadlock | +/// let _rg = lock.read(); | +/// ``` +///
+/// +/// The type parameter `T` represents the data that this lock protects. It is +/// required that `T` satisfies [`Send`] to be shared across threads and +/// [`Sync`] to allow concurrent access through readers. The RAII guards +/// returned from the locking methods implement [`Deref`] (and [`DerefMut`] +/// for the `write` methods) to allow access to the content of the lock. +/// +/// # Poisoning +/// +/// An `RwLock`, like [`Mutex`], will become poisoned on a panic. Note, however, +/// that an `RwLock` may only be poisoned if a panic occurs while it is locked +/// exclusively (write mode). If a panic occurs in any reader, then the lock +/// will not be poisoned. +/// +/// # Examples +/// +/// ``` +/// use mc_sgx_sync::RwLock; +/// +/// let lock = RwLock::new(5); +/// +/// // many reader locks can be held at once +/// { +/// let r1 = lock.read().unwrap(); +/// let r2 = lock.read().unwrap(); +/// assert_eq!(*r1, 5); +/// assert_eq!(*r2, 5); +/// } // read locks are dropped at this point +/// +/// // only one write lock may be held, however +/// { +/// let mut w = lock.write().unwrap(); +/// *w += 1; +/// assert_eq!(*w, 6); +/// } // write lock is dropped here +/// ``` +/// +/// [`Mutex`]: super::Mutex +pub struct RwLock { + inner: sys::RwLock, + poison: poison::Flag, + data: UnsafeCell, +} + +unsafe impl Send for RwLock {} +unsafe impl Sync for RwLock {} + +/// RAII structure used to release the shared read access of a lock when +/// dropped. +/// +/// This structure is created by the [`read`] and [`try_read`] methods on +/// [`RwLock`]. +/// +/// [`read`]: RwLock::read +/// [`try_read`]: RwLock::try_read +#[must_use = "if unused the RwLock will immediately unlock"] +#[clippy::has_significant_drop] +pub struct RwLockReadGuard<'a, T: ?Sized + 'a> { + // NB: we use a pointer instead of `&'a T` to avoid `noalias` violations, because a + // `Ref` argument doesn't hold immutability for its whole scope, only until it drops. + // `NonNull` is also covariant over `T`, just like we would have with `&T`. `NonNull` + // is preferable over `const* T` to allow for niche optimization. + data: NonNull, + inner_lock: &'a sys::RwLock, +} + +impl !Send for RwLockReadGuard<'_, T> {} +unsafe impl Sync for RwLockReadGuard<'_, T> {} + +/// RAII structure used to release the exclusive write access of a lock when +/// dropped. +/// +/// This structure is created by the [`write`] and [`try_write`] methods +/// on [`RwLock`]. +/// +/// [`write`]: RwLock::write +/// [`try_write`]: RwLock::try_write +#[must_use = "if unused the RwLock will immediately unlock"] +#[clippy::has_significant_drop] +pub struct RwLockWriteGuard<'a, T: ?Sized + 'a> { + lock: &'a RwLock, + poison: poison::Guard, +} + +impl !Send for RwLockWriteGuard<'_, T> {} +unsafe impl Sync for RwLockWriteGuard<'_, T> {} + +impl RwLock { + /// Creates a new instance of an `RwLock` which is unlocked. + /// + /// # Examples + /// + /// ``` + /// use mc_sgx_sync::RwLock; + /// + /// let lock = RwLock::new(5); + /// ``` + pub const fn new(t: T) -> RwLock { + RwLock { + inner: sys::RwLock::new(), + poison: poison::Flag::new(), + data: UnsafeCell::new(t), + } + } +} + +impl RwLock { + /// Locks this `RwLock` with shared read access, blocking the current thread + /// until it can be acquired. + /// + /// The calling thread will be blocked until there are no more writers which + /// hold the lock. There may be other readers currently inside the lock when + /// this method returns. This method does not provide any guarantees with + /// respect to the ordering of whether contentious readers or writers will + /// acquire the lock first. + /// + /// Returns an RAII guard which will release this thread's shared access + /// once it is dropped. + /// + /// # Errors + /// + /// This function will return an error if the `RwLock` is poisoned. An + /// `RwLock` is poisoned whenever a writer panics while holding an exclusive + /// lock. The failure will occur immediately after the lock has been + /// acquired. + /// + /// # Panics + /// + /// This function might panic when called if the lock is already held by the current thread. + /// + /// # Examples + /// + /// ``` + /// use mc_sgx_sync::RwLock; + /// + /// let lock = RwLock::new(1); + /// + /// let n = lock.read().unwrap(); + /// assert_eq!(*n, 1); + /// ``` + pub fn read(&self) -> LockResult> { + unsafe { + self.inner.read(); + RwLockReadGuard::new(self) + } + } + + /// Attempts to acquire this `RwLock` with shared read access. + /// + /// If the access could not be granted at this time, then `Err` is returned. + /// Otherwise, an RAII guard is returned which will release the shared access + /// when it is dropped. + /// + /// This function does not block. + /// + /// This function does not provide any guarantees with respect to the ordering + /// of whether contentious readers or writers will acquire the lock first. + /// + /// # Errors + /// + /// This function will return the [`Poisoned`] error if the `RwLock` is + /// poisoned. An `RwLock` is poisoned whenever a writer panics while holding + /// an exclusive lock. `Poisoned` will only be returned if the lock would + /// have otherwise been acquired. + /// + /// This function will return the [`WouldBlock`] error if the `RwLock` could + /// not be acquired because it was already locked exclusively. + /// + /// [`Poisoned`]: TryLockError::Poisoned + /// [`WouldBlock`]: TryLockError::WouldBlock + /// + /// # Examples + /// + /// ``` + /// use mc_sgx_sync::RwLock; + /// + /// let lock = RwLock::new(1); + /// + /// match lock.try_read() { + /// Ok(n) => assert_eq!(*n, 1), + /// Err(_) => unreachable!(), + /// }; + /// ``` + pub fn try_read(&self) -> TryLockResult> { + unsafe { + if self.inner.try_read() { + Ok(RwLockReadGuard::new(self)?) + } else { + Err(TryLockError::WouldBlock) + } + } + } + + /// Locks this `RwLock` with exclusive write access, blocking the current + /// thread until it can be acquired. + /// + /// This function will not return while other writers or other readers + /// currently have access to the lock. + /// + /// Returns an RAII guard which will drop the write access of this `RwLock` + /// when dropped. + /// + /// # Errors + /// + /// This function will return an error if the `RwLock` is poisoned. An + /// `RwLock` is poisoned whenever a writer panics while holding an exclusive + /// lock. An error will be returned when the lock is acquired. + /// + /// # Panics + /// + /// This function might panic when called if the lock is already held by the current thread. + /// + /// # Examples + /// + /// ``` + /// use mc_sgx_sync::RwLock; + /// + /// let lock = RwLock::new(1); + /// + /// let mut n = lock.write().unwrap(); + /// *n = 2; + /// + /// assert!(lock.try_read().is_err()); + /// ``` + pub fn write(&self) -> LockResult> { + unsafe { + self.inner.write(); + RwLockWriteGuard::new(self) + } + } + + /// Attempts to lock this `RwLock` with exclusive write access. + /// + /// If the lock could not be acquired at this time, then `Err` is returned. + /// Otherwise, an RAII guard is returned which will release the lock when + /// it is dropped. + /// + /// This function does not block. + /// + /// This function does not provide any guarantees with respect to the ordering + /// of whether contentious readers or writers will acquire the lock first. + /// + /// # Errors + /// + /// This function will return the [`Poisoned`] error if the `RwLock` is + /// poisoned. An `RwLock` is poisoned whenever a writer panics while holding + /// an exclusive lock. `Poisoned` will only be returned if the lock would + /// have otherwise been acquired. + /// + /// This function will return the [`WouldBlock`] error if the `RwLock` could + /// not be acquired because it was already locked exclusively. + /// + /// [`Poisoned`]: TryLockError::Poisoned + /// [`WouldBlock`]: TryLockError::WouldBlock + /// + /// + /// # Examples + /// + /// ``` + /// use mc_sgx_sync::RwLock; + /// + /// let lock = RwLock::new(1); + /// + /// let n = lock.read().unwrap(); + /// assert_eq!(*n, 1); + /// + /// assert!(lock.try_write().is_err()); + /// ``` + pub fn try_write(&self) -> TryLockResult> { + unsafe { + if self.inner.try_write() { + Ok(RwLockWriteGuard::new(self)?) + } else { + Err(TryLockError::WouldBlock) + } + } + } + + /// Determines whether the lock is poisoned. + /// + /// If another thread is active, the lock can still become poisoned at any + /// time. You should not trust a `false` value for program correctness + /// without additional synchronization. + pub fn is_poisoned(&self) -> bool { + self.poison.get() + } + + /// Clear the poisoned state from a lock + /// + /// If the lock is poisoned, it will remain poisoned until this function is called. This allows + /// recovering from a poisoned state and marking that it has recovered. For example, if the + /// value is overwritten by a known-good value, then the mutex can be marked as un-poisoned. Or + /// possibly, the value could be inspected to determine if it is in a consistent state, and if + /// so the poison is removed. + pub fn clear_poison(&self) { + self.poison.clear(); + } + + /// Consumes this `RwLock`, returning the underlying data. + /// + /// # Errors + /// + /// This function will return an error if the `RwLock` is poisoned. An + /// `RwLock` is poisoned whenever a writer panics while holding an exclusive + /// lock. An error will only be returned if the lock would have otherwise + /// been acquired. + /// + /// # Examples + /// + /// ``` + /// use mc_sgx_sync::RwLock; + /// + /// let lock = RwLock::new(String::new()); + /// { + /// let mut s = lock.write().unwrap(); + /// *s = "modified".to_owned(); + /// } + /// assert_eq!(lock.into_inner().unwrap(), "modified"); + /// ``` + pub fn into_inner(self) -> LockResult + where + T: Sized, + { + let data = self.data.into_inner(); + poison::map_result(self.poison.borrow(), |()| data) + } + + /// Returns a mutable reference to the underlying data. + /// + /// Since this call borrows the `RwLock` mutably, no actual locking needs to + /// take place -- the mutable borrow statically guarantees no locks exist. + /// + /// # Errors + /// + /// This function will return an error if the `RwLock` is poisoned. An + /// `RwLock` is poisoned whenever a writer panics while holding an exclusive + /// lock. An error will only be returned if the lock would have otherwise + /// been acquired. + /// + /// # Examples + /// + /// ``` + /// use mc_sgx_sync::RwLock; + /// + /// let mut lock = RwLock::new(0); + /// *lock.get_mut().unwrap() = 10; + /// assert_eq!(*lock.read().unwrap(), 10); + /// ``` + pub fn get_mut(&mut self) -> LockResult<&mut T> { + let data = self.data.get_mut(); + poison::map_result(self.poison.borrow(), |()| data) + } +} + +impl fmt::Debug for RwLock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("RwLock"); + match self.try_read() { + Ok(guard) => { + d.field("data", &&*guard); + } + Err(TryLockError::Poisoned(err)) => { + d.field("data", &&**err.get_ref()); + } + Err(TryLockError::WouldBlock) => { + struct LockedPlaceholder; + impl fmt::Debug for LockedPlaceholder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("") + } + } + d.field("data", &LockedPlaceholder); + } + } + d.field("poisoned", &self.poison.get()); + d.finish_non_exhaustive() + } +} + +impl Default for RwLock { + /// Creates a new `RwLock`, with the `Default` value for T. + fn default() -> RwLock { + RwLock::new(Default::default()) + } +} + +impl From for RwLock { + /// Creates a new instance of an `RwLock` which is unlocked. + /// This is equivalent to [`RwLock::new`]. + fn from(t: T) -> Self { + RwLock::new(t) + } +} + +impl<'rwlock, T: ?Sized> RwLockReadGuard<'rwlock, T> { + /// Create a new instance of `RwLockReadGuard` from a `RwLock`. + // SAFETY: if and only if `lock.inner.read()` (or `lock.inner.try_read()`) has been + // successfully called from the same thread before instantiating this object. + unsafe fn new(lock: &'rwlock RwLock) -> LockResult> { + poison::map_result(lock.poison.borrow(), |()| RwLockReadGuard { + data: NonNull::new_unchecked(lock.data.get()), + inner_lock: &lock.inner, + }) + } +} + +impl<'rwlock, T: ?Sized> RwLockWriteGuard<'rwlock, T> { + /// Create a new instance of `RwLockWriteGuard` from a `RwLock`. + // SAFETY: if and only if `lock.inner.write()` (or `lock.inner.try_write()`) has been + // successfully called from the same thread before instantiating this object. + unsafe fn new(lock: &'rwlock RwLock) -> LockResult> { + poison::map_result(lock.poison.guard(), |guard| RwLockWriteGuard { + lock, + poison: guard, + }) + } +} + +impl fmt::Debug for RwLockReadGuard<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Display for RwLockReadGuard<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Debug for RwLockWriteGuard<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Display for RwLockWriteGuard<'_, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl Deref for RwLockReadGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + // SAFETY: the conditions of `RwLockGuard::new` were satisfied when created. + unsafe { self.data.as_ref() } + } +} + +impl Deref for RwLockWriteGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + // SAFETY: the conditions of `RwLockWriteGuard::new` were satisfied when created. + unsafe { &*self.lock.data.get() } + } +} + +impl DerefMut for RwLockWriteGuard<'_, T> { + fn deref_mut(&mut self) -> &mut T { + // SAFETY: the conditions of `RwLockWriteGuard::new` were satisfied when created. + unsafe { &mut *self.lock.data.get() } + } +} + +impl Drop for RwLockReadGuard<'_, T> { + fn drop(&mut self) { + self.inner_lock.read_unlock(); + } +} + +impl Drop for RwLockWriteGuard<'_, T> { + fn drop(&mut self) { + self.lock.poison.done(&self.poison); + self.lock.inner.write_unlock(); + } +} diff --git a/sync/src/sys/locks/mod.rs b/sync/src/sys/locks/mod.rs index 70a2028..e95b784 100644 --- a/sync/src/sys/locks/mod.rs +++ b/sync/src/sys/locks/mod.rs @@ -8,3 +8,4 @@ mod rwlock; pub(crate) use condvar::Condvar; pub(crate) use mutex::Mutex; +pub(crate) use rwlock::RwLock; diff --git a/sync/src/sys/locks/rwlock.rs b/sync/src/sys/locks/rwlock.rs index a842890..d9b42db 100644 --- a/sync/src/sys/locks/rwlock.rs +++ b/sync/src/sys/locks/rwlock.rs @@ -2,8 +2,6 @@ //! Rust RwLock implementation used in SGX environments -#![allow(dead_code)] - use mc_sgx_tstdc::RwLock as SgxRwLock; /// SGX rwlock backend to use with the common Rust std lib