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