@@ -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 ) ,
@@ -673,6 +725,71 @@ fn remove_dir_recursive(
673725 }
674726}
675727
728+ /// Check if a path resolves to the root directory by comparing device/inode.
729+ /// Returns true if the path is root, false otherwise.
730+ /// On non-Unix systems, falls back to path-based check only.
731+ #[ cfg( unix) ]
732+ fn is_root_path ( path : & Path , options : & Options ) -> bool {
733+ // First check the simple path-based case (e.g., "/")
734+ let path_looks_like_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
735+
736+ // If preserve_root is enabled and we have cached root dev/ino,
737+ // also check if the path resolves to root via symlink or mount
738+ if options. preserve_root {
739+ if let Some ( ref root_dev_ino) = options. root_dev_ino {
740+ // Use symlink_metadata to get the actual target's dev/ino
741+ // after following symlinks (we need to follow the symlink to see
742+ // where it points)
743+ if let Ok ( metadata) = fs:: metadata ( path) {
744+ if root_dev_ino. is_root ( & metadata) {
745+ return true ;
746+ }
747+ } else {
748+ // Fallback: canonicalize the path and check if it resolves to "/"
749+ // This handles cases where metadata retrieval fails but the path
750+ // still resolves to root (e.g., on MacOS in some test scenarios)
751+ if let Ok ( canonical_path) = path. canonicalize ( ) {
752+ if canonical_path. has_root ( ) && canonical_path. parent ( ) . is_none ( ) {
753+ return true ;
754+ }
755+ }
756+ }
757+ }
758+ }
759+
760+ path_looks_like_root
761+ }
762+
763+ #[ cfg( not( unix) ) ]
764+ fn is_root_path ( path : & Path , _options : & Options ) -> bool {
765+ path. has_root ( ) && path. parent ( ) . is_none ( )
766+ }
767+
768+ /// Show appropriate error message for attempting to remove root.
769+ /// Differentiates between literal "/" and paths that resolve to root (e.g., symlinks).
770+ #[ cfg( unix) ]
771+ fn show_preserve_root_error ( path : & Path , _options : & Options ) {
772+ let path_looks_like_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
773+
774+ if path_looks_like_root {
775+ // Path is literally "/"
776+ show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
777+ } else {
778+ // Path resolves to root but isn't literally "/" (e.g., symlink to /)
779+ show_error ! (
780+ "{}" ,
781+ RmError :: DangerousRecursiveOperationSameAsRoot ( path. as_os_str( ) . to_os_string( ) )
782+ ) ;
783+ }
784+ show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
785+ }
786+
787+ #[ cfg( not( unix) ) ]
788+ fn show_preserve_root_error ( _path : & Path , _options : & Options ) {
789+ show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
790+ show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
791+ }
792+
676793fn handle_dir ( path : & Path , options : & Options , progress_bar : Option < & ProgressBar > ) -> bool {
677794 let mut had_err = false ;
678795
@@ -685,14 +802,13 @@ fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>
685802 return true ;
686803 }
687804
688- let is_root = path . has_root ( ) && path. parent ( ) . is_none ( ) ;
805+ let is_root = is_root_path ( path, options ) ;
689806 if options. recursive && ( !is_root || !options. preserve_root ) {
690807 had_err = remove_dir_recursive ( path, options, progress_bar) ;
691808 } else if options. dir && ( !is_root || !options. preserve_root ) {
692809 had_err = remove_dir ( path, options, progress_bar) . bitor ( had_err) ;
693810 } else if options. recursive {
694- show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
695- show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
811+ show_preserve_root_error ( path, options) ;
696812 had_err = true ;
697813 } else {
698814 show_error ! (
0 commit comments