1 /*
2 * Copyright (c) 2026 Jitendra Bhati
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7 /*
8 * Basic tests for fts_openat(). When called with AT_FDCWD the
9 * behaviour must be identical to fts_open().
10 */
11
12 #include <sys/stat.h>
13
14 #include <limits.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <stdbool.h>
18 #include <fcntl.h>
19 #include <errno.h>
20 #include <fts.h>
21 #include <string.h>
22 #include <unistd.h>
23 #include <sys/capsicum.h>
24
25 #include <atf-c.h>
26
27 #define FTS_TEST_MAXENTRIES 64
28
29 static int
fts_lexical_compar(const FTSENT * const * a,const FTSENT * const * b)30 fts_lexical_compar(const FTSENT * const *a, const FTSENT * const *b)
31 {
32 return (strcmp((*a)->fts_name, (*b)->fts_name));
33 }
34
35 /*
36 * fts_openat(AT_FDCWD, ...) must behave identically to fts_open().
37 */
38 ATF_TC(atfdcwd_matches_fts_open);
ATF_TC_HEAD(atfdcwd_matches_fts_open,tc)39 ATF_TC_HEAD(atfdcwd_matches_fts_open, tc)
40 {
41 atf_tc_set_md_var(tc, "descr",
42 "fts_openat(AT_FDCWD) behaves identically to fts_open");
43 }
44
ATF_TC_BODY(atfdcwd_matches_fts_open,tc)45 ATF_TC_BODY(atfdcwd_matches_fts_open, tc)
46 {
47 char *cwd, *abspath;
48 char *paths[2];
49 FTS *fts;
50 FTSENT *ent;
51
52 int *info1, *info2;
53 char (*names1)[NAME_MAX + 1], (*names2)[NAME_MAX + 1];
54 int n1, n2, i;
55
56 ATF_REQUIRE((info1 = malloc(FTS_TEST_MAXENTRIES *
57 sizeof(*info1))) != NULL);
58 ATF_REQUIRE((info2 = malloc(FTS_TEST_MAXENTRIES *
59 sizeof(*info2))) != NULL);
60 ATF_REQUIRE((names1 = malloc(FTS_TEST_MAXENTRIES *
61 sizeof(*names1))) != NULL);
62 ATF_REQUIRE((names2 = malloc(FTS_TEST_MAXENTRIES *
63 sizeof(*names2))) != NULL);
64
65 cwd = malloc(PATH_MAX);
66 ATF_REQUIRE(cwd != NULL);
67 abspath = malloc(PATH_MAX * 2);
68 ATF_REQUIRE(abspath != NULL);
69
70 ATF_REQUIRE(getcwd(cwd, PATH_MAX) != NULL);
71 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
72 ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755));
73 ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644)));
74 ATF_REQUIRE_EQ(0, close(creat("dir/other", 0644)));
75
76 snprintf(abspath, PATH_MAX * 2, "%s/dir", cwd);
77 paths[0] = abspath;
78 paths[1] = NULL;
79
80 /* Collect fts_open results. */
81 ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
82 fts_lexical_compar)) != NULL);
83 for (n1 = 0;
84 (ent = fts_read(fts)) != NULL && n1 < FTS_TEST_MAXENTRIES;
85 n1++) {
86 info1[n1] = ent->fts_info;
87 strlcpy(names1[n1], ent->fts_name, NAME_MAX + 1);
88 }
89 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close: %m");
90
91 /* Collect fts_openat results. */
92 ATF_REQUIRE((fts = fts_openat(AT_FDCWD, paths, FTS_PHYSICAL,
93 fts_lexical_compar)) != NULL);
94 for (n2 = 0;
95 (ent = fts_read(fts)) != NULL && n2 < FTS_TEST_MAXENTRIES;
96 n2++) {
97 info2[n2] = ent->fts_info;
98 strlcpy(names2[n2], ent->fts_name, NAME_MAX + 1);
99 }
100 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close: %m");
101
102 /* Compare. */
103 ATF_CHECK_EQ_MSG(n1, n2,
104 "entry count mismatch: fts_open=%d fts_openat=%d", n1, n2);
105 for (i = 0; i < n1 && i < n2; i++) {
106 ATF_CHECK_EQ_MSG(info1[i], info2[i],
107 "fts_info mismatch at entry %d: "
108 "fts_open=%d fts_openat=%d name=%s",
109 i, info1[i], info2[i], names1[i]);
110 ATF_CHECK_STREQ_MSG(names1[i], names2[i],
111 "fts_name mismatch at entry %d: "
112 "fts_open='%s' fts_openat='%s'",
113 i, names1[i], names2[i]);
114 }
115
116 free(cwd);
117 free(abspath);
118 free(info1);
119 free(info2);
120 free(names1);
121 free(names2);
122 }
123
124 /*
125 * fts_openat() with a real dirfd must work in Capsicum capability mode.
126 */
127 ATF_TC(openat_capsicum);
ATF_TC_HEAD(openat_capsicum,tc)128 ATF_TC_HEAD(openat_capsicum, tc)
129 {
130 atf_tc_set_md_var(tc, "descr",
131 "fts_openat() with dirfd works in Capsicum capability mode");
132 }
ATF_TC_BODY(openat_capsicum,tc)133 ATF_TC_BODY(openat_capsicum, tc)
134 {
135 char *paths[] = { ".", NULL };
136 FTS *fts;
137 FTSENT *ent;
138 int dirfd;
139 bool saw_file = false, saw_sub = false;
140
141 if (!feature_present("security_capabilities") ||
142 !feature_present("security_capability_mode"))
143 atf_tc_skip("Capsicum not available");
144
145 ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
146 ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755));
147 ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644)));
148 ATF_REQUIRE_EQ(0, close(creat("dir/other", 0644)));
149
150 ATF_REQUIRE((dirfd = open("dir", O_RDONLY | O_DIRECTORY)) >= 0);
151 ATF_REQUIRE_EQ(0, cap_enter());
152 ATF_REQUIRE((fts = fts_openat(dirfd, paths,
153 FTS_PHYSICAL | FTS_NOCHDIR, NULL)) != NULL);
154
155 while ((ent = fts_read(fts)) != NULL) {
156 if (ent->fts_info == FTS_DP)
157 continue;
158 if (strcmp(ent->fts_name, "sub") == 0 &&
159 ent->fts_info == FTS_D)
160 saw_sub = true;
161 if (strcmp(ent->fts_name, "file") == 0 &&
162 ent->fts_info == FTS_F)
163 saw_file = true;
164 }
165
166 ATF_CHECK_MSG(saw_sub, "must have visited 'sub' directory");
167 ATF_CHECK_MSG(saw_file, "must have visited 'file'");
168 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
169 }
170
171 /*
172 * Demonstrate the intended use of fts_dirfd: use
173 * fts_parent->fts_dirfd + fts_name to access files without
174 * relying on path-based operations.
175 */
176 ATF_TC(fts_dirfd_openat);
ATF_TC_HEAD(fts_dirfd_openat,tc)177 ATF_TC_HEAD(fts_dirfd_openat, tc)
178 {
179 atf_tc_set_md_var(tc, "descr",
180 "fts_parent->fts_dirfd + fts_name can be used with openat(2)");
181 }
182
ATF_TC_BODY(fts_dirfd_openat,tc)183 ATF_TC_BODY(fts_dirfd_openat, tc)
184 {
185 char *paths[] = { "dir1", "dir2", NULL };
186 FTS *fts;
187 FTSENT *ent;
188 struct stat sb_path, sb_dirfd;
189 int dirfd;
190 int nvisited = 0;
191
192 ATF_REQUIRE_EQ(0, mkdir("dir1", 0755));
193 ATF_REQUIRE_EQ(0, mkdir("dir1/sub", 0755));
194 ATF_REQUIRE_EQ(0, close(creat("dir1/sub/file", 0644)));
195 ATF_REQUIRE_EQ(0, mkdir("dir2", 0755));
196 ATF_REQUIRE_EQ(0, close(creat("dir2/file2", 0644)));
197
198 /*
199 * Open the current working directory as dirfd. fts_openat
200 * stores it in parent->fts_dirfd, allowing children to use
201 * fts_parent->fts_dirfd + fts_name with openat(2).
202 */
203 ATF_REQUIRE((dirfd = open(".", O_RDONLY | O_DIRECTORY)) >= 0);
204 ATF_REQUIRE((fts = fts_openat(dirfd, paths,
205 FTS_PHYSICAL, NULL)) != NULL);
206 close(dirfd);
207
208 while ((ent = fts_read(fts)) != NULL) {
209 if (ent->fts_info == FTS_DP)
210 continue;
211 if (ent->fts_level == FTS_ROOTLEVEL)
212 continue;
213
214 ATF_REQUIRE_MSG(ent->fts_parent->fts_dirfd >= 0,
215 "fts_parent->fts_dirfd must be valid for '%s'",
216 ent->fts_name);
217
218 ATF_REQUIRE_EQ_MSG(0,
219 fstatat(ent->fts_parent->fts_dirfd, ent->fts_name,
220 &sb_dirfd, AT_SYMLINK_NOFOLLOW),
221 "fstatat(fts_parent->fts_dirfd, '%s') failed: %m",
222 ent->fts_name);
223
224 ATF_REQUIRE_EQ_MSG(0,
225 lstat(ent->fts_accpath, &sb_path),
226 "lstat('%s') failed: %m", ent->fts_accpath);
227
228 ATF_CHECK_EQ_MSG(sb_path.st_ino, sb_dirfd.st_ino,
229 "inode mismatch for '%s': accpath=%ju dirfd=%ju",
230 ent->fts_name,
231 (uintmax_t)sb_path.st_ino,
232 (uintmax_t)sb_dirfd.st_ino);
233
234 nvisited++;
235 }
236
237 ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
238
239 /* dir1/sub, dir1/sub/file, dir2/file2 = 3 entries */
240 ATF_CHECK_EQ_MSG(3, nvisited,
241 "expected 3 entries, got %d", nvisited);
242 }
243
ATF_TP_ADD_TCS(tp)244 ATF_TP_ADD_TCS(tp)
245 {
246 ATF_TP_ADD_TC(tp, atfdcwd_matches_fts_open);
247 ATF_TP_ADD_TC(tp, openat_capsicum);
248 ATF_TP_ADD_TC(tp, fts_dirfd_openat);
249 return (atf_no_error());
250 }
251