Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.12] GH-89727: Partially fix shutil.rmtree() recursion error on deep trees (GH-119634) #119749

Merged
merged 2 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,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.

Expand Down Expand Up @@ -380,7 +384,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().
Expand Down
33 changes: 10 additions & 23 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,31 +617,18 @@ def _rmtree_islink(path):

# version vulnerable to race conditions
def _rmtree_unsafe(path, onexc):
try:
with os.scandir(path) as scandir_it:
entries = list(scandir_it)
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 OSError:
is_dir = False

if is_dir and not entry.is_junction():
def onerror(err):
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 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 OSError as err:
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,17 @@ def test_rmtree_on_named_pipe(self):
shutil.rmtree(TESTFN)
self.assertFalse(os.path.exists(TESTFN))

@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):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Partially fix issue with :func:`shutil.rmtree` where a :exc:`RecursionError`
is raised on deep directory trees. A recursion error is no longer raised
when :data:`!rmtree.avoids_symlink_attacks` is false.
Loading