Skip to content

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:

$ gcc -fsanitize=thread -g race.c -o race -lpthread
$ ./race

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
Her iki thread de 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.