A backend service tries to open:
The storage device does not contain a built-in object with that full name. The operating system must interpret the pathname one component at a time:
At each step, the current object must be a directory, the process must be allowed to search it, and the next name must identify an entry. Symbolic links and mount points can redirect the walk before the final object is reached.
This process is path resolution.
A pathname is a route through a hierarchy of directories, not a permanent identity stored inside a file.
Understanding the walk explains absolute and relative paths, current working directories, . and .., “not found” errors, symbolic-link surprises, and a large class of security bugs.
A regular file presents application bytes. A directory has a different role: it organizes the file-system namespace.
Conceptually, a directory maps names to file-system objects:
Applications do not edit a directory as though it were an ordinary text file. The file system controls its representation so operations such as creation, removal, and rename preserve namespace rules.
Each mapping is called a directory entry. The entry stores one component name and enough information for the file system to continue to the referenced object. The full pathname is not one giant key stored in a global table.
For example, the path:
is assembled from entries in several directories:
This recursive structure creates the familiar directory tree.
One file-system object can sometimes be reached through more than one directory entry. A name is therefore an association inside a directory, not the object's universal identity.
Unix-like systems present one hierarchical namespace whose root is written /.
A pathname beginning with / is absolute. Resolution starts at the root directory visible to the process:
The first slash denotes the starting point and separates later components. It is not the name of an ordinary directory entry.
Different processes can have different views of the file-system namespace because of isolation and mounting arrangements. “Start at /” means the root of the calling process's current namespace view, not necessarily one universal hardware location.
At the root, resolving .. remains at the root:
Both resolve as root under normal Unix path semantics. The walk cannot escape above its namespace root by adding more .. components.
A pathname that does not begin with / is relative:
Most pathname-based operations resolve a relative path from the process's current working directory.
Suppose the current working directory is:
Then:
starts at /srv/catalog and can reach:
The current working directory is process state, not text automatically prepended by the shell. The kernel retains a reference to the directory object. That reference can remain meaningful even if the directory is renamed while the process is inside it.
chdir() changes the current working directory:
getcwd() asks for a pathname representing the current location:
The working directory is inherited across fork() and normally preserved across exec(). Ordinary threads in one process share it, so a chdir() in one thread affects relative path resolution in the others. Long-running multithreaded services commonly avoid changing the working directory after startup for this reason.
Not every relative operation has to use the current working directory. Directory-relative interfaces can begin from an already opened directory, giving the application an explicit and stable base.
A pathname consists of components separated by /.
Within one component, / cannot appear because it is the separator. A null byte cannot appear in a Unix pathname because C-compatible system-call interfaces use it to terminate the string.
Other byte values may be allowed even when they are awkward to display. Spaces, tabs, and newlines can be part of filenames. Shell scripts must quote pathname variables rather than assuming names contain only letters and digits:
The -- tells many command-line tools that later arguments are operands, so a filename beginning with - is not interpreted as an option.
Two components have special path-resolution meaning:
For a process in /srv/catalog/config:
Names beginning with . are conventionally hidden by many listing tools, but .env is otherwise an ordinary component name. It is not the same as the special component ..
Repeated slashes are generally treated as separators:
normally walks the same component sequence as:
Whether component matching is case-sensitive depends on the file system and how it is configured. Linux file systems are commonly case-sensitive, so Config and config can be different names, but applications should not mistake that common behavior for a universal pathname rule.
The exact kernel implementation is optimized and file-system-specific, but the logical algorithm is stable.
For a path such as:
the resolver conceptually performs these steps:
/ selects the process's root; otherwise use the current working directory or an explicit directory descriptor.The final step depends on the system call. open() expects to open an object, mkdir() expects the final name not to exist, and unlink() expects an entry that can be removed. The same component walk supports different final operations.
This is why an error at the final name differs from an error in the middle. Creating:
can succeed when new.json is absent, but only if /, srv, and catalog already resolve as searchable directories.
Loading simulation...
Path-related errors are more useful when interpreted through the component walk.
ENOENT commonly means a required component does not exist. It can refer to the final name or to an earlier directory:
ENOTDIR means a component that must be traversed is not a directory:
EACCES can mean the process lacks search access on an intermediate directory, even if the final file's own mode would otherwise allow the requested operation.
ELOOP commonly indicates too many symbolic-link expansions, often because links form a cycle.
ENAMETOOLONG indicates that a component or pathname exceeds an enforced limit.
The important diagnostic habit is to ask:
At which component did the resolver stop, and what did it require from that component?
A message saying “file not found” does not prove that only the last name is missing.
Directory access has two distinct ideas.
Reading a directory permits enumerating its entry names. Searching a directory permits using a known name while resolving a path through it. On Unix, search access is represented through the directory's execute permission.
This creates combinations that initially seem surprising.
A process with search access but no read access might open:
if it already knows the component name and has suitable access to the final file, while being unable to list every name in /srv/private.
Conversely, seeing a name in a directory listing does not by itself grant access through the full path. Every directory traversed from the starting point must allow search.
For:
the process needs search access through /, /srv, /srv/catalog, and /srv/catalog/config. The final operation then applies its own access requirements to production.json.
Directory search is therefore part of path resolution, not just a property checked after the final object has been found.
A symbolic link is a distinct file-system object whose target is a pathname. When resolution follows the link, the resolver substitutes that target and continues.
Suppose the namespace contains:
Resolving:
follows the relative target from the directory containing the link:
The result is:
A relative symbolic-link target is interpreted relative to the link's containing directory, not relative to the calling process's current working directory.
If the target begins with /, resolution restarts from the process's root:
The remaining config.json component is then resolved under /opt/catalog/v4.
Links can point to other links, so the kernel limits the number of expansions. A cycle such as:
eventually fails with ELOOP rather than consuming unbounded kernel work.
Most pathname operations follow a symbolic link in the final component, but some operations can inspect or act on the link object itself. Intermediate links are still relevant unless the chosen interface explicitly restricts them.
A trailing slash requires the resolved final object to behave as a directory:
Symbolic-link behavior makes purely textual path simplification unsafe. If current is a link, removing current/.. from a string before resolution can produce a different destination from letting the kernel walk it.
. and .. During Path WalksIt is common to normalize:
into:
That transformation is valid when config resolves as an ordinary directory in the expected hierarchy. It is not a universal string rule.
Suppose instead:
During real resolution:
can enter the link target and then move to the parent of that resolved directory. Simply deleting the textual config/.. pair would keep the walk under /srv/catalog, potentially selecting a different object.
The same concern applies to security checks that reject a path only after applying home-grown string cleanup. Namespace resolution depends on object types and links encountered during the walk, not just on the characters in the input.
The kernel's resolver should be the authority for filesystem semantics. Applications should use descriptor-relative and constrained-resolution interfaces when the destination must remain within a trusted tree.
Unix does not normally require applications to address storage as unrelated device namespaces. A file system can be attached at a directory called a mount point.
Suppose a separate volume is mounted at:
Resolution begins in the original root file system:
When the walk reaches data, it crosses into the root of the mounted file system and continues there:
In /srv/catalog/data/orders/2026.db, the trailing orders/2026.db portion is stored in the mounted file system.
The transition is mostly transparent to the application. One pathname can cross file-system boundaries while remaining part of a single hierarchy.
Some operations cannot cross that boundary as one atomic namespace change. A direct rename between two mounted file systems, for example, normally fails with EXDEV because each file system manages its own directory entries and objects.
A mount point further demonstrates that a pathname describes a route through the current namespace. It is not a physical storage address.
mkdir() creates a new directory at a final name:
The parent path must already resolve, and the process must have appropriate access to add an entry there.
rmdir() removes an empty directory:
A nonempty directory cannot normally be removed with rmdir(). This protects its remaining namespace entries from becoming unreachable as an accidental side effect.
unlink() removes a non-directory name, while rename() changes a name or moves an entry between directories within the same file system. Existing open descriptors continue to refer to their already opened objects.
These operations update namespace relationships:
The path resolver walks all parent components before applying the requested change to the final directory entry.
POSIX applications commonly enumerate a directory with opendir(), readdir(), and closedir().
Each successful readdir() returns a directory entry containing a component name. It does not return a complete absolute pathname.
Code can skip . and .. when it does not want to process those special entries.
Directory order should not be treated as alphabetical or stable. Tools such as ls commonly sort names in user space for presentation. The order stored or returned by a file system is an implementation detail and can change as entries are added or removed.
Enumeration is also not necessarily a transactionally consistent snapshot of a directory that other processes are modifying. An entry returned by readdir() can be renamed or removed before the caller performs another operation on it.
Some systems expose a type hint in the directory entry, but that hint can be unknown or stale by the time it is used. Querying the referenced object through an appropriate metadata operation is the reliable way to obtain current type information.
Consider this sequence:
The check and open can refer to different objects. This is a time-of-check to time-of-use race, commonly shortened to TOCTOU.
Code such as:
does not guarantee that the opened object is the one that was checked.
When possible, open first and inspect the resulting descriptor:
Open the pathname, receive a stable descriptor, then inspect and use that opened object.
This binds later work to the opened object rather than repeating a name lookup.
For operations beneath a trusted directory, the *at family accepts a directory descriptor and a relative pathname:
The lookup starts from directory_fd, not from the process's mutable current working directory. If the directory is renamed, the descriptor still refers to that opened directory object.
AT_FDCWD can be supplied to request normal current-working-directory behavior, but an explicit directory descriptor is more stable in concurrent code.
Directory-relative lookup alone is not a complete sandbox. An absolute path can ignore the directory descriptor, .. can move upward, and symbolic links can redirect traversal. Code accepting hostile pathnames must use an operating-system-supported constrained walk or carefully open and validate components according to its threat model.
Suppose a service opens its data directory once:
It can then operate relative to that stable handle:
Related interfaces include mkdirat(), unlinkat(), renameat(), and fstatat().
This design has several advantages:
The remaining relative path still has to be resolved. Intermediate symbolic links, .., permissions, and mount crossings retain their normal meaning unless the specific interface and flags constrain them.
A directory descriptor is therefore a stable starting capability, not automatic proof that every supplied child path stays underneath it.
readdir() and fstatat()The following Linux program lists one directory. It obtains entry names with readdir() and queries each entry relative to the already opened directory with fstatat().
AT_SYMLINK_NOFOLLOW asks for metadata about a final symbolic link itself rather than its target.
Compile and run:
The order and contents vary by system. A simplified result might look like:
The example deliberately uses the directory descriptor instead of constructing:
That avoids string-length and separator mistakes and anchors each metadata lookup to the directory already opened by opendir().
The directory can still change during enumeration. Treating ENOENT as “this entry disappeared before inspection” is one reasonable policy for a listing tool. Applications with stronger consistency requirements need stronger coordination.
The namei utility from the util-linux package displays pathname components and symbolic-link expansion.
Create a small namespace:
Inspect the path:
The output shows each directory component, the current symbolic link, its target, and the final regular file. Exact permissions and owners depend on the machine.
Compare absolute and relative lookup in a subshell:
Both commands begin from the same working directory and reach the sample file through different component sequences.
Now ask the kernel for the canonical path of the currently existing target:
The result normally ends in:
readlink -f is useful for observation, but the returned string is not a permanent object handle. Another process can rename or replace components immediately afterward. Code that needs stable access should open the object and retain its descriptor.
A directory is a file-system-managed mapping from component names to objects. Combining those mappings produces a hierarchical namespace rooted at /. Absolute paths start at the process's root, while relative paths normally start at its current working directory or an explicit directory descriptor.
Path resolution walks one component at a time. Each intermediate object must be a searchable directory. The resolver handles . and .., follows symbolic links according to their targets, crosses mount points, and finally applies the requested operation.
Directory reading enumerates component names but does not provide a stable, sorted snapshot. opendir() and readdir() list entries, while descriptor-relative interfaces such as openat() and fstatat() anchor later operations to an already opened directory.
Pathnames are routes through a mutable namespace, not stable object handles. Checking and then reopening a path can race with rename or replacement. Once an application opens the intended object, a descriptor provides the stable reference.
The central mental model is:
Choose a starting directory, walk each component under kernel rules, then operate on the final resolved object or name.
5 quizzes