From 3bc3f53bf1b3dedc01b51cf872cccdc482981804 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Mon, 13 Jul 2026 01:15:31 -0700 Subject: [PATCH 1/2] fix ConDrv device --- litebox_shim_windows/src/lib.rs | 26 + litebox_shim_windows/src/loader/pe.rs | 36 +- litebox_shim_windows/src/syscalls/condrv.rs | 306 ++++++++++ litebox_shim_windows/src/syscalls/event.rs | 16 + litebox_shim_windows/src/syscalls/file.rs | 566 ++++++++++++------ .../src/syscalls/file_path.rs | 254 ++++++++ litebox_shim_windows/src/syscalls/mod.rs | 26 + .../src/syscalls/object_manager.rs | 201 +++++-- litebox_shim_windows/src/syscalls/symlink.rs | 6 +- 9 files changed, 1181 insertions(+), 256 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/condrv.rs create mode 100644 litebox_shim_windows/src/syscalls/file_path.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index b91b797352..ee38154698 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -1093,6 +1093,32 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtDeviceIoControlFile { + file_handle, + event, + apc_routine, + apc_context, + io_status_block, + io_control_code, + input_buffer, + input_buffer_length, + output_buffer, + output_buffer_length, + } => { + let status = self.sys_nt_device_io_control_file( + file_handle, + event, + apc_routine, + apc_context, + io_status_block, + io_control_code, + input_buffer, + input_buffer_length, + output_buffer, + output_buffer_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtApphelpCacheControl { service_class, service_data, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 8fc050db84..c859f81066 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -35,7 +35,7 @@ use crate::syscalls::process::{INITIAL_PROCESS_ID, INITIAL_THREAD_ID}; use crate::{MutPtr, ShimFS}; const NTDLL_WRITABLE_SECTIONS: &[&[u8]] = &[b".mrdata"]; -const NTDLL_PATHS: &[&str] = &["/Windows/System32/ntdll.dll", "/windows/system32/ntdll.dll"]; +const NTDLL_PATH: &str = "/Windows/System32/ntdll.dll"; const RUNTIME_FUNCTION_ENTRY_SIZE: usize = 12; const ZERO_CHUNK: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; const FILE_CHUNK_BYTES: usize = 64 * 1024; @@ -1038,26 +1038,24 @@ fn load_ntdll( fs: Arc, page_manager: &crate::WindowsPageManager, ) -> Result, WindowsLoadError> { - for path in NTDLL_PATHS { - match load_image_with_writable_sections( - fs.clone(), - path, - platform, - page_manager, - NTDLL_WRITABLE_SECTIONS, - ) { - Ok(image) => { - let exports = ntdll_exports::(&image)?; - litebox_util_log::debug!(path:% = path; "Loaded guest ntdll.dll"); - return Ok(Some(LoadedNtDll { image, exports })); - } - Err(error) if is_missing_file_error(&error) => {} - Err(error) => return Err(error), + match load_image_with_writable_sections( + fs, + NTDLL_PATH, + platform, + page_manager, + NTDLL_WRITABLE_SECTIONS, + ) { + Ok(image) => { + let exports = ntdll_exports::(&image)?; + litebox_util_log::debug!(path:% = NTDLL_PATH; "Loaded guest ntdll.dll"); + Ok(Some(LoadedNtDll { image, exports })) + } + Err(error) if is_missing_file_error(&error) => { + litebox_util_log::debug!("Guest ntdll.dll was not found in the initial filesystem"); + Ok(None) } + Err(error) => Err(error), } - - litebox_util_log::debug!("Guest ntdll.dll was not found in the initial filesystem"); - Ok(None) } fn load_image( diff --git a/litebox_shim_windows/src/syscalls/condrv.rs b/litebox_shim_windows/src/syscalls/condrv.rs new file mode 100644 index 0000000000..2dba4c65be --- /dev/null +++ b/litebox_shim_windows/src/syscalls/condrv.rs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows console driver support. + +use core::mem::size_of; + +use int_enum::IntEnum; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::nt_types::IoStatusBlock; +use crate::{ConstPtr, MutPtr}; + +const FILE_DEVICE_CONSOLE: u32 = 0x50; +const CD_SERVER_EA_NAME: &[u8] = b"server"; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +pub(crate) enum CondrvObject { + Input = 0, + Output = 1, + Server = 2, + Reference = 3, + Connect = 4, +} + +impl CondrvObject { + pub(crate) fn from_device_name(name: &str) -> Result { + match Self::from_component(name) { + Some(object @ (Self::Input | Self::Output | Self::Server)) => Ok(object), + Some(Self::Reference) => Err(NtStatus::INVALID_HANDLE), + Some(Self::Connect) => Err(NtStatus::OBJECT_TYPE_MISMATCH), + None => Err(NtStatus::OBJECT_NAME_NOT_FOUND), + } + } + + fn from_component(name: &str) -> Option { + if name.eq_ignore_ascii_case("Input") { + Some(Self::Input) + } else if name.eq_ignore_ascii_case("Output") { + Some(Self::Output) + } else if name.eq_ignore_ascii_case("Server") { + Some(Self::Server) + } else if name.eq_ignore_ascii_case("Reference") { + Some(Self::Reference) + } else if name.eq_ignore_ascii_case("Connect") { + Some(Self::Connect) + } else { + None + } + } + + pub(crate) fn relative_child(self, name: &str) -> Result { + let name = name.strip_prefix('\\').ok_or(NtStatus::NOT_FOUND)?; + let child = Self::from_component(name).ok_or(NtStatus::NOT_FOUND)?; + + match (self, child) { + (Self::Server, Self::Server | Self::Reference) + | (Self::Reference, Self::Server | Self::Connect | Self::Input | Self::Output) + | (Self::Input | Self::Output, Self::Server | Self::Input | Self::Output) => Ok(child), + (Self::Server, Self::Input | Self::Output) => Err(NtStatus::INVALID_DEVICE_STATE), + (Self::Reference | Self::Input | Self::Output, Self::Reference) => { + Err(NtStatus::OBJECT_TYPE_MISMATCH) + } + (Self::Server | Self::Input | Self::Output, Self::Connect) | (Self::Connect, _) => { + Err(NtStatus::INVALID_HANDLE) + } + } + } + + pub(crate) fn handle_path(self) -> &'static str { + match self { + Self::Input => "/dev/stdin", + Self::Output => "/dev/stdout", + Self::Server => r"\Device\ConDrv\Server", + Self::Reference => r"\Device\ConDrv\Reference", + Self::Connect => r"\Device\ConDrv\Connect", + } + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum IoControlMethod { + Buffered = 0, + InDirect = 1, + OutDirect = 2, + Neither = 3, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct IoControlAccess: u32 { + const ANY = 0; + const READ = 1; + const WRITE = 2; + const _ = !0; + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum ConsoleIoControlFunction { + LaunchServer = 13, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct FileFullEaInformation { + next_entry_offset: u32, + flags: u8, + ea_name_length: u8, + ea_value_length: u16, +} + +#[cfg(test)] +pub(crate) fn ea_buffer(name: &[u8], value_length: usize) -> alloc::vec::Vec { + let header = FileFullEaInformation { + next_entry_offset: 0, + flags: 0, + ea_name_length: u8::try_from(name.len()).unwrap(), + ea_value_length: u16::try_from(value_length).unwrap(), + }; + let mut buffer = alloc::vec::Vec::new(); + buffer.extend_from_slice(header.as_bytes()); + buffer.extend_from_slice(name); + buffer.push(0); + buffer.resize(buffer.len() + value_length, 0); + buffer +} + +pub(crate) fn validate_connect_server_ea( + ea_buffer: Option>, + ea_length: u32, +) -> Result<(), NtStatus> { + let Some(ea_buffer) = ea_buffer else { + return Err(NtStatus::EAS_NOT_SUPPORTED); + }; + let ea_length = usize::try_from(ea_length).unwrap(); + let Some(entry) = ConstPtr::::from_usize(ea_buffer.as_usize()) + .read_at_offset(0) + else { + return Err(NtStatus::ACCESS_VIOLATION); + }; + + let name_offset = size_of::(); + let name_length = usize::from(entry.ea_name_length); + let value_length = usize::from(entry.ea_value_length); + let value_offset = name_offset + .checked_add(name_length) + .and_then(|offset| offset.checked_add(1)) + .ok_or(NtStatus::EAS_NOT_SUPPORTED)?; + let entry_length = value_offset + .checked_add(value_length) + .ok_or(NtStatus::EAS_NOT_SUPPORTED)?; + if entry_length > ea_length { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + + let Some(name_address) = ea_buffer.as_usize().checked_add(name_offset) else { + return Err(NtStatus::EAS_NOT_SUPPORTED); + }; + let Some(name) = ConstPtr::::from_usize(name_address).to_owned_slice(name_length) + else { + return Err(NtStatus::ACCESS_VIOLATION); + }; + let Some(nul_address) = name_address.checked_add(name_length) else { + return Err(NtStatus::EAS_NOT_SUPPORTED); + }; + let Some(nul) = ConstPtr::::from_usize(nul_address).read_at_offset(0) else { + return Err(NtStatus::ACCESS_VIOLATION); + }; + if nul != 0 || !name.eq_ignore_ascii_case(CD_SERVER_EA_NAME) { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + + let Some(value_address) = ea_buffer.as_usize().checked_add(value_offset) else { + return Err(NtStatus::EAS_NOT_SUPPORTED); + }; + if ConstPtr::::from_usize(value_address) + .to_owned_slice(value_length) + .is_none() + { + return Err(NtStatus::ACCESS_VIOLATION); + } + + Ok(()) +} + +pub(crate) fn handle_ioctl( + condrv_object: CondrvObject, + io_status_block: MutPtr, + io_control_code: u32, + input_buffer: Option>, + input_buffer_length: u32, + output_buffer: Option>, + output_buffer_length: u32, +) -> NtStatus { + let device_type = io_control_code >> 16; + let access = IoControlAccess::from_bits_retain((io_control_code >> 14) & 0x3); + let function = (io_control_code >> 2) & 0xfff; + let method = IoControlMethod::try_from(io_control_code & 0x3); + + if device_type != FILE_DEVICE_CONSOLE || method != Ok(IoControlMethod::Neither) { + litebox_util_log::debug!( + condrv_object:? = condrv_object, + io_control_code:% = format_args!("{io_control_code:#x}"); + "Unsupported ConDrv IOCTL shape" + ); + return complete_ioctl::(io_status_block, NtStatus::NOT_SUPPORTED, 0); + } + + let Ok(function) = ConsoleIoControlFunction::try_from(function) else { + litebox_util_log::debug!( + condrv_object:? = condrv_object, + io_control_code:% = format_args!("{io_control_code:#x}"); + "Unsupported ConDrv IOCTL function" + ); + return complete_ioctl::(io_status_block, NtStatus::NOT_SUPPORTED, 0); + }; + + match (condrv_object, function) { + (CondrvObject::Server, ConsoleIoControlFunction::LaunchServer) + if access.is_empty() + && input_buffer.is_some() + && input_buffer_length != 0 + && output_buffer.is_none() + && output_buffer_length == 0 => + { + if input_buffer + .and_then(|input_buffer| input_buffer.read_at_offset(0)) + .is_none() + { + return complete_ioctl::(io_status_block, NtStatus::ACCESS_VIOLATION, 0); + } + complete_ioctl::(io_status_block, NtStatus::SUCCESS, 0) + } + _ => { + litebox_util_log::debug!( + condrv_object:? = condrv_object, + function:? = function, + io_control_code:% = format_args!("{io_control_code:#x}"); + "Unsupported ConDrv IOCTL for object" + ); + complete_ioctl::(io_status_block, NtStatus::NOT_SUPPORTED, 0) + } + } +} + +pub(crate) fn complete_ioctl( + io_status_block: MutPtr, + status: NtStatus, + information: usize, +) -> NtStatus { + if io_status_block + .write_at_offset(0, IoStatusBlock::new(status, information)) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + status +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn relative_children_match_host_parse_contexts() { + use CondrvObject::{Connect, Input, Output, Reference, Server}; + + for (parent, name, expected) in [ + (Server, r"\Server", Ok(Server)), + (Server, r"\Reference", Ok(Reference)), + (Server, r"\Connect", Err(NtStatus::INVALID_HANDLE)), + (Server, r"\Input", Err(NtStatus::INVALID_DEVICE_STATE)), + (Server, r"\Output", Err(NtStatus::INVALID_DEVICE_STATE)), + (Reference, r"\Server", Ok(Server)), + (Reference, r"\Connect", Ok(Connect)), + (Reference, r"\Input", Ok(Input)), + (Reference, r"\Output", Ok(Output)), + ( + Reference, + r"\Reference", + Err(NtStatus::OBJECT_TYPE_MISMATCH), + ), + (Input, r"\Server", Ok(Server)), + (Input, r"\Input", Ok(Input)), + (Input, r"\Output", Ok(Output)), + (Input, r"\Reference", Err(NtStatus::OBJECT_TYPE_MISMATCH)), + (Input, r"\Connect", Err(NtStatus::INVALID_HANDLE)), + (Output, r"\Server", Ok(Server)), + (Output, r"\Input", Ok(Input)), + (Output, r"\Output", Ok(Output)), + (Output, r"\Reference", Err(NtStatus::OBJECT_TYPE_MISMATCH)), + (Output, r"\Connect", Err(NtStatus::INVALID_HANDLE)), + ] { + assert_eq!(parent.relative_child(name), expected, "{parent:?} + {name}"); + } + + assert_eq!(Server.relative_child("Reference"), Err(NtStatus::NOT_FOUND)); + assert_eq!(Server.relative_child(r"\Missing"), Err(NtStatus::NOT_FOUND)); + } +} diff --git a/litebox_shim_windows/src/syscalls/event.rs b/litebox_shim_windows/src/syscalls/event.rs index 92bf3bdf97..09c5c0554e 100644 --- a/litebox_shim_windows/src/syscalls/event.rs +++ b/litebox_shim_windows/src/syscalls/event.rs @@ -399,6 +399,22 @@ impl Task { } } + pub(crate) fn set_event(&self, event_handle: Handle) -> NtStatus { + match self.modify_event(event_handle, None, |event| Ok(event.set())) { + Ok(()) => NtStatus::SUCCESS, + Err(status) => status, + } + } + + pub(crate) fn clear_event(&self, event_handle: Handle) -> Result<(), NtStatus> { + self.modify_event(event_handle, None, |event| Ok(event.clear())) + } + + pub(crate) fn check_event_modify_access(&self, event_handle: Handle) -> Result<(), NtStatus> { + let entry = self.event_entry(event_handle)?; + entry.with_entry(|entry| entry.granted_access.require(EventAccess::MODIFY_STATE)) + } + pub(crate) fn sys_nt_reset_event( &self, event_handle: Handle, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 76b48ccb48..9bb5457fcd 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -17,6 +17,8 @@ use crate::nt_types::{ AccessMask, IoStatusBlock, ObjectAttributes, UnicodeString, read_object_attributes, }; use crate::syscalls::Handle; +use crate::syscalls::condrv::{self, CondrvObject}; +use crate::syscalls::file_path::{FilePathResolver, FilePathRoot, FileTarget}; use crate::{ ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value, raw_handle_entry, }; @@ -27,12 +29,6 @@ const FILE_SHARE_READ: u32 = 0x0000_0001; const FILE_SHARE_WRITE: u32 = 0x0000_0002; const FILE_SHARE_DELETE: u32 = 0x0000_0004; -const CONDRV_INPUT_OBJECT: &str = "Input"; -const CONDRV_OUTPUT_OBJECT: &str = "Output"; -const CONDRV_SERVER_DEVICE: &str = "Server"; -const CONDRV_REFERENCE_OBJECT: &str = "Reference"; -const CONDRV_CONNECT_OBJECT: &str = "Connect"; - // These names and values are Windows ABI constants from WDK headers; Wine's // regular file/directory branch and ReactOS' filesystem device query path use // the same FILE_DEVICE_* and FILE_DEVICE_IS_MOUNTED vocabulary. @@ -93,13 +89,44 @@ impl FdEnabledSubsystemEntry for FileObject {} pub(crate) struct FileObject { path: String, - fd: TypedFd, + backing: FileObjectBacking, granted_access: FileAccess, share_access: FileShareAccess, - is_directory: bool, create_options: FileCreateOptions, } +enum FileObjectBacking { + Filesystem { + fd: TypedFd, + is_directory: bool, + }, + CondrvStream { + object: CondrvObject, + fd: TypedFd, + }, + CondrvControl(CondrvObject), +} + +impl FileObject { + fn condrv_object(&self) -> Option { + match self.backing { + FileObjectBacking::CondrvStream { object, .. } + | FileObjectBacking::CondrvControl(object) => Some(object), + FileObjectBacking::Filesystem { .. } => None, + } + } + + fn is_directory(&self) -> bool { + matches!( + self.backing, + FileObjectBacking::Filesystem { + is_directory: true, + .. + } + ) + } +} + bitflags::bitflags! { /// File object `ACCESS_MASK` rights accepted by `NtOpenFile`/`NtCreateFile`. /// @@ -353,16 +380,24 @@ impl Task { } pub(crate) fn close_file(&self, file: FileObject) { - let _ = self.fs.close(&file.fd); - if file - .create_options - .contains(FileCreateOptions::DELETE_ON_CLOSE) - { - if file.is_directory { - let _ = self.fs.rmdir(&file.path); - } else { - let _ = self.fs.unlink(&file.path); + match file.backing { + FileObjectBacking::Filesystem { fd, is_directory } => { + let _ = self.fs.close(&fd); + if file + .create_options + .contains(FileCreateOptions::DELETE_ON_CLOSE) + { + if is_directory { + let _ = self.fs.rmdir(&file.path); + } else { + let _ = self.fs.unlink(&file.path); + } + } } + FileObjectBacking::CondrvStream { fd, .. } => { + let _ = self.fs.close(&fd); + } + FileObjectBacking::CondrvControl(_) => {} } } @@ -485,6 +520,76 @@ impl Task { status } + #[expect( + clippy::too_many_arguments, + reason = "NtDeviceIoControlFile has ten ABI parameters; keeping them explicit preserves syscall ordering" + )] + pub(crate) fn sys_nt_device_io_control_file( + &self, + file_handle: Handle, + event: Handle, + apc_routine: Option>, + apc_context: Option>, + io_status_block: MutPtr, + io_control_code: u32, + input_buffer: Option>, + input_buffer_length: u32, + output_buffer: Option>, + output_buffer_length: u32, + ) -> NtStatus { + if let Err(status) = + probe_guest_output_preserving_value::(io_status_block) + { + return status; + } + if !event.is_null() + && let Err(status) = self.check_event_modify_access(event) + { + return status; + } + + let condrv_object = match self.file_entry(file_handle) { + Ok(entry) => entry.with_entry(FileObject::condrv_object), + Err(status) => return status, + }; + if !event.is_null() + && let Err(status) = self.clear_event(event) + { + return status; + } + let Some(condrv_object) = condrv_object else { + litebox_util_log::debug!( + file_handle = file_handle.as_raw(), + io_control_code:% = format_args!("{io_control_code:#x}"); + "Unsupported NtDeviceIoControlFile for non-ConDrv file handle" + ); + return NtStatus::INVALID_DEVICE_REQUEST; + }; + if apc_routine.is_some() || apc_context.is_some() { + litebox_util_log::debug!( + file_handle = file_handle.as_raw(), + apc_context = apc_context.map_or(0, |context| context.as_usize()); + "Ignoring NtDeviceIoControlFile APC completion arguments for synchronous completion" + ); + } + let status = condrv::handle_ioctl::( + condrv_object, + io_status_block, + io_control_code, + input_buffer, + input_buffer_length, + output_buffer, + output_buffer_length, + ); + if !event.is_null() { + let event_status = self.set_event(event); + if event_status != NtStatus::SUCCESS { + return event_status; + } + } + status + } + fn write_file_fs_device_information( &self, file_handle: Handle, @@ -553,17 +658,50 @@ impl Task { if object_attributes.object_name == 0 { return Err(NtStatus::INVALID_PARAMETER); } - if ea_buffer.is_some() || ea_length != 0 { - return Err(NtStatus::EAS_NOT_SUPPORTED); - } let desired_access = FileAccess::from_desired_access(desired_access); let create_options = FileCreateOptions::from_bits_retain(create_options); validate_create_options(desired_access, create_disposition, create_options)?; let share_access = FileShareAccess::from_share_access(share_access)?; - let path = self.object_attributes_to_fs_path(object_attributes)?; - self.check_file_sharing(&path, desired_access, share_access)?; + let (file, information) = match self.object_attributes_to_file_target(object_attributes)? { + FileTarget::Filesystem(path) => { + if ea_buffer.is_some() || ea_length != 0 { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + self.open_filesystem_target( + path, + desired_access, + share_access, + create_disposition, + create_options, + file_attributes, + ) + } + FileTarget::Condrv(object) => self.open_condrv_target( + object, + desired_access, + share_access, + create_disposition, + create_options, + file_attributes, + ea_buffer, + ea_length, + ), + }?; + let handle = self.insert_file_handle(file)?; + Ok((handle, information)) + } + fn open_filesystem_target( + &self, + path: String, + desired_access: FileAccess, + share_access: FileShareAccess, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, + file_attributes: u32, + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + self.check_file_sharing(&path, desired_access, share_access)?; if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { return self.open_or_create_directory( &path, @@ -575,7 +713,88 @@ impl Task { ); } - let existed_before_open = self.fs.file_status(&path).is_ok(); + let (fd, is_directory, information) = self.open_backing_fd( + &path, + desired_access, + create_disposition, + create_options, + file_attributes, + )?; + Ok(( + FileObject { + path, + backing: FileObjectBacking::Filesystem { fd, is_directory }, + granted_access: desired_access, + share_access, + create_options, + }, + information, + )) + } + + #[expect( + clippy::too_many_arguments, + reason = "ConDrv creation validates the parsed NtCreateFile fields at the device boundary" + )] + fn open_condrv_target( + &self, + object: CondrvObject, + desired_access: FileAccess, + share_access: FileShareAccess, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, + file_attributes: u32, + ea_buffer: Option>, + ea_length: u32, + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + if object == CondrvObject::Connect { + condrv::validate_connect_server_ea::(ea_buffer, ea_length)?; + } else if ea_buffer.is_some() || ea_length != 0 { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { + return Err(NtStatus::NOT_A_DIRECTORY); + } + + let path = String::from(object.handle_path()); + self.check_file_sharing(&path, desired_access, share_access)?; + let (backing, information) = match object { + CondrvObject::Input | CondrvObject::Output => { + let (fd, _, information) = self.open_backing_fd( + &path, + desired_access, + create_disposition, + create_options, + file_attributes, + )?; + (FileObjectBacking::CondrvStream { object, fd }, information) + } + CondrvObject::Server | CondrvObject::Reference | CondrvObject::Connect => ( + FileObjectBacking::CondrvControl(object), + FileCreateInformation::Opened, + ), + }; + Ok(( + FileObject { + path, + backing, + granted_access: desired_access, + share_access, + create_options, + }, + information, + )) + } + + fn open_backing_fd( + &self, + path: &str, + desired_access: FileAccess, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, + file_attributes: u32, + ) -> Result<(TypedFd, bool, FileCreateInformation), NtStatus> { + let existed_before_open = self.fs.file_status(path).is_ok(); if create_disposition == CreateDisposition::Supersede && existed_before_open && !desired_access.contains(FileAccess::DELETE) @@ -585,7 +804,7 @@ impl Task { let flags = desired_access.open_flags(create_disposition, create_options); let fd = self .fs - .open(&path, flags, create_mode(file_attributes)) + .open(path, flags, create_mode(file_attributes)) .map_err(|error| map_open_error(error, create_disposition))?; let file_status = match self.fs.fd_file_status(&fd) { Ok(file_status) => file_status, @@ -601,15 +820,11 @@ impl Task { return Err(NtStatus::OBJECT_TYPE_MISMATCH); } let information = create_disposition.success_information(existed_before_open); - let handle = self.insert_file_handle(FileObject { - path, + Ok(( fd, - granted_access: desired_access, - share_access, - is_directory: file_status.file_type == FileType::Directory, - create_options, - })?; - Ok((handle, information)) + file_status.file_type == FileType::Directory, + information, + )) } fn open_or_create_directory( @@ -620,7 +835,7 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, file_attributes: u32, - ) -> Result<(Handle, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { if matches!( create_disposition, CreateDisposition::Supersede @@ -662,37 +877,48 @@ impl Task { .open(path, flags, Mode::empty()) .map_err(|error| map_open_error(error, create_disposition))?; let information = create_disposition.success_information(existed_before_open); - let handle = self.insert_file_handle(FileObject { - path: String::from(path), - fd, - granted_access: desired_access, - share_access, - is_directory: true, - create_options, - })?; - Ok((handle, information)) + Ok(( + FileObject { + path: String::from(path), + backing: FileObjectBacking::Filesystem { + fd, + is_directory: true, + }, + granted_access: desired_access, + share_access, + create_options, + }, + information, + )) } - fn object_attributes_to_fs_path( + fn object_attributes_to_file_target( &self, object_attributes: ObjectAttributes, - ) -> Result { + ) -> Result { let object_name_ptr = ConstPtr::::from_usize(object_attributes.object_name); let object_name = object_name_ptr .read_at_offset(0) .ok_or(NtStatus::ACCESS_VIOLATION)?; let object_name = object_name.read_string::()?; - if object_attributes.root_directory.is_null() || is_absolute_windows_path(&object_name) { - return absolute_nt_file_name_to_fs_path(&object_name); + let resolver = FilePathResolver::new(&self.process.object_manager); + if object_attributes.root_directory.is_null() { + return resolver.resolve(FilePathRoot::Namespace, &object_name); } let root_file = self.file_entry(object_attributes.root_directory)?; root_file.with_entry(|root_file| { - if !root_file.is_directory { - return Err(NtStatus::NOT_A_DIRECTORY); + if let Some(parent) = root_file.condrv_object() { + return resolver.resolve(FilePathRoot::Condrv(parent), &object_name); } - relative_nt_file_name_to_fs_path(&root_file.path, &object_name) + resolver.resolve( + FilePathRoot::Filesystem { + path: &root_file.path, + is_directory: root_file.is_directory(), + }, + &object_name, + ) }) } @@ -843,120 +1069,6 @@ fn create_directory_mode(file_attributes: u32) -> Mode { create_mode(file_attributes) | Mode::XUSR } -/// Convert the NT file-name forms we currently support at the object-manager to -/// filesystem seam. -/// -/// Native NT reaches this seam by walking object-manager directories until it -/// reaches a device object, then the device parse routine hands the remaining -/// path to the filesystem driver. LiteBox intentionally uses the Wine-style -/// shortcut here instead: known NT prefixes are recognized as strings and then -/// mapped directly into the sandbox filesystem. Today that includes `\??\`, -/// `\\?\`, any drive-letter prefix, both `\SystemRoot\` and `/SystemRoot/`, -/// `\Device\HarddiskVolume1\`, and `\Device\ConDrv\`. A unified object-manager -/// walk through device objects into the backing filesystem namespace remains -/// outside this file-path mapper. -fn absolute_nt_file_name_to_fs_path(name: &str) -> Result { - let mut name = name; - if let Some(rest) = strip_case_insensitive_prefix(name, "\\??\\") { - name = rest; - } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\\\?\\") { - name = rest; - } - - if name.len() >= 3 && name.as_bytes()[1] == b':' && matches!(name.as_bytes()[2], b'\\' | b'/') { - name = &name[2..]; - } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\SystemRoot\\") { - return join_absolute_components("/Windows", rest); - } else if let Some(rest) = strip_case_insensitive_prefix(name, "/SystemRoot/") { - return join_absolute_components("/Windows", rest); - } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\Device\\HarddiskVolume1\\") { - return join_absolute_components("/", rest); - } else if let Some(device_name) = strip_case_insensitive_prefix(name, "\\Device\\ConDrv\\") { - return condrv_device_file(device_name).ok_or(NtStatus::OBJECT_NAME_NOT_FOUND); - } - - let path = name.trim_start_matches(['\\', '/']); - join_absolute_components("/", path) -} - -fn relative_nt_file_name_to_fs_path(root_path: &str, name: &str) -> Result { - if is_absolute_windows_path(name) { - return absolute_nt_file_name_to_fs_path(name); - } - join_absolute_components(root_path, name) -} - -fn join_absolute_components(root_path: &str, components: &str) -> Result { - let mut path = String::from(root_path.trim_end_matches('/')); - if path.is_empty() { - path.push('/'); - } - for component in components.split(['\\', '/']) { - if component.is_empty() || component == "." { - continue; - } - if component == ".." { - return Err(NtStatus::INVALID_PARAMETER); - } - if !path.ends_with('/') { - path.push('/'); - } - append_windows_component(&mut path, component); - } - Ok(path) -} - -fn condrv_device_file(device_name: &str) -> Option { - if device_name.eq_ignore_ascii_case(CONDRV_INPUT_OBJECT) { - return Some(String::from("/dev/stdin")); - } - if device_name.eq_ignore_ascii_case(CONDRV_OUTPUT_OBJECT) { - return Some(String::from("/dev/stdout")); - } - if device_name.eq_ignore_ascii_case(CONDRV_SERVER_DEVICE) - || device_name.eq_ignore_ascii_case(CONDRV_REFERENCE_OBJECT) - || device_name.eq_ignore_ascii_case(CONDRV_CONNECT_OBJECT) - { - return Some(String::from("/dev/null")); - } - None -} - -fn append_windows_component(path: &mut String, component: &str) { - if component.eq_ignore_ascii_case("Windows") { - path.push_str("Windows"); - } else if component.eq_ignore_ascii_case("System32") { - path.push_str("System32"); - } else if ends_with_ignore_ascii_case(component, ".dll") - || ends_with_ignore_ascii_case(component, ".nls") - { - path.push_str(&component.to_ascii_lowercase()); - } else { - path.push_str(component); - } -} - -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(|| &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 is_absolute_windows_path(name: &str) -> bool { - name.starts_with(['\\', '/']) - || name - .as_bytes() - .get(1..3) - .is_some_and(|bytes| bytes[0] == b':' && matches!(bytes[1], b'\\' | b'/')) -} - fn map_open_error(error: OpenError, create_disposition: CreateDisposition) -> NtStatus { match error { OpenError::PathError(error) => match error { @@ -1105,6 +1217,97 @@ mod tests { handle } + fn open_condrv_server(task: &Task) -> Handle { + let (_server_path, _server_name, server_attributes) = + open_object_attributes(r"\Device\ConDrv\Server"); + let mut io_status = IoStatusBlock::default(); + task.do_nt_create_file( + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + server_attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0 + } + + fn open_condrv_reference(task: &Task, server_handle: Handle) -> Handle { + let (_reference_path, _reference_name, mut reference_attributes) = + open_object_attributes(r"\Reference"); + reference_attributes.root_directory = server_handle; + let mut io_status = IoStatusBlock::default(); + task.do_nt_create_file( + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + reference_attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0 + } + + #[test] + fn nt_create_file_follows_condrv_server_reference_connect_sequence() { + let task = crate::tests::test_task(); + let server_handle = open_condrv_server(&task); + let reference_handle = open_condrv_reference(&task, server_handle); + let (_connect_path, _connect_name, mut connect_attributes) = + open_object_attributes(r"\Connect"); + connect_attributes.root_directory = reference_handle; + let ea = condrv::ea_buffer(b"server", 1340); + let mut connect_handle = Handle::default(); + let mut io_status = IoStatusBlock::default(); + + assert_eq!( + task.file_entry(server_handle) + .unwrap() + .with_entry(FileObject::condrv_object), + Some(CondrvObject::Server) + ); + assert_eq!( + task.file_entry(reference_handle) + .unwrap() + .with_entry(FileObject::condrv_object), + Some(CondrvObject::Reference) + ); + assert_eq!( + task.sys_nt_create_file( + mut_ptr(&mut connect_handle), + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + Some(const_ptr(&connect_attributes)), + mut_ptr(&mut io_status), + None, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + Some(const_ptr(&ea[0])), + u32::try_from(ea.len()).unwrap(), + ), + NtStatus::SUCCESS + ); + assert_eq!( + task.file_entry(connect_handle) + .unwrap() + .with_entry(FileObject::condrv_object), + Some(CondrvObject::Connect) + ); + + assert_eq!(task.sys_nt_close(connect_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(reference_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(server_handle), NtStatus::SUCCESS); + } + #[test] fn nt_query_volume_information_file_returns_fs_device_information() { run_with_test_platform_pointers(|| { @@ -1245,7 +1448,8 @@ mod tests { .unwrap(); create_existing_file(&task, "/tmp/dir/child.txt", b"child"); - let (_path, _name, attributes) = open_object_attributes("\\tmp\\dir-file-root.txt"); + let (_path, _name, attributes) = + open_object_attributes(r"\Device\HarddiskVolume1\tmp\dir-file-root.txt"); let mut handle = Handle::default(); let mut io_status = IoStatusBlock::default(); assert_eq!( @@ -1265,7 +1469,8 @@ mod tests { usize::from(FileCreateInformation::Opened) ); - let (_path, _name, directory_attributes) = open_object_attributes("\\tmp\\dir"); + let (_path, _name, directory_attributes) = + open_object_attributes(r"\Device\HarddiskVolume1\tmp\dir"); let directory_handle = task .do_nt_create_file( FILE_GENERIC_READ, @@ -1821,37 +2026,6 @@ mod tests { assert_eq!(handle, original_handle); } - #[test] - fn nt_create_file_maps_dos_paths_into_the_sandbox_fs() { - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\??\C:\Windows\System32\ntdll.dll").unwrap(), - "/Windows/System32/ntdll.dll" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\??\c:\windows\system32\KERNEL32.DLL").unwrap(), - "/Windows/System32/kernel32.dll" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path( - r"\Device\HarddiskVolume1\Windows\System32\c_1252.NLS" - ) - .unwrap(), - "/Windows/System32/c_1252.nls" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\SystemRoot\System32\kernel32.dll").unwrap(), - "/Windows/System32/kernel32.dll" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\Device\ConDrv\Output").unwrap(), - "/dev/stdout" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\Device\ConDrv\Connect").unwrap(), - "/dev/null" - ); - } - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] mod host_fidelity { use super::*; diff --git a/litebox_shim_windows/src/syscalls/file_path.rs b/litebox_shim_windows/src/syscalls/file_path.rs new file mode 100644 index 0000000000..5ac9e7a39c --- /dev/null +++ b/litebox_shim_windows/src/syscalls/file_path.rs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::string::String; + +use litebox_common_windows::nt_status::NtStatus; + +use crate::syscalls::condrv::CondrvObject; +use crate::syscalls::object_manager::{FileDeviceObject, ObjectManager}; + +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum FileTarget { + Filesystem(String), + Condrv(CondrvObject), +} + +pub(crate) enum FilePathRoot<'a> { + Namespace, + Filesystem { path: &'a str, is_directory: bool }, + Condrv(CondrvObject), +} + +pub(crate) struct FilePathResolver<'a, Platform: crate::ShimPlatform> { + object_manager: &'a ObjectManager, +} + +impl<'a, Platform: crate::ShimPlatform> FilePathResolver<'a, Platform> { + pub(crate) fn new(object_manager: &'a ObjectManager) -> Self { + Self { object_manager } + } + + pub(crate) fn resolve( + &self, + root: FilePathRoot<'_>, + name: &str, + ) -> Result { + match root { + FilePathRoot::Condrv(parent) => parent.relative_child(name).map(FileTarget::Condrv), + FilePathRoot::Namespace => self.resolve_absolute(name), + FilePathRoot::Filesystem { .. } if is_absolute_windows_path(name) => { + self.resolve_absolute(name) + } + FilePathRoot::Filesystem { + is_directory: false, + .. + } => Err(NtStatus::NOT_A_DIRECTORY), + FilePathRoot::Filesystem { path, .. } => { + join_absolute_components(path, name).map(FileTarget::Filesystem) + } + } + } + + fn resolve_absolute(&self, name: &str) -> Result { + if name.starts_with('/') { + return join_absolute_components("/", name).map(FileTarget::Filesystem); + } + if !is_absolute_windows_path(name) { + return Err(NtStatus::OBJECT_PATH_SYNTAX_BAD); + } + + let object_path = absolute_windows_file_name_to_object_path(name); + let (device, remaining) = self.object_manager.resolve_file_device(&object_path)?; + file_device_path_to_file_target(device, &remaining) + } +} + +fn absolute_windows_file_name_to_object_path(name: &str) -> String { + if let Some(rest) = strip_case_insensitive_prefix(name, "\\\\?\\") { + return alloc::format!(r"\??\{}", normalize_file_name_separators(rest)); + } + if name.starts_with('\\') { + return normalize_file_name_separators(name); + } + alloc::format!(r"\??\{}", normalize_file_name_separators(name)) +} + +fn normalize_file_name_separators(name: &str) -> String { + name.replace('/', "\\") +} + +fn file_device_path_to_file_target( + device: FileDeviceObject, + remaining: &str, +) -> Result { + match device { + FileDeviceObject::Filesystem { root_path } => { + join_absolute_components(&root_path, remaining).map(FileTarget::Filesystem) + } + FileDeviceObject::ConsoleDriver => { + CondrvObject::from_device_name(remaining).map(FileTarget::Condrv) + } + } +} + +fn join_absolute_components(root_path: &str, components: &str) -> Result { + let mut path = String::from(root_path.trim_end_matches('/')); + if path.is_empty() { + path.push('/'); + } + for component in components.split(['\\', '/']) { + if component.is_empty() || component == "." { + continue; + } + if component == ".." { + return Err(NtStatus::INVALID_PARAMETER); + } + if !path.ends_with('/') { + path.push('/'); + } + path.push_str(component); + } + Ok(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(|| &value[prefix.len()..]) +} + +fn is_absolute_windows_path(name: &str) -> bool { + name.starts_with(['\\', '/']) || is_absolute_windows_drive_path(name) +} + +fn is_absolute_windows_drive_path(name: &str) -> bool { + name.as_bytes() + .get(1..3) + .is_some_and(|bytes| bytes[0] == b':' && matches!(bytes[1], b'\\' | b'/')) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn namespace_paths_resolve_through_the_object_manager() { + let task = crate::tests::test_task(); + let resolver = FilePathResolver::new(&task.process.object_manager); + let resolve = |name| resolver.resolve(FilePathRoot::Namespace, name); + let resolve_path = |name| { + resolve(name).map(|target| match target { + FileTarget::Filesystem(path) => path, + FileTarget::Condrv(object) => String::from(object.handle_path()), + }) + }; + + assert_eq!( + resolve_path(r"\??\C:\Windows\System32\ntdll.dll").unwrap(), + "/Windows/System32/ntdll.dll" + ); + assert_eq!( + resolve_path(r"\??\c:\windows\system32\KERNEL32.DLL").unwrap(), + "/windows/system32/KERNEL32.DLL" + ); + assert_eq!( + resolve_path(r"\Device\HarddiskVolume1\Windows\System32\c_1252.NLS").unwrap(), + "/Windows/System32/c_1252.NLS" + ); + assert_eq!( + resolve_path(r"\SystemRoot\System32\kernel32.dll").unwrap(), + "/Windows/System32/kernel32.dll" + ); + assert_eq!( + resolve_path(r"\Device\ConDrv\Output").unwrap(), + "/dev/stdout" + ); + assert_eq!( + resolve_path(r"\Device\ConDrv\Reference"), + Err(NtStatus::INVALID_HANDLE) + ); + assert_eq!( + resolve_path(r"\Device\ConDrv\Connect"), + Err(NtStatus::OBJECT_TYPE_MISMATCH) + ); + assert_eq!( + resolve(r"\Missing\file.txt"), + Err(NtStatus::OBJECT_PATH_NOT_FOUND) + ); + assert_eq!( + resolve("/tmp/compatibility-path.txt"), + Ok(FileTarget::Filesystem(String::from( + "/tmp/compatibility-path.txt" + ))) + ); + assert_eq!( + resolve("/SystemRoot/not-an-object-path"), + Ok(FileTarget::Filesystem(String::from( + "/SystemRoot/not-an-object-path" + ))) + ); + assert_eq!( + resolve("relative.txt"), + Err(NtStatus::OBJECT_PATH_SYNTAX_BAD) + ); + } + + #[test] + fn root_kind_controls_relative_path_resolution() { + let task = crate::tests::test_task(); + let resolver = FilePathResolver::new(&task.process.object_manager); + + assert_eq!( + resolver.resolve( + FilePathRoot::Filesystem { + path: "/tmp/root", + is_directory: true, + }, + r"child\file.txt", + ), + Ok(FileTarget::Filesystem(String::from( + "/tmp/root/child/file.txt" + ))) + ); + assert_eq!( + resolver.resolve( + FilePathRoot::Filesystem { + path: "/tmp/root", + is_directory: true, + }, + r"C:\Windows\System32\ntdll.dll", + ), + Ok(FileTarget::Filesystem(String::from( + "/Windows/System32/ntdll.dll" + ))) + ); + assert_eq!( + resolver.resolve( + FilePathRoot::Filesystem { + path: "/tmp/root", + is_directory: true, + }, + r"MixedCase\File.TXT", + ), + Ok(FileTarget::Filesystem(String::from( + "/tmp/root/MixedCase/File.TXT" + ))) + ); + assert_eq!( + resolver.resolve( + FilePathRoot::Filesystem { + path: "/tmp/root.txt", + is_directory: false, + }, + "child.txt", + ), + Err(NtStatus::NOT_A_DIRECTORY) + ); + assert_eq!( + resolver.resolve(FilePathRoot::Condrv(CondrvObject::Reference), r"\Connect"), + Ok(FileTarget::Condrv(CondrvObject::Connect)) + ); + } +} diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 652a44fec2..09a92765c7 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -2,8 +2,10 @@ // Licensed under the MIT license. pub(crate) mod apphelp; +pub(crate) mod condrv; pub(crate) mod event; pub(crate) mod file; +pub(crate) mod file_path; pub(crate) mod iocp; pub(crate) mod mm; pub(crate) mod nls; @@ -313,6 +315,18 @@ pub(crate) enum SyscallRequest { length: u32, fs_information_class: u32, }, + NtDeviceIoControlFile { + file_handle: Handle, + event: Handle, + apc_routine: Option>, + apc_context: Option>, + io_status_block: Platform::RawMutPointer, + io_control_code: u32, + input_buffer: Option>, + input_buffer_length: u32, + output_buffer: Option>, + output_buffer_length: u32, + }, NtApphelpCacheControl { service_class: u32, service_data: Option>, @@ -725,6 +739,18 @@ impl SyscallRequest { length, fs_information_class, })), + NtSysno::NtDeviceIoControlFile => Some(sys_req!(NtDeviceIoControlFile { + file_handle:{Handle::from_raw}, + event:{Handle::from_raw}, + apc_routine:*, + apc_context:*, + io_status_block:*, + io_control_code, + input_buffer:*, + input_buffer_length, + output_buffer:*, + output_buffer_length, + })), NtSysno::NtApphelpCacheControl => Some(sys_req!(NtApphelpCacheControl { service_class, service_data:*, diff --git a/litebox_shim_windows/src/syscalls/object_manager.rs b/litebox_shim_windows/src/syscalls/object_manager.rs index 936966851a..09b4d1bb6c 100644 --- a/litebox_shim_windows/src/syscalls/object_manager.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -58,6 +58,8 @@ const SEEDED_DIRECTORY_PATHS: &[&str] = &[ // Wine's wineboot and ReactOS SMSS create KnownDllPath so ntdll can open/query // the DOS path prefix for known DLL lookups during loader initialization. const SEEDED_SYMLINK_PATHS: &[(&str, &str)] = &[ + (r"\??\C:", r"\Device\HarddiskVolume1"), + (r"\SystemRoot", r"\Device\HarddiskVolume1\Windows"), (r"\KnownDlls\KnownDllPath", r"C:\Windows\System32"), // TODO(windows-sessions): resolve this through the current session id once // the shim supports multiple Windows sessions. @@ -138,6 +140,12 @@ pub(crate) struct ObjectManager { root: Arc>, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum FileDeviceObject { + Filesystem { root_path: String }, + ConsoleDriver, +} + enum NamedObject { Directory { children: BTreeMap>>, @@ -151,6 +159,9 @@ enum NamedObject { Section { section: Weak>, }, + FileDevice { + device: FileDeviceObject, + }, } pub(super) enum ObjectLeafLookup { @@ -319,6 +330,7 @@ impl ObjectNode { new_symlink(target: String) => NamedObject::Symlink { target }; new_event(event: Weak>) => NamedObject::Event { event }; new_section(section: Weak>) => NamedObject::Section { section }; + new_file_device(device: FileDeviceObject) => NamedObject::FileDevice { device }; } fn child(&self, name: &str) -> Option> { @@ -353,11 +365,16 @@ impl ObjectNode { matches!(&*self.body.read(), NamedObject::Symlink { .. }) } + fn is_file_device(&self) -> bool { + matches!(&*self.body.read(), NamedObject::FileDevice { .. }) + } + object_leaf_accessors! { 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); section_object, ObjectLeafLookup>>, NamedObject::Section { section } => ObjectLeafLookup::from_weak(section); + file_device_object, ObjectLeafLookup, NamedObject::FileDevice { device } => ObjectLeafLookup::Live(device.clone()); } fn type_name(&self) -> Option<&'static str> { @@ -366,6 +383,7 @@ impl ObjectNode { NamedObject::Symlink { .. } => Some("SymbolicLink"), NamedObject::Event { event } => event.upgrade().map(|_| "Event"), NamedObject::Section { section } => section.upgrade().map(|_| "Section"), + NamedObject::FileDevice { .. } => Some("Device"), } } @@ -476,6 +494,17 @@ impl ObjectManager { ) } + fn create_file_device(&self, path: &str, device: FileDeviceObject) -> NtStatus { + self.create_child( + path, + |node| node.file_device_object(), + |path, parent, name| ObjectNode::new_file_device(path, parent, name, device), + NtStatus::OBJECT_TYPE_MISMATCH, + |_| NtStatus::OBJECT_NAME_EXISTS, + |_| NtStatus::SUCCESS, + ) + } + fn create_child( &self, path: &str, @@ -509,8 +538,11 @@ impl ObjectManager { return NtStatus::OBJECT_NAME_INVALID; } - let parent = match self.resolve_tail(parent_tail, NtStatus::OBJECT_PATH_NOT_FOUND, false) { - Ok(parent) => parent, + let parent = match self.resolve_tail(parent_tail, NtStatus::OBJECT_PATH_NOT_FOUND, true) { + Ok((parent, remaining)) if remaining.is_empty() => parent, + Ok((_, remaining)) => { + return unresolved_tail_status(&remaining, NtStatus::OBJECT_PATH_NOT_FOUND); + } Err(status) => return status, }; let mut body = parent.body.write(); @@ -545,7 +577,7 @@ impl ObjectManager { &self, path: &str, ) -> Result>, NtStatus> { - self.resolve_object_leaf(path, false, |node| { + self.resolve_object_leaf(path, true, |node| { node.directory_object().map(|()| Arc::clone(node)) }) } @@ -553,32 +585,58 @@ impl ObjectManager { pub(super) fn resolve_symlink( &self, path: &str, - open_final_symlink: bool, + follow_final_symlink: bool, ) -> Result>, NtStatus> { - self.resolve_object_leaf(path, open_final_symlink, |node| { + self.resolve_object_leaf(path, follow_final_symlink, |node| { node.symlink_target().map(|_| Arc::clone(node)) }) } pub(super) fn resolve_event(&self, path: &str) -> Result>, NtStatus> { - self.resolve_object_leaf(path, true, |node| node.event_object()) + self.resolve_object_leaf(path, false, |node| node.event_object()) } pub(super) fn resolve_section( &self, path: &str, ) -> Result>, NtStatus> { - self.resolve_object_leaf(path, false, |node| node.section_object()) + self.resolve_object_leaf(path, true, |node| node.section_object()) + } + + pub(crate) fn resolve_file_device( + &self, + path: &str, + ) -> Result<(FileDeviceObject, String), NtStatus> { + let tail = absolute_path_tail(path)?; + let (node, remaining) = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, true)?; + if node.is_file_device() { + return Ok((node.file_device_object().into_result()?, remaining)); + } + if remaining.is_empty() { + Err(NtStatus::OBJECT_TYPE_MISMATCH) + } else { + Err(unresolved_tail_status( + &remaining, + NtStatus::OBJECT_NAME_NOT_FOUND, + )) + } } fn resolve_object_leaf( &self, path: &str, - open_final_symlink: bool, + follow_final_symlink: bool, lookup: impl FnOnce(&Arc>) -> ObjectLeafLookup, ) -> Result { let tail = absolute_path_tail(path)?; - let node = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, open_final_symlink)?; + let (node, remaining) = + self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, follow_final_symlink)?; + if !remaining.is_empty() { + return Err(unresolved_tail_status( + &remaining, + NtStatus::OBJECT_NAME_NOT_FOUND, + )); + } lookup(&node).into_result() } @@ -603,18 +661,32 @@ impl ObjectManager { ); } + fn seed_file_device(&self, path: &str, device: FileDeviceObject) { + let status = self.create_file_device(path, device); + assert!( + status == NtStatus::SUCCESS, + "seeded NT file device must have seeded ancestors: {status:?}" + ); + } + fn resolve_tail( &self, tail: &str, final_missing_status: NtStatus, - open_final_symlink: bool, - ) -> Result>, NtStatus> { + follow_final_symlink: bool, + ) -> Result<(Arc>, String), NtStatus> { let mut tail = tail.to_string(); for _ in 0..=MAX_SYMLINK_REPARSE_DEPTH { - match self.resolve_tail_once(&tail, final_missing_status, open_final_symlink)? { - TailResolution::Resolved(node) => return Ok(node), - TailResolution::Reparse(next_tail) => tail = next_tail, + let (node, remaining) = match self.resolve_tail_once(&tail) { + Ok(resolution) => resolution, + Err(NtStatus::OBJECT_NAME_NOT_FOUND) => return Err(final_missing_status), + Err(status) => return Err(status), + }; + if node.is_symlink() && (!remaining.is_empty() || follow_final_symlink) { + tail = reparse_tail(&node, &remaining)?; + continue; } + return Ok((node, remaining)); } Err(NtStatus::NAME_TOO_LONG) } @@ -622,11 +694,9 @@ impl ObjectManager { fn resolve_tail_once( &self, tail: &str, - final_missing_status: NtStatus, - open_final_symlink: bool, - ) -> Result, NtStatus> { + ) -> Result<(Arc>, String), NtStatus> { if tail.is_empty() { - return Ok(TailResolution::Resolved(Arc::clone(&self.root))); + return Ok((Arc::clone(&self.root), String::new())); } let mut current = Arc::clone(&self.root); @@ -637,35 +707,46 @@ impl ObjectManager { } let final_component = components.peek().is_none(); let missing_status = if final_component { - final_missing_status + NtStatus::OBJECT_NAME_NOT_FOUND } else { NtStatus::OBJECT_PATH_NOT_FOUND }; let child = current.child(component).ok_or(missing_status)?; - if child.is_symlink() && (!final_component || !open_final_symlink) { - // This is the lazy-resolution point paired with - // NtCreateSymbolicLinkObject storing the target without lookup. - let target = normalize_reparse_target(&child.symlink_target().into_result()?)?; - let target_tail = absolute_path_tail(&target)?; - let remaining = components.collect::>().join("\\"); - let next_tail = if target_tail.is_empty() { - remaining - } else if remaining.is_empty() { - target_tail.to_string() - } else { - alloc::format!("{target_tail}\\{remaining}") - }; - return Ok(TailResolution::Reparse(next_tail)); + if !child.is_directory() { + return Ok((child, components.collect::>().join("\\"))); } current = child; } - Ok(TailResolution::Resolved(current)) + Ok((current, String::new())) } } -enum TailResolution { - Resolved(Arc>), - Reparse(String), +fn reparse_tail( + node: &ObjectNode, + remaining: &str, +) -> Result { + // This is the lazy-resolution point paired with NtCreateSymbolicLinkObject + // storing the target without lookup. + let target = normalize_reparse_target(&node.symlink_target().into_result()?)?; + let target_tail = absolute_path_tail(&target)?; + if target_tail.is_empty() { + Ok(remaining.to_string()) + } else if remaining.is_empty() { + Ok(target_tail.to_string()) + } else { + Ok(alloc::format!("{target_tail}\\{remaining}")) + } +} + +fn unresolved_tail_status(remaining: &str, final_missing_status: NtStatus) -> NtStatus { + debug_assert!(!remaining.is_empty()); + if remaining.split('\\').any(str::is_empty) { + NtStatus::OBJECT_NAME_INVALID + } else if remaining.contains('\\') { + NtStatus::OBJECT_PATH_NOT_FOUND + } else { + final_missing_status + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1239,6 +1320,13 @@ pub(crate) fn seed_object_manager() for path in SEEDED_DIRECTORY_PATHS { object_manager.seed_directory(path); } + object_manager.seed_file_device( + r"\Device\HarddiskVolume1", + FileDeviceObject::Filesystem { + root_path: "/".to_string(), + }, + ); + object_manager.seed_file_device(r"\Device\ConDrv", FileDeviceObject::ConsoleDriver); for (path, target) in SEEDED_SYMLINK_PATHS { object_manager.seed_symlink(path, target); } @@ -1444,7 +1532,7 @@ mod tests { ); let shortcut = object_manager - .resolve_symlink(WINDOWS_SHARED_SECTION_OBJECT, true) + .resolve_symlink(WINDOWS_SHARED_SECTION_OBJECT, false) .expect("Windows shared section shortcut is a symbolic link"); assert_eq!( shortcut.symlink_target().into_result(), @@ -1457,6 +1545,43 @@ mod tests { }); } + #[test] + fn seeded_file_devices_resolve_through_object_manager() { + let object_manager = seed_object_manager::(); + + assert_eq!( + object_manager.resolve_file_device(r"\Device\HarddiskVolume1\Windows"), + Ok(( + FileDeviceObject::Filesystem { + root_path: "/".to_string(), + }, + "Windows".to_string(), + )) + ); + assert_eq!( + object_manager.resolve_file_device(r"\??\C:\Windows\System32"), + Ok(( + FileDeviceObject::Filesystem { + root_path: "/".to_string(), + }, + r"Windows\System32".to_string(), + )) + ); + assert_eq!( + object_manager.resolve_file_device(r"\SystemRoot\System32"), + Ok(( + FileDeviceObject::Filesystem { + root_path: "/".to_string(), + }, + r"Windows\System32".to_string(), + )) + ); + assert_eq!( + object_manager.resolve_file_device(r"\Device\ConDrv\Output"), + Ok((FileDeviceObject::ConsoleDriver, "Output".to_string())) + ); + } + #[test] fn open_directory_rejects_openlink_attribute() { run_with_test_platform_pointers(|| { diff --git a/litebox_shim_windows/src/syscalls/symlink.rs b/litebox_shim_windows/src/syscalls/symlink.rs index 1b7fb4afe5..08aadc6c52 100644 --- a/litebox_shim_windows/src/syscalls/symlink.rs +++ b/litebox_shim_windows/src/syscalls/symlink.rs @@ -210,7 +210,7 @@ impl Task { let link = match self .process .object_manager - .resolve_symlink(&link_name.original_path, true) + .resolve_symlink(&link_name.original_path, false) { Ok(link) => link, Err(status) => return status, @@ -706,9 +706,9 @@ mod tests { let task = test_task(); let real = create_directory(&task, r"\BaseNamedObjects\LiteBoxDriveTarget"); let child = create_directory(&task, r"\BaseNamedObjects\LiteBoxDriveTarget\Child"); - let link = create_link(&task, r"\??\C:", r"\BaseNamedObjects\LiteBoxDriveTarget"); + let link = create_link(&task, r"\??\Z:", r"\BaseNamedObjects\LiteBoxDriveTarget"); - let opened = open_directory(&task, r"\??\C:\Child"); + let opened = open_directory(&task, r"\??\Z:\Child"); assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); assert_eq!(task.sys_nt_close(link), NtStatus::SUCCESS); assert_eq!(task.sys_nt_close(child), NtStatus::SUCCESS); From e730ca99ea9985390cf3057bc78b1e618962a0bf Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Mon, 13 Jul 2026 23:28:33 -0700 Subject: [PATCH 2/2] minor fix --- litebox_shim_windows/src/syscalls/condrv.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/litebox_shim_windows/src/syscalls/condrv.rs b/litebox_shim_windows/src/syscalls/condrv.rs index 2dba4c65be..d56d66eacd 100644 --- a/litebox_shim_windows/src/syscalls/condrv.rs +++ b/litebox_shim_windows/src/syscalls/condrv.rs @@ -138,7 +138,7 @@ pub(crate) fn validate_connect_server_ea( let Some(ea_buffer) = ea_buffer else { return Err(NtStatus::EAS_NOT_SUPPORTED); }; - let ea_length = usize::try_from(ea_length).unwrap(); + let ea_length = ea_length as usize; let Some(entry) = ConstPtr::::from_usize(ea_buffer.as_usize()) .read_at_offset(0) else { @@ -146,8 +146,8 @@ pub(crate) fn validate_connect_server_ea( }; let name_offset = size_of::(); - let name_length = usize::from(entry.ea_name_length); - let value_length = usize::from(entry.ea_value_length); + let name_length = entry.ea_name_length as usize; + let value_length = entry.ea_value_length as usize; let value_offset = name_offset .checked_add(name_length) .and_then(|offset| offset.checked_add(1)) @@ -162,23 +162,23 @@ pub(crate) fn validate_connect_server_ea( let Some(name_address) = ea_buffer.as_usize().checked_add(name_offset) else { return Err(NtStatus::EAS_NOT_SUPPORTED); }; - let Some(name) = ConstPtr::::from_usize(name_address).to_owned_slice(name_length) + let Some(name_with_nul) = + ConstPtr::::from_usize(name_address).to_owned_slice(name_length + 1) else { return Err(NtStatus::ACCESS_VIOLATION); }; - let Some(nul_address) = name_address.checked_add(name_length) else { + let Some((&0, name)) = name_with_nul.split_last() else { return Err(NtStatus::EAS_NOT_SUPPORTED); }; - let Some(nul) = ConstPtr::::from_usize(nul_address).read_at_offset(0) else { - return Err(NtStatus::ACCESS_VIOLATION); - }; - if nul != 0 || !name.eq_ignore_ascii_case(CD_SERVER_EA_NAME) { + if !name.eq_ignore_ascii_case(CD_SERVER_EA_NAME) { return Err(NtStatus::EAS_NOT_SUPPORTED); } let Some(value_address) = ea_buffer.as_usize().checked_add(value_offset) else { return Err(NtStatus::EAS_NOT_SUPPORTED); }; + // The ConDrv "server" EA value format is undocumented; probe the declared payload without + // interpreting it until its semantics are understood. if ConstPtr::::from_usize(value_address) .to_owned_slice(value_length) .is_none()