From c6af8378c19035086c15a9c756fc0c003e1d2d37 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Wed, 26 Aug 2026 17:59:12 +0200 Subject: [PATCH] dd: limit the copy buffer to the copy count `bsize` is the least common multiple of `ibs` and `obs`, which for relatively prime block sizes is their product: `ibs=16384 obs=16383` reserves 256 MiB of address space. With `count=1` at most one `ibs` record will ever be read, so that reservation is pure waste and fails outright under a modest `RLIMIT_AS`. Size the initial allocation with `calc_loop_bsize`, the same function the main loop already uses to shrink each read as the count runs out. `calc_loop_bsize` also had to stop overflowing: `count` is a user-supplied `u64`, so `rremain * ibs` panicked in debug and wrapped to a bogus (tiny) buffer size in release for a large `count=`. Saturate both the subtraction and the multiplication; the `cmp::min` against `ideal_bsize` then keeps the result correct. Taken over from #13373 by relative23; rebased onto the `AlignedBuf` read scratch, with the count overflow fixed inside `calc_loop_bsize` rather than guarded at the one call site. --- src/uu/dd/src/dd.rs | 12 ++++++++++-- tests/by-util/test_dd.rs | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index ac26efc79c2..8ea6a82fc32 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1231,6 +1231,12 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // Aligned read scratch sized to the block size (the max size needed). // 4 KiB alignment satisfies block devices that enforce a strict // `dma_alignment` for `iflag=direct` reads — see `AlignedBuf`. + // + // A `count=` smaller than the block size caps how much will ever be + // read, so allocate only that much: `ibs=16384 obs=16383 count=1` + // would otherwise reserve the least common multiple of the two block + // sizes (256 MiB) to copy 16 KiB. + let bsize = calc_loop_bsize(i.settings.count, &rstat, i.settings.ibs, bsize); let mut buf = AlignedBuf::new(bsize)?; // Separate scratch for `conv=block` / `conv=unblock`, which can change // the byte count and so cannot be done in-place in `buf`. @@ -1486,8 +1492,10 @@ fn calc_loop_bsize(count: Option, rstat: &ReadStat, ibs: usize, ideal_bsize match count { Some(Num::Blocks(rmax)) => { let rsofar = rstat.reads_complete + rstat.reads_partial; - let rremain = rmax - rsofar; - cmp::min(ideal_bsize as u64, rremain * ibs as u64) as usize + // `count=` is a user-supplied u64, so `count * ibs` can exceed + // u64: saturate rather than wrap to a bogus (tiny) buffer size. + let rremain = rmax.saturating_sub(rsofar); + cmp::min(ideal_bsize as u64, rremain.saturating_mul(ibs as u64)) as usize } Some(Num::Bytes(bmax)) => { // `iflag=count_bytes` limits input, so use bytes read. diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 07df2388028..8d09685419a 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -133,6 +133,27 @@ fn test_out_of_memory_skip() { .stderr_contains("memory"); } +// `count=` caps how much can ever be read, so the copy buffer must be sized +// to it rather than to the least common multiple of `ibs` and `obs` (256 MiB +// here, for a 16 KiB copy). +#[cfg(all(target_os = "linux", target_pointer_width = "64"))] +#[cfg_attr( + wasi_runner, + ignore = "address-space limits are not supported by the WASI runner" +)] +#[test] +fn test_count_limits_internal_buffer_allocation() { + use rlimit::Resource; + + const AS_LIMIT: u64 = 200 * 1024 * 1024; + + new_ucmd!() + .limit(Resource::AS, AS_LIMIT, AS_LIMIT) + .args(&["if=/dev/zero", "of=/dev/null"]) + .args(&["ibs=16384", "obs=16383", "count=1", "status=none"]) + .succeeds(); +} + #[test] fn test_huge_block_size_is_rejected_without_panicking() { // Regression test for #12844: a block size >= i64::MAX used to panic