CWE-667 is the failure to properly acquire or release a lock on a resource, which leads to unexpected changes in resource state and behavior. It is one of the most common ways concurrent code goes wrong, and it affects operating system kernels, servers, databases and any application that shares data between threads or processes.
How it happens
Locking is a form of synchronization. It makes sure independently running threads or processes do not interfere with each other when they touch the same resource. MITRE stresses that all of them are expected to follow the same steps. If the steps are not followed precisely, or if no locking is done at all, another thread can change the shared resource in a way the first one cannot see or predict. The result can be data or memory corruption, denial of service and similar problems.
The failure patterns fall into a few groups.
No lock where one is needed. Two threads update a shared structure with no coordination.
Inconsistent discipline. One code path takes the lock and another path touches the same data without it. Or two paths take two locks in opposite orders, which sets up a deadlock.
A lock never released. An error path returns early and skips the unlock, so every later caller waits forever.
A lock released when not held, or released twice. This corrupts the lock's internal state.
# Vulnerable: the early return leaves the lock held
lock = threading.Lock()
def update(item):
lock.acquire()
if not item.valid:
return # lock is never released
shared.append(item)
lock.release()
The record lists four CVEs that show how this plays out in real systems:
- CVE-2021-1782: a chain where improper locking leads to a race condition (CWE-362), exploited in the wild per CISA KEV.
- CVE-2009-0935: an attacker provides an invalid address to a memory-reading function, causing a mutex to be unlocked twice.
- CVE-2010-4210: a function in an OS kernel unlocks a mutex that was not previously locked, causing a panic or an overwrite of arbitrary memory.
- CVE-2008-4302: a chain where an OS kernel does not properly handle the failure of a function call (CWE-755), leading to an unlock of a resource that was not locked (CWE-832), with a resulting crash.
Two of the four share a theme. An error or odd input sends the code down a path where the lock bookkeeping no longer matches reality.
What an attacker gains
The consequence MITRE lists is availability: resource consumption, specifically CPU, because inconsistent locking discipline can lead to deadlock. The CVEs above show worse outcomes too, such as a kernel panic and an overwrite of arbitrary memory. The parent entry, improper synchronization, lists data modification and reads as possible results.
How to find it
The record lists automated static analysis (SAST), which can find some instances by analyzing source code or compiled code without executing it. These tools are good at spotting unlock without lock and paths that exit while holding a lock. Layer on these checks.
- List every lock in the codebase and write down what data it protects.
- For each lock, check every path between acquire and release, especially error paths and exceptions.
- Record the order in which locks are taken, and look for any pair taken in both orders.
- Review how failures of called functions are handled before an unlock.
- Stress test with many threads, and use deadlock and race detectors if your toolchain has them.
How to fix it
MITRE's mitigation is to use industry standard APIs to implement locking. Do not build your own. In plain steps:
- Use the lock types from your language or platform library.
- Tie release to scope so it happens on every exit path.
- Take multiple locks in one agreed global order.
- Keep the code inside a lock short, and avoid calling unknown code while holding one.
- Check return values from lock operations and from the calls inside the critical section.
- Where possible, remove the shared state instead of guarding it.
# Safer: the context manager releases on every path
def update(item):
with lock:
if not item.valid:
return
shared.append(item)
In C, the same idea applies: structure the function with one exit point where the unlock happens, so an early error cannot skip it.
Quick check
How can two correct-looking locks still deadlock?
If one code path takes lock A then B and another takes B then A, each can end up waiting for the other. Agree on one order.
Why is an unlock of a lock that was never held dangerous?
It corrupts the lock's state. The record's kernel example caused a panic or an overwrite of arbitrary memory.
What does MITRE recommend for implementing locks?
Use industry standard APIs rather than a custom mechanism.