Mehrfaches Entsperren einer kritischen Ressource
Beschreibung
Mehrfaches Entsperren einer kritischen Ressource ist eine Nebenläufigkeitsschwachstelle, bei der Software eine kritische Ressource öfter entsperrt als beabsichtigt, was zu einem unerwarteten Zustand im System führt. In nebenläufigen Umgebungen hat wiederholtes Entsperren einer Ressource unvorhersehbare Konsequenzen je nach Sperrtyp. Bei zählenden Semaphoren erhöhen zusätzliche Entsperr- (post/signal-) Operationen den verfügbaren Zähler über das beabsichtigte Maximum hinaus, was potenziell mehr Threads ermöglicht, gleichzeitig auf die Ressource zuzugreifen als beabsichtigt. Bei Mutexen ist das Entsperren eines bereits entsperrten Mutex typischerweise undefiniertes Verhalten, das den internen Sperrzustand beschädigen oder das Programm zum Absturz bringen kann.
Risiko
Mehrfaches Entsperren erzeugt ernsthafte Nebenläufigkeits- und Sicherheitsprobleme. Bei Semaphoren können künstlich aufgeblähte Zähler mehr gleichzeitigen Zugriff erlauben, als die Ressource sicher handhaben kann, was zu Datenkorruption oder Race Conditions führt. Bei Mutexen variiert das Verhalten je nach Implementierung - einige stürzen ab, einige beschädigen stillschweigend den Zustand, einige erlauben anderen Threads, die Sperre vorzeitig zu erwerben. Dies kann gegenseitige Ausschlussgarantien brechen und gleichzeitigen Zugriff auf kritische Abschnitte erlauben, die serialisiert werden sollten. Angreifer, die mehrfaches Entsperren auslösen können, können dies ausnutzen, um Synchronisationskontrollen zu umgehen, auf gemeinsame Ressourcen ohne korrektes Sperren zuzugreifen oder Denial of Service zu verursachen.
Lösung
Stellen Sie sicher, dass alle Kontrollpfade genau übereinstimmende Sperr- und Entsperrpaare haben. Verwenden Sie RAII-Muster mit Lock Guards, die automatisch das Entsperren handhaben. Verfolgen Sie den Sperrzustand explizit falls nötig. Entsperren Sie niemals eine Ressource, die der aktuelle Thread nicht hält. Verwenden Sie statische Analysewerkzeuge, um Sperr-/Entsperrpaarung zu verifizieren. Seien Sie in Fehlerbehandlungspfaden besonders vorsichtig, keine Ressourcen zu entsperren, die nicht erfolgreich gesperrt wurden. Erwägen Sie die Verwendung von try-finally oder ähnlichen Mustern, um sicherzustellen, dass Entsperren genau einmal erfolgt. Dokumentieren Sie Sperrprotokolle und überprüfen Sie Nebenläufigkeitscode sorgfältig.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Absturz, Beenden oder Neustart - Entsperren nicht gehaltener Sperren kann abstürzen oder undefiniertes Verhalten verursachen. |
| Integrität | Bereich: Integrität Speicher ändern - Beschädigter Sperrzustand kann unautorisierten Speicherzugriff erlauben. |
| Integrität | Bereich: Integrität Unerwarteter Zustand - System gerät in undefinierten Zustand, wenn Entsperrzahl die Sperrzahl übersteigt. |
Beispielcode
Verwundbarer Code
// Verwundbar: Semaphor öfter entsperrt als gesperrt
#include <semaphore.h>
sem_t resource_sem;
void vulnerable_semaphore_unlock(int error_occurred) {
sem_wait(&resource_sem); // Einmal sperren
if (error_occurred) {
// Entsperren im Fehlerpfad
sem_post(&resource_sem);
// ... Fehlerbehandlung ...
}
process_resource();
// Verwundbar: Entsperrt erneut bedingungslos
sem_post(&resource_sem); // Doppelte Entsperrung wenn error_occurred!
// Semaphor-Zähler jetzt höher als er sein sollte
}
// Verwundbar: Mutex entsperrt wenn nicht gehalten
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void vulnerable_mutex_unlock(int skip_lock) {
if (!skip_lock) {
pthread_mutex_lock(&mutex);
}
// ... verarbeiten ...
// Verwundbar: Entsperrt auch wenn nie gesperrt
pthread_mutex_unlock(&mutex); // Undefiniert wenn skip_lock wahr war
}
// Verwundbar: Fehlerpfad entsperrt, dann entsperrt normaler Pfad auch
void vulnerable_double_unlock() {
pthread_mutex_lock(&mutex);
int result = do_operation();
if (result < 0) {
pthread_mutex_unlock(&mutex); // Entsperren bei Fehler
handle_error();
// Fällt durch!
}
// Verwundbar: Bedingungslose Entsperrung
pthread_mutex_unlock(&mutex); // Doppelte Entsperrung wenn Fehler aufgetreten
}
// Verwundbar: Ausnahmebehandlung verursacht doppelte Entsperrung
#include <mutex>
std::mutex mtx;
class VulnerableUnlock {
public:
void vulnerable_exception() {
mtx.lock();
try {
riskyOperation();
} catch (...) {
mtx.unlock(); // Entsperren im catch
throw; // Erneut werfen
}
mtx.unlock(); // Entsperrt auch hier wenn keine Ausnahme
// Wenn Ausnahme nach catch's Entsperrung aber vor
// Weitergabe geworfen, Mutex möglicherweise doppelt entsperrt
}
// Verwundbar: Mehrere Rückgabepfade mit inkonsistenter Entsperrung
int vulnerable_returns() {
mtx.lock();
if (condition_a()) {
mtx.unlock();
return 1;
}
if (condition_b()) {
// Vergessen hier zu entsperren
return 2; // Sperre geleakt
}
mtx.unlock();
mtx.unlock(); // Verwundbar: Zusätzliche Entsperrung wenn normal erreicht!
return 0;
}
};
// Verwundbar: Entsperren im Destruktor wenn Sperre möglicherweise nicht gehalten
class VulnerableLockHolder {
private:
pthread_mutex_t* mutex;
bool locked;
public:
VulnerableLockHolder(pthread_mutex_t* m) : mutex(m), locked(false) {
pthread_mutex_lock(mutex);
locked = true;
}
void release() {
pthread_mutex_unlock(mutex);
// Verwundbar: Setzt nicht locked = false
}
~VulnerableLockHolder() {
// Verwundbar: Kann erneut entsperren wenn release() aufgerufen wurde
pthread_mutex_unlock(mutex);
}
};
// Verwundbar: CVE-2009-0935-Muster - ungültige Adresse verursacht doppelte Entsperrung
void vulnerable_invalid_address(void* user_addr) {
pthread_mutex_lock(&mutex);
// Funktion schlägt fehl bei ungültiger Adresse
int result = read_from_address(user_addr);
if (result < 0) {
// Fehlerbehandler entsperrt
pthread_mutex_unlock(&mutex);
// Aber Fehlerpfad setzt fort und entsperrt erneut!
}
// ... mehr Verarbeitung ...
pthread_mutex_unlock(&mutex); // Doppelte Entsperrung bei Fehler
}
// Verwundbar: Java ReentrantLock öfter entsperrt als gesperrt
import java.util.concurrent.locks.ReentrantLock;
public class VulnerableUnlock {
private final ReentrantLock lock = new ReentrantLock();
public void vulnerableUnbalanced() {
lock.lock();
try {
process();
} finally {
lock.unlock();
lock.unlock(); // Verwundbar: Zusätzliche Entsperrung wirft IllegalMonitorStateException
}
}
public void vulnerableConditionalUnlock(boolean wasLocked) {
if (wasLocked) {
lock.lock();
}
try {
process();
} finally {
// Verwundbar: Entsperrt auch wenn nie gesperrt
lock.unlock(); // Wirft wenn wasLocked false war
}
}
}
Korrigierter Code
// Korrigiert: Einzelne Entsperrung mit korrektem Kontrollfluss
#include <semaphore.h>
sem_t resource_sem;
void fixed_semaphore_unlock(int error_occurred) {
sem_wait(&resource_sem);
if (error_occurred) {
// Fehler behandeln ohne hier zu entsperren
handle_error();
// Durchfallen zu einzelnem Entsperrpunkt
} else {
process_resource();
}
// Korrigiert: Einzelner Entsperrpunkt
sem_post(&resource_sem);
}
// Alternative: Frühe Rückkehr mit Entsperrung
void fixed_early_return(int error_occurred) {
sem_wait(&resource_sem);
if (error_occurred) {
sem_post(&resource_sem); // Entsperren
handle_error();
return; // Funktion verlassen, nicht fortsetzen
}
process_resource();
sem_post(&resource_sem); // Nur erreicht wenn kein Fehler
}
// Korrigiert: Sperrzustand explizit verfolgen
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void fixed_track_state(int skip_lock) {
int holding_lock = 0;
if (!skip_lock) {
pthread_mutex_lock(&mutex);
holding_lock = 1;
}
// ... verarbeiten ...
// Korrigiert: Nur entsperren wenn wir die Sperre halten
if (holding_lock) {
pthread_mutex_unlock(&mutex);
}
}
// Korrigiert: Korrekte Fehlerbehandlung ohne doppelte Entsperrung
void fixed_error_handling() {
pthread_mutex_lock(&mutex);
int result = do_operation();
if (result < 0) {
handle_error();
// Nicht hier entsperren, zu einzelner Entsperrung durchfallen
}
// Korrigiert: Einzelner Entsperrpunkt für alle Pfade
pthread_mutex_unlock(&mutex);
}
// Korrigiert: RAII mit lock_guard verwenden
#include <mutex>
std::mutex mtx;
class FixedUnlock {
public:
void fixed_raii() {
std::lock_guard<std::mutex> guard(mtx);
riskyOperation(); // Wenn wirft, guard-Destruktor entsperrt einmal
// Einzelne automatische Entsperrung wenn guard den Gültigkeitsbereich verlässt
}
// Korrigiert: unique_lock für manuelle Kontrolle
int fixed_manual() {
std::unique_lock<std::mutex> lock(mtx);
if (condition_a()) {
lock.unlock(); // Markiert als entsperrt
return 1;
}
if (condition_b()) {
// Sperre immer noch gehalten, wird im Destruktor entsperrt
return 2;
}
// Destruktor handhabt Entsperrung
return 0;
}
};
// Korrigiert: Korrekte Lock-Holder-Klasse
class FixedLockHolder {
private:
pthread_mutex_t* mutex;
bool locked;
public:
FixedLockHolder(pthread_mutex_t* m) : mutex(m), locked(false) {
pthread_mutex_lock(mutex);
locked = true;
}
void release() {
if (locked) {
pthread_mutex_unlock(mutex);
locked = false; // Zustand verfolgen
}
}
~FixedLockHolder() {
// Korrigiert: Nur entsperren wenn noch gehalten
if (locked) {
pthread_mutex_unlock(mutex);
}
}
// Kopieren verhindern
FixedLockHolder(const FixedLockHolder&) = delete;
FixedLockHolder& operator=(const FixedLockHolder&) = delete;
};
// Korrigiert: Korrekte Validierung ohne doppelte Entsperrung
void fixed_invalid_address(void* user_addr) {
// Adresse vor Sperren validieren
if (!is_valid_address(user_addr)) {
handle_error();
return;
}
pthread_mutex_lock(&mutex);
int result = read_from_address(user_addr);
if (result < 0) {
// Fehlerbehandlung ohne Entsperrung
log_error();
}
// Korrigiert: Einzelner Entsperrpunkt
pthread_mutex_unlock(&mutex);
}
// Korrigiert: Java mit korrekter Sperrbehandlung
import java.util.concurrent.locks.ReentrantLock;
public class FixedUnlock {
private final ReentrantLock lock = new ReentrantLock();
public void fixedTryFinally() {
lock.lock();
try {
process();
} finally {
lock.unlock(); // Genau eine Entsperrung
}
}
public void fixedConditionalLock(boolean needsLock) {
boolean acquired = false;
if (needsLock) {
lock.lock();
acquired = true;
}
try {
process();
} finally {
// Korrigiert: Nur entsperren wenn wir erworben haben
if (acquired) {
lock.unlock();
}
}
}
// Alternative: tryLock-Muster
public void fixedTryLock() {
if (lock.tryLock()) {
try {
process();
} finally {
lock.unlock();
}
} else {
handleLockNotAcquired();
}
}
}
CVE-Beispiele
- CVE-2009-0935: Angreifer lieferte ungültige Adresse an Speicherlesefunktion, wodurch Mutex zweimal entsperrt wurde.
- CVE-2017-15265: Doppelte Entsperrung im Linux-Kernel ALSA-Sequencer führte zu Use-after-Free.
Referenzen
- MITRE Corporation. "CWE-765: Multiple Unlocks of a Critical Resource." https://cwe.mitre.org/data/definitions/765.html
- CERT C Coding Standard. "CON31-C. Do not destroy a mutex while it is locked."
- CERT C Coding Standard. "POS48-C. Do not unlock or destroy another POSIX thread's mutex."