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 // Workaround for #143078.
94 // While the Windows OS itself handles zero length strings,
95 // some security software that hooks system functions may expect it to
96 // be null terminated. So as a workaround we ensure zero length strings
97 // always point to a zero u16 even though it should never be read.
98 static EMPTY_STR: [u16; 1] = [0];
99 let mut path_str = c::UNICODE_STRING::from_ref(path);
100 if path_str.Length == 0 {
101 path_str.Buffer = EMPTY_STR.as_ptr().cast_mut();
102 }
103 let mut object = c::OBJECT_ATTRIBUTES {
104 ObjectName: &mut path_str,
105 RootDirectory: parent.as_raw_handle(),
106 Attributes: ATTRIBUTES.load(Ordering::Relaxed),
107 ..c::OBJECT_ATTRIBUTES::with_length()
108 };
109 let share = c::FILE_SHARE_DELETE | c::FILE_SHARE_READ | c::FILE_SHARE_WRITE;
110 let options = c::FILE_OPEN_REPARSE_POINT | options;
111 let result = nt_open_file(access, &object, share, options);
112
113 // Retry without OBJ_DONT_REPARSE if it's not supported.
114 if matches!(result, Err(WinError::INVALID_PARAMETER))
115 && ATTRIBUTES.load(Ordering::Relaxed) == c::OBJ_DONT_REPARSE
116 {
117 ATTRIBUTES.store(0, Ordering::Relaxed);
118 object.Attributes = 0;
119 nt_open_file(access, &object, share, options)
120 } else {
121 result
122 }
123 };
124
125 // Ignore not found errors
126 match result {
127 Ok(f) => Ok(Some(f)),
128 Err(
129 WinError::FILE_NOT_FOUND
130 | WinError::PATH_NOT_FOUND
131 | WinError::BAD_NETPATH
132 | WinError::BAD_NET_NAME
133 // `DELETE_PENDING` means something else is already trying to delete it
134 // so we assume that will eventually succeed.
135 | WinError::DELETE_PENDING,
136 ) => Ok(None),
137 Err(e) => Err(e),
138 }
139}
140
141fn open_dir(parent: &File, name: &[u16]) -> Result<Option<File>, WinError> {
142 // Open the directory for synchronous directory listing.
143 open_link_no_reparse(
144 parent,
145 name,
146 c::SYNCHRONIZE | c::FILE_LIST_DIRECTORY,
147 // "_IO_NONALERT" means that a synchronous call won't be interrupted.
148 c::FILE_SYNCHRONOUS_IO_NONALERT,
149 )
150}
151
152fn delete(parent: &File, name: &[u16]) -> Result<(), WinError> {
153 // Note that the `delete` function consumes the opened file to ensure it's
154 // dropped immediately. See module comments for why this is important.
155 match open_link_no_reparse(parent, name, c::DELETE, 0) {
156 Ok(Some(f)) => f.delete(),
157 Ok(None) => Ok(()),
158 Err(e) => Err(e),
159 }
160}
161
162/// A simple retry loop that keeps running `f` while it fails with the given
163/// error code or until `MAX_RETRIES` is reached.
164fn retry<T: PartialEq>(
165 mut f: impl FnMut() -> Result<T, WinError>,
166 ignore: WinError,
167) -> Result<T, WinError> {
168 let mut i = MAX_RETRIES;
169 loop {
170 i -= 1;
171 if i == 0 {
172 return f();
173 } else {
174 let result = f();
175 if result != Err(ignore) {
176 return result;
177 }
178 }
179 thread::yield_now();
180 }
181}
182
183pub fn remove_dir_all_iterative(dir: File) -> Result<(), WinError> {
184 let mut buffer = DirBuff::new();
185 let mut dirlist = vec![dir];
186
187 let mut restart = true;
188 'outer: while let Some(dir) = dirlist.pop() {
189 let more_data = dir.fill_dir_buff(&mut buffer, restart)?;
190 for (name, is_directory) in buffer.iter() {
191 if is_directory {
192 let Some(subdir) = open_dir(&dir, &name)? else { continue };
193 dirlist.push(dir);
194 dirlist.push(subdir);
195 continue 'outer;
196 } else {
197 // Attempt to delete, retrying on sharing violation errors as these
198 // can often be very temporary. E.g. if something takes just a
199 // bit longer than expected to release a file handle.
200 retry(|| delete(&dir, &name), WinError::SHARING_VIOLATION)?;
201 }
202 }
203 if more_data {
204 dirlist.push(dir);
205 restart = false;
206 } else {
207 // Attempt to delete, retrying on not empty errors because we may
208 // need to wait some time for files to be removed from the filesystem.
209 retry(|| delete(&dir, &[]), WinError::DIR_NOT_EMPTY)?;
210 restart = true;
211 }
212 }
213 Ok(())
214}