git: 31f9f09bf7b9 - main - fts: add fts_capsicum_test.c

Alan Somers <[email protected]>
Newsgroups gmane.os.freebsd.devel.cvs.src
Message-ID <6a846349.18331.5318604b__46723.3303746738$1787061106$gmane$org@gitrepo.freebsd.org>
The branch main has been updated by asomers:

URL: https://cgit.FreeBSD.org/src/commit/?id=31f9f09bf7b9b53533c9b731d9fade73eae379cc

commit 31f9f09bf7b9b53533c9b731d9fade73eae379cc
Author:     Jitendra Bhati <[email protected]>
AuthorDate: 2026-07-18 23:19:52 +0000
Commit:     Alan Somers <[email protected]>
CommitDate: 2026-08-18 13:50:41 +0000

    fts: add fts_capsicum_test.c
    
    Add three test cases verifying fts(3) Capsicum capability mode:
    
    - fts_dirfd_valid: verifies fts_dirfd is set for all non-root
      entries and openat(fts_dirfd, fts_name) identifies the same
      inode as fts_accpath
    - fts_dirfd_capsicum: verifies complete fts traversal works in
      Capsicum capability mode using fts_openat() and fts_dirfd
    - fts_dirfd_deep_tree: verifies fts_dirfd + fts_name is correct
      at all directory depths (7 non-root entries)
    
    Sponsored by:   Google LLC (GSoC 2026)
    Reviewed by:    asomers
    Pull Request:   https://github.com/freebsd/freebsd-src/pull/2332
---
 lib/libc/tests/gen/Makefile            |   1 +
 lib/libc/tests/gen/fts_capsicum_test.c | 210 +++++++++++++++++++++++++++++++++
 2 files changed, 211 insertions(+)

diff --git a/lib/libc/tests/gen/Makefile b/lib/libc/tests/gen/Makefile
index 1e26e46e8ddb..56d029531c54 100644
--- a/lib/libc/tests/gen/Makefile
+++ b/lib/libc/tests/gen/Makefile
@@ -13,6 +13,7 @@ ATF_TESTS_C+=		fpclassify2_test
 .if ${COMPILER_FEATURES:Mblocks}
 ATF_TESTS_C+=		fts_blocks_test
 .endif
+ATF_TESTS_C+=		fts_capsicum_test
 ATF_TESTS_C+=		fts_children_test
 ATF_TESTS_C+=		fts_misc_test
 ATF_TESTS_C+=		fts_open_test
diff --git a/lib/libc/tests/gen/fts_capsicum_test.c b/lib/libc/tests/gen/fts_capsicum_test.c
new file mode 100644
index 000000000000..d48512fc70ae
--- /dev/null
+++ b/lib/libc/tests/gen/fts_capsicum_test.c
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2026 Jitendra Bhati
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Tests for fts(3) in Capsicum capability mode using fts_dirfd.
+ */
+
+#include <sys/capsicum.h>
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <fts.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+/*
+ * Verify that fts_dirfd is set to a valid file descriptor for every
+ * entry returned by fts_read(), and that openat(fts_dirfd, fts_name)
+ * correctly identifies the same file as fts_accpath.
+ */
+ATF_TC(fts_dirfd_valid);
+ATF_TC_HEAD(fts_dirfd_valid, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "fts_dirfd is valid for all non-root entries");
+}
+ATF_TC_BODY(fts_dirfd_valid, tc)
+{
+	char *paths[] = { "dir", NULL };
+	FTS *fts;
+	FTSENT *ent;
+	struct stat sb_accpath, sb_dirfd;
+
+	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((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
+
+	while ((ent = fts_read(fts)) != NULL) {
+		if (ent->fts_info == FTS_D || ent->fts_info == FTS_DP)
+			ATF_REQUIRE_MSG(ent->fts_dirfd >= 0,
+			    "fts_dirfd must be valid for dir '%s' level %ld",
+			    ent->fts_name, ent->fts_level);
+
+		ATF_REQUIRE_EQ_MSG(0,
+		    fstatat(ent->fts_parent->fts_dirfd, ent->fts_name, &sb_dirfd,
+		    AT_SYMLINK_NOFOLLOW),
+		    "fstatat(fts_dirfd, fts_name) failed for '%s': %m",
+		    ent->fts_name);
+		ATF_REQUIRE_EQ_MSG(0,
+		    lstat(ent->fts_accpath, &sb_accpath),
+		    "lstat(fts_accpath) failed for '%s': %m",
+		    ent->fts_accpath);
+
+		ATF_CHECK_EQ_MSG(sb_accpath.st_ino, sb_dirfd.st_ino,
+		    "inode mismatch for '%s': "
+		    "fts_accpath ino=%ju fts_dirfd ino=%ju",
+		    ent->fts_name,
+		    (uintmax_t)sb_accpath.st_ino,
+		    (uintmax_t)sb_dirfd.st_ino);
+		ATF_CHECK_EQ_MSG(sb_accpath.st_dev, sb_dirfd.st_dev,
+		    "device mismatch for '%s'", ent->fts_name);
+	}
+
+	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
+}
+
+/*
+ * Verify that fts traversal works correctly in Capsicum capability
+ * mode using openat(fts_dirfd, fts_name) to access files.
+ */
+ATF_TC(fts_dirfd_capsicum);
+ATF_TC_HEAD(fts_dirfd_capsicum, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "fts traversal using fts_dirfd works in Capsicum capability mode");
+}
+ATF_TC_BODY(fts_dirfd_capsicum, tc)
+{
+	if (!feature_present("security_capabilities") ||
+	    !feature_present("security_capability_mode"))
+		atf_tc_skip("Capsicum not available");
+
+	char *paths[] = { ".", NULL };
+	FTS *fts;
+	FTSENT *ent;
+	int dirfd;
+	bool saw_file, saw_sub;
+
+	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);
+
+	/* Enter capability mode before fts_openat. */
+	ATF_REQUIRE_EQ(0, cap_enter());
+
+	ATF_REQUIRE((fts = fts_openat(dirfd, paths,
+	    FTS_PHYSICAL | FTS_NOCHDIR, NULL)) != NULL);
+	close(dirfd);
+
+	saw_file = false;
+	saw_sub = false;
+
+	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)
+			saw_file = true;
+
+		if (ent->fts_level == FTS_ROOTLEVEL)
+			continue;
+
+		struct stat sb;
+		ATF_CHECK_EQ_MSG(0,
+		    fstatat(ent->fts_parent->fts_dirfd, ent->fts_name, &sb,
+		    AT_SYMLINK_NOFOLLOW),
+		    "fstatat(fts_dirfd, fts_name) failed for "
+		    "'%s' in capability mode: %m",
+		    ent->fts_name);
+	}
+
+	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");
+}
+
+/*
+ * Verify that fts_dirfd + fts_name correctly identifies files even
+ * when fts has internally chdir'd into subdirectories.
+ */
+ATF_TC(fts_dirfd_deep_tree);
+ATF_TC_HEAD(fts_dirfd_deep_tree, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "fts_dirfd + fts_name is correct at all directory depths");
+}
+ATF_TC_BODY(fts_dirfd_deep_tree, tc)
+{
+	char *paths[] = { "dir", NULL };
+	FTS *fts;
+	FTSENT *ent;
+	struct stat sb_dirfd, sb_accpath;
+	int depth_checked = 0;
+
+	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+	ATF_REQUIRE_EQ(0, mkdir("dir/a", 0755));
+	ATF_REQUIRE_EQ(0, mkdir("dir/a/b", 0755));
+	ATF_REQUIRE_EQ(0, mkdir("dir/a/b/c", 0755));
+	ATF_REQUIRE_EQ(0, close(creat("dir/a/b/c/deep", 0644)));
+	ATF_REQUIRE_EQ(0, close(creat("dir/a/b/mid", 0644)));
+	ATF_REQUIRE_EQ(0, close(creat("dir/a/top", 0644)));
+	ATF_REQUIRE_EQ(0, close(creat("dir/root", 0644)));
+
+	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
+
+	while ((ent = fts_read(fts)) != NULL) {
+		if (ent->fts_info == FTS_DP ||
+		    ent->fts_level == FTS_ROOTLEVEL)
+			continue;
+
+		ATF_REQUIRE_MSG(ent->fts_parent->fts_dirfd >= 0,
+		    "fts_parent->fts_dirfd must be valid for '%s' at level %ld",
+		    ent->fts_name, ent->fts_level);
+
+		ATF_REQUIRE_EQ_MSG(0,
+		    fstatat(ent->fts_parent->fts_dirfd, ent->fts_name, &sb_dirfd,
+		    AT_SYMLINK_NOFOLLOW),
+		    "fstatat failed for '%s': %m", ent->fts_name);
+		ATF_REQUIRE_EQ_MSG(0,
+		    lstat(ent->fts_accpath, &sb_accpath),
+		    "lstat failed for '%s': %m", ent->fts_accpath);
+
+		ATF_CHECK_EQ_MSG(sb_accpath.st_ino, sb_dirfd.st_ino,
+		    "inode mismatch at depth %ld for '%s'",
+		    ent->fts_level, ent->fts_name);
+
+		depth_checked++;
+	}
+
+	/* 4 files (deep, mid, top, root) + 3 dirs (a, b, c) = 7 entries */
+	ATF_CHECK_EQ_MSG(7, depth_checked,
+	    "expected 7 entries, got %d", depth_checked);
+
+	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, fts_dirfd_valid);
+	ATF_TP_ADD_TC(tp, fts_dirfd_capsicum);
+	ATF_TP_ADD_TC(tp, fts_dirfd_deep_tree);
+
+	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.