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}