Race Conditions
Time-of-Check to Time-of-Use (TOCTOU)
Read aloud is unavailable in this browser.
The Window of Vulnerability
Among race conditions, one class stands out for its security implications and practical prevalence: Time-of-Check to Time-of-Use (TOCTOU) vulnerabilities. These races occur when a program checks a condition and later acts on that condition, but the checked state can change between the check and the use.
TOCTOU vulnerabilities are not merely theoretical concerns. They have been exploited to bypass security controls, escalate privileges, corrupt file systems, and compromise systems from the smallest embedded devices to the largest data centers. Understanding TOCTOU is essential for any engineer building secure software, especially systems that interact with file systems, network resources, or shared state.
This page provides a rigorous, comprehensive treatment of TOCTOU vulnerabilities: their structure, manifestations, exploitation techniques, and prevention strategies.
By the end of this page, you will: (1) Define TOCTOU precisely and distinguish it from other race types, (2) Understand the structural pattern that creates TOCTOU vulnerabilities, (3) Analyze classic TOCTOU attack scenarios, (4) Recognize TOCTOU vulnerabilities in code, and (5) Apply defensive techniques to eliminate or mitigate TOCTOU risks.
Formal Definition of TOCTOU
A Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists when:
- A program checks a property of a resource (permissions, existence, ownership, value)
- Time passes (potentially mere microseconds)
- The program uses the resource, assuming the checked property still holds
- An attacker or concurrent process modifies the resource between check and use
- The modification causes the use operation to behave differently than intended
The fundamental problem is that the check and use are not atomic—they are separate operations that can be interleaved with other operations.
The TOCTOU Timeline
| Time | Victim Program | Attacker / Concurrent Process |
|---|---|---|
| T1 | Check resource state (e.g., file permissions) | — |
| T2 | Check passes, proceed to use | — |
| T3 | — | Modify resource (swap file, change permissions) |
| T4 | Use resource (open, read, execute) | — |
| T5 | Unexpected outcome: Wrong file accessed, privilege escalation, etc. | — |
The window between T2 and T4 is the vulnerability window. The longer this window and the more predictable the timing, the easier the attack.
Why TOCTOU Is Especially Dangerous
TOCTOU vulnerabilities are particularly concerning because:
- •They often involve privileged operations. TOCTOU is most damaging when a privileged program makes decisions based on checks that unprivileged attackers can invalidate.
- •File system operations are inherently vulnerable. Most file APIs separate existence checks, permission checks, and actual operations into distinct calls.
- •They're easy to introduce, hard to spot. The check-then-use pattern is natural to programmers and passes code review without raising suspicion.
- •Exploitation can be deterministic. Unlike many race conditions, TOCTOU attacks can often be reliably exploited with careful timing or by widening the vulnerability window.
TOCTOU is formally catalogued as CWE-367 in the Common Weakness Enumeration. It has been the root cause of numerous CVEs (Common Vulnerabilities and Exposures) across operating systems, web servers, and applications. It's listed among the CWE Top 25 Most Dangerous Software Weaknesses.
Classic File System TOCTOU
The most common and historically significant TOCTOU vulnerabilities involve file system operations. Let's examine the classic attack pattern in detail.
The Vulnerable Pattern
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
/* * VULNERABLE CODE: Classic TOCTOU vulnerability * A setuid-root program that writes to a user-specified file */ #include <stdio.h>#include <unistd.h>#include <sys/stat.h>#include <fcntl.h> int write_log(const char *filename) { struct stat st; // TIME OF CHECK: Verify the file is owned by the calling user // and is a regular file (not a symlink to /etc/passwd) if (lstat(filename, &st) != 0) { return -1; } if (st.st_uid != getuid()) { fprintf(stderr, "Error: You don't own this file"); return -1; } if (!S_ISREG(st.st_mode)) { fprintf(stderr, "Error: Not a regular file"); return -1; } /* VULNERABILITY WINDOW BEGINS HERE */ /* Attacker can replace the file with a symlink during this gap! */ // TIME OF USE: Open and write to the file int fd = open(filename, O_WRONLY | O_APPEND); if (fd == -1) { return -1; } write(fd, "Log entry", 10); close(fd); return 0;}The Attack
An attacker runs this simultaneously with the vulnerable program:
#!/bin/bash
# Attacker script - runs while vulnerable program is executing
# Create a file the attacker owns
touch /tmp/mylogfile
# Start a loop that rapidly swaps the file with a symlink
while true; do
# Replace the file with a symlink to /etc/passwd
rm /tmp/mylogfile
ln -s /etc/passwd /tmp/mylogfile
# Change back to normal file
rm /tmp/mylogfile
touch /tmp/mylogfile
done &
# Invoke the vulnerable setuid program repeatedly
while true; do
/path/to/vulnerable_program /tmp/mylogfile
done
The attack works as follows:
- Attacker creates
/tmp/mylogfile(a regular file they own) - Victim program runs
lstat()- sees a regular file owned by attacker - Attacker quickly replaces the file with
ln -s /etc/passwd /tmp/mylogfile - Victim program runs
open()- follows symlink, opens/etc/passwd - Victim program writes to
/etc/passwdwith root privileges - System compromised
The symlink race can be won surprisingly often—on a typical system, with enough iterations, the attacker wins within seconds.
This exact vulnerability pattern has been found in real setuid programs, mail systems, temporary file handlers, and web servers. The classic /tmp symlink attacks of the 1990s and 2000s exploited exactly this structure. Many of these led to immediate root compromise of Unix systems.
The Symlink Race In Depth
The symlink race is the quintessential TOCTOU attack, warranting deeper examination of its mechanics and variations.
Why Symlinks Are Perfect for TOCTOU Attacks
Symbolic links (symlinks) are ideal for TOCTOU exploitation because:
- •No special privileges required. Any user can create symlinks in directories they can write to.
- •Instant redirection. A symlink changes the target of a pathname completely—a file suddenly 'becomes' any other file on the system.
- •Invisible to naive checks.
stat()follows symlinks, making checks oblivious to the redirection. Evenlstat()can be defeated with careful timing. - •Lightweight operation. Creating and deleting symlinks is fast, enabling rapid swapping in the vulnerability window.
- •Any writable directory.
/tmp, home directories, and web upload directories are natural attack venues.
Widening the Vulnerability Window
Attackers employ various techniques to increase the probability of winning the race:
CPU Contention: Spawn many CPU-intensive processes to slow down the victim program's execution between check and use.
Priority Manipulation: Use nice or real-time scheduling to give the attacker's swap process higher priority.
SIGSTOP Attack: If the attacker can signal the victim process (same user), they can pause it during the vulnerability window:
kill(victim_pid, SIGSTOP); // Pause victim mid-execution
rm_symlink_and_replace(); // Swap the file
kill(victim_pid, SIGCONT); // Resume victim
File System Latency: Exploit network file systems (NFS, SMB) where operations are slower, creating longer windows.
Measuring the Window
The vulnerability window duration depends on what happens between check and use. Adding logging, I/O operations, or complex logic between check and use widens the window significantly.
123456789101112
// EXTREMELY VULNERABLE: Widened window if (access(filename, W_OK) == 0) { // Check // These operations widen the vulnerability window: log_access_attempt(filename); // Database write notify_audit_system(filename); // Network call check_quota_limits(user); // Complex computation fd = open(filename, O_WRONLY); // Use (much later) ...}Understanding how attackers widen windows helps defenders shrink them. Minimize operations between check and use. Better yet, eliminate the check-then-use pattern entirely by using operations that are atomic or that derive the checked property from the same operation as the use.
TOCTOU Beyond File Systems
While file system TOCTOU is most common, the pattern appears in many other contexts. Recognizing these variations is essential for comprehensive security.
Database TOCTOU
1234567891011121314151617181920212223
# VULNERABLE: Database TOCTOU in a ticket booking system def book_ticket(user_id, event_id): # TIME OF CHECK: Query available seats result = db.query( "SELECT available_seats FROM events WHERE id = ?", event_id ) if result.available_seats > 0: # VULNERABILITY WINDOW: Another request could book the last seat # TIME OF USE: Perform the booking db.execute( "UPDATE events SET available_seats = available_seats - 1 " "WHERE id = ?", event_id ) db.execute( "INSERT INTO bookings (user_id, event_id) VALUES (?, ?)", user_id, event_id ) # Result: Possible overbooking if concurrent requestsNetwork/API TOCTOU
1234567891011121314
# VULNERABLE: API TOCTOU in authorization def transfer_funds(auth_token, from_account, to_account, amount): # TIME OF CHECK: Verify user can access from_account user = validate_token(auth_token) if not can_access_account(user, from_account): raise PermissionError("Access denied") # VULNERABILITY WINDOW # Admin could revoke access between check and use # Account ownership could change # TIME OF USE: Perform the transfer execute_transfer(from_account, to_account, amount)Kernel Double-Fetch TOCTOU
A particularly dangerous variant occurs in operating system kernels when copying data from user space:
1234567891011121314151617181920212223
// VULNERABLE: Kernel double-fetch int syscall_handler(struct user_request __user *req) { size_t size; // First fetch: Check size for sanity if (copy_from_user(&size, &req->size, sizeof(size))) return -EFAULT; if (size > MAX_SIZE) return -EINVAL; // Reject oversized requests // VULNERABILITY WINDOW: User-space can change req->size // Second fetch: Read the actual data // Uses req->size again - assumes it's still valid! char *buffer = kmalloc(size, GFP_KERNEL); if (copy_from_user(buffer, req->data, req->size)) // BUG! ... // If attacker changed req->size between checks, // we may read more than allocated: kernel heap overflow}Double-fetch vulnerabilities in kernels typically lead to arbitrary kernel code execution and complete system compromise. They've been found in Linux, Windows, and macOS kernels. The attacker uses a second thread to modify the user-space memory while the kernel is between fetches.
Prevention Techniques
Eliminating TOCTOU vulnerabilities requires understanding the fundamental problem: non-atomic check-then-use. Prevention strategies fall into several categories.
Strategy 1: Use Atomic Operations
The ideal fix is to use operations that check and use atomically:
123456789101112
-- SECURE: Atomic database operation -- Instead of: SELECT, then UPDATE-- Use: UPDATE with condition UPDATE events SET available_seats = available_seats - 1 WHERE id = :event_id AND available_seats > 0; -- Check rows affected: if 0, no seat was available-- The check and update are atomic within one statementStrategy 2: Open First, Check File Descriptor
For file operations, open the file first, then check properties of the open file descriptor:
12345678910111213141516171819202122232425262728
// SECURE: Check after open using file descriptor int secure_write_log(const char *filename) { // Open with restrictive flags int fd = open(filename, O_WRONLY | O_APPEND | O_NOFOLLOW); if (fd == -1) { return -1; // O_NOFOLLOW rejects symlinks } // Now check properties of the OPEN file, not the path struct stat st; if (fstat(fd, &st) != 0) { close(fd); return -1; } // Check ownership and type of the actual opened file if (st.st_uid != getuid() || !S_ISREG(st.st_mode)) { close(fd); return -1; } // Safe: we know exactly what file we have open write(fd, "Log entry", 10); close(fd); return 0;}Strategy 3: Safe Directory Operations
12345678910111213141516171819202122
// SECURE: Use directory file descriptors and *at() functions int secure_operations(const char *directory, const char *filename) { // Open the directory - establishes a fixed reference point int dirfd = open(directory, O_RDONLY | O_DIRECTORY); if (dirfd == -1) return -1; // Use openat() - operates relative to dirfd // Even if an attacker manipulates the path, dirfd is fixed int fd = openat(dirfd, filename, O_WRONLY | O_CREAT | O_NOFOLLOW, 0600); // Use fchown(), fchmod() on the fd - not path-based versions fchown(fd, getuid(), getgid()); fchmod(fd, 0600); // Operations happen on the fd we opened, not resolvable paths write(fd, "data", 4); close(fd); close(dirfd); return 0;}- •Use O_NOFOLLOW — Causes open() to fail on symlinks rather than following them.
- •Avoid world-writable directories — Create files in directories only the application can write to.
- •Use mkstemp() — Creates temporary files atomically with unpredictable names, avoiding race conditions.
- •Enable sticky bit — On /tmp, prevents users from deleting others' files.
- •Use safe path resolution — Linux's openat2() with RESOLVE_NO_SYMLINKS, RESOLVE_BENEATH flags.
- •Single-copy for kernel — In kernel code, copy user data once to kernel memory, then operate on the kernel copy only.
Once you have a file descriptor (or any handle to a resource), you've established a fixed binding. Subsequent operations on the fd are not subject to path manipulation. This is why 'open first, then check the fd' is the fundamental mitigation pattern for file system TOCTOU.
Modern Operating System Defenses
Operating system developers have implemented various mitigations to reduce TOCTOU attack surfaces, even when applications have vulnerabilities.
Protected Symlinks
Linux introduced protected symlinks (kernel.protected_symlinks sysctl):
- Symlinks in world-writable sticky directories (like /tmp) can only be followed if:
- The symlink owner matches the follower, OR
- The directory owner matches the symlink owner
This prevents the classic /tmp symlink attack even against vulnerable programs.
- •protected_symlinks — Restricts following symlinks in sticky directories (enabled by default in most modern distributions)
- •protected_hardlinks — Prevents creating hardlinks to files you don't own, blocking another attack variant
- •protected_fifos — Similar protection for FIFO/named pipe races
- •protected_regular — Prevents opening regular files not owned by you in world-writable sticky directories
The openat2() System Call
Linux 5.6 introduced openat2() with powerful flags for safe path resolution:
12345678910111213
#include <linux/openat2.h> int secure_open_modern(int dirfd, const char *path) { struct open_how how = { .flags = O_RDONLY, .resolve = RESOLVE_NO_SYMLINKS // Reject any symlinks in path | RESOLVE_BENEATH // Don't escape starting directory | RESOLVE_NO_MAGICLINKS // Reject /proc/self/fd/* etc. | RESOLVE_NO_XDEV, // Don't cross mount points }; return syscall(SYS_openat2, dirfd, path, &how, sizeof(how));}Mandatory Access Control
Systems like SELinux and AppArmor can confine TOCTOU damage:
- Even if a symlink attack succeeds, MAC policies can deny access to the true target
- Process confinement prevents setuid programs from affecting files outside their domain
- Audit logging captures attempted policy violations
These defenses provide defense in depth—even if the application has a TOCTOU vulnerability, system-level protections limit exploitability.
OS-level protections are mitigations, not complete solutions. Applications should still be designed to avoid TOCTOU patterns. Kernel protections may not exist on all systems, may be disabled, or may not cover all attack variants. Relying on application-level correctness is essential.
Identifying TOCTOU in Code Review
Recognizing TOCTOU vulnerabilities during code review is a critical skill. Here are patterns and red flags to watch for.
Dangerous API Combinations
| Check API | Use API | Safe Alternative |
|---|---|---|
access(path, W_OK) | open(path, O_WRONLY) | Open, then fstat() the fd |
stat(path, &st) | open(path, ...) | Open first with O_NOFOLLOW, fstat() fd |
lstat(path, &st) | open(path, ...) | Use openat() with RESOLVE flags |
realpath(path, buf) | open(buf, ...) | Avoid realpath; use fd-based operations |
readdir(dir) | unlink(entry->name) | Use unlinkat() with directory fd |
| SELECT ... WHERE | UPDATE / INSERT | Single atomic UPDATE with WHERE clause |
authorization_check() | perform_action() | Re-check at action time or use capabilities |
Code Review Red Flags
- •Path strings used multiple times — If a file path variable is used for checking and then used again for opening, suspect TOCTOU.
- •Check-then-use control flow —
if (can_access(...)) { actually_access(...); }patterns across separate operations. - •Database SELECT before UPDATE — Reading a value to check it, then writing based on the check.
- •Privileged programs handling user paths — Setuid programs, web servers, database engines that operate on user-supplied paths.
- •Operations in /tmp or shared directories — Any code operating in world-writable directories is elevated risk.
- •Kernel code with multiple copy_from_user() — Each copy is a potential double-fetch vulnerability.
Questions to Ask During Review
- Is there a timing gap between when properties are checked and when they are relied upon?
- Can an attacker modify the checked resource during this gap?
- Would such modification cause different behavior than intended?
- Is the checked property bound to the resource identity, or just a path/name?
If answers are Yes, Yes, Yes, No (or uncertain)—you likely have a TOCTOU vulnerability.
Static analysis tools can detect some TOCTOU patterns. Coverity, CodeQL, and specialized tools like MOPS identify dangerous API sequences. However, semantic TOCTOU (like business logic races) often requires manual review. Combine automated scanning with human expertise.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.