Linux syscalls - open / openat
In POSIX-compliant systems, the open family of system calls is responsible for converting pathnames into file descriptors (fd). Over decades of Unix evolution, these calls evolved from simple path lookup mechanisms into robust, directory-relative APIs designed to eliminate race conditions and simplify thread safety.
Primary Entry Points: open() vs openat()
open() / open2()
The classic Unix system call to open or create a file using an absolute or process-relative path.
int open(const char *pathname, int flags, ... /* mode_t mode */);
- Behavior: Resolves
pathnamestarting from the current working directory (CWD) if relative, or system root (/) if absolute. - Limitations: Inherently prone to path-traversal race conditions in multi-threaded programs or secure systems, because the current working directory is process-global.
openat()
Introduced in POSIX.1-2008 to solve CWD races and streamline directory traversal.
int openat(int dirfd, const char *pathname, int flags, ... /* mode_t mode */);
- Relative Resolution: If
pathnameis relative, it is resolved relative to the directory referenced bydirfdrather than the process's current working directory. - Special Descriptor (
AT_FDCWD): Ifdirfdis set toAT_FDCWD,openat()behaves identically toopen(), resolving relative to the process's CWD. - Absolute Path Fallback: If
pathnameis an absolute path (starts with/),dirfdis ignored.
Why openat() Solves Race Conditions & Security Issues?
- Thread Safety: In multi-threaded applications, changing the working directory via
chdir()affects all threads in the process. Usingopenat()with directory descriptors allows individual threads to safely traverse separate folder hierarchies concurrently without changing global state. - Path Traversal Protection: By pinning a directory descriptor (
dirfd), applications can open files within nested subdirectories relative to that pinned handle, preventing malicious actors on shared filesystems from swapping parent directories via symlink attacks during path resolution.
Complementary Open-Related POSIX System Calls
Beyond standard path opening, POSIX provides specialized calls to manage anonymous files, non-path file descriptors, and directory streams.
creat(): Equivalent toopen(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode). Historic shortcut from early Unix whenopen()flags were limited. Largely redundant today.openat2()(Linux-specific extension): Extendsopenat()with a extensible struct parameter (struct open_how) and flags likeRESOLVE_BNEATHorRESOLVE_NO_SYMLINKSto strictly restrict path resolution within target mount/directory trees.memfd_create()(Linux/FreeBSD extension): Creates an anonymous, unlinked file in RAM/tmpfs and returns an openfd. Widely used for sandboxing, inter-process communication (IPC), and shared memory without filesystem artifacts.opendir()/fdopendir(): Opens directory streams (DIR *) for reading directory entries (readdir).fdopendir()converts an existingdirfdinto aDIR *stream.
Essential Flags Driving open / openat Behavior
The behavior of open calls is heavily driven by bitwise-ORed flags passed to flags:
Access Modes (Mutually Exclusive - Pick One)
O_RDONLY: Open for read-only.O_WRONLY: Open for write-only.O_RDWR: Open for reading and writing.
Operational & Safety Flags
O_CREAT: Create the file if it does not exist (requires themodeparameter).O_EXCL: Used withO_CREAT. Fails if the file already exists. Ensures atomic file creation (prevents race conditions when creating lock files).O_TRUNC: If the file exists and is opened for writing, truncate its length to 0 bytes.O_APPEND: Forces all write operations to seek to the end of the file prior to each write (atomic append across processes).O_CLOEXEC: Sets the Close-on-Exec flag automatically upon opening. Prevents the file descriptor from leaking into child processes duringexecve(). (Essential for multi-threaded thread safety).O_NOFOLLOW: Fails the open if the tail component ofpathnameis a symbolic link.O_DIRECTORY: Fails the open ifpathnameis not a directory. Essential when getting a directory descriptor for subsequentopenat()calls.O_NONBLOCK/O_NDELAY: Opens the file in non-blocking mode (critical for FIFOs, sockets, and character devices).
What does "open" mean?
The direct output and primary job of open() (and openat()) is to return a file descriptor (fd).
For example, when a VMM calls open("/dev/kvm"), it opens a file descriptor to the Linux KVM kernel module. This file descriptor acts as the fundamental api entry point into the CPU's hardware virtualization capabilities (Intel VT-x or AMD-V). By itself, opening /dev/kvm just checks if KVM is available and gives the VMM a handle to issue commands. Once that file descriptor is open, the VMM uses a sequence of ioctl() system calls to build a virtual machine from scratch.
However, calling open() triggers several critical kernel-side state changes and operations beyond just assigning an integer handle.
Key Secondary Effects of open()
While getting an fd is the end result, invoking open() allows you to request specific kernel actions:
1. File Creation & Modification
- Atomic File Creation: Passing
O_CREATcreates a new inode on disk if the file doesn't already exist. Combining it withO_EXCLguarantees atomic creation (failing if it already exists), which is used for creating lock files and avoiding race conditions. - Truncation: Passing
O_TRUNCimmediately shrinks an existing file's size to 0 bytes upon opening, discarding its contents.
2. Access Rights & Permission Validation
open()is the primary gatekeeper for filesystem permissions. The kernel checks your process's UID/GID against the file's mode bits (rwx) or ACLs. Once anfdis granted with a specific mode (e.g.,O_RDONLY), subsequent reads or writes using thatfdskip permission re-checks.
3. Creating an Open File Description (Kernel State)
When you call open(), the kernel creates an internal Open File Description (an struct file in Linux), which tracks state shared across operations:
- File Offset / Position: The initial seek position (usually byte 0, or the end if
O_APPENDis set). - Access Flags: Storing flags like
O_NONBLOCKorO_APPENDthat govern how futureread()andwrite()calls behave.
4. Hardware and Driver Side Effects
Opening a file path in Unix isn't limited to regular files on disk:
- Character/Block Devices (
/dev/*): Callingopen()on a device driver (e.g.,/dev/tty,/dev/nvme0n1,/dev/gpu) triggers the driver's custom.open()hook. This can initialize hardware, allocate device buffers, or lock access to a device. - FIFOs / Named Pipes: Calling
open()on a FIFO blocks the calling thread until another process opens the other end (read vs. write synchronization).
5. Obtaining Non-I/O Metadata Handles (O_PATH)
By passing O_PATH on Linux, openat() returns an fd that explicitly cannot be used for reading or writing file data. Its sole purpose here is to act as a lightweight, pin-point handle for metadata operations (like statx, fchdir, or fchmodat) without performing file permission checks or loading file contents.
What happens if you open a file multiple times?
When you open the same file multiple times—whether in the same process or across different processes—the Linux kernel creates a new Open File Description (struct file) for every successful call to open() or openat().
Each call returns a distinct integer file descriptor (fd), but all of these descriptors ultimately point to the same underlying inode in memory.
The Three-Tier Kernel Structure
To understand what happens, it helps to look at the three layers the kernel uses to track opened files:
Process File Descriptor Table
[fd 3] ────────┐
[fd 4] ──────┐ │
│ │
System-Wide Open File Descriptions (Kernel Memory)
[struct file A] (offset: 0, flags: O_RDONLY) ──┐
[struct file B] (offset: 128, flags: O_WRONLY) ──┼──> Inode Table (Disk Object)
│ [struct inode]
[struct file C] (offset: 0, flags: O_WRONLY) ──┘ (File metadata, disk blocks)
- File Descriptor (
fd): Per-process integer index (e.g.,3,4) pointing to an open file description. - Open File Description (
struct file): System-wide kernel object created on everyopen()call. Stores state unique to that open instance, such as:- Current file position / offset (
f_pos) - Access mode and status flags (
O_RDONLY,O_APPEND,O_NONBLOCK)
- Current file position / offset (
- Inode (
struct inode): System-wide single kernel object representing the actual file on disk (metadata, size, permissions, data blocks).
Key Behaviors of Multiple Opens
1. Independent File Offsets
Because each open() creates a separate struct file, each descriptor maintains its own read/write offset:
- If Process A opens
file.txt(fd 3) and reads 100 bytes, its offset advances to 100. - If Process B (or Process A calling
open()a second time to getfd 4) reads from its descriptor, it starts reading at byte 0, unaffected by the other descriptor's position.
2. Simultaneous Writes and Concurrent Data Overwrites
If two descriptors open the file for writing without synchronization:
- They will overwrite each other's data because both start writing at offset 0 by default.
- The
O_APPENDException: If both open calls specifyO_APPEND, the kernel guarantees that everywrite()atomically seeks to the current end of the file (at the inode level) before writing, preventing data from being overwritten.
3. Independent Access Modes and Status Flags
Each open() call can request different permissions and operational flags:
- One
open()can requestO_RDONLY, while another requestsO_WRONLY. - Changing file flags using
fcntl(fd, F_SETFL, ...)onfd 3will not affectfd 4.
4. Shared Inode Operations
Because all descriptors point to the same underlying inode:
- Modifications are immediately visible: If
fd 3writes data,fd 4will read that newly written data immediately (subject to OS page cache consistency). - Traces and Attributes are Shared: Calling
ftruncate(fd3, 0)truncates the actual file on disk, instantly affecting whatfd4sees. - File Locking (
flockvsfcntllocks): BSD file locks (flock) operate on the open file description, whereas POSIX record locks (fcntl) operate on the(process, inode)pair.
How This Differs from dup() or fork()
It is important to distinguish opening a file multiple times from duplicating an existing descriptor:
| Mechanism | System Calls | File Descriptors | Open File Description (struct file) |
File Offset (f_pos) |
|---|---|---|---|---|
| Multiple Opens | open() + open() |
Distinct (3 & 4) |
Separate objects created | Independent |
| Descriptor Duplication | dup(fd) / dup2() |
Distinct (3 & 4) |
Shared same object | Shared (moving one moves the other) |
| Process Forking | fork() |
Same fd in parent & child |
Shared same object | Shared across processes |