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