[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>