* src/dd.c (main): Also account for seeking bytes in a few conditions.
* tests/dd/bytes.sh: Add a test case.
* tests/dd/fail-ftruncate-fstat.sh (fail_ftruncate_fstat): New function.
Use it to test seeking on bytes, and make sure fstat and ftruncate are
called.
* NEWS: Mention the bug fix.

Discovered by Anthropic's OSS Scanner, as bug ANT-2026-NLK8TXH1.
---
 NEWS                             |  5 +++
 src/dd.c                         |  7 ++--
 tests/dd/bytes.sh                | 11 +++++++
 tests/dd/fail-ftruncate-fstat.sh | 56 ++++++++++++++++++--------------
 4 files changed, 51 insertions(+), 28 deletions(-)

diff --git a/NEWS b/NEWS
index ff06b642f..d4345477a 100644
--- a/NEWS
+++ b/NEWS
@@ -58,6 +58,11 @@ GNU coreutils NEWS                                    -*- 
outline -*-
   setting the date and time.
   [bug introduced in coreutils-9.11]
 
+  'dd seek=N' no longer replaces the first N bytes of the file with a hole when
+  N is measured in bytes, i.e., when N ends with a 'B' suffix or
+  oflag=seek_bytes, and when N is smaller than the output block size.
+  [bug introduced with oflag=seek_bytes in coreutils-8.16]
+
   'factor' no longer treats inputs requiring the use of arbitrary-precision
   integers followed by a non-numeric suffix as zero if the suffix includes
   non-space characters, or the number itself when followed by spaces.
diff --git a/src/dd.c b/src/dd.c
index 26382a233..a480d9690 100644
--- a/src/dd.c
+++ b/src/dd.c
@@ -2482,11 +2482,12 @@ main (int argc, char **argv)
   else
     {
       mode_t perms = MODE_RW_UGO;
+      bool const seeking = seek_records || seek_bytes;
       int opts
         = (output_flags
            | (conversions_mask & C_NOCREAT ? 0 : O_CREAT)
            | (conversions_mask & C_EXCL ? O_EXCL : 0)
-           | (seek_records || (conversions_mask & C_NOTRUNC) ? 0 : O_TRUNC));
+           | (seeking || (conversions_mask & C_NOTRUNC) ? 0 : O_TRUNC));
 
       off_t size;
       if ((ckd_mul (&size, seek_records, output_blocksize)
@@ -2501,14 +2502,14 @@ main (int argc, char **argv)
       /* Open the output file with *read* access only if we might
          need to read to satisfy a 'seek=' request.  If we can't read
          the file, go ahead with write-only access; it might work.  */
-      if ((! seek_records
+      if ((! seeking
            || ifd_reopen (STDOUT_FILENO, output_file, O_RDWR | opts, perms) < 
0)
           && (ifd_reopen (STDOUT_FILENO, output_file, O_WRONLY | opts, perms)
               < 0))
         error (EXIT_FAILURE, errno, _("failed to open %s"),
                quoteaf (output_file));
 
-      if (seek_records != 0 && !(conversions_mask & C_NOTRUNC))
+      if (seeking && !(conversions_mask & C_NOTRUNC))
         {
           if (iftruncate (STDOUT_FILENO, size) != 0)
             {
diff --git a/tests/dd/bytes.sh b/tests/dd/bytes.sh
index 013cc0006..5490fc9ac 100755
--- a/tests/dd/bytes.sh
+++ b/tests/dd/bytes.sh
@@ -80,4 +80,15 @@ fi
 for count in B B1 Bx1 KBB BB KBb KBx x1 1x 1xx1; do
   returns_ 1 dd count=$count </dev/null >/dev/null || fail=1
 done
+
+printf abcdefghijklnopqrstuvwxyz >exp || framework_failure_
+for seek_arg in seek=12B 'seek=12 oflag=seek_bytes'; do
+  for bs_arg in '' bs=13 obs=13; do
+    printf abcdefghijklm >out &&
+    printf nopqrstuvwxyz >in || framework_failure_
+    dd if=in of=out $bs_arg $seek_arg status=none || fail=1
+    compare exp out || fail=1
+  done
+done
+
 Exit $fail
diff --git a/tests/dd/fail-ftruncate-fstat.sh b/tests/dd/fail-ftruncate-fstat.sh
index 70936f712..328bb2852 100755
--- a/tests/dd/fail-ftruncate-fstat.sh
+++ b/tests/dd/fail-ftruncate-fstat.sh
@@ -21,30 +21,36 @@ print_ver_ dd
 uses_strace_
 getlimits_
 
-# Setup the file "out" and preserve it's original contents in "exp-out".
-yes | head -n 2048 | tr -d '\n' > out || framework_failure_
-cp out exp-out || framework_failure_
-
-strace -o strace.out \
-  -e fault=ftruncate:error=EPERM \
-  -e fault=fstat:error=EPERM \
-  -P out \
-  -e quiet=path-resolution \
-  dd if=/dev/zero of=out count=1 seek=1 status=none 2>err
-
-ret=$?
-
-# This also implicitly checks for strace invocation issues
-grep -E '^(fstat|ftruncate).*INJECTED' strace.out ||
-  skip_ 'ftruncate or fstat are not intercepted'
-
-# After ftruncate fails, we use fstat to get the file type.
-echo "dd: cannot fstat 'out': $EPERM" > exp
-compare exp err || fail=1
-
-# coreutils 9.1 to 9.9 would mistakenly continue copying after ftruncate
-# failed and exit successfully.
-test "$ret" = 1 || fail=1
-compare exp-out out || fail=1
+fail_ftruncate_fstat ()
+{
+  ftruncate_fstat_syscalls=ftruncate,fstat,fstat64
+  strace -o strace.out -e fault=$ftruncate_fstat_syscalls:error=EPERM \
+    -P out -e quiet=path-resolution "$@" 2>err
+}
+
+fail_ftruncate_fstat true || skip_ 'cannot intercept ftruncate or fstat'
+
+for seek_arg in seek=1 seek=1B 'seek=1 oflag=seek_bytes'; do
+  # Setup the file "out" and preserve it's original contents in "exp-out".
+  yes | head -n 2048 | tr -d '\n' > out || framework_failure_
+  cp out exp-out || framework_failure_
+
+  fail_ftruncate_fstat dd if=/dev/zero of=out count=1 seek=1B status=none
+  ret=$?
+
+  # Seeking on bytes wouldn't perform call ftruncate in coreutils-9.12
+  # and earlier.
+  grep '^fstat.*INJECTED' strace.out || fail=1
+  grep '^ftruncate.*INJECTED' strace.out || fail=1
+
+  # After ftruncate fails, we use fstat to get the file type.
+  echo "dd: cannot fstat 'out': $EPERM" > exp
+  compare exp err || fail=1
+
+  # coreutils 9.1 to 9.9 would mistakenly continue copying after ftruncate
+  # failed and exit successfully.
+  test "$ret" = 1 || fail=1
+  compare exp-out out || fail=1
+done
 
 Exit $fail
-- 
2.56.0


Reply via email to