QUESTION
Do a name and its metadata returned by readdir form a consistent point-in-time snapshot?
getdents64 fills a buffer with multiple linux_dirent64 records starting at the directory file position. glibc readdir presents those entries to userspace one at a time. Each call advances the filesystem directory iterator, but does not lock out creation, deletion, or rename by another process.
d_type is convenience information and may be DT_UNKNOWN. Code that requires security or accuracy should use a directory fd with fstatat/openat rather than reassembling an absolute path from the name string.
STRUCTURE
Structure diagram
Follow each record's d_reclen to find the next entry. Adding only the d_name length fails to skip alignment padding.
CALL PATH
Call path
A directory stream is not an immutable snapshot of names. At the point each entry is processed, resolve the required object again with a dirfd-relative syscall.
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/readdir.c | iterate_dir(), getdents64(), filldir64() | Directory callback and userspace-record construction |
| fs/namei.c | vfs_statx(), filename_lookup() | Revalidate an entry name against the actual path and metadata |
| include/uapi/linux/dirent.h | linux_dirent64 | record length, type, offset ABI |
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 list_dir.c -o list_dir01#define _DEFAULT_SOURCE
02#include <dirent.h>
03#include <errno.h>
04#include <stdio.h>
05#include <sys/stat.h>
06
07int main(int argc, char **argv)
08{
09 const char *path = argc > 1 ? argv[1] : ".";
10 DIR *dir = opendir(path);
11 if (dir == NULL)
12 return 1;
13 int dfd = dirfd(dir);
14
15 errno = 0;
16 struct dirent *entry;
17 while ((entry = readdir(dir)) != NULL) {
18 struct stat st;
19 if (fstatat(dfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) < 0) {
20 if (errno == ENOENT)
21 continue;
22 perror("fstatat");
23 break;
24 }
25 printf("%-24s inode=%llu mode=%o\n", entry->d_name,
26 (unsigned long long)st.st_ino, st.st_mode & 07777);
27 }
28 if (entry == NULL && errno != 0)
29 perror("readdir");
30 return closedir(dir) != 0;
31}
CODE NOTES
Code notes
DIR *dir = opendirA DIR is a libc object that owns not only an fd but also a getdents buffer and current position. Avoid traversing the same stream concurrently from multiple threads.
int dfd = dirfdCalls fstatat relative to the same directory object without joining pathnames. The base object remains valid even if the directory is renamed.
errno = 0A NULL from readdir can mean either EOF or error, so set errno to 0 before the loop and distinguish the cases after it ends.
AT_SYMLINK_NOFOLLOWWhen an entry is a symlink, reads metadata for the symlink inode rather than its target. Do not trust d_type alone.
errno == ENOENTAllows the normal race in which another process deletes an entry between readdir and fstatat, then proceeds to the next item.
DETAILS
Detailed behavior
Record lengths are variable
Follow linux_dirent64.d_reclen to find the next record. A raw parser that advances by sizeof(struct dirent) is wrong because names and alignment change the length.
Using glibc readdir avoids direct ABI parsing, but its returned pointer may be overwritten by the next readdir call.
Results can be duplicated or omitted while a directory changes
A filesystem uses iterator cookies and hash/tree positions. A rename or insertion during traversal may make an old entry appear again or a new one be missed; there is no portable snapshot guarantee.
An exact inventory requires one of a higher-level protocol that prevents changes, a filesystem snapshot, or repeated validation.
A recursive walk must manage fd count and symlink policy
Open each child directory with openat and put it on a stack/queue to traverse without changing cwd. Keeping too many directory fds open at once can hit RLIMIT_NOFILE, so define a depth-first close policy.
Make mount-crossing, symlink-following, and bind-mount-cycle policies explicit using device/inode pairs and openat2 resolve flags.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
DIR stream | Created by opendir; its internal fd and buffer are released by closedir | fd, buffer, position |
dirent record | May be valid only from a readdir return until the next operation on the stream | d_name, d_ino, d_type |
directory fd | A number borrowed from dirfd and owned by closedir | Do not close separately; decide whether dup is required |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| Entries occasionally disappear | Concurrent rename/unlink during traversal | Check the source of changes and snapshot requirements |
| File type is UNKNOWN | The filesystem does not provide d_type | Confirm with fstatat |
| A readdir error is treated as EOF | errno was not distinguished | Check for the errno=0 pattern before the loop |
LAB
Verify it yourself
- While traversing a large directory, have another process create and rename files and record the possibility of duplicates and omissions.
- Use strace to confirm that one getdents64 call is decomposed into several readdir results.
- Write a recursive walker using only openat/fstatat and expose symlink and mount-crossing policies as separate options.
./list_dir .strace -e trace=openat,getdents64,newfstatat,close ./list_dir .PRIMARY REFERENCES