skip to content

A program checks a condition about a resource and then acts on the result of that check — for example verifying a file path is permitted and then opening it. Explain the time-of-check-to-time-of-use flaw this creates and how you eliminate it.

level: seniorimportance: should knowfreq 38%

answer

  1. check the name, use the name again — resolved twice
  2. attacker widens the window and retries forever
  3. operate on the handle, not the path
  4. one conditional op: exclusive create, CAS, ETag, unique constraint
  5. re-checking and shorter windows are not fixes

basics

~30 s

Between the check and the use, whatever the check examined can change — a path can be swapped for a symlink, a record can be deleted, permission can be revoked. The verified fact no longer holds when you act on it. The fix is to remove the gap: perform the check and the action as one atomic operation, or check the handle you actually operate on rather than the name you looked up.

solid answer

~1 min

Time-of-check-to-time-of-use is check-then-act across a **trust or resource boundary**, where an attacker or a concurrent actor controls the window. The classic form: validate that a path is inside an allowed directory, then open it — between the two, the path is replaced with a symlink pointing somewhere privileged. Non-security variants are just as common: check a row exists then update it; check quota then consume it; check a token is valid then use it after revocation. What makes it dangerous rather than merely flaky is that an attacker can widen the window deliberately — by slowing the filesystem, by looping the swap thousands of times, by contention — turning a nanosecond race into a reliable exploit. Eliminating it means removing the gap, not shrinking it: - **Operate on a handle, not a name.** Open first, then inspect the *opened object*; use directory-relative, no-follow, exclusive-create operations so the resolution and the action are a single kernel step. - **Use a conditional primitive.** Compare-and-swap, exclusive create, conditional write with an expected version/ETag, a unique constraint — let the authority that owns the state enforce it. - **Hold the invariant.** A lock or transaction spanning both halves, when both halves are inside your trust boundary. Retrying, sleeping, or re-checking after acting are not fixes.

code

text · 10 lines
text
# vulnerable: the path is resolved twice
if is_inside_allowed_dir(path) and not is_symlink(path):
    f = open(path)          # attacker swapped path -> symlink here
    write(f, data)

# safer: resolve once, then verify the object you hold
f = open_no_follow(path, relative_to=allowed_dir_handle)
if not describes_regular_file_owned_by_us(f):
    close(f); reject()
write(f, data)              # same object that was verified

go deeper

for a junior

Recognize the shape — validate, then act on the same name — and know that the thing can change in between so the check no longer guarantees anything.

for a middle

Give a concrete symlink or existence-check example and name the general fix: make check and action one atomic operation, or act on a handle you already hold.

for a senior

Cover the adversarial angle — window widening and unlimited retries — enumerate the fix families (handle-based, conditional/versioned, transactional) and explicitly reject re-checking and window shrinking.

for a principal

Position it as a boundary-ownership question: which component is the authority for the invariant, what atomic primitive it exposes, and how the design avoids name-based indirection across trust boundaries at all.

## The pattern Time-of-check-to-time-of-use (TOCTOU) is the check-then-act compound action, specialized to the case where the checked state lives outside your exclusive control — the filesystem, another process, another service, the database, another thread. The code reads: ``` if (is_allowed(resource_name)): # time of check ... operate(resource_name) # time of use ``` The validation was performed on the *name*, and the operation resolves the name **again**, at a later instant. Between those two resolutions the mapping from name to actual resource can change. Your program then applies a decision that was true about one object to a different object. ## Why it is worse than an ordinary race In a plain data race, the timing is chance and the window is nanoseconds. In TOCTOU, an adversary frequently controls both. - They can **widen the window**: force slow path resolution, mount a filesystem that stalls, put the file on a network share, saturate CPU so your thread is descheduled between the two lines. - They can **retry cheaply**: a loop that swaps a real file and a symlink millions of times only has to win once. A "1-in-a-million" race is a few seconds of attack. - The failure is **privilege escalation**, not a wrong count: the process opens a file it validated as safe and writes attacker-chosen content into something it should never touch. This is why "the window is too small to exploit" is never an acceptable analysis. ## Where it shows up - **Filesystem.** Check path is inside an allowed directory → open. Check file does not exist → create. Check ownership/permissions with a stat call → open. Check a file is a regular file → read it. All of these are exploitable by symlink or directory swaps. - **Authorization.** Check the caller has a role → perform the action after the role has been revoked, or after the tenant/ownership of the object changed. - **Resource accounting.** Check the quota/balance/inventory → consume it. Two callers both pass. - **Data layer.** Read a row and validate it in the application → write it back, clobbering an update made in between (lost update at the persistence level). - **Caching and provisioning.** Check cache miss → compute and populate; check the resource does not exist → create it, producing duplicates. ## How to fix it properly The governing idea: **make the check and the use the same operation, or make the use itself validate the assumption.** 1. **Handle-based access.** Acquire a reference to the actual object first, then do all checks on that reference, and perform all subsequent operations through it. A file descriptor refers to the opened object; swapping the name afterwards cannot redirect it. Use the primitives designed for this: open with a no-follow flag, directory-relative operations rooted at an already-open directory handle, exclusive-create so "create only if absent" is one atomic step, and inspect the opened descriptor rather than re-resolving the path. 2. **Conditional / atomic primitives.** Instead of "read, decide, write", issue an operation that succeeds only if the precondition still holds: compare-and-swap on a memory location; conditional write with an expected version, sequence number or entity tag; an `insert` guarded by a unique constraint; a single `UPDATE ... WHERE balance >= amount` that both tests and applies. The authority that owns the state arbitrates, and a failure comes back as an error you can handle rather than as silent corruption. 3. **Mutual exclusion across the pair.** When both halves are inside your own trust boundary, a lock or a database transaction with an appropriate isolation level (or explicit row locking) spanning check and act restores the atomicity. This does not help when the *adversary* is outside that boundary — a mutex in your process does not stop another process from swapping a symlink. 4. **Eliminate the name-based indirection.** Canonicalize once, then never use the path again; or work in a private directory the attacker cannot write to; or copy the resource under your control before validating it. ## Non-fixes to reject - **Re-checking after acting.** Now you have three racy steps instead of two. - **Shortening the window** by moving lines closer together, removing logging, or optimizing. A shorter window is still a window, and the attacker gets unlimited attempts. - **Sleeping or retrying** on failure — it changes the probability, never the possibility. - **Checking again inside the same function** — same problem, one level deeper. ## The review question Whenever you see a validation followed by an action, ask: *what identifies the thing between those two statements, and can anyone else change what that identifier refers to?* If the answer is a name, a key, a path, or a value that something outside your atomic unit can alter, you are looking at TOCTOU, and the correct fix moves the check inside the operation rather than in front of it.

  • Why does adding a mutex around the check and the action not necessarily fix a filesystem TOCTOU?
    A mutex only orders the threads inside your own process. A TOCTOU on a path is usually exploited by a different process — or a different user — that is not participating in your locking protocol and can swap the path at any moment. Locks fix races among cooperating participants inside one trust boundary; when the adversary is outside it, you need the operating system to make resolution and action a single step, via handle-based and no-follow primitives.
  • An interviewer says the exploit window is only a few microseconds so the risk is acceptable. How do you respond?
    Window size is not the right metric, because the attacker controls both the width and the number of attempts. They can slow the resolution (network filesystem, heavy load, forced page faults) and can retry the swap in a tight loop indefinitely, so a low per-attempt probability becomes a near-certain success in seconds. The correct standard is whether a successful interleaving is possible at all, not how likely it is per attempt.
  • How does the same flaw appear in a stateless HTTP API with no filesystem involved?
    As read-validate-write across two requests: a client GETs a resource, the application validates something about it, and a later PUT writes back a value computed from the stale copy, silently clobbering an update made in between. The standard fix is a conditional request — send the version or entity tag observed at read time and have the server reject the write if the resource has changed — which turns check-then-act into one atomic, server-arbitrated operation.

A doorman checks an ID against a guest list, hands it back, and only then opens the door — in between, the visitor swaps the ID with a friend's. The fix is to keep the ID, or to make checking and admitting a single motion.

saying these in an interview costs you the question

  • 'The window is only microseconds, nobody could hit it'
  • Fixing it by re-checking the condition after the operation instead of eliminating the gap
  • Believing an in-process lock protects against another process or user swapping a path
  • Validating a path string and then reopening it by name rather than working from the opened handle
  • Treating TOCTOU as purely a filesystem issue and missing quota, authorization and read-modify-write forms

context