git » summer » commit 19e61a8

walk: Check filesystem boundaries on directories

author Alberto Bertogli
2026-03-18 01:06:35 UTC
committer Alberto Bertogli
2026-03-18 01:23:13 UTC
parent a71d138e4ae92fa508a4e3f6d641a62f0c6304ec

walk: Check filesystem boundaries on directories

Currently, we check filesystem boundaries on the first files we find.

This avoids a fstat() call on directories, but we end up doing them on
the first files of each place instead. Depending on the layout of the
second filesystem, this can be more expensive, and also we end up
crossing the boundary even if it's just for a few stat calls.

This patch makes the walk check filesystem boundaries on directories
instead, which is cleaner and more correct.

It also adds a test so we can confirm the behaviour.

test/onefilesystem.t +15 -0
test/onefilesystem.tt +40 -0
walk.go +13 -5

diff --git a/test/onefilesystem.t b/test/onefilesystem.t
new file mode 100644
index 0000000..146cd53
--- /dev/null
+++ b/test/onefilesystem.t
@@ -0,0 +1,15 @@
+
+Tests for -x (one filesystem) flag.
+
+We use unshare to create a user+mount namespace, mount a tmpfs inside the test
+tree, and verify that -x prevents crossing the boundary.
+
+Skip if we can't create user+mount namespaces (e.g. CI environments).
+
+  $ unshare --user --mount --map-root-user true 2>/dev/null || exit 80
+
+Run the rest of the test inside the namespace, so we can mount a tmpfs.
+
+  $ unshare --user --mount --map-root-user cram3 $TESTDIR/onefilesystem.tt
+  .
+  # Ran 1 tests, 0 skipped, 0 failed.
diff --git a/test/onefilesystem.tt b/test/onefilesystem.tt
new file mode 100644
index 0000000..e5270cc
--- /dev/null
+++ b/test/onefilesystem.tt
@@ -0,0 +1,40 @@
+
+Note this runs inside unshare so we can mount things.
+
+Prepare the directories, files and mounts.
+
+  $ mkdir -p root/mnt root/dir
+  $ echo aaa > root/file1
+  $ echo bbb > root/dir/file2
+  $ mount -t tmpfs none root/mnt
+  $ echo ccc > root/mnt/file3
+  $ mkdir root/mnt/subdir
+  $ echo ddd > root/mnt/subdir/file4
+
+Generate with -x: only root device files should get checksums.
+
+  $ alias summer="$TESTDIR/../summer"
+  $ summer -v --parallel=1 -x generate root
+  "root/dir/file2": writing checksum \(checksum:\w+, mtime:\d+\) (re)
+  "root/file1": writing checksum \(checksum:\w+, mtime:\d+\) (re)
+  0s: 0 matched, 0 modified, 2 new, 0 corrupted
+
+Verify with -x: should see only root device files.
+
+  $ summer -v --parallel=1 -x verify root
+  "root/dir/file2": match \(checksum:\w+, mtime:\d+\) (re)
+  "root/file1": match \(checksum:\w+, mtime:\d+\) (re)
+  0s: 2 matched, 0 modified, 0 new, 0 corrupted
+
+Verify without -x: root device files match, mount files are missing.
+
+  $ summer -v --parallel=1 verify root
+  "root/dir/file2": match \(checksum:\w+, mtime:\d+\) (re)
+  "root/file1": match \(checksum:\w+, mtime:\d+\) (re)
+  "root/mnt/file3": missing checksum attribute
+  "root/mnt/subdir/file4": missing checksum attribute
+  0s: 2 matched, 0 modified, 2 new, 0 corrupted
+
+Unmount so the test directory can be gracefully cleaned up.
+
+  $ umount root/mnt
diff --git a/walk.go b/walk.go
index f8f0383..d41b829 100644
--- a/walk.go
+++ b/walk.go
@@ -22,6 +22,19 @@ func openAndInfo(path string, d fs.DirEntry, err error, rootDev deviceID) (bool,
 	if err != nil {
 		return false, nil, nil, err
 	}
+
+	// Check filesystem boundaries for directories, so we skip entire
+	// subtrees on other filesystems without descending into them.
+	if options.oneFilesystem && d.IsDir() {
+		info, err := d.Info()
+		if err != nil {
+			return false, nil, nil, err
+		}
+		if rootDev != getDevice(info) {
+			return false, nil, nil, fs.SkipDir
+		}
+	}
+
 	if d.IsDir() || !d.Type().IsRegular() {
 		return false, nil, nil, nil
 	}
@@ -45,11 +58,6 @@ func openAndInfo(path string, d fs.DirEntry, err error, rootDev deviceID) (bool,
 		return true, nil, nil, err
 	}
 
-	if options.oneFilesystem && rootDev != getDevice(info) {
-		fd.Close()
-		return false, nil, nil, fs.SkipDir
-	}
-
 	return true, fd, info, nil
 }