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());
+}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.