diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index c3e8bf40bb7..ed4ed7a5c41 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -685,6 +685,19 @@ fn wipe_file( #[cfg(not(target_os = "openbsd"))] let get_metadata = || fs::metadata(path); + #[cfg(unix)] + let (is_block_device, is_char_device) = match get_metadata() { + Ok(md) => { + use std::os::unix::fs::FileTypeExt; + let ft = md.file_type(); + (ft.is_block_device(), ft.is_char_device()) + } + Err(_) => (false, false), + }; + + #[cfg(not(unix))] + let (is_block_device, is_char_device) = (false, false); + // `Path::exists()` and `Path::is_file()` both collapse any metadata error // (including a permission error) into `false`, which made shred report a // file whose parent directory lacks search permission as "No such file or @@ -692,7 +705,7 @@ fn wipe_file( // "no such file" error while a permission error falls through to the // open-for-writing below, which surfaces the real reason. match get_metadata() { - Ok(md) if !md.is_file() => { + Ok(md) if !md.is_file() && !is_block_device && !is_char_device => { return Err(USimpleError::new( 1, translate_text!("shred-not-a-file", "file" => display_path.maybe_quote()), @@ -709,6 +722,10 @@ fn wipe_file( let metadata = get_metadata().map_err_context(|| translate!("shred-failed-to-get-metadata"))?; + // GNU enforces --exact for devices since they have a fixed physical size + // and padding would write beyond the intended boundary. + let exact = exact || is_block_device || is_char_device; + // If force is true, set file permissions to not-readonly. if force && !is_stdout { let mut perms = metadata.permissions(); @@ -731,8 +748,9 @@ fn wipe_file( // Fill up our pass sequence let mut pass_sequence = Vec::new(); - if metadata.len() != 0 { - // Only add passes if the file is non-empty + if metadata.len() != 0 || is_block_device || is_char_device { + // For regular files, only add passes if not empty. For devices, always run passes: + // block devices report len=0 via metadata, char devices write until ENOSPC if n_passes <= 3 { // Only random passes if n_passes <= 3 @@ -763,8 +781,19 @@ fn wipe_file( || translate_text!("shred-failed-to-open-for-writing", "file" => display_path.maybe_quote()), )?; + let write_until_enospc = is_char_device && size.is_none(); let size = match size { Some(size) => size, + None if is_block_device => { + //metadata.len() returns 0 for block devices on Linux; + //seeking to the end gives the real size + let len = file + .seek(SeekFrom::End(0)) + .map_err_context(|| translate!("shred-failed-to-get-metadata"))?; + file.rewind() + .map_err_context(|| translate!("shred-failed-to-get-metadata"))?; + len + } None => metadata.len(), }; @@ -778,7 +807,16 @@ fn wipe_file( ); } // size is an optional argument for exactly how many bytes we want to shred - do_pass(&mut file, &pass_type, exact, random_source, size).map_err_context( + do_pass( + &mut file, + &pass_type, + exact, + random_source, + size, + is_char_device, + write_until_enospc, + ) + .map_err_context( || translate_text!("shred-file-write-pass-failed", "file" => display_path.maybe_quote()), )?; } @@ -822,11 +860,30 @@ fn do_pass( exact: bool, random_source: Option<&RefCell>, file_size: u64, + is_char_device: bool, + write_until_enospc: bool, ) -> Result<(), io::Error> { - // We might be at the end of the file due to a previous iteration, so rewind. - file.rewind()?; - let mut writer = BytesWriter::from_pass_type(pass_type, random_source)?; + + // Char devices with no known size: write until ENOSPC (e.g. /dev/full). + // Char devices with --size, and all regular/block paths, fall through below. + if write_until_enospc { + loop { + let block = writer.bytes_for_pass(BLOCK_SIZE)?; + match file.write_all(block) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::StorageFull => return Ok(()), + Err(e) => return Err(e), + } + } + } + + // Regular files and block devices need rewinding between passes; + // char devices don't support seeking so we skip it. + if !is_char_device { + file.rewind()?; + } + let (number_of_blocks, bytes_left) = split_on_blocks(file_size, exact); // We start by writing BLOCK_SIZE times as many time as possible. @@ -839,7 +896,9 @@ fn do_pass( let block = writer.bytes_for_pass(bytes_left as usize)?; file.write_all(block)?; - file.sync_data()?; + if !is_char_device { + file.sync_data()?; + } Ok(()) } diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 519c4ceca61..47c951d4daf 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -692,3 +692,62 @@ fn test_shred_dash_rejects_stdout_pipe() { .stderr_contains("shred: -:") .stderr_does_not_contain("/dev/fd/1"); } + +// shred /dev/full should succeed (ENOSPC is a valid termination condition for devices) +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host device paths (/dev) not visible" +)] +fn test_shred_dev_full() { + new_ucmd!().arg("/dev/full").succeeds(); +} + +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host device paths (/dev) not visible" +)] +fn test_shred_dev_full_with_size() { + new_ucmd!() + .args(&["-n1", "--size=1", "/dev/full"]) + .fails() + .stderr_contains("No space left on device"); +} + +// shred /dev/null should loop until interrupted; with --size we can bound it +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host device paths (/dev) not visible" +)] +fn test_shred_dev_null() { + new_ucmd!().arg("--size=1M").arg("/dev/null").succeeds(); +} + +#[test] +#[cfg(target_os = "linux")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host device paths (/dev) not visible" +)] +fn test_shred_dev_null_zero_size() { + new_ucmd!() + .args(&["-n1", "--size=0", "/dev/null"]) + .succeeds(); +} + +#[test] +#[cfg(unix)] +fn test_shred_rejects_fifo() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.mkfifo("fifo"); + + ucmd.arg("fifo") + .fails_with_code(1) + .stderr_contains("shred: fifo: Not a file\n"); +}