From 10d4e380993a8d228911a107ab618a8f19b2cb71 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 7 Aug 2026 00:14:00 +0200 Subject: [PATCH] dd: report transfer statistics when the copy fails Should make test dd/partial-write.sh pass --- src/uu/dd/src/dd.rs | 22 ++++++++++++++++++++-- tests/by-util/test_dd.rs | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index dd8bf8a26d0..1ba8a1b6189 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -1191,6 +1191,8 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // blocks to this output. Read/write statistics are updated on // each iteration and cumulative statistics are reported to // the progress reporting thread. + // A failure ends the loop, so the statistics gathered so far still get reported. + let mut copy_error = None; while below_count_limit(i.settings.count, &rstat) { // Read a block from the input then write the block to the output. // @@ -1198,7 +1200,11 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // best buffer size for reading based on the number of // blocks already read and the number of blocks remaining. let loop_bsize = calc_loop_bsize(i.settings.count, &rstat, i.settings.ibs, bsize); - let rstat_update = read_helper(&mut i, &mut buf, loop_bsize)?; + let Ok(rstat_update) = + read_helper(&mut i, &mut buf, loop_bsize).map_err(|e| copy_error = Some(e)) + else { + break; + }; if rstat_update.is_empty() { if input_nocache { i.discard_cache(read_offset, 0); @@ -1208,7 +1214,9 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { } break; } - let wstat_update = o.write_blocks(&buf)?; + let Ok(wstat_update) = o.write_blocks(&buf).map_err(|e| copy_error = Some(e)) else { + break; + }; // Discard the system file cache for the read portion of // the input file. @@ -1255,6 +1263,16 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { prog_tx.send(prog_update).unwrap_or(()); } + if let Some(e) = copy_error { + // Flushing and syncing are pointless now, but the caller still wants the statistics. + let prog_update = ProgUpdate::new(rstat, wstat, start.elapsed(), ProgUpdateType::Final); + prog_tx.send(prog_update).unwrap_or(()); + output_thread + .join() + .expect("Failed to join with the output thread."); + return Err(e); + } + finalize(o, rstat, wstat, start, &prog_tx, output_thread, truncate) } diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 9c6d1d7b230..d409baa269e 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg fifoname FADV DONTNEED +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, availible, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat abcdefghijklm abcdefghi nabcde nabcdefg abcdefg fifoname FADV DONTNEED FSIZE SIGXFSZ sighandler use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -2209,3 +2209,40 @@ fn test_count_bytes_with_expanding_block_conv() { assert_eq!(bytecount::count(&output, b'a'), 1000); assert!(!output.contains(&b'Z')); } + +// A failed copy still has to report what it transferred, including complete +// and partial records. +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +fn test_stats_are_reported_when_a_write_fails() { + use rlimit::Resource; + + // Restores the previous SIGXFSZ disposition even if an assertion panics. + struct SigxfszGuard(libc::sighandler_t); + impl Drop for SigxfszGuard { + fn drop(&mut self) { + // SAFETY: restoring the disposition saved below. + unsafe { libc::signal(libc::SIGXFSZ, self.0) }; + } + } + + const CAP: u64 = 768 * 1024; + + // The child inherits the ignored SIGXFSZ, so exceeding RLIMIT_FSIZE shows + // up as a short write() instead of killing the process. + // SAFETY: signal() with SIG_IGN is async-signal-safe and the guard puts + // the old handler back. + let _sigxfsz = SigxfszGuard(unsafe { libc::signal(libc::SIGXFSZ, libc::SIG_IGN) }); + + let (at, mut ucmd) = at_and_ucmd!(); + let result = ucmd + .args(&["if=/dev/zero", "of=capped.bin", "bs=512K", "count=3"]) + .limit(Resource::FSIZE, CAP, CAP) + .fails(); + + // Under a 768 KiB cap, the first 512 KiB block is written in full, the + // second one is cut short at 256 KiB, and the third write fails. + result.stderr_contains("1+1 records out"); + result.stderr_contains("786432 bytes"); + assert_eq!(at.metadata("capped.bin").len(), CAP); +}