git: 7bf81e39d830 - main - ls: check fts_children() for errors that may not surface otherwise
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Wed, 11 Feb 2026 19:56:48 UTC
The branch main has been updated by kevans:
URL: https://cgit.FreeBSD.org/src/commit/?id=7bf81e39d83087dc7f984077b5eed5a48df794d4
commit 7bf81e39d83087dc7f984077b5eed5a48df794d4
Author: Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2026-02-11 19:55:55 +0000
Commit: Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2026-02-11 19:56:37 +0000
ls: check fts_children() for errors that may not surface otherwise
In particular, if one simply does a non-recursive `ls` on a directory
that is not accessible, there are some classes of errors that may cause
it to fail that wouldn't be surfaced unless we do an fts_read() that
will recurse into the inaccessible directory. Catch those kinds of
errors here since we cannot expect to an FTS_ERR/FTS_DNR entry to follow
up on them.
PR: 287451
Reviewed by: kib
Discusssed with: des
Differential Revision: https://reviews.freebsd.org/D51056
---
bin/ls/ls.c | 17 +++++++++++++++++
bin/ls/tests/ls_tests.sh | 30 ++++++++++++++++++++++++++++++
2 files changed, 47 insertions(+)
diff --git a/bin/ls/ls.c b/bin/ls/ls.c
index b3d0a508d714..c33d4d38c359 100644
--- a/bin/ls/ls.c
+++ b/bin/ls/ls.c
@@ -707,6 +707,23 @@ traverse(int argc, char *argv[], int options)
output = 1;
}
chp = fts_children(ftsp, ch_options);
+ if (chp == NULL && errno != 0) {
+ warn("%s", p->fts_path);
+ rval = 1;
+
+ /*
+ * Avoid further errors on this entry. We won't
+ * always get an FTS_ERR/FTS_DNR for errors
+ * in fts_children(), because opendir could
+ * have failed early on and that only flags an
+ * error for fts_read() when we try to recurse
+ * into it. We catch both the non-recursive and
+ * the recursive case here.
+ */
+ (void)fts_set(ftsp, p, FTS_SKIP);
+ break;
+ }
+
display(p, chp, options);
if (!f_recursive && chp != NULL)
diff --git a/bin/ls/tests/ls_tests.sh b/bin/ls/tests/ls_tests.sh
index c732b60b21a4..be662b75695d 100755
--- a/bin/ls/tests/ls_tests.sh
+++ b/bin/ls/tests/ls_tests.sh
@@ -476,6 +476,35 @@ b_flag_body()
atf_check -e empty -o match:'y\\vz' -s exit:0 ls -b
}
+atf_test_case childerr
+childerr_head()
+{
+ atf_set "descr" "Verify that fts_children() in pre-order errors are checked"
+ atf_set "require.user" "unprivileged"
+}
+
+childerr_body()
+{
+ atf_check mkdir -p root/dir root/edir
+ atf_check touch root/c
+
+ # Check that listing an empty directory hasn't regressed into being
+ # called an error.
+ atf_check -o match:"total 0" -e empty ls -l root/dir
+
+ atf_check chmod 0 root/dir
+
+ # If we did not abort after fts_children() properly, then stdout would
+ # have an output of the total files enumerated (0). Thus, assert that
+ # it's empty and that we see the correct error on stderr.
+ atf_check -s not-exit:0 -e match:"Permission denied" ls -l root/dir
+
+ # Now ensure that we didn't just stop there, we printed out a directory
+ # that would've been enumerated later.
+ atf_check -s not-exit:0 -o match:"^root/edir" \
+ -e match:"Permission denied" ls -lR root
+}
+
atf_test_case d_flag
d_flag_head()
{
@@ -971,6 +1000,7 @@ atf_init_test_cases()
#atf_add_test_case Z_flag
atf_add_test_case a_flag
atf_add_test_case b_flag
+ atf_add_test_case childerr
#atf_add_test_case c_flag
atf_add_test_case d_flag
atf_add_test_case f_flag