bug#80583: "dd" fails to account for short final write
Pádraig Brady <[email protected]> Wed, 11 Mar 2026 15:49:54 +0000
| Newsgroups | gmane.comp.gnu.core-utils.bugs |
|---|---|
| Message-ID | <[email protected]> |
On 09/03/2026 21:53, Laszlo Ersek wrote:
> Hello,
>
> with "dd" built from coreutils @ c5fb1c26de05 ("build: update gnulib
> submodule to latest", 2026-03-09), with the gnulib submodule advanced to
> d5f683434d1a ("doc: Fix documentation that was added today.",
> 2026-03-09):
>
> (
> set -e
> ulimit -S -f 1024
> trap '' XFSZ
> rm -f f
> src/dd if=/dev/urandom of=f bs=768K
> )
>
> The above command produces the regular file "f" with 1024*1024 bytes in
> it (as expected); however, "dd" prints the following to stderr:
>
> dd: error writing 'f': File too large
> 2+0 records in
> 1+0 records out
> 1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.00625178 s, 168 MB/s
>
> The third line ("1+0 records out") is incorrect. It should be "1+1
> records out", because the second (final) write outputs 256*1024 bytes.
> According to POSIX
> <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/dd.html#tag_20_31_11>,
> that counts as a partial output block:
>
> On completion, /dd/ shall write the number of input and output blocks
> to standard error. In the POSIX locale the following formats shall be
> used:
>
> [...]
>
> "%u+%u records out\n", </number of whole output blocks/>, </number of
> partial output blocks/>
>
> [...] A partial output block is one that was written with fewer bytes
> than specified by the output block size. [...]
>
> In dd_copy(), we have
>
> 2321 if (ibuf == obuf) /* If not C_TWOBUFS. */
> 2322 {
> 2323 size_t nwritten = iwrite (STDOUT_FILENO, obuf, n_bytes_read);
> 2324 w_bytes += nwritten;
> 2325 if (nwritten != n_bytes_read)
> 2326 {
> 2327 error (0, errno, _("error writing %s"), quoteaf (output_file));
> 2328 return EXIT_FAILURE;
> 2329 }
> 2330 else if (n_bytes_read == input_blocksize)
> 2331 w_full++;
> 2332 else
> 2333 w_partial++;
> 2334 continue;
> 2335 }
>
> The first execution of this code outputs a full block ("nwritten" ==
> 768*1024 bytes). The second execution outputs "nwritten" == 256*1024
> bytes, with "n_bytes_read" == 768*1024 bytes; yet "w_partial" is not
> incremented.
>
> The condition for reaching "w_partial++" is
>
> nwritten == n_bytes_read &&
> n_bytes_read != input_blocksize
>
> which seems correct to me (it means we managed to output everything we
> just read, but we couldn't read a full block -- and therefore we also
> couldn't write a (-n identically sized) full block). However, a partial
> write can also occur when the read was complete.
>
> The write_output() function (which is not used in this reproducer)
> counts partial output records differently:
>
> 1275 static void
> 1276 write_output (void)
> 1277 {
> 1278 size_t nwritten = iwrite (STDOUT_FILENO, obuf, output_blocksize);
> 1279 w_bytes += nwritten;
> 1280 if (nwritten != output_blocksize)
> 1281 {
> 1282 error (0, errno, _("writing to %s"), quoteaf (output_file));
> 1283 if (nwritten != 0)
> 1284 w_partial++;
> 1285 quit (EXIT_FAILURE);
> 1286 }
> 1287 else
> 1288 w_full++;
> 1289 oc = 0;
> 1290 }
>
> If we detect a short -- but not entirely fruitless -- write in this
> function, then we bump "w_partial" between error() and quit().
>
>
> The symptom is also reproducible by populating a block device with "dd"
> such that the last successful write has no room for a full (output)
> block. (In that case, ENOSPC is reported, rather than EFBIG.) That's in
> fact how I first encountered the problem; the "ignored SIGXFSZ + EFBIG
> errno" method is just a more convenient reproducer.
Thanks for the excellent report and reproducer.
This looks to be a bug since the original implementation.
Attached is a fix and test.
Marking this as done.
thanks!
Padraig
dd-partial-fail.patch
(text/x-patch, 3.3 KB)
From 1c2518816333d408eae11460ff577e78dc8ebf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email protected]> Date: Wed, 11 Mar 2026 15:39:20 +0000 Subject: [PATCH] dd: always diagnose partial writes on write failure * src/dd.c (dd_copy): Increment the partial write count upon failure. * tests/dd/partial-write.sh: Add a new test. * tests/local.mk: Reference the new test. * NEWS: Mention the bug fix. Fixes https://bugs.gnu.org/80583 --- NEWS | 4 ++++ src/dd.c | 2 ++ tests/dd/partial-write.sh | 33 +++++++++++++++++++++++++++++++++ tests/local.mk | 1 + 4 files changed, 40 insertions(+) create mode 100755 tests/dd/partial-write.sh diff --git a/NEWS b/NEWS index 97ed7e721..17532a0e5 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,10 @@ GNU coreutils NEWS -*- outline -*- ** Bug fixes + 'dd' now always diagnoses partial writes correctly upon write failure. + Previously it may have indicated that only full writes were performed. + [This bug was present in "the beginning".] + 'fold' will no longer truncate output when encountering 0xFF bytes. [bug introduced in coreutils-9.8] diff --git a/src/dd.c b/src/dd.c index 07b1c6445..5451b0ac9 100644 --- a/src/dd.c +++ b/src/dd.c @@ -2264,6 +2264,8 @@ dd_copy (void) if (nwritten != n_bytes_read) { diagnose (errno, _("error writing %s"), quoteaf (output_file)); + if (nwritten != 0) + w_partial++; return EXIT_FAILURE; } else if (n_bytes_read == input_blocksize) diff --git a/tests/dd/partial-write.sh b/tests/dd/partial-write.sh new file mode 100755 index 000000000..7b2859472 --- /dev/null +++ b/tests/dd/partial-write.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Ensure partial writes are properly diagnosed + +# Copyright (C) 2026 Free Software Foundation, Inc. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src +print_ver_ dd + +( + ulimit -S -f 1024 || skip_ 'unable to set file size ulimit' + trap '' XFSZ || skip_ 'unable to ignore SIGXFSZ' + dd if=/dev/zero of=f bs=768K count=2 2>err + echo $? > dd.ret +) + +if test "$(cat dd.ret)" = 1; then + grep -F '+1 records out' err || { cat err; fail=1; } +fi + +Exit $fail diff --git a/tests/local.mk b/tests/local.mk index 15766df16..f9cbf9a5d 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -612,6 +612,7 @@ all_tests = \ tests/dd/nocache_eof.sh \ tests/dd/nocache_fail.sh \ tests/dd/not-rewound.sh \ + tests/dd/partial-write.sh \ tests/dd/reblock.sh \ tests/dd/skip-seek.pl \ tests/dd/skip-seek2.sh \ -- 2.53.0