Race condition terjadi ketika hasil eksekusi bergantung pada urutan timing antar proses atau thread. Bentuk yang paling sering dijumpai di C adalah TOCTOU—Time-Of-Check to Time-Of-Use—di mana program memeriksa suatu kondisi (apakah file ada? apakah buffer cukup?) dan kemudian menggunakan hasil pemeriksaan tersebut untuk melakukan aksi (membuka file, menyalin data). Pada jendela waktu antara cek dan pakai, kondisi dapat berubah. CWE-367 (Time-of-Check Time-of-Use Race Condition) mencatat banyak kasus pada daemon privilege dan utility SUID. Pada chapter ini kita membandingkan pola TOCTOU rentan dengan padanannya yang atomik melalui O_CREAT | O_EXCL.

TOCTOU pada Pengecekan File #

Pola rentan paling klasik adalah pengecekan keberadaan file sebelum membuat file tersebut:

c
/* VULNERABLE */
if (access(path, F_OK) == 0) {
    /* file sudah ada — keluar */
} else {
    FILE *f = fopen(path, "w");  /* WINDOW: file bisa dibuat lain di sini */
    ...
}

Antara access() dan fopen(), proses lain dapat membuat symlink dengan nama yang sama yang menunjuk ke file target (misalnya /etc/passwd). Saat fopen dieksekusi, program kita justru menulis ke file target—arbitrary file write. Solusinya adalah operasi atomik: open(path, O_CREAT | O_EXCL, mode) yang melakukan check-and-create dalam satu panggilan sistem.

c
/* race_conditions.c */
/* TOCTOU pada pengecekan keberadaan file; solusi dengan O_CREAT|O_EXCL. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>

#define LOCK_FILE "/tmp/test_12a_race.lock"

/* VULNERABLE: TOCTOU — cek dulu, baru buat.
   Antar cek() dan create() ada jendela race condition. */
static int vulnerable_create(void) {
    if (access(LOCK_FILE, F_OK) == 0) {
        printf("[VULN] file sudah ada (access())\n");
        return 0;
    }
    /* WINDOW: bisa saja file dibuat oleh proses lain di sini. */
    FILE *f = fopen(LOCK_FILE, "w");
    if (f == NULL) {
        printf("[VULN] fopen gagal: %s\n", strerror(errno));
        return 0;
    }
    fputs("dibuat oleh vulnerable_create\n", f);
    fclose(f);
    printf("[VULN] file dibuat dengan fopen()\n");
    return 1;
}

/* AMAN: atomic create dengan O_CREAT|O_EXCL.
   Bila file sudah ada, open() gagal dengan EEXIST tanpa jendela race. */
static int safe_create(void) {
    int fd = open(LOCK_FILE, O_CREAT | O_EXCL | O_WRONLY, 0600);
    if (fd < 0) {
        if (errno == EEXIST) {
            printf("[SAFE] file sudah ada (O_EXCL)\n");
        } else {
            printf("[SAFE] open gagal: %s\n", strerror(errno));
        }
        return 0;
    }
    /* Tulis isi file dengan write() langsung pada fd. */
    const char *msg = "dibuat oleh safe_create\n";
    write(fd, msg, strlen(msg));
    close(fd);
    printf("[SAFE] file dibuat secara atomik (O_CREAT|O_EXCL, mode 0600)\n");
    return 1;
}

int main(void) {
    /* Bersihkan sisa test sebelumnya. */
    unlink(LOCK_FILE);

    printf("=== Iterasi 1: kondisi awal (file belum ada) ===\n");
    vulnerable_create();
    unlink(LOCK_FILE);
    safe_create();

    printf("\n=== Iterasi 2: file sudah ada ===\n");
    vulnerable_create();
    safe_create();

    /* Periksa permission file. */
    struct stat st;
    if (stat(LOCK_FILE, &st) == 0) {
        printf("\nPermission file: %04o (diharapkan 0600)\n",
               st.st_mode & 0777);
    }

    /* Bersih-bersih. */
    unlink(LOCK_FILE);
    return 0;
}

Output yang Dihasilkan:

text
=== Iterasi 1: kondisi awal (file belum ada) ===
[VULN] file dibuat dengan fopen()
[SAFE] file dibuat secara atomik (O_CREAT|O_EXCL, mode 0600)

=== Iterasi 2: file sudah ada ===
[VULN] file sudah ada (access())
[SAFE] file sudah ada (O_EXCL)

Permission file: 0600 (diharapkan 0600)

Mari kita telaah baris-baris kode di atas:

  1. Baris 14-26 (vulnerable_create): Pola TOCTOU klasik. access(LOCK_FILE, F_OK) memeriksa apakah file ada. Bila tidak ada, fungsi lanjut ke fopen(LOCK_FILE, "w"). Pada jendela antara dua panggilan ini—mikrosekon di CPU modern—proses lain dapat membuat symlink dengan nama yang sama. Ketika fopen dieksekusi, program kita menulis ke target symlink alih-alih file kunci yang dimaksud. Output Iterasi 1 mengonfirmasi perilaku normal: file belum ada, dibuat. Output Iterasi 2 menunjukkan access mendeteksi file dan fungsi mengembalikan 0—tetapi pendeteksian ini bersifat rentan.
  2. Baris 29-44 (safe_create): Operasi atomik dengan open(O_CREAT | O_EXCL | O_WRONLY, 0600). Kernel memeriksa keberadaan file dan menciptakannya dalam satu langkah yang tidak dapat di-interleave. Bila file sudah ada, open mengembalikan -1 dengan errno == EEXIST—tidak ada jendela untuk serangan symlink. Argumen mode 0600 juga diterapkan secara atomik (lihat juga umask caveat di bawah).
  3. Baris 47-49 (Iterasi 1, vulnerable): Pada iterasi pertama, file belum ada. access mengembalikan -1, fopen berhasil membuat file. Tidak ada serangan yang terlihat—tetapi kerentanan hadir bila proses lain berlomba.
  4. Baris 50-52 (Iterasi 1, safe): unlink(LOCK_FILE) menghapus file yang dibuat versi rentan. safe_create lalu membuat file dengan operasi atomik. Output [SAFE] file dibuat secara atomik mengonfirmasi keberhasilan.
  5. Baris 54-57 (Iterasi 2): File masih ada dari Iterasi 1. access mendeteksi file dan keluar lebih awal. safe_create juga mendeteksi file melalui EEXIST. Perbedaan kunci: versi rentan menyalahkan kondisi file pada satu waktu, versi aman menyalahkan operasi atomic pada waktu yang sama dengan penggunaan.

Counter Atomik dengan stdatomic.h #

Race condition tidak hanya muncul pada file—juga pada variabel shared antar thread. C11 menyediakan pustaka <stdatomic.h> dengan operasi atomik yang portable:

c
/* extra_race_conditions.c — atomic counter dengan stdatomic.h. */
#include <stdio.h>
#include <stdatomic.h>

static _Atomic int counter = 0;

static void increment_safe(void) {
    atomic_fetch_add(&counter, 1);
}

int main(void) {
    for (int i = 0; i < 1000; i++) {
        increment_safe();
    }
    printf("Counter setelah 1000 inkremen atomik: %d\n", atomic_load(&counter));

    int expected = 1000;
    int desired  = 0;
    /* Compare-and-swap atomik */
    if (atomic_compare_exchange_strong(&counter, &expected, desired)) {
        printf("CAS sukses: counter direset ke %d\n", atomic_load(&counter));
    } else {
        printf("CAS gagal: nilai aktual = %d (expected %d)\n",
               atomic_load(&counter), expected);
    }
    return 0;
}

Output yang Dihasilkan:

text
Counter setelah 1000 inkremen atomik: 1000
CAS sukses: counter direset ke 0

atomic_fetch_add mengganti operasi counter++ yang berbahaya pada multi-thread. Compare-and-swap (atomic_compare_exchange_strong) adalah primitif untuk membangun lock-free data structure: bila nilai counter masih sama dengan expected, ganti dengan desired secara atomik—bila tidak, kembalikan false tanpa modifikasi.

Umask dan Mode yang Sebenarnya Diterapkan #

Pada safe_create di atas, kita menulis 0600 sebagai argumen mode. Tetapi mode yang sebenarnya diterapkan adalah 0600 & ~umask. Bila umask pengguna adalah 022, mode final adalah 0600 & ~022 = 0600—aman. Bila umask adalah 077, mode final adalah 0600 & ~077 = 0600—masih aman. Namun bila umask adalah 000, mode yang diterapkan tetap 0600 karena 0600 & ~000 = 0600. Untuk memastikan mode presisi terlepas dari umask, panggil umask(0); sebelum open, atau fchmod(fd, 0600) setelah open berhasil.

TOCTOU pada Variabel Shared: Mutex dan Atomic #

Race condition juga muncul pada variabel yang diakses beberapa thread:

c
/* VULNERABLE pada multi-thread */
static int counter = 0;
void increment(void) {
    counter++;  /* read-modify-write tidak atomik */
}

/* AMAN */
#include <stdatomic.h>
static _Atomic int counter = 0;
void increment(void) {
    atomic_fetch_add(&counter, 1);  /* atomik */
}

counter++ membaca, menambah, dan menulis—tiga langkah yang dapat di-interleave antar thread. atomic_fetch_add melakukan ketiga langkah dalam satu instruksi mesin (LOCK XADD pada x86) yang tidak dapat di-interleave.

Daftar Operasi Atomik yang Disarankan #

Pola RentanPola Aman
access() + fopen()open(O_CREAT|O_EXCL)
stat() + open()open() lalu fstat(fd)
readlink() + open()open(O_NOFOLLOW)
tmpnam() + fopen()mkstemp()
counter++ (multi-thread)atomic_fetch_add(&counter, 1)
if (ptr == NULL) ptr = malloc() (lazy init)pthread_once() atau _Atomic(ptr)