diff options
| author | Jitendra Bhati <bhatijitendra2022@gmail.com> | 2026-08-15 15:03:56 +0000 |
|---|---|---|
| committer | Alan Somers <asomers@FreeBSD.org> | 2026-08-16 21:02:43 +0000 |
| commit | c1ff9c93b107ef54b332bab114191e0a90ca84b7 (patch) | |
| tree | 647d70d372cd3e9557e6d0bc70e3217e83e93878 | |
| parent | 95a4412e3a4d41b430f298ea2c2a51f8aa119c2c (diff) | |
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
| -rw-r--r-- | lib/libc/gen/fts.c | 2 | ||||
| -rw-r--r-- | lib/libc/tests/gen/fts_regress_test.c | 58 |
2 files changed, 59 insertions, 1 deletions
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()); } |
