Skip to content

Commit 54ca4c6

Browse files
miss-islingtonZackerySpytzserhiy-storchaka
authored
[3.11] bpo-35332: Handle os.close() errors in shutil.rmtree() (GH-23766) (GH-112764)
* Ignore os.close() errors when ignore_errors is True. * Pass os.close() errors to the error handler if specified. * os.close no longer retried after error. (cherry picked from commit 11d88a1) Co-authored-by: Zackery Spytz <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 23920a0 commit 54ca4c6

File tree

3 files changed

+56
-2
lines changed

3 files changed

+56
-2
lines changed

Lib/shutil.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,12 @@ def _rmtree_safe_fd(topfd, path, onerror):
660660
_rmtree_safe_fd(dirfd, fullname, onerror)
661661
try:
662662
os.close(dirfd)
663+
except OSError:
664+
# close() should not be retried after an error.
663665
dirfd_closed = True
666+
onerror(os.close, fullname, sys.exc_info())
667+
dirfd_closed = True
668+
try:
664669
os.rmdir(entry.name, dir_fd=topfd)
665670
except OSError:
666671
onerror(os.rmdir, fullname, sys.exc_info())
@@ -675,7 +680,10 @@ def _rmtree_safe_fd(topfd, path, onerror):
675680
onerror(os.path.islink, fullname, sys.exc_info())
676681
finally:
677682
if not dirfd_closed:
678-
os.close(dirfd)
683+
try:
684+
os.close(dirfd)
685+
except OSError:
686+
onerror(os.close, fullname, sys.exc_info())
679687
else:
680688
try:
681689
os.unlink(entry.name, dir_fd=topfd)
@@ -732,7 +740,12 @@ def onerror(*args):
732740
_rmtree_safe_fd(fd, path, onerror)
733741
try:
734742
os.close(fd)
743+
except OSError:
744+
# close() should not be retried after an error.
735745
fd_closed = True
746+
onerror(os.close, path, sys.exc_info())
747+
fd_closed = True
748+
try:
736749
os.rmdir(path, dir_fd=dir_fd)
737750
except OSError:
738751
onerror(os.rmdir, path, sys.exc_info())
@@ -744,7 +757,10 @@ def onerror(*args):
744757
onerror(os.path.islink, path, sys.exc_info())
745758
finally:
746759
if not fd_closed:
747-
os.close(fd)
760+
try:
761+
os.close(fd)
762+
except OSError:
763+
onerror(os.close, path, sys.exc_info())
748764
else:
749765
if dir_fd is not None:
750766
raise NotImplementedError("dir_fd unavailable on this platform")

Lib/test/test_shutil.py

+35
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,41 @@ def _raiser(*args, **kwargs):
409409
self.assertFalse(shutil._use_fd_functions)
410410
self.assertFalse(shutil.rmtree.avoids_symlink_attacks)
411411

412+
@unittest.skipUnless(shutil._use_fd_functions, "requires safe rmtree")
413+
def test_rmtree_fails_on_close(self):
414+
# Test that the error handler is called for failed os.close() and that
415+
# os.close() is only called once for a file descriptor.
416+
tmp = self.mkdtemp()
417+
dir1 = os.path.join(tmp, 'dir1')
418+
os.mkdir(dir1)
419+
dir2 = os.path.join(dir1, 'dir2')
420+
os.mkdir(dir2)
421+
def close(fd):
422+
orig_close(fd)
423+
nonlocal close_count
424+
close_count += 1
425+
raise OSError
426+
427+
close_count = 0
428+
with support.swap_attr(os, 'close', close) as orig_close:
429+
with self.assertRaises(OSError):
430+
shutil.rmtree(dir1)
431+
self.assertTrue(os.path.isdir(dir2))
432+
self.assertEqual(close_count, 2)
433+
434+
close_count = 0
435+
errors = []
436+
def onerror(*args):
437+
errors.append(args)
438+
with support.swap_attr(os, 'close', close) as orig_close:
439+
shutil.rmtree(dir1, onerror=onerror)
440+
self.assertEqual(len(errors), 2)
441+
self.assertIs(errors[0][0], close)
442+
self.assertEqual(errors[0][1], dir2)
443+
self.assertIs(errors[1][0], close)
444+
self.assertEqual(errors[1][1], dir1)
445+
self.assertEqual(close_count, 2)
446+
412447
@unittest.skipUnless(shutil._use_fd_functions, "dir_fd is not supported")
413448
def test_rmtree_with_dir_fd(self):
414449
tmp_dir = self.mkdtemp()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :func:`shutil.rmtree` function now ignores errors when calling
2+
:func:`os.close` when *ignore_errors* is ``True``, and
3+
:func:`os.close` no longer retried after error.

0 commit comments

Comments
 (0)