Skip to content

Commit c6af837

Browse files
committed
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 uutils#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.
1 parent 1a6fb19 commit c6af837

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

src/uu/dd/src/dd.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,12 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> {
12311231
// Aligned read scratch sized to the block size (the max size needed).
12321232
// 4 KiB alignment satisfies block devices that enforce a strict
12331233
// `dma_alignment` for `iflag=direct` reads — see `AlignedBuf`.
1234+
//
1235+
// A `count=` smaller than the block size caps how much will ever be
1236+
// read, so allocate only that much: `ibs=16384 obs=16383 count=1`
1237+
// would otherwise reserve the least common multiple of the two block
1238+
// sizes (256 MiB) to copy 16 KiB.
1239+
let bsize = calc_loop_bsize(i.settings.count, &rstat, i.settings.ibs, bsize);
12341240
let mut buf = AlignedBuf::new(bsize)?;
12351241
// Separate scratch for `conv=block` / `conv=unblock`, which can change
12361242
// the byte count and so cannot be done in-place in `buf`.
@@ -1486,8 +1492,10 @@ fn calc_loop_bsize(count: Option<Num>, rstat: &ReadStat, ibs: usize, ideal_bsize
14861492
match count {
14871493
Some(Num::Blocks(rmax)) => {
14881494
let rsofar = rstat.reads_complete + rstat.reads_partial;
1489-
let rremain = rmax - rsofar;
1490-
cmp::min(ideal_bsize as u64, rremain * ibs as u64) as usize
1495+
// `count=` is a user-supplied u64, so `count * ibs` can exceed
1496+
// u64: saturate rather than wrap to a bogus (tiny) buffer size.
1497+
let rremain = rmax.saturating_sub(rsofar);
1498+
cmp::min(ideal_bsize as u64, rremain.saturating_mul(ibs as u64)) as usize
14911499
}
14921500
Some(Num::Bytes(bmax)) => {
14931501
// `iflag=count_bytes` limits input, so use bytes read.

tests/by-util/test_dd.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,27 @@ fn test_out_of_memory_skip() {
133133
.stderr_contains("memory");
134134
}
135135

136+
// `count=` caps how much can ever be read, so the copy buffer must be sized
137+
// to it rather than to the least common multiple of `ibs` and `obs` (256 MiB
138+
// here, for a 16 KiB copy).
139+
#[cfg(all(target_os = "linux", target_pointer_width = "64"))]
140+
#[cfg_attr(
141+
wasi_runner,
142+
ignore = "address-space limits are not supported by the WASI runner"
143+
)]
144+
#[test]
145+
fn test_count_limits_internal_buffer_allocation() {
146+
use rlimit::Resource;
147+
148+
const AS_LIMIT: u64 = 200 * 1024 * 1024;
149+
150+
new_ucmd!()
151+
.limit(Resource::AS, AS_LIMIT, AS_LIMIT)
152+
.args(&["if=/dev/zero", "of=/dev/null"])
153+
.args(&["ibs=16384", "obs=16383", "count=1", "status=none"])
154+
.succeeds();
155+
}
156+
136157
#[test]
137158
fn test_huge_block_size_is_rejected_without_panicking() {
138159
// Regression test for #12844: a block size >= i64::MAX used to panic

0 commit comments

Comments
 (0)