TOCTOU race synthesizer
Four check-then-act race detectors. They cover the highest-impact race shapes beyond the file races the existing filesystem detector already handles. Each rule fires when the check and the act sit in the same function body with no atomic guard between them.
| Rule | Shape | Severity |
|---|---|---|
| TOCTOU-DB-001 | SELECT ... FROM x WHERE id = ? followed by UPDATE x SET ... WHERE id = ? with no FOR UPDATE, with_lock, version stamp, or transaction-level isolation between them | High |
| TOCTOU-BALANCE-001 | balance >= amount check followed by balance -= amount / UPDATE wallets SET balance = balance - ? WHERE id = ? without the check folded into the same UPDATE's WHERE | Critical |
| TOCTOU-QUEUE-001 | SELECT * FROM jobs WHERE status = 'pending' followed by UPDATE jobs SET status = 'running' WHERE id = ? without a WHERE status = 'pending' predicate | High |
| TOCTOU-IDOR-ACL-001 | can_access(user, resource_id) / is_owner(...) / has_permission(...) followed by a mutation on the same id without a refetch + recheck under transaction | Medium |
TOCTOU-BALANCE-001: the double-spend shape
This is the classic gift-card / wallet / credit-pool race:
async def pay(user_id, amount):
if balance >= amount: # check
balance -= amount # act
db.execute("UPDATE wallets SET balance = ? WHERE id = ?", (balance, user_id))
Two concurrent requests both pass the check. Both subtract the
amount. The wallet goes negative by amount while the user gets
two purchases.
Fix. Fold the check into the act:
async def pay(user_id, amount):
result = db.execute(
"UPDATE wallets SET balance = balance - $1 WHERE id = $2 AND balance >= $1",
(amount, user_id),
)
if result.rowcount != 1:
raise InsufficientBalance()
The database's row-level locking now guarantees the check and the act are atomic.
Pairs with
- The filesystem check-then-act (TOCTOU) detector for the filesystem-shaped race.
- Reverse-reach to find every entry point that can race a flagged check-then-act pair.