xref: /freebsd/lib/libc/tests/gen/fts_misc_test.c (revision a259b98fa211ed87bfee58c575de4e2de94ee0fa)
1 /*
2  * Copyright (c) 2025 Klara, Inc.
3  * Copyright (c) 2026 Jitendra Bhati
4  *
5  * SPDX-License-Identifier: BSD-2-Clause
6  */
7 
8 #include <sys/mount.h>
9 #include <sys/sysctl.h>
10 #include <sys/param.h>
11 #include <sys/stat.h>
12 #include <sys/syslimits.h>
13 #include <sys/uio.h>
14 
15 #include <errno.h>
16 #include <fcntl.h>
17 #include <fts.h>
18 #include <stdbool.h>
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <string.h>
22 #include <unistd.h>
23 
24 #include <atf-c.h>
25 
26 #include "fts_test.h"
27 
28 ATF_TC(fts_unrdir);
29 ATF_TC_HEAD(fts_unrdir, tc)
30 {
31 	atf_tc_set_md_var(tc, "descr", "unreadable directories");
32 	atf_tc_set_md_var(tc, "require.user", "unprivileged");
33 }
34 ATF_TC_BODY(fts_unrdir, tc)
35 {
36 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
37 	ATF_REQUIRE_EQ(0, mkdir("dir/unr", 0100));
38 	ATF_REQUIRE_EQ(0, mkdir("dir/unx", 0400));
39 	fts_test(tc, &(struct fts_testcase){
40 		    (char *[]){ "dir", NULL },
41 		    FTS_PHYSICAL,
42 		    (struct fts_expect[]){
43 			    { FTS_D,	"dir",	"dir" },
44 			    { FTS_D,	"unr",	"unr" },
45 			    { FTS_DNR,	"unr",	"unr" },
46 			    { FTS_D,	"unx",	"unx" },
47 			    { FTS_DP,	"unx",	"unx" },
48 			    { FTS_DP,	"dir",	"dir" },
49 			    { 0 }
50 		    },
51 	    });
52 }
53 
54 ATF_TC(fts_unrdir_nochdir);
55 ATF_TC_HEAD(fts_unrdir_nochdir, tc)
56 {
57 	atf_tc_set_md_var(tc, "descr", "unreadable directories (nochdir)");
58 	atf_tc_set_md_var(tc, "require.user", "unprivileged");
59 }
60 ATF_TC_BODY(fts_unrdir_nochdir, tc)
61 {
62 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
63 	ATF_REQUIRE_EQ(0, mkdir("dir/unr", 0100));
64 	ATF_REQUIRE_EQ(0, mkdir("dir/unx", 0400));
65 	fts_test(tc, &(struct fts_testcase){
66 		    (char *[]){ "dir", NULL },
67 		    FTS_PHYSICAL | FTS_NOCHDIR,
68 		    (struct fts_expect[]){
69 			    { FTS_D,	"dir",	"dir" },
70 			    { FTS_D,	"unr",	"dir/unr" },
71 			    { FTS_DNR,	"unr",	"dir/unr" },
72 			    { FTS_D,	"unx",	"dir/unx" },
73 			    { FTS_DP,	"unx",	"dir/unx" },
74 			    { FTS_DP,	"dir",	"dir" },
75 			    { 0 }
76 		    },
77 	    });
78 }
79 
80 /*
81  * With FTS_NOCHDIR and absolute paths, the application may call chdir(2)
82  * freely between fts_read() calls without corrupting the traversal.
83  */
84 ATF_TC(nochdir_app_can_chdir);
85 ATF_TC_HEAD(nochdir_app_can_chdir, tc)
86 {
87 	atf_tc_set_md_var(tc, "descr",
88 	    "FTS_NOCHDIR: application chdir between reads does not "
89 	    "corrupt traversal");
90 }
91 ATF_TC_BODY(nochdir_app_can_chdir, tc)
92 {
93 	char *cwd, *abspath;
94 	char *paths[2];
95 	char pwd[PATH_MAX];
96 	FTS *fts;
97 	FTSENT *ent;
98 	int entries;
99 
100 	cwd = malloc(PATH_MAX);
101 	ATF_REQUIRE(cwd != NULL);
102 	abspath = malloc(PATH_MAX * 2);
103 	ATF_REQUIRE(abspath != NULL);
104 
105 	ATF_REQUIRE(getcwd(cwd, PATH_MAX) != NULL);
106 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
107 	ATF_REQUIRE_EQ(0, close(creat("dir/a", 0644)));
108 	ATF_REQUIRE_EQ(0, close(creat("dir/b", 0644)));
109 
110 	snprintf(abspath, PATH_MAX * 2, "%s/dir", cwd);
111 	paths[0] = abspath;
112 	paths[1] = NULL;
113 
114 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL | FTS_NOCHDIR,
115 	    fts_lexical_compar)) != NULL);
116 
117 	/*
118 	 * Chdir to root once after fts_open() but before fts_read().
119 	 * With FTS_NOCHDIR, fts must not call chdir() itself, so the
120 	 * process CWD must remain "/" throughout the traversal.
121 	 */
122 	ATF_REQUIRE_EQ(0, chdir("/"));
123 
124 	entries = 0;
125 	while ((ent = fts_read(fts)) != NULL) {
126 		ATF_REQUIRE(getcwd(pwd, sizeof(pwd)) != NULL);
127 		ATF_CHECK_STREQ_MSG("/", pwd,
128 		    "PWD changed during FTS_NOCHDIR traversal");
129 		entries++;
130 	}
131 	ATF_CHECK_EQ_MSG(0, errno,
132 	    "traversal ended with errno %d", errno);
133 
134 	/* FTS_D dir, FTS_F a, FTS_F b, FTS_DP dir = 4 entries */
135 	ATF_CHECK_EQ_MSG(4, entries,
136 	    "expected 4 entries, got %d", entries);
137 
138 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
139 	free(cwd);
140 	free(abspath);
141 }
142 
143 /*
144  * fts_name is always NUL-terminated and fts_namelen always equals
145  * strlen(fts_name), regardless of traversal options or entry type.
146  */
147 ATF_TC(name_nul_terminated);
148 ATF_TC_HEAD(name_nul_terminated, tc)
149 {
150 	atf_tc_set_md_var(tc, "descr",
151 	    "fts_name is always NUL-terminated with correct fts_namelen");
152 }
153 ATF_TC_BODY(name_nul_terminated, tc)
154 {
155 	char *paths[] = { "root", NULL };
156 	FTS *fts;
157 	FTSENT *ent;
158 
159 	ATF_REQUIRE_EQ(0, mkdir("root", 0755));
160 	ATF_REQUIRE_EQ(0, mkdir("root/sub", 0755));
161 	ATF_REQUIRE_EQ(0, close(creat("root/sub/file.c", 0644)));
162 	ATF_REQUIRE_EQ(0, symlink("file.c", "root/sub/link"));
163 
164 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
165 	    fts_lexical_compar)) != NULL);
166 
167 	while ((ent = fts_read(fts)) != NULL) {
168 		ATF_CHECK_EQ_MSG(strlen(ent->fts_name), ent->fts_namelen,
169 		    "fts_namelen %zu != strlen(fts_name) %zu for '%s'",
170 		    ent->fts_namelen, strlen(ent->fts_name), ent->fts_name);
171 	}
172 
173 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
174 }
175 
176 /*
177  * Every FTS_D must be paired with exactly one FTS_DP.  fts_level must
178  * be FTS_ROOTLEVEL (0) for the root, incrementing by one per level.
179  */
180 ATF_TC(prepost_order_and_levels);
181 ATF_TC_HEAD(prepost_order_and_levels, tc)
182 {
183 	atf_tc_set_md_var(tc, "descr",
184 	    "FTS_D/FTS_DP are paired and fts_level increments correctly");
185 }
186 ATF_TC_BODY(prepost_order_and_levels, tc)
187 {
188 	char *paths[] = { "top", NULL };
189 	FTS *fts;
190 	FTSENT *ent;
191 	static const int stack_depth = 32;
192 	struct {
193 		const char	*name;
194 		long		 level;
195 	} stack[32];
196 	int depth;
197 
198 	ATF_REQUIRE_EQ(0, mkdir("top", 0755));
199 	ATF_REQUIRE_EQ(0, mkdir("top/mid", 0755));
200 	ATF_REQUIRE_EQ(0, mkdir("top/mid/bot", 0755));
201 	ATF_REQUIRE_EQ(0, close(creat("top/mid/bot/leaf", 0644)));
202 
203 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
204 	    fts_lexical_compar)) != NULL);
205 
206 	depth = 0;
207 	while ((ent = fts_read(fts)) != NULL) {
208 		if (ent->fts_info == FTS_D) {
209 			ATF_REQUIRE_MSG(depth < stack_depth,
210 			    "stack overflow in test");
211 			stack[depth].name  = ent->fts_name;
212 			stack[depth].level = ent->fts_level;
213 			depth++;
214 		} else if (ent->fts_info == FTS_DP) {
215 			ATF_REQUIRE_MSG(depth > 0,
216 			    "FTS_DP without matching FTS_D");
217 			depth--;
218 			ATF_CHECK_STREQ(stack[depth].name, ent->fts_name);
219 			ATF_CHECK_EQ(stack[depth].level, ent->fts_level);
220 		}
221 
222 		if (ent->fts_info == FTS_D || ent->fts_info == FTS_DP ||
223 		    ent->fts_info == FTS_F) {
224 			if (strcmp(ent->fts_name, "top") == 0)
225 				ATF_CHECK_EQ(FTS_ROOTLEVEL, ent->fts_level);
226 			else if (strcmp(ent->fts_name, "mid") == 0)
227 				ATF_CHECK_EQ(1, ent->fts_level);
228 			else if (strcmp(ent->fts_name, "bot") == 0)
229 				ATF_CHECK_EQ(2, ent->fts_level);
230 			else if (strcmp(ent->fts_name, "leaf") == 0)
231 				ATF_CHECK_EQ(3, ent->fts_level);
232 		}
233 	}
234 	ATF_CHECK_EQ_MSG(0, depth,
235 	    "%d unmatched FTS_D entries at end", depth);
236 
237 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
238 }
239 
240 /*
241  * FTSENT fields fts_errno, fts_dev, fts_ino, and fts_nlink must be
242  * consistent with what stat(2) returns for successfully visited entries.
243  */
244 ATF_TC(ftsent_fields);
245 ATF_TC_HEAD(ftsent_fields, tc)
246 {
247 	atf_tc_set_md_var(tc, "descr",
248 	    "FTSENT fts_errno/fts_dev/fts_ino/fts_nlink are correct");
249 }
250 ATF_TC_BODY(ftsent_fields, tc)
251 {
252 	char *paths[] = { "dir", NULL };
253 	FTS *fts;
254 	FTSENT *ent;
255 	struct stat sb;
256 
257 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
258 	ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
259 
260 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
261 	    fts_lexical_compar)) != NULL);
262 
263 	while ((ent = fts_read(fts)) != NULL) {
264 		ATF_CHECK_EQ_MSG(0, ent->fts_errno,
265 		    "fts_errno != 0 for '%s'", ent->fts_name);
266 
267 		if (ent->fts_info == FTS_D) {
268 			ATF_REQUIRE_EQ_MSG(0,
269 			    stat(ent->fts_accpath, &sb),
270 			    "stat(%s): %m", ent->fts_accpath);
271 			ATF_CHECK_EQ(sb.st_dev, ent->fts_dev);
272 			ATF_CHECK_EQ(sb.st_ino, ent->fts_ino);
273 			/*
274 			 * "dir" has exactly two links: one from its
275 			 * parent and one from its own "." entry.
276 			 */
277 			ATF_CHECK_EQ_MSG(2, ent->fts_nlink,
278 			    "expected fts_nlink == 2 for '%s', got %ju",
279 			    ent->fts_name, (uintmax_t)ent->fts_nlink);
280 		}
281 	}
282 
283 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
284 }
285 
286 /*
287  * Under FTS_PHYSICAL, symlinks are never followed, so a circular
288  * symlink loop cannot cause infinite recursion.  Both symlinks are
289  * returned as FTS_SL and traversal terminates.
290  */
291 ATF_TC(symlink_loop_physical);
292 ATF_TC_HEAD(symlink_loop_physical, tc)
293 {
294 	atf_tc_set_md_var(tc, "descr",
295 	    "circular symlink loop under FTS_PHYSICAL terminates");
296 }
297 ATF_TC_BODY(symlink_loop_physical, tc)
298 {
299 	char *paths[] = { "dir", NULL };
300 	FTS *fts;
301 	FTSENT *ent;
302 	int entries;
303 
304 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
305 	ATF_REQUIRE_EQ(0, symlink("b", "dir/a"));
306 	ATF_REQUIRE_EQ(0, symlink("a", "dir/b"));
307 
308 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL,
309 	    fts_lexical_compar)) != NULL);
310 
311 	entries = 0;
312 	while ((ent = fts_read(fts)) != NULL) {
313 		ATF_CHECK_MSG(
314 		    ent->fts_info == FTS_D  ||
315 		    ent->fts_info == FTS_DP ||
316 		    ent->fts_info == FTS_SL,
317 		    "unexpected fts_info %d for '%s'",
318 		    ent->fts_info, ent->fts_name);
319 		ATF_REQUIRE_MSG(++entries < 100,
320 		    "traversal exceeded 100 entries, probable infinite loop");
321 	}
322 
323 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
324 }
325 
326 /*
327  * Cycle detection via dev/ino comparison under FTS_LOGICAL: following
328  * a symlink that points back to an ancestor must produce FTS_DC rather
329  * than infinite recursion.
330  */
331 ATF_TC(cycle_detection);
332 ATF_TC_HEAD(cycle_detection, tc)
333 {
334 	atf_tc_set_md_var(tc, "descr",
335 	    "cycle via symlink under FTS_LOGICAL yields FTS_DC");
336 }
337 ATF_TC_BODY(cycle_detection, tc)
338 {
339 	char *paths[] = { "dir", NULL };
340 	FTS *fts;
341 	FTSENT *ent;
342 	int saw_dc, entries;
343 
344 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
345 	ATF_REQUIRE_EQ(0, symlink("..", "dir/up"));
346 
347 	ATF_REQUIRE((fts = fts_open(paths, FTS_LOGICAL,
348 	    fts_lexical_compar)) != NULL);
349 
350 	saw_dc = 0;
351 	entries = 0;
352 	while ((ent = fts_read(fts)) != NULL) {
353 		if (ent->fts_info == FTS_DC)
354 			saw_dc = 1;
355 		ATF_REQUIRE_MSG(++entries < 100,
356 		    "traversal exceeded 100 entries, probable infinite loop");
357 	}
358 	ATF_CHECK_MSG(saw_dc != 0,
359 	    "expected FTS_DC entry for the cycle");
360 
361 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
362 }
363 
364 /*
365  * fts_close() after the root directory has been deleted must not crash.
366  */
367 ATF_TC(close_after_root_deleted);
368 ATF_TC_HEAD(close_after_root_deleted, tc)
369 {
370 	atf_tc_set_md_var(tc, "descr",
371 	    "fts_close after root deletion must not crash");
372 }
373 ATF_TC_BODY(close_after_root_deleted, tc)
374 {
375 	char *orig_cwd, *final_cwd;
376 	char *paths[] = { "dir", NULL };
377 	FTS *fts;
378 
379 	orig_cwd = malloc(PATH_MAX);
380 	ATF_REQUIRE(orig_cwd != NULL);
381 	final_cwd = malloc(PATH_MAX);
382 	ATF_REQUIRE(final_cwd != NULL);
383 
384 	ATF_REQUIRE(getcwd(orig_cwd, PATH_MAX) != NULL);
385 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
386 	ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
387 
388 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
389 
390 	/* Read first entry then delete the tree. */
391 	ATF_REQUIRE(fts_read(fts) != NULL);
392 	ATF_REQUIRE_EQ(0, unlink("dir/file"));
393 	ATF_REQUIRE_EQ(0, rmdir("dir"));
394 
395 	/*
396 	 * Drain traversal -- errors are expected after deletion
397 	 * but fts_read() must not crash.
398 	 */
399 	while (fts_read(fts) != NULL)
400 		;
401 
402 	/* fts_close() must not crash regardless of return value. */
403 	(void)fts_close(fts);
404 
405 	/*
406 	 * After fts_close(), the process CWD must be restored to
407 	 * the original directory even though the traversal root
408 	 * was deleted mid-traversal.
409 	 */
410 	ATF_REQUIRE(getcwd(final_cwd, PATH_MAX) != NULL);
411 	ATF_CHECK_STREQ_MSG(orig_cwd, final_cwd,
412 	    "CWD after fts_close should be '%s', got '%s'",
413 	    orig_cwd, final_cwd);
414 
415 	free(orig_cwd);
416 	free(final_cwd);
417 }
418 
419 /*
420  * fts_close() after the root has been renamed must restore the process
421  * CWD to the original directory.
422  * Regression test for SVN r77497.
423  */
424 ATF_TC(close_after_root_moved);
425 ATF_TC_HEAD(close_after_root_moved, tc)
426 {
427 	atf_tc_set_md_var(tc, "descr",
428 	    "fts_close after root rename restores CWD (SVN r77497)");
429 }
430 ATF_TC_BODY(close_after_root_moved, tc)
431 {
432 	char *orig_cwd, *final_cwd;
433 	char *paths[] = { "dir", NULL };
434 	FTS *fts;
435 
436 	orig_cwd = malloc(PATH_MAX);
437 	ATF_REQUIRE(orig_cwd != NULL);
438 	final_cwd = malloc(PATH_MAX);
439 	ATF_REQUIRE(final_cwd != NULL);
440 
441 	ATF_REQUIRE(getcwd(orig_cwd, PATH_MAX) != NULL);
442 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
443 	ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
444 
445 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
446 
447 	/* Read first entry then rename the root mid-traversal. */
448 	ATF_REQUIRE(fts_read(fts) != NULL);
449 	ATF_REQUIRE_EQ(0, rename("dir", "dir_moved"));
450 
451 	while (fts_read(fts) != NULL)
452 		;
453 
454 	/* fts_close() must not crash. */
455 	(void)fts_close(fts);
456 
457 	ATF_REQUIRE(getcwd(final_cwd, PATH_MAX) != NULL);
458 	ATF_CHECK_STREQ_MSG(orig_cwd, final_cwd,
459 	    "CWD after fts_close should be '%s', got '%s'",
460 	    orig_cwd, final_cwd);
461 
462 	free(orig_cwd);
463 	free(final_cwd);
464 }
465 /*
466  * FTS_NOCHDIR with an empty terminal directory must not corrupt the
467  * path buffer for subsequent entries.
468  * Regression test for SVN r49772.
469  */
470 ATF_TC(nochdir_empty_terminal_dir);
471 ATF_TC_HEAD(nochdir_empty_terminal_dir, tc)
472 {
473 	atf_tc_set_md_var(tc, "descr",
474 	    "FTS_NOCHDIR + empty directory does not corrupt path "
475 	    "(SVN r49772)");
476 }
477 ATF_TC_BODY(nochdir_empty_terminal_dir, tc)
478 {
479 	ATF_REQUIRE_EQ(0, mkdir("parent", 0755));
480 	ATF_REQUIRE_EQ(0, mkdir("parent/empty", 0755));
481 	ATF_REQUIRE_EQ(0, close(creat("parent/sibling", 0644)));
482 
483 	fts_test(tc, &(struct fts_testcase){
484 		    (char *[]){ "parent", NULL },
485 		    FTS_PHYSICAL | FTS_NOCHDIR,
486 		    (struct fts_expect[]){
487 			    { FTS_D,  "parent",  "parent"         },
488 			    { FTS_D,  "empty",   "parent/empty"   },
489 			    { FTS_DP, "empty",   "parent/empty"   },
490 			    { FTS_F,  "sibling", "parent/sibling" },
491 			    { FTS_DP, "parent",  "parent"         },
492 			    { 0 }
493 		    },
494 	    });
495 }
496 
497 /*
498  * A nonexistent path yields FTS_NS with fts_errno set to a non-zero
499  * value identifying why stat(2) failed.
500  */
501 ATF_TC(ns_errno_set);
502 ATF_TC_HEAD(ns_errno_set, tc)
503 {
504 	atf_tc_set_md_var(tc, "descr",
505 	    "FTS_NS entry has non-zero fts_errno");
506 }
507 ATF_TC_BODY(ns_errno_set, tc)
508 {
509 	char *paths[] = { "nonexistent", NULL };
510 	FTS *fts;
511 	FTSENT *ent;
512 
513 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
514 
515 	ent = fts_read(fts);
516 	ATF_REQUIRE(ent != NULL);
517 	ATF_CHECK_EQ(FTS_NS, ent->fts_info);
518 	ATF_CHECK_MSG(ent->fts_errno != 0,
519 	    "FTS_NS entry must have non-zero fts_errno");
520 
521 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
522 }
523 
524 /*
525  * FTS_XDEV prevents traversal from crossing mount points.
526  * Mount a tmpfs on a subdirectory and verify fts does not
527  * descend into it when FTS_XDEV is set.
528  */
529 ATF_TC_WITH_CLEANUP(xdev);
530 ATF_TC_HEAD(xdev, tc)
531 {
532 	atf_tc_set_md_var(tc, "descr",
533 	    "FTS_XDEV does not cross mount points");
534 	atf_tc_set_md_var(tc, "require.user", "root");
535 }
536 ATF_TC_BODY(xdev, tc)
537 {
538 	struct iovec iov[4];
539 	char *paths[] = { "dir", NULL };
540 	FTS *fts;
541 	FTSENT *ent;
542 	bool crossed;
543 
544 	ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
545 	ATF_REQUIRE_EQ(0, mkdir("dir/mnt", 0755));
546 	ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
547 
548 	iov[0].iov_base = (void *)"fstype";
549 	iov[0].iov_len  = sizeof("fstype");
550 	iov[1].iov_base = (void *)"tmpfs";
551 	iov[1].iov_len  = sizeof("tmpfs");
552 	iov[2].iov_base = (void *)"fspath";
553 	iov[2].iov_len  = sizeof("fspath");
554 	iov[3].iov_base = (void *)"dir/mnt";
555 	iov[3].iov_len  = sizeof("dir/mnt");
556 
557 	if (nmount(iov, 4, 0) != 0)
558 		atf_tc_skip("could not mount tmpfs: %s", strerror(errno));
559 
560 	ATF_REQUIRE_EQ(0, close(creat("dir/mnt/inside", 0644)));
561 
562 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL | FTS_XDEV,
563 	    fts_lexical_compar)) != NULL);
564 
565 	crossed = false;
566 	while ((ent = fts_read(fts)) != NULL) {
567 		if (strcmp(ent->fts_name, "inside") == 0)
568 			crossed = true;
569 	}
570 	ATF_CHECK_MSG(!crossed,
571 	    "FTS_XDEV must not descend into tmpfs mount point");
572 
573 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
574 }
575 ATF_TC_CLEANUP(xdev, tc)
576 {
577 	(void)unmount("dir/mnt", 0);
578 }
579 
580 /*
581  * Return the number of open file descriptors in the current process
582  * via the kern.proc.nfds sysctl.
583  */
584 static int
585 count_open_fds(void)
586 {
587 	int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_NFDS, 0 };
588 	int nfds;
589 	size_t len = sizeof(nfds);
590 
591 	ATF_REQUIRE_EQ_MSG(0,
592 	    sysctl(mib, nitems(mib), &nfds, &len, NULL, 0),
593 	    "sysctl(kern.proc.nfds): %m");
594 	return (nfds);
595 }
596 
597 ATF_TC(no_fd_leak_on_early_close);
598 ATF_TC_HEAD(no_fd_leak_on_early_close, tc)
599 {
600 	atf_tc_set_md_var(tc, "descr",
601 	    "fts_close must not leak directory fds when the traversal "
602 	    "is abandoned before completion");
603 }
604 
605 ATF_TC_BODY(no_fd_leak_on_early_close, tc)
606 {
607 	char *paths[] = { ".", NULL };
608 	FTS *fts;
609 	int fds_before, fds_after;
610 
611 	/*
612 	 * Regression test for a file descriptor leak (bug 297557).
613 	 * fts_build() stores a dup'd fd in each directory entry's
614 	 * fts_dirfd.  If the traversal is abandoned partway through
615 	 * and fts_close() is called, the cleanup loop must close
616 	 * those fds; previously it freed the entries without closing
617 	 * fts_dirfd, leaking one fd per pending directory.
618 	 */
619 	ATF_REQUIRE_EQ(0, mkdir("sub1", 0755));
620 	ATF_REQUIRE_EQ(0, mkdir("sub1/deep", 0755));
621 	ATF_REQUIRE_EQ(0, close(creat("sub1/deep/file", 0644)));
622 
623 	fds_before = count_open_fds();
624 
625 	/*
626 	 * Open, descend a couple of levels, then abandon the traversal
627 	 * and close.  The directory entries for '.' and 'sub1' hold
628 	 * dup'd fds that must be released by fts_close().
629 	 */
630 	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
631 	ATF_REQUIRE(fts_read(fts) != NULL);	/* . */
632 	ATF_REQUIRE(fts_read(fts) != NULL);	/* sub1 */
633 	ATF_REQUIRE(fts_read(fts) != NULL);	/* deep */
634 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
635 
636 	fds_after = count_open_fds();
637 
638 	ATF_CHECK_EQ_MSG(fds_before, fds_after,
639 	    "fts_close leaked file descriptors: %d open before, "
640 	    "%d after", fds_before, fds_after);
641 }
642 
643 
644 ATF_TP_ADD_TCS(tp)
645 {
646 	fts_check_debug();
647 	ATF_TP_ADD_TC(tp, fts_unrdir);
648 	ATF_TP_ADD_TC(tp, fts_unrdir_nochdir);
649 	ATF_TP_ADD_TC(tp, nochdir_app_can_chdir);
650 	ATF_TP_ADD_TC(tp, name_nul_terminated);
651 	ATF_TP_ADD_TC(tp, prepost_order_and_levels);
652 	ATF_TP_ADD_TC(tp, ftsent_fields);
653 	ATF_TP_ADD_TC(tp, symlink_loop_physical);
654 	ATF_TP_ADD_TC(tp, cycle_detection);
655 	ATF_TP_ADD_TC(tp, close_after_root_deleted);
656 	ATF_TP_ADD_TC(tp, close_after_root_moved);
657 	ATF_TP_ADD_TC(tp, nochdir_empty_terminal_dir);
658 	ATF_TP_ADD_TC(tp, ns_errno_set);
659 	ATF_TP_ADD_TC(tp, xdev);
660 	ATF_TP_ADD_TC(tp, no_fd_leak_on_early_close);
661 
662 	return (atf_no_error());
663 }
664