CVE-2026-35365
Description
The mv utility in uutils coreutils improperly handles directory trees containing symbolic links during moves across filesystem boundaries. Instead of preserving symlinks, the implementation expands them, copying the linked targets as real files or directories at the destination. This can lead to resource exhaustion (disk space or time) if symlinks point to large external directories, unexpected duplication of sensitive data into unintended locations, or infinite recursion and repeated copying in the presence of symlink loops.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
coreutilscrates.io | < 0.7.0 | 0.7.0 |
Affected products
2Patches
19654e4abaf24mv: preserve symlinks during cross-device moves instead of expanding them
2 files changed · +180 −29
src/uu/mv/src/mv.rs+38 −29 modified@@ -1039,6 +1039,20 @@ fn copy_dir_contents_recursive( progress_bar: Option<&ProgressBar>, display_manager: Option<&MultiProgress>, ) -> io::Result<()> { + // Helper closure to print verbose messages + let print_verbose = |from: &Path, to: &Path| { + if verbose { + let message = + translate!("mv-verbose-renamed", "from" => from.quote(), "to" => to.quote()); + match display_manager { + Some(pb) => pb.suspend(|| { + println!("{message}"); + }), + None => println!("{message}"), + } + } + }; + let entries = fs::read_dir(from_dir)?; for entry in entries { @@ -1051,20 +1065,29 @@ fn copy_dir_contents_recursive( pb.set_message(from_path.to_string_lossy().to_string()); } - if from_path.is_dir() { - // Recursively copy subdirectory + if from_path.is_symlink() { + // Handle symlinks first, before checking is_dir() which follows symlinks. + // This prevents symlinks to directories from being expanded into full copies. + #[cfg(unix)] + { + copy_file_with_hardlinks_helper( + &from_path, + &to_path, + hardlink_tracker, + hardlink_scanner, + )?; + } + #[cfg(not(unix))] + { + rename_symlink_fallback(&from_path, &to_path)?; + } + + print_verbose(&from_path, &to_path); + } else if from_path.is_dir() { + // Recursively copy subdirectory (only real directories, not symlinks) fs::create_dir_all(&to_path)?; - // Print verbose message for directory - if verbose { - let message = translate!("mv-verbose-renamed", "from" => from_path.quote(), "to" => to_path.quote()); - match display_manager { - Some(pb) => pb.suspend(|| { - println!("{message}"); - }), - None => println!("{message}"), - } - } + print_verbose(&from_path, &to_path); copy_dir_contents_recursive( &from_path, @@ -1090,25 +1113,11 @@ fn copy_dir_contents_recursive( } #[cfg(not(unix))] { - if from_path.is_symlink() { - // Copy a symlink file (no-follow). - rename_symlink_fallback(&from_path, &to_path)?; - } else { - // Copy a regular file. - fs::copy(&from_path, &to_path)?; - } + // Symlinks are already handled above, so this is always a regular file + fs::copy(&from_path, &to_path)?; } - // Print verbose message for file - if verbose { - let message = translate!("mv-verbose-renamed", "from" => from_path.quote(), "to" => to_path.quote()); - match display_manager { - Some(pb) => pb.suspend(|| { - println!("{message}"); - }), - None => println!("{message}"), - } - } + print_verbose(&from_path, &to_path); } if let Some(pb) = progress_bar {
tests/by-util/test_mv.rs+142 −0 modified@@ -2864,3 +2864,145 @@ fn test_mv_xattr_enotsup_silent() { std::fs::remove_file("/dev/shm/mv_test").ok(); } } + +/// Test that symlinks inside directories are preserved during cross-device moves +/// (not expanded into full copies of their targets) +#[test] +#[cfg(target_os = "linux")] +fn test_mv_cross_device_symlink_preserved() { + use std::fs; + use std::os::unix::fs::symlink; + use tempfile::TempDir; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a directory with a symlink to /etc inside + at.mkdir("src_dir"); + at.write("src_dir/local.txt", "local content"); + symlink("/etc", at.plus("src_dir/etc_link")).expect("Failed to create symlink"); + + assert!(at.is_symlink("src_dir/etc_link")); + + // Force cross-filesystem move using /dev/shm (tmpfs) + let target_dir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory in /dev/shm"); + let target_path = target_dir.path().join("dst_dir"); + + scene + .ucmd() + .arg("src_dir") + .arg(target_path.to_str().unwrap()) + .succeeds() + .no_stderr(); + + assert!(!at.dir_exists("src_dir")); + + // Verify the symlink was preserved (not expanded) + let moved_symlink = target_path.join("etc_link"); + assert!( + moved_symlink.is_symlink(), + "etc_link should still be a symlink after cross-device move" + ); + assert_eq!( + fs::read_link(&moved_symlink).expect("Failed to read symlink"), + Path::new("/etc"), + "symlink should still point to /etc" + ); + + assert!(target_path.join("local.txt").exists()); +} + +/// Test that broken/dangling symlinks are preserved during cross-device moves +#[test] +#[cfg(target_os = "linux")] +fn test_mv_cross_device_broken_symlink_preserved() { + use std::fs; + use std::os::unix::fs::symlink; + use tempfile::TempDir; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a directory with a broken symlink inside + at.mkdir("src_dir"); + symlink("/nonexistent/path", at.plus("src_dir/broken_link")) + .expect("Failed to create broken symlink"); + + assert!(at.is_symlink("src_dir/broken_link")); + assert!(!at.file_exists("src_dir/broken_link")); + + // Force cross-filesystem move using /dev/shm (tmpfs) + let target_dir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory in /dev/shm"); + let target_path = target_dir.path().join("dst_dir"); + + scene + .ucmd() + .arg("src_dir") + .arg(target_path.to_str().unwrap()) + .succeeds() + .no_stderr(); + + assert!(!at.dir_exists("src_dir")); + + let moved_symlink = target_path.join("broken_link"); + assert!( + moved_symlink.is_symlink(), + "broken_link should still be a symlink after cross-device move" + ); + assert_eq!( + fs::read_link(&moved_symlink).expect("Failed to read broken symlink"), + Path::new("/nonexistent/path"), + "broken symlink should still point to its original (nonexistent) target" + ); +} + +/// Test that symlinks to regular files are preserved during cross-device moves +#[test] +#[cfg(target_os = "linux")] +fn test_mv_cross_device_file_symlink_preserved() { + use std::fs; + use std::os::unix::fs::symlink; + use tempfile::TempDir; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a directory with a file and a symlink to it + at.mkdir("src_dir"); + at.write("src_dir/target.txt", "target content"); + symlink(at.plus("src_dir/target.txt"), at.plus("src_dir/file_link")) + .expect("Failed to create file symlink"); + + assert!(at.is_symlink("src_dir/file_link")); + + // Force cross-filesystem move using /dev/shm (tmpfs) + let target_dir = + TempDir::new_in("/dev/shm/").expect("Unable to create temp directory in /dev/shm"); + let target_path = target_dir.path().join("dst_dir"); + + scene + .ucmd() + .arg("src_dir") + .arg(target_path.to_str().unwrap()) + .succeeds() + .no_stderr(); + + assert!(!at.dir_exists("src_dir")); + + // Verify the symlink was preserved (not expanded) + let moved_symlink = target_path.join("file_link"); + assert!( + moved_symlink.is_symlink(), + "file_link should still be a symlink after cross-device move" + ); + + // Verify the target file was also moved + let moved_target = target_path.join("target.txt"); + assert!(moved_target.exists()); + assert_eq!( + fs::read_to_string(&moved_target).expect("Failed to read target file"), + "target content" + ); +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/uutils/coreutils/pull/10546nvdIssue TrackingPatchWEB
- github.com/advisories/GHSA-66fx-fqv6-5wwxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-35365ghsaADVISORY
- github.com/uutils/coreutils/commit/9654e4abaf24449ef2279e9a16963edb5c8b8fefghsaWEB
- github.com/uutils/coreutils/releases/tag/0.7.0nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.