Skip to content

Commit e63bb9c

Browse files
miss-islingtonZackerySpytzserhiy-storchaka
authored
[3.12] bpo-35332: Handle os.close() errors in shutil.rmtree() (GH-23766) (GH-112763)
* 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 5acfb82 commit e63bb9c

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
@@ -674,7 +674,12 @@ def _rmtree_safe_fd(topfd, path, onexc):
674674
_rmtree_safe_fd(dirfd, fullname, onexc)
675675
try:
676676
os.close(dirfd)
677+
except OSError as err:
678+
# close() should not be retried after an error.
677679
dirfd_closed = True
680+
onexc(os.close, fullname, err)
681+
dirfd_closed = True
682+
try:
678683
os.rmdir(entry.name, dir_fd=topfd)
679684
except OSError as err:
680685
onexc(os.rmdir, fullname, err)
@@ -689,7 +694,10 @@ def _rmtree_safe_fd(topfd, path, onexc):
689694
onexc(os.path.islink, fullname, err)
690695
finally:
691696
if not dirfd_closed:
692-
os.close(dirfd)
697+
try:
698+
os.close(dirfd)
699+
except OSError as err:
700+
onexc(os.close, fullname, err)
693701
else:
694702
try:
695703
os.unlink(entry.name, dir_fd=topfd)
@@ -765,7 +773,12 @@ def onexc(*args):
765773
_rmtree_safe_fd(fd, path, onexc)
766774
try:
767775
os.close(fd)
776+
except OSError as err:
777+
# close() should not be retried after an error.
768778
fd_closed = True
779+
onexc(os.close, path, err)
780+
fd_closed = True
781+
try:
769782
os.rmdir(path, dir_fd=dir_fd)
770783
except OSError as err:
771784
onexc(os.rmdir, path, err)
@@ -777,7 +790,10 @@ def onexc(*args):
777790
onexc(os.path.islink, path, err)
778791
finally:
779792
if not fd_closed:
780-
os.close(fd)
793+
try:
794+
os.close(fd)
795+
except OSError as err:
796+
onexc(os.close, path, err)
781797
else:
782798
if dir_fd is not None:
783799
raise NotImplementedError("dir_fd unavailable on this platform")

Lib/test/test_shutil.py

+35
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,41 @@ def _raiser(*args, **kwargs):
578578
self.assertFalse(shutil._use_fd_functions)
579579
self.assertFalse(shutil.rmtree.avoids_symlink_attacks)
580580

581+
@unittest.skipUnless(shutil._use_fd_functions, "requires safe rmtree")
582+
def test_rmtree_fails_on_close(self):
583+
# Test that the error handler is called for failed os.close() and that
584+
# os.close() is only called once for a file descriptor.
585+
tmp = self.mkdtemp()
586+
dir1 = os.path.join(tmp, 'dir1')
587+
os.mkdir(dir1)
588+
dir2 = os.path.join(dir1, 'dir2')
589+
os.mkdir(dir2)
590+
def close(fd):
591+
orig_close(fd)
592+
nonlocal close_count
593+
close_count += 1
594+
raise OSError
595+
596+
close_count = 0
597+
with support.swap_attr(os, 'close', close) as orig_close:
598+
with self.assertRaises(OSError):
599+
shutil.rmtree(dir1)
600+
self.assertTrue(os.path.isdir(dir2))
601+
self.assertEqual(close_count, 2)
602+
603+
close_count = 0
604+
errors = []
605+
def onexc(*args):
606+
errors.append(args)
607+
with support.swap_attr(os, 'close', close) as orig_close:
608+
shutil.rmtree(dir1, onexc=onexc)
609+
self.assertEqual(len(errors), 2)
610+
self.assertIs(errors[0][0], close)
611+
self.assertEqual(errors[0][1], dir2)
612+
self.assertIs(errors[1][0], close)
613+
self.assertEqual(errors[1][1], dir1)
614+
self.assertEqual(close_count, 2)
615+
581616
@unittest.skipUnless(shutil._use_fd_functions, "dir_fd is not supported")
582617
def test_rmtree_with_dir_fd(self):
583618
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)