VYPR
Medium severity6.6NVD Advisory· Published Apr 22, 2026· Updated May 4, 2026

CVE-2026-35365

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.

PackageAffected versionsPatched versions
coreutilscrates.io
< 0.7.00.7.0

Affected products

2
  • Uutils/Coreutilsreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • cpe:2.3:a:uutils:coreutils:*:*:*:*:*:rust:*:*range: <0.7.0

Patches

1
9654e4abaf24

mv: preserve symlinks during cross-device moves instead of expanding them

https://github.com/uutils/coreutilsSylvestre LedruJan 28, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.