Skip to content

Lock order

Every thread that acquires two or more ranked locks must take them in strictly increasing rank order. Ranks are per lock class and spaced by ten, so a new lock can be slotted in without renumbering.

The authoritative table lives in doc/invariants/lock-order.md and is updated in the same commit that adds or moves a lock.

Order is only half the problem. A spin lock is bounded only while its holder keeps running, and preemption is involuntary, so a bare spin::Mutex guard can be held by a descheduled thread while every other CPU burns cycles behind it.

Primitive Use for
IrqSpinlock state an interrupt handler can reach; disables interrupts
PreemptSpinlock / PreemptRwLock everything else shared between threads
BlockingMutex / BlockingRwLock anything held across I/O or a park

Preempt* suppresses preemption rather than interrupts, which is what a lock held across real work needs: walking page tables or the VMA tree with interrupts off would charge that work to interrupt latency. Nothing reachable under a Preempt* guard may park, and thread_park, thread_sleep and thread_yield debug-assert on it.

10 VFS mount registry
30 inode.lock 35 dentry cache
40 page cache 42 in-flight 50 dirty keys
60 inode.mappers 70 VMAs 80 memory manager
90 shared memory registry
100 dirty inodes 110 block page cache shards
120 journal map 130 checkpoint tracker
140 block write lock 150 journal state
160 EFS mutable state
170 AHCI legacy 180 slot/NCQ waiters 190 MMIO 200 PCI config
204 mailbox queue 206 mailbox response
210 TTY 220 pipe 230 pty
240 net stack 250 port table 260 socket 270 connection
280 window registry 290 window events 300 mouse buttons
310 broadcasters 320 device poller lists 330 HDA playback
340 devfs registry
900 kernel mapper
910 frame allocator

Go through the macros, so the debug tracker sees the acquisition:

let g = ranked_lock!(RANK_EFS_MUTABLE, "efs::alloc_block", driver.mutable);
let g = ranked_read!(RANK_VFS, "VFS", VFS);
let g = ranked_write!(RANK_VFS, "vfs::mount", VFS);
let g = ranked_lock_same!(RANK_INODE, "vfs::rename", inode.lock);

ranked_lock_same! is for two locks of the same class, where the caller orders them by key, renaming across two directories for instance.

  • Page-cache locks (40) and block-cache shards (110) are never held across disk I/O. Drop them before calling a fill function.
  • inode.lock is released before any I/O the VFS performs on behalf of a write.
  • Same-rank locks of different classes are never co-held.
  • Some locks are deliberately unranked (owned_ops, WaitQueue.inner, the scheduler’s run queue) with the rationale written down. Do not rank them without reading it: the scheduler’s own locks are taken in short interrupt-disabled sections, and wrapping them would recurse into the preemption counter.

A guard on an unranked lock is invisible to assert_no_guards_held, which thread_exit calls. So ranking an otherwise-leaf lock is worth doing purely to make a dying thread’s held guards detectable.

The tracker counts violations at /proc/lock_order_stats. Two build features exercise it:

Terminal window
cargo build --features lock-order-self-test
cargo build --features lock-order-self-test-inversion # panics by design
make run-single # run the inversion on one core

lockordertest in /bin drives it from userspace.