# Race Conditions and TOCTOU ## Rule Check-then-act must be atomic. Never trust state between check and use. **Source:** [CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization](https://cwe.mitre.org/data/definitions/362.html) ## TOCTOU (Time-of-Check to Time-of-Use) ``` Thread A: check(x) --> use(x) Thread B: modify(x) ^-- state changes between check and use ``` ## Correct Pattern ```python import threading from contextlib import contextmanager # Pattern 1: Atomic check-and-act with locking class BankAccount: def __init__(self, balance: Decimal): self.balance = balance self._lock = threading.Lock() def withdraw(self, amount: Decimal) -> bool: """Atomic withdrawal - no race window.""" with self._lock: if self.balance >= amount: self.balance -= amount return True return False # Pattern 2: Database-level atomicity def transfer_funds(conn, from_id: int, to_id: int, amount: Decimal): """Use database transaction + row locks.""" with conn.begin(): # SELECT FOR UPDATE prevents concurrent modification from_acct = conn.execute( "SELECT balance FROM accounts WHERE id = %s FOR UPDATE", (from_id,) ).fetchone() if from_acct.balance < amount: raise InsufficientFunds() conn.execute( "UPDATE accounts SET balance = balance - %s WHERE id = %s", (amount, from_id) ) conn.execute( "UPDATE accounts SET balance = balance + %s WHERE id = %s", (amount, to_id) ) # Pattern 3: Compare-and-swap (optimistic locking) def update_with_version(conn, item_id: int, new_data: dict, expected_version: int): """Fail if version changed since we read it.""" result = conn.execute( """UPDATE items SET data = %s, version = version + 1 WHERE id = %s AND version = %s""", (new_data, item_id, expected_version) ) if result.rowcount == 0: raise ConcurrentModificationError("Item was modified by another request") ``` ## Incorrect Pattern ```python # Wrong: check-then-act without atomicity class BankAccount: def withdraw(self, amount): if self.balance >= amount: # Check # Race window! Another thread can withdraw here self.balance -= amount # Act return True return False # Wrong: file race condition def safe_write(path, data): if not os.path.exists(path): # Check # Race window! File could be created here with open(path, 'w') as f: # Act f.write(data) # Wrong: double-checked locking (broken in many languages) _instance = None _lock = threading.Lock() def get_instance(): if _instance is None: # First check without lock with _lock: if _instance is None: # Second check _instance = ExpensiveObject() return _instance ``` ## File System Races ```python import os import tempfile # Wrong: check then create def create_file(path): if os.path.exists(path): raise FileExistsError() with open(path, 'w') as f: # Race! f.write("data") # Correct: atomic creation (fails if exists) def create_file_safe(path): fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) try: os.write(fd, b"data") finally: os.close(fd) # Wrong: temp file with predictable name def bad_temp(): path = f"/tmp/myapp_{os.getpid()}.tmp" # Predictable! with open(path, 'w') as f: f.write(secret_data) # Correct: secure temp file def good_temp(): fd, path = tempfile.mkstemp() try: os.write(fd, secret_data.encode()) finally: os.close(fd) os.unlink(path) ``` ## Signup / Registration Races ```python # Wrong: check username then create def register(username: str, password: str): if User.query.filter_by(username=username).first(): raise UsernameExists() # Race window! Another request could register same username user = User(username=username, password=hash(password)) db.session.add(user) db.session.commit() # Correct: use database constraint, handle exception def register_safe(username: str, password: str): user = User(username=username, password=hash(password)) db.session.add(user) try: db.session.commit() # UNIQUE constraint enforced here except IntegrityError: db.session.rollback() raise UsernameExists() ``` ## Coupon / Discount Races ```python # Wrong: check-then-apply coupon def apply_coupon(order_id: int, coupon_code: str): coupon = Coupon.query.filter_by(code=coupon_code).first() if coupon.uses_remaining <= 0: raise CouponExhausted() # Race window! 100 requests could pass the check simultaneously order = Order.query.get(order_id) order.discount = coupon.discount coupon.uses_remaining -= 1 db.session.commit() # Correct: atomic decrement with row lock def apply_coupon_safe(order_id: int, coupon_code: str): with db.session.begin(): result = db.session.execute( """UPDATE coupons SET uses_remaining = uses_remaining - 1 WHERE code = :code AND uses_remaining > 0 RETURNING discount""", {"code": coupon_code} ) row = result.fetchone() if not row: raise CouponExhausted() db.session.execute( "UPDATE orders SET discount = :discount WHERE id = :id", {"discount": row.discount, "id": order_id} ) ``` ## Edge Cases - Rate limiters with race conditions allow bursts - Session creation races can create duplicates - Inventory/stock decrements need atomic operations - Distributed systems need distributed locks (Redis, etcd) - File permission checks before open (symlink attacks) - Signal handlers can interrupt between check and use