33// For the full copyright and license information, please view the LICENSE
44// file that was distributed with this source code.
55
6- // spell-checker:ignore (path) eacces inacc rm-r4
6+ // spell-checker:ignore (path) eacces inacc rm-r4 unlinkat fstatat
77
88use clap:: builder:: { PossibleValue , ValueParser } ;
99use clap:: { Arg , ArgAction , Command , parser:: ValueSource } ;
@@ -21,6 +21,8 @@ use thiserror::Error;
2121use uucore:: display:: Quotable ;
2222use uucore:: error:: { FromIo , UError , UResult } ;
2323use uucore:: parser:: shortcut_value_parser:: ShortcutValueParser ;
24+ #[ cfg( target_os = "linux" ) ]
25+ use uucore:: safe_traversal:: DirFd ;
2426use uucore:: translate;
2527
2628use uucore:: { format_usage, os_str_as_bytes, prompt_yes, show_error} ;
@@ -428,6 +430,140 @@ fn is_writable(_path: &Path) -> bool {
428430 true
429431}
430432
433+ #[ cfg( target_os = "linux" ) ]
434+ fn safe_remove_dir_recursive ( path : & Path , options : & Options ) -> bool {
435+ // Try to open the directory using DirFd for secure traversal
436+ let dir_fd = match DirFd :: open ( path) {
437+ Ok ( fd) => fd,
438+ Err ( e) => {
439+ show_error ! (
440+ "{}" ,
441+ e. map_err_context( || translate!( "rm-error-cannot-remove" , "file" => path. quote( ) ) )
442+ ) ;
443+ return true ;
444+ }
445+ } ;
446+
447+ let error = safe_remove_dir_recursive_impl ( path, & dir_fd, options) ;
448+
449+ // After processing all children, remove the directory itself
450+ if error {
451+ error
452+ } else {
453+ // Ask user permission if needed
454+ if options. interactive == InteractiveMode :: Always && !prompt_dir ( path, options) {
455+ return false ;
456+ }
457+
458+ // Use regular fs::remove_dir for the root since we can't unlinkat ourselves
459+ match fs:: remove_dir ( path) {
460+ Ok ( _) => false ,
461+ Err ( e) => {
462+ let e = e. map_err_context (
463+ || translate ! ( "rm-error-cannot-remove" , "file" => path. quote( ) ) ,
464+ ) ;
465+ show_error ! ( "{e}" ) ;
466+ true
467+ }
468+ }
469+ }
470+ }
471+
472+ #[ cfg( target_os = "linux" ) ]
473+ fn safe_remove_dir_recursive_impl ( path : & Path , dir_fd : & DirFd , options : & Options ) -> bool {
474+ // Check if we should descend into this directory
475+ if options. interactive == InteractiveMode :: Always
476+ && !is_dir_empty ( path)
477+ && !prompt_descend ( path)
478+ {
479+ return false ;
480+ }
481+
482+ // Read directory entries using safe traversal
483+ let entries = match dir_fd. read_dir ( ) {
484+ Ok ( entries) => entries,
485+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied => {
486+ // This is not considered an error - just like the original
487+ return false ;
488+ }
489+ Err ( e) => {
490+ show_error ! (
491+ "{}" ,
492+ e. map_err_context( || translate!( "rm-error-cannot-remove" , "file" => path. quote( ) ) )
493+ ) ;
494+ return true ;
495+ }
496+ } ;
497+
498+ let mut error = false ;
499+
500+ // Process each entry
501+ for entry_name in entries {
502+ let entry_path = path. join ( & entry_name) ;
503+
504+ // Get metadata for the entry using fstatat
505+ let entry_stat = match dir_fd. stat_at ( & entry_name, false ) {
506+ Ok ( stat) => stat,
507+ Err ( e) => {
508+ let e = e. map_err_context (
509+ || translate ! ( "rm-error-cannot-remove" , "file" => entry_path. quote( ) ) ,
510+ ) ;
511+ show_error ! ( "{e}" ) ;
512+ error = true ;
513+ continue ;
514+ }
515+ } ;
516+
517+ // Check if it's a directory
518+ let is_dir = ( entry_stat. st_mode & libc:: S_IFMT ) == libc:: S_IFDIR ;
519+
520+ if is_dir {
521+ // Recursively remove directory
522+ let subdir_fd = match dir_fd. open_subdir ( & entry_name) {
523+ Ok ( fd) => fd,
524+ Err ( e) => {
525+ let e = e. map_err_context (
526+ || translate ! ( "rm-error-cannot-remove" , "file" => entry_path. quote( ) ) ,
527+ ) ;
528+ show_error ! ( "{e}" ) ;
529+ error = true ;
530+ continue ;
531+ }
532+ } ;
533+
534+ let child_error = safe_remove_dir_recursive_impl ( & entry_path, & subdir_fd, options) ;
535+ error = error || child_error;
536+
537+ // Try to remove the directory (even if there were some child errors)
538+ // Ask user permission if needed
539+ if options. interactive == InteractiveMode :: Always && !prompt_dir ( & entry_path, options) {
540+ continue ;
541+ }
542+
543+ if let Err ( e) = dir_fd. unlink_at ( & entry_name, true ) {
544+ let e = e. map_err_context (
545+ || translate ! ( "rm-error-cannot-remove" , "file" => entry_path. quote( ) ) ,
546+ ) ;
547+ show_error ! ( "{e}" ) ;
548+ error = true ;
549+ }
550+ } else {
551+ // Remove file - check if user wants to remove it first
552+ if prompt_file ( & entry_path, options) {
553+ if let Err ( e) = dir_fd. unlink_at ( & entry_name, false ) {
554+ let e = e. map_err_context (
555+ || translate ! ( "rm-error-cannot-remove" , "file" => entry_path. quote( ) ) ,
556+ ) ;
557+ show_error ! ( "{e}" ) ;
558+ error = true ;
559+ }
560+ }
561+ }
562+ }
563+
564+ error
565+ }
566+
431567/// Recursively remove the directory tree rooted at the given path.
432568///
433569/// If `path` is a file or a symbolic link, just remove it. If it is a
@@ -454,25 +590,30 @@ fn remove_dir_recursive(path: &Path, options: &Options) -> bool {
454590 return false ;
455591 }
456592
457- // Special case: if we cannot access the metadata because the
458- // filename is too long, fall back to try
459- // `fs::remove_dir_all()`.
460- //
461- // TODO This is a temporary bandage; we shouldn't need to do this
462- // at all. Instead of using the full path like "x/y/z", which
463- // causes a `InvalidFilename` error when trying to access the file
464- // metadata, we should be able to use just the last part of the
465- // path, "z", and know that it is relative to the parent, "x/y".
466- if let Some ( s) = path. to_str ( ) {
467- if s. len ( ) > 1000 {
468- match fs:: remove_dir_all ( path) {
469- Ok ( _) => return false ,
470- Err ( e) => {
471- let e = e. map_err_context (
472- || translate ! ( "rm-error-cannot-remove" , "file" => path. quote( ) ) ,
473- ) ;
474- show_error ! ( "{e}" ) ;
475- return true ;
593+ // Use secure traversal on Linux for long paths
594+ #[ cfg( target_os = "linux" ) ]
595+ {
596+ if let Some ( s) = path. to_str ( ) {
597+ if s. len ( ) > 1000 {
598+ return safe_remove_dir_recursive ( path, options) ;
599+ }
600+ }
601+ }
602+
603+ // Fallback for non-Linux or shorter paths
604+ #[ cfg( not( target_os = "linux" ) ) ]
605+ {
606+ if let Some ( s) = path. to_str ( ) {
607+ if s. len ( ) > 1000 {
608+ match fs:: remove_dir_all ( path) {
609+ Ok ( _) => return false ,
610+ Err ( e) => {
611+ let e = e. map_err_context (
612+ || translate ! ( "rm-error-cannot-remove" , "file" => path. quote( ) ) ,
613+ ) ;
614+ show_error ! ( "{e}" ) ;
615+ return true ;
616+ }
476617 }
477618 }
478619 }
0 commit comments