@@ -456,35 +456,69 @@ impl Chmoder {
456
456
457
457
#[ cfg( target_os = "linux" ) ]
458
458
fn walk_dir_with_context ( & self , file_path : & Path , is_command_line_arg : bool ) -> UResult < ( ) > {
459
- let mut r = self . chmod_file ( file_path) ;
460
-
461
459
// Determine whether to traverse symlinks based on context and traversal mode
462
460
let should_follow_symlink = match self . traverse_symlinks {
463
461
TraverseSymlinks :: All => true ,
464
462
TraverseSymlinks :: First => is_command_line_arg, // Only follow symlinks that are command line args
465
463
TraverseSymlinks :: None => false ,
466
464
} ;
467
465
468
- // If the path is a directory (or we should follow symlinks), recurse into it using safe traversal
466
+ // Use safe syscalls for the root directory as well to prevent TOCTOU attacks
469
467
if ( !file_path. is_symlink ( ) || should_follow_symlink) && file_path. is_dir ( ) {
468
+ // For directories, use safe traversal from the start
470
469
match DirFd :: open ( file_path) {
471
470
Ok ( dir_fd) => {
472
- r = self . safe_traverse_dir ( & dir_fd, file_path) . and ( r) ;
471
+ // First chmod the directory itself using fchmod (safe)
472
+ let dir_result = self . safe_chmod_dir ( & dir_fd, file_path) ;
473
+ // Then traverse its contents
474
+ let traverse_result = self . safe_traverse_dir ( & dir_fd, file_path) ;
475
+ dir_result. and ( traverse_result)
473
476
}
474
477
Err ( err) => {
475
478
// Handle permission denied errors with proper file path context
476
479
if err. kind ( ) == std:: io:: ErrorKind :: PermissionDenied {
477
- r = r. and ( Err ( ChmodError :: PermissionDenied (
478
- file_path. to_string_lossy ( ) . to_string ( ) ,
480
+ Err (
481
+ ChmodError :: PermissionDenied ( file_path. to_string_lossy ( ) . to_string ( ) )
482
+ . into ( ) ,
479
483
)
480
- . into ( ) ) ) ;
481
484
} else {
482
- r = r . and ( Err ( err. into ( ) ) ) ;
485
+ Err ( err. into ( ) )
483
486
}
484
487
}
485
488
}
489
+ } else {
490
+ // For non-directories (files, symlinks), use the regular chmod_file method
491
+ self . chmod_file ( file_path)
486
492
}
487
- r
493
+ }
494
+
495
+ #[ cfg( target_os = "linux" ) ]
496
+ fn safe_chmod_dir ( & self , dir_fd : & DirFd , file_path : & Path ) -> UResult < ( ) > {
497
+ // Get the current mode using fstat (safe)
498
+ let stat = dir_fd
499
+ . fstat ( )
500
+ . map_err ( |_e| ChmodError :: PermissionDenied ( file_path. to_string_lossy ( ) . to_string ( ) ) ) ?;
501
+
502
+ let current_mode = stat. st_mode ;
503
+ let ( new_mode, _) = self . calculate_new_mode ( current_mode, true ) ?; // true = is_dir
504
+
505
+ // Use fchmod (safe) to change the directory's mode
506
+ if let Err ( _e) = dir_fd. fchmod ( new_mode) {
507
+ if self . verbose {
508
+ println ! (
509
+ "failed to change mode of {} to {:o}" ,
510
+ file_path. quote( ) ,
511
+ new_mode
512
+ ) ;
513
+ }
514
+ return Err (
515
+ ChmodError :: PermissionDenied ( file_path. to_string_lossy ( ) . to_string ( ) ) . into ( ) ,
516
+ ) ;
517
+ }
518
+
519
+ // Report the change if verbose
520
+ self . report_permission_change ( file_path, current_mode, new_mode) ;
521
+ Ok ( ( ) )
488
522
}
489
523
490
524
#[ cfg( target_os = "linux" ) ]
0 commit comments