TOCTOU竞争条件本质是检查与使用间存在时间窗口,表现为先用access/stat等检查路径状态、再用open/unlink等操作时路径被篡改,导致权限绕过或误操作;正确做法是跳过预检、直接操作并处理errno,或使用O_CREAT|O_EXCL等原子接口。

TOCTOU 竞争条件的本质是“检查和使用之间存在时间窗口”
在 C++(或任何系统编程语言)中,TOCTOU 不是某个函数的 bug,而是对文件系统操作的一种**时序误用模式**:你先用 access()、stat() 或 open() with O_EXCL 以外的方式“检查”一个路径(比如“文件是否存在”“是否有读权限”),再用 open()、fopen()、unlink() 等“使用”它——而这两次系统调用之间,路径的状态可能已被另一个进程/线程篡改。
典型后果包括:绕过权限校验、打开被替换的恶意文件、删除本不该删的文件、写入到符号链接指向的非预期位置。
为什么 access() + open() 是经典 TOCTOU 模式
access() 检查的是当前进程的有效 UID/GID 对路径的权限,但不加锁、不阻塞、不保证后续操作仍有效。攻击者可在 access() 返回成功后、open() 执行前,用 symlink() 替换目标路径为指向敏感文件(如 /etc/passwd)的符号链接。
-
access("/tmp/myfile", R_OK)返回 0 → 程序认为可读 - 攻击者执行
symlink("/etc/passwd", "/tmp/myfile") -
fd = open("/tmp/myfile", O_RDONLY)实际打开了/etc/passwd
正确做法是:跳过 access(),直接用 open() 并检查其返回值和 errno;或使用带原子语义的接口(如 Linux 的 O_PATH + openat() 配合 AT_NO_AUTOMOUNT,但仍有局限)。
立即学习“C++免费学习笔记(深入)”;
C++ 中真正能缓解 TOCTOU 的底层手段有限
标准 C++ 库()的 exists()、is_regular_file()、status() 全部基于 stat(),本质仍是检查;而 std::fstream 构造或 std::filesystem::remove() 是独立的使用操作——二者之间没有原子性保障。
可行的务实策略:
- 永远优先用
open()(C API)或fopen()直接尝试操作,根据errno(如EACCES、ENOENT、EISDIR)做分支处理,而非预检 - 涉及创建文件时,强制使用
O_CREAT | O_EXCL(配合open()),确保创建行为本身是原子的 - 删除文件前若需确认类型,用
unlinkat(AT_FDCWD, path, AT_REMOVEDIR)并捕获ENOTDIR/EISDIR错误,比先stat()再unlink()更可靠 - 避免在不可信目录(如
/tmp)中依赖路径字符串做逻辑判断;改用openat()+ 文件描述符传递,把“打开”动作尽可能前置并复用
一个易被忽略的点:符号链接遍历控制不是万能的
有人以为用 stat() 替换 lstat() 就能“穿透链接拿到真实 inode”,从而规避 TOCTOU——这是错的。因为 stat() 本身仍是检查,且在多级路径(如 /tmp/a/b/c)中,攻击者可在任意一级插入符号链接,而 stat() 的遍历过程本身就有多个中间检查点。
例如:stat("/tmp/a/b/c") 会依次检查 /tmp、/tmp/a、/tmp/a/b、/tmp/a/b/c 的可访问性与类型。只要其中任一环节被竞态替换,结果就不可信。真正降低风险的方式是:用 openat(AT_FDCWD, "a", O_PATH | O_NOFOLLOW) 获取目录 fd,再逐级 openat(dir_fd, "b", ...),全程用 O_NOFOLLOW 控制链接解析,并复用 fd 避免重复路径解析。
这很麻烦,所以现实项目中更常见的选择是:接受 TOCTOU 在某些场景下无法彻底消除,转而通过最小权限(如容器化、seccomp)、可信路径白名单、以及日志审计来限制危害面。










