Race Condition Enabling Link Following

Description

Race Condition Enabling Link Following is a Time-of-Check Time-of-Use (TOCTOU) vulnerability that occurs when a program checks a file's properties (such as whether it's a symbolic link) and later performs an operation on the same file, but an attacker can change the file to a symbolic link between the check and the use. This creates a window of opportunity where the program believes it is operating on a safe, regular file, but actually operates on a different file through a malicious symlink. The vulnerability is particularly dangerous when the program runs with elevated privileges (setuid/setgid), as attackers can redirect file operations to sensitive system files like /etc/passwd, /etc/shadow, or configuration files. The fundamental problem is that the filesystem state checked during the verification phase may not be the same as the state when the actual operation occurs, and this race condition can be reliably exploited through techniques like spinning up multiple threads to repeatedly swap files.

Risk

TOCTOU symlink race conditions enable local privilege escalation, arbitrary file overwrite, and unauthorized file access. These vulnerabilities are especially critical in setuid binaries, container runtimes, and system services that handle files on behalf of users. Docker suffered multiple critical TOCTOU vulnerabilities: CVE-2018-15664 allowed container escape through symlink racing during docker cp operations, and CVE-2024-23651 in BuildKit enabled attackers to delete or overwrite arbitrary host files during image builds. The Tesla Model 3 was compromised at Pwn2Own 2023 through a TOCTOU race condition (CVE-2023-3282) in ConnMan, allowing root-level code execution. Windows Defender was vulnerable to CVE-2019-1161, a TOCTOU race that allowed attackers to delete arbitrary files as SYSTEM. The exploitation technique is well-understood and reliable: attackers use high-frequency symlink switching combined with timing manipulation to win the race consistently. Even with small time windows, modern multi-core systems make exploitation practical through parallel attempts.

Solution

Use atomic filesystem operations that cannot be interrupted by symlink changes. Open files with the O_NOFOLLOW flag to fail if the target is a symbolic link rather than following it. Use file descriptor-based operations (fstat(), fchown(), fchmod()) after opening the file rather than path-based operations (stat(), chown(), chmod()) that can be raced. For setuid programs, temporarily drop privileges using seteuid(getuid()) before performing file operations in user-controlled directories, then restore privileges only when needed. When creating files in directories that may be writable by attackers (like /tmp), use mkstemp() or similar functions that atomically create and open files. Implement proper directory isolation: create files in directories that only the privileged process can write to. For container runtimes, use mount namespaces and prevent symlink resolution across mount boundaries. Consider using Linux capabilities (CAP_FOWNER, etc.) instead of full setuid privileges. Implement defense in depth by validating file paths multiple times and using chroot or pivot_root to restrict filesystem access.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Modify Files or Directories - Attackers can redirect write operations to overwrite arbitrary files by replacing the target with a symlink to sensitive system files like /etc/passwd or application configuration files.
ConfidentialityScope: Confidentiality

Read Files or Directories - Read operations can be redirected through symlinks to access files the attacker normally cannot read, exfiltrating sensitive data such as private keys, password hashes, or confidential configurations.
Access ControlScope: Access Control

Gain Privileges or Assume Identity - When exploited in setuid/setgid programs or privileged services, attackers can escalate to root or system-level access by manipulating security-critical files.

Example Code

Vulnerable Code

// VULNERABLE: Classic TOCTOU race condition in setuid program
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

void vulnerable_file_operation(const char *filename) {
    struct stat st;

    // VULNERABLE: Time-of-Check - verify file is not a symlink
    if (lstat(filename, &st) < 0) {
        perror("lstat");
        return;
    }

    // VULNERABLE: Check if it's a symlink
    if (S_ISLNK(st.st_mode)) {
        fprintf(stderr, "Error: %s is a symbolic link\n", filename);
        return;
    }

    // VULNERABLE: Check ownership
    if (st.st_uid != getuid()) {
        fprintf(stderr, "Error: You don't own %s\n", filename);
        return;
    }

    // RACE WINDOW: Attacker replaces file with symlink here!
    // Between lstat() and open(), attacker can:
    //   rm /tmp/userfile
    //   ln -s /etc/passwd /tmp/userfile

    // VULNERABLE: Time-of-Use - open and write to file
    // Now operating on /etc/passwd with root privileges!
    FILE *fp = fopen(filename, "w");  // Opens symlink target
    if (fp) {
        fprintf(fp, "User data written by privileged program\n");
        fclose(fp);
    }
}

// VULNERABLE: Setuid root program
int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        return 1;
    }

    // Running as root (setuid), but trusting user-provided path
    vulnerable_file_operation(argv[1]);
    return 0;
}
# VULNERABLE: Python TOCTOU race in privileged script
import os
import stat

def vulnerable_delete_user_file(filepath):
    # VULNERABLE: Check if file is a symlink
    if os.path.islink(filepath):
        raise ValueError("Symlinks not allowed")

    # VULNERABLE: Check if file is owned by requesting user
    file_stat = os.lstat(filepath)
    if file_stat.st_uid != os.getuid():
        raise PermissionError("You don't own this file")

    # RACE WINDOW: Attacker can replace file with symlink here
    # Script running as root will delete the symlink target

    # VULNERABLE: Time-of-Use with root privileges
    os.remove(filepath)  # May delete /etc/passwd if raced

def vulnerable_temp_file_creation():
    # VULNERABLE: Predictable temp file path
    temp_path = f"/tmp/app_temp_{os.getpid()}"

    # VULNERABLE: Check existence then create
    if os.path.exists(temp_path):
        os.remove(temp_path)

    # RACE WINDOW: Attacker creates symlink at temp_path

    # VULNERABLE: Creates/overwrites through symlink
    with open(temp_path, 'w') as f:
        f.write("sensitive data")

Fixed Code

// FIXED: Safe file operations using file descriptors and O_NOFOLLOW
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

int safe_file_operation(const char *filename) {
    struct stat st;
    int fd;
    uid_t real_uid = getuid();
    uid_t effective_uid = geteuid();

    // FIXED: Drop privileges before accessing user files
    if (seteuid(real_uid) < 0) {
        perror("seteuid");
        return -1;
    }

    // FIXED: Open with O_NOFOLLOW - fails if symlink
    // This is atomic - no race window
    fd = open(filename, O_WRONLY | O_NOFOLLOW);
    if (fd < 0) {
        if (errno == ELOOP) {
            fprintf(stderr, "Error: %s is a symbolic link\n", filename);
        } else {
            perror("open");
        }
        seteuid(effective_uid);  // Restore for cleanup
        return -1;
    }

    // FIXED: Use file descriptor operations, not path operations
    // These operate on the opened file, immune to symlink replacement
    if (fstat(fd, &st) < 0) {
        perror("fstat");
        close(fd);
        seteuid(effective_uid);
        return -1;
    }

    // FIXED: Verify ownership using file descriptor stat
    if (st.st_uid != real_uid) {
        fprintf(stderr, "Error: You don't own this file\n");
        close(fd);
        seteuid(effective_uid);
        return -1;
    }

    // FIXED: Write using file descriptor, not path
    const char *data = "User data written safely\n";
    if (write(fd, data, strlen(data)) < 0) {
        perror("write");
        close(fd);
        seteuid(effective_uid);
        return -1;
    }

    close(fd);

    // Restore privileges only when actually needed
    if (seteuid(effective_uid) < 0) {
        perror("seteuid restore");
        return -1;
    }

    return 0;
}

// FIXED: Safe temporary file creation
int safe_temp_file(char **out_path) {
    // FIXED: Use mkstemp for atomic create+open
    char template[] = "/tmp/app_XXXXXX";
    int fd = mkstemp(template);

    if (fd < 0) {
        perror("mkstemp");
        return -1;
    }

    // FIXED: Set restrictive permissions immediately
    if (fchmod(fd, 0600) < 0) {
        perror("fchmod");
        close(fd);
        unlink(template);
        return -1;
    }

    *out_path = strdup(template);
    return fd;
}
# FIXED: Python safe file operations
import os
import stat
import tempfile

def safe_delete_user_file(filepath, requesting_uid):
    """Safely delete a file, immune to TOCTOU symlink attacks."""

    # FIXED: Open with O_NOFOLLOW equivalent - don't follow symlinks
    try:
        # Use os.open with O_NOFOLLOW to detect symlinks atomically
        fd = os.open(filepath, os.O_RDONLY | os.O_NOFOLLOW)
    except OSError as e:
        if e.errno == errno.ELOOP:
            raise ValueError("Symlinks not allowed")
        raise

    try:
        # FIXED: Use fstat on file descriptor - immune to race
        file_stat = os.fstat(fd)

        # FIXED: Check ownership using fstat result
        if file_stat.st_uid != requesting_uid:
            raise PermissionError("You don't own this file")

        # FIXED: Get directory fd for unlinkat
        dir_path = os.path.dirname(os.path.abspath(filepath))
        filename = os.path.basename(filepath)
        dir_fd = os.open(dir_path, os.O_RDONLY | os.O_DIRECTORY)

        try:
            # FIXED: Use unlinkat with dir_fd for atomic operation
            os.unlink(filename, dir_fd=dir_fd)
        finally:
            os.close(dir_fd)
    finally:
        os.close(fd)

def safe_temp_file_creation():
    """Create temp file atomically, immune to symlink attacks."""

    # FIXED: Use tempfile module which handles atomicity
    fd, path = tempfile.mkstemp(prefix='app_', suffix='.tmp')

    try:
        # FIXED: Set permissions using file descriptor
        os.fchmod(fd, 0o600)

        # FIXED: Write using file descriptor
        os.write(fd, b"sensitive data")

        return path
    except:
        os.close(fd)
        os.unlink(path)
        raise
    finally:
        os.close(fd)

The vulnerable code demonstrates classic TOCTOU patterns where file properties are checked using path-based operations, creating a race window before the actual file operation. The fixed code eliminates the race by using O_NOFOLLOW to atomically reject symlinks, file descriptor-based operations that operate on the already-opened file, privilege dropping before accessing user-controlled paths, and atomic file creation with mkstemp().


Exploited in the Wild

Docker BuildKit TOCTOU Vulnerability (Docker/Global, 2024)

CVE-2024-23651 affected Docker BuildKit versions before 0.12.5, allowing attackers to exploit a TOCTOU race condition during cache mount operations. By rapidly modifying symlinks in a cache mount, attackers could trick BuildKit into resolving a symlink to an arbitrary host path, enabling them to read, write, or delete files outside the container during image builds. The vulnerability could be triggered by building a malicious Dockerfile or using compromised build contexts. This attack demonstrated how container isolation can be completely bypassed through filesystem race conditions, potentially compromising entire CI/CD pipelines and build infrastructure.

Tesla Model 3 ConnMan TOCTOU Exploit (Tesla/Pwn2Own 2023)

At Pwn2Own Automotive 2023, security researchers from Synacktiv exploited a TOCTOU race condition (CVE-2023-3282) in ConnMan, the network connection manager used in Tesla vehicles. The vulnerability existed in how ConnMan handled DHCP options and temporary file creation. By winning a race condition during network configuration, attackers could write arbitrary content to arbitrary files with root privileges. Combined with other vulnerabilities, this led to complete compromise of the Tesla infotainment system with root-level code execution. The attack required only that the vehicle connect to a malicious WiFi network, demonstrating real-world exploitability of symlink race conditions.


Tools to test/exploit

  • inotify-tools — Linux utilities for monitoring filesystem events, useful for understanding race timing and creating exploit conditions.

  • racing-pulse — Google Project Zero's research tool for exploiting filesystem race conditions, particularly useful for testing TOCTOU vulnerabilities.

  • symlink-racer — JFrog's tool for testing symlink TOCTOU vulnerabilities through rapid symlink switching.


CVE Examples

  • CVE-2024-23651 — Docker BuildKit TOCTOU race condition allowing container escape through symlink manipulation during cache mounts.

  • CVE-2023-3282 — ConnMan TOCTOU vulnerability exploited at Pwn2Own to gain root on Tesla Model 3.

  • CVE-2019-1161 — Windows Defender TOCTOU race allowing arbitrary file deletion as SYSTEM.

  • CVE-2018-15664 — Docker cp TOCTOU vulnerability enabling container escape through symlink racing.

  • CVE-2008-0525 — PatchLink Update client TOCTOU race condition allowing local privilege escalation.


References

  1. MITRE Corporation. "CWE-363: Race Condition Enabling Link Following." https://cwe.mitre.org/data/definitions/363.html

  2. CERT/CC. "TOC TOU - Time of Check, Time of Use Race Conditions." https://wiki.sei.cmu.edu/confluence/display/c/FIO45-C.+Avoid+TOCTOU+race+conditions+while+accessing+files

  3. Docker. "Docker Security Advisory: Multiple Vulnerabilities in BuildKit." February 2024. https://www.docker.com/blog/docker-security-advisory-multiple-vulnerabilities-in-runc-buildkit-and-moby/

  4. Wei, J. and Pu, C. "TOCTTOU Vulnerabilities in UNIX-Style File Systems: An Anatomical Study." USENIX Security 2005. https://www.usenix.org/legacy/event/fast05/tech/full_papers/wei/wei.pdf