Race condition¶
İki veya daha fazla code sequence, paylaşılan bir resource'a zorunlu exclusivity/atomicity olmadan erişir ve bir timing window birinin diğerine müdahale etmesine izin verir — CWE-362.
Mechanism¶
Invariant
Paylaşılan bir resource üzerindeki doğru bir concurrent operation, bir race condition'ın ihlal ettiği iki garantiye ihtiyaç duyar:
- Exclusivity — bir code sequence resource üzerinde çalışırken hiçbir başka sequence onu modify edemez.
- Atomicity — resource üzerindeki işlemler tek ve bölünemez bir birim olarak gerçekleşir; hiçbir concurrent thread/process aynı resource'a karşı "ortada" çalışamaz.
CWE-362 bir race condition'ı şöyle tanımlar: "paylaşılan bir resource'a geçici, exclusive erişim gerektiren bir concurrent code sequence, fakat paylaşılan resource'un eşzamanlı çalışan başka bir code sequence tarafından modify edilebileceği bir timing window mevcuttur." Bu window — bir sequence resource'u kullanmaya başladığı ile bitirdiği an arasında — istismar edilebilir boşluktur. Bir interfering code sequence (ikinci bir thread, başka bir process ya da attacker-controlled kod) araya sızar ve ilk sequence'ın stabil sandığı state'i değiştirir.
Memory-safety bug'ları (use-after-free, double-free, out-of-bounds), bir check ile onun koruduğu kullanım atomic olmadığında erişilebilir hâle gelir: resource check'i geçer, attacker araya girip onu geçersizleştirir, ardından kullanım artık geçersiz olan state üzerinde devam eder. Bir race'in nadiren nihai hedef olmasının sebebi budur — o, zararsız bir code path'i kontrollü bir anda bir UAF/OOB'ye çeviren primitive'dir. Kernel'de bu TOCTOU ve multi-variable race sınıfıdır; userspace'te ise shared global'ler, tahmin edilebilir adlı dosyalar ve unsynchronized data structure'lardır.
Walkthrough¶
Kanonik gösterici örnek (CWE-362'den) paylaşılan bir counter üzerindeki lost-update race'tir. İki thread de bir balance okur, hesaplar ve geri yazar — lock olmadan read/modify/write atomic değildir:
#include <pthread.h>
#include <stdio.h>
long balance = 100; /* shared resource */
void *withdraw(void *arg) {
long b = balance; /* CHECK/READ */
if (b >= 100) { /* timing window opens here */
b = b - 100; /* compute */
balance = b; /* WRITE — non-atomic with the read */
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, withdraw, NULL);
pthread_create(&t2, NULL, withdraw, NULL); /* both read 100 before either writes */
pthread_join(t1, NULL); pthread_join(t2, NULL);
printf("balance = %ld\n", balance);
return 0;
}
Şansa bel bağlamak yerine race'i gözlemlenebilir kılmak için ThreadSanitizer ile derle:
Beklenen: TSan balance üzerinde bir data race raporlar ve koşular arasında nihai balance non-deterministic olur (100'den iki kez 100-çekim imkânsız olması gerekirken çoğu zaman 0):
ThreadSanitizer çıktısı (kısaltılmış)
==================
WARNING: ThreadSanitizer: data race (pid=5123)
Write of size 8 at 0x... by thread T2:
#0 withdraw race.c:12
Previous read of size 8 at 0x... by thread T1:
#0 withdraw race.c:8
Location is global 'balance' of size 8
==================
balance = 0
100 okur, her ikisi de >= 100 check'ini geçer, her ikisi de 0 yazar: bir çekim sessizce kayboldu. Aynı şekil — bir window boyunca hayatta kalan stale bir değer — "değer" free edilmiş bir pointer olduğunda bir UAF'yi mümkün kılan şeydir.
Bruteforce her zaman yeterli değildir
Single-variable bir race genelde brute force'a teslim olur (sıra değişene kadar iki tarafı bir loop'ta döndür). Multi-variable race'ler, bir thread'deki iki memory access'in ikisinin de diğer thread'in window'unun içine düşmesi gerektiğinden, şans eseri isabet ettirmek pratikte imkânsız olabilir — thread başına execution süreleri örtüşmez. Gerçek exploit'ler window'u bilerek genişletir (CPU pinning, cache pressure, page fault'lar, scheduler manipülasyonu ya da interrupt yükseltme) ki |T1 + T_extend| > |T2| olsun.
Detection¶
- ThreadSanitizer (
-fsanitize=thread) ve Helgrind/DRD (Valgrind), runtime'da unsynchronized shared-memory access'leri işaretler. - Statik analiz (CodeQL, Coverity), tutulan bir lock olmadan shared state üzerindeki check-then-act ve read-modify-write pattern'lerini işaretler.
- Kod review: bir lock dışında dokunulan shared global'leri/dosyaları ve koruduğu kullanımdan herhangi bir blocking/yielding çağrıyla ayrılmış bir güvenlik check'ini ara (TOCTOU).
Mitigation¶
- Critical section'ı atomic yap: tüm read-modify-write boyunca bir mutex/spinlock tut ya da atomic operation'lar / compare-and-swap kullan.
- Window'u ortadan kaldır: private bir kopya üzerinde ya da kullanım ortasında geri alınamayan bir handle üzerinde çalış (örn. bir path'i yeniden çözmek yerine bir fd'yi bir kez aç ve onun üzerinde çalış —
openat/O_NOFOLLOW). - Shared mutable state yerine message passing ya da immutable data tercih et.
References¶
Bu primitive üzerine kurulan ilgili kernel-kategorisi teknikleri: scheduler-based race exploitation, EXPRACE interrupt-raising kernel race, double-fetch user/kernel TOCTOU.