Handles vs. Names: File Descriptors, Paths, and Royal Cartouches
In software engineering, two fundamentally different ways exist to reference a resource:
- By Name (hierarchical, human-readable, symbolic, and mutable).
- By Handle (opaque, indexical, capability-based, and immutable).
In UNIX systems, this distinction manifests in the difference between a File Path (/var/log/app.log) and a File Descriptor (int fd = 3).
Confusing the two is the root cause of subtle security vulnerabilities, file corruption, and race conditions. To understand why systems must decouple handles from names, we can look back at ancient civilizations and royal authority.
The Historical Analogy: Pharaoh Cartouches vs. The Royal Signet Ring
In Ancient Egypt, rulers possessed two very different expressions of identity:
Hierarchical & Mutable:
[ Public Cartouche / Monument Name ] โโโ Subject to chiseling, renaming, and forgery
(Damnatio Memoriae)
Direct & Immutable:
[ The Royal Signet Ring / Seal ] โโโ Direct operational capability; validates decrees
regardless of name changes
1. The Cartouche (The Symbolic Name)
A Pharaoh had five official royal names. The most prominent (the Nomen and Prenomen) were enclosed inside an oval loop of rope called a cartouche and carved onto obelisks, temple pylons, and monuments.
However, public names were inherently vulnerable. When a rival Pharaoh took powerโsuch as Thutmose III after Hatshepsut, or the restoration after Akhenatenโstonecutters were dispatched to chisel the predecessor's cartouche off the walls (damnatio memoriae). The physical stone remained, but the public name was erased or rewritten.
2. The Royal Signet Ring (The Operational Handle)
When royal officials issued grain, mobilized troops, or decreed taxes, they did not rely on public carvings on temple walls. They pressed the Pharaoh's signet ring into wet clay or beeswax.
The seal was an operational capability token. If you held the signet ring, your seal had binding authority. The ring did not depend on which cartouche was carved on the palace gate; it was a direct, unforgeable handle to imperial executive power.
The Kernel Reality: Paths vs. File Descriptors
The Linux kernel reflects this exact dualism through its Virtual File System (VFS).
User Space: Path: "/var/log/app.log" File Descriptor: 3
โ โ
โผ (Path Resolution) โผ (Direct Index)
Kernel Space: Directory Entries (dentry) Process fd Table (files_struct)
โ โ
โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
โผ โผ
Open File Description (struct file)
โ
โผ
Inode (struct inode)
โ
โผ
Disk Blocks / Memory Data
1. The Path: A Symbolic Coordinate
A path like /var/log/app.log is merely a string. It is not the file itself; it is a route map through a naming tree.
Every time you access a file by its path:
- The kernel must parse the path component by component (
/,var,log,app.log). - It looks up directory entries (dentries) in memory or on disk.
- It validates read/execute permissions at every single directory hop.
- It must resolve any symbolic links encountered along the way.
Because paths are symbolic, they are inherently unstable:
- Another process can rename the directory halfway through your lookup.
- Another process can delete the target file and replace it with a symlink to
/etc/shadow.
2. The File Descriptor: An Immutable Handle
When you call int fd = open("/var/log/app.log", O_RDWR);, the operating system translates the cartouche (the path) into a signet ring (the handle):
- The kernel assigns an integer (e.g.,
3) representing an index in your process's privatestruct files_structtable. - That slot points to an Open File Description (
struct file), which in turn pins the target inode (struct inode) and its backing disk blocks.
Once you hold the file descriptor, the path no longer matters:
- If another process renames
/var/log/app.logto/var/log/app.log.old, yourwrite(fd, ...)continues writing to the exact same bytes. - Even if another process deletes the file via
unlink("/var/log/app.log"), the name disappears from directory listings, but the kernel refuses to free the disk blocks. Because your file descriptor maintains an active reference count on the inode, you can continue reading and writing to the deleted file. The data is only freed when the last descriptor is closed.
Why This Matters: The TOCTOU Attack
Failing to understand the difference between handles and paths creates one of the most common security flaws in computing: Time-of-Check to Time-of-Use (TOCTOU) race conditions.
Consider this flawed C snippet running as root:
// VULNERABLE CODE: Uses symbolic paths twice
if (access("/tmp/user_report.txt", W_OK) == 0) {
// โโโ RACE WINDOW: Attacker swaps file for a symlink to /etc/passwd here!
int fd = open("/tmp/user_report.txt", O_WRONLY);
write(fd, "malicious data", 14);
}
Between the time access() checks the path and open() opens the path, the file system state can change. The check verified the cartouche at time T0, but the write acted on a different resource at time T1.
The Solution: Operate on Handles
Modern secure systems programming avoids path-based operations whenever possible, binding to a descriptor immediately and performing all subsequent operations on the handle:
// SECURE CODE: Open once with O_NOFOLLOW, operate strictly on the handle
int fd = open("/tmp/user_report.txt", O_WRONLY | O_NOFOLLOW);
if (fd >= 0) {
struct stat st;
fstat(fd, &st); // Inspect the handle, not the path!
if (st.st_uid == expected_uid) {
write(fd, "safe data", 9);
}
close(fd);
}
Modern Linux takes this further with directory-descriptor system calls like openat2() and the RESOLVE_BENEATH flag, forcing path resolution to remain securely pinned within an authorized directory handle.
Summary Comparison
| Attribute | File Path | File Descriptor |
|---|---|---|
| Historical Analogy | Public Cartouche on a Monument | Imperial Signet Ring |
| Nature | Symbolic coordinate string | Opaque integer token |
| Scope | Global hierarchical namespace | Private to the process table |
| Resolution Cost | High (dentry walk, permissions, symlinks) | O(1) array index lookup |
| Stability | Mutable (subject to renames, unlinks, symlinks) | Immutable (pinned reference to inode) |
| Vulnerability | Susceptible to TOCTOU symlink races | Immune to path-swapping races |
| System Calls | open, stat, chmod, unlink |
read, write, fstat, fchmod, openat |