git: 74e3453b9051 - stable/13 - diff: Improve directory loop detection

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Sun, 15 Feb 2026 08:58:56 UTC
The branch stable/13 has been updated by des:

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

commit 74e3453b90512fb0777dfcd4f0150ff41ba28ad1
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2026-02-11 16:24:54 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2026-02-15 08:58:47 +0000

    diff: Improve directory loop detection
    
    When we're done processing a directory, remove its entry from the tree
    of visited inodes, ensuring that we only report a loop when we encounter
    a descendant-to-ancestor link, not when we encounter a cousin-to-cousin
    or sibling-to-sibling link.
    
    MFC after:      1 week
    Reported by:    Bakul Shah <bakul@iitbombay.org>
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans
    Differential Revision:  https://reviews.freebsd.org/D55248
    
    (cherry picked from commit 71569594d860a59d8362770a56d806e1d31fb946)
---
 usr.bin/diff/diffdir.c          | 20 +++++++++++++++++---
 usr.bin/diff/tests/diff_test.sh |  5 +++++
 2 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/usr.bin/diff/diffdir.c b/usr.bin/diff/diffdir.c
index 200613927715..6d71d0eeada5 100644
--- a/usr.bin/diff/diffdir.c
+++ b/usr.bin/diff/diffdir.c
@@ -59,7 +59,8 @@ static struct inodetree v2 = RB_INITIALIZER(&v2);
 RB_GENERATE_STATIC(inodetree, inode, entry, inodecmp);
 
 static int
-vscandir(struct inodetree *tree, const char *path, struct dirent ***dirp,
+vscandir(struct inodetree *tree, struct inode **inop,
+    const char *path, struct dirent ***dirp,
     int (*select)(const struct dirent *),
     int (*compar)(const struct dirent **, const struct dirent **))
 {
@@ -84,6 +85,7 @@ vscandir(struct inodetree *tree, const char *path, struct dirent ***dirp,
 		goto fail;
 	RB_INSERT(inodetree, tree, ino);
 	close(fd);
+	*inop = ino;
 	return (ret);
 fail:
 	serrno = errno;
@@ -95,6 +97,13 @@ fail:
 	return (-1);
 }
 
+static void
+leavedir(struct inodetree *tree, struct inode *ino)
+{
+	RB_REMOVE(inodetree, tree, ino);
+	free(ino);
+}
+
 /*
  * Diff directory traversal. Will be called recursively if -r was specified.
  */
@@ -103,6 +112,7 @@ diffdir(char *p1, char *p2, int flags)
 {
 	struct dirent *dent1, **dp1, **edp1, **dirp1 = NULL;
 	struct dirent *dent2, **dp2, **edp2, **dirp2 = NULL;
+	struct inode *ino1 = NULL, *ino2 = NULL;
 	size_t dirlen1, dirlen2;
 	char path1[PATH_MAX], path2[PATH_MAX];
 	int pos;
@@ -130,7 +140,7 @@ diffdir(char *p1, char *p2, int flags)
 	 * Get a list of entries in each directory, skipping "excluded" files
 	 * and sorting alphabetically.
 	 */
-	pos = vscandir(&v1, path1, &dirp1, selectfile, alphasort);
+	pos = vscandir(&v1, &ino1, path1, &dirp1, selectfile, alphasort);
 	if (pos == -1) {
 		if (errno == ENOENT && (Nflag || Pflag)) {
 			pos = 0;
@@ -142,7 +152,7 @@ diffdir(char *p1, char *p2, int flags)
 	dp1 = dirp1;
 	edp1 = dirp1 + pos;
 
-	pos = vscandir(&v2, path2, &dirp2, selectfile, alphasort);
+	pos = vscandir(&v2, &ino2, path2, &dirp2, selectfile, alphasort);
 	if (pos == -1) {
 		if (errno == ENOENT && Nflag) {
 			pos = 0;
@@ -216,11 +226,15 @@ diffdir(char *p1, char *p2, int flags)
 
 closem:
 	if (dirp1 != NULL) {
+		if (ino1 != NULL)
+			leavedir(&v1, ino1);
 		for (dp1 = dirp1; dp1 < edp1; dp1++)
 			free(*dp1);
 		free(dirp1);
 	}
 	if (dirp2 != NULL) {
+		if (ino2 != NULL)
+			leavedir(&v2, ino2);
 		for (dp2 = dirp2; dp2 < edp2; dp2++)
 			free(*dp2);
 		free(dirp2);
diff --git a/usr.bin/diff/tests/diff_test.sh b/usr.bin/diff/tests/diff_test.sh
index 4d9233da1b1e..df98540083f9 100755
--- a/usr.bin/diff/tests/diff_test.sh
+++ b/usr.bin/diff/tests/diff_test.sh
@@ -302,6 +302,11 @@ dirloop_body()
 	    -e match:"a/foo/bar/up: Directory loop detected" \
 	    -e match:"b/foo/bar/up: Directory loop detected" \
 	    diff -r a b
+	atf_check rm [ab]/foo/bar/up
+	atf_check mkdir -p b/foo/bar
+	atf_check ln -s foo a/baz
+	atf_check ln -s foo b/baz
+	atf_check diff -r a b
 }
 
 bigc_head()