Deadlock

Description

Deadlock is a concurrency vulnerability where a product contains multiple threads or executable segments that are waiting for each other to release necessary locks, resulting in a permanent blocking condition where none of the threads can proceed. This typically occurs when threads acquire locks in different orders: Thread A holds Lock 1 and waits for Lock 2, while Thread B holds Lock 2 and waits for Lock 1. Neither thread can continue, and both will wait indefinitely. Deadlocks can also involve more than two threads in a circular wait chain, or occur with other synchronization primitives like semaphores and condition variables.

Risk

Deadlocks cause availability issues ranging from hung threads to complete system freeze. When critical threads deadlock, the application becomes unresponsive, requiring manual intervention or restart. In servers, deadlocked threads may consume resources while doing nothing, eventually exhausting thread pools. CPU consumption may occur if lock checks happen in tight loops (spin locks). In safety-critical systems, deadlocks can have catastrophic consequences. Attackers who understand application locking patterns may be able to trigger deadlocks deliberately, causing denial of service. Deadlocks are often intermittent and difficult to reproduce, making them challenging to diagnose.

Solution

Establish a consistent global lock ordering and ensure all code acquires locks in the same order. Use lock hierarchy where lower-level locks must be acquired before higher-level locks. Use tryLock with timeout rather than blocking indefinitely. Implement deadlock detection mechanisms in debug builds. Minimize lock scope—hold locks for the shortest time necessary. Avoid calling external code while holding locks. Use higher-level concurrency primitives like concurrent collections when possible. Ensure locks are released on exceptional conditions using finally blocks or RAII. Consider using lock-free algorithms for performance-critical code. Document locking protocols and review code for violations.

Common Consequences

ImpactDetails
AvailabilityScope: Availability

DoS: Resource Consumption - Each deadlocked thread hangs indefinitely, preventing tasks from completing and potentially exhausting thread pools.
AvailabilityScope: Availability

DoS: CPU Consumption - If lock checks occur in tight loops (spin locks), deadlock may cause high CPU usage while making no progress.
AvailabilityScope: Availability

System Hang - In severe cases, deadlock can make the entire application or system unresponsive.

Example Code

Vulnerable Code

// Vulnerable: Inconsistent lock ordering
#include <pthread.h>

pthread_mutex_t lock_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock_b = PTHREAD_MUTEX_INITIALIZER;

void* thread1_func(void* arg) {
    pthread_mutex_lock(&lock_a);  // Gets lock_a first
    sleep(1);  // Increases chance of deadlock for demonstration
    pthread_mutex_lock(&lock_b);  // Waits for lock_b

    // Critical section
    do_work();

    pthread_mutex_unlock(&lock_b);
    pthread_mutex_unlock(&lock_a);
    return NULL;
}

void* thread2_func(void* arg) {
    pthread_mutex_lock(&lock_b);  // Gets lock_b first (opposite order!)
    sleep(1);
    pthread_mutex_lock(&lock_a);  // Waits for lock_a - DEADLOCK!

    // Critical section
    do_work();

    pthread_mutex_unlock(&lock_a);
    pthread_mutex_unlock(&lock_b);
    return NULL;
}
// Vulnerable: Transfer between accounts with inconsistent lock order
public class VulnerableBankTransfer {

    public void transfer(Account from, Account to, double amount) {
        // Vulnerable: Lock order depends on method call order
        synchronized (from) {
            synchronized (to) {
                if (from.getBalance() >= amount) {
                    from.withdraw(amount);
                    to.deposit(amount);
                }
            }
        }
    }
}

// Thread 1: transfer(accountA, accountB, 100) - locks A then B
// Thread 2: transfer(accountB, accountA, 50)  - locks B then A
// DEADLOCK!
# Vulnerable: Nested lock acquisition in different orders
import threading

resource_lock = threading.Lock()
log_lock = threading.Lock()

def process_and_log(data):
    with resource_lock:
        result = process(data)
        with log_lock:  # Lock order: resource -> log
            log_result(result)

def log_and_process(data):
    with log_lock:
        log_input(data)
        with resource_lock:  # Lock order: log -> resource - DEADLOCK!
            process(data)
// Vulnerable: Self-deadlock with non-recursive mutex
#include <mutex>

class VulnerableSelfDeadlock {
    std::mutex mutex_;

public:
    void outer() {
        std::lock_guard<std::mutex> lock(mutex_);
        // ... do work ...
        inner();  // Calls method that also needs the lock
    }

    void inner() {
        std::lock_guard<std::mutex> lock(mutex_);  // DEADLOCK!
        // ... do more work ...
    }
};
// Vulnerable: Holding lock while waiting for condition
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0;

void* producer(void* arg) {
    pthread_mutex_lock(&mutex);
    produce_data();
    data_ready = 1;
    // Forgot to signal the condition!
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!data_ready) {
        // Waits forever if producer forgot to signal
        pthread_cond_wait(&cond, &mutex);
    }
    consume_data();
    pthread_mutex_unlock(&mutex);
    return NULL;
}

Fixed Code

// Fixed: Consistent lock ordering
#include <pthread.h>

pthread_mutex_t lock_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock_b = PTHREAD_MUTEX_INITIALIZER;

// Fixed: Both threads acquire locks in the same order (a, then b)
void* thread1_func(void* arg) {
    pthread_mutex_lock(&lock_a);
    pthread_mutex_lock(&lock_b);

    do_work();

    pthread_mutex_unlock(&lock_b);
    pthread_mutex_unlock(&lock_a);
    return NULL;
}

void* thread2_func(void* arg) {
    pthread_mutex_lock(&lock_a);  // Fixed: Same order as thread1
    pthread_mutex_lock(&lock_b);

    do_work();

    pthread_mutex_unlock(&lock_b);
    pthread_mutex_unlock(&lock_a);
    return NULL;
}
// Fixed: Lock ordering based on object identity
public class FixedBankTransfer {

    public void transfer(Account from, Account to, double amount) {
        // Fixed: Always lock in consistent order based on account ID
        Account first = from.getId() < to.getId() ? from : to;
        Account second = from.getId() < to.getId() ? to : from;

        synchronized (first) {
            synchronized (second) {
                if (from.getBalance() >= amount) {
                    from.withdraw(amount);
                    to.deposit(amount);
                }
            }
        }
    }
}

// Alternative: Use tryLock with timeout
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class FixedBankTransferTryLock {
    public boolean transfer(Account from, Account to, double amount) {
        long timeout = 1000;  // milliseconds

        while (true) {
            if (from.getLock().tryLock()) {
                try {
                    if (to.getLock().tryLock(timeout, TimeUnit.MILLISECONDS)) {
                        try {
                            if (from.getBalance() >= amount) {
                                from.withdraw(amount);
                                to.deposit(amount);
                                return true;
                            }
                            return false;
                        } finally {
                            to.getLock().unlock();
                        }
                    }
                } finally {
                    from.getLock().unlock();
                }
            }
            // Back off and retry
            Thread.sleep(10);
        }
    }
}
# Fixed: Single lock or consistent ordering
import threading

# Option 1: Single lock for related operations
combined_lock = threading.Lock()

def process_and_log_fixed(data):
    with combined_lock:
        result = process(data)
        log_result(result)

# Option 2: Consistent lock ordering
resource_lock = threading.Lock()
log_lock = threading.Lock()

def acquire_locks_in_order():
    """Always acquire in same order: log -> resource"""
    log_lock.acquire()
    resource_lock.acquire()

def release_locks_in_order():
    """Release in reverse order"""
    resource_lock.release()
    log_lock.release()

def process_and_log_ordered(data):
    acquire_locks_in_order()
    try:
        result = process(data)
        log_result(result)
    finally:
        release_locks_in_order()
// Fixed: Use std::lock for deadlock-free acquisition
#include <mutex>

class FixedMultipleLocks {
    std::mutex mutex_a_;
    std::mutex mutex_b_;

public:
    void safeOperation() {
        // Fixed: std::lock acquires both without deadlock
        std::lock(mutex_a_, mutex_b_);

        // adopt_lock tells lock_guard the mutex is already locked
        std::lock_guard<std::mutex> lock_a(mutex_a_, std::adopt_lock);
        std::lock_guard<std::mutex> lock_b(mutex_b_, std::adopt_lock);

        // ... work with both locks held ...
    }  // Both released safely
};

// C++17: Use std::scoped_lock
class FixedScopedLock {
    std::mutex mutex_a_;
    std::mutex mutex_b_;

public:
    void safeOperation() {
        // Fixed: scoped_lock handles multiple locks safely
        std::scoped_lock lock(mutex_a_, mutex_b_);
        // ... work ...
    }
};
// Fixed: Use recursive mutex for self-calls
#include <mutex>

class FixedSelfDeadlock {
    std::recursive_mutex mutex_;  // Fixed: Allows recursive locking

public:
    void outer() {
        std::lock_guard<std::recursive_mutex> lock(mutex_);
        // ... do work ...
        inner();  // Safe: recursive mutex allows re-locking
    }

    void inner() {
        std::lock_guard<std::recursive_mutex> lock(mutex_);
        // ... do more work ...
    }
};
// Fixed: Proper condition variable usage
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0;

void* producer(void* arg) {
    pthread_mutex_lock(&mutex);
    produce_data();
    data_ready = 1;
    pthread_cond_signal(&cond);  // Fixed: Signal the condition
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!data_ready) {
        pthread_cond_wait(&cond, &mutex);  // Will be signaled
    }
    consume_data();
    pthread_mutex_unlock(&mutex);
    return NULL;
}

CVE Examples

  • CVE-2009-1388: Kernel deadlock triggered by multiple simultaneous function calls during thread creation.
  • CVE-2006-4342: Deadlock occurring during resource removal operations.

  • CWE-667: Improper Locking (parent)
  • CWE-662: Improper Synchronization (parent)
  • CWE-764: Multiple Locks of a Critical Resource (related)
  • CWE-765: Multiple Unlocks of a Critical Resource (related)

References

  1. MITRE Corporation. "CWE-833: Deadlock." https://cwe.mitre.org/data/definitions/833.html
  2. CERT Java Secure Coding. "LCK08-J. Ensure actively held locks are released on exceptional conditions."
  3. Goetz, Brian. "Java Concurrency in Practice." Addison-Wesley, 2006.