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
| Impact | Details |
|---|---|
| Access Control | Scope: Privilege Escalation TOCTOU in setuid programs can allow attackers to access privileged files or execute code with elevated privileges. |
| Integrity | Scope: Data Manipulation Attackers can modify files or resources after security checks pass. |
| Confidentiality | Scope: 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
-
CVE-2019-5736 — runc container escape via TOCTOU.
-
CVE-2016-9806 — Linux kernel netlink race condition.
-
CVE-2018-6954 — systemd TOCTOU vulnerability.
References
-
MITRE. "CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition." https://cwe.mitre.org/data/definitions/367.html
-
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