Move remove_dir_all impl into a module

This commit is contained in:
Chris Denton 2024-08-31 02:40:35 +00:00
parent 0d634185df
commit bb9d5c4658
No known key found for this signature in database
GPG Key ID: 713472F2F45627DE
5 changed files with 253 additions and 169 deletions

View File

@ -254,7 +254,7 @@ pub struct WinError {
pub code: u32, pub code: u32,
} }
impl WinError { impl WinError {
const fn new(code: u32) -> Self { pub const fn new(code: u32) -> Self {
Self { code } Self { code }
} }
} }
@ -272,8 +272,11 @@ impl WinError {
// tidy-alphabetical-start // tidy-alphabetical-start
pub const ACCESS_DENIED: Self = Self::new(c::ERROR_ACCESS_DENIED); pub const ACCESS_DENIED: Self = Self::new(c::ERROR_ACCESS_DENIED);
pub const ALREADY_EXISTS: Self = Self::new(c::ERROR_ALREADY_EXISTS); pub const ALREADY_EXISTS: Self = Self::new(c::ERROR_ALREADY_EXISTS);
pub const BAD_NET_NAME: Self = Self::new(c::ERROR_BAD_NET_NAME);
pub const BAD_NETPATH: Self = Self::new(c::ERROR_BAD_NETPATH);
pub const CANT_ACCESS_FILE: Self = Self::new(c::ERROR_CANT_ACCESS_FILE); pub const CANT_ACCESS_FILE: Self = Self::new(c::ERROR_CANT_ACCESS_FILE);
pub const DELETE_PENDING: Self = Self::new(c::ERROR_DELETE_PENDING); pub const DELETE_PENDING: Self = Self::new(c::ERROR_DELETE_PENDING);
pub const DIR_NOT_EMPTY: Self = Self::new(c::ERROR_DIR_NOT_EMPTY);
pub const DIRECTORY: Self = Self::new(c::ERROR_DIRECTORY); pub const DIRECTORY: Self = Self::new(c::ERROR_DIRECTORY);
pub const FILE_NOT_FOUND: Self = Self::new(c::ERROR_FILE_NOT_FOUND); pub const FILE_NOT_FOUND: Self = Self::new(c::ERROR_FILE_NOT_FOUND);
pub const INSUFFICIENT_BUFFER: Self = Self::new(c::ERROR_INSUFFICIENT_BUFFER); pub const INSUFFICIENT_BUFFER: Self = Self::new(c::ERROR_INSUFFICIENT_BUFFER);

View File

@ -34,6 +34,7 @@ Windows.Wdk.Storage.FileSystem.FILE_WRITE_THROUGH
Windows.Wdk.Storage.FileSystem.NtCreateFile Windows.Wdk.Storage.FileSystem.NtCreateFile
Windows.Wdk.Storage.FileSystem.NTCREATEFILE_CREATE_DISPOSITION Windows.Wdk.Storage.FileSystem.NTCREATEFILE_CREATE_DISPOSITION
Windows.Wdk.Storage.FileSystem.NTCREATEFILE_CREATE_OPTIONS Windows.Wdk.Storage.FileSystem.NTCREATEFILE_CREATE_OPTIONS
Windows.Wdk.Storage.FileSystem.NtOpenFile
Windows.Wdk.Storage.FileSystem.NtReadFile Windows.Wdk.Storage.FileSystem.NtReadFile
Windows.Wdk.Storage.FileSystem.NtWriteFile Windows.Wdk.Storage.FileSystem.NtWriteFile
Windows.Wdk.Storage.FileSystem.SYMLINK_FLAG_RELATIVE Windows.Wdk.Storage.FileSystem.SYMLINK_FLAG_RELATIVE
@ -1931,10 +1932,14 @@ Windows.Win32.Foundation.RtlNtStatusToDosError
Windows.Win32.Foundation.SetHandleInformation Windows.Win32.Foundation.SetHandleInformation
Windows.Win32.Foundation.SetLastError Windows.Win32.Foundation.SetLastError
Windows.Win32.Foundation.STATUS_DELETE_PENDING Windows.Win32.Foundation.STATUS_DELETE_PENDING
Windows.Win32.Foundation.STATUS_DIRECTORY_NOT_EMPTY
Windows.Win32.Foundation.STATUS_END_OF_FILE Windows.Win32.Foundation.STATUS_END_OF_FILE
Windows.Win32.Foundation.STATUS_FILE_DELETED
Windows.Win32.Foundation.STATUS_INVALID_HANDLE
Windows.Win32.Foundation.STATUS_INVALID_PARAMETER Windows.Win32.Foundation.STATUS_INVALID_PARAMETER
Windows.Win32.Foundation.STATUS_NOT_IMPLEMENTED Windows.Win32.Foundation.STATUS_NOT_IMPLEMENTED
Windows.Win32.Foundation.STATUS_PENDING Windows.Win32.Foundation.STATUS_PENDING
Windows.Win32.Foundation.STATUS_SHARING_VIOLATION
Windows.Win32.Foundation.STATUS_SUCCESS Windows.Win32.Foundation.STATUS_SUCCESS
Windows.Win32.Foundation.TRUE Windows.Win32.Foundation.TRUE
Windows.Win32.Foundation.UNICODE_STRING Windows.Win32.Foundation.UNICODE_STRING

View File

@ -105,6 +105,7 @@
windows_targets::link!("kernel32.dll" "system" fn WriteConsoleW(hconsoleoutput : HANDLE, lpbuffer : PCWSTR, nnumberofcharstowrite : u32, lpnumberofcharswritten : *mut u32, lpreserved : *const core::ffi::c_void) -> BOOL); windows_targets::link!("kernel32.dll" "system" fn WriteConsoleW(hconsoleoutput : HANDLE, lpbuffer : PCWSTR, nnumberofcharstowrite : u32, lpnumberofcharswritten : *mut u32, lpreserved : *const core::ffi::c_void) -> BOOL);
windows_targets::link!("kernel32.dll" "system" fn WriteFileEx(hfile : HANDLE, lpbuffer : *const u8, nnumberofbytestowrite : u32, lpoverlapped : *mut OVERLAPPED, lpcompletionroutine : LPOVERLAPPED_COMPLETION_ROUTINE) -> BOOL); windows_targets::link!("kernel32.dll" "system" fn WriteFileEx(hfile : HANDLE, lpbuffer : *const u8, nnumberofbytestowrite : u32, lpoverlapped : *mut OVERLAPPED, lpcompletionroutine : LPOVERLAPPED_COMPLETION_ROUTINE) -> BOOL);
windows_targets::link!("ntdll.dll" "system" fn NtCreateFile(filehandle : *mut HANDLE, desiredaccess : FILE_ACCESS_RIGHTS, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, allocationsize : *const i64, fileattributes : FILE_FLAGS_AND_ATTRIBUTES, shareaccess : FILE_SHARE_MODE, createdisposition : NTCREATEFILE_CREATE_DISPOSITION, createoptions : NTCREATEFILE_CREATE_OPTIONS, eabuffer : *const core::ffi::c_void, ealength : u32) -> NTSTATUS); windows_targets::link!("ntdll.dll" "system" fn NtCreateFile(filehandle : *mut HANDLE, desiredaccess : FILE_ACCESS_RIGHTS, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, allocationsize : *const i64, fileattributes : FILE_FLAGS_AND_ATTRIBUTES, shareaccess : FILE_SHARE_MODE, createdisposition : NTCREATEFILE_CREATE_DISPOSITION, createoptions : NTCREATEFILE_CREATE_OPTIONS, eabuffer : *const core::ffi::c_void, ealength : u32) -> NTSTATUS);
windows_targets::link!("ntdll.dll" "system" fn NtOpenFile(filehandle : *mut HANDLE, desiredaccess : u32, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, shareaccess : u32, openoptions : u32) -> NTSTATUS);
windows_targets::link!("ntdll.dll" "system" fn NtReadFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *mut core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS); windows_targets::link!("ntdll.dll" "system" fn NtReadFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *mut core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS);
windows_targets::link!("ntdll.dll" "system" fn NtWriteFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *const core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS); windows_targets::link!("ntdll.dll" "system" fn NtWriteFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *const core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS);
windows_targets::link!("ntdll.dll" "system" fn RtlNtStatusToDosError(status : NTSTATUS) -> u32); windows_targets::link!("ntdll.dll" "system" fn RtlNtStatusToDosError(status : NTSTATUS) -> u32);
@ -2982,10 +2983,14 @@ pub struct STARTUPINFOW {
} }
pub type STARTUPINFOW_FLAGS = u32; pub type STARTUPINFOW_FLAGS = u32;
pub const STATUS_DELETE_PENDING: NTSTATUS = 0xC0000056_u32 as _; pub const STATUS_DELETE_PENDING: NTSTATUS = 0xC0000056_u32 as _;
pub const STATUS_DIRECTORY_NOT_EMPTY: NTSTATUS = 0xC0000101_u32 as _;
pub const STATUS_END_OF_FILE: NTSTATUS = 0xC0000011_u32 as _; pub const STATUS_END_OF_FILE: NTSTATUS = 0xC0000011_u32 as _;
pub const STATUS_FILE_DELETED: NTSTATUS = 0xC0000123_u32 as _;
pub const STATUS_INVALID_HANDLE: NTSTATUS = 0xC0000008_u32 as _;
pub const STATUS_INVALID_PARAMETER: NTSTATUS = 0xC000000D_u32 as _; pub const STATUS_INVALID_PARAMETER: NTSTATUS = 0xC000000D_u32 as _;
pub const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC0000002_u32 as _; pub const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC0000002_u32 as _;
pub const STATUS_PENDING: NTSTATUS = 0x103_u32 as _; pub const STATUS_PENDING: NTSTATUS = 0x103_u32 as _;
pub const STATUS_SHARING_VIOLATION: NTSTATUS = 0xC0000043_u32 as _;
pub const STATUS_SUCCESS: NTSTATUS = 0x0_u32 as _; pub const STATUS_SUCCESS: NTSTATUS = 0x0_u32 as _;
pub const STD_ERROR_HANDLE: STD_HANDLE = 4294967284u32; pub const STD_ERROR_HANDLE: STD_HANDLE = 4294967284u32;
pub type STD_HANDLE = u32; pub type STD_HANDLE = u32;

View File

@ -14,8 +14,11 @@
use crate::sys::path::maybe_verbatim; use crate::sys::path::maybe_verbatim;
use crate::sys::time::SystemTime; use crate::sys::time::SystemTime;
use crate::sys::{c, cvt, Align8}; use crate::sys::{c, cvt, Align8};
use crate::sys_common::{ignore_notfound, AsInner, FromInner, IntoInner}; use crate::sys_common::{AsInner, FromInner, IntoInner};
use crate::{fmt, ptr, slice, thread}; use crate::{fmt, ptr, slice};
mod remove_dir_all;
use remove_dir_all::remove_dir_all_iterative;
pub struct File { pub struct File {
handle: Handle, handle: Handle,
@ -646,6 +649,22 @@ fn basic_info(&self) -> io::Result<c::FILE_BASIC_INFO> {
Ok(info) Ok(info)
} }
} }
/// Deletes the file, consuming the file handle to ensure the delete occurs
/// as immediately as possible.
/// This attempts to use `posix_delete` but falls back to `win32_delete`
/// if that is not supported by the filesystem.
#[allow(unused)]
fn delete(self) -> Result<(), WinError> {
// If POSIX delete is not supported for this filesystem then fallback to win32 delete.
match self.posix_delete() {
Err(WinError::INVALID_PARAMETER)
| Err(WinError::NOT_SUPPORTED)
| Err(WinError::INVALID_FUNCTION) => self.win32_delete(),
result => result,
}
}
/// Delete using POSIX semantics. /// Delete using POSIX semantics.
/// ///
/// Files will be deleted as soon as the handle is closed. This is supported /// Files will be deleted as soon as the handle is closed. This is supported
@ -654,21 +673,23 @@ fn basic_info(&self) -> io::Result<c::FILE_BASIC_INFO> {
/// ///
/// If the operation is not supported for this filesystem or OS version /// If the operation is not supported for this filesystem or OS version
/// then errors will be `ERROR_NOT_SUPPORTED` or `ERROR_INVALID_PARAMETER`. /// then errors will be `ERROR_NOT_SUPPORTED` or `ERROR_INVALID_PARAMETER`.
fn posix_delete(&self) -> io::Result<()> { #[allow(unused)]
fn posix_delete(&self) -> Result<(), WinError> {
let info = c::FILE_DISPOSITION_INFO_EX { let info = c::FILE_DISPOSITION_INFO_EX {
Flags: c::FILE_DISPOSITION_FLAG_DELETE Flags: c::FILE_DISPOSITION_FLAG_DELETE
| c::FILE_DISPOSITION_FLAG_POSIX_SEMANTICS | c::FILE_DISPOSITION_FLAG_POSIX_SEMANTICS
| c::FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, | c::FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE,
}; };
api::set_file_information_by_handle(self.handle.as_raw_handle(), &info).io_result() api::set_file_information_by_handle(self.handle.as_raw_handle(), &info)
} }
/// Delete a file using win32 semantics. The file won't actually be deleted /// Delete a file using win32 semantics. The file won't actually be deleted
/// until all file handles are closed. However, marking a file for deletion /// until all file handles are closed. However, marking a file for deletion
/// will prevent anyone from opening a new handle to the file. /// will prevent anyone from opening a new handle to the file.
fn win32_delete(&self) -> io::Result<()> { #[allow(unused)]
fn win32_delete(&self) -> Result<(), WinError> {
let info = c::FILE_DISPOSITION_INFO { DeleteFile: c::TRUE as _ }; let info = c::FILE_DISPOSITION_INFO { DeleteFile: c::TRUE as _ };
api::set_file_information_by_handle(self.handle.as_raw_handle(), &info).io_result() api::set_file_information_by_handle(self.handle.as_raw_handle(), &info)
} }
/// Fill the given buffer with as many directory entries as will fit. /// Fill the given buffer with as many directory entries as will fit.
@ -684,21 +705,23 @@ fn win32_delete(&self) -> io::Result<()> {
/// A symlink directory is simply an empty directory with some "reparse" metadata attached. /// A symlink directory is simply an empty directory with some "reparse" metadata attached.
/// So if you open a link (not its target) and iterate the directory, /// So if you open a link (not its target) and iterate the directory,
/// you will always iterate an empty directory regardless of the target. /// you will always iterate an empty directory regardless of the target.
fn fill_dir_buff(&self, buffer: &mut DirBuff, restart: bool) -> io::Result<bool> { #[allow(unused)]
fn fill_dir_buff(&self, buffer: &mut DirBuff, restart: bool) -> Result<bool, WinError> {
let class = let class =
if restart { c::FileIdBothDirectoryRestartInfo } else { c::FileIdBothDirectoryInfo }; if restart { c::FileIdBothDirectoryRestartInfo } else { c::FileIdBothDirectoryInfo };
unsafe { unsafe {
let result = cvt(c::GetFileInformationByHandleEx( let result = c::GetFileInformationByHandleEx(
self.handle.as_raw_handle(), self.as_raw_handle(),
class, class,
buffer.as_mut_ptr().cast(), buffer.as_mut_ptr().cast(),
buffer.capacity() as _, buffer.capacity() as _,
)); );
match result { if result == 0 {
Ok(_) => Ok(true), let err = api::get_last_error();
Err(e) if e.raw_os_error() == Some(c::ERROR_NO_MORE_FILES as _) => Ok(false), if err.code == c::ERROR_NO_MORE_FILES { Ok(false) } else { Err(err) }
Err(e) => Err(e), } else {
Ok(true)
} }
} }
} }
@ -804,62 +827,6 @@ unsafe fn from_maybe_unaligned<'a>(p: *const u16, len: usize) -> Cow<'a, [u16]>
} }
} }
/// Open a link relative to the parent directory, ensure no symlinks are followed.
fn open_link_no_reparse(parent: &File, name: &[u16], access: u32) -> io::Result<File> {
// This is implemented using the lower level `NtCreateFile` function as
// unfortunately opening a file relative to a parent is not supported by
// win32 functions. It is however a fundamental feature of the NT kernel.
//
// See https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile
unsafe {
let mut handle = ptr::null_mut();
let mut io_status = c::IO_STATUS_BLOCK::PENDING;
let mut name_str = c::UNICODE_STRING::from_ref(name);
use crate::sync::atomic::{AtomicU32, Ordering};
// 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 object = c::OBJECT_ATTRIBUTES {
ObjectName: &mut name_str,
RootDirectory: parent.as_raw_handle(),
Attributes: ATTRIBUTES.load(Ordering::Relaxed),
..c::OBJECT_ATTRIBUTES::default()
};
let status = c::NtCreateFile(
&mut handle,
access,
&object,
&mut io_status,
crate::ptr::null_mut(),
0,
c::FILE_SHARE_DELETE | c::FILE_SHARE_READ | c::FILE_SHARE_WRITE,
c::FILE_OPEN,
// If `name` is a symlink then open the link rather than the target.
c::FILE_OPEN_REPARSE_POINT,
crate::ptr::null_mut(),
0,
);
// Convert an NTSTATUS to the more familiar Win32 error codes (aka "DosError")
if c::nt_success(status) {
Ok(File::from_raw_handle(handle))
} else 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.
Err(io::Error::from_raw_os_error(c::ERROR_DELETE_PENDING as i32))
} else if status == c::STATUS_INVALID_PARAMETER
&& ATTRIBUTES.load(Ordering::Relaxed) == c::OBJ_DONT_REPARSE
{
// Try without `OBJ_DONT_REPARSE`. See above.
ATTRIBUTES.store(0, Ordering::Relaxed);
open_link_no_reparse(parent, name, access)
} else {
Err(io::Error::from_raw_os_error(c::RtlNtStatusToDosError(status) as _))
}
}
}
impl AsInner<Handle> for File { impl AsInner<Handle> for File {
#[inline] #[inline]
fn as_inner(&self) -> &Handle { fn as_inner(&self) -> &Handle {
@ -1142,114 +1109,22 @@ pub fn rmdir(p: &Path) -> io::Result<()> {
Ok(()) Ok(())
} }
/// Open a file or directory without following symlinks. pub fn remove_dir_all(path: &Path) -> io::Result<()> {
fn open_link(path: &Path, access_mode: u32) -> io::Result<File> { // Open a file or directory without following symlinks.
let mut opts = OpenOptions::new(); let mut opts = OpenOptions::new();
opts.access_mode(access_mode); opts.access_mode(c::FILE_LIST_DIRECTORY);
// `FILE_FLAG_BACKUP_SEMANTICS` allows opening directories. // `FILE_FLAG_BACKUP_SEMANTICS` allows opening directories.
// `FILE_FLAG_OPEN_REPARSE_POINT` opens a link instead of its target. // `FILE_FLAG_OPEN_REPARSE_POINT` opens a link instead of its target.
opts.custom_flags(c::FILE_FLAG_BACKUP_SEMANTICS | c::FILE_FLAG_OPEN_REPARSE_POINT); opts.custom_flags(c::FILE_FLAG_BACKUP_SEMANTICS | c::FILE_FLAG_OPEN_REPARSE_POINT);
File::open(path, &opts) let file = File::open(path, &opts)?;
}
pub fn remove_dir_all(path: &Path) -> io::Result<()> {
let file = open_link(path, c::DELETE | c::FILE_LIST_DIRECTORY)?;
// Test if the file is not a directory or a symlink to a directory. // Test if the file is not a directory or a symlink to a directory.
if (file.basic_info()?.FileAttributes & c::FILE_ATTRIBUTE_DIRECTORY) == 0 { if (file.basic_info()?.FileAttributes & c::FILE_ATTRIBUTE_DIRECTORY) == 0 {
return Err(io::Error::from_raw_os_error(c::ERROR_DIRECTORY as _)); return Err(io::Error::from_raw_os_error(c::ERROR_DIRECTORY as _));
} }
match ignore_notfound(remove_dir_all_iterative(&file, File::posix_delete)) { // Remove the directory and all its contents.
Err(e) => { remove_dir_all_iterative(file).io_result()
if let Some(code) = e.raw_os_error() {
match code as u32 {
// If POSIX delete is not supported for this filesystem then fallback to win32 delete.
c::ERROR_NOT_SUPPORTED
| c::ERROR_INVALID_FUNCTION
| c::ERROR_INVALID_PARAMETER => {
remove_dir_all_iterative(&file, File::win32_delete)
}
_ => Err(e),
}
} else {
Err(e)
}
}
ok => ok,
}
}
fn remove_dir_all_iterative(f: &File, delete: fn(&File) -> io::Result<()>) -> io::Result<()> {
// When deleting files we may loop this many times when certain error conditions occur.
// This allows remove_dir_all to succeed when the error is temporary.
const MAX_RETRIES: u32 = 10;
let mut buffer = DirBuff::new();
let mut dirlist = vec![f.duplicate()?];
// FIXME: This is a hack so we can push to the dirlist vec after borrowing from it.
fn copy_handle(f: &File) -> mem::ManuallyDrop<File> {
unsafe { mem::ManuallyDrop::new(File::from_raw_handle(f.as_raw_handle())) }
}
let mut restart = true;
while let Some(dir) = dirlist.last() {
let dir = copy_handle(dir);
// Fill the buffer and iterate the entries.
let more_data = dir.fill_dir_buff(&mut buffer, restart)?;
restart = false;
for (name, is_directory) in buffer.iter() {
if is_directory {
let child_dir = open_link_no_reparse(
&dir,
&name,
c::SYNCHRONIZE | c::DELETE | c::FILE_LIST_DIRECTORY,
);
// On success, add the handle to the queue.
// If opening the directory fails we treat it the same as a file
if let Ok(child_dir) = child_dir {
dirlist.push(child_dir);
continue;
}
}
for i in 1..=MAX_RETRIES {
let result = open_link_no_reparse(&dir, &name, c::SYNCHRONIZE | c::DELETE);
match result {
Ok(f) => delete(&f)?,
// Already deleted, so skip.
Err(e) if e.kind() == io::ErrorKind::NotFound => break,
// Retry a few times if the file is locked or a delete is already in progress.
Err(e)
if i < MAX_RETRIES
&& (e.raw_os_error() == Some(c::ERROR_DELETE_PENDING as _)
|| e.raw_os_error() == Some(c::ERROR_SHARING_VIOLATION as _)) => {}
// Otherwise return the error.
Err(e) => return Err(e),
}
thread::yield_now();
}
}
// If there were no more files then delete the directory.
if !more_data {
if let Some(dir) = dirlist.pop() {
// Retry deleting a few times in case we need to wait for a file to be deleted.
for i in 1..=MAX_RETRIES {
let result = delete(&dir);
if let Err(e) = result {
if i == MAX_RETRIES || e.kind() != io::ErrorKind::DirectoryNotEmpty {
return Err(e);
}
thread::yield_now();
} else {
break;
}
}
}
}
}
Ok(())
} }
pub fn readlink(path: &Path) -> io::Result<PathBuf> { pub fn readlink(path: &Path) -> io::Result<PathBuf> {

View File

@ -0,0 +1,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(())
}