xref: /freebsd/lib/libc/tests/gen/fts_regress_test.c (revision 670738a17568f2579b866878f39d2a824a386297)
1*670738a1SJitendra Bhati /*
2*670738a1SJitendra Bhati  * Copyright (c) 2026 Jitendra Bhati
3*670738a1SJitendra Bhati  *
4*670738a1SJitendra Bhati  * SPDX-License-Identifier: BSD-2-Clause
5*670738a1SJitendra Bhati  */
6*670738a1SJitendra Bhati 
7*670738a1SJitendra Bhati /*
8*670738a1SJitendra Bhati  * Regression tests for specific FreeBSD bug reports fixed in fts(3).
9*670738a1SJitendra Bhati  */
10*670738a1SJitendra Bhati 
11*670738a1SJitendra Bhati #include <sys/stat.h>
12*670738a1SJitendra Bhati #include <sys/time.h>
13*670738a1SJitendra Bhati 
14*670738a1SJitendra Bhati #include <errno.h>
15*670738a1SJitendra Bhati #include <fcntl.h>
16*670738a1SJitendra Bhati #include <fts.h>
17*670738a1SJitendra Bhati #include <pthread.h>
18*670738a1SJitendra Bhati #include <stdbool.h>
19*670738a1SJitendra Bhati #include <stdio.h>
20*670738a1SJitendra Bhati #include <stdlib.h>
21*670738a1SJitendra Bhati #include <string.h>
22*670738a1SJitendra Bhati #include <time.h>
23*670738a1SJitendra Bhati #include <unistd.h>
24*670738a1SJitendra Bhati 
25*670738a1SJitendra Bhati #include <atf-c.h>
26*670738a1SJitendra Bhati 
27*670738a1SJitendra Bhati /*
28*670738a1SJitendra Bhati  * Thrash function for file-based race tests: repeatedly creates and
29*670738a1SJitendra Bhati  * deletes a regular file at the given path.
30*670738a1SJitendra Bhati  */
31*670738a1SJitendra Bhati static volatile bool race_stop;
32*670738a1SJitendra Bhati 
33*670738a1SJitendra Bhati static void *
race_thrash(void * arg)34*670738a1SJitendra Bhati race_thrash(void *arg)
35*670738a1SJitendra Bhati {
36*670738a1SJitendra Bhati 	const char *path = arg;
37*670738a1SJitendra Bhati 
38*670738a1SJitendra Bhati 	while (!race_stop) {
39*670738a1SJitendra Bhati 		(void)close(creat(path, 0644));
40*670738a1SJitendra Bhati 		(void)unlink(path);
41*670738a1SJitendra Bhati 	}
42*670738a1SJitendra Bhati 	return (NULL);
43*670738a1SJitendra Bhati }
44*670738a1SJitendra Bhati 
45*670738a1SJitendra Bhati /*
46*670738a1SJitendra Bhati  * Thrash function for directory-based race tests: repeatedly removes
47*670738a1SJitendra Bhati  * and recreates a directory at the given path.
48*670738a1SJitendra Bhati  */
49*670738a1SJitendra Bhati static void *
dir_thrash(void * arg)50*670738a1SJitendra Bhati dir_thrash(void *arg)
51*670738a1SJitendra Bhati {
52*670738a1SJitendra Bhati 	const char *path = arg;
53*670738a1SJitendra Bhati 
54*670738a1SJitendra Bhati 	while (!race_stop) {
55*670738a1SJitendra Bhati 		(void)rmdir(path);
56*670738a1SJitendra Bhati 		(void)mkdir(path, 0755);
57*670738a1SJitendra Bhati 	}
58*670738a1SJitendra Bhati 	return (NULL);
59*670738a1SJitendra Bhati }
60*670738a1SJitendra Bhati 
61*670738a1SJitendra Bhati /*
62*670738a1SJitendra Bhati  * PR 45723: A directory with read but no execute permission must be
63*670738a1SJitendra Bhati  * traversed.  Before the fix, fts_build() gave up silently when
64*670738a1SJitendra Bhati  * chdir() failed, producing no output at all.  The fix falls back to
65*670738a1SJitendra Bhati  * FTS_DONTCHDIR mode so the directory is still traversed using full
66*670738a1SJitendra Bhati  * relative paths.
67*670738a1SJitendra Bhati  *
68*670738a1SJitendra Bhati  * Requires an unprivileged user because root ignores permissions.
69*670738a1SJitendra Bhati  */
70*670738a1SJitendra Bhati ATF_TC(read_no_exec_dir);
ATF_TC_HEAD(read_no_exec_dir,tc)71*670738a1SJitendra Bhati ATF_TC_HEAD(read_no_exec_dir, tc)
72*670738a1SJitendra Bhati {
73*670738a1SJitendra Bhati 	atf_tc_set_md_var(tc, "descr",
74*670738a1SJitendra Bhati 	    "directory with read but no execute is traversed via "
75*670738a1SJitendra Bhati 	    "FTS_DONTCHDIR fallback");
76*670738a1SJitendra Bhati 	atf_tc_set_md_var(tc, "require.user", "unprivileged");
77*670738a1SJitendra Bhati }
ATF_TC_BODY(read_no_exec_dir,tc)78*670738a1SJitendra Bhati ATF_TC_BODY(read_no_exec_dir, tc)
79*670738a1SJitendra Bhati {
80*670738a1SJitendra Bhati 	char *paths[] = { "dir", NULL };
81*670738a1SJitendra Bhati 	FTS *fts;
82*670738a1SJitendra Bhati 	FTSENT *ent;
83*670738a1SJitendra Bhati 	bool saw_d, saw_file;
84*670738a1SJitendra Bhati 
85*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
86*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
87*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, chmod("dir", 0400));
88*670738a1SJitendra Bhati 
89*670738a1SJitendra Bhati 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
90*670738a1SJitendra Bhati 
91*670738a1SJitendra Bhati 	/*
92*670738a1SJitendra Bhati 	 * Before the fix, zero entries were produced.  After the fix,
93*670738a1SJitendra Bhati 	 * fts falls back to FTS_DONTCHDIR and traverses using full paths.
94*670738a1SJitendra Bhati 	 * Verify the directory is not silently skipped.
95*670738a1SJitendra Bhati 	 */
96*670738a1SJitendra Bhati 	saw_d = false;
97*670738a1SJitendra Bhati 	saw_file = false;
98*670738a1SJitendra Bhati 	while ((ent = fts_read(fts)) != NULL) {
99*670738a1SJitendra Bhati 		if (ent->fts_info == FTS_D &&
100*670738a1SJitendra Bhati 		    strcmp(ent->fts_name, "dir") == 0)
101*670738a1SJitendra Bhati 			saw_d = true;
102*670738a1SJitendra Bhati 		if (strcmp(ent->fts_name, "file") == 0)
103*670738a1SJitendra Bhati 			saw_file = true;
104*670738a1SJitendra Bhati 	}
105*670738a1SJitendra Bhati 
106*670738a1SJitendra Bhati 	ATF_CHECK_MSG(saw_d,
107*670738a1SJitendra Bhati 	    "FTS_D not returned for directory with mode 0400");
108*670738a1SJitendra Bhati 	ATF_CHECK_MSG(saw_file,
109*670738a1SJitendra Bhati 	    "file inside mode 0400 directory was not visited");
110*670738a1SJitendra Bhati 
111*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
112*670738a1SJitendra Bhati }
113*670738a1SJitendra Bhati 
114*670738a1SJitendra Bhati /*
115*670738a1SJitendra Bhati  * PR 196724: FTS_SLNONE must not be returned for a non-symlink.
116*670738a1SJitendra Bhati  *
117*670738a1SJitendra Bhati  * The fix ensures that FTS_SLNONE is only returned when lstat confirms
118*670738a1SJitendra Bhati  * the entry is actually a symlink.  Exercised by a time-bounded race
119*670738a1SJitendra Bhati  * where a background thread creates and deletes a regular file while
120*670738a1SJitendra Bhati  * fts traverses with FTS_LOGICAL.
121*670738a1SJitendra Bhati  */
122*670738a1SJitendra Bhati ATF_TC(no_slnone_for_nonsymlink);
ATF_TC_HEAD(no_slnone_for_nonsymlink,tc)123*670738a1SJitendra Bhati ATF_TC_HEAD(no_slnone_for_nonsymlink, tc)
124*670738a1SJitendra Bhati {
125*670738a1SJitendra Bhati 	atf_tc_set_md_var(tc, "descr",
126*670738a1SJitendra Bhati 	    "FTS_SLNONE must not be returned for a non-symlink");
127*670738a1SJitendra Bhati }
ATF_TC_BODY(no_slnone_for_nonsymlink,tc)128*670738a1SJitendra Bhati ATF_TC_BODY(no_slnone_for_nonsymlink, tc)
129*670738a1SJitendra Bhati {
130*670738a1SJitendra Bhati 	pthread_t thr;
131*670738a1SJitendra Bhati 	char *paths[] = { "dir", NULL };
132*670738a1SJitendra Bhati 	FTS *fts;
133*670738a1SJitendra Bhati 	FTSENT *ent;
134*670738a1SJitendra Bhati 	struct timespec start, now, elapsed;
135*670738a1SJitendra Bhati 
136*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
137*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, symlink("nonexistent", "dir/dead"));
138*670738a1SJitendra Bhati 
139*670738a1SJitendra Bhati 	race_stop = false;
140*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, race_thrash,
141*670738a1SJitendra Bhati 	    __DECONST(void *, "dir/victim")));
142*670738a1SJitendra Bhati 
143*670738a1SJitendra Bhati 	clock_gettime(CLOCK_MONOTONIC, &start);
144*670738a1SJitendra Bhati 	for (;;) {
145*670738a1SJitendra Bhati 		clock_gettime(CLOCK_MONOTONIC, &now);
146*670738a1SJitendra Bhati 		timespecsub(&now, &start, &elapsed);
147*670738a1SJitendra Bhati 		if (elapsed.tv_sec >= 1)
148*670738a1SJitendra Bhati 			break;
149*670738a1SJitendra Bhati 		fts = fts_open(paths, FTS_LOGICAL, NULL);
150*670738a1SJitendra Bhati 		ATF_REQUIRE(fts != NULL);
151*670738a1SJitendra Bhati 		while ((ent = fts_read(fts)) != NULL) {
152*670738a1SJitendra Bhati 			if (ent->fts_info == FTS_SLNONE &&
153*670738a1SJitendra Bhati 			    ent->fts_statp->st_mode != 0 &&
154*670738a1SJitendra Bhati 			    !S_ISLNK(ent->fts_statp->st_mode))
155*670738a1SJitendra Bhati 				ATF_CHECK_MSG(0,
156*670738a1SJitendra Bhati 				    "FTS_SLNONE returned for non-symlink '%s'",
157*670738a1SJitendra Bhati 				    ent->fts_name);
158*670738a1SJitendra Bhati 		}
159*670738a1SJitendra Bhati 		fts_close(fts);
160*670738a1SJitendra Bhati 	}
161*670738a1SJitendra Bhati 
162*670738a1SJitendra Bhati 	race_stop = true;
163*670738a1SJitendra Bhati 	pthread_join(thr, NULL);
164*670738a1SJitendra Bhati }
165*670738a1SJitendra Bhati 
166*670738a1SJitendra Bhati /*
167*670738a1SJitendra Bhati  * PR 262038: fts_build() must detect readdir(2) errors and not treat
168*670738a1SJitendra Bhati  * them as end-of-directory.  The man page specifies that FTS_DNR must
169*670738a1SJitendra Bhati  * immediately follow FTS_D, in place of FTS_DP.
170*670738a1SJitendra Bhati  *
171*670738a1SJitendra Bhati  * Requires an unprivileged user because root ignores permissions.
172*670738a1SJitendra Bhati  */
173*670738a1SJitendra Bhati ATF_TC(readdir_error_detected);
ATF_TC_HEAD(readdir_error_detected,tc)174*670738a1SJitendra Bhati ATF_TC_HEAD(readdir_error_detected, tc)
175*670738a1SJitendra Bhati {
176*670738a1SJitendra Bhati 	atf_tc_set_md_var(tc, "descr",
177*670738a1SJitendra Bhati 	    "readdir errors produce FTS_DNR with fts_errno set");
178*670738a1SJitendra Bhati 	atf_tc_set_md_var(tc, "require.user", "unprivileged");
179*670738a1SJitendra Bhati }
ATF_TC_BODY(readdir_error_detected,tc)180*670738a1SJitendra Bhati ATF_TC_BODY(readdir_error_detected, tc)
181*670738a1SJitendra Bhati {
182*670738a1SJitendra Bhati 	char *paths[] = { "dir", NULL };
183*670738a1SJitendra Bhati 	FTS *fts;
184*670738a1SJitendra Bhati 	FTSENT *ent;
185*670738a1SJitendra Bhati 
186*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
187*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
188*670738a1SJitendra Bhati 
189*670738a1SJitendra Bhati 	/*
190*670738a1SJitendra Bhati 	 * Mode 0100: execute only, no read.  chdir() succeeds but
191*670738a1SJitendra Bhati 	 * opendir/readdir fails.  fts must return FTS_D then FTS_DNR
192*670738a1SJitendra Bhati 	 * (not FTS_DP) per the man page.
193*670738a1SJitendra Bhati 	 */
194*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, chmod("dir", 0100));
195*670738a1SJitendra Bhati 
196*670738a1SJitendra Bhati 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
197*670738a1SJitendra Bhati 
198*670738a1SJitendra Bhati 	ATF_REQUIRE((ent = fts_read(fts)) != NULL);
199*670738a1SJitendra Bhati 	ATF_CHECK_EQ_MSG(FTS_D, ent->fts_info,
200*670738a1SJitendra Bhati 	    "expected FTS_D, got %d", ent->fts_info);
201*670738a1SJitendra Bhati 
202*670738a1SJitendra Bhati 	ATF_REQUIRE((ent = fts_read(fts)) != NULL);
203*670738a1SJitendra Bhati 	ATF_CHECK_EQ_MSG(FTS_DNR, ent->fts_info,
204*670738a1SJitendra Bhati 	    "expected FTS_DNR, got %d", ent->fts_info);
205*670738a1SJitendra Bhati 	ATF_CHECK_MSG(ent->fts_errno != 0,
206*670738a1SJitendra Bhati 	    "FTS_DNR must have non-zero fts_errno");
207*670738a1SJitendra Bhati 
208*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ_MSG(NULL, fts_read(fts),
209*670738a1SJitendra Bhati 	    "expected NULL after FTS_DNR");
210*670738a1SJitendra Bhati 
211*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
212*670738a1SJitendra Bhati }
213*670738a1SJitendra Bhati 
214*670738a1SJitendra Bhati /*
215*670738a1SJitendra Bhati  * SVN r246641: fts_safe_changedir() uses O_DIRECTORY to prevent a
216*670738a1SJitendra Bhati  * TOCTOU substitution attack where a directory is replaced with a
217*670738a1SJitendra Bhati  * non-directory between stat and open.  Exercised by a time-bounded
218*670738a1SJitendra Bhati  * race where a background thread repeatedly removes and recreates
219*670738a1SJitendra Bhati  * dir/sub while fts traverses.
220*670738a1SJitendra Bhati  */
221*670738a1SJitendra Bhati ATF_TC(odirectory_changedir);
ATF_TC_HEAD(odirectory_changedir,tc)222*670738a1SJitendra Bhati ATF_TC_HEAD(odirectory_changedir, tc)
223*670738a1SJitendra Bhati {
224*670738a1SJitendra Bhati 	atf_tc_set_md_var(tc, "descr",
225*670738a1SJitendra Bhati 	    "fts_safe_changedir handles concurrent dir/file substitution");
226*670738a1SJitendra Bhati }
ATF_TC_BODY(odirectory_changedir,tc)227*670738a1SJitendra Bhati ATF_TC_BODY(odirectory_changedir, tc)
228*670738a1SJitendra Bhati {
229*670738a1SJitendra Bhati 	pthread_t thr;
230*670738a1SJitendra Bhati 	char *paths[] = { "dir", NULL };
231*670738a1SJitendra Bhati 	FTS *fts;
232*670738a1SJitendra Bhati 	struct timespec start, now, elapsed;
233*670738a1SJitendra Bhati 
234*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
235*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755));
236*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644)));
237*670738a1SJitendra Bhati 
238*670738a1SJitendra Bhati 	/*
239*670738a1SJitendra Bhati 	 * Background thread races to remove and recreate dir/sub as a
240*670738a1SJitendra Bhati 	 * directory.  With O_DIRECTORY the open fails safely if dir/sub
241*670738a1SJitendra Bhati 	 * is temporarily absent or replaced.
242*670738a1SJitendra Bhati 	 */
243*670738a1SJitendra Bhati 	race_stop = false;
244*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, dir_thrash,
245*670738a1SJitendra Bhati 	    __DECONST(void *, "dir/sub")));
246*670738a1SJitendra Bhati 
247*670738a1SJitendra Bhati 	clock_gettime(CLOCK_MONOTONIC, &start);
248*670738a1SJitendra Bhati 	for (;;) {
249*670738a1SJitendra Bhati 		clock_gettime(CLOCK_MONOTONIC, &now);
250*670738a1SJitendra Bhati 		timespecsub(&now, &start, &elapsed);
251*670738a1SJitendra Bhati 		if (elapsed.tv_sec >= 1)
252*670738a1SJitendra Bhati 			break;
253*670738a1SJitendra Bhati 		fts = fts_open(paths, FTS_PHYSICAL, NULL);
254*670738a1SJitendra Bhati 		ATF_REQUIRE(fts != NULL);
255*670738a1SJitendra Bhati 		while (fts_read(fts) != NULL)
256*670738a1SJitendra Bhati 			;
257*670738a1SJitendra Bhati 		fts_close(fts);
258*670738a1SJitendra Bhati 	}
259*670738a1SJitendra Bhati 
260*670738a1SJitendra Bhati 	race_stop = true;
261*670738a1SJitendra Bhati 	pthread_join(thr, NULL);
262*670738a1SJitendra Bhati }
263*670738a1SJitendra Bhati 
264*670738a1SJitendra Bhati /*
265*670738a1SJitendra Bhati  * SVN r261589: fts must not double-free when the directory tree is
266*670738a1SJitendra Bhati  * concurrently modified.  Exercised by a time-bounded race where a
267*670738a1SJitendra Bhati  * background thread creates and deletes a file during traversal.
268*670738a1SJitendra Bhati  */
269*670738a1SJitendra Bhati ATF_TC(concurrent_modification);
ATF_TC_HEAD(concurrent_modification,tc)270*670738a1SJitendra Bhati ATF_TC_HEAD(concurrent_modification, tc)
271*670738a1SJitendra Bhati {
272*670738a1SJitendra Bhati 	atf_tc_set_md_var(tc, "descr",
273*670738a1SJitendra Bhati 	    "no crash when tree modified during traversal");
274*670738a1SJitendra Bhati }
ATF_TC_BODY(concurrent_modification,tc)275*670738a1SJitendra Bhati ATF_TC_BODY(concurrent_modification, tc)
276*670738a1SJitendra Bhati {
277*670738a1SJitendra Bhati 	pthread_t thr;
278*670738a1SJitendra Bhati 	char *paths[] = { "dir", NULL };
279*670738a1SJitendra Bhati 	FTS *fts;
280*670738a1SJitendra Bhati 	struct timespec start, now, elapsed;
281*670738a1SJitendra Bhati 
282*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
283*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, close(creat("dir/stable", 0644)));
284*670738a1SJitendra Bhati 
285*670738a1SJitendra Bhati 	race_stop = false;
286*670738a1SJitendra Bhati 	ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, race_thrash,
287*670738a1SJitendra Bhati 	    __DECONST(void *, "dir/victim")));
288*670738a1SJitendra Bhati 
289*670738a1SJitendra Bhati 	clock_gettime(CLOCK_MONOTONIC, &start);
290*670738a1SJitendra Bhati 	for (;;) {
291*670738a1SJitendra Bhati 		clock_gettime(CLOCK_MONOTONIC, &now);
292*670738a1SJitendra Bhati 		timespecsub(&now, &start, &elapsed);
293*670738a1SJitendra Bhati 		if (elapsed.tv_sec >= 1)
294*670738a1SJitendra Bhati 			break;
295*670738a1SJitendra Bhati 		fts = fts_open(paths, FTS_PHYSICAL, NULL);
296*670738a1SJitendra Bhati 		ATF_REQUIRE(fts != NULL);
297*670738a1SJitendra Bhati 		while (fts_read(fts) != NULL)
298*670738a1SJitendra Bhati 			;
299*670738a1SJitendra Bhati 		fts_close(fts);
300*670738a1SJitendra Bhati 	}
301*670738a1SJitendra Bhati 
302*670738a1SJitendra Bhati 	race_stop = true;
303*670738a1SJitendra Bhati 	pthread_join(thr, NULL);
304*670738a1SJitendra Bhati }
305*670738a1SJitendra Bhati 
ATF_TP_ADD_TCS(tp)306*670738a1SJitendra Bhati ATF_TP_ADD_TCS(tp)
307*670738a1SJitendra Bhati {
308*670738a1SJitendra Bhati 	ATF_TP_ADD_TC(tp, read_no_exec_dir);
309*670738a1SJitendra Bhati 	ATF_TP_ADD_TC(tp, no_slnone_for_nonsymlink);
310*670738a1SJitendra Bhati 	ATF_TP_ADD_TC(tp, readdir_error_detected);
311*670738a1SJitendra Bhati 	ATF_TP_ADD_TC(tp, odirectory_changedir);
312*670738a1SJitendra Bhati 	ATF_TP_ADD_TC(tp, concurrent_modification);
313*670738a1SJitendra Bhati 
314*670738a1SJitendra Bhati 	return (atf_no_error());
315*670738a1SJitendra Bhati }
316