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
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Ressourcenverbrauch (CPU) - Zusätzliche Sperren können Semaphor-Zähler erschöpfen und andere Threads blockieren. |
| Verfügbarkeit | Bereich: 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ät | Bereich: 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
- MITRE Corporation. "CWE-764: Multiple Locks of a Critical Resource." https://cwe.mitre.org/data/definitions/764.html
- CERT C Coding Standard. "CON31-C. Do not destroy a mutex while it is locked."
- C++ Core Guidelines. "CP.20: Use RAII, never plain lock()/unlock()."