Skip to main content

wgpu_core/lock/
ranked.rs

1//! Lock types that enforce well-ranked lock acquisition order.
2//!
3//! This module's [`Mutex`] and [`RwLock` types are instrumented to check that
4//! `wgpu-core` acquires locks according to their rank, to prevent deadlocks. To
5//! use it, put `--cfg wgpu_validate_locks` in `RUSTFLAGS`.
6//!
7//! The [`LockRank`] constants in the [`lock::rank`] module describe edges in a
8//! directed graph of lock acquisitions: each lock's rank says, if this is the most
9//! recently acquired lock that you are still holding, then these are the locks you
10//! are allowed to acquire next.
11//!
12//! As long as this graph doesn't have cycles, any number of threads can acquire
13//! locks along paths through the graph without deadlock:
14//!
15//! - Assume that if a thread is holding a lock, then it will either release it,
16//!   or block trying to acquire another one. No thread just sits on its locks
17//!   forever for unrelated reasons. If it did, then that would be a source of
18//!   deadlock "outside the system" that we can't do anything about.
19//!
20//! - This module asserts that threads acquire and release locks in a stack-like
21//!   order: a lock is dropped only when it is the *most recently acquired* lock
22//!   *still held* - call this the "youngest" lock. This stack-like ordering
23//!   isn't a Rust requirement; Rust lets you drop guards in any order you like.
24//!   This is a restriction we impose.
25//!
26//! - Consider the directed graph whose nodes are locks, and whose edges go from
27//!   each lock to its permitted followers, the locks in its [`LockRank::followers`]
28//!   set. The definition of the [`lock::rank`] module's [`LockRank`] constants
29//!   ensures that this graph has no cycles, including trivial cycles from a node to
30//!   itself.
31//!
32//! - This module then asserts that each thread attempts to acquire a lock only if
33//!   it is among its youngest lock's permitted followers. Thus, as a thread
34//!   acquires locks, it must be traversing a path through the graph along its
35//!   edges.
36//!
37//! - Because there are no cycles in the graph, whenever one thread is blocked
38//!   waiting to acquire a lock, that lock must be held by a different thread: if
39//!   you were allowed to acquire a lock you already hold, that would be a cycle in
40//!   the graph.
41//!
42//! - Furthermore, because the graph has no cycles, as we work our way from each
43//!   thread to the thread it is blocked waiting for, we must eventually reach an
44//!   end point: there must be some thread that is able to acquire its next lock, or
45//!   that is about to release a lock.
46//!
47//! Thus, the system as a whole is always able to make progress: it is free of
48//! deadlocks.
49//!
50//! Note that this validation only monitors each thread's behavior in isolation:
51//! there's only thread-local state, nothing communicated between threads. So we
52//! don't detect deadlocks, per se, only the potential to cause deadlocks. This
53//! means that the validation is conservative, but more reproducible, since it's not
54//! dependent on any particular interleaving of execution.
55//!
56//! [`lock::rank`]: crate::lock::rank
57
58use core::{cell::Cell, fmt, ops, panic::Location};
59
60use super::rank::LockRank;
61
62pub use LockState as RankData;
63
64/// A `Mutex` instrumented for deadlock prevention.
65///
66/// This is just a wrapper around a [`wgpu_sync::Mutex`], along with
67/// its rank in the `wgpu_core` lock ordering.
68///
69/// For details, see [the module documentation][self].
70pub struct Mutex<T> {
71    inner: wgpu_sync::Mutex<T>,
72    rank: LockRank,
73}
74
75/// A guard produced by locking [`Mutex`].
76///
77/// This is just a wrapper around a [`wgpu_sync::MutexGuard`], along
78/// with the state needed to track lock acquisition.
79///
80/// For details, see [the module documentation][self].
81pub struct MutexGuard<'a, T> {
82    inner: wgpu_sync::MutexGuard<'a, T>,
83    #[cfg_attr(not(miri), expect(unused))] // but `Drop` has important side effects
84    saved: LockStateGuard,
85}
86
87std::thread_local! {
88    static LOCK_STATE: Cell<LockState> = const { Cell::new(LockState::INITIAL) };
89}
90
91/// Per-thread state for the deadlock checker.
92#[derive(Debug, Copy, Clone)]
93pub struct LockState {
94    /// The last lock we acquired, and where.
95    last_acquired: Option<(LockRank, &'static Location<'static>)>,
96
97    /// The number of locks currently held.
98    ///
99    /// This is used to enforce stack-like lock acquisition and release.
100    depth: u32,
101}
102
103impl LockState {
104    const INITIAL: LockState = LockState {
105        last_acquired: None,
106        depth: 0,
107    };
108}
109
110/// A container that restores a [`LockState`] when dropped.
111///
112/// This type serves two purposes:
113///
114/// - Operations would like to be able to destructure lock guards and
115///   reassemble their pieces into new guards, but if the guard type
116///   itself implements `Drop`, we can't destructure it without unsafe
117///   code or pointless `Option`s whose state is almost always statically
118///   known.
119///
120/// - We can just implement `Drop` for this type once, and then use it in lock
121///   guards, rather than implementing `Drop` separately for each guard type.
122struct LockStateGuard(LockState);
123
124impl Drop for LockStateGuard {
125    fn drop(&mut self) {
126        release(self.0)
127    }
128}
129
130/// Check and record the acquisition of a lock with `new_rank`.
131///
132/// Check that acquiring a lock with `new_rank` is permitted at this point, and
133/// update the per-thread state accordingly.
134///
135/// Return the `LockState` that must be restored when this thread is released.
136fn acquire(new_rank: LockRank, location: &'static Location<'static>) -> LockState {
137    let result = LOCK_STATE.try_with(|cell| {
138        let state = cell.get();
139        // Initially, it's fine to acquire any lock. So we only
140        // need to check when `last_acquired` is `Some`.
141        if let Some((ref last_rank, ref last_location)) = state.last_acquired {
142            assert!(
143                last_rank.followers.contains(new_rank.bit),
144                "Attempt to acquire nested mutexes in wrong order:\n\
145                 last locked {:<35} at {}\n\
146                 now locking {:<35} at {}\n\
147                 Locking {} after locking {} is not permitted.",
148                last_rank.bit.member_name(),
149                last_location,
150                new_rank.bit.member_name(),
151                location,
152                new_rank.bit.member_name(),
153                last_rank.bit.member_name(),
154            );
155        }
156        cell.set(LockState {
157            last_acquired: Some((new_rank, location)),
158            depth: state.depth + 1,
159        });
160        state
161    });
162    result.unwrap_or(LockState::INITIAL)
163}
164
165/// Record the release of a lock whose saved state was `saved`.
166///
167/// Check that locks are being acquired in stacking order, and update the
168/// per-thread state accordingly.
169fn release(saved: LockState) {
170    let _ = LOCK_STATE.try_with(|cell| {
171        let saved_info = saved.last_acquired;
172
173        let prior = cell.replace(saved);
174
175        let (prior_rank, prior_location) = prior
176            .last_acquired
177            .expect("Releasing a lock, but no acquisition recorded");
178
179        // Although Rust allows mutex guards to be dropped in any
180        // order, this analysis requires that locks be acquired and
181        // released in stack order: the next lock to be released must be
182        // the most recently acquired lock still held.
183
184        match (saved.depth, saved_info) {
185            (saved_depth @ 0, None) => {
186                assert_eq!(
187                    prior.depth,
188                    saved_depth + 1,
189                    "Lock not released in stacking order\n\
190                released {:<35} locked at {:?}\n\
191                when not expecting any locks to be held\n",
192                    prior_rank.bit.member_name(),
193                    prior_location,
194                );
195            }
196            (0, Some(_)) => {
197                panic!("Found previous lock acquisition information, but saved.depth = 0");
198            }
199            (saved_depth, Some((saved_rank, saved_location))) => {
200                assert_eq!(
201                    prior.depth,
202                    saved_depth + 1,
203                    "Lock not released in stacking order\n\
204                expecting release of {:<35} locked at {:?}\n\
205                but instead released {:<35} locked at {:?}\n",
206                    saved_rank.bit.member_name(),
207                    saved_location,
208                    prior_rank.bit.member_name(),
209                    prior_location,
210                );
211            }
212            (saved_depth, None) => {
213                panic!(
214                "Found saved.depth = {saved_depth}, but no previous lock acquisition information"
215            );
216            }
217        }
218    });
219}
220
221impl<T> Mutex<T> {
222    pub fn new(rank: LockRank, value: T) -> Mutex<T> {
223        Mutex {
224            inner: wgpu_sync::Mutex::new(value),
225            rank,
226        }
227    }
228
229    #[track_caller]
230    pub fn lock(&self) -> MutexGuard<'_, T> {
231        let saved = acquire(self.rank, Location::caller());
232        MutexGuard {
233            inner: self.inner.lock(),
234            saved: LockStateGuard(saved),
235        }
236    }
237
238    pub fn into_inner(self) -> T {
239        self.inner.into_inner()
240    }
241}
242
243impl<'a, T> ops::Deref for MutexGuard<'a, T> {
244    type Target = T;
245
246    fn deref(&self) -> &Self::Target {
247        self.inner.deref()
248    }
249}
250
251impl<'a, T> ops::DerefMut for MutexGuard<'a, T> {
252    fn deref_mut(&mut self) -> &mut Self::Target {
253        self.inner.deref_mut()
254    }
255}
256
257impl<T: fmt::Debug> fmt::Debug for Mutex<T> {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        self.inner.fmt(f)
260    }
261}
262
263/// An `RwLock` instrumented for deadlock prevention.
264///
265/// This is just a wrapper around a [`wgpu_sync::RwLock`], along with
266/// its rank in the `wgpu_core` lock ordering.
267///
268/// For details, see [the module documentation][self].
269pub struct RwLock<T> {
270    inner: wgpu_sync::RwLock<T>,
271    rank: LockRank,
272}
273
274/// A read guard produced by locking [`RwLock`] for reading.
275///
276/// This is just a wrapper around a [`wgpu_sync::RwLockReadGuard`], along with
277/// the state needed to track lock acquisition.
278///
279/// For details, see [the module documentation][self].
280pub struct RwLockReadGuard<'a, T> {
281    inner: wgpu_sync::RwLockReadGuard<'a, T>,
282    saved: LockStateGuard,
283}
284
285/// A write guard produced by locking [`RwLock`] for writing.
286///
287/// This is just a wrapper around a [`wgpu_sync::RwLockWriteGuard`], along
288/// with the state needed to track lock acquisition.
289///
290/// For details, see [the module documentation][self].
291pub struct RwLockWriteGuard<'a, T> {
292    inner: wgpu_sync::RwLockWriteGuard<'a, T>,
293    #[cfg_attr(not(miri), expect(unused))] // but `Drop` has important side effects
294    saved: LockStateGuard,
295}
296
297impl<T> RwLock<T> {
298    pub fn new(rank: LockRank, value: T) -> RwLock<T> {
299        RwLock {
300            inner: wgpu_sync::RwLock::new(value),
301            rank,
302        }
303    }
304
305    #[track_caller]
306    pub fn read(&self) -> RwLockReadGuard<'_, T> {
307        let saved = acquire(self.rank, Location::caller());
308        RwLockReadGuard {
309            inner: self.inner.read(),
310            saved: LockStateGuard(saved),
311        }
312    }
313
314    #[track_caller]
315    pub fn write(&self) -> RwLockWriteGuard<'_, T> {
316        let saved = acquire(self.rank, Location::caller());
317        RwLockWriteGuard {
318            inner: self.inner.write(),
319            saved: LockStateGuard(saved),
320        }
321    }
322
323    /// Force an read-unlock operation on this lock.
324    ///
325    /// Safety:
326    /// - A read lock must be held which is not held by a guard.
327    pub unsafe fn force_unlock_read(&self, data: RankData) {
328        release(data);
329        unsafe { self.inner.force_unlock_read() };
330    }
331
332    /// Returns a reference to the underlying [`wgpu_sync::RwLock`].
333    ///
334    /// Marked as unsafe because it allows bypassing the rank system,
335    /// defeating the purpose of this type.
336    pub unsafe fn underlying(&self) -> &wgpu_sync::RwLock<T> {
337        &self.inner
338    }
339}
340
341impl<'a, T> RwLockReadGuard<'a, T> {
342    // Forget the read guard, leaving the lock in a locked state with no guard.
343    //
344    // Equivalent to std::mem::forget, but preserves the information about the lock
345    // rank.
346    pub fn forget(this: Self) -> RankData {
347        // Skip `Drop` for both the actual lock guard (`this.inner`) and the
348        // rank-checking state guard (`this.saved`)
349        let saved = core::mem::ManuallyDrop::new(this.saved);
350        core::mem::forget(this.inner);
351
352        // Return the `RankData` so the caller can pass it to `force_unlock_read`.
353        saved.0
354    }
355}
356
357impl<T: fmt::Debug> fmt::Debug for RwLock<T> {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        self.inner.fmt(f)
360    }
361}
362
363impl<'a, T> ops::Deref for RwLockReadGuard<'a, T> {
364    type Target = T;
365
366    fn deref(&self) -> &Self::Target {
367        self.inner.deref()
368    }
369}
370
371impl<'a, T> ops::Deref for RwLockWriteGuard<'a, T> {
372    type Target = T;
373
374    fn deref(&self) -> &Self::Target {
375        self.inner.deref()
376    }
377}
378
379impl<'a, T> ops::DerefMut for RwLockWriteGuard<'a, T> {
380    fn deref_mut(&mut self) -> &mut Self::Target {
381        self.inner.deref_mut()
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::Mutex;
388    use crate::lock::rank;
389
390    /// Locks can be acquired in the order indicated by their ranks.
391    #[test]
392    fn permitted() {
393        let lock1 = Mutex::new(rank::PAWN, ());
394        let lock2 = Mutex::new(rank::ROOK, ());
395
396        let _guard1 = lock1.lock();
397        let _guard2 = lock2.lock();
398    }
399
400    /// Locks can only be acquired in the order indicated by their ranks.
401    #[test]
402    #[should_panic(expected = "Locking pawn after locking rook")]
403    fn forbidden_unrelated() {
404        let lock1 = Mutex::new(rank::ROOK, ());
405        let lock2 = Mutex::new(rank::PAWN, ());
406
407        let _guard1 = lock1.lock();
408        let _guard2 = lock2.lock();
409    }
410
411    /// Lock acquisitions can't skip ranks.
412    ///
413    /// These two locks *could* be acquired in this order, but only if other locks
414    /// are acquired in between them. Skipping ranks isn't allowed.
415    #[test]
416    #[should_panic(expected = "Locking knight after locking pawn")]
417    fn forbidden_skip() {
418        let lock1 = Mutex::new(rank::PAWN, ());
419        let lock2 = Mutex::new(rank::KNIGHT, ());
420
421        let _guard1 = lock1.lock();
422        let _guard2 = lock2.lock();
423    }
424
425    /// Locks can be acquired and released in a stack-like order.
426    #[test]
427    fn stack_like() {
428        let lock1 = Mutex::new(rank::PAWN, ());
429        let lock2 = Mutex::new(rank::ROOK, ());
430        let lock3 = Mutex::new(rank::BISHOP, ());
431
432        let guard1 = lock1.lock();
433        let guard2 = lock2.lock();
434        drop(guard2);
435
436        let guard3 = lock3.lock();
437        drop(guard3);
438        drop(guard1);
439    }
440
441    /// Locks can only be acquired and released in a stack-like order.
442    #[test]
443    #[should_panic(expected = "Lock not released in stacking order")]
444    fn non_stack_like() {
445        let lock1 = Mutex::new(rank::PAWN, ());
446        let lock2 = Mutex::new(rank::ROOK, ());
447
448        let guard1 = lock1.lock();
449        let guard2 = lock2.lock();
450
451        // Avoid a double panic from dropping this while unwinding due to the panic
452        // we're testing for.
453        core::mem::forget(guard2);
454
455        drop(guard1);
456    }
457}