A privileged service may accept a relative pathname from a less trusted component while intending to access only files below a designated directory. Checking the string for .., rejecting an initial slash, or inspecting symbolic links before a later open() does not bind the check to the kernel lookup that acquires the file. Directory entries can change between operations, symbolic links can redirect traversal, and mount topology can alter the namespace reached by a path.

Linux openat2() addresses this boundary at pathname resolution itself. Its struct open_how includes a resolve field whose RESOLVE_* flags constrain the kernel walk performed for that single open. The resulting file descriptor is returned only if the lookup satisfies those constraints. This changes the security decision from a userspace prediction about a path into a condition enforced during acquisition.

A directory descriptor anchors the lookup

Like openat(), openat2() can resolve a relative path from a directory referenced by dirfd. A directory file descriptor is a more stable anchor than a textual prefix because renaming that directory does not retarget the descriptor to a different directory object.

That anchor alone does not confine traversal. Ordinary relative resolution can process .. components and symbolic links that lead outside the starting directory. The resolve policy determines which namespace transitions the kernel will permit while walking from the anchor.

RESOLVE_BENEATH rejects a resolution that would leave the descendants of dirfd. Absolute pathnames are rejected, as are absolute symbolic-link targets that would escape the anchored hierarchy. The property is containment beneath the supplied directory, not a general ban on symbolic links.

RESOLVE_IN_ROOT applies a different model. For that open operation, dirfd acts as the root of pathname resolution. An absolute input path is interpreted relative to that directory, absolute symbolic links are also rooted there, and .. at the temporary root cannot move above it. This resembles a per-operation root boundary rather than a process-wide chroot() state change.

Containment and symbolic-link rejection solve different problems. A relative symbolic link can remain entirely below the anchor and still be valid under RESOLVE_BENEATH. A service that requires every component to be a non-symlink object can add RESOLVE_NO_SYMLINKS.

RESOLVE_NO_SYMLINKS applies to all pathname components, unlike O_NOFOLLOW, which controls following of the final component. It also implies the no-magic-link restriction. This distinction matters for code that previously combined a directory descriptor with O_NOFOLLOW and assumed intermediate components received equivalent treatment.

Linux magic links, such as certain entries exposed through procfs, have semantics beyond ordinary stored symbolic-link text. RESOLVE_NO_MAGICLINKS blocks their resolution without banning ordinary symbolic links. The manual page notes that RESOLVE_BENEATH and RESOLVE_IN_ROOT currently block magic links as a side effect, but applications requiring that property are expected to request RESOLVE_NO_MAGICLINKS explicitly because the side effect is not promised as permanent API behavior.

A narrow policy can therefore permit ordinary symlinks while rejecting magic links, or prohibit both. The correct combination follows the namespace contract of the service rather than a universal rule that all symlinks are unsafe.

Mount crossings form another namespace boundary

A pathname can stay textually below a directory and still cross into another mounted filesystem. Bind mounts make this especially relevant because a subtree can expose objects from a separate location without any .. component or symbolic link.

RESOLVE_NO_XDEV rejects mount-point traversal, including bind mounts. Combined with a containment flag, it can require resolution to remain both within the anchored hierarchy and on the same mount. That is stronger than directory containment alone.

The stronger rule can also reject legitimate layouts. Systems commonly use bind mounts and separate mounts inside application trees. A service should therefore use RESOLVE_NO_XDEV only when crossing a mount is outside its intended object-selection policy. The flag expresses a concrete boundary; it is not a generic hardening switch with no compatibility cost.

Resolution failure is part of the security contract

openat2() can return EXDEV when RESOLVE_BENEATH or RESOLVE_IN_ROOT detects an attempted escape. It can also return EAGAIN when the kernel cannot safely establish that a .. traversal remains confined because of a concurrent namespace race. In that case, retrying the complete openat2() operation is permitted.

This behavior is significant for privileged callers. Treating EAGAIN as permission to fall back to a weaker openat() path would remove the property that caused the operation to fail safely. Compatibility handling must preserve the intended resolution policy rather than silently changing the acquisition primitive.

RESOLVE_CACHED has a separate use of EAGAIN: it requests a lookup that completes from cached information without revalidation or I/O. Failure under that flag indicates that the cached fast path was insufficient. A caller may retry without RESOLVE_CACHED while retaining the security-relevant containment and link restrictions.

Unknown or conflicting values in open_how are rejected rather than silently ignored. The structure is extensible, and callers are expected to zero-fill it so future fields begin with their defined zero behavior.

A successful open fixes the object, not the pathname

The security value of openat2() ends with acquisition of a file descriptor that satisfied the requested lookup constraints at that operation. Once returned, the descriptor refers to the opened file object even if directory entries are subsequently renamed or replaced.

That stability does not freeze the object’s contents, permissions, or other mutable state. If a service needs content integrity, immutable metadata, or authorization that remains valid after open, those properties require separate controls. Path-resolution confinement only determines which object can be acquired through the constrained namespace walk.

The distinction also limits claims about later path operations. Validating one path with openat2() and then reopening the same text through another API does not transfer the original guarantee. Security-sensitive work should continue from the acquired descriptor, or apply an equivalent constrained lookup to each separate pathname operation.

Per-open confinement keeps namespace policy local

openat2() does not create a mount namespace and does not isolate an entire process. Its resolution flags govern one pathname lookup. That scope is useful for services that handle both trusted internal paths and externally supplied names without changing process-global filesystem state.

The mechanism is strongest when the directory descriptor represents an intentionally selected authority boundary and the RESOLVE_* set matches the permitted namespace transitions. RESOLVE_BENEATH supplies descendant containment, RESOLVE_IN_ROOT supplies temporary rooted resolution, link flags control redirection classes, and RESOLVE_NO_XDEV controls mount crossings.

Those properties remain narrower than sandboxing. They do not constrain already-open descriptors, network access, unrelated syscalls, or future path lookups that omit the policy. Their security effect is precise: the kernel refuses to return a descriptor when the requested pathname cannot be resolved within the boundaries declared for that open.