Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/cspell.dictionaries/jargon.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ microbenchmarks
microbenchmarking
multibyte
multicall
newfs
nmerge
noatime
nomount
nocache
nocreat
noctty
Expand Down Expand Up @@ -171,6 +173,7 @@ inacc
maint
proc
procs
ramdisk

# * constants
xffff
Expand Down
23 changes: 16 additions & 7 deletions src/uu/mv/src/mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1082,15 +1082,24 @@ fn copy_dir_contents_recursive(
display_manager,
)?;
} else {
// Copy file with or without hardlink support based on platform
// Check if this is a FIFO to avoid blocking on fs::copy (issue #9656)
#[cfg(unix)]
{
copy_file_with_hardlinks_helper(
&from_path,
&to_path,
hardlink_tracker,
hardlink_scanner,
)?;
let metadata = from_path.symlink_metadata()?;
let file_type = metadata.file_type();

if is_fifo(file_type) {
// Handle FIFO specially to avoid blocking on fs::copy
rename_fifo_fallback(&from_path, &to_path)?;
} else {
// Copy file with hardlink support
copy_file_with_hardlinks_helper(
&from_path,
&to_path,
hardlink_tracker,
hardlink_scanner,
)?;
}
}
#[cfg(not(unix))]
{
Expand Down
45 changes: 45 additions & 0 deletions tests/by-util/test_mv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2537,6 +2537,51 @@ fn test_special_file_different_filesystem() {
std::fs::remove_dir_all("/dev/shm/tmp").unwrap();
}

/// Test moving a directory containing a FIFO file across different filesystems (issue #9656)
/// Without proper FIFO handling, this test will hang indefinitely when
/// copy_dir_contents_recursive tries to fs::copy() the FIFO
#[cfg(unix)]
#[test]
fn test_mv_dir_containing_fifo_cross_filesystem() {
use std::time::Duration;

let mut scene = TestScenario::new(util_name!());

// Test must be run as root (or with `sudo -E`)
if scene.cmd("whoami").run().stdout_str() != "root\n" {
return;
}

{
let at = &scene.fixtures;
at.mkdir("a");
at.mkfifo("a/f");
at.mkdir("mnt");
}

// Prepare the mount
let mountpoint_path = scene.fixtures.plus_as_string("mnt");
scene
.mount_temp_fs(&mountpoint_path)
.expect("mounting tmpfs failed");

// This will hang without the fix, so use timeout
// Move to the mounted tmpfs which is a different filesystem
scene
.ucmd()
.args(&["a", "mnt/dest"])
.timeout(Duration::from_secs(2))
.succeeds();

// Ditch the mount before the asserts
scene.umount_temp_fs();

let at = &scene.fixtures;
assert!(!at.dir_exists("a"));
assert!(at.dir_exists("mnt/dest"));
assert!(at.is_fifo("mnt/dest/f"));
}

/// Test cross-device move with permission denied error
/// This test mimics the scenario from the GNU part-fail test where
/// a cross-device move fails due to permission errors when removing the target file
Expand Down
76 changes: 76 additions & 0 deletions tests/uutests/src/lib/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,8 @@ pub struct TestScenario {
tmpd: Rc<TempDir>,
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
tmp_fs_mountpoint: Option<String>,
#[cfg(target_vendor = "apple")]
tmp_fs_ramdisk: Option<String>,
}

impl TestScenario {
Expand All @@ -1355,6 +1357,8 @@ impl TestScenario {
tmpd,
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
tmp_fs_mountpoint: None,
#[cfg(target_vendor = "apple")]
tmp_fs_ramdisk: None,
};
let mut fixture_path_builder = env::current_dir().unwrap();
fixture_path_builder.push(TESTS_DIR);
Expand Down Expand Up @@ -1422,6 +1426,64 @@ impl TestScenario {
Ok(())
}

/// Mounts a temporary filesystem at the specified mount point (macOS).
#[cfg(target_vendor = "apple")]
pub fn mount_temp_fs(&mut self, mount_point: &str) -> core::result::Result<(), String> {
if self.tmp_fs_ramdisk.is_some() {
return Err("already mounted".to_string());
}

// Create a 10MB ramdisk using hdiutil (10 * 2048 = 20480 512-byte sectors)
let attach_result = self
.cmd("hdiutil")
.args(&["attach", "-nomount", "ram://20480"])
.run();

if !attach_result.succeeded() {
return Err("Failed to create ramdisk".to_string());
}

let ramdisk_device = attach_result.stdout_str().trim().to_string();
if ramdisk_device.is_empty() {
return Err("hdiutil returned empty device name".to_string());
}

// Format the ramdisk with HFS+ filesystem
let format_result = self
.cmd("newfs_hfs")
.arg("-M")
.arg("700")
.arg(&ramdisk_device)
.run();

if !format_result.succeeded() {
// Clean up ramdisk on failure
let _ = self.cmd("hdiutil").args(&["detach", &ramdisk_device]).run();
return Err(format!(
"Failed to format ramdisk: {}",
format_result.stderr_str()
));
}

// Mount the ramdisk at the specified mount point
let mount_result = self
.cmd("mount")
.args(&["-t", "hfs", &ramdisk_device, mount_point])
.run();

if !mount_result.succeeded() {
// Clean up ramdisk on failure
let _ = self.cmd("hdiutil").args(&["detach", &ramdisk_device]).run();
return Err(format!(
"Failed to mount ramdisk: {}",
mount_result.stderr_str()
));
}

self.tmp_fs_ramdisk = Some(ramdisk_device);
Ok(())
}

#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
/// Unmounts the temporary filesystem if it is currently mounted.
pub fn umount_temp_fs(&mut self) {
Expand All @@ -1430,12 +1492,26 @@ impl TestScenario {
self.tmp_fs_mountpoint = None;
}
}

#[cfg(target_vendor = "apple")]
/// Unmounts and detaches the temporary ramdisk (macOS).
pub fn umount_temp_fs(&mut self) {
if let Some(ramdisk_device) = self.tmp_fs_ramdisk.as_ref() {
// hdiutil detach will unmount automatically
self.cmd("hdiutil")
.args(&["detach", ramdisk_device])
.succeeds();
self.tmp_fs_ramdisk = None;
}
}
}

impl Drop for TestScenario {
fn drop(&mut self) {
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
self.umount_temp_fs();
#[cfg(target_vendor = "apple")]
self.umount_temp_fs();
}
}

Expand Down
Loading