[PATCH] io: ftw: Use state stack instead of recursion (BZ 33882)
Adhemerval Zanella
adhemerval.zanella@linaro.org
Wed Feb 18 20:18:19 GMT 2026
The previous implementation of ftw relied on recursion to traverse
directories (ftw_dir calling process_entry calling ftw_dir). On
deep directory trees, this could lead to stack overflow (as exercised
with 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, and path offset).
* The new dynamic stack fielf (data::stack) holds pointers to
ftw_frame.
* The ftw_dir is rewritten to use a loop instead of recursion
and integrats the 'process_entry' logic directly into the
iterative loop to enable immediate state transitions without
function call overhead.
* Ensured 'data::dirstreams' pointers remain valid by decoupling
them from the resizeable stack logic.
The patch also cleanups some unused definitions and assumptions
(like free clobbering errno) and fixes an UB while handling the
callback function for ftw.
Checked on x86_64-linux-gnu and i686-linux-gnu.
---
io/Makefile | 1 +
io/ftw.c | 758 +++++++++++++++++++++---------------------
io/tst-nftw-bz33882.c | 106 ++++++
3 files changed, 490 insertions(+), 375 deletions(-)
create mode 100644 io/tst-nftw-bz33882.c
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..396e90c182 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 <assert.h>
+#include <dirent.h>
#include <fcntl.h>
#include <ftw.h>
-#include <limits.h>
-#include <search.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
#include <not-cancel.h>
+#include <search.h>
+#include <unistd.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 +35,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
@@ -161,6 +55,7 @@ struct dir_data
DIR *stream;
int streamfd;
char *content;
+ char *runp;
};
struct known_object
@@ -169,6 +64,23 @@ struct known_object
INO_T ino;
};
+/* Explicit stack frame for iteration. */
+struct ftw_frame
+{
+ struct dir_data dir;
+ struct STRUCT_STAT st; /* 'stat' of the directory itself. */
+ size_t base; /* Base offset in data->dirbuf. */
+ int level; /* Depth level. */
+ bool initialized; /* Whether stream been opened/processed. */
+ bool chdir_performed; /* Whether chdir into this directory. */
+};
+
+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. */
@@ -192,8 +104,8 @@ struct ftw_data
`ftw'. */
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;
@@ -201,7 +113,14 @@ struct ftw_data
/* Data structure for keeping fingerprints of already processed
object. This is needed when not using FTW_PHYS. */
void *known_objects;
+
+ struct ftw_frame **stack; /* Array of pointers to frames */
+ size_t stack_size;
+ size_t stack_capacity;
};
+#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 +150,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)
{
@@ -263,7 +177,7 @@ add_object (struct ftw_data *data, struct STRUCT_STAT *st)
}
-static inline int
+static inline bool
find_object (struct ftw_data *data, struct STRUCT_STAT *st)
{
struct known_object obj;
@@ -273,8 +187,7 @@ find_object (struct ftw_data *data, struct STRUCT_STAT *st)
}
-static inline int
-__attribute ((always_inline))
+static int
open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
{
int result = 0;
@@ -305,10 +218,7 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
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;
@@ -327,9 +237,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
@@ -337,6 +245,8 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
__closedir (st);
data->dirstreams[data->actdir]->stream = NULL;
data->dirstreams[data->actdir]->streamfd = -1;
+ data->dirstreams[data->actdir]->runp =
+ data->dirstreams[data->actdir]->content;
data->dirstreams[data->actdir] = NULL;
}
}
@@ -377,6 +287,7 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
{
dirp->streamfd = __dirfd (dirp->stream);
dirp->content = NULL;
+ dirp->runp = NULL;
data->dirstreams[data->actdir] = dirp;
if (++data->actdir == data->maxdir)
@@ -387,258 +298,358 @@ open_dir_stream (int *dfdp, struct ftw_data *data, struct dir_data *dirp)
return result;
}
+/* Stack helpers */
+static bool
+stack_push (struct ftw_data *data, struct ftw_frame *frame)
+{
+ if (data->stack_size == data->stack_capacity)
+ {
+ size_t new_cap
+ = data->stack_capacity == 0 ? 16 : data->stack_capacity * 2;
+ struct ftw_frame **new_stack
+ = realloc (data->stack, new_cap * sizeof (struct ftw_frame *));
+ if (new_stack == NULL)
+ return false;
+ data->stack = new_stack;
+ data->stack_capacity = new_cap;
+ }
+ data->stack[data->stack_size++] = frame;
+ return true;
+}
+
+static struct ftw_frame *
+stack_top(struct ftw_data *data)
+{
+ if (data->stack_size == 0)
+ return NULL;
+ return data->stack[data->stack_size - 1];
+}
+
+static void
+stack_pop (struct ftw_data *data)
+{
+ if (data->stack_size > 0)
+ {
+ struct ftw_frame *f = data->stack[--data->stack_size];
+ free (f);
+ }
+}
static int
-process_entry (struct ftw_data *data, struct dir_data *dir, const char *name,
- size_t namlen, int d_type)
+ftw_dir (struct ftw_data *data, struct STRUCT_STAT *start_st)
{
- 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))
+ struct ftw_frame *root = malloc (sizeof (struct ftw_frame));
+ if (root == NULL)
return -1;
- *((char *) __mempcpy (data->dirbuf + data->ftw.base, name, namlen)) = '\0';
+ root->dir.streamfd = -1;
+ root->st = *start_st;
+ root->base = data->ftw.base;
+ root->level = data->ftw.level;
+ root->initialized = false;
+ root->chdir_performed = false;
- int statres;
- if (dir->streamfd != -1)
- statres = FSTATAT (dir->streamfd, name, &st,
- (data->flags & FTW_PHYS) ? AT_SYMLINK_NOFOLLOW : 0);
- else
+ if (!stack_push (data, root))
{
- if ((data->flags & FTW_CHDIR) == 0)
- name = data->dirbuf;
-
- statres = ((data->flags & FTW_PHYS)
- ? LSTAT (name, &st)
- : STAT (name, &st));
+ free (root);
+ return -1;
}
- if (statres < 0)
+ int result = 0;
+
+ while (data->stack_size > 0)
{
- if (errno != EACCES && errno != ENOENT)
- result = -1;
- else if (data->flags & FTW_PHYS)
- flag = FTW_NS;
- else
+ struct ftw_frame *frame = stack_top (data);
+
+ if (!frame->initialized)
{
- /* 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)
+ struct dir_data *old_dir = NULL;
+ /* If there is a parent, get its fd reference if needed. */
+ if (data->stack_size > 1)
{
- if (__chdir ("/") < 0)
+ struct ftw_frame *parent = data->stack[data->stack_size - 2];
+ old_dir = &parent->dir;
+ }
+
+ int *dfdp = (old_dir == NULL) ? NULL : &old_dir->streamfd;
+ /* Open the stream for this directory. This might require that
+ another stream has to be closed. */
+ result = open_dir_stream (dfdp, data, &frame->dir);
+
+ if (result != 0)
+ {
+ if (errno == EACCES)
+ {
+ result = CALL_FUNC (data, data->dirbuf, &frame->st, FTW_DNR,
+ &data->ftw);
+ stack_pop (data);
+ if (result != 0)
+ break;
+ continue;
+ }
+ stack_pop (data);
+ return result;
+ }
+
+ /* First, report the directory (if not depth-first). */
+ if (!(data->flags & FTW_DEPTH))
+ {
+ result = CALL_FUNC (data, data->dirbuf, &frame->st, FTW_D,
+ &data->ftw);
+ if (result != 0)
+ break;
+ }
+
+ /* If necessary, change to this directory. */
+ if (data->flags & FTW_CHDIR)
+ {
+ if (__fchdir (__dirfd (frame->dir.stream)) != 0)
+ break;
+ frame->chdir_performed = true;
+ }
+
+ /* Next, update the `struct FTW' information. */
+ ++data->ftw.level;
+ char *startp = strchr (data->dirbuf, '\0');
+ assert (startp != data->dirbuf);
+ if (startp[-1] != '/')
+ *startp++ = '/';
+ data->ftw.base = startp - data->dirbuf;
+
+ frame->initialized = true;
+ }
+
+ char *name = NULL;
+ size_t namlen = 0;
+ bool entry_found = false;
+
+ if (frame->dir.stream != NULL)
+ {
+ struct dirent64 *d = __readdir64 (frame->dir.stream);
+ if (d != NULL)
+ {
+ name = d->d_name;
+ namlen = NAMLEN (d);
+ entry_found = true;
+ }
+ }
+ else
+ {
+ if (frame->dir.content != NULL && *frame->dir.runp != '\0')
+ {
+ name = frame->dir.runp;
+ char *endp = strchr (name, '\0');
+ namlen = endp - name;
+ frame->dir.runp = endp + 1;
+ entry_found = true;
+ }
+ }
+
+ if (entry_found)
+ {
+ if (name[0] == '.'
+ && (name[1] == '\0' || (name[1] == '.' && name[2] == '\0')))
+ /* Don't process the "." and ".." entries. */
+ continue;
+
+ size_t new_buflen = data->ftw.base + namlen + 2;
+ if (data->dirbufsize < new_buflen
+ && !ftw_allocate (data, 2 * new_buflen))
+ {
+ result = -1;
+ break;
+ }
+ *((char *) __mempcpy (data->dirbuf + data->ftw.base, name, namlen))
+ = '\0';
+
+ struct STRUCT_STAT st;
+ int flag = 0;
+ int statres;
+
+ if (frame->dir.streamfd != -1)
+ statres
+ = FSTATAT (frame->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 (frame->dir.streamfd != -1)
+ statres = FSTATAT (frame->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 (__chdir ("..") < 0)
- result = -1;
+ {
+ 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)
+ && (add_object (data, &st) == 0)))
+ {
+ struct ftw_frame *sub
+ = malloc (sizeof (struct ftw_frame));
+ if (sub == NULL)
+ {
+ result = -1;
+ break;
+ }
+
+ sub->st = st;
+ sub->base = data->ftw.base;
+ sub->level = data->ftw.level;
+ sub->initialized = false;
+ sub->chdir_performed = false;
+ sub->dir.streamfd = -1;
+
+ if (!stack_push (data, sub))
+ {
+ free (sub);
+ result = -1;
+ break;
+ }
+
+ /* Restart the loop to pop a new directory. */
+ continue;
+ }
+ }
+
+ if (flag != FTW_D || result != 0)
+ result = CALL_FUNC (data, data->dirbuf, &st,
+ data->cvt_arr[flag], &data->ftw);
+ }
+
+ if ((data->flags & FTW_ACTIONRETVAL) && result == FTW_SKIP_SUBTREE)
+ result = 0;
+ if (result != 0)
+ continue;
+ }
+ else
+ {
+ int save_err = errno;
+ if (frame->dir.stream != NULL)
+ {
+ __closedir (frame->dir.stream);
+ frame->dir.streamfd = -1;
+ if (data->actdir-- == 0)
+ data->actdir = data->maxdir - 1;
+ data->dirstreams[data->actdir] = NULL;
+ }
+ if (frame->dir.content != NULL)
+ {
+ free (frame->dir.content);
+ frame->dir.content = NULL;
+ }
+ __set_errno (save_err);
+
+ data->dirbuf[data->ftw.base - 1] = '\0';
+ data->ftw.level--;
+
+ /* Restore base to what it was when we entered this dir */
+ data->ftw.base = frame->base;
+
+ /* Report Directory (Post-order) */
+ if (result == 0 && (data->flags & FTW_DEPTH))
+ result = CALL_FUNC (data, data->dirbuf, &frame->st, FTW_DP,
+ &data->ftw);
+
+ if (frame->chdir_performed && result == 0)
+ {
+ if (!((data->flags & FTW_ACTIONRETVAL)
+ && (result == -1 || result == FTW_STOP)))
+ {
+ struct dir_data *parent_dir = NULL;
+ if (data->stack_size > 1)
+ {
+ parent_dir = &data->stack[data->stack_size - 2]->dir;
+ }
+
+ bool done = false;
+ if (parent_dir && parent_dir->stream != NULL)
+ if (__fchdir (__dirfd (parent_dir->stream)) == 0)
+ done = true;
+
+ if (!done)
+ {
+ if (data->ftw.base == 1 && __chdir ("/") < 0)
+ result = -1;
+ else if (__chdir ("..") < 0)
+ result = -1;
+ }
+ }
+ }
+
+ if ((data->flags & FTW_ACTIONRETVAL) && result == FTW_SKIP_SIBLINGS)
+ result = 0;
+
+ stack_pop (data);
+
+ if (result != 0)
+ break;
}
}
+ while (data->stack_size > 0)
+ {
+ struct ftw_frame *f = stack_top (data);
+ if (f->dir.stream)
+ __closedir (f->dir.stream);
+ if (f->dir.content)
+ free (f->dir.content);
+ stack_pop (data);
+ }
+ free (data->stack);
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 ftw_data data =
+ {
+ .dirstreams = NULL,
+ .stack = NULL,
+ .stack_size = 0,
+ .stack_capacity = 0
+ };
struct STRUCT_STAT st;
int result = 0;
- int save_err;
int cwdfd = -1;
char *cwd = NULL;
char *cp;
@@ -671,13 +682,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 +754,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 +774,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);
}
}
@@ -800,12 +806,10 @@ ftw_startup (const char *dir, int is_nftw, void *func, int descriptors,
__set_errno (save_err);
}
- /* Free all memory. */
+ /* Free all memory. */
out_fail:
- save_err = errno;
__tdestroy (data.known_objects, free);
free (data.dirstreams);
- __set_errno (save_err);
return result;
}
@@ -817,14 +821,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 +847,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 +863,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..cf788a12ab
--- /dev/null
+++ b/io/tst-nftw-bz33882.c
@@ -0,0 +1,106 @@
+
+/* 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>
--
2.43.0
More information about the Libc-alpha
mailing list