diff --git a/lib/libc/gen/scandir.3 b/lib/libc/gen/scandir.3 index 9ced9fa4ef9d..3da4500cefb9 100644 --- a/lib/libc/gen/scandir.3 +++ b/lib/libc/gen/scandir.3 @@ -1,270 +1,300 @@ .\" Copyright (c) 1983, 1991, 1993 .\" The Regents of the University of California. All rights reserved. .\" .\" Redistribution and use in source and binary forms, with or without .\" modification, are permitted provided that the following conditions .\" are met: .\" 1. Redistributions of source code must retain the above copyright .\" notice, this list of conditions and the following disclaimer. .\" 2. Redistributions in binary form must reproduce the above copyright .\" notice, this list of conditions and the following disclaimer in the .\" documentation and/or other materials provided with the distribution. .\" 3. Neither the name of the University nor the names of its contributors .\" may be used to endorse or promote products derived from this software .\" without specific prior written permission. .\" .\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND .\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE .\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE .\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE .\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL .\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS .\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) .\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT .\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd June 20, 2025 +.Dd June 25, 2025 .Dt SCANDIR 3 .Os .Sh NAME .Nm scandir , .Nm fdscandir , .Nm scandirat , .Nm scandir_b , .Nm fdscandir_b , .Nm fdscandirat_b , .Nm alphasort , .Nm versionsort .Nd scan a directory .Sh LIBRARY .Lb libc .Sh SYNOPSIS .In dirent.h .Ft int .Fo scandir .Fa "const char *dirname" .Fa "struct dirent ***namelist" .Fa "int \*(lp*select\*(rp\*(lpconst struct dirent *\*(rp" .Fa "int \*(lp*compar\*(rp\*(lpconst struct dirent **, const struct dirent **\*(rp" .Fc .Ft int .Fo fdscandir .Fa "int dirfd" .Fa "struct dirent ***namelist" .Fa "int \*(lp*select\*(rp\*(lpconst struct dirent *\*(rp" .Fa "int \*(lp*compar\*(rp\*(lpconst struct dirent **, const struct dirent **\*(rp" .Fc .Ft int .Fo scandirat .Fa "int dirfd" .Fa "const char *dirname" .Fa "struct dirent ***namelist" .Fa "int \*(lp*select\*(rp\*(lpconst struct dirent *\*(rp" .Fa "int \*(lp*compar\*(rp\*(lpconst struct dirent **, const struct dirent **\*(rp" .Fc .Ft int .Fo scandir_b .Fa "const char *dirname" .Fa "struct dirent ***namelist" .Fa "int \*(lp^select\*(rp\*(lpconst struct dirent *\*(rp" .Fa "int \*(lp^compar\*(rp\*(lpconst struct dirent **, const struct dirent **\*(rp" .Fc .Ft int .Fo fdscandir_b .Fa "int dirfd" .Fa "struct dirent ***namelist" .Fa "int \*(lp^select\*(rp\*(lpconst struct dirent *\*(rp" .Fa "int \*(lp^compar\*(rp\*(lpconst struct dirent **, const struct dirent **\*(rp" .Fc .Ft int .Fo scandirat_b .Fa "int dirfd" .Fa "const char *dirname" .Fa "struct dirent ***namelist" .Fa "int \*(lp^select\*(rp\*(lpconst struct dirent *\*(rp" .Fa "int \*(lp^compar\*(rp\*(lpconst struct dirent **, const struct dirent **\*(rp" .Fc .Ft int .Fn alphasort "const struct dirent **d1" "const struct dirent **d2" .Ft int .Fn versionsort "const struct dirent **d1" "const struct dirent **d2" .Sh DESCRIPTION The .Fn scandir function reads the directory .Fa dirname and builds an array of pointers to directory entries using .Xr malloc 3 . It returns the number of entries in the array. A pointer to the array of directory entries is stored in the location referenced by .Fa namelist (even if no entries were selected). .Pp The .Fa select argument is a pointer to a user supplied subroutine which is called by .Fn scandir to select which entries are to be included in the array. The select routine is passed a pointer to a directory entry and should return a non-zero value if the directory entry is to be included in the array. If .Fa select is null, then all the directory entries will be included. .Pp The .Fa compar argument is a pointer to a user supplied subroutine which is passed to .Xr qsort 3 to sort the completed array. If this pointer is null, the array is not sorted. .Pp The .Fn alphasort function is a routine which can be used for the .Fa compar argument to sort the array alphabetically using .Xr strcoll 3 . .Pp The .Fn versionsort function is a routine which can be used for the .Fa compar argument to sort the array naturally using .Xr strverscmp 3 . .Pp The memory allocated for the array can be deallocated with .Xr free 3 , by freeing each pointer in the array and then the array itself. .Pp The .Fn fdscandir function is similar to .Fn scandir , but takes a file descriptor referencing a directory instead of a path. The file descriptor is left open on return, regardless of outcome. .Pp The .Fn scandirat function is similar to .Fn scandir , but takes an additional .Fa dirfd argument. If the supplied .Fa dirname is absolute, the function's behavior is identical to that of .Fn scandir , the .Fa dirfd argument is unused. If .Fa dirname is relative, .Fa dirfd must be a valid file descriptor referencing a directory, in which case the .Fa dirname lookup is performed relative to the directory referenced by .Fa dirfd . If .Fa dirfd has the special value .Va AT_FDCWD , then the current process directory is used as the base for relative lookups. See .Xr openat 2 for additional details. .Pp The .Fn scandir_b , .Fn fdscandir_b , and .Fn scandirat_b functions behave in the same way as .Fn scandir , .Fn fdscandir , and .Fn scandirat , respectively, but take blocks as arguments instead of function pointers and call .Fn qsort_b rather than .Fn qsort . .Sh DIAGNOSTICS The .Fn scandir , .Fn fdscandir , .Fn scandirat , .Fn scandir_b , .Fn fdscandir_b , and .Fn scandirat_b functions return the number of directory entries found on succes. If the directory cannot be opened for reading, an error occurs while reading the directory, or .Xr malloc 3 cannot allocate enough memory to hold all the directory entries, they return \-1 and set .Va errno to an appropriate value. +.Sh ERRORS +The +.Fn scandir , +.Fn scandirat , +.Fn scandir_b , +and +.Fn scandirat_b +functions may fail and set +.Va errno +for any of the errors specified for the +.Xr opendir 3 , +.Xr malloc 3 , +.Xr readdir 3 , +and +.Xr closedir 3 +functions. +.Pp +The +.Fn fdscandir +and +.Fn fdscandir_b +functions may fail and set +.Va errno +for any of the errors specified for the +.Xr fdopendir 3 , +.Xr malloc 3 , +.Xr readdir 3 , +and +.Xr closedir 3 +functions. .Sh SEE ALSO .Xr openat 2 , .Xr directory 3 , .Xr malloc 3 , .Xr qsort 3 , .Xr strcoll 3 , .Xr strverscmp 3 , .Xr dir 5 .Sh STANDARDS The .Fn alphasort and .Fn scandir functions are expected to conform to .St -p1003.1-2008 . The .Fn scandirat and .Fn versionsort functions are GNU extensions and conform to no standard. The .Fn fdscandir , .Fn scandir_b , .Fn fdscandir_b , and .Fn scandirat_b functions are .Fx extensions. .Sh HISTORY The .Fn scandir and .Fn alphasort functions appeared in .Bx 4.2 . The .Fn scandir_b function was added in .Fx 11.0 . The .Fn scandirat and .Fn versionsort functions were added in .Fx 13.2 . The .Fn fdscandir , .Fn fdscandir_b , and .Fn scandirat_b functions were added in .Fx 15.0 . diff --git a/lib/libc/gen/scandir.c b/lib/libc/gen/scandir.c index 134c88713d39..56d77c29bd07 100644 --- a/lib/libc/gen/scandir.c +++ b/lib/libc/gen/scandir.c @@ -1,250 +1,258 @@ /*- * SPDX-License-Identifier: BSD-3-Clause * * Copyright (c) 1983, 1993 * The Regents of the University of California. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ /* * Scan the directory dirname calling select to make a list of selected * directory entries then sort using qsort and compare routine dcomp. * Returns the number of entries and a pointer to a list of pointers to * struct dirent (through namelist). Returns -1 if there were any errors. */ #include "namespace.h" #include #include #include #include #include #include #include "un-namespace.h" #ifdef I_AM_SCANDIR_B #include "block_abi.h" #define SELECT(x) CALL_BLOCK(select, x) #ifndef __BLOCKS__ void qsort_b(void *, size_t, size_t, void *); #endif #else #define SELECT(x) select(x) #endif #ifdef I_AM_SCANDIR_B typedef DECLARE_BLOCK(int, select_block, const struct dirent *); typedef DECLARE_BLOCK(int, dcomp_block, const struct dirent **, const struct dirent **); #else static int scandir_thunk_cmp(const void *p1, const void *p2, void *thunk); #endif static int #ifdef I_AM_SCANDIR_B scandir_dirp_b(DIR *dirp, struct dirent ***namelist, select_block select, dcomp_block dcomp) #else scandir_dirp(DIR *dirp, struct dirent ***namelist, int (*select)(const struct dirent *), int (*dcomp)(const struct dirent **, const struct dirent **)) #endif { struct dirent *d, *p = NULL, **names = NULL, **names2; size_t arraysz = 32, numitems = 0; int serrno; names = malloc(arraysz * sizeof(*names)); if (names == NULL) return (-1); - while ((d = readdir(dirp)) != NULL) { + while (errno = 0, (d = readdir(dirp)) != NULL) { if (select != NULL && !SELECT(d)) continue; /* just selected names */ /* * Make a minimum size copy of the data */ p = malloc(_GENERIC_DIRSIZ(d)); if (p == NULL) goto fail; p->d_fileno = d->d_fileno; p->d_type = d->d_type; p->d_reclen = d->d_reclen; p->d_namlen = d->d_namlen; memcpy(p->d_name, d->d_name, p->d_namlen + 1); /* * Check to make sure the array has space left and * realloc the maximum size. */ if (numitems >= arraysz) { arraysz = arraysz ? arraysz * 2 : 32; names2 = reallocarray(names, arraysz, sizeof(*names)); if (names2 == NULL) goto fail; names = names2; } names[numitems++] = p; } + /* + * Since we can't simultaneously return both -1 and a count, we + * must either suppress the error or discard the partial result. + * The latter seems the lesser of two evils. + */ + if (errno != 0) + goto fail; if (numitems && dcomp != NULL) #ifdef I_AM_SCANDIR_B qsort_b(names, numitems, sizeof(struct dirent *), (void*)dcomp); #else qsort_r(names, numitems, sizeof(struct dirent *), scandir_thunk_cmp, &dcomp); #endif *namelist = names; return (numitems); fail: serrno = errno; - free(p); + if (numitems == 0 || names[numitems - 1] != p) + free(p); while (numitems > 0) free(names[--numitems]); free(names); errno = serrno; return (-1); } int #ifdef I_AM_SCANDIR_B scandir_b(const char *dirname, struct dirent ***namelist, select_block select, dcomp_block dcomp) #else scandir(const char *dirname, struct dirent ***namelist, int (*select)(const struct dirent *), int (*dcomp)(const struct dirent **, const struct dirent **)) #endif { DIR *dirp; int ret, serrno; dirp = opendir(dirname); if (dirp == NULL) return (-1); ret = #ifdef I_AM_SCANDIR_B scandir_dirp_b #else scandir_dirp #endif (dirp, namelist, select, dcomp); serrno = errno; closedir(dirp); errno = serrno; return (ret); } int #ifdef I_AM_SCANDIR_B fdscandir_b(int dirfd, struct dirent ***namelist, select_block select, dcomp_block dcomp) #else fdscandir(int dirfd, struct dirent ***namelist, int (*select)(const struct dirent *), int (*dcomp)(const struct dirent **, const struct dirent **)) #endif { DIR *dirp; int ret, serrno; dirp = fdopendir(dirfd); if (dirp == NULL) return (-1); ret = #ifdef I_AM_SCANDIR_B scandir_dirp_b #else scandir_dirp #endif (dirp, namelist, select, dcomp); serrno = errno; fdclosedir(dirp); errno = serrno; return (ret); } int #ifdef I_AM_SCANDIR_B scandirat_b(int dirfd, const char *dirname, struct dirent ***namelist, select_block select, dcomp_block dcomp) #else scandirat(int dirfd, const char *dirname, struct dirent ***namelist, int (*select)(const struct dirent *), int (*dcomp)(const struct dirent **, const struct dirent **)) #endif { int fd, ret, serrno; fd = _openat(dirfd, dirname, O_RDONLY | O_DIRECTORY | O_CLOEXEC); if (fd == -1) return (-1); ret = #ifdef I_AM_SCANDIR_B fdscandir_b #else fdscandir #endif (fd, namelist, select, dcomp); serrno = errno; _close(fd); errno = serrno; return (ret); } #ifndef I_AM_SCANDIR_B /* * Alphabetic order comparison routine for those who want it. * POSIX 2008 requires that alphasort() uses strcoll(). */ int alphasort(const struct dirent **d1, const struct dirent **d2) { return (strcoll((*d1)->d_name, (*d2)->d_name)); } int versionsort(const struct dirent **d1, const struct dirent **d2) { return (strverscmp((*d1)->d_name, (*d2)->d_name)); } static int scandir_thunk_cmp(const void *p1, const void *p2, void *thunk) { int (*dc)(const struct dirent **, const struct dirent **); dc = *(int (**)(const struct dirent **, const struct dirent **))thunk; return (dc((const struct dirent **)p1, (const struct dirent **)p2)); } #endif #ifdef I_AM_SCANDIR_B __weak_reference(fdscandir_b, fscandir_b); #else __weak_reference(fdscandir, fscandir); #endif diff --git a/lib/libc/tests/gen/scandir_test.c b/lib/libc/tests/gen/scandir_test.c index 9a9940aca881..f7b52b5e3616 100644 --- a/lib/libc/tests/gen/scandir_test.c +++ b/lib/libc/tests/gen/scandir_test.c @@ -1,134 +1,197 @@ /*- * Copyright (c) 2025 Klara, Inc. * * SPDX-License-Identifier: BSD-2-Clause */ #include #include +#include #include +#include #include #include static void scandir_prepare(const struct atf_tc *tc) { ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); ATF_REQUIRE_EQ(0, mkdir("dir/dir", 0755)); ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644))); ATF_REQUIRE_EQ(0, symlink("file", "dir/link")); ATF_REQUIRE_EQ(0, mkdir("dir/skip", 0755)); } static void scandir_verify(const struct atf_tc *tc, int n, struct dirent **namelist) { ATF_REQUIRE_EQ_MSG(5, n, "return value is %d", n); ATF_CHECK_STREQ("link", namelist[0]->d_name); ATF_CHECK_STREQ("file", namelist[1]->d_name); ATF_CHECK_STREQ("dir", namelist[2]->d_name); ATF_CHECK_STREQ("..", namelist[3]->d_name); ATF_CHECK_STREQ(".", namelist[4]->d_name); } static int scandir_select(const struct dirent *ent) { return (strcmp(ent->d_name, "skip") != 0); } static int scandir_compare(const struct dirent **a, const struct dirent **b) { return (strcmp((*b)->d_name, (*a)->d_name)); } ATF_TC(scandir_test); ATF_TC_HEAD(scandir_test, tc) { atf_tc_set_md_var(tc, "descr", "Test scandir()"); } ATF_TC_BODY(scandir_test, tc) { struct dirent **namelist = NULL; int i, ret; scandir_prepare(tc); ret = scandir("dir", &namelist, scandir_select, scandir_compare); scandir_verify(tc, ret, namelist); for (i = 0; i < ret; i++) free(namelist[i]); free(namelist); } ATF_TC(fdscandir_test); ATF_TC_HEAD(fdscandir_test, tc) { atf_tc_set_md_var(tc, "descr", "Test fdscandir()"); } ATF_TC_BODY(fdscandir_test, tc) { struct dirent **namelist = NULL; int fd, i, ret; scandir_prepare(tc); ATF_REQUIRE((fd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); ret = fdscandir(fd, &namelist, scandir_select, scandir_compare); scandir_verify(tc, ret, namelist); for (i = 0; i < ret; i++) free(namelist[i]); free(namelist); ATF_REQUIRE_EQ(0, close(fd)); } ATF_TC(scandirat_test); ATF_TC_HEAD(scandirat_test, tc) { atf_tc_set_md_var(tc, "descr", "Test scandirat()"); } ATF_TC_BODY(scandirat_test, tc) { struct dirent **namelist = NULL; int fd, i, ret; scandir_prepare(tc); ATF_REQUIRE((fd = open("dir", O_DIRECTORY | O_SEARCH)) >= 0); ret = scandirat(fd, ".", &namelist, scandir_select, scandir_compare); scandir_verify(tc, ret, namelist); for (i = 0; i < ret; i++) free(namelist[i]); free(namelist); ATF_REQUIRE_EQ(0, close(fd)); } static int scandir_none(const struct dirent *ent __unused) { return (0); } ATF_TC(scandir_none); ATF_TC_HEAD(scandir_none, tc) { atf_tc_set_md_var(tc, "descr", "Test scandir() when no entries are selected"); } ATF_TC_BODY(scandir_none, tc) { struct dirent **namelist = NULL; ATF_REQUIRE_EQ(0, scandir(".", &namelist, scandir_none, alphasort)); ATF_REQUIRE(namelist); free(namelist); } +/* + * Test that scandir() propagates errors from readdir(): we create a + * directory with enough entries that it can't be read in a single + * getdirentries() call, then abuse the selection callback to close the + * file descriptor scandir() is using after the first call, causing the + * next one to fail, and verify that readdir() returns an error instead of + * a partial result. We make two passes, one in which nothing was + * selected before the error occurred, and one in which everything was. + */ +static int scandir_error_count; +static int scandir_error_fd; +static int scandir_error_select_return; + +static int +scandir_error_select(const struct dirent *ent __unused) +{ + if (scandir_error_count++ == 0) + close(scandir_error_fd); + return (scandir_error_select_return); +} + +ATF_TC(scandir_error); +ATF_TC_HEAD(scandir_error, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test that scandir() propagates errors from readdir()"); +} +ATF_TC_BODY(scandir_error, tc) +{ + char path[16]; + struct dirent **namelist = NULL; + int fd, i, ret; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + for (i = 0; i < 1024; i++) { + snprintf(path, sizeof(path), "dir/%04x", i); + ATF_REQUIRE_EQ(0, symlink(path + 4, path)); + } + + /* first pass, select nothing */ + ATF_REQUIRE((fd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + scandir_error_count = 0; + scandir_error_fd = fd; + scandir_error_select_return = 0; + ret = fdscandir(fd, &namelist, scandir_error_select, NULL); + ATF_CHECK_EQ(-1, ret); + ATF_CHECK_ERRNO(EBADF, ret < 0); + ATF_CHECK_EQ(NULL, namelist); + + /* second pass, select everything */ + ATF_REQUIRE((fd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + scandir_error_count = 0; + scandir_error_fd = fd; + scandir_error_select_return = 1; + ret = fdscandir(fd, &namelist, scandir_error_select, NULL); + ATF_CHECK_EQ(-1, ret); + ATF_CHECK_ERRNO(EBADF, ret < 0); + ATF_CHECK_EQ(NULL, namelist); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, scandir_test); ATF_TP_ADD_TC(tp, fdscandir_test); ATF_TP_ADD_TC(tp, scandirat_test); ATF_TP_ADD_TC(tp, scandir_none); + ATF_TP_ADD_TC(tp, scandir_error); return (atf_no_error()); }