Secure Coding in C · intermediate · ~20 min

TOCTOU — time-of-check vs time-of-use races

Recognise and eliminate the check-then-use race pattern.

Overview

Time-Of-Check / Time-Of-Use bugs: a check on a path is racy because the filesystem entry can change between the check and the actual operation. The fix: do the operation, then check the result via the resulting fd.

Why it matters

TOCTOU is a top-5 source of Linux privilege-escalation CVEs. Every audit checklist looks for the stat-then-open pattern.

Core concepts

The pattern. check_path(p) then open(p) — wrong. Attacker swaps p between the two.

The fix. Open first (atomic to the filesystem), then check via the open fd: fstat(fd, &st), not stat(path, &st).

O_NOFOLLOW. Refuse to traverse the LAST component if it's a symlink. Pair with per-component openat for full symlink-aware safety.

openat. Open relative to an open directory fd. Lets you walk a path piece-by-piece without ever letting the resolver follow attacker-controlled links.

Pentester mindset. When auditing setuid programs, look for any stat/access/lstat followed by open. Each pair is a candidate exploit.

Defensive coding habit. Never check a path; always check an fd. Use openat + O_NOFOLLOW when traversing untrusted paths.

Syntax notes

int open(const char *path, int flags);            /* O_NOFOLLOW etc. */
int openat(int dirfd, const char *path, int flags);
int fstat(int fd, struct stat *st);

Lesson

TOCTOU bugs happen when your program checks a file's property (exists, is regular, is owned by me) and THEN opens it. Between the check and the open, an attacker can swap the file (or the directory entry) — a classic race that has powered countless privilege-escalation exploits.

Code examples

/* WRONG */
if (stat(path, &st) == 0 && S_ISREG(st.st_mode)) {
    int fd = open(path, O_RDONLY);   /* attacker swapped path between stat and open */
}

/* RIGHT */
int fd = open(path, O_RDONLY | O_NOFOLLOW);
fstat(fd, &st);                       /* atomic — fd refers to whatever we actually opened */
if (!S_ISREG(st.st_mode)) { close(fd); return -1; }

Line by line

int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return -1;
struct stat st;
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode) || st.st_uid != getuid()) {
    close(fd); return -1;
}
/* fd is now safe to read from */

Common mistakes

  • Calling stat then open. The window between is the race.

Debugging tips

Reproduce a TOCTOU under strace: insert a usleep between check and use, then race a mv from another shell.

Memory safety

Pair with mkstemp for safe temp-file creation; never tmpnam.

Real-world uses

Every setuid binary on the system audits this pattern. sudo, su, passwd, ping all use openat + O_NOFOLLOW.

Practice tasks

  1. Identify the race in a 10-line stat-then-open snippet. 2. Rewrite using open-then-fstat. 3. Use openat to safely walk a/b/c.

Summary

Race lives between check and use. Fix by checking the fd, not the path. Use O_NOFOLLOW and openat for symlink-safe traversal.

Practice with these exercises