Secure Coding in C · intermediate · ~20 min
Recognise and eliminate the check-then-use race pattern.
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.
TOCTOU is a top-5 source of Linux privilege-escalation CVEs. Every audit checklist looks for the stat-then-open pattern.
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.
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);
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.
/* 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; }
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 */
Reproduce a TOCTOU under strace: insert a usleep between check and use, then race a mv from another shell.
Pair with mkstemp for safe temp-file creation; never tmpnam.
Every setuid binary on the system audits this pattern. sudo, su, passwd, ping all use openat + O_NOFOLLOW.
Race lives between check and use. Fix by checking the fd, not the path. Use O_NOFOLLOW and openat for symlink-safe traversal.