timing, not logic, picks the answer
Race Conditions
Two threads read-modify-write the same state, and the order they happen to interleave — not your code — chooses what survives.
When two actors touch one value at once, the interleaving decides the (wrong) result.
01in the wild
In the wild
Lost-Update Counter
Two actors read-modify-write the same value, and the interleaving loses updates.
example.py
import threading
counter = 0
def bump():
global counter
for _ in range(100_000):
counter += 1 # read, +1, write -- not atomic
# Two threads race; final value is < 200000
t1, t2 = threading.Thread(target=bump), threading.Thread(target=bump)
# RIGHT: a lock makes the read-modify-write atomic
lock = threading.Lock()
def bump_safe():
global counter
for _ in range(100_000):
with lock:
counter += 1counter += 1 is three operations. Without a lock, two threads interleave and lose updates.
// observed
racy: counter = 137422 (lost updates) locked: counter = 200000 (correct)
example.go
// Detect races before they ship: go test -race
var count int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() { count++; wg.Done() }() // -race flags this
}
// RIGHT: a mutex, or a channel to serialize access
var mu sync.Mutex
go func() { mu.Lock(); count++; mu.Unlock(); wg.Done() }()Go ships a race detector. Run it in CI and the data race fails the build instead of production.
// observed
go test -race: WARNING: DATA RACE with mutex: clean, deterministic
example.rs
// Rust refuses to compile a data race. Shared + mutable
// must go through a synchronizing type.
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || { *c.lock().unwrap() += 1; });The borrow checker enforces 'shared XOR mutable' at compile time. A data race is not expressible.
// observed
raw shared &mut across threads: won't compile Arc<Mutex<T>>: safe, checked statically
Check-Then-Act (TOCTOU)
The gap between testing a condition and acting on it is where a second actor slips in.
example.java
// SMELL: check-then-act across threads
if (instance == null) { // thread A and B both see null
instance = new Service(); // two instances created
}
// RIGHT: atomic or properly synchronized init
private static final AtomicReference<Service> ref = new AtomicReference<>();
ref.compareAndSet(null, new Service());The gap between the check and the act is where the race lives. compareAndSet closes it atomically.
// observed
naive: two Service instances under load CAS: exactly one wins, the rest reuse it
example.py
# SMELL: check-then-act on shared state
if username not in users: # two requests both see it free
users[username] = create() # both create -> duplicate user
# RIGHT: make the check-and-set one atomic step
with lock: # (or a DB unique constraint)
if username not in users:
users[username] = create()Reading 'is it free?' and writing 'claim it' must be one indivisible step, or two callers both win the check.
// observed
unlocked: two duplicate accounts created locked: one wins, the other sees it taken
02cross-pollination
Where this compounds
Nondeterminism
- TOCTOU Race (Check-Then-Act) × Improper Initialization
- Predictable ID Collision Under Load × Time, Money & Entropy
Data Corruption
- Torn Write / Lost-Update Corruption × Lack of Input Validation
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.