@@ -519,9 +519,18 @@ impl Chmoder {
519519 . handle_symlink_during_safe_recursion ( & entry_path, dir_fd, & entry_name)
520520 . and ( r) ;
521521 } else {
522- // For regular files and directories, chmod them
522+ // For regular files and directories, chmod them.
523+ // Always use NoFollow: we already confirmed via stat that the entry
524+ // is not a symlink, so this prevents TOCTOU races where an attacker
525+ // replaces the entry with a symlink between stat and chmod.
523526 r = self
524- . safe_chmod_file ( & entry_path, dir_fd, & entry_name, meta. mode ( ) & 0o7777 )
527+ . safe_chmod_file (
528+ & entry_path,
529+ dir_fd,
530+ & entry_name,
531+ meta. mode ( ) & 0o7777 ,
532+ SymlinkBehavior :: NoFollow ,
533+ )
525534 . and ( r) ;
526535
527536 // Recurse into subdirectories using the existing directory fd
@@ -555,13 +564,23 @@ impl Chmoder {
555564 // During recursion, determine behavior based on traversal mode
556565 match self . traverse_symlinks {
557566 TraverseSymlinks :: All => {
558- // Follow all symlinks during recursion
567+ // Follow all symlinks during recursion.
568+ // NOTE: This path-based stat + Follow is only reachable when the user
569+ // explicitly specifies -L (--dereference). In that case the user has
570+ // consciously opted in to following symlinks, so a path-based stat
571+ // followed by Follow is the intended behavior and not a TOCTOU concern.
559572 // Check if the symlink target is a directory, but handle dangling symlinks gracefully
560573 match fs:: metadata ( path) {
561574 Ok ( meta) if meta. is_dir ( ) => self . walk_dir_with_context ( path, false ) ,
562575 Ok ( meta) => {
563576 // It's a file symlink, chmod it using safe traversal
564- self . safe_chmod_file ( path, dir_fd, entry_name, meta. mode ( ) & 0o7777 )
577+ self . safe_chmod_file (
578+ path,
579+ dir_fd,
580+ entry_name,
581+ meta. mode ( ) & 0o7777 ,
582+ self . dereference . into ( ) ,
583+ )
565584 }
566585 Err ( _) => {
567586 // Dangling symlink, chmod it without dereferencing
@@ -584,13 +603,13 @@ impl Chmoder {
584603 dir_fd : & DirFd ,
585604 entry_name : & std:: ffi:: OsStr ,
586605 current_mode : u32 ,
606+ symlink_behavior : SymlinkBehavior ,
587607 ) -> UResult < ( ) > {
588608 // Calculate the new mode using the helper method
589609 let ( new_mode, _) = self . calculate_new_mode ( current_mode, file_path. is_dir ( ) ) ?;
590610
591611 // Use safe traversal to change the mode
592- let follow_symlinks = self . dereference ;
593- if let Err ( _e) = dir_fd. chmod_at ( entry_name, new_mode, follow_symlinks. into ( ) ) {
612+ if let Err ( _e) = dir_fd. chmod_at ( entry_name, new_mode, symlink_behavior) {
594613 if self . verbose {
595614 println ! (
596615 "failed to change mode of {} to {new_mode:o}" ,
0 commit comments