Skip site navigation (1)Skip section navigation (2)
Date:      Fri, 20 Jun 2025 11:10:51 GMT
From:      Dag-Erling =?utf-8?Q?Sm=C3=B8rgrav?= <des@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org
Subject:   git: 42092e1b6625 - main - diff: Detect loops when diffing directories.
Message-ID:  <202506201110.55KBApY4051266@gitrepo.freebsd.org>

next in thread | raw e-mail | index | archive | help
The branch main has been updated by des:

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

commit 42092e1b6625b8226de5f34d22b9a96c713626cb
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-06-20 11:10:35 +0000
Commit:     Dag-Erling Smørgrav <des@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
 }



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?202506201110.55KBApY4051266>