diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 3aae71ebf..9d64ad329 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -555,6 +555,7 @@ impl WindowsShim { process: process.clone(), fs, wait_state: wait::WaitState::new(self.0.platform), + io_completion_worker: Mutex::new(syscalls::iocp::IoCompletionWorkerState::new()), entry_point: load_info.entry_point, stack_top: load_info.stack_top, context: load_info.environment.context, @@ -737,6 +738,7 @@ struct Task { process: Arc>, fs: Arc, wait_state: wait::WaitState, + io_completion_worker: Mutex>, entry_point: usize, stack_top: usize, context: usize, @@ -748,6 +750,7 @@ impl Task { fn complete_current_thread(&self) { let thread_id = self.thread_object.thread_id(); self.thread_object.complete(|| { + self.release_io_completion_worker(); self.thread_object.abandon_owned_mutants(thread_id); self.process.detach_thread(thread_id); }); @@ -1174,6 +1177,47 @@ impl Task { object_attributes, number_of_concurrent_threads, ), + SyscallRequest::NtOpenIoCompletion { + io_completion_handle, + desired_access, + object_attributes, + } => self.sys_nt_open_io_completion( + io_completion_handle, + desired_access, + object_attributes, + ), + SyscallRequest::NtSetIoCompletion { + io_completion_handle, + completion_key, + completion_value, + status, + information, + } => self.sys_nt_set_io_completion( + io_completion_handle, + completion_key, + completion_value, + status, + information, + ), + SyscallRequest::NtRemoveIoCompletionEx { + io_completion_handle, + io_completion_information, + count, + num_entries_removed, + timeout, + alertable, + } => self.sys_nt_remove_io_completion_ex( + io_completion_handle, + io_completion_information, + count, + num_entries_removed, + timeout, + alertable, + ), + SyscallRequest::NtAlpcConnectPort { + port_handle, + port_name, + } => Self::sys_nt_alpc_connect_port(port_handle, port_name), SyscallRequest::NtConnectPort { port_handle, port_name, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 09ffe5c72..ceded0262 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -1286,6 +1286,8 @@ impl Task { length: u32, file_information_class: u32, ) -> NtStatus { + // TODO(windows-file-completion): Support FileCompletionInformation to associate a file + // handle with an IO completion port and its completion key. if FileHandleInformationClass::try_from(file_information_class) != Ok(FileHandleInformationClass::FilePositionInformation) { diff --git a/litebox_shim_windows/src/syscalls/iocp.rs b/litebox_shim_windows/src/syscalls/iocp.rs index c4b8a008a..79379cde5 100644 --- a/litebox_shim_windows/src/syscalls/iocp.rs +++ b/litebox_shim_windows/src/syscalls/iocp.rs @@ -3,16 +3,25 @@ //! Windows NT I/O completion port syscalls. -use alloc::sync::Arc; +use alloc::collections::{BTreeSet, VecDeque}; +use alloc::sync::{Arc, Weak}; use core::marker::PhantomData; +use litebox::event::observer::Observer; +use litebox::event::polling::{Pollee, TryOpError}; +use litebox::event::wait::WaitError; +use litebox::event::{Events, IOPollable}; use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; -use litebox::platform::{RawMutPointer as _, RawPointerProvider}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::sync::Mutex; use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; -use crate::nt_types::{AccessMask, ObjectAttributes, read_object_attributes}; +use crate::nt_types::{AccessMask, ObjectAttributes, ObjectAttributesFlags}; use crate::syscalls::Handle; -use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_buffer, probe_guest_output_preserving_value, +}; bitflags::bitflags! { #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -64,15 +73,117 @@ pub(crate) struct IoCompletionHandleObject { } pub(crate) struct IoCompletionObject { - _number_of_concurrent_threads: u32, - _not_send_without_platform: PhantomData, + concurrency_limit: usize, + active_threads: Mutex>, + packets: Mutex>, + pollee: Pollee, +} + +pub(crate) struct IoCompletionWorkerState { + port: Option>>, + suspended: bool, +} + +#[repr(C)] +#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes)] +pub(crate) struct IoCompletionPacket { + completion_key: usize, + completion_value: usize, + status: usize, + information: usize, } impl IoCompletionObject { fn new(number_of_concurrent_threads: u32) -> Self { Self { - _number_of_concurrent_threads: number_of_concurrent_threads, - _not_send_without_platform: PhantomData, + concurrency_limit: number_of_concurrent_threads.max(1) as usize, + active_threads: Mutex::new(BTreeSet::new()), + packets: Mutex::new(VecDeque::new()), + pollee: Pollee::new(), + } + } + + fn post(&self, packet: IoCompletionPacket) { + // TODO(windows-iocp-file-posting): Post packets when associated asynchronous file I/O + // completes, rather than relying exclusively on NtSetIoCompletion. + self.packets.lock().push_back(packet); + self.pollee.notify_observers(Events::IN); + } + + fn remove_with( + &self, + count: usize, + mut write_packet: impl FnMut(usize, IoCompletionPacket) -> Result<(), NtStatus>, + mut write_count: impl FnMut(u32) -> Result<(), NtStatus>, + ) -> Result { + let mut packets = self.packets.lock(); + let remove_count = count.min(packets.len()); + if remove_count == 0 { + return Ok(false); + } + for (index, packet) in packets.iter().take(remove_count).copied().enumerate() { + write_packet(index, packet)?; + } + write_count( + remove_count + .try_into() + .expect("packet count is bounded by the u32 input count"), + )?; + packets.drain(..remove_count); + Ok(true) + } + + fn remove_with_capacity( + &self, + thread_id: usize, + count: usize, + write_packet: impl FnMut(usize, IoCompletionPacket) -> Result<(), NtStatus>, + write_count: impl FnMut(u32) -> Result<(), NtStatus>, + ) -> Result { + let mut active_threads = self.active_threads.lock(); + let already_active = active_threads.contains(&thread_id); + if !already_active && active_threads.len() >= self.concurrency_limit { + return Ok(false); + } + let removed = self.remove_with(count, write_packet, write_count)?; + if removed && !already_active { + active_threads.insert(thread_id); + } + Ok(removed) + } + + fn deactivate(&self, thread_id: usize) { + if self.active_threads.lock().remove(&thread_id) { + self.pollee.notify_observers(Events::IN); + } + } + + fn reactivate(&self, thread_id: usize) { + // Windows permits a previously associated worker that blocked elsewhere to resume even + // when this temporarily exceeds the port's concurrency limit. + self.active_threads.lock().insert(thread_id); + } +} + +impl IoCompletionWorkerState { + pub(crate) fn new() -> Self { + Self { + port: None, + suspended: false, + } + } +} + +impl IOPollable for IoCompletionObject { + fn register_observer(&self, observer: Weak>, mask: Events) { + self.pollee.register_observer(observer, mask); + } + + fn check_io_events(&self) -> Events { + if self.packets.lock().is_empty() { + Events::empty() + } else { + Events::IN } } } @@ -83,20 +194,65 @@ impl IoCompletionHandleObject { } } -fn validate_io_completion_object_attributes( - object_attributes: Option>, -) -> Result<(), NtStatus> { - let Some(object_attributes) = object_attributes else { - return Ok(()); - }; - let object_attributes = read_object_attributes::(object_attributes)?; - if object_attributes.object_name == 0 && !object_attributes.root_directory.is_null() { - return Err(NtStatus::OBJECT_NAME_INVALID); - } - Ok(()) -} - impl Task { + pub(crate) fn suspend_io_completion_worker(&self) { + let mut worker = self.io_completion_worker.lock(); + debug_assert!(!worker.suspended); + if let Some(port) = worker.port.as_ref().and_then(Weak::upgrade) { + port.deactivate(self.thread_object.thread_id()); + worker.suspended = true; + } else { + worker.port = None; + } + } + + pub(crate) fn resume_io_completion_worker(&self) { + let mut worker = self.io_completion_worker.lock(); + if worker.suspended { + if let Some(port) = worker.port.as_ref().and_then(Weak::upgrade) { + port.reactivate(self.thread_object.thread_id()); + } else { + worker.port = None; + } + worker.suspended = false; + } + } + + pub(crate) fn release_io_completion_worker(&self) { + let mut worker = self.io_completion_worker.lock(); + if let Some(port) = worker.port.take() + && !worker.suspended + && let Some(port) = port.upgrade() + { + port.deactivate(self.thread_object.thread_id()); + } + worker.suspended = false; + } + + /// Check if the task is associated with the given IO completion port. If the task is + /// associated with a different port, it will be deactivated from that port. + fn prepare_io_completion_wait(&self, port: &Arc>) -> bool { + let mut worker = self.io_completion_worker.lock(); + debug_assert!(!worker.suspended); + if worker + .port + .as_ref() + .is_some_and(|active_port| active_port.as_ptr() == Arc::as_ptr(port)) + { + return true; + } + if let Some(active_port) = worker.port.take().and_then(|port| port.upgrade()) { + active_port.deactivate(self.thread_object.thread_id()); + } + false + } + + fn associate_io_completion_worker(&self, port: &Arc>) { + let mut worker = self.io_completion_worker.lock(); + debug_assert!(worker.port.is_none()); + worker.port = Some(Arc::downgrade(port)); + } + fn insert_io_completion_handle( &self, port: Arc>, @@ -129,16 +285,60 @@ impl Task { { return status; } - if let Err(status) = validate_io_completion_object_attributes::(object_attributes) - { - return status; + let (object_attributes, io_completion_name) = + match self.read_dispatcher_object_attributes(object_attributes, false) { + Ok(value) => value, + Err(status) => return status, + }; + let granted_access = IoCompletionAccess::from_desired_access(desired_access); + if let Some(io_completion_name) = io_completion_name { + let port = Arc::new(IoCompletionObject::new(number_of_concurrent_threads)); + return self.process.object_manager.create_io_completion( + &io_completion_name, + &port, + |port| { + let Some(object_attributes) = object_attributes else { + return NtStatus::INVALID_PARAMETER; + }; + if !ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) + .contains(ObjectAttributesFlags::OPENIF) + { + return NtStatus::OBJECT_NAME_COLLISION; + } + self.publish_io_completion_handle( + io_completion_handle, + port, + granted_access, + NtStatus::OBJECT_NAME_EXISTS, + ) + }, + || { + self.publish_io_completion_handle( + io_completion_handle, + Arc::clone(&port), + granted_access, + NtStatus::SUCCESS, + ) + }, + ); } - // TODO: model the IOCP packet queue, concurrency accounting, named-object lookup, - // and file-handle association once completion posting/removal and file completion - // context syscalls are implemented. let port = Arc::new(IoCompletionObject::new(number_of_concurrent_threads)); - let granted_access = IoCompletionAccess::from_desired_access(desired_access); + self.publish_io_completion_handle( + io_completion_handle, + port, + granted_access, + NtStatus::SUCCESS, + ) + } + + fn publish_io_completion_handle( + &self, + io_completion_handle: MutPtr, + port: Arc>, + granted_access: IoCompletionAccess, + success_status: NtStatus, + ) -> NtStatus { let Ok(handle) = self.insert_io_completion_handle(port, granted_access) else { return NtStatus::QUOTA_EXCEEDED; }; @@ -146,8 +346,155 @@ impl Task { self.close_io_completion_handle(handle); return NtStatus::ACCESS_VIOLATION; } + success_status + } + + pub(crate) fn sys_nt_open_io_completion( + &self, + io_completion_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + if let Err(status) = + probe_guest_output_preserving_value::(io_completion_handle) + { + return status; + } + let io_completion_name = + match self.read_dispatcher_object_attributes(object_attributes, true) { + Ok((_, Some(name))) => name, + Ok((_, None)) => return NtStatus::OBJECT_NAME_INVALID, + Err(status) => return status, + }; + let port = match self + .process + .object_manager + .resolve_io_completion(&io_completion_name) + { + Ok(port) => port, + Err(status) => return status, + }; + self.publish_io_completion_handle( + io_completion_handle, + port, + IoCompletionAccess::from_desired_access(desired_access), + NtStatus::SUCCESS, + ) + } + + pub(crate) fn sys_nt_set_io_completion( + &self, + io_completion_handle: Handle, + completion_key: usize, + completion_value: usize, + status: i32, + information: usize, + ) -> NtStatus { + let entry = match self.typed_handle_entry_with_access::>( + io_completion_handle, + IoCompletionAccess::MODIFY_STATE.bits(), + ) { + Ok(entry) => entry, + Err(status) => return status, + }; + entry.with_entry(|entry| { + entry.port.post(IoCompletionPacket { + completion_key, + completion_value, + status: status.cast_unsigned() as usize, + information, + }); + }); NtStatus::SUCCESS } + + pub(crate) fn sys_nt_remove_io_completion_ex( + &self, + io_completion_handle: Handle, + io_completion_information: MutPtr, + count: u32, + num_entries_removed: MutPtr, + timeout: Option>, + alertable: bool, + ) -> NtStatus { + if count == 0 { + return NtStatus::INVALID_PARAMETER; + } + let Some(buffer_length) = + (count as usize).checked_mul(core::mem::size_of::()) + else { + return NtStatus::INVALID_PARAMETER; + }; + if let Err(status) = probe_guest_output_buffer::( + MutPtr::::from_usize(io_completion_information.as_usize()), + buffer_length, + ) { + return status; + } + if let Err(status) = probe_guest_output_preserving_value::(num_entries_removed) + { + return status; + } + let timeout = match timeout { + Some(timeout) => match timeout.read_at_offset(0) { + Some(timeout) => Some(self.wait_timeout_duration(timeout)), + None => return NtStatus::ACCESS_VIOLATION, + }, + None => None, + }; + let entry = match self.typed_handle_entry_with_access::>( + io_completion_handle, + IoCompletionAccess::MODIFY_STATE.bits(), + ) { + Ok(entry) => entry, + Err(status) => return status, + }; + let port = entry.with_entry(IoCompletionHandleObject::port); + let already_associated = self.prepare_io_completion_wait(&port); + if alertable { + // TODO(windows-apc): Interrupt alertable IOCP waits to deliver queued user APCs. + litebox_util_log::debug!("Treating alertable IOCP wait as non-alertable"); + } + + match self.wait_on_events( + false, + timeout, + Events::IN, + |observer, mask| { + port.register_observer(observer, mask); + Ok(()) + }, + || match port.remove_with_capacity( + self.thread_object.thread_id(), + count as usize, + |index, packet| { + io_completion_information + .write_at_offset(index.cast_signed(), packet) + .ok_or(NtStatus::ACCESS_VIOLATION) + }, + |removed| { + num_entries_removed + .write_at_offset(0, removed) + .ok_or(NtStatus::ACCESS_VIOLATION) + }, + ) { + Ok(true) => { + if !already_associated { + self.associate_io_completion_worker(&port); + } + Ok(()) + } + Ok(false) => Err(TryOpError::TryAgain), + Err(status) => Err(TryOpError::Other(status)), + }, + ) { + Ok(()) => NtStatus::SUCCESS, + Err(TryOpError::WaitError(WaitError::TimedOut)) => NtStatus::TIMEOUT, + Err(TryOpError::WaitError(WaitError::Interrupted)) => NtStatus::ALERTED, + Err(TryOpError::TryAgain) => unreachable!("blocking wait cannot return TryAgain"), + Err(TryOpError::Other(status)) => status, + } + } } #[cfg(test)] @@ -167,6 +514,137 @@ mod tests { size_of::().trunc() } + fn packet(completion_key: usize) -> IoCompletionPacket { + IoCompletionPacket { + completion_key, + completion_value: 0, + status: 0, + information: 0, + } + } + + fn remove_one( + task: &Task, + port: &Arc>, + ) -> Result, NtStatus> { + let already_associated = task.prepare_io_completion_wait(port); + let mut packet = None; + let removed = port.remove_with_capacity( + task.thread_object.thread_id(), + 1, + |_, value| { + packet = Some(value); + Ok(()) + }, + |_| Ok(()), + )?; + if removed && !already_associated { + task.associate_io_completion_worker(port); + } + Ok(packet) + } + + #[test] + fn concurrency_limit_throttles_other_workers() { + let first = test_task(); + let second = first.clone_for_test().unwrap(); + let port = Arc::new(IoCompletionObject::new(1)); + port.post(packet(1)); + port.post(packet(2)); + + assert_eq!( + remove_one(&first, &port).unwrap().unwrap().completion_key, + 1 + ); + assert!(remove_one(&second, &port).unwrap().is_none()); + assert_eq!(port.packets.lock().len(), 1); + + assert_eq!( + remove_one(&first, &port).unwrap().unwrap().completion_key, + 2 + ); + first.release_io_completion_worker(); + second.release_io_completion_worker(); + } + + #[test] + fn blocking_worker_releases_concurrency_capacity() { + let first = test_task(); + let second = first.clone_for_test().unwrap(); + let port = Arc::new(IoCompletionObject::new(1)); + port.post(packet(1)); + port.post(packet(2)); + + assert!(remove_one(&first, &port).unwrap().is_some()); + first.suspend_io_completion_worker(); + assert_eq!( + remove_one(&second, &port).unwrap().unwrap().completion_key, + 2 + ); + + port.post(packet(3)); + assert!( + !port + .remove_with_capacity( + first.thread_object.thread_id(), + 1, + |_, _| unreachable!("a suspended worker must not dequeue at capacity"), + |_| unreachable!("a suspended worker must not dequeue at capacity"), + ) + .unwrap() + ); + assert_eq!(port.packets.lock().len(), 1); + + first.resume_io_completion_worker(); + assert_eq!(port.active_threads.lock().len(), 2); + first.release_io_completion_worker(); + second.release_io_completion_worker(); + assert!(port.active_threads.lock().is_empty()); + } + + #[test] + fn timed_out_wait_restores_worker_capacity() { + let task = test_task(); + let port = Arc::new(IoCompletionObject::new(1)); + port.post(packet(1)); + assert!(remove_one(&task, &port).unwrap().is_some()); + + let nonblocking_result: Result<(), TryOpError> = task.wait_on_events( + true, + None, + Events::IN, + |_, _| unreachable!("a nonblocking wait must not register an observer"), + || Err(TryOpError::TryAgain), + ); + assert!(matches!(nonblocking_result, Err(TryOpError::TryAgain))); + assert!( + port.active_threads + .lock() + .contains(&task.thread_object.thread_id()) + ); + assert!(!task.io_completion_worker.lock().suspended); + + let result: Result<(), TryOpError> = task.wait_on_events( + false, + Some(core::time::Duration::ZERO), + Events::IN, + |_, _| Ok::<(), NtStatus>(()), + || Err(TryOpError::TryAgain), + ); + + assert!(matches!( + result, + Err(TryOpError::WaitError(WaitError::TimedOut)) + )); + assert!( + port.active_threads + .lock() + .contains(&task.thread_object.thread_id()) + ); + assert!(!task.io_completion_worker.lock().suspended); + task.release_io_completion_worker(); + } + #[test] fn create_validates_object_attributes_without_clobbering_output() { let task = test_task(); diff --git a/litebox_shim_windows/src/syscalls/lpc.rs b/litebox_shim_windows/src/syscalls/lpc.rs index 8202b4cbb..8926a370d 100644 --- a/litebox_shim_windows/src/syscalls/lpc.rs +++ b/litebox_shim_windows/src/syscalls/lpc.rs @@ -781,6 +781,26 @@ pub(crate) struct ConnectPortParameters { } impl Task { + pub(crate) fn sys_nt_alpc_connect_port( + port_handle: MutPtr, + port_name: ConstPtr, + ) -> NtStatus { + if port_handle.write_at_offset(0, Handle::default()).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + let port_name = match port_name + .read_at_offset(0) + .ok_or(NtStatus::ACCESS_VIOLATION) + .and_then(UnicodeString::read_string::) + { + Ok(name) => name, + Err(status) => return status, + }; + + litebox_util_log::debug!(port_name:% = port_name; "ALPC port is unavailable"); + NtStatus::OBJECT_NAME_NOT_FOUND + } + pub(crate) fn sys_nt_connect_port(&self, params: ConnectPortParameters) -> NtStatus { if params.security_qos.read_at_offset(0).is_none() { return NtStatus::ACCESS_VIOLATION; diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index b16089a94..9ab2747b5 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -230,6 +230,30 @@ pub(crate) enum SyscallRequest { object_attributes: Option>, number_of_concurrent_threads: u32, }, + NtOpenIoCompletion { + io_completion_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, + NtSetIoCompletion { + io_completion_handle: Handle, + completion_key: usize, + completion_value: usize, + status: i32, + information: usize, + }, + NtRemoveIoCompletionEx { + io_completion_handle: Handle, + io_completion_information: Platform::RawMutPointer, + count: u32, + num_entries_removed: Platform::RawMutPointer, + timeout: Option>, + alertable: bool, + }, + NtAlpcConnectPort { + port_handle: Platform::RawMutPointer, + port_name: Platform::RawConstPointer, + }, NtConnectPort { port_handle: Platform::RawMutPointer, port_name: Platform::RawConstPointer, @@ -1015,6 +1039,30 @@ impl SyscallRequest { object_attributes:*, number_of_concurrent_threads, })), + NtSysno::NtOpenIoCompletion => Some(sys_req!(NtOpenIoCompletion { + io_completion_handle:*, + desired_access, + object_attributes:*, + })), + NtSysno::NtSetIoCompletion => Some(sys_req!(NtSetIoCompletion { + io_completion_handle: { Handle::from_raw }, + completion_key, + completion_value, + status, + information, + })), + NtSysno::NtRemoveIoCompletionEx => Some(sys_req!(NtRemoveIoCompletionEx { + io_completion_handle:{ Handle::from_raw }, + io_completion_information:*, + count, + num_entries_removed:*, + timeout:*, + alertable:{ |value: u8| value != 0 }, + })), + NtSysno::NtAlpcConnectPort => Some(sys_req!(NtAlpcConnectPort { + port_handle:*, + port_name:*, + })), NtSysno::NtConnectPort => Some(sys_req!(NtConnectPort { port_handle:*, port_name:*, diff --git a/litebox_shim_windows/src/syscalls/object_manager.rs b/litebox_shim_windows/src/syscalls/object_manager.rs index fdc0b4628..20d7223aa 100644 --- a/litebox_shim_windows/src/syscalls/object_manager.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -24,6 +24,7 @@ use crate::nt_types::{ }; use crate::syscalls::Handle; use crate::syscalls::event::EventObject; +use crate::syscalls::iocp::IoCompletionObject; use crate::syscalls::mutant::MutantObject; use crate::syscalls::section::{ SectionObject, WINDOWS_SESSION_SHARED_SECTION_OBJECT, WINDOWS_SHARED_SECTION_OBJECT, @@ -211,6 +212,9 @@ enum NamedObject { Event { event: Weak>, }, + IoCompletion { + io_completion: Weak>, + }, Mutant { mutant: Weak>, }, @@ -391,6 +395,7 @@ impl ObjectNode { new_directory() => NamedObject::Directory { children: BTreeMap::new() }; new_symlink(target: String) => NamedObject::Symlink { target }; new_event(event: Weak>) => NamedObject::Event { event }; + new_io_completion(io_completion: Weak>) => NamedObject::IoCompletion { io_completion }; new_mutant(mutant: Weak>) => NamedObject::Mutant { mutant }; new_semaphore(semaphore: Weak>) => NamedObject::Semaphore { semaphore }; new_section(section: Weak>) => NamedObject::Section { section }; @@ -438,6 +443,7 @@ impl ObjectNode { directory_object, ObjectLeafLookup<()>, NamedObject::Directory { .. } => ObjectLeafLookup::Live(()); pub(super) symlink_target, ObjectLeafLookup, NamedObject::Symlink { target } => ObjectLeafLookup::Live(target.clone()); event_object, ObjectLeafLookup>>, NamedObject::Event { event } => ObjectLeafLookup::from_weak(event); + io_completion_object, ObjectLeafLookup>>, NamedObject::IoCompletion { io_completion } => ObjectLeafLookup::from_weak(io_completion); mutant_object, ObjectLeafLookup>>, NamedObject::Mutant { mutant } => ObjectLeafLookup::from_weak(mutant); semaphore_object, ObjectLeafLookup>>, NamedObject::Semaphore { semaphore } => ObjectLeafLookup::from_weak(semaphore); section_object, ObjectLeafLookup>>, NamedObject::Section { section } => ObjectLeafLookup::from_weak(section); @@ -450,6 +456,9 @@ impl ObjectNode { NamedObject::Directory { .. } => Some("Directory"), NamedObject::Symlink { .. } => Some("SymbolicLink"), NamedObject::Event { event } => event.upgrade().map(|_| "Event"), + NamedObject::IoCompletion { io_completion } => { + io_completion.upgrade().map(|_| "IoCompletion") + } NamedObject::Mutant { mutant } => mutant.upgrade().map(|_| "Mutant"), NamedObject::Semaphore { semaphore } => semaphore.upgrade().map(|_| "Semaphore"), NamedObject::Section { section } => section.upgrade().map(|_| "Section"), @@ -549,6 +558,24 @@ impl ObjectManager { ) } + pub(super) fn create_io_completion( + &self, + path: &str, + io_completion: &Arc>, + on_exists: impl FnOnce(Arc>) -> NtStatus, + on_created: impl FnOnce() -> NtStatus, + ) -> NtStatus { + let io_completion = Arc::downgrade(io_completion); + self.create_child( + path, + |node| node.io_completion_object(), + |path, parent, name| ObjectNode::new_io_completion(path, parent, name, io_completion), + NtStatus::OBJECT_TYPE_MISMATCH, + on_exists, + |_| on_created(), + ) + } + pub(super) fn create_mutant( &self, path: &str, @@ -714,6 +741,13 @@ impl ObjectManager { self.resolve_object_leaf(path, false, |node| node.event_object()) } + pub(super) fn resolve_io_completion( + &self, + path: &str, + ) -> Result>, NtStatus> { + self.resolve_object_leaf(path, false, |node| node.io_completion_object()) + } + pub(super) fn resolve_mutant( &self, path: &str, diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index b68e8df29..2dc573083 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -1187,23 +1187,30 @@ impl Task { completion_filter, params.watch_tree, ); - let wait_cx = self.wait_cx(); - let wait_result = - self.global - .registry - .notification_pollee - .wait(&wait_cx, false, Events::IN, || { - if self.global.registry.notification_generation( - &path, - completion_filter, - params.watch_tree, - ) == generation - { - Err(TryOpError::::TryAgain) - } else { - Ok(()) - } - }); + let wait_result = self.wait_on_events( + false, + None, + Events::IN, + |observer, mask| { + self.global + .registry + .notification_pollee + .register_observer(observer, mask); + Ok(()) + }, + || { + if self.global.registry.notification_generation( + &path, + completion_filter, + params.watch_tree, + ) == generation + { + Err(TryOpError::::TryAgain) + } else { + Ok(()) + } + }, + ); let status = match wait_result { Ok(()) => NtStatus::NOTIFY_ENUM_DIR, Err(TryOpError::WaitError(litebox::event::wait::WaitError::Interrupted)) => { diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 798280645..ad95c30a6 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -766,12 +766,20 @@ impl Task { // anonymous backing that avoids kernel-side content storage. return Err(NtStatus::NOT_SUPPORTED); } + let is_reserved = section + .attributes + .contains(SectionAllocationAttributes::SEC_RESERVE); + let initial_permissions = if is_reserved { + MemoryRegionPermissions::empty() + } else { + permissions + }; let mapping = create_pages( &self.global.page_manager, None, length, CreatePagesFlags::empty(), - permissions, + initial_permissions, |_| Ok(0), ) .map_err(|_| { @@ -794,7 +802,11 @@ impl Task { size: mapped_size, allocation_protect: section.protection, type_: MemoryType::MEM_MAPPED, - pages: committed_pages(base, mapped_size, page_protection), + pages: if is_reserved { + RangeMap::new() + } else { + committed_pages(base, mapped_size, page_protection) + }, }, ); Ok(MappedPagefileSectionView { base, view_size }) diff --git a/litebox_shim_windows/src/syscalls/thread.rs b/litebox_shim_windows/src/syscalls/thread.rs index 847382d46..72c2e00f0 100644 --- a/litebox_shim_windows/src/syscalls/thread.rs +++ b/litebox_shim_windows/src/syscalls/thread.rs @@ -413,6 +413,7 @@ impl Task { process: self.process.clone(), fs: self.fs.clone(), wait_state: crate::wait::WaitState::new(self.global.platform), + io_completion_worker: Mutex::new(super::iocp::IoCompletionWorkerState::new()), entry_point: ntdll.ldr_initialize_thunk, stack_top: environment.stack_top, context: environment.context, diff --git a/litebox_shim_windows/src/syscalls/wait.rs b/litebox_shim_windows/src/syscalls/wait.rs index 28f5e2edd..013a77b14 100644 --- a/litebox_shim_windows/src/syscalls/wait.rs +++ b/litebox_shim_windows/src/syscalls/wait.rs @@ -121,9 +121,9 @@ impl Task { litebox_util_log::debug!("Treating alertable wait as non-alertable"); } - let wait_cx = self.wait_cx().with_timeout(timeout); - match wait_cx.wait_on_events( + match self.wait_on_events( false, + timeout, Events::IN, |observer, mask| { object.register_observer(observer, mask); @@ -157,7 +157,7 @@ impl Task { ) } - fn wait_timeout_duration(&self, timeout: i64) -> core::time::Duration { + pub(crate) fn wait_timeout_duration(&self, timeout: i64) -> core::time::Duration { const WINDOWS_TO_UNIX_EPOCH_SECONDS: u64 = 11_644_473_600; if timeout <= 0 { diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index c1d25f4bf..6f404cf3f 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -174,6 +174,9 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task Task { process: self.process.clone(), fs: self.fs.clone(), wait_state: crate::wait::WaitState::new(self.global.platform), + io_completion_worker: litebox::sync::Mutex::new( + crate::syscalls::iocp::IoCompletionWorkerState::new(), + ), entry_point: 0, stack_top: 0, context: 0, diff --git a/litebox_shim_windows/src/wait.rs b/litebox_shim_windows/src/wait.rs index 3704e81b5..96f9b0775 100644 --- a/litebox_shim_windows/src/wait.rs +++ b/litebox_shim_windows/src/wait.rs @@ -4,7 +4,13 @@ //! Wait state management. //! //! Use a dedicated module to prevent code from accidentally accessing -//! `wait_state` without going through `wait_cx()`. +//! `wait_state` without going through `wait_on_events()`. + +use alloc::sync::Weak; + +use litebox::event::Events; +use litebox::event::observer::Observer; +use litebox::event::polling::TryOpError; use crate::{ShimFS, ShimPlatform, Task}; @@ -17,9 +23,31 @@ impl WaitState { } impl Task { - /// Returns a wait context to use to perform interruptible waits. - pub(crate) fn wait_cx(&self) -> litebox::event::wait::WaitContext<'_, Platform> { - self.wait_state.0.context().with_check_for_interrupt(self) + pub(crate) fn wait_on_events( + &self, + nonblock: bool, + timeout: Option, + events: Events, + register_observer: impl FnOnce(Weak>, Events) -> Result<(), E>, + mut try_op: impl FnMut() -> Result>, + ) -> Result> { + match try_op() { + Err(TryOpError::TryAgain) if !nonblock => {} + ret => return ret, + } + + // The core helper probes again before blocking, so readiness racing with this point can + // briefly release the IOCP slot without a host block. Exact accounting requires a hook + // around WaitContext's platform block; keep this approximation local to the Windows shim. + self.suspend_io_completion_worker(); + let _resume_worker = litebox::utils::defer(|| self.resume_io_completion_worker()); + let wait_context = self + .wait_state + .0 + .context() + .with_check_for_interrupt(self) + .with_timeout(timeout); + wait_context.wait_on_events(false, events, register_observer, try_op) } /// Publishes the handle used by other threads to interrupt this one.