From 346155fb76144ab8f2f74118d88178565b9e2101 Mon Sep 17 00:00:00 2001 From: Mathieu Gaspard Date: Mon, 17 Aug 2026 09:41:18 +0200 Subject: [PATCH 1/3] Add no_probe_handle option to skip protection probing on Windows. By default, creating a file-backed mapping on Windows issues up to two extra CreateFileMappingW calls to probe whether the handle supports write and/or execute access, so that the section is created with the widest protection the handle allows and the view can later be transitioned with make_mut()/make_exec(). A side effect is that mapping a file read-only through a writable handle still creates a writable section on it. With MmapOptions::no_probe_handle() set, the section and view are created in a single call each, with exactly the protection the mapping type requires (e.g. PAGE_READONLY/FILE_MAP_READ for map()). This guarantees a read-only mapping never creates a writable section, at the cost that transitioning the returned mapping to a wider protection fails. --- src/lib.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/stub.rs | 23 +++++++++++++--- src/unix.rs | 5 ++++ src/windows.rs | 19 ++++++++----- 4 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f0c7605a..1fb9e06c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,6 +160,7 @@ pub struct MmapOptions { stack: bool, populate: bool, no_reserve_swap: bool, + no_probe_handle: bool, } impl MmapOptions { @@ -392,6 +393,44 @@ impl MmapOptions { self } + /// Do not probe the file handle for the widest supported protection + /// level when creating the mapping. + /// + /// This option only has an effect on Windows. There, by default, + /// creating a mapping issues up to two extra `CreateFileMappingW` calls + /// to discover whether the handle supports write and/or execute access, + /// so that the underlying section is created with the widest protection + /// the handle supports and the mapping can later be transitioned, e.g. + /// with [`Mmap::make_mut()`]. + /// + /// With this option set, the section and view are created with exactly + /// the protection the mapping type requires (e.g. `PAGE_READONLY` and + /// `FILE_MAP_READ` for [`map()`][MmapOptions::map]). This guarantees a + /// read-only mapping never creates a writable section on the file, but + /// transitioning the returned mapping to a wider protection will fail. + /// + /// # Example + /// + /// ``` + /// use memmap2::MmapOptions; + /// use std::fs::File; + /// + /// # fn main() -> std::io::Result<()> { + /// let file = File::open("LICENSE-MIT")?; + /// + /// let mmap = unsafe { + /// MmapOptions::new().no_probe_handle().map(&file)? + /// }; + /// + /// assert_eq!(&b"Copyright"[..], &mmap[..9]); + /// # Ok(()) + /// # } + /// ``` + pub fn no_probe_handle(&mut self) -> &mut Self { + self.no_probe_handle = true; + self + } + /// Creates a read-only memory map backed by a file. /// /// # Safety @@ -435,6 +474,7 @@ impl MmapOptions { self.offset, self.populate, self.no_reserve_swap, + !self.no_probe_handle, ) .map(|inner| Mmap { inner }) } @@ -460,6 +500,7 @@ impl MmapOptions { self.offset, self.populate, self.no_reserve_swap, + !self.no_probe_handle, ) .map(|inner| Mmap { inner }) } @@ -509,6 +550,7 @@ impl MmapOptions { self.offset, self.populate, self.no_reserve_swap, + !self.no_probe_handle, ) .map(|inner| MmapMut { inner }) } @@ -552,6 +594,7 @@ impl MmapOptions { self.offset, self.populate, self.no_reserve_swap, + !self.no_probe_handle, ) .map(|inner| MmapMut { inner }) } @@ -599,6 +642,7 @@ impl MmapOptions { self.offset, self.populate, self.no_reserve_swap, + !self.no_probe_handle, ) .map(|inner| Mmap { inner }) } @@ -648,6 +692,7 @@ impl MmapOptions { self.offset, self.populate, self.no_reserve_swap, + !self.no_probe_handle, ) .map(|inner| MmapRaw { inner }) } @@ -671,6 +716,7 @@ impl MmapOptions { self.offset, self.populate, self.no_reserve_swap, + !self.no_probe_handle, ) .map(|inner| MmapRaw { inner }) } @@ -1850,6 +1896,34 @@ mod test { assert_eq!(nulls, &read); } + #[test] + fn map_without_handle_probing() { + let tempdir = tempfile::tempdir().unwrap(); + let path = tempdir.path().join("mmap"); + + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(&path) + .unwrap(); + file.write_all(b"exact contents").unwrap(); + + let readonly = File::open(&path).unwrap(); + let mmap = unsafe { MmapOptions::new().no_probe_handle().map(&readonly).unwrap() }; + assert_eq!(b"exact contents", &mmap[..]); + + // Without probing, the section is created strictly read-only even + // for a writable handle, so the mapping cannot be transitioned to + // writable. + #[cfg(windows)] + { + let mmap = unsafe { MmapOptions::new().no_probe_handle().map(&file).unwrap() }; + assert!(mmap.make_mut().is_err()); + } + } + #[test] fn map_offset() { let tempdir = tempfile::tempdir().unwrap(); diff --git a/src/stub.rs b/src/stub.rs index aa2e527f..e103fa08 100644 --- a/src/stub.rs +++ b/src/stub.rs @@ -13,19 +13,33 @@ impl MmapInner { Err(io::ErrorKind::Unsupported.into()) } - pub fn map(_: usize, _: &File, _: u64, _: bool, _: bool) -> io::Result { + pub fn map(_: usize, _: &File, _: u64, _: bool, _: bool, _: bool) -> io::Result { MmapInner::new() } - pub fn map_exec(_: usize, _: &File, _: u64, _: bool, _: bool) -> io::Result { + pub fn map_exec( + _: usize, + _: &File, + _: u64, + _: bool, + _: bool, + _: bool, + ) -> io::Result { MmapInner::new() } - pub fn map_mut(_: usize, _: &File, _: u64, _: bool, _: bool) -> io::Result { + pub fn map_mut(_: usize, _: &File, _: u64, _: bool, _: bool, _: bool) -> io::Result { MmapInner::new() } - pub fn map_copy(_: usize, _: &File, _: u64, _: bool, _: bool) -> io::Result { + pub fn map_copy( + _: usize, + _: &File, + _: u64, + _: bool, + _: bool, + _: bool, + ) -> io::Result { MmapInner::new() } @@ -35,6 +49,7 @@ impl MmapInner { _: u64, _: bool, _: bool, + _: bool, ) -> io::Result { MmapInner::new() } diff --git a/src/unix.rs b/src/unix.rs index 6c63e108..32091ae2 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -248,6 +248,7 @@ impl MmapInner { offset: u64, populate: bool, no_reserve: bool, + _probe_handle: bool, ) -> io::Result { let populate = if populate { MAP_POPULATE } else { 0 }; let no_reserve = if no_reserve { MAP_NORESERVE } else { 0 }; @@ -266,6 +267,7 @@ impl MmapInner { offset: u64, populate: bool, no_reserve: bool, + _probe_handle: bool, ) -> io::Result { let populate = if populate { MAP_POPULATE } else { 0 }; let no_reserve = if no_reserve { MAP_NORESERVE } else { 0 }; @@ -284,6 +286,7 @@ impl MmapInner { offset: u64, populate: bool, no_reserve: bool, + _probe_handle: bool, ) -> io::Result { let populate = if populate { MAP_POPULATE } else { 0 }; let no_reserve = if no_reserve { MAP_NORESERVE } else { 0 }; @@ -302,6 +305,7 @@ impl MmapInner { offset: u64, populate: bool, no_reserve: bool, + _probe_handle: bool, ) -> io::Result { let populate = if populate { MAP_POPULATE } else { 0 }; let no_reserve = if no_reserve { MAP_NORESERVE } else { 0 }; @@ -320,6 +324,7 @@ impl MmapInner { offset: u64, populate: bool, no_reserve: bool, + _probe_handle: bool, ) -> io::Result { let populate = if populate { MAP_POPULATE } else { 0 }; let no_reserve = if no_reserve { MAP_NORESERVE } else { 0 }; diff --git a/src/windows.rs b/src/windows.rs index a9f9a239..33129b90 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -234,9 +234,10 @@ impl MmapInner { offset: u64, _populate: bool, _no_reserve: bool, + probe_handle: bool, ) -> io::Result { - let write = protection_supported(handle, PAGE_READWRITE); - let exec = protection_supported(handle, PAGE_EXECUTE_READ); + let write = probe_handle && protection_supported(handle, PAGE_READWRITE); + let exec = probe_handle && protection_supported(handle, PAGE_EXECUTE_READ); let mut access = FILE_MAP_READ; let protection = match (write, exec) { (true, true) => { @@ -267,8 +268,9 @@ impl MmapInner { offset: u64, _populate: bool, _no_reserve: bool, + probe_handle: bool, ) -> io::Result { - let write = protection_supported(handle, PAGE_READWRITE); + let write = probe_handle && protection_supported(handle, PAGE_READWRITE); let mut access = FILE_MAP_READ | FILE_MAP_EXECUTE; let protection = if write { access |= FILE_MAP_WRITE; @@ -290,8 +292,9 @@ impl MmapInner { offset: u64, _populate: bool, _no_reserve: bool, + probe_handle: bool, ) -> io::Result { - let exec = protection_supported(handle, PAGE_EXECUTE_READ); + let exec = probe_handle && protection_supported(handle, PAGE_EXECUTE_READ); let mut access = FILE_MAP_READ | FILE_MAP_WRITE; let protection = if exec { access |= FILE_MAP_EXECUTE; @@ -313,8 +316,9 @@ impl MmapInner { offset: u64, _populate: bool, _no_reserve: bool, + probe_handle: bool, ) -> io::Result { - let exec = protection_supported(handle, PAGE_EXECUTE_READWRITE); + let exec = probe_handle && protection_supported(handle, PAGE_EXECUTE_READWRITE); let mut access = FILE_MAP_COPY; let protection = if exec { access |= FILE_MAP_EXECUTE; @@ -336,9 +340,10 @@ impl MmapInner { offset: u64, _populate: bool, _no_reserve: bool, + probe_handle: bool, ) -> io::Result { - let write = protection_supported(handle, PAGE_READWRITE); - let exec = protection_supported(handle, PAGE_EXECUTE_READ); + let write = probe_handle && protection_supported(handle, PAGE_READWRITE); + let exec = probe_handle && protection_supported(handle, PAGE_EXECUTE_READ); let mut access = FILE_MAP_COPY; let protection = if exec { access |= FILE_MAP_EXECUTE; From ff56b907c0bc790917816b1890540b6f1d0b14ed Mon Sep 17 00:00:00 2001 From: Mathieu Gaspard Date: Mon, 17 Aug 2026 09:45:59 +0200 Subject: [PATCH 2/3] Cache the allocation granularity on Windows. GetSystemInfo is implemented as two NtQuerySystemInformation system calls, and allocation_granularity() is called at least twice over a mapping's lifetime (creation and unmapping on drop), plus once per flush. Cache the value in a static, the same way page_size() already is on unix; the racy initialization is benign since GetSystemInfo is idempotent and the granularity is constant for the lifetime of the system. --- src/windows.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/windows.rs b/src/windows.rs index 33129b90..f037b1e2 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -5,6 +5,7 @@ use std::fs::File; use std::mem::ManuallyDrop; use std::os::raw::c_void; use std::os::windows::io::{FromRawHandle, RawHandle}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::{io, mem, ptr}; type BOOL = i32; @@ -533,10 +534,21 @@ fn protection_supported(handle: RawHandle, protection: DWORD) -> bool { } fn allocation_granularity() -> usize { - unsafe { - let mut info = mem::zeroed(); - GetSystemInfo(&mut info); - info.dwAllocationGranularity as usize + static ALLOCATION_GRANULARITY: AtomicUsize = AtomicUsize::new(0); + + match ALLOCATION_GRANULARITY.load(Ordering::Relaxed) { + 0 => { + let allocation_granularity = unsafe { + let mut info = mem::zeroed(); + GetSystemInfo(&mut info); + info.dwAllocationGranularity as usize + }; + + ALLOCATION_GRANULARITY.store(allocation_granularity, Ordering::Relaxed); + + allocation_granularity + } + allocation_granularity => allocation_granularity, } } From c2be354b5ca15f1f58c5ecc4262585769d24c6d2 Mon Sep 17 00:00:00 2001 From: Maarten de Vries Date: Thu, 20 Aug 2026 10:39:40 +0200 Subject: [PATCH 3/3] Disable excessive boolean warning of clippy. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 1fb9e06c..2a83d3e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,6 +153,7 @@ where /// [`map_copy()`]: MmapOptions::map_copy() /// [`map_copy_read_only()`]: MmapOptions::map_copy_read_only() #[derive(Clone, Debug, Default)] +#[allow(clippy::struct_excessive_bools)] pub struct MmapOptions { offset: u64, len: Option,