diff --git a/Cargo.lock b/Cargo.lock index 7004dd036c..a67cf7a956 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1889,6 +1889,7 @@ dependencies = [ "anyhow", "clap", "litebox", + "litebox_broker_local_userland", "litebox_common_linux", "litebox_common_windows", "litebox_platform_linux_userland", @@ -1911,7 +1912,6 @@ dependencies = [ "litebox_shim_windows", "litebox_syscall_rewriter", "litebox_util_log", - "memmap2", "tar", "tracing-subscriber", ] @@ -2003,7 +2003,11 @@ dependencies = [ "bitflags", "int-enum", "litebox", + "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", "litebox_broker_protocol", + "litebox_broker_transport", "litebox_common_linux", "litebox_common_windows", "litebox_platform", @@ -2097,15 +2101,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memmap2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" -dependencies = [ - "libc", -] - [[package]] name = "minimal-lexical" version = "0.2.1" diff --git a/litebox_runner_windows_on_linux_userland/Cargo.toml b/litebox_runner_windows_on_linux_userland/Cargo.toml index f200cc6a2f..967e714f70 100644 --- a/litebox_runner_windows_on_linux_userland/Cargo.toml +++ b/litebox_runner_windows_on_linux_userland/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.97" clap = { version = "4.5.33", features = ["derive"] } litebox = { version = "0.1.0", path = "../litebox" } +litebox_broker_local_userland = { version = "0.1.0", path = "../litebox_broker_local_userland" } litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } litebox_platform_linux_userland = { version = "0.1.0", path = "../litebox_platform_linux_userland" } litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows" } diff --git a/litebox_runner_windows_on_linux_userland/src/lib.rs b/litebox_runner_windows_on_linux_userland/src/lib.rs index 4d214c1ca2..05d3e186f8 100644 --- a/litebox_runner_windows_on_linux_userland/src/lib.rs +++ b/litebox_runner_windows_on_linux_userland/src/lib.rs @@ -9,18 +9,18 @@ extern crate alloc; use anyhow::{Context as _, Result}; use clap::Parser; +use litebox_broker_local_userland as broker; use litebox_platform_linux_userland::LinuxUserland; use std::path::PathBuf; /// Run Windows PE programs with LiteBox on unmodified Linux. /// -/// The program binary and any initial filesystem contents must be provided inside a tar archive via -/// `--initial-files`. The program path refers to a path inside the tar archive. +/// The program binary and runtime files must be available in the broker-owned file system. #[derive(Parser, Debug)] pub struct CliArgs { /// The program and arguments passed to it (e.g., `/app/program.exe --help`). /// - /// The program path refers to a path inside the tar archive provided via `--initial-files`. + /// The program path refers to a path inside the broker-owned file system. #[arg(required = true, trailing_var_arg = true, value_hint = clap::ValueHint::CommandWithArguments)] pub program_and_arguments: Vec, /// Environment variables passed to the program (`K=V` pairs; can be invoked multiple times). @@ -32,18 +32,33 @@ pub struct CliArgs { /// Allow using unstable options. #[arg(short = 'Z', long = "unstable")] pub unstable: bool, - /// Tar archive containing the program and its runtime files. - #[arg(long = "initial-files", value_name = "PATH_TO_TAR", value_hint = clap::ValueHint::FilePath)] - pub initial_files: PathBuf, + /// Broker-supplied Unix socket path for the local control channel. + #[arg( + long = "broker-control-channel", + value_name = "PATH", + value_hint = clap::ValueHint::FilePath, + hide = true, + requires = "unstable", + help_heading = "Unstable Options" + )] + pub broker_control_channel: Option, + /// Broker-supplied proxy URL for managed HTTP and HTTPS egress. + #[arg( + long = "broker-proxy-url", + value_name = "URL", + hide = true, + requires = "broker_control_channel", + help_heading = "Unstable Options" + )] + pub broker_proxy_url: Option, } /// Run Windows PE programs with LiteBox on unmodified Linux. -/// -/// # Panics -/// -/// Panics if the initial in-memory file system fails to create `/tmp` - those -/// operations cannot fail against a freshly-constructed file system. pub fn run(cli_args: CliArgs) -> Result<()> { + if cli_args.broker_proxy_url.is_some() { + anyhow::bail!("managed broker proxy is not supported by this runner"); + } + tracing_subscriber::fmt() .with_timer(tracing_subscriber::fmt::time::uptime()) .with_level(true) @@ -60,37 +75,37 @@ pub fn run(cli_args: CliArgs) -> Result<()> { ); } - let tar_file = &cli_args.initial_files; - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - anyhow::bail!("Expected a .tar file, found {}", tar_file.display()); - } - let tar_data = std::fs::read(tar_file) - .with_context(|| format!("Could not read tar file at {}", tar_file.display()))?; - let platform = LinuxUserland::new(); - let shim_builder = litebox_shim_windows::WindowsShimBuilder::new(platform); + let control_socket = cli_args + .broker_control_channel + .as_deref() + .context("file operations require --broker-control-channel")?; + let broker::BrokerConnection { + local, + notifications, + coordinator, + positional_io_fds: _broker_positional_io_fds, + shutdown_fd: _broker_shutdown_fd, + } = litebox_platform_linux_userland::with_guest_signals_blocked(|| { + broker::connect(control_socket) + })?; + let litebox = litebox::LiteBox::new_with_broker_local(platform, local); + coordinator.install_dispatch(litebox.broker_failure_dispatcher()); + litebox_platform_linux_userland::with_guest_signals_blocked(|| { + broker::start_notification_receiver( + notifications, + coordinator, + litebox.broker_notification_dispatcher(), + ) + })?; + let shim_builder = + litebox_shim_windows::WindowsShimBuilder::new_with_litebox(platform, litebox); let (program_path, program_args) = cli_args .program_and_arguments .split_first() .context("program path missing - clap should have required at least one argument")?; - let initial_file_system = { - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo { - user: 1000, - group: 1000, - }, - }, - )]); - - shim_builder.default_fs(in_mem, tar_data.into()) - }; - let initial_file_system = std::sync::Arc::new(initial_file_system); - let shim = shim_builder.build(); let argv = std::iter::once(program_path.as_str()) .chain(program_args.iter().map(String::as_str)) @@ -113,7 +128,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } let program = shim - .load_program(initial_file_system, program_path, argv, envp) + .load_program(program_path, argv, envp) .context("failed to load Windows PE program")?; // SAFETY: `WindowsShimEntrypoints::init` populates `rip`/`rsp`/`eflags` inside // `run_thread` before the initial guest thread executes, so the `PtRegs::default()` diff --git a/litebox_runner_windows_userland/Cargo.toml b/litebox_runner_windows_userland/Cargo.toml index 49dce8f25a..05a3e888ca 100644 --- a/litebox_runner_windows_userland/Cargo.toml +++ b/litebox_runner_windows_userland/Cargo.toml @@ -12,7 +12,7 @@ litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } litebox_platform_windows_userland = { version = "0.1.0", path = "../litebox_platform_windows_userland" } litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = ["backend_tracing"] } -memmap2 = "0.9.8" + tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } [dev-dependencies] diff --git a/litebox_runner_windows_userland/src/lib.rs b/litebox_runner_windows_userland/src/lib.rs index 1f932600c9..a76b8c35ae 100644 --- a/litebox_runner_windows_userland/src/lib.rs +++ b/litebox_runner_windows_userland/src/lib.rs @@ -11,35 +11,15 @@ use anyhow::{Context as _, Result}; use clap::Parser; use litebox_broker_local_userland as broker; use litebox_platform_windows_userland::{GuestTlsMode, WindowsUserland}; -use memmap2::Mmap; -use std::path::{Path, PathBuf}; - -fn mmapped_file(path: impl AsRef) -> Result<&'static [u8]> { - let path = path.as_ref(); - let file = std::fs::File::open(path) - .with_context(|| format!("Could not open tar file at {}", path.display()))?; - let data = { - // SAFETY: The runner maps the input read-only and does not modify it. The caller must ensure - // the tar file is not modified externally while the guest is running. - // - // Leak the mapping so the borrowed tar data remains valid for the process-lifetime file - // system. - Box::leak(Box::new(unsafe { Mmap::map(&file) }.with_context( - || format!("Could not map tar file at {}", path.display()), - )?)) - }; - Ok(data) -} /// Runs a Windows PE program with LiteBox on unmodified Windows and returns its exit code. /// -/// The program binary and any initial filesystem contents must be provided inside a tar archive via -/// `--initial-files`. The program path refers to a path inside the tar archive. +/// The program binary and runtime files must be available in the broker-owned file system. #[derive(Parser, Debug)] pub struct CliArgs { /// The program and arguments passed to it (e.g., `/app/program.exe --help`). /// - /// The program path refers to a path inside the tar archive provided via `--initial-files`. + /// The program path refers to a path inside the broker-owned file system. #[arg(required = true, trailing_var_arg = true, value_hint = clap::ValueHint::CommandWithArguments)] pub program_and_arguments: Vec, /// Environment variables passed to the program (`K=V` pairs; can be invoked multiple times). @@ -60,17 +40,9 @@ pub struct CliArgs { help_heading = "Unstable Options" )] pub broker_control_channel: Option, - /// Tar archive containing the program and its runtime files. - #[arg(long = "initial-files", value_name = "PATH_TO_TAR", value_hint = clap::ValueHint::FilePath)] - pub initial_files: PathBuf, } /// Run Windows PE programs with LiteBox on unmodified Windows. -/// -/// # Panics -/// -/// Panics if the initial in-memory file system fails to create `/tmp` — those -/// operations cannot fail against a freshly-constructed file system. pub fn run(cli_args: CliArgs) -> Result { tracing_subscriber::fmt() .with_timer(tracing_subscriber::fmt::time::uptime()) @@ -82,56 +54,30 @@ pub fn run(cli_args: CliArgs) -> Result { ) .init(); - let tar_file = &cli_args.initial_files; - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - anyhow::bail!("Expected a .tar file, found {}", tar_file.display()); - } - let tar_data = mmapped_file(tar_file)?; - let platform = WindowsUserland::new(); WindowsUserland::set_guest_tls_mode(GuestTlsMode::Windows); - let broker_connection = cli_args + let control_pipe = cli_args .broker_control_channel .as_deref() - .map(broker::connect) - .transpose()?; - let shim_builder = if let Some(broker_connection) = broker_connection { - let broker::BrokerConnection { - local, - notifications, - } = broker_connection; - let litebox = litebox::LiteBox::new_with_broker_local(platform, local); - broker::start_notification_receiver( - notifications, - litebox.broker_notification_dispatcher(), - litebox.broker_failure_dispatcher(), - )?; - litebox_shim_windows::WindowsShimBuilder::new_with_litebox(platform, litebox) - } else { - litebox_shim_windows::WindowsShimBuilder::new(platform) - }; + .context("file operations require --broker-control-channel")?; + let broker::BrokerConnection { + local, + notifications, + } = broker::connect(control_pipe)?; + let litebox = litebox::LiteBox::new_with_broker_local(platform, local); + broker::start_notification_receiver( + notifications, + litebox.broker_notification_dispatcher(), + litebox.broker_failure_dispatcher(), + )?; + let shim_builder = + litebox_shim_windows::WindowsShimBuilder::new_with_litebox(platform, litebox); let (program_path, program_args) = cli_args .program_and_arguments .split_first() .context("program path missing — clap should have required at least one argument")?; - let initial_file_system = { - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo { - user: 1000, - group: 1000, - }, - }, - )]); - - shim_builder.default_fs(in_mem, tar_data.into()) - }; - let initial_file_system = std::sync::Arc::new(initial_file_system); - let shim = shim_builder.build(); let argv = std::iter::once(program_path.as_str()) .chain(program_args.iter().map(String::as_str)) @@ -154,7 +100,7 @@ pub fn run(cli_args: CliArgs) -> Result { } let program = shim - .load_program(initial_file_system, program_path, argv, envp) + .load_program(program_path, argv, envp) .context("failed to load Windows PE program")?; // SAFETY: `WindowsShimEntrypoints::init` populates `rip`/`rsp`/`eflags` inside // `run_thread` before the initial guest thread executes, so the `PtRegs::default()` diff --git a/litebox_runner_windows_userland/tests/run.rs b/litebox_runner_windows_userland/tests/run.rs index fba7099469..a99f796b45 100644 --- a/litebox_runner_windows_userland/tests/run.rs +++ b/litebox_runner_windows_userland/tests/run.rs @@ -22,9 +22,16 @@ fn run_hello_world_pe() { let (broker, runner) = build_windows_broker(); let mut separate_process = std::process::Command::new(&broker); - separate_process.arg("--runner").arg(runner); + separate_process + .arg("--fs-initial-files") + .arg(&tar_path) + .arg("--runner") + .arg(runner); let mut in_process = std::process::Command::new(broker); - in_process.args(["--unstable", "--in-process-runner"]); + in_process + .arg("--fs-initial-files") + .arg(&tar_path) + .args(["--unstable", "--in-process-runner"]); for (mode, mut command) in [ ("separate-process", separate_process), @@ -32,11 +39,7 @@ fn run_hello_world_pe() { ] { // Verbose log for failure triage; not load-bearing for any assertion. command.env("LITEBOX_LOG", "debug"); - command.args([ - "--initial-files", - tar_path.to_str().unwrap(), - "/kernel32_import.exe", - ]); + command.arg("/kernel32_import.exe"); println!("Running {mode} `{command:?}`"); let output = command .output() @@ -80,13 +83,9 @@ fn run_multithreaded_pe() { std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("kernel32_multithread.tar"); create_tar_with_dir(&test_dir, &tar_path); - let mut command = brokered_windows_runner_command(); + let mut command = brokered_windows_runner_command(&tar_path); command.env("LITEBOX_LOG", "debug"); - command.args([ - "--initial-files", - tar_path.to_str().unwrap(), - "/kernel32_multithread.exe", - ]); + command.arg("/kernel32_multithread.exe"); println!("Running `{command:?}`"); let output = command .output() @@ -131,13 +130,9 @@ fn run_crt_locale_pe() { let tar_path = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("crt_locale.tar"); create_tar_with_dir(&test_dir, &tar_path); - let mut command = brokered_windows_runner_command(); + let mut command = brokered_windows_runner_command(&tar_path); command.env("LITEBOX_LOG", "debug"); - command.args([ - "--initial-files", - tar_path.to_str().unwrap(), - "/crt_locale.exe", - ]); + command.arg("/crt_locale.exe"); println!("Running `{command:?}`"); let output = command .output() @@ -185,10 +180,14 @@ fn build_windows_broker() -> (std::path::PathBuf, std::path::PathBuf) { (broker, runner) } -fn brokered_windows_runner_command() -> std::process::Command { +fn brokered_windows_runner_command(initial_files: &std::path::Path) -> std::process::Command { let (broker, runner) = build_windows_broker(); let mut command = std::process::Command::new(broker); - command.arg("--runner").arg(runner); + command + .arg("--fs-initial-files") + .arg(initial_files) + .arg("--runner") + .arg(runner); command } diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml index 106c693c49..2d1faadd9f 100644 --- a/litebox_shim_windows/Cargo.toml +++ b/litebox_shim_windows/Cargo.toml @@ -21,9 +21,17 @@ thiserror = { version = "2.0.6", default-features = false } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } [target.'cfg(target_os = "linux")'.dev-dependencies] +litebox_broker_core = { path = "../litebox_broker_core/", version = "0.1.0", features = ["test-support"] } +litebox_broker_host = { path = "../litebox_broker_host/", version = "0.1.0", features = ["test-support"] } +litebox_broker_local = { path = "../litebox_broker_local/", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport/", version = "0.1.0" } litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland/", version = "0.1.0" } [target.'cfg(target_os = "windows")'.dev-dependencies] +litebox_broker_core = { path = "../litebox_broker_core/", version = "0.1.0", features = ["test-support"] } +litebox_broker_host = { path = "../litebox_broker_host/", version = "0.1.0", features = ["test-support"] } +litebox_broker_local = { path = "../litebox_broker_local/", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport/", version = "0.1.0" } litebox_platform_windows_userland = { path = "../litebox_platform_windows_userland/", version = "0.1.0" } [lints] diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 0037118878..ea0a456c1e 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -11,7 +11,6 @@ extern crate alloc; -use alloc::borrow::Cow; use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; @@ -33,16 +32,14 @@ use litebox_common_windows::{NtSysno, Win32Sysno}; use litebox_platform::time::TimeProvider; use crate::syscalls::event::{EventHandleObject, EventSubsystem}; -use crate::syscalls::file::{FileObject, FileObjectSubsystem}; +use crate::syscalls::file::FileObject; use crate::syscalls::iocp::{IoCompletionHandleObject, IoCompletionSubsystem}; use crate::syscalls::lpc::{LpcPortHandleObject, LpcPortSubsystem}; use crate::syscalls::mutant::{MutantHandleObject, MutantSubsystem}; use crate::syscalls::object_manager::{ DirectoryHandleObject, DirectoryObjectSubsystem, ObjectManager, }; -use crate::syscalls::registry::{ - NtNotifyChangeKeyRequest, RegistryKeyObject, RegistryKeySubsystem, -}; +use crate::syscalls::registry::{NtNotifyChangeKeyRequest, RegistryKeyObject}; use crate::syscalls::section::{ MapViewOfSectionParameters, SectionHandleObject, SectionObject, SectionSubsystem, }; @@ -60,14 +57,14 @@ use crate::syscalls::worker_factory::{ }; use crate::syscalls::{SyscallRequest, ThreadHandle, mm}; +mod fs; mod loader; mod nt_types; mod syscalls; mod wait; -#[allow(dead_code)] -mod fs; - +#[cfg(test)] +mod test_broker; #[cfg(test)] mod tests; @@ -218,11 +215,6 @@ impl Clone for WindowsSectionView { } } -pub type DefaultFS = WindowsFS; - -pub type WindowsFS = - litebox::fs::resolver::Resolver; - fn write_value(address: usize, value: T) -> Option<()> where Platform: RawPointerProvider, @@ -405,11 +397,6 @@ pub struct WindowsShimBuilder { } impl WindowsShimBuilder { - #[must_use] - pub fn new(platform: &'static Platform) -> Self { - Self::new_with_litebox(platform, LiteBox::new(platform)) - } - /// Creates a builder backed by an existing LiteBox instance. #[must_use] pub fn new_with_litebox(platform: &'static Platform, litebox: LiteBox) -> Self { @@ -421,28 +408,23 @@ impl WindowsShimBuilder { &self.litebox } - /// Build the default file system with the given in-memory layer and tar data. - #[must_use] - pub fn default_fs( - &self, - in_mem: litebox::fs::in_mem::InMem, - tar_data: Cow<'static, [u8]>, - ) -> DefaultFS { - default_fs(&self.litebox, in_mem, tar_data) - } - #[must_use] pub fn build(self) -> WindowsShim { + let litebox = Arc::new(self.litebox); + let fs = Arc::new(fs::Fs::regular(Arc::clone(&litebox))); let global = Arc::new(GlobalState { platform: self.platform, - page_manager: PageManager::new(&self.litebox), - registry: syscalls::registry::RegistryStore::new(&self.litebox), + page_manager: PageManager::new(&litebox), + registry: syscalls::registry::RegistryStore::new(fs::Fs::registry(Arc::clone( + &litebox, + ))), wnf_states: syscalls::wnf::WnfStateStore::new( syscalls::wnf::WnfStateStoreData::default(), ), mui_generation: AtomicU32::new(1), qpc_boot_instant: TimeProvider::now(self.platform), - litebox: self.litebox, + fs, + litebox, }); WindowsShim(global) } @@ -509,7 +491,6 @@ impl WindowsShim { /// Loads the program at `path` as the shim's initial task. pub fn load_program( &self, - fs: Arc>, path: &str, argv: Vec, envp: Vec, @@ -518,6 +499,7 @@ impl WindowsShim { #[cfg(not(target_os = "windows"))] let _ = map_windows_user_shared_data::(&self.0.page_manager) .ok_or(loader::WindowsLoadError::MapSharedMemory)?; + let fs = Arc::clone(&self.0.fs); let load_info = loader::PeLoader::new(self.0.platform, fs.clone(), &self.0.page_manager) .load(path, &argv, &envp)?; // TODO: shared section should be only created once and shared across all processes, not created per-process. @@ -541,7 +523,7 @@ impl WindowsShim { global: self.0.clone(), process: process.clone(), fs, - fs_context: litebox::fs::resolver::Context::new(), + fs_context: litebox::fs::Context::new(), wait_state: wait::WaitState::new(self.0.platform), io_completion_worker: Mutex::new(syscalls::iocp::IoCompletionWorkerState::new()), entry_point: load_info.entry_point, @@ -564,7 +546,8 @@ struct GlobalState { wnf_states: syscalls::wnf::WnfStateStore, mui_generation: AtomicU32, qpc_boot_instant: ::Instant, - litebox: LiteBox, + fs: Arc>, + litebox: Arc>, } /// Per-process Windows state shared by every thread in the process. @@ -730,8 +713,8 @@ impl Process { struct Task { global: Arc>, process: Arc>, - fs: Arc>, - fs_context: litebox::fs::resolver::Context, + fs: Arc>, + fs_context: litebox::fs::Context, wait_state: wait::WaitState, io_completion_worker: Mutex>, entry_point: usize, @@ -2517,8 +2500,8 @@ impl Task { }; } - try_metadata!(FileObjectSubsystem); - try_metadata!(RegistryKeySubsystem); + try_metadata!(FileObject); + try_metadata!(RegistryKeyObject); try_metadata!(EventSubsystem); try_metadata!(MutantSubsystem); try_metadata!(SemaphoreSubsystem); @@ -2567,8 +2550,8 @@ impl Task { }; } - try_set_attributes!(FileObjectSubsystem); - try_set_attributes!(RegistryKeySubsystem); + try_set_attributes!(FileObject); + try_set_attributes!(RegistryKeyObject); try_set_attributes!(EventSubsystem); try_set_attributes!(MutantSubsystem); try_set_attributes!(SemaphoreSubsystem); @@ -2739,8 +2722,8 @@ impl Task { }; } - try_duplicate!(FileObjectSubsystem); - try_duplicate!(RegistryKeySubsystem); + try_duplicate!(FileObject); + try_duplicate!(RegistryKeyObject); try_duplicate!(EventSubsystem); try_duplicate!(MutantSubsystem); try_duplicate!(SemaphoreSubsystem); @@ -2850,8 +2833,8 @@ impl Task { }; } - try_close!(FileObjectSubsystem, file); - try_close!(RegistryKeySubsystem, registry_key); + try_close!(FileObject, file); + try_close!(RegistryKeyObject, registry_key); try_close!(EventSubsystem, event); try_close!(MutantSubsystem, mutant); try_close!(SemaphoreSubsystem, semaphore); @@ -3016,9 +2999,9 @@ fn is_api_set_contract(dll_name: &str) -> bool { } trait RawHandleVisitor { - fn file(&self, file: FileObject); + fn file(&self, file: FileObject); - fn registry_key(&self, key: RegistryKeyObject); + fn registry_key(&self, key: RegistryKeyObject); fn event(&self, event: EventHandleObject); @@ -3055,11 +3038,11 @@ struct CloseRawHandleVisitor<'task, Platform: ShimPlatform> { } impl RawHandleVisitor for CloseRawHandleVisitor<'_, Platform> { - fn file(&self, file: FileObject) { + fn file(&self, file: FileObject) { self.task.close_file(file); } - fn registry_key(&self, key: RegistryKeyObject) { + fn registry_key(&self, key: RegistryKeyObject) { self.task.close_registry_key(key); } @@ -3186,27 +3169,3 @@ pub struct LoadedProgram { /// Handle used to wait for the loaded program to exit. pub process: Arc>, } - -fn default_fs( - litebox: &LiteBox, - in_mem: litebox::fs::in_mem::InMem, - tar_data: Cow<'static, [u8]>, -) -> WindowsFS -where - Platform: ShimPlatform, -{ - litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox::fs::overlay::Overlay::::new( - in_mem, - litebox::fs::tar_ro::TarRo::new(tar_data, allocators.next()), - allocators.next(), - ) - }) - .mount("/dev", litebox::fs::devices::Devices::new) - .build() - .unwrap(), - ) -} diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index cff5165f54..c6fde44fd6 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -10,12 +10,12 @@ use core::{ use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt as _; use litebox::{ - fs::{Mode, OFlags}, mm::linux::{ CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, VmemProtectError, }, platform::RawPointerProvider, }; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags}; use litebox_common_windows::loader::{ AccessMemory, Fault, KiUserInvertedFunctionTableEntry, KiUserInvertedFunctionTableHeader, MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, MapMemory, MappingInfo, PAGE_SIZE, PeExportError, @@ -102,14 +102,14 @@ pub(crate) struct WindowsThreadEnvironment { pub(crate) struct PeLoader<'a, Platform: crate::ShimPlatform> { platform: &'static Platform, - fs: Arc>, + fs: Arc>, page_manager: &'a crate::WindowsPageManager, } impl<'a, Platform: crate::ShimPlatform> PeLoader<'a, Platform> { pub(crate) fn new( platform: &'static Platform, - fs: Arc>, + fs: Arc>, page_manager: &'a crate::WindowsPageManager, ) -> Self { Self { @@ -139,13 +139,16 @@ impl<'a, Platform: crate::ShimPlatform> PeLoader<'a, Platform> { application_entry_point }; - let environment = self.create_process_environment(ProcessEnvironmentInput { - image: &image.parsed, - image_base_address: image.mapping.base_addr, - image_path: path, - argv, - envp, - })?; + let environment = create_process_environment( + self.page_manager, + ProcessEnvironmentInput { + image: &image.parsed, + image_base_address: image.mapping.base_addr, + image_path: path, + argv, + envp, + }, + )?; if let Some(ntdll) = &ntdll { let context = X64Context::initial_thread_context( ntdll.exports.rtl_user_thread_start, @@ -213,184 +216,180 @@ impl<'a, Platform: crate::ShimPlatform> PeLoader<'a, Platform> { Ok(()) } +} - fn create_process_environment( - &self, - input: ProcessEnvironmentInput<'_>, - ) -> Result { - let create_pages = |size: usize| -> Result { - let aligned_length = size.next_multiple_of(PAGE_SIZE); - let length = - NonZeroPageSize::new(aligned_length).ok_or(PeImageAccessError::AddressOverflow)?; - // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` leaves address - // selection to the page manager, so this cannot replace an existing mapping. - let ptr = unsafe { - self.page_manager.create_writable_pages( - None, - length, - CreatePagesFlags::empty(), - |_| Ok(0), - ) - }?; - Ok(ptr.as_usize()) - }; - let peb_ptr = create_pages(size_of::())?; - let api_set_map = API_SET_NAMESPACE; - let api_set_map_ptr = create_pages(api_set_map.len())?; - write_guest_slice::(api_set_map_ptr, api_set_map)?; - let win32_image_path = win32_image_path(input.image_path); - let dos_image_path = dos_image_path(input.image_path); - let current_directory_path = Utf16StringBuffer::new(r"C:\")?; - let dll_path = Utf16StringBuffer::new(r"C:\Windows\System32;C:\")?; - let image_path_name = Utf16StringBuffer::new(&dos_image_path)?; - let command_line = - Utf16StringBuffer::new(&windows_command_line(&win32_image_path, input.argv))?; - let window_title = Utf16StringBuffer::new(&dos_image_path)?; - let desktop_info = Utf16StringBuffer::new("")?; - let shell_info = Utf16StringBuffer::new("")?; - let runtime_data = Utf16StringBuffer::new("")?; - let redirection_dll_name = Utf16StringBuffer::new("")?; - let environment_block = windows_environment_block(input.envp); - let environment_size = checked_mul(environment_block.len(), size_of::())?; - let environment_ptr = create_pages(environment_size)?; - write_guest_slice::(environment_ptr, &environment_block)?; - let process_parameter_strings = [ - ¤t_directory_path, - &dll_path, - &image_path_name, - &command_line, - &window_title, - &desktop_info, - &shell_info, - &runtime_data, - &redirection_dll_name, - ]; - let process_parameters_length = process_parameter_strings.iter().try_fold( - size_of::(), - |length, string| { - length - .checked_add(usize::from(string.maximum_length)) - .ok_or(PeImageAccessError::AddressOverflow) - }, - )?; - let process_parameters_allocation_length = - process_parameters_length.next_multiple_of(PAGE_SIZE); - let process_parameters_ptr = create_pages(process_parameters_length)?; - - let mut process_parameters = RtlUserProcessParameters::new_zeroed(); - process_parameters.maximum_length = to_u32(process_parameters_allocation_length)?; - process_parameters.length = to_u32(process_parameters_length)?; - process_parameters.flags = RtlUserProcFlags::NORMALIZED.bits(); - process_parameters.environment = environment_ptr; - process_parameters.environment_size = - u64::try_from(environment_size).map_err(|_| PeImageAccessError::AddressOverflow)?; - let mut process_parameters_allocation = - GuestMemoryAllocator::new(process_parameters_ptr, process_parameters_length)?; - let guest_process_parameters = - process_parameters_allocation.allocate::()?; - process_parameters.current_directory.dos_path = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - ¤t_directory_path, - )?; - process_parameters.dll_path = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &dll_path, - )?; - process_parameters.image_path_name = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &image_path_name, - )?; - process_parameters.command_line = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &command_line, - )?; - process_parameters.window_title = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &window_title, - )?; - process_parameters.desktop_info = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &desktop_info, - )?; - process_parameters.shell_info = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &shell_info, - )?; - process_parameters.runtime_data = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &runtime_data, - )?; - process_parameters.redirection_dll_name = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &redirection_dll_name, - )?; - guest_process_parameters - .write_at_offset(0, process_parameters) - .ok_or(PeImageAccessError::MemoryAccess)?; - - let read_only_shared_memory_base = create_pages(WINDOWS_SHARED_SECTION_SIZE)?; - let mut shared_heap = - GuestMemoryAllocator::new(read_only_shared_memory_base, WINDOWS_SHARED_SECTION_SIZE)?; - let read_only_static_server_data = - initialize_windows_static_server_data::(&mut shared_heap)?; - let mut peb = ProcessEnvironmentBlock::new_zeroed(); - peb.image_base_address = input.image_base_address; - if input.image_base_address != input.image.image_base() || input.image.has_dynamic_base() { - peb.bit_field = PebBitField::IS_IMAGE_DYNAMICALLY_RELOCATED.bits(); - } - let process_heaps = initial_process_heaps_array(peb_ptr)?; - let fast_peb_lock = create_pages(size_of::())?; - write_guest_value::(fast_peb_lock, RtlCriticalSection::initialized(0))?; - let loader_lock = create_pages(size_of::())?; - write_guest_value::(loader_lock, RtlCriticalSection::initialized(0))?; - - peb.api_set_map = api_set_map_ptr; - peb.process_parameters = process_parameters_ptr; - peb.fast_peb_lock = fast_peb_lock; - peb.shared_data = read_only_shared_memory_base; - peb.number_of_processors = 1; - peb.critical_section_timeout = WINDOWS_CRITICAL_SECTION_TIMEOUT_100NS; - peb.heap_segment_reserve = WINDOWS_HEAP_SEGMENT_RESERVE; - peb.heap_segment_commit = WINDOWS_HEAP_SEGMENT_COMMIT; - peb.heap_de_commit_total_free_threshold = WINDOWS_HEAP_DECOMMIT_TOTAL_FREE_THRESHOLD; - peb.heap_de_commit_free_block_threshold = WINDOWS_HEAP_DECOMMIT_FREE_BLOCK_THRESHOLD; - peb.maximum_number_of_heaps = process_heaps.maximum_number_of_heaps; - peb.process_heaps = process_heaps.address; - peb.loader_lock = loader_lock; - peb.active_process_affinity_mask = 1; - peb.os_major_version = u32::from(crate::syscalls::sysinfo::WINDOWS_OS_MAJOR_VERSION); - peb.os_minor_version = u32::from(crate::syscalls::sysinfo::WINDOWS_OS_MINOR_VERSION); - peb.os_build_number = crate::syscalls::sysinfo::WINDOWS_OS_BUILD_NUMBER; - peb.os_platform_id = crate::syscalls::sysinfo::WINDOWS_OS_PLATFORM_WIN32_NT; - peb.image_subsystem = u32::from(input.image.subsystem()); - peb.image_subsystem_major_version = u32::from(input.image.major_subsystem_version()); - peb.image_subsystem_minor_version = u32::from(input.image.minor_subsystem_version()); - peb.read_only_shared_memory_base = read_only_shared_memory_base; - peb.read_only_static_server_data = read_only_static_server_data; - // TODO(csr-shared-section): model shared backing with distinct client and CSRSS - // virtual addresses instead of aliasing both PEB bases to this single mapping. - peb.csr_server_read_only_shared_memory_base = read_only_shared_memory_base as u64; - - write_guest_value::(peb_ptr, peb)?; - - let thread = create_thread_environment( - self.page_manager, - INITIAL_STACK_SIZE, - peb_ptr, - ClientId { - unique_process: INITIAL_PROCESS_ID, - unique_thread: INITIAL_THREAD_ID, - }, - true, - )?; - Ok(WindowsProcessEnvironment { - peb: peb_ptr, - teb: thread.teb, - context: thread.context, - stack_top: thread.stack_top, - windows_shared_section: read_only_shared_memory_base, - }) - } +/// Builds the synthetic Windows process environment (PEB, TEB, process parameters, and +/// initial thread state) for an image that has already been mapped. +/// +/// This only writes guest memory through `page_manager`, so it is independent of how the +/// image was obtained and needs no file system. +fn create_process_environment( + page_manager: &crate::WindowsPageManager, + input: ProcessEnvironmentInput<'_>, +) -> Result { + let create_pages = |size: usize| -> Result { + let aligned_length = size.next_multiple_of(PAGE_SIZE); + let length = + NonZeroPageSize::new(aligned_length).ok_or(PeImageAccessError::AddressOverflow)?; + // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` leaves address + // selection to the page manager, so this cannot replace an existing mapping. + let ptr = unsafe { + page_manager.create_writable_pages(None, length, CreatePagesFlags::empty(), |_| Ok(0)) + }?; + Ok(ptr.as_usize()) + }; + let peb_ptr = create_pages(size_of::())?; + let api_set_map = API_SET_NAMESPACE; + let api_set_map_ptr = create_pages(api_set_map.len())?; + write_guest_slice::(api_set_map_ptr, api_set_map)?; + let win32_image_path = win32_image_path(input.image_path); + let dos_image_path = dos_image_path(input.image_path); + let current_directory_path = Utf16StringBuffer::new(r"C:\")?; + let dll_path = Utf16StringBuffer::new(r"C:\Windows\System32;C:\")?; + let image_path_name = Utf16StringBuffer::new(&dos_image_path)?; + let command_line = + Utf16StringBuffer::new(&windows_command_line(&win32_image_path, input.argv))?; + let window_title = Utf16StringBuffer::new(&dos_image_path)?; + let desktop_info = Utf16StringBuffer::new("")?; + let shell_info = Utf16StringBuffer::new("")?; + let runtime_data = Utf16StringBuffer::new("")?; + let redirection_dll_name = Utf16StringBuffer::new("")?; + let environment_block = windows_environment_block(input.envp); + let environment_size = checked_mul(environment_block.len(), size_of::())?; + let environment_ptr = create_pages(environment_size)?; + write_guest_slice::(environment_ptr, &environment_block)?; + let process_parameter_strings = [ + ¤t_directory_path, + &dll_path, + &image_path_name, + &command_line, + &window_title, + &desktop_info, + &shell_info, + &runtime_data, + &redirection_dll_name, + ]; + let process_parameters_length = process_parameter_strings.iter().try_fold( + size_of::(), + |length, string| { + length + .checked_add(usize::from(string.maximum_length)) + .ok_or(PeImageAccessError::AddressOverflow) + }, + )?; + let process_parameters_allocation_length = + process_parameters_length.next_multiple_of(PAGE_SIZE); + let process_parameters_ptr = create_pages(process_parameters_length)?; + + let mut process_parameters = RtlUserProcessParameters::new_zeroed(); + process_parameters.maximum_length = to_u32(process_parameters_allocation_length)?; + process_parameters.length = to_u32(process_parameters_length)?; + process_parameters.flags = RtlUserProcFlags::NORMALIZED.bits(); + process_parameters.environment = environment_ptr; + process_parameters.environment_size = + u64::try_from(environment_size).map_err(|_| PeImageAccessError::AddressOverflow)?; + let mut process_parameters_allocation = + GuestMemoryAllocator::new(process_parameters_ptr, process_parameters_length)?; + let guest_process_parameters = + process_parameters_allocation.allocate::()?; + process_parameters.current_directory.dos_path = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + ¤t_directory_path, + )?; + process_parameters.dll_path = + allocate_guest_unicode_string::(&mut process_parameters_allocation, &dll_path)?; + process_parameters.image_path_name = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &image_path_name, + )?; + process_parameters.command_line = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &command_line, + )?; + process_parameters.window_title = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &window_title, + )?; + process_parameters.desktop_info = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &desktop_info, + )?; + process_parameters.shell_info = + allocate_guest_unicode_string::(&mut process_parameters_allocation, &shell_info)?; + process_parameters.runtime_data = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &runtime_data, + )?; + process_parameters.redirection_dll_name = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &redirection_dll_name, + )?; + guest_process_parameters + .write_at_offset(0, process_parameters) + .ok_or(PeImageAccessError::MemoryAccess)?; + + let read_only_shared_memory_base = create_pages(WINDOWS_SHARED_SECTION_SIZE)?; + let mut shared_heap = + GuestMemoryAllocator::new(read_only_shared_memory_base, WINDOWS_SHARED_SECTION_SIZE)?; + let read_only_static_server_data = + initialize_windows_static_server_data::(&mut shared_heap)?; + let mut peb = ProcessEnvironmentBlock::new_zeroed(); + peb.image_base_address = input.image_base_address; + if input.image_base_address != input.image.image_base() || input.image.has_dynamic_base() { + peb.bit_field = PebBitField::IS_IMAGE_DYNAMICALLY_RELOCATED.bits(); + } + let process_heaps = initial_process_heaps_array(peb_ptr)?; + let fast_peb_lock = create_pages(size_of::())?; + write_guest_value::(fast_peb_lock, RtlCriticalSection::initialized(0))?; + let loader_lock = create_pages(size_of::())?; + write_guest_value::(loader_lock, RtlCriticalSection::initialized(0))?; + + peb.api_set_map = api_set_map_ptr; + peb.process_parameters = process_parameters_ptr; + peb.fast_peb_lock = fast_peb_lock; + peb.shared_data = read_only_shared_memory_base; + peb.number_of_processors = 1; + peb.critical_section_timeout = WINDOWS_CRITICAL_SECTION_TIMEOUT_100NS; + peb.heap_segment_reserve = WINDOWS_HEAP_SEGMENT_RESERVE; + peb.heap_segment_commit = WINDOWS_HEAP_SEGMENT_COMMIT; + peb.heap_de_commit_total_free_threshold = WINDOWS_HEAP_DECOMMIT_TOTAL_FREE_THRESHOLD; + peb.heap_de_commit_free_block_threshold = WINDOWS_HEAP_DECOMMIT_FREE_BLOCK_THRESHOLD; + peb.maximum_number_of_heaps = process_heaps.maximum_number_of_heaps; + peb.process_heaps = process_heaps.address; + peb.loader_lock = loader_lock; + peb.active_process_affinity_mask = 1; + peb.os_major_version = u32::from(crate::syscalls::sysinfo::WINDOWS_OS_MAJOR_VERSION); + peb.os_minor_version = u32::from(crate::syscalls::sysinfo::WINDOWS_OS_MINOR_VERSION); + peb.os_build_number = crate::syscalls::sysinfo::WINDOWS_OS_BUILD_NUMBER; + peb.os_platform_id = crate::syscalls::sysinfo::WINDOWS_OS_PLATFORM_WIN32_NT; + peb.image_subsystem = u32::from(input.image.subsystem()); + peb.image_subsystem_major_version = u32::from(input.image.major_subsystem_version()); + peb.image_subsystem_minor_version = u32::from(input.image.minor_subsystem_version()); + peb.read_only_shared_memory_base = read_only_shared_memory_base; + peb.read_only_static_server_data = read_only_static_server_data; + // TODO(csr-shared-section): model shared backing with distinct client and CSRSS + // virtual addresses instead of aliasing both PEB bases to this single mapping. + peb.csr_server_read_only_shared_memory_base = read_only_shared_memory_base as u64; + + write_guest_value::(peb_ptr, peb)?; + + let thread = create_thread_environment( + page_manager, + INITIAL_STACK_SIZE, + peb_ptr, + ClientId { + unique_process: INITIAL_PROCESS_ID, + unique_thread: INITIAL_THREAD_ID, + }, + true, + )?; + Ok(WindowsProcessEnvironment { + peb: peb_ptr, + teb: thread.teb, + context: thread.context, + stack_top: thread.stack_top, + windows_shared_section: read_only_shared_memory_base, + }) } pub(crate) fn create_thread_environment( @@ -912,7 +911,7 @@ struct NtDllExports { fn load_ntdll( platform: &'static Platform, - fs: Arc>, + fs: Arc>, page_manager: &crate::WindowsPageManager, ) -> Result, WindowsLoadError> { match load_image_with_writable_sections( @@ -937,7 +936,7 @@ fn load_ntdll( fn load_image( platform: &'static Platform, - fs: Arc>, + fs: Arc>, path: &str, page_manager: &crate::WindowsPageManager, ) -> Result { @@ -946,7 +945,7 @@ fn load_image( pub(crate) fn load_image_section( platform: &'static Platform, - fs: Arc>, + fs: Arc>, path: &str, page_manager: &crate::WindowsPageManager, virtual_allocations: &crate::WindowsVirtualAllocations, @@ -959,7 +958,7 @@ pub(crate) fn load_image_section( pub(crate) struct ImageSectionMetadata { pub(crate) transfer_address: usize, - pub(crate) file_size: u32, + pub(crate) file_size: u64, pub(crate) subsystem: u32, pub(crate) subsystem_major_version: u16, pub(crate) subsystem_minor_version: u16, @@ -969,18 +968,16 @@ pub(crate) struct ImageSectionMetadata { } pub(crate) fn image_section_metadata( - fs: Arc>, + 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) + .file_status(&file.fd) .map_err(PeImageAccessError::FileStatus)? - .size - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow)?; + .size; Ok(ImageSectionMetadata { transfer_address: parsed .image_base() @@ -997,7 +994,7 @@ pub(crate) fn image_section_metadata( } fn load_image_with_writable_sections( - fs: Arc>, + fs: Arc>, path: &str, platform: &'static Platform, page_manager: &crate::WindowsPageManager, @@ -1108,16 +1105,17 @@ fn is_missing_file_error(error: &WindowsLoadError) -> bool { } struct PeImageFile { - fs: Arc>, - fd: litebox::fd::TypedFd>, + fs: Arc>, + fd: litebox::fs::FileFd, } impl PeImageFile { - fn open(fs: Arc>, path: &str) -> Result { - let fd = fs.open( - &litebox::fs::resolver::Context::new(), + fn open(fs: Arc>, path: &str) -> Result { + let fd = fs.open_file( + &litebox::fs::Context::new(), path, - OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), )?; Ok(Self { fs, fd }) @@ -1129,7 +1127,7 @@ impl PeImageFile { mut buf: &mut [u8], ) -> Result<(), PeImageAccessError> { while !buf.is_empty() { - let bytes_read = self.fs.read(&self.fd, buf, Some(offset))?; + let bytes_read = self.fs.read_file(&self.fd, buf, Some(offset))?; if bytes_read == 0 { return Err(PeImageAccessError::ShortRead); } @@ -1144,7 +1142,7 @@ impl PeImageFile { impl Drop for PeImageFile { fn drop(&mut self) { - if let Err(e) = self.fs.close(&self.fd) { + if let Err(e) = self.fs.close_file(&self.fd) { litebox_util_log::warn!(error:? = e; "failed to close PE image file"); } } @@ -1163,11 +1161,7 @@ impl ReadAt for &'_ PeImageFile { } fn size(&mut self) -> Result { - self.fs - .fd_file_status(&self.fd)? - .size - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow) + Ok(self.fs.file_status(&self.fd)?.size) } } @@ -2554,19 +2548,10 @@ mod tests { } fn created_process_environment_snapshot() -> CreatedProcessEnvironmentSnapshot { - let platform = crate::tests::test_platform(); - let litebox = litebox::LiteBox::new(platform); + // Process environment construction only writes guest memory, so this needs no files and + // uses the objectless broker association. + let litebox = crate::test_broker::litebox(crate::tests::test_platform()); let page_manager = crate::WindowsPageManager::::new(&litebox); - let fs = Arc::new(litebox::fs::resolver::Resolver::new( - &litebox, - litebox::fs::composer::Composer::builder() - .mount("/", |allocator| { - litebox::fs::in_mem::InMem::::new(allocator) - }) - .build() - .expect("valid test filesystem"), - )); - let loader = PeLoader::new(platform, fs, &page_manager); let image = loaded_module_image(application_module_base()); let image_base_address = image.mapping.base_addr; @@ -2580,15 +2565,17 @@ mod tests { CString::new("B=two").expect("valid envp[1]"), CString::new("a=one").expect("valid envp[2]"), ]; - let environment = loader - .create_process_environment(ProcessEnvironmentInput { + let environment = create_process_environment( + &page_manager, + ProcessEnvironmentInput { image: &image.parsed, image_base_address, image_path: "test.exe", argv: &argv, envp: &envp, - }) - .expect("failed to create synthetic Windows process environment"); + }, + ) + .expect("failed to create synthetic Windows process environment"); let peb = read_guest_value::(environment.peb); CreatedProcessEnvironmentSnapshot { diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 20f2fd47da..9f8c06d548 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -4,18 +4,20 @@ use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; -use core::marker::PhantomData; use core::mem::{align_of, offset_of, size_of}; use int_enum::IntEnum; -use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, SeekError, WriteError, }; -use litebox::fs::{FileStatus, FileType, Mode, OFlags, SeekWhence}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileMode as Mode, FileOpenFlags, FileSeekWhence as SeekWhence, FileStatus, + FileType, MAX_FILE_TRANSFER_SIZE, +}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::byteorder::native_endian::U32; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; @@ -48,8 +50,7 @@ const FILE_SHARE_READ: u32 = 0x0000_0001; const FILE_SHARE_WRITE: u32 = 0x0000_0002; const FILE_SHARE_DELETE: u32 = 0x0000_0004; -// Bound guest-controlled file I/O allocations while keeping backend call overhead reasonable. -const FILE_IO_CHUNK_SIZE: usize = 0x80_000; +const FILE_IO_CHUNK_SIZE: usize = MAX_FILE_TRANSFER_SIZE as usize; /// Append at the current end of file const FILE_WRITE_TO_END_OF_FILE: i64 = -1; @@ -261,7 +262,7 @@ struct FileStatusMetadata { end_of_file: i64, allocation_size: i64, file_attributes: FileAttributes, - file_id: Option, + file_id: u64, file_id_128: [u8; 16], } @@ -281,11 +282,9 @@ impl FileStatusMetadata { .checked_next_multiple_of(status.blksize.max(1)) .and_then(|size| i64::try_from(size).ok()) .unwrap_or(i64::MAX); - let file_id = u64::try_from(status.node_info.ino).ok(); + let file_id = status.node_info.ino; let mut file_id_128 = [0; 16]; - if let Some(file_id) = file_id { - file_id_128[..size_of::()].copy_from_slice(&file_id.to_ne_bytes()); - } + file_id_128[..size_of::()].copy_from_slice(&file_id.to_ne_bytes()); Self { end_of_file, allocation_size, @@ -304,9 +303,7 @@ impl DirectoryEntry { end_of_file: metadata.end_of_file, allocation_size: metadata.allocation_size, file_attributes: metadata.file_attributes, - file_id: metadata - .file_id - .map_or(-1, |file_id| i64::from_ne_bytes(file_id.to_ne_bytes())), + file_id: i64::from_ne_bytes(metadata.file_id.to_ne_bytes()), } } } @@ -448,17 +445,13 @@ struct FileStandardInformation { padding: [u8; 2], } -pub(crate) struct FileObjectSubsystem(PhantomData); - -impl FdEnabledSubsystem for FileObjectSubsystem { - type Entry = FileObject; +impl FdEnabledSubsystem for FileObject { + type Entry = Self; } -impl FdEnabledSubsystemEntry for FileObject {} +impl FdEnabledSubsystemEntry for FileObject {} -impl crate::WindowsHandleSubsystem - for FileObjectSubsystem -{ +impl crate::WindowsHandleSubsystem for FileObject { fn normalize_desired_access(desired_access: u32) -> u32 { FileAccess::from_desired_access(desired_access).bits() } @@ -480,9 +473,9 @@ impl crate::WindowsHandleSubsystem } } -pub(crate) struct FileObject { +pub(crate) struct FileObject { path: String, - backing: FileObjectBacking, + backing: FileObjectBacking, create_time_access: FileAccess, share_access: FileShareAccess, create_options: FileCreateOptions, @@ -497,15 +490,15 @@ struct DirectoryQueryState { entries: Vec, } -enum FileObjectBacking { +enum FileObjectBacking { Filesystem { - fd: TypedFd>, + fd: litebox::fs::FileFd, is_directory: bool, }, CondrvStream { object: CondrvObject, stream_object: Arc, - fd: TypedFd>, + fd: litebox::fs::FileFd, }, CondrvControl(CondrvObject), /// A handle to `\Device\KsecDD`. @@ -529,7 +522,7 @@ enum FileIoOperation { /// with the resolved absolute byte offset (`None` when the current file-pointer /// position should be used). type PreparedFileIo = ( - litebox::fd::EntryHandle>, + litebox::fd::EntryHandle, Option, ); @@ -542,7 +535,7 @@ enum FileSharingIdentity<'a> { } impl FileSharingIdentity<'_> { - fn matches(self, file: &FileObject) -> bool { + fn matches(self, file: &FileObject) -> bool { match self { Self::Path(path) => file.condrv_stream_object_id().is_none() && file.path == path, Self::CondrvObject(object_id) => file.condrv_stream_object_id() == Some(object_id), @@ -550,7 +543,7 @@ impl FileSharingIdentity<'_> { } } -impl FileObject { +impl FileObject { fn condrv_object(&self) -> Option { match self.backing { FileObjectBacking::CondrvStream { object, .. } @@ -679,7 +672,7 @@ impl FileAccess { self, create_disposition: CreateDisposition, create_options: FileCreateOptions, - ) -> OFlags { + ) -> (FileAccessMode, FileOpenFlags) { let wants_read = self.intersects(Self::FS_READ_ACCESS); let wants_write = self.intersects(Self::FS_WRITE_ACCESS) || matches!( @@ -689,32 +682,36 @@ impl FileAccess { | CreateDisposition::OverwriteIf ); - let mut flags = match (wants_read, wants_write) { - (true, true) => OFlags::RDWR, - (false, true) => OFlags::WRONLY, - _ => OFlags::RDONLY, + let access = match (wants_read, wants_write) { + (true, true) => FileAccessMode::ReadWrite, + (false, true) => FileAccessMode::WriteOnly, + _ => FileAccessMode::ReadOnly, }; + // Append-only rights are enforced per NT handle by `prepare_file_io`. + let mut flags = FileOpenFlags::NONE; match create_disposition { CreateDisposition::Overwrite => { - flags.insert(OFlags::TRUNC); + flags = flags.union(FileOpenFlags::TRUNCATE); } CreateDisposition::Supersede | CreateDisposition::OverwriteIf => { - flags.insert(OFlags::CREAT | OFlags::TRUNC); + flags = flags.union(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE); } - CreateDisposition::Create => flags.insert(OFlags::CREAT | OFlags::EXCL), - CreateDisposition::OpenIf => flags.insert(OFlags::CREAT), + CreateDisposition::Create => { + flags = flags.union(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE); + } + CreateDisposition::OpenIf => flags = flags.union(FileOpenFlags::CREATE), CreateDisposition::Open => {} } if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { - flags.insert(OFlags::DIRECTORY); + flags = flags.union(FileOpenFlags::DIRECTORY); } if create_options.contains(FileCreateOptions::NON_DIRECTORY_FILE) { - flags.insert(OFlags::NOFOLLOW); + flags = flags.union(FileOpenFlags::NO_FOLLOW); } - flags + (access, flags) } fn conflicts_with_share(self, share_access: FileShareAccess) -> bool { @@ -823,8 +820,8 @@ impl Task { fn file_entry( &self, handle: Handle, - ) -> Result>, NtStatus> { - raw_handle_entry::>( + ) -> Result, NtStatus> { + raw_handle_entry::( &self.global.litebox, &self.process.handles, handle, @@ -836,33 +833,26 @@ impl Task { &self, handle: Handle, operation: FileIoOperation, - ) -> Result< - ( - litebox::fd::EntryHandle>, - bool, - ), - NtStatus, - > { + ) -> Result<(litebox::fd::EntryHandle, bool), NtStatus> { let mut append_only = false; - let file = self.typed_handle_entry_with_access_check::>( - handle, - |granted_access| match operation { - FileIoOperation::Read => granted_access & FileAccess::READ_DATA.bits() != 0, - FileIoOperation::Write => { - append_only = granted_access & FileAccess::WRITE_DATA.bits() == 0 - && granted_access & FileAccess::APPEND_DATA.bits() != 0; - granted_access & (FileAccess::WRITE_DATA | FileAccess::APPEND_DATA).bits() != 0 + let file = + self.typed_handle_entry_with_access_check::(handle, |granted_access| { + match operation { + FileIoOperation::Read => granted_access & FileAccess::READ_DATA.bits() != 0, + FileIoOperation::Write => { + append_only = granted_access & FileAccess::WRITE_DATA.bits() == 0 + && granted_access & FileAccess::APPEND_DATA.bits() != 0; + granted_access & (FileAccess::WRITE_DATA | FileAccess::APPEND_DATA).bits() + != 0 + } } - }, - )?; + })?; Ok((file, append_only)) } pub(crate) fn image_section_file_path(&self, handle: Handle) -> Result { - let entry = self.typed_handle_entry_with_access::>( - handle, - FileAccess::EXECUTE.bits(), - )?; + let entry = + self.typed_handle_entry_with_access::(handle, FileAccess::EXECUTE.bits())?; entry.with_entry(|file| match &file.backing { FileObjectBacking::Filesystem { is_directory: false, @@ -877,36 +867,36 @@ impl Task { }) } - fn insert_file_handle(&self, file: FileObject) -> Result { + fn insert_file_handle(&self, file: FileObject) -> Result { let granted_access = file.create_time_access.bits(); - self.insert_typed_handle::>(file, granted_access, |file| { + self.insert_typed_handle::(file, granted_access, |file| { self.close_file(file); }) } pub(crate) fn close_file_handle(&self, handle: Handle) { - self.close_typed_handle::>(handle, |file| { + self.close_typed_handle::(handle, |file| { self.close_file(file); }); } - pub(crate) fn close_file(&self, file: FileObject) { + pub(crate) fn close_file(&self, file: FileObject) { match file.backing { FileObjectBacking::Filesystem { fd, is_directory } => { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); if file .create_options .contains(FileCreateOptions::DELETE_ON_CLOSE) { if is_directory { - let _ = self.fs.rmdir(&self.fs_context, &file.path); + let _ = self.fs.rmdir_file(&self.fs_context, &file.path); } else { - let _ = self.fs.unlink(&self.fs_context, &file.path); + let _ = self.fs.unlink_file(&self.fs_context, &file.path); } } } FileObjectBacking::CondrvStream { fd, .. } => { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => {} } @@ -981,11 +971,11 @@ impl Task { } Err(status) => return status, }; - let status = match self.fs.file_status(&self.fs_context, &path) { + let status = match self.fs.path_file_status(&self.fs_context, &path) { Ok(status) => status, Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { let parent = parent_directory_path(&path); - return if self.fs.file_status(&self.fs_context, parent).is_ok() { + return if self.fs.path_file_status(&self.fs_context, parent).is_ok() { NtStatus::OBJECT_NAME_NOT_FOUND } else { NtStatus::OBJECT_PATH_NOT_FOUND @@ -993,23 +983,17 @@ impl Task { } Err(error) => return map_file_status_error(error), }; - let readonly = !status.mode.intersects(Mode::WUSR | Mode::WGRP | Mode::WOTH); - let mut file_attributes = match status.file_type { - FileType::Directory => FileAttributes::DIRECTORY, - FileType::RegularFile => FileAttributes::ARCHIVE, + if !matches!( + status.file_type, + FileType::Directory | FileType::RegularFile + ) { // TODO(chardev-attributes): Probe native attributes for character devices and // future filesystem node types; regular files are host-grounded as ARCHIVE. - file_type => { - litebox_util_log::debug!( - path = path.as_str(), - file_type:? = file_type; - "Using archive attributes for nonstandard filesystem node" - ); - FileAttributes::ARCHIVE - } - }; - if readonly { - file_attributes |= FileAttributes::READONLY; + litebox_util_log::debug!( + path = path.as_str(), + file_type:? = status.file_type; + "Using archive attributes for nonstandard filesystem node" + ); } // TODO(fs-timestamps): Populate timestamps when FileStatus exposes them. litebox_util_log::debug!( @@ -1017,7 +1001,9 @@ impl Task { "Using zero timestamps for file attributes" ); let information = FileBasicInformation { - file_attributes: file_attributes.bits(), + file_attributes: FileStatusMetadata::from_status(&status) + .file_attributes + .bits(), ..FileBasicInformation::default() }; if file_information.write_at_offset(0, information).is_none() { @@ -1083,11 +1069,11 @@ impl Task { } Err(status) => return status, }; - let status = match self.fs.file_status(&self.fs_context, &path) { + let status = match self.fs.path_file_status(&self.fs_context, &path) { Ok(status) => status, Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { let parent = parent_directory_path(&path); - return if self.fs.file_status(&self.fs_context, parent).is_ok() { + return if self.fs.path_file_status(&self.fs_context, parent).is_ok() { NtStatus::OBJECT_NAME_NOT_FOUND } else { NtStatus::OBJECT_PATH_NOT_FOUND @@ -1098,9 +1084,7 @@ impl Task { let metadata = FileStatusMetadata::from_status(&status); // TODO(fs-timestamps): Populate timestamps when FileStatus exposes them. let information = FileStatBasicInformation { - file_id: metadata - .file_id - .map_or(0, |file_id| i64::from_ne_bytes(file_id.to_ne_bytes())), + file_id: i64::from_ne_bytes(metadata.file_id.to_ne_bytes()), allocation_size: metadata.allocation_size, end_of_file: metadata.end_of_file, file_attributes: metadata.file_attributes.bits(), @@ -1181,7 +1165,7 @@ impl Task { let status = match file.with_entry(|file| match &file.backing { FileObjectBacking::Filesystem { fd, .. } | FileObjectBacking::CondrvStream { fd, .. } => { - self.fs.fd_file_status(fd).map_err(map_file_status_error) + self.fs.file_status(fd).map_err(map_file_status_error) } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { Err(NtStatus::INVALID_DEVICE_REQUEST) @@ -1228,7 +1212,7 @@ impl Task { { return NtStatus::ACCESS_VIOLATION; } - let file = match self.typed_handle_entry_with_access_check::>( + let file = match self.typed_handle_entry_with_access_check::( file_handle, |granted_access| { granted_access & (FileAccess::READ_DATA | FileAccess::WRITE_DATA).bits() != 0 @@ -1249,10 +1233,12 @@ impl Task { if *is_directory { return Err(NtStatus::INVALID_DEVICE_REQUEST); } - self.fs.seek(fd, 0, SeekWhence::RelativeToCurrentOffset) + self.fs + .seek_file(fd, 0, SeekWhence::RelativeToCurrentOffset) } FileObjectBacking::CondrvStream { fd, .. } => { - self.fs.seek(fd, 0, SeekWhence::RelativeToCurrentOffset) + self.fs + .seek_file(fd, 0, SeekWhence::RelativeToCurrentOffset) } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { return Err(NtStatus::INVALID_DEVICE_REQUEST); @@ -1261,6 +1247,7 @@ impl Task { match seek { Ok(position) => Ok(position), Err(SeekError::ClosedFd) => Err(NtStatus::INVALID_HANDLE), + Err(SeekError::NotForSeeking) => Err(NtStatus::ACCESS_DENIED), Err(SeekError::NonSeekable | SeekError::InvalidOffset) => { Err(NtStatus::INVALID_DEVICE_REQUEST) } @@ -1318,7 +1305,7 @@ impl Task { if position < 0 { return NtStatus::INVALID_PARAMETER; } - let file = match self.typed_handle_entry_with_access_check::>( + let file = match self.typed_handle_entry_with_access_check::( file_handle, |granted_access| { granted_access & (FileAccess::READ_DATA | FileAccess::WRITE_DATA).bits() != 0 @@ -1339,10 +1326,12 @@ impl Task { if *is_directory { return Err(NtStatus::INVALID_DEVICE_REQUEST); } - self.fs.seek(fd, position, SeekWhence::RelativeToBeginning) + self.fs + .seek_file(fd, position, SeekWhence::RelativeToBeginning) } FileObjectBacking::CondrvStream { fd, .. } => { - self.fs.seek(fd, position, SeekWhence::RelativeToBeginning) + self.fs + .seek_file(fd, position, SeekWhence::RelativeToBeginning) } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { return Err(NtStatus::INVALID_DEVICE_REQUEST); @@ -1351,6 +1340,7 @@ impl Task { match seek { Ok(_) => Ok(()), Err(SeekError::ClosedFd) => Err(NtStatus::INVALID_HANDLE), + Err(SeekError::NotForSeeking) => Err(NtStatus::ACCESS_DENIED), Err(SeekError::InvalidOffset) => Err(NtStatus::INVALID_PARAMETER), Err(SeekError::NonSeekable) => Err(NtStatus::INVALID_DEVICE_REQUEST), Err(_) => Err(NtStatus::UNSUCCESSFUL), @@ -1471,10 +1461,10 @@ impl Task { if *is_directory { return Err(WriteError::NotAFile); } - self.fs.write(fd, &bytes, chunk_offset) + self.fs.write_file(fd, &bytes, chunk_offset) } FileObjectBacking::CondrvStream { fd, .. } => { - self.fs.write(fd, &bytes, chunk_offset) + self.fs.write_file(fd, &bytes, chunk_offset) } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { Err(WriteError::NotAFile) @@ -1501,7 +1491,7 @@ impl Task { .intersects(FileCreateOptions::SYNCHRONOUS_IO) && input_length != 0 { - let _ = self.fs.seek( + let _ = self.fs.seek_file( fd, (offset + total_written).cast_signed(), SeekWhence::RelativeToBeginning, @@ -1574,14 +1564,16 @@ impl Task { return Err(ReadError::NotAFile); } ( - self.fs.read(fd, &mut bytes[..chunk_length], chunk_offset), + self.fs + .read_file(fd, &mut bytes[..chunk_length], chunk_offset), true, ) } // TODO(condrv-large-read): Continue with per-operation nonblocking reads // after the first chunk once FileSystem can report WouldBlock. FileObjectBacking::CondrvStream { fd, .. } => ( - self.fs.read(fd, &mut bytes[..chunk_length], chunk_offset), + self.fs + .read_file(fd, &mut bytes[..chunk_length], chunk_offset), false, ), FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { @@ -1610,7 +1602,7 @@ impl Task { .intersects(FileCreateOptions::SYNCHRONOUS_IO) && output_length != 0 { - let _ = self.fs.seek( + let _ = self.fs.seek_file( fd, (offset + total_read).cast_signed(), SeekWhence::RelativeToBeginning, @@ -1667,9 +1659,12 @@ impl Task { && operation == FileIoOperation::Write => { let status = file - .with_entry(|file| self.fs.file_status(&self.fs_context, &file.path)) + .with_entry(|file| match &file.backing { + FileObjectBacking::Filesystem { fd, .. } => self.fs.file_status(fd), + _ => self.fs.path_file_status(&self.fs_context, &file.path), + }) .map_err(map_file_status_error)?; - Some(status.size) + Some(usize::try_from(status.size).map_err(|_| NtStatus::INVALID_PARAMETER)?) } Some(FILE_USE_FILE_POINTER_POSITION) | None => None, Some(offset) if offset >= 0 => { @@ -1688,7 +1683,11 @@ impl Task { "Ignoring file I/O byte-range lock key; byte-range locking is not supported yet" ); } - if offset.is_some_and(|offset| offset.checked_add(length).is_none()) { + if offset.is_some_and(|offset| { + offset + .checked_add(length) + .is_none_or(|end| isize::try_from(end).is_err()) + }) { return Err(NtStatus::INVALID_PARAMETER); } if !event.is_null() { @@ -1808,7 +1807,7 @@ impl Task { return NtStatus::INFO_LENGTH_MISMATCH; } - let file = match self.typed_handle_entry_with_access::>( + let file = match self.typed_handle_entry_with_access::( file_handle, FileAccess::LIST_DIRECTORY.bits(), ) { @@ -1870,7 +1869,7 @@ impl Task { fn query_directory( &self, - file: &mut FileObject, + file: &mut FileObject, supplied_pattern: Option, information_class: FileInformationClass, flags: DirectoryQueryFlags, @@ -1959,10 +1958,7 @@ impl Task { Ok((NtStatus::SUCCESS, output)) } - fn read_directory_entries( - &self, - file: &FileObject, - ) -> Result, NtStatus> { + fn read_directory_entries(&self, file: &FileObject) -> Result, NtStatus> { let FileObjectBacking::Filesystem { fd, is_directory } = &file.backing else { return Err(NtStatus::INVALID_PARAMETER); }; @@ -1970,24 +1966,28 @@ impl Task { return Err(NtStatus::INVALID_PARAMETER); } - let current_status = self.fs.fd_file_status(fd).map_err(map_file_status_error)?; + let current_status = self.fs.file_status(fd).map_err(map_file_status_error)?; let parent_path = parent_directory_path(&file.path); let parent_status = self .fs - .file_status(&self.fs_context, parent_path) + .path_file_status(&self.fs_context, parent_path) .map_err(map_file_status_error)?; let mut entries = alloc::vec![ DirectoryEntry::from_status(String::from("."), ¤t_status), DirectoryEntry::from_status(String::from(".."), &parent_status), ]; - for entry in self.fs.read_dir(fd).map_err(map_read_dir_error)? { + for entry in self + .fs + .read_file_directory(&self.fs_context, &file.path, fd) + .map_err(map_read_dir_error)? + { if entry.name == "." || entry.name == ".." { continue; } let path = child_path(&file.path, &entry.name); let status = self .fs - .file_status(&self.fs_context, path) + .path_file_status(&self.fs_context, &path) .map_err(map_file_status_error)?; entries.push(DirectoryEntry::from_status(entry.name, &status)); } @@ -2194,7 +2194,7 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, file_attributes: u32, - ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { self.check_file_sharing( FileSharingIdentity::Path(&path), desired_access, @@ -2244,7 +2244,7 @@ impl Task { create_options: FileCreateOptions, ea_buffer: Option>, ea_length: u32, - ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + ) -> 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 { @@ -2316,7 +2316,7 @@ impl Task { desired_access: FileAccess, share_access: FileShareAccess, create_options: FileCreateOptions, - ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { return Err(NtStatus::NOT_A_DIRECTORY); } @@ -2340,37 +2340,30 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, mode: Mode, - ) -> Result< - ( - TypedFd>, - bool, - FileCreateInformation, - ), - NtStatus, - > { - let existed_before_open = self.fs.file_status(&self.fs_context, path).is_ok(); + ) -> Result<(litebox::fs::FileFd, bool, FileCreateInformation), NtStatus> { + let existed_before_open = self.fs.path_file_status(&self.fs_context, path).is_ok(); if create_disposition == CreateDisposition::Supersede && existed_before_open && !desired_access.contains(FileAccess::DELETE) { return Err(NtStatus::ACCESS_DENIED); } - let flags = desired_access.open_flags(create_disposition, create_options); + let (access, flags) = desired_access.open_flags(create_disposition, create_options); let fd = self .fs - .open(&self.fs_context, path, flags, mode) + .open_file(&self.fs_context, path, access, flags, mode) .map_err(|error| map_open_error(error, create_disposition))?; - let file_status = match self.fs.fd_file_status(&fd) { + let file_status = match self.fs.file_status(&fd) { Ok(file_status) => file_status, Err(error) => { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(map_file_status_error(error)); } }; if create_options.contains(FileCreateOptions::NON_DIRECTORY_FILE) && file_status.file_type == FileType::Directory { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(NtStatus::OBJECT_TYPE_MISMATCH); } let information = create_disposition.success_information(existed_before_open); @@ -2389,7 +2382,7 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, file_attributes: u32, - ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { if matches!( create_disposition, CreateDisposition::Supersede @@ -2399,7 +2392,7 @@ impl Task { return Err(NtStatus::INVALID_PARAMETER); } - let existed_before_open = match self.fs.file_status(&self.fs_context, path) { + let existed_before_open = match self.fs.path_file_status(&self.fs_context, path) { Ok(status) => { if status.file_type != FileType::Directory { return Err(NtStatus::NOT_A_DIRECTORY); @@ -2413,7 +2406,7 @@ impl Task { ) => { self.fs - .mkdir( + .mkdir_file( &self.fs_context, path, create_directory_mode(file_attributes), @@ -2429,10 +2422,10 @@ impl Task { } else { CreateDisposition::Open }; - let flags = desired_access.open_flags(open_disposition, create_options); + let (access, flags) = desired_access.open_flags(open_disposition, create_options); let fd = self .fs - .open(&self.fs_context, path, flags, Mode::empty()) + .open_file(&self.fs_context, path, access, flags, Mode::empty()) .map_err(|error| map_open_error(error, create_disposition))?; let information = create_disposition.success_information(existed_before_open); Ok(( @@ -2488,7 +2481,7 @@ impl Task { let Some(handle) = Handle::from_raw_fd(raw_handle) else { continue; }; - let Some(entry) = raw_handle_entry::>( + let Some(entry) = raw_handle_entry::( &self.global.litebox, &self.process.handles, handle, @@ -2765,6 +2758,7 @@ fn map_read_dir_error(error: ReadDirError) -> NtStatus { match error { ReadDirError::ClosedFd => NtStatus::INVALID_HANDLE, ReadDirError::NotADirectory => NtStatus::NOT_A_DIRECTORY, + ReadDirError::NotForReading => NtStatus::ACCESS_DENIED, _ => NtStatus::UNSUCCESSFUL, } } @@ -2823,15 +2817,16 @@ mod tests { fn create_existing_file(task: &Task, path: &str, data: &[u8]) { let fd = task .fs - .open( + .open_file( &task.fs_context, path, - OFlags::CREAT | OFlags::RDWR, + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, Mode::RUSR | Mode::WUSR, ) .unwrap(); - assert_eq!(task.fs.write(&fd, data, Some(0)).unwrap(), data.len()); - task.fs.close(&fd).unwrap(); + assert_eq!(task.fs.write_file(&fd, data, Some(0)).unwrap(), data.len()); + task.fs.close_file(&fd).unwrap(); } fn create_file( @@ -2878,6 +2873,90 @@ mod tests { handle } + #[test] + fn regular_file_namespace_rejects_and_hides_the_registry_store() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task_with_broker_files(&[]); + + for path in [ + "/registry", + "/REGISTRY/machine", + r"\??\C:\registry\machine", + r"\Device\HarddiskVolume1\Registry", + ] { + assert_eq!( + create_file(&task, path, FILE_GENERIC_READ, FILE_OPEN).0, + NtStatus::OBJECT_PATH_NOT_FOUND, + "{path}" + ); + } + assert_eq!( + create_file( + &task, + "/registry/created-by-file-api", + FILE_GENERIC_WRITE, + FILE_CREATE + ) + .0, + NtStatus::OBJECT_PATH_NOT_FOUND + ); + + let (_path, _name, query_attributes) = open_object_attributes("/registry"); + let mut basic_information = FileBasicInformation::default(); + assert_eq!( + task.sys_nt_query_attributes_file( + Some(const_ptr(&query_attributes)), + mut_ptr(&mut basic_information), + ), + NtStatus::OBJECT_PATH_NOT_FOUND + ); + + let root = open_fs_root(&task); + let (_path, _name, mut relative_attributes) = open_object_attributes("registry"); + relative_attributes.root_directory = root; + let mut handle = Handle::default(); + let mut io_status = IoStatusBlock::default(); + assert_eq!( + task.sys_nt_open_file( + mut_ptr(&mut handle), + FILE_GENERIC_READ, + Some(const_ptr(&relative_attributes)), + mut_ptr(&mut io_status), + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + (FileCreateOptions::DIRECTORY_FILE + | FileCreateOptions::SYNCHRONOUS_IO_NONALERT) + .bits(), + ), + NtStatus::OBJECT_PATH_NOT_FOUND + ); + + let mut output = [0; 1024]; + assert_eq!( + query_directory( + &task, + root, + FileInformationClass::FileNamesInformation, + DirectoryQueryFlags::RESTART_SCAN, + None, + &mut io_status, + &mut output, + ), + NtStatus::SUCCESS + ); + let names = directory_record_names( + &output, + FileInformationClass::FileNamesInformation, + io_status.information, + ); + assert!( + names + .iter() + .all(|name| !name.eq_ignore_ascii_case("registry")), + "{names:?}" + ); + }); + } + fn open_ksecdd(task: &Task, desired_access: u32) -> Handle { let (_path, _name, attributes) = open_object_attributes(r"\Device\KsecDD"); let mut handle = Handle::default(); @@ -2900,7 +2979,7 @@ mod tests { fn ksecdd_requires_broker_and_rejects_unknown_controls() { run_with_test_platform_pointers(|| { const UNKNOWN_KSEC_IOCTL: u32 = 0x0039_0000; - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let handle = open_ksecdd(&task, FILE_GENERIC_READ | FILE_GENERIC_WRITE); let mut random = [0xa5; 32]; @@ -3181,10 +3260,10 @@ mod tests { #[test] fn nt_query_attributes_file_reports_file_type_attributes() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/query-attributes.txt", b"data"); task.fs - .mkdir( + .mkdir_file( &task.fs_context, "/tmp/query-attributes-dir", Mode::RUSR | Mode::WUSR | Mode::XUSR, @@ -3222,7 +3301,7 @@ mod tests { #[test] fn nt_duplicate_object_rejects_file_access_escalation() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/duplicate-read-only.txt", b"data"); let (status, source, _) = create_file( &task, @@ -3261,7 +3340,7 @@ mod tests { NtStatus::SUCCESS ); assert_eq!( - task.typed_handle::>(maximum_duplicate) + task.typed_handle::(maximum_duplicate) .and_then(|typed| { task.typed_handle_metadata(&typed) .map(|metadata| metadata.granted_access) @@ -3275,7 +3354,7 @@ mod tests { #[test] fn nt_write_file_forces_append_only_handles_to_end_of_file() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let path = "/tmp/append-only.txt"; create_existing_file(&task, path, b"data"); let (status, handle, _) = create_file( @@ -3308,12 +3387,18 @@ mod tests { let fd = task .fs - .open(&task.fs_context, path, OFlags::RDONLY, Mode::empty()) + .open_file( + &task.fs_context, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); let mut contents = [0; 5]; - assert_eq!(task.fs.read(&fd, &mut contents, Some(0)).unwrap(), 5); + assert_eq!(task.fs.read_file(&fd, &mut contents, Some(0)).unwrap(), 5); assert_eq!(&contents, b"data!"); - task.fs.close(&fd).unwrap(); + task.fs.close_file(&fd).unwrap(); }); } @@ -3336,13 +3421,13 @@ mod tests { #[test] fn nt_query_standard_information_uses_open_file_metadata() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let path = "/tmp/query-standard-open-file.txt"; create_existing_file(&task, path, b"original"); let (status, handle, _) = create_file(&task, path, FILE_GENERIC_READ, FILE_OPEN); assert_eq!(status, NtStatus::SUCCESS); - task.fs.unlink(&task.fs_context, path).unwrap(); + task.fs.unlink_file(&task.fs_context, path).unwrap(); create_existing_file(&task, path, b"replacement is longer"); let mut information = FileStandardInformation::default(); @@ -3366,7 +3451,7 @@ mod tests { #[test] fn nt_set_position_information_updates_synchronous_position() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let path = "/tmp/set-position-sync.txt"; create_existing_file(&task, path, b"0123456789"); let (status, handle, _) = create_file( @@ -3420,7 +3505,7 @@ mod tests { #[test] fn nt_set_position_information_rejects_duplicate_without_data_access() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let path = "/tmp/set-position-no-access.txt"; create_existing_file(&task, path, b"0123456789"); let (status, handle, _) = create_file( @@ -3441,7 +3526,7 @@ mod tests { #[test] fn nt_file_io_transfers_across_multiple_chunks() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (status, handle, _) = create_file( &task, "/tmp/chunked-file-io.txt", @@ -3497,7 +3582,7 @@ mod tests { #[test] fn nt_create_file_follows_condrv_connection_through_standard_streams() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let server_handle = open_condrv_server(&task); let reference_handle = open_condrv_reference(&task, server_handle); let (_connect_path, _connect_name, mut connect_attributes) = @@ -3754,7 +3839,7 @@ mod tests { #[test] fn nt_query_volume_information_file_returns_fs_device_information() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let handle = open_fs_root(&task); let mut io_status = IoStatusBlock::default(); let mut output = FileFsDeviceInformation { @@ -3791,9 +3876,9 @@ mod tests { #[test] fn nt_query_directory_file_ex_tracks_restart_single_and_no_cursor_flags() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); task.fs - .mkdir(&task.fs_context, "/tmp/query-cursor", Mode::RWXU) + .mkdir_file(&task.fs_context, "/tmp/query-cursor", Mode::RWXU) .unwrap(); create_existing_file(&task, "/tmp/query-cursor/alpha", b"a"); create_existing_file(&task, "/tmp/query-cursor/beta", b"b"); @@ -3882,7 +3967,7 @@ mod tests { #[test] fn nt_query_volume_information_file_leaves_iosb_untouched_on_failures() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let handle = open_fs_root(&task); let sentinel = IoStatusBlock::new(NtStatus::from_raw(0x1111_1111), 0x2222_2222); let mut io_status = sentinel; @@ -3975,10 +4060,10 @@ mod tests { #[test] fn nt_open_file_opens_existing_absolute_and_relative_files() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/dir-file-root.txt", b"root"); task.fs - .mkdir( + .mkdir_file( &task.fs_context, "/tmp/dir", Mode::RUSR | Mode::WUSR | Mode::XUSR, @@ -4045,7 +4130,7 @@ mod tests { #[test] fn nt_create_file_reports_disposition_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/existing.txt", b"old"); let (status, handle, io_status) = @@ -4121,7 +4206,7 @@ mod tests { #[test] fn nt_create_file_reports_missing_and_collision_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/existing-collision.txt", b"old"); let (status, _handle, io_status) = @@ -4149,7 +4234,7 @@ mod tests { #[test] fn nt_create_file_rejects_invalid_share_access() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/invalid-share.txt", b"old"); let (_path, _name, attributes) = open_object_attributes("/tmp/invalid-share.txt"); let mut io_status = IoStatusBlock::default(); @@ -4173,7 +4258,7 @@ mod tests { #[test] fn nt_create_file_directory_handles_can_root_relative_opens() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (_path, _name, attributes) = open_object_attributes("/tmp/created-dir"); let mut io_status = IoStatusBlock::default(); let directory_handle = task @@ -4212,9 +4297,9 @@ mod tests { #[test] fn nt_create_file_actual_directory_handles_can_root_relative_opens() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); task.fs - .mkdir( + .mkdir_file( &task.fs_context, "/tmp/implicit-dir", Mode::RUSR | Mode::WUSR | Mode::XUSR, @@ -4334,19 +4419,18 @@ mod tests { ), Ok(()) ); - assert!( - generic_read - .open_flags( - CreateDisposition::Open, - FileCreateOptions::NON_DIRECTORY_FILE - ) - .contains(OFlags::NOFOLLOW) + assert_eq!( + generic_read.open_flags( + CreateDisposition::Open, + FileCreateOptions::NON_DIRECTORY_FILE + ), + (FileAccessMode::ReadOnly, FileOpenFlags::NO_FOLLOW) ); } #[test] fn nt_create_file_enforces_share_access() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/shared.txt", b"old"); let (_path, _name, attributes) = open_object_attributes("/tmp/shared.txt"); let mut io_status = IoStatusBlock::default(); @@ -4386,7 +4470,7 @@ mod tests { #[test] fn nt_close_releases_file_handle_and_share_lock() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/close-shared.txt", b"old"); let (_path, _name, attributes) = open_object_attributes("/tmp/close-shared.txt"); let mut io_status = IoStatusBlock::default(); @@ -4445,7 +4529,7 @@ mod tests { #[test] fn nt_close_deletes_delete_on_close_file() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/delete-on-close.txt", b"old"); let (_path, _name, attributes) = open_object_attributes("/tmp/delete-on-close.txt"); let mut io_status = IoStatusBlock::default(); @@ -4467,20 +4551,20 @@ mod tests { assert!( task.fs - .file_status(&task.fs_context, "/tmp/delete-on-close.txt") + .path_file_status(&task.fs_context, "/tmp/delete-on-close.txt") .is_ok() ); assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); assert!(matches!( task.fs - .file_status(&task.fs_context, "/tmp/delete-on-close.txt"), + .path_file_status(&task.fs_context, "/tmp/delete-on-close.txt"), Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) )); } #[test] fn nt_close_deletes_delete_on_close_directory() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (_path, _name, attributes) = open_object_attributes("/tmp/delete-on-close-dir"); let mut io_status = IoStatusBlock::default(); let handle = task @@ -4503,20 +4587,20 @@ mod tests { assert!( task.fs - .file_status(&task.fs_context, "/tmp/delete-on-close-dir") + .path_file_status(&task.fs_context, "/tmp/delete-on-close-dir") .is_ok() ); assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); assert!(matches!( task.fs - .file_status(&task.fs_context, "/tmp/delete-on-close-dir"), + .path_file_status(&task.fs_context, "/tmp/delete-on-close-dir"), Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) )); } #[test] fn write_file_result_clears_handle_output_when_iosb_write_fails() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (_path, _name, attributes) = open_object_attributes("/tmp/iosb-fault.txt"); let mut io_status = IoStatusBlock::default(); let created_handle = task @@ -4815,7 +4899,7 @@ mod tests { .contains(FileDeviceCharacteristics::IS_MOUNTED) ); - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let handle = open_fs_root(&task); let mut output = FileFsDeviceInformation { device_type: 0, @@ -5000,7 +5084,7 @@ mod tests { }; close_host_handle(host_handle); - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/existing.txt", b"litebox"); let (_path, _name, attributes) = open_object_attributes("/tmp/existing.txt"); let mut litebox_handle = Handle::default(); @@ -5116,7 +5200,7 @@ mod tests { }; close_host_handle(host_handle); - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (_path, _name, attributes) = open_object_attributes("/tmp/supersede-created.txt"); let mut litebox_handle = Handle::default(); let mut litebox_io_status = IoStatusBlock::default(); diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs index 9106454e5b..89f7a8dc92 100644 --- a/litebox_shim_windows/src/syscalls/nls.rs +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -5,12 +5,11 @@ use alloc::format; use alloc::string::String; use alloc::vec::Vec; use core::mem::size_of; -use litebox::fd::TypedFd; use litebox::fs::errors::{FileStatusError, OpenError, PathError, ReadError}; -use litebox::fs::{FileType, Mode, OFlags}; use litebox::mm::linux::{CreatePagesFlags, MappingError, NonZeroPageSize}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags, FileType}; use litebox_common_windows::loader::PAGE_SIZE; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; @@ -446,8 +445,8 @@ struct MappedNlsSection { len: usize, } -struct NlsSectionFile { - fd: TypedFd>, +struct NlsSectionFile { + fd: litebox::fs::FileFd, len: usize, } @@ -724,12 +723,12 @@ impl Task { let alloc_len = match nls_section_alloc_len(section_len) { Ok(alloc_len) => alloc_len, Err(status) => { - let _ = self.fs.close(§ion_file.fd); + let _ = self.fs.close_file(§ion_file.fd); return Err(status); } }; let Some(page_len) = NonZeroPageSize::::new(alloc_len) else { - let _ = self.fs.close(§ion_file.fd); + let _ = self.fs.close_file(§ion_file.fd); return Err(NtStatus::INVALID_PARAMETER); }; @@ -750,7 +749,7 @@ impl Task { }, ) }; - let _ = self.fs.close(§ion_file.fd); + let _ = self.fs.close_file(§ion_file.fd); let mapping = mapping.map_err(|_| copy_status.unwrap_or(NtStatus::NO_MEMORY))?; Ok(MappedNlsSection { address: mapping.as_usize(), @@ -761,43 +760,45 @@ impl Task { fn open_nls_section_file( &self, request: NlsSectionRequest, - ) -> Result, NtStatus> { + ) -> Result { let path = nls_section_file_path(request.section_type, request.section_data)?; let fd = self .fs - .open( + .open_file( &self.fs_context, path.as_str(), - OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), ) .map_err(map_nls_open_error)?; - let status = match self.fs.fd_file_status(&fd) { + let status = match self.fs.file_status(&fd) { Ok(status) => status, Err(error) => { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(map_nls_file_status_error(error)); } }; if status.file_type != FileType::RegularFile { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(NtStatus::OBJECT_TYPE_MISMATCH); } if status.size == 0 { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(NtStatus::OBJECT_NAME_NOT_FOUND); } - Ok(NlsSectionFile { - fd, - len: status.size, - }) + let Ok(len) = usize::try_from(status.size) else { + let _ = self.fs.close_file(&fd); + return Err(NtStatus::SECTION_TOO_BIG); + }; + Ok(NlsSectionFile { fd, len }) } fn copy_nls_section_file( &self, - fd: &TypedFd>, + fd: &litebox::fs::FileFd, section_len: usize, output: MutPtr, ) -> Result { @@ -808,7 +809,7 @@ impl Task { let chunk_len = remaining.min(PAGE_SIZE); let read = self .fs - .read(fd, &mut chunk[..chunk_len], Some(offset)) + .read_file(fd, &mut chunk[..chunk_len], Some(offset)) .map_err(map_nls_read_error)?; if read == 0 { return Err(NtStatus::END_OF_FILE); @@ -1235,7 +1236,7 @@ mod tests { #[test] fn nt_get_nls_section_ptr_matches_host_section_content() { let host_file_bytes = host_system32_file_bytes("c_1252.nls"); - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/c_1252.nls", host_file_bytes.as_slice(), )]); @@ -1286,7 +1287,7 @@ mod tests { #[test] fn nt_initialize_nls_files_matches_host_outputs() { let host_file_bytes = host_system32_file_bytes("locale.nls"); - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/locale.nls", host_file_bytes.as_slice(), )]); @@ -1420,7 +1421,7 @@ mod tests { #[test] fn nt_get_nls_section_ptr_maps_file_backed_section() { let section_bytes = vec![1, 2, 3, 4, 5]; - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/c_1252.nls", section_bytes.as_slice(), )]); @@ -1465,7 +1466,7 @@ mod tests { #[test] fn nt_get_nls_section_ptr_rejects_invalid_arguments() { let bytes = [0xaa]; - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/c_437.nls", bytes.as_slice(), )]); @@ -1522,7 +1523,7 @@ mod tests { #[test] fn nt_initialize_nls_files_maps_locale_file() { let locale_bytes = vec![0x44; PAGE_SIZE + 1]; - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/locale.nls", locale_bytes.as_slice(), )]); diff --git a/litebox_shim_windows/src/syscalls/object_manager.rs b/litebox_shim_windows/src/syscalls/object_manager.rs index 063c701348..5f8783ebe8 100644 --- a/litebox_shim_windows/src/syscalls/object_manager.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -1616,7 +1616,8 @@ mod tests { }; use crate::tests::{ TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, null_mut_ptr, object_attributes, - run_with_test_platform_pointers, test_task, unicode_string, utf16_units, + run_with_test_platform_pointers, test_task, test_task_with_broker_files, unicode_string, + utf16_units, }; const DIRECTORY_QUERY: u32 = 0x0000_0001; @@ -1985,7 +1986,7 @@ mod tests { #[test] fn open_section_rejects_empty_known_dlls_with_zeroed_output() { run_with_test_platform_pointers(|| { - let task = test_task(); + let task = test_task_with_broker_files(&[]); let known_dlls_units = utf16_units(r"\KnownDlls"); let known_dlls_name = unicode_string(&known_dlls_units); let known_dlls_attrs = object_attributes( diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 4b1391b2cc..fbf8942a8c 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Windows registry syscalls backed by a private file-system-shaped store (i.e., -//! an overlay file system with in-memory and tar backends). +//! Windows registry syscalls backed by LiteBox's broker-backed file APIs. //! //! Registry keys are represented as directories and values as files under each //! key's `.values` directory: @@ -24,7 +23,6 @@ //! This is only an implementation detail: syscall handlers must expose registry //! object semantics rather than file semantics. -use core::marker::PhantomData; use core::mem::{offset_of, size_of}; use alloc::collections::BTreeMap; @@ -34,22 +32,22 @@ use alloc::vec; use alloc::vec::Vec; use int_enum::IntEnum; -use litebox::LiteBox; use litebox::event::{ Events, polling::{Pollee, TryOpError}, }; -use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, WriteError, }; -use litebox::fs::{FileType, Mode, OFlags}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::sync::Mutex; use litebox::utils::TruncateExt; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags, FileType}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; +use crate::fs::{Fs, REGISTRY_ROOT}; use crate::syscalls::Handle; use crate::{ConstPtr, MutPtr, Task, probe_guest_output_preserving_value, raw_handle_entry}; @@ -58,39 +56,39 @@ use crate::nt_types::{ read_unicode_string_at, }; -type RegistryFileSystem = - litebox::fs::resolver::Resolver; - -pub(crate) struct RegistryKeySubsystem(PhantomData); - -impl FdEnabledSubsystem for RegistryKeySubsystem { - type Entry = RegistryKeyObject; +impl FdEnabledSubsystem for RegistryKeyObject { + type Entry = Self; } -impl FdEnabledSubsystemEntry for RegistryKeyObject {} +impl FdEnabledSubsystemEntry for RegistryKeyObject {} -impl crate::WindowsHandleSubsystem - for RegistryKeySubsystem -{ +impl crate::WindowsHandleSubsystem for RegistryKeyObject { fn normalize_desired_access(desired_access: u32) -> u32 { RegistryKeyAccess::from_desired_access(desired_access).bits() } } -pub(crate) struct RegistryKeyObject { +pub(crate) struct RegistryKeyObject { path: String, - fd: TypedFd>, + fd: litebox::fs::FileFd, } pub(crate) struct RegistryStore { - fs: RegistryFileSystem, - fs_context: litebox::fs::resolver::Context, + fs: Fs, + fs_context: litebox::fs::Context, + /// Whether the built-in keys and values have been written to [`Self::fs`]. + /// + /// The defaults are written on first use rather than at construction so that + /// building a shim issues no file requests. + defaults_seeded: Mutex, notification_state: Mutex, notification_pollee: Pollee, } /// Reserved backing-store directory that contains a registry key's value files. const VALUES_DIR_NAME: &str = ".values"; +/// NT object-manager root accepted by registry syscalls. +const REGISTRY_NT_ROOT: &str = r"\Registry"; const DEFAULT_CODE_PAGE_KEY: &str = "\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nls\\CodePage"; const DEFAULT_SESSION_MANAGER_KEY: &str = @@ -389,19 +387,17 @@ impl RegistryKeyAccess { normalized }) } -} -impl From for OFlags { - fn from(desired_access: RegistryKeyAccess) -> Self { - let wants_read = desired_access.intersects(RegistryKeyAccess::FS_READ_ACCESS); - let wants_write = desired_access.intersects(RegistryKeyAccess::FS_WRITE_ACCESS); + fn open_flags(self) -> (FileAccessMode, FileOpenFlags) { + let wants_read = self.intersects(Self::FS_READ_ACCESS); + let wants_write = self.intersects(Self::FS_WRITE_ACCESS); let access = match (wants_read, wants_write) { - (true, true) => OFlags::RDWR, - (false, true) => OFlags::WRONLY, - _ => OFlags::RDONLY, + (true, true) => FileAccessMode::ReadWrite, + (false, true) => FileAccessMode::WriteOnly, + _ => FileAccessMode::ReadOnly, }; - access | OFlags::DIRECTORY + (access, FileOpenFlags::DIRECTORY) } } @@ -539,7 +535,7 @@ struct KeySummary { max_name_len: usize, values: usize, max_value_name_len: usize, - max_value_data_len: usize, + max_value_data_len: u64, } /// The `KEY_VALUE_BASIC_INFORMATION` structure defines a subset of the full @@ -602,211 +598,65 @@ struct RegistryValue { } impl RegistryStore { - pub(crate) fn new(litebox: &LiteBox) -> Self { - let in_mem = litebox::fs::in_mem::InMem::::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let fs = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox::fs::overlay::Overlay::::new( - in_mem, - litebox::fs::tar_ro::TarRo::new( - // TODO: Replace with tar file provided by the user - litebox::fs::tar_ro::EMPTY_TAR_FILE.into(), - allocators.next(), - ), - allocators.next(), - ) - }) - .build() - .unwrap(), - ); - let fs_context = litebox::fs::resolver::Context::new(); - { - let fs = &fs; - for key in [ - DEFAULT_SESSION_MANAGER_KEY, - DEFAULT_SEGMENT_HEAP_KEY, - DEFAULT_IMAGE_FILE_EXECUTION_OPTIONS_KEY, - DEFAULT_WINSOCK_PARAMETERS_KEY, - DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, - DEFAULT_WINSOCK_IPV4_TCP_ENTRY_KEY, - DEFAULT_WINSOCK_IPV4_UDP_ENTRY_KEY, - DEFAULT_WINSOCK_IPV6_TCP_ENTRY_KEY, - DEFAULT_WINSOCK_IPV6_UDP_ENTRY_KEY, - DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - ] { - if let Err(status) = create_key_in_fs(fs, &fs_context, key) { - litebox_util_log::error!(key:% = key, status:? = status; "failed to initialize registry key"); - break; - } - } - for (name, value) in [ - ("ACP", DEFAULT_ACP_VALUE), - ("OEMCP", DEFAULT_OEMCP_VALUE), - ("MACCP", DEFAULT_MACCP_VALUE), - ] { - if let Err(status) = write_value_in_fs( - fs, - &fs_context, - DEFAULT_CODE_PAGE_KEY, - name, - RegistryValueType::Sz, - value, - ) { - litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize registry value"); - break; - } - } - - let mut winsock_values = vec![ - ( - DEFAULT_WINSOCK_PARAMETERS_KEY, - "WinSock_Registry_Version", - RegistryValueType::Sz, - utf16le_nul("2.0"), - ), - ( - DEFAULT_WINSOCK_PARAMETERS_KEY, - "Current_Protocol_Catalog", - RegistryValueType::Sz, - utf16le_nul("Protocol_Catalog9"), - ), - ( - DEFAULT_WINSOCK_PARAMETERS_KEY, - "Current_NameSpace_Catalog", - RegistryValueType::Sz, - utf16le_nul("NameSpace_Catalog5"), - ), - ( - DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, - "Num_Catalog_Entries64", - RegistryValueType::Dword, - WINSOCK_PROTOCOL_CATALOG_ENTRY_COUNT.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, - "Next_Catalog_Entry_ID", - RegistryValueType::Dword, - WINSOCK_NEXT_PROTOCOL_CATALOG_ENTRY_ID - .to_le_bytes() - .to_vec(), - ), - ( - DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, - "Serial_Access_Num", - RegistryValueType::Dword, - WINSOCK_INITIAL_CATALOG_SERIAL.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, - "Num_Catalog_Entries64", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_CATALOG_ENTRY_COUNT.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, - "Serial_Access_Num", - RegistryValueType::Dword, - WINSOCK_INITIAL_CATALOG_SERIAL.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "LibraryPath", - RegistryValueType::Sz, - utf16le_nul("%SystemRoot%\\System32\\mswsock.dll"), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "DisplayString", - RegistryValueType::Sz, - utf16le_nul("@%SystemRoot%\\system32\\wshtcpip.dll,-60103"), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "ProviderId", - RegistryValueType::Binary, - WINSOCK_NAMESPACE_PROVIDER_ID.to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "SupportedNameSpace", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_DNS.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "Enabled", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_PROVIDER_ENABLED.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "Version", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_PROVIDER_VERSION.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "StoresServiceClassInfo", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_STORES_SERVICE_CLASS_INFO - .to_le_bytes() - .to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "ProviderInfo", - RegistryValueType::Binary, - Vec::new(), - ), - ]; - for protocol in &DEFAULT_WINSOCK_PROTOCOLS { - winsock_values.push(( - protocol.entry_key, - "PackedCatalogItem", - RegistryValueType::Binary, - default_winsock_protocol_catalog_item(protocol), - )); - winsock_values.push(( - protocol.entry_key, - "ProtocolName", - RegistryValueType::Sz, - utf16le_nul(protocol.protocol_name), - )); - } - for (key, name, value_type, value) in winsock_values { - if let Err(status) = - write_value_in_fs(fs, &fs_context, key, name, value_type, &value) - { - litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize Winsock registry value"); - break; - } - } - } + /// Creates a registry store over the guest's brokered file system. + /// + /// Construction performs no file operation: the built-in keys and values are + /// seeded lazily by [`Self::fs`] when the guest first uses the registry. + pub(crate) fn new(fs: Fs) -> Self { Self { fs, - fs_context, + fs_context: litebox::fs::Context::new(), + defaults_seeded: Mutex::new(false), notification_state: Mutex::new(RegistryNotificationState::default()), notification_pollee: Pollee::new(), } } + /// Returns the backing store, seeding the built-in registry contents on first use. + /// + /// Seeding is attempted exactly once. As at startup, a failure is logged and + /// abandons the rest of the defaults rather than failing the operation that + /// triggered it, so a store that cannot be seeded still answers requests. + fn fs(&self) -> &Fs { + { + let mut seeded = self.defaults_seeded.lock(); + if !*seeded { + *seeded = true; + seed_defaults(&self.fs, &self.fs_context); + } + } + &self.fs + } + fn open_key( &self, path: &str, desired_access: RegistryKeyAccess, - ) -> Result>, NtStatus> { - self.fs - .open(&self.fs_context, path, desired_access.into(), Mode::empty()) - .map_err(map_open_error) + ) -> Result { + let (access, flags) = desired_access.open_flags(); + match self + .fs() + .open_file(&self.fs_context, path, access, flags, Mode::empty()) + { + // Registry rights are enforced by the NT handle. An immutable lower overlay + // directory can still back mutations whose child paths are copied up. + Err(OpenError::ReadOnlyFileSystem) + if matches!( + access, + FileAccessMode::WriteOnly | FileAccessMode::ReadWrite + ) => + { + self.fs().open_file( + &self.fs_context, + path, + FileAccessMode::ReadOnly, + flags, + Mode::empty(), + ) + } + result => result, + } + .map_err(map_open_error) } fn read_value_at_path( @@ -816,34 +666,34 @@ impl RegistryStore { ) -> Result { let value_path = value_path(key_path, value_name)?; let status = self - .fs - .file_status(&self.fs_context, &*value_path) + .fs() + .path_file_status(&self.fs_context, &value_path) .map_err(map_file_status_error)?; if status.file_type != FileType::RegularFile { return Err(NtStatus::OBJECT_TYPE_MISMATCH); } - if status.size < REGISTRY_VALUE_TYPE_SIZE { + if status.size < REGISTRY_VALUE_TYPE_SIZE as u64 { return Err(NtStatus::UNSUCCESSFUL); } + let size = usize::try_from(status.size).map_err(|_| NtStatus::NO_MEMORY)?; + let mut data = Vec::new(); + data.try_reserve_exact(size) + .map_err(|_| NtStatus::NO_MEMORY)?; + data.resize(size, 0); let fd = self - .fs - .open( + .fs() + .open_file( &self.fs_context, - &*value_path, - OFlags::RDONLY, + &value_path, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), ) .map_err(map_open_error)?; - let mut data = vec![0; status.size]; - let read = self - .fs - .read(&fd, &mut data, Some(0)) - .map_err(map_read_error)?; - let _ = self.fs.close(&fd); - if read != data.len() { - return Err(NtStatus::UNSUCCESSFUL); - } + let result = read_exact_at(self.fs(), &fd, &mut data); + let _ = self.fs().close_file(&fd); + result?; let value_type = u32::from_le_bytes( data[..REGISTRY_VALUE_TYPE_SIZE] @@ -863,12 +713,13 @@ impl RegistryStore { value: &[u8], ) -> Result<(), NtStatus> { write_value_at_path( - &self.fs, + self.fs(), &self.fs_context, key_path, value_name, value_type, value, + FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE, )?; self.record_change(key_path, RegistryNotifyFilter::LAST_SET); Ok(()) @@ -932,9 +783,13 @@ impl RegistryStore { self.notification_pollee.notify_observers(Events::IN); } - fn key_summary(&self, key: &RegistryKeyObject) -> Result { + fn key_summary(&self, key: &RegistryKeyObject) -> Result { let mut summary = KeySummary::default(); - for entry in self.fs.read_dir(&key.fd).map_err(map_read_dir_error)? { + for entry in self + .fs() + .read_file_directory(&self.fs_context, &key.path, &key.fd) + .map_err(map_read_dir_error)? + { if entry.file_type == FileType::Directory && entry.name != "." && entry.name != ".." @@ -947,18 +802,14 @@ impl RegistryStore { } } - let values_path = format!("{}/{}", key.path.trim_end_matches('/'), VALUES_DIR_NAME); - let values_fd = self - .fs - .open( - &self.fs_context, - &*values_path, - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .map_err(map_open_error)?; - let values = self.fs.read_dir(&values_fd).map_err(map_read_dir_error); - let _ = self.fs.close(&values_fd); + let Some((values_path, values_fd)) = self.open_values_directory(&key.path)? else { + return Ok(summary); + }; + let values = self + .fs() + .read_file_directory(&self.fs_context, &values_path, &values_fd) + .map_err(map_read_dir_error); + let _ = self.fs().close_file(&values_fd); for entry in values? { if entry.file_type != FileType::RegularFile { continue; @@ -969,20 +820,40 @@ impl RegistryStore { .max(entry.name.encode_utf16().count() * size_of::()); let path = format!("{values_path}/{}", entry.name); let size = self - .fs - .file_status(&self.fs_context, &*path) + .fs() + .path_file_status(&self.fs_context, &path) .map_err(map_file_status_error)? .size; - if size < REGISTRY_VALUE_TYPE_SIZE { + if size < REGISTRY_VALUE_TYPE_SIZE as u64 { return Err(NtStatus::UNSUCCESSFUL); } summary.max_value_data_len = summary .max_value_data_len - .max(size - REGISTRY_VALUE_TYPE_SIZE); + .max(size - REGISTRY_VALUE_TYPE_SIZE as u64); } Ok(summary) } + fn open_values_directory( + &self, + key_path: &str, + ) -> Result, NtStatus> { + let values_path = values_directory_path(key_path); + match self.fs().open_file( + &self.fs_context, + &values_path, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, + Mode::empty(), + ) { + Ok(fd) => Ok(Some((values_path, fd))), + Err(OpenError::PathError( + PathError::NoSuchFileOrDirectory | PathError::MissingComponent, + )) => Ok(None), + Err(error) => Err(map_open_error(error)), + } + } + /// Returns the deterministically-ordered leaf name of the `index`-th direct /// subkey of `key`, or `None` when `index` is out of range. /// @@ -991,11 +862,15 @@ impl RegistryStore { /// subkey names are sorted before indexing. fn nth_subkey_name( &self, - key: &RegistryKeyObject, + key: &RegistryKeyObject, index: u32, ) -> Result, NtStatus> { let mut names = Vec::new(); - for entry in self.fs.read_dir(&key.fd).map_err(map_read_dir_error)? { + for entry in self + .fs() + .read_file_directory(&self.fs_context, &key.path, &key.fd) + .map_err(map_read_dir_error)? + { if entry.file_type == FileType::Directory && entry.name != "." && entry.name != ".." @@ -1009,18 +884,15 @@ impl RegistryStore { } /// Computes a [`KeySummary`] for the named direct subkey of `key`. - fn subkey_summary( - &self, - key: &RegistryKeyObject, - name: &str, - ) -> Result { + fn subkey_summary(&self, key: &RegistryKeyObject, name: &str) -> Result { let child_path = format!("{}/{}", key.path.trim_end_matches('/'), name); let child_fd = self - .fs - .open( + .fs() + .open_file( &self.fs_context, - &*child_path, - OFlags::RDONLY | OFlags::DIRECTORY, + &child_path, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; @@ -1029,7 +901,7 @@ impl RegistryStore { fd: child_fd, }; let summary = self.key_summary(&child); - let _ = self.fs.close(&child.fd); + let _ = self.fs().close_file(&child.fd); summary } @@ -1041,21 +913,17 @@ impl RegistryStore { /// files under the key's `.values` directory) are sorted before indexing. fn nth_value_name( &self, - key: &RegistryKeyObject, + key: &RegistryKeyObject, index: u32, ) -> Result, NtStatus> { - let values_path = format!("{}/{}", key.path.trim_end_matches('/'), VALUES_DIR_NAME); - let values_fd = self - .fs - .open( - &self.fs_context, - &*values_path, - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .map_err(map_open_error)?; - let entries = self.fs.read_dir(&values_fd).map_err(map_read_dir_error); - let _ = self.fs.close(&values_fd); + let Some((values_path, values_fd)) = self.open_values_directory(&key.path)? else { + return Ok(None); + }; + let entries = self + .fs() + .read_file_directory(&self.fs_context, &values_path, &values_fd) + .map_err(map_read_dir_error); + let _ = self.fs().close_file(&values_fd); let mut names = Vec::new(); for entry in entries? { if entry.file_type == FileType::RegularFile { @@ -1067,12 +935,181 @@ impl RegistryStore { } } +/// Writes the built-in registry keys and values into `fs`. +/// +/// Registry startup is best-effort: the first failure is logged and abandons the +/// remaining defaults in that group, matching the behavior guests saw when the +/// defaults were written during shim construction. +fn seed_defaults( + fs: &Fs, + fs_context: &litebox::fs::Context, +) { + for key in [ + DEFAULT_SESSION_MANAGER_KEY, + DEFAULT_SEGMENT_HEAP_KEY, + DEFAULT_IMAGE_FILE_EXECUTION_OPTIONS_KEY, + DEFAULT_WINSOCK_PARAMETERS_KEY, + DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, + DEFAULT_WINSOCK_IPV4_TCP_ENTRY_KEY, + DEFAULT_WINSOCK_IPV4_UDP_ENTRY_KEY, + DEFAULT_WINSOCK_IPV6_TCP_ENTRY_KEY, + DEFAULT_WINSOCK_IPV6_UDP_ENTRY_KEY, + DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + ] { + if let Err(status) = create_key_in_fs(fs, fs_context, key) { + litebox_util_log::error!(key:% = key, status:? = status; "failed to initialize registry key"); + break; + } + } + for (name, value) in [ + ("ACP", DEFAULT_ACP_VALUE), + ("OEMCP", DEFAULT_OEMCP_VALUE), + ("MACCP", DEFAULT_MACCP_VALUE), + ] { + if let Err(status) = write_value_in_fs( + fs, + fs_context, + DEFAULT_CODE_PAGE_KEY, + name, + RegistryValueType::Sz, + value, + ) { + litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize registry value"); + break; + } + } + + let mut winsock_values = vec![ + ( + DEFAULT_WINSOCK_PARAMETERS_KEY, + "WinSock_Registry_Version", + RegistryValueType::Sz, + utf16le_nul("2.0"), + ), + ( + DEFAULT_WINSOCK_PARAMETERS_KEY, + "Current_Protocol_Catalog", + RegistryValueType::Sz, + utf16le_nul("Protocol_Catalog9"), + ), + ( + DEFAULT_WINSOCK_PARAMETERS_KEY, + "Current_NameSpace_Catalog", + RegistryValueType::Sz, + utf16le_nul("NameSpace_Catalog5"), + ), + ( + DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, + "Num_Catalog_Entries64", + RegistryValueType::Dword, + WINSOCK_PROTOCOL_CATALOG_ENTRY_COUNT.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, + "Next_Catalog_Entry_ID", + RegistryValueType::Dword, + WINSOCK_NEXT_PROTOCOL_CATALOG_ENTRY_ID + .to_le_bytes() + .to_vec(), + ), + ( + DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, + "Serial_Access_Num", + RegistryValueType::Dword, + WINSOCK_INITIAL_CATALOG_SERIAL.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, + "Num_Catalog_Entries64", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_CATALOG_ENTRY_COUNT.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, + "Serial_Access_Num", + RegistryValueType::Dword, + WINSOCK_INITIAL_CATALOG_SERIAL.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "LibraryPath", + RegistryValueType::Sz, + utf16le_nul("%SystemRoot%\\System32\\mswsock.dll"), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "DisplayString", + RegistryValueType::Sz, + utf16le_nul("@%SystemRoot%\\system32\\wshtcpip.dll,-60103"), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "ProviderId", + RegistryValueType::Binary, + WINSOCK_NAMESPACE_PROVIDER_ID.to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "SupportedNameSpace", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_DNS.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "Enabled", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_PROVIDER_ENABLED.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "Version", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_PROVIDER_VERSION.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "StoresServiceClassInfo", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_STORES_SERVICE_CLASS_INFO + .to_le_bytes() + .to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "ProviderInfo", + RegistryValueType::Binary, + Vec::new(), + ), + ]; + for protocol in &DEFAULT_WINSOCK_PROTOCOLS { + winsock_values.push(( + protocol.entry_key, + "PackedCatalogItem", + RegistryValueType::Binary, + default_winsock_protocol_catalog_item(protocol), + )); + winsock_values.push(( + protocol.entry_key, + "ProtocolName", + RegistryValueType::Sz, + utf16le_nul(protocol.protocol_name), + )); + } + for (key, name, value_type, value) in winsock_values { + if let Err(status) = write_value_in_fs(fs, fs_context, key, name, value_type, &value) { + litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize Winsock registry value"); + break; + } + } +} + impl Task { fn registry_key_entry( &self, handle: Handle, - ) -> Result>, NtStatus> { - raw_handle_entry::>( + ) -> Result, NtStatus> { + raw_handle_entry::( &self.global.litebox, &self.process.handles, handle, @@ -1082,26 +1119,22 @@ impl Task { fn insert_registry_key_handle( &self, - key: RegistryKeyObject, + key: RegistryKeyObject, granted_access: RegistryKeyAccess, ) -> Result { - self.insert_typed_handle::>( - key, - granted_access.bits(), - |key| { - self.close_registry_key(key); - }, - ) + self.insert_typed_handle::(key, granted_access.bits(), |key| { + self.close_registry_key(key); + }) } pub(crate) fn close_registry_key_handle(&self, handle: Handle) { - self.close_typed_handle::>(handle, |key| { + self.close_typed_handle::(handle, |key| { self.close_registry_key(key); }); } - pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { - let _ = self.global.registry.fs.close(&key.fd); + pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { + let _ = self.global.registry.fs().close_file(&key.fd); } pub(crate) fn sys_nt_open_key( @@ -1239,8 +1272,8 @@ impl Task { let disposition = match self .global .registry - .fs - .file_status(&self.global.registry.fs_context, &path) + .fs() + .path_file_status(&self.global.registry.fs_context, &path) { Ok(status) if status.file_type == FileType::Directory => { RegistryKeyDisposition::OpenedExistingKey @@ -1250,7 +1283,7 @@ impl Task { PathError::NoSuchFileOrDirectory | PathError::MissingComponent, )) => { for created_path in create_key_path_in_fs( - &self.global.registry.fs, + self.global.registry.fs(), &self.global.registry.fs_context, &path, )? { @@ -1392,7 +1425,7 @@ impl Task { length: u32, result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = self.typed_handle_entry_with_access::>( + let key = self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::QUERY_VALUE.bits(), )?; @@ -1438,7 +1471,7 @@ impl Task { data: Option>, data_size: u32, ) -> NtStatus { - let key = match self.typed_handle_entry_with_access::>( + let key = match self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::SET_VALUE.bits(), ) { @@ -1484,7 +1517,7 @@ impl Task { &self, params: NtNotifyChangeKeyRequest, ) -> NtStatus { - let key = match self.typed_handle_entry_with_access::>( + let key = match self.typed_handle_entry_with_access::( params.key_handle, RegistryKeyAccess::NOTIFY.bits(), ) { @@ -1650,9 +1683,9 @@ impl Task { ) -> Result<(), NtStatus> { let key = if key_information_class == KeyInformationClass::Name { // Windows permits KeyNameInformation with any nonzero granted access. - self.typed_handle_entry_with_any_access::>(key_handle)? + self.typed_handle_entry_with_any_access::(key_handle)? } else { - self.typed_handle_entry_with_access::>( + self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::QUERY_VALUE.bits(), )? @@ -1727,7 +1760,8 @@ impl Task { max_class_len: 0, values: summary.values.trunc(), max_value_name_len: summary.max_value_name_len.trunc(), - max_value_data_len: summary.max_value_data_len.trunc(), + max_value_data_len: u32::try_from(summary.max_value_data_len) + .map_err(|_| NtStatus::UNSUCCESSFUL)?, class: [], }; write_query_information::( @@ -1775,7 +1809,8 @@ impl Task { max_name_len: summary.max_name_len.trunc(), values: summary.values.trunc(), max_value_name_len: summary.max_value_name_len.trunc(), - max_value_data_len: summary.max_value_data_len.trunc(), + max_value_data_len: u32::try_from(summary.max_value_data_len) + .map_err(|_| NtStatus::UNSUCCESSFUL)?, name_length: leaf_name.len().trunc(), padding: [0; 4], }; @@ -1871,7 +1906,7 @@ impl Task { length: u32, result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = self.typed_handle_entry_with_access::>( + let key = self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::ENUMERATE_SUB_KEYS.bits(), )?; @@ -1945,7 +1980,8 @@ impl Task { max_class_len: 0, values: summary.values.trunc(), max_value_name_len: summary.max_value_name_len.trunc(), - max_value_data_len: summary.max_value_data_len.trunc(), + max_value_data_len: u32::try_from(summary.max_value_data_len) + .map_err(|_| NtStatus::UNSUCCESSFUL)?, class: [], }; write_query_information::( @@ -1980,7 +2016,7 @@ impl Task { length: u32, result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = self.typed_handle_entry_with_access::>( + let key = self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::QUERY_VALUE.bits(), )?; @@ -2192,11 +2228,20 @@ fn default_winsock_protocol_catalog_item(protocol: &DefaultWinsockProtocol) -> V } fn absolute_nt_key_name_to_fs_path(name: &str) -> Result { - if !name.starts_with('\\') { + let Some(name) = name.strip_prefix('\\') else { return Err(NtStatus::INVALID_PARAMETER); + }; + let (root, remaining) = name + .split_once('\\') + .map_or((name, None), |(root, remaining)| (root, Some(remaining))); + if !root.eq_ignore_ascii_case(REGISTRY_NT_ROOT.trim_start_matches('\\')) { + return Err(NtStatus::INVALID_PARAMETER); + } + + let mut path = String::from(REGISTRY_ROOT); + if let Some(remaining) = remaining { + append_registry_components(&mut path, remaining)?; } - let mut path = String::from("/"); - append_registry_components(&mut path, name.trim_start_matches('\\'))?; Ok(path) } @@ -2234,53 +2279,98 @@ fn is_valid_key_component(component: &str) -> bool { } fn write_value_in_fs( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &Fs, + context: &litebox::fs::Context, key_nt_path: &str, value_name: &str, value_type: RegistryValueType, value: &[u8], ) -> Result<(), NtStatus> { let key_path = create_key_in_fs(fs, context, key_nt_path)?; - write_value_at_path(fs, context, &key_path, value_name, value_type.into(), value) + match write_value_at_path( + fs, + context, + &key_path, + value_name, + value_type.into(), + value, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + ) { + Err(NtStatus::OBJECT_NAME_COLLISION) => Ok(()), + result => result, + } } fn write_value_at_path( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &Fs, + context: &litebox::fs::Context, key_path: &str, value_name: &str, value_type: u32, value: &[u8], + flags: FileOpenFlags, ) -> Result<(), NtStatus> { + let values_path = values_directory_path(key_path); + ensure_directory_in_fs(fs, context, &values_path)?; let value_path = value_path(key_path, value_name)?; let fd = fs - .open( + .open_file( context, - &*value_path, - OFlags::CREAT | OFlags::WRONLY | OFlags::TRUNC, + &value_path, + FileAccessMode::WriteOnly, + flags, Mode::RUSR | Mode::WUSR | Mode::ROTH | Mode::WOTH, ) .map_err(map_open_error)?; - let written = fs - .write(&fd, &value_type.to_le_bytes(), Some(0)) - .map_err(map_write_error)?; - if written != REGISTRY_VALUE_TYPE_SIZE { - return Err(NtStatus::DISK_FULL); - } - let written = fs - .write(&fd, value, Some(REGISTRY_VALUE_TYPE_SIZE)) - .map_err(map_write_error)?; - if written != value.len() { - return Err(NtStatus::DISK_FULL); - } - let _ = fs.close(&fd); + let result = (|| { + write_all_at(fs, &fd, &value_type.to_le_bytes(), 0)?; + write_all_at(fs, &fd, value, REGISTRY_VALUE_TYPE_SIZE) + })(); + let _ = fs.close_file(&fd); + result +} + +fn read_exact_at( + fs: &Fs, + fd: &litebox::fs::FileFd, + mut data: &mut [u8], +) -> Result<(), NtStatus> { + let mut offset = 0; + while !data.is_empty() { + let read = fs + .read_file(fd, data, Some(offset)) + .map_err(map_read_error)?; + if read == 0 { + return Err(NtStatus::UNSUCCESSFUL); + } + offset = offset.checked_add(read).ok_or(NtStatus::UNSUCCESSFUL)?; + data = &mut data[read..]; + } + Ok(()) +} + +fn write_all_at( + fs: &Fs, + fd: &litebox::fs::FileFd, + mut data: &[u8], + mut offset: usize, +) -> Result<(), NtStatus> { + while !data.is_empty() { + let written = fs + .write_file(fd, data, Some(offset)) + .map_err(map_write_error)?; + if written == 0 { + return Err(NtStatus::DISK_FULL); + } + offset = offset.checked_add(written).ok_or(NtStatus::DISK_FULL)?; + data = &data[written..]; + } Ok(()) } fn create_key_in_fs( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &Fs, + context: &litebox::fs::Context, nt_path: &str, ) -> Result { let path = absolute_nt_key_name_to_fs_path(nt_path)?; @@ -2289,8 +2379,8 @@ fn create_key_in_fs( } fn create_key_path_in_fs( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &Fs, + context: &litebox::fs::Context, path: &str, ) -> Result, NtStatus> { let mut current = String::new(); @@ -2314,16 +2404,16 @@ fn create_key_path_in_fs( } fn ensure_directory_in_fs( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &Fs, + context: &litebox::fs::Context, path: &str, ) -> Result { - match fs.file_status(context, path) { + match fs.path_file_status(context, path) { Ok(status) if status.file_type == FileType::Directory => Ok(false), Ok(_) => Err(NtStatus::OBJECT_TYPE_MISMATCH), Err(FileStatusError::PathError( PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - )) => match fs.mkdir( + )) => match fs.mkdir_file( context, path, Mode::RUSR | Mode::WUSR | Mode::XUSR | Mode::ROTH | Mode::WOTH | Mode::XOTH, @@ -2344,16 +2434,16 @@ fn value_path(key_path: &str, value_name: &str) -> Result { return Err(NtStatus::INVALID_PARAMETER); } - let mut path = String::from(key_path); - if !path.ends_with('/') { - path.push('/'); - } - path.push_str(VALUES_DIR_NAME); + let mut path = values_directory_path(key_path); path.push('/'); path.push_str(&value_name.to_ascii_lowercase()); Ok(path) } +fn values_directory_path(key_path: &str) -> String { + format!("{}/{}", key_path.trim_end_matches('/'), VALUES_DIR_NAME) +} + fn is_valid_value_name(value_name: &str) -> bool { !value_name.is_empty() && value_name != "." @@ -2417,12 +2507,15 @@ fn map_read_error(error: ReadError) -> NtStatus { fn map_read_dir_error(error: ReadDirError) -> NtStatus { match error { ReadDirError::NotADirectory => NtStatus::NOT_A_DIRECTORY, + ReadDirError::NotForReading => NtStatus::ACCESS_DENIED, _ => NtStatus::UNSUCCESSFUL, } } #[cfg(test)] mod tests { + use alloc::sync::Arc; + use crate::tests::{ TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, object_attributes, test_platform, unicode_string, utf16_units as utf16, @@ -2431,6 +2524,7 @@ mod tests { use super::*; use core::mem::size_of; use litebox::LiteBox; + use litebox_broker_protocol::fs::{FileMode, FileUser}; extern crate std; @@ -2492,12 +2586,84 @@ mod tests { fn RegDeleteTreeW(hKey: *mut core::ffi::c_void, lpSubKey: *const u16) -> i32; } - fn test_registry() -> (LiteBox, RegistryStore) { - let litebox = LiteBox::new(test_platform()); - let registry = RegistryStore::new(&litebox); + /// Returns a registry store backed by a broker core that owns an empty registry hive. + /// + /// The store's defaults are written on first use, so callers observe them through any + /// registry operation, exactly as a guest does. + fn test_registry() -> (Arc>, RegistryStore) { + let mode = FileMode::RWXU | FileMode::RWXG | FileMode::RWXO; + let litebox = Arc::new(crate::test_broker::litebox_with_broker_files( + test_platform(), + alloc::vec![ + ( + "/".into(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode, + owner: FileUser::ROOT, + }, + ), + ( + REGISTRY_ROOT.into(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode, + owner: FileUser::ROOT, + }, + ), + ], + )); + let registry = RegistryStore::new(Fs::registry(Arc::clone(&litebox))); (litebox, registry) } + #[test] + fn registry_paths_are_confined_to_the_reserved_backing_root() { + assert_eq!( + absolute_nt_key_name_to_fs_path(r"\Registry").unwrap(), + REGISTRY_ROOT + ); + assert_eq!( + absolute_nt_key_name_to_fs_path(r"\REGISTRY\Machine\Software").unwrap(), + "/registry/machine/software" + ); + assert_eq!( + relative_nt_key_name_to_fs_path("/registry/machine", r"Software\LiteBox").unwrap(), + "/registry/machine/software/litebox" + ); + + for path in [ + "", + "Registry", + r"\\Registry", + r"\Machine", + r"\RegistrySibling", + r"\Registry\", + r"\Registry\.", + r"\Registry\..", + r"\Registry\Machine//Software", + ] { + assert_eq!( + absolute_nt_key_name_to_fs_path(path), + Err(NtStatus::INVALID_PARAMETER), + "{path}" + ); + } + } + + #[test] + fn registry_syscalls_reject_non_registry_absolute_roots() { + let task = crate::tests::test_task_with_broker_files(&[]); + for path in [r"\Machine\Software", r"\RegistrySibling\Software"] { + let path_utf16 = utf16(path); + let path_name = unicode_string(&path_utf16); + let object_attributes = object_attributes(&path_name, 0); + assert_eq!( + task.do_nt_open_key(RegistryKeyAccess::READ.bits(), object_attributes), + Err(NtStatus::INVALID_PARAMETER), + "{path}" + ); + } + } + fn open_key( task: &Task, object_attributes: ObjectAttributes, @@ -2665,30 +2831,39 @@ mod tests { #[test] fn registry_store_separates_values_from_subkeys() { - let (_litebox, registry) = test_registry(); + let (litebox, registry) = test_registry(); let key_path = absolute_nt_key_name_to_fs_path(DEFAULT_CODE_PAGE_KEY).unwrap(); let value_path = value_path(&key_path, "ACP").unwrap(); assert_eq!( registry - .fs - .file_status(®istry.fs_context, &*value_path) + .fs() + .path_file_status(®istry.fs_context, &value_path) .unwrap() .file_type, FileType::RegularFile ); assert_eq!( registry - .fs - .file_status(®istry.fs_context, &*value_path) + .fs() + .path_file_status(®istry.fs_context, &value_path) .unwrap() .size, - REGISTRY_VALUE_TYPE_SIZE + DEFAULT_ACP_VALUE.len() + (REGISTRY_VALUE_TYPE_SIZE + DEFAULT_ACP_VALUE.len()) as u64 ); let value = registry.read_value_at_path(&key_path, "ACP").unwrap(); assert_eq!(value.value_type, u32::from(RegistryValueType::Sz)); assert_eq!(value.data, DEFAULT_ACP_VALUE); + let broker_fs = litebox; + assert_eq!( + broker_fs + .path_file_status(&litebox::fs::Context::new(), value_path.as_str()) + .unwrap() + .file_type, + FileType::RegularFile + ); + let values_dir = absolute_nt_key_name_to_fs_path( "\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nls\\CodePage\\.values", ); @@ -2697,7 +2872,7 @@ mod tests { #[test] fn nt_create_key_reports_disposition_and_created_key_is_queryable() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_name = r"\Registry\Machine\Software\LiteBoxCreatedKey"; let key_name_utf16 = utf16(key_name); let key_name = unicode_string(&key_name_utf16); @@ -2802,7 +2977,7 @@ mod tests { #[test] fn nt_enumerate_key_lists_subkeys_in_stable_sorted_order() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let create_key = |path: &str| -> Handle { let name_utf16 = utf16(path); @@ -2896,7 +3071,7 @@ mod tests { #[test] fn nt_set_value_key_replaces_and_round_trips_raw_types_and_empty_data() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_name_utf16 = utf16(r"\Registry\Machine\Software\LiteBoxSetValueKey"); let key_name = unicode_string(&key_name_utf16); let object_attributes = object_attributes(&key_name, 0); @@ -2995,7 +3170,7 @@ mod tests { #[cfg(all(target_os = "windows", target_arch = "x86_64"))] #[test] fn registry_default_code_page_values_match_host() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); for name in ["ACP", "OEMCP", "MACCP"] { @@ -3030,7 +3205,7 @@ mod tests { #[test] fn nt_open_key_opens_existing_absolute_and_relative_keys() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let nls_name = utf16("\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nls"); let nls_name = unicode_string(&nls_name); let nls_object_attributes = object_attributes(&nls_name, 0); @@ -3048,7 +3223,7 @@ mod tests { #[test] fn nt_open_key_reports_missing_absolute_key() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let name = utf16("\\Registry\\Machine\\Software\\Missing"); let name = unicode_string(&name); let object_attributes = object_attributes(&name, 0); @@ -3062,7 +3237,7 @@ mod tests { fn synchronous_nt_notify_change_key_completes_after_matching_mutation() { use std::time::Duration; - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_name_utf16 = utf16(r"\Registry\Machine\Software\LiteBoxSynchronousNotify"); let key_name = unicode_string(&key_name_utf16); let object_attributes = object_attributes(&key_name, 0); @@ -3115,7 +3290,7 @@ mod tests { fn synchronous_nt_notify_change_key_completes_after_subkey_creation() { use std::time::Duration; - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_name_utf16 = utf16(r"\Registry\Machine\Software\LiteBoxSynchronousNameNotify"); let key_name = unicode_string(&key_name_utf16); let parent_attributes = object_attributes(&key_name, 0); @@ -3183,20 +3358,20 @@ mod tests { #[test] fn nt_open_key_checks_backing_fs_permissions() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let private_key = "\\Registry\\Machine\\Software\\Private"; let private_path = create_key_in_fs( - &task.global.registry.fs, + task.global.registry.fs(), &task.global.registry.fs_context, private_key, ) .unwrap(); task.global .registry - .fs - .chmod( + .fs() + .chmod_file( &task.global.registry.fs_context, - &*private_path, + &private_path, Mode::WUSR | Mode::XUSR, ) .unwrap(); @@ -3220,7 +3395,7 @@ mod tests { #[test] fn nt_close_removes_registry_key_handle() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let value_name = utf16("ACP"); let value_name = unicode_string(&value_name); @@ -3256,7 +3431,7 @@ mod tests { #[test] fn nt_query_value_key_reports_partial_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let value_name = utf16("ACP"); let value_name = unicode_string(&value_name); @@ -3293,7 +3468,7 @@ mod tests { fn nt_query_value_key_without_query_access_matches_host() { assert_eq!(host_query_value_with_set_only_access(), ERROR_ACCESS_DENIED); - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let code_page_name = utf16(DEFAULT_CODE_PAGE_KEY); let code_page_name = unicode_string(&code_page_name); let object_attributes = object_attributes(&code_page_name, 0); @@ -3321,7 +3496,7 @@ mod tests { #[test] fn nt_query_value_key_reports_basic_and_full_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let value_name = utf16("OEMCP"); let value_name = unicode_string(&value_name); @@ -3381,7 +3556,7 @@ mod tests { #[test] fn nt_enumerate_value_key_lists_values_in_stable_sorted_order() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); // The code-page key is seeded with ACP, OEMCP, and MACCP values, which // are stored lower-cased and therefore enumerate as acp, maccp, oemcp. let key_handle = open_code_page_key(&task); @@ -3461,7 +3636,7 @@ mod tests { #[test] fn nt_query_value_key_rejects_invalid_arguments() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let value_name = utf16("ACP"); let value_name = unicode_string(&value_name); @@ -3547,7 +3722,7 @@ mod tests { #[test] fn nt_query_key_reports_full_and_cached_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let mut full_bytes = [0u8; 64]; diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index cfba9d4c7c..9d260fd675 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -73,7 +73,7 @@ pub(crate) struct SectionHandleObject { pub(crate) struct SectionObject { fs_path: Option, - size: usize, + size: u64, attributes: SectionAllocationAttributes, protection: PageProtection, backing: SectionBacking, @@ -315,7 +315,7 @@ impl Task { }; let section = Arc::new(SectionObject { fs_path: None, - size, + size: size as u64, attributes, protection, backing: SectionBacking::Pagefile, @@ -361,7 +361,7 @@ impl Task { }; let section = Arc::new(SectionObject { fs_path: Some(fs_path), - size: metadata.file_size as usize, + size: metadata.file_size, attributes: SectionAllocationAttributes::SEC_FILE | SectionAllocationAttributes::SEC_IMAGE, protection: PageProtection::PAGE_EXECUTE_WRITECOPY, @@ -471,7 +471,7 @@ impl Task { fs_path:% = fs_path; "NtOpenSection: creating section for KnownDlls image" ); - let Ok(file_status) = self.fs.file_status(&self.fs_context, &fs_path) else { + let Ok(file_status) = self.fs.path_file_status(&self.fs_context, &fs_path) else { return NtStatus::OBJECT_NAME_NOT_FOUND; }; let section = Arc::new(SectionObject { @@ -723,16 +723,16 @@ impl Task { page_protection: PageProtection, permissions: MemoryRegionPermissions, ) -> Result { - if section_offset > section.size { - return Err(NtStatus::INVALID_VIEW_SIZE); - } - let remaining = section.size - section_offset; + let remaining = section + .size + .checked_sub(section_offset as u64) + .ok_or(NtStatus::INVALID_VIEW_SIZE)?; let view_size = if requested_view_size == 0 { - remaining + usize::try_from(remaining).map_err(|_| NtStatus::INVALID_VIEW_SIZE)? } else { requested_view_size }; - if view_size == 0 || view_size > remaining { + if view_size == 0 || view_size as u64 > remaining { return Err(NtStatus::INVALID_VIEW_SIZE); } let mapped_size = view_size @@ -837,11 +837,14 @@ impl Task { return NtStatus::INVALID_VIEW_SIZE; } let view_size = if requested_view_size == 0 { - section.size + let Ok(size) = usize::try_from(section.size) else { + return NtStatus::INVALID_VIEW_SIZE; + }; + size } else { requested_view_size }; - if view_size == 0 || view_size > section.size { + if view_size == 0 || view_size as u64 > section.size { return NtStatus::INVALID_VIEW_SIZE; } let Some(mapped_size) = view_size.checked_next_multiple_of(PAGE_SIZE) else { @@ -1044,7 +1047,7 @@ pub(crate) fn load_time_windows_shared_section( // pointers instead of exposing a zeroed generic pagefile section. Arc::new(SectionObject { fs_path: None, - size: WINDOWS_SHARED_SECTION_SIZE, + size: WINDOWS_SHARED_SECTION_SIZE as u64, attributes: SectionAllocationAttributes::SEC_COMMIT, protection: PageProtection::PAGE_READWRITE, backing: SectionBacking::CsrSharedSection { base }, @@ -1181,7 +1184,7 @@ fn write_section_basic_information( fn write_section_image_information( section: &SectionObject, - fs: Arc>, + fs: Arc>, section_information: MutPtr, section_information_length: usize, return_length: Option>, @@ -1201,6 +1204,9 @@ fn write_section_image_information( Err(crate::loader::WindowsLoadError::Access(_)) => return NtStatus::OBJECT_NAME_NOT_FOUND, Err(_) => return NtStatus::INVALID_FILE_FOR_SECTION, }; + let Ok(image_file_size) = u32::try_from(metadata.file_size) else { + return NtStatus::SECTION_TOO_BIG; + }; // 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 { @@ -1219,7 +1225,7 @@ fn write_section_image_information( image_contains_code: 1, image_flags: 0, loader_flags: 0, - image_file_size: metadata.file_size, + image_file_size, checksum: 0, }; let output = @@ -1269,7 +1275,9 @@ mod tests { use super::*; use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::syscalls::event::EventType; - use crate::tests::{TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task}; + use crate::tests::{ + TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task, test_task_with_broker_files, + }; #[cfg(all(target_os = "windows", target_arch = "x86_64"))] const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664; @@ -1484,8 +1492,10 @@ mod tests { #[test] fn nt_query_section_image_information_uses_pe_headers() { let image = host_kernel32_image(); - let task = - crate::tests::test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); + let task = crate::tests::test_task_with_broker_files(&[( + "/Windows/System32/kernel32.dll", + &image, + )]); let name = wide(r"\KnownDlls\kernel32.dll"); let unicode = unicode(&name); let attrs = object_attributes(&unicode); @@ -1540,7 +1550,7 @@ mod tests { #[test] fn nt_create_section_maps_file_backed_image() { let image = host_kernel32_image(); - let task = crate::tests::test_task_with_nls_files(&[("/tmp/kernel32.dll", &image)]); + let task = crate::tests::test_task_with_broker_files(&[("/tmp/kernel32.dll", &image)]); let file_handle = open_image_file(&task, r"\Device\HarddiskVolume1\tmp\kernel32.dll"); let mut section_handle = Handle::default(); @@ -1605,8 +1615,10 @@ mod tests { #[test] fn image_section_rejects_writable_view_protection() { let image = host_kernel32_image(); - let task = - crate::tests::test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); + let task = crate::tests::test_task_with_broker_files(&[( + "/Windows/System32/kernel32.dll", + &image, + )]); let name = wide(r"\KnownDlls\kernel32.dll"); let unicode = unicode(&name); let attrs = object_attributes(&unicode); @@ -1664,7 +1676,7 @@ mod tests { #[test] fn section_output_handles_follow_host_probe_contracts() { - let task = test_task(); + let task = test_task_with_broker_files(&[]); let name = wide(r"\KnownDlls\DefinitelyMissingLiteBoxProbe.dll"); let unicode = unicode(&name); let attrs = object_attributes(&unicode); diff --git a/litebox_shim_windows/src/test_broker.rs b/litebox_shim_windows/src/test_broker.rs new file mode 100644 index 0000000000..6dd4aa9687 --- /dev/null +++ b/litebox_shim_windows/src/test_broker.rs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Broker endpoints for the Windows shim's unit tests. +//! +//! The shim owns the guest side of the guest/broker boundary: NT syscall argument validation, +//! object and handle bookkeeping, path and flag translation, and error translation. Broker +//! authority — policy and filesystem semantics — belongs to `litebox_broker_core` and is tested +//! there. +//! +//! Ordinary shim tests therefore use [`litebox`], whose association negotiates the protocol and +//! owns shared memory but serves no objects: any request it receives is a bug in the test or in +//! the shim, and panics. The few tests that genuinely exercise file-backed behavior (registry +//! persistence and defaults, NLS section mapping, file syscalls, and file-backed sections) use +//! [`litebox_with_broker_files`], which owns a broker core for the test process. + +extern crate std; + +use alloc::{string::String, sync::Arc, vec::Vec}; + +use litebox_broker_core::{ + ObjectRights, PolicyEngine, + fs::{in_mem::InitialNode, resolver::Resolver}, + test_support::TestBrokerCoreBuilder, +}; +use litebox_broker_host::test_support::{InProcessBrokerSetup, shared_memory}; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::{ + BROKER_PROTOCOL_VERSION, + message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, + BrokerResponse, + }, +}; +use litebox_broker_transport::{ + channel::{LocalCallChannel, LocalSetupChannel}, + shared_memory::SharedMemory, +}; + +use crate::tests::TestPlatform; + +/// Returns a LiteBox whose broker association serves no objects. +/// +/// The association negotiates the protocol and owns real shared memory, so the local side of the +/// boundary behaves normally, but every request panics. This keeps tests that are not about +/// broker-backed resources honest about what they exercise. +pub(crate) fn litebox(platform: &'static TestPlatform) -> litebox::LiteBox { + let channel = ObjectlessChannel { + memory: shared_memory(), + }; + let (broker_local, ()) = BrokerLocal::negotiate(channel, |channel| { + let memory = Arc::clone(&channel.memory); + Ok((channel, memory, ())) + }) + .expect("the objectless broker fixture must negotiate"); + litebox::LiteBox::new_with_broker_local(platform, broker_local) +} + +/// The local end of an association that owns shared memory but no objects. +struct ObjectlessChannel { + memory: Arc, +} + +impl LocalSetupChannel for ObjectlessChannel { + type Error = core::convert::Infallible; + + fn send_handshake_request( + &mut self, + request: &BrokerHandshakeRequest, + ) -> core::result::Result<(), Self::Error> { + assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); + Ok(()) + } + + fn recv_handshake_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + })) + } +} + +impl LocalCallChannel for ObjectlessChannel { + type Error = core::convert::Infallible; + + fn call(&self, request: BrokerRequest) -> core::result::Result { + match request.operation { + BrokerOperation::File(request) => panic!( + "this task's broker serves no files; tests that need them must build their task \ + with `crate::tests::test_task_with_broker_files`: {request:?}" + ), + operation => panic!("this task's broker serves no objects: {operation:?}"), + } + } +} + +/// Returns a LiteBox associated with a broker core that serves `entries` from memory. +/// +/// # Panics +/// +/// Panics if a broker core already exists in this process. The broker core is a process +/// singleton, so at most one file-backed task may be built per test binary invocation; +/// `cargo nextest`, the supported runner, gives each test its own process. +pub(crate) fn litebox_with_broker_files( + platform: &'static TestPlatform, + entries: Vec<(String, InitialNode)>, +) -> litebox::LiteBox { + let in_mem = litebox_broker_core::fs::in_mem::InMem::::new_initialized(entries); + let fs = litebox_broker_core::fs::composer::Composer::builder() + .mount("/", |_| in_mem) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .unwrap(); + let broker = TestBrokerCoreBuilder::new(PolicyEngine::with_unauthenticated_rights( + ObjectRights::all(), + )) + .with_file_service(Arc::new(Resolver::::new(fs))) + .build() + .expect("a test process may build only one broker core"); + + let setup = InProcessBrokerSetup::new(broker); + let readiness = setup.readiness_sink(); + let (broker_local, ()) = BrokerLocal::negotiate(setup, |setup| { + let memory = setup.shared_memory(); + Ok((setup.activate(), memory, ())) + }) + .unwrap(); + let litebox = litebox::LiteBox::new_with_broker_local(platform, broker_local); + readiness.attach(litebox.broker_notification_dispatcher()); + litebox +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index c4ea7a7bfa..e4eed374f4 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -1,14 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +//! Shared fixtures and cross-cutting unit tests for the Windows shim. +//! +//! [`test_task`] builds a task over a broker association that serves no objects, so the shim's +//! unit tests exercise guest and shim code rather than broker authority. Only tests that are +//! genuinely about file-backed behavior use [`test_task_with_broker_files`]; see +//! [`crate::test_broker`]. + extern crate std; use alloc::sync::Arc; use alloc::vec::Vec; use core::mem::size_of; -use litebox::fs::{Mode, OFlags}; use litebox::platform::RawConstPointer as _; use litebox::utils::TruncateExt as _; +use litebox_broker_core::fs::in_mem::InitialNode; +use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::syscalls::Handle; @@ -112,74 +120,67 @@ fn map_csr_server_shared_memory( } pub(crate) fn test_task() -> Task { - test_task_with_nls_files(&[]) + test_task_from_litebox(crate::test_broker::litebox(test_platform())) } -pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task { - let platform = test_platform(); - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let shim_builder = crate::WindowsShimBuilder::::new(platform); - let fs = Arc::new(shim_builder.default_fs(in_mem, litebox::fs::tar_ro::EMPTY_TAR_FILE.into())); - let fs_context = litebox::fs::resolver::Context::new(); - { - let fs = &*fs; - fs.mkdir( - &fs_context, - "/tmp", - litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - ) - .expect("/tmp creation cannot fail on a fresh in-memory file system"); - fs.chown(&fs_context, "/tmp", Some(1000), Some(1000)) - .expect("/tmp chown cannot fail on a fresh in-memory file system"); - - if !nls_files.is_empty() { - fs.mkdir( - &fs_context, - "/Windows", - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("/Windows creation cannot fail on a fresh in-memory file system"); - fs.mkdir( - &fs_context, - "/Windows/System32", - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("/Windows/System32 creation cannot fail on a fresh in-memory file system"); - fs.mkdir( - &fs_context, - "/Windows/Globalization", - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("/Windows/Globalization creation cannot fail on a fresh in-memory file system"); - fs.mkdir( - &fs_context, - "/Windows/Globalization/Sorting", - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("/Windows/Globalization/Sorting creation cannot fail on a fresh in-memory file system"); - } - for (path, bytes) in nls_files { - let fd = fs - .open( - &fs_context, - *path, - OFlags::WRONLY | OFlags::CREAT, - Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::ROTH, - ) - .expect("NLS fixture creation should succeed"); - fs.write(&fd, bytes, Some(0)) - .expect("NLS fixture write should succeed"); - fs.close(&fd).expect("NLS fixture close should succeed"); - } +/// Returns a task whose broker serves `files` from an in-memory filesystem. +/// +/// Reserved for tests that genuinely exercise file-backed behavior. Every other test must use +/// [`test_task`], whose broker serves no files at all. The broker core is a process singleton, so +/// exactly one such task may be built per test process; `cargo nextest` runs each test in its own +/// process. +pub(crate) fn test_task_with_broker_files(files: &[(&str, &[u8])]) -> Task { + let directory = |owner| InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner, + }; + let mut entries = alloc::vec![ + ("/".into(), directory(UserInfo::ROOT)), + ( + "/tmp".into(), + directory(UserInfo { + user: 1000, + group: 1000, + }), + ), + (crate::fs::REGISTRY_ROOT.into(), directory(UserInfo::ROOT)), + ]; + if !files.is_empty() { + entries.extend([ + ("/Windows".into(), directory(UserInfo::ROOT)), + ("/Windows/System32".into(), directory(UserInfo::ROOT)), + ("/Windows/Globalization".into(), directory(UserInfo::ROOT)), + ( + "/Windows/Globalization/Sorting".into(), + directory(UserInfo::ROOT), + ), + ]); } + entries.extend(files.iter().map(|(path, bytes)| { + ( + (*path).into(), + InitialNode::File { + mode: Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::ROTH, + owner: UserInfo::ROOT, + data: (*bytes).to_vec().into(), + }, + ) + })); + + test_task_from_litebox(crate::test_broker::litebox_with_broker_files( + test_platform(), + entries, + )) +} + +fn test_task_from_litebox(litebox: litebox::LiteBox) -> Task { + let platform = test_platform(); + let shim_builder = + crate::WindowsShimBuilder::::new_with_litebox(platform, litebox); + let fs_context = litebox::fs::Context::new(); let shim = shim_builder.build(); let WindowsShim(global) = shim; + let fs = Arc::clone(&global.fs); let windows_shared_section_base = map_csr_server_shared_memory(&global.page_manager) .expect("mapping shared memory should succeed");