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()); }