A pathname can begin below a trusted directory and still escape that subtree during resolution. A .. component, symbolic link, magic link, or mount transition can change the object ultimately reached even when the initial directory file descriptor is trusted. Linux openat2() places constraints inside pathname resolution itself, so the kernel can reject a lookup that violates the selected boundary.

This differs from checking a pathname string before calling open(). Path resolution operates on filesystem objects and namespace state, not only text. openat2() extends the openat() model with a struct open_how whose resolve field controls traversal of pathname components.

dirfd establishes the lookup anchor

For a relative path, openat2() resolves from the directory referenced by dirfd, matching the basic anchoring model of openat(). The descriptor therefore gives the operation a stable directory reference rather than relying on the process current working directory.

The anchor alone is not a confinement rule. With ordinary openat(), a relative path can contain .., and symbolic links encountered during traversal can redirect lookup elsewhere. A trusted dirfd says where lookup starts; it does not by itself restrict every place lookup may reach.

The resolve field adds that missing policy layer. It is a bit mask, so several restrictions can be combined when their semantics fit the operation.

RESOLVE_BENEATH rejects escapes above the anchor

RESOLVE_BENEATH requires successful resolution to remain beneath the directory identified by dirfd. Absolute paths are rejected, as are absolute symbolic links that would leave the anchored hierarchy.

The important property is that the restriction participates in the kernel’s traversal. It is not a prefix comparison such as checking that a string starts with /srv/data. Prefix checks cannot represent symlink traversal, mount topology, or races that alter namespace relationships during lookup.

A conceptual request looks like this:

struct open_how how = {
    .flags = O_RDONLY | O_CLOEXEC,
    .resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS,
};

int fd = syscall(SYS_openat2, rootfd, path, &how, sizeof(how));

If the kernel detects an escape under the selected resolution policy, the call fails rather than returning a descriptor for the escaped target.

RESOLVE_IN_ROOT changes absolute-path semantics per operation

RESOLVE_IN_ROOT treats dirfd as the root directory for this pathname resolution. An absolute path is interpreted relative to that directory, and absolute symbolic links are likewise interpreted within that temporary root. A .. immediately above the selected root remains at that root, analogous to /.. at the process root.

This resembles a temporary root for one lookup, not a process-wide chroot() transition. No persistent root-directory state is changed for the caller.

RESOLVE_BENEATH and RESOLVE_IN_ROOT therefore express related but distinct boundaries. The former rejects attempts to escape below the anchor; the latter changes the lookup root so absolute forms are resolved inside the supplied directory.

O_NOFOLLOW affects the final pathname component. It does not prohibit symbolic links in earlier components.

RESOLVE_NO_SYMLINKS applies across all components and also implies RESOLVE_NO_MAGICLINKS. A lookup such as a/link/b/file fails if link is a symbolic link when this restriction is active, even though the final component is an ordinary file.

RESOLVE_NO_MAGICLINKS is narrower. It blocks kernel magic links, such as links exposed by some procfs entries, without rejecting ordinary symbolic links solely because they are symbolic links.

These flags should match the actual boundary being enforced. Rejecting every symbolic link can break valid filesystem layouts, so a blanket RESOLVE_NO_SYMLINKS policy is stronger than many operations require.

Mount traversal is a separate boundary

Remaining beneath a directory tree does not imply remaining on one mount. Bind mounts and ordinary mount points can introduce other mounted filesystems below the anchor.

RESOLVE_NO_XDEV rejects traversal across mount points, including bind mounts. This can be combined with other restrictions when crossing into another mount is outside the intended access model.

The distinction matters for security policy. A pathname may stay textually and hierarchically beneath an anchor while entering a mounted tree with different provenance or lifecycle. Conversely, applications that intentionally compose directory trees from bind mounts cannot use RESOLVE_NO_XDEV indiscriminately without rejecting legitimate paths.

Resolution races can produce EAGAIN

For RESOLVE_BENEATH and RESOLVE_IN_ROOT, the kernel may encounter a race in which it cannot safely establish that a .. traversal remains within the required boundary. In that case openat2() can fail with EAGAIN.

EAGAIN here is part of enforcing the constraint under concurrent namespace activity. Treating it as equivalent to a successful validation would discard the boundary. A caller can retry the operation according to its own retry and latency policy.

RESOLVE_CACHED uses EAGAIN for a different condition: the lookup cannot complete from cached path information alone. Error handling therefore has to account for the set of resolve flags used by the call.

The returned descriptor is the post-resolution capability

Once openat2() succeeds, the returned file descriptor refers to the object selected by the completed constrained lookup. Subsequent descriptor-based operations no longer need to re-resolve the original pathname for that open file description.

That makes the resolution boundary especially useful at interfaces that accept paths from a less-trusted context. The sensitive transition is from pathname to descriptor. Applying constraints during that transition avoids a design in which user space first attempts to prove a path safe and then asks the kernel to resolve it again under potentially changed namespace state.

The guarantee remains scoped to the individual lookup and the flags supplied. openat2() does not make an entire process filesystem-confined, and it does not replace access-control checks. It gives one pathname resolution a kernel-enforced traversal policy, then returns a descriptor only if that resolution satisfies the policy.