Multiple Locks of a Critical Resource

Description

Multiple Locks of a Critical Resource is a concurrency vulnerability where software locks a critical resource more times than intended, leading to an unexpected state in the system. In concurrent environments, repeatedly acquiring locks on the same resource has varying consequences depending on the lock type. With counting semaphores, extra lock acquisitions reduce the available resource count, potentially causing other threads to block indefinitely. With non-recursive mutexes, attempting to lock an already-held mutex typically causes deadlock or undefined behavior. This creates race conditions, resource starvation, denial of service, or unpredictable program behavior.

Risk

Multiple locks create serious concurrency issues. With counting semaphores, extra decrements may eventually exhaust the count, blocking all threads waiting for that resource—a denial of service. With non-recursive mutexes, the thread may deadlock waiting for a lock it already holds. Even if the lock type allows reentrant locking, the unlock count won't match, leaving resources permanently locked. This can lead to resource starvation where some threads never get access, performance degradation as threads wait unnecessarily, or complete system hang. In real-time or safety-critical systems, these issues can have severe consequences.

Solution

Ensure all control paths have exactly matching lock and unlock pairs. Use RAII patterns in C++ with lock guards that automatically release locks when scope exits. Use static analysis tools that verify lock/unlock pairing. Consider using recursive mutexes if reentrant locking is intentionally needed, but prefer restructuring code to avoid the need. If a thread cannot complete its work while holding a lock, release the lock before waiting for conditions to improve, then reacquire before retrying. Document locking requirements clearly and review concurrency code carefully.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

DoS: Resource Consumption (CPU) - Extra locks may exhaust semaphore counts, blocking other threads.
AvailabilityScope: Availability

DoS: Crash, Exit, or Restart - Deadlock from locking non-recursive mutex twice crashes or hangs the system.
IntegrityScope: Integrity

Unexpected State - System enters undefined state when lock counts don't match expectations.

Example Code

Vulnerable Code

// Vulnerable: Semaphore locked twice in some paths
#include <semaphore.h>
#include <pthread.h>

sem_t resource_sem;

void vulnerable_semaphore(int condition) {
    // First lock
    sem_wait(&resource_sem);

    if (condition) {
        // Vulnerable: Second lock on same semaphore
        sem_wait(&resource_sem);  // Decrements count again!

        // If this is a binary semaphore, thread blocks forever
        // If counting semaphore, depletes available count
    }

    // Process resource...

    // Only one unlock, but may have locked twice
    sem_post(&resource_sem);
}

// Vulnerable: Non-recursive mutex locked twice
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void vulnerable_mutex_relock() {
    pthread_mutex_lock(&mutex);

    // ... do some work ...

    if (needs_more_work()) {
        // Vulnerable: Attempting to lock already-held mutex
        pthread_mutex_lock(&mutex);  // DEADLOCK!
        // Thread blocks waiting for itself
    }

    pthread_mutex_unlock(&mutex);
}

// Vulnerable: Lock in loop without proper unlock
void vulnerable_loop_lock() {
    pthread_mutex_lock(&mutex);

    for (int i = 0; i < 10; i++) {
        if (should_retry(i)) {
            // Vulnerable: Locking again without unlock
            pthread_mutex_lock(&mutex);
            continue;
        }
        process_item(i);
    }

    pthread_mutex_unlock(&mutex);  // Only one unlock
}
// Vulnerable: Exception causes missing unlock, leading to re-lock issues
#include <mutex>

std::mutex mtx;

class VulnerableLocking {
public:
    void vulnerable_exception_path() {
        mtx.lock();

        try {
            riskyOperation();  // May throw
        } catch (...) {
            // Vulnerable: Forgot to unlock before retrying
            // Next call to this function will double-lock
        }

        mtx.unlock();
    }

    void vulnerable_early_return() {
        mtx.lock();

        if (some_condition()) {
            // Vulnerable: Return without unlock
            return;  // Lock held permanently
        }

        process();
        mtx.unlock();
    }

    // Vulnerable: Recursive call without recursive mutex
    void vulnerable_recursive(int depth) {
        mtx.lock();  // First lock

        if (depth > 0) {
            vulnerable_recursive(depth - 1);  // Tries to lock again!
            // Deadlock on non-recursive mutex
        }

        mtx.unlock();
    }
};
// Vulnerable: Java synchronized block entered twice
public class VulnerableLocking {

    private final Object lock = new Object();
    private int lockCount = 0;

    // Vulnerable: Manual lock counting mismanagement
    public void vulnerableManualLock() {
        synchronized (lock) {
            lockCount++;

            if (needsRelock()) {
                synchronized (lock) {  // Java allows this (reentrant)
                    lockCount++;  // But manual count now wrong
                }
                // Inner block exits, but lockCount still high
            }

            // lockCount may not match actual lock state
        }
        lockCount--;  // Only decremented once
    }

    // Vulnerable: Lock acquired in loop without proper tracking
    public void vulnerableLoopLock() throws InterruptedException {
        java.util.concurrent.Semaphore sem = new java.util.concurrent.Semaphore(5);

        for (int i = 0; i < 10; i++) {
            sem.acquire();  // Acquires 10 times

            // But only releases once after loop
        }

        sem.release();  // Only 1 release for 10 acquires
        // 4 permits permanently consumed
    }
}

Fixed Code

// Fixed: Proper semaphore handling with consistent lock/unlock
#include <semaphore.h>
#include <pthread.h>

sem_t resource_sem;

void fixed_semaphore(int condition) {
    // Lock once
    sem_wait(&resource_sem);

    // Handle condition without additional locking
    if (condition) {
        // Process condition without re-locking
        handle_condition();
    }

    // Process resource...

    // Matching unlock
    sem_post(&resource_sem);
}

// Fixed: Use recursive mutex if reentrant locking needed
pthread_mutex_t recursive_mutex;
pthread_mutexattr_t attr;

void init_recursive_mutex() {
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&recursive_mutex, &attr);
}

void fixed_recursive_lock() {
    pthread_mutex_lock(&recursive_mutex);

    if (needs_more_work()) {
        // Safe with recursive mutex - counts lock depth
        pthread_mutex_lock(&recursive_mutex);
        do_more_work();
        pthread_mutex_unlock(&recursive_mutex);  // Must unlock each lock
    }

    pthread_mutex_unlock(&recursive_mutex);
}

// Fixed: Restructure to avoid re-locking
void fixed_no_relock() {
    pthread_mutex_lock(&mutex);

    // Do all work within single lock hold
    if (needs_more_work()) {
        do_more_work();  // No additional locking
    }

    pthread_mutex_unlock(&mutex);
}

// Fixed: Release before retry, reacquire after
void fixed_release_retry() {
    pthread_mutex_lock(&mutex);

    while (!condition_met()) {
        // Fixed: Release lock while waiting
        pthread_mutex_unlock(&mutex);

        wait_for_condition();

        // Fixed: Reacquire lock
        pthread_mutex_lock(&mutex);
    }

    process_resource();
    pthread_mutex_unlock(&mutex);
}
// Fixed: Use RAII lock guards
#include <mutex>

std::mutex mtx;

class FixedLocking {
public:
    void fixed_exception_safe() {
        std::lock_guard<std::mutex> lock(mtx);  // RAII

        riskyOperation();  // If throws, lock_guard destructor unlocks

        // Automatic unlock when lock_guard goes out of scope
    }

    void fixed_early_return() {
        std::lock_guard<std::mutex> lock(mtx);

        if (some_condition()) {
            return;  // Safe: lock_guard unlocks automatically
        }

        process();
        // Automatic unlock
    }

    // Fixed: Use recursive_mutex for recursive calls
    std::recursive_mutex rec_mtx;

    void fixed_recursive(int depth) {
        std::lock_guard<std::recursive_mutex> lock(rec_mtx);

        if (depth > 0) {
            fixed_recursive(depth - 1);  // Safe with recursive_mutex
        }

        // Each level's lock_guard unlocks as it returns
    }

    // Best: Restructure to avoid recursion
    void fixed_no_recursion() {
        std::lock_guard<std::mutex> lock(mtx);

        // Iterative version instead of recursive
        for (int depth = 10; depth > 0; depth--) {
            process_level(depth);
        }
    }
};
// Fixed: Proper Java semaphore handling
import java.util.concurrent.Semaphore;

public class FixedLocking {

    private final Semaphore sem = new Semaphore(5);

    // Fixed: Match acquires and releases
    public void fixedSemaphore() throws InterruptedException {
        sem.acquire();
        try {
            processResource();
        } finally {
            sem.release();  // Always release in finally
        }
    }

    // Fixed: Track acquires and releases in loop
    public void fixedLoopSemaphore() throws InterruptedException {
        int acquired = 0;

        try {
            for (int i = 0; i < 10; i++) {
                sem.acquire();
                acquired++;
                // Process item
            }
        } finally {
            // Fixed: Release exactly as many as acquired
            for (int i = 0; i < acquired; i++) {
                sem.release();
            }
        }
    }

    // Fixed: Use try-with-resources pattern
    private final java.util.concurrent.locks.Lock lock =
        new java.util.concurrent.locks.ReentrantLock();

    public void fixedTryFinally() {
        lock.lock();
        try {
            processResource();
        } finally {
            lock.unlock();  // Always unlocks
        }
    }
}

CVE Examples

  • CVE-2008-1669: Linux kernel double-lock vulnerability in file system code causing denial of service.
  • CVE-2010-4243: Multiple lock acquisition leading to system hang.

References

  1. MITRE Corporation. "CWE-764: Multiple Locks of a Critical Resource." https://cwe.mitre.org/data/definitions/764.html
  2. CERT C Coding Standard. "CON31-C. Do not destroy a mutex while it is locked."
  3. C++ Core Guidelines. "CP.20: Use RAII, never plain lock()/unlock()."