DEV Community

Cover image for What Is TOCTOU? How Can Something Change Between Checking It and Using It?
Aditya Sharma
Aditya Sharma

Posted on

What Is TOCTOU? How Can Something Change Between Checking It and Using It?

A program checks whether a file is safe to open. The check passes. The program opens the file.

These two things happen in sequence. The check comes first, then the operation. That feels like the correct order. Verify, then act.

But here's the assumption hiding inside that sequence: the program trusts that the thing it opens is the same thing it checked.

What if it isn't?


The Gap You're Not Thinking About

When a program checks a resource and then uses that resource, those are two separate operations. The check happens. Time passes. Then the use happens.

"Time passes" sounds dramatic. It doesn't have to be long. It could be microseconds. The point isn't the duration. The point is that a window exists, and during that window, the world is allowed to change.

If something changes between the check and the use, the check's conclusion becomes outdated. The program acts on an assumption that is no longer true.

This is the shape of the problem:

CHECK
  ↓
"This is safe."
  ↓
  [time passes — even a tiny amount]
  ↓
  [state changes]
  ↓
USE
  ↓
"Wait... is this still what I checked?"
Enter fullscreen mode Exit fullscreen mode

The program can't rely on that earlier check anymore. It already moved on.


What a Race Condition Actually Is

Before naming this specific bug, it's worth understanding the broader category it belongs to.

A race condition is a bug where the correct outcome depends on the timing or ordering of events. Two things are happening, and which one finishes first changes what the program does. The program isn't built to handle that variation, so the behavior becomes unpredictable.

TOCTOU (Time-of-Check to Time-of-Use) is a specific form of race condition. The race is between:

  1. The program's check
  2. Any change to the checked state If a change wins that race, the program's assumption is wrong before it acts on it.

The code doesn't have to be slow for this to matter. A check that takes nanoseconds still isn't atomic with the operation that follows it. The window exists regardless.


The Filesystem Makes This Concrete

Filesystem operations are where TOCTOU becomes easiest to see clearly.

A pathname is not a direct handle to a file. It's a name. When a program uses a pathname, the operating system resolves it through the current state of the filesystem to find the actual resource.

"Current state" is the key phrase. Filesystem state is not frozen between operations. Other processes can create files, delete files, replace files, and rearrange directories. All of that can happen between any two operations a program performs.

So when a program checks a pathname and then opens that pathname as a separate step, it's asking two questions that may get different answers:

  • "What does this pathname resolve to right now?" (at check time)
  • "What does this pathname resolve to right now?" (at use time) Same question. Different moments. Potentially different answers.

How Filesystem State Can Change: Symbolic Links

One way this becomes concrete is through symbolic links.

A symbolic link is a filesystem entry that points to another path. When the OS resolves a pathname containing a symlink, it follows the link to its target. The symlink itself isn't the destination. It's a redirection.

This matters for TOCTOU because a symlink can be replaced. At check time, a pathname might resolve to a regular file. By the time the program opens the same pathname, another process may have swapped in a symlink pointing somewhere else entirely.

The program checked one thing. It opened another.

This isn't a flaw in how symlinks work. Symlinks are a normal, useful filesystem feature. The vulnerability comes from the program's assumption that resolving the same pathname twice will always reach the same resource.

That assumption isn't guaranteed by anything.


It's Not Just Filesystems

The check-then-use race isn't specific to files. Any shared state that can change between a check and a dependent operation has the same shape.

Consider a coupon redemption system. The server checks whether a coupon has a remaining use. It does. A moment later, the server redeems the coupon.

But "a moment later" isn't atomic. Between the check and the redemption, another request might have already used the last remaining count. The check observed valid state. By the time the redemption ran, that state was gone.

The server didn't make a logical error. It checked before acting. The problem is that checking and acting are two operations, and shared state can move between them.

Authorization decisions have the same shape. A permission check describes the state at one point in time. If that state changes before the access happens, the check's conclusion no longer reflects reality. The underlying structure is always the same: a result that was valid when produced but stale when relied upon.


Atomicity Is the Actual Answer

The natural response to all of this is to check again, right before using. But that doesn't help. A second check just creates two gaps instead of one. The race between the most recent check and the use still exists.

Speed doesn't fix it either. A faster check is still a separate operation. The problem isn't latency. It's that two operations that need to be atomic aren't.

If the gap between CHECK and USE is the problem, the solution is to eliminate the gap.

An atomic operation is one that completes without interruption from another operation's perspective. It either happens fully or not at all. Nothing can observe a half-completed state or modify the resource between the check and the action.

In filesystems, this often means using OS primitives that combine checking and acting into a single call. Instead of checking whether a file exists and then creating it, use an operation that creates the file only if it doesn't exist, atomically. The check and the creation are the same step.

In databases, transactions can group related reads and writes into a controlled unit so concurrent operations don't invalidate the assumptions those operations rely on.

Atomicity is a property of the operation, not of the source code. Writing a check and a use on adjacent lines doesn't make them atomic. The underlying system has to guarantee that nothing can intervene.


When It Becomes a Security Vulnerability

Not every check-then-use pattern is a security problem. If the checked state is something only the program itself modifies, and there are no concurrent actors, the practical risk is low.

It becomes a security vulnerability when the checked state crosses a security boundary and an external actor can influence it.

If a privileged program checks a pathname and then performs a privileged operation using that pathname, and another process can modify what that pathname resolves to between those steps, the privileged operation may act on a resource it never validated. The check was real. The privilege was real. But the resource the privilege was applied to isn't what the check examined.

The security check protected the past. The operation ran on the present.


Defenses That Follow From the Mechanism

Because the problem is the gap between checking and using, the defenses are all about closing or avoiding that gap.

Use atomic operations. When the language, OS, or library provides a way to check and act in one step, prefer it over the two-step equivalent.

Work with stable references. After a resource is opened, use a handle to that specific resource rather than resolving its name again. A file descriptor is tied to the file that was opened, not to the name it was opened from. Further operations on that descriptor bypass pathname resolution entirely.

Use appropriate synchronization when multiple concurrent actors share state that drives security decisions. Synchronization introduces complexity, and poorly designed locks can still leave gaps, but it's the right primitive when OS-level atomicity isn't available.

Treat the filesystem as mutable between operations. Any architecture that assumes a pathname resolves consistently across two separate calls is relying on a guarantee the OS doesn't make.


The Core Idea

A check tells you what was true at the moment of the check. It doesn't say anything about what will be true later.

If that distinction sounds obvious, it's because it is. What's less obvious is how often software implicitly assumes otherwise. A permission check at the start of a function. An existence check before an open. A balance read before a write. Each one is a snapshot. The operation that follows it happens in a later moment.

Between those two moments, the world is still running.

TOCTOU is what happens when something else uses that gap.

Top comments (0)