A pathname is not an object reference. It is an instruction for traversing a mutable namespace, and another task can alter directory entries, symbolic links, or mounts while that traversal is relevant to an application. Linux openat2() addresses this boundary by placing path-resolution constraints in the same kernel operation that returns the file descriptor.
That placement matters when a program accepts a pathname but intends to confine resolution to a directory tree. A user-space sequence that inspects components and later calls open() separates validation from use. openat2() can instead make selected traversal rules part of the lookup itself.
dirfd establishes the starting directory
Like openat(), openat2() accepts a directory file descriptor and a pathname. For a relative pathname, lookup starts from dirfd rather than necessarily from the process current working directory.
The descriptor therefore gives the operation a stable directory reference even if a pathname that once named that directory is renamed elsewhere. It also avoids process-global current-directory changes as a mechanism for selecting lookup context.
int rootfd = open("/srv/data", O_PATH | O_DIRECTORY | O_CLOEXEC);
struct open_how how = {
.flags = O_RDONLY | O_CLOEXEC,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS,
};
int fd = syscall(SYS_openat2, rootfd, user_path, &how, sizeof(how));The example asks the kernel to open user_path relative to rootfd while rejecting resolution that escapes beneath that directory. The policy is attached to this operation; it does not alter the process root or current directory.
RESOLVE_BENEATH rejects escape during traversal
RESOLVE_BENEATH requires successful resolution to remain beneath the directory referenced by dirfd. An absolute input path is rejected, and traversal through an absolute symbolic link is also incompatible with that constraint.
A path such as reports/2026.txt can resolve normally when every component remains below the starting directory. A path that attempts to escape through parent traversal is rejected when the kernel detects that escape.
rootfd -> /srv/data
reports/q3.txt allowed if lookup stays beneath rootfd
../secrets/key rejected as an escape
/etc/passwd rejected as an absolute pathThis is stronger than checking the textual pathname for ... Namespace traversal is semantic: symbolic links and mount topology affect the object ultimately reached. The constraint operates on kernel path resolution rather than on string shape.
The interface can return EAGAIN when the kernel cannot safely establish that a .. traversal stayed within the required boundary because of a concurrent rename race. That result is distinct from a confirmed escape, which is reported with EXDEV for these resolution constraints.
RESOLVE_IN_ROOT changes absolute-path interpretation
RESOLVE_IN_ROOT has a different contract. It treats dirfd as the root for this lookup. Absolute input paths are interpreted relative to that directory, and absolute symbolic-link targets are interpreted there as well.
With rootfd referring to /srv/image, opening /etc/app.conf under RESOLVE_IN_ROOT resolves as though /srv/image were the root for this operation. Parent traversal at that temporary root does not move above it.
This resembles a per-operation root boundary, but it is not a process-wide chroot() transition. Other path operations in the process retain their existing root unless they independently use a constrained interface.
RESOLVE_BENEATH and RESOLVE_IN_ROOT therefore encode related but nonidentical policies. The former rejects forms of traversal that leave the starting subtree; the latter changes the root used to interpret the path.
Symbolic-link policy has component-level scope
O_NOFOLLOW affects the final pathname component for open()-family semantics. It does not prohibit symbolic links in earlier components. A path such as current/config can still traverse current when that component is a symbolic link.
RESOLVE_NO_SYMLINKS applies across the path-resolution process and also implies RESOLVE_NO_MAGICLINKS. When any component requires symbolic-link resolution, the operation fails with ELOOP, subject to the documented final-component behavior when O_PATH and O_NOFOLLOW are combined.
That broader scope is useful only when symbolic links are actually outside the accepted namespace contract. Many normal filesystem layouts use them intentionally, so prohibiting every symbolic link is a policy decision rather than a generic hardening switch.
RESOLVE_NO_MAGICLINKS is narrower. It blocks magic links such as entries exposed through parts of procfs without banning ordinary symbolic links. Code relying on that property should request the flag explicitly rather than depending on another flag’s current incidental treatment of magic links.
Mount traversal is a separate constraint
Remaining beneath a directory does not imply remaining on one mount. A bind mount or another mounted filesystem can appear inside the directory tree while still being a descendant in pathname terms.
RESOLVE_NO_XDEV rejects traversal across mount points, including bind mounts. This makes mount topology part of the accepted lookup policy. It can also reject layouts that are otherwise routine, so the constraint fits cases where crossing a mount boundary is specifically disallowed.
The distinction prevents two policies from being conflated:
RESOLVE_BENEATH -> constrain directory ancestry
RESOLVE_NO_XDEV -> constrain mount traversalA caller can combine them when both properties are required.
RESOLVE_CACHED exposes a nonblocking lookup boundary
RESOLVE_CACHED adds a different kind of constraint. The operation succeeds only when path resolution can be completed from the kernel’s lookup cache without revalidation or filesystem I/O. Otherwise it returns EAGAIN.
That error does not mean the target is absent or inaccessible. It means the requested cache-only execution condition could not be satisfied. An application can route that case to a path that permits blocking work, such as another thread or a retry without RESOLVE_CACHED.
This makes execution policy observable through the same open interface. The returned file descriptor still represents the resolved file; the added flag constrains the work the kernel may perform to obtain it.
The open_how size is part of ABI evolution
openat2() receives a pointer to struct open_how plus its size. New fields can be appended to the structure over time, with zero values preserving behavior equivalent to absence of those extensions.
Callers should therefore zero-initialize the structure before assigning fields. A designated initializer provides that property for fields not explicitly set.
struct open_how how = {
.flags = O_RDONLY | O_CLOEXEC,
.resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS,
};The kernel also rejects unknown or conflicting values rather than silently accepting arbitrary flag bits. This makes feature negotiation and invalid configuration visible at the system-call boundary.
Constrained lookup closes the validation-use gap
The central property of openat2() is not merely a larger set of open flags. Its resolution controls let the kernel evaluate namespace constraints while resolving the pathname that produces the descriptor.
That changes the shape of code handling externally supplied paths. Instead of first proving facts about one observed namespace state and then opening against a potentially changed state, the caller specifies the properties that the successful lookup itself must satisfy. Failure is then reported by the operation that attempted to cross the boundary.
The resulting file descriptor is still subject to ordinary file-descriptor lifetime and permission rules. What openat2() adds is a way to make path traversal policy an explicit condition of acquiring that descriptor, where directory ancestry, symbolic links, mount crossings, and cache-only execution can each be represented as distinct constraints.