xref: /freebsd/lib/libc/tests/gen/fts_openat_test.c (revision 9aed7a7745512ca098cc2b247cef61ad57d63204)
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