Skip to content

dd: retry short writes so the error behind them is reported - #16196

Open
thelastbackspace wants to merge 3 commits into
uutils:mainfrom
thelastbackspace:fix/15747-dd-short-write
Open

thelastbackspace wants to merge 3 commits into
uutils:mainfrom
thelastbackspace:fix/15747-dd-short-write

Conversation

@thelastbackspace

Copy link
Copy Markdown

Output::write_block only retried a short write when iflag=fullblock was set. Without it, the rest of the block was dropped. If that short write was the last one of the copy (full disk or RLIMIT_FSIZE hitting the last block), dd exited 0 and left a truncated file. GNU dd keeps writing the rest of the block, gets the ENOSPC/EFBIG, and fails.

What changed:

  • write_block now loops until the whole block is written. EINTR is retried and any other error is returned. A write that returns 0 becomes a WriteZero error, so it can't spin forever.
  • With the retry, the write that fails is often the one that already wrote part of a block. To keep the stats right (1+1 records out, the byte count), Output remembers what a failed write_blocks call got through, and dd_copy adds it to the final stats. This also covers blocks written earlier in the same call, which used to be lost from the stats when a later one failed.
  • Added a small Output::new so the struct's new field doesn't have to be spelled out in every constructor and test.

Testing:

  • New test_short_last_write_fails in tests/by-util/test_dd.rs runs bs=512K count=2, ibs=512K obs=512K count=2 and ibs=512K obs=2M count=2 under a 768 KiB RLIMIT_FSIZE. It fails on main (all three exit 0) and passes with this change.
  • cargo test --no-default-features --features dd -- test_dd (160 passed), cargo test -p uu_dd, cargo fmt --check, cargo clippy -p uu_dd --all-targets.

Fixes #15747

@agammann agammann left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new o.take_failed_wstat() call runs before c.finish(), but finish() writes the padded tail of a conv=block record through write_to(). If a capped destination fails partway through that final write, Output records the partial bytes only after they have already been collected here, so the final report still undercounts them. Could the failed-write stats be collected after finish() as well, with an EOF-padding case where RLIMIT_FSIZE falls inside the padded block?

@thelastbackspace

Copy link
Copy Markdown
Author

Good catch, you're right. I collected the failed-write stats before finish(), so if the padded tail of the last record hit the limit, the partial bytes were never added. I added a test where the input has no trailing newline and RLIMIT_FSIZE falls inside the padding: it reported 3+0 records out / 196608 bytes instead of 3+1 / 204800. Moved the take_failed_wstat() call after finish() and it passes now.

@codspeed

codspeed Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 9 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 18 untouched benchmarks
⏩ 454 skipped benchmarks1


Comparing thelastbackspace:fix/15747-dd-short-write (63cd6da) with main (8bcbdd6)2

Open in CodSpeed

Footnotes

  1. 454 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (ed6eb51) during the generation of this report, so 8bcbdd6 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

Comment thread tests/by-util/test_dd.rs Outdated
.limit(Resource::FSIZE, CAP, CAP)
.ignore_sigxfsz()
.fails();
if args[0] == "bs=512K" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why only check the stats for bs=?
with obs=2M the error comes from the flush in finalize, which returns before printing any stats, and failed_wstat is never added there.
please handle that path and check the records/bytes for all three cases

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the flush path in finalize returned before sending the stats. In 5099fe3 it now adds what the failed flush wrote and prints the stats before returning the error. I also count the block that was read when writing it fails, which GNU does (we said 1+0 records in, GNU says 2+0).

The test now checks records in/out and bytes for all three cases. Compared with GNU dd 9.4 under the same 768K limit:

  • ibs=512K obs=512K: 2+0 in, 1+1 out, 786432 bytes, same as GNU
  • ibs=512K obs=2M: 2+0 in, 0+1 out, 786432 bytes, same as GNU (we printed nothing before)
  • bs=512K: GNU prints 1+0 records out with 786432 bytes, so it counts the bytes of the short write but not the record. I kept 1+1 so it matches the obs= case, but I can change it to follow GNU exactly if you prefer.

A short write was only retried with iflag=fullblock. Otherwise the rest
of the block was dropped, so a copy cut short by a full disk or a file
size limit on its last block exited 0 with a truncated output.

Always write the whole block, like GNU dd, and keep what a failed write
got through in the final statistics.

Fixes uutils#15747
The partial write stats were collected before finish() wrote the padded
tail of the last record, so a failure there was undercounted.
With a large obs=, the only write can be the flush in finalize(), which
returned before printing any statistics and never added what the failed
write got through. Print the stats on that path too, and count the block
that was read when writing it fails, as GNU dd does (2+0 records in).

Check records in/out and bytes for all three cases of the short-write test.
@sylvestre
sylvestre force-pushed the fix/15747-dd-short-write branch from 5099fe3 to 63cd6da Compare November 7, 2026 16:23

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dd: drops rest of block on short write (e.g. full disk) and exits 0

3 participants