Safeguard
Application Security•

CWE-765 Multiple Unlocks of a Critical Resource Explained

Unlocking a mutex or semaphore twice breaks the guarantee it provides. See how double unlocks happen, what they cause, and how to structure code to avoid them.

Safeguard Security Team
3 min read

CWE-765 is the mirror of its sibling CWE-764. The product unlocks a critical resource more times than intended, which leaves the system in an unexpected state.

Concurrent programs in any language can have it, and it often hides in error handling.

How it happens

A lock protects a shared resource only while its count stays honest. An extra unlock breaks that count. MITRE explains that with semaphores, extra calls to unlock raise the number of available resources, which likely results in a crash or unpredictable behavior when the system nears capacity.

The typical cause is a failure branch that unlocks, followed by a cleanup block that unlocks again.

// Vulnerable: error path and cleanup both unlock
pthread_mutex_lock(&m);
if (read_data() < 0) {
    pthread_mutex_unlock(&m);
    goto cleanup;
}
cleanup:
pthread_mutex_unlock(&m);

The record cites CVE-2009-0935, where an attacker supplied an invalid address to a memory reading function, causing a mutex to be unlocked twice. Notice that the input was ordinary data. The attacker only had to trigger the error path.

What an attacker gains

Availability and integrity. The consequences list a crash, exit or restart, memory modification, and an unexpected state. Two threads inside a section that should admit one can corrupt shared data.

How to find it

MITRE says static analysis can find some instances. Error injection testing is the better habit here: force each failure branch and confirm that lock state stays correct. Look for double unlock errors from your threading library as well.

How to fix it

Make sure every control path that locks a resource corresponds to exactly as many unlocks. Release in exactly one place per lock, which is easier if the unlock sits in a single cleanup block that every path reaches, or in a scoped guard that releases when the scope ends.

// Fix: one unlock, reached by every path
pthread_mutex_lock(&m);
if (read_data() < 0) goto cleanup;
cleanup:
pthread_mutex_unlock(&m);

What goes wrong after the extra unlock

The damage depends on the lock type, which is why MITRE lists such a broad set of consequences. Unlocking a mutex that is not held may be reported as an error, ignored, or, on some implementations, may let a second thread in while the first is still working. Two threads then update shared state at once, and the data can be corrupted in ways that are hard to reproduce.

With semaphores the failure is slower. Each extra release raises the count of available units above the real capacity. Nothing breaks until load approaches the true limit, and then more work is admitted than the resource can support, which shows up as crashes or erratic behavior under pressure.

That delay is why this weakness survives testing. Functional tests pass, and the fault appears only under load or after a specific error. Inject the error paths directly in tests, and add assertions on lock ownership where your threading library allows it. In code review, count the unlocks on every path of every function that locks, including the ones that jump to cleanup.

Quick check

Why is an error path the usual culprit?

Error handling often adds its own cleanup and then falls into the normal cleanup, so the same unlock runs twice.

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.