A program often checks a file before using it. It may confirm that a path is inside an allowed directory, that the target is not a symbolic link, that the file belongs to an expected user, or that it does not already exist. The code then opens, replaces, deletes, or executes the file.
The security problem is the gap between those two operations. If another actor can change the relevant filesystem state after the check but before the use, the program may validate one object and operate on another. This is a time-of-check to time-of-use race, often shortened to TOCTOU.
The consequence depends on the operation. A privileged service might overwrite an unintended file, read data outside the expected boundary, or apply a trusted action to a file that was substituted after validation.
This article develops one defensive rule: make the security decision about the same file object you actually use. You will learn why path checks can become stale, how handle-based and atomic filesystem operations reduce the race window, where link handling matters, and what residual risks remain.
A pathname is a lookup instruction, not a stable object
Start with a simple mental model. A pathname such as:
/work/jobs/report.txtis not the file itself. It tells the operating system how to look up an object in the filesystem namespace.
If the namespace can change, two lookups of the same pathname do not necessarily refer to the same object:
lookup path -> object A
filesystem changes
lookup path -> object BThis distinction matters whenever security code performs one lookup to validate a path and a later lookup to perform the sensitive operation.
Consider simplified pseudocode:
if is_allowed("/work/jobs/report.txt"):
file = open("/work/jobs/report.txt")
write_report(file)The code appears to enforce a useful order: check first, then write. But is_allowed() and open() are separate operations. If an untrusted actor can alter the path or one of its parent directories between them, the object opened by the second operation may not be the object that passed the first check.
The flaw is therefore not merely that the check was weak. The check can be perfectly correct about the object it observed and still become irrelevant before the use occurs.
State the threat model before choosing a defense
A filesystem race becomes security-relevant when an untrusted or less-trusted actor can change namespace state that a more privileged operation relies on.
Typical examples include shared writable directories, upload staging areas, temporary workspaces, build directories influenced by another process, and service workflows where one component creates a path that another component later consumes.
The control described here is intended to reduce the risk that an attacker or concurrent process substitutes a different filesystem object between validation and use.
It does not protect against every file-processing problem. If the file contents themselves are malicious, opening the correct file does not make parsing those contents safe. If the application has permission to damage the intended file, race-resistant lookup does not correct excessive privilege. If an attacker already controls the privileged process, filesystem lookup rules are not a meaningful containment boundary.
The important assumption is narrower: the process performing the sensitive action is trusted, but some filesystem namespace or input it uses can change concurrently.
See why a pre-check can lose its meaning
Suppose a service accepts a filename for a generated export. It wants to avoid overwriting an existing file, so it does this:
if not exists(destination):
create(destination)There are two separate decisions here:
check: destination does not exist
use: create destinationAnother process may create something at that path after exists() returns but before create() runs. If create() follows ordinary replacement or link behavior, the result may differ from what the check intended.
The safer design is to ask the filesystem to combine the important condition with the operation:
create destination only if it does not already existThat is an atomic operation for this decision: from the application’s point of view, there is no successful state in which the existence check refers to one moment and creation silently proceeds under a conflicting later state.
The exact API is platform-specific, but the principle is portable. When a filesystem interface can enforce a security condition as part of the open, create, rename, or replacement operation, prefer that interface over a separate pre-check.
Open first, then reason about the opened object
Some decisions cannot be expressed entirely as open-time conditions. You may need to inspect ownership, object type, permissions, or other metadata.
In that case, change the order of reasoning. Instead of inspecting a pathname and later reopening it, obtain a handle to the object and inspect the object represented by that handle:
handle = open_with_required_constraints(path)
metadata = inspect_open_object(handle)
if not acceptable(metadata):
close(handle)
reject()
use_open_object(handle)The key property is that validation and use refer to the same opened object rather than two independent pathname resolutions.
A handle here means the operating-system reference returned by a successful open operation, such as a file descriptor or equivalent platform object. The exact guarantees differ by operating system and filesystem, so production code should use the platform’s documented APIs rather than assuming every handle has identical semantics.
This pattern also improves the security question. Instead of asking, “What does this path appear to name right now?” the program asks, “What object did I actually open, and is that object acceptable for this operation?”
That is usually the decision that matters.
Treat symbolic links and parent directories as part of resolution
Rejecting a symbolic link at the final path component is useful in some designs, but it is not a complete path-confinement strategy.
A path is resolved component by component. Parent directories can matter too:
trusted-root / job / output.txt
^
this component also participates in lookupIf an attacker can replace or redirect an intermediate component, checking only output.txt may leave another substitution path.
For security-sensitive operations, define the boundary you intend to trust. A stronger design often starts from an already-open trusted directory and resolves descendants relative to that directory using platform facilities that can constrain link following or path traversal. This reduces dependence on repeatedly resolving a long absolute pathname through mutable namespace components.
Do not reproduce these semantics by string manipulation alone. Removing .., comparing prefixes, or rejecting a visible link name does not itself control how the operating system resolves the path at the moment of use.
Path normalization is useful for interpreting names consistently. Race resistance is a different property: it requires the filesystem operation itself to preserve the relationship between the object checked and the object used.
Use atomic operations for state transitions
The same mental model applies beyond opening files. Security-sensitive filesystem workflows often contain transitions such as:
create if absent
replace only this target
move completed file into place
claim work item onceWhen these transitions are implemented as “check, then act,” concurrent processes can invalidate the assumption between steps.
Prefer filesystem primitives that express the intended transition directly and atomically when the platform provides them. For example, creating a new file with a fail-if-present condition is stronger than checking for absence first. An atomic rename can be stronger than copying data into a final path while readers are allowed to observe partial state, provided the platform’s documented rename guarantees match the design.
Atomicity is not magic. It applies to a particular operation and filesystem contract. Network filesystems, unusual storage layers, and cross-filesystem moves may have different behavior. Verify the guarantees of the environment that enforces your security boundary.
Keep temporary workspaces private when possible
The easiest race to defend is one another actor cannot enter.
If a service creates temporary or intermediate files, place them in a directory that untrusted users and unrelated processes cannot modify when the operating model allows it. Restricting write access reduces the number of actors that can rename entries, insert links, or replace objects during the workflow.
This is defense in depth, not a reason to keep unsafe check-then-use code. Permissions can be misconfigured, deployment assumptions can change, and multiple components may legitimately share a workspace later. Race-resistant operations make the correctness condition local to the code instead of depending entirely on directory exclusivity.
For a simple single-user tool operating only inside a private directory, strict directory permissions plus ordinary file APIs may be sufficient for the threat model. For a privileged service consuming paths from less-trusted actors, stronger open-time constraints and handle-based validation are justified.
Do not turn retries into repeated races
A failed atomic operation often means the world changed in a way the program must reconsider. Treat that failure as information, not as an instruction to fall back to a weaker path.
For example:
attempt constrained open
|
+--> success -> inspect and use opened object
|
+--> failure -> reject or restart the whole decisionA dangerous recovery pattern is:
constrained operation fails
|
v
retry with ordinary unrestricted openThat converts a security check into a preference. Under contention or deliberate manipulation, the code may eventually take the path with fewer guarantees.
If retry is appropriate, repeat the complete security decision under the same constraints. If the operation cannot establish its required conditions, fail without performing the sensitive action.
Verify the control with adversarial concurrency tests
Race bugs can disappear during ordinary testing because the vulnerable interval is small. Testing should therefore exercise the assumption directly.
Create a test environment where one worker repeatedly changes the relevant path or directory entry while another performs the protected operation. The expected invariant should be simple and observable, such as:
writes occur only to objects created inside the test destinationor:
the service never accepts a link where a regular file is requiredThe purpose is not to reproduce a real attack. It is to create scheduling pressure and confirm that the protected operation either acts on an acceptable object or fails cleanly.
Also test normal concurrent behavior. A race-resistant design that frequently corrupts work, deadlocks, or falls back to unsafe behavior under load is not operationally sound.
Know what this control does not solve
Using atomic and handle-based filesystem operations reduces one class of namespace race. Several related risks still need separate controls.
Malicious content remains malicious content. A correctly opened image, archive, document, or executable can still exploit a vulnerable parser or violate application rules. Validate content and isolate risky processing where the threat model requires it.
Authorization still matters. Proving that a file is inside an intended directory does not prove that the current user is allowed to read or modify that file.
Least privilege still limits impact. If a process can write to sensitive system locations, a bug outside this particular path-handling flow may still misuse that authority. Give the process only the filesystem permissions it needs.
Platform semantics matter. Link handling, directory-relative lookup, atomic creation, rename behavior, and handle semantics are operating-system and filesystem concerns. Use well-maintained platform or framework facilities and verify their documented guarantees for the deployment environment.
These are complementary controls. Race resistance ensures that a security decision does not quietly detach from the object being acted upon; it does not replace the decisions about whether that object or action should be trusted in the first place.
Review file operations by asking one question
When reviewing security-sensitive file code, trace every important check to the later operation it is meant to protect.
If the flow looks like this:
inspect path
|
other code can run
|
resolve path again
|
perform sensitive actionask whether mutable filesystem state can make the second resolution identify a different object.
Then look for a design that collapses the condition into an atomic filesystem operation or carries forward a handle to the object that was actually opened. Protect the containing directory where practical, constrain link and descendant resolution according to the platform, and avoid weaker fallback behavior.
The goal is not to eliminate concurrency. The goal is to stop treating a past observation of a pathname as a permanent security fact.
Conclusion
A pathname can change meaning between two filesystem operations. That makes “check this path, then use this path” unsafe whenever a less-trusted actor can modify the namespace in between.
Build the defense around object identity instead. Express security conditions atomically when the filesystem supports them, inspect metadata through the opened object when possible, keep using that same object handle, and constrain path resolution from a trusted directory for higher-risk workflows.
The practical rule is simple: do not authorize one filesystem object and then accidentally operate on another. Make the check and the use refer to the same object, and treat any inability to preserve that relationship as a reason not to perform the sensitive action.