How to Fix: Directory read bug.
Directory reads break when a directory file descriptor is treated like a regular file stream, causing readdir, offset handling, or repeated scans to return inconsistent results.
This issue usually appears in low-level runtime, libc-compatibility, sandbox, or virtual filesystem layers where directory iteration is implemented manually. The failing C test case strongly suggests a bug in how directory entries are read, buffered, or rewound after opening a directory with file-descriptor-based APIs.
Table of Contents
Problem Overview
The issue title, Directory read bug, points to a failure in directory iteration logic. In POSIX systems, directories are not read the same way as normal files. While both are represented by file descriptors at the kernel level, user-space directory traversal normally goes through opendir, fdopendir, and readdir, which maintain internal state and buffering rules.
If an implementation mixes raw read() behavior, incorrect offset tracking, or invalid buffer parsing for directory entries, a test can fail in several ways:
- readdir() returns missing entries
- the same entry appears more than once
- end-of-directory is reached too early
- errno is set incorrectly
- reopening or rewinding the directory produces corrupted results
The included headers in the test case, especially dirent.h, fcntl.h, unistd.h, errno.h, and string.h, indicate the bug likely involves direct file descriptor usage combined with directory iteration validation.
Understanding the Root Cause
The root cause is usually one of these implementation mistakes:
1. Treating directories like regular files
A directory descriptor is not meant to be consumed with ordinary file semantics in portable code. On many systems, directory entry records are returned in a kernel-specific binary format. readdir() depends on a proper DIR structure, internal buffering, and record alignment. If your runtime reads bytes directly and assumes fixed-size records, parsing becomes unreliable.
2. Broken offset management
Directory iteration depends on a logical position within the directory stream. If the implementation mishandles seekdir, telldir, rewinddir, or internal offsets, then subsequent calls can skip entries or reread old ones. A common bug is updating the offset before validating the current record length.
3. Invalid handling of variable-length dirent records
On many platforms, directory entries are variable length. The parser must advance by d_reclen, not by sizeof(struct dirent). Using the structure size instead of the recorded entry length causes misalignment and data corruption.
4. Incorrect end-of-stream behavior
At end-of-directory, readdir() returns NULL and does not automatically indicate an error. The caller must set errno = 0 before calling readdir() if it wants to distinguish end-of-stream from failure. If your implementation sets or preserves errno incorrectly, tests that validate POSIX behavior will fail.
5. Mixing raw file descriptor operations with DIR stream state
If code calls read(), lseek(), or other descriptor-level operations after wrapping the descriptor with fdopendir(), the stream state can become inconsistent. Once a descriptor is owned by a DIR*, directory traversal should be performed through directory APIs only.
Step-by-Step Solution
The safest fix is to ensure the directory is handled through a proper directory stream abstraction and that entry parsing respects platform rules.
Step 1: Open the directory correctly
If you already have a directory file descriptor, convert it with fdopendir() instead of reading from it manually.
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(void) {
int fd = open(".", O_RDONLY | O_DIRECTORY);
if (fd < 0) {
perror("open");
return 1;
}
DIR *dir = fdopendir(fd);
if (!dir) {
perror("fdopendir");
close(fd);
return 1;
}
struct dirent *entry;
errno = 0;
while ((entry = readdir(dir)) != NULL) {
printf("%s\n", entry->d_name);
errno = 0;
}
if (errno != 0) {
perror("readdir");
closedir(dir);
return 1;
}
closedir(dir);
return 0;
}
Why this works: it delegates buffering, record parsing, and offset tracking to the directory stream implementation instead of incorrectly reusing plain file I/O semantics.
Step 2: If implementing readdir internally, parse records using reclen
If you are building a libc, runtime, or syscall translation layer, never advance through a directory buffer with sizeof(struct dirent). Use the record length supplied by the underlying directory entry format.
size_t offset = 0;
while (offset < bytes_read) {
struct dirent_like *rec = (struct dirent_like *)(buffer + offset);
if (rec->d_reclen == 0) {
errno = EIO;
return NULL;
}
process_entry(rec);
offset += rec->d_reclen;
}
Key fix: validate that d_reclen is non-zero, aligned, and does not move past the buffer boundary.
Step 3: Preserve correct end-of-directory semantics
Your implementation should differentiate between a clean end-of-stream and a real error.
errno = 0;
struct dirent *ent = readdir(dir);
if (ent == NULL) {
if (errno == 0) {
/* End of directory */
} else {
/* Actual error */
perror("readdir");
}
}
If your custom implementation returns NULL while leaving a stale non-zero errno, tests may incorrectly interpret normal completion as failure.
Step 4: Do not mix stream and descriptor positioning
After calling fdopendir(fd), avoid direct lseek(fd, …) or read(fd, …) calls. If you need restart behavior, use directory APIs:
rewinddir(dir);
errno = 0;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
puts(entry->d_name);
errno = 0;
}
This keeps the internal directory stream state consistent.
Step 5: Filter special entries intentionally
Some tests fail because implementations unexpectedly include or exclude . and ... If your logic compares directory contents, decide explicitly whether to keep them.
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
printf("real entry: %s\n", entry->d_name);
}
Step 6: Validate against the failing test pattern
For a test case like the reported one, check these assertions:
- opening a directory with open(…, O_DIRECTORY) succeeds
- fdopendir() attaches correctly to the descriptor
- readdir() returns all expected entries
- no duplicate entries appear unless the directory truly contains duplicates by name in separate contexts
- errno remains correct on end-of-directory
- closedir() properly releases the descriptor
Common Edge Cases
Large directories
If the directory contains many entries, partial buffer reads become more likely. A broken implementation may parse an incomplete trailing record. Always refill the buffer safely and only parse complete records.
Concurrent directory mutation
If files are added or removed while iterating, POSIX does not guarantee a perfectly stable listing. Your code should not crash or enter an infinite loop when directory contents change mid-scan.
Non-standard filesystems
Virtual filesystems, network filesystems, and sandboxed environments may expose directory iteration quirks. A custom compatibility layer must avoid hardcoding assumptions about entry layout beyond the platform contract.
Incorrect ownership of the file descriptor
After fdopendir(), the descriptor is managed by the directory stream. Calling close(fd) manually before closedir() can trigger undefined behavior or later read failures.
Failure to reset errno before validation
Many developers misread readdir() results because errno still contains an older error. Always clear it before the call if you need to inspect end-of-directory versus error behavior.
Assuming deterministic ordering
Directory entry order is not guaranteed. If the failing test compares output in a fixed order, sort the results in the test harness rather than relying on filesystem enumeration order.
FAQ
Why does reading a directory with read() cause problems?
Because directories expose implementation-specific record formats. readdir() exists to interpret those records safely, manage buffering, and advance positions correctly.
Should I use opendir() or fdopendir() for this bug?
Use opendir() when you start from a path. Use fdopendir() when you already have a valid directory file descriptor. Both are preferable to manual raw reads for normal iteration.
Why does readdir() return NULL even when nothing is wrong?
NULL means either end-of-directory or an error. Set errno = 0 before calling it. If it returns NULL and errno is still zero, iteration completed normally.
Conclusion
To fix a directory read bug, stop treating directory descriptors like regular files, use fdopendir/readdir correctly, honor variable-length dirent records, and preserve POSIX errno semantics. In most implementations, the real defect is not the directory itself but broken state management between low-level descriptor access and high-level directory stream traversal.