0xFFFF

offensive security research

Winning the Race: Signals, Symlinks, and TOC/TOU

2021-06-23 // Part 1

Race conditions are the class of bugs where the outcome depends on the timing of events that the developer assumed would happen in a specific order. Think of two runners sprinting for the same finish line - the program checks who is winning at one point, but by the time it acts on that information, the other runner has already crossed.

This is Part 1 of a series on race condition exploitation. We start with the fundamentals.

TOC/TOU - Time of Check, Time of Use

The most common race condition pattern. A program checks a condition (time of check), then later acts on the assumption that the condition still holds (time of use). If an attacker can change the state between the check and the use, the program's assumption is wrong.

Classic example - a setuid program that checks file ownership before reading:

// Vulnerable: gap between access() and open()
if (access(filename, R_OK) == 0) {
    // attacker swaps filename for symlink here
    int fd = open(filename, O_RDONLY);
    read(fd, buf, sizeof(buf));
}

The access() call checks if the real user can read the file. But between access() and open(), there is a window. During that window, an attacker can replace the file with a symlink to /etc/shadow. The open() call runs with elevated privileges (setuid) and follows the symlink.

Symlink races in /tmp

Temporary file creation is a classic target. If a privileged program creates a file in /tmp with a predictable name, an attacker can pre-create a symlink at that path pointing to a sensitive file.

# Privileged script writes to predictable temp file
echo "$config_data" > /tmp/app_config_backup

# Attack: pre-create symlink before the script runs
ln -sf /etc/crontab /tmp/app_config_backup

When the script writes its config data, it actually overwrites /etc/crontab. If the attacker controls any part of $config_data, they get cron execution.

Mitigations exist: mktemp with random names, O_NOFOLLOW, sticky bit on /tmp. But legacy software and shell scripts routinely get this wrong.

Signal handler races

Signal handlers interrupt normal program flow. If a signal handler modifies shared state that the main program is also using, the result is undefined behavior.

volatile int authenticated = 0;

void handle_alarm(int sig) {
    authenticated = 0;  // timeout - deauthenticate
    longjmp(env, 1);    // jump back to login
}

void process_request() {
    if (authenticated) {
        // Window: SIGALRM fires here, sets authenticated=0
        // but we already passed the check
        perform_privileged_action();
    }
}

The inverse is also exploitable: send a signal at the right moment to skip a security check entirely by forcing a longjmp past it.

Practical exploitation

Race conditions require winning the race - hitting the exact timing window. In practice this means running the attack in a tight loop. For filesystem races, tools like inotifywait can trigger the swap at the right moment. For network-based races, parallel requests (often hundreds or thousands) increase the odds.

Part 2 will cover file descriptor races, double-fetch bugs in kernel space, and practical tooling for reliable exploitation.