git: 9b8c2bffeba9 - main - fts: fix file descriptor leak in fts_close

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=9b8c2bffeba9861c3f5bda0e5a08943e5ed4853f

commit 9b8c2bffeba9861c3f5bda0e5a08943e5ed4853f
Author:     Jitendra Bhati <[email protected]>
AuthorDate: 2026-08-15 16:06:39 +0000
Commit:     Alan Somers <[email protected]>
CommitDate: 2026-08-17 20:06:57 +0000

    fts: fix file descriptor leak in fts_close
    
    fts_build() stores a dup'd file descriptor in each directory
    entry's fts_dirfd.  When a traversal is abandoned before
    completion and fts_close() is called, the cleanup loop freed
    each pending entry with free() without first closing its
    fts_dirfd, leaking one descriptor per pending directory.
    
    Close fts_dirfd before freeing each entry in the cleanup loop,
    matching the handling already applied to the dummy parent entry
    after the loop.
    
    Add a regression test that descends a couple of levels, abandons
    the traversal, closes, and asserts the open descriptor count is
    unchanged.
    
    PR:             297557
    Reported by:    asomers
    Fixes:          4bd01d6ae016
    Sponsored by:   Google LLC (GSoC 2026)
    Reviewed by:    asomers
    Pull Request:   https://github.com/freebsd/freebsd-src/pull/2373
---
 lib/libc/gen/fts.c                 |  2 ++
 lib/libc/tests/gen/fts_misc_test.c | 66 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 68 insertions(+)

diff --git a/lib/libc/gen/fts.c b/lib/libc/gen/fts.c
index 96741dab8beb..66c5f62d9a8f 100644
--- a/lib/libc/gen/fts.c
+++ b/lib/libc/gen/fts.c
@@ -356,6 +356,8 @@ fts_close(FTS *sp)
 		for (p = sp->fts_cur; p->fts_level >= FTS_ROOTLEVEL;) {
 			freep = p;
 			p = p->fts_link != NULL ? p->fts_link : p->fts_parent;
+			if (freep->fts_dirfd >= 0)
+				(void)_close(freep->fts_dirfd);
 			free(freep);
 		}
 		if (p->fts_dirfd >= 0)
diff --git a/lib/libc/tests/gen/fts_misc_test.c b/lib/libc/tests/gen/fts_misc_test.c
index 95593e26095c..6f035a21e173 100644
--- a/lib/libc/tests/gen/fts_misc_test.c
+++ b/lib/libc/tests/gen/fts_misc_test.c
@@ -6,6 +6,7 @@
  */
 
 #include <sys/mount.h>
+#include <sys/sysctl.h>
 #include <sys/param.h>
 #include <sys/stat.h>
 #include <sys/syslimits.h>
@@ -576,6 +577,70 @@ ATF_TC_CLEANUP(xdev, tc)
 	(void)unmount("dir/mnt", 0);
 }
 
+/*
+ * Return the number of open file descriptors in the current process
+ * via the kern.proc.nfds sysctl.
+ */
+static int
+count_open_fds(void)
+{
+	int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_NFDS, 0 };
+	int nfds;
+	size_t len = sizeof(nfds);
+
+	ATF_REQUIRE_EQ_MSG(0,
+	    sysctl(mib, nitems(mib), &nfds, &len, NULL, 0),
+	    "sysctl(kern.proc.nfds): %m");
+	return (nfds);
+}
+
+ATF_TC(no_fd_leak_on_early_close);
+ATF_TC_HEAD(no_fd_leak_on_early_close, tc)
+{
+	atf_tc_set_md_var(tc, "descr",
+	    "fts_close must not leak directory fds when the traversal "
+	    "is abandoned before completion");
+}
+
+ATF_TC_BODY(no_fd_leak_on_early_close, tc)
+{
+	char *paths[] = { ".", NULL };
+	FTS *fts;
+	int fds_before, fds_after;
+
+	/*
+	 * Regression test for a file descriptor leak (bug 297557).
+	 * fts_build() stores a dup'd fd in each directory entry's
+	 * fts_dirfd.  If the traversal is abandoned partway through
+	 * and fts_close() is called, the cleanup loop must close
+	 * those fds; previously it freed the entries without closing
+	 * fts_dirfd, leaking one fd per pending directory.
+	 */
+	ATF_REQUIRE_EQ(0, mkdir("sub1", 0755));
+	ATF_REQUIRE_EQ(0, mkdir("sub1/deep", 0755));
+	ATF_REQUIRE_EQ(0, close(creat("sub1/deep/file", 0644)));
+
+	fds_before = count_open_fds();
+
+	/*
+	 * Open, descend a couple of levels, then abandon the traversal
+	 * and close.  The directory entries for '.' and 'sub1' hold
+	 * dup'd fds that must be released by fts_close().
+	 */
+	ATF_REQUIRE((fts = fts_open(paths, FTS_PHYSICAL, NULL)) != NULL);
+	ATF_REQUIRE(fts_read(fts) != NULL);	/* . */
+	ATF_REQUIRE(fts_read(fts) != NULL);	/* sub1 */
+	ATF_REQUIRE(fts_read(fts) != NULL);	/* deep */
+	ATF_REQUIRE_EQ_MSG(0, fts_close(fts), "fts_close(): %m");
+
+	fds_after = count_open_fds();
+
+	ATF_CHECK_EQ_MSG(fds_before, fds_after,
+	    "fts_close leaked file descriptors: %d open before, "
+	    "%d after", fds_before, fds_after);
+}
+
+
 ATF_TP_ADD_TCS(tp)
 {
 	fts_check_debug();
@@ -592,6 +657,7 @@ ATF_TP_ADD_TCS(tp)
 	ATF_TP_ADD_TC(tp, nochdir_empty_terminal_dir);
 	ATF_TP_ADD_TC(tp, ns_errno_set);
 	ATF_TP_ADD_TC(tp, xdev);
+	ATF_TP_ADD_TC(tp, no_fd_leak_on_early_close);
 
 	return (atf_no_error());
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.