A program assumes operations happen in the order it wrote them. The CPU disagrees. So does the kernel scheduler, the network stack, the filesystem, and any attacker patient enough to run a tight loop. Race conditions exist in the gap between when you check a thing and when you use it. The attacker's whole job is to make that gap exploitable.
A race condition isn't a special bug. It's an unavoidable consequence of four ordinary properties stacking up: shared mutable state, concurrent access, a non-atomic operation, and missing synchronisation. Remove any one of these four and the bug is impossible. Forget any one of them and the bug is inevitable.
A piece of data that more than one thread of execution can read or write — an integer in memory, a row in a database, a file on disk, a counter on a remote API.
int balance = 100; /* shared */
Two or more flows reach that state at overlapping times. Threads, processes, async callbacks, network requests. The CPU and scheduler decide the actual order — not your source code.
thread_create(worker_a);
thread_create(worker_b);
What looks like one statement is several machine steps. counter++ is read, add, write — three points where another thread can interrupt you and read or change the same value.
counter++;
/* mov, inc, mov */
Nothing forces one of those flows to finish before the other touches the state. No mutex, no atomic instruction, no transaction, no row lock, no file descriptor — just hope.
// no lock, no atomic, no fence
Plates i and ii establish the mechanics: serial code is safe, concurrent code without locks is not. Plates iii through v are the three classic exploitation patterns — counter races, file TOCTOU, and bank-style state corruption. Plate vi is the symlink trick that turns any "write to /tmp" into a privileged-file overwrite primitive.
A single thread of execution. Operations happen in the order written. counter++ means the entire read-add-write sequence completes before the next statement. This is the model every C tutorial assumes — and the model that fails the moment you add a second thread.
Nothing in the source code below changes when we go concurrent. What changes is everything around it.
Spawn two threads, each running the same loop a thousand times. Expected total: 2000. Actual total: somewhere between 1000 and 2000, varying every run. Why? Because counter++ is three machine instructions, and the OS can preempt you between any of them.
If both threads execute READ at the same moment, both compute the same +1, both write it back. Two increments collapse into one. The bug is silent — no exception, no crash, just a wrong number.
A privileged process verifies a property of the world (a file's owner, a permission bit, a balance), then acts on that property. Between verify and act, an attacker changes the world. The privileged process trusted the verification and acted on a stale fact.
The classic Unix example: access("/tmp/x", W_OK) followed by open("/tmp/x", O_WRONLY). Between them, the attacker swaps /tmp/x for a symlink to /etc/passwd. access() said yes. open() opens passwd. Privileged code writes attacker-chosen content.
Account balance is $100. Two simultaneous requests arrive, each asking to withdraw $80. The naive backend: read balance → check balance ≥ 80 → balance -= 80 → write balance. Both requests can pass the check (balance was $100 when they read it). Both subtract $80. Both write back $20.
$160 leaves the account. Final balance: $20 instead of -$60. The check told the truth. The check was just stale by the time the write happened. Real cases: cryptocurrency exchanges, in-app purchase double-redemption, gift-card refund duplication.
A setuid program creates a working file at a predictable path: /tmp/myprog.tmp. Between deciding the path and opening it, the attacker — running in a tight loop — deletes the file and replaces it with a symlink pointing at /etc/shadow. The privileged open(O_CREAT|O_WRONLY) follows the symlink and writes attacker-chosen content into shadow.
Mitigation: never use predictable paths (mkstemp is the safe API), never re-open by path after a check (use file descriptors), and pass O_NOFOLLOW when you must open by path.
Web applications have the same shape. Click "redeem coupon" 50 times in parallel; if the backend's "has user redeemed?" check isn't atomic with the redemption write, the user redeems 50 coupons. Submit two simultaneous "transfer to friend" requests; both pass the balance check; both succeed.
Modern frameworks have async checked-then-mutated patterns everywhere. The fix is the same as for threads: the check and the use must be one atomic step — usually a single SQL UPDATE … WHERE balance ≥ amount with a row lock, or a database transaction with serializable isolation.
A real interleaving simulator. Two threads, with their operations laid out as cards along parallel rails. The scheduler picks one thread at a time to advance; the shared resource updates as ops fire. Toggle synchronisation and watch the same workload produce correct results — or not.
No single defense covers every variant. Locks serialise threads. Atomics make a single read-modify-write uninterruptible. Compare-and-swap lets you retry under conflict. File descriptors close the path-rebind window. Database transactions push the problem onto a system that's good at it. The cheapest defense — design — is to avoid sharing in the first place.
Serialise the critical section. Only one thread holds the lock at a time; everyone else waits. Coarse but correct. The cost is contention: a busy lock becomes a bottleneck. Use the smallest possible critical section.
lock(); counter++; unlock();
Single-instruction primitives that read-modify-write without preemption. __atomic_fetch_add in C, AtomicInteger.incrementAndGet in Java. Free of contention up to the point where the cache line itself becomes hot.
__atomic_fetch_add(&counter, 1, __ATOMIC_SEQ_CST);
Lock-free pattern: read old value, compute new, attempt to swap, retry on conflict. Foundation of optimistic concurrency, used inside databases, schedulers, and language runtimes.
do { old = x; } while (!CAS(&x, old, old+1));
Once a file descriptor is open, the kernel binds you to that inode — symlinks and unlinks don't matter. Replace path-based check-then-use (access+open) with open+fstat+fchown+fchmod.
fd = open(p, O_NOFOLLOW); fstat(fd, &st);
Make the entire check-and-mutate one statement, or wrap it in a transaction with row-level locks (SELECT ... FOR UPDATE) or serializable isolation. The database becomes responsible for ordering.
UPDATE acc SET bal=bal-80 WHERE id=1 AND bal>=80;
The cheapest race is the one you don't have. Immutable data structures, single-writer/multi-reader patterns, message-passing actors, share-nothing parallelism. If the state isn't shared, it can't be raced.
// no shared state ↔ no race possible