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
This commit is contained in:
Jitendra Bhati authored and Alan Somers committed 2026-08-17 18:17:45 -06:00
1 parent 9b8c2bffeb
commit 9aed7a7745
7 files changed
+346 -13

No files matched your search

+3 -1
View File
@@ -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 *));
+1
View File
@@ -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 \
+1
View File
@@ -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;
+33 -1
View File
@@ -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
+57 -11
View File
@@ -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;
+1
View File
@@ -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
+250
View File
@@ -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());
}