xref: /freebsd/lib/libc/tests/gen/fts_capsicum_test.c (revision 31f9f09bf7b9b53533c9b731d9fade73eae379cc)
1 /*
2  * Copyright (c) 2026 Jitendra Bhati
3  *
4  * SPDX-License-Identifier: BSD-2-Clause
5  *
6  * Tests for fts(3) in Capsicum capability mode using fts_dirfd.
7  */
8 
9 #include <sys/capsicum.h>
10 #include <sys/stat.h>
11 
12 #include <errno.h>
13 #include <fcntl.h>
14 #include <fts.h>
15 #include <stdbool.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <unistd.h>
19 
20 #include <atf-c.h>
21 
22 /*
23  * Verify that fts_dirfd is set to a valid file descriptor for every
24  * entry returned by fts_read(), and that openat(fts_dirfd, fts_name)
25  * correctly identifies the same file as fts_accpath.
26  */
27 ATF_TC(fts_dirfd_valid);
ATF_TC_HEAD(fts_dirfd_valid,tc)28 ATF_TC_HEAD(fts_dirfd_valid, tc)
29 {
30 	atf_tc_set_md_var(tc, "descr",
31 	    "fts_dirfd is valid for all non-root entries");
32 }
ATF_TC_BODY(fts_dirfd_valid,tc)33 ATF_TC_BODY(fts_dirfd_valid, tc)
34 {
35 	char *paths[] = { "dir", NULL };
36 	FTS *fts;
37 	FTSENT *ent;
38 	struct stat sb_accpath, sb_dirfd;
39 
40 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
41 	ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755));
42 	ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644)));
43 	ATF_REQUIRE_EQ(0, close(creat("dir/other", 0644)));
44 
45 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
46 
47 	while ((ent = fts_read(fts)) != NULL) {
48 		if (ent->fts_info == FTS_D || ent->fts_info == FTS_DP)
49 			ATF_REQUIRE_MSG(ent->fts_dirfd >= 0,
50 			    "fts_dirfd must be valid for dir '%s' level %ld",
51 			    ent->fts_name, ent->fts_level);
52 
53 		ATF_REQUIRE_EQ_MSG(0,
54 		    fstatat(ent->fts_parent->fts_dirfd, ent->fts_name, &sb_dirfd,
55 		    AT_SYMLINK_NOFOLLOW),
56 		    "fstatat(fts_dirfd, fts_name) failed for '%s': %m",
57 		    ent->fts_name);
58 		ATF_REQUIRE_EQ_MSG(0,
59 		    lstat(ent->fts_accpath, &sb_accpath),
60 		    "lstat(fts_accpath) failed for '%s': %m",
61 		    ent->fts_accpath);
62 
63 		ATF_CHECK_EQ_MSG(sb_accpath.st_ino, sb_dirfd.st_ino,
64 		    "inode mismatch for '%s': "
65 		    "fts_accpath ino=%ju fts_dirfd ino=%ju",
66 		    ent->fts_name,
67 		    (uintmax_t)sb_accpath.st_ino,
68 		    (uintmax_t)sb_dirfd.st_ino);
69 		ATF_CHECK_EQ_MSG(sb_accpath.st_dev, sb_dirfd.st_dev,
70 		    "device mismatch for '%s'", ent->fts_name);
71 	}
72 
73 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
74 }
75 
76 /*
77  * Verify that fts traversal works correctly in Capsicum capability
78  * mode using openat(fts_dirfd, fts_name) to access files.
79  */
80 ATF_TC(fts_dirfd_capsicum);
ATF_TC_HEAD(fts_dirfd_capsicum,tc)81 ATF_TC_HEAD(fts_dirfd_capsicum, tc)
82 {
83 	atf_tc_set_md_var(tc, "descr",
84 	    "fts traversal using fts_dirfd works in Capsicum capability mode");
85 }
ATF_TC_BODY(fts_dirfd_capsicum,tc)86 ATF_TC_BODY(fts_dirfd_capsicum, tc)
87 {
88 	if (!feature_present("security_capabilities") ||
89 	    !feature_present("security_capability_mode"))
90 		atf_tc_skip("Capsicum not available");
91 
92 	char *paths[] = { ".", NULL };
93 	FTS *fts;
94 	FTSENT *ent;
95 	int dirfd;
96 	bool saw_file, saw_sub;
97 
98 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
99 	ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755));
100 	ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644)));
101 	ATF_REQUIRE_EQ(0, close(creat("dir/other", 0644)));
102 
103 	ATF_REQUIRE((dirfd = open("dir", O_RDONLY | O_DIRECTORY)) >= 0);
104 
105 	/* Enter capability mode before fts_openat. */
106 	ATF_REQUIRE_EQ(0, cap_enter());
107 
108 	ATF_REQUIRE((fts = fts_openat(dirfd, paths,
109 	    FTS_PHYSICAL | FTS_NOCHDIR, NULL)) != NULL);
110 	close(dirfd);
111 
112 	saw_file = false;
113 	saw_sub = false;
114 
115 	while ((ent = fts_read(fts)) != NULL) {
116 		if (ent->fts_info == FTS_DP)
117 			continue;
118 
119 		if (strcmp(ent->fts_name, "sub") == 0 &&
120 		    ent->fts_info == FTS_D)
121 			saw_sub = true;
122 
123 		if (strcmp(ent->fts_name, "file") == 0)
124 			saw_file = true;
125 
126 		if (ent->fts_level == FTS_ROOTLEVEL)
127 			continue;
128 
129 		struct stat sb;
130 		ATF_CHECK_EQ_MSG(0,
131 		    fstatat(ent->fts_parent->fts_dirfd, ent->fts_name, &sb,
132 		    AT_SYMLINK_NOFOLLOW),
133 		    "fstatat(fts_dirfd, fts_name) failed for "
134 		    "'%s' in capability mode: %m",
135 		    ent->fts_name);
136 	}
137 
138 	ATF_CHECK_MSG(saw_sub, "must have visited 'sub' directory");
139 	ATF_CHECK_MSG(saw_file, "must have visited 'file'");
140 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
141 }
142 
143 /*
144  * Verify that fts_dirfd + fts_name correctly identifies files even
145  * when fts has internally chdir'd into subdirectories.
146  */
147 ATF_TC(fts_dirfd_deep_tree);
ATF_TC_HEAD(fts_dirfd_deep_tree,tc)148 ATF_TC_HEAD(fts_dirfd_deep_tree, tc)
149 {
150 	atf_tc_set_md_var(tc, "descr",
151 	    "fts_dirfd + fts_name is correct at all directory depths");
152 }
ATF_TC_BODY(fts_dirfd_deep_tree,tc)153 ATF_TC_BODY(fts_dirfd_deep_tree, tc)
154 {
155 	char *paths[] = { "dir", NULL };
156 	FTS *fts;
157 	FTSENT *ent;
158 	struct stat sb_dirfd, sb_accpath;
159 	int depth_checked = 0;
160 
161 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
162 	ATF_REQUIRE_EQ(0, mkdir("dir/a", 0755));
163 	ATF_REQUIRE_EQ(0, mkdir("dir/a/b", 0755));
164 	ATF_REQUIRE_EQ(0, mkdir("dir/a/b/c", 0755));
165 	ATF_REQUIRE_EQ(0, close(creat("dir/a/b/c/deep", 0644)));
166 	ATF_REQUIRE_EQ(0, close(creat("dir/a/b/mid", 0644)));
167 	ATF_REQUIRE_EQ(0, close(creat("dir/a/top", 0644)));
168 	ATF_REQUIRE_EQ(0, close(creat("dir/root", 0644)));
169 
170 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
171 
172 	while ((ent = fts_read(fts)) != NULL) {
173 		if (ent->fts_info == FTS_DP ||
174 		    ent->fts_level == FTS_ROOTLEVEL)
175 			continue;
176 
177 		ATF_REQUIRE_MSG(ent->fts_parent->fts_dirfd >= 0,
178 		    "fts_parent->fts_dirfd must be valid for '%s' at level %ld",
179 		    ent->fts_name, ent->fts_level);
180 
181 		ATF_REQUIRE_EQ_MSG(0,
182 		    fstatat(ent->fts_parent->fts_dirfd, ent->fts_name, &sb_dirfd,
183 		    AT_SYMLINK_NOFOLLOW),
184 		    "fstatat failed for '%s': %m", ent->fts_name);
185 		ATF_REQUIRE_EQ_MSG(0,
186 		    lstat(ent->fts_accpath, &sb_accpath),
187 		    "lstat failed for '%s': %m", ent->fts_accpath);
188 
189 		ATF_CHECK_EQ_MSG(sb_accpath.st_ino, sb_dirfd.st_ino,
190 		    "inode mismatch at depth %ld for '%s'",
191 		    ent->fts_level, ent->fts_name);
192 
193 		depth_checked++;
194 	}
195 
196 	/* 4 files (deep, mid, top, root) + 3 dirs (a, b, c) = 7 entries */
197 	ATF_CHECK_EQ_MSG(7, depth_checked,
198 	    "expected 7 entries, got %d", depth_checked);
199 
200 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
201 }
202 
ATF_TP_ADD_TCS(tp)203 ATF_TP_ADD_TCS(tp)
204 {
205 	ATF_TP_ADD_TC(tp, fts_dirfd_valid);
206 	ATF_TP_ADD_TC(tp, fts_dirfd_capsicum);
207 	ATF_TP_ADD_TC(tp, fts_dirfd_deep_tree);
208 
209 	return (atf_no_error());
210 }
211