openat2 Makes Path-Resolution Policy Part of the Open

A service receives a relative pathname and intends to open only objects below a directory it already trusts. A lexical check can reject obvious .. components, yet the filesystem namespace may contain symbolic links, mount points, or concurrent renames that change the path walk after that check. The security decision and the file open then describe two different moments.

Linux openat2() provides a narrower boundary. Its resolve flags constrain the kernel’s path-resolution operation that produces the file descriptor. The mechanism does not make arbitrary path handling safe, but it can move several confinement rules from preflight string logic into the lookup that actually selects the object.

A directory descriptor supplies the anchor

Like openat(), openat2() accepts a directory file descriptor and resolves a relative pathname from that directory. The important extension is struct open_how, whose resolve field applies restrictions to components encountered during the path walk.

RESOLVE_BENEATH rejects a resolution that would escape above the supplied directory. Absolute pathnames and absolute symbolic links are incompatible with that policy. This is materially different from checking that a normalized string starts with an expected prefix: the kernel evaluates the restriction while traversing the namespace.

RESOLVE_IN_ROOT has different semantics. It treats the directory descriptor as a temporary root for that lookup. An absolute pathname is interpreted from that directory, and attempts to traverse above it with .. remain at the temporary root. The flag resembles a per-operation root change rather than a process-wide chroot().

The two policies express different contracts. BENEATH rejects escape attempts; IN_ROOT scopes resolution into a supplied root. Code should select the contract that matches the operation rather than treating the flags as interchangeable confinement switches.

Containment does not necessarily mean that every symbolic link must be rejected. A relative symbolic link can remain inside the permitted tree and still satisfy a beneath-style boundary.

When a security boundary requires no symlink traversal at all, RESOLVE_NO_SYMLINKS applies that rule to every component encountered by the lookup. This differs from O_NOFOLLOW, which controls following of a symbolic link in the final component.

That distinction matters for a path such as tenant/current/report, where current is an intermediate symlink. A final-component rule does not govern that intermediate traversal. A resolution-wide rule does.

Linux also exposes RESOLVE_NO_MAGICLINKS for magic links such as relevant entries in procfs. Current RESOLVE_BENEATH and RESOLVE_IN_ROOT behavior also blocks magic-link resolution, but the documented interface warns that callers requiring this property should request RESOLVE_NO_MAGICLINKS explicitly rather than depend on that current side effect.

Mount crossings form another boundary

A pathname can remain lexically below a directory while crossing into another mounted filesystem. Bind mounts make this especially easy to miss because a mount can graft another subtree into an apparently ordinary directory hierarchy.

RESOLVE_NO_XDEV rejects mount-point crossings, including bind mounts. It therefore adds a filesystem-topology condition beyond pathname containment.

That restriction is not a universal hardening default. Systems commonly use mount points and bind mounts in legitimate directory layouts. Applying RESOLVE_NO_XDEV without a concrete boundary requirement can turn valid paths into failures. The security property must match the deployment model.

Race detection can produce a retryable failure

Kernel-enforced resolution policy does not imply that every concurrent namespace mutation can be hidden from the caller. With RESOLVE_BENEATH or RESOLVE_IN_ROOT, openat2() can return EAGAIN when the kernel cannot prove that a .. traversal remained confined because of a concurrent rename or mount change.

That failure is part of the security contract. Treating it as an ordinary success fallback to an unrestricted open() would discard the property that caused the call to fail. A caller may retry the constrained operation when its application semantics permit a retry.

Other policy failures are distinct. An escape detected under BENEATH or IN_ROOT can produce EXDEV; forbidden symlink traversal under RESOLVE_NO_SYMLINKS can produce ELOOP; crossing a mount under RESOLVE_NO_XDEV can also produce EXDEV. Error handling therefore participates in the boundary rather than merely reporting I/O trouble.

The returned descriptor closes the lookup phase

A successful call returns a file descriptor for the object selected by the constrained path walk. Subsequent descriptor-based operations act on that opened object rather than repeating the original pathname lookup.

This does not freeze the object or the filesystem around it. File contents can change, metadata can change subject to permissions, and directory entries can be renamed. The useful property is narrower: the pathname policy and object acquisition occurred in one kernel resolution operation, and the resulting descriptor refers to the object acquired by that operation.

That boundary complements descriptor-relative designs. A service can retain a descriptor for a trusted root, resolve untrusted relative names with explicit restrictions, then keep sensitive follow-up operations on descriptors where possible.

Resolution flags do not replace authorization

openat2() constrains pathname resolution; it does not decide whether an authenticated principal is entitled to the selected object. Normal filesystem permission checks still apply, and application-level authorization remains separate.

It also does not validate file contents, establish ownership expectations, prevent all filesystem races after the open, or turn a mutable directory tree into an immutable sandbox. A caller that requires a regular file, specific metadata, or a particular ownership relation still needs those conditions expressed and checked using suitable interfaces.

Kernel support is another implementation condition. openat2() is Linux-specific and was introduced in Linux 5.6. Software with compatibility paths for older kernels must ensure that a fallback preserves the required security property; silently replacing constrained resolution with an unrestricted pathname open is not equivalent.

The architectural gain is precise. A path boundary that exists only in string validation can diverge from the filesystem walk that follows. openat2() can attach containment, symlink, magic-link, and mount-crossing rules directly to that walk, making the file descriptor the result of the constrained operation rather than the result of a later, less restricted lookup.