A pathname passed to openat2() can be rejected even when the same pathname would resolve successfully through openat(). The difference comes from open_how.resolve: Linux can apply traversal constraints while resolving every component of that single open operation.
This changes the boundary around path handling. A directory file descriptor can act as more than a starting point; resolve flags can restrict escapes, symbolic-link traversal, mount crossings, and lookups that require work beyond cached state.
Resolution policy is attached to one operation
openat2() extends the openat() model with an extensible struct open_how:
#include <linux/openat2.h>
struct open_how how = {
.flags = O_RDONLY | O_CLOEXEC,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS,
};The resolve field is a bit mask that changes pathname resolution for the call. It does not alter the process root, current working directory, mount namespace, or later opens.
That per-operation scope is significant for services that accept path fragments from a less-trusted source. The policy travels with the lookup that needs it instead of relying on a process-wide directory transition.
The structure should be zero-initialized. Its ABI is designed so later kernels can append fields, with zero in new fields representing absence of the extension.
RESOLVE_BENEATH rejects escapes above dirfd
With RESOLVE_BENEATH, resolution must remain beneath the directory referenced by dirfd. A component that would make resolution leave that subtree causes the operation to fail.
Consider a service holding an O_PATH descriptor for /srv/data and receiving tenant/report.bin. A relative lookup can be constrained to descendants of that descriptor:
int fd = syscall(SYS_openat2, rootfd, "tenant/report.bin",
&how, sizeof(how));The constraint covers the path-resolution process, not merely a textual check for ... Absolute paths and absolute symbolic links are rejected under RESOLVE_BENEATH because they would select a resolution origin outside the supplied directory boundary.
This distinction avoids treating pathname strings as if they already represented resolved filesystem objects. Components can be symbolic links, mount points, or objects changed concurrently, so lexical filtering alone does not express the same kernel-enforced property.
RESOLVE_IN_ROOT gives the lookup a temporary root
RESOLVE_IN_ROOT uses the directory referenced by dirfd as the root for that operation. An absolute input path is interpreted relative to that directory, and an absolute symbolic link encountered during resolution is also interpreted relative to it.
Its handling of .. follows root-like semantics. Once resolution is at the supplied root, another parent component remains there rather than escaping above it.
That behavior differs from RESOLVE_BENEATH. The latter rejects escape attempts; RESOLVE_IN_ROOT changes the root context used by the lookup. It resembles a temporary chroot() for pathname resolution without permanently changing the process root.
The scope remains one openat2() call. It is not a replacement for broader isolation mechanisms when a process needs a persistent filesystem boundary.
Symlink controls have different widths
O_NOFOLLOW and RESOLVE_NO_SYMLINKS are not equivalent.
O_NOFOLLOW controls treatment of a symbolic link in the final path component for an open. Intermediate symbolic links can still participate in normal resolution.
RESOLVE_NO_SYMLINKS blocks symbolic-link resolution in every path component. It also implies RESOLVE_NO_MAGICLINKS.
Magic links are a separate Linux concept, notably exposed through entries such as /proc/<pid>/fd/*. RESOLVE_NO_MAGICLINKS blocks resolution of those objects without rejecting ordinary symbolic links.
For both no-link resolve modes, a special final-component case exists: when the final component is the prohibited link type and the open uses O_PATH | O_NOFOLLOW, the call can return an O_PATH descriptor referring to the link itself rather than following it.
Mount boundaries can be part of the policy
RESOLVE_NO_XDEV rejects path resolution that crosses a mount point. Bind mounts are included in that restriction.
This is stronger than requiring a path to remain textually beneath a directory. A subtree can contain mounted filesystems, so descendant path components do not necessarily remain on the same mount.
That strength can also reduce compatibility. Bind mounts are common in deployed filesystem layouts. A service that does not specifically require a same-mount property can reject otherwise valid paths if it enables RESOLVE_NO_XDEV indiscriminately.
Resolve flags therefore describe separate constraints that can be combined according to the actual boundary: subtree containment, link handling, and mount traversal are distinct properties.
RESOLVE_CACHED turns lookup work into EAGAIN
RESOLVE_CACHED adds a performance-oriented constraint. The operation succeeds only when pathname resolution can be completed from cached lookup information without revalidation or I/O. Otherwise openat2() fails with EAGAIN.
This flag does not mean that the named file is permanently cached, nor does it create a correctness shortcut around normal pathname semantics. It makes cache-only resolution a condition of this attempt.
An event-driven service can use that result as a dispatch boundary: a cache-only attempt stays on a latency-sensitive path, while EAGAIN can move the operation to a context permitted to perform a potentially slower lookup.
EAGAIN can also arise with RESOLVE_BENEATH or RESOLVE_IN_ROOT when the kernel cannot safely establish the requested escape constraint during a race. Callers therefore need to interpret the error in the context of the flags they supplied.
Constraint failures are part of the interface
Resolve-policy violations are observable as errors rather than silent fallback to ordinary lookup.
An escape detected under RESOLVE_BENEATH or RESOLVE_IN_ROOT can produce EXDEV. Crossing a mount with RESOLVE_NO_XDEV also produces EXDEV. A prohibited symbolic link under RESOLVE_NO_SYMLINKS produces ELOOP, as does a prohibited magic link under RESOLVE_NO_MAGICLINKS.
Unknown resolve bits are rejected. This strict handling is useful for policy-bearing calls because unsupported constraints are not silently discarded.
The resulting model is narrower than a general sandbox and stronger than pathname prevalidation. openat2() places kernel-enforced conditions directly on one resolution operation, allowing code to state which traversal properties must hold before a file descriptor is returned.