Watch a race condition happen, then close it

short · 40 min · Objective 2.3

Task

Write a small program with a time-of-check to time-of-use flaw, demonstrate that parallel requests defeat its check, then fix it by making check and use atomic. TOCTOU is much clearer once you have watched a balance go negative.

Steps

  1. Write /tmp/voucher.py: it keeps a balance in a file, and a redeem() that READS the balance, checks it is greater than zero, sleeps briefly, then writes balance minus one. The sleep makes the window visible; in real code it is microseconds.
  2. Run ten redemptions sequentially against a balance of five and confirm it stops at zero, as intended.
  3. Now run ten redemptions CONCURRENTLY with threads, against a fresh balance of five. Record the final balance.
  4. Explain in /tmp/toctou.md exactly which two operations the other threads slipped between, and why the check was true for all of them.
  5. Fix it by making check and use atomic: hold an exclusive lock with fcntl.flock across both, or use a single atomic operation.
  6. Re-run the concurrent test and confirm the balance now stops at zero. Note that shortening the sleep did not fix it and the lock did.

Verify

python3 /tmp/voucher.py --reset 5 --concurrent 10 --unsafe | tail -1
python3 /tmp/voucher.py --reset 5 --concurrent 10 --safe | tail -1
python3 - <<'PY'
import subprocess
u=subprocess.run(['python3','/tmp/voucher.py','--reset','5','--concurrent','10','--unsafe'],
                 capture_output=True,text=True).stdout.strip().split()[-1]
s=subprocess.run(['python3','/tmp/voucher.py','--reset','5','--concurrent','10','--safe'],
                 capture_output=True,text=True).stdout.strip().split()[-1]
print('unsafe final balance',u,'| safe final balance',s)
assert int(u)<0, 'the race did not trigger - increase the sleep or the thread count'
assert int(s)==0, 'the locked version still oversold'
PY

The assertions are the verification. The unsafe run must go negative, proving the flaw is real rather than described, and the safe run must land exactly on zero. If the unsafe run does not go negative, the window is too small to hit — widen the sleep, which is simulating a slower operation such as a database round trip.

Notes

The fix that did not work is as instructive as the one that did. Shortening the window makes exploitation harder and leaves the flaw present, which is why the lesson insists the answer is atomicity rather than speed.

This is an independent study companion for CompTIA Security+ SY0-701 and is not produced by or endorsed by CompTIA.