File Descriptor / I/O · Linux userspace / kernel ABI

Directory traversal and getdents64

Explains that readdir does not provide a snapshot of directory entries and shows how to handle d_type, telldir cookies, and concurrent changes safely.

Series
15 / 38
Build
cc -std=c17 -Wall -Wextra -O2 list_dir.c -o list_dir
Run
./list_dir .
Kernel
Linux 6.18.37 LTS

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 diagram

Figure 1. Variable-length records returned by one getdents64
userspace buffer
inooffreclentypealpha\0paddinginooffreclentypelong-name\0padding
record 1
<──────── d_reclen=32 ────────>
record 2
<──────────── d_reclen=40 ────────────>

Follow each record's d_reclen to find the next entry. Adding only the d_name length fails to skip alignment padding.

Call path

Figure 2. From userspace code to observable results
opendir directory fd/stream
getdents64 record batch
readdir return one entry
fstatat metadata relative to dirfd
closedir release fd and buffer

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.

Figure 3. Major points along the kernel-internal path
iterate_dir f_pos and lock
file->iterate_shared filesystem callback
dir_emit name/inode/type
filldir64 user record
copy_to_user return batch

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.

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.

FileFunction / structureWhat 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 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.

Buildcc -std=c17 -Wall -Wextra -O2 list_dir.c -o list_dir
01#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

Source line 10DIR *dir = opendir

A 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.

Source line 13int dfd = dirfd

Calls fstatat relative to the same directory object without joining pathnames. The base object remains valid even if the directory is renamed.

Source line 15errno = 0

A NULL from readdir can mean either EOF or error, so set errno to 0 before the loop and distinguish the cases after it ends.

Source line 19AT_SYMLINK_NOFOLLOW

When an entry is a symlink, reads metadata for the symlink inode rather than its target. Do not trust d_type alone.

Source line 20errno == ENOENT

Allows the normal race in which another process deletes an entry between readdir and fstatat, then proceeds to the next item.

Detailed behavior

01

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.

02

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.

03

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 and lifetimes

ObjectCreation and releaseValues to inspect
DIR streamCreated by opendir; its internal fd and buffer are released by closedirfd, buffer, position
dirent recordMay be valid only from a readdir return until the next operation on the streamd_name, d_ino, d_type
directory fdA number borrowed from dirfd and owned by closedirDo not close separately; decide whether dup is required

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
Entries occasionally disappearConcurrent rename/unlink during traversalCheck the source of changes and snapshot requirements
File type is UNKNOWNThe filesystem does not provide d_typeConfirm with fstatat
A readdir error is treated as EOFerrno was not distinguishedCheck for the errno=0 pattern before the loop

Verify it yourself

  1. While traversing a large directory, have another process create and rename files and record the possibility of duplicates and omissions.
  2. Use strace to confirm that one getdents64 call is decomposed into several readdir results.
  3. Write a recursive walker using only openat/fstatat and expose symlink and mount-crossing policies as separate options.
Run./list_dir .
Tracestrace -e trace=openat,getdents64,newfstatat,close ./list_dir .

Primary sources