Skip to content
pwnsy
exploitationadvanced#toctou#race-condition#exploitation#privilege-escalation#cwe

TOCTOU Race Conditions: The Gap Between Check and Use

How time-of-check to time-of-use races let attackers slip between a validation and the action, and how atomic operations close the window.

Most vulnerabilities are about what a program does. A time-of-check to time-of-use race is about when it does it. The program checks a condition, decides the condition is safe, and then acts, and between the check and the act, an attacker changes the thing that was checked. The decision was valid at the moment it was made and wrong by the moment it was used. MITRE tracks this as CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition, a specific case of the broader race-condition class CWE-362.

This is an advanced topic because the bug is invisible in single-threaded reasoning. The code reads correctly line by line. The flaw lives in the assumption that nothing changes between two adjacent operations, and that assumption fails whenever a concurrent attacker can act in the gap.

The check, the gap, and the use

Every TOCTOU bug has the same three parts:

  • Time of check. The program inspects a shared resource and confirms some property: this file is owned by the right user, this account has the right balance, this record is in the right state.
  • The window. Between the check and the use, execution pauses, yields, or simply takes time. The shared resource is not locked, so anything with access can modify it.
  • Time of use. The program performs the action it decided was safe, trusting the property it checked earlier, which may no longer hold.

The attacker's whole job is to act inside the window and change the resource so that the property the program verified is no longer the property the program operates on.

The filesystem case

The best-known TOCTOU bug lives in the filesystem, because files are shared, and a program often refers to a file by its path rather than by a stable handle to the exact object. A path is a name that the system resolves to a target each time it is used, and an attacker who controls the surrounding directory can change what the name resolves to.

The vulnerable shape is a program that checks a property of a path, then separately opens or operates on that same path. Between the two, an attacker repoints the name, commonly by replacing a file or directory component with a symbolic link to a different target, a technique tracked as link following, CWE-59. The check inspected the original target and approved it. The use resolves the name again and lands on the attacker's target. If the program runs with elevated privilege, this can turn a benign-looking file operation into writing over, or reading from, a sensitive location the attacker could not touch directly.

The core mistake is checking a name and then using the name again as if it still pointed at the same object.

A name is not a handle

The root error in filesystem TOCTOU is treating a path as if it were a stable reference to a specific file. It is not. A path is resolved fresh on every operation, so two operations on the same path can hit two different files if an attacker changes the directory in between. The fix is to resolve the name once, obtain a handle to that exact object, and perform every subsequent check and action through the handle so the name can never be re-resolved to something else.

The privilege case

TOCTOU is not limited to files. Any time a privileged program validates state that a less-privileged actor can influence, and then acts on that state after a delay, the same window exists. A program might confirm that a requester is authorized, then perform the action a moment later against state the requester has since altered. Or it might validate an input, then re-read that input from a shared location that the attacker rewrote after validation.

Shared memory, environment values, and any resource readable and writable by two actors at once can host the race. The unifying theme is a trust decision made against a mutable resource, with a gap before the decision is used, during which a lower-privileged party edits the resource.

Why these bugs are hard to catch

PropertyEffect
Timing-dependentThe bug only triggers when the attacker hits the window, so it rarely reproduces in normal testing
Single-thread-invisibleLine-by-line review sees valid logic, because the flaw is between the lines
Probabilistic exploitationAttackers retry to win the race, so a narrow window is still exploitable over many attempts
Environment-sensitiveSystem load and scheduling change the window size, so behavior varies by machine

A narrow window is not a safe window. An attacker who can attempt the race repeatedly only needs to win once, and automation lets them try enormous numbers of times. Designing on the assumption that a small gap is acceptable is a mistake.

How to defend against TOCTOU races

The goal is always the same: eliminate the gap, or make the checked object and the used object provably identical.

  1. Use handles, not names. Open the resource once to get a stable handle, then perform every check and every action through that handle. Operating on a handle guarantees you act on the exact object you inspected, defeating name re-resolution and link swaps.
  2. Prefer atomic check-and-act operations. Use system calls that perform the check and the action as one indivisible step, such as create-only-if-absent or open flags that refuse to follow links. Atomicity removes the window entirely.
  3. Avoid separate check-then-use on shared resources. Where the platform offers an operation that just attempts the action and reports failure safely, do that instead of checking first and acting later.
  4. Handle temporary files safely. Create temporary files with exclusive, atomic creation in directories not writable by others, so an attacker cannot pre-place or swap them.
  5. Apply least privilege. Run the code with the minimum rights it needs, and drop privileges before touching attacker-influenced resources, so winning the race yields little. See least privilege explained.
  6. Synchronize access to in-process shared state. For races inside a program, use locks or other synchronization so the check and the use cannot be interrupted by another thread, a discipline shared with web race conditions.
Close the window, do not just shrink it

Optimizing the code so the gap between check and use is very short is not a fix, because an attacker retries until they win. The reliable defenses remove the window rather than narrow it: act through a handle to the checked object, or use an atomic operation that checks and acts as one step. Treat any check-then-act sequence on a shared, attacker-reachable resource as a bug to redesign, not tune.

A worked example in slow motion

Picture a privileged program that accepts a filename from a less-privileged user and is supposed to operate only on files that user already owns. The intended safety check is: confirm the named file is owned by the caller, and only then open it and write to it. In pseudocode the shape is a check on the path, followed by an open on the same path.

Now play it out as two actors on a shared timeline.

  1. The privileged program receives the path, for example a file inside a directory the attacker controls.
  2. The program checks ownership of that path. At this instant the name points at an ordinary file the attacker really owns, so the check passes.
  3. The program's thread is preempted, or simply spends a few microseconds returning from the check and preparing to open. This is the window.
  4. In that window, the attacker replaces the directory entry. They delete or rename the innocent file and drop a symbolic link with the same name, pointing at a sensitive file they do not own, for instance a system configuration file.
  5. The program, still trusting the check it did in step 2, opens the path by name. Resolution now follows the link, and the program opens the sensitive target.
  6. The program writes to what it believes is the caller's harmless file. The write lands on the protected file, with the privileged program's rights.

Nothing in the program's own logic is wrong when read top to bottom. The ownership check was true when it ran. The failure is that the name was resolved twice, and the attacker changed what the name meant between the two resolutions. This is why the fix is to resolve once and hold a handle, so the second operation cannot re-resolve the name to a different object. The attacker's edit in step 4 then changes a name the program is no longer consulting.

The attacker does not need to win on the first try

Step 4 has to land inside a window that may be microseconds wide, which sounds hard until you remember the attacker controls the loop. They can invoke the privileged operation thousands of times and swap the file continuously in a tight loop, so any single success is enough. Some attackers widen the window deliberately, for example by making the path deep or by adding load that increases the chance the program is preempted at the right moment. A defense that relies on the window being small is defending against a single attempt, which is not the threat.

Detection signals

TOCTOU is primarily a design flaw to be engineered out, and it does not announce itself cleanly at runtime. Still, several signals are worth watching, in code review and in monitoring.

  • Check-then-act pairs on shared resources in source. The reviewable signature is a validating call on a name or shared value, followed by a separate operating call on the same name or value, with no handle or lock tying them together. Static analysis can flag the pattern where a path is stat-checked and then opened by path.
  • File operations that follow untrusted symlinks. At runtime, a privileged process opening a file that resolves through a symbolic link in a world-writable or user-writable directory is a red flag. Auditing frameworks that record link resolution during sensitive opens surface exactly this.
  • Bursts of rapid create, delete, and rename in a shared directory. Winning a filesystem race usually means hammering the directory to swap the target repeatedly. A spike of metadata churn on a temporary or spool directory coinciding with a privileged operation is a behavioral indicator.
  • Repeated invocation of a privileged operation with the same argument. An attacker retrying to hit the window calls the same entry point over and over. High-frequency repetition of an otherwise ordinary privileged action is anomalous.
  • Temporary files with predictable names in shared locations. Predictable names in a world-writable directory let an attacker pre-place a file or link to be found by the check. Their presence is a latent exposure even before an attempt.

TOCTOU sits inside the broader race-condition family and next to several file-handling weaknesses. Keeping the distinctions clear helps you pick the right fix.

ConceptCore issueRelationship to TOCTOU
TOCTOU (CWE-367)A checked property changes before it is usedThe specific check-to-use race
Race condition (CWE-362)Improper synchronization on a shared resourceThe general class TOCTOU belongs to
Link following (CWE-59)An operation follows an attacker-controlled linkThe usual mechanism a filesystem TOCTOU exploits
Improper synchronizationMissing locks around shared stateThe in-process cousin, fixed with locks rather than handles
Business-logic race on the webTwo requests both pass a one-time checkThe same timing gap at the application layer

The through-line is a trust decision made against something that can change before the decision is acted on. On the filesystem the changing thing is what a name resolves to, so handles and atomic opens are the fix. In memory shared between threads the changing thing is a variable, so locks or atomic operations are the fix. On a web application the changing thing is a record two requests both read as unused, so a database lock or an atomic conditional update is the fix. Our guide to web race conditions covers that last variant in depth.

Atomic operations that close the window

Concrete platform primitives exist specifically to collapse a check and an act into one step. Knowing them by category makes the defenses actionable.

  • Open flags that refuse unsafe resolution. Opening with a create-exclusive flag makes the call fail if the file already exists, so a check for absence and the creation happen as one atomic step. Flags that refuse to follow symbolic links make the open fail rather than silently resolving to an attacker's target.
  • Operate on descriptors, not paths. Once a file is open, use the descriptor for every further check and action. Reading ownership or type from the open descriptor, then acting through that same descriptor, guarantees the object cannot be swapped between operations. Directory-relative operations that take a directory handle plus a name close the same gap for multi-step path work.
  • Atomic create-and-rename for updates. To replace a file safely, write to a fresh temporary file and atomically rename it into place, so readers ever see either the old or the new content and never a half-written or swapped intermediate.
  • Compare-and-swap for in-memory state. For races inside a process, an atomic compare-and-swap performs the check and the update as one indivisible instruction, so no other thread can act between them.

The common property is indivisibility. If the check and the act cannot be separated by the scheduler, there is no window for the attacker to act in, and the class of bug disappears rather than shrinking.

Where the window comes from

It helps to be precise about what creates the gap, because the source shapes the fix. A window opens whenever execution can pause or interleave between the check and the use. On a preemptive operating system, the scheduler can suspend a thread at almost any instruction boundary, so even two adjacent lines of code have a gap another process can slip into. Blocking operations widen it further: any call that waits on input, a lock, or the disk hands the processor to other work, and the shared resource is unguarded for the whole wait. Multiple cores remove the need for the attacker to even be scheduled in between, because their code can run genuinely at the same time on another core. None of these are unusual conditions. They are the normal behavior of a modern system, which is why treating the gap as a rare accident is the wrong mental model. The gap is the default, and code that touches a shared resource twice has to assume something can change between the two touches.

Common mistakes

  • Shrinking the gap and calling it fixed. Reordering code so the check sits right before the use narrows the window but does not remove it, and an attacker who retries indefinitely still wins. This is the single most common false fix.
  • Re-checking after the use. Adding a second validation after acting does not help, because the damaging action already happened against the swapped object.
  • Trusting a path because an earlier call on it succeeded. Two calls on the same path are two independent resolutions. A successful check tells you nothing durable about what the next call will resolve to.
  • Using predictable temporary filenames. Building a temporary path from a process identifier or a timestamp lets an attacker guess it and pre-place a link. Safe temporary creation uses unpredictable names and exclusive creation in a directory others cannot write.
  • Assuming single-threaded code is immune. Even a single-threaded program yields the processor and shares the filesystem with other processes, so a filesystem TOCTOU does not need threads inside your program to be exploitable.

How the understanding evolved

TOCTOU was first understood as a filesystem problem in privileged programs, where checking a path and then acting on it by name was a widespread and natural-looking pattern. As operating systems added descriptor-based and directory-relative system calls, and open flags that refuse to follow links, the reliable fixes moved from careful ordering toward holding a handle to the exact object. The concept then generalized well beyond files. The same check-to-use gap shows up in shared memory between threads, in environment values read twice, and prominently in web applications where two concurrent requests both pass a one-time check before either commits. The lesson that hardened over time is that a validated property is only trustworthy for as long as the resource is held still, and that the durable defenses hold the resource still rather than trying to act faster than an attacker.

Frequently asked questions

Is a TOCTOU race the same as any race condition? It is a specific kind. A race condition is any incorrect behavior caused by unsynchronized access to a shared resource. TOCTOU is the particular case where a program checks a property and then uses it, and the property changes in between.

Does a very short window make the bug safe to ignore? No. An attacker can retry the operation as many times as they like, and automation makes enormous numbers of attempts cheap. A narrow window only lowers the success rate per attempt, not the eventual outcome.

Why is checking a filename before opening it wrong? Because a filename is resolved fresh on every use. The check resolves the name to one object, and the open resolves it again, possibly to a different object if an attacker changed the directory in between. Operating on an open handle avoids the second resolution.

Can least privilege alone prevent TOCTOU? It cannot prevent the race, but it shrinks the damage. If the program holds few rights, winning the race yields little, so dropping privileges before touching attacker-influenced resources is a strong mitigating layer alongside the structural fixes. See least privilege explained.

Do memory-safe languages remove TOCTOU? They remove memory-corruption bugs, but TOCTOU is a logic-and-timing flaw. A program in any language that checks a shared resource and then acts on it separately can have the race. The fixes are the same: handles, atomic operations, and synchronization.

Where do TOCTOU races appear outside the filesystem? In shared memory between threads, in values read from a shared location twice, in environment variables, and in web application logic where concurrent requests both pass a check that was meant to succeed once. The pattern is a trust decision against a mutable, shared resource.

How do I test for a race that rarely reproduces? Deliberately widen and hammer the window: add artificial delay or load between the check and the use in a test build, and run many concurrent attempts that swap the resource. Fuzzing and stress testing with an adversarial helper thread expose races that ordinary functional tests miss.

TOCTOU races are a reminder that correctness is not only about logic, it is about time. A check tells you the truth at one instant, and a program that acts on that truth later, without holding the resource still, is trusting the past. Close the gap with handles and atomic operations, run the sensitive code with least privilege, and the attacker loses the window they need. For broader context on how race conditions and other flaw classes are exploited, see our Exploit Intelligence dashboard.

Sources & further reading

Sharetwitterlinkedin

Related guides