Boosting Developer Productivity: Fixing stat() with readdir() Path Resolution

In the world of C programming, especially when interacting with the file system, subtle bugs can emerge that significantly impact developer productivity. A recent GitHub Community discussion highlighted one such tricky scenario involving the stat() function and entries returned by readdir(). This insight delves into the problem, its root cause, and provides robust solutions to help you avoid this common pitfall in your own projects.

Developer looking confused at code with incorrect file sizes
Developer looking confused at code with incorrect file sizes

The Problem: Misleading File Information from stat()

A developer attempting to write a simple ls-like utility encountered a perplexing issue: when listing files in a directory other than the current working directory, stat() would consistently report the same size and type (typically 4096 bytes, 'dir') for every file, regardless of its actual properties. For example, a 12-byte text file and a 5000-byte binary file would both appear as 4096-byte directories.

The original code snippet looked something like this:

#include 
#include 
#include 

int main(int argc, char **argv) {
    const char *dir = argc > 1 ? argv[1] : ".";
    DIR *d = opendir(dir);
    struct dirent *e;
    struct stat st;

    if (!d) {
        perror(dir);
        return (1);
    }

    while ((e = readdir(d)) != NULL) {
        stat(e->d_name, &st);
        printf("%-20s %8lld %s
", e->d_name, (long long)st.st_size, S_ISDIR(st.st_mode) ? "dir" : "file");
    }
    closedir(d);
    return (0);
}

The puzzling aspect was that it worked perfectly when listing the current directory, leading to extended debugging sessions and a dip in developer productivity.

Developer confidently coding with correct file system information
Developer confidently coding with correct file system information

Root Cause: Path Resolution and Stale Data

The core of the issue lies in how stat() resolves paths and the unchecked return value. When readdir() returns a struct dirent *e, e->d_name contains only the bare filename (e.g., "a.txt"). The stat() function, however, resolves paths relative to the current working directory (CWD) by default.

Consider this sequence:

  1. You open a directory like ./demo.
  2. readdir() gives you "a.txt".
  3. You call stat("a.txt", &st).

If your CWD is not ./demo, then stat("a.txt", &st) attempts to find ./a.txt. If ./a.txt doesn't exist (which it won't if the file is truly inside ./demo/a.txt), stat() fails, returning -1 and setting errno to ENOENT. Crucially, because the original code didn't check stat()'s return value, the st struct retained stale data from the last successful stat() call – which typically would be for the . or .. entries of the current directory, hence the consistent 4096-byte directory output.

The reason it worked for the current directory is simple: if the target directory is the CWD, then stat("a.txt", &st) correctly resolves to ./a.txt, and the operation succeeds.

Solutions for Accurate File Information

Fortunately, there are clear solutions to this problem, enhancing the reliability of your software project tool and improving overall developer productivity.

1. Recommended: Use fstatat() with the Directory File Descriptor

The most robust and recommended approach is to use fstatat(). This function allows you to specify a directory file descriptor (obtained from dirfd()) against which the relative path (e->d_name) should be resolved. This method is also race-condition safe, as it operates on an open file descriptor.

#include 
#include 
#include 
#include  // Required for fstatat

int main(int argc, char **argv) {
    const char *dir = argc > 1 ? argv[1] : ".";
    DIR *d = opendir(dir);
    struct dirent *e;
    struct stat st;

    if (!d) {
        perror(dir);
        return 1;
    }

    while ((e = readdir(d)) != NULL) {
        // Check return value of fstatat for errors
        if (fstatat(dirfd(d), e->d_name, &st, 0) == -1) {
            perror(e->d_name);
            continue; // Skip to next entry on error
        }
        printf("%-20s %8lld %s
", e->d_name, (long long)st.st_size, S_ISDIR(st.st_mode) ? "dir" : "file");
    }
    closedir(d);
    return 0;
}

2. Portable Alternative: Build the Full Path

If fstatat() is not available or you prefer a more portable solution, you can manually construct the full path to each file before calling stat(). This requires careful buffer management to prevent overflows.

#include 
#include 
#include 
#include  // For snprintf

// ... (main function setup as before) ...

    char path[PATH_MAX]; // Or a sufficiently large buffer
    while ((e = readdir(d)) != NULL) {
        // Build the full path: e.g., "demo/a.txt"
        snprintf(path, sizeof(path), "%s/%s", dir, e->d_name);

        // Check return value of stat for errors
        if (stat(path, &st) == -1) {
            perror(path);
            continue; // Skip to next entry on error
        }
        printf("%-20s %8lld %s
", e->d_name, (long long)st.st_size, S_ISDIR(st.st_mode) ? "dir" : "file");
    }
// ... (rest of main function) ...

Key Lessons for Enhanced Developer Productivity

  • Always Check Return Values: System calls like stat(), fstatat(), opendir(), and snprintf() can fail. Ignoring their return values can lead to silent bugs and stale data, severely hindering your developer productivity.
  • Understand Path Resolution: Be mindful of whether functions resolve paths relative to the current working directory or a specified file descriptor. e->d_name from readdir() is always relative to the directory opened by opendir(), not necessarily your CWD.
  • -Wall -Wextra Isn't Enough: While essential, compiler flags like -Wall -Wextra -Werror catch syntactic and some logical issues, but they won't detect purely logical bugs like incorrect path resolution. Thorough testing and understanding system call semantics are crucial.

By internalizing these lessons, you can write more robust C code, reduce debugging time, and significantly boost your developer productivity when building any software project tool or utility that interacts with the file system.

|

Dashboards, alerts, and review-ready summaries built on your GitHub activity.

 Install GitHub App to Start
Dashboard with engineering activity trends