Skip to content

Commit 118f222

Browse files
ZackerySpytzserhiy-storchaka
authored andcommitted
bpo-35332: Handle os.close() errors in shutil.rmtree() (pythonGH-23766)
* 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. Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 23f9995 commit 118f222

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
@@ -687,7 +687,12 @@ def _rmtree_safe_fd(topfd, path, onexc):
687687
_rmtree_safe_fd(dirfd, fullname, onexc)
688688
try:
689689
os.close(dirfd)
690+
except OSError as err:
691+
# close() should not be retried after an error.
690692
dirfd_closed = True
693+
onexc(os.close, fullname, err)
694+
dirfd_closed = True
695+
try:
691696
os.rmdir(entry.name, dir_fd=topfd)
692697
except FileNotFoundError:
693698
continue
@@ -704,7 +709,10 @@ def _rmtree_safe_fd(topfd, path, onexc):
704709
onexc(os.path.islink, fullname, err)
705710
finally:
706711
if not dirfd_closed:
707-
os.close(dirfd)
712+
try:
713+
os.close(dirfd)
714+
except OSError as err:
715+
onexc(os.close, fullname, err)
708716
else:
709717
try:
710718
os.unlink(entry.name, dir_fd=topfd)
@@ -782,7 +790,12 @@ def onexc(*args):
782790
_rmtree_safe_fd(fd, path, onexc)
783791
try:
784792
os.close(fd)
793+
except OSError as err:
794+
# close() should not be retried after an error.
785795
fd_closed = True
796+
onexc(os.close, path, err)
797+
fd_closed = True
798+
try:
786799
os.rmdir(path, dir_fd=dir_fd)
787800
except OSError as err:
788801
onexc(os.rmdir, path, err)
@@ -794,7 +807,10 @@ def onexc(*args):
794807
onexc(os.path.islink, path, err)
795808
finally:
796809
if not fd_closed:
797-
os.close(fd)
810+
try:
811+
os.close(fd)
812+
except OSError as err:
813+
onexc(os.close, path, err)
798814
else:
799815
if dir_fd is not None:
800816
raise NotImplementedError("dir_fd unavailable on this platform")

Lib/test/test_shutil.py

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

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