diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 7129e82522..4a688e8a32 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO sflag +// spell-checker:ignore RDONLY futimens utimensat use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; @@ -1904,7 +1905,23 @@ pub(crate) fn copy_attributes( handle_preserve(attributes.timestamps, || -> CopyResult<()> { let atime = FileTime::from_last_access_time(&source_metadata); let mtime = FileTime::from_last_modification_time(&source_metadata); - if dest.is_symlink() { + // `set_file_times` opens the destination (O_RDONLY) before calling + // futimens; opening a FIFO or device with no peer blocks forever, and a + // socket cannot be opened at all. For symlinks and these special files + // use the path-based, no-follow variant, which sets the times via + // utimensat without opening. + #[cfg(unix)] + let no_open = { + let ft = source_metadata.file_type(); + dest.is_symlink() + || ft.is_fifo() + || ft.is_socket() + || ft.is_char_device() + || ft.is_block_device() + }; + #[cfg(not(unix))] + let no_open = dest.is_symlink(); + if no_open { filetime::set_symlink_file_times(dest, atime, mtime)?; } else { filetime::set_file_times(dest, atime, mtime)?; diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index d973c39366..079a317d80 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -3370,6 +3370,24 @@ fn test_cp_fifo() { assert_eq!(permission, "prwx-wx--x".to_string()); } +#[test] +#[cfg(unix)] +fn test_cp_fifo_preserve_timestamps() { + // Preserving timestamps must not open the FIFO: opening a FIFO with no + // writer blocks forever. If this regresses, the test hangs instead of + // completing. See the `-a`/`--preserve=timestamps` path in copy_attributes. + let (at, mut ucmd) = at_and_ucmd!(); + at.mkfifo("pipe"); + at.set_mode("pipe", 0o624); + ucmd.arg("--preserve=timestamps") + .arg("-r") + .arg("pipe") + .arg("pipe_dup") + .succeeds() + .no_output(); + assert!(at.is_fifo("pipe_dup")); +} + #[rstest] #[case::recursive("-R")] #[case::archive("-a")]