[glibc] io: ftw: Use state stack instead of recursion (BZ 33882)

Adhemerval Zanella via Glibc-cvs <[email protected]>
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=418581126ac40ca18e381c990e0a2768aec21c51

commit 418581126ac40ca18e381c990e0a2768aec21c51
Author: Adhemerval Zanella <[email protected]>
Date:   Tue Feb 24 10:59:54 2026 -0300

    io: ftw: Use state stack instead of recursion (BZ 33882)
    
    The current implementation of ftw relies on recursion to traverse
    directories (ftw_dir calls process_entry, which calls ftw_dir).  In deep
    directory trees, this could lead to a stack overflow (as demonstrated by
    the new tst-nftw-bz33882.c test).
    
    This patch refactors ftw to use an explicit, heap-allocated stack to
    manage directory traversal:
    
      * The 'struct ftw_frame' encapsulates the state of a single directory
        level (directory stream, stat buffer, previous base offset, and
        current state).
    
      * The ftw_dir is rewritten to use a loop instead of recursion and
        an iterative loop to enable immediate state transitions without
        function call overhead.
    
    The patch also cleans up some unused definitions and assumptions (e.g.,
    free-clobbering errno) and fixes a UB when handling the ftw callback.
    
    Checked on x86_64-linux-gnu and i686-linux-gnu.
    Reviewed-by: DJ Delorie <[email protected]>

Diff:
---
 io/Makefile           |   1 +
 io/ftw.c              | 633 +++++++++++++++++++++++++++++---------------------
 io/tst-nftw-bz33882.c | 105 +++++++++
 3 files changed, 476 insertions(+), 263 deletions(-)

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/ftw.c b/io/ftw.c
index d29734813d..726c430eaf 100644
--- a/io/ftw.c
+++ b/io/ftw.c
@@ -16,116 +16,17 @@
    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 <assert.h>
+#include <dirent.h>
 #include <fcntl.h>
 #include <ftw.h>
-#include <limits.h>
+#include <not-cancel.h>
 #include <search.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
+#define NAMLEN(dirent) _D_EXACT_NAMLEN (dirent)
 
 /* Support for the LFS API version.  */
 #ifndef FTW_NAME
@@ -135,15 +36,9 @@ 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 LSTAT __lstat
+# define STAT __stat
+# define FSTATAT __fstatat
 # define FTW_FUNC_T __ftw_func_t
 # define NFTW_FUNC_T __nftw_func_t
 #endif
@@ -169,6 +64,64 @@ struct known_object
   INO_T ino;
 };
 
+/* Represents the execution state of a directory processing frame within the
+   iterative file tree walk loop.
+
+   Because the tree traversal is implemented iteratively using a custom stack
+   rather than standard recursion, this state machine tracks the progress
+   of each directory currently being visited.  */
+enum ftw_frame_state
+{
+  /* The initial state of a newly pushed directory frame.  Attempts to open
+     the directory stream.  If successful, transitions to
+     FTW_STATE_STREAM_LOOP.  */
+  FTW_STATE_INIT = 0,
+
+  /* Iterating over the directory entries directly from the open DIR stream
+     (using readdir).  If a subdirectory is encountered and needs to be
+     descended into, a new frame is added to the stack and execution pauses
+     here.  Transitions to FTW_STATE_CONTENT_LOOP if the stream was closed
+     and cached to free up file descriptors, or FTW_STATE_CLEANUP when
+     done.  */
+  FTW_STATE_STREAM_LOOP,
+
+  /* Iterating over directory entries from a cached memory buffer.  This state
+     is used as a fallback when the original DIR stream had to be closed
+     prematurely to prevent file descriptor exhaustion while descending into
+     deeply nested child directories.  Transitions to FTW_STATE_CLEANUP when
+     all cached entries are processed.  */
+  FTW_STATE_CONTENT_LOOP,
+
+  /* The final state, handles resource deallocation (closing remaining
+     streams, freeing cached content buffers), triggering post-traversal
+     callbacks (like FTW_DP for FTW_DEPTH walks), and restoring the
+     previous working directory if FTW_CHDIR was used.  */
+  FTW_STATE_CLEANUP
+};
+
+/* Keep track of visited directories.  */
+struct ftw_frame
+{
+  struct dir_data dir;
+  struct STRUCT_STAT st;
+  int previous_base;
+  char *runp;
+  enum ftw_frame_state state;
+};
+
+struct ftw_stack
+{
+  struct ftw_frame **stack;
+  size_t num_blocks;
+  ssize_t top;
+};
+
+typedef union
+{
+  NFTW_FUNC_T nftw_func;
+  FTW_FUNC_T ftw_func;
+} func_callback_t;
+
 struct ftw_data
 {
   /* Array with pointers to open directory streams.  */
@@ -193,7 +146,8 @@ struct ftw_data
   const int *cvt_arr;
 
   /* Callback function.  We always use the `nftw' form.  */
-  NFTW_FUNC_T func;
+  bool is_nftw;
+  func_callback_t func;
 
   /* Device of starting point.  Needed for FTW_MOUNT.  */
   dev_t dev;
@@ -202,6 +156,9 @@ struct ftw_data
      object.  This is needed when not using FTW_PHYS.  */
   void *known_objects;
 };
+#define CALL_FUNC(__ftw_data, __fp, __sb, __f, __ftw)                            \
+  ((__ftw_data)->is_nftw ? (__ftw_data)->func.nftw_func (__fp, __sb, __f, __ftw) \
+                         : (__ftw_data)->func.ftw_func (__fp, __sb, __f))
 
 static bool
 ftw_allocate (struct ftw_data *data, size_t newsize)
@@ -231,11 +188,6 @@ static const int ftw_arr[] =
 };
 
 
-/* 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)
 {
@@ -274,7 +226,6 @@ find_object (struct ftw_data *data, struct STRUCT_STAT *st)
 
 
 static inline int
-__attribute ((always_inline))
 open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
 {
   int result = 0;
@@ -306,9 +257,7 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
 		  if (newp == NULL)
 		    {
 		      /* No more memory.  */
-		      int save_err = errno;
 		      free (buf);
-		      __set_errno (save_err);
 		      return -1;
 		    }
 		  buf = newp;
@@ -327,9 +276,7 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
 	  data->dirstreams[data->actdir]->content = content;
 	  if (content == NULL)
 	    {
-	      int save_err = errno;
 	      free (buf);
-	      __set_errno (save_err);
 	      result = -1;
 	    }
 	  else
@@ -390,13 +337,15 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
 
 static int
 process_entry (struct ftw_data *data, struct dir_data *dir, const char *name,
-	       size_t namlen, int d_type)
+	       size_t namlen, struct STRUCT_STAT *out_st, bool *descend)
 {
   struct STRUCT_STAT st;
   int result = 0;
   int flag = 0;
   size_t new_buflen;
 
+  *descend = false;
+
   if (name[0] == '.' && (name[1] == '\0'
 			 || (name[1] == '.' && name[2] == '\0')))
     /* Don't process the "." and ".." entries.  */
@@ -466,11 +415,14 @@ process_entry (struct ftw_data *data, struct dir_data *dir, const char *name,
 	      || (!find_object (data, &st)
 		  /* Remember the object.  */
 		  && (result = add_object (data, &st)) == 0))
-	    result = ftw_dir (data, &st, dir);
+	    {
+               *out_st = st;
+               *descend = true;
+	    }
 	}
       else
-	result = (*data->func) (data->dirbuf, &st, data->cvt_arr[flag],
-				&data->ftw);
+	result = CALL_FUNC (data, data->dirbuf, &st, data->cvt_arr[flag],
+			    &data->ftw);
     }
 
   if ((data->flags & FTW_ACTIONRETVAL) && result == FTW_SKIP_SUBTREE)
@@ -480,165 +432,323 @@ process_entry (struct ftw_data *data, struct dir_data *dir, const char *name,
 }
 
 
-static int
-__attribute ((noinline))
-ftw_dir (struct ftw_data *data, struct STRUCT_STAT *st, struct dir_data *old_dir)
+/* The ftw_frame are kept as chunked array to minimize the reallocation cost
+   when the stack grows (since it contains STRUCT_STAT and extra metadata).
+   New chunks of ftw_framw are allocated and only freed when ftw returns.  */
+enum
 {
-  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);
+  FTW_STACK_CHUNK_BLOCKS  = 1,  /* Number of initial allocated chunks.  */
+  FTW_STACK_CHUNK_SIZE    = 32  /* Number of stack frames allocated per
+				   chunk.  */
+};
 
-      return result;
-    }
+static inline struct ftw_frame *
+frame_stack_get (struct ftw_stack *ftwst, int adj)
+{
+  return &ftwst->stack[(ftwst->top + adj) / FTW_STACK_CHUNK_SIZE]
+    [(ftwst->top + adj) % FTW_STACK_CHUNK_SIZE];
+}
 
-  /* 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;
-	}
-    }
+static inline void
+frame_stack_reset_top (struct ftw_stack *fwtst, const struct STRUCT_STAT *st)
+{
+  struct ftw_frame *frame = frame_stack_get (fwtst, 0);
+  frame->st = *st;
+  frame->state = FTW_STATE_INIT;
+  frame->dir.stream = NULL;
+  frame->dir.content = NULL;
+  frame->dir.streamfd = -1;
+}
 
-  /* If necessary, change to this directory.  */
-  if (data->flags & FTW_CHDIR)
+static bool
+frame_stack_init (struct ftw_stack *ftwst, const struct STRUCT_STAT *st)
+{
+  ftwst->num_blocks = FTW_STACK_CHUNK_BLOCKS;
+  ftwst->stack = malloc (FTW_STACK_CHUNK_BLOCKS * sizeof (*ftwst->stack));
+  if (ftwst->stack == NULL)
+    return false;
+
+  ftwst->stack[0] = malloc (FTW_STACK_CHUNK_SIZE * sizeof (struct ftw_frame));
+  if (ftwst->stack[0] == NULL)
     {
-      if (__fchdir (__dirfd (dir.stream)) < 0)
-	{
-	  result = -1;
-	  goto fail;
-	}
+      free (ftwst->stack);
+      return false;
     }
 
-  /* 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;
+  ftwst->top = 0;
+  frame_stack_reset_top (ftwst, st);
+  return true;
+}
+
+static void
+frame_stack_free (struct ftw_stack *ftwst)
+{
+  for (size_t i = 0; i < ftwst->num_blocks; i++)
+    free (ftwst->stack[i]);
+  free (ftwst->stack);
+}
 
-  while (dir.stream != NULL && (d = __readdir64 (dir.stream)) != NULL)
+static bool
+frame_stack_add (struct ftw_stack *ftwst, const struct STRUCT_STAT *st)
+{
+  if (ftwst->top + 1 >= ftwst->num_blocks * FTW_STACK_CHUNK_SIZE)
     {
-      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;
+      size_t new_blocks = ftwst->num_blocks + 1;
+      struct ftw_frame **new_stack = realloc (
+	  ftwst->stack, new_blocks * sizeof (*ftwst->stack));
+
+      if (new_stack == NULL)
+	return false;
+      ftwst->stack = new_stack;
+      ftwst->stack[ftwst->num_blocks] = malloc (
+	  FTW_STACK_CHUNK_SIZE * sizeof (struct ftw_frame));
+      if (ftwst->stack[ftwst->num_blocks] == NULL)
+	return false;
+      ftwst->num_blocks = new_blocks;
     }
+  ftwst->top++;
+  frame_stack_reset_top (ftwst, st);
+  return true;
+}
 
-  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;
+static void
+frame_closedir (struct ftw_data *data, struct ftw_frame *frame)
+{
+  int save_err = errno;
+  assert (frame->dir.content == NULL);
+  __closedir (frame->dir.stream);
+  frame->dir.streamfd = -1;
+  __set_errno (save_err);
+  if (data->actdir-- == 0)
+    data->actdir = data->maxdir - 1;
+  data->dirstreams[data->actdir] = NULL;
+  frame->dir.stream = NULL;
+}
 
-      assert (dir.content == NULL);
+static int
+ftw_dir (struct ftw_data *data, const struct STRUCT_STAT *st)
+{
+  struct ftw_stack ftwst;
+  if (!frame_stack_init (&ftwst, st))
+    return -1;
 
-      __closedir (dir.stream);
-      dir.streamfd = -1;
-      __set_errno (save_err);
+  int result = 0;
 
-      if (data->actdir-- == 0)
-	data->actdir = data->maxdir - 1;
-      data->dirstreams[data->actdir] = NULL;
-    }
-  else
+  while (ftwst.top >= 0)
     {
-      int save_err;
-      char *runp = dir.content;
+      struct ftw_frame *frame = frame_stack_get (&ftwst, 0);
+      struct dir_data *old_dir = (ftwst.top > 0)
+	? &frame_stack_get (&ftwst, -1)->dir : NULL;
 
-      while (result == 0 && *runp != '\0')
+      if (frame->state == FTW_STATE_INIT)
 	{
-	  char *endp = strchr (runp, '\0');
+	  frame->previous_base = data->ftw.base;
+	  result = open_dir_stream (
+	      old_dir == NULL ? NULL : &old_dir->streamfd, data, &frame->dir);
+	  if (result != 0)
+	    {
+	      if (errno == EACCES)
+		result = CALL_FUNC (data, data->dirbuf, &frame->st, FTW_DNR,
+				    &data->ftw);
+	      ftwst.top--;
+	      /* Intercept FTW_SKIP_SUBTREE when popping frame */
+	      if (ftwst.top >= 0 && (data->flags & FTW_ACTIONRETVAL)
+		  && result == FTW_SKIP_SUBTREE)
+		result = 0;
+	      continue;
+	    }
 
-	  // XXX Should store the d_type values as well?!
-	  result = process_entry (data, &dir, runp, endp - runp, DT_UNKNOWN);
+	  if (!(data->flags & FTW_DEPTH))
+	    {
+	      result = CALL_FUNC (data, data->dirbuf, &frame->st, FTW_D,
+				  &data->ftw);
+	      if (result != 0)
+		goto state0_fail;
+	    }
 
-	  runp = endp + 1;
+	  if (data->flags & FTW_CHDIR)
+	    {
+	      if (__fchdir (__dirfd (frame->dir.stream)) < 0)
+		{
+		  result = -1;
+		state0_fail:
+		  frame_closedir (data, frame);
+		  ftwst.top--;
+		  /* Intercept FTW_SKIP_SUBTREE when popping frame.  */
+		  if (ftwst.top >= 0 && (data->flags & FTW_ACTIONRETVAL)
+		      && result == FTW_SKIP_SUBTREE)
+		    result = 0;
+		  continue;
+		}
+	    }
+
+	  ++data->ftw.level;
+	  char *startp = strchr (data->dirbuf, '\0');
+	  assert (startp != data->dirbuf);
+	  if (startp[-1] != '/')
+	    *startp++ = '/';
+	  data->ftw.base = startp - data->dirbuf;
+
+	  frame->state = FTW_STATE_STREAM_LOOP;
+	  frame->runp = frame->dir.content;
 	}
+      else if (frame->state == FTW_STATE_STREAM_LOOP)
+	{
+	  if (result != 0)
+	    {
+	      frame->state = FTW_STATE_CLEANUP;
+	      continue;
+	    }
 
-      save_err = errno;
-      free (dir.content);
-      __set_errno (save_err);
-    }
+	  if (frame->dir.stream == NULL)
+	    {
+	      frame->state = FTW_STATE_CONTENT_LOOP;
+	      frame->runp = frame->dir.content;
+	      continue;
+	    }
 
-  if ((data->flags & FTW_ACTIONRETVAL) && result == FTW_SKIP_SIBLINGS)
-    result = 0;
+	  struct dirent64 *d = __readdir64 (frame->dir.stream);
+	  if (d != NULL)
+	    {
+	      struct STRUCT_STAT child_st;
+	      bool descend = false;
+	      result = process_entry (data, &frame->dir, d->d_name, NAMLEN (d),
+				      &child_st, &descend);
+
+	      if (result == 0 && descend)
+		{
+		  if (!frame_stack_add (&ftwst, &child_st))
+		    {
+		      result = -1;
+		      frame->state = FTW_STATE_CLEANUP;
+		    }
+		  continue;
+		}
+	      else if (result != 0)
+		{
+		  frame->state = FTW_STATE_CLEANUP;
+		  continue;
+		}
+	    }
+	  else
+	    frame->state = FTW_STATE_CLEANUP;
+	}
+      else if (frame->state == FTW_STATE_CONTENT_LOOP)
+	{
+	  /* Check if we are safely positioned to process the starting path.
+	     The 'result' variable here comes from one of two places:
 
-  /* Prepare the return, revert the `struct FTW' information.  */
-  data->dirbuf[data->ftw.base - 1] = '\0';
-  --data->ftw.level;
-  data->ftw.base = previous_base;
+	     1. Initialization: defaults to 0 at the top of ftw_startup.  If
+	        the FTW_CHDIR flag was NOT passed, it remains 0, meaning we
+		are good to go.
 
-  /* 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);
+	     2. Directory Change: If FTW_CHDIR WAS passed, 'result' holds the
+	        return value of the preceding __chdir() call (either moving
+		to "/" or the parsed base directory).
 
-  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 'result' is 0, the setup succeeded (or wasn't needed) and we
+	     can safely stat the initial object.  Othewise, the chdir failed,
+	     so we skip processing and fall through to the cleanup phase.  */
+	  if (result != 0)
+	    {
+	      frame->state = FTW_STATE_CLEANUP;
+	      continue;
+	    }
 
-      if (!done)
-	{
-	  if (data->ftw.base == 1)
+	  if (frame->runp != NULL && *frame->runp != '\0')
 	    {
-	      if (__chdir ("/") < 0)
-		result = -1;
+	      char *endp = strchr (frame->runp, '\0');
+	      struct STRUCT_STAT child_st;
+	      bool descend = false;
+
+	      result = process_entry (data, &frame->dir, frame->runp,
+				      endp - frame->runp, &child_st,
+				      &descend);
+	      frame->runp = endp + 1;
+
+	      if (result == 0 && descend)
+		{
+		  if (!frame_stack_add (&ftwst, &child_st))
+		    {
+		      result = -1;
+		      frame->state = FTW_STATE_CLEANUP;
+		    }
+		  continue;
+		}
+	      else if (result != 0)
+		{
+		  frame->state = FTW_STATE_CLEANUP;
+		  continue;
+		}
 	    }
 	  else
-	    if (__chdir ("..") < 0)
-	      result = -1;
+	    frame->state = FTW_STATE_CLEANUP;
+	}
+      else if (frame->state == FTW_STATE_CLEANUP)
+	{
+	  if (frame->dir.stream != NULL)
+	    frame_closedir (data, frame);
+	  else if (frame->dir.content != NULL)
+	    {
+	      free (frame->dir.content);
+	      frame->dir.content = NULL;
+	    }
+
+	  if ((data->flags & FTW_ACTIONRETVAL) && result == FTW_SKIP_SIBLINGS)
+	    result = 0;
+
+	  data->dirbuf[data->ftw.base - 1] = '\0';
+	  --data->ftw.level;
+	  data->ftw.base = frame->previous_base;
+
+	  if (result == 0 && (data->flags & FTW_DEPTH))
+	    result
+		= CALL_FUNC (data, data->dirbuf, &frame->st, FTW_DP,
+			     &data->ftw);
+
+	  if (old_dir != NULL && (data->flags & FTW_CHDIR)
+	      && (result == 0
+		  || ((data->flags & FTW_ACTIONRETVAL)
+		      && (result != -1 && result != FTW_STOP))))
+	    {
+	      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;
+		}
+	    }
+
+	  ftwst.top--;
+	  /* Intercept FTW_SKIP_SUBTREE when popping frame.  */
+	  if (ftwst.top >= 0 && (data->flags & FTW_ACTIONRETVAL)
+	      && result == FTW_SKIP_SUBTREE)
+	    result = 0;
 	}
     }
 
+  frame_stack_free (&ftwst);
+
   return result;
 }
 
 
 static int
-__attribute ((noinline))
-ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
-	     int flags)
+ftw_startup (const char *dir, bool is_nftw, func_callback_t 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;
@@ -671,13 +781,8 @@ ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
 
   data.flags = flags;
 
-  /* 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;
+  data.is_nftw = is_nftw;
+  data.func = func;
 
   /* Since we internally use the complete set of FTW_* values we need
      to reduce the value range before calling a `ftw' callback.  */
@@ -748,8 +853,8 @@ ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
 	      && errno == ENOENT
 	      && LSTAT (name, &st) == 0
 	      && S_ISLNK (st.st_mode))
-	    result = (*data.func) (data.dirbuf, &st, data.cvt_arr[FTW_SLN],
-				   &data.ftw);
+	    result = CALL_FUNC (&data, 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.  */
@@ -768,14 +873,14 @@ ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
 		result = add_object (&data, &st);
 
 	      if (result == 0)
-		result = ftw_dir (&data, &st, NULL);
+		result = ftw_dir (&data, &st);
 	    }
 	  else
 	    {
 	      int flag = S_ISLNK (st.st_mode) ? FTW_SL : FTW_F;
 
-	      result = (*data.func) (data.dirbuf, &st, data.cvt_arr[flag],
-				     &data.ftw);
+	      result = CALL_FUNC (&data, data.dirbuf, &st, data.cvt_arr[flag],
+				  &data.ftw);
 	    }
 	}
 
@@ -802,10 +907,8 @@ ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
 
   /* Free all memory.  */
  out_fail:
-  save_err = errno;
   __tdestroy (data.known_objects, free);
   free (data.dirstreams);
-  __set_errno (save_err);
 
   return result;
 }
@@ -817,14 +920,16 @@ ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
 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, (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, (func_callback_t) { .nftw_func = func },
+		      descriptors, flags);
 }
 #else
 
@@ -841,7 +946,8 @@ 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, (func_callback_t) { .nftw_func = func },
+		      descriptors, flags);
 }
 versioned_symbol (libc, NFTW_NEW_NAME, NFTW_NAME, GLIBC_2_3_3);
 
@@ -856,7 +962,8 @@ 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, (func_callback_t) { .nftw_func = func },
+		      descriptors, flags);
 }
 
 compat_symbol (libc, NFTW_OLD_NAME, NFTW_NAME, GLIBC_2_1);
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>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.