lockorder: Detect lock-order bugs in GoSkip to content
Blog
Concurrency and synchronization

lockorder

Checks contradictory mutex acquisition order and unreleased return paths.

Tiercore
Kindsdefect, hazard

What it detects

Acquire locks consistently and release them on every return path. A missing unlock is reported only when another path demonstrates that the function owns the corresponding release policy.

The full list of precision boundaries, with the fixtures that pin each one, is in the lockorder design notes.

Checks

Check Tier Kind What it detects
missing-release
core defect Reports return paths that leave an owned lock held.
recursive-acquire
core defect Reports attempts to acquire a lock that is already held.
contradictory-order
core hazard Reports bounded cycles in mutex acquisition order, with acquisition and helper-call evidence.
read-lock-write
core hazard Reports writes to an object while only its read lock is held.

Why this is flagged

Two goroutines that acquire the same locks in different orders can wait on each other forever. Failing to unlock on one return path can similarly block every later caller that needs the lock.

A read lock is shared, so writing to the object it protects while holding only the read lock races with every other reader.

Locks are compared by declaration, such as a field of a type or a package variable, and an ordering cycle is reported only with an acquisition witness for each edge.

How to fix it

Choose one order for acquiring multiple locks and use it everywhere. Arrange the matching unlock as soon as each lock is acquired, commonly with defer, so early returns cannot leave it held.

Examples

Flagged code

var first sync.Mutex
var second sync.Mutex
func forward() {
first.Lock()
defer first.Unlock()
second.Lock()
defer second.Unlock()
}
func reverse() {
second.Lock()
defer second.Unlock()
// gohawk: contradictory lock order: first and second
first.Lock()
defer first.Unlock()
}

Accepted code

var third sync.Mutex
var fourth sync.Mutex
func forwardSafely() {
third.Lock()
defer third.Unlock()
fourth.Lock()
defer fourth.Unlock()
}
func reverseSafely() {
third.Lock()
defer third.Unlock()
fourth.Lock()
defer fourth.Unlock()
}