CodeRigor

FREE LESSON

Directories, inodes & relative path operations

Separate names, inodes and open handles; walk directory descriptors with openat, reject symlinks, understand unlink/rename, permissions and the limits of userspace path policies.

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.

Names and handles reach the same inode
Unlink removes the name-to-inode edge. An open descriptor keeps its open-file reference and can still read the contents.
Names and handles reach the same inode
Unlink removes the name-to-inode edge. An open descriptor keeps its open-file reference and can still read the contents.
C++
#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.

Walk relative to opened directories
Each component becomes the anchor for the next openat. O_NOFOLLOW applies to every component; fstat checks the final opened object.
Walk relative to opened directories
Each component becomes the anchor for the next openat. O_NOFOLLOW applies to every component; fstat checks the final opened object.

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.

C++
#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.

Visibility and durability are separate steps
A common replacement protocol writes and fsyncs a temporary file, renames within the directory, then fsyncs the directory. This lesson does not claim to test power-loss behavior.
Visibility and durability are separate steps
A common replacement protocol writes and fsyncs a temporary file, renames within the directory, then fsyncs the directory. This lesson does not claim to test power-loss behavior.

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.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Use the file-descriptor-as-capability model to motivate directory-relative operations. The build uses Linux openat with an explicit component policy; it is not a Capsicum or general confinement implementation.