Skip to content

Commit 6a44de0

Browse files
authored
Map file cow windows (#1296)
* feat: add SurrogateMapping enum for Windows map_file_cow support Introduce SurrogateMapping enum (SandboxMemory/ReadOnlyFile) to HostRegionBase on Windows. This allows the surrogate process pipeline to distinguish between standard sandbox memory (guard pages, PAGE_READWRITE) and file-backed read-only mappings (no guard pages, PAGE_READONLY). - Add SurrogateMapping enum to memory_region.rs - Add surrogate_mapping field to HostRegionBase - Update Hash, MemoryRegionKind::add impls to include new field - Update host_region_base() in shared_mem.rs to set SandboxMemory - Update crashdump test to include surrogate_mapping field - Add unit tests for SurrogateMapping enum behaviour - Fix Justfile fmt-apply/fmt-check/witguest-wit for Windows (PowerShell) Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * feat: propagate SurrogateMapping through surrogate pipeline Pass SurrogateMapping through SurrogateProcess::map and WhpVm::map_memory so that file-backed read-only mappings (ReadOnlyFile) use PAGE_READONLY and skip guard pages, while sandbox memory (SandboxMemory) retains the existing PAGE_READWRITE + guard page behaviour. - Add mapping parameter to SurrogateProcess::map - Derive page protection and guard page logic from SurrogateMapping variant - Update WhpVm::map_memory to extract and forward surrogate_mapping - Update existing test call site in surrogate_process_manager - Add test: readonly_file_mapping_skips_guard_pages - Add test: surrogate_map_ref_counting Signed-off-by: Simon Davies <simon.davies@microsoft.com> Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * feat: add OwnedFileMapping RAII struct for Windows handle cleanup Introduce OwnedFileMapping to track host-side file mapping resources (MapViewOfFile view + CreateFileMappingW handle) with RAII cleanup. Add file_mappings Vec to MultiUseSandbox (Windows-only, positioned after vm field for correct drop order). - OwnedFileMapping::Drop calls UnmapViewOfFile then CloseHandle - unsafe impl Send + Sync (raw pointer only used during Drop) - Add test: owned_file_mapping_drop_releases_handles Signed-off-by: Simon Davies <simon.davies@microsoft.com> Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * feat: implement map_file_cow on Windows Replace the Windows stub with a full implementation that maps a file into the guest address space via the surrogate process pipeline: CreateFileMappingW -> MapViewOfFile -> SurrogateProcess::map -> WHvMapGpaRange2. Key details: - Opens file read-only, creates PAGE_READONLY mapping - Uses SurrogateMapping::ReadOnlyFile (no guard pages, PAGE_READONLY) - Guest gets READ|EXECUTE access via WHvMapGpaRange2 flags - Tracks host resources in OwnedFileMapping for RAII cleanup - Error path cleans up MapViewOfFile + CloseHandle on failure - CreateFileMappingW uses 0,0 for size (file's actual size, Windows rounds to page boundaries internally) Tests (264 passed, 0 failed): - test_map_file_cow_basic: map file, read from guest, verify content - test_map_file_cow_read_only_enforcement: write triggers violation - test_map_file_cow_poisoned: PoisonedSandbox check + restore - test_map_file_cow_multi_vm_same_file: two sandboxes, same file - test_map_file_cow_multi_vm_threaded: 5 threads, concurrent mapping - test_map_file_cow_cleanup_no_handle_leak: file deletable after drop Signed-off-by: Simon Davies <simon.davies@microsoft.com> Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * feat: wire map_file_cow cleanup into restore() Clean up host-side file mapping resources (OwnedFileMapping) when restore() unmaps regions. For each unmapped region, remove the corresponding OwnedFileMapping entry, whose Drop impl calls UnmapViewOfFile + CloseHandle. Also remove the dead_code allow now that guest_base is read. Tests (266 passed, 0 failed): - test_map_file_cow_snapshot_restore: map, snapshot, restore, verify data readable from snapshot memory - test_map_file_cow_snapshot_remapping_cycle: snapshot1 (empty) -> map file -> snapshot2 -> restore1 (unmapped) -> restore2 (folded) Signed-off-by: Simon Davies <simon.davies@microsoft.com> Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * fix: address code review findings H1: Add null check after MapViewOfFileNuma2 in SurrogateProcess::map H2: Store SurrogateMapping in HandleMapping, add debug_assert_eq on reuse M1: Clean up surrogate mapping on VirtualProtectEx failure (pre-existing) M2: Use checked_sub for ref count, log error on underflow M5: Early return with clear error for empty (0-byte) files L1: Fix incorrect Send/Sync safety comment on OwnedFileMapping L3: Rename _fp/_guest_base to file_path/guest_base L4: Use usize::try_from(file_size) instead of silent truncation N2: Use page_size::get() in test helper instead of magic 4096 N3: Change SurrogateMapping and surrogate_mapping field to pub(crate) N4: Replace low-value derive trait test with meaningful variant test Also: Change MemoryRegionType::Heap to Code for file mappings, add tracing::error in release-mode vacant unmap path. All 266 tests pass, 0 failures. Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * fix: add MappedFile region type, downgrade MemoryRegionType to pub(crate), fix clippy - Add MemoryRegionType::MappedFile variant for map_file_cow regions - Downgrade MemoryRegionType from pub to pub(crate) (not part of public API) - Use MappedFile consistently on both Windows and Linux in map_file_cow - Replace disallowed debug_assert_eq! with tracing::warn! in surrogate_process - Fix Justfile merge conflict Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Fix clippy Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Review Feedback Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> * Remove OwnedFileMapping Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> --------- Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com> Signed-off-by: Simon Davies <simon.davies@microsoft.com>
1 parent 3a28293 commit 6a44de0

File tree

6 files changed

+798
-50
lines changed

6 files changed

+798
-50
lines changed

Justfile

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ build target=default-target:
4343
guests: build-and-move-rust-guests build-and-move-c-guests
4444

4545
ensure-cargo-hyperlight:
46-
command -v cargo-hyperlight >/dev/null 2>&1 || cargo install --locked cargo-hyperlight
46+
{{ if os() == "windows" { "if (-not (Get-Command cargo-hyperlight -ErrorAction SilentlyContinue)) { cargo install --locked cargo-hyperlight }" } else { "command -v cargo-hyperlight >/dev/null 2>&1 || cargo install --locked cargo-hyperlight" } }}
4747

4848
witguest-wit:
49-
command -v wasm-tools >/dev/null 2>&1 || cargo install --locked wasm-tools
49+
{{ if os() == "windows" { "if (-not (Get-Command wasm-tools -ErrorAction SilentlyContinue)) { cargo install --locked wasm-tools }" } else { "command -v wasm-tools >/dev/null 2>&1 || cargo install --locked wasm-tools" } }}
5050
cd src/tests/rust_guests/witguest && wasm-tools component wit guest.wit -w -o interface.wasm
5151
cd src/tests/rust_guests/witguest && wasm-tools component wit two_worlds.wit -w -o twoworlds.wasm
5252

@@ -287,19 +287,21 @@ check:
287287
{{ cargo-cmd }} check -p hyperlight-host --features nanvix-unstable {{ target-triple-flag }}
288288
{{ cargo-cmd }} check -p hyperlight-host --features nanvix-unstable,executable_heap {{ target-triple-flag }}
289289

290-
fmt-check:
291-
rustup +nightly component list | grep -q "rustfmt.*installed" || rustup component add rustfmt --toolchain nightly
290+
fmt-check: (ensure-nightly-fmt)
292291
cargo +nightly fmt --all -- --check
293292
cargo +nightly fmt --manifest-path src/tests/rust_guests/simpleguest/Cargo.toml -- --check
294293
cargo +nightly fmt --manifest-path src/tests/rust_guests/dummyguest/Cargo.toml -- --check
295294
cargo +nightly fmt --manifest-path src/tests/rust_guests/witguest/Cargo.toml -- --check
296295
cargo +nightly fmt --manifest-path src/hyperlight_guest_capi/Cargo.toml -- --check
297296

297+
[private]
298+
ensure-nightly-fmt:
299+
{{ if os() == "windows" { "if (-not (rustup +nightly component list | Select-String 'rustfmt.*installed')) { rustup component add rustfmt --toolchain nightly }" } else { "rustup +nightly component list | grep -q 'rustfmt.*installed' || rustup component add rustfmt --toolchain nightly" } }}
300+
298301
check-license-headers:
299302
./dev/check-license-headers.sh
300303

301-
fmt-apply:
302-
rustup +nightly component list | grep -q "rustfmt.*installed" || rustup component add rustfmt --toolchain nightly
304+
fmt-apply: (ensure-nightly-fmt)
303305
cargo +nightly fmt --all
304306
cargo +nightly fmt --manifest-path src/tests/rust_guests/simpleguest/Cargo.toml
305307
cargo +nightly fmt --manifest-path src/tests/rust_guests/dummyguest/Cargo.toml

src/hyperlight_host/src/hypervisor/surrogate_process.rs

Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,23 @@ use tracing::{Span, instrument};
2323
use windows::Win32::Foundation::HANDLE;
2424
use windows::Win32::System::Memory::{
2525
MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFileNuma2, PAGE_NOACCESS, PAGE_PROTECTION_FLAGS,
26-
PAGE_READWRITE, UNMAP_VIEW_OF_FILE_FLAGS, UnmapViewOfFile2, VirtualProtectEx,
26+
PAGE_READONLY, PAGE_READWRITE, UNMAP_VIEW_OF_FILE_FLAGS, UnmapViewOfFile2, VirtualProtectEx,
2727
};
2828
use windows::Win32::System::SystemServices::NUMA_NO_PREFERRED_NODE;
2929

3030
use super::surrogate_process_manager::get_surrogate_process_manager;
3131
use super::wrappers::HandleWrapper;
3232
use crate::HyperlightError::WindowsAPIError;
33+
use crate::mem::memory_region::SurrogateMapping;
3334
use crate::{Result, log_then_return};
3435

3536
#[derive(Debug)]
3637
pub(crate) struct HandleMapping {
3738
pub(crate) use_count: u64,
3839
pub(crate) surrogate_base: *mut c_void,
40+
/// The mapping type used when this entry was first created.
41+
/// Used for debug assertions to catch conflicting re-maps.
42+
pub(crate) mapping_type: SurrogateMapping,
3943
}
4044

4145
/// Contains details of a surrogate process to be used by a Sandbox for providing memory to a HyperV VM on Windows.
@@ -57,18 +61,45 @@ impl SurrogateProcess {
5761
}
5862
}
5963

64+
/// Maps a file mapping handle into the surrogate process.
65+
///
66+
/// The `mapping` parameter controls the page protection and guard page
67+
/// behaviour:
68+
/// - [`SurrogateMapping::SandboxMemory`]: uses `PAGE_READWRITE` and sets
69+
/// guard pages (`PAGE_NOACCESS`) on the first and last pages.
70+
/// - [`SurrogateMapping::ReadOnlyFile`]: uses `PAGE_READONLY` with no
71+
/// guard pages.
72+
///
73+
/// If `host_base` was already mapped, the existing mapping is reused
74+
/// and the reference count is incremented (the `mapping` parameter is
75+
/// ignored in that case).
6076
pub(super) fn map(
6177
&mut self,
6278
handle: HandleWrapper,
6379
host_base: usize,
6480
host_size: usize,
81+
mapping: &SurrogateMapping,
6582
) -> Result<*mut c_void> {
6683
match self.mappings.entry(host_base) {
6784
Entry::Occupied(mut oe) => {
85+
if oe.get().mapping_type != *mapping {
86+
tracing::warn!(
87+
"Conflicting SurrogateMapping for host_base {host_base:#x}: \
88+
existing={:?}, requested={:?}",
89+
oe.get().mapping_type,
90+
mapping
91+
);
92+
}
6893
oe.get_mut().use_count += 1;
6994
Ok(oe.get().surrogate_base)
7095
}
7196
Entry::Vacant(ve) => {
97+
// Derive the page protection from the mapping type
98+
let page_protection = match mapping {
99+
SurrogateMapping::SandboxMemory => PAGE_READWRITE,
100+
SurrogateMapping::ReadOnlyFile => PAGE_READONLY,
101+
};
102+
72103
// Use MapViewOfFile2 to map memory into the surrogate process, the MapViewOfFile2 API is implemented in as an inline function in a windows header file
73104
// (see https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-mapviewoffile2#remarks) so we use the same API it uses in the header file here instead of
74105
// MapViewOfFile2 which does not exist in the rust crate (see https://github.com/microsoft/windows-rs/issues/2595)
@@ -80,43 +111,60 @@ impl SurrogateProcess {
80111
None,
81112
host_size,
82113
0,
83-
PAGE_READWRITE.0,
114+
page_protection.0,
84115
NUMA_NO_PREFERRED_NODE,
85116
)
86117
};
87-
let mut unused_out_old_prot_flags = PAGE_PROTECTION_FLAGS(0);
88118

89-
// the first page of the raw_size is the guard page
90-
let first_guard_page_start = surrogate_base.Value;
91-
if let Err(e) = unsafe {
92-
VirtualProtectEx(
93-
self.process_handle.into(),
94-
first_guard_page_start,
95-
PAGE_SIZE_USIZE,
96-
PAGE_NOACCESS,
97-
&mut unused_out_old_prot_flags,
98-
)
99-
} {
100-
log_then_return!(WindowsAPIError(e.clone()));
119+
if surrogate_base.Value.is_null() {
120+
log_then_return!(
121+
"MapViewOfFileNuma2 failed: {:?}",
122+
std::io::Error::last_os_error()
123+
);
101124
}
102125

103-
// the last page of the raw_size is the guard page
104-
let last_guard_page_start =
105-
unsafe { first_guard_page_start.add(host_size - PAGE_SIZE_USIZE) };
106-
if let Err(e) = unsafe {
107-
VirtualProtectEx(
108-
self.process_handle.into(),
109-
last_guard_page_start,
110-
PAGE_SIZE_USIZE,
111-
PAGE_NOACCESS,
112-
&mut unused_out_old_prot_flags,
113-
)
114-
} {
115-
log_then_return!(WindowsAPIError(e.clone()));
126+
// Only set guard pages for SandboxMemory mappings.
127+
// File-backed read-only mappings do not need guard pages
128+
// because the host does not write to them.
129+
if *mapping == SurrogateMapping::SandboxMemory {
130+
let mut unused_out_old_prot_flags = PAGE_PROTECTION_FLAGS(0);
131+
132+
// the first page of the raw_size is the guard page
133+
let first_guard_page_start = surrogate_base.Value;
134+
if let Err(e) = unsafe {
135+
VirtualProtectEx(
136+
self.process_handle.into(),
137+
first_guard_page_start,
138+
PAGE_SIZE_USIZE,
139+
PAGE_NOACCESS,
140+
&mut unused_out_old_prot_flags,
141+
)
142+
} {
143+
self.unmap_helper(surrogate_base.Value);
144+
log_then_return!(WindowsAPIError(e.clone()));
145+
}
146+
147+
// the last page of the raw_size is the guard page
148+
let last_guard_page_start =
149+
unsafe { first_guard_page_start.add(host_size - PAGE_SIZE_USIZE) };
150+
if let Err(e) = unsafe {
151+
VirtualProtectEx(
152+
self.process_handle.into(),
153+
last_guard_page_start,
154+
PAGE_SIZE_USIZE,
155+
PAGE_NOACCESS,
156+
&mut unused_out_old_prot_flags,
157+
)
158+
} {
159+
self.unmap_helper(surrogate_base.Value);
160+
log_then_return!(WindowsAPIError(e.clone()));
161+
}
116162
}
163+
117164
ve.insert(HandleMapping {
118165
use_count: 1,
119166
surrogate_base: surrogate_base.Value,
167+
mapping_type: *mapping,
120168
});
121169
Ok(surrogate_base.Value)
122170
}
@@ -126,15 +174,25 @@ impl SurrogateProcess {
126174
pub(super) fn unmap(&mut self, host_base: usize) {
127175
match self.mappings.entry(host_base) {
128176
Entry::Occupied(mut oe) => {
129-
oe.get_mut().use_count -= 1;
177+
oe.get_mut().use_count = oe.get().use_count.checked_sub(1).unwrap_or_else(|| {
178+
tracing::error!(
179+
"Surrogate unmap ref count underflow for host_base {:#x}",
180+
host_base
181+
);
182+
0
183+
});
130184
if oe.get().use_count == 0 {
131185
let entry = oe.remove();
132186
self.unmap_helper(entry.surrogate_base);
133187
}
134188
}
135189
Entry::Vacant(_) => {
190+
tracing::error!(
191+
"Attempted to unmap from surrogate a region at host_base {:#x} that was never mapped",
192+
host_base
193+
);
136194
#[cfg(debug_assertions)]
137-
panic!("Attempted to unmap from surrogate a region that was never mapped")
195+
panic!("Attempted to unmap from surrogate a region that was never mapped");
138196
}
139197
}
140198
}

src/hyperlight_host/src/hypervisor/surrogate_process_manager.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ mod tests {
459459
HandleWrapper::from(mem.get_mmap_file_handle()),
460460
mem.raw_ptr() as usize,
461461
mem.raw_mem_size(),
462+
&crate::mem::memory_region::SurrogateMapping::SandboxMemory,
462463
)
463464
.unwrap();
464465

@@ -498,4 +499,127 @@ mod tests {
498499
assert!(success.is_err());
499500
}
500501
}
502+
503+
/// Tests that [`SurrogateMapping::ReadOnlyFile`] skips guard pages entirely.
504+
///
505+
/// When mapping with `ReadOnlyFile`, the first and last pages should be
506+
/// accessible (no `PAGE_NOACCESS` guard pages set), unlike `SandboxMemory`
507+
/// which marks them as guard pages.
508+
#[test]
509+
fn readonly_file_mapping_skips_guard_pages() {
510+
const SIZE: usize = 4096;
511+
let mgr = get_surrogate_process_manager().unwrap();
512+
let mem = ExclusiveSharedMemory::new(SIZE).unwrap();
513+
514+
let mut process = mgr.get_surrogate_process().unwrap();
515+
let surrogate_address = process
516+
.map(
517+
HandleWrapper::from(mem.get_mmap_file_handle()),
518+
mem.raw_ptr() as usize,
519+
mem.raw_mem_size(),
520+
&crate::mem::memory_region::SurrogateMapping::ReadOnlyFile,
521+
)
522+
.unwrap();
523+
524+
let buffer = vec![0u8; SIZE];
525+
let bytes_read: Option<*mut usize> = None;
526+
let process_handle: HANDLE = process.process_handle.into();
527+
528+
unsafe {
529+
// read the first page — should succeed (no guard page for ReadOnlyFile)
530+
let success = windows::Win32::System::Diagnostics::Debug::ReadProcessMemory(
531+
process_handle,
532+
surrogate_address,
533+
buffer.as_ptr() as *mut c_void,
534+
SIZE,
535+
bytes_read,
536+
);
537+
assert!(
538+
success.is_ok(),
539+
"First page should be readable with ReadOnlyFile (no guard page)"
540+
);
541+
542+
// read the middle page — should also succeed
543+
let success = windows::Win32::System::Diagnostics::Debug::ReadProcessMemory(
544+
process_handle,
545+
surrogate_address.wrapping_add(SIZE),
546+
buffer.as_ptr() as *mut c_void,
547+
SIZE,
548+
bytes_read,
549+
);
550+
assert!(
551+
success.is_ok(),
552+
"Middle page should be readable with ReadOnlyFile"
553+
);
554+
555+
// read the last page — should succeed (no guard page for ReadOnlyFile)
556+
let success = windows::Win32::System::Diagnostics::Debug::ReadProcessMemory(
557+
process_handle,
558+
surrogate_address.wrapping_add(2 * SIZE),
559+
buffer.as_ptr() as *mut c_void,
560+
SIZE,
561+
bytes_read,
562+
);
563+
assert!(
564+
success.is_ok(),
565+
"Last page should be readable with ReadOnlyFile (no guard page)"
566+
);
567+
}
568+
}
569+
570+
/// Tests that the reference counting in [`SurrogateProcess::map`] works
571+
/// correctly — repeated maps to the same `host_base` increment the count
572+
/// and return the same surrogate address, regardless of the mapping type
573+
/// passed on subsequent calls.
574+
#[test]
575+
fn surrogate_map_ref_counting() {
576+
let mgr = get_surrogate_process_manager().unwrap();
577+
let mem = ExclusiveSharedMemory::new(4096).unwrap();
578+
579+
let mut process = mgr.get_surrogate_process().unwrap();
580+
let handle = HandleWrapper::from(mem.get_mmap_file_handle());
581+
let host_base = mem.raw_ptr() as usize;
582+
let host_size = mem.raw_mem_size();
583+
584+
// First map — creates the mapping
585+
let addr1 = process
586+
.map(
587+
handle,
588+
host_base,
589+
host_size,
590+
&crate::mem::memory_region::SurrogateMapping::SandboxMemory,
591+
)
592+
.unwrap();
593+
594+
// Second map — should reuse (ref count incremented)
595+
let addr2 = process
596+
.map(
597+
handle,
598+
host_base,
599+
host_size,
600+
&crate::mem::memory_region::SurrogateMapping::SandboxMemory,
601+
)
602+
.unwrap();
603+
604+
assert_eq!(
605+
addr1, addr2,
606+
"Repeated map should return the same surrogate address"
607+
);
608+
609+
// First unmap — decrements ref count but should NOT actually unmap
610+
process.unmap(host_base);
611+
612+
// The mapping should still be present (ref count was 2, now 1)
613+
assert!(
614+
process.mappings.contains_key(&host_base),
615+
"Mapping should still exist after first unmap (ref count > 0)"
616+
);
617+
618+
// Second unmap — ref count hits 0, actually unmaps
619+
process.unmap(host_base);
620+
assert!(
621+
!process.mappings.contains_key(&host_base),
622+
"Mapping should be removed after ref count reaches 0"
623+
);
624+
}
501625
}

0 commit comments

Comments
 (0)