Mehrfaches Sperren einer kritischen Ressource

Beschreibung

Mehrfaches Sperren einer kritischen Ressource ist eine Nebenläufigkeitsschwachstelle, bei der Software eine kritische Ressource öfter sperrt als beabsichtigt, was zu einem unerwarteten Zustand im System führt. In nebenläufigen Umgebungen hat wiederholtes Erwerben von Sperren auf derselben Ressource unterschiedliche Konsequenzen je nach Sperrtyp. Bei zählenden Semaphoren reduzieren zusätzliche Sperrerwerbe die verfügbare Ressourcenzahl und können andere Threads dazu bringen, unendlich zu blockieren. Bei nicht-rekursiven Mutexen verursacht der Versuch, einen bereits gehaltenen Mutex zu sperren, typischerweise Deadlock oder undefiniertes Verhalten. Dies schafft Race Conditions, Ressourcenverhungern, Denial of Service oder unvorhersehbares Programmverhalten.

Risiko

Mehrfaches Sperren erzeugt ernsthafte Nebenläufigkeitsprobleme. Bei zählenden Semaphoren können zusätzliche Dekrementierungen schließlich den Zähler erschöpfen und alle Threads blockieren, die auf diese Ressource warten - ein Denial of Service. Bei nicht-rekursiven Mutexen kann der Thread in einem Deadlock warten, der auf eine Sperre wartet, die er bereits hält. Selbst wenn der Sperrtyp wiedereintrittsfähiges Sperren erlaubt, stimmt die Entsperrzahl nicht überein, wodurch Ressourcen dauerhaft gesperrt bleiben. Dies kann zu Ressourcenverhungern führen, bei dem einige Threads nie Zugriff erhalten, Leistungsverschlechterung, da Threads unnötig warten, oder komplettem Systemstillstand. In Echtzeit- oder sicherheitskritischen Systemen können diese Probleme schwerwiegende Konsequenzen haben.

Lösung

Stellen Sie sicher, dass alle Kontrollpfade genau übereinstimmende Sperr- und Entsperrpaare haben. Verwenden Sie RAII-Muster in C++ mit Lock Guards, die automatisch Sperren freigeben, wenn der Gültigkeitsbereich endet. Verwenden Sie statische Analysewerkzeuge, die Sperr-/Entsperrpaarung verifizieren. Erwägen Sie die Verwendung rekursiver Mutexe, wenn wiedereintrittsfähiges Sperren absichtlich benötigt wird, aber bevorzugen Sie die Umstrukturierung des Codes, um den Bedarf zu vermeiden. Wenn ein Thread seine Arbeit nicht abschließen kann, während er eine Sperre hält, geben Sie die Sperre frei, bevor Sie auf die Verbesserung der Bedingungen warten, und erwerben Sie sie dann erneut, bevor Sie es erneut versuchen. Dokumentieren Sie Sperranforderungen klar und überprüfen Sie Nebenläufigkeitscode sorgfältig.

Häufige Auswirkungen

AuswirkungDetails
VerfügbarkeitBereich: Verfügbarkeit

DoS: Ressourcenverbrauch (CPU) - Zusätzliche Sperren können Semaphor-Zähler erschöpfen und andere Threads blockieren.
VerfügbarkeitBereich: Verfügbarkeit

DoS: Absturz, Beenden oder Neustart - Deadlock durch zweimaliges Sperren eines nicht-rekursiven Mutex stürzt das System ab oder hängt es.
IntegritätBereich: Integrität

Unerwarteter Zustand - System gerät in undefinierten Zustand, wenn Sperrzähler nicht den Erwartungen entsprechen.

Beispielcode

Verwundbarer Code

// Verwundbar: Semaphor in einigen Pfaden zweimal gesperrt
#include <semaphore.h>
#include <pthread.h>

sem_t resource_sem;

void vulnerable_semaphore(int condition) {
    // Erste Sperre
    sem_wait(&resource_sem);

    if (condition) {
        // Verwundbar: Zweite Sperre auf demselben Semaphor
        sem_wait(&resource_sem);  // Dekrementiert Zähler erneut!

        // Wenn dies ein binärer Semaphor ist, blockiert Thread für immer
        // Wenn zählender Semaphor, erschöpft verfügbaren Zähler
    }

    // Ressource verarbeiten...

    // Nur eine Entsperrung, aber möglicherweise zweimal gesperrt
    sem_post(&resource_sem);
}

// Verwundbar: Nicht-rekursiver Mutex zweimal gesperrt
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void vulnerable_mutex_relock() {
    pthread_mutex_lock(&mutex);

    // ... etwas Arbeit machen ...

    if (needs_more_work()) {
        // Verwundbar: Versuch, bereits gehaltenen Mutex zu sperren
        pthread_mutex_lock(&mutex);  // DEADLOCK!
        // Thread blockiert wartend auf sich selbst
    }

    pthread_mutex_unlock(&mutex);
}

// Verwundbar: Sperre in Schleife ohne korrekte Entsperrung
void vulnerable_loop_lock() {
    pthread_mutex_lock(&mutex);

    for (int i = 0; i < 10; i++) {
        if (should_retry(i)) {
            // Verwundbar: Erneutes Sperren ohne Entsperrung
            pthread_mutex_lock(&mutex);
            continue;
        }
        process_item(i);
    }

    pthread_mutex_unlock(&mutex);  // Nur eine Entsperrung
}
// Verwundbar: Ausnahme verursacht fehlende Entsperrung, führt zu Neusperrproblemen
#include <mutex>

std::mutex mtx;

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

        try {
            riskyOperation();  // Kann werfen
        } catch (...) {
            // Verwundbar: Vergessen zu entsperren vor erneutem Versuch
            // Nächster Aufruf dieser Funktion wird doppelt sperren
        }

        mtx.unlock();
    }

    void vulnerable_early_return() {
        mtx.lock();

        if (some_condition()) {
            // Verwundbar: Rückkehr ohne Entsperrung
            return;  // Sperre dauerhaft gehalten
        }

        process();
        mtx.unlock();
    }

    // Verwundbar: Rekursiver Aufruf ohne rekursiven Mutex
    void vulnerable_recursive(int depth) {
        mtx.lock();  // Erste Sperre

        if (depth > 0) {
            vulnerable_recursive(depth - 1);  // Versucht erneut zu sperren!
            // Deadlock bei nicht-rekursivem Mutex
        }

        mtx.unlock();
    }
};
// Verwundbar: Java synchronized-Block zweimal betreten
public class VulnerableLocking {

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

    // Verwundbar: Manuelle Sperrzählung Fehlverwaltung
    public void vulnerableManualLock() {
        synchronized (lock) {
            lockCount++;

            if (needsRelock()) {
                synchronized (lock) {  // Java erlaubt dies (wiedereintrittsfähig)
                    lockCount++;  // Aber manuelle Zählung jetzt falsch
                }
                // Innerer Block endet, aber lockCount immer noch hoch
            }

            // lockCount stimmt möglicherweise nicht mit tatsächlichem Sperrzustand überein
        }
        lockCount--;  // Nur einmal dekrementiert
    }

    // Verwundbar: Sperre in Schleife ohne korrektes Tracking erworben
    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();  // Erwirbt 10 mal

            // Aber gibt nur einmal nach Schleife frei
        }

        sem.release();  // Nur 1 Freigabe für 10 Erwerbungen
        // 4 Genehmigungen dauerhaft verbraucht
    }
}

Korrigierter Code

// Korrigiert: Korrekte Semaphor-Behandlung mit konsistenter Sperr-/Entsperrung
#include <semaphore.h>
#include <pthread.h>

sem_t resource_sem;

void fixed_semaphore(int condition) {
    // Einmal sperren
    sem_wait(&resource_sem);

    // Bedingung ohne zusätzliches Sperren behandeln
    if (condition) {
        // Bedingung ohne Neusperren verarbeiten
        handle_condition();
    }

    // Ressource verarbeiten...

    // Übereinstimmende Entsperrung
    sem_post(&resource_sem);
}

// Korrigiert: Rekursiven Mutex verwenden wenn wiedereintrittsfähiges Sperren benötigt
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()) {
        // Sicher mit rekursivem Mutex - zählt Sperrtiefe
        pthread_mutex_lock(&recursive_mutex);
        do_more_work();
        pthread_mutex_unlock(&recursive_mutex);  // Muss jede Sperre entsperren
    }

    pthread_mutex_unlock(&recursive_mutex);
}

// Korrigiert: Umstrukturieren um Neusperren zu vermeiden
void fixed_no_relock() {
    pthread_mutex_lock(&mutex);

    // Alle Arbeit innerhalb eines einzelnen Sperrhaltens erledigen
    if (needs_more_work()) {
        do_more_work();  // Kein zusätzliches Sperren
    }

    pthread_mutex_unlock(&mutex);
}

// Korrigiert: Vor Wiederholung freigeben, danach erneut erwerben
void fixed_release_retry() {
    pthread_mutex_lock(&mutex);

    while (!condition_met()) {
        // Korrigiert: Sperre während des Wartens freigeben
        pthread_mutex_unlock(&mutex);

        wait_for_condition();

        // Korrigiert: Sperre erneut erwerben
        pthread_mutex_lock(&mutex);
    }

    process_resource();
    pthread_mutex_unlock(&mutex);
}
// Korrigiert: RAII Lock Guards verwenden
#include <mutex>

std::mutex mtx;

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

        riskyOperation();  // Wenn wirft, lock_guard-Destruktor entsperrt

        // Automatische Entsperrung wenn lock_guard den Gültigkeitsbereich verlässt
    }

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

        if (some_condition()) {
            return;  // Sicher: lock_guard entsperrt automatisch
        }

        process();
        // Automatische Entsperrung
    }

    // Korrigiert: recursive_mutex für rekursive Aufrufe verwenden
    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);  // Sicher mit recursive_mutex
        }

        // lock_guard jeder Ebene entsperrt bei Rückkehr
    }

    // Am besten: Umstrukturieren um Rekursion zu vermeiden
    void fixed_no_recursion() {
        std::lock_guard<std::mutex> lock(mtx);

        // Iterative Version statt rekursiv
        for (int depth = 10; depth > 0; depth--) {
            process_level(depth);
        }
    }
};
// Korrigiert: Korrekte Java-Semaphor-Behandlung
import java.util.concurrent.Semaphore;

public class FixedLocking {

    private final Semaphore sem = new Semaphore(5);

    // Korrigiert: Erwerbungen und Freigaben abgleichen
    public void fixedSemaphore() throws InterruptedException {
        sem.acquire();
        try {
            processResource();
        } finally {
            sem.release();  // Immer in finally freigeben
        }
    }

    // Korrigiert: Erwerbungen und Freigaben in Schleife verfolgen
    public void fixedLoopSemaphore() throws InterruptedException {
        int acquired = 0;

        try {
            for (int i = 0; i < 10; i++) {
                sem.acquire();
                acquired++;
                // Element verarbeiten
            }
        } finally {
            // Korrigiert: Genau so viele freigeben wie erworben
            for (int i = 0; i < acquired; i++) {
                sem.release();
            }
        }
    }

    // Korrigiert: try-with-resources-Muster verwenden
    private final java.util.concurrent.locks.Lock lock =
        new java.util.concurrent.locks.ReentrantLock();

    public void fixedTryFinally() {
        lock.lock();
        try {
            processResource();
        } finally {
            lock.unlock();  // Entsperrt immer
        }
    }
}

CVE-Beispiele

  • CVE-2008-1669: Linux-Kernel-Double-Lock-Schwachstelle im Dateisystemcode, die Denial of Service verursacht.
  • CVE-2010-4243: Mehrfacher Sperrerwerb führt zu Systemstillstand.

Referenzen

  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()."