git: 42092e1b6625 - main - diff: Detect loops when diffing directories.
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Fri, 20 Jun 2025 11:10:51 UTC
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
}