From 7b99effaac9fa9f956b8860cb68a018a84c4b6f3 Mon Sep 17 00:00:00 2001 From: Reem Date: Fri, 6 Nov 2026 10:22:25 +0200 Subject: [PATCH 1/2] fix(fspy-shm): map the Windows IPC view above Cygwin's fixed layout The 4 GiB view the injected DLL maps before msys-2.0.dll starts lands on Cygwin's fixed-address shared regions, so MSYS2 programs in tracked tasks fail with Win32 error 487. Ask MapViewOfFile3 for an address at or above Cygwin's mmap arena, falling back to MapViewOfFile where it is unavailable. Fixes #830. --- crates/fspy_nostd/src/mm/windows.rs | 96 +++++++++++++++++++++++++++-- crates/fspy_shm/src/lib.rs | 19 ++++++ crates/fspy_shm/src/windows.rs | 28 +++++++-- 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs index f2a48eb57..47c0a8604 100644 --- a/crates/fspy_nostd/src/mm/windows.rs +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -4,10 +4,18 @@ use core::{ }; use bitflags::bitflags; -use windows_sys::Win32::System::Memory::{ - CreateFileMappingW, FILE_MAP_READ, FILE_MAP_WRITE, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, - MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, PAGE_READWRITE, UnmapViewOfFile, VirtualAlloc, - VirtualFree, +use windows_sys::Win32::{ + Foundation::HANDLE, + System::{ + LibraryLoader::GetProcAddress, + Memory::{ + CreateFileMappingW, FILE_MAP_READ, FILE_MAP_WRITE, MEM_ADDRESS_REQUIREMENTS, + MEM_COMMIT, MEM_EXTENDED_PARAMETER, MEM_EXTENDED_PARAMETER_0, + MEM_EXTENDED_PARAMETER_1, MEM_RELEASE, MEM_RESERVE, MEMORY_MAPPED_VIEW_ADDRESS, + MapViewOfFile, MemExtendedParameterAddressRequirements, PAGE_READWRITE, + UnmapViewOfFile, VIRTUAL_ALLOCATION_TYPE, VirtualAlloc, VirtualFree, + }, + }, }; use crate::{BorrowedHandle, OwnedHandle, Result, SecurityAttributes}; @@ -149,6 +157,86 @@ pub fn create_file_mapping( Ok(unsafe { OwnedHandle::from_raw_handle(mapping) }) } +/// `MapViewOfFile3`, which `kernelbase.dll` exports from Windows 10 1803 on. +type MapViewOfFile3Fn = unsafe extern "system" fn( + file_mapping: HANDLE, + process: HANDLE, + base_address: *const c_void, + offset: u64, + view_size: usize, + allocation_type: VIRTUAL_ALLOCATION_TYPE, + page_protection: u32, + extended_parameters: *mut MEM_EXTENDED_PARAMETER, + parameter_count: u32, +) -> MEMORY_MAPPED_VIEW_ADDRESS; + +/// Looks `MapViewOfFile3` up at run time rather than importing it, so this +/// crate still loads on Windows releases that predate it. +fn map_view_of_file3() -> Option { + let kernelbase = crate::windows::get_module_handle(crate::wide_cstr!("kernelbase.dll")).ok()?; + // SAFETY: `kernelbase` is a loaded module handle and the symbol name is a + // NUL-terminated ANSI string. + let symbol = unsafe { GetProcAddress(kernelbase.as_ptr(), c"MapViewOfFile3".as_ptr().cast()) }?; + // SAFETY: `MapViewOfFile3` has exactly the signature of `MapViewOfFile3Fn`. + Some(unsafe { core::mem::transmute:: isize, MapViewOfFile3Fn>(symbol) }) +} + +/// Maps the whole of `mapping` read-write, at an address no lower than +/// `lowest_address`. +/// +/// Returns `None` when this Windows has no `MapViewOfFile3` (before Windows 10 +/// 1803), so the caller can fall back to [`map_view_of_file`]. +/// +/// # Errors +/// +/// Returns the error reported by `MapViewOfFile3`, for example when no free +/// range of `bytes_to_map` bytes exists above `lowest_address`. +pub fn map_view_of_file_above( + mapping: BorrowedHandle<'_>, + bytes_to_map: usize, + lowest_address: usize, +) -> Option> { + let map_view_of_file3 = map_view_of_file3()?; + let mut requirements = MEM_ADDRESS_REQUIREMENTS { + LowestStartingAddress: ptr::without_provenance_mut(lowest_address), + // Null means the highest address the process can use. + HighestEndingAddress: ptr::null_mut(), + // Zero means the allocation granularity. + Alignment: 0, + }; + let mut parameter = MEM_EXTENDED_PARAMETER { + // The type occupies the low 8 bits. + Anonymous1: MEM_EXTENDED_PARAMETER_0 { + _bitfield: MemExtendedParameterAddressRequirements.cast_unsigned().into(), + }, + Anonymous2: MEM_EXTENDED_PARAMETER_1 { Pointer: (&raw mut requirements).cast() }, + }; + // `GetCurrentProcess()`'s pseudo-handle, which never needs closing. + let current_process: HANDLE = ptr::without_provenance_mut(usize::MAX); + // SAFETY: `mapping` keeps the opaque handle open for the call, and + // `parameter` points at `requirements`, both of which outlive the call. + // Windows validates the handle, size, protection, and address range; any + // mismatch is reported as a null result. + let view = unsafe { + map_view_of_file3( + mapping.as_raw_handle(), + current_process, + ptr::null(), + 0, + bytes_to_map, + 0, + PAGE_READWRITE, + &raw mut parameter, + 1, + ) + }; + Some( + core::ptr::NonNull::new(view.Value.cast::()) + .map(|ptr| MappingView { ptr }) + .ok_or_else(crate::Error::last_os_error), + ) +} + /// Calls `MapViewOfFile` and returns the new owned view. /// /// # Errors diff --git a/crates/fspy_shm/src/lib.rs b/crates/fspy_shm/src/lib.rs index 5070a6fed..bacf233f6 100644 --- a/crates/fspy_shm/src/lib.rs +++ b/crates/fspy_shm/src/lib.rs @@ -252,6 +252,25 @@ mod tests { } } + /// A 4 GiB view placed by the system lands on Cygwin's fixed-address + /// shared regions, which breaks every MSYS2 program a tracked task starts + /// (vite-task#830). Windows 10 1803 and later can place it above them. + #[cfg(windows)] + #[test] + fn four_gib_mapping_lies_above_the_cygwin_fixed_layout() { + const PRODUCTION_SIZE: usize = 4 * 1024 * 1024 * 1024; + + let path = BackingPath::new(); + let handle = create(path.as_c_str(), PRODUCTION_SIZE).unwrap(); + for mapping in [handle.map().unwrap(), open(path.as_c_str()).unwrap().map().unwrap()] { + assert!( + mapping.as_ptr() as usize >= super::windows::ABOVE_CYGWIN_FIXED_LAYOUT, + "view at {:p}", + mapping.as_ptr(), + ); + } + } + #[cfg(windows)] fn backing_file_sizes(path: &BackingPath) -> (u64, u64) { let file = File::open(&path.path).unwrap(); diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index a8c5142b0..2698cc259 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -23,6 +23,12 @@ use windows_sys::Win32::{ System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE}, }; +/// The top of Cygwin's mmap arena (`MMAP_STORAGE_HIGH` in +/// `winsup/cygwin/local_includes/memory_layout.h`). Everything Cygwin places +/// at a fixed address, from the executable at `0x100400000` up through the +/// arena, lies below it, and user space continues to `0x7FFF_FFFE_FFFF`. +pub const ABOVE_CYGWIN_FIXED_LAYOUT: usize = 0x7000_0000_0000; + const SHARE_ALL: FileShare = FileShare::READ.union(FileShare::WRITE).union(FileShare::DELETE); /// Opened shared memory that is not mapped yet. @@ -237,13 +243,25 @@ impl ShmHandle { 0, 0, )?; - let view = fspy_nostd::mm::map_view_of_file( + // Keep the view out of every range an MSYS2/Cygwin process maps at a + // fixed address. The injected DLL maps it before msys-2.0.dll starts, + // and a 4 GiB view placed by the system lands on Cygwin's shared + // regions, so `sh.exe` dies with Win32 error 487 (vite-task#830). + let view = match fspy_nostd::mm::map_view_of_file_above( mapping.as_handle(), - MappingAccess::READ | MappingAccess::WRITE, - 0, - 0, len, - )?; + ABOVE_CYGWIN_FIXED_LAYOUT, + ) { + Some(Ok(view)) => view, + // No `MapViewOfFile3`, or no room up there: let the system choose. + None | Some(Err(_)) => fspy_nostd::mm::map_view_of_file( + mapping.as_handle(), + MappingAccess::READ | MappingAccess::WRITE, + 0, + 0, + len, + )?, + }; // The view remains valid after its mapping-object handle closes. Ok(Mapping { view, len }) } From e3b7d4c253cfe4154a4d87fbd4fa8403d078a842 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 02:07:09 +0000 Subject: [PATCH 2/2] refactor(fspy-nostd): expose MapViewOfFile3 through a typed lookup Replace `map_view_of_file_above`, which returned `Option>` and fixed read-write access and offset 0, with `get_map_view_of_file3()` returning `Option` and `MapViewOfFile3::call`, a thin wrapper that takes protection, offset, size, and an optional lowest address. `None` now only means the function is missing; whether to fall back stays in `fspy_shm`. The value is `Copy`, so callers can keep it without hidden global state in `fspy_nostd`. Document why the lookup is loader-lock safe, use `GetCurrentProcess()` instead of a hand-written pseudo-handle, and test the wrapper in `fspy_nostd` against a paging-file mapping. Add the changelog entry and describe the view placement in the `fspy_shm` README. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01978g7Vcc3w5QWZTswuUsoN --- CHANGELOG.md | 1 + crates/fspy_nostd/Cargo.toml | 1 + crates/fspy_nostd/src/mm/windows.rs | 193 +++++++++++++++++++--------- crates/fspy_shm/README.md | 2 + crates/fspy_shm/src/windows.rs | 27 ++-- 5 files changed, 154 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 505c1af30..8d5cfa856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog +- **Fixed** On Windows, tasks with automatic file-access tracking can now run Git Bash's `sh` and other MSYS2 or Cygwin programs, which aborted with `fatal error - MapViewOfFileEx 'shared.5'(…) Win32 error 487` ([#830](https://github.com/voidzero-dev/vite-task/issues/830), [#952](https://github.com/voidzero-dev/vite-task/pull/952)). - **Fixed** On Linux, cached tasks no longer fail to spawn child processes in restricted sandboxes, for example rootless bubblewrap that denies the `seccomp` syscall. When the `seccomp` filter for file-access tracking cannot be installed, the process runs untracked instead and the run is reported as not cached. The not-cached message now reads "automatic file tracking couldn't record everything this task did", which fits both this case and a full tracking channel ([#700](https://github.com/voidzero-dev/vite-task/issues/700), [#701](https://github.com/voidzero-dev/vite-task/pull/701)). - **Fixed** `vp run ` in a package that doesn't define the task now reports `Task "" not found` when another package's `vite.config.*` fails to load, instead of failing with that package's error. Showing the tasks to choose from loads every package's config, so the task selector or task list is skipped then ([#987](https://github.com/voidzero-dev/vite-task/pull/987)). - **Fixed** A task specifier like `vp run nope#build`, where no workspace package is named `nope`, now fails with `Package 'nope' not found in the workspace` and exits 1, including from a task's command, instead of running nothing and exiting 0. A short name that auto-completes to several scoped packages, like `app` for `@a/app` and `@b/app`, now fails as ambiguous. `--filter nope` still only warns and exits 0, unless `--fail-if-no-match` is set ([#984](https://github.com/voidzero-dev/vite-task/pull/984)). diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index bddf0046d..6ad5ef3b0 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -30,6 +30,7 @@ windows-sys = { workspace = true, features = [ "Win32_Storage_FileSystem", "Win32_System_LibraryLoader", "Win32_System_Memory", + "Win32_System_Threading", ] } [lints] diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs index 47c0a8604..bd1a4927a 100644 --- a/crates/fspy_nostd/src/mm/windows.rs +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -10,11 +10,12 @@ use windows_sys::Win32::{ LibraryLoader::GetProcAddress, Memory::{ CreateFileMappingW, FILE_MAP_READ, FILE_MAP_WRITE, MEM_ADDRESS_REQUIREMENTS, - MEM_COMMIT, MEM_EXTENDED_PARAMETER, MEM_EXTENDED_PARAMETER_0, - MEM_EXTENDED_PARAMETER_1, MEM_RELEASE, MEM_RESERVE, MEMORY_MAPPED_VIEW_ADDRESS, - MapViewOfFile, MemExtendedParameterAddressRequirements, PAGE_READWRITE, - UnmapViewOfFile, VIRTUAL_ALLOCATION_TYPE, VirtualAlloc, VirtualFree, + MEM_COMMIT, MEM_EXTENDED_PARAMETER, MEM_EXTENDED_PARAMETER_0, MEM_EXTENDED_PARAMETER_1, + MEM_RELEASE, MEM_RESERVE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, + MemExtendedParameterAddressRequirements, PAGE_READWRITE, UnmapViewOfFile, + VIRTUAL_ALLOCATION_TYPE, VirtualAlloc, VirtualFree, }, + Threading::GetCurrentProcess, }, }; @@ -157,7 +158,7 @@ pub fn create_file_mapping( Ok(unsafe { OwnedHandle::from_raw_handle(mapping) }) } -/// `MapViewOfFile3`, which `kernelbase.dll` exports from Windows 10 1803 on. +/// The signature of `MapViewOfFile3`. type MapViewOfFile3Fn = unsafe extern "system" fn( file_mapping: HANDLE, process: HANDLE, @@ -170,71 +171,96 @@ type MapViewOfFile3Fn = unsafe extern "system" fn( parameter_count: u32, ) -> MEMORY_MAPPED_VIEW_ADDRESS; -/// Looks `MapViewOfFile3` up at run time rather than importing it, so this +/// `MapViewOfFile3`, which `kernelbase.dll` exports from Windows 10 1803 on. +/// +/// A value proves that this process has the function; only +/// [`get_map_view_of_file3`] creates one. The value is `Copy`, so a caller +/// that maps often can keep it instead of looking the function up again. +#[derive(Clone, Copy)] +pub struct MapViewOfFile3 { + function: MapViewOfFile3Fn, +} + +/// Looks [`MapViewOfFile3`] up at run time rather than importing it, so this /// crate still loads on Windows releases that predate it. -fn map_view_of_file3() -> Option { +/// +/// The lookup is safe under the loader lock: `kernelbase.dll` is loaded before +/// any other DLL's `DllMain` runs, and neither `GetModuleHandleW` nor +/// `GetProcAddress` loads a module, since `kernelbase.dll` implements +/// `MapViewOfFile3` rather than forwarding it. +/// +/// Returns `None` before Windows 10 1803. +#[must_use] +pub fn get_map_view_of_file3() -> Option { let kernelbase = crate::windows::get_module_handle(crate::wide_cstr!("kernelbase.dll")).ok()?; // SAFETY: `kernelbase` is a loaded module handle and the symbol name is a // NUL-terminated ANSI string. let symbol = unsafe { GetProcAddress(kernelbase.as_ptr(), c"MapViewOfFile3".as_ptr().cast()) }?; // SAFETY: `MapViewOfFile3` has exactly the signature of `MapViewOfFile3Fn`. - Some(unsafe { core::mem::transmute:: isize, MapViewOfFile3Fn>(symbol) }) + let function = unsafe { + core::mem::transmute:: isize, MapViewOfFile3Fn>(symbol) + }; + Some(MapViewOfFile3 { function }) } -/// Maps the whole of `mapping` read-write, at an address no lower than -/// `lowest_address`. -/// -/// Returns `None` when this Windows has no `MapViewOfFile3` (before Windows 10 -/// 1803), so the caller can fall back to [`map_view_of_file`]. -/// -/// # Errors -/// -/// Returns the error reported by `MapViewOfFile3`, for example when no free -/// range of `bytes_to_map` bytes exists above `lowest_address`. -pub fn map_view_of_file_above( - mapping: BorrowedHandle<'_>, - bytes_to_map: usize, - lowest_address: usize, -) -> Option> { - let map_view_of_file3 = map_view_of_file3()?; - let mut requirements = MEM_ADDRESS_REQUIREMENTS { - LowestStartingAddress: ptr::without_provenance_mut(lowest_address), - // Null means the highest address the process can use. - HighestEndingAddress: ptr::null_mut(), - // Zero means the allocation granularity. - Alignment: 0, - }; - let mut parameter = MEM_EXTENDED_PARAMETER { - // The type occupies the low 8 bits. - Anonymous1: MEM_EXTENDED_PARAMETER_0 { - _bitfield: MemExtendedParameterAddressRequirements.cast_unsigned().into(), - }, - Anonymous2: MEM_EXTENDED_PARAMETER_1 { Pointer: (&raw mut requirements).cast() }, - }; - // `GetCurrentProcess()`'s pseudo-handle, which never needs closing. - let current_process: HANDLE = ptr::without_provenance_mut(usize::MAX); - // SAFETY: `mapping` keeps the opaque handle open for the call, and - // `parameter` points at `requirements`, both of which outlive the call. - // Windows validates the handle, size, protection, and address range; any - // mismatch is reported as a null result. - let view = unsafe { - map_view_of_file3( - mapping.as_raw_handle(), - current_process, - ptr::null(), - 0, - bytes_to_map, - 0, - PAGE_READWRITE, - &raw mut parameter, - 1, - ) - }; - Some( - core::ptr::NonNull::new(view.Value.cast::()) - .map(|ptr| MappingView { ptr }) - .ok_or_else(crate::Error::last_os_error), - ) +impl MapViewOfFile3 { + /// Calls `MapViewOfFile3` to map `bytes_to_map` bytes of `mapping`, + /// starting at `offset`, into the current process at a system-chosen + /// address, no lower than `lowest_address` when one is given. + /// + /// # Errors + /// + /// Returns the error reported by `MapViewOfFile3`, for example when no + /// free range of `bytes_to_map` bytes exists above `lowest_address`. + pub fn call( + self, + mapping: BorrowedHandle<'_>, + protection: PageProtection, + offset: u64, + bytes_to_map: usize, + lowest_address: Option, + ) -> Result { + let mut requirements = MEM_ADDRESS_REQUIREMENTS { + LowestStartingAddress: ptr::without_provenance_mut(lowest_address.unwrap_or(0)), + // Null means the highest address the process can use. + HighestEndingAddress: ptr::null_mut(), + // Zero means the allocation granularity. + Alignment: 0, + }; + let mut parameter = MEM_EXTENDED_PARAMETER { + // The type occupies the low 8 bits. + Anonymous1: MEM_EXTENDED_PARAMETER_0 { + _bitfield: MemExtendedParameterAddressRequirements.cast_unsigned().into(), + }, + Anonymous2: MEM_EXTENDED_PARAMETER_1 { Pointer: (&raw mut requirements).cast() }, + }; + let (parameters, parameter_count) = + if lowest_address.is_some() { (&raw mut parameter, 1) } else { (ptr::null_mut(), 0) }; + // SAFETY: `self.function` is `MapViewOfFile3`. `mapping` keeps the + // opaque handle open for the call, and `parameters` is null or points + // at `parameter`, which points at `requirements`; both outlive the + // call. A null base address makes the system choose a free range, so + // the view cannot alias an existing allocation. Windows validates the + // handle, offset, size, protection, and address range; any mismatch is + // reported as a null result. + let view = unsafe { + (self.function)( + mapping.as_raw_handle(), + GetCurrentProcess(), + ptr::null(), + offset, + bytes_to_map, + 0, + protection as u32, + parameters, + parameter_count, + ) + }; + let Some(ptr) = NonNull::new(view.Value.cast::()) else { + return Err(crate::Error::last_os_error()); + }; + Ok(MappingView { ptr }) + } } /// Calls `MapViewOfFile` and returns the new owned view. @@ -266,3 +292,46 @@ pub fn map_view_of_file( }; Ok(MappingView { ptr }) } + +#[cfg(test)] +mod tests { + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + + use super::*; + + const SIZE: u32 = 64 * 1024; + + fn pagefile_mapping() -> OwnedHandle { + // SAFETY: `CreateFileMappingW` defines `INVALID_HANDLE_VALUE` as a + // request for memory backed by the paging file. + let pagefile = unsafe { BorrowedHandle::borrow_raw(INVALID_HANDLE_VALUE) }; + create_file_mapping(pagefile, None, PageProtection::ReadWrite, 0, SIZE).unwrap() + } + + fn map_with_map_view_of_file3(lowest_address: Option) -> MappingView { + let mapping = pagefile_mapping(); + let view = get_map_view_of_file3() + .expect("Windows 10 1803 and later export MapViewOfFile3") + .call(mapping.as_handle(), PageProtection::ReadWrite, 0, SIZE as usize, lowest_address) + .unwrap(); + // SAFETY: the view is `SIZE` writable bytes that only this test uses. + unsafe { + view.as_ptr().add(SIZE as usize - 1).write(7); + assert_eq!(view.as_ptr().add(SIZE as usize - 1).read(), 7); + } + view + } + + #[test] + fn map_view_of_file3_maps_at_or_above_the_lowest_address() { + const LOWEST_ADDRESS: usize = 0x7000_0000_0000; + + let view = map_with_map_view_of_file3(Some(LOWEST_ADDRESS)); + assert!(view.as_ptr().addr() >= LOWEST_ADDRESS, "view at {:p}", view.as_ptr()); + } + + #[test] + fn map_view_of_file3_maps_without_a_lowest_address() { + let _view = map_with_map_view_of_file3(None); + } +} diff --git a/crates/fspy_shm/README.md b/crates/fspy_shm/README.md index 8d845c060..e5b815f14 100644 --- a/crates/fspy_shm/README.md +++ b/crates/fspy_shm/README.md @@ -40,6 +40,8 @@ The crate is `no_std`: paths arrive as platform C strings and errors are raw OS `remove` unlinks the path on Unix. On Windows it relies on [POSIX delete semantics](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information_ex), which requires NTFS on Windows 10 1607 or newer: the name goes away at once, while existing handles keep working and [mapped views keep the data alive](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-createfilemappingw) until the last one goes away. +On Windows, `map` asks `MapViewOfFile3` for an address at or above `0x7000_0000_0000`, the top of Cygwin's mmap arena. The fspy DLL maps its view before `msys-2.0.dll` starts, and a 4 GiB view placed by the system lands on the address ranges Cygwin and MSYS2 programs such as Git Bash's `sh.exe` claim at fixed addresses, which makes them abort with Win32 error 487 ([#830](https://github.com/voidzero-dev/vite-task/issues/830)). Windows releases before Windows 10 1803 have no `MapViewOfFile3`; there, or when no room is left that high, `map` lets the system choose the address. + ## Options considered | Option | Decision | diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index 2698cc259..6c8554ede 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -247,14 +247,25 @@ impl ShmHandle { // fixed address. The injected DLL maps it before msys-2.0.dll starts, // and a 4 GiB view placed by the system lands on Cygwin's shared // regions, so `sh.exe` dies with Win32 error 487 (vite-task#830). - let view = match fspy_nostd::mm::map_view_of_file_above( - mapping.as_handle(), - len, - ABOVE_CYGWIN_FIXED_LAYOUT, - ) { - Some(Ok(view)) => view, - // No `MapViewOfFile3`, or no room up there: let the system choose. - None | Some(Err(_)) => fspy_nostd::mm::map_view_of_file( + let above_cygwin = fspy_nostd::mm::get_map_view_of_file3().and_then(|map_view_of_file3| { + map_view_of_file3 + .call( + mapping.as_handle(), + PageProtection::ReadWrite, + 0, + len, + Some(ABOVE_CYGWIN_FIXED_LAYOUT), + ) + .ok() + }); + let view = match above_cygwin { + Some(view) => view, + // Before Windows 10 1803 there is no `MapViewOfFile3`. A failure + // above Cygwin's layout, such as a 64-bit process linked without + // `/LARGEADDRESSAWARE`, rarely leaves room lower down either, but + // letting the system choose costs nothing and keeps the old + // behavior. + None => fspy_nostd::mm::map_view_of_file( mapping.as_handle(), MappingAccess::READ | MappingAccess::WRITE, 0,