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