From 2f27e55b69ae27cdb7d4e4e0cf06082a90f60550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Fri, 26 Jun 2026 19:05:48 +0300 Subject: [PATCH] dd: warn when skip goes past the end of the input file The File source seeked without checking the resulting offset against the file size, so dd did not print GNU's "cannot skip to specified offset" message. Add the check the seekable-stdin path already has, and guard both arms so an empty file stays silent, matching GNU. Closes #13047 --- src/uu/dd/src/dd.rs | 34 ++++++++++++++++++++------------ tests/by-util/test_dd.rs | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 782d2664c21..5edc0031ba0 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -27,7 +27,6 @@ use uucore::translate; use std::cmp; use std::env; use std::ffi::OsString; -#[cfg(unix)] use std::fs::Metadata; use std::fs::{File, OpenOptions}; use std::io::{self, Read, Seek, SeekFrom, Write}; @@ -231,7 +230,7 @@ impl Source { Self::StdinFile(f) } - fn skip(&mut self, n: u64, ibs: usize) -> io::Result { + fn skip(&mut self, n: u64, ibs: usize, name: &str) -> io::Result { match self { #[cfg(not(unix))] Self::Stdin(stdin) => { @@ -239,7 +238,7 @@ impl Source { if m < n { show_error!( "{}", - translate!("dd-error-cannot-skip-offset", "file" => "standard input") + translate!("dd-error-cannot-skip-offset", "file" => name) ); } Ok(m) @@ -253,7 +252,7 @@ impl Source { // this case prints the stats but sets the exit code to 1 show_error!( "{}", - translate!("dd-error-cannot-skip-invalid", "file" => "standard input") + translate!("dd-error-cannot-skip-invalid", "file" => name) ); set_exit_code(1); return Ok(len); @@ -263,10 +262,10 @@ impl Source { // Try seek first; fall back to read if not seekable match n.try_into().ok().map(|n| f.seek(SeekFrom::Current(n))) { Some(Ok(pos)) => { - if pos > file_len { + if file_len != 0 && pos > file_len { show_error!( "{}", - translate!("dd-error-cannot-skip-offset", "file" => "standard input") + translate!("dd-error-cannot-skip-offset", "file" => name) ); } Ok(n) @@ -278,7 +277,7 @@ impl Source { if m < n { show_error!( "{}", - translate!("dd-error-cannot-skip-offset", "file" => "standard input") + translate!("dd-error-cannot-skip-offset", "file" => name) ); } Ok(m) @@ -286,14 +285,25 @@ impl Source { _ => { show_error!( "{}", - translate!("dd-error-cannot-skip-invalid", "file" => "standard input") + translate!("dd-error-cannot-skip-invalid", "file" => name) ); set_exit_code(1); Ok(0) } } } - Self::File(f) => f.seek(SeekFrom::Current(n.try_into().unwrap())), + Self::File(f) => { + // Get file length before seeking to avoid race condition + let file_len = f.metadata().as_ref().map_or(u64::MAX, Metadata::len); + let pos = f.seek(SeekFrom::Current(n.try_into().unwrap()))?; + if file_len != 0 && pos > file_len { + show_error!( + "{}", + translate!("dd-error-cannot-skip-offset", "file" => name) + ); + } + Ok(pos) + } #[cfg(unix)] Self::Fifo(f) => read_and_discard(f, n, ibs), } @@ -386,7 +396,7 @@ impl<'a> Input<'a> { } if settings.skip > 0 { - src.skip(settings.skip, settings.ibs)?; + src.skip(settings.skip, settings.ibs, "standard input")?; } Ok(Self { src, settings }) } @@ -409,7 +419,7 @@ impl<'a> Input<'a> { let mut src = Source::File(src); if settings.skip > 0 { - src.skip(settings.skip, settings.ibs)?; + src.skip(settings.skip, settings.ibs, &filename.to_string_lossy())?; } Ok(Self { src, settings }) } @@ -423,7 +433,7 @@ impl<'a> Input<'a> { opts.custom_flags(make_linux_iflags(&settings.iflags).unwrap_or(0)); let mut src = Source::Fifo(opts.open(filename)?); if settings.skip > 0 { - src.skip(settings.skip, settings.ibs)?; + src.skip(settings.skip, settings.ibs, &filename.to_string_lossy())?; } Ok(Self { src, settings }) } diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 309a93ac020..b969cd4deaf 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -724,6 +724,48 @@ fn test_skip_beyond_file_seekable_stdin() { } } +/// Test that skipping beyond the end of a named input file prints a warning. +#[test] +fn test_skip_beyond_named_file() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("in", "abcd"); + ucmd.args(&["if=in", "bs=1", "skip=5", "count=0", "status=noxfer"]) + .succeeds() + .no_stdout() + .stderr_contains( + "'in': cannot skip to specified offset\n0+0 records in\n0+0 records out\n", + ); +} + +/// Test that skipping in an empty named input file does not print a warning. +#[test] +fn test_skip_in_empty_named_file_no_warning() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("in", ""); + ucmd.args(&["if=in", "bs=1", "skip=1", "count=0", "status=noxfer"]) + .succeeds() + .no_stdout() + .stderr_is("0+0 records in\n0+0 records out\n"); +} + +/// Test that skipping in an empty seekable stdin does not print a warning. +#[test] +#[cfg(unix)] +fn test_skip_in_empty_seekable_stdin_no_warning() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("in", ""); + let stdin = OwnedFileDescriptorOrHandle::open_file( + OpenOptions::new().read(true), + at.plus("in").as_path(), + ) + .unwrap(); + ucmd.args(&["bs=1", "skip=1", "count=0", "status=noxfer"]) + .set_stdin(Stdio::from(stdin)) + .succeeds() + .no_stdout() + .stderr_is("0+0 records in\n0+0 records out\n"); +} + #[test] fn test_seek_do_not_overwrite() { let (at, mut ucmd) = at_and_ucmd!();