QUESTION
Do two fds for the same file always have separate offsets?
An fd is an index in a per-process table, while an open file description is the kernel's struct file. Two calls to open() normally create two struct files, but fds duplicated by dup() or fork() refer to the same struct file and share its offset and file status flags.
Code that inspects a pathname and then opens it again allows a directory entry to change between the two operations. openat2 applies a dirfd and a resolution policy in a single lookup, blocking symlinks, mount crossings, and root escapes inside the kernel path walk.
STRUCTURE
Structure diagram
files_struct
- fd 0 → tty file
- fd 3 → file A
- fd 7 → file A
struct file A
- f_pos=4096
- f_flags=O_RDONLY
- f_path
struct path
- vfsmount
- dentry: config
- parent dentry
inode
- mode/uid/size
- address_space
- file_operations
When fd 3 and fd 7 refer to the same struct file, they share the file offset and status flags. FD_CLOEXEC belongs separately to each fd slot.
CALL PATH
Call path
Distinguish the lookup stage that obtains a path object from the stage that publishes the open file object in the fd table. On failure, both the reserved fd slot and temporary path references must be rolled back.
This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.
SOURCE COORDINATES
Linux 6.18.37 LTS source locations
Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.
| File | Function / structure | What to inspect |
|---|---|---|
| fs/open.c | SYSCALL_DEFINE4(openat2), do_sys_openat2() | Validate open_how and install the fd |
| fs/namei.c | path_openat(), link_path_walk() | Resolve components relative to dirfd with resolve restrictions |
| fs/file.c | get_unused_fd_flags(), fd_install() | Reserve an fd slot and publish struct file |
COMPLETE PROGRAM
Complete runnable example
The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.
cc -std=c17 -Wall -Wextra -O2 openat2_root.c -o openat2_root01#define _GNU_SOURCE
02#include <fcntl.h>
03#include <linux/openat2.h>
04#include <stdio.h>
05#include <sys/syscall.h>
06#include <unistd.h>
07
08int main(int argc, char **argv)
09{
10 if (argc != 3)
11 return 2;
12 int root = open(argv[1], O_PATH | O_DIRECTORY | O_CLOEXEC);
13 if (root < 0)
14 return 1;
15
16 struct open_how how = {
17 .flags = O_RDONLY | O_CLOEXEC,
18 .resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS
19 };
20 int fd = (int)syscall(SYS_openat2, root, argv[2], &how, sizeof(how));
21 if (fd < 0) {
22 perror("openat2");
23 close(root);
24 return 1;
25 }
26
27 char byte;
28 ssize_t n = read(fd, &byte, 1);
29 if (n == 1)
30 printf("first byte: 0x%02x\n", (unsigned char)byte);
31 close(fd);
32 close(root);
33 return n < 0;
34}
CODE NOTES
Code notes
O_PATH | O_DIRECTORYOpens the directory not to read it, but as a stable base handle for pathname resolution. O_DIRECTORY rejects a non-directory.
struct open_how howPasses the structure size with the syscall to distinguish future extensions. Unused fields must be 0.
RESOLVE_BENEATHRejects lookups that escape below the dirfd through .., an absolute symlink, a mount, or similar mechanisms. This is stronger than a simple string-prefix check.
SYS_openat2Passes the root fd and relative path through one kernel path walk, removing the pathname-replacement window between inspection and open.
close(root)After obtaining the target fd, the root fd is still an independent reference. Close each separately when its ownership ends.
DETAILS
Detailed behavior
Distinguish fd flags from open-description flags
FD_CLOEXEC belongs to an fd slot and is managed with fcntl(F_GETFD/F_SETFD). Status flags such as O_APPEND and O_NONBLOCK belong to struct file and are shared by duplicated fds.
Results from separate open() calls have independent file offsets and status flags even when they refer to the same inode.
A dirfd eliminates working-directory races
Code based on chdir changes the process-wide current working directory and therefore affects relative resolution in other threads. The openat family passes the base directory as an argument to each operation.
Even after the directory is renamed, an open dirfd continues to refer to that directory object and is therefore more stable than a pathname string.
Put validation and use in the same lookup
If code checks for a symlink with lstat and then calls open, an attacker can replace the entry between the calls. O_NOFOLLOW restricts only the final component and does not solve policy for intermediate symlinks.
openat2 resolve flags apply policy while kernel namei walks every component.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
files_struct | An fd table that can be shared by a process/thread group and is released at exit | fd array, close_on_exec |
struct file | Created by open/accept and released when its last fd/reference closes | f_pos, f_flags, f_path |
struct path | A mount-and-dentry reference pair retained during lookup | mnt, dentry, refcount |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| EXDEV | A RESOLVE_BENEATH/IN_ROOT policy violation | Inspect path components and mount/symlink traversal |
| EMFILE/ENFILE | The per-process or system-wide open-file limit | ulimit -n, file-nr |
| The offsets of duplicated fds move together | They share the same open file description | Inspect kcmp KCMP_FILE or /proc fdinfo |
LAB
Verify it yourself
- Duplicate the same fd, alternate reads through both numbers, and inspect /proc/self/fdinfo to confirm that the offset is shared.
- Create a symlink below the root that points outside through .. and verify that openat2 rejects it with EXDEV.
- Compare two separate open() calls with two dup() results and inspect sharing of f_pos and the O_NONBLOCK flag.
./openat2_root . README.mdstrace -e trace=openat,openat2,read,close ./openat2_root . README.mdPRIMARY REFERENCES