[PATCH v3] io: Refactor {n}ftw to use fts for stack safety and conformance (BZ 33882)

Adhemerval Zanella adhemerval.zanella@linaro.org
Fri Feb 13 17:00:43 GMT 2026


The current implementation of nftw uses recursive function calls to
traverse the directory tree.  This approach is susceptible to stack
overflow errors when traversing deeply nested directory structures,
which can occur in legitimate workloads or be triggered by malicious
inputs.

This patch reimplements nftw on top of fts, which uses an iterative
approach with limited stack usage.  However, fts semantics differ
slightly from glibc nftw requirements, which require some additional
handling:

  * Physical Walk (FTW_PHYS) Symlink Handling, where fts checks symlink
    targets even in physical mode (FTS_PHYS), returning FTS_SLNONE for
    broken links.  This requires explicitly mapping both FTS_SL and
    FTS_SLNONE to FTW_SL.  nftw (physical) must report the link itself,
    regardless of the target's validity.

  * Logical Walk (FTS_LOGICAL) and Cycles, where fts reports directory
    cycles as FTS_DC.  These are mapped to FTW_SLN (Symbolic Link that
    causes a cycle/cannot be followed), matching historical nftw behavior.

  * Mount Point Crossings (FTW_MOUNT), where FTS_XDEV stops fts from
    descending into mount points.  In FTW_DEPTH mode, nftw requires the
    mount point directory itself to be reported, and since fts never
    enters the directory, it never generates the FTS_DP (post-order)
    event.  This is fixed by capturing the root device ID, and if the
    directory is a mount point (different device ID), we treat the FTS_D
    event as the required visit and report it as FTW_DP.

  * Directory Changing (FTW_CHDIR), where FTS_LOGICAL implies FTS_NOCHDIR
    in fts, preventing automatic directory changes.  This is implemented
    by forcing FTS_NOCHDIR for all modes to maintain consistent fts
    behavior and then manually managing FTW_CHDIR.

  * Root Access Errors, where fts_read returns FTS_NS if the root path
    cannot be accessed, while ftw expects a hard failure (-1) for
    permission errors on the root.  This is fixed by an explicit check
    for FTS_ROOTLEVEL.

  * FTW_ACTIONRETVAL support, where it requires to map the FTW_SKIP_SUBTREE
    and FTW_SKIP_SIBLINGS to specific logic paths.

This change unifies the traversal logic and eliminates the recursion
limit for file tree walks.

Also added tests for FTW_DNR, which is current missing.

Checked on x86_64-linux-gnu and i686-linux-gnu.
--
Changes from v2:
* Return -1 in case of memory allocation failure.

Changes from v1:
* Make tst-nftw-bz33882 as UNSUPPORTED if it can not create the deep
  nested directory (some filesystem such as overlayfs does not support
  this).
---
 include/fts.h             |   50 ++
 io/Makefile               |    1 +
 io/fts.c                  |   41 +-
 io/fts64.c                |    1 +
 io/ftw.c                  | 1005 +++++++++++--------------------------
 io/ftw64-time64.c         |   10 +-
 io/ftw64.c                |   10 +-
 io/ftwtest-sh             |   13 +
 io/tst-nftw-bz33882.c     |  105 ++++
 sysdeps/wordsize-64/fts.c |   10 +-
 10 files changed, 522 insertions(+), 724 deletions(-)
 create mode 100644 io/tst-nftw-bz33882.c

diff --git a/include/fts.h b/include/fts.h
index ea36a9b9be..4a34d9357a 100644
--- a/include/fts.h
+++ b/include/fts.h
@@ -47,6 +47,56 @@ typedef struct _ftsent64_time64
 } FSTENT64_TIME64;
 
 # endif
+
+__typeof (fts_open) __fts_open;
+libc_hidden_proto (__fts_open);
+__typeof (fts64_open) __fts64_open;
+libc_hidden_proto (__fts64_open);
+__typeof (fts64_open) __fts64_open;
+libc_hidden_proto (__fts64_open);
+#if __TIMESIZE != 64
+extern FTS64_TIME64* __fts64_open_time64 (char *const*, int,
+					  int (*)(const FSTENT64_TIME64 **,
+						  const FSTENT64_TIME64 **));
+libc_hidden_proto (__fts64_open_time64)
+#endif
+
+__typeof (fts_close) __fts_close;
+libc_hidden_proto (__fts_close);
+__typeof (fts64_close) __fts64_close;
+libc_hidden_proto (__fts64_close);
+#if __TIMESIZE != 64
+extern int __fts64_close_time64 (FTS64_TIME64 *);
+libc_hidden_proto (__fts64_close_time64)
+#endif
+
+__typeof (fts_read) __fts_read;
+libc_hidden_proto (__fts_read);
+__typeof (fts64_read) __fts64_read;
+libc_hidden_proto (__fts64_read);
+#if __TIMESIZE != 64
+extern FSTENT64_TIME64* __fts64_read_time64 (FTS64_TIME64 *);
+libc_hidden_proto (__fts64_read_time64)
+#endif
+
+__typeof (fts_set) __fts_set;
+libc_hidden_proto (__fts_set);
+__typeof (fts64_set) __fts64_set;
+libc_hidden_proto (__fts64_set);
+#if __TIMESIZE != 64
+extern int __fts64_set_time64 (FTS64_TIME64 *, FSTENT64_TIME64 *, int);
+libc_hidden_proto (__fts64_set_time64)
+#endif
+
+__typeof (fts_children) __fts_children;
+libc_hidden_proto (__fts_children);
+__typeof (fts64_children) __fts64_children;
+libc_hidden_proto (__fts64_children);
+#if __TIMESIZE != 64
+extern FSTENT64_TIME64* __fts64_children_time64 (FTS64_TIME64 *, int);
+libc_hidden_proto (__fts64_children_time64)
+#endif
+
 #endif
 
 #endif /* _FTS_H  */
diff --git a/io/Makefile b/io/Makefile
index 707161e10b..80e50578b2 100644
--- a/io/Makefile
+++ b/io/Makefile
@@ -214,6 +214,7 @@ tests := \
   tst-mkdirat \
   tst-mkfifoat \
   tst-mknodat \
+  tst-nftw-bz33882 \
   tst-open-tmpfile \
   tst-openat \
   tst-posix_fallocate \
diff --git a/io/fts.c b/io/fts.c
index 27a15b1104..3288d2e0fc 100644
--- a/io/fts.c
+++ b/io/fts.c
@@ -74,11 +74,11 @@ static char sccsid[] = "@(#)fts.c	8.6 (Berkeley) 8/14/94";
 
 /* Support for the LFS API version.  */
 #ifndef FTS_OPEN
-#define FTS_OPEN fts_open
-#define FTS_CLOSE fts_close
-#define FTS_READ fts_read
-#define FTS_SET fts_set
-#define FTS_CHILDREN fts_children
+# define FTS_OPEN fts_open
+# define FTS_CLOSE fts_close
+# define FTS_READ fts_read
+# define FTS_SET fts_set
+# define FTS_CHILDREN fts_children
 # define FTSOBJ FTS
 # define FTSENTRY FTSENT
 # define INO_T ino_t
@@ -86,6 +86,20 @@ static char sccsid[] = "@(#)fts.c	8.6 (Berkeley) 8/14/94";
 # define STAT __stat
 # define LSTAT __lstat
 # define FSTAT __fstat
+# define FTS_INTERNAL_ALIAS
+#endif
+
+#ifdef FTS_INTERNAL_ALIAS
+# define _CONCAT(__x, __y)          __CONCAT (__x, __y)
+# define FTS_INTERNAL_FUNC(__name)  _CONCAT (__, __name)
+# define FTS_INTERNAL(__name)       FTS_INTERNAL_FUNC (__name)
+# define FTS_HIDDEN_DEF(__name) \
+  weak_alias (FTS_INTERNAL_FUNC (__name), __name); \
+  libc_hidden_def (FTS_INTERNAL_FUNC (__name));
+#else
+# define FTS_INTERNAL(__name)     __name
+# define FTS_HIDDEN_DEF(__name) \
+  libc_hidden_def (__name);
 #endif
 
 static FTSENTRY	*fts_alloc (FTSOBJ *, const char *, size_t);
@@ -119,8 +133,8 @@ static int      fts_safe_changedir (FTSOBJ *, FTSENTRY *, int, const char *);
 #define	BREAD		3		/* fts_read */
 
 FTSOBJ *
-FTS_OPEN (char * const *argv, int options,
-	  int (*compar) (const FTSENTRY **, const FTSENTRY **))
+FTS_INTERNAL(FTS_OPEN) (char * const *argv, int options,
+			int (*compar) (const FTSENTRY **, const FTSENTRY **))
 {
 	FTSOBJ *sp;
 	FTSENTRY *p, *root;
@@ -231,6 +245,7 @@ mem2:	free(sp->fts_path);
 mem1:	free(sp);
 	return (NULL);
 }
+FTS_HIDDEN_DEF (FTS_OPEN);
 
 static void
 fts_load (FTSOBJ *sp, FTSENTRY *p)
@@ -257,7 +272,7 @@ fts_load (FTSOBJ *sp, FTSENTRY *p)
 }
 
 int
-FTS_CLOSE (FTSOBJ *sp)
+FTS_INTERNAL (FTS_CLOSE) (FTSOBJ *sp)
 {
 	FTSENTRY *freep, *p;
 	int saved_errno;
@@ -300,6 +315,7 @@ FTS_CLOSE (FTSOBJ *sp)
 	free(sp);
 	return (0);
 }
+FTS_HIDDEN_DEF (FTS_CLOSE)
 
 /*
  * Special case of "/" at the end of the path so that slashes aren't
@@ -310,7 +326,7 @@ FTS_CLOSE (FTSOBJ *sp)
 	    ? p->fts_pathlen - 1 : p->fts_pathlen)
 
 FTSENTRY *
-FTS_READ (FTSOBJ *sp)
+FTS_INTERNAL (FTS_READ) (FTSOBJ *sp)
 {
 	FTSENTRY *p, *tmp;
 	int instr;
@@ -497,6 +513,7 @@ name:		t = sp->fts_path + NAPPEND(p->fts_parent);
 	p->fts_info = p->fts_errno ? FTS_ERR : FTS_DP;
 	return p;
 }
+FTS_HIDDEN_DEF (FTS_READ)
 
 /*
  * Fts_set takes the stream as an argument although it's not used in this
@@ -506,7 +523,7 @@ name:		t = sp->fts_path + NAPPEND(p->fts_parent);
  */
 /* ARGSUSED */
 int
-FTS_SET (FTSOBJ *sp, FTSENTRY *p, int instr)
+FTS_INTERNAL (FTS_SET) (FTSOBJ *sp, FTSENTRY *p, int instr)
 {
 	if (instr != 0 && instr != FTS_AGAIN && instr != FTS_FOLLOW &&
 	    instr != FTS_NOINSTR && instr != FTS_SKIP) {
@@ -516,9 +533,10 @@ FTS_SET (FTSOBJ *sp, FTSENTRY *p, int instr)
 	p->fts_instr = instr;
 	return (0);
 }
+FTS_HIDDEN_DEF (FTS_SET)
 
 FTSENTRY *
-FTS_CHILDREN(FTSOBJ *sp, int instr)
+FTS_INTERNAL(FTS_CHILDREN)(FTSOBJ *sp, int instr)
 {
 	FTSENTRY *p;
 	int fd;
@@ -582,6 +600,7 @@ FTS_CHILDREN(FTSOBJ *sp, int instr)
 	(void)__close(fd);
 	return (sp->fts_child);
 }
+FTS_HIDDEN_DEF (FTS_CHILDREN)
 
 static inline int
 dirent_not_directory(const struct dirent *dp)
diff --git a/io/fts64.c b/io/fts64.c
index 152910018e..8cb433aa1a 100644
--- a/io/fts64.c
+++ b/io/fts64.c
@@ -28,5 +28,6 @@
 #define STAT __stat64
 #define LSTAT __lstat64
 #define FSTAT __fstat64
+#define FTS_INTERNAL_ALIAS
 
 #include "fts.c"
diff --git a/io/ftw.c b/io/ftw.c
index d29734813d..f3a5ba32f6 100644
--- a/io/ftw.c
+++ b/io/ftw.c
@@ -16,116 +16,16 @@
    License along with the GNU C Library; if not, see
    <https://www.gnu.org/licenses/>.  */
 
-#ifdef HAVE_CONFIG_H
-# include <config.h>
-#endif
-
-#if __GNUC__
-# define alloca __builtin_alloca
-#else
-# if HAVE_ALLOCA_H
-#  include <alloca.h>
-# else
-#  ifdef _AIX
- #  pragma alloca
-#  else
-char *alloca ();
-#  endif
-# endif
-#endif
-
-#ifdef _LIBC
-# include <dirent.h>
-# define NAMLEN(dirent) _D_EXACT_NAMLEN (dirent)
-#else
-# if HAVE_DIRENT_H
-#  include <dirent.h>
-#  define NAMLEN(dirent) strlen ((dirent)->d_name)
-# else
-#  define dirent direct
-#  define NAMLEN(dirent) (dirent)->d_namlen
-#  if HAVE_SYS_NDIR_H
-#   include <sys/ndir.h>
-#  endif
-#  if HAVE_SYS_DIR_H
-#   include <sys/dir.h>
-#  endif
-#  if HAVE_NDIR_H
-#   include <ndir.h>
-#  endif
-# endif
-#endif
-
 #include <errno.h>
 #include <fcntl.h>
+#include <fts.h>
 #include <ftw.h>
-#include <limits.h>
+#include <scratch_buffer.h>
 #include <search.h>
+#include <stddef.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
-#include <not-cancel.h>
-#include <sys/param.h>
-#ifdef _LIBC
-# include <include/sys/stat.h>
-#else
-# include <sys/stat.h>
-#endif
-
-#if ! _LIBC && !HAVE_DECL_STPCPY && !defined stpcpy
-char *stpcpy ();
-#endif
-
-#if ! _LIBC && ! defined HAVE_MEMPCPY && ! defined mempcpy
-/* Be CAREFUL that there are no side effects in N.  */
-# define mempcpy(D, S, N) ((void *) ((char *) memcpy (D, S, N) + (N)))
-#endif
-
-/* #define NDEBUG 1 */
-#include <assert.h>
-
-#ifndef _LIBC
-# undef __chdir
-# define __chdir chdir
-# undef __closedir
-# define __closedir closedir
-# undef __fchdir
-# define __fchdir fchdir
-# undef __getcwd
-# define __getcwd(P, N) xgetcwd ()
-extern char *xgetcwd (void);
-# undef __mempcpy
-# define __mempcpy mempcpy
-# undef __opendir
-# define __opendir opendir
-# undef __readdir64
-# define __readdir64 readdir
-# undef __stpcpy
-# define __stpcpy stpcpy
-# undef __tdestroy
-# define __tdestroy tdestroy
-# undef __tfind
-# define __tfind tfind
-# undef __tsearch
-# define __tsearch tsearch
-# undef dirent64
-# define dirent64 dirent
-# undef MAX
-# define MAX(a, b) ((a) > (b) ? (a) : (b))
-#endif
-
-/* Arrange to make lstat calls go through the wrapper function
-   on systems with an lstat function that does not dereference symlinks
-   that are specified with a trailing slash.  */
-#if ! _LIBC && ! LSTAT_FOLLOWS_SLASHED_SYMLINK
-int rpl_lstat (const char *, struct stat *);
-# undef lstat
-# define lstat(Name, Stat_buf) rpl_lstat(Name, Stat_buf)
-#endif
-
-#ifndef __set_errno
-# define __set_errno(Val) errno = (Val)
-#endif
 
 /* Support for the LFS API version.  */
 #ifndef FTW_NAME
@@ -135,107 +35,24 @@ int rpl_lstat (const char *, struct stat *);
 # define NFTW_NEW_NAME __new_nftw
 # define INO_T ino_t
 # define STRUCT_STAT stat
-# ifdef _LIBC
-#  define LSTAT __lstat
-#  define STAT __stat
-#  define FSTATAT __fstatat
-# else
-#  define LSTAT lstat
-#  define XTAT stat
-#  define FSTATAT fstatat
-# endif
 # define FTW_FUNC_T __ftw_func_t
 # define NFTW_FUNC_T __nftw_func_t
 #endif
-
-/* We define PATH_MAX if the system does not provide a definition.
-   This does not artificially limit any operation.  PATH_MAX is simply
-   used as a guesstimate for the expected maximal path length.
-   Buffers will be enlarged if necessary.  */
-#ifndef PATH_MAX
-# define PATH_MAX 1024
+#ifndef FTS_TYPE
+# define FTS_TYPE    FTS
+# define FTSENT_TYPE FTSENT
+# define FTS_OPEN    __fts_open
+# define FTS_READ    __fts_read
+# define FTS_SET     __fts_set
+# define FTS_CLOSE   __fts_close
 #endif
 
-struct dir_data
-{
-  DIR *stream;
-  int streamfd;
-  char *content;
-};
-
 struct known_object
 {
   dev_t dev;
   INO_T ino;
 };
 
-struct ftw_data
-{
-  /* Array with pointers to open directory streams.  */
-  struct dir_data **dirstreams;
-  size_t actdir;
-  size_t maxdir;
-
-  /* Buffer containing name of currently processed object.  */
-  char *dirbuf;
-  size_t dirbufsize;
-
-  /* Passed as fourth argument to `nftw' callback.  The `base' member
-     tracks the content of the `dirbuf'.  */
-  struct FTW ftw;
-
-  /* Flags passed to `nftw' function.  0 for `ftw'.  */
-  int flags;
-
-  /* Conversion array for flag values.  It is the identity mapping for
-     `nftw' calls, otherwise it maps the values to those known by
-     `ftw'.  */
-  const int *cvt_arr;
-
-  /* Callback function.  We always use the `nftw' form.  */
-  NFTW_FUNC_T func;
-
-  /* Device of starting point.  Needed for FTW_MOUNT.  */
-  dev_t dev;
-
-  /* Data structure for keeping fingerprints of already processed
-     object.  This is needed when not using FTW_PHYS.  */
-  void *known_objects;
-};
-
-static bool
-ftw_allocate (struct ftw_data *data, size_t newsize)
-{
-  void *newp = realloc (data->dirstreams, data->maxdir
-					  * sizeof (struct dir_data *)
-					  + newsize);
-  if (newp == NULL)
-    return false;
-  data->dirstreams = newp;
-  data->dirbufsize = newsize;
-  data->dirbuf = (char *) data->dirstreams
-		 + data->maxdir * sizeof (struct dir_data *);
-  return true;
-}
-
-/* Internally we use the FTW_* constants used for `nftw'.  When invoked
-   as `ftw', map each flag to the subset of values used by `ftw'.  */
-static const int nftw_arr[] =
-{
-  FTW_F, FTW_D, FTW_DNR, FTW_NS, FTW_SL, FTW_DP, FTW_SLN
-};
-
-static const int ftw_arr[] =
-{
-  FTW_F, FTW_D, FTW_DNR, FTW_NS, FTW_F, FTW_D, FTW_NS
-};
-
-
-/* Forward declarations of local functions.  */
-static int ftw_dir (struct ftw_data *data, struct STRUCT_STAT *st,
-		    struct dir_data *old_dir);
-
-
 static int
 object_compare (const void *p1, const void *p2)
 {
@@ -250,399 +67,37 @@ object_compare (const void *p1, const void *p2)
   return (kp1->dev > kp2->dev) - (kp1->dev < kp2->dev);
 }
 
-
 static int
-add_object (struct ftw_data *data, struct STRUCT_STAT *st)
+add_object (void **known_objects, struct STRUCT_STAT *st)
 {
   struct known_object *newp = malloc (sizeof (struct known_object));
   if (newp == NULL)
     return -1;
   newp->dev = st->st_dev;
   newp->ino = st->st_ino;
-  return __tsearch (newp, &data->known_objects, object_compare) ? 0 : -1;
+  return __tsearch (newp, known_objects, object_compare) ? 0 : -1;
 }
 
 
 static inline int
-find_object (struct ftw_data *data, struct STRUCT_STAT *st)
+find_object (void **known_objects, struct STRUCT_STAT *st)
 {
   struct known_object obj;
   obj.dev = st->st_dev;
   obj.ino = st->st_ino;
-  return __tfind (&obj, &data->known_objects, object_compare) != NULL;
+  return __tfind (&obj, known_objects, object_compare) != NULL;
 }
 
-
-static inline int
-__attribute ((always_inline))
-open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
+union func_callback_t
 {
-  int result = 0;
-
-  if (data->dirstreams[data->actdir] != NULL)
-    {
-      /* Oh, oh.  We must close this stream.  Get all remaining
-	 entries and store them as a list in the `content' member of
-	 the `struct dir_data' variable.  */
-      size_t bufsize = 1024;
-      char *buf = malloc (bufsize);
-
-      if (buf == NULL)
-	result = -1;
-      else
-	{
-	  DIR *st = data->dirstreams[data->actdir]->stream;
-	  struct dirent64 *d;
-	  size_t actsize = 0;
-
-	  while ((d = __readdir64 (st)) != NULL)
-	    {
-	      size_t this_len = NAMLEN (d);
-	      if (actsize + this_len + 2 >= bufsize)
-		{
-		  char *newp;
-		  bufsize += MAX (1024, 2 * this_len);
-		  newp = (char *) realloc (buf, bufsize);
-		  if (newp == NULL)
-		    {
-		      /* No more memory.  */
-		      int save_err = errno;
-		      free (buf);
-		      __set_errno (save_err);
-		      return -1;
-		    }
-		  buf = newp;
-		}
-
-	      *((char *) __mempcpy (buf + actsize, d->d_name, this_len))
-		= '\0';
-	      actsize += this_len + 1;
-	    }
-
-	  /* Terminate the list with an additional NUL byte.  */
-	  buf[actsize++] = '\0';
-
-	  /* Shrink the buffer to what we actually need.  */
-	  void *content = realloc (buf, actsize);
-	  data->dirstreams[data->actdir]->content = content;
-	  if (content == NULL)
-	    {
-	      int save_err = errno;
-	      free (buf);
-	      __set_errno (save_err);
-	      result = -1;
-	    }
-	  else
-	    {
-	      __closedir (st);
-	      data->dirstreams[data->actdir]->stream = NULL;
-	      data->dirstreams[data->actdir]->streamfd = -1;
-	      data->dirstreams[data->actdir] = NULL;
-	    }
-	}
-    }
-
-  /* Open the new stream.  */
-  if (result == 0)
-    {
-      assert (data->dirstreams[data->actdir] == NULL);
-
-      if (dfdp != NULL && *dfdp != -1)
-	{
-	  int fd = __openat64_nocancel (*dfdp, data->dirbuf + data->ftw.base,
-					O_RDONLY | O_DIRECTORY | O_NDELAY);
-	  dirp->stream = NULL;
-	  if (fd != -1 && (dirp->stream = __fdopendir (fd)) == NULL)
-	    __close_nocancel_nostatus (fd);
-	}
-      else
-	{
-	  const char *name;
-
-	  if (data->flags & FTW_CHDIR)
-	    {
-	      name = data->dirbuf + data->ftw.base;
-	      if (name[0] == '\0')
-		name = ".";
-	    }
-	  else
-	    name = data->dirbuf;
-
-	  dirp->stream = __opendir (name);
-	}
-
-      if (dirp->stream == NULL)
-	result = -1;
-      else
-	{
-	  dirp->streamfd = __dirfd (dirp->stream);
-	  dirp->content = NULL;
-	  data->dirstreams[data->actdir] = dirp;
-
-	  if (++data->actdir == data->maxdir)
-	    data->actdir = 0;
-	}
-    }
-
-  return result;
-}
-
+  FTW_FUNC_T ftw_func;
+  NFTW_FUNC_T nftw_func;
+};
 
 static int
-process_entry (struct ftw_data *data, struct dir_data *dir, const char *name,
-	       size_t namlen, int d_type)
+ftw_startup (const char *dir, bool is_nftw, union func_callback_t func,
+	     int descriptors, int flags)
 {
-  struct STRUCT_STAT st;
-  int result = 0;
-  int flag = 0;
-  size_t new_buflen;
-
-  if (name[0] == '.' && (name[1] == '\0'
-			 || (name[1] == '.' && name[2] == '\0')))
-    /* Don't process the "." and ".." entries.  */
-    return 0;
-
-  new_buflen = data->ftw.base + namlen + 2;
-  if (data->dirbufsize < new_buflen
-      && !ftw_allocate (data, 2 * new_buflen))
-    return -1;
-
-  *((char *) __mempcpy (data->dirbuf + data->ftw.base, name, namlen)) = '\0';
-
-  int statres;
-  if (dir->streamfd != -1)
-    statres = FSTATAT (dir->streamfd, name, &st,
-		       (data->flags & FTW_PHYS) ? AT_SYMLINK_NOFOLLOW : 0);
-  else
-    {
-      if ((data->flags & FTW_CHDIR) == 0)
-	name = data->dirbuf;
-
-      statres = ((data->flags & FTW_PHYS)
-		 ? LSTAT (name, &st)
-		 : STAT (name, &st));
-    }
-
-  if (statres < 0)
-    {
-      if (errno != EACCES && errno != ENOENT)
-	result = -1;
-      else if (data->flags & FTW_PHYS)
-	flag = FTW_NS;
-      else
-	{
-	  /* Old code left ST undefined for dangling DT_LNK without
-	     FTW_PHYS set; a clarification at the POSIX level suggests
-	     it should contain information about the link (ala lstat).
-	     We do our best to fill in what data we can.  */
-	  if (dir->streamfd != -1)
-	    statres = FSTATAT (dir->streamfd, name, &st,
-			       AT_SYMLINK_NOFOLLOW);
-	  else
-	    statres = LSTAT (name, &st);
-	  if (statres == 0 && S_ISLNK (st.st_mode))
-	    flag = FTW_SLN;
-	  else
-	    flag = FTW_NS;
-	}
-    }
-  else
-    {
-      if (S_ISDIR (st.st_mode))
-	flag = FTW_D;
-      else if (S_ISLNK (st.st_mode))
-	flag = FTW_SL;
-      else
-	flag = FTW_F;
-    }
-
-  if (result == 0
-      && (flag == FTW_NS
-	  || !(data->flags & FTW_MOUNT) || st.st_dev == data->dev))
-    {
-      if (flag == FTW_D)
-	{
-	  if ((data->flags & FTW_PHYS)
-	      || (!find_object (data, &st)
-		  /* Remember the object.  */
-		  && (result = add_object (data, &st)) == 0))
-	    result = ftw_dir (data, &st, dir);
-	}
-      else
-	result = (*data->func) (data->dirbuf, &st, data->cvt_arr[flag],
-				&data->ftw);
-    }
-
-  if ((data->flags & FTW_ACTIONRETVAL) && result == FTW_SKIP_SUBTREE)
-    result = 0;
-
-  return result;
-}
-
-
-static int
-__attribute ((noinline))
-ftw_dir (struct ftw_data *data, struct STRUCT_STAT *st, struct dir_data *old_dir)
-{
-  struct dir_data dir;
-  struct dirent64 *d;
-  int previous_base = data->ftw.base;
-  int result;
-  char *startp;
-
-  /* Open the stream for this directory.  This might require that
-     another stream has to be closed.  */
-  result = open_dir_stream (old_dir == NULL ? NULL : &old_dir->streamfd,
-			    data, &dir);
-  if (result != 0)
-    {
-      if (errno == EACCES)
-	/* We cannot read the directory.  Signal this with a special flag.  */
-	result = (*data->func) (data->dirbuf, st, FTW_DNR, &data->ftw);
-
-      return result;
-    }
-
-  /* First, report the directory (if not depth-first).  */
-  if (!(data->flags & FTW_DEPTH))
-    {
-      result = (*data->func) (data->dirbuf, st, FTW_D, &data->ftw);
-      if (result != 0)
-	{
-	  int save_err;
-fail:
-	  save_err = errno;
-	  __closedir (dir.stream);
-	  dir.streamfd = -1;
-	  __set_errno (save_err);
-
-	  if (data->actdir-- == 0)
-	    data->actdir = data->maxdir - 1;
-	  data->dirstreams[data->actdir] = NULL;
-	  return result;
-	}
-    }
-
-  /* If necessary, change to this directory.  */
-  if (data->flags & FTW_CHDIR)
-    {
-      if (__fchdir (__dirfd (dir.stream)) < 0)
-	{
-	  result = -1;
-	  goto fail;
-	}
-    }
-
-  /* Next, update the `struct FTW' information.  */
-  ++data->ftw.level;
-  startp = strchr (data->dirbuf, '\0');
-  /* There always must be a directory name.  */
-  assert (startp != data->dirbuf);
-  if (startp[-1] != '/')
-    *startp++ = '/';
-  data->ftw.base = startp - data->dirbuf;
-
-  while (dir.stream != NULL && (d = __readdir64 (dir.stream)) != NULL)
-    {
-      int d_type = DT_UNKNOWN;
-#ifdef _DIRENT_HAVE_D_TYPE
-      d_type = d->d_type;
-#endif
-      result = process_entry (data, &dir, d->d_name, NAMLEN (d), d_type);
-      if (result != 0)
-	break;
-    }
-
-  if (dir.stream != NULL)
-    {
-      /* The stream is still open.  I.e., we did not need more
-	 descriptors.  Simply close the stream now.  */
-      int save_err = errno;
-
-      assert (dir.content == NULL);
-
-      __closedir (dir.stream);
-      dir.streamfd = -1;
-      __set_errno (save_err);
-
-      if (data->actdir-- == 0)
-	data->actdir = data->maxdir - 1;
-      data->dirstreams[data->actdir] = NULL;
-    }
-  else
-    {
-      int save_err;
-      char *runp = dir.content;
-
-      while (result == 0 && *runp != '\0')
-	{
-	  char *endp = strchr (runp, '\0');
-
-	  // XXX Should store the d_type values as well?!
-	  result = process_entry (data, &dir, runp, endp - runp, DT_UNKNOWN);
-
-	  runp = endp + 1;
-	}
-
-      save_err = errno;
-      free (dir.content);
-      __set_errno (save_err);
-    }
-
-  if ((data->flags & FTW_ACTIONRETVAL) && result == FTW_SKIP_SIBLINGS)
-    result = 0;
-
-  /* Prepare the return, revert the `struct FTW' information.  */
-  data->dirbuf[data->ftw.base - 1] = '\0';
-  --data->ftw.level;
-  data->ftw.base = previous_base;
-
-  /* Finally, if we process depth-first report the directory.  */
-  if (result == 0 && (data->flags & FTW_DEPTH))
-    result = (*data->func) (data->dirbuf, st, FTW_DP, &data->ftw);
-
-  if (old_dir
-      && (data->flags & FTW_CHDIR)
-      && (result == 0
-	  || ((data->flags & FTW_ACTIONRETVAL)
-	      && (result != -1 && result != FTW_STOP))))
-    {
-      /* Change back to the parent directory.  */
-      int done = 0;
-      if (old_dir->stream != NULL)
-	if (__fchdir (__dirfd (old_dir->stream)) == 0)
-	  done = 1;
-
-      if (!done)
-	{
-	  if (data->ftw.base == 1)
-	    {
-	      if (__chdir ("/") < 0)
-		result = -1;
-	    }
-	  else
-	    if (__chdir ("..") < 0)
-	      result = -1;
-	}
-    }
-
-  return result;
-}
-
-
-static int
-__attribute ((noinline))
-ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
-	     int flags)
-{
-  struct ftw_data data = { .dirstreams = NULL };
-  struct STRUCT_STAT st;
-  int result = 0;
-  int save_err;
-  int cwdfd = -1;
-  char *cwd = NULL;
-  char *cp;
-
   /* First make sure the parameters are reasonable.  */
   if (dir[0] == '\0')
     {
@@ -650,181 +105,319 @@ ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
       return -1;
     }
 
-  data.maxdir = descriptors < 1 ? 1 : descriptors;
-  data.actdir = 0;
-  /* PATH_MAX is always defined when we get here.  */
-  if (!ftw_allocate (&data, MAX (2 * strlen (dir), PATH_MAX)))
+  /* Data structure for keeping fingerprints of already processed
+     object.  This is needed when not using FTW_PHYS.  */
+  void *known_objects = NULL;
+  /* Device of starting point.  Needed for FTW_MOUNT.  */
+  dev_t root_dev = 0;
+
+  /* NB: The fts FTS_LOGICAL implies on FTS_NOCHDIR, so to to proper implement
+     FTW_CHDIR it requires manually manage the chdir / fchdir dance around
+     the user's callback.  The BUF is used to create the required path.  */
+  struct scratch_buffer buf;
+  scratch_buffer_init (&buf);
+
+  int start_fd = __open (".", O_RDONLY | O_CLOEXEC);
+  if (start_fd < -1)
     return -1;
-  memset (data.dirstreams, '\0', data.maxdir * sizeof (struct dir_data *));
-  cp = __stpcpy (data.dirbuf, dir);
-  /* Strip trailing slashes.  */
-  while (cp > data.dirbuf + 1 && cp[-1] == '/')
-    --cp;
-  *cp = '\0';
 
-  data.ftw.level = 0;
+  int fts_options = 0;
+  if (flags & FTW_PHYS)
+    fts_options |= FTS_PHYSICAL;
+  else
+    fts_options |= FTS_LOGICAL;
 
-  /* Find basename.  */
-  while (cp > data.dirbuf && cp[-1] != '/')
-    --cp;
-  data.ftw.base = cp - data.dirbuf;
+  if (flags & FTW_MOUNT)
+    fts_options |= FTS_XDEV;
 
-  data.flags = flags;
+  if (!(flags & FTW_CHDIR))
+    fts_options |= FTS_NOCHDIR;
 
-  /* This assignment might seem to be strange but it is what we want.
-     The trick is that the first three arguments to the `ftw' and
-     `nftw' callback functions are equal.  Therefore we can call in
-     every case the callback using the format of the `nftw' version
-     and get the correct result since the stack layout for a function
-     call in C allows this.  */
-  data.func = (NFTW_FUNC_T) func;
+  char *const paths[] = { (char *)dir, NULL };
 
-  /* Since we internally use the complete set of FTW_* values we need
-     to reduce the value range before calling a `ftw' callback.  */
-  data.cvt_arr = is_nftw ? nftw_arr : ftw_arr;
-
-  /* No object known so far.  */
-  data.known_objects = NULL;
-
-  /* Now go to the directory containing the initial file/directory.  */
-  if (flags & FTW_CHDIR)
+  /* NB: ingnore 'descriptors' limits since fts manages fds dynamically.  */
+  FTS_TYPE *ftsp = FTS_OPEN (paths, fts_options, NULL);
+  if (!ftsp)
     {
-      /* We have to be able to go back to the current working
-	 directory.  The best way to do this is to use a file
-	 descriptor.  */
-      cwdfd = __open (".", O_RDONLY | O_DIRECTORY);
-      if (cwdfd == -1)
-	{
-	  /* Try getting the directory name.  This can be needed if
-	     the current directory is executable but not readable.  */
-	  if (errno == EACCES)
-	    /* GNU extension ahead.  */
-	    cwd =  __getcwd (NULL, 0);
-
-	  if (cwd == NULL)
-	    goto out_fail;
-	}
-      else if (data.maxdir > 1)
-	/* Account for the file descriptor we use here.  */
-	--data.maxdir;
-
-      if (data.ftw.base > 0)
-	{
-	  /* Change to the directory the file is in.  In data.dirbuf
-	     we have a writable copy of the file name.  Just NUL
-	     terminate it for now and change the directory.  */
-	  if (data.ftw.base == 1)
-	    /* I.e., the file is in the root directory.  */
-	    result = __chdir ("/");
-	  else
-	    {
-	      char ch = data.dirbuf[data.ftw.base - 1];
-	      data.dirbuf[data.ftw.base - 1] = '\0';
-	      result = __chdir (data.dirbuf);
-	      data.dirbuf[data.ftw.base - 1] = ch;
-	    }
-	}
+      __close (start_fd);
+      return -1;
     }
 
-  /* Get stat info for start directory.  */
-  if (result == 0)
+  FTSENT_TYPE *ent = NULL;
+  int rc = 0;
+  int save_err;
+
+  bool postorder = (flags & FTW_DEPTH) != 0;
+
+  /* Used to proper support FTW_SKIP_SIBLINGS to avoid call fts_read again
+     of the next iteration.  */
+  bool skip_read = false;
+
+  while (true)
     {
-      const char *name;
+      if (!skip_read)
+	{
+	  errno = 0;
+          ent = FTS_READ (ftsp);
+        }
+      skip_read = false;
 
-      if (data.flags & FTW_CHDIR)
+      if (ent == NULL)
 	{
-	  name = data.dirbuf + data.ftw.base;
-	  if (name[0] == '\0')
-	    name = ".";
-	}
-      else
-	name = data.dirbuf;
+	  if (errno != 0)
+	    goto done;
+	  break;
+        }
 
-      if (((flags & FTW_PHYS)
-	   ? LSTAT (name, &st)
-	   : STAT (name, &st)) < 0)
+      if (ent->fts_level == FTS_ROOTLEVEL)
 	{
-	  if (!(flags & FTW_PHYS)
-	      && errno == ENOENT
-	      && LSTAT (name, &st) == 0
-	      && S_ISLNK (st.st_mode))
-	    result = (*data.func) (data.dirbuf, &st, data.cvt_arr[FTW_SLN],
-				   &data.ftw);
-	  else
-	    /* No need to call the callback since we cannot say anything
-	       about the object.  */
-	    result = -1;
-	}
-      else
-	{
-	  if (S_ISDIR (st.st_mode))
+	  /* If the STARTING path cannot be accessed, nftw must fail rather
+	     than calling the callback with FTW_NS (it is required only for
+	     FTS_ROOTLEVEL).  */
+	  if (ent->fts_info == FTS_NS && ent->fts_errno != 0)
 	    {
-	      /* Remember the device of the initial directory in case
-		 FTW_MOUNT is given.  */
-	      data.dev = st.st_dev;
+	      rc = -1;
+	      errno = ent->fts_errno;
+	      goto done;
+            }
 
-	      /* We know this directory now.  */
-	      if (!(flags & FTW_PHYS))
-		result = add_object (&data, &st);
+	  /* Remember the device of the initial directory in case FTW_MOUNT. */
+	  if (ent->fts_statp)
+	    root_dev = ent->fts_statp->st_dev;
+        }
 
-	      if (result == 0)
-		result = ftw_dir (&data, &st, NULL);
+      /* Handle FTW_MOUNT.  */
+      bool is_mount_crossing = false;
+      if ((flags & FTW_MOUNT) && ent->fts_level > FTS_ROOTLEVEL
+	  && ent->fts_statp)
+	if (ent->fts_statp->st_dev != root_dev)
+	  is_mount_crossing = true;
+
+      int fn_flag;
+      switch (ent->fts_info)
+	{
+	case FTS_D:
+	  /* Remember the device of the initial directory in case FTW_MOUNT
+	     is given.  */
+	  if (ent->fts_level == FTS_ROOTLEVEL)
+	    {
+	      if (!(flags & FTW_PHYS)
+		  && add_object (&known_objects, ent->fts_statp) == -1)
+		{
+		  rc = -1;
+		  goto done;
+		}
 	    }
 	  else
 	    {
-	      int flag = S_ISLNK (st.st_mode) ? FTW_SL : FTW_F;
-
-	      result = (*data.func) (data.dirbuf, &st, data.cvt_arr[flag],
-				     &data.ftw);
+	      if (!(flags & FTW_PHYS)
+		  && find_object (&known_objects, ent->fts_statp))
+		continue;
+	      if (add_object (&known_objects, ent->fts_statp) == -1)
+		{
+		  rc = -1;
+		  goto done;
+		}
 	    }
-	}
 
-      if ((flags & FTW_ACTIONRETVAL)
-	  && (result == FTW_SKIP_SUBTREE || result == FTW_SKIP_SIBLINGS))
-	result = 0;
+	  /* Normally we skip FTS_D in depth mode, but for a mount
+	     crossing, fts (FTS_XDEV) will NOT descend.  We will never
+	     get FTS_DP.  We must handle it manually.  */
+	  if (postorder && !is_mount_crossing)
+	    continue;
+
+	  /* fts returns FTS_D for directories it has not tried to open yet,
+	     even if it has not read permissions.  The nftw must report
+	     FTW_DNR instead.   */
+	  if (__faccessat (start_fd, ent->fts_accpath, R_OK, AT_EACCESS) == 0)
+	    fn_flag = FTW_D;
+	  else
+	    {
+	      fn_flag = FTW_DNR;
+	      FTS_SET (ftsp, ent, FTS_SKIP);
+	    }
+	  break;
+
+	case FTS_DP:
+	  if (!postorder)
+	    continue;
+	  fn_flag = FTW_DP;
+	  break;
+
+	case FTS_DEFAULT:
+        case FTS_F:
+	  fn_flag = FTW_F;
+	  break;
+
+        case FTS_SL:
+	  fn_flag = FTW_SL;
+	  break;
+
+	case FTS_SLNONE:
+	  fn_flag = FTW_SLN;
+	  break;
+
+	case FTS_DNR:
+	  fn_flag = FTW_DNR;
+	  break;
+
+	case FTS_NS:
+	  fn_flag = FTW_NS;
+	  break;
+
+        case FTS_DC:
+	  if (S_ISDIR (ent->fts_statp->st_mode))
+	    {
+	      if (!(flags & FTW_PHYS)
+		  && find_object (&known_objects, ent->fts_statp))
+		continue;
+	      if (add_object (&known_objects, ent->fts_statp) == -1)
+		{
+		  rc = 1;
+		  goto done;
+		}
+	    }
+	  /* A directory cycle was detected (Logical walk only) and
+	     instead of aborting with ELOOP, we report this as a
+	     symlink that cannot be successfully followed (FTW_SLN).  */
+          fn_flag = FTW_SLN;
+          break;
+
+        default:
+	  rc = -1;
+	  goto done;
+        }
+
+        struct FTW ftw_data = {
+	  .base = ent->fts_pathlen - ent->fts_namelen,
+	  .level = ent->fts_level
+	};
+
+        bool chdir_performed = false;
+	if (flags & FTW_CHDIR)
+	  {
+	    const char *target_dir = NULL;
+
+	    size_t dir_len = ent->fts_pathlen - ent->fts_namelen;
+	    if (dir_len == 0)
+	      /* Case: "filename" (Root level file/dir with no path prefix):
+		 the containing directory is the start directory.  */
+              target_dir = ".";
+            else
+	      {
+		/* Case: "path/to/filename": we need to extract "path/to"  */
+		if (!scratch_buffer_set_array_size (&buf, dir_len, 1))
+		  goto done;
+		memcpy (buf.data, ent->fts_accpath, dir_len);
+		char *target = buf.data;
+		if (dir_len > 1 && target[dir_len-1] == '/')
+		  target[dir_len-1] = '\0';
+		target_dir = target;
+	      }
+
+            if (__chdir (target_dir) == 0)
+              chdir_performed = true;
+            else
+	      {
+		rc = -1;
+		goto done;
+	      }
+	  }
+
+        rc = is_nftw
+	  ? func.nftw_func (ent->fts_path, ent->fts_statp, fn_flag, &ftw_data)
+	  : func.ftw_func (ent->fts_path, ent->fts_statp, fn_flag);
+
+	if (chdir_performed && __fchdir (start_fd) != 0)
+	  {
+	    rc = -1;
+	    goto done;
+	  }
+
+	if (!(flags & FTW_ACTIONRETVAL))
+	  {
+	    if (rc != 0)
+	      break;
+	    continue;
+	  }
+	/* FTW_ACTIONRETVAL support.  */
+	switch (rc)
+	  {
+	  case FTW_CONTINUE:
+	    /* Default behavior: just proceed to next entry.  */
+	    break;
+
+	  case FTW_STOP:
+	    /* Stop traversal immediately (success return code).  We set rc=0
+	       here because FTW_STOP is considered a "successful" stop,
+	       unlike a non-zero return in standard mode.  */
+	    rc = 0;
+	    goto done;
+
+	  case FTW_SKIP_SUBTREE:
+	    /* Only meaningful if we are currently visiting a directory in
+	       pre-order (FTS_D).  */
+	    if (ent->fts_info == FTS_D)
+	      FTS_SET (ftsp, ent, FTS_SKIP);
+	    break;
+
+	  case FTW_SKIP_SIBLINGS:
+	    /* We must skip everything until we emerge at a lower level
+	       (parent).  */
+	    {
+	      int current_level = ent->fts_level;
+
+	      /* Drain fts until level < current.  */
+	      while ((ent = FTS_READ (ftsp)) != NULL)
+		{
+		  if (ent->fts_level < current_level)
+		    {
+		      skip_read = true;
+		      break;
+		    }
+		}
+
+		if (ent == NULL)
+		  goto done;
+	    } break;
+
+	  default:
+	    rc = 0;
+	    goto done;
+	  }
     }
 
-  /* Return to the start directory (if necessary).  */
-  if (cwdfd != -1)
-    {
-      int save_err = errno;
-      __fchdir (cwdfd);
-      __close_nocancel_nostatus (cwdfd);
-      __set_errno (save_err);
-    }
-  else if (cwd != NULL)
-    {
-      int save_err = errno;
-      __chdir (cwd);
-      free (cwd);
-      __set_errno (save_err);
-    }
-
-  /* Free all memory.  */
- out_fail:
+done:
+  scratch_buffer_free (&buf);
+  __tdestroy (known_objects, free);
   save_err = errno;
-  __tdestroy (data.known_objects, free);
-  free (data.dirstreams);
+  FTS_CLOSE (ftsp);
+  __close (start_fd);
   __set_errno (save_err);
 
-  return result;
+  return rc;
 }
 
-
-
 /* Entry points.  */
 
 int
 FTW_NAME (const char *path, FTW_FUNC_T func, int descriptors)
 {
-  return ftw_startup (path, 0, func, descriptors, 0);
+  return ftw_startup (path,
+		      false,
+		      (union func_callback_t) { .ftw_func = func },
+		      descriptors,
+		      0);
 }
 
 #ifndef NFTW_OLD_NAME
 int
 NFTW_NAME (const char *path, NFTW_FUNC_T func, int descriptors, int flags)
 {
-  return ftw_startup (path, 1, func, descriptors, flags);
+  return ftw_startup (path, true,
+		      (union func_callback_t) { .nftw_func = func },
+		      descriptors,
+		      flags);
 }
 #else
 
@@ -841,7 +434,11 @@ NFTW_NEW_NAME (const char *path, NFTW_FUNC_T func, int descriptors, int flags)
       __set_errno (EINVAL);
       return -1;
     }
-  return ftw_startup (path, 1, func, descriptors, flags);
+  return ftw_startup (path,
+		      true,
+		      (union func_callback_t) { .nftw_func = func },
+		      descriptors,
+		      flags);
 }
 versioned_symbol (libc, NFTW_NEW_NAME, NFTW_NAME, GLIBC_2_3_3);
 
@@ -856,7 +453,11 @@ attribute_compat_text_section
 NFTW_OLD_NAME (const char *path, NFTW_FUNC_T func, int descriptors, int flags)
 {
   flags &= (FTW_PHYS | FTW_MOUNT | FTW_CHDIR | FTW_DEPTH);
-  return ftw_startup (path, 1, func, descriptors, flags);
+  return ftw_startup (path,
+		      true,
+		      (union func_callback_t) { .nftw_func = func },
+		      descriptors,
+		      flags);
 }
 
 compat_symbol (libc, NFTW_OLD_NAME, NFTW_NAME, GLIBC_2_1);
diff --git a/io/ftw64-time64.c b/io/ftw64-time64.c
index 2df871f802..517dc93d92 100644
--- a/io/ftw64-time64.c
+++ b/io/ftw64-time64.c
@@ -23,11 +23,15 @@
 # define NFTW_NAME      __nftw64_time64
 # define INO_T          ino64_t
 # define STRUCT_STAT    __stat64_t64
-# define LSTAT          __lstat64_time64
-# define STAT           __stat64_time64
-# define FSTATAT        __fstatat64_time64
 # define FTW_FUNC_T     __ftw64_time64_func_t
 # define NFTW_FUNC_T    __nftw64_time64_func_t
 
+# define FTS_TYPE       FTS64_TIME64
+# define FTSENT_TYPE    FSTENT64_TIME64
+# define FTS_OPEN       __fts64_open_time64
+# define FTS_READ       __fts64_read_time64
+# define FTS_SET        __fts64_set_time64
+# define FTS_CLOSE      __fts64_close_time64
+
 # include "ftw.c"
 #endif
diff --git a/io/ftw64.c b/io/ftw64.c
index 0d7cb30091..b61b0e24b8 100644
--- a/io/ftw64.c
+++ b/io/ftw64.c
@@ -22,10 +22,14 @@
 #define NFTW_NEW_NAME __new_nftw64
 #define INO_T ino64_t
 #define STRUCT_STAT stat64
-#define LSTAT __lstat64
-#define STAT __stat64
-#define FSTATAT __fstatat64
 #define FTW_FUNC_T __ftw64_func_t
 #define NFTW_FUNC_T __nftw64_func_t
 
+#define FTS_TYPE FTS64
+#define FTSENT_TYPE FTSENT64
+#define FTS_OPEN __fts64_open
+#define FTS_READ __fts64_read
+#define FTS_SET __fts64_set
+#define FTS_CLOSE __fts64_close
+
 #include "ftw.c"
diff --git a/io/ftwtest-sh b/io/ftwtest-sh
index 9758e18f0d..a8e5160210 100644
--- a/io/ftwtest-sh
+++ b/io/ftwtest-sh
@@ -62,6 +62,8 @@ ln -s $tmpdir/foo/lvl1/lvl2 $tmpdir/foo/lvl1/lvl2/link@2
 ln -s $tmpdir/foo/lvl1/lvl2/lvl3/lvl4 $tmpdir/foo/lvl1/link@1
 echo > $tmpdir/bar/xo
 chmod a-x,a+r $tmpdir/bar
+mkdir $tmpdir/ndir
+chmod a-r $tmpdir/ndir
 
 testout=$(mktemp $tmp/ftwtest-tmp-XXXXXX.out)
 
@@ -73,6 +75,7 @@ base = "$tmp/", file = "$ftwtest", flag = FTW_D, level = 0
 base = "$tmp/$ftwtest/", file = "bar", flag = FTW_D, level = 1
 base = "$tmp/$ftwtest/", file = "baz", flag = FTW_F, level = 1
 base = "$tmp/$ftwtest/", file = "foo", flag = FTW_D, level = 1
+base = "$tmp/$ftwtest/", file = "ndir", flag = FTW_DNR, level = 1
 base = "$tmp/$ftwtest/bar/", file = "xo", flag = FTW_NS, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1", flag = FTW_D, level = 2
 base = "$tmp/$ftwtest/foo/lvl1/", file = "file@1", flag = FTW_F, level = 3
@@ -92,6 +95,7 @@ base = "$tmp/", file = "$ftwtest", flag = FTW_DP, level = 0
 base = "$tmp/$ftwtest/", file = "bar", flag = FTW_DP, level = 1
 base = "$tmp/$ftwtest/", file = "baz", flag = FTW_F, level = 1
 base = "$tmp/$ftwtest/", file = "foo", flag = FTW_DP, level = 1
+base = "$tmp/$ftwtest/", file = "ndir", flag = FTW_DNR, level = 1
 base = "$tmp/$ftwtest/bar/", file = "xo", flag = FTW_NS, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1", flag = FTW_DP, level = 2
 base = "$tmp/$ftwtest/foo/lvl1/", file = "file@1", flag = FTW_F, level = 3
@@ -111,6 +115,7 @@ base = "$tmp/", file = "$ftwtest", flag = FTW_D, level = 0
 base = "$tmp/$ftwtest/", file = "bar", flag = FTW_D, level = 1
 base = "$tmp/$ftwtest/", file = "baz", flag = FTW_F, level = 1
 base = "$tmp/$ftwtest/", file = "foo", flag = FTW_D, level = 1
+base = "$tmp/$ftwtest/", file = "ndir", flag = FTW_DNR, level = 1
 base = "$tmp/$ftwtest/bar/", file = "xo", flag = FTW_NS, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1", flag = FTW_D, level = 2
 base = "$tmp/$ftwtest/foo/lvl1/", file = "file@1", flag = FTW_F, level = 3
@@ -125,7 +130,9 @@ EOF
 rm $testout
 
 # For the next test everything must be readable.
+chmod a+r $tmpdir/ndir
 chmod -fR a+x $tmpdir
+chmod a-r $tmpdir/ndir
 
 $testprogram --chdir $tmpdir |
     sort > $testout
@@ -138,6 +145,7 @@ base = "$tmp/", file = "$ftwtest", flag = FTW_D, cwd = $tmpreal, level = 0
 base = "$tmp/$ftwtest/", file = "bar", flag = FTW_D, cwd = $tmpreal/$ftwtest, level = 1
 base = "$tmp/$ftwtest/", file = "baz", flag = FTW_F, cwd = $tmpreal/$ftwtest, level = 1
 base = "$tmp/$ftwtest/", file = "foo", flag = FTW_D, cwd = $tmpreal/$ftwtest, level = 1
+base = "$tmp/$ftwtest/", file = "ndir", flag = FTW_DNR, cwd = $tmpreal/$ftwtest, level = 1
 base = "$tmp/$ftwtest/bar/", file = "xo", flag = FTW_F, cwd = $tmpreal/$ftwtest/bar, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1", flag = FTW_D, cwd = $tmpreal/$ftwtest/foo, level = 2
 base = "$tmp/$ftwtest/foo/lvl1/", file = "file@1", flag = FTW_F, cwd = $tmpreal/$ftwtest/foo/lvl1, level = 3
@@ -160,6 +168,7 @@ base = "", file = "$ftwtest", flag = FTW_D, cwd = $tmpreal, level = 0
 base = "$ftwtest/", file = "bar", flag = FTW_D, cwd = $tmpreal/$ftwtest, level = 1
 base = "$ftwtest/", file = "baz", flag = FTW_F, cwd = $tmpreal/$ftwtest, level = 1
 base = "$ftwtest/", file = "foo", flag = FTW_D, cwd = $tmpreal/$ftwtest, level = 1
+base = "$ftwtest/", file = "ndir", flag = FTW_DNR, cwd = $tmpreal/$ftwtest, level = 1
 base = "$ftwtest/bar/", file = "xo", flag = FTW_F, cwd = $tmpreal/$ftwtest/bar, level = 2
 base = "$ftwtest/foo/", file = "lvl1", flag = FTW_D, cwd = $tmpreal/$ftwtest/foo, level = 2
 base = "$ftwtest/foo/lvl1/", file = "file@1", flag = FTW_F, cwd = $tmpreal/$ftwtest/foo/lvl1, level = 3
@@ -182,6 +191,7 @@ base = "$ftwtest/", file = ".", flag = FTW_D, cwd = $tmpreal/$ftwtest, level = 0
 base = "$ftwtest/./", file = "bar", flag = FTW_D, cwd = $tmpreal/$ftwtest, level = 1
 base = "$ftwtest/./", file = "baz", flag = FTW_F, cwd = $tmpreal/$ftwtest, level = 1
 base = "$ftwtest/./", file = "foo", flag = FTW_D, cwd = $tmpreal/$ftwtest, level = 1
+base = "$ftwtest/./", file = "ndir", flag = FTW_DNR, cwd = $tmpreal/$ftwtest, level = 1
 base = "$ftwtest/./bar/", file = "xo", flag = FTW_F, cwd = $tmpreal/$ftwtest/bar, level = 2
 base = "$ftwtest/./foo/", file = "lvl1", flag = FTW_D, cwd = $tmpreal/$ftwtest/foo, level = 2
 base = "$ftwtest/./foo/lvl1/", file = "file@1", flag = FTW_F, cwd = $tmpreal/$ftwtest/foo/lvl1, level = 3
@@ -226,6 +236,7 @@ base = "$tmp/", file = "$ftwtest", flag = FTW_D, level = 0
 base = "$tmp/$ftwtest/", file = "bar", flag = FTW_D, level = 1
 base = "$tmp/$ftwtest/", file = "baz", flag = FTW_F, level = 1
 base = "$tmp/$ftwtest/", file = "foo", flag = FTW_D, level = 1
+base = "$tmp/$ftwtest/", file = "ndir", flag = FTW_DNR, level = 1
 base = "$tmp/$ftwtest/bar/", file = "xo", flag = FTW_F, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1", flag = FTW_D, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1b", flag = FTW_D, level = 2
@@ -250,6 +261,7 @@ base = "$tmp/", file = "$ftwtest", flag = FTW_D, level = 0
 base = "$tmp/$ftwtest/", file = "bar", flag = FTW_D, level = 1
 base = "$tmp/$ftwtest/", file = "baz", flag = FTW_F, level = 1
 base = "$tmp/$ftwtest/", file = "foo", flag = FTW_D, level = 1
+base = "$tmp/$ftwtest/", file = "ndir", flag = FTW_DNR, level = 1
 base = "$tmp/$ftwtest/bar/", file = "xo", flag = FTW_F, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1", flag = FTW_D, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1b", flag = FTW_D, level = 2
@@ -275,6 +287,7 @@ base = "$tmp/", file = "$ftwtest", flag = FTW_D, level = 0
 base = "$tmp/$ftwtest/", file = "bar", flag = FTW_D, level = 1
 base = "$tmp/$ftwtest/", file = "baz", flag = FTW_F, level = 1
 base = "$tmp/$ftwtest/", file = "foo", flag = FTW_D, level = 1
+base = "$tmp/$ftwtest/", file = "ndir", flag = FTW_DNR, level = 1
 base = "$tmp/$ftwtest/bar/", file = "xo", flag = FTW_F, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1", flag = FTW_D, level = 2
 base = "$tmp/$ftwtest/foo/", file = "lvl1b", flag = FTW_D, level = 2
diff --git a/io/tst-nftw-bz33882.c b/io/tst-nftw-bz33882.c
new file mode 100644
index 0000000000..5764558ff7
--- /dev/null
+++ b/io/tst-nftw-bz33882.c
@@ -0,0 +1,105 @@
+/* Check if nested directory level does not overflow the stack (BZ #33882)
+   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 <errno.h>
+#include <ftw.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <support/check.h>
+#include <support/support.h>
+#include <support/temp_file.h>
+#include <support/xunistd.h>
+#include <sys/resource.h>
+
+/* Typical stack frame for a recursive function is 64–256 bytes, with a nested
+   depth of 5000 would required around 640Kb of stack space.  */
+enum { nested_depth = 5000 };
+enum { stack_limit_kb = 512 };
+
+/* Short name to maximize depth/path ratio.  */
+static const char dir_name[] = "d";
+
+static void
+do_cleanup (void)
+{
+  xchdir ("..");
+  for (int i = 0; i < nested_depth; i++)
+    {
+      remove (dir_name);
+      xchdir ("..");
+    }
+  remove (dir_name);
+}
+#define CLEANUP_HANDLER do_cleanup
+
+static void
+check_mkdir (const char *path)
+{
+  int r = mkdir (path, 0700);
+  /* Some filesystem such as overlayfs does not support larger path required
+     to trigger the internal buffer reallocation.  */
+  if (r != 0)
+    {
+      if (errno == ENAMETOOLONG)
+	FAIL_UNSUPPORTED ("the filesystem does not support the required"
+			  "large path");
+      else
+	FAIL_EXIT1 ("mkdir (\"%s\", 0%o): %m", path, 0700);
+    }
+}
+
+static int
+my_func (const char *file, const struct stat *sb, int flag, struct FTW *ftwbuf)
+{
+  return 0;
+}
+
+/* Set the RLIMIT_AS limit to the value in *LIMIT.  */
+static void
+xsetrlimit_stack (const struct rlimit *limit)
+{
+  if (setrlimit (RLIMIT_STACK, limit) != 0)
+    FAIL_EXIT1 ("setrlimit (RLIMIT_STACK, %lu): %m",
+                (unsigned long) limit->rlim_cur);
+}
+
+static int
+do_test (void)
+{
+  xsetrlimit_stack (&(struct rlimit) { .rlim_cur = stack_limit_kb * 1024,
+				       .rlim_max = stack_limit_kb * 1024 });
+
+  char *tempdir = support_create_temp_directory ("tst-bz33882");
+
+  xchdir (tempdir);
+  for (int i = 0; i < nested_depth; i++)
+    {
+      check_mkdir (dir_name);
+      xchdir (dir_name);
+    }
+
+  TEST_COMPARE (nftw (tempdir, my_func, 20, 0), 0);
+
+  free (tempdir);
+
+  do_cleanup ();
+
+  return 0;
+}
+
+#include <support/test-driver.c>
diff --git a/sysdeps/wordsize-64/fts.c b/sysdeps/wordsize-64/fts.c
index 159dc1febe..b10d81714a 100644
--- a/sysdeps/wordsize-64/fts.c
+++ b/sysdeps/wordsize-64/fts.c
@@ -12,8 +12,8 @@
 #undef fts64_set
 #undef fts64_children
 
-weak_alias (fts_open, fts64_open)
-weak_alias (fts_close, fts64_close)
-weak_alias (fts_read, fts64_read)
-weak_alias (fts_set, fts64_set)
-weak_alias (fts_children, fts64_children)
+weak_alias (__fts_open, fts64_open)
+weak_alias (__fts_close, fts64_close)
+weak_alias (__fts_read, fts64_read)
+weak_alias (__fts_set, fts64_set)
+weak_alias (__fts_children, fts64_children)
-- 
2.43.0



More information about the Libc-alpha mailing list