The branch main has been updated by asomers:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=670738a17568f2579b866878f39d2a824a386297

commit 670738a17568f2579b866878f39d2a824a386297
Author:     Jitendra Bhati <[email protected]>
AuthorDate: 2026-06-03 19:47:18 +0000
Commit:     Alan Somers <[email protected]>
CommitDate: 2026-06-05 20:03:08 +0000

    fts: add fts regression tests
    
    Add ATF regression tests for previously-fixed fts(3) bugs:
    
    - PR 45723: directory with read but no execute is traversed via
      FTS_DONTCHDIR fallback, not silently skipped
      (commit 1e03bff7f2b7)
    - PR 196724: FTS_SLNONE must not be returned for a non-symlink;
      time-bounded race test runs for 1 second with concurrent
      file creation/deletion
      (commit bf4374c54589)
    - PR 262038: readdir(2) errors produce FTS_DNR with fts_errno
      set, not silently treated as end-of-directory
      (commit 0cff70ca6654)
    - SVN r246641: normal traversal works correctly with O_DIRECTORY
      fix in fts_safe_changedir()
      (commit f9928f1705ee)
    - SVN r261589: no crash when tree modified during traversal;
      time-bounded race test runs for 1 second with concurrent
      file creation/deletion
      (commit c6d38f088e5c)
    
    Sponsored by:   Google LLC (GSoC 2026)
    Reviewed by:    asomers
    MFC after:      2 weeks
    Pull Request:   https://github.com/freebsd/freebsd-src/pull/2257
---
 lib/libc/tests/gen/Makefile           |   2 +
 lib/libc/tests/gen/fts_regress_test.c | 315 ++++++++++++++++++++++++++++++++++
 2 files changed, 317 insertions(+)

diff --git a/lib/libc/tests/gen/Makefile b/lib/libc/tests/gen/Makefile
index 7213fb4d4431..97b32827a66a 100644
--- a/lib/libc/tests/gen/Makefile
+++ b/lib/libc/tests/gen/Makefile
@@ -14,6 +14,7 @@ ATF_TESTS_C+=         fts_children_test
 ATF_TESTS_C+=          fts_misc_test
 ATF_TESTS_C+=          fts_open_test
 ATF_TESTS_C+=          fts_options_test
+ATF_TESTS_C+=          fts_regress_test
 ATF_TESTS_C+=          fts_set_test
 ATF_TESTS_C+=          ftw_test
 ATF_TESTS_C+=          getentropy_test
@@ -99,6 +100,7 @@ LIBADD.fpsetround_test+=m
 LIBADD.siginfo_test+=  m
 
 LIBADD.nice_test+=     pthread
+LIBADD.fts_regress_test+=      pthread
 LIBADD.syslog_test+=   pthread
 
 CFLAGS+=               -I${.CURDIR}
diff --git a/lib/libc/tests/gen/fts_regress_test.c 
b/lib/libc/tests/gen/fts_regress_test.c
new file mode 100644
index 000000000000..cf4035a65259
--- /dev/null
+++ b/lib/libc/tests/gen/fts_regress_test.c
@@ -0,0 +1,315 @@
+/*
+ * Copyright (c) 2026 Jitendra Bhati
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Regression tests for specific FreeBSD bug reports fixed in fts(3).
+ */
+
+#include <sys/stat.h>
+#include <sys/time.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <fts.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+/*
+ * Thrash function for file-based race tests: repeatedly creates and
+ * deletes a regular file at the given path.
+ */
+static volatile bool race_stop;
+
+static void *
+race_thrash(void *arg)
+{
+       const char *path = arg;
+
+       while (!race_stop) {
+               (void)close(creat(path, 0644));
+               (void)unlink(path);
+       }
+       return (NULL);
+}
+
+/*
+ * Thrash function for directory-based race tests: repeatedly removes
+ * and recreates a directory at the given path.
+ */
+static void *
+dir_thrash(void *arg)
+{
+       const char *path = arg;
+
+       while (!race_stop) {
+               (void)rmdir(path);
+               (void)mkdir(path, 0755);
+       }
+       return (NULL);
+}
+
+/*
+ * PR 45723: A directory with read but no execute permission must be
+ * traversed.  Before the fix, fts_build() gave up silently when
+ * chdir() failed, producing no output at all.  The fix falls back to
+ * FTS_DONTCHDIR mode so the directory is still traversed using full
+ * relative paths.
+ *
+ * Requires an unprivileged user because root ignores permissions.
+ */
+ATF_TC(read_no_exec_dir);
+ATF_TC_HEAD(read_no_exec_dir, tc)
+{
+       atf_tc_set_md_var(tc, "descr",
+           "directory with read but no execute is traversed via "
+           "FTS_DONTCHDIR fallback");
+       atf_tc_set_md_var(tc, "require.user", "unprivileged");
+}
+ATF_TC_BODY(read_no_exec_dir, tc)
+{
+       char *paths[] = { "dir", NULL };
+       FTS *fts;
+       FTSENT *ent;
+       bool saw_d, saw_file;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
+       ATF_REQUIRE_EQ(0, chmod("dir", 0400));
+
+       ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
+
+       /*
+        * Before the fix, zero entries were produced.  After the fix,
+        * fts falls back to FTS_DONTCHDIR and traverses using full paths.
+        * Verify the directory is not silently skipped.
+        */
+       saw_d = false;
+       saw_file = false;
+       while ((ent = fts_read(fts)) != NULL) {
+               if (ent->fts_info == FTS_D &&
+                   strcmp(ent->fts_name, "dir") == 0)
+                       saw_d = true;
+               if (strcmp(ent->fts_name, "file") == 0)
+                       saw_file = true;
+       }
+
+       ATF_CHECK_MSG(saw_d,
+           "FTS_D not returned for directory with mode 0400");
+       ATF_CHECK_MSG(saw_file,
+           "file inside mode 0400 directory was not visited");
+
+       ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
+}
+
+/*
+ * PR 196724: FTS_SLNONE must not be returned for a non-symlink.
+ *
+ * The fix ensures that FTS_SLNONE is only returned when lstat confirms
+ * the entry is actually a symlink.  Exercised by a time-bounded race
+ * where a background thread creates and deletes a regular file while
+ * fts traverses with FTS_LOGICAL.
+ */
+ATF_TC(no_slnone_for_nonsymlink);
+ATF_TC_HEAD(no_slnone_for_nonsymlink, tc)
+{
+       atf_tc_set_md_var(tc, "descr",
+           "FTS_SLNONE must not be returned for a non-symlink");
+}
+ATF_TC_BODY(no_slnone_for_nonsymlink, tc)
+{
+       pthread_t thr;
+       char *paths[] = { "dir", NULL };
+       FTS *fts;
+       FTSENT *ent;
+       struct timespec start, now, elapsed;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE_EQ(0, symlink("nonexistent", "dir/dead"));
+
+       race_stop = false;
+       ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, race_thrash,
+           __DECONST(void *, "dir/victim")));
+
+       clock_gettime(CLOCK_MONOTONIC, &start);
+       for (;;) {
+               clock_gettime(CLOCK_MONOTONIC, &now);
+               timespecsub(&now, &start, &elapsed);
+               if (elapsed.tv_sec >= 1)
+                       break;
+               fts = fts_open(paths, FTS_LOGICAL, NULL);
+               ATF_REQUIRE(fts != NULL);
+               while ((ent = fts_read(fts)) != NULL) {
+                       if (ent->fts_info == FTS_SLNONE &&
+                           ent->fts_statp->st_mode != 0 &&
+                           !S_ISLNK(ent->fts_statp->st_mode))
+                               ATF_CHECK_MSG(0,
+                                   "FTS_SLNONE returned for non-symlink '%s'",
+                                   ent->fts_name);
+               }
+               fts_close(fts);
+       }
+
+       race_stop = true;
+       pthread_join(thr, NULL);
+}
+
+/*
+ * PR 262038: fts_build() must detect readdir(2) errors and not treat
+ * them as end-of-directory.  The man page specifies that FTS_DNR must
+ * immediately follow FTS_D, in place of FTS_DP.
+ *
+ * Requires an unprivileged user because root ignores permissions.
+ */
+ATF_TC(readdir_error_detected);
+ATF_TC_HEAD(readdir_error_detected, tc)
+{
+       atf_tc_set_md_var(tc, "descr",
+           "readdir errors produce FTS_DNR with fts_errno set");
+       atf_tc_set_md_var(tc, "require.user", "unprivileged");
+}
+ATF_TC_BODY(readdir_error_detected, tc)
+{
+       char *paths[] = { "dir", NULL };
+       FTS *fts;
+       FTSENT *ent;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE_EQ(0, close(creat("dir/file", 0644)));
+
+       /*
+        * Mode 0100: execute only, no read.  chdir() succeeds but
+        * opendir/readdir fails.  fts must return FTS_D then FTS_DNR
+        * (not FTS_DP) per the man page.
+        */
+       ATF_REQUIRE_EQ(0, chmod("dir", 0100));
+
+       ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
+
+       ATF_REQUIRE((ent = fts_read(fts)) != NULL);
+       ATF_CHECK_EQ_MSG(FTS_D, ent->fts_info,
+           "expected FTS_D, got %d", ent->fts_info);
+
+       ATF_REQUIRE((ent = fts_read(fts)) != NULL);
+       ATF_CHECK_EQ_MSG(FTS_DNR, ent->fts_info,
+           "expected FTS_DNR, got %d", ent->fts_info);
+       ATF_CHECK_MSG(ent->fts_errno != 0,
+           "FTS_DNR must have non-zero fts_errno");
+
+       ATF_REQUIRE_EQ_MSG(NULL, fts_read(fts),
+           "expected NULL after FTS_DNR");
+
+       ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
+}
+
+/*
+ * SVN r246641: fts_safe_changedir() uses O_DIRECTORY to prevent a
+ * TOCTOU substitution attack where a directory is replaced with a
+ * non-directory between stat and open.  Exercised by a time-bounded
+ * race where a background thread repeatedly removes and recreates
+ * dir/sub while fts traverses.
+ */
+ATF_TC(odirectory_changedir);
+ATF_TC_HEAD(odirectory_changedir, tc)
+{
+       atf_tc_set_md_var(tc, "descr",
+           "fts_safe_changedir handles concurrent dir/file substitution");
+}
+ATF_TC_BODY(odirectory_changedir, tc)
+{
+       pthread_t thr;
+       char *paths[] = { "dir", NULL };
+       FTS *fts;
+       struct timespec start, now, elapsed;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE_EQ(0, mkdir("dir/sub", 0755));
+       ATF_REQUIRE_EQ(0, close(creat("dir/sub/file", 0644)));
+
+       /*
+        * Background thread races to remove and recreate dir/sub as a
+        * directory.  With O_DIRECTORY the open fails safely if dir/sub
+        * is temporarily absent or replaced.
+        */
+       race_stop = false;
+       ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, dir_thrash,
+           __DECONST(void *, "dir/sub")));
+
+       clock_gettime(CLOCK_MONOTONIC, &start);
+       for (;;) {
+               clock_gettime(CLOCK_MONOTONIC, &now);
+               timespecsub(&now, &start, &elapsed);
+               if (elapsed.tv_sec >= 1)
+                       break;
+               fts = fts_open(paths, FTS_PHYSICAL, NULL);
+               ATF_REQUIRE(fts != NULL);
+               while (fts_read(fts) != NULL)
+                       ;
+               fts_close(fts);
+       }
+
+       race_stop = true;
+       pthread_join(thr, NULL);
+}
+
+/*
+ * SVN r261589: fts must not double-free when the directory tree is
+ * concurrently modified.  Exercised by a time-bounded race where a
+ * background thread creates and deletes a file during traversal.
+ */
+ATF_TC(concurrent_modification);
+ATF_TC_HEAD(concurrent_modification, tc)
+{
+       atf_tc_set_md_var(tc, "descr",
+           "no crash when tree modified during traversal");
+}
+ATF_TC_BODY(concurrent_modification, tc)
+{
+       pthread_t thr;
+       char *paths[] = { "dir", NULL };
+       FTS *fts;
+       struct timespec start, now, elapsed;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE_EQ(0, close(creat("dir/stable", 0644)));
+
+       race_stop = false;
+       ATF_REQUIRE_EQ(0, pthread_create(&thr, NULL, race_thrash,
+           __DECONST(void *, "dir/victim")));
+
+       clock_gettime(CLOCK_MONOTONIC, &start);
+       for (;;) {
+               clock_gettime(CLOCK_MONOTONIC, &now);
+               timespecsub(&now, &start, &elapsed);
+               if (elapsed.tv_sec >= 1)
+                       break;
+               fts = fts_open(paths, FTS_PHYSICAL, NULL);
+               ATF_REQUIRE(fts != NULL);
+               while (fts_read(fts) != NULL)
+                       ;
+               fts_close(fts);
+       }
+
+       race_stop = true;
+       pthread_join(thr, NULL);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+       ATF_TP_ADD_TC(tp, read_no_exec_dir);
+       ATF_TP_ADD_TC(tp, no_slnone_for_nonsymlink);
+       ATF_TP_ADD_TC(tp, readdir_error_detected);
+       ATF_TP_ADD_TC(tp, odirectory_changedir);
+       ATF_TP_ADD_TC(tp, concurrent_modification);
+
+       return (atf_no_error());
+}

Reply via email to