Rust compile+runtime deadlock prevention
0

Configure Feed

Select the types of activity you want to include in your feed.

Add README; remove stale doc comment reference

Jordan Isaacs (May 17, 2026, 1:52 AM UTC) fa52ff12 b96f41e7

+166 -2
+165
README.md
··· 1 + # lock-ordering 2 + 3 + Compile-time deadlock prevention for Rust. Declare a partial order over your 4 + locks and the type system refuses to compile any acquisition that violates it. 5 + 6 + ## Motivation 7 + 8 + Deadlock is one of the few classes of bug Rust does not catch. Two threads each 9 + hold a lock the other needs, neither can make progress, and the program hangs 10 + in production at 3am. 11 + 12 + The standard fix is to pick a global acquisition order and enforce it by code 13 + review — fragile, easy to regress, and invisible at the call site. This crate 14 + turns that convention into a type-level invariant: every lock is tagged with a 15 + *level*, the levels form a DAG, and acquiring a lock consumes a proof token 16 + that records your current position in the DAG. Acquiring out of order is a type 17 + error, not a runtime hang. 18 + 19 + A complementary runtime mode handles the case the type system cannot — dynamic 20 + collections like `HashMap<String, Mutex<T>>` where the acquisition order is 21 + known only at runtime. There, ascending keys are checked on each acquisition. 22 + 23 + ## Features 24 + 25 + ### Compile-time lock levels 26 + 27 + Declare the lock hierarchy as a DAG. `A -> B` means: while holding `A`, you may 28 + acquire `B`. Both `A` and `B` can also be the first lock acquired. 29 + 30 + ```rust 31 + use lock_ordering::{define_lock_levels, LockedAt, LockLevel, MutualExclusion}; 32 + use lock_ordering::lock::MutexLockLevel; 33 + use std::sync::Mutex; 34 + 35 + define_lock_levels! { 36 + ConfigCache -> MetricsCounter, 37 + } 38 + 39 + impl LockLevel for ConfigCache { type Method = MutualExclusion; } 40 + impl MutexLockLevel for ConfigCache { type Mutex = Mutex<String>; } 41 + 42 + impl LockLevel for MetricsCounter { type Method = MutualExclusion; } 43 + impl MutexLockLevel for MetricsCounter { type Mutex = Mutex<u64>; } 44 + 45 + let config = Mutex::new(String::from("v1")); 46 + let counter = Mutex::new(0u64); 47 + 48 + // SAFETY: no other LockedAt live on this thread. 49 + let locked = unsafe { LockedAt::new() }; 50 + 51 + let guard_cfg = locked.with_lock::<ConfigCache>(&config).unwrap(); 52 + let locked_cfg = LockedAt::from_guard(&guard_cfg); 53 + let mut guard_ctr = locked_cfg.with_lock::<MetricsCounter>(&counter).unwrap(); 54 + *guard_ctr += 1; 55 + 56 + let _ = guard_ctr.release(); 57 + let _ = guard_cfg.release(); 58 + ``` 59 + 60 + Acquiring `MetricsCounter` *before* `ConfigCache` is rejected at compile time — 61 + the required `LockBefore` bound is not satisfied. 62 + 63 + ### Strict ordering with `=>` 64 + 65 + `A -> B` makes `B` acquirable both standalone and after `A`. Use `=>` when a 66 + level must only ever be acquired underneath a specific parent: 67 + 68 + ```rust 69 + define_lock_levels! { 70 + Session -> Transaction, // both are roots 71 + Transaction => Row, // Row REQUIRES Transaction — never a root 72 + } 73 + ``` 74 + 75 + Acquiring `Row` as the first lock in a thread is a compile error. 76 + 77 + ### Cycle detection in the macro 78 + 79 + ```rust 80 + define_lock_levels! { 81 + A -> B, 82 + B -> A, 83 + } 84 + // error: cycle detected: A -> B -> A 85 + ``` 86 + 87 + ### RwLock support 88 + 89 + ```rust 90 + use lock_ordering::{LockLevel, ReadWrite}; 91 + use lock_ordering::lock::RwLockLevel; 92 + 93 + impl LockLevel for CacheIndex { type Method = ReadWrite; } 94 + impl RwLockLevel for CacheIndex { type RwLock = std::sync::RwLock<Vec<u32>>; } 95 + 96 + let read_guard = locked.with_read_lock::<CacheIndex>(&rwlock).unwrap(); 97 + let write_guard = locked.with_write_lock::<CacheIndex>(&rwlock).unwrap(); 98 + ``` 99 + 100 + ### Async (tokio) 101 + 102 + Enable the `async` feature. `wait_for_lock` / `wait_for_read` / `wait_for_write` 103 + mirror their sync counterparts and yield instead of blocking. 104 + 105 + ```rust 106 + let guard_a = locked.wait_for_lock::<AsyncLockA>(&mutex_a).await; 107 + let locked_a = LockedAt::from_guard(&guard_a); 108 + let guard_b = locked_a.wait_for_lock::<AsyncLockB>(&mutex_b).await; 109 + ``` 110 + 111 + ### Runtime ordered acquisition 112 + 113 + For `HashMap<K, Mutex<V>>` and similar collections, the levels are all the 114 + same — the order has to come from the keys at runtime. `OrderedLock` pairs a 115 + lock with an ordering key, and `OrderedAt` enforces strictly-ascending keys at 116 + each acquisition: 117 + 118 + ```rust 119 + use lock_ordering::ordered::{OrderedAt, OrderedLock}; 120 + 121 + let apple = OrderedLock { key: "apple", lock: Mutex::new(1u32) }; 122 + let banana = OrderedLock { key: "banana", lock: Mutex::new(2u32) }; 123 + let cherry = OrderedLock { key: "cherry", lock: Mutex::new(3u32) }; 124 + 125 + let locked = unsafe { LockedAt::new() }; 126 + let g_a = locked.with_ordered_lock::<MapLevel, _>(&apple).unwrap(); 127 + 128 + let ord_a = OrderedAt::from_guard(&g_a); 129 + let g_b = ord_a.with_lock::<MapLevel>(&banana).unwrap(); 130 + 131 + let ord_b = OrderedAt::from_guard(&g_b); 132 + let g_c = ord_b.with_lock::<MapLevel>(&cherry).unwrap(); 133 + // Acquiring a key <= the previous one panics. 134 + ``` 135 + 136 + Opt in to same-level chaining with `impl LockAfter<MapLevel> for MapLevel {}`, 137 + or leave it out to disallow it. `OrderedAt::into_locked_at()` exits ordered 138 + mode and recovers the compile-time token. 139 + 140 + ### `mem::forget` soundness 141 + 142 + `LockedAt::from_guard(&guard)` borrows the guard for the lifetime of the 143 + derived token, so the borrow checker rejects `mem::forget(guard)` while a 144 + child token is live. This closes the soundness gap described in 145 + [akonradi/lock-ordering#2](https://github.com/akonradi/lock-ordering/issues/2). 146 + 147 + ## Acknowledgements 148 + 149 + The core design — typed lock levels, `LockedAt`, and the `LockAfter` / 150 + `LockBefore` relation — is adapted from 151 + [`akonradi/lock-ordering`](https://github.com/akonradi/lock-ordering) by Alex 152 + Bakon (MIT). This crate diverges in three ways: 153 + 154 + - `LockedAt::new` is `unsafe`, making the "one token per thread" invariant an 155 + explicit obligation on the caller rather than an implicit assumption. 156 + - `with_lock` consumes the token by value and a new one is recovered through 157 + `from_guard`, which borrows the guard — preventing the `mem::forget` 158 + unsoundness in the original. The borrow-the-guard approach is inspired by 159 + the discussion in 160 + [akonradi/lock-ordering#2](https://github.com/akonradi/lock-ordering/issues/2). 161 + - A `define_lock_levels!` proc-macro generates the full transitive `LockAfter` 162 + closure and runs cycle detection, replacing the manual 163 + `impl_transitive_lock_order!` declarative macro. 164 + 165 + The runtime ordered acquisition system (`OrderedLock` / `OrderedAt`) is new.
+1 -2
lock-ordering/src/lockedat.rs
··· 8 8 //! borrows the guard — preventing `mem::forget` of the guard while the derived 9 9 //! token is live. 10 10 //! 11 - //! See [`LockedAt`] and [`LockGuard`] for the full API, and `.claude/plans/` 12 - //! for the design rationale. 11 + //! See [`LockedAt`] and [`LockGuard`] for the full API. 13 12 14 13 use core::marker::PhantomData; 15 14 use core::ops::{Deref, DerefMut};