From e7595770657d8506a4fc12b0cc2e8910e01cdeee Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 5 Jun 2026 17:20:08 +0200 Subject: [PATCH] cp: harden sparse_copy against 32-bit panic and EOF hang Use u64 for the file size and offset instead of narrowing to usize with try_into().unwrap(), which panicked on 32-bit when the source exceeded usize::MAX. Also break the copy loop on a zero-byte read so a source that shrinks mid-copy no longer spins forever. --- src/uu/cp/src/platform/linux.rs | 19 ++++++++++++------- tests/by-util/test_cp.rs | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/uu/cp/src/platform/linux.rs b/src/uu/cp/src/platform/linux.rs index 328c3f4a112..7b07a3b857b 100644 --- a/src/uu/cp/src/platform/linux.rs +++ b/src/uu/cp/src/platform/linux.rs @@ -167,23 +167,28 @@ where let mut src_file = open_source(source, nofollow)?; let dst_file = create_dest_restrictive(dest, false)?; - let size: usize = src_file.metadata()?.size().try_into().unwrap(); - ftruncate(&dst_file, size.try_into().unwrap())?; + let size = src_file.metadata()?.size(); + ftruncate(&dst_file, size)?; - let blksize = dst_file.metadata()?.blksize(); - let mut buf: Vec = vec![0; blksize.try_into().unwrap()]; - let mut current_offset: usize = 0; + let blksize = dst_file.metadata()?.blksize() as usize; + let mut buf: Vec = vec![0; blksize]; + let mut current_offset: u64 = 0; // TODO Perhaps we can employ the "fiemap ioctl" API to get the // file extent mappings: // https://www.kernel.org/doc/html/latest/filesystems/fiemap.html while current_offset < size { let this_read = src_file.read(&mut buf)?; + if this_read == 0 { + // The source shrank (e.g. truncated concurrently) before we reached + // its reported size; stop instead of looping forever on EOF reads. + break; + } let buf = &buf[..this_read]; if buf.iter().any(|&x| x != 0) { - dst_file.write_all_at(buf, current_offset.try_into().unwrap())?; + dst_file.write_all_at(buf, current_offset)?; } - current_offset += this_read; + current_offset += this_read as u64; } Ok(()) } diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 1b3fde67814..54fe1dbbd19 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -3942,6 +3942,26 @@ fn test_reflink_never_sparse_always() { assert_eq!(dest_metadata.len(), 1024 * 1024); } +#[cfg(any(target_os = "linux", target_os = "android"))] +#[test] +fn test_reflink_never_sparse_always_data_is_copied() { + // `--reflink=never --sparse=always` goes through the read-loop sparse copy. + // A file with data spread across several blocks must be copied byte-for-byte. + let (at, mut ucmd) = at_and_ucmd!(); + + let mut buf = vec![0u8; 4096 * 4 + 7]; + for i in [0, buf.len() / 2, buf.len() - 1] { + buf[i] = b'x'; + } + at.write_bytes("src", &buf); + + ucmd.args(&["--reflink=never", "--sparse=always", "src", "dest"]) + .succeeds() + .no_output(); + + assert_eq!(at.read_bytes("dest"), buf); +} + /// Test for preserving attributes of a hard link in a directory. #[test] #[cfg(not(any(target_os = "android", target_os = "openbsd")))]