From c5189f19c7208ae496c0eb11cfc286d6e3eafaa6 Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:53:59 -0300 Subject: [PATCH] dd: stop faulting in the whole copy buffer before reading The copy buffer was reserved and then filled with BUF_INIT_BYTE, so every page of bs= was written before the first read. A bs= far larger than the data being copied therefore cost its full size in resident memory and in the time to write it, even when the input was empty. Read::read does need an initialised slice, but zeroed pages are free: vec![0; n] allocates through alloc_zeroed, so the pages come from the kernel already zero and are never touched until something is read into them. The reservation is kept ahead of it so that an unobtainable bs= still reports an error instead of aborting. --- src/uu/dd/src/dd.rs | 58 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 723add1f14b..c52e670ab49 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1181,10 +1181,7 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { BlockWriter::Unbuffered(o) }; - // Create a common empty buffer with a capacity of the block size. - // This is the max size needed. - let mut buf = Vec::new(); - buf.try_reserve(bsize)?; // try_with_capacity is unstable https://github.com/rust-lang/rust/issues/91913 + let mut buf = alloc_copy_buffer(bsize)?; // The main read/write loop. // @@ -1348,6 +1345,26 @@ fn make_linux_oflags(oflags: &OFlags) -> Option { if flag == 0 { None } else { Some(flag) } } +/// The copy buffer, `bsize` bytes long and ready to be read into. +/// +/// `Read::read` fills an initialised slice, and zeroed pages are the only +/// initialisation that costs nothing: `vec![0; n]` allocates through +/// `alloc_zeroed`, so the pages arrive from the kernel already zero and stay +/// untouched until something is read into them. Writing a fill byte over +/// reserved capacity instead faults in the whole of `bs=` before the first +/// read, which is what made a large `bs=` cost its full size in time and in +/// resident memory even with nothing to copy. +/// +/// The reservation still happens first, because `vec![0; n]` aborts when the +/// allocation fails and `dd` reports that as an error instead. +fn alloc_copy_buffer(bsize: usize) -> io::Result> { + let mut probe: Vec = Vec::new(); + // try_with_capacity is unstable https://github.com/rust-lang/rust/issues/91913 + probe.try_reserve(bsize)?; + drop(probe); + Ok(vec![0u8; bsize]) +} + /// Read from an input (that is, a source of bytes) into the given buffer. /// /// This function also performs any conversions as specified by @@ -1543,6 +1560,39 @@ mod tests { use std::path::Path; + /// `dd` has to accept a `bs=` far larger than the data it will copy, the + /// way GNU dd does, so the copy buffer must not fault in its pages. + #[cfg(all(target_os = "linux", target_pointer_width = "64"))] + #[test] + fn alloc_copy_buffer_does_not_touch_its_pages() { + use crate::alloc_copy_buffer; + + fn peak_rss_kib() -> u64 { + std::fs::read_to_string("/proc/self/status") + .unwrap() + .lines() + .find_map(|line| line.strip_prefix("VmHWM:")) + .and_then(|v| v.trim().trim_end_matches("kB").trim().parse().ok()) + .unwrap() + } + + // Larger than anything else this test binary allocates, so the reading + // below cannot be attributed to another test. + const BSIZE: usize = 4 << 30; + const SLACK_KIB: u64 = 64 << 10; + + let before = peak_rss_kib(); + let buf = alloc_copy_buffer(BSIZE).unwrap(); + let after = peak_rss_kib(); + + assert_eq!(buf.len(), BSIZE); + assert!( + after - before < SLACK_KIB, + "a {BSIZE}-byte copy buffer raised peak RSS by {} KiB", + after - before + ); + } + #[test] fn bsize_test_primes() { let (n, m) = (7901, 7919);