Beware of Signals and Race Conditions

 

Beware of Signals and Race Conditions
Chapter 38.6 โ€” TOCTTOU Attacks, Signal Handling, and Stop/Resume Exploits
โšก TOCTTOU
๐Ÿ”” Signal Safety
๐Ÿ Race Conditions

Race Conditions in Privileged Programs

A race condition occurs when the behavior of a program depends on the timing of external events โ€” and an attacker can influence that timing to trigger unexpected behavior. In privileged programs, race conditions are especially dangerous because the window between a check and an action can be exploited to gain unauthorized access.

There are two related attack patterns: TOCTTOU (time-of-check, time-of-use) and signal-based stop/resume attacks. Both exploit the gap between when a program verified something and when it acts on that verification.

โฑ๏ธ TOCTTOU โ€” Time-of-Check, Time-of-Use

The classic pattern: a privileged program checks whether an operation is safe (e.g., verifies a file exists and is accessible), then later performs the operation based on that check. If an attacker can change the state between the check and the use, the verification is worthless.

Time Privileged Program Attacker (different process)
T1 stat(“/tmp/userfile”) โ†’ OK, owned by user 1000 Waiting…
T2 Context switch / signal / pause… rm /tmp/userfile; ln -s /etc/shadow /tmp/userfile
T3 open(“/tmp/userfile”) โ†’ opens /etc/shadow as ROOT! Success โ€” /etc/shadow read by attacker

The access() System Call โ€” A Classic TOCTTOU Trap

access() checks file accessibility using the real UID (not effective UID). Privileged programs often use it to check whether the real user (not root) can access a file before proceeding. But this creates a TOCTTOU window between access() and the subsequent open().

The fix: don’t use access() at all. Instead, temporarily drop privilege to the real UID, then call open() directly. This combines the check and the operation atomically from the kernel’s point of view.

๐Ÿ”” Signal-Based Stop/Resume Attacks

Any user can send SIGTSTP or SIGSTOP to a set-UID program they started. This stops the process mid-execution. The attacker then modifies the environment and sends SIGCONT to resume the program.

The program resumes executing based on assumptions it made before being stopped โ€” which are now false. This is effectively a way to widen the TOCTTOU window to an arbitrarily long duration, giving the attacker plenty of time to manipulate the environment.

Attack Scenario
Step 1: User runs set-UID program. Program checks that symlink /tmp/X points to a safe file.
Step 2: User sends SIGTSTP to stop the program.
Step 3: User changes /tmp/X to point to /etc/passwd.
Step 4: User sends SIGCONT to resume the program.
Step 5: Program continues, now operates on /etc/passwd thinking it’s the safe file. Root-level write to /etc/passwd!

Defenses Against Signal-Based Attacks

Catch or Ignore Stop Signals
Block SIGTSTP in critical sections. Note that SIGSTOP cannot be caught or ignored.
Eliminate TOCTTOU Windows
Use atomic operations: open the file (verify+open in one call), then use fstat() on the FD rather than stat() on the path.
Re-validate After Signal
After handling a signal, re-check all assumptions about the runtime environment before continuing.

๐Ÿ’ป Code Example 1: Atomic File Check with fstat() โ€” Avoiding TOCTTOU
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>

/*
 * WRONG approach: stat() then open() โ€” classic TOCTTOU
 * 
 * Attacker can replace /tmp/userfile with a symlink to /etc/shadow
 * BETWEEN the stat() call and the open() call.
 */
int unsafe_open(const char *path)
{
    struct stat sb;

    /* CHECK: verify file attributes */
    if (stat(path, &sb) == -1) {
        perror("stat");
        return -1;
    }

    if (sb.st_uid != getuid()) {
        fprintf(stderr, "File not owned by caller!\n");
        return -1;
    }

    /* USE: window here โ€” attacker can swap the file! */
    return open(path, O_RDWR);
}

/*
 * CORRECT approach: open() first, then fstat() on the FD
 *
 * fstat() operates on the already-open file descriptor.
 * Even if the pathname is changed after open(), fstat() still
 * checks the file we actually opened โ€” no TOCTTOU window.
 */
int safe_open(const char *path)
{
    int fd;
    struct stat sb;

    /* Step 1: Open the file โ€” creates an FD pinned to this inode */
    fd = open(path, O_RDWR);
    if (fd == -1) {
        perror("open");
        return -1;
    }

    /* Step 2: fstat() on the FD โ€” checks the OPENED file, not the path */
    if (fstat(fd, &sb) == -1) {
        perror("fstat");
        close(fd);
        return -1;
    }

    /* Step 3: Now validate โ€” we know this is the same file we opened */
    if (sb.st_uid != getuid()) {
        fprintf(stderr, "File not owned by caller!\n");
        close(fd);
        return -1;
    }

    /* Also check: it's a regular file (not a symlink, device, etc.) */
    if (!S_ISREG(sb.st_mode)) {
        fprintf(stderr, "Not a regular file!\n");
        close(fd);
        return -1;
    }

    printf("Safely opened: %s (inode %lu)\n", path, (unsigned long)sb.st_ino);
    return fd;
}

int main(void)
{
    int fd;

    printf("UID: %d, EUID: %d\n", (int)getuid(), (int)geteuid());

    fd = safe_open("/tmp/test_tocttou.txt");
    if (fd != -1) {
        printf("File opened safely, fd=%d\n", fd);
        close(fd);
    }

    return 0;
}

๐Ÿ’ป Code Example 2: Blocking SIGTSTP in Critical Sections
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

/*
 * Block SIGTSTP (Ctrl+Z / stop signal) during a critical section
 * where we cannot afford to be stopped mid-operation.
 *
 * SIGSTOP cannot be caught or blocked โ€” only SIGTSTP can be managed.
 */

void critical_section_start(sigset_t *old_mask)
{
    sigset_t block_set;

    sigemptyset(&block_set);
    sigaddset(&block_set, SIGTSTP);  /* Block Ctrl+Z */
    sigaddset(&block_set, SIGINT);   /* Optionally block Ctrl+C too */

    if (sigprocmask(SIG_BLOCK, &block_set, old_mask) == -1) {
        perror("sigprocmask block");
        exit(EXIT_FAILURE);
    }

    printf("[Critical] Signals blocked. Cannot be interrupted.\n");
}

void critical_section_end(const sigset_t *old_mask)
{
    /* Restore the original signal mask */
    if (sigprocmask(SIG_SETMASK, old_mask, NULL) == -1) {
        perror("sigprocmask restore");
        exit(EXIT_FAILURE);
    }

    printf("[Critical] Signals unblocked.\n");
}

int main(void)
{
    sigset_t saved_mask;

    printf("Program started. EUID=%d\n", (int)geteuid());

    /* --- Begin critical section --- */
    critical_section_start(&saved_mask);

    /*
     * In this window:
     *  1. Verify environment assumptions
     *  2. Perform privileged operation atomically
     *
     * User cannot stop the program with Ctrl+Z during this block.
     */
    printf("[Critical] Checking file ownership...\n");
    /* ... stat/open operations here ... */
    printf("[Critical] Performing privileged write...\n");
    /* ... privileged operation here ... */

    critical_section_end(&saved_mask);
    /* --- End critical section --- */

    printf("Back to normal operation. User can now Ctrl+Z.\n");

    /* In a real daemon, you'd sleep or do other non-critical work here */
    sleep(2);

    return 0;
}

๐Ÿ“Œ Key Terms

TOCTTOU Race Condition access() fstat() stat() Symlink Attack SIGTSTP SIGSTOP SIGCONT sigprocmask() Signal Blocking Critical Section
๐ŸŽฏ Interview Questions โ€” Signals and Race Conditions
Q1. What is a TOCTTOU race condition? Give a concrete example.TOCTTOU (Time-of-Check, Time-of-Use) is when a program checks a condition (e.g., stat() to verify file ownership), then later acts on it (open()). Between the check and use, an attacker can change the state (replace the file with a symlink to /etc/shadow). The check result is now invalid but the program proceeds with the operation.

Q2. Why is access() considered dangerous in privileged programs?access() checks accessibility using the real UID, but a separate open() call uses the effective UID. The gap between these two calls is a TOCTTOU window. An attacker can change the file between the access() check and the open() call. The safe alternative is to temporarily drop privilege to real UID and just call open() directly.

Q3. Why is fstat(fd) safer than stat(path) for verifying a file?stat(path) follows the path at call time โ€” if the path is replaced with a symlink after stat() but before open(), you open the wrong file. fstat(fd) operates on an already-open file descriptor pinned to a specific inode. No matter what happens to the path after open(), fstat() checks the file you actually opened.

Q4. How can SIGTSTP be used to exploit a privileged program?A user can send SIGTSTP (Ctrl+Z) to stop a set-UID program mid-execution, modify the runtime environment (change file symlinks, permissions), then send SIGCONT to resume. The program continues based on now-false assumptions about the environment, potentially being tricked into privileged operations on attacker-controlled resources.

Q5. Can SIGSTOP be blocked or caught? What about SIGTSTP?SIGSTOP cannot be caught, blocked, or ignored โ€” it always stops the process. SIGTSTP is the “soft” stop signal (from terminal Ctrl+Z) and CAN be caught, blocked, or ignored using sigprocmask() or sigaction(). Privileged programs can block SIGTSTP during critical operations.

Q6. What is the correct pattern to open a file atomically without TOCTTOU?Open the file first with open(), then use fstat() on the returned file descriptor to verify ownership, type, and other attributes. This is atomic from a security perspective because fstat() checks the file that is actually open, not a potentially-substituted path.

Next: Safe File Operations โ†’
umask, /tmp hazards, O_EXCL, mkstemp()

Continue to Part 7 โ† Part 5

Leave a Reply

Your email address will not be published. Required fields are marked *