diff options
author | Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> | 2024-05-29 20:41:03 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-29 20:41:03 (GMT) |
commit | 061abf8e4c1a0a920067aa73b0e41f40c89e340b (patch) | |
tree | 9a96ab7b452d756255902c34e235ff2bc61ff579 /Lib | |
parent | c3cfc04a736111cb61883cef08469ea46f86d9ce (diff) | |
download | cpython-061abf8e4c1a0a920067aa73b0e41f40c89e340b.zip cpython-061abf8e4c1a0a920067aa73b0e41f40c89e340b.tar.gz cpython-061abf8e4c1a0a920067aa73b0e41f40c89e340b.tar.bz2 |
[3.13] GH-89727: Partially fix `shutil.rmtree()` recursion error on deep trees (GH-119634) (#119748)
GH-89727: Partially fix `shutil.rmtree()` recursion error on deep trees (GH-119634)
Make `shutil._rmtree_unsafe()` call `os.walk()`, which is implemented
without recursion.
`shutil._rmtree_safe_fd()` is not affected and can still raise a recursion
error.
(cherry picked from commit a150679f90c6e3f017bd75cac3b8f727063cc4aa)
Co-authored-by: Barney Gale <barney.gale@gmail.com>
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/os.py | 9 | ||||
-rw-r--r-- | Lib/shutil.py | 38 | ||||
-rw-r--r-- | Lib/test/test_shutil.py | 11 |
3 files changed, 30 insertions, 28 deletions
@@ -281,6 +281,10 @@ def renames(old, new): __all__.extend(["makedirs", "removedirs", "renames"]) +# Private sentinel that makes walk() classify all symlinks and junctions as +# regular files. +_walk_symlinks_as_files = object() + def walk(top, topdown=True, onerror=None, followlinks=False): """Directory tree generator. @@ -382,7 +386,10 @@ def walk(top, topdown=True, onerror=None, followlinks=False): break try: - is_dir = entry.is_dir() + if followlinks is _walk_symlinks_as_files: + is_dir = entry.is_dir(follow_symlinks=False) and not entry.is_junction() + else: + is_dir = entry.is_dir() except OSError: # If is_dir() raises an OSError, consider the entry not to # be a directory, same behaviour as os.path.isdir(). diff --git a/Lib/shutil.py b/Lib/shutil.py index c9b4da3..03a9d75 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -606,37 +606,21 @@ else: # version vulnerable to race conditions def _rmtree_unsafe(path, onexc): - try: - with os.scandir(path) as scandir_it: - entries = list(scandir_it) - except FileNotFoundError: - return - except OSError as err: - onexc(os.scandir, path, err) - entries = [] - for entry in entries: - fullname = entry.path - try: - is_dir = entry.is_dir(follow_symlinks=False) - except FileNotFoundError: - continue - except OSError: - is_dir = False - - if is_dir and not entry.is_junction(): + def onerror(err): + if not isinstance(err, FileNotFoundError): + onexc(os.scandir, err.filename, err) + results = os.walk(path, topdown=False, onerror=onerror, followlinks=os._walk_symlinks_as_files) + for dirpath, dirnames, filenames in results: + for name in dirnames: + fullname = os.path.join(dirpath, name) try: - if entry.is_symlink(): - # This can only happen if someone replaces - # a directory with a symlink after the call to - # os.scandir or entry.is_dir above. - raise OSError("Cannot call rmtree on a symbolic link") + os.rmdir(fullname) except FileNotFoundError: continue except OSError as err: - onexc(os.path.islink, fullname, err) - continue - _rmtree_unsafe(fullname, onexc) - else: + onexc(os.rmdir, fullname, err) + for name in filenames: + fullname = os.path.join(dirpath, name) try: os.unlink(fullname) except FileNotFoundError: diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index df9e7a6..01f1390 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -741,6 +741,17 @@ class TestRmTree(BaseTest, unittest.TestCase): shutil.rmtree(TESTFN) raise + @unittest.skipIf(shutil._use_fd_functions, "fd-based functions remain unfixed (GH-89727)") + def test_rmtree_above_recursion_limit(self): + recursion_limit = 40 + # directory_depth > recursion_limit + directory_depth = recursion_limit + 10 + base = os.path.join(TESTFN, *(['d'] * directory_depth)) + os.makedirs(base) + + with support.infinite_recursion(recursion_limit): + shutil.rmtree(TESTFN) + class TestCopyTree(BaseTest, unittest.TestCase): |