TOCTOU - the gap between checking and using

Jun 15, 2026·2 min read

You peek into your wallet at the counter, see a ₹500 note, and decide "yes, I can afford this". You walk to billing, but by then your friend has borrowed that note. You try to pay — it's gone. You checked at one moment and used at another, and the world changed in between.

That gap is the whole story of TOCTOUTime Of Check To Time Of Use. It's the kind of bug that looks correct top to bottom, passes every test, and then quietly betrays you under concurrency.

TOCTOU is a race condition where the state you validated is no longer true by the time you act on it.

There's a quiet bit of psychology here. Our brains treat a fact, once verified, as settled — psychologists call it the frozenness of belief: we check once and assume the conclusion holds. Code inherits that bias from us. The check feels like it freezes reality, but reality keeps moving.

The bank withdrawal

A joint account has ₹1000. Two people open the app and both withdraw ₹1000 at the same instant.

Copy
def withdraw(account, amount):
    balance = db.get_balance(account)   # TIME OF CHECK
    if balance >= amount:
        # ... the other request sneaks in right here ...
        db.set_balance(account, balance - amount)  # TIME OF USE
        dispense(amount)

Both read 1000, both pass the if, both dispense. The bank gives away ₹2000. Each request did the "right" thing individually — the flaw is that both checked before either used.

The last seat

Copy
async function bookSeat(seatId, userId) {
  const seat = await db.getSeat(seatId);   // check: free?
  if (seat.status === 'available') {
    await db.updateSeat(seatId, { status: 'booked', userId }); // use
    return 'booked!';
  }
}

Two users hit this in the same millisecond. Both read available, both write booked. One seat, two confirmation emails. (Tatkal season, anyone?)

The file that wasn't the file you checked

This is the one that makes TOCTOU a security bug. A program running as root politely verifies you own a file before writing to it:

Copy
if (access("/home/alice/report.txt", W_OK) == 0) {
    // ... attacker swaps the path for a symlink here ...
    int fd = open("/home/alice/report.txt", O_WRONLY);  // use
    write(fd, data, len);
}

Between access() and open(), the attacker repoints that path to /etc/passwd. The check ran on the harmless file; the write lands on the system one. The filename was never the file — it was just a label, and labels can be re-pointed. (A nice echo of the philosopher's warning: the map is not the territory.)

Why we keep missing it

Read any snippet alone and your brain says "looks fine" — and it is, as long as nothing touches that state in between. TOCTOU hides because the code reads like a correct sentence: "if allowed, then do it." We're wired to trust that grammar.

The mental shift: a check is only a fact for an instant. The moment you stop holding it, it's a guess.

The fix is the same idea every time

Never "check harder" or "check twice". Instead, collapse the check and the use into one atomic step so nothing can slip into the gap.

Copy
-- check and use become ONE statement
UPDATE accounts SET balance = balance - 100
 WHERE id = 42 AND balance >= 100;

If 0 rows changed, there wasn't enough money — and no race ever existed. Same trick for the seat (... WHERE status = 'available'), only one request can win. When you can't say it in one statement, hold a lock around the gap; for files, work on an open descriptor (openat, O_NOFOLLOW) instead of a name; for counters, use compare-and-swap.

Takeaway

Whenever you see "check something, then act on it" as two steps, ask: what if the world changes between these lines? If the answer scares you, you've found a TOCTOU bug.

The gap between checking and using is small. The bugs that live there are not. Close the gap.

Share this article