A pathname that begins inside a trusted directory can resolve somewhere else before open() returns. Parent components, symbolic links, magic links, mount points, and concurrent namespace changes all participate in Linux pathname lookup. Checking a string before opening it therefore does not establish where the kernel will finish resolution.

Linux openat2() places restrictions inside the lookup operation itself. A caller supplies a directory file descriptor, ordinary open flags, and a resolve policy in struct open_how. The kernel then applies those constraints while walking every relevant path component. This moves a security boundary from pre-validation of pathname text into the operation that actually resolves the pathname.

A directory descriptor anchors lookup to an object

Relative paths passed to openat2() are resolved from dirfd, as with openat(). That distinction matters because a directory file descriptor refers to an already opened filesystem object rather than requiring the process to find the directory again from a pathname.

A service can open a directory that represents an allowed tree, retain that descriptor, and resolve request paths from it:

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

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

RESOLVE_BENEATH requires successful resolution to remain below the directory represented by rootfd. An absolute input path is rejected, as is an absolute symbolic link that would reset lookup to the process root.

The descriptor anchor and the resolution rule solve different problems. dirfd selects the starting object. RESOLVE_BENEATH constrains where the subsequent walk may go.

RESOLVE_BENEATH rejects escape rather than emulating a root

RESOLVE_BENEATH is suitable when the input is expected to name a descendant of the starting directory. A component that would resolve outside that subtree causes the operation to fail.

This differs from RESOLVE_IN_ROOT. With RESOLVE_IN_ROOT, the directory represented by dirfd acts as a temporary root for that single lookup. An absolute path is interpreted relative to that directory, absolute symbolic links are also interpreted within it, and .. at the root does not move above it.

The distinction affects application semantics:

RESOLVE_BENEATH  -> escaping forms are rejected
RESOLVE_IN_ROOT  -> lookup treats dirfd as its root

Neither mode changes the process-wide root. The restriction belongs to the individual openat2() operation, so unrelated threads do not inherit a temporary namespace state from it.

Both flags currently block magic-link resolution as a side effect, but the documented interface does not make that side effect a permanent substitute for RESOLVE_NO_MAGICLINKS. Code whose security policy requires magic links to be blocked should request that flag explicitly.

Remaining beneath a directory does not imply that symbolic links are forbidden. A relative symbolic link can legitimately point to another object inside the permitted subtree. RESOLVE_BENEATH can allow that case while still rejecting an escape.

RESOLVE_NO_SYMLINKS establishes the stricter rule: symbolic-link traversal is disallowed for all path components. This also implies the magic-link restriction. The scope is broader than O_NOFOLLOW, which controls handling of a trailing symbolic link rather than prohibiting symlink traversal throughout the complete path.

That difference is significant for a path such as:

assets/current/config.json

If current is a symbolic link, O_NOFOLLOW on the final open does not by itself prohibit traversal through current. RESOLVE_NO_SYMLINKS applies during the whole lookup.

A service should therefore select symlink behavior from its object model. A package store that requires direct directory entries may prohibit symlinks entirely. A deployment tree that intentionally uses internal symlinks can instead keep them while bounding resolution with RESOLVE_BENEATH.

Mount boundaries require a separate constraint

A path can remain textually and hierarchically beneath a directory while crossing into another mounted filesystem. Bind mounts make this especially relevant because a mount can expose an object from elsewhere at a location inside the allowed tree.

RESOLVE_NO_XDEV rejects traversal across mount points, including bind mounts. It is separate from the subtree constraint because “below this directory” and “on this mount” are different policies.

This flag can also reject legitimate layouts. Systems commonly use bind mounts and nested mounts as ordinary deployment mechanisms. Applying RESOLVE_NO_XDEV globally can turn a valid filesystem layout into an application error. It is most appropriate when staying on the starting mount is an actual security or consistency requirement.

Linux exposes magic links through interfaces such as /proc/<pid>/fd/* and /proc/<pid>/exe. They resemble symbolic links in userspace but can cause the kernel to jump to an object without following ordinary symlink semantics.

RESOLVE_NO_MAGICLINKS rejects those jumps. This is relevant when an untrusted directory can expose procfs entries or when a lookup may otherwise encounter a magic link that refers to an object outside the intended filesystem context.

The flag is narrower than RESOLVE_NO_SYMLINKS: ordinary symbolic links can remain available while magic links are prohibited. This lets a policy preserve common symlink-based layouts without accepting the special object-jump behavior of procfs magic links.

Kernel lookup can fail when an escape cannot be ruled out

Path resolution runs against a namespace that other processes may modify. Directory renames and mount changes can race with lookup. For RESOLVE_BENEATH and RESOLVE_IN_ROOT, Linux can return EAGAIN when it cannot safely establish that a .. component stayed within the required boundary.

That error is part of the security contract rather than evidence that the kernel silently relaxed the restriction. A caller may retry the operation. If an actual escape is detected, the documented failure is EXDEV.

This behavior is stronger than a sequence that canonicalizes a pathname and then opens the canonical result. Two separate operations leave an interval in which namespace state can change. openat2() evaluates the restrictions as part of the lookup that yields the returned descriptor.

RESOLVE_CACHED expresses a latency constraint, not a trust boundary

RESOLVE_CACHED asks the kernel to complete lookup only from cached information. If revalidation or I/O would be required, the call fails with EAGAIN. An application can use this as a fast path and send the operation to a slower execution path when cache-only lookup is unavailable.

This flag does not make a pathname safer merely because its components were cached. It constrains the work permitted during resolution. Security restrictions such as RESOLVE_BENEATH, RESOLVE_IN_ROOT, or the no-link policies remain separate properties and can be combined when their semantics fit the operation.

The shared EAGAIN result also means callers need to interpret failure in the context of the resolve flags they supplied. A cache miss and an inability to prove a safe parent traversal can both require retry logic, but they arise from different constraints.

The returned descriptor is the security-relevant result

Once openat2() succeeds, the returned descriptor refers to the object reached under the requested lookup policy. Security-sensitive code can operate on that descriptor instead of reconstructing trust from the original pathname.

The boundary remains deliberately narrow. openat2() does not authenticate file contents, freeze the surrounding namespace, or prove that the opened object belongs to an approved publisher. It constrains pathname resolution. Content integrity, authorization, ownership checks, and later mutation remain separate concerns.

That separation is useful in systems that accept path fragments from less-trusted inputs. The pathname string can remain merely a request. The kernel performs the walk from a trusted directory object, applies explicit restrictions during that walk, and returns a descriptor only when the requested resolution policy holds.