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/shadow or 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.

  1. Avoid Pre-Checking APIs: Do not use access(), stat(), or lstat() to make authorization decisions before an open() call. Instead, attempt the operation directly and handle any returned errors (such as EACCES or EEXIST).
  2. 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.
  3. Bind to Descriptors Immediately: Perform metadata checks on an open file descriptor using fstat() rather than stat(). Once a file descriptor is obtained via open(), it references a specific inode, rendering path manipulations like symlink swaps ineffective.
  4. Use Directory Descriptors: APIs such as openat(), fstatat(), and unlinkat() allow operations relative to an already-opened directory descriptor, drastically reducing the attack surface for path-traversal and renaming races.