Skip to content

Safe Path Traversal ​

A primary-source survey of how serious systems open, walk and delete filesystem paths without being redirected by whoever else can write to the tree — thirty years of it, from the paper that named TOCTTOU in 1996 to Linux's openat2, Go's os.Root, Rust's CVE-2022-21658 rewrite and libpathrs. Nine papers, five kernels, nine libraries and runtimes, three bodies of talks and reporting, and three runnable examples that pin the facts a CI run can check.

The evidence base for sparkles.base.dir_handle — the directory-handle module that sparkles:test-utils' TmpFS and the fileset walker (FSD5–FSD7) will be built on — and the reason that module's design was sent back for this survey first.

This survey answers eight questions:

  1. Is TOCTTOU a bug in programs or in the API? In the API. Bishop & Dilger proved it undecidable to find statically; Wei & Pu counted 224 dangerous syscall pairs; Watson showed even an in-kernel wrapper cannot make a path-string check atomic. See concepts § the race.
  2. Can user space win the race by being fast? No. The k-race was broken by filesystem mazes; hardness amplification was broken by a name-cache complexity attack. Every probabilistic defense "pulls the performance characteristics of the OS into the trusted computing base". See comparison § consensus.
  3. What survived? Walking once from a handle: safe_open in 2010, openat2 in 2020, and the component walk every library keeps as a fallback. See axis 1.
  4. Do the kernels agree on ..? No — Linux allows a .. that stays beneath, FreeBSD refuses even a temporary escape, Windows resolves it in the parser. See axis 2 and the flag matrix.
  5. Beneath or no-symlinks? Two different guarantees; the portable floor is the stricter one. See axis 3.
  6. How do the libraries fall back, and what do they get wrong? Cache failure, never success; never downgrade per lookup — and cap-std does. See axis 4.
  7. What does everyone do about deletion? The same thing, since 2022: fd-relative, never descend a link, list from the handle. See axis 6 and fd-remove-tree.d.
  8. What should sparkles.base.dir_handle do? See the delta table.

Master catalog ​

SubjectCategoryYearPrimitive / contributionLink
Bishop & Dilgerpaper1996Names the race; paths are indirect pointers, descriptors direct→
Dean & Hu / Borisov et al.paper2004/2005The k-race defense and the filesystem maze that broke it→
Wei & Pupaper2005224 check/use pairs; live attacks on rpm, vi, emacs→
Watsonpaper2007Syscall-wrapper races; check and use must share a handle→
Tsafrir et al.paper2008Component-at-a-time hardness amplification; "openat would solve this"→
Cai, Gui & Johnsonpaper2009Name-cache complexity attack; probabilistic defenses declared dead→
Chari, Halevi & Venemapaper2010safe_open: per-component openat walk with an explicit trust model→
Payer & Grosspaper2012DynaRace: user-space identity cache, termination on mismatch→
Raducu, Rodríguez & Álvarezpaper2022Systematic review of 41 defenses; inode-reuse table→
Linux openat2kernel2020RESOLVE_BENEATH / IN_ROOT / NO_SYMLINKS / NO_MAGICLINKS / NO_XDEV→
Linux procfs and magic linkskernel2019–2026Why /proc is the escape hatch; private procfs via fsopen→
FreeBSD and OpenBSDkernel2021+O_RESOLVE_BENEATH, Capsicum, O_EMPTY_PATH; unveil→
Darwinkernel2020+O_NOFOLLOW_ANY family — present, and unused by every consumer surveyed→
Windows NTkernel—NtCreateFile + RootDirectory, OBJ_DONT_REPARSE, handle-based delete→
libpathrslibrary2019–2026openat2 resolver + O_PATH fallback; the CVE catalogue→
filepath-securejoinlibrary2017–2026Lexical SecureJoin → OpenInRoot; the runc lineage→
Go os.Rootruntime2025Per-component walk with restart-from-root on ..; three platform tiers→
Rust stdruntime2022remove_dir_all rewrite on Unix and NT; fs::Dir tracking issue→
rustixbinding2021–2026Every *at, raw openat2, Dir::read_from re-open; I/O safety→
cap-stdlibrary2020–2026Capability Dir; ancestor-stack ..; behaviour-probed O_RESOLVE_BENEATH→
openat / openat-extbinding2016–2023The minimal dirfd wrapper and its deprecation→
gnulib ftswalker2005+FTS_CWDFD, the fd ring, the openat emulation ladder; coreutils rm→
CPython shutilwalker2012+rmtree.avoids_symlink_attacks; lstat/fstat identity without O_NOFOLLOW→
Aleksa Sarai's talkstalks2019–2025Path resolution in container runtimes, LPC → LCA → ASG → "in the trenches"→
LWN openat2 coveragearticles2014–2026O_BENEATH to openat2 to "the difficulty of safe path traversal"→
Forshaw's Windows symlinkstalk+tools2015–2021What an attacker plants between check and use on NT→

Taxonomy ​

By where the walk happens ​

WhereSubjects
Kernel, whole pathopenat2, FreeBSD, Windows OBJ_DONT_REPARSE, Darwin (unused)
User space, per componentChari, Tsafrir, Go, Rust, cap-std, libpathrs, gnulib, CPython
User space, by nameclassic SecureJoin (securejoin), Go on js/Plan 9, CPython on Windows

By .. policy ​

PolicySubjects
Rejected lexicallyGo (escape count), Windows (parser), the examples
Popped from an ancestor stackcap-std, gnulib fd ring
Restart from rootGo
Re-verified through procfslibpathrs, securejoin
Kernel-checked, EAGAIN on raceopenat2; FreeBSD refuses outright

By fallback honesty ​

BehaviourSubjects
Publishes which resolver is liveCPython
Caches failure only, never downgradeslibpathrs, securejoin
Downgrades silentlycap-std (after 4 EAGAIN), gnulib
Decided at compile timeRust std

Milestones ​

WhenWhat
1996Bishop & Dilger name TOCTTOU binding flaws
2004Dean & Hu propose the k-race
2005Borisov et al. break it with filesystem mazes; Wei & Pu enumerate 224 pairs
2006coreutils 6.0 moves du, chmod, chown onto an openat-based fts (fts)
2007Watson: syscall-wrapper races
2008Tsafrir et al.: hardness amplification; POSIX.1-2008 standardizes openat
2009Cai et al. break it again
2010Chari et al. publish safe_open
2012CPython 3.3 rmtree goes fd-based (cpython); DynaRace
2014Drysdale's O_BENEATH for Capsicum-on-Linux (LWN)
2016tailhook openat crate (crates)
2019runc CVE-2019-5736 via /proc/self/exe (procfs); Sarai's LPC talk (talks)
2020-03Linux 5.6 ships openat2; cap-std begins (capstd)
2021FreeBSD 13 O_RESOLVE_BENEATH (bsd)
2022-01Rust CVE-2022-21658 remove_dir_all rewrite (rust); Raducu survey
2024runc CVE-2024-21626 (leaked /proc fd); securejoin OpenInRoot (securejoin); libpathrs at ASG (talks)
2025-02Go 1.24 os.Root (go)
2026-01LWN: "The difficulty of safe path traversal" (lwn)

Reading paths ​

Sources ​

Every deep-dive carries its own Sources block. Repositories were read from local clones at the commits pinned in each citation; papers from the publisher PDFs; LWN and vendor documentation by URL. Nothing here is written from memory.