blob: c8b1a076768552ce42747f12fd5603e4f8b06bcb [file] [log] [blame]
//! 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::{Atomic, AtomicU32, Ordering};
use super::{AsRawHandle, DirBuff, File, FromRawHandle};
use crate::sys::c;
use crate::sys::pal::api::{UnicodeStrRef, WinError, unicode_str};
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.
/// `options` will be OR'd with `FILE_OPEN_REPARSE_POINT`.
fn open_link_no_reparse(
parent: &File,
path: UnicodeStrRef<'_>,
access: u32,
options: 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: Atomic<u32> = AtomicU32::new(c::OBJ_DONT_REPARSE);
let result = unsafe {
let mut object = c::OBJECT_ATTRIBUTES {
ObjectName: path.as_ptr(),
RootDirectory: parent.as_raw_handle(),
Attributes: ATTRIBUTES.load(Ordering::Relaxed),
..c::OBJECT_ATTRIBUTES::with_length()
};
let share = c::FILE_SHARE_DELETE | c::FILE_SHARE_READ | c::FILE_SHARE_WRITE;
let options = c::FILE_OPEN_REPARSE_POINT | options;
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: UnicodeStrRef<'_>) -> Result<Option<File>, WinError> {
// Open the directory for synchronous directory listing.
open_link_no_reparse(
parent,
name,
c::SYNCHRONIZE | c::FILE_LIST_DIRECTORY,
// "_IO_NONALERT" means that a synchronous call won't be interrupted.
c::FILE_SYNCHRONOUS_IO_NONALERT,
)
}
fn delete(parent: &File, name: UnicodeStrRef<'_>) -> 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::DELETE, 0) {
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() {
let name = unicode_str!(&name);
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.
let name = unicode_str!("");
retry(|| delete(&dir, name), WinError::DIR_NOT_EMPTY)?;
restart = true;
}
}
Ok(())
}