git: c1ff9c93b107 - main - fts: fix trailing-slash regression in fts_read after fts_children
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
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());
}