A pathname describes how to find an object now. An open descriptor refers to an object already selected by the kernel. Separating those ideas explains why renaming or unlinking a name does not retarget an existing open handle. Directory entries, inodes and open file descriptions have different lifetimes.
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
int main() {
char name[]="/tmp/unix-name-XXXXXX";int fd=::mkstemp(name);assert(fd>=0);
assert(::write(fd,"old",3)==3);assert(::unlink(name)==0);char data[4]{};
assert(::pread(fd,data,3,0)==3);std::printf("%s %d\n",data,::access(name,F_OK)==-1);::close(fd);
}Start from an opened directory
The process cwd is global mutable context. A helper that changes it to resolve
a relative filename also changes what unrelated code means by the same name.
openat(directory_fd, component, flags) instead makes its anchor explicit.
The build walks one component at a time, retains each new directory under RAII,
and uses it as the next anchor.
Its policy rejects absolute paths, empty components, . and .., trailing
slashes and embedded NUL. These are deliberately strict API rules, not claims
that such names are universally invalid in Unix. Names containing NUL cannot
be passed as a single C pathname; otherwise a string_view can disagree with
what the syscall sees.
O_NOFOLLOW applies to every component. O_DIRECTORY requires intermediate objects to be directories. After the final open, fstat checks the selected handle instead of asking about the name before use. Opening a FIFO read-only can block, so the final open uses O_NONBLOCK before rejecting nonregular objects.
A component policy has limits
This exercise is not a full filesystem confinement mechanism. Mount topology and concurrent directory reparenting require stronger reasoning; openat2 has kernel-enforced resolution flags for policies such as staying beneath an anchor. Keep the API's actual guarantee narrower than its name suggests. The build also avoids permissions tests that depend on privileged-user behavior.
Modes, links and replacement
The creation mode is filtered by umask. A descriptor's access mode is then a separate part of its open file description. Hard links are additional names for an inode; symlinks are pathname traversal objects. Removing the last name does not delete data while live open references still need it.
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
int main() {
char dir[]="/tmp/unix-mode-XXXXXX";assert(::mkdtemp(dir));
int root=::open(dir,O_RDONLY|O_DIRECTORY);assert(root>=0);auto previous=::umask(027);
int fd=::openat(root,"value",O_CREAT|O_EXCL|O_WRONLY,0666);assert(fd>=0);::umask(previous);
struct stat info{};assert(::fstat(fd,&info)==0);std::printf("%03o\n",unsigned(info.st_mode&0777));
::close(fd);assert(::unlinkat(root,"value",0)==0);::close(root);assert(::rmdir(dir)==0);
}Replacing a name with rename is an atomic namespace operation on a suitable same-filesystem setup. That does not make it a durable storage transaction. A replacement protocol commonly writes a temporary file, fsyncs it, renames it into the directory and fsyncs the directory. Handling partial failure and crash recovery is additional work; these exercises do not simulate power loss.
Before the fill, trace opening data/value when data is a symlink, then when
only value is a symlink. Every acquired temporary descriptor must close on
either rejection. Tests also prove that a legitimate opened handle survives
unlink. Continue to processes; later,
descriptor passing lets another process use an open handle.