Skip to content

Commit cf75720

Browse files
committed
mv,cp: fix xattr and symlink handling in cross-device operations
1 parent 41cd49f commit cf75720

3 files changed

Lines changed: 19 additions & 9 deletions

File tree

src/uu/cp/src/cp.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,8 +1713,6 @@ pub(crate) fn set_selinux_context(path: &Path, context: Option<&String>) -> Copy
17131713
/// user-writable if needed and restoring its original permissions afterward. This avoids "Operation
17141714
/// not permitted" errors on read-only files. Returns an error if permission or metadata operations fail,
17151715
/// or if xattr copying fails.
1716-
///
1717-
/// Uses file descriptor-based operations to avoid TOCTOU races during xattr copying.
17181716
#[cfg(all(unix, not(target_os = "android")))]
17191717
fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyResult<()> {
17201718
use std::fs::File;

src/uu/mv/src/mv.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,8 @@ fn rename_dir_fallback(
967967
(_, _) => None,
968968
};
969969

970-
// Retrieve xattrs using file descriptor to avoid TOCTOU races
970+
// Retrieve xattrs before copying (directories use path-based operations
971+
// since they cannot be opened in write mode for xattr operations)
971972
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
972973
let xattrs = {
973974
use std::fs::File;
@@ -989,12 +990,12 @@ fn rename_dir_fallback(
989990
display_manager,
990991
);
991992

992-
// Apply xattrs using file descriptor to avoid TOCTOU races
993+
// Apply xattrs after directory contents are copied, ignoring ENOTSUP errors
994+
// (filesystem doesn't support xattrs, which is acceptable for cross-device moves)
993995
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
994-
{
995-
use std::fs::OpenOptions;
996-
if let Ok(f) = OpenOptions::new().write(true).open(to) {
997-
fsxattr::apply_xattrs_fd(&f, xattrs)?;
996+
if let Err(e) = fsxattr::apply_xattrs(to, xattrs) {
997+
if e.raw_os_error() != Some(libc::EOPNOTSUPP) {
998+
return Err(e);
998999
}
9991000
}
10001001

@@ -1094,7 +1095,16 @@ fn copy_dir_contents_recursive(
10941095
rename_symlink_fallback(&from_path, &to_path)?;
10951096
}
10961097

1097-
print_verbose(&from_path, &to_path);
1098+
// Print verbose message for symlink
1099+
if verbose {
1100+
let message = translate!("mv-verbose-renamed", "from" => from_path.quote(), "to" => to_path.quote());
1101+
match display_manager {
1102+
Some(pb) => pb.suspend(|| {
1103+
println!("{message}");
1104+
}),
1105+
None => println!("{message}"),
1106+
}
1107+
}
10981108
} else if from_path.is_dir() {
10991109
// Recursively copy subdirectory (only real directories, not symlinks)
11001110
fs::create_dir_all(&to_path)?;

tests/by-util/test_mv.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2882,6 +2882,7 @@ fn test_mv_cross_device_symlink_preserved() {
28822882
at.write("src_dir/local.txt", "local content");
28832883
symlink("/etc", at.plus("src_dir/etc_link")).expect("Failed to create symlink");
28842884

2885+
// Verify initial state
28852886
assert!(at.is_symlink("src_dir/etc_link"));
28862887

28872888
// Force cross-filesystem move using /dev/shm (tmpfs)
@@ -2896,6 +2897,7 @@ fn test_mv_cross_device_symlink_preserved() {
28962897
.succeeds()
28972898
.no_stderr();
28982899

2900+
// Verify source was removed
28992901
assert!(!at.dir_exists("src_dir"));
29002902

29012903
// Verify the symlink was preserved (not expanded)

0 commit comments

Comments
 (0)