Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 <task>` in a package that doesn't define the task now reports `Task "<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)).
Expand Down
1 change: 1 addition & 0 deletions crates/fspy_nostd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ windows-sys = { workspace = true, features = [
"Win32_Storage_FileSystem",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Threading",
] }

[lints]
Expand Down
165 changes: 161 additions & 4 deletions crates/fspy_nostd/src/mm/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ 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,
},
Threading::GetCurrentProcess,
},
};

use crate::{BorrowedHandle, OwnedHandle, Result, SecurityAttributes};
Expand Down Expand Up @@ -149,6 +158,111 @@ pub fn create_file_mapping(
Ok(unsafe { OwnedHandle::from_raw_handle(mapping) })
}

/// The signature of `MapViewOfFile3`.
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;

/// `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.
///
/// 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<MapViewOfFile3> {
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`.
let function = unsafe {
core::mem::transmute::<unsafe extern "system" fn() -> isize, MapViewOfFile3Fn>(symbol)
};
Some(MapViewOfFile3 { function })
}

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<usize>,
) -> Result<MappingView> {
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::<u8>()) else {
return Err(crate::Error::last_os_error());
};
Ok(MappingView { ptr })
}
}

/// Calls `MapViewOfFile` and returns the new owned view.
///
/// # Errors
Expand Down Expand Up @@ -178,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<usize>) -> 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);
}
}
2 changes: 2 additions & 0 deletions crates/fspy_shm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
19 changes: 19 additions & 0 deletions crates/fspy_shm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
43 changes: 36 additions & 7 deletions crates/fspy_shm/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -237,13 +243,36 @@ impl ShmHandle {
0,
0,
)?;
let view = fspy_nostd::mm::map_view_of_file(
mapping.as_handle(),
MappingAccess::READ | MappingAccess::WRITE,
0,
0,
len,
)?;
// 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 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,
0,
len,
)?,
};
// The view remains valid after its mapping-object handle closes.
Ok(Mapping { view, len })
}
Expand Down
Loading