1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
//! The Windows implementation of std::fs::remove_dir_all.
//!
//! This needs to address two issues:
//!
//! - It must not be possible to trick this into deleting files outside of
//! the parent directory (see CVE-2022-21658).
//! - It should not fail if many threads or processes call `remove_dir_all`
//! on the same path.
//!
//! The first is handled by using the low-level `NtOpenFile` API to open a file
//! relative to a parent directory.
//!
//! The second is trickier. Deleting a file works by setting its "disposition"
//! to delete. However, it isn't actually deleted until the file is closed.
//! During the gap between these two events, the file is in a kind of limbo
//! state where it still exists in the filesystem but anything trying to open
//! it fails with an error.
//!
//! The mitigations we use here are:
//!
//! - When attempting to open the file, we treat ERROR_DELETE_PENDING as a
//! successful delete.
//! - If the file still hasn't been removed from the filesystem by the time we
//! attempt to delete the parent directory, we try to wait for it to finish.
//! We can't wait indefinitely though so after some number of spins, we give
//! up and return an error.
//!
//! In short, we can't guarantee this will always succeed in the event of a
//! race but we do make a best effort such that it *should* do so.
use core::ptr;
use core::sync::atomic::{AtomicU32, Ordering};
use super::{AsRawHandle, DirBuff, File, FromRawHandle};
use crate::sys::c;
use crate::sys::pal::windows::api::WinError;
use crate::thread;
// The maximum number of times to spin when waiting for deletes to complete.
const MAX_RETRIES: usize = 50;
/// A wrapper around a raw NtOpenFile call.
///
/// This isn't completely safe because `OBJECT_ATTRIBUTES` contains raw pointers.
unsafe fn nt_open_file(
access: u32,
object_attribute: &c::OBJECT_ATTRIBUTES,
share: u32,
options: u32,
) -> Result<File, WinError> {
unsafe {
let mut handle = ptr::null_mut();
let mut io_status = c::IO_STATUS_BLOCK::PENDING;
let status =
c::NtOpenFile(&mut handle, access, object_attribute, &mut io_status, share, options);
if c::nt_success(status) {
Ok(File::from_raw_handle(handle))
} else {
// Convert an NTSTATUS to the more familiar Win32 error code (aka "DosError")
let win_error = if status == c::STATUS_DELETE_PENDING {
// We make a special exception for `STATUS_DELETE_PENDING` because
// otherwise this will be mapped to `ERROR_ACCESS_DENIED` which is
// very unhelpful because that can also mean a permission error.
WinError::DELETE_PENDING
} else {
WinError::new(c::RtlNtStatusToDosError(status))
};
Err(win_error)
}
}
}
/// Open the file `path` in the directory `parent`, requesting the given `access` rights.
fn open_link_no_reparse(
parent: &File,
path: &[u16],
access: u32,
) -> Result<Option<File>, WinError> {
// This is implemented using the lower level `NtOpenFile` function as
// unfortunately opening a file relative to a parent is not supported by
// win32 functions.
//
// See https://learn.microsoft.com/windows/win32/api/winternl/nf-winternl-ntopenfile
// The `OBJ_DONT_REPARSE` attribute ensures that we haven't been
// tricked into following a symlink. However, it may not be available in
// earlier versions of Windows.
static ATTRIBUTES: AtomicU32 = AtomicU32::new(c::OBJ_DONT_REPARSE);
let result = unsafe {
let mut path_str = c::UNICODE_STRING::from_ref(path);
let mut object = c::OBJECT_ATTRIBUTES {
ObjectName: &mut path_str,
RootDirectory: parent.as_raw_handle(),
Attributes: ATTRIBUTES.load(Ordering::Relaxed),
..c::OBJECT_ATTRIBUTES::default()
};
let share = c::FILE_SHARE_DELETE | c::FILE_SHARE_READ | c::FILE_SHARE_WRITE;
let options = c::FILE_OPEN_REPARSE_POINT;
let result = nt_open_file(access, &object, share, options);
// Retry without OBJ_DONT_REPARSE if it's not supported.
if matches!(result, Err(WinError::INVALID_PARAMETER))
&& ATTRIBUTES.load(Ordering::Relaxed) == c::OBJ_DONT_REPARSE
{
ATTRIBUTES.store(0, Ordering::Relaxed);
object.Attributes = 0;
nt_open_file(access, &object, share, options)
} else {
result
}
};
// Ignore not found errors
match result {
Ok(f) => Ok(Some(f)),
Err(
WinError::FILE_NOT_FOUND
| WinError::PATH_NOT_FOUND
| WinError::BAD_NETPATH
| WinError::BAD_NET_NAME
// `DELETE_PENDING` means something else is already trying to delete it
// so we assume that will eventually succeed.
| WinError::DELETE_PENDING,
) => Ok(None),
Err(e) => Err(e),
}
}
fn open_dir(parent: &File, name: &[u16]) -> Result<Option<File>, WinError> {
open_link_no_reparse(parent, name, c::SYNCHRONIZE | c::FILE_LIST_DIRECTORY)
}
fn delete(parent: &File, name: &[u16]) -> Result<(), WinError> {
// Note that the `delete` function consumes the opened file to ensure it's
// dropped immediately. See module comments for why this is important.
match open_link_no_reparse(parent, name, c::SYNCHRONIZE | c::DELETE) {
Ok(Some(f)) => f.delete(),
Ok(None) => Ok(()),
Err(e) => Err(e),
}
}
/// A simple retry loop that keeps running `f` while it fails with the given
/// error code or until `MAX_RETRIES` is reached.
fn retry<T: PartialEq>(
mut f: impl FnMut() -> Result<T, WinError>,
ignore: WinError,
) -> Result<T, WinError> {
let mut i = MAX_RETRIES;
loop {
i -= 1;
if i == 0 {
return f();
} else {
let result = f();
if result != Err(ignore) {
return result;
}
}
thread::yield_now();
}
}
pub fn remove_dir_all_iterative(dir: File) -> Result<(), WinError> {
let mut buffer = DirBuff::new();
let mut dirlist = vec![dir];
let mut restart = true;
'outer: while let Some(dir) = dirlist.pop() {
let more_data = dir.fill_dir_buff(&mut buffer, restart)?;
for (name, is_directory) in buffer.iter() {
if is_directory {
let Some(subdir) = open_dir(&dir, &name)? else { continue };
dirlist.push(dir);
dirlist.push(subdir);
continue 'outer;
} else {
// Attempt to delete, retrying on sharing violation errors as these
// can often be very temporary. E.g. if something takes just a
// bit longer than expected to release a file handle.
retry(|| delete(&dir, &name), WinError::SHARING_VIOLATION)?;
}
}
if more_data {
dirlist.push(dir);
restart = false;
} else {
// Attempt to delete, retrying on not empty errors because we may
// need to wait some time for files to be removed from the filesystem.
retry(|| delete(&dir, &[]), WinError::DIR_NOT_EMPTY)?;
restart = true;
}
}
Ok(())
}