@@ -456,35 +456,70 @@ impl Chmoder {
456456
457457 #[ cfg( target_os = "linux" ) ]
458458 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-
461459 // Determine whether to traverse symlinks based on context and traversal mode
462460 let should_follow_symlink = match self . traverse_symlinks {
463461 TraverseSymlinks :: All => true ,
464462 TraverseSymlinks :: First => is_command_line_arg, // Only follow symlinks that are command line args
465463 TraverseSymlinks :: None => false ,
466464 } ;
467465
468- // If the path is a directory (or we should follow symlinks), recurse into it using safe traversal
469- if ( !file_path. is_symlink ( ) || should_follow_symlink) && file_path. is_dir ( ) {
466+ // Use safe syscalls for the root directory as well to prevent TOCTOU attacks
467+ if file_path. is_dir ( ) && ( !file_path. is_symlink ( ) || should_follow_symlink) {
468+ // For directories, use safe traversal from the start
470469 match DirFd :: open ( file_path) {
471470 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)
473476 }
474477 Err ( err) => {
475478 // Handle permission denied errors with proper file path context
476479 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 ( ) ,
479483 )
480- . into ( ) ) ) ;
481484 } else {
482- r = r . and ( Err ( err. into ( ) ) ) ;
485+ Err ( err. into ( ) )
483486 }
484487 }
485488 }
489+ } else {
490+ // For non-directories (files, symlinks), use the regular chmod_file method
491+ self . chmod_file ( file_path)
486492 }
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+ // Calculate the new mode using the helper method
504+ let ( new_mode, _) = self . calculate_new_mode ( current_mode, true ) ?; // true = is_dir
505+
506+ // Use fchmod (safe) to change the directory's mode
507+ if let Err ( _e) = dir_fd. fchmod ( new_mode) {
508+ if self . verbose {
509+ println ! (
510+ "failed to change mode of {} to {:o}" ,
511+ file_path. quote( ) ,
512+ new_mode
513+ ) ;
514+ }
515+ return Err (
516+ ChmodError :: PermissionDenied ( file_path. to_string_lossy ( ) . to_string ( ) ) . into ( ) ,
517+ ) ;
518+ }
519+
520+ // Report the change if verbose
521+ self . report_permission_change ( file_path, current_mode, new_mode) ;
522+ Ok ( ( ) )
488523 }
489524
490525 #[ cfg( target_os = "linux" ) ]
0 commit comments