Skip to content

Concepts ​

The vocabulary the deep-dives share. Each term is defined once here and grounded in the subject where it is most precisely stated; the deep-dives link back rather than redefine.

The race ​

TOCTTOU ​

Time of check to time of use: a program inspects a filesystem object by name, decides something, then acts on the name again — and the binding of name to object changed in between. Named and formalized by Bishop & Dilger (1996), who distinguish the check (access, stat, lstat) from the use (open, chmod, unlink) and observe that a name is not an object. Wei & Pu (2005) enumerate every check/use syscall pair; the count is 224.

Binding versus use ​

A path is a sequence of bindings (name → inode) walked left to right. A check establishes facts about the object at the end of one walk; a use starts a fresh walk. Nothing ties the two walks together, which is why every fix in this catalog is some way of walking once — in the kernel (openat2), or in user space one handle at a time (component walk).

Race window ​

The interval between check and use. Dean & Hu (2004) tried to make it statistically unwinnable (the k-race); Borisov et al. (2005) and Cai et al. (2009) showed the attacker can stretch it arbitrarily with a maze, so its length is the attacker's to choose, not the defender's.

k-race and hardness amplification ​

Repeating a check/use pair k times and requiring every repetition to agree, so the attacker must win 2k+1 consecutive races (Dean & Hu). Tsafrir et al. (2008) "amplify" this by walking the path one atom at a time so the defender's cache keeps each window tiny. Both are probabilistic defenses; both were broken by making the kernel's own lookup slow (Cai et al.). The field's conclusion — recorded by the systematic review — is that a defense must be race-free, not race-unlikely.

Filesystem maze ​

A chain of directories and symlinks, up to the kernel's symlink-expansion limit, arranged so that resolving one path takes a long, attacker-controlled time (Borisov et al.). Combined with atime polling or a name-cache hash-collision attack (Cai et al.) it lets an unprivileged process single-step a victim through its race window.

The fix ​

Directory handle (dirfd) ​

An open file descriptor (or NT HANDLE) on a directory, used as the origin of a relative lookup: openat(dirfd, "name", …). The handle names an inode, not a path, so a rename above it changes nothing and a symlink swap below it is visible as a symlink. Every library in the catalog is a wrapper around one (libpathrs, cap-std, Go os.Root, the openat crates, gnulib fts). On Windows the same object is OBJECT_ATTRIBUTES.RootDirectory (Windows NT).

Component walk ​

Resolving a relative path in user space one name at a time, each step an openat relative to the previous handle with O_NOFOLLOW | O_DIRECTORY (or the NT FILE_OPEN_REPARSE_POINT equivalent), so no step can follow a link and every step is atomic with respect to the handle before it. It is the fallback every library keeps for kernels without a scoped lookup (libpathrs's opath resolver, cap-std's manually module, Go's root_openat.go). The demonstration is examples/component-walk.d. Two costs: O(depth) syscalls, and a .. that must be handled in user space — see dot-dot.

Scoped lookup ​

A single in-kernel path resolution that refuses to leave a root: openat2 with RESOLVE_BENEATH or RESOLVE_IN_ROOT on Linux 5.6+, O_RESOLVE_BENEATH on FreeBSD. The kernel holds the locks a component walk cannot, so a .. can be checked against the root during the walk; when even the kernel cannot prove it (a concurrent rename), it fails closed with EAGAIN rather than guessing. Darwin's headers declare O_RESOLVE_BENEATH siblings that no surveyed consumer uses, and Windows has none; in practice both offer only a no-follow-any mode.

Two different guarantees that are easy to conflate. Beneath (RESOLVE_BENEATH, O_RESOLVE_BENEATH): the result lies under the root, symlinks that stay under it are followed. No symlinks (RESOLVE_NO_SYMLINKS, Darwin O_NOFOLLOW_ANY, NT OBJ_DONT_REPARSE): no component may be a link at all, which is stronger and cheaper to emulate but refuses legitimate links. The flag matrix shows the two disagreeing on the same path. A portable API must pick one and say which; the catalog's comparison argues for no symlinks as the portable floor.

Dot-dot ​

.. is the component the kernel and user space disagree about. In-kernel scoped lookup allows a .. that stays beneath (and detects one that does not); a component walk cannot see whether .. escaped without re-checking the root, so every user-space resolver either rejects it lexically before the first syscall (Go, this catalog's examples) or tracks a component stack and re-verifies (cap-std, libpathrs). Rejecting it up front is the only choice whose behaviour is identical on every platform.

A procfs symlink whose target is not a path but a kernel object: /proc/<pid>/fd/N, /proc/<pid>/root, /proc/self/exe. Following one can jump out of any root and re-open a file with different permissions — the runc CVE-2019-5736 mechanism. Refused by RESOLVE_NO_MAGICLINKS (implied by RESOLVE_NO_SYMLINKS and, in practice, by RESOLVE_BENEATH); the full account is procfs magic links.

O_PATH handle ​

A Linux descriptor that references an inode without opening it for I/O — "a location, not a file". Resolvers produce one, then re-open it through /proc/self/fd/N with the real flags, which is how libpathrs separates where from how. The re-open is itself a path lookup through procfs, which is why procfs must be trusted first.

Reparse point ​

The NTFS generalization of a symlink: a directory or file carrying a reparse tag (symbolic link, mount point / junction, AppExecLink, cloud placeholder…) that the object manager acts on during a parse. FILE_OPEN_REPARSE_POINT opens the point itself (final component only); OBJ_DONT_REPARSE refuses to parse through any. Forshaw's tools enumerate what an attacker can plant; Windows NT records what a defender must check.

Mount boundary ​

A bind mount or a mounted filesystem placed beneath a root changes what a path means without any symlink. RESOLVE_NO_XDEV refuses to cross one; elsewhere the only check is comparing st_dev (or NT FileIdInfo volume serial) before and after, which races.

fd-relative deletion ​

Removing a tree by opening each directory relative to its parent handle, listing it from the handle (fdopendir, NtQueryDirectoryFile) and unlinking each entry relative to it (unlinkat, FileDispositionInformationEx) — so a symlink planted mid-deletion is unlinked as a name, never descended. The shape of Rust's CVE-2022-21658 fix, CPython's rmtree and gnulib's fts; demonstrated by examples/fd-remove-tree.d.

Trust model ​

Chari, Halevi & Venema (2010) make explicit what every other design leaves implicit: a path is only as safe as the set of users who can modify each directory on it. Their safe_open tracks a per-component safe/unsafe bit, and once a component is writable by someone other than root and the caller, no later symlink, .. or multiply-linked file is accepted.

The systems, by what they are ​

Term in this catalogMeaning
ResolverThe code that turns a relative path plus a root handle into a handle (libpathrs, cap-std, Go)
Binding layerExposes the syscalls without choosing a policy (rustix, the openat crates)
WalkerEnumerates a tree from handles (gnulib fts, CPython fwalk)
MechanismWhat a kernel offers (Linux, BSD, Darwin, Windows)