git: c1ff9c93b107 - main - fts: fix trailing-slash regression in fts_read after fts_children

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Sun, 16 Aug 2026 21:03:22 UTC
The branch main has been updated by asomers:

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

commit c1ff9c93b107ef54b332bab114191e0a90ca84b7
Author:     Jitendra Bhati <bhatijitendra2022@gmail.com>
AuthorDate: 2026-08-15 15:03:56 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2026-08-16 21:02:43 +0000

    fts: fix trailing-slash regression in fts_read after fts_children
    
    When fts_read() descends into a directory whose children were
    already prefetched by fts_children() (as ls -R does), it changed
    directory using p->fts_name instead of p->fts_accpath.
    
    With a trailing slash on a relative root path (e.g. 'dir/'),
    the bare name was resolved relative to the wrong directory, so
    every sibling directory after the first failed with ENOENT and
    was reported as FTS_DNR.  This manifested as 'ls -lR dir/'
    skipping the contents of all but the first subdirectory.
    
    Restore the use of p->fts_accpath, matching the behavior prior
    to 4bd01d6ae016.
    
    Add a regression test that reproduces the exact conditions:
    fts_children() on each directory, FTS_PHYSICAL without
    FTS_NOCHDIR, and a trailing slash on the root path.
    
    Reported by:    Michael Butler <imb@protected-networks.net>
    Reviewed by:    asomers
    Fixes:          4bd01d6ae016
    Sponsored by:   Google LLC (GSoC 2026)
    Pull Request:   https://github.com/freebsd/freebsd-src/pull/2372
---
 lib/libc/gen/fts.c                    |  2 +-
 lib/libc/tests/gen/fts_regress_test.c | 58 +++++++++++++++++++++++++++++++++++
 2 files changed, 59 insertions(+), 1 deletion(-)

diff --git a/lib/libc/gen/fts.c b/lib/libc/gen/fts.c
index e388fce550b9..96741dab8beb 100644
--- a/lib/libc/gen/fts.c
+++ b/lib/libc/gen/fts.c
@@ -489,7 +489,7 @@ fts_read(FTS *sp)
 		 * FTS_STOP or the fts_info field of the node.
 		 */
 		if (sp->fts_child != NULL) {
-			if (fts_safe_changedir(sp, p, -1, p->fts_name)) {
+			if (fts_safe_changedir(sp, p, -1, p->fts_accpath)) {
 				p->fts_errno = errno;
 				p->fts_flags |= FTS_DONTCHDIR;
 				for (p = sp->fts_child; p != NULL;
diff --git a/lib/libc/tests/gen/fts_regress_test.c b/lib/libc/tests/gen/fts_regress_test.c
index 7075addf92b4..57a29e7f1ec8 100644
--- a/lib/libc/tests/gen/fts_regress_test.c
+++ b/lib/libc/tests/gen/fts_regress_test.c
@@ -362,6 +362,63 @@ ATF_TC_BODY(accpath_correct_after_descent, tc)
 	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
 }
 
+ATF_TC(trailing_slash_children);
+ATF_TC_HEAD(trailing_slash_children, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "fts_children() works for all siblings when the root path "
+	    "has a trailing slash and FTS_NOCHDIR is not set");
+}
+ATF_TC_BODY(trailing_slash_children, tc)
+{
+	char *paths[] = { "root/", NULL };
+	FTS *fts;
+	FTSENT *ent, *children;
+	int files_seen = 0;
+
+	/*
+	 * Reproduce the bug reported on freebsd-current: "ls -lR dir/"
+	 * (trailing slash, relative path) skipped the contents of all
+	 * subdirectories after the first.  It requires fts_children()
+	 * to be called on each directory (as ls -R does), FTS_PHYSICAL
+	 * without FTS_NOCHDIR, and a trailing slash on the root path.
+	 *
+	 * The root cause was that fts_read() descended into each
+	 * subdirectory using fts_safe_changedir(sp, p, -1, p->fts_name)
+	 * instead of p->fts_accpath.  With a trailing-slash root, the
+	 * bare name was resolved relative to the wrong directory, so
+	 * every sibling after the first failed with ENOENT and was
+	 * reported as FTS_DNR.
+	 */
+	ATF_REQUIRE_EQ(0, mkdir("root", 0755));
+	ATF_REQUIRE_EQ(0, mkdir("root/sub1", 0755));
+	ATF_REQUIRE_EQ(0, mkdir("root/sub2", 0755));
+	ATF_REQUIRE_EQ(0, mkdir("root/sub3", 0755));
+	ATF_REQUIRE_EQ(0, close(creat("root/sub1/file1", 0644)));
+	ATF_REQUIRE_EQ(0, close(creat("root/sub2/file2", 0644)));
+	ATF_REQUIRE_EQ(0, close(creat("root/sub3/file3", 0644)));
+
+	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
+
+	while ((ent = fts_read(fts)) != NULL) {
+		if (ent->fts_info == FTS_D) {
+			children = fts_children(fts, 0);
+			(void)children;
+		}
+		if (ent->fts_info == FTS_DNR || ent->fts_info == FTS_ERR)
+			atf_tc_fail("fts entry '%s' returned info=%d "
+			    "errno=%d — subdirectory contents skipped",
+			    ent->fts_name, ent->fts_info, ent->fts_errno);
+		if (ent->fts_info == FTS_F)
+			files_seen++;
+	}
+
+	ATF_CHECK_EQ_MSG(3, files_seen,
+	    "expected to visit 3 files, saw %d — sibling directories "
+	    "were skipped after the first", files_seen);
+	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
+}
+
 ATF_TP_ADD_TCS(tp)
 {
 	ATF_TP_ADD_TC(tp, read_no_exec_dir);
@@ -370,6 +427,7 @@ ATF_TP_ADD_TCS(tp)
 	ATF_TP_ADD_TC(tp, odirectory_changedir);
 	ATF_TP_ADD_TC(tp, concurrent_modification);
 	ATF_TP_ADD_TC(tp, accpath_correct_after_descent);
+	ATF_TP_ADD_TC(tp, trailing_slash_children);
 
 	return (atf_no_error());
 }