gh-81441: shutil.rmtree() FileNotFoundError race condition (GH-14064)

Ignore missing files and directories while enumerating
directory entries in shutil.rmtree().

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Jeffrey Kintscher 2023-12-05 01:33:51 -08:00 committed by GitHub
parent b31232ddf7
commit 268415bbb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 11 deletions

View file

@ -590,23 +590,21 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
dirs_exist_ok=dirs_exist_ok)
if hasattr(os.stat_result, 'st_file_attributes'):
def _rmtree_islink(path):
try:
st = os.lstat(path)
return (stat.S_ISLNK(st.st_mode) or
(st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT
and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
except OSError:
return False
def _rmtree_islink(st):
return (stat.S_ISLNK(st.st_mode) or
(st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT
and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
else:
def _rmtree_islink(path):
return os.path.islink(path)
def _rmtree_islink(st):
return stat.S_ISLNK(st.st_mode)
# version vulnerable to race conditions
def _rmtree_unsafe(path, onexc):
try:
with os.scandir(path) as scandir_it:
entries = list(scandir_it)
except FileNotFoundError:
return
except OSError as err:
onexc(os.scandir, path, err)
entries = []
@ -614,6 +612,8 @@ def _rmtree_unsafe(path, onexc):
fullname = entry.path
try:
is_dir = entry.is_dir(follow_symlinks=False)
except FileNotFoundError:
continue
except OSError:
is_dir = False
@ -624,6 +624,8 @@ def _rmtree_unsafe(path, onexc):
# a directory with a symlink after the call to
# os.scandir or entry.is_dir above.
raise OSError("Cannot call rmtree on a symbolic link")
except FileNotFoundError:
continue
except OSError as err:
onexc(os.path.islink, fullname, err)
continue
@ -631,10 +633,14 @@ def _rmtree_unsafe(path, onexc):
else:
try:
os.unlink(fullname)
except FileNotFoundError:
continue
except OSError as err:
onexc(os.unlink, fullname, err)
try:
os.rmdir(path)
except FileNotFoundError:
pass
except OSError as err:
onexc(os.rmdir, path, err)
@ -643,6 +649,8 @@ def _rmtree_safe_fd(topfd, path, onexc):
try:
with os.scandir(topfd) as scandir_it:
entries = list(scandir_it)
except FileNotFoundError:
return
except OSError as err:
err.filename = path
onexc(os.scandir, path, err)
@ -651,6 +659,8 @@ def _rmtree_safe_fd(topfd, path, onexc):
fullname = os.path.join(path, entry.name)
try:
is_dir = entry.is_dir(follow_symlinks=False)
except FileNotFoundError:
continue
except OSError:
is_dir = False
else:
@ -658,6 +668,8 @@ def _rmtree_safe_fd(topfd, path, onexc):
try:
orig_st = entry.stat(follow_symlinks=False)
is_dir = stat.S_ISDIR(orig_st.st_mode)
except FileNotFoundError:
continue
except OSError as err:
onexc(os.lstat, fullname, err)
continue
@ -665,6 +677,8 @@ def _rmtree_safe_fd(topfd, path, onexc):
try:
dirfd = os.open(entry.name, os.O_RDONLY, dir_fd=topfd)
dirfd_closed = False
except FileNotFoundError:
continue
except OSError as err:
onexc(os.open, fullname, err)
else:
@ -675,6 +689,8 @@ def _rmtree_safe_fd(topfd, path, onexc):
os.close(dirfd)
dirfd_closed = True
os.rmdir(entry.name, dir_fd=topfd)
except FileNotFoundError:
continue
except OSError as err:
onexc(os.rmdir, fullname, err)
else:
@ -692,6 +708,8 @@ def _rmtree_safe_fd(topfd, path, onexc):
else:
try:
os.unlink(entry.name, dir_fd=topfd)
except FileNotFoundError:
continue
except OSError as err:
onexc(os.unlink, fullname, err)
@ -781,7 +799,12 @@ def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
if dir_fd is not None:
raise NotImplementedError("dir_fd unavailable on this platform")
try:
if _rmtree_islink(path):
st = os.lstat(path)
except OSError as err:
onexc(os.lstat, path, err)
return
try:
if _rmtree_islink(st):
# symlinks to directories are forbidden, see bug #1669
raise OSError("Cannot call rmtree on a symbolic link")
except OSError as err: