Re: [PATCH v2] io: Fix silent readdir failures in ftw/nftw (BZ 33085)

Adhemerval Zanella Netto <[email protected]> Thu, 30 Jul 2026 14:58:42 -0300
Newsgroups gmane.comp.lib.glibc.alpha
Organization Linaro
Message-ID <[email protected]>
Ping.

On 20/04/26 14:43, Adhemerval Zanella wrote:
> 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.
> --
> Changes from v1:
> - Add extra free on open_dir_stream early exit.
> - Fixed typos.
> ---
>  io/Makefile          |   1 +
>  io/ftw-common.c      |  20 ++++-
>  io/tst-ftw-bz33085.c | 195 +++++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 214 insertions(+), 2 deletions(-)
>  create mode 100644 io/tst-ftw-bz33085.c
> 
> diff --git a/io/Makefile b/io/Makefile
> index 2a5b4dcb65..151dd36418 100644
> --- a/io/Makefile
> +++ b/io/Makefile
> @@ -202,6 +202,7 @@ tests := \
>    tst-fts-newflags \
>    tst-ftw-bz26353 \
>    tst-ftw-bz28126 \
> +  tst-ftw-bz33085 \
>    tst-ftw-lnk \
>    tst-futimens \
>    tst-futimes \
> diff --git a/io/ftw-common.c b/io/ftw-common.c
> index 07df0ab25a..17349a284d 100644
> --- a/io/ftw-common.c
> +++ b/io/ftw-common.c
> @@ -246,8 +246,19 @@ 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)
> +		    {
> +		      free (buf);
> +		      return -1;
> +		    }
> +		  break;
> +		}
>  	      size_t this_len = NAMLEN (d);
>  	      if (actsize + this_len + 2 >= bufsize)
>  		{
> @@ -607,6 +618,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 +643,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..c318b5a9f9
> --- /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 stresses the readdir calls from nftw, where failures should not
> +   handled as end of stream.  The first is 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>