Rollup merge of #131072 - Fulgen301:windows-rename-posix-semantics, r=ChrisDenton

Win: Use POSIX rename semantics for `std::fs::rename` if available

Windows 10 1601 introduced `FileRenameInfoEx` as well as `FILE_RENAME_FLAG_POSIX_SEMANTICS`, allowing for atomic renaming and renaming if the target file is has already been opened with `FILE_SHARE_DELETE`, in which case the file gets renamed on disk while the open file handle still refers to the old file, just like in POSIX. This resolves #123985, where atomic renaming proved difficult to impossible due to race conditions.

If `FileRenameInfoEx` isn't available due to missing support from the underlying filesystem or missing OS support, the renaming is retried with `FileRenameInfo`, which matches the behavior of `MoveFileEx`.

This PR also manually replicates parts of `MoveFileEx`'s internal logic, as reverse-engineered from the disassembly: If the source file is a reparse point and said reparse point is a mount point, the mount point itself gets renamed; otherwise the reparse point is resolved and the result renamed.

Notes:
- Currently, the `win7` target doesn't bother with `FileRenameInfoEx` at all; it's probably desirable to remove that special casing and try `FileRenameInfoEx` anyway if it doesn't exist, in case the binary is run on newer OS versions.

Fixes #123985
This commit is contained in:
Matthias Krüger
2024-12-21 22:16:02 +01:00
committed by GitHub
5 changed files with 199 additions and 4 deletions

View File

@@ -2295,6 +2295,7 @@ Windows.Win32.Storage.FileSystem.FILE_NAME_OPENED
Windows.Win32.Storage.FileSystem.FILE_READ_ATTRIBUTES
Windows.Win32.Storage.FileSystem.FILE_READ_DATA
Windows.Win32.Storage.FileSystem.FILE_READ_EA
Windows.Win32.Storage.FileSystem.FILE_RENAME_INFO
Windows.Win32.Storage.FileSystem.FILE_SHARE_DELETE
Windows.Win32.Storage.FileSystem.FILE_SHARE_MODE
Windows.Win32.Storage.FileSystem.FILE_SHARE_NONE
@@ -2603,5 +2604,7 @@ Windows.Win32.System.Threading.WaitForMultipleObjects
Windows.Win32.System.Threading.WaitForSingleObject
Windows.Win32.System.Threading.WakeAllConditionVariable
Windows.Win32.System.Threading.WakeConditionVariable
Windows.Win32.System.WindowsProgramming.FILE_RENAME_FLAG_POSIX_SEMANTICS
Windows.Win32.System.WindowsProgramming.FILE_RENAME_FLAG_REPLACE_IF_EXISTS
Windows.Win32.System.WindowsProgramming.PROGRESS_CONTINUE
Windows.Win32.UI.Shell.GetUserProfileDirectoryW

View File

@@ -2472,6 +2472,22 @@ pub const FILE_RANDOM_ACCESS: NTCREATEFILE_CREATE_OPTIONS = 2048u32;
pub const FILE_READ_ATTRIBUTES: FILE_ACCESS_RIGHTS = 128u32;
pub const FILE_READ_DATA: FILE_ACCESS_RIGHTS = 1u32;
pub const FILE_READ_EA: FILE_ACCESS_RIGHTS = 8u32;
pub const FILE_RENAME_FLAG_POSIX_SEMANTICS: u32 = 2u32;
pub const FILE_RENAME_FLAG_REPLACE_IF_EXISTS: u32 = 1u32;
#[repr(C)]
#[derive(Clone, Copy)]
pub struct FILE_RENAME_INFO {
pub Anonymous: FILE_RENAME_INFO_0,
pub RootDirectory: HANDLE,
pub FileNameLength: u32,
pub FileName: [u16; 1],
}
#[repr(C)]
#[derive(Clone, Copy)]
pub union FILE_RENAME_INFO_0 {
pub ReplaceIfExists: BOOLEAN,
pub Flags: u32,
}
pub const FILE_RESERVE_OPFILTER: NTCREATEFILE_CREATE_OPTIONS = 1048576u32;
pub const FILE_SEQUENTIAL_ONLY: NTCREATEFILE_CREATE_OPTIONS = 4u32;
pub const FILE_SESSION_AWARE: NTCREATEFILE_CREATE_OPTIONS = 262144u32;

View File

@@ -1,5 +1,6 @@
use super::api::{self, WinError};
use super::{IoResult, to_u16s};
use crate::alloc::{alloc, handle_alloc_error};
use crate::borrow::Cow;
use crate::ffi::{OsStr, OsString, c_void};
use crate::io::{self, BorrowedCursor, Error, IoSlice, IoSliceMut, SeekFrom};
@@ -1223,7 +1224,139 @@ pub fn unlink(p: &Path) -> io::Result<()> {
pub fn rename(old: &Path, new: &Path) -> io::Result<()> {
let old = maybe_verbatim(old)?;
let new = maybe_verbatim(new)?;
cvt(unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) })?;
let new_len_without_nul_in_bytes = (new.len() - 1).try_into().unwrap();
// The last field of FILE_RENAME_INFO, the file name, is unsized,
// and FILE_RENAME_INFO has two padding bytes.
// Therefore we need to make sure to not allocate less than
// size_of::<c::FILE_RENAME_INFO>() bytes, which would be the case with
// 0 or 1 character paths + a null byte.
let struct_size = mem::size_of::<c::FILE_RENAME_INFO>()
.max(mem::offset_of!(c::FILE_RENAME_INFO, FileName) + new.len() * mem::size_of::<u16>());
let struct_size: u32 = struct_size.try_into().unwrap();
let create_file = |extra_access, extra_flags| {
let handle = unsafe {
HandleOrInvalid::from_raw_handle(c::CreateFileW(
old.as_ptr(),
c::SYNCHRONIZE | c::DELETE | extra_access,
c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE,
ptr::null(),
c::OPEN_EXISTING,
c::FILE_ATTRIBUTE_NORMAL | c::FILE_FLAG_BACKUP_SEMANTICS | extra_flags,
ptr::null_mut(),
))
};
OwnedHandle::try_from(handle).map_err(|_| io::Error::last_os_error())
};
// The following code replicates `MoveFileEx`'s behavior as reverse-engineered from its disassembly.
// If `old` refers to a mount point, we move it instead of the target.
let handle = match create_file(c::FILE_READ_ATTRIBUTES, c::FILE_FLAG_OPEN_REPARSE_POINT) {
Ok(handle) => {
let mut file_attribute_tag_info: MaybeUninit<c::FILE_ATTRIBUTE_TAG_INFO> =
MaybeUninit::uninit();
let result = unsafe {
cvt(c::GetFileInformationByHandleEx(
handle.as_raw_handle(),
c::FileAttributeTagInfo,
file_attribute_tag_info.as_mut_ptr().cast(),
mem::size_of::<c::FILE_ATTRIBUTE_TAG_INFO>().try_into().unwrap(),
))
};
if let Err(err) = result {
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _)
|| err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _)
{
// `GetFileInformationByHandleEx` documents that not all underlying drivers support all file information classes.
// Since we know we passed the correct arguments, this means the underlying driver didn't understand our request;
// `MoveFileEx` proceeds by reopening the file without inhibiting reparse point behavior.
None
} else {
Some(Err(err))
}
} else {
// SAFETY: The struct has been initialized by GetFileInformationByHandleEx
let file_attribute_tag_info = unsafe { file_attribute_tag_info.assume_init() };
if file_attribute_tag_info.FileAttributes & c::FILE_ATTRIBUTE_REPARSE_POINT != 0
&& file_attribute_tag_info.ReparseTag != c::IO_REPARSE_TAG_MOUNT_POINT
{
// The file is not a mount point: Reopen the file without inhibiting reparse point behavior.
None
} else {
// The file is a mount point: Don't reopen the file so that the mount point gets renamed.
Some(Ok(handle))
}
}
}
// The underlying driver may not support `FILE_FLAG_OPEN_REPARSE_POINT`: Retry without it.
Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None,
Err(err) => Some(Err(err)),
}
.unwrap_or_else(|| create_file(0, 0))?;
let layout = core::alloc::Layout::from_size_align(
struct_size as _,
mem::align_of::<c::FILE_RENAME_INFO>(),
)
.unwrap();
let file_rename_info = unsafe { alloc(layout) } as *mut c::FILE_RENAME_INFO;
if file_rename_info.is_null() {
handle_alloc_error(layout);
}
// SAFETY: file_rename_info is a non-null pointer pointing to memory allocated by the global allocator.
let mut file_rename_info = unsafe { Box::from_raw(file_rename_info) };
// SAFETY: We have allocated enough memory for a full FILE_RENAME_INFO struct and a filename.
unsafe {
(&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
});
(&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
(&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
new.as_ptr()
.copy_to_nonoverlapping((&raw mut (*file_rename_info).FileName) as *mut u16, new.len());
}
// We don't use `set_file_information_by_handle` here as `FILE_RENAME_INFO` is used for both `FileRenameInfo` and `FileRenameInfoEx`.
let result = unsafe {
cvt(c::SetFileInformationByHandle(
handle.as_raw_handle(),
c::FileRenameInfoEx,
(&raw const *file_rename_info).cast::<c_void>(),
struct_size,
))
};
if let Err(err) = result {
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) {
// FileRenameInfoEx and FILE_RENAME_FLAG_POSIX_SEMANTICS were added in Windows 10 1607; retry with FileRenameInfo.
file_rename_info.Anonymous.ReplaceIfExists = 1;
cvt(unsafe {
c::SetFileInformationByHandle(
handle.as_raw_handle(),
c::FileRenameInfo,
(&raw const *file_rename_info).cast::<c_void>(),
struct_size,
)
})?;
} else {
return Err(err);
}
}
Ok(())
}