CWE-764 is what happens when a product locks a critical resource more times than intended, leaving the system in an unexpected state.
It matters in concurrent code: threads, processes, anything guarded by a mutex or semaphore.
How it happens
The effect depends on the lock type. With a binary lock, a second lock attempt on an already held lock may never succeed, and progress may be impossible. With semaphores, resources are pooled, so each extra lock reduces the pool and can lead to degraded performance or a denial of service.
The usual cause is a code path that takes the lock again, for instance a retry loop that locks on every pass without unlocking, or a function that calls another which also locks.
# Vulnerable: retry loop locks again without releasing
lock = threading.Lock()
def work():
while True:
lock.acquire()
if try_step():
break
# forgot to release before retrying
MITRE notes that if an attacker can trigger this, it resembles an unrestricted lock, CWE-412.
What an attacker gains
The listed consequences are CPU resource consumption, a crash, exit or restart, and an unexpected state. The realistic attacker goal is denial of service: freeze a worker or exhaust the pool by driving the faulty path.
How to find it
Static analysis can find some instances by modeling control flow, as MITRE notes. Concurrency stress tests help too. Look for threads parked forever on a lock, and for semaphore counts that trend toward zero under load.
How to fix it
Make sure every control path that locks a resource one or more times has exactly as many unlocks. If the code takes a lock and then finds it cannot do its work, release the lock before waiting for conditions to improve, then reacquire before trying again.
# Fix: with statement guarantees one release per acquire
def work():
while True:
with lock:
if try_step():
break
Scoped constructs such as with blocks, try and finally, or RAII guards make the pairing automatic.
Recursive locking and where it goes wrong
Many double lock bugs come from structure, not from a single bad line. A public method takes a lock and calls a helper. The helper, written separately, also takes the same lock. With a plain non reentrant mutex, the thread now waits for itself, and the program hangs.
Some libraries offer reentrant locks that count acquisitions and release only when the count returns to zero. They fix the hang, but they do not remove the need for balance, because every acquire still needs its release. A path that locks twice and unlocks once leaves the lock held forever.
The cleanest design is to take the lock at the outer boundary only and have inner helpers assume it is held, documenting that expectation in their names or comments. Where that is impractical, use scoped guards so each acquisition is paired with a release by construction.
From a security view, the hung or exhausted worker is the payoff. Any request an attacker can send that reaches the faulty path costs them little and ties up a thread or a semaphore slot for good.
Quick check
What is the rule for lock and unlock counts?
Every path through the code must unlock exactly as many times as it locked, including error paths.