From 42bf7ae6adf9369f60c865b7bc6fe45aa115a3c4 Mon Sep 17 00:00:00 2001 From: Zhang Wen Date: Thu, 18 Jun 2026 18:54:05 +0800 Subject: [PATCH 1/2] fix skip i64::MAX --- src/uu/dd/locales/en-US.ftl | 1 + src/uu/dd/src/dd.rs | 55 ++++++++++++++++++++++++++++++++++++- tests/by-util/test_dd.rs | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/uu/dd/locales/en-US.ftl b/src/uu/dd/locales/en-US.ftl index 3b72e4a8f6c..f6492cbd92f 100644 --- a/src/uu/dd/locales/en-US.ftl +++ b/src/uu/dd/locales/en-US.ftl @@ -128,6 +128,7 @@ dd-error-cannot-skip-invalid = '{ $file }': cannot skip: Invalid argument dd-error-cannot-seek-invalid = '{ $output }': cannot seek: Invalid argument dd-error-not-directory = setting flags for '{ $file }': Not a directory dd-error-failed-discard-cache = failed to discard cache for: { $file } +dd-error-reading-invalid = error reading '{ $file }': Invalid argument # Parse errors dd-error-unrecognized-operand = Unrecognized operand '{ $operand }' diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 782d2664c21..bd807813d89 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -293,8 +293,27 @@ impl Source { } } } + #[cfg(not(unix))] Self::File(f) => f.seek(SeekFrom::Current(n.try_into().unwrap())), #[cfg(unix)] + Self::File(f) => { + let file_len = f.metadata().as_ref().map_or(u64::MAX, Metadata::len); + match n.try_into() { + Ok(n) => { + let pos = f.seek(SeekFrom::Current(n))?; + if file_len > 0 && pos > file_len { + Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid skip")) + } else { + Ok(pos) + } + } + Err(_) => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid input buffer: {n}"), + )), + } + } + #[cfg(unix)] Self::Fifo(f) => read_and_discard(f, n, ibs), } } @@ -409,7 +428,23 @@ impl<'a> Input<'a> { let mut src = Source::File(src); if settings.skip > 0 { - src.skip(settings.skip, settings.ibs)?; + match src.skip(settings.skip, settings.ibs) { + Ok(_) => {} + Err(e) + if e.kind() == io::ErrorKind::InvalidInput + && e.to_string() == "invalid skip" => + { + // GNU compatibility: + // this case prints the stats but sets the exit code to 1 + show_error!( + "{}", + translate!("dd-error-cannot-skip-offset", "file" => filename.display()) + ); + #[cfg(unix)] + set_exit_code(1); + } + Err(e) => return Err(e.into()), + } } Ok(Self { src, settings }) } @@ -478,7 +513,25 @@ impl Read for Input<'_> { Ok(len) => return Ok(len), Err(e) if e.kind() == io::ErrorKind::Interrupted => (), Err(_) if self.settings.iconv.noerror => return Ok(base_idx), + #[cfg(not(unix))] Err(e) => return Err(e), + #[cfg(unix)] + Err(e) if e.kind() == io::ErrorKind::InvalidInput => { + // GNU compatibility: + // this case prints the stats but sets the exit code to 1 + if let Some(infile) = &self.settings.infile { + show_error!( + "{}", + translate!("dd-error-reading-invalid", "file" => infile) + ); + set_exit_code(1); + } + return Ok(0); + } + Err(e) => { + println!("catch the error"); + return Err(e); + } } } } diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 309a93ac020..4040c13161c 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -2128,3 +2128,51 @@ fn test_bs_not_positive() { } } } + +#[test] +#[cfg(unix)] +fn test_skip_i64_max() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let infile_name = "infile"; + at.touch(infile_name); + let infile = at.plus(infile_name); + + new_ucmd!() + .args(&[ + format!("if={}", infile.display()), + "of=outfile".to_string(), + "bs=1".to_string(), + "skip=9223372036854775807".to_string(), + ]) + .fails() + .no_stdout() + .code_is(1) + .stderr_does_not_contain(format!( + "dd: '{}': cannot skip to specified offset", + infile.display() + )) + .stderr_contains(format!( + "dd: error reading '{}': Invalid argument\n0+0 records in\n0+0 records out\n", + infile.display() + )); + + at.write("infile", "abcd"); + + new_ucmd!() + .args(&[ + format!("if={}", infile.display()), + "of=outfile".to_string(), + "bs=1".to_string(), + "skip=9223372036854775807".to_string(), + ]) + .fails() + .no_stdout() + .code_is(1) + .stderr_contains(format!( + "dd: '{}': cannot skip to specified offset\ndd: error reading '{}': Invalid argument\n0+0 records in\n0+0 records out\n", + infile.display(), + infile.display() + )); +} From 64fb06727f7e39c2129e79353cdb76fd2a07b163 Mon Sep 17 00:00:00 2001 From: Zhang Wen Date: Thu, 18 Jun 2026 20:02:40 +0800 Subject: [PATCH 2/2] fix seek i64:MAX when infile is not empty --- src/uu/dd/locales/en-US.ftl | 1 + src/uu/dd/src/dd.rs | 58 +++++++++++++++++++++++++++++----- tests/by-util/test_dd.rs | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/src/uu/dd/locales/en-US.ftl b/src/uu/dd/locales/en-US.ftl index f6492cbd92f..1a721b51843 100644 --- a/src/uu/dd/locales/en-US.ftl +++ b/src/uu/dd/locales/en-US.ftl @@ -129,6 +129,7 @@ dd-error-cannot-seek-invalid = '{ $output }': cannot seek: Invalid argument dd-error-not-directory = setting flags for '{ $file }': Not a directory dd-error-failed-discard-cache = failed to discard cache for: { $file } dd-error-reading-invalid = error reading '{ $file }': Invalid argument +dd-error-writing-invalid = error writing '{ $file }': Invalid argument # Parse errors dd-error-unrecognized-operand = Unrecognized operand '{ $operand }' diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index bd807813d89..594b97cbc09 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -925,18 +925,30 @@ impl<'a> Output<'a> { dst.set_len(settings.seek).ok(); } - Self::prepare_file(dst, settings) + Self::prepare_file(filename, dst, settings) } - fn prepare_file(dst: File, settings: &'a Settings) -> UResult { + fn prepare_file(filename: &Path, dst: File, settings: &'a Settings) -> UResult { let density = if settings.oconv.sparse { Density::Sparse } else { Density::Dense }; let mut dst = Dest::File(dst, density); - dst.seek(settings.seek, settings.obs) - .map_err_context(|| translate!("dd-error-failed-to-seek"))?; + match dst.seek(settings.seek, settings.obs) { + Ok(offset) => Ok(offset), + Err(e) if e.kind() == io::ErrorKind::InvalidInput => { + print!("====debug_for_test_fail===="); + show_error!( + "{}", + translate!("dd-error-writing-invalid", "file" => filename.display()) + ); + #[cfg(unix)] + set_exit_code(1); + return Ok(Self { dst, settings }); + } + Err(e) => Err(e).map_err_context(|| translate!("dd-error-failed-to-seek")), + }?; Ok(Self { dst, settings }) } @@ -956,7 +968,7 @@ impl<'a> Output<'a> { .map_err(|e| uucore::error::UIoError::from(io::Error::from(e)))?; } - Self::prepare_file(fx.into_file(), settings) + Self::prepare_file(Path::new("stdout"), fx.into_file(), settings) } /// Instantiate this struct with the given named pipe as a destination. @@ -1035,6 +1047,12 @@ impl<'a> Output<'a> { } } Err(e) if e.kind() == io::ErrorKind::Interrupted => (), + Err(e) if e.kind() == io::ErrorKind::InvalidInput => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "invalid input buffer", + )); + } Err(e) => return Err(e), } } @@ -1288,7 +1306,6 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { } break; } - let wstat_update = o.write_blocks(&buf)?; // Discard the system file cache for the read portion of // the input file. @@ -1302,6 +1319,34 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { } read_offset += read_len; + // Update the read stats before writing for better error handling + rstat += rstat_update; + + // we need to break the loop when encountering an invalid input buffer + let wstat_update = match o.write_blocks(&buf) { + Ok(wstat) => wstat, + Err(e) + if e.kind() == io::ErrorKind::InvalidInput + && e.to_string() == "invalid input buffer" => + { + // GNU compatibility: + // this case prints the stats but sets the exit code to 1 + if let Some(outfile) = &i.settings.outfile { + show_error!( + "{}", + translate!("dd-error-writing-invalid", "file" => outfile) + ); + #[cfg(unix)] + set_exit_code(1); + } + break; + } + Err(e) => { + show_error!("capture the error: {}", buf.len()); + return Err(e); + } + }; + // Discard the system file cache for the written portion // of the output file. // @@ -1320,7 +1365,6 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // error. Since it is just reporting progress and is not // crucial to the operation of `dd`, let's just ignore the // error. - rstat += rstat_update; wstat += wstat_update; #[cfg(target_os = "linux")] if check_and_reset_sigusr1() { diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 4040c13161c..9869e3d7afb 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -2176,3 +2176,65 @@ fn test_skip_i64_max() { infile.display() )); } + +#[test] +#[cfg(unix)] +fn test_seek_i64_max_infile_not_empty() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let infile_name = "infile"; + let outfile_name = "outfile"; + let infile = at.plus(infile_name); + let outfile = at.plus(outfile_name); + + at.write(infile_name, "a"); + at.touch(outfile_name); + + new_ucmd!() + .args(&[ + format!("if={}", infile.display()), + format!("of={}", outfile.display()), + "bs=1".to_string(), + "seek=9223372036854775807".to_string(), + ]) + .fails() + .no_stdout() + .code_is(1) + .stderr_contains(format!( + "dd: error writing '{}': Invalid argument\n1+0 records in\n0+0 records out\n", + outfile.display() + )); + + at.write(infile_name, "ab"); + + new_ucmd!() + .args(&[ + format!("if={}", infile.display()), + format!("of={}", outfile.display()), + "bs=1".to_string(), + "seek=9223372036854775807".to_string(), + ]) + .fails() + .no_stdout() + .code_is(1) + .stderr_contains(format!( + "dd: error writing '{}': Invalid argument\n1+0 records in\n0+0 records out\n", + outfile.display() + )); + + new_ucmd!() + .args(&[ + format!("if={}", infile.display()), + format!("of={}", outfile.display()), + "bs=1".to_string(), + "seek=9223372036854775806".to_string(), + ]) + .fails() + .no_stdout() + .code_is(1) + .stderr_contains(format!( + "dd: error writing '{}': Invalid argument\n2+0 records in\n1+0 records out\n", + outfile.display() + )); +}