diff --git a/Lib/shutil.py b/Lib/shutil.py --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -323,6 +323,48 @@ if errors: raise Error(errors) +def _safe_rmtree(path, ignore_errors=False, onerror=None): + """Recursively delete a directory tree. + + Same a rmtree but uses safe functions to avoid race conditions + as described in #4489. + + """ + if ignore_errors: + def onerror(*args): + pass + elif onerror is None: + def onerror(*args): + raise + try: + if os.path.islink(path): + # symlinks to directories are forbidden, see bug #1669 + raise OSError("Cannot call rmtree on a symbolic link") + except OSError: + onerror(os.path.islink, path, sys.exc_info()) + # can't continue even if onerror hook returns + return + + try: + for root, dirs, files, rootfd in os.fwalk(path, topdown=False): + for name in files: + try: + os.unlinkat(rootfd, name) + except os.error: + onerror(os.unlinkat, os.path.join(path, name), sys.exc_info()) + for name in dirs: + try: + os.unlinkat(rootfd, name, os.AT_REMOVEDIR) + except os.error: + onerror(os.unlinkat, os.path.join(path, name), sys.exc_info()) + except: + onerror(os.fwalk, path, sys.exc_info()) + + try: + os.rmdir(path) + except os.error: + onerror(os.rmdir, path, sys.exc_info()) + def rmtree(path, ignore_errors=False, onerror=None): """Recursively delete a directory tree. @@ -371,6 +413,8 @@ except os.error: onerror(os.rmdir, path, sys.exc_info()) +if hasattr(os, 'fwalk') and hasattr(os, 'unlinkat'): + rmtree = _safe_rmtree def _basename(path): # A basename() variant which first strips the trailing slash, if present. diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -152,11 +152,16 @@ # at os.listdir. The first failure may legally # be either. if self.errorState == 0: - if func is os.remove: + if func is os.remove or \ + hasattr(os, 'unlinkat') and func is os.unlinkat: self.assertEqual(arg, self.childpath) else: - self.assertIs(func, os.listdir, - "func must be either os.remove or os.listdir") + if shutil._safe_rmtree is shutil.rmtree: + self.assertIs(func, os.fwalk, + "func must be either os.remove or os.fwalk") + else: + self.assertIs(func, os.listdir, + "func must be either os.remove or os.listdir") self.assertEqual(arg, TESTFN) self.assertTrue(issubclass(exc[0], OSError)) self.errorState = 1