1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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(())
}