Skip to main content

TOCTOU race synthesizer

Four check-then-act race detectors that cover the highest-impact shapes outside the file-race surface the existing detector already handles. Each rule emits when the check and the act sit in the same function body without an atomic guard between them.

RuleShapeSeverity
TOCTOU-DB-001SELECT ... FROM x WHERE id = ? followed by UPDATE x SET ... WHERE id = ? with no FOR UPDATE, with_lock, version stamp, or transaction-level isolation between themHigh
TOCTOU-BALANCE-001balance >= amount check followed by balance -= amount / UPDATE wallets SET balance = balance - ? WHERE id = ? without the check folded into the same UPDATE's WHERECritical
TOCTOU-QUEUE-001SELECT * FROM jobs WHERE status = 'pending' followed by UPDATE jobs SET status = 'running' WHERE id = ? without a WHERE status = 'pending' predicateHigh
TOCTOU-IDOR-ACL-001can_access(user, resource_id) / is_owner(...) / has_permission(...) followed by a mutation on the same id without a refetch + recheck under transactionMedium

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

  • toctou_file_race.rs (existing detector) for the filesystem-shaped race.
  • Reverse-reach to find every entry point that can race a flagged check-then-act pair.