A launcher selects an executable from a directory, checks attributes or content, and then starts it. If selection and execution each resolve the pathname independently, a rename, symlink change, or directory replacement between those operations can make the executed object differ from the object that was checked.
Linux execveat() can move that boundary from a second pathname lookup to an already acquired file reference. With AT_EMPTY_PATH, an empty pathname tells the kernel to execute the object referred to by dirfd. That descriptor may have been opened with O_PATH. The execution decision still passes through normal kernel permission and executable-format checks, but object selection no longer depends on resolving the original pathname again.
Object identity survives pathname replacement
A file descriptor refers to an opened kernel object rather than storing a pathname that must be resolved again for every operation. After a launcher opens an executable, another process can rename the directory entry or replace the original pathname without redirecting that descriptor to the replacement.
This property changes the shape of a check-then-execute sequence. A pathname-based sequence can inspect one object and later execute another if namespace state changes between the two lookups. A descriptor-based sequence can inspect metadata through the acquired descriptor and pass the same reference to execveat().
The distinction is narrow but useful. It binds selection to an opened object; it does not freeze that object’s bytes. If an actor can modify the underlying file after the launcher verifies its content, descriptor-based execution alone does not preserve the verified byte sequence. File ownership, write permissions, immutable storage design, sealing mechanisms where applicable, or another integrity control must address content mutation.
AT_EMPTY_PATH removes the second lookup
execveat() also supports relative pathname execution against a directory descriptor. That form still performs pathname resolution for the supplied relative path. The stronger identity property for an already selected executable comes from an empty pathname combined with AT_EMPTY_PATH.
Conceptually, the sequence is:
int fd = open(candidate, O_PATH | O_CLOEXEC);
if (fd == -1)
fail();
verify_selected_object(fd);
char *const argv[] = { "worker", NULL };
char *const envp[] = { NULL };
execveat(fd, "", argv, envp, AT_EMPTY_PATH);For a native binary, setting close-on-exec on the selection descriptor prevents that descriptor from remaining open in the new program after a successful execution. The descriptor has served as the object-selection capability and normally has no further role.
AT_SYMLINK_NOFOLLOW addresses a different case. When a nonempty path identifies a symbolic link, that flag makes the call fail rather than following the final link. It does not replace the identity benefit of opening the target first and executing the resulting descriptor.
Scripts expose a close-on-exec conflict
Interpreter scripts create an important edge condition. For an executable script beginning with a shebang, the kernel starts the named interpreter and provides a reference that lets the interpreter access the script. In the AT_EMPTY_PATH form, that reference depends on the execution descriptor remaining available long enough for the interpreter path.
If close-on-exec is set on a descriptor that refers to such a script, execveat() can fail with ENOENT. Clearing close-on-exec avoids that failure, but then the descriptor is inherited by the interpreter. Recursive script execution can accumulate inherited descriptors and eventually consume descriptor-table capacity.
This is not equivalent to the native-binary case. A launcher that accepts both binaries and scripts must account for executable format rather than applying one descriptor-lifetime rule to every target. If descriptor noninheritance is a hard boundary, direct script execution through this pattern may be incompatible with that requirement.
Execution permission remains a separate gate
Holding a descriptor does not itself grant permission to execute the referenced file. execveat() applies the execution rules associated with execve(), including executable-format processing and permission checks. A descriptor acquired with O_PATH is therefore an object reference, not an authorization token that bypasses filesystem execute policy.
Other process-transition rules also remain in force. Set-user-ID and set-group-ID handling, file capabilities, no_new_privs, mount flags such as nosuid, tracing state, Linux Security Module decisions, and related execve() semantics are not replaced by descriptor-based selection.
That separation matters for security claims. execveat() can make executable identity stable across pathname namespace changes while the kernel independently decides whether the caller may execute that object and what credential transition is permitted.
Relative dirfd execution narrows namespace dependence without removing it
The nonempty relative-path form has a different operational role. Supplying a directory descriptor makes resolution relative to that directory rather than the process current working directory. This can keep execution anchored to a directory object even if the current working directory changes or a textual prefix elsewhere in the namespace is replaced.
The final path components are still resolved at call time, so concurrent changes beneath that directory can affect the selected object. If the launcher needs to bind execution to a specific file, it must acquire that file reference before the execution call.
This distinction mirrors a broader descriptor-oriented design pattern on Linux: directory descriptors can stabilize the starting point of a lookup, while file descriptors can stabilize the identity of the selected object. Those are related but different boundaries.
Descriptor selection does not establish content immutability
A security design can use execveat() to close a pathname race and still retain a content race. Suppose a privileged launcher opens a file, hashes its contents, and then executes it by descriptor. An attacker who cannot replace the descriptor’s object but can write to that same inode may alter bytes after hashing and before or during executable loading.
The required integrity property therefore has two parts. Object selection must remain attached to the inspected object, and the inspected content must not remain attacker-mutable across the verification-to-execution interval. The first part is supplied by descriptor identity. The second comes from storage and permission policy, or from a mechanism that makes the relevant content immutable for the required interval.
This separation keeps the guarantee precise. Descriptor-based execution is an identity primitive at the process-image boundary. It removes a class of namespace substitution from that boundary, but it does not turn a mutable file into an immutable executable.