VNX-832: Unlock of a Resource that is not Locked
Reports a lock release that runs on a path where the lock may not be held: Java unlock() after a discarded tryLock(), in a finally whose try acquires with lockInterruptibly()/tryLock(), or of a lock the file never acquires; Python release() after a discarded non-blocking acquire(); pthread unlock after a discarded trylock; C++ std::mutex unlock() after a discarded try_lock().
| Field | Value |
|---|---|
| Rule ID | VNX-832 |
| Severity | Low |
| SARIF level | note |
| Kind | sast |
| Languages | Java, Python, C, C++ |
| CWE | CWE-832 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:L/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L |
| Tags | unlock-not-locked cwe-832 concurrency locking |
| Source | internal/sast/rules/vnx-832.rego |
Overview
VNX-832 maps to CWE-832: Unlock of a Resource that is not Locked. It reports the release line in these shapes:
- Java (
java.util.concurrentlocks, includingrw.readLock()/writeLock()):unlock()of a lock whosetryLock()/tryLock(timeout)result was discarded as a bare statement in the 40 lines above;unlock()in afinallyblock whosetryacquires the same lock withlockInterruptibly()ortryLock(), so an interrupt, a failedtryLockor the early return after it still reaches the unlock;unlock()of a lock field or variable the file declares but never acquires and never passes to other code.
- Python (
threading):release()after a bareacquire(blocking=False),acquire(False)oracquire(timeout=...)whose result was discarded. - C/C++ (pthreads):
pthread_mutex_unlock/pthread_rwlock_unlockafter a barepthread_mutex_trylock,pthread_mutex_timedlockorpthread_rwlock_try*lock/timed*lockwhose return value was discarded. C++:m.unlock()after a barem.try_lock(),try_lock_for()ortry_lock_until()whose result was discarded.
Not reported: lock() before try with unlock() in finally; if (lock.tryLock()) { try ... finally unlock }; an unlock guarded by an if on the same or the preceding line (if (acquired), isHeldByCurrentThread()); a tryLock/acquire whose result is assigned or tested; blocking acquisition; begin/end method pairs in the same file; lock parameters, locks handed to another object or returned from a getter; comments and string literals. Double unlocks are VNX-765.
Why This Matters
Releasing a lock the thread does not hold throws IllegalMonitorStateException in Java (often from a finally, where it replaces the original exception), raises RuntimeError in Python, and is undefined behaviour for a default pthread mutex. Worse, the code between the failed acquisition and the release ran without the lock, so the data it protects was modified concurrently.
What Gets Flagged
lock.tryLock();
try {
reload();
} finally {
lock.unlock(); // FLAGGED: tryLock() result ignored
}
try {
lock.lockInterruptibly();
data.put(k, v);
} finally {
lock.unlock(); // FLAGGED: interrupt before acquisition still unlocks
}
self.lock.acquire(blocking=False)
try:
self._tick()
finally:
self.lock.release() # FLAGGED
Remediation
- Acquire before
try:lock.lock(); try { ... } finally { lock.unlock(); }. - Test the result of non-blocking or timed acquisition and release only when it succeeded:
if (lock.tryLock()) { try { ... } finally { lock.unlock(); } }. - In Python prefer
with lock:; in C checkpthread_mutex_trylock(&m) == 0before unlocking.
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-832
// nosec vnx-832
Or record an ignore rule: vulnetix ignore add --rule vnx-832 --reason "...". See Suppressing findings.