The branch main has been updated by des:

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

commit 42092e1b6625b8226de5f34d22b9a96c713626cb
Author:     Dag-Erling Smørgrav <d...@freebsd.org>
AuthorDate: 2025-06-20 11:10:35 +0000
Commit:     Dag-Erling Smørgrav <d...@freebsd.org>
CommitDate: 2025-06-20 11:10:35 +0000

    diff: Detect loops when diffing directories.
    
    Sponsored by:   Klara, Inc.
    Reviewed by:    markj
    Differential Revision:  https://reviews.freebsd.org/D50936
---
 usr.bin/diff/diffdir.c          | 85 ++++++++++++++++++++++++++++++++++++-----
 usr.bin/diff/tests/diff_test.sh | 17 +++++++++
 2 files changed, 92 insertions(+), 10 deletions(-)

diff --git a/usr.bin/diff/diffdir.c b/usr.bin/diff/diffdir.c
index 8d12e868f90e..df65a84ca391 100644
--- a/usr.bin/diff/diffdir.c
+++ b/usr.bin/diff/diffdir.c
@@ -21,10 +21,12 @@
  */
 
 #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>
@@ -41,6 +43,61 @@ static void print_only(const char *, size_t, const char *);
 
 #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 = fscandir(fd, 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.
  */
@@ -61,26 +118,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;
@@ -92,7 +145,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;
@@ -114,6 +167,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 ab731fa32d26..691b649813a1 100755
--- a/usr.bin/diff/tests/diff_test.sh
+++ b/usr.bin/diff/tests/diff_test.sh
@@ -23,6 +23,7 @@ atf_test_case binary
 atf_test_case functionname
 atf_test_case noderef
 atf_test_case ignorecase
+atf_test_case dirloop
 
 simple_body()
 {
@@ -364,6 +365,21 @@ ignorecase_body()
        atf_check -o empty -s exit:0 diff -u -r --ignore-file-name-case 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
@@ -390,4 +406,5 @@ atf_init_test_cases()
        atf_add_test_case functionname
        atf_add_test_case noderef
        atf_add_test_case ignorecase
+       atf_add_test_case dirloop
 }

Reply via email to