open() usually combines two effects: pathname resolution selects a filesystem object, then the returned file descriptor carries an access mode for data I/O. Linux O_PATH splits those effects. A successful open(path, O_PATH) returns a descriptor that refers to the selected object while ordinary read() and write() through that descriptor are not permitted.

That split is useful anywhere a process needs a durable kernel reference for later metadata or pathname-relative operations without opening the object for data transfer. It also changes race analysis: later operations can start from the descriptor rather than resolving the original pathname again.

O_PATH descriptors are references with a restricted operation set

An O_PATH descriptor is still a file descriptor and follows normal descriptor lifetime rules. It can be duplicated, inherited subject to descriptor flags, passed over a Unix-domain socket with SCM_RIGHTS, inspected with fstat(), and closed.

Its operation set is intentionally narrower than a descriptor opened with O_RDONLY, O_WRONLY, or O_RDWR. Data-transfer calls such as read() and write() fail because O_PATH does not establish an ordinary file access mode. The descriptor is therefore not a read-only descriptor with unusual syntax; it represents a different kernel interface.

This distinction also appears in status flags. F_GETFL reports O_PATH for such a descriptor. Applications that accept arbitrary descriptors can use that fact when their contract requires a data-capable open file description rather than a pathname reference.

The pathname can change after the reference is acquired

A pathname is a route through directory entries, not permanent object identity. After an O_PATH open succeeds, another process can rename or unlink the directory entry used during lookup. The descriptor continues to refer to the object selected by the successful open for as long as the kernel reference remains valid.

A later open() of the original pathname performs a fresh lookup and can therefore select a different object. The two operations have different semantics:

int ref = open("queue/current", O_PATH | O_CLOEXEC);
if (ref == -1)
    return -1;

/* queue/current may be renamed or replaced here */

struct stat st;
if (fstat(ref, &st) == -1)
    return -1;

fstat(ref, ...) operates on the referenced object. It does not re-resolve queue/current. This removes pathname replacement from the interval between selection and metadata inspection, although it does not freeze the object’s mutable metadata or contents.

Directory references become lookup anchors

When an O_PATH descriptor refers to a directory, interfaces in the *at() family can use it as a dirfd. Relative lookup then begins at the referenced directory rather than at the process working directory.

That property makes directory identity explicit across code that would otherwise concatenate path strings or depend on mutable process state. For example, openat(dirfd, "state", flags) resolves state relative to the directory represented by dirfd.

The descriptor does not impose containment by itself. Components such as .., symbolic links, and mount traversal still follow the semantics of the operation being invoked. Code that requires constrained resolution needs an interface that expresses those constraints, such as openat2() with suitable RESOLVE_* flags. An O_PATH directory descriptor supplies an anchor; it is not a sandbox boundary.

AT_EMPTY_PATH can target the descriptor itself

Several Linux *at() interfaces support AT_EMPTY_PATH. With an empty pathname and an eligible descriptor, the operation targets the object referenced by that descriptor instead of performing a new nonempty pathname lookup.

This creates a useful composition model: one operation acquires a reference, then a later operation acts on that reference through an API whose pathname parameter would otherwise require another lookup. Support is interface-specific, so callers must check the documented flags and kernel requirements for each syscall rather than assuming that every *at() function accepts an empty path.

The same mechanism can apply to an O_PATH descriptor referring to a non-directory object. The descriptor’s value is therefore broader than directory-relative lookup alone.

O_PATH does not automatically mean that a symbolic link itself becomes the referenced object. With normal lookup, symbolic links are followed according to pathname-resolution rules.

Combining O_PATH with O_NOFOLLOW changes the final-component case: if the final component is a symbolic link, the returned descriptor can refer to the link itself. Linux interfaces that accept an empty path can then perform supported operations on that referenced link without resolving its target again.

This is another identity boundary rather than a general permission bypass. Directory search permissions still participate in pathname resolution, and later operations apply their own authorization checks.

Reference possession does not erase authorization checks

An O_PATH descriptor proves that the process holds a kernel reference to an object; it does not grant every operation that can name that object. Permission checks depend on the later syscall and its documented rules.

This separation matters when descriptors cross process boundaries. Passing an O_PATH descriptor with SCM_RIGHTS transfers a reference that the receiver can use with operations compatible with that descriptor. It does not convert the receiver into the original opener, nor does it turn all metadata or namespace mutations into permitted actions.

A design that treats the descriptor as a capability must therefore define the exact operation set exposed by the receiving process and the kernel checks attached to those operations. The useful property is stable reference transfer, not unlimited file authority.

Object identity and object state remain separate

Holding an O_PATH descriptor stabilizes the selected object reference across later pathname changes, but it does not snapshot object state. File size, timestamps, ownership, mode bits, extended attributes, and directory contents can still change when other actors have authority to modify them.

That boundary prevents a common overstatement: replacing repeated pathname lookup with descriptor-relative operations closes one class of name-to-object race, but it does not make a sequence of separate syscalls atomic. If correctness depends on metadata remaining unchanged between inspection and action, the design still needs a mechanism that enforces that condition.

O_PATH is most precise when treated as a reference primitive. It lets Linux software retain and transfer the result of pathname selection independently from ordinary data I/O authority. That separation gives later operations a stable object anchor while leaving mutation, authorization, and operation-specific semantics visible as separate concerns.