The branch main has been updated by des:

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

commit dd81cc2bc5f25fb2c58bb59bf585deeeae196345
Author:     Dag-Erling Smørgrav <d...@freebsd.org>
AuthorDate: 2025-07-09 20:34:18 +0000
Commit:     Dag-Erling Smørgrav <d...@freebsd.org>
CommitDate: 2025-07-09 20:34:18 +0000

    getdirentries: Return ENOTDIR if not a directory.
    
    This is both more logical and more useful than EINVAL.
    
    While here, also check for VBAD and return EBADF in that case.  This can
    happen if the underlying filesystem got forcibly unmounted after the
    directory was opened.  Previously, this would also have returned EINVAL,
    which wasn't right but wasn't wrong either; however, ENOTDIR would not
    be appropriate.
    
    MFC after:      never
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans, kib
    Differential Revision:  https://reviews.freebsd.org/D51209
---
 lib/libc/gen/opendir2.c             |   5 +-
 lib/libsys/getdirentries.2          |  10 ++-
 sys/kern/vfs_syscalls.c             |  17 +++-
 tests/sys/kern/Makefile             |   1 +
 tests/sys/kern/getdirentries_test.c | 172 ++++++++++++++++++++++++++++++++++++
 5 files changed, 193 insertions(+), 12 deletions(-)

diff --git a/lib/libc/gen/opendir2.c b/lib/libc/gen/opendir2.c
index 928145b468c1..7f207ed73441 100644
--- a/lib/libc/gen/opendir2.c
+++ b/lib/libc/gen/opendir2.c
@@ -315,11 +315,8 @@ __opendir_common(int fd, int flags, bool use_current_pos)
                         */
                        dirp->dd_size = _getdirentries(dirp->dd_fd,
                            dirp->dd_buf, dirp->dd_len, &dirp->dd_seek);
-                       if (dirp->dd_size < 0) {
-                               if (errno == EINVAL)
-                                       errno = ENOTDIR;
+                       if (dirp->dd_size < 0)
                                goto fail;
-                       }
                        dirp->dd_flags |= __DTF_SKIPREAD;
                } else {
                        dirp->dd_size = 0;
diff --git a/lib/libsys/getdirentries.2 b/lib/libsys/getdirentries.2
index 0e5840ce25cd..202ae133f548 100644
--- a/lib/libsys/getdirentries.2
+++ b/lib/libsys/getdirentries.2
@@ -25,7 +25,7 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd September 5, 2023
+.Dd July 8, 2025
 .Dt GETDIRENTRIES 2
 .Os
 .Sh NAME
@@ -178,9 +178,7 @@ or non-NULL
 .Fa basep
 point outside the allocated address space.
 .It Bq Er EINVAL
-The file referenced by
-.Fa fd
-is not a directory, or
+The value of
 .Fa nbytes
 is too small for returning a directory entry or block of entries,
 or the current position pointer is invalid.
@@ -192,6 +190,10 @@ error occurred while reading from or writing to the file 
system.
 Corrupted data was detected while reading from the file system.
 .It Bq Er ENOENT
 Directory unlinked but still open.
+.It Bq Er ENOTDIR
+The file referenced by
+.Fa fd
+is not a directory.
 .El
 .Sh SEE ALSO
 .Xr lseek 2 ,
diff --git a/sys/kern/vfs_syscalls.c b/sys/kern/vfs_syscalls.c
index d880733cbfe7..c71e0d9ee569 100644
--- a/sys/kern/vfs_syscalls.c
+++ b/sys/kern/vfs_syscalls.c
@@ -4314,10 +4314,6 @@ kern_getdirentries(struct thread *td, int fd, char *buf, 
size_t count,
        vp = fp->f_vnode;
        foffset = foffset_lock(fp, 0);
 unionread:
-       if (vp->v_type != VDIR) {
-               error = EINVAL;
-               goto fail;
-       }
        if (__predict_false((vp->v_vflag & VV_UNLINKED) != 0)) {
                error = ENOENT;
                goto fail;
@@ -4330,6 +4326,19 @@ unionread:
        auio.uio_segflg = bufseg;
        auio.uio_td = td;
        vn_lock(vp, LK_SHARED | LK_RETRY);
+       /*
+        * We want to return ENOTDIR for anything that is not VDIR, but
+        * not for VBAD, and we can't check for VBAD while the vnode is
+        * unlocked.
+        */
+       if (vp->v_type != VDIR) {
+               if (vp->v_type == VBAD)
+                       error = EBADF;
+               else
+                       error = ENOTDIR;
+               VOP_UNLOCK(vp);
+               goto fail;
+       }
        AUDIT_ARG_VNODE1(vp);
        loff = auio.uio_offset = foffset;
 #ifdef MAC
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index 26c0013696c7..f2c24ad9dec9 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -18,6 +18,7 @@ ATF_TESTS_C+= kern_descrip_test
 # One test modifies the maxfiles limit, which can cause spurious test failures.
 TEST_METADATA.kern_descrip_test+= is_exclusive="true"
 ATF_TESTS_C+=  fdgrowtable_test
+ATF_TESTS_C+=  getdirentries_test
 ATF_TESTS_C+=  jail_lookup_root
 ATF_TESTS_C+=  inotify_test
 ATF_TESTS_C+=  kill_zombie
diff --git a/tests/sys/kern/getdirentries_test.c 
b/tests/sys/kern/getdirentries_test.c
new file mode 100644
index 000000000000..e66872ffe5b6
--- /dev/null
+++ b/tests/sys/kern/getdirentries_test.c
@@ -0,0 +1,172 @@
+/*-
+ * Copyright (c) 2025 Klara, Inc.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/stat.h>
+#include <sys/mount.h>
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdint.h>
+
+#include <atf-c.h>
+
+ATF_TC(getdirentries_ok);
+ATF_TC_HEAD(getdirentries_ok, tc)
+{
+       atf_tc_set_md_var(tc, "descr", "Successfully read a directory.");
+}
+ATF_TC_BODY(getdirentries_ok, tc)
+{
+       char dbuf[4096];
+       struct dirent *d;
+       off_t base;
+       ssize_t ret;
+       int dd, n;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0);
+       ATF_REQUIRE((ret = getdirentries(dd, dbuf, sizeof(dbuf), &base)) > 0);
+       ATF_REQUIRE_EQ(0, getdirentries(dd, dbuf, sizeof(dbuf), &base));
+       ATF_REQUIRE_EQ(base, lseek(dd, 0, SEEK_CUR));
+       ATF_CHECK_EQ(0, close(dd));
+       for (n = 0, d = (struct dirent *)dbuf;
+            d < (struct dirent *)(dbuf + ret);
+            d = (struct dirent *)((char *)d + d->d_reclen), n++)
+               /* nothing */ ;
+       ATF_CHECK_EQ((struct dirent *)(dbuf + ret), d);
+       ATF_CHECK_EQ(2, n);
+}
+
+ATF_TC(getdirentries_ebadf);
+ATF_TC_HEAD(getdirentries_ebadf, tc)
+{
+       atf_tc_set_md_var(tc, "descr", "Attempt to read a directory "
+           "from an invalid descriptor.");
+}
+ATF_TC_BODY(getdirentries_ebadf, tc)
+{
+       char dbuf[4096];
+       off_t base;
+       int fd;
+
+       ATF_REQUIRE((fd = open("file", O_CREAT | O_WRONLY, 0644)) >= 0);
+       ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base));
+       ATF_CHECK_EQ(EBADF, errno);
+       ATF_REQUIRE_EQ(0, close(fd));
+       ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base));
+       ATF_CHECK_EQ(EBADF, errno);
+}
+
+ATF_TC(getdirentries_efault);
+ATF_TC_HEAD(getdirentries_efault, tc)
+{
+       atf_tc_set_md_var(tc, "descr", "Attempt to read a directory "
+           "to an invalid buffer.");
+}
+ATF_TC_BODY(getdirentries_efault, tc)
+{
+       char dbuf[4096];
+       off_t base, *basep;
+       int dd;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0);
+       ATF_REQUIRE_EQ(-1, getdirentries(dd, NULL, sizeof(dbuf), &base));
+       ATF_CHECK_EQ(EFAULT, errno);
+       basep = NULL;
+       basep++;
+       ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), basep));
+       ATF_CHECK_EQ(EFAULT, errno);
+       ATF_CHECK_EQ(0, close(dd));
+}
+
+ATF_TC(getdirentries_einval);
+ATF_TC_HEAD(getdirentries_einval, tc)
+{
+       atf_tc_set_md_var(tc, "descr", "Attempt to read a directory "
+           "with various invalid parameters.");
+}
+ATF_TC_BODY(getdirentries_einval, tc)
+{
+       struct statfs fsb;
+       char dbuf[4096];
+       off_t base;
+       ssize_t ret;
+       int dd;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0);
+       ATF_REQUIRE_EQ(0, fstatfs(dd, &fsb));
+       /* nbytes too small */
+       ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, 8, &base));
+       ATF_CHECK_EQ(EINVAL, errno);
+       /* nbytes too big */
+       ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, SIZE_MAX, &base));
+       ATF_CHECK_EQ(EINVAL, errno);
+       /* invalid position */
+       ATF_REQUIRE((ret = getdirentries(dd, dbuf, sizeof(dbuf), &base)) > 0);
+       ATF_REQUIRE_EQ(0, getdirentries(dd, dbuf, sizeof(dbuf), &base));
+       ATF_REQUIRE(base > 0);
+       ATF_REQUIRE_EQ(base + 3, lseek(dd, 3, SEEK_CUR));
+       /* known to fail on ufs (FFS2) and zfs, and work on tmpfs */
+       if (strcmp(fsb.f_fstypename, "ufs") == 0 ||
+           strcmp(fsb.f_fstypename, "zfs") == 0) {
+               atf_tc_expect_fail("incorrectly returns 0 instead of EINVAL "
+                   "on %s", fsb.f_fstypename);
+       }
+       ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), &base));
+       ATF_CHECK_EQ(EINVAL, errno);
+       ATF_CHECK_EQ(0, close(dd));
+}
+
+ATF_TC(getdirentries_enoent);
+ATF_TC_HEAD(getdirentries_enoent, tc)
+{
+       atf_tc_set_md_var(tc, "descr", "Attempt to read a directory "
+           "after it is deleted.");
+}
+ATF_TC_BODY(getdirentries_enoent, tc)
+{
+       char dbuf[4096];
+       off_t base;
+       int dd;
+
+       ATF_REQUIRE_EQ(0, mkdir("dir", 0755));
+       ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0);
+       ATF_REQUIRE_EQ(0, rmdir("dir"));
+       ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), &base));
+       ATF_CHECK_EQ(ENOENT, errno);
+}
+
+ATF_TC(getdirentries_enotdir);
+ATF_TC_HEAD(getdirentries_enotdir, tc)
+{
+       atf_tc_set_md_var(tc, "descr", "Attempt to read a directory "
+           "from a descriptor not associated with a directory.");
+}
+ATF_TC_BODY(getdirentries_enotdir, tc)
+{
+       char dbuf[4096];
+       off_t base;
+       int fd;
+
+       ATF_REQUIRE((fd = open("file", O_CREAT | O_RDWR, 0644)) >= 0);
+       ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base));
+       ATF_CHECK_EQ(ENOTDIR, errno);
+       ATF_CHECK_EQ(0, close(fd));
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+       ATF_TP_ADD_TC(tp, getdirentries_ok);
+       ATF_TP_ADD_TC(tp, getdirentries_ebadf);
+       ATF_TP_ADD_TC(tp, getdirentries_efault);
+       ATF_TP_ADD_TC(tp, getdirentries_einval);
+       ATF_TP_ADD_TC(tp, getdirentries_enoent);
+       ATF_TP_ADD_TC(tp, getdirentries_enotdir);
+       return (atf_no_error());
+}

Reply via email to