diff --git a/Cargo.toml b/Cargo.toml index 10836c5b..d0836ff8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ rustix = { version = "0.36.0", features = ["fs"] } nt_version = "0.1.3" [target.'cfg(windows)'.dependencies.windows-sys] -version = "0.42.0" +version = "0.45.0" features = [ "Win32_Storage_FileSystem", "Win32_Foundation", diff --git a/cap-directories/Cargo.toml b/cap-directories/Cargo.toml index 54e59f09..e4d3c8d8 100644 --- a/cap-directories/Cargo.toml +++ b/cap-directories/Cargo.toml @@ -20,7 +20,7 @@ directories-next = "2.0.0" rustix = { version = "0.36.0" } [target.'cfg(windows)'.dependencies.windows-sys] -version = "0.42.0" +version = "0.45.0" features = [ "Win32_Foundation", ] diff --git a/cap-fs-ext/Cargo.toml b/cap-fs-ext/Cargo.toml index 0e9136aa..e4d7ba55 100644 --- a/cap-fs-ext/Cargo.toml +++ b/cap-fs-ext/Cargo.toml @@ -33,7 +33,7 @@ std = ["cap-std"] #async_std_arf_strings = ["cap-async-std/arf_strings", "async_std_fs_utf8", "arf-strings"] [target.'cfg(windows)'.dependencies.windows-sys] -version = "0.42.0" +version = "0.45.0" features = [ "Win32_Storage_FileSystem", ] diff --git a/cap-primitives/Cargo.toml b/cap-primitives/Cargo.toml index 0753c1d7..1a8da7d0 100644 --- a/cap-primitives/Cargo.toml +++ b/cap-primitives/Cargo.toml @@ -31,9 +31,12 @@ rustix = { version = "0.36.0", features = ["fs", "process", "procfs", "termios", winx = "0.34.0" [target.'cfg(windows)'.dependencies.windows-sys] -version = "0.42.0" +version = "0.45.0" features = [ - "Win32_Storage_FileSystem", "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_Kernel", "Win32_System_SystemServices", + "Win32_System_WindowsProgramming", ] diff --git a/cap-primitives/src/fs/maybe_owned_file.rs b/cap-primitives/src/fs/maybe_owned_file.rs index aa525b1d..2e1a888c 100644 --- a/cap-primitives/src/fs/maybe_owned_file.rs +++ b/cap-primitives/src/fs/maybe_owned_file.rs @@ -106,6 +106,7 @@ impl<'borrow> MaybeOwnedFile<'borrow> { /// Produce an owned `File`. This uses `open` on "." if needed to convert a /// borrowed `File` to an owned one. + #[cfg_attr(windows, allow(dead_code))] pub(super) fn into_file(self, options: &OpenOptions) -> io::Result { match self.inner { MaybeOwned::Owned(file) => Ok(file), diff --git a/cap-primitives/src/windows/fs/create_file_at_w.rs b/cap-primitives/src/windows/fs/create_file_at_w.rs new file mode 100644 index 00000000..b0236447 --- /dev/null +++ b/cap-primitives/src/windows/fs/create_file_at_w.rs @@ -0,0 +1,227 @@ +#![allow(unsafe_code)] + +use std::mem; +use std::os::windows::io::HandleOrInvalid; +use std::ptr::null_mut; +use windows_sys::core::PCWSTR; +use windows_sys::Win32::Foundation::{ + RtlNtStatusToDosError, SetLastError, ERROR_ALREADY_EXISTS, ERROR_FILE_EXISTS, + ERROR_INVALID_PARAMETER, ERROR_PATH_NOT_FOUND, HANDLE, INVALID_HANDLE_VALUE, NTSTATUS, + STATUS_OBJECT_NAME_COLLISION, SUCCESS, UNICODE_STRING, +}; +use windows_sys::Win32::Security::{ + SECURITY_ATTRIBUTES, SECURITY_DYNAMIC_TRACKING, SECURITY_QUALITY_OF_SERVICE, + SECURITY_STATIC_TRACKING, +}; +use windows_sys::Win32::Storage::FileSystem::{ + NtCreateFile, CREATE_ALWAYS, CREATE_NEW, DELETE, FILE_ACCESS_FLAGS, FILE_ATTRIBUTE_ARCHIVE, + FILE_ATTRIBUTE_COMPRESSED, FILE_ATTRIBUTE_DEVICE, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_EA, + FILE_ATTRIBUTE_ENCRYPTED, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_INTEGRITY_STREAM, + FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_NOT_CONTENT_INDEXED, FILE_ATTRIBUTE_NO_SCRUB_DATA, + FILE_ATTRIBUTE_OFFLINE, FILE_ATTRIBUTE_PINNED, FILE_ATTRIBUTE_READONLY, + FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS, FILE_ATTRIBUTE_RECALL_ON_OPEN, + FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_SPARSE_FILE, FILE_ATTRIBUTE_SYSTEM, + FILE_ATTRIBUTE_TEMPORARY, FILE_ATTRIBUTE_UNPINNED, FILE_ATTRIBUTE_VIRTUAL, FILE_CREATE, + FILE_CREATION_DISPOSITION, FILE_FLAGS_AND_ATTRIBUTES, FILE_FLAG_BACKUP_SEMANTICS, + FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_NO_BUFFERING, FILE_FLAG_OVERLAPPED, + FILE_FLAG_RANDOM_ACCESS, FILE_FLAG_SEQUENTIAL_SCAN, FILE_FLAG_WRITE_THROUGH, FILE_OPEN, + FILE_OPEN_IF, FILE_OVERWRITE, FILE_OVERWRITE_IF, FILE_READ_ATTRIBUTES, FILE_SHARE_MODE, + OPEN_ALWAYS, OPEN_EXISTING, SECURITY_CONTEXT_TRACKING, SECURITY_EFFECTIVE_ONLY, + SECURITY_SQOS_PRESENT, SYNCHRONIZE, TRUNCATE_EXISTING, +}; +use windows_sys::Win32::System::Kernel::{OBJ_CASE_INSENSITIVE, OBJ_INHERIT}; +use windows_sys::Win32::System::WindowsProgramming::{ + RtlFreeUnicodeString, RtlInitUnicodeString, FILE_DELETE_ON_CLOSE, FILE_NON_DIRECTORY_FILE, + FILE_NO_INTERMEDIATE_BUFFERING, FILE_OPENED, FILE_OPEN_FOR_BACKUP_INTENT, FILE_OVERWRITTEN, + FILE_RANDOM_ACCESS, FILE_SEQUENTIAL_ONLY, FILE_SYNCHRONOUS_IO_NONALERT, FILE_WRITE_THROUGH, + IO_STATUS_BLOCK, OBJECT_ATTRIBUTES, +}; + +// All currently known `FILE_ATTRIBUTE_*` constants, according to +// windows-sys' documentation. +const FILE_ATTRIBUTE_VALID_FLAGS: FILE_FLAGS_AND_ATTRIBUTES = FILE_ATTRIBUTE_EA + | FILE_ATTRIBUTE_DEVICE + | FILE_ATTRIBUTE_HIDDEN + | FILE_ATTRIBUTE_NORMAL + | FILE_ATTRIBUTE_PINNED + | FILE_ATTRIBUTE_SYSTEM + | FILE_ATTRIBUTE_ARCHIVE + | FILE_ATTRIBUTE_OFFLINE + | FILE_ATTRIBUTE_VIRTUAL + | FILE_ATTRIBUTE_READONLY + | FILE_ATTRIBUTE_UNPINNED + | FILE_ATTRIBUTE_DIRECTORY + | FILE_ATTRIBUTE_ENCRYPTED + | FILE_ATTRIBUTE_TEMPORARY + | FILE_ATTRIBUTE_COMPRESSED + | FILE_ATTRIBUTE_SPARSE_FILE + | FILE_ATTRIBUTE_NO_SCRUB_DATA + | FILE_ATTRIBUTE_REPARSE_POINT + | FILE_ATTRIBUTE_RECALL_ON_OPEN + | FILE_ATTRIBUTE_INTEGRITY_STREAM + | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED + | FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS; + +#[allow(non_snake_case)] +pub unsafe fn CreateFileAtW( + dir: HANDLE, + lpfilename: PCWSTR, + dwdesiredaccess: FILE_ACCESS_FLAGS, + dwsharemode: FILE_SHARE_MODE, + lpsecurityattributes: *const SECURITY_ATTRIBUTES, + dwcreationdisposition: FILE_CREATION_DISPOSITION, + dwflagsandattributes: FILE_FLAGS_AND_ATTRIBUTES, + _htemplatefile: HANDLE, +) -> HandleOrInvalid { + // Check for a null or empty filename. + if lpfilename.is_null() || *lpfilename == 0 { + SetLastError(ERROR_PATH_NOT_FOUND); + return HandleOrInvalid::from_raw_handle(INVALID_HANDLE_VALUE as _); + } + + // Convert `dwcreationdisposition` to the `createdisposition` argument + // to `NtCreateFile`. Do this before converting `lpfilename` so that + // we can return without having to free anything. + let createdisposition = match dwcreationdisposition { + CREATE_NEW => FILE_CREATE, + CREATE_ALWAYS => FILE_OVERWRITE_IF, + OPEN_EXISTING => FILE_OPEN, + OPEN_ALWAYS => FILE_OPEN_IF, + TRUNCATE_EXISTING => FILE_OVERWRITE, + _ => { + SetLastError(ERROR_INVALID_PARAMETER); + return HandleOrInvalid::from_raw_handle(INVALID_HANDLE_VALUE as _); + } + }; + + // Convert `lpfilename` to a `UNICODE_STRING`. After this, we'll need to + // call `RtlFreeUnicodeString` before returning. + let mut unicode_string = mem::zeroed::(); + RtlInitUnicodeString(&mut unicode_string, lpfilename); + + let mut handle = INVALID_HANDLE_VALUE; + + // Convert `dwdesiredaccess` and `dwflagsandattributes` to the + // `desiredaccess` argument to `NtCreateFile`. + let mut desiredaccess = dwdesiredaccess | SYNCHRONIZE | FILE_READ_ATTRIBUTES; + if dwflagsandattributes & FILE_FLAG_DELETE_ON_CLOSE != 0 { + desiredaccess |= DELETE; + } + + // Compute `objectattributes`' `Attributes` field. Case-insensitive is + // the expected behavior on Windows. + let mut attributes = OBJ_CASE_INSENSITIVE as _; + if !lpsecurityattributes.is_null() && (*lpsecurityattributes).bInheritHandle != 0 { + attributes |= OBJ_INHERIT as u32; + } + + // Compute the `objectattributes` argument to `NtCreateFile`. + let mut objectattributes = mem::zeroed::(); + objectattributes.Length = mem::size_of_val(&objectattributes) as _; + objectattributes.RootDirectory = dir; + objectattributes.ObjectName = &mut unicode_string; + objectattributes.Attributes = attributes; + if !lpsecurityattributes.is_null() { + objectattributes.SecurityDescriptor = (*lpsecurityattributes).lpSecurityDescriptor; + } + + // If needed, set `objectattributes`' `SecurityQualityOfService` field. + let mut qos; + if dwflagsandattributes & SECURITY_SQOS_PRESENT != 0 { + qos = mem::zeroed::(); + qos.Length = mem::size_of_val(&qos) as _; + qos.ImpersonationLevel = ((dwflagsandattributes >> 16) & 0x3) as _; + qos.ContextTrackingMode = if dwflagsandattributes & SECURITY_CONTEXT_TRACKING != 0 { + SECURITY_DYNAMIC_TRACKING + } else { + SECURITY_STATIC_TRACKING + }; + qos.EffectiveOnly = ((dwflagsandattributes & SECURITY_EFFECTIVE_ONLY) != 0) as _; + + objectattributes.SecurityQualityOfService = + (&mut qos as *mut SECURITY_QUALITY_OF_SERVICE).cast(); + } + + let mut iostatusblock = mem::zeroed::(); + + // Compute the `fileattributes` argument to `NtCreateFile`. Mask off + // unrecognized flags. + let fileattributes = dwflagsandattributes & FILE_ATTRIBUTE_VALID_FLAGS; + + // Compute the `createoptions` argument to `NtCreateFile`. + let mut createoptions = 0; + if dwflagsandattributes & FILE_FLAG_BACKUP_SEMANTICS == 0 { + createoptions |= FILE_NON_DIRECTORY_FILE; + } else { + createoptions |= FILE_OPEN_FOR_BACKUP_INTENT; + } + if dwflagsandattributes & FILE_FLAG_DELETE_ON_CLOSE != 0 { + createoptions |= FILE_DELETE_ON_CLOSE; + } + if dwflagsandattributes & FILE_FLAG_NO_BUFFERING != 0 { + createoptions |= FILE_NO_INTERMEDIATE_BUFFERING; + } + if dwflagsandattributes & FILE_FLAG_OVERLAPPED == 0 { + createoptions |= FILE_SYNCHRONOUS_IO_NONALERT; + } + if dwflagsandattributes & FILE_FLAG_RANDOM_ACCESS != 0 { + createoptions |= FILE_RANDOM_ACCESS; + } + if dwflagsandattributes & FILE_FLAG_SEQUENTIAL_SCAN != 0 { + createoptions |= FILE_SEQUENTIAL_ONLY; + } + if dwflagsandattributes & FILE_FLAG_WRITE_THROUGH != 0 { + createoptions |= FILE_WRITE_THROUGH; + } + + // Ok, we have what we need to call `NtCreateFile` now! + let status = NtCreateFile( + &mut handle, + desiredaccess, + &mut objectattributes, + &mut iostatusblock, + null_mut(), + fileattributes, + dwsharemode, + createdisposition, + createoptions, + null_mut(), + 0, + ); + + // Check for errors. + if nt_success(status) { + handle = INVALID_HANDLE_VALUE; + if status == STATUS_OBJECT_NAME_COLLISION { + SetLastError(ERROR_FILE_EXISTS); + } else { + SetLastError(RtlNtStatusToDosError(status)); + } + } else if (dwcreationdisposition == CREATE_ALWAYS + && iostatusblock.Information == FILE_OVERWRITTEN as _) + || (dwcreationdisposition == OPEN_ALWAYS && iostatusblock.Information == FILE_OPENED as _) + { + // Set `ERROR_ALREADY_EXISTS` according to the table for + // `dwCreationDisposition` in the [`CreateFileW` docs]. + // + // [`CreateFileW` docs]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew + SetLastError(ERROR_ALREADY_EXISTS); + } else { + // Otherwise indicate that we succeeded. + SetLastError(SUCCESS); + } + + // Free `unicode_string`. + RtlFreeUnicodeString(&mut unicode_string); + + HandleOrInvalid::from_raw_handle(handle as _) +} + +// The following is derived from Rust's library/std/src/sys/windows/c.rs +// at revision 47e6304e325463bc6608a6f1eb61391fa36dd76a. + +// Equivalent to the `NT_SUCCESS` C preprocessor macro. +// See: https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/using-ntstatus-values +pub fn nt_success(status: NTSTATUS) -> bool { + status >= 0 +} diff --git a/cap-primitives/src/windows/fs/mod.rs b/cap-primitives/src/windows/fs/mod.rs index 3cb4382e..7329ef1c 100644 --- a/cap-primitives/src/windows/fs/mod.rs +++ b/cap-primitives/src/windows/fs/mod.rs @@ -1,5 +1,6 @@ mod copy; mod create_dir_unchecked; +mod create_file_at_w; mod dir_entry_inner; mod dir_options_ext; mod dir_utils; diff --git a/cap-primitives/src/windows/fs/oflags.rs b/cap-primitives/src/windows/fs/oflags.rs index 8f5022a5..c282f21a 100644 --- a/cap-primitives/src/windows/fs/oflags.rs +++ b/cap-primitives/src/windows/fs/oflags.rs @@ -5,10 +5,11 @@ use windows_sys::Win32::Storage::FileSystem::{ FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, }; -/// Translate the given `cap_std` into `std` options. Also return a bool +/// Adjust an `OpenOptions` after all the flags are set, in preparation +/// for the to call a Windows API `open` function. Also return a bool /// indicating that the `trunc` flag was requested but could not be set, /// so the file should be truncated manually after opening. -pub(in super::super) fn open_options_to_std(opts: &OpenOptions) -> (fs::OpenOptions, bool) { +pub(in super::super) fn prepare_open_options_for_open(opts: &mut OpenOptions) -> bool { let mut trunc = opts.truncate; let mut manually_trunc = false; @@ -34,16 +35,30 @@ pub(in super::super) fn open_options_to_std(opts: &OpenOptions) -> (fs::OpenOpti // lookups on Windows. share_mode &= !FILE_SHARE_DELETE; } + opts.truncate(trunc) + .share_mode(share_mode) + .custom_flags(custom_flags); + + manually_trunc +} + +/// Translate the given `cap_std` into `std` options. Also return a bool +/// indicating that the `trunc` flag was requested but could not be set, +/// so the file should be truncated manually after opening. +pub(in super::super) fn open_options_to_std(opts: &OpenOptions) -> (fs::OpenOptions, bool) { + let mut opts = opts.clone(); + let manually_trunc = prepare_open_options_for_open(&mut opts); + let mut std_opts = fs::OpenOptions::new(); std_opts .read(opts.read) .write(opts.write) .append(opts.append) - .truncate(trunc) + .truncate(opts.truncate) .create(opts.create) .create_new(opts.create_new) - .share_mode(share_mode) - .custom_flags(custom_flags) + .share_mode(opts.ext.share_mode) + .custom_flags(opts.ext.custom_flags) .attributes(opts.ext.attributes); // Calling `sequence_qos_flags` with a value of 0 has the side effect diff --git a/cap-primitives/src/windows/fs/open_options_ext.rs b/cap-primitives/src/windows/fs/open_options_ext.rs index 798c88be..599b51a0 100644 --- a/cap-primitives/src/windows/fs/open_options_ext.rs +++ b/cap-primitives/src/windows/fs/open_options_ext.rs @@ -1,6 +1,16 @@ +#![allow(unsafe_code)] + +use crate::fs::OpenOptions; +use std::io; +use std::ptr::null_mut; +use windows_sys::Win32::Foundation::ERROR_INVALID_PARAMETER; +use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; use windows_sys::Win32::Storage::FileSystem::{ - FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, SECURITY_SQOS_PRESENT, + CREATE_ALWAYS, CREATE_NEW, FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_WRITE, FILE_SHARE_DELETE, + FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_WRITE_DATA, OPEN_ALWAYS, OPEN_EXISTING, + SECURITY_SQOS_PRESENT, TRUNCATE_EXISTING, }; +use windows_sys::Win32::System::SystemServices::{GENERIC_READ, GENERIC_WRITE}; #[derive(Debug, Clone)] pub(crate) struct OpenOptionsExt { @@ -8,9 +18,13 @@ pub(crate) struct OpenOptionsExt { pub(super) share_mode: u32, pub(super) custom_flags: u32, pub(super) attributes: u32, + pub(super) security_attributes: *mut SECURITY_ATTRIBUTES, pub(super) security_qos_flags: u32, } +unsafe impl Send for OpenOptionsExt {} +unsafe impl Sync for OpenOptionsExt {} + impl OpenOptionsExt { pub(crate) const fn new() -> Self { Self { @@ -18,6 +32,7 @@ impl OpenOptionsExt { share_mode: FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, custom_flags: 0, attributes: 0, + security_attributes: null_mut(), security_qos_flags: 0, } } @@ -47,3 +62,61 @@ impl OpenOptionsExt { self } } + +pub(crate) fn get_access_mode(options: &OpenOptions) -> io::Result { + match ( + options.read, + options.write, + options.append, + options.ext.access_mode, + ) { + (.., Some(mode)) => Ok(mode), + (true, false, false, None) => Ok(GENERIC_READ), + (false, true, false, None) => Ok(GENERIC_WRITE), + (true, true, false, None) => Ok(GENERIC_READ | GENERIC_WRITE), + (false, _, true, None) => Ok(FILE_GENERIC_WRITE & !FILE_WRITE_DATA), + (true, _, true, None) => Ok(GENERIC_READ | (FILE_GENERIC_WRITE & !FILE_WRITE_DATA)), + (false, false, false, None) => { + Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER as i32)) + } + } +} + +pub(crate) fn get_flags_and_attributes(options: &OpenOptions) -> u32 { + options.ext.custom_flags + | options.ext.attributes + | options.ext.security_qos_flags + | if options.create_new { + FILE_FLAG_OPEN_REPARSE_POINT + } else { + 0 + } +} + +pub(crate) fn get_creation_mode(options: &OpenOptions) -> io::Result { + const ERROR_INVALID_PARAMETER: i32 = 87; + + match (options.write, options.append) { + (true, false) => {} + (false, false) => { + if options.truncate || options.create || options.create_new { + return Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER)); + } + } + (_, true) => { + if options.truncate && !options.create_new { + return Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER)); + } + } + } + + Ok( + match (options.create, options.truncate, options.create_new) { + (false, false, false) => OPEN_EXISTING, + (true, false, false) => OPEN_ALWAYS, + (false, true, false) => TRUNCATE_EXISTING, + (true, true, false) => CREATE_ALWAYS, + (_, _, true) => CREATE_NEW, + }, + ) +} diff --git a/cap-primitives/src/windows/fs/open_unchecked.rs b/cap-primitives/src/windows/fs/open_unchecked.rs index 2aa3ab26..65cf51dd 100644 --- a/cap-primitives/src/windows/fs/open_unchecked.rs +++ b/cap-primitives/src/windows/fs/open_unchecked.rs @@ -1,11 +1,25 @@ -use super::get_path::concatenate; -use super::open_options_to_std; -use crate::fs::{errors, FollowSymlinks, OpenOptions, OpenUncheckedError, SymlinkKind}; +//! Windows implementation of `openat` functionality. + +#![allow(unsafe_code)] + +use super::create_file_at_w::CreateFileAtW; +use super::{open_options_to_std, prepare_open_options_for_open}; +use crate::fs::{ + errors, get_access_mode, get_creation_mode, get_flags_and_attributes, FollowSymlinks, + OpenOptions, OpenUncheckedError, SymlinkKind, +}; use crate::{ambient_authority, AmbientAuthority}; +use std::convert::TryInto; +use std::ffi::OsStr; +use std::os::windows::ffi::OsStrExt; use std::os::windows::fs::MetadataExt; +use std::os::windows::io::{AsRawHandle, OwnedHandle}; use std::path::Path; -use std::{fs, io}; -use windows_sys::Win32::Foundation; +use std::{fs, io, ptr}; +use windows_sys::Win32::Foundation::{ + self, GetLastError, SetLastError, ERROR_INSUFFICIENT_BUFFER, HANDLE, +}; +use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW; use windows_sys::Win32::Storage::FileSystem::{ FILE_ATTRIBUTE_DIRECTORY, FILE_FLAG_OPEN_REPARSE_POINT, }; @@ -17,8 +31,259 @@ pub(crate) fn open_unchecked( path: &Path, options: &OpenOptions, ) -> Result { - let full_path = concatenate(start, path).map_err(OpenUncheckedError::Other)?; - open_ambient_impl(&full_path, options, ambient_authority()) + let _ = ambient_authority; + + // We have the final `OpenOptions`; now prepare it for an `open`. + let mut prepared_opts = options.clone(); + let manually_trunc = prepare_open_options_for_open(&mut prepared_opts); + + handle_open_result(open_at(&start, path, &prepared_opts), &options, manually_trunc) +} + +// The following is derived from Rust's library/std/src/sys/windows/fs.rs +// at revision 56888c1e9b4135b511abd2d8e907099003d12281, except with a +// directory `start` parameter added and using `CreateFileAtW` instead of +// `CreateFileW`. + +fn open_at(start: &fs::File, path: &Path, opts: &OpenOptions) -> io::Result { + let path = maybe_verbatim(path)?; + let handle = unsafe { + CreateFileAtW( + start.as_raw_handle() as HANDLE, + path.as_ptr(), + get_access_mode(opts)?, + opts.ext.share_mode, + opts.ext.security_attributes, + get_creation_mode(opts)?, + get_flags_and_attributes(opts), + 0 as HANDLE, + ) + }; + if let Ok(handle) = handle.try_into() { + Ok(>::from(handle)) + } else { + Err(io::Error::last_os_error()) + } +} + +// The following is derived from Rust's library/std/src/sys/windows/path.rs +// at revision 0fe54d46509abbbe54292d0ff85f8429301be002. + +/// Returns a UTF-16 encoded path capable of bypassing the legacy `MAX_PATH` +/// limits. +/// +/// This path may or may not have a verbatim prefix. +pub(crate) fn maybe_verbatim(path: &Path) -> io::Result> { + // Normally the MAX_PATH is 260 UTF-16 code units (including the NULL). + // However, for APIs such as CreateDirectory[1], the limit is 248. + // + // [1]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createdirectorya#parameters + const LEGACY_MAX_PATH: usize = 248; + // UTF-16 encoded code points, used in parsing and building UTF-16 paths. + // All of these are in the ASCII range so they can be cast directly to `u16`. + const SEP: u16 = b'\\' as _; + const ALT_SEP: u16 = b'/' as _; + const QUERY: u16 = b'?' as _; + const COLON: u16 = b':' as _; + const DOT: u16 = b'.' as _; + const U: u16 = b'U' as _; + const N: u16 = b'N' as _; + const C: u16 = b'C' as _; + + // \\?\ + const VERBATIM_PREFIX: &[u16] = &[SEP, SEP, QUERY, SEP]; + // \??\ + const NT_PREFIX: &[u16] = &[SEP, QUERY, QUERY, SEP]; + // \\?\UNC\ + const UNC_PREFIX: &[u16] = &[SEP, SEP, QUERY, SEP, U, N, C, SEP]; + + let mut path = to_u16s(path)?; + if path.starts_with(VERBATIM_PREFIX) || path.starts_with(NT_PREFIX) || path == &[0] { + // Early return for paths that are already verbatim or empty. + return Ok(path); + } else if path.len() < LEGACY_MAX_PATH { + // Early return if an absolute path is less < 260 UTF-16 code units. + // This is an optimization to avoid calling `GetFullPathNameW` unnecessarily. + match path.as_slice() { + // Starts with `D:`, `D:\`, `D:/`, etc. + // Does not match if the path starts with a `\` or `/`. + [drive, COLON, 0] | [drive, COLON, SEP | ALT_SEP, ..] + if *drive != SEP && *drive != ALT_SEP => + { + return Ok(path); + } + // Starts with `\\`, `//`, etc + [SEP | ALT_SEP, SEP | ALT_SEP, ..] => return Ok(path), + _ => {} + } + } + + // Firstly, get the absolute path using `GetFullPathNameW`. + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew + let lpfilename = path.as_ptr(); + fill_utf16_buf( + // SAFETY: `fill_utf16_buf` ensures the `buffer` and `size` are valid. + // `lpfilename` is a pointer to a null terminated string that is not + // invalidated until after `GetFullPathNameW` returns successfully. + |buffer, size| unsafe { GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()) }, + |mut absolute| { + path.clear(); + + // Secondly, add the verbatim prefix. This is easier here because we know the + // path is now absolute and fully normalized (e.g. `/` has been changed to + // `\`). + let prefix = match absolute { + // C:\ => \\?\C:\ + [_, COLON, SEP, ..] => VERBATIM_PREFIX, + // \\.\ => \\?\ + [SEP, SEP, DOT, SEP, ..] => { + absolute = &absolute[4..]; + VERBATIM_PREFIX + } + // Leave \\?\ and \??\ as-is. + [SEP, SEP, QUERY, SEP, ..] | [SEP, QUERY, QUERY, SEP, ..] => &[], + // \\ => \\?\UNC\ + [SEP, SEP, ..] => { + absolute = &absolute[2..]; + UNC_PREFIX + } + // Anything else we leave alone. + _ => &[], + }; + + path.reserve_exact(prefix.len() + absolute.len() + 1); + path.extend_from_slice(prefix); + path.extend_from_slice(absolute); + path.push(0); + }, + )?; + Ok(path) +} + +// The following is derived from Rust's library/std/src/sys/windows/mod.rs +// at revision a9e5c1a309df80434ebc4c1f6bfaa5cb119b465d, except with the +// optimization in f50f8782fe5d6f617d9c5b20115a7639dc7521bc reverted, to +// avoid depending on Rust nightly features. + +pub fn unrolled_find_u16s(needle: u16, haystack: &[u16]) -> Option { + let ptr = haystack.as_ptr(); + let mut start = &haystack[..]; + + // For performance reasons unfold the loop eight times. + while start.len() >= 8 { + macro_rules! if_return { + ($($n:literal,)+) => { + $( + if start[$n] == needle { + return Some(((&start[$n] as *const u16) as usize - ptr as usize) / 2); + } + )+ + } + } + + if_return!(0, 1, 2, 3, 4, 5, 6, 7,); + + start = &start[8..]; + } + + for c in start { + if *c == needle { + return Some(((c as *const u16) as usize - ptr as usize) / 2); + } + } + None +} + +pub fn to_u16s>(s: S) -> std::io::Result> { + fn inner(s: &OsStr) -> std::io::Result> { + // Most paths are ASCII, so reserve capacity for as much as there are bytes + // in the OsStr plus one for the null-terminating character. We are not + // wasting bytes here as paths created by this function are primarily used + // in an ephemeral fashion. + let mut maybe_result = Vec::with_capacity(s.len() + 1); + maybe_result.extend(s.encode_wide()); + + if unrolled_find_u16s(0, &maybe_result).is_some() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "strings passed to WinAPI cannot contain NULs", + )); + } + maybe_result.push(0); + Ok(maybe_result) + } + inner(s.as_ref()) +} + +// Many Windows APIs follow a pattern of where we hand a buffer and then they +// will report back to us how large the buffer should be or how many bytes +// currently reside in the buffer. This function is an abstraction over these +// functions by making them easier to call. +// +// The first callback, `f1`, is yielded a (pointer, len) pair which can be +// passed to a syscall. The `ptr` is valid for `len` items (u16 in this case). +// The closure is expected to return what the syscall returns which will be +// interpreted by this function to determine if the syscall needs to be invoked +// again (with more buffer space). +// +// Once the syscall has completed (errors bail out early) the second closure is +// yielded the data which has been read from the syscall. The return value +// from this closure is then the return value of the function. +fn fill_utf16_buf(mut f1: F1, f2: F2) -> std::io::Result +where + F1: FnMut(*mut u16, u32) -> u32, + F2: FnOnce(&[u16]) -> T, +{ + // Start off with a stack buf but then spill over to the heap if we end up + // needing more space. + // + // This initial size also works around `GetFullPathNameW` returning + // incorrect size hints for some short paths: + // https://github.com/dylni/normpath/issues/5 + let mut stack_buf = [0u16; 512]; + let mut heap_buf = Vec::new(); + unsafe { + let mut n = stack_buf.len(); + loop { + let buf = if n <= stack_buf.len() { + &mut stack_buf[..] + } else { + let extra = n - heap_buf.len(); + heap_buf.reserve(extra); + heap_buf.set_len(n); + &mut heap_buf[..] + }; + + // This function is typically called on windows API functions which + // will return the correct length of the string, but these functions + // also return the `0` on error. In some cases, however, the + // returned "correct length" may actually be 0! + // + // To handle this case we call `SetLastError` to reset it to 0 and + // then check it again if we get the "0 error value". If the "last + // error" is still 0 then we interpret it as a 0 length buffer and + // not an actual error. + SetLastError(0); + let k = match f1(buf.as_mut_ptr(), n as u32) { + 0 if GetLastError() == 0 => 0, + 0 => return Err(std::io::Error::last_os_error()), + n => n, + } as usize; + if k == n && GetLastError() == ERROR_INSUFFICIENT_BUFFER { + n += 2; + } else if k > n { + n = k; + } else if k == n { + // It is impossible to reach this point. + // On success, k is the returned string length excluding the null. + // On failure, k is the required buffer length including the null. + // Therefore k never equals n. + unreachable!(); + } else { + return Ok(f2(&buf[..k])); + } + } + } } /// *Unsandboxed* function similar to `open_unchecked`, but which just operates @@ -29,8 +294,13 @@ pub(crate) fn open_ambient_impl( ambient_authority: AmbientAuthority, ) -> Result { let _ = ambient_authority; - let (opts, manually_trunc) = open_options_to_std(options); - match opts.open(path) { + let (std_opts, manually_trunc) = open_options_to_std(options); + handle_open_result(std_opts.open(path), &options, manually_trunc) +} + +fn handle_open_result(result: io::Result, options: &OpenOptions, manually_trunc: bool +) -> Result { + match result { Ok(f) => { let enforce_dir = options.dir_required; let enforce_nofollow = options.follow == FollowSymlinks::No diff --git a/cap-primitives/src/windows/fs/reopen_impl.rs b/cap-primitives/src/windows/fs/reopen_impl.rs index f476c9f2..4da4b725 100644 --- a/cap-primitives/src/windows/fs/reopen_impl.rs +++ b/cap-primitives/src/windows/fs/reopen_impl.rs @@ -1,11 +1,10 @@ -use crate::fs::OpenOptions; +use crate::fs::{get_access_mode, get_flags_and_attributes, OpenOptions}; use io_lifetimes::AsHandle; use std::{fs, io}; -use windows_sys::Win32::Foundation::ERROR_INVALID_PARAMETER; use windows_sys::Win32::Storage::FileSystem::{ - FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_WRITE_THROUGH, - FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_WRITE_DATA, SECURITY_CONTEXT_TRACKING, - SECURITY_DELEGATION, SECURITY_EFFECTIVE_ONLY, SECURITY_IDENTIFICATION, SECURITY_IMPERSONATION, + FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_WRITE_THROUGH, FILE_GENERIC_READ, FILE_GENERIC_WRITE, + SECURITY_CONTEXT_TRACKING, SECURITY_DELEGATION, SECURITY_EFFECTIVE_ONLY, + SECURITY_IDENTIFICATION, SECURITY_IMPERSONATION, }; use windows_sys::Win32::System::SystemServices::{GENERIC_READ, GENERIC_WRITE}; use winx::file::{AccessMode, Flags}; @@ -82,33 +81,3 @@ pub(crate) fn reopen_impl(file: &fs::File, options: &OpenOptions) -> io::Result< winx::file::reopen_file(file.as_handle(), new_access_mode, flags) } - -fn get_access_mode(options: &OpenOptions) -> io::Result { - match ( - options.read, - options.write, - options.append, - options.ext.access_mode, - ) { - (.., Some(mode)) => Ok(mode), - (true, false, false, None) => Ok(GENERIC_READ), - (false, true, false, None) => Ok(GENERIC_WRITE), - (true, true, false, None) => Ok(GENERIC_READ | GENERIC_WRITE), - (false, _, true, None) => Ok(FILE_GENERIC_WRITE & !FILE_WRITE_DATA), - (true, _, true, None) => Ok(GENERIC_READ | (FILE_GENERIC_WRITE & !FILE_WRITE_DATA)), - (false, false, false, None) => { - Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER as i32)) - } - } -} - -fn get_flags_and_attributes(options: &OpenOptions) -> u32 { - options.ext.custom_flags - | options.ext.attributes - | options.ext.security_qos_flags - | if options.create_new { - FILE_FLAG_OPEN_REPARSE_POINT - } else { - 0 - } -} diff --git a/cap-tempfile/Cargo.toml b/cap-tempfile/Cargo.toml index 60ce6a96..4be4b6f3 100644 --- a/cap-tempfile/Cargo.toml +++ b/cap-tempfile/Cargo.toml @@ -24,7 +24,7 @@ rand = "0.8.1" rustix = { version = "0.36.0", features = ["procfs"] } [target.'cfg(windows)'.dev-dependencies.windows-sys] -version = "0.42.0" +version = "0.45.0" features = [ "Win32_Foundation", ]