Skip to content
exploitationadvanced#toctou#race-condition#web-security#exploitation#privilege-escalation#appsec#concurrency#cwe

TOCTOU Race Conditions: Filesystem, Web and Limit-Overrun Attacks

How the gap between a check and a use is exploited, from symlink swaps in privileged programs to concurrent requests that redeem one voucher many times, and the atomic fixes that 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.

The bug is invisible in single-threaded reasoning. The code reads correctly line by line, and the flaw lives in the assumption that nothing changes between two adjacent operations. That assumption fails whenever a concurrent attacker can act in the gap. The same shape produces a privileged program overwriting a system file, and a gift card that pays for a dozen orders.

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 voucher is still unused.
  • 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 job is to act inside the window so that the property the program verified is no longer the property the program operates on.

The window is the normal behavior of a modern system. A preemptive scheduler can suspend a thread at almost any instruction boundary, so even two adjacent lines have a gap another process can slip into. Blocking calls that wait on input, a lock, or the disk widen it for the whole wait. Multiple cores remove the need for the attacker to be scheduled in between at all, because their code runs at the same time on another core. On a web server the same effect arrives at the request level: two requests are handled simultaneously by different workers, and each reads the same row before either writes it. Code that touches a shared resource twice has to assume something can change between the two touches.

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 turns a benign-looking file operation into writing over, or reading from, a sensitive location the attacker could not touch directly.

A name is not a handle

The root error in filesystem TOCTOU is treating a path as a stable reference to a specific file. It 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. 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 reaches well beyond 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.

The web case: limit-overrun attacks

Web applications inherited the pattern once they began enforcing limits over shared database state across many concurrent requests. Here the request itself looks completely legitimate. What makes it an attack is that many identical requests arrive at the same instant, and the application, written as though requests take turns, lets several slip through a gate that should have admitted one.

The shared state is usually a row in a database. The typical vulnerable sequence:

1. read the voucher: is it still unused?      (check)
2. it is unused, so apply the discount
3. mark the voucher as used                    (use)

Run this once and it is fine. Run two copies at the same time and both can execute step 1 before either reaches step 3. Both read the voucher as unused, both apply the discount, and both then mark it used, one harmlessly after the other. The check that was meant to enforce single use enforced nothing, because two requests observed the same pre-change state. On MITRE ATT&CK, exploiting an internet-facing app this way maps to Exploit Public-Facing Application (T1190).

The most profitable web race is the limit-overrun attack. A resource is supposed to be used a bounded number of times, once, or up to a balance, or within a quota, and the attacker exceeds that bound by squeezing extra uses into the timing window.

TargetIntended limitOverrun result
One-time voucher or gift cardRedeem onceRedeemed many times for stacked value
Account balance or walletSpend up to the balanceOverdraft or double-spend
Withdrawal or transferMove only funds you holdMove the same funds twice
Rate or usage quotaN actions per windowFar more than N slip through
Invitation or signup bonusOne per userRepeated bonuses claimed
Stock or inventorySell available unitsOversell beyond stock

Each is a check-then-act on a counter or a flag, and each falls to the same technique. The financial ones are why race conditions attract determined attackers: the exploit converts directly into money, and a successful burst can be repeated.

Forcing the window open

A race is a probabilistic bug, and the attacker's job is to raise the probability of landing inside the check-to-use window.

On the filesystem that means hammering a loop. The attacker invokes the privileged operation thousands of times and swaps the target file continuously, so any single success is enough. Some widen the window deliberately, by making the path deep so resolution takes longer, or by adding system load that increases the chance the program is preempted at the right moment.

On the web it means making requests arrive together. Sending them one after another rarely works, because network jitter spreads their arrival out and the first request finishes its check-and-act before the next begins. Attackers instead prepare many requests in advance and release them in a tight burst, so they land within microseconds of each other. Research on so-called single-packet techniques pushed this further by getting many requests to complete on the server at effectively the same moment, largely removing network timing as a source of noise.

Concurrency is the threat model

If your logic reads shared state, decides, and then writes in separate steps, assume an attacker can land many requests inside that gap at once. The safety of check-then-act code depends on requests taking turns, and an attacker specifically removes that assumption. Any invariant enforced across two application steps rather than one atomic operation is a candidate for a limit-overrun attack.

Where the windows hide

The gap rarely looks like a gap in the source. Four patterns account for most web races.

  • Read-then-write across a request. A handler fetches a record, inspects a field, decides, then saves a change. Voucher redemption, balance spending, and quota counting almost always take this shape.
  • The uniqueness gap. Code that checks whether a value already exists and, finding none, inserts it, can be raced into duplicates when two requests both find nothing before either writes. Signup flows, invitation claims, and one-per-user bonuses fail here when uniqueness is enforced only in application logic.
  • The multi-object invariant. Rules spanning several records, moving funds between accounts or decrementing stock while creating an order, involve several reads and writes, which widens the window and multiplies the inconsistent end states.
  • The distributed check. When the check and the action live in different services, or a cache holds the state being checked, the window stretches across a network hop.

Why more checks do not help

The instinct is to add a stronger check: read the flag, and if it is already set, refuse. This fails when the problem is that two requests both read the flag before either sets it. More checks in application code move the window without closing it. As long as the read and the write are distinct operations another request can interleave between, the race remains.

The same reasoning defeats naive rate limiting as a fix. A limiter that reads a counter, compares it, and increments it in separate steps is itself a check-then-act, and it can be raced exactly like the resource it was meant to protect. Rate limiting has real value for abuse control, covered in Rate Limiting Explained, and the cure for the race is atomicity in the operation that enforces the invariant.

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
Payload-indistinguishableOn the web the malicious request is byte-identical to a valid one, so signature detection has nothing to match

A narrow window is still an exploitable window. An attacker who can attempt the race repeatedly only needs to win once, and automation makes enormous numbers of attempts cheap.

Worked example: the file that changes underneath you

A privileged program 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 to confirm the named file is owned by the caller, and only then open it and write to it: a check on the path, followed by an open on the same path.

Play it out as two actors on a shared timeline.

  1. The program receives the path, a file inside a directory the attacker controls.
  2. It checks ownership of that path. The name points at an ordinary file the attacker really owns, so the check passes.
  3. The thread is preempted, or simply spends a few microseconds returning from the check and preparing to open. This is the window.
  4. 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 from step 2, opens the path by name. Resolution follows the link to 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 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. Resolve once and hold a handle, and the attacker's edit in step 4 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. A defense that relies on the window being small is defending against a single attempt, and a single attempt is not the threat.

Worked example: the gift card that pays many times

The same mechanism at the application layer. A user holds a gift card worth a fixed amount and a checkout that applies it. The handler reads the card to confirm a positive balance, applies that balance to the order, then sets the balance to zero. One request at a time, this is correct: the card pays once and is emptied.

Now the attacker sends ten checkout requests in a tight burst, each applying the same card to a separate order, all arriving within a few milliseconds. Several execute the read step before any reaches the write step. Each sees a positive balance, because none of the writes has landed. Each applies the full balance to its own order. Each then sets the balance to zero, harmlessly overwriting a value that is already zero. The card that should have paid once has paid for several orders, and the money is real.

Swap the gift card for an account balance and the overrun is an overdraft. Swap it for a one-per-user signup bonus and the overrun is repeated bonuses. Swap it for stock on hand and the overrun is overselling inventory the business does not have. The mechanism is identical each time.

How to defend against race conditions

The goal is to eliminate the gap, or to 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 action through it. That guarantees you act on the exact object you inspected, defeating name re-resolution and link swaps.
  2. Prefer atomic check-and-act system calls. Create-only-if-absent, or open flags that refuse to follow links, perform the check and the action as one indivisible step.
  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. Enforce the invariant in the database. A unique constraint makes a second redemption of the same voucher fail outright. A conditional update that only decrements a balance when sufficient funds exist, executed as one statement, cannot be split by a concurrent request.
  5. Use atomic operations, not read-modify-write. Prefer a single atomic decrement or compare-and-set over reading a value into the application, changing it, and writing it back. The round trip is the window.
  6. Lock the row for the transaction. When logic genuinely needs multiple steps, take an exclusive lock on the affected record at the start of the transaction so concurrent requests serialize.
  7. Make operations idempotent. Require an idempotency key on state-changing requests and record it, so a repeated request produces the original result once. This neutralizes bursts of identical requests, which is exactly what the attack sends.
  8. Keep the critical section small and single-writer. Route contended operations through one path that owns the invariant rather than duplicating the logic across endpoints. The limitation is that it needs the logic centralised in one path.
  9. Handle temporary files safely. Create them with exclusive, atomic creation and unpredictable names, in directories not writable by others, so an attacker cannot pre-place or swap them.
  10. Synchronize in-process shared state. For races inside a program, use locks so the check and the use cannot be interrupted by another thread.
  11. Test under real concurrency. Fire bursts of simultaneous requests in testing and assert the invariant holds.
  12. Apply least privilege. Run with minimum rights and drop privileges before touching attacker-influenced resources, so winning the race yields little. See least privilege explained, and secure coding practices for where this sits among the other structural habits.
Close the window

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

The primitives that make it atomic

PrimitiveLayerCloses
Exclusive-create open flagSystem callCheck-for-absence plus create
Flag that refuses to follow linksSystem callSymlink swap between check and open
Operate on the descriptor, not the pathSystem callSecond resolution of a name
Directory handle plus relative nameSystem callMulti-step path traversal races
Atomic create-and-renameFilesystemReaders seeing a half-written or swapped file
Compare-and-swapCPU instructionRead-modify-write between threads
Unique constraintDatabase schemaDuplicate inserts from a raced uniqueness check
Conditional atomic updateSingle SQL statementBalance and counter limit overruns
Row lock in a transactionDatabase transactionGenuinely multi-step application logic
Idempotency keyApplication plus storeRepeated or replayed submissions

The common property is indivisibility. If the check and the act cannot be separated by the scheduler, the class of bug disappears rather than shrinking. The table is a menu rather than a ranking. A single-use voucher is best served by a unique constraint. A balance is best served by a conditional update that only debits when funds suffice. Multi-object invariants that cannot fit one statement call for a row lock. Bursts of identical replays are absorbed by idempotency keys. Each carries a trade-off: a unique constraint expresses only uniqueness, a conditional update requires the rule to fit one statement, a row lock adds contention and must be scoped tightly, and idempotency keys require clients to send them and servers to record them. Most real systems combine several, choosing per invariant.

Detection signals

Races are a design flaw to be engineered out, and they do not announce themselves at runtime the way an injection attempt does. These signals are worth watching in code review and in monitoring.

  • Check-then-act pairs on shared resources in source. A validating call on a name or shared value, followed by a separate operating call on the same one, 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. A privileged process opening a file that resolves through a symbolic link in a world-writable directory is a red flag, and auditing frameworks that record link resolution during sensitive opens surface exactly this.
  • Bursts of create, delete, and rename in a shared directory. Winning a filesystem race means hammering the directory to swap the target. 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.
  • Temporary files with predictable names in shared locations. A name built from a process identifier or a timestamp lets an attacker guess it and pre-place a link, which is a latent exposure even before an attempt.
  • Bursts of near-identical state-changing requests. Many requests to the same sensitive endpoint from one session, within a very short window, is the web signature. Ordinary users do not redeem the same voucher ten times in fifty milliseconds. This overlaps with the traffic patterns in API abuse explained.
  • State-changing requests with no idempotency key repeating the same payload. Exactly the traffic that atomic and idempotent handling is meant to absorb, and its presence flags endpoints that lack it.
  • Invariant violations found after the fact. A voucher marked used twice, a negative balance, a quota exceeded, stock sold beyond what existed, duplicate records a uniqueness rule should have prevented, or reconciliation gaps in financial and inventory totals.

Invariant monitoring is the most reliable of these, because it detects the outcome of a race rather than trying to recognise the traffic that caused it. If the rule is that a voucher pays once, an alert on any voucher that paid twice catches the attack regardless of how the requests were shaped.

Finding races before an attacker does

Because the malicious request looks legitimate, testing has to reproduce the collision rather than the payload.

For filesystem and in-process races, widen and hammer the window deliberately: 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 functional tests miss.

For web races, prepare a batch of identical state-changing requests and release them together against a test environment, then check the resulting state. Redeem one voucher with twenty simultaneous requests and confirm it paid exactly once. Spend a balance with concurrent withdrawals and confirm it never went negative. Claim a one-per-user bonus in parallel and confirm exactly one landed.

Sequential tests hide the bug

A test suite that exercises endpoints one request at a time will pass against code that is completely exploitable, because the race only appears under concurrency. Coverage numbers give false comfort here. The only test that matters for this class lands multiple requests inside the same window on purpose and then checks that the invariant survived.

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
Limit overrun (T1190 in practice)Two 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 it is a variable, so locks or atomic instructions are the fix. In a web application it is a record two requests both read as unused, so a constraint, a lock, or a conditional update is the fix. This class survives every layer of runtime hardening, which is worth reading alongside exploit mitigations explained and its place in the OWASP Top 10.

Common mistakes

  • Shrinking the gap and calling it fixed. Reordering code so the check sits right before the use narrows the window and leaves the bug, because an attacker who retries indefinitely still wins. This is the single most common false fix.
  • Trusting a path because an earlier call on it succeeded. Two calls on the same path are two independent resolutions, and a successful check says nothing durable about what the next call resolves 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. A single-threaded program still yields the processor and shares the filesystem with other processes.
  • Treating races as too unreliable to matter. Modern techniques make the collision reproducible, and the financial variants convert directly into money. Treating the bug as improbable is how it survives to production.
  • Assuming a transaction alone is enough. Wrapping the read and write in a transaction without the right lock or an atomic operation can still let both requests read the same pre-change state under common isolation levels. The atomicity has to be real rather than nominal.

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 to shared memory between threads, environment values read twice, and web applications enforcing limits over shared database state. The web variants were long treated as unreliable, because network jitter spread requests out and made the collision hard to hit on purpose. Research into techniques that get many requests to complete on the server at effectively the same instant removed much of that noise and raised the class from a rare curiosity to one worth testing for directly. The defensive answer did not change with the tooling. A validated property is trustworthy only for as long as the resource is held still, and the durable defenses hold it 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.

Is TOCTOU the same as a web race condition?

Web race conditions are TOCTOU applied to shared application state across concurrent requests, and limit-overrun attacks are the common web expression of it. The general pattern is identical, with a database row as the shared resource instead of a file.

What is a web race condition in one sentence?

An application checks a condition and acts on it in separate steps, and an attacker fires concurrent requests so several pass the check before any completes the act.

Why do race conditions have financial impact?

The common web form is the limit-overrun, where a resource meant to be used a bounded number of times is used more. Redeeming a single voucher many times, overdrawing a balance, or claiming repeated bonuses each converts directly into money.

Does a very short window make the bug safe to ignore?

No. An attacker can retry as many times as they like, and automation makes enormous numbers of attempts cheap. A narrow window lowers the success rate per attempt and leaves the eventual outcome the same.

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.

How do I make an operation atomic?

Collapse the check and the act into one indivisible step: a unique constraint for uniqueness, a conditional update that only debits when funds suffice, an exclusive row lock when the logic genuinely needs multiple steps, idempotency keys to absorb repeated submissions, and exclusive-create flags plus descriptor operations on the filesystem.

Can rate limiting stop a race condition?

Not on its own. A naive limiter is itself a check-then-act on a counter and can be raced exactly like the resource it protects. The cure for the race is atomicity in the operation that enforces the invariant.

Can least privilege alone prevent TOCTOU?

It shrinks the damage without preventing the race. 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. TOCTOU is a logic-and-timing flaw, and 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 meant to succeed once.

How do I test for a race that rarely reproduces?

Create the collision on purpose. Add artificial delay between the check and the use in a test build and run many concurrent attempts that swap the resource, or fire a burst of genuinely concurrent identical requests at a test environment and assert the invariant held.

Race conditions are a reminder that correctness depends on time as much as on logic. 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. On the filesystem that means holding a handle instead of a name. On the web it means enforcing the limit in the database, locking what must be multi-step, and making state changes idempotent, so an attacker's burst of perfectly legitimate requests resolves to a single legitimate outcome. For broader context on how this and other flaw classes are exploited in the wild, see our Exploit Intelligence dashboard.

Sources & further reading