Security - TOCTOU

TOCTOU (pronounced "tock-too") stands for Time-of-Check to Time-of-Use. It is a software vulnerability and race condition that occurs when a program checks the status or validity of a resource (the Check) and then performs an operation on that resource (the Use), but the state of the resource changes in the tiny window of time between the two steps.

An attacker takes advantage of this gap by swapping or altering the resource right after the program verifies it, forcing the application to act on untrusted or unintended data.

Key Insight: Paths vs. File Descriptors

A classic distinction in Linux systems security highlights why TOCTOU vulnerabilities happen and how to eliminate them:

Checking a path and then acting on that path is a TOCTOU; checking an fd and then acting on that fd is not.

Why Checking a Path is a TOCTOU

A file path (e.g., /tmp/user_data.txt) is merely a string pointer to a location in the filesystem tree. It is not an immutable bound resource.

  1. Check: The program calls access("/tmp/user_data.txt", R_OK) to verify the current user owns and can read the file.
  2. Race Window: An attacker quickly deletes /tmp/user_data.txt and replaces it with a symbolic link pointing to /etc/shadow.
  3. Use: The program opens /tmp/user_data.txt using fopen("/tmp/user_data.txt", "r").

Because the path was re-resolved by the kernel during fopen, the program checks one target (/tmp/user_data.txt) but operates on another (/etc/shadow).

Why Checking a File Descriptor is NOT a TOCTOU

A file descriptor (fd) is an open, direct handle to a specific kernel object (a struct file referencing a specific inode).

  1. Open & Check: The program opens the file first to obtain an immutable handle (fd = open("/tmp/user_data.txt", O_RDONLY)), and then checks permissions on that descriptor using fstat(fd, &st).
  2. Race Attempt: The attacker replaces /tmp/user_data.txt with a symlink to /etc/shadow.
  3. Use: The program reads from fd using read(fd, buf, size).

Because read() operates on the file descriptor handle rather than re-resolving the string path, the attacker's symlink swap on the filesystem path has zero effect on the already-opened descriptor. The program acts strictly on the exact inode it checked.

Concrete Examples of TOCTOU

Example 1: Vulnerable POSIX File Handling (Path-Based)

// VULNERABLE: TOCTOU Race Condition
void process_user_file(const char *path) {
    // 1. CHECK: Verify file permissions using the path string
    if (access(path, R_OK) == 0) {

        // --- RACE WINDOW: Attacker replaces 'path' with a symlink to /etc/shadow ---

        // 2. USE: Open the path string
        FILE *f = fopen(path, "r");
        if (f) {
            // Program processes sensitive file contents...
            fclose(f);
        }
    }
}

Fix using File Descriptors (*at syscalls):

// SECURE: Operating on File Descriptors eliminates TOCTOU
void process_user_file_secure(const char *path) {
    // 1. OPEN FIRST: Get a descriptor bound to the target inode
    int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0) return;

    // 2. CHECK THE FD: Validate the inode via fstat() on the descriptor
    struct stat st;
    if (fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && st.st_uid == getuid()) {

        // 3. USE THE FD: Read directly from the validated descriptor
        // Attacker path swaps after open() do not affect 'fd'
        char buffer[1024];
        ssize_t bytes = read(fd, buffer, sizeof(buffer));
        (void)bytes;
    }
    close(fd);
}

Example 2: Multithreaded Memory / Pointer Manipulation (e.g., Double Fetch)

TOCTOU is not limited to filesystems; it also occurs in shared memory, kernel/user-space boundaries, and multithreaded code.

  1. Check: A kernel driver checks a pointer length in user-space memory:
if (user_req->length < MAX_ALLOWED) {
    // --- RACE WINDOW: Another user thread changes user_req->length ---
    memcpy(kernel_buf, user_req->data, user_req->length); // USE (Buffer Overflow)
}

  1. Fix: Read/copy the structure into local memory (or kernel memory) once before performing checks and operations.

Summary of Mitigation Strategies

  • Use File Descriptors & *at Syscalls: Use openat, fstat, fchmod, and fchown instead of path-based equivalents (stat, chmod, chown).
  • Atomic Operations: Create files with atomic flags like O_CREAT | O_EXCL (fails if the path already exists) to avoid check-then-create races.
  • Copy Before Check: For shared memory or user-space pointers, copy the state to thread-local memory first, then validate and use the local copy.