@@ -15,6 +15,8 @@ use std::ops::BitOr;
1515#[ cfg( unix) ]
1616use std:: os:: unix:: ffi:: OsStrExt ;
1717#[ cfg( unix) ]
18+ use std:: os:: unix:: fs:: MetadataExt ;
19+ #[ cfg( unix) ]
1820use std:: os:: unix:: fs:: PermissionsExt ;
1921use std:: path:: MAIN_SEPARATOR ;
2022use std:: path:: { Path , PathBuf } ;
@@ -29,6 +31,32 @@ mod platform;
2931#[ cfg( all( unix, not( target_os = "redox" ) ) ) ]
3032use platform:: { safe_remove_dir_recursive, safe_remove_empty_dir, safe_remove_file} ;
3133
34+ /// Cached device and inode numbers for the root directory.
35+ /// Used for --preserve-root to detect when a path resolves to "/".
36+ #[ cfg( unix) ]
37+ #[ derive( Debug , Clone , Copy ) ]
38+ pub struct RootDevIno {
39+ pub dev : u64 ,
40+ pub ino : u64 ,
41+ }
42+
43+ #[ cfg( unix) ]
44+ impl RootDevIno {
45+ /// Get the device and inode numbers for "/".
46+ /// Returns None if lstat("/") fails.
47+ pub fn new ( ) -> Option < Self > {
48+ fs:: symlink_metadata ( "/" ) . ok ( ) . map ( |meta| Self {
49+ dev : meta. dev ( ) ,
50+ ino : meta. ino ( ) ,
51+ } )
52+ }
53+
54+ /// Check if the given metadata matches the root device/inode.
55+ pub fn is_root ( & self , meta : & Metadata ) -> bool {
56+ meta. dev ( ) == self . dev && meta. ino ( ) == self . ino
57+ }
58+ }
59+
3260#[ derive( Debug , Error ) ]
3361enum RmError {
3462 #[ error( "{}" , translate!( "rm-error-missing-operand" , "util_name" => uucore:: execution_phrase( ) ) ) ]
@@ -41,6 +69,9 @@ enum RmError {
4169 CannotRemoveIsDirectory ( OsString ) ,
4270 #[ error( "{}" , translate!( "rm-error-dangerous-recursive-operation" ) ) ]
4371 DangerousRecursiveOperation ,
72+ #[ cfg( unix) ]
73+ #[ error( "{}" , translate!( "rm-error-dangerous-recursive-operation-same-as-root" , "path" => _0. to_string_lossy( ) ) ) ]
74+ DangerousRecursiveOperationSameAsRoot ( OsString ) ,
4475 #[ error( "{}" , translate!( "rm-error-use-no-preserve-root" ) ) ]
4576 UseNoPreserveRoot ,
4677 #[ error( "{}" , translate!( "rm-error-refusing-to-remove-directory" , "path" => _0. quote( ) ) ) ]
@@ -155,6 +186,10 @@ pub struct Options {
155186 pub one_fs : bool ,
156187 /// `--preserve-root`/`--no-preserve-root`
157188 pub preserve_root : bool ,
189+ /// Cached device/inode for "/" when preserve_root is enabled.
190+ /// Used to detect symlinks or paths that resolve to root.
191+ #[ cfg( unix) ]
192+ pub root_dev_ino : Option < RootDevIno > ,
158193 /// `-r`, `--recursive`
159194 pub recursive : bool ,
160195 /// `-d`, `--dir`
@@ -176,6 +211,8 @@ impl Default for Options {
176211 interactive : InteractiveMode :: PromptProtected ,
177212 one_fs : false ,
178213 preserve_root : true ,
214+ #[ cfg( unix) ]
215+ root_dev_ino : None ,
179216 recursive : false ,
180217 dir : false ,
181218 verbose : false ,
@@ -229,6 +266,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
229266 } )
230267 } ;
231268
269+ let preserve_root = !matches. get_flag ( OPT_NO_PRESERVE_ROOT ) ;
270+ let recursive = matches. get_flag ( OPT_RECURSIVE ) ;
271+
272+ // Cache the device/inode of "/" at startup when preserve_root is enabled
273+ // and we're doing recursive operations. This allows us to detect symlinks
274+ // or paths that resolve to root by comparing device/inode numbers.
275+ #[ cfg( unix) ]
276+ let root_dev_ino = if preserve_root && recursive {
277+ RootDevIno :: new ( )
278+ } else {
279+ None
280+ } ;
281+
232282 let options = Options {
233283 force : force_flag,
234284 interactive : {
@@ -245,8 +295,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
245295 }
246296 } ,
247297 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 ) ,
298+ preserve_root,
299+ #[ cfg( unix) ]
300+ root_dev_ino,
301+ recursive,
250302 dir : matches. get_flag ( OPT_DIR ) ,
251303 verbose : matches. get_flag ( OPT_VERBOSE ) ,
252304 progress : matches. get_flag ( OPT_PROGRESS ) ,
@@ -487,6 +539,25 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool {
487539 for filename in files {
488540 let file = Path :: new ( filename) ;
489541
542+ // Check if the path (potentially with trailing slash) resolves to root
543+ // This needs to happen before symlink_metadata to catch cases like "rootlink/"
544+ // where rootlink is a symlink to root.
545+ #[ cfg( unix) ]
546+ {
547+ // When a path has a trailing slash and points to a symlink to a directory,
548+ // we should check if it resolves to root before processing.
549+ if uucore:: fs:: path_ends_with_terminator ( file)
550+ && options. recursive
551+ && options. preserve_root
552+ {
553+ if is_root_path ( file, options) {
554+ show_preserve_root_error ( file, options) ;
555+ had_err = true ;
556+ continue ;
557+ }
558+ }
559+ }
560+
490561 had_err = match file. symlink_metadata ( ) {
491562 Ok ( metadata) => {
492563 // Create progress bar on first successful file metadata read
@@ -673,6 +744,70 @@ fn remove_dir_recursive(
673744 }
674745}
675746
747+ /// Check if a path resolves to the root directory by comparing device/inode.
748+ /// Returns true if the path is root, false otherwise.
749+ /// On non-Unix systems, falls back to path-based check only.
750+ #[ cfg( unix) ]
751+ fn is_root_path ( path : & Path , options : & Options ) -> bool {
752+ // First check the simple path-based case (e.g., "/")
753+ let path_looks_like_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
754+
755+ // If preserve_root is enabled and we have cached root dev/ino,
756+ // also check if the path resolves to root via symlink or mount
757+ if options. preserve_root {
758+ if let Some ( ref root_dev_ino) = options. root_dev_ino {
759+ // Use fs::metadata to get the target's dev/ino after following ALL symlinks
760+ // This should handle nested symlinks automatically
761+ if let Ok ( metadata) = fs:: metadata ( path) {
762+ if root_dev_ino. is_root ( & metadata) {
763+ return true ;
764+ }
765+ }
766+
767+ // Fallback: canonicalize the path and check if it resolves to "/"
768+ // This handles cases where metadata retrieval fails but the path
769+ // still resolves to root (e.g., on MacOS in some test scenarios)
770+ if let Ok ( canonical_path) = path. canonicalize ( ) {
771+ if canonical_path. has_root ( ) && canonical_path. parent ( ) . is_none ( ) {
772+ return true ;
773+ }
774+ }
775+ }
776+ }
777+
778+ path_looks_like_root
779+ }
780+
781+ #[ cfg( not( unix) ) ]
782+ fn is_root_path ( path : & Path , _options : & Options ) -> bool {
783+ path. has_root ( ) && path. parent ( ) . is_none ( )
784+ }
785+
786+ /// Show appropriate error message for attempting to remove root.
787+ /// Differentiates between literal "/" and paths that resolve to root (e.g., symlinks).
788+ #[ cfg( unix) ]
789+ fn show_preserve_root_error ( path : & Path , _options : & Options ) {
790+ let path_looks_like_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
791+
792+ if path_looks_like_root {
793+ // Path is literally "/"
794+ show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
795+ } else {
796+ // Path resolves to root but isn't literally "/" (e.g., symlink to /)
797+ show_error ! (
798+ "{}" ,
799+ RmError :: DangerousRecursiveOperationSameAsRoot ( path. as_os_str( ) . to_os_string( ) )
800+ ) ;
801+ }
802+ show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
803+ }
804+
805+ #[ cfg( not( unix) ) ]
806+ fn show_preserve_root_error ( _path : & Path , _options : & Options ) {
807+ show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
808+ show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
809+ }
810+
676811fn handle_dir ( path : & Path , options : & Options , progress_bar : Option < & ProgressBar > ) -> bool {
677812 let mut had_err = false ;
678813
@@ -685,14 +820,13 @@ fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>
685820 return true ;
686821 }
687822
688- let is_root = path . has_root ( ) && path. parent ( ) . is_none ( ) ;
823+ let is_root = is_root_path ( path, options ) ;
689824 if options. recursive && ( !is_root || !options. preserve_root ) {
690825 had_err = remove_dir_recursive ( path, options, progress_bar) ;
691826 } else if options. dir && ( !is_root || !options. preserve_root ) {
692827 had_err = remove_dir ( path, options, progress_bar) . bitor ( had_err) ;
693828 } else if options. recursive {
694- show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
695- show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
829+ show_preserve_root_error ( path, options) ;
696830 had_err = true ;
697831 } else {
698832 show_error ! (
0 commit comments