Deadlock

Beschreibung

Deadlock ist eine Nebenläufigkeits-Schwachstelle, bei der ein Produkt mehrere Threads oder ausführbare Segmente enthält, die aufeinander warten, um notwendige Sperren freizugeben, was zu einer permanenten Blockierung führt, bei der keiner der Threads fortfahren kann. Dies tritt typischerweise auf, wenn Threads Sperren in unterschiedlicher Reihenfolge erwerben: Thread A hält Sperre 1 und wartet auf Sperre 2, während Thread B Sperre 2 hält und auf Sperre 1 wartet. Keiner der Threads kann fortfahren, und beide werden unbegrenzt warten. Deadlocks können auch mehr als zwei Threads in einer zirkularen Wartekette umfassen oder mit anderen Synchronisationsprimitiven wie Semaphoren und Condition-Variablen auftreten.

Risiko

Deadlocks verursachen Verfügbarkeitsprobleme, die von blockierten Threads bis zum vollständigen Systemstillstand reichen. Wenn kritische Threads blockieren, wird die Anwendung nicht mehr reaktionsfähig und erfordert manuellen Eingriff oder Neustart. In Servern können blockierte Threads Ressourcen verbrauchen, während sie nichts tun, und schließlich Thread-Pools erschöpfen. CPU-Verbrauch kann auftreten, wenn Lock-Prüfungen in engen Schleifen stattfinden (Spin-Locks). In sicherheitskritischen Systemen können Deadlocks katastrophale Folgen haben. Angreifer, die Anwendungs-Locking-Muster verstehen, können möglicherweise Deadlocks absichtlich auslösen und so Denial-of-Service verursachen. Deadlocks sind oft intermittierend und schwer zu reproduzieren, was die Diagnose erschwert.

Lösung

Etablieren Sie eine konsistente globale Lock-Reihenfolge und stellen Sie sicher, dass aller Code Sperren in derselben Reihenfolge erwirbt. Verwenden Sie Lock-Hierarchien, bei denen Sperren niedrigerer Ebene vor Sperren höherer Ebene erworben werden müssen. Verwenden Sie tryLock mit Timeout anstatt unbegrenztes Blockieren. Implementieren Sie Deadlock-Erkennungsmechanismen in Debug-Builds. Minimieren Sie den Lock-Scope - halten Sie Sperren für die kürzeste notwendige Zeit. Vermeiden Sie den Aufruf externen Codes, während Sie Sperren halten. Verwenden Sie wenn möglich höherwertige Nebenläufigkeitsprimitive wie Concurrent Collections. Stellen Sie sicher, dass Sperren bei Ausnahmebedingungen mit finally-Blöcken oder RAII freigegeben werden. Erwägen Sie die Verwendung lock-freier Algorithmen für leistungskritischen Code. Dokumentieren Sie Locking-Protokolle und überprüfen Sie Code auf Verletzungen.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Verfügbarkeit

DoS: Ressourcenverbrauch - Jeder blockierte Thread hängt unbegrenzt und verhindert, dass Aufgaben abgeschlossen werden, was möglicherweise Thread-Pools erschöpft.
VerfügbarkeitBereich: Verfügbarkeit

DoS: CPU-Verbrauch - Wenn Lock-Prüfungen in engen Schleifen stattfinden (Spin-Locks), kann ein Deadlock hohe CPU-Auslastung verursachen, während kein Fortschritt gemacht wird.
VerfügbarkeitBereich: Verfügbarkeit

Systemstillstand - In schweren Fällen kann ein Deadlock die gesamte Anwendung oder das System nicht mehr reaktionsfähig machen.

Beispielcode

Anfälliger Code

// Anfällig: Inkonsistente Lock-Reihenfolge
#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);  // Erhält lock_a zuerst
    sleep(1);  // Erhöht Deadlock-Chance zur Demonstration
    pthread_mutex_lock(&lock_b);  // Wartet auf lock_b

    // Kritischer Abschnitt
    do_work();

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

void* thread2_func(void* arg) {
    pthread_mutex_lock(&lock_b);  // Erhält lock_b zuerst (umgekehrte Reihenfolge!)
    sleep(1);
    pthread_mutex_lock(&lock_a);  // Wartet auf lock_a - DEADLOCK!

    // Kritischer Abschnitt
    do_work();

    pthread_mutex_unlock(&lock_a);
    pthread_mutex_unlock(&lock_b);
    return NULL;
}
// Anfällig: Überweisung zwischen Konten mit inkonsistenter Lock-Reihenfolge
public class VulnerableBankTransfer {

    public void transfer(Account from, Account to, double amount) {
        // Anfällig: Lock-Reihenfolge hängt von Aufruf-Reihenfolge ab
        synchronized (from) {
            synchronized (to) {
                if (from.getBalance() >= amount) {
                    from.withdraw(amount);
                    to.deposit(amount);
                }
            }
        }
    }
}

// Thread 1: transfer(kontoA, kontoB, 100) - sperrt A dann B
// Thread 2: transfer(kontoB, kontoA, 50)  - sperrt B dann A
// DEADLOCK!
# Anfällig: Verschachtelte Lock-Erwerbung in unterschiedlicher Reihenfolge
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-Reihenfolge: resource -> log
            log_result(result)

def log_and_process(data):
    with log_lock:
        log_input(data)
        with resource_lock:  # Lock-Reihenfolge: log -> resource - DEADLOCK!
            process(data)
// Anfällig: Selbst-Deadlock mit nicht-rekursivem Mutex
#include <mutex>

class VulnerableSelfDeadlock {
    std::mutex mutex_;

public:
    void outer() {
        std::lock_guard<std::mutex> lock(mutex_);
        // ... Arbeit verrichten ...
        inner();  // Ruft Methode auf, die auch das Lock benötigt
    }

    void inner() {
        std::lock_guard<std::mutex> lock(mutex_);  // DEADLOCK!
        // ... mehr Arbeit verrichten ...
    }
};
// Anfällig: Lock halten während auf Bedingung gewartet wird
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;
    // Vergessen, die Bedingung zu signalisieren!
    pthread_mutex_unlock(&mutex);
    return NULL;
}

void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    while (!data_ready) {
        // Wartet ewig, wenn Producer vergessen hat zu signalisieren
        pthread_cond_wait(&cond, &mutex);
    }
    consume_data();
    pthread_mutex_unlock(&mutex);
    return NULL;
}

Korrigierter Code

// Korrigiert: Konsistente Lock-Reihenfolge
#include <pthread.h>

pthread_mutex_t lock_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock_b = PTHREAD_MUTEX_INITIALIZER;

// Korrigiert: Beide Threads erwerben Locks in derselben Reihenfolge (a, dann 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);  // Korrigiert: Gleiche Reihenfolge wie thread1
    pthread_mutex_lock(&lock_b);

    do_work();

    pthread_mutex_unlock(&lock_b);
    pthread_mutex_unlock(&lock_a);
    return NULL;
}
// Korrigiert: Lock-Reihenfolge basierend auf Objekt-Identität
public class FixedBankTransfer {

    public void transfer(Account from, Account to, double amount) {
        // Korrigiert: Immer in konsistenter Reihenfolge basierend auf Konto-ID sperren
        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: tryLock mit Timeout verwenden
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;  // Millisekunden

        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();
                }
            }
            // Zurückweichen und erneut versuchen
            Thread.sleep(10);
        }
    }
}
# Korrigiert: Einzelnes Lock oder konsistente Reihenfolge
import threading

# Option 1: Einzelnes Lock für verwandte Operationen
combined_lock = threading.Lock()

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

# Option 2: Konsistente Lock-Reihenfolge
resource_lock = threading.Lock()
log_lock = threading.Lock()

def acquire_locks_in_order():
    """Immer in derselben Reihenfolge erwerben: log -> resource"""
    log_lock.acquire()
    resource_lock.acquire()

def release_locks_in_order():
    """In umgekehrter Reihenfolge freigeben"""
    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()
// Korrigiert: std::lock für deadlock-freien Erwerb verwenden
#include <mutex>

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

public:
    void safeOperation() {
        // Korrigiert: std::lock erwirbt beide ohne Deadlock
        std::lock(mutex_a_, mutex_b_);

        // adopt_lock teilt lock_guard mit, dass der Mutex bereits gesperrt ist
        std::lock_guard<std::mutex> lock_a(mutex_a_, std::adopt_lock);
        std::lock_guard<std::mutex> lock_b(mutex_b_, std::adopt_lock);

        // ... Arbeit mit beiden gehaltenen Locks ...
    }  // Beide sicher freigegeben
};

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

public:
    void safeOperation() {
        // Korrigiert: scoped_lock behandelt mehrere Locks sicher
        std::scoped_lock lock(mutex_a_, mutex_b_);
        // ... Arbeit ...
    }
};
// Korrigiert: Rekursiven Mutex für Selbst-Aufrufe verwenden
#include <mutex>

class FixedSelfDeadlock {
    std::recursive_mutex mutex_;  // Korrigiert: Erlaubt rekursives Sperren

public:
    void outer() {
        std::lock_guard<std::recursive_mutex> lock(mutex_);
        // ... Arbeit verrichten ...
        inner();  // Sicher: Rekursiver Mutex erlaubt erneutes Sperren
    }

    void inner() {
        std::lock_guard<std::recursive_mutex> lock(mutex_);
        // ... mehr Arbeit verrichten ...
    }
};
// Korrigiert: Korrekte Condition-Variable-Verwendung
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);  // Korrigiert: Bedingung signalisieren
    pthread_mutex_unlock(&mutex);
    return NULL;
}

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

CVE-Beispiele

  • CVE-2009-1388: Kernel-Deadlock ausgelöst durch mehrere gleichzeitige Funktionsaufrufe während der Thread-Erstellung.
  • CVE-2006-4342: Deadlock bei Ressourcen-Entfernungsoperationen.

Verwandte CWEs

  • CWE-667: Unzulässiges Locking (Eltern)
  • CWE-662: Unzureichende Synchronisation (Eltern)
  • CWE-764: Mehrfaches Sperren einer kritischen Ressource (verwandt)
  • CWE-765: Mehrfaches Entsperren einer kritischen Ressource (verwandt)

Referenzen

  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. Götz, Brian. "Java Concurrency in Practice." Addison-Wesley, 2006.