Time-of-check to time-of-use

You checked the path, then used it — but the world changed in the gap between.

The idea

A TOCTOU race (time-of-check to time-of-use) happens when you inspect a file or path, decide it's safe, and then act on it as a separate step. Between those two steps an attacker — or just a second process — can swap what the path points to.

The classic shape is "check the file isn't a symlink, then open it." If an attacker replaces the file with a symlink to /etc/passwd in the window between the check and the open, your privileged write lands on the wrong target. The fix is to make check-and-use a single atomic operation on the same underlying object.

Press play to watch the check and the use race against a swap.

How it works

The vulnerable code looks safe: it validates, then acts. But access(), stat(), or "does this path exist?" all answer a question about a name, not a stable handle. By the time you call open(), the name may resolve to something else.

# Vulnerable: two lookups of the same name
if os.access(path, os.W_OK):      # time of CHECK
    with open(path, "w") as f:    # time of USE — path may have changed
        f.write(payload)

# Safer: open once, then check the handle you actually hold
fd = os.open(path, os.O_WRONLY | os.O_NOFOLLOW | os.O_CREAT, 0o600)
st = os.fstat(fd)                 # check the SAME object, by fd
if stat.S_ISREG(st.st_mode):
    os.write(fd, payload)

The safe version never re-resolves the name. O_NOFOLLOW refuses symlinks at open time, and fstat inspects the exact descriptor you hold — there is no second lookup to race.

Cost & signals

DimensionWhat to know
Race windowMicroseconds — but attackers retry millions of times to land in it
Blast radiusPrivileged writes/reads redirected to attacker-chosen files
SignalOperations on a path resolve to an inode that differs from the one checked
SignalUnexpected symlinks appearing in world-writable dirs like /tmp
Fix costLow — collapse check+use into one fd-based operation

Watch out for

Worked example

A backup tool runs as root and writes a log to /tmp/backup.log. It first checks the file is a regular file, then opens it to append. An attacker loops: delete /tmp/backup.log, then symlink it to /etc/shadow. If the symlink lands between the check and the open, root appends backup chatter into /etc/shadow — corrupting credentials. Replacing the check-then-open with a single os.open(path, O_WRONLY|O_NOFOLLOW) closes the window: the open itself refuses the symlink.

Check yourself

Why does using a file descriptor instead of re-opening the path defeat the race?

Where are TOCTOU races most likely?