From dc61b925e760dea36b99ca2c84898bc776dc06b7 Mon Sep 17 00:00:00 2001 From: yor1xd Date: Thu, 14 May 2026 14:39:42 -0300 Subject: [PATCH 1/5] shred: support character and block devices --- src/uu/shred/src/shred.rs | 72 ++++++++++++++++++++++++++++++++----- tests/by-util/test_shred.rs | 14 ++++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index c3e8bf40bb7..83404f78964 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 @@ -765,6 +783,16 @@ fn wipe_file( 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 +806,15 @@ 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, + ) + .map_err_context( || translate_text!("shred-file-write-pass-failed", "file" => display_path.maybe_quote()), )?; } @@ -822,11 +858,29 @@ fn do_pass( exact: bool, random_source: Option<&RefCell>, file_size: u64, + is_char_device: 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 is_char_device && file_size == 0 { + 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 +893,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..196cfb797b1 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -692,3 +692,17 @@ 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")] +fn test_shred_dev_full() { + new_ucmd!().arg("/dev/full").succeeds(); +} + +// shred /dev/null should loop until interrupted; with --size we can bound it +#[test] +#[cfg(target_os = "linux")] +fn test_shred_dev_null() { + new_ucmd!().arg("--size=1M").arg("/dev/null").succeeds(); +} From 5256b9b4aabe8ebe7391a5ea0f42a4472cd2f9fa Mon Sep 17 00:00:00 2001 From: yor1xd Date: Tue, 20 Oct 2026 14:23:36 -0300 Subject: [PATCH 2/5] shred: handle device edge case and add regression tests --- src/uu/shred/src/shred.rs | 5 ++++- tests/by-util/test_shred.rs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 83404f78964..0a04ec90f7d 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -781,6 +781,7 @@ fn wipe_file( || translate_text!("shred-failed-to-open-for-writing", "file" => display_path.maybe_quote()), )?; + let unbounded_char_device = is_char_device && size.is_none(); let size = match size { Some(size) => size, None if is_block_device => { @@ -813,6 +814,7 @@ fn wipe_file( random_source, size, is_char_device, + unbounded_char_device, ) .map_err_context( || translate_text!("shred-file-write-pass-failed", "file" => display_path.maybe_quote()), @@ -859,12 +861,13 @@ fn do_pass( random_source: Option<&RefCell>, file_size: u64, is_char_device: bool, + unbounded_char_device: bool, ) -> Result<(), io::Error> { 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 is_char_device && file_size == 0 { + if unbounded_char_device { loop { let block = writer.bytes_for_pass(BLOCK_SIZE)?; match file.write_all(block) { diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 196cfb797b1..8d84fea303a 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -706,3 +706,21 @@ fn test_shred_dev_full() { fn test_shred_dev_null() { new_ucmd!().arg("--size=1M").arg("/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().stderr_contains("fifo"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_shred_dev_null_zero_size() { + new_ucmd!() + .args(&["-n1", "--size=0", "/dev/null"]) + .succeeds(); +} From 8fb9f90de2d3768545853df0c2d741e6cbd20143 Mon Sep 17 00:00:00 2001 From: yor1xd Date: Tue, 20 Oct 2026 14:50:12 -0300 Subject: [PATCH 3/5] shred: skip device tests under WASI runner --- tests/by-util/test_shred.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 8d84fea303a..a0567557de6 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -696,12 +696,14 @@ fn test_shred_dash_rejects_stdout_pipe() { // 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(); } // shred /dev/null should loop until interrupted; with --size we can bound it #[test] +#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host device paths (/dev) not visible")] #[cfg(target_os = "linux")] fn test_shred_dev_null() { new_ucmd!().arg("--size=1M").arg("/dev/null").succeeds(); @@ -719,6 +721,7 @@ fn test_shred_rejects_fifo() { #[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"]) From 6e67b86b1620a513dff65a6be7c9db0f368178d5 Mon Sep 17 00:00:00 2001 From: yor1xd Date: Tue, 20 Oct 2026 14:57:04 -0300 Subject: [PATCH 4/5] shred: apply rustfmt --- tests/by-util/test_shred.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index a0567557de6..bef4c2949c4 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -696,14 +696,20 @@ fn test_shred_dash_rejects_stdout_pipe() { // 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")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host device paths (/dev) not visible" +)] fn test_shred_dev_full() { new_ucmd!().arg("/dev/full").succeeds(); } // shred /dev/null should loop until interrupted; with --size we can bound it #[test] -#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host device paths (/dev) not visible")] +#[cfg_attr( + wasi_runner, + ignore = "WASI sandbox: host device paths (/dev) not visible" +)] #[cfg(target_os = "linux")] fn test_shred_dev_null() { new_ucmd!().arg("--size=1M").arg("/dev/null").succeeds(); @@ -721,7 +727,10 @@ fn test_shred_rejects_fifo() { #[test] #[cfg(target_os = "linux")] -#[cfg_attr(wasi_runner, ignore = "WASI sandbox: host device paths (/dev) not visible")] +#[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"]) From 0782457bd3ed1b09386e32da0038b9315b34d501 Mon Sep 17 00:00:00 2001 From: gabrielhnf Date: Mon, 26 Oct 2026 00:15:53 -0300 Subject: [PATCH 5/5] shred: clarify character device handling and test bounded writes --- src/uu/shred/src/shred.rs | 8 ++++---- tests/by-util/test_shred.rs | 37 ++++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index 0a04ec90f7d..ed4ed7a5c41 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -781,7 +781,7 @@ fn wipe_file( || translate_text!("shred-failed-to-open-for-writing", "file" => display_path.maybe_quote()), )?; - let unbounded_char_device = is_char_device && size.is_none(); + let write_until_enospc = is_char_device && size.is_none(); let size = match size { Some(size) => size, None if is_block_device => { @@ -814,7 +814,7 @@ fn wipe_file( random_source, size, is_char_device, - unbounded_char_device, + write_until_enospc, ) .map_err_context( || translate_text!("shred-file-write-pass-failed", "file" => display_path.maybe_quote()), @@ -861,13 +861,13 @@ fn do_pass( random_source: Option<&RefCell>, file_size: u64, is_char_device: bool, - unbounded_char_device: bool, + write_until_enospc: bool, ) -> Result<(), io::Error> { 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 unbounded_char_device { + if write_until_enospc { loop { let block = writer.bytes_for_pass(BLOCK_SIZE)?; match file.write_all(block) { diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index bef4c2949c4..47c951d4daf 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -704,25 +704,28 @@ fn test_shred_dev_full() { new_ucmd!().arg("/dev/full").succeeds(); } -// 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" )] -#[cfg(target_os = "linux")] -fn test_shred_dev_null() { - new_ucmd!().arg("--size=1M").arg("/dev/null").succeeds(); +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(unix)] -fn test_shred_rejects_fifo() { - let (at, mut ucmd) = at_and_ucmd!(); - - at.mkfifo("fifo"); - - ucmd.arg("fifo").fails().stderr_contains("fifo"); +#[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] @@ -736,3 +739,15 @@ fn test_shred_dev_null_zero_size() { .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"); +}