@@ -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( target_os = "linux" ) ]
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. to_string_lossy( ) ) ) ]
@@ -153,6 +184,10 @@ pub struct Options {
153184 pub one_fs : bool ,
154185 /// `--preserve-root`/`--no-preserve-root`
155186 pub preserve_root : bool ,
187+ /// Cached device/inode for "/" when preserve_root is enabled.
188+ /// Used to detect symlinks or paths that resolve to root.
189+ #[ cfg( unix) ]
190+ pub root_dev_ino : Option < RootDevIno > ,
156191 /// `-r`, `--recursive`
157192 pub recursive : bool ,
158193 /// `-d`, `--dir`
@@ -174,6 +209,8 @@ impl Default for Options {
174209 interactive : InteractiveMode :: PromptProtected ,
175210 one_fs : false ,
176211 preserve_root : true ,
212+ #[ cfg( unix) ]
213+ root_dev_ino : None ,
177214 recursive : false ,
178215 dir : false ,
179216 verbose : false ,
@@ -226,6 +263,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
226263 } )
227264 } ;
228265
266+ let preserve_root = !matches. get_flag ( OPT_NO_PRESERVE_ROOT ) ;
267+ let recursive = matches. get_flag ( OPT_RECURSIVE ) ;
268+
269+ // Cache the device/inode of "/" at startup when preserve_root is enabled
270+ // and we're doing recursive operations. This allows us to detect symlinks
271+ // or paths that resolve to root by comparing device/inode numbers.
272+ #[ cfg( unix) ]
273+ let root_dev_ino = if preserve_root && recursive {
274+ RootDevIno :: new ( )
275+ } else {
276+ None
277+ } ;
278+
229279 let options = Options {
230280 force : force_flag,
231281 interactive : {
@@ -242,8 +292,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
242292 }
243293 } ,
244294 one_fs : matches. get_flag ( OPT_ONE_FILE_SYSTEM ) ,
245- preserve_root : !matches. get_flag ( OPT_NO_PRESERVE_ROOT ) ,
246- recursive : matches. get_flag ( OPT_RECURSIVE ) ,
295+ preserve_root,
296+ #[ cfg( unix) ]
297+ root_dev_ino,
298+ recursive,
247299 dir : matches. get_flag ( OPT_DIR ) ,
248300 verbose : matches. get_flag ( OPT_VERBOSE ) ,
249301 progress : matches. get_flag ( OPT_PROGRESS ) ,
@@ -684,6 +736,62 @@ fn remove_dir_recursive(
684736 }
685737}
686738
739+ /// Check if a path resolves to the root directory by comparing device/inode.
740+ /// Returns true if the path is root, false otherwise.
741+ /// On non-Unix systems, falls back to path-based check only.
742+ #[ cfg( unix) ]
743+ fn is_root_path ( path : & Path , options : & Options ) -> bool {
744+ // First check the simple path-based case (e.g., "/")
745+ let path_looks_like_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
746+
747+ // If preserve_root is enabled and we have cached root dev/ino,
748+ // also check if the path resolves to root via symlink or mount
749+ if options. preserve_root {
750+ if let Some ( ref root_dev_ino) = options. root_dev_ino {
751+ // Use symlink_metadata to get the actual target's dev/ino
752+ // after following symlinks (we need to follow the symlink to see
753+ // where it points)
754+ if let Ok ( metadata) = fs:: metadata ( path) {
755+ if root_dev_ino. is_root ( & metadata) {
756+ return true ;
757+ }
758+ }
759+ }
760+ }
761+
762+ path_looks_like_root
763+ }
764+
765+ #[ cfg( not( unix) ) ]
766+ fn is_root_path ( path : & Path , _options : & Options ) -> bool {
767+ path. has_root ( ) && path. parent ( ) . is_none ( )
768+ }
769+
770+ /// Show appropriate error message for attempting to remove root.
771+ /// Differentiates between literal "/" and paths that resolve to root (e.g., symlinks).
772+ #[ cfg( unix) ]
773+ fn show_preserve_root_error ( path : & Path , _options : & Options ) {
774+ let path_looks_like_root = path. has_root ( ) && path. parent ( ) . is_none ( ) ;
775+
776+ if path_looks_like_root {
777+ // Path is literally "/"
778+ show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
779+ } else {
780+ // Path resolves to root but isn't literally "/" (e.g., symlink to /)
781+ show_error ! (
782+ "{}" ,
783+ RmError :: DangerousRecursiveOperationSameAsRoot ( path. as_os_str( ) . to_os_string( ) )
784+ ) ;
785+ }
786+ show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
787+ }
788+
789+ #[ cfg( not( unix) ) ]
790+ fn show_preserve_root_error ( _path : & Path , _options : & Options ) {
791+ show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
792+ show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
793+ }
794+
687795fn handle_dir ( path : & Path , options : & Options , progress_bar : Option < & ProgressBar > ) -> bool {
688796 let mut had_err = false ;
689797
@@ -696,14 +804,13 @@ fn handle_dir(path: &Path, options: &Options, progress_bar: Option<&ProgressBar>
696804 return true ;
697805 }
698806
699- let is_root = path . has_root ( ) && path. parent ( ) . is_none ( ) ;
807+ let is_root = is_root_path ( path, options ) ;
700808 if options. recursive && ( !is_root || !options. preserve_root ) {
701809 had_err = remove_dir_recursive ( path, options, progress_bar) ;
702810 } else if options. dir && ( !is_root || !options. preserve_root ) {
703811 had_err = remove_dir ( path, options, progress_bar) . bitor ( had_err) ;
704812 } else if options. recursive {
705- show_error ! ( "{}" , RmError :: DangerousRecursiveOperation ) ;
706- show_error ! ( "{}" , RmError :: UseNoPreserveRoot ) ;
813+ show_preserve_root_error ( path, options) ;
707814 had_err = true ;
708815 } else {
709816 show_error ! (
0 commit comments