A pathname passed to openat2() can be resolved relative to a directory file descriptor while the kernel enforces restrictions on the resolution process itself. That distinction matters when a process accepts path components from a less-trusted source. A string check can inspect the pathname text, but it cannot by itself freeze the filesystem namespace while lookup proceeds.
openat2() places the policy beside the lookup. Its struct open_how separates ordinary open flags from resolve flags that constrain traversal. The resulting boundary is about resolution semantics, not merely the spelling of a path.
Directory-relative lookup supplies the anchor
Like openat(), openat2() accepts a dirfd and a pathname. A relative pathname begins lookup from the directory referenced by dirfd; an absolute pathname follows the system call’s absolute-path rules rather than using that descriptor as its starting point.
A process can retain a descriptor for an approved directory and use it as the anchor for later lookups:
struct open_how how = {
.flags = O_RDONLY | O_CLOEXEC,
.resolve = RESOLVE_BENEATH,
};
int fd = syscall(SYS_openat2, rootfd, path, &how, sizeof(how));The important object here is rootfd, not a textual prefix copied into path. A directory descriptor refers to an opened kernel object, so renaming that directory does not turn the descriptor into a different directory. Namespace changes can still affect entries reached beneath it, but the lookup anchor itself is descriptor-based.
RESOLVE_BENEATH rejects escapes during traversal
RESOLVE_BENEATH requires resolution to remain beneath the directory supplied by dirfd. Components that would escape that hierarchy cause the operation to fail. Absolute paths are also rejected under this mode.
This differs from deleting .. components in application code. A symbolic link encountered during lookup can redirect traversal even when the original input contains no visible parent component. Kernel resolution constraints apply to the path walk after filesystem objects and links participate.
The guarantee is intentionally scoped to one lookup. It does not create a permanent sandbox for the process, and it does not restrict unrelated file descriptors that the process already holds. The returned descriptor is an ordinary descriptor subject to its open mode and the capabilities of subsequent APIs.
RESOLVE_IN_ROOT changes the meaning of the lookup root
RESOLVE_IN_ROOT provides a related but different policy. For the duration of the lookup, the directory referenced by dirfd acts as the root for path resolution. Absolute paths and absolute symbolic-link targets are interpreted relative to that root rather than the process root.
That behavior is useful when path syntax should retain root-relative semantics inside a selected tree. It is not equivalent to changing the process root globally. The effect belongs to the individual openat2() operation.
RESOLVE_BENEATH and RESOLVE_IN_ROOT therefore express different contracts. The former rejects attempts to move above the anchor; the latter gives the lookup a temporary root interpretation. Code should select the contract that matches the path protocol instead of treating the flags as interchangeable hardening switches.
Link restrictions narrow redirection channels
RESOLVE_NO_SYMLINKS rejects symbolic links in every component of the lookup. It is stronger than O_NOFOLLOW, which concerns the final component for an ordinary open operation. When intermediate links are also outside the accepted model, a final-component-only restriction leaves a redirection channel open.
RESOLVE_NO_MAGICLINKS targets Linux magic links such as selected entries exposed through procfs. These objects can have semantics beyond ordinary symbolic links. The flag blocks that class without necessarily rejecting every ordinary symbolic link.
The distinction lets an interface state its actual constraint. A service that permits controlled symbolic links inside a tree may use a different policy from a service whose object model requires every component to be a concrete directory entry.
Mount boundaries are a separate axis
RESOLVE_NO_XDEV prevents resolution from crossing mount points, including bind mounts. Staying beneath a directory in pathname hierarchy does not otherwise imply staying on one mounted filesystem.
That separation is observable in environments where another filesystem is mounted inside the approved tree. RESOLVE_BENEATH can still regard the mounted subtree as beneath the anchor, while RESOLVE_NO_XDEV rejects the crossing.
Mount policy should therefore be explicit when filesystem identity matters. A hierarchy boundary and a mount boundary are different properties, even when a simple directory tree makes them appear identical.
Resolution races become API results
Filesystem namespaces can change concurrently. Components may be renamed, links may be replaced, and mount topology may change while another thread or process performs lookup. openat2() does not make the namespace static.
Its value is that selected resolution constraints are checked by the kernel as part of the lookup. The caller receives either a descriptor produced under those constraints or an error. This avoids a design in which application code first validates pathname state and then performs a separate open whose traversal can observe a changed namespace.
Some races can surface as errors such as EAGAIN for particular resolution conditions. Robust callers must treat failure as part of the contract and decide whether retrying is valid for their operation. A retry is a new lookup against potentially different namespace state; it is not continuation of the failed path walk.
Open flags and resolution flags govern different properties
The flags member of struct open_how controls properties associated with opening the final object, using flags such as O_RDONLY, O_CLOEXEC, or O_DIRECTORY. The resolve member constrains the traversal that selects that object.
Keeping those dimensions separate prevents a common modeling error. A descriptor can have the desired access mode yet have been reached through a traversal route that violates the service’s namespace policy. Conversely, strict traversal does not grant access that file permissions, mount state, or other kernel checks deny.
openat2() also validates its argument structure more strictly than interfaces that silently ignore some unknown flag bits. Callers should initialize unused fields to zero and pass the structure size expected by the kernel ABI contract.
The descriptor is the stable output boundary
After a successful call, later operations should normally use the returned descriptor rather than reconstructing the pathname and resolving it again. A second pathname lookup is a second namespace observation and can select a different object after concurrent changes.
This creates a precise interface boundary: pathname text is input to a constrained resolution operation; the file descriptor is the kernel reference produced by that operation. If a later API can operate on the descriptor directly, retaining that reference preserves the identity selected by the completed lookup.
The pattern does not solve every filesystem authorization problem. Directory permissions, descriptor inheritance, writable directories, mount administration, and the semantics of later operations remain relevant. Its narrower contribution is substantial: path traversal policy can be expressed as part of the system call that resolves the path, so validation and object acquisition share one kernel-mediated operation.