From f2b2f68def4958c64a9645fdf33017ee53d3531f Mon Sep 17 00:00:00 2001 From: Zackery Spytz Date: Tue, 5 Dec 2023 09:09:39 -0800 Subject: [PATCH] bpo-35332: Handle os.close() errors in shutil.rmtree() (GH-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. (cherry picked from commit 11d88a178b077e42025da538b890db3151a47070) Co-authored-by: Zackery Spytz Co-authored-by: Serhiy Storchaka --- Lib/shutil.py | 20 +++++++++-- Lib/test/test_shutil.py | 35 +++++++++++++++++++ .../2020-12-14-09-31-13.bpo-35332.s22wAx.rst | 3 ++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-12-14-09-31-13.bpo-35332.s22wAx.rst diff --git a/Lib/shutil.py b/Lib/shutil.py index d108986d130267..cae65a3bed2e18 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -660,7 +660,12 @@ def _rmtree_safe_fd(topfd, path, onerror): _rmtree_safe_fd(dirfd, fullname, onerror) try: os.close(dirfd) + except OSError: + # close() should not be retried after an error. dirfd_closed = True + onerror(os.close, fullname, sys.exc_info()) + dirfd_closed = True + try: os.rmdir(entry.name, dir_fd=topfd) except OSError: onerror(os.rmdir, fullname, sys.exc_info()) @@ -675,7 +680,10 @@ def _rmtree_safe_fd(topfd, path, onerror): onerror(os.path.islink, fullname, sys.exc_info()) finally: if not dirfd_closed: - os.close(dirfd) + try: + os.close(dirfd) + except OSError: + onerror(os.close, fullname, sys.exc_info()) else: try: os.unlink(entry.name, dir_fd=topfd) @@ -732,7 +740,12 @@ def onerror(*args): _rmtree_safe_fd(fd, path, onerror) try: os.close(fd) + except OSError: + # close() should not be retried after an error. fd_closed = True + onerror(os.close, path, sys.exc_info()) + fd_closed = True + try: os.rmdir(path, dir_fd=dir_fd) except OSError: onerror(os.rmdir, path, sys.exc_info()) @@ -744,7 +757,10 @@ def onerror(*args): onerror(os.path.islink, path, sys.exc_info()) finally: if not fd_closed: - os.close(fd) + try: + os.close(fd) + except OSError: + onerror(os.close, path, sys.exc_info()) else: if dir_fd is not None: raise NotImplementedError("dir_fd unavailable on this platform") diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 274a8b7377e3bb..0879e7f0c97141 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -409,6 +409,41 @@ def _raiser(*args, **kwargs): self.assertFalse(shutil._use_fd_functions) self.assertFalse(shutil.rmtree.avoids_symlink_attacks) + @unittest.skipUnless(shutil._use_fd_functions, "requires safe rmtree") + def test_rmtree_fails_on_close(self): + # Test that the error handler is called for failed os.close() and that + # os.close() is only called once for a file descriptor. + tmp = self.mkdtemp() + dir1 = os.path.join(tmp, 'dir1') + os.mkdir(dir1) + dir2 = os.path.join(dir1, 'dir2') + os.mkdir(dir2) + def close(fd): + orig_close(fd) + nonlocal close_count + close_count += 1 + raise OSError + + close_count = 0 + with support.swap_attr(os, 'close', close) as orig_close: + with self.assertRaises(OSError): + shutil.rmtree(dir1) + self.assertTrue(os.path.isdir(dir2)) + self.assertEqual(close_count, 2) + + close_count = 0 + errors = [] + def onerror(*args): + errors.append(args) + with support.swap_attr(os, 'close', close) as orig_close: + shutil.rmtree(dir1, onerror=onerror) + self.assertEqual(len(errors), 2) + self.assertIs(errors[0][0], close) + self.assertEqual(errors[0][1], dir2) + self.assertIs(errors[1][0], close) + self.assertEqual(errors[1][1], dir1) + self.assertEqual(close_count, 2) + @unittest.skipUnless(shutil._use_fd_functions, "dir_fd is not supported") def test_rmtree_with_dir_fd(self): tmp_dir = self.mkdtemp() diff --git a/Misc/NEWS.d/next/Library/2020-12-14-09-31-13.bpo-35332.s22wAx.rst b/Misc/NEWS.d/next/Library/2020-12-14-09-31-13.bpo-35332.s22wAx.rst new file mode 100644 index 00000000000000..80564b99a079c6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-12-14-09-31-13.bpo-35332.s22wAx.rst @@ -0,0 +1,3 @@ +The :func:`shutil.rmtree` function now ignores errors when calling +:func:`os.close` when *ignore_errors* is ``True``, and +:func:`os.close` no longer retried after error.