From 941d691942e1f69ee1c2ad9448187c7971673b87 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 10:33:49 -0700 Subject: [PATCH 01/16] Handle missing KnownDlls section opens Implement NtOpenSection as a host-grounded resolve-and-reject path for the empty KnownDlls namespace. Reuse object-manager name resolution, zero the output handle on non-AV rejects, and let the Windows loader fall back to file-based mapping. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 9 ++ .../src/syscalls/directory.rs | 151 ++++++++++++++++++ litebox_shim_windows/src/syscalls/mod.rs | 12 +- 3 files changed, 171 insertions(+), 1 deletion(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 3de8199688..db38140cf0 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -588,6 +588,15 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtOpenSection { + section_handle, + desired_access, + object_attributes, + } => { + let status = + self.sys_nt_open_section(section_handle, desired_access, object_attributes); + (status, ContinueOperation::Resume) + } SyscallRequest::NtQueryDirectoryObject { directory_handle, buffer, diff --git a/litebox_shim_windows/src/syscalls/directory.rs b/litebox_shim_windows/src/syscalls/directory.rs index 853056a618..c0cb6017dd 100644 --- a/litebox_shim_windows/src/syscalls/directory.rs +++ b/litebox_shim_windows/src/syscalls/directory.rs @@ -948,6 +948,48 @@ impl Task { NtStatus::SUCCESS } + pub(crate) fn sys_nt_open_section( + &self, + section_handle: MutPtr, + _desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + if section_handle + .write_at_offset(0, Handle::default()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + let Some(object_attributes) = object_attributes else { + return NtStatus::INVALID_PARAMETER; + }; + + let directory_name = + match self.read_directory_object_attributes(Some(object_attributes), true) { + Ok((Some(_), Some(directory_name))) => directory_name, + Ok(_) => return NtStatus::INVALID_PARAMETER, + Err(status) => return status, + }; + + // ReactOS opens sections through ObOpenObjectByName(..., MmSectionObjectType, ...), and + // Wine routes NtOpenSection to open_mapping. LiteBox seeds \KnownDlls as an object + // directory but does not seed section objects, matching the guest-observed file fallback: + // host 25H2 returns OBJECT_NAME_NOT_FOUND for a missing KnownDlls leaf and + // OBJECT_TYPE_MISMATCH when an existing directory is opened as a section. + // TODO(section-subsystem): once section objects exist, validate SECTION_* desired access + // and return ACCESS_DENIED for denied opens instead of resolving every object as missing. + // Also validate OBJ_OPENLINK there: host 25H2 returns INVALID_PARAMETER for OBJ_OPENLINK + // only when opening an existing section; missing section names still return name-miss. + match self + .process + .directory_namespace + .resolve_directory(&directory_name.original_path) + { + Ok(_) | Err(NtStatus::OBJECT_TYPE_MISMATCH) => NtStatus::OBJECT_TYPE_MISMATCH, + Err(status) => status, + } + } + pub(crate) fn sys_nt_query_directory_object( &self, params: DirectoryQueryParameters, @@ -1223,6 +1265,17 @@ mod tests { handle } + fn object_attributes_with_root( + name: &UnicodeString, + root_directory: Handle, + attributes: u32, + ) -> ObjectAttributes { + ObjectAttributes { + root_directory, + ..object_attributes(name, attributes) + } + } + fn expected_record_size(name: &str, type_name: &str) -> usize { size_of::() + name.encode_utf16().count() * size_of::() @@ -1326,6 +1379,104 @@ mod tests { }); } + #[test] + fn open_section_rejects_empty_known_dlls_with_zeroed_output() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let known_dlls_units = utf16_units(r"\KnownDlls"); + let known_dlls_name = unicode_string(&known_dlls_units); + let known_dlls_attrs = object_attributes( + &known_dlls_name, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + let mut known_dlls = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut known_dlls), + DIRECTORY_QUERY | DIRECTORY_TRAVERSE, + Some(const_ptr(&known_dlls_attrs)), + ), + NtStatus::SUCCESS + ); + let kernel32_units = utf16_units("KERNEL32.DLL"); + let kernel32 = unicode_string(&kernel32_units); + let attrs = object_attributes_with_root( + &kernel32, + known_dlls, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + let mut handle = Handle::from_raw(0x5555_5555); + + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!(handle, Handle::default()); + + let attrs = object_attributes_with_root( + &kernel32, + known_dlls, + (ObjectAttributesFlags::CASE_INSENSITIVE | ObjectAttributesFlags::OPENLINK).bits(), + ); + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!(handle, Handle::default()); + + let missing_parent_units = utf16_units(r"\MissingLiteBoxParent\KERNEL32.DLL"); + let missing_parent = unicode_string(&missing_parent_units); + let attrs = object_attributes( + &missing_parent, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::OBJECT_PATH_NOT_FOUND + ); + assert_eq!(handle, Handle::default()); + + let attrs = object_attributes( + &known_dlls_name, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::OBJECT_TYPE_MISMATCH + ); + assert_eq!(handle, Handle::default()); + + let attrs = object_attributes_with_root( + &kernel32, + Handle::from_raw(0x1234), + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::INVALID_HANDLE + ); + assert_eq!(handle, Handle::default()); + + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, None), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::default()); + + assert_eq!( + task.sys_nt_open_section(null_mut_ptr::(), 0x0d, Some(const_ptr(&attrs))), + NtStatus::ACCESS_VIOLATION + ); + + assert_eq!(task.sys_nt_close(known_dlls), NtStatus::SUCCESS); + }); + } + #[test] fn create_directory_distinguishes_null_object_name_from_empty_name() { run_with_test_platform_pointers(|| { diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index ced9039d02..f470e16249 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -143,6 +143,11 @@ pub(crate) enum SyscallRequest { desired_access: u32, object_attributes: Option>, }, + NtOpenSection { + section_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, NtQueryDirectoryObject { directory_handle: Handle, buffer: Platform::RawMutPointer, @@ -478,8 +483,13 @@ impl SyscallRequest { desired_access, object_attributes:*, })), + NtSysno::NtOpenSection => Some(sys_req!(NtOpenSection { + section_handle:*, + desired_access, + object_attributes:*, + })), NtSysno::NtQueryDirectoryObject => Some(sys_req!(NtQueryDirectoryObject { - directory_handle:{Handle::from_raw}, + directory_handle:{ Handle::from_raw }, buffer:*, buffer_length, return_single_entry, From 5c95c2a96b51d3b22dee51b7bb0e5e9b56b0d6c6 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 11:23:12 -0700 Subject: [PATCH 02/16] Implement Windows section object syscalls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_common_windows/src/loader.rs | 15 + litebox_common_windows/src/nt_status.rs | 8 + litebox_shim_windows/src/lib.rs | 175 ++ litebox_shim_windows/src/loader/mod.rs | 1 + litebox_shim_windows/src/loader/pe.rs | 52 + .../src/syscalls/directory.rs | 64 +- litebox_shim_windows/src/syscalls/mm.rs | 19 +- litebox_shim_windows/src/syscalls/mod.rs | 127 +- litebox_shim_windows/src/syscalls/section.rs | 1821 +++++++++++++++++ litebox_shim_windows/src/tests.rs | 2 + 10 files changed, 2237 insertions(+), 47 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/section.rs diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs index 486b7ae735..0e3f18134d 100644 --- a/litebox_common_windows/src/loader.rs +++ b/litebox_common_windows/src/loader.rs @@ -489,6 +489,21 @@ impl PeParsedFile { self.image.image_base } + #[must_use] + pub fn machine(&self) -> u16 { + self.image.machine + } + + #[must_use] + pub fn characteristics(&self) -> u16 { + self.image.characteristics + } + + #[must_use] + pub fn dll_characteristics(&self) -> u16 { + self.image.dll_characteristics + } + /// Returns whether the image opts into dynamic-base loading. #[must_use] pub fn has_dynamic_base(&self) -> bool { diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index 439cd7fe63..bfeb2249ee 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -159,6 +159,8 @@ impl NtStatus { 0xC0000046 => "STATUS_MUTANT_NOT_OWNED: Mutant not owned", 0xC0000047 => "STATUS_SEMAPHORE_LIMIT_EXCEEDED: Semaphore limit exceeded", 0xC0000048 => "STATUS_PORT_ALREADY_SET: Port already set", + 0xC0000049 => "STATUS_SECTION_NOT_IMAGE: Section not image", + 0xC000004E => "STATUS_SECTION_PROTECTION: Section protection", 0xC000004F => "STATUS_EAS_NOT_SUPPORTED: EAS not supported", 0xC0000050 => "STATUS_EA_TOO_LARGE: EA too large", 0xC0000056 => "STATUS_DELETE_PENDING: Delete pending", @@ -420,6 +422,9 @@ impl NtStatus { /// STATUS_SECTION_TOO_BIG pub const SECTION_TOO_BIG: Self = Self::from_raw(0xC0000040); + /// STATUS_SECTION_NOT_IMAGE + pub const SECTION_NOT_IMAGE: Self = Self::from_raw(0xC0000049); + /// STATUS_PORT_CONNECTION_REFUSED pub const PORT_CONNECTION_REFUSED: Self = Self::from_raw(0xC0000041); @@ -435,6 +440,9 @@ impl NtStatus { /// STATUS_INVALID_PAGE_PROTECTION pub const INVALID_PAGE_PROTECTION: Self = Self::from_raw(0xC0000045); + /// STATUS_SECTION_PROTECTION + pub const SECTION_PROTECTION: Self = Self::from_raw(0xC000004E); + /// STATUS_MUTANT_NOT_OWNED pub const MUTANT_NOT_OWNED: Self = Self::from_raw(0xC0000046); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index db38140cf0..a9a8d92476 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -38,6 +38,9 @@ use crate::syscalls::event::{EventHandleObject, EventObject, EventSubsystem}; use crate::syscalls::file::{FileObject, FileObjectSubsystem}; use crate::syscalls::iocp::{IoCompletionHandleObject, IoCompletionSubsystem}; use crate::syscalls::registry::{RegistryKeyObject, RegistryKeySubsystem}; +use crate::syscalls::section::{ + MapViewOfSectionParameters, SectionHandleObject, SectionObject, SectionSubsystem, +}; use crate::syscalls::symlink::{SymbolicLinkHandleObject, SymbolicLinkSubsystem}; use crate::syscalls::timer::{TimerCreateParameters, TimerHandleObject, TimerSubsystem}; use crate::syscalls::wait_completion_packet::{ @@ -90,6 +93,10 @@ pub(crate) type WindowsNlsSectionMappings = litebox::sync::RwLock>; pub(crate) type WindowsVirtualAllocations = litebox::sync::RwLock>; +pub(crate) type WindowsSectionNamespace = + litebox::sync::RwLock>>>; +pub(crate) type WindowsSectionViews = + litebox::sync::RwLock>>; pub(crate) type WindowsEventNamespace = litebox::sync::RwLock>>>; pub(crate) type WindowsDirectoryNamespace = DirectoryNamespace; @@ -103,6 +110,22 @@ pub(crate) struct WindowsVirtualAllocation { pub(crate) pages: rangemap::RangeMap, } +pub(crate) struct WindowsSectionView { + pub(crate) size: usize, + pub(crate) section_offset: usize, + pub(crate) section: Option>>, +} + +impl Clone for WindowsSectionView { + fn clone(&self) -> Self { + Self { + size: self.size, + section_offset: self.section_offset, + section: self.section.clone(), + } + } +} + pub type DefaultFS = WindowsFS; pub type WindowsFS = litebox::fs::layered::FileSystem< @@ -343,6 +366,8 @@ impl WindowsShim { handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), directory_namespace, event_namespace: WindowsEventNamespace::::new(BTreeMap::new()), + section_namespace: WindowsSectionNamespace::::new(BTreeMap::new()), + section_views: WindowsSectionViews::::new(BTreeMap::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), virtual_allocations: load_info.virtual_allocations, system_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), @@ -387,6 +412,8 @@ pub struct Process { handles: WindowsHandleStore, directory_namespace: WindowsDirectoryNamespace, event_namespace: WindowsEventNamespace, + section_namespace: WindowsSectionNamespace, + section_views: WindowsSectionViews, nls_section_mappings: WindowsNlsSectionMappings, virtual_allocations: WindowsVirtualAllocations, system_lcid: AtomicU32, @@ -671,6 +698,50 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtCreateSection { + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + } => { + let status = self.sys_nt_create_section( + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateSectionEx { + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + extended_parameters, + extended_parameter_count, + } => { + let status = self.sys_nt_create_section_ex( + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + extended_parameters, + extended_parameter_count, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtCreateWaitCompletionPacket { wait_completion_packet_handle, desired_access, @@ -1032,6 +1103,22 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtQuerySection { + section_handle, + section_information_class, + section_information, + section_information_length, + return_length, + } => { + let status = self.sys_nt_query_section( + section_handle, + section_information_class, + section_information, + section_information_length, + return_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtQueryInformationProcess { process_handle, process_information_class, @@ -1208,6 +1295,80 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtMapViewOfSection { + section_handle, + process_handle, + base_address, + zero_bits, + commit_size, + section_offset, + view_size, + inherit_disposition, + allocation_type, + page_protection, + } => { + let status = self.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle, + process_handle, + base_address, + zero_bits, + commit_size, + section_offset, + view_size, + inherit_disposition, + allocation_type, + page_protection, + }); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtMapViewOfSectionEx { + section_handle, + process_handle, + base_address, + zero_bits, + commit_size, + section_offset, + view_size, + inherit_disposition, + allocation_type, + page_protection, + extended_parameters, + extended_parameter_count, + } => { + let status = self.sys_nt_map_view_of_section_ex( + MapViewOfSectionParameters { + section_handle, + process_handle, + base_address, + zero_bits, + commit_size, + section_offset, + view_size, + inherit_disposition, + allocation_type, + page_protection, + }, + extended_parameters, + extended_parameter_count, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtUnmapViewOfSection { + process_handle, + base_address, + } => { + let status = self.sys_nt_unmap_view_of_section(process_handle, base_address); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtUnmapViewOfSectionEx { + process_handle, + base_address, + flags, + } => { + let status = + self.sys_nt_unmap_view_of_section_ex(process_handle, base_address, flags); + (status, ContinueOperation::Resume) + } SyscallRequest::NtTerminateProcess { process_handle, exit_status, @@ -1315,6 +1476,14 @@ impl Task { ) { return NtStatus::SUCCESS; } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |section| visitor.section(section), + ) { + return NtStatus::SUCCESS; + } NtStatus::INVALID_HANDLE } @@ -1351,6 +1520,8 @@ trait RawHandleVisitor { ); fn worker_factory(&self, worker_factory: WorkerFactoryHandleObject); + + fn section(&self, section: SectionHandleObject); } struct CloseRawHandleVisitor<'task, Platform: ShimPlatform, FS: ShimFS> { @@ -1398,6 +1569,10 @@ impl RawHandleVisitor fn worker_factory(&self, worker_factory: WorkerFactoryHandleObject) { Task::::close_worker_factory(worker_factory); } + + fn section(&self, section: SectionHandleObject) { + Task::::close_section(section); + } } /// The shim entrypoint object passed to the platform. diff --git a/litebox_shim_windows/src/loader/mod.rs b/litebox_shim_windows/src/loader/mod.rs index 3d7b9a32e0..7922fdcdb8 100644 --- a/litebox_shim_windows/src/loader/mod.rs +++ b/litebox_shim_windows/src/loader/mod.rs @@ -4,3 +4,4 @@ mod pe; pub(super) use pe::{PeLoader, WindowsLoadError}; +pub(crate) use pe::{image_section_metadata, load_image_section}; diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 05bcc2b536..4ba683cf64 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -1224,6 +1224,58 @@ fn load_image( load_image_with_writable_sections(fs, path, platform, page_manager, &[]) } +pub(crate) fn load_image_section( + platform: &'static Platform, + fs: Arc, + path: &str, + page_manager: &crate::WindowsPageManager, + virtual_allocations: &crate::WindowsVirtualAllocations, +) -> Result { + let image = load_image(platform, fs, path, page_manager)?; + let mapping = image.mapping; + register_image_virtual_allocation(virtual_allocations, mapping, image.pages); + Ok(mapping) +} + +pub(crate) struct ImageSectionMetadata { + pub(crate) transfer_address: usize, + pub(crate) file_size: u32, + pub(crate) subsystem: u32, + pub(crate) subsystem_major_version: u16, + pub(crate) subsystem_minor_version: u16, + pub(crate) image_characteristics: u16, + pub(crate) dll_characteristics: u16, + pub(crate) machine: u16, +} + +pub(crate) fn image_section_metadata( + fs: Arc, + path: &str, +) -> Result { + let file = PeImageFile::open(fs, path)?; + let parsed = PeParsedFile::parse(&mut &file).map_err(WindowsLoadError::Parse)?; + let file_size = file + .fs + .fd_file_status(&file.fd) + .map_err(PeImageAccessError::FileStatus)? + .size + .try_into() + .map_err(|_| PeImageAccessError::AddressOverflow)?; + Ok(ImageSectionMetadata { + transfer_address: parsed + .image_base() + .checked_add(parsed.entry_point_rva()) + .ok_or(PeImageAccessError::AddressOverflow)?, + file_size, + subsystem: u32::from(parsed.subsystem()), + subsystem_major_version: parsed.major_subsystem_version(), + subsystem_minor_version: parsed.minor_subsystem_version(), + image_characteristics: parsed.characteristics(), + dll_characteristics: parsed.dll_characteristics(), + machine: parsed.machine(), + }) +} + fn load_image_with_writable_sections( fs: Arc, path: &str, diff --git a/litebox_shim_windows/src/syscalls/directory.rs b/litebox_shim_windows/src/syscalls/directory.rs index c0cb6017dd..f992c12319 100644 --- a/litebox_shim_windows/src/syscalls/directory.rs +++ b/litebox_shim_windows/src/syscalls/directory.rs @@ -314,7 +314,10 @@ impl DirectoryNamespace { } } - fn resolve_directory(&self, path: &str) -> Result>, NtStatus> { + pub(super) fn resolve_directory( + &self, + path: &str, + ) -> Result>, NtStatus> { let tail = absolute_path_tail(path)?; let node = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, false)?; if node.is_directory() { @@ -324,6 +327,23 @@ impl DirectoryNamespace { } } + pub(super) fn resolve_object(&self, path: &str) -> Result>, NtStatus> { + let tail = absolute_path_tail(path)?; + self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, true) + } + + pub(super) fn parent_directory_exists(&self, path: &str) -> bool { + let path = trim_trailing_directory_path(path); + if path == r"\" { + return false; + } + let Some(index) = path.rfind('\\') else { + return false; + }; + let parent = if index == 0 { r"\" } else { &path[..index] }; + self.resolve_directory(parent).is_ok() + } + fn create_directory( &self, path: &str, @@ -948,48 +968,6 @@ impl Task { NtStatus::SUCCESS } - pub(crate) fn sys_nt_open_section( - &self, - section_handle: MutPtr, - _desired_access: u32, - object_attributes: Option>, - ) -> NtStatus { - if section_handle - .write_at_offset(0, Handle::default()) - .is_none() - { - return NtStatus::ACCESS_VIOLATION; - } - let Some(object_attributes) = object_attributes else { - return NtStatus::INVALID_PARAMETER; - }; - - let directory_name = - match self.read_directory_object_attributes(Some(object_attributes), true) { - Ok((Some(_), Some(directory_name))) => directory_name, - Ok(_) => return NtStatus::INVALID_PARAMETER, - Err(status) => return status, - }; - - // ReactOS opens sections through ObOpenObjectByName(..., MmSectionObjectType, ...), and - // Wine routes NtOpenSection to open_mapping. LiteBox seeds \KnownDlls as an object - // directory but does not seed section objects, matching the guest-observed file fallback: - // host 25H2 returns OBJECT_NAME_NOT_FOUND for a missing KnownDlls leaf and - // OBJECT_TYPE_MISMATCH when an existing directory is opened as a section. - // TODO(section-subsystem): once section objects exist, validate SECTION_* desired access - // and return ACCESS_DENIED for denied opens instead of resolving every object as missing. - // Also validate OBJ_OPENLINK there: host 25H2 returns INVALID_PARAMETER for OBJ_OPENLINK - // only when opening an existing section; missing section names still return name-miss. - match self - .process - .directory_namespace - .resolve_directory(&directory_name.original_path) - { - Ok(_) | Err(NtStatus::OBJECT_TYPE_MISMATCH) => NtStatus::OBJECT_TYPE_MISMATCH, - Err(status) => status, - } - } - pub(crate) fn sys_nt_query_directory_object( &self, params: DirectoryQueryParameters, diff --git a/litebox_shim_windows/src/syscalls/mm.rs b/litebox_shim_windows/src/syscalls/mm.rs index f67931a198..28b1aca6bf 100644 --- a/litebox_shim_windows/src/syscalls/mm.rs +++ b/litebox_shim_windows/src/syscalls/mm.rs @@ -40,7 +40,7 @@ bitflags::bitflags! { } impl PageProtection { - const BASE_MASK: u32 = 0xff; + pub(super) const BASE_MASK: u32 = 0xff; fn base(self) -> u32 { self.bits() & Self::BASE_MASK @@ -611,6 +611,17 @@ impl Task { None => return NtStatus::NOT_COMMITTED, }; + if !new_permissions.contains(MemoryRegionPermissions::READ) + && let Err(status) = super::section::synchronize_pagefile_views_in_range( + &self.process.section_views, + &self.process.virtual_allocations, + aligned_base, + aligned_len, + ) + { + return status; + } + if update_permissions( &self.global.page_manager, aligned_base, @@ -1065,7 +1076,9 @@ fn mark_pages_decommitted( allocation.pages.remove(base..end); } -fn parse_page_protection(protect: u32) -> Option<(PageProtection, MemoryRegionPermissions)> { +pub(super) fn parse_page_protection( + protect: u32, +) -> Option<(PageProtection, MemoryRegionPermissions)> { let protect = PageProtection::from_bits(protect)?; let permissions = page_protect_to_permissions(protect)?; Some((protect, permissions)) @@ -1124,7 +1137,7 @@ fn permissions_to_page_protect(permissions: MemoryRegionPermissions) -> PageProt } } -fn create_pages( +pub(super) fn create_pages( page_manager: &WindowsPageManager, suggested_address: Option>, length: NonZeroPageSize, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index f470e16249..1e3bda7d5f 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod mm; pub(crate) mod nls; pub(crate) mod process; pub(crate) mod registry; +pub(crate) mod section; pub(crate) mod symlink; mod sysinfo; pub(crate) mod thread; @@ -179,6 +180,26 @@ pub(crate) enum SyscallRequest { object_attributes: Option>, number_of_concurrent_threads: u32, }, + NtCreateSection { + section_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + maximum_size: Option>, + section_page_protection: u32, + allocation_attributes: u32, + file_handle: Handle, + }, + NtCreateSectionEx { + section_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + maximum_size: Option>, + section_page_protection: u32, + allocation_attributes: u32, + file_handle: Handle, + extended_parameters: Option>, + extended_parameter_count: u32, + }, NtCreateWaitCompletionPacket { wait_completion_packet_handle: Platform::RawMutPointer, desired_access: u32, @@ -351,6 +372,13 @@ pub(crate) enum SyscallRequest { system_information_length: u32, return_length: Option>, }, + NtQuerySection { + section_handle: Handle, + section_information_class: u32, + section_information: Platform::RawMutPointer, + section_information_length: usize, + return_length: Option>, + }, NtQueryInformationProcess { process_handle: ProcessHandle, process_information_class: u32, @@ -427,6 +455,41 @@ pub(crate) enum SyscallRequest { memory_information_length: usize, return_length: Option>, }, + NtMapViewOfSection { + section_handle: Handle, + process_handle: ProcessHandle, + base_address: Platform::RawMutPointer, + zero_bits: usize, + commit_size: usize, + section_offset: Option>, + view_size: Platform::RawMutPointer, + inherit_disposition: u32, + allocation_type: u32, + page_protection: u32, + }, + NtMapViewOfSectionEx { + section_handle: Handle, + process_handle: ProcessHandle, + base_address: Platform::RawMutPointer, + zero_bits: usize, + commit_size: usize, + section_offset: Option>, + view_size: Platform::RawMutPointer, + inherit_disposition: u32, + allocation_type: u32, + page_protection: u32, + extended_parameters: Option>, + extended_parameter_count: u32, + }, + NtUnmapViewOfSection { + process_handle: ProcessHandle, + base_address: usize, + }, + NtUnmapViewOfSectionEx { + process_handle: ProcessHandle, + base_address: usize, + flags: u32, + }, NtTerminateProcess { process_handle: ProcessHandle, exit_status: i32, @@ -439,7 +502,7 @@ impl SyscallRequest { pub(crate) fn try_from_raw(pt_regs: &litebox_common_linux::PtRegs) -> Option { macro_rules! sys_req { ($id:ident { $( $field:ident $(:$star:tt)? ),* $(,)? }) => { - sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] [ ]) + sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] [ ]) }; (@[$id:ident] [ $f:ident $(,)? $($field:ident $(:$star:tt)?),* ] [ $n:literal $(,)? $($ns:literal),* ] [ $($tail:tt)* ]) => { sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ $($ns),* ] [ $($tail)* $f: win_sys_req_arg::(pt_regs, $n)?, ]) @@ -519,6 +582,26 @@ impl SyscallRequest { object_attributes:*, number_of_concurrent_threads, })), + NtSysno::NtCreateSection => Some(sys_req!(NtCreateSection { + section_handle:*, + desired_access, + object_attributes:*, + maximum_size:*, + section_page_protection, + allocation_attributes, + file_handle:{ Handle::from_raw }, + })), + NtSysno::NtCreateSectionEx => Some(sys_req!(NtCreateSectionEx { + section_handle:*, + desired_access, + object_attributes:*, + maximum_size:*, + section_page_protection, + allocation_attributes, + file_handle:{ Handle::from_raw }, + extended_parameters:*, + extended_parameter_count, + })), NtSysno::NtCreateWaitCompletionPacket => Some(sys_req!( NtCreateWaitCompletionPacket { wait_completion_packet_handle:*, @@ -697,6 +780,13 @@ impl SyscallRequest { system_information_length, return_length:*, })), + NtSysno::NtQuerySection => Some(sys_req!(NtQuerySection { + section_handle: { Handle::from_raw }, + section_information_class, + section_information:*, + section_information_length, + return_length:*, + })), NtSysno::NtQueryInformationProcess => Some(sys_req!(NtQueryInformationProcess { process_handle: { ProcessHandle::from_raw }, process_information_class, @@ -775,6 +865,41 @@ impl SyscallRequest { memory_information_length, return_length:*, })), + NtSysno::NtMapViewOfSection => Some(sys_req!(NtMapViewOfSection { + section_handle: { Handle::from_raw }, + process_handle: { ProcessHandle::from_raw }, + base_address:*, + zero_bits, + commit_size, + section_offset:*, + view_size:*, + inherit_disposition, + allocation_type, + page_protection, + })), + NtSysno::NtMapViewOfSectionEx => Some(sys_req!(NtMapViewOfSectionEx { + section_handle: { Handle::from_raw }, + process_handle: { ProcessHandle::from_raw }, + base_address:*, + zero_bits, + commit_size, + section_offset:*, + view_size:*, + inherit_disposition, + allocation_type, + page_protection, + extended_parameters:*, + extended_parameter_count, + })), + NtSysno::NtUnmapViewOfSection => Some(sys_req!(NtUnmapViewOfSection { + process_handle: { ProcessHandle::from_raw }, + base_address, + })), + NtSysno::NtUnmapViewOfSectionEx => Some(sys_req!(NtUnmapViewOfSectionEx { + process_handle: { ProcessHandle::from_raw }, + base_address, + flags, + })), NtSysno::NtTerminateProcess => Some(sys_req!(NtTerminateProcess { process_handle: { ProcessHandle::from_raw }, exit_status, diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs new file mode 100644 index 0000000000..5a5f1b5c2a --- /dev/null +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -0,0 +1,1821 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec; +use alloc::vec::Vec; +use core::marker::PhantomData; +use core::mem::size_of; +use core::sync::atomic::{AtomicBool, Ordering}; + +use int_enum::IntEnum; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::mm::linux::{CreatePagesFlags, NonZeroPageSize}; +use litebox::platform::page_mgmt::MemoryRegionPermissions; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::sync::RwLock; +use litebox_common_windows::nt_status::NtStatus; +use rangemap::RangeMap; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::nt_types::{AccessMask, ObjectAttributes}; +use crate::syscalls::mm::{MemoryType, PageProtection, create_pages, parse_page_protection}; +use crate::syscalls::{Handle, ProcessHandle}; +use crate::{ + ConstPtr, MutPtr, PAGE_SIZE, ShimFS, ShimPlatform, Task, WindowsSectionView, + WindowsVirtualAllocations, +}; + +const VIEW_SHARE: u32 = 1; +const VIEW_UNMAP: u32 = 2; +const MEM_TOP_DOWN: u32 = 0x0010_0000; +const MEM_PHYSICAL: u32 = 0x0040_0000; +const MEM_DIFFERENT_IMAGE_BASE_OK: u32 = 0x0080_0000; +const SUPPORTED_MAP_ALLOCATION_TYPES: u32 = + MEM_TOP_DOWN | MEM_PHYSICAL | MEM_DIFFERENT_IMAGE_BASE_OK; + +enum SectionBacking { + Pagefile(RwLock>), + ImageFile, +} + +pub(crate) struct SectionSubsystem(PhantomData); + +impl FdEnabledSubsystem for SectionSubsystem { + type Entry = SectionHandleObject; +} + +impl FdEnabledSubsystemEntry for SectionHandleObject {} + +pub(crate) struct SectionHandleObject { + section: Arc>, + granted_access: SectionAccess, +} + +pub(crate) struct SectionObject { + fs_path: Option, + size: usize, + attributes: SectionAllocationAttributes, + protection: PageProtection, + backing: SectionBacking, + pagefile_view_active: AtomicBool, +} + +pub(crate) struct MapViewOfSectionParameters { + pub(crate) section_handle: Handle, + pub(crate) process_handle: ProcessHandle, + pub(crate) base_address: MutPtr, + pub(crate) zero_bits: usize, + pub(crate) commit_size: usize, + pub(crate) section_offset: Option>, + pub(crate) view_size: MutPtr, + pub(crate) inherit_disposition: u32, + pub(crate) allocation_type: u32, + pub(crate) page_protection: u32, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct SectionAllocationAttributes: u32 { + const SEC_FILE = 0x0080_0000; + const SEC_IMAGE = 0x0100_0000; + const SEC_RESERVE = 0x0400_0000; + const SEC_COMMIT = 0x0800_0000; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct SectionAccess: u32 { + const QUERY = 0x0001; + const MAP_WRITE = 0x0002; + const MAP_READ = 0x0004; + const MAP_EXECUTE = 0x0008; + const EXTEND_SIZE = 0x0010; + const MAP_EXECUTE_EXPLICIT = 0x0020; + + const GENERIC_READ_EXPANSION = AccessMask::STANDARD_RIGHTS_READ.bits() + | Self::QUERY.bits() + | Self::MAP_READ.bits(); + const GENERIC_WRITE_EXPANSION = AccessMask::STANDARD_RIGHTS_WRITE.bits() + | Self::MAP_WRITE.bits() + | Self::EXTEND_SIZE.bits(); + const GENERIC_EXECUTE_EXPANSION = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() + | Self::MAP_EXECUTE.bits(); + const ALL_ACCESS = AccessMask::STANDARD_RIGHTS_ALL.bits() + | Self::QUERY.bits() + | Self::MAP_WRITE.bits() + | Self::MAP_READ.bits() + | Self::MAP_EXECUTE.bits() + | Self::EXTEND_SIZE.bits(); + const GENERIC_ALL = AccessMask::GENERIC_ALL.bits(); + const GENERIC_EXECUTE = AccessMask::GENERIC_EXECUTE.bits(); + const GENERIC_WRITE = AccessMask::GENERIC_WRITE.bits(); + const GENERIC_READ = AccessMask::GENERIC_READ.bits(); + + const _ = !0; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct UnmapViewOfSectionFlags: u32 { + const _ = 0; + } +} + +impl SectionAccess { + fn from_desired_access(desired_access: u32) -> Self { + let mut access = Self::from_bits_retain(desired_access); + if access.contains(Self::GENERIC_READ) { + access.remove(Self::GENERIC_READ); + access.insert(Self::GENERIC_READ_EXPANSION); + } + if access.contains(Self::GENERIC_WRITE) { + access.remove(Self::GENERIC_WRITE); + access.insert(Self::GENERIC_WRITE_EXPANSION); + } + if access.contains(Self::GENERIC_EXECUTE) { + access.remove(Self::GENERIC_EXECUTE); + access.insert(Self::GENERIC_EXECUTE_EXPANSION); + } + if access.contains(Self::GENERIC_ALL) { + access.remove(Self::GENERIC_ALL); + access.insert(Self::ALL_ACCESS); + } + access + } + + fn require(self, required: Self) -> Result<(), NtStatus> { + if self.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum SectionInformationClass { + Basic = 0, + Image = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SectionBasicInformation { + base_address: usize, + attributes: u32, + _padding: u32, + size: i64, +} + +const _: () = assert!(size_of::() == 24); + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SectionImageInformation { + transfer_address: usize, + zero_bits: u32, + _padding0: u32, + maximum_stack_size: usize, + committed_stack_size: usize, + subsystem_type: u32, + subsystem_minor_version: u16, + subsystem_major_version: u16, + gp_value: u32, + image_characteristics: u16, + dll_characteristics: u16, + machine: u16, + image_contains_code: u8, + image_flags: u8, + loader_flags: u32, + image_file_size: u32, + checksum: u32, +} + +const _: () = assert!(size_of::() == 64); + +impl Task { + fn section_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + self.typed_handle_entry::>(handle) + } + + fn insert_section_handle( + &self, + section: Arc>, + granted_access: SectionAccess, + ) -> Result { + self.insert_typed_handle::>( + SectionHandleObject { + section, + granted_access, + }, + drop, + ) + } + + pub(crate) fn close_section_handle(&self, handle: Handle) { + self.close_typed_handle::>(handle, drop); + } + + pub(crate) fn close_section(section: SectionHandleObject) { + drop(section); + } + + // Wine's create_mapping and ReactOS MmCreateSection validate the output handle before creating + // the control object, then select pagefile/file/image backing from AllocationAttributes. + #[expect( + clippy::too_many_arguments, + reason = "NtCreateSection has seven ABI parameters; keeping ABI args explicit avoids reshuffling" + )] + pub(crate) fn sys_nt_create_section( + &self, + section_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + maximum_size: Option>, + section_page_protection: u32, + allocation_attributes: u32, + file_handle: Handle, + ) -> NtStatus { + // Host ntdll preserves the output handle for pre-creation validation failures such as a + // NULL MaximumSize pagefile section. + if let Err(status) = + crate::probe_guest_output_preserving_value::(section_handle) + { + return status; + } + let granted_access = SectionAccess::from_desired_access(desired_access); + if granted_access.is_empty() { + return NtStatus::ACCESS_DENIED; + } + let Some((protection, _)) = parse_page_protection(section_page_protection) else { + return NtStatus::INVALID_PAGE_PROTECTION; + }; + // NtCreateSection currently supports only pagefile-backed sections. File-backed image + // sections are synthesized by NtOpenSection for KnownDlls; accepting a file handle here + // requires section lifetime/sharing to be keyed by the underlying file object identity. + if !file_handle.is_null() { + litebox_util_log::debug!( + file_handle = file_handle.as_raw(), + allocation_attributes:% = format_args!("{allocation_attributes:#x}"), + section_page_protection:% = format_args!("{section_page_protection:#x}"), + desired_access:% = format_args!("{desired_access:#x}"); + "Unsupported file-backed NtCreateSection" + ); + return NtStatus::INVALID_HANDLE; + } + let allocation_attributes = + SectionAllocationAttributes::from_bits_retain(allocation_attributes); + let supported_create_attributes = + SectionAllocationAttributes::SEC_RESERVE | SectionAllocationAttributes::SEC_COMMIT; + if !allocation_attributes + .difference(supported_create_attributes) + .is_empty() + { + return NtStatus::INVALID_PARAMETER; + } + if !allocation_attributes.intersects(supported_create_attributes) { + return NtStatus::INVALID_PARAMETER; + } + + let Some(maximum_size) = maximum_size else { + return NtStatus::INVALID_PARAMETER_4; + }; + let maximum_size = match maximum_size.read_at_offset(0) { + Some(value) if value > 0 => value, + Some(_) => return NtStatus::INVALID_PARAMETER, + None => return NtStatus::ACCESS_VIOLATION, + }; + let Ok(size) = usize::try_from(maximum_size) else { + return NtStatus::SECTION_TOO_BIG; + }; + let Some(size) = size.checked_next_multiple_of(PAGE_SIZE) else { + return NtStatus::SECTION_TOO_BIG; + }; + if NonZeroPageSize::::new(size).is_none() { + return NtStatus::INVALID_PARAMETER; + } + + let name = match self.read_section_name(object_attributes) { + Ok(name) => name, + Err(status) => return status, + }; + let attributes = if allocation_attributes.contains(SectionAllocationAttributes::SEC_RESERVE) + { + SectionAllocationAttributes::SEC_RESERVE + } else { + SectionAllocationAttributes::SEC_COMMIT + }; + let section = Arc::new(SectionObject { + fs_path: None, + size, + attributes, + protection, + backing: SectionBacking::Pagefile(RwLock::new(vec![0; size])), + pagefile_view_active: AtomicBool::new(false), + }); + if let Some(name) = &name { + let status = self.insert_named_section(name, §ion); + if status != NtStatus::SUCCESS { + return status; + } + } + self.publish_section_handle(section_handle, section, granted_access) + } + + #[expect( + clippy::too_many_arguments, + reason = "NtCreateSectionEx extends NtCreateSection with two ABI parameters" + )] + pub(crate) fn sys_nt_create_section_ex( + &self, + section_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + maximum_size: Option>, + section_page_protection: u32, + allocation_attributes: u32, + file_handle: Handle, + extended_parameters: Option>, + extended_parameter_count: u32, + ) -> NtStatus { + if extended_parameters.is_some() || extended_parameter_count != 0 { + return NtStatus::INVALID_PARAMETER; + } + self.sys_nt_create_section( + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + ) + } + + // Wine's open_mapping and ReactOS object-manager open path first resolve a named Section object; + // KnownDlls misses then report object-manager path/name status rather than a link-specific error. + pub(crate) fn sys_nt_open_section( + &self, + section_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + // Host ntdll zeroes the output handle before resolving a missing section name. + if section_handle + .write_at_offset(0, Handle::default()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + let granted_access = SectionAccess::from_desired_access(desired_access); + if granted_access.is_empty() { + return NtStatus::ACCESS_DENIED; + } + if object_attributes.is_none() { + return NtStatus::INVALID_PARAMETER; + } + let name = match self.read_required_section_name(object_attributes) { + Ok(name) => name, + Err(status) => return status, + }; + if self + .process + .directory_namespace + .resolve_object(&name) + .is_ok() + { + return NtStatus::OBJECT_TYPE_MISMATCH; + } + if let Some(section) = self.named_section(&name) { + return self.publish_section_handle(section_handle, section, granted_access); + } + let Some(fs_path) = known_dll_section_fs_path(&name) else { + return section_missing_status( + self.process + .directory_namespace + .parent_directory_exists(&name), + ); + }; + let Ok(file_status) = self.fs.file_status(&fs_path) else { + return NtStatus::OBJECT_NAME_NOT_FOUND; + }; + let section = Arc::new(SectionObject { + fs_path: Some(fs_path), + size: file_status.size, + attributes: SectionAllocationAttributes::SEC_FILE + | SectionAllocationAttributes::SEC_IMAGE, + protection: PageProtection::PAGE_EXECUTE_WRITECOPY, + backing: SectionBacking::ImageFile, + pagefile_view_active: AtomicBool::new(false), + }); + self.publish_section_handle(section_handle, section, granted_access) + } + + // Wine's NtQuerySection and ReactOS NtQuerySection support Basic and Image classes with the same + // externally visible output sizes; Image information is populated from the PE section object. + pub(crate) fn sys_nt_query_section( + &self, + section_handle: Handle, + section_information_class: u32, + section_information: MutPtr, + section_information_length: usize, + return_length: Option>, + ) -> NtStatus { + let Ok(information_class) = SectionInformationClass::try_from(section_information_class) + else { + return NtStatus::INVALID_INFO_CLASS; + }; + let entry = match self.section_entry(section_handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + let result = entry.with_entry(|entry| { + entry + .granted_access + .require(SectionAccess::QUERY) + .map(|()| Arc::clone(&entry.section)) + }); + let section = match result { + Ok(section) => section, + Err(status) => return status, + }; + match information_class { + SectionInformationClass::Basic => write_section_basic_information::( + §ion, + section_information, + section_information_length, + return_length, + ), + SectionInformationClass::Image => write_section_image_information::( + §ion, + Arc::clone(&self.fs), + section_information, + section_information_length, + return_length, + ), + } + } + + // Wine's virtual_map_section and ReactOS MmMapViewOfSection dispatch by section backing: + // pagefile views copy from the control backing, while image views use the PE image map path. + pub(crate) fn sys_nt_map_view_of_section( + &self, + request: MapViewOfSectionParameters, + ) -> NtStatus { + if !request.process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + let Some(base) = request.base_address.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let Some(requested_view_size) = request.view_size.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let section_offset = match request.section_offset { + Some(section_offset) => match section_offset.read_at_offset(0) { + Some(value) if value >= 0 => usize::try_from(value).unwrap_or(usize::MAX), + Some(_) => return NtStatus::INVALID_PARAMETER, + None => return NtStatus::ACCESS_VIOLATION, + }, + None => 0, + }; + if base != 0 + || request.zero_bits != 0 + || request.commit_size != 0 + || !section_offset.is_multiple_of(PAGE_SIZE) + || !matches!(request.inherit_disposition, VIEW_SHARE | VIEW_UNMAP) + || request.allocation_type & !SUPPORTED_MAP_ALLOCATION_TYPES != 0 + { + return NtStatus::INVALID_PARAMETER; + } + let Some((page_protection, permissions)) = parse_page_protection(request.page_protection) + else { + return NtStatus::INVALID_PAGE_PROTECTION; + }; + let entry = match self.section_entry(request.section_handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + let result = entry.with_entry(|entry| { + required_map_access(page_protection) + .and_then(|required| entry.granted_access.require(required)) + .map(|()| Arc::clone(&entry.section)) + }); + let section = match result { + Ok(section) => section, + Err(status) => return status, + }; + match §ion.backing { + SectionBacking::Pagefile(_) => self.map_pagefile_section( + request, + §ion, + requested_view_size, + section_offset, + page_protection, + permissions, + ), + SectionBacking::ImageFile => self.map_image_section(request, §ion, page_protection), + } + } + + // Wine's NtMapViewOfSectionEx validates the MEM_EXTENDED_PARAMETER array before delegating to + // the same virtual_map_section path; ReactOS keeps the same base map operation split. + pub(crate) fn sys_nt_map_view_of_section_ex( + &self, + request: MapViewOfSectionParameters, + extended_parameters: Option>, + extended_parameter_count: u32, + ) -> NtStatus { + if extended_parameters.is_some() || extended_parameter_count != 0 { + // TODO(section-subsystem): model MEM_EXTENDED_PARAMETER address requirements. + return NtStatus::INVALID_PARAMETER; + } + self.sys_nt_map_view_of_section(request) + } + + // Wine and ReactOS route unmap through the virtual memory view teardown path after process-handle + // validation, so the shim consumes the tracked view before removing its pages. + pub(crate) fn sys_nt_unmap_view_of_section( + &self, + process_handle: ProcessHandle, + base_address: usize, + ) -> NtStatus { + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + let Some((view_base, view)) = self.remove_section_view_for_address(base_address) else { + return NtStatus::NOT_MAPPED_VIEW; + }; + let ptr = MutPtr::::from_usize(view_base); + if let Err(status) = synchronize_pagefile_view::( + view_base, + &view, + &self.process.virtual_allocations, + ) { + self.process.section_views.write().insert(view_base, view); + return status; + } + // SAFETY: Section views are tracked only after this shim successfully creates the pages; + // unmapping consumes the tracked view and removes the exact owned range. + if unsafe { self.global.page_manager.remove_pages(ptr, view.size) }.is_err() { + self.process.section_views.write().insert(view_base, view); + return NtStatus::UNABLE_TO_FREE_VM; + } + self.process.virtual_allocations.write().remove(&view_base); + release_pagefile_view_slot(&view); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_unmap_view_of_section_ex( + &self, + process_handle: ProcessHandle, + base_address: usize, + flags: u32, + ) -> NtStatus { + let flags = UnmapViewOfSectionFlags::from_bits_retain(flags); + if !flags.is_empty() { + return NtStatus::INVALID_PARAMETER; + } + self.sys_nt_unmap_view_of_section(process_handle, base_address) + } + + fn publish_section_handle( + &self, + section_handle: MutPtr, + section: Arc>, + granted_access: SectionAccess, + ) -> NtStatus { + let handle = match self.insert_section_handle(section, granted_access) { + Ok(handle) => handle, + Err(status) => return status, + }; + if section_handle.write_at_offset(0, handle).is_none() { + self.close_section_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + fn insert_named_section(&self, name: &str, section: &Arc>) -> NtStatus { + if self + .process + .directory_namespace + .resolve_object(name) + .is_ok() + { + return NtStatus::OBJECT_NAME_COLLISION; + } + if !self + .process + .directory_namespace + .parent_directory_exists(name) + { + return NtStatus::OBJECT_PATH_NOT_FOUND; + } + let key = section_key(name); + let mut namespace = self.process.section_namespace.write(); + if let Some(existing) = namespace.get(&key) + && existing.upgrade().is_some() + { + return NtStatus::OBJECT_NAME_EXISTS; + } + namespace.insert(key, Arc::downgrade(section)); + NtStatus::SUCCESS + } + + fn named_section(&self, name: &str) -> Option>> { + let key = section_key(name); + let mut namespace = self.process.section_namespace.write(); + let section = namespace.get(&key).and_then(alloc::sync::Weak::upgrade); + if section.is_none() { + namespace.remove(&key); + } + section + } + + fn map_pagefile_section( + &self, + request: MapViewOfSectionParameters, + section: &Arc>, + requested_view_size: usize, + section_offset: usize, + page_protection: PageProtection, + permissions: MemoryRegionPermissions, + ) -> NtStatus { + if section_offset > section.size { + return NtStatus::INVALID_VIEW_SIZE; + } + let remaining = section.size - section_offset; + let view_size = if requested_view_size == 0 { + remaining + } else { + requested_view_size + }; + if view_size == 0 || view_size > remaining { + return NtStatus::INVALID_VIEW_SIZE; + } + let Some(mapped_size) = view_size.checked_next_multiple_of(PAGE_SIZE) else { + return NtStatus::INVALID_VIEW_SIZE; + }; + let Some(length) = NonZeroPageSize::::new(mapped_size) else { + return NtStatus::INVALID_VIEW_SIZE; + }; + let backing = match §ion.backing { + SectionBacking::Pagefile(backing) => backing, + SectionBacking::ImageFile => return NtStatus::INVALID_FILE_FOR_SECTION, + }; + if section.pagefile_view_active.swap(true, Ordering::AcqRel) { + litebox_util_log::debug!( + section_size = section.size, + requested_view_size, + section_offset; + "Rejected second active pagefile section view" + ); + // Host 25H2 allows multiple simultaneous pagefile views of one section + // (second NtMapViewOfSection -> STATUS_SUCCESS). LiteBox returns + // TODO(section-subsystem): allow this once PageManager has first-class shared + // anonymous backing for one section object mapped at multiple virtual addresses. + return NtStatus::NOT_SUPPORTED; + } + let Ok(mapping) = create_pages( + &self.global.page_manager, + None, + length, + CreatePagesFlags::empty(), + permissions, + |ptr| { + let backing = backing.read(); + let bytes = &backing[section_offset..section_offset + mapped_size]; + ptr.copy_from_slice(0, bytes) + .ok_or(litebox::mm::linux::MappingError::OutOfMemory)?; + Ok(mapped_size) + }, + ) else { + section.pagefile_view_active.store(false, Ordering::Release); + return NtStatus::NO_MEMORY; + }; + let base = mapping.as_usize(); + if request.base_address.write_at_offset(0, base).is_none() + || request.view_size.write_at_offset(0, view_size).is_none() + { + let _ = remove_view_pages::(&self.global.page_manager, base, mapped_size); + section.pagefile_view_active.store(false, Ordering::Release); + return NtStatus::ACCESS_VIOLATION; + } + self.process.section_views.write().insert( + base, + WindowsSectionView { + size: mapped_size, + section_offset, + section: Some(Arc::clone(section)), + }, + ); + self.process.virtual_allocations.write().insert( + base, + crate::WindowsVirtualAllocation { + base, + size: mapped_size, + allocation_protect: section.protection, + type_: MemoryType::MEM_MAPPED, + pages: committed_pages(base, mapped_size, page_protection), + }, + ); + NtStatus::SUCCESS + } + + fn map_image_section( + &self, + request: MapViewOfSectionParameters, + section: &SectionObject, + page_protection: PageProtection, + ) -> NtStatus { + let Some(fs_path) = §ion.fs_path else { + return NtStatus::INVALID_FILE_FOR_SECTION; + }; + if matches!( + required_map_access(page_protection), + Ok(required) if required.contains(SectionAccess::MAP_WRITE) + ) { + litebox_util_log::debug!( + page_protection:% = format_args!("{:#x}", page_protection.bits()), + fs_path:% = fs_path; + "Rejected writable image section view" + ); + // Host 25H2 maps SEC_IMAGE with PAGE_READWRITE/PAGE_EXECUTE_READWRITE successfully + // (NtMapViewOfSection returns STATUS_IMAGE_NOT_AT_BASE in the probe). LiteBox rejects + // TODO(section-subsystem): allow this once image mappings support writable + // copy-on-write/shared image pages. + return NtStatus::SECTION_PROTECTION; + } + let mapping = match crate::loader::load_image_section( + self.global.platform, + Arc::clone(&self.fs), + fs_path, + &self.global.page_manager, + &self.process.virtual_allocations, + ) { + Ok(mapping) => mapping, + Err(crate::loader::WindowsLoadError::Access(_)) => { + return NtStatus::OBJECT_NAME_NOT_FOUND; + } + Err(crate::loader::WindowsLoadError::Load(_)) => return NtStatus::NO_MEMORY, + Err(_) => return NtStatus::INVALID_FILE_FOR_SECTION, + }; + if request + .base_address + .write_at_offset(0, mapping.base_addr) + .is_none() + || request + .view_size + .write_at_offset(0, mapping.image_size) + .is_none() + { + let _ = remove_view_pages::( + &self.global.page_manager, + mapping.base_addr, + mapping.mapping_size, + ); + self.process + .virtual_allocations + .write() + .remove(&mapping.base_addr); + return NtStatus::ACCESS_VIOLATION; + } + self.process.section_views.write().insert( + mapping.base_addr, + WindowsSectionView { + size: mapping.mapping_size, + section_offset: 0, + section: None, + }, + ); + NtStatus::SUCCESS + } + + fn remove_section_view_for_address( + &self, + base_address: usize, + ) -> Option<(usize, WindowsSectionView)> { + let mut views = self.process.section_views.write(); + let (&view_base, view) = views.range(..=base_address).next_back()?; + let view = view.clone(); + let view_end = view_base.checked_add(view.size)?; + if base_address < view_end { + views.remove(&view_base); + Some((view_base, view)) + } else { + None + } + } + + fn read_section_name( + &self, + object_attributes: Option>, + ) -> Result, NtStatus> { + let (_, directory_name) = + self.read_directory_object_attributes(object_attributes, false)?; + Ok(directory_name.map(|name| name.original_path)) + } + + fn read_required_section_name( + &self, + object_attributes: Option>, + ) -> Result { + let (_, Some(directory_name)) = + self.read_directory_object_attributes(object_attributes, true)? + else { + return Err(NtStatus::INVALID_PARAMETER); + }; + Ok(directory_name.original_path) + } +} + +fn section_key(path: &str) -> String { + path.to_ascii_lowercase() +} + +fn section_missing_status(parent_exists: bool) -> NtStatus { + if parent_exists { + NtStatus::OBJECT_NAME_NOT_FOUND + } else { + NtStatus::OBJECT_PATH_NOT_FOUND + } +} + +fn known_dll_section_fs_path(object_path: &str) -> Option { + let (dll_name, fs_directory) = + if let Some(rest) = strip_case_insensitive_prefix(object_path, r"\KnownDlls\") { + (rest, "/Windows/System32/") + } else if let Some(rest) = strip_case_insensitive_prefix(object_path, r"\KnownDlls32\") { + (rest, "/Windows/SysWOW64/") + } else { + return None; + }; + if dll_name.contains(['\\', '/']) || !ends_with_ignore_ascii_case(dll_name, ".dll") { + return None; + } + let mut fs_path = String::from(fs_directory); + fs_path.push_str(&dll_name.to_ascii_lowercase()); + Some(fs_path) +} + +fn strip_case_insensitive_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { + value + .get(..prefix.len()) + .is_some_and(|head| head.eq_ignore_ascii_case(prefix)) + .then_some(&value[prefix.len()..]) +} + +fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool { + value + .get(value.len().saturating_sub(suffix.len())..) + .is_some_and(|tail| tail.eq_ignore_ascii_case(suffix)) +} + +fn required_map_access(protection: PageProtection) -> Result { + let base = protection.bits() & PageProtection::BASE_MASK; + let required = if matches!( + base, + value if value == PageProtection::PAGE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + ) { + SectionAccess::MAP_WRITE + } else if matches!( + base, + value if value == PageProtection::PAGE_EXECUTE.bits() + || value == PageProtection::PAGE_EXECUTE_READ.bits() + || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() + ) { + SectionAccess::MAP_EXECUTE + } else if base == PageProtection::PAGE_NOACCESS.bits() { + return Err(NtStatus::SECTION_PROTECTION); + } else { + SectionAccess::MAP_READ + }; + Ok(required) +} + +fn write_section_basic_information( + section: &SectionObject, + section_information: MutPtr, + section_information_length: usize, + return_length: Option>, +) -> NtStatus { + let required_len = size_of::(); + if section_information_length < required_len { + return NtStatus::INFO_LENGTH_MISMATCH; + } + let Ok(size) = i64::try_from(section.size) else { + return NtStatus::SECTION_TOO_BIG; + }; + let info = SectionBasicInformation { + base_address: 0, + attributes: section.attributes.bits(), + _padding: 0, + size, + }; + let output = + MutPtr::::from_usize(section_information.as_usize()); + if output.write_at_offset(0, info).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(return_length) = return_length + && return_length.write_at_offset(0, required_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS +} + +fn write_section_image_information( + section: &SectionObject, + fs: Arc, + section_information: MutPtr, + section_information_length: usize, + return_length: Option>, +) -> NtStatus { + if !matches!(section.backing, SectionBacking::ImageFile) { + return NtStatus::SECTION_NOT_IMAGE; + } + let required_len = size_of::(); + if section_information_length < required_len { + return NtStatus::INFO_LENGTH_MISMATCH; + } + let Some(fs_path) = §ion.fs_path else { + return NtStatus::INVALID_FILE_FOR_SECTION; + }; + let metadata = match crate::loader::image_section_metadata(fs, fs_path) { + Ok(metadata) => metadata, + Err(crate::loader::WindowsLoadError::Access(_)) => return NtStatus::OBJECT_NAME_NOT_FOUND, + Err(_) => return NtStatus::INVALID_FILE_FOR_SECTION, + }; + // Host ntdll reports ReturnLength=64 for SectionImageInformation on x64; the public + // winternl.h layout ends at CheckSum and has no trailing extension fields. + let info = SectionImageInformation { + transfer_address: metadata.transfer_address, + zero_bits: 0, + _padding0: 0, + maximum_stack_size: 0, + committed_stack_size: 0, + subsystem_type: metadata.subsystem, + subsystem_minor_version: metadata.subsystem_minor_version, + subsystem_major_version: metadata.subsystem_major_version, + gp_value: 0, + image_characteristics: metadata.image_characteristics, + dll_characteristics: metadata.dll_characteristics, + machine: metadata.machine, + image_contains_code: 1, + image_flags: 0, + loader_flags: 0, + image_file_size: metadata.file_size, + checksum: 0, + }; + let output = + MutPtr::::from_usize(section_information.as_usize()); + if output.write_at_offset(0, info).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(return_length) = return_length + && return_length.write_at_offset(0, required_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS +} + +fn release_pagefile_view_slot(view: &WindowsSectionView) { + if let Some(section) = &view.section + && matches!(section.backing, SectionBacking::Pagefile(_)) + { + section.pagefile_view_active.store(false, Ordering::Release); + } +} + +pub(super) fn synchronize_pagefile_views_in_range( + views: &crate::WindowsSectionViews, + virtual_allocations: &WindowsVirtualAllocations, + base: usize, + size: usize, +) -> Result<(), NtStatus> { + let Some(end) = base.checked_add(size) else { + return Err(NtStatus::INVALID_PARAMETER); + }; + let snapshots = views + .read() + .iter() + .filter_map(|(&view_base, view)| { + let view_end = view_base.checked_add(view.size)?; + (view_base < end && base < view_end).then(|| (view_base, view.clone())) + }) + .collect::>(); + for (view_base, view) in snapshots { + synchronize_pagefile_view::(view_base, &view, virtual_allocations)?; + } + Ok(()) +} + +fn synchronize_pagefile_view( + view_base: usize, + view: &WindowsSectionView, + virtual_allocations: &WindowsVirtualAllocations, +) -> Result<(), NtStatus> { + let Some(section) = &view.section else { + return Ok(()); + }; + let SectionBacking::Pagefile(backing) = §ion.backing else { + return Ok(()); + }; + let mut backing = backing.write(); + let Some(end) = view.section_offset.checked_add(view.size) else { + return Err(NtStatus::INVALID_PARAMETER); + }; + if end > backing.len() { + return Err(NtStatus::INVALID_PARAMETER); + } + for (range_start, range_end) in + readable_section_view_ranges(virtual_allocations, view_base, view.size) + { + let range_len = range_end - range_start; + let view_offset = range_start - view_base; + let backing_start = view + .section_offset + .checked_add(view_offset) + .ok_or(NtStatus::INVALID_PARAMETER)?; + let backing_end = backing_start + .checked_add(range_len) + .ok_or(NtStatus::INVALID_PARAMETER)?; + if backing_end > backing.len() { + return Err(NtStatus::INVALID_PARAMETER); + } + let source = ConstPtr::::from_usize(range_start); + for (offset, byte) in backing[backing_start..backing_end].iter_mut().enumerate() { + let offset = isize::try_from(offset).map_err(|_| NtStatus::INVALID_PARAMETER)?; + *byte = source + .read_at_offset(offset) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + } + } + Ok(()) +} + +fn readable_section_view_ranges( + virtual_allocations: &WindowsVirtualAllocations, + view_base: usize, + view_size: usize, +) -> Vec<(usize, usize)> { + let Some(view_end) = view_base.checked_add(view_size) else { + return Vec::new(); + }; + let allocations = virtual_allocations.read(); + let Some((_, allocation)) = allocations.range(..=view_base).next_back() else { + return Vec::new(); + }; + if allocation + .base + .checked_add(allocation.size) + .is_none_or(|allocation_end| view_end > allocation_end) + { + return Vec::new(); + } + allocation + .pages + .overlapping(view_base..view_end) + .filter(|(_, protect)| page_protection_is_readable(**protect)) + .map(|(range, _)| { + let range_start = range.start.max(view_base); + let range_end = range.end.min(view_end); + (range_start, range_end) + }) + .filter(|(range_start, range_end)| range_start < range_end) + .collect() +} + +fn page_protection_is_readable(protect: PageProtection) -> bool { + matches!( + protect.bits() & PageProtection::BASE_MASK, + value if value == PageProtection::PAGE_READONLY.bits() + || value == PageProtection::PAGE_READWRITE.bits() + || value == PageProtection::PAGE_WRITECOPY.bits() + || value == PageProtection::PAGE_EXECUTE_READ.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() + ) +} + +fn committed_pages( + base: usize, + size: usize, + protect: PageProtection, +) -> RangeMap { + let mut pages = RangeMap::new(); + if let Some(end) = base.checked_add(size) { + pages.insert(base..end, protect); + } + pages +} + +fn remove_view_pages( + page_manager: &crate::WindowsPageManager, + base: usize, + size: usize, +) -> Result<(), ()> { + let ptr = MutPtr::::from_usize(base); + // SAFETY: The caller passes a section view range created by this module and not yet exposed, + // or a tracked view being rolled back after output write failure. + unsafe { page_manager.remove_pages(ptr, size) }.map_err(|_| ()) +} + +#[cfg(test)] +mod tests { + use core::mem::{size_of, size_of_val}; + + use litebox::platform::RawMutPointer as _; + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::{ObjectAttributes, UnicodeString}; + use crate::tests::{ + TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task, test_task_with_nls_files, + }; + + const TEST_PE_IMAGE_BASE: usize = 0x1800_0000; + const TEST_PE_ENTRY_RVA: u32 = 0x1000; + const TEST_PE_IMAGE_SIZE: u32 = 0x2000; + const TEST_PE_FILE_SIZE: u32 = 0x400; + const TEST_PE_SUBSYSTEM: u16 = 3; + const TEST_PE_MAJOR_SUBSYSTEM_VERSION: u16 = 10; + const TEST_PE_MINOR_SUBSYSTEM_VERSION: u16 = 0; + const TEST_PE_CHARACTERISTICS: u16 = 0x2022; + const TEST_PE_DLL_CHARACTERISTICS: u16 = 0x8160; + const TEST_PE_MACHINE: u16 = 0x8664; + + fn wide(value: &str) -> alloc::vec::Vec { + value.encode_utf16().collect() + } + + fn unicode(value: &[u16]) -> UnicodeString { + UnicodeString { + length: u16::try_from(size_of_val(value)).unwrap(), + maximum_length: u16::try_from(size_of_val(value)).unwrap(), + padding_0: [0; 4], + buffer: value.as_ptr() as usize, + } + } + + fn object_attributes(name: &UnicodeString) -> ObjectAttributes { + ObjectAttributes { + length: u32::try_from(size_of::()).unwrap(), + root_directory: Handle::from_raw(0), + object_name: core::ptr::from_ref(name) as usize, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + } + } + + fn create_pagefile_section( + task: &Task, + access: u32, + size: i64, + ) -> Handle { + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut handle), + access, + None, + Some(const_ptr(&size)), + PageProtection::PAGE_READWRITE.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::SUCCESS + ); + handle + } + + fn map_pagefile_section(task: &Task, handle: Handle) -> (usize, usize) { + let mut base = 0usize; + let mut view_size = 0usize; + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::SUCCESS + ); + (base, view_size) + } + + fn minimal_pe_image() -> alloc::vec::Vec { + let mut image = alloc::vec![0; usize::try_from(TEST_PE_FILE_SIZE).unwrap()]; + write_u16(&mut image, 0x00, 0x5a4d); + write_u32(&mut image, 0x3c, 0x80); + write_u32(&mut image, 0x80, 0x0000_4550); + + let file_header = 0x84; + write_u16(&mut image, file_header, TEST_PE_MACHINE); + write_u16(&mut image, file_header + 2, 1); + write_u16(&mut image, file_header + 16, 0xf0); + write_u16(&mut image, file_header + 18, TEST_PE_CHARACTERISTICS); + + let optional = 0x98; + write_u16(&mut image, optional, 0x20b); + write_u32(&mut image, optional + 16, TEST_PE_ENTRY_RVA); + write_u64(&mut image, optional + 24, TEST_PE_IMAGE_BASE as u64); + write_u32(&mut image, optional + 32, u32::try_from(PAGE_SIZE).unwrap()); + write_u32(&mut image, optional + 36, 0x200); + write_u16(&mut image, optional + 48, TEST_PE_MAJOR_SUBSYSTEM_VERSION); + write_u16(&mut image, optional + 50, TEST_PE_MINOR_SUBSYSTEM_VERSION); + write_u32(&mut image, optional + 56, TEST_PE_IMAGE_SIZE); + write_u32(&mut image, optional + 60, TEST_PE_FILE_SIZE); + write_u16(&mut image, optional + 68, TEST_PE_SUBSYSTEM); + write_u16(&mut image, optional + 70, TEST_PE_DLL_CHARACTERISTICS); + write_u64(&mut image, optional + 72, 0x100000); + write_u64(&mut image, optional + 80, 0x1000); + write_u64(&mut image, optional + 88, 0x100000); + write_u64(&mut image, optional + 96, 0x1000); + write_u32(&mut image, optional + 108, 16); + + let section = 0x188; + image[section..section + 5].copy_from_slice(b".text"); + write_u32(&mut image, section + 8, 1); + write_u32(&mut image, section + 12, TEST_PE_ENTRY_RVA); + write_u32(&mut image, section + 16, 0x200); + write_u32(&mut image, section + 20, 0x200); + write_u32(&mut image, section + 36, 0x6000_0020); + image + } + + fn write_u16(output: &mut [u8], offset: usize, value: u16) { + output[offset..offset + size_of::()].copy_from_slice(&value.to_le_bytes()); + } + + fn write_u32(output: &mut [u8], offset: usize, value: u32) { + output[offset..offset + size_of::()].copy_from_slice(&value.to_le_bytes()); + } + + fn write_u64(output: &mut [u8], offset: usize, value: u64) { + output[offset..offset + size_of::()].copy_from_slice(&value.to_le_bytes()); + } + + #[test] + fn nt_create_section_creates_queryable_pagefile_section() { + let task = test_task(); + let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2345); + let mut info = SectionBasicInformation { + base_address: usize::MAX, + attributes: u32::MAX, + _padding: u32::MAX, + size: -1, + }; + let mut return_length = 0usize; + + assert_eq!( + task.sys_nt_query_section( + handle, + SectionInformationClass::Basic as u32, + mut_byte_ptr(&mut info), + size_of::(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::SUCCESS + ); + assert_eq!(return_length, size_of::()); + assert_eq!(info.base_address, 0); + assert_eq!( + info.attributes, + SectionAllocationAttributes::SEC_COMMIT.bits() + ); + assert_eq!(info.size, 0x3000); + + let mut too_small = [0xcc; size_of::() - 1]; + let too_small_len = too_small.len(); + return_length = 0x5555_5555; + // Host 25H2 leaves ReturnLength untouched on INFO_LENGTH_MISMATCH + // (Basic len=23 -> ret stays sentinel) and writes 0x18 only on success. + assert_eq!( + task.sys_nt_query_section( + handle, + SectionInformationClass::Basic as u32, + mut_byte_ptr(&mut too_small), + too_small_len, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!(return_length, 0x5555_5555); + } + + #[test] + fn nt_query_section_image_information_uses_pe_headers() { + let image = minimal_pe_image(); + let task = test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); + let name = wide(r"\KnownDlls\kernel32.dll"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut handle), + SectionAccess::QUERY.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + + let mut info = SectionImageInformation { + transfer_address: 0, + zero_bits: u32::MAX, + _padding0: u32::MAX, + maximum_stack_size: usize::MAX, + committed_stack_size: usize::MAX, + subsystem_type: u32::MAX, + subsystem_minor_version: u16::MAX, + subsystem_major_version: u16::MAX, + gp_value: u32::MAX, + image_characteristics: u16::MAX, + dll_characteristics: u16::MAX, + machine: u16::MAX, + image_contains_code: u8::MAX, + image_flags: u8::MAX, + loader_flags: u32::MAX, + image_file_size: u32::MAX, + checksum: u32::MAX, + }; + let mut return_length = 0usize; + assert_eq!( + task.sys_nt_query_section( + handle, + SectionInformationClass::Image as u32, + mut_byte_ptr(&mut info), + size_of::(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::SUCCESS + ); + + assert_eq!(return_length, size_of::()); + assert_eq!( + info.transfer_address, + TEST_PE_IMAGE_BASE + TEST_PE_ENTRY_RVA as usize + ); + assert_eq!(info.subsystem_type, u32::from(TEST_PE_SUBSYSTEM)); + assert_eq!( + info.subsystem_major_version, + TEST_PE_MAJOR_SUBSYSTEM_VERSION + ); + assert_eq!( + info.subsystem_minor_version, + TEST_PE_MINOR_SUBSYSTEM_VERSION + ); + assert_eq!(info.image_characteristics, TEST_PE_CHARACTERISTICS); + assert_eq!(info.dll_characteristics, TEST_PE_DLL_CHARACTERISTICS); + assert_eq!(info.machine, TEST_PE_MACHINE); + assert_eq!(info.image_contains_code, 1); + assert_eq!(info.image_file_size, TEST_PE_FILE_SIZE); + + let mut too_small = [0xcc; size_of::() - 1]; + let too_small_len = too_small.len(); + return_length = 0x5555_5555; + // Host 25H2 leaves ReturnLength untouched on INFO_LENGTH_MISMATCH + // (Image len=63 -> ret stays sentinel) and writes 0x40 only on success. + assert_eq!( + task.sys_nt_query_section( + handle, + SectionInformationClass::Image as u32, + mut_byte_ptr(&mut too_small), + too_small_len, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!(return_length, 0x5555_5555); + } + + #[test] + fn image_section_rejects_writable_view_protection() { + let image = minimal_pe_image(); + let task = test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); + let name = wide(r"\KnownDlls\kernel32.dll"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut handle), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + + let mut base = 0usize; + let mut view_size = 0usize; + // Host 25H2 maps SEC_IMAGE with PAGE_READWRITE successfully + // (NtMapViewOfSection returns STATUS_IMAGE_NOT_AT_BASE). LiteBox rejects writable image + // views until image mappings are backed by real shared image pages. + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::SECTION_PROTECTION + ); + assert_eq!(base, 0); + assert_eq!(view_size, 0); + + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_EXECUTE_READ.bits(), + }), + NtStatus::SUCCESS + ); + assert_ne!(base, 0); + assert_ne!(view_size, 0); + } + + #[test] + fn section_output_handles_follow_host_probe_contracts() { + let task = test_task(); + let name = wide(r"\KnownDlls\DefinitelyMissingLiteBoxProbe.dll"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let mut open_handle = Handle::from_raw(0x1111_2222); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut open_handle), + SectionAccess::QUERY.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!(open_handle, Handle::default()); + + let mut create_handle = Handle::from_raw(0x3333_4444); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut create_handle), + SectionAccess::ALL_ACCESS.bits(), + None, + None, + PageProtection::PAGE_READWRITE.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::INVALID_PARAMETER_4 + ); + assert_eq!(create_handle, Handle::from_raw(0x3333_4444)); + } + + #[test] + fn nt_map_view_of_section_maps_writable_pagefile_section() { + let task = test_task(); + let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let (base, view_size) = map_pagefile_section(&task, handle); + assert_ne!(base, 0); + assert_eq!(view_size, 0x2000); + + let mapped = MutPtr::::from_usize(base); + assert_eq!(mapped.read_at_offset(0), Some(0)); + assert!(mapped.write_at_offset(0, 0xfeed_cafe).is_some()); + assert_eq!(mapped.read_at_offset(0), Some(0xfeed_cafe)); + + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, base + 0x100), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, base), + NtStatus::NOT_MAPPED_VIEW + ); + } + + #[test] + fn nt_map_view_of_section_ex_maps_and_unmaps_pagefile_section() { + let task = test_task(); + let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let mut base = 0usize; + let mut view_size = 0usize; + + assert_eq!( + task.sys_nt_map_view_of_section_ex( + MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }, + None, + 0, + ), + NtStatus::SUCCESS + ); + assert_ne!(base, 0); + assert_eq!(view_size, 0x2000); + + let mapped = MutPtr::::from_usize(base); + assert!(mapped.write_at_offset(0, 0x1234_5678).is_some()); + assert_eq!(mapped.read_at_offset(0), Some(0x1234_5678)); + assert_eq!( + task.sys_nt_unmap_view_of_section_ex(ProcessHandle::CURRENT, base, 0), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_unmap_view_of_section_ex(ProcessHandle::CURRENT, base, 0), + NtStatus::NOT_MAPPED_VIEW + ); + } + + #[test] + fn nt_map_view_of_section_ex_rejects_extended_parameters() { + let task = test_task(); + let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x1000); + let mut base = 0usize; + let mut view_size = 0usize; + let extended_parameter = 0u8; + + assert_eq!( + task.sys_nt_map_view_of_section_ex( + MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }, + Some(const_ptr(&extended_parameter)), + 1, + ), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(base, 0); + assert_eq!(view_size, 0); + } + + #[test] + fn pagefile_section_rejects_second_active_view_across_handles() { + let task = test_task(); + let name = wide(r"\BaseNamedObjects\LiteBoxSingleViewSection"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let size = 0x2000i64; + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut handle), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + Some(const_ptr(&size)), + PageProtection::PAGE_READWRITE.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::SUCCESS + ); + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut opened), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + let (first_base, first_size) = map_pagefile_section(&task, handle); + assert_eq!(first_size, 0x2000); + + let mut second_base = 0usize; + let mut second_size = 0usize; + // Host 25H2 permits this second simultaneous pagefile view (STATUS_SUCCESS). LiteBox + // deliberately returns STATUS_NOT_SUPPORTED until shared anonymous backing exists. + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: opened, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut second_base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut second_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::NOT_SUPPORTED + ); + assert_eq!(second_base, 0); + assert_eq!(second_size, 0); + + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), + NtStatus::SUCCESS + ); + let (second_base, second_size) = map_pagefile_section(&task, opened); + assert_eq!(second_size, 0x2000); + assert_ne!(second_base, 0); + } + + #[test] + fn pagefile_view_unmap_remap_preserves_guest_writes() { + let task = test_task(); + let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let (first_base, first_size) = map_pagefile_section(&task, handle); + assert_eq!(first_size, 0x2000); + + let first = MutPtr::::from_usize(first_base); + assert!(first.write_at_offset(0, 0xdead_beef).is_some()); + let first_second_page = MutPtr::::from_usize(first_base + PAGE_SIZE); + assert!(first_second_page.write_at_offset(0, 0x0bad_f00d).is_some()); + + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), + NtStatus::SUCCESS + ); + + let (second_base, second_size) = map_pagefile_section(&task, handle); + assert_eq!(second_size, 0x2000); + assert_ne!(second_base, 0); + let second = MutPtr::::from_usize(second_base); + assert_eq!(second.read_at_offset(0), Some(0xdead_beef)); + let second_second_page = MutPtr::::from_usize(second_base + PAGE_SIZE); + assert_eq!(second_second_page.read_at_offset(0), Some(0x0bad_f00d)); + } + + #[test] + fn dirty_pagefile_view_flushes_before_noaccess_protect() { + let task = test_task(); + let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let (first_base, first_size) = map_pagefile_section(&task, handle); + assert_eq!(first_size, 0x2000); + let first = MutPtr::::from_usize(first_base); + assert!(first.write_at_offset(0, 0x55aa_1234).is_some()); + + let mut protect_base = first_base; + let mut protect_size = first_size; + let mut old_protect = 0; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut protect_base), + mut_ptr(&mut protect_size), + PageProtection::PAGE_NOACCESS.bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), + NtStatus::SUCCESS + ); + + let mut second_base = 0usize; + let mut second_size = 0usize; + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut second_base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut second_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::SUCCESS + ); + assert_ne!(second_base, 0); + assert_eq!(second_size, 0x2000); + let second = MutPtr::::from_usize(second_base); + assert_eq!(second.read_at_offset(0), Some(0x55aa_1234)); + } + + #[test] + fn partial_noaccess_protect_flushes_still_readable_section_pages() { + let task = test_task(); + let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let (first_base, first_size) = map_pagefile_section(&task, handle); + assert_eq!(first_size, 0x2000); + + let first_second_page = MutPtr::::from_usize(first_base + PAGE_SIZE); + assert!(first_second_page.write_at_offset(0, 0x1111_1111).is_some()); + + let mut first_page_base = first_base; + let mut first_page_size = PAGE_SIZE; + let mut old_protect = 0; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut first_page_base), + mut_ptr(&mut first_page_size), + PageProtection::PAGE_NOACCESS.bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::SUCCESS + ); + + assert!(first_second_page.write_at_offset(0, 0x2222_2222).is_some()); + + let mut second_page_base = first_base + PAGE_SIZE; + let mut second_page_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut second_page_base), + mut_ptr(&mut second_page_size), + PageProtection::PAGE_NOACCESS.bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), + NtStatus::SUCCESS + ); + + let (second_base, second_size) = map_pagefile_section(&task, handle); + assert_eq!(second_size, 0x2000); + let second_second_page = MutPtr::::from_usize(second_base + PAGE_SIZE); + assert_eq!(second_second_page.read_at_offset(0), Some(0x2222_2222)); + } + + #[test] + fn nt_open_section_opens_existing_named_pagefile_section() { + let task = test_task(); + let name = wide(r"\BaseNamedObjects\LiteBoxNamedSection"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let size = 0x1000i64; + let mut created = Handle::default(); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut created), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + Some(const_ptr(&size)), + PageProtection::PAGE_READWRITE.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::SUCCESS + ); + + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut opened), + SectionAccess::QUERY.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + assert_ne!(opened, Handle::default()); + assert_ne!(opened, created); + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index c76ad3e2af..2583ab0396 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -155,6 +155,8 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task::new(RawDescriptorStorage::new()), directory_namespace, event_namespace: crate::WindowsEventNamespace::::new(BTreeMap::new()), + section_namespace: crate::WindowsSectionNamespace::::new(BTreeMap::new()), + section_views: crate::WindowsSectionViews::::new(BTreeMap::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), virtual_allocations: crate::WindowsVirtualAllocations::::new( BTreeMap::new(), From 0f0aec6ac03b835fb09a70adf899c1f0589b6b98 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 14:48:52 -0700 Subject: [PATCH 03/16] Document Windows section mapping limitations Explain that LiteBox currently rejects simultaneous pagefile section views because PageManager lacks shared anonymous backing, and document the single-concurrent-view workaround that preserves unmap/remap contents via section-owned backing storage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/windows_pe_support.md | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 doc/windows_pe_support.md diff --git a/doc/windows_pe_support.md b/doc/windows_pe_support.md new file mode 100644 index 0000000000..8f43061cfc --- /dev/null +++ b/doc/windows_pe_support.md @@ -0,0 +1,42 @@ +# Windows PE support + +This document records guest-visible Windows shim behavior that is intentionally +less complete than Windows while LiteBox's VM and object-manager support are +still evolving. + +## Section object mappings + +LiteBox does not yet support true shared mappings for Windows section objects. +The current `PageManager` can create independent anonymous page mappings, but it +does not have first-class shared anonymous backing that can be mapped at +multiple virtual addresses and kept coherent by the memory manager. + +For pagefile-backed sections, the Windows shim therefore allows only one active +view of a section object at a time. A second concurrent +`NtMapViewOfSection` for the same pagefile section is rejected with +`STATUS_NOT_SUPPORTED`, even though Windows permits it. This is an intentional +safety restriction: allowing two independent anonymous mappings would create +two incoherent aliases for one section object. + +The workaround is to make "one concurrent view" behave like Windows for the +supported lifecycle: + +1. The shared `SectionObject` owns a byte-vector backing store for pagefile + section contents. +2. Mapping a pagefile section creates anonymous pages and seeds them from that + backing store. +3. Before a mapped range becomes unreadable, and again when the view is + unmapped, the shim flushes readable guest bytes back into the backing store. +4. Unmapping releases the section object's active-view slot, so the same live + section handle can be mapped again later and observe the bytes written by the + previous view. + +This means LiteBox supports `map -> write -> unmap -> remap` persistence for a +single pagefile section view, but not simultaneous shared views. Real shared +pagefile mappings require PageManager support for shared anonymous backing. + +Image sections have a related limitation: LiteBox maps them as read/execute +image views and rejects writable image view requests with +`STATUS_SECTION_PROTECTION`. Windows can service writable image views with +copy-on-write or shared image-page behavior; LiteBox will need that backing +model before writable image views can be enabled safely. From 6d5b9d91f6b2bb4dd23be1c7c29e695e33e06802 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 15:02:02 -0700 Subject: [PATCH 04/16] Remove redundant section mapping comment Drop the NtMapViewOfSection comment that only restated the branch below without documenting a LiteBox-specific invariant or guest-visible behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 5a5f1b5c2a..fee26015ac 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -464,8 +464,6 @@ impl Task { } } - // Wine's virtual_map_section and ReactOS MmMapViewOfSection dispatch by section backing: - // pagefile views copy from the control backing, while image views use the PE image map path. pub(crate) fn sys_nt_map_view_of_section( &self, request: MapViewOfSectionParameters, From c7c46ca9dce1414d29073d21d2185b275b0c1861 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 15:05:27 -0700 Subject: [PATCH 05/16] Move section mapping note into code Replace the standalone markdown note with a concise Rust doc comment on the pagefile section backing that explains the no-shared-mapping limitation and unmap/remap workaround. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/windows_pe_support.md | 42 -------------------- litebox_shim_windows/src/syscalls/section.rs | 2 + 2 files changed, 2 insertions(+), 42 deletions(-) delete mode 100644 doc/windows_pe_support.md diff --git a/doc/windows_pe_support.md b/doc/windows_pe_support.md deleted file mode 100644 index 8f43061cfc..0000000000 --- a/doc/windows_pe_support.md +++ /dev/null @@ -1,42 +0,0 @@ -# Windows PE support - -This document records guest-visible Windows shim behavior that is intentionally -less complete than Windows while LiteBox's VM and object-manager support are -still evolving. - -## Section object mappings - -LiteBox does not yet support true shared mappings for Windows section objects. -The current `PageManager` can create independent anonymous page mappings, but it -does not have first-class shared anonymous backing that can be mapped at -multiple virtual addresses and kept coherent by the memory manager. - -For pagefile-backed sections, the Windows shim therefore allows only one active -view of a section object at a time. A second concurrent -`NtMapViewOfSection` for the same pagefile section is rejected with -`STATUS_NOT_SUPPORTED`, even though Windows permits it. This is an intentional -safety restriction: allowing two independent anonymous mappings would create -two incoherent aliases for one section object. - -The workaround is to make "one concurrent view" behave like Windows for the -supported lifecycle: - -1. The shared `SectionObject` owns a byte-vector backing store for pagefile - section contents. -2. Mapping a pagefile section creates anonymous pages and seeds them from that - backing store. -3. Before a mapped range becomes unreadable, and again when the view is - unmapped, the shim flushes readable guest bytes back into the backing store. -4. Unmapping releases the section object's active-view slot, so the same live - section handle can be mapped again later and observe the bytes written by the - previous view. - -This means LiteBox supports `map -> write -> unmap -> remap` persistence for a -single pagefile section view, but not simultaneous shared views. Real shared -pagefile mappings require PageManager support for shared anonymous backing. - -Image sections have a related limitation: LiteBox maps them as read/execute -image views and rejects writable image view requests with -`STATUS_SECTION_PROTECTION`. Windows can service writable image views with -copy-on-write or shared image-page behavior; LiteBox will need that backing -model before writable image views can be enabled safely. diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index fee26015ac..d6804f65da 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -36,6 +36,8 @@ const SUPPORTED_MAP_ALLOCATION_TYPES: u32 = MEM_TOP_DOWN | MEM_PHYSICAL | MEM_DIFFERENT_IMAGE_BASE_OK; enum SectionBacking { + /// LiteBox lacks shared anonymous mappings, so pagefile sections allow one + /// active view and flush it here to preserve unmap/remap contents. Pagefile(RwLock>), ImageFile, } From d866c71ac2a5dd772905af75a3d0bff3daf6bb4b Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 15:19:45 -0700 Subject: [PATCH 06/16] Trim redundant section syscall comments Remove section.rs comments that only restated Wine/ReactOS branch structure and kept the comments that anchor host-observed status behavior or LiteBox-specific limitations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index d6804f65da..0ffcd74054 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -230,8 +230,6 @@ impl Task { drop(section); } - // Wine's create_mapping and ReactOS MmCreateSection validate the output handle before creating - // the control object, then select pagefile/file/image backing from AllocationAttributes. #[expect( clippy::too_many_arguments, reason = "NtCreateSection has seven ABI parameters; keeping ABI args explicit avoids reshuffling" @@ -421,8 +419,6 @@ impl Task { self.publish_section_handle(section_handle, section, granted_access) } - // Wine's NtQuerySection and ReactOS NtQuerySection support Basic and Image classes with the same - // externally visible output sizes; Image information is populated from the PE section object. pub(crate) fn sys_nt_query_section( &self, section_handle: Handle, @@ -526,8 +522,6 @@ impl Task { } } - // Wine's NtMapViewOfSectionEx validates the MEM_EXTENDED_PARAMETER array before delegating to - // the same virtual_map_section path; ReactOS keeps the same base map operation split. pub(crate) fn sys_nt_map_view_of_section_ex( &self, request: MapViewOfSectionParameters, @@ -541,8 +535,6 @@ impl Task { self.sys_nt_map_view_of_section(request) } - // Wine and ReactOS route unmap through the virtual memory view teardown path after process-handle - // validation, so the shim consumes the tracked view before removing its pages. pub(crate) fn sys_nt_unmap_view_of_section( &self, process_handle: ProcessHandle, From 77791ffd50585ed0575a23f86b46ad97f3e7ed6e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 15:43:16 -0700 Subject: [PATCH 07/16] Validate pagefile section view protections Reject NtMapViewOfSection pagefile views whose requested protection is incompatible with the section's create-time protection, matching host and ReactOS behavior. Allow compatible PAGE_NOACCESS and copy views while still requiring SECTION_MAP_READ for PAGE_NOACCESS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/mm.rs | 2 +- litebox_shim_windows/src/syscalls/section.rs | 274 +++++++++++++++++-- 2 files changed, 253 insertions(+), 23 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/mm.rs b/litebox_shim_windows/src/syscalls/mm.rs index 28b1aca6bf..5e96f20fdb 100644 --- a/litebox_shim_windows/src/syscalls/mm.rs +++ b/litebox_shim_windows/src/syscalls/mm.rs @@ -1433,7 +1433,7 @@ fn allocation_granularity_aligned_candidate( } } -fn update_permissions( +pub(super) fn update_permissions( page_manager: &WindowsPageManager, aligned_base: usize, aligned_len: usize, diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 0ffcd74054..38346db9e5 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -20,7 +20,9 @@ use rangemap::RangeMap; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::nt_types::{AccessMask, ObjectAttributes}; -use crate::syscalls::mm::{MemoryType, PageProtection, create_pages, parse_page_protection}; +use crate::syscalls::mm::{ + MemoryType, PageProtection, create_pages, parse_page_protection, update_permissions, +}; use crate::syscalls::{Handle, ProcessHandle}; use crate::{ ConstPtr, MutPtr, PAGE_SIZE, ShimFS, ShimPlatform, Task, WindowsSectionView, @@ -501,8 +503,9 @@ impl Task { Err(status) => return status, }; let result = entry.with_entry(|entry| { - required_map_access(page_protection) - .and_then(|required| entry.granted_access.require(required)) + entry + .granted_access + .require(required_map_access(page_protection)) .map(|()| Arc::clone(&entry.section)) }); let section = match result { @@ -664,6 +667,14 @@ impl Task { SectionBacking::Pagefile(backing) => backing, SectionBacking::ImageFile => return NtStatus::INVALID_FILE_FOR_SECTION, }; + if !pagefile_view_protection_is_compatible(section.protection, page_protection) { + litebox_util_log::debug!( + section_protection:% = format_args!("{:#x}", section.protection.bits()), + page_protection:% = format_args!("{:#x}", page_protection.bits()); + "Rejected pagefile section view protection incompatible with section protection" + ); + return NtStatus::SECTION_PROTECTION; + } if section.pagefile_view_active.swap(true, Ordering::AcqRel) { litebox_util_log::debug!( section_size = section.size, @@ -682,7 +693,7 @@ impl Task { None, length, CreatePagesFlags::empty(), - permissions, + MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE, |ptr| { let backing = backing.read(); let bytes = &backing[section_offset..section_offset + mapped_size]; @@ -695,6 +706,14 @@ impl Task { return NtStatus::NO_MEMORY; }; let base = mapping.as_usize(); + if permissions != MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE + && update_permissions(&self.global.page_manager, base, mapped_size, permissions) + .is_err() + { + let _ = remove_view_pages::(&self.global.page_manager, base, mapped_size); + section.pagefile_view_active.store(false, Ordering::Release); + return NtStatus::ACCESS_VIOLATION; + } if request.base_address.write_at_offset(0, base).is_none() || request.view_size.write_at_offset(0, view_size).is_none() { @@ -732,10 +751,7 @@ impl Task { let Some(fs_path) = §ion.fs_path else { return NtStatus::INVALID_FILE_FOR_SECTION; }; - if matches!( - required_map_access(page_protection), - Ok(required) if required.contains(SectionAccess::MAP_WRITE) - ) { + if required_map_access(page_protection).contains(SectionAccess::MAP_WRITE) { litebox_util_log::debug!( page_protection:% = format_args!("{:#x}", page_protection.bits()), fs_path:% = fs_path; @@ -872,9 +888,11 @@ fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool { .is_some_and(|tail| tail.eq_ignore_ascii_case(suffix)) } -fn required_map_access(protection: PageProtection) -> Result { +fn required_map_access(protection: PageProtection) -> SectionAccess { let base = protection.bits() & PageProtection::BASE_MASK; - let required = if matches!( + if base == PageProtection::PAGE_NOACCESS.bits() { + SectionAccess::MAP_READ + } else if matches!( base, value if value == PageProtection::PAGE_READWRITE.bits() || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() @@ -887,12 +905,64 @@ fn required_map_access(protection: PageProtection) -> Result bool { + let view_base = view_protection.bits() & PageProtection::BASE_MASK; + if view_base == PageProtection::PAGE_NOACCESS.bits() { + return true; + } + + if page_protection_has_read(view_protection) && !page_protection_has_read(section_protection) { + return false; + } + if page_protection_has_direct_write(view_protection) + && !page_protection_has_direct_write(section_protection) + { + return false; + } + if page_protection_has_execute(view_protection) + && !page_protection_has_execute(section_protection) + { + return false; + } + true +} + +fn page_protection_has_read(protection: PageProtection) -> bool { + matches!( + protection.bits() & PageProtection::BASE_MASK, + value if value == PageProtection::PAGE_READONLY.bits() + || value == PageProtection::PAGE_READWRITE.bits() + || value == PageProtection::PAGE_WRITECOPY.bits() + || value == PageProtection::PAGE_EXECUTE_READ.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() + ) +} + +fn page_protection_has_direct_write(protection: PageProtection) -> bool { + matches!( + protection.bits() & PageProtection::BASE_MASK, + value if value == PageProtection::PAGE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + ) +} + +fn page_protection_has_execute(protection: PageProtection) -> bool { + matches!( + protection.bits() & PageProtection::BASE_MASK, + value if value == PageProtection::PAGE_EXECUTE.bits() + || value == PageProtection::PAGE_EXECUTE_READ.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() + ) } fn write_section_basic_information( @@ -1177,6 +1247,7 @@ mod tests { task: &Task, access: u32, size: i64, + protection: PageProtection, ) -> Handle { let mut handle = Handle::default(); assert_eq!( @@ -1185,7 +1256,7 @@ mod tests { access, None, Some(const_ptr(&size)), - PageProtection::PAGE_READWRITE.bits(), + protection.bits(), SectionAllocationAttributes::SEC_COMMIT.bits(), Handle::default(), ), @@ -1270,7 +1341,12 @@ mod tests { #[test] fn nt_create_section_creates_queryable_pagefile_section() { let task = test_task(); - let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2345); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2345, + PageProtection::PAGE_READWRITE, + ); let mut info = SectionBasicInformation { base_address: usize::MAX, attributes: u32::MAX, @@ -1496,7 +1572,12 @@ mod tests { #[test] fn nt_map_view_of_section_maps_writable_pagefile_section() { let task = test_task(); - let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); let (base, view_size) = map_pagefile_section(&task, handle); assert_ne!(base, 0); assert_eq!(view_size, 0x2000); @@ -1516,10 +1597,139 @@ mod tests { ); } + #[test] + fn pagefile_map_rejects_protection_incompatible_with_section_protection() { + let task = test_task(); + let readonly = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READONLY, + ); + let execute = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_EXECUTE, + ); + let readwrite = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); + + for (handle, page_protection) in [ + (readonly, PageProtection::PAGE_READWRITE), + (execute, PageProtection::PAGE_READONLY), + (readwrite, PageProtection::PAGE_EXECUTE_READ), + ] { + let mut base = 0usize; + let mut view_size = 0usize; + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: page_protection.bits(), + }), + NtStatus::SECTION_PROTECTION + ); + assert_eq!(base, 0); + assert_eq!(view_size, 0); + } + } + + #[test] + fn pagefile_map_accepts_compatible_noaccess_and_copy_protections() { + let task = test_task(); + let readonly = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READONLY, + ); + + // Host 25H2 and ReactOS allow PAGE_WRITECOPY and PAGE_NOACCESS views of + // a PAGE_READONLY pagefile section. + for page_protection in [ + PageProtection::PAGE_WRITECOPY, + PageProtection::PAGE_NOACCESS, + ] { + let mut base = 0usize; + let mut view_size = 0usize; + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: readonly, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: page_protection.bits(), + }), + NtStatus::SUCCESS + ); + assert_ne!(base, 0); + assert_eq!(view_size, 0x2000); + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, base), + NtStatus::SUCCESS + ); + } + } + + #[test] + fn pagefile_noaccess_view_requires_map_read_access() { + let task = test_task(); + let handle = create_pagefile_section( + &task, + SectionAccess::QUERY.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); + let mut base = 0usize; + let mut view_size = 0usize; + + // Host 25H2 returns STATUS_ACCESS_DENIED for PAGE_NOACCESS maps unless + // the section handle has SECTION_MAP_READ. + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_NOACCESS.bits(), + }), + NtStatus::ACCESS_DENIED + ); + assert_eq!(base, 0); + assert_eq!(view_size, 0); + } + #[test] fn nt_map_view_of_section_ex_maps_and_unmaps_pagefile_section() { let task = test_task(); - let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); let mut base = 0usize; let mut view_size = 0usize; @@ -1561,7 +1771,12 @@ mod tests { #[test] fn nt_map_view_of_section_ex_rejects_extended_parameters() { let task = test_task(); - let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x1000); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x1000, + PageProtection::PAGE_READWRITE, + ); let mut base = 0usize; let mut view_size = 0usize; let extended_parameter = 0u8; @@ -1655,7 +1870,12 @@ mod tests { #[test] fn pagefile_view_unmap_remap_preserves_guest_writes() { let task = test_task(); - let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); let (first_base, first_size) = map_pagefile_section(&task, handle); assert_eq!(first_size, 0x2000); @@ -1681,7 +1901,12 @@ mod tests { #[test] fn dirty_pagefile_view_flushes_before_noaccess_protect() { let task = test_task(); - let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); let (first_base, first_size) = map_pagefile_section(&task, handle); assert_eq!(first_size, 0x2000); let first = MutPtr::::from_usize(first_base); @@ -1731,7 +1956,12 @@ mod tests { #[test] fn partial_noaccess_protect_flushes_still_readable_section_pages() { let task = test_task(); - let handle = create_pagefile_section(&task, SectionAccess::ALL_ACCESS.bits(), 0x2000); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); let (first_base, first_size) = map_pagefile_section(&task, handle); assert_eq!(first_size, 0x2000); From f8d624c0d2fc6bd2ef92e0ca3ae29eaa8b9ebc00 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 15:57:28 -0700 Subject: [PATCH 08/16] Limit pagefile sections to one map Remove the section-owned Vec backing and pagefile flush machinery to avoid guest-controlled kernel memory amplification. Pagefile sections now commit only guest view pages, reject concurrent and post-unmap remaps with STATUS_NOT_SUPPORTED, and keep map-failure rollback paths retryable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/mm.rs | 13 +- litebox_shim_windows/src/syscalls/section.rs | 334 +++---------------- 2 files changed, 51 insertions(+), 296 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/mm.rs b/litebox_shim_windows/src/syscalls/mm.rs index 5e96f20fdb..000e2ea4f6 100644 --- a/litebox_shim_windows/src/syscalls/mm.rs +++ b/litebox_shim_windows/src/syscalls/mm.rs @@ -611,17 +611,6 @@ impl Task { None => return NtStatus::NOT_COMMITTED, }; - if !new_permissions.contains(MemoryRegionPermissions::READ) - && let Err(status) = super::section::synchronize_pagefile_views_in_range( - &self.process.section_views, - &self.process.virtual_allocations, - aligned_base, - aligned_len, - ) - { - return status; - } - if update_permissions( &self.global.page_manager, aligned_base, @@ -1433,7 +1422,7 @@ fn allocation_granularity_aligned_candidate( } } -pub(super) fn update_permissions( +fn update_permissions( page_manager: &WindowsPageManager, aligned_base: usize, aligned_len: usize, diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 38346db9e5..2b93a883de 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -3,8 +3,6 @@ use alloc::string::String; use alloc::sync::Arc; -use alloc::vec; -use alloc::vec::Vec; use core::marker::PhantomData; use core::mem::size_of; use core::sync::atomic::{AtomicBool, Ordering}; @@ -14,20 +12,14 @@ use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; use litebox::mm::linux::{CreatePagesFlags, NonZeroPageSize}; use litebox::platform::page_mgmt::MemoryRegionPermissions; use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; -use litebox::sync::RwLock; use litebox_common_windows::nt_status::NtStatus; use rangemap::RangeMap; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::nt_types::{AccessMask, ObjectAttributes}; -use crate::syscalls::mm::{ - MemoryType, PageProtection, create_pages, parse_page_protection, update_permissions, -}; +use crate::syscalls::mm::{MemoryType, PageProtection, create_pages, parse_page_protection}; use crate::syscalls::{Handle, ProcessHandle}; -use crate::{ - ConstPtr, MutPtr, PAGE_SIZE, ShimFS, ShimPlatform, Task, WindowsSectionView, - WindowsVirtualAllocations, -}; +use crate::{ConstPtr, MutPtr, PAGE_SIZE, ShimFS, ShimPlatform, Task, WindowsSectionView}; const VIEW_SHARE: u32 = 1; const VIEW_UNMAP: u32 = 2; @@ -37,10 +29,11 @@ const MEM_DIFFERENT_IMAGE_BASE_OK: u32 = 0x0080_0000; const SUPPORTED_MAP_ALLOCATION_TYPES: u32 = MEM_TOP_DOWN | MEM_PHYSICAL | MEM_DIFFERENT_IMAGE_BASE_OK; -enum SectionBacking { - /// LiteBox lacks shared anonymous mappings, so pagefile sections allow one - /// active view and flush it here to preserve unmap/remap contents. - Pagefile(RwLock>), +enum SectionBacking { + /// LiteBox lacks shared anonymous backing, so a pagefile section is + /// metadata-only until its single allowed view is mapped. Remap after unmap + /// is rejected instead of storing contents in shim memory or a file. + Pagefile, ImageFile, } @@ -62,8 +55,9 @@ pub(crate) struct SectionObject { size: usize, attributes: SectionAllocationAttributes, protection: PageProtection, - backing: SectionBacking, + backing: SectionBacking, pagefile_view_active: AtomicBool, + _platform: PhantomData, } pub(crate) struct MapViewOfSectionParameters { @@ -320,8 +314,9 @@ impl Task { size, attributes, protection, - backing: SectionBacking::Pagefile(RwLock::new(vec![0; size])), + backing: SectionBacking::Pagefile, pagefile_view_active: AtomicBool::new(false), + _platform: PhantomData, }); if let Some(name) = &name { let status = self.insert_named_section(name, §ion); @@ -417,6 +412,7 @@ impl Task { protection: PageProtection::PAGE_EXECUTE_WRITECOPY, backing: SectionBacking::ImageFile, pagefile_view_active: AtomicBool::new(false), + _platform: PhantomData, }); self.publish_section_handle(section_handle, section, granted_access) } @@ -512,8 +508,8 @@ impl Task { Ok(section) => section, Err(status) => return status, }; - match §ion.backing { - SectionBacking::Pagefile(_) => self.map_pagefile_section( + match section.backing { + SectionBacking::Pagefile => self.map_pagefile_section( request, §ion, requested_view_size, @@ -550,14 +546,6 @@ impl Task { return NtStatus::NOT_MAPPED_VIEW; }; let ptr = MutPtr::::from_usize(view_base); - if let Err(status) = synchronize_pagefile_view::( - view_base, - &view, - &self.process.virtual_allocations, - ) { - self.process.section_views.write().insert(view_base, view); - return status; - } // SAFETY: Section views are tracked only after this shim successfully creates the pages; // unmapping consumes the tracked view and removes the exact owned range. if unsafe { self.global.page_manager.remove_pages(ptr, view.size) }.is_err() { @@ -565,7 +553,6 @@ impl Task { return NtStatus::UNABLE_TO_FREE_VM; } self.process.virtual_allocations.write().remove(&view_base); - release_pagefile_view_slot(&view); NtStatus::SUCCESS } @@ -663,10 +650,10 @@ impl Task { let Some(length) = NonZeroPageSize::::new(mapped_size) else { return NtStatus::INVALID_VIEW_SIZE; }; - let backing = match §ion.backing { - SectionBacking::Pagefile(backing) => backing, + match section.backing { + SectionBacking::Pagefile => {} SectionBacking::ImageFile => return NtStatus::INVALID_FILE_FOR_SECTION, - }; + } if !pagefile_view_protection_is_compatible(section.protection, page_protection) { litebox_util_log::debug!( section_protection:% = format_args!("{:#x}", section.protection.bits()), @@ -680,12 +667,11 @@ impl Task { section_size = section.size, requested_view_size, section_offset; - "Rejected second active pagefile section view" + "Rejected additional pagefile section view" ); - // Host 25H2 allows multiple simultaneous pagefile views of one section - // (second NtMapViewOfSection -> STATUS_SUCCESS). LiteBox returns - // TODO(section-subsystem): allow this once PageManager has first-class shared - // anonymous backing for one section object mapped at multiple virtual addresses. + // Host 25H2 allows repeated and simultaneous pagefile views. LiteBox + // returns NOT_SUPPORTED until PageManager has first-class shared + // anonymous backing that avoids kernel-side content storage. return NtStatus::NOT_SUPPORTED; } let Ok(mapping) = create_pages( @@ -693,27 +679,13 @@ impl Task { None, length, CreatePagesFlags::empty(), - MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE, - |ptr| { - let backing = backing.read(); - let bytes = &backing[section_offset..section_offset + mapped_size]; - ptr.copy_from_slice(0, bytes) - .ok_or(litebox::mm::linux::MappingError::OutOfMemory)?; - Ok(mapped_size) - }, + permissions, + |_| Ok(0), ) else { section.pagefile_view_active.store(false, Ordering::Release); return NtStatus::NO_MEMORY; }; let base = mapping.as_usize(); - if permissions != MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE - && update_permissions(&self.global.page_manager, base, mapped_size, permissions) - .is_err() - { - let _ = remove_view_pages::(&self.global.page_manager, base, mapped_size); - section.pagefile_view_active.store(false, Ordering::Release); - return NtStatus::ACCESS_VIOLATION; - } if request.base_address.write_at_offset(0, base).is_none() || request.view_size.write_at_offset(0, view_size).is_none() { @@ -1053,125 +1025,6 @@ fn write_section_image_information( NtStatus::SUCCESS } -fn release_pagefile_view_slot(view: &WindowsSectionView) { - if let Some(section) = &view.section - && matches!(section.backing, SectionBacking::Pagefile(_)) - { - section.pagefile_view_active.store(false, Ordering::Release); - } -} - -pub(super) fn synchronize_pagefile_views_in_range( - views: &crate::WindowsSectionViews, - virtual_allocations: &WindowsVirtualAllocations, - base: usize, - size: usize, -) -> Result<(), NtStatus> { - let Some(end) = base.checked_add(size) else { - return Err(NtStatus::INVALID_PARAMETER); - }; - let snapshots = views - .read() - .iter() - .filter_map(|(&view_base, view)| { - let view_end = view_base.checked_add(view.size)?; - (view_base < end && base < view_end).then(|| (view_base, view.clone())) - }) - .collect::>(); - for (view_base, view) in snapshots { - synchronize_pagefile_view::(view_base, &view, virtual_allocations)?; - } - Ok(()) -} - -fn synchronize_pagefile_view( - view_base: usize, - view: &WindowsSectionView, - virtual_allocations: &WindowsVirtualAllocations, -) -> Result<(), NtStatus> { - let Some(section) = &view.section else { - return Ok(()); - }; - let SectionBacking::Pagefile(backing) = §ion.backing else { - return Ok(()); - }; - let mut backing = backing.write(); - let Some(end) = view.section_offset.checked_add(view.size) else { - return Err(NtStatus::INVALID_PARAMETER); - }; - if end > backing.len() { - return Err(NtStatus::INVALID_PARAMETER); - } - for (range_start, range_end) in - readable_section_view_ranges(virtual_allocations, view_base, view.size) - { - let range_len = range_end - range_start; - let view_offset = range_start - view_base; - let backing_start = view - .section_offset - .checked_add(view_offset) - .ok_or(NtStatus::INVALID_PARAMETER)?; - let backing_end = backing_start - .checked_add(range_len) - .ok_or(NtStatus::INVALID_PARAMETER)?; - if backing_end > backing.len() { - return Err(NtStatus::INVALID_PARAMETER); - } - let source = ConstPtr::::from_usize(range_start); - for (offset, byte) in backing[backing_start..backing_end].iter_mut().enumerate() { - let offset = isize::try_from(offset).map_err(|_| NtStatus::INVALID_PARAMETER)?; - *byte = source - .read_at_offset(offset) - .ok_or(NtStatus::ACCESS_VIOLATION)?; - } - } - Ok(()) -} - -fn readable_section_view_ranges( - virtual_allocations: &WindowsVirtualAllocations, - view_base: usize, - view_size: usize, -) -> Vec<(usize, usize)> { - let Some(view_end) = view_base.checked_add(view_size) else { - return Vec::new(); - }; - let allocations = virtual_allocations.read(); - let Some((_, allocation)) = allocations.range(..=view_base).next_back() else { - return Vec::new(); - }; - if allocation - .base - .checked_add(allocation.size) - .is_none_or(|allocation_end| view_end > allocation_end) - { - return Vec::new(); - } - allocation - .pages - .overlapping(view_base..view_end) - .filter(|(_, protect)| page_protection_is_readable(**protect)) - .map(|(range, _)| { - let range_start = range.start.max(view_base); - let range_end = range.end.min(view_end); - (range_start, range_end) - }) - .filter(|(range_start, range_end)| range_start < range_end) - .collect() -} - -fn page_protection_is_readable(protect: PageProtection) -> bool { - matches!( - protect.bits() & PageProtection::BASE_MASK, - value if value == PageProtection::PAGE_READONLY.bits() - || value == PageProtection::PAGE_READWRITE.bits() - || value == PageProtection::PAGE_WRITECOPY.bits() - || value == PageProtection::PAGE_EXECUTE_READ.bits() - || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() - || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() - ) -} - fn committed_pages( base: usize, size: usize, @@ -1649,19 +1502,18 @@ mod tests { #[test] fn pagefile_map_accepts_compatible_noaccess_and_copy_protections() { let task = test_task(); - let readonly = create_pagefile_section( - &task, - SectionAccess::ALL_ACCESS.bits(), - 0x2000, - PageProtection::PAGE_READONLY, - ); - // Host 25H2 and ReactOS allow PAGE_WRITECOPY and PAGE_NOACCESS views of // a PAGE_READONLY pagefile section. for page_protection in [ PageProtection::PAGE_WRITECOPY, PageProtection::PAGE_NOACCESS, ] { + let readonly = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READONLY, + ); let mut base = 0usize; let mut view_size = 0usize; assert_eq!( @@ -1862,44 +1714,29 @@ mod tests { task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), NtStatus::SUCCESS ); - let (second_base, second_size) = map_pagefile_section(&task, opened); - assert_eq!(second_size, 0x2000); - assert_ne!(second_base, 0); - } - - #[test] - fn pagefile_view_unmap_remap_preserves_guest_writes() { - let task = test_task(); - let handle = create_pagefile_section( - &task, - SectionAccess::ALL_ACCESS.bits(), - 0x2000, - PageProtection::PAGE_READWRITE, - ); - let (first_base, first_size) = map_pagefile_section(&task, handle); - assert_eq!(first_size, 0x2000); - - let first = MutPtr::::from_usize(first_base); - assert!(first.write_at_offset(0, 0xdead_beef).is_some()); - let first_second_page = MutPtr::::from_usize(first_base + PAGE_SIZE); - assert!(first_second_page.write_at_offset(0, 0x0bad_f00d).is_some()); - + second_base = 0; + second_size = 0; assert_eq!( - task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), - NtStatus::SUCCESS + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: opened, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut second_base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut second_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::NOT_SUPPORTED ); - - let (second_base, second_size) = map_pagefile_section(&task, handle); - assert_eq!(second_size, 0x2000); - assert_ne!(second_base, 0); - let second = MutPtr::::from_usize(second_base); - assert_eq!(second.read_at_offset(0), Some(0xdead_beef)); - let second_second_page = MutPtr::::from_usize(second_base + PAGE_SIZE); - assert_eq!(second_second_page.read_at_offset(0), Some(0x0bad_f00d)); + assert_eq!(second_base, 0); + assert_eq!(second_size, 0); } #[test] - fn dirty_pagefile_view_flushes_before_noaccess_protect() { + fn pagefile_view_unmap_remap_is_not_supported() { let task = test_task(); let handle = create_pagefile_section( &task, @@ -1909,22 +1746,7 @@ mod tests { ); let (first_base, first_size) = map_pagefile_section(&task, handle); assert_eq!(first_size, 0x2000); - let first = MutPtr::::from_usize(first_base); - assert!(first.write_at_offset(0, 0x55aa_1234).is_some()); - let mut protect_base = first_base; - let mut protect_size = first_size; - let mut old_protect = 0; - assert_eq!( - task.sys_nt_protect_virtual_memory( - ProcessHandle::CURRENT, - mut_ptr(&mut protect_base), - mut_ptr(&mut protect_size), - PageProtection::PAGE_NOACCESS.bits(), - mut_ptr(&mut old_protect), - ), - NtStatus::SUCCESS - ); assert_eq!( task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), NtStatus::SUCCESS @@ -1945,66 +1767,10 @@ mod tests { allocation_type: 0, page_protection: PageProtection::PAGE_READWRITE.bits(), }), - NtStatus::SUCCESS - ); - assert_ne!(second_base, 0); - assert_eq!(second_size, 0x2000); - let second = MutPtr::::from_usize(second_base); - assert_eq!(second.read_at_offset(0), Some(0x55aa_1234)); - } - - #[test] - fn partial_noaccess_protect_flushes_still_readable_section_pages() { - let task = test_task(); - let handle = create_pagefile_section( - &task, - SectionAccess::ALL_ACCESS.bits(), - 0x2000, - PageProtection::PAGE_READWRITE, - ); - let (first_base, first_size) = map_pagefile_section(&task, handle); - assert_eq!(first_size, 0x2000); - - let first_second_page = MutPtr::::from_usize(first_base + PAGE_SIZE); - assert!(first_second_page.write_at_offset(0, 0x1111_1111).is_some()); - - let mut first_page_base = first_base; - let mut first_page_size = PAGE_SIZE; - let mut old_protect = 0; - assert_eq!( - task.sys_nt_protect_virtual_memory( - ProcessHandle::CURRENT, - mut_ptr(&mut first_page_base), - mut_ptr(&mut first_page_size), - PageProtection::PAGE_NOACCESS.bits(), - mut_ptr(&mut old_protect), - ), - NtStatus::SUCCESS - ); - - assert!(first_second_page.write_at_offset(0, 0x2222_2222).is_some()); - - let mut second_page_base = first_base + PAGE_SIZE; - let mut second_page_size = PAGE_SIZE; - assert_eq!( - task.sys_nt_protect_virtual_memory( - ProcessHandle::CURRENT, - mut_ptr(&mut second_page_base), - mut_ptr(&mut second_page_size), - PageProtection::PAGE_NOACCESS.bits(), - mut_ptr(&mut old_protect), - ), - NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), - NtStatus::SUCCESS + NtStatus::NOT_SUPPORTED ); - - let (second_base, second_size) = map_pagefile_section(&task, handle); - assert_eq!(second_size, 0x2000); - let second_second_page = MutPtr::::from_usize(second_base + PAGE_SIZE); - assert_eq!(second_second_page.read_at_offset(0), Some(0x2222_2222)); + assert_eq!(second_base, 0); + assert_eq!(second_size, 0); } #[test] From 7177b705c8da21a00d80c6054eabaa976e12e31a Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 16:26:36 -0700 Subject: [PATCH 09/16] Clarify pagefile section deferred-sharing rejects The Pagefile backing doc-comment now records that the second-concurrent-view and remap-after-unmap rejects are one deferred capability (shared write-through backing), not two unrelated limitations. Doc-only; no behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 2b93a883de..a7ac6d8b3d 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -33,6 +33,12 @@ enum SectionBacking { /// LiteBox lacks shared anonymous backing, so a pagefile section is /// metadata-only until its single allowed view is mapped. Remap after unmap /// is rejected instead of storing contents in shim memory or a file. + /// + /// Shared write-through backing across concurrent views is the deferred + /// capability (see `TODO(section-subsystem)`); until it lands, a single view + /// is the only observable-faithful case, which is why both the + /// second-concurrent-view and the remap-after-unmap rejects exist. They are + /// one missing feature, not two unrelated limitations. Pagefile, ImageFile, } From 21d3c5cc03448c8d52c81632341f4483a70945b0 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 16:48:00 -0700 Subject: [PATCH 10/16] Remove low-signal section open comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index a7ac6d8b3d..fa1d8fd1ba 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -363,8 +363,6 @@ impl Task { ) } - // Wine's open_mapping and ReactOS object-manager open path first resolve a named Section object; - // KnownDlls misses then report object-manager path/name status rather than a link-specific error. pub(crate) fn sys_nt_open_section( &self, section_handle: MutPtr, From f494bf799e970805c1e10a7968d3fa2a7594b331 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 17:09:51 -0700 Subject: [PATCH 11/16] Use host image for section image tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 171 +++++++++++-------- 1 file changed, 98 insertions(+), 73 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index fa1d8fd1ba..64a6edc923 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -1054,6 +1054,8 @@ fn remove_view_pages( #[cfg(test)] mod tests { + extern crate std; + use core::mem::{size_of, size_of_val}; use litebox::platform::RawMutPointer as _; @@ -1061,20 +1063,10 @@ mod tests { use super::*; use crate::nt_types::{ObjectAttributes, UnicodeString}; - use crate::tests::{ - TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task, test_task_with_nls_files, - }; + use crate::tests::{TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task}; - const TEST_PE_IMAGE_BASE: usize = 0x1800_0000; - const TEST_PE_ENTRY_RVA: u32 = 0x1000; - const TEST_PE_IMAGE_SIZE: u32 = 0x2000; - const TEST_PE_FILE_SIZE: u32 = 0x400; - const TEST_PE_SUBSYSTEM: u16 = 3; - const TEST_PE_MAJOR_SUBSYSTEM_VERSION: u16 = 10; - const TEST_PE_MINOR_SUBSYSTEM_VERSION: u16 = 0; - const TEST_PE_CHARACTERISTICS: u16 = 0x2022; - const TEST_PE_DLL_CHARACTERISTICS: u16 = 0x8160; - const TEST_PE_MACHINE: u16 = 0x8664; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + use crate::tests::test_task_with_nls_files; fn wide(value: &str) -> alloc::vec::Vec { value.encode_utf16().collect() @@ -1143,56 +1135,90 @@ mod tests { (base, view_size) } - fn minimal_pe_image() -> alloc::vec::Vec { - let mut image = alloc::vec![0; usize::try_from(TEST_PE_FILE_SIZE).unwrap()]; - write_u16(&mut image, 0x00, 0x5a4d); - write_u32(&mut image, 0x3c, 0x80); - write_u32(&mut image, 0x80, 0x0000_4550); - - let file_header = 0x84; - write_u16(&mut image, file_header, TEST_PE_MACHINE); - write_u16(&mut image, file_header + 2, 1); - write_u16(&mut image, file_header + 16, 0xf0); - write_u16(&mut image, file_header + 18, TEST_PE_CHARACTERISTICS); - - let optional = 0x98; - write_u16(&mut image, optional, 0x20b); - write_u32(&mut image, optional + 16, TEST_PE_ENTRY_RVA); - write_u64(&mut image, optional + 24, TEST_PE_IMAGE_BASE as u64); - write_u32(&mut image, optional + 32, u32::try_from(PAGE_SIZE).unwrap()); - write_u32(&mut image, optional + 36, 0x200); - write_u16(&mut image, optional + 48, TEST_PE_MAJOR_SUBSYSTEM_VERSION); - write_u16(&mut image, optional + 50, TEST_PE_MINOR_SUBSYSTEM_VERSION); - write_u32(&mut image, optional + 56, TEST_PE_IMAGE_SIZE); - write_u32(&mut image, optional + 60, TEST_PE_FILE_SIZE); - write_u16(&mut image, optional + 68, TEST_PE_SUBSYSTEM); - write_u16(&mut image, optional + 70, TEST_PE_DLL_CHARACTERISTICS); - write_u64(&mut image, optional + 72, 0x100000); - write_u64(&mut image, optional + 80, 0x1000); - write_u64(&mut image, optional + 88, 0x100000); - write_u64(&mut image, optional + 96, 0x1000); - write_u32(&mut image, optional + 108, 16); - - let section = 0x188; - image[section..section + 5].copy_from_slice(b".text"); - write_u32(&mut image, section + 8, 1); - write_u32(&mut image, section + 12, TEST_PE_ENTRY_RVA); - write_u32(&mut image, section + 16, 0x200); - write_u32(&mut image, section + 20, 0x200); - write_u32(&mut image, section + 36, 0x6000_0020); - image + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn host_kernel32_image() -> std::vec::Vec { + let system_root = std::env::var_os("SystemRoot").expect("SystemRoot is set on Windows"); + std::fs::read( + std::path::PathBuf::from(system_root) + .join("System32") + .join("kernel32.dll"), + ) + .expect("host kernel32.dll is readable") } - fn write_u16(output: &mut [u8], offset: usize, value: u16) { - output[offset..offset + size_of::()].copy_from_slice(&value.to_le_bytes()); - } + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn host_known_dll_image_information() -> SectionImageInformation { + use core::ffi::c_void; + + #[link(name = "ntdll")] + unsafe extern "system" { + fn NtOpenSection( + section_handle: *mut *mut c_void, + desired_access: u32, + object_attributes: *const ObjectAttributes, + ) -> i32; + fn NtQuerySection( + section_handle: *mut c_void, + section_information_class: u32, + section_information: *mut SectionImageInformation, + section_information_length: usize, + return_length: *mut usize, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } - fn write_u32(output: &mut [u8], offset: usize, value: u32) { - output[offset..offset + size_of::()].copy_from_slice(&value.to_le_bytes()); - } + let name = wide(r"\KnownDlls\kernel32.dll"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let mut handle = core::ptr::null_mut(); + // SAFETY: The object attributes point to stack-owned UTF-16 data that remains live for the + // call, and the output handle pointer is a valid stack local. + let status = unsafe { + NtOpenSection( + &raw mut handle, + SectionAccess::QUERY.bits(), + &raw const attrs, + ) + }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); - fn write_u64(output: &mut [u8], offset: usize, value: u64) { - output[offset..offset + size_of::()].copy_from_slice(&value.to_le_bytes()); + let mut info = SectionImageInformation { + transfer_address: 0, + zero_bits: 0, + _padding0: 0, + maximum_stack_size: 0, + committed_stack_size: 0, + subsystem_type: 0, + subsystem_minor_version: 0, + subsystem_major_version: 0, + gp_value: 0, + image_characteristics: 0, + dll_characteristics: 0, + machine: 0, + image_contains_code: 0, + image_flags: 0, + loader_flags: 0, + image_file_size: 0, + checksum: 0, + }; + let mut return_length = 0usize; + // SAFETY: `handle` is a live section handle from host ntdll and both output pointers refer + // to valid stack locals. + let status = unsafe { + NtQuerySection( + handle, + SectionInformationClass::Image as u32, + &raw mut info, + size_of::(), + &raw mut return_length, + ) + }; + // SAFETY: The handle was returned by a successful host ntdll call in this test. + let close_status = unsafe { NtClose(handle) }; + assert_eq!(close_status, NtStatus::SUCCESS.as_raw()); + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + assert_eq!(return_length, size_of::()); + info } #[test] @@ -1248,9 +1274,11 @@ mod tests { assert_eq!(return_length, 0x5555_5555); } + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] #[test] fn nt_query_section_image_information_uses_pe_headers() { - let image = minimal_pe_image(); + let image = host_kernel32_image(); + let host_info = host_known_dll_image_information(); let task = test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); let name = wide(r"\KnownDlls\kernel32.dll"); let unicode = unicode(&name); @@ -1297,24 +1325,20 @@ mod tests { ); assert_eq!(return_length, size_of::()); - assert_eq!( - info.transfer_address, - TEST_PE_IMAGE_BASE + TEST_PE_ENTRY_RVA as usize - ); - assert_eq!(info.subsystem_type, u32::from(TEST_PE_SUBSYSTEM)); + assert_eq!(info.subsystem_type, host_info.subsystem_type); assert_eq!( info.subsystem_major_version, - TEST_PE_MAJOR_SUBSYSTEM_VERSION + host_info.subsystem_major_version ); assert_eq!( info.subsystem_minor_version, - TEST_PE_MINOR_SUBSYSTEM_VERSION + host_info.subsystem_minor_version ); - assert_eq!(info.image_characteristics, TEST_PE_CHARACTERISTICS); - assert_eq!(info.dll_characteristics, TEST_PE_DLL_CHARACTERISTICS); - assert_eq!(info.machine, TEST_PE_MACHINE); - assert_eq!(info.image_contains_code, 1); - assert_eq!(info.image_file_size, TEST_PE_FILE_SIZE); + assert_eq!(info.image_characteristics, host_info.image_characteristics); + assert_eq!(info.dll_characteristics, host_info.dll_characteristics); + assert_eq!(info.machine, host_info.machine); + assert_eq!(info.image_contains_code, host_info.image_contains_code); + assert_eq!(info.image_file_size, host_info.image_file_size); let mut too_small = [0xcc; size_of::() - 1]; let too_small_len = too_small.len(); @@ -1334,9 +1358,10 @@ mod tests { assert_eq!(return_length, 0x5555_5555); } + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] #[test] fn image_section_rejects_writable_view_protection() { - let image = minimal_pe_image(); + let image = host_kernel32_image(); let task = test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); let name = wide(r"\KnownDlls\kernel32.dll"); let unicode = unicode(&name); From 9dd2d79a2dbdec36182756bae68275690a7db39c Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 20:02:10 -0700 Subject: [PATCH 12/16] Merge pagefile single-view tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 40 +------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 64a6edc923..512a2ac55d 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -1686,7 +1686,7 @@ mod tests { } #[test] - fn pagefile_section_rejects_second_active_view_across_handles() { + fn pagefile_section_rejects_additional_views_across_handles_and_unmap() { let task = test_task(); let name = wide(r"\BaseNamedObjects\LiteBoxSingleViewSection"); let unicode = unicode(&name); @@ -1764,44 +1764,6 @@ mod tests { assert_eq!(second_size, 0); } - #[test] - fn pagefile_view_unmap_remap_is_not_supported() { - let task = test_task(); - let handle = create_pagefile_section( - &task, - SectionAccess::ALL_ACCESS.bits(), - 0x2000, - PageProtection::PAGE_READWRITE, - ); - let (first_base, first_size) = map_pagefile_section(&task, handle); - assert_eq!(first_size, 0x2000); - - assert_eq!( - task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), - NtStatus::SUCCESS - ); - - let mut second_base = 0usize; - let mut second_size = 0usize; - assert_eq!( - task.sys_nt_map_view_of_section(MapViewOfSectionParameters { - section_handle: handle, - process_handle: ProcessHandle::CURRENT, - base_address: mut_ptr(&mut second_base), - zero_bits: 0, - commit_size: 0, - section_offset: None, - view_size: mut_ptr(&mut second_size), - inherit_disposition: VIEW_SHARE, - allocation_type: 0, - page_protection: PageProtection::PAGE_READWRITE.bits(), - }), - NtStatus::NOT_SUPPORTED - ); - assert_eq!(second_base, 0); - assert_eq!(second_size, 0); - } - #[test] fn nt_open_section_opens_existing_named_pagefile_section() { let task = test_task(); From 53be6cbba166b95696d016ebdd28ef66dbdd9be4 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 20:13:29 -0700 Subject: [PATCH 13/16] Remove low-signal section Ex tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 83 -------------------- 1 file changed, 83 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 512a2ac55d..a832e108a5 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -1602,89 +1602,6 @@ mod tests { assert_eq!(view_size, 0); } - #[test] - fn nt_map_view_of_section_ex_maps_and_unmaps_pagefile_section() { - let task = test_task(); - let handle = create_pagefile_section( - &task, - SectionAccess::ALL_ACCESS.bits(), - 0x2000, - PageProtection::PAGE_READWRITE, - ); - let mut base = 0usize; - let mut view_size = 0usize; - - assert_eq!( - task.sys_nt_map_view_of_section_ex( - MapViewOfSectionParameters { - section_handle: handle, - process_handle: ProcessHandle::CURRENT, - base_address: mut_ptr(&mut base), - zero_bits: 0, - commit_size: 0, - section_offset: None, - view_size: mut_ptr(&mut view_size), - inherit_disposition: VIEW_SHARE, - allocation_type: 0, - page_protection: PageProtection::PAGE_READWRITE.bits(), - }, - None, - 0, - ), - NtStatus::SUCCESS - ); - assert_ne!(base, 0); - assert_eq!(view_size, 0x2000); - - let mapped = MutPtr::::from_usize(base); - assert!(mapped.write_at_offset(0, 0x1234_5678).is_some()); - assert_eq!(mapped.read_at_offset(0), Some(0x1234_5678)); - assert_eq!( - task.sys_nt_unmap_view_of_section_ex(ProcessHandle::CURRENT, base, 0), - NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_unmap_view_of_section_ex(ProcessHandle::CURRENT, base, 0), - NtStatus::NOT_MAPPED_VIEW - ); - } - - #[test] - fn nt_map_view_of_section_ex_rejects_extended_parameters() { - let task = test_task(); - let handle = create_pagefile_section( - &task, - SectionAccess::ALL_ACCESS.bits(), - 0x1000, - PageProtection::PAGE_READWRITE, - ); - let mut base = 0usize; - let mut view_size = 0usize; - let extended_parameter = 0u8; - - assert_eq!( - task.sys_nt_map_view_of_section_ex( - MapViewOfSectionParameters { - section_handle: handle, - process_handle: ProcessHandle::CURRENT, - base_address: mut_ptr(&mut base), - zero_bits: 0, - commit_size: 0, - section_offset: None, - view_size: mut_ptr(&mut view_size), - inherit_disposition: VIEW_SHARE, - allocation_type: 0, - page_protection: PageProtection::PAGE_READWRITE.bits(), - }, - Some(const_ptr(&extended_parameter)), - 1, - ), - NtStatus::INVALID_PARAMETER - ); - assert_eq!(base, 0); - assert_eq!(view_size, 0); - } - #[test] fn pagefile_section_rejects_additional_views_across_handles_and_unmap() { let task = test_task(); From 6415bccf4abc6670854d01b68b10e5cb27dd7d3e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 2 Jul 2026 00:28:59 -0700 Subject: [PATCH 14/16] Use full path for host section test fixture Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index a832e108a5..02630839cc 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -1065,9 +1065,6 @@ mod tests { use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::tests::{TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task}; - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] - use crate::tests::test_task_with_nls_files; - fn wide(value: &str) -> alloc::vec::Vec { value.encode_utf16().collect() } @@ -1279,7 +1276,8 @@ mod tests { fn nt_query_section_image_information_uses_pe_headers() { let image = host_kernel32_image(); let host_info = host_known_dll_image_information(); - let task = test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); + let task = + crate::tests::test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); let name = wide(r"\KnownDlls\kernel32.dll"); let unicode = unicode(&name); let attrs = object_attributes(&unicode); @@ -1362,7 +1360,8 @@ mod tests { #[test] fn image_section_rejects_writable_view_protection() { let image = host_kernel32_image(); - let task = test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); + let task = + crate::tests::test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); let name = wide(r"\KnownDlls\kernel32.dll"); let unicode = unicode(&name); let attrs = object_attributes(&unicode); From aac8345a96464ebc2fda51ffe89af660d6483d9e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 2 Jul 2026 00:36:29 -0700 Subject: [PATCH 15/16] Simplify section image metadata test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 99 ++------------------ 1 file changed, 9 insertions(+), 90 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 02630839cc..f586a1a477 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -1065,6 +1065,11 @@ mod tests { use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::tests::{TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task}; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const IMAGE_SUBSYSTEM_WINDOWS_CUI: u32 = 3; + fn wide(value: &str) -> alloc::vec::Vec { value.encode_utf16().collect() } @@ -1143,81 +1148,6 @@ mod tests { .expect("host kernel32.dll is readable") } - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] - fn host_known_dll_image_information() -> SectionImageInformation { - use core::ffi::c_void; - - #[link(name = "ntdll")] - unsafe extern "system" { - fn NtOpenSection( - section_handle: *mut *mut c_void, - desired_access: u32, - object_attributes: *const ObjectAttributes, - ) -> i32; - fn NtQuerySection( - section_handle: *mut c_void, - section_information_class: u32, - section_information: *mut SectionImageInformation, - section_information_length: usize, - return_length: *mut usize, - ) -> i32; - fn NtClose(handle: *mut c_void) -> i32; - } - - let name = wide(r"\KnownDlls\kernel32.dll"); - let unicode = unicode(&name); - let attrs = object_attributes(&unicode); - let mut handle = core::ptr::null_mut(); - // SAFETY: The object attributes point to stack-owned UTF-16 data that remains live for the - // call, and the output handle pointer is a valid stack local. - let status = unsafe { - NtOpenSection( - &raw mut handle, - SectionAccess::QUERY.bits(), - &raw const attrs, - ) - }; - assert_eq!(status, NtStatus::SUCCESS.as_raw()); - - let mut info = SectionImageInformation { - transfer_address: 0, - zero_bits: 0, - _padding0: 0, - maximum_stack_size: 0, - committed_stack_size: 0, - subsystem_type: 0, - subsystem_minor_version: 0, - subsystem_major_version: 0, - gp_value: 0, - image_characteristics: 0, - dll_characteristics: 0, - machine: 0, - image_contains_code: 0, - image_flags: 0, - loader_flags: 0, - image_file_size: 0, - checksum: 0, - }; - let mut return_length = 0usize; - // SAFETY: `handle` is a live section handle from host ntdll and both output pointers refer - // to valid stack locals. - let status = unsafe { - NtQuerySection( - handle, - SectionInformationClass::Image as u32, - &raw mut info, - size_of::(), - &raw mut return_length, - ) - }; - // SAFETY: The handle was returned by a successful host ntdll call in this test. - let close_status = unsafe { NtClose(handle) }; - assert_eq!(close_status, NtStatus::SUCCESS.as_raw()); - assert_eq!(status, NtStatus::SUCCESS.as_raw()); - assert_eq!(return_length, size_of::()); - info - } - #[test] fn nt_create_section_creates_queryable_pagefile_section() { let task = test_task(); @@ -1275,7 +1205,6 @@ mod tests { #[test] fn nt_query_section_image_information_uses_pe_headers() { let image = host_kernel32_image(); - let host_info = host_known_dll_image_information(); let task = crate::tests::test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); let name = wide(r"\KnownDlls\kernel32.dll"); @@ -1323,20 +1252,10 @@ mod tests { ); assert_eq!(return_length, size_of::()); - assert_eq!(info.subsystem_type, host_info.subsystem_type); - assert_eq!( - info.subsystem_major_version, - host_info.subsystem_major_version - ); - assert_eq!( - info.subsystem_minor_version, - host_info.subsystem_minor_version - ); - assert_eq!(info.image_characteristics, host_info.image_characteristics); - assert_eq!(info.dll_characteristics, host_info.dll_characteristics); - assert_eq!(info.machine, host_info.machine); - assert_eq!(info.image_contains_code, host_info.image_contains_code); - assert_eq!(info.image_file_size, host_info.image_file_size); + assert_eq!(info.machine, IMAGE_FILE_MACHINE_AMD64); + assert_eq!(info.subsystem_type, IMAGE_SUBSYSTEM_WINDOWS_CUI); + assert_eq!(info.image_contains_code, 1); + assert_eq!(info.image_file_size, u32::try_from(image.len()).unwrap()); let mut too_small = [0xcc; size_of::() - 1]; let too_small_len = too_small.len(); From 6dca07be5ff645059c857fc0a6a55c9a5b07c802 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 2 Jul 2026 00:40:48 -0700 Subject: [PATCH 16/16] Use zeroed section image test buffer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/syscalls/section.rs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index f586a1a477..7d770e83cb 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -1220,25 +1220,7 @@ mod tests { NtStatus::SUCCESS ); - let mut info = SectionImageInformation { - transfer_address: 0, - zero_bits: u32::MAX, - _padding0: u32::MAX, - maximum_stack_size: usize::MAX, - committed_stack_size: usize::MAX, - subsystem_type: u32::MAX, - subsystem_minor_version: u16::MAX, - subsystem_major_version: u16::MAX, - gp_value: u32::MAX, - image_characteristics: u16::MAX, - dll_characteristics: u16::MAX, - machine: u16::MAX, - image_contains_code: u8::MAX, - image_flags: u8::MAX, - loader_flags: u32::MAX, - image_file_size: u32::MAX, - checksum: u32::MAX, - }; + let mut info = ::new_zeroed(); let mut return_length = 0usize; assert_eq!( task.sys_nt_query_section(