Time-of-check Time-of-use (TOCTOU) Race Condition

Description

Time-of-check Time-of-use (TOCTOU) Race Condition occurs when a program checks the state of a resource and then uses that resource based on the check, but the resource's state can change between the check and use, invalidating the results of the check. This is a race condition that attackers can exploit by manipulating the resource between these two operations. Common examples include checking file permissions before accessing a file, verifying user credentials before granting access, or checking resource availability before allocation.

Risk

TOCTOU vulnerabilities are particularly dangerous in setuid programs and privileged services. An attacker can create symbolic links, replace files, or modify resources during the race window to gain unauthorized access, escalate privileges, or manipulate data. File system TOCTOU attacks are well-documented and can bypass security checks. The window of vulnerability may be small but is often exploitable, especially on systems under load or with attacker-controlled timing.

Solution

Use atomic operations that combine check and use into a single step. Use file descriptors instead of filenames after opening. Implement proper locking mechanisms. Use fstat() on an open file descriptor instead of stat() on a filename. Apply the principle of least privilege. On Unix systems, use O_NOFOLLOW flag to prevent symlink attacks. Use openat() and related functions for safe file operations. Consider using mandatory access controls (MAC) to limit race window impact.

Common Consequences

ImpactDetails
Access ControlScope: Privilege Escalation

TOCTOU in setuid programs can allow attackers to access privileged files or execute code with elevated privileges.
IntegrityScope: Data Manipulation

Attackers can modify files or resources after security checks pass.
ConfidentialityScope: Information Disclosure

TOCTOU can enable reading files that should be inaccessible.

Example Code + Solution Code

Vulnerable Code

// VULNERABLE: Classic TOCTOU with access() then open()
void read_file(const char *filename) {
    // Check if user has access
    if (access(filename, R_OK) == 0) {
        // RACE WINDOW: Attacker replaces file with symlink
        FILE *fp = fopen(filename, "r");
        // Now reading different file (e.g., /etc/shadow)!
        char buffer[1024];
        fread(buffer, 1, sizeof(buffer), fp);
        fclose(fp);
    }
}

// VULNERABLE: stat() then open()
void process_file(const char *filename) {
    struct stat st;
    if (stat(filename, &st) == 0) {
        // Check it's a regular file
        if (S_ISREG(st.st_mode)) {
            // RACE WINDOW: file replaced with symlink or directory
            int fd = open(filename, O_RDONLY);
            // Operating on different file!
        }
    }
}

// VULNERABLE: Check existence then create
void safe_create_file(const char *filename) {
    struct stat st;
    if (stat(filename, &st) != 0) {
        // File doesn't exist
        // RACE WINDOW: attacker creates file/symlink
        FILE *fp = fopen(filename, "w");
        // Writing to attacker-controlled location!
        fprintf(fp, "sensitive data");
        fclose(fp);
    }
}
// VULNERABLE: TOCTOU in setuid program
void setuid_write(const char *filename, const char *data) {
    // Running as root, checking if real user has access
    if (access(filename, W_OK) == 0) {
        // RACE WINDOW: attacker replaces with symlink to /etc/passwd
        FILE *fp = fopen(filename, "w");
        // Writing as root to attacker's target!
        fputs(data, fp);
        fclose(fp);
    }
}

// VULNERABLE: Directory creation race
void create_directory(const char *dir) {
    struct stat st;
    if (stat(dir, &st) != 0) {
        // Directory doesn't exist
        // RACE WINDOW: attacker creates symlink
        mkdir(dir, 0755);
        // Created directory at symlink target!
    }
}

// VULNERABLE: Temp file creation
void write_temp_file(const char *data) {
    char template[] = "/tmp/myapp.XXXXXX";
    // Get unique filename
    char *filename = mktemp(template);  // Deprecated!

    // RACE WINDOW: attacker creates file with same name
    FILE *fp = fopen(filename, "w");
    fputs(data, fp);
    fclose(fp);
}
# VULNERABLE: Check then access pattern
import os

def read_user_file(filename):
    # Check if file exists and is accessible
    if os.path.exists(filename):
        if os.access(filename, os.R_OK):
            # RACE WINDOW
            with open(filename, 'r') as f:
                return f.read()
    return None

# VULNERABLE: Check type then use
def process_path(path):
    if os.path.isfile(path):
        # RACE WINDOW: path changed to symlink
        with open(path, 'r') as f:
            return f.read()
    elif os.path.isdir(path):
        # RACE WINDOW: path changed
        return os.listdir(path)

Fixed Code

// SAFE: Open then check (using file descriptor)
void read_file_safe(const char *filename) {
    // Open first - this is atomic
    int fd = open(filename, O_RDONLY | O_NOFOLLOW);
    if (fd < 0) {
        perror("Cannot open file");
        return;
    }

    // Now check properties using the file descriptor
    struct stat st;
    if (fstat(fd, &st) == 0) {
        if (S_ISREG(st.st_mode)) {
            // Safe - we're operating on the opened file
            char buffer[1024];
            read(fd, buffer, sizeof(buffer));
        }
    }
    close(fd);
}

// SAFE: Use O_CREAT | O_EXCL for atomic creation
void safe_create_file_fixed(const char *filename) {
    // O_EXCL fails if file exists - atomic check+create
    int fd = open(filename, O_WRONLY | O_CREAT | O_EXCL, 0600);
    if (fd < 0) {
        if (errno == EEXIST) {
            // File exists - handle appropriately
            return;
        }
        perror("Cannot create file");
        return;
    }

    // File created safely
    write(fd, "sensitive data", 14);
    close(fd);
}

// SAFE: Use mkstemp for temp files
void write_temp_file_safe(const char *data) {
    char template[] = "/tmp/myapp.XXXXXX";
    // mkstemp atomically creates and opens the file
    int fd = mkstemp(template);
    if (fd < 0) {
        perror("Cannot create temp file");
        return;
    }

    // File is already open - no race
    write(fd, data, strlen(data));
    close(fd);

    // Optionally unlink the temp file when done
    unlink(template);
}
// SAFE: Drop privileges before file access
void setuid_write_safe(const char *filename, const char *data) {
    uid_t real_uid = getuid();

    // Temporarily drop privileges
    if (seteuid(real_uid) != 0) {
        perror("Cannot drop privileges");
        return;
    }

    // Now open as real user - no privilege escalation possible
    int fd = open(filename, O_WRONLY | O_NOFOLLOW);
    if (fd < 0) {
        perror("Cannot open file");
        seteuid(0);  // Restore if needed
        return;
    }

    // Write as real user
    write(fd, data, strlen(data));
    close(fd);

    // Restore privileges if needed for other operations
    seteuid(0);
}

// SAFE: Use openat() for directory-relative operations
void process_in_directory_safe(int dirfd, const char *filename) {
    // Open relative to directory file descriptor
    int fd = openat(dirfd, filename, O_RDONLY | O_NOFOLLOW);
    if (fd < 0) {
        return;
    }

    // Safe operations on opened file
    struct stat st;
    fstat(fd, &st);
    // ...
    close(fd);
}

// SAFE: Atomic directory creation
void create_directory_safe(const char *dir) {
    // mkdir is atomic - either creates or fails
    if (mkdir(dir, 0755) != 0) {
        if (errno == EEXIST) {
            // Check it's actually a directory
            struct stat st;
            if (stat(dir, &st) == 0 && S_ISDIR(st.st_mode)) {
                // Directory exists - OK
                return;
            }
            // Something else exists with that name
            fprintf(stderr, "Path exists but is not a directory\n");
        }
        perror("Cannot create directory");
    }
}
# SAFE: Open first, then check
import os
import stat

def read_user_file_safe(filename):
    try:
        # Open first - atomic
        fd = os.open(filename, os.O_RDONLY | os.O_NOFOLLOW)
        try:
            # Check using file descriptor
            st = os.fstat(fd)
            if stat.S_ISREG(st.st_mode):
                # Safe to read
                with os.fdopen(fd, 'r') as f:
                    return f.read()
        except:
            os.close(fd)
            raise
    except OSError as e:
        return None

# SAFE: Atomic temp file creation
import tempfile

def write_temp_safe(data):
    # mkstemp equivalent - atomic creation
    with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
        f.write(data)
        return f.name

# SAFE: Use exclusive creation
def create_file_safe(filename):
    try:
        # O_EXCL equivalent - atomic
        fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
        with os.fdopen(fd, 'w') as f:
            f.write("sensitive data")
        return True
    except FileExistsError:
        return False

Exploited in the Wild

Setuid /tmp Race Conditions (Historical)

Numerous Unix setuid programs have been exploited via TOCTOU attacks involving /tmp files. Attackers create symlinks during the race window to overwrite or read privileged files.

Linux Kernel TOCTOU Vulnerabilities

Multiple Linux kernel vulnerabilities have resulted from TOCTOU conditions in system calls, including CVE-2016-9806 which allowed privilege escalation via race conditions in netlink handling.

Docker Container Escape (2019)

CVE-2019-5736 allowed container escape through a TOCTOU race condition in runc, enabling overwrite of the host runc binary.


Tools to test/exploit

  • syzkaller — kernel fuzzer that can find race conditions.

  • RaceFuzzer — research tool for detecting races.

  • KLEE — symbolic execution that can detect TOCTOU.

  • ThreadSanitizer — runtime race detector.


CVE Examples


References

  1. MITRE. "CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition." https://cwe.mitre.org/data/definitions/367.html

  2. CERT. "FIO01-C: Be careful using functions that use file names for identification." https://wiki.sei.cmu.edu/confluence/display/c/FIO01-C.+Be+careful+using+functions+that+use+file+names+for+identification