From 269e53379434ad11df9f676408714c4dd53edc2b Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Fri, 26 Aug 2022 09:08:23 -0400 Subject: [PATCH] dd: handle stdout redirected to seekable file Fix a bug in `dd` where null bytes would be unintentionally written if stdout were redirected to a seekable file. For example, before this commit, if `dd` were invoked from the command-line as dd if=infile bs=1 count=10 seek=5 > /dev/sda1 then five zeros would be written to `/dev/sda1` before copying ten bytes of `infile` to `/dev/sda1`. After this commit, `dd` will correctly seek five bytes forward in `/dev/sda1` before copying the ten bytes of `infile`. Fixes #3542. --- src/uu/dd/src/dd.rs | 67 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index fdd6d62e0e2..bf1ee22fdce 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -23,8 +23,9 @@ use blocks::conv_block_unblock_helper; use std::cmp; use std::env; +use std::ffi::OsString; use std::fs::{File, OpenOptions}; -use std::io::{self, Read, Seek, Write}; +use std::io::{self, Read, Seek, SeekFrom, Write}; #[cfg(any(target_os = "linux", target_os = "android"))] use std::os::unix::fs::OpenOptionsExt; use std::path::Path; @@ -682,6 +683,49 @@ fn append_dashes_if_not_present(mut acc: Vec, mut s: String) -> Vec +/// outfile`, then this function returns the canonicalized path to +/// `outfile`, something like `"/path/to/outfile"`. +fn stdout_canonicalized() -> OsString { + match Path::new("/dev/stdout").canonicalize() { + Ok(p) => p.into_os_string(), + Err(_) => OsString::from("/dev/stdout"), + } +} + +/// Decide whether stdout is being redirected to a seekable file. +/// +/// For example, if this process were invoked from the command line as +/// +/// ```sh +/// dd if=/dev/zero bs=1 count=10 seek=5 > /dev/sda1 +/// ``` +/// +/// where `/dev/sda1` is a seekable block device then this function +/// would return true. If invoked as +/// +/// ```sh +/// dd if=/dev/zero bs=1 count=10 seek=5 +/// ``` +/// +/// then this function would return false. +fn is_stdout_redirected_to_seekable_file() -> bool { + let s = stdout_canonicalized(); + let p = Path::new(&s); + match File::open(p) { + Ok(mut f) => { + f.seek(SeekFrom::Current(0)).is_ok() + && f.seek(SeekFrom::End(0)).is_ok() + && f.seek(SeekFrom::Start(0)).is_ok() + } + Err(_) => false, + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let dashed_args = args @@ -720,6 +764,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { (true, true) => { let ifname = matches.value_of(options::INFILE).unwrap(); let ofname = matches.value_of(options::OUTFILE).unwrap(); + let i = Input::::new( ibs, print_level, @@ -742,13 +787,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let fname = matches.value_of(options::INFILE).unwrap(); let i = Input::::new(ibs, print_level, count, icflags, iflags, fname, skip_amount)?; - let o = Output::::new(obs, oflags, ocflags, seek_amount, "-")?; - o.dd_out(i).map_err_context(|| "IO error".to_string()) + if is_stdout_redirected_to_seekable_file() { + let ofname = stdout_canonicalized().into_string().unwrap(); + let o = Output::::new(obs, oflags, ocflags, seek_amount, &ofname)?; + o.dd_out(i).map_err_context(|| "IO error".to_string()) + } else { + let o = Output::::new(obs, oflags, ocflags, seek_amount, "-")?; + o.dd_out(i).map_err_context(|| "IO error".to_string()) + } } (false, false) => { let i = Input::::new(ibs, print_level, count, icflags, iflags, skip_amount)?; - let o = Output::::new(obs, oflags, ocflags, seek_amount, "-")?; - o.dd_out(i).map_err_context(|| "IO error".to_string()) + if is_stdout_redirected_to_seekable_file() { + let ofname = stdout_canonicalized().into_string().unwrap(); + let o = Output::::new(obs, oflags, ocflags, seek_amount, &ofname)?; + o.dd_out(i).map_err_context(|| "IO error".to_string()) + } else { + let o = Output::::new(obs, oflags, ocflags, seek_amount, "-")?; + o.dd_out(i).map_err_context(|| "IO error".to_string()) + } } } }