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

Alan Somers <[email protected]>
Newsgroups gmane.os.freebsd.devel.cvs.src,gmane.os.freebsd.current.scm
Message-ID <[email protected]>
The branch main has been updated by asomers:

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

commit c1ff9c93b107ef54b332bab114191e0a90ca84b7
Author:     Jitendra Bhati <[email protected]>
AuthorDate: 2026-08-15 15:03:56 +0000
Commit:     Alan Somers <[email protected]>
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 <[email protected]>
    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());
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.