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