public inbox for libc-alpha@sourceware.org
 help / color / mirror / Atom feed
From: Adhemerval Zanella <adhemerval.zanella@linaro.org>
To: libc-alpha@sourceware.org
Cc: DJ Delorie <dj@redhat.com>
Subject: [PATCH] io: Fix silent readdir failures in ftw/nftw (BZ 33085)
Date: Thu,  2 Apr 2026 14:12:09 -0300	[thread overview]
Message-ID: <20260402171224.17813-1-adhemerval.zanella@linaro.org> (raw)

The {n}ftw functions fails to distinguish between the end of a directory
stream and an read error.  The existing implementation treated all
NULL returns as the end of the stream, silently swallowing file system
or I/O errors and incorrectly reporting a successful traversal.

This patch fixes the issue in two places within io/ftw.c:

1. In open_dir_stream (the fallback loop used when the file
   descriptor limit is exhausted and directory entries must be cached).

2. In ftw_dir (the main directory reading loop, specifically within
   FTW_STATE_STREAM_LOOP).

The testcasuse uses the FUSE libsupport to trigger getdents failure
in this two readdir calls.

Checked on x86_64-linux-gnu and i686-linux-gnu.
---
 io/Makefile          |   1 +
 io/ftw.c             |  17 +++-
 io/tst-ftw-bz33085.c | 195 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 211 insertions(+), 2 deletions(-)
 create mode 100644 io/tst-ftw-bz33085.c

diff --git a/io/Makefile b/io/Makefile
index 80e50578b2..f0a5a0fa3a 100644
--- a/io/Makefile
+++ b/io/Makefile
@@ -200,6 +200,7 @@ tests := \
   tst-fts-lfs \
   tst-ftw-bz26353 \
   tst-ftw-bz28126 \
+  tst-ftw-bz33085 \
   tst-ftw-lnk \
   tst-futimens \
   tst-futimes \
diff --git a/io/ftw.c b/io/ftw.c
index 726c430eaf..e292a45aa6 100644
--- a/io/ftw.c
+++ b/io/ftw.c
@@ -246,8 +246,16 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
 	  struct dirent64 *d;
 	  size_t actsize = 0;
 
-	  while ((d = __readdir64 (st)) != NULL)
+	  while (1)
 	    {
+	      errno = 0;
+	      d = __readdir64 (st);
+	      if (d == NULL)
+		{
+		  if (errno != 0)
+		    return -1;
+		  break;
+		}
 	      size_t this_len = NAMLEN (d);
 	      if (actsize + this_len + 2 >= bufsize)
 		{
@@ -607,6 +615,7 @@ ftw_dir (struct ftw_data *data, const struct STRUCT_STAT *st)
 	      continue;
 	    }
 
+	  errno = 0;
 	  struct dirent64 *d = __readdir64 (frame->dir.stream);
 	  if (d != NULL)
 	    {
@@ -631,7 +640,11 @@ ftw_dir (struct ftw_data *data, const struct STRUCT_STAT *st)
 		}
 	    }
 	  else
-	    frame->state = FTW_STATE_CLEANUP;
+	    {
+	      frame->state = FTW_STATE_CLEANUP;
+	      if (errno != 0)
+		result = -1;
+	    }
 	}
       else if (frame->state == FTW_STATE_CONTENT_LOOP)
 	{
diff --git a/io/tst-ftw-bz33085.c b/io/tst-ftw-bz33085.c
new file mode 100644
index 0000000000..8ca96ba2f5
--- /dev/null
+++ b/io/tst-ftw-bz33085.c
@@ -0,0 +1,195 @@
+/* Test if nftw correctly handles readdir errors.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   The GNU C Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Lesser General Public
+   License as published by the Free Software Foundation; either
+   version 2.1 of the License, or (at your option) any later version.
+
+   The GNU C Library is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public
+   License along with the GNU C Library; if not, see
+   <https://www.gnu.org/licenses/>.  */
+
+#include <ftw.h>
+#include <dirent.h>
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <sys/stat.h>
+
+#include <support/check.h>
+#include <support/fuse.h>
+#include <support/support.h>
+
+/* The test stress the readdir calls from nftw, where failures should not
+   handled as end of stream.  The first it at 'ftw_dir'
+   (FTW_STATE_STREAM_LOOP) for the default entries read.  The another one is
+   at open_dir_stream where it is triggered when there is a file description
+   exaustion and the code< must close an existing stream to make room for the
+   new subdirectory stream.  */
+
+static _Atomic bool readdir_triggered = false;
+
+static void
+fuse_thread (struct support_fuse *f, void *closure)
+{
+  struct fuse_in_header *inh;
+
+  while ((inh = support_fuse_next (f)) != NULL)
+    {
+      switch (inh->opcode)
+        {
+        case FUSE_GETATTR:
+          {
+            /* We need to respond for both the root (1) and dir1 (2) */
+            if (inh->nodeid == 1 || inh->nodeid == 2)
+              {
+                struct fuse_attr_out out = { 0 };
+                out.attr_valid = 60;
+                out.attr.ino = inh->nodeid;
+                out.attr.mode = S_IFDIR | 0755;
+                out.attr.nlink = 3; /* Force nftw to look for subdirs */
+                support_fuse_reply (f, &out, sizeof (out));
+              }
+            else
+              support_fuse_reply_error (f, ENOENT);
+            break;
+          }
+
+        case FUSE_LOOKUP:
+          {
+            const char *name = (const char *) (inh + 1);
+            if (strcmp (name, "dir1") == 0)
+              {
+                struct fuse_entry_out out = { 0 };
+                out.nodeid = 2;
+                out.attr_valid = 60;
+                out.entry_valid = 60;
+                out.attr.ino = 2;
+                out.attr.mode = S_IFDIR | 0755;
+                out.attr.nlink = 2;
+                support_fuse_reply (f, &out, sizeof (out));
+              }
+            else
+              support_fuse_reply_error (f, ENOENT);
+            break;
+          }
+
+        case FUSE_OPENDIR:
+          {
+            struct fuse_open_out out = { 0 };
+            out.fh = inh->nodeid;
+            support_fuse_reply (f, &out, sizeof (out));
+            break;
+          }
+
+        case FUSE_READDIR:
+          {
+            const struct fuse_read_in *rin = support_fuse_cast (READ, inh);
+
+            if (inh->nodeid == 1) /* Reading the Root directory */
+              {
+                if (rin->offset == 0)
+                  {
+                    /* First readdir, this happens in FTW_STATE_STREAM_LOOP.
+                       We yield "dir1", prompting nftw to descend.  */
+                    char buf[256] = { 0 };
+                    struct fuse_dirent *d = (struct fuse_dirent *) buf;
+
+                    d->ino = 2;
+                    d->off = 1;
+                    d->type = DT_DIR;
+                    d->namelen = 4;
+                    strcpy (d->name, "dir1");
+
+                    size_t d_size =
+		      FUSE_DIRENT_ALIGN (sizeof (struct fuse_dirent)
+					 + d->namelen);
+                    support_fuse_reply (f, buf, d_size);
+                  }
+                else
+                  {
+		    /* Second readdir, this ONLY happens inside
+		       open_dir_stream() when nftw tries to cache the
+		       remaining entries before closing the stream to descend
+		       into "dir1".  */
+                    readdir_triggered = true;
+                    support_fuse_reply_error (f, EIO);
+                  }
+              }
+            else
+              /* Subdirectory logic (shouldn't be reached in this test) */
+              support_fuse_reply_empty (f);
+            break;
+          }
+
+        case FUSE_READDIRPLUS:
+          support_fuse_reply_error (f, EIO);
+          break;
+
+        case FUSE_ACCESS:
+        case FUSE_RELEASEDIR:
+          support_fuse_reply_empty (f);
+          break;
+
+        default:
+          support_fuse_reply_error (f, EIO);
+        }
+    }
+}
+
+static int
+nftw_cb (const char *fpath, const struct stat *sb, int typeflag,
+	 struct FTW *ftwbuf)
+{
+  return 0;
+}
+
+static int
+do_test (void)
+{
+  support_fuse_init ();
+  struct support_fuse *f = support_fuse_mount (fuse_thread, NULL);
+
+  {
+    /* This forces nftw to immediately exhaust its FD limit when it tries
+       to descend into 'dir1', forcing it into the open_dir_stream fallback
+       loop. */
+    errno = 0;
+    readdir_triggered = false;
+    int ret = nftw (support_fuse_mountpoint (f), nftw_cb, 1, FTW_PHYS);
+    /* Assert that we successfully hit the caching __readdir64 block */
+    TEST_VERIFY (readdir_triggered);
+
+    /* Assert that nftw correctly aborted and propagated the EIO */
+    TEST_COMPARE (ret, -1);
+    TEST_COMPARE (errno, EIO);
+  }
+
+  {
+    /* Use a high descriptor count (10) so nftw doesn't fall back to
+       caching */
+    errno = 0;
+    readdir_triggered = false;
+    int ret = nftw (support_fuse_mountpoint (f), nftw_cb, 10, FTW_PHYS);
+    /* Assert that the second readdir in the main loop was actually hit */
+    TEST_VERIFY (readdir_triggered);
+
+    /* Assert that nftw correctly aborts and propagates the EIO
+       This will fail until the `#if 0` block in ftw.c is patched) */
+    TEST_COMPARE (ret, -1);
+    TEST_COMPARE (errno, EIO);
+  }
+
+  support_fuse_unmount (f);
+  return 0;
+}
+
+#include <support/test-driver.c>
-- 
2.43.0


             reply	other threads:[~2026-04-02 17:12 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-02 17:12 Adhemerval Zanella [this message]
2026-04-17 22:43 ` DJ Delorie
2026-04-20 16:40   ` Adhemerval Zanella Netto

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260402171224.17813-1-adhemerval.zanella@linaro.org \
    --to=adhemerval.zanella@linaro.org \
    --cc=dj@redhat.com \
    --cc=libc-alpha@sourceware.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).