Something went wrong. Try again.
🦀🔐 Statically prevent deadlocks crates.io/crates/surelock
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360//! Factory for distributing [`KeyVoucher`]s//! across execution contexts.//!//! A [`Locksmith`] is a program-wide singleton created once (typically//! during system init). It issues//! [`KeyVoucher`]s that can be sent to//! other threads or cores. Each voucher is redeemed for a//! [`KeyHandle`](crate::key_handle::KeyHandle) on the target context.//!//! `Locksmith` is `!Send` and `!Sync` -- it stays on the creating//! thread/core. Only the vouchers travel.//!//! Only one `Locksmith` can exist at a time (enforced via a global//! `AtomicBool`). Creating a second returns `None`.//!//! # Examples//!//! ```rust//! use surelock::locksmith::Locksmith;//!//! let smith = Locksmith::new(4).unwrap(); // 4-core system//!//! let _v0 = smith.issue().unwrap();//! let _v1 = smith.issue().unwrap();//! let _v2 = smith.issue().unwrap();//! let _v3 = smith.issue().unwrap();//! assert!(smith.issue().is_err()); // limit reached//! ```
pub mod error;
use core::marker::PhantomData;
use crate::atomic::{AtomicBool, AtomicUsize, Ordering};
use crate::key_voucher::KeyVoucher;
/// Global singleton flag. Only one `Locksmith` can exist at a time.static LOCKSMITH_EXISTS: AtomicBool = AtomicBool::new(false);
/// Factory for distributing lock scope capabilities across threads/// or cores.////// Program-wide singleton -- only one `Locksmith` can exist at a/// time. Created via [`new`](Locksmith::new) (with a limit) or/// [`unlimited`](Locksmith::unlimited) (no limit). Issues/// [`KeyVoucher`]s that can be sent/// to target threads/cores and redeemed for/// [`KeyHandle`](crate::key_handle::KeyHandle)s.////// `!Send`, `!Sync` -- stays on the creating thread/core./// Only the vouchers travel.////// # Caveats////// The singleton invariant relies on a global `AtomicBool`. On Unix,/// `fork()` duplicates the parent's memory -- including the flag --/// into the child process. The child inherits `LOCKSMITH_EXISTS =/// true` but has no `Locksmith` value, so it can never create one/// and the flag is never cleared. Avoid `fork()` after creating a/// `Locksmith`, or use `exec` immediately (the standard `fork`/`exec`/// pattern resets the address space).pub struct Locksmith { issued: AtomicUsize, limit: Option<usize>, _not_send: PhantomData<*const ()>, // !Send, !Sync}
impl Locksmith { /// Try to create a new `Locksmith` with the given voucher limit. /// /// Returns an error if a `Locksmith` already exists. Only one /// can exist at a time (enforced via a global `AtomicBool`). /// When the `Locksmith` is dropped, the slot is released. /// /// # Errors /// /// Returns [`error::exists::LocksmithExistsError::AlreadyExists`] /// if another `Locksmith` is currently live. pub fn new(limit: usize) -> Result<Self, error::exists::LocksmithExistsError> { LOCKSMITH_EXISTS .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .map(|_| Self { issued: AtomicUsize::new(0), limit: Some(limit), _not_send: PhantomData, }) .map_err(|_| error::exists::LocksmithExistsError::AlreadyExists) }
/// Try to create a `Locksmith` with no voucher limit. /// /// Returns an error if a `Locksmith` already exists. /// `issue()` will always succeed (no limit check). /// /// # Errors /// /// Returns [`error::exists::LocksmithExistsError::AlreadyExists`] /// if another `Locksmith` is currently live. pub fn unlimited() -> Result<Self, error::exists::LocksmithExistsError> { LOCKSMITH_EXISTS .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .map(|_| Self { issued: AtomicUsize::new(0), limit: None, _not_send: PhantomData, }) .map_err(|_| error::exists::LocksmithExistsError::AlreadyExists) }
/// Create a `Locksmith` with the given limit, panicking if one /// already exists. /// /// Convenience wrapper over [`new`](Locksmith::new) for the common /// "construct once at init" case where a pre-existing `Locksmith` /// would be a programming error rather than a recoverable /// condition. /// /// # Panics /// /// Panics if a `Locksmith` already exists. /// /// # Examples /// /// ```rust /// use surelock::locksmith::Locksmith; /// /// let smith = Locksmith::create(4); // 4-core system /// assert_eq!(smith.limit(), Some(4)); /// ``` #[must_use] #[allow(clippy::expect_used)] pub fn create(limit: usize) -> Self { Self::new(limit).expect("surelock: a Locksmith already exists") }
/// Create an unlimited `Locksmith`, panicking if one already /// exists. /// /// Convenience wrapper over [`unlimited`](Locksmith::unlimited); /// see [`create`](Locksmith::create) for the rationale. /// /// # Panics /// /// Panics if a `Locksmith` already exists. /// /// # Examples /// /// ```rust /// use surelock::locksmith::Locksmith; /// /// let smith = Locksmith::create_unlimited(); /// assert_eq!(smith.limit(), None); /// ``` #[must_use] #[allow(clippy::expect_used)] pub fn create_unlimited() -> Self { Self::unlimited().expect("surelock: a Locksmith already exists") }
/// Issue a [`KeyVoucher`]. /// /// Returns an error if the limit has been reached. If the /// `Locksmith` is unlimited, always returns `Ok`. /// /// The voucher is `Send` and can be transferred to another /// thread or core. /// /// The counter only increments -- slots are not reclaimed when /// vouchers or key handles are dropped. For embedded systems with /// static core counts, issue exactly N vouchers at init. /// /// # Errors /// /// Returns [`error::issue::IssueError::LimitReached`] when a finite /// limit has been exhausted. pub fn issue(&self) -> Result<KeyVoucher, error::issue::IssueError> { match self.limit { None => { self.issued.fetch_add(1, Ordering::Relaxed); Ok(KeyVoucher::new_internal()) } Some(limit) => self .issued .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| { if n < limit { Some(n + 1) } else { None } }) .map(|_| KeyVoucher::new_internal()) .map_err(|_| error::issue::IssueError::LimitReached), } }
/// Number of vouchers issued so far. /// /// This counter only ever increments -- dropping a [`KeyVoucher`] /// does not decrease it (see [`remaining`](Locksmith::remaining)). /// /// # Examples /// /// ```rust /// use surelock::locksmith::Locksmith; /// /// let smith = Locksmith::new(3).unwrap(); /// assert_eq!(smith.issued(), 0); /// let _v = smith.issue().unwrap(); /// assert_eq!(smith.issued(), 1); /// ``` #[must_use] pub fn issued(&self) -> usize { self.issued.load(Ordering::Relaxed) }
/// The configured limit, or `None` if unlimited. /// /// # Examples /// /// ```rust /// use surelock::locksmith::Locksmith; /// /// assert_eq!(Locksmith::new(4).unwrap().limit(), Some(4)); /// ``` #[must_use] pub const fn limit(&self) -> Option<usize> { self.limit }
/// Remaining vouchers that can be issued, or `None` if unlimited. /// /// Because the issuance counter only increments, **dropping a /// voucher does not free a slot** -- `remaining` reflects the /// number *issued*, not the number currently live. /// /// # Examples /// /// ```rust /// use surelock::locksmith::Locksmith; /// /// let smith = Locksmith::new(2).unwrap(); /// assert_eq!(smith.remaining(), Some(2)); /// /// let voucher = smith.issue().unwrap(); /// assert_eq!(smith.remaining(), Some(1)); /// /// // Dropping the voucher does NOT restore the slot. /// drop(voucher); /// assert_eq!(smith.remaining(), Some(1)); /// /// // An unlimited Locksmith has no finite remaining count. /// drop(smith); /// assert_eq!(Locksmith::unlimited().unwrap().remaining(), None); /// ``` #[must_use] pub fn remaining(&self) -> Option<usize> { self.limit.map(|l| l.saturating_sub(self.issued())) }}
impl Drop for Locksmith { fn drop(&mut self) { LOCKSMITH_EXISTS.store(false, Ordering::Release); }}
/// Test-only seam: clear the global singleton flag without going/// through `Drop`. Needed to reset state after a *leak* test (which/// deliberately `mem::forget`s a `Locksmith`, leaving the flag set)./// `pub(crate)`, gated to `std` test builds; no public-API exposure.#[cfg(all(test, feature = "std"))]pub(crate) fn reset_singleton_for_test() { LOCKSMITH_EXISTS.store(false, Ordering::Release);}
impl core::fmt::Debug for Locksmith { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("Locksmith") .field("issued", &self.issued()) .field("limit", &self.limit) .finish() }}
#[cfg(all(test, feature = "std"))]#[allow(clippy::unwrap_used, clippy::expect_used)]mod tests { use std::{ mem, sync::{Mutex, MutexGuard, PoisonError}, };
use super::*;
// `LOCKSMITH_EXISTS` is global; these tests mutate it and must not // interleave. (Integration-test `Locksmith` cases use `#[serial]`, // but that crate's serialization is independent of this binary's, // so we use a local guard here.) static SINGLETON_GUARD: Mutex<()> = Mutex::new(());
fn guard() -> MutexGuard<'static, ()> { SINGLETON_GUARD .lock() .unwrap_or_else(PoisonError::into_inner) }
/// Characterization test for the documented leak hazard: leaking a /// `Locksmith` (here via `mem::forget`) never clears the singleton /// flag, so no further `Locksmith` can be created. Pins the current /// (footgun) contract so a silent change is caught. We restore the /// flag via the test-only seam so sibling tests are unaffected. #[test] fn leaking_locksmith_blocks_future_creation() { let _g = guard(); reset_singleton_for_test(); // ensure a clean slate
let smith = Locksmith::new(1).expect("first locksmith"); mem::forget(smith); // Drop never runs; flag stays set
assert!( matches!( Locksmith::new(1), Err(error::exists::LocksmithExistsError::AlreadyExists) ), "a leaked Locksmith must block future creation (documented hazard)" );
// Clean up the leaked flag so we don't poison other tests. reset_singleton_for_test(); assert!( Locksmith::new(1).is_ok(), "after reset, creation should succeed again" ); reset_singleton_for_test(); }
/// The issuance counter only increments: dropping a `KeyVoucher` /// does not reclaim a slot. Pins the documented non-reclamation /// invariant. #[test] fn dropping_voucher_does_not_reclaim_slot() { let _g = guard(); reset_singleton_for_test();
let smith = Locksmith::new(2).expect("locksmith"); assert_eq!(smith.remaining(), Some(2));
{ let _voucher = smith.issue().expect("first voucher"); assert_eq!(smith.remaining(), Some(1)); } // `_voucher` falls out of scope here (KeyVoucher has no Drop)
// The slot must NOT be reclaimed by the voucher going away. assert_eq!( smith.remaining(), Some(1), "a voucher leaving scope must not reclaim an issuance slot" );
reset_singleton_for_test(); }}