diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index f35b1a0db2c..b2ae21e4d03 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -719,6 +719,10 @@ fn wipe_file( } if remove_method != RemoveMethod::None { + file.set_len(0).map_err_context( + || translate!("shred-failed-to-remove-file", "file" => path.maybe_quote()), + )?; + do_remove(path, path_str, verbose, remove_method).map_err_context( || translate!("shred-failed-to-remove-file", "file" => path.maybe_quote()), )?; diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index 81f69fb5c66..ccbca2056d3 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -420,3 +420,30 @@ fn test_gnu_shred_passes_different_counts() { result.stderr_contains("pass 1/19 (random)"); result.stderr_contains("pass 19/19 (random)"); } + +#[test] +#[cfg(all(unix, feature = "chmod"))] +fn test_shred_truncates_before_unlink_readonly_parent() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let dir = "truncate_test"; + let file_path = "truncate_test/file"; + + at.mkdir(dir); + // Create a 1MB file + at.make_file(file_path).set_len(1024 * 1024).unwrap(); + + // Remove write permission from parent (unlink will fail) + scene.ccmd("chmod").arg("a-w").arg(dir).succeeds(); + + // shred -u will fail to unlink but must still truncate + scene.ucmd().arg("-u").arg(file_path).fails(); + + // Restore directory permissions + scene.ccmd("chmod").arg("u+w").arg(dir).succeeds(); + + // File still exists (couldn't unlink) but must have been truncated to 0 + assert!(at.file_exists(file_path)); + assert_eq!(at.metadata(file_path).len(), 0); +}