Unsynchronisierter Zugriff auf gemeinsam genutzte Daten in einem Multithread-Kontext
Beschreibung
Unsynchronisierter Zugriff auf gemeinsam genutzte Daten in einem Multithread-Kontext ist eine Schwachstelle, bei der ein Produkt den Zugriff auf gemeinsam genutzte Datenressourcen nicht ordnungsgemäß synchronisiert, wenn mehrere Threads dieselben Daten gleichzeitig lesen und schreiben können. Ohne geeignete Synchronisierungsmechanismen wie Locks, Mutexe oder atomare Operationen können Threads ihre Operationen auf unvorhersehbare Weise verschachteln, was zu Race-Conditions führt. Dies kann zu Datenkorruption, inkonsistentem Zustand, Sicherheitsumgehungen oder Anwendungsabstürzen führen, wenn ein Thread Daten liest, die ein anderer Thread gleichzeitig modifiziert.
Risiko
Unsynchronisierter Zugriff auf gemeinsam genutzte Daten erzeugt ernsthafte Zuverlässigkeits- und Sicherheitsrisiken. Race-Conditions können den Anwendungszustand beschädigen, was zu unvorhersehbarem Verhalten führt, das schwer zu reproduzieren und zu debuggen ist. In Sicherheitskontexten können Angreifer Race-Conditions ausnutzen, um Authentifizierungsprüfungen zu umgehen, Finanztransaktionen zu manipulieren oder Time-of-Check-to-Time-of-Use (TOCTOU)-Schwachstellen zu verursachen. Datenkorruption kann zu Denial-of-Service, Verlust der Datenintegrität oder Privilegieneskalation führen, wenn sicherheitskritische Variablen betroffen sind. Diese Bugs sind besonders gefährlich, weil sie während Tests möglicherweise nicht auftreten, aber unter Produktionslastbedingungen erscheinen.
Lösung
Implementieren Sie ordnungsgemäße Synchronisierung für alle gemeinsam genutzten Daten, auf die von mehreren Threads zugegriffen wird. Verwenden Sie sprachgerechte Synchronisierungsprimitive wie synchronized-Blöcke in Java, Locks in Python oder Mutexe in C/C++. Erwägen Sie die Verwendung von thread-sicheren Datenstrukturen und Collections, die für gleichzeitigen Zugriff konzipiert sind. Wenden Sie das Prinzip der minimalen Freigabe an - reduzieren Sie den gemeinsam genutzten Zustand wo möglich. Verwenden Sie atomare Operationen für einfache Zähleraktualisierungen. Implementieren Sie unveränderliche Objekte wo machbar. Dokumentieren Sie Thread-Sicherheitsgarantien für alle gemeinsam genutzten Ressourcen. Verwenden Sie statische Analysewerkzeuge, um potenzielle Race-Conditions während der Entwicklung zu erkennen.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Integrität | Bereich: Integrität Anwendungsdaten ändern - Race-Conditions können gemeinsam genutzte Daten beschädigen, wenn mehrere Threads gleichzeitige Lese-Modifiziere-Schreibe-Operationen ohne Synchronisierung durchführen. |
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Absturz, Beenden oder Neustart - Datenkorruption durch Race-Conditions kann Anwendungsabstürze, Endlosschleifen oder inkonsistente Zustände verursachen, die zu Fehlern führen. |
| Vertraulichkeit | Bereich: Vertraulichkeit Anwendungsdaten lesen - Threads können teilweise aktualisierte Daten lesen, was potenziell sensible Informationen in einem inkonsistenten Zustand offenlegt. |
Beispielcode
Verwundbarer Code
// Verwundbar: Unsynchronisierter gemeinsam genutzter Zähler
public class VulnerableCounter {
private int count = 0; // Gemeinsam genutzter veränderlicher Zustand
// Verwundbar: Keine Synchronisierung
public void increment() {
count++; // Nicht atomar: Lesen, Inkrementieren, Schreiben können verschachtelt werden
}
public int getCount() {
return count; // Kann veralteten Wert zurückgeben
}
}
// Verwundbar: Unsynchronisierter Zugriff auf gemeinsam genutzte Collection
public class VulnerableUserCache {
private Map<String, User> userCache = new HashMap<>(); // Nicht thread-sicher
// Verwundbar: Gleichzeitige Modifikation möglich
public User getUser(String userId) {
if (!userCache.containsKey(userId)) {
// Race-Condition: ein anderer Thread könnte gleichen Benutzer hinzufügen
User user = loadUserFromDatabase(userId);
userCache.put(userId, user); // Gleichzeitiges put kann HashMap beschädigen
}
return userCache.get(userId);
}
public void updateUser(User user) {
// Verwundbar: Keine Synchronisierung mit getUser
userCache.put(user.getId(), user);
}
}
// Verwundbar: Check-then-act Race-Condition
public class VulnerableAccountManager {
private Map<String, Double> balances = new HashMap<>();
// Verwundbar: TOCTOU Race-Condition
public boolean withdraw(String accountId, double amount) {
double balance = balances.get(accountId); // Prüfen
if (balance >= amount) { // Zeitlücke zwischen Prüfen und Handeln
// Ein anderer Thread könnte zwischen Prüfung und Aktualisierung abheben
balances.put(accountId, balance - amount); // Handeln
return true;
}
return false;
}
}
# Verwundbar: Unsynchronisierter gemeinsam genutzter Zustand in Python
import threading
class VulnerableCounter:
def __init__(self):
self.count = 0 # Gemeinsam genutzter veränderlicher Zustand
# Verwundbar: Keine Synchronisierung
def increment(self):
# Nicht atomar: Lesen, Inkrementieren, Schreiben können verschachtelt werden
current = self.count
self.count = current + 1
def get_count(self):
return self.count
# Verwundbar: Gemeinsam genutztes Singleton mit Race-Condition
class VulnerableSingleton:
_instance = None
@classmethod
def get_instance(cls):
# Verwundbar: Race-Condition bei verzögerter Initialisierung
if cls._instance is None:
# Mehrere Threads können diese Prüfung gleichzeitig passieren
cls._instance = cls() # Mehrere Instanzen könnten erstellt werden
return cls._instance
# Verwundbar: Unsynchronisiertes Bankkonto
class VulnerableAccount:
def __init__(self, balance):
self.balance = balance
# Verwundbar: Race-Condition bei Überweisung
def transfer_to(self, target, amount):
if self.balance >= amount:
# Race: Kontostand könnte sich zwischen Prüfung und Aktualisierung ändern
self.balance -= amount
target.balance += amount
return True
return False
// Verwundbar: Unsynchronisierte gemeinsam genutzte Daten in C
#include <pthread.h>
#include <stdio.h>
// Verwundbar: Globaler gemeinsam genutzter Zähler ohne Synchronisierung
int shared_counter = 0;
// Verwundbar: Mehrere Threads inkrementieren ohne Lock
void* vulnerable_increment(void* arg) {
for (int i = 0; i < 100000; i++) {
// Nicht atomar: Lese-Modifiziere-Schreibe Race-Condition
shared_counter++; // Data-Race!
}
return NULL;
}
// Verwundbar: Unsynchronisierte verkettete Liste
struct Node {
int data;
struct Node* next;
};
struct Node* head = NULL;
// Verwundbar: Gleichzeitige Listenmodifikation
void vulnerable_add_node(int data) {
struct Node* new_node = malloc(sizeof(struct Node));
new_node->data = data;
// Race-Condition: ein anderer Thread könnte head modifizieren
new_node->next = head;
head = new_node; // Verlorene Aktualisierung wenn anderer Thread gleichzeitig hinzufügt
}
// Verwundbar: Double-checked Locking (auf vielen Architekturen defekt)
int initialized = 0;
void* resource = NULL;
void* get_resource() {
if (!initialized) { // Erste Prüfung ohne Lock
// Verwundbar: Ein anderer Thread könnte initialisieren
resource = create_resource();
initialized = 1; // Kann von CPU/Compiler umgeordnet werden
}
return resource; // Kann teilweise initialisierte Ressource zurückgeben
}
Lösungscode
// Behoben: Ordnungsgemäß synchronisierter Zähler
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;
public class SecureCounter {
// Behoben: Atomaren Typ für thread-sichere Operationen verwenden
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Atomare Operation
}
public int getCount() {
return count.get();
}
}
// Behoben: Thread-sicherer Cache mit ordnungsgemäßer Synchronisierung
public class SecureUserCache {
// Behoben: ConcurrentHashMap für thread-sichere Operationen verwenden
private ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>();
public User getUser(String userId) {
// Behoben: computeIfAbsent ist atomar
return userCache.computeIfAbsent(userId, id -> loadUserFromDatabase(id));
}
public void updateUser(User user) {
userCache.put(user.getId(), user);
}
}
// Behoben: Synchronisierte Kontooperationen
public class SecureAccountManager {
private final Map<String, Double> balances = new HashMap<>();
private final Object lock = new Object();
public boolean withdraw(String accountId, double amount) {
// Behoben: Check-then-act synchronisieren
synchronized (lock) {
Double balance = balances.get(accountId);
if (balance != null && balance >= amount) {
balances.put(accountId, balance - amount);
return true;
}
return false;
}
}
// Behoben: Thread-sichere Überweisung
public boolean transfer(String fromId, String toId, double amount) {
synchronized (lock) {
Double fromBalance = balances.get(fromId);
if (fromBalance != null && fromBalance >= amount) {
balances.put(fromId, fromBalance - amount);
balances.merge(toId, amount, Double::sum);
return true;
}
return false;
}
}
}
// Behoben: Thread-sichere verzögerte Initialisierung
public class SecureSingleton {
// Behoben: Volatile stellt Sichtbarkeit über Threads hinweg sicher
private static volatile SecureSingleton instance;
private SecureSingleton() {}
public static SecureSingleton getInstance() {
// Behoben: Double-checked Locking mit volatile
if (instance == null) {
synchronized (SecureSingleton.class) {
if (instance == null) {
instance = new SecureSingleton();
}
}
}
return instance;
}
}
# Behoben: Ordnungsgemäß synchronisierter Python-Code
import threading
from threading import Lock, RLock
class SecureCounter:
def __init__(self):
self._count = 0
self._lock = Lock()
def increment(self):
# Behoben: Lock für Synchronisierung verwenden
with self._lock:
self._count += 1
def get_count(self):
with self._lock:
return self._count
# Behoben: Thread-sicheres Singleton mit Lock
class SecureSingleton:
_instance = None
_lock = Lock()
@classmethod
def get_instance(cls):
# Behoben: Double-checked Locking mit Lock
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
# Behoben: Thread-sicheres Konto mit atomaren Operationen
class SecureAccount:
def __init__(self, balance):
self._balance = balance
self._lock = RLock() # Reentranter Lock für verschachtelte Aufrufe
@property
def balance(self):
with self._lock:
return self._balance
def transfer_to(self, target, amount):
# Behoben: Beide Konten in konsistenter Reihenfolge sperren um Deadlock zu verhindern
accounts = sorted([self, target], key=id)
with accounts[0]._lock:
with accounts[1]._lock:
if self._balance >= amount:
self._balance -= amount
target._balance += amount
return True
return False
// Behoben: Ordnungsgemäß synchronisierter C-Code
#include <pthread.h>
#include <stdio.h>
#include <stdatomic.h>
// Behoben: Atomaren Typ für einfachen Zähler verwenden
atomic_int secure_counter = 0;
void* secure_increment(void* arg) {
for (int i = 0; i < 100000; i++) {
// Behoben: Atomares Inkrement
atomic_fetch_add(&secure_counter, 1);
}
return NULL;
}
// Behoben: Synchronisierte verkettete Liste mit Mutex
struct Node {
int data;
struct Node* next;
};
struct Node* head = NULL;
pthread_mutex_t list_mutex = PTHREAD_MUTEX_INITIALIZER;
void secure_add_node(int data) {
struct Node* new_node = malloc(sizeof(struct Node));
new_node->data = data;
// Behoben: Vor Modifizierung gemeinsam genutzter Liste sperren
pthread_mutex_lock(&list_mutex);
new_node->next = head;
head = new_node;
pthread_mutex_unlock(&list_mutex);
}
// Behoben: Thread-sichere verzögerte Initialisierung mit pthread_once
pthread_once_t init_once = PTHREAD_ONCE_INIT;
void* resource = NULL;
void init_resource() {
resource = create_resource();
}
void* get_resource_secure() {
// Behoben: pthread_once garantiert einmalige Initialisierung
pthread_once(&init_once, init_resource);
return resource;
}
// Behoben: Read-Write-Lock für leselastige Workloads
pthread_rwlock_t cache_lock = PTHREAD_RWLOCK_INITIALIZER;
struct CacheEntry* cache = NULL;
struct CacheEntry* read_cache(const char* key) {
// Behoben: Mehrere Leser erlaubt
pthread_rwlock_rdlock(&cache_lock);
struct CacheEntry* entry = find_entry(cache, key);
pthread_rwlock_unlock(&cache_lock);
return entry;
}
void write_cache(const char* key, void* value) {
// Behoben: Exklusiver Schreibzugriff
pthread_rwlock_wrlock(&cache_lock);
update_entry(&cache, key, value);
pthread_rwlock_unlock(&cache_lock);
}
CVE-Beispiele
- CVE-2021-21224: V8 JavaScript-Engine Race-Condition bei Typverwechslung.
- CVE-2019-11815: Linux-Kernel Race-Condition in net/rds/tcp.c führte zu Use-after-free.
- CVE-2016-9793: Linux-Kernel Race-Condition bei SCTP-Socket-Behandlung.
Referenzen
- MITRE Corporation. "CWE-567: Unsynchronized Access to Shared Data in a Multithreaded Context." https://cwe.mitre.org/data/definitions/567.html
- Oracle. "Java Concurrency Tutorial."
- CERT. "CON00-J: Synchronize access to shared mutable data."