TOCTOU File Race Conditions in C Explained
Time-of-Check to Time-of-Use (TOCTOU) race conditions are a critical class of software vulnerabilities that occur when a program checks the state of a resource and assumes that state remains constant until the resource is subsequently used. In the C programming language, these vulnerabilities frequently manifest during file operations where a program verifies a file's metadata—such as existence, ownership, or permissions—before opening, modifying, or executing it. This article breaks down the mechanics of TOCTOU flaws in C, explores the underlying causes that allow attackers to exploit them, and outlines the standard development practices required to prevent them.
The Core Problem: Non-Atomic Operations
The root cause of a TOCTOU race condition is the lack of atomicity
between two distinct system calls. In C, standard libraries and
operating system APIs often split operations into separate steps. For
example, a developer might use access() to check if a user
has permission to write to a file, and then call fopen() or
open() to access the file if the check passes.
// Vulnerable pattern
if (access("/tmp/shared_file.txt", W_OK) == 0) {
// A context switch or concurrent process can alter the file here
FILE *f = fopen("/tmp/shared_file.txt", "w");
if (f) {
fputs("Sensitive data", f);
fclose(f);
}
}Between the invocation of access() (the check) and
fopen() (the use), the kernel's scheduler may pause the
executing process to run another thread or process. During this brief
window, the state of the filesystem can change. If a malicious actor
alters the target file between these two operations, the application
proceeds based on obsolete, invalid assumptions.
File Paths vs. File Descriptors
TOCTOU vulnerabilities are heavily exacerbated by referencing files
via string paths rather than persistent file handles. Path resolution is
dynamic; every time a system call accepts a path string like
"/tmp/shared_file.txt", the kernel traverses the directory
tree from scratch to locate the corresponding inode on the disk.
This design introduces specific exploitation vectors:
- Symlink Swapping: An attacker can monitor the
system and replace a legitimate file with a symbolic link pointing to a
protected resource (such as
/etc/shadowor critical system binaries) immediately after the program passes its security check. - Directory Renaming: An attacker can rename an intermediate directory within the path, causing the program to write to or read from an unintended location.
Because the program evaluates the path twice—once to check validity and once to perform the I/O—the path may resolve to two entirely different physical files.
Concurrency and Preemptive Multitasking
Modern operating systems rely on preemptive multitasking, where the
CPU rapidly switches between running processes. Even without an
intentional delay in the C code, an attacker can flood the CPU or use
filesystem manipulation loops (such as calling rename() or
symlink() in a tight loop) to win the race condition. When
the application's time slice expires between the check and the use, the
attacker’s process executes and modifies the target path before the
original program resumes.
How to Prevent TOCTOU in C
Eliminating TOCTOU race conditions requires designing file operations so that checks and actions occur simultaneously (atomically), or ensuring operations bind directly to file descriptors rather than paths.
- Avoid Pre-Checking APIs: Do not use
access(),stat(), orlstat()to make authorization decisions before anopen()call. Instead, attempt the operation directly and handle any returned errors (such asEACCESorEEXIST). - Use Atomic Open Flags: Use POSIX
open()flags to guarantee atomic behavior:O_CREAT | O_EXCL: Ensures the file is created only if it does not already exist, preventing attackers from pre-creating files.O_NOFOLLOW: Fails the call if the target file is a symbolic link, mitigating symlink redirection attacks.
- Bind to Descriptors Immediately: Perform metadata
checks on an open file descriptor using
fstat()rather thanstat(). Once a file descriptor is obtained viaopen(), it references a specific inode, rendering path manipulations like symlink swaps ineffective. - Use Directory Descriptors: APIs such as
openat(),fstatat(), andunlinkat()allow operations relative to an already-opened directory descriptor, drastically reducing the attack surface for path-traversal and renaming races.