Secure Coding in C · intermediate · ~20 min

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

- Recognize the classic *check-a-path-then-use-a-path* pattern that opens a TOCTOU race. - Explain **why** a filename is not a stable identity while a file descriptor is. - Rewrite vulnerable `stat`-then-`open` code as `open`-then-`fstat` so every check runs against the object you actually hold. - Use `O_NOFOLLOW`, `O_EXCL`, `O_CLOEXEC`, and `mkstemp` to close create-time and symlink windows. - Traverse an untrusted path safely one component at a time with `openat`. - Audit setuid-style code the way a reviewer does: find each check/use pair and judge whether it can be swapped underneath.

Overview

A TOCTOU bug — short for Time-Of-Check to Time-Of-Use — is a race condition in the way a program touches files. The name spells out the flaw exactly: there is a moment when you check something, a later moment when you use it, and a gap in between that an attacker can exploit.

Here is the whole idea in plain language. Your program wants to be careful, so it first asks a question about a filename: does this file exist? is it a regular file? do I own it? Getting a satisfactory answer, it then acts on that same filename: it opens it, reads it, writes it, or deletes it. The trouble is that a filename is just a label, not the file itself. Between your question and your action, someone else can quietly repoint that label at a completely different file — often one they control, or one they should never be allowed to touch. Your careful check said "yes, this is safe," but the thing you finally used is not the thing you checked.

This lesson builds directly on two ideas you already met. In fopen and the stdio model you learned that opening a file hands you back a handle to a specific open file — the operating system, not the path string, now owns the connection. That handle is the key to the fix here: once you hold it, the object underneath cannot be swapped. In errno and error reporting you learned that every system call can fail and tells you why through errno; TOCTOU defenses lean hard on that, because the whole strategy is "attempt the operation and inspect the failure" rather than "ask permission first."

In terminology: a TOCTOU bug is a race condition between a check operation (stat, lstat, access) that takes a path and a use operation (open, unlink, chmod) that takes the same path. The defense is to bind check and use to one stable object — a file descriptor — so no swap is possible.

Why it matters

TOCTOU is one of the most persistent sources of Linux privilege-escalation CVEs, and it has its own catalog entry: CWE-367, Time-of-check Time-of-use Race Condition. It shows up again and again because the vulnerable code looks responsible — it is doing an extra safety check — while actually being unsafe.

The danger multiplies in privileged programs. A setuid-root utility runs with the operating system's full authority but is launched by an ordinary user who controls the filesystem around it. If that program checks a path the user can influence, the user can win the race and redirect a privileged read, write, or delete onto /etc/passwd, a root-owned config file, or a device node. That is how a harmless-looking log rotator or temp-file helper becomes a root exploit.

Because the pattern is so well known, every security audit checklist looks for it. Reviewers scan for a stat, access, or lstat on a path followed later by an open, unlink, or chmod on the same path. If you write or review C for anything that runs with elevated privilege or handles shared directories like /tmp, spotting and closing these windows is expected of you, not optional.

Core concepts

1. A path is a label, not an identity

Definition. A path (like /tmp/report.txt) is a string the kernel resolves — every time you use it — into an actual filesystem object. Two calls with the same path can resolve to two different objects if the directory entry changed in between.

Plain-language explanation. Think of a path as a name on a mailbox and the file as whatever is inside. You can read the label, walk away, and by the time you come back someone may have swapped the box's contents. The label still reads the same; the contents do not.

How it works internally. Path resolution walks the directory tree component by component, following symlinks, until it lands on an inode (the kernel's real handle to a file's data). Nothing pins that result in place. The next syscall re-walks from scratch.

You call stat("/tmp/f")            You call open("/tmp/f")
        |                                  |
   resolves to  --> inode 100        resolves to --> inode 777  (attacker swapped it!)
   [a safe file you own]             [/etc/shadow via a symlink]
        |                                  |
     time ----------- GAP (attacker acts here) ------------> time

When it matters / when it does not. It matters whenever the directory is writable by anyone you do not fully trust (world-writable /tmp, a user's home directory reached by a privileged process, a shared upload folder). In a directory only you can modify, no one can swap the entry, so the race cannot be triggered — but relying on that is fragile; permissions drift.

Common pitfall. Believing that because the two calls are "right next to each other" the window is too small to exploit. Attackers widen windows with filesystem tricks and simply retry thousands of times; a nanosecond gap hit reliably is still a gap.

Knowledge check. In your own words, why can stat(path) and open(path) end up referring to different files even though the string path never changed?

2. The vulnerable pattern: check the path, then use the path

Definition. The bug is any check syscall on a path followed by a use syscall on the same path, with the safety decision based on the check.

if (access(path, W_OK) == 0) {   /* CHECK: may I write here? */
    int fd = open(path, O_WRONLY); /* USE: attacker swapped path -> now writing elsewhere */
}

How it works internally. access even checks with the real user ID, which is exactly why people reach for it in setuid programs — and exactly why it is dangerous: it invites a check/use split. The check answers a question about the object visible now; the open acts on the object visible later.

When NOT to do this. Never gate a file operation on a prior path-based check in code that touches untrusted directories. There is essentially no safe use of access() followed by open() on the same path.

Common pitfall. Using access() at all for security decisions. Its own man page warns against it precisely because of this race.

Approach Checks Uses Swappable in between?
access(p) then open(p) the path the path Yes — vulnerable
stat(p) then open(p) the path the path Yes — vulnerable
open(p) then fstat(fd) the descriptor the descriptor No — safe

3. The fix: use first, then check the result

Definition. Perform the operation to obtain a file descriptor, then verify properties through that descriptor with fstat(fd, ...).

Plain-language explanation. Instead of asking "is the thing behind this label safe?" and then grabbing it, you grab it first and then inspect what you are actually holding. Once open returns a descriptor, the kernel has bound that number to one specific open file. The attacker can rename or replace the path all they like — your descriptor keeps pointing at the object you opened.

How it works internally. A file descriptor is an index into your process's open-file table, and each entry points at a concrete kernel file object (ultimately an inode). Path resolution is over; there is nothing left to re-resolve or swap.

  path "/tmp/f"  --(open, one atomic resolution)-->  fd 3
                                                       |
  attacker now renames /tmp/f  --------------->  (does not matter)
                                                       |
  fstat(3)  inspects  ------------------------->  the SAME object fd 3 holds

When to use / not use. Use fstat(fd) for every property you care about (type, owner, size, mode) after opening. The only thing you cannot fully learn from the descriptor alone is how the last path component was reached — that is what O_NOFOLLOW and openat handle.

Common pitfall. Opening the file safely and then still calling stat(path) for the check "because it is easier." That reintroduces the exact race you just removed. Check the fd, never the path.

Predict the output. A file /tmp/f is a regular file you own. Right after int fd = open("/tmp/f", O_RDONLY), an attacker replaces /tmp/f with a symlink to /etc/shadow. You then run fstat(fd, &st). Does st describe your original file or /etc/shadow? Why?

4. O_NOFOLLOW — refuse a symlinked final component

Definition. A flag to open that makes the call fail with errno == ELOOP if the last component of the path is a symbolic link.

Plain-language explanation. A symlink is a file whose contents are another path; opening it normally transparently follows it to the target. In a world-writable directory an attacker can drop a symlink where you expect a real file, aiming your privileged open at a sensitive target. O_NOFOLLOW says "if the final name is a symlink, do not follow it — fail instead."

How it works internally. During resolution the kernel notices the terminal component is a symlink and, because the flag is set, stops and returns an error rather than dereferencing it.

When to use / not use. Use it whenever you open a file by a path you do not fully control. Note the limitation: it only guards the last component. An attacker who controls an intermediate directory (/tmp/evil/../realfile) can still redirect you — that is why full safety needs per-component openat.

Common pitfall. Assuming O_NOFOLLOW makes the whole path symlink-proof. It protects one link in the chain, not all of them.

5. openat — walk the path one safe component at a time

Definition. openat(dirfd, name, flags) opens name relative to an already-open directory descriptor dirfd instead of resolving a whole path from the root.

Plain-language explanation. Rather than hand the kernel a long path and trust it to resolve every piece, you hold a descriptor to a directory you have already vetted and open just the next single name inside it. Combined with O_NOFOLLOW on each step, no component can be a symlink you did not approve, and no parent can be swapped after you have pinned it.

Goal: safely open  a/b/c  under a trusted start dir

  dfd0 = open(".", O_DIRECTORY)                    # pinned starting dir
  dfd1 = openat(dfd0, "a", O_DIRECTORY|O_NOFOLLOW)  # a is not a symlink
  dfd2 = openat(dfd1, "b", O_DIRECTORY|O_NOFOLLOW)  # b is not a symlink
  fd   = openat(dfd2, "c", O_RDONLY   |O_NOFOLLOW)  # c is the real file
  close dfd0, dfd1, dfd2

How it works internally. Each openat resolves exactly one name against a directory object you already hold, so there is no long unvetted walk and no window where a middle directory can be replaced under you.

When to use / not use. Use it for security-sensitive traversal of untrusted, multi-component paths. For a single file in a directory you fully own, a plain open with O_NOFOLLOW is usually enough and far simpler. (Modern kernels also offer O_PATH and the RESOLVE_* flags of openat2 for even stricter control; those are advanced extensions of this same idea.)

Common pitfall. Forgetting O_NOFOLLOW on the intermediate directory steps, which lets a symlinked directory component slip back in.

Find the bug. A reviewer sees: openat(dfd, "a", O_DIRECTORY) then openat(next, "b", O_DIRECTORY|O_NOFOLLOW). Why is the first call the weak link, and what one change fixes it?

6. Create-time races and mkstemp

Definition. A create-time TOCTOU happens when you pick a filename, check it is free, then create it — and an attacker plants a file (often a symlink) in the gap. The atomic defenses are O_CREAT | O_EXCL and, for temp files, mkstemp.

Plain-language explanation. O_EXCL (used with O_CREAT) tells the kernel: create this file and fail if it already exists — the check and the create become one indivisible step, so nothing can sneak in between. mkstemp(template) builds this in: it picks an unpredictable name and opens it exclusively for you, returning a descriptor.

char tmpl[] = "/tmp/appXXXXXX";  /* must end in >= 6 'X' */
int fd = mkstemp(tmpl);          /* atomic create, unpredictable name, returns fd */

When to use / not use. Use mkstemp for temporary files; use O_CREAT|O_EXCL when you must create a specific new file that must not pre-exist. Never use tmpnam, tempnam, or mktemp: they only generate a name, leaving a wide-open race before you create it.

Common pitfall. Fewer than six trailing Xs in the template (undefined/failing behavior) or reusing a predictable fixed name (an attacker pre-creates or symlinks it).

Function Returns Atomic + safe?
tmpnam / tempnam / mktemp a name only No — race before create
mkstemp an open fd Yes
open(..., O_CREAT|O_EXCL) an open fd Yes

Syntax notes

#include <fcntl.h>     /* open, openat, O_* flags */
#include <sys/stat.h>  /* fstat, struct stat, S_ISREG */
#include <unistd.h>    /* close, getuid */
#include <stdlib.h>    /* mkstemp */

/* USE first (get a descriptor), CHECK second (through that descriptor). */
int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
/*                             |            |
 *                             |            +-- close this fd on exec(): don't leak it
 *                             +--------------- fail (ELOOP) if last component is a symlink
 */
if (fd < 0) { /* inspect errno */ }

struct stat st;
if (fstat(fd, &st) < 0) { /* ... */ }   /* fstat takes the fd, NOT the path */
if (!S_ISREG(st.st_mode)) { /* refuse: not a regular file */ }

/* Relative-to-a-directory open, for safe per-component traversal: */
int dirfd = open(dir, O_DIRECTORY | O_NOFOLLOW);
int file  = openat(dirfd, "name", O_RDONLY | O_NOFOLLOW);

/* Atomic temp-file creation: */
char tmpl[] = "/tmp/appXXXXXX";
int tfd = mkstemp(tmpl);   /* creates + opens atomically; tmpl now holds the real name */

Key points: the check function (fstat) takes a descriptor; the flags O_NOFOLLOW, O_EXCL, and O_CLOEXEC each close a specific window; templates for mkstemp must end in at least six X characters.

Lesson

TOCTOU bugs happen when your program checks a file's property (does it exist, is it a regular file, do I own it) and then opens it.

Between the check and the open, an attacker can swap the file, or the directory entry, for something else.

This is a classic race condition, and it has powered countless privilege-escalation exploits.

Code examples

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/*
 * Safely open a file for reading only if it is a REGULAR file owned by us.
 * Strategy: USE first (open), then CHECK the descriptor (fstat) — never the path.
 * Returns an open fd on success, or -1 on failure (errno left meaningful where possible).
 */
static int open_regular_owned(const char *path)
{
    /* O_NOFOLLOW: refuse if the final path component is a symlink (ELOOP).
     * O_CLOEXEC : do not leak this descriptor across an exec(). */
    int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
    if (fd < 0) {
        fprintf(stderr, "open(%s): %s\n", path, strerror(errno));
        return -1;
    }

    struct stat st;
    if (fstat(fd, &st) < 0) {              /* CHECK the fd, not the path */
        fprintf(stderr, "fstat: %s\n", strerror(errno));
        close(fd);
        return -1;
    }

    /* These checks now describe the exact object fd refers to; it cannot be swapped. */
    if (!S_ISREG(st.st_mode)) {
        fprintf(stderr, "refuse: %s is not a regular file\n", path);
        close(fd);
        return -1;
    }
    if (st.st_uid != getuid()) {
        fprintf(stderr, "refuse: %s is not owned by us\n", path);
        close(fd);
        return -1;
    }
    return fd;                             /* caller owns fd and must close it */
}

int main(void)
{
    /* Create a safe temp file atomically, then open it back through the safe path. */
    char tmpl[] = "/tmp/toctouXXXXXX";
    int wfd = mkstemp(tmpl);               /* atomic: create + open, unpredictable name */
    if (wfd < 0) {
        fprintf(stderr, "mkstemp: %s\n", strerror(errno));
        return 1;
    }
    const char *msg = "hello from a race-free file\n";
    if (write(wfd, msg, strlen(msg)) < 0) {
        perror("write");
        close(wfd);
        unlink(tmpl);
        return 1;
    }
    close(wfd);

    int fd = open_regular_owned(tmpl);     /* the TOCTOU-safe open-then-check path */
    if (fd < 0) {
        unlink(tmpl);
        return 1;
    }

    char buf[64];
    ssize_t n = read(fd, buf, sizeof buf - 1);
    if (n < 0) {
        perror("read");
        close(fd);
        unlink(tmpl);
        return 1;
    }
    buf[n] = '\0';
    printf("read back: %s", buf);

    close(fd);                             /* resource cleanup */
    unlink(tmpl);                          /* remove the temp file */
    return 0;
}

What it does. It creates a temporary file atomically with mkstemp (unpredictable name, no create-time race), writes a line into it, then reopens it through open_regular_owned, which demonstrates the core defense: open first, then validate the descriptor with fstat, refusing anything that is not a regular file we own. Finally it reads the line back and cleans up (close, unlink).

Expected output (the XXXXXX in the path becomes random characters):

read back: hello from a race-free file

Key edge cases. If someone replaced the final component with a symlink between create and reopen, O_NOFOLLOW makes open fail with ELOOP and the program refuses. If the object is a directory, FIFO, or device, S_ISREG fails. If ownership does not match getuid(), it refuses. Every failure path closes the descriptor and removes the temp file — no leaks.

Line by line

We trace the successful run of main, then the safe-open helper.

  1. char tmpl[] = "/tmp/toctouXXXXXX"; — a writable array (not a string literal) because mkstemp overwrites the Xs in place.
  2. int wfd = mkstemp(tmpl); — the kernel picks a random name, creates it exclusively, and opens it, all atomically. tmpl now holds the real name (e.g. /tmp/toctouA1b2C3) and wfd is an open descriptor. No attacker could have pre-planted this file.
  3. write(wfd, msg, ...) then close(wfd) — we store the line and release the write descriptor.
  4. int fd = open_regular_owned(tmpl); — control enters the helper.
  5. Inside, open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC) — one atomic resolution binds fd to a concrete object. If the last component were a symlink, this returns -1 with errno == ELOOP.
  6. fstat(fd, &st) — fills st from the object fd holds, not from re-resolving the path. This is the pivotal line: the check now targets exactly what we opened.
  7. S_ISREG(st.st_mode) and st.st_uid != getuid() — both verdicts describe the pinned object; nothing can have been swapped after step 5.
  8. The helper returns fd; ownership of the descriptor passes to main.
  9. read(fd, buf, sizeof buf - 1) returns the byte count n; buf[n] = '\0' terminates the string so printf prints it safely.
  10. close(fd) and unlink(tmpl) release the descriptor and delete the temp file.

A small state trace of the security-relevant values:

Step fd st.st_mode type Decision
after open (5) valid (>= 0) unknown yet continue
after fstat (6) valid regular file continue
owner check (7) valid regular, uid matches accept
symlink present (5, alt) -1 (ELOOP) n/a refuse

The result is produced because the only property we ever trusted came from fstat(fd), and fd cannot be repointed once open returned it.

Common mistakes

Mistake 1 — access() (or stat()) then open() on the same path.

/* WRONG */
if (access(path, R_OK) == 0)        /* check the path */
    int fd = open(path, O_RDONLY);  /* use the path — attacker swapped it */

Why wrong: the safety verdict is about the object visible during access; open acts on whatever the path resolves to a moment later. Corrected: drop the pre-check entirely, open first, and inspect the fd (and errno on failure). Recognise it: any check syscall taking a path immediately followed by a use syscall taking the same path.

Mistake 2 — opening safely, then checking the path anyway.

/* WRONG */
int fd = open(path, O_RDONLY);
struct stat st;
stat(path, &st);   /* re-resolves the path: race reopened! */

Why wrong: you had the descriptor and threw the safety away by re-consulting the path. Corrected: fstat(fd, &st). Prevent it: make it a rule — after open, the path string is dead to you; only the fd speaks.

Mistake 3 — trusting O_NOFOLLOW for the whole path.

/* WRONG assumption */
int fd = open("/tmp/userdir/data", O_RDONLY | O_NOFOLLOW);
/* userdir could itself be an attacker symlink; only the LAST component is guarded */

Why wrong: O_NOFOLLOW guards only the final component. Corrected: walk with openat + O_NOFOLLOW on each component from a pinned starting directory. Recognise it: untrusted directories in the middle of the path.

Mistake 4 — using mktemp/tmpnam for temp files.

/* WRONG */
char *name = tmpnam(NULL);         /* just a name */
int fd = open(name, O_WRONLY | O_CREAT, 0600);  /* race: attacker planted it first */

Why wrong: name generation and creation are separate steps. Corrected: mkstemp(tmpl) (or O_CREAT|O_EXCL). Prevent it: treat tmpnam, tempnam, mktemp as banned in security-sensitive code.

Debugging tips

Compiler errors.

  • O_NOFOLLOW/O_CLOEXEC undeclared — you forgot #include <fcntl.h>, or need a feature macro (#define _GNU_SOURCE before includes) on some libc versions.
  • S_ISREG/struct stat unknown — add #include <sys/stat.h>.
  • implicit declaration of mkstemp/getuid — include <stdlib.h> and <unistd.h>.

Runtime errors (read errno).

  • open returns -1 with errno == ELOOP — the final component was a symlink and O_NOFOLLOW refused it. That is the defense working, not a bug.
  • mkstemp fails with EINVAL — the template did not end in at least six X characters.
  • EACCES/ENOENT on openat steps — a component is missing or unreadable; print which step failed.

Logic errors.

  • Checks pass but you are reading the wrong file: search your code for a stat/access on a path near an open on the same path — the classic pair.
  • You "fixed" it but still call stat(path) after opening; switch to fstat(fd).

Reproducing and observing a race (lab only).

  1. Insert a deliberate usleep(200000) between a (vulnerable) check and use to widen the window.
  2. From a second shell, loop a mv/ln -s to swap the target during that window.
  3. Trace with strace -f -e trace=openat,stat,lstat,fstat ./prog and read the sequence: a path-based check followed by a path-based use is the smell; a use followed by an fstat on a descriptor is the fix.

Questions to ask when it does not work. Am I checking a path or a descriptor? Is the directory writable by anyone I do not trust? Which exact component could be a symlink? Does my failure path close the fd and avoid acting on a refused file?

Memory safety

This is a security topic, so the concerns are about undefined trust as much as undefined behavior.

The vulnerability (labeled). TOCTOU race / CWE-367. Shown deliberately as the WRONG pattern:

/* VULNERABLE — do not ship */
if (stat(path, &st) == 0 && S_ISREG(st.st_mode))
    int fd = open(path, O_RDONLY);   /* object may differ from what stat saw */

The fix (paired). Open first, validate the descriptor, use O_NOFOLLOW, and (for temp files) mkstemp/O_CREAT|O_EXCL:

int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd >= 0 && fstat(fd, &st) == 0 && S_ISREG(st.st_mode) && st.st_uid == getuid())
    /* safe to use fd */;

Defensive principles for this topic.

  • Bind identity to a descriptor, not a name. The descriptor is the only stable handle.
  • Validate then act atomically. Prefer flags (O_EXCL, O_NOFOLLOW) that fold the check into the syscall.
  • Least privilege. Drop privileges before touching user-controlled paths where possible; the smaller your authority during the race, the smaller the damage if you lose it.
  • Fail closed. On any doubt (ELOOP, wrong type, wrong owner), refuse and clean up.

Ordinary C memory-safety reminders that still apply here.

  • Check every syscall return before using its result; an unchecked open returning -1 used as an fd corrupts later logic.
  • Size read buffers correctly and NUL-terminate before treating bytes as a string (buf[n] = '\0' only when n < sizeof buf).
  • Every successful open/mkstemp must be matched by a close; leaking descriptors is a resource bug and can defeat O_CLOEXEC's intent.
  • Watch off_t/size_t sizing when handling large files; do not truncate counts into int.

Real-world uses

Concrete real-world case. Setuid utilities such as sudo, su, passwd, and ping run as root but are invoked by unprivileged users, so they must never trust a path the user can influence. They open target files and validate the returned descriptors, use O_NOFOLLOW / openat for untrusted components, and create temp and lock files atomically. Historically, TOCTOU flaws in exactly this class of program (log rotators, mail spool handlers, temp-file helpers) turned into local root exploits — which is why the pattern is on every audit checklist. Beyond setuid, the same discipline protects web servers writing into shared upload dirs, container runtimes resolving mount paths, and package managers unpacking into system directories.

Best-practice habits.

For beginners:

  • Never follow a path-based check with a path-based use on the same path.
  • After open, use fstat(fd) for every property you check.
  • Use mkstemp for temporary files; never tmpnam/mktemp.
  • Add O_NOFOLLOW and O_CLOEXEC by habit when opening untrusted files.
  • Check every return value and print errno (strerror) so failures are visible.

For advanced practitioners:

  • Traverse untrusted multi-component paths with openat + O_NOFOLLOW from a pinned base directory; consider openat2 with RESOLVE_NO_SYMLINKS/RESOLVE_BENEATH on modern Linux.
  • Drop privileges (seteuid, capabilities) before touching user-controlled paths; keep the privileged window minimal.
  • Use O_DIRECTORY to assert a component is a directory, and O_PATH to reference without granting read/write.
  • In review, treat any access()/stat()/lstat() near an open()/unlink()/chmod() on the same path as a finding until proven safe; prefer descriptor-relative variants (fchmod, unlinkat, fstatat).
  • Document ownership: name which function owns and closes each descriptor.

Practice tasks

1. Beginner — Spot the race. Given a 10–15 line snippet that calls access(path, W_OK) and then, on success, open(path, O_WRONLY), write a short comment identifying the exact two lines that form the check/use pair and one sentence explaining what an attacker swaps in the gap. Concepts: check-vs-use, path-is-a-label. Hint: look for a path in both a check syscall and a use syscall.

2. Beginner — Descriptor-checked open. Write int open_reg(const char *path) that opens path read-only and returns the fd only if fstat reports a regular file; otherwise close and return -1. Requirements: use O_RDONLY | O_NOFOLLOW, check every return, no descriptor leaks. Example: on a regular file it returns a valid fd; on a directory it returns -1. Concepts: open-then-check, fstat, S_ISREG.

3. Intermediate — Safe temp file. Write a function that creates a temp file with mkstemp, writes a caller-supplied buffer to it, and returns the chosen filename via an out-parameter. Requirements: template ends in exactly six Xs, handle mkstemp failure via errno, close the fd, and on any write error unlink the file before returning failure. Constraints: no use of tmpnam/mktemp. Concepts: atomic create, O_EXCL semantics, cleanup.

4. Intermediate — Owner-and-type gate. Extend task 2 so it also refuses files not owned by the current user and refuses if open failed with ELOOP. Print a distinct message for each refusal reason. Input/output: feed it a symlink (expect an ELOOP refusal), a root-owned file (expect an owner refusal), and your own regular file (expect success). Concepts: st.st_uid vs getuid(), interpreting errno, fail-closed.

5. Challenge — Component-by-component safe traversal. Implement int open_beneath(const char *base, const char *rel) that opens base, then walks each /-separated component of rel with openat(..., O_NOFOLLOW), opening intermediate components with O_DIRECTORY and the final one O_RDONLY, returning the final fd or -1. Requirements: reject any component equal to ".."; close every intermediate directory descriptor; report which component failed. Constraints: never build and open the full joined path in one call. Hint: keep a "current directory fd" variable and replace it each step, closing the previous one. Concepts: openat, per-component O_NOFOLLOW, path-traversal defense. (Do not just call realpath — the point is safe stepwise resolution.)

Summary

A TOCTOU bug lives in the gap between a check and a use. The root cause is trusting a path — a mutable label the kernel re-resolves every time — as if it were a stable identity. Between stat/access on a path and open/unlink on that same path, an attacker in a writable directory can swap what the path points at, so the object you finally use is not the one you checked. This matters most in privileged (setuid) programs, where a won race becomes privilege escalation (CWE-367).

The fix: use first, then check the result through a file descriptor. Open the file, then call fstat(fd, ...) — the descriptor is pinned to one object and cannot be swapped. Most important syntax: open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC) then fstat(fd, &st); openat(dirfd, name, O_NOFOLLOW) for per-component traversal; mkstemp(template) for atomic temp files.

Common mistakes: checking a path then using it; opening safely then re-stat-ing the path; trusting O_NOFOLLOW for the whole path (it guards only the last component); using tmpnam/mktemp.

What to remember: never check a path — check a descriptor; make check-and-create atomic with O_EXCL/mkstemp; add O_NOFOLLOW for untrusted opens and openat for untrusted traversal; and always fail closed, closing the fd on refusal.

Practice with these exercises