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