Safeguard
Application Security•

CWE-764 Multiple Locks of a Critical Resource: Deadlocks and DoS

Locking a resource more times than you unlock it can hang a thread or drain a semaphore pool. Learn the cause and the one rule that prevents it.

Safeguard Security Team
3 min read

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.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.