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}