@@ -639,69 +639,68 @@ def onerror(err):
639639 onexc (os .rmdir , path , err )
640640
641641# Version using fd-based APIs to protect against races
642- def _rmtree_safe_fd (topfd , path , onexc ):
642+ def _rmtree_safe_fd (stack , onexc ):
643+ # Each stack item has four elements:
644+ # * func: The first operation to perform: os.lstat, os.close or os.rmdir.
645+ # Walking a directory starts with an os.lstat() to detect symlinks; in
646+ # this case, func is updated before subsequent operations and passed to
647+ # onexc() if an error occurs.
648+ # * dirfd: Open file descriptor, or None if we're processing the top-level
649+ # directory given to rmtree() and the user didn't supply dir_fd.
650+ # * path: Path of file to operate upon. This is passed to onexc() if an
651+ # error occurs.
652+ # * orig_entry: os.DirEntry, or None if we're processing the top-level
653+ # directory given to rmtree(). We used the cached stat() of the entry to
654+ # save a call to os.lstat() when walking subdirectories.
655+ func , dirfd , path , orig_entry = stack .pop ()
656+ name = path if orig_entry is None else orig_entry .name
643657 try :
658+ if func is os .close :
659+ os .close (dirfd )
660+ return
661+ if func is os .rmdir :
662+ os .rmdir (name , dir_fd = dirfd )
663+ return
664+
665+ # Note: To guard against symlink races, we use the standard
666+ # lstat()/open()/fstat() trick.
667+ assert func is os .lstat
668+ if orig_entry is None :
669+ orig_st = os .lstat (name , dir_fd = dirfd )
670+ else :
671+ orig_st = orig_entry .stat (follow_symlinks = False )
672+
673+ func = os .open # For error reporting.
674+ topfd = os .open (name , os .O_RDONLY | os .O_NONBLOCK , dir_fd = dirfd )
675+
676+ func = os .path .islink # For error reporting.
677+ try :
678+ if not os .path .samestat (orig_st , os .fstat (topfd )):
679+ # Symlinks to directories are forbidden, see GH-46010.
680+ raise OSError ("Cannot call rmtree on a symbolic link" )
681+ stack .append ((os .rmdir , dirfd , path , orig_entry ))
682+ finally :
683+ stack .append ((os .close , topfd , path , orig_entry ))
684+
685+ func = os .scandir # For error reporting.
644686 with os .scandir (topfd ) as scandir_it :
645687 entries = list (scandir_it )
646- except OSError as err :
647- err .filename = path
648- onexc (os .scandir , path , err )
649- return
650- for entry in entries :
651- fullname = os .path .join (path , entry .name )
652- try :
653- is_dir = entry .is_dir (follow_symlinks = False )
654- except OSError :
655- is_dir = False
656- else :
657- if is_dir :
658- try :
659- orig_st = entry .stat (follow_symlinks = False )
660- is_dir = stat .S_ISDIR (orig_st .st_mode )
661- except OSError as err :
662- onexc (os .lstat , fullname , err )
663- continue
664- if is_dir :
688+ for entry in entries :
689+ fullname = os .path .join (path , entry .name )
665690 try :
666- dirfd = os .open (entry .name , os .O_RDONLY | os .O_NONBLOCK , dir_fd = topfd )
667- dirfd_closed = False
668- except OSError as err :
669- onexc (os .open , fullname , err )
670- else :
671- try :
672- if os .path .samestat (orig_st , os .fstat (dirfd )):
673- _rmtree_safe_fd (dirfd , fullname , onexc )
674- try :
675- os .close (dirfd )
676- except OSError as err :
677- # close() should not be retried after an error.
678- dirfd_closed = True
679- onexc (os .close , fullname , err )
680- dirfd_closed = True
681- try :
682- os .rmdir (entry .name , dir_fd = topfd )
683- except OSError as err :
684- onexc (os .rmdir , fullname , err )
685- else :
686- try :
687- # This can only happen if someone replaces
688- # a directory with a symlink after the call to
689- # os.scandir or stat.S_ISDIR above.
690- raise OSError ("Cannot call rmtree on a symbolic "
691- "link" )
692- except OSError as err :
693- onexc (os .path .islink , fullname , err )
694- finally :
695- if not dirfd_closed :
696- try :
697- os .close (dirfd )
698- except OSError as err :
699- onexc (os .close , fullname , err )
700- else :
691+ if entry .is_dir (follow_symlinks = False ):
692+ # Traverse into sub-directory.
693+ stack .append ((os .lstat , topfd , fullname , entry ))
694+ continue
695+ except OSError :
696+ pass
701697 try :
702698 os .unlink (entry .name , dir_fd = topfd )
703699 except OSError as err :
704700 onexc (os .unlink , fullname , err )
701+ except OSError as err :
702+ err .filename = path
703+ onexc (func , path , err )
705704
706705_use_fd_functions = ({os .open , os .stat , os .unlink , os .rmdir } <=
707706 os .supports_dir_fd and
@@ -754,41 +753,16 @@ def onexc(*args):
754753 # While the unsafe rmtree works fine on bytes, the fd based does not.
755754 if isinstance (path , bytes ):
756755 path = os .fsdecode (path )
757- # Note: To guard against symlink races, we use the standard
758- # lstat()/open()/fstat() trick.
759- try :
760- orig_st = os .lstat (path , dir_fd = dir_fd )
761- except Exception as err :
762- onexc (os .lstat , path , err )
763- return
756+ stack = [(os .lstat , dir_fd , path , None )]
764757 try :
765- fd = os .open (path , os .O_RDONLY | os .O_NONBLOCK , dir_fd = dir_fd )
766- fd_closed = False
767- except Exception as err :
768- onexc (os .open , path , err )
769- return
770- try :
771- if os .path .samestat (orig_st , os .fstat (fd )):
772- _rmtree_safe_fd (fd , path , onexc )
773- try :
774- os .close (fd )
775- except OSError as err :
776- # close() should not be retried after an error.
777- fd_closed = True
778- onexc (os .close , path , err )
779- fd_closed = True
780- try :
781- os .rmdir (path , dir_fd = dir_fd )
782- except OSError as err :
783- onexc (os .rmdir , path , err )
784- else :
785- try :
786- # symlinks to directories are forbidden, see bug #1669
787- raise OSError ("Cannot call rmtree on a symbolic link" )
788- except OSError as err :
789- onexc (os .path .islink , path , err )
758+ while stack :
759+ _rmtree_safe_fd (stack , onexc )
790760 finally :
791- if not fd_closed :
761+ # Close any file descriptors still on the stack.
762+ while stack :
763+ func , fd , path , entry = stack .pop ()
764+ if func is not os .close :
765+ continue
792766 try :
793767 os .close (fd )
794768 except OSError as err :
0 commit comments