Skip to content

Commit a8c2352

Browse files
committed
rm: fix --preserve-root detection for nested symlinks with trailing slash
1 parent 26498f5 commit a8c2352

File tree

4 files changed

+129
-16
lines changed

4 files changed

+129
-16
lines changed

src/uu/rm/locales/en-US.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ rm-error-cannot-remove-no-such-file = cannot remove {$file}: No such file or dir
4040
rm-error-cannot-remove-permission-denied = cannot remove {$file}: Permission denied
4141
rm-error-cannot-remove-is-directory = cannot remove {$file}: Is a directory
4242
rm-error-dangerous-recursive-operation = it is dangerous to operate recursively on '/'
43+
rm-error-dangerous-recursive-operation-same-as-root = it is dangerous to operate recursively on '{$path}' (same as '/')
4344
rm-error-use-no-preserve-root = use --no-preserve-root to override this failsafe
4445
rm-error-refusing-to-remove-directory = refusing to remove '.' or '..' directory: skipping {$path}
4546
rm-error-cannot-remove = cannot remove {$file}

src/uu/rm/locales/fr-FR.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ rm-error-cannot-remove-no-such-file = impossible de supprimer {$file} : Aucun fi
4040
rm-error-cannot-remove-permission-denied = impossible de supprimer {$file} : Permission refusée
4141
rm-error-cannot-remove-is-directory = impossible de supprimer {$file} : C'est un répertoire
4242
rm-error-dangerous-recursive-operation = il est dangereux d'opérer récursivement sur '/'
43+
rm-error-dangerous-recursive-operation-same-as-root = il est dangereux d'opérer récursivement sur '{$path}' (identique à '/')
4344
rm-error-use-no-preserve-root = utilisez --no-preserve-root pour outrepasser cette protection
4445
rm-error-refusing-to-remove-directory = refus de supprimer le répertoire '.' ou '..' : ignorer {$path}
4546
rm-error-cannot-remove = impossible de supprimer {$file}

src/uu/rm/src/rm.rs

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use std::os::unix::ffi::OsStrExt;
1717
#[cfg(unix)]
1818
use std::os::unix::fs::PermissionsExt;
1919
use std::path::MAIN_SEPARATOR;
20-
use std::path::{Path, PathBuf};
20+
use std::path::Path;
2121
use thiserror::Error;
2222
use uucore::display::Quotable;
2323
use uucore::error::{FromIo, UError, UResult};
@@ -56,7 +56,7 @@ fn verbose_removed_file(path: &Path, options: &Options) {
5656
if options.verbose {
5757
println!(
5858
"{}",
59-
translate!("rm-verbose-removed", "file" => normalize(path).quote())
59+
translate!("rm-verbose-removed", "file" => uucore::fs::normalize_path(path).quote())
6060
);
6161
}
6262
}
@@ -66,7 +66,7 @@ fn verbose_removed_directory(path: &Path, options: &Options) {
6666
if options.verbose {
6767
println!(
6868
"{}",
69-
translate!("rm-verbose-removed-directory", "file" => normalize(path).quote())
69+
translate!("rm-verbose-removed-directory", "file" => uucore::fs::normalize_path(path).quote())
7070
);
7171
}
7272
}
@@ -229,6 +229,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
229229
})
230230
};
231231

232+
let preserve_root = !matches.get_flag(OPT_NO_PRESERVE_ROOT);
233+
let recursive = matches.get_flag(OPT_RECURSIVE);
234+
232235
let options = Options {
233236
force: force_flag,
234237
interactive: {
@@ -245,8 +248,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
245248
}
246249
},
247250
one_fs: matches.get_flag(OPT_ONE_FILE_SYSTEM),
248-
preserve_root: !matches.get_flag(OPT_NO_PRESERVE_ROOT),
249-
recursive: matches.get_flag(OPT_RECURSIVE),
251+
preserve_root,
252+
recursive,
250253
dir: matches.get_flag(OPT_DIR),
251254
verbose: matches.get_flag(OPT_VERBOSE),
252255
progress: matches.get_flag(OPT_PROGRESS),
@@ -487,6 +490,19 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
487490
for filename in files {
488491
let file = Path::new(filename);
489492

493+
// Check if the path (potentially with trailing slash) resolves to root
494+
// This needs to happen before symlink_metadata to catch cases like "rootlink/"
495+
// where rootlink is a symlink to root.
496+
if uucore::fs::path_ends_with_terminator(file)
497+
&& options.recursive
498+
&& options.preserve_root
499+
&& is_root_path(file)
500+
{
501+
show_preserve_root_error(file);
502+
had_err = true;
503+
continue;
504+
}
505+
490506
had_err = match file.symlink_metadata() {
491507
Ok(metadata) => {
492508
// Create progress bar on first successful file metadata read
@@ -673,6 +689,39 @@ fn remove_dir_recursive(
673689
}
674690
}
675691

692+
/// Check if a path resolves to the root directory.
693+
/// Returns true if the path is root, false otherwise.
694+
fn is_root_path(path: &Path) -> bool {
695+
// Check simple case: literal "/" path
696+
if path.has_root() && path.parent().is_none() {
697+
return true;
698+
}
699+
700+
// Check if path resolves to "/" after following symlinks
701+
if let Ok(canonical) = path.canonicalize() {
702+
canonical.has_root() && canonical.parent().is_none()
703+
} else {
704+
false
705+
}
706+
}
707+
708+
/// Show error message for attempting to remove root.
709+
fn show_preserve_root_error(path: &Path) {
710+
let path_looks_like_root = path.has_root() && path.parent().is_none();
711+
712+
if path_looks_like_root {
713+
// Path is literally "/"
714+
show_error!("{}", RmError::DangerousRecursiveOperation);
715+
} else {
716+
// Path resolves to root but isn't literally "/" (e.g., symlink to /)
717+
show_error!(
718+
"it is dangerous to operate recursively on '{}' (same as '/')",
719+
path.display()
720+
);
721+
}
722+
show_error!("{}", RmError::UseNoPreserveRoot);
723+
}
724+
676725
fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>) -> bool {
677726
let mut had_err = false;
678727

@@ -685,14 +734,13 @@ fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>
685734
return true;
686735
}
687736

688-
let is_root = path.has_root() && path.parent().is_none();
737+
let is_root = is_root_path(path);
689738
if options.recursive && (!is_root || !options.preserve_root) {
690739
had_err = remove_dir_recursive(path, options, progress_bar);
691740
} else if options.dir && (!is_root || !options.preserve_root) {
692741
had_err = remove_dir(path, options, progress_bar).bitor(had_err);
693742
} else if options.recursive {
694-
show_error!("{}", RmError::DangerousRecursiveOperation);
695-
show_error!("{}", RmError::UseNoPreserveRoot);
743+
show_preserve_root_error(path);
696744
had_err = true;
697745
} else {
698746
show_error!(
@@ -940,14 +988,6 @@ fn prompt_descend(path: &Path) -> bool {
940988
prompt_yes!("descend into directory {}?", path.quote())
941989
}
942990

943-
fn normalize(path: &Path) -> PathBuf {
944-
// copied from https://github.com/rust-lang/cargo/blob/2e4cfc2b7d43328b207879228a2ca7d427d188bb/src/cargo/util/paths.rs#L65-L90
945-
// both projects are MIT https://github.com/rust-lang/cargo/blob/master/LICENSE-MIT
946-
// for std impl progress see rfc https://github.com/rust-lang/rfcs/issues/2208
947-
// TODO: replace this once that lands
948-
uucore::fs::normalize_path(path)
949-
}
950-
951991
#[cfg(not(windows))]
952992
fn is_symlink_dir(_metadata: &Metadata) -> bool {
953993
false

tests/by-util/test_rm.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
5+
// spell-checker:ignore rootlink
56
#![allow(clippy::stable_sort_primitive)]
67

78
use std::process::Stdio;
@@ -1290,3 +1291,73 @@ fn test_symlink_to_readonly_no_prompt() {
12901291

12911292
assert!(!at.symlink_exists("bar"));
12921293
}
1294+
1295+
/// Test that --preserve-root properly detects symlinks pointing to root.
1296+
#[cfg(unix)]
1297+
#[test]
1298+
fn test_preserve_root_symlink_to_root() {
1299+
let (at, mut ucmd) = at_and_ucmd!();
1300+
1301+
// Create a symlink pointing to the root directory
1302+
at.symlink_dir("/", "rootlink");
1303+
1304+
// Attempting to recursively delete through this symlink should fail
1305+
// because it resolves to the same device/inode as "/"
1306+
ucmd.arg("-rf")
1307+
.arg("--preserve-root")
1308+
.arg("rootlink/")
1309+
.fails()
1310+
.stderr_contains("it is dangerous to operate recursively on")
1311+
.stderr_contains("(same as '/')");
1312+
1313+
// The symlink itself should still exist (we didn't delete it)
1314+
assert!(at.symlink_exists("rootlink"));
1315+
}
1316+
1317+
/// Test that --preserve-root properly detects nested symlinks pointing to root.
1318+
#[cfg(unix)]
1319+
#[test]
1320+
fn test_preserve_root_nested_symlink_to_root() {
1321+
let (at, mut ucmd) = at_and_ucmd!();
1322+
1323+
// Create a symlink pointing to the root directory
1324+
at.symlink_dir("/", "rootlink");
1325+
// Create another symlink pointing to the first symlink
1326+
at.symlink_dir("rootlink", "rootlink2");
1327+
1328+
// Attempting to recursively delete through nested symlinks should also fail
1329+
ucmd.arg("-rf")
1330+
.arg("--preserve-root")
1331+
.arg("rootlink2/")
1332+
.fails()
1333+
.stderr_contains("it is dangerous to operate recursively on")
1334+
.stderr_contains("(same as '/')");
1335+
}
1336+
1337+
/// Test that removing the symlink itself (not the target) still works.
1338+
#[cfg(unix)]
1339+
#[test]
1340+
fn test_preserve_root_symlink_removal_without_trailing_slash() {
1341+
let (at, mut ucmd) = at_and_ucmd!();
1342+
1343+
// Create a symlink pointing to the root directory
1344+
at.symlink_dir("/", "rootlink");
1345+
1346+
// Removing the symlink itself (without trailing slash) should succeed
1347+
// because we're removing the link, not traversing through it
1348+
ucmd.arg("--preserve-root").arg("rootlink").succeeds();
1349+
1350+
assert!(!at.symlink_exists("rootlink"));
1351+
}
1352+
1353+
/// Test that literal "/" is still properly protected.
1354+
#[test]
1355+
fn test_preserve_root_literal_root() {
1356+
new_ucmd!()
1357+
.arg("-rf")
1358+
.arg("--preserve-root")
1359+
.arg("/")
1360+
.fails()
1361+
.stderr_contains("it is dangerous to operate recursively on '/'")
1362+
.stderr_contains("use --no-preserve-root to override this failsafe");
1363+
}

0 commit comments

Comments
 (0)