2026-05-09
This C function is supposed to safely write user-supplied data to a file, but only if the path points to a regular file owned by the current user. It looks defensive — it checks the file's type and ownership before writing. Spot the security hole.
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
// Write `data` to `path`, but only if it's a regular file we own.
int safe_write(const char *path, const char *data) {
struct stat st;
if (stat(path, &st) != 0)
return -1;
if (!S_ISREG(st.st_mode)) {
fprintf(stderr, "refusing: not a regular file\n");
return -1;
}
if (st.st_uid != getuid()) {
fprintf(stderr, "refusing: not owned by caller\n");
return -1;
}
FILE *f = fopen(path, "w"); // open AFTER the check
if (!f) return -1;
fputs(data, f);
fclose(f);
return 0;
}
This is a textbook TOCTOU — Time-of-Check to Time-of-Use — race condition, and it's a privilege-escalation vector when the program runs setuid or as a service.
Between stat(path, &st) and fopen(path, "w"), the path is resolved twice. An attacker with write access to the containing directory can race the two calls:
/tmp/foo as a normal file owned by the victim user.safe_write("/tmp/foo", evil). The stat() succeeds — regular file, correct uid.fopen() runs, the attacker does rm /tmp/foo && ln -s /etc/passwd /tmp/foo.fopen() follows the symlink and merrily clobbers /etc/passwd with attacker-controlled bytes.The window is small but absolutely exploitable — attackers use filesystem-event tricks (inotify, sched_yield storms, even slow disks) to widen it to milliseconds. The CERT secure coding catalog has filed this exact pattern under FIO01-C for two decades.
The deeper lesson: a path is a name, not an object. Every syscall that takes a const char * path re-resolves it from scratch, so any check on a path is meaningless by the time you act on it. The kernel offers no atomicity between two pathname-based calls.
Open the file first, then check the resulting file descriptor with fstat(). The fd is bound to a specific inode — once you hold it, swapping the path can't redirect your write. Add O_NOFOLLOW to refuse symlinks, and O_CLOEXEC for hygiene.
#include <fcntl.h>
int safe_write(const char *path, const char *data) {
int fd = open(path, O_WRONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return -1;
struct stat st;
if (fstat(fd, &st) != 0) { close(fd); return -1; }
if (!S_ISREG(st.st_mode) || st.st_uid != getuid()) {
close(fd);
return -1;
}
FILE *f = fdopen(fd, "w"); // wrap the SAME fd
if (!f) { close(fd); return -1; }
fputs(data, f);
fclose(f); // closes fd too
return 0;
}
For new files, use openat() with a directory fd plus O_CREAT | O_EXCL | O_NOFOLLOW — that pattern is atomic against the racing-symlink attack entirely.
