The branch stable/13 has been updated by des:

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

commit bfd89763d2079a6f232e8324fc2c688cb336aed9
Author:     Dag-Erling Smørgrav <[email protected]>
AuthorDate: 2025-06-20 11:10:35 +0000
Commit:     Dag-Erling Smørgrav <[email protected]>
CommitDate: 2026-02-10 14:24:30 +0000

    diff: Detect loops when diffing directories.
    
    Sponsored by:   Klara, Inc.
    Reviewed by:    markj
    Differential Revision:  https://reviews.freebsd.org/D50936
    
    (cherry picked from commit 42092e1b6625b8226de5f34d22b9a96c713626cb)
---
 usr.bin/diff/diffdir.c          | 88 +++++++++++++++++++++++++++++++++++------
 usr.bin/diff/tests/diff_test.sh | 17 ++++++++
 2 files changed, 94 insertions(+), 11 deletions(-)

diff --git a/usr.bin/diff/diffdir.c b/usr.bin/diff/diffdir.c
index 1ffdf0a8de5b..627e5dd9b983 100644
--- a/usr.bin/diff/diffdir.c
+++ b/usr.bin/diff/diffdir.c
@@ -22,15 +22,18 @@
 
 #include <sys/cdefs.h>
 #include <sys/stat.h>
+#include <sys/tree.h>
 
 #include <dirent.h>
 #include <err.h>
 #include <errno.h>
+#include <fcntl.h>
 #include <fnmatch.h>
+#include <limits.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <limits.h>
+#include <unistd.h>
 
 #include "diff.h"
 
@@ -39,6 +42,61 @@ static void diffit(struct dirent *, char *, size_t, char *, 
size_t, int);
 
 #define d_status       d_type          /* we need to store status for -l */
 
+struct inode {
+       dev_t dev;
+       ino_t ino;
+       RB_ENTRY(inode) entry;
+};
+
+static int
+inodecmp(struct inode *a, struct inode *b)
+{
+       return (a->dev < b->dev ? -1 : a->dev > b->dev ? 1 :
+           a->ino < b->ino ? -1 : a->ino > b->ino ? 1 : 0);
+}
+
+RB_HEAD(inodetree, inode);
+static struct inodetree v1 = RB_INITIALIZER(&v1);
+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,
+    int (*select)(const struct dirent *),
+    int (*compar)(const struct dirent **, const struct dirent **))
+{
+       struct stat sb;
+       struct inode *ino = NULL;
+       int fd = -1, ret, serrno;
+
+       if ((fd = open(path, O_DIRECTORY | O_RDONLY)) < 0 ||
+           (ino = calloc(1, sizeof(*ino))) == NULL ||
+           fstat(fd, &sb) != 0)
+               goto fail;
+       ino->dev = sb.st_dev;
+       ino->ino = sb.st_ino;
+       if (RB_FIND(inodetree, tree, ino)) {
+               free(ino);
+               close(fd);
+               warnx("%s: Directory loop detected", path);
+               *dirp = NULL;
+               return (0);
+       }
+       if ((ret = scandir(path, dirp, select, compar)) < 0)
+               goto fail;
+       RB_INSERT(inodetree, tree, ino);
+       close(fd);
+       return (ret);
+fail:
+       serrno = errno;
+       if (ino != NULL)
+               free(ino);
+       if (fd >= 0)
+               close(fd);
+       errno = serrno;
+       return (-1);
+}
+
 /*
  * Diff directory traversal. Will be called recursively if -r was specified.
  */
@@ -59,26 +117,22 @@ diffdir(char *p1, char *p2, int flags)
                status |= 2;
                return;
        }
-       if (path1[dirlen1 - 1] != '/') {
-               path1[dirlen1++] = '/';
-               path1[dirlen1] = '\0';
-       }
+       while (dirlen1 > 1 && path1[dirlen1 - 1] == '/')
+               path1[--dirlen1] = '\0';
        dirlen2 = strlcpy(path2, *p2 ? p2 : ".", sizeof(path2));
        if (dirlen2 >= sizeof(path2) - 1) {
                warnc(ENAMETOOLONG, "%s", p2);
                status |= 2;
                return;
        }
-       if (path2[dirlen2 - 1] != '/') {
-               path2[dirlen2++] = '/';
-               path2[dirlen2] = '\0';
-       }
+       while (dirlen2 > 1 && path2[dirlen2 - 1] == '/')
+               path2[--dirlen2] = '\0';
 
        /*
         * Get a list of entries in each directory, skipping "excluded" files
         * and sorting alphabetically.
         */
-       pos = scandir(path1, &dirp1, selectfile, alphasort);
+       pos = vscandir(&v1, path1, &dirp1, selectfile, alphasort);
        if (pos == -1) {
                if (errno == ENOENT && (Nflag || Pflag)) {
                        pos = 0;
@@ -90,7 +144,7 @@ diffdir(char *p1, char *p2, int flags)
        dp1 = dirp1;
        edp1 = dirp1 + pos;
 
-       pos = scandir(path2, &dirp2, selectfile, alphasort);
+       pos = vscandir(&v2, path2, &dirp2, selectfile, alphasort);
        if (pos == -1) {
                if (errno == ENOENT && Nflag) {
                        pos = 0;
@@ -112,6 +166,18 @@ diffdir(char *p1, char *p2, int flags)
                        dp2++;
        }
 
+       /*
+        * Append separator so children's names can be appended directly.
+        */
+       if (path1[dirlen1 - 1] != '/') {
+               path1[dirlen1++] = '/';
+               path1[dirlen1] = '\0';
+       }
+       if (path2[dirlen2 - 1] != '/') {
+               path2[dirlen2++] = '/';
+               path2[dirlen2] = '\0';
+       }
+
        /*
         * Iterate through the two directory lists, diffing as we go.
         */
diff --git a/usr.bin/diff/tests/diff_test.sh b/usr.bin/diff/tests/diff_test.sh
index 29ab0eaec161..027febf69f64 100755
--- a/usr.bin/diff/tests/diff_test.sh
+++ b/usr.bin/diff/tests/diff_test.sh
@@ -19,6 +19,7 @@ atf_test_case label
 atf_test_case report_identical
 atf_test_case non_regular_file
 atf_test_case binary
+atf_test_case dirloop
 
 simple_body()
 {
@@ -284,6 +285,21 @@ binary_body()
        atf_check -o inline:"176c\nx\n.\n" -s exit:1 diff -ae A B
 }
 
+dirloop_head()
+{
+       atf_set "timeout" "10"
+}
+dirloop_body()
+{
+       atf_check mkdir -p a/foo/bar
+       atf_check ln -s .. a/foo/bar/up
+       atf_check cp -a a b
+       atf_check \
+           -e match:"a/foo/bar/up: Directory loop detected" \
+           -e match:"b/foo/bar/up: Directory loop detected" \
+           diff -r a b
+}
+
 atf_init_test_cases()
 {
        atf_add_test_case simple
@@ -306,4 +322,5 @@ atf_init_test_cases()
        atf_add_test_case report_identical
        atf_add_test_case non_regular_file
        atf_add_test_case binary
+       atf_add_test_case dirloop
 }

Reply via email to