From b413b91c317ffb2a360a5c4e0d952806689e0700 Mon Sep 17 00:00:00 2001 From: Markus Tervooren Date: Sun, 30 Aug 2026 22:27:19 +0200 Subject: [PATCH 1/3] mv symlink exponential walk fix --- src/uu/mv/src/hardlink.rs | 31 ++++++++++++++----------------- tests/by-util/test_mv.rs | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/uu/mv/src/hardlink.rs b/src/uu/mv/src/hardlink.rs index 46ca3329450..db57723680b 100644 --- a/src/uu/mv/src/hardlink.rs +++ b/src/uu/mv/src/hardlink.rs @@ -215,18 +215,16 @@ impl HardlinkGroupScanner { fn scan_single_path(&mut self, path: &Path) -> io::Result<()> { use std::os::unix::fs::MetadataExt; - if path.is_dir() { + let metadata = path.symlink_metadata()?; + if metadata.is_dir() { // Recursively scan directory contents self.scan_directory_recursive(path)?; - } else { - let metadata = path.symlink_metadata()?; - if metadata.is_file() && metadata.nlink() > 1 { - let key = (metadata.dev(), metadata.ino()); - self.hardlink_groups - .entry(key) - .or_default() - .push(path.to_path_buf()); - } + } else if metadata.is_file() && metadata.nlink() > 1 { + let key = (metadata.dev(), metadata.ino()); + self.hardlink_groups + .entry(key) + .or_default() + .push(path.to_path_buf()); } Ok(()) } @@ -240,14 +238,13 @@ impl HardlinkGroupScanner { let entry = entry?; let path = entry.path(); - if path.is_dir() { + // never descend through a symlink. + let metadata = path.symlink_metadata()?; + if metadata.is_dir() { self.scan_directory_recursive(&path)?; - } else { - let metadata = path.symlink_metadata()?; - if metadata.is_file() && metadata.nlink() > 1 { - let key = (metadata.dev(), metadata.ino()); - self.hardlink_groups.entry(key).or_default().push(path); - } + } else if metadata.is_file() && metadata.nlink() > 1 { + let key = (metadata.dev(), metadata.ino()); + self.hardlink_groups.entry(key).or_default().push(path); } } Ok(()) diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 7dfd5530507..77aaa470c95 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -1988,6 +1988,28 @@ fn test_move_should_not_fallback_to_copy() { assert!(!at.file_exists(target_file)); } +// A directory containing two symlinks that point back at an ancestor must not +// send the hardlink pre-scan into an exponential walk. +#[test] +#[cfg(unix)] +fn test_mv_dir_with_symlink_cycles_terminates() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkdir("dir"); + at.mkdir("dest"); + at.write("dir/file", "content"); + at.relative_symlink_dir(".", "dir/loop1"); + at.relative_symlink_dir(".", "dir/loop2"); + + ucmd.arg("dir").arg("dest/").succeeds().no_output(); + + assert!(at.dir_exists("dest/dir")); + assert_eq!(at.read("dest/dir/file"), "content"); + assert!(at.is_symlink("dest/dir/loop1")); + assert!(at.is_symlink("dest/dir/loop2")); + assert!(!at.dir_exists("dir")); +} + // Todo: // $ at.touch a b From 3622fa87dd34280136cafb979c65def8bc1e9f1b Mon Sep 17 00:00:00 2001 From: Markus Tervooren Date: Sun, 30 Aug 2026 23:16:52 +0200 Subject: [PATCH 2/3] symlink_metadata only on the non-directory branch --- src/uu/mv/src/hardlink.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/uu/mv/src/hardlink.rs b/src/uu/mv/src/hardlink.rs index db57723680b..7be44a2029d 100644 --- a/src/uu/mv/src/hardlink.rs +++ b/src/uu/mv/src/hardlink.rs @@ -238,13 +238,14 @@ impl HardlinkGroupScanner { let entry = entry?; let path = entry.path(); - // never descend through a symlink. - let metadata = path.symlink_metadata()?; - if metadata.is_dir() { + if entry.file_type()?.is_dir() { self.scan_directory_recursive(&path)?; - } else if metadata.is_file() && metadata.nlink() > 1 { - let key = (metadata.dev(), metadata.ino()); - self.hardlink_groups.entry(key).or_default().push(path); + } else { + let metadata = path.symlink_metadata()?; + if metadata.is_file() && metadata.nlink() > 1 { + let key = (metadata.dev(), metadata.ino()); + self.hardlink_groups.entry(key).or_default().push(path); + } } } Ok(()) From cb0812c369a6ff008aab0603e360fb0096930574 Mon Sep 17 00:00:00 2001 From: Markus Tervooren Date: Mon, 31 Aug 2026 00:11:39 +0200 Subject: [PATCH 3/3] use entry.metadata for files --- src/uu/mv/src/hardlink.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/uu/mv/src/hardlink.rs b/src/uu/mv/src/hardlink.rs index 7be44a2029d..1ee3c64ace0 100644 --- a/src/uu/mv/src/hardlink.rs +++ b/src/uu/mv/src/hardlink.rs @@ -236,15 +236,17 @@ impl HardlinkGroupScanner { let entries = std::fs::read_dir(dir)?; for entry in entries { let entry = entry?; - let path = entry.path(); if entry.file_type()?.is_dir() { - self.scan_directory_recursive(&path)?; + self.scan_directory_recursive(&entry.path())?; } else { - let metadata = path.symlink_metadata()?; + let metadata = entry.metadata()?; if metadata.is_file() && metadata.nlink() > 1 { let key = (metadata.dev(), metadata.ino()); - self.hardlink_groups.entry(key).or_default().push(path); + self.hardlink_groups + .entry(key) + .or_default() + .push(entry.path()); } } }