git: 9aed7a774551 - main - fts: add fts_openat() API
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 18 Aug 2026 00:22:59 UTC
The branch main has been updated by asomers:
URL: https://cgit.FreeBSD.org/src/commit/?id=9aed7a7745512ca098cc2b247cef61ad57d63204
commit 9aed7a7745512ca098cc2b247cef61ad57d63204
Author: Jitendra Bhati <bhatijitendra2022@gmail.com>
AuthorDate: 2026-06-12 20:48:17 +0000
Commit: Alan Somers <asomers@FreeBSD.org>
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());
+}