Below the Radar · 2 of 10L3offensive security
Time Is a Weapon: TOCTOU and the Gap Between Check and Use
Every check is a photograph of a moment. If you act on it later, an attacker only has to change the world in between.
Abstract
Security code is full of a hidden shape: check that something is safe, then use it. This piece studies the assumption that almost nobody writes down — that a fact stays true between the moment you verify it and the moment you rely on it. We name the gap a time-of-check to time-of-use window, trace it from the file-system races Bishop and Dilger formalized in 1996 to any check-then-act on shared mutable state, and then argue that a modern AI agent is a machine built almost entirely out of such windows: it validates the world at plan time and acts on it later, so an approval or a price or a permission can quietly go stale before the tool fires. The defense is a single discipline — bind the check to the use: shrink the window, operate on an immutable handle to the exact thing you checked, and re-verify at the point of action rather than trusting a stale snapshot.
A check is a photograph. When your code asks 'is this file safe to open?', 'does this account have the balance?', 'did the human approve this action?', it captures the truth of a single instant. The quiet assumption underneath almost all such code is that the photograph is still accurate when you finally act on it. It usually is — which is exactly why the assumption is invisible, and why an attacker who can change the world in the gap between the check and the use gets to turn your own verified decision against you. The bug is not in the check and not in the use. It is in the window between them.
The assumption: a checked fact stays true
Ask a developer what makes a validation correct and they will talk about the logic of the check itself — the right predicate, the right threshold, the right permission. They will rarely mention the property the whole thing silently rides on: that the thing they checked is still the same thing, in the same state, when they use it. Call the failure of that property a time-of-check to time-of-use flaw, TOCTOU for short.
Matt Bishop and Michael Dilger gave this pattern its name and its first rigorous treatment in 1996, studying how a program that checks a file by name and then opens it by name can be fooled if the name is made to point somewhere else in between. Their insight generalizes far past files: any sequence of 'verify a condition, then act on it' assumes the condition did not change during the pause, and any attacker who can act inside that pause can break the assumption without ever defeating the check.
The reason this class hides so well is that it is relational in time, not in code. Read the check and it is correct. Read the use and it is correct. Only the ordering — and the attacker's ability to interleave with it — makes the pair unsafe, so a reviewer studying either half in isolation sees nothing wrong.
- State the invariant out loud in the design: for every 'check then use', name what must stay unchanged in between and who could change it.
- Prefer designs where the check and the use target the same immutable handle, so 'still the same thing' is guaranteed rather than hoped.
- Treat every gap between a validation and the action it authorizes as a security-relevant window to be measured and minimized.
Anatomy of the window
The shape is always the same three beats: a check reads the state of some shared resource and judges it safe; a window opens while control moves on; a use acts on that resource trusting the earlier judgement. The vulnerability is that the resource is shared and mutable, so another party can slip into the window and change what the name or reference points to, leaving the use operating on something the check never saw.
The attacker's craft is timing, not force. They do not need to forge a credential or corrupt memory — they only need to win a race, replacing the safe thing with a dangerous one after it has been blessed and before it is touched. Because the check already said yes, every downstream defense treats the now-dangerous object as trusted.
The width of the window is the whole game. A microsecond gap on a local variable is nearly impossible to hit; a multi-second gap that spans a network call, a human approval, or an agent's planning step is a comfortable target. Widen your mental model from 'threads' to 'anything that can change state concurrently', and the windows appear everywhere.
- Shrink the window to the smallest possible interval, ideally to zero by fusing the check and the use into one atomic operation.
- Remove sharing where you can: operate on a private copy or an exclusive handle so no other party can mutate the checked object.
- Assume any window that spans a syscall, a network hop, a human, or a planning step is wide enough to lose the race.
The classic: file-system races
The textbook instance is a privileged program that wants to act on a file only if the requesting user may access it. It checks the path for permission, then opens the same path to do the work. In the gap, an attacker replaces the path — often by swapping a symbolic link — so the check inspects a harmless file the user owns while the open lands on a sensitive file the user does not. The program's own privilege carries out the access the check was meant to prevent.
Nothing here breaks the permission logic; the logic is applied to the wrong object because the name was re-pointed between check and use. This is the exact scenario Bishop and Dilger formalized, and it is catalogued today as a named weakness class precisely because it recurs whenever code re-resolves a name it has already validated.
The fix is to stop trusting the name twice. Resolve the object once, obtain a stable handle to it, and perform both the permission decision and the action against that single handle so there is no second lookup to hijack.
- Operate on file descriptors, not names: open the object once and perform all checks and actions against that descriptor, so the referent cannot change underneath you.
- Refuse to follow untrusted links during the sensitive open, and open relative to a trusted directory handle rather than re-walking an attacker-influenced path.
- Never re-resolve a name you have already validated; a second lookup is a second opportunity for the attacker to substitute the target.
Beyond files: the general check-then-act race
Files are just the most photogenic example. The same window opens around any shared, mutable fact that is verified and then relied upon: read a balance then debit it, read a quota then consume it, read that a coupon is unused then redeem it, confirm a token is valid then spend it. If two operations can run concurrently, both can pass the same check before either performs the use, and the resource is spent twice — the classic double-spend.
The defenders' toolbox here is decades old and precise: make the check and the use one indivisible step. A database transaction with the right isolation, an atomic compare-and-swap, a conditional update that only applies 'if the value is still what I read', or a lock held across both operations all collapse the window so no interleaving can sneak between the verification and the effect.
The subtle failures come from checks and uses that look atomic but are not — a read in one service and a write in another, a validation in application code and an enforcement in a downstream system. Wherever the two halves live in different places, the window between them is real.
- Fuse check and use into one atomic operation: transactional updates, compare-and-swap, or conditional writes guarded by the value you checked.
- Enforce single-use with idempotency keys and unique constraints so a repeated or raced operation is rejected by the system of record, not by an earlier check.
- Hold a lock, or use optimistic concurrency that aborts when the underlying value changed, across the entire check-to-use span rather than around each half.
The AI agent's check-then-use gap
A modern language-model agent is, structurally, a long chain of check-then-use windows — and the windows are unusually wide. The agent reads the world to form a plan, a human or a policy approves that plan, and only later does a tool actually act. Every one of those handoffs is a pause in which the checked facts can drift: the price read during planning changes before the buy executes; the document that justified an action is edited before the action runs; the permission that was present at approval is revoked before the tool fires; the human approved 'send a refund of ten dollars' but the amount the tool ultimately submits is computed from state that moved.
This is the mechanism behind a whole family of agent failures that get lumped under vague labels like excessive agency. The approval was sound for the world as it was checked; the use happened in a different world. An attacker who can influence the shared state between plan and act — a retrieved record, a queue, an account balance, a file the agent will re-open — does not need to jailbreak the model at all. They only need to win the race between the agent's decision and its action.
The countermeasures mirror the classic ones but must be stated in the agent's vocabulary: bind the approval to the exact, immutable action rather than to a description of it, and re-verify the critical facts at the instant of use rather than trusting the snapshot from planning time.
- Bind approval to the exact action: approve a specific, immutable request (its precise parameters and a content hash), not a natural-language plan the executor re-derives later.
- Re-validate at the point of use: re-read balances, prices, permissions, and document contents at execution time and abort if they changed since the check.
- Keep approval windows short and single-use, with idempotency keys so a delayed or replayed action cannot fire twice or fire stale.
- Make tool calls transactional where possible — the tool itself enforces the precondition atomically instead of trusting the agent's earlier read.
Why the window cannot simply be abolished
It is tempting to demand that check and use always be atomic and be done with it. Real systems will not allow it. The moment a decision spans two services, a network, a human reviewer, or an agent's reasoning step, you have a distributed operation, and there is no free way to make a check in one place and a use in another happen as a single instant. Concurrency and latency are not bugs to be removed; they are the medium the system runs in.
So the honest goal is not to eliminate the window but to make it unwinnable. That means one of three moves, in order of strength: fuse the two halves into a genuinely atomic operation where you can; bind the use to the exact object the check blessed, via an immutable handle, so a swap is impossible; or, where neither is available, re-verify at the point of use so a changed world is caught before the action commits.
Framed this way, TOCTOU stops being a grab-bag of file bugs and becomes a design question you can ask of any system: for each decision, is the thing I act on provably the thing I checked?
- Accept the window as inherent in any distributed or agentic decision and pick one durable control per seam — fuse, bind, or re-verify — instead of adding more checks.
- Where atomicity is impossible, make the use itself enforce the precondition (a conditional/transactional action), so a changed world aborts the commit.
- Minimize what a lost race can cost: least privilege and tight scopes on the action, so even a successful stale-use has a small blast radius.
Finding races before an attacker does
Races are notoriously hard to catch because they only appear under specific interleavings, so ordinary testing — which tends to run one thing at a time — sails right past them. The defensive method is to deliberately provoke the window: run the check and the use under concurrency, inject delays into the gap to widen it artificially, and assert an invariant that must hold at the moment of use rather than only at the moment of check.
For a file or resource path, the test tries to swap the referent during the window and confirms the code operates on a stable handle. For an agent, the test mutates the relevant state — a price, a record, a permission — between plan approval and tool execution and confirms the action either re-validates or aborts. The useful signal is simple: did any injected change between check and use ever reach the action unnoticed?
The harness must stay strictly defensive: it perturbs your own system's timing to reveal an unguarded window; it never attacks a third party, and it uses benign markers rather than real damaging payloads.
- Add concurrency and delay-injection tests around every check-then-use seam, and fail the build if an injected mid-window change ever reaches the action.
- Assert use-time invariants (the object is the one checked; the value is unchanged) inside the action itself, so the test and production share the same guard.
- Threats to validity: a passing race test only proves the interleavings you tried are safe — pair it with a design that binds check to use so the untested interleavings cannot matter.
The discipline: bind the check to the use
Every countermeasure here is one rule wearing different clothes: never act on a fact you merely used to hold; act only on a fact you still hold at the instant of action. For files, that means one handle for both the permission decision and the work. For shared state, one atomic operation for both the read and the write. For an agent, one immutable, hashed action for both the approval and the execution, re-verified at the moment it fires.
The reusable artifact is an assumption-ledger entry: the unstated assumption is that a checked fact stays true until use; the reason it fails is that shared state is mutable and check and use are not atomic; the observable tell is any change to the resource between the check and the action; and the assumption-free control is to fuse, bind, or re-verify at the point of use. A defender can take that entry to any system and ask the one question this class demands.
Ask it everywhere a decision precedes an action: is what I am about to act on provably the same as what I approved? Where the answer is 'probably', a window is open — and time is the attacker's to spend.
- Adopt a single rule — fuse, bind, or re-verify — for every check-then-use seam, and record which one each seam relies on.
- Log every gap between an authorization and its effect so stale-approval windows are visible and auditable.
- Audit the agent stack specifically for plan-time checks whose facts are trusted at act-time without re-validation.
Key takeaways
- A TOCTOU flaw lives in the window between a check and the use that trusts it — not in either half — so single-component review misses it.
- The broken assumption is that a checked fact stays true until you act; it fails because shared state is mutable and the two steps are not atomic.
- The classic file-system race hijacks a re-resolved name; the general form is any check-then-act on shared state, including double-spend.
- A modern AI agent is full of unusually wide check-then-use windows: facts validated at plan time are trusted at act time, so a stale approval, price, or permission becomes the exploit.
- You cannot out-check a race; safety comes from fusing check and use, binding the use to an immutable handle, or re-verifying at the point of action.
- Races hide from ordinary testing — provoke them deliberately with concurrency and delay injection, and fail closed when a mid-window change reaches the action.
Practitioner Toolkit
Copy-paste, strictly defensive artifacts you can use today. Nothing here attacks a real system.
Run this at every seam where a check precedes an action.
- Name the invariant: what must stay unchanged between the check and the use, and who could change it?
- Confirm the use acts on the SAME immutable handle the check blessed — no re-resolved name, no second lookup.
- Where possible, fuse check and use into one atomic operation (transaction, compare-and-swap, conditional write).
- For agents, verify approval is bound to the exact hashed action and that critical facts are re-read at execution time.
- Add single-use idempotency keys so a raced or replayed action cannot fire twice or fire stale.
- Add a concurrency test that mutates state inside the window and fails if the stale value reaches the action.
A drop-in rule for the file/resource check-then-use seam.
def safe_use(path, actor):
handle = open_no_follow(path) # resolve the object ONCE
meta = stat_handle(handle) # check against the HANDLE, not the name
if not permitted(actor, meta):
return reject("not allowed")
return act_on(handle) # use the SAME handle; referent cannot change
# Anti-pattern: check(path) then open(path) — the name can be re-pointed between the two.Reveals an unguarded check-then-use window in your own system.
def race_probe(run_check, run_use, mutate_state):
blessed = run_check() # what the check approved
inject_delay(ms=250) # widen the window artificially
mutate_state(marker="CANARY") # benign change inside the window
result = run_use()
assert marker_not_in(result), \
"stale value reached the action — TOCTOU window is exploitable"
# Never point this at a third party; it perturbs only your own system's timing.The highest-leverage steps before deeper hardening.
- Open once and act on the descriptor; never re-resolve a validated name.
- Wrap check-and-use in one transaction or compare-and-swap for shared mutable state.
- For agents, approve a hashed exact action and re-verify facts at execution time.
- Add idempotency keys so raced or replayed actions take effect at most once.
Glossary
- TOCTOU
- Time-of-check to time-of-use: a flaw where a resource changes between when it is verified and when it is used.
- Check-then-use window
- The interval between validating a condition and acting on it, during which shared state can change.
- Race condition
- A defect whose outcome depends on the timing or interleaving of concurrent operations on shared state.
- Atomic operation
- An operation that completes as a single indivisible step, so no other party can observe or change state midway.
- Compare-and-swap
- An atomic update that applies only if the value still equals the one previously read, collapsing a check-then-use into one step.
- Idempotency key
- A unique token that lets the system of record reject a repeated or raced operation so it takes effect at most once.
- Symbolic-link race
- Swapping the target of a name between a program's permission check and its open, so it acts on a different file.
References
- CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition
- CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')
- Bishop & Dilger, Checking for Race Conditions in File Accesses (Computing Systems, 1996)
- OWASP Top 10 for Large Language Model Applications
- MITRE ATLAS (Adversarial Threat Landscape for AI Systems)
- NIST AI Risk Management Framework (AI RMF 1.0)