git: 9aed7a774551 - main - fts: add fts_openat() API
Alan Somers <[email protected]>
| Newsgroups | gmane.os.freebsd.devel.cvs.src |
|---|---|
| Message-ID | <6a83a5e3.420a2.6566a6b7__7062.9238545493$1787012616$gmane$org@gitrepo.freebsd.org> |
The branch main has been updated by asomers: URL: https://cgit.FreeBSD.org/src/commit/?id=9aed7a7745512ca098cc2b247cef61ad57d63204 commit 9aed7a7745512ca098cc2b247cef61ad57d63204 Author: Jitendra Bhati <[email protected]> AuthorDate: 2026-06-12 20:48:17 +0000 Commit: Alan Somers <[email protected]> CommitDate: 2026-08-18 00:17:45 +0000 fts: add fts_openat() API Add fts_openat() as a new entry point for fts(3). When dirfd is AT_FDCWD the behaviour is identical to fts_open(). Passing a pre-opened directory fd allows fts traversal inside Capsicum capability mode where path-based operations are not permitted. Capability mode users should use fts_parent->fts_dirfd + fts_name with openat(2) to access files. Reviewed by: asomers Relnotes: yes Sponsored by: Google LLC (GSoC 2026) Pull Request: https://github.com/freebsd/freebsd-src/pull/2273 --- include/fts.h | 4 +- lib/libc/gen/Makefile.inc | 1 + lib/libc/gen/Symbol.map | 1 + lib/libc/gen/fts.3 | 34 ++++- lib/libc/gen/fts.c | 68 ++++++++-- lib/libc/tests/gen/Makefile | 1 + lib/libc/tests/gen/fts_openat_test.c | 250 +++++++++++++++++++++++++++++++++++ 7 files changed, 346 insertions(+), 13 deletions(-) diff --git a/include/fts.h b/include/fts.h index 7d01b7a195c7..4f16ad029a30 100644 --- a/include/fts.h +++ b/include/fts.h @@ -1,4 +1,4 @@ -/*- +/* * SPDX-License-Identifier: BSD-3-Clause * * Copyright (c) 1989, 1993 @@ -148,6 +148,8 @@ FTS *fts_get_stream(FTSENT *); #define fts_get_stream(ftsent) ((ftsent)->fts_fts) FTS *fts_open(char * const *, int, int (*)(const FTSENT * const *, const FTSENT * const *)); +FTS *fts_openat(int, char * const *, int, + int (*)(const FTSENT * const *, const FTSENT * const *)); #ifdef __BLOCKS__ FTS *fts_open_b(char * const *, int, int (^)(const FTSENT * const *, const FTSENT * const *)); diff --git a/lib/libc/gen/Makefile.inc b/lib/libc/gen/Makefile.inc index 582e519df0e5..f60425f27318 100644 --- a/lib/libc/gen/Makefile.inc +++ b/lib/libc/gen/Makefile.inc @@ -389,6 +389,7 @@ MLINKS+=frexp.3 frexpf.3 \ MLINKS+=fts.3 fts_children.3 \ fts.3 fts_close.3 \ fts.3 fts_open.3 \ + fts.3 fts_openat.3 \ fts.3 fts_read.3 \ fts.3 fts_set.3 \ fts.3 fts_set_clientptr.3 \ diff --git a/lib/libc/gen/Symbol.map b/lib/libc/gen/Symbol.map index 8c4853737064..ff3653945296 100644 --- a/lib/libc/gen/Symbol.map +++ b/lib/libc/gen/Symbol.map @@ -476,6 +476,7 @@ FBSD_1.9 { fts_read; fts_set; fts_set_clientptr; + fts_openat; posix_spawn_file_actions_addchdir; posix_spawn_file_actions_addfchdir; posix_spawnattr_getexecfd_np; diff --git a/lib/libc/gen/fts.3 b/lib/libc/gen/fts.3 index eb204a0dd3ac..94f10323543d 100644 --- a/lib/libc/gen/fts.3 +++ b/lib/libc/gen/fts.3 @@ -25,7 +25,7 @@ .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF .\" SUCH DAMAGE. .\" -.Dd August 9, 2026 +.Dd August 13, 2026 .Dt FTS 3 .Os .Sh NAME @@ -38,6 +38,8 @@ .Ft FTS * .Fn fts_open "char * const *path_argv" "int options" "int (*compar)(const FTSENT * const *, const FTSENT * const *)" .Ft FTS * +.Fn fts_openat "int dirfd" "char * const *path_argv" "int options" "int (*compar)(const FTSENT * const *, const FTSENT * const *)" +.Ft FTS * .Fn fts_open_b "char * const *path_argv" "int options" "int (^compar)(const FTSENT * const *, const FTSENT * const *)" .Ft FTSENT * .Fn fts_read "FTS *ftsp" @@ -621,6 +623,36 @@ except that it takes a block pointer instead of a function pointer. The block is copied before .Fn fts_open_b returns, so the original can safely go out of scope or be released. +.Ss Fn fts_openat +The +.Fn fts_openat +function is identical to +.Fn fts_open +except that it accepts a file descriptor +.Fa dirfd +as its first argument. +If +.Fa dirfd +is +.Dv AT_FDCWD , +the behaviour is identical to +.Fn fts_open . +Otherwise, +.Fa dirfd +must be an open file descriptor referring to a directory, +and the traversal is rooted there. +This allows +.Fn fts_openat +to be used inside Capsicum capability mode +.Pq Xr capsicum 4 , +where path-based operations are not permitted. +The +.Fn fts_openat +function duplicates +.Fa dirfd +internally, so the caller may close it after +.Fn fts_openat +returns. .Ss Fn fts_read The .Fn fts_read diff --git a/lib/libc/gen/fts.c b/lib/libc/gen/fts.c index 66c5f62d9a8f..090250e433ef 100644 --- a/lib/libc/gen/fts.c +++ b/lib/libc/gen/fts.c @@ -155,6 +155,10 @@ __fts_open(FTS *sp, char * const *argv, int rootfd) if ((parent = fts_alloc(sp, "", 0)) == NULL) goto mem2; parent->fts_level = FTS_ROOTPARENTLEVEL; + parent->fts_dirfd = AT_FDCWD; + + if (rootfd != AT_FDCWD) + parent->fts_dirfd = rootfd; /* Shush, GCC. */ tmp = NULL; @@ -169,11 +173,20 @@ __fts_open(FTS *sp, char * const *argv, int rootfd) p->fts_accpath = p->fts_name; p->fts_info = fts_stat(sp, p, ISSET(FTS_COMFOLLOWDIR) ? -1 : ISSET(FTS_COMFOLLOW), - -1); + rootfd == AT_FDCWD ? -1 : rootfd); /* Command-line "." and ".." are real directories. */ if (p->fts_info == FTS_DOT) p->fts_info = FTS_D; + if (p->fts_info == FTS_D) { + if (strcmp(p->fts_name, ".") == 0) + p->fts_dirfd = rootfd != AT_FDCWD ? + _dup(rootfd) : + _open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + else + p->fts_dirfd = _openat(rootfd, p->fts_name, + O_RDONLY | O_DIRECTORY | O_CLOEXEC); + } /* * If comparison routine supplied, traverse in sorted @@ -213,10 +226,16 @@ __fts_open(FTS *sp, char * const *argv, int rootfd) * descriptor we run anyway, just more slowly. We use _openat rather * than _dup because rootfd may be AT_FDCWD, not a real descriptor. */ - if (!ISSET(FTS_NOCHDIR) && - (sp->fts_rfd = _openat(rootfd, ".", O_RDONLY | - O_CLOEXEC, 0)) < 0) - SET(FTS_NOCHDIR); + + if (!ISSET(FTS_NOCHDIR)) { + if (rootfd != AT_FDCWD) + sp->fts_rfd = _dup(rootfd); + if (sp->fts_rfd < 0) + SET(FTS_NOCHDIR); + else if ((sp->fts_rfd = + _open(".", O_RDONLY | O_CLOEXEC, 0)) < 0) + SET(FTS_NOCHDIR); + } return (sp); mem3: fts_lfree(root); @@ -229,9 +248,17 @@ mem1: free(sp); FTS * fts_open(char * const *argv, int options, int (*compar)(const FTSENT * const *, const FTSENT * const *)) +{ + return (fts_openat(AT_FDCWD, argv, options, compar)); +} + +FTS * +fts_openat(int dirfd, char * const *argv, int options, + int (*compar)(const FTSENT * const *, const FTSENT * const *)) { struct _fts_private *priv; FTS *sp; + int rootfd; /* Options check. */ if (options & ~FTS_OPTIONMASK) { @@ -239,7 +266,7 @@ fts_open(char * const *argv, int options, return (NULL); } - /* fts_open() requires at least one path */ + /* fts_openat() requires at least one path */ if (*argv == NULL) { errno = EINVAL; return (NULL); @@ -251,10 +278,15 @@ fts_open(char * const *argv, int options, sp = &priv->ftsp_fts; sp->fts_compar = compar; sp->fts_options = options; + if (dirfd == AT_FDCWD) + rootfd = AT_FDCWD; + else if ((rootfd = _dup(dirfd)) < 0) { + free(priv); + return (NULL); + } + return (__fts_open(sp, argv, rootfd)); - return (__fts_open(sp, argv, AT_FDCWD)); } - #ifdef __BLOCKS__ FTS * fts_open_b(char * const *argv, int options, @@ -781,7 +813,16 @@ fts_build(FTS *sp, int type) oflag = DTF_NODUP; else oflag = DTF_HIDEW | DTF_NODUP; - if ((dirp = __opendir2(cur->fts_accpath, oflag)) == NULL) { + if (cur->fts_dirfd >= 0) { + int fd; + fd = _openat(cur->fts_dirfd, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (fd >= 0) + dirp = fdopendir(fd); + else + dirp = __opendir2(cur->fts_accpath, oflag); + } else + dirp = __opendir2(cur->fts_accpath, oflag); + if (dirp == NULL) { if (type == BREAD) { cur->fts_info = FTS_DNR; cur->fts_errno = errno; @@ -789,7 +830,8 @@ fts_build(FTS *sp, int type) return (NULL); } - cur->fts_dirfd = _dup(_dirfd(dirp)); + if (cur->fts_dirfd < 0) + cur->fts_dirfd = _dup(_dirfd(dirp)); /* * In the FTS_PHYSICAL | FTS_NOSTAT case, we want to avoid calling @@ -927,8 +969,12 @@ mem1: saved_errno = errno; p->fts_level = level; p->fts_parent = sp->fts_cur; + if (dp->d_type == DT_DIR) { + p->fts_dirfd = _openat(_dirfd(dirp), + p->fts_name, + O_RDONLY | O_DIRECTORY | O_CLOEXEC); + } p->fts_pathlen = len + dnamlen; - if (dp->d_type == DT_WHT) p->fts_flags |= FTS_ISW; diff --git a/lib/libc/tests/gen/Makefile b/lib/libc/tests/gen/Makefile index e451fbea644c..1e26e46e8ddb 100644 --- a/lib/libc/tests/gen/Makefile +++ b/lib/libc/tests/gen/Makefile @@ -16,6 +16,7 @@ ATF_TESTS_C+= fts_blocks_test ATF_TESTS_C+= fts_children_test ATF_TESTS_C+= fts_misc_test ATF_TESTS_C+= fts_open_test +ATF_TESTS_C+= fts_openat_test ATF_TESTS_C+= fts_options_test ATF_TESTS_C+= fts_regress_test ATF_TESTS_C+= fts_set_test diff --git a/lib/libc/tests/gen/fts_openat_test.c b/lib/libc/tests/gen/fts_openat_test.c new file mode 100644 index 000000000000..f6b6806d216f --- /dev/null +++ b/lib/libc/tests/gen/fts_openat_test.c @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2026 Jitendra Bhati + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/* + * Basic tests for fts_openat(). When called with AT_FDCWD the + * behaviour must be identical to fts_open(). + */ + +#include <sys/stat.h> + +#include <limits.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <fcntl.h> +#include <errno.h> +#include <fts.h> +#include <string.h> +#include <unistd.h> +#include <sys/capsicum.h> + +#include <atf-c.h> + +#define FTS_TEST_MAXENTRIES 64 + +static int +fts_lexical_compar(const FTSENT * const *a, const FTSENT * const *b) +{ + return (strcmp((*a)->fts_name, (*b)->fts_name)); +} + +/* + * fts_openat(AT_FDCWD, ...) must behave identically to fts_open(). + */ +ATF_TC(atfdcwd_matches_fts_open); +ATF_TC_HEAD(atfdcwd_matches_fts_open, tc) +{ + atf_tc_set_md_var(tc, "descr", + "fts_openat(AT_FDCWD) behaves identically to fts_open"); +} + +ATF_TC_BODY(atfdcwd_matches_fts_open, tc) +{ + char *cwd, *abspath; + char *paths[2]; + FTS *fts; + FTSENT *ent; + + int *info1, *info2; + char (*names1)[NAME_MAX + 1], (*names2)[NAME_MAX + 1]; + int n1, n2, i; + + ATF_REQUIRE((info1 = malloc(FTS_TEST_MAXENTRIES * + sizeof(*info1))) != NULL); + ATF_REQUIRE((info2 = malloc(FTS_TEST_MAXENTRIES * + sizeof(*info2))) != NULL); + ATF_REQUIRE((names1 = malloc(FTS_TEST_MAXENTRIES * + sizeof(*names1))) != NULL); + ATF_REQUIRE((names2 = malloc(FTS_TEST_MAXENTRIES * + sizeof(*names2))) != NULL); + + cwd = malloc(PATH_MAX); + ATF_REQUIRE(cwd != NULL); + abspath = malloc(PATH_MAX * 2); + ATF_REQUIRE(abspath != NULL); + + ATF_REQUIRE(getcwd(cwd, PATH_MAX) != NULL); + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644))); + ATF_REQUIRE_EQ(0, close(creat("dir/other", 0644))); + + snprintf(abspath, PATH_MAX * 2, "%s/dir", cwd); + paths[0] = abspath; + paths[1] = NULL; + + /* Collect fts_open results. */ + ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, + fts_lexical_compar)) != NULL); + for (n1 = 0; + (ent = fts_read(fts)) != NULL && n1 < FTS_TEST_MAXENTRIES; + n1++) { + info1[n1] = ent->fts_info; + strlcpy(names1[n1], ent->fts_name, NAME_MAX + 1); + } + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close: %m"); + + /* Collect fts_openat results. */ + ATF_REQUIRE((fts = fts_openat(AT_FDCWD, paths, FTS_PHYSICAL, + fts_lexical_compar)) != NULL); + for (n2 = 0; + (ent = fts_read(fts)) != NULL && n2 < FTS_TEST_MAXENTRIES; + n2++) { + info2[n2] = ent->fts_info; + strlcpy(names2[n2], ent->fts_name, NAME_MAX + 1); + } + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close: %m"); + + /* Compare. */ + ATF_CHECK_EQ_MSG(n1, n2, + "entry count mismatch: fts_open=%d fts_openat=%d", n1, n2); + for (i = 0; i < n1 && i < n2; i++) { + ATF_CHECK_EQ_MSG(info1[i], info2[i], + "fts_info mismatch at entry %d: " + "fts_open=%d fts_openat=%d name=%s", + i, info1[i], info2[i], names1[i]); + ATF_CHECK_STREQ_MSG(names1[i], names2[i], + "fts_name mismatch at entry %d: " + "fts_open='%s' fts_openat='%s'", + i, names1[i], names2[i]); + } + + free(cwd); + free(abspath); + free(info1); + free(info2); + free(names1); + free(names2); +} + +/* + * fts_openat() with a real dirfd must work in Capsicum capability mode. + */ +ATF_TC(openat_capsicum); +ATF_TC_HEAD(openat_capsicum, tc) +{ + atf_tc_set_md_var(tc, "descr", + "fts_openat() with dirfd works in Capsicum capability mode"); +} +ATF_TC_BODY(openat_capsicum, tc) +{ + char *paths[] = { ".", NULL }; + FTS *fts; + FTSENT *ent; + int dirfd; + bool saw_file = false, saw_sub = false; + + if (!feature_present("security_capabilities") || + !feature_present("security_capability_mode")) + atf_tc_skip("Capsicum not available"); + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644))); + ATF_REQUIRE_EQ(0, close(creat("dir/other", 0644))); + + ATF_REQUIRE((dirfd = open("dir", O_RDONLY | O_DIRECTORY)) >= 0); + ATF_REQUIRE_EQ(0, cap_enter()); + ATF_REQUIRE((fts = fts_openat(dirfd, paths, + FTS_PHYSICAL | FTS_NOCHDIR, NULL)) != NULL); + + while ((ent = fts_read(fts)) != NULL) { + if (ent->fts_info == FTS_DP) + continue; + if (strcmp(ent->fts_name, "sub") == 0 && + ent->fts_info == FTS_D) + saw_sub = true; + if (strcmp(ent->fts_name, "file") == 0 && + ent->fts_info == FTS_F) + saw_file = true; + } + + ATF_CHECK_MSG(saw_sub, "must have visited 'sub' directory"); + ATF_CHECK_MSG(saw_file, "must have visited 'file'"); + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); +} + +/* + * Demonstrate the intended use of fts_dirfd: use + * fts_parent->fts_dirfd + fts_name to access files without + * relying on path-based operations. + */ +ATF_TC(fts_dirfd_openat); +ATF_TC_HEAD(fts_dirfd_openat, tc) +{ + atf_tc_set_md_var(tc, "descr", + "fts_parent->fts_dirfd + fts_name can be used with openat(2)"); +} + +ATF_TC_BODY(fts_dirfd_openat, tc) +{ + char *paths[] = { "dir1", "dir2", NULL }; + FTS *fts; + FTSENT *ent; + struct stat sb_path, sb_dirfd; + int dirfd; + int nvisited = 0; + + ATF_REQUIRE_EQ(0, mkdir("dir1", 0755)); + ATF_REQUIRE_EQ(0, mkdir("dir1/sub", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir1/sub/file", 0644))); + ATF_REQUIRE_EQ(0, mkdir("dir2", 0755)); + ATF_REQUIRE_EQ(0, close(creat("dir2/file2", 0644))); + + /* + * Open the current working directory as dirfd. fts_openat + * stores it in parent->fts_dirfd, allowing children to use + * fts_parent->fts_dirfd + fts_name with openat(2). + */ + ATF_REQUIRE((dirfd = open(".", O_RDONLY | O_DIRECTORY)) >= 0); + ATF_REQUIRE((fts = fts_openat(dirfd, paths, + FTS_PHYSICAL, NULL)) != NULL); + close(dirfd); + + while ((ent = fts_read(fts)) != NULL) { + if (ent->fts_info == FTS_DP) + continue; + if (ent->fts_level == FTS_ROOTLEVEL) + continue; + + ATF_REQUIRE_MSG(ent->fts_parent->fts_dirfd >= 0, + "fts_parent->fts_dirfd must be valid for '%s'", + ent->fts_name); + + ATF_REQUIRE_EQ_MSG(0, + fstatat(ent->fts_parent->fts_dirfd, ent->fts_name, + &sb_dirfd, AT_SYMLINK_NOFOLLOW), + "fstatat(fts_parent->fts_dirfd, '%s') failed: %m", + ent->fts_name); + + ATF_REQUIRE_EQ_MSG(0, + lstat(ent->fts_accpath, &sb_path), + "lstat('%s') failed: %m", ent->fts_accpath); + + ATF_CHECK_EQ_MSG(sb_path.st_ino, sb_dirfd.st_ino, + "inode mismatch for '%s': accpath=%ju dirfd=%ju", + ent->fts_name, + (uintmax_t)sb_path.st_ino, + (uintmax_t)sb_dirfd.st_ino); + + nvisited++; + } + + ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m"); + + /* dir1/sub, dir1/sub/file, dir2/file2 = 3 entries */ + ATF_CHECK_EQ_MSG(3, nvisited, + "expected 3 entries, got %d", nvisited); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, atfdcwd_matches_fts_open); + ATF_TP_ADD_TC(tp, openat_capsicum); + ATF_TP_ADD_TC(tp, fts_dirfd_openat); + return (atf_no_error()); +}