Detecting race conditions
Race bugs don't live in one line - they live in the gap between two operations
that another actor can slip through. There's no tainted value to trace and no
single sink to grep; you find them by asking what state is shared, what runs
concurrently, and where a window opens between deciding something and acting on
it. This skill covers the recurring shapes and how to confirm a real window.
When to use
- Multithreaded / async / multiprocess code, or a shared resource (DB row, cache,
counter, file, in-memory map) touched by concurrent requests.
- A "check then act" sequence where the check's truth can expire before the act:
balance/quota checks, auth-then-use, uniqueness/dedup guards, file existence
checks, one-time tokens.
Scope check
Authorized source only. If you can't name the authorization, stop.
The shapes and how to confirm each
Confirmation of a race is an interleaving witness: two concrete operation
orders where one is safe and the other, achievable by an attacker, is not.
TOCTOU (time-of-check to time-of-use). State is validated, then used, and
it can change in between. Classic filesystem form: access(path) /
stat(path) then open(path) - a symlink swap in the window redirects the
open. General form: any check(x); … ; use(x) where x (or what it names) is
mutable by another actor in the gap. Confirm: identify the shared thing, show a
writer that can change it between check and use.
Check-then-act on shared state without atomicity. "If not exists, create";
"if balance ≥ n, deduct n"; "if not already redeemed, redeem." Two requests
both pass the check before either acts → double-spend, duplicate creation,
coupon reuse. Confirm: two concurrent runs both read the pre-act value; neither
the read+act is atomic (no transaction, lock, SELECT … FOR UPDATE, compare-
and-swap, or unique constraint).
Unsynchronized shared mutable state. A field/map/counter read and written
by multiple threads with no lock/atomic. Confirm: two threads reach the same
memory, at least one writes, no happens-before relationship (no shared lock,
no atomic, no ownership handoff) orders them.
Atomicity violation across a compound update. Several fields that must move
together are updated without a single critical section, so a concurrent reader
sees a torn, inconsistent state. Confirm: a reader path observes the fields
mid-update.
Lock misuse. Right idea, wrong execution: the lock is taken after the
shared access, a different lock guards read vs write, the lock is released
early, or the critical section doesn't cover the whole check-then-act. Confirm:
the guarded region doesn't actually span both the decision and the action.
Where to look
Shared sinks are the anchors: process-global/static mutable state, singleton
caches, ORM objects reused across requests, filesystem paths derived from a check,
and any DB write preceded by an application-level read that decides it. For each,
ask: who else touches this concurrently, and is the decision+action atomic?
Worked example
Double-spend via check-then-act. redeem(code):
row = db.get(code); if row.used: reject; … ; row.used = True; db.save(row).
Two requests with the same code both read used == False before either saves.
Witness: interleave R1.read, R2.read, R1.save, R2.save → both succeed.
No transaction, no SELECT … FOR UPDATE, no unique/CAS on used. Confirmed
atomicity/check-then-act race, severity high, impact = coupon/token reused N
times. Remediation: atomic conditional update (UPDATE … SET used=true WHERE code=? AND used=false) and act on affected-row-count.
Rationalizations to reject
- "It's guarded by a check right above." → A check above the act is exactly the
TOCTOU shape unless the check+act is atomic. The gap is the bug.
- "There's a lock in the function." → Does the critical section span both the
decision and the action, on every path, with the same lock as the writer?
- "Requests are basically serial in practice." → "Basically" is the window.
Assume concurrent unless the runtime guarantees otherwise.
- "The DB will sort it out." → Only with a transaction at the right isolation,
a constraint, or a conditional update. A plain read-then-write won't.
Executing this in practice
You need to identify shared state and the concurrent entry points that reach it,
then read the ordering around each access. A code property graph helps you find
every reader/writer of a shared object and every path into a check-then-act; a
concurrency-aware analyzer or a targeted stress/PoC harness corroborates a real
window. State each finding as an interleaving witness and emit per the
finding schema, with the two operation orders in
evidence and reachable = conditional on the interleaving.
Related
auditing-guard-gaps - when only some paths take the lock/transaction.
hunting-bugs-with-a-code-graph - the master loop that routes these here.
- FINDING-SCHEMA.md.
1---2name: detecting-race-conditions3description: Find concurrency and time-of-check/time-of-use bugs - TOCTOU, unsynchronized shared state, check-then-act, and atomicity violations - by reasoning about what state is shared, what can interleave, and where a window opens between a check and its use. Use on an authorized source target when the risk is ordering, not a single tainted value; when reviewing multithreaded code, shared caches/counters, filesystem checks, or "verify then act" sequences (balance checks, auth-then-use, dedup guards). Confirms each as an interleaving witness and emits the shared finding schema.4license: MIT5---67# Detecting race conditions89Race bugs don't live in one line - they live in the *gap* between two operations10that another actor can slip through. There's no tainted value to trace and no11single sink to grep; you find them by asking what state is shared, what runs12concurrently, and where a window opens between deciding something and acting on13it. This skill covers the recurring shapes and how to confirm a real window.1415## When to use1617- Multithreaded / async / multiprocess code, or a shared resource (DB row, cache,18 counter, file, in-memory map) touched by concurrent requests.19- A "check then act" sequence where the check's truth can expire before the act:20 balance/quota checks, auth-then-use, uniqueness/dedup guards, file existence21 checks, one-time tokens.2223## Scope check2425Authorized source only. If you can't name the authorization, stop.2627## The shapes and how to confirm each2829Confirmation of a race is an **interleaving witness**: two concrete operation30orders where one is safe and the other, achievable by an attacker, is not.31321. **TOCTOU (time-of-check to time-of-use).** State is validated, then used, and33 it can change in between. Classic filesystem form: `access(path)` /34 `stat(path)` then `open(path)` - a symlink swap in the window redirects the35 open. General form: any `check(x); … ; use(x)` where `x` (or what it names) is36 mutable by another actor in the gap. Confirm: identify the shared thing, show a37 writer that can change it between check and use.38392. **Check-then-act on shared state without atomicity.** "If not exists, create";40 "if balance ≥ n, deduct n"; "if not already redeemed, redeem." Two requests41 both pass the check before either acts → double-spend, duplicate creation,42 coupon reuse. Confirm: two concurrent runs both read the pre-act value; neither43 the read+act is atomic (no transaction, lock, `SELECT … FOR UPDATE`, compare-44 and-swap, or unique constraint).45463. **Unsynchronized shared mutable state.** A field/map/counter read and written47 by multiple threads with no lock/atomic. Confirm: two threads reach the same48 memory, at least one writes, no happens-before relationship (no shared lock,49 no atomic, no ownership handoff) orders them.50514. **Atomicity violation across a compound update.** Several fields that must move52 together are updated without a single critical section, so a concurrent reader53 sees a torn, inconsistent state. Confirm: a reader path observes the fields54 mid-update.55565. **Lock misuse.** Right idea, wrong execution: the lock is taken *after* the57 shared access, a different lock guards read vs write, the lock is released58 early, or the critical section doesn't cover the whole check-then-act. Confirm:59 the guarded region doesn't actually span both the decision and the action.6061## Where to look6263Shared sinks are the anchors: process-global/static mutable state, singleton64caches, ORM objects reused across requests, filesystem paths derived from a check,65and any DB write preceded by an application-level read that decides it. For each,66ask: *who else touches this concurrently, and is the decision+action atomic?*6768## Worked example6970> **Double-spend via check-then-act.** `redeem(code)`:71> `row = db.get(code); if row.used: reject; … ; row.used = True; db.save(row)`.72> Two requests with the same `code` both read `used == False` before either saves.73> Witness: interleave R1.read, R2.read, R1.save, R2.save → both succeed.74> No transaction, no `SELECT … FOR UPDATE`, no unique/CAS on `used`. **Confirmed75> atomicity/check-then-act race**, severity high, impact = coupon/token reused N76> times. Remediation: atomic conditional update (`UPDATE … SET used=true WHERE77> code=? AND used=false`) and act on affected-row-count.7879## Rationalizations to reject8081- *"It's guarded by a check right above."* → A check above the act is exactly the82 TOCTOU shape unless the check+act is atomic. The gap is the bug.83- *"There's a lock in the function."* → Does the critical section span *both* the84 decision and the action, on every path, with the *same* lock as the writer?85- *"Requests are basically serial in practice."* → "Basically" is the window.86 Assume concurrent unless the runtime guarantees otherwise.87- *"The DB will sort it out."* → Only with a transaction at the right isolation,88 a constraint, or a conditional update. A plain read-then-write won't.8990## Executing this in practice9192You need to identify shared state and the concurrent entry points that reach it,93then read the ordering around each access. A code property graph helps you find94every reader/writer of a shared object and every path into a check-then-act; a95concurrency-aware analyzer or a targeted stress/PoC harness corroborates a real96window. State each finding as an interleaving witness and emit per the97[finding schema](../../FINDING-SCHEMA.md), with the two operation orders in98`evidence` and `reachable = conditional` on the interleaving.99100## Related101102- `auditing-guard-gaps` - when only *some* paths take the lock/transaction.103- `hunting-bugs-with-a-code-graph` - the master loop that routes these here.104- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md).