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