From 3e6f7a79f7c1c2e240d8f1f4fb75b974ef807729 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 21 May 2026 13:47:22 -0700 Subject: [PATCH 001/319] Scaffold Windows runner: PE loader + placeholder NT shim (#860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR lays the groundwork for running unmodified Windows PE32+ executables under LiteBox on a Windows host. It adds three new crates wired to the existing North/South architecture: - *litebox_common_windows* — PE32+ parser and image mapping. - *litebox_shim_windows* — placeholder NT shim. Wires the loader to a PageManager, sets up the initial thread stack, and stubs syscall / exception / interrupt handlers (all currently Terminate — actual NT syscall dispatch lands in a follow-up). - *litebox_runner_windows_userland* — similar to other userland runner. Includes one end-to-end integration test (loads_minimal_pe_without_imports) that hand-builds a PE invoking NtTerminateProcess directly and runs it through the full pipeline. --- .github/workflows/ci.yml | 9 + Cargo.lock | 35 + Cargo.toml | 6 + litebox_common_windows/Cargo.toml | 11 + litebox_common_windows/src/lib.rs | 10 + litebox_common_windows/src/loader.rs | 652 +++++++++++++++++++ litebox_runner_windows_userland/Cargo.toml | 18 + litebox_runner_windows_userland/src/lib.rs | 136 ++++ litebox_runner_windows_userland/src/main.rs | 15 + litebox_runner_windows_userland/tests/run.rs | 166 +++++ litebox_shim_windows/Cargo.toml | 19 + litebox_shim_windows/src/lib.rs | 520 +++++++++++++++ 12 files changed, 1597 insertions(+) create mode 100644 litebox_common_windows/Cargo.toml create mode 100644 litebox_common_windows/src/lib.rs create mode 100644 litebox_common_windows/src/loader.rs create mode 100644 litebox_runner_windows_userland/Cargo.toml create mode 100644 litebox_runner_windows_userland/src/lib.rs create mode 100644 litebox_runner_windows_userland/src/main.rs create mode 100644 litebox_runner_windows_userland/tests/run.rs create mode 100644 litebox_shim_windows/Cargo.toml create mode 100644 litebox_shim_windows/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bddd041a29..ece633a33a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -242,6 +242,10 @@ jobs: # since it needs to actually access the file-system, pull in # relevant files, and then actually trigger LiteBox itself. # + # - `litebox_runner_windows_userland` is allowed to have `std` access + # since it needs to actually access the file-system, pull in + # relevant files, and then actually trigger LiteBox itself. + # # - `litebox_runner_lvbs` has a custom target (`no_std`), so it does # not work with the current no_std checker. # @@ -258,6 +262,9 @@ jobs: # `litebox_platform_linux_userland` (for debugging) which # depends on `litebox_platform_multiplex`. # + # - `litebox_shim_windows` itself is `no_std` but depends on + # `litebox_platform_multiplex`. + # # - `litebox_syscall_rewriter` is allowed to have `std` access since # it is a helper binary that runs in userland to AOT "compile" ELFs. # @@ -279,10 +286,12 @@ jobs: -not -path './litebox_platform_lvbs/Cargo.toml' \ -not -path './litebox_platform_multiplex/Cargo.toml' \ -not -path './litebox_runner_linux_userland/Cargo.toml' \ + -not -path './litebox_runner_windows_userland/Cargo.toml' \ -not -path './litebox_runner_lvbs/Cargo.toml' \ -not -path './litebox_runner_optee_on_linux_userland/Cargo.toml' \ -not -path './litebox_shim_linux/Cargo.toml' \ -not -path './litebox_shim_optee/Cargo.toml' \ + -not -path './litebox_shim_windows/Cargo.toml' \ -not -path './litebox_syscall_rewriter/Cargo.toml' \ -not -path './litebox_packager/Cargo.toml' \ -not -path './litebox_runner_snp/Cargo.toml' \ diff --git a/Cargo.lock b/Cargo.lock index 1e12d287fb..b62d88e958 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,6 +1491,14 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "litebox_common_windows" +version = "0.1.0" +dependencies = [ + "object", + "thiserror", +] + [[package]] name = "litebox_packager" version = "0.1.0" @@ -1689,6 +1697,21 @@ dependencies = [ "log", ] +[[package]] +name = "litebox_runner_windows_userland" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "litebox", + "litebox_common_linux", + "litebox_platform_multiplex", + "litebox_platform_windows_userland", + "litebox_shim_windows", + "litebox_util_log", + "tracing-subscriber", +] + [[package]] name = "litebox_shim_linux" version = "0.1.0" @@ -1734,6 +1757,18 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "litebox_shim_windows" +version = "0.1.0" +dependencies = [ + "litebox", + "litebox_common_linux", + "litebox_common_windows", + "litebox_platform_multiplex", + "litebox_util_log", + "thiserror", +] + [[package]] name = "litebox_syscall_rewriter" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 258dd51e99..e30b3904ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "litebox", "litebox_common_linux", + "litebox_common_windows", "litebox_common_optee", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", @@ -11,9 +12,11 @@ members = [ "litebox_platform_multiplex", "litebox_runner_linux_userland", "litebox_runner_linux_on_windows_userland", + "litebox_runner_windows_userland", "litebox_runner_lvbs", "litebox_runner_optee_on_linux_userland", "litebox_shim_linux", + "litebox_shim_windows", "litebox_syscall_rewriter", "litebox_packager", "litebox_runner_snp", @@ -27,6 +30,7 @@ members = [ default-members = [ "litebox", "litebox_common_linux", + "litebox_common_windows", "litebox_common_optee", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", @@ -35,7 +39,9 @@ default-members = [ "litebox_platform_multiplex", "litebox_runner_linux_userland", "litebox_runner_linux_on_windows_userland", + "litebox_runner_windows_userland", "litebox_shim_linux", + "litebox_shim_windows", "litebox_shim_optee", "litebox_syscall_rewriter", "litebox_packager", diff --git a/litebox_common_windows/Cargo.toml b/litebox_common_windows/Cargo.toml new file mode 100644 index 0000000000..56a6a7dbf9 --- /dev/null +++ b/litebox_common_windows/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "litebox_common_windows" +version = "0.1.0" +edition = "2024" + +[dependencies] +object = { version = "0.36.7", default-features = false, features = ["pe", "read_core"] } +thiserror = { version = "2.0.6", default-features = false } + +[lints] +workspace = true diff --git a/litebox_common_windows/src/lib.rs b/litebox_common_windows/src/lib.rs new file mode 100644 index 0000000000..af886a31c8 --- /dev/null +++ b/litebox_common_windows/src/lib.rs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Common Windows data structures and helpers suitable for LiteBox. + +#![no_std] + +extern crate alloc; + +pub mod loader; diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs new file mode 100644 index 0000000000..fd9c7e829d --- /dev/null +++ b/litebox_common_windows/src/loader.rs @@ -0,0 +1,652 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! PE loader-facing parser and mapper. +//! +//! This module parses PE metadata and maps images through platform-provided traits. + +use alloc::vec::Vec; +use core::cmp; +use core::mem::size_of; + +use object::endian::LittleEndian as LE; +use object::pe; +use object::pod::Pod; +use thiserror::Error; + +/// x86-64 page size used for all PE alignment and protection math. +pub const PAGE_SIZE: usize = 4096; + +/// Maximum supported section count. PE limit per spec is 96. +const MAX_SECTIONS: usize = 96; + +/// The result of parsing a PE32+ file. +#[derive(Debug)] +pub struct PeParsedFile { + /// Basic image metadata from the PE optional and COFF headers. + pub image: PeImageInfo, + /// Raw PE section headers in file order. + pub sections: Vec, + /// Data directory entries indexed by `IMAGE_DIRECTORY_ENTRY_*`. + pub data_directories: Vec, +} + +/// Basic PE image metadata needed by the Windows shim loader. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PeImageInfo { + pub machine: u16, + pub characteristics: u16, + pub image_base: usize, + pub entry_point_rva: usize, + pub size_of_image: usize, + pub size_of_headers: usize, + pub section_alignment: usize, + pub file_alignment: usize, + /// e.g. `IMAGE_SUBSYSTEM_WINDOWS_CUI`. + pub subsystem: u16, + pub dll_characteristics: u16, + pub size_of_heap_reserve: usize, + pub size_of_heap_commit: usize, +} + +/// Information about the mapped PE image. +pub struct MappingInfo { + pub base_addr: usize, + pub image_size: usize, + pub entry_point: usize, +} + +/// A PE data directory entry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PeDataDirectory { + pub virtual_address: u32, + pub size: u32, +} + +/// Errors that can occur when parsing a PE file. +#[derive(Debug, Error)] +pub enum PeParseError { + /// The input file could not be read. + #[error("I/O error")] + Io(#[source] E), + /// The file is not a supported Windows executable image. + #[error("unsupported PE image")] + UnsupportedImage, + /// A PE field overflowed the host representation used by this parser. + #[error("PE field overflow")] + Overflow, +} + +/// Errors that can occur when mapping a PE image into memory. +#[derive(Debug, Error)] +pub enum PeLoadError { + /// Memory mapping error. + #[error("memory mapping error")] + Map(#[source] E), + /// The image contains inconsistent or overflowing fields. + #[error("invalid PE image")] + InvalidImage, + /// The image had to be loaded away from its preferred base but has no base relocations. + #[error("PE image requires base relocations")] + RelocationRequired, + /// The image contains a relocation type this loader does not support. + #[error("unsupported PE base relocation type {0}")] + UnsupportedRelocation(u16), + /// A mapped memory access failed. + #[error(transparent)] + Fault(#[from] Fault), +} + +macro_rules! checked_add { + ($a:expr, $b:expr, $e:expr) => { + $a.checked_add($b).ok_or($e) + }; +} +macro_rules! checked_add_invalid { + ($a:expr, $b:expr) => { + checked_add!($a, $b, PeLoadError::InvalidImage) + }; +} +macro_rules! checked_add_overflow { + ($a:expr, $b:expr) => { + checked_add!($a, $b, PeParseError::Overflow) + }; +} + +macro_rules! checked_next_multiple_of { + ($x:expr, $align:expr, $e:expr) => { + $x.checked_next_multiple_of($align).ok_or($e) + }; +} + +impl PeParsedFile { + /// Parse a PE32+ x86-64 image from the given file. + /// + /// Only the PE headers are read into memory; section contents — including + /// `.reloc` — are left on disk and streamed by [`MapMemory::map_file`] during + /// [`PeParsedFile::load`]. Base relocations are then applied by reading the + /// mapped image in place, avoiding a redundant disk read of the `.reloc` bytes. + pub fn parse(file: &mut F) -> Result> { + let size = file.size().map_err(PeParseError::Io)?; + let file_size: usize = usize_from_u64(size)?; + + let (image, sections, data_directories) = parse_headers(file, file_size)?; + + Ok(PeParsedFile { + image, + sections, + data_directories, + }) + } + + /// Load the PE image into memory. + /// + /// This maps PE headers and sections into their image locations, + /// applies base relocations if the image was not loaded at its preferred base, + /// and then applies section protections. Import resolution is left to the shim + /// because it depends on the emulated Windows module environment. + pub fn load( + &self, + mapper: &mut M, + mem: &mut impl AccessMemory, + ) -> Result> { + let preferred_base = self.image.image_base; + let image_size = checked_next_multiple_of!( + self.image.size_of_image, + PAGE_SIZE, + PeLoadError::InvalidImage + )?; + if image_size == 0 { + return Err(PeLoadError::InvalidImage); + } + + let base_addr = mapper + .reserve(preferred_base, image_size, PAGE_SIZE) + .map_err(PeLoadError::Map)?; + let image_end = checked_add_invalid!(base_addr, image_size)?; + + let headers_size = self.image.size_of_headers; + if headers_size > image_size { + return Err(PeLoadError::InvalidImage); + } + if headers_size != 0 { + mapper + .map_file(base_addr, headers_size, 0, &Protection::R) + .map_err(PeLoadError::Map)?; + } + + for section in &self.sections { + let section_rva = section.virtual_address.get(LE) as usize; + let virtual_size = section.virtual_size.get(LE) as usize; + let raw_size = section.size_of_raw_data.get(LE) as usize; + let raw_offset = section.pointer_to_raw_data.get(LE) as usize; + let mapped_size = checked_next_multiple_of!( + cmp::max(virtual_size, raw_size), + PAGE_SIZE, + PeLoadError::InvalidImage + )?; + + if mapped_size == 0 { + continue; + } + let section_start = checked_add_invalid!(base_addr, section_rva)?; + let section_end = checked_add_invalid!(section_start, mapped_size)?; + if section_end > image_end { + return Err(PeLoadError::InvalidImage); + } + + if raw_size != 0 { + mapper + .map_file(section_start, raw_size, raw_offset as u64, &Protection::RW) + .map_err(PeLoadError::Map)?; + } + + if mapped_size > raw_size { + let zero_start = checked_add_invalid!(section_start, raw_size)?; + mapper + .map_zero(zero_start, mapped_size - raw_size, &Protection::RW) + .map_err(PeLoadError::Map)?; + } + } + + self.apply_base_relocations::(base_addr, mem)?; + + for section in &self.sections { + let section_rva: usize = section.virtual_address.get(LE) as usize; + let virtual_size: usize = section.virtual_size.get(LE) as usize; + let raw_size: usize = section.size_of_raw_data.get(LE) as usize; + let mapped_size = checked_next_multiple_of!( + cmp::max(virtual_size, raw_size), + PAGE_SIZE, + PeLoadError::InvalidImage + )?; + if mapped_size == 0 { + continue; + } + + let protect_start = page_align_down(checked_add_invalid!(base_addr, section_rva)?); + let protect_end = checked_next_multiple_of!( + base_addr + .checked_add(section_rva) + .and_then(|address| address.checked_add(mapped_size)) + .ok_or(PeLoadError::InvalidImage)?, + PAGE_SIZE, + PeLoadError::InvalidImage + )?; + if protect_end > image_end { + return Err(PeLoadError::InvalidImage); + } + + mapper + .protect( + protect_start, + protect_end - protect_start, + &Protection::from_section_characteristics(section.characteristics.get(LE)), + ) + .map_err(PeLoadError::Map)?; + } + + let entry_point = checked_add!( + base_addr, + self.image.entry_point_rva, + PeLoadError::InvalidImage + )?; + + Ok(MappingInfo { + base_addr, + image_size, + entry_point, + }) + } + + fn apply_base_relocations( + &self, + base_addr: usize, + mem: &mut impl AccessMemory, + ) -> Result<(), PeLoadError> { + let delta = base_addr.wrapping_sub(self.image.image_base); + if delta == 0 { + return Ok(()); + } + + // The `.reloc` directory is already mapped (the containing section was made RW above). + let reloc_dir = self + .data_directories + .get(pe::IMAGE_DIRECTORY_ENTRY_BASERELOC) + .filter(|d| d.size != 0) + .ok_or(PeLoadError::RelocationRequired)?; + + let image_end = checked_add_invalid!(base_addr, self.image.size_of_image)?; + let dir_addr = checked_add_invalid!(base_addr, reloc_dir.virtual_address as usize)?; + let dir_end = checked_add_invalid!(dir_addr, reloc_dir.size as usize)?; + + // `delta` represents a possibly-negative offset via two's-complement + // wrap in `usize`; the signed cast preserves the sign for `wrapping_add_signed`. + let delta_i64: i64 = delta.cast_signed() as i64; + + let mut cursor = dir_addr; + while cursor < dir_end { + let mut header_bytes = [0u8; size_of::()]; + mem.read(cursor, &mut header_bytes)?; + let (header, _) = object::pod::from_bytes::(&header_bytes) + .map_err(|()| PeLoadError::InvalidImage)?; + let page_rva = header.virtual_address.get(LE); + let block_size = header.size_of_block.get(LE) as usize; + if block_size < size_of::() || !block_size.is_multiple_of(2) { + return Err(PeLoadError::InvalidImage); + } + let block_end = checked_add_invalid!(cursor, block_size)?; + if block_end > dir_end { + return Err(PeLoadError::InvalidImage); + } + + let mut entry_addr = + checked_add_invalid!(cursor, size_of::())?; + while entry_addr < block_end { + let entry = mem_read_u16(mem, entry_addr)?; + let typ = entry >> 12; + let entry_offset = u32::from(entry & 0x0fff); + match typ { + pe::IMAGE_REL_BASED_ABSOLUTE => {} + pe::IMAGE_REL_BASED_DIR64 => { + let relocation_rva = checked_add_invalid!(page_rva, entry_offset)? as usize; + let relocation_address = checked_add_invalid!(base_addr, relocation_rva)?; + let relocation_end = + checked_add_invalid!(relocation_address, size_of::())?; + if relocation_end > image_end { + return Err(PeLoadError::InvalidImage); + } + let value = mem_read_u64(mem, relocation_address)?; + let relocated = value.wrapping_add_signed(delta_i64); + mem.write(relocation_address, &relocated.to_le_bytes())?; + } + typ => return Err(PeLoadError::UnsupportedRelocation(typ)), + } + entry_addr = checked_add_invalid!(entry_addr, size_of::())?; + } + cursor = block_end; + } + + Ok(()) + } +} + +fn mem_read_u16(mem: &mut impl AccessMemory, address: usize) -> Result> { + let mut buf = [0u8; size_of::()]; + mem.read(address, &mut buf)?; + Ok(u16::from_le_bytes(buf)) +} + +fn mem_read_u64(mem: &mut impl AccessMemory, address: usize) -> Result> { + let mut buf = [0u8; size_of::()]; + mem.read(address, &mut buf)?; + Ok(u64::from_le_bytes(buf)) +} + +type ParsedHeaders = ( + PeImageInfo, + Vec, + Vec, +); + +/// Read a POD struct of type `T` from `file` at `offset`. +/// +/// All `object::pe` structs have alignment 1 (their fields are +/// `#[repr(transparent)]` byte-array wrappers), so the byte buffer's alignment +/// trivially satisfies `from_bytes`'s check and the transmute happens inside +/// `object::pod` rather than here. +fn read_pod(file: &mut F, offset: u64) -> Result> { + let mut buf = alloc::vec![0u8; size_of::()]; + file.read_at(offset, &mut buf).map_err(PeParseError::Io)?; + let (val, _) = + object::pod::from_bytes::(&buf).map_err(|()| PeParseError::UnsupportedImage)?; + Ok(*val) +} + +/// Read `count` POD structs of type `T` from `file` starting at `offset`. +fn read_pod_vec( + file: &mut F, + offset: u64, + count: usize, +) -> Result, PeParseError> { + let bytes_len = count + .checked_mul(size_of::()) + .ok_or(PeParseError::Overflow)?; + let mut buf = alloc::vec![0u8; bytes_len]; + file.read_at(offset, &mut buf).map_err(PeParseError::Io)?; + let (slice, _) = object::pod::slice_from_bytes::(&buf, count) + .map_err(|()| PeParseError::UnsupportedImage)?; + Ok(slice.to_vec()) +} + +fn parse_headers( + file: &mut F, + file_size: usize, +) -> Result> { + // DOS header. + if file_size < size_of::() { + return Err(PeParseError::UnsupportedImage); + } + let dos: pe::ImageDosHeader = read_pod(file, 0)?; + if dos.e_magic.get(LE) != pe::IMAGE_DOS_SIGNATURE { + return Err(PeParseError::UnsupportedImage); + } + let nt_offset = u64::from(dos.e_lfanew.get(LE)); + + // NT headers (signature + COFF file header + 64-bit optional header). + let nt_end = checked_add_overflow!(nt_offset, size_of::() as u64)?; + if nt_end > file_size as u64 { + return Err(PeParseError::UnsupportedImage); + } + let nt: pe::ImageNtHeaders64 = read_pod(file, nt_offset)?; + if nt.signature.get(LE) != pe::IMAGE_NT_SIGNATURE { + return Err(PeParseError::UnsupportedImage); + } + if nt.optional_header.magic.get(LE) != pe::IMAGE_NT_OPTIONAL_HDR64_MAGIC { + return Err(PeParseError::UnsupportedImage); + } + + let machine = nt.file_header.machine.get(LE); + let characteristics = nt.file_header.characteristics.get(LE); + if machine != pe::IMAGE_FILE_MACHINE_AMD64 + || characteristics & pe::IMAGE_FILE_EXECUTABLE_IMAGE == 0 + { + return Err(PeParseError::UnsupportedImage); + } + + let opt = &nt.optional_header; + // Sub-page section alignment would let consecutive sections share a page, + // so the page-aligned protect range of one section could overwrite another's + // (e.g. RW `.data` downgrading the last page of RX `.text`). + if (opt.section_alignment.get(LE) as usize) < PAGE_SIZE { + return Err(PeParseError::UnsupportedImage); + } + + let image_base = usize_from_u64(opt.image_base.get(LE))?; + let entry_point_rva = opt.address_of_entry_point.get(LE) as usize; + let image = PeImageInfo { + machine, + characteristics, + image_base, + entry_point_rva, + size_of_image: opt.size_of_image.get(LE) as usize, + size_of_headers: opt.size_of_headers.get(LE) as usize, + section_alignment: opt.section_alignment.get(LE) as usize, + file_alignment: opt.file_alignment.get(LE) as usize, + subsystem: opt.subsystem.get(LE), + dll_characteristics: opt.dll_characteristics.get(LE), + size_of_heap_reserve: usize_from_u64(opt.size_of_heap_reserve.get(LE))?, + size_of_heap_commit: usize_from_u64(opt.size_of_heap_commit.get(LE))?, + }; + if image.size_of_headers > file_size { + return Err(PeParseError::UnsupportedImage); + } + if entry_point_rva >= image.size_of_image { + return Err(PeParseError::UnsupportedImage); + } + + // Data directories sit immediately after the optional header. `ImageNtHeaders64` + // already covers signature + file header + 64-bit optional header, so the + // directory array starts at `nt_offset + size_of::()`. + let num_rva_and_sizes = opt.number_of_rva_and_sizes.get(LE) as usize; + if num_rva_and_sizes > pe::IMAGE_NUMBEROF_DIRECTORY_ENTRIES { + return Err(PeParseError::UnsupportedImage); + } + let raw_dirs: Vec = read_pod_vec(file, nt_end, num_rva_and_sizes)?; + let data_directories: Vec<_> = raw_dirs + .iter() + .map(|dir| { + let virtual_address = dir.virtual_address.get(LE); + let size = dir.size.get(LE); + PeDataDirectory { + virtual_address, + size, + } + }) + .collect(); + for dir in &data_directories { + let end = checked_add!(dir.virtual_address, dir.size, PeParseError::Overflow)?; + if (end as usize) > image.size_of_image { + return Err(PeParseError::UnsupportedImage); + } + } + + // Section headers sit at `nt_offset + 4 (signature) + size_of::() + size_of_optional_header`. + let num_sections = nt.file_header.number_of_sections.get(LE) as usize; + if num_sections > MAX_SECTIONS { + return Err(PeParseError::UnsupportedImage); + } + let size_of_optional_header = u64::from(nt.file_header.size_of_optional_header.get(LE)); + let sections_offset = nt_offset + .checked_add(4 + size_of::() as u64) + .and_then(|n| n.checked_add(size_of_optional_header)) + .ok_or(PeParseError::Overflow)?; + let sections_end = checked_add_overflow!( + sections_offset, + (num_sections * size_of::()) as u64 + )?; + if sections_end > file_size as u64 { + return Err(PeParseError::UnsupportedImage); + } + let sections: Vec = read_pod_vec(file, sections_offset, num_sections)?; + validate_sections(&image, §ions, file_size)?; + + Ok((image, sections, data_directories)) +} + +/// Verify section invariants that the loader's mapping arithmetic depends on. +/// +/// The mapping pass page-aligns each section's protect range; if a hostile PE +/// places a section at a sub-`section_alignment` RVA, has sections overlap each +/// other, or has a section overlap the headers, the page-aligned protect range +/// of a later section could downgrade earlier protections (e.g. RW `.data` +/// silently making RX `.text` writable). Reject all such images at parse time. +fn validate_sections( + image: &PeImageInfo, + sections: &[pe::ImageSectionHeader], + file_size: usize, +) -> Result<(), PeParseError> { + let headers_end = + checked_next_multiple_of!(image.size_of_headers, PAGE_SIZE, PeParseError::Overflow)?; + let mut prev_end_rva: usize = 0; + for section in sections { + let section_rva = section.virtual_address.get(LE) as usize; + let virtual_size = section.virtual_size.get(LE) as usize; + let raw_size = section.size_of_raw_data.get(LE) as usize; + let raw_offset = section.pointer_to_raw_data.get(LE) as usize; + + if !section_rva.is_multiple_of(image.section_alignment) { + return Err(PeParseError::UnsupportedImage); + } + if section_rva < headers_end && (virtual_size != 0 || raw_size != 0) { + return Err(PeParseError::UnsupportedImage); + } + if section_rva < prev_end_rva { + return Err(PeParseError::UnsupportedImage); + } + let mapped_size = checked_next_multiple_of!( + cmp::max(virtual_size, raw_size), + PAGE_SIZE, + PeParseError::Overflow + )?; + let section_end_rva = checked_add_overflow!(section_rva, mapped_size)?; + if section_end_rva > image.size_of_image { + return Err(PeParseError::UnsupportedImage); + } + + if raw_size != 0 { + let raw_end = checked_add_overflow!(raw_offset, raw_size)?; + if raw_end > file_size { + return Err(PeParseError::UnsupportedImage); + } + } + + prev_end_rva = section_end_rva; + } + Ok(()) +} + +fn usize_from_u64(value: u64) -> Result> { + value.try_into().map_err(|_| PeParseError::Overflow) +} + +/// Round `address` down to the nearest [`PAGE_SIZE`] multiple. +pub fn page_align_down(address: usize) -> usize { + address & !(PAGE_SIZE - 1) +} + +/// Trait for reading PE binary data at specific offsets. +pub trait ReadAt { + type Error; + + /// Read `buf.len()` bytes at `offset`. Short reads are not permitted. + fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error>; + + fn size(&mut self) -> Result; +} + +/// Trait for reserving, mapping, and protecting PE image memory. +pub trait MapMemory { + type Error; + + /// Reserve a region of memory for the image, preferably at `preferred_base`. + /// + /// The returned address is the actual base address. If it differs from the + /// preferred image base, [`PeParsedFile::load`] applies base relocations. + fn reserve( + &mut self, + preferred_base: usize, + len: usize, + align: usize, + ) -> Result; + + /// Map zero-filled memory, replacing any existing mappings in the range. + fn map_zero( + &mut self, + address: usize, + len: usize, + prot: &Protection, + ) -> Result<(), Self::Error>; + + /// Map file-backed data at the specified file offset. + /// + /// The mapper owns the backing file or equivalent byte source, including + /// validating that the requested range exists. + /// + /// PE image sections are commonly file-aligned rather than page-aligned, + /// so implementations may need to satisfy this by mapping pages and copying + /// the requested file range into them. + fn map_file( + &mut self, + address: usize, + len: usize, + offset: u64, + prot: &Protection, + ) -> Result<(), Self::Error>; + + fn protect(&mut self, address: usize, len: usize, prot: &Protection) + -> Result<(), Self::Error>; +} + +/// Trait for reading and writing memory that has been mapped via [`MapMemory`]. +pub trait AccessMemory { + fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Fault>; + + fn write(&mut self, address: usize, data: &[u8]) -> Result<(), Fault>; +} + +#[derive(Debug, Error)] +#[error("memory access fault")] +pub struct Fault; + +/// Memory protection flags. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Protection { + pub read: bool, + pub write: bool, + pub execute: bool, +} + +impl Protection { + /// Read-write, no-execute. + pub(crate) const RW: Self = Self { + read: true, + write: true, + execute: false, + }; + + /// Read-only. + pub(crate) const R: Self = Self { + read: true, + write: false, + execute: false, + }; + + fn from_section_characteristics(characteristics: u32) -> Self { + Self { + read: characteristics & pe::IMAGE_SCN_MEM_READ != 0, + write: characteristics & pe::IMAGE_SCN_MEM_WRITE != 0, + execute: characteristics & pe::IMAGE_SCN_MEM_EXECUTE != 0, + } + } +} diff --git a/litebox_runner_windows_userland/Cargo.toml b/litebox_runner_windows_userland/Cargo.toml new file mode 100644 index 0000000000..52c45de4af --- /dev/null +++ b/litebox_runner_windows_userland/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "litebox_runner_windows_userland" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.97" +clap = { version = "4.5.33", features = ["derive"] } +litebox = { version = "0.1.0", path = "../litebox" } +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_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_windows_userland"] } +litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows", default-features = false, features = ["platform_windows_userland"] } +litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = ["backend_tracing"] } +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } + +[lints] +workspace = true diff --git a/litebox_runner_windows_userland/src/lib.rs b/litebox_runner_windows_userland/src/lib.rs new file mode 100644 index 0000000000..3abe06eaa9 --- /dev/null +++ b/litebox_runner_windows_userland/src/lib.rs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Restrict this crate to only work on Windows. For now, we are restricting this to only x86-64 +// Windows, but we _may_ allow for more in the future, if we find it useful to do so. +#![cfg(all(target_os = "windows", target_arch = "x86_64"))] + +extern crate alloc; + +use anyhow::{Context as _, Result}; +use clap::Parser; +use litebox_platform_multiplex::Platform; +use std::path::PathBuf; + +/// Run Windows PE programs with LiteBox on unmodified Windows. +/// +/// 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. +#[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`. + #[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). + #[arg(long = "env")] + pub environment_variables: Vec, + /// Forward the existing environment variables. + #[arg(long = "forward-env")] + pub forward_environment_variables: bool, + /// 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, +} + +/// 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()) + .with_level(true) + .with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_env_var("LITEBOX_LOG") + .from_env_lossy(), + ) + .init(); + + if cli_args.unstable { + litebox_util_log::warn!( + "Windows PE runner is currently a skeleton; shim functionality is not implemented yet" + ); + } + + 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 = Platform::new(); + litebox_platform_multiplex::set_platform(platform); + let shim_builder = litebox_shim_windows::WindowsShimBuilder::new(); + let litebox = shim_builder.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 mut in_mem = litebox::fs::in_mem::FileSystem::new(litebox); + in_mem.with_root_privileges(|fs| { + use litebox::fs::FileSystem as _; + fs.mkdir( + "/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("/tmp", Some(1000), Some(1000)) + .expect("/tmp chown cannot fail on a fresh in-memory file system"); + }); + + let tar_ro = litebox::fs::tar_ro::FileSystem::new(litebox, tar_data.into()); + shim_builder.default_fs(in_mem, tar_ro) + }; + 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)) + .map(to_cstring) + .collect::>>() + .context("argv contained an interior NUL byte")?; + let mut envp = cli_args + .environment_variables + .iter() + .map(|s| to_cstring(s)) + .collect::>>() + .context("--env value contained an interior NUL byte")?; + if cli_args.forward_environment_variables { + for (key, value) in std::env::vars() { + envp.push( + to_cstring(&format!("{key}={value}")) + .context("forwarded environment variable contained an interior NUL byte")?, + ); + } + } + + let program = shim + .load_program(initial_file_system, 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()` + // we hand in is fully initialized before any guest instruction runs. + unsafe { + litebox_platform_windows_userland::run_thread( + program.entrypoints, + &mut litebox_common_linux::PtRegs::default(), + ); + } + std::process::exit(program.process.wait()) +} + +fn to_cstring(s: &str) -> Result { + std::ffi::CString::new(s.as_bytes()).map_err(Into::into) +} diff --git a/litebox_runner_windows_userland/src/main.rs b/litebox_runner_windows_userland/src/main.rs new file mode 100644 index 0000000000..5f05643efa --- /dev/null +++ b/litebox_runner_windows_userland/src/main.rs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#[cfg(all(target_os = "windows", target_arch = "x86_64"))] +fn main() -> anyhow::Result<()> { + use clap::Parser as _; + use litebox_runner_windows_userland::CliArgs; + litebox_runner_windows_userland::run(CliArgs::parse()) +} + +#[cfg(not(all(target_os = "windows", target_arch = "x86_64")))] +fn main() { + eprintln!("This program is only supported on Windows x86_64"); + std::process::exit(1); +} diff --git a/litebox_runner_windows_userland/tests/run.rs b/litebox_runner_windows_userland/tests/run.rs new file mode 100644 index 0000000000..092256cca1 --- /dev/null +++ b/litebox_runner_windows_userland/tests/run.rs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#![cfg(all(target_os = "windows", target_arch = "x86_64"))] + +use std::ffi::c_void; + +unsafe extern "system" { + fn GetModuleHandleA(module_name: *const u8) -> *mut c_void; + fn GetProcAddress(module: *mut c_void, proc_name: *const u8) -> *const c_void; +} + +#[test] +fn loads_minimal_pe_without_imports() { + let test_dir = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("no_import"); + std::fs::create_dir_all(&test_dir).unwrap(); + let pe_path = build_no_import_pe(&test_dir); + println!("Built no-import PE fixture at `{}`", pe_path.display()); + + let tar_path = test_dir.join("no_import.tar"); + create_tar_with_exe(&test_dir, &tar_path, "no_import.exe"); + + let mut command = + std::process::Command::new(env!("CARGO_BIN_EXE_litebox_runner_windows_userland")); + command.args([ + "--initial-files", + tar_path.to_str().unwrap(), + "/no_import.exe", + ]); + println!("Running `{command:?}`"); + let output = command + .output() + .expect("failed to run litebox_runner_windows_userland"); + + assert!( + output.status.success(), + "runner failed to load no-import PE; status {:?}\nstdout:\n{}\nstderr:\n{}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn build_no_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { + let source_path = test_dir.join("no_import.rs"); + let exe_path = test_dir.join("no_import.exe"); + let syscall_number = nt_terminate_process_syscall_number(); + println!("Using NtTerminateProcess syscall number `{syscall_number:#x}`"); + std::fs::write( + &source_path, + minimal_pe_with_nt_terminate_process_syscall_source(syscall_number), + ) + .unwrap(); + + let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); + let output = std::process::Command::new(rustc) + .args([ + "--edition=2024", + source_path.to_str().unwrap(), + "-C", + "panic=abort", + "-C", + "link-arg=/ENTRY:mainCRTStartup", + "-C", + "link-arg=/SUBSYSTEM:CONSOLE", + "-C", + "link-arg=/NODEFAULTLIB", + "-o", + exe_path.to_str().unwrap(), + ]) + .output() + .expect("failed to run rustc for the no-import Windows PE fixture"); + + assert!( + output.status.success(), + "failed to build no-import Windows PE fixture\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + exe_path +} + +fn minimal_pe_with_nt_terminate_process_syscall_source(syscall_number: u32) -> String { + format!( + r#" +#![no_std] +#![no_main] + +#[unsafe(no_mangle)] +pub unsafe extern "system" fn mainCRTStartup() -> ! {{ + unsafe {{ + core::arch::asm!( + "mov rcx, -1", + "xor edx, edx", + "mov r10, rcx", + "mov eax, {syscall_number:#x}", + "syscall", + options(noreturn), + ); + }} +}} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo<'_>) -> ! {{ + loop {{ + core::hint::spin_loop(); + }} +}} +"# + ) +} + +fn nt_terminate_process_syscall_number() -> u32 { + // SAFETY: These are static NUL-terminated strings, and GetModuleHandleA does not retain them. + let ntdll = unsafe { GetModuleHandleA(c"ntdll.dll".as_ptr().cast()) }; + assert!( + !ntdll.is_null(), + "ntdll.dll is not loaded in the test process" + ); + + // SAFETY: These are static NUL-terminated strings, and GetProcAddress does not retain them. + let nt_terminate_process = + unsafe { GetProcAddress(ntdll, c"NtTerminateProcess".as_ptr().cast()) }; + assert!( + !nt_terminate_process.is_null(), + "NtTerminateProcess is not exported by ntdll.dll" + ); + + // SAFETY: `nt_terminate_process` points to executable code in the loaded ntdll image. Reading + // a small prefix of the function stub is sufficient to decode the `mov eax, imm32` syscall ID. + let stub = unsafe { std::slice::from_raw_parts(nt_terminate_process.cast::(), 32) }; + let syscall_offset = stub + .windows(2) + .position(|bytes| bytes == [0x0f, 0x05]) + .expect("NtTerminateProcess stub does not contain syscall instruction"); + let mov_eax_offset = stub[..syscall_offset] + .iter() + .position(|byte| *byte == 0xb8) + .expect("NtTerminateProcess stub does not load a syscall number into eax"); + + u32::from_le_bytes( + stub[mov_eax_offset + 1..mov_eax_offset + 5] + .try_into() + .unwrap(), + ) +} + +fn create_tar_with_exe(test_dir: &std::path::Path, tar_path: &std::path::Path, exe_name: &str) { + let output = std::process::Command::new("tar.exe") + .args([ + "-cf", + tar_path.to_str().unwrap(), + "-C", + test_dir.to_str().unwrap(), + exe_name, + ]) + .output() + .expect("failed to run tar.exe for the no-import Windows PE fixture"); + + assert!( + output.status.success(), + "failed to create tar for no-import Windows PE fixture\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml new file mode 100644 index 0000000000..81e808c5fd --- /dev/null +++ b/litebox_shim_windows/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "litebox_shim_windows" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox = { path = "../litebox/", version = "0.1.0" } +litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } +litebox_common_windows = { path = "../litebox_common_windows/", version = "0.1.0" } +litebox_platform_multiplex = { path = "../litebox_platform_multiplex/", version = "0.1.0", default-features = false } +litebox_util_log = { path = "../litebox_util_log", version = "0.1.0" } +thiserror = { version = "2.0.6", default-features = false } + +[features] +default = ["platform_windows_userland"] +platform_windows_userland = ["litebox_platform_multiplex/platform_windows_userland"] + +[lints] +workspace = true diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs new file mode 100644 index 0000000000..a2487f939f --- /dev/null +++ b/litebox_shim_windows/src/lib.rs @@ -0,0 +1,520 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! A placeholder Windows NT shim for LiteBox. +//! +//! This crate intentionally only exposes the runner-facing skeleton for now. +//! The actual NT syscall, PE loading, and Windows process environment support +//! will be filled in piece by piece. + +#![no_std] + +extern crate alloc; + +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::marker::PhantomData; + +use litebox::fd::TypedFd; +use litebox::fs::{Mode, OFlags}; +use litebox::mm::PageManager; +use litebox::mm::linux::{ + CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, VmemProtectError, +}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; +use litebox::{LiteBox, platform::RawPointerProvider}; +use litebox_common_windows::loader::{ + AccessMemory, Fault, MapMemory, PAGE_SIZE, PeLoadError, PeParseError, PeParsedFile, Protection, + ReadAt, page_align_down, +}; +use litebox_platform_multiplex::Platform; +use thiserror::Error; + +const INITIAL_STACK_SIZE: usize = 1024 * 1024; +const PLACEHOLDER_EXIT_CODE: i32 = 1; +const ZERO_CHUNK: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; +const FILE_CHUNK_BYTES: usize = 64 * 1024; + +type WindowsPageManager = PageManager; + +pub type DefaultFS = WindowsFS; + +pub(crate) type WindowsFS = litebox::fs::layered::FileSystem< + Platform, + litebox::fs::in_mem::FileSystem, + litebox::fs::layered::FileSystem< + Platform, + litebox::fs::devices::FileSystem, + litebox::fs::tar_ro::FileSystem, + >, +>; + +/// A trait required for file systems to be used by the Windows shim. +pub trait ShimFS: litebox::fs::FileSystem + Send + Sync + 'static {} +impl ShimFS for T {} + +/// Builds a Windows NT shim instance. +pub struct WindowsShimBuilder { + litebox: LiteBox, +} + +impl Default for WindowsShimBuilder { + fn default() -> Self { + Self::new() + } +} + +impl WindowsShimBuilder { + #[must_use] + pub fn new() -> Self { + let platform = litebox_platform_multiplex::platform(); + Self { + litebox: LiteBox::new(platform), + } + } + + #[must_use] + pub fn litebox(&self) -> &LiteBox { + &self.litebox + } + + /// Build a default layered file system with the given in-memory and tar read-only layers. + #[must_use] + pub fn default_fs( + &self, + in_mem_fs: litebox::fs::in_mem::FileSystem, + tar_ro_fs: litebox::fs::tar_ro::FileSystem, + ) -> DefaultFS { + default_fs(&self.litebox, in_mem_fs, tar_ro_fs) + } + + #[must_use] + pub fn build(self) -> WindowsShim { + let page_manager = Arc::new(PageManager::new(&self.litebox)); + WindowsShim { + litebox: Arc::new(self.litebox), + page_manager, + _fs: PhantomData, + } + } +} + +/// A placeholder Windows shim. +pub struct WindowsShim { + litebox: Arc>, + page_manager: Arc, + _fs: PhantomData, +} + +impl WindowsShim { + /// Loads the program at `path` as the shim's initial task. + /// + /// TODO: PEB/TEB setup and initial handle table state are not yet implemented. + pub fn load_program( + &self, + fs: Arc, + path: &str, + _argv: Vec, + _envp: Vec, + ) -> Result, WindowsLoadError> { + let file = PeImageFile::open(fs, path)?; + let parsed = PeParsedFile::parse(&mut &file).map_err(|e| match e { + PeParseError::Io(io) => WindowsLoadError::Access(io), + other => WindowsLoadError::Parse(other), + })?; + let mut mapper = PeImageMapper { + file: &file, + page_manager: &self.page_manager, + chunk: alloc::vec![0u8; FILE_CHUNK_BYTES], + }; + let mut memory = PeImageMemory; + let mapping = parsed.load(&mut mapper, &mut memory).map_err(|e| match e { + PeLoadError::Map(access) => WindowsLoadError::Access(access), + other => WindowsLoadError::Load(other), + })?; + let entry_point = mapping.entry_point; + + let length = + NonZeroPageSize::new(INITIAL_STACK_SIZE).ok_or(PeImageAccessError::AddressOverflow)?; + // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` does not set + // `fixed_addr`, so the kernel picks a free range — no existing mapping can be displaced. + let stack_base = unsafe { + self.page_manager + .create_stack_pages(None, length, CreatePagesFlags::empty()) + .map_err(PeImageAccessError::Mapping)? + }; + let stack_top = stack_base + .as_usize() + .checked_add(INITIAL_STACK_SIZE) + .ok_or(PeImageAccessError::AddressOverflow)?; + + Ok(LoadedProgram { + entrypoints: WindowsShimEntrypoints { + entry_point, + stack_top, + _fs: PhantomData, + }, + process: WindowsShimProcess, + }) + } + + #[must_use] + pub fn litebox(&self) -> &LiteBox { + &self.litebox + } +} + +/// The shim entrypoint object passed to the platform. +pub struct WindowsShimEntrypoints { + entry_point: usize, + stack_top: usize, + _fs: PhantomData, +} + +impl EnterShim for WindowsShimEntrypoints { + type ExecutionContext = litebox_common_linux::PtRegs; + + fn init(&self, ctx: &mut Self::ExecutionContext) -> ContinueOperation { + ctx.rip = self.entry_point; + ctx.rsp = self.stack_top; + ctx.eflags = 0x202; + litebox_util_log::debug!( + entry_point:% = format_args!("{:#x}", self.entry_point), + stack_top:% = format_args!("{:#x}", self.stack_top); + "Starting initial Windows guest thread" + ); + ContinueOperation::Resume + } + + fn syscall(&self, _ctx: &mut Self::ExecutionContext) -> ContinueOperation { + // TODO: Decode and dispatch NT syscalls. + ContinueOperation::Terminate + } + + fn exception( + &self, + ctx: &mut Self::ExecutionContext, + info: &ExceptionInfo, + ) -> ContinueOperation { + litebox_util_log::debug!( + exception:? = info.exception, + rip:% = format_args!("{:#x}", ctx.rip), + cr2:% = format_args!("{:#x}", info.cr2); + "Windows guest exception" + ); + // TODO: Translate hardware exceptions into Windows SEH where appropriate. + ContinueOperation::Terminate + } + + fn interrupt(&self, _ctx: &mut Self::ExecutionContext) -> ContinueOperation { + // TODO: Handle host interrupts for Windows guest waits/APCs. + ContinueOperation::Terminate + } +} + +/// A loaded Windows program and the process handle used to wait for it. +pub struct LoadedProgram { + /// The initial-thread entrypoint state passed to the platform's `run_thread`. + pub entrypoints: WindowsShimEntrypoints, + /// Handle used to wait for the loaded program to exit. + pub process: WindowsShimProcess, +} + +/// A placeholder handle to a process loaded via [`WindowsShim::load_program`]. +pub struct WindowsShimProcess; + +impl WindowsShimProcess { + /// Wait for the process to exit, returning its exit code. + /// + /// Currently a placeholder that returns a fixed exit code immediately. + /// Once NT process lifecycle exists, this will actually block. + #[must_use] + pub fn wait(&self) -> i32 { + PLACEHOLDER_EXIT_CODE + } +} + +/// Errors that can occur while opening, parsing, and mapping a Windows PE image. +#[derive(Debug, Error)] +pub enum WindowsLoadError { + /// PE parsing failed. + #[error("failed to parse PE image")] + Parse(#[source] PeParseError), + /// PE image mapping failed. + #[error("failed to load PE image")] + Load(#[source] PeLoadError), + /// Opening the PE image failed. + #[error(transparent)] + Access(#[from] PeImageAccessError), +} + +/// Errors from the shim-side PE image backing file and memory mapper. +#[derive(Debug, Error)] +pub enum PeImageAccessError { + /// Opening the executable failed. + #[error("failed to open PE image")] + Open(#[from] litebox::fs::errors::OpenError), + /// Reading the executable failed. + #[error("failed to read PE image")] + Read(#[from] litebox::fs::errors::ReadError), + /// Reading file metadata failed. + #[error("failed to read PE image metadata")] + FileStatus(#[from] litebox::fs::errors::FileStatusError), + /// The backing file ended before the requested range was read. + #[error("short read from PE image")] + ShortRead, + /// A PE file offset or image address overflowed this host representation. + #[error("PE image address overflow")] + AddressOverflow, + /// A memory mapping operation failed. + #[error(transparent)] + Mapping(#[from] MappingError), + /// A memory protection operation failed. + #[error(transparent)] + Protect(#[from] VmemProtectError), + /// A mapped memory access failed. + #[error("mapped PE image memory access failed")] + MemoryAccess, +} + +struct PeImageFile { + fs: Arc, + fd: TypedFd, +} + +impl PeImageFile { + fn open(fs: Arc, path: &str) -> Result { + let fd = fs.open(path, OFlags::RDONLY, Mode::empty())?; + Ok(Self { fs, fd }) + } + + fn read_exact_at( + &self, + mut offset: usize, + mut buf: &mut [u8], + ) -> Result<(), PeImageAccessError> { + while !buf.is_empty() { + let bytes_read = self.fs.read(&self.fd, buf, Some(offset))?; + if bytes_read == 0 { + return Err(PeImageAccessError::ShortRead); + } + offset = offset + .checked_add(bytes_read) + .ok_or(PeImageAccessError::AddressOverflow)?; + buf = &mut buf[bytes_read..]; + } + Ok(()) + } +} + +impl Drop for PeImageFile { + fn drop(&mut self) { + if let Err(e) = self.fs.close(&self.fd) { + litebox_util_log::warn!(error:? = e; "failed to close PE image file"); + } + } +} + +impl ReadAt for &'_ PeImageFile { + type Error = PeImageAccessError; + + fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error> { + self.read_exact_at( + offset + .try_into() + .map_err(|_| PeImageAccessError::AddressOverflow)?, + buf, + ) + } + + fn size(&mut self) -> Result { + self.fs + .fd_file_status(&self.fd)? + .size + .try_into() + .map_err(|_| PeImageAccessError::AddressOverflow) + } +} + +struct PeImageMapper<'a, FS: ShimFS> { + file: &'a PeImageFile, + page_manager: &'a WindowsPageManager, + /// Reusable per-call I/O staging buffer for [`MapMemory::map_file`]. + chunk: Vec, +} + +impl MapMemory for PeImageMapper<'_, FS> { + type Error = PeImageAccessError; + + fn reserve( + &mut self, + preferred_base: usize, + len: usize, + _align: usize, + ) -> Result { + let length = NonZeroPageSize::new(len).ok_or(PeImageAccessError::AddressOverflow)?; + let suggested_address = if preferred_base == 0 { + None + } else { + Some(NonZeroAddress::new(preferred_base).ok_or(PeImageAccessError::AddressOverflow)?) + }; + + // SAFETY: `CreatePagesFlags::empty()` does not set `fixed_addr`, so the kernel + // treats `suggested_address` as a hint and never silently unmaps an existing + // mapping; the documented overlap precondition therefore does not apply. + let ptr = unsafe { + self.page_manager.create_inaccessible_pages( + suggested_address, + length, + CreatePagesFlags::empty(), + |_| Ok(0), + )? + }; + Ok(ptr.as_usize()) + } + + fn map_zero( + &mut self, + address: usize, + len: usize, + prot: &Protection, + ) -> Result<(), Self::Error> { + make_pages_writable(self.page_manager, address, len)?; + let ptr = ::RawMutPointer::::from_usize(address); + let mut written = 0; + while written < len { + let chunk = (len - written).min(ZERO_CHUNK.len()); + ptr.copy_from_slice(written, &ZERO_CHUNK[..chunk]) + .ok_or(PeImageAccessError::MemoryAccess)?; + written += chunk; + } + protect_pages(self.page_manager, address, len, *prot) + } + + fn map_file( + &mut self, + address: usize, + len: usize, + offset: u64, + prot: &Protection, + ) -> Result<(), Self::Error> { + make_pages_writable(self.page_manager, address, len)?; + let ptr = ::RawMutPointer::::from_usize(address); + let file_offset: usize = offset + .try_into() + .map_err(|_| PeImageAccessError::AddressOverflow)?; + let mut read = 0; + while read < len { + let remaining = len - read; + let n = remaining.min(self.chunk.len()); + self.file.read_exact_at( + file_offset + .checked_add(read) + .ok_or(PeImageAccessError::AddressOverflow)?, + &mut self.chunk[..n], + )?; + ptr.copy_from_slice(read, &self.chunk[..n]) + .ok_or(PeImageAccessError::MemoryAccess)?; + read += n; + } + protect_pages(self.page_manager, address, len, *prot) + } + + fn protect( + &mut self, + address: usize, + len: usize, + prot: &Protection, + ) -> Result<(), Self::Error> { + protect_pages(self.page_manager, address, len, *prot) + } +} + +struct PeImageMemory; + +impl AccessMemory for PeImageMemory { + fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Fault> { + let ptr = ::RawConstPointer::::from_usize(address); + buf.copy_from_slice(&ptr.to_owned_slice(buf.len()).ok_or(Fault)?); + Ok(()) + } + + fn write(&mut self, address: usize, data: &[u8]) -> Result<(), Fault> { + let ptr = ::RawMutPointer::::from_usize(address); + ptr.copy_from_slice(0, data).ok_or(Fault) + } +} + +fn make_pages_writable( + page_manager: &WindowsPageManager, + address: usize, + len: usize, +) -> Result<(), PeImageAccessError> { + let (start, len) = page_range(address, len)?; + if len == 0 { + return Ok(()); + } + let ptr = ::RawMutPointer::::from_usize(start); + // SAFETY: Loading happens before the initial guest thread is allowed to execute. + unsafe { page_manager.make_pages_writable(ptr, len)? }; + Ok(()) +} + +fn protect_pages( + page_manager: &WindowsPageManager, + address: usize, + len: usize, + prot: Protection, +) -> Result<(), PeImageAccessError> { + let (start, len) = page_range(address, len)?; + if len == 0 { + return Ok(()); + } + let ptr = ::RawMutPointer::::from_usize(start); + // SAFETY: All `make_pages_*` calls happen during PE load, before the initial + // guest thread starts, so there is no concurrent read/write/execute on these + // pages. The RWX arm is only reached when a section's COFF characteristics + // demand WRITE|EXECUTE; the bytes copied into the section come from the + // attacker-controlled PE file and are not executed until protections are set, + // so this is no looser than running the same PE under the real Windows loader. + match (prot.read, prot.write, prot.execute) { + (_, true, true) => unsafe { page_manager.make_pages_rwx(ptr, len)? }, + (_, true, false) => unsafe { page_manager.make_pages_writable(ptr, len)? }, + (_, false, true) => unsafe { page_manager.make_pages_executable(ptr, len)? }, + (true, false, false) => unsafe { page_manager.make_pages_readable(ptr, len)? }, + (false, false, false) => unsafe { page_manager.make_pages_inaccessible(ptr, len)? }, + } + Ok(()) +} + +fn page_range(address: usize, len: usize) -> Result<(usize, usize), PeImageAccessError> { + if len == 0 { + return Ok((address, 0)); + } + let start = page_align_down(address); + let end = address + .checked_add(len) + .and_then(|v| v.checked_next_multiple_of(PAGE_SIZE)) + .ok_or(PeImageAccessError::AddressOverflow)?; + Ok((start, end - start)) +} + +fn default_fs( + litebox: &LiteBox, + in_mem_fs: litebox::fs::in_mem::FileSystem, + tar_ro_fs: litebox::fs::tar_ro::FileSystem, +) -> WindowsFS { + let dev_stdio = litebox::fs::devices::FileSystem::new(litebox); + litebox::fs::layered::FileSystem::new( + litebox, + in_mem_fs, + litebox::fs::layered::FileSystem::new( + litebox, + dev_stdio, + tar_ro_fs, + litebox::fs::layered::LayeringSemantics::LowerLayerReadOnly, + ), + litebox::fs::layered::LayeringSemantics::LowerLayerWritableFiles, + ) +} From 67af8a7d9a1e6c00d17d44d5127b52bb10badc8c Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 22 May 2026 11:49:38 -0700 Subject: [PATCH 002/319] Windows PE rewriter (#863) This PR supports rewriting PE and loading its trampoline. For syscall rewriting, there is a common pattern that the previous rewriter cannot handle: ``` test byte ptr [...], 1 jne +3 syscall ret int 0x2e ret ``` Adds a heuristic to rewrite it to: ``` 1: jmp to trampoline code ret nop ... jmp 1b ``` It also rewrites all gs based instructions to fs-based ones. For now, the guest program still starts with the main executable instead of ntdll because of lack of set up of PEB/TEB. --- Cargo.lock | 3 + litebox_common_windows/Cargo.toml | 1 + litebox_common_windows/src/loader.rs | 216 ++++++++- litebox_runner_windows_userland/Cargo.toml | 4 + litebox_runner_windows_userland/tests/run.rs | 142 +++++- litebox_shim_windows/src/lib.rs | 472 +++++-------------- litebox_shim_windows/src/loader/mod.rs | 8 + litebox_shim_windows/src/loader/pe.rs | 425 +++++++++++++++++ litebox_syscall_rewriter/Cargo.toml | 2 +- litebox_syscall_rewriter/src/lib.rs | 445 +++++++++++++++-- litebox_syscall_rewriter/src/main.rs | 10 +- 11 files changed, 1285 insertions(+), 443 deletions(-) create mode 100644 litebox_shim_windows/src/loader/mod.rs create mode 100644 litebox_shim_windows/src/loader/pe.rs diff --git a/Cargo.lock b/Cargo.lock index b62d88e958..c9b232a35b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1497,6 +1497,7 @@ version = "0.1.0" dependencies = [ "object", "thiserror", + "zerocopy", ] [[package]] @@ -1708,7 +1709,9 @@ dependencies = [ "litebox_platform_multiplex", "litebox_platform_windows_userland", "litebox_shim_windows", + "litebox_syscall_rewriter", "litebox_util_log", + "tar", "tracing-subscriber", ] diff --git a/litebox_common_windows/Cargo.toml b/litebox_common_windows/Cargo.toml index 56a6a7dbf9..aa6dcbcb4f 100644 --- a/litebox_common_windows/Cargo.toml +++ b/litebox_common_windows/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] object = { version = "0.36.7", default-features = false, features = ["pe", "read_core"] } thiserror = { version = "2.0.6", default-features = false } +zerocopy = { version = "0.8", features = ["derive"] } [lints] workspace = true diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs index fd9c7e829d..0d478d012c 100644 --- a/litebox_common_windows/src/loader.rs +++ b/litebox_common_windows/src/loader.rs @@ -8,6 +8,7 @@ use alloc::vec::Vec; use core::cmp; use core::mem::size_of; +use zerocopy::{FromBytes, IntoBytes}; use object::endian::LittleEndian as LE; use object::pe; @@ -26,9 +27,10 @@ pub struct PeParsedFile { /// Basic image metadata from the PE optional and COFF headers. pub image: PeImageInfo, /// Raw PE section headers in file order. - pub sections: Vec, + sections: Vec, /// Data directory entries indexed by `IMAGE_DIRECTORY_ENTRY_*`. pub data_directories: Vec, + trampoline: Option, } /// Basic PE image metadata needed by the Windows shim loader. @@ -56,6 +58,25 @@ pub struct MappingInfo { pub entry_point: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct PeTrampolineInfo { + rva: usize, + size: usize, + file_offset: u64, + syscall_entry_point: usize, +} + +#[repr(C, packed)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes)] +struct TrampolineHeader64 { + magic: [u8; 8], + file_offset: u64, + rva: u64, + trampoline_size: u64, +} + +const TRAMPOLINE_MAGIC: [u8; 8] = *b"LITEBOX0"; + /// A PE data directory entry. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PeDataDirectory { @@ -72,6 +93,12 @@ pub enum PeParseError { /// The file is not a supported Windows executable image. #[error("unsupported PE image")] UnsupportedImage, + /// The LiteBox trampoline footer is malformed. + #[error("bad LiteBox trampoline")] + BadTrampoline, + /// The LiteBox trampoline footer has an unsupported version. + #[error("invalid LiteBox trampoline version")] + BadTrampolineVersion, /// A PE field overflowed the host representation used by this parser. #[error("PE field overflow")] Overflow, @@ -136,9 +163,16 @@ impl PeParsedFile { image, sections, data_directories, + trampoline: None, }) } + /// Returns whether the image has a parsed LiteBox syscall trampoline. + #[must_use] + pub fn has_trampoline(&self) -> bool { + self.trampoline.is_some() + } + /// Load the PE image into memory. /// /// This maps PE headers and sections into their image locations, @@ -149,6 +183,18 @@ impl PeParsedFile { &self, mapper: &mut M, mem: &mut impl AccessMemory, + ) -> Result> { + self.load_with_writable_sections(mapper, mem, &[]) + } + + /// Load the PE image into memory, keeping selected sections writable. + /// + /// This is intended for target-specific loader data such as ntdll's `.mrdata`. + pub fn load_with_writable_sections( + &self, + mapper: &mut M, + mem: &mut impl AccessMemory, + writable_section_names: &[&[u8]], ) -> Result> { let preferred_base = self.image.image_base; let image_size = checked_next_multiple_of!( @@ -159,13 +205,15 @@ impl PeParsedFile { if image_size == 0 { return Err(PeLoadError::InvalidImage); } + let mapping_size = self.mapping_size::(image_size)?; let base_addr = mapper - .reserve(preferred_base, image_size, PAGE_SIZE) + .reserve(preferred_base, mapping_size, PAGE_SIZE) .map_err(PeLoadError::Map)?; let image_end = checked_add_invalid!(base_addr, image_size)?; let headers_size = self.image.size_of_headers; + if headers_size > image_size { return Err(PeLoadError::InvalidImage); } @@ -241,11 +289,15 @@ impl PeParsedFile { .protect( protect_start, protect_end - protect_start, - &Protection::from_section_characteristics(section.characteristics.get(LE)), + &Protection::from_section(section, writable_section_names), ) .map_err(PeLoadError::Map)?; } + if self.trampoline.is_some() { + self.load_trampoline(mapper, mem, base_addr)?; + } + let entry_point = checked_add!( base_addr, self.image.entry_point_rva, @@ -254,11 +306,131 @@ impl PeParsedFile { Ok(MappingInfo { base_addr, - image_size, + image_size: mapping_size, entry_point, }) } + /// Parse the LiteBox PE trampoline footer, if present. + /// + /// The trampoline RVA is relative to the image base. The first pointer-sized + /// word of the mapped trampoline is patched with `syscall_entry_point` when + /// the image is loaded. + pub fn parse_trampoline( + &mut self, + file: &mut F, + syscall_entry_point: usize, + ) -> Result<(), PeParseError> { + if syscall_entry_point == 0 { + return Ok(()); + } + + let file_size = file.size().map_err(PeParseError::Io)?; + let header_size = size_of::(); + if file_size < header_size as u64 { + return Ok(()); + } + + let header_offset = file_size - header_size as u64; + let mut header_buf = [0u8; size_of::()]; + file.read_at(header_offset, &mut header_buf) + .map_err(PeParseError::Io)?; + let header = TrampolineHeader64::read_from_bytes(&header_buf) + .map_err(|_| PeParseError::BadTrampoline)?; + let magic = header.magic; + if magic != TRAMPOLINE_MAGIC { + if &magic[0..7] == b"LITEBOX" { + return Err(PeParseError::BadTrampolineVersion); + } + return Ok(()); + } + + let file_offset = header.file_offset; + let rva = usize_from_u64(header.rva)?; + let trampoline_size = usize_from_u64(header.trampoline_size)?; + let image_size = checked_next_multiple_of!( + self.image.size_of_image, + PAGE_SIZE, + PeParseError::BadTrampoline + )?; + + if trampoline_size == 0 + || !file_offset.is_multiple_of(PAGE_SIZE as u64) + || !rva.is_multiple_of(PAGE_SIZE) + || rva < image_size + || file_offset + .checked_add(trampoline_size as u64) + .ok_or(PeParseError::BadTrampoline)? + != header_offset + { + return Err(PeParseError::BadTrampoline); + } + + self.trampoline = Some(PeTrampolineInfo { + rva, + size: trampoline_size, + file_offset, + syscall_entry_point, + }); + Ok(()) + } + + fn mapping_size(&self, image_size: usize) -> Result> { + let Some(trampoline) = &self.trampoline else { + return Ok(image_size); + }; + + trampoline + .rva + .checked_add(trampoline.size) + .and_then(|trampoline_end| trampoline_end.checked_next_multiple_of(PAGE_SIZE)) + .map(|trampoline_end| image_size.max(trampoline_end)) + .ok_or(PeLoadError::InvalidImage) + } + + fn load_trampoline( + &self, + mapper: &mut M, + mem: &mut impl AccessMemory, + base_addr: usize, + ) -> Result<(), PeLoadError> { + let trampoline = self.trampoline.as_ref().unwrap(); + let trampoline_start = base_addr + .checked_add(trampoline.rva) + .ok_or(PeLoadError::InvalidImage)?; + let trampoline_size = + checked_next_multiple_of!(trampoline.size, PAGE_SIZE, PeLoadError::InvalidImage)?; + mapper + .map_file( + trampoline_start, + trampoline_size, + trampoline.file_offset, + &Protection { + read: true, + write: true, + execute: false, + }, + ) + .map_err(PeLoadError::Map)?; + + mem.write( + trampoline_start, + &trampoline.syscall_entry_point.to_ne_bytes(), + )?; + + mapper + .protect( + trampoline_start, + trampoline_size, + &Protection { + read: true, + write: false, + execute: true, + }, + ) + .map_err(PeLoadError::Map) + } + fn apply_base_relocations( &self, base_addr: usize, @@ -464,12 +636,6 @@ fn parse_headers( } }) .collect(); - for dir in &data_directories { - let end = checked_add!(dir.virtual_address, dir.size, PeParseError::Overflow)?; - if (end as usize) > image.size_of_image { - return Err(PeParseError::UnsupportedImage); - } - } // Section headers sit at `nt_offset + 4 (signature) + size_of::() + size_of_optional_header`. let num_sections = nt.file_header.number_of_sections.get(LE) as usize; @@ -610,8 +776,10 @@ pub trait MapMemory { /// Trait for reading and writing memory that has been mapped via [`MapMemory`]. pub trait AccessMemory { + /// Read from memory. fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Fault>; + /// Write to memory. fn write(&mut self, address: usize, data: &[u8]) -> Result<(), Fault>; } @@ -642,11 +810,29 @@ impl Protection { execute: false, }; - fn from_section_characteristics(characteristics: u32) -> Self { - Self { - read: characteristics & pe::IMAGE_SCN_MEM_READ != 0, - write: characteristics & pe::IMAGE_SCN_MEM_WRITE != 0, - execute: characteristics & pe::IMAGE_SCN_MEM_EXECUTE != 0, + fn from_section(section: &pe::ImageSectionHeader, writable_section_names: &[&[u8]]) -> Self { + let characteristics = section.characteristics.get(LE); + let mut protection = Self { + read: characteristics & object::pe::IMAGE_SCN_MEM_READ != 0, + write: characteristics & object::pe::IMAGE_SCN_MEM_WRITE != 0, + execute: characteristics & object::pe::IMAGE_SCN_MEM_EXECUTE != 0, + }; + if writable_section_names + .iter() + .any(|name| section_name_eq(section, name)) + { + protection.write = true; } + + protection } } + +fn section_name_eq(section: &pe::ImageSectionHeader, name: &[u8]) -> bool { + let end = section + .name + .iter() + .position(|byte| *byte == 0) + .unwrap_or(section.name.len()); + §ion.name[..end] == name +} diff --git a/litebox_runner_windows_userland/Cargo.toml b/litebox_runner_windows_userland/Cargo.toml index 52c45de4af..940b884d1f 100644 --- a/litebox_runner_windows_userland/Cargo.toml +++ b/litebox_runner_windows_userland/Cargo.toml @@ -14,5 +14,9 @@ litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows", de litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = ["backend_tracing"] } tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +[dev-dependencies] +litebox_syscall_rewriter = { version = "0.1.0", path = "../litebox_syscall_rewriter" } +tar = "0.4" + [lints] workspace = true diff --git a/litebox_runner_windows_userland/tests/run.rs b/litebox_runner_windows_userland/tests/run.rs index 092256cca1..943a5aa215 100644 --- a/litebox_runner_windows_userland/tests/run.rs +++ b/litebox_runner_windows_userland/tests/run.rs @@ -13,15 +13,33 @@ unsafe extern "system" { #[test] fn loads_minimal_pe_without_imports() { let test_dir = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("no_import"); + let _ = std::fs::remove_dir_all(&test_dir); std::fs::create_dir_all(&test_dir).unwrap(); let pe_path = build_no_import_pe(&test_dir); - println!("Built no-import PE fixture at `{}`", pe_path.display()); - - let tar_path = test_dir.join("no_import.tar"); - create_tar_with_exe(&test_dir, &tar_path, "no_import.exe"); + println!( + "Built rewritten no-import PE fixture at `{}`", + pe_path.display() + ); + for dll_name in ["ntdll.dll", "kernel32.dll", "kernelbase.dll"] { + let dll_path = build_rewritten_system_dll(&test_dir, dll_name); + println!( + "Built rewritten {dll_name} fixture at `{}`", + dll_path.display() + ); + } + // ntdll's NLS init opens these locale tables before reaching the test's + // `NtTerminateProcess` syscall; copy them verbatim from the host. + for nls_name in ["c_1252.nls", "c_437.nls", "c_10000.nls", "locale.nls"] { + let nls_path = copy_host_system32_file(&test_dir, nls_name); + println!("Copied {nls_name} fixture at `{}`", nls_path.display()); + } + let tar_path = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("no_import.tar"); + create_tar_with_dir(&test_dir, &tar_path); let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_litebox_runner_windows_userland")); + // 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(), @@ -43,6 +61,7 @@ fn loads_minimal_pe_without_imports() { fn build_no_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { let source_path = test_dir.join("no_import.rs"); + let raw_exe_path = test_dir.join("no_import.raw.exe"); let exe_path = test_dir.join("no_import.exe"); let syscall_number = nt_terminate_process_syscall_number(); println!("Using NtTerminateProcess syscall number `{syscall_number:#x}`"); @@ -66,7 +85,7 @@ fn build_no_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { "-C", "link-arg=/NODEFAULTLIB", "-o", - exe_path.to_str().unwrap(), + raw_exe_path.to_str().unwrap(), ]) .output() .expect("failed to run rustc for the no-import Windows PE fixture"); @@ -77,6 +96,11 @@ fn build_no_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); + + let rewritten = + litebox_syscall_rewriter::rewrite_binary(&std::fs::read(raw_exe_path).unwrap(), None) + .expect("failed to rewrite no-import Windows PE fixture"); + std::fs::write(&exe_path, rewritten).unwrap(); exe_path } @@ -145,22 +169,96 @@ fn nt_terminate_process_syscall_number() -> u32 { ) } -fn create_tar_with_exe(test_dir: &std::path::Path, tar_path: &std::path::Path, exe_name: &str) { - let output = std::process::Command::new("tar.exe") - .args([ - "-cf", - tar_path.to_str().unwrap(), - "-C", - test_dir.to_str().unwrap(), - exe_name, - ]) - .output() - .expect("failed to run tar.exe for the no-import Windows PE fixture"); +fn build_rewritten_system_dll(test_dir: &std::path::Path, dll_name: &str) -> std::path::PathBuf { + let dll_path = fixture_system32_path(test_dir, dll_name); + let host_dll = std::fs::read(host_system32_file_path(dll_name)) + .unwrap_or_else(|error| panic!("failed to read host {dll_name}: {error}")); + let rewritten = match litebox_syscall_rewriter::rewrite_binary(&host_dll, None) { + Ok(rewritten) => rewritten, + Err(litebox_syscall_rewriter::Error::UnpatchableSyscalls(_)) => panic!( + "failed to rewrite host {dll_name}; required support: patch dense ntdll syscall stubs or provide a pre-rewritten guest DLL" + ), + Err(error) => panic!("failed to rewrite host {dll_name}: {error}"), + }; + std::fs::write(&dll_path, rewritten).unwrap(); + dll_path +} - assert!( - output.status.success(), - "failed to create tar for no-import Windows PE fixture\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); +fn copy_host_system32_file(test_dir: &std::path::Path, file_name: &str) -> std::path::PathBuf { + let fixture_path = fixture_system32_path(test_dir, file_name); + std::fs::copy(host_system32_file_path(file_name), &fixture_path) + .unwrap_or_else(|error| panic!("failed to copy host {file_name}: {error}")); + fixture_path +} + +fn fixture_system32_path(test_dir: &std::path::Path, file_name: &str) -> std::path::PathBuf { + let system32_dir = test_dir.join("Windows").join("System32"); + std::fs::create_dir_all(&system32_dir).unwrap(); + system32_dir.join(file_name) +} + +fn host_system32_file_path(file_name: &str) -> std::path::PathBuf { + std::env::var_os("SystemRoot") + .map_or_else( + || std::path::PathBuf::from(r"C:\Windows"), + std::path::PathBuf::from, + ) + .join("System32") + .join(file_name) +} + +fn create_tar_with_dir(test_dir: &std::path::Path, tar_path: &std::path::Path) { + let output_file = std::fs::File::create(tar_path) + .expect("failed to create tar for the no-import Windows PE fixture"); + let mut builder = tar::Builder::new(output_file); + append_regular_files_to_ustar(&mut builder, test_dir, test_dir); + builder + .finish() + .expect("failed to finalize tar for the no-import Windows PE fixture"); +} + +fn append_regular_files_to_ustar( + builder: &mut tar::Builder, + root: &std::path::Path, + dir: &std::path::Path, +) { + for entry in std::fs::read_dir(dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + append_regular_files_to_ustar(builder, root, &path); + continue; + } + + // Avoid nesting tar files from previous runs into the fixture archive. + if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("tar")) + { + continue; + } + + let data = std::fs::read(&path).unwrap_or_else(|error| { + panic!("failed to read fixture file {}: {error}", path.display()) + }); + let mut header = tar::Header::new_ustar(); + header.set_size(data.len() as u64); + header.set_mode(0o644); + header.set_uid(1000); + header.set_gid(1000); + header.set_mtime(0); + header.set_entry_type(tar::EntryType::Regular); + header.set_cksum(); + + let relative = path.strip_prefix(root).unwrap(); + let relative = relative.to_string_lossy().replace('\\', "/"); + builder + .append_data(&mut header, relative, data.as_slice()) + .unwrap_or_else(|error| { + panic!( + "failed to append fixture file {} to tar: {error}", + path.display() + ) + }); + } } diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index a2487f939f..5d0bc5bce7 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -14,29 +14,19 @@ extern crate alloc; use alloc::sync::Arc; use alloc::vec::Vec; use core::marker::PhantomData; +use core::sync::atomic::{AtomicI32, Ordering}; -use litebox::fd::TypedFd; -use litebox::fs::{Mode, OFlags}; +use litebox::LiteBox; use litebox::mm::PageManager; -use litebox::mm::linux::{ - CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, VmemProtectError, -}; -use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; -use litebox::{LiteBox, platform::RawPointerProvider}; -use litebox_common_windows::loader::{ - AccessMemory, Fault, MapMemory, PAGE_SIZE, PeLoadError, PeParseError, PeParsedFile, Protection, - ReadAt, page_align_down, -}; +use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; use litebox_platform_multiplex::Platform; -use thiserror::Error; -const INITIAL_STACK_SIZE: usize = 1024 * 1024; -const PLACEHOLDER_EXIT_CODE: i32 = 1; -const ZERO_CHUNK: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; -const FILE_CHUNK_BYTES: usize = 64 * 1024; +mod loader; -type WindowsPageManager = PageManager; +const DEFAULT_PROCESS_EXIT_CODE: i32 = 1; + +pub(crate) type WindowsPageManager = PageManager; pub type DefaultFS = WindowsFS; @@ -91,21 +81,15 @@ impl WindowsShimBuilder { #[must_use] pub fn build(self) -> WindowsShim { - let page_manager = Arc::new(PageManager::new(&self.litebox)); - WindowsShim { - litebox: Arc::new(self.litebox), - page_manager, + let global = Arc::new(GlobalState { + page_manager: PageManager::new(&self.litebox), _fs: PhantomData, - } + }); + WindowsShim(global) } } -/// A placeholder Windows shim. -pub struct WindowsShim { - litebox: Arc>, - page_manager: Arc, - _fs: PhantomData, -} +pub struct WindowsShim(Arc>); impl WindowsShim { /// Loads the program at `path` as the shim's initial task. @@ -117,81 +101,126 @@ impl WindowsShim { path: &str, _argv: Vec, _envp: Vec, - ) -> Result, WindowsLoadError> { - let file = PeImageFile::open(fs, path)?; - let parsed = PeParsedFile::parse(&mut &file).map_err(|e| match e { - PeParseError::Io(io) => WindowsLoadError::Access(io), - other => WindowsLoadError::Parse(other), - })?; - let mut mapper = PeImageMapper { - file: &file, - page_manager: &self.page_manager, - chunk: alloc::vec![0u8; FILE_CHUNK_BYTES], - }; - let mut memory = PeImageMemory; - let mapping = parsed.load(&mut mapper, &mut memory).map_err(|e| match e { - PeLoadError::Map(access) => WindowsLoadError::Access(access), - other => WindowsLoadError::Load(other), - })?; - let entry_point = mapping.entry_point; - - let length = - NonZeroPageSize::new(INITIAL_STACK_SIZE).ok_or(PeImageAccessError::AddressOverflow)?; - // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` does not set - // `fixed_addr`, so the kernel picks a free range — no existing mapping can be displaced. - let stack_base = unsafe { - self.page_manager - .create_stack_pages(None, length, CreatePagesFlags::empty()) - .map_err(PeImageAccessError::Mapping)? - }; - let stack_top = stack_base - .as_usize() - .checked_add(INITIAL_STACK_SIZE) - .ok_or(PeImageAccessError::AddressOverflow)?; - + ) -> Result, loader::WindowsLoadError> { + let load_info = loader::PeLoader::new(fs, &self.0.page_manager).load(path)?; + let process = Arc::new(Process { + ntdll_mapping: load_info.ntdll_mapping, + exit_code: AtomicI32::new(DEFAULT_PROCESS_EXIT_CODE), + }); Ok(LoadedProgram { entrypoints: WindowsShimEntrypoints { - entry_point, - stack_top, - _fs: PhantomData, + task: Task { + process: process.clone(), + entry_point: load_info.entry_point, + stack_top: load_info.stack_top, + _phantom: PhantomData, + }, + _not_send: PhantomData, }, - process: WindowsShimProcess, + process, }) } +} +/// Global shim state shared by all Windows tasks loaded by this shim. +struct GlobalState { + page_manager: WindowsPageManager, + _fs: PhantomData, +} + +/// Per-process Windows state shared by every thread in the process. +pub struct Process { + ntdll_mapping: Option, + exit_code: AtomicI32, +} + +impl Process { + /// Wait for the process to exit, returning its exit code. + /// + /// Currently a placeholder that returns a fixed exit code immediately. + /// Once NT process lifecycle exists, this will actually block. #[must_use] - pub fn litebox(&self) -> &LiteBox { - &self.litebox + pub fn wait(&self) -> i32 { + // TODO: Wait for the NT process object once process lifecycle exists. + self.exit_code.load(Ordering::Relaxed) } } -/// The shim entrypoint object passed to the platform. -pub struct WindowsShimEntrypoints { +struct Task { + process: Arc, entry_point: usize, stack_top: usize, - _fs: PhantomData, + _phantom: PhantomData, } -impl EnterShim for WindowsShimEntrypoints { - type ExecutionContext = litebox_common_linux::PtRegs; - - fn init(&self, ctx: &mut Self::ExecutionContext) -> ContinueOperation { +impl Task { + fn init(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { ctx.rip = self.entry_point; - ctx.rsp = self.stack_top; + let stack_top_alignment = self.stack_top % 16; + debug_assert!(stack_top_alignment == 0 || stack_top_alignment == 8); + ctx.rsp = if stack_top_alignment == 0 { + self.stack_top - core::mem::size_of::() + } else { + self.stack_top + }; ctx.eflags = 0x202; + ctx.rdx = self + .process + .ntdll_mapping + .as_ref() + .map_or(0, |mapping| mapping.base_addr); litebox_util_log::debug!( entry_point:% = format_args!("{:#x}", self.entry_point), stack_top:% = format_args!("{:#x}", self.stack_top); "Starting initial Windows guest thread" ); + ContinueOperation::Resume } - fn syscall(&self, _ctx: &mut Self::ExecutionContext) -> ContinueOperation { - // TODO: Decode and dispatch NT syscalls. + fn handle_syscall_request(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { + // TODO: Decode the NT syscall number and dispatch only NtTerminateProcess here. + litebox_util_log::debug!( + syscall_number = ctx.orig_rax, + process_handle:% = format_args!("{:#x}", ctx.r10), + exit_status:% = format_args!("{:#x}", ctx.rdx); + "Handling temporary NtTerminateProcess syscall" + ); + self.process + .exit_code + .store(windows_exit_status_to_i32(ctx.rdx), Ordering::Relaxed); ContinueOperation::Terminate } + fn handle_interrupt_request( + &self, + _ctx: &mut litebox_common_linux::PtRegs, + ) -> ContinueOperation { + litebox_util_log::debug!( + stack_top:% = format_args!("{:#x}", self.stack_top); + "Windows guest interrupt" + ); + ContinueOperation::Resume + } +} + +/// The shim entrypoint object passed to the platform. +pub struct WindowsShimEntrypoints { + task: Task, + _not_send: PhantomData<*const ()>, +} + +impl EnterShim for WindowsShimEntrypoints { + type ExecutionContext = litebox_common_linux::PtRegs; + + fn init(&self, ctx: &mut Self::ExecutionContext) -> ContinueOperation { + self.task.init(ctx) + } + + fn syscall(&self, ctx: &mut Self::ExecutionContext) -> ContinueOperation { + self.task.handle_syscall_request(ctx) + } + fn exception( &self, ctx: &mut Self::ExecutionContext, @@ -207,297 +236,22 @@ impl EnterShim for WindowsShimEntrypoints { ContinueOperation::Terminate } - fn interrupt(&self, _ctx: &mut Self::ExecutionContext) -> ContinueOperation { - // TODO: Handle host interrupts for Windows guest waits/APCs. - ContinueOperation::Terminate + fn interrupt(&self, ctx: &mut Self::ExecutionContext) -> ContinueOperation { + self.task.handle_interrupt_request(ctx) } } +fn windows_exit_status_to_i32(status: usize) -> i32 { + let low_bits = u32::try_from(status & 0xffff_ffff).unwrap_or_default(); + i32::from_ne_bytes(low_bits.to_ne_bytes()) +} + /// A loaded Windows program and the process handle used to wait for it. pub struct LoadedProgram { /// The initial-thread entrypoint state passed to the platform's `run_thread`. pub entrypoints: WindowsShimEntrypoints, /// Handle used to wait for the loaded program to exit. - pub process: WindowsShimProcess, -} - -/// A placeholder handle to a process loaded via [`WindowsShim::load_program`]. -pub struct WindowsShimProcess; - -impl WindowsShimProcess { - /// Wait for the process to exit, returning its exit code. - /// - /// Currently a placeholder that returns a fixed exit code immediately. - /// Once NT process lifecycle exists, this will actually block. - #[must_use] - pub fn wait(&self) -> i32 { - PLACEHOLDER_EXIT_CODE - } -} - -/// Errors that can occur while opening, parsing, and mapping a Windows PE image. -#[derive(Debug, Error)] -pub enum WindowsLoadError { - /// PE parsing failed. - #[error("failed to parse PE image")] - Parse(#[source] PeParseError), - /// PE image mapping failed. - #[error("failed to load PE image")] - Load(#[source] PeLoadError), - /// Opening the PE image failed. - #[error(transparent)] - Access(#[from] PeImageAccessError), -} - -/// Errors from the shim-side PE image backing file and memory mapper. -#[derive(Debug, Error)] -pub enum PeImageAccessError { - /// Opening the executable failed. - #[error("failed to open PE image")] - Open(#[from] litebox::fs::errors::OpenError), - /// Reading the executable failed. - #[error("failed to read PE image")] - Read(#[from] litebox::fs::errors::ReadError), - /// Reading file metadata failed. - #[error("failed to read PE image metadata")] - FileStatus(#[from] litebox::fs::errors::FileStatusError), - /// The backing file ended before the requested range was read. - #[error("short read from PE image")] - ShortRead, - /// A PE file offset or image address overflowed this host representation. - #[error("PE image address overflow")] - AddressOverflow, - /// A memory mapping operation failed. - #[error(transparent)] - Mapping(#[from] MappingError), - /// A memory protection operation failed. - #[error(transparent)] - Protect(#[from] VmemProtectError), - /// A mapped memory access failed. - #[error("mapped PE image memory access failed")] - MemoryAccess, -} - -struct PeImageFile { - fs: Arc, - fd: TypedFd, -} - -impl PeImageFile { - fn open(fs: Arc, path: &str) -> Result { - let fd = fs.open(path, OFlags::RDONLY, Mode::empty())?; - Ok(Self { fs, fd }) - } - - fn read_exact_at( - &self, - mut offset: usize, - mut buf: &mut [u8], - ) -> Result<(), PeImageAccessError> { - while !buf.is_empty() { - let bytes_read = self.fs.read(&self.fd, buf, Some(offset))?; - if bytes_read == 0 { - return Err(PeImageAccessError::ShortRead); - } - offset = offset - .checked_add(bytes_read) - .ok_or(PeImageAccessError::AddressOverflow)?; - buf = &mut buf[bytes_read..]; - } - Ok(()) - } -} - -impl Drop for PeImageFile { - fn drop(&mut self) { - if let Err(e) = self.fs.close(&self.fd) { - litebox_util_log::warn!(error:? = e; "failed to close PE image file"); - } - } -} - -impl ReadAt for &'_ PeImageFile { - type Error = PeImageAccessError; - - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error> { - self.read_exact_at( - offset - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow)?, - buf, - ) - } - - fn size(&mut self) -> Result { - self.fs - .fd_file_status(&self.fd)? - .size - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow) - } -} - -struct PeImageMapper<'a, FS: ShimFS> { - file: &'a PeImageFile, - page_manager: &'a WindowsPageManager, - /// Reusable per-call I/O staging buffer for [`MapMemory::map_file`]. - chunk: Vec, -} - -impl MapMemory for PeImageMapper<'_, FS> { - type Error = PeImageAccessError; - - fn reserve( - &mut self, - preferred_base: usize, - len: usize, - _align: usize, - ) -> Result { - let length = NonZeroPageSize::new(len).ok_or(PeImageAccessError::AddressOverflow)?; - let suggested_address = if preferred_base == 0 { - None - } else { - Some(NonZeroAddress::new(preferred_base).ok_or(PeImageAccessError::AddressOverflow)?) - }; - - // SAFETY: `CreatePagesFlags::empty()` does not set `fixed_addr`, so the kernel - // treats `suggested_address` as a hint and never silently unmaps an existing - // mapping; the documented overlap precondition therefore does not apply. - let ptr = unsafe { - self.page_manager.create_inaccessible_pages( - suggested_address, - length, - CreatePagesFlags::empty(), - |_| Ok(0), - )? - }; - Ok(ptr.as_usize()) - } - - fn map_zero( - &mut self, - address: usize, - len: usize, - prot: &Protection, - ) -> Result<(), Self::Error> { - make_pages_writable(self.page_manager, address, len)?; - let ptr = ::RawMutPointer::::from_usize(address); - let mut written = 0; - while written < len { - let chunk = (len - written).min(ZERO_CHUNK.len()); - ptr.copy_from_slice(written, &ZERO_CHUNK[..chunk]) - .ok_or(PeImageAccessError::MemoryAccess)?; - written += chunk; - } - protect_pages(self.page_manager, address, len, *prot) - } - - fn map_file( - &mut self, - address: usize, - len: usize, - offset: u64, - prot: &Protection, - ) -> Result<(), Self::Error> { - make_pages_writable(self.page_manager, address, len)?; - let ptr = ::RawMutPointer::::from_usize(address); - let file_offset: usize = offset - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow)?; - let mut read = 0; - while read < len { - let remaining = len - read; - let n = remaining.min(self.chunk.len()); - self.file.read_exact_at( - file_offset - .checked_add(read) - .ok_or(PeImageAccessError::AddressOverflow)?, - &mut self.chunk[..n], - )?; - ptr.copy_from_slice(read, &self.chunk[..n]) - .ok_or(PeImageAccessError::MemoryAccess)?; - read += n; - } - protect_pages(self.page_manager, address, len, *prot) - } - - fn protect( - &mut self, - address: usize, - len: usize, - prot: &Protection, - ) -> Result<(), Self::Error> { - protect_pages(self.page_manager, address, len, *prot) - } -} - -struct PeImageMemory; - -impl AccessMemory for PeImageMemory { - fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Fault> { - let ptr = ::RawConstPointer::::from_usize(address); - buf.copy_from_slice(&ptr.to_owned_slice(buf.len()).ok_or(Fault)?); - Ok(()) - } - - fn write(&mut self, address: usize, data: &[u8]) -> Result<(), Fault> { - let ptr = ::RawMutPointer::::from_usize(address); - ptr.copy_from_slice(0, data).ok_or(Fault) - } -} - -fn make_pages_writable( - page_manager: &WindowsPageManager, - address: usize, - len: usize, -) -> Result<(), PeImageAccessError> { - let (start, len) = page_range(address, len)?; - if len == 0 { - return Ok(()); - } - let ptr = ::RawMutPointer::::from_usize(start); - // SAFETY: Loading happens before the initial guest thread is allowed to execute. - unsafe { page_manager.make_pages_writable(ptr, len)? }; - Ok(()) -} - -fn protect_pages( - page_manager: &WindowsPageManager, - address: usize, - len: usize, - prot: Protection, -) -> Result<(), PeImageAccessError> { - let (start, len) = page_range(address, len)?; - if len == 0 { - return Ok(()); - } - let ptr = ::RawMutPointer::::from_usize(start); - // SAFETY: All `make_pages_*` calls happen during PE load, before the initial - // guest thread starts, so there is no concurrent read/write/execute on these - // pages. The RWX arm is only reached when a section's COFF characteristics - // demand WRITE|EXECUTE; the bytes copied into the section come from the - // attacker-controlled PE file and are not executed until protections are set, - // so this is no looser than running the same PE under the real Windows loader. - match (prot.read, prot.write, prot.execute) { - (_, true, true) => unsafe { page_manager.make_pages_rwx(ptr, len)? }, - (_, true, false) => unsafe { page_manager.make_pages_writable(ptr, len)? }, - (_, false, true) => unsafe { page_manager.make_pages_executable(ptr, len)? }, - (true, false, false) => unsafe { page_manager.make_pages_readable(ptr, len)? }, - (false, false, false) => unsafe { page_manager.make_pages_inaccessible(ptr, len)? }, - } - Ok(()) -} - -fn page_range(address: usize, len: usize) -> Result<(usize, usize), PeImageAccessError> { - if len == 0 { - return Ok((address, 0)); - } - let start = page_align_down(address); - let end = address - .checked_add(len) - .and_then(|v| v.checked_next_multiple_of(PAGE_SIZE)) - .ok_or(PeImageAccessError::AddressOverflow)?; - Ok((start, end - start)) + pub process: Arc, } fn default_fs( diff --git a/litebox_shim_windows/src/loader/mod.rs b/litebox_shim_windows/src/loader/mod.rs new file mode 100644 index 0000000000..1cc884abdc --- /dev/null +++ b/litebox_shim_windows/src/loader/mod.rs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +mod pe; + +pub(crate) use pe::PeLoader; + +pub use pe::WindowsLoadError; diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs new file mode 100644 index 0000000000..6cb5f62316 --- /dev/null +++ b/litebox_shim_windows/src/loader/pe.rs @@ -0,0 +1,425 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::{sync::Arc, vec::Vec}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _, SystemInfoProvider as _}; +use litebox::{ + fs::{Mode, OFlags}, + mm::linux::{ + CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, VmemProtectError, + }, + platform::RawPointerProvider, +}; +use litebox_common_windows::loader::{ + AccessMemory, Fault, MapMemory, MappingInfo, PAGE_SIZE, PeLoadError, PeParseError, + PeParsedFile, Protection, ReadAt, page_align_down, +}; +use litebox_platform_multiplex::Platform; +use thiserror::Error; + +use crate::ShimFS; + +const NTDLL_WRITABLE_SECTIONS: &[&[u8]] = &[b".mrdata"]; +const NTDLL_PATHS: &[&str] = &["/Windows/System32/ntdll.dll", "/windows/system32/ntdll.dll"]; +const ZERO_CHUNK: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; +const FILE_CHUNK_BYTES: usize = 64 * 1024; +const INITIAL_STACK_SIZE: usize = 1024 * 1024; + +/// Struct to hold the information needed to start the program. +pub(crate) struct PeLoadInfo { + pub(crate) entry_point: usize, + pub(crate) stack_top: usize, + pub(crate) ntdll_mapping: Option, +} + +/// Loader for Windows PE files. +pub(crate) struct PeLoader<'a, FS: ShimFS> { + fs: Arc, + page_manager: &'a crate::WindowsPageManager, +} + +impl<'a, FS: ShimFS> PeLoader<'a, FS> { + pub(crate) fn new(fs: Arc, page_manager: &'a crate::WindowsPageManager) -> Self { + Self { fs, page_manager } + } + + pub(crate) fn load(&self, path: &str) -> Result { + let image = load_image(self.fs.clone(), path, self.page_manager)?; + let application_entry_point = image.mapping.entry_point; + let ntdll = load_ntdll(self.fs.clone(), self.page_manager, NTDLL_PATHS)?; + + let length = + NonZeroPageSize::new(INITIAL_STACK_SIZE).ok_or(PeImageAccessError::AddressOverflow)?; + let stack_base = unsafe { + self.page_manager + .create_stack_pages(None, length, CreatePagesFlags::empty()) + .map_err(PeImageAccessError::Mapping)? + }; + let stack_top = stack_base + .as_usize() + .checked_add(INITIAL_STACK_SIZE) + .ok_or(PeImageAccessError::AddressOverflow)?; + let stack_top = if stack_top.is_multiple_of(16) { + stack_top - core::mem::size_of::() + } else { + stack_top + }; + + Ok(PeLoadInfo { + entry_point: application_entry_point, + stack_top, + ntdll_mapping: ntdll.map(|image| image.mapping), + }) + } +} + +struct LoadedImage { + mapping: MappingInfo, +} + +fn load_ntdll( + fs: Arc, + page_manager: &crate::WindowsPageManager, + ntdll_paths: &[&str], +) -> Result, WindowsLoadError> { + for path in ntdll_paths { + match load_image_with_writable_sections( + fs.clone(), + path, + page_manager, + NTDLL_WRITABLE_SECTIONS, + ) { + Ok(image) => { + litebox_util_log::debug!(path:% = path; "Loaded guest ntdll.dll"); + return Ok(Some(image)); + } + Err(error) if is_missing_file_error(&error) => {} + Err(error) => return Err(error), + } + } + + litebox_util_log::debug!("Guest ntdll.dll was not found in the initial filesystem"); + Ok(None) +} + +fn load_image( + fs: Arc, + path: &str, + page_manager: &crate::WindowsPageManager, +) -> Result { + load_image_with_writable_sections(fs, path, page_manager, &[]) +} + +fn load_image_with_writable_sections( + fs: Arc, + path: &str, + page_manager: &crate::WindowsPageManager, + writable_section_names: &[&[u8]], +) -> Result { + let file = PeImageFile::open(fs, path)?; + let mut parsed = PeParsedFile::parse(&mut &file).map_err(WindowsLoadError::Parse)?; + parsed + .parse_trampoline( + &mut &file, + litebox_platform_multiplex::platform().get_syscall_entry_point(), + ) + .map_err(WindowsLoadError::Parse)?; + let mut mapper = PeImageMapper { + file: &file, + page_manager, + chunk: alloc::vec![0u8; FILE_CHUNK_BYTES], + }; + let mut memory = PeImageMemory; + let mapping = parsed + .load_with_writable_sections(&mut mapper, &mut memory, writable_section_names) + .map_err(WindowsLoadError::Load)?; + Ok(LoadedImage { mapping }) +} + +/// Errors that can occur while opening, parsing, and mapping a Windows PE image. +#[derive(Debug, Error)] +pub enum WindowsLoadError { + /// PE parsing failed. + #[error("failed to parse PE image")] + Parse(#[source] PeParseError), + /// PE image mapping failed. + #[error("failed to load PE image")] + Load(#[source] PeLoadError), + /// Opening the PE image failed. + #[error(transparent)] + Access(#[from] PeImageAccessError), + /// Guest ntdll.dll does not export LdrInitializeThunk. + #[error("guest ntdll.dll does not export LdrInitializeThunk")] + MissingNtDllLoaderEntrypoint, + /// Guest ntdll.dll does not export RtlUserThreadStart. + #[error("guest ntdll.dll does not export RtlUserThreadStart")] + MissingNtDllThreadEntrypoint, + /// Guest ntdll.dll has not been rewritten for LiteBox syscall/GS handling. + #[error("guest ntdll.dll must be rewritten for LiteBox before entering its loader")] + UnrewrittenNtDll, +} + +fn is_missing_file_error(error: &WindowsLoadError) -> bool { + let WindowsLoadError::Access(PeImageAccessError::Open(error)) = error else { + return false; + }; + + matches!( + error, + litebox::fs::errors::OpenError::PathError( + litebox::fs::errors::PathError::NoSuchFileOrDirectory + | litebox::fs::errors::PathError::MissingComponent + ) + ) +} + +struct PeImageFile { + fs: Arc, + fd: litebox::fd::TypedFd, +} + +impl PeImageFile { + fn open(fs: Arc, path: &str) -> Result { + let fd = fs.open(path, OFlags::RDONLY, Mode::empty())?; + Ok(Self { fs, fd }) + } + + fn read_exact_at( + &self, + mut offset: usize, + mut buf: &mut [u8], + ) -> Result<(), PeImageAccessError> { + while !buf.is_empty() { + let bytes_read = self.fs.read(&self.fd, buf, Some(offset))?; + if bytes_read == 0 { + return Err(PeImageAccessError::ShortRead); + } + offset = offset + .checked_add(bytes_read) + .ok_or(PeImageAccessError::AddressOverflow)?; + buf = &mut buf[bytes_read..]; + } + Ok(()) + } +} + +impl Drop for PeImageFile { + fn drop(&mut self) { + if let Err(e) = self.fs.close(&self.fd) { + litebox_util_log::warn!(error:? = e; "failed to close PE image file"); + } + } +} + +impl ReadAt for &'_ PeImageFile { + type Error = PeImageAccessError; + + fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error> { + self.read_exact_at( + offset + .try_into() + .map_err(|_| PeImageAccessError::AddressOverflow)?, + buf, + ) + } + + fn size(&mut self) -> Result { + self.fs + .fd_file_status(&self.fd)? + .size + .try_into() + .map_err(|_| PeImageAccessError::AddressOverflow) + } +} + +struct PeImageMapper<'a, FS: ShimFS> { + file: &'a PeImageFile, + page_manager: &'a crate::WindowsPageManager, + /// Reusable per-call I/O staging buffer for [`MapMemory::map_file`]. + chunk: Vec, +} + +impl MapMemory for PeImageMapper<'_, FS> { + type Error = PeImageAccessError; + + fn reserve( + &mut self, + preferred_base: usize, + len: usize, + _align: usize, + ) -> Result { + let length = NonZeroPageSize::new(len).ok_or(PeImageAccessError::AddressOverflow)?; + let suggested_address = if preferred_base == 0 { + None + } else { + Some(NonZeroAddress::new(preferred_base).ok_or(PeImageAccessError::AddressOverflow)?) + }; + + // SAFETY: `CreatePagesFlags::empty()` does not set `fixed_addr`, so the kernel + // treats `suggested_address` as a hint and never silently unmaps an existing + // mapping; the documented overlap precondition therefore does not apply. + let ptr = unsafe { + self.page_manager.create_inaccessible_pages( + suggested_address, + length, + CreatePagesFlags::empty(), + |_| Ok(0), + )? + }; + Ok(ptr.as_usize()) + } + + fn map_zero( + &mut self, + address: usize, + len: usize, + prot: &Protection, + ) -> Result<(), Self::Error> { + make_pages_writable(self.page_manager, address, len)?; + let ptr = ::RawMutPointer::::from_usize(address); + let mut written = 0; + while written < len { + let chunk = (len - written).min(ZERO_CHUNK.len()); + ptr.copy_from_slice(written, &ZERO_CHUNK[..chunk]) + .ok_or(PeImageAccessError::MemoryAccess)?; + written += chunk; + } + protect_pages(self.page_manager, address, len, *prot) + } + + fn map_file( + &mut self, + address: usize, + len: usize, + offset: u64, + prot: &Protection, + ) -> Result<(), Self::Error> { + make_pages_writable(self.page_manager, address, len)?; + let ptr = ::RawMutPointer::::from_usize(address); + let file_offset: usize = offset + .try_into() + .map_err(|_| PeImageAccessError::AddressOverflow)?; + let mut read = 0; + while read < len { + let remaining = len - read; + let n = remaining.min(self.chunk.len()); + self.file.read_exact_at( + file_offset + .checked_add(read) + .ok_or(PeImageAccessError::AddressOverflow)?, + &mut self.chunk[..n], + )?; + ptr.copy_from_slice(read, &self.chunk[..n]) + .ok_or(PeImageAccessError::MemoryAccess)?; + read += n; + } + protect_pages(self.page_manager, address, len, *prot) + } + + fn protect( + &mut self, + address: usize, + len: usize, + prot: &Protection, + ) -> Result<(), Self::Error> { + protect_pages(self.page_manager, address, len, *prot) + } +} + +/// Errors from the shim-side PE image backing file and memory mapper. +#[derive(Debug, Error)] +pub enum PeImageAccessError { + /// Opening the executable failed. + #[error("failed to open PE image")] + Open(#[from] litebox::fs::errors::OpenError), + /// Reading the executable failed. + #[error("failed to read PE image")] + Read(#[from] litebox::fs::errors::ReadError), + /// Reading file metadata failed. + #[error("failed to read PE image metadata")] + FileStatus(#[from] litebox::fs::errors::FileStatusError), + /// The backing file ended before the requested range was read. + #[error("short read from PE image")] + ShortRead, + /// A PE file offset or image address overflowed this host representation. + #[error("PE image address overflow")] + AddressOverflow, + /// A memory mapping operation failed. + #[error(transparent)] + Mapping(#[from] MappingError), + /// A memory protection operation failed. + #[error(transparent)] + Protect(#[from] VmemProtectError), + /// A mapped memory access failed. + #[error("mapped PE image memory access failed")] + MemoryAccess, +} + +struct PeImageMemory; + +impl AccessMemory for PeImageMemory { + fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Fault> { + let ptr = ::RawConstPointer::::from_usize(address); + buf.copy_from_slice(&ptr.to_owned_slice(buf.len()).ok_or(Fault)?); + Ok(()) + } + + fn write(&mut self, address: usize, data: &[u8]) -> Result<(), Fault> { + let ptr = ::RawMutPointer::::from_usize(address); + ptr.copy_from_slice(0, data).ok_or(Fault) + } +} + +fn make_pages_writable( + page_manager: &crate::WindowsPageManager, + address: usize, + len: usize, +) -> Result<(), PeImageAccessError> { + let (start, len) = page_range(address, len)?; + if len == 0 { + return Ok(()); + } + let ptr = ::RawMutPointer::::from_usize(start); + // SAFETY: Loading happens before the initial guest thread is allowed to execute. + unsafe { page_manager.make_pages_writable(ptr, len)? }; + Ok(()) +} + +fn protect_pages( + page_manager: &crate::WindowsPageManager, + address: usize, + len: usize, + prot: Protection, +) -> Result<(), PeImageAccessError> { + let (start, len) = page_range(address, len)?; + if len == 0 { + return Ok(()); + } + let ptr = ::RawMutPointer::::from_usize(start); + // SAFETY: All `make_pages_*` calls happen during PE load, before the initial + // guest thread starts, so there is no concurrent read/write/execute on these + // pages. The RWX arm is only reached when a section's COFF characteristics + // demand WRITE|EXECUTE; the bytes copied into the section come from the + // attacker-controlled PE file and are not executed until protections are set, + // so this is no looser than running the same PE under the real Windows loader. + match (prot.read, prot.write, prot.execute) { + (_, true, true) => unsafe { page_manager.make_pages_rwx(ptr, len)? }, + (_, true, false) => unsafe { page_manager.make_pages_writable(ptr, len)? }, + (_, false, true) => unsafe { page_manager.make_pages_executable(ptr, len)? }, + (true, false, false) => unsafe { page_manager.make_pages_readable(ptr, len)? }, + (false, false, false) => unsafe { page_manager.make_pages_inaccessible(ptr, len)? }, + } + Ok(()) +} + +fn page_range(address: usize, len: usize) -> Result<(usize, usize), PeImageAccessError> { + if len == 0 { + return Ok((address, 0)); + } + let start = page_align_down(address); + let end = address + .checked_add(len) + .and_then(|v| v.checked_next_multiple_of(PAGE_SIZE)) + .ok_or(PeImageAccessError::AddressOverflow)?; + Ok((start, end - start)) +} diff --git a/litebox_syscall_rewriter/Cargo.toml b/litebox_syscall_rewriter/Cargo.toml index 644eb55a81..2864e95995 100644 --- a/litebox_syscall_rewriter/Cargo.toml +++ b/litebox_syscall_rewriter/Cargo.toml @@ -11,7 +11,7 @@ clap = ["dep:clap"] [dependencies] iced-x86 = { version = "1.21", default-features = false, features = ["no_std", "decoder", "encoder", "instr_info"] } -object = { version = "0.36.7", default-features = false, features = ["elf", "read_core"] } +object = { version = "0.36.7", default-features = false, features = ["elf", "pe", "read_core"] } thiserror = { version = "2.0.6", default-features = false } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 13c3df910a..5a19ebce5d 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Rewrite ELF files to hook syscalls +//! Rewrite binaries for LiteBox execution. //! //! This crate sets up a trampoline point for every `syscall` instruction in its input binary, //! allowing for conveniently taking control of a binary without ptrace/systrap/seccomp/... @@ -12,7 +12,8 @@ //! However, as an explicit goal, it is intended to provide low-overhead hooking of syscalls, //! without needing to undergo a user-kernel transition. //! -//! This crate currently only supports x86-64 (i.e., amd64) ELFs. +//! This crate currently supports x86-64 ELFs for syscall hooking and x86-64 PEs for syscall +//! hooking plus rewriting Windows TEB accesses from GS segment overrides to FS segment overrides. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; @@ -23,7 +24,9 @@ use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; +use object::pe::{IMAGE_SCN_CNT_CODE, IMAGE_SCN_MEM_EXECUTE}; use object::read::elf::{ElfFile, ProgramHeader as _}; +use object::read::pe::{ImageNtHeaders as _, ImageOptionalHeader as _, PeFile64}; use object::read::{Object as _, ObjectSection as _}; use thiserror::Error; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -73,6 +76,19 @@ const BUN_FOOTER_MARKER: &[u8] = b"\n---- Bun! ----\n"; /// This is checked by the loader to verify that the trampoline is valid. pub const TRAMPOLINE_MAGIC: &[u8; 8] = b"LITEBOX0"; +/// Rewrite a supported binary for LiteBox. +/// +/// ELF64 inputs are passed through [`hook_syscalls_in_elf`]. PE64 inputs have +/// executable-section GS segment overrides rewritten to FS and `syscall` +/// instructions redirected through a LiteBox trampoline footer. +pub fn rewrite_binary(input_binary: &[u8], trampoline: Option) -> Result> { + if is_pe_binary(input_binary) { + rewrite_pe_for_litebox(input_binary, trampoline) + } else { + hook_syscalls_in_elf(input_binary, trampoline) + } +} + /// Trampoline header for 64-bit: 8 (magic) + 8 (file_offset) + 8 (vaddr) + 8 (size) = 32 bytes #[repr(C, packed)] #[derive(FromBytes, IntoBytes, Immutable)] @@ -83,7 +99,7 @@ struct TrampolineHeader64 { trampoline_size: u64, } -/// Metadata about an executable section, extracted from the read-only ELF parse. +/// Metadata about an executable section, extracted from a read-only object parse. struct TextSectionInfo { /// Virtual address of the section vaddr: u64, @@ -93,6 +109,11 @@ struct TextSectionInfo { size: u64, } +struct SyscallPatchResult { + found_syscall: bool, + skipped_addrs: Vec, +} + /// Update the `input_binary` with a call to `trampoline` instead of any `syscall` instructions. /// /// The `trampoline` must be an absolute address if specified; if unspecified, it will be set to @@ -153,7 +174,7 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res fixup_phdr_alignment(buf); // Parse the ELF and extract all metadata we need, then drop the borrow so we can mutate buf. - let (arch, text_sections, control_transfer_targets, trampoline_base_addr) = { + let (arch, text_sections, trampoline_base_addr) = { let file = object::File::parse(&*buf).map_err(|e| Error::ParseError(e.to_string()))?; let arch = match file { @@ -172,72 +193,288 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res return Ok(input_binary.to_vec()); } - let control_transfer_targets = get_control_transfer_targets(arch, &*buf, &text_sections)?; - let trampoline_base_addr = find_addr_for_trampoline_code(&file)?; - ( - arch, - text_sections, - control_transfer_targets, - trampoline_base_addr, - ) + (arch, text_sections, trampoline_base_addr) + }; + + let mut trampoline_data = Vec::from(trampoline.unwrap_or(0).to_le_bytes()); + let patch_result = patch_syscalls_in_sections( + arch, + buf, + &text_sections, + trampoline_base_addr, + trampoline_base_addr, + &mut trampoline_data, + )?; + + // Build output: [patched ELF][padding to page boundary][trampoline code][header] + let mut out = buf.to_vec(); + append_trampoline_footer(&mut out, &mut trampoline_data, trampoline_base_addr, false); + + if !patch_result.skipped_addrs.is_empty() { + return Err(Error::UnpatchableSyscalls(format!( + "{} unpatchable syscall instruction(s) at {skipped_addrs:?}", + patch_result.skipped_addrs.len(), + skipped_addrs = patch_result.skipped_addrs, + ))); + } + Ok(out) +} + +/// Rewrite an x86-64 PE for LiteBox's current Windows shim. +/// +/// The PE file layout is preserved, but executable-section GS segment overrides +/// are rewritten to FS and `syscall` instructions are redirected through a +/// LiteBox trampoline appended as a file overlay. The Windows shim loader maps +/// that overlay by reading the footer this function appends. +pub fn rewrite_pe_for_litebox(input_binary: &[u8], trampoline: Option) -> Result> { + if is_already_hooked(input_binary, Arch::X86_64) { + return Ok(input_binary.to_vec()); + } + + let mut backing = vec![0u64; input_binary.len().div_ceil(8)]; + let buf: &mut [u8] = zerocopy::IntoBytes::as_mut_bytes(backing.as_mut_slice()); + buf[..input_binary.len()].copy_from_slice(input_binary); + let buf = &mut buf[..input_binary.len()]; + + let (text_sections, trampoline_base_rva, trampoline_base_addr) = { + let pe = PeFile64::parse(&*buf).map_err(|e| Error::ParseError(e.to_string()))?; + let optional_header = pe.nt_headers().optional_header(); + let size_of_image = u64::from(optional_header.size_of_image()); + let trampoline_base_rva = + checked_add_u64(size_of_image, 0xfff, "PE trampoline base")? & !0xfff; + let trampoline_base_addr = checked_add_u64( + optional_header.image_base(), + trampoline_base_rva, + "PE trampoline virtual address", + )?; + + let file = object::File::parse(&*buf).map_err(|e| Error::ParseError(e.to_string()))?; + match file { + object::File::Pe64(_) if file.architecture() == object::Architecture::X86_64 => {} + _ => return Ok(input_binary.to_vec()), + } + + let text_sections = match pe_text_sections(&file) { + Ok(sections) => sections, + Err(InternalError::NoTextSectionFound) => return Ok(input_binary.to_vec()), + Err(InternalError::Public(e)) => return Err(e), + Err(e) => unreachable!("unexpected internal error: {e:?}"), + }; + (text_sections, trampoline_base_rva, trampoline_base_addr) }; - // Build the trampoline code (without header - header goes at the end) - // The code starts with the syscall entry point placeholder (8 bytes for x86-64) - let mut trampoline_data = vec![]; - let trampoline = trampoline.unwrap_or(0); - trampoline_data.extend_from_slice(&trampoline.to_le_bytes()); - // Patch syscalls in-place in buf + for section in &text_sections { + let section_data = section_slice_mut(buf, section)?; + rewrite_gs_to_fs_in_section(Arch::X86_64, section.vaddr, section_data)?; + } + + let mut trampoline_data = Vec::from(trampoline.unwrap_or(0).to_le_bytes()); + // Windows ntdll packs some syscall stubs too tightly for the generic + // five-byte jump patcher; keep that PE-specific shape out of the generic path. + let patched_dense_windows_stubs = patch_dense_windows_syscall_stubs_in_sections( + Arch::X86_64, + buf, + &text_sections, + trampoline_base_addr, + trampoline_base_addr, + &mut trampoline_data, + )?; + let patch_result = patch_syscalls_in_sections( + Arch::X86_64, + buf, + &text_sections, + trampoline_base_addr, + trampoline_base_addr, + &mut trampoline_data, + )?; + + if !patched_dense_windows_stubs && !patch_result.found_syscall { + return Ok(buf.to_vec()); + } + + let mut out = buf.to_vec(); + append_trampoline_footer(&mut out, &mut trampoline_data, trampoline_base_rva, true); + + if !patch_result.skipped_addrs.is_empty() { + return Err(Error::UnpatchableSyscalls(format!( + "{} unpatchable syscall instruction(s) at {skipped_addrs:?}", + patch_result.skipped_addrs.len(), + skipped_addrs = patch_result.skipped_addrs, + ))); + } + + Ok(out) +} + +fn is_pe_binary(input_binary: &[u8]) -> bool { + if input_binary.len() < 0x40 || &input_binary[..2] != b"MZ" { + return false; + } + let pe_offset = u32::from_le_bytes(input_binary[0x3c..0x40].try_into().unwrap()) as usize; + input_binary + .get(pe_offset..pe_offset.saturating_add(4)) + .is_some_and(|magic| magic == b"PE\0\0") +} + +fn pe_text_sections( + file: &object::File<'_>, +) -> core::result::Result, InternalError> { + let text_sections: Vec<_> = file + .sections() + .filter_map(|section| { + let object::SectionFlags::Coff { characteristics } = section.flags() else { + return None; + }; + if characteristics & IMAGE_SCN_CNT_CODE == 0 { + return None; + } + if characteristics & IMAGE_SCN_MEM_EXECUTE == 0 { + return None; + } + let (file_offset, size) = section.file_range()?; + Some(TextSectionInfo { + vaddr: section.address(), + file_offset, + size, + }) + }) + .collect(); + if text_sections.is_empty() { + return Err(InternalError::NoTextSectionFound); + } + Ok(text_sections) +} + +fn rewrite_gs_to_fs_in_section( + arch: Arch, + section_base_addr: u64, + section_data: &mut [u8], +) -> Result { + let instructions = decode_section_instructions(arch, section_data, section_base_addr)?; + let mut rewritten = 0; + + for instruction in &instructions { + if instruction.memory_segment() != iced_x86::Register::GS { + continue; + } + + let offset = usize::try_from(instruction.ip() - section_base_addr).unwrap(); + let instruction_bytes = &mut section_data[offset..offset + instruction.len()]; + let Some(segment_prefix) = instruction_bytes.iter_mut().find(|byte| **byte == 0x65) else { + return Err(Error::DisassemblyFailure(format!( + "GS memory operand at {:#x} has no GS segment prefix", + instruction.ip() + ))); + }; + *segment_prefix = 0x64; + rewritten += 1; + } + + Ok(rewritten) +} + +fn patch_syscalls_in_sections( + arch: Arch, + buf: &mut [u8], + text_sections: &[TextSectionInfo], + trampoline_base_addr: u64, + syscall_entry_addr: u64, + trampoline_data: &mut Vec, +) -> Result { + let control_transfer_targets = get_control_transfer_targets(arch, &*buf, text_sections)?; + let mut found_syscall = false; let mut skipped_addrs = Vec::new(); - for s in &text_sections { - let section_data = section_slice_mut(buf, s)?; + + for section in text_sections { + let section_data = section_slice_mut(buf, section)?; match hook_syscalls_in_section( arch, &control_transfer_targets, - s.vaddr, + section.vaddr, section_data, trampoline_base_addr, - trampoline_base_addr, // entry point is at offset 0 of trampoline - &mut trampoline_data, + syscall_entry_addr, + trampoline_data, ) { - Ok(addrs) => skipped_addrs.extend(addrs), + Ok(addrs) => { + found_syscall = true; + skipped_addrs.extend(addrs); + } Err(InternalError::NoSyscallInstructionsFound) => {} Err(InternalError::Public(e)) => return Err(e), Err(e) => unreachable!("unexpected internal error: {e:?}"), } } - // Build output: [patched ELF][padding to page boundary][trampoline code][header] - let mut out = buf.to_vec(); + Ok(SyscallPatchResult { + found_syscall, + skipped_addrs, + }) +} + +fn patch_dense_windows_syscall_stubs_in_sections( + arch: Arch, + buf: &mut [u8], + text_sections: &[TextSectionInfo], + trampoline_base_addr: u64, + syscall_entry_addr: u64, + trampoline_data: &mut Vec, +) -> Result { + let control_transfer_targets = get_control_transfer_targets(arch, &*buf, text_sections)?; + let mut patched_any = false; + + for section in text_sections { + let section_data = section_slice_mut(buf, section)?; + let instructions = decode_section_instructions(arch, section_data, section.vaddr)?; + for (i, inst) in instructions.iter().enumerate() { + if inst.code() != iced_x86::Code::Syscall { + continue; + } + + patched_any |= patch_dense_windows_syscall_stub( + &control_transfer_targets, + section.vaddr, + section_data, + trampoline_base_addr, + syscall_entry_addr, + trampoline_data, + &instructions, + i, + )?; + } + } + + Ok(patched_any) +} + +fn append_trampoline_footer( + out: &mut Vec, + trampoline_data: &mut Vec, + header_vaddr: u64, + align_trampoline_size: bool, +) { let remain = out.len() % 0x1000; out.extend_from_slice(&vec![0; if remain == 0 { 0 } else { 0x1000 - remain }]); - // Calculate file offset where trampoline code starts let trampoline_file_offset = out.len() as u64; + if align_trampoline_size { + let trampoline_size = trampoline_data.len().next_multiple_of(0x1000); + trampoline_data.extend_from_slice(&vec![0; trampoline_size - trampoline_data.len()]); + } let trampoline_size = trampoline_data.len(); + out.extend_from_slice(trampoline_data); - // Append trampoline code - out.extend_from_slice(&trampoline_data); - - // Build the header (goes at the end of the file) - // The entry point placeholder is at offset 0 of the trampoline code, not in the header. let header = TrampolineHeader64 { magic: *TRAMPOLINE_MAGIC, file_offset: trampoline_file_offset, - vaddr: trampoline_base_addr, + vaddr: header_vaddr, trampoline_size: trampoline_size as u64, }; out.extend_from_slice(header.as_bytes()); - if !skipped_addrs.is_empty() { - return Err(Error::UnpatchableSyscalls(format!( - "{} unpatchable syscall instruction(s) at {skipped_addrs:?}", - skipped_addrs.len(), - ))); - } - Ok(out) } + /// (private) Get metadata for executable sections fn text_sections( file: &object::File<'_>, @@ -625,6 +862,131 @@ fn replace_with_trap( } } +#[allow(clippy::too_many_arguments)] +fn patch_dense_windows_syscall_stub( + control_transfer_targets: &BTreeSet, + section_base_addr: u64, + section_data: &mut [u8], + trampoline_base_addr: u64, + syscall_entry_addr: u64, + trampoline_data: &mut Vec, + instructions: &[iced_x86::Instruction], + inst_index: usize, +) -> Result { + if inst_index < 2 { + return Ok(false); + } + + let test_inst = &instructions[inst_index - 2]; + let jne_inst = &instructions[inst_index - 1]; + let syscall_inst = &instructions[inst_index]; + + if !is_dense_windows_syscall_stub_sequence(test_inst, jne_inst, section_base_addr, section_data) + { + return Ok(false); + } + + let stub_addr = test_inst.ip(); + let fallback_addr = checked_add_u64( + jne_inst.ip(), + DENSE_WINDOWS_SYSCALL_STUB_TAIL_FALLBACK_OFFSET as u64, + "dense Windows syscall fallback address", + )?; + let stub_end_addr = checked_add_u64( + jne_inst.ip(), + DENSE_WINDOWS_SYSCALL_STUB_TAIL.len() as u64, + "dense Windows syscall stub end address", + )?; + if control_transfer_targets + .iter() + .any(|target| (stub_addr..stub_end_addr).contains(target) && *target != fallback_addr) + { + return Ok(false); + } + + let target_addr = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64, + "dense Windows syscall trampoline target", + )?; + + let return_addr = syscall_inst.next_ip(); + let jmp_back_base = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64 + 7, + "dense Windows syscall trampoline return base", + )?; + // lea rcx, [rip + disp32] + trampoline_data.extend_from_slice(&[0x48, 0x8D, 0x0D]); + trampoline_data.extend_from_slice(&rel32_bytes( + return_addr, + jmp_back_base, + "dense Windows syscall trampoline return", + )?); + + // jmp qword ptr [rip + disp32] + trampoline_data.extend_from_slice(&[0xFF, 0x25]); + let entry_base = checked_add_u64( + trampoline_base_addr, + trampoline_data.len() as u64 + 4, + "dense Windows syscall trampoline entry base", + )?; + trampoline_data.extend_from_slice(&rel32_bytes( + syscall_entry_addr, + entry_base, + "dense Windows syscall trampoline entry", + )?); + + let stub_offset = usize::try_from(stub_addr - section_base_addr).unwrap(); + section_data[stub_offset] = 0xe9; + let patch_base = checked_add_u64(stub_addr, 5, "dense Windows syscall patch jump base")?; + section_data[stub_offset + 1..stub_offset + 5].copy_from_slice(&rel32_bytes( + target_addr, + patch_base, + "dense Windows syscall patch jump", + )?); + + let syscall_end_offset = usize::try_from(syscall_inst.next_ip() - section_base_addr).unwrap(); + for byte in &mut section_data[stub_offset + 5..syscall_end_offset] { + *byte = 0x90; + } + + let fallback_offset = usize::try_from(fallback_addr - section_base_addr).unwrap(); + section_data[fallback_offset] = 0xeb; + section_data[fallback_offset + 1] = + i8::try_from(i128::from(stub_addr) - i128::from(fallback_addr + 2)) + .map_err(|_| { + Error::AddressOverflow("dense Windows syscall fallback jump out of range".into()) + })? + .to_ne_bytes()[0]; + + Ok(true) +} + +fn is_dense_windows_syscall_stub_sequence( + test_inst: &iced_x86::Instruction, + jne_inst: &iced_x86::Instruction, + section_base_addr: u64, + section_data: &[u8], +) -> bool { + if !matches!( + test_inst.code(), + iced_x86::Code::Test_rm8_imm8 | iced_x86::Code::Test_rm8_imm8_F6r1 + ) || test_inst.immediate8() != 1 + { + return false; + } + + let Ok(tail_offset) = usize::try_from(jne_inst.ip() - section_base_addr) else { + return false; + }; + let Some(tail_end) = tail_offset.checked_add(DENSE_WINDOWS_SYSCALL_STUB_TAIL.len()) else { + return false; + }; + + section_data.get(tail_offset..tail_end) == Some(DENSE_WINDOWS_SYSCALL_STUB_TAIL) +} + fn checked_add_u64(base: u64, addend: u64, context: &'static str) -> Result { base.checked_add(addend) .ok_or_else(|| Error::AddressOverflow(format!("{context} address overflow"))) @@ -751,6 +1113,9 @@ fn get_control_transfer_targets( const MAX_X86_INSTRUCTION_LEN: usize = 15; const CHUNK_OVERLAP_LEN: usize = MAX_X86_INSTRUCTION_LEN - 1; const TARGET_DECODE_CHUNK_LEN: usize = 8 * 1024 * 1024; +// jne +3; syscall; ret; int 0x2e; ret +const DENSE_WINDOWS_SYSCALL_STUB_TAIL: &[u8] = &[0x75, 0x03, 0x0f, 0x05, 0xc3, 0xcd, 0x2e, 0xc3]; +const DENSE_WINDOWS_SYSCALL_STUB_TAIL_FALLBACK_OFFSET: usize = 5; fn bytes_until_next_4g_boundary(ptr: *const u8) -> usize { let low = (ptr as u64) & 0xFFFF_FFFF; diff --git a/litebox_syscall_rewriter/src/main.rs b/litebox_syscall_rewriter/src/main.rs index 7ef8eef14c..64b98e6430 100644 --- a/litebox_syscall_rewriter/src/main.rs +++ b/litebox_syscall_rewriter/src/main.rs @@ -10,10 +10,10 @@ use std::io::Write as _; use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _}; use std::path::PathBuf; -/// Rewrite ELF files to hook syscalls +/// Rewrite ELF files to hook syscalls, or PE files to hook syscalls and change GS TEB accesses to FS. #[derive(Parser, Debug)] struct CliArgs { - /// Path to input ELF binary + /// Path to input binary input_binary: PathBuf, /// Path to output the generated binary (default = .hooked) #[arg(short = 'o', long = "output")] @@ -47,10 +47,8 @@ fn main() -> anyhow::Result<()> { let mut input_binary = std::fs::File::open(&cli_args.input_binary)?; let mut input_binary_bytes = vec![]; input_binary.read_to_end(&mut input_binary_bytes)?; - let output_binary = litebox_syscall_rewriter::hook_syscalls_in_elf( - &input_binary_bytes, - cli_args.trampoline_addr, - )?; + let output_binary = + litebox_syscall_rewriter::rewrite_binary(&input_binary_bytes, cli_args.trampoline_addr)?; let output_path = cli_args.output_binary.unwrap_or_else(|| { cli_args.input_binary.with_file_name( cli_args From 21ecba457057ebb459a66ce2ee57af9e8788344c Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 26 May 2026 10:48:23 -0700 Subject: [PATCH 003/319] Rwrite Windows Nt syscall number (#866) This PR makes Windows PE syscall rewriting independent of the host/guest ntdll.dll syscall-number layout as it is not stable across different versions. The idea is that the rewriter first extracts syscall numbers from `ntdll` and then rewrites the binary to use our fixed syscall numbers. Since Windows PE supposes to call syscalls via `ntdll`, we only need to do it for one binary. --- Cargo.lock | 2 + litebox_common_windows/src/lib.rs | 531 +++++++++++++++++++ litebox_common_windows/src/loader.rs | 63 +-- litebox_runner_windows_userland/Cargo.toml | 1 + litebox_runner_windows_userland/tests/run.rs | 46 +- litebox_shim_windows/src/lib.rs | 12 +- litebox_shim_windows/src/loader/mod.rs | 4 +- litebox_shim_windows/src/loader/pe.rs | 19 +- litebox_syscall_rewriter/Cargo.toml | 1 + litebox_syscall_rewriter/src/lib.rs | 399 +++++++++++++- 10 files changed, 965 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9b232a35b..3f94349452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1706,6 +1706,7 @@ dependencies = [ "clap", "litebox", "litebox_common_linux", + "litebox_common_windows", "litebox_platform_multiplex", "litebox_platform_windows_userland", "litebox_shim_windows", @@ -1780,6 +1781,7 @@ dependencies = [ "clap", "iced-x86", "insta", + "litebox_common_windows", "object", "similar", "tempfile", diff --git a/litebox_common_windows/src/lib.rs b/litebox_common_windows/src/lib.rs index af886a31c8..ff24d3c0e3 100644 --- a/litebox_common_windows/src/lib.rs +++ b/litebox_common_windows/src/lib.rs @@ -8,3 +8,534 @@ extern crate alloc; pub mod loader; + +macro_rules! nt_sysnos { + ($(($number:literal, $name:ident)),+ $(,)?) => { + /// Stable LiteBox syscall numbers for NT syscalls handled by the Windows shim. + /// + /// Rewritten guest PE stubs load these numbers into `eax` instead of the + /// guest ntdll's build-specific syscall numbers. The values follow the + /// Windows default ordering so generated traces and hand-written test + /// stubs remain easy to compare with common syscall tables. + #[allow(clippy::enum_variant_names)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + #[repr(u32)] + pub enum NtSysno { + $($name = $number,)+ + } + + impl NtSysno { + #[must_use] + pub const fn from_raw(raw: usize) -> Option { + match raw { + $($number => Some(Self::$name),)+ + _ => None, + } + } + + #[must_use] + pub fn from_export_name(name: &str) -> Option { + match name { + $(stringify!($name) => Some(Self::$name),)+ + _ => None, + } + } + + #[must_use] + pub const fn as_raw(self) -> u32 { + self as u32 + } + } + }; +} + +nt_sysnos! { + (0x0, NtAccessCheck), + (0x1, NtWorkerFactoryWorkerReady), + (0x2, NtAcceptConnectPort), + (0x3, NtMapUserPhysicalPagesScatter), + (0x4, NtWaitForSingleObject), + (0x5, NtCallbackReturn), + (0x6, NtReadFile), + (0x7, NtDeviceIoControlFile), + (0x8, NtWriteFile), + (0x9, NtRemoveIoCompletion), + (0xa, NtReleaseSemaphore), + (0xb, NtReplyWaitReceivePort), + (0xc, NtReplyPort), + (0xd, NtSetInformationThread), + (0xe, NtSetEvent), + (0xf, NtClose), + (0x10, NtQueryObject), + (0x11, NtQueryInformationFile), + (0x12, NtOpenKey), + (0x13, NtEnumerateValueKey), + (0x14, NtFindAtom), + (0x15, NtQueryDefaultLocale), + (0x16, NtQueryKey), + (0x17, NtQueryValueKey), + (0x18, NtAllocateVirtualMemory), + (0x19, NtQueryInformationProcess), + (0x1a, NtWaitForMultipleObjects32), + (0x1b, NtWriteFileGather), + (0x1c, NtSetInformationProcess), + (0x1d, NtCreateKey), + (0x1e, NtFreeVirtualMemory), + (0x1f, NtImpersonateClientOfPort), + (0x20, NtReleaseMutant), + (0x21, NtQueryInformationToken), + (0x22, NtRequestWaitReplyPort), + (0x23, NtQueryVirtualMemory), + (0x24, NtOpenThreadToken), + (0x25, NtQueryInformationThread), + (0x26, NtOpenProcess), + (0x27, NtSetInformationFile), + (0x28, NtMapViewOfSection), + (0x29, NtAccessCheckAndAuditAlarm), + (0x2a, NtUnmapViewOfSection), + (0x2b, NtReplyWaitReceivePortEx), + (0x2c, NtTerminateProcess), + (0x2d, NtSetEventBoostPriority), + (0x2e, NtReadFileScatter), + (0x2f, NtOpenThreadTokenEx), + (0x30, NtOpenProcessTokenEx), + (0x31, NtQueryPerformanceCounter), + (0x32, NtEnumerateKey), + (0x33, NtOpenFile), + (0x34, NtDelayExecution), + (0x35, NtQueryDirectoryFile), + (0x36, NtQuerySystemInformation), + (0x37, NtOpenSection), + (0x38, NtQueryTimer), + (0x39, NtFsControlFile), + (0x3a, NtWriteVirtualMemory), + (0x3b, NtCloseObjectAuditAlarm), + (0x3c, NtDuplicateObject), + (0x3d, NtQueryAttributesFile), + (0x3e, NtClearEvent), + (0x3f, NtReadVirtualMemory), + (0x40, NtOpenEvent), + (0x41, NtAdjustPrivilegesToken), + (0x42, NtDuplicateToken), + (0x43, NtContinue), + (0x44, NtQueryDefaultUILanguage), + (0x45, NtQueueApcThread), + (0x46, NtYieldExecution), + (0x47, NtAddAtom), + (0x48, NtCreateEvent), + (0x49, NtQueryVolumeInformationFile), + (0x4a, NtCreateSection), + (0x4b, NtFlushBuffersFile), + (0x4c, NtApphelpCacheControl), + (0x4d, NtCreateProcessEx), + (0x4e, NtCreateThread), + (0x4f, NtIsProcessInJob), + (0x50, NtProtectVirtualMemory), + (0x51, NtQuerySection), + (0x52, NtResumeThread), + (0x53, NtTerminateThread), + (0x54, NtReadRequestData), + (0x55, NtCreateFile), + (0x56, NtQueryEvent), + (0x57, NtWriteRequestData), + (0x58, NtOpenDirectoryObject), + (0x59, NtAccessCheckByTypeAndAuditAlarm), + (0x5b, NtWaitForMultipleObjects), + (0x5c, NtSetInformationObject), + (0x5d, NtCancelIoFile), + (0x5e, NtTraceEvent), + (0x5f, NtPowerInformation), + (0x60, NtSetValueKey), + (0x61, NtCancelTimer), + (0x62, NtSetTimer), + (0x63, NtAccessCheckByType), + (0x64, NtAccessCheckByTypeResultList), + (0x65, NtAccessCheckByTypeResultListAndAuditAlarm), + (0x66, NtAccessCheckByTypeResultListAndAuditAlarmByHandle), + (0x67, NtAcquireCrossVmMutant), + (0x68, NtAcquireProcessActivityReference), + (0x69, NtAddAtomEx), + (0x6a, NtAddBootEntry), + (0x6b, NtAddDriverEntry), + (0x6c, NtAdjustGroupsToken), + (0x6d, NtAdjustTokenClaimsAndDeviceGroups), + (0x6e, NtAlertMultipleThreadByThreadId), + (0x6f, NtAlertResumeThread), + (0x70, NtAlertThread), + (0x71, NtAlertThreadByThreadId), + (0x72, NtAlertThreadByThreadIdEx), + (0x73, NtAllocateLocallyUniqueId), + (0x74, NtAllocateReserveObject), + (0x75, NtAllocateUserPhysicalPages), + (0x76, NtAllocateUserPhysicalPagesEx), + (0x77, NtAllocateUuids), + (0x78, NtAllocateVirtualMemoryEx), + (0x79, NtAlpcAcceptConnectPort), + (0x7a, NtAlpcCancelMessage), + (0x7b, NtAlpcConnectPort), + (0x7c, NtAlpcConnectPortEx), + (0x7d, NtAlpcCreatePort), + (0x7e, NtAlpcCreatePortSection), + (0x7f, NtAlpcCreateResourceReserve), + (0x80, NtAlpcCreateSectionView), + (0x81, NtAlpcCreateSecurityContext), + (0x82, NtAlpcDeletePortSection), + (0x83, NtAlpcDeleteResourceReserve), + (0x84, NtAlpcDeleteSectionView), + (0x85, NtAlpcDeleteSecurityContext), + (0x86, NtAlpcDisconnectPort), + (0x87, NtAlpcImpersonateClientContainerOfPort), + (0x88, NtAlpcImpersonateClientOfPort), + (0x89, NtAlpcOpenSenderProcess), + (0x8a, NtAlpcOpenSenderThread), + (0x8b, NtAlpcQueryInformation), + (0x8c, NtAlpcQueryInformationMessage), + (0x8d, NtAlpcRevokeSecurityContext), + (0x8e, NtAlpcSendWaitReceivePort), + (0x8f, NtAlpcSetInformation), + (0x90, NtAreMappedFilesTheSame), + (0x91, NtAssignProcessToJobObject), + (0x92, NtAssociateWaitCompletionPacket), + (0x93, NtCallEnclave), + (0x94, NtCancelIoFileEx), + (0x95, NtCancelSynchronousIoFile), + (0x96, NtCancelTimer2), + (0x97, NtCancelWaitCompletionPacket), + (0x98, NtChangeProcessState), + (0x99, NtChangeThreadState), + (0x9a, NtCommitComplete), + (0x9b, NtCommitEnlistment), + (0x9c, NtCommitRegistryTransaction), + (0x9d, NtCommitTransaction), + (0x9e, NtCompactKeys), + (0x9f, NtCompareObjects), + (0xa0, NtCompareSigningLevels), + (0xa1, NtCompareTokens), + (0xa2, NtCompleteConnectPort), + (0xa3, NtCompressKey), + (0xa4, NtConnectPort), + (0xa5, NtContinueEx), + (0xa6, NtConvertBetweenAuxiliaryCounterAndPerformanceCounter), + (0xa7, NtCopyFileChunk), + (0xa8, NtCreateCpuPartition), + (0xa9, NtCreateCrossVmEvent), + (0xaa, NtCreateCrossVmMutant), + (0xab, NtCreateDebugObject), + (0xac, NtCreateDirectoryObject), + (0xad, NtCreateDirectoryObjectEx), + (0xae, NtCreateEnclave), + (0xaf, NtCreateEnlistment), + (0xb0, NtCreateEventPair), + (0xb1, NtCreateIRTimer), + (0xb2, NtCreateIoCompletion), + (0xb3, NtCreateIoRing), + (0xb4, NtCreateJobObject), + (0xb5, NtCreateJobSet), + (0xb6, NtCreateKeyTransacted), + (0xb7, NtCreateKeyedEvent), + (0xb8, NtCreateLowBoxToken), + (0xb9, NtCreateMailslotFile), + (0xba, NtCreateMutant), + (0xbb, NtCreateNamedPipeFile), + (0xbc, NtCreatePagingFile), + (0xbd, NtCreatePartition), + (0xbe, NtCreatePort), + (0xbf, NtCreatePrivateNamespace), + (0xc0, NtCreateProcess), + (0xc1, NtCreateProcessStateChange), + (0xc2, NtCreateProfile), + (0xc3, NtCreateProfileEx), + (0xc4, NtCreateRegistryTransaction), + (0xc5, NtCreateResourceManager), + (0xc6, NtCreateSectionEx), + (0xc7, NtCreateSemaphore), + (0xc8, NtCreateSymbolicLinkObject), + (0xc9, NtCreateThreadEx), + (0xca, NtCreateThreadStateChange), + (0xcb, NtCreateTimer), + (0xcc, NtCreateTimer2), + (0xcd, NtCreateToken), + (0xce, NtCreateTokenEx), + (0xcf, NtCreateTransaction), + (0xd0, NtCreateTransactionManager), + (0xd1, NtCreateUserProcess), + (0xd2, NtCreateWaitCompletionPacket), + (0xd3, NtCreateWaitablePort), + (0xd4, NtCreateWnfStateName), + (0xd5, NtCreateWorkerFactory), + (0xd6, NtDebugActiveProcess), + (0xd7, NtDebugContinue), + (0xd8, NtDeleteAtom), + (0xd9, NtDeleteBootEntry), + (0xda, NtDeleteDriverEntry), + (0xdb, NtDeleteFile), + (0xdc, NtDeleteKey), + (0xdd, NtDeleteObjectAuditAlarm), + (0xde, NtDeletePrivateNamespace), + (0xdf, NtDeleteValueKey), + (0xe0, NtDeleteWnfStateData), + (0xe1, NtDeleteWnfStateName), + (0xe2, NtDirectGraphicsCall), + (0xe3, NtDisableLastKnownGood), + (0xe4, NtDisplayString), + (0xe5, NtDrawText), + (0xe6, NtEnableLastKnownGood), + (0xe7, NtEnumerateBootEntries), + (0xe8, NtEnumerateDriverEntries), + (0xe9, NtEnumerateSystemEnvironmentValuesEx), + (0xea, NtEnumerateTransactionObject), + (0xeb, NtExtendSection), + (0xec, NtFilterBootOption), + (0xed, NtFilterToken), + (0xee, NtFilterTokenEx), + (0xef, NtFlushBuffersFileEx), + (0xf0, NtFlushInstallUILanguage), + (0xf1, NtFlushInstructionCache), + (0xf2, NtFlushKey), + (0xf3, NtFlushProcessWriteBuffers), + (0xf4, NtFlushVirtualMemory), + (0xf5, NtFlushWriteBuffer), + (0xf6, NtFreeUserPhysicalPages), + (0xf7, NtFreezeRegistry), + (0xf8, NtFreezeTransactions), + (0xf9, NtGetCachedSigningLevel), + (0xfa, NtGetCompleteWnfStateSubscription), + (0xfb, NtGetContextThread), + (0xfc, NtGetCurrentProcessorNumber), + (0xfd, NtGetCurrentProcessorNumberEx), + (0xfe, NtGetDevicePowerState), + (0xff, NtGetMUIRegistryInfo), + (0x100, NtGetNextProcess), + (0x101, NtGetNextThread), + (0x102, NtGetNlsSectionPtr), + (0x103, NtGetNotificationResourceManager), + (0x104, NtGetWriteWatch), + (0x105, NtImpersonateAnonymousToken), + (0x106, NtImpersonateThread), + (0x107, NtInitializeEnclave), + (0x108, NtInitializeNlsFiles), + (0x109, NtInitializeRegistry), + (0x10a, NtInitiatePowerAction), + (0x10b, NtIsSystemResumeAutomatic), + (0x10c, NtIsUILanguageComitted), + (0x10d, NtListenPort), + (0x10e, NtLoadDriver), + (0x10f, NtLoadEnclaveData), + (0x110, NtLoadKey), + (0x111, NtLoadKey2), + (0x112, NtLoadKey3), + (0x113, NtLoadKeyEx), + (0x114, NtLockFile), + (0x115, NtLockProductActivationKeys), + (0x116, NtLockRegistryKey), + (0x117, NtLockVirtualMemory), + (0x118, NtMakePermanentObject), + (0x119, NtMakeTemporaryObject), + (0x11a, NtManageHotPatch), + (0x11b, NtManagePartition), + (0x11c, NtMapCMFModule), + (0x11d, NtMapUserPhysicalPages), + (0x11e, NtMapViewOfSectionEx), + (0x11f, NtModifyBootEntry), + (0x120, NtModifyDriverEntry), + (0x121, NtNotifyChangeDirectoryFile), + (0x122, NtNotifyChangeDirectoryFileEx), + (0x123, NtNotifyChangeKey), + (0x124, NtNotifyChangeMultipleKeys), + (0x125, NtNotifyChangeSession), + (0x126, NtOpenCpuPartition), + (0x127, NtOpenEnlistment), + (0x128, NtOpenEventPair), + (0x129, NtOpenIoCompletion), + (0x12a, NtOpenJobObject), + (0x12b, NtOpenKeyEx), + (0x12c, NtOpenKeyTransacted), + (0x12d, NtOpenKeyTransactedEx), + (0x12e, NtOpenKeyedEvent), + (0x12f, NtOpenMutant), + (0x130, NtOpenObjectAuditAlarm), + (0x131, NtOpenPartition), + (0x132, NtOpenPrivateNamespace), + (0x133, NtOpenProcessToken), + (0x134, NtOpenRegistryTransaction), + (0x135, NtOpenResourceManager), + (0x136, NtOpenSemaphore), + (0x137, NtOpenSession), + (0x138, NtOpenSymbolicLinkObject), + (0x139, NtOpenThread), + (0x13a, NtOpenTimer), + (0x13b, NtOpenTransaction), + (0x13c, NtOpenTransactionManager), + (0x13d, NtPlugPlayControl), + (0x13e, NtPrePrepareComplete), + (0x13f, NtPrePrepareEnlistment), + (0x140, NtPrepareComplete), + (0x141, NtPrepareEnlistment), + (0x142, NtPrivilegeCheck), + (0x143, NtPrivilegeObjectAuditAlarm), + (0x144, NtPrivilegedServiceAuditAlarm), + (0x145, NtPropagationComplete), + (0x146, NtPropagationFailed), + (0x147, NtPssCaptureVaSpaceBulk), + (0x148, NtPulseEvent), + (0x149, NtQueryAuxiliaryCounterFrequency), + (0x14a, NtQueryBootEntryOrder), + (0x14b, NtQueryBootOptions), + (0x14c, NtQueryDebugFilterState), + (0x14d, NtQueryDirectoryFileEx), + (0x14e, NtQueryDirectoryObject), + (0x14f, NtQueryDriverEntryOrder), + (0x150, NtQueryEaFile), + (0x151, NtQueryFullAttributesFile), + (0x152, NtQueryInformationAtom), + (0x153, NtQueryInformationByName), + (0x154, NtQueryInformationCpuPartition), + (0x155, NtQueryInformationEnlistment), + (0x156, NtQueryInformationJobObject), + (0x157, NtQueryInformationPort), + (0x158, NtQueryInformationResourceManager), + (0x159, NtQueryInformationTransaction), + (0x15a, NtQueryInformationTransactionManager), + (0x15b, NtQueryInformationWorkerFactory), + (0x15c, NtQueryInstallUILanguage), + (0x15d, NtQueryIntervalProfile), + (0x15e, NtQueryIoCompletion), + (0x15f, NtQueryIoRingCapabilities), + (0x160, NtQueryLicenseValue), + (0x161, NtQueryMultipleValueKey), + (0x162, NtQueryMutant), + (0x163, NtQueryOpenSubKeys), + (0x164, NtQueryOpenSubKeysEx), + (0x165, NtQueryPortInformationProcess), + (0x166, NtQueryQuotaInformationFile), + (0x167, NtQuerySecurityAttributesToken), + (0x168, NtQuerySecurityObject), + (0x169, NtQuerySecurityPolicy), + (0x16a, NtQuerySemaphore), + (0x16b, NtQuerySymbolicLinkObject), + (0x16c, NtQuerySystemEnvironmentValue), + (0x16d, NtQuerySystemEnvironmentValueEx), + (0x16e, NtQuerySystemInformationEx), + (0x16f, NtQueryTimerResolution), + (0x170, NtQueryWnfStateData), + (0x171, NtQueryWnfStateNameInformation), + (0x172, NtQueueApcThreadEx), + (0x173, NtQueueApcThreadEx2), + (0x174, NtRaiseException), + (0x175, NtRaiseHardError), + (0x176, NtReadOnlyEnlistment), + (0x177, NtReadVirtualMemoryEx), + (0x178, NtRecoverEnlistment), + (0x179, NtRecoverResourceManager), + (0x17a, NtRecoverTransactionManager), + (0x17b, NtRegisterProtocolAddressInformation), + (0x17c, NtRegisterThreadTerminatePort), + (0x17d, NtReleaseKeyedEvent), + (0x17e, NtReleaseWorkerFactoryWorker), + (0x17f, NtRemoveIoCompletionEx), + (0x180, NtRemoveProcessDebug), + (0x181, NtRenameKey), + (0x182, NtRenameTransactionManager), + (0x183, NtReplaceKey), + (0x184, NtReplacePartitionUnit), + (0x185, NtReplyWaitReplyPort), + (0x186, NtRequestPort), + (0x187, NtResetEvent), + (0x188, NtResetWriteWatch), + (0x189, NtRestoreKey), + (0x18a, NtResumeProcess), + (0x18b, NtRevertContainerImpersonation), + (0x18c, NtRollbackComplete), + (0x18d, NtRollbackEnlistment), + (0x18e, NtRollbackRegistryTransaction), + (0x18f, NtRollbackTransaction), + (0x190, NtRollforwardTransactionManager), + (0x191, NtSaveKey), + (0x192, NtSaveKeyEx), + (0x193, NtSaveMergedKeys), + (0x194, NtSecureConnectPort), + (0x195, NtSerializeBoot), + (0x196, NtSetBootEntryOrder), + (0x197, NtSetBootOptions), + (0x198, NtSetCachedSigningLevel), + (0x199, NtSetCachedSigningLevel2), + (0x19a, NtSetContextThread), + (0x19b, NtSetDebugFilterState), + (0x19c, NtSetDefaultHardErrorPort), + (0x19d, NtSetDefaultLocale), + (0x19e, NtSetDefaultUILanguage), + (0x19f, NtSetDriverEntryOrder), + (0x1a0, NtSetEaFile), + (0x1a1, NtSetEventEx), + (0x1a2, NtSetHighEventPair), + (0x1a3, NtSetHighWaitLowEventPair), + (0x1a4, NtSetIRTimer), + (0x1a5, NtSetInformationCpuPartition), + (0x1a6, NtSetInformationDebugObject), + (0x1a7, NtSetInformationEnlistment), + (0x1a8, NtSetInformationIoRing), + (0x1a9, NtSetInformationJobObject), + (0x1aa, NtSetInformationKey), + (0x1ab, NtSetInformationResourceManager), + (0x1ac, NtSetInformationSymbolicLink), + (0x1ad, NtSetInformationToken), + (0x1ae, NtSetInformationTransaction), + (0x1af, NtSetInformationTransactionManager), + (0x1b0, NtSetInformationVirtualMemory), + (0x1b1, NtSetInformationWorkerFactory), + (0x1b2, NtSetIntervalProfile), + (0x1b3, NtSetIoCompletion), + (0x1b4, NtSetIoCompletionEx), + (0x1b5, NtSetLdtEntries), + (0x1b6, NtSetLowEventPair), + (0x1b7, NtSetLowWaitHighEventPair), + (0x1b8, NtSetQuotaInformationFile), + (0x1b9, NtSetSecurityObject), + (0x1ba, NtSetSystemEnvironmentValue), + (0x1bb, NtSetSystemEnvironmentValueEx), + (0x1bc, NtSetSystemInformation), + (0x1bd, NtSetSystemPowerState), + (0x1be, NtSetSystemTime), + (0x1bf, NtSetThreadExecutionState), + (0x1c0, NtSetTimer2), + (0x1c1, NtSetTimerEx), + (0x1c2, NtSetTimerResolution), + (0x1c3, NtSetUuidSeed), + (0x1c4, NtSetVolumeInformationFile), + (0x1c5, NtSetWnfProcessNotificationEvent), + (0x1c6, NtShutdownSystem), + (0x1c7, NtShutdownWorkerFactory), + (0x1c8, NtSignalAndWaitForSingleObject), + (0x1c9, NtSinglePhaseReject), + (0x1ca, NtStartProfile), + (0x1cb, NtStopProfile), + (0x1cc, NtSubmitIoRing), + (0x1cd, NtSubscribeWnfStateChange), + (0x1ce, NtSuspendProcess), + (0x1cf, NtSuspendThread), + (0x1d0, NtSystemDebugControl), + (0x1d1, NtTerminateEnclave), + (0x1d2, NtTerminateJobObject), + (0x1d3, NtTestAlert), + (0x1d4, NtThawRegistry), + (0x1d5, NtThawTransactions), + (0x1d6, NtTraceControl), + (0x1d7, NtTranslateFilePath), + (0x1d8, NtUmsThreadYield), + (0x1d9, NtUnloadDriver), + (0x1da, NtUnloadKey), + (0x1db, NtUnloadKey2), + (0x1dc, NtUnloadKeyEx), + (0x1dd, NtUnlockFile), + (0x1de, NtUnlockVirtualMemory), + (0x1df, NtUnmapViewOfSectionEx), + (0x1e0, NtUnsubscribeWnfStateChange), + (0x1e1, NtUpdateWnfStateData), + (0x1e2, NtVdmControl), + (0x1e3, NtWaitForAlertByThreadId), + (0x1e4, NtWaitForDebugEvent), + (0x1e5, NtWaitForKeyedEvent), + (0x1e6, NtWaitForWorkViaWorkerFactory), + (0x1e7, NtWaitHighEventPair), + (0x1e8, NtWaitLowEventPair), +} diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs index 0d478d012c..e5ab3572f2 100644 --- a/litebox_common_windows/src/loader.rs +++ b/litebox_common_windows/src/loader.rs @@ -24,31 +24,26 @@ const MAX_SECTIONS: usize = 96; /// The result of parsing a PE32+ file. #[derive(Debug)] pub struct PeParsedFile { - /// Basic image metadata from the PE optional and COFF headers. - pub image: PeImageInfo, - /// Raw PE section headers in file order. + image: PeImageInfo, sections: Vec, - /// Data directory entries indexed by `IMAGE_DIRECTORY_ENTRY_*`. - pub data_directories: Vec, + data_directories: Vec, trampoline: Option, } -/// Basic PE image metadata needed by the Windows shim loader. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PeImageInfo { - pub machine: u16, - pub characteristics: u16, - pub image_base: usize, - pub entry_point_rva: usize, - pub size_of_image: usize, - pub size_of_headers: usize, - pub section_alignment: usize, - pub file_alignment: usize, - /// e.g. `IMAGE_SUBSYSTEM_WINDOWS_CUI`. - pub subsystem: u16, - pub dll_characteristics: u16, - pub size_of_heap_reserve: usize, - pub size_of_heap_commit: usize, +struct PeImageInfo { + machine: u16, + characteristics: u16, + image_base: usize, + entry_point_rva: usize, + size_of_image: usize, + size_of_headers: usize, + section_alignment: usize, + file_alignment: usize, + subsystem: u16, + dll_characteristics: u16, + size_of_heap_reserve: usize, + size_of_heap_commit: usize, } /// Information about the mapped PE image. @@ -77,29 +72,25 @@ struct TrampolineHeader64 { const TRAMPOLINE_MAGIC: [u8; 8] = *b"LITEBOX0"; -/// A PE data directory entry. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PeDataDirectory { - pub virtual_address: u32, - pub size: u32, +struct PeDataDirectory { + virtual_address: u32, + size: u32, } /// Errors that can occur when parsing a PE file. #[derive(Debug, Error)] pub enum PeParseError { - /// The input file could not be read. #[error("I/O error")] Io(#[source] E), - /// The file is not a supported Windows executable image. #[error("unsupported PE image")] UnsupportedImage, - /// The LiteBox trampoline footer is malformed. #[error("bad LiteBox trampoline")] BadTrampoline, - /// The LiteBox trampoline footer has an unsupported version. + /// The LiteBox trampoline magic was found but the version byte is unknown. #[error("invalid LiteBox trampoline version")] BadTrampolineVersion, - /// A PE field overflowed the host representation used by this parser. + /// A PE field overflowed the host's `usize` representation. #[error("PE field overflow")] Overflow, } @@ -107,19 +98,15 @@ pub enum PeParseError { /// Errors that can occur when mapping a PE image into memory. #[derive(Debug, Error)] pub enum PeLoadError { - /// Memory mapping error. #[error("memory mapping error")] Map(#[source] E), - /// The image contains inconsistent or overflowing fields. #[error("invalid PE image")] InvalidImage, /// The image had to be loaded away from its preferred base but has no base relocations. #[error("PE image requires base relocations")] RelocationRequired, - /// The image contains a relocation type this loader does not support. #[error("unsupported PE base relocation type {0}")] UnsupportedRelocation(u16), - /// A mapped memory access failed. #[error(transparent)] Fault(#[from] Fault), } @@ -716,7 +703,6 @@ fn usize_from_u64(value: u64) -> Result> { value.try_into().map_err(|_| PeParseError::Overflow) } -/// Round `address` down to the nearest [`PAGE_SIZE`] multiple. pub fn page_align_down(address: usize) -> usize { address & !(PAGE_SIZE - 1) } @@ -776,10 +762,7 @@ pub trait MapMemory { /// Trait for reading and writing memory that has been mapped via [`MapMemory`]. pub trait AccessMemory { - /// Read from memory. fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Fault>; - - /// Write to memory. fn write(&mut self, address: usize, data: &[u8]) -> Result<(), Fault>; } @@ -796,15 +779,13 @@ pub struct Protection { } impl Protection { - /// Read-write, no-execute. - pub(crate) const RW: Self = Self { + const RW: Self = Self { read: true, write: true, execute: false, }; - /// Read-only. - pub(crate) const R: Self = Self { + const R: Self = Self { read: true, write: false, execute: false, diff --git a/litebox_runner_windows_userland/Cargo.toml b/litebox_runner_windows_userland/Cargo.toml index 940b884d1f..c4bc5dfbe5 100644 --- a/litebox_runner_windows_userland/Cargo.toml +++ b/litebox_runner_windows_userland/Cargo.toml @@ -15,6 +15,7 @@ litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } [dev-dependencies] +litebox_common_windows = { version = "0.1.0", path = "../litebox_common_windows" } litebox_syscall_rewriter = { version = "0.1.0", path = "../litebox_syscall_rewriter" } tar = "0.4" diff --git a/litebox_runner_windows_userland/tests/run.rs b/litebox_runner_windows_userland/tests/run.rs index 943a5aa215..b0f8a74ba6 100644 --- a/litebox_runner_windows_userland/tests/run.rs +++ b/litebox_runner_windows_userland/tests/run.rs @@ -3,13 +3,6 @@ #![cfg(all(target_os = "windows", target_arch = "x86_64"))] -use std::ffi::c_void; - -unsafe extern "system" { - fn GetModuleHandleA(module_name: *const u8) -> *mut c_void; - fn GetProcAddress(module: *mut c_void, proc_name: *const u8) -> *const c_void; -} - #[test] fn loads_minimal_pe_without_imports() { let test_dir = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("no_import"); @@ -63,8 +56,8 @@ fn build_no_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { let source_path = test_dir.join("no_import.rs"); let raw_exe_path = test_dir.join("no_import.raw.exe"); let exe_path = test_dir.join("no_import.exe"); - let syscall_number = nt_terminate_process_syscall_number(); - println!("Using NtTerminateProcess syscall number `{syscall_number:#x}`"); + let syscall_number = litebox_common_windows::NtSysno::NtTerminateProcess.as_raw(); + println!("Using LiteBox NtTerminateProcess sysno `{syscall_number:#x}`"); std::fs::write( &source_path, minimal_pe_with_nt_terminate_process_syscall_source(syscall_number), @@ -134,41 +127,6 @@ fn panic(_info: &core::panic::PanicInfo<'_>) -> ! {{ ) } -fn nt_terminate_process_syscall_number() -> u32 { - // SAFETY: These are static NUL-terminated strings, and GetModuleHandleA does not retain them. - let ntdll = unsafe { GetModuleHandleA(c"ntdll.dll".as_ptr().cast()) }; - assert!( - !ntdll.is_null(), - "ntdll.dll is not loaded in the test process" - ); - - // SAFETY: These are static NUL-terminated strings, and GetProcAddress does not retain them. - let nt_terminate_process = - unsafe { GetProcAddress(ntdll, c"NtTerminateProcess".as_ptr().cast()) }; - assert!( - !nt_terminate_process.is_null(), - "NtTerminateProcess is not exported by ntdll.dll" - ); - - // SAFETY: `nt_terminate_process` points to executable code in the loaded ntdll image. Reading - // a small prefix of the function stub is sufficient to decode the `mov eax, imm32` syscall ID. - let stub = unsafe { std::slice::from_raw_parts(nt_terminate_process.cast::(), 32) }; - let syscall_offset = stub - .windows(2) - .position(|bytes| bytes == [0x0f, 0x05]) - .expect("NtTerminateProcess stub does not contain syscall instruction"); - let mov_eax_offset = stub[..syscall_offset] - .iter() - .position(|byte| *byte == 0xb8) - .expect("NtTerminateProcess stub does not load a syscall number into eax"); - - u32::from_le_bytes( - stub[mov_eax_offset + 1..mov_eax_offset + 5] - .try_into() - .unwrap(), - ) -} - fn build_rewritten_system_dll(test_dir: &std::path::Path, dll_name: &str) -> std::path::PathBuf { let dll_path = fixture_system32_path(test_dir, dll_name); let host_dll = std::fs::read(host_system32_file_path(dll_name)) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 5d0bc5bce7..1180efdd89 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -19,6 +19,7 @@ use core::sync::atomic::{AtomicI32, Ordering}; use litebox::LiteBox; use litebox::mm::PageManager; use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; +use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; use litebox_platform_multiplex::Platform; @@ -179,12 +180,19 @@ impl Task { } fn handle_syscall_request(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { - // TODO: Decode the NT syscall number and dispatch only NtTerminateProcess here. + if NtSysno::from_raw(ctx.orig_rax) != Some(NtSysno::NtTerminateProcess) { + litebox_util_log::debug!( + syscall_number = ctx.orig_rax; + "Unsupported Windows syscall" + ); + return ContinueOperation::Terminate; + } + litebox_util_log::debug!( syscall_number = ctx.orig_rax, process_handle:% = format_args!("{:#x}", ctx.r10), exit_status:% = format_args!("{:#x}", ctx.rdx); - "Handling temporary NtTerminateProcess syscall" + "Handling NtTerminateProcess syscall" ); self.process .exit_code diff --git a/litebox_shim_windows/src/loader/mod.rs b/litebox_shim_windows/src/loader/mod.rs index 1cc884abdc..3d7b9a32e0 100644 --- a/litebox_shim_windows/src/loader/mod.rs +++ b/litebox_shim_windows/src/loader/mod.rs @@ -3,6 +3,4 @@ mod pe; -pub(crate) use pe::PeLoader; - -pub use pe::WindowsLoadError; +pub(super) use pe::{PeLoader, WindowsLoadError}; diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 6cb5f62316..e9c2eadac5 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -25,14 +25,12 @@ const ZERO_CHUNK: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; const FILE_CHUNK_BYTES: usize = 64 * 1024; const INITIAL_STACK_SIZE: usize = 1024 * 1024; -/// Struct to hold the information needed to start the program. pub(crate) struct PeLoadInfo { pub(crate) entry_point: usize, pub(crate) stack_top: usize, pub(crate) ntdll_mapping: Option, } -/// Loader for Windows PE files. pub(crate) struct PeLoader<'a, FS: ShimFS> { fs: Arc, page_manager: &'a crate::WindowsPageManager, @@ -50,6 +48,9 @@ impl<'a, FS: ShimFS> PeLoader<'a, FS> { let length = NonZeroPageSize::new(INITIAL_STACK_SIZE).ok_or(PeImageAccessError::AddressOverflow)?; + // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` does not set + // `fixed_addr`, so the page manager picks an unused region — there is no overlapping- + // mapping precondition for the caller to uphold. let stack_base = unsafe { self.page_manager .create_stack_pages(None, length, CreatePagesFlags::empty()) @@ -139,13 +140,11 @@ fn load_image_with_writable_sections( /// Errors that can occur while opening, parsing, and mapping a Windows PE image. #[derive(Debug, Error)] pub enum WindowsLoadError { - /// PE parsing failed. #[error("failed to parse PE image")] Parse(#[source] PeParseError), - /// PE image mapping failed. #[error("failed to load PE image")] Load(#[source] PeLoadError), - /// Opening the PE image failed. + /// Accessing the PE backing file or its mapped memory failed. #[error(transparent)] Access(#[from] PeImageAccessError), /// Guest ntdll.dll does not export LdrInitializeThunk. @@ -329,28 +328,22 @@ impl MapMemory for PeImageMapper<'_, FS> { /// Errors from the shim-side PE image backing file and memory mapper. #[derive(Debug, Error)] pub enum PeImageAccessError { - /// Opening the executable failed. #[error("failed to open PE image")] Open(#[from] litebox::fs::errors::OpenError), - /// Reading the executable failed. #[error("failed to read PE image")] Read(#[from] litebox::fs::errors::ReadError), - /// Reading file metadata failed. #[error("failed to read PE image metadata")] FileStatus(#[from] litebox::fs::errors::FileStatusError), - /// The backing file ended before the requested range was read. + /// The backing file ended before the requested range was filled. #[error("short read from PE image")] ShortRead, - /// A PE file offset or image address overflowed this host representation. + /// A PE file offset or image address overflowed the host's `usize`. #[error("PE image address overflow")] AddressOverflow, - /// A memory mapping operation failed. #[error(transparent)] Mapping(#[from] MappingError), - /// A memory protection operation failed. #[error(transparent)] Protect(#[from] VmemProtectError), - /// A mapped memory access failed. #[error("mapped PE image memory access failed")] MemoryAccess, } diff --git a/litebox_syscall_rewriter/Cargo.toml b/litebox_syscall_rewriter/Cargo.toml index 2864e95995..d8488fd758 100644 --- a/litebox_syscall_rewriter/Cargo.toml +++ b/litebox_syscall_rewriter/Cargo.toml @@ -11,6 +11,7 @@ clap = ["dep:clap"] [dependencies] iced-x86 = { version = "1.21", default-features = false, features = ["no_std", "decoder", "encoder", "instr_info"] } +litebox_common_windows = { path = "../litebox_common_windows/", version = "0.1.0" } object = { version = "0.36.7", default-features = false, features = ["elf", "pe", "read_core"] } thiserror = { version = "2.0.6", default-features = false } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 5a19ebce5d..32da2930e4 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -18,12 +18,13 @@ #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; -use alloc::collections::BTreeSet; +use alloc::collections::{BTreeMap, BTreeSet}; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; +use litebox_common_windows::NtSysno; use object::pe::{IMAGE_SCN_CNT_CODE, IMAGE_SCN_MEM_EXECUTE}; use object::read::elf::{ElfFile, ProgramHeader as _}; use object::read::pe::{ImageNtHeaders as _, ImageOptionalHeader as _, PeFile64}; @@ -114,6 +115,12 @@ struct SyscallPatchResult { skipped_addrs: Vec, } +/// Limit on how far backward from a `syscall` we look for the `mov eax, imm32` +/// that loads its sysno. A real NT stub always sets `eax` within a handful of +/// instructions of the `syscall`; the bound keeps us from rewriting some +/// unrelated `mov eax` that happens to share an immediate value with a sysno. +const NT_SYSNO_REWRITE_LOOKBACK: usize = 16; + /// Update the `input_binary` with a call to `trampoline` instead of any `syscall` instructions. /// /// The `trampoline` must be an absolute address if specified; if unspecified, it will be set to @@ -198,11 +205,13 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res (arch, text_sections, trampoline_base_addr) }; + let control_transfer_targets = get_control_transfer_targets(arch, &*buf, &text_sections)?; let mut trampoline_data = Vec::from(trampoline.unwrap_or(0).to_le_bytes()); let patch_result = patch_syscalls_in_sections( arch, buf, &text_sections, + &control_transfer_targets, trampoline_base_addr, trampoline_base_addr, &mut trampoline_data, @@ -238,7 +247,7 @@ pub fn rewrite_pe_for_litebox(input_binary: &[u8], trampoline: Option) -> R buf[..input_binary.len()].copy_from_slice(input_binary); let buf = &mut buf[..input_binary.len()]; - let (text_sections, trampoline_base_rva, trampoline_base_addr) = { + let (text_sections, sysno_map, trampoline_base_rva, trampoline_base_addr) = { let pe = PeFile64::parse(&*buf).map_err(|e| Error::ParseError(e.to_string()))?; let optional_header = pe.nt_headers().optional_header(); let size_of_image = u64::from(optional_header.size_of_image()); @@ -262,13 +271,27 @@ pub fn rewrite_pe_for_litebox(input_binary: &[u8], trampoline: Option) -> R Err(InternalError::Public(e)) => return Err(e), Err(e) => unreachable!("unexpected internal error: {e:?}"), }; - (text_sections, trampoline_base_rva, trampoline_base_addr) + let sysno_map = pe_ntdll_sysno_map(&file, buf, &text_sections)?; + ( + text_sections, + sysno_map, + trampoline_base_rva, + trampoline_base_addr, + ) }; for section in &text_sections { let section_data = section_slice_mut(buf, section)?; rewrite_gs_to_fs_in_section(Arch::X86_64, section.vaddr, section_data)?; } + let control_transfer_targets = get_control_transfer_targets(Arch::X86_64, buf, &text_sections)?; + rewrite_nt_sysnos_in_sections( + Arch::X86_64, + buf, + &text_sections, + &sysno_map, + &control_transfer_targets, + )?; let mut trampoline_data = Vec::from(trampoline.unwrap_or(0).to_le_bytes()); // Windows ntdll packs some syscall stubs too tightly for the generic @@ -277,6 +300,7 @@ pub fn rewrite_pe_for_litebox(input_binary: &[u8], trampoline: Option) -> R Arch::X86_64, buf, &text_sections, + &control_transfer_targets, trampoline_base_addr, trampoline_base_addr, &mut trampoline_data, @@ -285,6 +309,7 @@ pub fn rewrite_pe_for_litebox(input_binary: &[u8], trampoline: Option) -> R Arch::X86_64, buf, &text_sections, + &control_transfer_targets, trampoline_base_addr, trampoline_base_addr, &mut trampoline_data, @@ -347,6 +372,212 @@ fn pe_text_sections( Ok(text_sections) } +/// For ntdll-like PEs, walks `Nt*` exports of `file`, reads the build-specific +/// sysno each stub loads into `eax`, and maps it to the stable LiteBox +/// [`NtSysno`] for that name. `Nt*` and `Zw*` always share sysno numbering +/// inside ntdll, so a map keyed on the build-specific number lets a later pass +/// rewrite both flavors (and any internal ntdll helpers that issue the same +/// syscall inline) uniformly. +fn pe_ntdll_sysno_map( + file: &object::File<'_>, + buf: &[u8], + text_sections: &[TextSectionInfo], +) -> Result> { + let mut map = BTreeMap::new(); + let mut exports_ntdll_loader_entrypoint = false; + + for export in file + .exports() + .map_err(|e| Error::ParseError(e.to_string()))? + { + let Ok(name) = core::str::from_utf8(export.name()) else { + continue; + }; + exports_ntdll_loader_entrypoint |= name == "LdrInitializeThunk"; + + let Some(sysno) = NtSysno::from_export_name(name) else { + continue; + }; + + let addr = export.address(); + let Some(section) = text_sections.iter().find(|s| { + s.vaddr + .checked_add(s.size) + .is_some_and(|end| addr >= s.vaddr && addr < end) + }) else { + continue; + }; + + let section_data = section_slice(buf, section)?; + let stub_offset = usize::try_from(addr - section.vaddr) + .map_err(|_| Error::ParseError("export offset out of range".into()))?; + if let Some(build_sysno) = read_nt_stub_sysno(section_data, stub_offset) { + map.insert(build_sysno, sysno); + } + } + + if !exports_ntdll_loader_entrypoint { + return Ok(BTreeMap::new()); + } + + Ok(map) +} + +/// Reads the `mov eax, imm32` immediate that precedes a `syscall` instruction +/// within the first 32 bytes of an NT syscall stub starting at `stub_offset`. +/// Returns `None` if the bytes do not match the expected stub shape. +fn read_nt_stub_sysno(section_data: &[u8], stub_offset: usize) -> Option { + let stub = section_data.get(stub_offset..)?; + let stub_len = stub.len().min(32); + let syscall_offset = stub[..stub_len] + .windows(2) + .position(|bytes| bytes == [0x0f, 0x05])?; + let mov_eax_offset = stub[..syscall_offset] + .windows(5) + .position(|bytes| bytes[0] == 0xb8)?; + let imm = u32::from_le_bytes( + stub[mov_eax_offset + 1..mov_eax_offset + 5] + .try_into() + .ok()?, + ); + Some(imm) +} + +fn rewrite_nt_sysnos_in_sections( + arch: Arch, + buf: &mut [u8], + text_sections: &[TextSectionInfo], + sysno_map: &BTreeMap, + control_transfer_targets: &BTreeSet, +) -> Result { + if sysno_map.is_empty() { + return Ok(0); + } + let mut rewritten = 0; + for section in text_sections { + let section_data = section_slice_mut(buf, section)?; + rewritten += rewrite_nt_sysnos_in_section( + arch, + section.vaddr, + section_data, + sysno_map, + control_transfer_targets, + )?; + } + Ok(rewritten) +} + +/// For every `syscall` in `section_data`, looks backward up to +/// [`NT_SYSNO_REWRITE_LOOKBACK`] instructions for the closest `mov r32, imm32` +/// that targets `eax`. If the immediate is a known build-specific sysno from +/// `sysno_map`, rewrites it in place to the stable LiteBox sysno. +/// +/// The backward walk stops at any unconditional control transfer (`jmp`, +/// `ret`, indirect branch, exception), at any instruction that is itself a +/// control-transfer target, and at any earlier write to `eax`. Conditional +/// branches are walked through, because the canonical NT stub has a `test +/// [...], 1; jne +3; syscall` sequence where execution reaches `syscall` by +/// falling through `jne`. Syscalls that are themselves jump targets are +/// skipped entirely — there's no way to know which `mov eax` the jumping code +/// arrived with. +fn rewrite_nt_sysnos_in_section( + arch: Arch, + section_base_addr: u64, + section_data: &mut [u8], + sysno_map: &BTreeMap, + control_transfer_targets: &BTreeSet, +) -> Result { + let instructions = decode_section_instructions(arch, section_data, section_base_addr)?; + let mut info_factory = iced_x86::InstructionInfoFactory::new(); + let mut rewritten = 0; + + for (i, inst) in instructions.iter().enumerate() { + if inst.code() != iced_x86::Code::Syscall { + continue; + } + if control_transfer_targets.contains(&inst.ip()) { + continue; + } + let lookback_start = i.saturating_sub(NT_SYSNO_REWRITE_LOOKBACK); + for j in (lookback_start..i).rev() { + let prev = &instructions[j]; + // A `jne`/`je`/etc. between `mov eax, sysno` and `syscall` is normal + // (the canonical NT stub has `test ...; jne +3; syscall`), so we + // keep walking through conditional branches and calls — they fall + // through to the next instruction in the common case. We only stop + // at unconditional transfers that prove the linear chain from + // `prev → next → ... → syscall` was never the execution path. + if matches!( + prev.flow_control(), + iced_x86::FlowControl::UnconditionalBranch + | iced_x86::FlowControl::IndirectBranch + | iced_x86::FlowControl::Call + | iced_x86::FlowControl::IndirectCall + | iced_x86::FlowControl::Return + | iced_x86::FlowControl::Exception + ) { + break; + } + if prev.code() == iced_x86::Code::Mov_r32_imm32 + && prev.op0_register() == iced_x86::Register::EAX + { + if let Some(&sysno) = sysno_map.get(&prev.immediate32()) { + let inst_offset = usize::try_from(prev.ip() - section_base_addr) + .map_err(|_| Error::ParseError("instruction offset out of range".into()))?; + // `Mov_r32_imm32` always encodes the 32-bit immediate as the + // last four bytes of the instruction, regardless of any REX + // prefix in front of the opcode. + let imm_end = inst_offset + .checked_add(prev.len()) + .ok_or_else(|| Error::AddressOverflow("mov eax end".into()))?; + let imm_start = imm_end + .checked_sub(4) + .ok_or_else(|| Error::ParseError("mov eax length < 4".into()))?; + section_data[imm_start..imm_end].copy_from_slice(&sysno.as_raw().to_le_bytes()); + rewritten += 1; + } + break; + } + if instruction_writes_eax(&mut info_factory, prev) { + break; + } + if control_transfer_targets.contains(&prev.ip()) { + break; + } + } + } + + Ok(rewritten) +} + +/// Returns `true` if `inst` writes (or partially writes) the `eax` register +/// family — `eax`, `rax`, `ax`, `al`, `ah` (including implicit writes such as +/// `cpuid`/`mul`/`div`/`cdq`). Used by the sysno rewriter to detect an EAX +/// clobber between a stale `mov eax, K` and a downstream `syscall`, so a +/// sequence like `mov eax, K; xor eax, eax; syscall` does not mis-rewrite `K` +/// as a sysno load. +fn instruction_writes_eax( + info_factory: &mut iced_x86::InstructionInfoFactory, + inst: &iced_x86::Instruction, +) -> bool { + use iced_x86::{OpAccess, Register}; + for used in info_factory.info(inst).used_registers() { + if !matches!( + used.access(), + OpAccess::Write | OpAccess::ReadWrite | OpAccess::CondWrite | OpAccess::ReadCondWrite + ) { + continue; + } + if matches!( + used.register(), + Register::EAX | Register::RAX | Register::AX | Register::AL | Register::AH + ) { + return true; + } + } + false +} + fn rewrite_gs_to_fs_in_section( arch: Arch, section_base_addr: u64, @@ -379,11 +610,11 @@ fn patch_syscalls_in_sections( arch: Arch, buf: &mut [u8], text_sections: &[TextSectionInfo], + control_transfer_targets: &BTreeSet, trampoline_base_addr: u64, syscall_entry_addr: u64, trampoline_data: &mut Vec, ) -> Result { - let control_transfer_targets = get_control_transfer_targets(arch, &*buf, text_sections)?; let mut found_syscall = false; let mut skipped_addrs = Vec::new(); @@ -391,7 +622,7 @@ fn patch_syscalls_in_sections( let section_data = section_slice_mut(buf, section)?; match hook_syscalls_in_section( arch, - &control_transfer_targets, + control_transfer_targets, section.vaddr, section_data, trampoline_base_addr, @@ -418,11 +649,11 @@ fn patch_dense_windows_syscall_stubs_in_sections( arch: Arch, buf: &mut [u8], text_sections: &[TextSectionInfo], + control_transfer_targets: &BTreeSet, trampoline_base_addr: u64, syscall_entry_addr: u64, trampoline_data: &mut Vec, ) -> Result { - let control_transfer_targets = get_control_transfer_targets(arch, &*buf, text_sections)?; let mut patched_any = false; for section in text_sections { @@ -434,7 +665,7 @@ fn patch_dense_windows_syscall_stubs_in_sections( } patched_any |= patch_dense_windows_syscall_stub( - &control_transfer_targets, + control_transfer_targets, section.vaddr, section_data, trampoline_base_addr, @@ -538,7 +769,7 @@ fn is_already_hooked(input_binary: &[u8], arch: Arch) -> bool { if vaddr % 0x1000 != 0 { return false; } - if file_offset + trampoline_size != header_start as u64 { + if file_offset.checked_add(trampoline_size) != Some(header_start as u64) { return false; } @@ -954,7 +1185,7 @@ fn patch_dense_windows_syscall_stub( let fallback_offset = usize::try_from(fallback_addr - section_base_addr).unwrap(); section_data[fallback_offset] = 0xeb; section_data[fallback_offset + 1] = - i8::try_from(i128::from(stub_addr) - i128::from(fallback_addr + 2)) + i8::try_from(i128::from(stub_addr) - i128::from(fallback_addr) - 2) .map_err(|_| { Error::AddressOverflow("dense Windows syscall fallback jump out of range".into()) })? @@ -1286,7 +1517,8 @@ fn hook_syscall_and_after( // any RIP-relative memory operands for the new location. let syscall_inst_end = syscall_inst.next_ip(); let postsyscall_bytes = if syscall_inst_end < replace_end { - let postsyscall_target = target_addr + preamble_len; + let postsyscall_target = + checked_add_u64(target_addr, preamble_len, "post-syscall trampoline target")?; match reencode_instructions( &instructions[(inst_index + 1)..replace_end_idx], postsyscall_target, @@ -1349,3 +1581,150 @@ fn hook_syscall_and_after( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + const NT_STUB_BUILD_SYSNO: u32 = 0x1234; + + fn nt_stub_bytes() -> [u8; 24] { + [ + 0x4c, 0x8b, 0xd1, // mov r10, rcx + 0xb8, 0x34, 0x12, 0x00, 0x00, // mov eax, 0x1234 + 0xf6, 0x04, 0x25, 0x08, 0x03, 0xfe, 0x7f, 0x01, // test byte ptr [...], 1 + 0x75, 0x03, // jne +3 + 0x0f, 0x05, // syscall + 0xc3, // ret + 0xcd, 0x2e, // int 2e + 0xc3, // ret + ] + } + + #[test] + fn read_nt_stub_sysno_extracts_build_specific_imm32() { + let stub = nt_stub_bytes(); + assert_eq!(read_nt_stub_sysno(&stub, 0), Some(NT_STUB_BUILD_SYSNO)); + } + + #[test] + fn read_nt_stub_sysno_rejects_stub_without_syscall() { + let stub = [0xb8, 0x34, 0x12, 0x00, 0x00, 0xc3]; + assert_eq!(read_nt_stub_sysno(&stub, 0), None); + } + + #[test] + fn rewrite_replaces_mov_eax_before_syscall() { + let mut stub = nt_stub_bytes(); + let mut map = BTreeMap::new(); + map.insert(NT_STUB_BUILD_SYSNO, NtSysno::NtTerminateProcess); + let targets = BTreeSet::new(); + + let rewritten = + rewrite_nt_sysnos_in_section(Arch::X86_64, 0, &mut stub, &map, &targets).unwrap(); + assert_eq!(rewritten, 1); + assert_eq!( + &stub[4..8], + &NtSysno::NtTerminateProcess.as_raw().to_le_bytes(), + ); + } + + #[test] + fn rewrite_covers_zw_alias_with_same_build_sysno() { + // Two stubs back-to-back sharing the same build-specific sysno, the way + // ntdll's Nt* / Zw* pair often look when emitted as separate stubs. + let mut section = Vec::new(); + section.extend_from_slice(&nt_stub_bytes()); + section.extend_from_slice(&nt_stub_bytes()); + + let mut map = BTreeMap::new(); + map.insert(NT_STUB_BUILD_SYSNO, NtSysno::NtTerminateProcess); + let targets = BTreeSet::new(); + + let rewritten = + rewrite_nt_sysnos_in_section(Arch::X86_64, 0, &mut section, &map, &targets).unwrap(); + assert_eq!(rewritten, 2); + let expected = NtSysno::NtTerminateProcess.as_raw().to_le_bytes(); + assert_eq!(§ion[4..8], &expected); + assert_eq!( + §ion[nt_stub_bytes().len() + 4..nt_stub_bytes().len() + 8], + &expected + ); + } + + #[test] + fn rewrite_leaves_mov_eax_with_unknown_imm_alone() { + let mut stub = nt_stub_bytes(); + let map: BTreeMap = BTreeMap::new(); + let targets = BTreeSet::new(); + + let rewritten = + rewrite_nt_sysnos_in_section(Arch::X86_64, 0, &mut stub, &map, &targets).unwrap(); + assert_eq!(rewritten, 0); + assert_eq!(&stub[4..8], &NT_STUB_BUILD_SYSNO.to_le_bytes()); + } + + #[test] + fn rewrite_skips_when_eax_is_clobbered_before_syscall() { + // `mov eax, K; xor eax, eax; syscall`. The mov's K matches a known + // build sysno, but the xor zeroes eax before the syscall — so K is not + // the sysno that feeds the syscall and must not be rewritten. + let mut section: Vec = vec![ + 0xb8, 0x34, 0x12, 0x00, 0x00, // mov eax, 0x1234 + 0x31, 0xc0, // xor eax, eax + 0x0f, 0x05, // syscall + ]; + + let mut map = BTreeMap::new(); + map.insert(NT_STUB_BUILD_SYSNO, NtSysno::NtTerminateProcess); + let targets = BTreeSet::new(); + + let rewritten = + rewrite_nt_sysnos_in_section(Arch::X86_64, 0, &mut section, &map, &targets).unwrap(); + assert_eq!(rewritten, 0); + assert_eq!(§ion[1..5], &NT_STUB_BUILD_SYSNO.to_le_bytes()); + } + + #[test] + fn rewrite_does_not_cross_basic_block_boundary() { + // `mov eax, K; ret; syscall`. The mov's K matches + // a known sysno but lives in a previous function (separated by `ret`); + // the syscall is reached by control flow that never touched that mov. + let mut section: Vec = vec![ + 0xb8, 0x34, 0x12, 0x00, 0x00, // mov eax, 0x1234 (in prior function) + 0xc3, // ret (block boundary) + 0x0f, 0x05, // syscall (next function) + ]; + + let mut map = BTreeMap::new(); + map.insert(NT_STUB_BUILD_SYSNO, NtSysno::NtTerminateProcess); + let targets = BTreeSet::new(); + + let rewritten = + rewrite_nt_sysnos_in_section(Arch::X86_64, 0, &mut section, &map, &targets).unwrap(); + assert_eq!(rewritten, 0); + assert_eq!(§ion[1..5], &NT_STUB_BUILD_SYSNO.to_le_bytes()); + } + + #[test] + fn rewrite_skips_syscall_that_is_jump_target() { + // `mov eax, K; syscall` where the syscall is jumped to from elsewhere. + // We can't trust that the preceding mov is what set eax for callers that + // arrived via the jump. + let syscall_offset: u64 = 5; + let mut section: Vec = vec![ + 0xb8, 0x34, 0x12, 0x00, 0x00, // mov eax, 0x1234 (offset 0..5) + 0x0f, 0x05, // syscall (offset 5..7) + ]; + + let mut map = BTreeMap::new(); + map.insert(NT_STUB_BUILD_SYSNO, NtSysno::NtTerminateProcess); + let mut targets = BTreeSet::new(); + targets.insert(syscall_offset); + + let rewritten = + rewrite_nt_sysnos_in_section(Arch::X86_64, 0, &mut section, &map, &targets).unwrap(); + assert_eq!(rewritten, 0); + assert_eq!(§ion[1..5], &NT_STUB_BUILD_SYSNO.to_le_bytes()); + } +} From c3a60d2f0b4f2c12c38a155b9b8c9327a2bbbad1 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 26 May 2026 12:00:42 -0700 Subject: [PATCH 004/319] Add Windows NtStatus (#868) This PR introduces a typed `NtStatus` abstraction and uses it in the Windows shim syscall path. --- litebox_common_windows/src/lib.rs | 1 + litebox_common_windows/src/nt_status.rs | 576 ++++++++++++++++++++++++ litebox_shim_windows/src/lib.rs | 25 +- 3 files changed, 591 insertions(+), 11 deletions(-) create mode 100644 litebox_common_windows/src/nt_status.rs diff --git a/litebox_common_windows/src/lib.rs b/litebox_common_windows/src/lib.rs index ff24d3c0e3..a405cf6c4e 100644 --- a/litebox_common_windows/src/lib.rs +++ b/litebox_common_windows/src/lib.rs @@ -8,6 +8,7 @@ extern crate alloc; pub mod loader; +pub mod nt_status; macro_rules! nt_sysnos { ($(($number:literal, $name:ident)),+ $(,)?) => { diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs new file mode 100644 index 0000000000..1816b98bff --- /dev/null +++ b/litebox_common_windows/src/nt_status.rs @@ -0,0 +1,576 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NTSTATUS error handling. See [`NtStatus`]. + +use thiserror::Error; + +/// Windows NTSTATUS error codes +/// +/// This is a transparent wrapper around Windows NTSTATUS values (i.e., `i32`s) intended +/// to provide some type safety by expecting explicit conversions to/from `i32`s. +/// +/// NTSTATUS is a 32-bit signed integer that encodes severity, facility, and error code. +/// Typically: +/// - 0x00000000 = STATUS_SUCCESS (no error) +/// - 0xC0000000+ = NT_ERROR (severe errors) +/// - 0x80000000+ = NT_WARNING (warnings) +/// - 0x40000000+ = NT_INFORMATION (informational) +/// +/// Values are sourced from Wine's `include/ntstatus.h`. +#[derive(PartialEq, Eq, Clone, Copy, Error)] +pub struct NtStatus { + value: i32, +} + +impl From for i32 { + fn from(e: NtStatus) -> Self { + e.value + } +} + +impl core::fmt::Display for NtStatus { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl core::fmt::Debug for NtStatus { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "NtStatus({:#x} = {})", self.value, self.as_str()) + } +} + +impl NtStatus { + /// Helper function that creates an NtStatus from a raw NTSTATUS bit pattern. + pub const fn from_raw(value: u32) -> Self { + Self { + value: value.cast_signed(), + } + } + + /// Returns the raw NTSTATUS value + #[must_use] + pub const fn as_raw(self) -> i32 { + self.value + } + + /// Returns true if the status indicates success (value >= 0) + #[must_use] + pub const fn is_success(self) -> bool { + self.value >= 0 + } + + /// Returns true if the status indicates an error (value < 0) + #[must_use] + pub const fn is_error(self) -> bool { + self.value < 0 + } + + /// Returns this status code as a usize bit pattern. + #[must_use] + pub const fn to_usize(self) -> usize { + self.value.cast_unsigned() as usize + } + + /// Human-friendly readable version of `self`. + pub const fn as_str(self) -> &'static str { + match u32::from_ne_bytes(self.value.to_ne_bytes()) { + 0x00000000 => "STATUS_SUCCESS: The operation completed successfully", + 0x00000001 => { + "STATUS_WAIT_1: Caller specified WaitAny and one of the dispatcher objects was set" + } + 0x00000002 => { + "STATUS_WAIT_2: Caller specified WaitAny and one of the dispatcher objects was set" + } + 0x00000003 => { + "STATUS_WAIT_3: Caller specified WaitAny and one of the dispatcher objects was set" + } + 0x00000102 => "STATUS_TIMEOUT: The given timeout interval expired", + 0x00010001 => "DBG_EXCEPTION_HANDLED: Exception handled by debugger", + 0x00010002 => "DBG_CONTINUE: Continue from exception", + 0x40000000 => "STATUS_OBJECT_NAME_EXISTS: The object name already exists", + 0x80000001 => "STATUS_GUARD_PAGE_VIOLATION: Page fault on a guarded page", + 0x80000002 => "STATUS_DATATYPE_MISALIGNMENT: Datatype misalignment", + 0x80000003 => "STATUS_BREAKPOINT: Breakpoint encountered", + 0x80000004 => "STATUS_SINGLE_STEP: Single instruction executed", + 0x80000005 => "STATUS_BUFFER_OVERFLOW: Buffer overflow", + 0xC0000001 => "STATUS_UNSUCCESSFUL: The operation completed with an error", + 0xC0000002 => "STATUS_NOT_IMPLEMENTED: The function is not implemented", + 0xC0000003 => "STATUS_INVALID_INFO_CLASS: Invalid information class", + 0xC0000004 => "STATUS_INFO_LENGTH_MISMATCH: Information length mismatch", + 0xC0000005 => "STATUS_ACCESS_VIOLATION: Access violation", + 0xC0000006 => "STATUS_IN_PAGE_ERROR: In-page I/O error", + 0xC0000007 => "STATUS_PAGEFILE_QUOTA: Pagefile quota exceeded", + 0xC0000008 => "STATUS_INVALID_HANDLE: Invalid handle", + 0xC0000009 => "STATUS_BAD_INITIAL_STACK: Bad initial stack", + 0xC000000A => "STATUS_BAD_INITIAL_PC: Bad initial PC", + 0xC000000B => "STATUS_INVALID_CID: Invalid CID", + 0xC000000C => "STATUS_TIMER_NOT_CANCELED: Timer not canceled", + 0xC000000D => "STATUS_INVALID_PARAMETER: Invalid parameter", + 0xC000000E => "STATUS_NO_SUCH_DEVICE: Device not found", + 0xC000000F => "STATUS_NO_SUCH_FILE: File not found", + 0xC0000010 => "STATUS_INVALID_DEVICE_REQUEST: Invalid device request", + 0xC0000011 => "STATUS_END_OF_FILE: End of file", + 0xC0000012 => "STATUS_WRONG_VOLUME: Wrong volume", + 0xC0000013 => "STATUS_NO_MEDIA_IN_DEVICE: No media in device", + 0xC0000014 => "STATUS_UNRECOGNIZED_MEDIA: Unrecognized media", + 0xC0000016 => "STATUS_MORE_PROCESSING_REQUIRED: More processing required", + 0xC0000017 => "STATUS_NO_MEMORY: Insufficient memory", + 0xC0000019 => "STATUS_NOT_MAPPED_VIEW: Not mapped view", + 0xC000001A => "STATUS_UNABLE_TO_FREE_VM: Unable to free virtual memory", + 0xC000001B => "STATUS_UNABLE_TO_DELETE_SECTION: Unable to delete section", + 0xC000001C => "STATUS_INVALID_SYSTEM_SERVICE: Invalid system service", + 0xC000001D => "STATUS_ILLEGAL_INSTRUCTION: Illegal instruction", + 0xC000001E => "STATUS_INVALID_LOCK_SEQUENCE: Invalid lock sequence", + 0xC000001F => "STATUS_INVALID_VIEW_SIZE: Invalid view size", + 0xC0000020 => "STATUS_INVALID_FILE_FOR_SECTION: Invalid file for section", + 0xC0000021 => "STATUS_ALREADY_COMMITTED: Already committed", + 0xC0000022 => "STATUS_ACCESS_DENIED: Access denied", + 0xC0000023 => "STATUS_BUFFER_TOO_SMALL: Buffer too small", + 0xC0000024 => "STATUS_OBJECT_TYPE_MISMATCH: Object type mismatch", + 0xC0000025 => "STATUS_NONCONTINUABLE_EXCEPTION: Noncontinuable exception", + 0xC0000026 => "STATUS_INVALID_DISPOSITION: Invalid disposition", + 0xC0000027 => "STATUS_UNWIND: Unwind in progress", + 0xC0000028 => "STATUS_BAD_STACK: Bad stack", + 0xC0000029 => "STATUS_INVALID_UNWIND_TARGET: Invalid unwind target", + 0xC000002D => "STATUS_NOT_COMMITTED: Not committed", + 0xC0000033 => "STATUS_OBJECT_NAME_INVALID: Object name invalid", + 0xC0000034 => "STATUS_OBJECT_NAME_NOT_FOUND: Object name not found", + 0xC0000035 => "STATUS_OBJECT_NAME_COLLISION: Object name already exists", + 0xC0000037 => "STATUS_PORT_DISCONNECTED: Port disconnected", + 0xC0000039 => "STATUS_OBJECT_PATH_INVALID: Object path invalid", + 0xC000003A => "STATUS_OBJECT_PATH_NOT_FOUND: Object path not found", + 0xC000003C => "STATUS_DATA_OVERRUN: Data overrun", + 0xC000003D => "STATUS_DATA_LATE_ERROR: Data late error", + 0xC000003E => "STATUS_DATA_ERROR: Data error", + 0xC000003F => "STATUS_CRC_ERROR: CRC error", + 0xC0000040 => "STATUS_SECTION_TOO_BIG: Section too big", + 0xC0000041 => "STATUS_PORT_CONNECTION_REFUSED: Port connection refused", + 0xC0000042 => "STATUS_INVALID_PORT_HANDLE: Invalid port handle", + 0xC0000043 => "STATUS_SHARING_VIOLATION: Sharing violation", + 0xC0000044 => "STATUS_QUOTA_EXCEEDED: Quota exceeded", + 0xC0000045 => "STATUS_INVALID_PAGE_PROTECTION: Invalid page protection", + 0xC0000046 => "STATUS_MUTANT_NOT_OWNED: Mutant not owned", + 0xC0000047 => "STATUS_SEMAPHORE_LIMIT_EXCEEDED: Semaphore limit exceeded", + 0xC0000048 => "STATUS_PORT_ALREADY_SET: Port already set", + 0xC000004F => "STATUS_EAS_NOT_SUPPORTED: EAS not supported", + 0xC0000050 => "STATUS_EA_TOO_LARGE: EA too large", + 0xC0000056 => "STATUS_DELETE_PENDING: Delete pending", + 0xC000005F => "STATUS_NO_SUCH_LOGON_SESSION: No such logon session", + 0xC0000060 => "STATUS_NO_SUCH_PRIVILEGE: No such privilege", + 0xC0000061 => "STATUS_PRIVILEGE_NOT_HELD: Privilege not held", + 0xC000007C => "STATUS_NO_TOKEN: No token", + 0xC000007D => "STATUS_BAD_INHERITANCE_ACL: Bad inheritance ACL", + 0xC000007E => "STATUS_RANGE_NOT_LOCKED: Range not locked", + 0xC000007F => "STATUS_DISK_FULL: Disk full", + 0xC0000080 => "STATUS_SERVER_DISABLED: Server disabled", + 0xC00000A2 => "STATUS_MEDIA_WRITE_PROTECTED: Media write protected", + 0xC00000A6 => "STATUS_CANT_OPEN_ANONYMOUS: Cannot open anonymous", + 0xC00000AC => "STATUS_PIPE_NOT_AVAILABLE: Pipe not available", + 0xC00000AD => "STATUS_INVALID_PIPE_STATE: Invalid pipe state", + 0xC00000AE => "STATUS_PIPE_BUSY: Pipe busy", + 0xC00000B0 => "STATUS_PIPE_DISCONNECTED: Pipe disconnected", + 0xC00000E6 => "STATUS_GENERIC_NOT_MAPPED: Generic not mapped", + 0xC00000EF => "STATUS_INVALID_PARAMETER_1: Invalid parameter 1", + 0xC00000FD => "STATUS_STACK_OVERFLOW: Stack overflow", + 0xC0000102 => "STATUS_FILE_CORRUPT_ERROR: File corrupt error", + 0xC0000103 => "STATUS_NOT_A_DIRECTORY: Not a directory", + 0xC0000104 => "STATUS_BAD_LOGON_SESSION_STATE: Bad logon session state", + 0xC0000105 => "STATUS_LOGON_SESSION_COLLISION: Logon session collision", + 0xC0000106 => "STATUS_NAME_TOO_LONG: Name too long", + 0xC0000107 => "STATUS_FILES_OPEN: Files open", + 0xC0000108 => "STATUS_CONNECTION_IN_USE: Connection in use", + 0xC0000109 => "STATUS_MESSAGE_NOT_FOUND: Message not found", + 0xC000010A => "STATUS_PROCESS_IS_TERMINATING: Process is terminating", + 0xC000010D => "STATUS_CANNOT_IMPERSONATE: Cannot impersonate", + 0xC0000121 => "STATUS_CANNOT_DELETE: Cannot delete", + 0xC0000128 => "STATUS_FILE_CLOSED: File closed", + 0xC0000142 => "STATUS_DLL_INIT_FAILED: DLL initialization failed", + 0xC0000161 => "STATUS_ILLEGAL_CHARACTER: Illegal character", + 0xC0000162 => "STATUS_UNMAPPABLE_CHARACTER: Unmappable character", + 0xC0000184 => "STATUS_INVALID_DEVICE_STATE: Invalid device state", + 0xC0000201 => "STATUS_NETWORK_OPEN_RESTRICTION: Network open restriction", + 0xC0000202 => "STATUS_NO_USER_SESSION_KEY: No user session key", + 0xC000022D => "STATUS_RETRY: The operation should be retried", + 0xC00002DF => "STATUS_SAM_NEED_BOOTKEY_PASSWORD: SAM needs boot key password", + 0xC00002E0 => "STATUS_SAM_NEED_BOOTKEY_FLOPPY: SAM needs boot key floppy", + 0xC0000282 => "STATUS_RANGE_LIST_CONFLICT: Range list conflict", + 0xC0000283 => "STATUS_SOURCE_ELEMENT_EMPTY: Source element empty", + 0xC0000284 => "STATUS_DESTINATION_ELEMENT_FULL: Destination element full", + 0xC0000285 => "STATUS_ILLEGAL_ELEMENT_ADDRESS: Illegal element address", + 0xC0000286 => "STATUS_MAGAZINE_NOT_PRESENT: Magazine not present", + 0xC0000287 => "STATUS_REINITIALIZATION_NEEDED: Reinitialization needed", + _ => "STATUS_UNKNOWN: Unknown status code", + } + } + + /// STATUS_SUCCESS + pub const SUCCESS: Self = Self::from_raw(0x00000000); + + /// STATUS_WAIT_1 + pub const WAIT_1: Self = Self::from_raw(0x00000001); + + /// STATUS_WAIT_2 + pub const WAIT_2: Self = Self::from_raw(0x00000002); + + /// STATUS_WAIT_3 + pub const WAIT_3: Self = Self::from_raw(0x00000003); + + /// STATUS_TIMEOUT + pub const TIMEOUT: Self = Self::from_raw(0x00000102); + + /// DBG_EXCEPTION_HANDLED + pub const EXCEPTION_HANDLED: Self = Self::from_raw(0x00010001); + + /// DBG_CONTINUE + pub const CONTINUE: Self = Self::from_raw(0x00010002); + + /// STATUS_OBJECT_NAME_EXISTS + pub const OBJECT_NAME_EXISTS: Self = Self::from_raw(0x40000000); + + /// STATUS_GUARD_PAGE_VIOLATION + pub const GUARD_PAGE_VIOLATION: Self = Self::from_raw(0x80000001); + + /// STATUS_DATATYPE_MISALIGNMENT + pub const DATATYPE_MISALIGNMENT: Self = Self::from_raw(0x80000002); + + /// STATUS_BREAKPOINT + pub const BREAKPOINT: Self = Self::from_raw(0x80000003); + + /// STATUS_SINGLE_STEP + pub const SINGLE_STEP: Self = Self::from_raw(0x80000004); + + /// STATUS_BUFFER_OVERFLOW + pub const BUFFER_OVERFLOW: Self = Self::from_raw(0x80000005); + + /// STATUS_UNSUCCESSFUL + pub const UNSUCCESSFUL: Self = Self::from_raw(0xC0000001); + + /// STATUS_NOT_IMPLEMENTED + pub const NOT_IMPLEMENTED: Self = Self::from_raw(0xC0000002); + + /// STATUS_INVALID_INFO_CLASS + pub const INVALID_INFO_CLASS: Self = Self::from_raw(0xC0000003); + + /// STATUS_INFO_LENGTH_MISMATCH + pub const INFO_LENGTH_MISMATCH: Self = Self::from_raw(0xC0000004); + + /// STATUS_ACCESS_VIOLATION + pub const ACCESS_VIOLATION: Self = Self::from_raw(0xC0000005); + + /// STATUS_IN_PAGE_ERROR + pub const IN_PAGE_ERROR: Self = Self::from_raw(0xC0000006); + + /// STATUS_PAGEFILE_QUOTA + pub const PAGEFILE_QUOTA: Self = Self::from_raw(0xC0000007); + + /// STATUS_INVALID_HANDLE + pub const INVALID_HANDLE: Self = Self::from_raw(0xC0000008); + + /// STATUS_BAD_INITIAL_STACK + pub const BAD_INITIAL_STACK: Self = Self::from_raw(0xC0000009); + + /// STATUS_BAD_INITIAL_PC + pub const BAD_INITIAL_PC: Self = Self::from_raw(0xC000000A); + + /// STATUS_INVALID_CID + pub const INVALID_CID: Self = Self::from_raw(0xC000000B); + + /// STATUS_TIMER_NOT_CANCELED + pub const TIMER_NOT_CANCELED: Self = Self::from_raw(0xC000000C); + + /// STATUS_INVALID_PARAMETER + pub const INVALID_PARAMETER: Self = Self::from_raw(0xC000000D); + + /// STATUS_NO_SUCH_DEVICE + pub const NO_SUCH_DEVICE: Self = Self::from_raw(0xC000000E); + + /// STATUS_NO_SUCH_FILE + pub const NO_SUCH_FILE: Self = Self::from_raw(0xC000000F); + + /// STATUS_INVALID_DEVICE_REQUEST + pub const INVALID_DEVICE_REQUEST: Self = Self::from_raw(0xC0000010); + + /// STATUS_END_OF_FILE + pub const END_OF_FILE: Self = Self::from_raw(0xC0000011); + + /// STATUS_WRONG_VOLUME + pub const WRONG_VOLUME: Self = Self::from_raw(0xC0000012); + + /// STATUS_NO_MEDIA_IN_DEVICE + pub const NO_MEDIA_IN_DEVICE: Self = Self::from_raw(0xC0000013); + + /// STATUS_UNRECOGNIZED_MEDIA + pub const UNRECOGNIZED_MEDIA: Self = Self::from_raw(0xC0000014); + + /// STATUS_MORE_PROCESSING_REQUIRED + pub const MORE_PROCESSING_REQUIRED: Self = Self::from_raw(0xC0000016); + + /// STATUS_NO_MEMORY + pub const NO_MEMORY: Self = Self::from_raw(0xC0000017); + + /// STATUS_NOT_MAPPED_VIEW + pub const NOT_MAPPED_VIEW: Self = Self::from_raw(0xC0000019); + + /// STATUS_UNABLE_TO_FREE_VM + pub const UNABLE_TO_FREE_VM: Self = Self::from_raw(0xC000001A); + + /// STATUS_UNABLE_TO_DELETE_SECTION + pub const UNABLE_TO_DELETE_SECTION: Self = Self::from_raw(0xC000001B); + + /// STATUS_INVALID_SYSTEM_SERVICE + pub const INVALID_SYSTEM_SERVICE: Self = Self::from_raw(0xC000001C); + + /// STATUS_ILLEGAL_INSTRUCTION + pub const ILLEGAL_INSTRUCTION: Self = Self::from_raw(0xC000001D); + + /// STATUS_INVALID_LOCK_SEQUENCE + pub const INVALID_LOCK_SEQUENCE: Self = Self::from_raw(0xC000001E); + + /// STATUS_INVALID_VIEW_SIZE + pub const INVALID_VIEW_SIZE: Self = Self::from_raw(0xC000001F); + + /// STATUS_INVALID_FILE_FOR_SECTION + pub const INVALID_FILE_FOR_SECTION: Self = Self::from_raw(0xC0000020); + + /// STATUS_ALREADY_COMMITTED + pub const ALREADY_COMMITTED: Self = Self::from_raw(0xC0000021); + + /// STATUS_ACCESS_DENIED + pub const ACCESS_DENIED: Self = Self::from_raw(0xC0000022); + + /// STATUS_BUFFER_TOO_SMALL + pub const BUFFER_TOO_SMALL: Self = Self::from_raw(0xC0000023); + + /// STATUS_OBJECT_TYPE_MISMATCH + pub const OBJECT_TYPE_MISMATCH: Self = Self::from_raw(0xC0000024); + + /// STATUS_NONCONTINUABLE_EXCEPTION + pub const NONCONTINUABLE_EXCEPTION: Self = Self::from_raw(0xC0000025); + + /// STATUS_INVALID_DISPOSITION + pub const INVALID_DISPOSITION: Self = Self::from_raw(0xC0000026); + + /// STATUS_UNWIND + pub const UNWIND: Self = Self::from_raw(0xC0000027); + + /// STATUS_BAD_STACK + pub const BAD_STACK: Self = Self::from_raw(0xC0000028); + + /// STATUS_INVALID_UNWIND_TARGET + pub const INVALID_UNWIND_TARGET: Self = Self::from_raw(0xC0000029); + + /// STATUS_NOT_COMMITTED + pub const NOT_COMMITTED: Self = Self::from_raw(0xC000002D); + + /// STATUS_OBJECT_NAME_INVALID + pub const OBJECT_NAME_INVALID: Self = Self::from_raw(0xC0000033); + + /// STATUS_OBJECT_NAME_NOT_FOUND + pub const OBJECT_NAME_NOT_FOUND: Self = Self::from_raw(0xC0000034); + + /// STATUS_OBJECT_NAME_COLLISION + pub const OBJECT_NAME_COLLISION: Self = Self::from_raw(0xC0000035); + + /// STATUS_PORT_DISCONNECTED + pub const PORT_DISCONNECTED: Self = Self::from_raw(0xC0000037); + + /// STATUS_OBJECT_PATH_INVALID + pub const OBJECT_PATH_INVALID: Self = Self::from_raw(0xC0000039); + + /// STATUS_OBJECT_PATH_NOT_FOUND + pub const OBJECT_PATH_NOT_FOUND: Self = Self::from_raw(0xC000003A); + + /// STATUS_DATA_OVERRUN + pub const DATA_OVERRUN: Self = Self::from_raw(0xC000003C); + + /// STATUS_DATA_LATE_ERROR + pub const DATA_LATE_ERROR: Self = Self::from_raw(0xC000003D); + + /// STATUS_DATA_ERROR + pub const DATA_ERROR: Self = Self::from_raw(0xC000003E); + + /// STATUS_CRC_ERROR + pub const CRC_ERROR: Self = Self::from_raw(0xC000003F); + + /// STATUS_SECTION_TOO_BIG + pub const SECTION_TOO_BIG: Self = Self::from_raw(0xC0000040); + + /// STATUS_PORT_CONNECTION_REFUSED + pub const PORT_CONNECTION_REFUSED: Self = Self::from_raw(0xC0000041); + + /// STATUS_INVALID_PORT_HANDLE + pub const INVALID_PORT_HANDLE: Self = Self::from_raw(0xC0000042); + + /// STATUS_SHARING_VIOLATION + pub const SHARING_VIOLATION: Self = Self::from_raw(0xC0000043); + + /// STATUS_QUOTA_EXCEEDED + pub const QUOTA_EXCEEDED: Self = Self::from_raw(0xC0000044); + + /// STATUS_INVALID_PAGE_PROTECTION + pub const INVALID_PAGE_PROTECTION: Self = Self::from_raw(0xC0000045); + + /// STATUS_MUTANT_NOT_OWNED + pub const MUTANT_NOT_OWNED: Self = Self::from_raw(0xC0000046); + + /// STATUS_SEMAPHORE_LIMIT_EXCEEDED + pub const SEMAPHORE_LIMIT_EXCEEDED: Self = Self::from_raw(0xC0000047); + + /// STATUS_PORT_ALREADY_SET + pub const PORT_ALREADY_SET: Self = Self::from_raw(0xC0000048); + + /// STATUS_EAS_NOT_SUPPORTED + pub const EAS_NOT_SUPPORTED: Self = Self::from_raw(0xC000004F); + + /// STATUS_EA_TOO_LARGE + pub const EA_TOO_LARGE: Self = Self::from_raw(0xC0000050); + + /// STATUS_DELETE_PENDING + pub const DELETE_PENDING: Self = Self::from_raw(0xC0000056); + + /// STATUS_NO_SUCH_LOGON_SESSION + pub const NO_SUCH_LOGON_SESSION: Self = Self::from_raw(0xC000005F); + + /// STATUS_NO_SUCH_PRIVILEGE + pub const NO_SUCH_PRIVILEGE: Self = Self::from_raw(0xC0000060); + + /// STATUS_PRIVILEGE_NOT_HELD + pub const PRIVILEGE_NOT_HELD: Self = Self::from_raw(0xC0000061); + + /// STATUS_NO_TOKEN + pub const NO_TOKEN: Self = Self::from_raw(0xC000007C); + + /// STATUS_BAD_INHERITANCE_ACL + pub const BAD_INHERITANCE_ACL: Self = Self::from_raw(0xC000007D); + + /// STATUS_RANGE_NOT_LOCKED + pub const RANGE_NOT_LOCKED: Self = Self::from_raw(0xC000007E); + + /// STATUS_DISK_FULL + pub const DISK_FULL: Self = Self::from_raw(0xC000007F); + + /// STATUS_SERVER_DISABLED + pub const SERVER_DISABLED: Self = Self::from_raw(0xC0000080); + + /// STATUS_MEDIA_WRITE_PROTECTED + pub const MEDIA_WRITE_PROTECTED: Self = Self::from_raw(0xC00000A2); + + /// STATUS_CANT_OPEN_ANONYMOUS + pub const CANT_OPEN_ANONYMOUS: Self = Self::from_raw(0xC00000A6); + + /// STATUS_PIPE_NOT_AVAILABLE + pub const PIPE_NOT_AVAILABLE: Self = Self::from_raw(0xC00000AC); + + /// STATUS_INVALID_PIPE_STATE + pub const INVALID_PIPE_STATE: Self = Self::from_raw(0xC00000AD); + + /// STATUS_PIPE_BUSY + pub const PIPE_BUSY: Self = Self::from_raw(0xC00000AE); + + /// STATUS_PIPE_DISCONNECTED + pub const PIPE_DISCONNECTED: Self = Self::from_raw(0xC00000B0); + + /// STATUS_GENERIC_NOT_MAPPED + pub const GENERIC_NOT_MAPPED: Self = Self::from_raw(0xC00000E6); + + /// STATUS_INVALID_PARAMETER_1 + pub const INVALID_PARAMETER_1: Self = Self::from_raw(0xC00000EF); + + /// STATUS_STACK_OVERFLOW + pub const STACK_OVERFLOW: Self = Self::from_raw(0xC00000FD); + + /// STATUS_FILE_CORRUPT_ERROR + pub const FILE_CORRUPT_ERROR: Self = Self::from_raw(0xC0000102); + + /// STATUS_NOT_A_DIRECTORY + pub const NOT_A_DIRECTORY: Self = Self::from_raw(0xC0000103); + + /// STATUS_BAD_LOGON_SESSION_STATE + pub const BAD_LOGON_SESSION_STATE: Self = Self::from_raw(0xC0000104); + + /// STATUS_LOGON_SESSION_COLLISION + pub const LOGON_SESSION_COLLISION: Self = Self::from_raw(0xC0000105); + + /// STATUS_NAME_TOO_LONG + pub const NAME_TOO_LONG: Self = Self::from_raw(0xC0000106); + + /// STATUS_FILES_OPEN + pub const FILES_OPEN: Self = Self::from_raw(0xC0000107); + + /// STATUS_CONNECTION_IN_USE + pub const CONNECTION_IN_USE: Self = Self::from_raw(0xC0000108); + + /// STATUS_MESSAGE_NOT_FOUND + pub const MESSAGE_NOT_FOUND: Self = Self::from_raw(0xC0000109); + + /// STATUS_PROCESS_IS_TERMINATING + pub const PROCESS_IS_TERMINATING: Self = Self::from_raw(0xC000010A); + + /// STATUS_CANNOT_IMPERSONATE + pub const CANNOT_IMPERSONATE: Self = Self::from_raw(0xC000010D); + + /// STATUS_CANNOT_DELETE + pub const CANNOT_DELETE: Self = Self::from_raw(0xC0000121); + + /// STATUS_FILE_CLOSED + pub const FILE_CLOSED: Self = Self::from_raw(0xC0000128); + + /// STATUS_DLL_INIT_FAILED + pub const DLL_INIT_FAILED: Self = Self::from_raw(0xC0000142); + + /// STATUS_ILLEGAL_CHARACTER + pub const ILLEGAL_CHARACTER: Self = Self::from_raw(0xC0000161); + + /// STATUS_UNMAPPABLE_CHARACTER + pub const UNMAPPABLE_CHARACTER: Self = Self::from_raw(0xC0000162); + + /// STATUS_INVALID_DEVICE_STATE + pub const INVALID_DEVICE_STATE: Self = Self::from_raw(0xC0000184); + + /// STATUS_NETWORK_OPEN_RESTRICTION + pub const NETWORK_OPEN_RESTRICTION: Self = Self::from_raw(0xC0000201); + + /// STATUS_NO_USER_SESSION_KEY + pub const NO_USER_SESSION_KEY: Self = Self::from_raw(0xC0000202); + + /// STATUS_RETRY + pub const RETRY: Self = Self::from_raw(0xC000022D); + + /// STATUS_SAM_NEED_BOOTKEY_PASSWORD + pub const SAM_NEED_BOOTKEY_PASSWORD: Self = Self::from_raw(0xC00002DF); + + /// STATUS_SAM_NEED_BOOTKEY_FLOPPY + pub const SAM_NEED_BOOTKEY_FLOPPY: Self = Self::from_raw(0xC00002E0); + + /// STATUS_RANGE_LIST_CONFLICT + pub const RANGE_LIST_CONFLICT: Self = Self::from_raw(0xC0000282); + + /// STATUS_SOURCE_ELEMENT_EMPTY + pub const SOURCE_ELEMENT_EMPTY: Self = Self::from_raw(0xC0000283); + + /// STATUS_DESTINATION_ELEMENT_FULL + pub const DESTINATION_ELEMENT_FULL: Self = Self::from_raw(0xC0000284); + + /// STATUS_ILLEGAL_ELEMENT_ADDRESS + pub const ILLEGAL_ELEMENT_ADDRESS: Self = Self::from_raw(0xC0000285); + + /// STATUS_MAGAZINE_NOT_PRESENT + pub const MAGAZINE_NOT_PRESENT: Self = Self::from_raw(0xC0000286); + + /// STATUS_REINITIALIZATION_NEEDED + pub const REINITIALIZATION_NEEDED: Self = Self::from_raw(0xC0000287); +} + +impl From for NtStatus { + fn from(value: i32) -> Self { + Self { value } + } +} + +impl From for NtStatus { + fn from(value: u32) -> Self { + Self::from_raw(value) + } +} diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 1180efdd89..fa3c8c0c8c 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -15,6 +15,7 @@ use alloc::sync::Arc; use alloc::vec::Vec; use core::marker::PhantomData; use core::sync::atomic::{AtomicI32, Ordering}; +use litebox_common_windows::nt_status::NtStatus; use litebox::LiteBox; use litebox::mm::PageManager; @@ -180,24 +181,26 @@ impl Task { } fn handle_syscall_request(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { - if NtSysno::from_raw(ctx.orig_rax) != Some(NtSysno::NtTerminateProcess) { + if NtSysno::from_raw(ctx.orig_rax) == Some(NtSysno::NtTerminateProcess) { litebox_util_log::debug!( - syscall_number = ctx.orig_rax; - "Unsupported Windows syscall" + syscall_number = ctx.orig_rax, + process_handle:% = format_args!("{:#x}", ctx.r10), + exit_status:% = format_args!("{:#x}", ctx.rdx); + "Handling NtTerminateProcess syscall" ); + self.process + .exit_code + .store(windows_exit_status_to_i32(ctx.rdx), Ordering::Relaxed); + ctx.rax = NtStatus::SUCCESS.to_usize(); return ContinueOperation::Terminate; } litebox_util_log::debug!( - syscall_number = ctx.orig_rax, - process_handle:% = format_args!("{:#x}", ctx.r10), - exit_status:% = format_args!("{:#x}", ctx.rdx); - "Handling NtTerminateProcess syscall" + syscall_number = ctx.orig_rax; + "Unsupported Windows syscall" ); - self.process - .exit_code - .store(windows_exit_status_to_i32(ctx.rdx), Ordering::Relaxed); - ContinueOperation::Terminate + ctx.rax = NtStatus::UNSUCCESSFUL.to_usize(); + ContinueOperation::Resume } fn handle_interrupt_request( From 4ccecc5c5b3ad84416368c399280ede6b8eb361c Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 27 May 2026 10:27:30 -0700 Subject: [PATCH 005/319] Parse Windows PE exports and initialize (#870) This PR extends the Windows PE loader to parse export table so the shim can discover and initialize `ntdll!KiUserInvertedFunctionTable` for rewritten guest ntdll startup. Adds Windows-only shim tests that compare against the host ntdll table: - verifies our export lookup finds the real KiUserInvertedFunctionTable address; - dumps the host inverted function table; - parses host-loaded ntdll and the test executable from memory and compares expected entries against the actual table. --- Cargo.lock | 1 + litebox_common_windows/src/loader.rs | 313 ++++++++++++++++++--- litebox_shim_windows/Cargo.toml | 1 + litebox_shim_windows/src/lib.rs | 25 ++ litebox_shim_windows/src/loader/pe.rs | 383 +++++++++++++++++++++++++- 5 files changed, 682 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f94349452..0cee927b85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1771,6 +1771,7 @@ dependencies = [ "litebox_platform_multiplex", "litebox_util_log", "thiserror", + "zerocopy", ] [[package]] diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs index e5ab3572f2..0215adc50d 100644 --- a/litebox_common_windows/src/loader.rs +++ b/litebox_common_windows/src/loader.rs @@ -4,11 +4,10 @@ //! PE loader-facing parser and mapper. //! //! This module parses PE metadata and maps images through platform-provided traits. - -use alloc::vec::Vec; +use alloc::{string::String, vec::Vec}; use core::cmp; use core::mem::size_of; -use zerocopy::{FromBytes, IntoBytes}; +use zerocopy::{FromBytes, Immutable, IntoBytes}; use object::endian::LittleEndian as LE; use object::pe; @@ -73,9 +72,41 @@ struct TrampolineHeader64 { const TRAMPOLINE_MAGIC: [u8; 8] = *b"LITEBOX0"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct PeDataDirectory { - virtual_address: u32, - size: u32, +pub struct PeDataDirectory { + /// Relative virtual address + pub rva: usize, + pub size: usize, +} + +/// Maximum number of entries in `ntdll!KiUserInvertedFunctionTable`. +pub const MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE: u32 = 512; + +/// Memory layout of this struct: +/// +/// ```text +/// +-----------------------------------+ +/// | KiUserInvertedFunctionTableHeader | +/// +-----------------------------------+ +/// | KiUserInvertedFunctionTableEntry[MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE] | +/// +-----------------------------------+ +/// ``` +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct KiUserInvertedFunctionTableHeader { + pub current_size: u32, + pub maximum_size: u32, + pub epoch: u32, + pub overflow: u8, + pub padding_0: [u8; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct KiUserInvertedFunctionTableEntry { + pub exception_directory_address: usize, + pub image_base: usize, + pub image_size: u32, + pub size_of_table: u32, } /// Errors that can occur when parsing a PE file. @@ -111,11 +142,28 @@ pub enum PeLoadError { Fault(#[from] Fault), } +/// Errors that can occur when parsing the export table of a loaded PE image. +#[derive(Debug, Error)] +pub enum PeExportError { + #[error("invalid PE export table")] + InvalidImage, + /// A PE export field overflowed the host's `usize` representation. + #[error("PE export field overflow")] + Overflow, + #[error(transparent)] + Fault(#[from] Fault), +} + macro_rules! checked_add { ($a:expr, $b:expr, $e:expr) => { $a.checked_add($b).ok_or($e) }; } +macro_rules! checked_mul { + ($a:expr, $b:expr, $e:expr) => { + $a.checked_mul($b).ok_or($e) + }; +} macro_rules! checked_add_invalid { ($a:expr, $b:expr) => { checked_add!($a, $b, PeLoadError::InvalidImage) @@ -160,6 +208,18 @@ impl PeParsedFile { self.trampoline.is_some() } + /// Returns the PE image size from the optional header. + #[must_use] + pub fn image_size(&self) -> usize { + self.image.size_of_image + } + + /// Returns the exception directory, if present. + #[must_use] + pub fn exception_directory(&self) -> Option { + self.data_directory(pe::IMAGE_DIRECTORY_ENTRY_EXCEPTION) + } + /// Load the PE image into memory. /// /// This maps PE headers and sections into their image locations, @@ -298,6 +358,125 @@ impl PeParsedFile { }) } + /// Look up selected named exports from an already-loaded PE image. + /// + /// The export name table is scanned once. The returned vector has the same + /// order as `names`; entries are `None` when the image does not export that + /// name as a concrete address. + pub fn find_export_addresses( + &self, + base_addr: usize, + mem: &mut impl AccessMemory, + names: &[&str], + ) -> Result>, PeExportError> { + let mut addresses = alloc::vec![None; names.len()]; + if names.is_empty() { + return Ok(addresses); + } + checked_add!(base_addr, self.image.size_of_image, PeExportError::Overflow)?; + + let Some(export_dir) = self.data_directory(pe::IMAGE_DIRECTORY_ENTRY_EXPORT) else { + return Ok(addresses); + }; + let export_rva = export_dir.rva; + let export_size = export_dir.size; + if export_size < size_of::() { + return Err(PeExportError::InvalidImage); + } + let export_end_rva = checked_add!(export_rva, export_size, PeExportError::Overflow)?; + if export_end_rva > self.image.size_of_image { + return Err(PeExportError::InvalidImage); + } + + let directory_address = base_addr + export_rva; + let directory: pe::ImageExportDirectory = + mem_read_pod::<_, PeExportError>(mem, directory_address)?; + + let function_count = directory.number_of_functions.get(LE) as usize; + let name_count = directory.number_of_names.get(LE) as usize; + let address_table_rva = directory.address_of_functions.get(LE) as usize; + let name_table_rva = directory.address_of_names.get(LE) as usize; + let name_ordinal_table_rva = directory.address_of_name_ordinals.get(LE) as usize; + + if function_count != 0 && address_table_rva == 0 { + return Err(PeExportError::InvalidImage); + } + validate_image_range( + self.image.size_of_image, + address_table_rva, + checked_mul!(function_count, size_of::(), PeExportError::Overflow)?, + )?; + if name_count == 0 { + return Ok(addresses); + } + if name_table_rva == 0 || name_ordinal_table_rva == 0 { + return Err(PeExportError::InvalidImage); + } + validate_image_range( + self.image.size_of_image, + name_table_rva, + checked_mul!(name_count, size_of::(), PeExportError::Overflow)?, + )?; + validate_image_range( + self.image.size_of_image, + name_ordinal_table_rva, + checked_mul!(name_count, size_of::(), PeExportError::Overflow)?, + )?; + + let mut found = 0; + for name_index in 0..name_count { + let name_pointer_address = base_addr + name_table_rva + name_index * size_of::(); + let name_rva = mem_read_pod::(mem, name_pointer_address)? as usize; + let export_name = + read_c_string_at_rva(base_addr, self.image.size_of_image, mem, name_rva)?; + let Some(requested_index) = names.iter().position(|name| *name == export_name) else { + continue; + }; + if addresses[requested_index].is_some() { + continue; + } + + let ordinal_index_address = + base_addr + name_ordinal_table_rva + name_index * size_of::(); + let ordinal_index = + mem_read_pod::(mem, ordinal_index_address)? as usize; + if ordinal_index >= function_count { + return Err(PeExportError::InvalidImage); + } + + let function_rva_address = + base_addr + address_table_rva + ordinal_index * size_of::(); + let function_rva = mem_read_pod::(mem, function_rva_address)?; + if let Some(address) = export_address( + base_addr, + self.image.size_of_image, + export_rva, + export_end_rva, + function_rva, + )? { + addresses[requested_index] = Some(address); + found += 1; + if found == names.len() { + break; + } + } + } + + Ok(addresses) + } + + fn data_directory(&self, index: usize) -> Option { + let directory = self + .data_directories + .get(index) + .filter(|directory| directory.rva != 0 && directory.size != 0)?; + directory + .rva + .checked_add(directory.size) + .filter(|end| *end <= self.image.size_of_image)?; + Some(*directory) + } + /// Parse the LiteBox PE trampoline footer, if present. /// /// The trampoline RVA is relative to the image base. The first pointer-sized @@ -436,8 +615,8 @@ impl PeParsedFile { .ok_or(PeLoadError::RelocationRequired)?; let image_end = checked_add_invalid!(base_addr, self.image.size_of_image)?; - let dir_addr = checked_add_invalid!(base_addr, reloc_dir.virtual_address as usize)?; - let dir_end = checked_add_invalid!(dir_addr, reloc_dir.size as usize)?; + let dir_addr = checked_add_invalid!(base_addr, reloc_dir.rva)?; + let dir_end = checked_add_invalid!(dir_addr, reloc_dir.size)?; // `delta` represents a possibly-negative offset via two's-complement // wrap in `usize`; the signed cast preserves the sign for `wrapping_add_signed`. @@ -445,10 +624,7 @@ impl PeParsedFile { let mut cursor = dir_addr; while cursor < dir_end { - let mut header_bytes = [0u8; size_of::()]; - mem.read(cursor, &mut header_bytes)?; - let (header, _) = object::pod::from_bytes::(&header_bytes) - .map_err(|()| PeLoadError::InvalidImage)?; + let header: pe::ImageBaseRelocation = mem_read_pod::<_, PeLoadError>(mem, cursor)?; let page_rva = header.virtual_address.get(LE); let block_size = header.size_of_block.get(LE) as usize; if block_size < size_of::() || !block_size.is_multiple_of(2) { @@ -462,7 +638,7 @@ impl PeParsedFile { let mut entry_addr = checked_add_invalid!(cursor, size_of::())?; while entry_addr < block_end { - let entry = mem_read_u16(mem, entry_addr)?; + let entry: u16 = mem_read_pod::<_, PeLoadError>(mem, entry_addr)?; let typ = entry >> 12; let entry_offset = u32::from(entry & 0x0fff); match typ { @@ -475,7 +651,8 @@ impl PeParsedFile { if relocation_end > image_end { return Err(PeLoadError::InvalidImage); } - let value = mem_read_u64(mem, relocation_address)?; + let value: u64 = + mem_read_pod::<_, PeLoadError>(mem, relocation_address)?; let relocated = value.wrapping_add_signed(delta_i64); mem.write(relocation_address, &relocated.to_le_bytes())?; } @@ -490,16 +667,81 @@ impl PeParsedFile { } } -fn mem_read_u16(mem: &mut impl AccessMemory, address: usize) -> Result> { - let mut buf = [0u8; size_of::()]; - mem.read(address, &mut buf)?; - Ok(u16::from_le_bytes(buf)) +fn export_address( + base_addr: usize, + image_size: usize, + export_rva: usize, + export_end_rva: usize, + function_rva: u32, +) -> Result, PeExportError> { + if function_rva == 0 { + return Ok(None); + } + + let function_rva = function_rva as usize; + if function_rva >= export_rva && function_rva < export_end_rva { + return Ok(None); + } + + validate_image_range(image_size, function_rva, 1)?; + Ok(Some(base_addr + function_rva)) +} + +fn read_c_string_at_rva( + base_addr: usize, + image_size: usize, + mem: &mut impl AccessMemory, + rva: usize, +) -> Result { + if rva >= image_size { + return Err(PeExportError::InvalidImage); + } + + let mut bytes = Vec::new(); + for current_rva in rva..image_size { + let address = base_addr + current_rva; + let byte: u8 = mem_read_pod::<_, PeExportError>(mem, address)?; + if byte == 0 { + return String::from_utf8(bytes).map_err(|_| PeExportError::InvalidImage); + } + bytes.push(byte); + } + + Err(PeExportError::InvalidImage) +} + +trait MemReadPodError: From { + fn invalid_pod() -> Self; +} + +impl MemReadPodError for PeExportError { + fn invalid_pod() -> Self { + Self::InvalidImage + } +} + +impl MemReadPodError for PeLoadError { + fn invalid_pod() -> Self { + Self::InvalidImage + } } -fn mem_read_u64(mem: &mut impl AccessMemory, address: usize) -> Result> { - let mut buf = [0u8; size_of::()]; - mem.read(address, &mut buf)?; - Ok(u64::from_le_bytes(buf)) +fn mem_read_pod( + mem: &mut impl AccessMemory, + address: usize, +) -> Result { + let mut buf = alloc::vec![0u8; size_of::()]; + mem.read(address, &mut buf).map_err(E::from)?; + let (value, _) = object::pod::from_bytes::(&buf).map_err(|()| E::invalid_pod())?; + Ok(*value) +} + +fn validate_image_range(image_size: usize, rva: usize, len: usize) -> Result<(), PeExportError> { + let end = checked_add!(rva, len, PeExportError::Overflow)?; + if end > image_size { + return Err(PeExportError::InvalidImage); + } + Ok(()) } type ParsedHeaders = ( @@ -514,7 +756,10 @@ type ParsedHeaders = ( /// `#[repr(transparent)]` byte-array wrappers), so the byte buffer's alignment /// trivially satisfies `from_bytes`'s check and the transmute happens inside /// `object::pod` rather than here. -fn read_pod(file: &mut F, offset: u64) -> Result> { +fn file_read_pod( + file: &mut F, + offset: u64, +) -> Result> { let mut buf = alloc::vec![0u8; size_of::()]; file.read_at(offset, &mut buf).map_err(PeParseError::Io)?; let (val, _) = @@ -523,7 +768,7 @@ fn read_pod(file: &mut F, offset: u64) -> Result( +fn file_read_pod_vec( file: &mut F, offset: u64, count: usize, @@ -546,7 +791,7 @@ fn parse_headers( if file_size < size_of::() { return Err(PeParseError::UnsupportedImage); } - let dos: pe::ImageDosHeader = read_pod(file, 0)?; + let dos: pe::ImageDosHeader = file_read_pod(file, 0)?; if dos.e_magic.get(LE) != pe::IMAGE_DOS_SIGNATURE { return Err(PeParseError::UnsupportedImage); } @@ -557,7 +802,7 @@ fn parse_headers( if nt_end > file_size as u64 { return Err(PeParseError::UnsupportedImage); } - let nt: pe::ImageNtHeaders64 = read_pod(file, nt_offset)?; + let nt: pe::ImageNtHeaders64 = file_read_pod(file, nt_offset)?; if nt.signature.get(LE) != pe::IMAGE_NT_SIGNATURE { return Err(PeParseError::UnsupportedImage); } @@ -611,16 +856,13 @@ fn parse_headers( if num_rva_and_sizes > pe::IMAGE_NUMBEROF_DIRECTORY_ENTRIES { return Err(PeParseError::UnsupportedImage); } - let raw_dirs: Vec = read_pod_vec(file, nt_end, num_rva_and_sizes)?; + let raw_dirs: Vec = file_read_pod_vec(file, nt_end, num_rva_and_sizes)?; let data_directories: Vec<_> = raw_dirs .iter() .map(|dir| { - let virtual_address = dir.virtual_address.get(LE); - let size = dir.size.get(LE); - PeDataDirectory { - virtual_address, - size, - } + let rva = dir.virtual_address.get(LE) as usize; + let size = dir.size.get(LE) as usize; + PeDataDirectory { rva, size } }) .collect(); @@ -641,7 +883,8 @@ fn parse_headers( if sections_end > file_size as u64 { return Err(PeParseError::UnsupportedImage); } - let sections: Vec = read_pod_vec(file, sections_offset, num_sections)?; + let sections: Vec = + file_read_pod_vec(file, sections_offset, num_sections)?; validate_sections(&image, §ions, file_size)?; Ok((image, sections, data_directories)) @@ -711,7 +954,7 @@ pub fn page_align_down(address: usize) -> usize { pub trait ReadAt { type Error; - /// Read `buf.len()` bytes at `offset`. Short reads are not permitted. + /// Read `buf.len()` bytes at `offset`. fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error>; fn size(&mut self) -> Result; diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml index 81e808c5fd..0850ff4358 100644 --- a/litebox_shim_windows/Cargo.toml +++ b/litebox_shim_windows/Cargo.toml @@ -10,6 +10,7 @@ litebox_common_windows = { path = "../litebox_common_windows/", version = "0.1.0 litebox_platform_multiplex = { path = "../litebox_platform_multiplex/", version = "0.1.0", default-features = false } litebox_util_log = { path = "../litebox_util_log", version = "0.1.0" } thiserror = { version = "2.0.6", default-features = false } +zerocopy = { version = "0.8", default-features = false, features = ["derive"] } [features] default = ["platform_windows_userland"] diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index fa3c8c0c8c..7878138ebc 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -46,6 +46,31 @@ pub(crate) type WindowsFS = litebox::fs::layered::FileSystem< pub trait ShimFS: litebox::fs::FileSystem + Send + Sync + 'static {} impl ShimFS for T {} +fn write_value(address: usize, value: T) -> Option<()> +where + T: zerocopy::FromBytes + zerocopy::IntoBytes, +{ + use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; + let ptr = ::RawMutPointer::::from_usize( + address, + ); + ptr.write_at_offset(0, value) +} + +fn write_slice(address: usize, values: &[T]) -> Option<()> +where + T: Copy + zerocopy::FromBytes + zerocopy::IntoBytes, +{ + use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; + let ptr = ::RawMutPointer::::from_usize( + address, + ); + for (index, value) in values.iter().copied().enumerate() { + ptr.write_at_offset(index.try_into().ok()?, value)?; + } + Some(()) +} + /// Builds a Windows NT shim instance. pub struct WindowsShimBuilder { litebox: LiteBox, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index e9c2eadac5..5f87df5c45 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -3,6 +3,7 @@ use alloc::{sync::Arc, vec::Vec}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, SystemInfoProvider as _}; +use litebox::utils::TruncateExt as _; use litebox::{ fs::{Mode, OFlags}, mm::linux::{ @@ -11,8 +12,9 @@ use litebox::{ platform::RawPointerProvider, }; use litebox_common_windows::loader::{ - AccessMemory, Fault, MapMemory, MappingInfo, PAGE_SIZE, PeLoadError, PeParseError, - PeParsedFile, Protection, ReadAt, page_align_down, + AccessMemory, Fault, KiUserInvertedFunctionTableEntry, KiUserInvertedFunctionTableHeader, + MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, MapMemory, MappingInfo, PAGE_SIZE, PeExportError, + PeLoadError, PeParseError, PeParsedFile, Protection, ReadAt, page_align_down, }; use litebox_platform_multiplex::Platform; use thiserror::Error; @@ -21,6 +23,7 @@ use crate::ShimFS; const NTDLL_WRITABLE_SECTIONS: &[&[u8]] = &[b".mrdata"]; const NTDLL_PATHS: &[&str] = &["/Windows/System32/ntdll.dll", "/windows/system32/ntdll.dll"]; +const RUNTIME_FUNCTION_ENTRY_SIZE: usize = 12; const ZERO_CHUNK: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; const FILE_CHUNK_BYTES: usize = 64 * 1024; const INITIAL_STACK_SIZE: usize = 1024 * 1024; @@ -46,6 +49,13 @@ impl<'a, FS: ShimFS> PeLoader<'a, FS> { let application_entry_point = image.mapping.entry_point; let ntdll = load_ntdll(self.fs.clone(), self.page_manager, NTDLL_PATHS)?; + if let Some(ntdll) = &ntdll { + if !ntdll.image.parsed.has_trampoline() { + return Err(WindowsLoadError::UnrewrittenNtDll); + } + Self::initialize_ki_user_inverted_function_table(&image, ntdll)?; + } + let length = NonZeroPageSize::new(INITIAL_STACK_SIZE).ok_or(PeImageAccessError::AddressOverflow)?; // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` does not set @@ -69,20 +79,99 @@ impl<'a, FS: ShimFS> PeLoader<'a, FS> { Ok(PeLoadInfo { entry_point: application_entry_point, stack_top, - ntdll_mapping: ntdll.map(|image| image.mapping), + ntdll_mapping: ntdll.map(|ntdll| ntdll.image.mapping), }) } + + fn initialize_ki_user_inverted_function_table( + application: &LoadedImage, + ntdll: &LoadedNtDll, + ) -> Result<(), WindowsLoadError> { + let table_address = ntdll.exports.ki_user_inverted_function_table; + + let mut entries = Vec::new(); + for image in [&ntdll.image, application] { + if let Some(entry) = image.inverted_function_table_entry()? { + entries.push(entry); + } + } + + let header = KiUserInvertedFunctionTableHeader { + current_size: entries.len().truncate(), + maximum_size: MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, + epoch: 0, + overflow: 0, + padding_0: [0; 3], + }; + + // `KI_USER_INVERTED_FUNCTION_TABLE` lives in ntdll's writable `.mrdata` section. + crate::write_value(table_address, header).ok_or(PeImageAccessError::MemoryAccess)?; + let entries_address = table_address + .checked_add(core::mem::size_of::()) + .ok_or(PeImageAccessError::AddressOverflow)?; + crate::write_slice(entries_address, &entries).ok_or(PeImageAccessError::MemoryAccess)?; + + litebox_util_log::debug!( + table:% = format_args!("{table_address:#x}"); + "Initialized ntdll!KiUserInvertedFunctionTable" + ); + + Ok(()) + } } struct LoadedImage { mapping: MappingInfo, + parsed: PeParsedFile, +} + +impl LoadedImage { + fn inverted_function_table_entry( + &self, + ) -> Result, WindowsLoadError> { + let Some(exception_directory) = self.parsed.exception_directory() else { + return Ok(None); + }; + if !exception_directory + .size + .is_multiple_of(RUNTIME_FUNCTION_ENTRY_SIZE) + { + return Err(WindowsLoadError::InvalidNtDllExceptionDirectory); + } + + let exception_directory_address = self + .mapping + .base_addr + .checked_add(exception_directory.rva) + .ok_or(PeImageAccessError::AddressOverflow)?; + let size_of_image = u32::try_from(self.parsed.image_size()) + .map_err(|_| PeImageAccessError::AddressOverflow)?; + + Ok(Some(KiUserInvertedFunctionTableEntry { + exception_directory_address, + image_base: self.mapping.base_addr, + image_size: size_of_image, + size_of_table: u32::try_from(exception_directory.size) + .map_err(|_| PeImageAccessError::AddressOverflow)?, + })) + } +} + +struct LoadedNtDll { + image: LoadedImage, + exports: NtDllExports, +} + +#[derive(Clone, Copy, Debug)] +struct NtDllExports { + ki_user_inverted_function_table: usize, } fn load_ntdll( fs: Arc, page_manager: &crate::WindowsPageManager, ntdll_paths: &[&str], -) -> Result, WindowsLoadError> { +) -> Result, WindowsLoadError> { for path in ntdll_paths { match load_image_with_writable_sections( fs.clone(), @@ -91,8 +180,9 @@ fn load_ntdll( NTDLL_WRITABLE_SECTIONS, ) { Ok(image) => { + let exports = ntdll_exports(&image)?; litebox_util_log::debug!(path:% = path; "Loaded guest ntdll.dll"); - return Ok(Some(image)); + return Ok(Some(LoadedNtDll { image, exports })); } Err(error) if is_missing_file_error(&error) => {} Err(error) => return Err(error), @@ -134,7 +224,36 @@ fn load_image_with_writable_sections( let mapping = parsed .load_with_writable_sections(&mut mapper, &mut memory, writable_section_names) .map_err(WindowsLoadError::Load)?; - Ok(LoadedImage { mapping }) + Ok(LoadedImage { mapping, parsed }) +} + +fn ntdll_exports(image: &LoadedImage) -> Result { + let export_names = [ + "LdrInitializeThunk", + "RtlUserThreadStart", + "KiUserInvertedFunctionTable", + ]; + let mut memory = PeImageMemory; + let addresses = image + .parsed + .find_export_addresses(image.mapping.base_addr, &mut memory, &export_names) + .map_err(WindowsLoadError::Export)?; + let [ + ldr_initialize_thunk, + rtl_user_thread_start, + ki_user_inverted_function_table, + ]: [Option; 3] = addresses + .try_into() + .map_err(|_| WindowsLoadError::MissingNtDllInvertedFunctionTable)?; + + ldr_initialize_thunk.ok_or(WindowsLoadError::MissingNtDllLoaderEntrypoint)?; + rtl_user_thread_start.ok_or(WindowsLoadError::MissingNtDllThreadEntrypoint)?; + let ki_user_inverted_function_table = ki_user_inverted_function_table + .ok_or(WindowsLoadError::MissingNtDllInvertedFunctionTable)?; + + Ok(NtDllExports { + ki_user_inverted_function_table, + }) } /// Errors that can occur while opening, parsing, and mapping a Windows PE image. @@ -144,6 +263,8 @@ pub enum WindowsLoadError { Parse(#[source] PeParseError), #[error("failed to load PE image")] Load(#[source] PeLoadError), + #[error("failed to parse PE export table")] + Export(#[source] PeExportError), /// Accessing the PE backing file or its mapped memory failed. #[error(transparent)] Access(#[from] PeImageAccessError), @@ -153,6 +274,12 @@ pub enum WindowsLoadError { /// Guest ntdll.dll does not export RtlUserThreadStart. #[error("guest ntdll.dll does not export RtlUserThreadStart")] MissingNtDllThreadEntrypoint, + /// Guest ntdll.dll does not export KiUserInvertedFunctionTable. + #[error("guest ntdll.dll does not export KiUserInvertedFunctionTable")] + MissingNtDllInvertedFunctionTable, + /// Guest ntdll.dll has an invalid exception directory. + #[error("guest ntdll.dll has an invalid exception directory")] + InvalidNtDllExceptionDirectory, /// Guest ntdll.dll has not been rewritten for LiteBox syscall/GS handling. #[error("guest ntdll.dll must be rewritten for LiteBox before entering its loader")] UnrewrittenNtDll, @@ -416,3 +543,247 @@ fn page_range(address: usize, len: usize) -> Result<(usize, usize), PeImageAcces .ok_or(PeImageAccessError::AddressOverflow)?; Ok((start, end - start)) } + +#[cfg(all(test, target_os = "windows", target_arch = "x86_64"))] +mod tests { + extern crate std; + + use alloc::{string::String, vec, vec::Vec}; + + use super::*; + + #[allow(non_snake_case)] + #[link(name = "kernel32")] + unsafe extern "system" { + fn GetModuleHandleW(lp_module_name: *const u16) -> *mut core::ffi::c_void; + fn GetProcAddress( + h_module: *mut core::ffi::c_void, + lp_proc_name: *const core::ffi::c_char, + ) -> *mut core::ffi::c_void; + fn GetModuleFileNameW( + h_module: *mut core::ffi::c_void, + lp_filename: *mut u16, + n_size: u32, + ) -> u32; + } + + #[test] + fn ntdll_exports_finds_ki_user_inverted_function_table() { + let ntdll = ntdll_module_base(); + let loaded_ntdll = loaded_module_image(ntdll); + + let exports = ntdll_exports(&loaded_ntdll).expect("failed to parse ntdll exports"); + let expected_table = own_inverted_function_table() as usize; + + assert_eq!( + exports.ki_user_inverted_function_table, expected_table, + "ntdll export lookup returned the wrong KiUserInvertedFunctionTable address" + ); + } + + #[test] + fn dumps_own_inverted_function_table() { + assert_eq!( + core::mem::size_of::(), + 16 + ); + assert_eq!(core::mem::size_of::(), 24); + + let table = own_inverted_function_table(); + // SAFETY: `own_inverted_function_table` resolves a live data export from the + // current process's already-loaded ntdll. The header is copied immediately. + let header = unsafe { read_table_value::(table) }; + + std::println!( + "ntdll!KiUserInvertedFunctionTable @ {:#x}: current_size={} maximum_size={} epoch={} overflow={}", + table as usize, + header.current_size, + header.maximum_size, + header.epoch, + header.overflow + ); + + assert!(header.maximum_size > 0); + assert!(header.maximum_size <= MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE); + assert!(header.current_size <= header.maximum_size); + assert!(header.current_size > 0); + + let entries = read_inverted_function_table_entries(table, header.current_size); + for (index, entry) in entries.iter().enumerate() { + let binary_name = module_name_from_base(entry.image_base); + std::println!( + " [{index}] binary=\"{}\" exception_directory={:#x} image_base={:#x} image_size={:#x} size_of_table={:#x}", + binary_name, + entry.exception_directory_address, + entry.image_base, + entry.image_size, + entry.size_of_table + ); + } + + assert_table_contains_entry( + &entries, + "ntdll.dll", + module_inverted_function_table_entry(ntdll_module_base()), + ); + assert_table_contains_entry( + &entries, + "the test executable", + module_inverted_function_table_entry(application_module_base()), + ); + } + + fn own_inverted_function_table() -> *const u8 { + let ntdll = ntdll_module_base(); + + // SAFETY: The module handle was returned by `GetModuleHandleW`, and the + // symbol name is a valid NUL-terminated C string literal. + let table = unsafe { GetProcAddress(ntdll, c"KiUserInvertedFunctionTable".as_ptr()) }; + assert!( + !table.is_null(), + "ntdll.dll does not export KiUserInvertedFunctionTable" + ); + + table.cast::() + } + + fn ntdll_module_base() -> *mut core::ffi::c_void { + module_base(Some("ntdll.dll")) + } + + fn application_module_base() -> *mut core::ffi::c_void { + module_base(None) + } + + fn module_base(name: Option<&str>) -> *mut core::ffi::c_void { + let module_name: Option> = name.map(|name| { + let mut name: Vec = name.encode_utf16().collect(); + name.push(0); + name + }); + let module_name_ptr = module_name.as_ref().map_or(core::ptr::null(), Vec::as_ptr); + // SAFETY: The string is NUL-terminated and points to a process-owned buffer + // that remains alive for the duration of the call. A null pointer asks for + // the current process's executable module. + let module = unsafe { GetModuleHandleW(module_name_ptr) }; + assert!(!module.is_null(), "module is not loaded in this process"); + + module + } + + fn module_inverted_function_table_entry( + module: *mut core::ffi::c_void, + ) -> KiUserInvertedFunctionTableEntry { + loaded_module_image(module) + .inverted_function_table_entry() + .expect("failed to build inverted function table entry") + .expect("loaded PE image has no exception directory") + } + + fn loaded_module_image(module: *mut core::ffi::c_void) -> LoadedImage { + let base_addr = module as usize; + let mut module_memory = ModuleMemory { + base: base_addr as *const u8, + }; + let parsed = PeParsedFile::parse(&mut module_memory) + .expect("failed to parse loaded PE image from memory"); + LoadedImage { + mapping: MappingInfo { + base_addr, + image_size: parsed.image_size(), + entry_point: base_addr, + }, + parsed, + } + } + + fn module_name_from_base(image_base: usize) -> String { + let module = image_base as *mut core::ffi::c_void; + let mut buffer = vec![0u16; 260]; + loop { + // SAFETY: `module` is the image base reported by ntdll's table, which is + // also the HMODULE for the loaded image. `buffer` is valid for `len` UTF-16 + // code units and remains alive for the duration of the call. + let len = unsafe { + GetModuleFileNameW( + module, + buffer.as_mut_ptr(), + u32::try_from(buffer.len()).unwrap(), + ) + } as usize; + if len == 0 { + return String::from(""); + } + if len < buffer.len() { + return String::from_utf16_lossy(&buffer[..len]); + } + buffer.resize(buffer.len() * 2, 0); + } + } + + fn read_inverted_function_table_entries( + table: *const u8, + current_size: u32, + ) -> Vec { + let entries = table.wrapping_add(core::mem::size_of::()); + let entry_size = core::mem::size_of::(); + (0..current_size as usize) + .map(|index| { + let entry_address = entries.wrapping_add(index * entry_size); + // SAFETY: The header just read from ntdll says `current_size` entries are + // initialized immediately after the header in this same exported table. + unsafe { read_table_value::(entry_address) } + }) + .collect() + } + + fn assert_table_contains_entry( + entries: &[KiUserInvertedFunctionTableEntry], + name: &str, + expected: KiUserInvertedFunctionTableEntry, + ) { + let actual = entries + .iter() + .find(|entry| entry.image_base == expected.image_base) + .unwrap_or_else(|| { + panic!("{name} was not present in the host inverted function table") + }); + + assert_eq!( + actual.exception_directory_address, + expected.exception_directory_address + ); + assert_eq!(actual.image_size, expected.image_size); + assert_eq!(actual.size_of_table, expected.size_of_table); + } + + unsafe fn read_table_value(address: *const u8) -> T { + // SAFETY: The caller guarantees that `address` points to at least + // `size_of::()` readable bytes. + let bytes = unsafe { core::slice::from_raw_parts(address, core::mem::size_of::()) }; + T::read_from_bytes(bytes).expect("failed to read table value") + } + + struct ModuleMemory { + base: *const u8, + } + + impl ReadAt for ModuleMemory { + type Error = core::convert::Infallible; + + fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error> { + let offset: usize = offset.try_into().unwrap(); + // SAFETY: The test only constructs `ModuleMemory` from live module image + // bases returned by `GetModuleHandleW`. `PeParsedFile::parse` reads PE + // headers and section headers, which remain mapped in loaded images. + unsafe { + core::ptr::copy_nonoverlapping(self.base.add(offset), buf.as_mut_ptr(), buf.len()); + } + Ok(()) + } + + fn size(&mut self) -> Result { + Ok(u64::MAX) + } + } +} From eb74f4f9c3fcef3fdf186792920f3d280076e0e8 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 27 May 2026 12:04:49 -0700 Subject: [PATCH 006/319] Decode syscalls for Windows shim (#871) Similar to `litebox_shim_linux`, add `SyscallRequest` to decode syscalls. --- litebox_shim_windows/src/lib.rs | 68 ++++++--- litebox_shim_windows/src/syscalls/mod.rs | 184 +++++++++++++++++++++++ 2 files changed, 232 insertions(+), 20 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/mod.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 7878138ebc..05f704eb70 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -15,6 +15,7 @@ use alloc::sync::Arc; use alloc::vec::Vec; use core::marker::PhantomData; use core::sync::atomic::{AtomicI32, Ordering}; +use litebox::platform::RawConstPointer as _; use litebox_common_windows::nt_status::NtStatus; use litebox::LiteBox; @@ -24,7 +25,10 @@ use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; use litebox_platform_multiplex::Platform; +use crate::syscalls::SyscallRequest; + mod loader; +mod syscalls; const DEFAULT_PROCESS_EXIT_CODE: i32 = 1; @@ -206,26 +210,55 @@ impl Task { } fn handle_syscall_request(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { - if NtSysno::from_raw(ctx.orig_rax) == Some(NtSysno::NtTerminateProcess) { + let Some(req) = SyscallRequest::::try_from_raw(ctx) else { litebox_util_log::debug!( - syscall_number = ctx.orig_rax, - process_handle:% = format_args!("{:#x}", ctx.r10), - exit_status:% = format_args!("{:#x}", ctx.rdx); - "Handling NtTerminateProcess syscall" + syscall:? = NtSysno::from_raw(ctx.orig_rax); + "Unsupported Windows syscall" ); - self.process - .exit_code - .store(windows_exit_status_to_i32(ctx.rdx), Ordering::Relaxed); - ctx.rax = NtStatus::SUCCESS.to_usize(); return ContinueOperation::Terminate; - } - + }; litebox_util_log::debug!( - syscall_number = ctx.orig_rax; - "Unsupported Windows syscall" + syscall:? = req; + "Handling Windows" ); - ctx.rax = NtStatus::UNSUCCESSFUL.to_usize(); - ContinueOperation::Resume + let (result, op) = match req { + SyscallRequest::NtTerminateProcess { + process_handle, + exit_status, + } => { + litebox_util_log::debug!( + syscall_number = ctx.orig_rax, + process_handle:% = format_args!("{:#x}", process_handle), + exit_status:% = format_args!("{:#x}", exit_status); + "Handling NtTerminateProcess syscall" + ); + self.process.exit_code.store(exit_status, Ordering::Relaxed); + (NtStatus::SUCCESS, ContinueOperation::Terminate) + } + SyscallRequest::NtAllocateVirtualMemory { + process_handle, + base_address, + zero_bits, + region_size, + allocation_type, + protect, + } => { + // TODO: placeholder for NtAllocateVirtualMemory + litebox_util_log::debug!( + process_handle:% = format_args!("{:#x}", process_handle), + base_address:% = format_args!("{:#x}", base_address.as_usize()), + zero_bits:% = format_args!("{:#x}", zero_bits), + region_size:% = format_args!("{:#x}", region_size.as_usize()), + allocation_type:% = format_args!("{:#x}", allocation_type), + protect:% = format_args!("{:#x}", protect); + "Handling NtAllocateVirtualMemory syscall" + ); + (NtStatus::UNSUCCESSFUL, ContinueOperation::Terminate) + } + }; + + ctx.rax = result.as_raw().cast_unsigned() as usize; + op } fn handle_interrupt_request( @@ -277,11 +310,6 @@ impl EnterShim for WindowsShimEntrypoints { } } -fn windows_exit_status_to_i32(status: usize) -> i32 { - let low_bits = u32::try_from(status & 0xffff_ffff).unwrap_or_default(); - i32::from_ne_bytes(low_bits.to_ne_bytes()) -} - /// A loaded Windows program and the process handle used to wait for it. pub struct LoadedProgram { /// The initial-thread entrypoint state passed to the platform's `run_thread`. diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs new file mode 100644 index 0000000000..caee1df24d --- /dev/null +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox::platform::{RawConstPointer as _, RawPointerProvider}; +use litebox::utils::TruncateExt as _; +use litebox_common_windows::NtSysno; +use litebox_common_windows::nt_status::NtStatus; + +const FIRST_STACK_ARGUMENT_OFFSET: usize = 0x28; + +#[derive(Debug)] +pub(crate) enum SyscallRequest { + NtTerminateProcess { + process_handle: usize, + exit_status: i32, + }, + NtAllocateVirtualMemory { + process_handle: usize, + base_address: Platform::RawMutPointer, + zero_bits: usize, + region_size: Platform::RawMutPointer, + allocation_type: u32, + protect: u32, + }, +} + +impl SyscallRequest { + pub(crate) fn try_from_raw(pt_regs: &litebox_common_linux::PtRegs) -> Option { + macro_rules! sys_req { + ($id:ident { $( $field:ident $(:$star:tt)? ),* $(,)? }) => { + sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] [ ]) + }; + (@[$id:ident] [ $f:ident $(,)? $($field:ident $(:$star:tt)?),* ] [ $n:literal $(,)? $($ns:literal),* ] [ $($tail:tt)* ]) => { + sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ $($ns),* ] [ $($tail)* $f: win_sys_req_arg::(pt_regs, $n)?, ]) + }; + (@[$id:ident] [ $f:ident : * $(,)? $($field:ident $(:$star:tt)?),* ] [ $n:literal $(,)? $($ns:literal),* ] [ $($tail:tt)* ]) => { + sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ $($ns),* ] [ $($tail)* $f: win_sys_req_ptr::(pt_regs, $n)?, ]) + }; + (@[$id:ident] [ $f:ident : { $expr:expr } $(,)? $($field:ident $(:$star:tt)?),* ] [ $n:literal $(,)? $($ns:literal),* ] [ $($tail:tt)* ]) => { + sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ $($ns),* ] [ $($tail)* $f: ($expr)(win_sys_req_arg::(pt_regs, $n)?), ]) + }; + (@[$id:ident] [ ] [ $($ns:literal),* ] [ $($tail:tt)* ]) => { + SyscallRequest::$id { $($tail)* } + }; + } + + match NtSysno::from_raw(pt_regs.orig_rax)? { + NtSysno::NtTerminateProcess => Some(sys_req!(NtTerminateProcess { + process_handle, + exit_status, + })), + NtSysno::NtAllocateVirtualMemory => Some(sys_req!(NtAllocateVirtualMemory { + process_handle, + base_address:*, + zero_bits, + region_size:*, + allocation_type, + protect, + })), + _ => None, + } + } +} + +fn win_syscall_arg( + pt_regs: &litebox_common_linux::PtRegs, + idx: usize, +) -> Option { + match idx { + 0 => Some(pt_regs.r10), + 1 => Some(pt_regs.rdx), + 2 => Some(pt_regs.r8), + 3 => Some(pt_regs.r9), + idx => { + // The first stack argument sits after the return address and x64 shadow space. + let stack_offset = FIRST_STACK_ARGUMENT_OFFSET + .checked_add((idx - 4).checked_mul(size_of::())?)?; + let stack_address = pt_regs.rsp.checked_add(stack_offset)?; + let stack_arg = Platform::RawConstPointer::::from_usize(stack_address); + stack_arg.read_at_offset(0) + } + } +} + +fn win_sys_req_arg( + pt_regs: &litebox_common_linux::PtRegs, + idx: usize, +) -> Option { + Some(T::reinterpret_truncated_from_usize(win_syscall_arg::< + Platform, + >(pt_regs, idx)?)) +} + +fn win_sys_req_ptr< + Platform: RawPointerProvider, + T: zerocopy::FromBytes, + P: ReinterpretUsizeAsPtr, +>( + pt_regs: &litebox_common_linux::PtRegs, + idx: usize, +) -> Option

{ + Some(P::reinterpret_usize_as_ptr(win_syscall_arg::( + pt_regs, idx, + )?)) +} + +trait ReinterpretTruncatedFromUsize: Sized { + fn reinterpret_truncated_from_usize(value: usize) -> Self; +} + +impl ReinterpretTruncatedFromUsize for usize { + fn reinterpret_truncated_from_usize(value: usize) -> Self { + value + } +} + +impl ReinterpretTruncatedFromUsize for u64 { + fn reinterpret_truncated_from_usize(value: usize) -> Self { + value as u64 + } +} + +impl ReinterpretTruncatedFromUsize for isize { + fn reinterpret_truncated_from_usize(value: usize) -> Self { + value.cast_signed() + } +} + +impl ReinterpretTruncatedFromUsize for NtStatus { + fn reinterpret_truncated_from_usize(value: usize) -> Self { + Self::from_raw(value.truncate()) + } +} + +macro_rules! reinterpret_truncated_unsigned { + ($($ty:ty),* $(,)?) => { + $( + impl ReinterpretTruncatedFromUsize for $ty { + fn reinterpret_truncated_from_usize(value: usize) -> Self { + value.truncate() + } + } + )* + }; +} + +macro_rules! reinterpret_truncated_signed { + ($($sty:ty),* $(,)?) => { + $( + impl ReinterpretTruncatedFromUsize for $sty { + fn reinterpret_truncated_from_usize(value: usize) -> Self { + value.cast_signed().truncate() + } + } + )* + }; +} + +reinterpret_truncated_unsigned!(u8, u16, u32); +reinterpret_truncated_signed!(i8, i16, i32); + +trait ReinterpretUsizeAsPtr: Sized { + fn reinterpret_usize_as_ptr(value: usize) -> Self; +} + +impl> + ReinterpretUsizeAsPtr> for P +{ + fn reinterpret_usize_as_ptr(value: usize) -> Self { + P::from_usize(value) + } +} + +impl> + ReinterpretUsizeAsPtr> for Option

+{ + fn reinterpret_usize_as_ptr(value: usize) -> Self { + if value == 0 { + None + } else { + Some(P::from_usize(value)) + } + } +} From 20de043baa385500529c57a4f9cf5a85a12443b6 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 27 May 2026 21:37:25 -0700 Subject: [PATCH 007/319] Enable more CI tests for Windows (#872) --- .github/workflows/ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ece633a33a..0de43233fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,17 +150,18 @@ jobs: with: tool: nextest@${{ env.NEXTEST_VERSION }} - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --locked --verbose --all-targets --all-features -p litebox_runner_linux_on_windows_userland - - run: cargo build --locked --verbose -p litebox_runner_linux_on_windows_userland - - run: cargo nextest run --locked --profile ci -p litebox_runner_linux_on_windows_userland + - run: cargo clippy --locked --verbose --all-targets --all-features -p litebox_runner_linux_on_windows_userland -p litebox_runner_windows_userland + - run: cargo build --locked --verbose -p litebox_runner_linux_on_windows_userland -p litebox_runner_windows_userland + - run: cargo nextest run --locked --profile ci -p litebox_runner_linux_on_windows_userland -p litebox_runner_windows_userland - run: cargo nextest run --locked --profile ci -p litebox_shim_linux --no-default-features --features platform_windows_userland + - run: cargo nextest run --locked --profile ci -p litebox_shim_windows - run: | - cargo test --locked --verbose --doc -p litebox_runner_linux_on_windows_userland + cargo test --locked --verbose --doc -p litebox_runner_linux_on_windows_userland -p litebox_runner_windows_userland # We need to run `cargo test --doc` separately because doc tests # aren't included in nextest at the moment. See relevant discussion at # https://github.com/nextest-rs/nextest/issues/16 - name: Build documentation (fail on warnings) - run: cargo doc --locked --verbose --no-deps --all-features --document-private-items -p litebox_runner_linux_on_windows_userland + run: cargo doc --locked --verbose --no-deps --all-features --document-private-items -p litebox_runner_linux_on_windows_userland -p litebox_runner_windows_userland build_and_test_snp: name: Build and Test SNP From 2f64f0557c388bb7588ff8d2ba0c1bb9ad28328e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 28 May 2026 10:45:56 -0700 Subject: [PATCH 008/319] Introduce Windows handle (#874) Adds typed Windows handle wrappers to the Windows shim and starts routing syscall process handles through those types instead of plain `usize` values. This PR also adds helper plumbing for converting LiteBox raw descriptors into Windows-style handles, including lookup and removal helpers that will be used by future NT object syscalls. --- litebox_shim_windows/src/lib.rs | 72 +++++++++++++-- litebox_shim_windows/src/syscalls/mod.rs | 109 ++++++++++++++++++++++- 2 files changed, 168 insertions(+), 13 deletions(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 05f704eb70..9ba60c5205 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -33,6 +33,8 @@ mod syscalls; const DEFAULT_PROCESS_EXIT_CODE: i32 = 1; pub(crate) type WindowsPageManager = PageManager; +pub(crate) type WindowsHandleStore = + litebox::sync::RwLock; pub type DefaultFS = WindowsFS; @@ -75,6 +77,57 @@ where Some(()) } +#[expect(dead_code, reason = "handle helpers are staged for NT object syscalls")] +pub(crate) fn insert_raw_handle( + litebox: &LiteBox, + handles: &WindowsHandleStore, + typed: litebox::fd::TypedFd, +) -> Result { + let mut handles = handles.write(); + let raw_fd = handles.fd_into_raw_integer(typed); + let Some(handle) = syscalls::Handle::from_raw_fd(raw_fd) else { + let typed = handles.fd_consume_raw_integer::(raw_fd).ok(); + drop(handles); + if let Some(typed) = typed { + let _ = litebox.descriptor_table_mut().remove(&typed); + } + return Err(NtStatus::QUOTA_EXCEEDED); + }; + Ok(handle) +} + +#[expect(dead_code, reason = "handle helpers are staged for NT object syscalls")] +pub(crate) fn raw_handle_entry( + litebox: &LiteBox, + handles: &WindowsHandleStore, + handle: syscalls::Handle, +) -> Option> { + let raw_fd = handle.raw_fd()?; + let typed = { + let handles = handles.read(); + handles.fd_from_raw_integer::(raw_fd).ok() + }?; + litebox.descriptor_table().entry_handle(&typed) +} + +#[expect(dead_code, reason = "handle helpers are staged for NT object syscalls")] +pub(crate) fn remove_raw_handle( + litebox: &LiteBox, + handles: &WindowsHandleStore, + handle: syscalls::Handle, +) { + let Some(raw_fd) = handle.raw_fd() else { + return; + }; + let typed = { + let mut handles = handles.write(); + handles.fd_consume_raw_integer::(raw_fd).ok() + }; + if let Some(typed) = typed { + let _ = litebox.descriptor_table_mut().remove(&typed); + } +} + /// Builds a Windows NT shim instance. pub struct WindowsShimBuilder { litebox: LiteBox, @@ -226,14 +279,15 @@ impl Task { process_handle, exit_status, } => { - litebox_util_log::debug!( - syscall_number = ctx.orig_rax, - process_handle:% = format_args!("{:#x}", process_handle), - exit_status:% = format_args!("{:#x}", exit_status); - "Handling NtTerminateProcess syscall" - ); - self.process.exit_code.store(exit_status, Ordering::Relaxed); - (NtStatus::SUCCESS, ContinueOperation::Terminate) + if !process_handle.is_null() && !process_handle.is_current() { + // TODO: allow terminating other processes + litebox_util_log::error!("Terminating other processes is not yet supported"); + (NtStatus::INVALID_HANDLE, ContinueOperation::Resume) + } else { + // TODO: Terminate all threads except the calling one if process_handle is zero. + self.process.exit_code.store(exit_status, Ordering::Relaxed); + (NtStatus::SUCCESS, ContinueOperation::Terminate) + } } SyscallRequest::NtAllocateVirtualMemory { process_handle, @@ -245,7 +299,7 @@ impl Task { } => { // TODO: placeholder for NtAllocateVirtualMemory litebox_util_log::debug!( - process_handle:% = format_args!("{:#x}", process_handle), + process_handle:% = format_args!("{:#x}", process_handle.as_raw()), base_address:% = format_args!("{:#x}", base_address.as_usize()), zero_bits:% = format_args!("{:#x}", zero_bits), region_size:% = format_args!("{:#x}", region_size.as_usize()), diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index caee1df24d..48096069af 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -5,17 +5,87 @@ use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; use litebox_common_windows::NtSysno; use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; const FIRST_STACK_ARGUMENT_OFFSET: usize = 0x28; +const HANDLE_SHIFT: u32 = 2; +const HANDLE_TAG_MASK: usize = (1usize << HANDLE_SHIFT) - 1; + +#[repr(transparent)] +#[derive( + Clone, Copy, Debug, Default, Eq, PartialEq, FromBytes, IntoBytes, Immutable, KnownLayout, +)] +pub(crate) struct Handle(usize); + +impl Handle { + #[must_use] + pub(crate) const fn from_raw(raw: usize) -> Self { + Self(raw) + } + + #[must_use] + pub(crate) fn from_raw_fd(raw_fd: usize) -> Option { + raw_fd + .checked_add(1)? + .checked_mul(1usize << HANDLE_SHIFT) + .map(Self) + } + + #[must_use] + pub(crate) fn raw_fd(self) -> Option { + if self.0 & HANDLE_TAG_MASK != 0 { + return None; + } + (self.0 >> HANDLE_SHIFT).checked_sub(1) + } + + #[must_use] + pub(crate) const fn as_raw(self) -> usize { + self.0 + } + + #[must_use] + pub(crate) const fn is_null(self) -> bool { + self.as_raw() == 0 + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct ProcessHandle(Handle); + +impl ProcessHandle { + pub(crate) const CURRENT: Self = Self::from_raw(usize::MAX); + + #[must_use] + pub(crate) const fn from_raw(raw: usize) -> Self { + Self(Handle::from_raw(raw)) + } + + #[must_use] + pub(crate) const fn as_raw(self) -> usize { + self.0.as_raw() + } + + #[must_use] + pub(crate) const fn is_null(self) -> bool { + self.0.is_null() + } + + #[must_use] + pub(crate) fn is_current(self) -> bool { + self == Self::CURRENT + } +} #[derive(Debug)] pub(crate) enum SyscallRequest { NtTerminateProcess { - process_handle: usize, + process_handle: ProcessHandle, exit_status: i32, }, NtAllocateVirtualMemory { - process_handle: usize, + process_handle: ProcessHandle, base_address: Platform::RawMutPointer, zero_bits: usize, region_size: Platform::RawMutPointer, @@ -46,11 +116,11 @@ impl SyscallRequest { match NtSysno::from_raw(pt_regs.orig_rax)? { NtSysno::NtTerminateProcess => Some(sys_req!(NtTerminateProcess { - process_handle, + process_handle: { ProcessHandle::from_raw }, exit_status, })), NtSysno::NtAllocateVirtualMemory => Some(sys_req!(NtAllocateVirtualMemory { - process_handle, + process_handle: { ProcessHandle::from_raw }, base_address:*, zero_bits, region_size:*, @@ -182,3 +252,34 @@ impl> } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn handle_encodes_raw_fds_and_rejects_invalid_values() { + let first_handle = Handle::from_raw_fd(0).expect("raw fd 0 should encode"); + assert_eq!(first_handle, Handle::from_raw(1usize << HANDLE_SHIFT)); + assert_eq!(first_handle.raw_fd(), Some(0)); + + let max_raw_fd = (usize::MAX >> HANDLE_SHIFT) - 1; + for raw_fd in [1, 42, max_raw_fd] { + let handle = Handle::from_raw_fd(raw_fd).expect("raw fd should encode"); + assert_eq!(handle.raw_fd(), Some(raw_fd)); + } + + assert_eq!(Handle::from_raw(0).raw_fd(), None); + + for tag in 1..=HANDLE_TAG_MASK { + assert_eq!(Handle::from_raw(tag).raw_fd(), None); + assert_eq!( + Handle::from_raw((2usize << HANDLE_SHIFT) | tag).raw_fd(), + None + ); + } + + assert_eq!(Handle::from_raw_fd(usize::MAX >> HANDLE_SHIFT), None); + assert_eq!(Handle::from_raw_fd(usize::MAX), None); + } +} From e8e825d97f539b1709727950e14918dcf8b500fc Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Thu, 28 May 2026 16:25:12 -0700 Subject: [PATCH 009/319] Fixup minor API updates due to Rust 1.96.0 --- litebox_shim_windows/src/loader/pe.rs | 2 +- litebox_shim_windows/src/syscalls/mod.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 5f87df5c45..2224460cef 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -97,7 +97,7 @@ impl<'a, FS: ShimFS> PeLoader<'a, FS> { } let header = KiUserInvertedFunctionTableHeader { - current_size: entries.len().truncate(), + current_size: entries.len().trunc(), maximum_size: MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, epoch: 0, overflow: 0, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 48096069af..5cbfb1452b 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -198,7 +198,7 @@ impl ReinterpretTruncatedFromUsize for isize { impl ReinterpretTruncatedFromUsize for NtStatus { fn reinterpret_truncated_from_usize(value: usize) -> Self { - Self::from_raw(value.truncate()) + Self::from_raw(value.trunc()) } } @@ -207,7 +207,7 @@ macro_rules! reinterpret_truncated_unsigned { $( impl ReinterpretTruncatedFromUsize for $ty { fn reinterpret_truncated_from_usize(value: usize) -> Self { - value.truncate() + value.trunc() } } )* @@ -219,7 +219,7 @@ macro_rules! reinterpret_truncated_signed { $( impl ReinterpretTruncatedFromUsize for $sty { fn reinterpret_truncated_from_usize(value: usize) -> Self { - value.cast_signed().truncate() + value.cast_signed().trunc() } } )* From a5b9b1edb7bb578b670e9a668ff8ad76d97b47fc Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 28 May 2026 17:41:32 -0700 Subject: [PATCH 010/319] Add setitimer/getitimer (#878) Implement syscalls `setitimer` and `getitime`, but `setitimer` does not support periodic timers yet (i.e., it is equivalent to `alarm` for now). Also add a new feature `alarm_fallback` to enable/disable the fallback alarm check. --- litebox_common_linux/src/lib.rs | 28 ++- .../tests/getitimer.c | 83 +++++++ litebox_runner_linux_userland/tests/helpers.h | 4 + .../tests/setitimer.c | 217 ++++++++++++++++++ litebox_shim_linux/Cargo.toml | 1 + litebox_shim_linux/src/lib.rs | 8 + litebox_shim_linux/src/syscalls/process.rs | 116 ++++++++-- litebox_shim_linux/src/syscalls/signal/mod.rs | 3 +- litebox_shim_linux/src/wait.rs | 2 + 9 files changed, 437 insertions(+), 25 deletions(-) create mode 100644 litebox_runner_linux_userland/tests/getitimer.c create mode 100644 litebox_runner_linux_userland/tests/setitimer.c diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 0dd500190a..5a00808479 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -940,7 +940,7 @@ pub struct TimeVal { tv_usec: suseconds_t, } #[repr(C)] -#[derive(Clone, FromBytes, IntoBytes)] +#[derive(Clone, Default, FromBytes, IntoBytes, Immutable)] pub struct ItimerVal { /// Timer interval interval: TimeVal, @@ -948,6 +948,25 @@ pub struct ItimerVal { value: TimeVal, } +impl ItimerVal { + pub fn new(interval: TimeVal, value: TimeVal) -> Self { + Self { interval, value } + } + + /// `it_value = duration`, `it_interval = 0` (single-shot timer). + pub fn single_shot(duration: Duration) -> Self { + Self::new(TimeVal::from(Duration::ZERO), TimeVal::from(duration)) + } + + pub fn it_interval(&self) -> TimeVal { + self.interval + } + + pub fn it_value(&self) -> TimeVal { + self.value + } +} + impl TryFrom for Duration { type Error = errno::Errno; @@ -2351,9 +2370,13 @@ pub enum SyscallRequest { Pause, SetITimer { which: IntervalTimer, - new_value: Platform::RawConstPointer, + new_value: Option>, old_value: Option>, }, + GetITimer { + which: IntervalTimer, + curr_value: Platform::RawMutPointer, + }, Statx { dirfd: i32, pathname: Option>, @@ -2828,6 +2851,7 @@ impl SyscallRequest { Sysno::alarm => sys_req!(Alarm { seconds }), Sysno::pause => SyscallRequest::Pause, Sysno::setitimer => sys_req!(SetITimer { which:?, new_value:*, old_value:* }), + Sysno::getitimer => sys_req!(GetITimer { which:?, curr_value:* }), Sysno::statx => sys_req!(Statx { dirfd, pathname:*, diff --git a/litebox_runner_linux_userland/tests/getitimer.c b/litebox_runner_linux_userland/tests/getitimer.c new file mode 100644 index 0000000000..6631468d0e --- /dev/null +++ b/litebox_runner_linux_userland/tests/getitimer.c @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Tests: getitimer happy path + cross-syscall observation (alarm sets the +// ITIMER_REAL state that getitimer reads back) and error branches. +// Goes through syscall(SYS_getitimer, ...) to exercise the raw kernel surface +// that LiteBox intercepts rather than the libc wrapper. + +#include "helpers.h" + +static int raw_getitimer(int which, struct itimerval *curr_value) { + return (int)syscall(SYS_getitimer, which, curr_value); +} + +static void expect_unarmed_timer(int which, const char *op) { + struct itimerval iv; + + memset(&iv, 0xff, sizeof(iv)); + TEST_ASSERT(raw_getitimer(which, &iv) == 0, op); + TEST_ASSERT(iv.it_value.tv_sec == 0 && iv.it_value.tv_usec == 0, + "it_value zero when timer is unarmed"); + TEST_ASSERT(iv.it_interval.tv_sec == 0 && iv.it_interval.tv_usec == 0, + "it_interval zero when timer is unarmed"); +} + +static void test_getitimer_no_timer_set(void) { + alarm(0); + expect_unarmed_timer(ITIMER_REAL, "getitimer(ITIMER_REAL) unarmed"); +} + +static void test_getitimer_after_alarm(void) { + // alarm(N) is documented as equivalent to setitimer(ITIMER_REAL, {0, N}, NULL). + // Setting alarm(10) and immediately reading should show ~10s remaining in + // it_value and zero interval. + alarm(0); + unsigned int prev = alarm(10); + TEST_ASSERT(prev == 0, "no prior alarm"); + + struct itimerval iv; + memset(&iv, 0xff, sizeof(iv)); + int rc = raw_getitimer(ITIMER_REAL, &iv); + TEST_ASSERT(rc == 0, "getitimer success after alarm"); + + long total_us = itimer_value_us(&iv); + TEST_ASSERT(total_us > 0 && total_us <= 10 * 1000000L, + "it_value in (0, 10s] after alarm(10)"); + TEST_ASSERT(iv.it_interval.tv_sec == 0 && iv.it_interval.tv_usec == 0, + "it_interval zero because alarm() never sets an interval"); + + alarm(0); +} + +static void test_getitimer_virtual_and_prof_zero(void) { + for (int which = ITIMER_VIRTUAL; which <= ITIMER_PROF; which++) { + expect_unarmed_timer(which, "getitimer ITIMER_VIRTUAL/PROF unarmed"); + } +} + +static void test_getitimer_einval(void) { + struct itimerval iv; + errno = 0; + int rc = raw_getitimer(99, &iv); + TEST_ASSERT(rc == -1 && errno == EINVAL, + "getitimer with bogus which -> EINVAL"); +} + +static void test_getitimer_efault(void) { + errno = 0; + int rc = raw_getitimer(ITIMER_REAL, NULL); + TEST_ASSERT(rc == -1 && errno == EFAULT, + "getitimer with NULL curr_value -> EFAULT"); +} + +int main(void) { + printf("getitimer tests starting...\n"); + test_getitimer_no_timer_set(); + test_getitimer_after_alarm(); + test_getitimer_virtual_and_prof_zero(); + test_getitimer_einval(); + test_getitimer_efault(); + printf("All getitimer tests passed.\n"); + return 0; +} diff --git a/litebox_runner_linux_userland/tests/helpers.h b/litebox_runner_linux_userland/tests/helpers.h index 6d185570ec..57f373cacc 100644 --- a/litebox_runner_linux_userland/tests/helpers.h +++ b/litebox_runner_linux_userland/tests/helpers.h @@ -28,6 +28,10 @@ } \ } while (0) +static inline long itimer_value_us(const struct itimerval *iv) { + return (long)iv->it_value.tv_sec * 1000000L + (long)iv->it_value.tv_usec; +} + static inline void die(const char *msg) { perror(msg); exit(1); diff --git a/litebox_runner_linux_userland/tests/setitimer.c b/litebox_runner_linux_userland/tests/setitimer.c new file mode 100644 index 0000000000..0f4bdf65ad --- /dev/null +++ b/litebox_runner_linux_userland/tests/setitimer.c @@ -0,0 +1,217 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Tests: setitimer(ITIMER_REAL, ...) — arm, disarm, old_value handoff, +// EINVAL on out-of-range tv_usec, EINVAL on bad `which`, EFAULT on NULL +// new_value. State changes are observed via getitimer() so the cross-syscall +// contract is exercised. Uses syscall(SYS_setitimer, ...) directly to hit the +// raw kernel surface that LiteBox intercepts. + +#include "helpers.h" + +#include + +static int raw_setitimer(int which, const struct itimerval *new_value, + struct itimerval *old_value) { + return (int)syscall(SYS_setitimer, which, new_value, old_value); +} + +static int raw_getitimer(int which, struct itimerval *curr_value) { + return (int)syscall(SYS_getitimer, which, curr_value); +} + +static void clear_alarm(void) { + struct itimerval zero = {{0, 0}, {0, 0}}; + (void)raw_setitimer(ITIMER_REAL, &zero, NULL); +} + +static void test_arm_single_shot(void) { + // Arm with it_value={10,0}, it_interval=0. + clear_alarm(); + struct itimerval nv = {{0, 0}, {10, 0}}; + int rc = raw_setitimer(ITIMER_REAL, &nv, NULL); + TEST_ASSERT(rc == 0, "setitimer arm success"); + + struct itimerval gv; + memset(&gv, 0, sizeof(gv)); + TEST_ASSERT(raw_getitimer(ITIMER_REAL, &gv) == 0, "getitimer after arm"); + long total_us = itimer_value_us(&gv); + TEST_ASSERT(total_us > 0 && total_us <= 10 * 1000000L, + "it_value in (0, 10s]"); + TEST_ASSERT(gv.it_interval.tv_sec == 0 && gv.it_interval.tv_usec == 0, + "it_interval zero (single-shot)"); + clear_alarm(); +} + +static void test_disarm(void) { + // Arm first, then disarm with all-zero new_value. + struct itimerval nv = {{0, 0}, {10, 0}}; + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &nv, NULL) == 0, "arm precondition"); + + struct itimerval zero = {{0, 0}, {0, 0}}; + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &zero, NULL) == 0, "disarm success"); + + struct itimerval gv; + memset(&gv, 0xff, sizeof(gv)); + TEST_ASSERT(raw_getitimer(ITIMER_REAL, &gv) == 0, "getitimer after disarm"); + TEST_ASSERT(gv.it_value.tv_sec == 0 && gv.it_value.tv_usec == 0, + "it_value zero after disarm"); + TEST_ASSERT(gv.it_interval.tv_sec == 0 && gv.it_interval.tv_usec == 0, + "it_interval zero after disarm"); +} + +static void test_disarm_ignores_interval(void) { + struct itimerval nv = {{0, 0}, {10, 0}}; + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &nv, NULL) == 0, "arm precondition"); + + struct itimerval disarm = {{1, 0}, {0, 0}}; + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &disarm, NULL) == 0, + "disarm with nonzero interval success"); + + struct itimerval gv; + memset(&gv, 0xff, sizeof(gv)); + TEST_ASSERT(raw_getitimer(ITIMER_REAL, &gv) == 0, + "getitimer after interval-only disarm"); + TEST_ASSERT(gv.it_value.tv_sec == 0 && gv.it_value.tv_usec == 0, + "it_value zero after interval-only disarm"); + TEST_ASSERT(gv.it_interval.tv_sec == 0 && gv.it_interval.tv_usec == 0, + "it_interval zero after interval-only disarm"); +} + +static void test_old_value_returns_previous(void) { + // Set a 10s timer, then replace with a 5s timer and capture old_value. + clear_alarm(); + struct itimerval first = {{0, 0}, {10, 0}}; + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &first, NULL) == 0, "first arm"); + + struct itimerval second = {{0, 0}, {5, 0}}; + struct itimerval old; + memset(&old, 0xff, sizeof(old)); + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &second, &old) == 0, "replace returns old"); + + long old_us = itimer_value_us(&old); + TEST_ASSERT(old_us > 0 && old_us <= 10 * 1000000L, + "old it_value reflects first arm (<= 10s)"); + TEST_ASSERT(old.it_interval.tv_sec == 0 && old.it_interval.tv_usec == 0, + "old it_interval zero"); + + // Replacement should be active now. + struct itimerval gv; + TEST_ASSERT(raw_getitimer(ITIMER_REAL, &gv) == 0, "getitimer after replace"); + long now_us = itimer_value_us(&gv); + TEST_ASSERT(now_us > 0 && now_us <= 5 * 1000000L, + "current it_value reflects replacement (<= 5s)"); + clear_alarm(); +} + +static void test_old_value_unarmed_returns_zero(void) { + clear_alarm(); + struct itimerval nv = {{0, 0}, {3, 0}}; + struct itimerval old; + memset(&old, 0xff, sizeof(old)); + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &nv, &old) == 0, "arm with old_value"); + TEST_ASSERT(old.it_value.tv_sec == 0 && old.it_value.tv_usec == 0, + "old it_value zero when previously unarmed"); + TEST_ASSERT(old.it_interval.tv_sec == 0 && old.it_interval.tv_usec == 0, + "old it_interval zero when previously unarmed"); + clear_alarm(); +} + +static void test_einval_usec_out_of_range(void) { + struct itimerval nv = {{0, 0}, {1, 1000000}}; + errno = 0; + int rc = raw_setitimer(ITIMER_REAL, &nv, NULL); + TEST_ASSERT(rc == -1 && errno == EINVAL, + "tv_usec >= 1000000 in it_value → EINVAL"); + + nv = (struct itimerval){{0, 1000000}, {1, 0}}; + errno = 0; + rc = raw_setitimer(ITIMER_REAL, &nv, NULL); + TEST_ASSERT(rc == -1 && errno == EINVAL, + "tv_usec >= 1000000 in it_interval → EINVAL"); +} + +static void test_einval_bad_which(void) { + struct itimerval nv = {{0, 0}, {1, 0}}; + errno = 0; + int rc = raw_setitimer(99, &nv, NULL); + TEST_ASSERT(rc == -1 && errno == EINVAL, "bad which → EINVAL"); +} + +static void test_efault_bad_old_value(void) { + // Bad old_value pointer with a valid new_value: Linux returns EFAULT but + // the timer state IS still mutated (kernel arms before writing old_value). + // Verified by host probe on kernel 6.6: rc=-1, errno=EFAULT, post-state + // shows the requested it_value ~5s remaining. + clear_alarm(); + struct itimerval nv = {{0, 0}, {5, 0}}; + errno = 0; + int rc = raw_setitimer(ITIMER_REAL, &nv, + (struct itimerval *)(uintptr_t)0x1); + TEST_ASSERT(rc == -1 && errno == EFAULT, "bad old_value → EFAULT"); + + struct itimerval gv; + TEST_ASSERT(raw_getitimer(ITIMER_REAL, &gv) == 0, "getitimer after EFAULT"); + long us = itimer_value_us(&gv); + TEST_ASSERT(us > 0 && us <= 5 * 1000000L, + "timer was armed before EFAULT write (state mutated, Linux quirk)"); + clear_alarm(); +} + +static void test_alarm_setitimer_share_state(void) { + // alarm(2) and setitimer(ITIMER_REAL, ...) share the same per-process + // timer; calls to one must be observable through the other. Verified by + // host probe on kernel 6.6 (setitimer(7s) → alarm(0) returns 7; + // alarm(10) → setitimer(&old) reports old.it_value ≈ 10s). + clear_alarm(); + + // Direction 1: setitimer arms; alarm(0) cancels and returns the previous + // remaining (rounded up to whole seconds). + struct itimerval nv = {{0, 0}, {7, 0}}; + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &nv, NULL) == 0, "setitimer arm"); + unsigned int via_alarm = alarm(0); + TEST_ASSERT(via_alarm > 0 && via_alarm <= 7, + "alarm(0) returns setitimer's remaining seconds, rounded up"); + + // Direction 2: alarm arms; setitimer(&old) reports the previous remaining + // in old.it_value with it_interval == 0. + clear_alarm(); + TEST_ASSERT(alarm(10) == 0, "alarm(10) on cleared state returns 0"); + struct itimerval nv2 = {{0, 0}, {3, 0}}; + struct itimerval old; + memset(&old, 0xff, sizeof(old)); + TEST_ASSERT(raw_setitimer(ITIMER_REAL, &nv2, &old) == 0, + "setitimer over alarm-armed state"); + long us = itimer_value_us(&old); + TEST_ASSERT(us > 0 && us <= 10 * 1000000L, + "old.it_value reflects prior alarm(10)"); + TEST_ASSERT(old.it_interval.tv_sec == 0 && old.it_interval.tv_usec == 0, + "old.it_interval zero (alarm() never sets an interval)"); + clear_alarm(); +} + +static void test_efault_null_new_value(void) { + errno = 0; + int rc = raw_setitimer(ITIMER_REAL, NULL, NULL); + // Linux: setitimer(which, NULL, NULL) is treated as "disarm" (per man page, + // "this is treated as being equivalent to a call in which the new_value + // fields are zero"). So no error. Verify our test asserts what Linux does. + TEST_ASSERT(rc == 0, "setitimer with NULL new_value treated as disarm (Linux quirk)"); +} + +int main(void) { + printf("setitimer tests starting...\n"); + test_arm_single_shot(); + test_disarm(); + test_disarm_ignores_interval(); + test_old_value_returns_previous(); + test_old_value_unarmed_returns_zero(); + test_einval_usec_out_of_range(); + test_einval_bad_which(); + test_efault_bad_old_value(); + test_efault_null_new_value(); + test_alarm_setitimer_share_state(); + clear_alarm(); + printf("All setitimer tests passed.\n"); + return 0; +} diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index 603a8edb10..8d6e231542 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -25,6 +25,7 @@ default = ["platform_linux_userland"] platform_linux_userland = ["litebox_platform_multiplex/platform_linux_userland_with_linux_syscall"] platform_windows_userland = ["litebox_platform_multiplex/platform_windows_userland"] platform_linux_snp = ["litebox_platform_multiplex/platform_linux_snp"] +alarm_fallback = [] [dev-dependencies] spin = { version = "0.9.8", default-features = false, features = ["spin_mutex"] } diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 8450bf0740..2fa2f21fe9 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -1021,6 +1021,14 @@ impl Task { SyscallRequest::Sigaltstack { ss, old_ss } => self.sys_sigaltstack(ss, old_ss, ctx), SyscallRequest::Alarm { seconds } => syscall!(sys_alarm(seconds)), SyscallRequest::Pause => syscall!(sys_pause()), + SyscallRequest::GetITimer { which, curr_value } => { + syscall!(sys_getitimer(which, curr_value)) + } + SyscallRequest::SetITimer { + which, + new_value, + old_value, + } => syscall!(sys_setitimer(which, new_value, old_value)), _ => { log_unsupported!("{request:?}"); Err(Errno::ENOSYS) diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index febb7f4544..fddd4aa597 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -25,7 +25,8 @@ use litebox::platform::{RawMutPointer as _, TimerHandle, TimerProvider}; use litebox::sync::Mutex; use litebox::utils::TruncateExt as _; use litebox_common_linux::{ - ArchPrctlArg, CloneFlags, FutexArgs, PrctlArg, TimeParam, errno::Errno, + ArchPrctlArg, CloneFlags, FutexArgs, IntervalTimer, ItimerVal, PrctlArg, TimeParam, + errno::Errno, }; use litebox_platform_multiplex::Platform; @@ -136,6 +137,20 @@ pub(crate) struct Alarm { pub(crate) deadline: Option<::Instant>, } +impl Alarm { + /// Returns the time remaining until [`Self::deadline`], or zero if the + /// alarm is not armed or its deadline has already passed. + pub(crate) fn remaining( + &self, + now: ::Instant, + ) -> Duration { + self.deadline + .as_ref() + .and_then(|d| d.checked_duration_since(&now)) + .unwrap_or(Duration::ZERO) + } +} + /// The locked portion of the process state. struct ProcessInner { /// If true, the whole process is exiting. @@ -1129,25 +1144,23 @@ impl Task { /// /// The alarm is per-process: all threads share the same alarm timer. pub(crate) fn sys_alarm(&self, seconds: u32) -> Result { + let prev = self.arm_real_timer(Duration::from_secs(u64::from(seconds)))?; + // Round remaining time up to whole seconds, saturating to u32::MAX. + if prev.is_zero() { + Ok(0) + } else { + let extra = u64::from(prev.subsec_nanos() > 0); + Ok(u32::try_from(prev.as_secs() + extra).unwrap_or(u32::MAX)) + } + } + + /// Arm or disarm the per-process `ITIMER_REAL` timer. Returns the raw + /// `Duration` remaining on the previous arming; zero means "was not + /// armed". `delay = 0` disarms. + fn arm_real_timer(&self, delay: Duration) -> Result { let mut alarm = self.process().alarm_timer.lock(); let now = self.global.platform.now(); - // Get remaining seconds from any previous alarm (rounded up to second). - let remaining = match alarm.deadline { - Some(deadline) => { - match deadline.checked_duration_since(&now) { - Some(dur) if !dur.is_zero() => { - let secs = dur.as_secs(); - let extra = u64::from(dur.subsec_nanos() > 0); - // Saturate to u32::MAX to avoid truncation. - u32::try_from(secs + extra).unwrap_or(u32::MAX) - } - _ => 0, // Deadline already passed or is now. - } - } - None => 0, - }; - - let delay = Duration::from_secs(u64::from(seconds)); + let prev = alarm.remaining(now); let new_deadline = if delay.is_zero() { None } else { @@ -1159,9 +1172,7 @@ impl Task { .platform .create_timer(litebox_common_linux::signal::Signal::SIGALRM) { - Ok(handle) => { - alarm.handle = Some(handle); - } + Ok(handle) => alarm.handle = Some(handle), Err(litebox::platform::TimerCreationError::Unsupported) => {} Err(_) => unimplemented!(), } @@ -1170,8 +1181,69 @@ impl Task { handle.set_timer(delay); } alarm.deadline = new_deadline; + Ok(prev) + } - Ok(remaining) + /// Handle syscall `setitimer`. + pub(crate) fn sys_setitimer( + &self, + which: IntervalTimer, + new_value: Option>, + old_value: Option>, + ) -> Result<(), Errno> { + let new = match new_value { + Some(ptr) => ptr.read_at_offset(0).ok_or(Errno::EFAULT)?, + // Linux supports NULL `new_value` but says it would be removed in the future. + None => ItimerVal::default(), + }; + // tv_usec range check is performed by `Duration::try_from(TimeVal)`. + let new_interval = Duration::try_from(new.it_interval())?; + let new_remaining = Duration::try_from(new.it_value())?; + + let prev = match which { + IntervalTimer::Real => { + if new_remaining.is_zero() { + ItimerVal::single_shot(self.arm_real_timer(Duration::ZERO)?) + } else if !new_interval.is_zero() { + // TODO: support periodic timers + log_unsupported!("setitimer: nonzero it_interval not supported"); + return Err(Errno::ENOSYS); + } else { + ItimerVal::single_shot(self.arm_real_timer(new_remaining)?) + } + } + IntervalTimer::Virtual | IntervalTimer::Prof => { + log_unsupported!("setitimer: ITIMER_VIRTUAL/PROF not supported"); + return Err(Errno::ENOSYS); + } + }; + + if let Some(out) = old_value { + out.write_at_offset(0, prev).ok_or(Errno::EFAULT)?; + } + Ok(()) + } + + /// Handle syscall `getitimer`. + pub(crate) fn sys_getitimer( + &self, + which: IntervalTimer, + curr_value: MutPtr, + ) -> Result<(), Errno> { + let value = match which { + IntervalTimer::Real => { + let alarm = self.process().alarm_timer.lock(); + let now = self.global.platform.now(); + alarm.remaining(now) + } + IntervalTimer::Virtual | IntervalTimer::Prof => { + log_unsupported!("getitimer: ITIMER_VIRTUAL/PROF not supported"); + Duration::ZERO + } + }; + curr_value + .write_at_offset(0, ItimerVal::single_shot(value)) + .ok_or(Errno::EFAULT) } /// Handle syscall `pause`. diff --git a/litebox_shim_linux/src/syscalls/signal/mod.rs b/litebox_shim_linux/src/syscalls/signal/mod.rs index af5764700c..e61e74d6dd 100644 --- a/litebox_shim_linux/src/syscalls/signal/mod.rs +++ b/litebox_shim_linux/src/syscalls/signal/mod.rs @@ -613,6 +613,8 @@ impl Task { /// enqueue `SIGALRM`. /// /// Note this is a fallback in case the platform does not support timers. + #[cfg(feature = "alarm_fallback")] + #[inline] pub(crate) fn check_alarm_deadline(&self) { use litebox::platform::TimeProvider as _; let mut alarm = self.process().alarm_timer.lock(); @@ -621,7 +623,6 @@ impl Task { // to check the deadline here. return; } - if alarm .deadline .is_some_and(|deadline| self.global.platform.now() >= deadline) diff --git a/litebox_shim_linux/src/wait.rs b/litebox_shim_linux/src/wait.rs index 7ab43cee62..1335b4337d 100644 --- a/litebox_shim_linux/src/wait.rs +++ b/litebox_shim_linux/src/wait.rs @@ -41,6 +41,7 @@ impl Task { self.global.platform.take_pending_signals(|signal| { self.queue_signals(signal); }); + #[cfg(feature = "alarm_fallback")] self.check_alarm_deadline(); self.process_signals(ctx); !self.is_exiting() @@ -54,6 +55,7 @@ impl litebox::event::wait::CheckForInterrupt for Task { self.global.platform.take_pending_signals(|sig| { self.queue_signals(sig); }); + #[cfg(feature = "alarm_fallback")] self.check_alarm_deadline(); self.is_exiting() || self.has_pending_signals() } From 36cd53b0f47b345715b78dac6c3365e1c902d235 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 13:32:48 -0700 Subject: [PATCH 011/319] Cherry-pick pipe refactor to ulitebox (#881) Cherry-picks c829e8d2f593376ea4e040b66cd6936c8549abf2 from main onto ulitebox. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wdcui <16925241+wdcui@users.noreply.github.com> --- litebox_shim_linux/src/lib.rs | 4 - litebox_shim_linux/src/syscalls/epoll.rs | 2 +- litebox_shim_linux/src/syscalls/file.rs | 99 +++----------- litebox_shim_linux/src/syscalls/mod.rs | 1 + litebox_shim_linux/src/syscalls/pipe.rs | 164 +++++++++++++++++++++++ 5 files changed, 181 insertions(+), 89 deletions(-) create mode 100644 litebox_shim_linux/src/syscalls/pipe.rs diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 2fa2f21fe9..e20945ca7d 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -342,10 +342,6 @@ fn default_fs( #[derive(Clone)] pub(crate) struct StdioStatusFlags(litebox::fs::OFlags); -/// Status flags for pipes -#[derive(Clone)] -pub(crate) struct PipeStatusFlags(pub litebox::fs::OFlags); - impl syscalls::file::FilesState { fn initialize_stdio_in_shared_descriptors_table(&self, global: &GlobalState) { use litebox::fs::{Mode, OFlags}; diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index f7356af998..3c9d30077c 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -143,7 +143,7 @@ impl EpollDescriptor { }; Some(poll(&proxy)) } - EpollDescriptor::Pipe(fd) => global.pipes.with_iopollable(fd, poll).ok(), + EpollDescriptor::Pipe(fd) => global.with_linux_pipe_iopollable(fd, poll).ok(), EpollDescriptor::Unix(fd) => { let handle = global.litebox.descriptor_table().entry_handle(fd)?; Some(handle.with_entry(|entry| poll(entry))) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index e024597d98..1ab1b2505f 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -391,9 +391,7 @@ impl Task { |fd| { espipe_for_non_seekable_offset(offset)?; self.global - .pipes - .read(&self.wait_cx(), fd, &mut buf.borrow_mut()) - .map_err(Errno::from) + .read_linux_pipe(&self.wait_cx(), fd, &mut buf.borrow_mut()) }, |fd| { let handle = self @@ -464,10 +462,7 @@ impl Task { }, |fd| { espipe_for_non_seekable_offset(offset)?; - self.global - .pipes - .write(&self.wait_cx(), fd, buf) - .map_err(Errno::from) + self.global.write_linux_pipe(&self.wait_cx(), fd, buf) }, |fd| { let handle = self @@ -675,7 +670,7 @@ impl Task { files.fs.close(&fd).map_err(Errno::from) } ConsumedFd::Network(fd) => self.global.close_socket(&self.wait_cx(), fd), - ConsumedFd::Pipes(fd) => self.global.pipes.close(&fd).map_err(Errno::from), + ConsumedFd::Pipes(fd) => self.global.close_linux_pipe(&fd), ConsumedFd::Eventfd(fd) => { let entry = { let mut dt = self.global.litebox.descriptor_table_mut(); @@ -966,14 +961,10 @@ where }, |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { - let half_pipe_type = task.global.pipes.half_pipe_type(fd)?; - let read_write_mode = match half_pipe_type { - litebox::pipes::HalfPipeType::SenderHalf => Mode::WUSR, - litebox::pipes::HalfPipeType::ReceiverHalf => Mode::RUSR, - }; - let pipe_mode = - read_write_mode.bits() | litebox_common_linux::InodeType::NamedPipe as u32; - Ok(T::from(synthetic(pipe_mode, 4096))) + Ok(T::from(synthetic( + task.global.linux_pipe_mode_bits(fd)?, + 4096, + ))) }, |_fd| Ok(T::from(synthetic(rw_user_mode, 4096))), |_fd| Ok(T::from(synthetic(rw_user_mode, 0))), @@ -1207,7 +1198,7 @@ impl Task { desc, |fd| getfl_from_metadata!(fd, crate::StdioStatusFlags), |fd| getfl_from_metadata!(fd, crate::syscalls::net::SocketOFlags), - |fd| getfl_from_metadata!(fd, crate::PipeStatusFlags), + |fd| self.global.linux_pipe_status_flags(fd), |fd| getfl_from_handle!(fd), |fd| getfl_from_handle!(fd), |fd| getfl_from_handle!(fd), @@ -1281,22 +1272,8 @@ impl Task { ) }, |fd| { - // Update the actual pipe non-blocking behavior self.global - .pipes - .update_flags( - fd, - litebox::pipes::Flags::NON_BLOCKING, - flags.intersects(OFlags::NONBLOCK), - ) - .map_err(Errno::from)?; - // Record all status flags in metadata for F_GETFL - setfl_in_metadata!( - fd, - crate::PipeStatusFlags, - unreachable!("all pipes have PipeStatusFlags when created"), - |_| {} - ) + .set_linux_pipe_status_flags(fd, flags, setfl_mask) }, |fd| { toggle_flags!(fd); @@ -1436,70 +1413,24 @@ impl Task { } } -const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; - impl Task { /// Handle syscall `pipe2` pub fn sys_pipe2(&self, flags: OFlags) -> Result<(u32, u32), Errno> { - let (pipe_flags, cloexec) = { - use litebox::pipes::Flags; - let mut f = Flags::empty(); - if flags.intersects((OFlags::CLOEXEC | OFlags::NONBLOCK | OFlags::DIRECT).complement()) - { - return Err(Errno::EINVAL); - } - f.set(Flags::NON_BLOCKING, flags.contains(OFlags::NONBLOCK)); - if flags.contains(OFlags::DIRECT) { - todo!("O_DIRECT not supported"); - } - (f, flags.contains(OFlags::CLOEXEC)) - }; - - let (writer, reader) = self.global.pipes.create_pipe( - DEFAULT_PIPE_BUF_SIZE, - pipe_flags, - // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. - core::num::NonZero::new(4096), - ); - - { - let initial_status = OFlags::from(pipe_flags); - let mut dt = self.global.litebox.descriptor_table_mut(); - let old = dt.set_entry_metadata( - &writer, - crate::PipeStatusFlags(initial_status | OFlags::WRONLY), - ); - assert!(old.is_none()); - let old = dt.set_entry_metadata( - &reader, - crate::PipeStatusFlags(initial_status | OFlags::RDONLY), - ); - assert!(old.is_none()); - } - - if cloexec { - let mut dt = self.global.litebox.descriptor_table_mut(); - let None = dt.set_fd_metadata(&writer, FileDescriptorFlags::FD_CLOEXEC) else { - unreachable!() - }; - let None = dt.set_fd_metadata(&reader, FileDescriptorFlags::FD_CLOEXEC) else { - unreachable!() - }; - } + let pipe = self.global.create_linux_pipe(flags)?; let files = self.files.borrow(); - let wr_raw_fd = files.insert_raw_fd(writer).map_err(|writer| { - self.global.pipes.close(&writer).unwrap(); + let wr_raw_fd = files.insert_raw_fd(pipe.writer).map_err(|writer| { + self.global.close_linux_pipe(&writer).unwrap(); Errno::EMFILE })?; - let rd_raw_fd = files.insert_raw_fd(reader).map_err(|reader| { + let rd_raw_fd = files.insert_raw_fd(pipe.reader).map_err(|reader| { let writer = files .raw_descriptor_store .write() .fd_consume_raw_integer(wr_raw_fd) .unwrap(); - self.global.pipes.close(&writer).unwrap(); - self.global.pipes.close(&reader).unwrap(); + self.global.close_linux_pipe(&writer).unwrap(); + self.global.close_linux_pipe(&reader).unwrap(); Errno::EMFILE })?; Ok((rd_raw_fd.try_into().unwrap(), wr_raw_fd.try_into().unwrap())) diff --git a/litebox_shim_linux/src/syscalls/mod.rs b/litebox_shim_linux/src/syscalls/mod.rs index a138e2160f..b5fa1a82ff 100644 --- a/litebox_shim_linux/src/syscalls/mod.rs +++ b/litebox_shim_linux/src/syscalls/mod.rs @@ -9,6 +9,7 @@ pub mod file; pub(crate) mod misc; pub(crate) mod mm; pub(crate) mod net; +pub(crate) mod pipe; pub mod process; pub(crate) mod unix; diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs new file mode 100644 index 0000000000..90273c164d --- /dev/null +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Linux ABI glue for the generic LiteBox pipe subsystem. +//! +//! `litebox::pipes` owns the in-process pipe buffer, endpoint, and readiness +//! mechanics. This module owns Linux-specific presentation: `pipe2` flags, +//! raw-fd metadata, `fcntl` status flags, and errno mapping. + +use core::num::NonZero; + +use litebox::{ + event::{IOPollable, wait::WaitContext}, + fd::MetadataError, + fs::{Mode, OFlags}, + pipes::{Flags, HalfPipeType, PipeFd}, +}; +use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; + +use crate::{GlobalState, Platform, ShimFS}; + +const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; + +/// Status flags for Linux pipe file descriptions. +/// +/// Access mode and Linux status flags are shim ABI state. The generic pipe +/// backend only needs the subset that affects pipe behavior, such as +/// nonblocking mode. +#[derive(Clone)] +pub(crate) struct PipeStatusFlags(OFlags); + +/// Both ends of a freshly created Linux pipe. +/// +/// `PipeFd` does not release the pipe on `Drop`; ends must either be inserted +/// into the fd table or explicitly released via [`GlobalState::close_linux_pipe`]. +pub(crate) struct LinuxPipeEnds { + pub(crate) reader: PipeFd, + pub(crate) writer: PipeFd, +} + +impl GlobalState { + pub(crate) fn create_linux_pipe(&self, flags: OFlags) -> Result { + let (pipe_flags, cloexec) = { + let mut pipe_flags = Flags::empty(); + if flags.intersects((OFlags::CLOEXEC | OFlags::NONBLOCK | OFlags::DIRECT).complement()) + { + return Err(Errno::EINVAL); + } + pipe_flags.set(Flags::NON_BLOCKING, flags.contains(OFlags::NONBLOCK)); + if flags.contains(OFlags::DIRECT) { + todo!("O_DIRECT not supported"); + } + (pipe_flags, flags.contains(OFlags::CLOEXEC)) + }; + + let (writer, reader) = self.pipes.create_pipe( + DEFAULT_PIPE_BUF_SIZE, + pipe_flags, + // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. + NonZero::new(4096), + ); + + let initial_status = OFlags::from(pipe_flags); + { + let mut dt = self.litebox.descriptor_table_mut(); + let old = + dt.set_entry_metadata(&writer, PipeStatusFlags(initial_status | OFlags::WRONLY)); + assert!(old.is_none()); + let old = + dt.set_entry_metadata(&reader, PipeStatusFlags(initial_status | OFlags::RDONLY)); + assert!(old.is_none()); + } + + if cloexec { + let mut dt = self.litebox.descriptor_table_mut(); + let None = dt.set_fd_metadata(&writer, FileDescriptorFlags::FD_CLOEXEC) else { + unreachable!() + }; + let None = dt.set_fd_metadata(&reader, FileDescriptorFlags::FD_CLOEXEC) else { + unreachable!() + }; + } + + Ok(LinuxPipeEnds { reader, writer }) + } + + pub(crate) fn close_linux_pipe(&self, fd: &PipeFd) -> Result<(), Errno> { + self.pipes.close(fd).map_err(Errno::from) + } + + pub(crate) fn read_linux_pipe( + &self, + cx: &WaitContext<'_, Platform>, + fd: &PipeFd, + buf: &mut [u8], + ) -> Result { + self.pipes.read(cx, fd, buf).map_err(Errno::from) + } + + pub(crate) fn write_linux_pipe( + &self, + cx: &WaitContext<'_, Platform>, + fd: &PipeFd, + buf: &[u8], + ) -> Result { + self.pipes.write(cx, fd, buf).map_err(Errno::from) + } + + pub(crate) fn linux_pipe_status_flags(&self, fd: &PipeFd) -> Result { + self.litebox + .descriptor_table() + .with_metadata(fd, |PipeStatusFlags(flags)| { + *flags & OFlags::STATUS_FLAGS_MASK + }) + .map_err(metadata_to_errno) + } + + pub(crate) fn set_linux_pipe_status_flags( + &self, + fd: &PipeFd, + flags: OFlags, + setfl_mask: OFlags, + ) -> Result<(), Errno> { + self.pipes + .update_flags(fd, Flags::NON_BLOCKING, flags.intersects(OFlags::NONBLOCK)) + .map_err(Errno::from)?; + + self.litebox + .descriptor_table_mut() + .with_metadata_mut(fd, |PipeStatusFlags(current)| { + let diff = (*current & setfl_mask) ^ flags; + if diff.intersects(OFlags::APPEND | OFlags::DIRECT | OFlags::NOATIME) { + log_unsupported!("unsupported flags"); + } + current.toggle(diff); + }) + .map_err(metadata_to_errno) + } + + pub(crate) fn linux_pipe_mode_bits(&self, fd: &PipeFd) -> Result { + let read_write_mode = match self.pipes.half_pipe_type(fd)? { + HalfPipeType::SenderHalf => Mode::WUSR, + HalfPipeType::ReceiverHalf => Mode::RUSR, + }; + Ok(read_write_mode.bits() | InodeType::NamedPipe as u32) + } + + pub(crate) fn with_linux_pipe_iopollable( + &self, + fd: &PipeFd, + f: impl FnOnce(&dyn IOPollable) -> R, + ) -> Result { + self.pipes.with_iopollable(fd, f).map_err(Errno::from) + } +} + +fn metadata_to_errno(err: MetadataError) -> Errno { + match err { + MetadataError::ClosedFd => Errno::EBADF, + MetadataError::NoSuchMetadata => { + unreachable!("Linux pipe descriptors always carry PipeStatusFlags") + } + } +} From eca82a58a51788a790881657c1217dcdd319d42e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 29 May 2026 15:32:28 -0700 Subject: [PATCH 012/319] Add syscall sendmmsg to ulitebox (#883) Cherry-picks `05acb097bf64210396b06e06655f02df39b9002f` from `main` onto `ulitebox`. --- litebox_common_linux/src/lib.rs | 56 +++-- .../tests/sendmmsg.c | 195 ++++++++++++++++++ litebox_shim_linux/src/lib.rs | 6 + litebox_shim_linux/src/syscalls/net.rs | 126 +++++++++-- 4 files changed, 343 insertions(+), 40 deletions(-) create mode 100644 litebox_runner_linux_userland/tests/sendmmsg.c diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 5a00808479..7f9f856702 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -327,22 +327,20 @@ pub type IoVec

= IoReadVec

; impl> Clone for IoWriteVec

{ fn clone(&self) -> Self { - Self { - iov_base: self.iov_base, - iov_len: self.iov_len, - } + *self } } +impl> Copy for IoWriteVec

{} + impl> Clone for IoReadVec

{ fn clone(&self) -> Self { - Self { - iov_base: self.iov_base, - iov_len: self.iov_len, - } + *self } } +impl> Copy for IoReadVec

{} + impl From for FileStat { fn from(value: litebox::fs::FileStatus) -> Self { // TODO: add more fields @@ -1869,22 +1867,31 @@ pub struct UserMsgHdr { impl Clone for UserMsgHdr { fn clone(&self) -> Self { - Self { - msg_name: self.msg_name, - msg_namelen: self.msg_namelen, - #[cfg(target_pointer_width = "64")] - _pad: 0, - msg_iov: self.msg_iov, - msg_iovlen: self.msg_iovlen, - msg_control: self.msg_control, - msg_controllen: self.msg_controllen, - msg_flags: self.msg_flags, - #[cfg(target_pointer_width = "64")] - _pad2: 0, - } + *self } } +impl Copy for UserMsgHdr {} + +/// Linux's `struct mmsghdr`: a `msghdr` paired with the number of bytes +/// transmitted, used by `sendmmsg`/`recvmmsg`. +#[derive(Debug, FromBytes, IntoBytes)] +#[repr(C, packed)] +pub struct UserMmsgHdr { + pub msg_hdr: UserMsgHdr, + pub msg_len: u32, + #[cfg(target_pointer_width = "64")] + _pad: u32, +} + +impl Clone for UserMmsgHdr { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for UserMmsgHdr {} + #[repr(i32)] #[derive(Debug, IntEnum)] pub enum SocketcallType { @@ -2114,6 +2121,12 @@ pub enum SyscallRequest { msg: Platform::RawConstPointer>, flags: SendFlags, }, + Sendmmsg { + sockfd: i32, + msgvec: Platform::RawMutPointer>, + vlen: u32, + flags: SendFlags, + }, Recvfrom { sockfd: i32, buf: Platform::RawMutPointer, @@ -2589,6 +2602,7 @@ impl SyscallRequest { Sysno::accept4 => sys_req!(Accept { sockfd, addr:*, addrlen:*, flags }), Sysno::sendto => sys_req!(Sendto { sockfd, buf:*, len, flags, addr:*, addrlen }), Sysno::sendmsg => sys_req!(Sendmsg { sockfd, msg:*, flags }), + Sysno::sendmmsg => sys_req!(Sendmmsg { sockfd, msgvec:*, vlen, flags }), Sysno::recvfrom => sys_req!(Recvfrom { sockfd, buf:*, len, flags, addr:*, addrlen:*, }), Sysno::recvmsg => sys_req!(Recvmsg { sockfd, msg:*, flags }), Sysno::shutdown => sys_req!(Shutdown { sockfd, how }), diff --git a/litebox_runner_linux_userland/tests/sendmmsg.c b/litebox_runner_linux_userland/tests/sendmmsg.c new file mode 100644 index 0000000000..2131f38880 --- /dev/null +++ b/litebox_runner_linux_userland/tests/sendmmsg.c @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "helpers.h" + +#include +#include + +// Use the raw syscall so we exercise exactly what LiteBox intercepts; glibc's +// wrapper would otherwise be free to massage arguments before reaching the +// kernel. +static long raw_sendmmsg(int fd, struct mmsghdr *msgvec, unsigned int vlen, + int flags) { + return syscall(SYS_sendmmsg, fd, msgvec, vlen, flags); +} + +static void test_three_messages(void) { + puts("Test 1: sendmmsg sends multiple datagrams and reports per-entry msg_len"); + + int sv[2]; + make_socket_pair(SOCK_DGRAM, sv); + + const char *payloads[3] = {"hello", "world!!", "third-msg"}; + struct iovec iov[3]; + struct mmsghdr hdrs[3]; + memset(hdrs, 0xAB, sizeof(hdrs)); + for (int i = 0; i < 3; i++) { + iov[i].iov_base = (void *)payloads[i]; + iov[i].iov_len = strlen(payloads[i]); + memset(&hdrs[i].msg_hdr, 0, sizeof(hdrs[i].msg_hdr)); + hdrs[i].msg_hdr.msg_iov = &iov[i]; + hdrs[i].msg_hdr.msg_iovlen = 1; + hdrs[i].msg_len = 0xDEADBEEF; + } + + errno = 0; + long n = raw_sendmmsg(sv[0], hdrs, 3, 0); + printf(" sendmmsg returned %ld (errno=%d %s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + TEST_ASSERT(n == 3, "sendmmsg returned 3"); + for (int i = 0; i < 3; i++) { + unsigned int got = hdrs[i].msg_len; + unsigned int want = (unsigned int)strlen(payloads[i]); + TEST_ASSERT(got == want, "msg_len matches payload length for each entry"); + } + + for (int i = 0; i < 3; i++) { + expect_recv_string(sv[1], payloads[i], "datagram arrives at peer with correct content"); + } + + close_pair(sv); +} + +static void test_zero_length_message(void) { + puts("Test 2: sendmmsg sends an entry with msg_iovlen == 0"); + + int sv[2]; + make_socket_pair(SOCK_DGRAM, sv); + + struct mmsghdr hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.msg_len = 0xDEADBEEF; + + errno = 0; + long n = raw_sendmmsg(sv[0], &hdr, 1, 0); + printf(" sendmmsg returned %ld (errno=%d %s), msg_len=%u\n", n, + errno, n < 0 ? strerror(errno) : "-", hdr.msg_len); + TEST_ASSERT(n == 1, "zero-length message counts as one sent message"); + TEST_ASSERT(hdr.msg_len == 0, "zero-length message reports msg_len == 0"); + + char buf[1] = {0x7f}; + errno = 0; + ssize_t r = recv(sv[1], buf, sizeof(buf), MSG_DONTWAIT); + printf(" recv returned %zd (errno=%d %s)\n", r, errno, + r < 0 ? strerror(errno) : "-"); + TEST_ASSERT(r == 0, "peer receives a zero-length datagram"); + + close_pair(sv); +} + +static void test_multi_iov_datagram(void) { + puts("Test 3: sendmmsg gathers multiple iovecs into one datagram"); + + int sv[2]; + make_socket_pair(SOCK_DGRAM, sv); + + const char first[] = "multi-"; + const char second[] = "iov"; + const char expected[] = "multi-iov"; + struct iovec iov[2] = { + {.iov_base = (void *)first, .iov_len = strlen(first)}, + {.iov_base = (void *)second, .iov_len = strlen(second)}, + }; + struct mmsghdr hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.msg_hdr.msg_iov = iov; + hdr.msg_hdr.msg_iovlen = 2; + hdr.msg_len = 0xDEADBEEF; + + errno = 0; + long n = raw_sendmmsg(sv[0], &hdr, 1, 0); + printf(" sendmmsg returned %ld (errno=%d %s), msg_len=%u\n", n, + errno, n < 0 ? strerror(errno) : "-", hdr.msg_len); + TEST_ASSERT(n == 1, "multi-iov datagram counts as one sent message"); + TEST_ASSERT(hdr.msg_len == strlen(expected), + "multi-iov datagram reports total payload length"); + + char buf[32] = {0}; + errno = 0; + ssize_t r = recv(sv[1], buf, sizeof(buf), MSG_DONTWAIT); + printf(" recv returned %zd (errno=%d %s), payload='%s'\n", r, errno, + r < 0 ? strerror(errno) : "-", r >= 0 ? buf : ""); + TEST_ASSERT(r == (ssize_t)strlen(expected) && strcmp(buf, expected) == 0, + "peer receives one gathered datagram"); + + close_pair(sv); +} + +static void test_vlen_zero(void) { + puts("Test 4: sendmmsg with vlen == 0 returns 0"); + + int sv[2]; + make_socket_pair(SOCK_DGRAM, sv); + + errno = 0; + long n = raw_sendmmsg(sv[0], NULL, 0, 0); + printf(" sendmmsg returned %ld (errno=%d %s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + TEST_ASSERT(n == 0, "vlen=0 returns 0"); + + close_pair(sv); +} + +static void test_errno_paths(void) { + puts("Test 5: sendmmsg errno on bad fd / bad msgvec pointer"); + + int sv[2]; + make_socket_pair(SOCK_DGRAM, sv); + + errno = 0; + long n = raw_sendmmsg(-1, NULL, 1, 0); + printf(" fd=-1 vlen=1: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + TEST_ASSERT(n == -1 && errno == EBADF, "bad fd returns EBADF"); + + errno = 0; + n = raw_sendmmsg(9999, NULL, 1, 0); + printf(" fd=9999 vlen=1: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + TEST_ASSERT(n == -1 && errno == EBADF, "unused fd returns EBADF"); + + errno = 0; + n = raw_sendmmsg(sv[0], NULL, 1, 0); + printf(" fd=ok msgvec=NULL vlen=1: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + TEST_ASSERT(n == -1 && errno == EFAULT, + "NULL msgvec with vlen>0 returns EFAULT"); + + close_pair(sv); +} + +static void test_first_message_fault(void) { + puts("Test 6: sendmmsg reports EFAULT when the first message faults"); + + int sv[2]; + make_socket_pair(SOCK_DGRAM, sv); + + struct iovec iov = {.iov_base = (void *)(uintptr_t)0x1, .iov_len = 16}; + struct mmsghdr hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.msg_hdr.msg_iov = &iov; + hdr.msg_hdr.msg_iovlen = 1; + + errno = 0; + long n = raw_sendmmsg(sv[0], &hdr, 1, 0); + printf(" sendmmsg returned %ld (errno=%d %s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + TEST_ASSERT(n == -1 && errno == EFAULT, + "first-message fault returns -1 with EFAULT"); + + close_pair(sv); +} + +int main(void) { + puts("sendmmsg parity test"); + test_three_messages(); + test_zero_length_message(); + test_multi_iov_datagram(); + test_vlen_zero(); + test_errno_paths(); + test_first_message_fault(); + + puts("\nAll sendmmsg tests passed."); + return 0; +} diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index e20945ca7d..1873ea359f 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -688,6 +688,12 @@ impl Task { addrlen, } => self.sys_sendto(sockfd, buf, len, flags, addr, addrlen), SyscallRequest::Sendmsg { sockfd, msg, flags } => self.sys_sendmsg(sockfd, msg, flags), + SyscallRequest::Sendmmsg { + sockfd, + msgvec, + vlen, + flags, + } => self.sys_sendmmsg(sockfd, msgvec, vlen, flags), SyscallRequest::Recvfrom { sockfd, buf, diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index 61d2cf3cae..a155261c5a 100644 --- a/litebox_shim_linux/src/syscalls/net.rs +++ b/litebox_shim_linux/src/syscalls/net.rs @@ -40,6 +40,10 @@ use crate::{ syscalls::unix::{CSockUnixAddr, UnixSocket, UnixSocketAddr}, }; +/// Linux's hard cap on the number of iovecs per `*msg`-style call, and on the +/// number of entries per `*mmsg`-style call. See `UIO_MAXIOV` in ``. +const UIO_MAXIOV: usize = 1024; + macro_rules! convert_flags { ($src:expr, $src_type:ty, $dst_type:ty, $($flag:ident),+ $(,)?) => { { @@ -754,6 +758,7 @@ impl GlobalState { let timeout = self.with_socket_options(fd, |opt| opt.send_timeout); let is_nonblock = self.get_status(fd).contains(OFlags::NONBLOCK) || flags.contains(SendFlags::DONTWAIT); + let is_empty_stream = buf.is_empty() && matches!(proxy.as_ref(), NetworkProxy::Stream(_)); cx.with_timeout(timeout) .wait_on_events( @@ -764,8 +769,10 @@ impl GlobalState { Ok(()) }, || match proxy.try_write(buf, new_flags, sockaddr) { + Ok(0) if buf.is_empty() => Ok(0), Ok(0) => Err(TryOpError::TryAgain), Ok(n) => Ok(n), + Err(litebox::net::errors::SendError::BufferFull) if is_empty_stream => Ok(0), Err(e) => Err(TryOpError::Other(Errno::from(e))), }, ) @@ -1158,6 +1165,36 @@ pub(crate) fn write_sockaddr_to_user( addrlen.write_at_offset(0, len).ok_or(Errno::EFAULT) } +fn copy_iovs_to_vec

( + iovs: &[litebox_common_linux::IoVec

], +) -> Result, Errno> +where + P: litebox::platform::RawMutPointer, +{ + let total_len = iovs.iter().try_fold(0usize, |total_len, iov| { + total_len.checked_add(iov.iov_len).ok_or(Errno::EINVAL) + })?; + let mut data = alloc::vec::Vec::new(); + data.try_reserve_exact(total_len) + .map_err(|_| Errno::ENOMEM)?; + data.resize(total_len, 0); + let mut offset = 0; + for iov in iovs { + if iov.iov_len == 0 { + continue; + } + let end = offset + iov.iov_len; + for (byte_offset, byte) in (0_isize..).zip(data[offset..end].iter_mut()) { + *byte = iov + .iov_base + .read_at_offset(byte_offset) + .ok_or(Errno::EFAULT)?; + } + offset = end; + } + Ok(data) +} + impl Task { /// Handle syscall `accept` pub(crate) fn sys_accept( @@ -1396,13 +1433,18 @@ impl Task { log_unsupported!("ancillary data is not supported"); return Err(Errno::EINVAL); } - if msg.msg_iovlen == 0 || msg.msg_iovlen > 1024 { - return Err(Errno::EINVAL); + if msg.msg_iovlen > UIO_MAXIOV { + return Err(Errno::EMSGSIZE); } - let iovs = msg - .msg_iov - .to_owned_slice(msg.msg_iovlen) - .ok_or(Errno::EFAULT)?; + let iovs = if msg.msg_iovlen == 0 { + None + } else { + Some( + msg.msg_iov + .to_owned_slice(msg.msg_iovlen) + .ok_or(Errno::EFAULT)?, + ) + }; let res = self.files.borrow().with_socket( &self.global, sockfd, @@ -1411,23 +1453,17 @@ impl Task { .clone() .map(|addr| addr.inet().ok_or(Errno::EAFNOSUPPORT)) .transpose()?; - super::file::write_to_iovec( - iovs.iter().map(|iov| (iov.iov_base, iov.iov_len)), - |buf| { - self.global - .sendto(&self.wait_cx(), fd, buf, flags, sock_addr) - }, - ) + let data = copy_iovs_to_vec(iovs.as_deref().unwrap_or_default())?; + self.global + .sendto(&self.wait_cx(), fd, &data, flags, sock_addr) }, |file| { let unix_addr = sock_addr .clone() .map(|addr| addr.unix().ok_or(Errno::EAFNOSUPPORT)) .transpose()?; - super::file::write_to_iovec( - iovs.iter().map(|iov| (iov.iov_base, iov.iov_len)), - |buf| file.sendto(self, buf, flags, unix_addr.clone()), - ) + let data = copy_iovs_to_vec(iovs.as_deref().unwrap_or_default())?; + file.sendto(self, &data, flags, unix_addr) }, ); if let Err(Errno::EPIPE) = res @@ -1438,6 +1474,58 @@ impl Task { res } + /// Handle syscall `sendmmsg` + pub(crate) fn sys_sendmmsg( + &self, + fd: i32, + msgvec: MutPtr>, + vlen: u32, + flags: SendFlags, + ) -> Result { + let Ok(sockfd) = u32::try_from(fd) else { + return Err(Errno::EBADF); + }; + + let vlen = (vlen as usize).min(UIO_MAXIOV); + + // Linux looks up the fd before touching vlen/msgvec, so a bogus fd + // takes priority over a bogus msgvec pointer or vlen == 0. + self.files.borrow().with_socket( + &self.global, + sockfd, + |_| Ok::<(), Errno>(()), + |_| Ok::<(), Errno>(()), + )?; + + if vlen == 0 { + return Ok(0); + } + + let stride = core::mem::size_of::>(); + let msg_len_off = + core::mem::offset_of!(litebox_common_linux::UserMmsgHdr, msg_len); + + let mut sent: usize = 0; + for i in 0..vlen { + let bail = |e: Errno| if sent > 0 { Ok(sent) } else { Err(e) }; + let Some(mmh) = msgvec.read_at_offset(isize::try_from(i).unwrap()) else { + return bail(Errno::EFAULT); + }; + let inner = mmh.msg_hdr; + let n = match self.do_sendmsg(sockfd, &inner, flags) { + Ok(n) => n, + Err(e) => return bail(e), + }; + let msg_len_ptr = + MutPtr::::from_usize(msgvec.as_usize() + i * stride + msg_len_off); + if msg_len_ptr.write_at_offset(0, n.trunc()).is_none() { + return bail(Errno::EFAULT); + } + sent += 1; + } + Ok(sent) + } + /// Handle syscall `recvfrom` pub(crate) fn sys_recvfrom( &self, @@ -1565,8 +1653,8 @@ impl Task { if msg_controllen != 0 { log_unsupported!("ancillary data is not supported"); } - if msg_iovlen > 1024 { - return Err(Errno::EINVAL); + if msg_iovlen > UIO_MAXIOV { + return Err(Errno::EMSGSIZE); } let iovs = msg_iov.to_owned_slice(msg_iovlen).ok_or(Errno::EFAULT)?; From 61b2fee6f65f94d797e9461320c138c67448aad7 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 29 May 2026 16:32:50 -0700 Subject: [PATCH 013/319] Support Windows registry (#884) This PR implements registry-backed handling for `NtOpenKey` and `NtQueryValueKey`. The registry backing store represents keys as directories and values as files under `.values`, allowing initialization of registry with a tar file. --- Cargo.lock | 1 + dev_tests/src/ratchet.rs | 1 + litebox_shim_windows/Cargo.toml | 1 + litebox_shim_windows/src/lib.rs | 68 +- litebox_shim_windows/src/nt_types.rs | 68 + litebox_shim_windows/src/syscalls/mod.rs | 43 +- litebox_shim_windows/src/syscalls/registry.rs | 1167 +++++++++++++++++ litebox_shim_windows/src/tests.rs | 48 + 8 files changed, 1376 insertions(+), 21 deletions(-) create mode 100644 litebox_shim_windows/src/nt_types.rs create mode 100644 litebox_shim_windows/src/syscalls/registry.rs create mode 100644 litebox_shim_windows/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 9a76dc3504..a307409060 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1780,6 +1780,7 @@ dependencies = [ name = "litebox_shim_windows" version = "0.1.0" dependencies = [ + "int-enum", "litebox", "litebox_common_linux", "litebox_common_windows", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index d5b76f0694..cd9d681397 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -44,6 +44,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_runner_snp/", 2), ("litebox_shim_linux/", 1), ("litebox_shim_optee/", 3), + ("litebox_shim_windows/", 1), ], |file| { Ok(file diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml index 0850ff4358..9f886c2b38 100644 --- a/litebox_shim_windows/Cargo.toml +++ b/litebox_shim_windows/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +int-enum = "1.2.0" litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } litebox_common_windows = { path = "../litebox_common_windows/", version = "0.1.0" } diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 9ba60c5205..306c8fc567 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -28,10 +28,17 @@ use litebox_platform_multiplex::Platform; use crate::syscalls::SyscallRequest; mod loader; +mod nt_types; mod syscalls; +#[cfg(test)] +mod tests; + const DEFAULT_PROCESS_EXIT_CODE: i32 = 1; +pub(crate) type ConstPtr = + ::RawConstPointer; +pub(crate) type MutPtr = ::RawMutPointer; pub(crate) type WindowsPageManager = PageManager; pub(crate) type WindowsHandleStore = litebox::sync::RwLock; @@ -77,7 +84,6 @@ where Some(()) } -#[expect(dead_code, reason = "handle helpers are staged for NT object syscalls")] pub(crate) fn insert_raw_handle( litebox: &LiteBox, handles: &WindowsHandleStore, @@ -96,7 +102,6 @@ pub(crate) fn insert_raw_handle( Ok(handle) } -#[expect(dead_code, reason = "handle helpers are staged for NT object syscalls")] pub(crate) fn raw_handle_entry( litebox: &LiteBox, handles: &WindowsHandleStore, @@ -110,7 +115,6 @@ pub(crate) fn raw_handle_entry( litebox.descriptor_table().entry_handle(&typed) } -#[expect(dead_code, reason = "handle helpers are staged for NT object syscalls")] pub(crate) fn remove_raw_handle( litebox: &LiteBox, handles: &WindowsHandleStore, @@ -167,6 +171,8 @@ impl WindowsShimBuilder { pub fn build(self) -> WindowsShim { let global = Arc::new(GlobalState { page_manager: PageManager::new(&self.litebox), + registry: syscalls::registry::RegistryStore::new(&self.litebox), + litebox: self.litebox, _fs: PhantomData, }); WindowsShim(global) @@ -189,11 +195,13 @@ impl WindowsShim { let load_info = loader::PeLoader::new(fs, &self.0.page_manager).load(path)?; let process = Arc::new(Process { ntdll_mapping: load_info.ntdll_mapping, + handles: WindowsHandleStore::new(litebox::fd::RawDescriptorStorage::new()), exit_code: AtomicI32::new(DEFAULT_PROCESS_EXIT_CODE), }); Ok(LoadedProgram { entrypoints: WindowsShimEntrypoints { task: Task { + global: self.0.clone(), process: process.clone(), entry_point: load_info.entry_point, stack_top: load_info.stack_top, @@ -209,12 +217,15 @@ impl WindowsShim { /// Global shim state shared by all Windows tasks loaded by this shim. struct GlobalState { page_manager: WindowsPageManager, + registry: syscalls::registry::RegistryStore, + litebox: LiteBox, _fs: PhantomData, } /// Per-process Windows state shared by every thread in the process. pub struct Process { ntdll_mapping: Option, + handles: WindowsHandleStore, exit_code: AtomicI32, } @@ -231,6 +242,7 @@ impl Process { } struct Task { + global: Arc>, process: Arc, entry_point: usize, stack_top: usize, @@ -275,19 +287,31 @@ impl Task { "Handling Windows" ); let (result, op) = match req { - SyscallRequest::NtTerminateProcess { - process_handle, - exit_status, + SyscallRequest::NtOpenKey { + key_handle, + desired_access, + object_attributes, } => { - if !process_handle.is_null() && !process_handle.is_current() { - // TODO: allow terminating other processes - litebox_util_log::error!("Terminating other processes is not yet supported"); - (NtStatus::INVALID_HANDLE, ContinueOperation::Resume) - } else { - // TODO: Terminate all threads except the calling one if process_handle is zero. - self.process.exit_code.store(exit_status, Ordering::Relaxed); - (NtStatus::SUCCESS, ContinueOperation::Terminate) - } + let status = self.sys_nt_open_key(key_handle, desired_access, object_attributes); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryValueKey { + key_handle, + value_name, + key_value_information_class, + key_value_information, + length, + result_length, + } => { + let status = self.sys_nt_query_value_key( + key_handle, + value_name, + key_value_information_class, + key_value_information, + length, + result_length, + ); + (status, ContinueOperation::Resume) } SyscallRequest::NtAllocateVirtualMemory { process_handle, @@ -309,6 +333,20 @@ impl Task { ); (NtStatus::UNSUCCESSFUL, ContinueOperation::Terminate) } + SyscallRequest::NtTerminateProcess { + process_handle, + exit_status, + } => { + if !process_handle.is_null() && !process_handle.is_current() { + // TODO: allow terminating other processes + litebox_util_log::error!("Terminating other processes is not yet supported"); + (NtStatus::INVALID_HANDLE, ContinueOperation::Resume) + } else { + // TODO: Terminate all threads except the calling one if process_handle is zero. + self.process.exit_code.store(exit_status, Ordering::Relaxed); + (NtStatus::SUCCESS, ContinueOperation::Terminate) + } + } }; ctx.rax = result.as_raw().cast_unsigned() as usize; diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs new file mode 100644 index 0000000000..25d569ea03 --- /dev/null +++ b/litebox_shim_windows/src/nt_types.rs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::string::String; +use litebox::platform::RawConstPointer as _; +use litebox_common_windows::nt_status::NtStatus; +use litebox_platform_multiplex::Platform; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::{ConstPtr, syscalls::Handle}; + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable)] +pub(crate) struct ObjectAttributes { + pub(crate) length: u32, + pub(crate) root_directory: Handle, + pub(crate) object_name: usize, + pub(crate) attributes: u32, + pub(crate) security_descriptor: usize, + pub(crate) security_quality_of_service: usize, +} + +pub(crate) fn read_object_attributes( + object_attributes: ConstPtr, +) -> Result { + let Some(object_attributes) = object_attributes.read_at_offset(0) else { + return Err(NtStatus::ACCESS_VIOLATION); + }; + if object_attributes.length as usize != size_of::() { + return Err(NtStatus::INVALID_PARAMETER); + } + Ok(object_attributes) +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub(crate) struct UnicodeString { + pub(crate) length: u16, + pub(crate) maximum_length: u16, + pub(crate) padding_0: [u8; 4], + pub(crate) buffer: usize, +} + +impl TryFrom for String { + type Error = NtStatus; + + fn try_from(unicode_string: UnicodeString) -> Result { + if !unicode_string.length.is_multiple_of(2) { + return Err(NtStatus::INVALID_PARAMETER); + } + if unicode_string.length == 0 { + return Ok(String::new()); + } + if unicode_string.buffer == 0 { + return Err(NtStatus::ACCESS_VIOLATION); + } + + let chars = usize::from(unicode_string.length / 2); + let buffer = + ::RawConstPointer::::from_usize( + unicode_string.buffer, + ); + let Some(units) = buffer.to_owned_slice(chars) else { + return Err(NtStatus::ACCESS_VIOLATION); + }; + Ok(String::from_utf16_lossy(&units)) + } +} diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 5cbfb1452b..accb011817 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -1,12 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +pub(crate) mod registry; + use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; use litebox_common_windows::NtSysno; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; +use crate::nt_types; + const FIRST_STACK_ARGUMENT_OFFSET: usize = 0x28; const HANDLE_SHIFT: u32 = 2; const HANDLE_TAG_MASK: usize = (1usize << HANDLE_SHIFT) - 1; @@ -78,11 +82,21 @@ impl ProcessHandle { } } +#[allow(clippy::enum_variant_names)] #[derive(Debug)] pub(crate) enum SyscallRequest { - NtTerminateProcess { - process_handle: ProcessHandle, - exit_status: i32, + NtOpenKey { + key_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, + NtQueryValueKey { + key_handle: Handle, + value_name: Platform::RawConstPointer, + key_value_information_class: u32, + key_value_information: Platform::RawMutPointer, + length: u32, + result_length: Platform::RawMutPointer, }, NtAllocateVirtualMemory { process_handle: ProcessHandle, @@ -92,6 +106,10 @@ pub(crate) enum SyscallRequest { allocation_type: u32, protect: u32, }, + NtTerminateProcess { + process_handle: ProcessHandle, + exit_status: i32, + }, } impl SyscallRequest { @@ -115,9 +133,18 @@ impl SyscallRequest { } match NtSysno::from_raw(pt_regs.orig_rax)? { - NtSysno::NtTerminateProcess => Some(sys_req!(NtTerminateProcess { - process_handle: { ProcessHandle::from_raw }, - exit_status, + NtSysno::NtOpenKey => Some(sys_req!(NtOpenKey { + key_handle:*, + desired_access, + object_attributes:*, + })), + NtSysno::NtQueryValueKey => Some(sys_req!(NtQueryValueKey { + key_handle:{Handle::from_raw}, + value_name:*, + key_value_information_class, + key_value_information:*, + length, + result_length:*, })), NtSysno::NtAllocateVirtualMemory => Some(sys_req!(NtAllocateVirtualMemory { process_handle: { ProcessHandle::from_raw }, @@ -127,6 +154,10 @@ impl SyscallRequest { allocation_type, protect, })), + NtSysno::NtTerminateProcess => Some(sys_req!(NtTerminateProcess { + process_handle: { ProcessHandle::from_raw }, + exit_status, + })), _ => None, } } diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs new file mode 100644 index 0000000000..e06b789392 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -0,0 +1,1167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows registry syscalls backed by a private file-system-shaped store (i.e., +//! a layered file system with in-mem and tar filesystems). +//! +//! Registry keys are represented as directories and values as files under each +//! key's `.values` directory: +//! +//! ```text +//! /registry/machine/system/currentcontrolset/control/nls/codepage/ +//! .values/ +//! acp +//! oemcp +//! maccp +//! ... +//! EUDCCodeRange/ +//! .values/ +//! 932 +//! ... +//! ... +//! ``` +//! +//! This is only an implementation detail: syscall handlers must expose registry +//! object semantics rather than file semantics. + +use core::mem::{offset_of, size_of}; + +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +use int_enum::IntEnum; +use litebox::LiteBox; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::fs::errors::{ + FileStatusError, MkdirError, OpenError, PathError, ReadError, WriteError, +}; +use litebox::fs::{FileSystem as _, FileType, Mode, OFlags}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::utils::TruncateExt; +use litebox_common_windows::nt_status::NtStatus; +use litebox_platform_multiplex::Platform; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::syscalls::Handle; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, raw_handle_entry, remove_raw_handle, +}; + +use crate::nt_types::{ObjectAttributes, UnicodeString, read_object_attributes}; + +struct RegistryKeySubsystem; + +impl FdEnabledSubsystem for RegistryKeySubsystem { + type Entry = RegistryKeyObject; +} + +impl FdEnabledSubsystemEntry for RegistryKeyObject {} + +struct RegistryKeyObject { + path: String, +} + +type RegistryFileSystem = litebox::fs::layered::FileSystem< + Platform, + litebox::fs::in_mem::FileSystem, + litebox::fs::tar_ro::FileSystem, +>; + +pub(crate) struct RegistryStore { + fs: RegistryFileSystem, +} + +const VALUES_DIR_NAME: &str = ".values"; +const DEFAULT_CODE_PAGE_KEY: &str = + "\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nls\\CodePage"; +const DEFAULT_SESSION_MANAGER_KEY: &str = + "\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Session Manager"; +const DEFAULT_SEGMENT_HEAP_KEY: &str = + "\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Session Manager\\Segment Heap"; +const DEFAULT_IMAGE_FILE_EXECUTION_OPTIONS_KEY: &str = "\\Registry\\Machine\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options"; +const DEFAULT_ACP_VALUE: &[u8] = &[b'1', 0, b'2', 0, b'5', 0, b'2', 0, 0, 0]; +const DEFAULT_OEMCP_VALUE: &[u8] = &[b'4', 0, b'3', 0, b'7', 0, 0, 0]; +const DEFAULT_MACCP_VALUE: &[u8] = &[b'1', 0, b'0', 0, b'0', 0, b'0', 0, b'0', 0, 0, 0]; +const REGISTRY_VALUE_TYPE_SIZE: usize = size_of::(); + +/// System-defined `REG_*` value types stored in `KEY_VALUE_*_INFORMATION::Type`. +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum RegistryValueType { + /// `REG_NONE`: data with no particular type. + None = 0, + /// `REG_SZ`: a null-terminated Unicode string. + Sz = 1, + /// `REG_EXPAND_SZ`: a null-terminated Unicode string with unexpanded environment references. + ExpandSz = 2, + /// `REG_BINARY`: binary data in any form. + Binary = 3, + /// `REG_DWORD` / `REG_DWORD_LITTLE_ENDIAN`: a little-endian 4-byte value. + Dword = 4, + /// `REG_DWORD_BIG_ENDIAN`: a big-endian 4-byte value. + DwordBigEndian = 5, + /// `REG_LINK`: a Unicode string naming a symbolic link. + Link = 6, + /// `REG_MULTI_SZ`: null-terminated strings terminated by another zero. + MultiSz = 7, + /// `REG_RESOURCE_LIST`: a device driver's hardware resource list. + ResourceList = 8, + /// `REG_FULL_RESOURCE_DESCRIPTOR`: hardware resources used by a physical device. + FullResourceDescriptor = 9, + /// `REG_RESOURCE_REQUIREMENTS_LIST`: possible hardware resources for a device. + ResourceRequirementsList = 10, + /// `REG_QWORD` / `REG_QWORD_LITTLE_ENDIAN`: a little-endian 8-byte value. + Qword = 11, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum KeyValueInformationClass { + Basic = 0, + Full = 1, + Partial = 2, +} + +/// The `KEY_VALUE_BASIC_INFORMATION` structure defines a subset of the full +/// information available for a value entry of a registry key. +/// +/// The variable-length `Name` field follows this fixed-size header. +/// See . +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct KeyValueBasicInformation { + title_index: u32, + value_type: u32, + name_length: u32, + // Followed by a variable-length name. + name: [u8; 0], +} + +/// The `KEY_VALUE_FULL_INFORMATION` structure defines information available +/// for a value entry of a registry key. +/// +/// The variable-length `Name` field follows this fixed-size header. The value +/// data starts at `data_offset` after any alignment padding. +/// See . +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct KeyValueFullInformation { + title_index: u32, + value_type: u32, + data_offset: u32, + data_length: u32, + name_length: u32, + // Followed by a variable-length name and aligned value data. + name: [u8; 0], + // Followed by aligned value data. + // ... + // Data[u8; data_length]; +} + +/// The `KEY_VALUE_PARTIAL_INFORMATION` structure defines a subset of the value +/// information available for a value entry of a registry key. +/// +/// The variable-length `Data` field follows this fixed-size header. +/// See . +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct KeyValuePartialInformation { + title_index: u32, + value_type: u32, + data_length: u32, + // Followed by variable-length value data. + data: [u8; 0], +} + +struct RegistryValue { + value_type: RegistryValueType, + data: Vec, +} + +impl RegistryStore { + pub(crate) fn new(litebox: &LiteBox) -> Self { + let mut in_mem = litebox::fs::in_mem::FileSystem::new(litebox); + in_mem.with_root_privileges(|fs| { + for key in [ + DEFAULT_SESSION_MANAGER_KEY, + DEFAULT_SEGMENT_HEAP_KEY, + DEFAULT_IMAGE_FILE_EXECUTION_OPTIONS_KEY, + ] { + if let Err(status) = create_key_in_fs(fs, 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, DEFAULT_CODE_PAGE_KEY, name, RegistryValueType::Sz, value) + { + litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize registry value"); + break; + } + } + }); + + let fs = litebox::fs::layered::FileSystem::new( + litebox, + in_mem, + litebox::fs::tar_ro::FileSystem::new( + litebox, + // TODO: Replace with tar file provided by the user + litebox::fs::tar_ro::EMPTY_TAR_FILE.into(), + ), + litebox::fs::layered::LayeringSemantics::LowerLayerReadOnly, + ); + + Self { fs } + } + + fn key_exists(&self, path: &str) -> Result { + match self.fs.file_status(path) { + Ok(status) => Ok(status.file_type == FileType::Directory), + Err(FileStatusError::PathError( + PathError::NoSuchFileOrDirectory | PathError::MissingComponent, + )) => Ok(false), + Err(FileStatusError::PathError(error)) => { + Err(map_path_error(error, NtStatus::OBJECT_NAME_NOT_FOUND)) + } + Err(_) => Err(NtStatus::UNSUCCESSFUL), + } + } + + fn read_value(&self, key_path: &str, value_name: &str) -> Result { + let value_path = value_path(key_path, value_name)?; + let status = self + .fs + .file_status(&*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 { + return Err(NtStatus::UNSUCCESSFUL); + } + + let fd = self + .fs + .open(&*value_path, OFlags::RDONLY, 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 value_type = RegistryValueType::try_from(u32::from_le_bytes( + data[..REGISTRY_VALUE_TYPE_SIZE] + .try_into() + .map_err(|_| NtStatus::UNSUCCESSFUL)?, + )) + .map_err(|_| NtStatus::UNSUCCESSFUL)?; + data.drain(..REGISTRY_VALUE_TYPE_SIZE); + + Ok(RegistryValue { value_type, data }) + } +} + +impl Task { + pub(crate) fn sys_nt_open_key( + &self, + key_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + let Some(object_attributes) = object_attributes else { + return NtStatus::INVALID_PARAMETER; + }; + let object_attributes = match read_object_attributes(object_attributes) { + Ok(object_attributes) => object_attributes, + Err(status) => return status, + }; + match self.do_nt_open_key(desired_access, object_attributes) { + Ok(handle) => { + if key_handle.write_at_offset(0, handle).is_none() { + remove_raw_handle::( + &self.global.litebox, + &self.process.handles, + handle, + ); + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::SUCCESS + } + Err(status) => status, + } + } + + fn do_nt_open_key( + &self, + desired_access: u32, + object_attributes: ObjectAttributes, + ) -> Result { + if object_attributes.object_name == 0 { + return Err(NtStatus::INVALID_PARAMETER); + } + + let object_name_ptr = ConstPtr::::from_usize(object_attributes.object_name); + let object_name = object_name_ptr + .read_at_offset(0) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + let key_name = String::try_from(object_name)?; + let path = if object_attributes.root_directory.is_null() || key_name.starts_with('\\') { + absolute_nt_key_name_to_fs_path(&key_name)? + } else { + let root_key = raw_handle_entry::( + &self.global.litebox, + &self.process.handles, + object_attributes.root_directory, + ) + .ok_or(NtStatus::INVALID_HANDLE)?; + root_key + .with_entry(|root_key| relative_nt_key_name_to_fs_path(&root_key.path, &key_name))? + }; + + match self.global.registry.key_exists(&path) { + Ok(true) => {} + Ok(false) => { + return Err(NtStatus::OBJECT_NAME_NOT_FOUND); + } + Err(status) => { + litebox_util_log::debug!( + desired_access:% = format_args!("{desired_access:#x}"), + root_directory:% = format_args!("{:#x}", object_attributes.root_directory.as_raw()), + name:% = key_name, + path:% = path, + status:? = status; + "NtOpenKey failed" + ); + return Err(status); + } + } + + let key = RegistryKeyObject { path }; + let mut descriptor_table = self.global.litebox.descriptor_table_mut(); + let typed = descriptor_table.insert::(key); + drop(descriptor_table); + + insert_raw_handle(&self.global.litebox, &self.process.handles, typed) + } + + pub(crate) fn sys_nt_query_value_key( + &self, + key_handle: Handle, + value_name: ConstPtr, + key_value_information_class: u32, + key_value_information: MutPtr, + length: u32, + result_length: MutPtr, + ) -> NtStatus { + let Some(value_name) = value_name.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let Ok(key_value_information_class) = + KeyValueInformationClass::try_from(key_value_information_class) + else { + litebox_util_log::debug!( + key_value_information_class = key_value_information_class; + "Unsupported NtQueryValueKey class" + ); + return NtStatus::INVALID_INFO_CLASS; + }; + match self.do_nt_query_value_key( + key_handle, + value_name, + key_value_information_class, + key_value_information, + length, + result_length, + ) { + Ok(()) => NtStatus::SUCCESS, + Err(status) => status, + } + } + + fn do_nt_query_value_key( + &self, + key_handle: Handle, + value_name: UnicodeString, + key_value_information_class: KeyValueInformationClass, + key_value_information: MutPtr, + length: u32, + result_length: MutPtr, + ) -> Result<(), NtStatus> { + let key = raw_handle_entry::( + &self.global.litebox, + &self.process.handles, + key_handle, + ) + .ok_or(NtStatus::INVALID_HANDLE)?; + let value_name = String::try_from(value_name)?; + let value = + key.with_entry(|key| self.global.registry.read_value(&key.path, &value_name))?; + let name = utf16le(&value_name); + match key_value_information_class { + KeyValueInformationClass::Basic => { + let required_length = size_of::() + .checked_add(name.len()) + .ok_or(NtStatus::UNSUCCESSFUL)?; + let information = KeyValueBasicInformation { + title_index: 0, + value_type: value.value_type.into(), + name_length: name.len().trunc(), + name: [0u8; 0], + }; + write_query_result_length(result_length, length, required_length)?; + write_query_information( + key_value_information, + information.as_bytes(), + &[(offset_of!(KeyValueBasicInformation, name), name.as_slice())], + )?; + } + KeyValueInformationClass::Full => { + let name_end = offset_of!(KeyValueFullInformation, name) + .checked_add(name.len()) + .ok_or(NtStatus::UNSUCCESSFUL)?; + let data_offset = name_end + .checked_next_multiple_of(4) + .ok_or(NtStatus::UNSUCCESSFUL)?; + let required_length = data_offset + .checked_add(value.data.len()) + .ok_or(NtStatus::UNSUCCESSFUL)?; + write_query_result_length(result_length, length, required_length)?; + let information = KeyValueFullInformation { + title_index: 0, + value_type: value.value_type.into(), + data_offset: data_offset.trunc(), + data_length: value.data.len().trunc(), + name_length: name.len().trunc(), + name: [0u8; 0], + }; + + write_query_information( + key_value_information, + information.as_bytes(), + &[ + (offset_of!(KeyValueFullInformation, name), name.as_slice()), + (data_offset, value.data.as_slice()), + ], + )?; + } + KeyValueInformationClass::Partial => { + let required_length = size_of::() + .checked_add(value.data.len()) + .ok_or(NtStatus::UNSUCCESSFUL)?; + write_query_result_length(result_length, length, required_length)?; + let information = KeyValuePartialInformation { + title_index: 0, + value_type: value.value_type.into(), + data_length: value.data.len().trunc(), + data: [0u8; 0], + }; + + write_query_information( + key_value_information, + information.as_bytes(), + &[( + offset_of!(KeyValuePartialInformation, data), + value.data.as_slice(), + )], + )?; + } + } + + litebox_util_log::debug!( + handle:% = format_args!("{:#x}", key_handle.as_raw()), + value_name:% = value_name, + key_value_information_class:? = key_value_information_class, + length = length; + "Handled NtQueryValueKey syscall" + ); + + Ok(()) + } +} + +fn write_query_result_length( + result_length: MutPtr, + buffer_length: u32, + required_length: usize, +) -> Result<(), NtStatus> { + result_length + .write_at_offset(0, required_length.trunc()) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + if (buffer_length as usize) < required_length { + return Err(NtStatus::BUFFER_OVERFLOW); + } + Ok(()) +} + +fn write_query_information( + key_value_information: MutPtr, + header: &[u8], + trailing_slices: &[(usize, &[u8])], +) -> Result<(), NtStatus> { + key_value_information + .write_slice_at_offset(0, header) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + for &(offset, bytes) in trailing_slices { + key_value_information + .write_slice_at_offset(offset.cast_signed(), bytes) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + } + Ok(()) +} + +fn utf16le(value: &str) -> Vec { + let mut bytes = Vec::new(); + for code_unit in value.encode_utf16() { + bytes.extend_from_slice(&code_unit.to_le_bytes()); + } + bytes +} + +fn absolute_nt_key_name_to_fs_path(name: &str) -> Result { + if !name.starts_with('\\') { + return Err(NtStatus::INVALID_PARAMETER); + } + let mut path = String::from("/"); + append_registry_components(&mut path, name.trim_start_matches('\\'))?; + Ok(path) +} + +fn relative_nt_key_name_to_fs_path(root: &str, name: &str) -> Result { + if name.starts_with('\\') { + return absolute_nt_key_name_to_fs_path(name); + } + let mut path = String::from(root); + append_registry_components(&mut path, name)?; + Ok(path) +} + +fn append_registry_components(path: &mut String, name: &str) -> Result<(), NtStatus> { + if name.is_empty() { + return Err(NtStatus::INVALID_PARAMETER); + } + for component in name.split('\\') { + if !is_valid_key_component(component) { + return Err(NtStatus::INVALID_PARAMETER); + } + if !path.ends_with('/') { + path.push('/'); + } + path.push_str(&component.to_ascii_lowercase()); + } + Ok(()) +} + +fn is_valid_key_component(component: &str) -> bool { + !component.is_empty() + && component != "." + && component != ".." + && !component.eq_ignore_ascii_case(VALUES_DIR_NAME) + && !component.contains('/') +} + +fn write_value_in_fs( + fs: &FS, + key_nt_path: &str, + value_name: &str, + value_type: RegistryValueType, + value: &[u8], +) -> Result<(), NtStatus> { + let key_path = create_key_in_fs(fs, key_nt_path)?; + let value_path = value_path(&key_path, value_name)?; + let fd = fs + .open( + &*value_path, + OFlags::CREAT | OFlags::WRONLY | OFlags::TRUNC, + Mode::RUSR | Mode::WUSR | Mode::ROTH | Mode::WOTH, + ) + .map_err(map_open_error)?; + let written = fs + .write(&fd, &u32::from(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); + Ok(()) +} + +fn create_key_in_fs( + fs: &FS, + nt_path: &str, +) -> Result { + let path = absolute_nt_key_name_to_fs_path(nt_path)?; + create_key_path_in_fs(fs, &path)?; + Ok(path) +} + +fn create_key_path_in_fs(fs: &FS, path: &str) -> Result<(), NtStatus> { + let mut current = String::new(); + for component in path.trim_start_matches('/').split('/') { + if component.is_empty() { + continue; + } + current.push('/'); + current.push_str(component); + ensure_directory_in_fs(fs, ¤t)?; + + let mut values_dir = current.clone(); + values_dir.push('/'); + values_dir.push_str(VALUES_DIR_NAME); + ensure_directory_in_fs(fs, &values_dir)?; + } + Ok(()) +} + +fn ensure_directory_in_fs( + fs: &FS, + path: &str, +) -> Result<(), NtStatus> { + match fs.file_status(path) { + Ok(status) if status.file_type == FileType::Directory => Ok(()), + Ok(_) => Err(NtStatus::OBJECT_TYPE_MISMATCH), + Err(FileStatusError::PathError( + PathError::NoSuchFileOrDirectory | PathError::MissingComponent, + )) => match fs.mkdir( + path, + Mode::RUSR | Mode::WUSR | Mode::XUSR | Mode::ROTH | Mode::WOTH | Mode::XOTH, + ) { + Ok(()) | Err(MkdirError::AlreadyExists) => Ok(()), + Err(error) => Err(map_mkdir_error(error)), + }, + Err(FileStatusError::PathError(error)) => { + Err(map_path_error(error, NtStatus::OBJECT_NAME_NOT_FOUND)) + } + Err(_) => Err(NtStatus::UNSUCCESSFUL), + } +} + +fn value_path(key_path: &str, value_name: &str) -> Result { + if !is_valid_value_name(value_name) { + return Err(NtStatus::INVALID_PARAMETER); + } + + let mut path = String::from(key_path); + if !path.ends_with('/') { + path.push('/'); + } + path.push_str(VALUES_DIR_NAME); + path.push('/'); + path.push_str(&value_name.to_ascii_lowercase()); + Ok(path) +} + +fn is_valid_value_name(value_name: &str) -> bool { + !value_name.is_empty() + && value_name != "." + && value_name != ".." + && !value_name.contains('/') + && !value_name.contains('\\') +} + +fn map_open_error(error: OpenError) -> NtStatus { + match error { + OpenError::PathError(error) => map_path_error(error, NtStatus::OBJECT_NAME_NOT_FOUND), + OpenError::AccessNotAllowed | OpenError::NoWritePerms | OpenError::ReadOnlyFileSystem => { + NtStatus::ACCESS_DENIED + } + OpenError::AlreadyExists => NtStatus::OBJECT_NAME_COLLISION, + _ => NtStatus::UNSUCCESSFUL, + } +} + +fn map_file_status_error(error: FileStatusError) -> NtStatus { + match error { + FileStatusError::PathError(error) => map_path_error(error, NtStatus::OBJECT_NAME_NOT_FOUND), + _ => NtStatus::UNSUCCESSFUL, + } +} + +fn map_mkdir_error(error: MkdirError) -> NtStatus { + match error { + MkdirError::AlreadyExists => NtStatus::OBJECT_NAME_COLLISION, + MkdirError::PathError(error) => map_path_error(error, NtStatus::OBJECT_PATH_NOT_FOUND), + MkdirError::NoWritePerms | MkdirError::ReadOnlyFileSystem => NtStatus::ACCESS_DENIED, + _ => NtStatus::UNSUCCESSFUL, + } +} + +fn map_path_error(error: PathError, not_found_status: NtStatus) -> NtStatus { + match error { + PathError::NoSuchFileOrDirectory | PathError::MissingComponent => not_found_status, + PathError::ComponentNotADirectory => NtStatus::NOT_A_DIRECTORY, + PathError::InvalidPathname => NtStatus::INVALID_PARAMETER, + PathError::NoSearchPerms { .. } => NtStatus::UNSUCCESSFUL, + } +} + +fn map_write_error(error: WriteError) -> NtStatus { + match error { + WriteError::NotForWriting => NtStatus::ACCESS_DENIED, + WriteError::NotAFile => NtStatus::OBJECT_TYPE_MISMATCH, + _ => NtStatus::UNSUCCESSFUL, + } +} + +fn map_read_error(error: ReadError) -> NtStatus { + match error { + ReadError::NotForReading => NtStatus::ACCESS_DENIED, + ReadError::NotAFile => NtStatus::OBJECT_TYPE_MISMATCH, + _ => NtStatus::UNSUCCESSFUL, + } +} + +#[cfg(test)] +mod tests { + use crate::tests::init_platform; + + use super::*; + use core::mem::size_of; + use litebox::LiteBox; + use zerocopy::{FromBytes, IntoBytes}; + + extern crate std; + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const ERROR_SUCCESS: i32 = 0; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const HKEY_LOCAL_MACHINE: *mut core::ffi::c_void = 0xffffffff80000002usize as _; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const KEY_QUERY_VALUE: u32 = 0x0001; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const HOST_CODE_PAGE_KEY: &str = "SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage"; + + const KEY_VALUE_PARTIAL_INFORMATION_DATA_OFFSET: usize = + offset_of!(KeyValuePartialInformation, data); + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[allow(non_snake_case)] + #[link(name = "advapi32")] + unsafe extern "system" { + fn RegOpenKeyExW( + hKey: *mut core::ffi::c_void, + lpSubKey: *const u16, + ulOptions: u32, + samDesired: u32, + phkResult: *mut *mut core::ffi::c_void, + ) -> i32; + fn RegQueryValueExW( + hKey: *mut core::ffi::c_void, + lpValueName: *const u16, + lpReserved: *mut u32, + lpType: *mut u32, + lpData: *mut u8, + lpcbData: *mut u32, + ) -> i32; + fn RegCloseKey(hKey: *mut core::ffi::c_void) -> i32; + } + + fn const_ptr(value: &T) -> ConstPtr { + ConstPtr::from_usize(core::ptr::from_ref(value).cast::() as usize) + } + + fn mut_ptr(value: &mut T) -> MutPtr { + MutPtr::from_usize(core::ptr::from_mut(value).cast::() as usize) + } + + fn mut_byte_ptr(value: &mut T) -> MutPtr { + MutPtr::from_usize(core::ptr::from_mut(value).cast::() as usize) + } + + fn unicode_string(value: &[u16]) -> UnicodeString { + let byte_len = u16::try_from(core::mem::size_of_val(value)).unwrap(); + UnicodeString { + length: byte_len, + maximum_length: byte_len, + padding_0: [0; 4], + buffer: value.as_ptr() as usize, + } + } + + fn utf16(value: &str) -> std::vec::Vec { + value.encode_utf16().collect() + } + + fn object_attributes(name: &UnicodeString) -> ObjectAttributes { + ObjectAttributes { + length: u32::try_from(size_of::()).unwrap(), + root_directory: Handle::default(), + object_name: core::ptr::from_ref(name) as usize, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + } + } + + fn test_registry() -> (LiteBox, RegistryStore) { + init_platform(); + let litebox = LiteBox::new(litebox_platform_multiplex::platform()); + let registry = RegistryStore::new(&litebox); + (litebox, registry) + } + + fn open_key( + task: &Task, + object_attributes: ObjectAttributes, + ) -> Result { + task.do_nt_open_key(0x20019, object_attributes) + } + + fn open_code_page_key(task: &Task) -> Handle { + 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); + open_key(task, object_attributes).expect("Failed to open code page key") + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn nul_terminated_utf16(value: &str) -> Vec { + let mut value: Vec = value.encode_utf16().collect(); + value.push(0); + value + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn host_registry_value(key_path: &str, value_name: &str) -> RegistryValue { + let key_path = nul_terminated_utf16(key_path); + let value_name = nul_terminated_utf16(value_name); + let mut key = core::ptr::null_mut(); + // SAFETY: The key path is NUL-terminated, `phkResult` points to a live output + // slot, and `HKEY_LOCAL_MACHINE` is the documented predefined registry handle. + let status = unsafe { + RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + key_path.as_ptr(), + 0, + KEY_QUERY_VALUE, + &raw mut key, + ) + }; + assert_eq!(status, ERROR_SUCCESS, "failed to open host registry key"); + + let mut value_type = 0; + let mut data_len = 0; + // SAFETY: The key handle was returned by `RegOpenKeyExW`, the value name is + // NUL-terminated, and the null data buffer requests the required byte length. + let status = unsafe { + RegQueryValueExW( + key, + value_name.as_ptr(), + core::ptr::null_mut(), + &raw mut value_type, + core::ptr::null_mut(), + &raw mut data_len, + ) + }; + assert_eq!(status, ERROR_SUCCESS, "failed to size host registry value"); + + let mut data = vec![0; data_len as usize]; + // SAFETY: `data` has exactly the byte length returned by the sizing query, + // and all other pointers remain valid for the duration of the call. + let status = unsafe { + RegQueryValueExW( + key, + value_name.as_ptr(), + core::ptr::null_mut(), + &raw mut value_type, + data.as_mut_ptr(), + &raw mut data_len, + ) + }; + assert_eq!(status, ERROR_SUCCESS, "failed to read host registry value"); + data.truncate(data_len as usize); + + // SAFETY: The key handle was returned by `RegOpenKeyExW` and has not been closed yet. + let status = unsafe { RegCloseKey(key) }; + assert_eq!(status, ERROR_SUCCESS, "failed to close host registry key"); + + RegistryValue { + value_type: RegistryValueType::try_from(value_type).expect("known registry value type"), + data, + } + } + + #[test] + fn registry_store_separates_values_from_subkeys() { + 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.key_exists(&key_path), Ok(true)); + assert_eq!( + registry.fs.file_status(&*value_path).unwrap().file_type, + FileType::RegularFile + ); + assert_eq!( + registry.fs.file_status(&*value_path).unwrap().size, + REGISTRY_VALUE_TYPE_SIZE + DEFAULT_ACP_VALUE.len() + ); + let value = registry.read_value(&key_path, "ACP").unwrap(); + assert_eq!(value.value_type, RegistryValueType::Sz); + assert_eq!(value.data, DEFAULT_ACP_VALUE); + + let values_dir = absolute_nt_key_name_to_fs_path( + "\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nls\\CodePage\\.values", + ); + assert_eq!(values_dir, Err(NtStatus::INVALID_PARAMETER)); + } + + #[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 key_handle = open_code_page_key(&task); + + for name in ["ACP", "OEMCP", "MACCP"] { + let host_value = host_registry_value(HOST_CODE_PAGE_KEY, name); + let value_name = utf16(name); + let value_name = unicode_string(&value_name); + let mut information = [0u8; 64]; + let mut result_length = 0; + + assert!( + task.do_nt_query_value_key( + key_handle, + value_name, + KeyValueInformationClass::Partial, + mut_byte_ptr(&mut information), + u32::try_from(information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .is_ok() + ); + + let information = &information[..(result_length as usize)]; + let (information, data) = + KeyValuePartialInformation::read_from_prefix(information).unwrap(); + + assert_eq!(host_value.value_type, RegistryValueType::Sz); + assert_eq!(information.value_type, host_value.value_type.into()); + assert_eq!(information.data_length as usize, host_value.data.len()); + assert_eq!(data, host_value.data.as_slice()); + } + } + + #[test] + fn nt_open_key_opens_existing_absolute_and_relative_keys() { + let task = crate::tests::test_task(); + 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); + let nls_handle = open_key(&task, nls_object_attributes).expect("Failed to open NLS key"); + assert_ne!(nls_handle, Handle::default()); + + let code_page_name = utf16("CodePage"); + let code_page_name = unicode_string(&code_page_name); + let mut code_page_object_attributes = object_attributes(&code_page_name); + code_page_object_attributes.root_directory = nls_handle; + let code_page_handle = + open_key(&task, code_page_object_attributes).expect("Failed to open code page key"); + assert_ne!(code_page_handle, Handle::default()); + } + + #[test] + fn nt_open_key_reports_missing_absolute_key() { + let task = crate::tests::test_task(); + let name = utf16("\\Registry\\Machine\\Software\\Missing"); + let name = unicode_string(&name); + let object_attributes = object_attributes(&name); + assert_eq!( + open_key(&task, object_attributes).unwrap_err(), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + } + + #[test] + fn nt_open_key_rejects_invalid_relative_root() { + let task = crate::tests::test_task(); + let name = utf16("Child"); + let name = unicode_string(&name); + let mut object_attributes = object_attributes(&name); + object_attributes.root_directory = Handle::from_raw(0x1234); + assert_eq!( + open_key(&task, object_attributes).unwrap_err(), + NtStatus::INVALID_HANDLE + ); + } + + #[test] + fn nt_query_value_key_reports_partial_information() { + let task = crate::tests::test_task(); + let key_handle = open_code_page_key(&task); + let value_name = utf16("ACP"); + let value_name = unicode_string(&value_name); + let mut information = [0u8; 64]; + let mut result_length = 0; + + assert!( + task.do_nt_query_value_key( + key_handle, + value_name, + KeyValueInformationClass::Partial, + mut_byte_ptr(&mut information), + u32::try_from(information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .is_ok() + ); + + assert_eq!( + result_length as usize, + size_of::() + DEFAULT_ACP_VALUE.len() + ); + let information = &information[..(result_length as usize)]; + let (information, data) = + KeyValuePartialInformation::read_from_prefix(information).unwrap(); + assert_eq!(information.title_index, 0); + assert_eq!(information.value_type, RegistryValueType::Sz.into()); + assert_eq!( + information.data_length, + u32::try_from(DEFAULT_ACP_VALUE.len()).unwrap() + ); + assert_eq!(data, DEFAULT_ACP_VALUE); + } + + #[test] + fn nt_query_value_key_reports_basic_and_full_information() { + let task = crate::tests::test_task(); + let key_handle = open_code_page_key(&task); + let value_name = utf16("OEMCP"); + let value_name = unicode_string(&value_name); + let mut basic_information = [0u8; 64]; + let mut full_information = [0u8; 64]; + let mut result_length = 0; + + assert!( + task.do_nt_query_value_key( + key_handle, + value_name, + KeyValueInformationClass::Basic, + mut_byte_ptr(&mut basic_information), + u32::try_from(basic_information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .is_ok() + ); + let name = utf16le("OEMCP"); + assert_eq!( + result_length as usize, + size_of::() + name.len() + ); + let basic_information = &basic_information[..(result_length as usize)]; + let (basic_information, basic_name) = + KeyValueBasicInformation::read_from_prefix(basic_information).unwrap(); + assert_eq!(basic_information.title_index, 0); + assert_eq!(basic_information.value_type, RegistryValueType::Sz.into()); + assert_eq!(basic_information.name_length as usize, name.len()); + assert_eq!(basic_name, name.as_slice()); + + assert!( + task.do_nt_query_value_key( + key_handle, + value_name, + KeyValueInformationClass::Full, + mut_byte_ptr(&mut full_information), + u32::try_from(full_information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .is_ok() + ); + let full_information = &full_information[..(result_length as usize)]; + let (full_header, full_tail) = + KeyValueFullInformation::read_from_prefix(full_information).unwrap(); + let data_offset = usize::try_from(full_header.data_offset).unwrap(); + assert_eq!(full_header.title_index, 0); + assert_eq!(full_header.value_type, RegistryValueType::Sz.into()); + assert_eq!(full_header.data_length as usize, DEFAULT_OEMCP_VALUE.len()); + assert_eq!(full_header.name_length as usize, name.len()); + assert_eq!(&full_tail[..name.len()], name.as_slice()); + assert_eq!( + &full_information[data_offset..data_offset + DEFAULT_OEMCP_VALUE.len()], + DEFAULT_OEMCP_VALUE + ); + } + + #[test] + fn nt_query_value_key_rejects_invalid_arguments() { + let task = crate::tests::test_task(); + let key_handle = open_code_page_key(&task); + let value_name = utf16("ACP"); + let value_name = unicode_string(&value_name); + let missing_value_name = utf16("Missing"); + let missing_value_name = unicode_string(&missing_value_name); + let mut information = [0u8; 64]; + let mut short_information = [0u8; KEY_VALUE_PARTIAL_INFORMATION_DATA_OFFSET - 1]; + let mut result_length = 0; + + assert_eq!( + task.do_nt_query_value_key( + Handle::from_raw(0x1234), + value_name, + KeyValueInformationClass::Partial, + mut_byte_ptr(&mut information), + u32::try_from(information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .unwrap_err(), + NtStatus::INVALID_HANDLE + ); + + assert_eq!( + task.do_nt_query_value_key( + key_handle, + missing_value_name, + KeyValueInformationClass::Partial, + mut_byte_ptr(&mut information), + u32::try_from(information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .unwrap_err(), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + + assert_eq!( + task.sys_nt_query_value_key( + key_handle, + const_ptr(&value_name), + 0xffff, + mut_byte_ptr(&mut information), + u32::try_from(information.len()).unwrap(), + mut_ptr(&mut result_length), + ), + NtStatus::INVALID_INFO_CLASS + ); + + assert_eq!( + task.do_nt_query_value_key( + key_handle, + value_name, + KeyValueInformationClass::Partial, + mut_byte_ptr(&mut short_information), + u32::try_from(short_information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .unwrap_err(), + NtStatus::BUFFER_OVERFLOW + ); + assert_eq!(result_length, 22); + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs new file mode 100644 index 0000000000..f2e4c0c74d --- /dev/null +++ b/litebox_shim_windows/src/tests.rs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +extern crate std; + +use alloc::sync::Arc; +use core::marker::PhantomData; +use core::sync::atomic::AtomicI32; +use litebox::LiteBox; +use litebox::fd::RawDescriptorStorage; + +use crate::{DefaultFS, GlobalState, Process, Task, WindowsHandleStore, WindowsPageManager}; + +pub(crate) fn init_platform() { + static PLATFORM_INIT: std::sync::Once = std::sync::Once::new(); + PLATFORM_INIT.call_once(|| { + #[cfg(target_os = "linux")] + let platform = crate::Platform::new(None); + + #[cfg(not(target_os = "linux"))] + let platform = crate::Platform::new(); + + litebox_platform_multiplex::set_platform(platform); + }); +} + +pub(crate) fn test_task() -> Task { + init_platform(); + let platform = litebox_platform_multiplex::platform(); + let litebox = LiteBox::new(platform); + let page_manager = WindowsPageManager::new(&litebox); + Task { + global: Arc::new(GlobalState { + registry: crate::syscalls::registry::RegistryStore::new(&litebox), + litebox, + page_manager, + _fs: PhantomData, + }), + process: Arc::new(Process { + ntdll_mapping: None, + handles: WindowsHandleStore::new(RawDescriptorStorage::new()), + exit_code: AtomicI32::new(0), + }), + entry_point: 0, + stack_top: 0, + _phantom: PhantomData, + } +} From 5e041d6ef5d5dab9e70e3d9c25b95719e2a53c33 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 29 May 2026 18:07:42 -0700 Subject: [PATCH 014/319] Make Windows shim generic over platform (#886) This PR makes `litebox_shim_windows` generic over the concrete LiteBox platform instead of depending on `litebox_platform_multiplex`. --- Cargo.lock | 4 +- litebox_runner_windows_userland/Cargo.toml | 3 +- litebox_runner_windows_userland/src/lib.rs | 7 +- litebox_shim_windows/Cargo.toml | 9 +- litebox_shim_windows/src/lib.rs | 153 +++++++++++------- litebox_shim_windows/src/loader/pe.rs | 92 ++++++----- litebox_shim_windows/src/nt_types.rs | 23 ++- litebox_shim_windows/src/syscalls/registry.rs | 85 +++++----- litebox_shim_windows/src/tests.rs | 32 ++-- 9 files changed, 231 insertions(+), 177 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a307409060..9a6caf7920 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1719,7 +1719,6 @@ dependencies = [ "litebox", "litebox_common_linux", "litebox_common_windows", - "litebox_platform_multiplex", "litebox_platform_windows_userland", "litebox_shim_windows", "litebox_syscall_rewriter", @@ -1784,7 +1783,8 @@ dependencies = [ "litebox", "litebox_common_linux", "litebox_common_windows", - "litebox_platform_multiplex", + "litebox_platform_linux_userland", + "litebox_platform_windows_userland", "litebox_util_log", "thiserror", "zerocopy", diff --git a/litebox_runner_windows_userland/Cargo.toml b/litebox_runner_windows_userland/Cargo.toml index c4bc5dfbe5..8f6fcdc10d 100644 --- a/litebox_runner_windows_userland/Cargo.toml +++ b/litebox_runner_windows_userland/Cargo.toml @@ -9,8 +9,7 @@ clap = { version = "4.5.33", features = ["derive"] } litebox = { version = "0.1.0", path = "../litebox" } 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_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_windows_userland"] } -litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows", default-features = false, features = ["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"] } tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } diff --git a/litebox_runner_windows_userland/src/lib.rs b/litebox_runner_windows_userland/src/lib.rs index 3abe06eaa9..dd206a3dd7 100644 --- a/litebox_runner_windows_userland/src/lib.rs +++ b/litebox_runner_windows_userland/src/lib.rs @@ -9,7 +9,7 @@ extern crate alloc; use anyhow::{Context as _, Result}; use clap::Parser; -use litebox_platform_multiplex::Platform; +use litebox_platform_windows_userland::WindowsUserland; use std::path::PathBuf; /// Run Windows PE programs with LiteBox on unmodified Windows. @@ -67,9 +67,8 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let tar_data = std::fs::read(tar_file) .with_context(|| format!("Could not read tar file at {}", tar_file.display()))?; - let platform = Platform::new(); - litebox_platform_multiplex::set_platform(platform); - let shim_builder = litebox_shim_windows::WindowsShimBuilder::new(); + let platform = WindowsUserland::new(); + let shim_builder = litebox_shim_windows::WindowsShimBuilder::new(platform); let litebox = shim_builder.litebox(); let (program_path, program_args) = cli_args diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml index 9f886c2b38..8eb8412df4 100644 --- a/litebox_shim_windows/Cargo.toml +++ b/litebox_shim_windows/Cargo.toml @@ -8,14 +8,15 @@ int-enum = "1.2.0" litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } litebox_common_windows = { path = "../litebox_common_windows/", version = "0.1.0" } -litebox_platform_multiplex = { path = "../litebox_platform_multiplex/", version = "0.1.0", default-features = false } litebox_util_log = { path = "../litebox_util_log", version = "0.1.0" } thiserror = { version = "2.0.6", default-features = false } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } -[features] -default = ["platform_windows_userland"] -platform_windows_userland = ["litebox_platform_multiplex/platform_windows_userland"] +[target.'cfg(target_os = "linux")'.dev-dependencies] +litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland/", version = "0.1.0" } + +[target.'cfg(target_os = "windows")'.dev-dependencies] +litebox_platform_windows_userland = { path = "../litebox_platform_windows_userland/", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 306c8fc567..e910f61c6d 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -15,15 +15,18 @@ use alloc::sync::Arc; use alloc::vec::Vec; use core::marker::PhantomData; use core::sync::atomic::{AtomicI32, Ordering}; -use litebox::platform::RawConstPointer as _; use litebox_common_windows::nt_status::NtStatus; use litebox::LiteBox; use litebox::mm::PageManager; +use litebox::platform::{ + CrngProvider, PageManagementProvider, RawConstPointer as _, RawMutPointer as _, + RawPointerProvider, StdioProvider, SystemInfoProvider, +}; use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; +use litebox::sync::RawSyncPrimitivesProvider; use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; -use litebox_platform_multiplex::Platform; use crate::syscalls::SyscallRequest; @@ -36,16 +39,34 @@ mod tests; const DEFAULT_PROCESS_EXIT_CODE: i32 = 1; -pub(crate) type ConstPtr = +/// A LiteBox platform with the services required by the Windows shim. +pub trait ShimPlatform: + RawSyncPrimitivesProvider + + RawPointerProvider + + PageManagementProvider + + SystemInfoProvider +{ +} + +impl ShimPlatform for T where + T: RawSyncPrimitivesProvider + + RawPointerProvider + + PageManagementProvider + + SystemInfoProvider +{ +} + +pub(crate) type ConstPtr = ::RawConstPointer; -pub(crate) type MutPtr = ::RawMutPointer; -pub(crate) type WindowsPageManager = PageManager; -pub(crate) type WindowsHandleStore = +pub(crate) type MutPtr = + ::RawMutPointer; +pub(crate) type WindowsPageManager = PageManager; +pub(crate) type WindowsHandleStore = litebox::sync::RwLock; -pub type DefaultFS = WindowsFS; +pub type DefaultFS = WindowsFS; -pub(crate) type WindowsFS = litebox::fs::layered::FileSystem< +pub type WindowsFS = litebox::fs::layered::FileSystem< Platform, litebox::fs::in_mem::FileSystem, litebox::fs::layered::FileSystem< @@ -59,22 +80,22 @@ pub(crate) type WindowsFS = litebox::fs::layered::FileSystem< pub trait ShimFS: litebox::fs::FileSystem + Send + Sync + 'static {} impl ShimFS for T {} -fn write_value(address: usize, value: T) -> Option<()> +fn write_value(address: usize, value: T) -> Option<()> where + Platform: RawPointerProvider, T: zerocopy::FromBytes + zerocopy::IntoBytes, { - use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; let ptr = ::RawMutPointer::::from_usize( address, ); ptr.write_at_offset(0, value) } -fn write_slice(address: usize, values: &[T]) -> Option<()> +fn write_slice(address: usize, values: &[T]) -> Option<()> where + Platform: RawPointerProvider, T: Copy + zerocopy::FromBytes + zerocopy::IntoBytes, { - use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; let ptr = ::RawMutPointer::::from_usize( address, ); @@ -84,11 +105,14 @@ where Some(()) } -pub(crate) fn insert_raw_handle( +pub(crate) fn insert_raw_handle( litebox: &LiteBox, - handles: &WindowsHandleStore, + handles: &WindowsHandleStore, typed: litebox::fd::TypedFd, -) -> Result { +) -> Result +where + Platform: RawSyncPrimitivesProvider, +{ let mut handles = handles.write(); let raw_fd = handles.fd_into_raw_integer(typed); let Some(handle) = syscalls::Handle::from_raw_fd(raw_fd) else { @@ -102,11 +126,14 @@ pub(crate) fn insert_raw_handle( Ok(handle) } -pub(crate) fn raw_handle_entry( +pub(crate) fn raw_handle_entry( litebox: &LiteBox, - handles: &WindowsHandleStore, + handles: &WindowsHandleStore, handle: syscalls::Handle, -) -> Option> { +) -> Option> +where + Platform: RawSyncPrimitivesProvider, +{ let raw_fd = handle.raw_fd()?; let typed = { let handles = handles.read(); @@ -115,11 +142,13 @@ pub(crate) fn raw_handle_entry( litebox.descriptor_table().entry_handle(&typed) } -pub(crate) fn remove_raw_handle( +pub(crate) fn remove_raw_handle( litebox: &LiteBox, - handles: &WindowsHandleStore, + handles: &WindowsHandleStore, handle: syscalls::Handle, -) { +) where + Platform: RawSyncPrimitivesProvider, +{ let Some(raw_fd) = handle.raw_fd() else { return; }; @@ -133,21 +162,16 @@ pub(crate) fn remove_raw_handle( } /// Builds a Windows NT shim instance. -pub struct WindowsShimBuilder { +pub struct WindowsShimBuilder { + platform: &'static Platform, litebox: LiteBox, } -impl Default for WindowsShimBuilder { - fn default() -> Self { - Self::new() - } -} - -impl WindowsShimBuilder { +impl WindowsShimBuilder { #[must_use] - pub fn new() -> Self { - let platform = litebox_platform_multiplex::platform(); + pub fn new(platform: &'static Platform) -> Self { Self { + platform, litebox: LiteBox::new(platform), } } @@ -163,13 +187,17 @@ impl WindowsShimBuilder { &self, in_mem_fs: litebox::fs::in_mem::FileSystem, tar_ro_fs: litebox::fs::tar_ro::FileSystem, - ) -> DefaultFS { + ) -> DefaultFS + where + Platform: CrngProvider + StdioProvider, + { default_fs(&self.litebox, in_mem_fs, tar_ro_fs) } #[must_use] - pub fn build(self) -> WindowsShim { + pub fn build(self) -> WindowsShim { let global = Arc::new(GlobalState { + platform: self.platform, page_manager: PageManager::new(&self.litebox), registry: syscalls::registry::RegistryStore::new(&self.litebox), litebox: self.litebox, @@ -179,9 +207,9 @@ impl WindowsShimBuilder { } } -pub struct WindowsShim(Arc>); +pub struct WindowsShim(Arc>); -impl WindowsShim { +impl WindowsShim { /// Loads the program at `path` as the shim's initial task. /// /// TODO: PEB/TEB setup and initial handle table state are not yet implemented. @@ -191,11 +219,12 @@ impl WindowsShim { path: &str, _argv: Vec, _envp: Vec, - ) -> Result, loader::WindowsLoadError> { - let load_info = loader::PeLoader::new(fs, &self.0.page_manager).load(path)?; + ) -> Result, loader::WindowsLoadError> { + let load_info = + loader::PeLoader::new(self.0.platform, fs, &self.0.page_manager).load(path)?; let process = Arc::new(Process { ntdll_mapping: load_info.ntdll_mapping, - handles: WindowsHandleStore::new(litebox::fd::RawDescriptorStorage::new()), + handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), exit_code: AtomicI32::new(DEFAULT_PROCESS_EXIT_CODE), }); Ok(LoadedProgram { @@ -215,21 +244,22 @@ impl WindowsShim { } /// Global shim state shared by all Windows tasks loaded by this shim. -struct GlobalState { - page_manager: WindowsPageManager, - registry: syscalls::registry::RegistryStore, +struct GlobalState { + platform: &'static Platform, + page_manager: WindowsPageManager, + registry: syscalls::registry::RegistryStore, litebox: LiteBox, _fs: PhantomData, } /// Per-process Windows state shared by every thread in the process. -pub struct Process { +pub struct Process { ntdll_mapping: Option, - handles: WindowsHandleStore, + handles: WindowsHandleStore, exit_code: AtomicI32, } -impl Process { +impl Process { /// Wait for the process to exit, returning its exit code. /// /// Currently a placeholder that returns a fixed exit code immediately. @@ -241,15 +271,15 @@ impl Process { } } -struct Task { - global: Arc>, - process: Arc, +struct Task { + global: Arc>, + process: Arc>, entry_point: usize, stack_top: usize, _phantom: PhantomData, } -impl Task { +impl Task { fn init(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { ctx.rip = self.entry_point; let stack_top_alignment = self.stack_top % 16; @@ -283,8 +313,8 @@ impl Task { return ContinueOperation::Terminate; }; litebox_util_log::debug!( - syscall:? = req; - "Handling Windows" + syscall:? = NtSysno::from_raw(ctx.orig_rax); + "Handling Windows syscall" ); let (result, op) = match req { SyscallRequest::NtOpenKey { @@ -366,12 +396,12 @@ impl Task { } /// The shim entrypoint object passed to the platform. -pub struct WindowsShimEntrypoints { - task: Task, +pub struct WindowsShimEntrypoints { + task: Task, _not_send: PhantomData<*const ()>, } -impl EnterShim for WindowsShimEntrypoints { +impl EnterShim for WindowsShimEntrypoints { type ExecutionContext = litebox_common_linux::PtRegs; fn init(&self, ctx: &mut Self::ExecutionContext) -> ContinueOperation { @@ -403,25 +433,28 @@ impl EnterShim for WindowsShimEntrypoints { } /// A loaded Windows program and the process handle used to wait for it. -pub struct LoadedProgram { +pub struct LoadedProgram { /// The initial-thread entrypoint state passed to the platform's `run_thread`. - pub entrypoints: WindowsShimEntrypoints, + pub entrypoints: WindowsShimEntrypoints, /// Handle used to wait for the loaded program to exit. - pub process: Arc, + pub process: Arc>, } -fn default_fs( +fn default_fs( litebox: &LiteBox, in_mem_fs: litebox::fs::in_mem::FileSystem, tar_ro_fs: litebox::fs::tar_ro::FileSystem, -) -> WindowsFS { - let dev_stdio = litebox::fs::devices::FileSystem::new(litebox); +) -> WindowsFS +where + Platform: ShimPlatform + CrngProvider + StdioProvider, +{ + let devices = litebox::fs::devices::FileSystem::new(litebox); litebox::fs::layered::FileSystem::new( litebox, in_mem_fs, litebox::fs::layered::FileSystem::new( litebox, - dev_stdio, + devices, tar_ro_fs, litebox::fs::layered::LayeringSemantics::LowerLayerReadOnly, ), diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 2224460cef..cda8dec17f 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -2,7 +2,8 @@ // Licensed under the MIT license. use alloc::{sync::Arc, vec::Vec}; -use litebox::platform::{RawConstPointer as _, RawMutPointer as _, SystemInfoProvider as _}; +use core::marker::PhantomData; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt as _; use litebox::{ fs::{Mode, OFlags}, @@ -16,7 +17,6 @@ use litebox_common_windows::loader::{ MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, MapMemory, MappingInfo, PAGE_SIZE, PeExportError, PeLoadError, PeParseError, PeParsedFile, Protection, ReadAt, page_align_down, }; -use litebox_platform_multiplex::Platform; use thiserror::Error; use crate::ShimFS; @@ -34,20 +34,34 @@ pub(crate) struct PeLoadInfo { pub(crate) ntdll_mapping: Option, } -pub(crate) struct PeLoader<'a, FS: ShimFS> { +pub(crate) struct PeLoader<'a, Platform: crate::ShimPlatform, FS: ShimFS> { + platform: &'static Platform, fs: Arc, - page_manager: &'a crate::WindowsPageManager, + page_manager: &'a crate::WindowsPageManager, } -impl<'a, FS: ShimFS> PeLoader<'a, FS> { - pub(crate) fn new(fs: Arc, page_manager: &'a crate::WindowsPageManager) -> Self { - Self { fs, page_manager } +impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { + pub(crate) fn new( + platform: &'static Platform, + fs: Arc, + page_manager: &'a crate::WindowsPageManager, + ) -> Self { + Self { + platform, + fs, + page_manager, + } } pub(crate) fn load(&self, path: &str) -> Result { - let image = load_image(self.fs.clone(), path, self.page_manager)?; + let image = load_image(self.platform, self.fs.clone(), path, self.page_manager)?; let application_entry_point = image.mapping.entry_point; - let ntdll = load_ntdll(self.fs.clone(), self.page_manager, NTDLL_PATHS)?; + let ntdll = load_ntdll( + self.platform, + self.fs.clone(), + self.page_manager, + NTDLL_PATHS, + )?; if let Some(ntdll) = &ntdll { if !ntdll.image.parsed.has_trampoline() { @@ -105,11 +119,13 @@ impl<'a, FS: ShimFS> PeLoader<'a, FS> { }; // `KI_USER_INVERTED_FUNCTION_TABLE` lives in ntdll's writable `.mrdata` section. - crate::write_value(table_address, header).ok_or(PeImageAccessError::MemoryAccess)?; + crate::write_value::(table_address, header) + .ok_or(PeImageAccessError::MemoryAccess)?; let entries_address = table_address .checked_add(core::mem::size_of::()) .ok_or(PeImageAccessError::AddressOverflow)?; - crate::write_slice(entries_address, &entries).ok_or(PeImageAccessError::MemoryAccess)?; + crate::write_slice::(entries_address, &entries) + .ok_or(PeImageAccessError::MemoryAccess)?; litebox_util_log::debug!( table:% = format_args!("{table_address:#x}"); @@ -167,20 +183,22 @@ struct NtDllExports { ki_user_inverted_function_table: usize, } -fn load_ntdll( +fn load_ntdll( + platform: &'static Platform, fs: Arc, - page_manager: &crate::WindowsPageManager, + page_manager: &crate::WindowsPageManager, ntdll_paths: &[&str], ) -> Result, WindowsLoadError> { for path in ntdll_paths { match load_image_with_writable_sections( fs.clone(), path, + platform, page_manager, NTDLL_WRITABLE_SECTIONS, ) { Ok(image) => { - let exports = ntdll_exports(&image)?; + let exports = ntdll_exports::(&image)?; litebox_util_log::debug!(path:% = path; "Loaded guest ntdll.dll"); return Ok(Some(LoadedNtDll { image, exports })); } @@ -193,47 +211,48 @@ fn load_ntdll( Ok(None) } -fn load_image( +fn load_image( + platform: &'static Platform, fs: Arc, path: &str, - page_manager: &crate::WindowsPageManager, + page_manager: &crate::WindowsPageManager, ) -> Result { - load_image_with_writable_sections(fs, path, page_manager, &[]) + load_image_with_writable_sections(fs, path, platform, page_manager, &[]) } -fn load_image_with_writable_sections( +fn load_image_with_writable_sections( fs: Arc, path: &str, - page_manager: &crate::WindowsPageManager, + platform: &'static Platform, + page_manager: &crate::WindowsPageManager, writable_section_names: &[&[u8]], ) -> Result { let file = PeImageFile::open(fs, path)?; let mut parsed = PeParsedFile::parse(&mut &file).map_err(WindowsLoadError::Parse)?; parsed - .parse_trampoline( - &mut &file, - litebox_platform_multiplex::platform().get_syscall_entry_point(), - ) + .parse_trampoline(&mut &file, platform.get_syscall_entry_point()) .map_err(WindowsLoadError::Parse)?; let mut mapper = PeImageMapper { file: &file, page_manager, chunk: alloc::vec![0u8; FILE_CHUNK_BYTES], }; - let mut memory = PeImageMemory; + let mut memory = PeImageMemory::(PhantomData); let mapping = parsed .load_with_writable_sections(&mut mapper, &mut memory, writable_section_names) .map_err(WindowsLoadError::Load)?; Ok(LoadedImage { mapping, parsed }) } -fn ntdll_exports(image: &LoadedImage) -> Result { +fn ntdll_exports( + image: &LoadedImage, +) -> Result { let export_names = [ "LdrInitializeThunk", "RtlUserThreadStart", "KiUserInvertedFunctionTable", ]; - let mut memory = PeImageMemory; + let mut memory = PeImageMemory::(PhantomData); let addresses = image .parsed .find_export_addresses(image.mapping.base_addr, &mut memory, &export_names) @@ -358,14 +377,14 @@ impl ReadAt for &'_ PeImageFile { } } -struct PeImageMapper<'a, FS: ShimFS> { +struct PeImageMapper<'a, Platform: crate::ShimPlatform, FS: ShimFS> { file: &'a PeImageFile, - page_manager: &'a crate::WindowsPageManager, + page_manager: &'a crate::WindowsPageManager, /// Reusable per-call I/O staging buffer for [`MapMemory::map_file`]. chunk: Vec, } -impl MapMemory for PeImageMapper<'_, FS> { +impl MapMemory for PeImageMapper<'_, Platform, FS> { type Error = PeImageAccessError; fn reserve( @@ -475,9 +494,9 @@ pub enum PeImageAccessError { MemoryAccess, } -struct PeImageMemory; +struct PeImageMemory(PhantomData); -impl AccessMemory for PeImageMemory { +impl AccessMemory for PeImageMemory { fn read(&mut self, address: usize, buf: &mut [u8]) -> Result<(), Fault> { let ptr = ::RawConstPointer::::from_usize(address); buf.copy_from_slice(&ptr.to_owned_slice(buf.len()).ok_or(Fault)?); @@ -490,8 +509,8 @@ impl AccessMemory for PeImageMemory { } } -fn make_pages_writable( - page_manager: &crate::WindowsPageManager, +fn make_pages_writable( + page_manager: &crate::WindowsPageManager, address: usize, len: usize, ) -> Result<(), PeImageAccessError> { @@ -505,8 +524,8 @@ fn make_pages_writable( Ok(()) } -fn protect_pages( - page_manager: &crate::WindowsPageManager, +fn protect_pages( + page_manager: &crate::WindowsPageManager, address: usize, len: usize, prot: Protection, @@ -572,7 +591,8 @@ mod tests { let ntdll = ntdll_module_base(); let loaded_ntdll = loaded_module_image(ntdll); - let exports = ntdll_exports(&loaded_ntdll).expect("failed to parse ntdll exports"); + let exports = ntdll_exports::(&loaded_ntdll) + .expect("failed to parse ntdll exports"); let expected_table = own_inverted_function_table() as usize; assert_eq!( diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index 25d569ea03..4cef74fe86 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -2,9 +2,8 @@ // Licensed under the MIT license. use alloc::string::String; -use litebox::platform::RawConstPointer as _; +use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use litebox_common_windows::nt_status::NtStatus; -use litebox_platform_multiplex::Platform; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::{ConstPtr, syscalls::Handle}; @@ -20,8 +19,8 @@ pub(crate) struct ObjectAttributes { pub(crate) security_quality_of_service: usize, } -pub(crate) fn read_object_attributes( - object_attributes: ConstPtr, +pub(crate) fn read_object_attributes( + object_attributes: ConstPtr, ) -> Result { let Some(object_attributes) = object_attributes.read_at_offset(0) else { return Err(NtStatus::ACCESS_VIOLATION); @@ -41,24 +40,22 @@ pub(crate) struct UnicodeString { pub(crate) buffer: usize, } -impl TryFrom for String { - type Error = NtStatus; - - fn try_from(unicode_string: UnicodeString) -> Result { - if !unicode_string.length.is_multiple_of(2) { +impl UnicodeString { + pub(crate) fn read_string(self) -> Result { + if !self.length.is_multiple_of(2) { return Err(NtStatus::INVALID_PARAMETER); } - if unicode_string.length == 0 { + if self.length == 0 { return Ok(String::new()); } - if unicode_string.buffer == 0 { + if self.buffer == 0 { return Err(NtStatus::ACCESS_VIOLATION); } - let chars = usize::from(unicode_string.length / 2); + let chars = usize::from(self.length / 2); let buffer = ::RawConstPointer::::from_usize( - unicode_string.buffer, + self.buffer, ); let Some(units) = buffer.to_owned_slice(chars) else { return Err(NtStatus::ACCESS_VIOLATION); diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index e06b789392..916fccf2c2 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -40,7 +40,6 @@ use litebox::fs::{FileSystem as _, FileType, Mode, OFlags}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt; use litebox_common_windows::nt_status::NtStatus; -use litebox_platform_multiplex::Platform; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::syscalls::Handle; @@ -62,14 +61,14 @@ struct RegistryKeyObject { path: String, } -type RegistryFileSystem = litebox::fs::layered::FileSystem< +type RegistryFileSystem = litebox::fs::layered::FileSystem< Platform, litebox::fs::in_mem::FileSystem, litebox::fs::tar_ro::FileSystem, >; -pub(crate) struct RegistryStore { - fs: RegistryFileSystem, +pub(crate) struct RegistryStore { + fs: RegistryFileSystem, } const VALUES_DIR_NAME: &str = ".values"; @@ -179,7 +178,7 @@ struct RegistryValue { data: Vec, } -impl RegistryStore { +impl RegistryStore { pub(crate) fn new(litebox: &LiteBox) -> Self { let mut in_mem = litebox::fs::in_mem::FileSystem::new(litebox); in_mem.with_root_privileges(|fs| { @@ -273,24 +272,24 @@ impl RegistryStore { } } -impl Task { +impl Task { pub(crate) fn sys_nt_open_key( &self, - key_handle: MutPtr, + key_handle: MutPtr, desired_access: u32, - object_attributes: Option>, + object_attributes: Option>, ) -> NtStatus { let Some(object_attributes) = object_attributes else { return NtStatus::INVALID_PARAMETER; }; - let object_attributes = match read_object_attributes(object_attributes) { + let object_attributes = match read_object_attributes::(object_attributes) { Ok(object_attributes) => object_attributes, Err(status) => return status, }; match self.do_nt_open_key(desired_access, object_attributes) { Ok(handle) => { if key_handle.write_at_offset(0, handle).is_none() { - remove_raw_handle::( + remove_raw_handle::( &self.global.litebox, &self.process.handles, handle, @@ -313,15 +312,16 @@ impl Task { return Err(NtStatus::INVALID_PARAMETER); } - let object_name_ptr = ConstPtr::::from_usize(object_attributes.object_name); + let object_name_ptr = + ConstPtr::::from_usize(object_attributes.object_name); let object_name = object_name_ptr .read_at_offset(0) .ok_or(NtStatus::ACCESS_VIOLATION)?; - let key_name = String::try_from(object_name)?; + let key_name = object_name.read_string::()?; let path = if object_attributes.root_directory.is_null() || key_name.starts_with('\\') { absolute_nt_key_name_to_fs_path(&key_name)? } else { - let root_key = raw_handle_entry::( + let root_key = raw_handle_entry::( &self.global.litebox, &self.process.handles, object_attributes.root_directory, @@ -354,17 +354,17 @@ impl Task { let typed = descriptor_table.insert::(key); drop(descriptor_table); - insert_raw_handle(&self.global.litebox, &self.process.handles, typed) + insert_raw_handle::(&self.global.litebox, &self.process.handles, typed) } pub(crate) fn sys_nt_query_value_key( &self, key_handle: Handle, - value_name: ConstPtr, + value_name: ConstPtr, key_value_information_class: u32, - key_value_information: MutPtr, + key_value_information: MutPtr, length: u32, - result_length: MutPtr, + result_length: MutPtr, ) -> NtStatus { let Some(value_name) = value_name.read_at_offset(0) else { return NtStatus::ACCESS_VIOLATION; @@ -396,17 +396,17 @@ impl Task { key_handle: Handle, value_name: UnicodeString, key_value_information_class: KeyValueInformationClass, - key_value_information: MutPtr, + key_value_information: MutPtr, length: u32, - result_length: MutPtr, + result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = raw_handle_entry::( + let key = raw_handle_entry::( &self.global.litebox, &self.process.handles, key_handle, ) .ok_or(NtStatus::INVALID_HANDLE)?; - let value_name = String::try_from(value_name)?; + let value_name = value_name.read_string::()?; let value = key.with_entry(|key| self.global.registry.read_value(&key.path, &value_name))?; let name = utf16le(&value_name); @@ -421,8 +421,8 @@ impl Task { name_length: name.len().trunc(), name: [0u8; 0], }; - write_query_result_length(result_length, length, required_length)?; - write_query_information( + write_query_result_length::(result_length, length, required_length)?; + write_query_information::( key_value_information, information.as_bytes(), &[(offset_of!(KeyValueBasicInformation, name), name.as_slice())], @@ -438,7 +438,7 @@ impl Task { let required_length = data_offset .checked_add(value.data.len()) .ok_or(NtStatus::UNSUCCESSFUL)?; - write_query_result_length(result_length, length, required_length)?; + write_query_result_length::(result_length, length, required_length)?; let information = KeyValueFullInformation { title_index: 0, value_type: value.value_type.into(), @@ -448,7 +448,7 @@ impl Task { name: [0u8; 0], }; - write_query_information( + write_query_information::( key_value_information, information.as_bytes(), &[ @@ -461,7 +461,7 @@ impl Task { let required_length = size_of::() .checked_add(value.data.len()) .ok_or(NtStatus::UNSUCCESSFUL)?; - write_query_result_length(result_length, length, required_length)?; + write_query_result_length::(result_length, length, required_length)?; let information = KeyValuePartialInformation { title_index: 0, value_type: value.value_type.into(), @@ -469,7 +469,7 @@ impl Task { data: [0u8; 0], }; - write_query_information( + write_query_information::( key_value_information, information.as_bytes(), &[( @@ -492,8 +492,8 @@ impl Task { } } -fn write_query_result_length( - result_length: MutPtr, +fn write_query_result_length( + result_length: MutPtr, buffer_length: u32, required_length: usize, ) -> Result<(), NtStatus> { @@ -506,8 +506,8 @@ fn write_query_result_length( Ok(()) } -fn write_query_information( - key_value_information: MutPtr, +fn write_query_information( + key_value_information: MutPtr, header: &[u8], trailing_slices: &[(usize, &[u8])], ) -> Result<(), NtStatus> { @@ -731,7 +731,7 @@ fn map_read_error(error: ReadError) -> NtStatus { #[cfg(test)] mod tests { - use crate::tests::init_platform; + use crate::tests::{TestFS, TestPlatform, test_platform}; use super::*; use core::mem::size_of; @@ -774,16 +774,16 @@ mod tests { fn RegCloseKey(hKey: *mut core::ffi::c_void) -> i32; } - fn const_ptr(value: &T) -> ConstPtr { - ConstPtr::from_usize(core::ptr::from_ref(value).cast::() as usize) + fn const_ptr(value: &T) -> ConstPtr { + ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) } - fn mut_ptr(value: &mut T) -> MutPtr { - MutPtr::from_usize(core::ptr::from_mut(value).cast::() as usize) + fn mut_ptr(value: &mut T) -> MutPtr { + MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) } - fn mut_byte_ptr(value: &mut T) -> MutPtr { - MutPtr::from_usize(core::ptr::from_mut(value).cast::() as usize) + fn mut_byte_ptr(value: &mut T) -> MutPtr { + MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) } fn unicode_string(value: &[u16]) -> UnicodeString { @@ -811,21 +811,20 @@ mod tests { } } - fn test_registry() -> (LiteBox, RegistryStore) { - init_platform(); - let litebox = LiteBox::new(litebox_platform_multiplex::platform()); + fn test_registry() -> (LiteBox, RegistryStore) { + let litebox = LiteBox::new(test_platform()); let registry = RegistryStore::new(&litebox); (litebox, registry) } fn open_key( - task: &Task, + task: &Task, object_attributes: ObjectAttributes, ) -> Result { task.do_nt_open_key(0x20019, object_attributes) } - fn open_code_page_key(task: &Task) -> Handle { + fn open_code_page_key(task: &Task) -> Handle { 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); diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index f2e4c0c74d..76a285af07 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -11,26 +11,32 @@ use litebox::fd::RawDescriptorStorage; use crate::{DefaultFS, GlobalState, Process, Task, WindowsHandleStore, WindowsPageManager}; -pub(crate) fn init_platform() { - static PLATFORM_INIT: std::sync::Once = std::sync::Once::new(); - PLATFORM_INIT.call_once(|| { +#[cfg(target_os = "linux")] +pub(crate) type TestPlatform = litebox_platform_linux_userland::LinuxUserland; +#[cfg(target_os = "windows")] +pub(crate) type TestPlatform = litebox_platform_windows_userland::WindowsUserland; +pub(crate) type TestFS = DefaultFS; + +pub(crate) fn test_platform() -> &'static TestPlatform { + static PLATFORM: std::sync::OnceLock<&'static TestPlatform> = std::sync::OnceLock::new(); + PLATFORM.get_or_init(|| { #[cfg(target_os = "linux")] - let platform = crate::Platform::new(None); + let platform = TestPlatform::new(None); - #[cfg(not(target_os = "linux"))] - let platform = crate::Platform::new(); + #[cfg(target_os = "windows")] + let platform = TestPlatform::new(); - litebox_platform_multiplex::set_platform(platform); - }); + platform + }) } -pub(crate) fn test_task() -> Task { - init_platform(); - let platform = litebox_platform_multiplex::platform(); +pub(crate) fn test_task() -> Task { + let platform = test_platform(); let litebox = LiteBox::new(platform); - let page_manager = WindowsPageManager::new(&litebox); + let page_manager = WindowsPageManager::::new(&litebox); Task { global: Arc::new(GlobalState { + platform, registry: crate::syscalls::registry::RegistryStore::new(&litebox), litebox, page_manager, @@ -38,7 +44,7 @@ pub(crate) fn test_task() -> Task { }), process: Arc::new(Process { ntdll_mapping: None, - handles: WindowsHandleStore::new(RawDescriptorStorage::new()), + handles: WindowsHandleStore::::new(RawDescriptorStorage::new()), exit_code: AtomicI32::new(0), }), entry_point: 0, From 1819fb7826b375d410240da2b6efd9895d894159 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Mon, 1 Jun 2026 10:19:01 -0700 Subject: [PATCH 015/319] Fix Windows registry handling (#888) This PR refactors the code for `NtOpenKey` and `NtQueryValueKey` to take into account the access permissions. --- Cargo.lock | 1 + litebox_shim_windows/Cargo.toml | 1 + litebox_shim_windows/src/lib.rs | 12 +- litebox_shim_windows/src/nt_types.rs | 27 ++ litebox_shim_windows/src/syscalls/registry.rs | 421 +++++++++++++++--- 5 files changed, 384 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a6caf7920..74a8729d4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1779,6 +1779,7 @@ dependencies = [ name = "litebox_shim_windows" version = "0.1.0" dependencies = [ + "bitflags 2.11.0", "int-enum", "litebox", "litebox_common_linux", diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml index 8eb8412df4..fbae828f9a 100644 --- a/litebox_shim_windows/Cargo.toml +++ b/litebox_shim_windows/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +bitflags = { version = "2.9.0", default-features = false } int-enum = "1.2.0" litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index e910f61c6d..514fda1dc6 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -45,6 +45,7 @@ pub trait ShimPlatform: + RawPointerProvider + PageManagementProvider + SystemInfoProvider + + 'static { } @@ -53,6 +54,7 @@ impl ShimPlatform for T where + RawPointerProvider + PageManagementProvider + SystemInfoProvider + + 'static { } @@ -109,6 +111,7 @@ pub(crate) fn insert_raw_handle, handles: &WindowsHandleStore, typed: litebox::fd::TypedFd, + cleanup_entry: impl FnOnce(Subsystem::Entry), ) -> Result where Platform: RawSyncPrimitivesProvider, @@ -118,8 +121,8 @@ where let Some(handle) = syscalls::Handle::from_raw_fd(raw_fd) else { let typed = handles.fd_consume_raw_integer::(raw_fd).ok(); drop(handles); - if let Some(typed) = typed { - let _ = litebox.descriptor_table_mut().remove(&typed); + if let Some(entry) = typed.and_then(|typed| litebox.descriptor_table_mut().remove(&typed)) { + cleanup_entry(entry); } return Err(NtStatus::QUOTA_EXCEEDED); }; @@ -146,6 +149,7 @@ pub(crate) fn remove_raw_handle, handles: &WindowsHandleStore, handle: syscalls::Handle, + cleanup_entry: impl FnOnce(Subsystem::Entry), ) where Platform: RawSyncPrimitivesProvider, { @@ -156,8 +160,8 @@ pub(crate) fn remove_raw_handle(raw_fd).ok() }; - if let Some(typed) = typed { - let _ = litebox.descriptor_table_mut().remove(&typed); + if let Some(entry) = typed.and_then(|typed| litebox.descriptor_table_mut().remove(&typed)) { + cleanup_entry(entry); } } diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index 4cef74fe86..3097598cd4 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -8,6 +8,33 @@ use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::{ConstPtr, syscalls::Handle}; +bitflags::bitflags! { + /// Common Windows object-manager `ACCESS_MASK` rights shared by NT object types. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct AccessMask: u32 { + const DELETE = 0x0001_0000; + const READ_CONTROL = 0x0002_0000; + const WRITE_DAC = 0x0004_0000; + const WRITE_OWNER = 0x0008_0000; + const SYNCHRONIZE = 0x0010_0000; + const STANDARD_RIGHTS_READ = Self::READ_CONTROL.bits(); + const STANDARD_RIGHTS_WRITE = Self::READ_CONTROL.bits(); + const STANDARD_RIGHTS_EXECUTE = Self::READ_CONTROL.bits(); + const STANDARD_RIGHTS_ALL = Self::DELETE.bits() + | Self::READ_CONTROL.bits() + | Self::WRITE_DAC.bits() + | Self::WRITE_OWNER.bits() + | Self::SYNCHRONIZE.bits(); + + const GENERIC_ALL = 0x1000_0000; + const GENERIC_EXECUTE = 0x2000_0000; + const GENERIC_WRITE = 0x4000_0000; + const GENERIC_READ = 0x8000_0000; + + const _ = !0; + } +} + #[repr(C)] #[derive(Clone, Copy, Debug, FromBytes, Immutable)] pub(crate) struct ObjectAttributes { diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 916fccf2c2..8004c2bc8d 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -24,6 +24,7 @@ //! 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::string::String; @@ -32,7 +33,7 @@ use alloc::vec::Vec; use int_enum::IntEnum; use litebox::LiteBox; -use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}; use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadError, WriteError, }; @@ -47,26 +48,28 @@ use crate::{ ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, raw_handle_entry, remove_raw_handle, }; -use crate::nt_types::{ObjectAttributes, UnicodeString, read_object_attributes}; +use crate::nt_types::{AccessMask, ObjectAttributes, UnicodeString, read_object_attributes}; -struct RegistryKeySubsystem; +type RegistryFileSystem = litebox::fs::layered::FileSystem< + Platform, + litebox::fs::in_mem::FileSystem, + litebox::fs::tar_ro::FileSystem, +>; -impl FdEnabledSubsystem for RegistryKeySubsystem { - type Entry = RegistryKeyObject; +struct RegistryKeySubsystem(PhantomData); + +impl FdEnabledSubsystem for RegistryKeySubsystem { + type Entry = RegistryKeyObject; } -impl FdEnabledSubsystemEntry for RegistryKeyObject {} +impl FdEnabledSubsystemEntry for RegistryKeyObject {} -struct RegistryKeyObject { +struct RegistryKeyObject { path: String, + fd: TypedFd>, + granted_access: RegistryKeyAccess, } -type RegistryFileSystem = litebox::fs::layered::FileSystem< - Platform, - litebox::fs::in_mem::FileSystem, - litebox::fs::tar_ro::FileSystem, ->; - pub(crate) struct RegistryStore { fs: RegistryFileSystem, } @@ -84,6 +87,77 @@ const DEFAULT_OEMCP_VALUE: &[u8] = &[b'4', 0, b'3', 0, b'7', 0, 0, 0]; const DEFAULT_MACCP_VALUE: &[u8] = &[b'1', 0, b'0', 0, b'0', 0, b'0', 0, b'0', 0, 0, 0]; const REGISTRY_VALUE_TYPE_SIZE: usize = size_of::(); +bitflags::bitflags! { + /// Registry key `ACCESS_MASK` rights accepted by `NtOpenKey`/`NtCreateKey`. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct RegistryKeyAccess: u32 { + const QUERY_VALUE = 0x0001; + const SET_VALUE = 0x0002; + const CREATE_SUB_KEY = 0x0004; + const ENUMERATE_SUB_KEYS = 0x0008; + const NOTIFY = 0x0010; + const CREATE_LINK = 0x0020; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() + | Self::QUERY_VALUE.bits() + | Self::ENUMERATE_SUB_KEYS.bits() + | Self::NOTIFY.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits() + | Self::SET_VALUE.bits() + | Self::CREATE_SUB_KEY.bits(); + const EXECUTE = Self::READ.bits(); + const ALL_ACCESS = (AccessMask::STANDARD_RIGHTS_ALL.bits() + | Self::QUERY_VALUE.bits() + | Self::SET_VALUE.bits() + | Self::CREATE_SUB_KEY.bits() + | Self::ENUMERATE_SUB_KEYS.bits() + | Self::NOTIFY.bits() + | Self::CREATE_LINK.bits()) + & !AccessMask::SYNCHRONIZE.bits(); + + const FS_READ_ACCESS = Self::QUERY_VALUE.bits() + | Self::ENUMERATE_SUB_KEYS.bits() + | Self::NOTIFY.bits() + | AccessMask::GENERIC_READ.bits() + | AccessMask::GENERIC_EXECUTE.bits() + | AccessMask::GENERIC_ALL.bits(); + const FS_WRITE_ACCESS = Self::SET_VALUE.bits() + | Self::CREATE_SUB_KEY.bits() + | Self::CREATE_LINK.bits() + | AccessMask::DELETE.bits() + | AccessMask::WRITE_DAC.bits() + | AccessMask::WRITE_OWNER.bits() + | AccessMask::GENERIC_WRITE.bits() + | AccessMask::GENERIC_ALL.bits(); + + const _ = !0; + } +} + +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); + + let access = match (wants_read, wants_write) { + (true, true) => OFlags::RDWR, + (false, true) => OFlags::WRONLY, + _ => OFlags::RDONLY, + }; + access | OFlags::DIRECTORY + } +} + +impl RegistryKeyAccess { + fn can_query_value(self) -> bool { + const QUERY_ACCESS_BITS: u32 = RegistryKeyAccess::QUERY_VALUE.bits() + | AccessMask::GENERIC_READ.bits() + | AccessMask::GENERIC_EXECUTE.bits() + | AccessMask::GENERIC_ALL.bits(); + self.intersects(Self::from_bits_retain(QUERY_ACCESS_BITS)) + } +} + /// System-defined `REG_*` value types stored in `KEY_VALUE_*_INFORMATION::Type`. #[repr(u32)] #[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] @@ -220,20 +294,21 @@ impl RegistryStore { Self { fs } } - fn key_exists(&self, path: &str) -> Result { - match self.fs.file_status(path) { - Ok(status) => Ok(status.file_type == FileType::Directory), - Err(FileStatusError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - )) => Ok(false), - Err(FileStatusError::PathError(error)) => { - Err(map_path_error(error, NtStatus::OBJECT_NAME_NOT_FOUND)) - } - Err(_) => Err(NtStatus::UNSUCCESSFUL), - } + fn open_key( + &self, + path: &str, + desired_access: RegistryKeyAccess, + ) -> Result>, NtStatus> { + self.fs + .open(path, desired_access.into(), Mode::empty()) + .map_err(map_open_error) } - fn read_value(&self, key_path: &str, value_name: &str) -> Result { + fn read_value_at_path( + &self, + key_path: &str, + value_name: &str, + ) -> Result { let value_path = value_path(key_path, value_name)?; let status = self .fs @@ -273,6 +348,48 @@ impl RegistryStore { } impl Task { + fn registry_key_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + raw_handle_entry::>( + &self.global.litebox, + &self.process.handles, + handle, + ) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn insert_registry_key_handle( + &self, + key: RegistryKeyObject, + ) -> Result { + let typed = self + .global + .litebox + .descriptor_table_mut() + .insert::>(key); + insert_raw_handle::>( + &self.global.litebox, + &self.process.handles, + typed, + |key| self.close_registry_key(key), + ) + } + + fn remove_registry_key_handle(&self, handle: Handle) { + remove_raw_handle::>( + &self.global.litebox, + &self.process.handles, + handle, + |key| self.close_registry_key(key), + ); + } + + fn close_registry_key(&self, key: RegistryKeyObject) { + let _ = self.global.registry.fs.close(&key.fd); + } + pub(crate) fn sys_nt_open_key( &self, key_handle: MutPtr, @@ -289,11 +406,7 @@ impl Task { match self.do_nt_open_key(desired_access, object_attributes) { Ok(handle) => { if key_handle.write_at_offset(0, handle).is_none() { - remove_raw_handle::( - &self.global.litebox, - &self.process.handles, - handle, - ); + self.remove_registry_key_handle(handle); return NtStatus::ACCESS_VIOLATION; } @@ -321,40 +434,33 @@ impl Task { let path = if object_attributes.root_directory.is_null() || key_name.starts_with('\\') { absolute_nt_key_name_to_fs_path(&key_name)? } else { - let root_key = raw_handle_entry::( - &self.global.litebox, - &self.process.handles, - object_attributes.root_directory, - ) - .ok_or(NtStatus::INVALID_HANDLE)?; + let root_key = self.registry_key_entry(object_attributes.root_directory)?; root_key .with_entry(|root_key| relative_nt_key_name_to_fs_path(&root_key.path, &key_name))? }; - match self.global.registry.key_exists(&path) { - Ok(true) => {} - Ok(false) => { - return Err(NtStatus::OBJECT_NAME_NOT_FOUND); - } - Err(status) => { - litebox_util_log::debug!( - desired_access:% = format_args!("{desired_access:#x}"), - root_directory:% = format_args!("{:#x}", object_attributes.root_directory.as_raw()), - name:% = key_name, - path:% = path, - status:? = status; - "NtOpenKey failed" - ); - return Err(status); - } - } - - let key = RegistryKeyObject { path }; - let mut descriptor_table = self.global.litebox.descriptor_table_mut(); - let typed = descriptor_table.insert::(key); - drop(descriptor_table); - - insert_raw_handle::(&self.global.litebox, &self.process.handles, typed) + let desired_access = RegistryKeyAccess::from_bits_retain(desired_access); + let fd = self + .global + .registry + .open_key(&path, desired_access) + .inspect_err(|status| { + if *status != NtStatus::OBJECT_NAME_NOT_FOUND { + litebox_util_log::debug!( + desired_access:? = desired_access, + root_directory:% = format_args!("{:#x}", object_attributes.root_directory.as_raw()), + name:% = key_name, + path:% = path, + status:? = status; + "NtOpenKey failed" + ); + } + })?; + self.insert_registry_key_handle(RegistryKeyObject { + path, + fd, + granted_access: desired_access, + }) } pub(crate) fn sys_nt_query_value_key( @@ -400,15 +506,17 @@ impl Task { length: u32, result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = raw_handle_entry::( - &self.global.litebox, - &self.process.handles, - key_handle, - ) - .ok_or(NtStatus::INVALID_HANDLE)?; + let key = self.registry_key_entry(key_handle)?; let value_name = value_name.read_string::()?; - let value = - key.with_entry(|key| self.global.registry.read_value(&key.path, &value_name))?; + let value = key.with_entry(|key| { + if !key.granted_access.can_query_value() { + return Err(NtStatus::ACCESS_DENIED); + } + // TODO: Open the value relative to `key.fd` once the FS has an openat-style API. + self.global + .registry + .read_value_at_path(&key.path, &value_name) + })?; let name = utf16le(&value_name); match key_value_information_class { KeyValueInformationClass::Basic => { @@ -740,14 +848,18 @@ mod tests { extern crate std; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const ERROR_ACCESS_DENIED: i32 = 5; #[cfg(all(target_os = "windows", target_arch = "x86_64"))] const ERROR_SUCCESS: i32 = 0; #[cfg(all(target_os = "windows", target_arch = "x86_64"))] - const HKEY_LOCAL_MACHINE: *mut core::ffi::c_void = 0xffffffff80000002usize as _; + const HKEY_CURRENT_USER: *mut core::ffi::c_void = 0xffffffff80000001usize as _; #[cfg(all(target_os = "windows", target_arch = "x86_64"))] - const KEY_QUERY_VALUE: u32 = 0x0001; + const HKEY_LOCAL_MACHINE: *mut core::ffi::c_void = 0xffffffff80000002usize as _; #[cfg(all(target_os = "windows", target_arch = "x86_64"))] const HOST_CODE_PAGE_KEY: &str = "SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage"; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const HOST_ACCESS_TEST_KEY: &str = "Software\\LiteBoxRegistryAccessTest"; const KEY_VALUE_PARTIAL_INFORMATION_DATA_OFFSET: usize = offset_of!(KeyValuePartialInformation, data); @@ -756,6 +868,17 @@ mod tests { #[allow(non_snake_case)] #[link(name = "advapi32")] unsafe extern "system" { + fn RegCreateKeyExW( + hKey: *mut core::ffi::c_void, + lpSubKey: *const u16, + Reserved: u32, + lpClass: *const u16, + dwOptions: u32, + samDesired: u32, + lpSecurityAttributes: *const core::ffi::c_void, + phkResult: *mut *mut core::ffi::c_void, + lpdwDisposition: *mut u32, + ) -> i32; fn RegOpenKeyExW( hKey: *mut core::ffi::c_void, lpSubKey: *const u16, @@ -771,7 +894,16 @@ mod tests { lpData: *mut u8, lpcbData: *mut u32, ) -> i32; + fn RegSetValueExW( + hKey: *mut core::ffi::c_void, + lpValueName: *const u16, + Reserved: u32, + dwType: u32, + lpData: *const u8, + cbData: u32, + ) -> i32; fn RegCloseKey(hKey: *mut core::ffi::c_void) -> i32; + fn RegDeleteTreeW(hKey: *mut core::ffi::c_void, lpSubKey: *const u16) -> i32; } fn const_ptr(value: &T) -> ConstPtr { @@ -821,7 +953,7 @@ mod tests { task: &Task, object_attributes: ObjectAttributes, ) -> Result { - task.do_nt_open_key(0x20019, object_attributes) + task.do_nt_open_key(RegistryKeyAccess::READ.bits(), object_attributes) } fn open_code_page_key(task: &Task) -> Handle { @@ -850,7 +982,7 @@ mod tests { HKEY_LOCAL_MACHINE, key_path.as_ptr(), 0, - KEY_QUERY_VALUE, + RegistryKeyAccess::QUERY_VALUE.bits(), &raw mut key, ) }; @@ -898,13 +1030,95 @@ mod tests { } } + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn host_query_value_with_set_only_access() -> i32 { + let key_path = nul_terminated_utf16(HOST_ACCESS_TEST_KEY); + let value_name = nul_terminated_utf16("Value"); + let mut key = core::ptr::null_mut(); + // SAFETY: The key path is NUL-terminated, output pointers are live slots, + // and `HKEY_CURRENT_USER` is the documented predefined registry handle. + let status = unsafe { + RegCreateKeyExW( + HKEY_CURRENT_USER, + key_path.as_ptr(), + 0, + core::ptr::null(), + 0, + RegistryKeyAccess::QUERY_VALUE.bits() | RegistryKeyAccess::SET_VALUE.bits(), + core::ptr::null(), + &raw mut key, + core::ptr::null_mut(), + ) + }; + assert_eq!(status, ERROR_SUCCESS, "failed to create host test key"); + + let data = [b'x', 0, 0, 0]; + // SAFETY: The key handle was returned by `RegCreateKeyExW`, the value name + // is NUL-terminated, and `data` is valid for the specified byte length. + let status = unsafe { + RegSetValueExW( + key, + value_name.as_ptr(), + 0, + RegistryValueType::Sz.into(), + data.as_ptr(), + data.len().trunc(), + ) + }; + assert_eq!(status, ERROR_SUCCESS, "failed to seed host test value"); + // SAFETY: The key handle was returned by `RegCreateKeyExW` and has not + // been closed yet. + let status = unsafe { RegCloseKey(key) }; + assert_eq!(status, ERROR_SUCCESS, "failed to close host test key"); + + // SAFETY: The key path is NUL-terminated, `phkResult` points to a live + // output slot, and `HKEY_CURRENT_USER` is the documented predefined handle. + let status = unsafe { + RegOpenKeyExW( + HKEY_CURRENT_USER, + key_path.as_ptr(), + 0, + RegistryKeyAccess::SET_VALUE.bits(), + &raw mut key, + ) + }; + assert_eq!(status, ERROR_SUCCESS, "failed to reopen host test key"); + + let mut value_type = 0; + let mut data_len = 0; + // SAFETY: The key handle was returned by `RegOpenKeyExW`, the value name + // is NUL-terminated, and the null data buffer requests the required length. + let query_status = unsafe { + RegQueryValueExW( + key, + value_name.as_ptr(), + core::ptr::null_mut(), + &raw mut value_type, + core::ptr::null_mut(), + &raw mut data_len, + ) + }; + + // SAFETY: The key handle was returned by `RegOpenKeyExW` and has not been closed yet. + let close_status = unsafe { RegCloseKey(key) }; + assert_eq!(close_status, ERROR_SUCCESS, "failed to close host test key"); + // SAFETY: The key path is NUL-terminated and rooted under the documented + // predefined `HKEY_CURRENT_USER` handle. + let delete_status = unsafe { RegDeleteTreeW(HKEY_CURRENT_USER, key_path.as_ptr()) }; + assert_eq!( + delete_status, ERROR_SUCCESS, + "failed to delete host test key" + ); + + query_status + } + #[test] fn registry_store_separates_values_from_subkeys() { 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.key_exists(&key_path), Ok(true)); assert_eq!( registry.fs.file_status(&*value_path).unwrap().file_type, FileType::RegularFile @@ -913,7 +1127,7 @@ mod tests { registry.fs.file_status(&*value_path).unwrap().size, REGISTRY_VALUE_TYPE_SIZE + DEFAULT_ACP_VALUE.len() ); - let value = registry.read_value(&key_path, "ACP").unwrap(); + let value = registry.read_value_at_path(&key_path, "ACP").unwrap(); assert_eq!(value.value_type, RegistryValueType::Sz); assert_eq!(value.data, DEFAULT_ACP_VALUE); @@ -1002,6 +1216,34 @@ mod tests { ); } + #[test] + fn nt_open_key_checks_backing_fs_permissions() { + let task = crate::tests::test_task(); + let private_key = "\\Registry\\Machine\\Software\\Private"; + let private_path = create_key_in_fs(&task.global.registry.fs, private_key).unwrap(); + task.global + .registry + .fs + .chmod(&*private_path, Mode::WUSR | Mode::XUSR) + .unwrap(); + + let private_name = utf16(private_key); + let private_name = unicode_string(&private_name); + let read_object_attributes = object_attributes(&private_name); + assert_eq!( + open_key(&task, read_object_attributes).unwrap_err(), + NtStatus::ACCESS_DENIED + ); + + let private_name = utf16(private_key); + let private_name = unicode_string(&private_name); + let write_object_attributes = object_attributes(&private_name); + let handle = task + .do_nt_open_key(RegistryKeyAccess::SET_VALUE.bits(), write_object_attributes) + .expect("write-only access should use write filesystem permissions"); + assert_ne!(handle, Handle::default()); + } + #[test] fn nt_query_value_key_reports_partial_information() { let task = crate::tests::test_task(); @@ -1039,6 +1281,37 @@ mod tests { assert_eq!(data, DEFAULT_ACP_VALUE); } + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + 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 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); + let key_handle = task + .do_nt_open_key(RegistryKeyAccess::SET_VALUE.bits(), object_attributes) + .expect("write-only open should succeed against the seeded registry store"); + let value_name = utf16("ACP"); + let value_name = unicode_string(&value_name); + let mut information = [0u8; 64]; + let mut result_length = 0; + + assert_eq!( + task.do_nt_query_value_key( + key_handle, + value_name, + KeyValueInformationClass::Partial, + mut_byte_ptr(&mut information), + u32::try_from(information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .unwrap_err(), + NtStatus::ACCESS_DENIED + ); + } + #[test] fn nt_query_value_key_reports_basic_and_full_information() { let task = crate::tests::test_task(); From 4d72f8e7b8d8256c92c505f1a86b445a3682254f Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Mon, 1 Jun 2026 11:24:54 -0700 Subject: [PATCH 016/319] Add syscall recvmmsg to ulitebox (#889) Cherry-picks 1d2d4caedf9804b53f89a069bf76748d7c4dc1cb (`Add syscall recvmmsg (#861)`) onto `ulitebox`. --- litebox/src/net/socket_channel.rs | 145 ++++-- litebox_common_linux/src/errno/mod.rs | 28 ++ litebox_common_linux/src/lib.rs | 19 + .../tests/recvmmsg.c | 470 ++++++++++++++++++ litebox_shim_linux/src/lib.rs | 7 + litebox_shim_linux/src/syscalls/net.rs | 140 +++++- litebox_shim_linux/src/transport.rs | 4 +- 7 files changed, 760 insertions(+), 53 deletions(-) create mode 100644 litebox_runner_linux_userland/tests/recvmmsg.c diff --git a/litebox/src/net/socket_channel.rs b/litebox/src/net/socket_channel.rs index 9505203ed8..766ab9f8b4 100644 --- a/litebox/src/net/socket_channel.rs +++ b/litebox/src/net/socket_channel.rs @@ -59,9 +59,6 @@ use crate::sync::{Mutex, RawSyncPrimitivesProvider}; use crate::{ event::{Events, IOPollable, observer::Observer, polling::Pollee}, net::ReceiveFlags, -}; -use crate::{ - net::errors::{ReceiveError, SendError}, platform::TimeProvider, }; @@ -120,8 +117,8 @@ impl SocketAsyncErrorState { #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u32)] pub enum SocketState { - /// Socket is closed or in initial state - Closed = 0, + /// Socket is in initial state. + Initial = 0, /// Socket is connecting (TCP SYN sent) Connecting = 1, /// Socket is connected and ready for data transfer @@ -130,15 +127,46 @@ pub enum SocketState { Listening = 3, /// Socket encountered an error Error = 4, + /// Socket is closed. + Closed = 5, +} + +/// Possible errors from [`NetworkProxy::try_read`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChannelReadError { + /// The local read side has been shut down. + ReadShutdown, + /// The stream has not reached a connected state. + NotConnected, + /// The stream is closed. + ConnectionClosed, +} + +/// Possible errors from [`NetworkProxy::try_write`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChannelWriteError { + /// The local write side has been shut down. + WriteShutdown, + /// The stream has not reached a connected state. + NotConnected, + /// The stream is closed. + ConnectionClosed, + /// The destination address cannot be used. + Unaddressable, + /// The transmit buffer is full. + BufferFull, + /// A destination address is required but was not provided. + DestinationAddressRequired, } impl From for SocketState { fn from(v: u32) -> Self { match v { - 0 => SocketState::Closed, + 0 => SocketState::Initial, 1 => SocketState::Connecting, 2 => SocketState::Connected, 3 => SocketState::Listening, + 5 => SocketState::Closed, _ => SocketState::Error, } } @@ -175,14 +203,10 @@ impl NetworkProxy } /// Set the async socket error. - pub(super) fn set_async_error(&self, error: super::errors::SocketAsyncError) { + pub fn set_async_error(&self, error: super::errors::SocketAsyncError) { match self { - NetworkProxy::Stream(channel) => { - channel.set_async_error(error); - } - NetworkProxy::Datagram(channel) => { - channel.set_async_error(error); - } + NetworkProxy::Stream(channel) => channel.set_async_error(error), + NetworkProxy::Datagram(channel) => channel.set_async_error(error), NetworkProxy::Raw => {} } } @@ -205,7 +229,7 @@ impl NetworkProxy buf: &mut [u8], flags: super::ReceiveFlags, source_addr: Option<&mut Option>, - ) -> Result { + ) -> Result { match self { NetworkProxy::Stream(channel) => channel.try_read(buf, flags, source_addr), NetworkProxy::Datagram(channel) => channel.try_read(buf, flags, source_addr), @@ -222,14 +246,14 @@ impl NetworkProxy buf: &[u8], flags: super::SendFlags, destination: Option, - ) -> Result { + ) -> Result { if !flags.is_empty() { unimplemented!() } if let Some(addr) = destination && (addr.port() == 0 || addr.ip().is_unspecified()) { - return Err(SendError::Unaddressable); + return Err(ChannelWriteError::Unaddressable); } match self { NetworkProxy::Stream(channel) => channel.try_write(buf), @@ -331,7 +355,7 @@ impl StreamChannelInner StreamSocketChannel>, - ) -> Result { + ) -> Result { if self.inner.read_shutdown.load(Ordering::Acquire) { - return Err(ReceiveError::SocketInInvalidState); - } - - match self.inner.state() { - SocketState::Connected => {} - _ => return Err(ReceiveError::SocketInInvalidState), + return Err(ChannelReadError::ReadShutdown); } let mut rx_cons = self.inner.rx_cons.lock(); @@ -418,7 +437,15 @@ impl StreamSocketChannel 0 { + return Ok(n); + } + match self.inner.state() { + SocketState::Connected => Ok(0), + SocketState::Closed | SocketState::Error => Err(ChannelReadError::ConnectionClosed), + _ => Err(ChannelReadError::NotConnected), + } } /// Write data to the socket from the provided buffer. @@ -428,14 +455,17 @@ impl StreamSocketChannel Result { + pub fn try_write(&self, buf: &[u8]) -> Result { if self.inner.write_shutdown.load(Ordering::Acquire) { - return Err(SendError::SocketInInvalidState); + return Err(ChannelWriteError::WriteShutdown); } match self.state() { SocketState::Connected => {} - _ => return Err(SendError::SocketInInvalidState), + SocketState::Closed | SocketState::Error => { + return Err(ChannelWriteError::ConnectionClosed); + } + _ => return Err(ChannelWriteError::NotConnected), } let mut tx_prod = self.inner.tx_prod.lock(); @@ -446,7 +476,7 @@ impl StreamSocketChannel IOPollable } match self.inner.state() { - SocketState::Closed => events |= Events::HUP | Events::OUT, + SocketState::Initial | SocketState::Closed => events |= Events::HUP | Events::OUT, SocketState::Error => events |= Events::ERR | Events::OUT, SocketState::Connected if self.is_writable() => events |= Events::OUT, _ => {} @@ -791,7 +821,7 @@ impl DatagramSocketChannel

>, - ) -> Result { + ) -> Result { let mut rx_cons = self.inner.rx_cons.lock(); if let Some(msg) = rx_cons.try_pop() { @@ -814,10 +844,14 @@ impl DatagramSocketChannel

) -> Result { + pub fn send_to( + &self, + data: &[u8], + addr: Option, + ) -> Result { if addr.is_none() && !self.inner.is_connected.load(Ordering::Acquire) { // No destination specified and socket is not connected - return Err(SendError::DestinationAddressRequired); + return Err(ChannelWriteError::DestinationAddressRequired); } let size = data.len(); @@ -832,7 +866,7 @@ impl DatagramSocketChannel

Err(SendError::BufferFull), + Err(_) => Err(ChannelWriteError::BufferFull), } } @@ -1006,8 +1040,7 @@ mod tests { fn stream_channel_initial_state() { let channel: StreamSocketChannel = StreamSocketChannel::new(); - // Initial state should be Closed - assert_eq!(channel.state(), SocketState::Closed); + assert_eq!(channel.state(), SocketState::Initial); // Should not be readable initially assert!(!channel.is_readable()); @@ -1078,12 +1111,12 @@ mod tests { // Try to read while not connected let mut buf = [0u8; 32]; let result = channel.try_read(&mut buf, super::super::ReceiveFlags::empty(), None); - assert!(matches!(result, Err(ReceiveError::SocketInInvalidState))); + assert!(matches!(result, Err(ChannelReadError::NotConnected))); // Try to write while not connected let data = b"test"; let result = channel.try_write(data); - assert!(matches!(result, Err(SendError::SocketInInvalidState))); + assert!(matches!(result, Err(ChannelWriteError::NotConnected))); } #[test] @@ -1105,7 +1138,31 @@ mod tests { // Should fail to read let mut buf = [0u8; 32]; let result = channel.try_read(&mut buf, super::super::ReceiveFlags::empty(), None); - assert!(matches!(result, Err(ReceiveError::SocketInInvalidState))); + assert!(matches!(result, Err(ChannelReadError::ReadShutdown))); + } + + #[test] + fn stream_channel_closed_after_connected_drains_rx_before_eof() { + let channel: StreamSocketChannel = StreamSocketChannel::new(); + channel.set_state(SocketState::Connected); + + let data = b"data"; + channel.push_rx_data_with(|buf: &mut [u8]| { + let to_copy = core::cmp::min(buf.len(), data.len()); + buf[..to_copy].copy_from_slice(&data[..to_copy]); + to_copy + }); + channel.set_state(SocketState::Closed); + + let mut buf = [0u8; 32]; + let read = channel + .try_read(&mut buf, super::super::ReceiveFlags::empty(), None) + .unwrap(); + assert_eq!(read, data.len()); + assert_eq!(&buf[..read], data); + + let result = channel.try_read(&mut buf, super::super::ReceiveFlags::empty(), None); + assert!(matches!(result, Err(ChannelReadError::ConnectionClosed))); } #[test] @@ -1118,7 +1175,7 @@ mod tests { // Should fail to write let result = channel.try_write(b"data"); - assert!(matches!(result, Err(SendError::SocketInInvalidState))); + assert!(matches!(result, Err(ChannelWriteError::WriteShutdown))); } #[test] @@ -1178,7 +1235,6 @@ mod tests { fn stream_channel_io_events() { let channel: StreamSocketChannel = StreamSocketChannel::new(); - // Closed state should have HUP let events = channel.check_io_events(); assert!(events.contains(Events::HUP)); @@ -1310,7 +1366,7 @@ mod tests { // Next send should fail let result = channel.send_to(&[99], Some(DUMMY_ADDR)); - assert!(matches!(result, Err(SendError::BufferFull))); + assert!(matches!(result, Err(ChannelWriteError::BufferFull))); } #[test] @@ -1319,7 +1375,10 @@ mod tests { // Sending without an address on an unconnected socket should fail let result = channel.send_to(&[1, 2, 3], None); - assert!(matches!(result, Err(SendError::DestinationAddressRequired))); + assert!(matches!( + result, + Err(ChannelWriteError::DestinationAddressRequired) + )); } #[test] diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 0aa23aae28..044959b6b4 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -401,6 +401,19 @@ impl From for Errno { } } +impl TryFrom for litebox::net::errors::SocketAsyncError { + type Error = Errno; + + fn try_from(value: Errno) -> Result { + match value { + Errno::ECONNREFUSED => Ok(litebox::net::errors::SocketAsyncError::ConnectionRefused), + Errno::ECONNRESET => Ok(litebox::net::errors::SocketAsyncError::ConnectionReset), + Errno::ETIMEDOUT => Ok(litebox::net::errors::SocketAsyncError::TimedOut), + _ => Err(value), + } + } +} + impl From for Errno { fn from(value: litebox::net::errors::LocalAddrError) -> Self { match value { @@ -461,6 +474,21 @@ impl From for Errno { } } +impl From for Errno { + fn from(value: litebox::net::socket_channel::ChannelWriteError) -> Self { + match value { + litebox::net::socket_channel::ChannelWriteError::WriteShutdown + | litebox::net::socket_channel::ChannelWriteError::NotConnected + | litebox::net::socket_channel::ChannelWriteError::ConnectionClosed => Errno::EPIPE, + litebox::net::socket_channel::ChannelWriteError::Unaddressable => Errno::EINVAL, + litebox::net::socket_channel::ChannelWriteError::BufferFull => Errno::EAGAIN, + litebox::net::socket_channel::ChannelWriteError::DestinationAddressRequired => { + Errno::EDESTADDRREQ + } + } + } +} + impl From for Errno { fn from(value: litebox::net::errors::ReceiveError) -> Self { match value { diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 7f9f856702..58a43aa6b2 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -1799,6 +1799,9 @@ bitflags::bitflags! { const TRUNC = 0x20; /// `MSG_WAITALL`: wait for the full amount of data const WAITALL = 0x100; + /// `MSG_WAITFORONE`: `recvmmsg` only — turn on `MSG_DONTWAIT` after the + /// first message has been received. + const WAITFORONE = 0x10000; /// const _ = !0; } @@ -1878,7 +1881,9 @@ impl Copy for UserMsgHdr { + /// the per-message `msghdr` pub msg_hdr: UserMsgHdr, + /// bytes transmitted for this entry, written back by the kernel pub msg_len: u32, #[cfg(target_pointer_width = "64")] _pad: u32, @@ -2140,6 +2145,13 @@ pub enum SyscallRequest { msg: Platform::RawMutPointer>, flags: ReceiveFlags, }, + Recvmmsg { + sockfd: i32, + msgvec: Platform::RawMutPointer>, + vlen: u32, + flags: ReceiveFlags, + timeout: TimeParam, + }, Shutdown { sockfd: i32, how: i32, @@ -2605,6 +2617,13 @@ impl SyscallRequest { Sysno::sendmmsg => sys_req!(Sendmmsg { sockfd, msgvec:*, vlen, flags }), Sysno::recvfrom => sys_req!(Recvfrom { sockfd, buf:*, len, flags, addr:*, addrlen:*, }), Sysno::recvmsg => sys_req!(Recvmsg { sockfd, msg:*, flags }), + Sysno::recvmmsg => sys_req!(Recvmmsg { + sockfd, + msgvec:*, + vlen, + flags, + timeout: { =*> TimeParam::timespec_old } + }), Sysno::shutdown => sys_req!(Shutdown { sockfd, how }), Sysno::bind => sys_req!(Bind { sockfd, sockaddr:*, addrlen }), Sysno::listen => sys_req!(Listen { sockfd, backlog }), diff --git a/litebox_runner_linux_userland/tests/recvmmsg.c b/litebox_runner_linux_userland/tests/recvmmsg.c new file mode 100644 index 0000000000..609d34db12 --- /dev/null +++ b/litebox_runner_linux_userland/tests/recvmmsg.c @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int g_fail = 0; + +static void check(int cond, const char *what) { + if (cond) { + printf(" PASS: %s\n", what); + } else { + printf(" FAIL: %s\n", what); + g_fail = 1; + } +} + +// Use the raw syscall so we exercise exactly what LiteBox intercepts; glibc's +// wrapper would otherwise be free to massage arguments before reaching the +// kernel. The libc prototype takes a non-const timespec, so do likewise. +static long sys_recvmmsg(int fd, struct mmsghdr *msgvec, unsigned int vlen, + int flags, struct timespec *timeout) { + return syscall(SYS_recvmmsg, fd, msgvec, vlen, flags, timeout); +} + +// Helper: build a vlen-sized array of mmsghdrs, each with one iov pointing at +// the corresponding row of `bufs`. msg_len is poisoned so we can prove the +// kernel wrote it. +static void build_recv_hdrs(struct mmsghdr *hdrs, struct iovec *iov, + char (*bufs)[64], unsigned int vlen) { + memset(hdrs, 0xAB, sizeof(*hdrs) * vlen); + for (unsigned int i = 0; i < vlen; i++) { + memset(bufs[i], 0, 64); + iov[i].iov_base = bufs[i]; + iov[i].iov_len = 63; // leave room for a NUL + memset(&hdrs[i].msg_hdr, 0, sizeof(hdrs[i].msg_hdr)); + hdrs[i].msg_hdr.msg_iov = &iov[i]; + hdrs[i].msg_hdr.msg_iovlen = 1; + hdrs[i].msg_len = 0xDEADBEEF; + } +} + +// --------------------------------------------------------------------------- +// Test 1: pre-buffer three datagrams and drain them in one recvmmsg call. +// Default flags=0 still works here because the data is already queued, so the +// "block until vlen messages" semantics never have to actually wait. +// --------------------------------------------------------------------------- +static void test_three_messages(void) { + puts("Test 1: recvmmsg drains multiple queued datagrams in one call"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + const char *payloads[3] = {"hello", "world!!", "third-msg"}; + for (int i = 0; i < 3; i++) { + ssize_t n = send(sv[0], payloads[i], strlen(payloads[i]), 0); + if (n != (ssize_t)strlen(payloads[i])) { + perror("send"); + exit(2); + } + } + + struct iovec iov[3]; + struct mmsghdr hdrs[3]; + char bufs[3][64]; + build_recv_hdrs(hdrs, iov, bufs, 3); + + errno = 0; + long n = sys_recvmmsg(sv[1], hdrs, 3, 0, NULL); + printf(" recvmmsg returned %ld (errno=%d %s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + check(n == 3, "recvmmsg returned 3 (number of messages received)"); + + for (int i = 0; i < 3; i++) { + unsigned int got = hdrs[i].msg_len; + unsigned int want = (unsigned int)strlen(payloads[i]); + if (got != want) { + printf(" msg_len[%d] = %u, want %u\n", i, got, want); + } + check(got == want, "msg_len matches payload length for each entry"); + check(strcmp(bufs[i], payloads[i]) == 0, + "iov buffer holds the expected datagram payload"); + } + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test 2: vlen == 0 returns 0 immediately, no errno, no work done. +// --------------------------------------------------------------------------- +static void test_vlen_zero(void) { + puts("Test 2: recvmmsg with vlen == 0 returns 0"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + errno = 0; + long n = sys_recvmmsg(sv[1], NULL, 0, 0, NULL); + printf(" recvmmsg returned %ld (errno=%d %s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + check(n == 0, "vlen=0 returns 0"); + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test 3: errno mapping for bad fd / bad msgvec pointer (when no message has +// been received yet). +// --------------------------------------------------------------------------- +static void test_errno_paths(void) { + puts("Test 3: recvmmsg errno on bad fd / bad msgvec pointer"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + errno = 0; + long n = sys_recvmmsg(-1, NULL, 1, 0, NULL); + printf(" fd=-1 vlen=1: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + check(n == -1 && errno == EBADF, "bad fd returns EBADF"); + + errno = 0; + n = sys_recvmmsg(9999, NULL, 1, 0, NULL); + printf(" fd=9999 vlen=1: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + check(n == -1 && errno == EBADF, "unused fd returns EBADF"); + + errno = 0; + n = sys_recvmmsg(sv[1], NULL, 1, MSG_DONTWAIT, NULL); + printf(" fd=ok msgvec=NULL vlen=1 DONTWAIT: ret=%ld errno=%d (%s)\n", n, + errno, n < 0 ? strerror(errno) : "-"); + check(n == -1 && errno == EFAULT, "NULL msgvec with vlen>0 returns EFAULT"); + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test 4: MSG_DONTWAIT on an empty socket returns -1 / EAGAIN. +// --------------------------------------------------------------------------- +static void test_dontwait_empty(void) { + puts("Test 4: MSG_DONTWAIT on empty queue returns EAGAIN"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + struct iovec iov[2]; + struct mmsghdr hdrs[2]; + char bufs[2][64]; + build_recv_hdrs(hdrs, iov, bufs, 2); + + errno = 0; + long n = sys_recvmmsg(sv[1], hdrs, 2, MSG_DONTWAIT, NULL); + printf(" recvmmsg DONTWAIT empty: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + check(n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK), + "empty queue with DONTWAIT returns EAGAIN/EWOULDBLOCK"); + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test 5: MSG_DONTWAIT — partial drain. Pre-buffer two datagrams, ask for +// five; we should get two back and msg_len for those two should be set. +// --------------------------------------------------------------------------- +static void test_dontwait_partial(void) { + puts("Test 5: MSG_DONTWAIT returns however many are queued (partial)"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + const char *payloads[2] = {"alpha", "beta!!"}; + for (int i = 0; i < 2; i++) { + ssize_t n = send(sv[0], payloads[i], strlen(payloads[i]), 0); + if (n != (ssize_t)strlen(payloads[i])) { + perror("send"); + exit(2); + } + } + + struct iovec iov[5]; + struct mmsghdr hdrs[5]; + char bufs[5][64]; + build_recv_hdrs(hdrs, iov, bufs, 5); + + errno = 0; + long n = sys_recvmmsg(sv[1], hdrs, 5, MSG_DONTWAIT, NULL); + printf(" recvmmsg DONTWAIT vlen=5 queued=2: ret=%ld errno=%d (%s)\n", n, + errno, n < 0 ? strerror(errno) : "-"); + check(n == 2, "returns 2 (number queued)"); + for (int i = 0; i < 2; i++) { + unsigned int got = hdrs[i].msg_len; + unsigned int want = (unsigned int)strlen(payloads[i]); + if (got != want) { + printf(" msg_len[%d] = %u, want %u\n", i, got, want); + } + check(got == want, "msg_len matches payload length"); + check(strcmp(bufs[i], payloads[i]) == 0, "payload arrived in iov"); + } + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test: tv_nsec/tv_sec validation. Linux validates the timespec before the +// fd/msgvec — `poll_select_set_timeout` runs first in `do_recvmmsg`. +// --------------------------------------------------------------------------- +static void test_bad_timespec(void) { + puts("Test: invalid timespec returns EINVAL (before EBADF/EFAULT)"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + struct iovec iov[1]; + struct mmsghdr hdrs[1]; + char bufs[1][64]; + build_recv_hdrs(hdrs, iov, bufs, 1); + + // tv_sec < 0 + struct timespec ts_neg_sec = {-1, 0}; + errno = 0; + long n = sys_recvmmsg(sv[1], hdrs, 1, MSG_DONTWAIT, &ts_neg_sec); + printf(" tv_sec=-1: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + check(n == -1 && errno == EINVAL, "negative tv_sec returns EINVAL"); + + // tv_nsec >= 1_000_000_000 + struct timespec ts_big_nsec = {0, 1000000000}; + errno = 0; + n = sys_recvmmsg(sv[1], hdrs, 1, MSG_DONTWAIT, &ts_big_nsec); + printf(" tv_nsec=1e9: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + check(n == -1 && errno == EINVAL, "tv_nsec >= 1e9 returns EINVAL"); + + // Validation precedes EBADF + errno = 0; + n = sys_recvmmsg(-1, hdrs, 1, MSG_DONTWAIT, &ts_neg_sec); + printf(" fd=-1 + tv_sec=-1: ret=%ld errno=%d (%s)\n", n, errno, + n < 0 ? strerror(errno) : "-"); + check(n == -1 && errno == EINVAL, + "timespec validation runs before fd check"); + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test: non-NULL timeout on an empty queue with MSG_DONTWAIT still returns +// EAGAIN (DONTWAIT short-circuits the inner recvmsg before the deadline ever +// matters). +// --------------------------------------------------------------------------- +static void test_timeout_dontwait_empty(void) { + puts("Test: timeout + DONTWAIT on empty queue returns EAGAIN"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + struct iovec iov[2]; + struct mmsghdr hdrs[2]; + char bufs[2][64]; + build_recv_hdrs(hdrs, iov, bufs, 2); + + struct timespec ts = {0, 10000000}; // 10ms + errno = 0; + long n = sys_recvmmsg(sv[1], hdrs, 2, MSG_DONTWAIT, &ts); + printf(" recvmmsg DONTWAIT timeout=10ms empty: ret=%ld errno=%d (%s)\n", n, + errno, n < 0 ? strerror(errno) : "-"); + check(n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK), + "DONTWAIT on empty with timeout returns EAGAIN"); + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test: zero timespec ({0,0}) on a queued socket reads exactly ONE message +// before the deadline check fires. `poll_select_set_timeout` parks the +// deadline at {0,0}, which compares as already past after the first recvmsg +// returns, so the loop exits with datagrams=1. +// --------------------------------------------------------------------------- +static void test_timeout_zero_caps_at_one(void) { + puts("Test: timeout={0,0} reads exactly 1 message even with vlen > 1"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + const char *payloads[3] = {"first", "second", "third!"}; + for (int i = 0; i < 3; i++) { + ssize_t sent = send(sv[0], payloads[i], strlen(payloads[i]), 0); + if (sent != (ssize_t)strlen(payloads[i])) { + perror("send"); + exit(2); + } + } + + struct iovec iov[3]; + struct mmsghdr hdrs[3]; + char bufs[3][64]; + build_recv_hdrs(hdrs, iov, bufs, 3); + + struct timespec ts = {0, 0}; + errno = 0; + long n = sys_recvmmsg(sv[1], hdrs, 3, 0, &ts); + printf(" recvmmsg ts={0,0} queued=3 vlen=3: ret=%ld errno=%d (%s)\n", n, + errno, n < 0 ? strerror(errno) : "-"); + check(n == 1, "timeout={0,0} returns 1"); + check(hdrs[0].msg_len == strlen(payloads[0]), + "msg_len[0] matches first payload length"); + check(strcmp(bufs[0], payloads[0]) == 0, + "iov[0] holds the first payload"); + // msg_len[1] was poisoned to 0xDEADBEEF and shouldn't have been written. + check(hdrs[1].msg_len == 0xDEADBEEF, "msg_len[1] left untouched"); + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test: a generous timeout doesn't truncate a multi-message drain. With a 5s +// deadline and three queued datagrams, all three are read before the deadline +// is even close. +// --------------------------------------------------------------------------- +static void test_timeout_generous_drains_all(void) { + puts("Test: generous timeout drains all queued messages"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + const char *payloads[3] = {"aaa", "bbbb", "ccccc"}; + for (int i = 0; i < 3; i++) { + ssize_t sent = send(sv[0], payloads[i], strlen(payloads[i]), 0); + if (sent != (ssize_t)strlen(payloads[i])) { + perror("send"); + exit(2); + } + } + + struct iovec iov[5]; + struct mmsghdr hdrs[5]; + char bufs[5][64]; + build_recv_hdrs(hdrs, iov, bufs, 5); + + struct timespec ts = {5, 0}; + errno = 0; + long n = sys_recvmmsg(sv[1], hdrs, 5, MSG_DONTWAIT, &ts); + printf(" recvmmsg DONTWAIT ts=5s queued=3 vlen=5: ret=%ld errno=%d (%s)\n", + n, errno, n < 0 ? strerror(errno) : "-"); + check(n == 3, "drained all 3 queued messages"); + for (int i = 0; i < 3; i++) { + check(hdrs[i].msg_len == strlen(payloads[i]), + "msg_len matches payload length"); + check(strcmp(bufs[i], payloads[i]) == 0, "payload arrived in iov"); + } + // Linux writes the remaining time back into the user's timespec on + // success (see `put_timespec64` in `__sys_recvmmsg`). Native probe with + // queued(3)/vlen=5 reports e.g. {4, 999997470}: drain takes microseconds + // so the residual is just-under 5s, but the kernel did update it. + printf(" remaining timespec: {%ld, %ld}\n", (long)ts.tv_sec, (long)ts.tv_nsec); + check(ts.tv_sec < 5, "remaining tv_sec decremented below original 5"); + check(ts.tv_sec >= 0, "remaining tv_sec did not go negative"); + + close(sv[0]); + close(sv[1]); +} + +// --------------------------------------------------------------------------- +// Test 6: MSG_WAITFORONE — pre-buffer two datagrams, ask for five with +// MSG_WAITFORONE. We should get exactly two (the first read blocks, but data +// is already there; subsequent reads are non-blocking). +// --------------------------------------------------------------------------- +static void test_waitforone(void) { + puts("Test 6: MSG_WAITFORONE drains pre-buffered datagrams"); + + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) < 0) { + perror("socketpair"); + exit(2); + } + + const char *payloads[2] = {"one", "two-two"}; + for (int i = 0; i < 2; i++) { + ssize_t n = send(sv[0], payloads[i], strlen(payloads[i]), 0); + if (n != (ssize_t)strlen(payloads[i])) { + perror("send"); + exit(2); + } + } + + struct iovec iov[5]; + struct mmsghdr hdrs[5]; + char bufs[5][64]; + build_recv_hdrs(hdrs, iov, bufs, 5); + + errno = 0; + long n = sys_recvmmsg(sv[1], hdrs, 5, MSG_WAITFORONE, NULL); + printf(" recvmmsg WAITFORONE vlen=5 queued=2: ret=%ld errno=%d (%s)\n", n, + errno, n < 0 ? strerror(errno) : "-"); + check(n == 2, "returns 2 (drains all queued after first)"); + for (int i = 0; i < 2; i++) { + unsigned int got = hdrs[i].msg_len; + unsigned int want = (unsigned int)strlen(payloads[i]); + check(got == want, "msg_len matches payload length"); + check(strcmp(bufs[i], payloads[i]) == 0, "payload arrived in iov"); + } + + close(sv[0]); + close(sv[1]); +} + +int main(void) { + puts("recvmmsg parity test"); + test_three_messages(); + test_vlen_zero(); + test_errno_paths(); + test_dontwait_empty(); + test_dontwait_partial(); + test_waitforone(); + test_bad_timespec(); + test_timeout_dontwait_empty(); + test_timeout_zero_caps_at_one(); + test_timeout_generous_drains_all(); + + if (g_fail) { + puts("\nRESULT: BUG(S) REPRODUCED"); + return 1; + } + puts("\nAll recvmmsg tests passed."); + return 0; +} diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 1873ea359f..67a8fadf65 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -703,6 +703,13 @@ impl Task { addrlen, } => self.sys_recvfrom(sockfd, buf, len, flags, addr, addrlen), SyscallRequest::Recvmsg { sockfd, msg, flags } => self.sys_recvmsg(sockfd, msg, flags), + SyscallRequest::Recvmmsg { + sockfd, + msgvec, + vlen, + flags, + timeout, + } => self.sys_recvmmsg(sockfd, msgvec, vlen, flags, timeout), SyscallRequest::Shutdown { sockfd, how } => syscall!(sys_shutdown(sockfd, how)), SyscallRequest::Bind { sockfd, diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index a155261c5a..997ccb632b 100644 --- a/litebox_shim_linux/src/syscalls/net.rs +++ b/litebox_shim_linux/src/syscalls/net.rs @@ -5,7 +5,7 @@ use core::{ ffi::CStr, - mem::offset_of, + mem::{offset_of, size_of}, net::{Ipv4Addr, SocketAddr, SocketAddrV4}, }; @@ -21,15 +21,15 @@ use litebox::{ net::{ CloseBehavior, TcpOptionData, errors::AcceptError, - socket_channel::{NetworkProxy, SocketState}, + socket_channel::{ChannelReadError, ChannelWriteError, NetworkProxy, SocketState}, }, - platform::{RawConstPointer as _, RawMutPointer as _}, + platform::{Instant as _, RawConstPointer as _, RawMutPointer as _, TimeProvider as _}, utils::TruncateExt as _, }; use litebox_common_linux::{ AddressFamily, FileDescriptorFlags, IPProtocol, ReceiveFlags, SendFlags, ShutdownHow, - SockFlags, SockType, SocketOption, SocketOptionName, TcpOption, UnixProtocol, errno::Errno, - signal::Signal, + SockFlags, SockType, SocketOption, SocketOptionName, TcpOption, UnixProtocol, UserMmsgHdr, + UserMsgHdr, errno::Errno, signal::Signal, }; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -41,7 +41,7 @@ use crate::{ }; /// Linux's hard cap on the number of iovecs per `*msg`-style call, and on the -/// number of entries per `*mmsg`-style call. See `UIO_MAXIOV` in ``. +/// number of entries per `sendmmsg`. See `UIO_MAXIOV` in ``. const UIO_MAXIOV: usize = 1024; macro_rules! convert_flags { @@ -772,7 +772,7 @@ impl GlobalState { Ok(0) if buf.is_empty() => Ok(0), Ok(0) => Err(TryOpError::TryAgain), Ok(n) => Ok(n), - Err(litebox::net::errors::SendError::BufferFull) if is_empty_stream => Ok(0), + Err(ChannelWriteError::BufferFull) if is_empty_stream => Ok(0), Err(e) => Err(TryOpError::Other(Errno::from(e))), }, ) @@ -830,7 +830,12 @@ impl GlobalState { || match proxy.try_read(buf, new_flags, source_addr.as_deref_mut()) { Ok(0) => Err(TryOpError::TryAgain), Ok(n) => Ok(n), - Err(e) => Err(TryOpError::Other(Errno::from(e))), + Err(ChannelReadError::ReadShutdown) => Ok(0), + Err(ChannelReadError::ConnectionClosed) => match proxy.get_async_error(true) { + Some(err) => Err(TryOpError::Other(err.into())), + None => Ok(0), + }, + Err(ChannelReadError::NotConnected) => Err(TryOpError::Other(Errno::ENOTCONN)), }, ) .map_err(Errno::from) @@ -1642,6 +1647,14 @@ impl Task { return Err(Errno::EINVAL); } + self.do_recvmsg(sockfd, msg_ptr, flags) + } + fn do_recvmsg( + &self, + sockfd: u32, + msg_ptr: MutPtr>, + flags: ReceiveFlags, + ) -> Result { let msg = msg_ptr.read_at_offset(0).ok_or(Errno::EFAULT)?; // Copy fields out of the packed struct to avoid unaligned references. @@ -1749,6 +1762,117 @@ impl Task { Ok(total_received) } + /// Handle syscall `recvmmsg` + pub(crate) fn sys_recvmmsg( + &self, + fd: i32, + msgvec: MutPtr>, + vlen: u32, + flags: ReceiveFlags, + timeout: litebox_common_linux::TimeParam, + ) -> Result { + let supported_flags = + ReceiveFlags::DONTWAIT | ReceiveFlags::TRUNC | ReceiveFlags::WAITFORONE; + if flags.intersects(supported_flags.complement()) { + log_unsupported!("Unsupported recvmmsg flags: {:?}", flags); + return Err(Errno::EINVAL); + } + + // Linux's `do_recvmmsg` validates the timespec before looking up the fd, + // so a bad timeout takes precedence over EBADF. + let timeout_duration = timeout.read()?; + + let Ok(sockfd) = u32::try_from(fd) else { + return Err(Errno::EBADF); + }; + + let vlen = vlen as usize; + + // Linux looks up the fd before touching vlen/msgvec, so a bogus fd + // takes priority over a bogus msgvec pointer or vlen == 0. + let inet_proxy = self.files.borrow().with_socket( + &self.global, + sockfd, + |fd| self.global.get_proxy(fd).map(Some), + |_| Ok(None), + )?; + + if vlen == 0 { + return Ok(0); + } + + // A `None` deadline means either no user-supplied timeout or a saturating overflow + // — both are treated as "no deadline". + let deadline = timeout_duration.and_then(|d| self.global.platform.now().checked_add(d)); + + let stride = size_of::>(); + let msg_len_off = offset_of!(UserMmsgHdr, msg_len); + let msgvec_base = msgvec.as_usize(); + let msgvec_len = vlen.checked_mul(stride).ok_or(Errno::EFAULT)?; + if msgvec_base.checked_add(msgvec_len).is_none() { + return Err(Errno::EFAULT); + } + + // WAITFORONE is mmsg-only; the inner recvmsg doesn't recognize it. + let waitforone = flags.contains(ReceiveFlags::WAITFORONE); + let mut iter_flags = flags.difference(ReceiveFlags::WAITFORONE); + let mut received: usize = 0; + let mut last_err: Option = None; + let mut async_error_to_restore = None; + for i in 0..vlen { + let base = msgvec_base + i * stride; + let inner_ptr = MutPtr::>::from_usize(base); + let n = match self.do_recvmsg(sockfd, inner_ptr, iter_flags) { + Ok(n) => n, + Err(e) => { + if received > 0 { + async_error_to_restore = e.try_into().ok(); + } + last_err = Some(e); + break; + } + }; + let msg_len_ptr = MutPtr::::from_usize(base + msg_len_off); + if msg_len_ptr.write_at_offset(0, n.trunc()).is_none() { + last_err = Some(Errno::EFAULT); + break; + } + received += 1; + if waitforone { + iter_flags.insert(ReceiveFlags::DONTWAIT); + } + + // Per the man page, the timeout is checked only after the receipt of each datagram. + if let Some(deadline) = deadline + && self.global.platform.now() >= deadline + { + break; + } + } + + if received == 0 { + // The only way to exit the loop with received=0 is via an inner + // recvmsg error; EAGAIN is the conservative fallback for the + // structurally unreachable case. + return Err(last_err.unwrap_or(Errno::EAGAIN)); + } + + // Stash the suppressed async socket error back onto the socket. + if let (Some(async_error), Some(proxy)) = (async_error_to_restore, inet_proxy) { + proxy.set_async_error(async_error); + } + + // Match Linux's `__sys_recvmmsg`: the remaining timespec is only + // written back when at least one datagram was received. A write + // EFAULT here overrides the success return, mirroring Linux. + let remaining = deadline + .and_then(|d| d.checked_duration_since(&self.global.platform.now())) + .unwrap_or(core::time::Duration::ZERO); + timeout.write(remaining)?; + + Ok(received) + } + pub(crate) fn sys_setsockopt( &self, sockfd: i32, diff --git a/litebox_shim_linux/src/transport.rs b/litebox_shim_linux/src/transport.rs index b4628a60a7..ce450f53e8 100644 --- a/litebox_shim_linux/src/transport.rs +++ b/litebox_shim_linux/src/transport.rs @@ -7,7 +7,7 @@ use alloc::boxed::Box; use alloc::sync::Arc; use litebox::fs::nine_p::transport; -use litebox::net::socket_channel::NetworkProxy; +use litebox::net::socket_channel::{ChannelWriteError, NetworkProxy}; use litebox::net::{ReceiveFlags, SendFlags}; use litebox_common_linux::{SockFlags, SockType, errno::Errno}; @@ -121,7 +121,7 @@ impl transport::Write for ShimTransport { loop { match self.proxy.try_write(buf, SendFlags::empty(), None) { Ok(n) => return Ok(n), - Err(litebox::net::errors::SendError::BufferFull) => { + Err(ChannelWriteError::BufferFull) => { // TX ring full — spin until space opens up. core::hint::spin_loop(); } From 45cdc0f84b78e3e7caf4e489a81f0e50f4943611 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Mon, 1 Jun 2026 20:04:28 -0700 Subject: [PATCH 017/319] Syscall and thread-pointer access rewriter for AArch64 Linux ELF (#890) This PR adds the AArch64 (Arm64) backend for the syscall rewriter. It replaces each syscall (`SVC #imm`) and thread-pointer access (`TPIDR_EL0` write/read) with a branch into a trampoline of per-site gates. SVC gates are similar to that of the `x86_64` syscall rewriter. Unlike `x86_64` (which leverages an unused segment register), it virtualizes `TPIDR_EL0`: the host owns the hardware register (e.g., `TPIDR_EL0` on Linux) as a per-thread anchor while the guest thread pointer lives in a slot off it. Co-authored-by: Sangho Lee --- .github/workflows/ci.yml | 3 + dev_tests/src/boilerplate.rs | 1 + litebox_syscall_rewriter/src/arm64.rs | 1467 +++++++++++++++++ litebox_syscall_rewriter/src/lib.rs | 147 +- .../tests/aarch64_tests.rs | 194 +++ litebox_syscall_rewriter/tests/hello-aarch64 | Bin 0 -> 1040 bytes .../tests/snapshot_tests.rs | 84 +- .../snapshot_tests__hello-aarch64-diff.snap | 29 + 8 files changed, 1891 insertions(+), 34 deletions(-) create mode 100644 litebox_syscall_rewriter/src/arm64.rs create mode 100644 litebox_syscall_rewriter/tests/aarch64_tests.rs create mode 100644 litebox_syscall_rewriter/tests/hello-aarch64 create mode 100644 litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-aarch64-diff.snap diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b5c8ce10c..cdd2a5f151 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,9 @@ jobs: - name: Install diod run: | sudo apt install -y diod + - name: Install AArch64 binutils + run: | + sudo apt install -y binutils-aarch64-linux-gnu - uses: Swatinem/rust-cache@v2 - name: Cache custom out directories uses: actions/cache@v5 diff --git a/dev_tests/src/boilerplate.rs b/dev_tests/src/boilerplate.rs index c29e14ebf7..d223de97fa 100644 --- a/dev_tests/src/boilerplate.rs +++ b/dev_tests/src/boilerplate.rs @@ -141,4 +141,5 @@ const SKIP_FILES: &[&str] = &[ "litebox_runner_linux_on_windows_userland/tests/test-bins/thread_static", "litebox_syscall_rewriter/tests/hello", "litebox_syscall_rewriter/tests/hello-32", + "litebox_syscall_rewriter/tests/hello-aarch64", ]; diff --git a/litebox_syscall_rewriter/src/arm64.rs b/litebox_syscall_rewriter/src/arm64.rs new file mode 100644 index 0000000000..1dbd32e397 --- /dev/null +++ b/litebox_syscall_rewriter/src/arm64.rs @@ -0,0 +1,1467 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! AArch64 (ARM64) syscall rewriting support for Linux ELF binaries. +//! +//! Every AArch64 instruction is 4 bytes including a direct branch (`B imm26`) +//! with a ±128MB range. This lets us replace a single instruction with +//! a branch into the trampoline without instruction borrowing. +//! +//! The trampoline is placed just past the highest mapped segment, so every +//! site-to-gate branch points forward. A site farther than the `B imm26` +//! ±128MB reach from its gate cannot redirect; it is replaced with a sentinel +//! `BRK #TRAP_BRK_IMM` and reported as a trapped site. Any trapped site makes +//! the rewrite incomplete, so the ELF-level caller rejects the binary with +//! `Error::UnpatchableSyscalls`, mirroring the x86-64 unpatchable-syscall path. +//! Executing the `BRK` raises a synchronous debug exception, so a trapped site +//! faults the guest rather than letting the unpatched instruction escape to the +//! host kernel. Recognizing the `TRAP_BRK_IMM` immediate in the runtime — to +//! attribute the trap to the rewriter rather than a guest breakpoint — is +//! planned but not yet implemented. +//! +//! ### Assumption: executable sections contain only instructions +//! +//! The patch scan walks each executable section word-by-word and treats every +//! 4-byte word that matches the `SVC`/`MSR TPIDR_EL0`/`MRS TPIDR_EL0` bit +//! patterns as that instruction. It does **not** distinguish inline data — literal +//! pools or jump tables embedded in `.text` — from code, because a fixed-width +//! decode cannot tell a data word from an instruction with the same bits. In +//! practice this is safe: default AArch64 codegen places constants in `.rodata`, +//! not `.text`, and the odds of an unrelated data word colliding with these +//! patterns are tiny. A binary that stores such a word inside an executable section +//! would have it rewritten; bounding the scan to symbol-defined function ranges +//! (via `STT_FUNC` extents) would remove the assumption (TODO). +//! +//! Two kinds of access are involved, three forms of instruction gated: +//! +//! * `SVC #imm` — the syscall instruction (any immediate; Linux ignores it). +//! Replaced with a branch to a per-site *SVC gate* that records the return +//! address and falls through to the shared SVC handler, a thin shim that +//! tail-jumps to the syscall callback. +//! * `MSR TPIDR_EL0, Xn` — a write to the thread pointer. Replaced with a branch +//! to a per-site *MSR gate* that stores the guest value into the guest +//! thread-pointer slot at `[TPIDR_EL0 + GUEST_TPIDR_OFFSET]`. +//! * `MRS Xd, TPIDR_EL0` — a read of the thread pointer. Replaced with a branch +//! to a per-site *MRS gate* that loads the guest value from the same slot. +//! `MRS XZR, TPIDR_EL0` is a discarded read and is left native. +//! +//! ## Thread-pointer virtualization +//! +//! The host owns the hardware `TPIDR_EL0` as a per-thread anchor; the guest's +//! logical thread pointer is a host-managed memory slot at `[TPIDR_EL0 + +//! GUEST_TPIDR_OFFSET]`. Every gated guest read/write of the thread pointer +//! addresses that slot with a scaled `LDR`/`STR` off the anchor: +//! +//! * the MSR gate reads the anchor (`MRS X16, TPIDR_EL0`) and stores the guest +//! value into the slot; +//! * the MRS gate reads the anchor and loads the guest value from the slot. +//! +//! This mirrors the x64 model: the host keeps the native thread-pointer anchor, +//! the guest is statically relegated off it, and the gates emit nothing +//! TLS-related to the callback. +//! +//! ## Gate scratch storage and the stack invariant +//! +//! `SVC` and `MSR TPIDR_EL0` clobber no general-purpose registers, so a gate has +//! no free scratch register on entry. The SVC and MSR gates therefore spill their +//! scratch registers (and, for SVC, the computed return address the callback +//! reads back) to a frame carved out of the guest stack with `SUB SP, SP, #frame` +//! / `ADD SP, SP, #frame`. The MRS gate needs no frame: it reuses its own +//! destination register as scratch and never touches the stack. +//! +//! Consequently the SVC and MSR gates **require `SP` to hold a valid, writable, +//! 16-byte-aligned stack at the patched site** — the same condition the kernel +//! relies on when it writes a signal frame below `SP`, and which every conforming +//! AArch64 caller already satisfies at a syscall boundary. The gate decrements +//! `SP` before storing, so nothing (signal delivery included) writes into the +//! frame while it is live; there is no red-zone hazard. A site reached with `SP` +//! pointing at unmapped or guard memory would fault where the native instruction +//! would not. AArch64 offers no cheaper alternative: with no segment-relative +//! store (unlike x86's `gs:`-relative spill) and no free register, reaching any +//! runtime-owned scratch area would itself require first clobbering an unsaved +//! guest register to materialize a base pointer. +//! +//! ## Trampoline layout (Linux) +//! +//! ```text +//! Offset 0: [8 bytes] syscall callback address (filled at load time) +//! Offset 8: [8 bytes] shared SVC handler (LDR X16,; BR X16) +//! Offset 16: per-site gates (SVC: 24 bytes, MSR: 36 bytes, MRS: 12 bytes) +//! ``` +//! +//! A binary with **no** patch sites gets no trampoline at all: the rewriter +//! appends only a size-0 sentinel header (matching the x86-64 path), recording +//! that the image was checked and needs no redirection. Signal returns are +//! handled by the runtime (see "Signal returns" below). +//! +//! The offset-0 callback address is **filled in by the loader/runtime, not by +//! this crate.** The emitted trampolines are therefore *not runnable as-is*: a +//! loader must write the syscall-callback address at offset 0 before any guest +//! `SVC` reaches a gate. (`callback` may be passed to [`hook_syscalls_aarch64`] +//! to prefill offset 0.) The callback reads host TLS from `TPIDR_EL0` and the +//! guest thread pointer from `[TPIDR_EL0 + GUEST_TPIDR_OFFSET]` itself. +//! +//! ## Signal returns +//! +//! This crate emits no sigreturn gate; `rt_sigreturn` is handled by the runtime. +//! The runtime installs its own sigreturn trampoline address into the signal +//! frame's return slot; because that is an absolute address (not a `B`), a +//! single runtime-owned gate is reachable from any guest regardless of the +//! ±128MB branch range, so no per-binary gate is required. +//! +//! ## Runtime contract +//! +//! Per thread, the runtime sets the hardware `TPIDR_EL0` to the host anchor and +//! reserves the guest thread-pointer slot at `[TPIDR_EL0 + GUEST_TPIDR_OFFSET]`. +//! No new callback ABI is introduced: the callback reaches host TLS through +//! `TPIDR_EL0` directly. Multi-threaded correctness depends only on the runtime +//! keeping the anchor valid and the slot reachable for every thread it starts; +//! no process-global table is involved, so concurrent threads never contend. +//! +//! ## Host-OS scope +//! +//! This module fully virtualizes the guest thread pointer against a stable +//! per-thread host anchor register. The model is host-OS-agnostic; only the +//! choice of anchor register varies per host, selected by [`Host`] (a gate names +//! its anchor through [`Host::anchor_read`]). On a Linux host the anchor is +//! `TPIDR_EL0` itself: the kernel preserves it across host execution, so the host +//! can keep its own value there as the anchor while the guest thread pointer +//! lives in the slot beside it. The instruction encoders and gate framing here +//! are host-agnostic; see [`Host`] for the per-host anchor registers and what +//! each additional host requires. + +use alloc::format; +use alloc::vec::Vec; + +use crate::{Error, Result, TextSectionInfo, checked_add_u64}; + +// ============================================================ +// Constants +// ============================================================ + +/// `SVC #0` (supervisor call) — the canonical syscall instruction. +const SVC_0: u32 = 0xD400_0001; + +/// Mask/match for *any* `SVC #imm16`. Linux dispatches every `SVC64` exception +/// to the syscall handler regardless of the immediate (the syscall number comes +/// from `x8`), so all immediates are rewritten, not just `svc #0`. The `imm16` +/// field occupies bits \[20:5]; masking it out leaves bits \[4:0] = `0b00001`, +/// which distinguishes `SVC` from `HVC` (`…0b10`) and `SMC` (`…0b11`). +const SVC_OPCODE_MASK: u32 = 0xFFE0_001F; +const SVC_OPCODE_BITS: u32 = SVC_0; + +/// Mask/match for `MSR TPIDR_EL0, Xt` (`0xD51BD04t`, the low 5 bits select Xt). +const MSR_TPIDR_EL0_MASK: u32 = 0xFFFF_FFE0; +const MSR_TPIDR_EL0_BITS: u32 = 0xD51B_D040; + +/// Mask/match for `MRS Xd, TPIDR_EL0` (`0xD53BD04d`, the low 5 bits select Xd). +const MRS_TPIDR_EL0_MASK: u32 = 0xFFFF_FFE0; +const MRS_TPIDR_EL0_BITS: u32 = 0xD53B_D040; + +/// `BRK` immediate planted at a patch site whose gate lies outside the `B` +/// instruction's ±128MB reach. Executing the site raises a synchronous debug +/// exception (`SIGTRAP`) carrying this immediate, faulting the guest rather than +/// letting the unpatched instruction escape to the host kernel; the site is also +/// reported as a trapped site so the ELF-level caller can reject the binary. +/// +/// Recognizing this immediate in the runtime — to attribute the trap to the +/// rewriter rather than a guest breakpoint — is planned but not yet implemented. +const TRAP_BRK_IMM: u16 = 0xB10B; + +// --- Register operands used by the emitted gates/handlers --- +// +// X16/X17 are the intra-procedure scratch registers (IP0/IP1), and register +// number 31 names the stack pointer in a base-register position. + +/// First scratch register (IP0). +const X16: u8 = 16; +/// Second scratch register (IP1). +const X17: u8 = 17; +/// Stack pointer (encoded as register 31 in a base-register field). +const SP: u8 = 31; +/// Zero register (register 31 in a transfer-register field, where it reads as +/// zero / discards writes — distinct from `SP`'s base-register meaning). +const XZR: u8 = 31; + +// --- Guest thread-pointer virtualization --- +// +// The host owns the hardware `TPIDR_EL0` as a per-thread anchor; the guest's +// logical thread pointer is a memory slot the runtime reserves at a fixed byte +// offset from that anchor. Every gated guest read/write of the thread pointer +// addresses the slot with a scaled `LDR`/`STR` off `TPIDR_EL0`. + +/// Byte offset from the host anchor in `TPIDR_EL0` at which the runtime reserves +/// this thread's guest thread-pointer slot. Every guest read/write of the thread +/// pointer is virtualized to `[TPIDR_EL0 + GUEST_TPIDR_OFFSET]` via a scaled +/// `LDR`/`STR`. +/// +/// Fixed ABI offset: the runtime points `TPIDR_EL0` at a per-thread block whose +/// `guest_tp` field sits just past the AArch64 variant-1 16-byte TCB header, so a +/// stray "deref `TPIDR_EL0` as a TCB" cannot mistake the guest pointer for the +/// dtv slot. Because the scaled immediate is baked into statically rewritten +/// binaries, this value is part of the rewriter/runtime ABI and must match the +/// runtime's block layout. +const GUEST_TPIDR_OFFSET: u16 = 16; + +// --- SVC gate stack frame --- +// +// The SVC gate touches only X16, so it needs a minimal 16-byte frame: one slot +// for the saved guest X16 and one for the computed post-SVC return address. + +/// SVC gate frame size (`SUB/ADD SP, SP, #SVC_FRAME_BYTES`). 16-byte aligned. +const SVC_FRAME_BYTES: u16 = 16; +/// Saved guest X16. +const SVC_FRAME_OFF_X16: u16 = 0; +/// Computed post-SVC return address. +const SVC_FRAME_OFF_RETADDR: u16 = 8; + +// --- MSR gate stack frame --- +// +// The MSR gate spills X16/X17 (one `STP`/`LDP` pair) and stages the captured +// guest value so the source register needs no special-casing. + +/// MSR gate frame size (`SUB/ADD SP, SP, #MSR_FRAME_BYTES`). 16-byte aligned. +const MSR_FRAME_BYTES: u16 = 32; +/// Saved X16 (and, +8, X17 via the `STP`/`LDP` pair). +const MSR_FRAME_OFF_X16: u16 = 0; +/// Captured guest thread-pointer value, staged while all guest registers are +/// still pristine. +const MSR_FRAME_OFF_VALUE: u16 = 16; + +// --- Trampoline layout offsets (all in bytes) --- + +/// Callback address slot. +const HEADER_CALLBACK_OFFSET: usize = 0; + +/// Shared SVC handler, placed just past the 8-byte callback slot. Per-site gates +/// follow it and are each appended dynamically, so this shared prologue is the +/// only fixed-offset region the emitters reference. +const SHARED_SVC_HANDLER_OFFSET: usize = HEADER_CALLBACK_OFFSET + 8; + +// ============================================================ +// Instruction encoders +// +// Each encoder returns the 32-bit little-endian instruction word. Encoders that +// can fail range checks return `Option`; callers convert `None` into an +// `Error::AddressOverflow` with context. +// +// Each encoder ORs an [`Opcode`] base with its shifted, masked operands. The +// `IMM*_MASK` values isolate the immediate fields shared by several encoders. +// ============================================================ + +/// 26-bit `imm26` branch-offset field (`B`/`BL`), bits \[25:0]. +const IMM26_MASK: u32 = 0x03FF_FFFF; +/// 19-bit `imm19` offset field (`B.cond`/`LDR`-literal/`ADRP` immhi), bits \[18:0]. +const IMM19_MASK: u32 = 0x0007_FFFF; + +/// Base opcode of an emitted instruction: every fixed bit set with all operand +/// fields zeroed. An encoder selects a variant and ORs in its operands via +/// [`Opcode::bits`]. (`MRS TPIDR_EL0` is encoded from [`Opcode::MrsTpidrEl0`], +/// whose bits equal [`MRS_TPIDR_EL0_BITS`] — the scan-detection pattern in +/// [`find_patch_sites`].) +#[repr(u32)] +#[derive(Clone, Copy)] +enum Opcode { + B = 0x1400_0000, + LdrLiteral = 0x5800_0000, + Adrp = 0x9000_0000, + Br = 0xD61F_0000, + SubImm = 0xD100_0000, + AddImm = 0x9100_0000, + StrUimm = 0xF900_0000, + LdrUimm = 0xF940_0000, + Stp = 0xA900_0000, + Ldp = 0xA940_0000, + MrsTpidrEl0 = MRS_TPIDR_EL0_BITS, + Brk = 0xD420_0000, +} + +impl Opcode { + /// The base opcode word, for ORing in operand fields. + const fn bits(self) -> u32 { + self as u32 + } +} + +// --- Shared instruction-format encoders --- +// +// Several instructions share one field layout and differ only by opcode, so +// each layout is encoded once here and selected by an `Opcode`. [`Insn::encode`] +// dispatches each variant to its format here; every range check lives in exactly +// one place per format. + +/// `op | imm26` — PC-relative branch (`B`/`BL`), ±128MB, 4-byte aligned. +fn branch_imm26(op: Opcode, offset: i64) -> Option { + if offset % 4 != 0 { + return None; + } + let imm26 = i32::try_from(offset >> 2).ok()?; + if !(-(1 << 25)..(1 << 25)).contains(&imm26) { + return None; + } + Some(op.bits() | (imm26.cast_unsigned() & IMM26_MASK)) +} + +/// `op | imm19<<5 | low` — PC-relative imm19 form (`B.cond`/`LDR`-literal), ±1MB, +/// 4-byte aligned. `low` is the instruction's 5-bit \[4:0] field: `Rt`, or the +/// condition code for `B.cond`. +fn pcrel_imm19(op: Opcode, offset: i64, low: u32) -> Option { + if offset % 4 != 0 { + return None; + } + let imm19 = i32::try_from(offset >> 2).ok()?; + if !(-(1 << 18)..(1 << 18)).contains(&imm19) { + return None; + } + Some(op.bits() | ((imm19.cast_unsigned() & IMM19_MASK) << 5) | low) +} + +/// `op | rn<<5` — instruction whose only operand is a register in the `Rn` field +/// (`BR`/`RET`). +fn reg_in_rn(op: Opcode, rn: u8) -> u32 { + op.bits() | (u32::from(rn) << 5) +} + +/// `op | imm12<<10 | rn<<5 | rd` — 12-bit-immediate add/sub form +/// (`ADD`/`SUB`/`ADDS`). The caller supplies an already-scaled `imm12`. +fn data_imm12(op: Opcode, rd: u8, rn: u8, imm12: u16) -> Option { + if imm12 >= (1 << 12) { + return None; + } + Some(op.bits() | (u32::from(imm12) << 10) | (u32::from(rn) << 5) | u32::from(rd)) +} + +/// `op | imm12<<10 | rn<<5 | rt` — unsigned scaled (×8) 64-bit load/store +/// (`STR`/`LDR [Xn, #imm]`). `imm_bytes` must be a multiple of 8. +fn ldst_uimm12(op: Opcode, rt: u8, rn: u8, imm_bytes: u16) -> Option { + if !imm_bytes.is_multiple_of(8) { + return None; + } + let imm12 = imm_bytes / 8; + if imm12 >= (1 << 12) { + return None; + } + Some(op.bits() | (u32::from(imm12) << 10) | (u32::from(rn) << 5) | u32::from(rt)) +} + +/// `op | imm7<<15 | rt2<<10 | rn<<5 | rt` — signed scaled (×8) 64-bit load/store +/// pair (`STP`/`LDP`). `imm_bytes` must be a multiple of 8 within ±512 bytes. +fn ldst_pair(op: Opcode, rt: u8, rt2: u8, rn: u8, imm_bytes: i16) -> Option { + if imm_bytes % 8 != 0 { + return None; + } + let imm7 = imm_bytes / 8; + if !(-64..=63).contains(&imm7) { + return None; + } + let imm7_u = u32::from(imm7.cast_unsigned() & 0x7F); + Some(op.bits() | (imm7_u << 15) | (u32::from(rt2) << 10) | (u32::from(rn) << 5) | u32::from(rt)) +} + +/// `base | rt` — system-register move (`MRS`/`MSR`); `base` already encodes the +/// system register and transfer direction. +fn sysreg_move(base: u32, rt: u8) -> u32 { + base | u32::from(rt) +} + +/// A single AArch64 instruction emitted into a trampoline, described by its +/// mnemonic and operands. [`Insn::encode`] produces the 32-bit little-endian +/// word; range-checked forms return `None` when an operand is out of range. +/// +/// Register operands are register numbers (`X16`, `SP`, ...). This enum, with +/// the format helpers above, is the only place instruction bit layouts live; +/// the gate emitters build `Insn` values and never touch raw opcodes. +#[derive(Clone, Copy)] +enum Insn { + /// `B` (unconditional branch), PC-relative, ±128MB, 4-byte aligned. + B(i64), + /// `ADRP Xd, #page_off` — page-relative address, ±4GB (in 4KB pages). + Adrp { rd: u8, page_off: i64 }, + /// `LDR Xt, ` (PC-relative literal load), ±1MB, 4-byte aligned. + LdrLiteral { rt: u8, off: i64 }, + /// `BR Xn` (branch to register). + Br(u8), + /// `SUB SP, SP, #imm12`. + SubSp(u16), + /// `ADD SP, SP, #imm12`. + AddSp(u16), + /// `ADD Xd, Xn, #imm12`. + AddImm { rd: u8, rn: u8, imm12: u16 }, + /// `STR Xt, [Xn, #imm_bytes]` (unsigned scaled; `imm_bytes` multiple of 8). + StrUimm { rt: u8, rn: u8, imm_bytes: u16 }, + /// `LDR Xt, [Xn, #imm_bytes]` (unsigned scaled; `imm_bytes` multiple of 8). + LdrUimm { rt: u8, rn: u8, imm_bytes: u16 }, + /// `STP Xt, Xt2, [Xn, #imm_bytes]` (signed scaled; `imm_bytes` multiple of 8). + Stp { + rt: u8, + rt2: u8, + rn: u8, + imm_bytes: i16, + }, + /// `LDP Xt, Xt2, [Xn, #imm_bytes]` (signed scaled; `imm_bytes` multiple of 8). + Ldp { + rt: u8, + rt2: u8, + rn: u8, + imm_bytes: i16, + }, + /// `MRS Xt, TPIDR_EL0` (read thread pointer). + MrsTpidrEl0(u8), + /// `BRK #imm16` — software breakpoint raising a synchronous debug exception. + Brk(u16), +} + +impl Insn { + /// Encode to a 32-bit little-endian instruction word, or `None` if an + /// operand is outside the instruction's encodable range. + fn encode(self) -> Option { + match self { + Insn::B(off) => branch_imm26(Opcode::B, off), + Insn::Adrp { rd, page_off } => { + let imm = i32::try_from(page_off).ok()?; + if !(-(1 << 20)..(1 << 20)).contains(&imm) { + return None; + } + let imm = imm.cast_unsigned(); + let immlo = (imm & 0x3) << 29; + let immhi = ((imm >> 2) & IMM19_MASK) << 5; + Some(Opcode::Adrp.bits() | immlo | immhi | u32::from(rd)) + } + Insn::LdrLiteral { rt, off } => pcrel_imm19(Opcode::LdrLiteral, off, u32::from(rt)), + Insn::Br(rn) => Some(reg_in_rn(Opcode::Br, rn)), + Insn::SubSp(imm12) => data_imm12(Opcode::SubImm, SP, SP, imm12), + Insn::AddSp(imm12) => data_imm12(Opcode::AddImm, SP, SP, imm12), + Insn::AddImm { rd, rn, imm12 } => data_imm12(Opcode::AddImm, rd, rn, imm12), + Insn::StrUimm { rt, rn, imm_bytes } => ldst_uimm12(Opcode::StrUimm, rt, rn, imm_bytes), + Insn::LdrUimm { rt, rn, imm_bytes } => ldst_uimm12(Opcode::LdrUimm, rt, rn, imm_bytes), + Insn::Stp { + rt, + rt2, + rn, + imm_bytes, + } => ldst_pair(Opcode::Stp, rt, rt2, rn, imm_bytes), + Insn::Ldp { + rt, + rt2, + rn, + imm_bytes, + } => ldst_pair(Opcode::Ldp, rt, rt2, rn, imm_bytes), + Insn::MrsTpidrEl0(rt) => Some(sysreg_move(Opcode::MrsTpidrEl0.bits(), rt)), + Insn::Brk(imm) => Some(Opcode::Brk.bits() | (u32::from(imm) << 5)), + } + } +} + +// ============================================================ +// Host anchor selection +// ============================================================ + +/// The host OS the rewritten guest runs under. +/// +/// The guest thread pointer is virtualized the same way on every host; only the +/// *anchor register* a gate reads to reach the host's per-thread block varies. +/// [`Host`] selects that register, so a gate names the anchor through +/// [`Host::anchor_read`] rather than hardcoding a system register. Adding a host +/// is a new variant plus its anchor-read arm. +/// +/// Other host OSes need a different stable anchor register (a future variant +/// supplying its own [`Host::anchor_read`]), and beyond that a host-specific +/// shared SVC handler — not just a different trampoline base address: +/// +/// * **Linux-on-macOS** (Apple Silicon): XNU clobbers `TPIDR_EL0` on +/// signals/preemption and zeroes `x18` on every exception entry, so neither +/// register survives a host transition. The stable anchor becomes the +/// read-only `TPIDRRO_EL0` (which XNU keeps per-pthread), and *both* +/// `TPIDR_EL0` and `x18` must be fully virtualized — `x18` via per-site gates. +/// * **Linux-on-Windows** (Windows on ARM64): Windows does not preserve +/// `TPIDR_EL0` across context switches and reserves `x18` as the TEB pointer +/// (always valid). The TEB is the stable anchor: the per-thread TLS state is +/// reached through a TEB TLS slot, and `TPIDR_EL0` (plus guest `x18`, where the +/// guest uses it) is virtualized against that. +#[derive(Clone, Copy)] +pub(crate) enum Host { + /// Linux host. The kernel preserves `TPIDR_EL0` across host execution, so the + /// host keeps its anchor there and the guest thread-pointer slot lives beside + /// it; the anchor read is `MRS Xd, TPIDR_EL0`. + Linux, +} + +impl Host { + /// The instruction a gate uses to read this host's per-thread anchor into + /// `rd`. + fn anchor_read(self, rd: u8) -> Insn { + match self { + Host::Linux => Insn::MrsTpidrEl0(rd), + } + } +} + +// ============================================================ +// Patch-site scanning +// ============================================================ + +/// A located instruction to rewrite. +struct PatchSite { + /// Byte offset of the instruction within the ELF file image. + file_offset: usize, + /// Virtual address of the instruction. + vaddr: u64, + kind: PatchKind, +} + +/// The kind of instruction at a [`PatchSite`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PatchKind { + /// `SVC #imm` for any immediate. Linux dispatches every `SVC64` to the + /// syscall handler regardless of the immediate (the number comes from `x8`), + /// so the immediate is not significant and is not recorded. + Svc, + /// `MSR TPIDR_EL0, Xt`; the `u8` is the source register (0-31). + MsrTpidr(u8), + /// `MRS Xd, TPIDR_EL0`; the `u8` is the destination register (0-30). + MrsTpidr(u8), +} + +/// Scan all executable sections for `SVC #imm`, `MSR TPIDR_EL0` (thread-pointer +/// writes), and `MRS TPIDR_EL0` (thread-pointer reads). AArch64 instructions are +/// always 4-byte aligned, so we step in 4-byte units. Returns sites in ascending +/// file order. +/// +/// `MRS XZR, TPIDR_EL0` is a discarded read (register 31 as an `LDR` base would +/// mean `SP`), so it is left native; every other `MRS Xd, TPIDR_EL0` is gated. +fn find_patch_sites(sections: &[TextSectionInfo], buf: &[u8]) -> Result> { + let mut sites = Vec::new(); + + for section in sections { + let start = usize::try_from(section.file_offset) + .map_err(|_| Error::ParseError("section file offset too large".into()))?; + let size = usize::try_from(section.size) + .map_err(|_| Error::ParseError("section size too large".into()))?; + let end = start + .checked_add(size) + .filter(|&e| e <= buf.len()) + .ok_or_else(|| Error::ParseError("section extends beyond file".into()))?; + let section_data = &buf[start..end]; + + for i in (0..section_data.len()).step_by(4) { + if i + 4 > section_data.len() { + break; + } + let insn = u32::from_le_bytes(section_data[i..i + 4].try_into().unwrap()); + let kind = if (insn & SVC_OPCODE_MASK) == SVC_OPCODE_BITS { + PatchKind::Svc + } else if (insn & MSR_TPIDR_EL0_MASK) == MSR_TPIDR_EL0_BITS { + PatchKind::MsrTpidr((insn & 0x1F) as u8) + } else if (insn & MRS_TPIDR_EL0_MASK) == MRS_TPIDR_EL0_BITS { + let rd = (insn & 0x1F) as u8; + // `MRS XZR, TPIDR_EL0` discards its result (a no-op read); gating + // it would mean using register 31 as an `LDR` base (= SP), so + // leave it native. + if rd == XZR { + continue; + } + PatchKind::MrsTpidr(rd) + } else { + continue; + }; + sites.push(PatchSite { + file_offset: start + i, + vaddr: checked_add_u64(section.vaddr, i as u64, "patch site")?, + kind, + }); + } + } + + Ok(sites) +} + +// ============================================================ +// Main hooking entry point +// ============================================================ + +/// Outcome of rewriting one AArch64 image's patch sites. +pub(crate) struct HookOutcome { + /// Trampoline blob the caller appends after the ELF (page-aligned). + pub trampoline: Vec, + /// Virtual addresses of patch sites that could not be redirected to their + /// gate — the inbound `B` or one of the gate's own branches fell outside the + /// branch's ±128MB range — and were replaced with a trap instead of a + /// redirect. A non-empty list means the rewrite is incomplete: those sites + /// fault at runtime rather than entering the trampoline. + pub trapped_sites: Vec, +} + +/// Hook all `SVC #imm`, `MSR TPIDR_EL0` writes, and `MRS TPIDR_EL0` reads in an +/// AArch64 ELF image. (`MRS XZR, TPIDR_EL0` is a discarded read and is left +/// native — see the module docs.) +/// +/// `buf` is patched in place; the returned [`HookOutcome::trampoline`] is the +/// blob that the caller appends after the ELF (page-aligned). +/// `trampoline_base_addr` is the virtual address the trampoline will be mapped +/// at; `callback` is the absolute address stored in the callback slot (0 if the +/// loader fills it in later). +/// +/// Returns `Ok(None)` when the image contains no patch sites: no trampoline is +/// needed and the caller emits a size-0 sentinel header instead (matching the +/// x86-64 path). Signal returns are handled by the runtime — not a per-binary +/// gate — so a syscall-free binary needs no trampoline at all. +/// +/// Otherwise returns `Ok(Some(outcome))`. A site whose inbound `B` cannot reach +/// its gate, or whose gate cannot branch back within the `B` instruction's +/// ±128MB reach, cannot be redirected; it is replaced with a trap and listed in +/// [`HookOutcome::trapped_sites`] so the caller can reject the incomplete +/// rewrite, mirroring the x86-64 unpatchable-syscall path. +pub(crate) fn hook_syscalls_aarch64( + buf: &mut [u8], + text_sections: &[TextSectionInfo], + trampoline_base_addr: u64, + callback: u64, + host: Host, +) -> Result> { + let sites = find_patch_sites(text_sections, buf)?; + + if sites.is_empty() { + // No patch sites: nothing to redirect, so no trampoline is + // emitted. The caller writes a size-0 sentinel header instead. + return Ok(None); + } + + let mut trampoline_data: Vec = Vec::new(); + emit_shared_prologue(&mut trampoline_data, trampoline_base_addr, callback)?; + + let mut trapped_sites: Vec = Vec::new(); + + for site in &sites { + let gate_offset = trampoline_data.len(); + let gate_vaddr = + checked_add_u64(trampoline_base_addr, gate_offset as u64, "trampoline gate")?; + + // A site is redirected to its gate with a single in-place `B` (±128MB + // forward reach), and each gate branches back to `site + 4` (the SVC gate + // also reaches its shared handler). The gate's return branch spans a wider + // displacement than the inbound one, so the inbound branch encoding is + // necessary but not sufficient: the gate is built only when the inbound + // branch fits, and a gate whose own branches are out of range reports + // `GateBuild::Unreachable` and appends nothing. If either the inbound + // branch or the gate is unreachable, replace the site with the sentinel + // trap, record it as unpatchable, and emit no gate. + let b_offset = gate_vaddr + .cast_signed() + .saturating_sub(site.vaddr.cast_signed()); + let inbound = Insn::B(b_offset).encode(); + + let build = if inbound.is_some() { + match site.kind { + PatchKind::Svc => emit_svc_gate( + &mut trampoline_data, + gate_offset, + trampoline_base_addr, + site, + )?, + PatchKind::MsrTpidr(rt) => emit_msr_gate( + &mut trampoline_data, + gate_offset, + trampoline_base_addr, + site, + rt, + host, + )?, + PatchKind::MrsTpidr(rd) => emit_mrs_gate( + &mut trampoline_data, + gate_offset, + trampoline_base_addr, + site, + rd, + host, + )?, + } + } else { + GateBuild::Unreachable + }; + + if let (Some(b_insn), GateBuild::Emitted) = (inbound, build) { + // Replace the original instruction with `B `. + buf[site.file_offset..site.file_offset + 4].copy_from_slice(&b_insn.to_le_bytes()); + } else { + let brk = Insn::Brk(TRAP_BRK_IMM) + .encode() + .expect("BRK always encodes"); + buf[site.file_offset..site.file_offset + 4].copy_from_slice(&brk.to_le_bytes()); + trapped_sites.push(site.vaddr); + } + } + + Ok(Some(HookOutcome { + trampoline: trampoline_data, + trapped_sites, + })) +} + +/// Emit the header slot and the shared SVC handler — the fixed-size shared +/// prologue that per-site gates follow. +fn emit_shared_prologue( + trampoline_data: &mut Vec, + trampoline_base_addr: u64, + callback: u64, +) -> Result<()> { + // Offset 0: callback address. + trampoline_data.extend_from_slice(&callback.to_le_bytes()); + + emit_shared_svc_handler( + trampoline_data, + SHARED_SVC_HANDLER_OFFSET, + trampoline_base_addr, + )?; + + Ok(()) +} + +// ============================================================ +// SVC gate + shared SVC handler +// ============================================================ + +/// Whether a gate was fully emitted or could not be placed within reach. +/// +/// A gate redirects back to the guest (and, for the SVC gate, out to the shared +/// handler) with PC-relative branches. When any of those branches is out of +/// range the gate emits nothing and reports [`GateBuild::Unreachable`], leaving +/// the trampoline blob untouched so the caller can trap the originating site. +enum GateBuild { + Emitted, + Unreachable, +} + +/// Per-site SVC gate (6 instructions, 24 bytes, 16-byte frame). +/// +/// Saves only X16 (already a scratch register), computes the post-SVC return +/// address into X16, records it on the frame, then branches to the shared SVC +/// handler. Guest X17/X18/LR and NZCV are untouched; the callback finds the +/// post-SVC return address at `[SP, #8]` and restores X16 from `[SP, #0]`. +/// +/// Frame layout (relative to the decremented SP): `[0]=X16 [8]=return_addr`. +/// Requires `SP` to address a valid writable stack at the site (see the module +/// docs, "Gate scratch storage and the stack invariant"). +fn emit_svc_gate( + trampoline_data: &mut Vec, + gate_offset: usize, + trampoline_base_addr: u64, + site: &PatchSite, +) -> Result { + let gate_vaddr = checked_add_u64(trampoline_base_addr, gate_offset as u64, "SVC gate")?; + let mut asm = Asm::new(gate_vaddr); + + // SUB SP, SP, #16 ; STR X16, [SP] — save the guest X16. + asm.emit(Insn::SubSp(SVC_FRAME_BYTES)); + asm.emit(Insn::StrUimm { + rt: X16, + rn: SP, + imm_bytes: SVC_FRAME_OFF_X16, + }); + + // ADRP X16, ; ADD X16, X16, # — post-SVC return + // address. + let return_addr = checked_add_u64(site.vaddr, 4, "SVC return")?; + if !asm.adrp(X16, return_addr)? { + return Ok(GateBuild::Unreachable); + } + let page_lo = u16::try_from(return_addr & 0xFFF).expect("masked to 12 bits"); + asm.emit(Insn::AddImm { + rd: X16, + rn: X16, + imm12: page_lo, + }); + + // STR X16, [SP, #8] — record the return address. + asm.emit(Insn::StrUimm { + rt: X16, + rn: SP, + imm_bytes: SVC_FRAME_OFF_RETADDR, + }); + + // B . + let handler_vaddr = checked_add_u64( + trampoline_base_addr, + SHARED_SVC_HANDLER_OFFSET as u64, + "SVC handler", + )?; + if !asm.branch_to(handler_vaddr)? { + return Ok(GateBuild::Unreachable); + } + + trampoline_data.extend_from_slice(&asm.finish()); + Ok(GateBuild::Emitted) +} + +/// Shared SVC handler (2 instructions, 8 bytes). +/// +/// A thin shim that conveys nothing TLS-related: it loads the syscall-callback +/// pointer from the trampoline header and tail-jumps to it. The callback reads +/// host TLS from `TPIDR_EL0` (the host anchor) and the guest thread pointer from +/// `[TPIDR_EL0 + GUEST_TPIDR_OFFSET]` itself, so the handler carries no TLS state. +/// +/// Nothing in the handler clobbers NZCV, so the guest's pre-svc flags reach the +/// callback unchanged with no save/restore. +fn emit_shared_svc_handler( + trampoline_data: &mut Vec, + handler_offset: usize, + trampoline_base_addr: u64, +) -> Result<()> { + let handler_vaddr = + checked_add_u64(trampoline_base_addr, handler_offset as u64, "SVC handler")?; + let callback_vaddr = checked_add_u64( + trampoline_base_addr, + HEADER_CALLBACK_OFFSET as u64, + "callback slot", + )?; + let mut asm = Asm::new(handler_vaddr); + + // LDR X16, =callback ; BR X16. Nothing here clobbers NZCV, so the guest's + // pre-svc flags reach the callback unchanged with no save/restore. + asm.ldr_literal(X16, callback_vaddr)?; + asm.emit(Insn::Br(X16)); + + trampoline_data.extend_from_slice(&asm.finish()); + Ok(()) +} + +// ============================================================ +// MSR + MRS gates +// ============================================================ + +/// Per-site MSR gate (9 instructions, 36 bytes, 32-byte frame). +/// +/// Virtualizes a guest `MSR TPIDR_EL0, Xn` write. The hardware register holds the +/// host anchor, so the gate stores the guest value into the guest thread-pointer +/// slot at `[TPIDR_EL0 + GUEST_TPIDR_OFFSET]`: +/// +/// 1. spill X16/X17 and capture the guest value `Xn` to the frame while all guest +/// registers are still pristine (so `Xn` needs no special-casing, even when it +/// is one of the scratch registers just spilled (X16/X17) or XZR); +/// 2. `MRS X16, TPIDR_EL0` reads the host anchor; +/// 3. reload the captured value into X17 and `STR X17, [X16, #GUEST_TPIDR_OFFSET]` +/// stores it into the guest thread-pointer slot; +/// 4. restore X16/X17 and branch back to the instruction after the original MSR. +/// +/// The slot is always reachable because `TPIDR_EL0` is the host anchor the +/// runtime keeps valid, so a guest value of `0` (XZR) is an ordinary store — never +/// a fault. +/// +/// The X16/X17 spill and the captured value use a guest-stack frame, so this gate +/// requires `SP` to address a valid writable stack at the site (see the module +/// docs, "Gate scratch storage and the stack invariant"). +/// +/// `MSR TPIDR_EL0` does not touch the condition flags and the gate uses only +/// plain loads/stores and `B` (never `BL`), so NZCV and X30 reach the guest +/// unchanged with no save/restore. +fn emit_msr_gate( + trampoline_data: &mut Vec, + gate_offset: usize, + trampoline_base_addr: u64, + site: &PatchSite, + rt: u8, + host: Host, +) -> Result { + let gate_vaddr = checked_add_u64(trampoline_base_addr, gate_offset as u64, "MSR gate")?; + let mut asm = Asm::new(gate_vaddr); + + // SUB SP, SP, #32 ; STP X16, X17, [SP] — spill the gate's scratch registers. + asm.emit(Insn::SubSp(MSR_FRAME_BYTES)); + asm.emit(Insn::Stp { + rt: X16, + rt2: X17, + rn: SP, + imm_bytes: MSR_FRAME_OFF_X16.cast_signed(), + }); + + // STR Xn, [SP, #16] — capture the guest value while all guest registers are + // still pristine, so Xn needs no special-casing even when it is one of the + // scratch registers just spilled (X16/X17) or XZR. + asm.emit(Insn::StrUimm { + rt, + rn: SP, + imm_bytes: MSR_FRAME_OFF_VALUE, + }); + + // MRS X16, — read the host anchor. + asm.emit(host.anchor_read(X16)); + + // LDR X17, [SP, #16] ; STR X17, [X16, #GUEST_TPIDR_OFFSET] — store the guest + // value into its slot off the host anchor. + asm.emit(Insn::LdrUimm { + rt: X17, + rn: SP, + imm_bytes: MSR_FRAME_OFF_VALUE, + }); + asm.emit(Insn::StrUimm { + rt: X17, + rn: X16, + imm_bytes: GUEST_TPIDR_OFFSET, + }); + + // Restore: LDP X16, X17, [SP] ; ADD SP, SP, #32. + asm.emit(Insn::Ldp { + rt: X16, + rt2: X17, + rn: SP, + imm_bytes: MSR_FRAME_OFF_X16.cast_signed(), + }); + asm.emit(Insn::AddSp(MSR_FRAME_BYTES)); + + // B . + if !asm.branch_to(checked_add_u64(site.vaddr, 4, "MSR return")?)? { + return Ok(GateBuild::Unreachable); + } + + trampoline_data.extend_from_slice(&asm.finish()); + Ok(GateBuild::Emitted) +} + +/// Per-site MRS gate (3 instructions, 12 bytes). +/// +/// Virtualizes a guest `MRS Xd, TPIDR_EL0` read. The hardware register holds the +/// host anchor, so the gate reads the anchor and then loads the guest thread +/// pointer from its slot, reusing `Xd` as scratch (no frame needed): +/// `MRS Xd, TPIDR_EL0 ; LDR Xd, [Xd, #GUEST_TPIDR_OFFSET] ; B `. +fn emit_mrs_gate( + trampoline_data: &mut Vec, + gate_offset: usize, + trampoline_base_addr: u64, + site: &PatchSite, + rd: u8, + host: Host, +) -> Result { + let gate_vaddr = checked_add_u64(trampoline_base_addr, gate_offset as u64, "MRS gate")?; + let mut asm = Asm::new(gate_vaddr); + asm.emit(host.anchor_read(rd)); + asm.emit(Insn::LdrUimm { + rt: rd, + rn: rd, + imm_bytes: GUEST_TPIDR_OFFSET, + }); + if !asm.branch_to(checked_add_u64(site.vaddr, 4, "MRS return")?)? { + return Ok(GateBuild::Unreachable); + } + trampoline_data.extend_from_slice(&asm.finish()); + Ok(GateBuild::Emitted) +} + +// ============================================================ +// Small helpers +// ============================================================ + +/// A position-tracking assembler for one trampoline fragment (a gate or a shared +/// handler). It owns the emitted words and the base virtual address of the first +/// word, so the current vaddr — [`Asm::here`] — is always known without manual +/// instruction counting. +/// +/// Branches and loads to an absolute target ([`Asm::branch_to`], +/// [`Asm::ldr_literal`], [`Asm::adrp`]) resolve immediately against +/// [`Asm::here`]. The per-site branches ([`Asm::branch_to`], [`Asm::adrp`]) +/// report an out-of-range target by emitting nothing and returning `false`, so +/// the caller can trap the site; [`Asm::ldr_literal`] (used only by the fixed +/// prologue) instead errors, since a prologue that cannot be placed is fatal. +struct Asm { + base_vaddr: u64, + code: Vec, +} + +impl Asm { + fn new(base_vaddr: u64) -> Self { + Asm { + base_vaddr, + code: Vec::new(), + } + } + + /// Virtual address of the next instruction to be emitted. + fn here(&self) -> Result { + checked_add_u64( + self.base_vaddr, + self.code.len() as u64, + "trampoline gate next-instruction", + ) + } + + /// Append a raw little-endian word. + fn push_word(&mut self, word: u32) { + self.code.extend_from_slice(&word.to_le_bytes()); + } + + /// Append a fixed-operand instruction. Every operand at the call sites is a + /// compile-time-known register or frame offset, so encoding cannot fail; a + /// `None` would be a rewriter bug rather than an unencodable program. + fn emit(&mut self, insn: Insn) { + let word = insn.encode().expect("statically valid instruction"); + self.push_word(word); + } + + /// `B ` — unconditional branch to an absolute address. Returns + /// whether the target was within the branch's ±128MB reach: an out-of-range + /// target emits nothing and yields `false`, so the caller can trap the + /// originating site instead of failing the whole rewrite. + fn branch_to(&mut self, target_vaddr: u64) -> Result { + let offset = self.delta_to(target_vaddr)?; + let Some(word) = Insn::B(offset).encode() else { + return Ok(false); + }; + self.push_word(word); + Ok(true) + } + + /// `LDR Xt, =target` — PC-relative literal load of an absolute address. + fn ldr_literal(&mut self, rt: u8, target_vaddr: u64) -> Result<()> { + let offset = self.delta_to(target_vaddr)?; + let word = Insn::LdrLiteral { rt, off: offset } + .encode() + .ok_or_else(|| { + Error::AddressOverflow(format!("LDR literal offset {offset:#x} out of ±1MB range")) + })?; + self.push_word(word); + Ok(()) + } + + /// `ADRP Xd, ` — page-relative address of an absolute target. + /// Returns whether the target's page was within ADRP's ±4GB reach (see + /// [`Asm::branch_to`] for the out-of-range contract). + fn adrp(&mut self, rd: u8, target_vaddr: u64) -> Result { + let here = self.here()?; + let page_off = (target_vaddr & !0xFFF) + .cast_signed() + .saturating_sub((here & !0xFFF).cast_signed()) + >> 12; + let Some(word) = Insn::Adrp { rd, page_off }.encode() else { + return Ok(false); + }; + self.push_word(word); + Ok(true) + } + + /// Signed byte distance from [`Asm::here`] to `target_vaddr`. The subtraction + /// saturates so a pathological address can't overflow it; a distance the + /// branch can't encode is rejected by the encoder's range check at the call + /// site, with the saturated value reported for diagnostics. + fn delta_to(&self, target_vaddr: u64) -> Result { + Ok(target_vaddr + .cast_signed() + .saturating_sub(self.here()?.cast_signed())) + } + + /// Return the emitted bytes. + fn finish(self) -> Vec { + self.code + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + // Gate and shared-handler sizes. The emitters append each gate dynamically + // (`gate_offset = trampoline_data.len()`), so these sizes drive no emission; + // the tests use them to slice individual gates out of the trampoline blob and + // to assert its total length. `GATES_START_OFFSET` is the fixed shared-prologue + // size that the per-site gates follow. + const SVC_GATE_INSNS: usize = 6; + const SVC_GATE_SIZE: usize = SVC_GATE_INSNS * 4; + const SHARED_SVC_HANDLER_INSNS: usize = 2; + const SHARED_SVC_HANDLER_SIZE: usize = SHARED_SVC_HANDLER_INSNS * 4; + const MSR_GATE_INSNS: usize = 9; + const MSR_GATE_SIZE: usize = MSR_GATE_INSNS * 4; + const MRS_GATE_INSNS: usize = 3; + const MRS_GATE_SIZE: usize = MRS_GATE_INSNS * 4; + const GATES_START_OFFSET: usize = SHARED_SVC_HANDLER_OFFSET + SHARED_SVC_HANDLER_SIZE; + + /// Top-6 opcode bits, isolating the `B`/`BL` major opcode for read-back checks. + const OPCODE_TOP6_MASK: u32 = 0xFC00_0000; + + fn word_at(data: &[u8], byte_off: usize) -> u32 { + u32::from_le_bytes(data[byte_off..byte_off + 4].try_into().unwrap()) + } + + /// `MSR TPIDR_EL0, Xrt` guest instruction word (the low 5 bits select Xrt). + /// The rewriter only matches/scans this form; it never emits it, so the + /// encoder lives only here for building test inputs. + fn msr_tpidr_el0(rt: u8) -> u32 { + MSR_TPIDR_EL0_BITS | u32::from(rt) + } + + /// Helper: emit just the shared SVC handler and return its instruction words. + fn shared_svc_handler_words() -> vec::Vec { + let mut buf = vec::Vec::new(); + emit_shared_svc_handler(&mut buf, 0, 0x1000).unwrap(); + buf.chunks_exact(4) + .map(|w| u32::from_le_bytes(w.try_into().unwrap())) + .collect() + } + + #[test] + fn svc_handler_jumps_to_callback_without_tls() { + let words = shared_svc_handler_words(); + assert_eq!(words.len(), SHARED_SVC_HANDLER_INSNS); + // The handler conveys nothing TLS-related: it loads the callback pointer + // and tail-jumps. The callback reads host TLS from TPIDR_EL0 itself. + assert_eq!( + words[1], + Insn::Br(X16).encode().unwrap(), + "handler ends in BR X16" + ); + // No MRS TPIDR_EL0 anywhere in the handler. + assert!( + !words + .iter() + .any(|&w| w & MRS_TPIDR_EL0_MASK == MRS_TPIDR_EL0_BITS) + ); + } + + #[test] + fn encoders_match_known_words() { + // `B #0`. + assert_eq!(Insn::B(0).encode().unwrap(), 0x1400_0000); + // `B #4` advances one instruction. + assert_eq!(Insn::B(4).encode().unwrap(), 0x1400_0001); + // `B #-4` is the all-ones imm26. + assert_eq!(Insn::B(-4).encode().unwrap(), 0x17FF_FFFF); + // `BR X16`. + assert_eq!(Insn::Br(16).encode().unwrap(), 0xD61F_0200); + // TPIDR_EL0 accessor. + assert_eq!(Insn::MrsTpidrEl0(9).encode().unwrap(), 0xD53B_D049); + // `MSR TPIDR_EL0, X9` guest word (scanned, never emitted). + assert_eq!(msr_tpidr_el0(9), 0xD51B_D049); + // Scaled (×8) 64-bit load/store: `ldr x9,[x9,#16]` / `str x17,[x16,#16]`. + assert_eq!( + Insn::LdrUimm { + rt: 9, + rn: 9, + imm_bytes: 16 + } + .encode() + .unwrap(), + 0xF940_0929 + ); + assert_eq!( + Insn::StrUimm { + rt: 17, + rn: 16, + imm_bytes: 16 + } + .encode() + .unwrap(), + 0xF900_0A11 + ); + // The guest thread-pointer slot lives at the fixed ABI offset + // GuestThreadBlock::guest_tp; pin both the value and the emitted word. + assert_eq!(GUEST_TPIDR_OFFSET, 16); + // Slot access at GUEST_TPIDR_OFFSET: `ldr x9,[x9,#16]` / `str x17,[x16,#16]`. + assert_eq!( + Insn::LdrUimm { + rt: 9, + rn: 9, + imm_bytes: GUEST_TPIDR_OFFSET + } + .encode() + .unwrap(), + 0xF940_0929 + ); + assert_eq!( + Insn::StrUimm { + rt: 17, + rn: 16, + imm_bytes: GUEST_TPIDR_OFFSET + } + .encode() + .unwrap(), + 0xF900_0A11 + ); + // `BRK #0xB10B`, the trap that replaces an out-of-range patch site. + assert_eq!(Insn::Brk(TRAP_BRK_IMM).encode().unwrap(), 0xD436_2160); + } + + #[test] + fn encoder_range_checks() { + assert!(Insn::B(2).encode().is_none()); // not 4-aligned + assert!(Insn::B(1 << 27).encode().is_none()); // out of ±128MB + assert!( + Insn::StrUimm { + rt: 0, + rn: 0, + imm_bytes: 4 + } + .encode() + .is_none() + ); // not 8-scaled + } + + #[test] + fn asm_ldr_literal_computes_pc_relative_offset() { + let mut asm = Asm::new(0x1000); + asm.emit(Insn::Br(30)); // [0] at 0x1000 + asm.ldr_literal(16, 0x1010).unwrap(); // [1] at 0x1004, target 0x1010 => +0xC + let code = asm.finish(); + assert_eq!( + word_at(&code, 4), + Insn::LdrLiteral { rt: 16, off: 0xC }.encode().unwrap() + ); + } + + /// Build a one-section image whose section data == the supplied words and + /// run the hooker. Returns `(patched_section, trampoline)`. Panics if the + /// input has no patch sites (use [`hook_words_opt`] for that case). + fn hook_words(words: &[u32], base: u64, tramp_base: u64) -> (Vec, Vec) { + let (patched, outcome) = hook_words_opt(words, base, tramp_base); + ( + patched, + outcome + .expect("expected a trampoline (input has patch sites)") + .trampoline, + ) + } + + /// Like [`hook_words`] but returns the raw `Option` outcome so callers can + /// assert the "no patch sites" (`None`) sentinel and trapped-site cases. + fn hook_words_opt(words: &[u32], base: u64, tramp_base: u64) -> (Vec, Option) { + let mut buf = Vec::new(); + for w in words { + buf.extend_from_slice(&w.to_le_bytes()); + } + let sections = vec![TextSectionInfo { + vaddr: base, + file_offset: 0, + size: buf.len() as u64, + }]; + let outcome = + hook_syscalls_aarch64(&mut buf, §ions, tramp_base, 0, Host::Linux).unwrap(); + (buf, outcome) + } + + #[test] + fn no_patch_sites_emit_no_trampoline() { + // No patch sites: a NOP-only section yields no trampoline at all, so the + // caller emits a size-0 sentinel (matching the x86-64 path). + let (_patched, tramp) = hook_words_opt(&[0xD503_201F], 0x1000, 0x100000); + assert!(tramp.is_none()); + } + + #[test] + fn svc_is_replaced_with_branch_into_gate() { + let base = 0x1000; + let tramp_base = 0x200000; + let (patched, tramp) = hook_words(&[SVC_0], base, tramp_base); + + // The SVC word became a `B`. + let patched_word = word_at(&patched, 0); + assert_eq!( + patched_word & OPCODE_TOP6_MASK, + Opcode::B.bits(), + "expected B opcode" + ); + + // It targets the first per-site gate at GATES_START_OFFSET. + let imm26 = i64::from(patched_word & IMM26_MASK); + let disp = imm26 << 2; // positive here + let target = base + disp.cast_unsigned(); + assert_eq!(target, tramp_base + GATES_START_OFFSET as u64); + + // The gate's first instruction is SUB SP, SP, #SVC_FRAME_BYTES. + assert_eq!( + word_at(&tramp, GATES_START_OFFSET), + Insn::SubSp(SVC_FRAME_BYTES).encode().unwrap() + ); + // Total = prologue + one SVC gate. + assert_eq!(tramp.len(), GATES_START_OFFSET + SVC_GATE_SIZE); + } + + #[test] + fn svc_with_nonzero_immediate_is_also_rewritten() { + // Linux dispatches every `SVC64` exception to the syscall handler + // regardless of the immediate (the syscall number comes from x8), so + // `svc #imm` with imm != 0 must be rewritten too. imm16 occupies bits + // [20:5], so `svc #1` is `SVC_0 | (1 << 5)`. + let svc_imm1 = SVC_0 | (1 << 5); + let (patched, tramp) = hook_words(&[svc_imm1], 0x1000, 0x200000); + // The SVC word became a `B` into the gate. + assert_eq!(word_at(&patched, 0) & OPCODE_TOP6_MASK, Opcode::B.bits()); + assert_eq!(tramp.len(), GATES_START_OFFSET + SVC_GATE_SIZE); + } + + #[test] + fn site_beyond_branch_range_is_trapped() { + // The trampoline sits 256MB above the section, past the `B` instruction's + // ±128MB reach, so the site cannot branch into its gate. It is replaced + // with the sentinel `BRK`, surfaced as a trapped site, and no gate is + // emitted for it, leaving the trampoline at the prologue-only size. + let (patched, outcome) = hook_words_opt(&[SVC_0], 0x1000, 0x1000_0000); + let outcome = outcome.expect("expected a trampoline (input has patch sites)"); + assert_eq!( + word_at(&patched, 0), + Insn::Brk(TRAP_BRK_IMM).encode().unwrap() + ); + assert_eq!(outcome.trapped_sites, vec![0x1000]); + assert_eq!(outcome.trampoline.len(), GATES_START_OFFSET); + } + + #[test] + fn msr_gate_return_branch_out_of_range_is_trapped() { + // Boundary window where the site can reach its gate but the gate cannot + // reach back. The MSR gate's return `B` is its last instruction, at + // `gate + 32`, branching to `site + 4`; its displacement magnitude is + // `b_offset + 28`, larger than the inbound `B`'s `b_offset`. Placing the + // gate at the maximum encodable forward offset (`2^27 - 4`) makes the + // inbound branch encode while the return needs `-(2^27 + 24)`, just past + // the `-2^27` reach. The site must still be trapped gracefully — replaced + // with `BRK` and surfaced through `trapped_sites` — not error out. + let base = 0x1000u64; + let max_fwd = (1u64 << 27) - 4; // largest 4-aligned forward `B` offset + let tramp_base = base + max_fwd - GATES_START_OFFSET as u64; + let (patched, outcome) = hook_words_opt(&[msr_tpidr_el0(5)], base, tramp_base); + let outcome = outcome.expect("expected a trampoline (input has patch sites)"); + assert_eq!( + word_at(&patched, 0), + Insn::Brk(TRAP_BRK_IMM).encode().unwrap() + ); + assert_eq!(outcome.trapped_sites, vec![base]); + // No gate emitted for the trapped site: prologue-only trampoline. + assert_eq!(outcome.trampoline.len(), GATES_START_OFFSET); + } + + #[test] + fn hvc_and_smc_are_not_treated_as_svc() { + // `HVC #0` (…02) and `SMC #0` (…03) share the SVC opcode base but differ + // in bits [1:0]; they must not be rewritten as syscalls. + let hvc_0 = 0xD400_0002u32; + let smc_0 = 0xD400_0003u32; + let (_p, tramp) = hook_words_opt(&[hvc_0, smc_0], 0x1000, 0x200000); + assert!(tramp.is_none(), "HVC/SMC must not be matched as SVC"); + } + + #[test] + fn msr_and_mrs_both_get_gates() { + let base = 0x1000; + let tramp_base = 0x300000; + // MSR TPIDR_EL0, X5 then MRS X9, TPIDR_EL0. + let words = [msr_tpidr_el0(5), Insn::MrsTpidrEl0(9).encode().unwrap()]; + let (patched, tramp) = hook_words(&words, base, tramp_base); + // Both the write and the read are rewritten to a branch into their gate. + assert_eq!(word_at(&patched, 0) & OPCODE_TOP6_MASK, Opcode::B.bits()); + assert_eq!(word_at(&patched, 4) & OPCODE_TOP6_MASK, Opcode::B.bits()); + // Trampoline = prologue + one MSR gate + one MRS gate. + assert_eq!( + tramp.len(), + GATES_START_OFFSET + MSR_GATE_SIZE + MRS_GATE_SIZE + ); + } + + #[test] + fn mrs_with_xzr_dest_is_left_native() { + // `MRS XZR, TPIDR_EL0` reads-and-discards; it must not be rewritten. + let mrs_xzr = Insn::MrsTpidrEl0(31).encode().unwrap(); + let (_p, tramp) = hook_words_opt(&[mrs_xzr], 0x1000, 0x200000); + assert!(tramp.is_none(), "MRS XZR, TPIDR_EL0 must be left native"); + } + + #[test] + fn msr_gate_stores_guest_value_to_slot_for_any_register() { + const BL_TOP6: u32 = 0x9400_0000; + for n in [5u8, 16, 17, 30, 31] { + let (_p, tramp) = hook_words(&[msr_tpidr_el0(n)], 0x1000, 0x500000); + let gate = &tramp[GATES_START_OFFSET..GATES_START_OFFSET + MSR_GATE_SIZE]; + // Self-contained: never BL out. + assert!( + (0..MSR_GATE_INSNS).all(|i| word_at(gate, i * 4) & OPCODE_TOP6_MASK != BL_TOP6) + ); + // Capture the guest value while pristine: STR Xn, [SP, #16]. + let capture = Insn::StrUimm { + rt: n, + rn: SP, + imm_bytes: MSR_FRAME_OFF_VALUE, + } + .encode() + .unwrap(); + let cap_i = (0..MSR_GATE_INSNS) + .find(|&i| word_at(gate, i * 4) == capture) + .expect("MSR gate must capture the guest value (incl. XZR=0) while pristine"); + // Read the host anchor: MRS X16, TPIDR_EL0. + let anchor = Insn::MrsTpidrEl0(X16).encode().unwrap(); + let anc_i = (0..MSR_GATE_INSNS) + .find(|&i| word_at(gate, i * 4) == anchor) + .expect("MSR gate must read the host anchor"); + // Store to the slot: STR X17, [X16, #GUEST_TPIDR_OFFSET]. + let store = Insn::StrUimm { + rt: X17, + rn: X16, + imm_bytes: GUEST_TPIDR_OFFSET, + } + .encode() + .unwrap(); + let st_i = (0..MSR_GATE_INSNS) + .find(|&i| word_at(gate, i * 4) == store) + .expect("MSR gate must store the guest value into its slot"); + assert!( + cap_i < anc_i && anc_i < st_i, + "capture -> anchor -> store order" + ); + // Ends in B back to the guest (not the last word being the store). + assert_eq!( + word_at(gate, (MSR_GATE_INSNS - 1) * 4) & OPCODE_TOP6_MASK, + Opcode::B.bits() + ); + } + } + + #[test] + fn mrs_gate_loads_guest_tp_from_slot() { + for d in [5u8, 16, 17, 30] { + let (_p, tramp) = + hook_words(&[Insn::MrsTpidrEl0(d).encode().unwrap()], 0x1000, 0x400000); + let gate = &tramp[GATES_START_OFFSET..GATES_START_OFFSET + MRS_GATE_SIZE]; + assert_eq!(word_at(gate, 0), Insn::MrsTpidrEl0(d).encode().unwrap()); + assert_eq!( + word_at(gate, 4), + Insn::LdrUimm { + rt: d, + rn: d, + imm_bytes: GUEST_TPIDR_OFFSET + } + .encode() + .unwrap() + ); + assert_eq!(word_at(gate, 8) & OPCODE_TOP6_MASK, Opcode::B.bits()); + } + } + + #[test] + fn svc_gate_saves_only_x16_and_records_return() { + let base = 0x1000; + let tramp_base = 0x600000; + let (_p, tramp) = hook_words(&[SVC_0], base, tramp_base); + let gate = &tramp[GATES_START_OFFSET..GATES_START_OFFSET + SVC_GATE_SIZE]; + // SUB SP,#16 ; STR X16,[SP] ; ADRP X16,.. ; ADD X16,X16,#.. ; STR X16,[SP,#8] ; B + assert_eq!( + word_at(gate, 0), + Insn::SubSp(SVC_FRAME_BYTES).encode().unwrap() + ); + assert_eq!( + word_at(gate, 4), + Insn::StrUimm { + rt: X16, + rn: SP, + imm_bytes: SVC_FRAME_OFF_X16 + } + .encode() + .unwrap() + ); + assert_eq!( + word_at(gate, 16), + Insn::StrUimm { + rt: X16, + rn: SP, + imm_bytes: SVC_FRAME_OFF_RETADDR + } + .encode() + .unwrap() + ); + // Self-contained tail branch to the shared handler (B, never BL). + assert_eq!(word_at(gate, 20) & OPCODE_TOP6_MASK, Opcode::B.bits()); + assert_eq!(tramp.len(), GATES_START_OFFSET + SVC_GATE_SIZE); + } +} diff --git a/litebox_syscall_rewriter/src/lib.rs b/litebox_syscall_rewriter/src/lib.rs index 9b584d7e02..15d38adc21 100644 --- a/litebox_syscall_rewriter/src/lib.rs +++ b/litebox_syscall_rewriter/src/lib.rs @@ -14,10 +14,19 @@ //! //! This crate currently supports x86-64 ELFs for syscall hooking and x86-64 PEs for syscall //! hooking plus rewriting Windows TEB accesses from GS segment overrides to FS segment overrides. +//! +//! It also supports AArch64 ELFs. AArch64 support currently targets **Linux guests on Linux +//! hosts** and rewrites `SVC #imm` syscalls plus both directions of guest thread-pointer access +//! (`MSR TPIDR_EL0` writes and `MRS TPIDR_EL0` reads): the host owns the hardware `TPIDR_EL0` +//! anchor and the guest thread pointer is fully virtualized to a host-managed memory slot. This +//! thread-pointer virtualization is Linux-host-specific; other hosts (Linux-on-Windows, +//! Linux-on-macOS) must anchor and virtualize TLS differently. See the `arm64` module for details. #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; +mod arm64; + use alloc::collections::{BTreeMap, BTreeSet}; use alloc::format; use alloc::string::{String, ToString}; @@ -136,19 +145,29 @@ const NT_SYSNO_REWRITE_LOOKBACK: usize = 16; /// - trampoline virtual address (8 bytes) /// - trampoline size (8 bytes) /// -/// This layout allows loaders to read just the last 32 bytes to get the metadata. When there is no -/// syscall instruction in the binary, the rewriter appends a header-only marker with -/// `trampoline_size == 0` so the loader/audit path can tell the binary was processed. +/// This layout allows loaders to read just the last 32 bytes to get the metadata. +/// +/// When there is nothing to patch, both architectures append only a 32-byte +/// header carrying a `trampoline_size = 0` *sentinel* (no trampoline body), so a +/// loader can distinguish "processed, nothing to patch" from "never processed"; +/// no instructions are rewritten in that case. +/// +/// AArch64 differs in one way: it also rewrites guest thread-pointer accesses +/// (`MSR TPIDR_EL0` writes and `MRS TPIDR_EL0` reads), so a binary containing one +/// is patched (and gets a non-empty trampoline) even when it has no syscall +/// (`SVC`) instructions at all. (See the `arm64` module docs.) /// /// Returns the rewritten binary. Binaries that cannot or do not need to be /// patched (relocatable objects, non-ELF files, already-hooked binaries, -/// binaries without executable sections or syscall instructions) are returned -/// unchanged — these are not errors. +/// binaries without executable sections) are returned unchanged — these are +/// not errors. See the per-architecture behavior above. /// /// Returns `Err` for genuinely broken inputs (corrupt ELF, unsupported /// executables like Bun, arithmetic overflow) and for binaries that contain -/// syscall instructions that could not be patched (replaced with `icebp; hlt` -/// so they trap instead of escaping to the host kernel). +/// patch sites that could not be redirected. An unpatchable site is replaced +/// with a trapping instruction so it faults instead of escaping to the host +/// kernel: `icebp; hlt` on x86-64, and `BRK` on AArch64 (where a patch site is +/// an `SVC`, `MSR TPIDR_EL0`, or `MRS TPIDR_EL0` instruction). pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Result> { if input_binary.ends_with(BUN_FOOTER_MARKER) { return Err(Error::UnsupportedExecutable( @@ -159,9 +178,16 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res // Relocatable object files (.o) must not be patched: they are linker // input, not executable code. Rewriting instructions or appending // trampoline data would corrupt the object file for the linker. - // Check the ELF e_type field (bytes 16..18) before doing any work. + // Check the ELF e_type field (bytes 16..18) before doing any work. The + // encoding of multi-byte fields is selected by e_ident[EI_DATA] (byte 5), + // so decode e_type in that endianness rather than assuming little-endian. if input_binary.len() >= 18 { - let e_type = u16::from_le_bytes([input_binary[16], input_binary[17]]); + let e_type_bytes = [input_binary[16], input_binary[17]]; + let e_type = if input_binary[5] == object::elf::ELFDATA2MSB { + u16::from_be_bytes(e_type_bytes) + } else { + u16::from_le_bytes(e_type_bytes) + }; if e_type == object::elf::ET_REL { return Ok(input_binary.to_vec()); } @@ -185,7 +211,11 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res let file = object::File::parse(&*buf).map_err(|e| Error::ParseError(e.to_string()))?; let arch = match file { - object::File::Elf64(_) => Arch::X86_64, + object::File::Elf64(_) => match file.architecture() { + object::Architecture::X86_64 => Arch::X86_64, + object::Architecture::Aarch64 => Arch::Aarch64, + _ => return Ok(input_binary.to_vec()), + }, _ => return Ok(input_binary.to_vec()), }; @@ -205,6 +235,20 @@ pub fn hook_syscalls_in_elf(input_binary: &[u8], trampoline: Option) -> Res (arch, text_sections, trampoline_base_addr) }; + // AArch64 uses a fully separate rewriting strategy (single-instruction + // branch replacement, no instruction borrowing). Dispatch to it before any + // x86-only work (iced-x86 decoding would misinterpret AArch64 bytes). + // See the `arm64` module docs. + if arch == Arch::Aarch64 { + return hook_aarch64_elf( + input_binary, + buf, + &text_sections, + trampoline_base_addr, + trampoline.unwrap_or(0), + ); + } + let control_transfer_targets = get_control_transfer_targets(arch, &*buf, &text_sections)?; let mut trampoline_data = Vec::from(trampoline.unwrap_or(0).to_le_bytes()); let patch_result = patch_syscalls_in_sections( @@ -718,6 +762,60 @@ fn append_trampoline_footer( out.extend_from_slice(header.as_bytes()); } +/// Rewrite an AArch64 ELF, appending the trampoline and trailing header. +/// +/// `input_binary` is the original, unmodified ELF; `buf` is the mutable copy +/// (patched in place by the arm64 module). `callback` is the absolute address +/// stored in the trampoline's callback slot (0 when the loader fills it in +/// later). +/// +/// Like the x86-64 path, a binary with no patch sites is emitted as the +/// original bytes followed by a size-0 trampoline sentinel header (the arm64 +/// module signals this by returning `None`). Otherwise the output layout is +/// `[patched ELF][padding to page boundary][trampoline code][header]`. +fn hook_aarch64_elf( + input_binary: &[u8], + buf: &mut [u8], + text_sections: &[TextSectionInfo], + trampoline_base_addr: u64, + callback: u64, +) -> Result> { + let Some(outcome) = arm64::hook_syscalls_aarch64( + buf, + text_sections, + trampoline_base_addr, + callback, + arm64::Host::Linux, + )? + else { + // No patch sites: emit the original binary with a size-0 sentinel + // header so the loader knows there is no trampoline to map. + let mut out = input_binary.to_vec(); + let header = TrampolineHeader64 { + magic: *TRAMPOLINE_MAGIC, + file_offset: 0, + vaddr: 0, + trampoline_size: 0, + }; + out.extend_from_slice(header.as_bytes()); + return Ok(out); + }; + + // Build output: [patched ELF][padding to page boundary][trampoline][header]. + let mut trampoline_data = outcome.trampoline; + let mut out = buf.to_vec(); + append_trampoline_footer(&mut out, &mut trampoline_data, trampoline_base_addr, false); + + if !outcome.trapped_sites.is_empty() { + return Err(Error::UnpatchableSyscalls(format!( + "{} unpatchable instruction(s) (SVC / MSR / MRS TPIDR_EL0) at {trapped:?}", + outcome.trapped_sites.len(), + trapped = outcome.trapped_sites, + ))); + } + Ok(out) +} + /// (private) Get metadata for executable sections fn text_sections( file: &object::File<'_>, @@ -754,7 +852,7 @@ fn text_sections( /// Check if the binary is already hooked by looking for TRAMPOLINE_MAGIC at the end of the file. fn is_already_hooked(input_binary: &[u8], arch: Arch) -> bool { let header_size = match arch { - Arch::X86_64 => size_of::(), + Arch::X86_64 | Arch::Aarch64 => size_of::(), }; if input_binary.len() < header_size { @@ -773,8 +871,9 @@ fn is_already_hooked(input_binary: &[u8], arch: Arch) -> bool { (header.file_offset, header.vaddr, header.trampoline_size); if trampoline_size == 0 { - // Size=0 sentinel: the rewriter processed this binary but found no - // syscall instructions. It is already hooked (nothing to do). + // Size=0 sentinel: the rewriter processed this binary but found nothing + // to patch — no syscall instructions, and on AArch64 no `MSR`/`MRS + // TPIDR_EL0` accesses either. It is already hooked (nothing to do). return true; } if file_offset % 0x1000 != 0 { @@ -793,6 +892,7 @@ fn is_already_hooked(input_binary: &[u8], arch: Arch) -> bool { #[derive(PartialEq, Eq, Clone, Copy, Debug, Hash)] enum Arch { X86_64, + Aarch64, } /// (private) Hook all syscalls in `section`, possibly extending `trampoline_data` to do so. @@ -820,6 +920,7 @@ fn hook_syscalls_in_section( continue; } } + Arch::Aarch64 => unreachable!("AArch64 uses the arm64 module, not iced-x86"), } found_any = true; @@ -1388,6 +1489,7 @@ fn decode_section_instructions( ) -> Result> { let bitness = match arch { Arch::X86_64 => 64, + Arch::Aarch64 => unreachable!("AArch64 uses the arm64 module, not iced-x86"), }; let mut instructions = Vec::new(); @@ -1611,6 +1713,25 @@ fn hook_syscall_and_after( mod tests { use super::*; + #[test] + fn aarch64_out_of_range_site_is_rejected_as_unpatchable() { + // A trampoline mapped 256MB above the text is outside the site's ±128MB + // branch reach, so the `SVC` is trapped and the rewrite is rejected, + // mirroring the x86-64 unpatchable-syscall contract. + let mut buf = 0xD400_0001u32.to_le_bytes().to_vec(); // SVC #0 + let input = buf.clone(); + let sections = vec![TextSectionInfo { + vaddr: 0x1000, + file_offset: 0, + size: buf.len() as u64, + }]; + let err = hook_aarch64_elf(&input, &mut buf, §ions, 0x1000_0000, 0).unwrap_err(); + assert!( + matches!(err, Error::UnpatchableSyscalls(_)), + "expected UnpatchableSyscalls, got {err:?}" + ); + } + const NT_STUB_BUILD_SYSNO: u32 = 0x1234; fn nt_stub_bytes() -> [u8; 24] { diff --git a/litebox_syscall_rewriter/tests/aarch64_tests.rs b/litebox_syscall_rewriter/tests/aarch64_tests.rs new file mode 100644 index 0000000000..f663db09ca --- /dev/null +++ b/litebox_syscall_rewriter/tests/aarch64_tests.rs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Integration tests for the AArch64 (Linux) rewriter, exercised through the +//! public [`hook_syscalls_in_elf`] entry point. +//! +//! These assert byte-level invariants rather than an objdump snapshot: an +//! aarch64 objdump is not reliably available on the (x86) test host, and the +//! emitted trampoline is a clean reimplementation whose exact bytes differ from +//! the reference implementation. + +// Deliberate, range-checked casts on a 64-bit host throughout this test. +#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + +use litebox_syscall_rewriter::{TRAMPOLINE_MAGIC, hook_syscalls_in_elf}; + +const HELLO_AARCH64: &[u8] = include_bytes!("hello-aarch64"); + +/// `SVC #0`. +const SVC_0: u32 = 0xD400_0001; + +/// `MSR TPIDR_EL0, Xt` / `MRS Xd, TPIDR_EL0`: the low 5 bits select the register, +/// so mask them off to match the opcode. +const TPIDR_REG_MASK: u32 = 0xFFFF_FFE0; +const MSR_TPIDR_BITS: u32 = 0xD51B_D040; +const MRS_TPIDR_BITS: u32 = 0xD53B_D040; + +fn read_u16(data: &[u8], off: usize) -> u16 { + u16::from_le_bytes(data[off..off + 2].try_into().unwrap()) +} +fn read_u32(data: &[u8], off: usize) -> u32 { + u32::from_le_bytes(data[off..off + 4].try_into().unwrap()) +} +fn read_u64(data: &[u8], off: usize) -> u64 { + u64::from_le_bytes(data[off..off + 8].try_into().unwrap()) +} + +/// Minimal ELF64 section-header walk: returns `(file_offset, vaddr, size)` for +/// every executable (`SHF_EXECINSTR`) `PROGBITS` section. +fn exec_sections(data: &[u8]) -> Vec<(usize, u64, usize)> { + let e_shoff = read_u64(data, 40) as usize; + let e_shentsize = read_u16(data, 58) as usize; + let e_shnum = read_u16(data, 60) as usize; + let mut out = Vec::new(); + for i in 0..e_shnum { + let base = e_shoff + i * e_shentsize; + let sh_type = read_u32(data, base + 4); + let sh_flags = read_u64(data, base + 8); + let sh_addr = read_u64(data, base + 16); + let sh_offset = read_u64(data, base + 24) as usize; + let sh_size = read_u64(data, base + 32) as usize; + // SHT_PROGBITS = 1, SHF_EXECINSTR = 0x4. + if sh_type == 1 && (sh_flags & 0x4) != 0 { + out.push((sh_offset, sh_addr, sh_size)); + } + } + out +} + +/// File offsets and virtual addresses of every `SVC #0` in the executable +/// sections of `data`. +fn svc_sites(data: &[u8]) -> Vec<(usize, u64)> { + let mut sites = Vec::new(); + for (file_off, vaddr, size) in exec_sections(data) { + let mut i = 0; + while i + 4 <= size { + if read_u32(data, file_off + i) == SVC_0 { + sites.push((file_off + i, vaddr + i as u64)); + } + i += 4; + } + } + sites +} + +/// File offset of the first instruction in `data`'s executable sections whose +/// bits satisfy `(insn & mask) == bits`, if any. +fn first_site(data: &[u8], mask: u32, bits: u32) -> Option { + for (file_off, _vaddr, size) in exec_sections(data) { + let mut i = 0; + while i + 4 <= size { + if read_u32(data, file_off + i) & mask == bits { + return Some(file_off + i); + } + i += 4; + } + } + None +} + +/// Decode the trailing [`TrampolineHeader64`]: `(file_offset, vaddr, size)`. +fn trampoline_header(out: &[u8]) -> (u64, u64, u64) { + let header = &out[out.len() - 32..]; + assert_eq!(&header[..8], TRAMPOLINE_MAGIC, "trampoline magic mismatch"); + ( + read_u64(header, 8), + read_u64(header, 16), + read_u64(header, 24), + ) +} + +#[test] +fn aarch64_hello_world_is_hooked() { + let original_sites = svc_sites(HELLO_AARCH64); + assert_eq!(original_sites.len(), 3, "expected 3 SVC sites in fixture"); + + let callback = 0xDEAD_0000u64; + let out = hook_syscalls_in_elf(HELLO_AARCH64, Some(callback)).unwrap(); + + // Output grew: original (patched, same length) + padding + trampoline + header. + assert!(out.len() > HELLO_AARCH64.len()); + + // --- Trailing header invariants --- + let (file_offset, vaddr, size) = trampoline_header(&out); + assert!( + size != 0, + "fixture has SVC sites, so a trampoline is emitted" + ); + assert_eq!( + file_offset % 0x1000, + 0, + "trampoline file offset page-aligned" + ); + assert_eq!(vaddr % 0x1000, 0, "trampoline vaddr page-aligned"); + assert_eq!( + file_offset + size, + (out.len() - 32) as u64, + "trampoline must end right before the 32-byte header" + ); + + // --- Trampoline prologue invariants --- + let tramp = &out[file_offset as usize..(file_offset + size) as usize]; + // Offset 0: callback slot holds the value we passed in. + assert_eq!(read_u64(tramp, 0), callback, "callback slot"); + // Offset 8: the shared SVC handler — LDR X16,; BR X16. + assert_eq!( + read_u32(tramp, 8), + 0x58FF_FFD0, + "LDR X16, (pcrel -8)" + ); + assert_eq!(read_u32(tramp, 12), 0xD61F_0200, "BR X16"); + + // --- Every SVC became a branch into the trampoline region --- + let tramp_range = vaddr..(vaddr + size); + for (file_off, site_vaddr) in &original_sites { + let word = read_u32(&out, *file_off); + assert_eq!( + word & 0xFC00_0000, + 0x1400_0000, + "SVC at {site_vaddr:#x} should be rewritten to B" + ); + // Reconstruct the branch target and confirm it lands in the trampoline. + let imm26 = i64::from(word & 0x03FF_FFFF); + // Sign-extend the 26-bit immediate, then scale by 4. + let disp = (imm26 << 38) >> 38 << 2; + let target = site_vaddr.wrapping_add(disp as u64); + assert!( + tramp_range.contains(&target), + "branch target {target:#x} not in trampoline range {tramp_range:?}" + ); + } + + // --- Thread-pointer handling --- + // The `MSR TPIDR_EL0` write is virtualized: rewritten to a branch into the + // trampoline's MSR gate. + let msr_off = first_site(HELLO_AARCH64, TPIDR_REG_MASK, MSR_TPIDR_BITS) + .expect("fixture has an MSR TPIDR_EL0 write"); + assert_eq!( + read_u32(&out, msr_off) & 0xFC00_0000, + 0x1400_0000, + "MSR TPIDR_EL0 should be rewritten to B" + ); + + // The `MRS TPIDR_EL0` read is virtualized: rewritten to a branch into the + // MRS gate. + let mrs_off = first_site(HELLO_AARCH64, TPIDR_REG_MASK, MRS_TPIDR_BITS) + .expect("fixture has an MRS TPIDR_EL0 read"); + assert_eq!( + read_u32(&out, mrs_off) & 0xFC00_0000, + 0x1400_0000, + "MRS TPIDR_EL0 should be rewritten to B" + ); +} + +#[test] +fn aarch64_rehooking_is_idempotent() { + let out = hook_syscalls_in_elf(HELLO_AARCH64, Some(0)).unwrap(); + // Running the rewriter on an already-hooked binary returns it unchanged. + let again = hook_syscalls_in_elf(&out, Some(0)).unwrap(); + assert_eq!( + again, out, + "already-hooked binary must be returned unchanged" + ); +} diff --git a/litebox_syscall_rewriter/tests/hello-aarch64 b/litebox_syscall_rewriter/tests/hello-aarch64 new file mode 100644 index 0000000000000000000000000000000000000000..e9fce87199aab2078dfeefd46ea01a9dafba1c7b GIT binary patch literal 1040 zcmd6ly-LJD5XUDUClQ+n3c@w#M;!!3M4Kp_a3H?Iy5|K_oE{`woRukUeG4mFd!HZ# z?*V)SpFn4JXCp+{#>Oe~pZU#yhU_MH!}BxW^T37=&p=h|r5c$hK<(?2*&%vT=s~Ex zy{{H#DOW~QUTqch1Y7>04D0@4R39&*sA>jKHE#htym(cE$9TP8H~P3zHGn;0za1gK zv)X&KKDv$&-al+@byX+d{oBZ6+>?4I)~a>Q!M7ZIk9qff5{LiD!A~80CE%6?EUok<9T_f!?Y%`OP&F?TOQ=T8N2FPeOq9(?M5m-j0UNsm_x Q(NFVFJipmGP12qJ1ek3}5C8xG literal 0 HcmV?d00001 diff --git a/litebox_syscall_rewriter/tests/snapshot_tests.rs b/litebox_syscall_rewriter/tests/snapshot_tests.rs index 1efe0be826..2afc255a17 100644 --- a/litebox_syscall_rewriter/tests/snapshot_tests.rs +++ b/litebox_syscall_rewriter/tests/snapshot_tests.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -fn objdump(binary: &[u8]) -> String { +fn objdump(objdump_cmd: &str, binary: &[u8]) -> String { use std::io::Write; use std::process::Command; use tempfile::NamedTempFile; @@ -11,7 +11,7 @@ fn objdump(binary: &[u8]) -> String { temp_file.write_all(binary).unwrap(); // Run objdump on the temporary file and capture the output - let output = Command::new("objdump") + let output = Command::new(objdump_cmd) .arg("-d") .arg(temp_file.path()) .output() @@ -25,6 +25,21 @@ fn objdump(binary: &[u8]) -> String { .join("\n") } +/// Return the first objdump-like command that exists on the host from +/// `candidates`, or `None` if none are available. +fn find_objdump(candidates: &[&str]) -> Option { + use std::process::Command; + candidates + .iter() + .find(|cmd| { + Command::new(cmd) + .arg("--version") + .output() + .is_ok_and(|o| o.status.success()) + }) + .map(|cmd| (*cmd).to_owned()) +} + fn trampoline_range(binary: &[u8]) -> Option> { const MAGIC: &[u8; 8] = litebox_syscall_rewriter::TRAMPOLINE_MAGIC; @@ -49,33 +64,38 @@ fn normalize_objdump_line(line: &str, trampoline_range: Option<&std::ops::Range< return line.trim_end().to_owned(); }; let tokens: Vec<_> = rest.split_whitespace().collect(); - let Some((mnemonic_idx, mnemonic)) = tokens - .iter() - .enumerate() - .find(|(_, token)| !token.chars().all(|ch| ch.is_ascii_hexdigit())) - else { - return line.trim_end().to_owned(); - }; - if *mnemonic == "jmp" - && let Some(target) = tokens - .get(mnemonic_idx + 1) - .and_then(|token| u64::from_str_radix(token.trim_start_matches("0x"), 16).ok()) - && trampoline_range.contains(&target) - { - let offset = target - trampoline_range.start; - return format!("{address}:\t"); + + // A control-transfer into the trampoline appears as a branch mnemonic + // (`jmp` on x86, `b`/`bl` on AArch64) followed by an absolute target. When + // that target lands in the trampoline region, render it relative to the + // trampoline base so the snapshot is independent of the trampoline's exact + // address. Other branches (and same-mnemonic branches that stay in the + // original code) are left untouched. + for (i, token) in tokens.iter().enumerate() { + if !matches!(*token, "jmp" | "b" | "bl") { + continue; + } + if let Some(target) = tokens + .get(i + 1) + .and_then(|t| u64::from_str_radix(t.trim_start_matches("0x"), 16).ok()) + && trampoline_range.contains(&target) + { + let offset = target - trampoline_range.start; + return format!("{address}:\t"); + } } line.trim_end().to_owned() } const HELLO_INPUT_64: &[u8] = include_bytes!("hello"); +const HELLO_INPUT_AARCH64: &[u8] = include_bytes!("hello-aarch64"); -fn run_snapshot_test(input: &[u8], snapshot: &str) { +fn run_snapshot_test(objdump_cmd: &str, input: &[u8], snapshot: &str) { let output = litebox_syscall_rewriter::hook_syscalls_in_elf(input, None).unwrap(); let diff = similar::udiff::unified_diff( similar::Algorithm::Myers, - &objdump(input), - &objdump(&output), + &objdump(objdump_cmd, input), + &objdump(objdump_cmd, &output), 3, Some(("original", "rewritten")), ); @@ -85,5 +105,27 @@ fn run_snapshot_test(input: &[u8], snapshot: &str) { #[test] fn snapshot_test_hello_world_x86_64() { - run_snapshot_test(HELLO_INPUT_64, "hello-diff"); + run_snapshot_test("objdump", HELLO_INPUT_64, "hello-diff"); +} + +#[test] +fn snapshot_test_hello_world_aarch64() { + // The `hello-aarch64` fixture exercises every rewrite path: an `MSR + // TPIDR_EL0` write (→ branch into an MSR gate), an `MRS TPIDR_EL0` read + // (→ branch into an MRS gate), and several `SVC #0`s. Only `MRS XZR, + // TPIDR_EL0` is left native, and the fixture has none. + // objdump only disassembles the original `.text`, so the diff captures the + // call-site rewriting, not the appended trampoline's gate internals. + // + // The host objdump usually cannot disassemble AArch64; prefer a cross or + // LLVM objdump. Skip (rather than fail) when no capable tool is installed, + // so x86-only dev environments still pass. + let Some(objdump_cmd) = find_objdump(&["aarch64-linux-gnu-objdump", "llvm-objdump"]) else { + eprintln!( + "skipping snapshot_test_hello_world_aarch64: no AArch64-capable objdump \ + (install binutils-aarch64-linux-gnu or llvm)" + ); + return; + }; + run_snapshot_test(&objdump_cmd, HELLO_INPUT_AARCH64, "hello-aarch64-diff"); } diff --git a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-aarch64-diff.snap b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-aarch64-diff.snap new file mode 100644 index 0000000000..2f1dd63c73 --- /dev/null +++ b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-aarch64-diff.snap @@ -0,0 +1,29 @@ +--- +source: litebox_syscall_rewriter/tests/snapshot_tests.rs +expression: diff +--- +--- original ++++ rewritten +@@ -4,15 +4,15 @@ + Disassembly of section .text: + + 0000000000400110 <_start>: +- 400110: d51bd045 msr tpidr_el0, x5 +- 400114: d53bd049 mrs x9, tpidr_el0 ++ 400110: ++ 400114: + 400118: d2800808 mov x8, #0x40 // #64 + 40011c: d2800020 mov x0, #0x1 // #1 + 400120: 910003e1 mov x1, sp + 400124: d28001c2 mov x2, #0xe // #14 +- 400128: d4000001 svc #0x0 ++ 400128: + 40012c: d2801588 mov x8, #0xac // #172 +- 400130: d4000001 svc #0x0 ++ 400130: + 400134: d2800ba8 mov x8, #0x5d // #93 + 400138: d2800000 mov x0, #0x0 // #0 +- 40013c: d4000001 svc #0x0 +\ No newline at end of file ++ 40013c: +\ No newline at end of file From 8907e4829be20b2929952845b5414ce1f97995a5 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 2 Jun 2026 11:07:16 -0700 Subject: [PATCH 018/319] Cherry-pick preadv/pwritev to ulitebox (#892) Cherry-picks 46edacbf6ae4ae23254a468998e7408c32a8b0c3 ("Add preadv/pwritev (#864)") onto `ulitebox`. --- litebox_common_linux/src/errno/mod.rs | 4 +- litebox_common_linux/src/lib.rs | 16 + litebox_runner_linux_userland/tests/iov_max.c | 181 +++++++++ litebox_runner_linux_userland/tests/preadv.c | 169 ++++++++ litebox_runner_linux_userland/tests/pwritev.c | 189 +++++++++ litebox_shim_linux/src/lib.rs | 24 ++ litebox_shim_linux/src/syscalls/file.rs | 374 +++++++++++++----- 7 files changed, 851 insertions(+), 106 deletions(-) create mode 100644 litebox_runner_linux_userland/tests/iov_max.c create mode 100644 litebox_runner_linux_userland/tests/preadv.c create mode 100644 litebox_runner_linux_userland/tests/pwritev.c diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 044959b6b4..5153a83fa5 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -187,7 +187,7 @@ impl From for Errno { fn from(value: litebox::fs::errors::ReadError) -> Self { match value { litebox::fs::errors::ReadError::NotAFile => Errno::EISDIR, - litebox::fs::errors::ReadError::NotForReading => Errno::EACCES, + litebox::fs::errors::ReadError::NotForReading => Errno::EBADF, litebox::fs::errors::ReadError::Io => Errno::EIO, _ => unimplemented!(), } @@ -198,7 +198,7 @@ impl From for Errno { fn from(value: litebox::fs::errors::WriteError) -> Self { match value { litebox::fs::errors::WriteError::NotAFile => Errno::EISDIR, - litebox::fs::errors::WriteError::NotForWriting => Errno::EACCES, + litebox::fs::errors::WriteError::NotForWriting => Errno::EBADF, litebox::fs::errors::WriteError::Io => Errno::EIO, _ => unimplemented!(), } diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 58a43aa6b2..0521f95148 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -2077,6 +2077,20 @@ pub enum SyscallRequest { iovec: Platform::RawConstPointer>>, iovcnt: usize, }, + Preadv { + fd: i32, + iovec: Platform::RawConstPointer>>, + iovcnt: usize, + pos_l: usize, + pos_h: usize, + }, + Pwritev { + fd: i32, + iovec: Platform::RawConstPointer>>, + iovcnt: usize, + pos_l: usize, + pos_h: usize, + }, Access { pathname: Platform::RawConstPointer, mode: AccessFlags, @@ -2573,6 +2587,8 @@ impl SyscallRequest { }), Sysno::readv => sys_req!(Readv { fd, iovec:*, iovcnt }), Sysno::writev => sys_req!(Writev { fd, iovec:*, iovcnt }), + Sysno::preadv => sys_req!(Preadv { fd, iovec:*, iovcnt, pos_l, pos_h }), + Sysno::pwritev => sys_req!(Pwritev { fd, iovec:*, iovcnt, pos_l, pos_h }), Sysno::access => sys_req!(Access { pathname:*, mode }), Sysno::pipe => sys_req!(Pipe2 { pipefd:*, flags: { litebox::fs::OFlags::empty() } }), Sysno::pipe2 => sys_req!(Pipe2 { pipefd:* ,flags }), diff --git a/litebox_runner_linux_userland/tests/iov_max.c b/litebox_runner_linux_userland/tests/iov_max.c new file mode 100644 index 0000000000..dc9e8c37be --- /dev/null +++ b/litebox_runner_linux_userland/tests/iov_max.c @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Tests: readv/writev/preadv/pwritev reject iovcnt > IOV_MAX (1024) and +// negative iovcnt with EINVAL, matching Linux's documented behavior. The +// boundary value IOV_MAX itself must still succeed. + +#include "helpers.h" + +#include +#include +#include + +// Linux documents IOV_MAX as 1024 in ; the kernel's UIO_MAXIOV +// matches. Use the constant rather than the header value so the test asserts +// the documented Linux contract even if a vendor's limits.h drifts. +#define LB_IOV_MAX 1024 + +static ssize_t raw_preadv(int fd, const struct iovec *iov, long iovcnt, + off_t offset) { + return syscall(SYS_preadv, (long)fd, iov, iovcnt, (long)offset, 0L); +} + +static ssize_t raw_pwritev(int fd, const struct iovec *iov, long iovcnt, + off_t offset) { + return syscall(SYS_pwritev, (long)fd, iov, iovcnt, (long)offset, 0L); +} + +static ssize_t raw_readv(int fd, const struct iovec *iov, long iovcnt) { + return syscall(SYS_readv, (long)fd, iov, iovcnt); +} + +static ssize_t raw_writev(int fd, const struct iovec *iov, long iovcnt) { + return syscall(SYS_writev, (long)fd, iov, iovcnt); +} + +// A 1025-entry iov array pointing into a single byte. Sizes are 1 so the +// boundary call (iovcnt == IOV_MAX) succeeds with a small total transfer. +static char iov_byte; +static struct iovec iov_array[LB_IOV_MAX + 1]; + +static void seed_iov_array(void) { + for (size_t i = 0; i < sizeof(iov_array) / sizeof(iov_array[0]); i++) { + iov_array[i].iov_base = &iov_byte; + iov_array[i].iov_len = 1; + } +} + +static int open_seeded(const char *path) { + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "open test file failed"); + // Seed at least IOV_MAX bytes so the boundary readv/preadv has data to read. + char buf[LB_IOV_MAX]; + memset(buf, 'A', sizeof(buf)); + TEST_ASSERT(write(fd, buf, sizeof(buf)) == (ssize_t)sizeof(buf), + "seed write failed"); + TEST_ASSERT(lseek(fd, 0, SEEK_SET) == 0, "seed rewind failed"); + return fd; +} + +static void test_readv(void) { + const char *path = "/tmp/test_iov_max_readv.bin"; + int fd = open_seeded(path); + + errno = 0; + TEST_ASSERT(raw_readv(fd, iov_array, LB_IOV_MAX + 1) == -1 && errno == EINVAL, + "readv with iovcnt > IOV_MAX should fail with EINVAL"); + + errno = 0; + TEST_ASSERT(raw_readv(fd, iov_array, -1) == -1 && errno == EINVAL, + "readv with negative iovcnt should fail with EINVAL"); + + TEST_ASSERT(lseek(fd, 0, SEEK_SET) == 0, "rewind before boundary call failed"); + TEST_ASSERT(raw_readv(fd, iov_array, LB_IOV_MAX) == LB_IOV_MAX, + "readv with iovcnt == IOV_MAX should succeed"); + + close(fd); + unlink(path); +} + +static void test_writev(void) { + const char *path = "/tmp/test_iov_max_writev.bin"; + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "open writev test file failed"); + + errno = 0; + TEST_ASSERT(raw_writev(fd, iov_array, LB_IOV_MAX + 1) == -1 && errno == EINVAL, + "writev with iovcnt > IOV_MAX should fail with EINVAL"); + + errno = 0; + TEST_ASSERT(raw_writev(fd, iov_array, -1) == -1 && errno == EINVAL, + "writev with negative iovcnt should fail with EINVAL"); + + TEST_ASSERT(raw_writev(fd, iov_array, LB_IOV_MAX) == LB_IOV_MAX, + "writev with iovcnt == IOV_MAX should succeed"); + + close(fd); + unlink(path); +} + +static void test_preadv(void) { + const char *path = "/tmp/test_iov_max_preadv.bin"; + int fd = open_seeded(path); + + errno = 0; + TEST_ASSERT(raw_preadv(fd, iov_array, LB_IOV_MAX + 1, 0) == -1 && errno == EINVAL, + "preadv with iovcnt > IOV_MAX should fail with EINVAL"); + + errno = 0; + TEST_ASSERT(raw_preadv(fd, iov_array, -1, 0) == -1 && errno == EINVAL, + "preadv with negative iovcnt should fail with EINVAL"); + + TEST_ASSERT(raw_preadv(fd, iov_array, LB_IOV_MAX, 0) == LB_IOV_MAX, + "preadv with iovcnt == IOV_MAX should succeed"); + + close(fd); + unlink(path); +} + +static void test_pwritev(void) { + const char *path = "/tmp/test_iov_max_pwritev.bin"; + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "open pwritev test file failed"); + + errno = 0; + TEST_ASSERT(raw_pwritev(fd, iov_array, LB_IOV_MAX + 1, 0) == -1 && + errno == EINVAL, + "pwritev with iovcnt > IOV_MAX should fail with EINVAL"); + + errno = 0; + TEST_ASSERT(raw_pwritev(fd, iov_array, -1, 0) == -1 && errno == EINVAL, + "pwritev with negative iovcnt should fail with EINVAL"); + + TEST_ASSERT(raw_pwritev(fd, iov_array, LB_IOV_MAX, 0) == LB_IOV_MAX, + "pwritev with iovcnt == IOV_MAX should succeed"); + + close(fd); + unlink(path); +} + +static void test_error_precedence(void) { + errno = 0; + TEST_ASSERT(raw_readv(-1, iov_array, LB_IOV_MAX + 1) == -1 && errno == EBADF, + "readv with bad fd and oversized iovcnt should fail with EBADF"); + + errno = 0; + TEST_ASSERT(raw_writev(-1, iov_array, LB_IOV_MAX + 1) == -1 && errno == EBADF, + "writev with bad fd and oversized iovcnt should fail with EBADF"); + + errno = 0; + TEST_ASSERT(raw_preadv(-1, iov_array, LB_IOV_MAX + 1, -1) == -1 && + errno == EINVAL, + "preadv with negative offset should fail before fd and iovcnt checks"); + + errno = 0; + TEST_ASSERT(raw_pwritev(-1, iov_array, LB_IOV_MAX + 1, -1) == -1 && + errno == EINVAL, + "pwritev with negative offset should fail before fd and iovcnt checks"); + + errno = 0; + TEST_ASSERT(raw_preadv(-1, iov_array, LB_IOV_MAX + 1, 0) == -1 && + errno == EBADF, + "preadv with bad fd and oversized iovcnt should fail with EBADF"); + + errno = 0; + TEST_ASSERT(raw_pwritev(-1, iov_array, LB_IOV_MAX + 1, 0) == -1 && + errno == EBADF, + "pwritev with bad fd and oversized iovcnt should fail with EBADF"); +} + +int main(void) { + printf("===== iov_max tests =====\n"); + seed_iov_array(); + test_readv(); + test_writev(); + test_preadv(); + test_pwritev(); + test_error_precedence(); + printf("All iov_max tests passed.\n"); + return 0; +} diff --git a/litebox_runner_linux_userland/tests/preadv.c b/litebox_runner_linux_userland/tests/preadv.c new file mode 100644 index 0000000000..ebed58c00b --- /dev/null +++ b/litebox_runner_linux_userland/tests/preadv.c @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Tests: preadv positional vectored read. + +#include "helpers.h" + +#include +#include + +// The kernel's preadv syscall takes (fd, vec, vlen, pos_l, pos_h). On 64-bit +// platforms pos_h is unused and must be 0. +static ssize_t raw_preadv(int fd, const struct iovec *iov, int iovcnt, + off_t offset) { + return syscall(SYS_preadv, (long)fd, iov, (long)iovcnt, (long)offset, 0L); +} + +static const char kAlphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +static const size_t kAlphabetLen = sizeof(kAlphabet) - 1; + +static int open_alphabet_file(const char *path) { + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "open test file failed"); + TEST_ASSERT(write(fd, kAlphabet, kAlphabetLen) == (ssize_t)kAlphabetLen, + "seed write failed"); + TEST_ASSERT(lseek(fd, 0, SEEK_SET) == 0, "rewind seed failed"); + return fd; +} + +static void test_happy_path(void) { + const char *path = "/tmp/test_preadv_happy.bin"; + int fd = open_alphabet_file(path); + + char buf1[5]; + char buf2[5]; + memset(buf1, 0, sizeof(buf1)); + memset(buf2, 0, sizeof(buf2)); + struct iovec iov[2] = { + {.iov_base = buf1, .iov_len = sizeof(buf1)}, + {.iov_base = buf2, .iov_len = sizeof(buf2)}, + }; + + ssize_t n = raw_preadv(fd, iov, 2, 0); + TEST_ASSERT(n == 10, "preadv at offset 0 should return 10"); + TEST_ASSERT(memcmp(buf1, "ABCDE", 5) == 0, "first iov mismatch at offset 0"); + TEST_ASSERT(memcmp(buf2, "FGHIJ", 5) == 0, "second iov mismatch at offset 0"); + + // File position must be unchanged by preadv. + off_t pos = lseek(fd, 0, SEEK_CUR); + TEST_ASSERT(pos == 0, "preadv must not advance the file offset"); + + memset(buf1, 0, sizeof(buf1)); + memset(buf2, 0, sizeof(buf2)); + n = raw_preadv(fd, iov, 2, 10); + TEST_ASSERT(n == 10, "preadv at offset 10 should return 10"); + TEST_ASSERT(memcmp(buf1, "KLMNO", 5) == 0, "first iov mismatch at offset 10"); + TEST_ASSERT(memcmp(buf2, "PQRST", 5) == 0, "second iov mismatch at offset 10"); + + pos = lseek(fd, 0, SEEK_CUR); + TEST_ASSERT(pos == 0, "preadv must still not advance the offset"); + + close(fd); + unlink(path); +} + +static void test_short_read_at_eof(void) { + const char *path = "/tmp/test_preadv_eof.bin"; + int fd = open_alphabet_file(path); + + char buf1[10]; + char buf2[10]; + memset(buf1, 0xff, sizeof(buf1)); + memset(buf2, 0xff, sizeof(buf2)); + struct iovec iov[2] = { + {.iov_base = buf1, .iov_len = sizeof(buf1)}, + {.iov_base = buf2, .iov_len = sizeof(buf2)}, + }; + + // Only 6 bytes left starting at offset 20. + ssize_t n = raw_preadv(fd, iov, 2, 20); + TEST_ASSERT(n == 6, "preadv near EOF should return only the remaining bytes"); + TEST_ASSERT(memcmp(buf1, "UVWXYZ", 6) == 0, "EOF short-read content mismatch"); + + // At EOF returns 0. + n = raw_preadv(fd, iov, 2, (off_t)kAlphabetLen); + TEST_ASSERT(n == 0, "preadv at EOF should return 0"); + + // Past EOF returns 0. + n = raw_preadv(fd, iov, 2, (off_t)kAlphabetLen + 100); + TEST_ASSERT(n == 0, "preadv past EOF should return 0"); + + close(fd); + unlink(path); +} + +static void test_zero_iovcnt(void) { + const char *path = "/tmp/test_preadv_zero.bin"; + int fd = open_alphabet_file(path); + + struct iovec iov[1] = {{.iov_base = NULL, .iov_len = 0}}; + ssize_t n = raw_preadv(fd, iov, 0, 0); + TEST_ASSERT(n == 0, "preadv with iovcnt 0 should return 0"); + + close(fd); + unlink(path); +} + +static void test_bad_fd(void) { + char buf[4]; + struct iovec iov[1] = {{.iov_base = buf, .iov_len = sizeof(buf)}}; + + errno = 0; + ssize_t n = raw_preadv(-1, iov, 1, 0); + TEST_ASSERT(n == -1 && errno == EBADF, "preadv on fd -1 should fail with EBADF"); + + // Open then close to get a known-invalid fd value. + int fd = open("/tmp/test_preadv_bad.bin", O_RDWR | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "open for closed-fd test failed"); + close(fd); + unlink("/tmp/test_preadv_bad.bin"); + + errno = 0; + n = raw_preadv(fd, iov, 1, 0); + TEST_ASSERT(n == -1 && errno == EBADF, + "preadv on closed fd should fail with EBADF"); +} + +static void test_negative_offset(void) { + const char *path = "/tmp/test_preadv_negoff.bin"; + int fd = open_alphabet_file(path); + + char buf[4]; + struct iovec iov[1] = {{.iov_base = buf, .iov_len = sizeof(buf)}}; + + errno = 0; + ssize_t n = raw_preadv(fd, iov, 1, -1); + TEST_ASSERT(n == -1 && errno == EINVAL, + "preadv with negative offset should fail with EINVAL"); + + close(fd); + unlink(path); +} + +static void test_pipe_espipe(void) { + int p[2]; + TEST_ASSERT(pipe(p) == 0, "pipe creation failed"); + + char buf[4]; + struct iovec iov[1] = {{.iov_base = buf, .iov_len = sizeof(buf)}}; + errno = 0; + ssize_t n = raw_preadv(p[0], iov, 1, 0); + TEST_ASSERT(n == -1 && errno == ESPIPE, + "preadv on a pipe should fail with ESPIPE"); + + close(p[0]); + close(p[1]); +} + +int main(void) { + printf("===== preadv tests =====\n"); + test_happy_path(); + test_short_read_at_eof(); + test_zero_iovcnt(); + test_bad_fd(); + test_negative_offset(); + test_pipe_espipe(); + printf("All preadv tests passed.\n"); + return 0; +} diff --git a/litebox_runner_linux_userland/tests/pwritev.c b/litebox_runner_linux_userland/tests/pwritev.c new file mode 100644 index 0000000000..42e3710070 --- /dev/null +++ b/litebox_runner_linux_userland/tests/pwritev.c @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Tests: pwritev positional vectored write. + +#include "helpers.h" + +#include +#include + +// The kernel's pwritev syscall takes (fd, vec, vlen, pos_l, pos_h). On 64-bit +// platforms pos_h is unused and must be 0. +static ssize_t raw_pwritev(int fd, const struct iovec *iov, int iovcnt, + off_t offset) { + return syscall(SYS_pwritev, (long)fd, iov, (long)iovcnt, (long)offset, 0L); +} + +static int open_blank_file(const char *path) { + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "open test file failed"); + return fd; +} + +static void test_happy_path(void) { + const char *path = "/tmp/test_pwritev_happy.bin"; + int fd = open_blank_file(path); + + char a[5] = "ABCDE"; + char b[5] = "FGHIJ"; + struct iovec iov[2] = { + {.iov_base = a, .iov_len = sizeof(a)}, + {.iov_base = b, .iov_len = sizeof(b)}, + }; + + ssize_t n = raw_pwritev(fd, iov, 2, 0); + TEST_ASSERT(n == 10, "pwritev at offset 0 should return 10"); + + // pwritev must not advance the file offset. + off_t pos = lseek(fd, 0, SEEK_CUR); + TEST_ASSERT(pos == 0, "pwritev must not advance the file offset"); + + char readback[10]; + memset(readback, 0, sizeof(readback)); + TEST_ASSERT(pread(fd, readback, sizeof(readback), 0) == 10, "readback failed"); + TEST_ASSERT(memcmp(readback, "ABCDEFGHIJ", 10) == 0, + "readback content mismatch at offset 0"); + + char c[5] = "KLMNO"; + char d[5] = "PQRST"; + struct iovec iov2[2] = { + {.iov_base = c, .iov_len = sizeof(c)}, + {.iov_base = d, .iov_len = sizeof(d)}, + }; + n = raw_pwritev(fd, iov2, 2, 10); + TEST_ASSERT(n == 10, "pwritev at offset 10 should return 10"); + + pos = lseek(fd, 0, SEEK_CUR); + TEST_ASSERT(pos == 0, "pwritev must still not advance the offset"); + + char full[20]; + memset(full, 0, sizeof(full)); + TEST_ASSERT(pread(fd, full, sizeof(full), 0) == 20, "full readback failed"); + TEST_ASSERT(memcmp(full, "ABCDEFGHIJKLMNOPQRST", 20) == 0, + "full content mismatch after second pwritev"); + + close(fd); + unlink(path); +} + +static void test_extends_file(void) { + const char *path = "/tmp/test_pwritev_extend.bin"; + int fd = open_blank_file(path); + + char buf[4] = "WXYZ"; + struct iovec iov[1] = {{.iov_base = buf, .iov_len = sizeof(buf)}}; + + ssize_t n = raw_pwritev(fd, iov, 1, 100); + TEST_ASSERT(n == 4, "pwritev past EOF should extend the file"); + + off_t end = lseek(fd, 0, SEEK_END); + TEST_ASSERT(end == 104, "file size should be offset + bytes written"); + + char readback[4]; + TEST_ASSERT(pread(fd, readback, sizeof(readback), 100) == 4, "readback failed"); + TEST_ASSERT(memcmp(readback, "WXYZ", 4) == 0, + "extended file content mismatch"); + + close(fd); + unlink(path); +} + +static void test_zero_iovcnt(void) { + const char *path = "/tmp/test_pwritev_zero.bin"; + int fd = open_blank_file(path); + + struct iovec iov[1] = {{.iov_base = NULL, .iov_len = 0}}; + ssize_t n = raw_pwritev(fd, iov, 0, 0); + TEST_ASSERT(n == 0, "pwritev with iovcnt 0 should return 0"); + + off_t end = lseek(fd, 0, SEEK_END); + TEST_ASSERT(end == 0, "file should still be empty"); + + close(fd); + unlink(path); +} + +static void test_bad_fd(void) { + char buf[4] = "data"; + struct iovec iov[1] = {{.iov_base = buf, .iov_len = sizeof(buf)}}; + + errno = 0; + ssize_t n = raw_pwritev(-1, iov, 1, 0); + TEST_ASSERT(n == -1 && errno == EBADF, "pwritev on fd -1 should fail with EBADF"); + + int fd = open("/tmp/test_pwritev_bad.bin", O_RDWR | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "open for closed-fd test failed"); + close(fd); + unlink("/tmp/test_pwritev_bad.bin"); + + errno = 0; + n = raw_pwritev(fd, iov, 1, 0); + TEST_ASSERT(n == -1 && errno == EBADF, + "pwritev on closed fd should fail with EBADF"); +} + +static void test_negative_offset(void) { + const char *path = "/tmp/test_pwritev_negoff.bin"; + int fd = open_blank_file(path); + + char buf[4] = "data"; + struct iovec iov[1] = {{.iov_base = buf, .iov_len = sizeof(buf)}}; + + errno = 0; + ssize_t n = raw_pwritev(fd, iov, 1, -1); + TEST_ASSERT(n == -1 && errno == EINVAL, + "pwritev with negative offset should fail with EINVAL"); + + close(fd); + unlink(path); +} + +static void test_pipe_espipe(void) { + int p[2]; + TEST_ASSERT(pipe(p) == 0, "pipe creation failed"); + + char buf[4] = "data"; + struct iovec iov[1] = {{.iov_base = buf, .iov_len = sizeof(buf)}}; + errno = 0; + ssize_t n = raw_pwritev(p[1], iov, 1, 0); + TEST_ASSERT(n == -1 && errno == ESPIPE, + "pwritev on a pipe should fail with ESPIPE"); + + close(p[0]); + close(p[1]); +} + +static void test_readonly_fd(void) { + const char *path = "/tmp/test_pwritev_ro.bin"; + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "open rw for setup failed"); + TEST_ASSERT(write(fd, "x", 1) == 1, "setup write failed"); + close(fd); + + fd = open(path, O_RDONLY); + TEST_ASSERT(fd >= 0, "reopen read-only failed"); + + char buf[4] = "data"; + struct iovec iov[1] = {{.iov_base = buf, .iov_len = sizeof(buf)}}; + errno = 0; + ssize_t n = raw_pwritev(fd, iov, 1, 0); + TEST_ASSERT(n == -1 && errno == EBADF, + "pwritev on read-only fd should fail with EBADF"); + + close(fd); + unlink(path); +} + +int main(void) { + printf("===== pwritev tests =====\n"); + test_happy_path(); + test_extends_file(); + test_zero_iovcnt(); + test_bad_fd(); + test_negative_offset(); + test_pipe_espipe(); + test_readonly_fd(); + printf("All pwritev tests passed.\n"); + return 0; +} diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 67a8fadf65..8c0f7144fd 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -75,6 +75,16 @@ fn log_unsupported_fmt(args: core::fmt::Arguments<'_>) { } } +#[cfg(target_pointer_width = "64")] +fn preadv_pwritev_offset(pos_l: usize, _pos_h: usize) -> i64 { + pos_l.reinterpret_as_signed() as i64 +} + +#[cfg(target_pointer_width = "32")] +fn preadv_pwritev_offset(pos_l: usize, pos_h: usize) -> i64 { + ((pos_h as u64) << 32 | pos_l as u64).reinterpret_as_signed() +} + pub struct LinuxShimEntrypoints { task: Task, // The task should not be moved once it's bound to a platform thread so that @@ -644,6 +654,20 @@ impl Task { SyscallRequest::Brk { addr } => self.sys_brk(addr), SyscallRequest::Readv { fd, iovec, iovcnt } => self.sys_readv(fd, iovec, iovcnt), SyscallRequest::Writev { fd, iovec, iovcnt } => self.sys_writev(fd, iovec, iovcnt), + SyscallRequest::Preadv { + fd, + iovec, + iovcnt, + pos_l, + pos_h, + } => self.sys_preadv(fd, iovec, iovcnt, preadv_pwritev_offset(pos_l, pos_h)), + SyscallRequest::Pwritev { + fd, + iovec, + iovcnt, + pos_l, + pos_h, + } => self.sys_pwritev(fd, iovec, iovcnt, preadv_pwritev_offset(pos_l, pos_h)), SyscallRequest::Access { pathname, mode } => pathname .to_cstring() .map_or(Err(Errno::EFAULT), |path| syscall!(sys_access(path, mode))), diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 1ab1b2505f..c7561b1cbe 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -12,6 +12,7 @@ use litebox::{ event::{Events, wait::WaitError}, fd::{FdEnabledSubsystem, MetadataError, TypedFd}, fs::{Mode, OFlags, SeekWhence}, + mm::linux::PAGE_SIZE, path, platform::{RawConstPointer, RawMutPointer}, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, @@ -709,98 +710,196 @@ impl Task { self.do_close(raw_fd) } + /// Handle syscall `preadv` + pub(crate) fn sys_preadv( + &self, + fd: i32, + iovec: ConstPtr>>, + iovcnt: usize, + offset: i64, + ) -> Result { + let base_offset = usize::try_from(offset).map_err(|_| Errno::EINVAL)?; + self.check_raw_fd_exists(fd)?; + check_iovcnt(iovcnt)?; + let iovs: &[IoReadVec>] = &iovec.to_owned_slice(iovcnt).ok_or(Errno::EFAULT)?; + let mut kernel_buffer = vec![0u8; PAGE_SIZE]; + read_from_iovec(iovs, &mut kernel_buffer, |buf, total| { + let cur_offset = base_offset.checked_add(total).ok_or(Errno::EOVERFLOW)?; + self.sys_read(fd, buf, Some(cur_offset)) + }) + } + + /// Handle syscall `pwritev` + pub(crate) fn sys_pwritev( + &self, + fd: i32, + iovec: ConstPtr>>, + iovcnt: usize, + offset: i64, + ) -> Result { + let base_offset = usize::try_from(offset).map_err(|_| Errno::EINVAL)?; + self.check_raw_fd_exists(fd)?; + check_iovcnt(iovcnt)?; + let iovs: &[IoWriteVec>] = + &iovec.to_owned_slice(iovcnt).ok_or(Errno::EFAULT)?; + // TODO: Linux ignores pwritev's offset for O_APPEND files; see the O_APPEND bug documented in pwrite(2). + write_to_iovec(iovs, |buf, total| { + let cur_offset = base_offset.checked_add(total).ok_or(Errno::EOVERFLOW)?; + self.sys_write(fd, buf, Some(cur_offset)) + }) + } + /// Handle syscall `readv` - pub fn sys_readv( + pub(crate) fn sys_readv( &self, fd: i32, iovec: ConstPtr>>, iovcnt: usize, ) -> Result { - let Ok(raw_fd) = u32::try_from(fd).and_then(usize::try_from) else { - return Err(Errno::EBADF); - }; + self.check_raw_fd_exists(fd)?; + check_iovcnt(iovcnt)?; let iovs: &[IoReadVec>] = &iovec.to_owned_slice(iovcnt).ok_or(Errno::EFAULT)?; - let files = self.files.borrow(); - let mut total_read = 0; - let mut kernel_buffer = vec![ - 0u8; - iovs.iter() - .map(|i| i.iov_len) - .max() - .unwrap_or_default() - .min(super::super::MAX_KERNEL_BUF_SIZE) - ]; - for iov in iovs { - if iov.iov_len == 0 { - continue; - } - let Ok(_iov_len) = isize::try_from(iov.iov_len) else { - return Err(Errno::EINVAL); + let mut kernel_buffer = vec![0u8; PAGE_SIZE]; + // TODO: The data transfers performed by readv() and writev() are atomic: the data + // written by writev() is written as a single block that is not intermingled with + // output from writes in other processes + read_from_iovec(iovs, &mut kernel_buffer, |buf, _total| { + self.sys_read(fd, buf, None) + }) + } +} + +impl Task { + fn check_raw_fd_exists(&self, fd: i32) -> Result<(), Errno> { + let raw_fd = usize::try_from(fd).map_err(|_| Errno::EBADF)?; + if self + .files + .borrow() + .raw_descriptor_store + .read() + .is_alive(raw_fd) + { + Ok(()) + } else { + Err(Errno::EBADF) + } + } +} + +/// Linux's `IOV_MAX` / `UIO_MAXIOV`: the kernel rejects iovec counts above this +/// with `EINVAL` for `readv`/`writev`/`preadv`/`pwritev`. +const IOV_MAX: usize = 1024; +const SSIZE_MAX: usize = isize::MAX as usize; + +fn check_iovcnt(iovcnt: usize) -> Result<(), Errno> { + if iovcnt > IOV_MAX { + Err(Errno::EINVAL) + } else { + Ok(()) + } +} + +fn check_iov_lens(iov_lens: impl IntoIterator) -> Result<(), Errno> { + let mut total = 0usize; + for iov_len in iov_lens { + total = total.checked_add(iov_len).ok_or(Errno::EINVAL)?; + if total > SSIZE_MAX { + return Err(Errno::EINVAL); + } + } + Ok(()) +} + +/// Drain reads into a sequence of user iovecs. +fn read_from_iovec( + iovs: &[IoReadVec

], + kernel_buffer: &mut [u8], + mut read_fn: F, +) -> Result +where + P: RawMutPointer, + F: FnMut(&mut [u8], usize) -> Result, +{ + check_iov_lens(iovs.iter().map(|iov| iov.iov_len))?; + + let bail = |total: usize, e: Errno| if total > 0 { Ok(total) } else { Err(e) }; + let mut total_read = 0; + 'outer: for iov in iovs { + let iov_base = iov.iov_base; + let iov_len = iov.iov_len; + if iov_len == 0 { + continue; + } + let mut iov_filled = 0; + while iov_filled < iov_len { + let to_read = (iov_len - iov_filled).min(kernel_buffer.len()); + let size = match read_fn(&mut kernel_buffer[..to_read], total_read) { + Ok(0) => break 'outer, + Ok(s) => s, + Err(e) => return bail(total_read, e), }; - // TODO: The data transfers performed by readv() and writev() are atomic: the data - // written by writev() is written as a single block that is not intermingled with - // output from writes in other processes - let size = files - .run_on_raw_fd( - raw_fd, - |fd| { - files - .fs - .read(fd, &mut kernel_buffer, None) - .map_err(Errno::from) - }, - |_fd| todo!("net"), - |_fd| todo!("pipes"), - |_fd| todo!("eventfd"), - |_fd| Err(Errno::EINVAL), - |_fd| todo!("unix"), - ) - .flatten()?; - iov.iov_base - .copy_from_slice(0, &kernel_buffer[..size]) - .ok_or(Errno::EFAULT)?; + if iov_base + .copy_from_slice(iov_filled, &kernel_buffer[..size]) + .is_none() + { + return bail(total_read, Errno::EFAULT); + } + iov_filled += size; total_read += size; - if size < iov.iov_len { - // Okay to transfer fewer bytes than requested - break; + if size < to_read { + // Short read from the source — treat as EOF for the remaining iovecs. + break 'outer; } } - Ok(total_read) } + Ok(total_read) } -pub(super) fn write_to_iovec(iovs: I, write_fn: F) -> Result +/// Drain writes from a sequence of user iovecs. +/// +/// `write_fn` receives the contents of each iovec along with the total number of +/// bytes already written from earlier iovecs. +pub(super) fn write_to_iovec(iovs: &[IoWriteVec

], mut write_fn: F) -> Result where P: RawConstPointer, - I: IntoIterator, - F: Fn(&[u8]) -> Result, + F: FnMut(&[u8], usize) -> Result, { + check_iov_lens(iovs.iter().map(|iov| iov.iov_len))?; + + // If any bytes have already been delivered from earlier iovecs, an error + // collapses to `Ok(total)` so partial progress is reported to user space. + let bail = |total: usize, e: Errno| if total > 0 { Ok(total) } else { Err(e) }; + let mut kernel_buffer = alloc::vec::Vec::new(); let mut total_written = 0; - for (iov_base, iov_len) in iovs { + 'outer: for iov in iovs { + let iov_base = iov.iov_base; + let iov_len = iov.iov_len; if iov_len == 0 { continue; } - let Some(slice) = iov_base.to_owned_slice(iov_len) else { - return if total_written > 0 { - Ok(total_written) - } else { - Err(Errno::EFAULT) - }; - }; - let size = match write_fn(&slice) { - Ok(size) => size, - Err(err) => { - return if total_written > 0 { - Ok(total_written) - } else { - Err(err) + if kernel_buffer.is_empty() { + kernel_buffer.resize(PAGE_SIZE, 0); + } + let mut iov_written = 0; + while iov_written < iov_len { + let to_write = (iov_len - iov_written).min(kernel_buffer.len()); + let base_offset = isize::try_from(iov_written).unwrap(); + for (byte_offset, byte) in (0_isize..).zip(kernel_buffer[..to_write].iter_mut()) { + let Some(value) = iov_base.read_at_offset(base_offset + byte_offset) else { + return bail(total_written, Errno::EFAULT); }; + *byte = value; + } + let size = match write_fn(&kernel_buffer[..to_write], total_written) { + Ok(size) => size, + Err(err) => return bail(total_written, err), + }; + iov_written += size; + total_written += size; + if size < to_write { + // Okay to transfer fewer bytes than requested. + break 'outer; } - }; - total_written += size; - if size < iov_len { - // Okay to transfer fewer bytes than requested - break; } } Ok(total_written) @@ -808,50 +907,20 @@ where impl Task { /// Handle syscall `writev` - pub fn sys_writev( + pub(crate) fn sys_writev( &self, fd: i32, iovec: ConstPtr>>, iovcnt: usize, ) -> Result { - let Ok(raw_fd) = u32::try_from(fd).and_then(usize::try_from) else { - return Err(Errno::EBADF); - }; + self.check_raw_fd_exists(fd)?; + check_iovcnt(iovcnt)?; let iovs: &[IoWriteVec>] = &iovec.to_owned_slice(iovcnt).ok_or(Errno::EFAULT)?; - let files = self.files.borrow(); // TODO: The data transfers performed by readv() and writev() are atomic: the data // written by writev() is written as a single block that is not intermingled with // output from writes in other processes - let res = files - .run_on_raw_fd( - raw_fd, - |fd| { - write_to_iovec(iovs.iter().map(|iov| (iov.iov_base, iov.iov_len)), |buf| { - files.fs.write(fd, buf, None).map_err(Errno::from) - }) - }, - |fd| { - write_to_iovec(iovs.iter().map(|iov| (iov.iov_base, iov.iov_len)), |buf| { - self.global.sendto( - &self.wait_cx(), - fd, - buf, - litebox_common_linux::SendFlags::empty(), - None, - ) - }) - }, - |_fd| todo!("pipes"), - |_fd| todo!("eventfd"), - |_fd| Err(Errno::EINVAL), - |_fd| todo!("unix"), - ) - .flatten(); - if let Err(Errno::EPIPE) = res { - self.send_signal(Signal::SIGPIPE, signal::siginfo_kill(Signal::SIGPIPE)); - } - res + write_to_iovec(iovs, |buf, _total| self.sys_write(fd, buf, None)) } /// Handle syscall `access` @@ -2279,14 +2348,16 @@ mod tests { ]; let calls = Cell::new(0); - let result = write_to_iovec(iovs.iter().map(|iov| (iov.iov_base, iov.iov_len)), |buf| { + let result = write_to_iovec(&iovs, |buf, total| { let call = calls.get(); calls.set(call + 1); if call == 0 { assert_eq!(buf, first); + assert_eq!(total, 0); Ok(buf.len()) } else { assert_eq!(buf, second); + assert_eq!(total, first.len()); Err(Errno::EPIPE) } }); @@ -2295,6 +2366,101 @@ mod tests { assert_eq!(calls.get(), 2); } + #[test] + fn read_from_iovec_breaks_on_eof() { + let mut first = [0u8; 4]; + let mut second = [0u8; 4]; + let iovs = [ + IoReadVec { + iov_base: MutPtr::from_usize(first.as_mut_ptr().expose_provenance()), + iov_len: first.len(), + }, + IoReadVec { + iov_base: MutPtr::from_usize(second.as_mut_ptr().expose_provenance()), + iov_len: second.len(), + }, + ]; + let mut kernel_buffer = [0u8; 8]; + let calls = Cell::new(0); + + let result = read_from_iovec(&iovs, &mut kernel_buffer, |buf, total| { + let call = calls.get(); + calls.set(call + 1); + if call == 0 { + assert_eq!(total, 0); + buf.fill(b'a'); + Ok(buf.len()) + } else { + assert_eq!(total, 4); + Ok(0) + } + }); + + assert_eq!(result, Ok(4)); + assert_eq!(calls.get(), 2); + assert_eq!(&first, b"aaaa"); + assert_eq!(&second, &[0u8; 4]); + } + + #[test] + fn read_from_iovec_chunks_iov_larger_than_kernel_buffer() { + let mut dest = [0u8; 12]; + let iovs = [IoReadVec { + iov_base: MutPtr::from_usize(dest.as_mut_ptr().expose_provenance()), + iov_len: dest.len(), + }]; + let mut kernel_buffer = [0u8; 4]; + let calls = Cell::new(0); + + let result = read_from_iovec(&iovs, &mut kernel_buffer, |buf, total| { + assert_eq!(buf.len(), 4); + assert_eq!(total, calls.get() * 4); + let marker = b'a' + u8::try_from(calls.get()).unwrap(); + buf.fill(marker); + calls.set(calls.get() + 1); + Ok(buf.len()) + }); + + assert_eq!(result, Ok(12)); + assert_eq!(calls.get(), 3); + assert_eq!(&dest, b"aaaabbbbcccc"); + } + + #[test] + fn read_from_iovec_returns_partial_after_later_error() { + let mut first = [0u8; 4]; + let mut second = [0u8; 4]; + let iovs = [ + IoReadVec { + iov_base: MutPtr::from_usize(first.as_mut_ptr().expose_provenance()), + iov_len: first.len(), + }, + IoReadVec { + iov_base: MutPtr::from_usize(second.as_mut_ptr().expose_provenance()), + iov_len: second.len(), + }, + ]; + let mut kernel_buffer = [0u8; 4]; + let calls = Cell::new(0); + + let result = read_from_iovec(&iovs, &mut kernel_buffer, |buf, total| { + let call = calls.get(); + calls.set(call + 1); + if call == 0 { + assert_eq!(total, 0); + buf.fill(b'x'); + Ok(buf.len()) + } else { + assert_eq!(total, 4); + Err(Errno::EIO) + } + }); + + assert_eq!(result, Ok(4)); + assert_eq!(calls.get(), 2); + assert_eq!(&first, b"xxxx"); + } + #[test] fn fspath_new() { // Absolute paths should never invoke the get_cwd closure. From afe6cddcdb01230cb63b15f154fbf97be427b652 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 2 Jun 2026 11:32:23 -0700 Subject: [PATCH 019/319] Support Windows file open/create/close syscalls (#894) Adds Windows file object syscall support to the Windows shim. --- litebox_shim_windows/src/lib.rs | 140 +- litebox_shim_windows/src/nt_types.rs | 21 + litebox_shim_windows/src/syscalls/file.rs | 1805 +++++++++++++++++ litebox_shim_windows/src/syscalls/mod.rs | 49 + litebox_shim_windows/src/syscalls/registry.rs | 46 +- litebox_shim_windows/src/tests.rs | 16 +- 6 files changed, 2066 insertions(+), 11 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/file.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 514fda1dc6..49e3845a1b 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -29,6 +29,8 @@ use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; use crate::syscalls::SyscallRequest; +use crate::syscalls::file::{FileObject, FileObjectSubsystem}; +use crate::syscalls::registry::{RegistryKeyObject, RegistryKeySubsystem}; mod loader; mod nt_types; @@ -121,7 +123,11 @@ where let Some(handle) = syscalls::Handle::from_raw_fd(raw_fd) else { let typed = handles.fd_consume_raw_integer::(raw_fd).ok(); drop(handles); - if let Some(entry) = typed.and_then(|typed| litebox.descriptor_table_mut().remove(&typed)) { + let entry = typed.and_then(|typed| { + let mut descriptor_table = litebox.descriptor_table_mut(); + descriptor_table.remove(&typed) + }); + if let Some(entry) = entry { cleanup_entry(entry); } return Err(NtStatus::QUOTA_EXCEEDED); @@ -156,13 +162,34 @@ pub(crate) fn remove_raw_handle(litebox, handles, raw_fd, cleanup_entry); +} + +pub(crate) fn remove_raw_handle_by_raw_fd( + litebox: &LiteBox, + handles: &WindowsHandleStore, + raw_fd: usize, + cleanup_entry: impl FnOnce(Subsystem::Entry), +) -> bool +where + Platform: RawSyncPrimitivesProvider, +{ let typed = { let mut handles = handles.write(); handles.fd_consume_raw_integer::(raw_fd).ok() }; - if let Some(entry) = typed.and_then(|typed| litebox.descriptor_table_mut().remove(&typed)) { + let Some(typed) = typed else { + return false; + }; + let entry = { + let mut descriptor_table = litebox.descriptor_table_mut(); + descriptor_table.remove(&typed) + }; + if let Some(entry) = entry { cleanup_entry(entry); } + true } /// Builds a Windows NT shim instance. @@ -225,7 +252,7 @@ impl WindowsShim { _envp: Vec, ) -> Result, loader::WindowsLoadError> { let load_info = - loader::PeLoader::new(self.0.platform, fs, &self.0.page_manager).load(path)?; + loader::PeLoader::new(self.0.platform, fs.clone(), &self.0.page_manager).load(path)?; let process = Arc::new(Process { ntdll_mapping: load_info.ntdll_mapping, handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), @@ -236,9 +263,9 @@ impl WindowsShim { task: Task { global: self.0.clone(), process: process.clone(), + fs, entry_point: load_info.entry_point, stack_top: load_info.stack_top, - _phantom: PhantomData, }, _not_send: PhantomData, }, @@ -278,9 +305,9 @@ impl Process { struct Task { global: Arc>, process: Arc>, + fs: Arc, entry_point: usize, stack_top: usize, - _phantom: PhantomData, } impl Task { @@ -321,6 +348,56 @@ impl Task { "Handling Windows syscall" ); let (result, op) = match req { + SyscallRequest::NtClose { handle } => { + let status = self.sys_nt_close(handle); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtOpenFile { + file_handle, + desired_access, + object_attributes, + io_status_block, + share_access, + open_options, + } => { + let status = self.sys_nt_open_file( + file_handle, + desired_access, + object_attributes, + io_status_block, + share_access, + open_options, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateFile { + file_handle, + desired_access, + object_attributes, + io_status_block, + allocation_size, + file_attributes, + share_access, + create_disposition, + create_options, + ea_buffer, + ea_length, + } => { + let status = self.sys_nt_create_file( + file_handle, + desired_access, + object_attributes, + io_status_block, + allocation_size, + file_attributes, + share_access, + create_disposition, + create_options, + ea_buffer, + ea_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtOpenKey { key_handle, desired_access, @@ -387,6 +464,37 @@ impl Task { op } + pub(crate) fn sys_nt_close(&self, handle: syscalls::Handle) -> NtStatus { + let Some(raw_fd) = handle.raw_fd() else { + return NtStatus::INVALID_HANDLE; + }; + self.close_raw_fd(raw_fd, CloseRawHandleVisitor { task: self }) + } + + fn close_raw_fd( + &self, + raw_fd: usize, + visitor: impl RawHandleVisitor, + ) -> NtStatus { + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |file| visitor.file(file), + ) { + return NtStatus::SUCCESS; + } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |key| visitor.registry_key(key), + ) { + return NtStatus::SUCCESS; + } + NtStatus::INVALID_HANDLE + } + fn handle_interrupt_request( &self, _ctx: &mut litebox_common_linux::PtRegs, @@ -399,6 +507,28 @@ impl Task { } } +trait RawHandleVisitor { + fn file(&self, file: FileObject); + + fn registry_key(&self, key: RegistryKeyObject); +} + +struct CloseRawHandleVisitor<'task, Platform: ShimPlatform, FS: ShimFS> { + task: &'task Task, +} + +impl RawHandleVisitor + for CloseRawHandleVisitor<'_, Platform, FS> +{ + fn file(&self, file: FileObject) { + self.task.close_file(file); + } + + fn registry_key(&self, key: RegistryKeyObject) { + self.task.close_registry_key(key); + } +} + /// The shim entrypoint object passed to the platform. pub struct WindowsShimEntrypoints { task: Task, diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index 3097598cd4..78071c3c52 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -2,6 +2,7 @@ // Licensed under the MIT license. use alloc::string::String; +use core::mem::offset_of; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -46,6 +47,26 @@ pub(crate) struct ObjectAttributes { pub(crate) security_quality_of_service: usize, } +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromBytes, IntoBytes, Immutable)] +pub(crate) struct IoStatusBlock { + pub(crate) status: i32, + pub(crate) padding_0: [u8; 4], + pub(crate) information: usize, +} + +const _: () = assert!(offset_of!(IoStatusBlock, information) == 0x8); + +impl IoStatusBlock { + pub(crate) const fn new(status: NtStatus, information: usize) -> Self { + Self { + status: status.as_raw(), + padding_0: [0; 4], + information, + } + } +} + pub(crate) fn read_object_attributes( object_attributes: ConstPtr, ) -> Result { diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs new file mode 100644 index 0000000000..6fd433907f --- /dev/null +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -0,0 +1,1805 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::string::String; +use core::marker::PhantomData; + +use int_enum::IntEnum; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}; +use litebox::fs::errors::{FileStatusError, MkdirError, OpenError, PathError}; +use litebox::fs::{FileType, Mode, OFlags}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::{ + AccessMask, IoStatusBlock, ObjectAttributes, UnicodeString, read_object_attributes, +}; +use crate::syscalls::Handle; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, raw_handle_entry, remove_raw_handle, +}; + +const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001; + +const FILE_SHARE_READ: u32 = 0x0000_0001; +const FILE_SHARE_WRITE: u32 = 0x0000_0002; +const FILE_SHARE_DELETE: u32 = 0x0000_0004; + +const CONDRV_INPUT_OBJECT: &str = "Input"; +const CONDRV_OUTPUT_OBJECT: &str = "Output"; +const CONDRV_SERVER_DEVICE: &str = "Server"; +const CONDRV_REFERENCE_OBJECT: &str = "Reference"; +const CONDRV_CONNECT_OBJECT: &str = "Connect"; + +#[repr(usize)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum FileCreateInformation { + /// An existing file was deleted and a new file was created in its place. + Superseded = 0, + /// An existing file was opened. + Opened = 1, + Created = 2, + /// An existing file was overwritten. + Overwritten = 3, + Exists = 4, + DoesNotExist = 5, +} + +pub(crate) struct FileObjectSubsystem(PhantomData); + +impl FdEnabledSubsystem for FileObjectSubsystem { + type Entry = FileObject; +} + +impl FdEnabledSubsystemEntry for FileObject {} + +pub(crate) struct FileObject { + path: String, + fd: TypedFd, + granted_access: FileAccess, + share_access: FileShareAccess, + is_directory: bool, + create_options: FileCreateOptions, +} + +bitflags::bitflags! { + /// File object `ACCESS_MASK` rights accepted by `NtOpenFile`/`NtCreateFile`. + /// + /// Generic-right mappings and create/open disposition behavior follow + /// Microsoft Learn's `NtCreateFile` documentation. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct FileAccess: u32 { + const READ_DATA = 0x0001; + const LIST_DIRECTORY = Self::READ_DATA.bits(); + const WRITE_DATA = 0x0002; + const ADD_FILE = Self::WRITE_DATA.bits(); + const APPEND_DATA = 0x0004; + const ADD_SUBDIRECTORY = Self::APPEND_DATA.bits(); + const READ_EA = 0x0008; + const WRITE_EA = 0x0010; + const EXECUTE = 0x0020; + const TRAVERSE = Self::EXECUTE.bits(); + const DELETE_CHILD = 0x0040; + const READ_ATTRIBUTES = 0x0080; + const WRITE_ATTRIBUTES = 0x0100; + const DELETE = AccessMask::DELETE.bits(); + const SYNCHRONIZE = AccessMask::SYNCHRONIZE.bits(); + const GENERIC_ALL = AccessMask::GENERIC_ALL.bits(); + const GENERIC_EXECUTE = AccessMask::GENERIC_EXECUTE.bits(); + const GENERIC_WRITE = AccessMask::GENERIC_WRITE.bits(); + const GENERIC_READ = AccessMask::GENERIC_READ.bits(); + + const GENERIC_READ_EXPANSION = AccessMask::STANDARD_RIGHTS_READ.bits() + | Self::READ_DATA.bits() + | Self::READ_ATTRIBUTES.bits() + | Self::READ_EA.bits() + | Self::SYNCHRONIZE.bits(); + const GENERIC_WRITE_EXPANSION = AccessMask::STANDARD_RIGHTS_WRITE.bits() + | Self::WRITE_DATA.bits() + | Self::WRITE_ATTRIBUTES.bits() + | Self::WRITE_EA.bits() + | Self::APPEND_DATA.bits() + | Self::SYNCHRONIZE.bits(); + const GENERIC_EXECUTE_EXPANSION = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() + | Self::EXECUTE.bits() + | Self::READ_ATTRIBUTES.bits() + | Self::SYNCHRONIZE.bits(); + const ALL_ACCESS = AccessMask::STANDARD_RIGHTS_ALL.bits() + | Self::READ_DATA.bits() + | Self::WRITE_DATA.bits() + | Self::APPEND_DATA.bits() + | Self::READ_EA.bits() + | Self::WRITE_EA.bits() + | Self::EXECUTE.bits() + | Self::DELETE_CHILD.bits() + | Self::READ_ATTRIBUTES.bits() + | Self::WRITE_ATTRIBUTES.bits(); + + const FS_READ_ACCESS = Self::READ_DATA.bits() + | Self::READ_EA.bits() + | Self::READ_ATTRIBUTES.bits() + | Self::EXECUTE.bits(); + const FS_WRITE_ACCESS = Self::WRITE_DATA.bits() + | Self::APPEND_DATA.bits() + | Self::WRITE_EA.bits() + | Self::WRITE_ATTRIBUTES.bits() + | Self::DELETE.bits() + | AccessMask::WRITE_DAC.bits() + | AccessMask::WRITE_OWNER.bits(); + + const SHARE_READ_ACCESS = Self::READ_DATA.bits() + | Self::READ_EA.bits() + | Self::READ_ATTRIBUTES.bits() + | Self::EXECUTE.bits(); + const SHARE_WRITE_ACCESS = Self::WRITE_DATA.bits() + | Self::APPEND_DATA.bits() + | Self::WRITE_EA.bits() + | Self::WRITE_ATTRIBUTES.bits(); + const SHARE_DELETE_ACCESS = Self::DELETE.bits() + | Self::DELETE_CHILD.bits(); + + const _ = !0; + } +} + +impl FileAccess { + fn from_desired_access(desired_access: u32) -> Self { + let mut access = Self::from_bits_retain(desired_access); + if access.contains(Self::GENERIC_READ) { + access.remove(Self::GENERIC_READ); + access.insert(Self::GENERIC_READ_EXPANSION); + } + if access.contains(Self::GENERIC_WRITE) { + access.remove(Self::GENERIC_WRITE); + access.insert(Self::GENERIC_WRITE_EXPANSION); + } + if access.contains(Self::GENERIC_EXECUTE) { + access.remove(Self::GENERIC_EXECUTE); + access.insert(Self::GENERIC_EXECUTE_EXPANSION); + } + if access.contains(Self::GENERIC_ALL) { + access.remove(Self::GENERIC_ALL); + access.insert(Self::ALL_ACCESS); + } + access + } + + fn open_flags( + self, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, + ) -> OFlags { + let wants_read = self.intersects(Self::FS_READ_ACCESS); + let wants_write = self.intersects(Self::FS_WRITE_ACCESS) + || matches!( + create_disposition, + CreateDisposition::Supersede + | CreateDisposition::Overwrite + | CreateDisposition::OverwriteIf + ); + + let mut flags = match (wants_read, wants_write) { + (true, true) => OFlags::RDWR, + (false, true) => OFlags::WRONLY, + _ => OFlags::RDONLY, + }; + + match create_disposition { + CreateDisposition::Overwrite => { + flags.insert(OFlags::TRUNC); + } + CreateDisposition::Supersede | CreateDisposition::OverwriteIf => { + flags.insert(OFlags::CREAT | OFlags::TRUNC); + } + CreateDisposition::Create => flags.insert(OFlags::CREAT | OFlags::EXCL), + CreateDisposition::OpenIf => flags.insert(OFlags::CREAT), + CreateDisposition::Open => {} + } + + if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { + flags.insert(OFlags::DIRECTORY); + } + if create_options.contains(FileCreateOptions::NON_DIRECTORY_FILE) { + flags.insert(OFlags::NOFOLLOW); + } + + flags + } + + fn conflicts_with_share(self, share_access: FileShareAccess) -> bool { + self.intersects(Self::SHARE_READ_ACCESS) && !share_access.contains(FileShareAccess::READ) + || self.intersects(Self::SHARE_WRITE_ACCESS) + && !share_access.contains(FileShareAccess::WRITE) + || self.intersects(Self::SHARE_DELETE_ACCESS) + && !share_access.contains(FileShareAccess::DELETE) + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct FileShareAccess: u32 { + const READ = FILE_SHARE_READ; + const WRITE = FILE_SHARE_WRITE; + const DELETE = FILE_SHARE_DELETE; + const _ = !0; + } +} + +impl FileShareAccess { + const VALID_BITS: u32 = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + + fn from_share_access(share_access: u32) -> Result { + if share_access & !Self::VALID_BITS != 0 { + return Err(NtStatus::INVALID_PARAMETER); + } + Ok(Self::from_bits_retain(share_access)) + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct FileCreateOptions: u32 { + const DIRECTORY_FILE = 0x0000_0001; + const WRITE_THROUGH = 0x0000_0002; + const SEQUENTIAL_ONLY = 0x0000_0004; + const NO_INTERMEDIATE_BUFFERING = 0x0000_0008; + const SYNCHRONOUS_IO_ALERT = 0x0000_0010; + const SYNCHRONOUS_IO_NONALERT = 0x0000_0020; + const NON_DIRECTORY_FILE = 0x0000_0040; + const CREATE_TREE_CONNECTION = 0x0000_0080; + const COMPLETE_IF_OPLOCKED = 0x0000_0100; + const NO_EA_KNOWLEDGE = 0x0000_0200; + const OPEN_REMOTE_INSTANCE = 0x0000_0400; + const RANDOM_ACCESS = 0x0000_0800; + const DELETE_ON_CLOSE = 0x0000_1000; + const OPEN_BY_FILE_ID = 0x0000_2000; + const OPEN_FOR_BACKUP_INTENT = 0x0000_4000; + const NO_COMPRESSION = 0x0000_8000; + const OPEN_REQUIRING_OPLOCK = 0x0001_0000; + const DISALLOW_EXCLUSIVE = 0x0002_0000; + const SESSION_AWARE = 0x0004_0000; + const RESERVE_OPFILTER = 0x0010_0000; + const OPEN_REPARSE_POINT = 0x0020_0000; + const OPEN_NO_RECALL = 0x0040_0000; + const OPEN_FOR_FREE_SPACE_QUERY = 0x0080_0000; + const CONTAINS_EXTENDED_CREATE_INFORMATION = 0x1000_0000; + + const _ = !0; + } +} + +impl FileCreateOptions { + const SYNCHRONOUS_IO: Self = Self::SYNCHRONOUS_IO_ALERT.union(Self::SYNCHRONOUS_IO_NONALERT); + + const DIRECTORY_COMPATIBLE: Self = Self::DIRECTORY_FILE + .union(Self::SYNCHRONOUS_IO_ALERT) + .union(Self::SYNCHRONOUS_IO_NONALERT) + .union(Self::WRITE_THROUGH) + .union(Self::COMPLETE_IF_OPLOCKED) + .union(Self::OPEN_FOR_BACKUP_INTENT) + .union(Self::DELETE_ON_CLOSE) + .union(Self::OPEN_BY_FILE_ID) + .union(Self::NO_COMPRESSION) + .union(Self::OPEN_REPARSE_POINT) + .union(Self::OPEN_FOR_FREE_SPACE_QUERY); +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum CreateDisposition { + Supersede = 0, + Create = 1, + Open = 2, + OpenIf = 3, + Overwrite = 4, + OverwriteIf = 5, +} + +impl CreateDisposition { + fn success_information(self, existed_before_open: bool) -> FileCreateInformation { + match (self, existed_before_open) { + (Self::Supersede, true) => FileCreateInformation::Superseded, + (Self::Supersede | Self::Create | Self::OpenIf | Self::OverwriteIf, false) => { + FileCreateInformation::Created + } + (Self::Overwrite | Self::OverwriteIf, true) => FileCreateInformation::Overwritten, + _ => FileCreateInformation::Opened, + } + } +} + +impl Task { + fn file_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + raw_handle_entry::>( + &self.global.litebox, + &self.process.handles, + handle, + ) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn insert_file_handle(&self, file: FileObject) -> Result { + let typed = self + .global + .litebox + .descriptor_table_mut() + .insert::>(file); + insert_raw_handle::>( + &self.global.litebox, + &self.process.handles, + typed, + |file| self.close_file(file), + ) + } + + pub(crate) fn close_file_handle(&self, handle: Handle) { + remove_raw_handle::>( + &self.global.litebox, + &self.process.handles, + handle, + |file| self.close_file(file), + ); + } + + pub(crate) fn close_file(&self, file: FileObject) { + let _ = self.fs.close(&file.fd); + if file + .create_options + .contains(FileCreateOptions::DELETE_ON_CLOSE) + { + if file.is_directory { + let _ = self.fs.rmdir(&file.path); + } else { + let _ = self.fs.unlink(&file.path); + } + } + } + + pub(crate) fn sys_nt_open_file( + &self, + file_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + io_status_block: MutPtr, + share_access: u32, + open_options: u32, + ) -> NtStatus { + let Some(object_attributes) = object_attributes else { + return NtStatus::INVALID_PARAMETER; + }; + let object_attributes = match read_object_attributes::(object_attributes) { + Ok(object_attributes) => object_attributes, + Err(status) => return status, + }; + if let Err(status) = probe_file_outputs::(file_handle, io_status_block) { + return status; + } + let result = self.do_nt_create_file( + desired_access, + object_attributes, + io_status_block, + FILE_ATTRIBUTE_READONLY, + share_access, + CreateDisposition::Open, + open_options, + None, + 0, + ); + write_file_result::(file_handle, io_status_block, result, |handle| { + self.close_file_handle(handle); + }) + } + + #[expect( + clippy::too_many_arguments, + reason = "NtCreateFile has eleven ABI parameters; keeping the syscall handler aligned with that shape avoids argument reshuffling bugs" + )] + pub(crate) fn sys_nt_create_file( + &self, + file_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + io_status_block: MutPtr, + _allocation_size: Option>, + file_attributes: u32, + share_access: u32, + create_disposition: u32, + create_options: u32, + ea_buffer: Option>, + ea_length: u32, + ) -> NtStatus { + let Some(object_attributes) = object_attributes else { + return NtStatus::INVALID_PARAMETER; + }; + let object_attributes = match read_object_attributes::(object_attributes) { + Ok(object_attributes) => object_attributes, + Err(status) => return status, + }; + let Ok(create_disposition) = CreateDisposition::try_from(create_disposition) else { + return NtStatus::INVALID_PARAMETER; + }; + if let Err(status) = probe_file_outputs::(file_handle, io_status_block) { + return status; + } + let result = self.do_nt_create_file( + desired_access, + object_attributes, + io_status_block, + file_attributes, + share_access, + create_disposition, + create_options, + ea_buffer, + ea_length, + ); + write_file_result::(file_handle, io_status_block, result, |handle| { + self.close_file_handle(handle); + }) + } + + // Microsoft Learn documents `NtCreateFile` as the common create/open primitive, + // with `NtOpenFile` being its open-existing subset. + #[expect( + clippy::too_many_arguments, + reason = "This helper carries the parsed NtCreateFile ABI fields through one shared NtOpenFile/NtCreateFile path" + )] + fn do_nt_create_file( + &self, + desired_access: u32, + object_attributes: ObjectAttributes, + io_status_block: MutPtr, + file_attributes: u32, + share_access: u32, + create_disposition: CreateDisposition, + create_options: u32, + ea_buffer: Option>, + ea_length: u32, + ) -> Result<(Handle, FileCreateInformation), NtStatus> { + if io_status_block.as_usize() == 0 { + return Err(NtStatus::ACCESS_VIOLATION); + } + if object_attributes.object_name == 0 { + return Err(NtStatus::INVALID_PARAMETER); + } + if ea_buffer.is_some() || ea_length != 0 { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + let desired_access = FileAccess::from_desired_access(desired_access); + let create_options = FileCreateOptions::from_bits_retain(create_options); + validate_create_options(desired_access, create_disposition, create_options)?; + + let share_access = FileShareAccess::from_share_access(share_access)?; + let path = self.object_attributes_to_fs_path(object_attributes)?; + self.check_file_sharing(&path, desired_access, share_access)?; + + if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { + return self.open_or_create_directory( + &path, + desired_access, + share_access, + create_disposition, + create_options, + file_attributes, + ); + } + + let existed_before_open = self.fs.file_status(&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 fd = self + .fs + .open(&path, flags, create_mode(file_attributes)) + .map_err(|error| map_open_error(error, create_disposition))?; + let file_status = match self.fs.fd_file_status(&fd) { + Ok(file_status) => file_status, + Err(error) => { + let _ = self.fs.close(&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); + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + } + let information = create_disposition.success_information(existed_before_open); + let handle = self.insert_file_handle(FileObject { + path, + fd, + granted_access: desired_access, + share_access, + is_directory: file_status.file_type == FileType::Directory, + create_options, + })?; + Ok((handle, information)) + } + + fn open_or_create_directory( + &self, + path: &str, + desired_access: FileAccess, + share_access: FileShareAccess, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, + file_attributes: u32, + ) -> Result<(Handle, FileCreateInformation), NtStatus> { + if matches!( + create_disposition, + CreateDisposition::Supersede + | CreateDisposition::Overwrite + | CreateDisposition::OverwriteIf + ) { + return Err(NtStatus::INVALID_PARAMETER); + } + + let existed_before_open = match self.fs.file_status(path) { + Ok(status) => { + if status.file_type != FileType::Directory { + return Err(NtStatus::NOT_A_DIRECTORY); + } + true + } + Err(_) + if matches!( + create_disposition, + CreateDisposition::Create | CreateDisposition::OpenIf + ) => + { + self.fs + .mkdir(path, create_directory_mode(file_attributes)) + .map_err(map_mkdir_error)?; + false + } + Err(error) => return Err(map_file_status_error(error)), + }; + + let open_disposition = if existed_before_open { + create_disposition + } else { + CreateDisposition::Open + }; + let flags = desired_access.open_flags(open_disposition, create_options); + let fd = self + .fs + .open(path, flags, Mode::empty()) + .map_err(|error| map_open_error(error, create_disposition))?; + let information = create_disposition.success_information(existed_before_open); + let handle = self.insert_file_handle(FileObject { + path: String::from(path), + fd, + granted_access: desired_access, + share_access, + is_directory: true, + create_options, + })?; + Ok((handle, information)) + } + + fn object_attributes_to_fs_path( + &self, + object_attributes: ObjectAttributes, + ) -> Result { + let object_name_ptr = + ConstPtr::::from_usize(object_attributes.object_name); + let object_name = object_name_ptr + .read_at_offset(0) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + let object_name = object_name.read_string::()?; + if object_attributes.root_directory.is_null() || is_absolute_windows_path(&object_name) { + return absolute_nt_file_name_to_fs_path(&object_name); + } + + let root_file = self.file_entry(object_attributes.root_directory)?; + root_file.with_entry(|root_file| { + if !root_file.is_directory { + return Err(NtStatus::NOT_A_DIRECTORY); + } + relative_nt_file_name_to_fs_path(&root_file.path, &object_name) + }) + } + + fn check_file_sharing( + &self, + path: &str, + desired_access: FileAccess, + share_access: FileShareAccess, + ) -> Result<(), NtStatus> { + let raw_handles: alloc::vec::Vec = + self.process.handles.read().iter_alive().collect(); + for raw_handle in raw_handles { + let Some(handle) = Handle::from_raw_fd(raw_handle) else { + continue; + }; + let Some(entry) = raw_handle_entry::>( + &self.global.litebox, + &self.process.handles, + handle, + ) else { + continue; + }; + let conflicts = entry.with_entry(|file| { + file.path == path + && (desired_access.conflicts_with_share(file.share_access) + || file.granted_access.conflicts_with_share(share_access)) + }); + if conflicts { + return Err(NtStatus::SHARING_VIOLATION); + } + } + Ok(()) + } +} + +fn probe_file_outputs( + file_handle: MutPtr, + io_status_block: MutPtr, +) -> Result<(), NtStatus> { + probe_writable::(file_handle)?; + probe_writable::(io_status_block) +} + +fn probe_writable(ptr: MutPtr) -> Result<(), NtStatus> +where + Platform: RawPointerProvider, + T: zerocopy::FromBytes + zerocopy::IntoBytes, +{ + let value = ptr.read_at_offset(0).ok_or(NtStatus::ACCESS_VIOLATION)?; + ptr.write_at_offset(0, value) + .ok_or(NtStatus::ACCESS_VIOLATION) +} + +fn write_file_result( + file_handle: MutPtr, + io_status_block: MutPtr, + result: Result<(Handle, FileCreateInformation), NtStatus>, + cleanup_handle: impl FnOnce(Handle), +) -> NtStatus { + match result { + Ok((handle, information)) => { + if write_file_success::(file_handle, io_status_block, handle, information) + .is_none() + { + cleanup_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + Err(status) => { + let _ = io_status_block + .write_at_offset(0, IoStatusBlock::new(status, failure_information(status))); + status + } + } +} + +fn write_file_success( + file_handle: MutPtr, + io_status_block: MutPtr, + handle: Handle, + information: FileCreateInformation, +) -> Option<()> { + file_handle.write_at_offset(0, Handle::default())?; + io_status_block + .write_at_offset(0, IoStatusBlock::new(NtStatus::SUCCESS, information.into()))?; + file_handle.write_at_offset(0, handle) +} + +fn failure_information(status: NtStatus) -> usize { + match status { + NtStatus::OBJECT_NAME_COLLISION => FileCreateInformation::Exists.into(), + NtStatus::OBJECT_NAME_NOT_FOUND | NtStatus::OBJECT_PATH_NOT_FOUND => { + FileCreateInformation::DoesNotExist.into() + } + _ => 0, + } +} + +fn validate_create_options( + desired_access: FileAccess, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, +) -> Result<(), NtStatus> { + if create_options.contains(FileCreateOptions::DIRECTORY_FILE) + && matches!( + create_disposition, + CreateDisposition::Supersede + | CreateDisposition::Overwrite + | CreateDisposition::OverwriteIf + ) + { + return Err(NtStatus::INVALID_PARAMETER); + } + + if create_options.contains(FileCreateOptions::DIRECTORY_FILE) + && !create_options + .difference(FileCreateOptions::DIRECTORY_COMPATIBLE) + .is_empty() + { + return Err(NtStatus::INVALID_PARAMETER); + } + + if create_options.contains(FileCreateOptions::SYNCHRONOUS_IO) { + return Err(NtStatus::INVALID_PARAMETER); + } + + if create_options.intersects(FileCreateOptions::SYNCHRONOUS_IO) + && !desired_access.contains(FileAccess::SYNCHRONIZE) + { + return Err(NtStatus::INVALID_PARAMETER); + } + + if create_options.contains(FileCreateOptions::NO_INTERMEDIATE_BUFFERING) + && desired_access.contains(FileAccess::APPEND_DATA) + { + return Err(NtStatus::INVALID_PARAMETER); + } + + if create_options.contains(FileCreateOptions::DELETE_ON_CLOSE) + && !desired_access.contains(FileAccess::DELETE) + { + return Err(NtStatus::INVALID_PARAMETER); + } + + Ok(()) +} + +fn create_mode(file_attributes: u32) -> Mode { + if file_attributes & FILE_ATTRIBUTE_READONLY == 0 { + Mode::RUSR | Mode::WUSR + } else { + Mode::RUSR + } +} + +fn create_directory_mode(file_attributes: u32) -> Mode { + create_mode(file_attributes) | Mode::XUSR +} + +fn absolute_nt_file_name_to_fs_path(name: &str) -> Result { + let mut name = name; + if let Some(rest) = strip_case_insensitive_prefix(name, "\\??\\") { + name = rest; + } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\\\?\\") { + name = rest; + } + + if name.len() >= 3 && name.as_bytes()[1] == b':' && matches!(name.as_bytes()[2], b'\\' | b'/') { + name = &name[2..]; + } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\SystemRoot\\") { + return join_absolute_components("/Windows", rest); + } else if let Some(rest) = strip_case_insensitive_prefix(name, "/SystemRoot/") { + return join_absolute_components("/Windows", rest); + } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\Device\\HarddiskVolume1\\") { + return join_absolute_components("/", rest); + } else if let Some(device_name) = strip_case_insensitive_prefix(name, "\\Device\\ConDrv\\") { + return condrv_device_file(device_name).ok_or(NtStatus::OBJECT_NAME_NOT_FOUND); + } + + let path = name.trim_start_matches(['\\', '/']); + join_absolute_components("/", path) +} + +fn relative_nt_file_name_to_fs_path(root_path: &str, name: &str) -> Result { + if is_absolute_windows_path(name) { + return absolute_nt_file_name_to_fs_path(name); + } + join_absolute_components(root_path, name) +} + +fn join_absolute_components(root_path: &str, components: &str) -> Result { + let mut path = String::from(root_path.trim_end_matches('/')); + if path.is_empty() { + path.push('/'); + } + for component in components.split(['\\', '/']) { + if component.is_empty() || component == "." { + continue; + } + if component == ".." { + return Err(NtStatus::INVALID_PARAMETER); + } + if !path.ends_with('/') { + path.push('/'); + } + append_windows_component(&mut path, component); + } + Ok(path) +} + +fn condrv_device_file(device_name: &str) -> Option { + if device_name.eq_ignore_ascii_case(CONDRV_INPUT_OBJECT) { + return Some(String::from("/dev/stdin")); + } + if device_name.eq_ignore_ascii_case(CONDRV_OUTPUT_OBJECT) { + return Some(String::from("/dev/stdout")); + } + if device_name.eq_ignore_ascii_case(CONDRV_SERVER_DEVICE) + || device_name.eq_ignore_ascii_case(CONDRV_REFERENCE_OBJECT) + || device_name.eq_ignore_ascii_case(CONDRV_CONNECT_OBJECT) + { + return Some(String::from("/dev/null")); + } + None +} + +fn append_windows_component(path: &mut String, component: &str) { + if component.eq_ignore_ascii_case("Windows") { + path.push_str("Windows"); + } else if component.eq_ignore_ascii_case("System32") { + path.push_str("System32"); + } else if ends_with_ignore_ascii_case(component, ".dll") + || ends_with_ignore_ascii_case(component, ".nls") + { + path.push_str(&component.to_ascii_lowercase()); + } else { + path.push_str(component); + } +} + +fn strip_case_insensitive_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { + value + .get(..prefix.len()) + .is_some_and(|head| head.eq_ignore_ascii_case(prefix)) + .then(|| &value[prefix.len()..]) +} + +fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool { + value + .get(value.len().saturating_sub(suffix.len())..) + .is_some_and(|tail| tail.eq_ignore_ascii_case(suffix)) +} + +fn is_absolute_windows_path(name: &str) -> bool { + name.starts_with(['\\', '/']) + || name + .as_bytes() + .get(1..3) + .is_some_and(|bytes| bytes[0] == b':' && matches!(bytes[1], b'\\' | b'/')) +} + +fn map_open_error(error: OpenError, create_disposition: CreateDisposition) -> NtStatus { + match error { + OpenError::PathError(error) => match error { + PathError::NoSuchFileOrDirectory => match create_disposition { + CreateDisposition::Create + | CreateDisposition::OpenIf + | CreateDisposition::OverwriteIf + | CreateDisposition::Supersede => NtStatus::OBJECT_PATH_NOT_FOUND, + CreateDisposition::Open | CreateDisposition::Overwrite => { + NtStatus::OBJECT_NAME_NOT_FOUND + } + }, + PathError::MissingComponent => NtStatus::OBJECT_PATH_NOT_FOUND, + PathError::ComponentNotADirectory => NtStatus::NOT_A_DIRECTORY, + PathError::InvalidPathname => NtStatus::INVALID_PARAMETER, + PathError::NoSearchPerms { .. } => NtStatus::UNSUCCESSFUL, + }, + OpenError::AccessNotAllowed | OpenError::NoWritePerms | OpenError::ReadOnlyFileSystem => { + NtStatus::ACCESS_DENIED + } + OpenError::AlreadyExists => NtStatus::OBJECT_NAME_COLLISION, + _ => NtStatus::UNSUCCESSFUL, + } +} + +fn map_file_status_error(error: FileStatusError) -> NtStatus { + match error { + FileStatusError::PathError(PathError::NoSuchFileOrDirectory) => { + NtStatus::OBJECT_NAME_NOT_FOUND + } + FileStatusError::PathError(PathError::MissingComponent) => NtStatus::OBJECT_PATH_NOT_FOUND, + FileStatusError::PathError(PathError::ComponentNotADirectory) => NtStatus::NOT_A_DIRECTORY, + FileStatusError::PathError(PathError::InvalidPathname) => NtStatus::INVALID_PARAMETER, + _ => NtStatus::UNSUCCESSFUL, + } +} + +fn map_mkdir_error(error: MkdirError) -> NtStatus { + match error { + MkdirError::AlreadyExists => NtStatus::OBJECT_NAME_COLLISION, + MkdirError::PathError(PathError::NoSuchFileOrDirectory | PathError::MissingComponent) => { + NtStatus::OBJECT_PATH_NOT_FOUND + } + MkdirError::PathError(PathError::ComponentNotADirectory) => NtStatus::NOT_A_DIRECTORY, + MkdirError::PathError(PathError::InvalidPathname) => NtStatus::INVALID_PARAMETER, + MkdirError::NoWritePerms | MkdirError::ReadOnlyFileSystem => NtStatus::ACCESS_DENIED, + _ => NtStatus::UNSUCCESSFUL, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{TestFS, TestPlatform}; + use litebox::fs::FileSystem as _; + use zerocopy::{FromBytes, IntoBytes}; + + extern crate std; + + const FILE_GENERIC_READ: u32 = AccessMask::STANDARD_RIGHTS_READ.bits() + | FileAccess::READ_DATA.bits() + | FileAccess::READ_ATTRIBUTES.bits() + | FileAccess::READ_EA.bits() + | AccessMask::SYNCHRONIZE.bits(); + const FILE_GENERIC_WRITE: u32 = AccessMask::STANDARD_RIGHTS_WRITE.bits() + | FileAccess::WRITE_DATA.bits() + | FileAccess::WRITE_ATTRIBUTES.bits() + | FileAccess::WRITE_EA.bits() + | FileAccess::APPEND_DATA.bits() + | AccessMask::SYNCHRONIZE.bits(); + const FILE_SUPERSEDE: u32 = 0; + const FILE_OPEN: u32 = 2; + const FILE_CREATE: u32 = 1; + const FILE_OVERWRITE: u32 = 4; + + fn const_ptr(value: &T) -> ConstPtr { + ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) + } + + fn mut_ptr(value: &mut T) -> MutPtr { + MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) + } + + fn null_mut_ptr() -> MutPtr { + MutPtr::::from_usize(0) + } + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = crate::tests::test_platform(); + ::run_test_thread(f) + } + + fn unicode_string(value: &[u16]) -> UnicodeString { + let byte_len = u16::try_from(core::mem::size_of_val(value)).unwrap(); + UnicodeString { + length: byte_len, + maximum_length: byte_len, + padding_0: [0; 4], + buffer: value.as_ptr() as usize, + } + } + + fn utf16(value: &str) -> std::vec::Vec { + value.encode_utf16().collect() + } + + fn object_attributes(name: &UnicodeString) -> ObjectAttributes { + ObjectAttributes { + length: u32::try_from(core::mem::size_of::()).unwrap(), + root_directory: Handle::default(), + object_name: core::ptr::from_ref(name) as usize, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + } + } + + fn open_object_attributes( + path: &str, + ) -> ( + std::vec::Vec, + std::boxed::Box, + ObjectAttributes, + ) { + let path = utf16(path); + let name = std::boxed::Box::new(unicode_string(&path)); + let attributes = object_attributes(&name); + (path, name, attributes) + } + + fn create_existing_file(task: &Task, path: &str, data: &[u8]) { + let fd = task + .fs + .open(path, OFlags::CREAT | OFlags::RDWR, Mode::RUSR | Mode::WUSR) + .unwrap(); + assert_eq!(task.fs.write(&fd, data, Some(0)).unwrap(), data.len()); + task.fs.close(&fd).unwrap(); + } + + fn create_file( + task: &Task, + path: &str, + desired_access: u32, + create_disposition: u32, + ) -> (NtStatus, Handle, IoStatusBlock) { + let (_path, _name, attributes) = open_object_attributes(path); + let mut handle = Handle::default(); + let mut io_status = IoStatusBlock::default(); + let status = task.sys_nt_create_file( + mut_ptr(&mut handle), + desired_access, + Some(const_ptr(&attributes)), + mut_ptr(&mut io_status), + None, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + create_disposition, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ); + (status, handle, io_status) + } + + #[test] + fn nt_open_file_opens_existing_absolute_and_relative_files() { + let task = crate::tests::test_task(); + create_existing_file(&task, "/tmp/dir-file-root.txt", b"root"); + task.fs + .mkdir("/tmp/dir", Mode::RUSR | Mode::WUSR | Mode::XUSR) + .unwrap(); + create_existing_file(&task, "/tmp/dir/child.txt", b"child"); + + let (_path, _name, attributes) = open_object_attributes("\\tmp\\dir-file-root.txt"); + 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(&attributes)), + mut_ptr(&mut io_status), + FILE_SHARE_READ, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + ), + NtStatus::SUCCESS + ); + assert_ne!(handle, Handle::default()); + assert_eq!( + io_status.information, + usize::from(FileCreateInformation::Opened) + ); + + let (_path, _name, directory_attributes) = open_object_attributes("\\tmp\\dir"); + let directory_handle = task + .do_nt_create_file( + FILE_GENERIC_READ, + directory_attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ, + CreateDisposition::Open, + (FileCreateOptions::DIRECTORY_FILE | FileCreateOptions::SYNCHRONOUS_IO_NONALERT) + .bits(), + None, + 0, + ) + .unwrap() + .0; + let (_path, _child_name, mut child_attributes) = open_object_attributes("child.txt"); + child_attributes.root_directory = directory_handle; + let (child_handle, information) = task + .do_nt_create_file( + FILE_GENERIC_READ, + child_attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap(); + assert_ne!(child_handle, Handle::default()); + assert_eq!(information, FileCreateInformation::Opened); + } + + #[test] + fn nt_create_file_reports_disposition_information() { + let task = crate::tests::test_task(); + create_existing_file(&task, "/tmp/existing.txt", b"old"); + + let (status, handle, io_status) = + create_file(&task, "/tmp/existing.txt", FILE_GENERIC_READ, FILE_OPEN); + assert_eq!(status, NtStatus::SUCCESS); + assert_ne!(handle, Handle::default()); + assert_eq!(io_status.status, NtStatus::SUCCESS.as_raw()); + assert_eq!( + io_status.information, + usize::from(FileCreateInformation::Opened) + ); + + let (status, handle, io_status) = create_file( + &task, + "/tmp/created.txt", + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + FILE_CREATE, + ); + assert_eq!(status, NtStatus::SUCCESS); + assert_ne!(handle, Handle::default()); + assert_eq!( + io_status.information, + usize::from(FileCreateInformation::Created) + ); + + let (status, handle, io_status) = create_file( + &task, + "/tmp/supersede-created.txt", + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + FILE_SUPERSEDE, + ); + assert_eq!(status, NtStatus::SUCCESS); + assert_ne!(handle, Handle::default()); + assert_eq!( + io_status.information, + usize::from(FileCreateInformation::Created) + ); + + let (status, _handle, _io_status) = create_file( + &task, + "/tmp/existing.txt", + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + FILE_SUPERSEDE, + ); + assert_eq!(status, NtStatus::ACCESS_DENIED); + + let (status, handle, io_status) = create_file( + &task, + "/tmp/existing.txt", + FILE_GENERIC_READ | FILE_GENERIC_WRITE | AccessMask::DELETE.bits(), + FILE_SUPERSEDE, + ); + assert_eq!(status, NtStatus::SUCCESS); + assert_ne!(handle, Handle::default()); + assert_eq!( + io_status.information, + usize::from(FileCreateInformation::Superseded) + ); + + let (status, handle, io_status) = create_file( + &task, + "/tmp/created.txt", + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + FILE_OVERWRITE, + ); + assert_eq!(status, NtStatus::SUCCESS); + assert_ne!(handle, Handle::default()); + assert_eq!( + io_status.information, + usize::from(FileCreateInformation::Overwritten) + ); + } + + #[test] + fn nt_create_file_reports_missing_and_collision_information() { + let task = crate::tests::test_task(); + create_existing_file(&task, "/tmp/existing-collision.txt", b"old"); + + let (status, _handle, io_status) = + create_file(&task, "/tmp/missing.txt", FILE_GENERIC_READ, FILE_OPEN); + assert_eq!(status, NtStatus::OBJECT_NAME_NOT_FOUND); + assert_eq!(io_status.status, NtStatus::OBJECT_NAME_NOT_FOUND.as_raw()); + assert_eq!( + io_status.information, + usize::from(FileCreateInformation::DoesNotExist) + ); + + let (status, _handle, io_status) = create_file( + &task, + "/tmp/existing-collision.txt", + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + FILE_CREATE, + ); + assert_eq!(status, NtStatus::OBJECT_NAME_COLLISION); + assert_eq!(io_status.status, NtStatus::OBJECT_NAME_COLLISION.as_raw()); + assert_eq!( + io_status.information, + usize::from(FileCreateInformation::Exists) + ); + } + + #[test] + fn nt_create_file_rejects_invalid_share_access() { + let task = crate::tests::test_task(); + 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(); + + assert_eq!( + task.do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + 0x8, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap_err(), + NtStatus::INVALID_PARAMETER + ); + } + + #[test] + fn nt_create_file_directory_handles_can_root_relative_opens() { + let task = crate::tests::test_task(); + let (_path, _name, attributes) = open_object_attributes("/tmp/created-dir"); + let mut io_status = IoStatusBlock::default(); + let directory_handle = task + .do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Create, + (FileCreateOptions::DIRECTORY_FILE | FileCreateOptions::SYNCHRONOUS_IO_NONALERT) + .bits(), + None, + 0, + ) + .unwrap() + .0; + let (_path, _name, mut child_attributes) = open_object_attributes("child.txt"); + child_attributes.root_directory = directory_handle; + let child_handle = task + .do_nt_create_file( + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + child_attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Create, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0; + assert_ne!(child_handle, Handle::default()); + } + + #[test] + fn nt_create_file_actual_directory_handles_can_root_relative_opens() { + let task = crate::tests::test_task(); + task.fs + .mkdir("/tmp/implicit-dir", Mode::RUSR | Mode::WUSR | Mode::XUSR) + .unwrap(); + create_existing_file(&task, "/tmp/implicit-dir/child.txt", b"child"); + let (_path, _name, attributes) = open_object_attributes("/tmp/implicit-dir"); + let mut io_status = IoStatusBlock::default(); + let directory_handle = task + .do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0; + let (_path, _name, mut child_attributes) = open_object_attributes("child.txt"); + child_attributes.root_directory = directory_handle; + + let (child_handle, information) = task + .do_nt_create_file( + FILE_GENERIC_READ, + child_attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap(); + assert_ne!(child_handle, Handle::default()); + assert_eq!(information, FileCreateInformation::Opened); + } + + #[test] + fn nt_create_file_validates_create_options() { + let generic_read = FileAccess::from_desired_access(FILE_GENERIC_READ); + let synchronize = FileAccess::SYNCHRONIZE; + + assert_eq!( + validate_create_options( + generic_read, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO, + ), + Err(NtStatus::INVALID_PARAMETER) + ); + assert_eq!( + validate_create_options( + FileAccess::READ_DATA, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT, + ), + Err(NtStatus::INVALID_PARAMETER) + ); + assert_eq!( + validate_create_options( + FileAccess::APPEND_DATA | synchronize, + CreateDisposition::Open, + FileCreateOptions::NO_INTERMEDIATE_BUFFERING, + ), + Err(NtStatus::INVALID_PARAMETER) + ); + assert_eq!( + validate_create_options( + generic_read, + CreateDisposition::Open, + FileCreateOptions::DELETE_ON_CLOSE, + ), + Err(NtStatus::INVALID_PARAMETER) + ); + assert_eq!( + validate_create_options( + generic_read, + CreateDisposition::Overwrite, + FileCreateOptions::DIRECTORY_FILE, + ), + Err(NtStatus::INVALID_PARAMETER) + ); + assert_eq!( + validate_create_options( + generic_read, + CreateDisposition::Open, + FileCreateOptions::DIRECTORY_FILE | FileCreateOptions::SEQUENTIAL_ONLY, + ), + Err(NtStatus::INVALID_PARAMETER) + ); + assert_eq!( + validate_create_options( + generic_read, + CreateDisposition::Open, + FileCreateOptions::DIRECTORY_FILE + | FileCreateOptions::WRITE_THROUGH + | FileCreateOptions::SYNCHRONOUS_IO_NONALERT, + ), + Ok(()) + ); + assert_eq!( + validate_create_options( + generic_read | FileAccess::DELETE, + CreateDisposition::Open, + FileCreateOptions::DIRECTORY_FILE + | FileCreateOptions::DELETE_ON_CLOSE + | FileCreateOptions::COMPLETE_IF_OPLOCKED + | FileCreateOptions::OPEN_REPARSE_POINT + | FileCreateOptions::OPEN_FOR_FREE_SPACE_QUERY + | FileCreateOptions::NO_COMPRESSION + | FileCreateOptions::SYNCHRONOUS_IO_NONALERT, + ), + Ok(()) + ); + assert!( + generic_read + .open_flags( + CreateDisposition::Open, + FileCreateOptions::NON_DIRECTORY_FILE + ) + .contains(OFlags::NOFOLLOW) + ); + } + + #[test] + fn nt_create_file_enforces_share_access() { + let task = crate::tests::test_task(); + 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(); + let first_handle = task + .do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + 0, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0; + assert_ne!(first_handle, Handle::default()); + + let (_path, _name, attributes) = open_object_attributes("/tmp/shared.txt"); + assert_eq!( + task.do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap_err(), + NtStatus::SHARING_VIOLATION + ); + } + + #[test] + fn nt_close_releases_file_handle_and_share_lock() { + let task = crate::tests::test_task(); + 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(); + let first_handle = task + .do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + 0, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0; + + let (_path, _name, attributes) = open_object_attributes("/tmp/close-shared.txt"); + assert_eq!( + task.do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap_err(), + NtStatus::SHARING_VIOLATION + ); + + assert_eq!(task.sys_nt_close(first_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(first_handle), NtStatus::INVALID_HANDLE); + + let (_path, _name, attributes) = open_object_attributes("/tmp/close-shared.txt"); + let second_handle = task + .do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0; + assert_eq!(task.sys_nt_close(second_handle), NtStatus::SUCCESS); + } + + #[test] + fn nt_close_deletes_delete_on_close_file() { + let task = crate::tests::test_task(); + 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(); + let handle = task + .do_nt_create_file( + FILE_GENERIC_READ | AccessMask::DELETE.bits(), + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Open, + (FileCreateOptions::SYNCHRONOUS_IO_NONALERT | FileCreateOptions::DELETE_ON_CLOSE) + .bits(), + None, + 0, + ) + .unwrap() + .0; + + assert!(task.fs.file_status("/tmp/delete-on-close.txt").is_ok()); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + assert!(matches!( + task.fs.file_status("/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 (_path, _name, attributes) = open_object_attributes("/tmp/delete-on-close-dir"); + let mut io_status = IoStatusBlock::default(); + let handle = task + .do_nt_create_file( + FILE_GENERIC_READ | AccessMask::DELETE.bits(), + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Create, + (FileCreateOptions::DIRECTORY_FILE + | FileCreateOptions::SYNCHRONOUS_IO_NONALERT + | FileCreateOptions::DELETE_ON_CLOSE) + .bits(), + None, + 0, + ) + .unwrap() + .0; + + assert!(task.fs.file_status("/tmp/delete-on-close-dir").is_ok()); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + assert!(matches!( + task.fs.file_status("/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 (_path, _name, attributes) = open_object_attributes("/tmp/iosb-fault.txt"); + let mut io_status = IoStatusBlock::default(); + let created_handle = task + .do_nt_create_file( + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Create, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0; + let mut handle_output = created_handle; + + let status = run_with_test_platform_pointers(|| { + write_file_result::( + mut_ptr(&mut handle_output), + null_mut_ptr::(), + Ok((created_handle, FileCreateInformation::Created)), + |handle| task.close_file_handle(handle), + ) + }); + + assert_eq!(status, NtStatus::ACCESS_VIOLATION); + assert_eq!(handle_output, Handle::default()); + assert_eq!(task.sys_nt_close(created_handle), NtStatus::INVALID_HANDLE); + let (_path, _name, attributes) = open_object_attributes("/tmp/iosb-fault.txt"); + let reopened_handle = task + .do_nt_create_file( + FILE_GENERIC_READ, + attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0; + assert_eq!(task.sys_nt_close(reopened_handle), NtStatus::SUCCESS); + } + + #[test] + fn probe_file_outputs_preserves_handle_output_when_iosb_probe_fails() { + let original_handle = Handle::from_raw_fd(0).unwrap(); + let mut handle = original_handle; + + let status = run_with_test_platform_pointers(|| { + probe_file_outputs::(mut_ptr(&mut handle), null_mut_ptr()) + }); + + assert_eq!(status, Err(NtStatus::ACCESS_VIOLATION)); + assert_eq!(handle, original_handle); + } + + #[test] + fn nt_create_file_maps_dos_paths_into_the_sandbox_fs() { + assert_eq!( + absolute_nt_file_name_to_fs_path(r"\??\C:\Windows\System32\ntdll.dll").unwrap(), + "/Windows/System32/ntdll.dll" + ); + assert_eq!( + absolute_nt_file_name_to_fs_path(r"\??\c:\windows\system32\KERNEL32.DLL").unwrap(), + "/Windows/System32/kernel32.dll" + ); + assert_eq!( + absolute_nt_file_name_to_fs_path( + r"\Device\HarddiskVolume1\Windows\System32\c_1252.NLS" + ) + .unwrap(), + "/Windows/System32/c_1252.nls" + ); + assert_eq!( + absolute_nt_file_name_to_fs_path(r"\SystemRoot\System32\kernel32.dll").unwrap(), + "/Windows/System32/kernel32.dll" + ); + assert_eq!( + absolute_nt_file_name_to_fs_path(r"\Device\ConDrv\Output").unwrap(), + "/dev/stdout" + ); + assert_eq!( + absolute_nt_file_name_to_fs_path(r"\Device\ConDrv\Connect").unwrap(), + "/dev/null" + ); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + mod host_fidelity { + use super::*; + use core::ffi::c_void; + + #[link(name = "ntdll")] + unsafe extern "system" { + fn NtCreateFile( + FileHandle: *mut *mut c_void, + DesiredAccess: u32, + ObjectAttributes: *const ObjectAttributes, + IoStatusBlock: *mut IoStatusBlock, + AllocationSize: *const i64, + FileAttributes: u32, + ShareAccess: u32, + CreateDisposition: u32, + CreateOptions: u32, + EaBuffer: *const c_void, + EaLength: u32, + ) -> i32; + fn NtOpenFile( + FileHandle: *mut *mut c_void, + DesiredAccess: u32, + ObjectAttributes: *const ObjectAttributes, + IoStatusBlock: *mut IoStatusBlock, + ShareAccess: u32, + OpenOptions: u32, + ) -> i32; + fn NtClose(Handle: *mut c_void) -> i32; + } + + fn host_nt_path(path: &std::path::Path) -> std::string::String { + std::format!(r"\??\{}", path.display()) + } + + fn test_tmp_dir(name: &str) -> std::path::PathBuf { + std::env::var_os("CARGO_TARGET_TMPDIR") + .map_or_else(std::env::temp_dir, std::path::PathBuf::from) + .join(name) + } + + fn host_object_attributes(name: &UnicodeString) -> ObjectAttributes { + object_attributes(name) + } + + fn close_host_handle(handle: *mut c_void) { + if !handle.is_null() { + // SAFETY: The handle was returned by `NtCreateFile`/`NtOpenFile` in this test. + let status = unsafe { NtClose(handle) }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + } + } + + #[test] + fn nt_open_file_existing_file_matches_host_status_and_information() { + let test_dir = + test_tmp_dir("nt_open_file_existing_file_matches_host_status_and_information"); + let _ = std::fs::remove_dir_all(&test_dir); + std::fs::create_dir_all(&test_dir).unwrap(); + let host_file = test_dir.join("existing.txt"); + std::fs::write(&host_file, b"host").unwrap(); + + let host_name_units = utf16(&host_nt_path(&host_file)); + let host_name = unicode_string(&host_name_units); + let host_attributes = host_object_attributes(&host_name); + let mut host_handle = core::ptr::null_mut(); + let mut host_io_status = IoStatusBlock::default(); + // SAFETY: All pointers reference live test locals, and ObjectName is an NT path + // to the temporary file created above. + let host_status = unsafe { + NtOpenFile( + &raw mut host_handle, + FILE_GENERIC_READ, + &raw const host_attributes, + &raw mut host_io_status, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + 0, + ) + }; + close_host_handle(host_handle); + + let task = crate::tests::test_task(); + 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(); + let mut litebox_io_status = IoStatusBlock::default(); + let litebox_status = task.sys_nt_open_file( + mut_ptr(&mut litebox_handle), + FILE_GENERIC_READ, + Some(const_ptr(&attributes)), + mut_ptr(&mut litebox_io_status), + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + ); + + assert_eq!(host_status, litebox_status.as_raw()); + assert_eq!(host_io_status.status, litebox_io_status.status); + assert_eq!(host_io_status.information, litebox_io_status.information); + } + + #[test] + fn nt_create_file_supersede_missing_matches_host_status_and_information() { + let test_dir = test_tmp_dir( + "nt_create_file_supersede_missing_matches_host_status_and_information", + ); + let _ = std::fs::remove_dir_all(&test_dir); + std::fs::create_dir_all(&test_dir).unwrap(); + let host_file = test_dir.join("created.txt"); + + let host_name_units = utf16(&host_nt_path(&host_file)); + let host_name = unicode_string(&host_name_units); + let host_attributes = host_object_attributes(&host_name); + let mut host_handle = core::ptr::null_mut(); + let mut host_io_status = IoStatusBlock::default(); + // SAFETY: All pointers reference live test locals, the optional pointer + // arguments are null, and ObjectName points to a path in the test directory. + let host_status = unsafe { + NtCreateFile( + &raw mut host_handle, + FILE_GENERIC_READ | FILE_GENERIC_WRITE | AccessMask::DELETE.bits(), + &raw const host_attributes, + &raw mut host_io_status, + core::ptr::null(), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_SUPERSEDE, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + core::ptr::null(), + 0, + ) + }; + close_host_handle(host_handle); + + let task = crate::tests::test_task(); + let (_path, _name, attributes) = open_object_attributes("/tmp/supersede-created.txt"); + let mut litebox_handle = Handle::default(); + let mut litebox_io_status = IoStatusBlock::default(); + let litebox_status = task.sys_nt_create_file( + mut_ptr(&mut litebox_handle), + FILE_GENERIC_READ | FILE_GENERIC_WRITE | AccessMask::DELETE.bits(), + Some(const_ptr(&attributes)), + mut_ptr(&mut litebox_io_status), + None, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_SUPERSEDE, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ); + + assert_eq!(host_status, litebox_status.as_raw()); + assert_eq!(host_io_status.status, litebox_io_status.status); + assert_eq!(host_io_status.information, litebox_io_status.information); + } + } +} diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index accb011817..8e1b7530a4 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +pub(crate) mod file; pub(crate) mod registry; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; @@ -85,6 +86,30 @@ impl ProcessHandle { #[allow(clippy::enum_variant_names)] #[derive(Debug)] pub(crate) enum SyscallRequest { + NtClose { + handle: Handle, + }, + NtOpenFile { + file_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + io_status_block: Platform::RawMutPointer, + share_access: u32, + open_options: u32, + }, + NtCreateFile { + file_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + io_status_block: Platform::RawMutPointer, + allocation_size: Option>, + file_attributes: u32, + share_access: u32, + create_disposition: u32, + create_options: u32, + ea_buffer: Option>, + ea_length: u32, + }, NtOpenKey { key_handle: Platform::RawMutPointer, desired_access: u32, @@ -133,6 +158,30 @@ impl SyscallRequest { } match NtSysno::from_raw(pt_regs.orig_rax)? { + NtSysno::NtClose => Some(sys_req!(NtClose { + handle: { Handle::from_raw }, + })), + NtSysno::NtOpenFile => Some(sys_req!(NtOpenFile { + file_handle:*, + desired_access, + object_attributes:*, + io_status_block:*, + share_access, + open_options, + })), + NtSysno::NtCreateFile => Some(sys_req!(NtCreateFile { + file_handle:*, + desired_access, + object_attributes:*, + io_status_block:*, + allocation_size:*, + file_attributes, + share_access, + create_disposition, + create_options, + ea_buffer:*, + ea_length, + })), NtSysno::NtOpenKey => Some(sys_req!(NtOpenKey { key_handle:*, desired_access, diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 8004c2bc8d..1c20bed42b 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -56,7 +56,7 @@ type RegistryFileSystem = litebox::fs::layered::FileSystem< litebox::fs::tar_ro::FileSystem, >; -struct RegistryKeySubsystem(PhantomData); +pub(crate) struct RegistryKeySubsystem(PhantomData); impl FdEnabledSubsystem for RegistryKeySubsystem { type Entry = RegistryKeyObject; @@ -64,7 +64,7 @@ impl FdEnabledSubsystem for RegistryKeySubsystem< impl FdEnabledSubsystemEntry for RegistryKeyObject {} -struct RegistryKeyObject { +pub(crate) struct RegistryKeyObject { path: String, fd: TypedFd>, granted_access: RegistryKeyAccess, @@ -377,7 +377,7 @@ impl Task { ) } - fn remove_registry_key_handle(&self, handle: Handle) { + pub(crate) fn close_registry_key_handle(&self, handle: Handle) { remove_raw_handle::>( &self.global.litebox, &self.process.handles, @@ -386,7 +386,7 @@ impl Task { ); } - fn close_registry_key(&self, key: RegistryKeyObject) { + pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { let _ = self.global.registry.fs.close(&key.fd); } @@ -406,7 +406,7 @@ impl Task { match self.do_nt_open_key(desired_access, object_attributes) { Ok(handle) => { if key_handle.write_at_offset(0, handle).is_none() { - self.remove_registry_key_handle(handle); + self.close_registry_key_handle(handle); return NtStatus::ACCESS_VIOLATION; } @@ -1244,6 +1244,42 @@ mod tests { assert_ne!(handle, Handle::default()); } + #[test] + fn nt_close_removes_registry_key_handle() { + let task = crate::tests::test_task(); + let key_handle = open_code_page_key(&task); + let value_name = utf16("ACP"); + let value_name = unicode_string(&value_name); + let mut information = [0u8; 64]; + let mut result_length = 0; + + assert!( + task.do_nt_query_value_key( + key_handle, + value_name, + KeyValueInformationClass::Partial, + mut_byte_ptr(&mut information), + u32::try_from(information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .is_ok() + ); + assert_eq!(task.sys_nt_close(key_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(key_handle), NtStatus::INVALID_HANDLE); + assert_eq!( + task.do_nt_query_value_key( + key_handle, + value_name, + KeyValueInformationClass::Partial, + mut_byte_ptr(&mut information), + u32::try_from(information.len()).unwrap(), + mut_ptr(&mut result_length), + ) + .unwrap_err(), + NtStatus::INVALID_HANDLE + ); + } + #[test] fn nt_query_value_key_reports_partial_information() { let task = crate::tests::test_task(); diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 76a285af07..b8c1412b82 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -34,6 +34,20 @@ pub(crate) fn test_task() -> Task { let platform = test_platform(); let litebox = LiteBox::new(platform); let page_manager = WindowsPageManager::::new(&litebox); + let mut in_mem = litebox::fs::in_mem::FileSystem::new(&litebox); + in_mem.with_root_privileges(|fs| { + use litebox::fs::FileSystem as _; + fs.mkdir( + "/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("/tmp", Some(1000), Some(1000)) + .expect("/tmp chown cannot fail on a fresh in-memory file system"); + }); + let tar_ro = + litebox::fs::tar_ro::FileSystem::new(&litebox, litebox::fs::tar_ro::EMPTY_TAR_FILE.into()); + let fs = Arc::new(crate::default_fs(&litebox, in_mem, tar_ro)); Task { global: Arc::new(GlobalState { platform, @@ -47,8 +61,8 @@ pub(crate) fn test_task() -> Task { handles: WindowsHandleStore::::new(RawDescriptorStorage::new()), exit_code: AtomicI32::new(0), }), + fs, entry_point: 0, stack_top: 0, - _phantom: PhantomData, } } From 267a2674a5323f28b415166cb1e826d5a205f1f1 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 2 Jun 2026 14:55:40 -0700 Subject: [PATCH 020/319] Cherry-pick sendfile syscall to ulitebox (#895) Cherry-picks 361c0e89c07acfbda53b45c6f3d3776df09e7f0d (`Add syscall sendfile (#867)`) onto `ulitebox`. --- litebox_common_linux/src/lib.rs | 7 + .../tests/sendfile.c | 394 ++++++++++++++++++ litebox_shim_linux/src/lib.rs | 6 + litebox_shim_linux/src/syscalls/file.rs | 133 +++++- 4 files changed, 528 insertions(+), 12 deletions(-) create mode 100644 litebox_runner_linux_userland/tests/sendfile.c diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 0521f95148..a859ffbcd7 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -2067,6 +2067,12 @@ pub enum SyscallRequest { count: usize, offset: i64, }, + Sendfile { + out_fd: i32, + in_fd: i32, + offset: Option>, + count: usize, + }, Readv { fd: i32, iovec: Platform::RawConstPointer>>, @@ -2585,6 +2591,7 @@ impl SyscallRequest { count, offset }), + Sysno::sendfile => sys_req!(Sendfile { out_fd, in_fd, offset:*, count }), Sysno::readv => sys_req!(Readv { fd, iovec:*, iovcnt }), Sysno::writev => sys_req!(Writev { fd, iovec:*, iovcnt }), Sysno::preadv => sys_req!(Preadv { fd, iovec:*, iovcnt, pos_l, pos_h }), diff --git a/litebox_runner_linux_userland/tests/sendfile.c b/litebox_runner_linux_userland/tests/sendfile.c new file mode 100644 index 0000000000..5a6dc20dcd --- /dev/null +++ b/litebox_runner_linux_userland/tests/sendfile.c @@ -0,0 +1,394 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define _GNU_SOURCE +#include "helpers.h" + +#include +#include + +#define SRC_PATH "/tmp/lb_sendfile_src" +#define DST_PATH "/tmp/lb_sendfile_dst" + +// Raw syscall — the shim intercepts SYS_sendfile. +static ssize_t sys_sendfile(int out_fd, int in_fd, off_t *offset, size_t count) { + return (ssize_t)syscall(SYS_sendfile, out_fd, in_fd, offset, count); +} + +static int make_src_with_data(const char *data, size_t len) { + int fd = open(SRC_PATH, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) die("open SRC_PATH"); + if (write(fd, data, len) != (ssize_t)len) die("write src"); + if (lseek(fd, 0, SEEK_SET) < 0) die("lseek src"); + return fd; +} + +static int make_dst_empty(void) { + int fd = open(DST_PATH, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) die("open DST_PATH"); + return fd; +} + +static off_t fd_pos(int fd) { + off_t p = lseek(fd, 0, SEEK_CUR); + if (p < 0) die("lseek SEEK_CUR"); + return p; +} + +static void set_nonblocking(int fd) { + int flags = fcntl(fd, F_GETFL); + if (flags < 0) die("fcntl F_GETFL"); + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) die("fcntl F_SETFL O_NONBLOCK"); +} + +static void fill_pipe_until_eagain(int write_fd) { + char buf[4096]; + memset(buf, 'p', sizeof(buf)); + + for (;;) { + ssize_t n = write(write_fd, buf, sizeof(buf)); + if (n > 0) continue; + if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return; + if (n < 0) die("fill pipe"); + TEST_ASSERT(0, "fill pipe: zero-byte write"); + } +} + +static void drain_pipe_exact(int read_fd, size_t want) { + char buf[4096]; + + while (want > 0) { + size_t chunk = want < sizeof(buf) ? want : sizeof(buf); + ssize_t n = read(read_fd, buf, chunk); + if (n < 0) die("drain pipe"); + TEST_ASSERT(n != 0, "drain pipe: EOF before requested bytes"); + want -= (size_t)n; + } +} + +static void read_full(int fd, char *buf, size_t want) { + size_t got = 0; + while (got < want) { + ssize_t n = read(fd, buf + got, want - got); + if (n < 0) die("read"); + TEST_ASSERT(n != 0, "short read"); + got += (size_t)n; + } +} + +static void test_happy_null_offset(void) { + const char data[] = "abcdefghijklmnopqrstuvwxyz"; + const size_t len = sizeof(data) - 1; + int src = make_src_with_data(data, len); + int dst = make_dst_empty(); + + ssize_t r = sys_sendfile(dst, src, NULL, len); + TEST_ASSERT(r == (ssize_t)len, "happy_null_offset: return count"); + TEST_ASSERT(fd_pos(src) == (off_t)len, "happy_null_offset: source position"); + TEST_ASSERT(fd_pos(dst) == (off_t)len, "happy_null_offset: destination position"); + + if (lseek(dst, 0, SEEK_SET) < 0) die("lseek dst"); + char buf[64] = {0}; + read_full(dst, buf, len); + TEST_ASSERT(memcmp(buf, data, len) == 0, "happy_null_offset: dst content"); + + close(src); + close(dst); +} + +static void test_happy_with_offset(void) { + const char data[] = "0123456789ABCDEF"; + const size_t len = sizeof(data) - 1; + int src = make_src_with_data(data, len); + int dst = make_dst_empty(); + + if (lseek(src, 3, SEEK_SET) < 0) die("lseek src to 3"); + off_t off = 5; + ssize_t r = sys_sendfile(dst, src, &off, 4); + TEST_ASSERT(r == 4, "happy_with_offset: return count"); + TEST_ASSERT(off == 9, "happy_with_offset: offset pointer"); + // src position must NOT have moved when an explicit offset was supplied. + TEST_ASSERT(fd_pos(src) == 3, "happy_with_offset: source position unchanged"); + TEST_ASSERT(fd_pos(dst) == 4, "happy_with_offset: destination position"); + + if (lseek(dst, 0, SEEK_SET) < 0) die("lseek dst"); + char buf[8] = {0}; + read_full(dst, buf, 4); + TEST_ASSERT(memcmp(buf, "5678", 4) == 0, "happy_with_offset: dst content"); + + close(src); + close(dst); +} + +static void test_count_exceeds_remaining(void) { + const char data[] = "12345678"; + const size_t len = sizeof(data) - 1; + int src = make_src_with_data(data, len); + int dst = make_dst_empty(); + + if (lseek(src, 5, SEEK_SET) < 0) die("lseek src to 5"); + ssize_t r = sys_sendfile(dst, src, NULL, 100); + TEST_ASSERT(r == 3, "count_exceeds_remaining: return count"); + TEST_ASSERT(fd_pos(src) == (off_t)len, "count_exceeds_remaining: source position"); + + close(src); + close(dst); +} + +static void test_offset_past_eof(void) { + const char data[] = "tiny"; + int src = make_src_with_data(data, sizeof(data) - 1); + int dst = make_dst_empty(); + + off_t off = 100; + ssize_t r = sys_sendfile(dst, src, &off, 8); + TEST_ASSERT(r == 0, "offset_past_eof: return count"); + TEST_ASSERT(off == 100, "offset_past_eof: offset pointer unchanged"); + TEST_ASSERT(fd_pos(dst) == 0, "offset_past_eof: destination position"); + + close(src); + close(dst); +} + +static void test_count_zero(void) { + const char data[] = "anything"; + int src = make_src_with_data(data, sizeof(data) - 1); + int dst = make_dst_empty(); + + ssize_t r = sys_sendfile(dst, src, NULL, 0); + TEST_ASSERT(r == 0, "count_zero_null_off: return count"); + TEST_ASSERT(fd_pos(src) == 0, "count_zero_null_off: source position unchanged"); + + off_t off = 4; + r = sys_sendfile(dst, src, &off, 0); + TEST_ASSERT(r == 0, "count_zero_with_off: return count"); + TEST_ASSERT(off == 4, "count_zero_with_off: offset pointer unchanged"); + TEST_ASSERT(fd_pos(src) == 0, "count_zero_with_off: source position unchanged"); + + close(src); + close(dst); +} + +static void test_bad_in_fd(void) { + int dst = make_dst_empty(); + errno = 0; + ssize_t r = sys_sendfile(dst, 9999, NULL, 4); + TEST_ASSERT(r == -1 && errno == EBADF, "bad_in_fd: EBADF"); + close(dst); +} + +static void test_bad_out_fd(void) { + const char data[] = "data"; + int src = make_src_with_data(data, sizeof(data) - 1); + if (lseek(src, 2, SEEK_SET) < 0) die("lseek src to 2"); + errno = 0; + ssize_t r = sys_sendfile(9999, src, NULL, 4); + TEST_ASSERT(r == -1 && errno == EBADF, "bad_out_fd: EBADF"); + TEST_ASSERT(fd_pos(src) == 2, "bad_out_fd: source position unchanged"); + close(src); +} + +static void test_bad_out_fd_checked_before_bad_in_fd_type(void) { + int pfd[2]; + if (pipe(pfd) != 0) die("pipe"); + if (write(pfd[1], "data", 4) != 4) die("write pipe"); + + errno = 0; + ssize_t r = sys_sendfile(9999, pfd[0], NULL, 4); + TEST_ASSERT(r == -1 && errno == EBADF, "bad_out_fd_before_bad_in_fd_type: EBADF"); + + close(pfd[0]); + close(pfd[1]); +} + +static void test_negative_offset(void) { + const char data[] = "data"; + int src = make_src_with_data(data, sizeof(data) - 1); + int dst = make_dst_empty(); + off_t off = -1; + errno = 0; + ssize_t r = sys_sendfile(dst, src, &off, 4); + TEST_ASSERT(r == -1 && errno == EINVAL, "negative_offset: EINVAL"); + close(src); + close(dst); +} + +static void test_file_to_pipe_null_offset(void) { + const char data[] = "abcdefghij"; + const size_t len = sizeof(data) - 1; + int src = make_src_with_data(data, len); + int pfd[2]; + if (pipe(pfd) != 0) die("pipe"); + + if (lseek(src, 2, SEEK_SET) < 0) die("lseek src to 2"); + ssize_t r = sys_sendfile(pfd[1], src, NULL, 5); + TEST_ASSERT(r == 5, "file_to_pipe_null_offset: return count"); + TEST_ASSERT(fd_pos(src) == 7, "file_to_pipe_null_offset: source position"); + + char buf[8] = {0}; + read_full(pfd[0], buf, 5); + TEST_ASSERT(memcmp(buf, "cdefg", 5) == 0, "file_to_pipe_null_offset: pipe content"); + + close(src); + close(pfd[0]); + close(pfd[1]); +} + +static void test_full_nonblocking_pipe_keeps_null_offset_position(void) { + char data[8192]; + for (size_t i = 0; i < sizeof(data); i++) data[i] = (char)('A' + (i % 26)); + + int src = make_src_with_data(data, sizeof(data)); + int pfd[2]; + if (pipe(pfd) != 0) die("pipe"); + set_nonblocking(pfd[1]); + fill_pipe_until_eagain(pfd[1]); + + if (lseek(src, 123, SEEK_SET) < 0) die("lseek src to 123"); + errno = 0; + ssize_t r = sys_sendfile(pfd[1], src, NULL, sizeof(data)); + TEST_ASSERT(r == -1 && (errno == EAGAIN || errno == EWOULDBLOCK), + "full_nonblocking_pipe_null_offset: EAGAIN"); + TEST_ASSERT(fd_pos(src) == 123, + "full_nonblocking_pipe_null_offset: source position unchanged"); + + close(src); + close(pfd[0]); + close(pfd[1]); +} + +static void test_partial_nonblocking_pipe_error_is_deferred(void) { + char data[16384]; + for (size_t i = 0; i < sizeof(data); i++) data[i] = (char)('a' + (i % 26)); + + int src = make_src_with_data(data, sizeof(data)); + int pfd[2]; + if (pipe(pfd) != 0) die("pipe"); + set_nonblocking(pfd[1]); + fill_pipe_until_eagain(pfd[1]); + drain_pipe_exact(pfd[0], 4096); + + if (lseek(src, 123, SEEK_SET) < 0) die("lseek src to 123"); + errno = 0; + ssize_t r = sys_sendfile(pfd[1], src, NULL, sizeof(data)); + TEST_ASSERT(r > 0, "partial_nonblocking_pipe_error_deferred: partial success"); + off_t want_pos = 123 + r; + TEST_ASSERT(fd_pos(src) == want_pos, + "partial_nonblocking_pipe_error_deferred: source position"); + + errno = 0; + ssize_t retry = sys_sendfile(pfd[1], src, NULL, sizeof(data)); + TEST_ASSERT(retry == -1 && (errno == EAGAIN || errno == EWOULDBLOCK), + "partial_nonblocking_pipe_error_deferred retry: EAGAIN"); + TEST_ASSERT(fd_pos(src) == want_pos, + "partial_nonblocking_pipe_error_deferred retry: source position unchanged"); + + close(src); + close(pfd[0]); + close(pfd[1]); +} + +static void test_file_to_pipe_with_offset(void) { + const char data[] = "ABCDEFGHIJ"; + const size_t len = sizeof(data) - 1; + int src = make_src_with_data(data, len); + int pfd[2]; + if (pipe(pfd) != 0) die("pipe"); + + // Park src at a position that must NOT change after sendfile. + if (lseek(src, 1, SEEK_SET) < 0) die("lseek src to 1"); + off_t off = 4; + ssize_t r = sys_sendfile(pfd[1], src, &off, 3); + TEST_ASSERT(r == 3, "file_to_pipe_with_offset: return count"); + TEST_ASSERT(off == 7, "file_to_pipe_with_offset: offset pointer"); + TEST_ASSERT(fd_pos(src) == 1, "file_to_pipe_with_offset: source position unchanged"); + + char buf[8] = {0}; + read_full(pfd[0], buf, 3); + TEST_ASSERT(memcmp(buf, "EFG", 3) == 0, "file_to_pipe_with_offset: pipe content"); + + close(src); + close(pfd[0]); + close(pfd[1]); +} + +// Linux returns EINVAL when a non-pread-capable in_fd is paired with a NULL +// offset, and ESPIPE when it is paired with an explicit offset (the +// FMODE_PREAD check fires first). Verify both branches for each non-regular +// fd type the shim can encounter as in_fd. +static void expect_einval_espipe_in_fd(int in_fd, const char *label) { + int dst = make_dst_empty(); + + errno = 0; + ssize_t r = sys_sendfile(dst, in_fd, NULL, 4); + TEST_ASSERT(r == -1 && errno == EINVAL, label); + + off_t off = 0; + errno = 0; + r = sys_sendfile(dst, in_fd, &off, 4); + TEST_ASSERT(r == -1 && errno == ESPIPE, label); + + close(dst); +} + +static void test_pipe_in_fd(void) { + int pfd[2]; + if (pipe(pfd) != 0) die("pipe"); + if (write(pfd[1], "data", 4) != 4) die("write pipe"); + expect_einval_espipe_in_fd(pfd[0], "pipe in_fd"); + close(pfd[0]); + close(pfd[1]); +} + +static void test_eventfd_in_fd(void) { + int efd = eventfd(7, 0); + if (efd < 0) die("eventfd"); + expect_einval_espipe_in_fd(efd, "eventfd in_fd"); + close(efd); +} + +static void test_unix_stream_in_fd(void) { + int sv[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) die("socketpair stream"); + if (write(sv[1], "data", 4) != 4) die("write unix stream"); + expect_einval_espipe_in_fd(sv[0], "unix stream in_fd"); + close(sv[0]); + close(sv[1]); +} + +static void test_unix_dgram_in_fd(void) { + int sv[2]; + if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0) die("socketpair dgram"); + if (write(sv[1], "data", 4) != 4) die("write unix dgram"); + expect_einval_espipe_in_fd(sv[0], "unix dgram in_fd"); + close(sv[0]); + close(sv[1]); +} + +int main(void) { + printf("== sendfile syscall tests ==\n"); + + test_happy_null_offset(); + test_happy_with_offset(); + test_count_exceeds_remaining(); + test_offset_past_eof(); + test_count_zero(); + test_bad_in_fd(); + test_bad_out_fd(); + test_bad_out_fd_checked_before_bad_in_fd_type(); + test_negative_offset(); + test_file_to_pipe_null_offset(); + test_full_nonblocking_pipe_keeps_null_offset_position(); + test_partial_nonblocking_pipe_error_is_deferred(); + test_file_to_pipe_with_offset(); + test_pipe_in_fd(); + test_eventfd_in_fd(); + test_unix_stream_in_fd(); + test_unix_dgram_in_fd(); + + unlink(SRC_PATH); + unlink(DST_PATH); + + printf("All sendfile tests passed.\n"); + return 0; +} diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 8c0f7144fd..9c59462e3e 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -628,6 +628,12 @@ impl Task { Some(buf) => self.sys_pwrite64(fd, &buf, offset), None => Err(Errno::EFAULT), }, + SyscallRequest::Sendfile { + out_fd, + in_fd, + offset, + count, + } => syscall!(sys_sendfile(out_fd, in_fd, offset, count)), SyscallRequest::Mmap { addr, length, diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index c7561b1cbe..600acf8523 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -517,6 +517,125 @@ impl Task { let pos = usize::try_from(offset).map_err(|_| Errno::EINVAL)?; self.sys_write(fd, buf, Some(pos)) } + + fn rewind_sendfile_in_fd(&self, in_raw_fd: usize, unread_n: usize) -> Result<(), Errno> { + if unread_n == 0 { + return Ok(()); + } + + let rewind = isize::try_from(unread_n).map_err(|_| Errno::EOVERFLOW)?; + let files = self.files.borrow(); + files + .run_on_raw_fd( + in_raw_fd, + |fd| { + files + .fs + .seek(fd, -rewind, SeekWhence::RelativeToCurrentOffset) + .map(|_| ()) + .map_err(Errno::from) + }, + |_fd| Err(Errno::EINVAL), + |_fd| Err(Errno::EINVAL), + |_fd| Err(Errno::EINVAL), + |_fd| Err(Errno::EINVAL), + |_fd| Err(Errno::EINVAL), + ) + .flatten() + } + + /// Handle syscall `sendfile` + pub(crate) fn sys_sendfile( + &self, + out_fd: i32, + in_fd: i32, + offset_ptr: Option>, + count: usize, + ) -> Result { + let Ok(in_raw_fd) = u32::try_from(in_fd).and_then(usize::try_from) else { + return Err(Errno::EBADF); + }; + // TODO: Linux rejects `sendfile` with `EINVAL` when `out_fd` has `O_APPEND` set. + self.check_raw_fd_exists(out_fd)?; + + let mut cur_off = offset_ptr + .map(|p| { + let off = p.read_at_offset(0).ok_or(Errno::EFAULT)?; + if off < 0 { + return Err(Errno::EINVAL); + } + usize::try_from(off).map_err(|_| Errno::EINVAL) + }) + .transpose()?; + + let mut kernel_buf = vec![0u8; count.min(PAGE_SIZE)]; + let mut total: usize = 0; + + while total < count { + let to_read = (count - total).min(kernel_buf.len()); + + // Non-FS sources are not seekable; Linux returns ESPIPE for any + // non-pread-capable source when an offset is supplied, EINVAL otherwise. + let non_fs_err = if cur_off.is_some() { + Errno::ESPIPE + } else { + Errno::EINVAL + }; + let read_result = { + let buf_slice = &mut kernel_buf[..to_read]; + let files = self.files.borrow(); + files + .run_on_raw_fd( + in_raw_fd, + |fd| files.fs.read(fd, buf_slice, cur_off).map_err(Errno::from), + |_fd| Err(non_fs_err), + |_fd| Err(non_fs_err), + |_fd| Err(non_fs_err), + |_fd| Err(non_fs_err), + |_fd| Err(non_fs_err), + ) + .flatten() + }; + let read_n = match read_result { + Ok(0) => break, + Ok(n) => n, + Err(e) if total == 0 => return Err(e), + Err(_) => break, + }; + + let write_result = self.sys_write(out_fd, &kernel_buf[..read_n], None); + let write_n = match write_result { + Ok(n) => n, + Err(e) => { + if offset_ptr.is_none() { + self.rewind_sendfile_in_fd(in_raw_fd, read_n)?; + } + if total == 0 { + return Err(e); + } + break; + } + }; + + total += write_n; + if let Some(ref mut off) = cur_off { + *off += write_n; + } + if write_n < read_n { + if offset_ptr.is_none() { + self.rewind_sendfile_in_fd(in_raw_fd, read_n - write_n)?; + } + break; + } + } + + if let (Some(p), Some(off)) = (offset_ptr, cur_off) { + let off = i64::try_from(off).map_err(|_| Errno::EOVERFLOW)?; + p.write_at_offset(0, off).ok_or(Errno::EFAULT)?; + } + + Ok(total) + } } fn espipe_for_non_seekable_offset(offset: Option) -> Result<(), Errno> { @@ -2171,19 +2290,9 @@ impl Task { newfd: Option, flags: Option, ) -> Result { - let Ok(oldfd) = u32::try_from(oldfd) else { - return Err(Errno::EBADF); - }; + self.check_raw_fd_exists(oldfd)?; + let oldfd = u32::try_from(oldfd).map_err(|_| Errno::EBADF)?; let oldfd_usize = usize::try_from(oldfd).or(Err(Errno::EBADF))?; - if !self - .files - .borrow() - .raw_descriptor_store - .read() - .is_alive(oldfd_usize) - { - return Err(Errno::EBADF); - } if let Some(newfd) = newfd { // dup2/dup3 let Ok(newfd) = u32::try_from(newfd) else { From 0d0a3e0a17482928a24fed502e87decdf25e6b70 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 3 Jun 2026 10:38:56 -0700 Subject: [PATCH 021/319] Cherry-pick faccessat support to ulitebox (#897) Cherry-picks e605ebb18e61e3e53ccc68883904bdb979cda0b2 onto ulitebox. --- litebox_common_linux/src/lib.rs | 18 +- litebox_runner_linux_userland/src/lib.rs | 38 ++++- .../tests/faccessat.c | 80 +++++++++ .../tests/faccessat2.c | 156 ++++++++++++++++++ litebox_runner_linux_userland/tests/helpers.h | 8 + litebox_shim_linux/src/lib.rs | 11 +- litebox_shim_linux/src/syscalls/file.rs | 155 ++++++++++++++--- 7 files changed, 430 insertions(+), 36 deletions(-) create mode 100644 litebox_runner_linux_userland/tests/faccessat.c create mode 100644 litebox_runner_linux_userland/tests/faccessat2.c diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index a859ffbcd7..ece0167d7b 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -2097,9 +2097,11 @@ pub enum SyscallRequest { pos_l: usize, pos_h: usize, }, - Access { + Faccessat { + dirfd: i32, pathname: Platform::RawConstPointer, mode: AccessFlags, + flags: AtFlags, }, Madvise { addr: Platform::RawMutPointer, @@ -2596,7 +2598,19 @@ impl SyscallRequest { Sysno::writev => sys_req!(Writev { fd, iovec:*, iovcnt }), Sysno::preadv => sys_req!(Preadv { fd, iovec:*, iovcnt, pos_l, pos_h }), Sysno::pwritev => sys_req!(Pwritev { fd, iovec:*, iovcnt, pos_l, pos_h }), - Sysno::access => sys_req!(Access { pathname:*, mode }), + Sysno::access => SyscallRequest::Faccessat { + dirfd: AT_FDCWD, + pathname: ctx.sys_req_ptr(0), + mode: ctx.sys_req_arg(1), + flags: AtFlags::empty(), + }, + Sysno::faccessat => SyscallRequest::Faccessat { + dirfd: ctx.sys_req_arg(0), + pathname: ctx.sys_req_ptr(1), + mode: ctx.sys_req_arg(2), + flags: AtFlags::empty(), + }, + Sysno::faccessat2 => sys_req!(Faccessat { dirfd, pathname:*, mode, flags }), Sysno::pipe => sys_req!(Pipe2 { pipefd:*, flags: { litebox::fs::OFlags::empty() } }), Sysno::pipe2 => sys_req!(Pipe2 { pipefd:* ,flags }), Sysno::madvise => sys_req!(Madvise { addr:*, length, behavior:? }), diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index e341904330..9a18b4a7c2 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -11,6 +11,11 @@ use std::path::{Path, PathBuf}; extern crate alloc; +// Use a stable non-root guest identity instead of mirroring the host user. This keeps shim +// credentials aligned with the in-memory filesystem default user and avoids truncating high host IDs. +const DEFAULT_GUEST_UID: u16 = 1000; +const DEFAULT_GUEST_GID: u16 = 1000; + /// Run Linux programs with LiteBox on unmodified Linux /// /// Detailed logging can be controlled via the `LITEBOX_LOG` environment variable. For example: @@ -198,6 +203,20 @@ pub fn run(cli_args: CliArgs) -> Result<()> { litebox_platform_multiplex::set_platform(platform); let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); let litebox = shim_builder.litebox(); + // SAFETY: `gettid` takes no pointer arguments and has no Rust-side aliasing requirements. + let tid = unsafe { libc::syscall(libc::SYS_gettid) } + .try_into() + .context("failed to convert gettid result to i32")?; + // SAFETY: `getppid` takes no arguments and has no Rust-side aliasing requirements. + let ppid = unsafe { libc::getppid() }; + let task_params = litebox_common_linux::TaskParams { + pid: tid, + ppid, + uid: u32::from(DEFAULT_GUEST_UID), + euid: u32::from(DEFAULT_GUEST_UID), + gid: u32::from(DEFAULT_GUEST_GID), + egid: u32::from(DEFAULT_GUEST_GID), + }; let initial_file_system = { let mut in_mem = litebox::fs::in_mem::FileSystem::new(litebox); @@ -207,6 +226,15 @@ pub fn run(cli_args: CliArgs) -> Result<()> { if let Some(prog_data) = prog_data { let prog = std::path::absolute(Path::new(&cli_args.program_and_arguments[0])).unwrap(); let ancestors: Vec<_> = prog.ancestors().collect(); + let chown_to_initial_user = |fs: &mut litebox::fs::in_mem::FileSystem, + path: &Path| { + fs.chown( + path.to_str().unwrap(), + Some(DEFAULT_GUEST_UID), + Some(DEFAULT_GUEST_GID), + ) + .unwrap(); + }; let mut prev_user = 0; for (path, &mode_and_user) in ancestors .into_iter() @@ -220,9 +248,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { in_mem.with_root_privileges(|fs| { fs.mkdir(path.to_str().unwrap(), mode_and_user.0).unwrap(); if mode_and_user.1 != 0 { - // This file is owned by a non-root user, so we need to set the ownership to our default user - fs.chown(path.to_str().unwrap(), Some(1000), Some(1000)) - .unwrap(); + chown_to_initial_user(fs, path); } }); } else { @@ -254,9 +280,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { in_mem.with_root_privileges(|fs| { open_file(fs, prog.to_str().unwrap(), last.0); if last.1 != 0 { - // This file is owned by a non-root user, so we need to set the ownership to our default user - fs.chown(prog.to_str().unwrap(), Some(1000), Some(1000)) - .unwrap(); + chown_to_initial_user(fs, &prog); } }); } else { @@ -357,8 +381,6 @@ pub fn run(cli_args: CliArgs) -> Result<()> { envp }; - let task_params = platform.init_task(); - #[cfg(target_arch = "x86_64")] litebox_platform_linux_userland::LinuxUserland::enable_seccomp_filter(); diff --git a/litebox_runner_linux_userland/tests/faccessat.c b/litebox_runner_linux_userland/tests/faccessat.c new file mode 100644 index 0000000000..e7c0b24a5f --- /dev/null +++ b/litebox_runner_linux_userland/tests/faccessat.c @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "helpers.h" + +static long raw_faccessat(int dirfd, const char *pathname, int mode) { + return syscall(SYS_faccessat, dirfd, pathname, mode); +} + +static void expect_faccessat_ok(int dirfd, const char *pathname, int mode, const char *op) { + errno = 0; + TEST_ASSERT(raw_faccessat(dirfd, pathname, mode) == 0, op); +} + +static void expect_faccessat_errno(int dirfd, const char *pathname, int mode, + int expected_errno, const char *op) { + errno = 0; + long ret = raw_faccessat(dirfd, pathname, mode); + TEST_ASSERT(ret == -1 && errno == expected_errno, op); +} + +static void test_at_fdcwd_success(void) { + const char *path = "/tmp/lb_faccessat_success"; + unlink(path); + create_test_file(path, 0600); + + expect_faccessat_ok(AT_FDCWD, path, F_OK, "faccessat AT_FDCWD F_OK should succeed"); + expect_faccessat_ok(AT_FDCWD, path, R_OK | W_OK, + "faccessat AT_FDCWD R_OK|W_OK should succeed"); + + struct stat st; + TEST_ASSERT(stat(path, &st) == 0, "stat should observe file after faccessat"); + + unlink(path); +} + +static void test_missing_path_enoent(void) { + const char *path = "/tmp/lb_faccessat_missing"; + unlink(path); + + expect_faccessat_errno(AT_FDCWD, path, F_OK, ENOENT, + "faccessat on a missing path should fail with ENOENT"); +} + +static void test_mode_permission_denied(void) { + const char *path = "/tmp/lb_faccessat_readonly"; + unlink(path); + create_test_file(path, 0400); + + expect_faccessat_errno(AT_FDCWD, path, W_OK, EACCES, + "faccessat W_OK on read-only file should fail with EACCES"); + + errno = 0; + int fd = open(path, O_RDONLY); + TEST_ASSERT(fd >= 0, "open should observe that the file still exists"); + close(fd); + + unlink(path); +} + +static void test_invalid_mode_einval(void) { + const char *path = "/tmp/lb_faccessat_invalid_mode"; + unlink(path); + create_test_file(path, 0600); + + expect_faccessat_errno(AT_FDCWD, path, R_OK | 8, EINVAL, + "faccessat with invalid mode bits should fail with EINVAL"); + + unlink(path); +} + +int main(void) { + printf("===== faccessat tests =====\n"); + test_at_fdcwd_success(); + test_missing_path_enoent(); + test_mode_permission_denied(); + test_invalid_mode_einval(); + printf("All faccessat tests passed.\n"); + return 0; +} diff --git a/litebox_runner_linux_userland/tests/faccessat2.c b/litebox_runner_linux_userland/tests/faccessat2.c new file mode 100644 index 0000000000..6db4f43ab8 --- /dev/null +++ b/litebox_runner_linux_userland/tests/faccessat2.c @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "helpers.h" + +#ifndef SYS_faccessat2 +#error SYS_faccessat2 is not defined on this build host +#endif + +static long raw_faccessat2(int dirfd, const char *pathname, int mode, int flags) { + return syscall(SYS_faccessat2, dirfd, pathname, mode, flags); +} + +static void expect_faccessat2_ok(int dirfd, const char *pathname, int mode, int flags, + const char *op) { + errno = 0; + TEST_ASSERT(raw_faccessat2(dirfd, pathname, mode, flags) == 0, op); +} + +static void expect_faccessat2_errno(int dirfd, const char *pathname, int mode, int flags, + int expected_errno, const char *op) { + errno = 0; + long ret = raw_faccessat2(dirfd, pathname, mode, flags); + TEST_ASSERT(ret == -1 && errno == expected_errno, op); +} + +static void test_at_fdcwd_success(void) { + const char *path = "/tmp/lb_faccessat2_success"; + unlink(path); + create_test_file(path, 0600); + + expect_faccessat2_ok(AT_FDCWD, path, F_OK, 0, + "faccessat2 AT_FDCWD F_OK should succeed"); + expect_faccessat2_ok(AT_FDCWD, path, R_OK | W_OK, 0, + "faccessat2 AT_FDCWD R_OK|W_OK should succeed"); + + struct stat st; + TEST_ASSERT(stat(path, &st) == 0, "stat should observe file after faccessat2"); + + unlink(path); +} + +static void test_accepted_flags_regular_file(void) { + const char *path = "/tmp/lb_faccessat2_flags"; + unlink(path); + create_test_file(path, 0600); + + expect_faccessat2_ok(AT_FDCWD, path, F_OK, AT_EACCESS, + "faccessat2 AT_EACCESS should succeed for accessible file"); + + // TODO: Add symlink follow/no-follow coverage once LiteBox file systems can + // distinguish stat and lstat semantics for symlink targets. + expect_faccessat2_ok(AT_FDCWD, path, F_OK, AT_SYMLINK_NOFOLLOW, + "faccessat2 AT_SYMLINK_NOFOLLOW should succeed for regular file"); + + unlink(path); +} + +static void test_owner_bits_take_precedence(void) { + const char *other_read_path = "/tmp/lb_faccessat2_other_read"; + const char *owner_read_path = "/tmp/lb_faccessat2_owner_read"; + unlink(other_read_path); + unlink(owner_read_path); + create_test_file(other_read_path, 0004); + create_test_file(owner_read_path, 0400); + + expect_faccessat2_errno(AT_FDCWD, other_read_path, R_OK, 0, EACCES, + "owner read should not fall through to other read bit"); + expect_faccessat2_errno(AT_FDCWD, other_read_path, R_OK, AT_EACCESS, EACCES, + "AT_EACCESS owner read should not fall through to other read bit"); + expect_faccessat2_ok(AT_FDCWD, owner_read_path, R_OK, AT_EACCESS, + "AT_EACCESS owner read bit should allow R_OK"); + + unlink(other_read_path); + unlink(owner_read_path); +} + +static void test_empty_path_success(void) { + const char *path = "/tmp/lb_faccessat2_empty_path"; + unlink(path); + create_test_file(path, 0400); + + int fd = open(path, O_RDONLY); + TEST_ASSERT(fd >= 0, "open test file failed"); + + expect_faccessat2_ok(fd, "", R_OK, AT_EMPTY_PATH, + "faccessat2 AT_EMPTY_PATH R_OK should succeed on fd"); + expect_faccessat2_errno(fd, "", W_OK, AT_EMPTY_PATH, EACCES, + "faccessat2 AT_EMPTY_PATH W_OK should fail with EACCES"); + + struct stat st; + TEST_ASSERT(fstat(fd, &st) == 0, "fstat should observe fd after faccessat2"); + + close(fd); + unlink(path); +} + +static void test_missing_path_enoent(void) { + const char *path = "/tmp/lb_faccessat2_missing"; + unlink(path); + + expect_faccessat2_errno(AT_FDCWD, path, F_OK, 0, ENOENT, + "faccessat2 on a missing path should fail with ENOENT"); +} + +static void test_mode_permission_denied(void) { + const char *path = "/tmp/lb_faccessat2_readonly"; + unlink(path); + create_test_file(path, 0400); + + expect_faccessat2_errno(AT_FDCWD, path, W_OK, 0, EACCES, + "faccessat2 W_OK on read-only file should fail with EACCES"); + + errno = 0; + int fd = open(path, O_RDONLY); + TEST_ASSERT(fd >= 0, "open should observe that the file still exists"); + close(fd); + + unlink(path); +} + +static void test_invalid_mode_einval(void) { + const char *path = "/tmp/lb_faccessat2_invalid_mode"; + unlink(path); + create_test_file(path, 0600); + + expect_faccessat2_errno(AT_FDCWD, path, R_OK | 8, 0, EINVAL, + "faccessat2 with invalid mode bits should fail with EINVAL"); + + unlink(path); +} + +static void test_invalid_flags_einval(void) { + const char *path = "/tmp/lb_faccessat2_invalid_flags"; + unlink(path); + create_test_file(path, 0600); + + expect_faccessat2_errno(AT_FDCWD, path, F_OK, 0x40000000, EINVAL, + "faccessat2 with invalid flags should fail with EINVAL"); + + unlink(path); +} + +int main(void) { + printf("===== faccessat2 tests =====\n"); + test_at_fdcwd_success(); + test_accepted_flags_regular_file(); + test_owner_bits_take_precedence(); + test_empty_path_success(); + test_missing_path_enoent(); + test_mode_permission_denied(); + test_invalid_mode_einval(); + test_invalid_flags_einval(); + printf("All faccessat2 tests passed.\n"); + return 0; +} diff --git a/litebox_runner_linux_userland/tests/helpers.h b/litebox_runner_linux_userland/tests/helpers.h index 57f373cacc..106c32dc3a 100644 --- a/litebox_runner_linux_userland/tests/helpers.h +++ b/litebox_runner_linux_userland/tests/helpers.h @@ -10,12 +10,14 @@ #define _GNU_SOURCE #include +#include #include #include #include #include #include #include +#include #include #include @@ -37,6 +39,12 @@ static inline void die(const char *msg) { exit(1); } +static inline void create_test_file(const char *path, mode_t mode) { + int fd = open(path, O_RDONLY | O_CREAT, mode); + TEST_ASSERT(fd >= 0, "create test file failed"); + TEST_ASSERT(close(fd) == 0, "close test file failed"); +} + static inline void expect_sys_shutdown(int fd, int how, const char *op) { errno = 0; if (syscall(SYS_shutdown, fd, how) != 0) { diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 9c59462e3e..09835c5415 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -674,9 +674,14 @@ impl Task { pos_l, pos_h, } => self.sys_pwritev(fd, iovec, iovcnt, preadv_pwritev_offset(pos_l, pos_h)), - SyscallRequest::Access { pathname, mode } => pathname - .to_cstring() - .map_or(Err(Errno::EFAULT), |path| syscall!(sys_access(path, mode))), + SyscallRequest::Faccessat { + dirfd, + pathname, + mode, + flags, + } => pathname.to_cstring().map_or(Err(Errno::EFAULT), |path| { + syscall!(sys_faccessat(dirfd, path, mode, flags)) + }), SyscallRequest::Madvise { addr, length, diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 600acf8523..a857477f4f 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -18,8 +18,9 @@ use litebox::{ utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; use litebox_common_linux::{ - AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, InodeType, - IoReadVec, IoWriteVec, IoctlArg, Statx, StatxMask, TimeParam, errno::Errno, signal::Signal, + AccessFlags, AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, + InodeType, IoReadVec, IoWriteVec, IoctlArg, Statx, StatxMask, TimeParam, errno::Errno, + signal::Signal, }; use litebox_platform_multiplex::Platform; use thiserror::Error; @@ -27,6 +28,21 @@ use thiserror::Error; use crate::{ConstPtr, GlobalState, MutPtr, ShimFS, Task, syscalls::signal}; use core::sync::atomic::{AtomicUsize, Ordering}; +#[derive(Clone, Copy)] +struct AccessUserInfo { + user: u32, + group: u32, +} + +impl From for AccessUserInfo { + fn from(value: litebox::fs::UserInfo) -> Self { + Self { + user: u32::from(value.user), + group: u32::from(value.group), + } + } +} + /// Task state shared by `CLONE_FS`. pub(crate) struct FsState { umask: core::sync::atomic::AtomicU32, @@ -1042,37 +1058,124 @@ impl Task { write_to_iovec(iovs, |buf, _total| self.sys_write(fd, buf, None)) } - /// Handle syscall `access` - pub fn sys_access( - &self, - pathname: impl path::Arg, - mode: litebox_common_linux::AccessFlags, + fn validate_access_mode(mode: &AccessFlags) -> Result<(), Errno> { + let valid_mode = AccessFlags::R_OK | AccessFlags::W_OK | AccessFlags::X_OK; + if mode.intersects(valid_mode.complement()) { + return Err(Errno::EINVAL); + } + Ok(()) + } + + fn do_access_mode( + mode: Mode, + owner: AccessUserInfo, + caller: AccessUserInfo, + access_mode: &AccessFlags, ) -> Result<(), Errno> { - let pathname = self.resolve_path(pathname)?; - let status = self.files.borrow().fs.file_status(pathname)?; - if mode == litebox_common_linux::AccessFlags::F_OK { + if access_mode.is_empty() { return Ok(()); } - // TODO: the check is done using the calling process's real UID and GID. - // Here we assume the caller owns the file. - if mode.contains(litebox_common_linux::AccessFlags::R_OK) - && !status.mode.contains(litebox::fs::Mode::RUSR) - { + if caller.user == 0 { + if access_mode.contains(AccessFlags::X_OK) + && !mode.intersects(Mode::XUSR | Mode::XGRP | Mode::XOTH) + { + return Err(Errno::EACCES); + } + return Ok(()); + } + // TODO: Linux also uses group bits when `owner.group` is in the caller's supplementary + // group list. `AccessUserInfo` only carries the real/effective primary group today. + let (read, write, execute) = if caller.user == owner.user { + (Mode::RUSR, Mode::WUSR, Mode::XUSR) + } else if caller.group == owner.group { + (Mode::RGRP, Mode::WGRP, Mode::XGRP) + } else { + (Mode::ROTH, Mode::WOTH, Mode::XOTH) + }; + if access_mode.contains(AccessFlags::R_OK) && !mode.contains(read) { return Err(Errno::EACCES); } - if mode.contains(litebox_common_linux::AccessFlags::W_OK) - && !status.mode.contains(litebox::fs::Mode::WUSR) - { + if access_mode.contains(AccessFlags::W_OK) && !mode.contains(write) { return Err(Errno::EACCES); } - if mode.contains(litebox_common_linux::AccessFlags::X_OK) - && !status.mode.contains(litebox::fs::Mode::XUSR) - { + if access_mode.contains(AccessFlags::X_OK) && !mode.contains(execute) { return Err(Errno::EACCES); } Ok(()) } + fn access_user(&self, flags: &AtFlags) -> AccessUserInfo { + if flags.contains(AtFlags::AT_EACCESS) { + AccessUserInfo { + user: self.credentials.euid, + group: self.credentials.egid, + } + } else { + AccessUserInfo { + user: self.credentials.uid, + group: self.credentials.gid, + } + } + } + + fn do_access( + &self, + pathname: impl path::Arg, + mode: AccessFlags, + caller: AccessUserInfo, + ) -> Result<(), Errno> { + let status = self.files.borrow().fs.file_status(pathname)?; + let owner = status.owner.into(); + Self::do_access_mode(status.mode, owner, caller, &mode) + } + + /// Handle syscall `faccessat` + pub(crate) fn sys_faccessat( + &self, + dirfd: i32, + pathname: impl path::Arg, + mode: AccessFlags, + flags: AtFlags, + ) -> Result<(), Errno> { + let supported_flags = + AtFlags::AT_EACCESS | AtFlags::AT_SYMLINK_NOFOLLOW | AtFlags::AT_EMPTY_PATH; + // TODO: `AT_SYMLINK_NOFOLLOW` is accepted for Linux compatibility, but LiteBox file + // status lookups do not currently follow symlinks in any backend. + if flags.intersects(supported_flags.complement()) { + return Err(Errno::EINVAL); + } + + Self::validate_access_mode(&mode)?; + let caller = self.access_user(&flags); + let get_cwd = || self.fs.borrow().cwd.read().clone(); + let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; + match fs_path { + FsPath::Absolute { path } => self.do_access(path, mode, caller), + FsPath::Cwd if flags.contains(AtFlags::AT_EMPTY_PATH) => { + let cwd = get_cwd(); + self.do_access(cwd, mode, caller) + } + FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { + let stat: FileStat = descriptor_stat(fd as usize, self)?; + let owner = AccessUserInfo { + user: stat.st_uid, + group: stat.st_gid, + }; + Self::do_access_mode( + Mode::from_bits_truncate(stat.st_mode & 0o7777), + owner, + caller, + &mode, + ) + } + FsPath::Cwd | FsPath::Fd(_) => Err(Errno::ENOENT), + FsPath::FdRelative { .. } => { + log_unsupported!("fd-relative faccessat is not supported yet"); + Err(Errno::EINVAL) + } + } + } + /// Read the target of a symbolic link /// /// The caller must pass an absolute path. @@ -2774,8 +2877,14 @@ mod tests { // ── sys_lstat: lstat the relative file ── task.sys_lstat("file.txt").unwrap(); - // ── sys_access: check relative file is accessible ── - task.sys_access("file.txt", AccessFlags::F_OK).unwrap(); + // ── sys_faccessat: check relative file is accessible ── + task.sys_faccessat( + litebox_common_linux::AT_FDCWD, + "file.txt", + AccessFlags::F_OK, + AtFlags::empty(), + ) + .unwrap(); // ── sys_mkdir: create a subdirectory via relative path ── task.sys_mkdir("subdir", 0o777).unwrap(); From c6091fb02200398ac98f7e8d76db2541d6e96663 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 3 Jun 2026 15:41:06 -0700 Subject: [PATCH 022/319] Set up initial Windows PEB/TEB process environment (#899) This PR adds the initial Windows process-environment setup needed to enter guest `ntdll!LdrInitializeThunk` instead of jumping directly to the application entry point. - Add Windows x64 ABI structs - Seed kernel-created PEB/TEB fields - Route startup through rewritten `ntdll!LdrInitializeThunk` when ntdll is present. - Set the guest TEB base through the platform punchthrough before resuming the initial Windows thread. - Add host-diagnostic tests that compare synthetic PEB/TEB snapshots against the current host PEB/TEB layout. With this PR, the hello world PE program now starts with `ntdll` and stops at some unsupported syscall. --- litebox_common_windows/src/loader.rs | 43 +- litebox_runner_windows_userland/tests/run.rs | 10 +- litebox_shim_windows/src/lib.rs | 62 +- litebox_shim_windows/src/loader/pe.rs | 873 ++++++++++++++++++- litebox_shim_windows/src/nt_types.rs | 604 +++++++++++++ litebox_shim_windows/src/tests.rs | 2 + 6 files changed, 1569 insertions(+), 25 deletions(-) diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs index 0215adc50d..af8a308df8 100644 --- a/litebox_common_windows/src/loader.rs +++ b/litebox_common_windows/src/loader.rs @@ -7,6 +7,7 @@ use alloc::{string::String, vec::Vec}; use core::cmp; use core::mem::size_of; +use object::read::pe::ImageOptionalHeader as _; use zerocopy::{FromBytes, Immutable, IntoBytes}; use object::endian::LittleEndian as LE; @@ -19,6 +20,7 @@ pub const PAGE_SIZE: usize = 4096; /// Maximum supported section count. PE limit per spec is 96. const MAX_SECTIONS: usize = 96; +const IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE: u16 = 0x0040; /// The result of parsing a PE32+ file. #[derive(Debug)] @@ -30,7 +32,7 @@ pub struct PeParsedFile { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct PeImageInfo { +pub struct PeImageInfo { machine: u16, characteristics: u16, image_base: usize, @@ -40,9 +42,11 @@ struct PeImageInfo { section_alignment: usize, file_alignment: usize, subsystem: u16, + /// Major subsystem version from the PE optional header. + major_subsystem_version: u16, + /// Minor subsystem version from the PE optional header. + minor_subsystem_version: u16, dll_characteristics: u16, - size_of_heap_reserve: usize, - size_of_heap_commit: usize, } /// Information about the mapped PE image. @@ -214,6 +218,35 @@ impl PeParsedFile { self.image.size_of_image } + /// Returns the preferred image base from the optional header. + #[must_use] + pub fn image_base(&self) -> usize { + self.image.image_base + } + + /// Returns whether the image opts into dynamic-base loading. + #[must_use] + pub fn has_dynamic_base(&self) -> bool { + self.image.dll_characteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE != 0 + } + + #[must_use] + pub fn subsystem(&self) -> u16 { + self.image.subsystem + } + + /// Returns the major subsystem version. + #[must_use] + pub fn major_subsystem_version(&self) -> u16 { + self.image.major_subsystem_version + } + + /// Returns the minor subsystem version. + #[must_use] + pub fn minor_subsystem_version(&self) -> u16 { + self.image.minor_subsystem_version + } + /// Returns the exception directory, if present. #[must_use] pub fn exception_directory(&self) -> Option { @@ -838,9 +871,9 @@ fn parse_headers( section_alignment: opt.section_alignment.get(LE) as usize, file_alignment: opt.file_alignment.get(LE) as usize, subsystem: opt.subsystem.get(LE), + major_subsystem_version: opt.major_subsystem_version(), + minor_subsystem_version: opt.minor_subsystem_version(), dll_characteristics: opt.dll_characteristics.get(LE), - size_of_heap_reserve: usize_from_u64(opt.size_of_heap_reserve.get(LE))?, - size_of_heap_commit: usize_from_u64(opt.size_of_heap_commit.get(LE))?, }; if image.size_of_headers > file_size { return Err(PeParseError::UnsupportedImage); diff --git a/litebox_runner_windows_userland/tests/run.rs b/litebox_runner_windows_userland/tests/run.rs index b0f8a74ba6..d79f2b1018 100644 --- a/litebox_runner_windows_userland/tests/run.rs +++ b/litebox_runner_windows_userland/tests/run.rs @@ -42,13 +42,17 @@ fn loads_minimal_pe_without_imports() { let output = command .output() .expect("failed to run litebox_runner_windows_userland"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let reached_unsupported_syscall = stdout.contains("Unsupported Windows syscall") + || stderr.contains("Unsupported Windows syscall"); assert!( - output.status.success(), + output.status.success() || reached_unsupported_syscall, "runner failed to load no-import PE; status {:?}\nstdout:\n{}\nstderr:\n{}", output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) + stdout, + stderr ); } diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 49e3845a1b..4acda0ffdb 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -20,8 +20,9 @@ use litebox_common_windows::nt_status::NtStatus; use litebox::LiteBox; use litebox::mm::PageManager; use litebox::platform::{ - CrngProvider, PageManagementProvider, RawConstPointer as _, RawMutPointer as _, - RawPointerProvider, StdioProvider, SystemInfoProvider, + CrngProvider, PageManagementProvider, PunchthroughProvider, PunchthroughToken, + RawConstPointer as _, RawMutPointer as _, RawPointerProvider, StdioProvider, + SystemInfoProvider, }; use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; use litebox::sync::RawSyncPrimitivesProvider; @@ -109,6 +110,28 @@ where Some(()) } +fn set_guest_teb(platform: &Platform, teb_address: usize) -> bool +where + Platform: PunchthroughProvider + RawPointerProvider, + ::PunchthroughToken<'static>: PunchthroughToken< + Punchthrough = litebox_common_linux::PunchthroughSyscall<'static, Platform>, + >, +{ + let punchthrough: litebox_common_linux::PunchthroughSyscall<'static, Platform> = + litebox_common_linux::PunchthroughSyscall::SetFsBase { addr: teb_address }; + let Some(token) = platform.get_punchthrough_token_for(punchthrough) else { + litebox_util_log::warn!(teb:% = format_args!("{teb_address:#x}"); "Failed to get punchthrough token for Windows TEB base"); + return false; + }; + + if let Err(error) = token.execute() { + litebox_util_log::warn!(error:? = error, teb:% = format_args!("{teb_address:#x}"); "Failed to set Windows TEB base"); + return false; + } + + true +} + pub(crate) fn insert_raw_handle( litebox: &LiteBox, handles: &WindowsHandleStore, @@ -266,6 +289,8 @@ impl WindowsShim { fs, entry_point: load_info.entry_point, stack_top: load_info.stack_top, + teb_address: load_info.environment.teb, + context: load_info.environment.context, }, _not_send: PhantomData, }, @@ -308,19 +333,27 @@ struct Task { fs: Arc, entry_point: usize, stack_top: usize, + context: usize, + teb_address: usize, } impl Task { - fn init(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { + fn init(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation + where + Platform: PunchthroughProvider, + ::PunchthroughToken<'static>: PunchthroughToken< + Punchthrough = litebox_common_linux::PunchthroughSyscall<'static, Platform>, + >, + { + if !set_guest_teb(self.global.platform, self.teb_address) { + return ContinueOperation::Terminate; + } + ctx.rip = self.entry_point; - let stack_top_alignment = self.stack_top % 16; - debug_assert!(stack_top_alignment == 0 || stack_top_alignment == 8); - ctx.rsp = if stack_top_alignment == 0 { - self.stack_top - core::mem::size_of::() - } else { - self.stack_top - }; + debug_assert!(self.stack_top % 16 == core::mem::size_of::()); + ctx.rsp = self.stack_top; ctx.eflags = 0x202; + ctx.rcx = self.context; ctx.rdx = self .process .ntdll_mapping @@ -535,7 +568,14 @@ pub struct WindowsShimEntrypoints { _not_send: PhantomData<*const ()>, } -impl EnterShim for WindowsShimEntrypoints { +impl EnterShim for WindowsShimEntrypoints +where + Platform: ShimPlatform + PunchthroughProvider, + ::PunchthroughToken<'static>: PunchthroughToken< + Punchthrough = litebox_common_linux::PunchthroughSyscall<'static, Platform>, + >, + FS: ShimFS, +{ type ExecutionContext = litebox_common_linux::PtRegs; fn init(&self, ctx: &mut Self::ExecutionContext) -> ContinueOperation { diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index cda8dec17f..5869d1cc19 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use alloc::{sync::Arc, vec::Vec}; +use alloc::{string::String, sync::Arc, vec::Vec}; use core::marker::PhantomData; use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt as _; @@ -18,8 +18,13 @@ use litebox_common_windows::loader::{ PeLoadError, PeParseError, PeParsedFile, Protection, ReadAt, page_align_down, }; use thiserror::Error; +use zerocopy::{FromZeros, IntoBytes}; use crate::ShimFS; +use crate::nt_types::{ + ClientId, PebBitField, ProcessEnvironmentBlock, RtlUserProcFlags, RtlUserProcessParameters, + ThreadEnvironmentBlock, UnicodeString, X64Context, +}; const NTDLL_WRITABLE_SECTIONS: &[&[u8]] = &[b".mrdata"]; const NTDLL_PATHS: &[&str] = &["/Windows/System32/ntdll.dll", "/windows/system32/ntdll.dll"]; @@ -27,11 +32,37 @@ const RUNTIME_FUNCTION_ENTRY_SIZE: usize = 12; const ZERO_CHUNK: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; const FILE_CHUNK_BYTES: usize = 64 * 1024; const INITIAL_STACK_SIZE: usize = 1024 * 1024; +const WINDOWS_SHARED_SECTION_SIZE: usize = 0x1_0000; +const CSR_SERVER_DLL_MAX: usize = 4; +const BASESRV_SERVERDLL_INDEX: usize = 1; +// TODO: this is an artificial offset and should be replaced with the actual offset +const WINDOWS_STATIC_SERVER_DATA_TABLE_OFFSET: usize = 0x750; +const WINDOWS_BASE_STATIC_SERVER_DATA_OFFSET: usize = + WINDOWS_STATIC_SERVER_DATA_TABLE_OFFSET + CSR_SERVER_DLL_MAX * core::mem::size_of::(); +const WINDOWS_OS_MAJOR_VERSION: u32 = 10; +const WINDOWS_OS_MINOR_VERSION: u32 = 0; +const WINDOWS_OS_BUILD_NUMBER: u16 = 19041; +const WINDOWS_OS_PLATFORM_WIN32_NT: u32 = 2; +const WINDOWS_CRITICAL_SECTION_TIMEOUT_100NS: i64 = -150 * 10_000_000; +const WINDOWS_HEAP_SEGMENT_RESERVE: u64 = 1024 * 1024; +const WINDOWS_HEAP_SEGMENT_COMMIT: u64 = 2 * PAGE_SIZE as u64; +const WINDOWS_HEAP_DECOMMIT_TOTAL_FREE_THRESHOLD: u64 = 64 * 1024; +const WINDOWS_HEAP_DECOMMIT_FREE_BLOCK_THRESHOLD: u64 = PAGE_SIZE as u64; +const WINDOWS_NT_TIB_VERSION: usize = 30 << 8; +const INITIAL_PROCESS_ID: usize = 1; +const INITIAL_THREAD_ID: usize = 1; + +pub(crate) struct WindowsProcessEnvironment { + pub(crate) peb: usize, + pub(crate) teb: usize, + pub(crate) context: usize, +} pub(crate) struct PeLoadInfo { pub(crate) entry_point: usize, pub(crate) stack_top: usize, pub(crate) ntdll_mapping: Option, + pub(crate) environment: WindowsProcessEnvironment, } pub(crate) struct PeLoader<'a, Platform: crate::ShimPlatform, FS: ShimFS> { @@ -63,12 +94,15 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { NTDLL_PATHS, )?; - if let Some(ntdll) = &ntdll { + let entry_point = if let Some(ntdll) = &ntdll { if !ntdll.image.parsed.has_trampoline() { return Err(WindowsLoadError::UnrewrittenNtDll); } Self::initialize_ki_user_inverted_function_table(&image, ntdll)?; - } + ntdll.exports.ldr_initialize_thunk + } else { + application_entry_point + }; let length = NonZeroPageSize::new(INITIAL_STACK_SIZE).ok_or(PeImageAccessError::AddressOverflow)?; @@ -90,10 +124,29 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { stack_top }; + let environment = self.create_process_environment( + &image.parsed, + image.mapping.base_addr, + path, + stack_base.as_usize(), + stack_top, + )?; + if let Some(ntdll) = &ntdll { + let context = X64Context::initial_thread_context( + ntdll.exports.rtl_user_thread_start, + application_entry_point, + stack_top, + environment.peb, + ); + crate::write_slice::(environment.context, context.as_bytes()) + .ok_or(PeImageAccessError::MemoryAccess)?; + } + Ok(PeLoadInfo { - entry_point: application_entry_point, + entry_point, stack_top, ntdll_mapping: ntdll.map(|ntdll| ntdll.image.mapping), + environment, }) } @@ -134,6 +187,171 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { Ok(()) } + + fn create_process_environment( + &self, + image: &PeParsedFile, + image_base_address: usize, + image_path: &str, + stack_base: usize, + stack_top: usize, + ) -> 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)?; + let ptr = unsafe { + self.page_manager.create_writable_pages( + None, + length, + CreatePagesFlags::empty(), + |_| Ok(0), + ) + }?; + let base = ptr.as_usize(); + Ok(base) + }; + let teb_ptr = create_pages(core::mem::size_of::())?; + let peb_ptr = create_pages(core::mem::size_of::())?; + let ctx_ptr = create_pages(core::mem::size_of::())?; + + let dos_image_path = dos_image_path(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(&dos_image_path)?; + 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 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( + core::mem::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 = u32::try_from(process_parameters_allocation_length) + .map_err(|_| PeImageAccessError::AddressOverflow)?; + process_parameters.length = u32::try_from(process_parameters_length) + .map_err(|_| PeImageAccessError::AddressOverflow)?; + process_parameters.flags = RtlUserProcFlags::NORMALIZED.bits(); + let mut process_parameter_tail = process_parameters_ptr + .checked_add(core::mem::size_of::()) + .ok_or(PeImageAccessError::AddressOverflow)?; + process_parameters.current_directory.dos_path = write_process_parameter_string::( + &mut process_parameter_tail, + ¤t_directory_path, + )?; + process_parameters.dll_path = + write_process_parameter_string::(&mut process_parameter_tail, &dll_path)?; + process_parameters.image_path_name = write_process_parameter_string::( + &mut process_parameter_tail, + &image_path_name, + )?; + process_parameters.command_line = + write_process_parameter_string::(&mut process_parameter_tail, &command_line)?; + process_parameters.window_title = + write_process_parameter_string::(&mut process_parameter_tail, &window_title)?; + process_parameters.desktop_info = + write_process_parameter_string::(&mut process_parameter_tail, &desktop_info)?; + process_parameters.shell_info = + write_process_parameter_string::(&mut process_parameter_tail, &shell_info)?; + process_parameters.runtime_data = + write_process_parameter_string::(&mut process_parameter_tail, &runtime_data)?; + process_parameters.redirection_dll_name = write_process_parameter_string::( + &mut process_parameter_tail, + &redirection_dll_name, + )?; + crate::write_value::(process_parameters_ptr, process_parameters) + .ok_or(PeImageAccessError::MemoryAccess)?; + + let read_only_shared_memory_base = create_pages(WINDOWS_SHARED_SECTION_SIZE)?; + let read_only_static_server_data = read_only_shared_memory_base + .checked_add(WINDOWS_STATIC_SERVER_DATA_TABLE_OFFSET) + .ok_or(PeImageAccessError::AddressOverflow)?; + let base_static_server_data = read_only_shared_memory_base + .checked_add(WINDOWS_BASE_STATIC_SERVER_DATA_OFFSET) + .ok_or(PeImageAccessError::AddressOverflow)?; + let base_static_server_data_entry = read_only_static_server_data + .checked_add(BASESRV_SERVERDLL_INDEX * core::mem::size_of::()) + .ok_or(PeImageAccessError::AddressOverflow)?; + crate::write_value::(base_static_server_data_entry, base_static_server_data) + .ok_or(PeImageAccessError::MemoryAccess)?; + + let mut peb = ProcessEnvironmentBlock::new_zeroed(); + peb.image_base_address = image_base_address; + if image_base_address != image.image_base() || image.has_dynamic_base() { + peb.bit_field = PebBitField::IS_IMAGE_DYNAMICALLY_RELOCATED.bits(); + } + let process_heaps = initial_process_heaps_array(peb_ptr)?; + peb.process_parameters = process_parameters_ptr; + 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.active_process_affinity_mask = 1; + peb.os_major_version = WINDOWS_OS_MAJOR_VERSION; + peb.os_minor_version = WINDOWS_OS_MINOR_VERSION; + peb.os_build_number = WINDOWS_OS_BUILD_NUMBER; + peb.os_platform_id = WINDOWS_OS_PLATFORM_WIN32_NT; + peb.image_subsystem = u32::from(image.subsystem()); + peb.image_subsystem_major_version = u32::from(image.major_subsystem_version()); + peb.image_subsystem_minor_version = u32::from(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; + peb.csr_server_read_only_shared_memory_base = read_only_shared_memory_base as u64; + crate::write_value::(peb_ptr, peb).ok_or(PeImageAccessError::MemoryAccess)?; + + let mut teb = ThreadEnvironmentBlock::new_zeroed(); + teb.nt_tib.exception_list = 0; + teb.nt_tib.stack_base = stack_top; + teb.nt_tib.stack_limit = stack_base; + teb.nt_tib.fiber_data_or_version = WINDOWS_NT_TIB_VERSION; + teb.nt_tib.self_pointer = teb_ptr; + // TODO: set real ID + teb.client_id = ClientId { + unique_process: INITIAL_PROCESS_ID, + unique_thread: INITIAL_THREAD_ID, + }; + teb.thread_local_storage_pointer = + teb_ptr + core::mem::offset_of!(ThreadEnvironmentBlock, tls_slots); + teb.process_environment_block = peb_ptr; + teb.real_client_id = teb.client_id; + teb.activation_context_stack_pointer = + teb_ptr + core::mem::offset_of!(ThreadEnvironmentBlock, activation_stack); + teb.static_unicode_string = + initial_teb_static_unicode_string(teb_ptr, &teb.static_unicode_buffer)?; + teb.deallocation_stack = stack_base; + crate::write_value::(teb_ptr, teb).ok_or(PeImageAccessError::MemoryAccess)?; + Ok(WindowsProcessEnvironment { + peb: peb_ptr, + teb: teb_ptr, + context: ctx_ptr, + }) + } } struct LoadedImage { @@ -180,6 +398,11 @@ struct LoadedNtDll { #[derive(Clone, Copy, Debug)] struct NtDllExports { + /// `LdrInitializeThunk` + ldr_initialize_thunk: usize, + /// `RtlUserThreadStart` + rtl_user_thread_start: usize, + /// `KiUserInvertedFunctionTable` ki_user_inverted_function_table: usize, } @@ -265,12 +488,16 @@ fn ntdll_exports( .try_into() .map_err(|_| WindowsLoadError::MissingNtDllInvertedFunctionTable)?; - ldr_initialize_thunk.ok_or(WindowsLoadError::MissingNtDllLoaderEntrypoint)?; - rtl_user_thread_start.ok_or(WindowsLoadError::MissingNtDllThreadEntrypoint)?; + let ldr_initialize_thunk = + ldr_initialize_thunk.ok_or(WindowsLoadError::MissingNtDllLoaderEntrypoint)?; + let rtl_user_thread_start = + rtl_user_thread_start.ok_or(WindowsLoadError::MissingNtDllThreadEntrypoint)?; let ki_user_inverted_function_table = ki_user_inverted_function_table .ok_or(WindowsLoadError::MissingNtDllInvertedFunctionTable)?; Ok(NtDllExports { + ldr_initialize_thunk, + rtl_user_thread_start, ki_user_inverted_function_table, }) } @@ -563,17 +790,117 @@ fn page_range(address: usize, len: usize) -> Result<(usize, usize), PeImageAcces Ok((start, end - start)) } +fn dos_image_path(path: &str) -> String { + let mut dos_path = String::from(r"\??\C:"); + if !path.starts_with('/') && !path.starts_with('\\') { + dos_path.push('\\'); + } + for ch in path.chars() { + dos_path.push(if ch == '/' { '\\' } else { ch }); + } + dos_path +} + +fn write_process_parameter_string( + process_parameter_tail: &mut usize, + string: &Utf16StringBuffer, +) -> Result { + let buffer = *process_parameter_tail; + crate::write_slice::(buffer, &string.units) + .ok_or(PeImageAccessError::MemoryAccess)?; + *process_parameter_tail = (*process_parameter_tail) + .checked_add(usize::from(string.maximum_length)) + .ok_or(PeImageAccessError::AddressOverflow)?; + Ok(UnicodeString { + length: string.length, + maximum_length: string.maximum_length, + padding_0: [0; 4], + buffer, + }) +} + +struct InitialProcessHeaps { + address: usize, + maximum_number_of_heaps: u32, +} + +fn initial_process_heaps_array(peb_ptr: usize) -> Result { + let peb_size = core::mem::size_of::(); + let address = peb_ptr + .checked_add(peb_size) + .ok_or(PeImageAccessError::AddressOverflow)?; + let maximum_number_of_heaps = + (peb_size.next_multiple_of(PAGE_SIZE) - peb_size) / core::mem::size_of::(); + Ok(InitialProcessHeaps { + address, + maximum_number_of_heaps: maximum_number_of_heaps.trunc(), + }) +} + +fn initial_teb_static_unicode_string( + teb_ptr: usize, + static_unicode_buffer: &[u16], +) -> Result { + let buffer = teb_ptr + .checked_add(core::mem::offset_of!( + ThreadEnvironmentBlock, + static_unicode_buffer + )) + .ok_or(PeImageAccessError::AddressOverflow)?; + Ok(UnicodeString { + length: 0, + maximum_length: u16::try_from(core::mem::size_of_val(static_unicode_buffer)) + .map_err(|_| PeImageAccessError::AddressOverflow)?, + padding_0: [0; 4], + buffer, + }) +} + +struct Utf16StringBuffer { + length: u16, + maximum_length: u16, + units: Vec, +} + +impl Utf16StringBuffer { + fn new(value: &str) -> Result { + let mut units: Vec = value.encode_utf16().collect(); + let length = utf16_byte_len(units.len())?; + units.push(0); + let maximum_length = utf16_byte_len(units.len())?; + Ok(Self { + length, + maximum_length, + units, + }) + } +} + +fn utf16_byte_len(units: usize) -> Result { + units + .checked_mul(core::mem::size_of::()) + .and_then(|bytes| u16::try_from(bytes).ok()) + .ok_or(PeImageAccessError::AddressOverflow) +} + #[cfg(all(test, target_os = "windows", target_arch = "x86_64"))] mod tests { extern crate std; use alloc::{string::String, vec, vec::Vec}; + use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use super::*; + use crate::nt_types::{ProcessEnvironmentBlock, ThreadEnvironmentBlock, UnicodeString}; + + const TEST_STACK_BASE: usize = 0x7000_0000; + const TEST_STACK_TOP: usize = TEST_STACK_BASE + 0x100000; #[allow(non_snake_case)] #[link(name = "kernel32")] unsafe extern "system" { + fn GetCurrentProcessId() -> u32; + fn GetCurrentThreadId() -> u32; fn GetModuleHandleW(lp_module_name: *const u16) -> *mut core::ffi::c_void; fn GetProcAddress( h_module: *mut core::ffi::c_void, @@ -586,6 +913,335 @@ mod tests { ) -> u32; } + macro_rules! print_diff_fields { + ($prefix:literal, $synthetic:expr, $host:expr, [$($field:ident),+ $(,)?]) => { + $( + print_diff_field!($prefix, $synthetic, $host, $field); + )+ + }; + } + + macro_rules! print_diff_field { + ($prefix:literal, $synthetic:expr, $host:expr, csd_version) => { + print_unicode_string_diff( + concat!($prefix, ".", stringify!(csd_version)), + ($synthetic).csd_version, + ($host).csd_version, + ); + }; + ($prefix:literal, $synthetic:expr, $host:expr, static_unicode_string) => { + print_unicode_string_diff( + concat!($prefix, ".", stringify!(static_unicode_string)), + ($synthetic).static_unicode_string, + ($host).static_unicode_string, + ); + }; + ($prefix:literal, $synthetic:expr, $host:expr, $field:ident) => { + print_field_diff( + concat!($prefix, ".", stringify!($field)), + ($synthetic).$field, + ($host).$field, + ); + }; + } + + #[allow(clippy::similar_names)] + #[test] + fn prints_created_teb_host_diff() { + let created = created_process_environment_snapshot(); + let host_teb = host_teb_snapshot(); + let host_teb_address = host_teb_address(); + let host_peb_address = host_peb_address(); + + assert_eq!(created.teb.nt_tib.self_pointer, created.environment.teb); + assert_eq!(host_teb.nt_tib.self_pointer, host_teb_address); + assert_eq!( + created.teb.process_environment_block, + created.environment.peb + ); + assert_eq!(host_teb.process_environment_block, host_peb_address); + assert_eq!(host_teb.client_id, host_client_id()); + + print_diff_header("synthetic TEB vs host TEB"); + print_diff_fields!( + "TEB.NtTib", + created.teb.nt_tib, + host_teb.nt_tib, + [ + exception_list, + stack_base, + stack_limit, + sub_system_tib, + fiber_data_or_version, + arbitrary_user_pointer, + self_pointer, + ] + ); + print_diff_fields!( + "TEB", + created.teb, + host_teb, + [ + environment_pointer, + client_id, + active_rpc_handle, + thread_local_storage_pointer, + process_environment_block, + last_error_value, + count_of_owned_critical_sections, + csr_client_thread, + win_32_thread_info, + user_32_reserved, + user_reserved, + padding_user_reserved, + wow_32_reserved, + current_locale, + fp_software_status_register, + reserved_for_debugger_instrumentation, + system_reserved_1, + heap_fls_data, + rng_state, + placeholder_compatibility_mode, + placeholder_hydration_always_explicit, + placeholder_reserved, + proxied_process_id, + activation_stack, + working_on_behalf_ticket, + exception_code, + padding_0, + activation_context_stack_pointer, + instrumentation_callback_sp, + instrumentation_callback_previous_pc, + instrumentation_callback_previous_sp, + tx_fs_context, + instrumentation_callback_disabled, + unaligned_load_store_exceptions, + padding_1, + gdi_teb_batch, + real_client_id, + gdi_cached_process_handle, + gdi_client_pid, + gdi_client_tid, + gdi_thread_local_info, + win_32_client_info, + gl_dispatch_table, + gl_reserved_1, + gl_reserved_2, + gl_section_info, + gl_section, + gl_table, + gl_current_rc, + gl_context, + last_status_value, + padding_2, + static_unicode_string, + static_unicode_buffer, + padding_3, + deallocation_stack, + tls_slots, + tls_links, + vdm, + reserved_for_nt_rpc, + dbg_ss_reserved, + hard_error_mode, + padding_4, + instrumentation, + activity_id, + sub_process_tag, + perflib_data, + etw_trace_data, + win_sock_data, + gdi_batch_count, + ideal_processor_value, + guaranteed_stack_bytes, + padding_5, + reserved_for_perf, + reserved_for_ole, + waiting_on_loader_lock, + padding_6, + saved_priority_state, + reserved_for_code_coverage, + thread_pool_data, + tls_expansion_slots, + chpe_v_2_cpu_area_info, + unused, + mui_generation, + is_impersonating, + nls_cache, + p_shim_data, + heap_data, + padding_7, + current_transaction_handle, + active_frame, + fls_data, + preferred_languages, + user_pref_languages, + merged_pref_languages, + mui_impersonation, + cross_teb_flags, + same_teb_flags, + txn_scope_enter_callback, + txn_scope_exit_callback, + txn_scope_context, + lock_count, + wow_teb_offset, + resource_ret_value, + reserved_for_wdf, + reserved_for_crt, + effective_container_id, + last_sleep_counter, + spin_call_count, + padding_8, + extended_feature_disable_mask, + scheduler_shared_data_slot, + heap_walk_context, + primary_group_affinity, + rcu, + ] + ); + } + + #[test] + fn prints_created_peb_host_diff() { + let created = created_process_environment_snapshot(); + let host_peb = host_peb_snapshot(); + let base_static_server_data: usize = read_guest_value( + created.peb.read_only_static_server_data + + BASESRV_SERVERDLL_INDEX * core::mem::size_of::(), + ); + + assert_eq!(created.peb.image_base_address, created.image_base_address); + assert_eq!( + created.peb.read_only_static_server_data, + created.peb.read_only_shared_memory_base + WINDOWS_STATIC_SERVER_DATA_TABLE_OFFSET + ); + assert_eq!( + base_static_server_data, + created.peb.read_only_shared_memory_base + WINDOWS_BASE_STATIC_SERVER_DATA_OFFSET + ); + assert_ne!(host_peb.image_base_address, 0); + + print_diff_header("synthetic PEB vs host PEB"); + print_diff_fields!( + "PEB", + created.peb, + host_peb, + [ + inherited_address_space, + read_image_file_exec_options, + being_debugged, + ] + ); + print_peb_bit_field_diff( + "PEB.bit_field", + crate::nt_types::PebBitField::from_bits_retain(created.peb.bit_field), + crate::nt_types::PebBitField::from_bits_retain(host_peb.bit_field), + ); + print_diff_fields!( + "PEB", + created.peb, + host_peb, + [ + padding_0, + mutant, + image_base_address, + ldr, + process_parameters, + sub_system_data, + process_heap, + fast_peb_lock, + atl_thunk_s_list_ptr, + ifeo_key, + cross_process_flags, + padding_1, + kernel_callback_table, + system_reserved, + atl_thunk_s_list_ptr_32, + api_set_map, + tls_expansion_counter, + padding_2, + tls_bitmap, + tls_bitmap_bits, + read_only_shared_memory_base, + shared_data, + read_only_static_server_data, + ansi_code_page_data, + oem_code_page_data, + unicode_case_table_data, + number_of_processors, + nt_global_flag, + critical_section_timeout, + heap_segment_reserve, + heap_segment_commit, + heap_de_commit_total_free_threshold, + heap_de_commit_free_block_threshold, + number_of_heaps, + maximum_number_of_heaps, + process_heaps, + gdi_shared_handle_table, + process_starter_helper, + gdi_dc_attribute_list, + padding_3, + loader_lock, + os_major_version, + os_minor_version, + os_build_number, + os_csd_version, + os_platform_id, + image_subsystem, + image_subsystem_major_version, + image_subsystem_minor_version, + padding_4, + active_process_affinity_mask, + gdi_handle_buffer, + post_process_init_routine, + tls_expansion_bitmap, + tls_expansion_bitmap_bits, + session_id, + padding_5, + app_compat_flags, + app_compat_flags_user, + p_shim_data, + app_compat_info, + csd_version, + activation_context_data, + process_assembly_storage_map, + system_default_activation_context_data, + system_assembly_storage_map, + minimum_stack_commit, + spare_pointers, + patch_loader_data, + chpe_v2_process_info, + app_model_feature_state, + spare_ulongs, + active_code_page, + oem_code_page, + use_case_mapping, + unused_nls_field, + padding_6a, + wer_registration_data, + wer_ship_assert_ptr, + ec_code_bit_map, + p_image_header_hash, + tracing_flags, + padding_6, + csr_server_read_only_shared_memory_base, + tpp_workerp_list_lock, + tpp_workerp_list, + wait_on_address_hash_table, + telemetry_coverage_header, + cloud_file_flags, + cloud_file_diag_flags, + placeholder_compatibility_mode, + placeholder_compatibility_mode_reserved, + leap_second_data, + leap_second_flags, + nt_global_flag_2, + extended_feature_disable_mask, + ] + ); + } + #[test] fn ntdll_exports_finds_ki_user_inverted_function_table() { let ntdll = ntdll_module_base(); @@ -653,6 +1309,211 @@ mod tests { ); } + struct CreatedProcessEnvironmentSnapshot { + environment: WindowsProcessEnvironment, + peb: ProcessEnvironmentBlock, + teb: ThreadEnvironmentBlock, + image_base_address: usize, + } + + fn created_process_environment_snapshot() -> CreatedProcessEnvironmentSnapshot { + let platform = crate::tests::test_platform(); + let litebox = litebox::LiteBox::new(platform); + let page_manager = crate::WindowsPageManager::::new(&litebox); + let fs = Arc::new(litebox::fs::in_mem::FileSystem::new(&litebox)); + let loader = PeLoader::new(platform, fs, &page_manager); + let image = loaded_module_image(application_module_base()); + + let image_base_address = image.mapping.base_addr; + let environment = loader + .create_process_environment( + &image.parsed, + image_base_address, + "test.exe", + TEST_STACK_BASE, + TEST_STACK_TOP, + ) + .expect("failed to create synthetic Windows process environment"); + + CreatedProcessEnvironmentSnapshot { + peb: read_guest_value(environment.peb), + teb: read_guest_value(environment.teb), + environment, + image_base_address, + } + } + + fn print_field_diff(field: &str, synthetic: T, host: T) + where + T: core::fmt::Debug + IntoBytes + zerocopy::Immutable, + { + let status = if synthetic.as_bytes() == host.as_bytes() { + "✓" + } else { + "X" + }; + let synthetic = format_field_value(&synthetic); + let host = format_field_value(&host); + std::println!("{status:<2} {field:<48} {synthetic} | {host}"); + } + + fn print_peb_bit_field_diff( + field: &str, + synthetic: crate::nt_types::PebBitField, + host: crate::nt_types::PebBitField, + ) { + let status = if synthetic.bits() == host.bits() { + "✓" + } else { + "X" + }; + let synthetic = format_field_value(&synthetic); + let host = format_field_value(&host); + std::println!("{status:<2} {field:<48} {synthetic} | {host}"); + } + + fn print_unicode_string_diff(field: &str, synthetic: UnicodeString, host: UnicodeString) { + let synthetic = decode_guest_unicode_string(synthetic); + let host = decode_host_unicode_string(host); + let status = if synthetic == host { "✓" } else { "X" }; + let synthetic = format_field_value(&synthetic); + let host = format_field_value(&host); + std::println!("{status:<2} {field:<48} {synthetic} | {host}"); + } + + fn decode_guest_unicode_string(value: UnicodeString) -> String { + let Some(chars) = unicode_string_chars(value) else { + return std::format!("", value.length); + }; + if chars == 0 { + return String::new(); + } + if value.buffer == 0 { + return String::from(""); + } + + let ptr = + ::RawConstPointer::::from_usize( + value.buffer, + ); + let Some(units) = ptr.to_owned_slice(chars) else { + return String::from(""); + }; + String::from_utf16_lossy(&units) + } + + fn decode_host_unicode_string(value: UnicodeString) -> String { + let Some(chars) = unicode_string_chars(value) else { + return std::format!("", value.length); + }; + if chars == 0 { + return String::new(); + } + if value.buffer == 0 { + return String::from(""); + } + + // SAFETY: Host PEB/TEB snapshots contain pointers owned by the current + // process; the `UNICODE_STRING.Length` field bounds the UTF-16 slice. + let units = unsafe { core::slice::from_raw_parts(value.buffer as *const u16, chars) }; + String::from_utf16_lossy(units) + } + + fn unicode_string_chars(value: UnicodeString) -> Option { + if value.length.is_multiple_of(2) { + Some(usize::from(value.length / 2)) + } else { + None + } + } + + fn format_field_value(value: &T) -> String { + const MAX_VALUE_LEN: usize = 96; + + let mut value = std::format!("{value:x?}"); + if value.len() <= MAX_VALUE_LEN { + return value; + } + + let mut end = MAX_VALUE_LEN; + while !value.is_char_boundary(end) { + end -= 1; + } + value.truncate(end); + value.push_str("..."); + value + } + + fn print_diff_header(title: &str) { + std::println!("{title}"); + std::println!(" {:<48} synthetic | host", "field"); + std::println!(" {:<48} ----------------", "-----"); + } + + fn read_guest_value(address: usize) -> T + where + T: Copy + zerocopy::FromBytes, + { + let ptr = + ::RawConstPointer::::from_usize( + address, + ); + ptr.read_at_offset(0) + .expect("failed to read synthetic guest process environment value") + } + + fn host_teb_snapshot() -> ThreadEnvironmentBlock { + // SAFETY: `host_teb_address` returns the current thread's live host TEB pointer. + unsafe { read_host_value(host_teb_address() as *const ThreadEnvironmentBlock) } + } + + fn host_peb_snapshot() -> ProcessEnvironmentBlock { + // SAFETY: `host_peb_address` returns the current process's live host PEB pointer. + unsafe { read_host_value(host_peb_address() as *const ProcessEnvironmentBlock) } + } + + fn host_teb_address() -> usize { + let teb: usize; + // SAFETY: On x86_64 Windows, GS:[0x30] is the current thread's TEB pointer. + unsafe { + core::arch::asm!( + "mov {}, gs:[0x30]", + out(reg) teb, + options(nostack, preserves_flags, readonly), + ); + } + teb + } + + fn host_peb_address() -> usize { + let peb: usize; + // SAFETY: On x86_64 Windows, GS:[0x60] is the current process's PEB pointer. + unsafe { + core::arch::asm!( + "mov {}, gs:[0x60]", + out(reg) peb, + options(nostack, preserves_flags, readonly), + ); + } + peb + } + + fn host_client_id() -> ClientId { + // SAFETY: These kernel32 calls take no pointers and return IDs for the current process/thread. + let unique_process = unsafe { GetCurrentProcessId() }; + // SAFETY: These kernel32 calls take no pointers and return IDs for the current process/thread. + let unique_thread = unsafe { GetCurrentThreadId() }; + ClientId { + unique_process: usize::try_from(unique_process).unwrap(), + unique_thread: usize::try_from(unique_thread).unwrap(), + } + } + + unsafe fn read_host_value(address: *const T) -> T { + // SAFETY: The caller guarantees `address` points into a live host PEB/TEB object. + unsafe { core::ptr::read_volatile(address) } + } + fn own_inverted_function_table() -> *const u8 { let ntdll = ntdll_module_base(); diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index 78071c3c52..b1b135b0d4 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -9,6 +9,131 @@ use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::{ConstPtr, syscalls::Handle}; +pub const X64_CONTEXT_CONTROL: u32 = 0x0010_0001; +pub const X64_CONTEXT_INTEGER: u32 = 0x0010_0002; +pub const X64_CONTEXT_FLOATING_POINT: u32 = 0x0010_0008; +pub const X64_CONTEXT_DEBUG_REGISTERS: u32 = 0x0010_0010; + +const INITIAL_CONTEXT_MXCSR: u32 = 0x1f80; +const USER_MODE_CODE_SELECTOR: u16 = 0x33; +const USER_MODE_STACK_SELECTOR: u16 = 0x2b; +const INITIAL_CONTEXT_EFLAGS: u32 = 0x200; + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct X64Context { + pub p1_home: u64, + pub p2_home: u64, + pub p3_home: u64, + pub p4_home: u64, + pub p5_home: u64, + pub p6_home: u64, + pub context_flags: u32, + pub mx_csr: u32, + pub seg_cs: u16, + pub seg_ds: u16, + pub seg_es: u16, + pub seg_fs: u16, + pub seg_gs: u16, + pub seg_ss: u16, + pub e_flags: u32, + pub dr0: u64, + pub dr1: u64, + pub dr2: u64, + pub dr3: u64, + pub dr6: u64, + pub dr7: u64, + pub rax: u64, + pub rcx: u64, + pub rdx: u64, + pub rbx: u64, + pub rsp: u64, + pub rbp: u64, + pub rsi: u64, + pub rdi: u64, + pub r8: u64, + pub r9: u64, + pub r10: u64, + pub r11: u64, + pub r12: u64, + pub r13: u64, + pub r14: u64, + pub r15: u64, + pub rip: u64, + pub extended_state: [u8; 0x3d0], +} + +impl Default for X64Context { + fn default() -> Self { + Self { + p1_home: 0, + p2_home: 0, + p3_home: 0, + p4_home: 0, + p5_home: 0, + p6_home: 0, + context_flags: 0, + mx_csr: 0, + seg_cs: 0, + seg_ds: 0, + seg_es: 0, + seg_fs: 0, + seg_gs: 0, + seg_ss: 0, + e_flags: 0, + dr0: 0, + dr1: 0, + dr2: 0, + dr3: 0, + dr6: 0, + dr7: 0, + rax: 0, + rcx: 0, + rdx: 0, + rbx: 0, + rsp: 0, + rbp: 0, + rsi: 0, + rdi: 0, + r8: 0, + r9: 0, + r10: 0, + r11: 0, + r12: 0, + r13: 0, + r14: 0, + r15: 0, + rip: 0, + extended_state: [0; 0x3d0], + } + } +} + +impl X64Context { + pub(crate) fn initial_thread_context( + thread_entry_point: usize, + application_entry_point: usize, + stack_top: usize, + peb: usize, + ) -> X64Context { + X64Context { + context_flags: X64_CONTEXT_CONTROL + | X64_CONTEXT_INTEGER + | X64_CONTEXT_FLOATING_POINT + | X64_CONTEXT_DEBUG_REGISTERS, + mx_csr: INITIAL_CONTEXT_MXCSR, + seg_cs: USER_MODE_CODE_SELECTOR, + seg_ss: USER_MODE_STACK_SELECTOR, + e_flags: INITIAL_CONTEXT_EFLAGS, + rcx: application_entry_point as u64, + rdx: peb as u64, + rsp: stack_top as u64, + rip: thread_entry_point as u64, + ..X64Context::default() + } + } +} + bitflags::bitflags! { /// Common Windows object-manager `ACCESS_MASK` rights shared by NT object types. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -111,3 +236,482 @@ impl UnicodeString { Ok(String::from_utf16_lossy(&units)) } } + +bitflags::bitflags! { + /// Packed process flags stored in `PEB.BitField`. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct PebBitField: u8 { + const IMAGE_USES_LARGE_PAGES = 1 << 0; + const IS_PROTECTED_PROCESS = 1 << 1; + const IS_IMAGE_DYNAMICALLY_RELOCATED = 1 << 2; + const SKIP_PATCHING_USER32_FORWARDERS = 1 << 3; + const IS_PACKAGED_PROCESS = 1 << 4; + const IS_APP_CONTAINER = 1 << 5; + const IS_PROTECTED_PROCESS_LIGHT = 1 << 6; + const IS_LONG_PATH_AWARE_PROCESS = 1 << 7; + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct ProcessEnvironmentBlock { + pub inherited_address_space: u8, + pub read_image_file_exec_options: u8, + pub being_debugged: u8, + /// [`PebBitField`] + pub bit_field: u8, + pub padding_0: [u8; 4], + pub mutant: usize, + pub image_base_address: usize, + pub ldr: usize, + /// Pointer to [`RtlUserProcessParameters`]. + pub process_parameters: usize, + pub sub_system_data: usize, + pub process_heap: usize, + pub fast_peb_lock: usize, + pub atl_thunk_s_list_ptr: usize, + pub ifeo_key: usize, + pub cross_process_flags: u32, + pub padding_1: [u8; 4], + pub kernel_callback_table: usize, + pub system_reserved: u32, + pub atl_thunk_s_list_ptr_32: u32, + pub api_set_map: usize, + pub tls_expansion_counter: u32, + pub padding_2: [u8; 4], + pub tls_bitmap: usize, + pub tls_bitmap_bits: [u32; 2], + pub read_only_shared_memory_base: usize, + pub shared_data: usize, + pub read_only_static_server_data: usize, + pub ansi_code_page_data: usize, + pub oem_code_page_data: usize, + pub unicode_case_table_data: usize, + pub number_of_processors: u32, + pub nt_global_flag: u32, + pub critical_section_timeout: i64, + pub heap_segment_reserve: u64, + pub heap_segment_commit: u64, + pub heap_de_commit_total_free_threshold: u64, + pub heap_de_commit_free_block_threshold: u64, + pub number_of_heaps: u32, + pub maximum_number_of_heaps: u32, + pub process_heaps: usize, + pub gdi_shared_handle_table: usize, + pub process_starter_helper: usize, + pub gdi_dc_attribute_list: u32, + pub padding_3: [u8; 4], + pub loader_lock: usize, + pub os_major_version: u32, + pub os_minor_version: u32, + pub os_build_number: u16, + pub os_csd_version: u16, + pub os_platform_id: u32, + pub image_subsystem: u32, + pub image_subsystem_major_version: u32, + pub image_subsystem_minor_version: u32, + pub padding_4: [u8; 4], + pub active_process_affinity_mask: u64, + pub gdi_handle_buffer: [u32; 60], + pub post_process_init_routine: usize, + pub tls_expansion_bitmap: usize, + pub tls_expansion_bitmap_bits: [u32; 32], + pub session_id: u32, + pub padding_5: [u8; 4], + pub app_compat_flags: u64, + pub app_compat_flags_user: u64, + pub p_shim_data: usize, + pub app_compat_info: usize, + pub csd_version: UnicodeString, + pub activation_context_data: usize, + pub process_assembly_storage_map: usize, + pub system_default_activation_context_data: usize, + pub system_assembly_storage_map: usize, + pub minimum_stack_commit: u64, + pub spare_pointers: [usize; 2], + pub patch_loader_data: usize, + pub chpe_v2_process_info: usize, + pub app_model_feature_state: u32, + pub spare_ulongs: [u32; 2], + pub active_code_page: u16, + pub oem_code_page: u16, + pub use_case_mapping: u16, + pub unused_nls_field: u16, + pub padding_6a: [u8; 4], + pub wer_registration_data: usize, + pub wer_ship_assert_ptr: usize, + pub ec_code_bit_map: usize, + pub p_image_header_hash: usize, + pub tracing_flags: u32, + pub padding_6: [u8; 4], + pub csr_server_read_only_shared_memory_base: u64, + pub tpp_workerp_list_lock: u64, + pub tpp_workerp_list: ListEntry, + pub wait_on_address_hash_table: [usize; 128], + pub telemetry_coverage_header: usize, + pub cloud_file_flags: u32, + pub cloud_file_diag_flags: u32, + pub placeholder_compatibility_mode: i8, + pub placeholder_compatibility_mode_reserved: [i8; 7], + pub leap_second_data: usize, + pub leap_second_flags: u32, + pub nt_global_flag_2: u32, + pub extended_feature_disable_mask: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct NtTib { + pub exception_list: usize, + pub stack_base: usize, + pub stack_limit: usize, + pub sub_system_tib: usize, + pub fiber_data_or_version: usize, + pub arbitrary_user_pointer: usize, + pub self_pointer: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct ActivationContextStack { + _reserved: [u8; 0x28], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct GdiTebBatch { + _reserved: [u8; 0x4e8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, FromBytes, IntoBytes, Immutable)] +pub struct ClientId { + pub unique_process: usize, + pub unique_thread: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct ListEntry { + pub flink: usize, + pub blink: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct Guid { + pub data: [u8; 16], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct GroupAffinity { + pub mask: usize, + pub group: u16, + pub reserved: [u16; 3], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct ThreadEnvironmentBlock { + pub nt_tib: NtTib, + pub environment_pointer: usize, + pub client_id: ClientId, + pub active_rpc_handle: usize, + pub thread_local_storage_pointer: usize, + /// Pointer to [`ProcessEnvironmentBlock`]. + pub process_environment_block: usize, + pub last_error_value: u32, + pub count_of_owned_critical_sections: u32, + pub csr_client_thread: usize, + pub win_32_thread_info: usize, + pub user_32_reserved: [u32; 26], + pub user_reserved: [u32; 5], + pub padding_user_reserved: [u8; 4], + pub wow_32_reserved: usize, + pub current_locale: u32, + pub fp_software_status_register: u32, + pub reserved_for_debugger_instrumentation: [usize; 16], + pub system_reserved_1: [usize; 25], + pub heap_fls_data: usize, + pub rng_state: [u64; 4], + pub placeholder_compatibility_mode: i8, + pub placeholder_hydration_always_explicit: u8, + pub placeholder_reserved: [i8; 10], + pub proxied_process_id: u32, + pub activation_stack: ActivationContextStack, + pub working_on_behalf_ticket: [u8; 8], + pub exception_code: i32, + pub padding_0: [u8; 4], + pub activation_context_stack_pointer: usize, + pub instrumentation_callback_sp: u64, + pub instrumentation_callback_previous_pc: u64, + pub instrumentation_callback_previous_sp: u64, + pub tx_fs_context: u32, + pub instrumentation_callback_disabled: u8, + pub unaligned_load_store_exceptions: u8, + pub padding_1: [u8; 2], + pub gdi_teb_batch: GdiTebBatch, + pub real_client_id: ClientId, + pub gdi_cached_process_handle: usize, + pub gdi_client_pid: u32, + pub gdi_client_tid: u32, + pub gdi_thread_local_info: usize, + pub win_32_client_info: [u64; 62], + pub gl_dispatch_table: [usize; 233], + pub gl_reserved_1: [u64; 29], + pub gl_reserved_2: usize, + pub gl_section_info: usize, + pub gl_section: usize, + pub gl_table: usize, + pub gl_current_rc: usize, + pub gl_context: usize, + pub last_status_value: u32, + pub padding_2: [u8; 4], + pub static_unicode_string: UnicodeString, + pub static_unicode_buffer: [u16; 261], + pub padding_3: [u8; 6], + pub deallocation_stack: usize, + pub tls_slots: [usize; 64], + pub tls_links: ListEntry, + pub vdm: usize, + pub reserved_for_nt_rpc: usize, + pub dbg_ss_reserved: [usize; 2], + pub hard_error_mode: u32, + pub padding_4: [u8; 4], + pub instrumentation: [usize; 11], + pub activity_id: Guid, + pub sub_process_tag: usize, + pub perflib_data: usize, + pub etw_trace_data: usize, + pub win_sock_data: usize, + pub gdi_batch_count: u32, + pub ideal_processor_value: u32, + pub guaranteed_stack_bytes: u32, + pub padding_5: [u8; 4], + pub reserved_for_perf: usize, + pub reserved_for_ole: usize, + pub waiting_on_loader_lock: u32, + pub padding_6: [u8; 4], + pub saved_priority_state: usize, + pub reserved_for_code_coverage: u64, + pub thread_pool_data: usize, + pub tls_expansion_slots: usize, + pub chpe_v_2_cpu_area_info: usize, + pub unused: usize, + pub mui_generation: u32, + pub is_impersonating: u32, + pub nls_cache: usize, + pub p_shim_data: usize, + pub heap_data: u32, + pub padding_7: [u8; 4], + pub current_transaction_handle: usize, + pub active_frame: usize, + pub fls_data: usize, + pub preferred_languages: usize, + pub user_pref_languages: usize, + pub merged_pref_languages: usize, + pub mui_impersonation: u32, + pub cross_teb_flags: u16, + pub same_teb_flags: u16, + pub txn_scope_enter_callback: usize, + pub txn_scope_exit_callback: usize, + pub txn_scope_context: usize, + pub lock_count: u32, + pub wow_teb_offset: i32, + pub resource_ret_value: usize, + pub reserved_for_wdf: usize, + pub reserved_for_crt: u64, + pub effective_container_id: Guid, + pub last_sleep_counter: u64, + pub spin_call_count: u32, + pub padding_8: [u8; 4], + pub extended_feature_disable_mask: u64, + pub scheduler_shared_data_slot: usize, + pub heap_walk_context: usize, + pub primary_group_affinity: GroupAffinity, + pub rcu: [u32; 2], +} + +bitflags::bitflags! { + /// Flags stored in `RTL_USER_PROCESS_PARAMETERS.Flags`. + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] + pub struct RtlUserProcFlags: u32 { + /// Pointers in the process-parameter block are absolute addresses. + const NORMALIZED = 0x0000_0001; + const PROFILE_USER = 0x0000_0002; + const PROFILE_KERNEL = 0x0000_0004; + const PROFILE_SERVER = 0x0000_0008; + const UNKNOWN = 0x0000_0010; + /// Reserve low address space at process creation. + const RESERVE_1MB = 0x0000_0020; + /// Reserve low address space at process creation. + const RESERVE_16MB = 0x0000_0040; + const CASE_SENSITIVE = 0x0000_0080; + const DISABLE_HEAP_DECOMMIT = 0x0000_0100; + const PROCESS_OR_1 = 0x0000_0200; + const PROCESS_OR_2 = 0x0000_0400; + const DLL_REDIRECTION_LOCAL = 0x0000_1000; + /// An application manifest was detected during process creation. + const APP_MANIFEST_PRESENT = 0x0000_2000; + /// The corresponding Image File Execution Options key was missing at process creation. + const IMAGE_KEY_MISSING = 0x0000_4000; + /// System-global IFEO development override support is enabled. + const DEV_OVERRIDE_ENABLED = 0x0000_8000; + const OPTIN_PROCESS = 0x0002_0000; + const SESSION_OWNER = 0x0004_0000; + const HANDLE_USER_CALLBACK_EXCEPTIONS = 0x0008_0000; + const PROTECTED_PROCESS = 0x0040_0000; + const NO_IMAGE_EXPANSION_MITIGATION = 0x0200_0000; + const APPX_LOADER_ALTERNATE_FORWARDER = 0x0400_0000; + const APPX_GLOBAL_OVERRIDE = 0x0800_0000; + /// Allow the loader to use OneCore API-set forwarders when resolving imports. + const ONECORE_FORWARDERS_ENABLED = 0x2000_0000; + /// Opt back in to the normal `ExitProcess` path that detaches DLLs on exit. + const EXIT_PROCESS_NORMAL = 0x4000_0000; + const SECURE_PROCESS = 0x8000_0000; + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct CurDir { + pub dos_path: UnicodeString, + pub handle: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct RtlDriveLetterCurdir { + /// Per-drive current-directory flags. + pub flags: u16, + /// Length of the drive current-directory entry. + pub length: u16, + /// Timestamp associated with this drive current-directory entry. + pub time_stamp: u32, + /// DOS path for this drive's current directory. + pub dos_path: UnicodeString, +} + +/// Memory layout of this struct: +/// +/// ```text +/// +-------------------------------+ +/// | RTL_USER_PROCESS_PARAMETERS | +/// | fixed-size struct | +/// +-------------------------------+ +/// | CurrentDirectory.DosPath | +/// | (string buffer) | +/// +-------------------------------+ +/// | DllPath | +/// +-------------------------------+ +/// | ImagePathName | +/// +-------------------------------+ +/// | CommandLine | +/// +-------------------------------+ +/// | WindowTitle | +/// +-------------------------------+ +/// | DesktopInfo | +/// +-------------------------------+ +/// | ShellInfo | +/// +-------------------------------+ +/// | RuntimeData | +/// +-------------------------------+ +/// | RedirectionDllName | +/// +-------------------------------+ +/// ``` +/// +/// See for details on the fields of this struct. +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub struct RtlUserProcessParameters { + /// Total allocated size of this process-parameter buffer, in bytes. + pub maximum_length: u32, + /// Size of the process-parameter block, including any inline variable-length strings. + pub length: u32, + /// Process-parameter flags (see [`RtlUserProcFlags`]). + pub flags: u32, + /// Debug flags associated with these process parameters. + pub debug_flags: u32, + /// Console session handle, inherited or derived from process creation options. + pub console_handle: usize, + /// Console behavior flags, such as ignoring Ctrl+C requests. + pub console_flags: u32, + /// Reserved alignment padding. + pub padding_0: [u8; 4], + /// Standard input handle from `STARTUPINFO.hStdInput`. + pub standard_input: usize, + /// Standard output handle from `STARTUPINFO.hStdOutput`. + pub standard_output: usize, + /// Standard error handle from `STARTUPINFO.hStdError`. + pub standard_error: usize, + /// Current directory path and handle. + pub current_directory: CurDir, + /// Semicolon-separated DOS-style DLL search paths. + pub dll_path: UnicodeString, + /// Full DOS-style path to the executable image. + pub image_path_name: UnicodeString, + /// Command line string passed to the process. + pub command_line: UnicodeString, + /// Pointer to the separately allocated environment block. + pub environment: usize, + /// Initial window X position when `window_flags` requests a position. + pub starting_x: u32, + /// Initial window Y position when `window_flags` requests a position. + pub starting_y: u32, + /// Initial window width when `window_flags` requests a size. + pub count_x: u32, + /// Initial window height when `window_flags` requests a size. + pub count_y: u32, + /// Initial console screen-buffer width in character cells. + pub count_chars_x: u32, + /// Initial console screen-buffer height in character cells. + pub count_chars_y: u32, + /// Initial console text/background color attributes. + pub fill_attribute: u32, + /// `STARTUPINFO` flags describing which startup fields are valid. + pub window_flags: u32, + /// `ShowWindow` value used when `window_flags` includes `STARTF_USESHOWWINDOW`. + pub show_window_flags: u32, + /// Reserved alignment padding. + pub padding_1: [u8; 4], + /// Console window title, shortcut path, or AppUserModelID depending on `window_flags`. + pub window_title: UnicodeString, + /// Window station and desktop name, such as `WinSta0\Default`. + pub desktop_info: UnicodeString, + /// Startup shell data corresponding to `STARTUPINFO.lpReserved`. + pub shell_info: UnicodeString, + /// Runtime data corresponding to `STARTUPINFO.lpReserved2` and `cbReserved2`. + pub runtime_data: UnicodeString, + /// Per-drive current-directory entries for the 32 DOS drive letters. + pub current_directories: [RtlDriveLetterCurdir; 32], + /// Allocated size of the environment block, in bytes. + pub environment_size: u64, + /// Environment version incremented when environment strings change. + pub environment_version: u64, + /// Package dependency metadata pointer. + pub package_dependency_data: usize, + /// Console process group identifier used to scope control-signal delivery. + pub process_group_id: u32, + /// Requested worker-thread count for parallel DLL loading. + pub loader_threads: u32, + /// DLL path used for packaged-app import redirection. + pub redirection_dll_name: UnicodeString, + /// Heap partition name. + pub heap_partition_name: UnicodeString, + /// Pointer to default thread-pool CPU-set masks. + pub default_threadpool_cpu_set_masks: usize, + /// Number of default thread-pool CPU-set masks. + pub default_threadpool_cpu_set_mask_count: u32, + /// Maximum default thread-pool thread count. + pub default_threadpool_thread_maximum: u32, + /// Heap memory type mask. + pub heap_memory_type_mask: u32, + /// Reserved tail padding. + pub padding_2: [u8; 4], +} + +const _: [(); 0x1878] = [(); core::mem::size_of::()]; +const _: [(); 0x7d0] = [(); core::mem::size_of::()]; +const _: [(); 0x4d0] = [(); core::mem::size_of::()]; +const _: [(); 0x448] = [(); core::mem::size_of::()]; diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index b8c1412b82..9f797ea0af 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -64,5 +64,7 @@ pub(crate) fn test_task() -> Task { fs, entry_point: 0, stack_top: 0, + context: 0, + teb_address: 0, } } From a1288adee2f703680e3d2b5223d30fe86544c18b Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 4 Jun 2026 10:47:39 -0700 Subject: [PATCH 023/319] Implement Windows NLS section and locale syscalls (#901) Adds Windows NLS syscall support to `litebox_shim_windows`. - Implements `NtGetNlsSectionPtr`, `NtInitializeNlsFiles`, and default locale/UI language query/set syscalls. - Maps NLS data from the sandbox VFS into read-only guest pages and caches mapped NLS sections per process. - Adds process-local NLS/locale state for system LCID, user LCID, and user UI language. --- litebox_shim_windows/src/lib.rs | 87 +- litebox_shim_windows/src/syscalls/file.rs | 17 +- litebox_shim_windows/src/syscalls/mod.rs | 59 ++ litebox_shim_windows/src/syscalls/nls.rs | 960 ++++++++++++++++++++++ litebox_shim_windows/src/tests.rs | 48 +- 5 files changed, 1154 insertions(+), 17 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/nls.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 4acda0ffdb..b0da2c5246 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -11,10 +11,11 @@ extern crate alloc; +use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; use core::marker::PhantomData; -use core::sync::atomic::{AtomicI32, Ordering}; +use core::sync::atomic::{AtomicI32, AtomicU32, Ordering}; use litebox_common_windows::nt_status::NtStatus; use litebox::LiteBox; @@ -68,6 +69,8 @@ pub(crate) type MutPtr = pub(crate) type WindowsPageManager = PageManager; pub(crate) type WindowsHandleStore = litebox::sync::RwLock; +pub(crate) type WindowsNlsSectionMappings = + litebox::sync::RwLock>; pub type DefaultFS = WindowsFS; @@ -110,6 +113,18 @@ where Some(()) } +pub(crate) fn probe_guest_output_preserving_value( + ptr: MutPtr, +) -> Result<(), NtStatus> +where + Platform: RawPointerProvider, + T: zerocopy::FromBytes + zerocopy::IntoBytes, +{ + let value = ptr.read_at_offset(0).ok_or(NtStatus::ACCESS_VIOLATION)?; + ptr.write_at_offset(0, value) + .ok_or(NtStatus::ACCESS_VIOLATION) +} + fn set_guest_teb(platform: &Platform, teb_address: usize) -> bool where Platform: PunchthroughProvider + RawPointerProvider, @@ -278,7 +293,12 @@ impl WindowsShim { loader::PeLoader::new(self.0.platform, fs.clone(), &self.0.page_manager).load(path)?; let process = Arc::new(Process { ntdll_mapping: load_info.ntdll_mapping, + peb_address: load_info.environment.peb, handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), + nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), + system_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), + user_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), + user_ui_language: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), exit_code: AtomicI32::new(DEFAULT_PROCESS_EXIT_CODE), }); Ok(LoadedProgram { @@ -311,7 +331,12 @@ struct GlobalState { /// Per-process Windows state shared by every thread in the process. pub struct Process { ntdll_mapping: Option, + peb_address: usize, handles: WindowsHandleStore, + nls_section_mappings: WindowsNlsSectionMappings, + system_lcid: AtomicU32, + user_lcid: AtomicU32, + user_ui_language: AtomicU32, exit_code: AtomicI32, } @@ -457,6 +482,66 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtGetNlsSectionPtr { + section_type, + section_data, + context_data, + section_pointer, + section_size, + } => { + let status = self.sys_nt_get_nls_section_ptr( + section_type, + section_data, + context_data, + section_pointer, + section_size, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtInitializeNlsFiles { + base_address, + default_locale_id, + default_casing_table_size, + } => { + let status = self.sys_nt_initialize_nls_files( + base_address, + default_locale_id, + default_casing_table_size, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryDefaultLocale { + user_profile, + default_locale_id, + } => { + let status = self.sys_nt_query_default_locale(user_profile, default_locale_id); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtSetDefaultLocale { + user_profile, + default_locale_id, + } => { + let status = self.sys_nt_set_default_locale(user_profile, default_locale_id); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryDefaultUILanguage { + default_ui_language, + } => { + let status = self.sys_nt_query_default_ui_language(default_ui_language); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtSetDefaultUILanguage { + default_ui_language, + } => { + let status = self.sys_nt_set_default_ui_language(default_ui_language); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryInstallUILanguage { + install_ui_language, + } => { + let status = self.sys_nt_query_install_ui_language(install_ui_language); + (status, ContinueOperation::Resume) + } SyscallRequest::NtAllocateVirtualMemory { process_handle, base_address, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 6fd433907f..e7788a6751 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -16,7 +16,8 @@ use crate::nt_types::{ }; use crate::syscalls::Handle; use crate::{ - ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, raw_handle_entry, remove_raw_handle, + ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, + raw_handle_entry, remove_raw_handle, }; const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001; @@ -643,18 +644,8 @@ fn probe_file_outputs( file_handle: MutPtr, io_status_block: MutPtr, ) -> Result<(), NtStatus> { - probe_writable::(file_handle)?; - probe_writable::(io_status_block) -} - -fn probe_writable(ptr: MutPtr) -> Result<(), NtStatus> -where - Platform: RawPointerProvider, - T: zerocopy::FromBytes + zerocopy::IntoBytes, -{ - let value = ptr.read_at_offset(0).ok_or(NtStatus::ACCESS_VIOLATION)?; - ptr.write_at_offset(0, value) - .ok_or(NtStatus::ACCESS_VIOLATION) + probe_guest_output_preserving_value::(file_handle)?; + probe_guest_output_preserving_value::(io_status_block) } fn write_file_result( diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 8e1b7530a4..405bee7451 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -2,6 +2,7 @@ // Licensed under the MIT license. pub(crate) mod file; +pub(crate) mod nls; pub(crate) mod registry; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; @@ -123,6 +124,35 @@ pub(crate) enum SyscallRequest { length: u32, result_length: Platform::RawMutPointer, }, + NtGetNlsSectionPtr { + section_type: u32, + section_data: u32, + context_data: usize, + section_pointer: Platform::RawMutPointer, + section_size: Option>, + }, + NtInitializeNlsFiles { + base_address: Platform::RawMutPointer, + default_locale_id: Platform::RawMutPointer, + default_casing_table_size: Platform::RawMutPointer, + }, + NtQueryDefaultLocale { + user_profile: u8, + default_locale_id: Platform::RawMutPointer, + }, + NtSetDefaultLocale { + user_profile: u8, + default_locale_id: u32, + }, + NtQueryDefaultUILanguage { + default_ui_language: Platform::RawMutPointer, + }, + NtSetDefaultUILanguage { + default_ui_language: u16, + }, + NtQueryInstallUILanguage { + install_ui_language: Platform::RawMutPointer, + }, NtAllocateVirtualMemory { process_handle: ProcessHandle, base_address: Platform::RawMutPointer, @@ -195,6 +225,35 @@ impl SyscallRequest { length, result_length:*, })), + NtSysno::NtGetNlsSectionPtr => Some(sys_req!(NtGetNlsSectionPtr { + section_type, + section_data, + context_data, + section_pointer:*, + section_size:*, + })), + NtSysno::NtInitializeNlsFiles => Some(sys_req!(NtInitializeNlsFiles { + base_address:*, + default_locale_id:*, + default_casing_table_size:*, + })), + NtSysno::NtQueryDefaultLocale => Some(sys_req!(NtQueryDefaultLocale { + user_profile, + default_locale_id:*, + })), + NtSysno::NtSetDefaultLocale => Some(sys_req!(NtSetDefaultLocale { + user_profile, + default_locale_id, + })), + NtSysno::NtQueryDefaultUILanguage => Some(sys_req!(NtQueryDefaultUILanguage { + default_ui_language:*, + })), + NtSysno::NtSetDefaultUILanguage => Some(sys_req!(NtSetDefaultUILanguage { + default_ui_language, + })), + NtSysno::NtQueryInstallUILanguage => Some(sys_req!(NtQueryInstallUILanguage { + install_ui_language:*, + })), NtSysno::NtAllocateVirtualMemory => Some(sys_req!(NtAllocateVirtualMemory { process_handle: { ProcessHandle::from_raw }, base_address:*, diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs new file mode 100644 index 0000000000..83f63b5a14 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -0,0 +1,960 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::format; +use alloc::string::String; +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_common_windows::loader::PAGE_SIZE; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::ProcessEnvironmentBlock; +use crate::{MutPtr, ShimFS, ShimPlatform, Task, probe_guest_output_preserving_value, write_value}; + +pub(crate) const DEFAULT_LOCALE_ID: u32 = 0x0409; + +const ANSI_CODE_PAGE: u32 = 1252; +const OEM_CODE_PAGE: u32 = 437; +const UNICODE_CASE_TABLE: u32 = 10000; +const NLS_SECTION_LOCALE: u32 = 2; +const NLS_SECTION_SORTKEYS: u32 = 9; +const NLS_SECTION_CASEMAP: u32 = 10; +const NLS_SECTION_CODEPAGE: u32 = 11; +const NLS_SECTION_NORMALIZE: u32 = 12; + +struct NlsSectionRequest { + section_type: u32, + section_data: u32, + context_data: usize, + section_pointer: MutPtr, + section_size: Option>, +} + +impl Clone for NlsSectionRequest { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for NlsSectionRequest {} + +#[derive(Clone, Copy)] +struct MappedNlsSection { + address: usize, + len: usize, +} + +struct NlsSectionFile { + fd: TypedFd, + len: usize, +} + +impl Task { + pub(crate) fn sys_nt_get_nls_section_ptr( + &self, + section_type: u32, + section_data: u32, + context_data: usize, + section_pointer: MutPtr, + section_size: Option>, + ) -> NtStatus { + let request = NlsSectionRequest { + section_type, + section_data, + context_data, + section_pointer, + section_size, + }; + + if as litebox::platform::RawConstPointer>::as_usize( + &request.section_pointer, + ) == 0 + { + return NtStatus::INVALID_PARAMETER; + } + + let cache_key = (request.section_type, request.section_data); + if let Some(mapped_section) = self.cached_nls_section(cache_key) { + return self.write_nls_section_result(request, mapped_section, true); + } + + let mapped_section = match self.map_nls_section_file(request) { + Ok(mapped_section) => mapped_section, + Err(status) => { + litebox_util_log::debug!( + section_type = request.section_type, + section_data = request.section_data, + status:? = status; + "NtGetNlsSectionPtr section is not available" + ); + return status; + } + }; + + let (mapped_section, cached) = self.publish_nls_section_mapping(cache_key, mapped_section); + let status = self.write_nls_section_result(request, mapped_section, cached); + if status != NtStatus::SUCCESS && !cached { + self.remove_owned_cached_nls_section(cache_key, mapped_section); + } + status + } + + pub(crate) fn sys_nt_initialize_nls_files( + &self, + base_address: MutPtr, + default_locale_id: MutPtr, + _default_casing_table_size: MutPtr, + ) -> NtStatus { + if base_address.as_usize() == 0 { + return NtStatus::ACCESS_VIOLATION; + } + + let request = NlsSectionRequest { + section_type: NLS_SECTION_LOCALE, + section_data: 0, + context_data: 0, + section_pointer: base_address, + section_size: None, + }; + let cache_key = (NLS_SECTION_LOCALE, 0); + let (mapped_section, cached) = + if let Some(mapped_section) = self.cached_nls_section(cache_key) { + (mapped_section, true) + } else { + let mapped_section = match self.map_nls_section_file(request) { + Ok(mapped_section) => mapped_section, + Err(status) => return status, + }; + self.publish_nls_section_mapping(cache_key, mapped_section) + }; + + let locale_id = self + .process + .system_lcid + .load(core::sync::atomic::Ordering::Relaxed); + if probe_guest_output_preserving_value::(base_address).is_err() + || probe_guest_output_preserving_value::(default_locale_id).is_err() + || default_locale_id.write_at_offset(0, locale_id).is_none() + || base_address + .write_at_offset(0, mapped_section.address) + .is_none() + { + if !cached { + self.remove_owned_cached_nls_section(cache_key, mapped_section); + } + return NtStatus::ACCESS_VIOLATION; + } + + litebox_util_log::debug!( + base:% = format_args!("{:#x}", mapped_section.address), + default_locale_id = locale_id; + "Handled NtInitializeNlsFiles syscall" + ); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_query_default_locale( + &self, + user_profile: u8, + default_locale_id: MutPtr, + ) -> NtStatus { + let locale_id = if user_profile == 0 { + self.process + .system_lcid + .load(core::sync::atomic::Ordering::Relaxed) + } else { + self.process + .user_lcid + .load(core::sync::atomic::Ordering::Relaxed) + }; + write_required_output::(default_locale_id, locale_id) + } + + pub(crate) fn sys_nt_set_default_locale( + &self, + user_profile: u8, + default_locale_id: u32, + ) -> NtStatus { + if user_profile == 0 { + self.process + .system_lcid + .store(default_locale_id, core::sync::atomic::Ordering::Relaxed); + } else { + self.process + .user_lcid + .store(default_locale_id, core::sync::atomic::Ordering::Relaxed); + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_query_default_ui_language( + &self, + default_ui_language: MutPtr, + ) -> NtStatus { + let lang_id = lang_id_from_locale_id( + self.process + .user_ui_language + .load(core::sync::atomic::Ordering::Relaxed), + ); + write_required_output::(default_ui_language, lang_id) + } + + pub(crate) fn sys_nt_set_default_ui_language(&self, default_ui_language: u16) -> NtStatus { + self.process.user_ui_language.store( + u32::from(default_ui_language), + core::sync::atomic::Ordering::Relaxed, + ); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_query_install_ui_language( + &self, + install_ui_language: MutPtr, + ) -> NtStatus { + let lang_id = lang_id_from_locale_id( + self.process + .system_lcid + .load(core::sync::atomic::Ordering::Relaxed), + ); + write_required_output::(install_ui_language, lang_id) + } + + fn map_nls_section_file( + &self, + request: NlsSectionRequest, + ) -> Result { + let section_file = self.open_nls_section_file(request)?; + let section_len = section_file.len; + let alloc_len = match nls_section_alloc_len(section_len) { + Ok(alloc_len) => alloc_len, + Err(status) => { + let _ = self.fs.close(§ion_file.fd); + return Err(status); + } + }; + let Some(page_len) = NonZeroPageSize::::new(alloc_len) else { + let _ = self.fs.close(§ion_file.fd); + return Err(NtStatus::INVALID_PARAMETER); + }; + + let mut copy_status = None; + // SAFETY: No fixed address is requested, so the page manager chooses an unused guest + // range. The callback only initializes the newly allocated pages before they are exposed. + let mapping = unsafe { + self.global.page_manager.create_readable_pages( + None, + page_len, + CreatePagesFlags::POPULATE_PAGES_IMMEDIATELY, + |ptr| match self.copy_nls_section_file(§ion_file.fd, section_len, ptr) { + Ok(copied) => Ok(copied), + Err(status) => { + copy_status = Some(status); + Err(MappingError::OutOfMemory) + } + }, + ) + }; + let _ = self.fs.close(§ion_file.fd); + let mapping = mapping.map_err(|_| copy_status.unwrap_or(NtStatus::NO_MEMORY))?; + Ok(MappedNlsSection { + address: mapping.as_usize(), + len: alloc_len, + }) + } + + fn open_nls_section_file( + &self, + request: NlsSectionRequest, + ) -> Result, NtStatus> { + let path = nls_section_file_path(request.section_type, request.section_data)?; + let fd = self + .fs + .open(path.as_str(), OFlags::RDONLY, Mode::empty()) + .map_err(map_nls_open_error)?; + + let status = match self.fs.fd_file_status(&fd) { + Ok(status) => status, + Err(error) => { + let _ = self.fs.close(&fd); + return Err(map_nls_file_status_error(error)); + } + }; + if status.file_type != FileType::RegularFile { + let _ = self.fs.close(&fd); + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + } + if status.size == 0 { + let _ = self.fs.close(&fd); + return Err(NtStatus::OBJECT_NAME_NOT_FOUND); + } + + Ok(NlsSectionFile { + fd, + len: status.size, + }) + } + + fn copy_nls_section_file( + &self, + fd: &TypedFd, + section_len: usize, + output: MutPtr, + ) -> Result { + let mut offset = 0; + while offset < section_len { + let mut chunk = [0; PAGE_SIZE]; + let remaining = section_len - offset; + let chunk_len = remaining.min(PAGE_SIZE); + let read = self + .fs + .read(fd, &mut chunk[..chunk_len], Some(offset)) + .map_err(map_nls_read_error)?; + if read == 0 { + return Err(NtStatus::END_OF_FILE); + } + let Ok(output_offset) = isize::try_from(offset) else { + return Err(NtStatus::INVALID_PARAMETER); + }; + if output + .write_slice_at_offset(output_offset, &chunk[..read]) + .is_none() + { + return Err(NtStatus::ACCESS_VIOLATION); + } + offset += read; + } + Ok(offset) + } + + fn write_nls_section_result( + &self, + request: NlsSectionRequest, + mapped_section: MappedNlsSection, + cached: bool, + ) -> NtStatus { + if probe_guest_output_preserving_value::(request.section_pointer).is_err() + { + return NtStatus::ACCESS_VIOLATION; + } + let Ok(len) = u32::try_from(mapped_section.len) else { + return NtStatus::SECTION_TOO_BIG; + }; + if let Some(section_size) = request.section_size + && section_size.write_at_offset(0, len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if request + .section_pointer + .write_at_offset(0, mapped_section.address) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + self.set_peb_nls_pointer(request.section_data, mapped_section.address); + + litebox_util_log::debug!( + section_type = request.section_type, + section_data = request.section_data, + context_data:% = format_args!("{:#x}", request.context_data), + mapped_address:% = format_args!("{:#x}", mapped_section.address), + section_len = mapped_section.len, + cached = cached; + "Handled NtGetNlsSectionPtr syscall" + ); + + NtStatus::SUCCESS + } + + fn cached_nls_section(&self, cache_key: (u32, u32)) -> Option { + self.process + .nls_section_mappings + .read() + .get(&cache_key) + .copied() + .map(|(address, len)| MappedNlsSection { address, len }) + } + + fn publish_nls_section_mapping( + &self, + cache_key: (u32, u32), + mapped_section: MappedNlsSection, + ) -> (MappedNlsSection, bool) { + let mut mappings = self.process.nls_section_mappings.write(); + if let Some((mapped_address, section_len)) = mappings.get(&cache_key).copied() { + drop(mappings); + self.unmap_owned_nls_section(mapped_section); + return ( + MappedNlsSection { + address: mapped_address, + len: section_len, + }, + true, + ); + } + + mappings.insert(cache_key, (mapped_section.address, mapped_section.len)); + (mapped_section, false) + } + + fn remove_owned_cached_nls_section( + &self, + cache_key: (u32, u32), + mapped_section: MappedNlsSection, + ) { + let mut mappings = self.process.nls_section_mappings.write(); + let remove_cached_mapping = + mappings.get(&cache_key).copied() == Some((mapped_section.address, mapped_section.len)); + if remove_cached_mapping { + mappings.remove(&cache_key); + } + drop(mappings); + + if remove_cached_mapping { + self.unmap_owned_nls_section(mapped_section); + } + } + + fn set_peb_nls_pointer(&self, section_data: u32, mapped_address: usize) { + if self.process.peb_address == 0 { + return; + } + + let Some(field_offset) = (match section_data { + ANSI_CODE_PAGE => Some(core::mem::offset_of!( + ProcessEnvironmentBlock, + ansi_code_page_data + )), + OEM_CODE_PAGE => Some(core::mem::offset_of!( + ProcessEnvironmentBlock, + oem_code_page_data + )), + UNICODE_CASE_TABLE => Some(core::mem::offset_of!( + ProcessEnvironmentBlock, + unicode_case_table_data + )), + _ => None, + }) else { + return; + }; + + let peb_field = + MutPtr::::from_usize(self.process.peb_address + field_offset); + let _ = peb_field.write_at_offset(0, mapped_address); + } + + fn unmap_owned_nls_section(&self, mapped_section: MappedNlsSection) { + // SAFETY: The mapping was created by this syscall path and has not been published on the + // failing path, so no guest execution can hold a valid reference to it yet. + let _ = unsafe { + self.global.page_manager.remove_pages( + MutPtr::::from_usize(mapped_section.address), + mapped_section.len, + ) + }; + } +} + +fn nls_section_file_path(section_type: u32, section_data: u32) -> Result { + match section_type { + NLS_SECTION_LOCALE if section_data == 0 => Ok(String::from("/Windows/System32/locale.nls")), + NLS_SECTION_SORTKEYS if section_data == 0 => Ok(String::from( + "/Windows/Globalization/Sorting/sortdefault.nls", + )), + NLS_SECTION_CASEMAP if section_data == 0 => { + Ok(String::from("/Windows/System32/l_intl.nls")) + } + NLS_SECTION_CASEMAP => Err(NtStatus::UNSUCCESSFUL), + NLS_SECTION_CODEPAGE => Ok(format!("/Windows/System32/c_{section_data:03}.nls")), + NLS_SECTION_NORMALIZE => normalize_nls_file_name(section_data) + .map(|name| format!("/Windows/System32/{name}.nls")) + .ok_or(NtStatus::OBJECT_NAME_NOT_FOUND), + _ => Err(NtStatus::INVALID_PARAMETER_1), + } +} + +fn nls_section_alloc_len(section_len: usize) -> Result { + let alloc_len = section_len + .checked_next_multiple_of(PAGE_SIZE) + .ok_or(NtStatus::SECTION_TOO_BIG)?; + if u32::try_from(alloc_len).is_err() { + return Err(NtStatus::SECTION_TOO_BIG); + } + Ok(alloc_len) +} + +fn lang_id_from_locale_id(locale_id: u32) -> u16 { + u16::try_from(locale_id & u32::from(u16::MAX)).expect("masked locale id fits in a LANGID") +} + +fn normalize_nls_file_name(section_data: u32) -> Option<&'static str> { + match section_data { + 1 => Some("normnfc"), + 2 => Some("normnfd"), + 5 => Some("normnfkc"), + 6 => Some("normnfkd"), + 13 => Some("normidna"), + _ => None, + } +} + +fn write_required_output(output: MutPtr, value: T) -> NtStatus +where + Platform: RawPointerProvider, + T: zerocopy::FromBytes + zerocopy::IntoBytes, +{ + if write_value::(output.as_usize(), value).is_some() { + NtStatus::SUCCESS + } else { + NtStatus::ACCESS_VIOLATION + } +} + +fn map_nls_open_error(error: OpenError) -> NtStatus { + match error { + OpenError::PathError( + PathError::NoSuchFileOrDirectory + | PathError::MissingComponent + | PathError::ComponentNotADirectory, + ) => NtStatus::OBJECT_NAME_NOT_FOUND, + OpenError::PathError(PathError::NoSearchPerms { .. }) | OpenError::AccessNotAllowed => { + NtStatus::ACCESS_DENIED + } + _ => NtStatus::UNSUCCESSFUL, + } +} + +fn map_nls_file_status_error(error: FileStatusError) -> NtStatus { + match error { + FileStatusError::PathError( + PathError::NoSuchFileOrDirectory + | PathError::MissingComponent + | PathError::ComponentNotADirectory, + ) => NtStatus::OBJECT_NAME_NOT_FOUND, + FileStatusError::PathError(PathError::NoSearchPerms { .. }) => NtStatus::ACCESS_DENIED, + _ => NtStatus::UNSUCCESSFUL, + } +} + +fn map_nls_read_error(error: ReadError) -> NtStatus { + match error { + ReadError::NotForReading => NtStatus::ACCESS_DENIED, + _ => NtStatus::UNSUCCESSFUL, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use litebox::platform::RawPointerProvider; + use zerocopy::{FromBytes, IntoBytes}; + + extern crate std; + + type TestPlatform = crate::tests::TestPlatform; + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + unsafe extern "system" { + fn NtGetNlsSectionPtr( + section_type: u32, + section_data: u32, + context_data: *mut core::ffi::c_void, + section_pointer: *mut *const u8, + section_size: *mut u32, + ) -> i32; + + fn NtInitializeNlsFiles( + base_address: *mut *const u8, + default_locale_id: *mut u32, + default_casing_table_size: *mut i64, + ) -> i32; + + fn NtQueryDefaultLocale(user_profile: u8, default_locale_id: *mut u32) -> i32; + + fn NtQueryDefaultUILanguage(default_ui_language: *mut u16) -> i32; + + fn NtQueryInstallUILanguage(install_ui_language: *mut u16) -> i32; + } + + fn mut_ptr(value: &mut T) -> MutPtr { + MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn host_system32_file_bytes(file_name: &str) -> std::vec::Vec { + std::fs::read( + std::path::PathBuf::from( + std::env::var_os("SystemRoot") + .unwrap_or_else(|| std::ffi::OsString::from(r"C:\Windows")), + ) + .join("System32") + .join(file_name), + ) + .unwrap() + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn host_status(status: i32) -> NtStatus { + NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) + } + + #[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(&[( + "/Windows/System32/c_1252.nls", + section_bytes.as_slice(), + )]); + let mut section_pointer = 0usize; + let mut section_size = 0u32; + + assert_eq!( + task.sys_nt_get_nls_section_ptr( + NLS_SECTION_CODEPAGE, + ANSI_CODE_PAGE, + 0, + mut_ptr(&mut section_pointer), + Some(mut_ptr(&mut section_size)), + ), + NtStatus::SUCCESS + ); + + assert_ne!(section_pointer, 0); + assert_eq!(section_size, u32::try_from(PAGE_SIZE).unwrap()); + let mapped = ::RawConstPointer::::from_usize( + section_pointer, + ); + assert_eq!( + mapped.to_owned_slice(section_bytes.len()).unwrap().as_ref(), + section_bytes.as_slice() + ); + + let mut second_section_pointer = 0usize; + assert_eq!( + task.sys_nt_get_nls_section_ptr( + NLS_SECTION_CODEPAGE, + ANSI_CODE_PAGE, + 0, + mut_ptr(&mut second_section_pointer), + None, + ), + NtStatus::SUCCESS + ); + assert_eq!(second_section_pointer, section_pointer); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[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(&[( + "/Windows/System32/c_1252.nls", + host_file_bytes.as_slice(), + )]); + + let mut host_section_pointer = core::ptr::null::(); + let mut host_section_size = 0u32; + // SAFETY: The pointers reference local output variables, and the section type/data pair is + // the same supported codepage section requested by normal Windows process startup. + let status = unsafe { + NtGetNlsSectionPtr( + NLS_SECTION_CODEPAGE, + ANSI_CODE_PAGE, + core::ptr::null_mut(), + core::ptr::addr_of_mut!(host_section_pointer), + core::ptr::addr_of_mut!(host_section_size), + ) + }; + assert_eq!(host_status(status), NtStatus::SUCCESS); + assert!(!host_section_pointer.is_null()); + + let mut section_pointer = 0usize; + let mut section_size = 0u32; + assert_eq!( + task.sys_nt_get_nls_section_ptr( + NLS_SECTION_CODEPAGE, + ANSI_CODE_PAGE, + 0, + mut_ptr(&mut section_pointer), + Some(mut_ptr(&mut section_size)), + ), + NtStatus::SUCCESS + ); + + let host_section_len = usize::try_from(host_section_size).unwrap(); + assert_eq!(section_size, host_section_size); + let mapped = ::RawConstPointer::::from_usize( + section_pointer, + ); + // SAFETY: A successful host NtGetNlsSectionPtr returned a non-null pointer and size for a + // process-lifetime read-only NLS mapping. + let host_section = + unsafe { core::slice::from_raw_parts(host_section_pointer, host_section_len) }; + assert_eq!( + mapped.to_owned_slice(host_section_len).unwrap().as_ref(), + host_section + ); + } + + #[test] + fn nt_get_nls_section_ptr_rejects_invalid_arguments() { + let bytes = [0xaa]; + let task = crate::tests::test_task_with_nls_files(&[( + "/Windows/System32/c_437.nls", + bytes.as_slice(), + )]); + let mut section_pointer = 0usize; + + assert_eq!( + task.sys_nt_get_nls_section_ptr( + NLS_SECTION_CODEPAGE, + OEM_CODE_PAGE, + 0, + MutPtr::::from_usize(0), + None, + ), + NtStatus::INVALID_PARAMETER + ); + assert_eq!( + task.sys_nt_get_nls_section_ptr( + NLS_SECTION_CODEPAGE, + ANSI_CODE_PAGE, + 0, + mut_ptr(&mut section_pointer), + None, + ), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!(section_pointer, 0); + } + + #[test] + fn nls_section_file_path_formats_codepage_names() { + assert_eq!( + nls_section_file_path(NLS_SECTION_CODEPAGE, 37).unwrap(), + "/Windows/System32/c_037.nls" + ); + assert_eq!( + nls_section_file_path(NLS_SECTION_CODEPAGE, ANSI_CODE_PAGE).unwrap(), + "/Windows/System32/c_1252.nls" + ); + } + + #[test] + fn nls_section_alloc_len_rejects_unrepresentable_sections() { + assert_eq!(nls_section_alloc_len(1).unwrap(), PAGE_SIZE); + assert_eq!( + nls_section_alloc_len(usize::MAX), + Err(NtStatus::SECTION_TOO_BIG) + ); + assert_eq!( + nls_section_alloc_len(usize::try_from(u32::MAX).unwrap()), + Err(NtStatus::SECTION_TOO_BIG) + ); + } + + #[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(&[( + "/Windows/System32/locale.nls", + locale_bytes.as_slice(), + )]); + let mut base_address = 0usize; + let mut locale_id = 0u32; + let mut casing_table_size = 0x1234_5678i64; + + assert_eq!( + task.sys_nt_initialize_nls_files( + mut_ptr(&mut base_address), + mut_ptr(&mut locale_id), + mut_ptr(&mut casing_table_size), + ), + NtStatus::SUCCESS + ); + + assert_ne!(base_address, 0); + assert_eq!(locale_id, DEFAULT_LOCALE_ID); + assert_eq!(casing_table_size, 0x1234_5678); + let mapped = + ::RawConstPointer::::from_usize(base_address); + assert_eq!( + mapped.to_owned_slice(locale_bytes.len()).unwrap().as_ref(), + locale_bytes.as_slice() + ); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[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(&[( + "/Windows/System32/locale.nls", + host_file_bytes.as_slice(), + )]); + + let mut host_base_address = core::ptr::null::(); + let mut host_locale_id = 0x1234_5678u32; + let mut host_casing_table_size = 0x1234_5678i64; + // SAFETY: The pointers reference local output variables and mirror the normal process + // startup call shape; the returned mapping is process-lifetime read-only NLS data. + let status = unsafe { + NtInitializeNlsFiles( + core::ptr::addr_of_mut!(host_base_address), + core::ptr::addr_of_mut!(host_locale_id), + core::ptr::addr_of_mut!(host_casing_table_size), + ) + }; + assert_eq!(host_status(status), NtStatus::SUCCESS); + assert!(!host_base_address.is_null()); + + task.process + .system_lcid + .store(host_locale_id, core::sync::atomic::Ordering::Relaxed); + let mut base_address = 0usize; + let mut locale_id = 0x1234_5678u32; + let mut casing_table_size = 0x1234_5678i64; + assert_eq!( + task.sys_nt_initialize_nls_files( + mut_ptr(&mut base_address), + mut_ptr(&mut locale_id), + mut_ptr(&mut casing_table_size), + ), + NtStatus::SUCCESS + ); + + assert_eq!(locale_id, host_locale_id); + assert_eq!(casing_table_size, host_casing_table_size); + let mapped = + ::RawConstPointer::::from_usize(base_address); + // SAFETY: A successful host NtInitializeNlsFiles returned a non-null process-lifetime NLS + // mapping, and the fixture file length bounds the comparison. + let host_section = + unsafe { core::slice::from_raw_parts(host_base_address, host_file_bytes.len()) }; + assert_eq!( + mapped + .to_owned_slice(host_file_bytes.len()) + .unwrap() + .as_ref(), + host_section + ); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn locale_query_syscalls_match_host_outputs() { + let task = crate::tests::test_task(); + let mut host_system_locale = 0u32; + let mut host_user_locale = 0u32; + let mut host_user_ui_language = 0u16; + let mut host_install_ui_language = 0u16; + + // SAFETY: The pointers reference local output variables for read-only host locale queries. + unsafe { + assert_eq!( + host_status(NtQueryDefaultLocale( + 0, + core::ptr::addr_of_mut!(host_system_locale), + )), + NtStatus::SUCCESS + ); + assert_eq!( + host_status(NtQueryDefaultLocale( + 1, + core::ptr::addr_of_mut!(host_user_locale), + )), + NtStatus::SUCCESS + ); + assert_eq!( + host_status(NtQueryDefaultUILanguage(core::ptr::addr_of_mut!( + host_user_ui_language + ))), + NtStatus::SUCCESS + ); + assert_eq!( + host_status(NtQueryInstallUILanguage(core::ptr::addr_of_mut!( + host_install_ui_language + ))), + NtStatus::SUCCESS + ); + } + + task.process + .system_lcid + .store(host_system_locale, core::sync::atomic::Ordering::Relaxed); + task.process + .user_lcid + .store(host_user_locale, core::sync::atomic::Ordering::Relaxed); + task.process.user_ui_language.store( + u32::from(host_user_ui_language), + core::sync::atomic::Ordering::Relaxed, + ); + + let mut locale_id = 0u32; + let mut language = 0u16; + assert_eq!( + task.sys_nt_query_default_locale(0, mut_ptr(&mut locale_id)), + NtStatus::SUCCESS + ); + assert_eq!(locale_id, host_system_locale); + assert_eq!( + task.sys_nt_query_default_locale(1, mut_ptr(&mut locale_id)), + NtStatus::SUCCESS + ); + assert_eq!(locale_id, host_user_locale); + assert_eq!( + task.sys_nt_query_default_ui_language(mut_ptr(&mut language)), + NtStatus::SUCCESS + ); + assert_eq!(language, host_user_ui_language); + task.process.system_lcid.store( + u32::from(host_install_ui_language), + core::sync::atomic::Ordering::Relaxed, + ); + assert_eq!( + task.sys_nt_query_install_ui_language(mut_ptr(&mut language)), + NtStatus::SUCCESS + ); + assert_eq!(language, host_install_ui_language); + } + + #[test] + fn locale_syscalls_query_and_update_process_locale_state() { + let task = crate::tests::test_task(); + let mut locale_id = 0u32; + let mut language = 0u16; + + assert_eq!( + task.sys_nt_query_default_locale(0, mut_ptr(&mut locale_id)), + NtStatus::SUCCESS + ); + assert_eq!(locale_id, DEFAULT_LOCALE_ID); + + assert_eq!(task.sys_nt_set_default_locale(0, 0x0411), NtStatus::SUCCESS); + assert_eq!( + task.sys_nt_query_default_locale(0, mut_ptr(&mut locale_id)), + NtStatus::SUCCESS + ); + assert_eq!(locale_id, 0x0411); + + assert_eq!( + task.sys_nt_set_default_ui_language(0x040c), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_query_default_ui_language(mut_ptr(&mut language)), + NtStatus::SUCCESS + ); + assert_eq!(language, 0x040c); + + assert_eq!( + task.sys_nt_query_install_ui_language(mut_ptr(&mut language)), + NtStatus::SUCCESS + ); + assert_eq!(language, 0x0411); + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 9f797ea0af..c353df0be6 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -3,13 +3,18 @@ extern crate std; +use alloc::collections::BTreeMap; use alloc::sync::Arc; use core::marker::PhantomData; -use core::sync::atomic::AtomicI32; +use core::sync::atomic::{AtomicI32, AtomicU32}; use litebox::LiteBox; use litebox::fd::RawDescriptorStorage; +use litebox::fs::{FileSystem as _, Mode, OFlags}; -use crate::{DefaultFS, GlobalState, Process, Task, WindowsHandleStore, WindowsPageManager}; +use crate::{ + DefaultFS, GlobalState, Process, Task, WindowsHandleStore, WindowsNlsSectionMappings, + WindowsPageManager, +}; #[cfg(target_os = "linux")] pub(crate) type TestPlatform = litebox_platform_linux_userland::LinuxUserland; @@ -31,12 +36,15 @@ pub(crate) fn test_platform() -> &'static TestPlatform { } pub(crate) fn test_task() -> Task { + test_task_with_nls_files(&[]) +} + +pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task { let platform = test_platform(); let litebox = LiteBox::new(platform); let page_manager = WindowsPageManager::::new(&litebox); let mut in_mem = litebox::fs::in_mem::FileSystem::new(&litebox); in_mem.with_root_privileges(|fs| { - use litebox::fs::FileSystem as _; fs.mkdir( "/tmp", litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, @@ -44,6 +52,35 @@ pub(crate) fn test_task() -> Task { .expect("/tmp creation cannot fail on a fresh in-memory file system"); fs.chown("/tmp", Some(1000), Some(1000)) .expect("/tmp chown cannot fail on a fresh in-memory file system"); + + if !nls_files.is_empty() { + fs.mkdir("/Windows", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("/Windows creation cannot fail on a fresh in-memory file system"); + fs.mkdir("/Windows/System32", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("/Windows/System32 creation cannot fail on a fresh in-memory file system"); + fs.mkdir( + "/Windows/Globalization", + Mode::RWXU | Mode::RWXG | Mode::RWXO, + ) + .expect("/Windows/Globalization creation cannot fail on a fresh in-memory file system"); + fs.mkdir( + "/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( + *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"); + } }); let tar_ro = litebox::fs::tar_ro::FileSystem::new(&litebox, litebox::fs::tar_ro::EMPTY_TAR_FILE.into()); @@ -58,7 +95,12 @@ pub(crate) fn test_task() -> Task { }), process: Arc::new(Process { ntdll_mapping: None, + peb_address: 0, handles: WindowsHandleStore::::new(RawDescriptorStorage::new()), + nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), + system_lcid: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), + user_lcid: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), + user_ui_language: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), exit_code: AtomicI32::new(0), }), fs, From 0118f437ef948f83f1780a80071bded2aa9d417b Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 4 Jun 2026 17:41:14 -0700 Subject: [PATCH 024/319] Support NtQueryPerformanceCounter (#904) This PR adds Windows shim support for NtQueryPerformanceCounter, which uses the platform `TimeProvider` to return a monotonic counter relative to the shim startup, with a fixed frequency. --- litebox_common_windows/src/nt_status.rs | 4 + litebox_shim_windows/src/lib.rs | 28 +- litebox_shim_windows/src/syscalls/file.rs | 15 +- litebox_shim_windows/src/syscalls/mod.rs | 23 ++ litebox_shim_windows/src/syscalls/nls.rs | 6 +- litebox_shim_windows/src/syscalls/registry.rs | 15 +- litebox_shim_windows/src/syscalls/sysinfo.rs | 322 ++++++++++++++++++ litebox_shim_windows/src/tests.rs | 29 +- 8 files changed, 406 insertions(+), 36 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/sysinfo.rs diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index 1816b98bff..0a91d4b069 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -171,6 +171,7 @@ impl NtStatus { 0xC00000AD => "STATUS_INVALID_PIPE_STATE: Invalid pipe state", 0xC00000AE => "STATUS_PIPE_BUSY: Pipe busy", 0xC00000B0 => "STATUS_PIPE_DISCONNECTED: Pipe disconnected", + 0xC00000BB => "STATUS_NOT_SUPPORTED: The request is not supported", 0xC00000E6 => "STATUS_GENERIC_NOT_MAPPED: Generic not mapped", 0xC00000EF => "STATUS_INVALID_PARAMETER_1: Invalid parameter 1", 0xC00000FD => "STATUS_STACK_OVERFLOW: Stack overflow", @@ -472,6 +473,9 @@ impl NtStatus { /// STATUS_PIPE_DISCONNECTED pub const PIPE_DISCONNECTED: Self = Self::from_raw(0xC00000B0); + /// STATUS_NOT_SUPPORTED + pub const NOT_SUPPORTED: Self = Self::from_raw(0xC00000BB); + /// STATUS_GENERIC_NOT_MAPPED pub const GENERIC_NOT_MAPPED: Self = Self::from_raw(0xC00000E6); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index b0da2c5246..7cf6b6123a 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -23,7 +23,7 @@ use litebox::mm::PageManager; use litebox::platform::{ CrngProvider, PageManagementProvider, PunchthroughProvider, PunchthroughToken, RawConstPointer as _, RawMutPointer as _, RawPointerProvider, StdioProvider, - SystemInfoProvider, + SystemInfoProvider, TimeProvider, }; use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; use litebox::sync::RawSyncPrimitivesProvider; @@ -49,6 +49,7 @@ pub trait ShimPlatform: + RawPointerProvider + PageManagementProvider + SystemInfoProvider + + TimeProvider + 'static { } @@ -58,6 +59,7 @@ impl ShimPlatform for T where + RawPointerProvider + PageManagementProvider + SystemInfoProvider + + TimeProvider + 'static { } @@ -269,6 +271,7 @@ impl WindowsShimBuilder { platform: self.platform, page_manager: PageManager::new(&self.litebox), registry: syscalls::registry::RegistryStore::new(&self.litebox), + qpc_boot_instant: TimeProvider::now(self.platform), litebox: self.litebox, _fs: PhantomData, }); @@ -324,6 +327,7 @@ struct GlobalState { platform: &'static Platform, page_manager: WindowsPageManager, registry: syscalls::registry::RegistryStore, + qpc_boot_instant: ::Instant, litebox: LiteBox, _fs: PhantomData, } @@ -542,6 +546,28 @@ impl Task { let status = self.sys_nt_query_install_ui_language(install_ui_language); (status, ContinueOperation::Resume) } + SyscallRequest::NtQueryPerformanceCounter { + performance_counter, + performance_frequency, + } => { + let status = self + .sys_nt_query_performance_counter(performance_counter, performance_frequency); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { + flag, + source, + destination, + conversion_error, + } => { + let status = Self::sys_nt_convert_between_auxiliary_counter_and_performance_counter( + flag, + source, + destination, + conversion_error, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtAllocateVirtualMemory { process_handle, base_address, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index e7788a6751..82e4c7165e 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -910,9 +910,8 @@ fn map_mkdir_error(error: MkdirError) -> NtStatus { #[cfg(test)] mod tests { use super::*; - use crate::tests::{TestFS, TestPlatform}; + use crate::tests::{TestFS, TestPlatform, const_ptr, mut_ptr, null_mut_ptr}; use litebox::fs::FileSystem as _; - use zerocopy::{FromBytes, IntoBytes}; extern crate std; @@ -932,18 +931,6 @@ mod tests { const FILE_CREATE: u32 = 1; const FILE_OVERWRITE: u32 = 4; - fn const_ptr(value: &T) -> ConstPtr { - ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) - } - - fn mut_ptr(value: &mut T) -> MutPtr { - MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) - } - - fn null_mut_ptr() -> MutPtr { - MutPtr::::from_usize(0) - } - fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { let _ = crate::tests::test_platform(); ::run_test_thread(f) diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 405bee7451..c48df24255 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod file; pub(crate) mod nls; pub(crate) mod registry; +pub(crate) mod sysinfo; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; @@ -153,6 +154,16 @@ pub(crate) enum SyscallRequest { NtQueryInstallUILanguage { install_ui_language: Platform::RawMutPointer, }, + NtQueryPerformanceCounter { + performance_counter: Platform::RawMutPointer, + performance_frequency: Option>, + }, + NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { + flag: u32, + source: Platform::RawConstPointer, + destination: Platform::RawMutPointer, + conversion_error: Option>, + }, NtAllocateVirtualMemory { process_handle: ProcessHandle, base_address: Platform::RawMutPointer, @@ -254,6 +265,18 @@ impl SyscallRequest { NtSysno::NtQueryInstallUILanguage => Some(sys_req!(NtQueryInstallUILanguage { install_ui_language:*, })), + NtSysno::NtQueryPerformanceCounter => Some(sys_req!(NtQueryPerformanceCounter { + performance_counter:*, + performance_frequency:*, + })), + NtSysno::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter => Some( + sys_req!(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { + flag, + source:*, + destination:*, + conversion_error:*, + }), + ), NtSysno::NtAllocateVirtualMemory => Some(sys_req!(NtAllocateVirtualMemory { process_handle: { ProcessHandle::from_raw }, base_address:*, diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs index 83f63b5a14..d0935ee3e6 100644 --- a/litebox_shim_windows/src/syscalls/nls.rs +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -549,9 +549,9 @@ fn map_nls_read_error(error: ReadError) -> NtStatus { #[cfg(test)] mod tests { use super::*; + use crate::tests::mut_ptr; use alloc::vec; use litebox::platform::RawPointerProvider; - use zerocopy::{FromBytes, IntoBytes}; extern crate std; @@ -580,10 +580,6 @@ mod tests { fn NtQueryInstallUILanguage(install_ui_language: *mut u16) -> i32; } - fn mut_ptr(value: &mut T) -> MutPtr { - MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) - } - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] fn host_system32_file_bytes(file_name: &str) -> std::vec::Vec { std::fs::read( diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 1c20bed42b..be0de0b49f 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -839,12 +839,11 @@ fn map_read_error(error: ReadError) -> NtStatus { #[cfg(test)] mod tests { - use crate::tests::{TestFS, TestPlatform, test_platform}; + use crate::tests::{TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_platform}; use super::*; use core::mem::size_of; use litebox::LiteBox; - use zerocopy::{FromBytes, IntoBytes}; extern crate std; @@ -906,18 +905,6 @@ mod tests { fn RegDeleteTreeW(hKey: *mut core::ffi::c_void, lpSubKey: *const u16) -> i32; } - fn const_ptr(value: &T) -> ConstPtr { - ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) - } - - fn mut_ptr(value: &mut T) -> MutPtr { - MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) - } - - fn mut_byte_ptr(value: &mut T) -> MutPtr { - MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) - } - fn unicode_string(value: &[u16]) -> UnicodeString { let byte_len = u16::try_from(core::mem::size_of_val(value)).unwrap(); UnicodeString { diff --git a/litebox_shim_windows/src/syscalls/sysinfo.rs b/litebox_shim_windows/src/syscalls/sysinfo.rs new file mode 100644 index 0000000000..0c1c320189 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/sysinfo.rs @@ -0,0 +1,322 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox::platform::{Instant as _, RawConstPointer as _, RawMutPointer as _}; +use litebox_common_windows::nt_status::NtStatus; + +use crate::{ConstPtr, MutPtr, ShimFS, ShimPlatform, Task}; + +const QPC_FREQUENCY_HZ: i64 = 1_000_000_000; + +impl Task { + pub(crate) fn sys_nt_query_performance_counter( + &self, + performance_counter: MutPtr, + performance_frequency: Option>, + ) -> NtStatus { + let elapsed = self + .global + .platform + .now() + .duration_since(&self.global.qpc_boot_instant); + let ticks = duration_as_qpc_ticks(elapsed); + + if performance_counter.write_at_offset(0, ticks).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(performance_frequency) = performance_frequency + && performance_frequency + .write_at_offset(0, QPC_FREQUENCY_HZ) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + litebox_util_log::debug!( + performance_counter = ticks, + performance_frequency = QPC_FREQUENCY_HZ; + "Handled NtQueryPerformanceCounter syscall" + ); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_convert_between_auxiliary_counter_and_performance_counter( + _flag: u32, + source: ConstPtr, + _destination: MutPtr, + _conversion_error: Option>, + ) -> NtStatus { + if source.as_usize() == 0 { + return NtStatus::ACCESS_VIOLATION; + } + + // Wine reports auxiliary counter conversion as unsupported after validating the source. + NtStatus::NOT_SUPPORTED + } +} + +fn duration_as_qpc_ticks(duration: core::time::Duration) -> i64 { + i64::try_from(core::cmp::min(duration.as_nanos(), i64::MAX as u128)).unwrap_or(i64::MAX) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{const_ptr, mut_ptr, null_const_ptr, null_mut_ptr}; + use core::time::Duration; + use litebox::platform::ThreadProvider; + + extern crate std; + + const QPC_SLEEP_DURATION: Duration = Duration::from_millis(25); + const QPC_SLEEP_TOLERANCE: Duration = Duration::from_millis(10); + + type TestPlatform = crate::tests::TestPlatform; + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + unsafe extern "system" { + fn NtQueryPerformanceCounter(counter: *mut i64, frequency: *mut i64) -> i32; + + fn NtConvertBetweenAuxiliaryCounterAndPerformanceCounter( + flag: u32, + source: *const u64, + destination: *mut u64, + conversion_error: *mut u64, + ) -> i32; + } + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = crate::tests::test_platform(); + ::run_test_thread(f) + } + + fn sys_nt_convert_between_auxiliary_counter_and_performance_counter( + flag: u32, + source: ConstPtr, + destination: MutPtr, + conversion_error: Option>, + ) -> NtStatus { + Task::::sys_nt_convert_between_auxiliary_counter_and_performance_counter( + flag, + source, + destination, + conversion_error, + ) + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn host_status(status: i32) -> NtStatus { + NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) + } + + fn qpc_delta_nanos(start: i64, end: i64) -> u128 { + assert!(end >= start); + u128::try_from(end - start).unwrap() + } + + #[test] + fn nt_query_performance_counter_writes_monotonic_counter_and_frequency() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut first_counter = -1i64; + let mut second_counter = -1i64; + let mut frequency = 0i64; + + assert_eq!( + task.sys_nt_query_performance_counter( + mut_ptr(&mut first_counter), + Some(mut_ptr(&mut frequency)), + ), + NtStatus::SUCCESS + ); + assert_eq!(frequency, QPC_FREQUENCY_HZ); + assert!(first_counter >= 0); + + assert_eq!( + task.sys_nt_query_performance_counter(mut_ptr(&mut second_counter), None), + NtStatus::SUCCESS + ); + assert!(second_counter >= first_counter); + }); + } + + #[test] + fn nt_query_performance_counter_rejects_null_counter() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut frequency = 0i64; + + assert_eq!( + task.sys_nt_query_performance_counter( + null_mut_ptr(), + Some(mut_ptr(&mut frequency)), + ), + NtStatus::ACCESS_VIOLATION + ); + }); + } + + #[test] + fn nt_convert_between_auxiliary_counter_and_performance_counter_is_not_supported() { + run_with_test_platform_pointers(|| { + let source = 0u64; + let mut destination = 0u64; + let mut conversion_error = 0u64; + + assert_eq!( + sys_nt_convert_between_auxiliary_counter_and_performance_counter( + 0, + null_const_ptr(), + mut_ptr(&mut destination), + Some(mut_ptr(&mut conversion_error)), + ), + NtStatus::ACCESS_VIOLATION + ); + assert_eq!( + sys_nt_convert_between_auxiliary_counter_and_performance_counter( + 0, + const_ptr(&source), + mut_ptr(&mut destination), + Some(mut_ptr(&mut conversion_error)), + ), + NtStatus::NOT_SUPPORTED + ); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn nt_query_performance_counter_status_matches_host_ntdll() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut host_counter = 0i64; + let mut host_frequency = 0i64; + let mut guest_counter = 0i64; + let mut guest_frequency = 0i64; + + // SAFETY: This Windows-only test calls the process ntdll export with valid local + // output pointers and checks only the returned status and written scalar values. + let host_valid_status = unsafe { + host_status(NtQueryPerformanceCounter( + &raw mut host_counter, + &raw mut host_frequency, + )) + }; + let guest_status = task.sys_nt_query_performance_counter( + mut_ptr(&mut guest_counter), + Some(mut_ptr(&mut guest_frequency)), + ); + + assert_eq!(guest_status, host_valid_status); + assert!(guest_counter >= 0); + assert!(guest_frequency > 0); + assert!(host_counter >= 0); + assert!(host_frequency > 0); + + // SAFETY: Passing a null counter pointer intentionally probes host ntdll's invalid + // output behavior; the non-null frequency pointer is a valid local output. + let host_null_counter_status = unsafe { + host_status(NtQueryPerformanceCounter( + core::ptr::null_mut(), + &raw mut host_frequency, + )) + }; + let guest_null_counter_status = task.sys_nt_query_performance_counter( + null_mut_ptr(), + Some(mut_ptr(&mut guest_frequency)), + ); + assert_eq!(guest_null_counter_status, host_null_counter_status); + }); + } + + #[test] + fn nt_query_performance_counter_duration_tracks_sleep_duration() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut guest_frequency = 0i64; + let mut guest_start = 0i64; + let mut guest_end = 0i64; + + let guest_start_status = task.sys_nt_query_performance_counter( + mut_ptr(&mut guest_start), + Some(mut_ptr(&mut guest_frequency)), + ); + + std::thread::sleep(QPC_SLEEP_DURATION); + + let guest_end_status = task.sys_nt_query_performance_counter( + mut_ptr(&mut guest_end), + Some(mut_ptr(&mut guest_frequency)), + ); + + assert_eq!(guest_start_status, NtStatus::SUCCESS); + assert_eq!(guest_end_status, NtStatus::SUCCESS); + assert_eq!(guest_frequency, QPC_FREQUENCY_HZ); + + let guest_duration_nanos = qpc_delta_nanos(guest_start, guest_end); + let minimum_duration_nanos = QPC_SLEEP_DURATION + .saturating_sub(QPC_SLEEP_TOLERANCE) + .as_nanos(); + let maximum_duration_nanos = QPC_SLEEP_DURATION + .saturating_add(QPC_SLEEP_TOLERANCE) + .as_nanos(); + + assert!( + guest_duration_nanos >= minimum_duration_nanos, + "guest duration {guest_duration_nanos}ns was shorter than requested sleep minus tolerance {minimum_duration_nanos}ns", + ); + assert!( + guest_duration_nanos <= maximum_duration_nanos, + "guest duration {guest_duration_nanos}ns was longer than requested sleep plus tolerance {maximum_duration_nanos}ns", + ); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn nt_convert_between_auxiliary_counter_status_matches_host_ntdll() { + run_with_test_platform_pointers(|| { + let source = 0u64; + let mut destination = 0u64; + let mut conversion_error = 0u64; + + // SAFETY: Passing a null source pointer intentionally probes host ntdll's invalid + // input behavior; the output pointers are valid local scalars for the duration. + let host_null_source_status = unsafe { + host_status(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter( + 0, + core::ptr::null(), + &raw mut destination, + &raw mut conversion_error, + )) + }; + let guest_null_source_status = + sys_nt_convert_between_auxiliary_counter_and_performance_counter( + 0, + null_const_ptr(), + mut_ptr(&mut destination), + Some(mut_ptr(&mut conversion_error)), + ); + assert_eq!(guest_null_source_status, host_null_source_status); + + // SAFETY: All pointers passed to host ntdll point at local scalar variables that live + // for the whole call; the function does not retain them. + let host_valid_source_status = unsafe { + host_status(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter( + 0, + &raw const source, + &raw mut destination, + &raw mut conversion_error, + )) + }; + let guest_valid_source_status = + sys_nt_convert_between_auxiliary_counter_and_performance_counter( + 0, + const_ptr(&source), + mut_ptr(&mut destination), + Some(mut_ptr(&mut conversion_error)), + ); + assert_eq!(guest_valid_source_status, host_valid_source_status); + }); + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index c353df0be6..560a1260c6 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -10,10 +10,11 @@ use core::sync::atomic::{AtomicI32, AtomicU32}; use litebox::LiteBox; use litebox::fd::RawDescriptorStorage; use litebox::fs::{FileSystem as _, Mode, OFlags}; +use litebox::platform::RawConstPointer as _; use crate::{ - DefaultFS, GlobalState, Process, Task, WindowsHandleStore, WindowsNlsSectionMappings, - WindowsPageManager, + ConstPtr, DefaultFS, GlobalState, MutPtr, Process, Task, WindowsHandleStore, + WindowsNlsSectionMappings, WindowsPageManager, }; #[cfg(target_os = "linux")] @@ -22,6 +23,29 @@ pub(crate) type TestPlatform = litebox_platform_linux_userland::LinuxUserland; pub(crate) type TestPlatform = litebox_platform_windows_userland::WindowsUserland; pub(crate) type TestFS = DefaultFS; +pub(crate) fn const_ptr(value: &T) -> ConstPtr { + ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) +} + +pub(crate) fn mut_ptr( + value: &mut T, +) -> MutPtr { + MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) +} + +pub(crate) fn mut_byte_ptr(value: &mut T) -> MutPtr { + MutPtr::::from_usize(core::ptr::from_mut(value).cast::() as usize) +} + +pub(crate) fn null_const_ptr() -> ConstPtr { + ConstPtr::::from_usize(0) +} + +pub(crate) fn null_mut_ptr() -> MutPtr +{ + MutPtr::::from_usize(0) +} + pub(crate) fn test_platform() -> &'static TestPlatform { static PLATFORM: std::sync::OnceLock<&'static TestPlatform> = std::sync::OnceLock::new(); PLATFORM.get_or_init(|| { @@ -89,6 +113,7 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task Date: Fri, 12 Jun 2026 11:08:55 -0700 Subject: [PATCH 025/319] fix(loader): page-align head/tail munmap in MapMemory::reserve (cherry-pick of #891) (#908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of #891 (commit `5d4a5b7f`) onto `ulitebox`. --- **fix(loader): page-align head/tail munmap in `MapMemory::reserve`** `MapMemory::reserve` allocates an over-sized `PROT_NONE` mapping and trims the unaligned head + tail with `munmap` so the caller gets a block aligned to `align`. The tail `munmap` was passed `(aligned_ptr + len, mapping_end - end)` directly — but `len` (an ELF's `max_vaddr - min_vaddr` span) is in general **not** page-aligned, and `munmap` rejects non-page-aligned start addresses with `EINVAL`. This surfaced as `execve` → `ENOEXEC` (`exit_code: 127`) on every guest `fork+exec` of binaries whose PT_LOAD span ends mid-page — e.g. prebuilt linux-x64 node.js (`0x6403D68`). **Fix:** factor the trim arithmetic into a pure helper `compute_reserved_regions` in `litebox_common_linux::loader` that rounds the tail start up to `PAGE_SIZE` and the mapping end up to the kernel's actual page-rounded extent. Both `litebox_shim_linux` and `litebox_shim_optee` now call the helper (the bug was duplicated in both shims). Covered by new unit tests in `reserve_regions_tests` (regression case pins the EINVAL pattern). --- Verified on this branch: - `cargo test -p litebox_common_linux` → 5/5 reserve_regions tests pass. - `cargo test -p litebox_shim_optee` → 2/2 pre-existing tests pass. - `cargo clippy --all-targets -- -D warnings` clean on `litebox_common_linux`, `litebox_shim_linux` (`--features platform_linux_userland`), and `litebox_shim_optee`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_common_linux/src/loader.rs | 218 +++++++++++++++++++++++++++ litebox_shim_linux/src/loader/elf.rs | 25 +-- litebox_shim_optee/src/loader/elf.rs | 25 +-- 3 files changed, 248 insertions(+), 20 deletions(-) diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index 9a2afa9158..ceb1f25c57 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -619,6 +619,83 @@ pub trait MapMemory { -> Result<(), Self::Error>; } +/// The result of computing the head/tail trim regions for an over-sized +/// anonymous reservation made by [`MapMemory::reserve`]. +/// +/// See [`compute_reserved_regions`] for details. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ReservedRegions { + /// Base address of the requested `len` bytes inside the over-sized + /// reservation, aligned up to the `align` argument passed to + /// [`compute_reserved_regions`]. + pub aligned_ptr: usize, + /// `(start, len)` of the page-aligned head slice that should be + /// released with `munmap`, or `None` if no head trim is needed. + pub head_unmap: Option<(usize, usize)>, + /// `(start, len)` of the page-aligned tail slice that should be + /// released with `munmap`, or `None` if no tail trim is needed. + pub tail_unmap: Option<(usize, usize)>, +} + +/// Given an over-sized anonymous reservation `[mapping_ptr, mapping_ptr + +/// mapping_len)` returned by `mmap`, compute the `align`-aligned sub-range +/// of length `len` to keep, plus the page-aligned head and tail slices to +/// release with `munmap`. +/// +/// `mmap`/`munmap` operate at page granularity, so this helper is careful +/// to round both the head slice and the tail slice to whole pages: +/// +/// * `mapping_ptr` is assumed to be page-aligned (the kernel guarantees +/// this) and `align` is assumed to be a multiple of `PAGE_SIZE`, so the +/// head slice is naturally page-aligned. +/// * `len` (the caller's requested length — typically an ELF's +/// `max_vaddr - min_vaddr` span) is **not** required to be page-aligned. +/// The kernel rounds the original `mmap` allocation up to a whole number +/// of pages, so the actual mapped region extends to +/// `(mapping_ptr + mapping_len).next_multiple_of(PAGE_SIZE)`. The tail +/// slice is computed in page units: release everything from the first +/// page strictly after `aligned_ptr + len` to that page-aligned end. +/// +/// Prior to this helper, callers used `(aligned_ptr + len, mapping_end - +/// (aligned_ptr + len))` directly as the tail `munmap` args. Whenever +/// `len` ended mid-page (e.g. node.js's prebuilt linux-x64 binary has a +/// PT_LOAD span of `0x6403D68`), the kernel rejected the `munmap` with +/// `EINVAL`, surfacing as `execve` → `ENOEXEC` for any guest fork+exec +/// of node. +pub fn compute_reserved_regions( + mapping_ptr: usize, + mapping_len: usize, + len: usize, + align: usize, +) -> ReservedRegions { + let aligned_ptr = mapping_ptr.next_multiple_of(align); + let end = aligned_ptr + len; + let mapping_end = mapping_ptr + mapping_len; + // The kernel rounds the mmap allocation up to a whole number of pages, + // so the *actual* mapped region is + // `[mapping_ptr, mapping_end.next_multiple_of(PAGE_SIZE))`. + let mapping_end_aligned = mapping_end.next_multiple_of(PAGE_SIZE); + + let head_unmap = if aligned_ptr == mapping_ptr { + None + } else { + Some((mapping_ptr, aligned_ptr - mapping_ptr)) + }; + + let tail_start = end.next_multiple_of(PAGE_SIZE); + let tail_unmap = if tail_start < mapping_end_aligned { + Some((tail_start, mapping_end_aligned - tail_start)) + } else { + None + }; + + ReservedRegions { + aligned_ptr, + head_unmap, + tail_unmap, + } +} + /// Trait for reading and writing memory that has been mapped via [`MapMemory`]. pub trait AccessMemory { /// Read from memory. @@ -686,3 +763,144 @@ impl Protection { flags } } + +#[cfg(test)] +mod reserve_regions_tests { + extern crate std; + use super::{PAGE_SIZE, ReservedRegions, compute_reserved_regions}; + + /// The exact non-page-aligned PT_LOAD span observed for the prebuilt + /// linux-x64 node.js binary in the `litebox-test` Docker image, which + /// triggered the EINVAL fault on every guest fork+exec of node prior + /// to commit 05b091ba. + const NODE_LEN: usize = 0x6403D68; + + /// A non-page-aligned `mapping_len` doesn't really happen in practice + /// (callers always pass `len + (align.max(PAGE_SIZE) - PAGE_SIZE)`), + /// but we test the helper's tolerance to it anyway, because the kernel + /// rounds up to whole pages and so should we. + fn assert_page_aligned(regions: &ReservedRegions) { + if let Some((addr, size)) = regions.head_unmap { + assert_eq!(addr % PAGE_SIZE, 0, "head start not page-aligned"); + assert_eq!(size % PAGE_SIZE, 0, "head size not page-aligned"); + } + if let Some((addr, size)) = regions.tail_unmap { + assert_eq!(addr % PAGE_SIZE, 0, "tail start not page-aligned"); + assert_eq!(size % PAGE_SIZE, 0, "tail size not page-aligned"); + } + } + + /// Reservation matches request exactly (`align == PAGE_SIZE`): no + /// head or tail trim needed when `len` is a page multiple. + #[test] + fn page_aligned_len_no_trim() { + let mapping_ptr = 0x4000_0000; + let len = 0x10_0000; // 1 MiB, page-aligned + let align = PAGE_SIZE; + let mapping_len = len + (align.max(PAGE_SIZE) - PAGE_SIZE); + let r = compute_reserved_regions(mapping_ptr, mapping_len, len, align); + assert_eq!(r.aligned_ptr, mapping_ptr); + assert_eq!(r.head_unmap, None); + assert_eq!(r.tail_unmap, None); + assert_page_aligned(&r); + } + + /// Larger `align` than PAGE_SIZE: head trim happens when `mapping_ptr` + /// isn't already aligned to `align`; tail trim mirrors the slack. + #[test] + fn larger_align_trims_head_and_tail() { + let align = 0x10_0000; // 1 MiB + let len = 0x1234_0000; // page-aligned + let mapping_len = len + (align - PAGE_SIZE); + // mapping_ptr page-aligned but not align-aligned. + let mapping_ptr = 0x4000_0000 + PAGE_SIZE; + let r = compute_reserved_regions(mapping_ptr, mapping_len, len, align); + assert_eq!(r.aligned_ptr % align, 0); + assert!(r.aligned_ptr >= mapping_ptr); + assert!(r.aligned_ptr + len <= mapping_ptr + mapping_len); + // Total trimmed = (align - PAGE_SIZE). + let head = r.head_unmap.map_or(0, |(_, s)| s); + let tail = r.tail_unmap.map_or(0, |(_, s)| s); + assert_eq!(head + tail, align - PAGE_SIZE); + assert_page_aligned(&r); + } + + /// With `align == PAGE_SIZE` the over-allocation slack is zero so the + /// old formula's `if end != mapping_end` check happened to skip the + /// `munmap` entirely — even though `end` was non-page-aligned. The new + /// helper reaches the same "no tail trim" conclusion the right way: + /// `tail_start = end.next_multiple_of(PAGE_SIZE)` equals the + /// page-rounded mapping end. + #[test] + fn node_align_page_size_no_tail_trim_needed() { + let mapping_ptr = 0x4000_0000; + let align = PAGE_SIZE; + let len = NODE_LEN; + let mapping_len = len + (align.max(PAGE_SIZE) - PAGE_SIZE); + let r = compute_reserved_regions(mapping_ptr, mapping_len, len, align); + assert_eq!(r.aligned_ptr, mapping_ptr); + assert_eq!(r.head_unmap, None); + assert_eq!(r.tail_unmap, None); + assert_page_aligned(&r); + } + + /// Stronger version of the node case: non-page-aligned `len` with a + /// larger `align`, so the trailing slack actually does require a tail + /// `munmap`. Under the old formula, the tail munmap start was + /// `aligned_ptr + len` (non-page-aligned) and the kernel rejected it. + /// Under the helper, the tail start is rounded up to the next page. + #[test] + fn non_page_aligned_len_with_large_align_trims_page_aligned_tail() { + let align = 0x20_0000_usize; // 2 MiB + let len = NODE_LEN; // ends at 0xD68 within a page + let mapping_len = len + (align - PAGE_SIZE); + let mapping_ptr = 0x4000_0000_usize; // page-aligned but not 2 MiB-aligned + + // Old formula tail args. + let old_aligned_ptr = mapping_ptr.next_multiple_of(align); + let old_end = old_aligned_ptr + len; + let old_mapping_end = mapping_ptr + mapping_len; + let old_tail_size = old_mapping_end - old_end; + assert_ne!( + old_end % PAGE_SIZE, + 0, + "old tail start would be non-page-aligned (the EINVAL trigger)", + ); + assert_eq!( + old_tail_size % PAGE_SIZE, + 0, + "old tail size happened to be page-aligned", + ); + + let r = compute_reserved_regions(mapping_ptr, mapping_len, len, align); + assert_eq!(r.aligned_ptr, old_aligned_ptr); + let (tail_start, tail_size) = r.tail_unmap.expect("tail trim expected with large align"); + // Tail covers everything from the page after the requested end to + // the page-rounded end of the actual reservation. + let page_end = (r.aligned_ptr + len).next_multiple_of(PAGE_SIZE); + let mapping_end_aligned = old_mapping_end.next_multiple_of(PAGE_SIZE); + assert_eq!(tail_start, page_end); + assert_eq!(tail_size, mapping_end_aligned - tail_start); + // The page that contains the last byte of the reserved range stays + // mapped (the caller still owns up to byte `aligned_ptr + len`). + assert!(tail_start >= r.aligned_ptr + len); + assert_page_aligned(&r); + } + + /// Head and tail trim sizes together exhaust the over-allocation slack. + #[test] + fn head_plus_tail_equals_slack_when_len_page_aligned() { + let align = 0x40_0000; // 4 MiB + let page_aligned_len = 0x80_0000; + let mapping_len = page_aligned_len + (align - PAGE_SIZE); + for offset_pages in 0..8 { + let mapping_ptr = 0x4000_0000 + offset_pages * PAGE_SIZE; + let r = compute_reserved_regions(mapping_ptr, mapping_len, page_aligned_len, align); + assert_eq!(r.aligned_ptr % align, 0); + let head = r.head_unmap.map_or(0, |(_, s)| s); + let tail = r.tail_unmap.map_or(0, |(_, s)| s); + assert_eq!(head + tail, align - PAGE_SIZE); + assert_page_aligned(&r); + } + } +} diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index b7c66b2252..d867676a84 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -88,18 +88,23 @@ impl litebox_common_linux::loader::MapMemory for ElfFile<'_, FS> { )? .as_usize(); - let ptr = mapping_ptr.next_multiple_of(align); - let end = ptr + len; - let mapping_end = mapping_ptr + mapping_len; - if ptr != mapping_ptr { - self.task - .sys_munmap(MutPtr::from_usize(mapping_ptr), ptr - mapping_ptr)?; + // See `compute_reserved_regions` for why the trim regions must be + // computed in page units: `len` (an ELF's `max_vaddr - min_vaddr` + // span) is in general not page-aligned, and `munmap` rejects + // non-page-aligned start addresses with EINVAL. + let regions = litebox_common_linux::loader::compute_reserved_regions( + mapping_ptr, + mapping_len, + len, + align, + ); + if let Some((addr, size)) = regions.head_unmap { + self.task.sys_munmap(MutPtr::from_usize(addr), size)?; } - if end != mapping_end { - self.task - .sys_munmap(MutPtr::from_usize(end), mapping_end - end)?; + if let Some((addr, size)) = regions.tail_unmap { + self.task.sys_munmap(MutPtr::from_usize(addr), size)?; } - Ok(ptr) + Ok(regions.aligned_ptr) } fn map_file( diff --git a/litebox_shim_optee/src/loader/elf.rs b/litebox_shim_optee/src/loader/elf.rs index 2cfe831678..f5b685919c 100644 --- a/litebox_shim_optee/src/loader/elf.rs +++ b/litebox_shim_optee/src/loader/elf.rs @@ -91,18 +91,23 @@ impl litebox_common_linux::loader::MapMemory for ElfFileInMemory<'_> { )? .as_usize(); - let ptr = mapping_ptr.next_multiple_of(align); - let end = ptr + len; - let mapping_end = mapping_ptr + mapping_len; - if ptr != mapping_ptr { - self.task - .sys_munmap(MutPtr::from_usize(mapping_ptr), ptr - mapping_ptr)?; + // See `compute_reserved_regions` for why the trim regions must be + // computed in page units: `len` (an ELF's `max_vaddr - min_vaddr` + // span) is in general not page-aligned, and `munmap` rejects + // non-page-aligned start addresses with EINVAL. + let regions = litebox_common_linux::loader::compute_reserved_regions( + mapping_ptr, + mapping_len, + len, + align, + ); + if let Some((addr, size)) = regions.head_unmap { + self.task.sys_munmap(MutPtr::from_usize(addr), size)?; } - if end != mapping_end { - self.task - .sys_munmap(MutPtr::from_usize(end), mapping_end - end)?; + if let Some((addr, size)) = regions.tail_unmap { + self.task.sys_munmap(MutPtr::from_usize(addr), size)?; } - Ok(ptr) + Ok(regions.aligned_ptr) } /// This function imitates file-based mapping by using the in-memory ELF file. From 84cee321cb15b6d9fef8010a7b7a59879118de68 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 12 Jun 2026 13:50:41 -0700 Subject: [PATCH 026/319] Add Windows virtual memory syscall (#909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR implements handling for `NtAllocateVirtualMemory`, `NtAllocateVirtualMemoryEx`, `NtFreeVirtualMemory`, `NtProtectVirtualMemory`, and `NtQueryVirtualMemory`. For allocation, the shim creates the requested virtual address range in the page manager, then records it in the per-process `WindowsVirtualAllocations` table. A pure `MEM_RESERVE` creates the backing reserved range with no guest-accessible permissions and inserts an allocation entry whose committed-page `RangeMap` is empty. `MEM_RESERVE | MEM_COMMIT` creates the same allocation entry but also records the committed page range and its protection in the `RangeMap`. A later `MEM_COMMIT` into an existing reservation updates that existing allocation’s committed-page tracking rather than creating a new allocation, so reservation ownership and committed page state stay separate. --- Cargo.lock | 1 + litebox_common_windows/src/loader.rs | 1 + litebox_common_windows/src/nt_status.rs | 4 + litebox_shim_windows/Cargo.toml | 1 + litebox_shim_windows/src/lib.rs | 103 +- litebox_shim_windows/src/loader/pe.rs | 92 +- litebox_shim_windows/src/syscalls/mm.rs | 2847 ++++++++++++++++++++++ litebox_shim_windows/src/syscalls/mod.rs | 66 +- litebox_shim_windows/src/tests.rs | 3 + 9 files changed, 3094 insertions(+), 24 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/mm.rs diff --git a/Cargo.lock b/Cargo.lock index 74a8729d4a..ccb9a483db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,6 +1787,7 @@ dependencies = [ "litebox_platform_linux_userland", "litebox_platform_windows_userland", "litebox_util_log", + "rangemap", "thiserror", "zerocopy", ] diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs index af8a308df8..73aec1eaf0 100644 --- a/litebox_common_windows/src/loader.rs +++ b/litebox_common_windows/src/loader.rs @@ -50,6 +50,7 @@ pub struct PeImageInfo { } /// Information about the mapped PE image. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct MappingInfo { pub base_addr: usize, pub image_size: usize, diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index 0a91d4b069..66fd022a52 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -117,6 +117,7 @@ impl NtStatus { 0xC0000014 => "STATUS_UNRECOGNIZED_MEDIA: Unrecognized media", 0xC0000016 => "STATUS_MORE_PROCESSING_REQUIRED: More processing required", 0xC0000017 => "STATUS_NO_MEMORY: Insufficient memory", + 0xC0000018 => "STATUS_CONFLICTING_ADDRESSES: Conflicting addresses", 0xC0000019 => "STATUS_NOT_MAPPED_VIEW: Not mapped view", 0xC000001A => "STATUS_UNABLE_TO_FREE_VM: Unable to free virtual memory", 0xC000001B => "STATUS_UNABLE_TO_DELETE_SECTION: Unable to delete section", @@ -311,6 +312,9 @@ impl NtStatus { /// STATUS_NO_MEMORY pub const NO_MEMORY: Self = Self::from_raw(0xC0000017); + /// STATUS_CONFLICTING_ADDRESSES + pub const CONFLICTING_ADDRESSES: Self = Self::from_raw(0xC0000018); + /// STATUS_NOT_MAPPED_VIEW pub const NOT_MAPPED_VIEW: Self = Self::from_raw(0xC0000019); diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml index fbae828f9a..3a1c96d41e 100644 --- a/litebox_shim_windows/Cargo.toml +++ b/litebox_shim_windows/Cargo.toml @@ -10,6 +10,7 @@ litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } litebox_common_windows = { path = "../litebox_common_windows/", version = "0.1.0" } litebox_util_log = { path = "../litebox_util_log", version = "0.1.0" } +rangemap = { version = "1.5.1", features = ["const_fn"] } thiserror = { version = "2.0.6", default-features = false } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 7cf6b6123a..a41da96c5d 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -30,9 +30,9 @@ use litebox::sync::RawSyncPrimitivesProvider; use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; -use crate::syscalls::SyscallRequest; use crate::syscalls::file::{FileObject, FileObjectSubsystem}; use crate::syscalls::registry::{RegistryKeyObject, RegistryKeySubsystem}; +use crate::syscalls::{SyscallRequest, mm}; mod loader; mod nt_types; @@ -73,6 +73,17 @@ pub(crate) type WindowsHandleStore = litebox::sync::RwLock; pub(crate) type WindowsNlsSectionMappings = litebox::sync::RwLock>; +pub(crate) type WindowsVirtualAllocations = + litebox::sync::RwLock>; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct WindowsVirtualAllocation { + pub(crate) base: usize, + pub(crate) size: usize, + pub(crate) allocation_protect: syscalls::mm::PageProtection, + pub(crate) type_: syscalls::mm::MemoryType, + pub(crate) pages: rangemap::RangeMap, +} pub type DefaultFS = WindowsFS; @@ -299,6 +310,7 @@ impl WindowsShim { peb_address: load_info.environment.peb, handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), + virtual_allocations: load_info.virtual_allocations, system_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), user_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), user_ui_language: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), @@ -338,6 +350,7 @@ pub struct Process { peb_address: usize, handles: WindowsHandleStore, nls_section_mappings: WindowsNlsSectionMappings, + virtual_allocations: WindowsVirtualAllocations, system_lcid: AtomicU32, user_lcid: AtomicU32, user_ui_language: AtomicU32, @@ -576,17 +589,85 @@ impl Task { allocation_type, protect, } => { - // TODO: placeholder for NtAllocateVirtualMemory - litebox_util_log::debug!( - process_handle:% = format_args!("{:#x}", process_handle.as_raw()), - base_address:% = format_args!("{:#x}", base_address.as_usize()), - zero_bits:% = format_args!("{:#x}", zero_bits), - region_size:% = format_args!("{:#x}", region_size.as_usize()), - allocation_type:% = format_args!("{:#x}", allocation_type), - protect:% = format_args!("{:#x}", protect); - "Handling NtAllocateVirtualMemory syscall" + let status = self.sys_nt_allocate_virtual_memory( + process_handle, + base_address, + zero_bits, + region_size, + allocation_type, + protect, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtAllocateVirtualMemoryEx { + process_handle, + base_address, + region_size, + allocation_type, + protect, + extended_parameters, + extended_parameter_count, + } => { + let status = self.sys_nt_allocate_virtual_memory_ex( + process_handle, + base_address, + region_size, + allocation_type, + protect, + mm::MemoryExtendedParameters { + parameters: extended_parameters, + count: extended_parameter_count, + }, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtFreeVirtualMemory { + process_handle, + base_address, + region_size, + free_type, + } => { + let status = self.sys_nt_free_virtual_memory( + process_handle, + base_address, + region_size, + free_type, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtProtectVirtualMemory { + process_handle, + base_address, + region_size, + new_protect, + old_protect, + } => { + let status = self.sys_nt_protect_virtual_memory( + process_handle, + base_address, + region_size, + new_protect, + old_protect, ); - (NtStatus::UNSUCCESSFUL, ContinueOperation::Terminate) + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryVirtualMemory { + process_handle, + base_address, + memory_information_class, + memory_information, + memory_information_length, + return_length, + } => { + let status = self.sys_nt_query_virtual_memory( + process_handle, + base_address, + memory_information_class, + memory_information, + memory_information_length, + return_length, + ); + (status, ContinueOperation::Resume) } SyscallRequest::NtTerminateProcess { process_handle, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 5869d1cc19..daef241b1d 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use alloc::collections::btree_map::BTreeMap; use alloc::{string::String, sync::Arc, vec::Vec}; use core::marker::PhantomData; use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; @@ -17,6 +18,7 @@ use litebox_common_windows::loader::{ MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, MapMemory, MappingInfo, PAGE_SIZE, PeExportError, PeLoadError, PeParseError, PeParsedFile, Protection, ReadAt, page_align_down, }; +use rangemap::RangeMap; use thiserror::Error; use zerocopy::{FromZeros, IntoBytes}; @@ -25,6 +27,7 @@ use crate::nt_types::{ ClientId, PebBitField, ProcessEnvironmentBlock, RtlUserProcFlags, RtlUserProcessParameters, ThreadEnvironmentBlock, UnicodeString, X64Context, }; +use crate::syscalls::mm::{MemoryType, PageProtection}; const NTDLL_WRITABLE_SECTIONS: &[&[u8]] = &[b".mrdata"]; const NTDLL_PATHS: &[&str] = &["/Windows/System32/ntdll.dll", "/windows/system32/ntdll.dll"]; @@ -58,10 +61,11 @@ pub(crate) struct WindowsProcessEnvironment { pub(crate) context: usize, } -pub(crate) struct PeLoadInfo { +pub(crate) struct PeLoadInfo { pub(crate) entry_point: usize, pub(crate) stack_top: usize, pub(crate) ntdll_mapping: Option, + pub(crate) virtual_allocations: crate::WindowsVirtualAllocations, pub(crate) environment: WindowsProcessEnvironment, } @@ -84,7 +88,7 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { } } - pub(crate) fn load(&self, path: &str) -> Result { + pub(crate) fn load(&self, path: &str) -> Result, WindowsLoadError> { let image = load_image(self.platform, self.fs.clone(), path, self.page_manager)?; let application_entry_point = image.mapping.entry_point; let ntdll = load_ntdll( @@ -142,10 +146,22 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { .ok_or(PeImageAccessError::MemoryAccess)?; } + let virtual_allocations = + crate::WindowsVirtualAllocations::::new(BTreeMap::new()); + register_image_virtual_allocation(&virtual_allocations, image.mapping, image.pages); + let ntdll_mapping = if let Some(ntdll) = ntdll { + let mapping = ntdll.image.mapping; + register_image_virtual_allocation(&virtual_allocations, mapping, ntdll.image.pages); + Some(mapping) + } else { + None + }; + Ok(PeLoadInfo { entry_point, stack_top, - ntdll_mapping: ntdll.map(|ntdll| ntdll.image.mapping), + ntdll_mapping, + virtual_allocations, environment, }) } @@ -354,8 +370,26 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { } } +fn register_image_virtual_allocation( + virtual_allocations: &crate::WindowsVirtualAllocations, + mapping: MappingInfo, + pages: RangeMap, +) { + virtual_allocations.write().insert( + mapping.base_addr, + crate::WindowsVirtualAllocation { + base: mapping.base_addr, + size: mapping.image_size, + allocation_protect: PageProtection::PAGE_EXECUTE_WRITECOPY, + type_: MemoryType::MEM_IMAGE, + pages, + }, + ); +} + struct LoadedImage { mapping: MappingInfo, + pages: RangeMap, parsed: PeParsedFile, } @@ -459,12 +493,17 @@ fn load_image_with_writable_sections( file: &file, page_manager, chunk: alloc::vec![0u8; FILE_CHUNK_BYTES], + pages: RangeMap::new(), }; let mut memory = PeImageMemory::(PhantomData); let mapping = parsed .load_with_writable_sections(&mut mapper, &mut memory, writable_section_names) .map_err(WindowsLoadError::Load)?; - Ok(LoadedImage { mapping, parsed }) + Ok(LoadedImage { + mapping, + pages: mapper.pages, + parsed, + }) } fn ntdll_exports( @@ -609,6 +648,26 @@ struct PeImageMapper<'a, Platform: crate::ShimPlatform, FS: ShimFS> { page_manager: &'a crate::WindowsPageManager, /// Reusable per-call I/O staging buffer for [`MapMemory::map_file`]. chunk: Vec, + pages: RangeMap, +} + +impl PeImageMapper<'_, Platform, FS> { + fn record_pages( + &mut self, + address: usize, + len: usize, + protect: PageProtection, + ) -> Result<(), PeImageAccessError> { + let (start, len) = page_range(address, len)?; + if len == 0 { + return Ok(()); + } + let end = start + .checked_add(len) + .ok_or(PeImageAccessError::AddressOverflow)?; + self.pages.insert(start..end, protect); + Ok(()) + } } impl MapMemory for PeImageMapper<'_, Platform, FS> { @@ -638,7 +697,9 @@ impl MapMemory for PeImageMapper<'_, |_| Ok(0), )? }; - Ok(ptr.as_usize()) + let base = ptr.as_usize(); + self.record_pages(base, len, PageProtection::PAGE_NOACCESS)?; + Ok(base) } fn map_zero( @@ -656,7 +717,8 @@ impl MapMemory for PeImageMapper<'_, .ok_or(PeImageAccessError::MemoryAccess)?; written += chunk; } - protect_pages(self.page_manager, address, len, *prot) + protect_pages(self.page_manager, address, len, *prot)?; + self.record_pages(address, len, page_protection_from_loader_protection(*prot)) } fn map_file( @@ -685,7 +747,8 @@ impl MapMemory for PeImageMapper<'_, .ok_or(PeImageAccessError::MemoryAccess)?; read += n; } - protect_pages(self.page_manager, address, len, *prot) + protect_pages(self.page_manager, address, len, *prot)?; + self.record_pages(address, len, page_protection_from_loader_protection(*prot)) } fn protect( @@ -694,7 +757,19 @@ impl MapMemory for PeImageMapper<'_, len: usize, prot: &Protection, ) -> Result<(), Self::Error> { - protect_pages(self.page_manager, address, len, *prot) + protect_pages(self.page_manager, address, len, *prot)?; + self.record_pages(address, len, page_protection_from_loader_protection(*prot)) + } +} + +fn page_protection_from_loader_protection(protect: Protection) -> PageProtection { + match (protect.read, protect.write, protect.execute) { + (_, true, true) => PageProtection::PAGE_EXECUTE_READWRITE, + (_, true, false) => PageProtection::PAGE_READWRITE, + (true, false, true) => PageProtection::PAGE_EXECUTE_READ, + (false, false, true) => PageProtection::PAGE_EXECUTE, + (true, false, false) => PageProtection::PAGE_READONLY, + (false, false, false) => PageProtection::PAGE_NOACCESS, } } @@ -1574,6 +1649,7 @@ mod tests { image_size: parsed.image_size(), entry_point: base_addr, }, + pages: RangeMap::new(), parsed, } } diff --git a/litebox_shim_windows/src/syscalls/mm.rs b/litebox_shim_windows/src/syscalls/mm.rs new file mode 100644 index 0000000000..269b233d3f --- /dev/null +++ b/litebox_shim_windows/src/syscalls/mm.rs @@ -0,0 +1,2847 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::mem::size_of; + +use int_enum::IntEnum; +use litebox::mm::linux::{CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize}; +use litebox::platform::page_mgmt::{AllocationError, MemoryRegionPermissions}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox_common_windows::nt_status::NtStatus; +use rangemap::RangeMap; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::syscalls::ProcessHandle; +use crate::{ + ConstPtr, MutPtr, PAGE_SIZE, ShimFS, ShimPlatform, Task, WindowsPageManager, + WindowsVirtualAllocation, WindowsVirtualAllocations, +}; + +const ALLOCATION_GRANULARITY: usize = 0x1_0000; +const ALLOCATION_SEARCH_ATTEMPTS: usize = 8; +const MEMORY_WORKING_SET_LIST_MIN_SIZE: usize = 16; +const MEM_EXTENDED_PARAMETER_TYPE_MASK: u64 = 0xff; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct PageProtection: u32 { + const PAGE_NOACCESS = 0x01; + const PAGE_READONLY = 0x02; + const PAGE_READWRITE = 0x04; + const PAGE_WRITECOPY = 0x08; + const PAGE_EXECUTE = 0x10; + const PAGE_EXECUTE_READ = 0x20; + const PAGE_EXECUTE_READWRITE = 0x40; + const PAGE_EXECUTE_WRITECOPY = 0x80; + const PAGE_GUARD = 0x100; + const PAGE_NOCACHE = 0x200; + const PAGE_WRITECOMBINE = 0x400; + } +} + +impl PageProtection { + const BASE_MASK: u32 = 0xff; + + fn base(self) -> u32 { + self.bits() & Self::BASE_MASK + } + + fn has_valid_modifier_combination(self) -> bool { + let noaccess = self.base() == Self::PAGE_NOACCESS.bits(); + let guard = self.contains(Self::PAGE_GUARD); + let nocache = self.contains(Self::PAGE_NOCACHE); + let writecombine = self.contains(Self::PAGE_WRITECOMBINE); + + !(noaccess && (guard || nocache || writecombine) + || guard && (nocache || writecombine) + || nocache && writecombine) + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct AllocationType: u32 { + const MEM_COMMIT = 0x1000; + const MEM_RESERVE = 0x2000; + const MEM_RESET = 0x80000; + const MEM_TOP_DOWN = 0x100000; + const MEM_WRITE_WATCH = 0x200000; + const MEM_PHYSICAL = 0x400000; + const MEM_RESET_UNDO = 0x1000000; + const MEM_LARGE_PAGES = 0x20000000; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct FreeType: u32 { + const MEM_COALESCE_PLACEHOLDERS = 0x1; + const MEM_PRESERVE_PLACEHOLDER = 0x2; + const MEM_DECOMMIT = 0x4000; + const MEM_RELEASE = 0x8000; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct MemoryState: u32 { + const MEM_COMMIT = 0x1000; + const MEM_RESERVE = 0x2000; + const MEM_FREE = 0x10000; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct MemoryType: u32 { + const MEM_PRIVATE = 0x20000; + const MEM_MAPPED = 0x40000; + const MEM_IMAGE = 0x1000000; + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +enum MemoryInformationClass { + Basic = 0, + WorkingSetList = 4, + Image = 6, + ImageExtension = 14, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromBytes, Immutable, IntoBytes)] +struct MemoryImageInformation { + image_base: usize, + size_of_image: usize, + image_flags: u32, + _padding: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, FromBytes, Immutable, IntoBytes)] +struct MemoryImageExtensionInformation { + extension_type: u32, + flags: u32, + extension_image_base_rva: usize, + extension_size: usize, +} + +#[repr(u64)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +enum MemoryExtendedParameterType { + AddressRequirements = 1, + NumaNode = 2, + AttributeFlags = 5, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromBytes, Immutable, IntoBytes)] +struct MemoryBasicInformation { + base_address: usize, + allocation_base: usize, + allocation_protect: u32, + partition_id: u16, + _padding0: u16, + region_size: usize, + state: u32, + protect: u32, + type_: u32, + _padding1: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +pub(crate) struct MemoryExtendedParameter { + type_: u64, + value: usize, +} + +pub(crate) struct MemoryExtendedParameters { + pub(crate) parameters: Option>, + pub(crate) count: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct MemoryAddressRequirements { + lowest_starting_address: usize, + highest_ending_address: usize, + alignment: usize, +} + +fn validate_memory_extended_parameters( + extended_parameters: MemoryExtendedParameters, +) -> Result<(), NtStatus> { + if extended_parameters.count == 0 { + return Ok(()); + } + + let Some(parameters) = extended_parameters.parameters else { + return Err(NtStatus::INVALID_PARAMETER); + }; + + let mut present = 0u32; + for index in 0..extended_parameters.count { + let parameter = parameters + .read_at_offset(index.try_into().map_err(|_| NtStatus::INVALID_PARAMETER)?) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + validate_memory_extended_parameter::(parameter, &mut present)?; + } + + Ok(()) +} + +fn validate_memory_extended_parameter( + parameter: MemoryExtendedParameter, + present: &mut u32, +) -> Result<(), NtStatus> { + if parameter.type_ & !MEM_EXTENDED_PARAMETER_TYPE_MASK != 0 { + return Err(NtStatus::INVALID_PARAMETER); + } + + let parameter_type_raw = parameter.type_ & MEM_EXTENDED_PARAMETER_TYPE_MASK; + let parameter_type = MemoryExtendedParameterType::try_from(parameter_type_raw) + .map_err(|_| NtStatus::INVALID_PARAMETER)?; + let parameter_bit = u32::try_from(parameter_type_raw) + .ok() + .and_then(|parameter_type| 1u32.checked_shl(parameter_type)) + .ok_or(NtStatus::INVALID_PARAMETER)?; + if *present & parameter_bit != 0 { + return Err(NtStatus::INVALID_PARAMETER); + } + *present |= parameter_bit; + + match parameter_type { + MemoryExtendedParameterType::AddressRequirements => { + let address_requirements = + ConstPtr::::from_usize(parameter.value) + .read_at_offset(0) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + if address_requirements.lowest_starting_address != 0 + || address_requirements.highest_ending_address != 0 + || !matches!(address_requirements.alignment, 0 | ALLOCATION_GRANULARITY) + { + return Err(NtStatus::INVALID_PARAMETER); + } + Ok(()) + } + MemoryExtendedParameterType::NumaNode => Ok(()), + MemoryExtendedParameterType::AttributeFlags => { + if parameter.value == 0 { + Ok(()) + } else { + Err(NtStatus::INVALID_PARAMETER) + } + } + } +} + +impl Task { + pub(crate) fn sys_nt_allocate_virtual_memory_ex( + &self, + process_handle: ProcessHandle, + base_address: MutPtr, + region_size: MutPtr, + allocation_type: u32, + protect: u32, + extended_parameters: MemoryExtendedParameters, + ) -> NtStatus { + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + + if let Err(status) = validate_memory_extended_parameters::(extended_parameters) { + return status; + } + + // TODO: Apply supported extended parameters (especially MEM_ADDRESS_REQUIREMENTS) to the + // allocation search once PageManager can honor caller-specified placement constraints. + self.sys_nt_allocate_virtual_memory( + process_handle, + base_address, + 0, + region_size, + allocation_type, + protect, + ) + } + + pub(crate) fn sys_nt_allocate_virtual_memory( + &self, + process_handle: ProcessHandle, + base_address: MutPtr, + zero_bits: usize, + region_size: MutPtr, + allocation_type: u32, + protect: u32, + ) -> NtStatus { + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + let Some(base) = base_address.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let Some(size) = region_size.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + if base_address.write_at_offset(0, base).is_none() + || region_size.write_at_offset(0, size).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + let Some(allocation_type) = AllocationType::from_bits(allocation_type) else { + return NtStatus::INVALID_PARAMETER; + }; + let supported_allocation_types = AllocationType::MEM_COMMIT + | AllocationType::MEM_RESERVE + | AllocationType::MEM_RESET + | AllocationType::MEM_TOP_DOWN; + if size == 0 + || !supported_allocation_types.contains(allocation_type) + || (zero_bits > 21 && zero_bits < 32) + || (zero_bits != 0 && base != 0) + { + return NtStatus::INVALID_PARAMETER; + } + if allocation_type.contains(AllocationType::MEM_RESET) { + if allocation_type != AllocationType::MEM_RESET { + return NtStatus::INVALID_PARAMETER; + } + return self.reset_virtual_memory(base, size, protect, base_address, region_size); + } + if !allocation_type.intersects(AllocationType::MEM_COMMIT | AllocationType::MEM_RESERVE) { + return NtStatus::INVALID_PARAMETER; + } + + let new_allocation = base == 0 || allocation_type.contains(AllocationType::MEM_RESERVE); + let Some((aligned_base, aligned_len)) = (if new_allocation { + reserve_allocation_region(base, size) + } else { + page_aligned_region(base, size) + }) else { + return NtStatus::INVALID_PARAMETER; + }; + let Some((protect, permissions)) = parse_page_protection(protect) else { + return NtStatus::INVALID_PAGE_PROTECTION; + }; + + if !new_allocation { + return self.commit_existing_virtual_memory( + aligned_base, + aligned_len, + protect, + permissions, + base_address, + region_size, + ); + } + + let Some(length) = NonZeroPageSize::new(aligned_len) else { + return NtStatus::INVALID_PARAMETER; + }; + let initial_permissions = if allocation_type.contains(AllocationType::MEM_COMMIT) { + permissions + } else { + MemoryRegionPermissions::empty() + }; + let top_down = allocation_type.contains(AllocationType::MEM_TOP_DOWN); + let allocation = if base == 0 { + create_allocation_granularity_aligned_pages::( + &self.global.page_manager, + length, + initial_permissions, + zero_bits, + top_down, + ) + } else { + create_pages::( + &self.global.page_manager, + NonZeroAddress::new(aligned_base), + length, + CreatePagesFlags::FIXED_ADDR | CreatePagesFlags::NOREPLACE, + initial_permissions, + |_| Ok(0), + ) + .map_err(mapping_error_to_nt_status) + }; + let ptr = match allocation { + Ok(ptr) => ptr, + Err(status) => return status, + }; + + if base_address.write_at_offset(0, ptr.as_usize()).is_none() + || region_size.write_at_offset(0, aligned_len).is_none() + { + let ptr = MutPtr::::from_usize(ptr.as_usize()); + // SAFETY: The mapping was just created by this syscall and has not been published in + // the allocation table. Removing it rolls back failed output writeback. + let _ = unsafe { self.global.page_manager.remove_pages(ptr, aligned_len) }; + return NtStatus::ACCESS_VIOLATION; + } + self.process.virtual_allocations.write().insert( + ptr.as_usize(), + WindowsVirtualAllocation { + base: ptr.as_usize(), + size: aligned_len, + allocation_protect: protect, + type_: MemoryType::MEM_PRIVATE, + pages: if allocation_type.contains(AllocationType::MEM_COMMIT) { + committed_pages(ptr.as_usize(), aligned_len, protect) + } else { + RangeMap::new() + }, + }, + ); + + litebox_util_log::debug!( + base:% = format_args!("{:#x}", base), + aligned_base:% = format_args!("{:#x}", ptr.as_usize()), + aligned_len, + allocation_type:% = format_args!("{:#x}", allocation_type.bits()), + protect:% = format_args!("{:#x}", protect.bits()); + "Handled NtAllocateVirtualMemory syscall" + ); + NtStatus::SUCCESS + } + + fn reset_virtual_memory( + &self, + base: usize, + size: usize, + protect: u32, + base_address: MutPtr, + region_size: MutPtr, + ) -> NtStatus { + if parse_page_protection(protect).is_none() { + return NtStatus::INVALID_PAGE_PROTECTION; + } + let Some((aligned_base, aligned_len)) = page_aligned_region(base, size) else { + return NtStatus::INVALID_PARAMETER; + }; + let Some(allocation) = + find_virtual_allocation(&self.process.virtual_allocations, aligned_base, aligned_len) + else { + return NtStatus::INVALID_PARAMETER; + }; + if allocation.type_ != MemoryType::MEM_PRIVATE { + return NtStatus::INVALID_PARAMETER; + } + if !matches!( + scan_allocation_pages(&allocation, aligned_base, aligned_len), + Some(PageRangeScan::FullyCommitted(_)) + ) { + return NtStatus::CONFLICTING_ADDRESSES; + } + + if base_address.write_at_offset(0, aligned_base).is_none() + || region_size.write_at_offset(0, aligned_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + fn commit_existing_virtual_memory( + &self, + aligned_base: usize, + aligned_len: usize, + protect: PageProtection, + permissions: MemoryRegionPermissions, + base_address: MutPtr, + region_size: MutPtr, + ) -> NtStatus { + if find_private_virtual_allocation( + &self.process.virtual_allocations, + aligned_base, + aligned_len, + ) + .is_none() + { + return NtStatus::INVALID_PARAMETER; + } + if update_permissions( + &self.global.page_manager, + aligned_base, + aligned_len, + permissions, + ) + .is_err() + { + return NtStatus::INVALID_PARAMETER; + } + if base_address.write_at_offset(0, aligned_base).is_none() + || region_size.write_at_offset(0, aligned_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + set_committed_pages_protect( + &self.process.virtual_allocations, + aligned_base, + aligned_len, + protect, + ); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_free_virtual_memory( + &self, + process_handle: ProcessHandle, + base_address: MutPtr, + region_size: MutPtr, + free_type: u32, + ) -> NtStatus { + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + + let Some(base) = base_address.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let Some(size) = region_size.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + if base_address.write_at_offset(0, base).is_none() + || region_size.write_at_offset(0, size).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + let Some(free_type) = FreeType::from_bits(free_type) else { + return NtStatus::INVALID_PARAMETER; + }; + if base == 0 || !matches!(free_type, FreeType::MEM_DECOMMIT | FreeType::MEM_RELEASE) { + return NtStatus::INVALID_PARAMETER; + } + + let Some((aligned_base, aligned_len)) = + free_region(&self.process.virtual_allocations, base, size, free_type) + else { + return NtStatus::INVALID_PARAMETER; + }; + let ptr = MutPtr::::from_usize(aligned_base); + if free_type == FreeType::MEM_DECOMMIT { + // SAFETY: The range is page-aligned and belongs to a private allocation tracked for + // this process. Decommit discards page contents while leaving the address range + // reserved for later recommit. + if unsafe { self.global.page_manager.reset_pages(ptr, aligned_len, true) }.is_err() { + return NtStatus::UNABLE_TO_FREE_VM; + } + if update_permissions( + &self.global.page_manager, + aligned_base, + aligned_len, + MemoryRegionPermissions::empty(), + ) + .is_err() + { + return NtStatus::UNABLE_TO_FREE_VM; + } + mark_pages_decommitted(&self.process.virtual_allocations, aligned_base, aligned_len); + } else { + // SAFETY: The range is page-aligned and belongs to an allocation tracked for this + // process. The guest requested release, so the pages must not be used after success. + if unsafe { self.global.page_manager.remove_pages(ptr, aligned_len) }.is_err() { + return NtStatus::UNABLE_TO_FREE_VM; + } + self.process + .virtual_allocations + .write() + .remove(&aligned_base); + } + if base_address.write_at_offset(0, aligned_base).is_none() + || region_size.write_at_offset(0, aligned_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_protect_virtual_memory( + &self, + process_handle: ProcessHandle, + base_address: MutPtr, + region_size: MutPtr, + new_protect: u32, + old_protect: MutPtr, + ) -> NtStatus { + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + + let Some(base) = base_address.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let Some(size) = region_size.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let Some(old_protect_probe) = old_protect.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + if base_address.write_at_offset(0, base).is_none() + || region_size.write_at_offset(0, size).is_none() + || old_protect.write_at_offset(0, old_protect_probe).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if base == 0 || size == 0 { + return NtStatus::INVALID_PARAMETER; + } + let Some((aligned_base, aligned_len)) = page_aligned_region(base, size) else { + return NtStatus::INVALID_PARAMETER; + }; + let Some((new_protect, new_permissions)) = parse_page_protection(new_protect) else { + return NtStatus::INVALID_PAGE_PROTECTION; + }; + let old_protect_value = match scan_protect_range( + &self.process.virtual_allocations, + aligned_base, + aligned_len, + ) { + Some(PageRangeScan::FullyCommitted(first_protect)) => first_protect, + Some(PageRangeScan::ContainsUncommitted) => { + if old_protect + .write_at_offset(0, PageProtection::PAGE_NOACCESS.bits()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + return NtStatus::NOT_COMMITTED; + } + None => return NtStatus::NOT_COMMITTED, + }; + + if update_permissions( + &self.global.page_manager, + aligned_base, + aligned_len, + new_permissions, + ) + .is_err() + { + return NtStatus::ACCESS_VIOLATION; + } + set_committed_pages_protect( + &self.process.virtual_allocations, + aligned_base, + aligned_len, + new_protect, + ); + + // ReactOS NtProtectVirtualMemory writes OldProtection, BaseAddress, then RegionSize after + // MiProtectVirtualMemory succeeds; failed writeback does not roll back the protection. + if old_protect + .write_at_offset(0, old_protect_value.bits()) + .is_none() + || base_address.write_at_offset(0, aligned_base).is_none() + || region_size.write_at_offset(0, aligned_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + litebox_util_log::debug!( + process_handle:? = process_handle, + base:% = format_args!("{:#x}", base), + size = size, + aligned_base:% = format_args!("{:#x}", aligned_base), + aligned_len = aligned_len, + new_protect:% = format_args!("{:#x}", new_protect), + old_protect:% = format_args!("{:#x}", old_protect_value); + "Handled NtProtectVirtualMemory syscall" + ); + + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_query_virtual_memory( + &self, + process_handle: ProcessHandle, + base_address: usize, + memory_information_class: u32, + memory_information: MutPtr, + memory_information_length: usize, + return_length: Option>, + ) -> NtStatus { + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + let Ok(memory_information_class) = + MemoryInformationClass::try_from(memory_information_class) + else { + return NtStatus::INVALID_INFO_CLASS; + }; + + match memory_information_class { + MemoryInformationClass::Basic => self.write_memory_basic_information( + base_address, + memory_information, + memory_information_length, + return_length, + ), + MemoryInformationClass::WorkingSetList => Self::write_memory_working_set_list( + memory_information, + memory_information_length, + return_length, + ), + MemoryInformationClass::Image => self.write_memory_image_information( + process_handle, + base_address, + memory_information, + memory_information_length, + return_length, + ), + MemoryInformationClass::ImageExtension => self + .write_memory_image_extension_information( + process_handle, + base_address, + memory_information, + memory_information_length, + return_length, + ), + } + } + + fn write_memory_basic_information( + &self, + base_address: usize, + memory_information: MutPtr, + memory_information_length: usize, + return_length: Option>, + ) -> NtStatus { + if let Err(status) = check_and_write_length::( + return_length, + memory_information_length, + size_of::(), + ) { + return status; + } + + let Some(info) = query_memory_basic_information::( + &self.global.page_manager, + &self.process.virtual_allocations, + base_address, + ) else { + return NtStatus::INVALID_PARAMETER; + }; + let output = + MutPtr::::from_usize(memory_information.as_usize()); + if output.write_at_offset(0, info).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::SUCCESS + } + + fn write_memory_image_information( + &self, + process_handle: ProcessHandle, + base_address: usize, + memory_information: MutPtr, + memory_information_length: usize, + return_length: Option>, + ) -> NtStatus { + if let Err(status) = check_and_write_length::( + return_length, + memory_information_length, + size_of::(), + ) { + return status; + } + + let Some(allocation) = + find_image_allocation_containing(&self.process.virtual_allocations, base_address) + else { + return NtStatus::INVALID_PARAMETER; + }; + + let info = MemoryImageInformation { + image_base: allocation.base, + size_of_image: allocation.size, + image_flags: 0, + _padding: 0, + }; + let output = + MutPtr::::from_usize(memory_information.as_usize()); + if output.write_at_offset(0, info).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + + litebox_util_log::debug!( + process_handle:? = process_handle, + base:% = format_args!("{base_address:#x}"), + image_base:% = format_args!("{:#x}", allocation.base), + image_size = allocation.size; + "Handled NtQueryVirtualMemory MemoryImageInformation syscall" + ); + + NtStatus::SUCCESS + } + + fn write_memory_image_extension_information( + &self, + process_handle: ProcessHandle, + base_address: usize, + memory_information: MutPtr, + memory_information_length: usize, + return_length: Option>, + ) -> NtStatus { + if let Err(status) = check_and_write_length::( + return_length, + memory_information_length, + size_of::(), + ) { + return status; + } + + let Some(allocation) = + find_image_allocation_containing(&self.process.virtual_allocations, base_address) + else { + return NtStatus::INVALID_PARAMETER; + }; + + let image_extension_information = + MutPtr::::from_usize( + memory_information.as_usize(), + ); + let Some(request) = image_extension_information.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + // TODO: The buffer is an input request before it becomes output; only the default request for + // absent image extension information is supported for now. + if request != MemoryImageExtensionInformation::default() { + return NtStatus::INVALID_PARAMETER; + } + + // TODO: Report real image extension metadata when PE image extension data is modeled. + if image_extension_information + .write_at_offset(0, MemoryImageExtensionInformation::default()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + litebox_util_log::debug!( + process_handle:? = process_handle, + base:% = format_args!("{base_address:#x}"), + image_base:% = format_args!("{:#x}", allocation.base), + image_size = allocation.size; + "Handled NtQueryVirtualMemory MemoryImageExtensionInformation syscall" + ); + + NtStatus::SUCCESS + } + + fn write_memory_working_set_list( + memory_information: MutPtr, + memory_information_length: usize, + return_length: Option>, + ) -> NtStatus { + if memory_information_length < MEMORY_WORKING_SET_LIST_MIN_SIZE { + return NtStatus::INFO_LENGTH_MISMATCH; + } + if let Some(return_length) = return_length + && return_length + .write_at_offset(0, memory_information_length) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + // TODO: Model working set residency and report real entries instead of an empty list. + for offset in 0..memory_information_length { + let Ok(offset) = isize::try_from(offset) else { + return NtStatus::INVALID_PARAMETER; + }; + if memory_information.write_at_offset(offset, 0).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + } + + litebox_util_log::debug!( + memory_information_length; + "Handled NtQueryVirtualMemory MemoryWorkingSetList syscall" + ); + + NtStatus::SUCCESS + } +} + +fn check_and_write_length( + return_length: Option>, + memory_information_length: usize, + required_len: usize, +) -> Result<(), NtStatus> { + if let Some(return_length) = return_length + && return_length.write_at_offset(0, required_len).is_none() + { + return Err(NtStatus::ACCESS_VIOLATION); + } + if memory_information_length < required_len { + return Err(NtStatus::INFO_LENGTH_MISMATCH); + } + Ok(()) +} + +fn page_aligned_region(base: usize, size: usize) -> Option<(usize, usize)> { + let aligned_base = base & !(PAGE_SIZE - 1); + let end = base.checked_add(size)?; + let aligned_end = end.checked_add(PAGE_SIZE - 1)? & !(PAGE_SIZE - 1); + let aligned_len = aligned_end.checked_sub(aligned_base)?; + if aligned_base == 0 || aligned_len == 0 { + return None; + } + Some((aligned_base, aligned_len)) +} + +fn reserve_allocation_region(base: usize, size: usize) -> Option<(usize, usize)> { + let aligned_base = if base == 0 { + 0 + } else { + base & !(ALLOCATION_GRANULARITY - 1) + }; + if base != 0 && aligned_base == 0 { + return None; + } + let end = base.checked_add(size)?; + let aligned_end = end.checked_add(PAGE_SIZE - 1)? & !(PAGE_SIZE - 1); + let aligned_len = aligned_end.checked_sub(aligned_base)?; + if aligned_len == 0 { + return None; + } + Some((aligned_base, aligned_len)) +} + +fn free_region( + virtual_allocations: &WindowsVirtualAllocations, + base: usize, + size: usize, + free_type: FreeType, +) -> Option<(usize, usize)> { + if size == 0 { + let allocation = virtual_allocations + .read() + .get(&base) + .filter(|allocation| allocation.type_ == MemoryType::MEM_PRIVATE) + .cloned()?; + return Some((allocation.base, allocation.size)); + } + + if free_type == FreeType::MEM_RELEASE { + return None; + } + + let (aligned_base, aligned_len) = page_aligned_region(base, size)?; + find_private_virtual_allocation(virtual_allocations, aligned_base, aligned_len)?; + Some((aligned_base, aligned_len)) +} + +fn committed_pages( + base: usize, + size: usize, + protect: PageProtection, +) -> RangeMap { + let mut pages = RangeMap::new(); + let Some(end) = base.checked_add(size) else { + return pages; + }; + pages.insert(base..end, protect); + pages +} + +fn find_virtual_allocation( + virtual_allocations: &WindowsVirtualAllocations, + base: usize, + size: usize, +) -> Option { + let end = base.checked_add(size)?; + virtual_allocations + .read() + .range(..=base) + .next_back() + .map(|(_, allocation)| allocation.clone()) + .filter(|allocation| { + allocation + .base + .checked_add(allocation.size) + .is_some_and(|allocation_end| end <= allocation_end) + }) +} + +fn find_private_virtual_allocation( + virtual_allocations: &WindowsVirtualAllocations, + base: usize, + size: usize, +) -> Option { + find_virtual_allocation(virtual_allocations, base, size) + .filter(|allocation| allocation.type_ == MemoryType::MEM_PRIVATE) +} + +fn find_image_allocation_containing( + virtual_allocations: &WindowsVirtualAllocations, + base: usize, +) -> Option { + find_virtual_allocation(virtual_allocations, base, 1) + .filter(|allocation| allocation.type_ == MemoryType::MEM_IMAGE) +} + +fn find_virtual_allocation_containing( + virtual_allocations: &WindowsVirtualAllocations, + base: usize, +) -> Option { + find_virtual_allocation(virtual_allocations, base, 1) +} + +enum PageRangeScan { + FullyCommitted(PageProtection), + ContainsUncommitted, +} + +fn scan_allocation_pages( + allocation: &WindowsVirtualAllocation, + base: usize, + size: usize, +) -> Option { + let end = base.checked_add(size)?; + let allocation_end = allocation.base.checked_add(allocation.size)?; + let scan_end = end.min(allocation_end); + let mut first_protect = None; + let mut cursor = base; + for (range, protect) in allocation.pages.overlapping(base..scan_end) { + let range_start = range.start.max(base); + if cursor < range_start { + return Some(PageRangeScan::ContainsUncommitted); + } + first_protect.get_or_insert(*protect); + cursor = cursor.max(range.end.min(scan_end)); + if cursor == end { + break; + } + } + + if cursor == end { + Some(PageRangeScan::FullyCommitted(first_protect?)) + } else { + Some(PageRangeScan::ContainsUncommitted) + } +} + +fn scan_protect_range( + virtual_allocations: &WindowsVirtualAllocations, + base: usize, + size: usize, +) -> Option { + let allocation = find_virtual_allocation_containing(virtual_allocations, base)?; + scan_allocation_pages(&allocation, base, size) +} + +fn set_committed_pages_protect( + virtual_allocations: &WindowsVirtualAllocations, + base: usize, + size: usize, + protect: PageProtection, +) { + let Some(end) = base.checked_add(size) else { + return; + }; + let mut allocations = virtual_allocations.write(); + let Some((_, allocation)) = allocations.range_mut(..=base).next_back() else { + return; + }; + allocation.pages.insert(base..end, protect); +} + +fn mark_pages_decommitted( + virtual_allocations: &WindowsVirtualAllocations, + base: usize, + size: usize, +) { + let Some(end) = base.checked_add(size) else { + return; + }; + let mut allocations = virtual_allocations.write(); + let Some((_, allocation)) = allocations.range_mut(..=base).next_back() else { + return; + }; + allocation.pages.remove(base..end); +} + +fn parse_page_protection(protect: u32) -> Option<(PageProtection, MemoryRegionPermissions)> { + let protect = PageProtection::from_bits(protect)?; + let permissions = page_protect_to_permissions(protect)?; + Some((protect, permissions)) +} + +fn page_protect_to_permissions(protect: PageProtection) -> Option { + if !protect.has_valid_modifier_combination() { + return None; + } + + match protect.base() { + value if value == PageProtection::PAGE_NOACCESS.bits() => { + Some(MemoryRegionPermissions::empty()) + } + value if value == PageProtection::PAGE_READONLY.bits() => { + Some(MemoryRegionPermissions::READ) + } + value + if value == PageProtection::PAGE_READWRITE.bits() + || value == PageProtection::PAGE_WRITECOPY.bits() => + { + Some(MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE) + } + value + if value == PageProtection::PAGE_EXECUTE.bits() + || value == PageProtection::PAGE_EXECUTE_READ.bits() => + { + Some(MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC) + } + value + if value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() => + { + Some( + MemoryRegionPermissions::READ + | MemoryRegionPermissions::WRITE + | MemoryRegionPermissions::EXEC, + ) + } + _ => None, + } +} + +fn permissions_to_page_protect(permissions: MemoryRegionPermissions) -> PageProtection { + match ( + permissions.contains(MemoryRegionPermissions::READ), + permissions.contains(MemoryRegionPermissions::WRITE), + permissions.contains(MemoryRegionPermissions::EXEC), + ) { + (false, false, false) => PageProtection::PAGE_NOACCESS, + (true, false, false) => PageProtection::PAGE_READONLY, + (_, true, false) => PageProtection::PAGE_READWRITE, + (false, false, true) => PageProtection::PAGE_EXECUTE, + (true, false, true) => PageProtection::PAGE_EXECUTE_READ, + (_, true, true) => PageProtection::PAGE_EXECUTE_READWRITE, + } +} + +fn create_pages( + page_manager: &WindowsPageManager, + suggested_address: Option>, + length: NonZeroPageSize, + flags: CreatePagesFlags, + permissions: MemoryRegionPermissions, + op: impl FnOnce(MutPtr) -> Result, +) -> Result, MappingError> { + // SAFETY: This creates guest mappings through the LiteBox page manager. The caller controls + // fixed-address behavior, and `op` only initializes the new mapping before it is exposed. + unsafe { + match permissions { + permissions if permissions.is_empty() => { + page_manager.create_inaccessible_pages(suggested_address, length, flags, op) + } + MemoryRegionPermissions::READ => { + page_manager.create_readable_pages(suggested_address, length, flags, op) + } + permissions + if permissions + == MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE => + { + page_manager.create_writable_pages(suggested_address, length, flags, op) + } + permissions + if permissions == MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC => + { + page_manager.create_executable_pages(suggested_address, length, flags, op) + } + permissions + if permissions + == MemoryRegionPermissions::READ + | MemoryRegionPermissions::WRITE + | MemoryRegionPermissions::EXEC => + { + let ptr = + page_manager.create_writable_pages(suggested_address, length, flags, op)?; + page_manager + .make_pages_rwx(ptr, length.as_usize()) + .map_err(|_| MappingError::OutOfMemory)?; + Ok(ptr) + } + _ => unreachable!("Windows page protection parser produced unsupported permissions"), + } + } +} + +enum HoleSearchResult { + Allocated(MutPtr), + RetryWithFreshMappings, + Exhausted, +} + +fn create_aligned_pages_in_hole( + page_manager: &WindowsPageManager, + hole_start: usize, + hole_end: usize, + length: NonZeroPageSize, + permissions: MemoryRegionPermissions, + top_down: bool, +) -> Result, MappingError> { + let Some(mut candidate) = + allocation_granularity_aligned_candidate(hole_start, hole_end, length.as_usize(), top_down) + else { + return Ok(HoleSearchResult::Exhausted); + }; + + loop { + match create_pages( + page_manager, + NonZeroAddress::new(candidate), + length, + CreatePagesFlags::FIXED_ADDR | CreatePagesFlags::NOREPLACE, + permissions, + |_| Ok(0), + ) { + Ok(ptr) => return Ok(HoleSearchResult::Allocated(ptr)), + Err(MappingError::MapError(AllocationError::AddressInUse)) => { + return Ok(HoleSearchResult::RetryWithFreshMappings); + } + Err(MappingError::MapError(AllocationError::AddressInUseByPlatform)) => {} + Err(error) => return Err(error), + } + + let Some(next_candidate) = next_allocation_granularity_candidate( + candidate, + length.as_usize(), + hole_start, + hole_end, + top_down, + ) else { + return Ok(HoleSearchResult::Exhausted); + }; + candidate = next_candidate; + } +} + +fn next_allocation_granularity_candidate( + candidate: usize, + length: usize, + hole_start: usize, + hole_end: usize, + top_down: bool, +) -> Option { + if top_down { + candidate + .checked_sub(ALLOCATION_GRANULARITY) + .filter(|next| *next >= hole_start) + } else { + let next_candidate = candidate.checked_add(ALLOCATION_GRANULARITY)?; + let next_end = next_candidate.checked_add(length)?; + (next_end <= hole_end).then_some(next_candidate) + } +} + +fn zero_bits_address_limit(zero_bits: usize) -> Option { + if zero_bits > 32 { + // NtAllocateVirtualMemory treats ZeroBits as a bitmask when > 32. + zero_bits.checked_add(1) + } else if zero_bits < usize::BITS as usize { + let shift = (usize::BITS as usize - zero_bits).try_into().ok()?; + 1usize.checked_shl(shift) + } else { + None + } +} + +fn mapping_error_to_nt_status(error: MappingError) -> NtStatus { + match error { + MappingError::UnAligned + | MappingError::BadFD(_) + | MappingError::NotAFile + | MappingError::NotForReading + | MappingError::MapError( + AllocationError::Unaligned + | AllocationError::BelowMinAddress + | AllocationError::AboveMaxAddress, + ) => NtStatus::INVALID_PARAMETER, + MappingError::MapError( + AllocationError::AddressInUse + | AllocationError::AddressInUseByPlatform + | AllocationError::AddressPartiallyInUse, + ) => NtStatus::CONFLICTING_ADDRESSES, + MappingError::OutOfMemory | MappingError::MapError(AllocationError::OutOfMemory) | _ => { + NtStatus::NO_MEMORY + } + } +} + +fn create_allocation_granularity_aligned_pages( + page_manager: &WindowsPageManager, + length: NonZeroPageSize, + permissions: MemoryRegionPermissions, + zero_bits: usize, + top_down: bool, +) -> Result, NtStatus> { + let mut max_start = Platform::TASK_ADDR_MAX + .checked_sub(length.as_usize()) + .ok_or(NtStatus::NO_MEMORY)?; + if let Some(limit) = zero_bits_address_limit(zero_bits) { + max_start = max_start.min( + limit + .checked_sub(length.as_usize()) + .ok_or(NtStatus::NO_MEMORY)?, + ); + } + let min_start = Platform::TASK_ADDR_MIN.next_multiple_of(ALLOCATION_GRANULARITY); + let search_end = max_start + .checked_add(length.as_usize()) + .ok_or(NtStatus::NO_MEMORY)?; + + // TODO: consider adding support for different allocation strategies and granularity to page manager + 'search: for _ in 0..ALLOCATION_SEARCH_ATTEMPTS { + let mut mappings = page_manager.mappings(); + mappings.sort_by_key(|(range, _)| range.start); + + if top_down { + let mut hole_end = search_end; + for (range, _) in mappings.iter().rev() { + if range.end <= min_start { + break; + } + if range.start >= search_end { + continue; + } + if range.end < hole_end { + match create_aligned_pages_in_hole( + page_manager, + range.end.max(min_start), + hole_end, + length, + permissions, + true, + ) + .map_err(mapping_error_to_nt_status)? + { + HoleSearchResult::Allocated(ptr) => return Ok(ptr), + HoleSearchResult::RetryWithFreshMappings => continue 'search, + HoleSearchResult::Exhausted => {} + } + } + if range.start < hole_end { + hole_end = range.start; + } + if hole_end <= min_start { + break; + } + } + + match create_aligned_pages_in_hole( + page_manager, + min_start, + hole_end, + length, + permissions, + true, + ) + .map_err(mapping_error_to_nt_status)? + { + HoleSearchResult::Allocated(ptr) => return Ok(ptr), + HoleSearchResult::RetryWithFreshMappings => continue 'search, + HoleSearchResult::Exhausted => {} + } + } else { + let mut hole_start = min_start; + for (range, _) in &mappings { + if range.start >= search_end { + break; + } + if range.end <= hole_start { + continue; + } + if range.start > hole_start { + match create_aligned_pages_in_hole( + page_manager, + hole_start, + range.start.min(search_end), + length, + permissions, + false, + ) + .map_err(mapping_error_to_nt_status)? + { + HoleSearchResult::Allocated(ptr) => return Ok(ptr), + HoleSearchResult::RetryWithFreshMappings => continue 'search, + HoleSearchResult::Exhausted => {} + } + } + if range.end > hole_start { + hole_start = range.end; + } + if hole_start >= search_end { + break; + } + } + + match create_aligned_pages_in_hole( + page_manager, + hole_start, + search_end, + length, + permissions, + false, + ) + .map_err(mapping_error_to_nt_status)? + { + HoleSearchResult::Allocated(ptr) => return Ok(ptr), + HoleSearchResult::RetryWithFreshMappings => continue 'search, + HoleSearchResult::Exhausted => {} + } + } + + return Err(NtStatus::NO_MEMORY); + } + + Err(NtStatus::NO_MEMORY) +} + +fn allocation_granularity_aligned_candidate( + hole_start: usize, + hole_end: usize, + length: usize, + top_down: bool, +) -> Option { + if top_down { + let max_candidate = hole_end.checked_sub(length)? & !(ALLOCATION_GRANULARITY - 1); + (max_candidate >= hole_start).then_some(max_candidate) + } else { + let min_candidate = hole_start.next_multiple_of(ALLOCATION_GRANULARITY); + min_candidate + .checked_add(length) + .is_some_and(|end| end <= hole_end) + .then_some(min_candidate) + } +} + +fn update_permissions( + page_manager: &WindowsPageManager, + aligned_base: usize, + aligned_len: usize, + permissions: MemoryRegionPermissions, +) -> Result<(), ()> { + let ptr = MutPtr::::from_usize(aligned_base); + // SAFETY: This applies the guest's explicit VM protection/free request to a page-aligned range + // tracked by the LiteBox page manager. The page manager serializes the VMA update. + let result = unsafe { + match permissions { + permissions if permissions.is_empty() => { + page_manager.make_pages_inaccessible(ptr, aligned_len) + } + MemoryRegionPermissions::READ => page_manager.make_pages_readable(ptr, aligned_len), + permissions + if permissions + == MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE => + { + page_manager.make_pages_writable(ptr, aligned_len) + } + permissions + if permissions == MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC => + { + page_manager.make_pages_executable(ptr, aligned_len) + } + permissions + if permissions + == MemoryRegionPermissions::READ + | MemoryRegionPermissions::WRITE + | MemoryRegionPermissions::EXEC => + { + page_manager.make_pages_rwx(ptr, aligned_len) + } + _ => return Err(()), + } + }; + + result.map_err(|_| ()) +} + +fn query_memory_basic_information( + page_manager: &WindowsPageManager, + virtual_allocations: &WindowsVirtualAllocations, + base_address: usize, +) -> Option { + let query_base = base_address & !(PAGE_SIZE - 1); + if query_base >= Platform::TASK_ADDR_MAX { + return None; + } + + let mut mappings = page_manager.mappings(); + mappings.sort_by_key(|(range, _)| range.start); + if let Some(allocation) = find_virtual_allocation_containing(virtual_allocations, query_base) { + return query_allocation_basic_information(allocation, query_base); + } + + if let Some((range, flags)) = mappings + .iter() + .find(|(range, _)| range.contains(&base_address)) + { + let protect = permissions_to_page_protect(MemoryRegionPermissions::from(*flags)); + return Some(MemoryBasicInformation { + base_address: range.start, + allocation_base: range.start, + allocation_protect: protect.bits(), + partition_id: 0, + _padding0: 0, + region_size: range.end - range.start, + state: MemoryState::MEM_COMMIT.bits(), + protect: protect.bits(), + type_: MemoryType::MEM_PRIVATE.bits(), + _padding1: 0, + }); + } + + let next_mapping_start = mappings + .iter() + .find(|(range, _)| range.start > query_base) + .map_or(Platform::TASK_ADDR_MAX, |(range, _)| range.start); + + Some(MemoryBasicInformation { + base_address: query_base, + allocation_base: 0, + allocation_protect: 0, + partition_id: 0, + _padding0: 0, + region_size: next_mapping_start.saturating_sub(query_base), + state: MemoryState::MEM_FREE.bits(), + protect: 0, + type_: 0, + _padding1: 0, + }) +} + +fn query_allocation_basic_information( + allocation: WindowsVirtualAllocation, + query_base: usize, +) -> Option { + let allocation_end = allocation.base.checked_add(allocation.size)?; + let (state, protect) = private_page_state_and_protect(&allocation, query_base); + let mut region_end = query_base.checked_add(PAGE_SIZE)?; + while region_end < allocation_end { + if private_page_state_and_protect(&allocation, region_end) != (state, protect) { + break; + } + region_end = region_end.checked_add(PAGE_SIZE)?; + } + + Some(MemoryBasicInformation { + base_address: query_base, + allocation_base: allocation.base, + allocation_protect: allocation.allocation_protect.bits(), + partition_id: 0, + _padding0: 0, + region_size: region_end - query_base, + state, + protect, + type_: allocation.type_.bits(), + _padding1: 0, + }) +} + +fn private_page_state_and_protect( + allocation: &WindowsVirtualAllocation, + page: usize, +) -> (u32, u32) { + allocation + .pages + .get(&page) + .map_or((MemoryState::MEM_RESERVE.bits(), 0), |protect| { + (MemoryState::MEM_COMMIT.bits(), protect.bits()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{mut_byte_ptr, mut_ptr}; + use litebox::platform::ThreadProvider; + + extern crate std; + + type TestPlatform = crate::tests::TestPlatform; + type TestTask = Task; + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = crate::tests::test_platform(); + ::run_test_thread(f) + } + + fn allocate_committed_rw(task: &TestTask, size: usize) -> (usize, usize) { + let mut base = 0usize; + let mut region_size = size; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 0, + mut_ptr(&mut region_size), + (AllocationType::MEM_RESERVE | AllocationType::MEM_COMMIT).bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + (base, region_size) + } + + fn release_allocation(task: &TestTask, base: usize) { + let mut release_base = base; + let mut release_size = 0usize; + assert_eq!( + task.sys_nt_free_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut release_base), + mut_ptr(&mut release_size), + FreeType::MEM_RELEASE.bits(), + ), + NtStatus::SUCCESS + ); + } + + fn query_basic_information(task: &TestTask, base: usize) -> MemoryBasicInformation { + let mut info = MemoryBasicInformation::default(); + let mut return_length = 0usize; + assert_eq!( + task.sys_nt_query_virtual_memory( + ProcessHandle::CURRENT, + base, + MemoryInformationClass::Basic as u32, + mut_byte_ptr(&mut info), + size_of::(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::SUCCESS + ); + assert_eq!(return_length, size_of::()); + info + } + + #[test] + fn allocate_virtual_memory_commit_only_null_base_creates_committed_region() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut base = 0usize; + let mut region_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 0, + mut_ptr(&mut region_size), + AllocationType::MEM_COMMIT.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + assert_ne!(base, 0); + assert_eq!(region_size, PAGE_SIZE); + + let info = query_basic_information(&task, base); + assert_eq!(info.state, MemoryState::MEM_COMMIT.bits()); + assert_eq!(info.protect, PageProtection::PAGE_READWRITE.bits()); + + release_allocation(&task, base); + }); + } + + #[test] + fn allocate_virtual_memory_fixed_reserve_rounds_base_to_allocation_granularity() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let requested_base = ALLOCATION_GRANULARITY * 8 + PAGE_SIZE + 123; + let expected_base = requested_base & !(ALLOCATION_GRANULARITY - 1); + let mut base = requested_base; + let mut region_size = 1usize; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 0, + mut_ptr(&mut region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + assert_eq!(base, expected_base); + assert_eq!(region_size, PAGE_SIZE * 2); + + release_allocation(&task, base); + }); + } + + #[test] + fn allocate_virtual_memory_zero_bits_is_allowed_for_null_base() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut base = 0usize; + let mut region_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 1, + mut_ptr(&mut region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + assert_ne!(base, 0); + assert_eq!(region_size, PAGE_SIZE); + + release_allocation(&task, base); + }); + } + + #[test] + fn allocate_virtual_memory_fixed_collision_returns_conflicting_addresses() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut base = 0usize; + let mut region_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 0, + mut_ptr(&mut region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + + let mut fixed_base = base; + let mut fixed_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut fixed_base), + 0, + mut_ptr(&mut fixed_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::CONFLICTING_ADDRESSES + ); + + release_allocation(&task, base); + }); + } + + #[test] + fn allocate_virtual_memory_mem_top_down_prefers_higher_addresses() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + + let mut bottom_base = 0usize; + let mut bottom_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut bottom_base), + 0, + mut_ptr(&mut bottom_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + + let mut top_base = 0usize; + let mut top_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut top_base), + 0, + mut_ptr(&mut top_size), + (AllocationType::MEM_RESERVE | AllocationType::MEM_TOP_DOWN).bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + + assert!(top_base > bottom_base); + + release_allocation(&task, top_base); + release_allocation(&task, bottom_base); + }); + } + + #[test] + fn allocate_virtual_memory_mem_reset_preserves_committed_state_and_protection() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let (base, _) = allocate_committed_rw(&task, PAGE_SIZE); + + let mut reset_base = base + 1; + let mut reset_size = 1usize; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut reset_base), + 0, + mut_ptr(&mut reset_size), + AllocationType::MEM_RESET.bits(), + PageProtection::PAGE_NOACCESS.bits(), + ), + NtStatus::SUCCESS + ); + assert_eq!(reset_base, base); + assert_eq!(reset_size, PAGE_SIZE); + + let info = query_basic_information(&task, base); + assert_eq!(info.state, MemoryState::MEM_COMMIT.bits()); + assert_eq!(info.protect, PageProtection::PAGE_READWRITE.bits()); + + release_allocation(&task, base); + }); + } + + #[test] + fn allocate_virtual_memory_mem_reset_rejects_combined_flags() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let (base, _) = allocate_committed_rw(&task, PAGE_SIZE); + + let mut reset_base = base; + let mut reset_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut reset_base), + 0, + mut_ptr(&mut reset_size), + (AllocationType::MEM_RESET | AllocationType::MEM_COMMIT).bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::INVALID_PARAMETER + ); + + release_allocation(&task, base); + }); + } + + #[test] + fn protect_virtual_memory_rounds_outputs_and_reports_old_protection() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let (base, allocation_size) = allocate_committed_rw(&task, PAGE_SIZE * 2 - 1); + assert_eq!(base % ALLOCATION_GRANULARITY, 0); + assert_eq!(allocation_size, PAGE_SIZE * 2); + + let mut protect_base = base + 1; + let mut protect_size = 1usize; + let mut old_protect = 0u32; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut protect_base), + mut_ptr(&mut protect_size), + PageProtection::PAGE_READONLY.bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::SUCCESS + ); + assert_eq!(protect_base, base); + assert_eq!(protect_size, PAGE_SIZE); + assert_eq!(old_protect, PageProtection::PAGE_READWRITE.bits()); + + let info = query_basic_information(&task, base); + assert_eq!(info.base_address, base); + assert_eq!(info.allocation_base, base); + assert_eq!( + info.allocation_protect, + PageProtection::PAGE_READWRITE.bits() + ); + assert_eq!(info.region_size, PAGE_SIZE); + assert_eq!(info.state, MemoryState::MEM_COMMIT.bits()); + assert_eq!(info.protect, PageProtection::PAGE_READONLY.bits()); + assert_eq!(info.type_, MemoryType::MEM_PRIVATE.bits()); + + release_allocation(&task, base); + }); + } + + #[test] + fn protect_virtual_memory_allows_committed_page_noaccess() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut base = 0usize; + let mut region_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 0, + mut_ptr(&mut region_size), + (AllocationType::MEM_RESERVE | AllocationType::MEM_COMMIT).bits(), + PageProtection::PAGE_NOACCESS.bits(), + ), + NtStatus::SUCCESS + ); + + let info = query_basic_information(&task, base); + assert_eq!(info.state, MemoryState::MEM_COMMIT.bits()); + assert_eq!(info.protect, PageProtection::PAGE_NOACCESS.bits()); + + let mut protect_base = base; + let mut protect_size = PAGE_SIZE; + let mut old_protect = 0u32; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut protect_base), + mut_ptr(&mut protect_size), + PageProtection::PAGE_READONLY.bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::SUCCESS + ); + assert_eq!(old_protect, PageProtection::PAGE_NOACCESS.bits()); + + release_allocation(&task, base); + }); + } + + #[test] + fn protect_virtual_memory_rejects_reserved_pages() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut base = 0usize; + let mut region_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 0, + mut_ptr(&mut region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + + let mut protect_base = base; + let mut protect_size = PAGE_SIZE; + let mut old_protect = u32::MAX; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut protect_base), + mut_ptr(&mut protect_size), + PageProtection::PAGE_READONLY.bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::NOT_COMMITTED + ); + assert_eq!(old_protect, PageProtection::PAGE_NOACCESS.bits()); + + for invalid_protect in [ + PageProtection::PAGE_NOACCESS | PageProtection::PAGE_GUARD, + PageProtection::PAGE_NOACCESS | PageProtection::PAGE_NOCACHE, + PageProtection::PAGE_NOACCESS | PageProtection::PAGE_WRITECOMBINE, + PageProtection::PAGE_READWRITE + | PageProtection::PAGE_NOCACHE + | PageProtection::PAGE_WRITECOMBINE, + PageProtection::PAGE_READWRITE + | PageProtection::PAGE_GUARD + | PageProtection::PAGE_NOCACHE, + ] { + let mut protect_base = base; + let mut protect_size = PAGE_SIZE; + let mut old_protect = u32::MAX; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut protect_base), + mut_ptr(&mut protect_size), + invalid_protect.bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::INVALID_PAGE_PROTECTION + ); + assert_eq!(old_protect, u32::MAX); + } + + release_allocation(&task, base); + }); + } + + #[test] + fn protect_virtual_memory_rejects_partly_reserved_range() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut base = 0usize; + let mut region_size = PAGE_SIZE * 2; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 0, + mut_ptr(&mut region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + let mut commit_base = base; + let mut commit_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut commit_base), + 0, + mut_ptr(&mut commit_size), + AllocationType::MEM_COMMIT.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + + let mut protect_base = base; + let mut protect_size = PAGE_SIZE * 2; + let mut old_protect = u32::MAX; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut protect_base), + mut_ptr(&mut protect_size), + PageProtection::PAGE_READONLY.bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::NOT_COMMITTED + ); + assert_eq!(old_protect, PageProtection::PAGE_NOACCESS.bits()); + assert_eq!( + query_basic_information(&task, base).protect, + PageProtection::PAGE_READWRITE.bits() + ); + + release_allocation(&task, base); + }); + } + + #[test] + fn free_virtual_memory_decommit_zero_size_at_allocation_base_decommits_whole_region() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let (base, allocation_size) = allocate_committed_rw(&task, PAGE_SIZE * 2); + + let mut decommit_base = base; + let mut decommit_size = 0usize; + assert_eq!( + task.sys_nt_free_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut decommit_base), + mut_ptr(&mut decommit_size), + FreeType::MEM_DECOMMIT.bits(), + ), + NtStatus::SUCCESS + ); + assert_eq!(decommit_base, base); + assert_eq!(decommit_size, allocation_size); + + let info = query_basic_information(&task, base); + assert_eq!(info.state, MemoryState::MEM_RESERVE.bits()); + assert_eq!(info.protect, 0); + assert_eq!(info.region_size, allocation_size); + + release_allocation(&task, base); + }); + } + + #[test] + fn free_virtual_memory_decommit_discards_page_contents() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let (base, _) = allocate_committed_rw(&task, PAGE_SIZE); + let ptr = MutPtr::::from_usize(base); + assert_eq!(ptr.write_at_offset(0, 0xa5), Some(())); + + let mut decommit_base = base; + let mut decommit_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_free_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut decommit_base), + mut_ptr(&mut decommit_size), + FreeType::MEM_DECOMMIT.bits(), + ), + NtStatus::SUCCESS + ); + + let mut commit_base = base; + let mut commit_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut commit_base), + 0, + mut_ptr(&mut commit_size), + AllocationType::MEM_COMMIT.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + NtStatus::SUCCESS + ); + assert_eq!(ptr.read_at_offset(0), Some(0)); + + release_allocation(&task, base); + }); + } + + #[test] + fn protect_virtual_memory_preserves_page_modifier_bits() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut base = 0usize; + let mut region_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut base), + 0, + mut_ptr(&mut region_size), + (AllocationType::MEM_RESERVE | AllocationType::MEM_COMMIT).bits(), + (PageProtection::PAGE_READWRITE | PageProtection::PAGE_NOCACHE).bits(), + ), + NtStatus::SUCCESS + ); + assert_eq!( + query_basic_information(&task, base).protect, + (PageProtection::PAGE_READWRITE | PageProtection::PAGE_NOCACHE).bits() + ); + + let mut protect_base = base; + let mut protect_size = PAGE_SIZE; + let mut old_protect = 0u32; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut protect_base), + mut_ptr(&mut protect_size), + (PageProtection::PAGE_READONLY | PageProtection::PAGE_WRITECOMBINE).bits(), + mut_ptr(&mut old_protect), + ), + NtStatus::SUCCESS + ); + assert_eq!( + old_protect, + (PageProtection::PAGE_READWRITE | PageProtection::PAGE_NOCACHE).bits() + ); + assert_eq!( + query_basic_information(&task, base).protect, + (PageProtection::PAGE_READONLY | PageProtection::PAGE_WRITECOMBINE).bits() + ); + + release_allocation(&task, base); + }); + } + + #[test] + fn protect_virtual_memory_rejects_invalid_page_protection() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let (base, _) = allocate_committed_rw(&task, PAGE_SIZE); + + let mut protect_base = base; + let mut protect_size = PAGE_SIZE; + let mut old_protect = u32::MAX; + assert_eq!( + task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut protect_base), + mut_ptr(&mut protect_size), + 0, + mut_ptr(&mut old_protect), + ), + NtStatus::INVALID_PAGE_PROTECTION + ); + assert_eq!(old_protect, u32::MAX); + + release_allocation(&task, base); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + mod host_fidelity { + use super::*; + use core::ffi::c_void; + + #[link(name = "ntdll")] + unsafe extern "system" { + fn NtAllocateVirtualMemory( + process_handle: *mut c_void, + base_address: *mut *mut c_void, + zero_bits: usize, + region_size: *mut usize, + allocation_type: u32, + protect: u32, + ) -> i32; + + fn NtProtectVirtualMemory( + process_handle: *mut c_void, + base_address: *mut *mut c_void, + region_size: *mut usize, + new_protect: u32, + old_protect: *mut u32, + ) -> i32; + + fn NtQueryVirtualMemory( + process_handle: *mut c_void, + base_address: *const c_void, + memory_information_class: u32, + memory_information: *mut c_void, + memory_information_length: usize, + return_length: *mut usize, + ) -> i32; + + fn NtFreeVirtualMemory( + process_handle: *mut c_void, + base_address: *mut *mut c_void, + region_size: *mut usize, + free_type: u32, + ) -> i32; + } + + fn current_process() -> *mut c_void { + usize::MAX as *mut c_void + } + + fn host_status(status: i32) -> NtStatus { + NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) + } + + #[test] + fn allocate_query_free_outputs_match_host_ntdll() { + run_with_test_platform_pointers(|| { + let mut host_base = core::ptr::null_mut::(); + let mut host_region_size = PAGE_SIZE; + // SAFETY: The output pointers are valid locals and the current-process pseudo + // handle targets this process. The allocation is released before return. + let host_allocate_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_base, + 0, + &raw mut host_region_size, + AllocationType::MEM_COMMIT.bits(), + PageProtection::PAGE_NOACCESS.bits(), + )) + }; + assert_eq!(host_allocate_status, NtStatus::SUCCESS); + + let mut host_info = MemoryBasicInformation::default(); + let mut host_return_length = 0usize; + // SAFETY: The host allocation is live, and the output buffer and return length are + // valid locals that ntdll writes synchronously. + let host_query_status = unsafe { + host_status(NtQueryVirtualMemory( + current_process(), + host_base, + MemoryInformationClass::Basic as u32, + (&raw mut host_info).cast(), + size_of::(), + &raw mut host_return_length, + )) + }; + assert_eq!(host_query_status, NtStatus::SUCCESS); + + let task = crate::tests::test_task(); + let mut guest_base = 0usize; + let mut guest_region_size = PAGE_SIZE; + let guest_allocate_status = task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_base), + 0, + mut_ptr(&mut guest_region_size), + AllocationType::MEM_COMMIT.bits(), + PageProtection::PAGE_NOACCESS.bits(), + ); + assert_eq!(guest_allocate_status, host_allocate_status); + assert_eq!(guest_region_size, host_region_size); + + let guest_info = query_basic_information(&task, guest_base); + assert_eq!(guest_info.base_address, guest_base); + assert_eq!(host_info.base_address, host_base as usize); + assert_eq!(guest_info.allocation_base, guest_base); + assert_eq!(host_info.allocation_base, host_base as usize); + assert_eq!(guest_info.allocation_protect, host_info.allocation_protect); + assert_eq!(guest_info.region_size, host_info.region_size); + assert_eq!(guest_info.state, host_info.state); + assert_eq!(guest_info.protect, host_info.protect); + assert_eq!(guest_info.type_, host_info.type_); + + let mut guest_release_base = guest_base; + let mut guest_release_size = 0usize; + let guest_free_status = task.sys_nt_free_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_release_base), + mut_ptr(&mut guest_release_size), + FreeType::MEM_RELEASE.bits(), + ); + let mut host_release_size = 0usize; + // SAFETY: Releases the host allocation created by this test. + let host_free_status = unsafe { + host_status(NtFreeVirtualMemory( + current_process(), + &raw mut host_base, + &raw mut host_release_size, + FreeType::MEM_RELEASE.bits(), + )) + }; + assert_eq!(guest_free_status, host_free_status); + assert_eq!(guest_release_size, host_release_size); + }); + } + + #[test] + fn reserve_alignment_outputs_match_host_ntdll() { + run_with_test_platform_pointers(|| { + let mut probe_base = core::ptr::null_mut::(); + let mut probe_region_size = ALLOCATION_GRANULARITY * 2; + // SAFETY: The output pointers are valid locals and the current-process pseudo + // handle targets this process. The allocation is released before reuse below. + let probe_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut probe_base, + 0, + &raw mut probe_region_size, + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + )) + }; + assert_eq!(probe_status, NtStatus::SUCCESS); + + let mut probe_release_base = probe_base; + let mut probe_release_size = 0usize; + // SAFETY: Releases the host allocation created above so the fixed-address probe + // can reuse the same address range. + let probe_free_status = unsafe { + host_status(NtFreeVirtualMemory( + current_process(), + &raw mut probe_release_base, + &raw mut probe_release_size, + FreeType::MEM_RELEASE.bits(), + )) + }; + assert_eq!(probe_free_status, NtStatus::SUCCESS); + + let requested_base = probe_base.wrapping_byte_add(PAGE_SIZE + 123); + let mut host_base = requested_base; + let mut host_region_size = 1usize; + // SAFETY: The fixed address range was just released and the output pointers are + // valid locals. The allocation is released before the guest probe runs. + let host_allocate_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_base, + 0, + &raw mut host_region_size, + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + )) + }; + assert_eq!(host_allocate_status, NtStatus::SUCCESS); + + let mut host_release_base = host_base; + let mut host_release_size = 0usize; + // SAFETY: Releases the fixed host allocation created by this test. + let host_free_status = unsafe { + host_status(NtFreeVirtualMemory( + current_process(), + &raw mut host_release_base, + &raw mut host_release_size, + FreeType::MEM_RELEASE.bits(), + )) + }; + assert_eq!(host_free_status, NtStatus::SUCCESS); + + let task = crate::tests::test_task(); + let mut guest_base = requested_base as usize; + let mut guest_region_size = 1usize; + let guest_allocate_status = task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_base), + 0, + mut_ptr(&mut guest_region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ); + assert_eq!(guest_allocate_status, host_allocate_status); + assert_eq!(guest_base, host_base as usize); + assert_eq!(guest_region_size, host_region_size); + + release_allocation(&task, guest_base); + }); + } + + #[test] + fn allocate_virtual_memory_zero_bits_bitmask_matches_host_ntdll() { + run_with_test_platform_pointers(|| { + // When ZeroBits > 32, Windows treats it as the maximum virtual address for the + // allocation (exclusive upper bound = zero_bits + 1). A value of 0x7FFF_FFFF + // restricts the allocation to below 2 GiB. + let zero_bits_max_addr: usize = 0x7FFF_FFFF; + let limit: usize = zero_bits_max_addr + 1; + + let mut host_base = core::ptr::null_mut::(); + let mut host_region_size = PAGE_SIZE; + // SAFETY: Output pointers are valid locals and the current-process pseudo handle + // targets this process. The allocation is released before return. + let host_allocate_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_base, + zero_bits_max_addr, + &raw mut host_region_size, + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + )) + }; + assert_eq!(host_allocate_status, NtStatus::SUCCESS); + assert!( + host_base as usize + host_region_size <= limit, + "host allocation exceeds ZeroBits max address" + ); + + let task = crate::tests::test_task(); + let mut guest_base = 0usize; + let mut guest_region_size = PAGE_SIZE; + let guest_allocate_status = task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_base), + zero_bits_max_addr, + mut_ptr(&mut guest_region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ); + assert_eq!(guest_allocate_status, host_allocate_status); + assert!( + guest_base + guest_region_size <= limit, + "guest allocation exceeds ZeroBits max address" + ); + + release_allocation(&task, guest_base); + + let mut host_release_size = 0usize; + // SAFETY: Releases the host allocation created by this test. + let host_free_status = unsafe { + host_status(NtFreeVirtualMemory( + current_process(), + &raw mut host_base, + &raw mut host_release_size, + FreeType::MEM_RELEASE.bits(), + )) + }; + assert_eq!(host_free_status, NtStatus::SUCCESS); + }); + } + + #[test] + fn mem_reset_reserved_pages_matches_host_ntdll() { + run_with_test_platform_pointers(|| { + let mut host_base = core::ptr::null_mut::(); + let mut host_region_size = PAGE_SIZE; + // SAFETY: The output pointers are valid locals and the current-process pseudo + // handle targets this process. The allocation is released before return. + let host_reserve_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_base, + 0, + &raw mut host_region_size, + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + )) + }; + assert_eq!(host_reserve_status, NtStatus::SUCCESS); + + let mut host_reset_base = host_base.wrapping_byte_add(1); + let mut host_reset_size = 1usize; + // SAFETY: The host allocation is reserved but uncommitted; the output pointers are + // valid locals and ntdll does not retain them. + let host_reset_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_reset_base, + 0, + &raw mut host_reset_size, + AllocationType::MEM_RESET.bits(), + PageProtection::PAGE_NOACCESS.bits(), + )) + }; + assert_eq!(host_reset_status, NtStatus::CONFLICTING_ADDRESSES); + + let task = crate::tests::test_task(); + let mut guest_base = 0usize; + let mut guest_region_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_base), + 0, + mut_ptr(&mut guest_region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + host_reserve_status + ); + + let mut guest_reset_base = guest_base + 1; + let mut guest_reset_size = 1usize; + let guest_reset_status = task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_reset_base), + 0, + mut_ptr(&mut guest_reset_size), + AllocationType::MEM_RESET.bits(), + PageProtection::PAGE_NOACCESS.bits(), + ); + assert_eq!(guest_reset_status, host_reset_status); + assert_eq!( + guest_reset_base - guest_base, + host_reset_base as usize - host_base as usize + ); + assert_eq!(guest_reset_size, host_reset_size); + + release_allocation(&task, guest_base); + + let mut host_release_size = 0usize; + // SAFETY: Releases the host allocation created by this test. + let host_free_status = unsafe { + host_status(NtFreeVirtualMemory( + current_process(), + &raw mut host_base, + &raw mut host_release_size, + FreeType::MEM_RELEASE.bits(), + )) + }; + assert_eq!(host_free_status, NtStatus::SUCCESS); + }); + } + + #[test] + fn protect_virtual_memory_outputs_match_host_ntdll() { + run_with_test_platform_pointers(|| { + let mut host_base = core::ptr::null_mut::(); + let mut host_region_size = PAGE_SIZE * 2 - 1; + // SAFETY: The output pointers are valid local variables and the pseudo process + // handle targets the current process. The allocation is released before return. + let host_allocate_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_base, + 0, + &raw mut host_region_size, + (AllocationType::MEM_RESERVE | AllocationType::MEM_COMMIT).bits(), + PageProtection::PAGE_READWRITE.bits(), + )) + }; + assert_eq!(host_allocate_status, NtStatus::SUCCESS); + + let mut host_protect_base = host_base.wrapping_byte_add(1); + let mut host_protect_size = 1usize; + let mut host_old_protect = 0u32; + // SAFETY: The host allocation above covers the requested byte; all output pointers + // are valid locals and ntdll does not retain them. + let host_protect_status = unsafe { + host_status(NtProtectVirtualMemory( + current_process(), + &raw mut host_protect_base, + &raw mut host_protect_size, + PageProtection::PAGE_READONLY.bits(), + &raw mut host_old_protect, + )) + }; + + let task = crate::tests::test_task(); + let (guest_base, _) = allocate_committed_rw(&task, PAGE_SIZE * 2 - 1); + let mut guest_protect_base = guest_base + 1; + let mut guest_protect_size = 1usize; + let mut guest_old_protect = 0u32; + let guest_protect_status = task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_protect_base), + mut_ptr(&mut guest_protect_size), + PageProtection::PAGE_READONLY.bits(), + mut_ptr(&mut guest_old_protect), + ); + + assert_eq!(guest_protect_status, host_protect_status); + assert_eq!(guest_old_protect, host_old_protect); + assert_eq!(guest_protect_base, guest_base); + assert_eq!(guest_protect_size, host_protect_size); + + release_allocation(&task, guest_base); + + let mut host_release_size = 0usize; + // SAFETY: Releases the host allocation created by this test. + let host_free_status = unsafe { + host_status(NtFreeVirtualMemory( + current_process(), + &raw mut host_base, + &raw mut host_release_size, + FreeType::MEM_RELEASE.bits(), + )) + }; + assert_eq!(host_free_status, NtStatus::SUCCESS); + }); + } + + #[test] + fn protect_virtual_memory_mixed_committed_protections_match_host_ntdll() { + run_with_test_platform_pointers(|| { + let mut host_base = core::ptr::null_mut::(); + let mut host_region_size = PAGE_SIZE * 2; + // SAFETY: The output pointers are valid local variables and the pseudo process + // handle targets the current process. The allocation is released before return. + let host_allocate_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_base, + 0, + &raw mut host_region_size, + (AllocationType::MEM_RESERVE | AllocationType::MEM_COMMIT).bits(), + PageProtection::PAGE_READWRITE.bits(), + )) + }; + assert_eq!(host_allocate_status, NtStatus::SUCCESS); + + let mut host_second_page_base = host_base.wrapping_byte_add(PAGE_SIZE); + let mut host_second_page_size = PAGE_SIZE; + let mut host_second_old_protect = 0u32; + // SAFETY: The host allocation above covers the requested second page; output + // pointers are valid locals and ntdll does not retain them. + let host_second_protect_status = unsafe { + host_status(NtProtectVirtualMemory( + current_process(), + &raw mut host_second_page_base, + &raw mut host_second_page_size, + PageProtection::PAGE_READONLY.bits(), + &raw mut host_second_old_protect, + )) + }; + assert_eq!(host_second_protect_status, NtStatus::SUCCESS); + + let task = crate::tests::test_task(); + let (guest_base, _) = allocate_committed_rw(&task, PAGE_SIZE * 2); + let mut guest_second_page_base = guest_base + PAGE_SIZE; + let mut guest_second_page_size = PAGE_SIZE; + let mut guest_second_old_protect = 0u32; + let guest_second_protect_status = task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_second_page_base), + mut_ptr(&mut guest_second_page_size), + PageProtection::PAGE_READONLY.bits(), + mut_ptr(&mut guest_second_old_protect), + ); + assert_eq!(guest_second_protect_status, host_second_protect_status); + assert_eq!(guest_second_old_protect, host_second_old_protect); + assert_eq!( + guest_second_page_base - guest_base, + host_second_page_base as usize - host_base as usize + ); + assert_eq!(guest_second_page_size, host_second_page_size); + + let mut host_mixed_base = host_base; + let mut host_mixed_size = PAGE_SIZE * 2; + let mut host_mixed_old_protect = 0u32; + // SAFETY: The host range is fully committed with mixed protections; output + // pointers are valid locals and ntdll does not retain them. + let host_mixed_protect_status = unsafe { + host_status(NtProtectVirtualMemory( + current_process(), + &raw mut host_mixed_base, + &raw mut host_mixed_size, + PageProtection::PAGE_EXECUTE_READ.bits(), + &raw mut host_mixed_old_protect, + )) + }; + + let mut guest_mixed_base = guest_base; + let mut guest_mixed_size = PAGE_SIZE * 2; + let mut guest_mixed_old_protect = 0u32; + let guest_mixed_protect_status = task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_mixed_base), + mut_ptr(&mut guest_mixed_size), + PageProtection::PAGE_EXECUTE_READ.bits(), + mut_ptr(&mut guest_mixed_old_protect), + ); + assert_eq!(guest_mixed_protect_status, host_mixed_protect_status); + assert_eq!(guest_mixed_old_protect, host_mixed_old_protect); + assert_eq!(guest_mixed_base, guest_base); + assert_eq!(host_mixed_base, host_base); + assert_eq!(guest_mixed_size, host_mixed_size); + + release_allocation(&task, guest_base); + + let mut host_release_size = 0usize; + // SAFETY: Releases the host allocation created by this test. + let host_free_status = unsafe { + host_status(NtFreeVirtualMemory( + current_process(), + &raw mut host_base, + &raw mut host_release_size, + FreeType::MEM_RELEASE.bits(), + )) + }; + assert_eq!(host_free_status, NtStatus::SUCCESS); + }); + } + + #[test] + fn protect_virtual_memory_uncommitted_ranges_match_host_ntdll() { + run_with_test_platform_pointers(|| { + let mut host_base = core::ptr::null_mut::(); + let mut host_region_size = PAGE_SIZE * 2; + // SAFETY: The output pointers are valid locals and the current-process pseudo + // handle targets this process. The allocation is released before return. + let host_reserve_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_base, + 0, + &raw mut host_region_size, + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + )) + }; + assert_eq!(host_reserve_status, NtStatus::SUCCESS); + + let task = crate::tests::test_task(); + let mut guest_base = 0usize; + let mut guest_region_size = PAGE_SIZE * 2; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_base), + 0, + mut_ptr(&mut guest_region_size), + AllocationType::MEM_RESERVE.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + host_reserve_status + ); + + let mut host_protect_base = host_base; + let mut host_protect_size = PAGE_SIZE; + let mut host_old_protect = u32::MAX; + // SAFETY: The host range is reserved but uncommitted; output pointers are valid + // locals and ntdll does not retain them. + let host_reserved_protect_status = unsafe { + host_status(NtProtectVirtualMemory( + current_process(), + &raw mut host_protect_base, + &raw mut host_protect_size, + PageProtection::PAGE_READONLY.bits(), + &raw mut host_old_protect, + )) + }; + + let mut guest_protect_base = guest_base; + let mut guest_protect_size = PAGE_SIZE; + let mut guest_old_protect = u32::MAX; + let guest_reserved_protect_status = task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_protect_base), + mut_ptr(&mut guest_protect_size), + PageProtection::PAGE_READONLY.bits(), + mut_ptr(&mut guest_old_protect), + ); + assert_eq!(guest_reserved_protect_status, host_reserved_protect_status); + assert_eq!( + guest_protect_base - guest_base, + host_protect_base as usize - host_base as usize + ); + assert_eq!(guest_protect_size, host_protect_size); + assert_eq!(guest_old_protect, host_old_protect); + + let mut host_commit_base = host_base; + let mut host_commit_size = PAGE_SIZE; + // SAFETY: Commits the first page inside the live host reservation; output pointers + // are valid locals and the reservation is released before return. + let host_commit_status = unsafe { + host_status(NtAllocateVirtualMemory( + current_process(), + &raw mut host_commit_base, + 0, + &raw mut host_commit_size, + AllocationType::MEM_COMMIT.bits(), + PageProtection::PAGE_READWRITE.bits(), + )) + }; + assert_eq!(host_commit_status, NtStatus::SUCCESS); + + let mut guest_commit_base = guest_base; + let mut guest_commit_size = PAGE_SIZE; + assert_eq!( + task.sys_nt_allocate_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_commit_base), + 0, + mut_ptr(&mut guest_commit_size), + AllocationType::MEM_COMMIT.bits(), + PageProtection::PAGE_READWRITE.bits(), + ), + host_commit_status + ); + + let mut host_mixed_protect_base = host_base; + let mut host_mixed_protect_size = PAGE_SIZE * 2; + let mut host_mixed_old_protect = u32::MAX; + // SAFETY: The host range spans one committed page and one reserved page; output + // pointers are valid locals and ntdll does not retain them. + let host_mixed_protect_status = unsafe { + host_status(NtProtectVirtualMemory( + current_process(), + &raw mut host_mixed_protect_base, + &raw mut host_mixed_protect_size, + PageProtection::PAGE_READONLY.bits(), + &raw mut host_mixed_old_protect, + )) + }; + + let mut guest_mixed_protect_base = guest_base; + let mut guest_mixed_protect_size = PAGE_SIZE * 2; + let mut guest_mixed_old_protect = u32::MAX; + let guest_mixed_protect_status = task.sys_nt_protect_virtual_memory( + ProcessHandle::CURRENT, + mut_ptr(&mut guest_mixed_protect_base), + mut_ptr(&mut guest_mixed_protect_size), + PageProtection::PAGE_READONLY.bits(), + mut_ptr(&mut guest_mixed_old_protect), + ); + assert_eq!(guest_mixed_protect_status, host_mixed_protect_status); + assert_eq!( + guest_mixed_protect_base - guest_base, + host_mixed_protect_base as usize - host_base as usize + ); + assert_eq!(guest_mixed_protect_size, host_mixed_protect_size); + assert_eq!(guest_mixed_old_protect, host_mixed_old_protect); + + release_allocation(&task, guest_base); + + let mut host_release_size = 0usize; + // SAFETY: Releases the host allocation created by this test. + let host_free_status = unsafe { + host_status(NtFreeVirtualMemory( + current_process(), + &raw mut host_base, + &raw mut host_release_size, + FreeType::MEM_RELEASE.bits(), + )) + }; + assert_eq!(host_free_status, NtStatus::SUCCESS); + }); + } + } +} diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index c48df24255..6440c7cc4c 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -2,6 +2,7 @@ // Licensed under the MIT license. pub(crate) mod file; +pub(crate) mod mm; pub(crate) mod nls; pub(crate) mod registry; pub(crate) mod sysinfo; @@ -69,11 +70,6 @@ impl ProcessHandle { Self(Handle::from_raw(raw)) } - #[must_use] - pub(crate) const fn as_raw(self) -> usize { - self.0.as_raw() - } - #[must_use] pub(crate) const fn is_null(self) -> bool { self.0.is_null() @@ -172,6 +168,36 @@ pub(crate) enum SyscallRequest { allocation_type: u32, protect: u32, }, + NtAllocateVirtualMemoryEx { + process_handle: ProcessHandle, + base_address: Platform::RawMutPointer, + region_size: Platform::RawMutPointer, + allocation_type: u32, + protect: u32, + extended_parameters: Option>, + extended_parameter_count: u32, + }, + NtFreeVirtualMemory { + process_handle: ProcessHandle, + base_address: Platform::RawMutPointer, + region_size: Platform::RawMutPointer, + free_type: u32, + }, + NtProtectVirtualMemory { + process_handle: ProcessHandle, + base_address: Platform::RawMutPointer, + region_size: Platform::RawMutPointer, + new_protect: u32, + old_protect: Platform::RawMutPointer, + }, + NtQueryVirtualMemory { + process_handle: ProcessHandle, + base_address: usize, + memory_information_class: u32, + memory_information: Platform::RawMutPointer, + memory_information_length: usize, + return_length: Option>, + }, NtTerminateProcess { process_handle: ProcessHandle, exit_status: i32, @@ -285,6 +311,36 @@ impl SyscallRequest { allocation_type, protect, })), + NtSysno::NtAllocateVirtualMemoryEx => Some(sys_req!(NtAllocateVirtualMemoryEx { + process_handle: { ProcessHandle::from_raw }, + base_address:*, + region_size:*, + allocation_type, + protect, + extended_parameters:*, + extended_parameter_count, + })), + NtSysno::NtFreeVirtualMemory => Some(sys_req!(NtFreeVirtualMemory { + process_handle: { ProcessHandle::from_raw }, + base_address:*, + region_size:*, + free_type, + })), + NtSysno::NtProtectVirtualMemory => Some(sys_req!(NtProtectVirtualMemory { + process_handle: { ProcessHandle::from_raw }, + base_address:*, + region_size:*, + new_protect, + old_protect:*, + })), + NtSysno::NtQueryVirtualMemory => Some(sys_req!(NtQueryVirtualMemory { + process_handle: { ProcessHandle::from_raw }, + base_address, + memory_information_class, + memory_information:*, + memory_information_length, + return_length:*, + })), NtSysno::NtTerminateProcess => Some(sys_req!(NtTerminateProcess { process_handle: { ProcessHandle::from_raw }, exit_status, diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 560a1260c6..7aff2a3415 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -123,6 +123,9 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task::new(RawDescriptorStorage::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), + virtual_allocations: crate::WindowsVirtualAllocations::::new( + BTreeMap::new(), + ), system_lcid: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), user_lcid: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), user_ui_language: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), From b82edf78f6a8402535f01fd4f548ce061c52b1fa Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 12 Jun 2026 16:02:49 -0700 Subject: [PATCH 027/319] Implement Windows NT event object support (#912) This adds support for: `NtCreateEvent`, `NtOpenEvent`, `NtSetEvent`, `NtResetEvent`, `NtClearEvent`, `NtPulseEvent`, `NtQueryEvent`, `NtSetEventBoostPriority`. --- litebox_shim_windows/src/lib.rs | 99 +- litebox_shim_windows/src/syscalls/event.rs | 1198 +++++++++++++++++ litebox_shim_windows/src/syscalls/file.rs | 29 +- litebox_shim_windows/src/syscalls/mod.rs | 78 ++ litebox_shim_windows/src/syscalls/registry.rs | 42 +- litebox_shim_windows/src/tests.rs | 26 + 6 files changed, 1415 insertions(+), 57 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/event.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index a41da96c5d..c5bfe697a4 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -12,7 +12,8 @@ extern crate alloc; use alloc::collections::BTreeMap; -use alloc::sync::Arc; +use alloc::string::String; +use alloc::sync::{Arc, Weak}; use alloc::vec::Vec; use core::marker::PhantomData; use core::sync::atomic::{AtomicI32, AtomicU32, Ordering}; @@ -30,6 +31,7 @@ use litebox::sync::RawSyncPrimitivesProvider; use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; +use crate::syscalls::event::{EventHandleObject, EventObject, EventSubsystem}; use crate::syscalls::file::{FileObject, FileObjectSubsystem}; use crate::syscalls::registry::{RegistryKeyObject, RegistryKeySubsystem}; use crate::syscalls::{SyscallRequest, mm}; @@ -75,6 +77,8 @@ pub(crate) type WindowsNlsSectionMappings = litebox::sync::RwLock>; pub(crate) type WindowsVirtualAllocations = litebox::sync::RwLock>; +pub(crate) type WindowsEventNamespace = + litebox::sync::RwLock>>>; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct WindowsVirtualAllocation { @@ -309,6 +313,7 @@ impl WindowsShim { ntdll_mapping: load_info.ntdll_mapping, peb_address: load_info.environment.peb, handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), + event_namespace: WindowsEventNamespace::::new(BTreeMap::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), virtual_allocations: load_info.virtual_allocations, system_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), @@ -345,10 +350,11 @@ struct GlobalState { } /// Per-process Windows state shared by every thread in the process. -pub struct Process { +pub struct Process { ntdll_mapping: Option, peb_address: usize, handles: WindowsHandleStore, + event_namespace: WindowsEventNamespace, nls_section_mappings: WindowsNlsSectionMappings, virtual_allocations: WindowsVirtualAllocations, system_lcid: AtomicU32, @@ -357,7 +363,7 @@ pub struct Process { exit_code: AtomicI32, } -impl Process { +impl Process { /// Wait for the process to exit, returning its exit code. /// /// Currently a placeholder that returns a fixed exit code immediately. @@ -427,6 +433,76 @@ impl Task { let status = self.sys_nt_close(handle); (status, ContinueOperation::Resume) } + SyscallRequest::NtCreateEvent { + event_handle, + desired_access, + object_attributes, + event_type, + initial_state, + } => { + let status = self.sys_nt_create_event( + event_handle, + desired_access, + object_attributes, + event_type, + initial_state, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtOpenEvent { + event_handle, + desired_access, + object_attributes, + } => { + let status = + self.sys_nt_open_event(event_handle, desired_access, object_attributes); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtSetEvent { + event_handle, + previous_state, + } => { + let status = self.sys_nt_set_event(event_handle, previous_state); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtResetEvent { + event_handle, + previous_state, + } => { + let status = self.sys_nt_reset_event(event_handle, previous_state); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtClearEvent { event_handle } => { + let status = self.sys_nt_clear_event(event_handle); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtPulseEvent { + event_handle, + previous_state, + } => { + let status = self.sys_nt_pulse_event(event_handle, previous_state); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryEvent { + event_handle, + event_information_class, + event_information, + event_information_length, + return_length, + } => { + let status = self.sys_nt_query_event( + event_handle, + event_information_class, + event_information, + event_information_length, + return_length, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtSetEventBoostPriority { event_handle } => { + let status = self.sys_nt_set_event_boost_priority(event_handle); + (status, ContinueOperation::Resume) + } SyscallRequest::NtOpenFile { file_handle, desired_access, @@ -683,6 +759,9 @@ impl Task { (NtStatus::SUCCESS, ContinueOperation::Terminate) } } + SyscallRequest::NtManageHotPatch => { + (NtStatus::NOT_IMPLEMENTED, ContinueOperation::Resume) + } }; ctx.rax = result.as_raw().cast_unsigned() as usize; @@ -717,6 +796,14 @@ impl Task { ) { return NtStatus::SUCCESS; } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |event| visitor.event(event), + ) { + return NtStatus::SUCCESS; + } NtStatus::INVALID_HANDLE } @@ -736,6 +823,8 @@ trait RawHandleVisitor { fn file(&self, file: FileObject); fn registry_key(&self, key: RegistryKeyObject); + + fn event(&self, event: EventHandleObject); } struct CloseRawHandleVisitor<'task, Platform: ShimPlatform, FS: ShimFS> { @@ -752,6 +841,10 @@ impl RawHandleVisitor fn registry_key(&self, key: RegistryKeyObject) { self.task.close_registry_key(key); } + + fn event(&self, event: EventHandleObject) { + Task::::close_event(event); + } } /// The shim entrypoint object passed to the platform. diff --git a/litebox_shim_windows/src/syscalls/event.rs b/litebox_shim_windows/src/syscalls/event.rs new file mode 100644 index 0000000000..0a5b371e42 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/event.rs @@ -0,0 +1,1198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NT event object syscalls. + +use alloc::string::String; +use alloc::sync::{Arc, Weak}; +use core::marker::PhantomData; +use core::mem::size_of; + +use int_enum::IntEnum; +use litebox::event::{Events, IOPollable, observer::Observer, polling::Pollee}; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; +use litebox::sync::Mutex; +use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::nt_types::{AccessMask, ObjectAttributes, UnicodeString, read_object_attributes}; +use crate::syscalls::Handle; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, + raw_handle_entry, remove_raw_handle, +}; + +const OBJ_CASE_INSENSITIVE: u32 = 0x0000_0040; +const OBJ_OPENIF: u32 = 0x0000_0080; +const OBJ_OPENLINK: u32 = 0x0000_0100; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +pub(crate) enum EventType { + Notification = 0, + Synchronization = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +pub(crate) struct EventBasicInformation { + event_type: u32, + event_state: i32, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum EventInformationClass { + Basic = 0, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct EventAccess: u32 { + const QUERY_STATE = 0x0001; + const MODIFY_STATE = 0x0002; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() | Self::QUERY_STATE.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits() | Self::MODIFY_STATE.bits(); + const EXECUTE = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() | AccessMask::SYNCHRONIZE.bits(); + const ALL_ACCESS = AccessMask::STANDARD_RIGHTS_ALL.bits() + | Self::QUERY_STATE.bits() + | Self::MODIFY_STATE.bits(); + + const _ = !0; + } +} + +impl EventAccess { + fn from_desired_access(desired_access: u32) -> Self { + let mut access = Self::from_bits_retain(desired_access); + if desired_access & AccessMask::GENERIC_READ.bits() != 0 { + access.insert(Self::READ); + } + if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { + access.insert(Self::WRITE); + } + if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { + access.insert(Self::EXECUTE); + } + if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { + access.insert(Self::ALL_ACCESS); + } + access.remove(Self::from_bits_retain( + AccessMask::GENERIC_READ.bits() + | AccessMask::GENERIC_WRITE.bits() + | AccessMask::GENERIC_EXECUTE.bits() + | AccessMask::GENERIC_ALL.bits(), + )); + access + } + + fn require(self, required: Self) -> Result<(), NtStatus> { + if self.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +pub(crate) struct EventSubsystem(PhantomData); + +impl FdEnabledSubsystem for EventSubsystem { + type Entry = EventHandleObject; +} + +impl FdEnabledSubsystemEntry for EventHandleObject {} + +pub(crate) struct EventHandleObject { + event: Arc>, + granted_access: EventAccess, +} + +pub(crate) struct EventObject { + event_type: EventType, + signaled: Mutex, + pollee: Pollee, +} + +impl EventObject { + fn new(event_type: EventType, initial_state: bool) -> Self { + Self { + event_type, + signaled: Mutex::new(initial_state), + pollee: Pollee::new(), + } + } + + fn set(&self) -> i32 { + let previous = self.replace_state(true); + if previous == 0 { + self.pollee.notify_observers(Events::IN); + } + previous + } + + fn set_boost_priority(&self) -> Result { + if self.event_type != EventType::Synchronization { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + } + Ok(self.set()) + } + + fn reset(&self) -> i32 { + self.replace_state(false) + } + + fn clear(&self) -> i32 { + self.replace_state(false) + } + + fn pulse(&self) -> i32 { + let previous = self.replace_state(true); + self.pollee.notify_observers(Events::IN); + self.replace_state(false); + previous + } + + fn query(&self) -> EventBasicInformation { + EventBasicInformation { + event_type: self.event_type as u32, + event_state: i32::from(*self.signaled.lock()), + } + } + + fn replace_state(&self, next: bool) -> i32 { + let mut signaled = self.signaled.lock(); + let previous = i32::from(*signaled); + *signaled = next; + previous + } +} + +impl IOPollable for EventObject { + fn register_observer(&self, observer: Weak>, mask: Events) { + self.pollee.register_observer(observer, mask); + } + + fn check_io_events(&self) -> Events { + if *self.signaled.lock() { + Events::IN + } else { + Events::empty() + } + } +} + +struct EventName { + key: String, +} + +const EVENT_BASIC_INFORMATION_SIZE_U32: u32 = 8; +const _: () = + assert!(size_of::() == EVENT_BASIC_INFORMATION_SIZE_U32 as usize); + +fn read_event_name( + object_name: usize, + object_attributes: &ObjectAttributes, +) -> Result, NtStatus> { + if object_name == 0 { + if !object_attributes.root_directory.is_null() { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + return Ok(None); + } + if !object_attributes.root_directory.is_null() { + return Err(NtStatus::OBJECT_PATH_NOT_FOUND); + } + + let unicode_string = ConstPtr::::from_usize(object_name) + .read_at_offset(0) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + if unicode_string.length == 0 || !unicode_string.length.is_multiple_of(2) { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + if unicode_string.buffer == 0 { + return Err(NtStatus::ACCESS_VIOLATION); + } + let mut key = unicode_string.read_string::()?; + if key.is_empty() { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + if object_attributes.attributes & OBJ_CASE_INSENSITIVE != 0 { + key = key.to_ascii_lowercase(); + } + Ok(Some(EventName { key })) +} + +fn read_event_object_attributes( + object_attributes: Option>, + require_name: bool, +) -> Result<(Option, Option), NtStatus> { + let Some(object_attributes_ptr) = object_attributes else { + if require_name { + return Err(NtStatus::INVALID_PARAMETER); + } + return Ok((None, None)); + }; + let object_attributes = read_object_attributes::(object_attributes_ptr)?; + if object_attributes.attributes & OBJ_OPENLINK != 0 { + return Err(NtStatus::INVALID_PARAMETER); + } + let event_name = + read_event_name::(object_attributes.object_name, &object_attributes)?; + if require_name && event_name.is_none() { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + Ok((Some(object_attributes), event_name)) +} + +impl Task { + fn event_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + raw_handle_entry::>( + &self.global.litebox, + &self.process.handles, + handle, + ) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn insert_event_handle( + &self, + event: Arc>, + granted_access: EventAccess, + ) -> Result { + let typed = self + .global + .litebox + .descriptor_table_mut() + .insert::>(EventHandleObject { + event, + granted_access, + }); + insert_raw_handle::>( + &self.global.litebox, + &self.process.handles, + typed, + drop, + ) + } + + pub(crate) fn close_event_handle(&self, handle: Handle) { + remove_raw_handle::>( + &self.global.litebox, + &self.process.handles, + handle, + drop, + ); + } + + pub(crate) fn close_event(event: EventHandleObject) { + drop(event); + } + + pub(crate) fn sys_nt_create_event( + &self, + event_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + event_type: u32, + initial_state: u8, + ) -> NtStatus { + let Ok(event_type) = EventType::try_from(event_type) else { + return NtStatus::INVALID_PARAMETER; + }; + if let Err(status) = probe_guest_output_preserving_value::(event_handle) { + return status; + } + + let (object_attributes, event_name) = + match read_event_object_attributes::(object_attributes, false) { + Ok(value) => value, + Err(status) => return status, + }; + let granted_access = EventAccess::from_desired_access(desired_access); + + if let Some(event_name) = event_name { + let mut namespace = self.process.event_namespace.write(); + let existing = + if let Some(event) = namespace.get(&event_name.key).and_then(Weak::upgrade) { + Some(event) + } else { + namespace.remove(&event_name.key); + None + }; + if let Some(event) = existing { + let Some(object_attributes) = object_attributes else { + return NtStatus::INVALID_PARAMETER; + }; + if object_attributes.attributes & OBJ_OPENIF == 0 { + return NtStatus::OBJECT_NAME_COLLISION; + } + let Ok(handle) = self.insert_event_handle(event, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if event_handle.write_at_offset(0, handle).is_none() { + self.close_event_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + return NtStatus::OBJECT_NAME_EXISTS; + } + + let event = Arc::new(EventObject::new(event_type, initial_state != 0)); + let Ok(handle) = self.insert_event_handle(event.clone(), granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if event_handle.write_at_offset(0, handle).is_none() { + self.close_event_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + namespace.insert(event_name.key, Arc::downgrade(&event)); + return NtStatus::SUCCESS; + } + + let event = Arc::new(EventObject::new(event_type, initial_state != 0)); + let Ok(handle) = self.insert_event_handle(event, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if event_handle.write_at_offset(0, handle).is_none() { + self.close_event_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_open_event( + &self, + event_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(event_handle) { + return status; + } + let event_name = match read_event_object_attributes::(object_attributes, true) { + Ok((_, Some(event_name))) => event_name, + Ok((_, None)) => return NtStatus::OBJECT_NAME_INVALID, + Err(status) => return status, + }; + let event = { + let mut namespace = self.process.event_namespace.write(); + if let Some(event) = namespace.get(&event_name.key).and_then(Weak::upgrade) { + event + } else { + namespace.remove(&event_name.key); + return NtStatus::OBJECT_NAME_NOT_FOUND; + } + }; + + let Ok(handle) = + self.insert_event_handle(event, EventAccess::from_desired_access(desired_access)) + else { + return NtStatus::QUOTA_EXCEEDED; + }; + if event_handle.write_at_offset(0, handle).is_none() { + self.close_event_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_set_event( + &self, + event_handle: Handle, + previous_state: Option>, + ) -> NtStatus { + if let Some(previous_state) = previous_state + && let Err(status) = probe_guest_output_preserving_value::(previous_state) + { + return status; + } + + match self.modify_event(event_handle, previous_state, |event| Ok(event.set())) { + Ok(()) => NtStatus::SUCCESS, + Err(status) => status, + } + } + + pub(crate) fn sys_nt_reset_event( + &self, + event_handle: Handle, + previous_state: Option>, + ) -> NtStatus { + if let Some(previous_state) = previous_state + && let Err(status) = probe_guest_output_preserving_value::(previous_state) + { + return status; + } + + match self.modify_event(event_handle, previous_state, |event| Ok(event.reset())) { + Ok(()) => NtStatus::SUCCESS, + Err(status) => status, + } + } + + pub(crate) fn sys_nt_clear_event(&self, event_handle: Handle) -> NtStatus { + match self.modify_event(event_handle, None, |event| Ok(event.clear())) { + Ok(()) => NtStatus::SUCCESS, + Err(status) => status, + } + } + + pub(crate) fn sys_nt_pulse_event( + &self, + event_handle: Handle, + previous_state: Option>, + ) -> NtStatus { + if let Some(previous_state) = previous_state + && let Err(status) = probe_guest_output_preserving_value::(previous_state) + { + return status; + } + + match self.modify_event(event_handle, previous_state, |event| Ok(event.pulse())) { + Ok(()) => NtStatus::SUCCESS, + Err(status) => status, + } + } + + pub(crate) fn sys_nt_set_event_boost_priority(&self, event_handle: Handle) -> NtStatus { + match self.modify_event(event_handle, None, EventObject::set_boost_priority) { + Ok(()) => NtStatus::SUCCESS, + Err(status) => status, + } + } + + pub(crate) fn sys_nt_query_event( + &self, + event_handle: Handle, + event_information_class: u32, + event_information: MutPtr, + event_information_length: u32, + return_length: Option>, + ) -> NtStatus { + let Ok(EventInformationClass::Basic) = + EventInformationClass::try_from(event_information_class) + else { + return NtStatus::INVALID_INFO_CLASS; + }; + if event_information_length as usize != size_of::() { + return NtStatus::INFO_LENGTH_MISMATCH; + } + if let Err(status) = probe_guest_output_preserving_value::(event_information) { + return status; + } + if let Some(return_length) = return_length + && let Err(status) = probe_guest_output_preserving_value::(return_length) + { + return status; + } + + let Ok(entry) = self.event_entry(event_handle) else { + return NtStatus::INVALID_HANDLE; + }; + let query = entry.with_entry(|entry| { + entry + .granted_access + .require(EventAccess::QUERY_STATE) + .map(|()| entry.event.query()) + }); + let info = match query { + Ok(info) => info, + Err(status) => return status, + }; + if event_information.write_at_offset(0, info).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(return_length) = return_length + && return_length + .write_at_offset(0, EVENT_BASIC_INFORMATION_SIZE_U32) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + fn modify_event( + &self, + event_handle: Handle, + previous_state: Option>, + op: impl FnOnce(&EventObject) -> Result, + ) -> Result<(), NtStatus> { + let entry = self.event_entry(event_handle)?; + let previous = entry.with_entry(|entry| { + entry.granted_access.require(EventAccess::MODIFY_STATE)?; + op(&entry.event) + })?; + if let Some(previous_state) = previous_state + && previous_state.write_at_offset(0, previous).is_none() + { + return Err(NtStatus::ACCESS_VIOLATION); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + use core::mem::size_of; + + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::ObjectAttributes; + use crate::tests::{const_ptr, mut_ptr, object_attributes, test_task, unicode_string}; + + const EVENT_QUERY_STATE: u32 = 0x0001; + const EVENT_MODIFY_STATE: u32 = 0x0002; + const EVENT_ALL_ACCESS: u32 = 0x001f_0003; + + fn event_basic_information_size() -> u32 { + u32::try_from(size_of::()) + .expect("EVENT_BASIC_INFORMATION fits in ULONG") + } + + fn object_attributes_size() -> u32 { + u32::try_from(size_of::()).expect("OBJECT_ATTRIBUTES fits in ULONG") + } + + #[test] + fn create_rejects_invalid_event_type() { + let task = test_task(); + let mut handle = Handle::from_raw(usize::MAX); + + assert_eq!( + task.sys_nt_create_event(mut_ptr(&mut handle), EVENT_ALL_ACCESS, None, 2, 0), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::from_raw(usize::MAX)); + } + + #[test] + fn set_reset_clear_pulse_return_previous_state() { + let task = test_task(); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut handle), + EVENT_ALL_ACCESS, + None, + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + + let mut previous = -1; + assert_eq!( + task.sys_nt_set_event(handle, Some(mut_ptr(&mut previous))), + NtStatus::SUCCESS + ); + assert_eq!(previous, 0); + assert_eq!( + task.sys_nt_set_event(handle, Some(mut_ptr(&mut previous))), + NtStatus::SUCCESS + ); + assert_eq!(previous, 1); + assert_eq!( + task.sys_nt_reset_event(handle, Some(mut_ptr(&mut previous))), + NtStatus::SUCCESS + ); + assert_eq!(previous, 1); + assert_eq!( + task.sys_nt_reset_event(handle, Some(mut_ptr(&mut previous))), + NtStatus::SUCCESS + ); + assert_eq!(previous, 0); + + assert_eq!(task.sys_nt_set_event(handle, None), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_clear_event(handle), NtStatus::SUCCESS); + assert_eq!( + task.sys_nt_pulse_event(handle, Some(mut_ptr(&mut previous))), + NtStatus::SUCCESS + ); + assert_eq!(previous, 0); + } + + #[test] + fn query_event_reports_type_state_and_return_length() { + let task = test_task(); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut handle), + EVENT_ALL_ACCESS, + None, + EventType::Synchronization as u32, + 1, + ), + NtStatus::SUCCESS + ); + + let mut info = EventBasicInformation { + event_type: 99, + event_state: -1, + }; + let mut return_length = 0; + assert_eq!( + task.sys_nt_query_event( + handle, + EventInformationClass::Basic as u32, + mut_ptr(&mut info), + event_basic_information_size(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::SUCCESS + ); + assert_eq!( + info, + EventBasicInformation { + event_type: EventType::Synchronization as u32, + event_state: 1, + } + ); + assert_eq!(return_length, event_basic_information_size()); + } + + #[test] + fn query_validates_class_and_exact_length() { + let task = test_task(); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut handle), + EVENT_ALL_ACCESS, + None, + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + let mut info = EventBasicInformation { + event_type: 0, + event_state: 0, + }; + + assert_eq!( + task.sys_nt_query_event( + handle, + 1, + mut_ptr(&mut info), + event_basic_information_size(), + None, + ), + NtStatus::INVALID_INFO_CLASS + ); + assert_eq!( + task.sys_nt_query_event( + handle, + EventInformationClass::Basic as u32, + mut_ptr(&mut info), + event_basic_information_size() - 1, + None, + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + } + + #[test] + fn handle_access_is_enforced() { + let task = test_task(); + let mut query_only = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut query_only), + EVENT_QUERY_STATE, + None, + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + let mut modify_only = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut modify_only), + EVENT_MODIFY_STATE, + None, + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + + assert_eq!( + task.sys_nt_set_event(query_only, None), + NtStatus::ACCESS_DENIED + ); + + let mut info = EventBasicInformation { + event_type: 0, + event_state: 0, + }; + assert_eq!( + task.sys_nt_query_event( + modify_only, + EventInformationClass::Basic as u32, + mut_ptr(&mut info), + event_basic_information_size(), + None, + ), + NtStatus::ACCESS_DENIED + ); + } + + #[test] + fn named_event_open_shares_state() { + let task = test_task(); + let name_units: Vec = "\\BaseNamedObjects\\LiteBoxEvent".encode_utf16().collect(); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, OBJ_CASE_INSENSITIVE); + + let mut created = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut created), + EVENT_ALL_ACCESS, + Some(const_ptr(&attrs)), + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_open_event( + mut_ptr(&mut opened), + EVENT_ALL_ACCESS, + Some(const_ptr(&attrs)) + ), + NtStatus::SUCCESS + ); + assert_ne!(created, opened); + + assert_eq!(task.sys_nt_set_event(created, None), NtStatus::SUCCESS); + let mut info = EventBasicInformation { + event_type: 0, + event_state: 0, + }; + assert_eq!( + task.sys_nt_query_event( + opened, + EventInformationClass::Basic as u32, + mut_ptr(&mut info), + event_basic_information_size(), + None, + ), + NtStatus::SUCCESS + ); + assert_eq!(info.event_state, 1); + } + + #[test] + fn open_event_requires_existing_name() { + let task = test_task(); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_event(mut_ptr(&mut handle), EVENT_ALL_ACCESS, None), + NtStatus::INVALID_PARAMETER + ); + + let unnamed_attrs = ObjectAttributes { + length: object_attributes_size(), + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + assert_eq!( + task.sys_nt_open_event( + mut_ptr(&mut handle), + EVENT_ALL_ACCESS, + Some(const_ptr(&unnamed_attrs)), + ), + NtStatus::OBJECT_NAME_INVALID + ); + } + + #[test] + fn create_openif_existing_named_event_returns_name_exists() { + let task = test_task(); + let name_units: Vec = "\\BaseNamedObjects\\LiteBoxOpenIf".encode_utf16().collect(); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, OBJ_CASE_INSENSITIVE); + let openif_attrs = ObjectAttributes { + attributes: OBJ_CASE_INSENSITIVE | OBJ_OPENIF, + ..attrs + }; + + let mut first = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut first), + EVENT_ALL_ACCESS, + Some(const_ptr(&attrs)), + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + let mut collision = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut collision), + EVENT_ALL_ACCESS, + Some(const_ptr(&attrs)), + EventType::Notification as u32, + 0, + ), + NtStatus::OBJECT_NAME_COLLISION + ); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut collision), + EVENT_MODIFY_STATE, + Some(const_ptr(&openif_attrs)), + EventType::Notification as u32, + 0, + ), + NtStatus::OBJECT_NAME_EXISTS + ); + assert_eq!(task.sys_nt_set_event(collision, None), NtStatus::SUCCESS); + } + + #[test] + fn close_invalidates_event_handle() { + let task = test_task(); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut handle), + EVENT_ALL_ACCESS, + None, + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + assert_eq!( + task.sys_nt_set_event(handle, None), + NtStatus::INVALID_HANDLE + ); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + mod host_fidelity { + use core::ffi::c_void; + + use super::*; + + #[link(name = "ntdll")] + unsafe extern "system" { + fn NtCreateEvent( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + event_type: u32, + initial_state: u8, + ) -> i32; + fn NtOpenEvent( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + ) -> i32; + fn NtSetEvent(handle: *mut c_void, previous_state: *mut i32) -> i32; + fn NtResetEvent(handle: *mut c_void, previous_state: *mut i32) -> i32; + fn NtClearEvent(handle: *mut c_void) -> i32; + fn NtPulseEvent(handle: *mut c_void, previous_state: *mut i32) -> i32; + fn NtSetEventBoostPriority(handle: *mut c_void) -> i32; + fn NtQueryEvent( + handle: *mut c_void, + event_information_class: u32, + event_information: *mut EventBasicInformation, + event_information_length: u32, + return_length: *mut u32, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } + + fn assert_status_eq(shim: NtStatus, host: i32) { + assert_eq!(shim.as_raw(), host); + } + + fn close_host_handle(handle: *mut c_void) { + if !handle.is_null() { + // SAFETY: The handle was returned by a successful host ntdll call in this test. + let status = unsafe { NtClose(handle) }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + } + } + + fn host_query_event(handle: *mut c_void) -> (i32, EventBasicInformation, u32) { + let mut info = EventBasicInformation { + event_type: 0, + event_state: 0, + }; + let mut return_length = 0; + // SAFETY: `handle` is a live host event handle and the output pointers reference + // stack locals that are valid for the duration of the call. + let status = unsafe { + NtQueryEvent( + handle, + EventInformationClass::Basic as u32, + &raw mut info, + event_basic_information_size(), + &raw mut return_length, + ) + }; + (status, info, return_length) + } + + fn shim_query_event( + task: &Task, + handle: Handle, + ) -> (NtStatus, EventBasicInformation, u32) { + let mut info = EventBasicInformation { + event_type: 0, + event_state: 0, + }; + let mut return_length = 0; + let status = task.sys_nt_query_event( + handle, + EventInformationClass::Basic as u32, + mut_ptr(&mut info), + event_basic_information_size(), + Some(mut_ptr(&mut return_length)), + ); + (status, info, return_length) + } + + fn assert_queries_match( + task: &Task, + host_handle: *mut c_void, + shim_handle: Handle, + ) { + let (host_status, host_info, host_length) = host_query_event(host_handle); + let (shim_status, shim_info, shim_length) = shim_query_event(task, shim_handle); + assert_status_eq(shim_status, host_status); + assert_eq!(shim_info, host_info); + assert_eq!(shim_length, host_length); + } + + #[test] + fn create_query_reset_matches_host_outputs() { + let mut host_handle = core::ptr::null_mut(); + // SAFETY: The output pointer references a live stack local, null object attributes are + // accepted by NtCreateEvent, and the handle is closed before the test returns. + let host_create_status = unsafe { + NtCreateEvent( + &raw mut host_handle, + EVENT_ALL_ACCESS, + core::ptr::null(), + EventType::Notification as u32, + 1, + ) + }; + assert_eq!(host_create_status, NtStatus::SUCCESS.as_raw()); + let (host_query_status, host_info, host_length) = host_query_event(host_handle); + assert_eq!(host_query_status, NtStatus::SUCCESS.as_raw()); + let mut host_previous = 0; + // SAFETY: `host_handle` is a live event handle and `host_previous` is a valid output. + let host_reset_status = unsafe { NtResetEvent(host_handle, &raw mut host_previous) }; + + let task = test_task(); + let mut shim_handle = Handle::default(); + let shim_create_status = task.sys_nt_create_event( + mut_ptr(&mut shim_handle), + EVENT_ALL_ACCESS, + None, + EventType::Notification as u32, + 1, + ); + assert_status_eq(shim_create_status, host_create_status); + let (shim_query_status, shim_info, shim_length) = shim_query_event(&task, shim_handle); + assert_status_eq(shim_query_status, host_query_status); + let mut shim_previous = 0; + let shim_reset_status = + task.sys_nt_reset_event(shim_handle, Some(mut_ptr(&mut shim_previous))); + + assert_status_eq(shim_reset_status, host_reset_status); + assert_eq!(shim_info, host_info); + assert_eq!(shim_length, host_length); + assert_eq!(shim_previous, host_previous); + + close_host_handle(host_handle); + } + + #[test] + fn set_clear_pulse_and_boost_match_host_state() { + let mut host_handle = core::ptr::null_mut(); + // SAFETY: The output pointer references a live stack local, null object attributes are + // accepted by NtCreateEvent, and the handle is closed before the test returns. + let status = unsafe { + NtCreateEvent( + &raw mut host_handle, + EVENT_ALL_ACCESS, + core::ptr::null(), + EventType::Notification as u32, + 0, + ) + }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + + let task = test_task(); + let mut shim_handle = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut shim_handle), + EVENT_ALL_ACCESS, + None, + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + + let mut host_previous = -1; + let mut shim_previous = -1; + // SAFETY: `host_handle` is a live event handle and `host_previous` is a valid output. + let host_status = unsafe { NtSetEvent(host_handle, &raw mut host_previous) }; + let shim_status = task.sys_nt_set_event(shim_handle, Some(mut_ptr(&mut shim_previous))); + assert_status_eq(shim_status, host_status); + assert_eq!(shim_previous, host_previous); + assert_queries_match(&task, host_handle, shim_handle); + + // SAFETY: `host_handle` is a live event handle. + let host_status = unsafe { NtClearEvent(host_handle) }; + let shim_status = task.sys_nt_clear_event(shim_handle); + assert_status_eq(shim_status, host_status); + assert_queries_match(&task, host_handle, shim_handle); + + host_previous = -1; + shim_previous = -1; + // SAFETY: `host_handle` is a live event handle and `host_previous` is a valid output. + let host_status = unsafe { NtPulseEvent(host_handle, &raw mut host_previous) }; + let shim_status = + task.sys_nt_pulse_event(shim_handle, Some(mut_ptr(&mut shim_previous))); + assert_status_eq(shim_status, host_status); + assert_eq!(shim_previous, host_previous); + assert_queries_match(&task, host_handle, shim_handle); + + // SAFETY: `host_handle` is a live event handle. + let host_status = unsafe { NtSetEventBoostPriority(host_handle) }; + let shim_status = task.sys_nt_set_event_boost_priority(shim_handle); + assert_status_eq(shim_status, host_status); + assert_queries_match(&task, host_handle, shim_handle); + + close_host_handle(host_handle); + } + + #[test] + fn boost_priority_sets_synchronization_event() { + let mut host_handle = core::ptr::null_mut(); + // SAFETY: The output pointer references a live stack local, null object attributes are + // accepted by NtCreateEvent, and the handle is closed before the test returns. + let status = unsafe { + NtCreateEvent( + &raw mut host_handle, + EVENT_ALL_ACCESS, + core::ptr::null(), + EventType::Synchronization as u32, + 0, + ) + }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + + let task = test_task(); + let mut shim_handle = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut shim_handle), + EVENT_ALL_ACCESS, + None, + EventType::Synchronization as u32, + 0, + ), + NtStatus::SUCCESS + ); + + // SAFETY: `host_handle` is a live synchronization event handle. + let host_status = unsafe { NtSetEventBoostPriority(host_handle) }; + let shim_status = task.sys_nt_set_event_boost_priority(shim_handle); + assert_status_eq(shim_status, host_status); + assert_queries_match(&task, host_handle, shim_handle); + + close_host_handle(host_handle); + } + + #[test] + fn named_open_matches_host_state_sharing() { + let unique = 0u8; + let name_units: Vec = + alloc::format!(r"\BaseNamedObjects\LiteBoxEventFidelity{:p}", &unique,) + .encode_utf16() + .collect(); + let name = unicode_string(&name_units); + let attributes = object_attributes(&name, OBJ_CASE_INSENSITIVE); + + let mut host_created = core::ptr::null_mut(); + let mut host_opened = core::ptr::null_mut(); + // SAFETY: Pointers reference live stack locals and ObjectAttributes points to a live + // UnicodeString naming a BaseNamedObjects event for the duration of the calls. + let host_create_status = unsafe { + NtCreateEvent( + &raw mut host_created, + EVENT_ALL_ACCESS, + &raw const attributes, + EventType::Notification as u32, + 0, + ) + }; + assert_eq!(host_create_status, NtStatus::SUCCESS.as_raw()); + // SAFETY: Same live ObjectAttributes as above, and output pointer is valid. + let host_open_status = unsafe { + NtOpenEvent( + &raw mut host_opened, + EVENT_MODIFY_STATE, + &raw const attributes, + ) + }; + assert_eq!(host_open_status, NtStatus::SUCCESS.as_raw()); + + let task = test_task(); + let mut shim_created = Handle::default(); + let shim_create_status = task.sys_nt_create_event( + mut_ptr(&mut shim_created), + EVENT_ALL_ACCESS, + Some(const_ptr(&attributes)), + EventType::Notification as u32, + 0, + ); + assert_status_eq(shim_create_status, host_create_status); + let mut shim_opened = Handle::default(); + let shim_open_status = task.sys_nt_open_event( + mut_ptr(&mut shim_opened), + EVENT_MODIFY_STATE, + Some(const_ptr(&attributes)), + ); + assert_status_eq(shim_open_status, host_open_status); + + // SAFETY: `host_opened` is a live event handle opened with EVENT_MODIFY_STATE. + let host_set_status = unsafe { NtSetEvent(host_opened, core::ptr::null_mut()) }; + let shim_set_status = task.sys_nt_set_event(shim_opened, None); + assert_status_eq(shim_set_status, host_set_status); + assert_queries_match(&task, host_created, shim_created); + + close_host_handle(host_opened); + close_host_handle(host_created); + } + } +} diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 82e4c7165e..51d592ca0a 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -910,7 +910,9 @@ fn map_mkdir_error(error: MkdirError) -> NtStatus { #[cfg(test)] mod tests { use super::*; - use crate::tests::{TestFS, TestPlatform, const_ptr, mut_ptr, null_mut_ptr}; + use crate::tests::{ + TestFS, TestPlatform, const_ptr, mut_ptr, null_mut_ptr, object_attributes, unicode_string, + }; use litebox::fs::FileSystem as _; extern crate std; @@ -936,31 +938,10 @@ mod tests { ::run_test_thread(f) } - fn unicode_string(value: &[u16]) -> UnicodeString { - let byte_len = u16::try_from(core::mem::size_of_val(value)).unwrap(); - UnicodeString { - length: byte_len, - maximum_length: byte_len, - padding_0: [0; 4], - buffer: value.as_ptr() as usize, - } - } - fn utf16(value: &str) -> std::vec::Vec { value.encode_utf16().collect() } - fn object_attributes(name: &UnicodeString) -> ObjectAttributes { - ObjectAttributes { - length: u32::try_from(core::mem::size_of::()).unwrap(), - root_directory: Handle::default(), - object_name: core::ptr::from_ref(name) as usize, - attributes: 0, - security_descriptor: 0, - security_quality_of_service: 0, - } - } - fn open_object_attributes( path: &str, ) -> ( @@ -970,7 +951,7 @@ mod tests { ) { let path = utf16(path); let name = std::boxed::Box::new(unicode_string(&path)); - let attributes = object_attributes(&name); + let attributes = object_attributes(&name, 0); (path, name, attributes) } @@ -1666,7 +1647,7 @@ mod tests { } fn host_object_attributes(name: &UnicodeString) -> ObjectAttributes { - object_attributes(name) + object_attributes(name, 0) } fn close_host_handle(handle: *mut c_void) { diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 6440c7cc4c..6bc19d00cc 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +pub(crate) mod event; pub(crate) mod file; pub(crate) mod mm; pub(crate) mod nls; @@ -87,6 +88,43 @@ pub(crate) enum SyscallRequest { NtClose { handle: Handle, }, + NtCreateEvent { + event_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + event_type: u32, + initial_state: u8, + }, + NtOpenEvent { + event_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, + NtSetEvent { + event_handle: Handle, + previous_state: Option>, + }, + NtResetEvent { + event_handle: Handle, + previous_state: Option>, + }, + NtClearEvent { + event_handle: Handle, + }, + NtPulseEvent { + event_handle: Handle, + previous_state: Option>, + }, + NtQueryEvent { + event_handle: Handle, + event_information_class: u32, + event_information: Platform::RawMutPointer, + event_information_length: u32, + return_length: Option>, + }, + NtSetEventBoostPriority { + event_handle: Handle, + }, NtOpenFile { file_handle: Platform::RawMutPointer, desired_access: u32, @@ -202,6 +240,8 @@ pub(crate) enum SyscallRequest { process_handle: ProcessHandle, exit_status: i32, }, + /// TODO: not supported yet + NtManageHotPatch, } impl SyscallRequest { @@ -228,6 +268,43 @@ impl SyscallRequest { NtSysno::NtClose => Some(sys_req!(NtClose { handle: { Handle::from_raw }, })), + NtSysno::NtCreateEvent => Some(sys_req!(NtCreateEvent { + event_handle:*, + desired_access, + object_attributes:*, + event_type, + initial_state, + })), + NtSysno::NtOpenEvent => Some(sys_req!(NtOpenEvent { + event_handle:*, + desired_access, + object_attributes:*, + })), + NtSysno::NtSetEvent => Some(sys_req!(NtSetEvent { + event_handle:{Handle::from_raw}, + previous_state:*, + })), + NtSysno::NtResetEvent => Some(sys_req!(NtResetEvent { + event_handle:{Handle::from_raw}, + previous_state:*, + })), + NtSysno::NtClearEvent => Some(sys_req!(NtClearEvent { + event_handle: { Handle::from_raw }, + })), + NtSysno::NtPulseEvent => Some(sys_req!(NtPulseEvent { + event_handle:{Handle::from_raw}, + previous_state:*, + })), + NtSysno::NtQueryEvent => Some(sys_req!(NtQueryEvent { + event_handle:{Handle::from_raw}, + event_information_class, + event_information:*, + event_information_length, + return_length:*, + })), + NtSysno::NtSetEventBoostPriority => Some(sys_req!(NtSetEventBoostPriority { + event_handle: { Handle::from_raw }, + })), NtSysno::NtOpenFile => Some(sys_req!(NtOpenFile { file_handle:*, desired_access, @@ -345,6 +422,7 @@ impl SyscallRequest { process_handle: { ProcessHandle::from_raw }, exit_status, })), + NtSysno::NtManageHotPatch => Some(SyscallRequest::NtManageHotPatch), _ => None, } } diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index be0de0b49f..375cb170a0 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -839,7 +839,10 @@ fn map_read_error(error: ReadError) -> NtStatus { #[cfg(test)] mod tests { - use crate::tests::{TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_platform}; + use crate::tests::{ + TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, object_attributes, test_platform, + unicode_string, + }; use super::*; use core::mem::size_of; @@ -905,31 +908,10 @@ mod tests { fn RegDeleteTreeW(hKey: *mut core::ffi::c_void, lpSubKey: *const u16) -> i32; } - fn unicode_string(value: &[u16]) -> UnicodeString { - let byte_len = u16::try_from(core::mem::size_of_val(value)).unwrap(); - UnicodeString { - length: byte_len, - maximum_length: byte_len, - padding_0: [0; 4], - buffer: value.as_ptr() as usize, - } - } - fn utf16(value: &str) -> std::vec::Vec { value.encode_utf16().collect() } - fn object_attributes(name: &UnicodeString) -> ObjectAttributes { - ObjectAttributes { - length: u32::try_from(size_of::()).unwrap(), - root_directory: Handle::default(), - object_name: core::ptr::from_ref(name) as usize, - attributes: 0, - security_descriptor: 0, - security_quality_of_service: 0, - } - } - fn test_registry() -> (LiteBox, RegistryStore) { let litebox = LiteBox::new(test_platform()); let registry = RegistryStore::new(&litebox); @@ -946,7 +928,7 @@ mod tests { fn open_code_page_key(task: &Task) -> Handle { 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); + let object_attributes = object_attributes(&code_page_name, 0); open_key(task, object_attributes).expect("Failed to open code page key") } @@ -1165,13 +1147,13 @@ mod tests { let task = crate::tests::test_task(); 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); + let nls_object_attributes = object_attributes(&nls_name, 0); let nls_handle = open_key(&task, nls_object_attributes).expect("Failed to open NLS key"); assert_ne!(nls_handle, Handle::default()); let code_page_name = utf16("CodePage"); let code_page_name = unicode_string(&code_page_name); - let mut code_page_object_attributes = object_attributes(&code_page_name); + let mut code_page_object_attributes = object_attributes(&code_page_name, 0); code_page_object_attributes.root_directory = nls_handle; let code_page_handle = open_key(&task, code_page_object_attributes).expect("Failed to open code page key"); @@ -1183,7 +1165,7 @@ mod tests { let task = crate::tests::test_task(); let name = utf16("\\Registry\\Machine\\Software\\Missing"); let name = unicode_string(&name); - let object_attributes = object_attributes(&name); + let object_attributes = object_attributes(&name, 0); assert_eq!( open_key(&task, object_attributes).unwrap_err(), NtStatus::OBJECT_NAME_NOT_FOUND @@ -1195,7 +1177,7 @@ mod tests { let task = crate::tests::test_task(); let name = utf16("Child"); let name = unicode_string(&name); - let mut object_attributes = object_attributes(&name); + let mut object_attributes = object_attributes(&name, 0); object_attributes.root_directory = Handle::from_raw(0x1234); assert_eq!( open_key(&task, object_attributes).unwrap_err(), @@ -1216,7 +1198,7 @@ mod tests { let private_name = utf16(private_key); let private_name = unicode_string(&private_name); - let read_object_attributes = object_attributes(&private_name); + let read_object_attributes = object_attributes(&private_name, 0); assert_eq!( open_key(&task, read_object_attributes).unwrap_err(), NtStatus::ACCESS_DENIED @@ -1224,7 +1206,7 @@ mod tests { let private_name = utf16(private_key); let private_name = unicode_string(&private_name); - let write_object_attributes = object_attributes(&private_name); + let write_object_attributes = object_attributes(&private_name, 0); let handle = task .do_nt_open_key(RegistryKeyAccess::SET_VALUE.bits(), write_object_attributes) .expect("write-only access should use write filesystem permissions"); @@ -1312,7 +1294,7 @@ mod tests { let task = crate::tests::test_task(); 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); + let object_attributes = object_attributes(&code_page_name, 0); let key_handle = task .do_nt_open_key(RegistryKeyAccess::SET_VALUE.bits(), object_attributes) .expect("write-only open should succeed against the seeded registry store"); diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 7aff2a3415..c28f34ff00 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -6,12 +6,15 @@ extern crate std; use alloc::collections::BTreeMap; use alloc::sync::Arc; use core::marker::PhantomData; +use core::mem::size_of; use core::sync::atomic::{AtomicI32, AtomicU32}; use litebox::LiteBox; use litebox::fd::RawDescriptorStorage; use litebox::fs::{FileSystem as _, Mode, OFlags}; use litebox::platform::RawConstPointer as _; +use crate::nt_types::{ObjectAttributes, UnicodeString}; +use crate::syscalls::Handle; use crate::{ ConstPtr, DefaultFS, GlobalState, MutPtr, Process, Task, WindowsHandleStore, WindowsNlsSectionMappings, WindowsPageManager, @@ -46,6 +49,28 @@ pub(crate) fn null_mut_ptr() -> Mu MutPtr::::from_usize(0) } +pub(crate) fn unicode_string(units: &[u16]) -> UnicodeString { + let byte_len = u16::try_from(core::mem::size_of_val(units)).expect("test name fits in USHORT"); + UnicodeString { + length: byte_len, + maximum_length: byte_len, + padding_0: [0; 4], + buffer: units.as_ptr() as usize, + } +} + +pub(crate) fn object_attributes(name: &UnicodeString, attributes: u32) -> ObjectAttributes { + ObjectAttributes { + length: u32::try_from(size_of::()) + .expect("OBJECT_ATTRIBUTES fits in ULONG"), + root_directory: Handle::default(), + object_name: core::ptr::from_ref(name) as usize, + attributes, + security_descriptor: 0, + security_quality_of_service: 0, + } +} + pub(crate) fn test_platform() -> &'static TestPlatform { static PLATFORM: std::sync::OnceLock<&'static TestPlatform> = std::sync::OnceLock::new(); PLATFORM.get_or_init(|| { @@ -122,6 +147,7 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task::new(RawDescriptorStorage::new()), + event_namespace: crate::WindowsEventNamespace::::new(BTreeMap::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), virtual_allocations: crate::WindowsVirtualAllocations::::new( BTreeMap::new(), From 424a9d20d993e1d4a98987521fda014c79338966 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 12 Jun 2026 22:21:30 -0700 Subject: [PATCH 028/319] Add Windows shim support for system information (#914) Add Windows shim support for `NtQuerySystemInformation` and `NtQuerySystemInformationEx`. The implementation models a stable synthetic environment instead of exposing host topology: one processor, one NUMA entry, fixed memory/CPU/cache/flush values. --- litebox_shim_windows/src/lib.rs | 32 + litebox_shim_windows/src/syscalls/mm.rs | 2 +- litebox_shim_windows/src/syscalls/mod.rs | 30 +- litebox_shim_windows/src/syscalls/sysinfo.rs | 1401 +++++++++++++++++- 4 files changed, 1441 insertions(+), 24 deletions(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index c5bfe697a4..83f3b01143 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -643,6 +643,38 @@ impl Task { .sys_nt_query_performance_counter(performance_counter, performance_frequency); (status, ContinueOperation::Resume) } + SyscallRequest::NtQuerySystemInformation { + system_information_class, + system_information, + system_information_length, + return_length, + } => { + let status = Self::sys_nt_query_system_information( + system_information_class, + system_information, + system_information_length, + return_length, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQuerySystemInformationEx { + system_information_class, + input_buffer, + input_buffer_length, + system_information, + system_information_length, + return_length, + } => { + let status = Self::sys_nt_query_system_information_ex( + system_information_class, + input_buffer, + input_buffer_length, + system_information, + system_information_length, + return_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, source, diff --git a/litebox_shim_windows/src/syscalls/mm.rs b/litebox_shim_windows/src/syscalls/mm.rs index 269b233d3f..f67931a198 100644 --- a/litebox_shim_windows/src/syscalls/mm.rs +++ b/litebox_shim_windows/src/syscalls/mm.rs @@ -17,7 +17,7 @@ use crate::{ WindowsVirtualAllocation, WindowsVirtualAllocations, }; -const ALLOCATION_GRANULARITY: usize = 0x1_0000; +pub(super) const ALLOCATION_GRANULARITY: usize = 0x1_0000; const ALLOCATION_SEARCH_ATTEMPTS: usize = 8; const MEMORY_WORKING_SET_LIST_MIN_SIZE: usize = 16; const MEM_EXTENDED_PARAMETER_TYPE_MASK: u64 = 0xff; diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 6bc19d00cc..df467ea9e1 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -6,7 +6,7 @@ pub(crate) mod file; pub(crate) mod mm; pub(crate) mod nls; pub(crate) mod registry; -pub(crate) mod sysinfo; +mod sysinfo; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; @@ -192,6 +192,20 @@ pub(crate) enum SyscallRequest { performance_counter: Platform::RawMutPointer, performance_frequency: Option>, }, + NtQuerySystemInformation { + system_information_class: u32, + system_information: Platform::RawMutPointer, + system_information_length: u32, + return_length: Option>, + }, + NtQuerySystemInformationEx { + system_information_class: u32, + input_buffer: Option>, + input_buffer_length: u32, + system_information: Platform::RawMutPointer, + system_information_length: u32, + return_length: Option>, + }, NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag: u32, source: Platform::RawConstPointer, @@ -372,6 +386,20 @@ impl SyscallRequest { performance_counter:*, performance_frequency:*, })), + NtSysno::NtQuerySystemInformation => Some(sys_req!(NtQuerySystemInformation { + system_information_class, + system_information:*, + system_information_length, + return_length:*, + })), + NtSysno::NtQuerySystemInformationEx => Some(sys_req!(NtQuerySystemInformationEx { + system_information_class, + input_buffer:*, + input_buffer_length, + system_information:*, + system_information_length, + return_length:*, + })), NtSysno::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter => Some( sys_req!(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, diff --git a/litebox_shim_windows/src/syscalls/sysinfo.rs b/litebox_shim_windows/src/syscalls/sysinfo.rs index 0c1c320189..902ea224c5 100644 --- a/litebox_shim_windows/src/syscalls/sysinfo.rs +++ b/litebox_shim_windows/src/syscalls/sysinfo.rs @@ -1,14 +1,582 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox::platform::{Instant as _, RawConstPointer as _, RawMutPointer as _}; +use core::mem::size_of; + +use int_enum::IntEnum; +use litebox::platform::{ + Instant as _, PageManagementProvider, RawConstPointer as _, RawMutPointer as _, +}; +use litebox::utils::TruncateExt as _; use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; -use crate::{ConstPtr, MutPtr, ShimFS, ShimPlatform, Task}; +use crate::nt_types::GroupAffinity; +use crate::syscalls::mm::ALLOCATION_GRANULARITY; +use crate::{ConstPtr, MutPtr, PAGE_SIZE, ShimFS, ShimPlatform, Task}; const QPC_FREQUENCY_HZ: i64 = 1_000_000_000; +// These fixed values are deterministic sandbox answers: a default 15.625 ms +// timer tick, one synthetic processor, and a stable 4 GiB physical-memory view. +// They avoid leaking host topology while satisfying Windows CRT/environment +// probes that require plausible system-information success outputs. +const TIMER_RESOLUTION_100NS: u32 = 156_250; +const DEFAULT_PHYSICAL_PAGES: u32 = 1024 * 1024; +const NUMBER_OF_PROCESSORS: u8 = 1; +const PROCESSOR_AFFINITY_MASK: usize = (1usize << NUMBER_OF_PROCESSORS) - 1; +// SystemFlushInformation values observed from host ntdll on Windows 11 24H2. +// LiteBox keeps them fixed because they describe the synthetic CPU contract. +const SUPPORTED_FLUSH_METHODS: u32 = 0x7; +const SUPPORTED_FLUSH_PROCESSOR_FEATURES: u32 = 0x40; +const CACHE_UNIFIED: u32 = 0; +const DWORD_SIZE_U32: u32 = 4; +const NUMA_NODE_COUNT: usize = NUMBER_OF_PROCESSORS as usize; +const PROCESSOR_ARCHITECTURE_AMD64: u16 = 9; +const SYSTEM_VERIFIER_INFORMATION_LENGTH: u32 = 0x90; +const SYSTEM_VERIFIER_INFORMATION_LENGTH_USIZE: usize = 0x90; +const X64_SYSTEM_RANGE_START: usize = 0xffff_8000_0000_0000; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +enum SystemInformationClass { + Basic = 0, + Processor = 1, + RangeStart = 50, + Verifier = 51, + NumaProcessorMap = 55, + EmulationBasic = 62, + LogicalProcessorAndGroup = 107, + Flush = 192, + HypervisorSharedPage = 197, + FeatureConfigurationSection = 211, + ProcessorFeaturesBitMap = 250, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +enum LogicalProcessorRelationship { + ProcessorCore = 0, + NumaNode = 1, + Cache = 2, + ProcessorPackage = 3, + Group = 4, + All = 0xffff, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SystemBasicInformation { + reserved: u32, + timer_resolution: u32, + page_size: u32, + number_of_physical_pages: u32, + lowest_physical_page_number: u32, + highest_physical_page_number: u32, + allocation_granularity: u32, + _padding0: u32, + minimum_user_mode_address: usize, + maximum_user_mode_address: usize, + active_processors_affinity_mask: usize, + number_of_processors: u8, + _padding1: [u8; 7], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SystemProcessorInformation { + processor_architecture: u16, + processor_level: u16, + processor_revision: u16, + maximum_processors: u16, + processor_feature_bits: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SystemNumaInformation { + highest_node_number: u32, + reserved: u32, + active_processors_group_affinity: [GroupAffinity; NUMA_NODE_COUNT], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SystemFlushInformation { + supported_flush_methods: u32, + processor_features: u32, + reserved: [u32; 6], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SystemHypervisorSharedPageInformation { + hypervisor_shared_user_va: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SystemRangeStartInformation { + system_range_start: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SystemProcessorFeaturesBitMapInformation { + feature_bits: [u64; 2], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessorRelationship { + flags: u8, + efficiency_class: u8, + reserved: [u8; 20], + group_count: u16, + group_mask: [GroupAffinity; 1], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct NumaNodeRelationship { + node_number: u32, + reserved: [u8; 18], + group_count: u16, + group_mask: GroupAffinity, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct CacheRelationship { + level: u8, + associativity: u8, + line_size: u16, + cache_size: u32, + cache_type: u32, + reserved: [u8; 18], + group_count: u16, + group_mask: GroupAffinity, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessorGroupInfo { + maximum_processor_count: u8, + active_processor_count: u8, + reserved: [u8; 38], + active_processor_mask: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct GroupRelationship { + maximum_group_count: u16, + active_group_count: u16, + reserved: [u8; 20], + group_info: [ProcessorGroupInfo; 1], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessorRelationshipInformation { + relationship: u32, + size: u32, + processor: ProcessorRelationship, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct NumaNodeRelationshipInformation { + relationship: u32, + size: u32, + numa_node: NumaNodeRelationship, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct CacheRelationshipInformation { + relationship: u32, + size: u32, + cache: CacheRelationship, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct GroupRelationshipInformation { + relationship: u32, + size: u32, + group: GroupRelationship, +} impl Task { + pub(crate) fn sys_nt_query_system_information( + system_information_class: u32, + system_information: MutPtr, + system_information_length: u32, + return_length: Option>, + ) -> NtStatus { + let Ok(system_information_class) = + SystemInformationClass::try_from(system_information_class) + else { + litebox_util_log::debug!( + system_information_class = system_information_class; + "Unsupported NtQuerySystemInformation class" + ); + return NtStatus::INVALID_INFO_CLASS; + }; + + let status = match system_information_class { + SystemInformationClass::Basic | SystemInformationClass::EmulationBasic => { + Self::write_exact_system_information( + system_information, + system_information_length, + return_length, + &system_basic_information::(), + ) + } + SystemInformationClass::Processor => Self::write_system_information( + system_information, + system_information_length, + return_length, + &system_processor_information(), + ), + SystemInformationClass::RangeStart => Self::write_exact_system_information( + system_information, + system_information_length, + return_length, + &SystemRangeStartInformation { + system_range_start: X64_SYSTEM_RANGE_START, + }, + ), + SystemInformationClass::Verifier => Self::write_system_verifier_information( + system_information, + system_information_length, + return_length, + ), + SystemInformationClass::NumaProcessorMap => Self::write_numa_processor_map_information( + system_information, + system_information_length, + return_length, + ), + SystemInformationClass::Flush => Self::write_system_information( + system_information, + system_information_length, + return_length, + &system_flush_information(), + ), + SystemInformationClass::HypervisorSharedPage => Self::write_system_information( + system_information, + system_information_length, + return_length, + &SystemHypervisorSharedPageInformation { + hypervisor_shared_user_va: 0, + }, + ), + SystemInformationClass::ProcessorFeaturesBitMap => Self::write_system_information( + system_information, + system_information_length, + return_length, + &SystemProcessorFeaturesBitMapInformation { + feature_bits: [0; 2], + }, + ), + SystemInformationClass::LogicalProcessorAndGroup + | SystemInformationClass::FeatureConfigurationSection => NtStatus::INVALID_INFO_CLASS, + }; + + if status == NtStatus::SUCCESS { + litebox_util_log::debug!( + system_information_class:? = system_information_class, + system_information_length = system_information_length; + "Handled NtQuerySystemInformation syscall" + ); + } + + status + } + + pub(crate) fn sys_nt_query_system_information_ex( + system_information_class: u32, + input_buffer: Option>, + input_buffer_length: u32, + system_information: MutPtr, + system_information_length: u32, + return_length: Option>, + ) -> NtStatus { + if input_buffer_length < DWORD_SIZE_U32 { + return NtStatus::INVALID_PARAMETER; + } + let Some(input_buffer) = input_buffer else { + return NtStatus::INVALID_PARAMETER; + }; + + let Ok(system_information_class) = + SystemInformationClass::try_from(system_information_class) + else { + litebox_util_log::debug!( + system_information_class = system_information_class; + "Unsupported NtQuerySystemInformationEx class" + ); + return NtStatus::INVALID_INFO_CLASS; + }; + + let status = match system_information_class { + SystemInformationClass::LogicalProcessorAndGroup => { + Self::write_logical_processor_and_group_information( + input_buffer, + system_information, + system_information_length, + return_length, + ) + } + // TODO: Windows returns section handles for this class. LiteBox does not yet model those + // NT section objects, so do not publish a fabricated success payload. + SystemInformationClass::FeatureConfigurationSection => NtStatus::INVALID_INFO_CLASS, + _ => { + litebox_util_log::debug!( + system_information_class:? = system_information_class; + "Unsupported NtQuerySystemInformationEx class" + ); + NtStatus::INVALID_INFO_CLASS + } + }; + + if status == NtStatus::SUCCESS { + litebox_util_log::debug!( + system_information_class:? = system_information_class, + system_information_length = system_information_length; + "Handled NtQuerySystemInformationEx syscall" + ); + } + + status + } + + fn write_logical_processor_and_group_information( + input_buffer: ConstPtr, + system_information: MutPtr, + system_information_length: u32, + return_length: Option>, + ) -> NtStatus { + let input_buffer = ConstPtr::::from_usize(input_buffer.as_usize()); + let Some(relationship) = input_buffer.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + + let Ok(relationship) = LogicalProcessorRelationship::try_from(relationship) else { + return NtStatus::UNSUCCESSFUL; + }; + + match relationship { + LogicalProcessorRelationship::ProcessorCore => Self::write_system_information( + system_information, + system_information_length, + return_length, + &processor_relationship_information(LogicalProcessorRelationship::ProcessorCore), + ), + LogicalProcessorRelationship::NumaNode => Self::write_system_information( + system_information, + system_information_length, + return_length, + &numa_node_relationship_information(), + ), + LogicalProcessorRelationship::Cache => Self::write_system_information( + system_information, + system_information_length, + return_length, + &cache_relationship_information(), + ), + LogicalProcessorRelationship::ProcessorPackage => Self::write_system_information( + system_information, + system_information_length, + return_length, + &processor_relationship_information(LogicalProcessorRelationship::ProcessorPackage), + ), + LogicalProcessorRelationship::Group => Self::write_system_information( + system_information, + system_information_length, + return_length, + &group_relationship_information(), + ), + LogicalProcessorRelationship::All => { + let core = + processor_relationship_information(LogicalProcessorRelationship::ProcessorCore); + let numa = numa_node_relationship_information(); + let cache = cache_relationship_information(); + let package = processor_relationship_information( + LogicalProcessorRelationship::ProcessorPackage, + ); + let group = group_relationship_information(); + let records = [ + core.as_bytes(), + numa.as_bytes(), + cache.as_bytes(), + package.as_bytes(), + group.as_bytes(), + ]; + let required_len = records.iter().try_fold(0u32, |total, record| { + total.checked_add(u32::try_from(record.len()).ok()?) + }); + let Some(required_len) = required_len else { + return NtStatus::INVALID_PARAMETER; + }; + + Self::write_sized_system_information( + system_information, + system_information_length, + return_length, + required_len, + move |system_information| { + let mut offset = 0; + for record in records { + system_information + .write_slice_at_offset(offset, record) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + offset = offset.wrapping_add_unsigned(record.len()); + } + Ok(()) + }, + ) + } + } + } + + fn write_system_information( + system_information: MutPtr, + system_information_length: u32, + return_length: Option>, + information: &T, + ) -> NtStatus { + let required_len = size_of::().trunc(); + Self::write_sized_system_information( + system_information, + system_information_length, + return_length, + required_len, + |system_information| { + system_information + .write_slice_at_offset(0, information.as_bytes()) + .ok_or(NtStatus::ACCESS_VIOLATION) + }, + ) + } + + fn write_exact_system_information( + system_information: MutPtr, + system_information_length: u32, + return_length: Option>, + information: &T, + ) -> NtStatus { + let required_len = size_of::().trunc(); + if system_information_length != required_len { + return Self::write_return_length_for_short_buffer(return_length, required_len); + } + + Self::write_system_information( + system_information, + system_information_length, + return_length, + information, + ) + } + + fn write_numa_processor_map_information( + system_information: MutPtr, + system_information_length: u32, + return_length: Option>, + ) -> NtStatus { + if system_information_length < DWORD_SIZE_U32 { + return Self::write_return_length_for_short_buffer(return_length, DWORD_SIZE_U32); + } + + if system_information_length < size_of::().trunc() { + let highest_node_number = + MutPtr::::from_usize(system_information.as_usize()); + if highest_node_number.write_at_offset(0, 0).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if Self::write_return_length(return_length, DWORD_SIZE_U32).is_err() { + return NtStatus::ACCESS_VIOLATION; + } + return NtStatus::SUCCESS; + } + + Self::write_system_information( + system_information, + system_information_length, + return_length, + &system_numa_information(), + ) + } + + fn write_system_verifier_information( + system_information: MutPtr, + system_information_length: u32, + return_length: Option>, + ) -> NtStatus { + if system_information_length < SYSTEM_VERIFIER_INFORMATION_LENGTH { + return Self::write_return_length_for_short_buffer( + return_length, + SYSTEM_VERIFIER_INFORMATION_LENGTH, + ); + } + + let verifier_information = [0u8; SYSTEM_VERIFIER_INFORMATION_LENGTH_USIZE]; + if system_information + .write_slice_at_offset(0, &verifier_information) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if Self::write_return_length(return_length, 0).is_err() { + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::SUCCESS + } + + fn write_sized_system_information( + system_information: MutPtr, + system_information_length: u32, + return_length: Option>, + required_len: u32, + write_payload: impl FnOnce(MutPtr) -> Result<(), NtStatus>, + ) -> NtStatus { + if system_information_length < required_len { + return Self::write_return_length_for_short_buffer(return_length, required_len); + } + if let Err(status) = write_payload(system_information) { + return status; + } + if Self::write_return_length(return_length, required_len).is_err() { + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::SUCCESS + } + + fn write_return_length_for_short_buffer( + return_length: Option>, + required_len: u32, + ) -> NtStatus { + if Self::write_return_length(return_length, required_len).is_err() { + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::INFO_LENGTH_MISMATCH + } + + fn write_return_length( + return_length: Option>, + required_len: u32, + ) -> Result<(), NtStatus> { + if let Some(return_length) = return_length + && return_length.write_at_offset(0, required_len).is_none() + { + return Err(NtStatus::ACCESS_VIOLATION); + } + Ok(()) + } + pub(crate) fn sys_nt_query_performance_counter( &self, performance_counter: MutPtr, @@ -55,14 +623,143 @@ impl Task { } } +fn system_basic_information() -> SystemBasicInformation { + let maximum_user_mode_address = + >::TASK_ADDR_MAX.saturating_sub(1); + SystemBasicInformation { + reserved: 0, + timer_resolution: TIMER_RESOLUTION_100NS, + page_size: u32::try_from(PAGE_SIZE).expect("PAGE_SIZE fits in ULONG"), + number_of_physical_pages: DEFAULT_PHYSICAL_PAGES, + lowest_physical_page_number: 0, + highest_physical_page_number: DEFAULT_PHYSICAL_PAGES.saturating_sub(1), + allocation_granularity: ALLOCATION_GRANULARITY.trunc(), + _padding0: 0, + minimum_user_mode_address: >::TASK_ADDR_MIN, + maximum_user_mode_address, + active_processors_affinity_mask: PROCESSOR_AFFINITY_MASK, + number_of_processors: NUMBER_OF_PROCESSORS, + _padding1: [0; 7], + } +} + +fn system_processor_information() -> SystemProcessorInformation { + // TODO: x64 Windows reports AMD64 architecture with family/level 6 for modern + // x86-64 CPUs. The revision and feature bitmap are synthetic. + SystemProcessorInformation { + processor_architecture: PROCESSOR_ARCHITECTURE_AMD64, + processor_level: 6, + processor_revision: 0, + maximum_processors: u16::from(NUMBER_OF_PROCESSORS), + processor_feature_bits: 0, + } +} + +fn system_numa_information() -> SystemNumaInformation { + let mut active_processors_group_affinity = [GroupAffinity { + mask: 0, + group: 0, + reserved: [0; 3], + }; NUMA_NODE_COUNT]; + active_processors_group_affinity[0] = processor_group_affinity(); + + SystemNumaInformation { + highest_node_number: 0, + reserved: 0, + active_processors_group_affinity, + } +} + +fn system_flush_information() -> SystemFlushInformation { + SystemFlushInformation { + supported_flush_methods: SUPPORTED_FLUSH_METHODS, + processor_features: SUPPORTED_FLUSH_PROCESSOR_FEATURES, + reserved: [0; 6], + } +} + +fn processor_group_affinity() -> GroupAffinity { + GroupAffinity { + mask: PROCESSOR_AFFINITY_MASK, + group: 0, + reserved: [0; 3], + } +} + +fn processor_relationship_information( + relationship: LogicalProcessorRelationship, +) -> ProcessorRelationshipInformation { + ProcessorRelationshipInformation { + relationship: relationship as u32, + size: size_of::().trunc(), + processor: ProcessorRelationship { + flags: 0, + efficiency_class: 0, + reserved: [0; 20], + group_count: 1, + group_mask: [processor_group_affinity()], + }, + } +} + +fn numa_node_relationship_information() -> NumaNodeRelationshipInformation { + NumaNodeRelationshipInformation { + relationship: LogicalProcessorRelationship::NumaNode as u32, + size: size_of::().trunc(), + numa_node: NumaNodeRelationship { + node_number: 0, + reserved: [0; 18], + group_count: 1, + group_mask: processor_group_affinity(), + }, + } +} + +fn cache_relationship_information() -> CacheRelationshipInformation { + // Deliberate sandbox topology: one generic L1 unified cache for one synthetic processor. + // The relationship ABI follows WDK winnt.h; the field values avoid leaking host cache details. + CacheRelationshipInformation { + relationship: LogicalProcessorRelationship::Cache as u32, + size: size_of::().trunc(), + cache: CacheRelationship { + level: 1, + associativity: 0xff, + line_size: 64, + cache_size: 32 * 1024, + cache_type: CACHE_UNIFIED, + reserved: [0; 18], + group_count: 1, + group_mask: processor_group_affinity(), + }, + } +} + +fn group_relationship_information() -> GroupRelationshipInformation { + GroupRelationshipInformation { + relationship: LogicalProcessorRelationship::Group as u32, + size: size_of::().trunc(), + group: GroupRelationship { + maximum_group_count: 1, + active_group_count: 1, + reserved: [0; 20], + group_info: [ProcessorGroupInfo { + maximum_processor_count: NUMBER_OF_PROCESSORS, + active_processor_count: NUMBER_OF_PROCESSORS, + reserved: [0; 38], + active_processor_mask: PROCESSOR_AFFINITY_MASK, + }], + }, + } +} + fn duration_as_qpc_ticks(duration: core::time::Duration) -> i64 { - i64::try_from(core::cmp::min(duration.as_nanos(), i64::MAX as u128)).unwrap_or(i64::MAX) + i64::try_from(duration.as_nanos().min(i64::MAX as u128)).unwrap_or(i64::MAX) } #[cfg(test)] mod tests { use super::*; - use crate::tests::{const_ptr, mut_ptr, null_const_ptr, null_mut_ptr}; + use crate::tests::{const_ptr, mut_byte_ptr, mut_ptr, null_const_ptr, null_mut_ptr}; use core::time::Duration; use litebox::platform::ThreadProvider; @@ -72,9 +769,32 @@ mod tests { const QPC_SLEEP_TOLERANCE: Duration = Duration::from_millis(10); type TestPlatform = crate::tests::TestPlatform; + type TestTask = Task; + + const LOGICAL_PROCESSOR_ALL_INFORMATION_SIZE: usize = + size_of::() * 2 + + size_of::() + + size_of::() + + size_of::(); #[cfg(all(target_os = "windows", target_arch = "x86_64"))] unsafe extern "system" { + fn NtQuerySystemInformation( + system_information_class: u32, + system_information: *mut core::ffi::c_void, + system_information_length: u32, + return_length: *mut u32, + ) -> i32; + + fn NtQuerySystemInformationEx( + system_information_class: u32, + input_buffer: *const core::ffi::c_void, + input_buffer_length: u32, + system_information: *mut core::ffi::c_void, + system_information_length: u32, + return_length: *mut u32, + ) -> i32; + fn NtQueryPerformanceCounter(counter: *mut i64, frequency: *mut i64) -> i32; fn NtConvertBetweenAuxiliaryCounterAndPerformanceCounter( @@ -90,20 +810,6 @@ mod tests { ::run_test_thread(f) } - fn sys_nt_convert_between_auxiliary_counter_and_performance_counter( - flag: u32, - source: ConstPtr, - destination: MutPtr, - conversion_error: Option>, - ) -> NtStatus { - Task::::sys_nt_convert_between_auxiliary_counter_and_performance_counter( - flag, - source, - destination, - conversion_error, - ) - } - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] fn host_status(status: i32) -> NtStatus { NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) @@ -114,6 +820,657 @@ mod tests { u128::try_from(end - start).unwrap() } + fn empty_basic_information() -> SystemBasicInformation { + SystemBasicInformation { + reserved: u32::MAX, + timer_resolution: 0, + page_size: 0, + number_of_physical_pages: 0, + lowest_physical_page_number: 0, + highest_physical_page_number: 0, + allocation_granularity: 0, + _padding0: 0, + minimum_user_mode_address: 0, + maximum_user_mode_address: 0, + active_processors_affinity_mask: 0, + number_of_processors: 0, + _padding1: [0; 7], + } + } + + fn const_byte_ptr(value: &T) -> ConstPtr { + ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) + } + + #[test] + fn nt_query_system_information_ex_validates_query_input() { + run_with_test_platform_pointers(|| { + let relationship = LogicalProcessorRelationship::All as u32; + let mut output = [0u8; size_of::()]; + let mut return_length = 0; + + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::LogicalProcessorAndGroup as u32, + None, + DWORD_SIZE_U32, + mut_byte_ptr(&mut output), + u32::try_from(output.len()).unwrap(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(return_length, 0); + + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::LogicalProcessorAndGroup as u32, + Some(const_byte_ptr(&relationship)), + DWORD_SIZE_U32 - 1, + mut_byte_ptr(&mut output), + u32::try_from(output.len()).unwrap(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INVALID_PARAMETER + ); + + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::LogicalProcessorAndGroup as u32, + Some(null_const_ptr()), + DWORD_SIZE_U32, + mut_byte_ptr(&mut output), + u32::try_from(output.len()).unwrap(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::ACCESS_VIOLATION + ); + }); + } + + #[test] + fn nt_query_system_information_ex_rejects_unsupported_classes() { + run_with_test_platform_pointers(|| { + let query = LogicalProcessorRelationship::All as u32; + let mut output = [0u8; size_of::()]; + + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::Basic as u32, + Some(const_byte_ptr(&query)), + DWORD_SIZE_U32, + mut_byte_ptr(&mut output), + u32::try_from(output.len()).unwrap(), + None, + ), + NtStatus::INVALID_INFO_CLASS + ); + + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::FeatureConfigurationSection as u32, + Some(const_byte_ptr(&query)), + DWORD_SIZE_U32, + mut_byte_ptr(&mut output), + u32::try_from(output.len()).unwrap(), + None, + ), + NtStatus::INVALID_INFO_CLASS + ); + + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + u32::MAX, + None, + 0, + mut_byte_ptr(&mut output), + u32::try_from(output.len()).unwrap(), + None, + ), + NtStatus::INVALID_PARAMETER + ); + + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + u32::MAX, + Some(const_byte_ptr(&query)), + DWORD_SIZE_U32, + mut_byte_ptr(&mut output), + u32::try_from(output.len()).unwrap(), + None, + ), + NtStatus::INVALID_INFO_CLASS + ); + }); + } + + #[test] + fn nt_query_system_information_ex_reports_required_logical_processor_length() { + run_with_test_platform_pointers(|| { + let relationship = LogicalProcessorRelationship::All as u32; + let mut output = [0u8; 1]; + let mut return_length = 0; + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::LogicalProcessorAndGroup as u32, + Some(const_byte_ptr(&relationship)), + DWORD_SIZE_U32, + mut_byte_ptr(&mut output), + 0, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!( + return_length, + u32::try_from(LOGICAL_PROCESSOR_ALL_INFORMATION_SIZE).unwrap() + ); + }); + } + + #[test] + fn nt_query_system_information_reports_basic_information() { + run_with_test_platform_pointers(|| { + let mut info = empty_basic_information(); + let mut return_length = 0; + + assert_eq!( + TestTask::sys_nt_query_system_information( + SystemInformationClass::Basic as u32, + mut_byte_ptr(&mut info), + size_of::().trunc(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::SUCCESS + ); + + assert_eq!(return_length, size_of::().trunc()); + assert_eq!(info.page_size, u32::try_from(PAGE_SIZE).unwrap()); + assert_eq!( + info.allocation_granularity, + u32::try_from(ALLOCATION_GRANULARITY).unwrap() + ); + assert_eq!(info.number_of_processors, NUMBER_OF_PROCESSORS); + assert_eq!( + info.minimum_user_mode_address, + >::TASK_ADDR_MIN + ); + assert_eq!( + info.maximum_user_mode_address, + >::TASK_ADDR_MAX - 1 + ); + }); + } + + #[test] + fn nt_query_system_information_validates_class_and_buffer_length() { + run_with_test_platform_pointers(|| { + let mut info = [0u8; size_of::()]; + let mut return_length = 0; + let basic_len: u32 = size_of::().trunc(); + + assert_eq!( + TestTask::sys_nt_query_system_information( + SystemInformationClass::Basic as u32, + mut_byte_ptr(&mut info), + basic_len - 1, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!(return_length, basic_len); + + assert_eq!( + TestTask::sys_nt_query_system_information( + SystemInformationClass::Basic as u32, + mut_byte_ptr(&mut info), + basic_len + 1, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!(return_length, basic_len); + + assert_eq!( + TestTask::sys_nt_query_system_information( + u32::MAX, + mut_byte_ptr(&mut info), + u32::try_from(info.len()).unwrap(), + None, + ), + NtStatus::INVALID_INFO_CLASS + ); + }); + } + + #[test] + fn nt_query_system_information_reports_partial_numa_processor_map() { + run_with_test_platform_pointers(|| { + let mut highest_node_number = u32::MAX; + let mut return_length = 0; + + assert_eq!( + TestTask::sys_nt_query_system_information( + SystemInformationClass::NumaProcessorMap as u32, + mut_byte_ptr(&mut highest_node_number), + DWORD_SIZE_U32, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::SUCCESS + ); + assert_eq!(highest_node_number, 0); + assert_eq!(return_length, DWORD_SIZE_U32); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn nt_query_system_information_basic_status_matches_host_ntdll() { + run_with_test_platform_pointers(|| { + let mut host_info = [0u8; size_of::()]; + let mut host_return_length = 0; + let mut guest_info = empty_basic_information(); + let mut guest_return_length = 0; + let information_length = size_of::().trunc(); + + // SAFETY: The output buffer and return-length pointer are valid locals and ntdll does + // not retain them. + let host_basic_status = unsafe { + host_status(NtQuerySystemInformation( + SystemInformationClass::Basic as u32, + host_info.as_mut_ptr().cast(), + information_length, + &raw mut host_return_length, + )) + }; + let guest_status = TestTask::sys_nt_query_system_information( + SystemInformationClass::Basic as u32, + mut_byte_ptr(&mut guest_info), + information_length, + Some(mut_ptr(&mut guest_return_length)), + ); + assert_eq!(guest_status, host_basic_status); + assert_eq!(guest_return_length, host_return_length); + assert_eq!(guest_info.page_size, u32::try_from(PAGE_SIZE).unwrap()); + assert_eq!( + guest_info.allocation_granularity, + u32::try_from(ALLOCATION_GRANULARITY).unwrap() + ); + + let mut host_short_return_length = 0; + let mut guest_short_return_length = 0; + // SAFETY: Passing a short valid output buffer probes host ntdll's length handling; all + // pointers are valid local variables. + let host_short_status = unsafe { + host_status(NtQuerySystemInformation( + SystemInformationClass::Basic as u32, + host_info.as_mut_ptr().cast(), + information_length - 1, + &raw mut host_short_return_length, + )) + }; + let guest_short_status = TestTask::sys_nt_query_system_information( + SystemInformationClass::Basic as u32, + mut_byte_ptr(&mut guest_info), + information_length - 1, + Some(mut_ptr(&mut guest_short_return_length)), + ); + assert_eq!(guest_short_status, host_short_status); + assert_eq!(guest_short_return_length, host_short_return_length); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn nt_query_system_information_fixed_class_lengths_match_host_ntdll() { + fn query_host( + class: SystemInformationClass, + output: &mut [u8], + return_length: &mut u32, + ) -> NtStatus { + // SAFETY: The output buffer and return-length pointer are valid locals and ntdll does + // not retain them. + unsafe { + host_status(NtQuerySystemInformation( + class as u32, + output.as_mut_ptr().cast(), + u32::try_from(output.len()).unwrap(), + return_length, + )) + } + } + + run_with_test_platform_pointers(|| { + let cases = [ + ( + SystemInformationClass::Processor, + size_of::(), + ), + ( + SystemInformationClass::RangeStart, + size_of::(), + ), + ( + SystemInformationClass::Verifier, + SYSTEM_VERIFIER_INFORMATION_LENGTH_USIZE, + ), + ( + SystemInformationClass::NumaProcessorMap, + size_of::(), + ), + ( + SystemInformationClass::EmulationBasic, + size_of::(), + ), + ( + SystemInformationClass::Flush, + size_of::(), + ), + ( + SystemInformationClass::HypervisorSharedPage, + size_of::(), + ), + ( + SystemInformationClass::ProcessorFeaturesBitMap, + size_of::(), + ), + ]; + + for (class, length) in cases { + let mut host_output = std::vec![0u8; length]; + let mut guest_output = std::vec![0u8; length]; + let mut host_return_length = 0; + let mut guest_return_length = 0; + let information_length = u32::try_from(length).unwrap(); + + let host_status = query_host(class, &mut host_output, &mut host_return_length); + let guest_status = TestTask::sys_nt_query_system_information( + class as u32, + mut_byte_ptr(&mut guest_output[0]), + information_length, + Some(mut_ptr(&mut guest_return_length)), + ); + + assert_eq!(guest_status, host_status, "{class:?}"); + assert_eq!(guest_return_length, host_return_length, "{class:?}"); + } + + let exact_length_cases = [ + ( + SystemInformationClass::Basic, + size_of::(), + ), + ( + SystemInformationClass::EmulationBasic, + size_of::(), + ), + ( + SystemInformationClass::RangeStart, + size_of::(), + ), + ]; + + for (class, length) in exact_length_cases { + let mut host_output = std::vec![0u8; length + 1]; + let mut guest_output = std::vec![0u8; length + 1]; + let mut host_return_length = 0; + let mut guest_return_length = 0; + let information_length = u32::try_from(length + 1).unwrap(); + + let host_status = query_host(class, &mut host_output, &mut host_return_length); + let guest_status = TestTask::sys_nt_query_system_information( + class as u32, + mut_byte_ptr(&mut guest_output[0]), + information_length, + Some(mut_ptr(&mut guest_return_length)), + ); + + assert_eq!(guest_status, host_status, "{class:?}"); + assert_eq!(guest_return_length, host_return_length, "{class:?}"); + } + }); + } + + #[test] + fn nt_query_system_information_does_not_publish_return_length_before_output_probe() { + run_with_test_platform_pointers(|| { + let mut return_length = u32::MAX; + + assert_eq!( + TestTask::sys_nt_query_system_information( + SystemInformationClass::Basic as u32, + null_mut_ptr(), + size_of::().trunc(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::ACCESS_VIOLATION + ); + assert_eq!(return_length, u32::MAX); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn nt_query_system_information_null_output_return_length_order_matches_host_ntdll() { + run_with_test_platform_pointers(|| { + let mut host_return_length = u32::MAX; + let mut guest_return_length = u32::MAX; + let information_length = size_of::().trunc(); + + // SAFETY: This intentionally passes a null output buffer to probe host ntdll's + // NTSTATUS and return-length ordering; the return-length pointer is a valid local. + let host_status = unsafe { + host_status(NtQuerySystemInformation( + SystemInformationClass::Basic as u32, + core::ptr::null_mut(), + information_length, + &raw mut host_return_length, + )) + }; + let guest_status = TestTask::sys_nt_query_system_information( + SystemInformationClass::Basic as u32, + null_mut_ptr(), + information_length, + Some(mut_ptr(&mut guest_return_length)), + ); + + assert_eq!(guest_status, host_status); + assert_eq!(guest_return_length, host_return_length); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn nt_query_system_information_ex_input_validation_order_matches_host_ntdll() { + run_with_test_platform_pointers(|| { + let query = LogicalProcessorRelationship::All as u32; + let mut host_output = [0u8; size_of::()]; + let mut guest_output = [0u8; size_of::()]; + let mut host_return_length = u32::MAX; + let mut guest_return_length = u32::MAX; + + // SAFETY: This intentionally passes a null input buffer to probe host ntdll's + // validation order. Output and return-length pointers are valid locals. + let host_null_status = unsafe { + host_status(NtQuerySystemInformationEx( + SystemInformationClass::Basic as u32, + core::ptr::null(), + 0, + host_output.as_mut_ptr().cast(), + u32::try_from(host_output.len()).unwrap(), + &raw mut host_return_length, + )) + }; + let guest_null_status = TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::Basic as u32, + None, + 0, + mut_byte_ptr(&mut guest_output), + u32::try_from(guest_output.len()).unwrap(), + Some(mut_ptr(&mut guest_return_length)), + ); + assert_eq!(guest_null_status, host_null_status); + assert_eq!(guest_null_status, NtStatus::INVALID_PARAMETER); + assert_eq!(guest_return_length, u32::MAX); + + // SAFETY: This uses a valid input DWORD and local output buffers to confirm that + // class validation still happens after the required input-buffer check succeeds. + let host_unknown_status = unsafe { + host_status(NtQuerySystemInformationEx( + u32::MAX, + core::ptr::from_ref(&query).cast(), + DWORD_SIZE_U32, + host_output.as_mut_ptr().cast(), + u32::try_from(host_output.len()).unwrap(), + &raw mut host_return_length, + )) + }; + let guest_unknown_status = TestTask::sys_nt_query_system_information_ex( + u32::MAX, + Some(const_byte_ptr(&query)), + DWORD_SIZE_U32, + mut_byte_ptr(&mut guest_output), + u32::try_from(guest_output.len()).unwrap(), + Some(mut_ptr(&mut guest_return_length)), + ); + assert_eq!(guest_unknown_status, host_unknown_status); + assert_eq!(guest_unknown_status, NtStatus::INVALID_INFO_CLASS); + assert_eq!(guest_return_length, u32::MAX); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn nt_query_system_information_ex_logical_processor_status_matches_host_ntdll() { + fn query_host(relationship: u32, output: &mut [u8], return_length: &mut u32) -> NtStatus { + // SAFETY: This Windows-only test passes valid local input, output, and length pointers + // to ntdll and ntdll does not retain them. + unsafe { + host_status(NtQuerySystemInformationEx( + SystemInformationClass::LogicalProcessorAndGroup as u32, + core::ptr::from_ref(&relationship).cast(), + DWORD_SIZE_U32, + output.as_mut_ptr().cast(), + u32::try_from(output.len()).unwrap(), + return_length, + )) + } + } + + run_with_test_platform_pointers(|| { + let mut host_output = [0u8; 4096]; + let mut guest_output = [0u8; LOGICAL_PROCESSOR_ALL_INFORMATION_SIZE]; + let mut host_return_length = 0; + let mut guest_return_length = 0; + + let host_all_status = query_host( + LogicalProcessorRelationship::All as u32, + &mut host_output, + &mut host_return_length, + ); + let all_relationship = LogicalProcessorRelationship::All as u32; + let guest_all_status = TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::LogicalProcessorAndGroup as u32, + Some(const_byte_ptr(&all_relationship)), + DWORD_SIZE_U32, + mut_byte_ptr(&mut guest_output), + u32::try_from(guest_output.len()).unwrap(), + Some(mut_ptr(&mut guest_return_length)), + ); + assert_eq!(guest_all_status, host_all_status); + assert_eq!(guest_all_status, NtStatus::SUCCESS); + assert!(host_return_length > 0); + assert_eq!( + guest_return_length, + u32::try_from(guest_output.len()).unwrap() + ); + + host_return_length = 0; + guest_return_length = 0; + let host_cache_status = query_host( + LogicalProcessorRelationship::Cache as u32, + &mut host_output, + &mut host_return_length, + ); + let cache_relationship = LogicalProcessorRelationship::Cache as u32; + let guest_cache_status = TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::LogicalProcessorAndGroup as u32, + Some(const_byte_ptr(&cache_relationship)), + DWORD_SIZE_U32, + mut_byte_ptr(&mut guest_output), + u32::try_from(guest_output.len()).unwrap(), + Some(mut_ptr(&mut guest_return_length)), + ); + assert_eq!(guest_cache_status, host_cache_status); + assert_eq!(guest_cache_status, NtStatus::SUCCESS); + assert!(host_return_length >= size_of::().trunc()); + assert_eq!( + guest_return_length, + size_of::().trunc() + ); + + host_return_length = u32::MAX; + guest_return_length = u32::MAX; + let host_unknown_status = + query_host(u32::MAX, &mut host_output, &mut host_return_length); + let guest_unknown_status = TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::LogicalProcessorAndGroup as u32, + Some(const_byte_ptr(&u32::MAX)), + DWORD_SIZE_U32, + mut_byte_ptr(&mut guest_output), + u32::try_from(guest_output.len()).unwrap(), + Some(mut_ptr(&mut guest_return_length)), + ); + assert_eq!(guest_unknown_status, host_unknown_status); + assert_eq!(guest_return_length, u32::MAX); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn nt_query_system_information_ex_feature_configuration_section_is_not_fabricated() { + run_with_test_platform_pointers(|| { + let request = [0u64; 4]; + let mut host_output = [0u8; 0x68]; + let mut guest_output = [0u8; 0x68]; + let mut host_return_length = 0; + let mut guest_return_length = u32::MAX; + + // SAFETY: This Windows-only test passes valid local input, output, and length pointers + // to ntdll and ntdll does not retain them. + let host_status = unsafe { + host_status(NtQuerySystemInformationEx( + SystemInformationClass::FeatureConfigurationSection as u32, + core::ptr::from_ref(&request).cast(), + u32::try_from(core::mem::size_of_val(&request)).unwrap(), + host_output.as_mut_ptr().cast(), + u32::try_from(host_output.len()).unwrap(), + &raw mut host_return_length, + )) + }; + if host_status == NtStatus::SUCCESS { + assert_eq!( + host_return_length, + u32::try_from(host_output.len()).unwrap() + ); + assert!(host_output.iter().any(|byte| *byte != 0)); + } + + assert_eq!( + TestTask::sys_nt_query_system_information_ex( + SystemInformationClass::FeatureConfigurationSection as u32, + Some(const_byte_ptr(&request)), + u32::try_from(core::mem::size_of_val(&request)).unwrap(), + mut_byte_ptr(&mut guest_output), + u32::try_from(guest_output.len()).unwrap(), + Some(mut_ptr(&mut guest_return_length)), + ), + NtStatus::INVALID_INFO_CLASS + ); + assert_eq!(guest_return_length, u32::MAX); + }); + } + #[test] fn nt_query_performance_counter_writes_monotonic_counter_and_frequency() { run_with_test_platform_pointers(|| { @@ -164,7 +1521,7 @@ mod tests { let mut conversion_error = 0u64; assert_eq!( - sys_nt_convert_between_auxiliary_counter_and_performance_counter( + TestTask::sys_nt_convert_between_auxiliary_counter_and_performance_counter( 0, null_const_ptr(), mut_ptr(&mut destination), @@ -173,7 +1530,7 @@ mod tests { NtStatus::ACCESS_VIOLATION ); assert_eq!( - sys_nt_convert_between_auxiliary_counter_and_performance_counter( + TestTask::sys_nt_convert_between_auxiliary_counter_and_performance_counter( 0, const_ptr(&source), mut_ptr(&mut destination), @@ -291,7 +1648,7 @@ mod tests { )) }; let guest_null_source_status = - sys_nt_convert_between_auxiliary_counter_and_performance_counter( + TestTask::sys_nt_convert_between_auxiliary_counter_and_performance_counter( 0, null_const_ptr(), mut_ptr(&mut destination), @@ -310,7 +1667,7 @@ mod tests { )) }; let guest_valid_source_status = - sys_nt_convert_between_auxiliary_counter_and_performance_counter( + TestTask::sys_nt_convert_between_auxiliary_counter_and_performance_counter( 0, const_ptr(&source), mut_ptr(&mut destination), From 27d6279d287c414ad93515f193533b58bd79e742 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Mon, 15 Jun 2026 08:40:32 -0700 Subject: [PATCH 029/319] Cherry pick "Suppress LVBS/OP-TEE todo! macros" (#915) Co-authored-by: Sangho Lee --- litebox_common_linux/src/mm.rs | 21 +- .../src/arch/x86/mm/paging.rs | 202 ++++++++++++++---- litebox_platform_lvbs/src/lib.rs | 48 ++--- .../src/mshv/mem_integrity.rs | 33 +++ litebox_shim_optee/src/lib.rs | 5 +- litebox_shim_optee/src/msg_handler.rs | 6 + 6 files changed, 243 insertions(+), 72 deletions(-) diff --git a/litebox_common_linux/src/mm.rs b/litebox_common_linux/src/mm.rs index 75346a1721..0c66c4016d 100644 --- a/litebox_common_linux/src/mm.rs +++ b/litebox_common_linux/src/mm.rs @@ -71,7 +71,16 @@ pub fn do_mmap< ProtFlags::PROT_NONE => unsafe { pm.create_inaccessible_pages(suggested_addr, length, flags, op) }, - _ => todo!("Unsupported prot flags {:?}", prot), + _ => { + #[cfg(debug_assertions)] + todo!("Unsupported prot flags {:?}", prot); + // TODO: create inaccessible pages for now. Creating mapping + // for both executable and writable might be needed for JIT. + #[cfg(not(debug_assertions))] + unsafe { + pm.create_inaccessible_pages(suggested_addr, length, flags, op) + } + } } } @@ -134,7 +143,12 @@ pub fn sys_mprotect< ProtFlags::PROT_READ => unsafe { pm.make_pages_readable(addr, len) }, ProtFlags::PROT_NONE => unsafe { pm.make_pages_inaccessible(addr, len) }, ProtFlags::PROT_READ_WRITE_EXEC => unsafe { pm.make_pages_rwx(addr, len) }, - _ => todo!("Unsupported prot flags {:?}", prot), + _ => { + #[cfg(debug_assertions)] + todo!("Unsupported prot flags {:?}", prot); + #[cfg(not(debug_assertions))] + return Err(Errno::EINVAL); + } } .map_err(Errno::from) } @@ -185,7 +199,10 @@ pub fn sys_mremap< } if flags.intersects(MRemapFlags::MREMAP_FIXED | MRemapFlags::MREMAP_DONTUNMAP) { + #[cfg(debug_assertions)] todo!("Unsupported flags {:?}", flags); + #[cfg(not(debug_assertions))] + return Err(Errno::EINVAL); } unsafe { diff --git a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs index 4495c1e7a4..82edf68248 100644 --- a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs +++ b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs @@ -317,38 +317,123 @@ impl X64PageTable<'_, M, ALIGN> { frame: _, offset: _, flags, - } => match inner.unmap(start) { - Ok((frame, _)) => { - match unsafe { inner.map_to(new_start, frame, flags, &mut allocator) } { - Ok(_) => {} - Err(e) => match e { - MapToError::PageAlreadyMapped(_) => { - return Err(page_mgmt::RemapError::AlreadyAllocated); + } => { + // Pre-check the destination so we never destroy the old PTE for a remap + // that can't complete. `translate(new_start)` reports `Mapped` for both + // present leaves and parent huge-page coverage, ruling out both + // `MapToError::PageAlreadyMapped` and `MapToError::ParentEntryHugePage` + // from the subsequent `map_to`. + match inner.translate(new_start.start_address()) { + TranslateResult::Mapped { .. } => { + return Err(page_mgmt::RemapError::AlreadyAllocated); + } + TranslateResult::InvalidFrameAddress(pa) => { + #[cfg(debug_assertions)] + todo!("Invalid frame address at remap destination: {:#x}", pa); + #[cfg(not(debug_assertions))] + { + crate::serial_println!( + "Invalid frame address at remap destination: {:#x}", + pa + ); + return Err(page_mgmt::RemapError::Unaligned); + } + } + TranslateResult::NotMapped => {} + } + match inner.unmap(start) { + Ok((frame, _)) => { + match unsafe { inner.map_to(new_start, frame, flags, &mut allocator) } { + Ok(_) => {} + Err(MapToError::FrameAllocationFailed) => { + // Best-effort: restore the page we just unmapped before + // bailing out. Earlier iterations of the loop have already + // migrated their pages and are NOT unwound here, so the + // caller may still observe a partial move on this error. + // `unmap` leaves the parent tables for `start` in place, so + // restoring the old mapping does not require allocation. + if let Err(rollback_err) = + unsafe { inner.map_to(start, frame, flags, &mut allocator) } + { + crate::serial_println!( + "BUG: remap rollback failed: {:?}", + rollback_err + ); + } + return Err(page_mgmt::RemapError::OutOfMemory); } - MapToError::ParentEntryHugePage => { - todo!("return Err(page_mgmt::RemapError::RemapToHugePage);") + // Ruled out by the pre-check above; if the destination + // state drifts from what `translate` reported, fall back + // to a structured error rather than panicking the kernel. + Err(MapToError::PageAlreadyMapped(_)) => { + debug_assert!( + false, + "BUG: map_to reported PageAlreadyMapped after pre-check at {:#x}", + new_start.start_address() + ); + crate::serial_println!( + "BUG: map_to reported PageAlreadyMapped after pre-check at {:#x}", + new_start.start_address() + ); + return Err(page_mgmt::RemapError::AlreadyAllocated); } - MapToError::FrameAllocationFailed => { - return Err(page_mgmt::RemapError::OutOfMemory); + Err(MapToError::ParentEntryHugePage) => { + debug_assert!( + false, + "BUG: map_to reported ParentEntryHugePage after pre-check at {:#x}", + new_start.start_address() + ); + crate::serial_println!( + "BUG: map_to reported ParentEntryHugePage after pre-check at {:#x}", + new_start.start_address() + ); + return Err(page_mgmt::RemapError::AlreadyAllocated); } - }, + } + } + Err(X64UnmapError::PageNotMapped) => { + debug_assert!( + false, + "BUG: unmap reported PageNotMapped after translate said Mapped at {:#x}", + start.start_address() + ); + crate::serial_println!( + "BUG: unmap reported PageNotMapped after translate said Mapped at {:#x}", + start.start_address() + ); + return Err(page_mgmt::RemapError::Unaligned); + } + Err(X64UnmapError::ParentEntryHugePage) => { + #[cfg(debug_assertions)] + todo!("return Err(page_mgmt::RemapError::RemapToHugePage);"); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("BUG: attempt to unmap a huge page"); + return Err(page_mgmt::RemapError::Unaligned); + } + } + Err(X64UnmapError::InvalidFrameAddress(pa)) => { + // TODO: `panic!()` -> `todo!()` because user-driven interrupts or exceptions must not halt the kernel. + // We should handle this exception carefully (i.e., clean up the context and data structures belonging to an erroneous process). + #[cfg(debug_assertions)] + todo!("Invalid frame address: {:#x}", pa); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("Invalid frame address: {:#x}", pa); + return Err(page_mgmt::RemapError::Unaligned); + } } } - Err(X64UnmapError::PageNotMapped) => { - unreachable!() - } - Err(X64UnmapError::ParentEntryHugePage) => { - todo!("return Err(page_mgmt::RemapError::RemapToHugePage);") - } - Err(X64UnmapError::InvalidFrameAddress(pa)) => { - // TODO: `panic!()` -> `todo!()` because user-driven interrupts or exceptions must not halt the kernel. - // We should handle this exception carefully (i.e., clean up the context and data structures belonging to an errorneous process). - todo!("Invalid frame address: {:#x}", pa); - } - }, + } TranslateResult::NotMapped => {} TranslateResult::InvalidFrameAddress(pa) => { + #[cfg(debug_assertions)] todo!("Invalid frame address: {:#x}", pa); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("Invalid frame address: {:#x}", pa); + return Err(page_mgmt::RemapError::Unaligned); + } } } start += 1; @@ -386,14 +471,11 @@ impl X64PageTable<'_, M, ALIGN> { offset: _, flags, } => { - // If it is changed to writable, we leave it to page fault handler (COW) - let change_to_write = new_flags.contains(PageTableFlags::WRITABLE) - && !flags.contains(PageTableFlags::WRITABLE); - let new_flags = if change_to_write { - new_flags - PageTableFlags::WRITABLE - } else { - new_flags - }; + // COW lazy-enable was unimplemented, so granting WRITABLE via a later + // fault would land in the unimplemented COW path and kill the task. + // Install the writable PTE directly until COW (and shared frames) land. + // FIXME: when COW is implemented, restore the lazy-enable masking that + // was removed here so a R->RW mprotect defers WRITABLE to the fault path. if flags != new_flags { match unsafe { inner.update_flags(page, (flags & !Self::MPROTECT_PTE_MASK) | new_flags) @@ -402,7 +484,15 @@ impl X64PageTable<'_, M, ALIGN> { Err(e) => match e { FlagUpdateError::PageNotMapped => unreachable!(), FlagUpdateError::ParentEntryHugePage => { - todo!("return Err(ProtectError::ProtectHugePage);") + #[cfg(debug_assertions)] + todo!("BUG: attempt to protect a huge page"); + #[cfg(not(debug_assertions))] + { + crate::serial_println!( + "BUG: attempt to protect a huge page" + ); + return Err(page_mgmt::PermissionUpdateError::Unaligned); + } } }, } @@ -410,7 +500,13 @@ impl X64PageTable<'_, M, ALIGN> { } TranslateResult::NotMapped => {} TranslateResult::InvalidFrameAddress(pa) => { + #[cfg(debug_assertions)] todo!("Invalid frame address: {:#x}", pa); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("Invalid frame address: {:#x}", pa); + return Err(page_mgmt::PermissionUpdateError::Unaligned); + } } } } @@ -480,16 +576,25 @@ impl X64PageTable<'_, M, ALIGN> { offset: _, flags: _, } => { - assert!( - target_frame.start_address() == frame.start_address(), - "{page:?} is already mapped to {frame:?} instead of {target_frame:?}" - ); - + if target_frame.start_address() != frame.start_address() { + crate::serial_println!( + "BUG: {page:?} already mapped to {frame:?} instead of {target_frame:?}" + ); + return Err(MapToError::PageAlreadyMapped( + PhysFrame::::containing_address(frame.start_address()), + )); + } continue; } TranslateResult::NotMapped => {} TranslateResult::InvalidFrameAddress(pa) => { + #[cfg(debug_assertions)] todo!("Invalid frame address: {:#x}", pa); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("Invalid frame address: {:#x}", pa); + return Err(MapToError::FrameAllocationFailed); + } } } @@ -781,7 +886,13 @@ impl PageTableImpl for X64PageTabl return Ok(()); } else { // Copy-on-Write + #[cfg(debug_assertions)] todo!("COW"); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("BUG: Copy-on-Write not implemented"); + return Err(PageFaultError::AllocationFailed); + } } } @@ -790,7 +901,16 @@ impl PageTableImpl for X64PageTabl return Ok(()); } + #[cfg(debug_assertions)] todo!("Page fault on present page: {:#x}", page.start_address()); + #[cfg(not(debug_assertions))] + { + crate::serial_println!( + "Page fault on present page: {:#x}", + page.start_address() + ); + return Err(PageFaultError::AccessError("Page fault on present page")); + } } TranslateResult::NotMapped => { let mut allocator = PageTableAllocator::::new(); @@ -826,7 +946,13 @@ impl PageTableImpl for X64PageTabl } } TranslateResult::InvalidFrameAddress(pa) => { + #[cfg(debug_assertions)] todo!("Invalid frame address: {:#x}", pa); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("Invalid frame address: {:#x}", pa); + return Err(PageFaultError::AccessError("Invalid frame address")); + } } } Ok(()) diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index 48bc1c97ad..e41f43ea00 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -1041,38 +1041,24 @@ impl RawMutex { val: u32, timeout: Option, ) -> Result { - loop { - // No need to wait if the value already changed. - if self - .underlying_atomic() - .load(core::sync::atomic::Ordering::Relaxed) - != val - { - return Err(ImmediatelyWokenUp); - } - - let ret = Host::block_or_maybe_timeout(&self.inner, val, timeout); + // No need to wait if the value already changed. + if self + .underlying_atomic() + .load(core::sync::atomic::Ordering::Relaxed) + != val + { + return Err(ImmediatelyWokenUp); + } - match ret { - Ok(()) => { - return Ok(UnblockedOrTimedOut::Unblocked); - } - Err(Errno::EAGAIN) => { - // If the futex value does not match val, then the call fails - // immediately with the error EAGAIN. - return Err(ImmediatelyWokenUp); - } - Err(Errno::EINTR) => { - // return Err(ImmediatelyWokenUp); - todo!("EINTR"); - } - Err(Errno::ETIMEDOUT) => { - return Ok(UnblockedOrTimedOut::TimedOut); - } - Err(e) => { - panic!("Error: {e:?}"); - } - } + #[allow(clippy::match_same_arms)] + match Host::block_or_maybe_timeout(&self.inner, val, timeout) { + Ok(()) => Ok(UnblockedOrTimedOut::Unblocked), + // If the futex value does not match val, then the call fails + // immediately with the error EAGAIN. + Err(Errno::EAGAIN) => Err(ImmediatelyWokenUp), + Err(Errno::EINTR) => Ok(UnblockedOrTimedOut::Unblocked), + Err(Errno::ETIMEDOUT) => Ok(UnblockedOrTimedOut::TimedOut), + Err(e) => panic!("Error: {e:?}"), } } } diff --git a/litebox_platform_lvbs/src/mshv/mem_integrity.rs b/litebox_platform_lvbs/src/mshv/mem_integrity.rs index 57eb5c5727..f4ac3aef11 100644 --- a/litebox_platform_lvbs/src/mshv/mem_integrity.rs +++ b/litebox_platform_lvbs/src/mshv/mem_integrity.rs @@ -212,7 +212,13 @@ fn identify_direct_relocations( R_X86_64_64 => 8, R_X86_64_32 | R_X86_64_32S | R_X86_64_PLT32 | R_X86_64_PC32 => 4, _ => { + #[cfg(debug_assertions)] todo!("Unsupported relocation type {:?}", rela.r_type); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("Unsupported relocation type {:?}", rela.r_type); + return Err(KernelElfError::UnsupportedRelocation); + } } }; let start = r_offset; @@ -300,7 +306,13 @@ fn identify_indirect_relocations( R_X86_64_64 => 8, R_X86_64_32 | R_X86_64_32S | R_X86_64_PLT32 | R_X86_64_PC32 => 4, _ => { + #[cfg(debug_assertions)] todo!("Unsupported relocation type {:?}", rela.r_type); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("Unsupported relocation type {:?}", rela.r_type); + return Err(KernelElfError::UnsupportedRelocation); + } } }; @@ -386,12 +398,23 @@ pub fn verify_kernel_module_signature( let (signature, digest_alg, signature_alg) = decode_signature(signature_der)?; // We only support RSA with SHA-256 or SHA-512 for now as most Linux distributions use this combination. + #[allow(clippy::manual_assert)] if (digest_alg != ID_SHA_256 && digest_alg != ID_SHA_512) || (signature_alg != RSA_ENCRYPTION) { + #[cfg(debug_assertions)] todo!( "Unsupported digest or signature algorithm: {:?}, {:?}", digest_alg, signature_alg ); + #[cfg(not(debug_assertions))] + { + crate::serial_println!( + "Unsupported digest or signature algorithm: {:?}, {:?}", + digest_alg, + signature_alg + ); + return Err(VerificationError::Unsupported); + } } for cert in certs { let key_info = &cert.tbs_certificate.subject_public_key_info; @@ -530,8 +553,15 @@ pub fn verify_kernel_pe_signature( .to_der() .map_err(|_| VerificationError::InvalidSignature)?; let digest_algorithm_oid = authenticode_signature.signer_info().digest_alg.oid; + #[allow(clippy::manual_assert)] if digest_algorithm_oid != ID_SHA_256 && digest_algorithm_oid != ID_SHA_512 { + #[cfg(debug_assertions)] todo!("Unsupported digest algorithm: {:?}", digest_algorithm_oid); + #[cfg(not(debug_assertions))] + { + crate::serial_println!("Unsupported digest algorithm: {:?}", digest_algorithm_oid); + return Err(VerificationError::Unsupported); + } } let mut signature_verified = false; @@ -732,6 +762,9 @@ pub enum KernelElfError { ElfParseFailed, #[error("required section not found")] SectionNotFound, + #[cfg_attr(debug_assertions, allow(dead_code))] + #[error("unsupported relocation type")] + UnsupportedRelocation, } /// Errors for module signature verification. diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index 4aaecdeb70..a91fe4a6ef 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -96,7 +96,10 @@ impl litebox::shim::EnterShim for OpteeShimEntrypoints { } fn interrupt(&self, _ctx: &mut Self::ExecutionContext) -> ContinueOperation { - todo!("Handle interrupt in OP-TEE shim"); + #[cfg(debug_assertions)] + todo!("OP-TEE shim doesn't support interrupt"); + #[cfg(not(debug_assertions))] + ContinueOperation::Terminate } } diff --git a/litebox_shim_optee/src/msg_handler.rs b/litebox_shim_optee/src/msg_handler.rs index e95a458f33..b1c125101c 100644 --- a/litebox_shim_optee/src/msg_handler.rs +++ b/litebox_shim_optee/src/msg_handler.rs @@ -356,7 +356,13 @@ pub fn handle_optee_msg_args(msg_args: &OpteeMsgArgs) -> Result<(), OpteeSmcRetu | OpteeMessageCommand::InvokeCommand | OpteeMessageCommand::CloseSession => return Err(OpteeSmcReturnCode::Ok), _ => { + #[cfg(debug_assertions)] todo!("Unimplemented OpteeMessageCommand: {:?}", msg_args.cmd); + #[cfg(not(debug_assertions))] + { + litebox_util_log::debug!(cmd:? = msg_args.cmd; "Unimplemented OpteeMessageCommand"); + return Err(OpteeSmcReturnCode::EBadCmd); + } } } Ok(()) From 01f86f237a88a8d995ef182be73c0674f60f9145 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Mon, 15 Jun 2026 09:11:03 -0700 Subject: [PATCH 030/319] Cherry pick "Use checked arithmetic against TA/ldelf-controllable integers (#814)" (#916) Co-authored-by: Sangho Lee --- litebox_common_linux/src/loader.rs | 18 ++-- litebox_shim_optee/src/loader/elf.rs | 10 ++- litebox_shim_optee/src/msg_handler.rs | 11 +-- litebox_shim_optee/src/ptr.rs | 27 +++--- litebox_shim_optee/src/syscalls/ldelf.rs | 110 ++++++++++++++++++----- litebox_shim_optee/src/syscalls/mm.rs | 6 +- litebox_shim_optee/src/syscalls/tee.rs | 16 +++- 7 files changed, 142 insertions(+), 56 deletions(-) diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index ceb1f25c57..54abbee523 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -397,13 +397,21 @@ impl ElfParsedFile { } if let Some(trampoline) = &self.trampoline { min = min.min(trampoline.vaddr); - max = max.max(trampoline.vaddr + trampoline.size); + max = max.max( + trampoline + .vaddr + .checked_add(trampoline.size) + .ok_or(ElfLoadError::InvalidProgramHeader)?, + ); } let min = page_align_down(min); - let max = page_align_up(max); - mapper - .reserve(max - min, align) - .map_err(ElfLoadError::Map)? + let max = max + .checked_next_multiple_of(PAGE_SIZE) + .ok_or(ElfLoadError::InvalidProgramHeader)?; + let span = max + .checked_sub(min) + .ok_or(ElfLoadError::InvalidProgramHeader)?; + mapper.reserve(span, align).map_err(ElfLoadError::Map)? } else { // For ET_EXEC, load at the fixed addresses specified in the ELF. 0 diff --git a/litebox_shim_optee/src/loader/elf.rs b/litebox_shim_optee/src/loader/elf.rs index f5b685919c..18c490874f 100644 --- a/litebox_shim_optee/src/loader/elf.rs +++ b/litebox_shim_optee/src/loader/elf.rs @@ -45,7 +45,8 @@ fn read_at(elf: &ElfFileInMemory, offset: u64, buf: &mut [u8]) -> Result<(), Err if offset >= elf.buffer.len() { return Err(Errno::ENODATA); } - let end = core::cmp::min(offset + buf.len(), elf.buffer.len()); + let available = elf.buffer.len() - offset; + let end = offset + core::cmp::min(buf.len(), available); let len = end - offset; buf[..len].copy_from_slice(&elf.buffer[offset..end]); Ok(()) @@ -78,7 +79,9 @@ impl litebox_common_linux::loader::MapMemory for ElfFileInMemory<'_> { fn reserve(&mut self, len: usize, align: usize) -> Result { // Allocate a mapping large enough that even if it's maximally misaligned we can // still fit `len` bytes. - let mapping_len = len + (align.max(PAGE_SIZE) - PAGE_SIZE); + let mapping_len = len + .checked_add(align.max(PAGE_SIZE) - PAGE_SIZE) + .ok_or(Errno::ENOMEM)?; let mapping_ptr = self .task .sys_mmap( @@ -141,7 +144,8 @@ impl litebox_common_linux::loader::MapMemory for ElfFileInMemory<'_> { // MAP_ANONYMOUS ensures remaining bytes are zero if src is shorter than len. let offset: usize = offset.trunc(); if len > 0 && offset < self.buffer.len() { - let end = core::cmp::min(offset + len, self.buffer.len()); + let available = self.buffer.len() - offset; + let end = offset + core::cmp::min(len, available); let src = &self.buffer[offset..end]; let user_ptr = UserMutPtr::::from_usize(mapped_addr); user_ptr diff --git a/litebox_shim_optee/src/msg_handler.rs b/litebox_shim_optee/src/msg_handler.rs index b1c125101c..9187d5b7fc 100644 --- a/litebox_shim_optee/src/msg_handler.rs +++ b/litebox_shim_optee/src/msg_handler.rs @@ -388,10 +388,6 @@ pub struct TaRequestInfo { /// It copies the entire parameter data from the normal world shared memory into the secure world's /// memory to create `UteeParamOwned` structures to avoid potential data corruption during TA /// execution. -/// -/// # Panics -/// -/// Panics if any conversion from `u64` to `usize` fails. OP-TEE shim doesn't support a 32-bit environment. pub fn decode_ta_request( msg_args: &OpteeMsgArgs, ) -> Result, OpteeSmcReturnCode> { @@ -444,7 +440,12 @@ pub fn decode_ta_request( out_shm_info: [const { None }; UteeParamOwned::TEE_NUM_PARAMS], }; - let num_params = msg_args.num_params as usize; + if num_params + .checked_sub(skip) + .is_none_or(|n| n > UteeParamOwned::TEE_NUM_PARAMS) + { + return Err(OpteeSmcReturnCode::EBadCmd); + } for (i, param) in msg_args .params .iter() diff --git a/litebox_shim_optee/src/ptr.rs b/litebox_shim_optee/src/ptr.rs index 2a54bac25c..06a492506e 100644 --- a/litebox_shim_optee/src/ptr.rs +++ b/litebox_shim_optee/src/ptr.rs @@ -94,11 +94,6 @@ fn align_down(address: usize, align: usize) -> usize { address & !(align - 1) } -#[inline] -fn align_up(len: usize, align: usize) -> usize { - len.next_multiple_of(align) -} - /// Represent a physical pointer to an object with on-demand mapping. /// - `pages`: An array of page-aligned physical addresses. We expect physical addresses in this array are /// virtually contiguous. @@ -167,18 +162,23 @@ impl PhysMutPtr { )); } let start_page = align_down(pa, ALIGN); - let end_page = align_up( - pa.checked_add(bytes).ok_or(PhysPointerError::Overflow)?, - ALIGN, - ); - let mut pages = alloc::vec::Vec::with_capacity((end_page - start_page) / ALIGN); + let end_page = pa + .checked_add(bytes) + .and_then(|end| end.checked_next_multiple_of(ALIGN)) + .ok_or(PhysPointerError::Overflow)?; + let span = end_page + .checked_sub(start_page) + .ok_or(PhysPointerError::Overflow)?; + let mut pages = alloc::vec::Vec::with_capacity(span / ALIGN); let mut current_page = start_page; while current_page < end_page { pages.push( PhysPageAddr::::new(current_page) .ok_or(PhysPointerError::InvalidPhysicalAddress(current_page))?, ); - current_page += ALIGN; + current_page = current_page + .checked_add(ALIGN) + .ok_or(PhysPointerError::Overflow)?; } Self::new(&pages, pa - start_page) } @@ -377,7 +377,10 @@ impl PhysMutPtr { ) .ok_or(PhysPointerError::Overflow)?; let start = skip / ALIGN; - let end = (skip + size).div_ceil(ALIGN); + let end = skip + .checked_add(size) + .ok_or(PhysPointerError::Overflow)? + .div_ceil(ALIGN); unsafe { self.map_range(start, end, perms)?; } diff --git a/litebox_shim_optee/src/syscalls/ldelf.rs b/litebox_shim_optee/src/syscalls/ldelf.rs index 398a1464e3..88683bbba0 100644 --- a/litebox_shim_optee/src/syscalls/ldelf.rs +++ b/litebox_shim_optee/src/syscalls/ldelf.rs @@ -13,7 +13,60 @@ fn align_down(addr: usize, align: usize) -> usize { addr & !(align - 1) } +/// Calls `sys_munmap(addr, len)` when dropped, unless `disarm()` has been called first. +/// +/// Used to ensure a mapping created by `sys_mmap` is released on every error +/// path of `sys_map_zi` / `sys_map_bin`. After the syscall has fully succeeded +/// and ownership of the mapping has been transferred to the caller, call +/// `disarm()` to suppress the unmap. +#[must_use = "MmapGuard unmaps on drop unless disarm() is called; bind it"] +struct MmapGuard<'a> { + task: &'a Task, + addr: UserMutPtr, + len: usize, +} + +impl<'a> MmapGuard<'a> { + fn new(task: &'a Task, addr: UserMutPtr, len: usize) -> Self { + Self { task, addr, len } + } + + fn disarm(self) { + core::mem::forget(self); + } +} + +impl Drop for MmapGuard<'_> { + fn drop(&mut self) { + let _ = self.task.sys_munmap(self.addr, self.len); + } +} + impl Task { + #[inline] + fn checked_map_size( + num_bytes: usize, + pad_begin: usize, + pad_end: usize, + ) -> Result { + num_bytes + .checked_add(pad_begin) + .and_then(|t| t.checked_add(pad_end)) + .and_then(|t| t.checked_next_multiple_of(PAGE_SIZE)) + .ok_or(TeeResult::BadParameters) + } + + #[inline] + fn get_aligned_start_of_pad_end( + padded_start: usize, + num_bytes: usize, + ) -> Result { + padded_start + .checked_add(num_bytes) + .and_then(|end| end.checked_next_multiple_of(PAGE_SIZE)) + .ok_or(TeeResult::BadParameters) + } + /// OP-TEE's syscall to map zero-initialized memory with padding. /// This function pads `pad_begin` bytes before and `pad_end` bytes after the /// zero-initialized `num_bytes` bytes. `va` can contain a hint address which @@ -48,11 +101,7 @@ impl Task { } // TODO: Check whether flags contains `LDELF_MAP_FLAG_SHAREABLE` once we support sharing of file-based mappings. - let total_size = num_bytes - .checked_add(pad_begin) - .and_then(|t| t.checked_add(pad_end)) - .ok_or(TeeResult::BadParameters)? - .next_multiple_of(PAGE_SIZE); + let total_size = Self::checked_map_size(num_bytes, pad_begin, pad_end)?; if addr.checked_add(total_size).is_none() { return Err(TeeResult::BadParameters); } @@ -67,7 +116,12 @@ impl Task { let addr = self .sys_mmap(addr, total_size, ProtFlags::PROT_READ_WRITE, flags, -1, 0) .map_err(|_| TeeResult::OutOfMemory)?; - let padded_start = addr.as_usize() + pad_begin; + let guard = MmapGuard::new(self, addr, total_size); + + let padded_start = addr + .as_usize() + .checked_add(pad_begin) + .ok_or(TeeResult::BadParameters)?; // Unmap the padding regions to free physical memory. // Using munmap instead of mprotect(PROT_NONE) actually deallocates the frames. @@ -77,8 +131,11 @@ impl Task { let _ = self.sys_munmap(addr, pad_begin_end - addr.as_usize()); } // pad_end region: [align_up(padded_start + num_bytes, PAGE_SIZE), addr + total_size) - let pad_end_start = (padded_start + num_bytes).next_multiple_of(PAGE_SIZE); - let region_end = addr.as_usize() + total_size; + let pad_end_start = Self::get_aligned_start_of_pad_end(padded_start, num_bytes)?; + let region_end = addr + .as_usize() + .checked_add(total_size) + .ok_or(TeeResult::BadParameters)?; if pad_end_start < region_end { let _ = self.sys_munmap( UserMutPtr::from_usize(pad_end_start), @@ -87,6 +144,7 @@ impl Task { } let _ = va.write_at_offset(0, padded_start); + guard.disarm(); Ok(()) } @@ -172,11 +230,7 @@ impl Task { return Err(TeeResult::BadParameters); } - let total_size = num_bytes - .checked_add(pad_begin) - .and_then(|t| t.checked_add(pad_end)) - .ok_or(TeeResult::BadParameters)? - .next_multiple_of(PAGE_SIZE); + let total_size = Self::checked_map_size(num_bytes, pad_begin, pad_end)?; if addr.checked_add(total_size).is_none() { return Err(TeeResult::BadParameters); } @@ -225,9 +279,13 @@ impl Task { 0, ) .map_err(|_| TeeResult::OutOfMemory)?; - let padded_start = addr.as_usize() + pad_begin; + let guard = MmapGuard::new(self, addr, mmap_size); + + let padded_start = addr + .as_usize() + .checked_add(pad_begin) + .ok_or(TeeResult::BadParameters)?; if padded_start == 0 { - let _ = self.sys_munmap(addr, total_size).ok(); return Err(TeeResult::BadFormat); } @@ -250,16 +308,16 @@ impl Task { } else if flags.contains(LdelfMapFlags::LDELF_MAP_FLAG_EXECUTABLE) { prot |= ProtFlags::PROT_EXEC; } + let prot_start = align_down(padded_start, PAGE_SIZE); + let prot_len = padded_start + .checked_sub(prot_start) + .and_then(|offset| offset.checked_add(num_bytes)) + .and_then(|len| len.checked_next_multiple_of(PAGE_SIZE)) + .ok_or(TeeResult::BadParameters)?; if self - .sys_mprotect( - UserMutPtr::from_usize(align_down(padded_start, PAGE_SIZE)), - (num_bytes + padded_start - align_down(padded_start, PAGE_SIZE)) - .next_multiple_of(PAGE_SIZE), - prot, - ) + .sys_mprotect(UserMutPtr::from_usize(prot_start), prot_len, prot) .is_err() { - let _ = self.sys_munmap(addr, total_size).ok(); return Err(TeeResult::AccessDenied); } @@ -271,8 +329,11 @@ impl Task { let _ = self.sys_munmap(addr, pad_begin_end - addr.as_usize()); } // pad_end region: [align_up(padded_start + num_bytes, PAGE_SIZE), addr + total_size) - let pad_end_start = (padded_start + num_bytes).next_multiple_of(PAGE_SIZE); - let region_end = addr.as_usize() + total_size; + let pad_end_start = Self::get_aligned_start_of_pad_end(padded_start, num_bytes)?; + let region_end = addr + .as_usize() + .checked_add(total_size) + .ok_or(TeeResult::BadParameters)?; if pad_end_start < region_end { let _ = self.sys_munmap( UserMutPtr::from_usize(pad_end_start), @@ -281,6 +342,7 @@ impl Task { } let _ = va.write_at_offset(0, padded_start); + guard.disarm(); Ok(()) } diff --git a/litebox_shim_optee/src/syscalls/mm.rs b/litebox_shim_optee/src/syscalls/mm.rs index 5bd3fa90a3..116421b8f2 100644 --- a/litebox_shim_optee/src/syscalls/mm.rs +++ b/litebox_shim_optee/src/syscalls/mm.rs @@ -9,9 +9,9 @@ use litebox_common_linux::{MapFlags, ProtFlags, errno::Errno}; use crate::{Task, UserMutPtr}; #[inline] -fn align_up(addr: usize, align: usize) -> usize { +fn align_up(addr: usize, align: usize) -> Option { debug_assert!(align.is_power_of_two()); - (addr + align - 1) & !(align - 1) + addr.checked_next_multiple_of(align) } impl Task { @@ -66,7 +66,7 @@ impl Task { return Err(Errno::EINVAL); } - let aligned_len = align_up(len, PAGE_SIZE); + let aligned_len = align_up(len, PAGE_SIZE).ok_or(Errno::ENOMEM)?; if aligned_len == 0 { return Err(Errno::ENOMEM); } diff --git a/litebox_shim_optee/src/syscalls/tee.rs b/litebox_shim_optee/src/syscalls/tee.rs index 4f605b50ce..6ee19b6f76 100644 --- a/litebox_shim_optee/src/syscalls/tee.rs +++ b/litebox_shim_optee/src/syscalls/tee.rs @@ -21,9 +21,9 @@ use crate::{ }; #[inline] -fn align_up(addr: usize, align: usize) -> usize { +fn align_up(addr: usize, align: usize) -> Option { debug_assert!(align.is_power_of_two()); - (addr + align - 1) & !(align - 1) + addr.checked_next_multiple_of(align) } #[inline] @@ -255,9 +255,17 @@ impl Task { let len = len .checked_add(buf.as_usize() - align_down(buf.as_usize(), PAGE_SIZE)) .ok_or(TeeResult::AccessConflict)?; - NonZeroPageSize::::new(align_up(len, PAGE_SIZE)) - .ok_or(TeeResult::AccessConflict)? + NonZeroPageSize::::new( + align_up(len, PAGE_SIZE).ok_or(TeeResult::AccessConflict)?, + ) + .ok_or(TeeResult::AccessConflict)? }; + // Reject ranges where `start + aligned_len` would wrap, so downstream + // permission lookups don't operate on a truncated address range. + let _ = start + .as_usize() + .checked_add(aligned_len.as_usize()) + .ok_or(TeeResult::AccessConflict)?; if let Some(perms) = self.global.pm.get_memory_permissions(start, aligned_len) { if (flags.contains(TeeMemoryAccessRights::TEE_MEMORY_ACCESS_READ) && !perms.contains(MemoryRegionPermissions::READ)) From dc02b5e95dee4cf4587cb7615ace5847b28a8fd9 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Mon, 15 Jun 2026 09:52:16 -0700 Subject: [PATCH 031/319] Cherry pick "Add secure random number generator to LVBS platform" (#917) Co-authored-by: Sangho Lee --- Cargo.lock | 2 + litebox_platform_lvbs/Cargo.toml | 2 + litebox_platform_lvbs/src/host/lvbs_impl.rs | 146 +++++++++++++++++++- litebox_shim_optee/src/lib.rs | 2 +- litebox_shim_optee/src/loader/ta_stack.rs | 13 +- litebox_shim_optee/src/syscalls/cryp.rs | 12 +- 6 files changed, 156 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccb9a483db..286b3b24ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1585,6 +1585,8 @@ dependencies = [ "num_enum", "object", "once_cell", + "rand_chacha", + "rand_core", "rangemap", "raw-cpuid", "rsa", diff --git a/litebox_platform_lvbs/Cargo.toml b/litebox_platform_lvbs/Cargo.toml index 0c628a3460..ee3a988a1f 100644 --- a/litebox_platform_lvbs/Cargo.toml +++ b/litebox_platform_lvbs/Cargo.toml @@ -31,6 +31,8 @@ object = { version = "0.36.7", default-features = false, features = ["pe"] } digest = { version = "0.10.7", default-features = false } aligned-vec = { version = "0.6.4", default-features = false } raw-cpuid = "11.6.0" +rand_chacha = { version = "0.3.1", default-features = false } +rand_core = { version = "0.6.4", default-features = false } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } zeroize = { version = "1.8", default-features = false } diff --git a/litebox_platform_lvbs/src/host/lvbs_impl.rs b/litebox_platform_lvbs/src/host/lvbs_impl.rs index a58ec66dd4..2e0c79fc7b 100644 --- a/litebox_platform_lvbs/src/host/lvbs_impl.rs +++ b/litebox_platform_lvbs/src/host/lvbs_impl.rs @@ -7,6 +7,8 @@ use crate::{ Errno, HostInterface, arch::ioport::serial_print_string, host::per_cpu_variables::with_per_cpu_variables, }; +use digest::Digest; +use rand_core::{RngCore, SeedableRng}; use zeroize::Zeroizing; pub type LvbsLinuxKernel = crate::LinuxKernel; @@ -103,16 +105,73 @@ unsafe impl litebox::platform::ThreadLocalStorageProvider for LvbsLinuxKernel { impl litebox::platform::CrngProvider for LvbsLinuxKernel { fn fill_bytes_crng(&self, buf: &mut [u8]) { - // FIXME: generate real random data. - static RANDOM: spin::mutex::SpinMutex = - spin::mutex::SpinMutex::new(litebox::utils::rng::FastRng::new_from_seed( - core::num::NonZeroU64::new(0x4d595df4d0f33173).unwrap(), - )); + static RANDOM: spin::mutex::SpinMutex> = spin::mutex::SpinMutex::new(None); + let mut random = RANDOM.lock(); - for b in buf.chunks_mut(8) { - b.copy_from_slice(&random.next_u64().to_ne_bytes()[..b.len()]); + random + .get_or_insert_with(|| { + LvbsCrng::new( + PRK_ONCE.get().expect("Platform root key not initialized"), + rdrand_seed().expect("RDRAND unavailable during CRNG initialization"), + ) + }) + .fill_bytes(buf, rdrand_seed); + } +} + +type CrngSeed = ::Seed; + +const CRNG_RESEED_INTERVAL_BYTES: usize = 1024 * 1024; +const CRNG_RESEED_BACKOFF_BYTES: usize = 64 * 1024; +const CRNG_RESEED_STATE_BYTES: usize = 32; +const RDRAND_RETRY_ATTEMPTS: u32 = 10; + +struct LvbsCrng { + random: rand_chacha::ChaCha20Rng, + bytes_until_reseed: usize, + reseed_counter: usize, +} + +impl LvbsCrng { + fn new(prk: &[u8; PRK_LEN], rdrand_seed: CrngSeed) -> Self { + Self { + random: rand_chacha::ChaCha20Rng::from_seed(crng_seed_from_prk_and_rdrand( + prk, + rdrand_seed, + )), + bytes_until_reseed: CRNG_RESEED_INTERVAL_BYTES, + reseed_counter: 0, } } + + fn fill_bytes(&mut self, mut buf: &mut [u8], rdrand_seed: impl Fn() -> Option) { + while !buf.is_empty() { + let len = buf.len().min(self.bytes_until_reseed); + let (chunk, rest) = buf.split_at_mut(len); + self.random.fill_bytes(chunk); + buf = rest; + self.bytes_until_reseed -= len; + + if self.bytes_until_reseed == 0 { + match rdrand_seed() { + Some(seed) => self.reseed(seed), + None => self.bytes_until_reseed = CRNG_RESEED_BACKOFF_BYTES, + } + } + } + } + + fn reseed(&mut self, rdrand_seed: CrngSeed) { + self.reseed_counter += 1; + let mut current_state = Zeroizing::new([0u8; CRNG_RESEED_STATE_BYTES]); + self.random.fill_bytes(&mut *current_state); + self.random = rand_chacha::ChaCha20Rng::from_seed(crng_reseed_from_rdrand_and_state( + rdrand_seed, + self.reseed_counter, + ¤t_state, + )); + self.bytes_until_reseed = CRNG_RESEED_INTERVAL_BYTES; + } } /// Length of the Platform Root Key in bytes. @@ -156,6 +215,51 @@ impl litebox::platform::DerivedKeyProvider for LvbsLinuxKernel { } } +fn rdrand_seed() -> Option { + let mut seed = CrngSeed::default(); + for chunk in seed.chunks_mut(8) { + let mut word = 0; + let mut ok = false; + for _ in 0..RDRAND_RETRY_ATTEMPTS { + // Safety: `RDRAND` is available on the LVBS target CPUs. A false + // carry flag means random data is temporarily unavailable. + if unsafe { core::arch::x86_64::_rdrand64_step(&mut word) } == 1 { + ok = true; + break; + } + core::hint::spin_loop(); + } + if !ok { + return None; + } + chunk.copy_from_slice(&word.to_le_bytes()[..chunk.len()]); + } + Some(seed) +} + +fn crng_seed_from_prk_and_rdrand(prk: &[u8; PRK_LEN], rdrand_seed: CrngSeed) -> CrngSeed { + sha2::Sha256::new() + .chain_update(b"litebox-lvbs-crng-seed-v1") + .chain_update(prk) + .chain_update(rdrand_seed) + .finalize() + .into() +} + +fn crng_reseed_from_rdrand_and_state( + rdrand_seed: CrngSeed, + reseed_counter: usize, + current_state: &[u8; CRNG_RESEED_STATE_BYTES], +) -> CrngSeed { + sha2::Sha256::new() + .chain_update(b"litebox-lvbs-crng-reseed-v1") + .chain_update(rdrand_seed) + .chain_update(reseed_counter.to_le_bytes()) + .chain_update(current_state) + .finalize() + .into() +} + pub struct HostLvbsInterface; impl HostLvbsInterface {} @@ -205,3 +309,31 @@ impl HostInterface for HostLvbsInterface { unimplemented!() } } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + const TEST_PRK: [u8; PRK_LEN] = [0x42; PRK_LEN]; + const INIT_SEED: CrngSeed = [0xA5; 32]; + const RESEED_SEED: CrngSeed = [0x5A; 32]; + + #[test] + fn crosses_reseed_boundary_twice_with_accurate_budget() { + let mut crng = LvbsCrng::new(&TEST_PRK, INIT_SEED); + let mut buf = vec![0u8; CRNG_RESEED_INTERVAL_BYTES * 2 + 7]; + crng.fill_bytes(&mut buf, || Some(RESEED_SEED)); + assert_eq!(crng.reseed_counter, 2); + assert_eq!(crng.bytes_until_reseed, CRNG_RESEED_INTERVAL_BYTES - 7); + } + + #[test] + fn rdrand_failure_engages_backoff_without_reseed() { + let mut crng = LvbsCrng::new(&TEST_PRK, INIT_SEED); + let mut buf = vec![0u8; CRNG_RESEED_INTERVAL_BYTES]; + crng.fill_bytes(&mut buf, || None); + assert_eq!(crng.reseed_counter, 0); + assert_eq!(crng.bytes_until_reseed, CRNG_RESEED_BACKOFF_BYTES); + } +} diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index a91fe4a6ef..dc75d1e89f 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -750,7 +750,7 @@ impl Task { ElfLoaderError::MappingError(litebox::mm::linux::MappingError::OutOfMemory), )?; ta_stack - .init(params) + .init(self.global.platform, params) .ok_or(ElfLoaderError::InvalidStackAddr)?; Ok(ThreadInitState::Ta { diff --git a/litebox_shim_optee/src/loader/ta_stack.rs b/litebox_shim_optee/src/loader/ta_stack.rs index e71a9877a7..272525dc0a 100644 --- a/litebox_shim_optee/src/loader/ta_stack.rs +++ b/litebox_shim_optee/src/loader/ta_stack.rs @@ -10,7 +10,7 @@ use litebox::{ use litebox_common_optee::{LdelfArg, TeeParamType, UteeParamOwned, UteeParams}; use zerocopy::IntoBytes; -use crate::UserMutPtr; +use crate::{Platform, UserMutPtr}; #[inline] fn align_down(addr: usize, align: usize) -> usize { @@ -226,7 +226,7 @@ impl TaStack { Some(()) } - pub(crate) fn init(&mut self, params: &[UteeParamOwned]) -> Option<()> { + pub(crate) fn init(&mut self, platform: &Platform, params: &[UteeParamOwned]) -> Option<()> { if params.len() > UteeParams::TEE_NUM_PARAMS { return None; } @@ -259,11 +259,10 @@ impl TaStack { self.set_utee_params()?; - // TODO: generate a random value - self.push_bytes(&[ - 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, - 0xBE, 0xEF, - ])?; + // Random 16-byte stack canary + let mut canary = [0u8; 16]; + ::fill_bytes_crng(platform, &mut canary); + self.push_bytes(&canary)?; // ensure stack is aligned self.pos = align_down(self.pos, Self::STACK_ALIGNMENT); diff --git a/litebox_shim_optee/src/syscalls/cryp.rs b/litebox_shim_optee/src/syscalls/cryp.rs index 3ddf0c4b22..8845ee4829 100644 --- a/litebox_shim_optee/src/syscalls/cryp.rs +++ b/litebox_shim_optee/src/syscalls/cryp.rs @@ -304,14 +304,14 @@ impl Task { Ok(()) } + #[allow(clippy::unnecessary_wraps)] pub(crate) fn sys_cryp_random_number_generate(&self, buf: &mut [u8]) -> Result<(), TeeResult> { - if buf.is_empty() { - return Err(TeeResult::BadParameters); + if !buf.is_empty() { + ::fill_bytes_crng( + self.global.platform, + buf, + ); } - ::fill_bytes_crng( - self.global.platform, - buf, - ); Ok(()) } } From 7ea5b6e75f4c6721cb0a41422e3ae6e1ab990e4e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 16 Jun 2026 10:36:25 -0700 Subject: [PATCH 032/319] Initialize more Windows PEB fields (#918) This PR fills in more of the synthetic Windows process startup state. - Build and install a synthetic API-set namespace in `PEB.ApiSetMap`. **Note** that it is synthetic (i.e., a subset of what my host machine has). - Pass runner `argv`/`envp` into the Windows PE loader and populate them to guest. - Initialize additional PEB startup fields including process parameters, heap metadata, `FastPebLock`, loader lock, OS/subsystem metadata, `ReadOnlySharedMemoryBase`, and `ReadOnlyStaticServerData`. - Model the read-only shared server-data region enough to provide base server static data, Windows/System32 directory strings, and the rebase anchor expected by startup code. **Note** that they are modeled from a real Windows CDB probe and the actual struct layout is still unknown. --- litebox_common_windows/src/loader.rs | 277 ++++- litebox_common_windows/src/nt_status.rs | 4 + litebox_shim_windows/src/lib.rs | 25 +- litebox_shim_windows/src/loader/pe.rs | 1403 ++++++++++++++++++++--- 4 files changed, 1531 insertions(+), 178 deletions(-) diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs index 73aec1eaf0..486b7ae735 100644 --- a/litebox_common_windows/src/loader.rs +++ b/litebox_common_windows/src/loader.rs @@ -54,6 +54,8 @@ pub struct PeImageInfo { pub struct MappingInfo { pub base_addr: usize, pub image_size: usize, + /// image_size + trampoline size + pub mapping_size: usize, pub entry_point: usize, } @@ -114,6 +116,262 @@ pub struct KiUserInvertedFunctionTableEntry { pub size_of_table: u32, } +pub const API_SET_NAMESPACE_VERSION: u32 = 6; +pub const API_SET_NAMESPACE_HASH_FACTOR: u32 = 31; +pub const MAX_API_SET_NAMESPACE_SIZE: usize = 16 * 1024 * 1024; +const API_SET_NAMESPACE_ENTRY_FLAGS: u32 = 1; + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, FromBytes, Immutable, IntoBytes)] +pub struct ApiSetNamespace { + pub version: u32, + pub size: u32, + pub flags: u32, + pub count: u32, + pub entry_offset: u32, + pub hash_offset: u32, + pub hash_factor: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, FromBytes, Immutable, IntoBytes)] +pub struct ApiSetNamespaceEntry { + pub flags: u32, + pub name_offset: u32, + pub name_length: u32, + pub hashed_length: u32, + pub value_offset: u32, + pub value_count: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, FromBytes, Immutable, IntoBytes)] +pub struct ApiSetValueEntry { + pub flags: u32, + pub name_offset: u32, + pub name_length: u32, + pub value_offset: u32, + pub value_length: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, FromBytes, Immutable, IntoBytes)] +pub struct ApiSetHashEntry { + pub hash: u32, + pub index: u32, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ApiSetMapping<'a> { + /// API-set contract name, such as `api-ms-win-core-file-l1-2-3`. + pub contract: &'a str, + /// Host DLL name used as the default namespace value for the contract. + pub host_dll: &'a str, + /// Optional exact contract prefix used when computing the namespace hash. + pub hashed_prefix: Option<&'a str>, +} + +impl<'a> ApiSetMapping<'a> { + /// Creates a mapping whose hash prefix is derived by trimming the last dash suffix. + #[must_use] + pub const fn new(contract: &'a str, host_dll: &'a str) -> Self { + Self { + contract, + host_dll, + hashed_prefix: None, + } + } + + /// Creates a mapping with an explicit hash prefix for contracts that do not follow the + /// usual version-suffix naming pattern. + #[must_use] + pub const fn with_hashed_prefix( + contract: &'a str, + host_dll: &'a str, + hashed_prefix: &'a str, + ) -> Self { + Self { + contract, + host_dll, + hashed_prefix: Some(hashed_prefix), + } + } +} + +#[derive(Debug, Error)] +pub enum ApiSetNamespaceBuildError { + #[error("API-set namespace field overflow")] + Overflow, + #[error("API-set namespace is too large")] + TooLarge, + #[error("API-set namespace mapping has an invalid hashed prefix")] + InvalidHashedPrefix, +} + +pub fn build_api_set_namespace( + mappings: &[(&str, &str)], +) -> Result, ApiSetNamespaceBuildError> { + let mappings = mappings + .iter() + .map(|&(contract, host_dll)| ApiSetMapping::new(contract, host_dll)) + .collect::>(); + build_api_set_namespace_from_mappings(&mappings) +} + +/// Builds an API-set namespace from mappings that can carry explicit hash prefixes. +pub fn build_api_set_namespace_from_mappings( + mappings: &[ApiSetMapping<'_>], +) -> Result, ApiSetNamespaceBuildError> { + let mut mappings = mappings.to_vec(); + mappings.sort_by(|left, right| left.contract.cmp(right.contract)); + + let count = mappings.len(); + let entry_offset = size_of::(); + let value_offset = api_set_checked_add( + entry_offset, + api_set_checked_mul(count, size_of::())?, + )?; + let strings_offset = api_set_checked_add( + value_offset, + api_set_checked_mul(count, size_of::())?, + )?; + let mut string_data = Vec::new(); + let mut entries = Vec::with_capacity(count); + let mut values = Vec::with_capacity(count); + let mut hashes = Vec::with_capacity(count); + + for (index, mapping) in mappings.iter().enumerate() { + let contract = mapping.contract; + let host_dll = mapping.host_dll; + let hashed_prefix = mapping + .hashed_prefix + .unwrap_or_else(|| &contract[..api_set_hashed_name_len(contract)]); + if !hashed_prefix.is_ascii() || !contract.starts_with(hashed_prefix) { + return Err(ApiSetNamespaceBuildError::InvalidHashedPrefix); + } + + let name = utf16_bytes(contract)?; + let host = utf16_bytes(host_dll)?; + let name_offset = api_set_checked_add(strings_offset, string_data.len())?; + string_data.extend_from_slice(&name); + let host_offset = api_set_checked_add(strings_offset, string_data.len())?; + string_data.extend_from_slice(&host); + let value_entry_offset = api_set_checked_add( + value_offset, + api_set_checked_mul(index, size_of::())?, + )?; + entries.push(ApiSetNamespaceEntry { + flags: API_SET_NAMESPACE_ENTRY_FLAGS, + name_offset: api_set_to_u32(name_offset)?, + name_length: api_set_to_u32(name.len())?, + hashed_length: api_set_to_u32(utf16_byte_len(hashed_prefix)?)?, + value_offset: api_set_to_u32(value_entry_offset)?, + value_count: 1, + }); + values.push(ApiSetValueEntry { + flags: 0, + name_offset: 0, + name_length: 0, + value_offset: api_set_to_u32(host_offset)?, + value_length: api_set_to_u32(host.len())?, + }); + hashes.push(ApiSetHashEntry { + hash: api_set_hash_prefix(hashed_prefix), + index: api_set_to_u32(index)?, + }); + } + + let hash_offset = + api_set_checked_add(strings_offset, string_data.len())?.next_multiple_of(size_of::()); + let size = api_set_checked_add( + hash_offset, + api_set_checked_mul(count, size_of::())?, + )?; + if size > MAX_API_SET_NAMESPACE_SIZE { + return Err(ApiSetNamespaceBuildError::TooLarge); + } + hashes.sort_by_key(|entry| (entry.hash, entry.index)); + + let namespace = ApiSetNamespace { + version: API_SET_NAMESPACE_VERSION, + size: api_set_to_u32(size)?, + flags: 0, + count: api_set_to_u32(count)?, + entry_offset: api_set_to_u32(entry_offset)?, + hash_offset: api_set_to_u32(hash_offset)?, + hash_factor: API_SET_NAMESPACE_HASH_FACTOR, + }; + + let mut bytes = Vec::with_capacity(size); + bytes.extend_from_slice(namespace.as_bytes()); + for entry in &entries { + bytes.extend_from_slice(entry.as_bytes()); + } + for value in &values { + bytes.extend_from_slice(value.as_bytes()); + } + bytes.extend_from_slice(&string_data); + bytes.resize(hash_offset, 0); + for hash in &hashes { + bytes.extend_from_slice(hash.as_bytes()); + } + debug_assert_eq!(bytes.len(), size); + Ok(bytes) +} + +#[must_use] +pub fn api_set_hash(name: &str) -> u32 { + api_set_hash_prefix(&name[..api_set_hashed_name_len(name)]) +} + +/// Computes the API-set hash for an already selected contract prefix. +#[must_use] +pub fn api_set_hash_prefix(prefix: &str) -> u32 { + prefix.bytes().fold(0, |hash, byte| { + hash.wrapping_mul(API_SET_NAMESPACE_HASH_FACTOR) + .wrapping_add(u32::from(byte.to_ascii_lowercase())) + }) +} + +fn api_set_hashed_name_len(name: &str) -> usize { + name.rfind('-').unwrap_or(name.len()) +} + +fn utf16_bytes(value: &str) -> Result, ApiSetNamespaceBuildError> { + let mut bytes = Vec::with_capacity( + value + .len() + .checked_mul(size_of::()) + .ok_or(ApiSetNamespaceBuildError::Overflow)?, + ); + for unit in value.encode_utf16() { + bytes.extend_from_slice(&unit.to_le_bytes()); + } + Ok(bytes) +} + +fn utf16_byte_len(value: &str) -> Result { + value + .encode_utf16() + .count() + .checked_mul(size_of::()) + .ok_or(ApiSetNamespaceBuildError::Overflow) +} + +fn api_set_checked_add(left: usize, right: usize) -> Result { + left.checked_add(right) + .ok_or(ApiSetNamespaceBuildError::Overflow) +} + +fn api_set_checked_mul(left: usize, right: usize) -> Result { + left.checked_mul(right) + .ok_or(ApiSetNamespaceBuildError::Overflow) +} + +fn api_set_to_u32(value: usize) -> Result { + u32::try_from(value).map_err(|_| ApiSetNamespaceBuildError::Overflow) +} + /// Errors that can occur when parsing a PE file. #[derive(Debug, Error)] pub enum PeParseError { @@ -219,6 +477,12 @@ impl PeParsedFile { self.image.size_of_image } + /// Returns the PE entry-point RVA from the optional header. + #[must_use] + pub fn entry_point_rva(&self) -> usize { + self.image.entry_point_rva + } + /// Returns the preferred image base from the optional header. #[must_use] pub fn image_base(&self) -> usize { @@ -375,8 +639,8 @@ impl PeParsedFile { .map_err(PeLoadError::Map)?; } - if self.trampoline.is_some() { - self.load_trampoline(mapper, mem, base_addr)?; + if let Some(trampoline) = &self.trampoline { + Self::load_trampoline(mapper, mem, base_addr, trampoline)?; } let entry_point = checked_add!( @@ -387,7 +651,8 @@ impl PeParsedFile { Ok(MappingInfo { base_addr, - image_size: mapping_size, + image_size, + mapping_size, entry_point, }) } @@ -589,12 +854,14 @@ impl PeParsedFile { } fn load_trampoline( - &self, mapper: &mut M, mem: &mut impl AccessMemory, base_addr: usize, + trampoline: &PeTrampolineInfo, ) -> Result<(), PeLoadError> { - let trampoline = self.trampoline.as_ref().unwrap(); + if trampoline.size == 0 { + return Ok(()); + } let trampoline_start = base_addr .checked_add(trampoline.rva) .ok_or(PeLoadError::InvalidImage)?; diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index 66fd022a52..de40ada1c6 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -87,6 +87,7 @@ impl NtStatus { "STATUS_WAIT_3: Caller specified WaitAny and one of the dispatcher objects was set" } 0x00000102 => "STATUS_TIMEOUT: The given timeout interval expired", + 0x00000103 => "STATUS_PENDING: The operation that was requested is pending completion", 0x00010001 => "DBG_EXCEPTION_HANDLED: Exception handled by debugger", 0x00010002 => "DBG_CONTINUE: Continue from exception", 0x40000000 => "STATUS_OBJECT_NAME_EXISTS: The object name already exists", @@ -210,6 +211,9 @@ impl NtStatus { /// STATUS_SUCCESS pub const SUCCESS: Self = Self::from_raw(0x00000000); + /// STATUS_PENDING + pub const PENDING: Self = Self::from_raw(0x00000103); + /// STATUS_WAIT_1 pub const WAIT_1: Self = Self::from_raw(0x00000001); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 83f3b01143..9d2c95db19 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -116,6 +116,21 @@ where ptr.write_at_offset(0, value) } +fn write_field_at_offset( + base: MutPtr, + field_offset: usize, + value: Field, +) -> Option<()> +where + Platform: RawPointerProvider, + Struct: zerocopy::FromBytes + zerocopy::IntoBytes, + Field: zerocopy::FromBytes + zerocopy::IntoBytes, +{ + let address = base.as_usize().checked_add(field_offset)?; + let ptr = MutPtr::::from_usize(address); + ptr.write_at_offset(0, value) +} + fn write_slice(address: usize, values: &[T]) -> Option<()> where Platform: RawPointerProvider, @@ -298,17 +313,15 @@ pub struct WindowsShim(Arc WindowsShim { /// Loads the program at `path` as the shim's initial task. - /// - /// TODO: PEB/TEB setup and initial handle table state are not yet implemented. pub fn load_program( &self, fs: Arc, path: &str, - _argv: Vec, - _envp: Vec, + argv: Vec, + envp: Vec, ) -> Result, loader::WindowsLoadError> { - let load_info = - loader::PeLoader::new(self.0.platform, fs.clone(), &self.0.page_manager).load(path)?; + let load_info = loader::PeLoader::new(self.0.platform, fs.clone(), &self.0.page_manager) + .load(path, &argv, &envp)?; let process = Arc::new(Process { ntdll_mapping: load_info.ntdll_mapping, peb_address: load_info.environment.peb, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index daef241b1d..7e68cf778d 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -2,8 +2,11 @@ // Licensed under the MIT license. use alloc::collections::btree_map::BTreeMap; -use alloc::{string::String, sync::Arc, vec::Vec}; -use core::marker::PhantomData; +use alloc::{ffi::CString, string::String, sync::Arc, vec::Vec}; +use core::{ + marker::PhantomData, + mem::{align_of, size_of}, +}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt as _; use litebox::{ @@ -16,18 +19,19 @@ use litebox::{ use litebox_common_windows::loader::{ AccessMemory, Fault, KiUserInvertedFunctionTableEntry, KiUserInvertedFunctionTableHeader, MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, MapMemory, MappingInfo, PAGE_SIZE, PeExportError, - PeLoadError, PeParseError, PeParsedFile, Protection, ReadAt, page_align_down, + PeLoadError, PeParseError, PeParsedFile, Protection, ReadAt, build_api_set_namespace, + page_align_down, }; use rangemap::RangeMap; use thiserror::Error; -use zerocopy::{FromZeros, IntoBytes}; +use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout}; -use crate::ShimFS; use crate::nt_types::{ ClientId, PebBitField, ProcessEnvironmentBlock, RtlUserProcFlags, RtlUserProcessParameters, ThreadEnvironmentBlock, UnicodeString, X64Context, }; use crate::syscalls::mm::{MemoryType, PageProtection}; +use crate::{MutPtr, ShimFS}; const NTDLL_WRITABLE_SECTIONS: &[&[u8]] = &[b".mrdata"]; const NTDLL_PATHS: &[&str] = &["/Windows/System32/ntdll.dll", "/windows/system32/ntdll.dll"]; @@ -38,14 +42,14 @@ const INITIAL_STACK_SIZE: usize = 1024 * 1024; const WINDOWS_SHARED_SECTION_SIZE: usize = 0x1_0000; const CSR_SERVER_DLL_MAX: usize = 4; const BASESRV_SERVERDLL_INDEX: usize = 1; -// TODO: this is an artificial offset and should be replaced with the actual offset -const WINDOWS_STATIC_SERVER_DATA_TABLE_OFFSET: usize = 0x750; -const WINDOWS_BASE_STATIC_SERVER_DATA_OFFSET: usize = - WINDOWS_STATIC_SERVER_DATA_TABLE_OFFSET + CSR_SERVER_DLL_MAX * core::mem::size_of::(); -const WINDOWS_OS_MAJOR_VERSION: u32 = 10; -const WINDOWS_OS_MINOR_VERSION: u32 = 0; +const WINDOWS_DIRECTORY: &str = r"C:\Windows"; +const WINDOWS_SYSTEM_DIRECTORY: &str = r"C:\Windows\System32"; +const WINDOWS_NAMED_OBJECT_DIRECTORY: &str = r"\BaseNamedObjects"; +const WINDOWS_OS_MAJOR_VERSION: u16 = 10; +const WINDOWS_OS_MINOR_VERSION: u16 = 0; const WINDOWS_OS_BUILD_NUMBER: u16 = 19041; const WINDOWS_OS_PLATFORM_WIN32_NT: u32 = 2; +const WINDOWS_TIME_ZONE_ID_INVALID: u32 = u32::MAX; const WINDOWS_CRITICAL_SECTION_TIMEOUT_100NS: i64 = -150 * 10_000_000; const WINDOWS_HEAP_SEGMENT_RESERVE: u64 = 1024 * 1024; const WINDOWS_HEAP_SEGMENT_COMMIT: u64 = 2 * PAGE_SIZE as u64; @@ -55,6 +59,16 @@ const WINDOWS_NT_TIB_VERSION: usize = 30 << 8; const INITIAL_PROCESS_ID: usize = 1; const INITIAL_THREAD_ID: usize = 1; +macro_rules! write_static_server_data_field { + ($platform:ty, $base:expr, $field:ident, $value:expr $(,)?) => { + write_guest_field_at_offset::<$platform, _, _>( + $base, + core::mem::offset_of!(BaseStaticServerData, $field), + $value, + ) + }; +} + pub(crate) struct WindowsProcessEnvironment { pub(crate) peb: usize, pub(crate) teb: usize, @@ -69,6 +83,16 @@ pub(crate) struct PeLoadInfo { pub(crate) environment: WindowsProcessEnvironment, } +struct ProcessEnvironmentInput<'a> { + image: &'a PeParsedFile, + image_base_address: usize, + image_path: &'a str, + argv: &'a [CString], + envp: &'a [CString], + stack_base: usize, + stack_allocation_top: usize, +} + pub(crate) struct PeLoader<'a, Platform: crate::ShimPlatform, FS: ShimFS> { platform: &'static Platform, fs: Arc, @@ -88,15 +112,15 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { } } - pub(crate) fn load(&self, path: &str) -> Result, WindowsLoadError> { + pub(crate) fn load( + &self, + path: &str, + argv: &[CString], + envp: &[CString], + ) -> Result, WindowsLoadError> { let image = load_image(self.platform, self.fs.clone(), path, self.page_manager)?; let application_entry_point = image.mapping.entry_point; - let ntdll = load_ntdll( - self.platform, - self.fs.clone(), - self.page_manager, - NTDLL_PATHS, - )?; + let ntdll = load_ntdll(self.platform, self.fs.clone(), self.page_manager)?; let entry_point = if let Some(ntdll) = &ntdll { if !ntdll.image.parsed.has_trampoline() { @@ -111,30 +135,31 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { let length = NonZeroPageSize::new(INITIAL_STACK_SIZE).ok_or(PeImageAccessError::AddressOverflow)?; // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` does not set - // `fixed_addr`, so the page manager picks an unused region — there is no overlapping- - // mapping precondition for the caller to uphold. + // `fixed_addr`, so the page manager picks an unused region and cannot replace a mapping. let stack_base = unsafe { self.page_manager .create_stack_pages(None, length, CreatePagesFlags::empty()) - .map_err(PeImageAccessError::Mapping)? - }; - let stack_top = stack_base + } + .map_err(PeImageAccessError::Mapping)?; + let stack_allocation_top = stack_base .as_usize() .checked_add(INITIAL_STACK_SIZE) .ok_or(PeImageAccessError::AddressOverflow)?; - let stack_top = if stack_top.is_multiple_of(16) { - stack_top - core::mem::size_of::() + let stack_top = if stack_allocation_top.is_multiple_of(16) { + stack_allocation_top - core::mem::size_of::() } else { - stack_top + stack_allocation_top }; - let environment = self.create_process_environment( - &image.parsed, - image.mapping.base_addr, - path, - stack_base.as_usize(), - stack_top, - )?; + let environment = self.create_process_environment(ProcessEnvironmentInput { + image: &image.parsed, + image_base_address: image.mapping.base_addr, + image_path: path, + argv, + envp, + stack_base: stack_base.as_usize(), + stack_allocation_top, + })?; if let Some(ntdll) = &ntdll { let context = X64Context::initial_thread_context( ntdll.exports.rtl_user_thread_start, @@ -142,8 +167,7 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { stack_top, environment.peb, ); - crate::write_slice::(environment.context, context.as_bytes()) - .ok_or(PeImageAccessError::MemoryAccess)?; + write_guest_slice::(environment.context, context.as_bytes())?; } let virtual_allocations = @@ -188,13 +212,11 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { }; // `KI_USER_INVERTED_FUNCTION_TABLE` lives in ntdll's writable `.mrdata` section. - crate::write_value::(table_address, header) - .ok_or(PeImageAccessError::MemoryAccess)?; + write_guest_value::(table_address, header)?; let entries_address = table_address .checked_add(core::mem::size_of::()) .ok_or(PeImageAccessError::AddressOverflow)?; - crate::write_slice::(entries_address, &entries) - .ok_or(PeImageAccessError::MemoryAccess)?; + write_guest_slice::(entries_address, &entries)?; litebox_util_log::debug!( table:% = format_args!("{table_address:#x}"); @@ -206,16 +228,14 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { fn create_process_environment( &self, - image: &PeParsedFile, - image_base_address: usize, - image_path: &str, - stack_base: usize, - stack_top: usize, + 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, @@ -224,23 +244,32 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { |_| Ok(0), ) }?; - let base = ptr.as_usize(); - Ok(base) + Ok(ptr.as_usize()) }; - let teb_ptr = create_pages(core::mem::size_of::())?; - let peb_ptr = create_pages(core::mem::size_of::())?; - let ctx_ptr = create_pages(core::mem::size_of::())?; + let teb_ptr = create_pages(size_of::())?; + let peb_ptr = create_pages(size_of::())?; + let api_set_map = build_api_set_namespace(API_SET_MAPPINGS) + .map_err(|_| PeImageAccessError::AddressOverflow)?; + let api_set_map_ptr = create_pages(api_set_map.len())?; + write_guest_slice::(api_set_map_ptr, &api_set_map)?; + let ctx_ptr = create_pages(size_of::())?; - let dos_image_path = dos_image_path(image_path); + 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(&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, @@ -253,7 +282,7 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { &redirection_dll_name, ]; let process_parameters_length = process_parameter_strings.iter().try_fold( - core::mem::size_of::(), + size_of::(), |length, string| { length .checked_add(usize::from(string.maximum_length)) @@ -265,61 +294,76 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { let process_parameters_ptr = create_pages(process_parameters_length)?; let mut process_parameters = RtlUserProcessParameters::new_zeroed(); - process_parameters.maximum_length = u32::try_from(process_parameters_allocation_length) - .map_err(|_| PeImageAccessError::AddressOverflow)?; - process_parameters.length = u32::try_from(process_parameters_length) - .map_err(|_| PeImageAccessError::AddressOverflow)?; + process_parameters.maximum_length = to_u32(process_parameters_allocation_length)?; + process_parameters.length = to_u32(process_parameters_length)?; process_parameters.flags = RtlUserProcFlags::NORMALIZED.bits(); - let mut process_parameter_tail = process_parameters_ptr - .checked_add(core::mem::size_of::()) - .ok_or(PeImageAccessError::AddressOverflow)?; - process_parameters.current_directory.dos_path = write_process_parameter_string::( - &mut process_parameter_tail, + 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 = - write_process_parameter_string::(&mut process_parameter_tail, &dll_path)?; - process_parameters.image_path_name = write_process_parameter_string::( - &mut process_parameter_tail, + 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 = - write_process_parameter_string::(&mut process_parameter_tail, &command_line)?; - process_parameters.window_title = - write_process_parameter_string::(&mut process_parameter_tail, &window_title)?; - process_parameters.desktop_info = - write_process_parameter_string::(&mut process_parameter_tail, &desktop_info)?; - process_parameters.shell_info = - write_process_parameter_string::(&mut process_parameter_tail, &shell_info)?; - process_parameters.runtime_data = - write_process_parameter_string::(&mut process_parameter_tail, &runtime_data)?; - process_parameters.redirection_dll_name = write_process_parameter_string::( - &mut process_parameter_tail, + 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, )?; - crate::write_value::(process_parameters_ptr, process_parameters) + 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 read_only_static_server_data = read_only_shared_memory_base - .checked_add(WINDOWS_STATIC_SERVER_DATA_TABLE_OFFSET) - .ok_or(PeImageAccessError::AddressOverflow)?; - let base_static_server_data = read_only_shared_memory_base - .checked_add(WINDOWS_BASE_STATIC_SERVER_DATA_OFFSET) - .ok_or(PeImageAccessError::AddressOverflow)?; - let base_static_server_data_entry = read_only_static_server_data - .checked_add(BASESRV_SERVERDLL_INDEX * core::mem::size_of::()) - .ok_or(PeImageAccessError::AddressOverflow)?; - crate::write_value::(base_static_server_data_entry, base_static_server_data) - .ok_or(PeImageAccessError::MemoryAccess)?; - + 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 = image_base_address; - if image_base_address != image.image_base() || image.has_dynamic_base() { + 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; @@ -328,23 +372,24 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { 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 = WINDOWS_OS_MAJOR_VERSION; - peb.os_minor_version = WINDOWS_OS_MINOR_VERSION; + peb.os_major_version = u32::from(WINDOWS_OS_MAJOR_VERSION); + peb.os_minor_version = u32::from(WINDOWS_OS_MINOR_VERSION); peb.os_build_number = WINDOWS_OS_BUILD_NUMBER; peb.os_platform_id = WINDOWS_OS_PLATFORM_WIN32_NT; - peb.image_subsystem = u32::from(image.subsystem()); - peb.image_subsystem_major_version = u32::from(image.major_subsystem_version()); - peb.image_subsystem_minor_version = u32::from(image.minor_subsystem_version()); + 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; peb.csr_server_read_only_shared_memory_base = read_only_shared_memory_base as u64; - crate::write_value::(peb_ptr, peb).ok_or(PeImageAccessError::MemoryAccess)?; + write_guest_value::(peb_ptr, peb)?; let mut teb = ThreadEnvironmentBlock::new_zeroed(); teb.nt_tib.exception_list = 0; - teb.nt_tib.stack_base = stack_top; - teb.nt_tib.stack_limit = stack_base; + teb.nt_tib.stack_base = input.stack_allocation_top; + teb.nt_tib.stack_limit = input.stack_base; teb.nt_tib.fiber_data_or_version = WINDOWS_NT_TIB_VERSION; teb.nt_tib.self_pointer = teb_ptr; // TODO: set real ID @@ -360,8 +405,8 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { teb_ptr + core::mem::offset_of!(ThreadEnvironmentBlock, activation_stack); teb.static_unicode_string = initial_teb_static_unicode_string(teb_ptr, &teb.static_unicode_buffer)?; - teb.deallocation_stack = stack_base; - crate::write_value::(teb_ptr, teb).ok_or(PeImageAccessError::MemoryAccess)?; + teb.deallocation_stack = input.stack_base; + write_guest_value::(teb_ptr, teb)?; Ok(WindowsProcessEnvironment { peb: peb_ptr, teb: teb_ptr, @@ -370,6 +415,91 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { } } +fn initialize_windows_static_server_data( + shared_heap: &mut GuestMemoryAllocator, +) -> Result { + let read_only_static_server_data = + shared_heap.allocate_array::(CSR_SERVER_DLL_MAX)?; + let client_base_static_server_data = + shared_heap.allocate::()?; + initialize_static_server_data::(shared_heap, client_base_static_server_data)?; + + read_only_static_server_data + .write_at_offset( + BASESRV_SERVERDLL_INDEX.cast_signed(), + client_base_static_server_data.as_usize(), + ) + .ok_or(PeImageAccessError::MemoryAccess)?; + Ok(read_only_static_server_data.as_usize()) +} + +fn initialize_static_server_data( + shared_heap: &mut GuestMemoryAllocator, + base_static_server_data: MutPtr, +) -> Result<(), PeImageAccessError> { + let windows_directory = + allocate_guest_unicode_string_from_str::(shared_heap, WINDOWS_DIRECTORY)?; + write_static_server_data_field!( + Platform, + base_static_server_data, + windows_directory, + windows_directory, + )?; + let windows_system_directory = + allocate_guest_unicode_string_from_str::(shared_heap, WINDOWS_SYSTEM_DIRECTORY)?; + write_static_server_data_field!( + Platform, + base_static_server_data, + windows_system_directory, + windows_system_directory, + )?; + let named_object_directory = allocate_guest_unicode_string_from_str::( + shared_heap, + WINDOWS_NAMED_OBJECT_DIRECTORY, + )?; + write_static_server_data_field!( + Platform, + base_static_server_data, + named_object_directory, + named_object_directory, + )?; + write_static_server_data_field!( + Platform, + base_static_server_data, + windows_major_version, + WINDOWS_OS_MAJOR_VERSION, + )?; + write_static_server_data_field!( + Platform, + base_static_server_data, + windows_minor_version, + WINDOWS_OS_MINOR_VERSION, + )?; + write_static_server_data_field!( + Platform, + base_static_server_data, + build_number, + WINDOWS_OS_BUILD_NUMBER, + )?; + + let ini_file_mapping = shared_heap + .allocate::()? + .as_usize(); + write_static_server_data_field!( + Platform, + base_static_server_data, + ini_file_mapping, + ini_file_mapping, + )?; + write_static_server_data_field!( + Platform, + base_static_server_data, + termsrv_client_time_zone_id, + WINDOWS_TIME_ZONE_ID_INVALID, + )?; + Ok(()) +} + fn register_image_virtual_allocation( virtual_allocations: &crate::WindowsVirtualAllocations, mapping: MappingInfo, @@ -379,7 +509,7 @@ fn register_image_virtual_allocation( mapping.base_addr, crate::WindowsVirtualAllocation { base: mapping.base_addr, - size: mapping.image_size, + size: mapping.mapping_size, allocation_protect: PageProtection::PAGE_EXECUTE_WRITECOPY, type_: MemoryType::MEM_IMAGE, pages, @@ -425,6 +555,482 @@ impl LoadedImage { } } +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, FromBytes, Immutable, IntoBytes, KnownLayout)] +struct RtlCriticalSection { + debug_info: usize, + lock_count: i32, + recursion_count: u32, + owning_thread: usize, + lock_semaphore: usize, + spin_count: usize, +} + +impl RtlCriticalSection { + const fn initialized(spin_count: usize) -> Self { + Self { + debug_info: usize::MAX, + lock_count: -1, + recursion_count: 0, + owning_thread: 0, + lock_semaphore: 0, + spin_count, + } + } +} + +struct GuestMemoryAllocator { + cursor: usize, + end: usize, +} + +impl GuestMemoryAllocator { + fn new(base: usize, size: usize) -> Result { + let cursor = base; + let end = checked_add(base, size)?; + if cursor > end { + return Err(PeImageAccessError::AddressOverflow); + } + Ok(Self { cursor, end }) + } + + fn allocate(&mut self) -> Result, PeImageAccessError> + where + Platform: RawPointerProvider, + T: FromBytes + IntoBytes, + { + self.allocate_array::(1) + } + + fn allocate_array( + &mut self, + count: usize, + ) -> Result, PeImageAccessError> + where + Platform: RawPointerProvider, + T: FromBytes + IntoBytes, + { + let address = self.allocate_bytes(checked_mul(size_of::(), count)?, align_of::())?; + Ok(MutPtr::::from_usize(address)) + } + + fn allocate_bytes( + &mut self, + size: usize, + alignment: usize, + ) -> Result { + debug_assert!(alignment.is_power_of_two()); + let address = self + .cursor + .checked_next_multiple_of(alignment) + .ok_or(PeImageAccessError::AddressOverflow)?; + let cursor = checked_add(address, size)?; + if cursor > self.end { + return Err(PeImageAccessError::AddressOverflow); + } + self.cursor = cursor; + Ok(address) + } +} + +// Reference layout from ReactOS `sdk/include/reactos/subsys/win/base.h`. +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct BaseStaticServerData { + windows_directory: UnicodeString, + windows_system_directory: UnicodeString, + named_object_directory: UnicodeString, + windows_major_version: u16, + windows_minor_version: u16, + build_number: u16, + csd_number: u16, + rc_number: u16, + csd_version: [u16; 128], + padding_0: [u8; 6], + sys_info: SystemBasicInformation, + time_of_day: SystemTimeOfDayInformation, + ini_file_mapping: usize, + nls_user_info: NlsUserInfo, + default_separate_vdm: u8, + is_wow_task_ready: u8, + padding_1: [u8; 6], + windows_sys32_x86_directory: UnicodeString, + f_termsrv_app_install_mode: u8, + padding_2: [u8; 3], + tzi_termsrv_client_time_zone: TimeZoneInformation, + kt_termsrv_client_bias: KSystemTime, + termsrv_client_time_zone_id: u32, + luid_device_maps_enabled: u8, + padding_3: [u8; 3], + termsrv_client_time_zone_change_num: u32, +} + +#[allow(clippy::struct_field_names)] +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct IniFileMapping { + file_names: usize, + default_file_name_mapping: usize, + win_ini_file_mapping: usize, + reserved: u32, + padding: [u8; 4], +} + +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct SystemBasicInformation { + reserved: u32, + timer_resolution: u32, + page_size: u32, + number_of_physical_pages: u32, + lowest_physical_page_number: u32, + highest_physical_page_number: u32, + allocation_granularity: u32, + padding_0: [u8; 4], + minimum_user_mode_address: usize, + maximum_user_mode_address: usize, + active_processors_affinity_mask: usize, + number_of_processors: u8, + padding_1: [u8; 7], +} + +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct SystemTimeOfDayInformation { + boot_time: i64, + current_time: i64, + time_zone_bias: i64, + time_zone_id: u32, + reserved: u32, + boot_time_bias: u64, + sleep_time_bias: u64, +} + +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct NlsUserInfo { + s_language: [u16; 80], + i_country: [u16; 80], + s_country: [u16; 80], + s_list: [u16; 80], + i_measure: [u16; 80], + i_paper_size: [u16; 80], + s_decimal: [u16; 80], + s_thousand: [u16; 80], + s_grouping: [u16; 80], + i_digits: [u16; 80], + i_l_zero: [u16; 80], + i_neg_number: [u16; 80], + s_native_digits: [u16; 80], + num_shape: [u16; 80], + s_currency: [u16; 80], + s_mon_dec_sep: [u16; 80], + s_mon_thou_sep: [u16; 80], + s_mon_grouping: [u16; 80], + i_curr_digits: [u16; 80], + i_currency: [u16; 80], + i_neg_curr: [u16; 80], + s_positive_sign: [u16; 80], + s_negative_sign: [u16; 80], + s_time_format: [u16; 80], + s_time: [u16; 80], + i_time: [u16; 80], + i_tl_zero: [u16; 80], + i_time_prefix: [u16; 80], + s_1159: [u16; 80], + s_2359: [u16; 80], + s_short_date: [u16; 80], + s_date: [u16; 80], + i_date: [u16; 80], + s_year_month: [u16; 80], + s_long_date: [u16; 80], + i_cal_type: [u16; 80], + i_first_day_of_week: [u16; 80], + i_first_week_of_year: [u16; 80], + locale: [u16; 80], + user_locale_id: u32, + interactive_user_luid: Luid, + ul_cache_update_count: u32, +} + +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct Luid { + low_part: u32, + high_part: i32, +} + +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct TimeZoneInformation { + bias: i32, + standard_name: [u16; 32], + standard_date: SystemTime, + standard_bias: i32, + daylight_name: [u16; 32], + daylight_date: SystemTime, + daylight_bias: i32, +} + +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct SystemTime { + year: u16, + month: u16, + day_of_week: u16, + day: u16, + hour: u16, + minute: u16, + second: u16, + milliseconds: u16, +} + +#[repr(C)] +#[derive(FromBytes, IntoBytes)] +struct KSystemTime { + low_part: u32, + high_1_time: i32, + high_2_time: i32, +} + +const API_SET_MAPPINGS: &[(&str, &str)] = &[ + ("api-ms-win-core-apiquery-l1-1-0", "ntdll.dll"), + ("api-ms-win-core-apiquery-l1-1-2", "ntdll.dll"), + ("api-ms-win-core-apiquery-l2-1-1", "kernelbase.dll"), + ("api-ms-win-core-appcompat-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-appcompat-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-appinit-l1-1-0", "kernel32.dll"), + ("api-ms-win-core-atoms-l1-1-0", "kernel32.dll"), + ("api-ms-win-core-backgroundtask-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-calendar-l1-1-0", "kernel32.dll"), + ("api-ms-win-core-comm-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-comm-l1-1-2", "kernelbase.dll"), + ("api-ms-win-core-commandlinetoargv-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-console-ansi-l2-1-0", "kernel32.dll"), + ("api-ms-win-core-console-internal-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-console-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-console-l1-2-0", "kernelbase.dll"), + ("api-ms-win-core-console-l1-2-1", "kernelbase.dll"), + ("api-ms-win-core-console-l1-2-2", "kernelbase.dll"), + ("api-ms-win-core-console-l2-1-0", "kernelbase.dll"), + ("api-ms-win-core-console-l2-2-0", "kernelbase.dll"), + ("api-ms-win-core-console-l3-1-0", "kernelbase.dll"), + ("api-ms-win-core-console-l3-2-0", "kernelbase.dll"), + ("api-ms-win-core-crt-l1-1-0", "ntdll.dll"), + ("api-ms-win-core-crt-l2-1-0", "kernelbase.dll"), + ("api-ms-win-core-datetime-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-datetime-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-datetime-l1-1-2", "kernelbase.dll"), + ("api-ms-win-core-debug-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-debug-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-debug-l1-1-2", "kernelbase.dll"), + ("api-ms-win-core-delayload-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-delayload-l1-1-1", "kernelbase.dll"), + ("api-ms-win-downlevel-shlwapi-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-errorhandling-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-errorhandling-l1-1-2", "kernelbase.dll"), + ("api-ms-win-core-errorhandling-l1-1-3", "kernelbase.dll"), + ("api-ms-win-core-fibers-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-fibers-l1-1-2", "kernelbase.dll"), + ("api-ms-win-core-fibers-l2-1-0", "kernelbase.dll"), + ("api-ms-win-core-fibers-l2-1-1", "kernelbase.dll"), + ("api-ms-win-core-file-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-file-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-file-l1-2-0", "kernelbase.dll"), + ("api-ms-win-core-file-l1-2-1", "kernelbase.dll"), + ("api-ms-win-core-file-l1-2-2", "kernelbase.dll"), + ("api-ms-win-core-file-l1-2-3", "kernelbase.dll"), + ("api-ms-win-core-file-l1-2-5", "kernelbase.dll"), + ("api-ms-win-core-file-l2-1-0", "kernelbase.dll"), + ("api-ms-win-core-file-l2-1-1", "kernelbase.dll"), + ("api-ms-win-core-file-l2-1-2", "kernelbase.dll"), + ("api-ms-win-core-file-l2-1-3", "kernelbase.dll"), + ("api-ms-win-core-file-l2-1-4", "kernelbase.dll"), + ("api-ms-win-core-handle-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-heap-obsolete-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-heap-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-heap-l1-2-0", "kernelbase.dll"), + ("api-ms-win-core-heap-l2-1-0", "kernelbase.dll"), + ("api-ms-win-core-interlocked-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-io-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-io-l1-1-1", "kernel32.dll"), + ("api-ms-win-core-job-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-largeinteger-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-libraryloader-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-libraryloader-l1-2-0", "kernelbase.dll"), + ("api-ms-win-core-libraryloader-l1-2-1", "kernelbase.dll"), + ("api-ms-win-core-libraryloader-l1-2-2", "kernelbase.dll"), + ("api-ms-win-core-libraryloader-l1-2-3", "kernelbase.dll"), + ("api-ms-win-core-libraryloader-l2-1-0", "kernelbase.dll"), + ("api-ms-win-core-localization-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-localization-l1-2-0", "kernelbase.dll"), + ("api-ms-win-core-localization-l1-2-4", "kernelbase.dll"), + ("api-ms-win-core-localization-l2-1-0", "kernelbase.dll"), + ( + "api-ms-win-core-localization-private-l1-1-0", + "kernelbase.dll", + ), + ("api-ms-win-core-localregistry-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-memory-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-memory-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-memory-l1-1-2", "kernelbase.dll"), + ("api-ms-win-core-memory-l1-1-9", "kernelbase.dll"), + ("api-ms-win-core-misc-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-namedpipe-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-namedpipe-l1-2-1", "kernelbase.dll"), + ("api-ms-win-core-namedpipe-l1-2-2", "kernelbase.dll"), + ("api-ms-win-core-namespace-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-normalization-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-path-l1-1-0", "kernelbase.dll"), + ( + "api-ms-win-core-processenvironment-l1-1-0", + "kernelbase.dll", + ), + ( + "api-ms-win-core-processenvironment-l1-1-1", + "kernelbase.dll", + ), + ( + "api-ms-win-core-processenvironment-l1-2-0", + "kernelbase.dll", + ), + ("api-ms-win-core-processsnapshot-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-processthreads-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-processthreads-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-processthreads-l1-1-2", "kernelbase.dll"), + ("api-ms-win-core-processthreads-l1-1-3", "kernelbase.dll"), + ("api-ms-win-core-processthreads-l1-1-8", "kernel32.dll"), + ("api-ms-win-core-processtopology-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-profile-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-pcw-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-psapi-ansi-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-psapi-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-realtime-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-registry-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-rtlsupport-l1-1-0", "ntdll.dll"), + ("api-ms-win-core-rtlsupport-l1-1-1", "ntdll.dll"), + ("api-ms-win-core-rtlsupport-l1-2-2", "ntdll.dll"), + ("api-ms-win-core-sidebyside-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-string-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-string-l2-1-1", "kernelbase.dll"), + ("api-ms-win-core-synch-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-synch-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-synch-l1-2-0", "kernelbase.dll"), + ("api-ms-win-core-synch-l1-2-1", "kernelbase.dll"), + ("api-ms-win-core-sysinfo-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-sysinfo-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-sysinfo-l1-2-0", "kernelbase.dll"), + ("api-ms-win-core-sysinfo-l1-2-1", "kernelbase.dll"), + ("api-ms-win-core-sysinfo-l1-2-3", "kernelbase.dll"), + ("api-ms-win-core-sysinfo-l1-2-8", "kernelbase.dll"), + ("api-ms-win-core-systemtopology-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-systemtopology-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-threadpool-legacy-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-threadpool-l1-2-0", "kernelbase.dll"), + ( + "api-ms-win-core-threadpool-private-l1-1-0", + "kernelbase.dll", + ), + ("api-ms-win-core-timezone-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-util-l1-1-0", "kernelbase.dll"), + ( + "api-ms-win-core-windowserrorreporting-l1-1-0", + "kernelbase.dll", + ), + ( + "api-ms-win-core-windowserrorreporting-l1-1-1", + "kernelbase.dll", + ), + ( + "api-ms-win-core-windowserrorreporting-l1-1-2", + "kernelbase.dll", + ), + ( + "api-ms-win-core-windowserrorreporting-l1-1-3", + "kernelbase.dll", + ), + ("api-ms-win-core-wow64-l1-1-0", "kernelbase.dll"), + ("api-ms-win-core-wow64-l1-1-1", "kernelbase.dll"), + ("api-ms-win-core-wow64-l1-1-3", "kernelbase.dll"), + ("api-ms-win-core-xstate-l2-1-0", "kernelbase.dll"), + ("api-ms-win-core-xstate-l2-1-1", "kernelbase.dll"), + ("api-ms-win-core-xstate-l2-1-2", "kernelbase.dll"), + ("api-ms-win-eventing-consumer-l1-1-0", "sechost.dll"), + ("api-ms-win-eventing-consumer-l1-1-1", "sechost.dll"), + ("api-ms-win-eventing-controller-l1-1-0", "sechost.dll"), + ("api-ms-win-eventing-provider-l1-1-0", "kernelbase.dll"), + ("api-ms-win-security-audit-l1-1-0", "sechost.dll"), + ("api-ms-win-security-audit-l1-1-1", "sechost.dll"), + ("api-ms-win-security-appcontainer-l1-1-0", "kernelbase.dll"), + ("api-ms-win-security-base-l1-1-0", "kernelbase.dll"), + ("api-ms-win-security-base-l1-2-0", "kernelbase.dll"), + ("api-ms-win-security-base-private-l1-1-0", "kernelbase.dll"), + ("api-ms-win-security-lsalookup-l1-1-0", "sechost.dll"), + ("api-ms-win-security-sddl-l1-1-0", "sechost.dll"), + ("api-ms-win-service-core-l1-1-0", "sechost.dll"), + ("api-ms-win-service-core-l1-1-1", "sechost.dll"), + ("api-ms-win-service-core-l1-1-2", "sechost.dll"), + ("api-ms-win-service-management-l1-1-0", "sechost.dll"), + ("api-ms-win-service-management-l2-1-0", "sechost.dll"), + ("api-ms-win-service-private-l1-1-0", "sechost.dll"), + ("api-ms-win-service-private-l1-1-2", "sechost.dll"), + ("api-ms-win-service-private-l1-1-3", "sechost.dll"), + ("api-ms-win-service-winsvc-l1-1-0", "sechost.dll"), + ("ext-ms-win-appcompat-apphelp-l1-1-2", "apphelp.dll"), + ("ext-ms-win-authz-context-l1-1-0", "authz.dll"), + ("ext-ms-win-core-winrt-remote-l1-1-0", ""), + ("ext-ms-win-oobe-query-l1-1-0", ""), + ( + "ext-ms-win-packagevirtualizationcontext-l1-1-0", + "daxexec.dll", + ), + ("ext-ms-win-rpc-ssl-l1-1-0", "rpcrtremote.dll"), +]; + +fn checked_add(left: usize, right: usize) -> Result { + left.checked_add(right) + .ok_or(PeImageAccessError::AddressOverflow) +} + +fn checked_mul(left: usize, right: usize) -> Result { + left.checked_mul(right) + .ok_or(PeImageAccessError::AddressOverflow) +} + +fn to_u32(value: usize) -> Result { + u32::try_from(value).map_err(|_| PeImageAccessError::AddressOverflow) +} + +fn write_guest_value(address: usize, value: T) -> Result<(), PeImageAccessError> +where + Platform: RawPointerProvider, + T: FromBytes + IntoBytes, +{ + crate::write_value::(address, value).ok_or(PeImageAccessError::MemoryAccess) +} + +fn write_guest_field_at_offset( + base: MutPtr, + field_offset: usize, + value: Field, +) -> Result<(), PeImageAccessError> +where + Platform: RawPointerProvider, + Struct: FromBytes + IntoBytes, + Field: FromBytes + IntoBytes, +{ + crate::write_field_at_offset::(base, field_offset, value) + .ok_or(PeImageAccessError::MemoryAccess) +} + +fn write_guest_slice(address: usize, values: &[T]) -> Result<(), PeImageAccessError> +where + Platform: RawPointerProvider, + T: Copy + FromBytes + IntoBytes, +{ + crate::write_slice::(address, values).ok_or(PeImageAccessError::MemoryAccess) +} + struct LoadedNtDll { image: LoadedImage, exports: NtDllExports, @@ -444,9 +1050,8 @@ fn load_ntdll( platform: &'static Platform, fs: Arc, page_manager: &crate::WindowsPageManager, - ntdll_paths: &[&str], ) -> Result, WindowsLoadError> { - for path in ntdll_paths { + for path in NTDLL_PATHS { match load_image_with_writable_sections( fs.clone(), path, @@ -668,6 +1273,16 @@ impl PeImageMapper<'_, Platform, FS> self.pages.insert(start..end, protect); Ok(()) } + + fn protect_and_record_pages( + &mut self, + address: usize, + len: usize, + prot: Protection, + ) -> Result<(), PeImageAccessError> { + protect_pages(self.page_manager, address, len, prot)?; + self.record_pages(address, len, page_protection_from_loader_protection(prot)) + } } impl MapMemory for PeImageMapper<'_, Platform, FS> { @@ -717,8 +1332,7 @@ impl MapMemory for PeImageMapper<'_, .ok_or(PeImageAccessError::MemoryAccess)?; written += chunk; } - protect_pages(self.page_manager, address, len, *prot)?; - self.record_pages(address, len, page_protection_from_loader_protection(*prot)) + self.protect_and_record_pages(address, len, *prot) } fn map_file( @@ -747,8 +1361,7 @@ impl MapMemory for PeImageMapper<'_, .ok_or(PeImageAccessError::MemoryAccess)?; read += n; } - protect_pages(self.page_manager, address, len, *prot)?; - self.record_pages(address, len, page_protection_from_loader_protection(*prot)) + self.protect_and_record_pages(address, len, *prot) } fn protect( @@ -757,8 +1370,7 @@ impl MapMemory for PeImageMapper<'_, len: usize, prot: &Protection, ) -> Result<(), Self::Error> { - protect_pages(self.page_manager, address, len, *prot)?; - self.record_pages(address, len, page_protection_from_loader_protection(*prot)) + self.protect_and_record_pages(address, len, *prot) } } @@ -865,33 +1477,93 @@ fn page_range(address: usize, len: usize) -> Result<(usize, usize), PeImageAcces Ok((start, end - start)) } -fn dos_image_path(path: &str) -> String { - let mut dos_path = String::from(r"\??\C:"); +fn win32_image_path(path: &str) -> String { + let mut win32_path = String::from("C:"); if !path.starts_with('/') && !path.starts_with('\\') { - dos_path.push('\\'); + win32_path.push('\\'); } for ch in path.chars() { - dos_path.push(if ch == '/' { '\\' } else { ch }); + win32_path.push(if ch == '/' { '\\' } else { ch }); } + win32_path +} + +fn dos_image_path(path: &str) -> String { + let mut dos_path = String::from(r"\??\"); + dos_path.push_str(&win32_image_path(path)); dos_path } -fn write_process_parameter_string( - process_parameter_tail: &mut usize, - string: &Utf16StringBuffer, -) -> Result { - let buffer = *process_parameter_tail; - crate::write_slice::(buffer, &string.units) - .ok_or(PeImageAccessError::MemoryAccess)?; - *process_parameter_tail = (*process_parameter_tail) - .checked_add(usize::from(string.maximum_length)) - .ok_or(PeImageAccessError::AddressOverflow)?; - Ok(UnicodeString { - length: string.length, - maximum_length: string.maximum_length, - padding_0: [0; 4], - buffer, - }) +fn windows_command_line(image_path: &str, argv: &[CString]) -> String { + let mut command_line = String::new(); + if let Some(arg0) = argv.first() { + push_windows_quoted_arg(&mut command_line, &cstring_to_string(arg0)); + } else { + push_windows_quoted_arg(&mut command_line, image_path); + } + for arg in argv.iter().skip(1) { + command_line.push(' '); + push_windows_quoted_arg(&mut command_line, &cstring_to_string(arg)); + } + command_line +} + +fn push_windows_quoted_arg(command_line: &mut String, arg: &str) { + if !arg.is_empty() && !arg.contains([' ', '\t', '"']) { + command_line.push_str(arg); + return; + } + + command_line.push('"'); + let mut backslashes = 0; + for ch in arg.chars() { + if ch == '\\' { + backslashes += 1; + } else if ch == '"' { + for _ in 0..=backslashes * 2 { + command_line.push('\\'); + } + command_line.push('"'); + backslashes = 0; + } else { + for _ in 0..backslashes { + command_line.push('\\'); + } + command_line.push(ch); + backslashes = 0; + } + } + for _ in 0..backslashes * 2 { + command_line.push('\\'); + } + command_line.push('"'); +} + +fn windows_environment_block(envp: &[CString]) -> Vec { + let mut variables = envp.iter().map(cstring_to_string).collect::>(); + variables.sort_by(|left, right| { + left.bytes() + .map(|byte| byte.to_ascii_uppercase()) + .cmp(right.bytes().map(|byte| byte.to_ascii_uppercase())) + }); + + let mut block = Vec::new(); + for variable in variables { + block.extend(variable.encode_utf16()); + block.push(0); + } + block.push(0); + if envp.is_empty() { + block.push(0); + } + block +} + +fn cstring_to_string(value: &CString) -> String { + match core::str::from_utf8(value.as_bytes()) { + Ok(value) => String::from(value), + Err(_) => String::from_utf8_lossy(value.as_bytes()).into_owned(), + } } struct InitialProcessHeaps { @@ -912,6 +1584,30 @@ fn initial_process_heaps_array(peb_ptr: usize) -> Result( + shared_heap: &mut GuestMemoryAllocator, + value: &str, +) -> Result { + let string = Utf16StringBuffer::new(value)?; + allocate_guest_unicode_string::(shared_heap, &string) +} + +fn allocate_guest_unicode_string( + allocation: &mut GuestMemoryAllocator, + string: &Utf16StringBuffer, +) -> Result { + let buffer = allocation.allocate_array::(string.units.len())?; + buffer + .write_slice_at_offset(0, &string.units) + .ok_or(PeImageAccessError::MemoryAccess)?; + Ok(UnicodeString { + length: string.length, + maximum_length: string.maximum_length, + padding_0: [0; 4], + buffer: buffer.as_usize(), + }) +} + fn initial_teb_static_unicode_string( teb_ptr: usize, static_unicode_buffer: &[u16], @@ -964,6 +1660,10 @@ mod tests { use alloc::{string::String, vec, vec::Vec}; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; + use litebox_common_windows::loader::{ + ApiSetHashEntry, ApiSetNamespace, ApiSetNamespaceEntry, ApiSetValueEntry, + MAX_API_SET_NAMESPACE_SIZE, api_set_hash_prefix, + }; use super::*; use crate::nt_types::{ProcessEnvironmentBlock, ThreadEnvironmentBlock, UnicodeString}; @@ -986,6 +1686,7 @@ mod tests { lp_filename: *mut u16, n_size: u32, ) -> u32; + fn RtlGetCurrentPeb() -> *const ProcessEnvironmentBlock; } macro_rules! print_diff_fields { @@ -1004,6 +1705,7 @@ mod tests { ($host).csd_version, ); }; + ($prefix:literal, $synthetic:expr, $host:expr, static_unicode_string) => { print_unicode_string_diff( concat!($prefix, ".", stringify!(static_unicode_string)), @@ -1020,28 +1722,346 @@ mod tests { }; } - #[allow(clippy::similar_names)] + fn table_offset(base: u32, index: u32, entry_size: usize) -> Option { + (base as usize).checked_add((index as usize).checked_mul(entry_size)?) + } + + fn read_utf16_string(bytes: &[u8], offset: u32, len: u32) -> Option { + let offset = offset as usize; + let len = len as usize; + let end = offset.checked_add(len)?; + let bytes = bytes.get(offset..end)?; + let mut chunks = bytes.chunks_exact(size_of::()); + if !chunks.remainder().is_empty() { + return None; + } + let units = chunks + .by_ref() + .map(|chunk| u16::from_le_bytes(chunk.try_into().expect("u16 byte chunk"))) + .collect::>(); + Some(String::from_utf16_lossy(&units)) + } + + fn parse_api_set_value_entry(bytes: &[u8], offset: usize) -> Option { + Some( + ApiSetValueEntry::read_from_prefix(bytes.get(offset..)?) + .ok()? + .0, + ) + } + + fn api_set_value_entry_value(entry: ApiSetValueEntry, bytes: &[u8]) -> Option { + read_utf16_string(bytes, entry.value_offset, entry.value_length) + } + + fn api_set_value_entry_name(entry: ApiSetValueEntry, bytes: &[u8]) -> Option { + read_utf16_string(bytes, entry.name_offset, entry.name_length) + } + + fn parse_api_set_hash_entry(bytes: &[u8], offset: usize) -> Option { + Some( + ApiSetHashEntry::read_from_prefix(bytes.get(offset..)?) + .ok()? + .0, + ) + } + + fn parse_api_set_namespace_entry(bytes: &[u8], offset: usize) -> Option { + Some( + ApiSetNamespaceEntry::read_from_prefix(bytes.get(offset..)?) + .ok()? + .0, + ) + } + + fn api_set_namespace_entry_name(entry: ApiSetNamespaceEntry, bytes: &[u8]) -> Option { + read_utf16_string(bytes, entry.name_offset, entry.name_length) + } + + fn api_set_namespace_entry_value( + entry: ApiSetNamespaceEntry, + bytes: &[u8], + index: u32, + ) -> Option { + if index >= entry.value_count { + return None; + } + parse_api_set_value_entry( + bytes, + table_offset(entry.value_offset, index, size_of::())?, + ) + } + + fn parse_api_set_namespace(bytes: &[u8]) -> Option { + let namespace = ApiSetNamespace::read_from_prefix(bytes).ok()?.0; + let size = namespace.size as usize; + if size != bytes.len() + || !(size_of::()..=MAX_API_SET_NAMESPACE_SIZE).contains(&size) + { + return None; + } + if table_offset( + namespace.entry_offset, + namespace.count, + size_of::(), + )? > size + { + return None; + } + if table_offset( + namespace.hash_offset, + namespace.count, + size_of::(), + )? > size + { + return None; + } + Some(namespace) + } + + fn api_set_namespace_entry( + namespace: ApiSetNamespace, + bytes: &[u8], + index: u32, + ) -> Option { + if index >= namespace.count { + return None; + } + parse_api_set_namespace_entry( + bytes, + table_offset( + namespace.entry_offset, + index, + size_of::(), + )?, + ) + } + + fn api_set_namespace_hash_entry( + namespace: ApiSetNamespace, + bytes: &[u8], + index: u32, + ) -> Option { + if index >= namespace.count { + return None; + } + parse_api_set_hash_entry( + bytes, + table_offset(namespace.hash_offset, index, size_of::())?, + ) + } + + fn host_api_set_namespace_bytes() -> Vec { + let peb = unsafe { + // SAFETY: `RtlGetCurrentPeb` returns the current process PEB pointer on Windows. + RtlGetCurrentPeb().as_ref() + } + .expect("host PEB"); + let namespace_ptr = peb.api_set_map as *const ApiSetNamespace; + let namespace = unsafe { + // SAFETY: `ApiSetMap` points at the host process API_SET_NAMESPACE while the + // process is alive; we read only the fixed header first to learn its size. + namespace_ptr.as_ref() + } + .expect("host API_SET_NAMESPACE header"); + let size = namespace.size as usize; + assert!( + (size_of::()..=MAX_API_SET_NAMESPACE_SIZE).contains(&size), + "host API_SET_NAMESPACE has unexpected size {size:#x}" + ); + let bytes = unsafe { + // SAFETY: The size was read from the validated namespace header above, and the + // host API-set namespace is immutable process-wide data owned by ntdll. + core::slice::from_raw_parts(peb.api_set_map as *const u8, size) + }; + bytes.to_vec() + } + + fn api_set_default_value(bytes: &[u8], contract: &str) -> Option { + let namespace = parse_api_set_namespace(bytes)?; + for index in 0..namespace.count { + let entry = api_set_namespace_entry(namespace, bytes, index)?; + if api_set_namespace_entry_name(entry, bytes)?.eq_ignore_ascii_case(contract) { + let value = api_set_namespace_entry_value(entry, bytes, 0)?; + return api_set_value_entry_value(value, bytes); + } + } + None + } + + fn assert_api_set_hash_table(bytes: &[u8]) { + let namespace = parse_api_set_namespace(bytes).expect("valid API_SET_NAMESPACE"); + let mut previous = None; + for hash_index in 0..namespace.count { + let hash_entry = + api_set_namespace_hash_entry(namespace, bytes, hash_index).expect("hash entry"); + let entry = api_set_namespace_entry(namespace, bytes, hash_entry.index) + .expect("hash entry target"); + let name = api_set_namespace_entry_name(entry, bytes).expect("hash entry target name"); + let expected_hash = api_set_hash_with_hashed_length(&name, entry.hashed_length) + .expect("valid API-set hashed length"); + assert_eq!(hash_entry.hash, expected_hash, "hash for {name}"); + if let Some((previous_hash, previous_index)) = previous { + assert!( + (previous_hash, previous_index) <= (hash_entry.hash, hash_entry.index), + "API-set hash table is not sorted" + ); + } + previous = Some((hash_entry.hash, hash_entry.index)); + } + } + + fn api_set_hash_with_hashed_length(name: &str, hashed_length: u32) -> Option { + let code_unit_bytes = u32::try_from(size_of::()).ok()?; + if !name.is_ascii() || !hashed_length.is_multiple_of(code_unit_bytes) { + return None; + } + let hashed_units = usize::try_from(hashed_length / code_unit_bytes).ok()?; + let prefix = name.get(..hashed_units)?; + Some(api_set_hash_prefix(prefix)) + } + + fn dump_api_set_entries(bytes: &[u8], namespace: ApiSetNamespace) { + std::println!("entries:"); + for index in 0..namespace.count { + let entry = api_set_namespace_entry(namespace, bytes, index).expect("namespace entry"); + std::println!( + " {index:04} name={} flags={:#x} hashed_len={} values={}", + api_set_namespace_entry_name(entry, bytes) + .unwrap_or_else(|| String::from("")), + entry.flags, + entry.hashed_length, + entry.value_count + ); + for value_index in 0..entry.value_count { + let value = api_set_namespace_entry_value(entry, bytes, value_index) + .expect("namespace value"); + let name = api_set_value_entry_name(value, bytes).unwrap_or_default(); + let value_name = api_set_value_entry_value(value, bytes) + .unwrap_or_else(|| String::from("")); + std::println!( + " [{value_index}] name={} value={} flags={:#x}", + if name.is_empty() { "" } else { &name }, + value_name, + value.flags + ); + } + } + } + + fn dump_api_set_hash_entries(bytes: &[u8], namespace: ApiSetNamespace) { + std::println!("hash entries:"); + for index in 0..namespace.count { + let entry = api_set_namespace_hash_entry(namespace, bytes, index).expect("hash entry"); + std::println!( + " {index:04} hash={:#010x} index={}", + entry.hash, + entry.index + ); + } + } + + fn dump_api_set_namespace(api_set_map: ApiSetNamespace, bytes: &[u8], label: &str) { + std::println!("{label}"); + std::println!("API_SET_NAMESPACE len={:#x}", bytes.len(),); + std::println!(" version: {:#010x}", api_set_map.version); + std::println!(" size: {:#010x}", api_set_map.size); + std::println!(" flags: {:#010x}", api_set_map.flags); + std::println!(" count: {:#010x}", api_set_map.count); + std::println!("entry_offset: {:#010x}", api_set_map.entry_offset); + std::println!(" hash_offset: {:#010x}", api_set_map.hash_offset); + std::println!(" hash_factor: {:#010x}", api_set_map.hash_factor); + std::println!(); + dump_api_set_entries(bytes, api_set_map); + std::println!(); + dump_api_set_hash_entries(bytes, api_set_map); + std::println!(); + } + + #[test] + fn dump_host_api_set_namespace() { + let host_bytes = host_api_set_namespace_bytes(); + let host = parse_api_set_namespace(&host_bytes).expect("valid host API_SET_NAMESPACE"); + dump_api_set_namespace(host, &host_bytes, "host"); + } + + #[test] + fn api_set_namespace_matches_host_invariants() { + let host_bytes = host_api_set_namespace_bytes(); + let host = parse_api_set_namespace(&host_bytes).expect("valid host API_SET_NAMESPACE"); + let synthetic_bytes = + build_api_set_namespace(API_SET_MAPPINGS).expect("LiteBox API_SET_NAMESPACE builds"); + let synthetic = + parse_api_set_namespace(&synthetic_bytes).expect("valid synthetic API_SET_NAMESPACE"); + + assert_eq!(synthetic.version, host.version); + assert_eq!(synthetic.hash_factor, host.hash_factor); + assert_eq!(synthetic.flags, 0); + assert_api_set_hash_table(&host_bytes); + assert_api_set_hash_table(&synthetic_bytes); + + let mut host_checked = 0; + let mut host_mismatches = Vec::new(); + for &(contract, expected_host) in API_SET_MAPPINGS { + let synthetic_host = api_set_default_value(&synthetic_bytes, contract); + assert_eq!( + synthetic_host.as_deref(), + Some(expected_host), + "synthetic mapping for {contract}" + ); + if let Some(host_value) = api_set_default_value(&host_bytes, contract) { + if !host_value.eq_ignore_ascii_case(expected_host) { + host_mismatches.push(std::format!( + "{contract}: expected {expected_host}, got {host_value}" + )); + } + host_checked += 1; + } + } + assert!( + host_mismatches.is_empty(), + "host API-set mapping mismatches:\n{}", + host_mismatches.join("\n") + ); + assert!( + host_checked >= 3, + "expected at least three synthetic API-set contracts on the host, found {host_checked}" + ); + + for (contract, expected_host) in [ + ("api-ms-win-core-rtlsupport-l1-1-0", "ntdll.dll"), + ("api-ms-win-core-file-l1-2-3", "kernelbase.dll"), + ("api-ms-win-eventing-consumer-l1-1-0", "sechost.dll"), + ] { + assert_eq!( + api_set_default_value(&synthetic_bytes, contract).as_deref(), + Some(expected_host), + "synthetic mapping for {contract}" + ); + } + } + #[test] fn prints_created_teb_host_diff() { - let created = created_process_environment_snapshot(); - let host_teb = host_teb_snapshot(); - let host_teb_address = host_teb_address(); - let host_peb_address = host_peb_address(); + let synthetic = created_process_environment_snapshot(); + let current_teb = host_teb_snapshot(); + let teb_self = host_teb_address(); + let peb_address = host_peb_address(); - assert_eq!(created.teb.nt_tib.self_pointer, created.environment.teb); - assert_eq!(host_teb.nt_tib.self_pointer, host_teb_address); + assert_eq!(synthetic.teb.nt_tib.self_pointer, synthetic.environment.teb); + assert_eq!(current_teb.nt_tib.self_pointer, teb_self); assert_eq!( - created.teb.process_environment_block, - created.environment.peb + synthetic.teb.process_environment_block, + synthetic.environment.peb ); - assert_eq!(host_teb.process_environment_block, host_peb_address); - assert_eq!(host_teb.client_id, host_client_id()); + assert_eq!(current_teb.process_environment_block, peb_address); + assert_eq!(current_teb.client_id, host_client_id()); print_diff_header("synthetic TEB vs host TEB"); print_diff_fields!( "TEB.NtTib", - created.teb.nt_tib, - host_teb.nt_tib, + synthetic.teb.nt_tib, + current_teb.nt_tib, [ exception_list, stack_base, @@ -1054,8 +2074,8 @@ mod tests { ); print_diff_fields!( "TEB", - created.teb, - host_teb, + synthetic.teb, + current_teb, [ environment_pointer, client_id, @@ -1180,20 +2200,8 @@ mod tests { fn prints_created_peb_host_diff() { let created = created_process_environment_snapshot(); let host_peb = host_peb_snapshot(); - let base_static_server_data: usize = read_guest_value( - created.peb.read_only_static_server_data - + BASESRV_SERVERDLL_INDEX * core::mem::size_of::(), - ); assert_eq!(created.peb.image_base_address, created.image_base_address); - assert_eq!( - created.peb.read_only_static_server_data, - created.peb.read_only_shared_memory_base + WINDOWS_STATIC_SERVER_DATA_TABLE_OFFSET - ); - assert_eq!( - base_static_server_data, - created.peb.read_only_shared_memory_base + WINDOWS_BASE_STATIC_SERVER_DATA_OFFSET - ); assert_ne!(host_peb.image_base_address, 0); print_diff_header("synthetic PEB vs host PEB"); @@ -1317,6 +2325,29 @@ mod tests { ); } + #[test] + fn process_parameters_include_argv_and_environment() { + let created = created_process_environment_snapshot(); + + // `RTL_USER_PROCESS_PARAMETERS.CommandLine` stores the original command line for + // `CommandLineToArgvW`, and the environment is a sorted UTF-16 `name=value\0...\0\0` + // block as documented for `GetEnvironmentStringsW`. + assert_eq!( + decode_guest_unicode_string(created.process_parameters.command_line), + "test.exe \"arg with space\" \"quote\\\"arg\"" + ); + assert_ne!(created.process_parameters.environment, 0); + assert_eq!( + read_guest_utf16_units( + created.process_parameters.environment, + usize::try_from(created.process_parameters.environment_size) + .expect("environment size fits usize") + / size_of::(), + ), + utf16_environment_units(&["a=one", "B=two", "c=three"]) + ); + } + #[test] fn ntdll_exports_finds_ki_user_inverted_function_table() { let ntdll = ntdll_module_base(); @@ -1388,6 +2419,7 @@ mod tests { environment: WindowsProcessEnvironment, peb: ProcessEnvironmentBlock, teb: ThreadEnvironmentBlock, + process_parameters: RtlUserProcessParameters, image_base_address: usize, } @@ -1400,24 +2432,58 @@ mod tests { let image = loaded_module_image(application_module_base()); let image_base_address = image.mapping.base_addr; + let argv = [ + CString::new("test.exe").expect("valid argv[0]"), + CString::new("arg with space").expect("valid argv[1]"), + CString::new("quote\"arg").expect("valid argv[2]"), + ]; + let envp = [ + CString::new("c=three").expect("valid envp[0]"), + CString::new("B=two").expect("valid envp[1]"), + CString::new("a=one").expect("valid envp[2]"), + ]; let environment = loader - .create_process_environment( - &image.parsed, + .create_process_environment(ProcessEnvironmentInput { + image: &image.parsed, image_base_address, - "test.exe", - TEST_STACK_BASE, - TEST_STACK_TOP, - ) + image_path: "test.exe", + argv: &argv, + envp: &envp, + stack_base: TEST_STACK_BASE, + stack_allocation_top: TEST_STACK_TOP, + }) .expect("failed to create synthetic Windows process environment"); + let peb = read_guest_value::(environment.peb); CreatedProcessEnvironmentSnapshot { - peb: read_guest_value(environment.peb), + process_parameters: read_guest_value(peb.process_parameters), + peb, teb: read_guest_value(environment.teb), environment, image_base_address, } } + fn read_guest_utf16_units(address: usize, units: usize) -> Vec { + let ptr = + ::RawConstPointer::::from_usize( + address, + ); + ptr.to_owned_slice(units) + .expect("guest UTF-16 block is readable") + .to_vec() + } + + fn utf16_environment_units(vars: &[&str]) -> Vec { + let mut units = Vec::new(); + for var in vars { + units.extend(var.encode_utf16()); + units.push(0); + } + units.push(0); + units + } + fn print_field_diff(field: &str, synthetic: T, host: T) where T: core::fmt::Debug + IntoBytes + zerocopy::Immutable, @@ -1585,7 +2651,7 @@ mod tests { } unsafe fn read_host_value(address: *const T) -> T { - // SAFETY: The caller guarantees `address` points into a live host PEB/TEB object. + // SAFETY: The caller guarantees `address` points into live host loader/PEB/TEB state. unsafe { core::ptr::read_volatile(address) } } @@ -1647,7 +2713,10 @@ mod tests { mapping: MappingInfo { base_addr, image_size: parsed.image_size(), - entry_point: base_addr, + mapping_size: parsed.image_size(), + entry_point: base_addr + .checked_add(parsed.entry_point_rva()) + .expect("module entry point address fits usize"), }, pages: RangeMap::new(), parsed, From 308d266be4aa560bcf78ce3966b97c02333cd187 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 17 Jun 2026 10:17:23 -0700 Subject: [PATCH 033/319] Support Windows NtQueryInformationProcess (#924) --- litebox_shim_windows/src/lib.rs | 20 + litebox_shim_windows/src/loader/pe.rs | 3 +- litebox_shim_windows/src/syscalls/mod.rs | 15 + litebox_shim_windows/src/syscalls/process.rs | 383 +++++++++++++++++++ litebox_shim_windows/src/tests.rs | 2 + 5 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/process.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 9d2c95db19..5386417883 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -332,6 +332,8 @@ impl WindowsShim { system_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), user_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), user_ui_language: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), + default_hard_error_mode: AtomicU32::new(0), + cookie: syscalls::process::default_process_cookie(), exit_code: AtomicI32::new(DEFAULT_PROCESS_EXIT_CODE), }); Ok(LoadedProgram { @@ -373,6 +375,8 @@ pub struct Process { system_lcid: AtomicU32, user_lcid: AtomicU32, user_ui_language: AtomicU32, + default_hard_error_mode: AtomicU32, + cookie: u32, exit_code: AtomicI32, } @@ -688,6 +692,22 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtQueryInformationProcess { + process_handle, + process_information_class, + process_information, + process_information_length, + return_length, + } => { + let status = self.sys_nt_query_information_process( + process_handle, + process_information_class, + process_information, + process_information_length, + return_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, source, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 7e68cf778d..7885d4f3ce 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -31,6 +31,7 @@ use crate::nt_types::{ ThreadEnvironmentBlock, UnicodeString, X64Context, }; use crate::syscalls::mm::{MemoryType, PageProtection}; +use crate::syscalls::process::{INITIAL_PROCESS_ID, INITIAL_THREAD_ID}; use crate::{MutPtr, ShimFS}; const NTDLL_WRITABLE_SECTIONS: &[&[u8]] = &[b".mrdata"]; @@ -56,8 +57,6 @@ const WINDOWS_HEAP_SEGMENT_COMMIT: u64 = 2 * PAGE_SIZE as u64; const WINDOWS_HEAP_DECOMMIT_TOTAL_FREE_THRESHOLD: u64 = 64 * 1024; const WINDOWS_HEAP_DECOMMIT_FREE_BLOCK_THRESHOLD: u64 = PAGE_SIZE as u64; const WINDOWS_NT_TIB_VERSION: usize = 30 << 8; -const INITIAL_PROCESS_ID: usize = 1; -const INITIAL_THREAD_ID: usize = 1; macro_rules! write_static_server_data_field { ($platform:ty, $base:expr, $field:ident, $value:expr $(,)?) => { diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index df467ea9e1..41667e0b3b 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod event; pub(crate) mod file; pub(crate) mod mm; pub(crate) mod nls; +pub(crate) mod process; pub(crate) mod registry; mod sysinfo; @@ -206,6 +207,13 @@ pub(crate) enum SyscallRequest { system_information_length: u32, return_length: Option>, }, + NtQueryInformationProcess { + process_handle: ProcessHandle, + process_information_class: u32, + process_information: Platform::RawMutPointer, + process_information_length: u32, + return_length: Option>, + }, NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag: u32, source: Platform::RawConstPointer, @@ -400,6 +408,13 @@ impl SyscallRequest { system_information_length, return_length:*, })), + NtSysno::NtQueryInformationProcess => Some(sys_req!(NtQueryInformationProcess { + process_handle: { ProcessHandle::from_raw }, + process_information_class, + process_information:*, + process_information_length, + return_length:*, + })), NtSysno::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter => Some( sys_req!(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, diff --git a/litebox_shim_windows/src/syscalls/process.rs b/litebox_shim_windows/src/syscalls/process.rs new file mode 100644 index 0000000000..b9eeafc9d5 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/process.rs @@ -0,0 +1,383 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::sync::atomic::Ordering; +use int_enum::IntEnum; +use litebox::platform::RawMutPointer as _; +use litebox::utils::TruncateExt; +use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::syscalls::ProcessHandle; +use crate::{MutPtr, ShimFS, ShimPlatform, Task}; + +const ACTIVE_PROCESS_EXIT_STATUS: i32 = 0x0000_0103; +const NORMAL_PROCESS_BASE_PRIORITY: i32 = 8; +pub(crate) const INITIAL_PROCESS_ID: usize = 1; +pub(crate) const INITIAL_THREAD_ID: usize = 1; +const GUEST_PARENT_PROCESS_ID: usize = 0; +const GUEST_PROCESS_AFFINITY_MASK: usize = 1; +const PROCESS_DEBUG_FLAGS_NO_DEBUGGER: u32 = 1; +const PROCESS_COOKIE: u32 = 0xdead_beef; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +enum ProcessInformationClass { + BasicInformation = 0, + DebugPort = 7, + DefaultHardErrorMode = 12, + Wow64Information = 26, + DebugFlags = 31, + TlsInformation = 35, + Cookie = 36, + ConsoleHostProcess = 49, + ImageInformation = 53, + SchedulerSharedData = 112, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessBasicInformation { + exit_status: i32, + _padding0: u32, + peb_base_address: usize, + affinity_mask: usize, + base_priority: i32, + _padding1: u32, + unique_process_id: usize, + inherited_from_unique_process_id: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessDefaultHardErrorMode { + default_hard_error_mode: u32, +} + +impl Task { + pub(crate) fn sys_nt_query_information_process( + &self, + process_handle: ProcessHandle, + process_information_class: u32, + process_information: MutPtr, + process_information_length: u32, + return_length: Option>, + ) -> NtStatus { + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + + let Ok(process_information_class) = + ProcessInformationClass::try_from(process_information_class) + else { + litebox_util_log::debug!( + process_information_class = process_information_class; + "Unsupported NtQueryInformationProcess class" + ); + return NtStatus::INVALID_INFO_CLASS; + }; + + let status = match process_information_class { + ProcessInformationClass::BasicInformation => Self::write_process_information( + process_information, + process_information_length, + return_length, + &self.process_basic_information(), + ), + ProcessInformationClass::DebugPort | ProcessInformationClass::Wow64Information => { + Self::write_process_information( + process_information, + process_information_length, + return_length, + &0usize, + ) + } + ProcessInformationClass::DebugFlags => Self::write_process_information( + process_information, + process_information_length, + return_length, + &PROCESS_DEBUG_FLAGS_NO_DEBUGGER, + ), + ProcessInformationClass::DefaultHardErrorMode => Self::write_process_information( + process_information, + process_information_length, + return_length, + &ProcessDefaultHardErrorMode { + default_hard_error_mode: self + .process + .default_hard_error_mode + .load(Ordering::Acquire), + }, + ), + ProcessInformationClass::Cookie => Self::write_process_information( + process_information, + process_information_length, + return_length, + &self.process.cookie, + ), + ProcessInformationClass::ConsoleHostProcess + | ProcessInformationClass::TlsInformation + | ProcessInformationClass::ImageInformation + | ProcessInformationClass::SchedulerSharedData => { + litebox_util_log::debug!( + process_information_class:? = process_information_class; + "Unsupported NtQueryInformationProcess class" + ); + NtStatus::INVALID_INFO_CLASS + } + }; + + if status == NtStatus::SUCCESS { + litebox_util_log::debug!( + process_information_class:? = process_information_class, + process_information_length = process_information_length; + "Handled NtQueryInformationProcess syscall" + ); + } + + status + } + + fn write_process_information( + process_information: MutPtr, + process_information_length: u32, + return_length: Option>, + information: &T, + ) -> NtStatus { + let required_len = size_of::().trunc(); + if process_information_length < required_len { + return NtStatus::INFO_LENGTH_MISMATCH; + } + if process_information + .write_slice_at_offset(0, information.as_bytes()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(return_length) = return_length + && return_length.write_at_offset(0, required_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::SUCCESS + } + + fn process_basic_information(&self) -> ProcessBasicInformation { + ProcessBasicInformation { + exit_status: ACTIVE_PROCESS_EXIT_STATUS, + _padding0: 0, + peb_base_address: self.process.peb_address, + affinity_mask: GUEST_PROCESS_AFFINITY_MASK, + base_priority: NORMAL_PROCESS_BASE_PRIORITY, + _padding1: 0, + unique_process_id: INITIAL_PROCESS_ID, + inherited_from_unique_process_id: GUEST_PARENT_PROCESS_ID, + } + } +} + +pub(crate) const fn default_process_cookie() -> u32 { + // TODO: use CrngProvider to generate a random cookie + PROCESS_COOKIE +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{mut_byte_ptr, mut_ptr, null_mut_ptr}; + use litebox::platform::ThreadProvider; + + const RETURN_LENGTH_SENTINEL: u32 = 0xaaaa_aaaa; + + type TestPlatform = crate::tests::TestPlatform; + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = crate::tests::test_platform(); + ::run_test_thread(f) + } + + #[test] + fn nt_query_information_process_validates_arguments() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut info = [0u8; size_of::()]; + let mut return_length = 0; + let basic_information_len: u32 = size_of::().trunc(); + + assert_eq!( + task.sys_nt_query_information_process( + ProcessHandle::CURRENT, + ProcessInformationClass::BasicInformation as u32, + mut_byte_ptr(&mut info), + basic_information_len - 1, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!( + return_length, 0, + "ReactOS sets ReturnLength only after the exact-size check for this class; a host Windows probe shows the same result" + ); + + assert_eq!( + task.sys_nt_query_information_process( + ProcessHandle::from_raw(0x1234), + ProcessInformationClass::BasicInformation as u32, + mut_byte_ptr(&mut info), + basic_information_len, + None, + ), + NtStatus::INVALID_HANDLE + ); + + assert_eq!( + task.sys_nt_query_information_process( + ProcessHandle::CURRENT, + 0xffff, + mut_byte_ptr(&mut info), + basic_information_len, + None, + ), + NtStatus::INVALID_INFO_CLASS + ); + + assert_eq!( + task.sys_nt_query_information_process( + ProcessHandle::CURRENT, + ProcessInformationClass::BasicInformation as u32, + null_mut_ptr::(), + basic_information_len, + None, + ), + NtStatus::ACCESS_VIOLATION + ); + + return_length = RETURN_LENGTH_SENTINEL; + assert_eq!( + task.sys_nt_query_information_process( + ProcessHandle::CURRENT, + ProcessInformationClass::BasicInformation as u32, + null_mut_ptr::(), + basic_information_len, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::ACCESS_VIOLATION + ); + assert_eq!( + return_length, RETURN_LENGTH_SENTINEL, + "a host Windows probe leaves ReturnLength unchanged when ProcessInformation faults" + ); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + mod host_fidelity { + use core::ffi::c_void; + + use super::*; + + #[link(name = "ntdll")] + unsafe extern "system" { + fn NtQueryInformationProcess( + process_handle: *mut c_void, + process_information_class: u32, + process_information: *mut c_void, + process_information_length: u32, + return_length: *mut u32, + ) -> i32; + } + + fn empty_basic_information() -> ProcessBasicInformation { + ProcessBasicInformation { + exit_status: 0, + _padding0: 0, + peb_base_address: 0, + affinity_mask: 0, + base_priority: 0, + _padding1: 0, + unique_process_id: 0, + inherited_from_unique_process_id: usize::MAX, + } + } + + fn host_nt_query_information_process( + process_information_class: ProcessInformationClass, + process_information: *mut c_void, + process_information_length: u32, + return_length: *mut u32, + ) -> NtStatus { + // SAFETY: The host ntdll call treats these as user-mode output pointers, probes them, + // and does not retain them. Tests pass either valid locals or null to observe NTSTATUS + // and output side effects. + let status = unsafe { + NtQueryInformationProcess( + usize::MAX as *mut c_void, + process_information_class as u32, + process_information, + process_information_length, + return_length, + ) + }; + NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) + } + + #[test] + fn nt_query_information_process_basic_length_mismatch_matches_host() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut host_info = empty_basic_information(); + let mut shim_info = empty_basic_information(); + let mut host_return_length = RETURN_LENGTH_SENTINEL; + let mut shim_return_length = RETURN_LENGTH_SENTINEL; + let basic_information_len: u32 = size_of::().trunc(); + let short_length = basic_information_len - 1; + + let host = host_nt_query_information_process( + ProcessInformationClass::BasicInformation, + core::ptr::addr_of_mut!(host_info).cast::(), + short_length, + core::ptr::addr_of_mut!(host_return_length), + ); + let shim = task.sys_nt_query_information_process( + ProcessHandle::CURRENT, + ProcessInformationClass::BasicInformation as u32, + mut_byte_ptr(&mut shim_info), + short_length, + Some(mut_ptr(&mut shim_return_length)), + ); + + assert_eq!(shim, host); + assert_eq!(shim_return_length, host_return_length); + assert_eq!(shim_info.peb_base_address, 0); + }); + } + + #[test] + fn nt_query_information_process_invalid_output_leaves_return_length_unchanged() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let mut host_return_length = RETURN_LENGTH_SENTINEL; + let mut shim_return_length = RETURN_LENGTH_SENTINEL; + let basic_information_len: u32 = size_of::().trunc(); + + let host = host_nt_query_information_process( + ProcessInformationClass::BasicInformation, + core::ptr::null_mut(), + basic_information_len, + core::ptr::addr_of_mut!(host_return_length), + ); + let shim = task.sys_nt_query_information_process( + ProcessHandle::CURRENT, + ProcessInformationClass::BasicInformation as u32, + null_mut_ptr::(), + basic_information_len, + Some(mut_ptr(&mut shim_return_length)), + ); + + assert_eq!(shim, host); + assert_eq!(shim_return_length, host_return_length); + }); + } + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index c28f34ff00..739fb8d93a 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -155,6 +155,8 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task Date: Wed, 17 Jun 2026 13:37:59 -0700 Subject: [PATCH 034/319] Initial broker design and implementation (#880) This PR implements the broker based on a new design and adds the end-to-end implementation for the broker to provide the support for non-blocking evenfd. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 9 + Cargo.lock | 52 ++ Cargo.toml | 14 +- dev_tests/src/ratchet.rs | 1 + litebox/Cargo.toml | 2 + litebox/src/broker/error.rs | 85 ++++ litebox/src/broker/mod.rs | 90 ++++ litebox/src/event/counter.rs | 191 ++++++++ litebox/src/event/mod.rs | 1 + litebox/src/lib.rs | 3 + litebox/src/litebox.rs | 40 ++ litebox_broker_core/Cargo.toml | 10 + litebox_broker_core/src/error.rs | 49 ++ litebox_broker_core/src/event.rs | 211 ++++++++ litebox_broker_core/src/identity.rs | 71 +++ litebox_broker_core/src/lib.rs | 122 +++++ litebox_broker_core/src/object.rs | 355 ++++++++++++++ litebox_broker_core/src/policy.rs | 247 ++++++++++ litebox_broker_host/Cargo.toml | 11 + litebox_broker_host/src/error.rs | 38 ++ litebox_broker_host/src/lib.rs | 460 ++++++++++++++++++ litebox_broker_local/Cargo.toml | 10 + litebox_broker_local/src/error.rs | 127 +++++ litebox_broker_local/src/event.rs | 101 ++++ litebox_broker_local/src/lib.rs | 281 +++++++++++ litebox_broker_protocol/Cargo.toml | 9 + litebox_broker_protocol/src/channel.rs | 86 ++++ litebox_broker_protocol/src/error.rs | 107 ++++ litebox_broker_protocol/src/event.rs | 167 +++++++ litebox_broker_protocol/src/lib.rs | 63 +++ litebox_broker_protocol/src/message.rs | 101 ++++ litebox_broker_protocol/src/object.rs | 62 +++ litebox_broker_protocol/src/wire.rs | 314 ++++++++++++ .../src/wire/core_message.rs | 59 +++ litebox_broker_protocol/src/wire/event.rs | 177 +++++++ litebox_broker_protocol/src/wire/primitive.rs | 113 +++++ litebox_broker_transport/Cargo.toml | 10 + litebox_broker_transport/src/lib.rs | 10 + litebox_broker_transport/src/unix_socket.rs | 378 ++++++++++++++ litebox_broker_userland/Cargo.toml | 21 + litebox_broker_userland/src/main.rs | 32 ++ .../tests/userland_broker.rs | 139 ++++++ litebox_common_linux/src/errno/mod.rs | 14 + litebox_platform_linux_userland/src/lib.rs | 69 +++ litebox_runner_linux_userland/Cargo.toml | 5 + litebox_runner_linux_userland/src/broker.rs | 67 +++ litebox_runner_linux_userland/src/lib.rs | 24 +- litebox_runner_linux_userland/tests/eventfd.c | 176 +++++++ litebox_runner_linux_userland/tests/run.rs | 213 ++++++++ litebox_shim_linux/src/lib.rs | 13 +- litebox_shim_linux/src/syscalls/epoll.rs | 18 +- litebox_shim_linux/src/syscalls/eventfd.rs | 332 ++++++++----- litebox_shim_linux/src/syscalls/file.rs | 2 +- 53 files changed, 5231 insertions(+), 131 deletions(-) create mode 100644 litebox/src/broker/error.rs create mode 100644 litebox/src/broker/mod.rs create mode 100644 litebox/src/event/counter.rs create mode 100644 litebox_broker_core/Cargo.toml create mode 100644 litebox_broker_core/src/error.rs create mode 100644 litebox_broker_core/src/event.rs create mode 100644 litebox_broker_core/src/identity.rs create mode 100644 litebox_broker_core/src/lib.rs create mode 100644 litebox_broker_core/src/object.rs create mode 100644 litebox_broker_core/src/policy.rs create mode 100644 litebox_broker_host/Cargo.toml create mode 100644 litebox_broker_host/src/error.rs create mode 100644 litebox_broker_host/src/lib.rs create mode 100644 litebox_broker_local/Cargo.toml create mode 100644 litebox_broker_local/src/error.rs create mode 100644 litebox_broker_local/src/event.rs create mode 100644 litebox_broker_local/src/lib.rs create mode 100644 litebox_broker_protocol/Cargo.toml create mode 100644 litebox_broker_protocol/src/channel.rs create mode 100644 litebox_broker_protocol/src/error.rs create mode 100644 litebox_broker_protocol/src/event.rs create mode 100644 litebox_broker_protocol/src/lib.rs create mode 100644 litebox_broker_protocol/src/message.rs create mode 100644 litebox_broker_protocol/src/object.rs create mode 100644 litebox_broker_protocol/src/wire.rs create mode 100644 litebox_broker_protocol/src/wire/core_message.rs create mode 100644 litebox_broker_protocol/src/wire/event.rs create mode 100644 litebox_broker_protocol/src/wire/primitive.rs create mode 100644 litebox_broker_transport/Cargo.toml create mode 100644 litebox_broker_transport/src/lib.rs create mode 100644 litebox_broker_transport/src/unix_socket.rs create mode 100644 litebox_broker_userland/Cargo.toml create mode 100644 litebox_broker_userland/src/main.rs create mode 100644 litebox_broker_userland/tests/userland_broker.rs create mode 100644 litebox_runner_linux_userland/src/broker.rs create mode 100644 litebox_runner_linux_userland/tests/eventfd.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdd2a5f151..5ee5fcc701 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,6 +230,13 @@ jobs: # - `litebox_platform_windows_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # + # - `litebox_broker_transport` is allowed to have `std` access, + # since it owns hosted concrete broker transport implementations, + # including the current Unix-domain-socket control channel. + # + # - `litebox_broker_userland` is allowed to have `std` access, + # since it is the hosted userland broker executable. + # # - `litebox_platform_lvbs` has a custom target (`no_std`), so it does # not work with the current no_std checker. # @@ -285,6 +292,8 @@ jobs: # can safely use std. find . -type f -name 'Cargo.toml' \ -not -path './Cargo.toml' \ + -not -path './litebox_broker_transport/Cargo.toml' \ + -not -path './litebox_broker_userland/Cargo.toml' \ -not -path './litebox_platform_linux_userland/Cargo.toml' \ -not -path './litebox_platform_windows_userland/Cargo.toml' \ -not -path './litebox_runner_linux_on_windows_userland/Cargo.toml' \ diff --git a/Cargo.lock b/Cargo.lock index 286b3b24ef..01fde33766 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1460,6 +1460,8 @@ dependencies = [ "buddy_system_allocator", "either", "hashbrown", + "litebox_broker_local", + "litebox_broker_protocol", "litebox_util_log", "rangemap", "ringbuf", @@ -1474,6 +1476,51 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "litebox_broker_core" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_host" +version = "0.1.0" +dependencies = [ + "litebox_broker_core", + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_local" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_protocol" +version = "0.1.0" + +[[package]] +name = "litebox_broker_transport" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_userland" +version = "0.1.0" +dependencies = [ + "clap", + "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", + "litebox_broker_protocol", + "litebox_broker_transport", +] + [[package]] name = "litebox_common_linux" version = "0.1.0" @@ -1647,6 +1694,11 @@ dependencies = [ "glob", "libc", "litebox", + "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", + "litebox_broker_protocol", + "litebox_broker_transport", "litebox_common_linux", "litebox_platform_linux_userland", "litebox_platform_multiplex", diff --git a/Cargo.toml b/Cargo.toml index e30b3904ac..6456081c01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,13 @@ [workspace] resolver = "2" members = [ - "litebox", + "litebox", + "litebox_broker_local", + "litebox_broker_core", + "litebox_broker_protocol", + "litebox_broker_host", + "litebox_broker_transport", + "litebox_broker_userland", "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", @@ -29,6 +35,12 @@ members = [ ] default-members = [ "litebox", + "litebox_broker_local", + "litebox_broker_core", + "litebox_broker_protocol", + "litebox_broker_host", + "litebox_broker_transport", + "litebox_broker_userland", "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index cd9d681397..09ff7ad2c8 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -34,6 +34,7 @@ fn ratchet_globals() -> Result<()> { ratchet( &[ ("dev_bench/", 1), + ("litebox_broker_core/", 1), ("litebox/", 9), ("litebox_platform_linux_kernel/", 6), ("litebox_platform_linux_userland/", 5), diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 44c0f35c72..6e8c3ecfe7 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -20,6 +20,8 @@ buddy_system_allocator = { version = "0.11.0", default-features = false, feature # Depend on (currently unreleased) slabmalloc `main`, which contains some fixes on top of `0.11.0` slabmalloc = { git = "https://github.com/gz/rust-slabmalloc.git", rev = "19480b2e82704210abafe575fb9699184c1be110" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } +litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } +litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.60.2", features = [ diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs new file mode 100644 index 0000000000..2ffff32896 --- /dev/null +++ b/litebox/src/broker/error.rs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox_broker_protocol::ErrorCode; + +use crate::event::{counter::EventCounterError, polling::TryOpError}; + +/// Error returned by the deployment-provided broker control path. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrokerControlError { + /// The broker control transport failed. + Transport, + /// The broker returned an operation error. + Broker(ErrorCode), + /// The broker returned a response shape that does not match the request. + UnexpectedResponse, +} + +/// Internal normalized error for broker-backed object adapters. +/// +/// This keeps protocol/control-channel failures separate from the public +/// object-specific API error exposed by each local-core facade. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum BrokerObjectError { + /// The deployment-provided broker control path failed. + Control, + /// The broker rejected the cached object handle, type, or rights. + InvalidObject, + /// The object operation would block in its current broker-side state. + WouldBlock, + /// The object or broker-side state cannot grow further. + ResourceExhausted, + /// The broker returned a response shape that does not match the request. + UnexpectedResponse, + /// The broker reported a non-recoverable or unsupported object error. + Internal, +} + +impl From for BrokerObjectError { + fn from(error: BrokerControlError) -> Self { + match error { + BrokerControlError::Transport => Self::Control, + BrokerControlError::Broker(error) => error.into(), + BrokerControlError::UnexpectedResponse => Self::UnexpectedResponse, + } + } +} + +impl From for BrokerObjectError { + fn from(error: ErrorCode) -> Self { + match error { + ErrorCode::InvalidRights + | ErrorCode::UnknownObject + | ErrorCode::WrongObjectType + | ErrorCode::StaleHandle => Self::InvalidObject, + ErrorCode::WouldBlock => Self::WouldBlock, + ErrorCode::ResourceExhausted => Self::ResourceExhausted, + _ => Self::Internal, + } + } +} + +pub(crate) fn map_broker_object_result( + result: Result, +) -> Result> { + match result { + Ok(value) => Ok(value), + Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), + Err(error) => Err(TryOpError::Other(error.into())), + } +} + +impl From for EventCounterError { + fn from(error: BrokerObjectError) -> Self { + match error { + BrokerObjectError::WouldBlock => Self::WouldBlock, + BrokerObjectError::ResourceExhausted => Self::ResourceExhausted, + BrokerObjectError::UnexpectedResponse => Self::UnexpectedResponse, + BrokerObjectError::Control + | BrokerObjectError::InvalidObject + | BrokerObjectError::Internal => Self::Io, + } + } +} diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs new file mode 100644 index 0000000000..e3edc541fe --- /dev/null +++ b/litebox/src/broker/mod.rs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::sync::Arc; + +use litebox_broker_local::{BrokerLocal, BrokerLocalError}; +use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; + +use crate::sync::{Mutex, RawSyncPrimitivesProvider}; + +pub(crate) mod error; +pub use error::BrokerControlError; + +/// Local-core access to the negotiated broker control channel. +/// +/// LiteBox owns broker-backed local objects and constructs broker protocol +/// requests. Deployment code owns endpoint selection and supplies the connected +/// transport behind this protocol-level boundary. +pub trait BrokerControl: Send + Sync { + /// Sends one active BrokerCore request and returns its response. + fn request( + &self, + request: CoreRequest, + ) -> core::result::Result; +} + +struct BrokerLocalControl { + local: Mutex>, +} + +impl BrokerLocalControl +where + Platform: RawSyncPrimitivesProvider, +{ + const fn new(local: BrokerLocal) -> Self { + Self { + local: Mutex::new(local), + } + } +} + +impl BrokerControl for BrokerLocalControl +where + Platform: RawSyncPrimitivesProvider, + T: LocalControlChannel + Send, +{ + fn request( + &self, + request: CoreRequest, + ) -> core::result::Result { + self.local + .lock() + .active_core_request(request) + .map_err(broker_control_error) + } +} + +fn broker_control_error(error: BrokerLocalError) -> BrokerControlError { + match error { + BrokerLocalError::Broker(error) => BrokerControlError::Broker(error), + BrokerLocalError::UnexpectedResponse(_) => BrokerControlError::UnexpectedResponse, + _ => BrokerControlError::Transport, + } +} + +pub(crate) fn control_from_local(local: BrokerLocal) -> Arc +where + Platform: RawSyncPrimitivesProvider, + T: LocalControlChannel + Send + 'static, +{ + Arc::new(BrokerLocalControl::::new(local)) +} + +pub(crate) struct BrokerState { + control: Option>, + _marker: core::marker::PhantomData, +} + +impl BrokerState { + pub(crate) fn new(control: Option>) -> Self { + Self { + control, + _marker: core::marker::PhantomData, + } + } + + pub(crate) fn control(&self) -> Option> { + self.control.clone() + } +} diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs new file mode 100644 index 0000000000..c6bbfe447a --- /dev/null +++ b/litebox/src/event/counter.rs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::sync::Arc; + +pub use litebox_broker_protocol::EventConsumeMode as EventCounterReadMode; +use litebox_broker_protocol::{ + AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, + CreateEventRequest, EventRequest, EventResponse, ObjectHandle, ReadinessState, + WaitEventRequest, WaitOutcome, +}; + +use crate::{ + LiteBox, + broker::{ + BrokerControl, + error::{BrokerObjectError, map_broker_object_result}, + }, + event::{ + Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, + wait::WaitContext, + }, + platform::TimeProvider, + sync::RawSyncPrimitivesProvider, +}; + +/// Errors returned by local-core event counters. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventCounterError { + /// The requested operation is invalid for this event counter. + InvalidInput, + /// The operation would block. + WouldBlock, + /// The event counter cannot accept more state. + ResourceExhausted, + /// The backing authority or transport failed. + Io, + /// The backing authority returned a response shape that does not match the request. + UnexpectedResponse, + /// No backing authority is available for this event counter. + Unavailable, +} + +/// A local-core event counter object. +pub struct EventCounter { + broker: Arc, + handle: ObjectHandle, + pollee: Pollee, + blocking_operations_supported: bool, +} + +impl EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + /// Creates a local-core event counter. + pub fn new(litebox: &LiteBox, initial_count: u64) -> Result { + let Some(broker) = litebox.broker_control() else { + return Err(EventCounterError::Unavailable); + }; + let response = broker + .request(CoreRequest::Event(EventRequest::Create( + CreateEventRequest::new(initial_count), + ))) + .map_err(BrokerObjectError::from) + .and_then(event_response_from_core) + .map_err(EventCounterError::from)?; + let EventResponse::Create(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse.into()); + }; + Ok(Self { + broker, + handle: response.handle, + pollee: Pollee::new(), + blocking_operations_supported: true, + }) + } + + /// Returns whether blocking reads and writes are supported. + pub fn supports_blocking_operations(&self) -> bool { + self.blocking_operations_supported + } + + /// Reads the event counter. + pub fn read( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + mode: EventCounterReadMode, + ) -> Result> { + self.pollee.wait(cx, nonblock, Events::IN, || { + let response = map_broker_object_result(self.consume(mode))?; + if response.readiness.write_ready { + self.pollee.notify_observers(Events::OUT); + } + Ok(response.value) + }) + } + + /// Writes readiness credits to the event counter. + pub fn write( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + value: u64, + ) -> Result> { + if value == u64::MAX { + return Err(TryOpError::Other(EventCounterError::InvalidInput)); + } + self.pollee.wait(cx, nonblock, Events::OUT, || { + let readiness = map_broker_object_result(self.add(value))?; + if value != 0 && readiness.read_ready { + self.pollee.notify_observers(Events::IN); + } + Ok(core::mem::size_of::()) + }) + } + + fn consume( + &self, + mode: EventCounterReadMode, + ) -> Result { + let response = self.request_event(EventRequest::Consume(ConsumeEventRequest::new( + self.handle, + mode, + )))?; + let EventResponse::Consume(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(response) + } + + fn add(&self, value: u64) -> Result { + let response = + self.request_event(EventRequest::Add(AddEventRequest::new(self.handle, value)))?; + let EventResponse::Add(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(response.readiness) + } + + fn readiness_state(&self) -> Result { + let response = + self.request_event(EventRequest::Wait(WaitEventRequest::new(self.handle)))?; + let EventResponse::Wait(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(match response.outcome { + WaitOutcome::Ready(readiness) | WaitOutcome::WouldBlock(readiness) => readiness, + _ => return Err(BrokerObjectError::UnexpectedResponse), + }) + } + + fn request_event(&self, request: EventRequest) -> Result { + self.broker + .request(CoreRequest::Event(request)) + .map_err(BrokerObjectError::from) + .and_then(event_response_from_core) + } +} + +impl IOPollable for EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { + self.pollee.register_observer(observer, mask); + } + + fn check_io_events(&self) -> Events { + let Ok(readiness) = self.readiness_state() else { + return Events::empty(); + }; + let mut events = Events::empty(); + if readiness.read_ready { + events |= Events::IN; + } + if readiness.write_ready { + events |= Events::OUT; + } + events + } +} + +fn event_response_from_core(response: CoreResponse) -> Result { + match response { + CoreResponse::Event(response) => Ok(response), + _ => Err(BrokerObjectError::UnexpectedResponse), + } +} diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index 24d5b68323..6089b6b08e 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -3,6 +3,7 @@ //! Events related functionality +pub mod counter; pub mod observer; pub mod polling; pub mod wait; diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index f3d80997a3..f01e028f94 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -39,3 +39,6 @@ mod utilities; // Public utilities that might be used in other LiteBox crates. pub mod utils; + +mod broker; +pub use broker::{BrokerControl, BrokerControlError}; diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 2fb209c225..35cca92393 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -5,7 +5,11 @@ use alloc::sync::Arc; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::LocalControlChannel; + use crate::{ + broker::{self, BrokerControl, BrokerState}, fd::Descriptors, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -30,6 +34,35 @@ impl LiteBox { /// If the `enforce_singleton_litebox_instance` compilation feature has been enabled, and more /// than one instance is made, will panic. pub fn new(platform: &'static Platform) -> Self { + Self::new_inner(platform, None) + } + + /// Create a new [`LiteBox`] instance with broker control installed. + pub fn new_with_broker_control( + platform: &'static Platform, + broker_control: Arc, + ) -> Self { + Self::new_inner(platform, Some(broker_control)) + } + + /// Create a new [`LiteBox`] instance with a negotiated broker-local control adapter installed. + pub fn new_with_broker_local( + platform: &'static Platform, + broker_local: BrokerLocal, + ) -> Self + where + T: LocalControlChannel + Send + 'static, + { + Self::new_inner( + platform, + Some(broker::control_from_local::(broker_local)), + ) + } + + fn new_inner( + platform: &'static Platform, + broker_control: Option>, + ) -> Self { // This check ensures that there is exactly one `LiteBox` instance in the process. // // LiteBox itself supports having multiple instances (and subsystems correctly make any @@ -65,6 +98,7 @@ impl LiteBox { crate::sync::lock_tracing::LockTracker::init(platform); let descriptors = RwLock::new(Descriptors::new_from_litebox_creation()); + let broker = BrokerState::new(broker_control); litebox_util_log::trace!("LiteBox instance initialized"); @@ -72,6 +106,7 @@ impl LiteBox { x: Arc::new(LiteBoxX { platform, descriptors, + broker, }), } } @@ -106,10 +141,15 @@ impl LiteBox { ) -> impl core::ops::DerefMut> + use<'_, Platform> { self.x.descriptors.write() } + + pub(crate) fn broker_control(&self) -> Option> { + self.x.broker.control() + } } /// The actual body of [`LiteBox`], containing any components that might be shared. pub(crate) struct LiteBoxX { pub(crate) platform: &'static Platform, descriptors: RwLock>, + broker: BrokerState, } diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml new file mode 100644 index 0000000000..9b98941474 --- /dev/null +++ b/litebox_broker_core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "litebox_broker_core" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs new file mode 100644 index 0000000000..8f9dceace7 --- /dev/null +++ b/litebox_broker_core/src/error.rs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +/// Broker authority error category. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum BrokerError { + /// Policy denied the operation. + PolicyDenied, + /// The referenced object does not exist. + UnknownObject, + /// The referenced object generation is stale. + StaleHandle, + /// The referenced object type does not match the operation. + WrongObjectType, + /// The caller lacks the required broker rights. + InvalidRights, + /// Broker-side resource exhaustion. + ResourceExhausted, + /// A broker core has already been created in this process. + BrokerCoreAlreadyExists, + /// The operation would block in the current object state. + WouldBlock, + /// The operation is not implemented by this BrokerCore. + UnsupportedOperation, + /// Policy returned a decision that does not match the authorized operation. + InvalidPolicyDecision, +} + +impl fmt::Display for BrokerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PolicyDenied => f.write_str("broker policy denied the operation"), + Self::UnknownObject => f.write_str("unknown broker object"), + Self::StaleHandle => f.write_str("stale broker handle"), + Self::WrongObjectType => f.write_str("wrong broker object type"), + Self::InvalidRights => f.write_str("invalid broker rights"), + Self::ResourceExhausted => f.write_str("broker resource exhausted"), + Self::BrokerCoreAlreadyExists => f.write_str("broker core already exists"), + Self::WouldBlock => f.write_str("broker operation would block"), + Self::UnsupportedOperation => f.write_str("unsupported broker operation"), + Self::InvalidPolicyDecision => f.write_str("invalid broker policy decision"), + } + } +} + +impl core::error::Error for BrokerError {} diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs new file mode 100644 index 0000000000..523489cea2 --- /dev/null +++ b/litebox_broker_core/src/event.rs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::object::{ObjectId, ObjectKind}; +use crate::{BrokerAssociation, BrokerCore, BrokerError, ObjectRights, ObjectType, Result}; +use litebox_broker_protocol::{ + EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, +}; + +const MAX_EVENT_COUNT: u64 = u64::MAX - 1; + +impl BrokerCore { + /// Creates a broker-owned event object. + pub fn create_event(&mut self, association: &BrokerAssociation) -> Result { + self.create_event_with_count(association, 0) + } + + /// Creates a broker-owned event object with initial readiness credits. + pub fn create_event_with_count( + &mut self, + association: &BrokerAssociation, + initial_count: u64, + ) -> Result { + if initial_count > MAX_EVENT_COUNT { + return Err(BrokerError::ResourceExhausted); + } + let rights = self.authorize_create_object(association, ObjectType::Event)?; + + self.insert_object_with_reference( + association, + ObjectKind::Event(EventObject::new(initial_count)), + ObjectType::Event, + rights, + ) + } + + /// Checks whether an event wait would complete now. + /// + /// Blocking is intentionally outside BrokerCore for the first proof of + /// concept. Userland or kernel deployments can block on deployment-specific + /// wait primitives after BrokerCore authorizes and reports readiness state. + pub fn wait_event( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + ) -> Result { + let authorized = + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + let state = Self::filter_readiness_for_rights( + self.event_state(authorized.object_id)?, + authorized.rights, + ); + Ok(if state.read_ready { + WaitOutcome::Ready(state) + } else { + WaitOutcome::WouldBlock(state) + }) + } + + /// Adds readiness credits to a broker-owned event object. + pub fn add_event( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + value: u64, + ) -> Result { + let authorized = + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WRITE)?; + match &mut self.object_mut(authorized.object_id)?.kind { + ObjectKind::Event(event) => event + .add(value) + .map(|state| Self::filter_readiness_for_rights(state, authorized.rights)), + } + } + + /// Consumes readiness credits from a broker-owned event object. + pub fn consume_event( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + mode: EventConsumeMode, + ) -> Result { + let authorized = + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + match &mut self.object_mut(authorized.object_id)?.kind { + ObjectKind::Event(event) => event.consume(mode).map(|response| { + EventConsumption::new( + response.value, + Self::filter_readiness_for_rights(response.readiness, authorized.rights), + ) + }), + } + } + + fn filter_readiness_for_rights(state: ReadinessState, rights: ObjectRights) -> ReadinessState { + ReadinessState::new( + rights.contains(ObjectRights::WAIT) && state.read_ready, + rights.contains(ObjectRights::WRITE) && state.write_ready, + state.generation, + ) + } + + fn event_state(&self, object_id: ObjectId) -> Result { + match &self.object(object_id)?.kind { + ObjectKind::Event(event) => Ok(event.readiness_state()), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct EventObject { + count: u64, + readiness_generation: u64, +} + +impl EventObject { + pub(crate) const fn new(count: u64) -> Self { + Self { + count, + readiness_generation: 0, + } + } + + pub(crate) const fn readiness_state(self) -> ReadinessState { + ReadinessState::new( + self.count > 0, + self.count < MAX_EVENT_COUNT, + self.readiness_generation, + ) + } + + fn add(&mut self, value: u64) -> Result { + let new_count = self + .count + .checked_add(value) + .filter(|count| *count <= MAX_EVENT_COUNT) + .ok_or(BrokerError::WouldBlock)?; + let next_generation = self.next_generation()?; + self.count = new_count; + self.readiness_generation = next_generation; + Ok(self.readiness_state()) + } + + fn consume(&mut self, mode: EventConsumeMode) -> Result { + if self.count == 0 { + return Err(BrokerError::WouldBlock); + } + + let value = match mode { + EventConsumeMode::All => self.count, + EventConsumeMode::One => 1, + _ => return Err(BrokerError::UnsupportedOperation), + }; + let next_generation = self.next_generation()?; + self.count -= value; + self.readiness_generation = next_generation; + Ok(EventConsumption::new(value, self.readiness_state())) + } + + fn next_generation(&self) -> Result { + self.readiness_generation + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn event_readiness_state_only_reports_authorized_directions() { + let readiness = ReadinessState::new(true, true, 7); + + assert_eq!( + BrokerCore::filter_readiness_for_rights(readiness, ObjectRights::WAIT), + ReadinessState::new(true, false, 7) + ); + assert_eq!( + BrokerCore::filter_readiness_for_rights(readiness, ObjectRights::WRITE), + ReadinessState::new(false, true, 7) + ); + } + + #[test] + fn add_event_does_not_mutate_count_when_generation_is_exhausted() { + let mut event = EventObject { + count: 1, + readiness_generation: u64::MAX, + }; + + assert_eq!(event.add(1), Err(BrokerError::ResourceExhausted)); + assert_eq!(event.count, 1); + assert_eq!(event.readiness_generation, u64::MAX); + } + + #[test] + fn consume_event_does_not_mutate_count_when_generation_is_exhausted() { + let mut event = EventObject { + count: 1, + readiness_generation: u64::MAX, + }; + + assert_eq!( + event.consume(EventConsumeMode::One), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(event.count, 1); + assert_eq!(event.readiness_generation, u64::MAX); + } +} diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs new file mode 100644 index 0000000000..5b26f26d06 --- /dev/null +++ b/litebox_broker_core/src/identity.rs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{BrokerCore, Result, allocate_id}; + +/// Caller identity information supplied by the broker entry layer. +/// +/// The first userland proof of concept does not authenticate Unix-socket peers, +/// but BrokerCore still accepts an explicit credential value so authenticated +/// servers or hosts can plumb identity through the same association-creation seam. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum CallerCredential { + /// Explicit deployment mode for the initial unauthenticated userland POC. + Unauthenticated, +} + +/// Broker-assigned guest process identity. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ProcessId(u64); + +impl ProcessId { + pub(crate) const fn new(raw: u64) -> Self { + Self(raw) + } +} + +/// Broker-owned authority token for one authenticated caller association. +/// +/// User mode does not choose this value. The broker entry layer authenticates +/// the caller, then BrokerCore assigns this identity for all operations received +/// on that association. +#[derive(Debug, PartialEq, Eq)] +pub struct BrokerAssociation { + /// Broker-assigned guest process identity. + process_id: ProcessId, + /// Broker-entry-authenticated caller credential for this association. + caller_credential: CallerCredential, +} + +impl BrokerAssociation { + /// Creates an authenticated association identity. + pub(crate) const fn new(process_id: ProcessId, caller_credential: CallerCredential) -> Self { + Self { + process_id, + caller_credential, + } + } + + pub(crate) const fn process_id(&self) -> ProcessId { + self.process_id + } + + /// Returns the broker-entry-authenticated caller credential for this association. + pub const fn caller_credential(&self) -> CallerCredential { + self.caller_credential + } +} + +impl BrokerCore { + /// Allocates broker authority state for one authenticated caller association. + pub fn create_association( + &mut self, + caller_credential: CallerCredential, + ) -> Result { + let process_id = allocate_id(&mut self.next_process_id)?; + let association = BrokerAssociation::new(ProcessId::new(process_id), caller_credential); + Ok(association) + } +} diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs new file mode 100644 index 0000000000..e1c54f5d58 --- /dev/null +++ b/litebox_broker_core/src/lib.rs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Broker authority core independent of protocol envelopes and channels. +//! +//! `litebox_broker_core` owns broker-side object identity, reference lifetime, +//! rights checks, reference generation checks, and policy calls. It may use +//! shared semantic DTOs from `litebox_broker_protocol` for values that both the +//! local core and broker understand, such as handles and readiness state. It +//! deliberately has no dependency on protocol envelopes, channel traits, wire +//! codecs, Unix sockets, shared-memory rings, kernel traps, or any other +//! channel implementation. + +#![no_std] + +extern crate alloc; +#[cfg(test)] +extern crate std; + +mod error; +mod event; +mod identity; +mod object; +mod policy; + +use alloc::collections::BTreeMap; +use core::sync::atomic::{AtomicBool, Ordering}; + +pub use error::BrokerError; +pub use identity::{BrokerAssociation, CallerCredential}; +use litebox_broker_protocol::ObjectReferenceId; +use object::{ObjectEntry, ObjectId, ObjectReference}; +pub use object::{ObjectRights, ObjectType}; +pub use policy::{ObjectOperation, PolicyDecision, PolicyEngine, PolicyOperation, PolicyProfile}; + +/// BrokerCore result type. +pub type Result = core::result::Result; + +/// Resource limits for broker-owned authority state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct BrokerCoreLimits { + /// Maximum live broker objects. + pub max_objects: usize, + /// Maximum live object references. + pub max_references: usize, +} + +impl BrokerCoreLimits { + /// Conservative default limits for initial broker deployments. + pub const DEFAULT: Self = Self { + max_objects: 4096, + max_references: 4096, + }; + + /// Creates a broker core limit set. + pub const fn new(max_objects: usize, max_references: usize) -> Self { + Self { + max_objects, + max_references, + } + } +} + +impl Default for BrokerCoreLimits { + fn default() -> Self { + Self::DEFAULT + } +} + +/// Channel-independent broker authority state. +/// +/// A broker process may construct only one broker core for its process +/// lifetime. Constructors return [`BrokerError::BrokerCoreAlreadyExists`] if a +/// core has already been constructed. +pub struct BrokerCore { + policy: PolicyEngine, + limits: BrokerCoreLimits, + next_process_id: u64, + next_object_id: u64, + next_reference_id: u64, + objects: BTreeMap, + references: BTreeMap, +} + +static BROKER_CORE_CREATED: AtomicBool = AtomicBool::new(false); + +impl BrokerCore { + /// Creates the broker core with the provided policy engine. + pub fn new(policy: PolicyEngine) -> Result { + Self::new_with_limits(policy, BrokerCoreLimits::DEFAULT) + } + + /// Creates the broker core with explicit authority-state limits. + pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Result { + BROKER_CORE_CREATED + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .map_err(|_| BrokerError::BrokerCoreAlreadyExists)?; + + Ok(Self { + policy, + limits, + next_process_id: 1, + next_object_id: 1, + next_reference_id: 1, + objects: BTreeMap::new(), + references: BTreeMap::new(), + }) + } +} + +const EXHAUSTED_ID: u64 = 0; + +fn allocate_id(next_id: &mut u64) -> Result { + if *next_id == EXHAUSTED_ID { + return Err(BrokerError::ResourceExhausted); + } + + let id = *next_id; + *next_id = id.checked_add(1).unwrap_or(EXHAUSTED_ID); + Ok(id) +} diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs new file mode 100644 index 0000000000..06cca10ffc --- /dev/null +++ b/litebox_broker_core/src/object.rs @@ -0,0 +1,355 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::ops::BitOr; + +use crate::event::EventObject; +use crate::identity::{BrokerAssociation, ProcessId}; +use crate::{BrokerCore, BrokerError, PolicyDecision, PolicyOperation, Result, allocate_id}; +use litebox_broker_protocol::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; + +/// Broker object type known to the authority core and policy engine. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ObjectType { + /// Broker-owned event object. + Event, +} + +/// Broker rights attached to an object reference. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct ObjectRights(u32); + +impl ObjectRights { + /// Empty rights set. + pub const NONE: Self = Self(0); + /// Right to wait for readiness. + pub const WAIT: Self = Self(1 << 0); + /// Right to mutate object state, such as adding event readiness credits. + pub const WRITE: Self = Self(1 << 1); + + /// Returns true when no rights are present. + pub const fn is_empty(self) -> bool { + self.0 == 0 + } + + /// Returns true when all `required` rights are present. + pub const fn contains(self, required: Self) -> bool { + (self.0 & required.0) == required.0 + } + + /// Returns the union of two rights sets. + #[must_use] + pub const fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } +} + +impl BitOr for ObjectRights { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + self.union(rhs) + } +} + +/// Broker-owned object identifier. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ObjectId(u64); + +impl ObjectId { + /// Creates an object identifier from its raw value. + const fn new(raw: u64) -> Self { + Self(raw) + } +} + +const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGeneration::new(1); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ObjectReference { + pub(crate) object_id: ObjectId, + pub(crate) reference_generation: ObjectReferenceGeneration, + pub(crate) owner: ProcessId, + pub(crate) object_type: ObjectType, + pub(crate) rights: ObjectRights, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ObjectEntry { + pub(crate) kind: ObjectKind, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ObjectKind { + Event(EventObject), +} + +impl ObjectKind { + pub(crate) const fn object_type(self) -> ObjectType { + match self { + Self::Event(_) => ObjectType::Event, + } + } +} + +impl BrokerCore { + /// Inserts a broker object and mints its first owned reference. + /// + /// The current POC never reuses reference slots, so the reference + /// generation starts at the authority-owned first generation. Any future + /// reference-slot reuse path must bump the generation before reissuing a + /// slot so stale handles cannot validate against a recycled reference. + pub(crate) fn insert_object_with_reference( + &mut self, + association: &BrokerAssociation, + kind: ObjectKind, + object_type: ObjectType, + rights: ObjectRights, + ) -> Result { + if self.objects.len() >= self.limits.max_objects + || self.references.len() >= self.limits.max_references + { + return Err(BrokerError::ResourceExhausted); + } + + let object_id = self.allocate_object_id()?; + let reference_id = self.allocate_reference_id()?; + let reference_generation = FIRST_REFERENCE_GENERATION; + + self.objects.insert(object_id, ObjectEntry { kind }); + self.references.insert( + reference_id, + ObjectReference { + object_id, + reference_generation, + owner: association.process_id(), + object_type, + rights, + }, + ); + + Ok(ObjectHandle::new(reference_id, reference_generation)) + } + + pub(crate) fn authorize_create_object( + &mut self, + association: &BrokerAssociation, + object_type: ObjectType, + ) -> Result { + match self.policy.authorize(PolicyOperation::create_object( + association.caller_credential(), + object_type, + ))? { + PolicyDecision::GrantObjectReference { rights } => Ok(rights), + _ => Err(BrokerError::InvalidPolicyDecision), + } + } + + pub(crate) fn authorize_use_object( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + object_type: ObjectType, + rights: ObjectRights, + ) -> Result { + let reference = self.validate_handle(association, handle, object_type, rights)?; + let object_id = reference.object_id; + let reference_rights = reference.rights; + match self.policy.authorize(PolicyOperation::use_object( + association.caller_credential(), + object_type, + rights, + ))? { + PolicyDecision::Authorized => Ok(AuthorizedObject { + object_id, + rights: reference_rights, + }), + _ => Err(BrokerError::InvalidPolicyDecision), + } + } + + pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { + self.objects + .get(&object_id) + .ok_or(BrokerError::UnknownObject) + } + + pub(crate) fn object_mut(&mut self, object_id: ObjectId) -> Result<&mut ObjectEntry> { + self.objects + .get_mut(&object_id) + .ok_or(BrokerError::UnknownObject) + } + + fn validate_handle( + &self, + association: &BrokerAssociation, + handle: ObjectHandle, + expected_type: ObjectType, + required_rights: ObjectRights, + ) -> Result { + let reference = self.reference_for_handle(association, handle)?; + if reference.object_type != expected_type { + return Err(BrokerError::WrongObjectType); + } + if !reference.rights.contains(required_rights) { + return Err(BrokerError::InvalidRights); + } + + let object = self + .objects + .get(&reference.object_id) + .ok_or(BrokerError::UnknownObject)?; + if object.kind.object_type() != expected_type { + return Err(BrokerError::WrongObjectType); + } + + Ok(*reference) + } + + fn allocate_object_id(&mut self) -> Result { + allocate_id(&mut self.next_object_id).map(ObjectId::new) + } + + fn allocate_reference_id(&mut self) -> Result { + allocate_id(&mut self.next_reference_id).map(ObjectReferenceId::new) + } +} + +impl BrokerCore { + /// Closes one object reference owned by an association. + /// + /// The underlying object is released when this was the last live reference. + pub fn close_object_reference( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + ) -> Result<()> { + let object_id = self.reference_for_handle(association, handle)?.object_id; + if !self.objects.contains_key(&object_id) { + return Err(BrokerError::UnknownObject); + } + + self.references.remove(&handle.reference_id); + self.drop_object_if_unreferenced(object_id); + Ok(()) + } + + /// Closes a broker association and releases references owned by it. + pub fn close_association(&mut self, association: BrokerAssociation) { + let process_id = association.process_id(); + self.references + .retain(|_, reference| reference.owner != process_id); + let references = &self.references; + self.objects.retain(|object_id, _| { + references + .values() + .any(|reference| reference.object_id == *object_id) + }); + } + + fn reference_for_handle( + &self, + association: &BrokerAssociation, + handle: ObjectHandle, + ) -> Result<&ObjectReference> { + let reference = self + .references + .get(&handle.reference_id) + .ok_or(BrokerError::UnknownObject)?; + if reference.owner != association.process_id() { + return Err(BrokerError::UnknownObject); + } + if reference.reference_generation != handle.reference_generation { + return Err(BrokerError::StaleHandle); + } + Ok(reference) + } + + fn drop_object_if_unreferenced(&mut self, object_id: ObjectId) { + if !self + .references + .values() + .any(|reference| reference.object_id == object_id) + { + self.objects.remove(&object_id); + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct AuthorizedObject { + pub(crate) object_id: ObjectId, + pub(crate) rights: ObjectRights, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{BrokerError, CallerCredential, PolicyEngine}; + use litebox_broker_protocol::WaitOutcome; + + #[test] + fn allocator_issues_max_id_then_exhausts() { + let mut next_id = u64::MAX; + + assert_eq!(allocate_id(&mut next_id), Ok(u64::MAX)); + assert_eq!(next_id, 0); + assert_eq!( + allocate_id(&mut next_id), + Err(BrokerError::ResourceExhausted) + ); + } + + #[test] + fn object_reference_lifecycle_uses_public_core_constructor_once() { + let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); + let owner = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let other = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let handle = core.create_event(&owner).unwrap(); + + assert_eq!( + core.close_object_reference(&other, handle), + Err(BrokerError::UnknownObject) + ); + + let stale = ObjectHandle::new( + handle.reference_id, + ObjectReferenceGeneration::new(handle.reference_generation.get() + 1), + ); + assert_eq!( + core.close_object_reference(&owner, stale), + Err(BrokerError::StaleHandle) + ); + assert!(matches!( + core.wait_event(&owner, handle), + Ok(WaitOutcome::WouldBlock(_)) + )); + + assert_eq!(core.close_object_reference(&owner, handle), Ok(())); + assert!(core.references.is_empty()); + assert!(core.objects.is_empty()); + assert_eq!( + core.close_object_reference(&owner, handle), + Err(BrokerError::UnknownObject) + ); + + let association = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let _handle = core.create_event(&association).unwrap(); + assert_eq!(core.references.len(), 1); + assert_eq!(core.objects.len(), 1); + + core.close_association(association); + + assert!(core.references.is_empty()); + assert!(core.objects.is_empty()); + } +} diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs new file mode 100644 index 0000000000..1c2ecdaeba --- /dev/null +++ b/litebox_broker_core/src/policy.rs @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{BrokerError, CallerCredential, ObjectRights, ObjectType}; + +/// Broker operation submitted to the policy engine. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PolicyOperation { + /// Perform an operation on a broker-owned object type. + Object { + /// Broker-entry-authenticated credential for the caller. + caller_credential: CallerCredential, + /// Object type targeted by the operation. + object_type: ObjectType, + /// Operation requested for the object type. + operation: ObjectOperation, + }, +} + +/// Generic object operation submitted to the policy engine. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ObjectOperation { + /// Create a new broker-owned object. + Create, + /// Use an existing object handle with the requested rights. + Use { rights: ObjectRights }, +} + +/// Policy decision returned after authorizing a broker operation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PolicyDecision { + /// Operation is authorized and does not grant new authority material. + Authorized, + /// Object creation is authorized with rights for the initial object reference. + GrantObjectReference { + /// Rights to attach to the newly minted object reference. + rights: ObjectRights, + }, +} + +impl PolicyOperation { + /// Creates a policy operation for creating a broker-owned object type. + pub const fn create_object( + caller_credential: CallerCredential, + object_type: ObjectType, + ) -> Self { + Self::Object { + caller_credential, + object_type, + operation: ObjectOperation::Create, + } + } + + /// Creates a policy operation for using a broker-owned object with rights. + pub const fn use_object( + caller_credential: CallerCredential, + object_type: ObjectType, + rights: ObjectRights, + ) -> Self { + Self::Object { + caller_credential, + object_type, + operation: ObjectOperation::Use { rights }, + } + } +} + +/// Configured broker policy. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PolicyProfile { + /// Deny every operation. + DefaultDeny, + /// Allow the current event-object surface. + EventOnly { + /// Rights to attach to newly created event references. + event_reference_rights: ObjectRights, + /// Maximum event rights this policy may authorize for use requests. + event_use_rights: ObjectRights, + }, +} + +/// Broker policy decision and audit component. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyEngine { + profile: PolicyProfile, +} + +impl PolicyEngine { + /// Creates a policy engine from a policy profile. + pub const fn new(profile: PolicyProfile) -> Self { + Self { profile } + } + + /// Creates a policy engine that denies every operation. + pub const fn default_deny() -> Self { + Self::new(PolicyProfile::DefaultDeny) + } + + /// Creates a policy engine that allows only the current event-object surface. + pub const fn event_only() -> Self { + Self::event_only_with_reference_rights(EVENT_REFERENCE_RIGHTS) + } + + /// Creates an event-only policy engine with explicit initial reference rights. + /// + /// Use authorization still allows the normal event-only rights; BrokerCore's + /// reference validation enforces the rights on each created reference. + pub const fn event_only_with_reference_rights(event_reference_rights: ObjectRights) -> Self { + Self::new(PolicyProfile::EventOnly { + event_reference_rights, + event_use_rights: EVENT_REFERENCE_RIGHTS, + }) + } + + /// Authorizes or denies a broker operation. + pub(crate) fn authorize( + &mut self, + operation: PolicyOperation, + ) -> Result { + match self.profile { + PolicyProfile::DefaultDeny => Err(BrokerError::PolicyDenied), + PolicyProfile::EventOnly { + event_reference_rights, + event_use_rights, + } => authorize_event_only(event_reference_rights, event_use_rights, operation), + } + } +} + +impl Default for PolicyEngine { + fn default() -> Self { + Self::default_deny() + } +} + +/// Policy profile that allows only the current event-object surface. +/// +/// The default event create operation grants `WAIT | WRITE` on the initial +/// reference. Use requests may ask for any non-empty subset of configured event +/// use rights; BrokerCore separately enforces each reference's actual rights. +const EVENT_REFERENCE_RIGHTS: ObjectRights = ObjectRights::WAIT.union(ObjectRights::WRITE); + +fn authorize_event_only( + event_reference_rights: ObjectRights, + event_use_rights: ObjectRights, + operation: PolicyOperation, +) -> Result { + match operation { + PolicyOperation::Object { + caller_credential: CallerCredential::Unauthenticated, + object_type: ObjectType::Event, + operation: ObjectOperation::Create, + } => Ok(PolicyDecision::GrantObjectReference { + rights: event_reference_rights, + }), + PolicyOperation::Object { + caller_credential: CallerCredential::Unauthenticated, + object_type: ObjectType::Event, + operation: ObjectOperation::Use { rights }, + } if !rights.is_empty() && event_use_rights.contains(rights) => { + Ok(PolicyDecision::Authorized) + } + PolicyOperation::Object { + object_type: ObjectType::Event, + .. + } => Err(BrokerError::PolicyDenied), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn event_only_policy_allows_only_current_event_surface() { + let mut policy = PolicyEngine::event_only(); + + assert_eq!( + policy.authorize(PolicyOperation::create_object( + CallerCredential::Unauthenticated, + ObjectType::Event + )), + Ok(PolicyDecision::GrantObjectReference { + rights: ObjectRights::WAIT | ObjectRights::WRITE + }) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WAIT + )), + Ok(PolicyDecision::Authorized) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WRITE + )), + Ok(PolicyDecision::Authorized) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WAIT | ObjectRights::WRITE + )), + Ok(PolicyDecision::Authorized) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::NONE + )), + Err(BrokerError::PolicyDenied) + ); + } + + #[test] + fn explicit_event_reference_rights_do_not_narrow_event_use_policy() { + let mut policy = PolicyEngine::event_only_with_reference_rights(ObjectRights::WAIT); + + assert_eq!( + policy.authorize(PolicyOperation::create_object( + CallerCredential::Unauthenticated, + ObjectType::Event + )), + Ok(PolicyDecision::GrantObjectReference { + rights: ObjectRights::WAIT + }) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WRITE + )), + Ok(PolicyDecision::Authorized) + ); + } +} diff --git a/litebox_broker_host/Cargo.toml b/litebox_broker_host/Cargo.toml new file mode 100644 index 0000000000..fdeb2b609b --- /dev/null +++ b/litebox_broker_host/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "litebox_broker_host" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_host/src/error.rs b/litebox_broker_host/src/error.rs new file mode 100644 index 0000000000..81a34d6462 --- /dev/null +++ b/litebox_broker_host/src/error.rs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +/// Errors returned by a broker-host receive/send loop. +#[derive(Debug)] +#[non_exhaustive] +pub enum BrokerHostError { + /// The host could not authenticate the peer or allocate broker association state. + AssociationSetup, + /// The concrete channel failed. + Channel(E), +} + +impl fmt::Display for BrokerHostError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AssociationSetup => f.write_str("broker association setup failed"), + Self::Channel(error) => write!(f, "broker channel failed: {error}"), + } + } +} + +impl core::error::Error for BrokerHostError +where + E: core::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::AssociationSetup => None, + Self::Channel(error) => Some(error), + } + } +} + +/// Broker-host receive/send loop result type. +pub type Result = core::result::Result>; diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs new file mode 100644 index 0000000000..e0cc97d7fc --- /dev/null +++ b/litebox_broker_host/src/lib.rs @@ -0,0 +1,460 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Channel-neutral broker-side protocol/core adapter. +//! +//! This crate wires `litebox_broker_core` to any implementation of the neutral +//! host-side control-channel trait. Concrete channels live in separate crates such as +//! `litebox_broker_transport`. + +#![no_std] + +#[cfg(test)] +extern crate std; + +use core::fmt; + +use litebox_broker_core::{BrokerAssociation, BrokerCore, BrokerError, CallerCredential}; +use litebox_broker_protocol::{ + AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, + CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, + INITIAL_PROTOCOL_VERSION, PeerCredential, ProtocolVersion, ReceivedBrokerRequest, + WaitEventResponse, +}; + +mod error; + +pub use error::{BrokerHostError, Result}; + +/// Protocol version this broker host implementation supports. +pub const HOST_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; + +/// Serves one broker connection over the provided connected control channel. +pub fn serve_connection( + core: &mut BrokerCore, + channel: &mut T, +) -> Result +where + T: HostControlChannel, +{ + let peer_credential = channel + .peer_credential() + .map_err(BrokerHostError::Channel)?; + let caller_credential = caller_credential_from_peer(peer_credential) + .map_err(|()| BrokerHostError::AssociationSetup)?; + let association = core + .create_association(caller_credential) + .map_err(|_error| BrokerHostError::AssociationSetup)?; + + let result = serve_request_loop(core, channel, &association); + core.close_association(association); + result +} + +fn serve_request_loop( + core: &mut BrokerCore, + channel: &mut T, + association: &BrokerAssociation, +) -> Result +where + T: HostControlChannel, +{ + let mut state = ConnectionState::AwaitingNegotiation; + loop { + let Some(received) = channel.recv_request().map_err(BrokerHostError::Channel)? else { + break; + }; + + let dispatch = handle_received_request(core, association, &mut state, received); + channel + .send_response(&dispatch.response) + .map_err(BrokerHostError::Channel)?; + if let DispatchOutcome::Close(reason) = dispatch.outcome { + return Ok(ConnectionTermination::BrokerClosed(reason)); + } + } + + Ok(ConnectionTermination::PeerClosed) +} + +fn caller_credential_from_peer( + peer_credential: PeerCredential, +) -> core::result::Result { + if peer_credential == PeerCredential::Unauthenticated { + Ok(CallerCredential::Unauthenticated) + } else { + Err(()) + } +} + +fn handle_received_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + state: &mut ConnectionState, + received: ReceivedBrokerRequest, +) -> BrokerDispatch { + match received { + ReceivedBrokerRequest::Request(request) => { + handle_request(core, association, state, request) + } + _ => handle_unknown_request(*state), + } +} + +fn handle_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + state: &mut ConnectionState, + request: BrokerRequest, +) -> BrokerDispatch { + match *state { + ConnectionState::AwaitingNegotiation => match request { + BrokerRequest::Negotiate { protocol_version } => { + negotiate_version(state, protocol_version) + } + _ => BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ), + }, + ConnectionState::Active { + negotiated_protocol_version, + } => handle_active_request(core, association, negotiated_protocol_version, request), + } +} + +fn handle_active_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + _negotiated_protocol_version: ProtocolVersion, + request: BrokerRequest, +) -> BrokerDispatch { + match request { + BrokerRequest::Negotiate { .. } => BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ), + BrokerRequest::Core(request) => { + BrokerDispatch::continue_after(handle_core_request(core, association, request)) + } + _ => BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)), + } +} + +fn handle_core_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + request: CoreRequest, +) -> BrokerResponse { + match request { + CoreRequest::Event(request) => handle_event_request(core, association, request), + _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), + } +} + +fn handle_event_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + request: EventRequest, +) -> BrokerResponse { + match request { + EventRequest::Create(request) => handle_core_result( + core.create_event_with_count(association, request.initial_count), + |handle| event_response(EventResponse::Create(CreateEventResponse::new(handle))), + ), + EventRequest::Wait(request) => { + handle_core_result(core.wait_event(association, request.handle), |outcome| { + event_response(EventResponse::Wait(WaitEventResponse::new(outcome))) + }) + } + EventRequest::Add(request) => handle_core_result( + core.add_event(association, request.handle, request.value), + |readiness| event_response(EventResponse::Add(AddEventResponse::new(readiness))), + ), + EventRequest::Consume(request) => handle_core_result( + core.consume_event(association, request.handle, request.mode), + |consumption| event_response(EventResponse::Consume(consumption)), + ), + _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), + } +} + +fn handle_unknown_request(state: ConnectionState) -> BrokerDispatch { + if state == ConnectionState::AwaitingNegotiation { + BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ) + } else { + BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)) + } +} + +fn negotiate_version( + state: &mut ConnectionState, + protocol_version: ProtocolVersion, +) -> BrokerDispatch { + if protocol_version.is_supported_by(HOST_PROTOCOL_VERSION) { + *state = ConnectionState::Active { + negotiated_protocol_version: protocol_version, + }; + BrokerDispatch::continue_after(BrokerResponse::Negotiated { + broker_protocol_version: HOST_PROTOCOL_VERSION, + }) + } else { + BrokerDispatch::continue_after(BrokerResponse::VersionMismatch { + broker_protocol_version: HOST_PROTOCOL_VERSION, + }) + } +} + +fn handle_core_result( + result: litebox_broker_core::Result, + into_response: impl FnOnce(T) -> BrokerResponse, +) -> BrokerResponse { + match result { + Ok(value) => into_response(value), + Err(error) => BrokerResponse::Error(to_protocol_error(error)), + } +} + +const fn event_response(response: EventResponse) -> BrokerResponse { + BrokerResponse::Core(CoreResponse::Event(response)) +} + +fn to_protocol_error(error: BrokerError) -> ErrorCode { + match error { + BrokerError::PolicyDenied => ErrorCode::PolicyDenied, + BrokerError::UnknownObject => ErrorCode::UnknownObject, + BrokerError::StaleHandle => ErrorCode::StaleHandle, + BrokerError::WrongObjectType => ErrorCode::WrongObjectType, + BrokerError::InvalidRights => ErrorCode::InvalidRights, + BrokerError::ResourceExhausted => ErrorCode::ResourceExhausted, + BrokerError::WouldBlock => ErrorCode::WouldBlock, + BrokerError::UnsupportedOperation => ErrorCode::UnsupportedOperation, + _ => ErrorCode::Internal, + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConnectionState { + AwaitingNegotiation, + Active { + negotiated_protocol_version: ProtocolVersion, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct BrokerDispatch { + response: BrokerResponse, + outcome: DispatchOutcome, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DispatchOutcome { + Continue, + Close(CloseReason), +} + +impl BrokerDispatch { + const fn continue_after(response: BrokerResponse) -> Self { + Self { + response, + outcome: DispatchOutcome::Continue, + } + } + + const fn close_after(response: BrokerResponse, reason: CloseReason) -> Self { + Self { + response, + outcome: DispatchOutcome::Close(reason), + } + } +} + +/// Reason the broker host closed the connection after sending a response. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CloseReason { + /// The peer violated the request sequencing state machine. + ProtocolViolation, +} + +impl fmt::Display for CloseReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ProtocolViolation => f.write_str("protocol violation"), + } + } +} + +/// Terminal outcome for a successfully served broker connection. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConnectionTermination { + /// The peer cleanly closed the channel. + PeerClosed, + /// The host sent a terminal protocol response and closed the connection. + BrokerClosed(CloseReason), +} + +#[cfg(test)] +mod tests { + use super::*; + use litebox_broker_core::PolicyEngine; + use litebox_broker_protocol::CreateEventRequest; + + #[test] + fn host_request_handling_uses_one_broker_core() { + let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); + + serve_connection_negotiates_routes_one_request_and_returns_peer_closed(&mut core); + serve_connection_closes_after_protocol_violation(&mut core); + serve_connection_returns_channel_error_when_response_send_fails(&mut core); + } + + fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed( + core: &mut BrokerCore, + ) { + let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ + Ok(Some(ReceivedBrokerRequest::Request( + BrokerRequest::Negotiate { + protocol_version: HOST_PROTOCOL_VERSION, + }, + ))), + Ok(Some(ReceivedBrokerRequest::Request(event_create_request( + 0, + )))), + Ok(None), + ])); + + assert_eq!( + serve_connection(core, &mut channel).unwrap(), + ConnectionTermination::PeerClosed + ); + assert_eq!( + channel.responses[0], + BrokerResponse::Negotiated { + broker_protocol_version: HOST_PROTOCOL_VERSION + } + ); + let handle = match &channel.responses[1] { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { + response.handle + } + response => panic!("unexpected response: {response:?}"), + }; + assert_ne!(handle.reference_id.get(), 0); + } + + fn serve_connection_closes_after_protocol_violation(core: &mut BrokerCore) { + let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ + Ok(Some(ReceivedBrokerRequest::Request(event_create_request( + 0, + )))), + Ok(Some(ReceivedBrokerRequest::Request( + BrokerRequest::Negotiate { + protocol_version: HOST_PROTOCOL_VERSION, + }, + ))), + ])); + + assert_eq!( + serve_connection(core, &mut channel).unwrap(), + ConnectionTermination::BrokerClosed(CloseReason::ProtocolViolation) + ); + assert_eq!( + channel.responses, + [BrokerResponse::Error(ErrorCode::ProtocolState)] + ); + assert_eq!(channel.requests.len(), 1); + } + + fn serve_connection_returns_channel_error_when_response_send_fails(core: &mut BrokerCore) { + let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([Ok(Some( + ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { + protocol_version: HOST_PROTOCOL_VERSION, + }), + ))])); + channel.send_error = Some(FakeChannelError::Send); + + match serve_connection(core, &mut channel) { + Err(BrokerHostError::Channel(FakeChannelError::Send)) => {} + result => panic!("unexpected serve result: {result:?}"), + } + assert!(channel.responses.is_empty()); + } + + const fn event_request(request: EventRequest) -> BrokerRequest { + BrokerRequest::Core(CoreRequest::Event(request)) + } + + const fn event_create_request(initial_count: u64) -> BrokerRequest { + event_request(EventRequest::Create(CreateEventRequest::new(initial_count))) + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum FakeChannelError { + Send, + } + + impl fmt::Display for FakeChannelError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Send => f.write_str("fake send error"), + } + } + } + + impl core::error::Error for FakeChannelError {} + + struct FakeHostControlChannel { + requests: + std::vec::Vec, FakeChannelError>>, + responses: std::vec::Vec, + send_error: Option, + } + + impl FakeHostControlChannel { + fn new( + requests: std::vec::Vec< + core::result::Result, FakeChannelError>, + >, + ) -> Self { + Self { + requests, + responses: std::vec::Vec::new(), + send_error: None, + } + } + } + + impl HostControlChannel for FakeHostControlChannel { + type Error = FakeChannelError; + + fn peer_credential(&self) -> core::result::Result { + Ok(PeerCredential::Unauthenticated) + } + + fn recv_request( + &mut self, + ) -> core::result::Result, Self::Error> { + if self.requests.is_empty() { + Ok(None) + } else { + self.requests.remove(0) + } + } + + fn send_response( + &mut self, + response: &BrokerResponse, + ) -> core::result::Result<(), Self::Error> { + if let Some(error) = self.send_error { + return Err(error); + } + self.responses.push(response.clone()); + Ok(()) + } + } +} diff --git a/litebox_broker_local/Cargo.toml b/litebox_broker_local/Cargo.toml new file mode 100644 index 0000000000..d3574f9258 --- /dev/null +++ b/litebox_broker_local/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "litebox_broker_local" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs new file mode 100644 index 0000000000..45696b20d2 --- /dev/null +++ b/litebox_broker_local/src/error.rs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +use litebox_broker_protocol::{BrokerResponse, ErrorCode, ProtocolVersion}; + +/// Errors returned by the broker-local control adapter. +#[derive(Debug)] +#[non_exhaustive] +pub enum BrokerLocalError { + /// The control channel failed. + Channel(E), + /// An operation requiring an active broker session was called before negotiation. + NotNegotiated, + /// Negotiation was requested after the local adapter was already active. + AlreadyNegotiated, + /// The broker closed the channel before returning a response. + ChannelClosed, + /// The broker returned a response this local adapter does not understand. + UnknownResponse, + /// The broker accepted negotiation with a version that cannot serve the request. + IncompatibleNegotiation { + /// Protocol version requested by this local adapter. + requested: ProtocolVersion, + /// Protocol version advertised by the broker. + broker_protocol_version: ProtocolVersion, + }, + /// This local adapter cannot speak the requested protocol version. + UnsupportedLocalVersion { + /// Protocol version requested by the caller. + requested: ProtocolVersion, + /// Protocol version supported by this local implementation. + local_protocol_version: ProtocolVersion, + }, + /// The active broker session cannot serve an operation requiring a newer version. + UnsupportedNegotiatedVersion { + /// Protocol version required by the operation. + required: ProtocolVersion, + /// Effective protocol version negotiated for this connection. + negotiated_protocol_version: ProtocolVersion, + }, + /// The broker does not support the requested protocol version. + UnsupportedVersion { + /// Protocol version requested by this local adapter. + requested: ProtocolVersion, + /// Protocol version advertised by the broker. + broker_protocol_version: ProtocolVersion, + }, + /// The broker rejected the request. + Broker(ErrorCode), + /// The broker returned a response type that does not match the request. + UnexpectedResponse(BrokerResponse), +} + +impl fmt::Display for BrokerLocalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Channel(error) => write!(f, "broker channel failed: {error}"), + Self::NotNegotiated => { + write!( + f, + "broker local adapter has not negotiated protocol version" + ) + } + Self::AlreadyNegotiated => f.write_str("broker local adapter already negotiated"), + Self::ChannelClosed => write!(f, "broker closed the channel"), + Self::UnknownResponse => f.write_str("unknown broker response"), + Self::IncompatibleNegotiation { + requested, + broker_protocol_version, + } => write!( + f, + "broker accepted incompatible protocol negotiation: requested {requested:?}, broker supports {broker_protocol_version:?}" + ), + Self::UnsupportedLocalVersion { + requested, + local_protocol_version, + } => write!( + f, + "broker local adapter cannot request protocol version {requested:?}; local adapter supports {local_protocol_version:?}" + ), + Self::UnsupportedNegotiatedVersion { + required, + negotiated_protocol_version, + } => write!( + f, + "broker session protocol version {negotiated_protocol_version:?} does not support required version {required:?}" + ), + Self::UnsupportedVersion { + requested, + broker_protocol_version, + } => write!( + f, + "broker does not support requested protocol version {requested:?}; broker supports {broker_protocol_version:?}" + ), + Self::Broker(error) => write!(f, "broker rejected request: {error}"), + Self::UnexpectedResponse(response) => { + write!(f, "broker returned unexpected response: {response:?}") + } + } + } +} + +impl core::error::Error for BrokerLocalError +where + E: core::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Channel(error) => Some(error), + Self::Broker(error) => Some(error), + Self::NotNegotiated + | Self::AlreadyNegotiated + | Self::ChannelClosed + | Self::UnknownResponse + | Self::IncompatibleNegotiation { .. } + | Self::UnsupportedLocalVersion { .. } + | Self::UnsupportedNegotiatedVersion { .. } + | Self::UnsupportedVersion { .. } + | Self::UnexpectedResponse(_) => None, + } + } +} + +/// Broker-local control adapter result type. +pub type Result = core::result::Result>; diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs new file mode 100644 index 0000000000..7c07501c5d --- /dev/null +++ b/litebox_broker_local/src/event.rs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox_broker_protocol::{ + AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, + CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, + INITIAL_PROTOCOL_VERSION, LocalControlChannel, ObjectHandle, ProtocolVersion, ReadinessState, + WaitEventRequest, WaitOutcome, +}; + +use crate::{BrokerLocal, BrokerLocalError, Result}; + +const EVENT_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; + +impl BrokerLocal { + /// Creates a broker-owned event object. + pub fn create_event(&mut self) -> Result { + self.create_event_with_count(0) + } + + /// Creates a broker-owned event object with initial readiness credits. + pub fn create_event_with_count( + &mut self, + initial_count: u64, + ) -> Result { + self.ensure_event_protocol()?; + match self.request(event_request(EventRequest::Create( + CreateEventRequest::new(initial_count), + )))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { + Ok(response.handle) + } + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + + /// Checks whether an event wait would complete now. + pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { + self.ensure_event_protocol()?; + match self.request(event_request(EventRequest::Wait(WaitEventRequest::new( + handle, + ))))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response))) => { + Ok(response.outcome) + } + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + + /// Adds readiness credits to a broker-owned event object. + pub fn add_event( + &mut self, + handle: ObjectHandle, + value: u64, + ) -> Result { + self.ensure_event_protocol()?; + match self.request(event_request(EventRequest::Add(AddEventRequest::new( + handle, value, + ))))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(response))) => { + Ok(response.readiness) + } + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + + /// Consumes readiness credits from a broker-owned event object. + pub fn consume_event( + &mut self, + handle: ObjectHandle, + mode: EventConsumeMode, + ) -> Result { + self.ensure_event_protocol()?; + match self.request(event_request(EventRequest::Consume( + ConsumeEventRequest::new(handle, mode), + )))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(response))) => { + Ok(response) + } + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } +} + +const fn event_request(request: EventRequest) -> BrokerRequest { + BrokerRequest::Core(CoreRequest::Event(request)) +} + +impl BrokerLocal { + fn ensure_event_protocol(&self) -> Result<(), T::Error> { + let negotiated = self.ensure_negotiated()?; + if EVENT_PROTOCOL_VERSION.is_supported_by(negotiated) { + Ok(()) + } else { + Err(BrokerLocalError::UnsupportedNegotiatedVersion { + required: EVENT_PROTOCOL_VERSION, + negotiated_protocol_version: negotiated, + }) + } + } +} diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs new file mode 100644 index 0000000000..610cd65128 --- /dev/null +++ b/litebox_broker_local/src/lib.rs @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Typed broker-local control adapter for broker requests. +//! +//! The local control adapter owns request/response sequencing but does not own a channel. +//! Userland, kernel, or ring-buffer deployments can provide channels by +//! implementing [`litebox_broker_protocol::LocalControlChannel`]. + +#![no_std] + +#[cfg(test)] +extern crate std; + +mod error; +mod event; + +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, INITIAL_PROTOCOL_VERSION, + LocalControlChannel, ProtocolVersion, ReceivedBrokerResponse, +}; + +pub use error::{BrokerLocalError, Result}; + +/// Protocol version this broker-local implementation requests by default. +pub const LOCAL_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; + +/// Typed broker-local control adapter for broker operations. +pub struct BrokerLocal { + channel: T, + state: ConnectionState, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConnectionState { + AwaitingNegotiation, + Active { + negotiated_protocol_version: ProtocolVersion, + }, +} + +impl BrokerLocal { + /// Creates a broker-local control adapter over an already-connected control channel. + pub const fn new(channel: T) -> Self { + Self { + channel, + state: ConnectionState::AwaitingNegotiation, + } + } + + /// Returns the underlying control channel for deployment-specific configuration. + pub fn control_channel_mut(&mut self) -> &mut T { + &mut self.channel + } +} + +impl BrokerLocal { + /// Negotiates the default broker-local protocol version. + /// + /// Returns the effective protocol version this connection will speak. + pub fn negotiate(&mut self) -> Result { + self.negotiate_version(LOCAL_PROTOCOL_VERSION) + } + + /// Negotiates a caller-selected protocol version. + /// + /// Returns the effective protocol version this connection will speak. Feature + /// gating must use this effective version, not the broker's max-supported + /// version returned by the wire negotiation response. + pub fn negotiate_version( + &mut self, + protocol_version: ProtocolVersion, + ) -> Result { + if self.state != ConnectionState::AwaitingNegotiation { + return Err(BrokerLocalError::AlreadyNegotiated); + } + if !protocol_version.is_supported_by(LOCAL_PROTOCOL_VERSION) { + return Err(BrokerLocalError::UnsupportedLocalVersion { + requested: protocol_version, + local_protocol_version: LOCAL_PROTOCOL_VERSION, + }); + } + + let response = self.request(BrokerRequest::Negotiate { protocol_version })?; + match response { + BrokerResponse::Negotiated { + broker_protocol_version, + } => { + if !protocol_version.is_supported_by(broker_protocol_version) { + return Err(BrokerLocalError::IncompatibleNegotiation { + requested: protocol_version, + broker_protocol_version, + }); + } + self.state = ConnectionState::Active { + negotiated_protocol_version: protocol_version, + }; + Ok(protocol_version) + } + BrokerResponse::VersionMismatch { + broker_protocol_version, + } => Err(BrokerLocalError::UnsupportedVersion { + requested: protocol_version, + broker_protocol_version, + }), + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + + /// Returns the effective protocol version this connection negotiated. + /// + /// Feature gating must use this effective version because the broker may + /// support a newer minor version than this local adapter requested. + pub fn negotiated_protocol_version(&self) -> Option { + match self.state { + ConnectionState::AwaitingNegotiation => None, + ConnectionState::Active { + negotiated_protocol_version, + } => Some(negotiated_protocol_version), + } + } + + pub(crate) fn ensure_negotiated(&self) -> Result { + match self.state { + ConnectionState::AwaitingNegotiation => Err(BrokerLocalError::NotNegotiated), + ConnectionState::Active { + negotiated_protocol_version, + } => Ok(negotiated_protocol_version), + } + } + + pub(crate) fn request(&mut self, request: BrokerRequest) -> Result { + match self.raw_request(request)? { + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response => Ok(response), + } + } + + fn raw_request(&mut self, request: BrokerRequest) -> Result { + self.channel + .send_request(&request) + .map_err(BrokerLocalError::Channel)?; + match self + .channel + .recv_response() + .map_err(BrokerLocalError::Channel)? + .ok_or(BrokerLocalError::ChannelClosed)? + { + ReceivedBrokerResponse::Response(response) => Ok(response), + _ => Err(BrokerLocalError::UnknownResponse), + } + } + + /// Sends one BrokerCore request on an active connection. + pub fn active_core_request(&mut self, request: CoreRequest) -> Result { + self.ensure_negotiated()?; + match self.request(BrokerRequest::Core(request))? { + BrokerResponse::Core(response) => Ok(response), + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::convert::Infallible; + use litebox_broker_protocol::ProtocolVersion; + + #[test] + fn event_operations_require_negotiation_without_sending() { + let channel = FakeControlChannel::new(None); + let mut local = BrokerLocal::new(channel); + + assert!(matches!( + local.create_event(), + Err(BrokerLocalError::NotNegotiated) + )); + assert_eq!(local.channel.sent_request, None); + } + + #[test] + fn negotiate_sends_default_version_and_activates_local_connection() { + let requested = LOCAL_PROTOCOL_VERSION; + let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { + broker_protocol_version: LOCAL_PROTOCOL_VERSION, + })); + let mut local = BrokerLocal::new(channel); + + assert_eq!(local.negotiate().unwrap(), requested); + assert_eq!( + local.channel.sent_request, + Some(BrokerRequest::Negotiate { + protocol_version: requested + }) + ); + assert_eq!(local.negotiated_protocol_version(), Some(requested)); + } + + #[test] + fn negotiate_version_rejects_locally_unsupported_version_without_sending() { + let too_new = ProtocolVersion::new( + LOCAL_PROTOCOL_VERSION.major, + LOCAL_PROTOCOL_VERSION.minor + 1, + ); + let channel = FakeControlChannel::new(None); + let mut local = BrokerLocal::new(channel); + + assert!(matches!( + local.negotiate_version(too_new), + Err(BrokerLocalError::UnsupportedLocalVersion { + requested, + local_protocol_version + }) if requested == too_new && local_protocol_version == LOCAL_PROTOCOL_VERSION + )); + assert_eq!(local.negotiated_protocol_version(), None); + assert_eq!(local.channel.sent_request, None); + } + + #[test] + fn active_core_request_wraps_request_and_unwraps_response() { + use litebox_broker_protocol::{ + CoreRequest, CoreResponse, EventRequest, EventResponse, ObjectHandle, + ObjectReferenceGeneration, ObjectReferenceId, ReadinessState, WaitEventRequest, + WaitEventResponse, WaitOutcome, + }; + + let handle = + ObjectHandle::new(ObjectReferenceId::new(7), ObjectReferenceGeneration::new(1)); + let request = CoreRequest::Event(EventRequest::Wait(WaitEventRequest::new(handle))); + let response = CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)), + ))); + let channel = FakeControlChannel::new(Some(BrokerResponse::Core(response.clone()))); + let mut local = BrokerLocal::new(channel); + local.state = ConnectionState::Active { + negotiated_protocol_version: LOCAL_PROTOCOL_VERSION, + }; + + assert_eq!( + local.active_core_request(request.clone()).unwrap(), + response + ); + assert_eq!( + local.channel.sent_request, + Some(BrokerRequest::Core(request)) + ); + } + + struct FakeControlChannel { + sent_request: Option, + response: Option, + } + + impl FakeControlChannel { + const fn new(response: Option) -> Self { + Self { + sent_request: None, + response, + } + } + } + + impl LocalControlChannel for FakeControlChannel { + type Error = Infallible; + + fn send_request( + &mut self, + request: &BrokerRequest, + ) -> core::result::Result<(), Self::Error> { + self.sent_request = Some(request.clone()); + Ok(()) + } + + fn recv_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(self.response.take().map(ReceivedBrokerResponse::Response)) + } + } +} diff --git a/litebox_broker_protocol/Cargo.toml b/litebox_broker_protocol/Cargo.toml new file mode 100644 index 0000000000..2fccbca4ee --- /dev/null +++ b/litebox_broker_protocol/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "litebox_broker_protocol" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[lints] +workspace = true diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs new file mode 100644 index 0000000000..83f4d848df --- /dev/null +++ b/litebox_broker_protocol/src/channel.rs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{BrokerRequest, BrokerResponse}; + +/// Peer identity information supplied by the channel or host layer. +/// +/// The first userland proof of concept does not authenticate Unix-socket peers, +/// but channels still return an explicit credential value so the host layer +/// can map authenticated peer identity into BrokerCore caller identity. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum PeerCredential { + /// Explicit deployment mode for the initial unauthenticated userland POC. + /// + /// Channels that are expected to authenticate peers must return an error + /// from [`HostControlChannel::peer_credential`] when authentication is + /// unavailable or fails; this variant is only for deployments that + /// deliberately choose unauthenticated operation. + Unauthenticated, +} + +/// Broker authority request received from a control channel. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ReceivedBrokerRequest { + /// A request understood by the current protocol crate. + Request(BrokerRequest), + /// A request emitted by a newer peer and not understood by this process. + Unknown, +} + +impl From for ReceivedBrokerRequest { + fn from(request: BrokerRequest) -> Self { + Self::Request(request) + } +} + +/// Broker authority response received from a control channel. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ReceivedBrokerResponse { + /// A response understood by the current protocol crate. + Response(BrokerResponse), + /// A response emitted by a newer broker and not understood by this process. + Unknown, +} + +impl From for ReceivedBrokerResponse { + fn from(response: BrokerResponse) -> Self { + Self::Response(response) + } +} + +/// Local-side control channel for broker authority calls. +pub trait LocalControlChannel { + /// Channel-specific error type. + type Error; + + /// Sends one broker request. + fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error>; + + /// Receives one broker response. + /// + /// Returns `Ok(None)` when the broker closed the channel cleanly before + /// starting another response frame. + fn recv_response(&mut self) -> Result, Self::Error>; +} + +/// Host-side control channel for broker authority calls. +pub trait HostControlChannel { + /// Channel-specific error type. + type Error; + + /// Returns the peer credential authenticated for this channel endpoint. + fn peer_credential(&self) -> Result; + + /// Receives one broker request. + /// + /// Returns `Ok(None)` when the peer closed the channel cleanly before + /// starting another request frame. + fn recv_request(&mut self) -> Result, Self::Error>; + + /// Sends one broker response. + fn send_response(&mut self, response: &BrokerResponse) -> Result<(), Self::Error>; +} diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs new file mode 100644 index 0000000000..e2113225ff --- /dev/null +++ b/litebox_broker_protocol/src/error.rs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +/// ABI-neutral broker error category. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ErrorCode { + /// The requested protocol version is unsupported. + UnsupportedVersion, + /// The request is structurally invalid. + MalformedRequest, + /// The request is validly encoded but violates the connection state machine. + ProtocolState, + /// The request is unsupported by this broker protocol implementation. + UnsupportedOperation, + /// Broker hit an internal condition or an error category this protocol cannot represent. + Internal, + /// Policy denied the operation. + PolicyDenied, + /// The referenced object does not exist. + UnknownObject, + /// The referenced object generation is stale. + StaleHandle, + /// The referenced object type does not match the operation. + WrongObjectType, + /// The caller lacks the required broker rights. + InvalidRights, + /// Broker-side resource exhaustion. + ResourceExhausted, + /// The operation would block in the current event state. + WouldBlock, + /// Error code emitted by a newer broker and not understood by this local peer. + /// + /// This variant is reserved for raw codes not assigned by this protocol + /// version. + Unknown(u16), +} + +impl ErrorCode { + /// Raw error values are part of the broker wire ABI; do not renumber + /// assigned values. + /// + /// Values `0` and `1` remain unassigned so null/default-looking values never + /// represent concrete broker errors. + /// + /// Converts a raw protocol error code to an error category. + pub const fn from_raw(raw: u16) -> Self { + match raw { + 2 => Self::UnsupportedVersion, + 3 => Self::MalformedRequest, + 10 => Self::ProtocolState, + 11 => Self::UnsupportedOperation, + 12 => Self::Internal, + 4 => Self::PolicyDenied, + 5 => Self::UnknownObject, + 6 => Self::StaleHandle, + 7 => Self::WrongObjectType, + 8 => Self::InvalidRights, + 9 => Self::ResourceExhausted, + 13 => Self::WouldBlock, + raw => Self::Unknown(raw), + } + } + + /// Returns the raw protocol error code. + pub const fn as_raw(self) -> u16 { + match self { + Self::UnsupportedVersion => 2, + Self::MalformedRequest => 3, + Self::ProtocolState => 10, + Self::UnsupportedOperation => 11, + Self::Internal => 12, + Self::PolicyDenied => 4, + Self::UnknownObject => 5, + Self::StaleHandle => 6, + Self::WrongObjectType => 7, + Self::InvalidRights => 8, + Self::ResourceExhausted => 9, + Self::WouldBlock => 13, + Self::Unknown(raw) => raw, + } + } +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedVersion => f.write_str("unsupported broker protocol version"), + Self::MalformedRequest => f.write_str("malformed broker request"), + Self::ProtocolState => f.write_str("broker protocol state violation"), + Self::UnsupportedOperation => f.write_str("unsupported broker operation"), + Self::Internal => f.write_str("internal broker error"), + Self::PolicyDenied => f.write_str("broker policy denied the operation"), + Self::UnknownObject => f.write_str("unknown broker object"), + Self::StaleHandle => f.write_str("stale broker handle"), + Self::WrongObjectType => f.write_str("wrong broker object type"), + Self::InvalidRights => f.write_str("invalid broker rights"), + Self::ResourceExhausted => f.write_str("broker resource exhausted"), + Self::WouldBlock => f.write_str("broker operation would block"), + Self::Unknown(raw) => write!(f, "unknown broker error code {raw}"), + } + } +} + +impl core::error::Error for ErrorCode {} diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs new file mode 100644 index 0000000000..efbaf71377 --- /dev/null +++ b/litebox_broker_protocol/src/event.rs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::ObjectHandle; + +/// Broker-authoritative readiness state for one object. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReadinessState { + /// Whether an event read/consume operation can complete without blocking. + pub read_ready: bool, + /// Whether an event write/add operation can complete without blocking. + pub write_ready: bool, + /// Monotonic readiness generation used to invalidate user-side readiness caches. + pub generation: u64, +} + +impl ReadinessState { + /// Creates a readiness state. + pub const fn new(read_ready: bool, write_ready: bool, generation: u64) -> Self { + Self { + read_ready, + write_ready, + generation, + } + } +} + +/// Result of checking whether a broker event read wait would complete now. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum WaitOutcome { + /// The object is read-ready now. + Ready(ReadinessState), + /// The object is not read-ready; deployment-specific wait plumbing may block. + WouldBlock(ReadinessState), +} + +/// How a broker event consume operation should remove readiness credits. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventConsumeMode { + /// Consume all currently available credits. + All, + /// Consume one credit. + One, +} + +/// Request to create a broker-owned event object. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct CreateEventRequest { + /// Initial readiness credits. + pub initial_count: u64, +} + +impl CreateEventRequest { + /// Creates an event create request. + pub const fn new(initial_count: u64) -> Self { + Self { initial_count } + } +} + +/// Response to an event create request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CreateEventResponse { + /// Created event handle. + pub handle: ObjectHandle, +} + +impl CreateEventResponse { + /// Creates an event create response. + pub const fn new(handle: ObjectHandle) -> Self { + Self { handle } + } +} + +/// Request to check whether an event wait would complete now. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WaitEventRequest { + /// Event handle. + pub handle: ObjectHandle, +} + +impl WaitEventRequest { + /// Creates an event wait request. + pub const fn new(handle: ObjectHandle) -> Self { + Self { handle } + } +} + +/// Response to an event wait request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WaitEventResponse { + /// Current wait outcome. + pub outcome: WaitOutcome, +} + +impl WaitEventResponse { + /// Creates an event wait response. + pub const fn new(outcome: WaitOutcome) -> Self { + Self { outcome } + } +} + +/// Request to add readiness credits to an event. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AddEventRequest { + /// Event handle. + pub handle: ObjectHandle, + /// Readiness credits to add. + pub value: u64, +} + +impl AddEventRequest { + /// Creates an event add request. + pub const fn new(handle: ObjectHandle, value: u64) -> Self { + Self { handle, value } + } +} + +/// Response to an event add request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AddEventResponse { + /// Readiness state after adding credits. + pub readiness: ReadinessState, +} + +impl AddEventResponse { + /// Creates an event add response. + pub const fn new(readiness: ReadinessState) -> Self { + Self { readiness } + } +} + +/// Request to consume readiness credits from an event. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ConsumeEventRequest { + /// Event handle. + pub handle: ObjectHandle, + /// Consume mode. + pub mode: EventConsumeMode, +} + +impl ConsumeEventRequest { + /// Creates an event consume request. + pub const fn new(handle: ObjectHandle, mode: EventConsumeMode) -> Self { + Self { handle, mode } + } +} + +/// Result of consuming readiness credits from a broker-owned event object. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EventConsumption { + /// Number of readiness credits consumed. + pub value: u64, + /// Readiness state after consuming credits. + pub readiness: ReadinessState, +} + +impl EventConsumption { + /// Creates an event consumption result. + pub const fn new(value: u64, readiness: ReadinessState) -> Self { + Self { value, readiness } + } +} + +/// Response to an event consume request. +pub type ConsumeEventResponse = EventConsumption; diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs new file mode 100644 index 0000000000..0f081bec33 --- /dev/null +++ b/litebox_broker_protocol/src/lib.rs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Shared broker protocol types and channel contracts. +//! +//! This crate describes broker-visible opaque handles, errors, versions, +//! request/response messages, and the transport-neutral control-channel +//! contracts used to carry them. It does not know whether messages move over +//! Unix sockets, shared rings, kernel traps, or another IPC mechanism. + +#![no_std] + +extern crate alloc; + +pub mod channel; +pub mod error; +pub mod event; +pub mod message; +pub mod object; +pub mod wire; + +pub use channel::{ + HostControlChannel, LocalControlChannel, PeerCredential, ReceivedBrokerRequest, + ReceivedBrokerResponse, +}; +pub use error::ErrorCode; +pub use event::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, + CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, + WaitEventRequest, WaitEventResponse, WaitOutcome, +}; +pub use message::{ + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, +}; +pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; + +/// Major/minor broker protocol version. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ProtocolVersion { + /// Incompatible protocol version. + pub major: u16, + /// Backward-compatible protocol revision within a major version. + pub minor: u16, +} + +impl ProtocolVersion { + /// Creates a protocol version. + pub const fn new(major: u16, minor: u16) -> Self { + Self { major, minor } + } + + /// Returns whether this requested version is supported by `supported`. + /// + /// Minor revisions are backward-compatible within a major version, so a + /// broker can serve a peer requesting the same major version and a minor + /// version no newer than the broker supports. + pub const fn is_supported_by(self, supported: Self) -> bool { + self.major == supported.major && self.minor <= supported.minor + } +} + +/// Initial broker protocol version implemented by the split-broker POC. +pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs new file mode 100644 index 0000000000..2b321711b2 --- /dev/null +++ b/litebox_broker_protocol/src/message.rs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::ProtocolVersion; +use crate::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, + CreateEventRequest, CreateEventResponse, ErrorCode, WaitEventRequest, WaitEventResponse, +}; + +/// Broker request sent over the control channel. +/// +/// The outer broker request is intentionally small. Object-family and +/// domain-specific operations are grouped below it so new object families do not +/// accumulate as unrelated top-level broker variants. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrokerRequest { + /// Protocol negotiation request. + Negotiate { + /// Required protocol version. + protocol_version: ProtocolVersion, + }, + /// BrokerCore authority request. + Core(CoreRequest), +} + +/// Request adapted by the broker host into a BrokerCore domain call. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CoreRequest { + /// Event object request family. + Event(EventRequest), +} + +/// Broker-owned event object request. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventRequest { + /// Create a broker-owned event object. + Create(CreateEventRequest), + /// Check whether an event wait would complete now. + Wait(WaitEventRequest), + /// Add readiness credits to an event. + Add(AddEventRequest), + /// Consume readiness credits from an event. + Consume(ConsumeEventRequest), +} + +/// Broker response sent over the control channel. +/// +/// Common connection/protocol outcomes stay at this layer. Domain payloads are +/// grouped under [`CoreResponse`] so future object families can evolve without +/// turning the broker envelope into a flat operation/result list. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrokerResponse { + /// Negotiation result. + Negotiated { + /// Broker protocol version supported by this endpoint. + /// + /// The broker returns its supported version after validating that the + /// requested version is supported according to + /// [`ProtocolVersion::is_supported_by`](crate::ProtocolVersion::is_supported_by). + broker_protocol_version: ProtocolVersion, + }, + /// Negotiation failed because the requested version is unsupported. + /// + /// The connection remains in negotiation state and the local peer may retry + /// with a compatible version using the broker-supported version advertised + /// here. + VersionMismatch { + /// Broker protocol version supported by this endpoint. + broker_protocol_version: ProtocolVersion, + }, + /// BrokerCore authority response. + Core(CoreResponse), + /// Operation failed with an ABI-neutral broker error. + Error(ErrorCode), +} + +/// Response returned by a BrokerCore domain request. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CoreResponse { + /// Event object response family. + Event(EventResponse), +} + +/// Broker-owned event object response. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventResponse { + /// Create operation response. + Create(CreateEventResponse), + /// Wait operation response. + Wait(WaitEventResponse), + /// Add operation response. + Add(AddEventResponse), + /// Consume operation response. + Consume(ConsumeEventResponse), +} diff --git a/litebox_broker_protocol/src/object.rs b/litebox_broker_protocol/src/object.rs new file mode 100644 index 0000000000..141be47be0 --- /dev/null +++ b/litebox_broker_protocol/src/object.rs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// Broker object reference handle returned to the local core. +/// +/// The local core may cache this value, but the broker remains authoritative for +/// object identity, object lifetime, reference lifetime, type, rights, and +/// reference generation. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ObjectHandle { + /// Opaque broker reference identifier owned by one authenticated process association. + pub reference_id: ObjectReferenceId, + /// Reference generation used to reject stale handles after reference-slot reuse. + pub reference_generation: ObjectReferenceGeneration, +} + +impl ObjectHandle { + /// Creates an object handle. + pub const fn new( + reference_id: ObjectReferenceId, + reference_generation: ObjectReferenceGeneration, + ) -> Self { + Self { + reference_id, + reference_generation, + } + } +} + +/// Broker-owned object reference identifier. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ObjectReferenceId(u64); + +impl ObjectReferenceId { + /// Creates an object reference identifier from its raw protocol value. + pub const fn new(raw: u64) -> Self { + Self(raw) + } + + /// Returns the raw protocol value. + pub const fn get(self) -> u64 { + self.0 + } +} + +/// Generation attached to a broker object reference. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ObjectReferenceGeneration(u64); + +impl ObjectReferenceGeneration { + /// Creates a reference generation from its raw protocol value. + pub const fn new(raw: u64) -> Self { + Self(raw) + } + + /// Returns the raw protocol value. + pub const fn get(self) -> u64 { + self.0 + } +} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs new file mode 100644 index 0000000000..af5a3a171a --- /dev/null +++ b/litebox_broker_protocol/src/wire.rs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Reusable byte codec for broker request/response control-channel messages. +//! +//! The wire codec mirrors the protocol DTO hierarchy: +//! - this module owns public encode/decode entry points and top-level broker +//! envelope tags; +//! - `core_message` owns `CoreRequest`/`CoreResponse` family tags; +//! - object-family modules such as `event` own their operation and nested value +//! tags; +//! - `primitive` owns shared scalar/value encoders. +//! +//! New object families should add a core family tag and a private family codec +//! module instead of adding flat helpers here. Existing payloads are positional; +//! changing fields is an ABI change, so prefer a new operation tag or explicit +//! negotiated-version gate for payload evolution. + +use core::fmt; + +use alloc::vec::Vec; + +use crate::{ + BrokerRequest, BrokerResponse, ErrorCode, ReceivedBrokerRequest, ReceivedBrokerResponse, +}; + +use primitive::{Decoder, Encoder}; + +mod core_message; +mod event; +mod primitive; + +const REQUEST_TAG_NEGOTIATE: u8 = 0; +const REQUEST_TAG_CORE: u8 = 1; + +const RESPONSE_TAG_NEGOTIATED: u8 = 0; +const RESPONSE_TAG_CORE: u8 = 1; +const RESPONSE_TAG_ERROR: u8 = 2; +const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; + +/// Error produced while encoding or decoding a broker wire message. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum WireError { + /// The frame ended before a complete field could be decoded. + TruncatedFrame, + /// The frame contained bytes after the decoded message. + TrailingBytes, + /// A boolean field was not encoded as 0 or 1. + InvalidBoolean, + /// A decoder offset overflowed. + OffsetOverflow, +} + +impl fmt::Display for WireError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TruncatedFrame => f.write_str("truncated broker wire frame"), + Self::TrailingBytes => f.write_str("trailing broker wire bytes"), + Self::InvalidBoolean => f.write_str("invalid broker wire boolean"), + Self::OffsetOverflow => f.write_str("broker wire offset overflow"), + } + } +} + +impl core::error::Error for WireError {} + +/// Encodes a broker request body. +/// +/// Successful encodings are always non-empty because the first byte is the +/// message tag. +pub fn encode_request(request: BrokerRequest) -> Vec { + let mut encoder = Encoder::default(); + match request { + BrokerRequest::Negotiate { protocol_version } => { + encoder.u8(REQUEST_TAG_NEGOTIATE); + encoder.protocol_version(protocol_version); + } + BrokerRequest::Core(request) => { + encoder.u8(REQUEST_TAG_CORE); + core_message::encode_core_request(&mut encoder, request); + } + } + encoder.finish() +} + +/// Decodes a broker request body. +pub fn decode_request(frame: &[u8]) -> Result { + let mut decoder = Decoder::new(frame); + let tag = decoder.u8()?; + let request = match tag { + REQUEST_TAG_NEGOTIATE => BrokerRequest::Negotiate { + protocol_version: decoder.protocol_version()?, + }, + REQUEST_TAG_CORE => match core_message::decode_core_request(&mut decoder)? { + Some(request) => BrokerRequest::Core(request), + None => return Ok(ReceivedBrokerRequest::Unknown), + }, + _ => return Ok(ReceivedBrokerRequest::Unknown), + }; + decoder.finish()?; + Ok(ReceivedBrokerRequest::Request(request)) +} + +/// Encodes a broker response body. +/// +/// Successful encodings are always non-empty because the first byte is the +/// message tag. +pub fn encode_response(response: BrokerResponse) -> Vec { + let mut encoder = Encoder::default(); + match response { + BrokerResponse::Negotiated { + broker_protocol_version, + } => { + encoder.u8(RESPONSE_TAG_NEGOTIATED); + encoder.protocol_version(broker_protocol_version); + } + BrokerResponse::VersionMismatch { + broker_protocol_version, + } => { + encoder.u8(RESPONSE_TAG_VERSION_MISMATCH); + encoder.protocol_version(broker_protocol_version); + } + BrokerResponse::Core(response) => { + encoder.u8(RESPONSE_TAG_CORE); + core_message::encode_core_response(&mut encoder, response); + } + BrokerResponse::Error(error) => { + encoder.u8(RESPONSE_TAG_ERROR); + encoder.u16(error.as_raw()); + } + } + encoder.finish() +} + +/// Decodes a broker response body. +pub fn decode_response(frame: &[u8]) -> Result { + let mut decoder = Decoder::new(frame); + let tag = decoder.u8()?; + let response = match tag { + RESPONSE_TAG_NEGOTIATED => BrokerResponse::Negotiated { + broker_protocol_version: decoder.protocol_version()?, + }, + RESPONSE_TAG_VERSION_MISMATCH => BrokerResponse::VersionMismatch { + broker_protocol_version: decoder.protocol_version()?, + }, + RESPONSE_TAG_CORE => match core_message::decode_core_response(&mut decoder)? { + Some(response) => BrokerResponse::Core(response), + None => return Ok(ReceivedBrokerResponse::Unknown), + }, + RESPONSE_TAG_ERROR => { + let error = ErrorCode::from_raw(decoder.u16()?); + BrokerResponse::Error(error) + } + _ => return Ok(ReceivedBrokerResponse::Unknown), + }; + decoder.finish()?; + Ok(ReceivedBrokerResponse::Response(response)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, + CoreResponse, CreateEventRequest, CreateEventResponse, EventConsumeMode, EventRequest, + EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, + ReadinessState, WaitEventRequest, WaitEventResponse, WaitOutcome, + }; + + #[test] + fn request_codec_round_trips_all_variants() { + let handle = sample_handle(); + let requests = [ + BrokerRequest::Negotiate { + protocol_version: ProtocolVersion::new(1, 0), + }, + event_request(EventRequest::Create(CreateEventRequest::new(0))), + event_request(EventRequest::Create(CreateEventRequest::new(7))), + event_request(EventRequest::Wait(WaitEventRequest::new(handle))), + event_request(EventRequest::Add(AddEventRequest::new(handle, 3))), + event_request(EventRequest::Consume(ConsumeEventRequest::new( + handle, + EventConsumeMode::All, + ))), + event_request(EventRequest::Consume(ConsumeEventRequest::new( + handle, + EventConsumeMode::One, + ))), + ]; + + for request in requests { + assert_eq!( + decode_request(&encode_request(request.clone())).unwrap(), + ReceivedBrokerRequest::Request(request) + ); + } + } + + #[test] + fn response_codec_round_trips_all_variants() { + let handle = sample_handle(); + let responses = [ + BrokerResponse::Negotiated { + broker_protocol_version: ProtocolVersion::new(1, 0), + }, + BrokerResponse::VersionMismatch { + broker_protocol_version: ProtocolVersion::new(1, 0), + }, + event_response(EventResponse::Create(CreateEventResponse::new(handle))), + event_response(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::Ready(ReadinessState::new(true, false, 8)), + ))), + event_response(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 9)), + ))), + event_response(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(true, true, 10), + ))), + event_response(EventResponse::Consume(ConsumeEventResponse::new( + 3, + ReadinessState::new(false, true, 11), + ))), + BrokerResponse::Error(ErrorCode::PolicyDenied), + BrokerResponse::Error(ErrorCode::WouldBlock), + BrokerResponse::Error(ErrorCode::Internal), + ]; + + for response in responses { + assert_eq!( + decode_response(&encode_response(response.clone())).unwrap(), + ReceivedBrokerResponse::Response(response) + ); + } + } + + #[test] + fn decode_rejects_malformed_request_frames() { + assert_eq!( + decode_request(&[0xff, 1, 2, 3]), + Ok(ReceivedBrokerRequest::Unknown) + ); + let mut unknown_consume_mode = encode_request(event_request(EventRequest::Consume( + ConsumeEventRequest::new(sample_handle(), EventConsumeMode::All), + ))); + *unknown_consume_mode.last_mut().unwrap() = 0xff; + assert_eq!( + decode_request(&unknown_consume_mode), + Ok(ReceivedBrokerRequest::Unknown) + ); + assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); + let mut frame = encode_request(event_request(EventRequest::Create( + CreateEventRequest::new(0), + ))); + frame.push(0xff); + assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); + } + + #[test] + fn decode_rejects_malformed_response_frames() { + assert_eq!( + decode_response(&[0xff, 1, 2, 3]), + Ok(ReceivedBrokerResponse::Unknown) + ); + assert_eq!( + decode_response(&[1, 0, 1, 0xff]), + Ok(ReceivedBrokerResponse::Unknown) + ); + assert_eq!( + decode_response(&[2, 0xff, 0xff]), + Ok(ReceivedBrokerResponse::Response(BrokerResponse::Error( + ErrorCode::Unknown(0xffff) + ))) + ); + + let mut invalid_bool = [1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!( + decode_response(&invalid_bool), + Err(WireError::InvalidBoolean) + ); + + invalid_bool[3] = 1; + invalid_bool[4] = 1; + invalid_bool[12] = 1; + let mut frame = invalid_bool.to_vec(); + frame.push(0xff); + assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); + } + + #[test] + fn event_add_response_wire_shape_is_pinned() { + assert_eq!( + encode_response(event_response(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(true, false, 0x0102_0304_0506_0708) + )))), + [1, 0, 2, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1] + ); + } + + const fn sample_handle() -> ObjectHandle { + ObjectHandle::new( + ObjectReferenceId::new(13), + ObjectReferenceGeneration::new(14), + ) + } + + const fn event_request(request: EventRequest) -> BrokerRequest { + BrokerRequest::Core(CoreRequest::Event(request)) + } + + const fn event_response(response: EventResponse) -> BrokerResponse { + BrokerResponse::Core(CoreResponse::Event(response)) + } +} diff --git a/litebox_broker_protocol/src/wire/core_message.rs b/litebox_broker_protocol/src/wire/core_message.rs new file mode 100644 index 0000000000..dbd0665da3 --- /dev/null +++ b/litebox_broker_protocol/src/wire/core_message.rs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{CoreRequest, CoreResponse}; + +use super::WireError; +use super::event; +use super::primitive::{Decoder, Encoder}; + +// Core tags select object-family codecs. Add new object families here, then +// keep their operation-specific tags inside a dedicated family module. +const CORE_REQUEST_TAG_EVENT: u8 = 0; +const CORE_RESPONSE_TAG_EVENT: u8 = 0; + +pub(super) fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) { + match request { + CoreRequest::Event(request) => { + encoder.u8(CORE_REQUEST_TAG_EVENT); + event::encode_event_request(encoder, request); + } + } +} + +pub(super) fn decode_core_request( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { + let request = match decoder.u8()? { + CORE_REQUEST_TAG_EVENT => match event::decode_event_request(decoder)? { + Some(request) => CoreRequest::Event(request), + None => return Ok(None), + }, + _ => return Ok(None), + }; + + Ok(Some(request)) +} + +pub(super) fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) { + match response { + CoreResponse::Event(response) => { + encoder.u8(CORE_RESPONSE_TAG_EVENT); + event::encode_event_response(encoder, response); + } + } +} + +pub(super) fn decode_core_response( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { + let response = match decoder.u8()? { + CORE_RESPONSE_TAG_EVENT => match event::decode_event_response(decoder)? { + Some(response) => CoreResponse::Event(response), + None => return Ok(None), + }, + _ => return Ok(None), + }; + + Ok(Some(response)) +} diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs new file mode 100644 index 0000000000..eaf4cfa018 --- /dev/null +++ b/litebox_broker_protocol/src/wire/event.rs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, + CreateEventRequest, CreateEventResponse, EventConsumeMode, EventRequest, EventResponse, + ReadinessState, WaitEventRequest, WaitEventResponse, WaitOutcome, +}; + +use super::WireError; +use super::primitive::{Decoder, Encoder}; + +// Event operation tags live with the event family. Future event operations +// should add tags here; unrelated object families should get their own module. +const EVENT_REQUEST_TAG_CREATE: u8 = 0; +const EVENT_REQUEST_TAG_WAIT: u8 = 1; +const EVENT_REQUEST_TAG_ADD: u8 = 2; +const EVENT_REQUEST_TAG_CONSUME: u8 = 3; + +const EVENT_RESPONSE_TAG_CREATED: u8 = 0; +const EVENT_RESPONSE_TAG_WAITED: u8 = 1; +const EVENT_RESPONSE_TAG_ADDED: u8 = 2; +const EVENT_RESPONSE_TAG_CONSUMED: u8 = 3; + +const WAIT_OUTCOME_TAG_READY: u8 = 1; +const WAIT_OUTCOME_TAG_WOULD_BLOCK: u8 = 2; +const EVENT_CONSUME_MODE_TAG_ALL: u8 = 1; +const EVENT_CONSUME_MODE_TAG_ONE: u8 = 2; + +pub(super) fn encode_event_request(encoder: &mut Encoder, request: EventRequest) { + match request { + EventRequest::Create(request) => { + encoder.u8(EVENT_REQUEST_TAG_CREATE); + encoder.u64(request.initial_count); + } + EventRequest::Wait(request) => { + encoder.u8(EVENT_REQUEST_TAG_WAIT); + encoder.handle(request.handle); + } + EventRequest::Add(request) => { + encoder.u8(EVENT_REQUEST_TAG_ADD); + encoder.handle(request.handle); + encoder.u64(request.value); + } + EventRequest::Consume(request) => { + encoder.u8(EVENT_REQUEST_TAG_CONSUME); + encoder.handle(request.handle); + encode_consume_mode(encoder, request.mode); + } + } +} + +pub(super) fn decode_event_request( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { + let request = match decoder.u8()? { + EVENT_REQUEST_TAG_CREATE => EventRequest::Create(CreateEventRequest::new(decoder.u64()?)), + EVENT_REQUEST_TAG_WAIT => EventRequest::Wait(WaitEventRequest::new(decoder.handle()?)), + EVENT_REQUEST_TAG_ADD => { + EventRequest::Add(AddEventRequest::new(decoder.handle()?, decoder.u64()?)) + } + EVENT_REQUEST_TAG_CONSUME => EventRequest::Consume(ConsumeEventRequest::new( + decoder.handle()?, + match decode_consume_mode(decoder)? { + Some(mode) => mode, + None => return Ok(None), + }, + )), + _ => return Ok(None), + }; + + Ok(Some(request)) +} + +pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventResponse) { + match response { + EventResponse::Create(response) => { + encoder.u8(EVENT_RESPONSE_TAG_CREATED); + encoder.handle(response.handle); + } + EventResponse::Wait(response) => { + encoder.u8(EVENT_RESPONSE_TAG_WAITED); + encode_wait_outcome(encoder, response.outcome); + } + EventResponse::Add(response) => { + encoder.u8(EVENT_RESPONSE_TAG_ADDED); + encode_readiness(encoder, response.readiness); + } + EventResponse::Consume(response) => { + encoder.u8(EVENT_RESPONSE_TAG_CONSUMED); + encoder.u64(response.value); + encode_readiness(encoder, response.readiness); + } + } +} + +pub(super) fn decode_event_response( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { + let response = match decoder.u8()? { + EVENT_RESPONSE_TAG_CREATED => { + EventResponse::Create(CreateEventResponse::new(decoder.handle()?)) + } + EVENT_RESPONSE_TAG_WAITED => EventResponse::Wait(WaitEventResponse::new( + match decode_wait_outcome(decoder)? { + Some(outcome) => outcome, + None => return Ok(None), + }, + )), + EVENT_RESPONSE_TAG_ADDED => { + EventResponse::Add(AddEventResponse::new(decode_readiness(decoder)?)) + } + EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(ConsumeEventResponse::new( + decoder.u64()?, + decode_readiness(decoder)?, + )), + _ => return Ok(None), + }; + + Ok(Some(response)) +} + +fn encode_wait_outcome(encoder: &mut Encoder, outcome: WaitOutcome) { + match outcome { + WaitOutcome::Ready(readiness) => { + encoder.u8(WAIT_OUTCOME_TAG_READY); + encode_readiness(encoder, readiness); + } + WaitOutcome::WouldBlock(readiness) => { + encoder.u8(WAIT_OUTCOME_TAG_WOULD_BLOCK); + encode_readiness(encoder, readiness); + } + } +} + +fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result, WireError> { + match decoder.u8()? { + WAIT_OUTCOME_TAG_READY => Ok(Some(WaitOutcome::Ready(decode_readiness(decoder)?))), + WAIT_OUTCOME_TAG_WOULD_BLOCK => { + Ok(Some(WaitOutcome::WouldBlock(decode_readiness(decoder)?))) + } + _ => Ok(None), + } +} + +fn encode_readiness(encoder: &mut Encoder, readiness: ReadinessState) { + encoder.bool(readiness.read_ready); + encoder.bool(readiness.write_ready); + encoder.u64(readiness.generation); +} + +fn decode_readiness(decoder: &mut Decoder<'_>) -> Result { + Ok(ReadinessState::new( + decoder.bool()?, + decoder.bool()?, + decoder.u64()?, + )) +} + +fn encode_consume_mode(encoder: &mut Encoder, mode: EventConsumeMode) { + match mode { + EventConsumeMode::All => { + encoder.u8(EVENT_CONSUME_MODE_TAG_ALL); + } + EventConsumeMode::One => { + encoder.u8(EVENT_CONSUME_MODE_TAG_ONE); + } + } +} + +fn decode_consume_mode(decoder: &mut Decoder<'_>) -> Result, WireError> { + match decoder.u8()? { + EVENT_CONSUME_MODE_TAG_ALL => Ok(Some(EventConsumeMode::All)), + EVENT_CONSUME_MODE_TAG_ONE => Ok(Some(EventConsumeMode::One)), + _ => Ok(None), + } +} diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs new file mode 100644 index 0000000000..cdfad237ac --- /dev/null +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::vec::Vec; + +use crate::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion}; + +use super::WireError; + +#[derive(Default)] +pub(super) struct Encoder { + bytes: Vec, +} + +impl Encoder { + pub(super) fn finish(self) -> Vec { + self.bytes + } + + pub(super) fn bool(&mut self, value: bool) { + self.u8(u8::from(value)); + } + + pub(super) fn u8(&mut self, value: u8) { + self.bytes.push(value); + } + + pub(super) fn u16(&mut self, value: u16) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + pub(super) fn u64(&mut self, value: u64) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + pub(super) fn protocol_version(&mut self, version: ProtocolVersion) { + self.u16(version.major); + self.u16(version.minor); + } + + pub(super) fn handle(&mut self, handle: ObjectHandle) { + self.u64(handle.reference_id.get()); + self.u64(handle.reference_generation.get()); + } +} + +pub(super) struct Decoder<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Decoder<'a> { + pub(super) const fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + pub(super) fn finish(&self) -> Result<(), WireError> { + if self.offset == self.bytes.len() { + Ok(()) + } else { + Err(WireError::TrailingBytes) + } + } + + pub(super) fn bool(&mut self) -> Result { + match self.u8()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(WireError::InvalidBoolean), + } + } + + pub(super) fn u8(&mut self) -> Result { + let bytes = self.take(1)?; + Ok(bytes[0]) + } + + pub(super) fn u16(&mut self) -> Result { + let bytes = self.take(2)?; + Ok(u16::from_le_bytes([bytes[0], bytes[1]])) + } + + pub(super) fn u64(&mut self) -> Result { + let bytes = self.take(8)?; + Ok(u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ])) + } + + pub(super) fn protocol_version(&mut self) -> Result { + Ok(ProtocolVersion::new(self.u16()?, self.u16()?)) + } + + pub(super) fn handle(&mut self) -> Result { + let reference_id = ObjectReferenceId::new(self.u64()?); + let reference_generation = ObjectReferenceGeneration::new(self.u64()?); + + Ok(ObjectHandle::new(reference_id, reference_generation)) + } + + fn take(&mut self, len: usize) -> Result<&'a [u8], WireError> { + let end = self + .offset + .checked_add(len) + .ok_or(WireError::OffsetOverflow)?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or(WireError::TruncatedFrame)?; + self.offset = end; + Ok(bytes) + } +} diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml new file mode 100644 index 0000000000..da5909d569 --- /dev/null +++ b/litebox_broker_transport/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "litebox_broker_transport" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs new file mode 100644 index 0000000000..9603907abc --- /dev/null +++ b/litebox_broker_transport/src/lib.rs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Broker transport implementations. +//! +//! Transports own hosted or platform-specific framing and I/O. Portable broker +//! protocol messages, local-side adapters, host-side request handling, and core +//! authority state live in separate crates. + +pub mod unix_socket; diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs new file mode 100644 index 0000000000..766896ba9b --- /dev/null +++ b/litebox_broker_transport/src/unix_socket.rs @@ -0,0 +1,378 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Unix-domain-socket broker channel for hosted userland deployments. +//! +//! This module deliberately uses `std` because Unix-domain sockets and `std::io` +//! framing are hosted userland concerns. Portable broker interfaces live in the +//! no_std protocol, local, core, and host crates. + +use std::io::{self, Read, Write}; +use std::os::unix::net::UnixStream; +use std::path::Path; +use std::time::{Duration, Instant}; + +use litebox_broker_protocol::wire::{ + WireError, decode_request, decode_response, encode_request, encode_response, +}; +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, HostControlChannel, LocalControlChannel, PeerCredential, + ReceivedBrokerRequest, ReceivedBrokerResponse, +}; + +const MAX_FRAME_LEN: usize = 64 * 1024; + +/// Local-side Unix-domain-socket control channel for the hosted userland POC. +pub struct UnixStreamLocalControlChannel { + stream: UnixStream, + io_timeout: Option, + io_deadline: Option, + active_request_deadline: Option, +} + +impl UnixStreamLocalControlChannel { + /// Creates a local control channel from an already-connected Unix stream. + pub const fn from_connected(stream: UnixStream) -> Self { + Self { + stream, + io_timeout: None, + io_deadline: None, + active_request_deadline: None, + } + } + + /// Connects to a userland broker Unix socket. + pub fn connect(path: impl AsRef) -> io::Result { + UnixStream::connect(path).map(Self::from_connected) + } + + /// Sets the read and write timeout for broker control-channel operations. + pub fn set_io_timeout(&mut self, timeout: Option) -> io::Result<()> { + self.io_timeout = timeout; + self.io_deadline = None; + self.active_request_deadline = None; + self.set_stream_io_timeout(timeout) + } + + /// Sets a wall-clock deadline for broker control-channel operations. + pub fn set_io_deadline(&mut self, deadline: Option) -> io::Result<()> { + self.io_deadline = deadline; + self.active_request_deadline = None; + match deadline { + Some(deadline) => self.set_stream_io_timeout(Some(io_timeout_for_deadline(deadline)?)), + None => self.set_stream_io_timeout(self.io_timeout), + } + } + + fn set_stream_io_timeout(&self, timeout: Option) -> io::Result<()> { + self.stream.set_read_timeout(timeout)?; + self.stream.set_write_timeout(timeout) + } + + fn current_deadline(&mut self) -> io::Result> { + if let Some(deadline) = self.io_deadline { + return Ok(Some(deadline)); + } + if let Some(deadline) = self.active_request_deadline { + return Ok(Some(deadline)); + } + let Some(timeout) = self.io_timeout else { + return Ok(None); + }; + let deadline = deadline_after(timeout)?; + self.active_request_deadline = Some(deadline); + Ok(Some(deadline)) + } + + fn clear_active_request_deadline(&mut self) -> io::Result<()> { + if self.io_deadline.is_none() { + self.active_request_deadline = None; + self.set_stream_io_timeout(self.io_timeout)?; + } + Ok(()) + } +} + +/// Host-side Unix-domain-socket control channel for the hosted userland POC. +pub struct UnixStreamHostControlChannel { + stream: UnixStream, + io_deadline: Option, +} + +impl UnixStreamHostControlChannel { + /// Creates a host control channel from an accepted Unix stream. + pub const fn from_accepted(stream: UnixStream) -> Self { + Self { + stream, + io_deadline: None, + } + } + + /// Sets a wall-clock deadline for all broker control-channel operations. + pub fn set_io_deadline(&mut self, deadline: Option) -> io::Result<()> { + self.io_deadline = deadline; + if let Some(deadline) = deadline { + let timeout = io_timeout_for_deadline(deadline)?; + self.stream.set_read_timeout(Some(timeout))?; + self.stream.set_write_timeout(Some(timeout)) + } else { + self.stream.set_read_timeout(None)?; + self.stream.set_write_timeout(None) + } + } +} + +impl LocalControlChannel for UnixStreamLocalControlChannel { + type Error = io::Error; + + fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { + let frame = encode_request(request.clone()); + let deadline = self.current_deadline()?; + let result = write_frame_with_deadline(&mut self.stream, &frame, deadline); + if result.is_err() { + self.active_request_deadline = None; + } + result + } + + fn recv_response(&mut self) -> io::Result> { + let deadline = self.current_deadline()?; + let result = match read_frame_with_deadline(&mut self.stream, deadline)? { + Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), + None => Ok(None), + }; + self.clear_active_request_deadline()?; + result + } +} + +impl HostControlChannel for UnixStreamHostControlChannel { + type Error = io::Error; + + fn peer_credential(&self) -> io::Result { + // TODO(broker): replace the PoC placeholder with Unix peer credential extraction + // before this channel is used as an authenticated deployment boundary. + Ok(PeerCredential::Unauthenticated) + } + + fn recv_request(&mut self) -> io::Result> { + let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { + return Ok(None); + }; + decode_request(&frame).map(Some).map_err(wire_error) + } + + fn send_response(&mut self, response: &BrokerResponse) -> io::Result<()> { + write_frame_with_deadline( + &mut self.stream, + &encode_response(response.clone()), + self.io_deadline, + ) + } +} + +fn read_frame_with_deadline( + stream: &mut UnixStream, + deadline: Option, +) -> io::Result>> { + let mut len_buf = [0; 4]; + let mut read = 0; + while read < len_buf.len() { + refresh_stream_io_deadline(stream, deadline)?; + match stream.read(&mut len_buf[read..]) { + Ok(0) if read == 0 => return Ok(None), + Ok(0) => return Err(invalid_data("truncated broker frame length")), + Ok(len) => read += len, + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + + let len = u32::from_le_bytes(len_buf) as usize; + if len == 0 || len > MAX_FRAME_LEN { + return Err(invalid_data("invalid broker frame length")); + } + + let mut frame = vec![0; len]; + let mut read = 0; + while read < frame.len() { + refresh_stream_io_deadline(stream, deadline)?; + match stream.read(&mut frame[read..]) { + Ok(0) => return Err(invalid_data("truncated broker frame")), + Ok(len) => read += len, + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + Ok(Some(frame)) +} + +fn write_frame_with_deadline( + stream: &mut UnixStream, + frame: &[u8], + deadline: Option, +) -> io::Result<()> { + if frame.is_empty() || frame.len() > MAX_FRAME_LEN { + return Err(invalid_data("invalid broker frame length")); + } + let len = u32::try_from(frame.len()).map_err(|_| invalid_data("broker frame too large"))?; + write_all_with_deadline(stream, &len.to_le_bytes(), deadline)?; + write_all_with_deadline(stream, frame, deadline) +} + +fn write_all_with_deadline( + stream: &mut UnixStream, + mut buffer: &[u8], + deadline: Option, +) -> io::Result<()> { + while !buffer.is_empty() { + refresh_stream_io_deadline(stream, deadline)?; + match stream.write(buffer) { + Ok(0) => { + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "failed to write broker frame", + )); + } + Ok(written) => buffer = &buffer[written..], + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + Ok(()) +} + +fn refresh_stream_io_deadline(stream: &UnixStream, deadline: Option) -> io::Result<()> { + if let Some(deadline) = deadline { + let timeout = io_timeout_for_deadline(deadline)?; + stream.set_read_timeout(Some(timeout))?; + stream.set_write_timeout(Some(timeout))?; + } + Ok(()) +} + +fn io_timeout_for_deadline(deadline: Instant) -> io::Result { + let timeout = deadline + .checked_duration_since(Instant::now()) + .filter(|timeout| !timeout.is_zero()) + .ok_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "broker I/O deadline expired"))?; + Ok(timeout) +} + +fn deadline_after(timeout: Duration) -> io::Result { + Instant::now() + .checked_add(timeout) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "broker I/O timeout overflow")) +} + +fn invalid_data(message: &'static str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} + +fn wire_error(error: WireError) -> io::Error { + io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid broker wire message: {error}"), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frame_round_trip() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + write_frame_with_deadline(&mut writer, &[1, 2, 3], None).unwrap(); + + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap() + .unwrap(), + [1, 2, 3] + ); + } + + #[test] + fn clean_eof_before_frame_is_close() { + let (writer, mut reader) = UnixStream::pair().unwrap(); + drop(writer); + + assert!( + read_frame_with_deadline(&mut reader, None) + .unwrap() + .is_none() + ); + } + + #[test] + fn malformed_frames_are_invalid() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&[1, 0]).unwrap(); + drop(writer); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&0u32.to_le_bytes()).unwrap(); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer + .write_all(&u32::try_from(MAX_FRAME_LEN + 1).unwrap().to_le_bytes()) + .unwrap(); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&4u32.to_le_bytes()).unwrap(); + writer.write_all(&[1, 2]).unwrap(); + drop(writer); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + } + + #[test] + fn local_response_read_io_timeout_is_wall_clock() { + let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); + channel + .set_io_timeout(Some(Duration::from_millis(50))) + .unwrap(); + + let reader = std::thread::spawn(move || channel.recv_response().unwrap_err()); + host_stream.write_all(&8u32.to_le_bytes()).unwrap(); + for _ in 0..8 { + std::thread::sleep(Duration::from_millis(20)); + if host_stream.write_all(&[0]).is_err() { + break; + } + } + + let error = reader.join().expect("timeout reader panicked"); + assert!( + matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut + ), + "unexpected timeout error kind: {error:?}" + ); + } +} diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml new file mode 100644 index 0000000000..8e3502f1c1 --- /dev/null +++ b/litebox_broker_userland/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "litebox_broker_userland" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.33", features = ["derive"] } +litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } +litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } + +[[bin]] +name = "litebox-broker-userland" +path = "src/main.rs" + +[dev-dependencies] +litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs new file mode 100644 index 0000000000..d3c91f578a --- /dev/null +++ b/litebox_broker_userland/src/main.rs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::error::Error; +use std::os::unix::net::UnixListener; +use std::path::PathBuf; +use std::time::{Duration, Instant}; + +use clap::Parser; +use litebox_broker_core::{BrokerCore, PolicyEngine}; +use litebox_broker_host::serve_connection; +use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; + +const SESSION_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Parser, Debug)] +struct CliArgs { + /// Broker Unix socket path to bind. + #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] + socket: PathBuf, +} + +fn main() -> Result<(), Box> { + let args = CliArgs::parse(); + let listener = UnixListener::bind(args.socket)?; + let (stream, _) = listener.accept()?; + let mut channel = UnixStreamHostControlChannel::from_accepted(stream); + channel.set_io_deadline(Some(Instant::now() + SESSION_TIMEOUT))?; + let mut broker = BrokerCore::new(PolicyEngine::event_only())?; + serve_connection(&mut broker, &mut channel)?; + Ok(()) +} diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs new file mode 100644 index 0000000000..1d18bdb427 --- /dev/null +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::env; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, ExitStatus}; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use litebox_broker_host::HOST_PROTOCOL_VERSION; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::{ReadinessState, WaitOutcome}; +use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; + +#[test] +fn separate_process_broker_serves_event_object_requests() { + let socket_path = SocketPathGuard::new(unique_socket_path()); + let mut child = ChildGuard::new(spawn_broker(socket_path.path())); + let mut channel = connect_with_retry(socket_path.path()).unwrap(); + channel + .set_io_timeout(Some(Duration::from_secs(5))) + .unwrap(); + let mut local = BrokerLocal::new(channel); + + assert_eq!(local.negotiate().unwrap(), HOST_PROTOCOL_VERSION); + + let handle = local.create_event().unwrap(); + assert_eq!( + local.wait_event(handle).unwrap(), + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) + ); + + assert_eq!( + local.add_event(handle, 1).unwrap(), + ReadinessState::new(true, true, 1) + ); + + assert_eq!( + local.wait_event(handle).unwrap(), + WaitOutcome::Ready(ReadinessState::new(true, true, 1)) + ); + drop(local); + assert!(child.wait().unwrap().success()); +} + +fn spawn_broker(socket_path: &Path) -> Child { + Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")) + .arg("--socket") + .arg(socket_path) + .spawn() + .unwrap() +} + +struct ChildGuard { + child: Option, +} + +impl ChildGuard { + fn new(child: Child) -> Self { + Self { child: Some(child) } + } + + fn wait(&mut self) -> io::Result { + let status = self.child.as_mut().expect("child process missing").wait(); + if status.is_ok() { + self.child = None; + } + status + } +} + +impl Drop for ChildGuard { + fn drop(&mut self) { + if let Some(mut child) = self.child.take() { + match child.try_wait() { + Ok(Some(_status)) => {} + Ok(None) => { + let _ = child.kill(); + let _ = child.wait(); + } + Err(_error) => { + let _ = child.kill(); + let _ = child.wait(); + } + } + } + } +} + +struct SocketPathGuard { + path: PathBuf, +} + +impl SocketPathGuard { + fn new(path: PathBuf) -> Self { + Self { path } + } + + fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for SocketPathGuard { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } +} + +fn connect_with_retry(socket_path: &Path) -> io::Result { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match UnixStreamLocalControlChannel::connect(socket_path) { + Ok(channel) => return Ok(channel), + Err(error) if Instant::now() < deadline => { + if error.kind() != io::ErrorKind::NotFound + && error.kind() != io::ErrorKind::ConnectionRefused + { + return Err(error); + } + thread::sleep(Duration::from_millis(10)); + } + Err(error) => return Err(error), + } + } +} + +fn unique_socket_path() -> PathBuf { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + env::temp_dir().join(format!( + "litebox-broker-userland-{}-{now}.sock", + std::process::id() + )) +} diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 5153a83fa5..3c1177a76c 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -536,6 +536,20 @@ where } } +impl From for Errno { + fn from(value: litebox::event::counter::EventCounterError) -> Self { + match value { + litebox::event::counter::EventCounterError::InvalidInput => Errno::EINVAL, + litebox::event::counter::EventCounterError::WouldBlock + | litebox::event::counter::EventCounterError::ResourceExhausted => Errno::EAGAIN, + litebox::event::counter::EventCounterError::Io + | litebox::event::counter::EventCounterError::UnexpectedResponse + | litebox::event::counter::EventCounterError::Unavailable => Errno::EIO, + _ => Errno::EIO, + } + } +} + impl From for Errno { fn from(value: litebox::fs::errors::ReadDirError) -> Self { match value { diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index d87e55fa43..ece99163f7 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -481,6 +481,75 @@ impl LinuxUserland { .unwrap(), ], ), + // Broker control-channel I/O runs through a host Unix socket in the + // current POC. The transport refreshes read/write timeouts around + // each request. + ( + libc::SYS_setsockopt, + vec![ + SeccompRule::new(vec![ + SeccompCondition::new( + 1, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::SOL_SOCKET as u64, + ) + .unwrap(), + SeccompCondition::new( + 2, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::SO_RCVTIMEO as u64, + ) + .unwrap(), + ]) + .unwrap(), + SeccompRule::new(vec![ + SeccompCondition::new( + 1, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::SOL_SOCKET as u64, + ) + .unwrap(), + SeccompCondition::new( + 2, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::SO_SNDTIMEO as u64, + ) + .unwrap(), + ]) + .unwrap(), + ], + ), + // Connected UnixStream I/O may use sendto/recvfrom rather than raw + // read/write. Limit these rules to connected-socket calls that do + // not name a peer address. + ( + libc::SYS_sendto, + vec![ + SeccompRule::new(vec![ + SeccompCondition::new(4, SeccompCmpArgLen::Qword, SeccompCmpOp::Eq, 0) + .unwrap(), + SeccompCondition::new(5, SeccompCmpArgLen::Qword, SeccompCmpOp::Eq, 0) + .unwrap(), + ]) + .unwrap(), + ], + ), + ( + libc::SYS_recvfrom, + vec![ + SeccompRule::new(vec![ + SeccompCondition::new(4, SeccompCmpArgLen::Qword, SeccompCmpOp::Eq, 0) + .unwrap(), + SeccompCondition::new(5, SeccompCmpArgLen::Qword, SeccompCmpOp::Eq, 0) + .unwrap(), + ]) + .unwrap(), + ], + ), (libc::SYS_close, vec![]), ]; let rule_map: std::collections::BTreeMap> = diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 6a5b883745..25c47f8a3d 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -8,6 +8,9 @@ anyhow = "1.0.97" clap = { version = "4.5.33", features = ["derive"] } libc = { version = "0.2.169", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } +litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } +litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } +litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } 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_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_linux_userland"] } @@ -21,6 +24,8 @@ litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = sha2 = "0.10" walkdir = "2.0" glob = "0.3" +litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core" } +litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } [features] lock_tracing = ["litebox/lock_tracing"] diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs new file mode 100644 index 0000000000..7634d861a2 --- /dev/null +++ b/litebox_runner_linux_userland/src/broker.rs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::{ + path::Path, + thread, + time::{Duration, Instant}, +}; + +use anyhow::{Context as _, Result}; +use litebox_broker_local::BrokerLocal; +use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; + +const SETUP_TIMEOUT: Duration = Duration::from_secs(5); +const ACTIVE_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); +const RETRY_DELAY: Duration = Duration::from_millis(20); +type Local = BrokerLocal; + +pub(crate) struct BrokerConnection { + local: Local, +} + +pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { + match socket_path { + Some(path) => connect_to_endpoint(path).map(Some), + None => Ok(None), + } +} + +impl BrokerConnection { + pub(crate) fn into_local(self) -> Local { + self.local + } +} + +fn connect_to_endpoint(socket_path: &Path) -> Result { + let setup_deadline = Instant::now() + SETUP_TIMEOUT; + let mut local = connect_with_retry(socket_path, setup_deadline) + .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; + local + .control_channel_mut() + .set_io_timeout(Some(ACTIVE_REQUEST_TIMEOUT)) + .context("failed to configure broker active request timeout")?; + Ok(BrokerConnection { local }) +} + +fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { + loop { + match UnixStreamLocalControlChannel::connect(socket_path) { + Ok(mut channel) => { + channel + .set_io_deadline(Some(setup_deadline)) + .context("failed to configure broker setup deadline")?; + let mut local = BrokerLocal::new(channel); + local.negotiate().context("broker negotiation failed")?; + return Ok(local); + } + Err(error) => { + if Instant::now() >= setup_deadline { + return Err(error).context("timed out connecting to broker"); + } + } + } + let remaining = setup_deadline.saturating_duration_since(Instant::now()); + thread::sleep(RETRY_DELAY.min(remaining)); + } +} diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 9a18b4a7c2..23e866c586 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -9,6 +9,8 @@ use memmap2::Mmap; use std::os::linux::fs::MetadataExt as _; use std::path::{Path, PathBuf}; +mod broker; + extern crate alloc; // Use a stable non-root guest identity instead of mirroring the host user. This keeps shim @@ -77,6 +79,15 @@ pub struct CliArgs { help_heading = "Unstable Options" )] pub program_from_tar: bool, + /// Connect to an already-running broker Unix socket and verify the control path. + #[arg( + long = "broker-socket", + value_name = "PATH", + value_hint = clap::ValueHint::FilePath, + requires = "unstable", + help_heading = "Unstable Options" + )] + pub broker_socket: Option, } struct MmappedFile { @@ -201,7 +212,18 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } litebox_platform_multiplex::set_platform(platform); - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); + let broker_connection = broker::connect(cli_args.broker_socket.as_deref())?; + + let shim_builder = if let Some(broker_connection) = broker_connection { + litebox_shim_linux::LinuxShimBuilder::new_with_litebox( + litebox::LiteBox::new_with_broker_local( + litebox_platform_multiplex::platform(), + broker_connection.into_local(), + ), + ) + } else { + litebox_shim_linux::LinuxShimBuilder::new() + }; let litebox = shim_builder.litebox(); // SAFETY: `gettid` takes no pointer arguments and has no Rust-side aliasing requirements. let tid = unsafe { libc::syscall(libc::SYS_gettid) } diff --git a/litebox_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c new file mode 100644 index 0000000000..5ed53b1943 --- /dev/null +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include +#include +#include +#include + +static int expect_eagain_read(int fd) { + uint64_t value = 0; + errno = 0; + if (read(fd, &value, sizeof(value)) != -1) { + return 1; + } + return errno == EAGAIN ? 0 : 2; +} + +static int write_value(int fd, uint64_t value) { + return write(fd, &value, sizeof(value)) == sizeof(value) ? 0 : 1; +} + +static int read_value(int fd, uint64_t expected) { + uint64_t value = 0; + if (read(fd, &value, sizeof(value)) != sizeof(value)) { + return 1; + } + return value == expected ? 0 : 2; +} + +static int expect_poll_events(int fd, short expected) { + struct pollfd poll_fd = { + .fd = fd, + .events = POLLIN | POLLOUT, + }; + errno = 0; + int ready = poll(&poll_fd, 1, 0); + if (ready < 0) { + return 1; + } + if ((poll_fd.revents & (POLLIN | POLLOUT)) != expected) { + return 2; + } + return 0; +} + +static int expect_eagain_write(int fd, uint64_t value) { + errno = 0; + if (write(fd, &value, sizeof(value)) != -1) { + return 1; + } + return errno == EAGAIN ? 0 : 2; +} + +static int clear_nonblock_with_ioctl(int fd) { + int nonblock = 0; + return ioctl(fd, FIONBIO, &nonblock) == 0 ? 0 : 1; +} + +int main(void) { + int fd = eventfd(0, EFD_NONBLOCK); + if (fd < 0) { + return 10; + } + if (expect_poll_events(fd, POLLOUT) != 0) { + return 11; + } + if (expect_eagain_read(fd) != 0) { + return 12; + } + if (write_value(fd, 3) != 0) { + return 13; + } + if (expect_poll_events(fd, POLLIN | POLLOUT) != 0) { + return 14; + } + if (read_value(fd, 3) != 0) { + return 15; + } + if (expect_poll_events(fd, POLLOUT) != 0) { + return 16; + } + if (write_value(fd, 2) != 0) { + return 17; + } + if (write_value(fd, 5) != 0) { + return 18; + } + if (read_value(fd, 7) != 0) { + return 19; + } + if (write_value(fd, 9) != 0) { + return 20; + } + if (read_value(fd, 9) != 0) { + return 21; + } + if (write_value(fd, 11) != 0) { + return 22; + } + if (read_value(fd, 11) != 0) { + return 23; + } + if (expect_eagain_read(fd) != 0) { + return 24; + } + uint64_t invalid = UINT64_MAX; + errno = 0; + if (write(fd, &invalid, sizeof(invalid)) != -1 || errno != EINVAL) { + return 25; + } + if (write_value(fd, UINT64_MAX - 1) != 0) { + return 26; + } + if (expect_poll_events(fd, POLLIN) != 0) { + return 27; + } + if (expect_eagain_write(fd, 1) != 0) { + return 28; + } + if (read_value(fd, UINT64_MAX - 1) != 0) { + return 29; + } + if (expect_poll_events(fd, POLLOUT) != 0) { + return 30; + } + close(fd); + + int ioctl_toggle_fd = eventfd(1, EFD_NONBLOCK); + if (ioctl_toggle_fd < 0) { + return 31; + } + if (clear_nonblock_with_ioctl(ioctl_toggle_fd) != 0) { + return 32; + } + if (read_value(ioctl_toggle_fd, 1) != 0) { + return 33; + } + close(ioctl_toggle_fd); + + int semaphore_fd = eventfd(0, EFD_NONBLOCK | EFD_SEMAPHORE); + if (semaphore_fd < 0) { + return 40; + } + if (expect_poll_events(semaphore_fd, POLLOUT) != 0) { + return 41; + } + if (write_value(semaphore_fd, 3) != 0) { + return 42; + } + if (expect_poll_events(semaphore_fd, POLLIN | POLLOUT) != 0) { + return 43; + } + if (read_value(semaphore_fd, 1) != 0) { + return 44; + } + if (expect_poll_events(semaphore_fd, POLLIN | POLLOUT) != 0) { + return 45; + } + if (read_value(semaphore_fd, 1) != 0) { + return 46; + } + if (read_value(semaphore_fd, 1) != 0) { + return 47; + } + if (expect_poll_events(semaphore_fd, POLLOUT) != 0) { + return 48; + } + if (expect_eagain_read(semaphore_fd) != 0) { + return 49; + } + close(semaphore_fd); + + return 0; +} diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 3df36a8502..912a523f02 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -9,6 +9,9 @@ use std::{ path::{Path, PathBuf}, }; +const BROKER_HELPER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); +const BROKER_ONLY_C_TESTS: &[&str] = &["eventfd.c"]; + #[must_use] struct Runner { command: std::process::Command, @@ -119,6 +122,12 @@ impl Runner { self } + #[cfg(all(target_arch = "x86_64", target_os = "linux"))] + fn broker_socket(&mut self, socket_path: &Path) -> &mut Self { + self.command.arg("--broker-socket").arg(socket_path); + self + } + #[cfg_attr(not(target_arch = "x86_64"), expect(dead_code))] fn with_fs_path(&mut self, f: impl FnOnce(&Path)) -> &mut Self { f(&self.tar_dir); @@ -186,9 +195,18 @@ fn find_c_test_files(dir: &str) -> Vec { files } +fn is_broker_only_c_test(path: &Path) -> bool { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| BROKER_ONLY_C_TESTS.contains(&name)) +} + #[test] fn test_dynamic_lib_with_rewriter() { for path in find_c_test_files("./tests") { + if is_broker_only_c_test(&path) { + continue; + } let stem = path .file_stem() .and_then(|s| s.to_str()) @@ -202,6 +220,9 @@ fn test_dynamic_lib_with_rewriter() { #[test] fn test_static_exec_with_rewriter() { for path in find_c_test_files("./tests") { + if is_broker_only_c_test(&path) { + continue; + } let stem = path .file_stem() .and_then(|s| s.to_str()) @@ -226,6 +247,198 @@ fn run_which(prog: &str) -> std::path::PathBuf { prog_path } +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +fn unique_test_socket_path(name: &str) -> PathBuf { + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + std::env::temp_dir().join(format!( + "litebox-{name}-{}-{nonce}.sock", + std::process::id() + )) +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +struct TestBroker { + thread: Option>, + done_rx: std::sync::mpsc::Receiver<()>, + event_request_count_rx: std::sync::mpsc::Receiver, + socket_path: PathBuf, +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl TestBroker { + fn next_event_request_count(&self) -> usize { + self.event_request_count_rx + .recv_timeout(BROKER_HELPER_TIMEOUT) + .expect("broker test host did not report event request count") + } + + fn join(mut self) { + self.done_rx + .recv_timeout(BROKER_HELPER_TIMEOUT) + .expect("broker test host did not finish"); + self.thread + .take() + .expect("broker test host thread missing") + .join() + .expect("broker test host panicked"); + let _ = std::fs::remove_file(&self.socket_path); + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl Drop for TestBroker { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.socket_path); + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +fn spawn_test_broker( + socket_path: &Path, + policy: litebox_broker_core::PolicyEngine, + connection_count: usize, +) -> TestBroker { + let _ = std::fs::remove_file(socket_path); + + let (ready_tx, ready_rx) = std::sync::mpsc::channel(); + let (done_tx, done_rx) = std::sync::mpsc::channel(); + let (event_request_count_tx, event_request_count_rx) = std::sync::mpsc::channel(); + let server_socket_path = socket_path.to_path_buf(); + let cleanup_socket_path = socket_path.to_path_buf(); + let broker_thread = std::thread::spawn(move || { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let listener = std::os::unix::net::UnixListener::bind(&server_socket_path) + .expect("failed to bind broker test socket"); + let mut core = + litebox_broker_core::BrokerCore::new(policy).expect("failed to create broker core"); + ready_tx.send(()).expect("failed to report broker ready"); + + for _ in 0..connection_count { + let (stream, _) = listener + .accept() + .expect("failed to accept broker local control connection"); + stream + .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) + .expect("failed to configure broker test read timeout"); + stream + .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) + .expect("failed to configure broker test write timeout"); + let mut channel = CountingHostControlChannel::new( + litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream), + ); + let termination = litebox_broker_host::serve_connection(&mut core, &mut channel) + .expect("broker host failed"); + assert_eq!( + termination, + litebox_broker_host::ConnectionTermination::PeerClosed + ); + event_request_count_tx + .send(channel.event_request_count()) + .expect("failed to report broker event request count"); + } + })); + let _ = std::fs::remove_file(&server_socket_path); + let _ = done_tx.send(()); + if let Err(panic) = result { + std::panic::resume_unwind(panic); + } + }); + + ready_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("broker test host did not start"); + TestBroker { + thread: Some(broker_thread), + done_rx, + event_request_count_rx, + socket_path: cleanup_socket_path, + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +struct CountingHostControlChannel { + inner: T, + event_request_count: usize, +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl CountingHostControlChannel { + const fn new(inner: T) -> Self { + Self { + inner, + event_request_count: 0, + } + } + + const fn event_request_count(&self) -> usize { + self.event_request_count + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl litebox_broker_protocol::HostControlChannel for CountingHostControlChannel +where + T: litebox_broker_protocol::HostControlChannel, +{ + type Error = T::Error; + + fn peer_credential(&self) -> Result { + self.inner.peer_credential() + } + + fn recv_request( + &mut self, + ) -> Result, Self::Error> { + let received = self.inner.recv_request()?; + if matches!( + received, + Some(litebox_broker_protocol::ReceivedBrokerRequest::Request( + litebox_broker_protocol::BrokerRequest::Core( + litebox_broker_protocol::CoreRequest::Event(_) + ) + )) + ) { + self.event_request_count += 1; + } + Ok(received) + } + + fn send_response( + &mut self, + response: &litebox_broker_protocol::BrokerResponse, + ) -> Result<(), Self::Error> { + self.inner.send_response(response) + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[test] +fn test_runner_broker_integration_with_rewriter() { + let true_path = run_which("true"); + let target = common::compile("./tests/eventfd.c", "broker_eventfd_rewriter", false, false); + let socket_path = unique_test_socket_path("runner-broker"); + let broker_thread = spawn_test_broker( + &socket_path, + litebox_broker_core::PolicyEngine::event_only(), + 2, + ); + + Runner::new(&true_path, "broker_true_rewriter") + .broker_socket(&socket_path) + .run(); + assert_eq!(broker_thread.next_event_request_count(), 0); + + Runner::new(&target, "broker_eventfd_rewriter") + .broker_socket(&socket_path) + .run(); + assert!(broker_thread.next_event_request_count() > 0); + + broker_thread.join(); +} + #[cfg(target_arch = "x86_64")] #[test] fn test_node_with_rewriter() { diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 09835c5415..9fa295a244 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -14,10 +14,10 @@ extern crate alloc; +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; -use alloc::sync::Arc; use core::cell::{Cell, RefCell}; use litebox::{ LiteBox, @@ -165,10 +165,13 @@ impl LinuxShimBuilder { /// Returns a new shim builder. pub fn new() -> Self { let platform = litebox_platform_multiplex::platform(); - Self { - platform, - litebox: LiteBox::new(platform), - } + Self::new_with_litebox(LiteBox::new(platform)) + } + + /// Returns a new shim builder using an already-created LiteBox instance. + pub fn new_with_litebox(litebox: LiteBox) -> Self { + let platform = litebox_platform_multiplex::platform(); + Self { platform, litebox } } /// Returns the litebox object for the shim. diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 3c9d30077c..df1aad275c 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -635,7 +635,10 @@ mod test { #[test] fn test_epoll_with_eventfd() { let (task, epoll) = setup_epoll(); - let eventfd = crate::syscalls::eventfd::EventFile::new(0, EfdFlags::CLOEXEC); + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::CLOEXEC) + .unwrap(); let typed = task .global .litebox @@ -658,8 +661,7 @@ mod test { ) .unwrap(); - // spawn a thread to write to the eventfd - { + let writer = { let global = task.global.clone(); let files = Arc::clone(&files); std::thread::spawn(move || { @@ -674,11 +676,12 @@ mod test { .with_entry(&typed, |entry| { entry.write(&WaitState::new(platform()).context(), 1) }); - }); - } + }) + }; epoll .wait(&task.global, &WaitState::new(platform()).context(), 1024) .unwrap(); + writer.join().unwrap(); } #[test] @@ -730,7 +733,10 @@ mod test { let task = crate::syscalls::tests::init_platform(None); let mut set = super::PollSet::with_capacity(0); - let eventfd = crate::syscalls::eventfd::EventFile::new(0, EfdFlags::empty()); + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(); let typed = task .global diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 1c18bc8edb..0102a5a8e6 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -8,6 +8,7 @@ use core::sync::atomic::AtomicU32; use litebox::{ event::{ Events, IOPollable, + counter::{EventCounter, EventCounterError, EventCounterReadMode}, observer::Observer, polling::{Pollee, TryOpError}, wait::WaitContext, @@ -15,10 +16,11 @@ use litebox::{ fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, fs::OFlags, platform::TimeProvider, - sync::RawSyncPrimitivesProvider, + sync::{Mutex, RawSyncPrimitivesProvider}, }; use litebox_common_linux::{EfdFlags, errno::Errno}; -use litebox_platform_multiplex::Platform; + +use crate::{GlobalState, Platform, ShimFS}; pub(crate) struct EventfdSubsystem; impl FdEnabledSubsystem for EventfdSubsystem { @@ -26,101 +28,205 @@ impl FdEnabledSubsystem for EventfdSubsystem { } impl FdEnabledSubsystemEntry for EventFile {} +/// Backing counter for a Linux eventfd file description. +/// +/// New blocking eventfds still use the shim-local implementation to keep the +/// initial broker-backed scope narrow. Broker-backed nonblocking eventfds can +/// still be switched to blocking mode because the local-core counter can block +/// through LiteBox-local readiness notifications. +enum EventFileCounter { + ShimLocal { + count: Mutex, + pollee: Pollee, + }, + LocalCore(EventCounter), +} + pub(crate) struct EventFile { - counter: litebox::sync::Mutex, + counter: EventFileCounter, /// File status flags (see [`OFlags::STATUS_FLAGS_MASK`]) status: AtomicU32, semaphore: bool, - pollee: Pollee, } -impl EventFile { - pub(crate) fn new(count: u64, flags: EfdFlags) -> Self { - let mut status = OFlags::RDWR; - status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); - - Self { - counter: litebox::sync::Mutex::new(count), - status: AtomicU32::new(status.bits()), - semaphore: flags.contains(EfdFlags::SEMAPHORE), +impl EventFileCounter { + fn shim_local(count: u64) -> Self { + Self::ShimLocal { + count: Mutex::new(count), pollee: Pollee::new(), } } - fn try_read(&self) -> Result> { - let mut counter = self.counter.lock(); - if *counter == 0 { + fn local_core(counter: EventCounter) -> Self { + Self::LocalCore(counter) + } + + fn read( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + semaphore: bool, + ) -> Result { + match self { + Self::ShimLocal { count, pollee } => pollee + .wait(cx, nonblock, Events::IN, || { + Self::try_read_local(count, pollee, semaphore) + }) + .map_err(Errno::from), + Self::LocalCore(counter) => counter + .read(cx, nonblock, consume_mode(semaphore)) + .map_err(Errno::from), + } + } + + fn write( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + value: u64, + ) -> Result { + match self { + Self::ShimLocal { count, pollee } => pollee + .wait(cx, nonblock, Events::OUT, || { + Self::try_write_local(count, pollee, value) + }) + .map_err(Errno::from), + Self::LocalCore(counter) => counter.write(cx, nonblock, value).map_err(Errno::from), + } + } + + fn try_read_local( + count: &Mutex, + pollee: &Pollee, + semaphore: bool, + ) -> Result> { + let mut count = count.lock(); + if *count == 0 { return Err(TryOpError::TryAgain); } - let res = if self.semaphore { 1 } else { *counter }; - *counter -= res; + let res = if semaphore { 1 } else { *count }; + *count -= res; - drop(counter); - self.pollee.notify_observers(Events::OUT); + drop(count); + pollee.notify_observers(Events::OUT); Ok(res) } - pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { - self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::IN, - || self.try_read(), - ) - .map_err(Errno::from) - } - - fn try_write(&self, value: u64) -> Result> { - let mut counter = self.counter.lock(); - if let Some(new_value) = (*counter).checked_add(value) { - // The maximum value that may be stored in the counter is the largest unsigned - // 64-bit value minus 1 (i.e., 0xfffffffffffffffe) - if new_value != u64::MAX { - *counter = new_value; - drop(counter); - self.pollee.notify_observers(Events::IN); - return Ok(8); - } + fn try_write_local( + count: &Mutex, + pollee: &Pollee, + value: u64, + ) -> Result> { + if value == u64::MAX { + return Err(TryOpError::Other(Errno::EINVAL)); + } + + let mut count = count.lock(); + if let Some(new_value) = (*count).checked_add(value) + && new_value != u64::MAX + { + *count = new_value; + drop(count); + pollee.notify_observers(Events::IN); + return Ok(core::mem::size_of::()); } Err(TryOpError::TryAgain) } + fn check_io_events(&self) -> Events { + match self { + Self::ShimLocal { count, .. } => { + let count = count.lock(); + let mut events = Events::empty(); + if *count != 0 { + events |= Events::IN; + } + if *count < u64::MAX - 1 { + events |= Events::OUT; + } + events + } + Self::LocalCore(counter) => counter.check_io_events(), + } + } + + fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { + match self { + Self::ShimLocal { pollee, .. } => pollee.register_observer(observer, mask), + Self::LocalCore(counter) => counter.register_observer(observer, mask), + } + } +} + +impl EventFile { + fn new(counter: EventFileCounter, flags: EfdFlags) -> Self { + let mut status = OFlags::RDWR; + status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); + Self { + counter, + status: AtomicU32::new(status.bits()), + semaphore: flags.contains(EfdFlags::SEMAPHORE), + } + } + + pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { + self.counter.read(cx, self.is_nonblocking(), self.semaphore) + } + pub(crate) fn write(&self, cx: &WaitContext<'_, Platform>, value: u64) -> Result { - self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::OUT, - || self.try_write(value), - ) - .map_err(Errno::from) + self.counter.write(cx, self.is_nonblocking(), value) } super::common_functions_for_file_status!(); + + fn is_nonblocking(&self) -> bool { + self.get_status().contains(OFlags::NONBLOCK) + } } impl IOPollable for EventFile { fn check_io_events(&self) -> Events { - let counter = self.counter.lock(); - let mut events = Events::empty(); - if *counter != 0 { - events |= Events::IN; - } - // if it is possible to write a value of at least "1" - // without blocking, the file is writable - let is_writable = *counter < u64::MAX - 1; - if is_writable { - events |= Events::OUT; + self.counter.check_io_events() + } + + fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { + self.counter.register_observer(observer, mask); + } +} + +impl GlobalState { + pub(crate) fn create_linux_eventfd( + &self, + initval: u32, + flags: EfdFlags, + ) -> Result, Errno> { + if flags + .intersects((EfdFlags::SEMAPHORE | EfdFlags::CLOEXEC | EfdFlags::NONBLOCK).complement()) + { + return Err(Errno::EINVAL); } - events + let count = u64::from(initval); + let counter = if flags.contains(EfdFlags::NONBLOCK) { + match EventCounter::new(&self.litebox, count) { + Ok(counter) => EventFileCounter::local_core(counter), + Err(EventCounterError::Unavailable) => EventFileCounter::shim_local(count), + Err(error) => return Err(error.into()), + } + } else { + EventFileCounter::shim_local(count) + }; + Ok(EventFile::new(counter, flags)) } +} - fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { - self.pollee.register_observer(observer, mask); +fn consume_mode(semaphore: bool) -> EventCounterReadMode { + if semaphore { + EventCounterReadMode::One + } else { + EventCounterReadMode::All } } @@ -134,30 +240,43 @@ mod tests { #[test] fn test_semaphore_eventfd() { - let _task = crate::syscalls::tests::init_platform(None); + let task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new(super::EventFile::new(0, EfdFlags::SEMAPHORE)); + let eventfd = alloc::sync::Arc::new( + task.global + .create_linux_eventfd(0, EfdFlags::SEMAPHORE) + .unwrap(), + ); let total = 8; - for _ in 0..total { - let copied_eventfd = eventfd.clone(); - std::thread::spawn(move || { - copied_eventfd - .read(&WaitState::new(platform()).context()) - .unwrap(); - }); - } + let handles: std::vec::Vec<_> = (0..total) + .map(|_| { + let copied_eventfd = eventfd.clone(); + std::thread::spawn(move || { + copied_eventfd + .read(&WaitState::new(platform()).context()) + .unwrap(); + }) + }) + .collect(); std::thread::sleep(core::time::Duration::from_millis(500)); eventfd .write(&WaitState::new(platform()).context(), total) .unwrap(); + for handle in handles { + handle.join().unwrap(); + } } #[test] fn test_blocking_eventfd() { - let _task = crate::syscalls::tests::init_platform(None); + let task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new(super::EventFile::new(0, EfdFlags::empty())); + let eventfd = alloc::sync::Arc::new( + task.global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(), + ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { copied_eventfd @@ -180,9 +299,13 @@ mod tests { #[test] fn test_blocking_eventfd_no_race_on_massive_readwrite() { - let _task = crate::syscalls::tests::init_platform(None); + let task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new(super::EventFile::new(0, EfdFlags::empty())); + let eventfd = alloc::sync::Arc::new( + task.global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(), + ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { for _ in 0..10000 { @@ -199,46 +322,21 @@ mod tests { } #[test] - fn test_nonblocking_eventfd() { - let _task = crate::syscalls::tests::init_platform(None); - - let eventfd = alloc::sync::Arc::new(super::EventFile::new(0, EfdFlags::NONBLOCK)); - let copied_eventfd = eventfd.clone(); - std::thread::spawn(move || { - // first write should succeed immediately - copied_eventfd - .write(&WaitState::new(platform()).context(), 1) - .unwrap(); - // block until the first read finishes - while let Err(e) = - copied_eventfd.write(&WaitState::new(platform()).context(), u64::MAX - 1) - { - assert_eq!(e, Errno::EAGAIN, "Unexpected error: {e:?}"); - core::hint::spin_loop(); - } - }); - - let read = |eventfd: &super::EventFile, - expected_value: u64| { - loop { - match eventfd.read(&WaitState::new(platform()).context()) { - Ok(ret) => { - assert_eq!(ret, expected_value); - break; - } - Err(Errno::EAGAIN) => { - // busy wait - // TODO: use poll rather than busy wait - } - Err(e) => panic!("Unexpected error: {e:?}"), - } - core::hint::spin_loop(); - } - }; + fn test_nonblocking_eventfd_uses_shim_local_without_broker_control() { + let task = crate::syscalls::tests::init_platform(None); - // block until the first write - read(&eventfd, 1); - // block until the second write - read(&eventfd, u64::MAX - 1); + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::NONBLOCK) + .unwrap(); + assert_eq!( + eventfd.read(&WaitState::new(platform()).context()), + Err(Errno::EAGAIN) + ); + assert_eq!( + eventfd.write(&WaitState::new(platform()).context(), 1), + Ok(8) + ); + assert_eq!(eventfd.read(&WaitState::new(platform()).context()), Ok(1)); } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index a857477f4f..3560d8956c 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -1734,7 +1734,7 @@ impl Task { return Err(Errno::EINVAL); } - let eventfd = super::eventfd::EventFile::new(u64::from(initval), flags); + let eventfd = self.global.create_linux_eventfd(initval, flags)?; let mut dt = self.global.litebox.descriptor_table_mut(); let typed = dt.insert::(eventfd); if flags.contains(EfdFlags::CLOEXEC) { From 27922439e1a0085495277efe3cba5d26c659fe4e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 17 Jun 2026 15:25:25 -0700 Subject: [PATCH 035/319] Clean up broker eventfd helpers (#926) Inline small helper wrappers and remove the always-true event counter blocking capability flag. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 26 +++------- litebox/src/event/counter.rs | 7 --- litebox/src/litebox.rs | 4 +- litebox_broker_host/src/lib.rs | 24 ++++++--- litebox_shim_linux/src/syscalls/eventfd.rs | 57 ++++++++++------------ 5 files changed, 51 insertions(+), 67 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index e3edc541fe..537f7554d5 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -24,7 +24,7 @@ pub trait BrokerControl: Send + Sync { ) -> core::result::Result; } -struct BrokerLocalControl { +pub(crate) struct BrokerLocalControl { local: Mutex>, } @@ -32,7 +32,7 @@ impl BrokerLocalControl where Platform: RawSyncPrimitivesProvider, { - const fn new(local: BrokerLocal) -> Self { + pub(crate) const fn new(local: BrokerLocal) -> Self { Self { local: Mutex::new(local), } @@ -51,26 +51,14 @@ where self.local .lock() .active_core_request(request) - .map_err(broker_control_error) + .map_err(|error| match error { + BrokerLocalError::Broker(error) => BrokerControlError::Broker(error), + BrokerLocalError::UnexpectedResponse(_) => BrokerControlError::UnexpectedResponse, + _ => BrokerControlError::Transport, + }) } } -fn broker_control_error(error: BrokerLocalError) -> BrokerControlError { - match error { - BrokerLocalError::Broker(error) => BrokerControlError::Broker(error), - BrokerLocalError::UnexpectedResponse(_) => BrokerControlError::UnexpectedResponse, - _ => BrokerControlError::Transport, - } -} - -pub(crate) fn control_from_local(local: BrokerLocal) -> Arc -where - Platform: RawSyncPrimitivesProvider, - T: LocalControlChannel + Send + 'static, -{ - Arc::new(BrokerLocalControl::::new(local)) -} - pub(crate) struct BrokerState { control: Option>, _marker: core::marker::PhantomData, diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index c6bbfe447a..711d36dca3 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -47,7 +47,6 @@ pub struct EventCounter { broker: Arc, handle: ObjectHandle, pollee: Pollee, - blocking_operations_supported: bool, } impl EventCounter @@ -73,15 +72,9 @@ where broker, handle: response.handle, pollee: Pollee::new(), - blocking_operations_supported: true, }) } - /// Returns whether blocking reads and writes are supported. - pub fn supports_blocking_operations(&self) -> bool { - self.blocking_operations_supported - } - /// Reads the event counter. pub fn read( &self, diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 35cca92393..4de3768ceb 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -55,7 +55,9 @@ impl LiteBox { { Self::new_inner( platform, - Some(broker::control_from_local::(broker_local)), + Some(Arc::new(broker::BrokerLocalControl::::new( + broker_local, + ))), ) } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index e0cc97d7fc..d63cc13e8d 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -160,20 +160,32 @@ fn handle_event_request( match request { EventRequest::Create(request) => handle_core_result( core.create_event_with_count(association, request.initial_count), - |handle| event_response(EventResponse::Create(CreateEventResponse::new(handle))), + |handle| { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create( + CreateEventResponse::new(handle), + ))) + }, ), EventRequest::Wait(request) => { handle_core_result(core.wait_event(association, request.handle), |outcome| { - event_response(EventResponse::Wait(WaitEventResponse::new(outcome))) + BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( + WaitEventResponse::new(outcome), + ))) }) } EventRequest::Add(request) => handle_core_result( core.add_event(association, request.handle, request.value), - |readiness| event_response(EventResponse::Add(AddEventResponse::new(readiness))), + |readiness| { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Add( + AddEventResponse::new(readiness), + ))) + }, ), EventRequest::Consume(request) => handle_core_result( core.consume_event(association, request.handle, request.mode), - |consumption| event_response(EventResponse::Consume(consumption)), + |consumption| { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(consumption))) + }, ), _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), } @@ -218,10 +230,6 @@ fn handle_core_result( } } -const fn event_response(response: EventResponse) -> BrokerResponse { - BrokerResponse::Core(CoreResponse::Event(response)) -} - fn to_protocol_error(error: BrokerError) -> ErrorCode { match error { BrokerError::PolicyDenied => ErrorCode::PolicyDenied, diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 0102a5a8e6..37c4757b20 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -57,10 +57,6 @@ impl EventFileCounter) -> Self { - Self::LocalCore(counter) - } - fn read( &self, cx: &WaitContext<'_, Platform>, @@ -74,7 +70,15 @@ impl EventFileCounter counter - .read(cx, nonblock, consume_mode(semaphore)) + .read( + cx, + nonblock, + if semaphore { + EventCounterReadMode::One + } else { + EventCounterReadMode::All + }, + ) .map_err(Errno::from), } } @@ -211,7 +215,7 @@ impl GlobalState { let count = u64::from(initval); let counter = if flags.contains(EfdFlags::NONBLOCK) { match EventCounter::new(&self.litebox, count) { - Ok(counter) => EventFileCounter::local_core(counter), + Ok(counter) => EventFileCounter::LocalCore(counter), Err(EventCounterError::Unavailable) => EventFileCounter::shim_local(count), Err(error) => return Err(error.into()), } @@ -222,14 +226,6 @@ impl GlobalState { } } -fn consume_mode(semaphore: bool) -> EventCounterReadMode { - if semaphore { - EventCounterReadMode::One - } else { - EventCounterReadMode::All - } -} - #[cfg(test)] mod tests { use litebox::event::wait::WaitState; @@ -240,13 +236,12 @@ mod tests { #[test] fn test_semaphore_eventfd() { - let task = crate::syscalls::tests::init_platform(None); + let _task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new( - task.global - .create_linux_eventfd(0, EfdFlags::SEMAPHORE) - .unwrap(), - ); + let eventfd = alloc::sync::Arc::new(super::EventFile::new( + super::EventFileCounter::shim_local(0), + EfdFlags::SEMAPHORE, + )); let total = 8; let handles: std::vec::Vec<_> = (0..total) .map(|_| { @@ -270,13 +265,12 @@ mod tests { #[test] fn test_blocking_eventfd() { - let task = crate::syscalls::tests::init_platform(None); + let _task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new( - task.global - .create_linux_eventfd(0, EfdFlags::empty()) - .unwrap(), - ); + let eventfd = alloc::sync::Arc::new(super::EventFile::new( + super::EventFileCounter::shim_local(0), + EfdFlags::empty(), + )); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { copied_eventfd @@ -299,13 +293,12 @@ mod tests { #[test] fn test_blocking_eventfd_no_race_on_massive_readwrite() { - let task = crate::syscalls::tests::init_platform(None); + let _task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new( - task.global - .create_linux_eventfd(0, EfdFlags::empty()) - .unwrap(), - ); + let eventfd = alloc::sync::Arc::new(super::EventFile::new( + super::EventFileCounter::shim_local(0), + EfdFlags::empty(), + )); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { for _ in 0..10000 { From aff4ecd8cddf83ad87e05840124cfbd8ae48d75e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 17 Jun 2026 16:18:57 -0700 Subject: [PATCH 036/319] Add a Linux runner for Windows shim (#929) This PR adds a Linux userland runner for the Windows shim so that we can ensure that Windows shim is host agnostic. This is especially important for coding agents because they keep using host APIs to cut corners. With more comprehensive tests, I believe less human intervention is needed. To run the hello-world program from powershell ``` wsl.exe bash -lc 'cd /mnt/c/Users/weitengchen/work/litebox && LITEBOX_LOG=debug cargo run -q -p litebox_runner_windows_on_linux_userland -- --initial-files /mnt/c/Users/weitengchen/work/litebox/target/tmp/no_import.tar /no_import.exe' ``` Both the tar and PE files are generated by the test `loads_minimal_pe_without_imports` running on Windows. --- .github/workflows/ci.yml | 5 + Cargo.lock | 16 ++ Cargo.toml | 2 + .../Cargo.toml | 21 +++ .../src/lib.rs | 135 ++++++++++++++++ .../src/main.rs | 15 ++ litebox_shim_windows/src/loader/pe.rs | 145 +++++++++++++++++- 7 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 litebox_runner_windows_on_linux_userland/Cargo.toml create mode 100644 litebox_runner_windows_on_linux_userland/src/lib.rs create mode 100644 litebox_runner_windows_on_linux_userland/src/main.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ee5fcc701..caf96d0399 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -250,6 +250,10 @@ jobs: # access since it needs to actually access the file-system, pull in # relevant files, and then actually trigger LiteBox itself. # + # - `litebox_runner_windows_on_linux_userland` is allowed to have `std` + # access since it needs to actually access the file-system, pull in + # relevant files, and then actually trigger LiteBox itself. + # # - `litebox_runner_linux_userland` is allowed to have `std` access # since it needs to actually access the file-system, pull in # relevant files, and then actually trigger LiteBox itself. @@ -297,6 +301,7 @@ jobs: -not -path './litebox_platform_linux_userland/Cargo.toml' \ -not -path './litebox_platform_windows_userland/Cargo.toml' \ -not -path './litebox_runner_linux_on_windows_userland/Cargo.toml' \ + -not -path './litebox_runner_windows_on_linux_userland/Cargo.toml' \ -not -path './litebox_platform_lvbs/Cargo.toml' \ -not -path './litebox_platform_multiplex/Cargo.toml' \ -not -path './litebox_runner_linux_userland/Cargo.toml' \ diff --git a/Cargo.lock b/Cargo.lock index 01fde33766..ca763d2838 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1764,6 +1764,22 @@ dependencies = [ "log", ] +[[package]] +name = "litebox_runner_windows_on_linux_userland" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "litebox", + "litebox_common_linux", + "litebox_common_windows", + "litebox_platform_linux_userland", + "litebox_shim_windows", + "litebox_util_log", + "tar", + "tracing-subscriber", +] + [[package]] name = "litebox_runner_windows_userland" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6456081c01..26ae2d6760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "litebox_platform_multiplex", "litebox_runner_linux_userland", "litebox_runner_linux_on_windows_userland", + "litebox_runner_windows_on_linux_userland", "litebox_runner_windows_userland", "litebox_runner_lvbs", "litebox_runner_optee_on_linux_userland", @@ -51,6 +52,7 @@ default-members = [ "litebox_platform_multiplex", "litebox_runner_linux_userland", "litebox_runner_linux_on_windows_userland", + "litebox_runner_windows_on_linux_userland", "litebox_runner_windows_userland", "litebox_shim_linux", "litebox_shim_windows", diff --git a/litebox_runner_windows_on_linux_userland/Cargo.toml b/litebox_runner_windows_on_linux_userland/Cargo.toml new file mode 100644 index 0000000000..f200cc6a2f --- /dev/null +++ b/litebox_runner_windows_on_linux_userland/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "litebox_runner_windows_on_linux_userland" +version = "0.1.0" +edition = "2024" + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies] +anyhow = "1.0.97" +clap = { version = "4.5.33", features = ["derive"] } +litebox = { version = "0.1.0", path = "../litebox" } +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" } +litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = ["backend_tracing"] } +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dev-dependencies] +litebox_common_windows = { version = "0.1.0", path = "../litebox_common_windows" } +tar = "0.4" + +[lints] +workspace = true diff --git a/litebox_runner_windows_on_linux_userland/src/lib.rs b/litebox_runner_windows_on_linux_userland/src/lib.rs new file mode 100644 index 0000000000..83022065a5 --- /dev/null +++ b/litebox_runner_windows_on_linux_userland/src/lib.rs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Restrict this crate to only work on Linux. For now, we are restricting this to only x86-64 +// Linux, but we _may_ allow for more in the future, if we find it useful to do so. +#![cfg(all(target_os = "linux", target_arch = "x86_64"))] + +extern crate alloc; + +use anyhow::{Context as _, Result}; +use clap::Parser; +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. +#[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`. + #[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). + #[arg(long = "env")] + pub environment_variables: Vec, + /// Forward the existing environment variables. + #[arg(long = "forward-env")] + pub forward_environment_variables: bool, + /// 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, +} + +/// 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<()> { + tracing_subscriber::fmt() + .with_timer(tracing_subscriber::fmt::time::uptime()) + .with_level(true) + .with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_env_var("LITEBOX_LOG") + .from_env_lossy(), + ) + .init(); + + if cli_args.unstable { + litebox_util_log::warn!( + "Windows PE on Linux runner is currently a skeleton; shim functionality is not implemented yet" + ); + } + + 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(None); + let shim_builder = litebox_shim_windows::WindowsShimBuilder::new(platform); + let litebox = shim_builder.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 mut in_mem = litebox::fs::in_mem::FileSystem::new(litebox); + in_mem.with_root_privileges(|fs| { + use litebox::fs::FileSystem as _; + fs.mkdir( + "/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("/tmp", Some(1000), Some(1000)) + .expect("/tmp chown cannot fail on a fresh in-memory file system"); + }); + + let tar_ro = litebox::fs::tar_ro::FileSystem::new(litebox, tar_data.into()); + shim_builder.default_fs(in_mem, tar_ro) + }; + 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)) + .map(to_cstring) + .collect::>>() + .context("argv contained an interior NUL byte")?; + let mut envp = cli_args + .environment_variables + .iter() + .map(|s| to_cstring(s)) + .collect::>>() + .context("--env value contained an interior NUL byte")?; + if cli_args.forward_environment_variables { + for (key, value) in std::env::vars() { + envp.push( + to_cstring(&format!("{key}={value}")) + .context("forwarded environment variable contained an interior NUL byte")?, + ); + } + } + + let program = shim + .load_program(initial_file_system, 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()` + // we hand in is fully initialized before any guest instruction runs. + unsafe { + litebox_platform_linux_userland::run_thread( + program.entrypoints, + &mut litebox_common_linux::PtRegs::default(), + ); + } + std::process::exit(program.process.wait()) +} + +fn to_cstring(s: &str) -> Result { + std::ffi::CString::new(s.as_bytes()).map_err(Into::into) +} diff --git a/litebox_runner_windows_on_linux_userland/src/main.rs b/litebox_runner_windows_on_linux_userland/src/main.rs new file mode 100644 index 0000000000..825ddbed21 --- /dev/null +++ b/litebox_runner_windows_on_linux_userland/src/main.rs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#[cfg(all(target_os = "linux", target_arch = "x86_64"))] +fn main() -> anyhow::Result<()> { + use clap::Parser as _; + use litebox_runner_windows_on_linux_userland::CliArgs; + litebox_runner_windows_on_linux_userland::run(CliArgs::parse()) +} + +#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] +fn main() { + eprintln!("This program is only supported on Linux x86_64"); + std::process::exit(1); +} diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 7885d4f3ce..05bcc2b536 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -50,6 +50,12 @@ const WINDOWS_OS_MAJOR_VERSION: u16 = 10; const WINDOWS_OS_MINOR_VERSION: u16 = 0; const WINDOWS_OS_BUILD_NUMBER: u16 = 19041; const WINDOWS_OS_PLATFORM_WIN32_NT: u32 = 2; +#[cfg(not(target_os = "windows"))] +const WINDOWS_USER_SHARED_DATA_BASE: usize = 0x7FFE_0000; +#[cfg(not(target_os = "windows"))] +const WINDOWS_KUSER_SHARED_DATA_XSTATE_CONFIGURATION_SIZE: usize = 0x348; +#[cfg(not(target_os = "windows"))] +const WINDOWS_NT_PRODUCT_WORKSTATION: u32 = 1; const WINDOWS_TIME_ZONE_ID_INVALID: u32 = u32::MAX; const WINDOWS_CRITICAL_SECTION_TIMEOUT_100NS: i64 = -150 * 10_000_000; const WINDOWS_HEAP_SEGMENT_RESERVE: u64 = 1024 * 1024; @@ -68,6 +74,94 @@ macro_rules! write_static_server_data_field { }; } +/// Layout from Wine `include/ddk/wdm.h` and ReactOS `sdk/include/wine/ddk/wdm.h`. +#[cfg(not(target_os = "windows"))] +#[repr(C)] +#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)] +struct KUserSharedData { + tick_count_low_deprecated: u32, + tick_count_multiplier: u32, + interrupt_time: KSystemTime, + system_time: KSystemTime, + time_zone_bias: KSystemTime, + image_number_low: u16, + image_number_high: u16, + nt_system_root: [u16; 260], + max_stack_trace_depth: u32, + crypto_exponent: u32, + time_zone_id: u32, + large_page_minimum: u32, + ait_sampling_value: u32, + app_compat_flag: u32, + rng_seed_version: u64, + global_validation_run_level: u32, + time_zone_bias_stamp: u32, + nt_build_number: u32, + nt_product_type: u32, + product_type_is_valid: u8, + reserved_0: u8, + native_processor_architecture: u16, + nt_major_version: u32, + nt_minor_version: u32, + processor_features: [u8; 64], + reserved_1: u32, + reserved_3: u32, + time_slip: u32, + alternative_architecture: u32, + boot_id: u32, + system_expiration_date: i64, + suite_mask: u32, + kd_debugger_enabled: u8, + nx_support_policy: u8, + cycles_per_yield: u16, + active_console_id: u32, + dismount_count: u32, + com_plus_package: u32, + last_system_rit_event_tick_count: u32, + number_of_physical_pages: u32, + safe_boot_mode: u8, + virtualization_flags: u8, + padding_2ee: [u8; 2], + shared_data_flags: u32, + data_flags_pad: [u32; 1], + test_ret_instruction: u64, + qpc_frequency: i64, + system_call: u32, + user_cet_available_environments: u32, + system_call_pad: [u64; 2], + tick_count: [u8; 0x10], + cookie: u32, + cookie_pad: [u32; 1], + console_session_foreground_process_id: i64, + time_update_lock: u64, + baseline_system_time_qpc: u64, + baseline_interrupt_time_qpc: u64, + qpc_system_time_increment: u64, + qpc_interrupt_time_increment: u64, + qpc_system_time_increment_shift: u8, + qpc_interrupt_time_increment_shift: u8, + unparked_processor_count: u16, + enclave_feature_mask: [u32; 4], + telemetry_coverage_round: u32, + user_mode_global_logger: [u16; 16], + image_file_execution_options: u32, + lang_generation_count: u32, + active_processor_affinity: u32, + padding_3ac: u32, + interrupt_time_bias: u64, + qpc_bias: u64, + active_processor_count: u32, + active_group_count: u8, + padding_3c5: u8, + qpc_data: u16, + time_zone_bias_effective_start: i64, + time_zone_bias_effective_end: i64, + x_state: [u8; WINDOWS_KUSER_SHARED_DATA_XSTATE_CONFIGURATION_SIZE], + feature_configuration_change_stamp: KSystemTime, + spare: u32, + user_pointer_auth_mask: u64, +} + pub(crate) struct WindowsProcessEnvironment { pub(crate) peb: usize, pub(crate) teb: usize, @@ -117,6 +211,9 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { argv: &[CString], envp: &[CString], ) -> Result, WindowsLoadError> { + #[cfg(not(target_os = "windows"))] + map_windows_user_shared_data::(self.page_manager)?; + let image = load_image(self.platform, self.fs.clone(), path, self.page_manager)?; let application_entry_point = image.mapping.entry_point; let ntdll = load_ntdll(self.platform, self.fs.clone(), self.page_manager)?; @@ -785,7 +882,7 @@ struct SystemTime { } #[repr(C)] -#[derive(FromBytes, IntoBytes)] +#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)] struct KSystemTime { low_part: u32, high_1_time: i32, @@ -1030,6 +1127,52 @@ where crate::write_slice::(address, values).ok_or(PeImageAccessError::MemoryAccess) } +/// Wine and ReactOS model KUSER_SHARED_DATA as a fixed user page at +/// 0x7FFE0000. Native Windows hosts already provide that page; Non-Windows hosts +/// need LiteBox to create it before guest ntdll reads it during startup. +#[cfg(not(target_os = "windows"))] +fn map_windows_user_shared_data( + page_manager: &crate::WindowsPageManager, +) -> Result<(), PeImageAccessError> { + let address = NonZeroAddress::new(WINDOWS_USER_SHARED_DATA_BASE) + .ok_or(PeImageAccessError::AddressOverflow)?; + let length = NonZeroPageSize::new(size_of::().next_multiple_of(PAGE_SIZE)) + .ok_or(PeImageAccessError::AddressOverflow)?; + let shared_data = windows_user_shared_data(); + let shared_data_bytes = shared_data.as_bytes(); + // SAFETY: `NOREPLACE` makes the fixed mapping fail instead of replacing any + // existing host or guest mapping at the shared-data address. + unsafe { + page_manager.create_readable_pages( + Some(address), + length, + CreatePagesFlags::FIXED_ADDR | CreatePagesFlags::NOREPLACE, + |ptr| { + ptr.copy_from_slice(0, shared_data_bytes) + .ok_or(MappingError::OutOfMemory)?; + Ok(0) + }, + ) + } + .map_err(PeImageAccessError::from) + .map(|_| ()) +} + +#[cfg(not(target_os = "windows"))] +fn windows_user_shared_data() -> KUserSharedData { + let mut shared_data = KUserSharedData::new_zeroed(); + shared_data.nt_build_number = u32::from(WINDOWS_OS_BUILD_NUMBER); + shared_data.nt_product_type = WINDOWS_NT_PRODUCT_WORKSTATION; + shared_data.product_type_is_valid = 1; + shared_data.nt_major_version = u32::from(WINDOWS_OS_MAJOR_VERSION); + shared_data.nt_minor_version = u32::from(WINDOWS_OS_MINOR_VERSION); + for (index, code_unit) in WINDOWS_DIRECTORY.encode_utf16().enumerate() { + shared_data.nt_system_root[index] = code_unit; + } + + shared_data +} + struct LoadedNtDll { image: LoadedImage, exports: NtDllExports, From c6c88aaca8ff6b4c174a5886983154c4a24c7ceb Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 17 Jun 2026 16:35:50 -0700 Subject: [PATCH 037/319] Clean up LiteBox broker constructors (#928) Remove the unused broker-control constructor from LiteBox and keep broker-control plumbing internal to the local core. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 2 +- litebox/src/broker/mod.rs | 4 ++-- litebox/src/lib.rs | 1 - litebox/src/litebox.rs | 14 +++----------- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 2ffff32896..afc81bfc31 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -8,7 +8,7 @@ use crate::event::{counter::EventCounterError, polling::TryOpError}; /// Error returned by the deployment-provided broker control path. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] -pub enum BrokerControlError { +pub(crate) enum BrokerControlError { /// The broker control transport failed. Transport, /// The broker returned an operation error. diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 537f7554d5..d8aaf567c6 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -9,14 +9,14 @@ use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; pub(crate) mod error; -pub use error::BrokerControlError; +use error::BrokerControlError; /// Local-core access to the negotiated broker control channel. /// /// LiteBox owns broker-backed local objects and constructs broker protocol /// requests. Deployment code owns endpoint selection and supplies the connected /// transport behind this protocol-level boundary. -pub trait BrokerControl: Send + Sync { +pub(crate) trait BrokerControl: Send + Sync { /// Sends one active BrokerCore request and returns its response. fn request( &self, diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index f01e028f94..f78b7dc24a 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -41,4 +41,3 @@ mod utilities; pub mod utils; mod broker; -pub use broker::{BrokerControl, BrokerControlError}; diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 4de3768ceb..5b65d80373 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -9,7 +9,7 @@ use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::LocalControlChannel; use crate::{ - broker::{self, BrokerControl, BrokerState}, + broker::{self, BrokerState}, fd::Descriptors, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -37,14 +37,6 @@ impl LiteBox { Self::new_inner(platform, None) } - /// Create a new [`LiteBox`] instance with broker control installed. - pub fn new_with_broker_control( - platform: &'static Platform, - broker_control: Arc, - ) -> Self { - Self::new_inner(platform, Some(broker_control)) - } - /// Create a new [`LiteBox`] instance with a negotiated broker-local control adapter installed. pub fn new_with_broker_local( platform: &'static Platform, @@ -63,7 +55,7 @@ impl LiteBox { fn new_inner( platform: &'static Platform, - broker_control: Option>, + broker_control: Option>, ) -> Self { // This check ensures that there is exactly one `LiteBox` instance in the process. // @@ -144,7 +136,7 @@ impl LiteBox { self.x.descriptors.write() } - pub(crate) fn broker_control(&self) -> Option> { + pub(crate) fn broker_control(&self) -> Option> { self.x.broker.control() } } From 8634457d00088d5e082ca6dc80f70c12474fb69f Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 17 Jun 2026 18:22:08 -0700 Subject: [PATCH 038/319] Cherry-pick mkdirat syscall support to ulitebox (#931) Cherry-picks a193cce873b7528ab2adf72bfc80e1c92f2266bb (`Add syscall mkdirat (#898)`) onto `ulitebox`. --- litebox_common_linux/src/lib.rs | 10 +- litebox_runner_linux_userland/tests/mkdirat.c | 125 ++++++++++++++++++ litebox_shim_linux/src/lib.rs | 10 +- litebox_shim_linux/src/syscalls/file.rs | 44 ++++-- litebox_shim_linux/src/syscalls/tests.rs | 8 +- 5 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 litebox_runner_linux_userland/tests/mkdirat.c diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index ece0167d7b..5dee9a26f6 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -1987,7 +1987,8 @@ pub enum SyscallRequest { pathname: Platform::RawConstPointer, buf: Platform::RawMutPointer, }, - Mkdir { + Mkdirat { + dirfd: i32, pathname: Platform::RawConstPointer, mode: u32, }, @@ -2529,7 +2530,12 @@ impl SyscallRequest { Sysno::stat => sys_req!(Stat { pathname:*, buf:* }), Sysno::fstat => sys_req!(Fstat { fd, buf:* }), Sysno::lstat => sys_req!(Lstat { pathname:*, buf:* }), - Sysno::mkdir => sys_req!(Mkdir { pathname:*, mode }), + Sysno::mkdir => SyscallRequest::Mkdirat { + dirfd: AT_FDCWD, + pathname: ctx.sys_req_ptr(0), + mode: ctx.sys_req_arg(1), + }, + Sysno::mkdirat => sys_req!(Mkdirat { dirfd, pathname:*, mode }), Sysno::chdir => sys_req!(Chdir { pathname:* }), #[cfg(target_arch = "x86_64")] Sysno::mmap => sys_req!(Mmap { diff --git a/litebox_runner_linux_userland/tests/mkdirat.c b/litebox_runner_linux_userland/tests/mkdirat.c new file mode 100644 index 0000000000..4eecfc84f5 --- /dev/null +++ b/litebox_runner_linux_userland/tests/mkdirat.c @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "helpers.h" + +#include +#include + +#ifndef SYS_mkdirat +#error SYS_mkdirat is not defined on this build host +#endif + +static long raw_mkdirat(int dirfd, const char *pathname, mode_t mode) { + return syscall(SYS_mkdirat, dirfd, pathname, mode); +} + +static void expect_mkdirat_success(int dirfd, const char *pathname, mode_t mode, + const char *msg) { + errno = 0; + long ret = raw_mkdirat(dirfd, pathname, mode); + TEST_ASSERT(ret == 0, msg); +} + +static void expect_mkdirat_errno(int dirfd, const char *pathname, mode_t mode, + int expected_errno, const char *msg) { + errno = 0; + long ret = raw_mkdirat(dirfd, pathname, mode); + TEST_ASSERT(ret == -1, msg); + TEST_ASSERT(errno == expected_errno, msg); +} + +static void expect_directory_mode(const char *path, mode_t mode, const char *msg) { + struct stat st; + + errno = 0; + TEST_ASSERT(stat(path, &st) == 0, msg); + TEST_ASSERT(S_ISDIR(st.st_mode), "stat should observe a directory"); + TEST_ASSERT((st.st_mode & 0777) == mode, msg); +} + +static void create_regular_file(const char *path) { + int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0600); + TEST_ASSERT(fd >= 0, "create test file failed"); + TEST_ASSERT(close(fd) == 0, "close test file failed"); +} + +static void test_at_fdcwd_relative_success(void) { + const char *name = "lb_mkdirat_relative"; + const char *path = "/tmp/lb_mkdirat_relative"; + char old_cwd[4096]; + + rmdir(path); + TEST_ASSERT(getcwd(old_cwd, sizeof(old_cwd)) != NULL, "getcwd failed"); + TEST_ASSERT(chdir("/tmp") == 0, "chdir /tmp failed"); + + expect_mkdirat_success(AT_FDCWD, name, 0777, + "mkdirat AT_FDCWD relative should succeed"); + expect_directory_mode(path, 0755, + "stat should observe mkdirat AT_FDCWD relative result"); + + TEST_ASSERT(chdir(old_cwd) == 0, "restore cwd failed"); + TEST_ASSERT(rmdir(path) == 0, "cleanup relative directory failed"); +} + +static void test_absolute_path_ignores_dirfd(void) { + const char *path = "/tmp/lb_mkdirat_absolute"; + + rmdir(path); + expect_mkdirat_success(-2, path, 0700, + "mkdirat absolute path should ignore invalid dirfd"); + expect_directory_mode(path, 0700, + "stat should observe mkdirat absolute path result"); + TEST_ASSERT(rmdir(path) == 0, "cleanup absolute directory failed"); +} + +static void test_existing_path_eexist(void) { + const char *path = "/tmp/lb_mkdirat_existing"; + + rmdir(path); + TEST_ASSERT(mkdir(path, 0700) == 0, "setup existing directory failed"); + expect_mkdirat_errno(AT_FDCWD, path, 0700, EEXIST, + "mkdirat existing path should fail with EEXIST"); + TEST_ASSERT(rmdir(path) == 0, "cleanup existing directory failed"); +} + +static void test_missing_parent_enoent(void) { + const char *parent = "/tmp/lb_mkdirat_missing_parent"; + const char *path = "/tmp/lb_mkdirat_missing_parent/child"; + + rmdir(path); + rmdir(parent); + expect_mkdirat_errno(AT_FDCWD, path, 0700, ENOENT, + "mkdirat missing parent should fail with ENOENT"); +} + +static void test_component_not_directory_enotdir(void) { + const char *file = "/tmp/lb_mkdirat_file"; + const char *path = "/tmp/lb_mkdirat_file/child"; + + unlink(file); + create_regular_file(file); + expect_mkdirat_errno(AT_FDCWD, path, 0700, ENOTDIR, + "mkdirat through regular file should fail with ENOTDIR"); + TEST_ASSERT(unlink(file) == 0, "cleanup regular file failed"); +} + +static void test_empty_path_enoent(void) { + expect_mkdirat_errno(AT_FDCWD, "", 0700, ENOENT, + "mkdirat empty path should fail with ENOENT"); +} + +int main(void) { + mode_t old_umask = umask(0022); + + printf("===== mkdirat tests =====\n"); + test_at_fdcwd_relative_success(); + test_absolute_path_ignores_dirfd(); + test_existing_path_eexist(); + test_missing_parent_enoent(); + test_component_not_directory_enotdir(); + test_empty_path_enoent(); + umask(old_umask); + printf("All mkdirat tests passed.\n"); + return 0; +} \ No newline at end of file diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 9fa295a244..54dda0669a 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -596,9 +596,13 @@ impl Task { .map_err(|_| Errno::EINVAL) .and_then(|seekwhence| self.sys_lseek(fd, offset, seekwhence)) } - SyscallRequest::Mkdir { pathname, mode } => pathname - .to_cstring() - .map_or(Err(Errno::EINVAL), |path| syscall!(sys_mkdir(path, mode))), + SyscallRequest::Mkdirat { + dirfd, + pathname, + mode, + } => pathname.to_cstring().map_or(Err(Errno::EFAULT), |path| { + syscall!(sys_mkdirat(dirfd, path, mode)) + }), SyscallRequest::Chdir { pathname } => pathname .to_cstring() .map_or(Err(Errno::EINVAL), |path| syscall!(sys_chdir(path))), diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 3560d8956c..a270c4b184 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -718,10 +718,8 @@ impl Task { .flatten() } - /// Handle syscall `mkdir` - pub fn sys_mkdir(&self, pathname: impl path::Arg, mode: u32) -> Result<(), Errno> { - let pathname = self.resolve_path(pathname)?; - let mode = Mode::from_bits_retain(mode) & !self.get_umask(); + fn do_mkdir(&self, pathname: impl path::Arg, mode: Mode) -> Result<(), Errno> { + let mode = mode & !self.get_umask(); self.files .borrow() .fs @@ -729,6 +727,17 @@ impl Task { .map_err(Errno::from) } + /// Handle syscall `mkdirat` + pub(crate) fn sys_mkdirat( + &self, + dirfd: i32, + pathname: impl path::Arg, + mode: u32, + ) -> Result<(), Errno> { + let pathname = self.resolve_path_at(dirfd, pathname)?; + self.do_mkdir(pathname, Mode::from_bits_retain(mode)) + } + pub(crate) fn do_close(&self, raw_fd: usize) -> Result<(), Errno> { self.do_close_and_replace::(raw_fd, None) } @@ -2726,7 +2735,8 @@ mod tests { assert_eq!(cwd, "/"); // chdir + getcwd round trip. - task.sys_mkdir("/test_chdir_dir", 0o777).unwrap(); + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "/test_chdir_dir", 0o777) + .unwrap(); task.sys_chdir("/test_chdir_dir").unwrap(); let len = task.sys_getcwd(&mut buf).unwrap(); let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); @@ -2762,8 +2772,14 @@ mod tests { let task = crate::syscalls::tests::init_platform(None); // Create nested dirs: /rel_parent/rel_child - task.sys_mkdir("/rel_parent", 0o777).unwrap(); - task.sys_mkdir("/rel_parent/rel_child", 0o777).unwrap(); + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "/rel_parent", 0o777) + .unwrap(); + task.sys_mkdirat( + litebox_common_linux::AT_FDCWD, + "/rel_parent/rel_child", + 0o777, + ) + .unwrap(); // chdir to /rel_parent first, then relative chdir into child. task.sys_chdir("/rel_parent").unwrap(); @@ -2831,7 +2847,11 @@ mod tests { .unwrap_err(), Errno::ENOENT ); - assert_eq!(task.sys_mkdir("", 0o755).unwrap_err(), Errno::ENOENT); + assert_eq!( + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "", 0o755) + .unwrap_err(), + Errno::ENOENT + ); assert_eq!( task.sys_mknodat( litebox_common_linux::AT_FDCWD, @@ -2858,7 +2878,8 @@ mod tests { let task = crate::syscalls::tests::init_platform(None); // Set up: mkdir + chdir into /cwd_test/. - task.sys_mkdir("/cwd_test", 0o777).unwrap(); + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "/cwd_test", 0o777) + .unwrap(); task.sys_chdir("/cwd_test").unwrap(); // ── sys_open: create a file via relative path ── @@ -2886,8 +2907,9 @@ mod tests { ) .unwrap(); - // ── sys_mkdir: create a subdirectory via relative path ── - task.sys_mkdir("subdir", 0o777).unwrap(); + // ── create a subdirectory via relative path ── + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "subdir", 0o777) + .unwrap(); task.sys_stat("/cwd_test/subdir").unwrap(); // verify via absolute // ── sys_openat (AT_FDCWD + relative): open inside the new subdir ── diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 81f4e07ed0..661e9dfd41 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -433,7 +433,7 @@ fn test_umask_behavior() { // 3. Create a directory with mode 0o777; with umask 0o077 should become 0o700. let dir_mode = (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits(); let test_dir = "/umask_rs_test_dir"; - task.sys_mkdir(test_dir, dir_mode) + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, test_dir, dir_mode) .expect("Failed to create test directory"); let stat_dir = task @@ -536,7 +536,7 @@ fn test_unlinkat() { // 2. Create a directory and attempt to unlink without AT_REMOVEDIR -> EISDIR. let dir_path = "/unlink_dir"; let dir_mode = (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits(); - task.sys_mkdir(dir_path, dir_mode) + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, dir_path, dir_mode) .expect("Failed to create directory"); assert_eq!( task.sys_unlinkat(0, dir_path, AtFlags::empty()), @@ -546,7 +546,7 @@ fn test_unlinkat() { // 3. Create a non-empty directory and remove with AT_REMOVEDIR -> ENOTEMPTY. let nonempty_dir = "/unlink_dir_nonempty"; - task.sys_mkdir(nonempty_dir, dir_mode) + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, nonempty_dir, dir_mode) .expect("Failed to create non-empty directory"); let inner_file_fd = task .sys_open( @@ -585,7 +585,7 @@ fn test_unlinkat() { // 6. Create and remove another empty directory to ensure repeatability. let empty_dir2 = "/unlink_empty_dir"; - task.sys_mkdir(empty_dir2, dir_mode) + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, empty_dir2, dir_mode) .expect("Failed to create second empty directory"); task.sys_unlinkat(0, empty_dir2, AtFlags::AT_REMOVEDIR) .expect("Should remove second empty directory"); From e68b762dcb025475117f5e96612ad66873286ca5 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 17 Jun 2026 19:31:01 -0700 Subject: [PATCH 039/319] Use slotmap for broker object references (#930) Replace broker object/reference maps with slotmap-backed authority state and collapse object handles to opaque reference IDs. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 10 ++ litebox/src/broker/error.rs | 7 +- litebox_broker_core/Cargo.toml | 1 + litebox_broker_core/src/error.rs | 3 - litebox_broker_core/src/event.rs | 17 ++- litebox_broker_core/src/lib.rs | 35 ++--- litebox_broker_core/src/object.rs | 133 ++++++++---------- litebox_broker_host/src/lib.rs | 3 +- litebox_broker_local/src/lib.rs | 8 +- litebox_broker_protocol/src/error.rs | 9 +- litebox_broker_protocol/src/lib.rs | 7 +- litebox_broker_protocol/src/object.rs | 62 -------- litebox_broker_protocol/src/wire.rs | 9 +- litebox_broker_protocol/src/wire/primitive.rs | 10 +- 14 files changed, 116 insertions(+), 198 deletions(-) delete mode 100644 litebox_broker_protocol/src/object.rs diff --git a/Cargo.lock b/Cargo.lock index ca763d2838..d3fdf18ff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1481,6 +1481,7 @@ name = "litebox_broker_core" version = "0.1.0" dependencies = [ "litebox_broker_protocol", + "slotmap", ] [[package]] @@ -2828,6 +2829,15 @@ dependencies = [ "log", ] +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index afc81bfc31..10d71c6afc 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -50,10 +50,9 @@ impl From for BrokerObjectError { impl From for BrokerObjectError { fn from(error: ErrorCode) -> Self { match error { - ErrorCode::InvalidRights - | ErrorCode::UnknownObject - | ErrorCode::WrongObjectType - | ErrorCode::StaleHandle => Self::InvalidObject, + ErrorCode::InvalidRights | ErrorCode::UnknownObject | ErrorCode::WrongObjectType => { + Self::InvalidObject + } ErrorCode::WouldBlock => Self::WouldBlock, ErrorCode::ResourceExhausted => Self::ResourceExhausted, _ => Self::Internal, diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index 9b98941474..f002550b79 100644 --- a/litebox_broker_core/Cargo.toml +++ b/litebox_broker_core/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +slotmap = { version = "1.1.1", default-features = false } [lints] workspace = true diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index 8f9dceace7..75f0f7ed08 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -11,8 +11,6 @@ pub enum BrokerError { PolicyDenied, /// The referenced object does not exist. UnknownObject, - /// The referenced object generation is stale. - StaleHandle, /// The referenced object type does not match the operation. WrongObjectType, /// The caller lacks the required broker rights. @@ -34,7 +32,6 @@ impl fmt::Display for BrokerError { match self { Self::PolicyDenied => f.write_str("broker policy denied the operation"), Self::UnknownObject => f.write_str("unknown broker object"), - Self::StaleHandle => f.write_str("stale broker handle"), Self::WrongObjectType => f.write_str("wrong broker object type"), Self::InvalidRights => f.write_str("invalid broker rights"), Self::ResourceExhausted => f.write_str("broker resource exhausted"), diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 523489cea2..41f14daf00 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::object::{ObjectId, ObjectKind}; +use crate::object::{ObjectEntry, ObjectId}; use crate::{BrokerAssociation, BrokerCore, BrokerError, ObjectRights, ObjectType, Result}; use litebox_broker_protocol::{ EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, @@ -28,8 +28,7 @@ impl BrokerCore { self.insert_object_with_reference( association, - ObjectKind::Event(EventObject::new(initial_count)), - ObjectType::Event, + ObjectEntry::Event(EventObject::new(initial_count)), rights, ) } @@ -66,8 +65,8 @@ impl BrokerCore { ) -> Result { let authorized = self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WRITE)?; - match &mut self.object_mut(authorized.object_id)?.kind { - ObjectKind::Event(event) => event + match self.object_mut(authorized.object_id)? { + ObjectEntry::Event(event) => event .add(value) .map(|state| Self::filter_readiness_for_rights(state, authorized.rights)), } @@ -82,8 +81,8 @@ impl BrokerCore { ) -> Result { let authorized = self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; - match &mut self.object_mut(authorized.object_id)?.kind { - ObjectKind::Event(event) => event.consume(mode).map(|response| { + match self.object_mut(authorized.object_id)? { + ObjectEntry::Event(event) => event.consume(mode).map(|response| { EventConsumption::new( response.value, Self::filter_readiness_for_rights(response.readiness, authorized.rights), @@ -101,8 +100,8 @@ impl BrokerCore { } fn event_state(&self, object_id: ObjectId) -> Result { - match &self.object(object_id)?.kind { - ObjectKind::Event(event) => Ok(event.readiness_state()), + match self.object(object_id)? { + ObjectEntry::Event(event) => Ok(event.readiness_state()), } } } diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index e1c54f5d58..6bf5cd321b 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -4,7 +4,7 @@ //! Broker authority core independent of protocol envelopes and channels. //! //! `litebox_broker_core` owns broker-side object identity, reference lifetime, -//! rights checks, reference generation checks, and policy calls. It may use +//! rights checks, handle validity checks, and policy calls. It may use //! shared semantic DTOs from `litebox_broker_protocol` for values that both the //! local core and broker understand, such as handles and readiness state. It //! deliberately has no dependency on protocol envelopes, channel traits, wire @@ -14,6 +14,7 @@ #![no_std] extern crate alloc; + #[cfg(test)] extern crate std; @@ -23,12 +24,14 @@ mod identity; mod object; mod policy; -use alloc::collections::BTreeMap; use core::sync::atomic::{AtomicBool, Ordering}; +use alloc::collections::BTreeMap; +use slotmap::SlotMap; + pub use error::BrokerError; pub use identity::{BrokerAssociation, CallerCredential}; -use litebox_broker_protocol::ObjectReferenceId; +use litebox_broker_protocol::ObjectHandle; use object::{ObjectEntry, ObjectId, ObjectReference}; pub use object::{ObjectRights, ObjectType}; pub use policy::{ObjectOperation, PolicyDecision, PolicyEngine, PolicyOperation, PolicyProfile}; @@ -68,6 +71,8 @@ impl Default for BrokerCoreLimits { } } +const MAX_OBJECTS: usize = u32::MAX as usize - 1; + /// Channel-independent broker authority state. /// /// A broker process may construct only one broker core for its process @@ -77,10 +82,9 @@ pub struct BrokerCore { policy: PolicyEngine, limits: BrokerCoreLimits, next_process_id: u64, - next_object_id: u64, - next_reference_id: u64, - objects: BTreeMap, - references: BTreeMap, + next_reference_handle: u64, + objects: SlotMap, + references: BTreeMap, } static BROKER_CORE_CREATED: AtomicBool = AtomicBool::new(false); @@ -93,6 +97,10 @@ impl BrokerCore { /// Creates the broker core with explicit authority-state limits. pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Result { + if limits.max_objects > MAX_OBJECTS { + return Err(BrokerError::ResourceExhausted); + } + BROKER_CORE_CREATED .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .map_err(|_| BrokerError::BrokerCoreAlreadyExists)?; @@ -101,22 +109,15 @@ impl BrokerCore { policy, limits, next_process_id: 1, - next_object_id: 1, - next_reference_id: 1, - objects: BTreeMap::new(), + next_reference_handle: 1, + objects: SlotMap::with_key(), references: BTreeMap::new(), }) } } -const EXHAUSTED_ID: u64 = 0; - fn allocate_id(next_id: &mut u64) -> Result { - if *next_id == EXHAUSTED_ID { - return Err(BrokerError::ResourceExhausted); - } - let id = *next_id; - *next_id = id.checked_add(1).unwrap_or(EXHAUSTED_ID); + *next_id = id.checked_add(1).ok_or(BrokerError::ResourceExhausted)?; Ok(id) } diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 06cca10ffc..6b4355e495 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -6,7 +6,7 @@ use core::ops::BitOr; use crate::event::EventObject; use crate::identity::{BrokerAssociation, ProcessId}; use crate::{BrokerCore, BrokerError, PolicyDecision, PolicyOperation, Result, allocate_id}; -use litebox_broker_protocol::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; +use litebox_broker_protocol::ObjectHandle; /// Broker object type known to the authority core and policy engine. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -54,40 +54,24 @@ impl BitOr for ObjectRights { } } -/// Broker-owned object identifier. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ObjectId(u64); - -impl ObjectId { - /// Creates an object identifier from its raw value. - const fn new(raw: u64) -> Self { - Self(raw) - } +slotmap::new_key_type! { + /// Broker-owned object identifier. + pub(crate) struct ObjectId; } -const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGeneration::new(1); - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct ObjectReference { pub(crate) object_id: ObjectId, - pub(crate) reference_generation: ObjectReferenceGeneration, pub(crate) owner: ProcessId, - pub(crate) object_type: ObjectType, pub(crate) rights: ObjectRights, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct ObjectEntry { - pub(crate) kind: ObjectKind, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum ObjectKind { +pub(crate) enum ObjectEntry { Event(EventObject), } -impl ObjectKind { +impl ObjectEntry { pub(crate) const fn object_type(self) -> ObjectType { match self { Self::Event(_) => ObjectType::Event, @@ -97,16 +81,10 @@ impl ObjectKind { impl BrokerCore { /// Inserts a broker object and mints its first owned reference. - /// - /// The current POC never reuses reference slots, so the reference - /// generation starts at the authority-owned first generation. Any future - /// reference-slot reuse path must bump the generation before reissuing a - /// slot so stale handles cannot validate against a recycled reference. pub(crate) fn insert_object_with_reference( &mut self, association: &BrokerAssociation, - kind: ObjectKind, - object_type: ObjectType, + object: ObjectEntry, rights: ObjectRights, ) -> Result { if self.objects.len() >= self.limits.max_objects @@ -115,23 +93,19 @@ impl BrokerCore { return Err(BrokerError::ResourceExhausted); } - let object_id = self.allocate_object_id()?; - let reference_id = self.allocate_reference_id()?; - let reference_generation = FIRST_REFERENCE_GENERATION; - - self.objects.insert(object_id, ObjectEntry { kind }); - self.references.insert( - reference_id, + let handle = ObjectHandle(allocate_id(&mut self.next_reference_handle)?); + let object_id = self.objects.insert(object); + let old_reference = self.references.insert( + handle, ObjectReference { object_id, - reference_generation, owner: association.process_id(), - object_type, rights, }, ); + debug_assert!(old_reference.is_none()); - Ok(ObjectHandle::new(reference_id, reference_generation)) + Ok(handle) } pub(crate) fn authorize_create_object( @@ -173,13 +147,13 @@ impl BrokerCore { pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { self.objects - .get(&object_id) + .get(object_id) .ok_or(BrokerError::UnknownObject) } pub(crate) fn object_mut(&mut self, object_id: ObjectId) -> Result<&mut ObjectEntry> { self.objects - .get_mut(&object_id) + .get_mut(object_id) .ok_or(BrokerError::UnknownObject) } @@ -191,31 +165,20 @@ impl BrokerCore { required_rights: ObjectRights, ) -> Result { let reference = self.reference_for_handle(association, handle)?; - if reference.object_type != expected_type { - return Err(BrokerError::WrongObjectType); - } if !reference.rights.contains(required_rights) { return Err(BrokerError::InvalidRights); } let object = self .objects - .get(&reference.object_id) + .get(reference.object_id) .ok_or(BrokerError::UnknownObject)?; - if object.kind.object_type() != expected_type { + if object.object_type() != expected_type { return Err(BrokerError::WrongObjectType); } Ok(*reference) } - - fn allocate_object_id(&mut self) -> Result { - allocate_id(&mut self.next_object_id).map(ObjectId::new) - } - - fn allocate_reference_id(&mut self) -> Result { - allocate_id(&mut self.next_reference_id).map(ObjectReferenceId::new) - } } impl BrokerCore { @@ -228,11 +191,11 @@ impl BrokerCore { handle: ObjectHandle, ) -> Result<()> { let object_id = self.reference_for_handle(association, handle)?.object_id; - if !self.objects.contains_key(&object_id) { + if !self.objects.contains_key(object_id) { return Err(BrokerError::UnknownObject); } - self.references.remove(&handle.reference_id); + self.references.remove(&handle); self.drop_object_if_unreferenced(object_id); Ok(()) } @@ -246,7 +209,7 @@ impl BrokerCore { self.objects.retain(|object_id, _| { references .values() - .any(|reference| reference.object_id == *object_id) + .any(|reference| reference.object_id == object_id) }); } @@ -257,14 +220,11 @@ impl BrokerCore { ) -> Result<&ObjectReference> { let reference = self .references - .get(&handle.reference_id) + .get(&handle) .ok_or(BrokerError::UnknownObject)?; if reference.owner != association.process_id() { return Err(BrokerError::UnknownObject); } - if reference.reference_generation != handle.reference_generation { - return Err(BrokerError::StaleHandle); - } Ok(reference) } @@ -274,7 +234,7 @@ impl BrokerCore { .values() .any(|reference| reference.object_id == object_id) { - self.objects.remove(&object_id); + self.objects.remove(object_id); } } } @@ -288,21 +248,36 @@ pub(crate) struct AuthorizedObject { #[cfg(test)] mod tests { use super::*; - use crate::{BrokerError, CallerCredential, PolicyEngine}; - use litebox_broker_protocol::WaitOutcome; + use crate::{BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine, allocate_id}; + use litebox_broker_protocol::{ObjectHandle, WaitOutcome}; #[test] - fn allocator_issues_max_id_then_exhausts() { + fn allocator_exhausts_before_id_overflow() { let mut next_id = u64::MAX; - assert_eq!(allocate_id(&mut next_id), Ok(u64::MAX)); - assert_eq!(next_id, 0); + assert_eq!( + allocate_id(&mut next_id), + Err(BrokerError::ResourceExhausted) + ); assert_eq!( allocate_id(&mut next_id), Err(BrokerError::ResourceExhausted) ); } + #[test] + fn oversized_object_slotmap_limits_are_rejected_before_core_construction() { + let too_many_entries = u32::MAX as usize; + + assert!(matches!( + BrokerCore::new_with_limits( + PolicyEngine::event_only(), + BrokerCoreLimits::new(too_many_entries, 1) + ), + Err(BrokerError::ResourceExhausted) + )); + } + #[test] fn object_reference_lifecycle_uses_public_core_constructor_once() { let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); @@ -313,20 +288,19 @@ mod tests { .create_association(CallerCredential::Unauthenticated) .unwrap(); let handle = core.create_event(&owner).unwrap(); + let unknown_handle = ObjectHandle(handle.0 + 1); + assert_ne!(unknown_handle, handle); assert_eq!( - core.close_object_reference(&other, handle), + core.wait_event(&owner, unknown_handle), Err(BrokerError::UnknownObject) ); - let stale = ObjectHandle::new( - handle.reference_id, - ObjectReferenceGeneration::new(handle.reference_generation.get() + 1), - ); assert_eq!( - core.close_object_reference(&owner, stale), - Err(BrokerError::StaleHandle) + core.close_object_reference(&other, handle), + Err(BrokerError::UnknownObject) ); + assert!(matches!( core.wait_event(&owner, handle), Ok(WaitOutcome::WouldBlock(_)) @@ -351,5 +325,16 @@ mod tests { assert!(core.references.is_empty()); assert!(core.objects.is_empty()); + + let association = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + core.next_reference_handle = u64::MAX; + assert_eq!( + core.create_event(&association), + Err(BrokerError::ResourceExhausted) + ); + assert!(core.references.is_empty()); + assert!(core.objects.is_empty()); } } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index d63cc13e8d..43063229b4 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -234,7 +234,6 @@ fn to_protocol_error(error: BrokerError) -> ErrorCode { match error { BrokerError::PolicyDenied => ErrorCode::PolicyDenied, BrokerError::UnknownObject => ErrorCode::UnknownObject, - BrokerError::StaleHandle => ErrorCode::StaleHandle, BrokerError::WrongObjectType => ErrorCode::WrongObjectType, BrokerError::InvalidRights => ErrorCode::InvalidRights, BrokerError::ResourceExhausted => ErrorCode::ResourceExhausted, @@ -352,7 +351,7 @@ mod tests { } response => panic!("unexpected response: {response:?}"), }; - assert_ne!(handle.reference_id.get(), 0); + assert_ne!(handle.0, 0); } fn serve_connection_closes_after_protocol_violation(core: &mut BrokerCore) { diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 610cd65128..2c345efcad 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -220,13 +220,11 @@ mod tests { #[test] fn active_core_request_wraps_request_and_unwraps_response() { use litebox_broker_protocol::{ - CoreRequest, CoreResponse, EventRequest, EventResponse, ObjectHandle, - ObjectReferenceGeneration, ObjectReferenceId, ReadinessState, WaitEventRequest, - WaitEventResponse, WaitOutcome, + CoreRequest, CoreResponse, EventRequest, EventResponse, ObjectHandle, ReadinessState, + WaitEventRequest, WaitEventResponse, WaitOutcome, }; - let handle = - ObjectHandle::new(ObjectReferenceId::new(7), ObjectReferenceGeneration::new(1)); + let handle = ObjectHandle(7); let request = CoreRequest::Event(EventRequest::Wait(WaitEventRequest::new(handle))); let response = CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)), diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index e2113225ff..c379c3769d 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -21,8 +21,6 @@ pub enum ErrorCode { PolicyDenied, /// The referenced object does not exist. UnknownObject, - /// The referenced object generation is stale. - StaleHandle, /// The referenced object type does not match the operation. WrongObjectType, /// The caller lacks the required broker rights. @@ -42,8 +40,8 @@ impl ErrorCode { /// Raw error values are part of the broker wire ABI; do not renumber /// assigned values. /// - /// Values `0` and `1` remain unassigned so null/default-looking values never - /// represent concrete broker errors. + /// Values `0`, `1`, and `6` remain unassigned so null/default-looking values + /// never represent concrete broker errors and retired values are not reused. /// /// Converts a raw protocol error code to an error category. pub const fn from_raw(raw: u16) -> Self { @@ -55,7 +53,6 @@ impl ErrorCode { 12 => Self::Internal, 4 => Self::PolicyDenied, 5 => Self::UnknownObject, - 6 => Self::StaleHandle, 7 => Self::WrongObjectType, 8 => Self::InvalidRights, 9 => Self::ResourceExhausted, @@ -74,7 +71,6 @@ impl ErrorCode { Self::Internal => 12, Self::PolicyDenied => 4, Self::UnknownObject => 5, - Self::StaleHandle => 6, Self::WrongObjectType => 7, Self::InvalidRights => 8, Self::ResourceExhausted => 9, @@ -94,7 +90,6 @@ impl fmt::Display for ErrorCode { Self::Internal => f.write_str("internal broker error"), Self::PolicyDenied => f.write_str("broker policy denied the operation"), Self::UnknownObject => f.write_str("unknown broker object"), - Self::StaleHandle => f.write_str("stale broker handle"), Self::WrongObjectType => f.write_str("wrong broker object type"), Self::InvalidRights => f.write_str("invalid broker rights"), Self::ResourceExhausted => f.write_str("broker resource exhausted"), diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 0f081bec33..7372703475 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -16,7 +16,6 @@ pub mod channel; pub mod error; pub mod event; pub mod message; -pub mod object; pub mod wire; pub use channel::{ @@ -32,7 +31,11 @@ pub use event::{ pub use message::{ BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, }; -pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; + +/// Opaque broker object reference handle. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ObjectHandle(pub u64); /// Major/minor broker protocol version. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/litebox_broker_protocol/src/object.rs b/litebox_broker_protocol/src/object.rs deleted file mode 100644 index 141be47be0..0000000000 --- a/litebox_broker_protocol/src/object.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -/// Broker object reference handle returned to the local core. -/// -/// The local core may cache this value, but the broker remains authoritative for -/// object identity, object lifetime, reference lifetime, type, rights, and -/// reference generation. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct ObjectHandle { - /// Opaque broker reference identifier owned by one authenticated process association. - pub reference_id: ObjectReferenceId, - /// Reference generation used to reject stale handles after reference-slot reuse. - pub reference_generation: ObjectReferenceGeneration, -} - -impl ObjectHandle { - /// Creates an object handle. - pub const fn new( - reference_id: ObjectReferenceId, - reference_generation: ObjectReferenceGeneration, - ) -> Self { - Self { - reference_id, - reference_generation, - } - } -} - -/// Broker-owned object reference identifier. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ObjectReferenceId(u64); - -impl ObjectReferenceId { - /// Creates an object reference identifier from its raw protocol value. - pub const fn new(raw: u64) -> Self { - Self(raw) - } - - /// Returns the raw protocol value. - pub const fn get(self) -> u64 { - self.0 - } -} - -/// Generation attached to a broker object reference. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ObjectReferenceGeneration(u64); - -impl ObjectReferenceGeneration { - /// Creates a reference generation from its raw protocol value. - pub const fn new(raw: u64) -> Self { - Self(raw) - } - - /// Returns the raw protocol value. - pub const fn get(self) -> u64 { - self.0 - } -} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index af5a3a171a..0415a28789 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -164,8 +164,8 @@ mod tests { use crate::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, CreateEventResponse, EventConsumeMode, EventRequest, - EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, - ReadinessState, WaitEventRequest, WaitEventResponse, WaitOutcome, + EventResponse, ObjectHandle, ProtocolVersion, ReadinessState, WaitEventRequest, + WaitEventResponse, WaitOutcome, }; #[test] @@ -298,10 +298,7 @@ mod tests { } const fn sample_handle() -> ObjectHandle { - ObjectHandle::new( - ObjectReferenceId::new(13), - ObjectReferenceGeneration::new(14), - ) + ObjectHandle(13) } const fn event_request(request: EventRequest) -> BrokerRequest { diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs index cdfad237ac..042a956fbc 100644 --- a/litebox_broker_protocol/src/wire/primitive.rs +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; -use crate::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion}; +use crate::{ObjectHandle, ProtocolVersion}; use super::WireError; @@ -39,8 +39,7 @@ impl Encoder { } pub(super) fn handle(&mut self, handle: ObjectHandle) { - self.u64(handle.reference_id.get()); - self.u64(handle.reference_generation.get()); + self.u64(handle.0); } } @@ -92,10 +91,7 @@ impl<'a> Decoder<'a> { } pub(super) fn handle(&mut self) -> Result { - let reference_id = ObjectReferenceId::new(self.u64()?); - let reference_generation = ObjectReferenceGeneration::new(self.u64()?); - - Ok(ObjectHandle::new(reference_id, reference_generation)) + Ok(ObjectHandle(self.u64()?)) } fn take(&mut self, len: usize) -> Result<&'a [u8], WireError> { From e91e1bd582f7702bdc771b78e8d0a31f7717209f Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 17 Jun 2026 19:58:21 -0700 Subject: [PATCH 040/319] Use bitflags for broker object rights (#932) Use bitflags for broker object rights. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1 + litebox_broker_core/Cargo.toml | 1 + litebox_broker_core/src/object.rs | 45 ++++++------------------------- litebox_broker_core/src/policy.rs | 2 +- 4 files changed, 11 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3fdf18ff4..251696bafe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1480,6 +1480,7 @@ dependencies = [ name = "litebox_broker_core" version = "0.1.0" dependencies = [ + "bitflags 2.11.0", "litebox_broker_protocol", "slotmap", ] diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index f002550b79..6572527096 100644 --- a/litebox_broker_core/Cargo.toml +++ b/litebox_broker_core/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +bitflags = { version = "2.9.0", default-features = false } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } slotmap = { version = "1.1.1", default-features = false } diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 6b4355e495..90be78b49c 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use core::ops::BitOr; - use crate::event::EventObject; use crate::identity::{BrokerAssociation, ProcessId}; use crate::{BrokerCore, BrokerError, PolicyDecision, PolicyOperation, Result, allocate_id}; @@ -16,41 +14,14 @@ pub enum ObjectType { Event, } -/// Broker rights attached to an object reference. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub struct ObjectRights(u32); - -impl ObjectRights { - /// Empty rights set. - pub const NONE: Self = Self(0); - /// Right to wait for readiness. - pub const WAIT: Self = Self(1 << 0); - /// Right to mutate object state, such as adding event readiness credits. - pub const WRITE: Self = Self(1 << 1); - - /// Returns true when no rights are present. - pub const fn is_empty(self) -> bool { - self.0 == 0 - } - - /// Returns true when all `required` rights are present. - pub const fn contains(self, required: Self) -> bool { - (self.0 & required.0) == required.0 - } - - /// Returns the union of two rights sets. - #[must_use] - pub const fn union(self, other: Self) -> Self { - Self(self.0 | other.0) - } -} - -impl BitOr for ObjectRights { - type Output = Self; - - fn bitor(self, rhs: Self) -> Self::Output { - self.union(rhs) +bitflags::bitflags! { + /// Broker rights attached to an object reference. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] + pub struct ObjectRights: u32 { + /// Right to wait for readiness. + const WAIT = 1 << 0; + /// Right to mutate object state, such as adding event readiness credits. + const WRITE = 1 << 1; } } diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index 1c2ecdaeba..be330b8e34 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -216,7 +216,7 @@ mod tests { policy.authorize(PolicyOperation::use_object( CallerCredential::Unauthenticated, ObjectType::Event, - ObjectRights::NONE + ObjectRights::empty() )), Err(BrokerError::PolicyDenied) ); From 9556dc1b3fe08354b107920ef148d591e898cb65 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 17 Jun 2026 20:50:54 -0700 Subject: [PATCH 041/319] Simplify broker policy and state layers (#933) This PR simplifies the broker policy and state layers. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 4 +- litebox/src/broker/mod.rs | 20 --- litebox/src/litebox.rs | 13 +- litebox_broker_core/src/error.rs | 6 - litebox_broker_core/src/event.rs | 15 +- litebox_broker_core/src/lib.rs | 4 +- litebox_broker_core/src/object.rs | 70 ++-------- litebox_broker_core/src/policy.rs | 198 +++++++-------------------- litebox_broker_host/src/lib.rs | 1 - litebox_broker_protocol/src/error.rs | 9 +- 10 files changed, 81 insertions(+), 259 deletions(-) diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 10d71c6afc..787aa90647 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -50,9 +50,7 @@ impl From for BrokerObjectError { impl From for BrokerObjectError { fn from(error: ErrorCode) -> Self { match error { - ErrorCode::InvalidRights | ErrorCode::UnknownObject | ErrorCode::WrongObjectType => { - Self::InvalidObject - } + ErrorCode::InvalidRights | ErrorCode::UnknownObject => Self::InvalidObject, ErrorCode::WouldBlock => Self::WouldBlock, ErrorCode::ResourceExhausted => Self::ResourceExhausted, _ => Self::Internal, diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index d8aaf567c6..5e837e1476 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use alloc::sync::Arc; - use litebox_broker_local::{BrokerLocal, BrokerLocalError}; use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; @@ -58,21 +56,3 @@ where }) } } - -pub(crate) struct BrokerState { - control: Option>, - _marker: core::marker::PhantomData, -} - -impl BrokerState { - pub(crate) fn new(control: Option>) -> Self { - Self { - control, - _marker: core::marker::PhantomData, - } - } - - pub(crate) fn control(&self) -> Option> { - self.control.clone() - } -} diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 5b65d80373..ba5d06c46a 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -9,7 +9,7 @@ use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::LocalControlChannel; use crate::{ - broker::{self, BrokerState}, + broker, fd::Descriptors, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -90,9 +90,8 @@ impl LiteBox { // prints, if the feature is enabled. #[cfg(feature = "lock_tracing")] crate::sync::lock_tracing::LockTracker::init(platform); - - let descriptors = RwLock::new(Descriptors::new_from_litebox_creation()); - let broker = BrokerState::new(broker_control); + let descriptors: RwLock> = + RwLock::new(Descriptors::new_from_litebox_creation()); litebox_util_log::trace!("LiteBox instance initialized"); @@ -100,7 +99,7 @@ impl LiteBox { x: Arc::new(LiteBoxX { platform, descriptors, - broker, + broker: broker_control, }), } } @@ -137,7 +136,7 @@ impl LiteBox { } pub(crate) fn broker_control(&self) -> Option> { - self.x.broker.control() + self.x.broker.clone() } } @@ -145,5 +144,5 @@ impl LiteBox { pub(crate) struct LiteBoxX { pub(crate) platform: &'static Platform, descriptors: RwLock>, - broker: BrokerState, + broker: Option>, } diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index 75f0f7ed08..e3b7da6d4c 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -11,8 +11,6 @@ pub enum BrokerError { PolicyDenied, /// The referenced object does not exist. UnknownObject, - /// The referenced object type does not match the operation. - WrongObjectType, /// The caller lacks the required broker rights. InvalidRights, /// Broker-side resource exhaustion. @@ -23,8 +21,6 @@ pub enum BrokerError { WouldBlock, /// The operation is not implemented by this BrokerCore. UnsupportedOperation, - /// Policy returned a decision that does not match the authorized operation. - InvalidPolicyDecision, } impl fmt::Display for BrokerError { @@ -32,13 +28,11 @@ impl fmt::Display for BrokerError { match self { Self::PolicyDenied => f.write_str("broker policy denied the operation"), Self::UnknownObject => f.write_str("unknown broker object"), - Self::WrongObjectType => f.write_str("wrong broker object type"), Self::InvalidRights => f.write_str("invalid broker rights"), Self::ResourceExhausted => f.write_str("broker resource exhausted"), Self::BrokerCoreAlreadyExists => f.write_str("broker core already exists"), Self::WouldBlock => f.write_str("broker operation would block"), Self::UnsupportedOperation => f.write_str("unsupported broker operation"), - Self::InvalidPolicyDecision => f.write_str("invalid broker policy decision"), } } } diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 41f14daf00..4bd0c02fb7 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -2,7 +2,7 @@ // Licensed under the MIT license. use crate::object::{ObjectEntry, ObjectId}; -use crate::{BrokerAssociation, BrokerCore, BrokerError, ObjectRights, ObjectType, Result}; +use crate::{BrokerAssociation, BrokerCore, BrokerError, ObjectRights, Result}; use litebox_broker_protocol::{ EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, }; @@ -24,7 +24,7 @@ impl BrokerCore { if initial_count > MAX_EVENT_COUNT { return Err(BrokerError::ResourceExhausted); } - let rights = self.authorize_create_object(association, ObjectType::Event)?; + let rights = self.authorize_create_event(association)?; self.insert_object_with_reference( association, @@ -39,12 +39,11 @@ impl BrokerCore { /// concept. Userland or kernel deployments can block on deployment-specific /// wait primitives after BrokerCore authorizes and reports readiness state. pub fn wait_event( - &mut self, + &self, association: &BrokerAssociation, handle: ObjectHandle, ) -> Result { - let authorized = - self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + let authorized = self.authorize_use_event(association, handle, ObjectRights::WAIT)?; let state = Self::filter_readiness_for_rights( self.event_state(authorized.object_id)?, authorized.rights, @@ -63,8 +62,7 @@ impl BrokerCore { handle: ObjectHandle, value: u64, ) -> Result { - let authorized = - self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WRITE)?; + let authorized = self.authorize_use_event(association, handle, ObjectRights::WRITE)?; match self.object_mut(authorized.object_id)? { ObjectEntry::Event(event) => event .add(value) @@ -79,8 +77,7 @@ impl BrokerCore { handle: ObjectHandle, mode: EventConsumeMode, ) -> Result { - let authorized = - self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + let authorized = self.authorize_use_event(association, handle, ObjectRights::WAIT)?; match self.object_mut(authorized.object_id)? { ObjectEntry::Event(event) => event.consume(mode).map(|response| { EventConsumption::new( diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 6bf5cd321b..b0759141af 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -32,9 +32,9 @@ use slotmap::SlotMap; pub use error::BrokerError; pub use identity::{BrokerAssociation, CallerCredential}; use litebox_broker_protocol::ObjectHandle; +pub use object::ObjectRights; use object::{ObjectEntry, ObjectId, ObjectReference}; -pub use object::{ObjectRights, ObjectType}; -pub use policy::{ObjectOperation, PolicyDecision, PolicyEngine, PolicyOperation, PolicyProfile}; +pub use policy::{PolicyEngine, PolicyProfile}; /// BrokerCore result type. pub type Result = core::result::Result; diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 90be78b49c..f3cf88f18a 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -3,17 +3,9 @@ use crate::event::EventObject; use crate::identity::{BrokerAssociation, ProcessId}; -use crate::{BrokerCore, BrokerError, PolicyDecision, PolicyOperation, Result, allocate_id}; +use crate::{BrokerCore, BrokerError, Result, allocate_id}; use litebox_broker_protocol::ObjectHandle; -/// Broker object type known to the authority core and policy engine. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum ObjectType { - /// Broker-owned event object. - Event, -} - bitflags::bitflags! { /// Broker rights attached to an object reference. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] @@ -42,14 +34,6 @@ pub(crate) enum ObjectEntry { Event(EventObject), } -impl ObjectEntry { - pub(crate) const fn object_type(self) -> ObjectType { - match self { - Self::Event(_) => ObjectType::Event, - } - } -} - impl BrokerCore { /// Inserts a broker object and mints its first owned reference. pub(crate) fn insert_object_with_reference( @@ -79,41 +63,24 @@ impl BrokerCore { Ok(handle) } - pub(crate) fn authorize_create_object( - &mut self, + pub(crate) fn authorize_create_event( + &self, association: &BrokerAssociation, - object_type: ObjectType, ) -> Result { - match self.policy.authorize(PolicyOperation::create_object( - association.caller_credential(), - object_type, - ))? { - PolicyDecision::GrantObjectReference { rights } => Ok(rights), - _ => Err(BrokerError::InvalidPolicyDecision), - } + self.policy + .authorize_create_event(association.caller_credential()) } - pub(crate) fn authorize_use_object( - &mut self, + pub(crate) fn authorize_use_event( + &self, association: &BrokerAssociation, handle: ObjectHandle, - object_type: ObjectType, rights: ObjectRights, - ) -> Result { - let reference = self.validate_handle(association, handle, object_type, rights)?; - let object_id = reference.object_id; - let reference_rights = reference.rights; - match self.policy.authorize(PolicyOperation::use_object( - association.caller_credential(), - object_type, - rights, - ))? { - PolicyDecision::Authorized => Ok(AuthorizedObject { - object_id, - rights: reference_rights, - }), - _ => Err(BrokerError::InvalidPolicyDecision), - } + ) -> Result { + let reference = self.validate_handle(association, handle, rights)?; + self.policy + .authorize_use_event(association.caller_credential(), rights)?; + Ok(reference) } pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { @@ -132,7 +99,6 @@ impl BrokerCore { &self, association: &BrokerAssociation, handle: ObjectHandle, - expected_type: ObjectType, required_rights: ObjectRights, ) -> Result { let reference = self.reference_for_handle(association, handle)?; @@ -140,13 +106,9 @@ impl BrokerCore { return Err(BrokerError::InvalidRights); } - let object = self - .objects + self.objects .get(reference.object_id) .ok_or(BrokerError::UnknownObject)?; - if object.object_type() != expected_type { - return Err(BrokerError::WrongObjectType); - } Ok(*reference) } @@ -210,12 +172,6 @@ impl BrokerCore { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct AuthorizedObject { - pub(crate) object_id: ObjectId, - pub(crate) rights: ObjectRights, -} - #[cfg(test)] mod tests { use super::*; diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index be330b8e34..0fca65108a 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -1,72 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::{BrokerError, CallerCredential, ObjectRights, ObjectType}; - -/// Broker operation submitted to the policy engine. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum PolicyOperation { - /// Perform an operation on a broker-owned object type. - Object { - /// Broker-entry-authenticated credential for the caller. - caller_credential: CallerCredential, - /// Object type targeted by the operation. - object_type: ObjectType, - /// Operation requested for the object type. - operation: ObjectOperation, - }, -} - -/// Generic object operation submitted to the policy engine. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum ObjectOperation { - /// Create a new broker-owned object. - Create, - /// Use an existing object handle with the requested rights. - Use { rights: ObjectRights }, -} - -/// Policy decision returned after authorizing a broker operation. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum PolicyDecision { - /// Operation is authorized and does not grant new authority material. - Authorized, - /// Object creation is authorized with rights for the initial object reference. - GrantObjectReference { - /// Rights to attach to the newly minted object reference. - rights: ObjectRights, - }, -} - -impl PolicyOperation { - /// Creates a policy operation for creating a broker-owned object type. - pub const fn create_object( - caller_credential: CallerCredential, - object_type: ObjectType, - ) -> Self { - Self::Object { - caller_credential, - object_type, - operation: ObjectOperation::Create, - } - } - - /// Creates a policy operation for using a broker-owned object with rights. - pub const fn use_object( - caller_credential: CallerCredential, - object_type: ObjectType, - rights: ObjectRights, - ) -> Self { - Self::Object { - caller_credential, - object_type, - operation: ObjectOperation::Use { rights }, - } - } -} +use crate::{BrokerError, CallerCredential, ObjectRights}; /// Configured broker policy. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -102,7 +37,7 @@ impl PolicyEngine { /// Creates a policy engine that allows only the current event-object surface. pub const fn event_only() -> Self { - Self::event_only_with_reference_rights(EVENT_REFERENCE_RIGHTS) + Self::event_only_with_reference_rights(DEFAULT_EVENT_RIGHTS) } /// Creates an event-only policy engine with explicit initial reference rights. @@ -112,21 +47,44 @@ impl PolicyEngine { pub const fn event_only_with_reference_rights(event_reference_rights: ObjectRights) -> Self { Self::new(PolicyProfile::EventOnly { event_reference_rights, - event_use_rights: EVENT_REFERENCE_RIGHTS, + event_use_rights: DEFAULT_EVENT_RIGHTS, }) } - /// Authorizes or denies a broker operation. - pub(crate) fn authorize( - &mut self, - operation: PolicyOperation, - ) -> Result { + pub(crate) fn authorize_create_event( + &self, + caller_credential: CallerCredential, + ) -> Result { match self.profile { - PolicyProfile::DefaultDeny => Err(BrokerError::PolicyDenied), PolicyProfile::EventOnly { event_reference_rights, - event_use_rights, - } => authorize_event_only(event_reference_rights, event_use_rights, operation), + .. + } if caller_credential == CallerCredential::Unauthenticated => { + Ok(event_reference_rights) + } + PolicyProfile::DefaultDeny | PolicyProfile::EventOnly { .. } => { + Err(BrokerError::PolicyDenied) + } + } + } + + pub(crate) fn authorize_use_event( + &self, + caller_credential: CallerCredential, + rights: ObjectRights, + ) -> Result<(), BrokerError> { + match self.profile { + PolicyProfile::EventOnly { + event_use_rights, .. + } if caller_credential == CallerCredential::Unauthenticated + && !rights.is_empty() + && event_use_rights.contains(rights) => + { + Ok(()) + } + PolicyProfile::DefaultDeny | PolicyProfile::EventOnly { .. } => { + Err(BrokerError::PolicyDenied) + } } } } @@ -142,34 +100,7 @@ impl Default for PolicyEngine { /// The default event create operation grants `WAIT | WRITE` on the initial /// reference. Use requests may ask for any non-empty subset of configured event /// use rights; BrokerCore separately enforces each reference's actual rights. -const EVENT_REFERENCE_RIGHTS: ObjectRights = ObjectRights::WAIT.union(ObjectRights::WRITE); - -fn authorize_event_only( - event_reference_rights: ObjectRights, - event_use_rights: ObjectRights, - operation: PolicyOperation, -) -> Result { - match operation { - PolicyOperation::Object { - caller_credential: CallerCredential::Unauthenticated, - object_type: ObjectType::Event, - operation: ObjectOperation::Create, - } => Ok(PolicyDecision::GrantObjectReference { - rights: event_reference_rights, - }), - PolicyOperation::Object { - caller_credential: CallerCredential::Unauthenticated, - object_type: ObjectType::Event, - operation: ObjectOperation::Use { rights }, - } if !rights.is_empty() && event_use_rights.contains(rights) => { - Ok(PolicyDecision::Authorized) - } - PolicyOperation::Object { - object_type: ObjectType::Event, - .. - } => Err(BrokerError::PolicyDenied), - } -} +const DEFAULT_EVENT_RIGHTS: ObjectRights = ObjectRights::WAIT.union(ObjectRights::WRITE); #[cfg(test)] mod tests { @@ -177,71 +108,44 @@ mod tests { #[test] fn event_only_policy_allows_only_current_event_surface() { - let mut policy = PolicyEngine::event_only(); + let policy = PolicyEngine::event_only(); assert_eq!( - policy.authorize(PolicyOperation::create_object( - CallerCredential::Unauthenticated, - ObjectType::Event - )), - Ok(PolicyDecision::GrantObjectReference { - rights: ObjectRights::WAIT | ObjectRights::WRITE - }) + policy.authorize_create_event(CallerCredential::Unauthenticated), + Ok(ObjectRights::WAIT | ObjectRights::WRITE) ); assert_eq!( - policy.authorize(PolicyOperation::use_object( - CallerCredential::Unauthenticated, - ObjectType::Event, - ObjectRights::WAIT - )), - Ok(PolicyDecision::Authorized) + policy.authorize_use_event(CallerCredential::Unauthenticated, ObjectRights::WAIT), + Ok(()) ); assert_eq!( - policy.authorize(PolicyOperation::use_object( - CallerCredential::Unauthenticated, - ObjectType::Event, - ObjectRights::WRITE - )), - Ok(PolicyDecision::Authorized) + policy.authorize_use_event(CallerCredential::Unauthenticated, ObjectRights::WRITE), + Ok(()) ); assert_eq!( - policy.authorize(PolicyOperation::use_object( + policy.authorize_use_event( CallerCredential::Unauthenticated, - ObjectType::Event, ObjectRights::WAIT | ObjectRights::WRITE - )), - Ok(PolicyDecision::Authorized) + ), + Ok(()) ); assert_eq!( - policy.authorize(PolicyOperation::use_object( - CallerCredential::Unauthenticated, - ObjectType::Event, - ObjectRights::empty() - )), + policy.authorize_use_event(CallerCredential::Unauthenticated, ObjectRights::empty()), Err(BrokerError::PolicyDenied) ); } #[test] fn explicit_event_reference_rights_do_not_narrow_event_use_policy() { - let mut policy = PolicyEngine::event_only_with_reference_rights(ObjectRights::WAIT); + let policy = PolicyEngine::event_only_with_reference_rights(ObjectRights::WAIT); assert_eq!( - policy.authorize(PolicyOperation::create_object( - CallerCredential::Unauthenticated, - ObjectType::Event - )), - Ok(PolicyDecision::GrantObjectReference { - rights: ObjectRights::WAIT - }) + policy.authorize_create_event(CallerCredential::Unauthenticated), + Ok(ObjectRights::WAIT) ); assert_eq!( - policy.authorize(PolicyOperation::use_object( - CallerCredential::Unauthenticated, - ObjectType::Event, - ObjectRights::WRITE - )), - Ok(PolicyDecision::Authorized) + policy.authorize_use_event(CallerCredential::Unauthenticated, ObjectRights::WRITE), + Ok(()) ); } } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 43063229b4..898d437ef3 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -234,7 +234,6 @@ fn to_protocol_error(error: BrokerError) -> ErrorCode { match error { BrokerError::PolicyDenied => ErrorCode::PolicyDenied, BrokerError::UnknownObject => ErrorCode::UnknownObject, - BrokerError::WrongObjectType => ErrorCode::WrongObjectType, BrokerError::InvalidRights => ErrorCode::InvalidRights, BrokerError::ResourceExhausted => ErrorCode::ResourceExhausted, BrokerError::WouldBlock => ErrorCode::WouldBlock, diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index c379c3769d..a269321c98 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -21,8 +21,6 @@ pub enum ErrorCode { PolicyDenied, /// The referenced object does not exist. UnknownObject, - /// The referenced object type does not match the operation. - WrongObjectType, /// The caller lacks the required broker rights. InvalidRights, /// Broker-side resource exhaustion. @@ -40,8 +38,8 @@ impl ErrorCode { /// Raw error values are part of the broker wire ABI; do not renumber /// assigned values. /// - /// Values `0`, `1`, and `6` remain unassigned so null/default-looking values - /// never represent concrete broker errors and retired values are not reused. + /// Values `0`, `1`, `6`, and `7` remain unassigned so null/default-looking + /// values never represent concrete broker errors and retired values are not reused. /// /// Converts a raw protocol error code to an error category. pub const fn from_raw(raw: u16) -> Self { @@ -53,7 +51,6 @@ impl ErrorCode { 12 => Self::Internal, 4 => Self::PolicyDenied, 5 => Self::UnknownObject, - 7 => Self::WrongObjectType, 8 => Self::InvalidRights, 9 => Self::ResourceExhausted, 13 => Self::WouldBlock, @@ -71,7 +68,6 @@ impl ErrorCode { Self::Internal => 12, Self::PolicyDenied => 4, Self::UnknownObject => 5, - Self::WrongObjectType => 7, Self::InvalidRights => 8, Self::ResourceExhausted => 9, Self::WouldBlock => 13, @@ -90,7 +86,6 @@ impl fmt::Display for ErrorCode { Self::Internal => f.write_str("internal broker error"), Self::PolicyDenied => f.write_str("broker policy denied the operation"), Self::UnknownObject => f.write_str("unknown broker object"), - Self::WrongObjectType => f.write_str("wrong broker object type"), Self::InvalidRights => f.write_str("invalid broker rights"), Self::ResourceExhausted => f.write_str("broker resource exhausted"), Self::WouldBlock => f.write_str("broker operation would block"), From f1b342cf38052f0d1a067d24aa9e7a0aee7634ce Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 17 Jun 2026 21:17:23 -0700 Subject: [PATCH 042/319] Use thiserror for broker errors (#934) This PR converts the broker error enums to use thiserror-derived Display/Error implementations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 6 ++ litebox/src/broker/error.rs | 25 +++--- litebox/src/event/counter.rs | 15 ++-- litebox_broker_core/Cargo.toml | 1 + litebox_broker_core/src/error.rs | 34 +++------ litebox_broker_host/Cargo.toml | 1 + litebox_broker_host/src/error.rs | 31 ++------ litebox_broker_host/src/lib.rs | 36 +++------ litebox_broker_local/Cargo.toml | 1 + litebox_broker_local/src/error.rs | 109 ++++++--------------------- litebox_broker_protocol/Cargo.toml | 1 + litebox_broker_protocol/src/error.rs | 45 ++++------- litebox_broker_protocol/src/wire.rs | 26 ++----- 13 files changed, 96 insertions(+), 235 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 251696bafe..e841dfa79e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1483,6 +1483,7 @@ dependencies = [ "bitflags 2.11.0", "litebox_broker_protocol", "slotmap", + "thiserror", ] [[package]] @@ -1491,6 +1492,7 @@ version = "0.1.0" dependencies = [ "litebox_broker_core", "litebox_broker_protocol", + "thiserror", ] [[package]] @@ -1498,11 +1500,15 @@ name = "litebox_broker_local" version = "0.1.0" dependencies = [ "litebox_broker_protocol", + "thiserror", ] [[package]] name = "litebox_broker_protocol" version = "0.1.0" +dependencies = [ + "thiserror", +] [[package]] name = "litebox_broker_transport" diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 787aa90647..353f8866da 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -2,18 +2,19 @@ // Licensed under the MIT license. use litebox_broker_protocol::ErrorCode; +use thiserror::Error; use crate::event::{counter::EventCounterError, polling::TryOpError}; /// Error returned by the deployment-provided broker control path. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] #[non_exhaustive] pub(crate) enum BrokerControlError { - /// The broker control transport failed. + #[error("broker control transport failed")] Transport, - /// The broker returned an operation error. - Broker(ErrorCode), - /// The broker returned a response shape that does not match the request. + #[error("broker returned operation error: {0}")] + Broker(#[source] ErrorCode), + #[error("broker returned unexpected response")] UnexpectedResponse, } @@ -21,19 +22,19 @@ pub(crate) enum BrokerControlError { /// /// This keeps protocol/control-channel failures separate from the public /// object-specific API error exposed by each local-core facade. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub(crate) enum BrokerObjectError { - /// The deployment-provided broker control path failed. + #[error("broker control failed")] Control, - /// The broker rejected the cached object handle, type, or rights. + #[error("invalid broker object")] InvalidObject, - /// The object operation would block in its current broker-side state. + #[error("broker object operation would block")] WouldBlock, - /// The object or broker-side state cannot grow further. + #[error("broker object resource exhausted")] ResourceExhausted, - /// The broker returned a response shape that does not match the request. + #[error("broker returned unexpected response")] UnexpectedResponse, - /// The broker reported a non-recoverable or unsupported object error. + #[error("internal broker object error")] Internal, } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 711d36dca3..a402a680b4 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -9,6 +9,7 @@ use litebox_broker_protocol::{ CreateEventRequest, EventRequest, EventResponse, ObjectHandle, ReadinessState, WaitEventRequest, WaitOutcome, }; +use thiserror::Error; use crate::{ LiteBox, @@ -25,20 +26,20 @@ use crate::{ }; /// Errors returned by local-core event counters. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] #[non_exhaustive] pub enum EventCounterError { - /// The requested operation is invalid for this event counter. + #[error("invalid event counter input")] InvalidInput, - /// The operation would block. + #[error("event counter operation would block")] WouldBlock, - /// The event counter cannot accept more state. + #[error("event counter resource exhausted")] ResourceExhausted, - /// The backing authority or transport failed. + #[error("event counter I/O failed")] Io, - /// The backing authority returned a response shape that does not match the request. + #[error("event counter received unexpected response")] UnexpectedResponse, - /// No backing authority is available for this event counter. + #[error("event counter backing authority unavailable")] Unavailable, } diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index 6572527096..86666b5731 100644 --- a/litebox_broker_core/Cargo.toml +++ b/litebox_broker_core/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" bitflags = { version = "2.9.0", default-features = false } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } slotmap = { version = "1.1.1", default-features = false } +thiserror = { version = "2.0.6", default-features = false } [lints] workspace = true diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index e3b7da6d4c..1416ff7cdc 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -1,40 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use core::fmt; +use thiserror::Error; /// Broker authority error category. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum BrokerError { - /// Policy denied the operation. + #[error("broker policy denied the operation")] PolicyDenied, - /// The referenced object does not exist. + #[error("unknown broker object")] UnknownObject, - /// The caller lacks the required broker rights. + #[error("invalid broker rights")] InvalidRights, - /// Broker-side resource exhaustion. + #[error("broker resource exhausted")] ResourceExhausted, - /// A broker core has already been created in this process. + #[error("broker core already exists")] BrokerCoreAlreadyExists, - /// The operation would block in the current object state. + #[error("broker operation would block")] WouldBlock, - /// The operation is not implemented by this BrokerCore. + #[error("unsupported broker operation")] UnsupportedOperation, } - -impl fmt::Display for BrokerError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::PolicyDenied => f.write_str("broker policy denied the operation"), - Self::UnknownObject => f.write_str("unknown broker object"), - Self::InvalidRights => f.write_str("invalid broker rights"), - Self::ResourceExhausted => f.write_str("broker resource exhausted"), - Self::BrokerCoreAlreadyExists => f.write_str("broker core already exists"), - Self::WouldBlock => f.write_str("broker operation would block"), - Self::UnsupportedOperation => f.write_str("unsupported broker operation"), - } - } -} - -impl core::error::Error for BrokerError {} diff --git a/litebox_broker_host/Cargo.toml b/litebox_broker_host/Cargo.toml index fdeb2b609b..3457368fdd 100644 --- a/litebox_broker_host/Cargo.toml +++ b/litebox_broker_host/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +thiserror = { version = "2.0.6", default-features = false } [lints] workspace = true diff --git a/litebox_broker_host/src/error.rs b/litebox_broker_host/src/error.rs index 81a34d6462..c9d8f5a4a0 100644 --- a/litebox_broker_host/src/error.rs +++ b/litebox_broker_host/src/error.rs @@ -1,37 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use core::fmt; +use thiserror::Error; /// Errors returned by a broker-host receive/send loop. -#[derive(Debug)] +#[derive(Debug, Error)] #[non_exhaustive] pub enum BrokerHostError { - /// The host could not authenticate the peer or allocate broker association state. + #[error("broker association setup failed")] AssociationSetup, - /// The concrete channel failed. - Channel(E), -} - -impl fmt::Display for BrokerHostError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::AssociationSetup => f.write_str("broker association setup failed"), - Self::Channel(error) => write!(f, "broker channel failed: {error}"), - } - } -} - -impl core::error::Error for BrokerHostError -where - E: core::error::Error + 'static, -{ - fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { - match self { - Self::AssociationSetup => None, - Self::Channel(error) => Some(error), - } - } + #[error("broker channel failed: {0}")] + Channel(#[source] E), } /// Broker-host receive/send loop result type. diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 898d437ef3..e1fecb6b87 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -382,10 +382,10 @@ mod tests { protocol_version: HOST_PROTOCOL_VERSION, }), ))])); - channel.send_error = Some(FakeChannelError::Send); + channel.send_error = true; match serve_connection(core, &mut channel) { - Err(BrokerHostError::Channel(FakeChannelError::Send)) => {} + Err(BrokerHostError::Channel(())) => {} result => panic!("unexpected serve result: {result:?}"), } assert!(channel.responses.is_empty()); @@ -399,44 +399,26 @@ mod tests { event_request(EventRequest::Create(CreateEventRequest::new(initial_count))) } - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - enum FakeChannelError { - Send, - } - - impl fmt::Display for FakeChannelError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Send => f.write_str("fake send error"), - } - } - } - - impl core::error::Error for FakeChannelError {} - struct FakeHostControlChannel { - requests: - std::vec::Vec, FakeChannelError>>, + requests: std::vec::Vec, ()>>, responses: std::vec::Vec, - send_error: Option, + send_error: bool, } impl FakeHostControlChannel { fn new( - requests: std::vec::Vec< - core::result::Result, FakeChannelError>, - >, + requests: std::vec::Vec, ()>>, ) -> Self { Self { requests, responses: std::vec::Vec::new(), - send_error: None, + send_error: false, } } } impl HostControlChannel for FakeHostControlChannel { - type Error = FakeChannelError; + type Error = (); fn peer_credential(&self) -> core::result::Result { Ok(PeerCredential::Unauthenticated) @@ -456,8 +438,8 @@ mod tests { &mut self, response: &BrokerResponse, ) -> core::result::Result<(), Self::Error> { - if let Some(error) = self.send_error { - return Err(error); + if self.send_error { + return Err(()); } self.responses.push(response.clone()); Ok(()) diff --git a/litebox_broker_local/Cargo.toml b/litebox_broker_local/Cargo.toml index d3574f9258..40d00c9ce9 100644 --- a/litebox_broker_local/Cargo.toml +++ b/litebox_broker_local/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +thiserror = { version = "2.0.6", default-features = false } [lints] workspace = true diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 45696b20d2..d79aa4a6b5 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -1,127 +1,64 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use core::fmt; - use litebox_broker_protocol::{BrokerResponse, ErrorCode, ProtocolVersion}; +use thiserror::Error; /// Errors returned by the broker-local control adapter. -#[derive(Debug)] +#[derive(Debug, Error)] #[non_exhaustive] pub enum BrokerLocalError { - /// The control channel failed. - Channel(E), - /// An operation requiring an active broker session was called before negotiation. + #[error("broker channel failed: {0}")] + Channel(#[source] E), + #[error("broker local adapter has not negotiated protocol version")] NotNegotiated, - /// Negotiation was requested after the local adapter was already active. + #[error("broker local adapter already negotiated")] AlreadyNegotiated, - /// The broker closed the channel before returning a response. + #[error("broker closed the channel")] ChannelClosed, - /// The broker returned a response this local adapter does not understand. + #[error("unknown broker response")] UnknownResponse, - /// The broker accepted negotiation with a version that cannot serve the request. + #[error( + "broker accepted incompatible protocol negotiation: requested {requested:?}, broker supports {broker_protocol_version:?}" + )] IncompatibleNegotiation { /// Protocol version requested by this local adapter. requested: ProtocolVersion, /// Protocol version advertised by the broker. broker_protocol_version: ProtocolVersion, }, - /// This local adapter cannot speak the requested protocol version. + #[error( + "broker local adapter cannot request protocol version {requested:?}; local adapter supports {local_protocol_version:?}" + )] UnsupportedLocalVersion { /// Protocol version requested by the caller. requested: ProtocolVersion, /// Protocol version supported by this local implementation. local_protocol_version: ProtocolVersion, }, - /// The active broker session cannot serve an operation requiring a newer version. + #[error( + "broker session protocol version {negotiated_protocol_version:?} does not support required version {required:?}" + )] UnsupportedNegotiatedVersion { /// Protocol version required by the operation. required: ProtocolVersion, /// Effective protocol version negotiated for this connection. negotiated_protocol_version: ProtocolVersion, }, - /// The broker does not support the requested protocol version. + #[error( + "broker does not support requested protocol version {requested:?}; broker supports {broker_protocol_version:?}" + )] UnsupportedVersion { /// Protocol version requested by this local adapter. requested: ProtocolVersion, /// Protocol version advertised by the broker. broker_protocol_version: ProtocolVersion, }, - /// The broker rejected the request. - Broker(ErrorCode), - /// The broker returned a response type that does not match the request. + #[error("broker rejected request: {0}")] + Broker(#[source] ErrorCode), + #[error("broker returned unexpected response: {0:?}")] UnexpectedResponse(BrokerResponse), } -impl fmt::Display for BrokerLocalError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Channel(error) => write!(f, "broker channel failed: {error}"), - Self::NotNegotiated => { - write!( - f, - "broker local adapter has not negotiated protocol version" - ) - } - Self::AlreadyNegotiated => f.write_str("broker local adapter already negotiated"), - Self::ChannelClosed => write!(f, "broker closed the channel"), - Self::UnknownResponse => f.write_str("unknown broker response"), - Self::IncompatibleNegotiation { - requested, - broker_protocol_version, - } => write!( - f, - "broker accepted incompatible protocol negotiation: requested {requested:?}, broker supports {broker_protocol_version:?}" - ), - Self::UnsupportedLocalVersion { - requested, - local_protocol_version, - } => write!( - f, - "broker local adapter cannot request protocol version {requested:?}; local adapter supports {local_protocol_version:?}" - ), - Self::UnsupportedNegotiatedVersion { - required, - negotiated_protocol_version, - } => write!( - f, - "broker session protocol version {negotiated_protocol_version:?} does not support required version {required:?}" - ), - Self::UnsupportedVersion { - requested, - broker_protocol_version, - } => write!( - f, - "broker does not support requested protocol version {requested:?}; broker supports {broker_protocol_version:?}" - ), - Self::Broker(error) => write!(f, "broker rejected request: {error}"), - Self::UnexpectedResponse(response) => { - write!(f, "broker returned unexpected response: {response:?}") - } - } - } -} - -impl core::error::Error for BrokerLocalError -where - E: core::error::Error + 'static, -{ - fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { - match self { - Self::Channel(error) => Some(error), - Self::Broker(error) => Some(error), - Self::NotNegotiated - | Self::AlreadyNegotiated - | Self::ChannelClosed - | Self::UnknownResponse - | Self::IncompatibleNegotiation { .. } - | Self::UnsupportedLocalVersion { .. } - | Self::UnsupportedNegotiatedVersion { .. } - | Self::UnsupportedVersion { .. } - | Self::UnexpectedResponse(_) => None, - } - } -} - /// Broker-local control adapter result type. pub type Result = core::result::Result>; diff --git a/litebox_broker_protocol/Cargo.toml b/litebox_broker_protocol/Cargo.toml index 2fccbca4ee..9cfecae7ec 100644 --- a/litebox_broker_protocol/Cargo.toml +++ b/litebox_broker_protocol/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +thiserror = { version = "2.0.6", default-features = false } [lints] workspace = true diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index a269321c98..2c28328f1a 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -1,36 +1,37 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use core::fmt; +use thiserror::Error; /// ABI-neutral broker error category. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ErrorCode { - /// The requested protocol version is unsupported. + #[error("unsupported broker protocol version")] UnsupportedVersion, - /// The request is structurally invalid. + #[error("malformed broker request")] MalformedRequest, - /// The request is validly encoded but violates the connection state machine. + #[error("broker protocol state violation")] ProtocolState, - /// The request is unsupported by this broker protocol implementation. + #[error("unsupported broker operation")] UnsupportedOperation, - /// Broker hit an internal condition or an error category this protocol cannot represent. + #[error("internal broker error")] Internal, - /// Policy denied the operation. + #[error("broker policy denied the operation")] PolicyDenied, - /// The referenced object does not exist. + #[error("unknown broker object")] UnknownObject, - /// The caller lacks the required broker rights. + #[error("invalid broker rights")] InvalidRights, - /// Broker-side resource exhaustion. + #[error("broker resource exhausted")] ResourceExhausted, - /// The operation would block in the current event state. + #[error("broker operation would block")] WouldBlock, /// Error code emitted by a newer broker and not understood by this local peer. /// /// This variant is reserved for raw codes not assigned by this protocol /// version. + #[error("unknown broker error code {0}")] Unknown(u16), } @@ -75,23 +76,3 @@ impl ErrorCode { } } } - -impl fmt::Display for ErrorCode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::UnsupportedVersion => f.write_str("unsupported broker protocol version"), - Self::MalformedRequest => f.write_str("malformed broker request"), - Self::ProtocolState => f.write_str("broker protocol state violation"), - Self::UnsupportedOperation => f.write_str("unsupported broker operation"), - Self::Internal => f.write_str("internal broker error"), - Self::PolicyDenied => f.write_str("broker policy denied the operation"), - Self::UnknownObject => f.write_str("unknown broker object"), - Self::InvalidRights => f.write_str("invalid broker rights"), - Self::ResourceExhausted => f.write_str("broker resource exhausted"), - Self::WouldBlock => f.write_str("broker operation would block"), - Self::Unknown(raw) => write!(f, "unknown broker error code {raw}"), - } - } -} - -impl core::error::Error for ErrorCode {} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 0415a28789..1d5b278f9d 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -16,9 +16,8 @@ //! changing fields is an ABI change, so prefer a new operation tag or explicit //! negotiated-version gate for payload evolution. -use core::fmt; - use alloc::vec::Vec; +use thiserror::Error; use crate::{ BrokerRequest, BrokerResponse, ErrorCode, ReceivedBrokerRequest, ReceivedBrokerResponse, @@ -39,32 +38,19 @@ const RESPONSE_TAG_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; /// Error produced while encoding or decoding a broker wire message. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] #[non_exhaustive] pub enum WireError { - /// The frame ended before a complete field could be decoded. + #[error("truncated broker wire frame")] TruncatedFrame, - /// The frame contained bytes after the decoded message. + #[error("trailing broker wire bytes")] TrailingBytes, - /// A boolean field was not encoded as 0 or 1. + #[error("invalid broker wire boolean")] InvalidBoolean, - /// A decoder offset overflowed. + #[error("broker wire offset overflow")] OffsetOverflow, } -impl fmt::Display for WireError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::TruncatedFrame => f.write_str("truncated broker wire frame"), - Self::TrailingBytes => f.write_str("trailing broker wire bytes"), - Self::InvalidBoolean => f.write_str("invalid broker wire boolean"), - Self::OffsetOverflow => f.write_str("broker wire offset overflow"), - } - } -} - -impl core::error::Error for WireError {} - /// Encodes a broker request body. /// /// Successful encodings are always non-empty because the first byte is the From 6995b1d96b9f306f38d5ecefbc4dc6757ed4e75a Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 19 Jun 2026 13:02:43 -0700 Subject: [PATCH 043/319] Add partals threadpool support to the Windows shim (#940) Add Windows shim support for the NT threadpool-related object subset: I/O completion ports, wait completion packets, worker factories, and Timer2 handles. Note that this is a compatibility subset focused on object creation, validation, access checks, and observable state. Full IOCP packet queues, timer scheduling/callback behavior, and actual worker-thread creation/draining are intentionally left as follow-up work. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_common_windows/src/nt_status.rs | 16 + litebox_shim_windows/src/lib.rs | 204 +++ litebox_shim_windows/src/syscalls/event.rs | 14 + litebox_shim_windows/src/syscalls/iocp.rs | 353 ++++ litebox_shim_windows/src/syscalls/mod.rs | 135 ++ litebox_shim_windows/src/syscalls/timer.rs | 569 +++++++ .../src/syscalls/wait_completion_packet.rs | 1468 +++++++++++++++++ .../src/syscalls/worker_factory.rs | 1130 +++++++++++++ 8 files changed, 3889 insertions(+) create mode 100644 litebox_shim_windows/src/syscalls/iocp.rs create mode 100644 litebox_shim_windows/src/syscalls/timer.rs create mode 100644 litebox_shim_windows/src/syscalls/wait_completion_packet.rs create mode 100644 litebox_shim_windows/src/syscalls/worker_factory.rs diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index de40ada1c6..22e33592f7 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -176,6 +176,9 @@ impl NtStatus { 0xC00000BB => "STATUS_NOT_SUPPORTED: The request is not supported", 0xC00000E6 => "STATUS_GENERIC_NOT_MAPPED: Generic not mapped", 0xC00000EF => "STATUS_INVALID_PARAMETER_1: Invalid parameter 1", + 0xC00000F0 => "STATUS_INVALID_PARAMETER_2: Invalid parameter 2", + 0xC00000F1 => "STATUS_INVALID_PARAMETER_3: Invalid parameter 3", + 0xC00000F2 => "STATUS_INVALID_PARAMETER_4: Invalid parameter 4", 0xC00000FD => "STATUS_STACK_OVERFLOW: Stack overflow", 0xC0000102 => "STATUS_FILE_CORRUPT_ERROR: File corrupt error", 0xC0000103 => "STATUS_NOT_A_DIRECTORY: Not a directory", @@ -187,6 +190,7 @@ impl NtStatus { 0xC0000109 => "STATUS_MESSAGE_NOT_FOUND: Message not found", 0xC000010A => "STATUS_PROCESS_IS_TERMINATING: Process is terminating", 0xC000010D => "STATUS_CANNOT_IMPERSONATE: Cannot impersonate", + 0xC0000120 => "STATUS_CANCELLED: The operation was cancelled", 0xC0000121 => "STATUS_CANNOT_DELETE: Cannot delete", 0xC0000128 => "STATUS_FILE_CLOSED: File closed", 0xC0000142 => "STATUS_DLL_INIT_FAILED: DLL initialization failed", @@ -490,6 +494,15 @@ impl NtStatus { /// STATUS_INVALID_PARAMETER_1 pub const INVALID_PARAMETER_1: Self = Self::from_raw(0xC00000EF); + /// STATUS_INVALID_PARAMETER_2 + pub const INVALID_PARAMETER_2: Self = Self::from_raw(0xC00000F0); + + /// STATUS_INVALID_PARAMETER_3 + pub const INVALID_PARAMETER_3: Self = Self::from_raw(0xC00000F1); + + /// STATUS_INVALID_PARAMETER_4 + pub const INVALID_PARAMETER_4: Self = Self::from_raw(0xC00000F2); + /// STATUS_STACK_OVERFLOW pub const STACK_OVERFLOW: Self = Self::from_raw(0xC00000FD); @@ -523,6 +536,9 @@ impl NtStatus { /// STATUS_CANNOT_IMPERSONATE pub const CANNOT_IMPERSONATE: Self = Self::from_raw(0xC000010D); + /// STATUS_CANCELLED + pub const CANCELLED: Self = Self::from_raw(0xC0000120); + /// STATUS_CANNOT_DELETE pub const CANNOT_DELETE: Self = Self::from_raw(0xC0000121); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 5386417883..54a45a50ff 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -33,7 +33,16 @@ use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; use crate::syscalls::event::{EventHandleObject, EventObject, EventSubsystem}; use crate::syscalls::file::{FileObject, FileObjectSubsystem}; +use crate::syscalls::iocp::{IoCompletionHandleObject, IoCompletionSubsystem}; use crate::syscalls::registry::{RegistryKeyObject, RegistryKeySubsystem}; +use crate::syscalls::timer::{TimerCreateParameters, TimerHandleObject, TimerSubsystem}; +use crate::syscalls::wait_completion_packet::{ + WaitCompletionPacketAssociateParameters, WaitCompletionPacketHandleObject, + WaitCompletionPacketSubsystem, +}; +use crate::syscalls::worker_factory::{ + WorkerFactoryCreateParameters, WorkerFactoryHandleObject, WorkerFactorySubsystem, +}; use crate::syscalls::{SyscallRequest, mm}; mod loader; @@ -466,6 +475,139 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtCreateIoCompletion { + io_completion_handle, + desired_access, + object_attributes, + number_of_concurrent_threads, + } => { + let status = self.sys_nt_create_io_completion( + io_completion_handle, + desired_access, + object_attributes, + number_of_concurrent_threads, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateWaitCompletionPacket { + wait_completion_packet_handle, + desired_access, + object_attributes, + } => { + let status = self.sys_nt_create_wait_completion_packet( + wait_completion_packet_handle, + desired_access, + object_attributes, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtAssociateWaitCompletionPacket { + wait_completion_packet_handle, + io_completion_handle, + target_object_handle, + key_context, + apc_context, + io_status, + io_status_information, + already_signaled, + } => { + let status = self.sys_nt_associate_wait_completion_packet( + WaitCompletionPacketAssociateParameters { + wait_completion_packet_handle, + io_completion_handle, + target_object_handle, + key_context, + apc_context, + io_status, + io_status_information, + already_signaled, + }, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCancelWaitCompletionPacket { + wait_completion_packet_handle, + remove_signaled_packet, + } => { + let status = self.sys_nt_cancel_wait_completion_packet( + wait_completion_packet_handle, + remove_signaled_packet, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateWorkerFactory { + worker_factory_handle, + desired_access, + object_attributes, + completion_port_handle, + worker_process_handle, + start_routine, + start_parameter, + max_thread_count, + stack_reserve, + stack_commit, + } => { + let status = self.sys_nt_create_worker_factory(WorkerFactoryCreateParameters { + worker_factory_handle, + desired_access, + object_attributes, + completion_port_handle, + worker_process_handle, + start_routine, + start_parameter, + max_thread_count, + stack_reserve, + stack_commit, + }); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtSetInformationWorkerFactory { + worker_factory_handle, + worker_factory_information_class, + worker_factory_information, + worker_factory_information_length, + } => { + let status = self.sys_nt_set_information_worker_factory( + worker_factory_handle, + worker_factory_information_class, + worker_factory_information, + worker_factory_information_length, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtShutdownWorkerFactory { + worker_factory_handle, + pending_worker_count, + } => { + let status = self + .sys_nt_shutdown_worker_factory(worker_factory_handle, pending_worker_count); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateTimer2 { + timer_handle, + timer_id, + object_attributes, + attributes, + desired_access, + } => { + let status = self.sys_nt_create_timer2(TimerCreateParameters { + timer_handle, + timer_id, + object_attributes, + attributes, + desired_access, + }); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtSetTimer2 { + timer_handle, + due_time, + period, + parameters, + } => { + let status = self.sys_nt_set_timer2(timer_handle, due_time, period, parameters); + (status, ContinueOperation::Resume) + } SyscallRequest::NtOpenEvent { event_handle, desired_access, @@ -869,6 +1011,38 @@ impl Task { ) { return NtStatus::SUCCESS; } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |io_completion| visitor.io_completion(io_completion), + ) { + return NtStatus::SUCCESS; + } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |timer| visitor.timer(timer), + ) { + return NtStatus::SUCCESS; + } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |wait_completion_packet| visitor.wait_completion_packet(wait_completion_packet), + ) { + return NtStatus::SUCCESS; + } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |worker_factory| visitor.worker_factory(worker_factory), + ) { + return NtStatus::SUCCESS; + } NtStatus::INVALID_HANDLE } @@ -890,6 +1064,17 @@ trait RawHandleVisitor { fn registry_key(&self, key: RegistryKeyObject); fn event(&self, event: EventHandleObject); + + fn io_completion(&self, io_completion: IoCompletionHandleObject); + + fn timer(&self, timer: TimerHandleObject); + + fn wait_completion_packet( + &self, + wait_completion_packet: WaitCompletionPacketHandleObject, + ); + + fn worker_factory(&self, worker_factory: WorkerFactoryHandleObject); } struct CloseRawHandleVisitor<'task, Platform: ShimPlatform, FS: ShimFS> { @@ -910,6 +1095,25 @@ impl RawHandleVisitor fn event(&self, event: EventHandleObject) { Task::::close_event(event); } + + fn io_completion(&self, io_completion: IoCompletionHandleObject) { + Task::::close_io_completion(io_completion); + } + + fn timer(&self, timer: TimerHandleObject) { + Task::::close_timer(timer); + } + + fn wait_completion_packet( + &self, + wait_completion_packet: WaitCompletionPacketHandleObject, + ) { + Task::::close_wait_completion_packet(wait_completion_packet); + } + + fn worker_factory(&self, worker_factory: WorkerFactoryHandleObject) { + Task::::close_worker_factory(worker_factory); + } } /// The shim entrypoint object passed to the platform. diff --git a/litebox_shim_windows/src/syscalls/event.rs b/litebox_shim_windows/src/syscalls/event.rs index 0a5b371e42..7807341a1b 100644 --- a/litebox_shim_windows/src/syscalls/event.rs +++ b/litebox_shim_windows/src/syscalls/event.rs @@ -162,6 +162,10 @@ impl EventObject { } } + pub(crate) fn is_signaled(&self) -> bool { + *self.signaled.lock() + } + fn replace_state(&self, next: bool) -> i32 { let mut signaled = self.signaled.lock(); let previous = i32::from(*signaled); @@ -170,6 +174,16 @@ impl EventObject { } } +impl EventHandleObject { + pub(crate) fn require_access(&self, required: EventAccess) -> Result<(), NtStatus> { + self.granted_access.require(required) + } + + pub(crate) fn is_signaled(&self) -> bool { + self.event.is_signaled() + } +} + impl IOPollable for EventObject { fn register_observer(&self, observer: Weak>, mask: Events) { self.pollee.register_observer(observer, mask); diff --git a/litebox_shim_windows/src/syscalls/iocp.rs b/litebox_shim_windows/src/syscalls/iocp.rs new file mode 100644 index 0000000000..57ccac2e66 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/iocp.rs @@ -0,0 +1,353 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NT I/O completion port syscalls. + +use alloc::sync::Arc; +use core::marker::PhantomData; + +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawMutPointer as _, RawPointerProvider}; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::{AccessMask, ObjectAttributes, read_object_attributes}; +use crate::syscalls::Handle; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, + remove_raw_handle, +}; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct IoCompletionAccess: u32 { + const QUERY_STATE = 0x0001; + const MODIFY_STATE = 0x0002; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() | Self::QUERY_STATE.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits() | Self::MODIFY_STATE.bits(); + const EXECUTE = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() | AccessMask::SYNCHRONIZE.bits(); + const ALL_ACCESS = AccessMask::STANDARD_RIGHTS_ALL.bits() + | Self::QUERY_STATE.bits() + | Self::MODIFY_STATE.bits(); + + const _ = !0; + } +} + +impl IoCompletionAccess { + fn from_desired_access(desired_access: u32) -> Self { + let mut access = Self::from_bits_retain(desired_access); + if desired_access & AccessMask::GENERIC_READ.bits() != 0 { + access.insert(Self::READ); + } + if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { + access.insert(Self::WRITE); + } + if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { + access.insert(Self::EXECUTE); + } + if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { + access.insert(Self::ALL_ACCESS); + } + access.remove(Self::from_bits_retain( + AccessMask::GENERIC_READ.bits() + | AccessMask::GENERIC_WRITE.bits() + | AccessMask::GENERIC_EXECUTE.bits() + | AccessMask::GENERIC_ALL.bits(), + )); + access + } + + pub(crate) fn require(self, required: Self) -> Result<(), NtStatus> { + if self.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +pub(crate) struct IoCompletionSubsystem(PhantomData); + +impl FdEnabledSubsystem for IoCompletionSubsystem { + type Entry = IoCompletionHandleObject; +} + +impl FdEnabledSubsystemEntry for IoCompletionHandleObject {} + +pub(crate) struct IoCompletionHandleObject { + port: Arc>, + granted_access: IoCompletionAccess, +} + +pub(crate) struct IoCompletionObject { + _number_of_concurrent_threads: u32, + _not_send_without_platform: PhantomData, +} + +impl IoCompletionObject { + fn new(number_of_concurrent_threads: u32) -> Self { + Self { + _number_of_concurrent_threads: number_of_concurrent_threads, + _not_send_without_platform: PhantomData, + } + } +} + +impl IoCompletionHandleObject { + pub(crate) fn port(&self) -> Arc> { + self.port.clone() + } + + pub(crate) fn require_access(&self, required: IoCompletionAccess) -> Result<(), NtStatus> { + self.granted_access.require(required) + } +} + +fn validate_io_completion_object_attributes( + object_attributes: Option>, +) -> Result<(), NtStatus> { + let Some(object_attributes) = object_attributes else { + return Ok(()); + }; + let object_attributes = read_object_attributes::(object_attributes)?; + if object_attributes.object_name == 0 && !object_attributes.root_directory.is_null() { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + Ok(()) +} + +impl Task { + fn insert_io_completion_handle( + &self, + port: Arc>, + granted_access: IoCompletionAccess, + ) -> Result { + let typed = self + .global + .litebox + .descriptor_table_mut() + .insert::>(IoCompletionHandleObject { + port, + granted_access, + }); + insert_raw_handle::>( + &self.global.litebox, + &self.process.handles, + typed, + drop, + ) + } + + pub(crate) fn close_io_completion_handle(&self, handle: Handle) { + remove_raw_handle::>( + &self.global.litebox, + &self.process.handles, + handle, + drop, + ); + } + + pub(crate) fn close_io_completion(io_completion: IoCompletionHandleObject) { + drop(io_completion); + } + + pub(crate) fn sys_nt_create_io_completion( + &self, + io_completion_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + number_of_concurrent_threads: u32, + ) -> NtStatus { + if let Err(status) = + probe_guest_output_preserving_value::(io_completion_handle) + { + return status; + } + if let Err(status) = validate_io_completion_object_attributes::(object_attributes) + { + return status; + } + + // TODO: model the IOCP packet queue, concurrency accounting, named-object lookup, + // and file-handle association once completion posting/removal and file completion + // context syscalls are implemented. + let port = Arc::new(IoCompletionObject::new(number_of_concurrent_threads)); + let granted_access = IoCompletionAccess::from_desired_access(desired_access); + let Ok(handle) = self.insert_io_completion_handle(port, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if io_completion_handle.write_at_offset(0, handle).is_none() { + self.close_io_completion_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } +} + +#[cfg(test)] +mod tests { + use core::mem::size_of; + + use litebox::utils::TruncateExt as _; + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::ObjectAttributes; + use crate::tests::{const_ptr, mut_ptr, test_task}; + + const IO_COMPLETION_ALL_ACCESS: u32 = 0x001f_0003; + + fn object_attributes_size() -> u32 { + size_of::().trunc() + } + + #[test] + fn create_validates_object_attributes_without_clobbering_output() { + let task = test_task(); + let mut handle = Handle::from_raw(usize::MAX); + let bad_length = ObjectAttributes { + length: 1, + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + + assert_eq!( + task.sys_nt_create_io_completion( + mut_ptr(&mut handle), + IO_COMPLETION_ALL_ACCESS, + Some(const_ptr(&bad_length)), + 0, + ), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::from_raw(usize::MAX)); + + let root_without_name = ObjectAttributes { + length: object_attributes_size(), + root_directory: Handle::from_raw(4), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + assert_eq!( + task.sys_nt_create_io_completion( + mut_ptr(&mut handle), + IO_COMPLETION_ALL_ACCESS, + Some(const_ptr(&root_without_name)), + 0, + ), + NtStatus::OBJECT_NAME_INVALID + ); + assert_eq!(handle, Handle::from_raw(usize::MAX)); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn host_create_io_completion_status_fidelity() { + use core::ffi::c_void; + + unsafe extern "system" { + fn NtCreateIoCompletion( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + number_of_concurrent_threads: u32, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } + + let mut host_handle = core::ptr::null_mut(); + // SAFETY: The output pointer is valid, object attributes are null as accepted by native + // NtCreateIoCompletion, and the returned host handle is closed before leaving the test. + let host_success = unsafe { + let status = NtCreateIoCompletion( + &raw mut host_handle, + IO_COMPLETION_ALL_ACCESS, + core::ptr::null(), + 0, + ); + if status == NtStatus::SUCCESS.as_raw() && !host_handle.is_null() { + assert_eq!(NtClose(host_handle), NtStatus::SUCCESS.as_raw()); + } + status + }; + + let task = test_task(); + let mut shim_handle = Handle::default(); + assert_eq!( + task.sys_nt_create_io_completion( + mut_ptr(&mut shim_handle), + IO_COMPLETION_ALL_ACCESS, + None, + 0, + ) + .as_raw(), + host_success + ); + assert!(!shim_handle.is_null()); + + let bad_length = ObjectAttributes { + length: 1, + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + // SAFETY: The host output and attributes pointers are valid locals; the bad length is the + // parameter being tested. + let host_bad_length = unsafe { + NtCreateIoCompletion( + &raw mut host_handle, + IO_COMPLETION_ALL_ACCESS, + &raw const bad_length, + 0, + ) + }; + assert_eq!( + task.sys_nt_create_io_completion( + mut_ptr(&mut shim_handle), + IO_COMPLETION_ALL_ACCESS, + Some(const_ptr(&bad_length)), + 0, + ) + .as_raw(), + host_bad_length + ); + + let root_without_name = ObjectAttributes { + length: object_attributes_size(), + root_directory: Handle::from_raw(4), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + // SAFETY: The host output and attributes pointers are valid locals; root without an object + // name is the probed native behavior. + let host_root_without_name = unsafe { + NtCreateIoCompletion( + &raw mut host_handle, + IO_COMPLETION_ALL_ACCESS, + &raw const root_without_name, + 0, + ) + }; + let mut shim_root_without_name_handle = Handle::from_raw(usize::MAX); + assert_eq!( + task.sys_nt_create_io_completion( + mut_ptr(&mut shim_root_without_name_handle), + IO_COMPLETION_ALL_ACCESS, + Some(const_ptr(&root_without_name)), + 0, + ) + .as_raw(), + host_root_without_name + ); + } +} diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 41667e0b3b..28ff98bd0c 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -3,11 +3,15 @@ pub(crate) mod event; pub(crate) mod file; +pub(crate) mod iocp; pub(crate) mod mm; pub(crate) mod nls; pub(crate) mod process; pub(crate) mod registry; mod sysinfo; +pub(crate) mod timer; +pub(crate) mod wait_completion_packet; +pub(crate) mod worker_factory; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; @@ -81,6 +85,11 @@ impl ProcessHandle { pub(crate) fn is_current(self) -> bool { self == Self::CURRENT } + + #[must_use] + pub(crate) const fn as_handle(self) -> Handle { + self.0 + } } #[allow(clippy::enum_variant_names)] @@ -96,6 +105,66 @@ pub(crate) enum SyscallRequest { event_type: u32, initial_state: u8, }, + NtCreateIoCompletion { + io_completion_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + number_of_concurrent_threads: u32, + }, + NtCreateWaitCompletionPacket { + wait_completion_packet_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, + NtAssociateWaitCompletionPacket { + wait_completion_packet_handle: Handle, + io_completion_handle: Handle, + target_object_handle: Handle, + key_context: usize, + apc_context: usize, + io_status: i32, + io_status_information: usize, + already_signaled: Option>, + }, + NtCancelWaitCompletionPacket { + wait_completion_packet_handle: Handle, + remove_signaled_packet: u8, + }, + NtCreateWorkerFactory { + worker_factory_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + completion_port_handle: Handle, + worker_process_handle: ProcessHandle, + start_routine: usize, + start_parameter: usize, + max_thread_count: u32, + stack_reserve: usize, + stack_commit: usize, + }, + NtSetInformationWorkerFactory { + worker_factory_handle: Handle, + worker_factory_information_class: u32, + worker_factory_information: Platform::RawConstPointer, + worker_factory_information_length: u32, + }, + NtShutdownWorkerFactory { + worker_factory_handle: Handle, + pending_worker_count: Platform::RawMutPointer, + }, + NtCreateTimer2 { + timer_handle: Platform::RawMutPointer, + timer_id: Option>, + object_attributes: Option>, + attributes: u32, + desired_access: u32, + }, + NtSetTimer2 { + timer_handle: Handle, + due_time: Option>, + period: Option>, + parameters: Option>, + }, NtOpenEvent { event_handle: Platform::RawMutPointer, desired_access: u32, @@ -297,6 +366,72 @@ impl SyscallRequest { event_type, initial_state, })), + NtSysno::NtCreateIoCompletion => Some(sys_req!(NtCreateIoCompletion { + io_completion_handle:*, + desired_access, + object_attributes:*, + number_of_concurrent_threads, + })), + NtSysno::NtCreateWaitCompletionPacket => Some(sys_req!( + NtCreateWaitCompletionPacket { + wait_completion_packet_handle:*, + desired_access, + object_attributes:*, + } + )), + NtSysno::NtAssociateWaitCompletionPacket => Some(sys_req!( + NtAssociateWaitCompletionPacket { + wait_completion_packet_handle:{Handle::from_raw}, + io_completion_handle:{Handle::from_raw}, + target_object_handle:{Handle::from_raw}, + key_context, + apc_context, + io_status, + io_status_information, + already_signaled:*, + } + )), + NtSysno::NtCancelWaitCompletionPacket => Some(sys_req!(NtCancelWaitCompletionPacket { + wait_completion_packet_handle: { Handle::from_raw }, + remove_signaled_packet, + })), + NtSysno::NtCreateWorkerFactory => Some(sys_req!(NtCreateWorkerFactory { + worker_factory_handle:*, + desired_access, + object_attributes:*, + completion_port_handle:{Handle::from_raw}, + worker_process_handle:{ProcessHandle::from_raw}, + start_routine, + start_parameter, + max_thread_count, + stack_reserve, + stack_commit, + })), + NtSysno::NtSetInformationWorkerFactory => Some(sys_req!( + NtSetInformationWorkerFactory { + worker_factory_handle:{Handle::from_raw}, + worker_factory_information_class, + worker_factory_information:*, + worker_factory_information_length, + } + )), + NtSysno::NtShutdownWorkerFactory => Some(sys_req!(NtShutdownWorkerFactory { + worker_factory_handle:{Handle::from_raw}, + pending_worker_count:*, + })), + NtSysno::NtCreateTimer2 => Some(sys_req!(NtCreateTimer2 { + timer_handle:*, + timer_id:*, + object_attributes:*, + attributes, + desired_access, + })), + NtSysno::NtSetTimer2 => Some(sys_req!(NtSetTimer2 { + timer_handle:{Handle::from_raw}, + due_time:*, + period:*, + parameters:*, + })), NtSysno::NtOpenEvent => Some(sys_req!(NtOpenEvent { event_handle:*, desired_access, diff --git a/litebox_shim_windows/src/syscalls/timer.rs b/litebox_shim_windows/src/syscalls/timer.rs new file mode 100644 index 0000000000..607f99481b --- /dev/null +++ b/litebox_shim_windows/src/syscalls/timer.rs @@ -0,0 +1,569 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NT timer syscalls. + +use alloc::sync::Arc; +use core::marker::PhantomData; + +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::{AccessMask, ObjectAttributes}; +use crate::syscalls::Handle; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, + raw_handle_entry, remove_raw_handle, +}; + +const TIMER2_ATTRIBUTE_IR_TIMER: u32 = 0x0000_0002; +const TIMER2_ATTRIBUTE_HIGH_RESOLUTION: u32 = 0x0000_0004; +const TIMER2_ATTRIBUTE_NO_WAKE: u32 = 0x0000_0008; +const TIMER2_ATTRIBUTE_NOTIFICATION: u32 = 0x8000_0000; +const TIMER2_ATTRIBUTE_KNOWN_MASK: u32 = TIMER2_ATTRIBUTE_IR_TIMER + | TIMER2_ATTRIBUTE_HIGH_RESOLUTION + | TIMER2_ATTRIBUTE_NO_WAKE + | TIMER2_ATTRIBUTE_NOTIFICATION; +const TIMER2_ATTRIBUTE_RESERVED_MASK: u32 = !TIMER2_ATTRIBUTE_KNOWN_MASK; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct TimerAccess: u32 { + const QUERY_STATE = 0x0001; + const MODIFY_STATE = 0x0002; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() | Self::QUERY_STATE.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits() | Self::MODIFY_STATE.bits(); + const EXECUTE = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() | AccessMask::SYNCHRONIZE.bits(); + const ALL_ACCESS = AccessMask::STANDARD_RIGHTS_ALL.bits() + | Self::QUERY_STATE.bits() + | Self::MODIFY_STATE.bits(); + + const _ = !0; + } +} + +impl TimerAccess { + fn from_desired_access(desired_access: u32) -> Self { + let mut access = Self::from_bits_retain(desired_access); + if desired_access & AccessMask::GENERIC_READ.bits() != 0 { + access.insert(Self::READ); + } + if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { + access.insert(Self::WRITE); + } + if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { + access.insert(Self::EXECUTE); + } + if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { + access.insert(Self::ALL_ACCESS); + } + access.remove(Self::from_bits_retain( + AccessMask::GENERIC_READ.bits() + | AccessMask::GENERIC_WRITE.bits() + | AccessMask::GENERIC_EXECUTE.bits() + | AccessMask::GENERIC_ALL.bits(), + )); + access + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct Timer2Attributes: u32 { + const HIGH_RESOLUTION = TIMER2_ATTRIBUTE_HIGH_RESOLUTION; + const NO_WAKE = TIMER2_ATTRIBUTE_NO_WAKE; + const NOTIFICATION = TIMER2_ATTRIBUTE_NOTIFICATION; + + const _ = !0; + } +} + +pub(crate) struct TimerSubsystem(PhantomData); + +impl FdEnabledSubsystem for TimerSubsystem { + type Entry = TimerHandleObject; +} + +impl FdEnabledSubsystemEntry for TimerHandleObject {} + +pub(crate) struct TimerHandleObject { + _timer: Arc>, + granted_access: TimerAccess, +} + +impl TimerHandleObject { + pub(crate) fn require_access(&self, required: TimerAccess) -> Result<(), NtStatus> { + if self.granted_access.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +pub(crate) struct TimerObject { + _attributes: Timer2Attributes, + _not_send_without_platform: PhantomData, +} + +pub(crate) struct TimerCreateParameters { + pub(crate) timer_handle: MutPtr, + pub(crate) timer_id: Option>, + pub(crate) object_attributes: Option>, + pub(crate) attributes: u32, + pub(crate) desired_access: u32, +} + +fn validate_timer2_before_output( + params: &TimerCreateParameters, +) -> Result<(), NtStatus> { + if params.object_attributes.is_some() { + return Err(NtStatus::INVALID_PARAMETER_3); + } + if params.attributes & TIMER2_ATTRIBUTE_RESERVED_MASK != 0 { + return Err(NtStatus::INVALID_PARAMETER_4); + } + if params.attributes & TIMER2_ATTRIBUTE_IR_TIMER == 0 && params.timer_id.is_some() { + return Err(NtStatus::INVALID_PARAMETER_2); + } + Ok(()) +} + +fn validate_timer2_after_output( + params: &TimerCreateParameters, +) -> Result<(), NtStatus> { + if params.attributes & TIMER2_ATTRIBUTE_IR_TIMER == 0 { + return Ok(()); + } + if params.timer_id.is_some() { + Err(NtStatus::ACCESS_DENIED) + } else { + Err(NtStatus::INVALID_PARAMETER) + } +} + +impl Task { + fn timer_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + raw_handle_entry::>( + &self.global.litebox, + &self.process.handles, + handle, + ) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn insert_timer_handle( + &self, + timer: Arc>, + granted_access: TimerAccess, + ) -> Result { + let typed = self + .global + .litebox + .descriptor_table_mut() + .insert::>(TimerHandleObject { + _timer: timer, + granted_access, + }); + insert_raw_handle::>( + &self.global.litebox, + &self.process.handles, + typed, + drop, + ) + } + + pub(crate) fn close_timer_handle(&self, handle: Handle) { + remove_raw_handle::>( + &self.global.litebox, + &self.process.handles, + handle, + drop, + ); + } + + pub(crate) fn close_timer(timer: TimerHandleObject) { + drop(timer); + } + + pub(crate) fn sys_nt_create_timer2(&self, params: TimerCreateParameters) -> NtStatus { + if let Err(status) = validate_timer2_before_output(¶ms) { + return status; + } + if let Err(status) = probe_guest_output_preserving_value::(params.timer_handle) + { + return status; + } + if let Err(status) = validate_timer2_after_output(¶ms) { + return status; + } + + let timer = Arc::new(TimerObject { + // TODO: store timer state once NtSetTimer2 schedules due times and waiters can + // observe expiration/signaling instead of only validating the handle shape. + _attributes: Timer2Attributes::from_bits_retain(params.attributes), + _not_send_without_platform: PhantomData, + }); + let granted_access = TimerAccess::from_desired_access(params.desired_access); + let Ok(handle) = self.insert_timer_handle(timer, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if params.timer_handle.write_at_offset(0, handle).is_none() { + self.close_timer_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_set_timer2( + &self, + timer_handle: Handle, + due_time: Option>, + period: Option>, + parameters: Option>, + ) -> NtStatus { + let timer = match self.timer_entry(timer_handle) { + Ok(timer) => timer, + Err(status) => return status, + }; + if let Err(status) = + timer.with_entry(|timer| timer.require_access(TimerAccess::MODIFY_STATE)) + { + return status; + } + + let _due_time = match due_time { + Some(due_time) => match due_time.read_at_offset(0) { + Some(due_time) => Some(due_time), + None => return NtStatus::ACCESS_VIOLATION, + }, + None => None, + }; + let _period = match period { + Some(period) => match period.read_at_offset(0) { + Some(period) => Some(period), + None => return NtStatus::ACCESS_VIOLATION, + }, + None => None, + }; + + // TODO: parse T2_SET_PARAMETERS and model callbacks/tolerable delay when the timer + // object grows real scheduling and notification behavior. + let _ = parameters; + + // TODO: store due_time/period, transition the timer's signaled state, and notify + // waiters or associated wait-completion packets instead of returning a no-op success. + NtStatus::SUCCESS + } +} + +#[cfg(test)] +mod tests { + use core::mem::size_of; + + use litebox::platform::ThreadProvider; + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::ObjectAttributes; + use crate::tests::{TestPlatform, const_ptr, mut_ptr, null_mut_ptr, test_platform, test_task}; + + const TIMER_ALL_ACCESS: u32 = 0x001f_0003; + + fn object_attributes_size() -> u32 { + u32::try_from(size_of::()).expect("OBJECT_ATTRIBUTES fits in ULONG") + } + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = test_platform(); + ::run_test_thread(f) + } + + fn create_timer2( + task: &Task, + handle: &mut Handle, + timer_id: Option>, + object_attributes: Option>, + attributes: u32, + ) -> NtStatus { + task.sys_nt_create_timer2(TimerCreateParameters { + timer_handle: mut_ptr(handle), + timer_id, + object_attributes, + attributes, + desired_access: TIMER_ALL_ACCESS, + }) + } + + #[test] + fn set_timer2_accepts_created_timer() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let mut handle = Handle::default(); + let due_time = -10_000i64; + let period = 0i64; + + assert_eq!( + create_timer2(&task, &mut handle, None, None, 0), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_set_timer2( + handle, + Some(const_ptr(&due_time)), + Some(const_ptr(&period)), + None + ), + NtStatus::SUCCESS + ); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + }); + } + + #[test] + fn create_rejects_object_attributes_before_output_pointer() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let bad_length = ObjectAttributes { + length: 1, + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + + assert_eq!( + task.sys_nt_create_timer2(TimerCreateParameters { + timer_handle: null_mut_ptr(), + timer_id: None, + object_attributes: Some(const_ptr(&bad_length)), + attributes: 0, + desired_access: TIMER_ALL_ACCESS, + }), + NtStatus::INVALID_PARAMETER_3 + ); + }); + } + + #[test] + fn create_validates_reserved_bits_and_non_ir_timer_id_before_output_pointer() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let timer_id = 1u32; + + assert_eq!( + task.sys_nt_create_timer2(TimerCreateParameters { + timer_handle: null_mut_ptr(), + timer_id: None, + object_attributes: None, + attributes: 1, + desired_access: TIMER_ALL_ACCESS, + }), + NtStatus::INVALID_PARAMETER_4 + ); + assert_eq!( + task.sys_nt_create_timer2(TimerCreateParameters { + timer_handle: null_mut_ptr(), + timer_id: Some(const_ptr(&timer_id)), + object_attributes: None, + attributes: TIMER2_ATTRIBUTE_NOTIFICATION, + desired_access: TIMER_ALL_ACCESS, + }), + NtStatus::INVALID_PARAMETER_2 + ); + }); + } + + #[test] + fn create_probes_output_pointer_before_ir_timer_validation() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let timer_id = 1u32; + + assert_eq!( + task.sys_nt_create_timer2(TimerCreateParameters { + timer_handle: null_mut_ptr(), + timer_id: None, + object_attributes: None, + attributes: TIMER2_ATTRIBUTE_IR_TIMER, + desired_access: TIMER_ALL_ACCESS, + }), + NtStatus::ACCESS_VIOLATION + ); + assert_eq!( + task.sys_nt_create_timer2(TimerCreateParameters { + timer_handle: null_mut_ptr(), + timer_id: Some(const_ptr(&timer_id)), + object_attributes: None, + attributes: TIMER2_ATTRIBUTE_IR_TIMER, + desired_access: TIMER_ALL_ACCESS, + }), + NtStatus::ACCESS_VIOLATION + ); + }); + } + + #[test] + fn create_rejects_ir_timers_without_clobbering_output() { + let task = test_task(); + let timer_id = 1u32; + let mut handle = Handle::from_raw(usize::MAX); + + assert_eq!( + create_timer2(&task, &mut handle, None, None, TIMER2_ATTRIBUTE_IR_TIMER), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::from_raw(usize::MAX)); + assert_eq!( + create_timer2( + &task, + &mut handle, + Some(const_ptr(&timer_id)), + None, + TIMER2_ATTRIBUTE_IR_TIMER + ), + NtStatus::ACCESS_DENIED + ); + assert_eq!(handle, Handle::from_raw(usize::MAX)); + } + + #[test] + fn create_rejections_do_not_clobber_output() { + let task = test_task(); + let timer_id = 1u32; + let bad_length = ObjectAttributes { + length: 1, + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + let valid_length = ObjectAttributes { + length: object_attributes_size(), + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + + for (timer_id, object_attributes, attributes, expected_status) in [ + ( + None, + Some(const_ptr(&bad_length)), + 0, + NtStatus::INVALID_PARAMETER_3, + ), + ( + None, + Some(const_ptr(&valid_length)), + 0, + NtStatus::INVALID_PARAMETER_3, + ), + (None, None, 1, NtStatus::INVALID_PARAMETER_4), + ( + Some(const_ptr(&timer_id)), + None, + TIMER2_ATTRIBUTE_HIGH_RESOLUTION, + NtStatus::INVALID_PARAMETER_2, + ), + ] { + let mut handle = Handle::from_raw(usize::MAX); + assert_eq!( + create_timer2(&task, &mut handle, timer_id, object_attributes, attributes), + expected_status + ); + assert_eq!(handle, Handle::from_raw(usize::MAX)); + } + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn host_create_timer2_status_fidelity() { + use core::ffi::c_void; + + unsafe extern "system" { + fn NtCreateTimer2( + handle: *mut *mut c_void, + timer_id: *const u32, + object_attributes: *const ObjectAttributes, + attributes: u32, + desired_access: u32, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } + + let task = test_task(); + let timer_id = 1u32; + let bad_length = ObjectAttributes { + length: 1, + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + + for (timer_id, object_attributes, attributes) in [ + (None, None, 0), + (None, None, TIMER2_ATTRIBUTE_HIGH_RESOLUTION), + (None, None, TIMER2_ATTRIBUTE_NO_WAKE), + ( + None, + None, + TIMER2_ATTRIBUTE_HIGH_RESOLUTION | TIMER2_ATTRIBUTE_NO_WAKE, + ), + (None, None, TIMER2_ATTRIBUTE_NOTIFICATION), + ( + None, + None, + TIMER2_ATTRIBUTE_NOTIFICATION + | TIMER2_ATTRIBUTE_HIGH_RESOLUTION + | TIMER2_ATTRIBUTE_NO_WAKE, + ), + (None, None, 1), + (Some(&timer_id), None, TIMER2_ATTRIBUTE_HIGH_RESOLUTION), + (None, Some(&bad_length), 0), + (None, None, TIMER2_ATTRIBUTE_IR_TIMER), + (Some(&timer_id), None, TIMER2_ATTRIBUTE_IR_TIMER), + ] { + let mut host_handle = core::ptr::null_mut(); + // SAFETY: The output pointer is valid, optional input pointers reference local values + // for the duration of the call, and successful host handles are closed below. + let host_status = unsafe { + NtCreateTimer2( + &raw mut host_handle, + timer_id.map_or(core::ptr::null(), core::ptr::from_ref), + object_attributes.map_or(core::ptr::null(), core::ptr::from_ref), + attributes, + TIMER_ALL_ACCESS, + ) + }; + if host_status == NtStatus::SUCCESS.as_raw() && !host_handle.is_null() { + // SAFETY: The handle was returned by NtCreateTimer2 in this test. + assert_eq!(unsafe { NtClose(host_handle) }, NtStatus::SUCCESS.as_raw()); + } + + let mut shim_handle = Handle::default(); + let shim_status = create_timer2( + &task, + &mut shim_handle, + timer_id.map(const_ptr), + object_attributes.map(const_ptr), + attributes, + ); + assert_eq!(shim_status.as_raw(), host_status); + if shim_status == NtStatus::SUCCESS { + assert!(!shim_handle.is_null()); + assert_eq!(task.sys_nt_close(shim_handle), NtStatus::SUCCESS); + } + } + } +} diff --git a/litebox_shim_windows/src/syscalls/wait_completion_packet.rs b/litebox_shim_windows/src/syscalls/wait_completion_packet.rs new file mode 100644 index 0000000000..63b9d7b0ae --- /dev/null +++ b/litebox_shim_windows/src/syscalls/wait_completion_packet.rs @@ -0,0 +1,1468 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NT wait completion packet syscalls. + +use alloc::sync::Arc; +use core::marker::PhantomData; + +use litebox::fd::{ErrRawIntFd, FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawMutPointer as _, RawPointerProvider}; +use litebox::sync::Mutex; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::{AccessMask, ObjectAttributes, read_object_attributes}; +use crate::syscalls::Handle; +use crate::syscalls::event::{EventAccess, EventSubsystem}; +use crate::syscalls::iocp::{IoCompletionAccess, IoCompletionSubsystem}; +use crate::syscalls::timer::{TimerAccess, TimerSubsystem}; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, + remove_raw_handle, +}; + +const STANDARD_RIGHTS_REQUIRED: u32 = AccessMask::DELETE.bits() + | AccessMask::READ_CONTROL.bits() + | AccessMask::WRITE_DAC.bits() + | AccessMask::WRITE_OWNER.bits(); + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct WaitCompletionPacketAccess: u32 { + const SET_STATE = 0x0001; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() | Self::SET_STATE.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits() | Self::SET_STATE.bits(); + const EXECUTE = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() | Self::SET_STATE.bits(); + const ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | Self::SET_STATE.bits(); + + const _ = !0; + } +} + +impl WaitCompletionPacketAccess { + fn from_desired_access(desired_access: u32) -> Self { + let mut access = Self::from_bits_retain(desired_access); + if desired_access & AccessMask::GENERIC_READ.bits() != 0 { + access.insert(Self::READ); + } + if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { + access.insert(Self::WRITE); + } + if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { + access.insert(Self::EXECUTE); + } + if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { + access.insert(Self::ALL_ACCESS); + } + access.remove(Self::from_bits_retain( + AccessMask::GENERIC_READ.bits() + | AccessMask::GENERIC_WRITE.bits() + | AccessMask::GENERIC_EXECUTE.bits() + | AccessMask::GENERIC_ALL.bits(), + )); + access + } + + fn require(self, required: Self) -> Result<(), NtStatus> { + if self.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +pub(crate) struct WaitCompletionPacketSubsystem(PhantomData); + +impl FdEnabledSubsystem for WaitCompletionPacketSubsystem { + type Entry = WaitCompletionPacketHandleObject; +} + +impl FdEnabledSubsystemEntry + for WaitCompletionPacketHandleObject +{ +} + +pub(crate) struct WaitCompletionPacketHandleObject { + packet: Arc>, + granted_access: WaitCompletionPacketAccess, +} + +pub(crate) struct WaitCompletionPacketObject { + association: Mutex>, + _not_send_without_platform: PhantomData, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct WaitCompletionPacketAssociation { + _key_context: usize, + _apc_context: usize, + _io_status: i32, + _io_status_information: usize, + already_signaled: bool, +} + +pub(crate) struct WaitCompletionPacketAssociateParameters { + pub(crate) wait_completion_packet_handle: Handle, + pub(crate) io_completion_handle: Handle, + pub(crate) target_object_handle: Handle, + pub(crate) key_context: usize, + pub(crate) apc_context: usize, + pub(crate) io_status: i32, + pub(crate) io_status_information: usize, + pub(crate) already_signaled: Option>, +} + +fn validate_wait_completion_packet_object_attributes( + object_attributes: Option>, +) -> Result<(), NtStatus> { + let Some(object_attributes) = object_attributes else { + return Ok(()); + }; + let object_attributes = read_object_attributes::(object_attributes)?; + if object_attributes.object_name == 0 && !object_attributes.root_directory.is_null() { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + Ok(()) +} + +impl Task { + fn wait_completion_packet_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> + { + let Some(raw_fd) = handle.raw_fd() else { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + }; + let typed = { + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::>(raw_fd) { + Ok(typed) => typed, + Err(ErrRawIntFd::NotFound | ErrRawIntFd::InvalidSubsystem) => { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + } + } + }; + self.global + .litebox + .descriptor_table() + .entry_handle(&typed) + .ok_or(NtStatus::OBJECT_TYPE_MISMATCH) + } + + fn wait_completion_packet_entry_for_cancel( + &self, + handle: Handle, + ) -> Result>, NtStatus> + { + let Some(raw_fd) = handle.raw_fd() else { + return Err(NtStatus::INVALID_HANDLE); + }; + let typed = { + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::>(raw_fd) { + Ok(typed) => typed, + Err(ErrRawIntFd::NotFound) => return Err(NtStatus::INVALID_HANDLE), + Err(ErrRawIntFd::InvalidSubsystem) => return Err(NtStatus::OBJECT_TYPE_MISMATCH), + } + }; + self.global + .litebox + .descriptor_table() + .entry_handle(&typed) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn validate_io_completion_for_wait_completion_packet( + &self, + handle: Handle, + ) -> Result<(), NtStatus> { + let Some(raw_fd) = handle.raw_fd() else { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + }; + let typed = { + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::>(raw_fd) { + Ok(typed) => typed, + Err(ErrRawIntFd::NotFound | ErrRawIntFd::InvalidSubsystem) => { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + } + } + }; + let Some(entry) = self.global.litebox.descriptor_table().entry_handle(&typed) else { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + }; + entry.with_entry(|entry| entry.require_access(IoCompletionAccess::MODIFY_STATE)) + } + + fn target_object_signaled_for_wait_completion_packet( + &self, + handle: Handle, + ) -> Result { + // TODO: support every waitable target object type that Windows accepts here; the + // current subset only recognizes event and timer handles. + let Some(raw_fd) = handle.raw_fd() else { + return Err(NtStatus::ACCESS_DENIED); + }; + if let Some(signaled) = self.event_signaled_for_wait_completion_packet(raw_fd)? { + return Ok(signaled); + } + if let Some(signaled) = self.timer_signaled_for_wait_completion_packet(raw_fd)? { + return Ok(signaled); + } + Err(NtStatus::INVALID_PARAMETER_3) + } + + fn event_signaled_for_wait_completion_packet( + &self, + raw_fd: usize, + ) -> Result, NtStatus> { + let typed = { + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::>(raw_fd) { + Ok(typed) => typed, + Err(ErrRawIntFd::NotFound) => return Err(NtStatus::ACCESS_DENIED), + Err(ErrRawIntFd::InvalidSubsystem) => return Ok(None), + } + }; + let Some(entry) = self.global.litebox.descriptor_table().entry_handle(&typed) else { + return Err(NtStatus::ACCESS_DENIED); + }; + entry + .with_entry(|entry| { + entry + .require_access(EventAccess::from_bits_retain( + AccessMask::SYNCHRONIZE.bits(), + )) + .map(|()| entry.is_signaled()) + }) + .map(Some) + } + + fn timer_signaled_for_wait_completion_packet( + &self, + raw_fd: usize, + ) -> Result, NtStatus> { + let typed = { + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::>(raw_fd) { + Ok(typed) => typed, + Err(ErrRawIntFd::NotFound) => return Err(NtStatus::ACCESS_DENIED), + Err(ErrRawIntFd::InvalidSubsystem) => return Ok(None), + } + }; + let Some(entry) = self.global.litebox.descriptor_table().entry_handle(&typed) else { + return Err(NtStatus::ACCESS_DENIED); + }; + // TODO: return the timer object's real signaled state after NtSetTimer2 models + // due-time expiration and periodic re-signaling. + entry + .with_entry(|entry| { + entry + .require_access(TimerAccess::from_bits_retain( + AccessMask::SYNCHRONIZE.bits(), + )) + .map(|()| false) + }) + .map(Some) + } + + fn insert_wait_completion_packet_handle( + &self, + packet: Arc>, + granted_access: WaitCompletionPacketAccess, + ) -> Result { + let typed = self + .global + .litebox + .descriptor_table_mut() + .insert::>(WaitCompletionPacketHandleObject { + packet, + granted_access, + }); + insert_raw_handle::>( + &self.global.litebox, + &self.process.handles, + typed, + drop, + ) + } + + pub(crate) fn close_wait_completion_packet_handle(&self, handle: Handle) { + remove_raw_handle::>( + &self.global.litebox, + &self.process.handles, + handle, + drop, + ); + } + + pub(crate) fn close_wait_completion_packet( + wait_completion_packet: WaitCompletionPacketHandleObject, + ) { + drop(wait_completion_packet); + } + + pub(crate) fn sys_nt_create_wait_completion_packet( + &self, + wait_completion_packet_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + if let Err(status) = + probe_guest_output_preserving_value::(wait_completion_packet_handle) + { + return status; + } + if let Err(status) = + validate_wait_completion_packet_object_attributes::(object_attributes) + { + return status; + } + + let packet = Arc::new(WaitCompletionPacketObject { + association: Mutex::new(None), + _not_send_without_platform: PhantomData, + }); + let granted_access = WaitCompletionPacketAccess::from_desired_access(desired_access); + let Ok(handle) = self.insert_wait_completion_packet_handle(packet, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if wait_completion_packet_handle + .write_at_offset(0, handle) + .is_none() + { + self.close_wait_completion_packet_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_associate_wait_completion_packet( + &self, + params: WaitCompletionPacketAssociateParameters, + ) -> NtStatus { + let entry = match self.wait_completion_packet_entry(params.wait_completion_packet_handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + let packet = match entry.with_entry(|entry| { + entry + .granted_access + .require(WaitCompletionPacketAccess::SET_STATE) + .map(|()| entry.packet.clone()) + }) { + Ok(packet) => packet, + Err(status) => return status, + }; + + if let Err(status) = + self.validate_io_completion_for_wait_completion_packet(params.io_completion_handle) + { + return status; + } + { + let association = packet.association.lock(); + if association.is_some() { + return NtStatus::INVALID_PARAMETER_1; + } + } + let already_signaled = match self + .target_object_signaled_for_wait_completion_packet(params.target_object_handle) + { + Ok(already_signaled) => already_signaled, + Err(status) => return status, + }; + if let Some(already_signaled_ptr) = params.already_signaled + && already_signaled_ptr + .write_at_offset(0, u8::from(already_signaled)) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + // TODO: link the packet into the target object's wait notification path and post to + // the associated IOCP when the target is already signaled or becomes signaled later. + let mut association = packet.association.lock(); + if association.is_some() { + return NtStatus::INVALID_PARAMETER_1; + } + *association = Some(WaitCompletionPacketAssociation { + _key_context: params.key_context, + _apc_context: params.apc_context, + _io_status: params.io_status, + _io_status_information: params.io_status_information, + already_signaled, + }); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_cancel_wait_completion_packet( + &self, + wait_completion_packet_handle: Handle, + remove_signaled_packet: u8, + ) -> NtStatus { + let entry = + match self.wait_completion_packet_entry_for_cancel(wait_completion_packet_handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + let packet = match entry.with_entry(|entry| { + entry + .granted_access + .require(WaitCompletionPacketAccess::SET_STATE) + .map(|()| Arc::clone(&entry.packet)) + }) { + Ok(packet) => packet, + Err(status) => return status, + }; + + let mut association = packet.association.lock(); + let Some(current_association) = *association else { + return NtStatus::CANCELLED; + }; + if current_association.already_signaled && remove_signaled_packet == 0 { + return NtStatus::PENDING; + } + + // TODO: if a signaled packet has been posted to the IOCP, honor + // remove_signaled_packet by removing that queued completion packet. + *association = None; + NtStatus::SUCCESS + } +} + +#[cfg(test)] +mod tests { + use core::mem::size_of; + + use litebox::platform::{RawConstPointer as _, ThreadProvider}; + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::ObjectAttributes; + use crate::tests::{TestPlatform, const_ptr, mut_ptr, test_platform, test_task}; + + const WAIT_COMPLETION_PACKET_SET_STATE: u32 = 0x0000_0001; + const WAIT_COMPLETION_PACKET_ALL_ACCESS: u32 = 0x000f_0001; + const IO_COMPLETION_QUERY_STATE: u32 = 0x0000_0001; + const IO_COMPLETION_ALL_ACCESS: u32 = 0x001f_0003; + const EVENT_QUERY_STATE: u32 = 0x0000_0001; + const EVENT_ALL_ACCESS: u32 = 0x001f_0003; + const SYNCHRONIZE: u32 = 0x0010_0000; + + fn object_attributes_size() -> u32 { + u32::try_from(size_of::()).expect("OBJECT_ATTRIBUTES fits in ULONG") + } + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = test_platform(); + ::run_test_thread(f) + } + + fn create_wait_completion_packet( + task: &Task, + handle: &mut Handle, + object_attributes: Option>, + ) -> NtStatus { + task.sys_nt_create_wait_completion_packet( + mut_ptr(handle), + WAIT_COMPLETION_PACKET_ALL_ACCESS, + object_attributes, + ) + } + + fn create_wait_completion_packet_with_access( + task: &Task, + handle: &mut Handle, + desired_access: u32, + ) -> NtStatus { + task.sys_nt_create_wait_completion_packet(mut_ptr(handle), desired_access, None) + } + + fn create_io_completion( + task: &Task, + handle: &mut Handle, + desired_access: u32, + ) -> NtStatus { + task.sys_nt_create_io_completion(mut_ptr(handle), desired_access, None, 0) + } + + fn create_event( + task: &Task, + handle: &mut Handle, + desired_access: u32, + initial_state: bool, + ) -> NtStatus { + task.sys_nt_create_event( + mut_ptr(handle), + desired_access, + None, + 0, + u8::from(initial_state), + ) + } + + fn create_timer( + task: &Task, + handle: &mut Handle, + desired_access: u32, + ) -> NtStatus { + task.sys_nt_create_timer2(crate::syscalls::timer::TimerCreateParameters { + timer_handle: mut_ptr(handle), + timer_id: None, + object_attributes: None, + attributes: 0, + desired_access, + }) + } + + fn associate_wait_completion_packet( + task: &Task, + packet: Handle, + io_completion: Handle, + target: Handle, + already_signaled: Option>, + ) -> NtStatus { + task.sys_nt_associate_wait_completion_packet(WaitCompletionPacketAssociateParameters { + wait_completion_packet_handle: packet, + io_completion_handle: io_completion, + target_object_handle: target, + key_context: 0x1111, + apc_context: 0x2222, + io_status: NtStatus::SUCCESS.as_raw(), + io_status_information: 0x3333, + already_signaled, + }) + } + + fn cancel_wait_completion_packet( + task: &Task, + packet: Handle, + remove_signaled_packet: bool, + ) -> NtStatus { + task.sys_nt_cancel_wait_completion_packet(packet, u8::from(remove_signaled_packet)) + } + + #[test] + fn create_validates_object_attributes_without_clobbering_output() { + let task = test_task(); + let mut handle = Handle::from_raw(usize::MAX); + let bad_length = ObjectAttributes { + length: 1, + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + + assert_eq!( + create_wait_completion_packet(&task, &mut handle, Some(const_ptr(&bad_length))), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::from_raw(usize::MAX)); + + let root_without_name = ObjectAttributes { + length: object_attributes_size(), + root_directory: Handle::from_raw(4), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + assert_eq!( + create_wait_completion_packet(&task, &mut handle, Some(const_ptr(&root_without_name))), + NtStatus::OBJECT_NAME_INVALID + ); + assert_eq!(handle, Handle::from_raw(usize::MAX)); + } + + #[test] + fn associate_writes_signal_state_and_marks_packet_busy() { + let task = test_task(); + let mut packet = Handle::default(); + let mut io_completion = Handle::default(); + let mut event = Handle::default(); + let mut already_signaled = 0xaa; + + assert_eq!( + create_wait_completion_packet(&task, &mut packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut event, EVENT_ALL_ACCESS, false), + NtStatus::SUCCESS + ); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::SUCCESS + ); + assert_eq!(already_signaled, 0); + + already_signaled = 0xaa; + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::INVALID_PARAMETER_1 + ); + assert_eq!(already_signaled, 0xaa); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + Handle::from_raw(0x1234), + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::INVALID_PARAMETER_1 + ); + assert_eq!(already_signaled, 0xaa); + } + + #[test] + fn associate_reports_already_signaled_target() { + let task = test_task(); + let mut packet = Handle::default(); + let mut io_completion = Handle::default(); + let mut event = Handle::default(); + let mut already_signaled = 0xaa; + + assert_eq!( + create_wait_completion_packet(&task, &mut packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut event, EVENT_ALL_ACCESS, true), + NtStatus::SUCCESS + ); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::SUCCESS + ); + assert_eq!(already_signaled, 1); + } + + #[test] + fn associate_accepts_timer_target_as_unsignaled() { + let task = test_task(); + let mut packet = Handle::default(); + let mut io_completion = Handle::default(); + let mut timer = Handle::default(); + let mut already_signaled = 0xaa; + + assert_eq!( + create_wait_completion_packet(&task, &mut packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_timer(&task, &mut timer, SYNCHRONIZE), + NtStatus::SUCCESS + ); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + timer, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::SUCCESS + ); + assert_eq!(already_signaled, 0); + } + + #[test] + fn associate_invalid_already_signaled_does_not_commit_association() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let mut packet = Handle::default(); + let mut io_completion = Handle::default(); + let mut event = Handle::default(); + let mut already_signaled = 0xaa; + + assert_eq!( + create_wait_completion_packet(&task, &mut packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut event, EVENT_ALL_ACCESS, true), + NtStatus::SUCCESS + ); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(MutPtr::::from_usize(1)), + ), + NtStatus::ACCESS_VIOLATION + ); + assert_eq!( + cancel_wait_completion_packet(&task, packet, true), + NtStatus::CANCELLED + ); + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::SUCCESS + ); + assert_eq!(already_signaled, 1); + }); + } + + #[test] + fn associate_enforces_native_observed_access_masks() { + let task = test_task(); + let mut packet = Handle::default(); + let mut packet_without_set_state = Handle::default(); + let mut io_completion = Handle::default(); + let mut io_completion_query_only = Handle::default(); + let mut event = Handle::default(); + let mut event_without_synchronize = Handle::default(); + let mut already_signaled = 0xaa; + + assert_eq!( + create_wait_completion_packet_with_access( + &task, + &mut packet, + WAIT_COMPLETION_PACKET_SET_STATE, + ), + NtStatus::SUCCESS + ); + assert_eq!( + create_wait_completion_packet_with_access(&task, &mut packet_without_set_state, 0), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion( + &task, + &mut io_completion_query_only, + IO_COMPLETION_QUERY_STATE, + ), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut event, SYNCHRONIZE, false), + NtStatus::SUCCESS + ); + assert_eq!( + create_event( + &task, + &mut event_without_synchronize, + EVENT_QUERY_STATE, + false, + ), + NtStatus::SUCCESS + ); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet_without_set_state, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::ACCESS_DENIED + ); + assert_eq!(already_signaled, 0xaa); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion_query_only, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::ACCESS_DENIED + ); + assert_eq!(already_signaled, 0xaa); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event_without_synchronize, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::ACCESS_DENIED + ); + assert_eq!(already_signaled, 0xaa); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::SUCCESS + ); + assert_eq!(already_signaled, 0); + } + + #[test] + fn associate_distinguishes_handle_errors_like_native_windows() { + let task = test_task(); + let mut packet = Handle::default(); + let mut io_completion = Handle::default(); + let mut target_io_completion = Handle::default(); + let mut event = Handle::default(); + let mut already_signaled = 0xaa; + + assert_eq!( + create_wait_completion_packet(&task, &mut packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut target_io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut event, EVENT_ALL_ACCESS, false), + NtStatus::SUCCESS + ); + + assert_eq!( + associate_wait_completion_packet( + &task, + Handle::from_raw(0x1234), + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::OBJECT_TYPE_MISMATCH + ); + assert_eq!(already_signaled, 0xaa); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + Handle::from_raw(0x1234), + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::OBJECT_TYPE_MISMATCH + ); + assert_eq!(already_signaled, 0xaa); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + Handle::from_raw(0x1234), + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::ACCESS_DENIED + ); + assert_eq!(already_signaled, 0xaa); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + target_io_completion, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::INVALID_PARAMETER_3 + ); + assert_eq!(already_signaled, 0xaa); + } + + #[test] + fn cancel_clears_unsignaled_association_and_allows_reuse() { + let task = test_task(); + let mut packet = Handle::default(); + let mut io_completion = Handle::default(); + let mut event = Handle::default(); + let mut already_signaled = 0xaa; + + assert_eq!( + create_wait_completion_packet(&task, &mut packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut event, EVENT_ALL_ACCESS, false), + NtStatus::SUCCESS + ); + + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::SUCCESS + ); + assert_eq!( + cancel_wait_completion_packet(&task, packet, false), + NtStatus::SUCCESS + ); + assert_eq!( + cancel_wait_completion_packet(&task, packet, false), + NtStatus::CANCELLED + ); + + already_signaled = 0xaa; + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::SUCCESS + ); + assert_eq!(already_signaled, 0); + } + + #[test] + fn cancel_signaled_packet_obeys_remove_signaled_packet() { + let task = test_task(); + let mut packet = Handle::default(); + let mut io_completion = Handle::default(); + let mut event = Handle::default(); + let mut already_signaled = 0xaa; + + assert_eq!( + create_wait_completion_packet(&task, &mut packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_io_completion(&task, &mut io_completion, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut event, EVENT_ALL_ACCESS, true), + NtStatus::SUCCESS + ); + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::SUCCESS + ); + assert_eq!(already_signaled, 1); + + assert_eq!( + cancel_wait_completion_packet(&task, packet, false), + NtStatus::PENDING + ); + already_signaled = 0xaa; + assert_eq!( + associate_wait_completion_packet( + &task, + packet, + io_completion, + event, + Some(mut_ptr(&mut already_signaled)), + ), + NtStatus::INVALID_PARAMETER_1 + ); + assert_eq!(already_signaled, 0xaa); + + assert_eq!( + cancel_wait_completion_packet(&task, packet, true), + NtStatus::SUCCESS + ); + assert_eq!( + cancel_wait_completion_packet(&task, packet, true), + NtStatus::CANCELLED + ); + } + + #[test] + fn cancel_distinguishes_handle_errors_and_requires_set_state() { + let task = test_task(); + let mut event = Handle::default(); + let mut packet_without_set_state = Handle::default(); + + assert_eq!( + create_event(&task, &mut event, EVENT_ALL_ACCESS, false), + NtStatus::SUCCESS + ); + assert_eq!( + create_wait_completion_packet_with_access(&task, &mut packet_without_set_state, 0), + NtStatus::SUCCESS + ); + + assert_eq!( + cancel_wait_completion_packet(&task, Handle::default(), false), + NtStatus::INVALID_HANDLE + ); + assert_eq!( + cancel_wait_completion_packet(&task, Handle::from_raw(0x1234), true), + NtStatus::INVALID_HANDLE + ); + assert_eq!( + cancel_wait_completion_packet(&task, event, false), + NtStatus::OBJECT_TYPE_MISMATCH + ); + assert_eq!( + cancel_wait_completion_packet(&task, packet_without_set_state, false), + NtStatus::ACCESS_DENIED + ); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn host_cancel_wait_completion_packet_status_fidelity() { + use core::ffi::c_void; + + unsafe extern "system" { + fn NtCreateWaitCompletionPacket( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + ) -> i32; + fn NtCancelWaitCompletionPacket(handle: *mut c_void, remove_signaled_packet: u8) + -> i32; + fn NtAssociateWaitCompletionPacket( + packet: *mut c_void, + io_completion: *mut c_void, + target: *mut c_void, + key_context: *mut c_void, + apc_context: *mut c_void, + io_status: i32, + io_status_information: usize, + already_signaled: *mut u8, + ) -> i32; + fn NtCreateIoCompletion( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + number_of_concurrent_threads: u32, + ) -> i32; + fn NtCreateEvent( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + event_type: u32, + initial_state: u8, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } + + unsafe fn close_host(handle: *mut c_void) { + if !handle.is_null() { + // SAFETY: The caller passes a live host handle returned by an NtCreate* call. + assert_eq!(unsafe { NtClose(handle) }, NtStatus::SUCCESS.as_raw()); + } + } + + let task = test_task(); + + // SAFETY: The null handle is an input-only value and no memory is dereferenced. + let host_null = unsafe { NtCancelWaitCompletionPacket(core::ptr::null_mut(), 0) }; + assert_eq!( + cancel_wait_completion_packet(&task, Handle::default(), false).as_raw(), + host_null + ); + + let mut host_event = core::ptr::null_mut(); + // SAFETY: The output pointer is valid and the returned handle is closed below. + assert_eq!( + unsafe { + NtCreateEvent( + &raw mut host_event, + EVENT_ALL_ACCESS, + core::ptr::null(), + 0, + 0, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + let mut shim_event = Handle::default(); + assert_eq!( + create_event(&task, &mut shim_event, EVENT_ALL_ACCESS, false), + NtStatus::SUCCESS + ); + // SAFETY: The host event handle is valid for the duration of this call. + let host_wrong_type = unsafe { NtCancelWaitCompletionPacket(host_event, 0) }; + assert_eq!( + cancel_wait_completion_packet(&task, shim_event, false).as_raw(), + host_wrong_type + ); + // SAFETY: The event handle was returned by NtCreateEvent in this test. + unsafe { close_host(host_event) }; + + let mut host_packet = core::ptr::null_mut(); + // SAFETY: The output pointer is valid and the returned handle is closed below. + assert_eq!( + unsafe { + NtCreateWaitCompletionPacket( + &raw mut host_packet, + WAIT_COMPLETION_PACKET_ALL_ACCESS, + core::ptr::null(), + ) + }, + NtStatus::SUCCESS.as_raw() + ); + let mut shim_packet = Handle::default(); + assert_eq!( + create_wait_completion_packet(&task, &mut shim_packet, None), + NtStatus::SUCCESS + ); + // SAFETY: The host packet handle is valid for the duration of this call. + let host_unassociated = unsafe { NtCancelWaitCompletionPacket(host_packet, 0) }; + assert_eq!( + cancel_wait_completion_packet(&task, shim_packet, false).as_raw(), + host_unassociated + ); + // SAFETY: The packet handle was returned by NtCreateWaitCompletionPacket in this test. + unsafe { close_host(host_packet) }; + + let mut host_packet_no_set = core::ptr::null_mut(); + // SAFETY: The output pointer is valid and the returned handle is closed below. + assert_eq!( + unsafe { + NtCreateWaitCompletionPacket(&raw mut host_packet_no_set, 0, core::ptr::null()) + }, + NtStatus::SUCCESS.as_raw() + ); + let mut shim_packet_no_set = Handle::default(); + assert_eq!( + create_wait_completion_packet_with_access(&task, &mut shim_packet_no_set, 0), + NtStatus::SUCCESS + ); + // SAFETY: The host packet handle is valid for the duration of this call. + let host_no_set = unsafe { NtCancelWaitCompletionPacket(host_packet_no_set, 0) }; + assert_eq!( + cancel_wait_completion_packet(&task, shim_packet_no_set, false).as_raw(), + host_no_set + ); + // SAFETY: The packet handle was returned by NtCreateWaitCompletionPacket in this test. + unsafe { close_host(host_packet_no_set) }; + + let mut host_iocp = core::ptr::null_mut(); + let mut host_assoc_packet = core::ptr::null_mut(); + let mut host_target = core::ptr::null_mut(); + // SAFETY: Output pointers are valid and successful handles are closed below. + assert_eq!( + unsafe { + NtCreateIoCompletion( + &raw mut host_iocp, + IO_COMPLETION_ALL_ACCESS, + core::ptr::null(), + 0, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + // SAFETY: Output pointer is valid and the returned handle is closed below. + assert_eq!( + unsafe { + NtCreateWaitCompletionPacket( + &raw mut host_assoc_packet, + WAIT_COMPLETION_PACKET_ALL_ACCESS, + core::ptr::null(), + ) + }, + NtStatus::SUCCESS.as_raw() + ); + // SAFETY: Output pointer is valid and the returned handle is closed below. + assert_eq!( + unsafe { + NtCreateEvent( + &raw mut host_target, + EVENT_ALL_ACCESS, + core::ptr::null(), + 0, + 0, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + let mut host_already_signaled = 0xaa; + // SAFETY: All handles are valid and the output byte points to local storage. + assert_eq!( + unsafe { + NtAssociateWaitCompletionPacket( + host_assoc_packet, + host_iocp, + host_target, + core::ptr::null_mut(), + core::ptr::null_mut(), + 0, + 0, + &raw mut host_already_signaled, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + let mut shim_iocp = Handle::default(); + let mut shim_assoc_packet = Handle::default(); + let mut shim_target = Handle::default(); + let mut shim_already_signaled = 0xaa; + assert_eq!( + create_io_completion(&task, &mut shim_iocp, IO_COMPLETION_ALL_ACCESS), + NtStatus::SUCCESS + ); + assert_eq!( + create_wait_completion_packet(&task, &mut shim_assoc_packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut shim_target, EVENT_ALL_ACCESS, false), + NtStatus::SUCCESS + ); + assert_eq!( + associate_wait_completion_packet( + &task, + shim_assoc_packet, + shim_iocp, + shim_target, + Some(mut_ptr(&mut shim_already_signaled)), + ), + NtStatus::SUCCESS + ); + // SAFETY: The host packet handle is valid for the duration of this call. + let host_cancel_unsignaled = unsafe { NtCancelWaitCompletionPacket(host_assoc_packet, 0) }; + assert_eq!( + cancel_wait_completion_packet(&task, shim_assoc_packet, false).as_raw(), + host_cancel_unsignaled + ); + // SAFETY: Handles were returned by NtCreate* calls in this test. + unsafe { + close_host(host_target); + close_host(host_assoc_packet); + close_host(host_iocp); + } + + let mut host_iocp = core::ptr::null_mut(); + let mut host_signaled_packet = core::ptr::null_mut(); + let mut host_signaled_event = core::ptr::null_mut(); + // SAFETY: Output pointers are valid and successful handles are closed below. + assert_eq!( + unsafe { + NtCreateIoCompletion( + &raw mut host_iocp, + IO_COMPLETION_ALL_ACCESS, + core::ptr::null(), + 0, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + // SAFETY: Output pointer is valid and the returned handle is closed below. + assert_eq!( + unsafe { + NtCreateWaitCompletionPacket( + &raw mut host_signaled_packet, + WAIT_COMPLETION_PACKET_ALL_ACCESS, + core::ptr::null(), + ) + }, + NtStatus::SUCCESS.as_raw() + ); + // SAFETY: Output pointer is valid and the returned handle is closed below. + assert_eq!( + unsafe { + NtCreateEvent( + &raw mut host_signaled_event, + EVENT_ALL_ACCESS, + core::ptr::null(), + 0, + 1, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + host_already_signaled = 0xaa; + // SAFETY: All handles are valid and the output byte points to local storage. + assert_eq!( + unsafe { + NtAssociateWaitCompletionPacket( + host_signaled_packet, + host_iocp, + host_signaled_event, + core::ptr::null_mut(), + core::ptr::null_mut(), + 0, + 0, + &raw mut host_already_signaled, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + let mut shim_signaled_packet = Handle::default(); + let mut shim_signaled_event = Handle::default(); + shim_already_signaled = 0xaa; + assert_eq!( + create_wait_completion_packet(&task, &mut shim_signaled_packet, None), + NtStatus::SUCCESS + ); + assert_eq!( + create_event(&task, &mut shim_signaled_event, EVENT_ALL_ACCESS, true), + NtStatus::SUCCESS + ); + assert_eq!( + associate_wait_completion_packet( + &task, + shim_signaled_packet, + shim_iocp, + shim_signaled_event, + Some(mut_ptr(&mut shim_already_signaled)), + ), + NtStatus::SUCCESS + ); + // SAFETY: The host packet handle is valid for the duration of these calls. + let host_cancel_signaled_pending = + unsafe { NtCancelWaitCompletionPacket(host_signaled_packet, 0) }; + assert_eq!( + cancel_wait_completion_packet(&task, shim_signaled_packet, false).as_raw(), + host_cancel_signaled_pending + ); + // SAFETY: The host packet handle is valid for the duration of this call. + let host_cancel_signaled_remove = + unsafe { NtCancelWaitCompletionPacket(host_signaled_packet, 1) }; + assert_eq!( + cancel_wait_completion_packet(&task, shim_signaled_packet, true).as_raw(), + host_cancel_signaled_remove + ); + // SAFETY: Handles were returned by NtCreate* calls in this test. + unsafe { + close_host(host_signaled_event); + close_host(host_signaled_packet); + close_host(host_iocp); + } + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn host_create_wait_completion_packet_status_fidelity() { + use core::ffi::c_void; + + unsafe extern "system" { + fn NtCreateWaitCompletionPacket( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } + + let task = test_task(); + + let bad_length = ObjectAttributes { + length: 1, + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + let root_without_name = ObjectAttributes { + length: object_attributes_size(), + root_directory: Handle::from_raw(4), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + + for object_attributes in [None, Some(&bad_length), Some(&root_without_name)] { + let mut host_handle = core::ptr::null_mut(); + // SAFETY: The output pointer is valid, optional attributes reference local values for + // the duration of the call, and successful host handles are closed below. + let host_status = unsafe { + NtCreateWaitCompletionPacket( + &raw mut host_handle, + WAIT_COMPLETION_PACKET_ALL_ACCESS, + object_attributes.map_or(core::ptr::null(), core::ptr::from_ref), + ) + }; + if host_status == NtStatus::SUCCESS.as_raw() && !host_handle.is_null() { + // SAFETY: The handle was returned by NtCreateWaitCompletionPacket in this test. + assert_eq!(unsafe { NtClose(host_handle) }, NtStatus::SUCCESS.as_raw()); + } + + let mut shim_handle = Handle::from_raw(usize::MAX); + let shim_status = create_wait_completion_packet( + &task, + &mut shim_handle, + object_attributes.map(const_ptr), + ); + assert_eq!(shim_status.as_raw(), host_status); + if shim_status == NtStatus::SUCCESS { + assert!(!shim_handle.is_null()); + assert_eq!(task.sys_nt_close(shim_handle), NtStatus::SUCCESS); + } else { + assert_eq!(shim_handle, Handle::from_raw(usize::MAX)); + } + } + } +} diff --git a/litebox_shim_windows/src/syscalls/worker_factory.rs b/litebox_shim_windows/src/syscalls/worker_factory.rs new file mode 100644 index 0000000000..22f62f0a51 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/worker_factory.rs @@ -0,0 +1,1130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NT worker factory syscalls. + +use alloc::sync::Arc; +use core::marker::PhantomData; +use core::mem::size_of; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +use litebox::fd::{ErrRawIntFd, FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::{AccessMask, ObjectAttributes, read_object_attributes}; +use crate::syscalls::iocp::{IoCompletionAccess, IoCompletionObject, IoCompletionSubsystem}; +use crate::syscalls::{Handle, ProcessHandle}; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, + remove_raw_handle, +}; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct WorkerFactoryAccess: u32 { + const RELEASE_WORKER = 0x0001; + const WAIT = 0x0002; + const SET_INFORMATION = 0x0004; + const QUERY_INFORMATION = 0x0008; + const READY_WORKER = 0x0010; + const SHUTDOWN = 0x0020; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() | Self::QUERY_INFORMATION.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits() | Self::SET_INFORMATION.bits(); + const EXECUTE = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() + | AccessMask::SYNCHRONIZE.bits() + | Self::WAIT.bits(); + const ALL_ACCESS = AccessMask::STANDARD_RIGHTS_ALL.bits() + | Self::RELEASE_WORKER.bits() + | Self::WAIT.bits() + | Self::SET_INFORMATION.bits() + | Self::QUERY_INFORMATION.bits() + | Self::READY_WORKER.bits() + | Self::SHUTDOWN.bits(); + + const _ = !0; + } +} + +impl WorkerFactoryAccess { + fn from_desired_access(desired_access: u32) -> Self { + let mut access = Self::from_bits_retain(desired_access); + if desired_access & AccessMask::GENERIC_READ.bits() != 0 { + access.insert(Self::READ); + } + if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { + access.insert(Self::WRITE); + } + if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { + access.insert(Self::EXECUTE); + } + if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { + access.insert(Self::ALL_ACCESS); + } + access.remove(Self::from_bits_retain( + AccessMask::GENERIC_READ.bits() + | AccessMask::GENERIC_WRITE.bits() + | AccessMask::GENERIC_EXECUTE.bits() + | AccessMask::GENERIC_ALL.bits(), + )); + access + } + + fn require(self, required: Self) -> Result<(), NtStatus> { + if self.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum WorkerFactoryInformationClass { + BindingCount = 3, + ThreadMinimum = 4, + ThreadMaximum = 5, + ThreadSoftMaximum = 14, +} + +impl WorkerFactoryInformationClass { + fn from_raw(raw: u32) -> Result { + match raw { + 3 => Ok(Self::BindingCount), + 4 => Ok(Self::ThreadMinimum), + 5 => Ok(Self::ThreadMaximum), + 14 => Ok(Self::ThreadSoftMaximum), + _ => Err(NtStatus::INVALID_INFO_CLASS), + } + } +} + +pub(crate) struct WorkerFactorySubsystem(PhantomData); + +impl FdEnabledSubsystem for WorkerFactorySubsystem { + type Entry = WorkerFactoryHandleObject; +} + +impl FdEnabledSubsystemEntry + for WorkerFactoryHandleObject +{ +} + +pub(crate) struct WorkerFactoryHandleObject { + factory: Arc>, + granted_access: WorkerFactoryAccess, +} + +pub(crate) struct WorkerFactoryObject { + _completion_port: Arc>, + _start_routine: usize, + _start_parameter: usize, + binding_count: AtomicU32, + thread_minimum: AtomicU32, + thread_maximum: AtomicU32, + thread_soft_maximum: AtomicU32, + shutdown: AtomicBool, + _stack_reserve: usize, + _stack_commit: usize, +} + +pub(crate) struct WorkerFactoryCreateParameters { + pub(crate) worker_factory_handle: MutPtr, + pub(crate) desired_access: u32, + pub(crate) object_attributes: Option>, + pub(crate) completion_port_handle: Handle, + pub(crate) worker_process_handle: ProcessHandle, + pub(crate) start_routine: usize, + pub(crate) start_parameter: usize, + pub(crate) max_thread_count: u32, + pub(crate) stack_reserve: usize, + pub(crate) stack_commit: usize, +} + +fn validate_worker_factory_object_attributes( + object_attributes: Option>, +) -> Result<(), NtStatus> { + let Some(object_attributes) = object_attributes else { + return Ok(()); + }; + let object_attributes = read_object_attributes::(object_attributes)?; + if object_attributes.object_name == 0 && !object_attributes.root_directory.is_null() { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + Ok(()) +} + +fn commit_worker_factory_shutdown( + factory: &WorkerFactoryObject, + pending_worker_count: MutPtr, +) -> NtStatus { + if pending_worker_count.write_at_offset(0, 0).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + factory.shutdown.store(true, Ordering::Relaxed); + NtStatus::SUCCESS +} + +impl Task { + fn worker_factory_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> + { + let Some(raw_fd) = handle.raw_fd() else { + return Err(NtStatus::INVALID_HANDLE); + }; + let typed = { + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::>(raw_fd) { + Ok(typed) => typed, + Err(ErrRawIntFd::NotFound) => return Err(NtStatus::INVALID_HANDLE), + Err(ErrRawIntFd::InvalidSubsystem) => { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + } + } + }; + self.global + .litebox + .descriptor_table() + .entry_handle(&typed) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn io_completion_port( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + let Some(raw_fd) = handle.raw_fd() else { + return Err(NtStatus::INVALID_HANDLE); + }; + let typed = { + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::>(raw_fd) { + Ok(typed) => typed, + Err(ErrRawIntFd::NotFound) => return Err(NtStatus::INVALID_HANDLE), + Err(ErrRawIntFd::InvalidSubsystem) => { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + } + } + }; + let Some(entry) = self.global.litebox.descriptor_table().entry_handle(&typed) else { + return Err(NtStatus::INVALID_HANDLE); + }; + entry.with_entry(|entry| { + entry.require_access(IoCompletionAccess::MODIFY_STATE)?; + Ok(entry.port()) + }) + } + + fn validate_worker_process_handle( + &self, + process_handle: ProcessHandle, + ) -> Result<(), NtStatus> { + if process_handle.is_current() { + return Ok(()); + } + let Some(raw_fd) = process_handle.as_handle().raw_fd() else { + return Err(NtStatus::INVALID_HANDLE); + }; + if self.process.handles.read().is_alive(raw_fd) { + Err(NtStatus::OBJECT_TYPE_MISMATCH) + } else { + Err(NtStatus::INVALID_HANDLE) + } + } + + fn insert_worker_factory_handle( + &self, + factory: Arc>, + granted_access: WorkerFactoryAccess, + ) -> Result { + let typed = self + .global + .litebox + .descriptor_table_mut() + .insert::>(WorkerFactoryHandleObject { + factory, + granted_access, + }); + insert_raw_handle::>( + &self.global.litebox, + &self.process.handles, + typed, + drop, + ) + } + + pub(crate) fn close_worker_factory_handle(&self, handle: Handle) { + remove_raw_handle::>( + &self.global.litebox, + &self.process.handles, + handle, + drop, + ); + } + + pub(crate) fn close_worker_factory(worker_factory: WorkerFactoryHandleObject) { + drop(worker_factory); + } + + pub(crate) fn sys_nt_create_worker_factory( + &self, + params: WorkerFactoryCreateParameters, + ) -> NtStatus { + if let Err(status) = + probe_guest_output_preserving_value::(params.worker_factory_handle) + { + return status; + } + let completion_port = match self.io_completion_port(params.completion_port_handle) { + Ok(port) => port, + Err(status) => return status, + }; + if let Err(status) = self.validate_worker_process_handle(params.worker_process_handle) { + return status; + } + if let Err(status) = + validate_worker_factory_object_attributes::(params.object_attributes) + { + return status; + } + + let factory = Arc::new(WorkerFactoryObject { + // TODO: create and manage actual worker threads using start_routine/start_parameter + // once worker dispatch, NtWaitForWorkViaWorkerFactory, and + // NtReleaseWorkerFactoryWorker are implemented. + _completion_port: completion_port, + _start_routine: params.start_routine, + _start_parameter: params.start_parameter, + binding_count: AtomicU32::new(0), + thread_minimum: AtomicU32::new(0), + thread_maximum: AtomicU32::new(params.max_thread_count), + thread_soft_maximum: AtomicU32::new(params.max_thread_count), + shutdown: AtomicBool::new(false), + _stack_reserve: params.stack_reserve, + _stack_commit: params.stack_commit, + }); + let granted_access = WorkerFactoryAccess::from_desired_access(params.desired_access); + let Ok(handle) = self.insert_worker_factory_handle(factory, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if params + .worker_factory_handle + .write_at_offset(0, handle) + .is_none() + { + self.close_worker_factory_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_set_information_worker_factory( + &self, + handle: Handle, + information_class: u32, + information: ConstPtr, + information_length: u32, + ) -> NtStatus { + litebox_util_log::debug!( + information_class = information_class, + information_length = information_length; + "NtSetInformationWorkerFactory parameters" + ); + let Ok(information_class) = WorkerFactoryInformationClass::from_raw(information_class) + else { + return NtStatus::INVALID_INFO_CLASS; + }; + if information_length as usize != size_of::() { + return NtStatus::INFO_LENGTH_MISMATCH; + } + let Some(value_bytes) = information.to_owned_slice(size_of::()) else { + return NtStatus::ACCESS_VIOLATION; + }; + let value = u32::from_le_bytes( + value_bytes + .as_ref() + .try_into() + .expect("ULONG input is four bytes"), + ); + + let entry = match self.worker_factory_entry(handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + entry + .with_entry(|entry| { + entry + .granted_access + .require(WorkerFactoryAccess::SET_INFORMATION)?; + // TODO: enforce these limits against real worker creation/drain behavior + // once worker threads are modeled; today they are only recorded. + match information_class { + WorkerFactoryInformationClass::BindingCount => { + // TODO: bind this to real worker/IOCP association state once worker + // factories track live bindings. + entry.factory.binding_count.store(value, Ordering::Relaxed); + } + WorkerFactoryInformationClass::ThreadMinimum => { + let maximum = entry.factory.thread_maximum.load(Ordering::Relaxed); + if value > maximum { + return Err(NtStatus::INVALID_PARAMETER); + } + entry.factory.thread_minimum.store(value, Ordering::Relaxed); + } + WorkerFactoryInformationClass::ThreadMaximum => { + let minimum = entry.factory.thread_minimum.load(Ordering::Relaxed); + if value < minimum { + return Err(NtStatus::INVALID_PARAMETER); + } + entry.factory.thread_maximum.store(value, Ordering::Relaxed); + } + WorkerFactoryInformationClass::ThreadSoftMaximum => { + let maximum = entry.factory.thread_maximum.load(Ordering::Relaxed); + if value > maximum { + return Err(NtStatus::INVALID_PARAMETER); + } + entry + .factory + .thread_soft_maximum + .store(value, Ordering::Relaxed); + } + } + Ok(()) + }) + .map_or_else(|status| status, |()| NtStatus::SUCCESS) + } + + pub(crate) fn sys_nt_shutdown_worker_factory( + &self, + handle: Handle, + pending_worker_count: MutPtr, + ) -> NtStatus { + if let Err(status) = + probe_guest_output_preserving_value::(pending_worker_count) + { + return status; + } + let entry = match self.worker_factory_entry(handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + let factory = match entry.with_entry(|entry| { + entry + .granted_access + .require(WorkerFactoryAccess::SHUTDOWN)?; + Ok(Arc::clone(&entry.factory)) + }) { + Ok(factory) => factory, + Err(status) => return status, + }; + // TODO: report the actual pending worker count and wake/release workers once worker + // threads are modeled; the current subset has no workers to drain. + commit_worker_factory_shutdown(&factory, pending_worker_count) + } +} + +#[cfg(test)] +mod tests { + use core::mem::size_of; + + use litebox::platform::ThreadProvider; + use litebox::utils::TruncateExt as _; + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::ObjectAttributes; + use crate::tests::{TestFS, TestPlatform, mut_ptr, null_mut_ptr, test_platform, test_task}; + + const EVENT_ALL_ACCESS: u32 = 0x001f_0003; + const IO_COMPLETION_QUERY_STATE: u32 = 0x0000_0001; + const IO_COMPLETION_ALL_ACCESS: u32 = 0x001f_0003; + const WORKER_FACTORY_ALL_ACCESS: u32 = 0x001f_003f; + const WORKER_FACTORY_QUERY_INFORMATION: u32 = 0x0008; + const WORKER_FACTORY_SHUTDOWN: u32 = 0x0020; + const START_ROUTINE: usize = 0x1234_5678; + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = test_platform(); + ::run_test_thread(f) + } + + fn create_io_completion_handle(task: &Task) -> Handle { + create_io_completion_handle_with_access(task, IO_COMPLETION_ALL_ACCESS) + } + + fn create_io_completion_handle_with_access( + task: &Task, + access: u32, + ) -> Handle { + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_io_completion(mut_ptr(&mut handle), access, None, 0), + NtStatus::SUCCESS + ); + handle + } + + fn create_worker_factory( + task: &Task, + worker_factory_handle: &mut Handle, + object_attributes: Option>, + completion_port_handle: Handle, + worker_process_handle: ProcessHandle, + ) -> NtStatus { + create_worker_factory_with_access( + task, + worker_factory_handle, + WORKER_FACTORY_ALL_ACCESS, + object_attributes, + completion_port_handle, + worker_process_handle, + ) + } + + fn create_worker_factory_with_access( + task: &Task, + worker_factory_handle: &mut Handle, + desired_access: u32, + object_attributes: Option>, + completion_port_handle: Handle, + worker_process_handle: ProcessHandle, + ) -> NtStatus { + task.sys_nt_create_worker_factory(WorkerFactoryCreateParameters { + worker_factory_handle: mut_ptr(worker_factory_handle), + desired_access, + object_attributes, + completion_port_handle, + worker_process_handle, + start_routine: START_ROUTINE, + start_parameter: 0, + max_thread_count: 1, + stack_reserve: 0, + stack_commit: 0, + }) + } + + fn information_ptr(value: &u32) -> ConstPtr { + ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) + } + + #[test] + fn create_requires_modify_state_on_completion_port() { + let task = test_task(); + let io_completion = + create_io_completion_handle_with_access(&task, IO_COMPLETION_QUERY_STATE); + let mut worker_factory = Handle::from_raw(usize::MAX); + + assert_eq!( + create_worker_factory( + &task, + &mut worker_factory, + None, + io_completion, + ProcessHandle::CURRENT + ), + NtStatus::ACCESS_DENIED + ); + assert_eq!(worker_factory, Handle::from_raw(usize::MAX)); + assert_eq!(task.sys_nt_close(io_completion), NtStatus::SUCCESS); + } + + #[test] + fn set_information_rejects_wrong_object_type_and_missing_access() { + let task = test_task(); + let io_completion = create_io_completion_handle(&task); + let value = 1; + let mut event = Handle::default(); + assert_eq!( + task.sys_nt_create_event(mut_ptr(&mut event), EVENT_ALL_ACCESS, None, 0, 0), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_set_information_worker_factory( + event, + WorkerFactoryInformationClass::ThreadMaximum as u32, + information_ptr(&value), + size_of::().trunc(), + ), + NtStatus::OBJECT_TYPE_MISMATCH + ); + + let mut worker_factory = Handle::default(); + assert_eq!( + create_worker_factory_with_access( + &task, + &mut worker_factory, + WORKER_FACTORY_QUERY_INFORMATION, + None, + io_completion, + ProcessHandle::CURRENT, + ), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_set_information_worker_factory( + worker_factory, + WorkerFactoryInformationClass::ThreadMaximum as u32, + information_ptr(&value), + size_of::().trunc(), + ), + NtStatus::ACCESS_DENIED + ); + + assert_eq!(task.sys_nt_close(worker_factory), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(event), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(io_completion), NtStatus::SUCCESS); + } + + #[test] + fn shutdown_sets_pending_worker_count_to_zero() { + let task = test_task(); + let io_completion = create_io_completion_handle(&task); + let mut worker_factory = Handle::default(); + assert_eq!( + create_worker_factory( + &task, + &mut worker_factory, + None, + io_completion, + ProcessHandle::CURRENT, + ), + NtStatus::SUCCESS + ); + + let mut pending_worker_count = 7; + assert_eq!( + task.sys_nt_shutdown_worker_factory(worker_factory, mut_ptr(&mut pending_worker_count)), + NtStatus::SUCCESS + ); + assert_eq!(pending_worker_count, 0); + + assert_eq!(task.sys_nt_close(worker_factory), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(io_completion), NtStatus::SUCCESS); + } + + #[test] + fn shutdown_output_fault_preserves_factory_state() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let io_completion = create_io_completion_handle(&task); + let mut worker_factory = Handle::default(); + assert_eq!( + create_worker_factory( + &task, + &mut worker_factory, + None, + io_completion, + ProcessHandle::CURRENT, + ), + NtStatus::SUCCESS + ); + let factory = task + .worker_factory_entry(worker_factory) + .expect("worker factory handle is valid") + .with_entry(|entry| Arc::clone(&entry.factory)); + assert!(!factory.shutdown.load(Ordering::Relaxed)); + + assert_eq!( + task.sys_nt_shutdown_worker_factory(worker_factory, null_mut_ptr()), + NtStatus::ACCESS_VIOLATION + ); + assert!(!factory.shutdown.load(Ordering::Relaxed)); + + assert_eq!( + commit_worker_factory_shutdown(&factory, null_mut_ptr()), + NtStatus::ACCESS_VIOLATION + ); + assert!(!factory.shutdown.load(Ordering::Relaxed)); + + assert_eq!(task.sys_nt_close(worker_factory), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(io_completion), NtStatus::SUCCESS); + }); + } + + #[test] + fn shutdown_validates_pending_worker_count_before_handle() { + run_with_test_platform_pointers(|| { + let task = test_task(); + + assert_eq!( + task.sys_nt_shutdown_worker_factory(Handle::default(), null_mut_ptr()), + NtStatus::ACCESS_VIOLATION + ); + }); + } + + #[test] + fn shutdown_rejects_wrong_object_type_and_missing_access() { + let task = test_task(); + let io_completion = create_io_completion_handle(&task); + let mut pending_worker_count = 1; + let mut event = Handle::default(); + assert_eq!( + task.sys_nt_create_event(mut_ptr(&mut event), EVENT_ALL_ACCESS, None, 0, 0), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_shutdown_worker_factory(event, mut_ptr(&mut pending_worker_count)), + NtStatus::OBJECT_TYPE_MISMATCH + ); + assert_eq!(pending_worker_count, 1); + + let mut worker_factory = Handle::default(); + assert_eq!( + create_worker_factory_with_access( + &task, + &mut worker_factory, + WORKER_FACTORY_QUERY_INFORMATION, + None, + io_completion, + ProcessHandle::CURRENT, + ), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_shutdown_worker_factory(worker_factory, mut_ptr(&mut pending_worker_count)), + NtStatus::ACCESS_DENIED + ); + assert_eq!(pending_worker_count, 1); + + assert_eq!(task.sys_nt_close(worker_factory), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(event), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(io_completion), NtStatus::SUCCESS); + + let io_completion = create_io_completion_handle(&task); + let mut worker_factory = Handle::default(); + assert_eq!( + create_worker_factory_with_access( + &task, + &mut worker_factory, + WORKER_FACTORY_SHUTDOWN, + None, + io_completion, + ProcessHandle::CURRENT, + ), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_shutdown_worker_factory(worker_factory, mut_ptr(&mut pending_worker_count)), + NtStatus::SUCCESS + ); + + assert_eq!(task.sys_nt_close(worker_factory), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(io_completion), NtStatus::SUCCESS); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn host_create_worker_factory_status_fidelity() { + use core::ffi::c_void; + + unsafe extern "system" { + fn NtCreateEvent( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + event_type: u32, + initial_state: u8, + ) -> i32; + fn NtCreateIoCompletion( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + number_of_concurrent_threads: u32, + ) -> i32; + fn NtCreateWorkerFactory( + handle: *mut *mut c_void, + desired_access: u32, + object_attributes: *const ObjectAttributes, + completion_port_handle: *mut c_void, + worker_process_handle: *mut c_void, + start_routine: *mut c_void, + start_parameter: *mut c_void, + max_thread_count: u32, + stack_reserve: usize, + stack_commit: usize, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } + + let mut host_io_completion = core::ptr::null_mut(); + // SAFETY: The output pointer is valid, attributes are null, and the handle is closed below. + let status = unsafe { + NtCreateIoCompletion( + &raw mut host_io_completion, + IO_COMPLETION_ALL_ACCESS, + core::ptr::null(), + 0, + ) + }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + + let task = test_task(); + let io_completion = create_io_completion_handle(&task); + + let mut host_worker_factory = core::ptr::null_mut(); + // SAFETY: All handles and pointers are valid for this status probe; the returned worker + // factory handle is closed before leaving the test. + let host_success = unsafe { + let status = NtCreateWorkerFactory( + &raw mut host_worker_factory, + WORKER_FACTORY_ALL_ACCESS, + core::ptr::null(), + host_io_completion, + usize::MAX as *mut c_void, + START_ROUTINE as *mut c_void, + core::ptr::null_mut(), + 1, + 0, + 0, + ); + if status == NtStatus::SUCCESS.as_raw() && !host_worker_factory.is_null() { + assert_eq!(NtClose(host_worker_factory), NtStatus::SUCCESS.as_raw()); + } + status + }; + + let mut shim_worker_factory = Handle::default(); + assert_eq!( + create_worker_factory( + &task, + &mut shim_worker_factory, + None, + io_completion, + ProcessHandle::CURRENT + ) + .as_raw(), + host_success + ); + assert!(!shim_worker_factory.is_null()); + assert_eq!(task.sys_nt_close(shim_worker_factory), NtStatus::SUCCESS); + + let mut host_query_io_completion = core::ptr::null_mut(); + // SAFETY: The output pointer is valid, attributes are null, and the handle is closed below. + let status = unsafe { + NtCreateIoCompletion( + &raw mut host_query_io_completion, + IO_COMPLETION_QUERY_STATE, + core::ptr::null(), + 0, + ) + }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + let mut shim_query_io_completion = Handle::default(); + assert_eq!( + task.sys_nt_create_io_completion( + mut_ptr(&mut shim_query_io_completion), + IO_COMPLETION_QUERY_STATE, + None, + 0 + ), + NtStatus::SUCCESS + ); + + let mut host_query_worker_factory = core::ptr::null_mut(); + // SAFETY: All pointers are valid; the completion port intentionally lacks modify access to + // compare native access checking with the shim. + let host_query_only_completion = unsafe { + let status = NtCreateWorkerFactory( + &raw mut host_query_worker_factory, + WORKER_FACTORY_ALL_ACCESS, + core::ptr::null(), + host_query_io_completion, + usize::MAX as *mut c_void, + START_ROUTINE as *mut c_void, + core::ptr::null_mut(), + 1, + 0, + 0, + ); + if status == NtStatus::SUCCESS.as_raw() && !host_query_worker_factory.is_null() { + assert_eq!( + NtClose(host_query_worker_factory), + NtStatus::SUCCESS.as_raw() + ); + } + status + }; + let mut shim_query_worker_factory = Handle::default(); + assert_eq!( + create_worker_factory( + &task, + &mut shim_query_worker_factory, + None, + shim_query_io_completion, + ProcessHandle::CURRENT + ) + .as_raw(), + host_query_only_completion + ); + if !shim_query_worker_factory.is_null() { + assert_eq!( + task.sys_nt_close(shim_query_worker_factory), + NtStatus::SUCCESS + ); + } + assert_eq!( + task.sys_nt_close(shim_query_io_completion), + NtStatus::SUCCESS + ); + + let bad_length = ObjectAttributes { + length: 1, + root_directory: Handle::default(), + object_name: 0, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + }; + // SAFETY: The host output and attributes pointers are valid locals; the bad length is the + // parameter being tested. + let host_bad_length = unsafe { + NtCreateWorkerFactory( + &raw mut host_worker_factory, + WORKER_FACTORY_ALL_ACCESS, + &raw const bad_length, + host_io_completion, + usize::MAX as *mut c_void, + START_ROUTINE as *mut c_void, + core::ptr::null_mut(), + 1, + 0, + 0, + ) + }; + assert_eq!( + create_worker_factory( + &task, + &mut shim_worker_factory, + Some(crate::tests::const_ptr(&bad_length)), + io_completion, + ProcessHandle::CURRENT + ) + .as_raw(), + host_bad_length + ); + + let mut host_event = core::ptr::null_mut(); + // SAFETY: The output pointer is valid, attributes are null, and the handle is closed below. + let status = unsafe { + NtCreateEvent( + &raw mut host_event, + EVENT_ALL_ACCESS, + core::ptr::null(), + 0, + 0, + ) + }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + let mut shim_event = Handle::default(); + assert_eq!( + task.sys_nt_create_event(mut_ptr(&mut shim_event), EVENT_ALL_ACCESS, None, 0, 0), + NtStatus::SUCCESS + ); + + // SAFETY: The event handle is valid but intentionally has the wrong object type for the + // completion-port argument. + let host_wrong_completion_type = unsafe { + NtCreateWorkerFactory( + &raw mut host_worker_factory, + WORKER_FACTORY_ALL_ACCESS, + core::ptr::null(), + host_event, + usize::MAX as *mut c_void, + START_ROUTINE as *mut c_void, + core::ptr::null_mut(), + 1, + 0, + 0, + ) + }; + assert_eq!( + create_worker_factory( + &task, + &mut shim_worker_factory, + None, + shim_event, + ProcessHandle::CURRENT + ) + .as_raw(), + host_wrong_completion_type + ); + + // SAFETY: All non-process arguments are valid; the event handle is intentionally passed as + // the process handle to probe native type checking. + let host_wrong_process_type = unsafe { + NtCreateWorkerFactory( + &raw mut host_worker_factory, + WORKER_FACTORY_ALL_ACCESS, + core::ptr::null(), + host_io_completion, + host_event, + START_ROUTINE as *mut c_void, + core::ptr::null_mut(), + 1, + 0, + 0, + ) + }; + assert_eq!( + create_worker_factory( + &task, + &mut shim_worker_factory, + None, + io_completion, + ProcessHandle::from_raw(shim_event.as_raw()) + ) + .as_raw(), + host_wrong_process_type + ); + + assert_eq!(task.sys_nt_close(shim_event), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(io_completion), NtStatus::SUCCESS); + // SAFETY: Handles were created successfully above and have not yet been closed. + unsafe { + assert_eq!(NtClose(host_event), NtStatus::SUCCESS.as_raw()); + assert_eq!(NtClose(host_io_completion), NtStatus::SUCCESS.as_raw()); + assert_eq!( + NtClose(host_query_io_completion), + NtStatus::SUCCESS.as_raw() + ); + } + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn host_set_worker_factory_status_fidelity() { + use core::ffi::c_void; + + unsafe extern "system" { + fn NtSetInformationWorkerFactory( + handle: *mut c_void, + worker_factory_information_class: u32, + worker_factory_information: *const c_void, + worker_factory_information_length: u32, + ) -> i32; + } + + let task = test_task(); + let value = 1; + + for (handle, class, info, length) in [ + ( + core::ptr::null_mut(), + WorkerFactoryInformationClass::ThreadMaximum as u32, + (&raw const value).cast(), + size_of::().trunc(), + ), + // This causes a crash + // ( + // core::ptr::null_mut(), + // WorkerFactoryInformationClass::ThreadMaximum as u32, + // core::ptr::null(), + // size_of::().trunc(), + // ), + ( + core::ptr::null_mut(), + 16, + (&raw const value).cast(), + size_of::().trunc(), + ), + ( + core::ptr::null_mut(), + WorkerFactoryInformationClass::ThreadMaximum as u32, + (&raw const value).cast(), + 0, + ), + ] { + // SAFETY: These probes intentionally use an invalid handle; any non-null input pointer + // points to a live local and no native worker factory can be started. + let host_status = unsafe { NtSetInformationWorkerFactory(handle, class, info, length) }; + assert_eq!( + task.sys_nt_set_information_worker_factory( + Handle::default(), + class, + if info.is_null() { + crate::tests::null_const_ptr() + } else { + information_ptr(&value) + }, + length, + ) + .as_raw(), + host_status + ); + } + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn host_set_worker_factory_wrong_type_status_fidelity() { + use core::ffi::c_void; + + unsafe extern "system" { + fn NtCreateEvent( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + event_type: u32, + initial_state: u8, + ) -> i32; + fn NtSetInformationWorkerFactory( + handle: *mut c_void, + worker_factory_information_class: u32, + worker_factory_information: *const c_void, + worker_factory_information_length: u32, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } + + let task = test_task(); + let value = 1; + let mut host_event = core::ptr::null_mut(); + // SAFETY: The output pointer is valid, attributes are null, and the handle is closed below. + let status = unsafe { + NtCreateEvent( + &raw mut host_event, + EVENT_ALL_ACCESS, + core::ptr::null(), + 0, + 0, + ) + }; + assert_eq!(status, NtStatus::SUCCESS.as_raw()); + let mut shim_event = Handle::default(); + assert_eq!( + task.sys_nt_create_event(mut_ptr(&mut shim_event), EVENT_ALL_ACCESS, None, 0, 0), + NtStatus::SUCCESS + ); + + // SAFETY: The event handle is valid but intentionally has the wrong object type. + let host_wrong_type = unsafe { + NtSetInformationWorkerFactory( + host_event, + WorkerFactoryInformationClass::ThreadMaximum as u32, + (&raw const value).cast(), + size_of::().trunc(), + ) + }; + assert_eq!( + task.sys_nt_set_information_worker_factory( + shim_event, + WorkerFactoryInformationClass::ThreadMaximum as u32, + information_ptr(&value), + size_of::().trunc(), + ) + .as_raw(), + host_wrong_type + ); + + assert_eq!(task.sys_nt_close(shim_event), NtStatus::SUCCESS); + // SAFETY: The host event handle was created successfully above and has not yet been closed. + unsafe { + assert_eq!(NtClose(host_event), NtStatus::SUCCESS.as_raw()); + } + } +} From 9355059a188b38baecab41a6df57f855ce543f03 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Fri, 19 Jun 2026 16:03:24 -0700 Subject: [PATCH 044/319] Cherry pick "Fix OP-TEE TA reentry stack alignment (#937)" (#941) Co-authored-by: Sangho Lee --- litebox_shim_optee/src/loader/ta_stack.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/litebox_shim_optee/src/loader/ta_stack.rs b/litebox_shim_optee/src/loader/ta_stack.rs index 272525dc0a..16ab1e9149 100644 --- a/litebox_shim_optee/src/loader/ta_stack.rs +++ b/litebox_shim_optee/src/loader/ta_stack.rs @@ -264,9 +264,14 @@ impl TaStack { ::fill_bytes_crng(platform, &mut canary); self.push_bytes(&canary)?; - // ensure stack is aligned + // `reenter_thread` *jumps* into the TA entry point (which is a function) rather than + // calls it. Adjust the stack pointer to ensure post-call stack alignment. self.pos = align_down(self.pos, Self::STACK_ALIGNMENT); - assert_eq!(self.pos, align_down(self.pos, Self::STACK_ALIGNMENT)); + self.pos = self.pos.checked_sub(core::mem::size_of::())?; + assert_eq!( + self.pos % Self::STACK_ALIGNMENT, + core::mem::size_of::() + ); Some(()) } From 28c6c57911f6bffc19a17aa5a99b7949fc521589 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Fri, 19 Jun 2026 21:15:04 -0700 Subject: [PATCH 045/319] Cherry pick "Add support for multiple PTAs (#906)" (#943) Co-authored-by: Sangho Lee --- litebox_shim_optee/src/lib.rs | 55 +++++-- litebox_shim_optee/src/syscalls/pta.rs | 212 +++++++++++++++++++++---- litebox_shim_optee/src/syscalls/tee.rs | 26 ++- 3 files changed, 228 insertions(+), 65 deletions(-) diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index dc75d1e89f..d94b688565 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -9,11 +9,12 @@ extern crate alloc; use crate::loader::elf::ElfLoaderError; +use crate::syscalls::pta::PseudoTa; use aes::{Aes128, Aes192, Aes256}; use alloc::{sync::Arc, vec}; use core::cell::Cell; use ctr::Ctr128BE; -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use litebox::{ LiteBox, mm::{PageManager, linux::PAGE_SIZE}, @@ -148,6 +149,7 @@ impl OpteeShimBuilder { pm: PageManager::new(&self.litebox), _litebox: self.litebox, ta_uuid_map: TaUuidMap::new(), + pta_busy: spin::mutex::SpinMutex::new(HashSet::new()), }); OpteeShim(global) } @@ -163,6 +165,14 @@ struct GlobalState { _litebox: litebox::LiteBox, /// The TA UUID to binary map for TA loading. ta_uuid_map: TaUuidMap, + /// Tracks which non-concurrent PTAs (i.e., PTAs w/o `TaFlags::CONCURRENT`) + /// are currently busy. A busy PTA is *rejected* with `TeeResult::Busy` + /// rather than queued. + /// + /// TODO: OP-TEE serializes concurrent access to a non-concurrent PTA by + /// blocking/queuing the caller until the PTA is free. We currently reject + /// instead of serialize; revisit if a PTA needs true serialization. + pta_busy: spin::mutex::SpinMutex>, } impl GlobalState { @@ -244,6 +254,7 @@ impl OpteeShim { tee_cryp_state_map: TeeCrypStateMap::new(), tee_obj_map: TeeObjMap::new(), ta_handle_map: TaHandleMap::new(), + pta_sessions: spin::mutex::SpinMutex::new(HashMap::new()), ta_entry_point: Cell::new(0), ta_stack_base_addr: Cell::new(0), ta_prepared: Cell::new(false), @@ -439,7 +450,7 @@ impl Task { if let Some(ta_uuid) = ta_uuid.read_at_offset(0) && let Some(usr_params) = usr_params.read_at_offset(0) { - Task::sys_open_ta_session( + self.sys_open_ta_session( ta_uuid, cancel_req_to, usr_params, @@ -450,7 +461,7 @@ impl Task { Err(TeeResult::BadParameters) } } - SyscallRequest::CloseTaSession { ta_sess_id } => Task::sys_close_ta_session(ta_sess_id), + SyscallRequest::CloseTaSession { ta_sess_id } => self.sys_close_ta_session(ta_sess_id), SyscallRequest::InvokeTaCommand { ta_sess_id, cancel_req_to, @@ -1285,6 +1296,8 @@ struct Task { tee_obj_map: TeeObjMap, /// TA handle to UUID map ta_handle_map: TaHandleMap, + /// PTA sessions opened by this TA task, mapping each session ID to its PTA. + pta_sessions: spin::mutex::SpinMutex>, /// TA entry point ta_entry_point: Cell, /// TA stack base address @@ -1313,6 +1326,12 @@ impl ThreadState { } } +impl Drop for Task { + fn drop(&mut self) { + self.close_all_pta_sessions(); + } +} + #[derive(Clone, Copy, Default)] pub(crate) enum ThreadInitState { #[default] @@ -1341,8 +1360,7 @@ pub(crate) enum ThreadInitState { /// With MAX_RECYCLABLE_SESSION_ID = 65536: /// - Bitmap memory usage: 65536 bits = 8 KB /// - Recyclable IDs: 1..=65536 (65536 IDs) -/// - Fallback (non-recyclable) IDs: 65537..=0xffff_fffd (~4.3B IDs, excluding PTA_SESSION_ID) -/// - PTA_SESSION_ID (0xffff_fffe) is reserved and never allocated +/// - Fallback (non-recyclable) IDs: 65537..=u32::MAX (~4.3B IDs) /// /// Design notes: /// - A single TA instance can serve many concurrent sessions (no per-instance cap), @@ -1357,6 +1375,8 @@ pub(crate) struct SessionIdPool { pool: litebox::utils::id_pool::IdPool, /// Next one-time ID when the recyclable pool is exhausted. fallback_next: u32, + /// Whether all fallback IDs have been issued. + fallback_exhausted: bool, } fn session_id_pool() -> &'static spin::mutex::SpinMutex { @@ -1367,6 +1387,7 @@ fn session_id_pool() -> &'static spin::mutex::SpinMutex { SessionIdPool::MAX_RECYCLABLE_SESSION_ID, ), fallback_next: SessionIdPool::MAX_RECYCLABLE_SESSION_ID + 1, + fallback_exhausted: false, }) }) } @@ -1374,27 +1395,32 @@ fn session_id_pool() -> &'static spin::mutex::SpinMutex { impl SessionIdPool { /// Maximum recyclable session ID tracked by the bitmap. const MAX_RECYCLABLE_SESSION_ID: u32 = 65536; - /// Reserved session ID for PTA. - const PTA_SESSION_ID: u32 = 0xffff_fffe; - /// Allocate a new session ID. /// /// Returns `None` if all recyclable session IDs are currently in use and /// the fallback one-time IDs are exhausted. pub fn allocate() -> Option { let mut pool = session_id_pool().lock(); + pool.allocate_inner() + } + fn allocate_inner(&mut self) -> Option { // Try recyclable pool first (pool ID 0 → session ID 1, etc.) - if let Some(id) = pool.pool.allocate() { + if let Some(id) = self.pool.allocate() { return Some(id + 1); } // Bitmap exhausted - use fallback one-time IDs if available - let fallback_id = pool.fallback_next; - if fallback_id >= Self::PTA_SESSION_ID { + if self.fallback_exhausted { return None; } - pool.fallback_next = fallback_id + 1; + + let fallback_id = self.fallback_next; + if fallback_id == u32::MAX { + self.fallback_exhausted = true; + } else { + self.fallback_next = fallback_id + 1; + } Some(fallback_id) } @@ -1406,10 +1432,6 @@ impl SessionIdPool { session_id_pool().lock().pool.recycle(session_id - 1); } - - pub fn get_pta_session_id() -> u32 { - Self::PTA_SESSION_ID - } } pub type NormalWorldConstPtr = crate::ptr::PhysConstPtr; @@ -1434,6 +1456,7 @@ mod test_utils { tee_cryp_state_map: TeeCrypStateMap::new(), tee_obj_map: TeeObjMap::new(), ta_handle_map: TaHandleMap::new(), + pta_sessions: spin::mutex::SpinMutex::new(HashMap::new()), ta_entry_point: Cell::new(0), ta_stack_base_addr: Cell::new(0), ta_prepared: Cell::new(false), diff --git a/litebox_shim_optee/src/syscalls/pta.rs b/litebox_shim_optee/src/syscalls/pta.rs index 8bde368d5f..30bffefa0c 100644 --- a/litebox_shim_optee/src/syscalls/pta.rs +++ b/litebox_shim_optee/src/syscalls/pta.rs @@ -13,18 +13,78 @@ use litebox::platform::{ }; use litebox::utils::TruncateExt; use litebox_common_optee::{ - HUK_SUBKEY_MAX_LEN, HukSubkeyUsage, TeeParamType, TeeResult, TeeUuid, UteeParams, + HUK_SUBKEY_MAX_LEN, HukSubkeyUsage, TaFlags, TeeParamType, TeeResult, TeeUuid, UteeParams, }; use num_enum::TryFromPrimitive; use sha2::Sha256; use zeroize::{Zeroize, Zeroizing}; -pub const PTA_SYSTEM_UUID: TeeUuid = TeeUuid { - time_low: 0x3a2f_8978, - time_mid: 0x5dc0, - time_hi_and_version: 0x11e8, - clock_seq_and_node: [0x9c, 0x2d, 0xfa, 0x7a, 0xe0, 0x1b, 0xbe, 0xbc], -}; +struct SystemPta; + +/// A common interface to interact with various PTAs including the system PTA. +/// +/// Add new PTAs here as needed. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum PseudoTa { + System, +} + +impl PseudoTa { + pub(crate) fn from_uuid(uuid: &TeeUuid) -> Option { + match *uuid { + SystemPta::UUID => Some(Self::System), + _ => None, + } + } + + /// Open a session to this PTA, returning the allocated session ID. + fn open_session(self, params: &UteeParams) -> Result { + match self { + Self::System => SystemPta::open_session(params), + } + } + + pub(crate) fn invoke_command( + self, + task: &Task, + cmd_id: u32, + params: &UteeParams, + ) -> Result<(), TeeResult> { + let _busy = task.try_set_busy(self)?; + match self { + Self::System => SystemPta::invoke_command(task, cmd_id, params), + } + } + + fn close_session(self, task: &Task, session_id: u32) { + match self { + Self::System => SystemPta::close_session(task, session_id), + } + } + + fn flags(self) -> TaFlags { + match self { + Self::System => SystemPta::FLAGS, + } + } +} + +const PTA_DEFAULT_FLAGS: TaFlags = TaFlags::SINGLE_INSTANCE + .union(TaFlags::MULTI_SESSION) + .union(TaFlags::INSTANCE_KEEP_ALIVE); + +const MAX_PTA_SESSIONS_PER_TASK: usize = 100; + +struct PtaBusyGuard<'a> { + task: &'a Task, + pta: PseudoTa, +} + +impl Drop for PtaBusyGuard<'_> { + fn drop(&mut self) { + self.task.global.pta_busy.lock().remove(&self.pta); + } +} const PTA_SYSTEM_ADD_RNG_ENTROPY: u32 = 0; const PTA_SYSTEM_DERIVE_TA_UNIQUE_KEY: u32 = 1; @@ -51,7 +111,7 @@ const TA_DERIVED_EXTRA_DATA_MAX_SIZE: usize = 1024; /// `PTA_SYSTEM_*` command ID from `optee_os/lib/libutee/include/pta_system.h` #[derive(Clone, Copy, TryFromPrimitive)] #[repr(u32)] -pub enum PtaSystemCommandId { +enum PtaSystemCommandId { AddRngEntropy = PTA_SYSTEM_ADD_RNG_ENTROPY, DeriveTaUniqueKey = PTA_SYSTEM_DERIVE_TA_UNIQUE_KEY, MapZi = PTA_SYSTEM_MAP_ZI, @@ -68,36 +128,117 @@ pub enum PtaSystemCommandId { SuppPluginInvoke = PTA_SYSTEM_SUPP_PLUGIN_INVOKE, } -/// Checks whether a given TA is a (system) PTA and its parameter is valid. -pub fn is_pta(ta_uuid: &TeeUuid, params: &UteeParams) -> bool { - // TODO: consider other PTAs - *ta_uuid == PTA_SYSTEM_UUID - && params.get_type(0).is_ok_and(|t| t == TeeParamType::None) - && params.get_type(1).is_ok_and(|t| t == TeeParamType::None) - && params.get_type(2).is_ok_and(|t| t == TeeParamType::None) - && params.get_type(3).is_ok_and(|t| t == TeeParamType::None) -} +type HmacSha256 = Hmac; -// TODO: replace it with a proper implementation. -pub fn close_pta_session(_ta_session_id: u32) {} +impl Task { + /// Try to mark a non-concurrent PTA as busy, returning a guard that clears + /// the busy state on drop. This gates both session opening and command + /// invocation. + /// + /// Returns `Ok(None)` for PTAs flagged `TaFlags::CONCURRENT` (no gating). + /// For a non-concurrent PTA that is busy, returns `Err(Busy)` immediately. + fn try_set_busy(&self, pta: PseudoTa) -> Result>, TeeResult> { + if pta.flags().contains(TaFlags::CONCURRENT) { + return Ok(None); + } -/// Check whether a given session ID is associated with a PTA. -pub fn is_pta_session(ta_sess_id: u32) -> bool { - ta_sess_id == crate::SessionIdPool::get_pta_session_id() -} + let mut busy = self.global.pta_busy.lock(); + if busy.contains(&pta) { + return Err(TeeResult::Busy); + } -type HmacSha256 = Hmac; + busy.insert(pta); + Ok(Some(PtaBusyGuard { task: self, pta })) + } -impl Task { - /// Handle a command of the system PTA. - pub fn handle_system_pta_command( + pub(crate) fn open_pta_session( &self, - cmd_id: u32, + pta: PseudoTa, params: &UteeParams, - ) -> Result<(), TeeResult> { + ) -> Result { + let _busy = self.try_set_busy(pta)?; + + // OP-TEE OS permits multiple sessions to the same PTA. We cap the number + // of PTA sessions per TA instance to prevent a TA from exhausting session + // IDs or memory. The cap is checked while holding the lock, then the lock + // is released before `open_session` runs. + { + let pta_sessions = self.pta_sessions.lock(); + if pta_sessions.len() >= MAX_PTA_SESSIONS_PER_TASK { + return Err(TeeResult::Busy); + } + } + + // Run the PTA hook without holding `pta_sessions`. OP-TEE `Task` is + // single-threaded, so nothing else mutates `pta_sessions` in the meantime. + // Keeping the hook outside the lock to avoid a self deadlock. + let session_id = pta.open_session(params)?; + + let prev = self.pta_sessions.lock().insert(session_id, pta); + debug_assert!( + prev.is_none(), + "freshly allocated session ID collided with an existing PTA session", + ); + Ok(session_id) + } + + pub(crate) fn close_pta_session(&self, ta_session_id: u32) -> Option { + let mut pta_sessions = self.pta_sessions.lock(); + let pta = pta_sessions.remove(&ta_session_id)?; + drop(pta_sessions); + pta.close_session(self, ta_session_id); + crate::SessionIdPool::recycle(ta_session_id); + Some(pta) + } + + /// Get the PTA associated with a session (if exists). + pub(crate) fn pta_for_session(&self, ta_sess_id: u32) -> Option { + self.pta_sessions.lock().get(&ta_sess_id).copied() + } + + pub(crate) fn close_all_pta_sessions(&self) { + // Drain into a local buffer and release the lock before invoking + // `close_session` to avoid potential dead locks. + let sessions: Vec<(u32, PseudoTa)> = self.pta_sessions.lock().drain().collect(); + for (session_id, pta) in sessions { + pta.close_session(self, session_id); + crate::SessionIdPool::recycle(session_id); + } + } +} + +impl SystemPta { + const FLAGS: TaFlags = PTA_DEFAULT_FLAGS.union(TaFlags::CONCURRENT); + + const UUID: TeeUuid = TeeUuid { + time_low: 0x3a2f_8978, + time_mid: 0x5dc0, + time_hi_and_version: 0x11e8, + clock_seq_and_node: [0x9c, 0x2d, 0xfa, 0x7a, 0xe0, 0x1b, 0xbe, 0xbc], + }; + + fn open_session(params: &UteeParams) -> Result { + if !params.has_types([ + TeeParamType::None, + TeeParamType::None, + TeeParamType::None, + TeeParamType::None, + ]) { + return Err(TeeResult::BadParameters); + } + + crate::SessionIdPool::allocate().ok_or(TeeResult::Busy) + } + + fn close_session(_task: &Task, _session_id: u32) { + // System PTA has no per-session state + } + + /// Handle a command of the system PTA. + fn invoke_command(task: &Task, cmd_id: u32, params: &UteeParams) -> Result<(), TeeResult> { #[allow(clippy::single_match_else)] match PtaSystemCommandId::try_from(cmd_id).map_err(|_| TeeResult::BadParameters)? { - PtaSystemCommandId::DeriveTaUniqueKey => self.derive_ta_unique_key(params), + PtaSystemCommandId::DeriveTaUniqueKey => Self::derive_ta_unique_key(task, params), _ => { #[cfg(debug_assertions)] todo!("support other system PTA commands {cmd_id}"); @@ -111,7 +252,7 @@ impl Task { /// /// This follows the OP-TEE `system_derive_ta_unique_key` implementation from /// `core/pta/system.c`. - fn derive_ta_unique_key(&self, params: &UteeParams) -> Result<(), TeeResult> { + fn derive_ta_unique_key(task: &Task, params: &UteeParams) -> Result<(), TeeResult> { use TeeParamType::{MemrefInput, MemrefOutput, None}; if !params.has_types([MemrefInput, MemrefOutput, None, None]) { @@ -153,9 +294,10 @@ impl Task { let subkey_ptr = UserMutPtr::::from_usize(subkey_addr.trunc()); // subkey = KDF(huk, usage || ta_uuid || extra_data) - let ta_uuid_bytes = self.ta_app_id.to_le_bytes(); + let ta_uuid_bytes = task.ta_app_id.to_le_bytes(); let mut subkey_buf = Zeroizing::new(vec![0u8; subkey_size]); - self.huk_subkey_derive( + Self::huk_subkey_derive( + task, HukSubkeyUsage::UniqueTa, &[&ta_uuid_bytes, &extra_data], &mut subkey_buf, @@ -171,7 +313,7 @@ impl Task { /// /// This follows the OP-TEE `huk_subkey_derive` interface from `core/kernel/huk_subkey.c`. fn huk_subkey_derive( - &self, + task: &Task, usage: HukSubkeyUsage, const_data: &[&[u8]], subkey: &mut [u8], @@ -193,7 +335,7 @@ impl Task { output: subkey, }; - self.global + task.global .platform .derive_key(Some(huk_subkey_derive_inner), kdf_params) .map_err(|err| match err { diff --git a/litebox_shim_optee/src/syscalls/tee.rs b/litebox_shim_optee/src/syscalls/tee.rs index 6ee19b6f76..764606392b 100644 --- a/litebox_shim_optee/src/syscalls/tee.rs +++ b/litebox_shim_optee/src/syscalls/tee.rs @@ -15,10 +15,7 @@ use litebox_common_optee::{ use num_enum::TryFromPrimitive; use zerocopy::IntoBytes; -use crate::{ - Task, UserConstPtr, UserMutPtr, - syscalls::pta::{close_pta_session, is_pta, is_pta_session}, -}; +use crate::{Task, UserConstPtr, UserMutPtr, syscalls::pta::PseudoTa}; #[inline] fn align_up(addr: usize, align: usize) -> Option { @@ -165,6 +162,7 @@ impl Task { /// A system call to open a session with a PTA or another user-mode TA. pub fn sys_open_ta_session( + &self, ta_uuid: TeeUuid, _cancel_req_to: u32, usr_params: UteeParams, @@ -175,12 +173,14 @@ impl Task { ret_orig .write_at_offset(0, TeeOrigin::Tee) .ok_or(TeeResult::AccessDenied)?; - if is_pta(&ta_uuid, &usr_params) { + if let Some(pta) = PseudoTa::from_uuid(&ta_uuid) { // `open_ta_session` syscall lets a user-mode TA open a session to a PTA which provides // several import services (it works as a proxy for extra system calls). - ta_sess_id - .write_at_offset(0, crate::SessionIdPool::get_pta_session_id()) - .ok_or(TeeResult::AccessDenied)?; + let session_id = self.open_pta_session(pta, &usr_params)?; + if ta_sess_id.write_at_offset(0, session_id).is_none() { + self.close_pta_session(session_id); + return Err(TeeResult::AccessDenied); + } Ok(()) } else { // `open_ta_session` syscall lets a user-mode TA open a session to another user-mode TA @@ -196,9 +196,8 @@ impl Task { /// A system call to close an opened session. #[allow(clippy::unnecessary_wraps)] - pub fn sys_close_ta_session(ta_sess_id: u32) -> Result<(), TeeResult> { - if is_pta_session(ta_sess_id) { - close_pta_session(ta_sess_id); + pub fn sys_close_ta_session(&self, ta_sess_id: u32) -> Result<(), TeeResult> { + if self.close_pta_session(ta_sess_id).is_some() { Ok(()) } else { #[cfg(debug_assertions)] @@ -221,9 +220,8 @@ impl Task { ret_orig .write_at_offset(0, TeeOrigin::Tee) .ok_or(TeeResult::AccessDenied)?; - if is_pta_session(ta_sess_id) { - // TODO: check whether `ta_sess_id` is associated with the system PTA. - self.handle_system_pta_command(cmd_id, ¶ms) + if let Some(pta) = self.pta_for_session(ta_sess_id) { + pta.invoke_command(self, cmd_id, ¶ms) } else { #[cfg(debug_assertions)] todo!("support inter TA interaction"); From 89c29678732a98c9604ef509027fbb83ca11ca2d Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 22 Jun 2026 14:32:10 -0700 Subject: [PATCH 046/319] Refine broker core session API (#935) This PR makes BrokerCore a shared handle with session-scoped broker authority state and routes broker event operations through BrokerSession-owned object helpers. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 12 +- litebox/src/event/counter.rs | 23 +- litebox_broker_core/Cargo.toml | 3 +- litebox_broker_core/src/event.rs | 219 ++++---------- litebox_broker_core/src/identity.rs | 71 ----- litebox_broker_core/src/lib.rs | 91 +++--- litebox_broker_core/src/object.rs | 267 ----------------- litebox_broker_core/src/policy.rs | 144 ++++------ litebox_broker_core/src/session.rs | 272 ++++++++++++++++++ litebox_broker_host/src/lib.rs | 92 +++--- litebox_broker_local/src/lib.rs | 2 +- litebox_broker_protocol/src/event.rs | 5 +- litebox_broker_protocol/src/wire.rs | 15 +- litebox_broker_protocol/src/wire/event.rs | 7 +- litebox_broker_userland/src/main.rs | 8 +- .../tests/userland_broker.rs | 6 +- litebox_runner_linux_userland/tests/run.rs | 8 +- 17 files changed, 514 insertions(+), 731 deletions(-) delete mode 100644 litebox_broker_core/src/identity.rs delete mode 100644 litebox_broker_core/src/object.rs create mode 100644 litebox_broker_core/src/session.rs diff --git a/Cargo.lock b/Cargo.lock index e841dfa79e..5405cf950b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1481,8 +1481,9 @@ name = "litebox_broker_core" version = "0.1.0" dependencies = [ "bitflags 2.11.0", + "hashbrown", "litebox_broker_protocol", - "slotmap", + "spin 0.9.8", "thiserror", ] @@ -2836,15 +2837,6 @@ dependencies = [ "log", ] -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - [[package]] name = "smallvec" version = "1.15.1" diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index a402a680b4..5fad8e0afd 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -134,18 +134,6 @@ where Ok(response.readiness) } - fn readiness_state(&self) -> Result { - let response = - self.request_event(EventRequest::Wait(WaitEventRequest::new(self.handle)))?; - let EventResponse::Wait(response) = response else { - return Err(BrokerObjectError::UnexpectedResponse); - }; - Ok(match response.outcome { - WaitOutcome::Ready(readiness) | WaitOutcome::WouldBlock(readiness) => readiness, - _ => return Err(BrokerObjectError::UnexpectedResponse), - }) - } - fn request_event(&self, request: EventRequest) -> Result { self.broker .request(CoreRequest::Event(request)) @@ -163,7 +151,16 @@ where } fn check_io_events(&self) -> Events { - let Ok(readiness) = self.readiness_state() else { + let Ok(response) = + self.request_event(EventRequest::Wait(WaitEventRequest::new(self.handle))) + else { + return Events::empty(); + }; + let EventResponse::Wait(response) = response else { + return Events::empty(); + }; + let (WaitOutcome::Ready(readiness) | WaitOutcome::WouldBlock(readiness)) = response.outcome + else { return Events::empty(); }; let mut events = Events::empty(); diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index 86666b5731..7f28f63ddb 100644 --- a/litebox_broker_core/Cargo.toml +++ b/litebox_broker_core/Cargo.toml @@ -5,8 +5,9 @@ edition = "2024" [dependencies] bitflags = { version = "2.9.0", default-features = false } +hashbrown = "0.15.2" litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -slotmap = { version = "1.1.1", default-features = false } +spin = { version = "0.9.8", default-features = false, features = ["rwlock"] } thiserror = { version = "2.0.6", default-features = false } [lints] diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 4bd0c02fb7..447102c28a 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -1,140 +1,84 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::object::{ObjectEntry, ObjectId}; -use crate::{BrokerAssociation, BrokerCore, BrokerError, ObjectRights, Result}; +//! Broker-owned event object operations. + +use crate::session::{ObjectEntry, ObjectRights}; +use crate::{BrokerError, BrokerSession, Result}; use litebox_broker_protocol::{ EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, }; -const MAX_EVENT_COUNT: u64 = u64::MAX - 1; - -impl BrokerCore { - /// Creates a broker-owned event object. - pub fn create_event(&mut self, association: &BrokerAssociation) -> Result { - self.create_event_with_count(association, 0) - } - - /// Creates a broker-owned event object with initial readiness credits. - pub fn create_event_with_count( - &mut self, - association: &BrokerAssociation, - initial_count: u64, - ) -> Result { - if initial_count > MAX_EVENT_COUNT { - return Err(BrokerError::ResourceExhausted); - } - let rights = self.authorize_create_event(association)?; - - self.insert_object_with_reference( - association, - ObjectEntry::Event(EventObject::new(initial_count)), - rights, - ) - } +pub(crate) const MAX_EVENT_COUNT: u64 = u64::MAX - 1; - /// Checks whether an event wait would complete now. - /// - /// Blocking is intentionally outside BrokerCore for the first proof of - /// concept. Userland or kernel deployments can block on deployment-specific - /// wait primitives after BrokerCore authorizes and reports readiness state. - pub fn wait_event( - &self, - association: &BrokerAssociation, - handle: ObjectHandle, - ) -> Result { - let authorized = self.authorize_use_event(association, handle, ObjectRights::WAIT)?; - let state = Self::filter_readiness_for_rights( - self.event_state(authorized.object_id)?, - authorized.rights, - ); - Ok(if state.read_ready { - WaitOutcome::Ready(state) - } else { - WaitOutcome::WouldBlock(state) - }) +/// Creates a broker-owned event object with initial readiness credits. +pub fn create(session: &BrokerSession, initial_count: u64) -> Result { + if initial_count > MAX_EVENT_COUNT { + return Err(BrokerError::ResourceExhausted); } - /// Adds readiness credits to a broker-owned event object. - pub fn add_event( - &mut self, - association: &BrokerAssociation, - handle: ObjectHandle, - value: u64, - ) -> Result { - let authorized = self.authorize_use_event(association, handle, ObjectRights::WRITE)?; - match self.object_mut(authorized.object_id)? { - ObjectEntry::Event(event) => event - .add(value) - .map(|state| Self::filter_readiness_for_rights(state, authorized.rights)), - } - } + session.create_object_reference(ObjectEntry::Event(EventObject::new(initial_count))) +} - /// Consumes readiness credits from a broker-owned event object. - pub fn consume_event( - &mut self, - association: &BrokerAssociation, - handle: ObjectHandle, - mode: EventConsumeMode, - ) -> Result { - let authorized = self.authorize_use_event(association, handle, ObjectRights::WAIT)?; - match self.object_mut(authorized.object_id)? { - ObjectEntry::Event(event) => event.consume(mode).map(|response| { - EventConsumption::new( - response.value, - Self::filter_readiness_for_rights(response.readiness, authorized.rights), - ) - }), +/// Checks whether an event wait would complete now. +/// +/// Blocking is intentionally outside BrokerCore for the first proof of +/// concept. Userland or kernel deployments can block on deployment-specific +/// wait primitives after BrokerCore authorizes and reports readiness state. +pub fn wait(session: &BrokerSession, handle: ObjectHandle) -> Result { + let required_rights = ObjectRights::WAIT; + session.with_authorized_object(handle, required_rights, |object| match object { + ObjectEntry::Event(event) => { + let readiness = ReadinessState::new(event.count > 0, event.count < MAX_EVENT_COUNT); + Ok(if readiness.read_ready { + WaitOutcome::Ready(readiness) + } else { + WaitOutcome::WouldBlock(readiness) + }) } - } + }) +} - fn filter_readiness_for_rights(state: ReadinessState, rights: ObjectRights) -> ReadinessState { - ReadinessState::new( - rights.contains(ObjectRights::WAIT) && state.read_ready, - rights.contains(ObjectRights::WRITE) && state.write_ready, - state.generation, - ) - } +/// Adds readiness credits to a broker-owned event object. +pub fn add(session: &BrokerSession, handle: ObjectHandle, value: u64) -> Result { + let required_rights = ObjectRights::WRITE; + session.with_authorized_object_mut(handle, required_rights, |object| match object { + ObjectEntry::Event(event) => event.add(value), + }) +} - fn event_state(&self, object_id: ObjectId) -> Result { - match self.object(object_id)? { - ObjectEntry::Event(event) => Ok(event.readiness_state()), - } - } +/// Consumes readiness credits from a broker-owned event object. +pub fn consume( + session: &BrokerSession, + handle: ObjectHandle, + mode: EventConsumeMode, +) -> Result { + let required_rights = ObjectRights::WAIT; + session.with_authorized_object_mut(handle, required_rights, |object| match object { + ObjectEntry::Event(event) => event.consume(mode), + }) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct EventObject { count: u64, - readiness_generation: u64, } impl EventObject { - pub(crate) const fn new(count: u64) -> Self { - Self { - count, - readiness_generation: 0, - } - } - - pub(crate) const fn readiness_state(self) -> ReadinessState { - ReadinessState::new( - self.count > 0, - self.count < MAX_EVENT_COUNT, - self.readiness_generation, - ) + const fn new(count: u64) -> Self { + Self { count } } fn add(&mut self, value: u64) -> Result { - let new_count = self + self.count = self .count .checked_add(value) .filter(|count| *count <= MAX_EVENT_COUNT) .ok_or(BrokerError::WouldBlock)?; - let next_generation = self.next_generation()?; - self.count = new_count; - self.readiness_generation = next_generation; - Ok(self.readiness_state()) + Ok(ReadinessState::new( + self.count > 0, + self.count < MAX_EVENT_COUNT, + )) } fn consume(&mut self, mode: EventConsumeMode) -> Result { @@ -147,61 +91,10 @@ impl EventObject { EventConsumeMode::One => 1, _ => return Err(BrokerError::UnsupportedOperation), }; - let next_generation = self.next_generation()?; self.count -= value; - self.readiness_generation = next_generation; - Ok(EventConsumption::new(value, self.readiness_state())) - } - - fn next_generation(&self) -> Result { - self.readiness_generation - .checked_add(1) - .ok_or(BrokerError::ResourceExhausted) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn event_readiness_state_only_reports_authorized_directions() { - let readiness = ReadinessState::new(true, true, 7); - - assert_eq!( - BrokerCore::filter_readiness_for_rights(readiness, ObjectRights::WAIT), - ReadinessState::new(true, false, 7) - ); - assert_eq!( - BrokerCore::filter_readiness_for_rights(readiness, ObjectRights::WRITE), - ReadinessState::new(false, true, 7) - ); - } - - #[test] - fn add_event_does_not_mutate_count_when_generation_is_exhausted() { - let mut event = EventObject { - count: 1, - readiness_generation: u64::MAX, - }; - - assert_eq!(event.add(1), Err(BrokerError::ResourceExhausted)); - assert_eq!(event.count, 1); - assert_eq!(event.readiness_generation, u64::MAX); - } - - #[test] - fn consume_event_does_not_mutate_count_when_generation_is_exhausted() { - let mut event = EventObject { - count: 1, - readiness_generation: u64::MAX, - }; - - assert_eq!( - event.consume(EventConsumeMode::One), - Err(BrokerError::ResourceExhausted) - ); - assert_eq!(event.count, 1); - assert_eq!(event.readiness_generation, u64::MAX); + Ok(EventConsumption::new( + value, + ReadinessState::new(self.count > 0, self.count < MAX_EVENT_COUNT), + )) } } diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs deleted file mode 100644 index 5b26f26d06..0000000000 --- a/litebox_broker_core/src/identity.rs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use crate::{BrokerCore, Result, allocate_id}; - -/// Caller identity information supplied by the broker entry layer. -/// -/// The first userland proof of concept does not authenticate Unix-socket peers, -/// but BrokerCore still accepts an explicit credential value so authenticated -/// servers or hosts can plumb identity through the same association-creation seam. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[non_exhaustive] -pub enum CallerCredential { - /// Explicit deployment mode for the initial unauthenticated userland POC. - Unauthenticated, -} - -/// Broker-assigned guest process identity. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ProcessId(u64); - -impl ProcessId { - pub(crate) const fn new(raw: u64) -> Self { - Self(raw) - } -} - -/// Broker-owned authority token for one authenticated caller association. -/// -/// User mode does not choose this value. The broker entry layer authenticates -/// the caller, then BrokerCore assigns this identity for all operations received -/// on that association. -#[derive(Debug, PartialEq, Eq)] -pub struct BrokerAssociation { - /// Broker-assigned guest process identity. - process_id: ProcessId, - /// Broker-entry-authenticated caller credential for this association. - caller_credential: CallerCredential, -} - -impl BrokerAssociation { - /// Creates an authenticated association identity. - pub(crate) const fn new(process_id: ProcessId, caller_credential: CallerCredential) -> Self { - Self { - process_id, - caller_credential, - } - } - - pub(crate) const fn process_id(&self) -> ProcessId { - self.process_id - } - - /// Returns the broker-entry-authenticated caller credential for this association. - pub const fn caller_credential(&self) -> CallerCredential { - self.caller_credential - } -} - -impl BrokerCore { - /// Allocates broker authority state for one authenticated caller association. - pub fn create_association( - &mut self, - caller_credential: CallerCredential, - ) -> Result { - let process_id = allocate_id(&mut self.next_process_id)?; - let association = BrokerAssociation::new(ProcessId::new(process_id), caller_credential); - Ok(association) - } -} diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index b0759141af..95aac9b09f 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -19,32 +19,31 @@ extern crate alloc; extern crate std; mod error; -mod event; -mod identity; -mod object; +pub mod event; mod policy; +mod session; +use alloc::sync::Arc; use core::sync::atomic::{AtomicBool, Ordering}; -use alloc::collections::BTreeMap; -use slotmap::SlotMap; +use hashbrown::HashMap; +use litebox_broker_protocol::ObjectHandle; +use spin::rwlock::RwLock; pub use error::BrokerError; -pub use identity::{BrokerAssociation, CallerCredential}; -use litebox_broker_protocol::ObjectHandle; -pub use object::ObjectRights; -use object::{ObjectEntry, ObjectId, ObjectReference}; -pub use policy::{PolicyEngine, PolicyProfile}; +pub use policy::{PolicyEngine, PolicyProfile, PrincipalRights}; +use session::ObjectReference; +pub use session::{BrokerSession, CallerCredential, ObjectRights}; /// BrokerCore result type. pub type Result = core::result::Result; /// Resource limits for broker-owned authority state. +/// +/// These limits are global to the broker core, not per session. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct BrokerCoreLimits { - /// Maximum live broker objects. - pub max_objects: usize, /// Maximum live object references. pub max_references: usize, } @@ -52,16 +51,12 @@ pub struct BrokerCoreLimits { impl BrokerCoreLimits { /// Conservative default limits for initial broker deployments. pub const DEFAULT: Self = Self { - max_objects: 4096, max_references: 4096, }; /// Creates a broker core limit set. - pub const fn new(max_objects: usize, max_references: usize) -> Self { - Self { - max_objects, - max_references, - } + pub const fn new(max_references: usize) -> Self { + Self { max_references } } } @@ -71,20 +66,18 @@ impl Default for BrokerCoreLimits { } } -const MAX_OBJECTS: usize = u32::MAX as usize - 1; - -/// Channel-independent broker authority state. +/// Channel-independent broker authority handle. /// /// A broker process may construct only one broker core for its process /// lifetime. Constructors return [`BrokerError::BrokerCoreAlreadyExists`] if a /// core has already been constructed. +#[derive(Clone)] pub struct BrokerCore { - policy: PolicyEngine, - limits: BrokerCoreLimits, - next_process_id: u64, - next_reference_handle: u64, - objects: SlotMap, - references: BTreeMap, + pub(crate) policy: PolicyEngine, + pub(crate) limits: BrokerCoreLimits, + pub(crate) next_session_id: Arc>, + pub(crate) next_reference_handle: Arc>, + pub(crate) references: Arc>>, } static BROKER_CORE_CREATED: AtomicBool = AtomicBool::new(false); @@ -97,10 +90,6 @@ impl BrokerCore { /// Creates the broker core with explicit authority-state limits. pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Result { - if limits.max_objects > MAX_OBJECTS { - return Err(BrokerError::ResourceExhausted); - } - BROKER_CORE_CREATED .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .map_err(|_| BrokerError::BrokerCoreAlreadyExists)?; @@ -108,16 +97,38 @@ impl BrokerCore { Ok(Self { policy, limits, - next_process_id: 1, - next_reference_handle: 1, - objects: SlotMap::with_key(), - references: BTreeMap::new(), + next_session_id: Arc::new(RwLock::new(1)), + next_reference_handle: Arc::new(RwLock::new(1)), + references: Arc::new(RwLock::new(HashMap::new())), }) } -} -fn allocate_id(next_id: &mut u64) -> Result { - let id = *next_id; - *next_id = id.checked_add(1).ok_or(BrokerError::ResourceExhausted)?; - Ok(id) + pub(crate) fn allocate_reference_handle(&self) -> Result { + let mut next_reference_handle = self.next_reference_handle.write(); + let handle = ObjectHandle(*next_reference_handle); + *next_reference_handle = handle + .0 + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?; + Ok(handle) + } + + /// Allocates broker authority state for one authenticated caller session. + pub fn create_session(&self, caller_credential: CallerCredential) -> Result { + let mut next_session_id = self.next_session_id.write(); + let session_id = *next_session_id; + *next_session_id = session_id + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?; + Ok(BrokerSession::new( + self.clone(), + session::SessionId(session_id), + caller_credential, + )) + } + + pub(crate) fn close_session(&self, session_id: session::SessionId) { + let mut references = self.references.write(); + references.retain(|_, reference| reference.session_id != session_id); + } } diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs deleted file mode 100644 index f3cf88f18a..0000000000 --- a/litebox_broker_core/src/object.rs +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use crate::event::EventObject; -use crate::identity::{BrokerAssociation, ProcessId}; -use crate::{BrokerCore, BrokerError, Result, allocate_id}; -use litebox_broker_protocol::ObjectHandle; - -bitflags::bitflags! { - /// Broker rights attached to an object reference. - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] - pub struct ObjectRights: u32 { - /// Right to wait for readiness. - const WAIT = 1 << 0; - /// Right to mutate object state, such as adding event readiness credits. - const WRITE = 1 << 1; - } -} - -slotmap::new_key_type! { - /// Broker-owned object identifier. - pub(crate) struct ObjectId; -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct ObjectReference { - pub(crate) object_id: ObjectId, - pub(crate) owner: ProcessId, - pub(crate) rights: ObjectRights, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum ObjectEntry { - Event(EventObject), -} - -impl BrokerCore { - /// Inserts a broker object and mints its first owned reference. - pub(crate) fn insert_object_with_reference( - &mut self, - association: &BrokerAssociation, - object: ObjectEntry, - rights: ObjectRights, - ) -> Result { - if self.objects.len() >= self.limits.max_objects - || self.references.len() >= self.limits.max_references - { - return Err(BrokerError::ResourceExhausted); - } - - let handle = ObjectHandle(allocate_id(&mut self.next_reference_handle)?); - let object_id = self.objects.insert(object); - let old_reference = self.references.insert( - handle, - ObjectReference { - object_id, - owner: association.process_id(), - rights, - }, - ); - debug_assert!(old_reference.is_none()); - - Ok(handle) - } - - pub(crate) fn authorize_create_event( - &self, - association: &BrokerAssociation, - ) -> Result { - self.policy - .authorize_create_event(association.caller_credential()) - } - - pub(crate) fn authorize_use_event( - &self, - association: &BrokerAssociation, - handle: ObjectHandle, - rights: ObjectRights, - ) -> Result { - let reference = self.validate_handle(association, handle, rights)?; - self.policy - .authorize_use_event(association.caller_credential(), rights)?; - Ok(reference) - } - - pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { - self.objects - .get(object_id) - .ok_or(BrokerError::UnknownObject) - } - - pub(crate) fn object_mut(&mut self, object_id: ObjectId) -> Result<&mut ObjectEntry> { - self.objects - .get_mut(object_id) - .ok_or(BrokerError::UnknownObject) - } - - fn validate_handle( - &self, - association: &BrokerAssociation, - handle: ObjectHandle, - required_rights: ObjectRights, - ) -> Result { - let reference = self.reference_for_handle(association, handle)?; - if !reference.rights.contains(required_rights) { - return Err(BrokerError::InvalidRights); - } - - self.objects - .get(reference.object_id) - .ok_or(BrokerError::UnknownObject)?; - - Ok(*reference) - } -} - -impl BrokerCore { - /// Closes one object reference owned by an association. - /// - /// The underlying object is released when this was the last live reference. - pub fn close_object_reference( - &mut self, - association: &BrokerAssociation, - handle: ObjectHandle, - ) -> Result<()> { - let object_id = self.reference_for_handle(association, handle)?.object_id; - if !self.objects.contains_key(object_id) { - return Err(BrokerError::UnknownObject); - } - - self.references.remove(&handle); - self.drop_object_if_unreferenced(object_id); - Ok(()) - } - - /// Closes a broker association and releases references owned by it. - pub fn close_association(&mut self, association: BrokerAssociation) { - let process_id = association.process_id(); - self.references - .retain(|_, reference| reference.owner != process_id); - let references = &self.references; - self.objects.retain(|object_id, _| { - references - .values() - .any(|reference| reference.object_id == object_id) - }); - } - - fn reference_for_handle( - &self, - association: &BrokerAssociation, - handle: ObjectHandle, - ) -> Result<&ObjectReference> { - let reference = self - .references - .get(&handle) - .ok_or(BrokerError::UnknownObject)?; - if reference.owner != association.process_id() { - return Err(BrokerError::UnknownObject); - } - Ok(reference) - } - - fn drop_object_if_unreferenced(&mut self, object_id: ObjectId) { - if !self - .references - .values() - .any(|reference| reference.object_id == object_id) - { - self.objects.remove(object_id); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine, allocate_id}; - use litebox_broker_protocol::{ObjectHandle, WaitOutcome}; - - #[test] - fn allocator_exhausts_before_id_overflow() { - let mut next_id = u64::MAX; - - assert_eq!( - allocate_id(&mut next_id), - Err(BrokerError::ResourceExhausted) - ); - assert_eq!( - allocate_id(&mut next_id), - Err(BrokerError::ResourceExhausted) - ); - } - - #[test] - fn oversized_object_slotmap_limits_are_rejected_before_core_construction() { - let too_many_entries = u32::MAX as usize; - - assert!(matches!( - BrokerCore::new_with_limits( - PolicyEngine::event_only(), - BrokerCoreLimits::new(too_many_entries, 1) - ), - Err(BrokerError::ResourceExhausted) - )); - } - - #[test] - fn object_reference_lifecycle_uses_public_core_constructor_once() { - let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); - let owner = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let other = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let handle = core.create_event(&owner).unwrap(); - let unknown_handle = ObjectHandle(handle.0 + 1); - - assert_ne!(unknown_handle, handle); - assert_eq!( - core.wait_event(&owner, unknown_handle), - Err(BrokerError::UnknownObject) - ); - - assert_eq!( - core.close_object_reference(&other, handle), - Err(BrokerError::UnknownObject) - ); - - assert!(matches!( - core.wait_event(&owner, handle), - Ok(WaitOutcome::WouldBlock(_)) - )); - - assert_eq!(core.close_object_reference(&owner, handle), Ok(())); - assert!(core.references.is_empty()); - assert!(core.objects.is_empty()); - assert_eq!( - core.close_object_reference(&owner, handle), - Err(BrokerError::UnknownObject) - ); - - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let _handle = core.create_event(&association).unwrap(); - assert_eq!(core.references.len(), 1); - assert_eq!(core.objects.len(), 1); - - core.close_association(association); - - assert!(core.references.is_empty()); - assert!(core.objects.is_empty()); - - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - core.next_reference_handle = u64::MAX; - assert_eq!( - core.create_event(&association), - Err(BrokerError::ResourceExhausted) - ); - assert!(core.references.is_empty()); - assert!(core.objects.is_empty()); - } -} diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index 0fca65108a..c736e96276 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use crate::session::ObjectKind; use crate::{BrokerError, CallerCredential, ObjectRights}; /// Configured broker policy. @@ -9,15 +10,36 @@ use crate::{BrokerError, CallerCredential, ObjectRights}; pub enum PolicyProfile { /// Deny every operation. DefaultDeny, - /// Allow the current event-object surface. - EventOnly { - /// Rights to attach to newly created event references. - event_reference_rights: ObjectRights, - /// Maximum event rights this policy may authorize for use requests. - event_use_rights: ObjectRights, + /// Static rights for known broker principals. + Static { + /// Rights for the unauthenticated principal used by the initial POC. + unauthenticated: PrincipalRights, }, } +/// Rights granted to one broker principal. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct PrincipalRights { + /// Rights for event objects. + pub event: ObjectRights, +} + +impl PrincipalRights { + /// Grants all currently supported object rights. + pub const fn all() -> Self { + Self { + event: ObjectRights::WAIT.union(ObjectRights::WRITE), + } + } + + fn object_rights(self, object_kind: ObjectKind) -> ObjectRights { + match object_kind { + ObjectKind::Event => self.event, + } + } +} + /// Broker policy decision and audit component. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PolicyEngine { @@ -35,57 +57,27 @@ impl PolicyEngine { Self::new(PolicyProfile::DefaultDeny) } - /// Creates a policy engine that allows only the current event-object surface. - pub const fn event_only() -> Self { - Self::event_only_with_reference_rights(DEFAULT_EVENT_RIGHTS) + /// Creates a policy engine with rights for the unauthenticated principal. + pub const fn with_unauthenticated_rights(unauthenticated: PrincipalRights) -> Self { + Self::new(PolicyProfile::Static { unauthenticated }) } - /// Creates an event-only policy engine with explicit initial reference rights. - /// - /// Use authorization still allows the normal event-only rights; BrokerCore's - /// reference validation enforces the rights on each created reference. - pub const fn event_only_with_reference_rights(event_reference_rights: ObjectRights) -> Self { - Self::new(PolicyProfile::EventOnly { - event_reference_rights, - event_use_rights: DEFAULT_EVENT_RIGHTS, - }) - } - - pub(crate) fn authorize_create_event( + pub(crate) fn principal_object_rights( &self, caller_credential: CallerCredential, + object_kind: ObjectKind, ) -> Result { - match self.profile { - PolicyProfile::EventOnly { - event_reference_rights, - .. - } if caller_credential == CallerCredential::Unauthenticated => { - Ok(event_reference_rights) - } - PolicyProfile::DefaultDeny | PolicyProfile::EventOnly { .. } => { - Err(BrokerError::PolicyDenied) - } - } - } - - pub(crate) fn authorize_use_event( - &self, - caller_credential: CallerCredential, - rights: ObjectRights, - ) -> Result<(), BrokerError> { - match self.profile { - PolicyProfile::EventOnly { - event_use_rights, .. - } if caller_credential == CallerCredential::Unauthenticated - && !rights.is_empty() - && event_use_rights.contains(rights) => - { - Ok(()) - } - PolicyProfile::DefaultDeny | PolicyProfile::EventOnly { .. } => { - Err(BrokerError::PolicyDenied) + let principal_rights = match (self.profile, caller_credential) { + (PolicyProfile::Static { unauthenticated }, CallerCredential::Unauthenticated) => { + unauthenticated } + (PolicyProfile::DefaultDeny, _) => return Err(BrokerError::PolicyDenied), + }; + let rights = principal_rights.object_rights(object_kind); + if rights.is_empty() { + return Err(BrokerError::PolicyDenied); } + Ok(rights) } } @@ -95,57 +87,41 @@ impl Default for PolicyEngine { } } -/// Policy profile that allows only the current event-object surface. -/// -/// The default event create operation grants `WAIT | WRITE` on the initial -/// reference. Use requests may ask for any non-empty subset of configured event -/// use rights; BrokerCore separately enforces each reference's actual rights. -const DEFAULT_EVENT_RIGHTS: ObjectRights = ObjectRights::WAIT.union(ObjectRights::WRITE); - #[cfg(test)] mod tests { use super::*; #[test] - fn event_only_policy_allows_only_current_event_surface() { - let policy = PolicyEngine::event_only(); + fn static_policy_allows_configured_principal_rights() { + let policy = PolicyEngine::with_unauthenticated_rights(PrincipalRights::all()); assert_eq!( - policy.authorize_create_event(CallerCredential::Unauthenticated), + policy.principal_object_rights(CallerCredential::Unauthenticated, ObjectKind::Event), Ok(ObjectRights::WAIT | ObjectRights::WRITE) ); - assert_eq!( - policy.authorize_use_event(CallerCredential::Unauthenticated, ObjectRights::WAIT), - Ok(()) - ); - assert_eq!( - policy.authorize_use_event(CallerCredential::Unauthenticated, ObjectRights::WRITE), - Ok(()) - ); - assert_eq!( - policy.authorize_use_event( - CallerCredential::Unauthenticated, - ObjectRights::WAIT | ObjectRights::WRITE - ), - Ok(()) - ); - assert_eq!( - policy.authorize_use_event(CallerCredential::Unauthenticated, ObjectRights::empty()), - Err(BrokerError::PolicyDenied) - ); } #[test] - fn explicit_event_reference_rights_do_not_narrow_event_use_policy() { - let policy = PolicyEngine::event_only_with_reference_rights(ObjectRights::WAIT); + fn static_policy_returns_configured_principal_rights() { + let policy = PolicyEngine::with_unauthenticated_rights(PrincipalRights { + event: ObjectRights::WAIT, + }); assert_eq!( - policy.authorize_create_event(CallerCredential::Unauthenticated), + policy.principal_object_rights(CallerCredential::Unauthenticated, ObjectKind::Event), Ok(ObjectRights::WAIT) ); + } + + #[test] + fn empty_principal_rights_deny_object_authorization() { + let policy = PolicyEngine::with_unauthenticated_rights(PrincipalRights { + event: ObjectRights::empty(), + }); + assert_eq!( - policy.authorize_use_event(CallerCredential::Unauthenticated, ObjectRights::WRITE), - Ok(()) + policy.principal_object_rights(CallerCredential::Unauthenticated, ObjectKind::Event), + Err(BrokerError::PolicyDenied) ); } } diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs new file mode 100644 index 0000000000..7d91cf8a0a --- /dev/null +++ b/litebox_broker_core/src/session.rs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::sync::Arc; + +use crate::event::EventObject; +use crate::{BrokerCore, BrokerError, Result}; +use hashbrown::HashMap; +use litebox_broker_protocol::ObjectHandle; +use spin::rwlock::RwLock; + +/// Caller identity information supplied by the broker entry layer. +/// +/// The first userland proof of concept does not authenticate Unix-socket peers, +/// but BrokerCore still accepts an explicit credential value so authenticated +/// servers or hosts can plumb identity through the same session-creation seam. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum CallerCredential { + /// Explicit deployment mode for the initial unauthenticated userland POC. + Unauthenticated, +} + +/// Broker-assigned session identity. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct SessionId(pub u64); + +bitflags::bitflags! { + /// Broker rights attached to an object reference. + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] + pub struct ObjectRights: u32 { + /// Right to wait for readiness. + const WAIT = 1 << 0; + /// Right to mutate object state, such as adding event readiness credits. + const WRITE = 1 << 1; + } +} + +pub(crate) struct ObjectReference { + pub(crate) object: Arc>, + pub(crate) session_id: SessionId, + pub(crate) rights: ObjectRights, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ObjectEntry { + Event(EventObject), +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ObjectKind { + Event, +} + +impl ObjectEntry { + fn kind(self) -> ObjectKind { + match self { + Self::Event(_) => ObjectKind::Event, + } + } +} + +/// Broker-owned authority token for one authenticated caller session. +/// +/// User mode does not choose this value. The broker entry layer authenticates +/// the caller, then BrokerCore assigns this identity for all operations received +/// on that session. Dropping the session releases all object references it owns. +pub struct BrokerSession { + pub(crate) core: BrokerCore, + /// Broker-assigned session identity. + pub(crate) session_id: SessionId, + /// Broker-entry-authenticated caller credential for this session. + pub(crate) caller_credential: CallerCredential, +} + +impl BrokerSession { + /// Creates an authenticated session identity. + pub(crate) fn new( + core: BrokerCore, + session_id: SessionId, + caller_credential: CallerCredential, + ) -> Self { + Self { + core, + session_id, + caller_credential, + } + } + + pub(crate) fn create_object_reference(&self, object: ObjectEntry) -> Result { + let rights = self + .core + .policy + .principal_object_rights(self.caller_credential, object.kind())?; + let mut references = self.core.references.write(); + if references.len() >= self.core.limits.max_references { + return Err(BrokerError::ResourceExhausted); + } + let handle = self.core.allocate_reference_handle()?; + references.insert( + handle, + ObjectReference { + object: Arc::new(RwLock::new(object)), + session_id: self.session_id, + rights, + }, + ); + + Ok(handle) + } + + pub(crate) fn with_authorized_object( + &self, + handle: ObjectHandle, + required_rights: ObjectRights, + f: impl FnOnce(&ObjectEntry) -> Result, + ) -> Result { + let object = { + let references = self.core.references.read(); + self.authorize_use_object(&references, handle, required_rights)? + }; + let object = object.read(); + f(&object) + } + + pub(crate) fn with_authorized_object_mut( + &self, + handle: ObjectHandle, + required_rights: ObjectRights, + f: impl FnOnce(&mut ObjectEntry) -> Result, + ) -> Result { + let object = { + let references = self.core.references.read(); + self.authorize_use_object(&references, handle, required_rights)? + }; + let mut object = object.write(); + f(&mut object) + } + + fn authorize_use_object( + &self, + references: &HashMap, + handle: ObjectHandle, + required_rights: ObjectRights, + ) -> Result>> { + let reference = references.get(&handle).ok_or(BrokerError::UnknownObject)?; + if reference.session_id != self.session_id { + return Err(BrokerError::UnknownObject); + } + if !reference.rights.contains(required_rights) { + return Err(BrokerError::InvalidRights); + } + let object = Arc::clone(&reference.object); + Ok(object) + } + + /// Closes one object reference owned by this session. + /// + /// The underlying object is released when this was the last live reference. + pub fn close_object_reference(&self, handle: ObjectHandle) -> Result<()> { + let mut references = self.core.references.write(); + let reference = references.get(&handle).ok_or(BrokerError::UnknownObject)?; + if reference.session_id != self.session_id { + return Err(BrokerError::UnknownObject); + } + references.remove(&handle); + Ok(()) + } +} + +impl Drop for BrokerSession { + fn drop(&mut self) { + self.core.close_session(self.session_id); + } +} + +#[cfg(test)] +mod tests { + use crate::{ + BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine, PrincipalRights, + event, + }; + use litebox_broker_protocol::{ + EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, + }; + + #[test] + fn object_reference_lifecycle_uses_public_core_constructor_once() { + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_unauthenticated_rights(PrincipalRights::all()), + BrokerCoreLimits::new(1), + ) + .unwrap(); + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let other = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let handle = event::create(&session, 0).unwrap(); + let unknown_handle = ObjectHandle(handle.0 + 1); + + assert_ne!(unknown_handle, handle); + assert_eq!( + event::wait(&session, unknown_handle), + Err(BrokerError::UnknownObject) + ); + + assert_eq!( + other.close_object_reference(handle), + Err(BrokerError::UnknownObject) + ); + + assert_eq!( + event::wait(&session, handle), + Ok(WaitOutcome::WouldBlock(ReadinessState::new(false, true))) + ); + assert_eq!( + event::add(&session, handle, 1), + Ok(ReadinessState::new(true, true)) + ); + assert_eq!( + event::consume(&session, handle, EventConsumeMode::One), + Ok(EventConsumption::new(1, ReadinessState::new(false, true))) + ); + assert_eq!( + event::create(&session, 0), + Err(BrokerError::ResourceExhausted) + ); + + assert_eq!(session.close_object_reference(handle), Ok(())); + { + let references = broker.references.read(); + assert!(references.is_empty()); + } + assert_eq!( + session.close_object_reference(handle), + Err(BrokerError::UnknownObject) + ); + + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let _handle = event::create(&session, 0).unwrap(); + { + let references = broker.references.read(); + assert_eq!(references.len(), 1); + } + + drop(session); + + { + let references = broker.references.read(); + assert!(references.is_empty()); + } + + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + { + let mut next_reference_handle = broker.next_reference_handle.write(); + *next_reference_handle = u64::MAX; + } + assert_eq!( + event::create(&session, 0), + Err(BrokerError::ResourceExhausted) + ); + let references = broker.references.read(); + assert!(references.is_empty()); + } +} diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index e1fecb6b87..57cf662072 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -14,7 +14,7 @@ extern crate std; use core::fmt; -use litebox_broker_core::{BrokerAssociation, BrokerCore, BrokerError, CallerCredential}; +use litebox_broker_core::{BrokerCore, BrokerError, BrokerSession, CallerCredential, event}; use litebox_broker_protocol::{ AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, @@ -31,7 +31,7 @@ pub const HOST_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; /// Serves one broker connection over the provided connected control channel. pub fn serve_connection( - core: &mut BrokerCore, + core: &BrokerCore, channel: &mut T, ) -> Result where @@ -42,19 +42,16 @@ where .map_err(BrokerHostError::Channel)?; let caller_credential = caller_credential_from_peer(peer_credential) .map_err(|()| BrokerHostError::AssociationSetup)?; - let association = core - .create_association(caller_credential) + let session = core + .create_session(caller_credential) .map_err(|_error| BrokerHostError::AssociationSetup)?; - let result = serve_request_loop(core, channel, &association); - core.close_association(association); - result + serve_request_loop(channel, &session) } fn serve_request_loop( - core: &mut BrokerCore, channel: &mut T, - association: &BrokerAssociation, + session: &BrokerSession, ) -> Result where T: HostControlChannel, @@ -65,7 +62,7 @@ where break; }; - let dispatch = handle_received_request(core, association, &mut state, received); + let dispatch = handle_received_request(session, &mut state, received); channel .send_response(&dispatch.response) .map_err(BrokerHostError::Channel)?; @@ -88,22 +85,18 @@ fn caller_credential_from_peer( } fn handle_received_request( - core: &mut BrokerCore, - association: &BrokerAssociation, + session: &BrokerSession, state: &mut ConnectionState, received: ReceivedBrokerRequest, ) -> BrokerDispatch { match received { - ReceivedBrokerRequest::Request(request) => { - handle_request(core, association, state, request) - } + ReceivedBrokerRequest::Request(request) => handle_request(session, state, request), _ => handle_unknown_request(*state), } } fn handle_request( - core: &mut BrokerCore, - association: &BrokerAssociation, + session: &BrokerSession, state: &mut ConnectionState, request: BrokerRequest, ) -> BrokerDispatch { @@ -119,13 +112,12 @@ fn handle_request( }, ConnectionState::Active { negotiated_protocol_version, - } => handle_active_request(core, association, negotiated_protocol_version, request), + } => handle_active_request(session, negotiated_protocol_version, request), } } fn handle_active_request( - core: &mut BrokerCore, - association: &BrokerAssociation, + session: &BrokerSession, _negotiated_protocol_version: ProtocolVersion, request: BrokerRequest, ) -> BrokerDispatch { @@ -135,46 +127,37 @@ fn handle_active_request( CloseReason::ProtocolViolation, ), BrokerRequest::Core(request) => { - BrokerDispatch::continue_after(handle_core_request(core, association, request)) + BrokerDispatch::continue_after(handle_core_request(session, request)) } _ => BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)), } } -fn handle_core_request( - core: &mut BrokerCore, - association: &BrokerAssociation, - request: CoreRequest, -) -> BrokerResponse { +fn handle_core_request(session: &BrokerSession, request: CoreRequest) -> BrokerResponse { match request { - CoreRequest::Event(request) => handle_event_request(core, association, request), + CoreRequest::Event(request) => handle_event_request(session, request), _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), } } -fn handle_event_request( - core: &mut BrokerCore, - association: &BrokerAssociation, - request: EventRequest, -) -> BrokerResponse { +fn handle_event_request(session: &BrokerSession, request: EventRequest) -> BrokerResponse { match request { - EventRequest::Create(request) => handle_core_result( - core.create_event_with_count(association, request.initial_count), - |handle| { + EventRequest::Create(request) => { + handle_core_result(event::create(session, request.initial_count), |handle| { BrokerResponse::Core(CoreResponse::Event(EventResponse::Create( CreateEventResponse::new(handle), ))) - }, - ), + }) + } EventRequest::Wait(request) => { - handle_core_result(core.wait_event(association, request.handle), |outcome| { + handle_core_result(event::wait(session, request.handle), |outcome| { BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( WaitEventResponse::new(outcome), ))) }) } EventRequest::Add(request) => handle_core_result( - core.add_event(association, request.handle, request.value), + event::add(session, request.handle, request.value), |readiness| { BrokerResponse::Core(CoreResponse::Event(EventResponse::Add( AddEventResponse::new(readiness), @@ -182,7 +165,7 @@ fn handle_event_request( }, ), EventRequest::Consume(request) => handle_core_result( - core.consume_event(association, request.handle, request.mode), + event::consume(session, request.handle, request.mode), |consumption| { BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(consumption))) }, @@ -307,21 +290,22 @@ pub enum ConnectionTermination { #[cfg(test)] mod tests { use super::*; - use litebox_broker_core::PolicyEngine; + use litebox_broker_core::{PolicyEngine, PrincipalRights}; use litebox_broker_protocol::CreateEventRequest; #[test] fn host_request_handling_uses_one_broker_core() { - let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); - - serve_connection_negotiates_routes_one_request_and_returns_peer_closed(&mut core); - serve_connection_closes_after_protocol_violation(&mut core); - serve_connection_returns_channel_error_when_response_send_fails(&mut core); + let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( + PrincipalRights::all(), + )) + .unwrap(); + + serve_connection_negotiates_routes_one_request_and_returns_peer_closed(&broker); + serve_connection_closes_after_protocol_violation(&broker); + serve_connection_returns_channel_error_when_response_send_fails(&broker); } - fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed( - core: &mut BrokerCore, - ) { + fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request( BrokerRequest::Negotiate { @@ -335,7 +319,7 @@ mod tests { ])); assert_eq!( - serve_connection(core, &mut channel).unwrap(), + serve_connection(broker, &mut channel).unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -353,7 +337,7 @@ mod tests { assert_ne!(handle.0, 0); } - fn serve_connection_closes_after_protocol_violation(core: &mut BrokerCore) { + fn serve_connection_closes_after_protocol_violation(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request(event_create_request( 0, @@ -366,7 +350,7 @@ mod tests { ])); assert_eq!( - serve_connection(core, &mut channel).unwrap(), + serve_connection(broker, &mut channel).unwrap(), ConnectionTermination::BrokerClosed(CloseReason::ProtocolViolation) ); assert_eq!( @@ -376,7 +360,7 @@ mod tests { assert_eq!(channel.requests.len(), 1); } - fn serve_connection_returns_channel_error_when_response_send_fails(core: &mut BrokerCore) { + fn serve_connection_returns_channel_error_when_response_send_fails(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([Ok(Some( ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { protocol_version: HOST_PROTOCOL_VERSION, @@ -384,7 +368,7 @@ mod tests { ))])); channel.send_error = true; - match serve_connection(core, &mut channel) { + match serve_connection(broker, &mut channel) { Err(BrokerHostError::Channel(())) => {} result => panic!("unexpected serve result: {result:?}"), } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 2c345efcad..1f30861447 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -227,7 +227,7 @@ mod tests { let handle = ObjectHandle(7); let request = CoreRequest::Event(EventRequest::Wait(WaitEventRequest::new(handle))); let response = CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)), + WaitOutcome::WouldBlock(ReadinessState::new(false, true)), ))); let channel = FakeControlChannel::new(Some(BrokerResponse::Core(response.clone()))); let mut local = BrokerLocal::new(channel); diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs index efbaf71377..3dbb53c76a 100644 --- a/litebox_broker_protocol/src/event.rs +++ b/litebox_broker_protocol/src/event.rs @@ -10,17 +10,14 @@ pub struct ReadinessState { pub read_ready: bool, /// Whether an event write/add operation can complete without blocking. pub write_ready: bool, - /// Monotonic readiness generation used to invalidate user-side readiness caches. - pub generation: u64, } impl ReadinessState { /// Creates a readiness state. - pub const fn new(read_ready: bool, write_ready: bool, generation: u64) -> Self { + pub const fn new(read_ready: bool, write_ready: bool) -> Self { Self { read_ready, write_ready, - generation, } } } diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 1d5b278f9d..66918bd2f6 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -195,17 +195,17 @@ mod tests { }, event_response(EventResponse::Create(CreateEventResponse::new(handle))), event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::Ready(ReadinessState::new(true, false, 8)), + WaitOutcome::Ready(ReadinessState::new(true, false)), ))), event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, true, 9)), + WaitOutcome::WouldBlock(ReadinessState::new(false, true)), ))), event_response(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(true, true, 10), + ReadinessState::new(true, true), ))), event_response(EventResponse::Consume(ConsumeEventResponse::new( 3, - ReadinessState::new(false, true, 11), + ReadinessState::new(false, true), ))), BrokerResponse::Error(ErrorCode::PolicyDenied), BrokerResponse::Error(ErrorCode::WouldBlock), @@ -259,7 +259,7 @@ mod tests { ))) ); - let mut invalid_bool = [1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let mut invalid_bool = [1, 0, 2, 2, 0]; assert_eq!( decode_response(&invalid_bool), Err(WireError::InvalidBoolean) @@ -267,7 +267,6 @@ mod tests { invalid_bool[3] = 1; invalid_bool[4] = 1; - invalid_bool[12] = 1; let mut frame = invalid_bool.to_vec(); frame.push(0xff); assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); @@ -277,9 +276,9 @@ mod tests { fn event_add_response_wire_shape_is_pinned() { assert_eq!( encode_response(event_response(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(true, false, 0x0102_0304_0506_0708) + ReadinessState::new(true, false) )))), - [1, 0, 2, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1] + [1, 0, 2, 1, 0] ); } diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index eaf4cfa018..94285a432b 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -146,15 +146,10 @@ fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result, fn encode_readiness(encoder: &mut Encoder, readiness: ReadinessState) { encoder.bool(readiness.read_ready); encoder.bool(readiness.write_ready); - encoder.u64(readiness.generation); } fn decode_readiness(decoder: &mut Decoder<'_>) -> Result { - Ok(ReadinessState::new( - decoder.bool()?, - decoder.bool()?, - decoder.u64()?, - )) + Ok(ReadinessState::new(decoder.bool()?, decoder.bool()?)) } fn encode_consume_mode(encoder: &mut Encoder, mode: EventConsumeMode) { diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index d3c91f578a..df9b03d22d 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -7,7 +7,7 @@ use std::path::PathBuf; use std::time::{Duration, Instant}; use clap::Parser; -use litebox_broker_core::{BrokerCore, PolicyEngine}; +use litebox_broker_core::{BrokerCore, PolicyEngine, PrincipalRights}; use litebox_broker_host::serve_connection; use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; @@ -26,7 +26,9 @@ fn main() -> Result<(), Box> { let (stream, _) = listener.accept()?; let mut channel = UnixStreamHostControlChannel::from_accepted(stream); channel.set_io_deadline(Some(Instant::now() + SESSION_TIMEOUT))?; - let mut broker = BrokerCore::new(PolicyEngine::event_only())?; - serve_connection(&mut broker, &mut channel)?; + let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( + PrincipalRights::all(), + ))?; + serve_connection(&broker, &mut channel)?; Ok(()) } diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 1d18bdb427..3bd0a0188d 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -29,17 +29,17 @@ fn separate_process_broker_serves_event_object_requests() { let handle = local.create_event().unwrap(); assert_eq!( local.wait_event(handle).unwrap(), - WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) + WaitOutcome::WouldBlock(ReadinessState::new(false, true)) ); assert_eq!( local.add_event(handle, 1).unwrap(), - ReadinessState::new(true, true, 1) + ReadinessState::new(true, true) ); assert_eq!( local.wait_event(handle).unwrap(), - WaitOutcome::Ready(ReadinessState::new(true, true, 1)) + WaitOutcome::Ready(ReadinessState::new(true, true)) ); drop(local); assert!(child.wait().unwrap().success()); diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 912a523f02..0a90564f56 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -312,7 +312,7 @@ fn spawn_test_broker( let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let listener = std::os::unix::net::UnixListener::bind(&server_socket_path) .expect("failed to bind broker test socket"); - let mut core = + let broker = litebox_broker_core::BrokerCore::new(policy).expect("failed to create broker core"); ready_tx.send(()).expect("failed to report broker ready"); @@ -329,7 +329,7 @@ fn spawn_test_broker( let mut channel = CountingHostControlChannel::new( litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream), ); - let termination = litebox_broker_host::serve_connection(&mut core, &mut channel) + let termination = litebox_broker_host::serve_connection(&broker, &mut channel) .expect("broker host failed"); assert_eq!( termination, @@ -422,7 +422,9 @@ fn test_runner_broker_integration_with_rewriter() { let socket_path = unique_test_socket_path("runner-broker"); let broker_thread = spawn_test_broker( &socket_path, - litebox_broker_core::PolicyEngine::event_only(), + litebox_broker_core::PolicyEngine::with_unauthenticated_rights( + litebox_broker_core::PrincipalRights::all(), + ), 2, ); From 7b5c720ae5c61ea091b708f927d3ed6a48d848bd Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 22 Jun 2026 16:04:34 -0700 Subject: [PATCH 047/319] Simplify broker protocol version handling (#944) This PR simplifies the protocol version handling and some request/response code. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 15 +- litebox_broker_host/src/lib.rs | 61 ++--- litebox_broker_local/src/error.rs | 9 - litebox_broker_local/src/event.rs | 23 +- litebox_broker_local/src/lib.rs | 229 ++++++++---------- litebox_broker_protocol/src/lib.rs | 27 +-- litebox_broker_protocol/src/message.rs | 3 +- litebox_broker_protocol/src/wire.rs | 6 +- litebox_broker_protocol/src/wire/primitive.rs | 5 +- .../tests/userland_broker.rs | 16 +- litebox_runner_linux_userland/src/broker.rs | 7 +- 11 files changed, 167 insertions(+), 234 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 5e837e1476..64b5eb03b9 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -2,7 +2,9 @@ // Licensed under the MIT license. use litebox_broker_local::{BrokerLocal, BrokerLocalError}; -use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, LocalControlChannel, +}; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; @@ -46,13 +48,18 @@ where &self, request: CoreRequest, ) -> core::result::Result { - self.local + let response = self + .local .lock() - .active_core_request(request) + .request(BrokerRequest::Core(request)) .map_err(|error| match error { BrokerLocalError::Broker(error) => BrokerControlError::Broker(error), BrokerLocalError::UnexpectedResponse(_) => BrokerControlError::UnexpectedResponse, _ => BrokerControlError::Transport, - }) + })?; + match response { + BrokerResponse::Core(response) => Ok(response), + _ => Err(BrokerControlError::UnexpectedResponse), + } } } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 57cf662072..00c4550b60 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -16,19 +16,15 @@ use core::fmt; use litebox_broker_core::{BrokerCore, BrokerError, BrokerSession, CallerCredential, event}; use litebox_broker_protocol::{ - AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, - CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, - INITIAL_PROTOCOL_VERSION, PeerCredential, ProtocolVersion, ReceivedBrokerRequest, - WaitEventResponse, + AddEventResponse, BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, CoreRequest, + CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, + PeerCredential, ReceivedBrokerRequest, WaitEventResponse, }; mod error; pub use error::{BrokerHostError, Result}; -/// Protocol version this broker host implementation supports. -pub const HOST_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; - /// Serves one broker connection over the provided connected control channel. pub fn serve_connection( core: &BrokerCore, @@ -103,24 +99,27 @@ fn handle_request( match *state { ConnectionState::AwaitingNegotiation => match request { BrokerRequest::Negotiate { protocol_version } => { - negotiate_version(state, protocol_version) + if protocol_version == BROKER_PROTOCOL_VERSION { + *state = ConnectionState::Active; + BrokerDispatch::continue_after(BrokerResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }) + } else { + BrokerDispatch::continue_after(BrokerResponse::VersionMismatch { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }) + } } _ => BrokerDispatch::close_after( BrokerResponse::Error(ErrorCode::ProtocolState), CloseReason::ProtocolViolation, ), }, - ConnectionState::Active { - negotiated_protocol_version, - } => handle_active_request(session, negotiated_protocol_version, request), + ConnectionState::Active => handle_active_request(session, request), } } -fn handle_active_request( - session: &BrokerSession, - _negotiated_protocol_version: ProtocolVersion, - request: BrokerRequest, -) -> BrokerDispatch { +fn handle_active_request(session: &BrokerSession, request: BrokerRequest) -> BrokerDispatch { match request { BrokerRequest::Negotiate { .. } => BrokerDispatch::close_after( BrokerResponse::Error(ErrorCode::ProtocolState), @@ -185,24 +184,6 @@ fn handle_unknown_request(state: ConnectionState) -> BrokerDispatch { } } -fn negotiate_version( - state: &mut ConnectionState, - protocol_version: ProtocolVersion, -) -> BrokerDispatch { - if protocol_version.is_supported_by(HOST_PROTOCOL_VERSION) { - *state = ConnectionState::Active { - negotiated_protocol_version: protocol_version, - }; - BrokerDispatch::continue_after(BrokerResponse::Negotiated { - broker_protocol_version: HOST_PROTOCOL_VERSION, - }) - } else { - BrokerDispatch::continue_after(BrokerResponse::VersionMismatch { - broker_protocol_version: HOST_PROTOCOL_VERSION, - }) - } -} - fn handle_core_result( result: litebox_broker_core::Result, into_response: impl FnOnce(T) -> BrokerResponse, @@ -228,9 +209,7 @@ fn to_protocol_error(error: BrokerError) -> ErrorCode { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ConnectionState { AwaitingNegotiation, - Active { - negotiated_protocol_version: ProtocolVersion, - }, + Active, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -309,7 +288,7 @@ mod tests { let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request( BrokerRequest::Negotiate { - protocol_version: HOST_PROTOCOL_VERSION, + protocol_version: BROKER_PROTOCOL_VERSION, }, ))), Ok(Some(ReceivedBrokerRequest::Request(event_create_request( @@ -325,7 +304,7 @@ mod tests { assert_eq!( channel.responses[0], BrokerResponse::Negotiated { - broker_protocol_version: HOST_PROTOCOL_VERSION + broker_protocol_version: BROKER_PROTOCOL_VERSION } ); let handle = match &channel.responses[1] { @@ -344,7 +323,7 @@ mod tests { )))), Ok(Some(ReceivedBrokerRequest::Request( BrokerRequest::Negotiate { - protocol_version: HOST_PROTOCOL_VERSION, + protocol_version: BROKER_PROTOCOL_VERSION, }, ))), ])); @@ -363,7 +342,7 @@ mod tests { fn serve_connection_returns_channel_error_when_response_send_fails(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([Ok(Some( ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { - protocol_version: HOST_PROTOCOL_VERSION, + protocol_version: BROKER_PROTOCOL_VERSION, }), ))])); channel.send_error = true; diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index d79aa4a6b5..40b333fe66 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -36,15 +36,6 @@ pub enum BrokerLocalError { /// Protocol version supported by this local implementation. local_protocol_version: ProtocolVersion, }, - #[error( - "broker session protocol version {negotiated_protocol_version:?} does not support required version {required:?}" - )] - UnsupportedNegotiatedVersion { - /// Protocol version required by the operation. - required: ProtocolVersion, - /// Effective protocol version negotiated for this connection. - negotiated_protocol_version: ProtocolVersion, - }, #[error( "broker does not support requested protocol version {requested:?}; broker supports {broker_protocol_version:?}" )] diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 7c07501c5d..6552075964 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -4,14 +4,11 @@ use litebox_broker_protocol::{ AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, - INITIAL_PROTOCOL_VERSION, LocalControlChannel, ObjectHandle, ProtocolVersion, ReadinessState, - WaitEventRequest, WaitOutcome, + LocalControlChannel, ObjectHandle, ReadinessState, WaitEventRequest, WaitOutcome, }; use crate::{BrokerLocal, BrokerLocalError, Result}; -const EVENT_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; - impl BrokerLocal { /// Creates a broker-owned event object. pub fn create_event(&mut self) -> Result { @@ -23,7 +20,6 @@ impl BrokerLocal { &mut self, initial_count: u64, ) -> Result { - self.ensure_event_protocol()?; match self.request(event_request(EventRequest::Create( CreateEventRequest::new(initial_count), )))? { @@ -36,7 +32,6 @@ impl BrokerLocal { /// Checks whether an event wait would complete now. pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { - self.ensure_event_protocol()?; match self.request(event_request(EventRequest::Wait(WaitEventRequest::new( handle, ))))? { @@ -53,7 +48,6 @@ impl BrokerLocal { handle: ObjectHandle, value: u64, ) -> Result { - self.ensure_event_protocol()?; match self.request(event_request(EventRequest::Add(AddEventRequest::new( handle, value, ))))? { @@ -70,7 +64,6 @@ impl BrokerLocal { handle: ObjectHandle, mode: EventConsumeMode, ) -> Result { - self.ensure_event_protocol()?; match self.request(event_request(EventRequest::Consume( ConsumeEventRequest::new(handle, mode), )))? { @@ -85,17 +78,3 @@ impl BrokerLocal { const fn event_request(request: EventRequest) -> BrokerRequest { BrokerRequest::Core(CoreRequest::Event(request)) } - -impl BrokerLocal { - fn ensure_event_protocol(&self) -> Result<(), T::Error> { - let negotiated = self.ensure_negotiated()?; - if EVENT_PROTOCOL_VERSION.is_supported_by(negotiated) { - Ok(()) - } else { - Err(BrokerLocalError::UnsupportedNegotiatedVersion { - required: EVENT_PROTOCOL_VERSION, - negotiated_protocol_version: negotiated, - }) - } - } -} diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 1f30861447..fa817f09dc 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -16,15 +16,12 @@ mod error; mod event; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, INITIAL_PROTOCOL_VERSION, - LocalControlChannel, ProtocolVersion, ReceivedBrokerResponse, + BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, LocalControlChannel, + ReceivedBrokerResponse, }; pub use error::{BrokerLocalError, Result}; -/// Protocol version this broker-local implementation requests by default. -pub const LOCAL_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; - /// Typed broker-local control adapter for broker operations. pub struct BrokerLocal { channel: T, @@ -34,9 +31,7 @@ pub struct BrokerLocal { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ConnectionState { AwaitingNegotiation, - Active { - negotiated_protocol_version: ProtocolVersion, - }, + Active, } impl BrokerLocal { @@ -55,84 +50,54 @@ impl BrokerLocal { } impl BrokerLocal { - /// Negotiates the default broker-local protocol version. - /// - /// Returns the effective protocol version this connection will speak. - pub fn negotiate(&mut self) -> Result { - self.negotiate_version(LOCAL_PROTOCOL_VERSION) - } - - /// Negotiates a caller-selected protocol version. + /// Sends one broker request. /// - /// Returns the effective protocol version this connection will speak. Feature - /// gating must use this effective version, not the broker's max-supported - /// version returned by the wire negotiation response. - pub fn negotiate_version( - &mut self, - protocol_version: ProtocolVersion, - ) -> Result { - if self.state != ConnectionState::AwaitingNegotiation { - return Err(BrokerLocalError::AlreadyNegotiated); - } - if !protocol_version.is_supported_by(LOCAL_PROTOCOL_VERSION) { - return Err(BrokerLocalError::UnsupportedLocalVersion { - requested: protocol_version, - local_protocol_version: LOCAL_PROTOCOL_VERSION, - }); - } - - let response = self.request(BrokerRequest::Negotiate { protocol_version })?; - match response { - BrokerResponse::Negotiated { - broker_protocol_version, - } => { - if !protocol_version.is_supported_by(broker_protocol_version) { - return Err(BrokerLocalError::IncompatibleNegotiation { - requested: protocol_version, - broker_protocol_version, - }); - } - self.state = ConnectionState::Active { - negotiated_protocol_version: protocol_version, - }; - Ok(protocol_version) - } - BrokerResponse::VersionMismatch { - broker_protocol_version, - } => Err(BrokerLocalError::UnsupportedVersion { - requested: protocol_version, - broker_protocol_version, - }), - response => Err(BrokerLocalError::UnexpectedResponse(response)), - } - } - - /// Returns the effective protocol version this connection negotiated. - /// - /// Feature gating must use this effective version because the broker may - /// support a newer minor version than this local adapter requested. - pub fn negotiated_protocol_version(&self) -> Option { + /// Negotiation is the only request allowed before the connection is active. + pub fn request(&mut self, request: BrokerRequest) -> Result { match self.state { - ConnectionState::AwaitingNegotiation => None, - ConnectionState::Active { - negotiated_protocol_version, - } => Some(negotiated_protocol_version), - } - } - - pub(crate) fn ensure_negotiated(&self) -> Result { - match self.state { - ConnectionState::AwaitingNegotiation => Err(BrokerLocalError::NotNegotiated), - ConnectionState::Active { - negotiated_protocol_version, - } => Ok(negotiated_protocol_version), - } - } - - pub(crate) fn request(&mut self, request: BrokerRequest) -> Result { - match self.raw_request(request)? { - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response => Ok(response), + ConnectionState::AwaitingNegotiation => match request { + BrokerRequest::Negotiate { protocol_version } => { + if protocol_version != BROKER_PROTOCOL_VERSION { + return Err(BrokerLocalError::UnsupportedLocalVersion { + requested: protocol_version, + local_protocol_version: BROKER_PROTOCOL_VERSION, + }); + } + + match self.raw_request(BrokerRequest::Negotiate { protocol_version })? { + BrokerResponse::Negotiated { + broker_protocol_version, + } => { + if protocol_version != broker_protocol_version { + return Err(BrokerLocalError::IncompatibleNegotiation { + requested: protocol_version, + broker_protocol_version, + }); + } + self.state = ConnectionState::Active; + Ok(BrokerResponse::Negotiated { + broker_protocol_version, + }) + } + BrokerResponse::VersionMismatch { + broker_protocol_version, + } => Err(BrokerLocalError::UnsupportedVersion { + requested: protocol_version, + broker_protocol_version, + }), + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + _ => Err(BrokerLocalError::NotNegotiated), + }, + ConnectionState::Active => match request { + BrokerRequest::Negotiate { .. } => Err(BrokerLocalError::AlreadyNegotiated), + request => match self.raw_request(request)? { + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response => Ok(response), + }, + }, } } @@ -150,15 +115,6 @@ impl BrokerLocal { _ => Err(BrokerLocalError::UnknownResponse), } } - - /// Sends one BrokerCore request on an active connection. - pub fn active_core_request(&mut self, request: CoreRequest) -> Result { - self.ensure_negotiated()?; - match self.request(BrokerRequest::Core(request))? { - BrokerResponse::Core(response) => Ok(response), - response => Err(BrokerLocalError::UnexpectedResponse(response)), - } - } } #[cfg(test)] @@ -180,71 +136,92 @@ mod tests { } #[test] - fn negotiate_sends_default_version_and_activates_local_connection() { - let requested = LOCAL_PROTOCOL_VERSION; + fn negotiation_request_activates_local_connection() { + let requested = BROKER_PROTOCOL_VERSION; let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { - broker_protocol_version: LOCAL_PROTOCOL_VERSION, + broker_protocol_version: BROKER_PROTOCOL_VERSION, })); let mut local = BrokerLocal::new(channel); - assert_eq!(local.negotiate().unwrap(), requested); + assert_eq!( + local + .request(BrokerRequest::Negotiate { + protocol_version: requested + }) + .unwrap(), + BrokerResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION + } + ); assert_eq!( local.channel.sent_request, Some(BrokerRequest::Negotiate { protocol_version: requested }) ); - assert_eq!(local.negotiated_protocol_version(), Some(requested)); + assert_eq!(local.state, ConnectionState::Active); } #[test] - fn negotiate_version_rejects_locally_unsupported_version_without_sending() { - let too_new = ProtocolVersion::new( - LOCAL_PROTOCOL_VERSION.major, - LOCAL_PROTOCOL_VERSION.minor + 1, - ); + fn negotiation_request_rejects_locally_unsupported_version_without_sending() { + let too_new = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); let channel = FakeControlChannel::new(None); let mut local = BrokerLocal::new(channel); assert!(matches!( - local.negotiate_version(too_new), + local.request(BrokerRequest::Negotiate { + protocol_version: too_new + }), Err(BrokerLocalError::UnsupportedLocalVersion { requested, local_protocol_version - }) if requested == too_new && local_protocol_version == LOCAL_PROTOCOL_VERSION + }) if requested == too_new && local_protocol_version == BROKER_PROTOCOL_VERSION )); - assert_eq!(local.negotiated_protocol_version(), None); + assert_eq!(local.state, ConnectionState::AwaitingNegotiation); assert_eq!(local.channel.sent_request, None); } #[test] - fn active_core_request_wraps_request_and_unwraps_response() { - use litebox_broker_protocol::{ - CoreRequest, CoreResponse, EventRequest, EventResponse, ObjectHandle, ReadinessState, - WaitEventRequest, WaitEventResponse, WaitOutcome, - }; - - let handle = ObjectHandle(7); - let request = CoreRequest::Event(EventRequest::Wait(WaitEventRequest::new(handle))); - let response = CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, true)), - ))); - let channel = FakeControlChannel::new(Some(BrokerResponse::Core(response.clone()))); + fn negotiation_request_rejects_broker_different_version_response() { + let broker_protocol_version = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); + let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { + broker_protocol_version, + })); let mut local = BrokerLocal::new(channel); - local.state = ConnectionState::Active { - negotiated_protocol_version: LOCAL_PROTOCOL_VERSION, - }; - assert_eq!( - local.active_core_request(request.clone()).unwrap(), - response - ); + assert!(matches!( + local.request(BrokerRequest::Negotiate { + protocol_version: BROKER_PROTOCOL_VERSION + }), + Err(BrokerLocalError::IncompatibleNegotiation { + requested, + broker_protocol_version: broker + }) if requested == BROKER_PROTOCOL_VERSION && broker == broker_protocol_version + )); + assert_eq!(local.state, ConnectionState::AwaitingNegotiation); assert_eq!( local.channel.sent_request, - Some(BrokerRequest::Core(request)) + Some(BrokerRequest::Negotiate { + protocol_version: BROKER_PROTOCOL_VERSION + }) ); } + #[test] + fn active_connection_rejects_negotiation_without_sending() { + let channel = FakeControlChannel::new(None); + let mut local = BrokerLocal::new(channel); + local.state = ConnectionState::Active; + + assert!(matches!( + local.request(BrokerRequest::Negotiate { + protocol_version: BROKER_PROTOCOL_VERSION + }), + Err(BrokerLocalError::AlreadyNegotiated) + )); + assert_eq!(local.channel.sent_request, None); + } + struct FakeControlChannel { sent_request: Option, response: Option, diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 7372703475..d5d97e1220 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -37,30 +37,17 @@ pub use message::{ #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ObjectHandle(pub u64); -/// Major/minor broker protocol version. +/// Broker protocol version. +#[repr(transparent)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ProtocolVersion { - /// Incompatible protocol version. - pub major: u16, - /// Backward-compatible protocol revision within a major version. - pub minor: u16, -} +pub struct ProtocolVersion(pub u16); impl ProtocolVersion { /// Creates a protocol version. - pub const fn new(major: u16, minor: u16) -> Self { - Self { major, minor } - } - - /// Returns whether this requested version is supported by `supported`. - /// - /// Minor revisions are backward-compatible within a major version, so a - /// broker can serve a peer requesting the same major version and a minor - /// version no newer than the broker supports. - pub const fn is_supported_by(self, supported: Self) -> bool { - self.major == supported.major && self.minor <= supported.minor + pub const fn new(version: u16) -> Self { + Self(version) } } -/// Initial broker protocol version implemented by the split-broker POC. -pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); +/// Current broker protocol version. +pub const BROKER_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1); diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index 2b321711b2..e467f2dd32 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -59,8 +59,7 @@ pub enum BrokerResponse { /// Broker protocol version supported by this endpoint. /// /// The broker returns its supported version after validating that the - /// requested version is supported according to - /// [`ProtocolVersion::is_supported_by`](crate::ProtocolVersion::is_supported_by). + /// requested version matches it. broker_protocol_version: ProtocolVersion, }, /// Negotiation failed because the requested version is unsupported. diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 66918bd2f6..b3cc9fb31e 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -159,7 +159,7 @@ mod tests { let handle = sample_handle(); let requests = [ BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new(1, 0), + protocol_version: ProtocolVersion::new(1), }, event_request(EventRequest::Create(CreateEventRequest::new(0))), event_request(EventRequest::Create(CreateEventRequest::new(7))), @@ -188,10 +188,10 @@ mod tests { let handle = sample_handle(); let responses = [ BrokerResponse::Negotiated { - broker_protocol_version: ProtocolVersion::new(1, 0), + broker_protocol_version: ProtocolVersion::new(1), }, BrokerResponse::VersionMismatch { - broker_protocol_version: ProtocolVersion::new(1, 0), + broker_protocol_version: ProtocolVersion::new(1), }, event_response(EventResponse::Create(CreateEventResponse::new(handle))), event_response(EventResponse::Wait(WaitEventResponse::new( diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs index 042a956fbc..6d6d7d45a7 100644 --- a/litebox_broker_protocol/src/wire/primitive.rs +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -34,8 +34,7 @@ impl Encoder { } pub(super) fn protocol_version(&mut self, version: ProtocolVersion) { - self.u16(version.major); - self.u16(version.minor); + self.u16(version.0); } pub(super) fn handle(&mut self, handle: ObjectHandle) { @@ -87,7 +86,7 @@ impl<'a> Decoder<'a> { } pub(super) fn protocol_version(&mut self) -> Result { - Ok(ProtocolVersion::new(self.u16()?, self.u16()?)) + Ok(ProtocolVersion::new(self.u16()?)) } pub(super) fn handle(&mut self) -> Result { diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 3bd0a0188d..052c3b972e 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -9,9 +9,10 @@ use std::process::{Child, Command, ExitStatus}; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use litebox_broker_host::HOST_PROTOCOL_VERSION; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::{ReadinessState, WaitOutcome}; +use litebox_broker_protocol::{ + BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, ReadinessState, WaitOutcome, +}; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; #[test] @@ -24,7 +25,16 @@ fn separate_process_broker_serves_event_object_requests() { .unwrap(); let mut local = BrokerLocal::new(channel); - assert_eq!(local.negotiate().unwrap(), HOST_PROTOCOL_VERSION); + assert_eq!( + local + .request(BrokerRequest::Negotiate { + protocol_version: BROKER_PROTOCOL_VERSION, + }) + .unwrap(), + BrokerResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION + } + ); let handle = local.create_event().unwrap(); assert_eq!( diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 7634d861a2..f7e5b38c08 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -9,6 +9,7 @@ use std::{ use anyhow::{Context as _, Result}; use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, BrokerRequest}; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -52,7 +53,11 @@ fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { From 821e5c11bfd91b5f4c995f5fad0eacefb5a80a51 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Mon, 22 Jun 2026 17:32:45 -0700 Subject: [PATCH 048/319] Cherry pick "Add key-value field visitor to HostLogger" (#945) Co-authored-by: Sangho Lee --- litebox_runner_lvbs/src/main.rs | 3 +-- litebox_runner_snp/src/main.rs | 3 +-- litebox_util_log/src/lib.rs | 29 +++++++++++++++++++++++++++++ litebox_util_log/tests/facade.rs | 16 ++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/litebox_runner_lvbs/src/main.rs b/litebox_runner_lvbs/src/main.rs index a744dbd097..cbca1504cb 100644 --- a/litebox_runner_lvbs/src/main.rs +++ b/litebox_runner_lvbs/src/main.rs @@ -30,9 +30,8 @@ impl log::Log for HostLogger { } fn log(&self, record: &log::Record) { - use core::fmt::Write; let mut buf: arrayvec::ArrayString<1024> = arrayvec::ArrayString::new(); - let _ = writeln!(buf, "[{}] {}", record.level(), record.args()); + let _ = litebox_util_log::format_record(&mut buf, record); litebox_platform_lvbs::arch::ioport::serial_print_string(&buf); } diff --git a/litebox_runner_snp/src/main.rs b/litebox_runner_snp/src/main.rs index fdde2a705b..9002248aba 100644 --- a/litebox_runner_snp/src/main.rs +++ b/litebox_runner_snp/src/main.rs @@ -26,9 +26,8 @@ impl log::Log for HostLogger { } fn log(&self, record: &log::Record) { - use core::fmt::Write; let mut buf: arrayvec::ArrayString<1024> = arrayvec::ArrayString::new(); - let _ = writeln!(buf, "[{}] {}", record.level(), record.args()); + let _ = litebox_util_log::format_record(&mut buf, record); ghcb_prints(&buf); } diff --git a/litebox_util_log/src/lib.rs b/litebox_util_log/src/lib.rs index 9b89b4ffa1..ed0174dcf2 100644 --- a/litebox_util_log/src/lib.rs +++ b/litebox_util_log/src/lib.rs @@ -97,6 +97,35 @@ pub use backend_log::SpanGuard; #[cfg(feature = "backend_tracing")] pub use backend_tracing::SpanGuard; +/// Converts a [`log::Record`] into the compact host-console format. +/// +/// Formats the record as `[LEVEL] message key=value ...\n` into `writer`. +#[cfg(feature = "backend_log")] +pub fn format_record( + writer: &mut W, + record: &log::Record<'_>, +) -> core::fmt::Result { + struct FieldVisitor<'a, W>(&'a mut W); + + impl log::kv::VisitSource<'_> for FieldVisitor<'_, W> { + fn visit_pair( + &mut self, + key: log::kv::Key<'_>, + value: log::kv::Value<'_>, + ) -> Result<(), log::kv::Error> { + write!(self.0, " {key}={value}")?; + Ok(()) + } + } + + write!(writer, "[{}] {}", record.level(), record.args())?; + record + .key_values() + .visit(&mut FieldVisitor(writer)) + .map_err(|_| core::fmt::Error)?; + writeln!(writer) +} + /// Internal module exposing backend types for use by exported macros. /// /// This module is public only because macros need access to backend types at the diff --git a/litebox_util_log/tests/facade.rs b/litebox_util_log/tests/facade.rs index a2147a6b7a..f05029c05f 100644 --- a/litebox_util_log/tests/facade.rs +++ b/litebox_util_log/tests/facade.rs @@ -193,3 +193,19 @@ fn test_instrument() { nested(); TestStruct { value: 7 }.instrumented_method(); } + +#[cfg(feature = "backend_log")] +#[test] +fn test_format_record_includes_key_values() { + let key_values = [("count", 42), ("name", 7)]; + let record = log::Record::builder() + .level(log::Level::Info) + .args(format_args!("hello")) + .key_values(&key_values) + .build(); + let mut output = String::new(); + + litebox_util_log::format_record(&mut output, &record).unwrap(); + + assert_eq!(output, "[INFO] hello count=42 name=7\n"); +} From 95c408e801f49b3f8294c9d9859ae32dffe8fbc2 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 22 Jun 2026 21:22:10 -0700 Subject: [PATCH 049/319] Remove unknown broker message envelopes (#947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the `Unknown` request/response envelopes from the broker protocol channel API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_host/src/lib.rs | 66 +++++-------------- litebox_broker_local/src/error.rs | 2 - litebox_broker_local/src/lib.rs | 16 ++--- litebox_broker_protocol/src/channel.rs | 36 +--------- litebox_broker_protocol/src/lib.rs | 5 +- litebox_broker_protocol/src/wire.rs | 49 ++++++-------- .../src/wire/core_message.rs | 26 +++----- litebox_broker_protocol/src/wire/event.rs | 48 +++++--------- litebox_broker_transport/src/unix_socket.rs | 5 +- litebox_runner_linux_userland/tests/run.rs | 14 ++-- 10 files changed, 77 insertions(+), 190 deletions(-) diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 00c4550b60..f58c62405f 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -18,7 +18,7 @@ use litebox_broker_core::{BrokerCore, BrokerError, BrokerSession, CallerCredenti use litebox_broker_protocol::{ AddEventResponse, BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, - PeerCredential, ReceivedBrokerRequest, WaitEventResponse, + PeerCredential, WaitEventResponse, }; mod error; @@ -54,11 +54,11 @@ where { let mut state = ConnectionState::AwaitingNegotiation; loop { - let Some(received) = channel.recv_request().map_err(BrokerHostError::Channel)? else { + let Some(request) = channel.recv_request().map_err(BrokerHostError::Channel)? else { break; }; - let dispatch = handle_received_request(session, &mut state, received); + let dispatch = handle_request(session, &mut state, request); channel .send_response(&dispatch.response) .map_err(BrokerHostError::Channel)?; @@ -80,17 +80,6 @@ fn caller_credential_from_peer( } } -fn handle_received_request( - session: &BrokerSession, - state: &mut ConnectionState, - received: ReceivedBrokerRequest, -) -> BrokerDispatch { - match received { - ReceivedBrokerRequest::Request(request) => handle_request(session, state, request), - _ => handle_unknown_request(*state), - } -} - fn handle_request( session: &BrokerSession, state: &mut ConnectionState, @@ -173,17 +162,6 @@ fn handle_event_request(session: &BrokerSession, request: EventRequest) -> Broke } } -fn handle_unknown_request(state: ConnectionState) -> BrokerDispatch { - if state == ConnectionState::AwaitingNegotiation { - BrokerDispatch::close_after( - BrokerResponse::Error(ErrorCode::ProtocolState), - CloseReason::ProtocolViolation, - ) - } else { - BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)) - } -} - fn handle_core_result( result: litebox_broker_core::Result, into_response: impl FnOnce(T) -> BrokerResponse, @@ -286,14 +264,10 @@ mod tests { fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ - Ok(Some(ReceivedBrokerRequest::Request( - BrokerRequest::Negotiate { - protocol_version: BROKER_PROTOCOL_VERSION, - }, - ))), - Ok(Some(ReceivedBrokerRequest::Request(event_create_request( - 0, - )))), + Ok(Some(BrokerRequest::Negotiate { + protocol_version: BROKER_PROTOCOL_VERSION, + })), + Ok(Some(event_create_request(0))), Ok(None), ])); @@ -318,14 +292,10 @@ mod tests { fn serve_connection_closes_after_protocol_violation(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ - Ok(Some(ReceivedBrokerRequest::Request(event_create_request( - 0, - )))), - Ok(Some(ReceivedBrokerRequest::Request( - BrokerRequest::Negotiate { - protocol_version: BROKER_PROTOCOL_VERSION, - }, - ))), + Ok(Some(event_create_request(0))), + Ok(Some(BrokerRequest::Negotiate { + protocol_version: BROKER_PROTOCOL_VERSION, + })), ])); assert_eq!( @@ -341,9 +311,9 @@ mod tests { fn serve_connection_returns_channel_error_when_response_send_fails(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([Ok(Some( - ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { + BrokerRequest::Negotiate { protocol_version: BROKER_PROTOCOL_VERSION, - }), + }, ))])); channel.send_error = true; @@ -363,15 +333,13 @@ mod tests { } struct FakeHostControlChannel { - requests: std::vec::Vec, ()>>, + requests: std::vec::Vec, ()>>, responses: std::vec::Vec, send_error: bool, } impl FakeHostControlChannel { - fn new( - requests: std::vec::Vec, ()>>, - ) -> Self { + fn new(requests: std::vec::Vec, ()>>) -> Self { Self { requests, responses: std::vec::Vec::new(), @@ -387,9 +355,7 @@ mod tests { Ok(PeerCredential::Unauthenticated) } - fn recv_request( - &mut self, - ) -> core::result::Result, Self::Error> { + fn recv_request(&mut self) -> core::result::Result, Self::Error> { if self.requests.is_empty() { Ok(None) } else { diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 40b333fe66..724b8a8898 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -16,8 +16,6 @@ pub enum BrokerLocalError { AlreadyNegotiated, #[error("broker closed the channel")] ChannelClosed, - #[error("unknown broker response")] - UnknownResponse, #[error( "broker accepted incompatible protocol negotiation: requested {requested:?}, broker supports {broker_protocol_version:?}" )] diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index fa817f09dc..6fba854114 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -17,7 +17,6 @@ mod event; use litebox_broker_protocol::{ BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, LocalControlChannel, - ReceivedBrokerResponse, }; pub use error::{BrokerLocalError, Result}; @@ -105,15 +104,10 @@ impl BrokerLocal { self.channel .send_request(&request) .map_err(BrokerLocalError::Channel)?; - match self - .channel + self.channel .recv_response() .map_err(BrokerLocalError::Channel)? - .ok_or(BrokerLocalError::ChannelClosed)? - { - ReceivedBrokerResponse::Response(response) => Ok(response), - _ => Err(BrokerLocalError::UnknownResponse), - } + .ok_or(BrokerLocalError::ChannelClosed) } } @@ -247,10 +241,8 @@ mod tests { Ok(()) } - fn recv_response( - &mut self, - ) -> core::result::Result, Self::Error> { - Ok(self.response.take().map(ReceivedBrokerResponse::Response)) + fn recv_response(&mut self) -> core::result::Result, Self::Error> { + Ok(self.response.take()) } } } diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index 83f4d848df..e4a0b6b316 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -20,38 +20,6 @@ pub enum PeerCredential { Unauthenticated, } -/// Broker authority request received from a control channel. -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum ReceivedBrokerRequest { - /// A request understood by the current protocol crate. - Request(BrokerRequest), - /// A request emitted by a newer peer and not understood by this process. - Unknown, -} - -impl From for ReceivedBrokerRequest { - fn from(request: BrokerRequest) -> Self { - Self::Request(request) - } -} - -/// Broker authority response received from a control channel. -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum ReceivedBrokerResponse { - /// A response understood by the current protocol crate. - Response(BrokerResponse), - /// A response emitted by a newer broker and not understood by this process. - Unknown, -} - -impl From for ReceivedBrokerResponse { - fn from(response: BrokerResponse) -> Self { - Self::Response(response) - } -} - /// Local-side control channel for broker authority calls. pub trait LocalControlChannel { /// Channel-specific error type. @@ -64,7 +32,7 @@ pub trait LocalControlChannel { /// /// Returns `Ok(None)` when the broker closed the channel cleanly before /// starting another response frame. - fn recv_response(&mut self) -> Result, Self::Error>; + fn recv_response(&mut self) -> Result, Self::Error>; } /// Host-side control channel for broker authority calls. @@ -79,7 +47,7 @@ pub trait HostControlChannel { /// /// Returns `Ok(None)` when the peer closed the channel cleanly before /// starting another request frame. - fn recv_request(&mut self) -> Result, Self::Error>; + fn recv_request(&mut self) -> Result, Self::Error>; /// Sends one broker response. fn send_response(&mut self, response: &BrokerResponse) -> Result<(), Self::Error>; diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index d5d97e1220..f536f21d9f 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -18,10 +18,7 @@ pub mod event; pub mod message; pub mod wire; -pub use channel::{ - HostControlChannel, LocalControlChannel, PeerCredential, ReceivedBrokerRequest, - ReceivedBrokerResponse, -}; +pub use channel::{HostControlChannel, LocalControlChannel, PeerCredential}; pub use error::ErrorCode; pub use event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index b3cc9fb31e..1a4d8f3b86 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -19,9 +19,7 @@ use alloc::vec::Vec; use thiserror::Error; -use crate::{ - BrokerRequest, BrokerResponse, ErrorCode, ReceivedBrokerRequest, ReceivedBrokerResponse, -}; +use crate::{BrokerRequest, BrokerResponse, ErrorCode}; use primitive::{Decoder, Encoder}; @@ -47,6 +45,8 @@ pub enum WireError { TrailingBytes, #[error("invalid broker wire boolean")] InvalidBoolean, + #[error("invalid broker wire tag")] + InvalidTag, #[error("broker wire offset overflow")] OffsetOverflow, } @@ -71,21 +71,18 @@ pub fn encode_request(request: BrokerRequest) -> Vec { } /// Decodes a broker request body. -pub fn decode_request(frame: &[u8]) -> Result { +pub fn decode_request(frame: &[u8]) -> Result { let mut decoder = Decoder::new(frame); let tag = decoder.u8()?; let request = match tag { REQUEST_TAG_NEGOTIATE => BrokerRequest::Negotiate { protocol_version: decoder.protocol_version()?, }, - REQUEST_TAG_CORE => match core_message::decode_core_request(&mut decoder)? { - Some(request) => BrokerRequest::Core(request), - None => return Ok(ReceivedBrokerRequest::Unknown), - }, - _ => return Ok(ReceivedBrokerRequest::Unknown), + REQUEST_TAG_CORE => BrokerRequest::Core(core_message::decode_core_request(&mut decoder)?), + _ => return Err(WireError::InvalidTag), }; decoder.finish()?; - Ok(ReceivedBrokerRequest::Request(request)) + Ok(request) } /// Encodes a broker response body. @@ -120,7 +117,7 @@ pub fn encode_response(response: BrokerResponse) -> Vec { } /// Decodes a broker response body. -pub fn decode_response(frame: &[u8]) -> Result { +pub fn decode_response(frame: &[u8]) -> Result { let mut decoder = Decoder::new(frame); let tag = decoder.u8()?; let response = match tag { @@ -130,18 +127,17 @@ pub fn decode_response(frame: &[u8]) -> Result BrokerResponse::VersionMismatch { broker_protocol_version: decoder.protocol_version()?, }, - RESPONSE_TAG_CORE => match core_message::decode_core_response(&mut decoder)? { - Some(response) => BrokerResponse::Core(response), - None => return Ok(ReceivedBrokerResponse::Unknown), - }, + RESPONSE_TAG_CORE => { + BrokerResponse::Core(core_message::decode_core_response(&mut decoder)?) + } RESPONSE_TAG_ERROR => { let error = ErrorCode::from_raw(decoder.u16()?); BrokerResponse::Error(error) } - _ => return Ok(ReceivedBrokerResponse::Unknown), + _ => return Err(WireError::InvalidTag), }; decoder.finish()?; - Ok(ReceivedBrokerResponse::Response(response)) + Ok(response) } #[cfg(test)] @@ -178,7 +174,7 @@ mod tests { for request in requests { assert_eq!( decode_request(&encode_request(request.clone())).unwrap(), - ReceivedBrokerRequest::Request(request) + request ); } } @@ -215,24 +211,21 @@ mod tests { for response in responses { assert_eq!( decode_response(&encode_response(response.clone())).unwrap(), - ReceivedBrokerResponse::Response(response) + response ); } } #[test] fn decode_rejects_malformed_request_frames() { - assert_eq!( - decode_request(&[0xff, 1, 2, 3]), - Ok(ReceivedBrokerRequest::Unknown) - ); + assert_eq!(decode_request(&[0xff, 1, 2, 3]), Err(WireError::InvalidTag)); let mut unknown_consume_mode = encode_request(event_request(EventRequest::Consume( ConsumeEventRequest::new(sample_handle(), EventConsumeMode::All), ))); *unknown_consume_mode.last_mut().unwrap() = 0xff; assert_eq!( decode_request(&unknown_consume_mode), - Ok(ReceivedBrokerRequest::Unknown) + Err(WireError::InvalidTag) ); assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); let mut frame = encode_request(event_request(EventRequest::Create( @@ -246,17 +239,15 @@ mod tests { fn decode_rejects_malformed_response_frames() { assert_eq!( decode_response(&[0xff, 1, 2, 3]), - Ok(ReceivedBrokerResponse::Unknown) + Err(WireError::InvalidTag) ); assert_eq!( decode_response(&[1, 0, 1, 0xff]), - Ok(ReceivedBrokerResponse::Unknown) + Err(WireError::InvalidTag) ); assert_eq!( decode_response(&[2, 0xff, 0xff]), - Ok(ReceivedBrokerResponse::Response(BrokerResponse::Error( - ErrorCode::Unknown(0xffff) - ))) + Ok(BrokerResponse::Error(ErrorCode::Unknown(0xffff))) ); let mut invalid_bool = [1, 0, 2, 2, 0]; diff --git a/litebox_broker_protocol/src/wire/core_message.rs b/litebox_broker_protocol/src/wire/core_message.rs index dbd0665da3..3f0061e789 100644 --- a/litebox_broker_protocol/src/wire/core_message.rs +++ b/litebox_broker_protocol/src/wire/core_message.rs @@ -21,18 +21,13 @@ pub(super) fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) { } } -pub(super) fn decode_core_request( - decoder: &mut Decoder<'_>, -) -> Result, WireError> { +pub(super) fn decode_core_request(decoder: &mut Decoder<'_>) -> Result { let request = match decoder.u8()? { - CORE_REQUEST_TAG_EVENT => match event::decode_event_request(decoder)? { - Some(request) => CoreRequest::Event(request), - None => return Ok(None), - }, - _ => return Ok(None), + CORE_REQUEST_TAG_EVENT => CoreRequest::Event(event::decode_event_request(decoder)?), + _ => return Err(WireError::InvalidTag), }; - Ok(Some(request)) + Ok(request) } pub(super) fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) { @@ -44,16 +39,11 @@ pub(super) fn encode_core_response(encoder: &mut Encoder, response: CoreResponse } } -pub(super) fn decode_core_response( - decoder: &mut Decoder<'_>, -) -> Result, WireError> { +pub(super) fn decode_core_response(decoder: &mut Decoder<'_>) -> Result { let response = match decoder.u8()? { - CORE_RESPONSE_TAG_EVENT => match event::decode_event_response(decoder)? { - Some(response) => CoreResponse::Event(response), - None => return Ok(None), - }, - _ => return Ok(None), + CORE_RESPONSE_TAG_EVENT => CoreResponse::Event(event::decode_event_response(decoder)?), + _ => return Err(WireError::InvalidTag), }; - Ok(Some(response)) + Ok(response) } diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index 94285a432b..2e26905b5f 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -50,9 +50,7 @@ pub(super) fn encode_event_request(encoder: &mut Encoder, request: EventRequest) } } -pub(super) fn decode_event_request( - decoder: &mut Decoder<'_>, -) -> Result, WireError> { +pub(super) fn decode_event_request(decoder: &mut Decoder<'_>) -> Result { let request = match decoder.u8()? { EVENT_REQUEST_TAG_CREATE => EventRequest::Create(CreateEventRequest::new(decoder.u64()?)), EVENT_REQUEST_TAG_WAIT => EventRequest::Wait(WaitEventRequest::new(decoder.handle()?)), @@ -61,15 +59,12 @@ pub(super) fn decode_event_request( } EVENT_REQUEST_TAG_CONSUME => EventRequest::Consume(ConsumeEventRequest::new( decoder.handle()?, - match decode_consume_mode(decoder)? { - Some(mode) => mode, - None => return Ok(None), - }, + decode_consume_mode(decoder)?, )), - _ => return Ok(None), + _ => return Err(WireError::InvalidTag), }; - Ok(Some(request)) + Ok(request) } pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventResponse) { @@ -94,19 +89,14 @@ pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventRespon } } -pub(super) fn decode_event_response( - decoder: &mut Decoder<'_>, -) -> Result, WireError> { +pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result { let response = match decoder.u8()? { EVENT_RESPONSE_TAG_CREATED => { EventResponse::Create(CreateEventResponse::new(decoder.handle()?)) } - EVENT_RESPONSE_TAG_WAITED => EventResponse::Wait(WaitEventResponse::new( - match decode_wait_outcome(decoder)? { - Some(outcome) => outcome, - None => return Ok(None), - }, - )), + EVENT_RESPONSE_TAG_WAITED => { + EventResponse::Wait(WaitEventResponse::new(decode_wait_outcome(decoder)?)) + } EVENT_RESPONSE_TAG_ADDED => { EventResponse::Add(AddEventResponse::new(decode_readiness(decoder)?)) } @@ -114,10 +104,10 @@ pub(super) fn decode_event_response( decoder.u64()?, decode_readiness(decoder)?, )), - _ => return Ok(None), + _ => return Err(WireError::InvalidTag), }; - Ok(Some(response)) + Ok(response) } fn encode_wait_outcome(encoder: &mut Encoder, outcome: WaitOutcome) { @@ -133,13 +123,11 @@ fn encode_wait_outcome(encoder: &mut Encoder, outcome: WaitOutcome) { } } -fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result, WireError> { +fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result { match decoder.u8()? { - WAIT_OUTCOME_TAG_READY => Ok(Some(WaitOutcome::Ready(decode_readiness(decoder)?))), - WAIT_OUTCOME_TAG_WOULD_BLOCK => { - Ok(Some(WaitOutcome::WouldBlock(decode_readiness(decoder)?))) - } - _ => Ok(None), + WAIT_OUTCOME_TAG_READY => Ok(WaitOutcome::Ready(decode_readiness(decoder)?)), + WAIT_OUTCOME_TAG_WOULD_BLOCK => Ok(WaitOutcome::WouldBlock(decode_readiness(decoder)?)), + _ => Err(WireError::InvalidTag), } } @@ -163,10 +151,10 @@ fn encode_consume_mode(encoder: &mut Encoder, mode: EventConsumeMode) { } } -fn decode_consume_mode(decoder: &mut Decoder<'_>) -> Result, WireError> { +fn decode_consume_mode(decoder: &mut Decoder<'_>) -> Result { match decoder.u8()? { - EVENT_CONSUME_MODE_TAG_ALL => Ok(Some(EventConsumeMode::All)), - EVENT_CONSUME_MODE_TAG_ONE => Ok(Some(EventConsumeMode::One)), - _ => Ok(None), + EVENT_CONSUME_MODE_TAG_ALL => Ok(EventConsumeMode::All), + EVENT_CONSUME_MODE_TAG_ONE => Ok(EventConsumeMode::One), + _ => Err(WireError::InvalidTag), } } diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 766896ba9b..eca3d1cd52 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -17,7 +17,6 @@ use litebox_broker_protocol::wire::{ }; use litebox_broker_protocol::{ BrokerRequest, BrokerResponse, HostControlChannel, LocalControlChannel, PeerCredential, - ReceivedBrokerRequest, ReceivedBrokerResponse, }; const MAX_FRAME_LEN: usize = 64 * 1024; @@ -135,7 +134,7 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { result } - fn recv_response(&mut self) -> io::Result> { + fn recv_response(&mut self) -> io::Result> { let deadline = self.current_deadline()?; let result = match read_frame_with_deadline(&mut self.stream, deadline)? { Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), @@ -155,7 +154,7 @@ impl HostControlChannel for UnixStreamHostControlChannel { Ok(PeerCredential::Unauthenticated) } - fn recv_request(&mut self) -> io::Result> { + fn recv_request(&mut self) -> io::Result> { let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { return Ok(None); }; diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 0a90564f56..04fd3bff45 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -391,19 +391,17 @@ where fn recv_request( &mut self, - ) -> Result, Self::Error> { - let received = self.inner.recv_request()?; + ) -> Result, Self::Error> { + let request = self.inner.recv_request()?; if matches!( - received, - Some(litebox_broker_protocol::ReceivedBrokerRequest::Request( - litebox_broker_protocol::BrokerRequest::Core( - litebox_broker_protocol::CoreRequest::Event(_) - ) + request, + Some(litebox_broker_protocol::BrokerRequest::Core( + litebox_broker_protocol::CoreRequest::Event(_) )) ) { self.event_request_count += 1; } - Ok(received) + Ok(request) } fn send_response( From d4e341dac72afa0a85a5700d2662291e8a55f2f4 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Tue, 23 Jun 2026 09:15:22 -0700 Subject: [PATCH 050/319] Cherry pick "Add system PTA `map_zi` and `unmap`" (#948) Co-authored-by: Sangho Lee --- litebox_common_optee/src/lib.rs | 16 +++- litebox_shim_optee/src/lib.rs | 34 +++++++- litebox_shim_optee/src/syscalls/ldelf.rs | 57 +++++++++----- litebox_shim_optee/src/syscalls/mod.rs | 26 +++++++ litebox_shim_optee/src/syscalls/pta.rs | 99 ++++++++++++++++++++++-- litebox_shim_optee/src/syscalls/tee.rs | 27 ++++--- 6 files changed, 219 insertions(+), 40 deletions(-) diff --git a/litebox_common_optee/src/lib.rs b/litebox_common_optee/src/lib.rs index bcff985e1a..b2cf533cae 100644 --- a/litebox_common_optee/src/lib.rs +++ b/litebox_common_optee/src/lib.rs @@ -80,7 +80,7 @@ pub enum SyscallRequest { ta_sess_id: u32, cancel_req_to: u32, cmd_id: u32, - params: Platform::RawConstPointer, + params: Platform::RawMutPointer, ret_orig: Platform::RawMutPointer, }, CheckAccessRights { @@ -194,7 +194,7 @@ impl SyscallRequest { ta_sess_id: u32::try_from(ctx.syscall_arg(0)).map_err(|_| Errno::EINVAL)?, cancel_req_to: u32::try_from(ctx.syscall_arg(1)).map_err(|_| Errno::EINVAL)?, cmd_id: u32::try_from(ctx.syscall_arg(2)).map_err(|_| Errno::EINVAL)?, - params: Platform::RawConstPointer::from_usize(ctx.syscall_arg(3)), + params: Platform::RawMutPointer::from_usize(ctx.syscall_arg(3)), ret_orig: Platform::RawMutPointer::from_usize(ctx.syscall_arg(4)), }, TeeSyscallNr::CheckAccessRights => SyscallRequest::CheckAccessRights { @@ -510,6 +510,18 @@ impl UteeParams { (0..Self::TEE_NUM_PARAMS).all(|i| self.get_type(i).is_ok_and(|t| t == expected[i])) } + /// Return `true` if any parameter is an output or inout type, i.e., the + /// command may write results that must be copied back to the caller. + pub fn needs_copy_back(&self) -> bool { + use TeeParamType::{MemrefInout, MemrefOutput, ValueInout, ValueOutput}; + (0..Self::TEE_NUM_PARAMS).any(|i| { + matches!( + self.get_type(i), + Ok(ValueOutput | ValueInout | MemrefOutput | MemrefInout) + ) + }) + } + pub fn get_type(&self, index: usize) -> Result { let type_byte = match index { 0 => self.types.type_0(), diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index d94b688565..b3e933fe8f 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -469,8 +469,24 @@ impl Task { params, ret_orig, } => { - if let Some(params) = params.read_at_offset(0) { - self.sys_invoke_ta_command(ta_sess_id, cancel_req_to, cmd_id, params, ret_orig) + if let Some(mut params_copied) = params.read_at_offset(0) { + self.sys_invoke_ta_command( + ta_sess_id, + cancel_req_to, + cmd_id, + &mut params_copied, + ret_orig, + ) + .and_then(|cleanup| { + if !params_copied.needs_copy_back() + || params.write_at_offset(0, params_copied).is_some() + { + Ok(()) + } else { + cleanup.run(self); + Err(TeeResult::AccessDenied) + } + }) } else { Err(TeeResult::BadParameters) } @@ -664,7 +680,19 @@ impl Task { pad_begin, pad_end, flags, - } => self.sys_map_zi(va, num_bytes, pad_begin, pad_end, flags), + } => match va.read_at_offset(0) { + Some(hint) => self + .sys_map_zi(hint, num_bytes, pad_begin, pad_end, flags) + .and_then(|(mapped, cleanup)| { + if va.write_at_offset(0, mapped).is_some() { + Ok(()) + } else { + cleanup.run(self); + Err(TeeResult::AccessDenied) + } + }), + None => Err(TeeResult::BadParameters), + }, LdelfSyscallRequest::OpenBin { uuid, uuid_size, diff --git a/litebox_shim_optee/src/syscalls/ldelf.rs b/litebox_shim_optee/src/syscalls/ldelf.rs index 88683bbba0..bcc3f7b5f0 100644 --- a/litebox_shim_optee/src/syscalls/ldelf.rs +++ b/litebox_shim_optee/src/syscalls/ldelf.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use crate::syscalls::Cleanup; use crate::{Task, UserMutPtr}; use litebox::mm::linux::PAGE_SIZE; use litebox::platform::{RawConstPointer, RawMutPointer, SystemInfoProvider as _}; @@ -68,28 +69,27 @@ impl Task { } /// OP-TEE's syscall to map zero-initialized memory with padding. - /// This function pads `pad_begin` bytes before and `pad_end` bytes after the - /// zero-initialized `num_bytes` bytes. `va` can contain a hint address which - /// is `pad_begin` bytes lower than the starting address of the memory region. - /// (`start - pad_begin`, ..., `start`, ..., `start + num_bytes`, ..., `start + num_bytes + pad_end`) - /// Memory regions between `start - pad_begin` and `start` and between - /// `start + num_bytes` and `start + num_bytes + pad_end` are reserved and must not be used. + /// + /// Maps `pad_begin + num_bytes + pad_end` bytes (rounded up to a page) and + /// zero-initializes the `num_bytes` usable region. `va` is a page-aligned + /// hint for the *base of the whole mapping* (`0` means no hint). The usable + /// region thus starts at `start = va + pad_begin`; the `pad_begin`/`pad_end` + /// regions are reserved and must not be accessed. + /// + /// On success, returns `start` plus a `Cleanup` that unmaps the usable + /// region. The caller communicates the address back to userspace and must + /// run the cleanup if that write-back fails. pub fn sys_map_zi( &self, - va: UserMutPtr, + va: usize, num_bytes: usize, pad_begin: usize, pad_end: usize, flags: LdelfMapFlags, - ) -> Result<(), TeeResult> { - let Some(addr) = va.read_at_offset(0) else { - return Err(TeeResult::BadParameters); - }; - + ) -> Result<(usize, Cleanup), TeeResult> { #[cfg(debug_assertions)] litebox_util_log::debug!( - va:% = format_args!("{:#x}", va.as_usize()), - addr:% = format_args!("{:#x}", addr), + va:% = format_args!("{:#x}", va), num_bytes:% = num_bytes, flags:% = format_args!("{:#x}", flags); "sys_map_zi" @@ -101,20 +101,28 @@ impl Task { } // TODO: Check whether flags contains `LDELF_MAP_FLAG_SHAREABLE` once we support sharing of file-based mappings. + // OP-TEE requires the address hint and padding to be page-aligned. + if !va.is_multiple_of(PAGE_SIZE) + || !pad_begin.is_multiple_of(PAGE_SIZE) + || !pad_end.is_multiple_of(PAGE_SIZE) + { + return Err(TeeResult::AccessConflict); + } + let total_size = Self::checked_map_size(num_bytes, pad_begin, pad_end)?; - if addr.checked_add(total_size).is_none() { + if va.checked_add(total_size).is_none() { return Err(TeeResult::BadParameters); } // `sys_map_zi` always creates read/writeable mapping. // // We map with PROT_READ_WRITE first, then mprotect padding regions to PROT_NONE. let mut flags = MapFlags::MAP_PRIVATE | MapFlags::MAP_ANONYMOUS; - if addr != 0 { + if va != 0 { flags |= MapFlags::MAP_FIXED; } let addr = self - .sys_mmap(addr, total_size, ProtFlags::PROT_READ_WRITE, flags, -1, 0) + .sys_mmap(va, total_size, ProtFlags::PROT_READ_WRITE, flags, -1, 0) .map_err(|_| TeeResult::OutOfMemory)?; let guard = MmapGuard::new(self, addr, total_size); @@ -143,9 +151,12 @@ impl Task { ); } - let _ = va.write_at_offset(0, padded_start); guard.disarm(); - Ok(()) + let cleanup = Cleanup::Unmap { + addr: padded_start, + len: pad_end_start - padded_start, + }; + Ok((padded_start, cleanup)) } /// OP-TEE's syscall to open a TA binary. @@ -215,6 +226,14 @@ impl Task { return Err(TeeResult::BadParameters); } + // OP-TEE requires the address hint and padding to be page-aligned. + if !addr.is_multiple_of(PAGE_SIZE) + || !pad_begin.is_multiple_of(PAGE_SIZE) + || !pad_end.is_multiple_of(PAGE_SIZE) + { + return Err(TeeResult::AccessConflict); + } + if self.ta_handle_map.get(handle).is_none() { return Err(TeeResult::BadParameters); } diff --git a/litebox_shim_optee/src/syscalls/mod.rs b/litebox_shim_optee/src/syscalls/mod.rs index 33e02a0e3e..d77303fbad 100644 --- a/litebox_shim_optee/src/syscalls/mod.rs +++ b/litebox_shim_optee/src/syscalls/mod.rs @@ -3,6 +3,9 @@ //! Syscalls Handlers +use crate::{Task, UserMutPtr}; +use litebox::platform::RawConstPointer as _; + pub(crate) mod cryp; pub(crate) mod ldelf; pub(crate) mod mm; @@ -11,3 +14,26 @@ pub(crate) mod tee; #[cfg(test)] pub(crate) mod tests; + +/// Undo a syscall/command's side effects if dispatch fails after the command +/// succeeded (currently only when copying results back out to the guest fails). +#[derive(Default)] +#[must_use = "must be run when dispatch fails after the command, or the side effect leaks"] +pub(crate) enum Cleanup { + #[default] + None, + /// Unmap a region. `addr` must be page-aligned; `len` is rounded up by `sys_munmap`. + Unmap { addr: usize, len: usize }, +} + +impl Cleanup { + /// Undo the side effect. Runs only on an error path, so failures are ignored. + pub(crate) fn run(self, task: &Task) { + match self { + Self::None => {} + Self::Unmap { addr, len } => { + let _ = task.sys_munmap(UserMutPtr::::from_usize(addr), len); + } + } + } +} diff --git a/litebox_shim_optee/src/syscalls/pta.rs b/litebox_shim_optee/src/syscalls/pta.rs index 30bffefa0c..ab0c426bac 100644 --- a/litebox_shim_optee/src/syscalls/pta.rs +++ b/litebox_shim_optee/src/syscalls/pta.rs @@ -4,16 +4,19 @@ //! Implementation of pseudo TAs (PTAs) which export system services as //! the functions of built-in TAs. +use crate::syscalls::Cleanup; use crate::{Task, UserConstPtr, UserMutPtr}; use alloc::vec; use alloc::vec::Vec; use hmac::{Hmac, Mac}; +use litebox::mm::linux::PAGE_SIZE; use litebox::platform::{ DerivedKeyError, DerivedKeyProvider, KDFParams, RawConstPointer as _, RawMutPointer as _, }; use litebox::utils::TruncateExt; use litebox_common_optee::{ - HUK_SUBKEY_MAX_LEN, HukSubkeyUsage, TaFlags, TeeParamType, TeeResult, TeeUuid, UteeParams, + HUK_SUBKEY_MAX_LEN, HukSubkeyUsage, LdelfMapFlags, TaFlags, TeeParamType, TeeResult, TeeUuid, + UteeParams, }; use num_enum::TryFromPrimitive; use sha2::Sha256; @@ -48,8 +51,8 @@ impl PseudoTa { self, task: &Task, cmd_id: u32, - params: &UteeParams, - ) -> Result<(), TeeResult> { + params: &mut UteeParams, + ) -> Result { let _busy = task.try_set_busy(self)?; match self { Self::System => SystemPta::invoke_command(task, cmd_id, params), @@ -235,10 +238,19 @@ impl SystemPta { } /// Handle a command of the system PTA. - fn invoke_command(task: &Task, cmd_id: u32, params: &UteeParams) -> Result<(), TeeResult> { - #[allow(clippy::single_match_else)] + /// + /// See `Cleanup` for the returned rollback; most commands have no cleanup. + fn invoke_command( + task: &Task, + cmd_id: u32, + params: &mut UteeParams, + ) -> Result { match PtaSystemCommandId::try_from(cmd_id).map_err(|_| TeeResult::BadParameters)? { - PtaSystemCommandId::DeriveTaUniqueKey => Self::derive_ta_unique_key(task, params), + PtaSystemCommandId::DeriveTaUniqueKey => { + Self::derive_ta_unique_key(task, params).map(|()| Cleanup::None) + } + PtaSystemCommandId::MapZi => Self::map_zi(task, params), + PtaSystemCommandId::Unmap => Self::unmap(task, params).map(|()| Cleanup::None), _ => { #[cfg(debug_assertions)] todo!("support other system PTA commands {cmd_id}"); @@ -346,6 +358,81 @@ impl SystemPta { Ok(()) } + + fn map_zi(task: &Task, params: &mut UteeParams) -> Result { + use TeeParamType::{None, ValueInout, ValueInput}; + + if !params.has_types([ValueInput, ValueInout, ValueInput, None]) { + return Err(TeeResult::BadParameters); + } + + let (num_bytes, flags) = params + .get_values(0) + .map_err(|_| TeeResult::BadParameters)? + .ok_or(TeeResult::BadParameters)?; + if num_bytes == 0 { + return Err(TeeResult::BadParameters); + } + let (addr_high, addr_low) = params + .get_values(1) + .map_err(|_| TeeResult::BadParameters)? + .ok_or(TeeResult::BadParameters)?; + let (pad_begin, pad_end) = params + .get_values(2) + .map_err(|_| TeeResult::BadParameters)? + .ok_or(TeeResult::BadParameters)?; + + if addr_high & 0xffff_ffff_0000_0000 != 0 || addr_low & 0xffff_ffff_0000_0000 != 0 { + return Err(TeeResult::BadParameters); + } + let addr: usize = ((addr_high << 32) | addr_low).trunc(); + let (mapped, cleanup) = task.sys_map_zi( + addr, + num_bytes.trunc(), + pad_begin.trunc(), + pad_end.trunc(), + LdelfMapFlags::from_bits_retain(flags.trunc()), + )?; + + // Return the mapped address to the caller via the inout value param. + // This `set_values` cannot fail because the index is fixed/known. + let _ = params.set_values(1, (mapped as u64) >> 32, (mapped as u64) & 0xffff_ffff); + + // The caller runs `cleanup` (unmap) if it encounters an error. + Ok(cleanup) + } + + fn unmap(task: &Task, params: &UteeParams) -> Result<(), TeeResult> { + use TeeParamType::{None, ValueInput}; + + if !params.has_types([ValueInput, ValueInput, None, None]) { + return Err(TeeResult::BadParameters); + } + + let (size, must_be_zero) = params + .get_values(0) + .map_err(|_| TeeResult::BadParameters)? + .ok_or(TeeResult::BadParameters)?; + if must_be_zero != 0 { + return Err(TeeResult::BadParameters); + } + let (addr_high, addr_low) = params + .get_values(1) + .map_err(|_| TeeResult::BadParameters)? + .ok_or(TeeResult::BadParameters)?; + + if addr_high & 0xffff_ffff_0000_0000 != 0 || addr_low & 0xffff_ffff_0000_0000 != 0 { + return Err(TeeResult::BadParameters); + } + let addr: usize = ((addr_high << 32) | addr_low).trunc(); + let size: usize = size.trunc(); + let size = size + .checked_next_multiple_of(PAGE_SIZE) + .ok_or(TeeResult::BadParameters)?; + + task.sys_munmap(UserMutPtr::::from_usize(addr), size) + .map_err(|_| TeeResult::BadParameters) + } } /// A KDF callback that derives a subkey from `huk` and `params.context` to be passed to diff --git a/litebox_shim_optee/src/syscalls/tee.rs b/litebox_shim_optee/src/syscalls/tee.rs index 764606392b..1b5945c6ad 100644 --- a/litebox_shim_optee/src/syscalls/tee.rs +++ b/litebox_shim_optee/src/syscalls/tee.rs @@ -15,7 +15,10 @@ use litebox_common_optee::{ use num_enum::TryFromPrimitive; use zerocopy::IntoBytes; -use crate::{Task, UserConstPtr, UserMutPtr, syscalls::pta::PseudoTa}; +use crate::{ + Task, UserConstPtr, UserMutPtr, + syscalls::{Cleanup, pta::PseudoTa}, +}; #[inline] fn align_up(addr: usize, align: usize) -> Option { @@ -170,9 +173,6 @@ impl Task { ret_orig: UserMutPtr, ) -> Result<(), TeeResult> { // `cancel_req_to` is a timeout value. Ignore it for now. - ret_orig - .write_at_offset(0, TeeOrigin::Tee) - .ok_or(TeeResult::AccessDenied)?; if let Some(pta) = PseudoTa::from_uuid(&ta_uuid) { // `open_ta_session` syscall lets a user-mode TA open a session to a PTA which provides // several import services (it works as a proxy for extra system calls). @@ -181,6 +181,9 @@ impl Task { self.close_pta_session(session_id); return Err(TeeResult::AccessDenied); } + // Best-effort write-back of the return origin, matching OP-TEE OS + // (`syscall_open_ta_session`): the copy result is ignored. + let _ = ret_orig.write_at_offset(0, TeeOrigin::Tee); Ok(()) } else { // `open_ta_session` syscall lets a user-mode TA open a session to another user-mode TA @@ -208,20 +211,24 @@ impl Task { } /// A system call to invoke a command on a TA. + /// + /// Returns `Cleanup` that the caller must run if the surrounding + /// dispatch then fails. pub fn sys_invoke_ta_command( &self, ta_sess_id: u32, _cancel_req_to: u32, cmd_id: u32, - params: UteeParams, + params: &mut UteeParams, ret_orig: UserMutPtr, - ) -> Result<(), TeeResult> { + ) -> Result { // `cancel_req_to` is a timeout value. Ignore it for now. - ret_orig - .write_at_offset(0, TeeOrigin::Tee) - .ok_or(TeeResult::AccessDenied)?; if let Some(pta) = self.pta_for_session(ta_sess_id) { - pta.invoke_command(self, cmd_id, ¶ms) + let cleanup = pta.invoke_command(self, cmd_id, params)?; + // Best-effort write-back of the return origin, matching OP-TEE OS + // (`syscall_invoke_ta_command`): the copy result is ignored. + let _ = ret_orig.write_at_offset(0, TeeOrigin::Tee); + Ok(cleanup) } else { #[cfg(debug_assertions)] todo!("support inter TA interaction"); From ced10e0ee549cf6c2ab4e414a6b243faa588d224 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 23 Jun 2026 10:00:40 -0700 Subject: [PATCH 051/319] Sort broker error codes (#949) This PR reorders broker ErrorCode raw mappings to follow enum order. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_protocol/src/error.rs | 39 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index 2c28328f1a..7ec946eaf2 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -36,25 +36,24 @@ pub enum ErrorCode { } impl ErrorCode { - /// Raw error values are part of the broker wire ABI; do not renumber - /// assigned values. + /// Raw error values are part of the broker wire ABI. /// - /// Values `0`, `1`, `6`, and `7` remain unassigned so null/default-looking - /// values never represent concrete broker errors and retired values are not reused. + /// Value `0` is unassigned so null/default-looking values never represent + /// concrete broker errors. /// /// Converts a raw protocol error code to an error category. pub const fn from_raw(raw: u16) -> Self { match raw { - 2 => Self::UnsupportedVersion, - 3 => Self::MalformedRequest, - 10 => Self::ProtocolState, - 11 => Self::UnsupportedOperation, - 12 => Self::Internal, - 4 => Self::PolicyDenied, - 5 => Self::UnknownObject, + 1 => Self::UnsupportedVersion, + 2 => Self::MalformedRequest, + 3 => Self::ProtocolState, + 4 => Self::UnsupportedOperation, + 5 => Self::Internal, + 6 => Self::PolicyDenied, + 7 => Self::UnknownObject, 8 => Self::InvalidRights, 9 => Self::ResourceExhausted, - 13 => Self::WouldBlock, + 10 => Self::WouldBlock, raw => Self::Unknown(raw), } } @@ -62,16 +61,16 @@ impl ErrorCode { /// Returns the raw protocol error code. pub const fn as_raw(self) -> u16 { match self { - Self::UnsupportedVersion => 2, - Self::MalformedRequest => 3, - Self::ProtocolState => 10, - Self::UnsupportedOperation => 11, - Self::Internal => 12, - Self::PolicyDenied => 4, - Self::UnknownObject => 5, + Self::UnsupportedVersion => 1, + Self::MalformedRequest => 2, + Self::ProtocolState => 3, + Self::UnsupportedOperation => 4, + Self::Internal => 5, + Self::PolicyDenied => 6, + Self::UnknownObject => 7, Self::InvalidRights => 8, Self::ResourceExhausted => 9, - Self::WouldBlock => 13, + Self::WouldBlock => 10, Self::Unknown(raw) => raw, } } From c89eea2780b5ed98d1456934f2e6dcb733e3aa65 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 23 Jun 2026 16:14:27 -0700 Subject: [PATCH 052/319] Use explicit broker protocol DTO literals (#950) This PR removes trivial `new()` constructors from public-field broker protocol DTOs. Call sites now use tuple or field literals so the values being set are explicit at construction time. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/event/counter.rs | 20 ++-- litebox_broker_core/src/event.rs | 22 +++-- litebox_broker_core/src/session.rs | 18 +++- litebox_broker_host/src/lib.rs | 12 +-- litebox_broker_local/src/event.rs | 24 ++--- litebox_broker_protocol/src/event.rs | 66 ------------- litebox_broker_protocol/src/lib.rs | 9 +- litebox_broker_protocol/src/wire.rs | 97 +++++++++++-------- litebox_broker_protocol/src/wire/event.rs | 58 ++++++----- litebox_broker_protocol/src/wire/primitive.rs | 2 +- .../tests/userland_broker.rs | 15 ++- 11 files changed, 165 insertions(+), 178 deletions(-) diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 5fad8e0afd..f417ee39e2 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -61,7 +61,7 @@ where }; let response = broker .request(CoreRequest::Event(EventRequest::Create( - CreateEventRequest::new(initial_count), + CreateEventRequest { initial_count }, ))) .map_err(BrokerObjectError::from) .and_then(event_response_from_core) @@ -115,10 +115,10 @@ where &self, mode: EventCounterReadMode, ) -> Result { - let response = self.request_event(EventRequest::Consume(ConsumeEventRequest::new( - self.handle, + let response = self.request_event(EventRequest::Consume(ConsumeEventRequest { + handle: self.handle, mode, - )))?; + }))?; let EventResponse::Consume(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); }; @@ -126,8 +126,10 @@ where } fn add(&self, value: u64) -> Result { - let response = - self.request_event(EventRequest::Add(AddEventRequest::new(self.handle, value)))?; + let response = self.request_event(EventRequest::Add(AddEventRequest { + handle: self.handle, + value, + }))?; let EventResponse::Add(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); }; @@ -151,9 +153,9 @@ where } fn check_io_events(&self) -> Events { - let Ok(response) = - self.request_event(EventRequest::Wait(WaitEventRequest::new(self.handle))) - else { + let Ok(response) = self.request_event(EventRequest::Wait(WaitEventRequest { + handle: self.handle, + })) else { return Events::empty(); }; let EventResponse::Wait(response) = response else { diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 447102c28a..67040bdb83 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -29,7 +29,10 @@ pub fn wait(session: &BrokerSession, handle: ObjectHandle) -> Result { - let readiness = ReadinessState::new(event.count > 0, event.count < MAX_EVENT_COUNT); + let readiness = ReadinessState { + read_ready: event.count > 0, + write_ready: event.count < MAX_EVENT_COUNT, + }; Ok(if readiness.read_ready { WaitOutcome::Ready(readiness) } else { @@ -75,10 +78,10 @@ impl EventObject { .checked_add(value) .filter(|count| *count <= MAX_EVENT_COUNT) .ok_or(BrokerError::WouldBlock)?; - Ok(ReadinessState::new( - self.count > 0, - self.count < MAX_EVENT_COUNT, - )) + Ok(ReadinessState { + read_ready: self.count > 0, + write_ready: self.count < MAX_EVENT_COUNT, + }) } fn consume(&mut self, mode: EventConsumeMode) -> Result { @@ -92,9 +95,12 @@ impl EventObject { _ => return Err(BrokerError::UnsupportedOperation), }; self.count -= value; - Ok(EventConsumption::new( + Ok(EventConsumption { value, - ReadinessState::new(self.count > 0, self.count < MAX_EVENT_COUNT), - )) + readiness: ReadinessState { + read_ready: self.count > 0, + write_ready: self.count < MAX_EVENT_COUNT, + }, + }) } } diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index 7d91cf8a0a..b0634c89d2 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -214,15 +214,27 @@ mod tests { assert_eq!( event::wait(&session, handle), - Ok(WaitOutcome::WouldBlock(ReadinessState::new(false, true))) + Ok(WaitOutcome::WouldBlock(ReadinessState { + read_ready: false, + write_ready: true, + })) ); assert_eq!( event::add(&session, handle, 1), - Ok(ReadinessState::new(true, true)) + Ok(ReadinessState { + read_ready: true, + write_ready: true, + }) ); assert_eq!( event::consume(&session, handle, EventConsumeMode::One), - Ok(EventConsumption::new(1, ReadinessState::new(false, true))) + Ok(EventConsumption { + value: 1, + readiness: ReadinessState { + read_ready: false, + write_ready: true, + }, + }) ); assert_eq!( event::create(&session, 0), diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index f58c62405f..ac62dd661c 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -133,23 +133,23 @@ fn handle_event_request(session: &BrokerSession, request: EventRequest) -> Broke EventRequest::Create(request) => { handle_core_result(event::create(session, request.initial_count), |handle| { BrokerResponse::Core(CoreResponse::Event(EventResponse::Create( - CreateEventResponse::new(handle), + CreateEventResponse { handle }, ))) }) } EventRequest::Wait(request) => { handle_core_result(event::wait(session, request.handle), |outcome| { BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( - WaitEventResponse::new(outcome), + WaitEventResponse { outcome }, ))) }) } EventRequest::Add(request) => handle_core_result( event::add(session, request.handle, request.value), |readiness| { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Add( - AddEventResponse::new(readiness), - ))) + BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(AddEventResponse { + readiness, + }))) }, ), EventRequest::Consume(request) => handle_core_result( @@ -329,7 +329,7 @@ mod tests { } const fn event_create_request(initial_count: u64) -> BrokerRequest { - event_request(EventRequest::Create(CreateEventRequest::new(initial_count))) + event_request(EventRequest::Create(CreateEventRequest { initial_count })) } struct FakeHostControlChannel { diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 6552075964..c3626344cb 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -20,9 +20,9 @@ impl BrokerLocal { &mut self, initial_count: u64, ) -> Result { - match self.request(event_request(EventRequest::Create( - CreateEventRequest::new(initial_count), - )))? { + match self.request(event_request(EventRequest::Create(CreateEventRequest { + initial_count, + })))? { BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { Ok(response.handle) } @@ -32,9 +32,9 @@ impl BrokerLocal { /// Checks whether an event wait would complete now. pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { - match self.request(event_request(EventRequest::Wait(WaitEventRequest::new( + match self.request(event_request(EventRequest::Wait(WaitEventRequest { handle, - ))))? { + })))? { BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response))) => { Ok(response.outcome) } @@ -48,9 +48,10 @@ impl BrokerLocal { handle: ObjectHandle, value: u64, ) -> Result { - match self.request(event_request(EventRequest::Add(AddEventRequest::new( - handle, value, - ))))? { + match self.request(event_request(EventRequest::Add(AddEventRequest { + handle, + value, + })))? { BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(response))) => { Ok(response.readiness) } @@ -64,9 +65,10 @@ impl BrokerLocal { handle: ObjectHandle, mode: EventConsumeMode, ) -> Result { - match self.request(event_request(EventRequest::Consume( - ConsumeEventRequest::new(handle, mode), - )))? { + match self.request(event_request(EventRequest::Consume(ConsumeEventRequest { + handle, + mode, + })))? { BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(response))) => { Ok(response) } diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs index 3dbb53c76a..e29e91d824 100644 --- a/litebox_broker_protocol/src/event.rs +++ b/litebox_broker_protocol/src/event.rs @@ -12,16 +12,6 @@ pub struct ReadinessState { pub write_ready: bool, } -impl ReadinessState { - /// Creates a readiness state. - pub const fn new(read_ready: bool, write_ready: bool) -> Self { - Self { - read_ready, - write_ready, - } - } -} - /// Result of checking whether a broker event read wait would complete now. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] @@ -49,13 +39,6 @@ pub struct CreateEventRequest { pub initial_count: u64, } -impl CreateEventRequest { - /// Creates an event create request. - pub const fn new(initial_count: u64) -> Self { - Self { initial_count } - } -} - /// Response to an event create request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CreateEventResponse { @@ -63,13 +46,6 @@ pub struct CreateEventResponse { pub handle: ObjectHandle, } -impl CreateEventResponse { - /// Creates an event create response. - pub const fn new(handle: ObjectHandle) -> Self { - Self { handle } - } -} - /// Request to check whether an event wait would complete now. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WaitEventRequest { @@ -77,13 +53,6 @@ pub struct WaitEventRequest { pub handle: ObjectHandle, } -impl WaitEventRequest { - /// Creates an event wait request. - pub const fn new(handle: ObjectHandle) -> Self { - Self { handle } - } -} - /// Response to an event wait request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WaitEventResponse { @@ -91,13 +60,6 @@ pub struct WaitEventResponse { pub outcome: WaitOutcome, } -impl WaitEventResponse { - /// Creates an event wait response. - pub const fn new(outcome: WaitOutcome) -> Self { - Self { outcome } - } -} - /// Request to add readiness credits to an event. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct AddEventRequest { @@ -107,13 +69,6 @@ pub struct AddEventRequest { pub value: u64, } -impl AddEventRequest { - /// Creates an event add request. - pub const fn new(handle: ObjectHandle, value: u64) -> Self { - Self { handle, value } - } -} - /// Response to an event add request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct AddEventResponse { @@ -121,13 +76,6 @@ pub struct AddEventResponse { pub readiness: ReadinessState, } -impl AddEventResponse { - /// Creates an event add response. - pub const fn new(readiness: ReadinessState) -> Self { - Self { readiness } - } -} - /// Request to consume readiness credits from an event. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ConsumeEventRequest { @@ -137,13 +85,6 @@ pub struct ConsumeEventRequest { pub mode: EventConsumeMode, } -impl ConsumeEventRequest { - /// Creates an event consume request. - pub const fn new(handle: ObjectHandle, mode: EventConsumeMode) -> Self { - Self { handle, mode } - } -} - /// Result of consuming readiness credits from a broker-owned event object. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct EventConsumption { @@ -153,12 +94,5 @@ pub struct EventConsumption { pub readiness: ReadinessState, } -impl EventConsumption { - /// Creates an event consumption result. - pub const fn new(value: u64, readiness: ReadinessState) -> Self { - Self { value, readiness } - } -} - /// Response to an event consume request. pub type ConsumeEventResponse = EventConsumption; diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index f536f21d9f..5ee814bda2 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -39,12 +39,5 @@ pub struct ObjectHandle(pub u64); #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ProtocolVersion(pub u16); -impl ProtocolVersion { - /// Creates a protocol version. - pub const fn new(version: u16) -> Self { - Self(version) - } -} - /// Current broker protocol version. -pub const BROKER_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(1); +pub const BROKER_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion(1); diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 1a4d8f3b86..360879ad2c 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -144,8 +144,8 @@ pub fn decode_response(frame: &[u8]) -> Result { mod tests { use super::*; use crate::{ - AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, - CoreResponse, CreateEventRequest, CreateEventResponse, EventConsumeMode, EventRequest, + AddEventRequest, AddEventResponse, ConsumeEventRequest, CoreRequest, CoreResponse, + CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, EventRequest, EventResponse, ObjectHandle, ProtocolVersion, ReadinessState, WaitEventRequest, WaitEventResponse, WaitOutcome, }; @@ -155,20 +155,24 @@ mod tests { let handle = sample_handle(); let requests = [ BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new(1), + protocol_version: ProtocolVersion(1), }, - event_request(EventRequest::Create(CreateEventRequest::new(0))), - event_request(EventRequest::Create(CreateEventRequest::new(7))), - event_request(EventRequest::Wait(WaitEventRequest::new(handle))), - event_request(EventRequest::Add(AddEventRequest::new(handle, 3))), - event_request(EventRequest::Consume(ConsumeEventRequest::new( + event_request(EventRequest::Create(CreateEventRequest { + initial_count: 0, + })), + event_request(EventRequest::Create(CreateEventRequest { + initial_count: 7, + })), + event_request(EventRequest::Wait(WaitEventRequest { handle })), + event_request(EventRequest::Add(AddEventRequest { handle, value: 3 })), + event_request(EventRequest::Consume(ConsumeEventRequest { handle, - EventConsumeMode::All, - ))), - event_request(EventRequest::Consume(ConsumeEventRequest::new( + mode: EventConsumeMode::All, + })), + event_request(EventRequest::Consume(ConsumeEventRequest { handle, - EventConsumeMode::One, - ))), + mode: EventConsumeMode::One, + })), ]; for request in requests { @@ -184,25 +188,37 @@ mod tests { let handle = sample_handle(); let responses = [ BrokerResponse::Negotiated { - broker_protocol_version: ProtocolVersion::new(1), + broker_protocol_version: ProtocolVersion(1), }, BrokerResponse::VersionMismatch { - broker_protocol_version: ProtocolVersion::new(1), + broker_protocol_version: ProtocolVersion(1), }, - event_response(EventResponse::Create(CreateEventResponse::new(handle))), - event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::Ready(ReadinessState::new(true, false)), - ))), - event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, true)), - ))), - event_response(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(true, true), - ))), - event_response(EventResponse::Consume(ConsumeEventResponse::new( - 3, - ReadinessState::new(false, true), - ))), + event_response(EventResponse::Create(CreateEventResponse { handle })), + event_response(EventResponse::Wait(WaitEventResponse { + outcome: WaitOutcome::Ready(ReadinessState { + read_ready: true, + write_ready: false, + }), + })), + event_response(EventResponse::Wait(WaitEventResponse { + outcome: WaitOutcome::WouldBlock(ReadinessState { + read_ready: false, + write_ready: true, + }), + })), + event_response(EventResponse::Add(AddEventResponse { + readiness: ReadinessState { + read_ready: true, + write_ready: true, + }, + })), + event_response(EventResponse::Consume(EventConsumption { + value: 3, + readiness: ReadinessState { + read_ready: false, + write_ready: true, + }, + })), BrokerResponse::Error(ErrorCode::PolicyDenied), BrokerResponse::Error(ErrorCode::WouldBlock), BrokerResponse::Error(ErrorCode::Internal), @@ -219,18 +235,20 @@ mod tests { #[test] fn decode_rejects_malformed_request_frames() { assert_eq!(decode_request(&[0xff, 1, 2, 3]), Err(WireError::InvalidTag)); - let mut unknown_consume_mode = encode_request(event_request(EventRequest::Consume( - ConsumeEventRequest::new(sample_handle(), EventConsumeMode::All), - ))); + let mut unknown_consume_mode = + encode_request(event_request(EventRequest::Consume(ConsumeEventRequest { + handle: sample_handle(), + mode: EventConsumeMode::All, + }))); *unknown_consume_mode.last_mut().unwrap() = 0xff; assert_eq!( decode_request(&unknown_consume_mode), Err(WireError::InvalidTag) ); assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); - let mut frame = encode_request(event_request(EventRequest::Create( - CreateEventRequest::new(0), - ))); + let mut frame = encode_request(event_request(EventRequest::Create(CreateEventRequest { + initial_count: 0, + }))); frame.push(0xff); assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); } @@ -266,9 +284,12 @@ mod tests { #[test] fn event_add_response_wire_shape_is_pinned() { assert_eq!( - encode_response(event_response(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(true, false) - )))), + encode_response(event_response(EventResponse::Add(AddEventResponse { + readiness: ReadinessState { + read_ready: true, + write_ready: false, + }, + }))), [1, 0, 2, 1, 0] ); } diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index 2e26905b5f..7f308a774a 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -2,8 +2,8 @@ // Licensed under the MIT license. use crate::{ - AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, - CreateEventRequest, CreateEventResponse, EventConsumeMode, EventRequest, EventResponse, + AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, + CreateEventResponse, EventConsumeMode, EventConsumption, EventRequest, EventResponse, ReadinessState, WaitEventRequest, WaitEventResponse, WaitOutcome, }; @@ -52,15 +52,20 @@ pub(super) fn encode_event_request(encoder: &mut Encoder, request: EventRequest) pub(super) fn decode_event_request(decoder: &mut Decoder<'_>) -> Result { let request = match decoder.u8()? { - EVENT_REQUEST_TAG_CREATE => EventRequest::Create(CreateEventRequest::new(decoder.u64()?)), - EVENT_REQUEST_TAG_WAIT => EventRequest::Wait(WaitEventRequest::new(decoder.handle()?)), - EVENT_REQUEST_TAG_ADD => { - EventRequest::Add(AddEventRequest::new(decoder.handle()?, decoder.u64()?)) - } - EVENT_REQUEST_TAG_CONSUME => EventRequest::Consume(ConsumeEventRequest::new( - decoder.handle()?, - decode_consume_mode(decoder)?, - )), + EVENT_REQUEST_TAG_CREATE => EventRequest::Create(CreateEventRequest { + initial_count: decoder.u64()?, + }), + EVENT_REQUEST_TAG_WAIT => EventRequest::Wait(WaitEventRequest { + handle: decoder.handle()?, + }), + EVENT_REQUEST_TAG_ADD => EventRequest::Add(AddEventRequest { + handle: decoder.handle()?, + value: decoder.u64()?, + }), + EVENT_REQUEST_TAG_CONSUME => EventRequest::Consume(ConsumeEventRequest { + handle: decoder.handle()?, + mode: decode_consume_mode(decoder)?, + }), _ => return Err(WireError::InvalidTag), }; @@ -91,19 +96,19 @@ pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventRespon pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result { let response = match decoder.u8()? { - EVENT_RESPONSE_TAG_CREATED => { - EventResponse::Create(CreateEventResponse::new(decoder.handle()?)) - } - EVENT_RESPONSE_TAG_WAITED => { - EventResponse::Wait(WaitEventResponse::new(decode_wait_outcome(decoder)?)) - } - EVENT_RESPONSE_TAG_ADDED => { - EventResponse::Add(AddEventResponse::new(decode_readiness(decoder)?)) - } - EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(ConsumeEventResponse::new( - decoder.u64()?, - decode_readiness(decoder)?, - )), + EVENT_RESPONSE_TAG_CREATED => EventResponse::Create(CreateEventResponse { + handle: decoder.handle()?, + }), + EVENT_RESPONSE_TAG_WAITED => EventResponse::Wait(WaitEventResponse { + outcome: decode_wait_outcome(decoder)?, + }), + EVENT_RESPONSE_TAG_ADDED => EventResponse::Add(AddEventResponse { + readiness: decode_readiness(decoder)?, + }), + EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(EventConsumption { + value: decoder.u64()?, + readiness: decode_readiness(decoder)?, + }), _ => return Err(WireError::InvalidTag), }; @@ -137,7 +142,10 @@ fn encode_readiness(encoder: &mut Encoder, readiness: ReadinessState) { } fn decode_readiness(decoder: &mut Decoder<'_>) -> Result { - Ok(ReadinessState::new(decoder.bool()?, decoder.bool()?)) + Ok(ReadinessState { + read_ready: decoder.bool()?, + write_ready: decoder.bool()?, + }) } fn encode_consume_mode(encoder: &mut Encoder, mode: EventConsumeMode) { diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs index 6d6d7d45a7..b038804306 100644 --- a/litebox_broker_protocol/src/wire/primitive.rs +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -86,7 +86,7 @@ impl<'a> Decoder<'a> { } pub(super) fn protocol_version(&mut self) -> Result { - Ok(ProtocolVersion::new(self.u16()?)) + Ok(ProtocolVersion(self.u16()?)) } pub(super) fn handle(&mut self) -> Result { diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 052c3b972e..962d961393 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -39,17 +39,26 @@ fn separate_process_broker_serves_event_object_requests() { let handle = local.create_event().unwrap(); assert_eq!( local.wait_event(handle).unwrap(), - WaitOutcome::WouldBlock(ReadinessState::new(false, true)) + WaitOutcome::WouldBlock(ReadinessState { + read_ready: false, + write_ready: true, + }) ); assert_eq!( local.add_event(handle, 1).unwrap(), - ReadinessState::new(true, true) + ReadinessState { + read_ready: true, + write_ready: true, + } ); assert_eq!( local.wait_event(handle).unwrap(), - WaitOutcome::Ready(ReadinessState::new(true, true)) + WaitOutcome::Ready(ReadinessState { + read_ready: true, + write_ready: true, + }) ); drop(local); assert!(child.wait().unwrap().success()); From 6d21451d707d43d8b5b7abee3657b1aab36e62cc Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 23 Jun 2026 18:16:39 -0700 Subject: [PATCH 053/319] Clean up broker protocol enums (#951) Simplify broker protocol enums by removing redundant `non_exhaustive` markers, the `WaitOutcome` wrapper, and the `ErrorCode::Unknown` variant. Event waits now return `ReadinessState` directly, and unknown raw broker error codes decode as invalid wire tags. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 1 - litebox/src/event/counter.rs | 16 ++++------ litebox/src/pipes.rs | 1 - litebox/src/sync/lock_tracing.rs | 1 - litebox_broker_core/src/event.rs | 22 ++++--------- litebox_broker_core/src/session.rs | 6 ++-- litebox_broker_host/src/lib.rs | 9 ++---- litebox_broker_local/src/event.rs | 6 ++-- litebox_broker_local/src/lib.rs | 4 +++ litebox_broker_protocol/src/error.rs | 31 +++++++------------ litebox_broker_protocol/src/event.rs | 15 ++------- litebox_broker_protocol/src/lib.rs | 2 +- litebox_broker_protocol/src/message.rs | 6 ---- litebox_broker_protocol/src/wire.rs | 16 +++++----- litebox_broker_protocol/src/wire/event.rs | 29 ++--------------- .../tests/userland_broker.rs | 10 +++--- 16 files changed, 56 insertions(+), 119 deletions(-) diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 353f8866da..e53a39bde0 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -8,7 +8,6 @@ use crate::event::{counter::EventCounterError, polling::TryOpError}; /// Error returned by the deployment-provided broker control path. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] pub(crate) enum BrokerControlError { #[error("broker control transport failed")] Transport, diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index f417ee39e2..041149abb6 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -7,7 +7,7 @@ pub use litebox_broker_protocol::EventConsumeMode as EventCounterReadMode; use litebox_broker_protocol::{ AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, EventRequest, EventResponse, ObjectHandle, ReadinessState, - WaitEventRequest, WaitOutcome, + WaitEventRequest, }; use thiserror::Error; @@ -64,7 +64,7 @@ where CreateEventRequest { initial_count }, ))) .map_err(BrokerObjectError::from) - .and_then(event_response_from_core) + .map(event_response_from_core) .map_err(EventCounterError::from)?; let EventResponse::Create(response) = response else { return Err(BrokerObjectError::UnexpectedResponse.into()); @@ -140,7 +140,7 @@ where self.broker .request(CoreRequest::Event(request)) .map_err(BrokerObjectError::from) - .and_then(event_response_from_core) + .map(event_response_from_core) } } @@ -161,10 +161,7 @@ where let EventResponse::Wait(response) = response else { return Events::empty(); }; - let (WaitOutcome::Ready(readiness) | WaitOutcome::WouldBlock(readiness)) = response.outcome - else { - return Events::empty(); - }; + let readiness = response.readiness; let mut events = Events::empty(); if readiness.read_ready { events |= Events::IN; @@ -176,9 +173,8 @@ where } } -fn event_response_from_core(response: CoreResponse) -> Result { +fn event_response_from_core(response: CoreResponse) -> EventResponse { match response { - CoreResponse::Event(response) => Ok(response), - _ => Err(BrokerObjectError::UnexpectedResponse), + CoreResponse::Event(response) => response, } } diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index edda8b1945..8b685d7f9d 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -342,7 +342,6 @@ struct WriteEnd { /// Potential errors when writing or reading from a pipe #[derive(Error, Debug)] -#[non_exhaustive] enum PipeError { #[error("this end has been shut down")] ThisEndShutdown, diff --git a/litebox/src/sync/lock_tracing.rs b/litebox/src/sync/lock_tracing.rs index c7b4abf7f0..a0aa929a6f 100644 --- a/litebox/src/sync/lock_tracing.rs +++ b/litebox/src/sync/lock_tracing.rs @@ -88,7 +88,6 @@ const CONFIG_ENABLE_RECORDING: bool = true; const CONFIG_MAX_RECORDED_EVENTS: usize = 1_000_000; /// The kind of lock that has been applied, either for locking or unlocking. -#[non_exhaustive] #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub(crate) enum LockType { RwLock, diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 67040bdb83..acb19efbf3 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -5,9 +5,7 @@ use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result}; -use litebox_broker_protocol::{ - EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, -}; +use litebox_broker_protocol::{EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState}; pub(crate) const MAX_EVENT_COUNT: u64 = u64::MAX - 1; @@ -25,20 +23,13 @@ pub fn create(session: &BrokerSession, initial_count: u64) -> Result Result { +pub fn wait(session: &BrokerSession, handle: ObjectHandle) -> Result { let required_rights = ObjectRights::WAIT; session.with_authorized_object(handle, required_rights, |object| match object { - ObjectEntry::Event(event) => { - let readiness = ReadinessState { - read_ready: event.count > 0, - write_ready: event.count < MAX_EVENT_COUNT, - }; - Ok(if readiness.read_ready { - WaitOutcome::Ready(readiness) - } else { - WaitOutcome::WouldBlock(readiness) - }) - } + ObjectEntry::Event(event) => Ok(ReadinessState { + read_ready: event.count > 0, + write_ready: event.count < MAX_EVENT_COUNT, + }), }) } @@ -92,7 +83,6 @@ impl EventObject { let value = match mode { EventConsumeMode::All => self.count, EventConsumeMode::One => 1, - _ => return Err(BrokerError::UnsupportedOperation), }; self.count -= value; Ok(EventConsumption { diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index b0634c89d2..ece0e32ac5 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -182,7 +182,7 @@ mod tests { event, }; use litebox_broker_protocol::{ - EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, + EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, }; #[test] @@ -214,10 +214,10 @@ mod tests { assert_eq!( event::wait(&session, handle), - Ok(WaitOutcome::WouldBlock(ReadinessState { + Ok(ReadinessState { read_ready: false, write_ready: true, - })) + }) ); assert_eq!( event::add(&session, handle, 1), diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index ac62dd661c..a35d875c9d 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -99,7 +99,7 @@ fn handle_request( }) } } - _ => BrokerDispatch::close_after( + BrokerRequest::Core(_) => BrokerDispatch::close_after( BrokerResponse::Error(ErrorCode::ProtocolState), CloseReason::ProtocolViolation, ), @@ -117,14 +117,12 @@ fn handle_active_request(session: &BrokerSession, request: BrokerRequest) -> Bro BrokerRequest::Core(request) => { BrokerDispatch::continue_after(handle_core_request(session, request)) } - _ => BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)), } } fn handle_core_request(session: &BrokerSession, request: CoreRequest) -> BrokerResponse { match request { CoreRequest::Event(request) => handle_event_request(session, request), - _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), } } @@ -138,9 +136,9 @@ fn handle_event_request(session: &BrokerSession, request: EventRequest) -> Broke }) } EventRequest::Wait(request) => { - handle_core_result(event::wait(session, request.handle), |outcome| { + handle_core_result(event::wait(session, request.handle), |readiness| { BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( - WaitEventResponse { outcome }, + WaitEventResponse { readiness }, ))) }) } @@ -158,7 +156,6 @@ fn handle_event_request(session: &BrokerSession, request: EventRequest) -> Broke BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(consumption))) }, ), - _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), } } diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index c3626344cb..1a538735b6 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -4,7 +4,7 @@ use litebox_broker_protocol::{ AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, - LocalControlChannel, ObjectHandle, ReadinessState, WaitEventRequest, WaitOutcome, + LocalControlChannel, ObjectHandle, ReadinessState, WaitEventRequest, }; use crate::{BrokerLocal, BrokerLocalError, Result}; @@ -31,12 +31,12 @@ impl BrokerLocal { } /// Checks whether an event wait would complete now. - pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { + pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { match self.request(event_request(EventRequest::Wait(WaitEventRequest { handle, })))? { BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response))) => { - Ok(response.outcome) + Ok(response.readiness) } response => Err(BrokerLocalError::UnexpectedResponse(response)), } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 6fba854114..41ace4cf43 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -52,6 +52,10 @@ impl BrokerLocal { /// Sends one broker request. /// /// Negotiation is the only request allowed before the connection is active. + #[expect( + clippy::match_wildcard_for_single_variants, + reason = "wildcards keep state-machine fallbacks grouped by behavior" + )] pub fn request(&mut self, request: BrokerRequest) -> Result { match self.state { ConnectionState::AwaitingNegotiation => match request { diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index 7ec946eaf2..ae94cd0e0a 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -27,12 +27,6 @@ pub enum ErrorCode { ResourceExhausted, #[error("broker operation would block")] WouldBlock, - /// Error code emitted by a newer broker and not understood by this local peer. - /// - /// This variant is reserved for raw codes not assigned by this protocol - /// version. - #[error("unknown broker error code {0}")] - Unknown(u16), } impl ErrorCode { @@ -42,19 +36,19 @@ impl ErrorCode { /// concrete broker errors. /// /// Converts a raw protocol error code to an error category. - pub const fn from_raw(raw: u16) -> Self { + pub const fn from_raw(raw: u16) -> Option { match raw { - 1 => Self::UnsupportedVersion, - 2 => Self::MalformedRequest, - 3 => Self::ProtocolState, - 4 => Self::UnsupportedOperation, - 5 => Self::Internal, - 6 => Self::PolicyDenied, - 7 => Self::UnknownObject, - 8 => Self::InvalidRights, - 9 => Self::ResourceExhausted, - 10 => Self::WouldBlock, - raw => Self::Unknown(raw), + 1 => Some(Self::UnsupportedVersion), + 2 => Some(Self::MalformedRequest), + 3 => Some(Self::ProtocolState), + 4 => Some(Self::UnsupportedOperation), + 5 => Some(Self::Internal), + 6 => Some(Self::PolicyDenied), + 7 => Some(Self::UnknownObject), + 8 => Some(Self::InvalidRights), + 9 => Some(Self::ResourceExhausted), + 10 => Some(Self::WouldBlock), + _ => None, } } @@ -71,7 +65,6 @@ impl ErrorCode { Self::InvalidRights => 8, Self::ResourceExhausted => 9, Self::WouldBlock => 10, - Self::Unknown(raw) => raw, } } } diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs index e29e91d824..1cf506d661 100644 --- a/litebox_broker_protocol/src/event.rs +++ b/litebox_broker_protocol/src/event.rs @@ -12,19 +12,8 @@ pub struct ReadinessState { pub write_ready: bool, } -/// Result of checking whether a broker event read wait would complete now. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum WaitOutcome { - /// The object is read-ready now. - Ready(ReadinessState), - /// The object is not read-ready; deployment-specific wait plumbing may block. - WouldBlock(ReadinessState), -} - /// How a broker event consume operation should remove readiness credits. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] pub enum EventConsumeMode { /// Consume all currently available credits. All, @@ -56,8 +45,8 @@ pub struct WaitEventRequest { /// Response to an event wait request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WaitEventResponse { - /// Current wait outcome. - pub outcome: WaitOutcome, + /// Current readiness state. + pub readiness: ReadinessState, } /// Request to add readiness credits to an event. diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 5ee814bda2..dc36d17d2a 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -23,7 +23,7 @@ pub use error::ErrorCode; pub use event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, - WaitEventRequest, WaitEventResponse, WaitOutcome, + WaitEventRequest, WaitEventResponse, }; pub use message::{ BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index e467f2dd32..adb870e801 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -13,7 +13,6 @@ use crate::{ /// domain-specific operations are grouped below it so new object families do not /// accumulate as unrelated top-level broker variants. #[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] pub enum BrokerRequest { /// Protocol negotiation request. Negotiate { @@ -26,7 +25,6 @@ pub enum BrokerRequest { /// Request adapted by the broker host into a BrokerCore domain call. #[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] pub enum CoreRequest { /// Event object request family. Event(EventRequest), @@ -34,7 +32,6 @@ pub enum CoreRequest { /// Broker-owned event object request. #[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] pub enum EventRequest { /// Create a broker-owned event object. Create(CreateEventRequest), @@ -52,7 +49,6 @@ pub enum EventRequest { /// grouped under [`CoreResponse`] so future object families can evolve without /// turning the broker envelope into a flat operation/result list. #[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] pub enum BrokerResponse { /// Negotiation result. Negotiated { @@ -79,7 +75,6 @@ pub enum BrokerResponse { /// Response returned by a BrokerCore domain request. #[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] pub enum CoreResponse { /// Event object response family. Event(EventResponse), @@ -87,7 +82,6 @@ pub enum CoreResponse { /// Broker-owned event object response. #[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] pub enum EventResponse { /// Create operation response. Create(CreateEventResponse), diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 360879ad2c..65c655bca1 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -131,7 +131,7 @@ pub fn decode_response(frame: &[u8]) -> Result { BrokerResponse::Core(core_message::decode_core_response(&mut decoder)?) } RESPONSE_TAG_ERROR => { - let error = ErrorCode::from_raw(decoder.u16()?); + let error = ErrorCode::from_raw(decoder.u16()?).ok_or(WireError::InvalidTag)?; BrokerResponse::Error(error) } _ => return Err(WireError::InvalidTag), @@ -147,7 +147,7 @@ mod tests { AddEventRequest, AddEventResponse, ConsumeEventRequest, CoreRequest, CoreResponse, CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, EventRequest, EventResponse, ObjectHandle, ProtocolVersion, ReadinessState, WaitEventRequest, - WaitEventResponse, WaitOutcome, + WaitEventResponse, }; #[test] @@ -195,16 +195,16 @@ mod tests { }, event_response(EventResponse::Create(CreateEventResponse { handle })), event_response(EventResponse::Wait(WaitEventResponse { - outcome: WaitOutcome::Ready(ReadinessState { + readiness: ReadinessState { read_ready: true, write_ready: false, - }), + }, })), event_response(EventResponse::Wait(WaitEventResponse { - outcome: WaitOutcome::WouldBlock(ReadinessState { + readiness: ReadinessState { read_ready: false, write_ready: true, - }), + }, })), event_response(EventResponse::Add(AddEventResponse { readiness: ReadinessState { @@ -261,11 +261,11 @@ mod tests { ); assert_eq!( decode_response(&[1, 0, 1, 0xff]), - Err(WireError::InvalidTag) + Err(WireError::InvalidBoolean) ); assert_eq!( decode_response(&[2, 0xff, 0xff]), - Ok(BrokerResponse::Error(ErrorCode::Unknown(0xffff))) + Err(WireError::InvalidTag) ); let mut invalid_bool = [1, 0, 2, 2, 0]; diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index 7f308a774a..c96a2d74e6 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -4,7 +4,7 @@ use crate::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, EventRequest, EventResponse, - ReadinessState, WaitEventRequest, WaitEventResponse, WaitOutcome, + ReadinessState, WaitEventRequest, WaitEventResponse, }; use super::WireError; @@ -22,8 +22,6 @@ const EVENT_RESPONSE_TAG_WAITED: u8 = 1; const EVENT_RESPONSE_TAG_ADDED: u8 = 2; const EVENT_RESPONSE_TAG_CONSUMED: u8 = 3; -const WAIT_OUTCOME_TAG_READY: u8 = 1; -const WAIT_OUTCOME_TAG_WOULD_BLOCK: u8 = 2; const EVENT_CONSUME_MODE_TAG_ALL: u8 = 1; const EVENT_CONSUME_MODE_TAG_ONE: u8 = 2; @@ -80,7 +78,7 @@ pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventRespon } EventResponse::Wait(response) => { encoder.u8(EVENT_RESPONSE_TAG_WAITED); - encode_wait_outcome(encoder, response.outcome); + encode_readiness(encoder, response.readiness); } EventResponse::Add(response) => { encoder.u8(EVENT_RESPONSE_TAG_ADDED); @@ -100,7 +98,7 @@ pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result EventResponse::Wait(WaitEventResponse { - outcome: decode_wait_outcome(decoder)?, + readiness: decode_readiness(decoder)?, }), EVENT_RESPONSE_TAG_ADDED => EventResponse::Add(AddEventResponse { readiness: decode_readiness(decoder)?, @@ -115,27 +113,6 @@ pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result { - encoder.u8(WAIT_OUTCOME_TAG_READY); - encode_readiness(encoder, readiness); - } - WaitOutcome::WouldBlock(readiness) => { - encoder.u8(WAIT_OUTCOME_TAG_WOULD_BLOCK); - encode_readiness(encoder, readiness); - } - } -} - -fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result { - match decoder.u8()? { - WAIT_OUTCOME_TAG_READY => Ok(WaitOutcome::Ready(decode_readiness(decoder)?)), - WAIT_OUTCOME_TAG_WOULD_BLOCK => Ok(WaitOutcome::WouldBlock(decode_readiness(decoder)?)), - _ => Err(WireError::InvalidTag), - } -} - fn encode_readiness(encoder: &mut Encoder, readiness: ReadinessState) { encoder.bool(readiness.read_ready); encoder.bool(readiness.write_ready); diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 962d961393..883b65f38f 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -11,7 +11,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::{ - BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, ReadinessState, WaitOutcome, + BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, ReadinessState, }; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; @@ -39,10 +39,10 @@ fn separate_process_broker_serves_event_object_requests() { let handle = local.create_event().unwrap(); assert_eq!( local.wait_event(handle).unwrap(), - WaitOutcome::WouldBlock(ReadinessState { + ReadinessState { read_ready: false, write_ready: true, - }) + } ); assert_eq!( @@ -55,10 +55,10 @@ fn separate_process_broker_serves_event_object_requests() { assert_eq!( local.wait_event(handle).unwrap(), - WaitOutcome::Ready(ReadinessState { + ReadinessState { read_ready: true, write_ready: true, - }) + } ); drop(local); assert!(child.wait().unwrap().success()); From 04ec01a9e1ff05449d72f712e3e1f4473e295f85 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 24 Jun 2026 08:14:02 -0700 Subject: [PATCH 054/319] Make broker local negotiation explicit (#952) Make `BrokerLocal` negotiated-by-construction so active broker requests cannot run before negotiation. Remove unrecoverable negotiation-state errors from the public local adapter API. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 15 +- litebox_broker_local/src/error.rs | 36 +-- litebox_broker_local/src/event.rs | 67 +++--- litebox_broker_local/src/lib.rs | 222 ++++++------------ .../tests/userland_broker.rs | 17 +- litebox_runner_linux_userland/src/broker.rs | 8 +- 6 files changed, 120 insertions(+), 245 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 64b5eb03b9..8607c4b632 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -2,9 +2,7 @@ // Licensed under the MIT license. use litebox_broker_local::{BrokerLocal, BrokerLocalError}; -use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, LocalControlChannel, -}; +use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; @@ -51,15 +49,14 @@ where let response = self .local .lock() - .request(BrokerRequest::Core(request)) + .request(request) .map_err(|error| match error { + BrokerLocalError::Channel(_) | BrokerLocalError::ChannelClosed => { + BrokerControlError::Transport + } BrokerLocalError::Broker(error) => BrokerControlError::Broker(error), BrokerLocalError::UnexpectedResponse(_) => BrokerControlError::UnexpectedResponse, - _ => BrokerControlError::Transport, })?; - match response { - BrokerResponse::Core(response) => Ok(response), - _ => Err(BrokerControlError::UnexpectedResponse), - } + Ok(response) } } diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 724b8a8898..98201c3653 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -1,48 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox_broker_protocol::{BrokerResponse, ErrorCode, ProtocolVersion}; +use litebox_broker_protocol::{BrokerResponse, ErrorCode}; use thiserror::Error; -/// Errors returned by the broker-local control adapter. +/// Errors returned by active broker-local control requests. #[derive(Debug, Error)] -#[non_exhaustive] pub enum BrokerLocalError { #[error("broker channel failed: {0}")] Channel(#[source] E), - #[error("broker local adapter has not negotiated protocol version")] - NotNegotiated, - #[error("broker local adapter already negotiated")] - AlreadyNegotiated, #[error("broker closed the channel")] ChannelClosed, - #[error( - "broker accepted incompatible protocol negotiation: requested {requested:?}, broker supports {broker_protocol_version:?}" - )] - IncompatibleNegotiation { - /// Protocol version requested by this local adapter. - requested: ProtocolVersion, - /// Protocol version advertised by the broker. - broker_protocol_version: ProtocolVersion, - }, - #[error( - "broker local adapter cannot request protocol version {requested:?}; local adapter supports {local_protocol_version:?}" - )] - UnsupportedLocalVersion { - /// Protocol version requested by the caller. - requested: ProtocolVersion, - /// Protocol version supported by this local implementation. - local_protocol_version: ProtocolVersion, - }, - #[error( - "broker does not support requested protocol version {requested:?}; broker supports {broker_protocol_version:?}" - )] - UnsupportedVersion { - /// Protocol version requested by this local adapter. - requested: ProtocolVersion, - /// Protocol version advertised by the broker. - broker_protocol_version: ProtocolVersion, - }, #[error("broker rejected request: {0}")] Broker(#[source] ErrorCode), #[error("broker returned unexpected response: {0:?}")] diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 1a538735b6..2851289fd3 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -2,8 +2,8 @@ // Licensed under the MIT license. use litebox_broker_protocol::{ - AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, - CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, + AddEventRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, + CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, LocalControlChannel, ObjectHandle, ReadinessState, WaitEventRequest, }; @@ -20,25 +20,24 @@ impl BrokerLocal { &mut self, initial_count: u64, ) -> Result { - match self.request(event_request(EventRequest::Create(CreateEventRequest { - initial_count, - })))? { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { - Ok(response.handle) - } - response => Err(BrokerLocalError::UnexpectedResponse(response)), + let response = + self.request_event(EventRequest::Create(CreateEventRequest { initial_count }))?; + match response { + EventResponse::Create(response) => Ok(response.handle), + response => Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Core( + CoreResponse::Event(response), + ))), } } /// Checks whether an event wait would complete now. pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { - match self.request(event_request(EventRequest::Wait(WaitEventRequest { - handle, - })))? { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response))) => { - Ok(response.readiness) - } - response => Err(BrokerLocalError::UnexpectedResponse(response)), + let response = self.request_event(EventRequest::Wait(WaitEventRequest { handle }))?; + match response { + EventResponse::Wait(response) => Ok(response.readiness), + response => Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Core( + CoreResponse::Event(response), + ))), } } @@ -48,14 +47,12 @@ impl BrokerLocal { handle: ObjectHandle, value: u64, ) -> Result { - match self.request(event_request(EventRequest::Add(AddEventRequest { - handle, - value, - })))? { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(response))) => { - Ok(response.readiness) - } - response => Err(BrokerLocalError::UnexpectedResponse(response)), + let response = self.request_event(EventRequest::Add(AddEventRequest { handle, value }))?; + match response { + EventResponse::Add(response) => Ok(response.readiness), + response => Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Core( + CoreResponse::Event(response), + ))), } } @@ -65,18 +62,18 @@ impl BrokerLocal { handle: ObjectHandle, mode: EventConsumeMode, ) -> Result { - match self.request(event_request(EventRequest::Consume(ConsumeEventRequest { - handle, - mode, - })))? { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(response))) => { - Ok(response) - } - response => Err(BrokerLocalError::UnexpectedResponse(response)), + let response = + self.request_event(EventRequest::Consume(ConsumeEventRequest { handle, mode }))?; + match response { + EventResponse::Consume(response) => Ok(response), + response => Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Core( + CoreResponse::Event(response), + ))), } } -} -const fn event_request(request: EventRequest) -> BrokerRequest { - BrokerRequest::Core(CoreRequest::Event(request)) + fn request_event(&mut self, request: EventRequest) -> Result { + let CoreResponse::Event(response) = self.request(CoreRequest::Event(request))?; + Ok(response) + } } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 41ace4cf43..72b6b30592 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -16,7 +16,8 @@ mod error; mod event; use litebox_broker_protocol::{ - BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, LocalControlChannel, + BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, ErrorCode, + LocalControlChannel, }; pub use error::{BrokerLocalError, Result}; @@ -24,200 +25,131 @@ pub use error::{BrokerLocalError, Result}; /// Typed broker-local control adapter for broker operations. pub struct BrokerLocal { channel: T, - state: ConnectionState, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ConnectionState { - AwaitingNegotiation, - Active, -} - -impl BrokerLocal { - /// Creates a broker-local control adapter over an already-connected control channel. - pub const fn new(channel: T) -> Self { - Self { - channel, - state: ConnectionState::AwaitingNegotiation, - } - } - +impl BrokerLocal { /// Returns the underlying control channel for deployment-specific configuration. pub fn control_channel_mut(&mut self) -> &mut T { &mut self.channel } -} -impl BrokerLocal { - /// Sends one broker request. - /// - /// Negotiation is the only request allowed before the connection is active. - #[expect( - clippy::match_wildcard_for_single_variants, - reason = "wildcards keep state-machine fallbacks grouped by behavior" - )] - pub fn request(&mut self, request: BrokerRequest) -> Result { - match self.state { - ConnectionState::AwaitingNegotiation => match request { - BrokerRequest::Negotiate { protocol_version } => { - if protocol_version != BROKER_PROTOCOL_VERSION { - return Err(BrokerLocalError::UnsupportedLocalVersion { - requested: protocol_version, - local_protocol_version: BROKER_PROTOCOL_VERSION, - }); - } - - match self.raw_request(BrokerRequest::Negotiate { protocol_version })? { - BrokerResponse::Negotiated { - broker_protocol_version, - } => { - if protocol_version != broker_protocol_version { - return Err(BrokerLocalError::IncompatibleNegotiation { - requested: protocol_version, - broker_protocol_version, - }); - } - self.state = ConnectionState::Active; - Ok(BrokerResponse::Negotiated { - broker_protocol_version, - }) - } - BrokerResponse::VersionMismatch { - broker_protocol_version, - } => Err(BrokerLocalError::UnsupportedVersion { - requested: protocol_version, - broker_protocol_version, - }), - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response => Err(BrokerLocalError::UnexpectedResponse(response)), - } - } - _ => Err(BrokerLocalError::NotNegotiated), - }, - ConnectionState::Active => match request { - BrokerRequest::Negotiate { .. } => Err(BrokerLocalError::AlreadyNegotiated), - request => match self.raw_request(request)? { - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response => Ok(response), - }, + /// Negotiates the broker protocol over an already-connected control channel. + pub fn negotiate(mut channel: T) -> Result { + let requested = BROKER_PROTOCOL_VERSION; + match raw_request( + &mut channel, + BrokerRequest::Negotiate { + protocol_version: requested, }, + )? { + response @ BrokerResponse::Negotiated { + broker_protocol_version, + } => { + if requested != broker_protocol_version { + return Err(BrokerLocalError::UnexpectedResponse(response)); + } + Ok(Self { channel }) + } + BrokerResponse::VersionMismatch { .. } => { + Err(BrokerLocalError::Broker(ErrorCode::UnsupportedVersion)) + } + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response @ BrokerResponse::Core(_) => { + Err(BrokerLocalError::UnexpectedResponse(response)) + } } } - fn raw_request(&mut self, request: BrokerRequest) -> Result { - self.channel - .send_request(&request) - .map_err(BrokerLocalError::Channel)?; - self.channel - .recv_response() - .map_err(BrokerLocalError::Channel)? - .ok_or(BrokerLocalError::ChannelClosed) + /// Sends one active BrokerCore request. + pub fn request(&mut self, request: CoreRequest) -> Result { + match raw_request(&mut self.channel, BrokerRequest::Core(request))? { + BrokerResponse::Core(response) => Ok(response), + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } } } +fn raw_request( + channel: &mut T, + request: BrokerRequest, +) -> Result { + channel + .send_request(&request) + .map_err(BrokerLocalError::Channel)?; + channel + .recv_response() + .map_err(BrokerLocalError::Channel)? + .ok_or(BrokerLocalError::ChannelClosed) +} + #[cfg(test)] mod tests { use super::*; use core::convert::Infallible; - use litebox_broker_protocol::ProtocolVersion; - - #[test] - fn event_operations_require_negotiation_without_sending() { - let channel = FakeControlChannel::new(None); - let mut local = BrokerLocal::new(channel); - - assert!(matches!( - local.create_event(), - Err(BrokerLocalError::NotNegotiated) - )); - assert_eq!(local.channel.sent_request, None); - } + use litebox_broker_protocol::{ + CreateEventRequest, CreateEventResponse, EventRequest, EventResponse, ObjectHandle, + ProtocolVersion, + }; #[test] - fn negotiation_request_activates_local_connection() { - let requested = BROKER_PROTOCOL_VERSION; + fn negotiate_returns_active_local_connection() { let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { broker_protocol_version: BROKER_PROTOCOL_VERSION, })); - let mut local = BrokerLocal::new(channel); + let local = BrokerLocal::negotiate(channel).unwrap(); - assert_eq!( - local - .request(BrokerRequest::Negotiate { - protocol_version: requested - }) - .unwrap(), - BrokerResponse::Negotiated { - broker_protocol_version: BROKER_PROTOCOL_VERSION - } - ); assert_eq!( local.channel.sent_request, Some(BrokerRequest::Negotiate { - protocol_version: requested + protocol_version: BROKER_PROTOCOL_VERSION }) ); - assert_eq!(local.state, ConnectionState::Active); } #[test] - fn negotiation_request_rejects_locally_unsupported_version_without_sending() { - let too_new = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); - let channel = FakeControlChannel::new(None); - let mut local = BrokerLocal::new(channel); + fn active_request_sends_core_request() { + let handle = ObjectHandle(7); + let request = CoreRequest::Event(EventRequest::Create(CreateEventRequest { + initial_count: 0, + })); + let response = CoreResponse::Event(EventResponse::Create(CreateEventResponse { handle })); + let channel = FakeControlChannel::new(Some(BrokerResponse::Core(response.clone()))); + let mut local = BrokerLocal { channel }; - assert!(matches!( - local.request(BrokerRequest::Negotiate { - protocol_version: too_new - }), - Err(BrokerLocalError::UnsupportedLocalVersion { - requested, - local_protocol_version - }) if requested == too_new && local_protocol_version == BROKER_PROTOCOL_VERSION - )); - assert_eq!(local.state, ConnectionState::AwaitingNegotiation); - assert_eq!(local.channel.sent_request, None); + assert_eq!(local.request(request.clone()).unwrap(), response); + assert_eq!( + local.channel.sent_request, + Some(BrokerRequest::Core(request)) + ); } #[test] - fn negotiation_request_rejects_broker_different_version_response() { + fn negotiate_rejects_broker_different_version_response() { let broker_protocol_version = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { broker_protocol_version, })); - let mut local = BrokerLocal::new(channel); assert!(matches!( - local.request(BrokerRequest::Negotiate { - protocol_version: BROKER_PROTOCOL_VERSION - }), - Err(BrokerLocalError::IncompatibleNegotiation { - requested, + BrokerLocal::negotiate(channel), + Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Negotiated { broker_protocol_version: broker - }) if requested == BROKER_PROTOCOL_VERSION && broker == broker_protocol_version + })) if broker == broker_protocol_version )); - assert_eq!(local.state, ConnectionState::AwaitingNegotiation); - assert_eq!( - local.channel.sent_request, - Some(BrokerRequest::Negotiate { - protocol_version: BROKER_PROTOCOL_VERSION - }) - ); } #[test] - fn active_connection_rejects_negotiation_without_sending() { - let channel = FakeControlChannel::new(None); - let mut local = BrokerLocal::new(channel); - local.state = ConnectionState::Active; + fn negotiate_rejects_broker_unsupported_version_response() { + let broker_protocol_version = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); + let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { + broker_protocol_version, + })); assert!(matches!( - local.request(BrokerRequest::Negotiate { - protocol_version: BROKER_PROTOCOL_VERSION - }), - Err(BrokerLocalError::AlreadyNegotiated) + BrokerLocal::negotiate(channel), + Err(BrokerLocalError::Broker(ErrorCode::UnsupportedVersion)) )); - assert_eq!(local.channel.sent_request, None); } struct FakeControlChannel { diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 883b65f38f..d0ccb967c0 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -10,9 +10,7 @@ use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::{ - BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, ReadinessState, -}; +use litebox_broker_protocol::ReadinessState; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; #[test] @@ -23,18 +21,7 @@ fn separate_process_broker_serves_event_object_requests() { channel .set_io_timeout(Some(Duration::from_secs(5))) .unwrap(); - let mut local = BrokerLocal::new(channel); - - assert_eq!( - local - .request(BrokerRequest::Negotiate { - protocol_version: BROKER_PROTOCOL_VERSION, - }) - .unwrap(), - BrokerResponse::Negotiated { - broker_protocol_version: BROKER_PROTOCOL_VERSION - } - ); + let mut local = BrokerLocal::negotiate(channel).unwrap(); let handle = local.create_event().unwrap(); assert_eq!( diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index f7e5b38c08..1d7a2525e7 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -9,7 +9,6 @@ use std::{ use anyhow::{Context as _, Result}; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, BrokerRequest}; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -52,12 +51,7 @@ fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { From 6379f28fdbb9d8f50e8689ce8bc271d428c402c8 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 24 Jun 2026 08:43:34 -0700 Subject: [PATCH 055/319] Use From for broker error conversions (#953) Replace ad hoc broker error mapping helpers with `From` conversions. Remove stale broker host association setup errors and let broker setup failures flow through normal broker error conversion. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 24 ++++++++++++++------- litebox/src/broker/mod.rs | 15 ++----------- litebox/src/event/counter.rs | 9 +++----- litebox_broker_core/src/error.rs | 16 ++++++++++++++ litebox_broker_host/src/error.rs | 12 +++++++++-- litebox_broker_host/src/lib.rs | 36 +++++++------------------------- 6 files changed, 55 insertions(+), 57 deletions(-) diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index e53a39bde0..3cd575c425 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use litebox_broker_local::BrokerLocalError; use litebox_broker_protocol::ErrorCode; use thiserror::Error; @@ -58,13 +59,22 @@ impl From for BrokerObjectError { } } -pub(crate) fn map_broker_object_result( - result: Result, -) -> Result> { - match result { - Ok(value) => Ok(value), - Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), - Err(error) => Err(TryOpError::Other(error.into())), +impl From> for BrokerControlError { + fn from(error: BrokerLocalError) -> Self { + match error { + BrokerLocalError::Channel(_) | BrokerLocalError::ChannelClosed => Self::Transport, + BrokerLocalError::Broker(error) => Self::Broker(error), + BrokerLocalError::UnexpectedResponse(_) => Self::UnexpectedResponse, + } + } +} + +impl From for TryOpError { + fn from(error: BrokerObjectError) -> Self { + match error { + BrokerObjectError::WouldBlock => Self::TryAgain, + error => Self::Other(error.into()), + } } } diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 8607c4b632..ab27914b7c 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox_broker_local::{BrokerLocal, BrokerLocalError}; +use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; @@ -46,17 +46,6 @@ where &self, request: CoreRequest, ) -> core::result::Result { - let response = self - .local - .lock() - .request(request) - .map_err(|error| match error { - BrokerLocalError::Channel(_) | BrokerLocalError::ChannelClosed => { - BrokerControlError::Transport - } - BrokerLocalError::Broker(error) => BrokerControlError::Broker(error), - BrokerLocalError::UnexpectedResponse(_) => BrokerControlError::UnexpectedResponse, - })?; - Ok(response) + Ok(self.local.lock().request(request)?) } } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 041149abb6..4a73d6a6de 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -13,10 +13,7 @@ use thiserror::Error; use crate::{ LiteBox, - broker::{ - BrokerControl, - error::{BrokerObjectError, map_broker_object_result}, - }, + broker::{BrokerControl, error::BrokerObjectError}, event::{ Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, wait::WaitContext, @@ -84,7 +81,7 @@ where mode: EventCounterReadMode, ) -> Result> { self.pollee.wait(cx, nonblock, Events::IN, || { - let response = map_broker_object_result(self.consume(mode))?; + let response = self.consume(mode)?; if response.readiness.write_ready { self.pollee.notify_observers(Events::OUT); } @@ -103,7 +100,7 @@ where return Err(TryOpError::Other(EventCounterError::InvalidInput)); } self.pollee.wait(cx, nonblock, Events::OUT, || { - let readiness = map_broker_object_result(self.add(value))?; + let readiness = self.add(value)?; if value != 0 && readiness.read_ready { self.pollee.notify_observers(Events::IN); } diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index 1416ff7cdc..7c45916d9f 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -3,6 +3,8 @@ use thiserror::Error; +use litebox_broker_protocol::ErrorCode; + /// Broker authority error category. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq, Hash)] #[non_exhaustive] @@ -22,3 +24,17 @@ pub enum BrokerError { #[error("unsupported broker operation")] UnsupportedOperation, } + +impl From for ErrorCode { + fn from(error: BrokerError) -> Self { + match error { + BrokerError::PolicyDenied => Self::PolicyDenied, + BrokerError::UnknownObject => Self::UnknownObject, + BrokerError::InvalidRights => Self::InvalidRights, + BrokerError::ResourceExhausted => Self::ResourceExhausted, + BrokerError::BrokerCoreAlreadyExists => Self::Internal, + BrokerError::WouldBlock => Self::WouldBlock, + BrokerError::UnsupportedOperation => Self::UnsupportedOperation, + } + } +} diff --git a/litebox_broker_host/src/error.rs b/litebox_broker_host/src/error.rs index c9d8f5a4a0..d69d901235 100644 --- a/litebox_broker_host/src/error.rs +++ b/litebox_broker_host/src/error.rs @@ -1,16 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use litebox_broker_core::BrokerError; +use litebox_broker_protocol::ErrorCode; use thiserror::Error; /// Errors returned by a broker-host receive/send loop. #[derive(Debug, Error)] #[non_exhaustive] pub enum BrokerHostError { - #[error("broker association setup failed")] - AssociationSetup, #[error("broker channel failed: {0}")] Channel(#[source] E), + #[error("broker setup failed: {0}")] + Broker(#[source] ErrorCode), +} + +impl From for BrokerHostError { + fn from(error: BrokerError) -> Self { + Self::Broker(error.into()) + } } /// Broker-host receive/send loop result type. diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index a35d875c9d..ca468908fc 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -14,7 +14,7 @@ extern crate std; use core::fmt; -use litebox_broker_core::{BrokerCore, BrokerError, BrokerSession, CallerCredential, event}; +use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential, event}; use litebox_broker_protocol::{ AddEventResponse, BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, @@ -36,11 +36,11 @@ where let peer_credential = channel .peer_credential() .map_err(BrokerHostError::Channel)?; - let caller_credential = caller_credential_from_peer(peer_credential) - .map_err(|()| BrokerHostError::AssociationSetup)?; - let session = core - .create_session(caller_credential) - .map_err(|_error| BrokerHostError::AssociationSetup)?; + let caller_credential = match peer_credential { + PeerCredential::Unauthenticated => CallerCredential::Unauthenticated, + _ => return Err(BrokerHostError::Broker(ErrorCode::PolicyDenied)), + }; + let session = core.create_session(caller_credential)?; serve_request_loop(channel, &session) } @@ -70,16 +70,6 @@ where Ok(ConnectionTermination::PeerClosed) } -fn caller_credential_from_peer( - peer_credential: PeerCredential, -) -> core::result::Result { - if peer_credential == PeerCredential::Unauthenticated { - Ok(CallerCredential::Unauthenticated) - } else { - Err(()) - } -} - fn handle_request( session: &BrokerSession, state: &mut ConnectionState, @@ -165,19 +155,7 @@ fn handle_core_result( ) -> BrokerResponse { match result { Ok(value) => into_response(value), - Err(error) => BrokerResponse::Error(to_protocol_error(error)), - } -} - -fn to_protocol_error(error: BrokerError) -> ErrorCode { - match error { - BrokerError::PolicyDenied => ErrorCode::PolicyDenied, - BrokerError::UnknownObject => ErrorCode::UnknownObject, - BrokerError::InvalidRights => ErrorCode::InvalidRights, - BrokerError::ResourceExhausted => ErrorCode::ResourceExhausted, - BrokerError::WouldBlock => ErrorCode::WouldBlock, - BrokerError::UnsupportedOperation => ErrorCode::UnsupportedOperation, - _ => ErrorCode::Internal, + Err(error) => BrokerResponse::Error(error.into()), } } From ee6e5c5a413e38ff0ce4bd760556f624771f6927 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 24 Jun 2026 09:05:09 -0700 Subject: [PATCH 056/319] Clean up broker helper indirection (#954) This PR inlines broker-host event result handling, dispatch construction, and local event response extraction at their call sites while preserving existing behavior. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/event/counter.rs | 15 ++-- litebox_broker_host/src/lib.rs | 125 +++++++++++++-------------------- 2 files changed, 55 insertions(+), 85 deletions(-) diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 4a73d6a6de..c12d9ac4c7 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -61,8 +61,8 @@ where CreateEventRequest { initial_count }, ))) .map_err(BrokerObjectError::from) - .map(event_response_from_core) .map_err(EventCounterError::from)?; + let CoreResponse::Event(response) = response; let EventResponse::Create(response) = response else { return Err(BrokerObjectError::UnexpectedResponse.into()); }; @@ -134,10 +134,11 @@ where } fn request_event(&self, request: EventRequest) -> Result { - self.broker + let CoreResponse::Event(response) = self + .broker .request(CoreRequest::Event(request)) - .map_err(BrokerObjectError::from) - .map(event_response_from_core) + .map_err(BrokerObjectError::from)?; + Ok(response) } } @@ -169,9 +170,3 @@ where events } } - -fn event_response_from_core(response: CoreResponse) -> EventResponse { - match response { - CoreResponse::Event(response) => response, - } -} diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index ca468908fc..05ef5ad0c1 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -80,19 +80,25 @@ fn handle_request( BrokerRequest::Negotiate { protocol_version } => { if protocol_version == BROKER_PROTOCOL_VERSION { *state = ConnectionState::Active; - BrokerDispatch::continue_after(BrokerResponse::Negotiated { - broker_protocol_version: BROKER_PROTOCOL_VERSION, - }) + BrokerDispatch { + response: BrokerResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }, + outcome: DispatchOutcome::Continue, + } } else { - BrokerDispatch::continue_after(BrokerResponse::VersionMismatch { - broker_protocol_version: BROKER_PROTOCOL_VERSION, - }) + BrokerDispatch { + response: BrokerResponse::VersionMismatch { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }, + outcome: DispatchOutcome::Continue, + } } } - BrokerRequest::Core(_) => BrokerDispatch::close_after( - BrokerResponse::Error(ErrorCode::ProtocolState), - CloseReason::ProtocolViolation, - ), + BrokerRequest::Core(_) => BrokerDispatch { + response: BrokerResponse::Error(ErrorCode::ProtocolState), + outcome: DispatchOutcome::Close(CloseReason::ProtocolViolation), + }, }, ConnectionState::Active => handle_active_request(session, request), } @@ -100,62 +106,47 @@ fn handle_request( fn handle_active_request(session: &BrokerSession, request: BrokerRequest) -> BrokerDispatch { match request { - BrokerRequest::Negotiate { .. } => BrokerDispatch::close_after( - BrokerResponse::Error(ErrorCode::ProtocolState), - CloseReason::ProtocolViolation, - ), - BrokerRequest::Core(request) => { - BrokerDispatch::continue_after(handle_core_request(session, request)) - } - } -} - -fn handle_core_request(session: &BrokerSession, request: CoreRequest) -> BrokerResponse { - match request { - CoreRequest::Event(request) => handle_event_request(session, request), + BrokerRequest::Negotiate { .. } => BrokerDispatch { + response: BrokerResponse::Error(ErrorCode::ProtocolState), + outcome: DispatchOutcome::Close(CloseReason::ProtocolViolation), + }, + BrokerRequest::Core(CoreRequest::Event(request)) => BrokerDispatch { + response: handle_event_request(session, request), + outcome: DispatchOutcome::Continue, + }, } } fn handle_event_request(session: &BrokerSession, request: EventRequest) -> BrokerResponse { match request { - EventRequest::Create(request) => { - handle_core_result(event::create(session, request.initial_count), |handle| { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Create( - CreateEventResponse { handle }, - ))) - }) + EventRequest::Create(request) => match event::create(session, request.initial_count) { + Ok(handle) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Create( + CreateEventResponse { handle }, + ))), + Err(error) => BrokerResponse::Error(error.into()), + }, + EventRequest::Wait(request) => match event::wait(session, request.handle) { + Ok(readiness) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( + WaitEventResponse { readiness }, + ))), + Err(error) => BrokerResponse::Error(error.into()), + }, + EventRequest::Add(request) => { + match event::add(session, request.handle, request.value) { + Ok(readiness) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Add( + AddEventResponse { readiness }, + ))), + Err(error) => BrokerResponse::Error(error.into()), + } } - EventRequest::Wait(request) => { - handle_core_result(event::wait(session, request.handle), |readiness| { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( - WaitEventResponse { readiness }, - ))) - }) + EventRequest::Consume(request) => { + match event::consume(session, request.handle, request.mode) { + Ok(consumption) => { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(consumption))) + } + Err(error) => BrokerResponse::Error(error.into()), + } } - EventRequest::Add(request) => handle_core_result( - event::add(session, request.handle, request.value), - |readiness| { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(AddEventResponse { - readiness, - }))) - }, - ), - EventRequest::Consume(request) => handle_core_result( - event::consume(session, request.handle, request.mode), - |consumption| { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(consumption))) - }, - ), - } -} - -fn handle_core_result( - result: litebox_broker_core::Result, - into_response: impl FnOnce(T) -> BrokerResponse, -) -> BrokerResponse { - match result { - Ok(value) => into_response(value), - Err(error) => BrokerResponse::Error(error.into()), } } @@ -177,22 +168,6 @@ enum DispatchOutcome { Close(CloseReason), } -impl BrokerDispatch { - const fn continue_after(response: BrokerResponse) -> Self { - Self { - response, - outcome: DispatchOutcome::Continue, - } - } - - const fn close_after(response: BrokerResponse, reason: CloseReason) -> Self { - Self { - response, - outcome: DispatchOutcome::Close(reason), - } - } -} - /// Reason the broker host closed the connection after sending a response. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] From 69e4e5c94b82bdcc8bede6757c8013726187869a Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 24 Jun 2026 09:29:29 -0700 Subject: [PATCH 057/319] Use descriptive broker channel generics (#955) Addresses PR #880 feedback by using descriptive `Channel` generic names for broker channel-bound APIs. This also moves channel trait bounds onto the broker-local wrapper structs where the type is only usable with those bounds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 16 ++++++++++------ litebox/src/litebox.rs | 12 ++++++------ litebox_broker_host/src/lib.rs | 16 ++++++++-------- litebox_broker_local/src/event.rs | 14 +++++++------- litebox_broker_local/src/lib.rs | 18 +++++++++--------- litebox_runner_linux_userland/tests/run.rs | 15 +++++++-------- 6 files changed, 47 insertions(+), 44 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index ab27914b7c..990202f08e 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -22,25 +22,29 @@ pub(crate) trait BrokerControl: Send + Sync { ) -> core::result::Result; } -pub(crate) struct BrokerLocalControl { - local: Mutex>, +pub(crate) struct BrokerLocalControl< + Platform: RawSyncPrimitivesProvider, + Channel: LocalControlChannel + Send, +> { + local: Mutex>, } -impl BrokerLocalControl +impl BrokerLocalControl where Platform: RawSyncPrimitivesProvider, + Channel: LocalControlChannel + Send, { - pub(crate) const fn new(local: BrokerLocal) -> Self { + pub(crate) const fn new(local: BrokerLocal) -> Self { Self { local: Mutex::new(local), } } } -impl BrokerControl for BrokerLocalControl +impl BrokerControl for BrokerLocalControl where Platform: RawSyncPrimitivesProvider, - T: LocalControlChannel + Send, + Channel: LocalControlChannel + Send, { fn request( &self, diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index ba5d06c46a..7ddc6bd585 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -38,18 +38,18 @@ impl LiteBox { } /// Create a new [`LiteBox`] instance with a negotiated broker-local control adapter installed. - pub fn new_with_broker_local( + pub fn new_with_broker_local( platform: &'static Platform, - broker_local: BrokerLocal, + broker_local: BrokerLocal, ) -> Self where - T: LocalControlChannel + Send + 'static, + Channel: LocalControlChannel + Send + 'static, { Self::new_inner( platform, - Some(Arc::new(broker::BrokerLocalControl::::new( - broker_local, - ))), + Some(Arc::new( + broker::BrokerLocalControl::::new(broker_local), + )), ) } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 05ef5ad0c1..8dc2f1807f 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -26,12 +26,12 @@ mod error; pub use error::{BrokerHostError, Result}; /// Serves one broker connection over the provided connected control channel. -pub fn serve_connection( +pub fn serve_connection( core: &BrokerCore, - channel: &mut T, -) -> Result + channel: &mut Channel, +) -> Result where - T: HostControlChannel, + Channel: HostControlChannel, { let peer_credential = channel .peer_credential() @@ -45,12 +45,12 @@ where serve_request_loop(channel, &session) } -fn serve_request_loop( - channel: &mut T, +fn serve_request_loop( + channel: &mut Channel, session: &BrokerSession, -) -> Result +) -> Result where - T: HostControlChannel, + Channel: HostControlChannel, { let mut state = ConnectionState::AwaitingNegotiation; loop { diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 2851289fd3..7867d193e2 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -9,9 +9,9 @@ use litebox_broker_protocol::{ use crate::{BrokerLocal, BrokerLocalError, Result}; -impl BrokerLocal { +impl BrokerLocal { /// Creates a broker-owned event object. - pub fn create_event(&mut self) -> Result { + pub fn create_event(&mut self) -> Result { self.create_event_with_count(0) } @@ -19,7 +19,7 @@ impl BrokerLocal { pub fn create_event_with_count( &mut self, initial_count: u64, - ) -> Result { + ) -> Result { let response = self.request_event(EventRequest::Create(CreateEventRequest { initial_count }))?; match response { @@ -31,7 +31,7 @@ impl BrokerLocal { } /// Checks whether an event wait would complete now. - pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { + pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { let response = self.request_event(EventRequest::Wait(WaitEventRequest { handle }))?; match response { EventResponse::Wait(response) => Ok(response.readiness), @@ -46,7 +46,7 @@ impl BrokerLocal { &mut self, handle: ObjectHandle, value: u64, - ) -> Result { + ) -> Result { let response = self.request_event(EventRequest::Add(AddEventRequest { handle, value }))?; match response { EventResponse::Add(response) => Ok(response.readiness), @@ -61,7 +61,7 @@ impl BrokerLocal { &mut self, handle: ObjectHandle, mode: EventConsumeMode, - ) -> Result { + ) -> Result { let response = self.request_event(EventRequest::Consume(ConsumeEventRequest { handle, mode }))?; match response { @@ -72,7 +72,7 @@ impl BrokerLocal { } } - fn request_event(&mut self, request: EventRequest) -> Result { + fn request_event(&mut self, request: EventRequest) -> Result { let CoreResponse::Event(response) = self.request(CoreRequest::Event(request))?; Ok(response) } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 72b6b30592..4626176f7c 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -23,18 +23,18 @@ use litebox_broker_protocol::{ pub use error::{BrokerLocalError, Result}; /// Typed broker-local control adapter for broker operations. -pub struct BrokerLocal { - channel: T, +pub struct BrokerLocal { + channel: Channel, } -impl BrokerLocal { +impl BrokerLocal { /// Returns the underlying control channel for deployment-specific configuration. - pub fn control_channel_mut(&mut self) -> &mut T { + pub fn control_channel_mut(&mut self) -> &mut Channel { &mut self.channel } /// Negotiates the broker protocol over an already-connected control channel. - pub fn negotiate(mut channel: T) -> Result { + pub fn negotiate(mut channel: Channel) -> Result { let requested = BROKER_PROTOCOL_VERSION; match raw_request( &mut channel, @@ -61,7 +61,7 @@ impl BrokerLocal { } /// Sends one active BrokerCore request. - pub fn request(&mut self, request: CoreRequest) -> Result { + pub fn request(&mut self, request: CoreRequest) -> Result { match raw_request(&mut self.channel, BrokerRequest::Core(request))? { BrokerResponse::Core(response) => Ok(response), BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), @@ -70,10 +70,10 @@ impl BrokerLocal { } } -fn raw_request( - channel: &mut T, +fn raw_request( + channel: &mut Channel, request: BrokerRequest, -) -> Result { +) -> Result { channel .send_request(&request) .map_err(BrokerLocalError::Channel)?; diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 04fd3bff45..203d8a123e 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -359,14 +359,14 @@ fn spawn_test_broker( } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -struct CountingHostControlChannel { - inner: T, +struct CountingHostControlChannel { + inner: Channel, event_request_count: usize, } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -impl CountingHostControlChannel { - const fn new(inner: T) -> Self { +impl CountingHostControlChannel { + const fn new(inner: Channel) -> Self { Self { inner, event_request_count: 0, @@ -379,11 +379,10 @@ impl CountingHostControlChannel { } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -impl litebox_broker_protocol::HostControlChannel for CountingHostControlChannel -where - T: litebox_broker_protocol::HostControlChannel, +impl + litebox_broker_protocol::HostControlChannel for CountingHostControlChannel { - type Error = T::Error; + type Error = Channel::Error; fn peer_credential(&self) -> Result { self.inner.peer_credential() From ec36ae3e0c65f00cdf98761e535a202a0748c8bf Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 24 Jun 2026 10:30:13 -0700 Subject: [PATCH 058/319] Treat broker response mismatches as fatal (#956) This PR removes `UnexpectedResponse` errors from the local-core and broker-local layers and panics when the broker violates request/response protocol invariants since there is no sensible recovery path for these errors. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 24 ++++---- litebox/src/event/counter.rs | 17 ++++-- litebox_broker_local/src/error.rs | 4 +- litebox_broker_local/src/event.rs | 44 +++++++++----- litebox_broker_local/src/lib.rs | 88 +++++++++++++++++++++++---- litebox_common_linux/src/errno/mod.rs | 2 +- 6 files changed, 127 insertions(+), 52 deletions(-) diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 3cd575c425..3eb7f4923f 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -14,8 +14,6 @@ pub(crate) enum BrokerControlError { Transport, #[error("broker returned operation error: {0}")] Broker(#[source] ErrorCode), - #[error("broker returned unexpected response")] - UnexpectedResponse, } /// Internal normalized error for broker-backed object adapters. @@ -32,10 +30,8 @@ pub(crate) enum BrokerObjectError { WouldBlock, #[error("broker object resource exhausted")] ResourceExhausted, - #[error("broker returned unexpected response")] - UnexpectedResponse, - #[error("internal broker object error")] - Internal, + #[error("broker object permission denied")] + PermissionDenied, } impl From for BrokerObjectError { @@ -43,7 +39,6 @@ impl From for BrokerObjectError { match error { BrokerControlError::Transport => Self::Control, BrokerControlError::Broker(error) => error.into(), - BrokerControlError::UnexpectedResponse => Self::UnexpectedResponse, } } } @@ -54,7 +49,13 @@ impl From for BrokerObjectError { ErrorCode::InvalidRights | ErrorCode::UnknownObject => Self::InvalidObject, ErrorCode::WouldBlock => Self::WouldBlock, ErrorCode::ResourceExhausted => Self::ResourceExhausted, - _ => Self::Internal, + ErrorCode::PolicyDenied => Self::PermissionDenied, + ErrorCode::UnsupportedVersion + | ErrorCode::MalformedRequest + | ErrorCode::ProtocolState + | ErrorCode::UnsupportedOperation + | ErrorCode::Internal => panic!("broker returned unrecoverable error: {error}"), + _ => panic!("broker returned unsupported error: {error}"), } } } @@ -64,7 +65,6 @@ impl From> for BrokerControlError { match error { BrokerLocalError::Channel(_) | BrokerLocalError::ChannelClosed => Self::Transport, BrokerLocalError::Broker(error) => Self::Broker(error), - BrokerLocalError::UnexpectedResponse(_) => Self::UnexpectedResponse, } } } @@ -83,10 +83,8 @@ impl From for EventCounterError { match error { BrokerObjectError::WouldBlock => Self::WouldBlock, BrokerObjectError::ResourceExhausted => Self::ResourceExhausted, - BrokerObjectError::UnexpectedResponse => Self::UnexpectedResponse, - BrokerObjectError::Control - | BrokerObjectError::InvalidObject - | BrokerObjectError::Internal => Self::Io, + BrokerObjectError::PermissionDenied => Self::PermissionDenied, + BrokerObjectError::Control | BrokerObjectError::InvalidObject => Self::Io, } } } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index c12d9ac4c7..4c8b96c068 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -32,10 +32,10 @@ pub enum EventCounterError { WouldBlock, #[error("event counter resource exhausted")] ResourceExhausted, + #[error("event counter permission denied")] + PermissionDenied, #[error("event counter I/O failed")] Io, - #[error("event counter received unexpected response")] - UnexpectedResponse, #[error("event counter backing authority unavailable")] Unavailable, } @@ -52,6 +52,11 @@ where Platform: RawSyncPrimitivesProvider + TimeProvider, { /// Creates a local-core event counter. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a protocol + /// response that does not match the issued event request. pub fn new(litebox: &LiteBox, initial_count: u64) -> Result { let Some(broker) = litebox.broker_control() else { return Err(EventCounterError::Unavailable); @@ -64,7 +69,7 @@ where .map_err(EventCounterError::from)?; let CoreResponse::Event(response) = response; let EventResponse::Create(response) = response else { - return Err(BrokerObjectError::UnexpectedResponse.into()); + panic!("broker returned unexpected event response: {response:?}"); }; Ok(Self { broker, @@ -117,7 +122,7 @@ where mode, }))?; let EventResponse::Consume(response) = response else { - return Err(BrokerObjectError::UnexpectedResponse); + panic!("broker returned unexpected event response: {response:?}"); }; Ok(response) } @@ -128,7 +133,7 @@ where value, }))?; let EventResponse::Add(response) = response else { - return Err(BrokerObjectError::UnexpectedResponse); + panic!("broker returned unexpected event response: {response:?}"); }; Ok(response.readiness) } @@ -157,7 +162,7 @@ where return Events::empty(); }; let EventResponse::Wait(response) = response else { - return Events::empty(); + panic!("broker returned unexpected event response: {response:?}"); }; let readiness = response.readiness; let mut events = Events::empty(); diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 98201c3653..542e9fba57 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox_broker_protocol::{BrokerResponse, ErrorCode}; +use litebox_broker_protocol::ErrorCode; use thiserror::Error; /// Errors returned by active broker-local control requests. @@ -13,8 +13,6 @@ pub enum BrokerLocalError { ChannelClosed, #[error("broker rejected request: {0}")] Broker(#[source] ErrorCode), - #[error("broker returned unexpected response: {0:?}")] - UnexpectedResponse(BrokerResponse), } /// Broker-local control adapter result type. diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 7867d193e2..fc3286c686 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -2,12 +2,12 @@ // Licensed under the MIT license. use litebox_broker_protocol::{ - AddEventRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, - CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, - LocalControlChannel, ObjectHandle, ReadinessState, WaitEventRequest, + AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, + CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, LocalControlChannel, + ObjectHandle, ReadinessState, WaitEventRequest, }; -use crate::{BrokerLocal, BrokerLocalError, Result}; +use crate::{BrokerLocal, Result}; impl BrokerLocal { /// Creates a broker-owned event object. @@ -16,6 +16,11 @@ impl BrokerLocal { } /// Creates a broker-owned event object with initial readiness credits. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a protocol + /// response that does not match the issued event request. pub fn create_event_with_count( &mut self, initial_count: u64, @@ -24,24 +29,30 @@ impl BrokerLocal { self.request_event(EventRequest::Create(CreateEventRequest { initial_count }))?; match response { EventResponse::Create(response) => Ok(response.handle), - response => Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Core( - CoreResponse::Event(response), - ))), + response => panic!("broker returned unexpected event response: {response:?}"), } } /// Checks whether an event wait would complete now. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a protocol + /// response that does not match the issued event request. pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { let response = self.request_event(EventRequest::Wait(WaitEventRequest { handle }))?; match response { EventResponse::Wait(response) => Ok(response.readiness), - response => Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Core( - CoreResponse::Event(response), - ))), + response => panic!("broker returned unexpected event response: {response:?}"), } } /// Adds readiness credits to a broker-owned event object. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a protocol + /// response that does not match the issued event request. pub fn add_event( &mut self, handle: ObjectHandle, @@ -50,13 +61,16 @@ impl BrokerLocal { let response = self.request_event(EventRequest::Add(AddEventRequest { handle, value }))?; match response { EventResponse::Add(response) => Ok(response.readiness), - response => Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Core( - CoreResponse::Event(response), - ))), + response => panic!("broker returned unexpected event response: {response:?}"), } } /// Consumes readiness credits from a broker-owned event object. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a protocol + /// response that does not match the issued event request. pub fn consume_event( &mut self, handle: ObjectHandle, @@ -66,9 +80,7 @@ impl BrokerLocal { self.request_event(EventRequest::Consume(ConsumeEventRequest { handle, mode }))?; match response { EventResponse::Consume(response) => Ok(response), - response => Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Core( - CoreResponse::Event(response), - ))), + response => panic!("broker returned unexpected event response: {response:?}"), } } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 4626176f7c..ec54ed0034 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -34,6 +34,11 @@ impl BrokerLocal { } /// Negotiates the broker protocol over an already-connected control channel. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a protocol + /// response that does not match the negotiation request. pub fn negotiate(mut channel: Channel) -> Result { let requested = BROKER_PROTOCOL_VERSION; match raw_request( @@ -45,27 +50,54 @@ impl BrokerLocal { response @ BrokerResponse::Negotiated { broker_protocol_version, } => { - if requested != broker_protocol_version { - return Err(BrokerLocalError::UnexpectedResponse(response)); - } + assert_eq!( + requested, broker_protocol_version, + "broker returned unexpected negotiation response: {response:?}" + ); Ok(Self { channel }) } BrokerResponse::VersionMismatch { .. } => { Err(BrokerLocalError::Broker(ErrorCode::UnsupportedVersion)) } - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + BrokerResponse::Error(error) => match error { + ErrorCode::UnsupportedVersion | ErrorCode::PolicyDenied => { + Err(BrokerLocalError::Broker(error)) + } + ErrorCode::MalformedRequest + | ErrorCode::ProtocolState + | ErrorCode::UnsupportedOperation + | ErrorCode::Internal => panic!("broker returned unrecoverable error: {error}"), + _ => panic!("broker returned unexpected negotiation error: {error}"), + }, response @ BrokerResponse::Core(_) => { - Err(BrokerLocalError::UnexpectedResponse(response)) + panic!("broker returned unexpected negotiation response: {response:?}") } } } /// Sends one active BrokerCore request. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a protocol + /// response that does not match an active core request. pub fn request(&mut self, request: CoreRequest) -> Result { match raw_request(&mut self.channel, BrokerRequest::Core(request))? { BrokerResponse::Core(response) => Ok(response), - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response => Err(BrokerLocalError::UnexpectedResponse(response)), + BrokerResponse::Error(error) => match error { + ErrorCode::PolicyDenied + | ErrorCode::UnknownObject + | ErrorCode::InvalidRights + | ErrorCode::ResourceExhausted + | ErrorCode::WouldBlock => Err(BrokerLocalError::Broker(error)), + ErrorCode::UnsupportedVersion + | ErrorCode::MalformedRequest + | ErrorCode::ProtocolState + | ErrorCode::UnsupportedOperation + | ErrorCode::Internal => panic!("broker returned unrecoverable error: {error}"), + _ => panic!("broker returned unsupported error: {error}"), + }, + response => panic!("broker returned unexpected active response: {response:?}"), } } } @@ -125,18 +157,40 @@ mod tests { } #[test] + fn active_request_returns_recoverable_broker_error() { + let request = CoreRequest::Event(EventRequest::Create(CreateEventRequest { + initial_count: 0, + })); + let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::WouldBlock))); + let mut local = BrokerLocal { channel }; + + assert!(matches!( + local.request(request), + Err(BrokerLocalError::Broker(ErrorCode::WouldBlock)) + )); + } + + #[test] + #[should_panic(expected = "broker returned unrecoverable error")] + fn active_request_panics_on_unrecoverable_broker_error() { + let request = CoreRequest::Event(EventRequest::Create(CreateEventRequest { + initial_count: 0, + })); + let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::Internal))); + let mut local = BrokerLocal { channel }; + + let _ = local.request(request); + } + + #[test] + #[should_panic(expected = "broker returned unexpected negotiation response")] fn negotiate_rejects_broker_different_version_response() { let broker_protocol_version = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { broker_protocol_version, })); - assert!(matches!( - BrokerLocal::negotiate(channel), - Err(BrokerLocalError::UnexpectedResponse(BrokerResponse::Negotiated { - broker_protocol_version: broker - })) if broker == broker_protocol_version - )); + let _ = BrokerLocal::negotiate(channel); } #[test] @@ -152,6 +206,14 @@ mod tests { )); } + #[test] + #[should_panic(expected = "broker returned unrecoverable error")] + fn negotiate_panics_on_unrecoverable_broker_error() { + let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::Internal))); + + let _ = BrokerLocal::negotiate(channel); + } + struct FakeControlChannel { sent_request: Option, response: Option, diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 3c1177a76c..ca932167dd 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -542,8 +542,8 @@ impl From for Errno { litebox::event::counter::EventCounterError::InvalidInput => Errno::EINVAL, litebox::event::counter::EventCounterError::WouldBlock | litebox::event::counter::EventCounterError::ResourceExhausted => Errno::EAGAIN, + litebox::event::counter::EventCounterError::PermissionDenied => Errno::EACCES, litebox::event::counter::EventCounterError::Io - | litebox::event::counter::EventCounterError::UnexpectedResponse | litebox::event::counter::EventCounterError::Unavailable => Errno::EIO, _ => Errno::EIO, } From 2033004b45db86d178b948c2b9d4f8db04ba8eb6 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 24 Jun 2026 15:04:24 -0700 Subject: [PATCH 059/319] Let userland broker choose runner socket path (#957) The broker now creates a private temporary socket directory, launches the local runner with that socket path, serves the broker connection, and cleans up the runner/socket state. The runner broker-socket flag is hidden because it is now an internal broker-to-runner handoff. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 2 + litebox_broker_userland/Cargo.toml | 7 + litebox_broker_userland/src/main.rs | 45 ++++-- .../tests/userland_broker.rs | 153 +++++++++--------- litebox_runner_linux_userland/src/lib.rs | 3 +- 5 files changed, 122 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5405cf950b..c59fa65cb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1523,11 +1523,13 @@ name = "litebox_broker_userland" version = "0.1.0" dependencies = [ "clap", + "libc", "litebox_broker_core", "litebox_broker_host", "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", + "tempfile", ] [[package]] diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index 8e3502f1c1..e9253548ff 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -8,12 +8,19 @@ clap = { version = "4.5.33", features = ["derive"] } litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0" } litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } +tempfile = { version = "3", default-features = false } [[bin]] name = "litebox-broker-userland" path = "src/main.rs" +[[test]] +name = "userland_broker" +path = "tests/userland_broker.rs" +harness = false + [dev-dependencies] +libc = { version = "0.2.169", default-features = false } litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index df9b03d22d..6fb7d9ddf0 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -2,33 +2,54 @@ // Licensed under the MIT license. use std::error::Error; +use std::ffi::OsString; use std::os::unix::net::UnixListener; use std::path::PathBuf; -use std::time::{Duration, Instant}; +use std::process::Command; +use std::thread; use clap::Parser; use litebox_broker_core::{BrokerCore, PolicyEngine, PrincipalRights}; use litebox_broker_host::serve_connection; use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; -const SESSION_TIMEOUT: Duration = Duration::from_secs(5); - #[derive(Parser, Debug)] struct CliArgs { - /// Broker Unix socket path to bind. - #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] - socket: PathBuf, + /// Local runner executable to launch. + #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::ExecutablePath)] + runner: PathBuf, + /// Arguments to pass to the local runner. + #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true, value_hint = clap::ValueHint::CommandWithArguments)] + runner_arguments: Vec, } fn main() -> Result<(), Box> { let args = CliArgs::parse(); - let listener = UnixListener::bind(args.socket)?; - let (stream, _) = listener.accept()?; - let mut channel = UnixStreamHostControlChannel::from_accepted(stream); - channel.set_io_deadline(Some(Instant::now() + SESSION_TIMEOUT))?; + let socket_dir = tempfile::Builder::new() + .prefix("litebox-broker-userland-") + .tempdir()?; + let socket_path = socket_dir.path().join("broker.sock"); + let listener = UnixListener::bind(&socket_path)?; let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( PrincipalRights::all(), ))?; - serve_connection(&broker, &mut channel)?; - Ok(()) + + let mut runner_command = Command::new(&args.runner); + runner_command + .arg("--unstable") + .arg("--broker-socket") + .arg(&socket_path) + .args(&args.runner_arguments); + let mut runner = runner_command.spawn()?; + let _runner_waiter = thread::spawn(move || { + if let Err(error) = runner.wait() { + eprintln!("failed to wait for local runner: {error}"); + } + }); + + loop { + let (stream, _) = listener.accept()?; + let mut channel = UnixStreamHostControlChannel::from_accepted(stream); + serve_connection(&broker, &mut channel)?; + } } diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index d0ccb967c0..06db85996b 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -2,22 +2,77 @@ // Licensed under the MIT license. use std::env; -use std::fs; +use std::ffi::{OsStr, OsString}; use std::io; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command, ExitStatus}; +use std::os::unix::process::ExitStatusExt; +use std::path::Path; +use std::process::{Child, Command}; use std::thread; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::ReadinessState; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; -#[test] -fn separate_process_broker_serves_event_object_requests() { - let socket_path = SocketPathGuard::new(unique_socket_path()); - let mut child = ChildGuard::new(spawn_broker(socket_path.path())); - let mut channel = connect_with_retry(socket_path.path()).unwrap(); +const RUNNER_ARGUMENT: &str = "broker-userland-test-runner"; + +fn main() { + let args = env::args_os().skip(1).collect::>(); + if args + .first() + .is_some_and(|arg| arg == OsStr::new("--unstable")) + { + run_fake_runner(&args); + } else { + run_parent_test(); + } +} + +fn run_parent_test() { + // This custom-harness integration test uses its own executable as the broker's + // runner. Cargo starts this executable without broker args, so it runs the + // parent path here. The broker then starts the same executable with the real + // runner argv (`--unstable --broker-socket `), which runs `run_fake_runner`. + // After the fake runner finishes its broker requests, it terminates the broker + // parent process; this lets the test exercise the long-running broker without a + // test-only shutdown path. + let mut broker = ChildGuard { + child: Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")) + .arg("--runner") + .arg(env::current_exe().unwrap()) + .arg(RUNNER_ARGUMENT) + .spawn() + .unwrap(), + }; + + let deadline = Instant::now() + Duration::from_secs(5); + while Instant::now() < deadline { + if let Some(status) = broker.child.try_wait().unwrap() { + assert_eq!(status.signal(), Some(libc::SIGTERM)); + return; + } + thread::sleep(Duration::from_millis(10)); + } + panic!("timed out waiting for broker to stop"); +} + +fn run_fake_runner(args: &[OsString]) { + assert_eq!( + args.first().map(OsString::as_os_str), + Some(OsStr::new("--unstable")) + ); + assert_eq!( + args.get(1).map(OsString::as_os_str), + Some(OsStr::new("--broker-socket")) + ); + assert_eq!( + args.get(3).map(OsString::as_os_str), + Some(OsStr::new(RUNNER_ARGUMENT)) + ); + assert_eq!(args.len(), 4, "unexpected runner arguments: {args:?}"); + + let socket_path = args.get(2).unwrap(); + let mut channel = connect_with_retry(Path::new(socket_path)).unwrap(); channel .set_io_timeout(Some(Duration::from_secs(5))) .unwrap(); @@ -48,73 +103,32 @@ fn separate_process_broker_serves_event_object_requests() { } ); drop(local); - assert!(child.wait().unwrap().success()); -} -fn spawn_broker(socket_path: &Path) -> Child { - Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")) - .arg("--socket") - .arg(socket_path) - .spawn() - .unwrap() + // SAFETY: `getppid` takes no pointer arguments and has no Rust-side aliasing requirements. + let broker_pid = unsafe { libc::getppid() }; + // SAFETY: `broker_pid` is the runner's parent process and `SIGTERM` is a valid signal number. + let kill_result = unsafe { libc::kill(broker_pid, libc::SIGTERM) }; + assert_eq!( + kill_result, + 0, + "failed to stop broker: {}", + io::Error::last_os_error() + ); } struct ChildGuard { - child: Option, -} - -impl ChildGuard { - fn new(child: Child) -> Self { - Self { child: Some(child) } - } - - fn wait(&mut self) -> io::Result { - let status = self.child.as_mut().expect("child process missing").wait(); - if status.is_ok() { - self.child = None; - } - status - } + child: Child, } impl Drop for ChildGuard { fn drop(&mut self) { - if let Some(mut child) = self.child.take() { - match child.try_wait() { - Ok(Some(_status)) => {} - Ok(None) => { - let _ = child.kill(); - let _ = child.wait(); - } - Err(_error) => { - let _ = child.kill(); - let _ = child.wait(); - } - } + if !matches!(self.child.try_wait(), Ok(Some(_status))) { + let _ = self.child.kill(); + let _ = self.child.wait(); } } } -struct SocketPathGuard { - path: PathBuf, -} - -impl SocketPathGuard { - fn new(path: PathBuf) -> Self { - Self { path } - } - - fn path(&self) -> &Path { - &self.path - } -} - -impl Drop for SocketPathGuard { - fn drop(&mut self) { - let _ = fs::remove_file(&self.path); - } -} - fn connect_with_retry(socket_path: &Path) -> io::Result { let deadline = Instant::now() + Duration::from_secs(5); loop { @@ -132,14 +146,3 @@ fn connect_with_retry(socket_path: &Path) -> io::Result PathBuf { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - env::temp_dir().join(format!( - "litebox-broker-userland-{}-{now}.sock", - std::process::id() - )) -} diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 23e866c586..efe4406325 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -79,11 +79,12 @@ pub struct CliArgs { help_heading = "Unstable Options" )] pub program_from_tar: bool, - /// Connect to an already-running broker Unix socket and verify the control path. + /// Broker-supplied Unix socket path for the local control channel. #[arg( long = "broker-socket", value_name = "PATH", value_hint = clap::ValueHint::FilePath, + hide = true, requires = "unstable", help_heading = "Unstable Options" )] From 39671d117ff6381acb23b6b089667230ebf21e0c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 24 Jun 2026 15:44:39 -0700 Subject: [PATCH 060/319] Clean up broker module imports (#959) This PR cleans up broker-related imports to avoid directly importing modules such as `std::env`, `std::thread`, `std::io`, and broker `event` modules. It switches affected code to direct type/trait/const imports or explicit fully-qualified paths, improving readability without changing behavior. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_core/src/session.rs | 17 ++-- litebox_broker_host/src/lib.rs | 40 +++++----- .../src/wire/core_message.rs | 11 +-- litebox_broker_transport/src/unix_socket.rs | 79 +++++++++---------- litebox_broker_userland/src/main.rs | 3 +- .../tests/userland_broker.rs | 20 +++-- litebox_runner_linux_userland/src/broker.rs | 3 +- 7 files changed, 85 insertions(+), 88 deletions(-) diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index ece0e32ac5..61c9e2395b 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -179,7 +179,6 @@ impl Drop for BrokerSession { mod tests { use crate::{ BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine, PrincipalRights, - event, }; use litebox_broker_protocol::{ EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, @@ -198,12 +197,12 @@ mod tests { let other = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); - let handle = event::create(&session, 0).unwrap(); + let handle = crate::event::create(&session, 0).unwrap(); let unknown_handle = ObjectHandle(handle.0 + 1); assert_ne!(unknown_handle, handle); assert_eq!( - event::wait(&session, unknown_handle), + crate::event::wait(&session, unknown_handle), Err(BrokerError::UnknownObject) ); @@ -213,21 +212,21 @@ mod tests { ); assert_eq!( - event::wait(&session, handle), + crate::event::wait(&session, handle), Ok(ReadinessState { read_ready: false, write_ready: true, }) ); assert_eq!( - event::add(&session, handle, 1), + crate::event::add(&session, handle, 1), Ok(ReadinessState { read_ready: true, write_ready: true, }) ); assert_eq!( - event::consume(&session, handle, EventConsumeMode::One), + crate::event::consume(&session, handle, EventConsumeMode::One), Ok(EventConsumption { value: 1, readiness: ReadinessState { @@ -237,7 +236,7 @@ mod tests { }) ); assert_eq!( - event::create(&session, 0), + crate::event::create(&session, 0), Err(BrokerError::ResourceExhausted) ); @@ -254,7 +253,7 @@ mod tests { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); - let _handle = event::create(&session, 0).unwrap(); + let _handle = crate::event::create(&session, 0).unwrap(); { let references = broker.references.read(); assert_eq!(references.len(), 1); @@ -275,7 +274,7 @@ mod tests { *next_reference_handle = u64::MAX; } assert_eq!( - event::create(&session, 0), + crate::event::create(&session, 0), Err(BrokerError::ResourceExhausted) ); let references = broker.references.read(); diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 8dc2f1807f..4eee962992 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -12,9 +12,9 @@ #[cfg(test)] extern crate std; -use core::fmt; +use core::fmt::{Display, Formatter, Result as FmtResult}; -use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential, event}; +use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential}; use litebox_broker_protocol::{ AddEventResponse, BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, @@ -119,20 +119,24 @@ fn handle_active_request(session: &BrokerSession, request: BrokerRequest) -> Bro fn handle_event_request(session: &BrokerSession, request: EventRequest) -> BrokerResponse { match request { - EventRequest::Create(request) => match event::create(session, request.initial_count) { - Ok(handle) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Create( - CreateEventResponse { handle }, - ))), - Err(error) => BrokerResponse::Error(error.into()), - }, - EventRequest::Wait(request) => match event::wait(session, request.handle) { - Ok(readiness) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( - WaitEventResponse { readiness }, - ))), - Err(error) => BrokerResponse::Error(error.into()), - }, + EventRequest::Create(request) => { + match litebox_broker_core::event::create(session, request.initial_count) { + Ok(handle) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Create( + CreateEventResponse { handle }, + ))), + Err(error) => BrokerResponse::Error(error.into()), + } + } + EventRequest::Wait(request) => { + match litebox_broker_core::event::wait(session, request.handle) { + Ok(readiness) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( + WaitEventResponse { readiness }, + ))), + Err(error) => BrokerResponse::Error(error.into()), + } + } EventRequest::Add(request) => { - match event::add(session, request.handle, request.value) { + match litebox_broker_core::event::add(session, request.handle, request.value) { Ok(readiness) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Add( AddEventResponse { readiness }, ))), @@ -140,7 +144,7 @@ fn handle_event_request(session: &BrokerSession, request: EventRequest) -> Broke } } EventRequest::Consume(request) => { - match event::consume(session, request.handle, request.mode) { + match litebox_broker_core::event::consume(session, request.handle, request.mode) { Ok(consumption) => { BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(consumption))) } @@ -176,8 +180,8 @@ pub enum CloseReason { ProtocolViolation, } -impl fmt::Display for CloseReason { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl Display for CloseReason { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { Self::ProtocolViolation => f.write_str("protocol violation"), } diff --git a/litebox_broker_protocol/src/wire/core_message.rs b/litebox_broker_protocol/src/wire/core_message.rs index 3f0061e789..04ee6cefbb 100644 --- a/litebox_broker_protocol/src/wire/core_message.rs +++ b/litebox_broker_protocol/src/wire/core_message.rs @@ -4,7 +4,6 @@ use crate::{CoreRequest, CoreResponse}; use super::WireError; -use super::event; use super::primitive::{Decoder, Encoder}; // Core tags select object-family codecs. Add new object families here, then @@ -16,14 +15,14 @@ pub(super) fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) { match request { CoreRequest::Event(request) => { encoder.u8(CORE_REQUEST_TAG_EVENT); - event::encode_event_request(encoder, request); + super::event::encode_event_request(encoder, request); } } } pub(super) fn decode_core_request(decoder: &mut Decoder<'_>) -> Result { let request = match decoder.u8()? { - CORE_REQUEST_TAG_EVENT => CoreRequest::Event(event::decode_event_request(decoder)?), + CORE_REQUEST_TAG_EVENT => CoreRequest::Event(super::event::decode_event_request(decoder)?), _ => return Err(WireError::InvalidTag), }; @@ -34,14 +33,16 @@ pub(super) fn encode_core_response(encoder: &mut Encoder, response: CoreResponse match response { CoreResponse::Event(response) => { encoder.u8(CORE_RESPONSE_TAG_EVENT); - event::encode_event_response(encoder, response); + super::event::encode_event_response(encoder, response); } } } pub(super) fn decode_core_response(decoder: &mut Decoder<'_>) -> Result { let response = match decoder.u8()? { - CORE_RESPONSE_TAG_EVENT => CoreResponse::Event(event::decode_event_response(decoder)?), + CORE_RESPONSE_TAG_EVENT => { + CoreResponse::Event(super::event::decode_event_response(decoder)?) + } _ => return Err(WireError::InvalidTag), }; diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index eca3d1cd52..0d1d2d2ab0 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -7,7 +7,7 @@ //! framing are hosted userland concerns. Portable broker interfaces live in the //! no_std protocol, local, core, and host crates. -use std::io::{self, Read, Write}; +use std::io::{Error, ErrorKind, Read, Result as IoResult, Write}; use std::os::unix::net::UnixStream; use std::path::Path; use std::time::{Duration, Instant}; @@ -41,12 +41,12 @@ impl UnixStreamLocalControlChannel { } /// Connects to a userland broker Unix socket. - pub fn connect(path: impl AsRef) -> io::Result { + pub fn connect(path: impl AsRef) -> IoResult { UnixStream::connect(path).map(Self::from_connected) } /// Sets the read and write timeout for broker control-channel operations. - pub fn set_io_timeout(&mut self, timeout: Option) -> io::Result<()> { + pub fn set_io_timeout(&mut self, timeout: Option) -> IoResult<()> { self.io_timeout = timeout; self.io_deadline = None; self.active_request_deadline = None; @@ -54,7 +54,7 @@ impl UnixStreamLocalControlChannel { } /// Sets a wall-clock deadline for broker control-channel operations. - pub fn set_io_deadline(&mut self, deadline: Option) -> io::Result<()> { + pub fn set_io_deadline(&mut self, deadline: Option) -> IoResult<()> { self.io_deadline = deadline; self.active_request_deadline = None; match deadline { @@ -63,12 +63,12 @@ impl UnixStreamLocalControlChannel { } } - fn set_stream_io_timeout(&self, timeout: Option) -> io::Result<()> { + fn set_stream_io_timeout(&self, timeout: Option) -> IoResult<()> { self.stream.set_read_timeout(timeout)?; self.stream.set_write_timeout(timeout) } - fn current_deadline(&mut self) -> io::Result> { + fn current_deadline(&mut self) -> IoResult> { if let Some(deadline) = self.io_deadline { return Ok(Some(deadline)); } @@ -83,7 +83,7 @@ impl UnixStreamLocalControlChannel { Ok(Some(deadline)) } - fn clear_active_request_deadline(&mut self) -> io::Result<()> { + fn clear_active_request_deadline(&mut self) -> IoResult<()> { if self.io_deadline.is_none() { self.active_request_deadline = None; self.set_stream_io_timeout(self.io_timeout)?; @@ -108,7 +108,7 @@ impl UnixStreamHostControlChannel { } /// Sets a wall-clock deadline for all broker control-channel operations. - pub fn set_io_deadline(&mut self, deadline: Option) -> io::Result<()> { + pub fn set_io_deadline(&mut self, deadline: Option) -> IoResult<()> { self.io_deadline = deadline; if let Some(deadline) = deadline { let timeout = io_timeout_for_deadline(deadline)?; @@ -122,9 +122,9 @@ impl UnixStreamHostControlChannel { } impl LocalControlChannel for UnixStreamLocalControlChannel { - type Error = io::Error; + type Error = Error; - fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { + fn send_request(&mut self, request: &BrokerRequest) -> IoResult<()> { let frame = encode_request(request.clone()); let deadline = self.current_deadline()?; let result = write_frame_with_deadline(&mut self.stream, &frame, deadline); @@ -134,7 +134,7 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { result } - fn recv_response(&mut self) -> io::Result> { + fn recv_response(&mut self) -> IoResult> { let deadline = self.current_deadline()?; let result = match read_frame_with_deadline(&mut self.stream, deadline)? { Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), @@ -146,22 +146,22 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { } impl HostControlChannel for UnixStreamHostControlChannel { - type Error = io::Error; + type Error = Error; - fn peer_credential(&self) -> io::Result { + fn peer_credential(&self) -> IoResult { // TODO(broker): replace the PoC placeholder with Unix peer credential extraction // before this channel is used as an authenticated deployment boundary. Ok(PeerCredential::Unauthenticated) } - fn recv_request(&mut self) -> io::Result> { + fn recv_request(&mut self) -> IoResult> { let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { return Ok(None); }; decode_request(&frame).map(Some).map_err(wire_error) } - fn send_response(&mut self, response: &BrokerResponse) -> io::Result<()> { + fn send_response(&mut self, response: &BrokerResponse) -> IoResult<()> { write_frame_with_deadline( &mut self.stream, &encode_response(response.clone()), @@ -173,7 +173,7 @@ impl HostControlChannel for UnixStreamHostControlChannel { fn read_frame_with_deadline( stream: &mut UnixStream, deadline: Option, -) -> io::Result>> { +) -> IoResult>> { let mut len_buf = [0; 4]; let mut read = 0; while read < len_buf.len() { @@ -182,7 +182,7 @@ fn read_frame_with_deadline( Ok(0) if read == 0 => return Ok(None), Ok(0) => return Err(invalid_data("truncated broker frame length")), Ok(len) => read += len, - Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) => return Err(error), } } @@ -199,7 +199,7 @@ fn read_frame_with_deadline( match stream.read(&mut frame[read..]) { Ok(0) => return Err(invalid_data("truncated broker frame")), Ok(len) => read += len, - Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) => return Err(error), } } @@ -210,7 +210,7 @@ fn write_frame_with_deadline( stream: &mut UnixStream, frame: &[u8], deadline: Option, -) -> io::Result<()> { +) -> IoResult<()> { if frame.is_empty() || frame.len() > MAX_FRAME_LEN { return Err(invalid_data("invalid broker frame length")); } @@ -223,25 +223,25 @@ fn write_all_with_deadline( stream: &mut UnixStream, mut buffer: &[u8], deadline: Option, -) -> io::Result<()> { +) -> IoResult<()> { while !buffer.is_empty() { refresh_stream_io_deadline(stream, deadline)?; match stream.write(buffer) { Ok(0) => { - return Err(io::Error::new( - io::ErrorKind::WriteZero, + return Err(Error::new( + ErrorKind::WriteZero, "failed to write broker frame", )); } Ok(written) => buffer = &buffer[written..], - Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) => return Err(error), } } Ok(()) } -fn refresh_stream_io_deadline(stream: &UnixStream, deadline: Option) -> io::Result<()> { +fn refresh_stream_io_deadline(stream: &UnixStream, deadline: Option) -> IoResult<()> { if let Some(deadline) = deadline { let timeout = io_timeout_for_deadline(deadline)?; stream.set_read_timeout(Some(timeout))?; @@ -250,27 +250,27 @@ fn refresh_stream_io_deadline(stream: &UnixStream, deadline: Option) -> Ok(()) } -fn io_timeout_for_deadline(deadline: Instant) -> io::Result { +fn io_timeout_for_deadline(deadline: Instant) -> IoResult { let timeout = deadline .checked_duration_since(Instant::now()) .filter(|timeout| !timeout.is_zero()) - .ok_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "broker I/O deadline expired"))?; + .ok_or_else(|| Error::new(ErrorKind::TimedOut, "broker I/O deadline expired"))?; Ok(timeout) } -fn deadline_after(timeout: Duration) -> io::Result { +fn deadline_after(timeout: Duration) -> IoResult { Instant::now() .checked_add(timeout) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "broker I/O timeout overflow")) + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "broker I/O timeout overflow")) } -fn invalid_data(message: &'static str) -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, message) +fn invalid_data(message: &'static str) -> Error { + Error::new(ErrorKind::InvalidData, message) } -fn wire_error(error: WireError) -> io::Error { - io::Error::new( - io::ErrorKind::InvalidData, +fn wire_error(error: WireError) -> Error { + Error::new( + ErrorKind::InvalidData, format!("invalid broker wire message: {error}"), ) } @@ -313,7 +313,7 @@ mod tests { read_frame_with_deadline(&mut reader, None) .unwrap_err() .kind(), - io::ErrorKind::InvalidData + ErrorKind::InvalidData ); let (mut writer, mut reader) = UnixStream::pair().unwrap(); @@ -322,7 +322,7 @@ mod tests { read_frame_with_deadline(&mut reader, None) .unwrap_err() .kind(), - io::ErrorKind::InvalidData + ErrorKind::InvalidData ); let (mut writer, mut reader) = UnixStream::pair().unwrap(); @@ -333,7 +333,7 @@ mod tests { read_frame_with_deadline(&mut reader, None) .unwrap_err() .kind(), - io::ErrorKind::InvalidData + ErrorKind::InvalidData ); let (mut writer, mut reader) = UnixStream::pair().unwrap(); @@ -344,7 +344,7 @@ mod tests { read_frame_with_deadline(&mut reader, None) .unwrap_err() .kind(), - io::ErrorKind::InvalidData + ErrorKind::InvalidData ); } @@ -367,10 +367,7 @@ mod tests { let error = reader.join().expect("timeout reader panicked"); assert!( - matches!( - error.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut - ), + matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut), "unexpected timeout error kind: {error:?}" ); } diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 6fb7d9ddf0..84598aa7eb 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -6,7 +6,6 @@ use std::ffi::OsString; use std::os::unix::net::UnixListener; use std::path::PathBuf; use std::process::Command; -use std::thread; use clap::Parser; use litebox_broker_core::{BrokerCore, PolicyEngine, PrincipalRights}; @@ -41,7 +40,7 @@ fn main() -> Result<(), Box> { .arg(&socket_path) .args(&args.runner_arguments); let mut runner = runner_command.spawn()?; - let _runner_waiter = thread::spawn(move || { + let _runner_waiter = std::thread::spawn(move || { if let Err(error) = runner.wait() { eprintln!("failed to wait for local runner: {error}"); } diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 06db85996b..ea4090fa57 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -1,13 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use std::env; use std::ffi::{OsStr, OsString}; -use std::io; +use std::io::{Error, ErrorKind, Result}; use std::os::unix::process::ExitStatusExt; use std::path::Path; use std::process::{Child, Command}; -use std::thread; use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; @@ -17,7 +15,7 @@ use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; const RUNNER_ARGUMENT: &str = "broker-userland-test-runner"; fn main() { - let args = env::args_os().skip(1).collect::>(); + let args = std::env::args_os().skip(1).collect::>(); if args .first() .is_some_and(|arg| arg == OsStr::new("--unstable")) @@ -39,7 +37,7 @@ fn run_parent_test() { let mut broker = ChildGuard { child: Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")) .arg("--runner") - .arg(env::current_exe().unwrap()) + .arg(std::env::current_exe().unwrap()) .arg(RUNNER_ARGUMENT) .spawn() .unwrap(), @@ -51,7 +49,7 @@ fn run_parent_test() { assert_eq!(status.signal(), Some(libc::SIGTERM)); return; } - thread::sleep(Duration::from_millis(10)); + std::thread::sleep(Duration::from_millis(10)); } panic!("timed out waiting for broker to stop"); } @@ -112,7 +110,7 @@ fn run_fake_runner(args: &[OsString]) { kill_result, 0, "failed to stop broker: {}", - io::Error::last_os_error() + Error::last_os_error() ); } @@ -129,18 +127,18 @@ impl Drop for ChildGuard { } } -fn connect_with_retry(socket_path: &Path) -> io::Result { +fn connect_with_retry(socket_path: &Path) -> Result { let deadline = Instant::now() + Duration::from_secs(5); loop { match UnixStreamLocalControlChannel::connect(socket_path) { Ok(channel) => return Ok(channel), Err(error) if Instant::now() < deadline => { - if error.kind() != io::ErrorKind::NotFound - && error.kind() != io::ErrorKind::ConnectionRefused + if error.kind() != ErrorKind::NotFound + && error.kind() != ErrorKind::ConnectionRefused { return Err(error); } - thread::sleep(Duration::from_millis(10)); + std::thread::sleep(Duration::from_millis(10)); } Err(error) => return Err(error), } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 1d7a2525e7..fb000e8f5e 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -3,7 +3,6 @@ use std::{ path::Path, - thread, time::{Duration, Instant}, }; @@ -61,6 +60,6 @@ fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result Date: Wed, 24 Jun 2026 16:16:30 -0700 Subject: [PATCH 061/319] Avoid broker socket timeout syscalls (#960) This PR avoids weakening the Linux userland seccomp filter for broker socket timeouts. The runner now uses a setup-only broker negotiation deadline, clears it before active broker requests, and no longer needs `setsockopt(SO_RCVTIMEO/SO_SNDTIMEO)` in the seccomp allowlist. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_transport/src/unix_socket.rs | 12 ++---- litebox_platform_linux_userland/src/lib.rs | 42 --------------------- litebox_runner_linux_userland/src/broker.rs | 14 +++---- 3 files changed, 10 insertions(+), 58 deletions(-) diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 0d1d2d2ab0..f9b49a4529 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -82,14 +82,6 @@ impl UnixStreamLocalControlChannel { self.active_request_deadline = Some(deadline); Ok(Some(deadline)) } - - fn clear_active_request_deadline(&mut self) -> IoResult<()> { - if self.io_deadline.is_none() { - self.active_request_deadline = None; - self.set_stream_io_timeout(self.io_timeout)?; - } - Ok(()) - } } /// Host-side Unix-domain-socket control channel for the hosted userland POC. @@ -140,7 +132,9 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), None => Ok(None), }; - self.clear_active_request_deadline()?; + if self.io_deadline.is_none() && self.active_request_deadline.take().is_some() { + self.set_stream_io_timeout(self.io_timeout)?; + } result } } diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index ece99163f7..770ccaf4ed 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -481,48 +481,6 @@ impl LinuxUserland { .unwrap(), ], ), - // Broker control-channel I/O runs through a host Unix socket in the - // current POC. The transport refreshes read/write timeouts around - // each request. - ( - libc::SYS_setsockopt, - vec![ - SeccompRule::new(vec![ - SeccompCondition::new( - 1, - SeccompCmpArgLen::Dword, - SeccompCmpOp::Eq, - libc::SOL_SOCKET as u64, - ) - .unwrap(), - SeccompCondition::new( - 2, - SeccompCmpArgLen::Dword, - SeccompCmpOp::Eq, - libc::SO_RCVTIMEO as u64, - ) - .unwrap(), - ]) - .unwrap(), - SeccompRule::new(vec![ - SeccompCondition::new( - 1, - SeccompCmpArgLen::Dword, - SeccompCmpOp::Eq, - libc::SOL_SOCKET as u64, - ) - .unwrap(), - SeccompCondition::new( - 2, - SeccompCmpArgLen::Dword, - SeccompCmpOp::Eq, - libc::SO_SNDTIMEO as u64, - ) - .unwrap(), - ]) - .unwrap(), - ], - ), // Connected UnixStream I/O may use sendto/recvfrom rather than raw // read/write. Limit these rules to connected-socket calls that do // not name a peer address. diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index fb000e8f5e..dce9cc9cce 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -11,7 +11,6 @@ use litebox_broker_local::BrokerLocal; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); -const ACTIVE_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); const RETRY_DELAY: Duration = Duration::from_millis(20); type Local = BrokerLocal; @@ -34,12 +33,8 @@ impl BrokerConnection { fn connect_to_endpoint(socket_path: &Path) -> Result { let setup_deadline = Instant::now() + SETUP_TIMEOUT; - let mut local = connect_with_retry(socket_path, setup_deadline) + let local = connect_with_retry(socket_path, setup_deadline) .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; - local - .control_channel_mut() - .set_io_timeout(Some(ACTIVE_REQUEST_TIMEOUT)) - .context("failed to configure broker active request timeout")?; Ok(BrokerConnection { local }) } @@ -50,7 +45,12 @@ fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { From 5a71ab519c01ec4c91a42a5ea479533b74b1983c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 24 Jun 2026 16:54:58 -0700 Subject: [PATCH 062/319] Remove broker protocol root re-exports (#962) This PR removes broad root-level re-exports from litebox_broker_protocol so consumers import broker protocol types from their defining modules. Updates broker/local/host/transport/runner call sites accordingly without changing wire shapes or behavior. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 2 +- litebox/src/broker/mod.rs | 3 ++- litebox/src/event/counter.rs | 9 ++++---- litebox/src/litebox.rs | 2 +- litebox_broker_core/src/error.rs | 2 +- litebox_broker_core/src/event.rs | 3 ++- litebox_broker_core/src/session.rs | 5 ++--- litebox_broker_host/src/error.rs | 2 +- litebox_broker_host/src/lib.rs | 12 +++++----- litebox_broker_local/src/error.rs | 2 +- litebox_broker_local/src/event.rs | 10 +++++---- litebox_broker_local/src/lib.rs | 18 +++++++-------- litebox_broker_protocol/src/channel.rs | 2 +- litebox_broker_protocol/src/lib.rs | 11 ---------- litebox_broker_protocol/src/message.rs | 5 +++-- litebox_broker_protocol/src/wire.rs | 12 +++++----- .../src/wire/core_message.rs | 2 +- litebox_broker_protocol/src/wire/event.rs | 7 +++--- litebox_broker_transport/src/unix_socket.rs | 5 ++--- .../tests/userland_broker.rs | 2 +- litebox_runner_linux_userland/tests/run.rs | 22 +++++++++++-------- 21 files changed, 70 insertions(+), 68 deletions(-) diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 3eb7f4923f..c5497262bb 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -2,7 +2,7 @@ // Licensed under the MIT license. use litebox_broker_local::BrokerLocalError; -use litebox_broker_protocol::ErrorCode; +use litebox_broker_protocol::error::ErrorCode; use thiserror::Error; use crate::event::{counter::EventCounterError, polling::TryOpError}; diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 990202f08e..25ba577ca2 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -2,7 +2,8 @@ // Licensed under the MIT license. use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; +use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::message::{CoreRequest, CoreResponse}; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 4c8b96c068..c590b0ca0a 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -3,12 +3,13 @@ use alloc::sync::Arc; -pub use litebox_broker_protocol::EventConsumeMode as EventCounterReadMode; -use litebox_broker_protocol::{ - AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, - CreateEventRequest, EventRequest, EventResponse, ObjectHandle, ReadinessState, +use litebox_broker_protocol::ObjectHandle; +pub use litebox_broker_protocol::event::EventConsumeMode as EventCounterReadMode; +use litebox_broker_protocol::event::{ + AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, ReadinessState, WaitEventRequest, }; +use litebox_broker_protocol::message::{CoreRequest, CoreResponse, EventRequest, EventResponse}; use thiserror::Error; use crate::{ diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 7ddc6bd585..ffd1b1c2ca 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -6,7 +6,7 @@ use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::LocalControlChannel; +use litebox_broker_protocol::channel::LocalControlChannel; use crate::{ broker, diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index 7c45916d9f..cd610a999f 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -3,7 +3,7 @@ use thiserror::Error; -use litebox_broker_protocol::ErrorCode; +use litebox_broker_protocol::error::ErrorCode; /// Broker authority error category. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq, Hash)] diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index acb19efbf3..db72026e3a 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -5,7 +5,8 @@ use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result}; -use litebox_broker_protocol::{EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState}; +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::event::{EventConsumeMode, EventConsumption, ReadinessState}; pub(crate) const MAX_EVENT_COUNT: u64 = u64::MAX - 1; diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index 61c9e2395b..c4e15396b3 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -180,9 +180,8 @@ mod tests { use crate::{ BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine, PrincipalRights, }; - use litebox_broker_protocol::{ - EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, - }; + use litebox_broker_protocol::ObjectHandle; + use litebox_broker_protocol::event::{EventConsumeMode, EventConsumption, ReadinessState}; #[test] fn object_reference_lifecycle_uses_public_core_constructor_once() { diff --git a/litebox_broker_host/src/error.rs b/litebox_broker_host/src/error.rs index d69d901235..1338637827 100644 --- a/litebox_broker_host/src/error.rs +++ b/litebox_broker_host/src/error.rs @@ -2,7 +2,7 @@ // Licensed under the MIT license. use litebox_broker_core::BrokerError; -use litebox_broker_protocol::ErrorCode; +use litebox_broker_protocol::error::ErrorCode; use thiserror::Error; /// Errors returned by a broker-host receive/send loop. diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 4eee962992..f21923cd2c 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -15,10 +15,12 @@ extern crate std; use core::fmt::{Display, Formatter, Result as FmtResult}; use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential}; -use litebox_broker_protocol::{ - AddEventResponse, BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, CoreRequest, - CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, - PeerCredential, WaitEventResponse, +use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; +use litebox_broker_protocol::channel::{HostControlChannel, PeerCredential}; +use litebox_broker_protocol::error::ErrorCode; +use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse, WaitEventResponse}; +use litebox_broker_protocol::message::{ + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, }; mod error; @@ -202,7 +204,7 @@ pub enum ConnectionTermination { mod tests { use super::*; use litebox_broker_core::{PolicyEngine, PrincipalRights}; - use litebox_broker_protocol::CreateEventRequest; + use litebox_broker_protocol::event::CreateEventRequest; #[test] fn host_request_handling_uses_one_broker_core() { diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 542e9fba57..812fc38aab 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox_broker_protocol::ErrorCode; +use litebox_broker_protocol::error::ErrorCode; use thiserror::Error; /// Errors returned by active broker-local control requests. diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index fc3286c686..ead22f9b08 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox_broker_protocol::{ - AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, - CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, LocalControlChannel, - ObjectHandle, ReadinessState, WaitEventRequest, +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::event::{ + AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, + EventConsumeMode, ReadinessState, WaitEventRequest, }; +use litebox_broker_protocol::message::{CoreRequest, CoreResponse, EventRequest, EventResponse}; use crate::{BrokerLocal, Result}; diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index ec54ed0034..bb76a533aa 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -5,7 +5,7 @@ //! //! The local control adapter owns request/response sequencing but does not own a channel. //! Userland, kernel, or ring-buffer deployments can provide channels by -//! implementing [`litebox_broker_protocol::LocalControlChannel`]. +//! implementing [`litebox_broker_protocol::channel::LocalControlChannel`]. #![no_std] @@ -15,10 +15,10 @@ extern crate std; mod error; mod event; -use litebox_broker_protocol::{ - BROKER_PROTOCOL_VERSION, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, ErrorCode, - LocalControlChannel, -}; +use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; +use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::error::ErrorCode; +use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse, CoreRequest, CoreResponse}; pub use error::{BrokerLocalError, Result}; @@ -119,10 +119,10 @@ fn raw_request( mod tests { use super::*; use core::convert::Infallible; - use litebox_broker_protocol::{ - CreateEventRequest, CreateEventResponse, EventRequest, EventResponse, ObjectHandle, - ProtocolVersion, - }; + use litebox_broker_protocol::ObjectHandle; + use litebox_broker_protocol::ProtocolVersion; + use litebox_broker_protocol::event::{CreateEventRequest, CreateEventResponse}; + use litebox_broker_protocol::message::{EventRequest, EventResponse}; #[test] fn negotiate_returns_active_local_connection() { diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index e4a0b6b316..c0579d5823 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::{BrokerRequest, BrokerResponse}; +use crate::message::{BrokerRequest, BrokerResponse}; /// Peer identity information supplied by the channel or host layer. /// diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index dc36d17d2a..de47dd864d 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -18,17 +18,6 @@ pub mod event; pub mod message; pub mod wire; -pub use channel::{HostControlChannel, LocalControlChannel, PeerCredential}; -pub use error::ErrorCode; -pub use event::{ - AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, - CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, - WaitEventRequest, WaitEventResponse, -}; -pub use message::{ - BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, -}; - /// Opaque broker object reference handle. #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index adb870e801..a2669a5e61 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -2,9 +2,10 @@ // Licensed under the MIT license. use crate::ProtocolVersion; -use crate::{ +use crate::error::ErrorCode; +use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, - CreateEventRequest, CreateEventResponse, ErrorCode, WaitEventRequest, WaitEventResponse, + CreateEventRequest, CreateEventResponse, WaitEventRequest, WaitEventResponse, }; /// Broker request sent over the control channel. diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 65c655bca1..cd99f9f833 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -19,7 +19,8 @@ use alloc::vec::Vec; use thiserror::Error; -use crate::{BrokerRequest, BrokerResponse, ErrorCode}; +use crate::error::ErrorCode; +use crate::message::{BrokerRequest, BrokerResponse}; use primitive::{Decoder, Encoder}; @@ -143,12 +144,13 @@ pub fn decode_response(frame: &[u8]) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::{ - AddEventRequest, AddEventResponse, ConsumeEventRequest, CoreRequest, CoreResponse, - CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, EventRequest, - EventResponse, ObjectHandle, ProtocolVersion, ReadinessState, WaitEventRequest, + use crate::event::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, + CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, WaitEventRequest, WaitEventResponse, }; + use crate::message::{CoreRequest, CoreResponse, EventRequest, EventResponse}; + use crate::{ObjectHandle, ProtocolVersion}; #[test] fn request_codec_round_trips_all_variants() { diff --git a/litebox_broker_protocol/src/wire/core_message.rs b/litebox_broker_protocol/src/wire/core_message.rs index 04ee6cefbb..892d385cca 100644 --- a/litebox_broker_protocol/src/wire/core_message.rs +++ b/litebox_broker_protocol/src/wire/core_message.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::{CoreRequest, CoreResponse}; +use crate::message::{CoreRequest, CoreResponse}; use super::WireError; use super::primitive::{Decoder, Encoder}; diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index c96a2d74e6..7892f94cd1 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::{ +use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, - CreateEventResponse, EventConsumeMode, EventConsumption, EventRequest, EventResponse, - ReadinessState, WaitEventRequest, WaitEventResponse, + CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, WaitEventRequest, + WaitEventResponse, }; +use crate::message::{EventRequest, EventResponse}; use super::WireError; use super::primitive::{Decoder, Encoder}; diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index f9b49a4529..d5faed05dc 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -12,12 +12,11 @@ use std::os::unix::net::UnixStream; use std::path::Path; use std::time::{Duration, Instant}; +use litebox_broker_protocol::channel::{HostControlChannel, LocalControlChannel, PeerCredential}; +use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse}; use litebox_broker_protocol::wire::{ WireError, decode_request, decode_response, encode_request, encode_response, }; -use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, HostControlChannel, LocalControlChannel, PeerCredential, -}; const MAX_FRAME_LEN: usize = 64 * 1024; diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index ea4090fa57..9177e669e4 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -9,7 +9,7 @@ use std::process::{Child, Command}; use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::ReadinessState; +use litebox_broker_protocol::event::ReadinessState; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; const RUNNER_ARGUMENT: &str = "broker-userland-test-runner"; diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 203d8a123e..7287e740ed 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -359,13 +359,15 @@ fn spawn_test_broker( } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -struct CountingHostControlChannel { +struct CountingHostControlChannel { inner: Channel, event_request_count: usize, } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -impl CountingHostControlChannel { +impl + CountingHostControlChannel +{ const fn new(inner: Channel) -> Self { Self { inner, @@ -379,23 +381,25 @@ impl CountingHostControlCh } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -impl - litebox_broker_protocol::HostControlChannel for CountingHostControlChannel +impl + litebox_broker_protocol::channel::HostControlChannel for CountingHostControlChannel { type Error = Channel::Error; - fn peer_credential(&self) -> Result { + fn peer_credential( + &self, + ) -> Result { self.inner.peer_credential() } fn recv_request( &mut self, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { let request = self.inner.recv_request()?; if matches!( request, - Some(litebox_broker_protocol::BrokerRequest::Core( - litebox_broker_protocol::CoreRequest::Event(_) + Some(litebox_broker_protocol::message::BrokerRequest::Core( + litebox_broker_protocol::message::CoreRequest::Event(_) )) ) { self.event_request_count += 1; @@ -405,7 +409,7 @@ impl fn send_response( &mut self, - response: &litebox_broker_protocol::BrokerResponse, + response: &litebox_broker_protocol::message::BrokerResponse, ) -> Result<(), Self::Error> { self.inner.send_response(response) } From 3c9c273adcd580c3fef7b2aae3d920ab4ed9367a Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Wed, 24 Jun 2026 18:32:51 -0700 Subject: [PATCH 063/319] Cherry pick "Fix TLB flush hypercall (LVBS platform)" (#964) Co-authored-by: Sangho Lee --- litebox_platform_lvbs/src/mshv/hvcall_mm.rs | 50 +++++++-------------- litebox_platform_lvbs/src/mshv/mod.rs | 22 +++++++-- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/litebox_platform_lvbs/src/mshv/hvcall_mm.rs b/litebox_platform_lvbs/src/mshv/hvcall_mm.rs index 31c236037b..3955dec8dc 100644 --- a/litebox_platform_lvbs/src/mshv/hvcall_mm.rs +++ b/litebox_platform_lvbs/src/mshv/hvcall_mm.rs @@ -5,10 +5,10 @@ #[cfg(not(test))] use crate::mshv::{ - HV_FLUSH_ALL_VIRTUAL_ADDRESS_SPACES, HV_FLUSH_EX_VP_SET_BANKS, HV_GENERIC_SET_SPARSE_4K, - HVCALL_FLUSH_VIRTUAL_ADDRESS_LIST_EX, HVCALL_FLUSH_VIRTUAL_ADDRESS_SPACE_EX, - HvInputFlushVirtualAddressListEx, HvInputFlushVirtualAddressSpaceEx, - hvcall::hv_do_hypercall, + HV_FLUSH_ALL_VIRTUAL_ADDRESS_SPACES, HV_FLUSH_EX_ALL_BANKS_VALID, + HV_FLUSH_EX_VP_SET_QWORD_COUNT, HV_GENERIC_SET_SPARSE_4K, HVCALL_FLUSH_VIRTUAL_ADDRESS_LIST_EX, + HVCALL_FLUSH_VIRTUAL_ADDRESS_SPACE_EX, HvInputFlushVirtualAddressListEx, + HvInputFlushVirtualAddressSpaceEx, vtl_switch::{is_only_vp_in_vtl1, vtl1_vp_mask}, }; use crate::{ @@ -22,27 +22,6 @@ use crate::{ }; use litebox::utils::TruncateExt; -/// Compute the valid-bank bitmask for a sparse VP set -/// (). -/// -/// Returns a `u64` where bit *i* is set if `vp_set_bank_contents[i]` is -/// non-zero, indicating that the corresponding bank carries at least one -/// target VP. The hypervisor uses this mask to skip empty banks. -#[cfg(not(test))] -#[inline] -fn vp_set_valid_bank_mask(vp_set_bank_contents: [u64; HV_FLUSH_EX_VP_SET_BANKS]) -> u64 { - vp_set_bank_contents - .iter() - .enumerate() - .fold(0u64, |mask, (bank, contents)| { - if *contents != 0 { - mask | (1u64 << bank) - } else { - mask - } - }) -} - /// Hyper-V Hypercall to prevent lower VTLs (i.e., VTL0) from accessing a specified range of /// guest physical memory pages with a given protection flag. pub fn hv_modify_vtl_protection_mask( @@ -103,9 +82,8 @@ pub(crate) fn hv_flush_virtual_address_space() -> Result<(), HypervCallError> { } let vp_mask = vtl1_vp_mask(); - let valid_bank_mask = vp_set_valid_bank_mask(vp_mask); debug_assert!( - valid_bank_mask != 0, + vp_mask.iter().any(|&bank| bank != 0), "caller is in VTL1 but VP mask is empty" ); @@ -115,12 +93,17 @@ pub(crate) fn hv_flush_virtual_address_space() -> Result<(), HypervCallError> { address_space: 0, flags: HV_FLUSH_ALL_VIRTUAL_ADDRESS_SPACES, vp_set_format: HV_GENERIC_SET_SPARSE_4K, - vp_set_valid_bank_mask: valid_bank_mask, + vp_set_valid_bank_mask: HV_FLUSH_EX_ALL_BANKS_VALID, vp_set_bank_contents: vp_mask, }; - hv_do_hypercall( - u64::from(HVCALL_FLUSH_VIRTUAL_ADDRESS_SPACE_EX), + // The VP set is this hypercall's variable header, so it must be + // issued as a rep hypercall (with zero rep elements) that declares + // the bank count via `varhead`. + hv_do_rep_hypercall( + HVCALL_FLUSH_VIRTUAL_ADDRESS_SPACE_EX, + 0, + HV_FLUSH_EX_VP_SET_QWORD_COUNT, (&raw const *input).cast::(), core::ptr::null_mut(), )?; @@ -159,9 +142,8 @@ pub(crate) fn hv_flush_virtual_address_list( } let vp_mask = vtl1_vp_mask(); - let valid_bank_mask = vp_set_valid_bank_mask(vp_mask); debug_assert!( - valid_bank_mask != 0, + vp_mask.iter().any(|&bank| bank != 0), "caller is in VTL1 but VP mask is empty" ); @@ -170,7 +152,7 @@ pub(crate) fn hv_flush_virtual_address_list( input.address_space = 0; input.flags = HV_FLUSH_ALL_VIRTUAL_ADDRESS_SPACES; input.vp_set_format = HV_GENERIC_SET_SPARSE_4K; - input.vp_set_valid_bank_mask = valid_bank_mask; + input.vp_set_valid_bank_mask = HV_FLUSH_EX_ALL_BANKS_VALID; input.vp_set_bank_contents = vp_mask; let mut remaining = page_count; @@ -199,7 +181,7 @@ pub(crate) fn hv_flush_virtual_address_list( hv_do_rep_hypercall( HVCALL_FLUSH_VIRTUAL_ADDRESS_LIST_EX, gva_count, - HvInputFlushVirtualAddressListEx::VP_SET_QWORD_COUNT, + HV_FLUSH_EX_VP_SET_QWORD_COUNT, (&raw const *input).cast::(), core::ptr::null_mut(), )?; diff --git a/litebox_platform_lvbs/src/mshv/mod.rs b/litebox_platform_lvbs/src/mshv/mod.rs index 826daaa391..40dd206c4a 100644 --- a/litebox_platform_lvbs/src/mshv/mod.rs +++ b/litebox_platform_lvbs/src/mshv/mod.rs @@ -534,6 +534,24 @@ pub const HV_GENERIC_SET_SPARSE_4K: u64 = 0; /// Each bank contains 64 VPs. pub const HV_FLUSH_EX_VP_SET_BANKS: usize = MAX_CORES.div_ceil(64); +/// `valid_bank_mask` value that marks every VP-set bank valid. +/// +/// The sparse VP-set format permits a *valid* bank to carry an all-zero +/// contents mask (no VPs). Always declaring all `HV_FLUSH_EX_VP_SET_BANKS` +/// banks valid lets the input keep a fixed-size `vp_set_bank_contents` array +/// while targeting the VPs whose bits are set. Empty banks -> no flush. +pub const HV_FLUSH_EX_ALL_BANKS_VALID: u64 = (1u64 << HV_FLUSH_EX_VP_SET_BANKS) - 1; + +/// Number of 64-bit words the VP set contributes to an EX flush hypercall's +/// variable header (`varhead`). +/// +/// Only `vp_set_bank_contents` is variable-header; `vp_set_format` and +/// `vp_set_valid_bank_mask` are part of the hypercall's fixed input header. +/// All `HV_FLUSH_EX_VP_SET_BANKS` banks are always sent, so this applies to +/// both `HvInputFlushVirtualAddressSpaceEx` and `HvInputFlushVirtualAddressListEx`. +#[expect(clippy::cast_possible_truncation)] +pub const HV_FLUSH_EX_VP_SET_QWORD_COUNT: u16 = HV_FLUSH_EX_VP_SET_BANKS as u16; + /// Input structure for `HvCallFlushVirtualAddressSpaceEx` (0x0013). /// /// Layout (): @@ -579,10 +597,6 @@ const HV_FLUSH_EX_MAX_GVAS: usize = ((PAGE_SIZE as u32 / (u64::BITS / 8)) as usize; impl HvInputFlushVirtualAddressListEx { - /// Number of 64-bit words occupied by the VP-set variable header. - #[allow(clippy::cast_possible_truncation)] - pub const VP_SET_QWORD_COUNT: u16 = (2 + HV_FLUSH_EX_VP_SET_BANKS) as u16; - /// Maximum number of GVA range entries per EX hypercall invocation. pub const MAX_GVAS_PER_REQUEST: usize = HV_FLUSH_EX_MAX_GVAS; } From 217308dc2fd0e7e66b2662c415190fe9f082a31f Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 25 Jun 2026 16:29:22 -0700 Subject: [PATCH 064/319] Split broker request phases and remove core message layer (#965) Splits broker negotiation into dedicated handshake request/response types while keeping active broker operations in the active request/response envelope. Removes the `CoreRequest`/`CoreResponse` message layer so event requests and responses sit directly under active broker messages, with wire/channel/host/local call sites updated consistently. Wrong-phase frames are handled as protocol violations. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 12 +- litebox/src/event/counter.rs | 20 +- litebox_broker_host/src/lib.rs | 329 ++++++++++-------- litebox_broker_local/src/event.rs | 17 +- litebox_broker_local/src/lib.rs | 144 +++++--- litebox_broker_protocol/src/channel.rs | 51 ++- litebox_broker_protocol/src/message.rs | 65 ++-- litebox_broker_protocol/src/wire.rs | 313 ++++++++++++----- .../src/wire/core_message.rs | 50 --- litebox_broker_transport/src/unix_socket.rs | 110 +++++- .../tests/userland_broker.rs | 2 +- litebox_runner_linux_userland/tests/run.rs | 58 +-- 12 files changed, 728 insertions(+), 443 deletions(-) delete mode 100644 litebox_broker_protocol/src/wire/core_message.rs diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 25ba577ca2..408aa955fc 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -3,7 +3,7 @@ use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::channel::LocalControlChannel; -use litebox_broker_protocol::message::{CoreRequest, CoreResponse}; +use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse}; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; @@ -16,11 +16,11 @@ use error::BrokerControlError; /// requests. Deployment code owns endpoint selection and supplies the connected /// transport behind this protocol-level boundary. pub(crate) trait BrokerControl: Send + Sync { - /// Sends one active BrokerCore request and returns its response. + /// Sends one active broker request and returns its response. fn request( &self, - request: CoreRequest, - ) -> core::result::Result; + request: BrokerRequest, + ) -> core::result::Result; } pub(crate) struct BrokerLocalControl< @@ -49,8 +49,8 @@ where { fn request( &self, - request: CoreRequest, - ) -> core::result::Result { + request: BrokerRequest, + ) -> core::result::Result { Ok(self.local.lock().request(request)?) } } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index c590b0ca0a..bdda5cf734 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -9,7 +9,9 @@ use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, ReadinessState, WaitEventRequest, }; -use litebox_broker_protocol::message::{CoreRequest, CoreResponse, EventRequest, EventResponse}; +use litebox_broker_protocol::message::{ + BrokerRequest, BrokerResponse, EventRequest, EventResponse, +}; use thiserror::Error; use crate::{ @@ -63,13 +65,12 @@ where return Err(EventCounterError::Unavailable); }; let response = broker - .request(CoreRequest::Event(EventRequest::Create( + .request(BrokerRequest::Event(EventRequest::Create( CreateEventRequest { initial_count }, ))) .map_err(BrokerObjectError::from) .map_err(EventCounterError::from)?; - let CoreResponse::Event(response) = response; - let EventResponse::Create(response) = response else { + let BrokerResponse::Event(EventResponse::Create(response)) = response else { panic!("broker returned unexpected event response: {response:?}"); }; Ok(Self { @@ -140,11 +141,14 @@ where } fn request_event(&self, request: EventRequest) -> Result { - let CoreResponse::Event(response) = self + match self .broker - .request(CoreRequest::Event(request)) - .map_err(BrokerObjectError::from)?; - Ok(response) + .request(BrokerRequest::Event(request)) + .map_err(BrokerObjectError::from)? + { + BrokerResponse::Event(response) => Ok(response), + BrokerResponse::Error(error) => Err(error.into()), + } } } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index f21923cd2c..376cb43692 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -12,22 +12,20 @@ #[cfg(test)] extern crate std; -use core::fmt::{Display, Formatter, Result as FmtResult}; - use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential}; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; -use litebox_broker_protocol::channel::{HostControlChannel, PeerCredential}; +use litebox_broker_protocol::channel::{HostControlChannel, HostReceive, PeerCredential}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse, WaitEventResponse}; use litebox_broker_protocol::message::{ - BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, + BrokerHandshakeResponse, BrokerRequest, BrokerResponse, EventRequest, EventResponse, }; mod error; pub use error::{BrokerHostError, Result}; -/// Serves one broker connection over the provided connected control channel. +/// Authenticates, negotiates, and serves one broker connection over the control channel. pub fn serve_connection( core: &BrokerCore, channel: &mut Channel, @@ -44,6 +42,41 @@ where }; let session = core.create_session(caller_credential)?; + loop { + let request = match channel + .recv_handshake_request() + .map_err(BrokerHostError::Channel)? + { + HostReceive::Message(request) => request, + HostReceive::ProtocolViolation => { + channel + .send_handshake_response(&BrokerHandshakeResponse::Error( + ErrorCode::ProtocolState, + )) + .map_err(BrokerHostError::Channel)?; + return Ok(ConnectionTermination::ProtocolViolation); + } + HostReceive::PeerClosed => return Ok(ConnectionTermination::PeerClosed), + }; + + let negotiated = request.protocol_version == BROKER_PROTOCOL_VERSION; + let response = if negotiated { + BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + } + } else { + BrokerHandshakeResponse::VersionMismatch { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + } + }; + channel + .send_handshake_response(&response) + .map_err(BrokerHostError::Channel)?; + if negotiated { + break; + } + } + serve_request_loop(channel, &session) } @@ -54,68 +87,30 @@ fn serve_request_loop( where Channel: HostControlChannel, { - let mut state = ConnectionState::AwaitingNegotiation; loop { - let Some(request) = channel.recv_request().map_err(BrokerHostError::Channel)? else { - break; + let request = match channel.recv_request().map_err(BrokerHostError::Channel)? { + HostReceive::Message(request) => request, + HostReceive::ProtocolViolation => { + channel + .send_response(&BrokerResponse::Error(ErrorCode::ProtocolState)) + .map_err(BrokerHostError::Channel)?; + return Ok(ConnectionTermination::ProtocolViolation); + } + HostReceive::PeerClosed => break, }; - let dispatch = handle_request(session, &mut state, request); + let response = handle_request(session, request); channel - .send_response(&dispatch.response) + .send_response(&response) .map_err(BrokerHostError::Channel)?; - if let DispatchOutcome::Close(reason) = dispatch.outcome { - return Ok(ConnectionTermination::BrokerClosed(reason)); - } } Ok(ConnectionTermination::PeerClosed) } -fn handle_request( - session: &BrokerSession, - state: &mut ConnectionState, - request: BrokerRequest, -) -> BrokerDispatch { - match *state { - ConnectionState::AwaitingNegotiation => match request { - BrokerRequest::Negotiate { protocol_version } => { - if protocol_version == BROKER_PROTOCOL_VERSION { - *state = ConnectionState::Active; - BrokerDispatch { - response: BrokerResponse::Negotiated { - broker_protocol_version: BROKER_PROTOCOL_VERSION, - }, - outcome: DispatchOutcome::Continue, - } - } else { - BrokerDispatch { - response: BrokerResponse::VersionMismatch { - broker_protocol_version: BROKER_PROTOCOL_VERSION, - }, - outcome: DispatchOutcome::Continue, - } - } - } - BrokerRequest::Core(_) => BrokerDispatch { - response: BrokerResponse::Error(ErrorCode::ProtocolState), - outcome: DispatchOutcome::Close(CloseReason::ProtocolViolation), - }, - }, - ConnectionState::Active => handle_active_request(session, request), - } -} - -fn handle_active_request(session: &BrokerSession, request: BrokerRequest) -> BrokerDispatch { +fn handle_request(session: &BrokerSession, request: BrokerRequest) -> BrokerResponse { match request { - BrokerRequest::Negotiate { .. } => BrokerDispatch { - response: BrokerResponse::Error(ErrorCode::ProtocolState), - outcome: DispatchOutcome::Close(CloseReason::ProtocolViolation), - }, - BrokerRequest::Core(CoreRequest::Event(request)) => BrokerDispatch { - response: handle_event_request(session, request), - outcome: DispatchOutcome::Continue, - }, + BrokerRequest::Event(request) => handle_event_request(session, request), } } @@ -123,88 +118,54 @@ fn handle_event_request(session: &BrokerSession, request: EventRequest) -> Broke match request { EventRequest::Create(request) => { match litebox_broker_core::event::create(session, request.initial_count) { - Ok(handle) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Create( - CreateEventResponse { handle }, - ))), + Ok(handle) => { + BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })) + } Err(error) => BrokerResponse::Error(error.into()), } } EventRequest::Wait(request) => { match litebox_broker_core::event::wait(session, request.handle) { - Ok(readiness) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait( - WaitEventResponse { readiness }, - ))), + Ok(readiness) => { + BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { readiness })) + } Err(error) => BrokerResponse::Error(error.into()), } } EventRequest::Add(request) => { match litebox_broker_core::event::add(session, request.handle, request.value) { - Ok(readiness) => BrokerResponse::Core(CoreResponse::Event(EventResponse::Add( - AddEventResponse { readiness }, - ))), + Ok(readiness) => { + BrokerResponse::Event(EventResponse::Add(AddEventResponse { readiness })) + } Err(error) => BrokerResponse::Error(error.into()), } } EventRequest::Consume(request) => { match litebox_broker_core::event::consume(session, request.handle, request.mode) { - Ok(consumption) => { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(consumption))) - } + Ok(consumption) => BrokerResponse::Event(EventResponse::Consume(consumption)), Err(error) => BrokerResponse::Error(error.into()), } } } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ConnectionState { - AwaitingNegotiation, - Active, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct BrokerDispatch { - response: BrokerResponse, - outcome: DispatchOutcome, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum DispatchOutcome { - Continue, - Close(CloseReason), -} - -/// Reason the broker host closed the connection after sending a response. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum CloseReason { - /// The peer violated the request sequencing state machine. - ProtocolViolation, -} - -impl Display for CloseReason { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match self { - Self::ProtocolViolation => f.write_str("protocol violation"), - } - } -} - -/// Terminal outcome for a successfully served broker connection. +/// Terminal outcome after processing one broker connection. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum ConnectionTermination { /// The peer cleanly closed the channel. PeerClosed, - /// The host sent a terminal protocol response and closed the connection. - BrokerClosed(CloseReason), + /// The broker sent a protocol-state error before closing the channel. + ProtocolViolation, } #[cfg(test)] mod tests { use super::*; use litebox_broker_core::{PolicyEngine, PrincipalRights}; + use litebox_broker_protocol::ProtocolVersion; use litebox_broker_protocol::event::CreateEventRequest; + use litebox_broker_protocol::message::BrokerHandshakeRequest; #[test] fn host_request_handling_uses_one_broker_core() { @@ -214,90 +175,149 @@ mod tests { .unwrap(); serve_connection_negotiates_routes_one_request_and_returns_peer_closed(&broker); - serve_connection_closes_after_protocol_violation(&broker); + serve_connection_retries_after_version_mismatch(&broker); + serve_connection_rejects_active_request_before_negotiation(&broker); + serve_connection_rejects_handshake_request_after_negotiation(&broker); serve_connection_returns_channel_error_when_response_send_fails(&broker); } fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { - let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ - Ok(Some(BrokerRequest::Negotiate { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, - })), - Ok(Some(event_create_request(0))), - Ok(None), - ])); + }))]), + std::vec::Vec::from([ + Ok(HostReceive::Message(BrokerRequest::Event( + EventRequest::Create(CreateEventRequest { initial_count: 0 }), + ))), + Ok(HostReceive::PeerClosed), + ]), + ); assert_eq!( serve_connection(broker, &mut channel).unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( - channel.responses[0], - BrokerResponse::Negotiated { + channel.handshake_responses[0], + BrokerHandshakeResponse::Negotiated { broker_protocol_version: BROKER_PROTOCOL_VERSION } ); - let handle = match &channel.responses[1] { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { - response.handle - } + let handle = match &channel.responses[0] { + BrokerResponse::Event(EventResponse::Create(response)) => response.handle, response => panic!("unexpected response: {response:?}"), }; assert_ne!(handle.0, 0); } - fn serve_connection_closes_after_protocol_violation(broker: &BrokerCore) { - let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ - Ok(Some(event_create_request(0))), - Ok(Some(BrokerRequest::Negotiate { + fn serve_connection_retries_after_version_mismatch(broker: &BrokerCore) { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([ + Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1), + })), + Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + })), + ]), + std::vec::Vec::from([Ok(HostReceive::PeerClosed)]), + ); + + assert_eq!( + serve_connection(broker, &mut channel).unwrap(), + ConnectionTermination::PeerClosed + ); + assert_eq!( + channel.handshake_responses, + [ + BrokerHandshakeResponse::VersionMismatch { + broker_protocol_version: BROKER_PROTOCOL_VERSION + }, + BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION + } + ] + ); + } + + fn serve_connection_rejects_active_request_before_negotiation(broker: &BrokerCore) { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::ProtocolViolation)]), + std::vec::Vec::new(), + ); + + assert_eq!( + serve_connection(broker, &mut channel).unwrap(), + ConnectionTermination::ProtocolViolation + ); + assert_eq!( + channel.handshake_responses, + [BrokerHandshakeResponse::Error(ErrorCode::ProtocolState)] + ); + assert!(channel.responses.is_empty()); + } + + fn serve_connection_rejects_handshake_request_after_negotiation(broker: &BrokerCore) { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, - })), - ])); + }))]), + std::vec::Vec::from([Ok(HostReceive::ProtocolViolation)]), + ); assert_eq!( serve_connection(broker, &mut channel).unwrap(), - ConnectionTermination::BrokerClosed(CloseReason::ProtocolViolation) + ConnectionTermination::ProtocolViolation + ); + assert_eq!( + channel.handshake_responses, + [BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION + }] ); assert_eq!( channel.responses, [BrokerResponse::Error(ErrorCode::ProtocolState)] ); - assert_eq!(channel.requests.len(), 1); } fn serve_connection_returns_channel_error_when_response_send_fails(broker: &BrokerCore) { - let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([Ok(Some( - BrokerRequest::Negotiate { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, - }, - ))])); + }))]), + std::vec::Vec::new(), + ); channel.send_error = true; match serve_connection(broker, &mut channel) { Err(BrokerHostError::Channel(())) => {} result => panic!("unexpected serve result: {result:?}"), } - assert!(channel.responses.is_empty()); - } - - const fn event_request(request: EventRequest) -> BrokerRequest { - BrokerRequest::Core(CoreRequest::Event(request)) - } - - const fn event_create_request(initial_count: u64) -> BrokerRequest { - event_request(EventRequest::Create(CreateEventRequest { initial_count })) + assert!(channel.handshake_responses.is_empty()); } struct FakeHostControlChannel { - requests: std::vec::Vec, ()>>, + handshake_requests: + std::vec::Vec, ()>>, + requests: std::vec::Vec, ()>>, + handshake_responses: std::vec::Vec, responses: std::vec::Vec, send_error: bool, } impl FakeHostControlChannel { - fn new(requests: std::vec::Vec, ()>>) -> Self { + fn new( + handshake_requests: std::vec::Vec< + core::result::Result, ()>, + >, + requests: std::vec::Vec, ()>>, + ) -> Self { Self { + handshake_requests, requests, + handshake_responses: std::vec::Vec::new(), responses: std::vec::Vec::new(), send_error: false, } @@ -311,9 +331,32 @@ mod tests { Ok(PeerCredential::Unauthenticated) } - fn recv_request(&mut self) -> core::result::Result, Self::Error> { + fn recv_handshake_request( + &mut self, + ) -> core::result::Result, Self::Error> { + if self.handshake_requests.is_empty() { + Ok(HostReceive::PeerClosed) + } else { + self.handshake_requests.remove(0) + } + } + + fn send_handshake_response( + &mut self, + response: &BrokerHandshakeResponse, + ) -> core::result::Result<(), Self::Error> { + if self.send_error { + return Err(()); + } + self.handshake_responses.push(response.clone()); + Ok(()) + } + + fn recv_request( + &mut self, + ) -> core::result::Result, Self::Error> { if self.requests.is_empty() { - Ok(None) + Ok(HostReceive::PeerClosed) } else { self.requests.remove(0) } diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index ead22f9b08..539e3a8d7e 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -7,16 +7,13 @@ use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, EventConsumeMode, ReadinessState, WaitEventRequest, }; -use litebox_broker_protocol::message::{CoreRequest, CoreResponse, EventRequest, EventResponse}; +use litebox_broker_protocol::message::{ + BrokerRequest, BrokerResponse, EventRequest, EventResponse, +}; -use crate::{BrokerLocal, Result}; +use crate::{BrokerLocal, BrokerLocalError, Result}; impl BrokerLocal { - /// Creates a broker-owned event object. - pub fn create_event(&mut self) -> Result { - self.create_event_with_count(0) - } - /// Creates a broker-owned event object with initial readiness credits. /// /// # Panics @@ -87,7 +84,9 @@ impl BrokerLocal { } fn request_event(&mut self, request: EventRequest) -> Result { - let CoreResponse::Event(response) = self.request(CoreRequest::Event(request))?; - Ok(response) + match self.request(BrokerRequest::Event(request))? { + BrokerResponse::Event(response) => Ok(response), + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + } } } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index bb76a533aa..44e1cec22e 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -18,7 +18,9 @@ mod event; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::error::ErrorCode; -use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse, CoreRequest, CoreResponse}; +use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, +}; pub use error::{BrokerLocalError, Result}; @@ -41,13 +43,18 @@ impl BrokerLocal { /// response that does not match the negotiation request. pub fn negotiate(mut channel: Channel) -> Result { let requested = BROKER_PROTOCOL_VERSION; - match raw_request( - &mut channel, - BrokerRequest::Negotiate { - protocol_version: requested, - }, - )? { - response @ BrokerResponse::Negotiated { + let request = BrokerHandshakeRequest { + protocol_version: requested, + }; + channel + .send_handshake_request(&request) + .map_err(BrokerLocalError::Channel)?; + match channel + .recv_handshake_response() + .map_err(BrokerLocalError::Channel)? + .ok_or(BrokerLocalError::ChannelClosed)? + { + response @ BrokerHandshakeResponse::Negotiated { broker_protocol_version, } => { assert_eq!( @@ -56,10 +63,10 @@ impl BrokerLocal { ); Ok(Self { channel }) } - BrokerResponse::VersionMismatch { .. } => { + BrokerHandshakeResponse::VersionMismatch { .. } => { Err(BrokerLocalError::Broker(ErrorCode::UnsupportedVersion)) } - BrokerResponse::Error(error) => match error { + BrokerHandshakeResponse::Error(error) => match error { ErrorCode::UnsupportedVersion | ErrorCode::PolicyDenied => { Err(BrokerLocalError::Broker(error)) } @@ -69,21 +76,25 @@ impl BrokerLocal { | ErrorCode::Internal => panic!("broker returned unrecoverable error: {error}"), _ => panic!("broker returned unexpected negotiation error: {error}"), }, - response @ BrokerResponse::Core(_) => { - panic!("broker returned unexpected negotiation response: {response:?}") - } } } - /// Sends one active BrokerCore request. + /// Sends one active broker request. /// /// # Panics /// /// Panics if the broker reports an unrecoverable error or returns a protocol - /// response that does not match an active core request. - pub fn request(&mut self, request: CoreRequest) -> Result { - match raw_request(&mut self.channel, BrokerRequest::Core(request))? { - BrokerResponse::Core(response) => Ok(response), + /// response that does not match an active request. + pub fn request(&mut self, request: BrokerRequest) -> Result { + self.channel + .send_request(&request) + .map_err(BrokerLocalError::Channel)?; + match self + .channel + .recv_response() + .map_err(BrokerLocalError::Channel)? + .ok_or(BrokerLocalError::ChannelClosed)? + { BrokerResponse::Error(error) => match error { ErrorCode::PolicyDenied | ErrorCode::UnknownObject @@ -97,24 +108,11 @@ impl BrokerLocal { | ErrorCode::Internal => panic!("broker returned unrecoverable error: {error}"), _ => panic!("broker returned unsupported error: {error}"), }, - response => panic!("broker returned unexpected active response: {response:?}"), + response @ BrokerResponse::Event(_) => Ok(response), } } } -fn raw_request( - channel: &mut Channel, - request: BrokerRequest, -) -> Result { - channel - .send_request(&request) - .map_err(BrokerLocalError::Channel)?; - channel - .recv_response() - .map_err(BrokerLocalError::Channel)? - .ok_or(BrokerLocalError::ChannelClosed) -} - #[cfg(test)] mod tests { use super::*; @@ -126,42 +124,43 @@ mod tests { #[test] fn negotiate_returns_active_local_connection() { - let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { - broker_protocol_version: BROKER_PROTOCOL_VERSION, - })); + let channel = FakeControlChannel::new( + Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }), + None, + ); let local = BrokerLocal::negotiate(channel).unwrap(); assert_eq!( - local.channel.sent_request, - Some(BrokerRequest::Negotiate { + local.channel.sent_handshake_request, + Some(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION }) ); } #[test] - fn active_request_sends_core_request() { + fn active_request_sends_event_request() { let handle = ObjectHandle(7); - let request = CoreRequest::Event(EventRequest::Create(CreateEventRequest { + let request = BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, })); - let response = CoreResponse::Event(EventResponse::Create(CreateEventResponse { handle })); - let channel = FakeControlChannel::new(Some(BrokerResponse::Core(response.clone()))); + let response = BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })); + let channel = FakeControlChannel::new(None, Some(response.clone())); let mut local = BrokerLocal { channel }; assert_eq!(local.request(request.clone()).unwrap(), response); - assert_eq!( - local.channel.sent_request, - Some(BrokerRequest::Core(request)) - ); + assert_eq!(local.channel.sent_request, Some(request)); } #[test] fn active_request_returns_recoverable_broker_error() { - let request = CoreRequest::Event(EventRequest::Create(CreateEventRequest { + let request = BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, })); - let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::WouldBlock))); + let channel = + FakeControlChannel::new(None, Some(BrokerResponse::Error(ErrorCode::WouldBlock))); let mut local = BrokerLocal { channel }; assert!(matches!( @@ -173,10 +172,11 @@ mod tests { #[test] #[should_panic(expected = "broker returned unrecoverable error")] fn active_request_panics_on_unrecoverable_broker_error() { - let request = CoreRequest::Event(EventRequest::Create(CreateEventRequest { + let request = BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, })); - let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::Internal))); + let channel = + FakeControlChannel::new(None, Some(BrokerResponse::Error(ErrorCode::Internal))); let mut local = BrokerLocal { channel }; let _ = local.request(request); @@ -186,9 +186,12 @@ mod tests { #[should_panic(expected = "broker returned unexpected negotiation response")] fn negotiate_rejects_broker_different_version_response() { let broker_protocol_version = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); - let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { - broker_protocol_version, - })); + let channel = FakeControlChannel::new( + Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version, + }), + None, + ); let _ = BrokerLocal::negotiate(channel); } @@ -196,9 +199,12 @@ mod tests { #[test] fn negotiate_rejects_broker_unsupported_version_response() { let broker_protocol_version = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); - let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { - broker_protocol_version, - })); + let channel = FakeControlChannel::new( + Some(BrokerHandshakeResponse::VersionMismatch { + broker_protocol_version, + }), + None, + ); assert!(matches!( BrokerLocal::negotiate(channel), @@ -209,20 +215,30 @@ mod tests { #[test] #[should_panic(expected = "broker returned unrecoverable error")] fn negotiate_panics_on_unrecoverable_broker_error() { - let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::Internal))); + let channel = FakeControlChannel::new( + Some(BrokerHandshakeResponse::Error(ErrorCode::Internal)), + None, + ); let _ = BrokerLocal::negotiate(channel); } struct FakeControlChannel { + sent_handshake_request: Option, sent_request: Option, + handshake_response: Option, response: Option, } impl FakeControlChannel { - const fn new(response: Option) -> Self { + const fn new( + handshake_response: Option, + response: Option, + ) -> Self { Self { + sent_handshake_request: None, sent_request: None, + handshake_response, response, } } @@ -231,6 +247,20 @@ mod tests { impl LocalControlChannel for FakeControlChannel { type Error = Infallible; + fn send_handshake_request( + &mut self, + request: &BrokerHandshakeRequest, + ) -> core::result::Result<(), Self::Error> { + self.sent_handshake_request = Some(request.clone()); + Ok(()) + } + + fn recv_handshake_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(self.handshake_response.take()) + } + fn send_request( &mut self, request: &BrokerRequest, diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index c0579d5823..728053ca03 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::message::{BrokerRequest, BrokerResponse}; +use crate::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, +}; /// Peer identity information supplied by the channel or host layer. /// @@ -20,15 +22,38 @@ pub enum PeerCredential { Unauthenticated, } +/// Host-side receive outcome for peer-to-broker control messages. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HostReceive { + /// The peer sent a well-formed message for the current protocol phase. + Message(T), + /// The peer sent a well-formed message for a different protocol phase. + ProtocolViolation, + /// The peer closed the channel cleanly before starting another frame. + PeerClosed, +} + /// Local-side control channel for broker authority calls. pub trait LocalControlChannel { /// Channel-specific error type. type Error; - /// Sends one broker request. + /// Sends one broker handshake request. + fn send_handshake_request( + &mut self, + request: &BrokerHandshakeRequest, + ) -> Result<(), Self::Error>; + + /// Receives one broker handshake response. + /// + /// Returns `Ok(None)` when the broker closed the channel cleanly before + /// starting another response frame. + fn recv_handshake_response(&mut self) -> Result, Self::Error>; + + /// Sends one active broker request. fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error>; - /// Receives one broker response. + /// Receives one active broker response. /// /// Returns `Ok(None)` when the broker closed the channel cleanly before /// starting another response frame. @@ -43,12 +68,20 @@ pub trait HostControlChannel { /// Returns the peer credential authenticated for this channel endpoint. fn peer_credential(&self) -> Result; - /// Receives one broker request. - /// - /// Returns `Ok(None)` when the peer closed the channel cleanly before - /// starting another request frame. - fn recv_request(&mut self) -> Result, Self::Error>; + /// Receives one broker handshake request. + fn recv_handshake_request( + &mut self, + ) -> Result, Self::Error>; + + /// Sends one broker handshake response. + fn send_handshake_response( + &mut self, + response: &BrokerHandshakeResponse, + ) -> Result<(), Self::Error>; + + /// Receives one active broker request. + fn recv_request(&mut self) -> Result, Self::Error>; - /// Sends one broker response. + /// Sends one active broker response. fn send_response(&mut self, response: &BrokerResponse) -> Result<(), Self::Error>; } diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index a2669a5e61..44a7277a51 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -8,49 +8,23 @@ use crate::event::{ CreateEventRequest, CreateEventResponse, WaitEventRequest, WaitEventResponse, }; -/// Broker request sent over the control channel. -/// -/// The outer broker request is intentionally small. Object-family and -/// domain-specific operations are grouped below it so new object families do not -/// accumulate as unrelated top-level broker variants. +/// Broker handshake request sent before the control channel is active. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum BrokerRequest { - /// Protocol negotiation request. - Negotiate { - /// Required protocol version. - protocol_version: ProtocolVersion, - }, - /// BrokerCore authority request. - Core(CoreRequest), +pub struct BrokerHandshakeRequest { + /// Required protocol version. + pub protocol_version: ProtocolVersion, } -/// Request adapted by the broker host into a BrokerCore domain call. +/// Broker request sent over an active control channel. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum CoreRequest { +pub enum BrokerRequest { /// Event object request family. Event(EventRequest), } -/// Broker-owned event object request. +/// Broker handshake response sent before the control channel is active. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum EventRequest { - /// Create a broker-owned event object. - Create(CreateEventRequest), - /// Check whether an event wait would complete now. - Wait(WaitEventRequest), - /// Add readiness credits to an event. - Add(AddEventRequest), - /// Consume readiness credits from an event. - Consume(ConsumeEventRequest), -} - -/// Broker response sent over the control channel. -/// -/// Common connection/protocol outcomes stay at this layer. Domain payloads are -/// grouped under [`CoreResponse`] so future object families can evolve without -/// turning the broker envelope into a flat operation/result list. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum BrokerResponse { +pub enum BrokerHandshakeResponse { /// Negotiation result. Negotiated { /// Broker protocol version supported by this endpoint. @@ -68,17 +42,30 @@ pub enum BrokerResponse { /// Broker protocol version supported by this endpoint. broker_protocol_version: ProtocolVersion, }, - /// BrokerCore authority response. - Core(CoreResponse), - /// Operation failed with an ABI-neutral broker error. + /// Handshake failed with an ABI-neutral broker error. Error(ErrorCode), } -/// Response returned by a BrokerCore domain request. +/// Broker-owned event object request. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum CoreResponse { +pub enum EventRequest { + /// Create a broker-owned event object. + Create(CreateEventRequest), + /// Check whether an event wait would complete now. + Wait(WaitEventRequest), + /// Add readiness credits to an event. + Add(AddEventRequest), + /// Consume readiness credits from an event. + Consume(ConsumeEventRequest), +} + +/// Broker response sent over an active control channel. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BrokerResponse { /// Event object response family. Event(EventResponse), + /// Operation failed with an ABI-neutral broker error. + Error(ErrorCode), } /// Broker-owned event object response. diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index cd99f9f833..46600424c4 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -6,33 +6,33 @@ //! The wire codec mirrors the protocol DTO hierarchy: //! - this module owns public encode/decode entry points and top-level broker //! envelope tags; -//! - `core_message` owns `CoreRequest`/`CoreResponse` family tags; //! - object-family modules such as `event` own their operation and nested value //! tags; //! - `primitive` owns shared scalar/value encoders. //! -//! New object families should add a core family tag and a private family codec -//! module instead of adding flat helpers here. Existing payloads are positional; -//! changing fields is an ABI change, so prefer a new operation tag or explicit -//! negotiated-version gate for payload evolution. +//! New object families should add a top-level broker message tag and a private +//! family codec module instead of adding flat helpers here. Existing payloads +//! are positional; changing fields is an ABI change, so prefer a new operation +//! tag or explicit negotiated-version gate for payload evolution. use alloc::vec::Vec; use thiserror::Error; use crate::error::ErrorCode; -use crate::message::{BrokerRequest, BrokerResponse}; +use crate::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, +}; use primitive::{Decoder, Encoder}; -mod core_message; mod event; mod primitive; const REQUEST_TAG_NEGOTIATE: u8 = 0; -const REQUEST_TAG_CORE: u8 = 1; +const REQUEST_TAG_EVENT: u8 = 1; const RESPONSE_TAG_NEGOTIATED: u8 = 0; -const RESPONSE_TAG_CORE: u8 = 1; +const RESPONSE_TAG_EVENT: u8 = 1; const RESPONSE_TAG_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; @@ -48,10 +48,38 @@ pub enum WireError { InvalidBoolean, #[error("invalid broker wire tag")] InvalidTag, + #[error("broker wire message is not valid in this protocol phase")] + WrongMessagePhase, #[error("broker wire offset overflow")] OffsetOverflow, } +/// Encodes a broker handshake request body. +/// +/// Successful encodings are always non-empty because the first byte is the +/// message tag. +pub fn encode_handshake_request(request: BrokerHandshakeRequest) -> Vec { + let mut encoder = Encoder::default(); + encoder.u8(REQUEST_TAG_NEGOTIATE); + encoder.protocol_version(request.protocol_version); + encoder.finish() +} + +/// Decodes a broker handshake request body. +pub fn decode_handshake_request(frame: &[u8]) -> Result { + let mut decoder = Decoder::new(frame); + let tag = decoder.u8()?; + let request = match tag { + REQUEST_TAG_NEGOTIATE => BrokerHandshakeRequest { + protocol_version: decoder.protocol_version()?, + }, + REQUEST_TAG_EVENT => return Err(WireError::WrongMessagePhase), + _ => return Err(WireError::InvalidTag), + }; + decoder.finish()?; + Ok(request) +} + /// Encodes a broker request body. /// /// Successful encodings are always non-empty because the first byte is the @@ -59,13 +87,9 @@ pub enum WireError { pub fn encode_request(request: BrokerRequest) -> Vec { let mut encoder = Encoder::default(); match request { - BrokerRequest::Negotiate { protocol_version } => { - encoder.u8(REQUEST_TAG_NEGOTIATE); - encoder.protocol_version(protocol_version); - } - BrokerRequest::Core(request) => { - encoder.u8(REQUEST_TAG_CORE); - core_message::encode_core_request(&mut encoder, request); + BrokerRequest::Event(request) => { + encoder.u8(REQUEST_TAG_EVENT); + event::encode_event_request(&mut encoder, request); } } encoder.finish() @@ -76,40 +100,34 @@ pub fn decode_request(frame: &[u8]) -> Result { let mut decoder = Decoder::new(frame); let tag = decoder.u8()?; let request = match tag { - REQUEST_TAG_NEGOTIATE => BrokerRequest::Negotiate { - protocol_version: decoder.protocol_version()?, - }, - REQUEST_TAG_CORE => BrokerRequest::Core(core_message::decode_core_request(&mut decoder)?), + REQUEST_TAG_NEGOTIATE => return Err(WireError::WrongMessagePhase), + REQUEST_TAG_EVENT => BrokerRequest::Event(event::decode_event_request(&mut decoder)?), _ => return Err(WireError::InvalidTag), }; decoder.finish()?; Ok(request) } -/// Encodes a broker response body. +/// Encodes a broker handshake response body. /// /// Successful encodings are always non-empty because the first byte is the /// message tag. -pub fn encode_response(response: BrokerResponse) -> Vec { +pub fn encode_handshake_response(response: BrokerHandshakeResponse) -> Vec { let mut encoder = Encoder::default(); match response { - BrokerResponse::Negotiated { + BrokerHandshakeResponse::Negotiated { broker_protocol_version, } => { encoder.u8(RESPONSE_TAG_NEGOTIATED); encoder.protocol_version(broker_protocol_version); } - BrokerResponse::VersionMismatch { + BrokerHandshakeResponse::VersionMismatch { broker_protocol_version, } => { encoder.u8(RESPONSE_TAG_VERSION_MISMATCH); encoder.protocol_version(broker_protocol_version); } - BrokerResponse::Core(response) => { - encoder.u8(RESPONSE_TAG_CORE); - core_message::encode_core_response(&mut encoder, response); - } - BrokerResponse::Error(error) => { + BrokerHandshakeResponse::Error(error) => { encoder.u8(RESPONSE_TAG_ERROR); encoder.u16(error.as_raw()); } @@ -117,20 +135,56 @@ pub fn encode_response(response: BrokerResponse) -> Vec { encoder.finish() } -/// Decodes a broker response body. -pub fn decode_response(frame: &[u8]) -> Result { +/// Decodes a broker handshake response body. +pub fn decode_handshake_response(frame: &[u8]) -> Result { let mut decoder = Decoder::new(frame); let tag = decoder.u8()?; let response = match tag { - RESPONSE_TAG_NEGOTIATED => BrokerResponse::Negotiated { + RESPONSE_TAG_NEGOTIATED => BrokerHandshakeResponse::Negotiated { broker_protocol_version: decoder.protocol_version()?, }, - RESPONSE_TAG_VERSION_MISMATCH => BrokerResponse::VersionMismatch { + RESPONSE_TAG_EVENT => return Err(WireError::WrongMessagePhase), + RESPONSE_TAG_VERSION_MISMATCH => BrokerHandshakeResponse::VersionMismatch { broker_protocol_version: decoder.protocol_version()?, }, - RESPONSE_TAG_CORE => { - BrokerResponse::Core(core_message::decode_core_response(&mut decoder)?) + RESPONSE_TAG_ERROR => { + let error = ErrorCode::from_raw(decoder.u16()?).ok_or(WireError::InvalidTag)?; + BrokerHandshakeResponse::Error(error) } + _ => return Err(WireError::InvalidTag), + }; + decoder.finish()?; + Ok(response) +} + +/// Encodes a broker response body. +/// +/// Successful encodings are always non-empty because the first byte is the +/// message tag. +pub fn encode_response(response: BrokerResponse) -> Vec { + let mut encoder = Encoder::default(); + match response { + BrokerResponse::Event(response) => { + encoder.u8(RESPONSE_TAG_EVENT); + event::encode_event_response(&mut encoder, response); + } + BrokerResponse::Error(error) => { + encoder.u8(RESPONSE_TAG_ERROR); + encoder.u16(error.as_raw()); + } + } + encoder.finish() +} + +/// Decodes a broker response body. +pub fn decode_response(frame: &[u8]) -> Result { + let mut decoder = Decoder::new(frame); + let tag = decoder.u8()?; + let response = match tag { + RESPONSE_TAG_NEGOTIATED | RESPONSE_TAG_VERSION_MISMATCH => { + return Err(WireError::WrongMessagePhase); + } + RESPONSE_TAG_EVENT => BrokerResponse::Event(event::decode_event_response(&mut decoder)?), RESPONSE_TAG_ERROR => { let error = ErrorCode::from_raw(decoder.u16()?).ok_or(WireError::InvalidTag)?; BrokerResponse::Error(error) @@ -149,29 +203,40 @@ mod tests { CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, WaitEventRequest, WaitEventResponse, }; - use crate::message::{CoreRequest, CoreResponse, EventRequest, EventResponse}; + use crate::message::{EventRequest, EventResponse}; use crate::{ObjectHandle, ProtocolVersion}; + #[test] + fn handshake_request_codec_round_trips_all_variants() { + let requests = [BrokerHandshakeRequest { + protocol_version: ProtocolVersion(1), + }]; + + for request in requests { + assert_eq!( + decode_handshake_request(&encode_handshake_request(request.clone())).unwrap(), + request + ); + } + } + #[test] fn request_codec_round_trips_all_variants() { - let handle = sample_handle(); + let handle = ObjectHandle(13); let requests = [ - BrokerRequest::Negotiate { - protocol_version: ProtocolVersion(1), - }, - event_request(EventRequest::Create(CreateEventRequest { + BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, })), - event_request(EventRequest::Create(CreateEventRequest { + BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 7, })), - event_request(EventRequest::Wait(WaitEventRequest { handle })), - event_request(EventRequest::Add(AddEventRequest { handle, value: 3 })), - event_request(EventRequest::Consume(ConsumeEventRequest { + BrokerRequest::Event(EventRequest::Wait(WaitEventRequest { handle })), + BrokerRequest::Event(EventRequest::Add(AddEventRequest { handle, value: 3 })), + BrokerRequest::Event(EventRequest::Consume(ConsumeEventRequest { handle, mode: EventConsumeMode::All, })), - event_request(EventRequest::Consume(ConsumeEventRequest { + BrokerRequest::Event(EventRequest::Consume(ConsumeEventRequest { handle, mode: EventConsumeMode::One, })), @@ -186,35 +251,50 @@ mod tests { } #[test] - fn response_codec_round_trips_all_variants() { - let handle = sample_handle(); + fn handshake_response_codec_round_trips_all_variants() { let responses = [ - BrokerResponse::Negotiated { + BrokerHandshakeResponse::Negotiated { broker_protocol_version: ProtocolVersion(1), }, - BrokerResponse::VersionMismatch { + BrokerHandshakeResponse::VersionMismatch { broker_protocol_version: ProtocolVersion(1), }, - event_response(EventResponse::Create(CreateEventResponse { handle })), - event_response(EventResponse::Wait(WaitEventResponse { + BrokerHandshakeResponse::Error(ErrorCode::PolicyDenied), + BrokerHandshakeResponse::Error(ErrorCode::Internal), + ]; + + for response in responses { + assert_eq!( + decode_handshake_response(&encode_handshake_response(response.clone())).unwrap(), + response + ); + } + } + + #[test] + fn response_codec_round_trips_all_variants() { + let handle = ObjectHandle(13); + let responses = [ + BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })), + BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { readiness: ReadinessState { read_ready: true, write_ready: false, }, })), - event_response(EventResponse::Wait(WaitEventResponse { + BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { readiness: ReadinessState { read_ready: false, write_ready: true, }, })), - event_response(EventResponse::Add(AddEventResponse { + BrokerResponse::Event(EventResponse::Add(AddEventResponse { readiness: ReadinessState { read_ready: true, write_ready: true, }, })), - event_response(EventResponse::Consume(EventConsumption { + BrokerResponse::Event(EventResponse::Consume(EventConsumption { value: 3, readiness: ReadinessState { read_ready: false, @@ -234,27 +314,92 @@ mod tests { } } + #[test] + fn decode_rejects_malformed_handshake_request_frames() { + assert_eq!( + decode_handshake_request(&[0xff, 1, 2, 3]), + Err(WireError::InvalidTag) + ); + assert_eq!( + decode_handshake_request(&[0, 1]), + Err(WireError::TruncatedFrame) + ); + assert_eq!( + decode_handshake_request(&encode_request(BrokerRequest::Event(EventRequest::Create( + CreateEventRequest { initial_count: 0 }, + )))), + Err(WireError::WrongMessagePhase) + ); + let mut frame = encode_handshake_request(BrokerHandshakeRequest { + protocol_version: ProtocolVersion(1), + }); + frame.push(0xff); + assert_eq!( + decode_handshake_request(&frame), + Err(WireError::TrailingBytes) + ); + } + #[test] fn decode_rejects_malformed_request_frames() { assert_eq!(decode_request(&[0xff, 1, 2, 3]), Err(WireError::InvalidTag)); - let mut unknown_consume_mode = - encode_request(event_request(EventRequest::Consume(ConsumeEventRequest { - handle: sample_handle(), + assert_eq!( + decode_request(&encode_handshake_request(BrokerHandshakeRequest { + protocol_version: ProtocolVersion(1), + })), + Err(WireError::WrongMessagePhase) + ); + let mut unknown_consume_mode = encode_request(BrokerRequest::Event(EventRequest::Consume( + ConsumeEventRequest { + handle: ObjectHandle(13), mode: EventConsumeMode::All, - }))); + }, + ))); *unknown_consume_mode.last_mut().unwrap() = 0xff; assert_eq!( decode_request(&unknown_consume_mode), Err(WireError::InvalidTag) ); - assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); - let mut frame = encode_request(event_request(EventRequest::Create(CreateEventRequest { - initial_count: 0, - }))); + let mut frame = encode_request(BrokerRequest::Event(EventRequest::Create( + CreateEventRequest { initial_count: 0 }, + ))); frame.push(0xff); assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); } + #[test] + fn decode_rejects_malformed_handshake_response_frames() { + assert_eq!( + decode_handshake_response(&[0xff, 1, 2, 3]), + Err(WireError::InvalidTag) + ); + assert_eq!( + decode_handshake_response(&[0, 1]), + Err(WireError::TruncatedFrame) + ); + assert_eq!( + decode_handshake_response(&[2, 0xff, 0xff]), + Err(WireError::InvalidTag) + ); + assert_eq!( + decode_handshake_response(&encode_response(BrokerResponse::Event( + EventResponse::Create(CreateEventResponse { + handle: ObjectHandle(13), + }), + ))), + Err(WireError::WrongMessagePhase) + ); + + let mut frame = encode_handshake_response(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: ProtocolVersion(1), + }); + frame.push(0xff); + assert_eq!( + decode_handshake_response(&frame), + Err(WireError::TrailingBytes) + ); + } + #[test] fn decode_rejects_malformed_response_frames() { assert_eq!( @@ -262,7 +407,15 @@ mod tests { Err(WireError::InvalidTag) ); assert_eq!( - decode_response(&[1, 0, 1, 0xff]), + decode_response(&encode_handshake_response( + BrokerHandshakeResponse::Negotiated { + broker_protocol_version: ProtocolVersion(1), + }, + )), + Err(WireError::WrongMessagePhase) + ); + assert_eq!( + decode_response(&[1, 1, 0xff]), Err(WireError::InvalidBoolean) ); assert_eq!( @@ -270,14 +423,14 @@ mod tests { Err(WireError::InvalidTag) ); - let mut invalid_bool = [1, 0, 2, 2, 0]; + let mut invalid_bool = [1, 2, 2, 0]; assert_eq!( decode_response(&invalid_bool), Err(WireError::InvalidBoolean) ); + invalid_bool[2] = 1; invalid_bool[3] = 1; - invalid_bool[4] = 1; let mut frame = invalid_bool.to_vec(); frame.push(0xff); assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); @@ -286,25 +439,15 @@ mod tests { #[test] fn event_add_response_wire_shape_is_pinned() { assert_eq!( - encode_response(event_response(EventResponse::Add(AddEventResponse { - readiness: ReadinessState { - read_ready: true, - write_ready: false, - }, - }))), - [1, 0, 2, 1, 0] + encode_response(BrokerResponse::Event(EventResponse::Add( + AddEventResponse { + readiness: ReadinessState { + read_ready: true, + write_ready: false, + }, + } + ))), + [1, 2, 1, 0] ); } - - const fn sample_handle() -> ObjectHandle { - ObjectHandle(13) - } - - const fn event_request(request: EventRequest) -> BrokerRequest { - BrokerRequest::Core(CoreRequest::Event(request)) - } - - const fn event_response(response: EventResponse) -> BrokerResponse { - BrokerResponse::Core(CoreResponse::Event(response)) - } } diff --git a/litebox_broker_protocol/src/wire/core_message.rs b/litebox_broker_protocol/src/wire/core_message.rs deleted file mode 100644 index 892d385cca..0000000000 --- a/litebox_broker_protocol/src/wire/core_message.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use crate::message::{CoreRequest, CoreResponse}; - -use super::WireError; -use super::primitive::{Decoder, Encoder}; - -// Core tags select object-family codecs. Add new object families here, then -// keep their operation-specific tags inside a dedicated family module. -const CORE_REQUEST_TAG_EVENT: u8 = 0; -const CORE_RESPONSE_TAG_EVENT: u8 = 0; - -pub(super) fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) { - match request { - CoreRequest::Event(request) => { - encoder.u8(CORE_REQUEST_TAG_EVENT); - super::event::encode_event_request(encoder, request); - } - } -} - -pub(super) fn decode_core_request(decoder: &mut Decoder<'_>) -> Result { - let request = match decoder.u8()? { - CORE_REQUEST_TAG_EVENT => CoreRequest::Event(super::event::decode_event_request(decoder)?), - _ => return Err(WireError::InvalidTag), - }; - - Ok(request) -} - -pub(super) fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) { - match response { - CoreResponse::Event(response) => { - encoder.u8(CORE_RESPONSE_TAG_EVENT); - super::event::encode_event_response(encoder, response); - } - } -} - -pub(super) fn decode_core_response(decoder: &mut Decoder<'_>) -> Result { - let response = match decoder.u8()? { - CORE_RESPONSE_TAG_EVENT => { - CoreResponse::Event(super::event::decode_event_response(decoder)?) - } - _ => return Err(WireError::InvalidTag), - }; - - Ok(response) -} diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index d5faed05dc..42de615f64 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -12,10 +12,16 @@ use std::os::unix::net::UnixStream; use std::path::Path; use std::time::{Duration, Instant}; -use litebox_broker_protocol::channel::{HostControlChannel, LocalControlChannel, PeerCredential}; -use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse}; +use litebox_broker_protocol::channel::{ + HostControlChannel, HostReceive, LocalControlChannel, PeerCredential, +}; +use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, +}; use litebox_broker_protocol::wire::{ - WireError, decode_request, decode_response, encode_request, encode_response, + WireError, decode_handshake_request, decode_handshake_response, decode_request, + decode_response, encode_handshake_request, encode_handshake_response, encode_request, + encode_response, }; const MAX_FRAME_LEN: usize = 64 * 1024; @@ -115,6 +121,30 @@ impl UnixStreamHostControlChannel { impl LocalControlChannel for UnixStreamLocalControlChannel { type Error = Error; + fn send_handshake_request(&mut self, request: &BrokerHandshakeRequest) -> IoResult<()> { + let frame = encode_handshake_request(request.clone()); + let deadline = self.current_deadline()?; + let result = write_frame_with_deadline(&mut self.stream, &frame, deadline); + if result.is_err() { + self.active_request_deadline = None; + } + result + } + + fn recv_handshake_response(&mut self) -> IoResult> { + let deadline = self.current_deadline()?; + let frame = read_frame_with_deadline(&mut self.stream, deadline); + if self.io_deadline.is_none() && self.active_request_deadline.take().is_some() { + self.set_stream_io_timeout(self.io_timeout)?; + } + match frame? { + Some(frame) => decode_handshake_response(&frame) + .map(Some) + .map_err(wire_error), + None => Ok(None), + } + } + fn send_request(&mut self, request: &BrokerRequest) -> IoResult<()> { let frame = encode_request(request.clone()); let deadline = self.current_deadline()?; @@ -127,14 +157,14 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { fn recv_response(&mut self) -> IoResult> { let deadline = self.current_deadline()?; - let result = match read_frame_with_deadline(&mut self.stream, deadline)? { - Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), - None => Ok(None), - }; + let frame = read_frame_with_deadline(&mut self.stream, deadline); if self.io_deadline.is_none() && self.active_request_deadline.take().is_some() { self.set_stream_io_timeout(self.io_timeout)?; } - result + match frame? { + Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), + None => Ok(None), + } } } @@ -147,11 +177,34 @@ impl HostControlChannel for UnixStreamHostControlChannel { Ok(PeerCredential::Unauthenticated) } - fn recv_request(&mut self) -> IoResult> { + fn recv_handshake_request(&mut self) -> IoResult> { let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { - return Ok(None); + return Ok(HostReceive::PeerClosed); }; - decode_request(&frame).map(Some).map_err(wire_error) + match decode_handshake_request(&frame) { + Ok(request) => Ok(HostReceive::Message(request)), + Err(WireError::WrongMessagePhase) => Ok(HostReceive::ProtocolViolation), + Err(error) => Err(wire_error(error)), + } + } + + fn send_handshake_response(&mut self, response: &BrokerHandshakeResponse) -> IoResult<()> { + write_frame_with_deadline( + &mut self.stream, + &encode_handshake_response(response.clone()), + self.io_deadline, + ) + } + + fn recv_request(&mut self) -> IoResult> { + let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { + return Ok(HostReceive::PeerClosed); + }; + match decode_request(&frame) { + Ok(request) => Ok(HostReceive::Message(request)), + Err(WireError::WrongMessagePhase) => Ok(HostReceive::ProtocolViolation), + Err(error) => Err(wire_error(error)), + } } fn send_response(&mut self, response: &BrokerResponse) -> IoResult<()> { @@ -364,4 +417,39 @@ mod tests { "unexpected timeout error kind: {error:?}" ); } + + #[test] + fn host_reports_wrong_phase_request_frames_as_protocol_violations() { + let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); + write_frame_with_deadline( + &mut peer_stream, + &encode_request(BrokerRequest::Event( + litebox_broker_protocol::message::EventRequest::Create( + litebox_broker_protocol::event::CreateEventRequest { initial_count: 0 }, + ), + )), + None, + ) + .unwrap(); + assert_eq!( + channel.recv_handshake_request().unwrap(), + HostReceive::ProtocolViolation + ); + + let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); + write_frame_with_deadline( + &mut peer_stream, + &encode_handshake_request(BrokerHandshakeRequest { + protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }), + None, + ) + .unwrap(); + assert_eq!( + channel.recv_request().unwrap(), + HostReceive::ProtocolViolation + ); + } } diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 9177e669e4..11cb8e60a5 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -76,7 +76,7 @@ fn run_fake_runner(args: &[OsString]) { .unwrap(); let mut local = BrokerLocal::negotiate(channel).unwrap(); - let handle = local.create_event().unwrap(); + let handle = local.create_event_with_count(0).unwrap(); assert_eq!( local.wait_event(handle).unwrap(), ReadinessState { diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 7287e740ed..4d30417de0 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -326,9 +326,10 @@ fn spawn_test_broker( stream .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test write timeout"); - let mut channel = CountingHostControlChannel::new( - litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream), - ); + let mut channel = CountingHostControlChannel { + inner: litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream), + event_request_count: 0, + }; let termination = litebox_broker_host::serve_connection(&broker, &mut channel) .expect("broker host failed"); assert_eq!( @@ -336,7 +337,7 @@ fn spawn_test_broker( litebox_broker_host::ConnectionTermination::PeerClosed ); event_request_count_tx - .send(channel.event_request_count()) + .send(channel.event_request_count) .expect("failed to report broker event request count"); } })); @@ -364,22 +365,6 @@ struct CountingHostControlChannel - CountingHostControlChannel -{ - const fn new(inner: Channel) -> Self { - Self { - inner, - event_request_count: 0, - } - } - - const fn event_request_count(&self) -> usize { - self.event_request_count - } -} - #[cfg(all(target_arch = "x86_64", target_os = "linux"))] impl litebox_broker_protocol::channel::HostControlChannel for CountingHostControlChannel @@ -392,15 +377,38 @@ impl self.inner.peer_credential() } + fn recv_handshake_request( + &mut self, + ) -> Result< + litebox_broker_protocol::channel::HostReceive< + litebox_broker_protocol::message::BrokerHandshakeRequest, + >, + Self::Error, + > { + self.inner.recv_handshake_request() + } + + fn send_handshake_response( + &mut self, + response: &litebox_broker_protocol::message::BrokerHandshakeResponse, + ) -> Result<(), Self::Error> { + self.inner.send_handshake_response(response) + } + fn recv_request( &mut self, - ) -> Result, Self::Error> { + ) -> Result< + litebox_broker_protocol::channel::HostReceive< + litebox_broker_protocol::message::BrokerRequest, + >, + Self::Error, + > { let request = self.inner.recv_request()?; if matches!( - request, - Some(litebox_broker_protocol::message::BrokerRequest::Core( - litebox_broker_protocol::message::CoreRequest::Event(_) - )) + &request, + litebox_broker_protocol::channel::HostReceive::Message( + litebox_broker_protocol::message::BrokerRequest::Event(_) + ) ) { self.event_request_count += 1; } From a4f52b335a49d1ca719411c21a4407bc6a205e47 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 25 Jun 2026 16:40:07 -0700 Subject: [PATCH 065/319] Surface broker eventfd poll failures (#967) Returns `ERR` from broker-backed eventfd polling when the broker/control request fails with an error other than `WouldBlock`. Notifies registered event observers on those failures so waiters can observe the error state. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/event/counter.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index bdda5cf734..815fc7d937 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -144,10 +144,20 @@ where match self .broker .request(BrokerRequest::Event(request)) - .map_err(BrokerObjectError::from)? - { + .map_err(BrokerObjectError::from) + .inspect_err(|&error| { + if error != BrokerObjectError::WouldBlock { + self.pollee.notify_observers(Events::ERR); + } + })? { BrokerResponse::Event(response) => Ok(response), - BrokerResponse::Error(error) => Err(error.into()), + BrokerResponse::Error(error) => { + let error = error.into(); + if error != BrokerObjectError::WouldBlock { + self.pollee.notify_observers(Events::ERR); + } + Err(error) + } } } } @@ -161,10 +171,12 @@ where } fn check_io_events(&self) -> Events { - let Ok(response) = self.request_event(EventRequest::Wait(WaitEventRequest { + let response = match self.request_event(EventRequest::Wait(WaitEventRequest { handle: self.handle, - })) else { - return Events::empty(); + })) { + Ok(response) => response, + Err(BrokerObjectError::WouldBlock) => return Events::empty(), + Err(_) => return Events::ERR, }; let EventResponse::Wait(response) = response else { panic!("broker returned unexpected event response: {response:?}"); From 24da39c166ed64e603063a8ef3412ad079c830ba Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 25 Jun 2026 16:55:44 -0700 Subject: [PATCH 066/319] Serve userland broker clients independently (#968) Serves accepted userland broker clients on independent handler threads so a stalled peer does not block the accept loop. Logs handler-spawn failures and drops that connection without terminating the broker. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_userland/src/main.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 84598aa7eb..b6c2ba559b 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -48,7 +48,17 @@ fn main() -> Result<(), Box> { loop { let (stream, _) = listener.accept()?; - let mut channel = UnixStreamHostControlChannel::from_accepted(stream); - serve_connection(&broker, &mut channel)?; + let broker = broker.clone(); + if let Err(error) = std::thread::Builder::new() + .name("litebox-broker-connection".to_owned()) + .spawn(move || { + let mut channel = UnixStreamHostControlChannel::from_accepted(stream); + if let Err(error) = serve_connection(&broker, &mut channel) { + eprintln!("failed to serve broker connection: {error}"); + } + }) + { + eprintln!("failed to spawn broker connection handler: {error}"); + } } } From 610baa56da5ce71b2bdbe5416a6495b6d9be1f58 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 25 Jun 2026 17:09:10 -0700 Subject: [PATCH 067/319] Limit broker socket deadlines to setup (#969) Limits Unix broker socket deadlines to connection setup by applying the deadline only to local handshake send/receive. Clears socket timeouts after the handshake response so active broker requests run without the setup deadline, and removes the now-unneeded generic channel deadline configuration surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_local/src/lib.rs | 5 - litebox_broker_transport/src/unix_socket.rs | 137 +++++------------- .../tests/userland_broker.rs | 7 +- litebox_runner_linux_userland/src/broker.rs | 17 +-- 4 files changed, 42 insertions(+), 124 deletions(-) diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 44e1cec22e..a61cf21ff4 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -30,11 +30,6 @@ pub struct BrokerLocal { } impl BrokerLocal { - /// Returns the underlying control channel for deployment-specific configuration. - pub fn control_channel_mut(&mut self) -> &mut Channel { - &mut self.channel - } - /// Negotiates the broker protocol over an already-connected control channel. /// /// # Panics diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 42de615f64..877507d5c6 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -29,9 +29,7 @@ const MAX_FRAME_LEN: usize = 64 * 1024; /// Local-side Unix-domain-socket control channel for the hosted userland POC. pub struct UnixStreamLocalControlChannel { stream: UnixStream, - io_timeout: Option, - io_deadline: Option, - active_request_deadline: Option, + setup_deadline: Option, } impl UnixStreamLocalControlChannel { @@ -39,9 +37,7 @@ impl UnixStreamLocalControlChannel { pub const fn from_connected(stream: UnixStream) -> Self { Self { stream, - io_timeout: None, - io_deadline: None, - active_request_deadline: None, + setup_deadline: None, } } @@ -50,71 +46,31 @@ impl UnixStreamLocalControlChannel { UnixStream::connect(path).map(Self::from_connected) } - /// Sets the read and write timeout for broker control-channel operations. - pub fn set_io_timeout(&mut self, timeout: Option) -> IoResult<()> { - self.io_timeout = timeout; - self.io_deadline = None; - self.active_request_deadline = None; - self.set_stream_io_timeout(timeout) - } - - /// Sets a wall-clock deadline for broker control-channel operations. - pub fn set_io_deadline(&mut self, deadline: Option) -> IoResult<()> { - self.io_deadline = deadline; - self.active_request_deadline = None; - match deadline { - Some(deadline) => self.set_stream_io_timeout(Some(io_timeout_for_deadline(deadline)?)), - None => self.set_stream_io_timeout(self.io_timeout), - } - } - - fn set_stream_io_timeout(&self, timeout: Option) -> IoResult<()> { - self.stream.set_read_timeout(timeout)?; - self.stream.set_write_timeout(timeout) - } - - fn current_deadline(&mut self) -> IoResult> { - if let Some(deadline) = self.io_deadline { - return Ok(Some(deadline)); - } - if let Some(deadline) = self.active_request_deadline { - return Ok(Some(deadline)); - } - let Some(timeout) = self.io_timeout else { - return Ok(None); - }; - let deadline = deadline_after(timeout)?; - self.active_request_deadline = Some(deadline); - Ok(Some(deadline)) + /// Connects to a userland broker Unix socket with a deadline for setup I/O. + /// + /// TODO: `UnixStream` does not expose a connect timeout, so this + /// deadline currently covers setup I/O after the initial connect + /// succeeds, but not a blocking connect call. + pub fn connect_with_setup_deadline( + path: impl AsRef, + deadline: Instant, + ) -> IoResult { + UnixStream::connect(path).map(|stream| Self { + stream, + setup_deadline: Some(deadline), + }) } } /// Host-side Unix-domain-socket control channel for the hosted userland POC. pub struct UnixStreamHostControlChannel { stream: UnixStream, - io_deadline: Option, } impl UnixStreamHostControlChannel { /// Creates a host control channel from an accepted Unix stream. pub const fn from_accepted(stream: UnixStream) -> Self { - Self { - stream, - io_deadline: None, - } - } - - /// Sets a wall-clock deadline for all broker control-channel operations. - pub fn set_io_deadline(&mut self, deadline: Option) -> IoResult<()> { - self.io_deadline = deadline; - if let Some(deadline) = deadline { - let timeout = io_timeout_for_deadline(deadline)?; - self.stream.set_read_timeout(Some(timeout))?; - self.stream.set_write_timeout(Some(timeout)) - } else { - self.stream.set_read_timeout(None)?; - self.stream.set_write_timeout(None) - } + Self { stream } } } @@ -123,21 +79,16 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { fn send_handshake_request(&mut self, request: &BrokerHandshakeRequest) -> IoResult<()> { let frame = encode_handshake_request(request.clone()); - let deadline = self.current_deadline()?; - let result = write_frame_with_deadline(&mut self.stream, &frame, deadline); - if result.is_err() { - self.active_request_deadline = None; - } - result + write_frame_with_deadline(&mut self.stream, &frame, self.setup_deadline) } fn recv_handshake_response(&mut self) -> IoResult> { - let deadline = self.current_deadline()?; - let frame = read_frame_with_deadline(&mut self.stream, deadline); - if self.io_deadline.is_none() && self.active_request_deadline.take().is_some() { - self.set_stream_io_timeout(self.io_timeout)?; + let frame = read_frame_with_deadline(&mut self.stream, self.setup_deadline)?; + if self.setup_deadline.take().is_some() { + self.stream.set_read_timeout(None)?; + self.stream.set_write_timeout(None)?; } - match frame? { + match frame { Some(frame) => decode_handshake_response(&frame) .map(Some) .map_err(wire_error), @@ -147,21 +98,11 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { fn send_request(&mut self, request: &BrokerRequest) -> IoResult<()> { let frame = encode_request(request.clone()); - let deadline = self.current_deadline()?; - let result = write_frame_with_deadline(&mut self.stream, &frame, deadline); - if result.is_err() { - self.active_request_deadline = None; - } - result + write_frame_with_deadline(&mut self.stream, &frame, None) } fn recv_response(&mut self) -> IoResult> { - let deadline = self.current_deadline()?; - let frame = read_frame_with_deadline(&mut self.stream, deadline); - if self.io_deadline.is_none() && self.active_request_deadline.take().is_some() { - self.set_stream_io_timeout(self.io_timeout)?; - } - match frame? { + match read_frame_with_deadline(&mut self.stream, None)? { Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), None => Ok(None), } @@ -178,7 +119,7 @@ impl HostControlChannel for UnixStreamHostControlChannel { } fn recv_handshake_request(&mut self) -> IoResult> { - let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { + let Some(frame) = read_frame_with_deadline(&mut self.stream, None)? else { return Ok(HostReceive::PeerClosed); }; match decode_handshake_request(&frame) { @@ -192,12 +133,12 @@ impl HostControlChannel for UnixStreamHostControlChannel { write_frame_with_deadline( &mut self.stream, &encode_handshake_response(response.clone()), - self.io_deadline, + None, ) } fn recv_request(&mut self) -> IoResult> { - let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { + let Some(frame) = read_frame_with_deadline(&mut self.stream, None)? else { return Ok(HostReceive::PeerClosed); }; match decode_request(&frame) { @@ -208,11 +149,7 @@ impl HostControlChannel for UnixStreamHostControlChannel { } fn send_response(&mut self, response: &BrokerResponse) -> IoResult<()> { - write_frame_with_deadline( - &mut self.stream, - &encode_response(response.clone()), - self.io_deadline, - ) + write_frame_with_deadline(&mut self.stream, &encode_response(response.clone()), None) } } @@ -304,12 +241,6 @@ fn io_timeout_for_deadline(deadline: Instant) -> IoResult { Ok(timeout) } -fn deadline_after(timeout: Duration) -> IoResult { - Instant::now() - .checked_add(timeout) - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "broker I/O timeout overflow")) -} - fn invalid_data(message: &'static str) -> Error { Error::new(ErrorKind::InvalidData, message) } @@ -395,14 +326,14 @@ mod tests { } #[test] - fn local_response_read_io_timeout_is_wall_clock() { + fn local_handshake_response_read_setup_deadline_is_wall_clock() { let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); - channel - .set_io_timeout(Some(Duration::from_millis(50))) - .unwrap(); + let mut channel = UnixStreamLocalControlChannel { + stream: local_stream, + setup_deadline: Some(Instant::now() + Duration::from_millis(50)), + }; - let reader = std::thread::spawn(move || channel.recv_response().unwrap_err()); + let reader = std::thread::spawn(move || channel.recv_handshake_response().unwrap_err()); host_stream.write_all(&8u32.to_le_bytes()).unwrap(); for _ in 0..8 { std::thread::sleep(Duration::from_millis(20)); diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 11cb8e60a5..6bc055aed4 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -70,10 +70,7 @@ fn run_fake_runner(args: &[OsString]) { assert_eq!(args.len(), 4, "unexpected runner arguments: {args:?}"); let socket_path = args.get(2).unwrap(); - let mut channel = connect_with_retry(Path::new(socket_path)).unwrap(); - channel - .set_io_timeout(Some(Duration::from_secs(5))) - .unwrap(); + let channel = connect_with_retry(Path::new(socket_path)).unwrap(); let mut local = BrokerLocal::negotiate(channel).unwrap(); let handle = local.create_event_with_count(0).unwrap(); @@ -130,7 +127,7 @@ impl Drop for ChildGuard { fn connect_with_retry(socket_path: &Path) -> Result { let deadline = Instant::now() + Duration::from_secs(5); loop { - match UnixStreamLocalControlChannel::connect(socket_path) { + match UnixStreamLocalControlChannel::connect_with_setup_deadline(socket_path, deadline) { Ok(channel) => return Ok(channel), Err(error) if Instant::now() < deadline => { if error.kind() != ErrorKind::NotFound diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index dce9cc9cce..05737cae24 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -40,17 +40,12 @@ fn connect_to_endpoint(socket_path: &Path) -> Result { fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { loop { - match UnixStreamLocalControlChannel::connect(socket_path) { - Ok(mut channel) => { - channel - .set_io_deadline(Some(setup_deadline)) - .context("failed to configure broker setup deadline")?; - let mut local = - BrokerLocal::negotiate(channel).context("broker negotiation failed")?; - local - .control_channel_mut() - .set_io_deadline(None) - .context("failed to clear broker setup deadline")?; + match UnixStreamLocalControlChannel::connect_with_setup_deadline( + socket_path, + setup_deadline, + ) { + Ok(channel) => { + let local = BrokerLocal::negotiate(channel).context("broker negotiation failed")?; return Ok(local); } Err(error) => { From f8819aea59e6ec645c25359f569f74c17c91592c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 25 Jun 2026 17:22:27 -0700 Subject: [PATCH 068/319] Document broker POC design limits (#970) Clarifies two intentional limits in the initial broker PR #880: the policy engine is a placeholder static policy surface, and broker control calls are currently blocking. Notes that richer policy and non-blocking broker integration will be added later. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 4 ++++ litebox_broker_core/src/policy.rs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 408aa955fc..8665e106ae 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -15,6 +15,10 @@ use error::BrokerControlError; /// LiteBox owns broker-backed local objects and constructs broker protocol /// requests. Deployment code owns endpoint selection and supplies the connected /// transport behind this protocol-level boundary. +/// +/// The current interface is intentionally blocking for the initial broker POC. +/// Longer-term broker integrations should move away from blocking control calls +/// once the local-core wait and notification model supports that shape. pub(crate) trait BrokerControl: Send + Sync { /// Sends one active broker request and returns its response. fn request( diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index c736e96276..b5c921fcf2 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -41,6 +41,10 @@ impl PrincipalRights { } /// Broker policy decision and audit component. +/// +/// This initial engine is a placeholder static policy surface for the broker +/// POC. A fuller policy model is intentionally deferred until the broker needs +/// authenticated principals, richer rules, and audit integration. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PolicyEngine { profile: PolicyProfile, From 0090c90b51950867d9c89a1a3fc2b67acd0b2938 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 26 Jun 2026 09:54:52 -0700 Subject: [PATCH 069/319] Fix broker-backed eventfd lifecycle semantics (#973) Adds an explicit broker object close request and uses it when broker-backed event counters are dropped, so closed eventfds release their broker object references before session teardown. Keeps broker-backed eventfd operations nonblocking even if file status flags are later changed, until broker readiness notifications can safely wake local waiters. Fixes #966. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 64 ++++++++++++--- litebox/src/event/counter.rs | 92 ++++++++-------------- litebox_broker_host/src/lib.rs | 44 ++++++++++- litebox_broker_local/src/event.rs | 3 + litebox_broker_local/src/lib.rs | 32 +++++++- litebox_broker_protocol/src/message.rs | 6 +- litebox_broker_protocol/src/wire.rs | 29 ++++++- litebox_shim_linux/src/syscalls/eventfd.rs | 14 ++-- 8 files changed, 204 insertions(+), 80 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 8665e106ae..6d51432bf8 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -2,8 +2,9 @@ // Licensed under the MIT license. use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; -use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse}; +use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode, ReadinessState}; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; @@ -20,11 +21,29 @@ use error::BrokerControlError; /// Longer-term broker integrations should move away from blocking control calls /// once the local-core wait and notification model supports that shape. pub(crate) trait BrokerControl: Send + Sync { - /// Sends one active broker request and returns its response. - fn request( + fn create_event_with_count( &self, - request: BrokerRequest, - ) -> core::result::Result; + initial_count: u64, + ) -> core::result::Result; + + fn wait_event( + &self, + handle: ObjectHandle, + ) -> core::result::Result; + + fn add_event( + &self, + handle: ObjectHandle, + value: u64, + ) -> core::result::Result; + + fn consume_event( + &self, + handle: ObjectHandle, + mode: EventConsumeMode, + ) -> core::result::Result; + + fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError>; } pub(crate) struct BrokerLocalControl< @@ -51,10 +70,37 @@ where Platform: RawSyncPrimitivesProvider, Channel: LocalControlChannel + Send, { - fn request( + fn create_event_with_count( + &self, + initial_count: u64, + ) -> core::result::Result { + Ok(self.local.lock().create_event_with_count(initial_count)?) + } + + fn wait_event( &self, - request: BrokerRequest, - ) -> core::result::Result { - Ok(self.local.lock().request(request)?) + handle: ObjectHandle, + ) -> core::result::Result { + Ok(self.local.lock().wait_event(handle)?) + } + + fn add_event( + &self, + handle: ObjectHandle, + value: u64, + ) -> core::result::Result { + Ok(self.local.lock().add_event(handle, value)?) + } + + fn consume_event( + &self, + handle: ObjectHandle, + mode: EventConsumeMode, + ) -> core::result::Result { + Ok(self.local.lock().consume_event(handle, mode)?) + } + + fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError> { + Ok(self.local.lock().close_object(handle)?) } } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 815fc7d937..510d8f32fd 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -5,18 +5,15 @@ use alloc::sync::Arc; use litebox_broker_protocol::ObjectHandle; pub use litebox_broker_protocol::event::EventConsumeMode as EventCounterReadMode; -use litebox_broker_protocol::event::{ - AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, ReadinessState, - WaitEventRequest, -}; -use litebox_broker_protocol::message::{ - BrokerRequest, BrokerResponse, EventRequest, EventResponse, -}; +use litebox_broker_protocol::event::{ConsumeEventResponse, ReadinessState}; use thiserror::Error; use crate::{ LiteBox, - broker::{BrokerControl, error::BrokerObjectError}, + broker::{ + BrokerControl, + error::{BrokerControlError, BrokerObjectError}, + }, event::{ Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, wait::WaitContext, @@ -64,18 +61,13 @@ where let Some(broker) = litebox.broker_control() else { return Err(EventCounterError::Unavailable); }; - let response = broker - .request(BrokerRequest::Event(EventRequest::Create( - CreateEventRequest { initial_count }, - ))) + let handle = broker + .create_event_with_count(initial_count) .map_err(BrokerObjectError::from) .map_err(EventCounterError::from)?; - let BrokerResponse::Event(EventResponse::Create(response)) = response else { - panic!("broker returned unexpected event response: {response:?}"); - }; Ok(Self { broker, - handle: response.handle, + handle, pollee: Pollee::new(), }) } @@ -119,46 +111,32 @@ where &self, mode: EventCounterReadMode, ) -> Result { - let response = self.request_event(EventRequest::Consume(ConsumeEventRequest { - handle: self.handle, - mode, - }))?; - let EventResponse::Consume(response) = response else { - panic!("broker returned unexpected event response: {response:?}"); - }; - Ok(response) + self.broker + .consume_event(self.handle, mode) + .map_err(|error| self.broker_request_error(error)) } fn add(&self, value: u64) -> Result { - let response = self.request_event(EventRequest::Add(AddEventRequest { - handle: self.handle, - value, - }))?; - let EventResponse::Add(response) = response else { - panic!("broker returned unexpected event response: {response:?}"); - }; - Ok(response.readiness) + self.broker + .add_event(self.handle, value) + .map_err(|error| self.broker_request_error(error)) } - fn request_event(&self, request: EventRequest) -> Result { - match self - .broker - .request(BrokerRequest::Event(request)) - .map_err(BrokerObjectError::from) - .inspect_err(|&error| { - if error != BrokerObjectError::WouldBlock { - self.pollee.notify_observers(Events::ERR); - } - })? { - BrokerResponse::Event(response) => Ok(response), - BrokerResponse::Error(error) => { - let error = error.into(); - if error != BrokerObjectError::WouldBlock { - self.pollee.notify_observers(Events::ERR); - } - Err(error) - } + fn broker_request_error(&self, error: BrokerControlError) -> BrokerObjectError { + let error = error.into(); + if error != BrokerObjectError::WouldBlock { + self.pollee.notify_observers(Events::ERR); } + error + } +} + +impl Drop for EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + fn drop(&mut self) { + let _ = self.broker.close_object(self.handle); } } @@ -171,17 +149,15 @@ where } fn check_io_events(&self) -> Events { - let response = match self.request_event(EventRequest::Wait(WaitEventRequest { - handle: self.handle, - })) { - Ok(response) => response, + let readiness = match self + .broker + .wait_event(self.handle) + .map_err(|error| self.broker_request_error(error)) + { + Ok(readiness) => readiness, Err(BrokerObjectError::WouldBlock) => return Events::empty(), Err(_) => return Events::ERR, }; - let EventResponse::Wait(response) = response else { - panic!("broker returned unexpected event response: {response:?}"); - }; - let readiness = response.readiness; let mut events = Events::empty(); if readiness.read_ready { events |= Events::IN; diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 376cb43692..20fd3cb3d0 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -110,6 +110,10 @@ where fn handle_request(session: &BrokerSession, request: BrokerRequest) -> BrokerResponse { match request { + BrokerRequest::CloseObject(handle) => match session.close_object_reference(handle) { + Ok(()) => BrokerResponse::ObjectClosed, + Err(error) => BrokerResponse::Error(error.into()), + }, BrokerRequest::Event(request) => handle_event_request(session, request), } } @@ -163,9 +167,9 @@ pub enum ConnectionTermination { mod tests { use super::*; use litebox_broker_core::{PolicyEngine, PrincipalRights}; - use litebox_broker_protocol::ProtocolVersion; - use litebox_broker_protocol::event::CreateEventRequest; + use litebox_broker_protocol::event::{CreateEventRequest, WaitEventRequest}; use litebox_broker_protocol::message::BrokerHandshakeRequest; + use litebox_broker_protocol::{ObjectHandle, ProtocolVersion}; #[test] fn host_request_handling_uses_one_broker_core() { @@ -179,6 +183,7 @@ mod tests { serve_connection_rejects_active_request_before_negotiation(&broker); serve_connection_rejects_handshake_request_after_negotiation(&broker); serve_connection_returns_channel_error_when_response_send_fails(&broker); + active_request_closes_object_reference(&broker); } fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { @@ -298,6 +303,41 @@ mod tests { assert!(channel.handshake_responses.is_empty()); } + fn active_request_closes_object_reference(broker: &BrokerCore) { + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let response = handle_request( + &session, + BrokerRequest::Event(EventRequest::Create(CreateEventRequest { + initial_count: 0, + })), + ); + let BrokerResponse::Event(EventResponse::Create(response)) = response else { + panic!("unexpected create response: {response:?}"); + }; + let handle = response.handle; + + assert_eq!( + handle_request(&session, BrokerRequest::CloseObject(handle)), + BrokerResponse::ObjectClosed + ); + assert_eq!( + handle_request( + &session, + BrokerRequest::Event(EventRequest::Wait(WaitEventRequest { handle })) + ), + BrokerResponse::Error(ErrorCode::UnknownObject) + ); + assert_eq!( + handle_request( + &session, + BrokerRequest::CloseObject(ObjectHandle(handle.0 + 1)) + ), + BrokerResponse::Error(ErrorCode::UnknownObject) + ); + } + struct FakeHostControlChannel { handshake_requests: std::vec::Vec, ()>>, diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 539e3a8d7e..829391965c 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -87,6 +87,9 @@ impl BrokerLocal { match self.request(BrokerRequest::Event(request))? { BrokerResponse::Event(response) => Ok(response), BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response @ BrokerResponse::ObjectClosed => { + panic!("broker returned unexpected event response: {response:?}"); + } } } } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index a61cf21ff4..f90e481af1 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -15,12 +15,12 @@ extern crate std; mod error; mod event; -use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, }; +use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; pub use error::{BrokerLocalError, Result}; @@ -103,7 +103,23 @@ impl BrokerLocal { | ErrorCode::Internal => panic!("broker returned unrecoverable error: {error}"), _ => panic!("broker returned unsupported error: {error}"), }, - response @ BrokerResponse::Event(_) => Ok(response), + response @ (BrokerResponse::Event(_) | BrokerResponse::ObjectClosed) => Ok(response), + } + } + + /// Closes one broker object reference. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a protocol + /// response that does not match an object close request. + pub fn close_object(&mut self, handle: ObjectHandle) -> Result<(), Channel::Error> { + match self.request(BrokerRequest::CloseObject(handle))? { + BrokerResponse::ObjectClosed => Ok(()), + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response @ BrokerResponse::Event(_) => { + panic!("broker returned unexpected close response: {response:?}"); + } } } } @@ -149,6 +165,18 @@ mod tests { assert_eq!(local.channel.sent_request, Some(request)); } + #[test] + fn close_object_sends_close_object_request() { + let handle = ObjectHandle(7); + let request = BrokerRequest::CloseObject(handle); + let response = BrokerResponse::ObjectClosed; + let channel = FakeControlChannel::new(None, Some(response.clone())); + let mut local = BrokerLocal { channel }; + + assert!(local.close_object(handle).is_ok()); + assert_eq!(local.channel.sent_request, Some(request)); + } + #[test] fn active_request_returns_recoverable_broker_error() { let request = BrokerRequest::Event(EventRequest::Create(CreateEventRequest { diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index 44a7277a51..36c8666370 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::ProtocolVersion; use crate::error::ErrorCode; use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, CreateEventResponse, WaitEventRequest, WaitEventResponse, }; +use crate::{ObjectHandle, ProtocolVersion}; /// Broker handshake request sent before the control channel is active. #[derive(Clone, Debug, PartialEq, Eq)] @@ -18,6 +18,8 @@ pub struct BrokerHandshakeRequest { /// Broker request sent over an active control channel. #[derive(Clone, Debug, PartialEq, Eq)] pub enum BrokerRequest { + /// Close one broker object reference. + CloseObject(ObjectHandle), /// Event object request family. Event(EventRequest), } @@ -62,6 +64,8 @@ pub enum EventRequest { /// Broker response sent over an active control channel. #[derive(Clone, Debug, PartialEq, Eq)] pub enum BrokerResponse { + /// Object close operation completed. + ObjectClosed, /// Event object response family. Event(EventResponse), /// Operation failed with an ABI-neutral broker error. diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 46600424c4..4e2ac06448 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -30,11 +30,13 @@ mod primitive; const REQUEST_TAG_NEGOTIATE: u8 = 0; const REQUEST_TAG_EVENT: u8 = 1; +const REQUEST_TAG_CLOSE_OBJECT: u8 = 2; const RESPONSE_TAG_NEGOTIATED: u8 = 0; const RESPONSE_TAG_EVENT: u8 = 1; const RESPONSE_TAG_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; +const RESPONSE_TAG_OBJECT_CLOSED: u8 = 4; /// Error produced while encoding or decoding a broker wire message. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] @@ -73,7 +75,7 @@ pub fn decode_handshake_request(frame: &[u8]) -> Result BrokerHandshakeRequest { protocol_version: decoder.protocol_version()?, }, - REQUEST_TAG_EVENT => return Err(WireError::WrongMessagePhase), + REQUEST_TAG_EVENT | REQUEST_TAG_CLOSE_OBJECT => return Err(WireError::WrongMessagePhase), _ => return Err(WireError::InvalidTag), }; decoder.finish()?; @@ -87,6 +89,10 @@ pub fn decode_handshake_request(frame: &[u8]) -> Result Vec { let mut encoder = Encoder::default(); match request { + BrokerRequest::CloseObject(handle) => { + encoder.u8(REQUEST_TAG_CLOSE_OBJECT); + encoder.handle(handle); + } BrokerRequest::Event(request) => { encoder.u8(REQUEST_TAG_EVENT); event::encode_event_request(&mut encoder, request); @@ -101,6 +107,7 @@ pub fn decode_request(frame: &[u8]) -> Result { let tag = decoder.u8()?; let request = match tag { REQUEST_TAG_NEGOTIATE => return Err(WireError::WrongMessagePhase), + REQUEST_TAG_CLOSE_OBJECT => BrokerRequest::CloseObject(decoder.handle()?), REQUEST_TAG_EVENT => BrokerRequest::Event(event::decode_event_request(&mut decoder)?), _ => return Err(WireError::InvalidTag), }; @@ -143,7 +150,9 @@ pub fn decode_handshake_response(frame: &[u8]) -> Result BrokerHandshakeResponse::Negotiated { broker_protocol_version: decoder.protocol_version()?, }, - RESPONSE_TAG_EVENT => return Err(WireError::WrongMessagePhase), + RESPONSE_TAG_EVENT | RESPONSE_TAG_OBJECT_CLOSED => { + return Err(WireError::WrongMessagePhase); + } RESPONSE_TAG_VERSION_MISMATCH => BrokerHandshakeResponse::VersionMismatch { broker_protocol_version: decoder.protocol_version()?, }, @@ -164,6 +173,9 @@ pub fn decode_handshake_response(frame: &[u8]) -> Result Vec { let mut encoder = Encoder::default(); match response { + BrokerResponse::ObjectClosed => { + encoder.u8(RESPONSE_TAG_OBJECT_CLOSED); + } BrokerResponse::Event(response) => { encoder.u8(RESPONSE_TAG_EVENT); event::encode_event_response(&mut encoder, response); @@ -189,6 +201,7 @@ pub fn decode_response(frame: &[u8]) -> Result { let error = ErrorCode::from_raw(decoder.u16()?).ok_or(WireError::InvalidTag)?; BrokerResponse::Error(error) } + RESPONSE_TAG_OBJECT_CLOSED => BrokerResponse::ObjectClosed, _ => return Err(WireError::InvalidTag), }; decoder.finish()?; @@ -224,6 +237,7 @@ mod tests { fn request_codec_round_trips_all_variants() { let handle = ObjectHandle(13); let requests = [ + BrokerRequest::CloseObject(handle), BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, })), @@ -275,6 +289,7 @@ mod tests { fn response_codec_round_trips_all_variants() { let handle = ObjectHandle(13); let responses = [ + BrokerResponse::ObjectClosed, BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })), BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { readiness: ReadinessState { @@ -330,6 +345,12 @@ mod tests { )))), Err(WireError::WrongMessagePhase) ); + assert_eq!( + decode_handshake_request(&encode_request(BrokerRequest::CloseObject(ObjectHandle( + 13 + )))), + Err(WireError::WrongMessagePhase) + ); let mut frame = encode_handshake_request(BrokerHandshakeRequest { protocol_version: ProtocolVersion(1), }); @@ -389,6 +410,10 @@ mod tests { ))), Err(WireError::WrongMessagePhase) ); + assert_eq!( + decode_handshake_response(&encode_response(BrokerResponse::ObjectClosed)), + Err(WireError::WrongMessagePhase) + ); let mut frame = encode_handshake_response(BrokerHandshakeResponse::Negotiated { broker_protocol_version: ProtocolVersion(1), diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 37c4757b20..4e926e9e3d 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -30,10 +30,9 @@ impl FdEnabledSubsystemEntry for EventFile {} /// Backing counter for a Linux eventfd file description. /// -/// New blocking eventfds still use the shim-local implementation to keep the -/// initial broker-backed scope narrow. Broker-backed nonblocking eventfds can -/// still be switched to blocking mode because the local-core counter can block -/// through LiteBox-local readiness notifications. +/// New blocking eventfds use the shim-local path. Broker-backed counters stay +/// nonblocking even if file status flags are later changed, until broker +/// readiness notifications can wake local waiters. enum EventFileCounter { ShimLocal { count: Mutex, @@ -72,7 +71,9 @@ impl EventFileCounter counter .read( cx, - nonblock, + // Broker-backed eventfds cannot safely park local waiters + // until broker readiness notifications exist. + true, if semaphore { EventCounterReadMode::One } else { @@ -95,7 +96,8 @@ impl EventFileCounter counter.write(cx, nonblock, value).map_err(Errno::from), + // See the matching LocalCore read path for why this stays nonblocking. + Self::LocalCore(counter) => counter.write(cx, true, value).map_err(Errno::from), } } From 59be876952713477d7e16b3b7b59ce9987950963 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Fri, 26 Jun 2026 10:32:11 -0700 Subject: [PATCH 070/319] Cherry pick "Refactor OP-TEE TA lifecycle management" (#975) Co-authored-by: Sangho Lee --- litebox_runner_lvbs/src/lib.rs | 633 +++++----- .../src/lib.rs | 7 +- .../src/tests.rs | 6 +- litebox_shim_optee/src/lib.rs | 9 +- litebox_shim_optee/src/session.rs | 1066 +++++++++++++---- 5 files changed, 1113 insertions(+), 608 deletions(-) diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index 0ae319e59d..893afe57fa 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -5,9 +5,7 @@ extern crate alloc; -use alloc::boxed::Box; -use alloc::sync::Arc; -use alloc::vec; +use alloc::{boxed::Box, vec}; use core::{ops::Neg, panic::PanicInfo}; use litebox::{ mm::linux::PAGE_SIZE, @@ -43,12 +41,9 @@ use litebox_platform_multiplex::Platform; use litebox_shim_optee::msg_handler::{ decode_ta_request, handle_optee_msg_args, handle_optee_smc_args, update_optee_msg_args, }; -use litebox_shim_optee::session::{ - CreationReservation, SessionIdGuard, SessionManager, TaInstance, allocate_session_id, -}; +use litebox_shim_optee::session::{OpenSessionTarget, SessionManager, TaInstance}; use litebox_shim_optee::{NormalWorldConstPtr, NormalWorldMutPtr, UserConstPtr}; use once_cell::race::OnceBox; -use spin::mutex::SpinMutex; /// Seed the initial heap regions so the global allocator has enough memory /// for slab-backed allocations (the slab needs >= 2 MB backing pages). @@ -358,6 +353,30 @@ unsafe fn delete_task_page_table(task_pt_id: usize) -> Result<(), OpteeSmcReturn } } +/// Enforces the invariant that the core must be on the base (kernel) page +/// table before returning to VTL0: the guard switches to the TA's task +/// page table on entry and switches back on drop, covering early-return +/// and `?` paths. +/// +/// `switch_to_base_page_table` is an idempotent CR3 write, so teardown +/// paths that switch to base internally before deleting the task page +/// table can run before this guard's `Drop` — the redundant write at +/// drop time is benign. +struct TaskPageTableGuard; + +impl TaskPageTableGuard { + fn enter(task_pt_id: usize) -> Result { + unsafe { switch_to_task_page_table(task_pt_id)? }; + Ok(Self) + } +} + +impl Drop for TaskPageTableGuard { + fn drop(&mut self) { + unsafe { switch_to_base_page_table() }; + } +} + /// Tears down a TA's memory mappings and page table. /// /// This performs the following steps in order: @@ -497,98 +516,54 @@ fn handle_open_session( let client_identity = ta_req_info.client_identity; let params = &ta_req_info.params; - // Look up cached TA flags to determine single vs multi-instance. - // For the first-ever load of a UUID (no cached flags), conservatively - // assume single-instance to preserve all safety invariants. - let is_single_instance = session_manager() - .get_known_flags(&ta_uuid) - .is_none_or(|f| f.is_single_instance()); - - // Resolve or create the TA instance. - // For single-instance TAs, `with_creation_slot` re-checks the cache - // under its lock and serializes instance creation per UUID. - // If a cache hit returns a zombie (an instance torn down by a - // concurrent close/panic), evict the dead entry and ask the Linux driver - // to retry so it can create a fresh TA instance. - match session_manager().with_creation_slot(&ta_uuid, is_single_instance, || { - open_session_new_instance( + session_manager().with_ta(&ta_uuid, |target| match target { + OpenSessionTarget::Sibling(instance) => open_session_single_instance( + msg_args, + msg_args_phys_addr, + instance, + params, + &ta_req_info, + ), + OpenSessionTarget::NewInstance => open_session_new_instance( msg_args, msg_args_phys_addr, params, ta_uuid, client_identity, &ta_req_info, - ) - })? { - CreationReservation::ExistingSingleInstance(existing) => { - match open_session_single_instance( - msg_args, - msg_args_phys_addr, - existing.clone(), - params, - ta_uuid, - &ta_req_info, - )? { - OpenSessionOutcome::Handled => Ok(()), - OpenSessionOutcome::InstanceDestroyed => { - // Evict the zombie. Analog of OP-TEE's `maybe_release_ta_ctx` - // removing the dead ctx from `tee_ctxes`. - let _ = session_manager().remove_single_instance_if_same(&ta_uuid, &existing); - Err(OpteeSmcReturnCode::EThreadLimit) - } - } + ), + OpenSessionTarget::Busy => { + // Single-instance TA without MULTI_SESSION already has a live + // session. Per OP-TEE OS `tee_ta_init_session_with_context`, + // return TEE_ERROR_BUSY with origin TEE via msg_args. + msg_args.ret = TeeResult::Busy; + msg_args.ret_origin = TeeOrigin::Tee; + write_non_ta_msg_args_to_normal_world(msg_args, msg_args_phys_addr)?; + Ok(()) } - CreationReservation::SlotReserved => Ok(()), - } -} - -/// Outcome of [`open_session_single_instance`]. -enum OpenSessionOutcome { - /// Session was successfully opened, TA returned a non-fatal error, or TA panicked - /// and the instance was destroyed inline. No extra cleanup effort is needed. - Handled, - /// The cached `TaInstance` is `closed` and must not be entered. - InstanceDestroyed, + }) } /// Open a new session on an existing single-instance TA. /// -/// Returns `Err(OpteeSmcReturnCode::EThreadLimit)` if the TA instance is currently in use. -/// The Linux driver will wait and retry automatically. -/// Returns [`OpenSessionOutcome::InstanceDestroyed`] if the cached TA is closed. -/// /// If the TA's OpenSession entry point returns an error, the session is not registered. -/// On TARGET_DEAD the cached instance is destroyed unconditionally; any sibling sessions -/// become orphans that fail-fast on next access via the `instance.closed` check. +/// On TARGET_DEAD, sessions for the failed instance are marked `Dead`, the matching +/// single-instance cache entry is evicted, and the TA instance is torn down. /// For cleanup semantics, see OP-TEE OS `tee_ta_open_session()` in `tee_ta_manager.c`. -#[allow(clippy::type_complexity)] fn open_session_single_instance( msg_args: &mut OpteeMsgArgs, msg_args_phys_addr: u64, - instance_arc: Arc>, + instance: &TaInstance, params: &[litebox_common_optee::UteeParamOwned], - ta_uuid: litebox_common_optee::TeeUuid, ta_req_info: &litebox_shim_optee::msg_handler::TaRequestInfo, -) -> Result { - // Use try_lock to avoid spinning - return EThreadLimit if TA is in use - // The Linux driver will handle this by waiting and retrying - let mut instance = instance_arc - .try_lock() - .ok_or(OpteeSmcReturnCode::EThreadLimit)?; - - // `closed == true` means the instance is terminal and must not be entered. - if instance.closed { - return Ok(OpenSessionOutcome::InstanceDestroyed); - } - let task_pt_id = instance.task_page_table_id; +) -> Result<(), OpteeSmcReturnCode> { + let task_pt_id = instance.task_page_table_id(); + let ta_uuid = instance.uuid(); + let ta_flags = instance.loaded_program().ta_flags; - // Allocate session ID BEFORE calling load_ta_context so TA gets correct ID. - // Use SessionIdGuard to ensure the ID is recycled on any error path - // (before it is registered with the session manager). - let session_id_guard = - SessionIdGuard::new(allocate_session_id().ok_or(OpteeSmcReturnCode::EBusy)?); - // Safe to unwrap: guard was just created with Some(id). - let runner_session_id = session_id_guard.id().unwrap(); + let mut session_token = session_manager().try_acquire_open_session_token()?; + // Safe to unwrap: session ID has been just created. + let runner_session_id = session_token.session_id().unwrap(); debug_serial_println!( "Reusing single-instance TA: uuid={:?}, task_pt_id={}, session_id={}", @@ -597,14 +572,12 @@ fn open_session_single_instance( runner_session_id ); - let ta_flags = instance.loaded_program.ta_flags; - // Switch to the existing TA's page table - unsafe { switch_to_task_page_table(task_pt_id)? }; + let _task_pt_guard = TaskPageTableGuard::enter(task_pt_id)?; // Load TA context with parameters for OpenSession - pass actual session_id instance - .loaded_program + .loaded_program() .entrypoints .as_ref() .ok_or(OpteeSmcReturnCode::EBadCmd)? @@ -620,14 +593,14 @@ fn open_session_single_instance( let mut ctx = litebox_common_linux::PtRegs::default(); unsafe { litebox_platform_lvbs::reenter_thread_ref( - instance.loaded_program.entrypoints.as_ref().unwrap(), + instance.loaded_program().entrypoints.as_ref().unwrap(), &mut ctx, ); } // Read TA output parameters from the stack buffer let params_address = instance - .loaded_program + .loaded_program() .params_address .ok_or(OpteeSmcReturnCode::EBadAddr)?; let ta_params = UserConstPtr::::from_usize(params_address) @@ -647,7 +620,7 @@ fn open_session_single_instance( ); // Write error response BEFORE switching page tables (accesses user memory). - // Keep the instance lock held until this completes so another core cannot + // `with_ta`'s serialization keeps the instance alive so another core cannot // tear down the active page table while this core is copying TA outputs. let write_result = write_msg_args_to_normal_world( msg_args, @@ -664,25 +637,23 @@ fn open_session_single_instance( if return_code == TeeResult::TargetDead { debug_serial_println!("Single-instance TA panicked during OpenSession, cleaning up"); - let _ = session_manager().remove_single_instance_if_same(&ta_uuid, &instance_arc); - instance.closed = true; - + session_manager().mark_sessions_dead_for_instance(instance); // Safety: We are about to tear down this TA instance; // no references to user-space memory will be held afterwards. - unsafe { teardown_ta_page_table(&instance.shim, task_pt_id) }; + unsafe { + teardown_ta_page_table(instance.shim(), task_pt_id); + }; // TODO: Per OP-TEE OS semantics, if the TA has INSTANCE_KEEP_ALIVE but not // INSTANCE_KEEP_CRASHED, we should respawn the TA here instead of just // cleaning it up. Currently we always clean up on panic. } - drop(instance); write_result?; - return Ok(OpenSessionOutcome::Handled); + return Ok(()); } // Treat write-back failure as OpenSession failure: do not publish the session. - let runner_session_id = session_id_guard.id().unwrap(); let write_result = write_msg_args_to_normal_world( msg_args, msg_args_phys_addr, @@ -696,48 +667,39 @@ fn open_session_single_instance( // deliver the session id to the normal world, so it will never issue a // matching CloseSession. For a non-keep-alive instance with no siblings // we tear the whole instance down, reclaiming the TA-side state, and the - // session id can be recycled normally. For keep-alive or shared + // session id is recycled by the token's drop. For keep-alive or shared // instances the TA still holds session-local state tagged with this id, - // so we forget the id (disarm the guard) to prevent a future OpenSession + // so we forget the id (disarm the token) to prevent a future OpenSession // from reusing it and colliding with the orphaned TA-side bookkeeping. if let Err(e) = write_result { - if !ta_flags.is_keep_alive() - && session_manager() - .sessions() - .count_sessions_for_instance(&instance_arc) - == 0 + if !ta_flags.is_keep_alive() && session_manager().count_sessions_for_instance(instance) == 0 { - let _ = session_manager().remove_single_instance_if_same(&ta_uuid, &instance_arc); - instance.closed = true; - + let _ = session_manager().evict_cached_instance(instance); // Safety: We are about to tear down this TA instance; // no references to user-space memory will be held afterwards. - unsafe { teardown_ta_page_table(&instance.shim, task_pt_id) }; + unsafe { + teardown_ta_page_table(instance.shim(), task_pt_id); + }; } else { - let _ = session_id_guard.disarm(); + session_token.disarm(); } - drop(instance); return Err(e); } - // Success: register session and disarm the guard (ownership transfers to session map) - session_manager().register_session(runner_session_id, instance_arc.clone(), ta_uuid, ta_flags); - session_id_guard.disarm(); - - drop(instance); + // Success: register a sibling session pointing at the existing instance. + session_manager().register_sibling_session(runner_session_id, instance)?; + session_token.disarm(); debug_serial_println!( "OpenSession complete on single-instance TA: session_id={}", runner_session_id ); - Ok(OpenSessionOutcome::Handled) + Ok(()) } -/// Create a new TA instance for a session. -/// -/// The caller must invoke this inside [`SessionManager::with_creation_slot`] -/// to ensure a creation slot is held during execution and released afterward. +/// Create a new TA instance for a session. Must be called from within a +/// [`SessionManager::with_ta`] closure. /// /// If ldelf loading or OpenSession entry point fails, the page table is torn down. /// Per OP-TEE OS semantics: if OpenSession returns non-success, cleanup happens. @@ -749,32 +711,27 @@ fn open_session_new_instance( client_identity: Option, ta_req_info: &litebox_shim_optee::msg_handler::TaRequestInfo, ) -> Result<(), OpteeSmcReturnCode> { - let ta_bin = find_ta_binary(ta_uuid).ok_or(OpteeSmcReturnCode::ENotAvail)?; + let Some(ta_bin) = find_ta_binary(ta_uuid) else { + msg_args.session = 0; + msg_args.ret = TeeResult::ItemNotFound; + msg_args.ret_origin = TeeOrigin::Tee; + write_non_ta_msg_args_to_normal_world(msg_args, msg_args_phys_addr)?; + return Ok(()); + }; - // Create and switch to new page table - let task_pt_id = create_task_page_table()?; + // Token is declared before `task_pt_guard` so it drops AFTER it. + // Marker only releases once CR3 is back to base. See + // `try_acquire_open_session_token` for why. + let mut session_token = session_manager().try_acquire_open_session_token()?; + let runner_session_id = session_token.session_id().unwrap(); + let task_pt_id = create_task_page_table()?; debug_serial_println!("Created task page table ID: {}", task_pt_id); - unsafe { - switch_to_task_page_table(task_pt_id).inspect_err(|_| { - // Safety: switch_to_task_page_table failed, so task page table is not active. - let _ = delete_task_page_table(task_pt_id); - })?; - } - - // Allocate session ID before loading - return EBusy to normal world if exhausted. - // Use SessionIdGuard to ensure the ID is recycled on any error path - // (before it is registered with the session manager). - let session_id_guard = SessionIdGuard::new(allocate_session_id().ok_or_else(|| { - // Safety: We're switching to base page table; no user-space refs held. - unsafe { switch_to_base_page_table() }; - // Safety: We've switched to the base page table above. + let _task_pt_guard = TaskPageTableGuard::enter(task_pt_id).inspect_err(|_| { + // Safety: switch_to_task_page_table failed, so task page table is not active. let _ = unsafe { delete_task_page_table(task_pt_id) }; - OpteeSmcReturnCode::EBusy - })?); - // Safe to unwrap: guard was just created with Some(id). - let runner_session_id = session_id_guard.id().unwrap(); + })?; // Load ldelf and TA - Box immediately to keep at fixed heap address let shim = litebox_shim_optee::OpteeShimBuilder::new().build(); @@ -920,9 +877,7 @@ fn open_session_new_instance( // Write back BEFORE publishing the instance. If the write fails, the // session is neither registered nor cached, so we just tear down the - // local resources and let `session_id_guard` recycle the ID on drop. - // Safe to unwrap: guard has not been disarmed yet. - let runner_session_id = session_id_guard.id().unwrap(); + // local resources and let `session_token` recycle the ID on drop. write_msg_args_to_normal_world( msg_args, msg_args_phys_addr, @@ -937,22 +892,15 @@ fn open_session_new_instance( unsafe { teardown_ta_page_table(&shim, task_pt_id) }; })?; - // Success: create TA instance - loaded_program is already boxed, no move happens - let instance = Arc::new(SpinMutex::new(TaInstance { + // Success: register the new session with the manager. + session_manager().register_new_session( + runner_session_id, shim, loaded_program, - task_page_table_id: task_pt_id, - closed: false, - })); - - // Success: register session and disarm the guard (ownership transfers to session map) - session_manager().register_session(runner_session_id, instance.clone(), ta_uuid, ta_flags); - session_id_guard.disarm(); - - // Cache single-instance TAs only after the opening session owns the instance. - if ta_flags.is_single_instance() { - session_manager().cache_single_instance(ta_uuid, instance.clone()); - } + task_pt_id, + ta_uuid, + ); + session_token.disarm(); debug_serial_println!( "OpenSession complete: session_id={}, single_instance={}", @@ -963,6 +911,29 @@ fn open_session_new_instance( Ok(()) } +/// Tear down a `Dead` session entry observed at Invoke/Close handler entry. +/// +/// Must be called from within a `with_session` closure so its serialization +/// covers the cleanup. +fn finalize_dead_session( + session_id: u32, + msg_args: &mut OpteeMsgArgs, + msg_args_phys_addr: u64, + return_code: TeeResult, + log_prefix: &str, +) -> Result<(), OpteeSmcReturnCode> { + session_manager().unregister_session(session_id); + msg_args.ret = return_code; + msg_args.ret_origin = TeeOrigin::Tee; + write_non_ta_msg_args_to_normal_world(msg_args, msg_args_phys_addr)?; + debug_serial_println!( + "{}: session_id={} on dead TA session", + log_prefix, + session_id + ); + Ok(()) +} + /// Handle InvokeCommand. /// /// Looks up the session by ID, switches to its page table, and runs the command. @@ -981,129 +952,103 @@ fn handle_invoke_command( let params = &ta_req_info.params; let session_id = ta_req_info.session; - // Get the session entry from the session map (need full entry for potential cleanup) - let session_entry = session_manager() - .get_session_entry(session_id) - .ok_or(OpteeSmcReturnCode::EBadCmd)?; - // Use try_lock to avoid spinning - return EThreadLimit if TA is in use - // The Linux driver will handle this by waiting and retrying - let Some(mut instance) = session_entry.instance.try_lock() else { - return Err(OpteeSmcReturnCode::EThreadLimit); - }; - // `closed == true` means the TA instance is terminal and must not be entered. - // The session is orphaned. Report TARGET_DEAD to the client. - if instance.closed { - drop(instance); - session_manager().unregister_session(session_id); - msg_args.ret = TeeResult::TargetDead; - msg_args.ret_origin = TeeOrigin::Tee; - write_non_ta_msg_args_to_normal_world(msg_args, msg_args_phys_addr)?; - debug_serial_println!( - "InvokeCommand: session_id={} on closed TA instance", - session_id - ); - return Ok(()); - } - let task_pt_id = instance.task_page_table_id; - - // Switch to the TA instance's page table - unsafe { switch_to_task_page_table(task_pt_id)? }; + session_manager().with_session(session_id, |instance| { + let Some(instance) = instance else { + return finalize_dead_session( + session_id, + msg_args, + msg_args_phys_addr, + TeeResult::TargetDead, + "InvokeCommand", + ); + }; + let task_pt_id = instance.task_page_table_id(); - debug_serial_println!( - "InvokeCommand: session_id={}, task_pt_id={}, cmd_id={}", - session_id, - task_pt_id, - cmd_id - ); + let _task_pt_guard = TaskPageTableGuard::enter(task_pt_id)?; - // Load TA context with parameters and cmd_id - pass actual session_id - let entrypoints_ref = instance.loaded_program.entrypoints.as_ref().unwrap(); - entrypoints_ref - .load_ta_context( - params.as_slice(), - Some(session_id), - UteeEntryFunc::InvokeCommand as u32, - Some(cmd_id), - ) - .map_err(|_| OpteeSmcReturnCode::EBadCmd)?; - - // Run the TA entry function using reference-based reenter to avoid moving the shim - let mut ctx = litebox_common_linux::PtRegs::default(); - unsafe { - litebox_platform_lvbs::reenter_thread_ref( - instance.loaded_program.entrypoints.as_ref().unwrap(), - &mut ctx, + debug_serial_println!( + "InvokeCommand: session_id={}, task_pt_id={}, cmd_id={}", + session_id, + task_pt_id, + cmd_id ); - } - // params_address is constant - stack buffer is reused across invocations - let params_address = instance - .loaded_program - .params_address - .ok_or(OpteeSmcReturnCode::EBadAddr)?; - let ta_params = UserConstPtr::::from_usize(params_address) - .read_at_offset(0) - .ok_or(OpteeSmcReturnCode::EBadAddr)?; + // Set up the entry-point parameters for InvokeCommand. + let entrypoints_ref = instance.loaded_program().entrypoints.as_ref().unwrap(); + entrypoints_ref + .load_ta_context( + params.as_slice(), + Some(session_id), + UteeEntryFunc::InvokeCommand as u32, + Some(cmd_id), + ) + .map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + let mut ctx = litebox_common_linux::PtRegs::default(); + unsafe { + litebox_platform_lvbs::reenter_thread_ref( + instance.loaded_program().entrypoints.as_ref().unwrap(), + &mut ctx, + ); + } - let return_code: u32 = ctx.rax.trunc(); - let return_code = TeeResult::try_from(return_code).unwrap_or(TeeResult::GenericError); + // params_address is constant - stack buffer is reused across invocations + let params_address = instance + .loaded_program() + .params_address + .ok_or(OpteeSmcReturnCode::EBadAddr)?; + let ta_params = UserConstPtr::::from_usize(params_address) + .read_at_offset(0) + .ok_or(OpteeSmcReturnCode::EBadAddr)?; - // Write response BEFORE switching page tables (accesses user memory). - // Keep the instance lock held until this completes so another core cannot - // tear down the active page table while this core is copying TA outputs. - let write_result = write_msg_args_to_normal_world( - msg_args, - msg_args_phys_addr, - return_code, - None, - Some(&ta_params), - Some(&ta_req_info), - ); + let return_code: u32 = ctx.rax.trunc(); + let return_code = TeeResult::try_from(return_code).unwrap_or(TeeResult::GenericError); - // Per OP-TEE OS: if TA panics (TARGET_DEAD), the TA context is - // unrecoverable; all sessions on the same single-instance TA are - // implicitly dead (Ref: tee_ta_invoke_command() in tee_ta_manager.c). - if return_code == TeeResult::TargetDead { - debug_serial_println!( - "InvokeCommand: TA panicked (TARGET_DEAD), session_id={}", - session_id + // Write response BEFORE switching page tables (accesses user memory). + // `with_session`'s serialization keeps the entry stable so another core cannot + // tear down the active page table while this core is copying TA outputs. + let write_result = write_msg_args_to_normal_world( + msg_args, + msg_args_phys_addr, + return_code, + None, + Some(&ta_params), + Some(&ta_req_info), ); - let ta_uuid = session_entry.ta_uuid; - let ta_flags = session_entry.ta_flags; - - // Remove this session from the map. Sibling sessions on the same - // single-instance TA will be cleaned up lazily on their next - // invoke/close via the `instance.closed` check. - session_manager().unregister_session(session_id); - - // Clear single-instance cache so new OpenSessions for this UUID - // create a fresh instance instead of hitting the zombie one. - if ta_flags.is_single_instance() { - let _ = - session_manager().remove_single_instance_if_same(&ta_uuid, &session_entry.instance); - } + // Per OP-TEE OS: if TA panics (TARGET_DEAD), the TA context is + // unrecoverable; all sessions on the same single-instance TA are + // implicitly dead (Ref: tee_ta_invoke_command() in tee_ta_manager.c). + if return_code == TeeResult::TargetDead { + debug_serial_println!( + "InvokeCommand: TA panicked (TARGET_DEAD), session_id={}", + session_id + ); - instance.closed = true; + if instance.loaded_program().ta_flags.is_single_instance() { + session_manager().mark_sessions_dead_for_instance(instance); + } - // Safety: We are about to tear down this TA instance; - // no references to user-space memory will be held afterwards. - // The lock is held, so no other core can enter the TA. - unsafe { teardown_ta_page_table(&instance.shim, task_pt_id) }; + session_manager().unregister_session(session_id); - drop(instance); + // Safety: We are about to tear down this TA instance; + // no references to user-space memory will be held afterwards. + unsafe { + teardown_ta_page_table(instance.shim(), task_pt_id); + }; - debug_serial_println!( - "InvokeCommand: cleaned up dead TA instance, task_pt_id={}", - task_pt_id - ); + debug_serial_println!( + "InvokeCommand: cleaned up dead TA instance, task_pt_id={}", + task_pt_id + ); - // TODO: Per OP-TEE OS semantics, if the TA has INSTANCE_KEEP_ALIVE but not - // INSTANCE_KEEP_CRASHED, we should respawn the TA here instead of just - // cleaning it up. Currently we always clean up on panic. - } + // TODO: Per OP-TEE OS semantics, if the TA has INSTANCE_KEEP_ALIVE but not + // INSTANCE_KEEP_CRASHED, we should respawn the TA here instead of just + // cleaning it up. Currently we always clean up on panic. + } - write_result + write_result + }) } /// Handle CloseSession command. @@ -1123,87 +1068,63 @@ fn handle_close_session( debug_serial_println!("CloseSession: session_id={}", session_id); - // Get the session entry from the session map - let session_entry = session_manager() - .get_session_entry(session_id) - .ok_or(OpteeSmcReturnCode::EBadCmd)?; - // Use try_lock to avoid spinning - return EThreadLimit if TA is in use - // The Linux driver will handle this by waiting and retrying - let Some(mut instance) = session_entry.instance.try_lock() else { - return Err(OpteeSmcReturnCode::EThreadLimit); - }; - // `closed == true` means the TA instance is terminal and must not be entered. - // From the client's perspective the session no longer exists, so - // CloseSession is trivially successful. - if instance.closed { - drop(instance); - session_manager().unregister_session(session_id); - msg_args.ret = TeeResult::Success; - msg_args.ret_origin = TeeOrigin::Tee; - write_non_ta_msg_args_to_normal_world(msg_args, msg_args_phys_addr)?; - debug_serial_println!( - "CloseSession complete: session_id={}, TA instance closed", - session_id - ); - return Ok(()); - } - let task_pt_id = instance.task_page_table_id; - - // Switch to the TA instance's page table - unsafe { switch_to_task_page_table(task_pt_id)? }; + session_manager().with_session(session_id, |instance| { + let Some(instance) = instance else { + return finalize_dead_session( + session_id, + msg_args, + msg_args_phys_addr, + TeeResult::Success, + "CloseSession", + ); + }; + let task_pt_id = instance.task_page_table_id(); + + let _task_pt_guard = TaskPageTableGuard::enter(task_pt_id)?; + + // Set up the entry-point parameters for CloseSession. + instance + .loaded_program() + .entrypoints + .as_ref() + .unwrap() + .load_ta_context( + &[], + Some(session_id), + UteeEntryFunc::CloseSession as u32, + None, + ) + .map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + // Run the TA entry function (TA_CloseSessionEntryPoint) + let mut ctx = litebox_common_linux::PtRegs::default(); + unsafe { + litebox_platform_lvbs::reenter_thread_ref( + instance.loaded_program().entrypoints.as_ref().unwrap(), + &mut ctx, + ); + } - // Load TA context for CloseSession (no params, no cmd_id) - pass actual session_id - instance - .loaded_program - .entrypoints - .as_ref() - .unwrap() - .load_ta_context( - &[], - Some(session_id), - UteeEntryFunc::CloseSession as u32, + // CloseSession always succeeds (TA_CloseSessionEntryPoint returns void) + let write_result = write_msg_args_to_normal_world( + msg_args, + msg_args_phys_addr, + TeeResult::Success, + None, + None, None, - ) - .map_err(|_| OpteeSmcReturnCode::EBadCmd)?; - - // Run the TA entry function (TA_CloseSessionEntryPoint) - let mut ctx = litebox_common_linux::PtRegs::default(); - unsafe { - litebox_platform_lvbs::reenter_thread_ref( - instance.loaded_program.entrypoints.as_ref().unwrap(), - &mut ctx, ); - } - // CloseSession always succeeds (TA_CloseSessionEntryPoint returns void) - let write_result = write_msg_args_to_normal_world( - msg_args, - msg_args_phys_addr, - TeeResult::Success, - None, - None, - None, - ); + let removed_flags = session_manager().unregister_session(session_id); + + let remaining_sessions = session_manager().count_sessions_for_instance(instance); - // Clone the instance Arc before dropping the lock for later cleanup check - let instance_arc = session_entry.instance.clone(); - - // Remove the session entry from the map - let removed_entry = session_manager().unregister_session(session_id); - - // Check if this was the last session using the TA instance by counting - // remaining sessions that reference this instance. - let remaining_sessions = session_manager() - .sessions() - .count_sessions_for_instance(&instance_arc); - - // If this was the last session using the TA instance, clean up (unless keep_alive is set) - if remaining_sessions == 0 { - if let Some(entry) = removed_entry { - // If this is a single-instance TA with keep_alive flag, don't remove it from memory. - // Note: keep_alive is only meaningful for single-instance TAs. - if entry.ta_flags.is_single_instance() && entry.ta_flags.is_keep_alive() { - drop(instance); + // Last session on this instance — tear it down unless `keep_alive` + // is set (only meaningful for single-instance TAs). + if remaining_sessions == 0 + && let Some(flags) = removed_flags + { + if flags.is_single_instance() && flags.is_keep_alive() { debug_serial_println!( "CloseSession complete: session_id={}, TA kept alive (INSTANCE_KEEP_ALIVE flag)", session_id @@ -1211,35 +1132,31 @@ fn handle_close_session( return write_result; } - // Clear single-instance cache if this was a single-instance TA - if entry.ta_flags.is_single_instance() { - let _ = - session_manager().remove_single_instance_if_same(&entry.ta_uuid, &instance_arc); + // If this was a single-instance TA, clear the cached instance. This is safe because + // we confirm no sibling sessions remain. We don't need to mark anything `Dead` first. + if flags.is_single_instance() { + let _ = session_manager() + .evict_cached_instance(instance); } - instance.closed = true; - // Safety: We are about to tear down this TA instance; // no references to user-space memory will be held afterwards. - // The lock is held, so no other core can enter the TA. - unsafe { teardown_ta_page_table(&instance.shim, task_pt_id) }; - - // Drop the instance to release shim/loaded_program resources - drop(instance); + unsafe { + teardown_ta_page_table(instance.shim(), task_pt_id); + }; debug_serial_println!( "CloseSession complete: deleted task_pt_id={} (last session)", task_pt_id ); + } else { + debug_serial_println!( + "CloseSession complete: session_id={}, other sessions remaining on TA", + session_id + ); } - } else { - drop(instance); - debug_serial_println!( - "CloseSession complete: session_id={}, other sessions remaining on TA", - session_id - ); - } - write_result + write_result + }) } /// Update msg_args with return values and write back to normal world memory. diff --git a/litebox_runner_optee_on_linux_userland/src/lib.rs b/litebox_runner_optee_on_linux_userland/src/lib.rs index 7e45993767..ae37334cd4 100644 --- a/litebox_runner_optee_on_linux_userland/src/lib.rs +++ b/litebox_runner_optee_on_linux_userland/src/lib.rs @@ -5,7 +5,7 @@ use anyhow::{Context as _, Result}; use clap::Parser; use litebox_common_optee::{TeeUuid, UteeEntryFunc, UteeParamOwned}; use litebox_platform_multiplex::Platform; -use litebox_shim_optee::session::allocate_session_id; +use litebox_shim_optee::session::SessionManager; use std::path::PathBuf; mod tests; @@ -109,17 +109,20 @@ fn run_ta_with_default_commands( ldelf_bin: &[u8], ta_bin: &[u8], ) { + let session_manager = SessionManager::new(); for func_id in [UteeEntryFunc::OpenSession, UteeEntryFunc::CloseSession] { let params = [const { UteeParamOwned::None }; UteeParamOwned::TEE_NUM_PARAMS]; if func_id == UteeEntryFunc::OpenSession { + let session_token = session_manager.try_acquire_open_session_token().unwrap(); + let session_id = session_token.session_id().unwrap(); let loaded_program = shim .load_ldelf( ldelf_bin, TeeUuid::default(), Some(ta_bin), None, - allocate_session_id().unwrap(), + session_id, ) .map_err(|_| { panic!("Failed to load ldelf"); diff --git a/litebox_runner_optee_on_linux_userland/src/tests.rs b/litebox_runner_optee_on_linux_userland/src/tests.rs index 4bfe1f3da2..9bfe30f8ec 100644 --- a/litebox_runner_optee_on_linux_userland/src/tests.rs +++ b/litebox_runner_optee_on_linux_userland/src/tests.rs @@ -8,7 +8,7 @@ use litebox::platform::RawConstPointer; use litebox::utils::TruncateExt; use litebox_common_optee::{TeeParamType, UteeEntryFunc, UteeParamOwned, UteeParams}; -use litebox_shim_optee::session::allocate_session_id; +use litebox_shim_optee::session::SessionManager; use litebox_shim_optee::{LoadedProgram, UserConstPtr}; use serde::Deserialize; use std::path::PathBuf; @@ -26,6 +26,7 @@ pub fn run_ta_with_test_commands( serde_json::from_str(&json_str).unwrap() }; let mut ta_info: Option = None; + let session_manager = SessionManager::new(); for cmd in ta_commands { assert!( @@ -49,13 +50,14 @@ pub fn run_ta_with_test_commands( if func_id == UteeEntryFunc::OpenSession { let ta_head = litebox_common_optee::parse_ta_head(ta_bin) .expect("Failed to parse TA header from ta_bin"); + let session_token = session_manager.try_acquire_open_session_token().unwrap(); let loaded = shim .load_ldelf( ldelf_bin, ta_head.uuid, Some(ta_bin), None, - allocate_session_id().unwrap(), + session_token.session_id().unwrap(), ) .map_err(|_| { panic!("Failed to load TA"); diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index b3e933fe8f..3f9cdf9235 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -38,10 +38,7 @@ pub mod msg_handler; pub mod ptr; // Re-export session management types for convenience -pub use session::{ - CreationReservation, MAX_TA_INSTANCES, SessionEntry, SessionManager, SessionMap, - SingleInstanceCache, TaInstance, allocate_session_id, -}; +pub use session::{OpenSessionTarget, SessionManager, SessionToken, TaInstance}; const MAX_KERNEL_BUF_SIZE: usize = 0x80_000; @@ -1453,6 +1450,10 @@ impl SessionIdPool { } /// Recycle a session ID for reuse. Fallback IDs are not recycled. + /// + /// "Recycled" only marks the bit free; [`IdPool`](litebox::utils::id_pool::IdPool) + /// is hint+wrap, so the ID is not handed out again until every higher ID + /// has been allocated first. pub fn recycle(session_id: u32) { if session_id == 0 || session_id > Self::MAX_RECYCLABLE_SESSION_ID { return; diff --git a/litebox_shim_optee/src/session.rs b/litebox_shim_optee/src/session.rs index 5d21b404a8..3e462da290 100644 --- a/litebox_shim_optee/src/session.rs +++ b/litebox_shim_optee/src/session.rs @@ -10,10 +10,16 @@ //! //! ## Concurrency Model //! -//! Single-instance TAs (with `TA_FLAG_SINGLE_INSTANCE | TA_FLAG_MULTI_SESSION`) share -//! one TA instance across multiple sessions. When multiple CPUs try to invoke commands -//! on the same TA instance concurrently, we use `try_lock()` and return -//! `OPTEE_SMC_RETURN_ETHREAD_LIMIT` at the SMC level if the lock is held. +//! TA execution is serialized externally; [`TaInstance`] is shared without +//! an inner mutex. The exclusivity invariant lives in [`SessionManager`] +//! and is acquired through an internal RAII `SessionToken` that bundles +//! whichever locks the current operation requires — see `SessionToken`'s +//! doc for the per-case breakdown. +//! +//! Both [`SessionManager::with_ta`] (OpenSession) and +//! [`SessionManager::with_session`] (Invoke/Close) acquire the token +//! non-blockingly, run the caller's closure under it, and release on +//! return. On contention they return `EThreadLimit`. //! //! ### Difference from OP-TEE OS //! @@ -34,6 +40,10 @@ //! the waiting logic in normal world (where scheduling is appropriate), without //! requiring RPCs that would give untrusted code control over secure world execution. //! +//! Panic cleanup paths flip all sessions for the failed instance to `Dead` +//! and evict the matching cached instance via +//! [`SessionManager::mark_sessions_dead_for_instance`]. +//! //! Reference: //! //! ## OP-TEE OS Thread IDs and RPC @@ -94,122 +104,165 @@ use crate::{LoadedProgram, OpteeShim, SessionIdPool}; use alloc::sync::Arc; +use core::sync::atomic::{AtomicBool, Ordering}; use hashbrown::{HashMap, HashSet}; use litebox_common_optee::{OpteeSmcReturnCode, TaFlags, TeeUuid}; use spin::mutex::SpinMutex; /// Maximum number of concurrent TA instances to avoid out of memory situations. -pub const MAX_TA_INSTANCES: usize = 16; +const MAX_TA_INSTANCES: usize = 16; -/// A loaded TA instance that can be shared across multiple sessions. +/// A loaded TA instance. /// -/// For single-instance TAs (with `TA_FLAG_SINGLE_INSTANCE`), one TA instance -/// is shared across all sessions. The TA is loaded once and stays in memory until -/// the last session closes (or with `TA_FLAG_INSTANCE_KEEP_ALIVE`, until explicit destroy). -/// -/// Each instance has its own task page table that provides memory isolation from other TAs. +/// For single-instance TAs one instance is shared across all sessions; the +/// TA stays in memory until the last session closes (if it does not have the +/// `TA_FLAG_INSTANCE_KEEP_ALIVE` flag). Each instance has its own task page +/// table that provides memory isolation from other TAs. pub struct TaInstance { /// The shim must be kept alive to keep the loaded program's memory mappings valid. - pub shim: OpteeShim, + shim: OpteeShim, /// The loaded TA program state including entrypoints. /// Boxed to keep it at a fixed heap address - the Task inside must not be moved /// after initialization because it contains internal state that may not survive moves. - pub loaded_program: alloc::boxed::Box, - /// The task page table ID associated with this TA instance. Valid only - /// while `closed == false`. - pub task_page_table_id: usize, - /// Set when the TA is committed to teardown (panic or last session closed). Any lock - /// holders should check `closed` before touching `task_page_table_id` and bail if true. + loaded_program: alloc::boxed::Box, + /// The task page table ID associated with this TA instance. /// - /// The per-instance lock must be held when setting `closed = true` and across - /// the subsequent `teardown_ta_page_table`. - pub closed: bool, + /// Also serves as the instance's identity for sibling-tracking + /// operations: page table ids are minted by `create_task_page_table()` + /// and not reused until the owning instance is fully torn down. + task_page_table_id: usize, + ta_uuid: TeeUuid, } -// SAFETY: TaInstance is protected by SpinMutex and try_lock (`SessionEntry`) +impl TaInstance { + pub fn task_page_table_id(&self) -> usize { + self.task_page_table_id + } + + pub fn shim(&self) -> &OpteeShim { + &self.shim + } + + pub fn loaded_program(&self) -> &LoadedProgram { + &self.loaded_program + } + + pub fn uuid(&self) -> TeeUuid { + self.ta_uuid + } +} + +// SAFETY: `TaInstance`'s interior (`shim`, `loaded_program`) is not +// auto-`Send`/`Sync`, but every access goes through a `SessionToken` that +// serializes execution on the per-UUID lock (single-instance TAs) or the +// per-`session_id` marker (multi-instance TAs), so at most one core is +// ever inside a given instance. See the module-level "Concurrency Model". unsafe impl Send for TaInstance {} unsafe impl Sync for TaInstance {} -/// Per-session entry in the session map. +/// What an OpenSession should do given the current cache state for a +/// `uuid`, as decided by [`SessionManager::with_ta`] under its +/// serialization. The closure dispatches on the variant. +pub enum OpenSessionTarget<'a> { + /// No cached single-instance instance for this UUID (either it's + /// not single-instance, or the cache is empty). Closure should load + /// a fresh TA and call `register_new_session`. + NewInstance, + /// A cached single-instance TA is available for sharing. Closure + /// should reuse it for a sibling session via `register_sibling_session`. + Sibling(&'a TaInstance), + /// A cached single-instance TA exists but it lacks `TA_FLAG_MULTI_SESSION` + /// and already has at least one live session. Per OP-TEE OS + /// `tee_ta_init_session_with_context`, reject with + /// `TEE_ERROR_BUSY` (origin TEE). + Busy, +} + +/// Per-session entry in the session map. The `Dead` variant retains +/// `(ta_uuid, ta_flags)` so cleanup paths and `try_acquire_for_session`'s +/// snapshot still have them after the instance is gone. #[derive(Clone)] -pub struct SessionEntry { - /// The TA instance (may be shared with other sessions for single-instance TAs). - pub instance: Arc>, - /// The TA UUID (needed for cleanup of single-instance TAs). - pub ta_uuid: TeeUuid, - /// TA flags parsed from the `.ta_head` section. - pub ta_flags: TaFlags, +enum SessionEntry { + Live(Arc), + Dead { ta_uuid: TeeUuid, ta_flags: TaFlags }, +} + +impl SessionEntry { + fn ta_uuid(&self) -> TeeUuid { + match self { + SessionEntry::Live(arc) => arc.ta_uuid, + SessionEntry::Dead { ta_uuid, .. } => *ta_uuid, + } + } + + fn ta_flags(&self) -> TaFlags { + match self { + SessionEntry::Live(arc) => arc.loaded_program.ta_flags, + SessionEntry::Dead { ta_flags, .. } => *ta_flags, + } + } } /// Session map for tracking active sessions. /// /// Maps runner-allocated session IDs to session entries. -pub struct SessionMap { +struct SessionMap { inner: SpinMutex>, } impl SessionMap { /// Create a new empty session map. - pub fn new() -> Self { + fn new() -> Self { Self { inner: SpinMutex::new(HashMap::new()), } } - /// Get a session's TA instance by session ID. - pub fn get(&self, session_id: u32) -> Option>> { - self.inner - .lock() - .get(&session_id) - .map(|e| e.instance.clone()) - } - /// Get full session entry by session ID. - pub fn get_entry(&self, session_id: u32) -> Option { + fn get_entry(&self, session_id: u32) -> Option { self.inner.lock().get(&session_id).cloned() } - /// Insert a session into the map. - pub fn insert( - &self, - session_id: u32, - instance: Arc>, - ta_uuid: TeeUuid, - ta_flags: TaFlags, - ) { - self.inner.lock().insert( - session_id, - SessionEntry { - instance, - ta_uuid, - ta_flags, - }, - ); + /// Insert a live session into the map. + fn insert_live(&self, session_id: u32, instance: Arc) { + self.inner + .lock() + .insert(session_id, SessionEntry::Live(instance)); } /// Remove a session from the map. - pub fn remove(&self, session_id: u32) -> Option { + fn remove(&self, session_id: u32) -> Option { self.inner.lock().remove(&session_id) } - /// Get the number of active sessions. - pub fn len(&self) -> usize { - self.inner.lock().len() - } - - /// Check if the session map is empty. - pub fn is_empty(&self) -> bool { - self.inner.lock().is_empty() - } - - /// Count sessions for a specific TA instance (by Arc pointer equality). - pub fn count_sessions_for_instance(&self, instance: &Arc>) -> usize { + /// Count live sessions whose instance has the given page table id. + fn count_sessions_for_pt(&self, task_page_table_id: usize) -> usize { self.inner .lock() .values() - .filter(|e| Arc::ptr_eq(&e.instance, instance)) + .filter(|e| match e { + SessionEntry::Live(arc) => arc.task_page_table_id == task_page_table_id, + SessionEntry::Dead { .. } => false, + }) .count() } + + /// Mark all live sessions whose instance has the given page table id + /// as `Dead`, capturing the instance's uuid and flags on the way out + /// so cleanup paths still have them. + fn mark_sessions_dead_for_pt(&self, task_page_table_id: usize) { + for entry in self.inner.lock().values_mut() { + let dead = match entry { + SessionEntry::Live(arc) if arc.task_page_table_id == task_page_table_id => { + Some((arc.ta_uuid, arc.loaded_program.ta_flags)) + } + _ => None, + }; + if let Some((ta_uuid, ta_flags)) = dead { + *entry = SessionEntry::Dead { ta_uuid, ta_flags }; + } + } + } } impl Default for SessionMap { @@ -222,33 +275,35 @@ impl Default for SessionMap { /// /// Single-instance TAs (with `TA_FLAG_SINGLE_INSTANCE`) share a single TA instance /// across all sessions. This cache stores instances by UUID for fast reuse lookup. -pub struct SingleInstanceCache { - inner: SpinMutex>>>, +struct SingleInstanceCache { + inner: SpinMutex>>, } impl SingleInstanceCache { /// Create a new empty cache. - pub fn new() -> Self { + fn new() -> Self { Self { inner: SpinMutex::new(HashMap::new()), } } /// Get a cached single-instance TA by UUID. - pub fn get(&self, uuid: &TeeUuid) -> Option>> { + fn get(&self, uuid: &TeeUuid) -> Option> { self.inner.lock().get(uuid).cloned() } /// Cache a single-instance TA by UUID. - pub fn insert(&self, uuid: TeeUuid, instance: Arc>) { + fn insert(&self, uuid: TeeUuid, instance: Arc) { self.inner.lock().insert(uuid, instance); } - /// Remove a cached single-instance TA only if it is the expected instance. - fn remove_if_same(&self, uuid: &TeeUuid, expected: &Arc>) -> bool { + /// Evict only if the cached instance matches `task_page_table_id`. + /// Distinguishes the live instance from a freshly-created one with the + /// same UUID when the caller wants to remove a specific one. + fn remove_matching_instance(&self, uuid: &TeeUuid, task_page_table_id: usize) -> bool { let mut guard = self.inner.lock(); match guard.get(uuid) { - Some(current) if Arc::ptr_eq(current, expected) => { + Some(current) if current.task_page_table_id == task_page_table_id => { guard.remove(uuid); true } @@ -257,14 +312,9 @@ impl SingleInstanceCache { } /// Get the number of cached single-instance TAs. - pub fn len(&self) -> usize { + fn len(&self) -> usize { self.inner.lock().len() } - - /// Check if the cache is empty. - pub fn is_empty(&self) -> bool { - self.inner.lock().is_empty() - } } impl Default for SingleInstanceCache { @@ -277,177 +327,498 @@ impl Default for SingleInstanceCache { /// /// Delegates to `SessionIdPool::allocate` for unified session ID management. /// Returns `None` if all session IDs are exhausted. -pub fn allocate_session_id() -> Option { +fn allocate_session_id() -> Option { SessionIdPool::allocate() } /// Recycle a session ID for potential future reuse. /// /// Delegates to `SessionIdPool::recycle`. -pub fn recycle_session_id(session_id: u32) { +fn recycle_session_id(session_id: u32) { SessionIdPool::recycle(session_id); } -/// RAII guard that recycles a session ID on drop unless disarmed. -/// -/// Session IDs are allocated before the TA is invoked and only registered on -/// success via [`SessionManager::register_session`]. This guard ensures it is -/// recycled on all error paths before this registration. -pub struct SessionIdGuard { - session_id: Option, +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum HeldUuidLock { + SingleInstance(TeeUuid), + TaLoad, } -impl SessionIdGuard { - /// Create a new guard that will recycle `session_id` on drop. - pub fn new(session_id: u32) -> Self { - Self { - session_id: Some(session_id), - } - } +/// An unified RAII token to safely execute an OP-TEE TA operation with +/// instance- or session-specific serialization primitives. +/// +/// Bundles whichever combination of locks the current operation requires: +/// +/// - **Known single-instance TAs**: a per-UUID lock flag (a `bool` slot in +/// `single_instance_locks`) that serializes all sessions on the same TA. +/// - **First-ever load of a not-yet-known UUID** (OpenSession only): the +/// global `ta_load_lock`, used until the TA's flags (single-instance vs +/// multi-instance) are observed. +/// - **Existing-session operations** (Invoke/Close): a per-session-id +/// marker (slot in `SessionManager::active_sessions`) that prevents +/// concurrent SMC entry by another core for the same id. +/// - **OpenSession (runner-facing)**: same per-session-id marker plus +/// a freshly-allocated `session_id` whose recycling the token owns +/// until [`Self::disarm`]. Acquired via +/// [`SessionManager::try_acquire_open_session_token`]. +/// +/// For known multi-instance OpenSession the token (from `with_ta`) +/// holds nothing — each session gets its own private instance, so no +/// exclusion is required there. +/// +/// On drop the held UUID-level lock is released first (whether per-UUID +/// or the global load lock), then the per-session-id marker, then +/// (if still owned) the session id is recycled. +pub struct SessionToken<'a> { + manager: &'a SessionManager, + /// Logical UUID-level lock owned by this token. The actual lock state + /// lives in `SessionManager`; `Drop` releases it (clears the held flag). + uuid_lock: Option, + /// `Some(id)` while the token holds the active-session marker for `id` + /// in [`SessionManager::active_sessions`]. Drop releases the marker. + active_session_id: Option, + /// Whether `active_session_id` should also be recycled to the id pool + /// on drop (in addition to releasing the marker). Set when the id was + /// freshly allocated by + /// [`SessionManager::try_acquire_open_session_token`]; cleared by + /// [`Self::disarm`] after the id is transferred to the session map via + /// `register_*_session`. Only meaningful when `active_session_id` is + /// `Some`; ignored otherwise. + owns_id_recycling: bool, +} - /// Return the guarded session ID, or `None` if already disarmed. - pub fn id(&self) -> Option { - self.session_id +impl SessionToken<'_> { + /// Session id this token reserves the active-session marker for, if any. + /// Set for tokens minted by + /// [`SessionManager::try_acquire_open_session_token`] or + /// `try_acquire_for_session` (Invoke/Close). + pub fn session_id(&self) -> Option { + self.active_session_id } - /// Disarm the guard so the session ID is **not** recycled on drop. - /// - /// Call this after the session ID has been successfully registered. - /// Once registered, [`SessionManager::unregister_session`] owns recycling. - /// - /// Returns `None` if the guard was already disarmed. - pub fn disarm(mut self) -> Option { - self.session_id.take() + /// Transfer id-recycling responsibility off the token. Call after the + /// id has been registered via `register_new_session` / + /// `register_sibling_session`; from that point the session map (via + /// `unregister_session`) owns recycling, and the token's drop will + /// only release the marker (and any locks). + pub fn disarm(&mut self) { + self.owns_id_recycling = false; } } -impl Drop for SessionIdGuard { +impl Drop for SessionToken<'_> { fn drop(&mut self) { - if let Some(id) = self.session_id { - recycle_session_id(id); + if let Some(lock) = self.uuid_lock.take() { + self.manager.release_uuid_lock(lock); + } + if let Some(id) = self.active_session_id.take() { + self.manager.active_sessions.lock().remove(&id); + if self.owns_id_recycling { + recycle_session_id(id); + } } } } -/// Result of [`SessionManager::with_creation_slot`]. -pub enum CreationReservation { - /// An existing single-instance TA was found (another core cached it - /// between our initial lookup and the reservation). Reuse this instance. - ExistingSingleInstance(Arc>), - /// The creation closure ran successfully inside the reserved slot. - SlotReserved, -} - -/// State for coordinating concurrent instance creation. -/// -/// Guarded by a single lock to provide atomic capacity checks and -/// duplicate-UUID prevention. -struct CreationState { - /// UUIDs of single-instance TAs currently being loaded. Prevents multiple cores - /// from simultaneously creating a new instance for the same single-instance - /// TA UUID (which would violate the single-instance invariant). - /// Multi-instance TAs are not tracked here. They can be created concurrently. - pending_uuids: HashSet, - /// Number of instances currently being created (not yet registered). This - /// covers both single-instance and multi-instance TAs. - /// Added to [`SessionManager::instance_count`] for accurate capacity checks. - pending_count: usize, -} - /// Session manager that coordinates session and instance lifecycle. /// -/// This provides a unified interface for: -/// - Opening sessions (with single-instance TA reuse) -/// - Looking up sessions -/// - Closing sessions (with proper cleanup) +/// The public entry points are the closure-bound [`SessionManager::with_ta`] +/// (OpenSession) and [`SessionManager::with_session`] (Invoke/Close), which +/// run the caller's closure under an internal `SessionToken`. State +/// mutations the closure performs on the manager (registration, +/// sibling-marking, cache eviction) are serialized by that token. pub struct SessionManager { /// Active sessions mapped by session ID. sessions: SessionMap, /// Cache of single-instance TAs by UUID. single_instance_cache: SingleInstanceCache, - /// Coordination state for concurrent instance creation. - creation_state: SpinMutex, + /// Number of instances currently being created (not yet registered). + /// Added to [`SessionManager::instance_count`] for the capacity check + /// in [`SessionManager::with_ta`] so two concurrent loads cannot both + /// pass the limit before either registers. + pending_count: SpinMutex, /// Cached TA flags by UUID, populated on first successful session registration. + /// + /// TODO: a TA's flags (in particular single- vs multi-instance) can + /// change across a version update of the same UUID. Key this map by + /// `(uuid, version)` — or invalidate on version mismatch — once TA + /// versioning is wired through, so a re-loaded TA isn't serialized + /// under the old flags. known_flags: SpinMutex>, + /// Per-UUID serialization state for single-instance TA handling + /// (`true` == held). Entries are created lazily only for UUIDs that + /// have been observed to be single-instance — never for unknown UUIDs + /// whose load might fail or turn out to be multi-instance. + /// + /// We do not remove its entry even if the instance is destroyed to + /// support a future reload of the same TA. This is bounded in + /// practice because we only support a few managed TAs. This entry + /// management should be aligned with `known_flags`. + single_instance_locks: SpinMutex>, + /// Global gate that serializes the first-ever load of not-yet-known + /// UUIDs. Held by a first-loader until the TA's flags are observed; for a + /// single-instance TA, ownership is then handed off to its per-UUID lock + /// (see [`SessionToken`]). Known multi-instance UUIDs take no lock. + ta_load_lock: AtomicBool, + /// Session ids currently being handled (Invoke/Close). Guards a session + /// against concurrent SMC entry by another core that targets the same id. + active_sessions: SpinMutex>, } impl SessionManager { - /// Create a new session manager. pub fn new() -> Self { Self { sessions: SessionMap::new(), single_instance_cache: SingleInstanceCache::new(), - creation_state: SpinMutex::new(CreationState { - pending_uuids: HashSet::new(), - pending_count: 0, - }), + pending_count: SpinMutex::new(0), known_flags: SpinMutex::new(HashMap::new()), + single_instance_locks: SpinMutex::new(HashMap::new()), + ta_load_lock: AtomicBool::new(false), + active_sessions: SpinMutex::new(HashSet::new()), } } - /// Get the session map. - pub fn sessions(&self) -> &SessionMap { - &self.sessions - } - - /// Get the single-instance cache. - pub fn single_instance_cache(&self) -> &SingleInstanceCache { - &self.single_instance_cache - } - - /// Cache a single-instance TA. - pub fn cache_single_instance(&self, uuid: TeeUuid, instance: Arc>) { - self.single_instance_cache.insert(uuid, instance); + /// Allocate a fresh `session_id` and reserve its active-session slot. + /// See [`SessionToken`] for what the returned token carries. + /// + /// # Drop-order requirement + /// + /// On the OpenSession path the runner activates a TA page table + /// (`TaskPageTableGuard`) inside the same scope. The token *must* be + /// declared **before** that guard so it drops **after** it — the + /// marker must outlive the CR3 switch back to base, otherwise a + /// forged Close on the freshly-registered session can win the + /// marker race and tear down the task page table while CR3 still + /// points at it. (Single-instance is already covered by `with_ta`'s + /// per-UUID lock; this is the only defense for multi-instance.) + /// + /// # Errors + /// - `EBusy` if the id pool is exhausted. + pub fn try_acquire_open_session_token(&self) -> Result, OpteeSmcReturnCode> { + let session_id = allocate_session_id().ok_or(OpteeSmcReturnCode::EBusy)?; + // The id pool's hint+wrap allocator defers reuse of recycled ids, + // so a freshly-allocated id can never collide with a marker slot + // that's still held by a previous owner. + let inserted = self.active_sessions.lock().insert(session_id); + if !inserted && !cfg!(debug_assertions) { + litebox_util_log::warn!(session_id = session_id; "freshly-allocated session_id collided with an active marker"); + } + debug_assert!( + inserted, + "freshly-allocated session_id collided with an active marker" + ); + Ok(SessionToken { + manager: self, + uuid_lock: None, + active_session_id: Some(session_id), + owns_id_recycling: true, + }) } - /// Get a session by ID. - pub fn get_session(&self, session_id: u32) -> Option>> { - self.sessions.get(session_id) + /// Retire a dead single-instance TA from service. + /// + /// Marks every session currently pointing at `instance` as `Dead` and + /// evicts the matching entry from the single-instance cache. Use when + /// tearing down a *failed* TA that may still have sibling sessions. + pub fn mark_sessions_dead_for_instance(&self, instance: &TaInstance) { + self.sessions + .mark_sessions_dead_for_pt(instance.task_page_table_id); + let _ = self.evict_cached_instance(instance); } - /// Get full session entry by ID. - pub fn get_session_entry(&self, session_id: u32) -> Option { - self.sessions.get_entry(session_id) + /// Count live sessions currently pointing at `instance` (`Dead` entries + /// are skipped). Used by the last-close path to detect whether teardown + /// is appropriate. + pub fn count_sessions_for_instance(&self, instance: &TaInstance) -> usize { + self.sessions + .count_sessions_for_pt(instance.task_page_table_id) } /// Look up previously observed TA flags for a UUID. /// /// Returns `None` if this UUID has never been successfully loaded. /// Callers should conservatively assume single-instance when `None`. - pub fn get_known_flags(&self, uuid: &TeeUuid) -> Option { + fn get_known_flags(&self, uuid: &TeeUuid) -> Option { self.known_flags.lock().get(uuid).copied() } - /// Register a new session. - pub fn register_session( + /// Try to take the per-UUID serialization state non-blockingly. + fn try_acquire_uuid_lock(&self, uuid: TeeUuid) -> Option { + let mut locks = self.single_instance_locks.lock(); + let held = locks.entry(uuid).or_insert(false); + if *held { + None + } else { + *held = true; + Some(HeldUuidLock::SingleInstance(uuid)) + } + } + + fn release_uuid_lock(&self, lock: HeldUuidLock) { + match lock { + HeldUuidLock::SingleInstance(uuid) => { + if let Some(held) = self.single_instance_locks.lock().get_mut(&uuid) { + debug_assert!(*held); + *held = false; + } + } + HeldUuidLock::TaLoad => { + let was_held = self.ta_load_lock.swap(false, Ordering::Release); + debug_assert!(was_held); + } + } + } + + /// Try to take the global `ta_load_lock` non-blockingly. + fn try_acquire_ta_load_lock(&self) -> Option { + self.ta_load_lock + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .ok() + .map(|_| HeldUuidLock::TaLoad) + } + + /// Acquire a `SessionToken` for an OpenSession request. + /// + /// Dispatches by what's known about `uuid`: + /// + /// - **Known single-instance**: per-UUID lock flag. + /// - **Known multi-instance**: no lock (each session is independent). + /// - **Unknown**: the global `ta_load_lock`. This serializes first-loads + /// of all not-yet-known UUIDs together, but avoids minting a per-UUID + /// lock entry until the TA has been confirmed single-instance. A failed + /// or multi-instance load therefore leaves no stale entry in + /// `single_instance_locks`. + /// + /// Returns `Err(EThreadLimit)` on contention. + fn try_acquire_for_open(&self, uuid: TeeUuid) -> Result, OpteeSmcReturnCode> { + let uuid_lock = match self.get_known_flags(&uuid) { + Some(flags) if flags.is_single_instance() => Some( + self.try_acquire_uuid_lock(uuid) + .ok_or(OpteeSmcReturnCode::EThreadLimit)?, + ), + Some(_) => None, + None => Some( + self.try_acquire_ta_load_lock() + .ok_or(OpteeSmcReturnCode::EThreadLimit)?, + ), + }; + Ok(SessionToken { + manager: self, + uuid_lock, + active_session_id: None, + owns_id_recycling: false, + }) + } + + /// Acquire a token + validated entry for an Invoke/Close on an existing + /// session. Returns the entry that survived the post-lock re-read so + /// callers don't need to look it up again. + /// + /// Always reserves the per-session-id slot in `active_sessions`. For + /// single-instance TAs additionally takes the per-UUID lock so + /// sibling sessions on the same TA serialize against this operation. + /// + /// Returns `Err(EBadCmd)` if `session_id` is not registered, or + /// `Err(EThreadLimit)` if another core is inside the same session or + /// holds the per-UUID lock for the same single-instance TA. On failure + /// any partial acquisition is released via the token's `Drop`. + /// + /// # Ordering + /// + /// The per-UUID lock is acquired *before* the final session-map + /// re-read. This excludes concurrent `mark_sessions_dead_for_instance` + /// and cache eviction (which callers perform only while holding the UUID + /// lock), so the `Live` / `Dead` state observed in the re-read remains + /// authoritative for the lifetime of the returned token. Reading the + /// entry before taking the UUID lock would let a sibling complete the + /// entire mark-dead / evict / teardown sequence between our read and our + /// lock acquisition, leaving us holding a stale `Live` entry pointing + /// at a torn-down page table. + /// + /// Defense in depth: the entry's `(uuid, flags)` are validated against + /// the state observed before inserting the active-session marker. If + /// they diverge (the id was recycled and reused under a different TA + /// between our first read and the marker insert), we return + /// `EThreadLimit` so the Linux driver retries. + fn try_acquire_for_session( + &self, + session_id: u32, + ) -> Result<(SessionToken<'_>, SessionEntry), OpteeSmcReturnCode> { + let entry = self + .sessions + .get_entry(session_id) + .ok_or(OpteeSmcReturnCode::EBadCmd)?; + let pre_marker_uuid = entry.ta_uuid(); + let pre_marker_single = entry.ta_flags().is_single_instance(); + + if !self.active_sessions.lock().insert(session_id) { + return Err(OpteeSmcReturnCode::EThreadLimit); + } + let mut token = SessionToken { + manager: self, + uuid_lock: None, + active_session_id: Some(session_id), + owns_id_recycling: false, + }; + + // Take the per-UUID lock BEFORE the final re-read for single- + // instance TAs. This blocks any concurrent mark-dead / cache + // eviction so the re-read result is stable. On failure, the + // token's `Drop` releases the marker we already took. + if pre_marker_single { + token.uuid_lock = Some( + self.try_acquire_uuid_lock(pre_marker_uuid) + .ok_or(OpteeSmcReturnCode::EThreadLimit)?, + ); + } + + // Re-read under both locks and validate against the pre-marker state. + let entry_now = self + .sessions + .get_entry(session_id) + .ok_or(OpteeSmcReturnCode::EBadCmd)?; + if entry_now.ta_uuid() != pre_marker_uuid + || entry_now.ta_flags().is_single_instance() != pre_marker_single + { + return Err(OpteeSmcReturnCode::EThreadLimit); + } + + Ok((token, entry_now)) + } + + /// Drive an Invoke/Close to completion under the right serialization + /// (see [`SessionToken`] for the locks held). Passes + /// `Some(&TaInstance)` to `f` for live sessions, `None` for dead + /// ones. State mutations `f` performs on the manager + /// (`unregister_session`, `mark_sessions_dead_for_instance`, + /// `evict_cached_instance`) are serialized against concurrent + /// Invoke/Close on the same session and (single-instance) the same UUID. + /// + /// Returns `Err(EBadCmd)` if `session_id` is not registered, or + /// `Err(EThreadLimit)` on lock contention (driver retries + /// transparently). + pub fn with_session(&self, session_id: u32, f: F) -> Result<(), OpteeSmcReturnCode> + where + F: for<'a> FnOnce(Option<&'a TaInstance>) -> Result<(), OpteeSmcReturnCode>, + { + let (_token, entry) = self.try_acquire_for_session(session_id)?; + let instance = match &entry { + SessionEntry::Live(arc) => Some(&**arc), + SessionEntry::Dead { .. } => None, + }; + f(instance) + } + + /// Register a session for a freshly-loaded TA. The three parts (`shim`, + /// `loaded_program`, `task_page_table_id`) are taken by value and stored + /// inside the manager; for single-instance TAs the instance is also + /// cached under `ta_uuid` for later reuse. + /// + /// # Publication order + /// + /// `sessions` and (for single-instance) `single_instance_cache` are + /// populated *before* `known_flags`. Other openers gate on + /// `known_flags` to decide their lock path — once they observe `uuid` + /// as known single-instance, the cache is guaranteed to already + /// contain the entry, so they take the sibling/cache-hit branch + /// rather than racing into a duplicate load. + /// + /// # Unknown→per-UUID transition + /// + /// For single-instance TAs we mark the per-UUID state held *before* + /// publishing `known_flags` so any later opener that observes `uuid` + /// as known single-instance and routes to the per-UUID state finds it + /// already held. [`Self::with_ta`] adopts this state for *its own* + /// `uuid` by replacing the token's load-lock marker with a + /// per-UUID marker. This is UUID-keyed end-to-end: no shared side + /// channel, so concurrent `with_ta` calls for different UUIDs cannot + /// interfere with each other's adoptions. + /// + /// `try_acquire_uuid_lock` succeeds only on the load-lock path (caller + /// holds `ta_load_lock`, no sessions or `known_flags` entry for + /// `uuid` yet). On the known-cache-evicted path the caller already + /// holds the per-UUID state and acquisition returns `None`, so + /// nothing changes (the caller's existing lock is sufficient). + pub fn register_new_session( &self, session_id: u32, - instance: Arc>, + shim: OpteeShim, + loaded_program: alloc::boxed::Box, + task_page_table_id: usize, ta_uuid: TeeUuid, - ta_flags: TaFlags, ) { + let ta_flags = loaded_program.ta_flags; + let arc = Arc::new(TaInstance { + shim, + loaded_program, + task_page_table_id, + ta_uuid, + }); + + // Pre-hold per-UUID state for atomic unknown→per-UUID transition + // (see method doc). On known-cache-evicted paths this returns + // `None` because the caller already owns the per-UUID state. + if ta_flags.is_single_instance() { + let _ = self.try_acquire_uuid_lock(ta_uuid); + } + + self.sessions.insert_live(session_id, arc.clone()); + if ta_flags.is_single_instance() { + self.single_instance_cache.insert(ta_uuid, arc); + } + // Publish `known_flags` last — this is the gate other openers check. self.known_flags.lock().entry(ta_uuid).or_insert(ta_flags); - self.sessions - .insert(session_id, instance, ta_uuid, ta_flags); } - /// Unregister a session, recycle its session ID, and return the entry. - pub fn unregister_session(&self, session_id: u32) -> Option { + /// Register a session that re-uses an existing single-instance TA. + /// + /// `instance` is the cached handle handed to the + /// [`SessionManager::with_ta`] closure on the cache-hit branch. + pub fn register_sibling_session( + &self, + session_id: u32, + instance: &TaInstance, + ) -> Result<(), OpteeSmcReturnCode> { + let arc = self + .single_instance_cache + .get(&instance.ta_uuid) + .filter(|cached| cached.task_page_table_id == instance.task_page_table_id) + .ok_or(OpteeSmcReturnCode::EBadCmd)?; + // `known_flags` is already populated for this UUID — sibling path + // implies the instance was previously registered. + self.sessions.insert_live(session_id, arc); + Ok(()) + } + + /// Unregister a session and recycle its session ID. Returns whether + /// the session was registered and what flags it had (the latter for + /// callers that need to dispatch on `is_single_instance` / + /// `is_keep_alive` after removal). + pub fn unregister_session(&self, session_id: u32) -> Option { let entry = self.sessions.remove(session_id); if entry.is_some() { recycle_session_id(session_id); } - entry + entry.map(|e| e.ta_flags()) } - /// Remove a single-instance TA from the cache only if the currently - /// cached `Arc` is the same as `expected`. - pub fn remove_single_instance_if_same( - &self, - uuid: &TeeUuid, - expected: &Arc>, - ) -> bool { - self.single_instance_cache.remove_if_same(uuid, expected) + /// Evict `instance` from the single-instance cache. No-op (returns + /// `false`) if the cached entry under `instance.uuid()` is a different + /// instance — matched by `task_page_table_id` to distinguish the + /// caller's instance from a freshly-cached replacement. + /// + /// TA panic teardown should use + /// [`SessionManager::mark_sessions_dead_for_instance`] instead; it marks + /// all sessions for the failed instance dead and evicts the cache entry + /// in one transition. Later [`SessionManager::with_session`] calls for + /// existing session IDs will observe `Dead` on re-read, while later + /// [`SessionManager::with_ta`] calls for the UUID cannot reuse the dead + /// cached instance. + /// Callers on the last-session-close path may skip the mark step — by + /// that point there are no sibling sessions to fence out. + pub fn evict_cached_instance(&self, instance: &TaInstance) -> bool { + self.single_instance_cache + .remove_matching_instance(&instance.ta_uuid, instance.task_page_table_id) } /// Get the total count of unique TA instances (for limit checking). @@ -455,7 +826,7 @@ impl SessionManager { /// This counts: /// - All single-instance TAs in the cache (each UUID = 1 instance, regardless of session count) /// - All multi-instance TA sessions (each session = 1 instance) - pub fn instance_count(&self) -> usize { + fn instance_count(&self) -> usize { let single_instance_count = self.single_instance_cache.len(); let multi_instance_count = self.count_multi_instance_sessions(); single_instance_count + multi_instance_count @@ -467,78 +838,91 @@ impl SessionManager { .inner .lock() .values() - .filter(|e| !e.ta_flags.is_single_instance()) + .filter(|e| !e.ta_flags().is_single_instance()) .count() } - /// Check if instance limit is reached. - pub fn is_at_capacity(&self) -> bool { - self.instance_count() >= MAX_TA_INSTANCES - } - - /// Atomically reserve a creation slot and run `f` to create a new TA instance. + /// Drive an OpenSession to completion under the right serialization. /// - /// Behavior depends on whether the TA is: + /// Acquires the UUID-level lock for `uuid` (see [`SessionToken`] for + /// the case breakdown), classifies the cache state, and dispatches + /// via [`OpenSessionTarget`]: /// - /// - **Single-instance**: Re-checks the single-instance cache under the lock to - /// close TOCTOU windows, and prevents duplicate concurrent creation of - /// the same UUID via `pending_uuids`. + /// - [`OpenSessionTarget::Sibling`] for a cached single-instance TA + /// that admits another session. + /// - [`OpenSessionTarget::Busy`] for the OP-TEE-OS-defined + /// `TA_FLAG_MULTI_SESSION` violation (single-instance without + /// MULTI_SESSION already has a live session). + /// - [`OpenSessionTarget::NewInstance`] otherwise: reserves a + /// creation slot (capacity check against + /// `instance_count() + pending_count`) and lets the closure load + /// and register a fresh instance. /// - /// - **Multi-instance**: Each session gets its own independent TA instance, - /// matching OP-TEE OS behavior. Multiple cores may create instances of - /// the same UUID concurrently. - pub fn with_creation_slot( - &self, - uuid: &TeeUuid, - is_single_instance: bool, - f: F, - ) -> Result + /// `pending_count` exists only for capacity accounting so two + /// concurrent multi-instance loads can't both pass the limit before + /// either registers. The single-instance / unknown paths are + /// serialized by the UUID-level lock itself. + pub fn with_ta(&self, uuid: &TeeUuid, f: F) -> Result<(), OpteeSmcReturnCode> where - F: FnOnce() -> Result<(), OpteeSmcReturnCode>, + F: for<'a> FnOnce(OpenSessionTarget<'a>) -> Result<(), OpteeSmcReturnCode>, { - { - let mut state = self.creation_state.lock(); - - if is_single_instance { - // Check the single-instance cache under the creation lock. A - // hit means another core finished creating the instance for - // this UUID; reuse it instead of starting a new load. - if let Some(existing) = self.single_instance_cache.get(uuid) { - return Ok(CreationReservation::ExistingSingleInstance(existing)); - } + let mut token = self.try_acquire_for_open(*uuid)?; + // Captured before `f` runs so we know whether to perform the + // load-lock→per-UUID adoption step after successful registration. + let on_ta_load_path = matches!(token.uuid_lock, Some(HeldUuidLock::TaLoad)); - // Another core is currently in the middle of creating an instance - // for this single-instance UUID. The instance isn't cached yet, - // so we cannot reuse it. Return EThreadLimit to have the - // normal-world driver wait and retry. - if state.pending_uuids.contains(uuid) { - return Err(OpteeSmcReturnCode::EThreadLimit); - } - } + // Cache lookup is unconditional: it returns `None` for known + // multi-instance and unknown UUIDs (never populated), and only + // returns `Some` for known single-instance UUIDs whose entry the + // per-UUID lock above keeps stable. + if let Some(existing) = self.single_instance_cache.get(uuid) { + // MULTI_SESSION enforcement (matches OP-TEE OS + // `tee_ta_init_session_with_context`). Under the per-UUID lock + // the session count is stable across this check and the + // closure, so a parallel Close/Invoke can't change it. + let flags = existing.loaded_program().ta_flags; + let target = + if !flags.is_multi_session() && self.count_sessions_for_instance(&existing) > 0 { + OpenSessionTarget::Busy + } else { + OpenSessionTarget::Sibling(&existing) + }; + return f(target); + } + { + let mut pending = self.pending_count.lock(); // Capacity check including in-flight creations. - let total = self.instance_count() + state.pending_count; - if total >= MAX_TA_INSTANCES { + if self.instance_count() + *pending >= MAX_TA_INSTANCES { return Err(OpteeSmcReturnCode::ENomem); } - - if is_single_instance { - state.pending_uuids.insert(*uuid); - } - state.pending_count += 1; + *pending += 1; } - let result = f(); + let result = f(OpenSessionTarget::NewInstance); { - let mut state = self.creation_state.lock(); - if is_single_instance { - state.pending_uuids.remove(uuid); - } - state.pending_count = state.pending_count.saturating_sub(1); + let mut pending = self.pending_count.lock(); + *pending = pending.saturating_sub(1); } - result.map(|()| CreationReservation::SlotReserved) + // Complete the load-lock→per-UUID transition (see + // `register_new_session` doc). Only fires when we held the + // `ta_load_lock` AND the closure registered a single-instance + // TA for *our* `uuid`. The per-UUID state is already held from + // `register_new_session`'s pre-hold; swap the token to own that + // state and release the load lock. Token drop then releases the + // per-UUID state at the end of `with_ta`. UUID-keyed throughout, so + // concurrent `with_ta(other_uuid)` cannot adopt our lock. + if result.is_ok() + && on_ta_load_path + && self.single_instance_cache.get(uuid).is_some() + && let Some(old) = token.uuid_lock.replace(HeldUuidLock::SingleInstance(*uuid)) + { + self.release_uuid_lock(old); + } + + result } } @@ -547,3 +931,201 @@ impl Default for SessionManager { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::syscalls::tests::init_platform; + + fn make_shim() -> OpteeShim { + let _ = init_platform(); + crate::OpteeShimBuilder::new().build() + } + + fn make_loaded_program(ta_flags: TaFlags) -> alloc::boxed::Box { + alloc::boxed::Box::new(LoadedProgram { + entrypoints: None, + params_address: None, + ta_flags, + }) + } + + fn make_uuid(seed: u8) -> TeeUuid { + TeeUuid::from_bytes([seed; 16]) + } + + fn single_instance_flags() -> TaFlags { + TaFlags::SINGLE_INSTANCE | TaFlags::MULTI_SESSION + } + + /// Test helper: call `register_new_session` directly and release the + /// pre-held per-UUID lock state the way `with_ta` would, so subsequent + /// operations (Invoke/Close, evict, count, etc.) aren't blocked. + fn register_for_test( + manager: &SessionManager, + session_id: u32, + ta_flags: TaFlags, + task_page_table_id: usize, + ta_uuid: TeeUuid, + ) { + manager.register_new_session( + session_id, + make_shim(), + make_loaded_program(ta_flags), + task_page_table_id, + ta_uuid, + ); + if ta_flags.is_single_instance() + && let Some(held) = manager.single_instance_locks.lock().get_mut(&ta_uuid) + { + *held = false; + } + } + + /// Identity is by `task_page_table_id`, not by Arc pointer. After an + /// instance is evicted and a fresh one registered under the same UUID, + /// the stale handle must not evict the new one. + #[test] + fn evict_cached_instance_distinguishes_stale_handle() { + let manager = SessionManager::new(); + let uuid = make_uuid(0xA4); + + register_for_test(&manager, 105, single_instance_flags(), 10, uuid); + let arc_first = manager.single_instance_cache.get(&uuid).unwrap(); + manager.evict_cached_instance(&arc_first); + + register_for_test(&manager, 106, single_instance_flags(), 11, uuid); + assert!(!manager.evict_cached_instance(&arc_first)); + assert!(manager.single_instance_cache.get(&uuid).is_some()); + } + + /// `mark_sessions_dead_for_instance` retires the cached single-instance + /// TA: Live entries become Dead, stop counting for + /// `count_sessions_for_instance`, `with_session` thereafter sees `None`, + /// and new opens cannot reuse the dead cached instance. + #[test] + fn mark_dead_makes_with_session_observe_none() { + let manager = SessionManager::new(); + let uuid = make_uuid(0xA6); + register_for_test(&manager, 108, single_instance_flags(), 55, uuid); + let arc = manager.single_instance_cache.get(&uuid).unwrap(); + assert_eq!(manager.count_sessions_for_instance(&arc), 1); + + manager.mark_sessions_dead_for_instance(&arc); + assert_eq!(manager.count_sessions_for_instance(&arc), 0); + assert!(manager.single_instance_cache.get(&uuid).is_none()); + + manager + .with_session(108, |instance| { + assert!(instance.is_none()); + Ok(()) + }) + .unwrap(); + } + + /// A failed first-load of an unknown UUID must not mint a per-UUID + /// lock entry. Such loads serialize on `ta_load_lock`, so + /// `single_instance_locks` stays empty when the load fails or the TA + /// turns out to be multi-instance. + #[test] + fn with_ta_does_not_mint_lock_entry_for_failed_unknown_load() { + let manager = SessionManager::new(); + let uuid = make_uuid(0xA9); + assert!(manager.get_known_flags(&uuid).is_none()); + + let _ = manager.with_ta(&uuid, |_| Err(OpteeSmcReturnCode::ENotAvail)); + assert!(manager.single_instance_locks.lock().get(&uuid).is_none()); + assert!(manager.get_known_flags(&uuid).is_none()); + } + + /// `pending_count` is bumped only on the create path, never on the + /// cache-hit path, and is decremented when the closure returns whether + /// success or failure — across multiple calls it must return to zero. + #[test] + fn pending_count_returns_to_zero_across_paths() { + let manager = SessionManager::new(); + let uuid_multi = make_uuid(0xC0); + let uuid_single = make_uuid(0xC1); + + // Successful create path. + manager + .with_ta(&uuid_multi, |target| { + assert!(matches!(target, OpenSessionTarget::NewInstance)); + manager.register_new_session( + 301, + make_shim(), + make_loaded_program(TaFlags::default()), + 80, + uuid_multi, + ); + Ok(()) + }) + .unwrap(); + assert_eq!(*manager.pending_count.lock(), 0); + + // Failing create path on an unknown UUID. + let _ = manager.with_ta(&uuid_single, |_| Err(OpteeSmcReturnCode::ENotAvail)); + assert_eq!(*manager.pending_count.lock(), 0); + + // Cache-hit path doesn't touch pending_count. + register_for_test(&manager, 302, single_instance_flags(), 81, uuid_single); + manager.with_ta(&uuid_single, |_| Ok(())).unwrap(); + assert_eq!(*manager.pending_count.lock(), 0); + } + + /// After `with_ta` completes the unknown→per-UUID transition, the + /// per-UUID lock state must be released — a subsequent acquisition for + /// the same UUID must succeed. + #[test] + fn with_ta_releases_per_uuid_lock_after_unknown_load() { + let manager = SessionManager::new(); + let uuid = make_uuid(0xD0); + + manager + .with_ta(&uuid, |target| { + assert!(matches!(target, OpenSessionTarget::NewInstance)); + manager.register_new_session( + 401, + make_shim(), + make_loaded_program(single_instance_flags()), + 90, + uuid, + ); + Ok(()) + }) + .unwrap(); + + assert_eq!( + manager.single_instance_locks.lock().get(&uuid), + Some(&false) + ); + assert!(manager.try_acquire_uuid_lock(uuid).is_some()); + } + + /// A concurrent `with_ta` for an unrelated UUID must NOT adopt or + /// release the per-UUID lock held by another first-load opener. + /// Adoption is keyed by the `with_ta` call's own UUID, so an opener + /// for a different UUID leaves the original opener's per-UUID lock + /// untouched. + #[test] + fn unrelated_with_ta_does_not_adopt_other_uuids_lock() { + let manager = SessionManager::new(); + let uuid_locked = make_uuid(0xE1); + let uuid_other = make_uuid(0xE2); + + // Simulate the "lock pre-taken during a first-load" state. This + // mirrors what `register_new_session` does mid-first-load before + // `with_ta` adopts. + assert!(manager.try_acquire_uuid_lock(uuid_locked).is_some()); + + // A `with_ta` call for a completely different UUID must not touch + // `uuid_locked`'s lock. The closure registers nothing, but the + // post-`f` adoption logic still runs. + manager.with_ta(&uuid_other, |_| Ok(())).unwrap(); + + assert_eq!( + manager.single_instance_locks.lock().get(&uuid_locked), + Some(&true) + ); + } +} From 73f03be7e85aea020f3e186771f5f678a7607624 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 26 Jun 2026 12:24:12 -0700 Subject: [PATCH 071/319] Test broker-backed eventfd fd lifecycle (#974) Extends the broker-backed eventfd runner test with focused fd-lifecycle coverage for dup close ordering, dup2 replacement, shared O_NONBLOCK status, stale fd behavior, and final CloseObject release. The C test covers guest-visible behavior; the runner harness only counts CloseObject requests because broker-side object release is not observable from inside the guest. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_runner_linux_userland/tests/eventfd.c | 161 +++++++++++++++++- litebox_runner_linux_userland/tests/run.rs | 31 ++-- 2 files changed, 174 insertions(+), 18 deletions(-) diff --git a/litebox_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c index 5ed53b1943..39252b1671 100644 --- a/litebox_runner_linux_userland/tests/eventfd.c +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -2,6 +2,7 @@ // Licensed under the MIT license. #include +#include #include #include #include @@ -17,10 +18,27 @@ static int expect_eagain_read(int fd) { return errno == EAGAIN ? 0 : 2; } +static int expect_ebadf_read(int fd) { + uint64_t value = 0; + errno = 0; + if (read(fd, &value, sizeof(value)) != -1) { + return 1; + } + return errno == EBADF ? 0 : 2; +} + static int write_value(int fd, uint64_t value) { return write(fd, &value, sizeof(value)) == sizeof(value) ? 0 : 1; } +static int expect_ebadf_write(int fd, uint64_t value) { + errno = 0; + if (write(fd, &value, sizeof(value)) != -1) { + return 1; + } + return errno == EBADF ? 0 : 2; +} + static int read_value(int fd, uint64_t expected) { uint64_t value = 0; if (read(fd, &value, sizeof(value)) != sizeof(value)) { @@ -58,6 +76,26 @@ static int clear_nonblock_with_ioctl(int fd) { return ioctl(fd, FIONBIO, &nonblock) == 0 ? 0 : 1; } +static int expect_nonblock(int fd, int expected) { + int flags = fcntl(fd, F_GETFL); + if (flags < 0) { + return 1; + } + return ((flags & O_NONBLOCK) != 0) == expected ? 0 : 2; +} + +static int expect_ebadf_close(int fd) { + errno = 0; + if (close(fd) != -1) { + return 1; + } + return errno == EBADF ? 0 : 2; +} + +static int expect_close(int fd) { + return close(fd) == 0 ? 0 : 1; +} + int main(void) { int fd = eventfd(0, EFD_NONBLOCK); if (fd < 0) { @@ -125,7 +163,9 @@ int main(void) { if (expect_poll_events(fd, POLLOUT) != 0) { return 30; } - close(fd); + if (expect_close(fd) != 0) { + return 120; + } int ioctl_toggle_fd = eventfd(1, EFD_NONBLOCK); if (ioctl_toggle_fd < 0) { @@ -137,7 +177,9 @@ int main(void) { if (read_value(ioctl_toggle_fd, 1) != 0) { return 33; } - close(ioctl_toggle_fd); + if (expect_close(ioctl_toggle_fd) != 0) { + return 121; + } int semaphore_fd = eventfd(0, EFD_NONBLOCK | EFD_SEMAPHORE); if (semaphore_fd < 0) { @@ -170,7 +212,120 @@ int main(void) { if (expect_eagain_read(semaphore_fd) != 0) { return 49; } - close(semaphore_fd); + if (expect_close(semaphore_fd) != 0) { + return 122; + } + + int dup_source_fd = eventfd(0, EFD_NONBLOCK); + if (dup_source_fd < 0) { + return 60; + } + int dup_fd = dup(dup_source_fd); + if (dup_fd < 0) { + return 61; + } + if (write_value(dup_source_fd, 7) != 0) { + return 62; + } + if (read_value(dup_fd, 7) != 0) { + return 63; + } + if (expect_close(dup_source_fd) != 0) { + return 123; + } + if (expect_ebadf_write(dup_source_fd, 1) != 0) { + return 64; + } + if (write_value(dup_fd, 3) != 0) { + return 65; + } + if (read_value(dup_fd, 3) != 0) { + return 66; + } + if (expect_close(dup_fd) != 0) { + return 124; + } + if (expect_ebadf_close(dup_fd) != 0) { + return 67; + } + if (expect_ebadf_read(dup_fd) != 0) { + return 68; + } + + int close_original_fd = eventfd(0, EFD_NONBLOCK); + if (close_original_fd < 0) { + return 70; + } + int close_dup_fd = dup(close_original_fd); + if (close_dup_fd < 0) { + return 71; + } + if (expect_close(close_dup_fd) != 0) { + return 125; + } + if (write_value(close_original_fd, 5) != 0) { + return 72; + } + if (read_value(close_original_fd, 5) != 0) { + return 73; + } + if (expect_close(close_original_fd) != 0) { + return 126; + } + + int dup2_source_fd = eventfd(0, EFD_NONBLOCK); + int dup2_replaced_fd = eventfd(0, EFD_NONBLOCK); + if (dup2_source_fd < 0 || dup2_replaced_fd < 0) { + return 80; + } + if (dup2(dup2_source_fd, dup2_replaced_fd) != dup2_replaced_fd) { + return 81; + } + if (expect_close(dup2_source_fd) != 0) { + return 127; + } + if (write_value(dup2_replaced_fd, 11) != 0) { + return 82; + } + if (read_value(dup2_replaced_fd, 11) != 0) { + return 83; + } + if (expect_close(dup2_replaced_fd) != 0) { + return 128; + } + + int status_fd = eventfd(0, EFD_NONBLOCK); + if (status_fd < 0) { + return 100; + } + int status_dup_fd = dup(status_fd); + if (status_dup_fd < 0) { + return 101; + } + if (expect_nonblock(status_fd, 1) != 0 || expect_nonblock(status_dup_fd, 1) != 0) { + return 102; + } + if (fcntl(status_dup_fd, F_SETFL, 0) != 0) { + return 103; + } + if (expect_nonblock(status_fd, 0) != 0 || expect_nonblock(status_dup_fd, 0) != 0) { + return 104; + } + if (fcntl(status_fd, F_SETFL, O_NONBLOCK) != 0) { + return 105; + } + if (expect_nonblock(status_fd, 1) != 0 || expect_nonblock(status_dup_fd, 1) != 0) { + return 106; + } + if (expect_eagain_read(status_fd) != 0) { + return 107; + } + if (expect_close(status_fd) != 0) { + return 129; + } + if (expect_close(status_dup_fd) != 0) { + return 130; + } return 0; } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 4d30417de0..a5684a90a3 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -263,16 +263,16 @@ fn unique_test_socket_path(name: &str) -> PathBuf { struct TestBroker { thread: Option>, done_rx: std::sync::mpsc::Receiver<()>, - event_request_count_rx: std::sync::mpsc::Receiver, + close_object_count_rx: std::sync::mpsc::Receiver, socket_path: PathBuf, } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] impl TestBroker { - fn next_event_request_count(&self) -> usize { - self.event_request_count_rx + fn next_close_object_count(&self) -> usize { + self.close_object_count_rx .recv_timeout(BROKER_HELPER_TIMEOUT) - .expect("broker test host did not report event request count") + .expect("broker test host did not report close-object count") } fn join(mut self) { @@ -305,7 +305,7 @@ fn spawn_test_broker( let (ready_tx, ready_rx) = std::sync::mpsc::channel(); let (done_tx, done_rx) = std::sync::mpsc::channel(); - let (event_request_count_tx, event_request_count_rx) = std::sync::mpsc::channel(); + let (close_object_count_tx, close_object_count_rx) = std::sync::mpsc::channel(); let server_socket_path = socket_path.to_path_buf(); let cleanup_socket_path = socket_path.to_path_buf(); let broker_thread = std::thread::spawn(move || { @@ -328,7 +328,7 @@ fn spawn_test_broker( .expect("failed to configure broker test write timeout"); let mut channel = CountingHostControlChannel { inner: litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream), - event_request_count: 0, + close_object_count: 0, }; let termination = litebox_broker_host::serve_connection(&broker, &mut channel) .expect("broker host failed"); @@ -336,9 +336,9 @@ fn spawn_test_broker( termination, litebox_broker_host::ConnectionTermination::PeerClosed ); - event_request_count_tx - .send(channel.event_request_count) - .expect("failed to report broker event request count"); + close_object_count_tx + .send(channel.close_object_count) + .expect("failed to report broker close-object count"); } })); let _ = std::fs::remove_file(&server_socket_path); @@ -354,7 +354,7 @@ fn spawn_test_broker( TestBroker { thread: Some(broker_thread), done_rx, - event_request_count_rx, + close_object_count_rx, socket_path: cleanup_socket_path, } } @@ -362,7 +362,7 @@ fn spawn_test_broker( #[cfg(all(target_arch = "x86_64", target_os = "linux"))] struct CountingHostControlChannel { inner: Channel, - event_request_count: usize, + close_object_count: usize, } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] @@ -407,10 +407,10 @@ impl if matches!( &request, litebox_broker_protocol::channel::HostReceive::Message( - litebox_broker_protocol::message::BrokerRequest::Event(_) + litebox_broker_protocol::message::BrokerRequest::CloseObject(_) ) ) { - self.event_request_count += 1; + self.close_object_count += 1; } Ok(request) } @@ -440,12 +440,13 @@ fn test_runner_broker_integration_with_rewriter() { Runner::new(&true_path, "broker_true_rewriter") .broker_socket(&socket_path) .run(); - assert_eq!(broker_thread.next_event_request_count(), 0); + assert_eq!(broker_thread.next_close_object_count(), 0); Runner::new(&target, "broker_eventfd_rewriter") .broker_socket(&socket_path) .run(); - assert!(broker_thread.next_event_request_count() > 0); + // eventfd.c creates eight eventfd objects; each should release one broker object. + assert_eq!(broker_thread.next_close_object_count(), 8); broker_thread.join(); } From 6695258307e6a2e07371ec1ca1b0c82771609ecb Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Mon, 29 Jun 2026 14:49:23 -0700 Subject: [PATCH 072/319] Cherry pick "Fix cross-page-table sharing of mutable mappings (LVBS platform)" (#982) Co-authored-by: Sangho Lee --- .../src/arch/x86/mm/paging.rs | 77 +++++++++++-------- litebox_platform_lvbs/src/lib.rs | 19 +++-- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs index 82edf68248..4e2866e85d 100644 --- a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs +++ b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs @@ -35,6 +35,29 @@ use crate::mm::{ #[cfg(not(test))] const TLB_SINGLE_PAGE_FLUSH_CEILING: usize = 33; +/// Bit position of the PML4 (level-4) index within a virtual address, for +/// x86-64 4-level paging: 12 page-offset bits + 9 bits each for P1-P3. +const PML4_SHIFT: u32 = 39; + +/// Mask for a 9-bit page-table index (512 entries per table). +const PML4_INDEX_MASK: u64 = 0x1FF; + +/// Number of bytes of virtual address space covered by one PML4 slot (512 GiB). +const PML4_SLOT_SIZE: u64 = 1 << PML4_SHIFT; + +/// PML4 index of the first VTL1-kernel slot (`PA + KERNEL_OFFSET`). +/// +/// Only slots `>= KERNEL_PML4_START` are safe to share between page tables: +/// their intermediate tables (P3/P2/P1) are fixed after boot, so sharing them +/// is read-only. Lower slots (user, direct-map, vmap) get intermediate tables +/// allocated and freed at runtime on whichever page table is active. Sharing +/// those would let a task mutate the base's intermediate tables (and make +/// frame ownership ambiguous at teardown), so each page table must own them. +/// +/// `KERNEL_OFFSET` is `PML4_SLOT_SIZE` aligned, so this is an exact cutoff. +pub(crate) const KERNEL_PML4_START: usize = + ((crate::KERNEL_OFFSET >> PML4_SHIFT) & PML4_INDEX_MASK) as usize; + /// Flush TLB entries for a contiguous page range across all cores. /// /// Uses Hyper-V hypercalls so that remote cores sharing the same page table @@ -271,8 +294,8 @@ impl X64PageTable<'_, M, ALIGN> { Ok(()) } - /// Clean up intermediate page table frames (P1-P3) for a task page table - /// that is being destroyed. + /// Clean up task-owned intermediate page table frames (P1-P3) for a task + /// page table that is being destroyed. /// /// # Safety /// @@ -280,13 +303,19 @@ impl X64PageTable<'_, M, ALIGN> { /// - All user data frames have been released before calling this function (e.g., using `PageManager::release_memory()`) /// - The page table is no longer active (not loaded in CR3) pub(crate) unsafe fn cleanup_page_table_frames(&self) { - use x86_64::structures::paging::mapper::CleanUp; - - // Clean up all empty P1 - P3 tables let mut allocator = PageTableAllocator::::new(); + // Task-owned slots span the VA below the kernel region, i.e., + // `0 ..= KERNEL_PML4_START * PML4_SLOT_SIZE - 1`. The kernel region at + // and above `KERNEL_PML4_START` is base-owned/shared. + let start = Page::::from_start_address(VirtAddr::new(0)).unwrap(); + let end = Page::::containing_address(VirtAddr::new( + KERNEL_PML4_START as u64 * PML4_SLOT_SIZE - 1, + )); // Safety: The page table is being destroyed and will not be reused. unsafe { - self.inner.lock().clean_up(&mut allocator); + self.inner + .lock() + .clean_up_addr_range(Page::range_inclusive(start, end), &mut allocator); } } @@ -612,7 +641,8 @@ impl X64PageTable<'_, M, ALIGN> { } else { flags }; - let table_flags = page_flags - PageTableFlags::NO_EXECUTE; // parent entries should not have NO_EXECUTE + // Parent entries use a stable permissive constant, not leaf-derived flags. + let table_flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE; match unsafe { inner.map_to_with_table_flags( @@ -755,14 +785,12 @@ impl X64PageTable<'_, M, ALIGN> { unsafe { Self::init(frame.start_address()) } } - /// Copy all non-zero PML4 entries from `source` into this page table. - /// - /// This is used to share kernel page table structures (P3/P2/P1) between - /// the base page table and task page tables, avoiding per-task allocation - /// of intermediate page table frames for the kernel region. + /// Share the VTL1-kernel P3/P2/P1 tables from `source` by copying its + /// kernel PML4 entries (slots `>= KERNEL_PML4_START`), avoiding per-task + /// allocation of the kernel intermediate frames. Lower slots (user, + /// direct-map, vmap) are deliberately not shared; see [`KERNEL_PML4_START`]. /// - /// Only entries that are present in `source` and absent in `self` are copied. - /// Entries already present in `self` are left unchanged. + /// Only entries present in `source` and absent in `self` are copied. pub(crate) fn copy_pml4_entries_from(&self, source: &Self) { let mut dst = self.inner.lock(); let src = source.inner.lock(); @@ -770,6 +798,7 @@ impl X64PageTable<'_, M, ALIGN> { .level_4_table_mut() .iter_mut() .zip(src.level_4_table().iter()) + .skip(KERNEL_PML4_START) { if !src_entry.is_unused() && dst_entry.is_unused() { dst_entry.set_addr(src_entry.addr(), src_entry.flags()); @@ -777,26 +806,6 @@ impl X64PageTable<'_, M, ALIGN> { } } - /// Clear PML4 entries that are shared with the base page table. - /// - /// This must be called before `cleanup_page_table_frames` / `drop` to - /// prevent the task page table from freeing P3/P2/P1 frames that are - /// owned by the base page table. - pub(crate) fn clear_shared_pml4_entries(&self, base: &Self) { - let mut dst = self.inner.lock(); - let src = base.inner.lock(); - for (dst_entry, src_entry) in dst - .level_4_table_mut() - .iter_mut() - .zip(src.level_4_table().iter()) - { - // If the entry points to the same P3 frame as the base, it is shared. - if !src_entry.is_unused() && dst_entry.addr() == src_entry.addr() { - dst_entry.set_unused(); - } - } - } - /// This function changes the address space of the current processor/core using the given page table /// (e.g., its CR3 register) and returns the physical frame of the previous top-level page table. /// It preserves the CR3 flags. diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index e41f43ea00..adcee05b7f 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -328,8 +328,9 @@ impl PageTableManager { pub fn create_task_page_table(&self) -> Result { let pt = unsafe { mm::PageTable::new_top_level() }; - // Share kernel page table structures by copying PML4 entries from the - // base page table. This is safe because kernel mappings are never modified. + // Share the base page table's kernel intermediate tables (kernel PML4 + // slots only). This is safe because the kernel mapping structure is + // fixed after boot; lower slots are not shared (see `copy_pml4_entries_from`). pt.copy_pml4_entries_from(&self.base_page_table); let pt = alloc::boxed::Box::new(pt); @@ -380,13 +381,11 @@ impl PageTableManager { if let Some(pt) = task_pts.remove(&task_pt_id) { drop(task_pts); - // Clear PML4 entries that point to the base page table's P3/P2/P1 - // frames. Without this, cleanup_page_table_frames and Drop would - // free page table frames owned by the base page table. - pt.clear_shared_pml4_entries(&self.base_page_table); - // Safety: We're about to delete this page table, so it's safe to - // free the remaining (user-space) intermediate page table frames. + // free the task-owned intermediate page table frames (user, + // direct-map, and vmap slots). The kernel slots are shared with the + // base page table and are deliberately left untouched, so its + // P3/P2/P1 frames are not freed. unsafe { pt.cleanup_page_table_frames(); } @@ -763,8 +762,8 @@ impl LinuxKernel { let mut boxed = box_new_zeroed::(); // Use memcpy_fallible instead of ptr::copy_nonoverlapping to handle - // the race where another core unmaps this page (via a shared page - // table) between map_vtl0_guard and the copy. The mapping is valid + // the race where another core running on the same page table unmaps + // this page between map_vtl0_guard and the copy. The mapping is valid // at this point, so a fault is not expected in the common case. // TODO: Once VTL0 page-range locking is in place, this fallible copy // may become unnecessary since the lock would prevent concurrent From b0552d430a6c753cbb1f6ed978be14f022a81733 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Mon, 29 Jun 2026 15:01:42 -0700 Subject: [PATCH 073/319] Cherry pick "Fix: Serialize packed-`OpteeMsgArgs` access" (#983) Co-authored-by: Sangho Lee --- dev_tests/src/ratchet.rs | 2 +- litebox_runner_lvbs/src/lib.rs | 5 +++- litebox_shim_optee/src/msg_handler.rs | 38 ++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 09ff7ad2c8..bf83c20de2 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -44,7 +44,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_runner_lvbs/", 6), ("litebox_runner_snp/", 2), ("litebox_shim_linux/", 1), - ("litebox_shim_optee/", 3), + ("litebox_shim_optee/", 4), ("litebox_shim_windows/", 1), ], |file| { diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index 893afe57fa..6db84928a2 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -39,7 +39,8 @@ use litebox_platform_lvbs::{ }; use litebox_platform_multiplex::Platform; use litebox_shim_optee::msg_handler::{ - decode_ta_request, handle_optee_msg_args, handle_optee_smc_args, update_optee_msg_args, + decode_ta_request, handle_optee_msg_args, handle_optee_smc_args, packed_msg_args_lock, + update_optee_msg_args, }; use litebox_shim_optee::session::{OpenSessionTarget, SessionManager, TaInstance}; use litebox_shim_optee::{NormalWorldConstPtr, NormalWorldMutPtr, UserConstPtr}; @@ -1246,6 +1247,8 @@ fn write_non_ta_msg_args_to_normal_world( )?; // SAFETY: Writing msg_args back to normal world memory at a valid physical address. // The blob contains the serialized variable-length optee_msg_arg structure(s). + // Serialize the packed-page write. See `packed_msg_args_lock`. + let _packed_guard = packed_msg_args_lock(); unsafe { ptr.write_slice_at_offset(0, &blob) }?; Ok(()) } diff --git a/litebox_shim_optee/src/msg_handler.rs b/litebox_shim_optee/src/msg_handler.rs index 9187d5b7fc..024df141a9 100644 --- a/litebox_shim_optee/src/msg_handler.rs +++ b/litebox_shim_optee/src/msg_handler.rs @@ -207,7 +207,12 @@ pub fn handle_optee_smc_args( OpteeSmcFunction::CallWithArg => { let msg_args_addr = smc.optee_msg_args_phys_addr()?; let msg_args_addr: usize = msg_args_addr.trunc(); - let (msg_args, _) = read_optee_msg_args_from_phys(msg_args_addr, false)?; + // Serialize the packed-page read against a concurrent write-back. See + // `packed_msg_args_lock`. + let (msg_args, _) = { + let _packed_guard = packed_msg_args_lock(); + read_optee_msg_args_from_phys(msg_args_addr, false)? + }; Ok(OpteeSmcResult::CallWithArg { msg_args, rpc_args: None, @@ -217,7 +222,10 @@ pub fn handle_optee_smc_args( OpteeSmcFunction::CallWithRpcArg => { let msg_args_addr = smc.optee_msg_args_phys_addr()?; let msg_args_addr: usize = msg_args_addr.trunc(); - let (msg_args, rpc_args) = read_optee_msg_args_from_phys(msg_args_addr, true)?; + let (msg_args, rpc_args) = { + let _packed_guard = packed_msg_args_lock(); + read_optee_msg_args_from_phys(msg_args_addr, true)? + }; Ok(OpteeSmcResult::CallWithArg { msg_args, rpc_args, @@ -237,7 +245,12 @@ pub fn handle_optee_smc_args( main_max + optee_msg_args_total_size(OpteeRpcArgs::MAX_RPC_ARG_PARAM_COUNT.trunc()); let mut blob = alloc::vec![0u8; copy_size]; - shm_info.read_at(offset, &mut blob)?; + // Serialize the packed-page read against a concurrent write-back. See + // `packed_msg_args_lock`. + { + let _packed_guard = packed_msg_args_lock(); + shm_info.read_at(offset, &mut blob)?; + } let (msg_args, rpc_args) = parse_optee_msg_args(&blob, true)?; // Compute the physical address of `OpteeMsgArgs` @@ -383,6 +396,25 @@ pub struct TaRequestInfo { pub out_shm_info: [Option>; UteeParamOwned::TEE_NUM_PARAMS], } +/// Acquire the lock serializing packed-`OpteeMsgArgs` page access on the base page table. +/// +/// The OP-TEE driver packs multiple requests into sub-page slots of one frame which can be +/// concurrently access by multiple cores which are on the base page table. Since LiteBox +/// currently doesn't support shared mapping, it uses this lock to serialize the concurrent +/// access. Note that cores on different task page tables (i.e., instances) do not need to +/// acquire this lock since they maintain their own mappings. +/// +/// Hold the guard only across the packed-page read/write. +/// +/// TODO: This is a temporary mitigation. It should be replaced by a more fundamental +/// approach such as shared mapping support, physical address range reservation, and/or +/// sub-page access control. +#[must_use] +pub fn packed_msg_args_lock() -> spin::mutex::SpinMutexGuard<'static, ()> { + static PACKED_MSG_ARGS_LOCK: spin::mutex::SpinMutex<()> = spin::mutex::SpinMutex::new(()); + PACKED_MSG_ARGS_LOCK.lock() +} + /// This function decodes a TA request contained in `OpteeMsgArgs`. /// /// It copies the entire parameter data from the normal world shared memory into the secure world's From e0524d95001862b636d969bfd9d8e8c03ee35f95 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Mon, 29 Jun 2026 18:07:41 -0700 Subject: [PATCH 074/319] Windows NT Object Manager: directory + symbolic-link object support (#981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR brings up the Windows shim’s NT Object Manager directory and symbolic-link subset, including directory create/open/query/enumeration, symbolic-link create/open/query, object-namespace traversal through symbolic links. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_common_windows/src/nt_status.rs | 10 + litebox_shim_windows/src/lib.rs | 207 +- litebox_shim_windows/src/nt_types.rs | 44 + .../src/syscalls/directory.rs | 1973 +++++++++++++++++ litebox_shim_windows/src/syscalls/event.rs | 105 +- litebox_shim_windows/src/syscalls/file.rs | 68 +- litebox_shim_windows/src/syscalls/iocp.rs | 52 +- litebox_shim_windows/src/syscalls/mod.rs | 86 + litebox_shim_windows/src/syscalls/registry.rs | 64 +- litebox_shim_windows/src/syscalls/symlink.rs | 876 ++++++++ litebox_shim_windows/src/syscalls/timer.rs | 50 +- .../src/syscalls/wait_completion_packet.rs | 52 +- .../src/syscalls/worker_factory.rs | 52 +- litebox_shim_windows/src/tests.rs | 13 +- 14 files changed, 3339 insertions(+), 313 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/directory.rs create mode 100644 litebox_shim_windows/src/syscalls/symlink.rs diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index 22e33592f7..439cd7fe63 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -88,6 +88,7 @@ impl NtStatus { } 0x00000102 => "STATUS_TIMEOUT: The given timeout interval expired", 0x00000103 => "STATUS_PENDING: The operation that was requested is pending completion", + 0x00000105 => "STATUS_MORE_ENTRIES: More entries are available", 0x00010001 => "DBG_EXCEPTION_HANDLED: Exception handled by debugger", 0x00010002 => "DBG_CONTINUE: Continue from exception", 0x40000000 => "STATUS_OBJECT_NAME_EXISTS: The object name already exists", @@ -96,6 +97,7 @@ impl NtStatus { 0x80000003 => "STATUS_BREAKPOINT: Breakpoint encountered", 0x80000004 => "STATUS_SINGLE_STEP: Single instruction executed", 0x80000005 => "STATUS_BUFFER_OVERFLOW: Buffer overflow", + 0x8000001A => "STATUS_NO_MORE_ENTRIES: No more entries are available", 0xC0000001 => "STATUS_UNSUCCESSFUL: The operation completed with an error", 0xC0000002 => "STATUS_NOT_IMPLEMENTED: The function is not implemented", 0xC0000003 => "STATUS_INVALID_INFO_CLASS: Invalid information class", @@ -143,6 +145,7 @@ impl NtStatus { 0xC0000037 => "STATUS_PORT_DISCONNECTED: Port disconnected", 0xC0000039 => "STATUS_OBJECT_PATH_INVALID: Object path invalid", 0xC000003A => "STATUS_OBJECT_PATH_NOT_FOUND: Object path not found", + 0xC000003B => "STATUS_OBJECT_PATH_SYNTAX_BAD: Object path syntax is invalid", 0xC000003C => "STATUS_DATA_OVERRUN: Data overrun", 0xC000003D => "STATUS_DATA_LATE_ERROR: Data late error", 0xC000003E => "STATUS_DATA_ERROR: Data error", @@ -230,6 +233,9 @@ impl NtStatus { /// STATUS_TIMEOUT pub const TIMEOUT: Self = Self::from_raw(0x00000102); + /// STATUS_MORE_ENTRIES + pub const MORE_ENTRIES: Self = Self::from_raw(0x00000105); + /// DBG_EXCEPTION_HANDLED pub const EXCEPTION_HANDLED: Self = Self::from_raw(0x00010001); @@ -253,6 +259,8 @@ impl NtStatus { /// STATUS_BUFFER_OVERFLOW pub const BUFFER_OVERFLOW: Self = Self::from_raw(0x80000005); + /// STATUS_NO_MORE_ENTRIES + pub const NO_MORE_ENTRIES: Self = Self::from_raw(0x8000001A); /// STATUS_UNSUCCESSFUL pub const UNSUCCESSFUL: Self = Self::from_raw(0xC0000001); @@ -394,6 +402,8 @@ impl NtStatus { /// STATUS_OBJECT_PATH_NOT_FOUND pub const OBJECT_PATH_NOT_FOUND: Self = Self::from_raw(0xC000003A); + /// STATUS_OBJECT_PATH_SYNTAX_BAD + pub const OBJECT_PATH_SYNTAX_BAD: Self = Self::from_raw(0xC000003B); /// STATUS_DATA_OVERRUN pub const DATA_OVERRUN: Self = Self::from_raw(0xC000003C); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 54a45a50ff..5ee53e4f92 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -31,10 +31,14 @@ use litebox::sync::RawSyncPrimitivesProvider; use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; +use crate::syscalls::directory::{ + DirectoryHandleObject, DirectoryNamespace, DirectoryObjectSubsystem, +}; use crate::syscalls::event::{EventHandleObject, EventObject, EventSubsystem}; use crate::syscalls::file::{FileObject, FileObjectSubsystem}; use crate::syscalls::iocp::{IoCompletionHandleObject, IoCompletionSubsystem}; use crate::syscalls::registry::{RegistryKeyObject, RegistryKeySubsystem}; +use crate::syscalls::symlink::{SymbolicLinkHandleObject, SymbolicLinkSubsystem}; use crate::syscalls::timer::{TimerCreateParameters, TimerHandleObject, TimerSubsystem}; use crate::syscalls::wait_completion_packet::{ WaitCompletionPacketAssociateParameters, WaitCompletionPacketHandleObject, @@ -88,6 +92,7 @@ pub(crate) type WindowsVirtualAllocations = litebox::sync::RwLock>; pub(crate) type WindowsEventNamespace = litebox::sync::RwLock>>>; +pub(crate) type WindowsDirectoryNamespace = DirectoryNamespace; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct WindowsVirtualAllocation { @@ -188,7 +193,7 @@ where true } -pub(crate) fn insert_raw_handle( +fn insert_raw_handle( litebox: &LiteBox, handles: &WindowsHandleStore, typed: litebox::fd::TypedFd, @@ -230,7 +235,7 @@ where litebox.descriptor_table().entry_handle(&typed) } -pub(crate) fn remove_raw_handle( +fn remove_raw_handle( litebox: &LiteBox, handles: &WindowsHandleStore, handle: syscalls::Handle, @@ -245,7 +250,7 @@ pub(crate) fn remove_raw_handle(litebox, handles, raw_fd, cleanup_entry); } -pub(crate) fn remove_raw_handle_by_raw_fd( +fn remove_raw_handle_by_raw_fd( litebox: &LiteBox, handles: &WindowsHandleStore, raw_fd: usize, @@ -331,10 +336,12 @@ impl WindowsShim { ) -> Result, loader::WindowsLoadError> { let load_info = loader::PeLoader::new(self.0.platform, fs.clone(), &self.0.page_manager) .load(path, &argv, &envp)?; + let directory_namespace = syscalls::directory::seed_directory_namespace(); let process = Arc::new(Process { ntdll_mapping: load_info.ntdll_mapping, peb_address: load_info.environment.peb, handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), + directory_namespace, event_namespace: WindowsEventNamespace::::new(BTreeMap::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), virtual_allocations: load_info.virtual_allocations, @@ -378,6 +385,7 @@ pub struct Process { ntdll_mapping: Option, peb_address: usize, handles: WindowsHandleStore, + directory_namespace: WindowsDirectoryNamespace, event_namespace: WindowsEventNamespace, nls_section_mappings: WindowsNlsSectionMappings, virtual_allocations: WindowsVirtualAllocations, @@ -442,6 +450,69 @@ impl Task { ContinueOperation::Resume } + fn typed_handle_entry( + &self, + handle: syscalls::Handle, + ) -> Result, NtStatus> + where + Subsystem: litebox::fd::FdEnabledSubsystem, + { + let Some(raw_fd) = handle.raw_fd() else { + return Err(NtStatus::INVALID_HANDLE); + }; + let typed = { + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::(raw_fd) { + Ok(typed) => typed, + Err(litebox::fd::ErrRawIntFd::NotFound) => return Err(NtStatus::INVALID_HANDLE), + Err(litebox::fd::ErrRawIntFd::InvalidSubsystem) => { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + } + } + }; + self.global + .litebox + .descriptor_table() + .entry_handle(&typed) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn insert_typed_handle( + &self, + entry: Subsystem::Entry, + cleanup_entry: impl FnOnce(Subsystem::Entry), + ) -> Result + where + Subsystem: litebox::fd::FdEnabledSubsystem, + { + let typed = self + .global + .litebox + .descriptor_table_mut() + .insert::(entry); + insert_raw_handle::( + &self.global.litebox, + &self.process.handles, + typed, + cleanup_entry, + ) + } + + fn close_typed_handle( + &self, + handle: syscalls::Handle, + cleanup_entry: impl FnOnce(Subsystem::Entry), + ) where + Subsystem: litebox::fd::FdEnabledSubsystem, + { + remove_raw_handle::( + &self.global.litebox, + &self.process.handles, + handle, + cleanup_entry, + ); + } + fn handle_syscall_request(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { let Some(req) = SyscallRequest::::try_from_raw(ctx) else { litebox_util_log::debug!( @@ -475,6 +546,108 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtCreateDirectoryObject { + directory_handle, + desired_access, + object_attributes, + } => { + let status = self.sys_nt_create_directory_object( + directory_handle, + desired_access, + object_attributes, + syscalls::Handle::default(), + 0, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateDirectoryObjectEx { + directory_handle, + desired_access, + object_attributes, + shadow_directory_handle, + flags, + } => { + let status = self.sys_nt_create_directory_object( + directory_handle, + desired_access, + object_attributes, + shadow_directory_handle, + flags, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtOpenDirectoryObject { + directory_handle, + desired_access, + object_attributes, + } => { + let status = self.sys_nt_open_directory_object( + directory_handle, + desired_access, + object_attributes, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryDirectoryObject { + directory_handle, + buffer, + buffer_length, + return_single_entry, + restart_scan, + context, + return_length, + } => { + let status = self.sys_nt_query_directory_object( + syscalls::directory::DirectoryQueryParameters { + directory_handle, + buffer, + buffer_length, + return_single_entry, + restart_scan, + context, + return_length, + }, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateSymbolicLinkObject { + link_handle, + desired_access, + object_attributes, + link_target, + } => { + let status = self.sys_nt_create_symbolic_link_object( + link_handle, + desired_access, + object_attributes, + link_target, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtOpenSymbolicLinkObject { + link_handle, + desired_access, + object_attributes, + } => { + let status = self.sys_nt_open_symbolic_link_object( + link_handle, + desired_access, + object_attributes, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQuerySymbolicLinkObject { + link_handle, + link_target, + returned_length, + } => { + let status = self.sys_nt_query_symbolic_link_object( + link_handle, + link_target, + returned_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtCreateIoCompletion { io_completion_handle, desired_access, @@ -1011,6 +1184,22 @@ impl Task { ) { return NtStatus::SUCCESS; } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |directory| visitor.directory(directory), + ) { + return NtStatus::SUCCESS; + } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |link| visitor.symbolic_link(link), + ) { + return NtStatus::SUCCESS; + } if remove_raw_handle_by_raw_fd::>( &self.global.litebox, &self.process.handles, @@ -1065,6 +1254,10 @@ trait RawHandleVisitor { fn event(&self, event: EventHandleObject); + fn directory(&self, directory: DirectoryHandleObject); + + fn symbolic_link(&self, link: SymbolicLinkHandleObject); + fn io_completion(&self, io_completion: IoCompletionHandleObject); fn timer(&self, timer: TimerHandleObject); @@ -1096,6 +1289,14 @@ impl RawHandleVisitor Task::::close_event(event); } + fn directory(&self, directory: DirectoryHandleObject) { + Task::::close_directory(directory); + } + + fn symbolic_link(&self, link: SymbolicLinkHandleObject) { + Task::::close_symbolic_link(link); + } + fn io_completion(&self, io_completion: IoCompletionHandleObject) { Task::::close_io_completion(io_completion); } diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index b1b135b0d4..9438e3b9a3 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -161,6 +161,47 @@ bitflags::bitflags! { } } +bitflags::bitflags! { + /// Flags carried in `OBJECT_ATTRIBUTES.Attributes`. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct ObjectAttributesFlags: u32 { + const CASE_INSENSITIVE = 0x0000_0040; + const OPENIF = 0x0000_0080; + const OPENLINK = 0x0000_0100; + + const _ = !0; + } +} + +impl AccessMask { + pub(crate) fn expand_generic_access( + desired_access: u32, + generic_read: u32, + generic_write: u32, + generic_execute: u32, + generic_all: u32, + ) -> u32 { + let mut access = desired_access; + if desired_access & Self::GENERIC_READ.bits() != 0 { + access |= generic_read; + } + if desired_access & Self::GENERIC_WRITE.bits() != 0 { + access |= generic_write; + } + if desired_access & Self::GENERIC_EXECUTE.bits() != 0 { + access |= generic_execute; + } + if desired_access & Self::GENERIC_ALL.bits() != 0 { + access |= generic_all; + } + access + & !(Self::GENERIC_READ.bits() + | Self::GENERIC_WRITE.bits() + | Self::GENERIC_EXECUTE.bits() + | Self::GENERIC_ALL.bits()) + } +} + #[repr(C)] #[derive(Clone, Copy, Debug, FromBytes, Immutable)] pub(crate) struct ObjectAttributes { @@ -218,6 +259,9 @@ impl UnicodeString { if !self.length.is_multiple_of(2) { return Err(NtStatus::INVALID_PARAMETER); } + if self.maximum_length < self.length { + return Err(NtStatus::INVALID_PARAMETER); + } if self.length == 0 { return Ok(String::new()); } diff --git a/litebox_shim_windows/src/syscalls/directory.rs b/litebox_shim_windows/src/syscalls/directory.rs new file mode 100644 index 0000000000..c0eaafd651 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/directory.rs @@ -0,0 +1,1973 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NT object-manager directory syscalls. + +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString as _}; +use alloc::sync::{Arc, Weak}; +use alloc::vec::Vec; +use core::cmp::Ordering; +use core::hash::{Hash, Hasher}; +use core::marker::PhantomData; +use core::mem::size_of; + +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; +use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::nt_types::{ + AccessMask, ObjectAttributes, ObjectAttributesFlags, UnicodeString, read_object_attributes, +}; +use crate::syscalls::Handle; +use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; + +const MAX_SYMLINK_REPARSE_DEPTH: usize = 64; +const STANDARD_RIGHTS_REQUIRED: u32 = AccessMask::DELETE.bits() + | AccessMask::READ_CONTROL.bits() + | AccessMask::WRITE_DAC.bits() + | AccessMask::WRITE_OWNER.bits(); + +// Wine's server seeds these object-manager directories during init_directories/create_session; +// ReactOS initializes the same root-style namespace through ObpRootDirectoryObject. +const SEEDED_DIRECTORY_PATHS: &[&str] = &[ + r"\", + r"\??", + r"\BaseNamedObjects", + r"\Device", + r"\Driver", + r"\KnownDlls", + r"\KernelObjects", + r"\NLS", + r"\ObjectTypes", + r"\Sessions", + r"\Sessions\0", + r"\Sessions\0\BaseNamedObjects", + r"\Sessions\0\DosDevices", + r"\Sessions\0\Windows", + r"\Sessions\0\Windows\WindowStations", + r"\Sessions\BNOLINKS", +]; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct DirectoryAccess: u32 { + const QUERY = 0x0001; + const TRAVERSE = 0x0002; + const CREATE_OBJECT = 0x0004; + const CREATE_SUBDIRECTORY = 0x0008; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() + | Self::QUERY.bits() + | Self::TRAVERSE.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits() + | Self::CREATE_OBJECT.bits() + | Self::CREATE_SUBDIRECTORY.bits(); + const EXECUTE = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() + | Self::QUERY.bits() + | Self::TRAVERSE.bits(); + const ALL_ACCESS = STANDARD_RIGHTS_REQUIRED + | Self::QUERY.bits() + | Self::TRAVERSE.bits() + | Self::CREATE_OBJECT.bits() + | Self::CREATE_SUBDIRECTORY.bits(); + + const _ = !0; + } +} + +impl DirectoryAccess { + fn from_desired_access(desired_access: u32) -> Self { + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + )) + } + + fn require(self, required: Self) -> Result<(), NtStatus> { + if self.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +pub(crate) struct DirectoryObjectSubsystem(PhantomData); + +impl FdEnabledSubsystem for DirectoryObjectSubsystem { + type Entry = DirectoryHandleObject; +} + +impl FdEnabledSubsystemEntry for DirectoryHandleObject {} + +pub(crate) struct DirectoryHandleObject { + directory: Arc>, + granted_access: DirectoryAccess, +} + +pub(super) struct ObjectNode { + path: String, + name: String, + parent: Option>>, + body: litebox::sync::RwLock>, + _not_send_without_platform: PhantomData, +} + +pub(crate) struct DirectoryNamespace { + root: Arc>, +} + +enum NamedObject { + Directory { + children: BTreeMap>>, + }, + Symlink { + target: String, + }, +} + +#[derive(Clone, Debug)] +struct ObjectName(String); + +impl ObjectName { + // ReactOS and Wine keep the creator's object name but compare through a + // case-insensitive object-manager lookup key. + fn new(name: &str) -> Self { + Self(name.to_string()) + } +} + +impl PartialEq for ObjectName { + fn eq(&self, other: &Self) -> bool { + self.0.eq_ignore_ascii_case(&other.0) + } +} + +impl Eq for ObjectName {} + +impl PartialOrd for ObjectName { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ObjectName { + fn cmp(&self, other: &Self) -> Ordering { + self.0 + .bytes() + .map(|byte| byte.to_ascii_lowercase()) + .cmp(other.0.bytes().map(|byte| byte.to_ascii_lowercase())) + } +} + +impl Hash for ObjectName { + fn hash(&self, state: &mut H) { + for byte in self.0.bytes() { + byte.to_ascii_lowercase().hash(state); + } + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +struct ObjectDirectoryInformation { + name: UnicodeString, + type_name: UnicodeString, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct DirectoryEntrySnapshot { + name: String, + type_name: &'static str, +} + +impl ObjectDirectoryInformation { + const fn new(name: UnicodeString, type_name: UnicodeString) -> Self { + Self { name, type_name } + } + + const fn zero() -> Self { + Self { + name: UnicodeString { + length: 0, + maximum_length: 0, + padding_0: [0; 4], + buffer: 0, + }, + type_name: UnicodeString { + length: 0, + maximum_length: 0, + padding_0: [0; 4], + buffer: 0, + }, + } + } +} + +pub(crate) struct DirectoryQueryParameters { + pub(crate) directory_handle: Handle, + pub(crate) buffer: MutPtr, + pub(crate) buffer_length: u32, + pub(crate) return_single_entry: u8, + pub(crate) restart_scan: u8, + pub(crate) context: MutPtr, + pub(crate) return_length: Option>, +} + +impl ObjectNode { + fn new_directory( + path: String, + parent: Option>>, + name: String, + ) -> Self { + Self { + path, + name, + parent, + body: litebox::sync::RwLock::::new(NamedObject::Directory { + children: BTreeMap::new(), + }), + _not_send_without_platform: PhantomData, + } + } + + fn new_symlink( + path: String, + parent: Option>>, + name: String, + target: String, + ) -> Self { + Self { + path, + name, + parent, + body: litebox::sync::RwLock::::new(NamedObject::Symlink { target }), + _not_send_without_platform: PhantomData, + } + } + + fn child(&self, name: &str) -> Option> { + match &*self.body.read() { + NamedObject::Directory { children } => children.get(&ObjectName::new(name)).cloned(), + NamedObject::Symlink { .. } => None, + } + } + + fn children_snapshot(&self) -> Result, NtStatus> { + match &*self.body.read() { + NamedObject::Directory { children } => Ok(children + .values() + .map(|child| DirectoryEntrySnapshot { + name: child.name.clone(), + type_name: child.type_name(), + }) + .collect()), + NamedObject::Symlink { .. } => Err(NtStatus::OBJECT_TYPE_MISMATCH), + } + } + + pub(super) fn is_directory(&self) -> bool { + matches!(&*self.body.read(), NamedObject::Directory { .. }) + } + + pub(super) fn is_symlink(&self) -> bool { + matches!(&*self.body.read(), NamedObject::Symlink { .. }) + } + + pub(super) fn symlink_target(&self) -> Result { + match &*self.body.read() { + NamedObject::Symlink { target } => Ok(target.clone()), + NamedObject::Directory { .. } => Err(NtStatus::OBJECT_TYPE_MISMATCH), + } + } + + fn type_name(&self) -> &'static str { + match &*self.body.read() { + NamedObject::Directory { .. } => "Directory", + NamedObject::Symlink { .. } => "SymbolicLink", + } + } + + fn parent(&self) -> Option> { + self.parent.as_ref().and_then(Weak::upgrade) + } +} + +impl DirectoryNamespace { + fn new() -> Self { + Self { + root: Arc::new(ObjectNode::new_directory( + r"\".to_string(), + None, + String::new(), + )), + } + } + + fn resolve_directory(&self, path: &str) -> Result>, NtStatus> { + let tail = absolute_path_tail(path)?; + let node = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, false)?; + if node.is_directory() { + Ok(node) + } else { + Err(NtStatus::OBJECT_TYPE_MISMATCH) + } + } + + fn create_directory( + &self, + path: &str, + on_exists: impl FnOnce(Arc>) -> NtStatus, + on_created: impl FnOnce(Arc>) -> NtStatus, + ) -> NtStatus { + self.create_child( + path, + ObjectNode::is_directory, + ObjectNode::new_directory, + on_exists, + on_created, + ) + } + + pub(super) fn create_symlink( + &self, + path: &str, + target: String, + on_exists: impl FnOnce(Arc>) -> NtStatus, + on_created: impl FnOnce(Arc>) -> NtStatus, + ) -> NtStatus { + self.create_child( + path, + ObjectNode::is_symlink, + |path, parent, name| ObjectNode::new_symlink(path, parent, name, target), + on_exists, + on_created, + ) + } + + fn create_child( + &self, + path: &str, + existing_matches: impl Fn(&ObjectNode) -> bool, + construct: impl FnOnce( + String, + Option>>, + String, + ) -> ObjectNode, + on_exists: impl FnOnce(Arc>) -> NtStatus, + on_created: impl FnOnce(Arc>) -> NtStatus, + ) -> NtStatus { + let tail = match absolute_path_tail(path) { + Ok(tail) => tail, + Err(status) => return status, + }; + if tail.is_empty() { + return on_exists(Arc::clone(&self.root)); + } + + let (parent_tail, leaf_name) = match tail.rsplit_once('\\') { + Some((parent, leaf)) => (parent, leaf), + None => ("", tail), + }; + if leaf_name.is_empty() { + return NtStatus::OBJECT_NAME_INVALID; + } + + let parent = match self.resolve_tail(parent_tail, NtStatus::OBJECT_PATH_NOT_FOUND, false) { + Ok(parent) => parent, + Err(status) => return status, + }; + let mut body = parent.body.write(); + let NamedObject::Directory { children } = &mut *body else { + return NtStatus::OBJECT_TYPE_MISMATCH; + }; + let leaf_key = ObjectName::new(leaf_name); + if let Some(existing) = children.get(&leaf_key) { + if !existing_matches(existing) { + return NtStatus::OBJECT_TYPE_MISMATCH; + } + return on_exists(Arc::clone(existing)); + } + + let node = Arc::new(construct( + join_directory_path(&parent.path, leaf_name), + Some(Arc::downgrade(&parent)), + leaf_name.to_string(), + )); + debug_assert!(node.parent().is_some()); + let status = on_created(Arc::clone(&node)); + if status == NtStatus::SUCCESS { + children.insert(leaf_key, node); + } + status + } + + pub(super) fn resolve_symlink( + &self, + path: &str, + open_final_symlink: bool, + ) -> Result>, NtStatus> { + let tail = absolute_path_tail(path)?; + let node = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, open_final_symlink)?; + if node.is_symlink() { + Ok(node) + } else { + Err(NtStatus::OBJECT_TYPE_MISMATCH) + } + } + + fn seed_directory(&self, path: &str) { + let status = self.create_directory(path, |_| NtStatus::SUCCESS, |_| NtStatus::SUCCESS); + assert!( + status == NtStatus::SUCCESS, + "seeded NT object directory must have seeded ancestors: {status:?}" + ); + } + + fn resolve_tail( + &self, + tail: &str, + final_missing_status: NtStatus, + open_final_symlink: bool, + ) -> Result>, NtStatus> { + let mut tail = tail.to_string(); + for _ in 0..=MAX_SYMLINK_REPARSE_DEPTH { + match self.resolve_tail_once(&tail, final_missing_status, open_final_symlink)? { + TailResolution::Resolved(node) => return Ok(node), + TailResolution::Reparse(next_tail) => tail = next_tail, + } + } + Err(NtStatus::NAME_TOO_LONG) + } + + fn resolve_tail_once( + &self, + tail: &str, + final_missing_status: NtStatus, + open_final_symlink: bool, + ) -> Result, NtStatus> { + if tail.is_empty() { + return Ok(TailResolution::Resolved(Arc::clone(&self.root))); + } + + let mut current = Arc::clone(&self.root); + let mut components = tail.split('\\').peekable(); + while let Some(component) = components.next() { + if component.is_empty() { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + let final_component = components.peek().is_none(); + let missing_status = if final_component { + final_missing_status + } else { + NtStatus::OBJECT_PATH_NOT_FOUND + }; + let child = current.child(component).ok_or(missing_status)?; + if child.is_symlink() && (!final_component || !open_final_symlink) { + // This is the lazy-resolution point paired with + // NtCreateSymbolicLinkObject storing the target without lookup. + let target = normalize_reparse_target(&child.symlink_target()?)?; + let target_tail = absolute_path_tail(&target)?; + let remaining = components.collect::>().join("\\"); + let next_tail = if target_tail.is_empty() { + remaining + } else if remaining.is_empty() { + target_tail.to_string() + } else { + alloc::format!("{target_tail}\\{remaining}") + }; + return Ok(TailResolution::Reparse(next_tail)); + } + current = child; + } + Ok(TailResolution::Resolved(current)) + } +} + +enum TailResolution { + Resolved(Arc>), + Reparse(String), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct DirectoryName { + pub(super) original_path: String, +} + +fn trim_trailing_directory_path(path: &str) -> &str { + if path == r"\" { + path + } else { + path.trim_end_matches('\\') + } +} + +fn normalize_reparse_target(path: &str) -> Result { + if !path.starts_with('\\') { + return Err(NtStatus::OBJECT_PATH_SYNTAX_BAD); + } + if path.len() > 1 && path[1..].contains(r"\\") { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + Ok(trim_trailing_directory_path(path).to_string()) +} + +fn absolute_path_tail(path: &str) -> Result<&str, NtStatus> { + let path = trim_trailing_directory_path(path); + if path == r"\" { + return Ok(""); + } + path.strip_prefix('\\') + .ok_or(NtStatus::OBJECT_PATH_SYNTAX_BAD) +} + +fn join_directory_path(root_path: &str, name: &str) -> String { + if root_path == r"\" { + alloc::format!(r"\{name}") + } else { + alloc::format!(r"{root_path}\{name}") + } +} + +fn read_directory_name_string( + object_name: usize, +) -> Result, NtStatus> { + debug_assert!(object_name != 0); + let unicode_string = ConstPtr::::from_usize(object_name) + .read_at_offset(0) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + if unicode_string.length == 0 { + return Ok(None); + } + if !unicode_string.length.is_multiple_of(2) { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + if unicode_string.buffer == 0 { + return Err(NtStatus::ACCESS_VIOLATION); + } + Ok(Some(unicode_string.read_string::()?)) +} + +fn utf16_byte_len(value: &str) -> Result { + let len = value + .encode_utf16() + .count() + .checked_mul(size_of::()) + .ok_or(NtStatus::NAME_TOO_LONG)?; + if len > u16::MAX as usize { + return Err(NtStatus::NAME_TOO_LONG); + } + Ok(len) +} + +fn directory_record_size(entry: &DirectoryEntrySnapshot) -> Result { + size_of::() + .checked_add(utf16_byte_len(&entry.name)?) + .and_then(|size| size.checked_add(size_of::())) + .and_then(|size| size.checked_add(utf16_byte_len(entry.type_name).ok()?)) + .and_then(|size| size.checked_add(size_of::())) + .ok_or(NtStatus::NAME_TOO_LONG) +} + +fn directory_query_required_size(entries: &[DirectoryEntrySnapshot]) -> Result { + entries + .iter() + .try_fold(size_of::(), |size, entry| { + size.checked_add(directory_record_size(entry)?) + .ok_or(NtStatus::NAME_TOO_LONG) + }) +} + +fn byte_offset(offset: usize) -> Result { + isize::try_from(offset).map_err(|_| NtStatus::BUFFER_TOO_SMALL) +} + +fn write_utf16_nul_terminated( + buffer: MutPtr, + offset: usize, + value: &str, +) -> Result<(), NtStatus> { + let mut bytes = Vec::new(); + for unit in value.encode_utf16() { + bytes.extend_from_slice(&unit.to_le_bytes()); + } + bytes.extend_from_slice(&0u16.to_le_bytes()); + buffer + .write_slice_at_offset(byte_offset(offset)?, &bytes) + .ok_or(NtStatus::ACCESS_VIOLATION) +} + +fn output_unicode_string( + buffer_base: usize, + offset: usize, + len: usize, +) -> Result { + let len = u16::try_from(len).map_err(|_| NtStatus::NAME_TOO_LONG)?; + let maximum_length = len + .checked_add(u16::try_from(size_of::()).expect("WCHAR size fits in USHORT")) + .ok_or(NtStatus::NAME_TOO_LONG)?; + Ok(UnicodeString { + length: len, + maximum_length, + padding_0: [0; 4], + buffer: buffer_base + .checked_add(offset) + .ok_or(NtStatus::NAME_TOO_LONG)?, + }) +} + +fn write_directory_records( + buffer: MutPtr, + buffer_base: usize, + entries: &[DirectoryEntrySnapshot], +) -> Result<(), NtStatus> { + let header_size = size_of::(); + let mut string_offset = entries + .len() + .checked_add(1) + .and_then(|records| records.checked_mul(header_size)) + .ok_or(NtStatus::NAME_TOO_LONG)?; + + for (index, entry) in entries.iter().enumerate() { + let name_len = utf16_byte_len(&entry.name)?; + let type_len = utf16_byte_len(entry.type_name)?; + let name_offset = string_offset; + let type_offset = name_offset + .checked_add(name_len) + .and_then(|offset| offset.checked_add(size_of::())) + .ok_or(NtStatus::NAME_TOO_LONG)?; + let record = ObjectDirectoryInformation::new( + output_unicode_string(buffer_base, name_offset, name_len)?, + output_unicode_string(buffer_base, type_offset, type_len)?, + ); + buffer + .write_slice_at_offset( + byte_offset( + index + .checked_mul(header_size) + .ok_or(NtStatus::NAME_TOO_LONG)?, + )?, + record.as_bytes(), + ) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + write_utf16_nul_terminated::(buffer, name_offset, &entry.name)?; + write_utf16_nul_terminated::(buffer, type_offset, entry.type_name)?; + string_offset = type_offset + .checked_add(type_len) + .and_then(|offset| offset.checked_add(size_of::())) + .ok_or(NtStatus::NAME_TOO_LONG)?; + } + + let terminator_offset = entries + .len() + .checked_mul(header_size) + .ok_or(NtStatus::NAME_TOO_LONG)?; + buffer + .write_slice_at_offset( + byte_offset(terminator_offset)?, + ObjectDirectoryInformation::zero().as_bytes(), + ) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + Ok(()) +} + +fn probe_output_buffer( + buffer: MutPtr, + buffer_length: usize, +) -> Result<(), NtStatus> { + if buffer_length == 0 { + return Ok(()); + } + let value = buffer.read_at_offset(0).ok_or(NtStatus::ACCESS_VIOLATION)?; + buffer + .write_at_offset(0, value) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + let last_offset = byte_offset(buffer_length - 1)?; + let value = buffer + .read_at_offset(last_offset) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + buffer + .write_at_offset(last_offset, value) + .ok_or(NtStatus::ACCESS_VIOLATION) +} + +impl Task { + fn directory_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> + { + self.typed_handle_entry::>(handle) + } + + fn directory_object_for_name_resolution( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + let entry = self.directory_entry(handle)?; + entry.with_entry(|entry| { + entry.granted_access.require(DirectoryAccess::TRAVERSE)?; + Ok(Arc::clone(&entry.directory)) + }) + } + + pub(super) fn read_directory_object_attributes( + &self, + object_attributes: Option>, + require_name: bool, + ) -> Result<(Option, Option), NtStatus> { + let Some(object_attributes_ptr) = object_attributes else { + if require_name { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + return Ok((None, None)); + }; + let object_attributes = read_object_attributes::(object_attributes_ptr)?; + + if object_attributes.object_name == 0 { + if require_name { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + if !object_attributes.root_directory.is_null() { + // Wine and ReactOS match Windows: a NULL ObjectName plus RootDirectory + // is invalid, while a present zero-length UNICODE_STRING creates unnamed. + return Err(NtStatus::OBJECT_NAME_INVALID); + } + return Ok((Some(object_attributes), None)); + } + + let Some(raw_name) = read_directory_name_string::(object_attributes.object_name)? + else { + if require_name { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + return Ok((Some(object_attributes), None)); + }; + if raw_name.is_empty() { + if require_name { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + return Ok((Some(object_attributes), None)); + } + + let original_path = if object_attributes.root_directory.is_null() { + if !raw_name.starts_with('\\') { + return Err(NtStatus::OBJECT_PATH_SYNTAX_BAD); + } + raw_name + } else { + if raw_name.starts_with('\\') { + return Err(NtStatus::OBJECT_PATH_SYNTAX_BAD); + } + let root = + self.directory_object_for_name_resolution(object_attributes.root_directory)?; + join_directory_path(&root.path, &raw_name) + }; + if original_path.len() > 1 && original_path[1..].contains(r"\\") { + return Err(NtStatus::OBJECT_NAME_INVALID); + } + Ok(( + Some(object_attributes), + Some(DirectoryName { original_path }), + )) + } + + fn insert_directory_handle( + &self, + directory: Arc>, + granted_access: DirectoryAccess, + ) -> Result { + self.insert_typed_handle::>( + DirectoryHandleObject { + directory, + granted_access, + }, + drop, + ) + } + + pub(crate) fn close_directory_handle(&self, handle: Handle) { + self.close_typed_handle::>(handle, drop); + } + + pub(crate) fn close_directory(directory: DirectoryHandleObject) { + drop(directory); + } + + pub(crate) fn sys_nt_create_directory_object( + &self, + directory_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + shadow_directory_handle: Handle, + flags: u32, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(directory_handle) { + return status; + } + if flags != 0 { + return NtStatus::INVALID_PARAMETER; + } + if !shadow_directory_handle.is_null() + && let Err(status) = self.directory_entry(shadow_directory_handle) + { + return status; + } + let (object_attributes, directory_name) = + match self.read_directory_object_attributes(object_attributes, false) { + Ok(value) => value, + Err(status) => return status, + }; + if let Some(object_attributes) = object_attributes + && ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) + .contains(ObjectAttributesFlags::OPENLINK) + { + return NtStatus::INVALID_PARAMETER; + } + let granted_access = DirectoryAccess::from_desired_access(desired_access); + + if let Some(directory_name) = directory_name { + return self.process.directory_namespace.create_directory( + &directory_name.original_path, + |directory| { + let Some(object_attributes) = object_attributes else { + return NtStatus::INVALID_PARAMETER; + }; + if !ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) + .contains(ObjectAttributesFlags::OPENIF) + { + return NtStatus::OBJECT_NAME_COLLISION; + } + let Ok(handle) = self.insert_directory_handle(directory, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if directory_handle.write_at_offset(0, handle).is_none() { + self.close_directory_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::OBJECT_NAME_EXISTS + }, + |directory| { + let Ok(handle) = self.insert_directory_handle(directory, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if directory_handle.write_at_offset(0, handle).is_none() { + self.close_directory_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + }, + ); + } + + let directory = Arc::new(ObjectNode::new_directory( + String::new(), + None, + String::new(), + )); + let Ok(handle) = self.insert_directory_handle(directory, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if directory_handle.write_at_offset(0, handle).is_none() { + self.close_directory_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_open_directory_object( + &self, + directory_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(directory_handle) { + return status; + } + let directory_name = match self.read_directory_object_attributes(object_attributes, true) { + Ok((Some(object_attributes), Some(directory_name))) => { + if ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) + .contains(ObjectAttributesFlags::OPENLINK) + { + return NtStatus::INVALID_PARAMETER; + } + directory_name + } + Ok((_, None)) => return NtStatus::OBJECT_NAME_INVALID, + Ok((None, Some(_))) => return NtStatus::INVALID_PARAMETER, + Err(status) => return status, + }; + let directory = { + match self + .process + .directory_namespace + .resolve_directory(&directory_name.original_path) + { + Ok(directory) => directory, + Err(status) => return status, + } + }; + let Ok(handle) = self.insert_directory_handle( + directory, + DirectoryAccess::from_desired_access(desired_access), + ) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if directory_handle.write_at_offset(0, handle).is_none() { + self.close_directory_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + litebox_util_log::debug!( + object_name:% = directory_name.original_path.as_str(), + desired_access:% = format_args!("{desired_access:#x}"); + "Handled NtOpenDirectoryObject syscall" + ); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_query_directory_object( + &self, + params: DirectoryQueryParameters, + ) -> NtStatus { + let entry = match self.directory_entry(params.directory_handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + if let Err(status) = + entry.with_entry(|entry| entry.granted_access.require(DirectoryAccess::QUERY)) + { + return status; + } + let directory = entry.with_entry(|entry| Arc::clone(&entry.directory)); + let entries = match directory.children_snapshot() { + Ok(entries) => entries, + Err(status) => return status, + }; + let buffer_length = params.buffer_length as usize; + if let Err(status) = probe_output_buffer::(params.buffer, buffer_length) { + return status; + } + + let start_index = if params.restart_scan != 0 { + 0 + } else { + let Some(context) = params.context.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + context as usize + }; + if start_index >= entries.len() { + let context = + u32::try_from(entries.len()).expect("directory entry count fits in ULONG"); + // Saturate the opaque resume cookie at end-of-directory so repeated + // continuation calls remain stable. + if params.context.write_at_offset(0, context).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(return_length) = params.return_length + && return_length.write_at_offset(0, 0).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + return NtStatus::NO_MORE_ENTRIES; + } + + let end_for_required = if params.return_single_entry != 0 { + start_index + 1 + } else { + entries.len() + }; + let total_required = + match directory_query_required_size(&entries[start_index..end_for_required]) { + Ok(size) => size, + Err(status) => return status, + }; + let first_required = + match directory_query_required_size(core::slice::from_ref(&entries[start_index])) { + Ok(size) => size, + Err(status) => return status, + }; + if buffer_length < first_required { + if let Some(return_length) = params.return_length { + let required = u32::try_from(total_required).map_err(|_| NtStatus::NAME_TOO_LONG); + let Ok(required) = required else { + return NtStatus::NAME_TOO_LONG; + }; + if return_length.write_at_offset(0, required).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + } + return if params.return_single_entry != 0 { + NtStatus::BUFFER_TOO_SMALL + } else { + NtStatus::MORE_ENTRIES + }; + } + + let buffer_base = params.buffer.as_usize(); + let mut next_index = start_index; + let mut required_for_written = size_of::(); + while next_index < entries.len() { + let entry_size = match directory_record_size(&entries[next_index]) { + Ok(size) => size, + Err(status) => return status, + }; + if required_for_written + .checked_add(entry_size) + .is_none_or(|needed| needed > buffer_length) + { + break; + } + required_for_written += entry_size; + next_index += 1; + if params.return_single_entry != 0 { + break; + } + } + + if let Err(status) = write_directory_records::( + params.buffer, + buffer_base, + &entries[start_index..next_index], + ) { + return status; + } + let status = if next_index < entries.len() { + NtStatus::MORE_ENTRIES + } else { + NtStatus::SUCCESS + }; + + let context = u32::try_from(next_index).expect("directory entry count fits in ULONG"); + if params.context.write_at_offset(0, context).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(return_length) = params.return_length { + let Ok(returned) = u32::try_from(total_required) else { + return NtStatus::NAME_TOO_LONG; + }; + if return_length.write_at_offset(0, returned).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + } + status + } +} + +pub(crate) fn seed_directory_namespace() +-> crate::WindowsDirectoryNamespace { + let namespace = DirectoryNamespace::new(); + for path in SEEDED_DIRECTORY_PATHS { + namespace.seed_directory(path); + } + namespace +} + +#[cfg(test)] +mod tests { + use core::mem::size_of; + + use litebox::platform::ThreadProvider; + use litebox::utils::TruncateExt as _; + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::{ObjectAttributes, ObjectAttributesFlags}; + use crate::tests::{ + TestPlatform, const_ptr, mut_ptr, null_mut_ptr, object_attributes, test_task, + unicode_string, utf16_units, + }; + + const DIRECTORY_QUERY: u32 = 0x0000_0001; + const DIRECTORY_TRAVERSE: u32 = 0x0000_0002; + const DIRECTORY_ALL_ACCESS: u32 = 0x000f_000f; + + #[derive(Clone, Debug, Eq, PartialEq)] + struct ParsedDirectoryInformation { + name: String, + type_name: String, + } + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = crate::tests::test_platform(); + ::run_test_thread(f) + } + + fn read_u16(buffer: &[u8], offset: usize) -> u16 { + u16::from_le_bytes(buffer[offset..offset + 2].try_into().expect("u16 bytes")) + } + + fn read_usize(buffer: &[u8], offset: usize) -> usize { + usize::from_le_bytes( + buffer[offset..offset + size_of::()] + .try_into() + .expect("usize bytes"), + ) + } + + fn read_utf16_string( + buffer: &[u8], + buffer_base: usize, + address: usize, + length: usize, + ) -> String { + let offset = address + .checked_sub(buffer_base) + .expect("string buffer points into output buffer"); + assert!( + offset + .checked_add(length) + .is_some_and(|end| end <= buffer.len()), + "string buffer range stays inside output buffer" + ); + let units: alloc::vec::Vec = buffer[offset..offset + length] + .chunks_exact(2) + .map(|bytes| u16::from_le_bytes(bytes.try_into().expect("u16 bytes"))) + .collect(); + String::from_utf16_lossy(&units) + } + + fn read_directory_information(buffer: &[u8], offset: usize) -> ParsedDirectoryInformation { + let buffer_base = buffer.as_ptr() as usize; + let name_len = read_u16(buffer, offset) as usize; + let name_max = read_u16(buffer, offset + 2) as usize; + let name_buffer = read_usize(buffer, offset + 8); + let type_len = read_u16(buffer, offset + 16) as usize; + let type_max = read_u16(buffer, offset + 18) as usize; + let type_buffer = read_usize(buffer, offset + 24); + assert_eq!(name_max, name_len + size_of::()); + assert_eq!(type_max, type_len + size_of::()); + let name_offset = name_buffer + .checked_sub(buffer_base) + .expect("name buffer points into output buffer"); + let type_offset = type_buffer + .checked_sub(buffer_base) + .expect("type buffer points into output buffer"); + assert_eq!(read_u16(buffer, name_offset + name_len), 0); + assert_eq!(read_u16(buffer, type_offset + type_len), 0); + ParsedDirectoryInformation { + name: read_utf16_string(buffer, buffer_base, name_buffer, name_len), + type_name: read_utf16_string(buffer, buffer_base, type_buffer, type_len), + } + } + + fn assert_zero_directory_information(buffer: &[u8], offset: usize) { + assert_eq!(read_u16(buffer, offset), 0); + assert_eq!(read_u16(buffer, offset + 2), 0); + assert_eq!(read_usize(buffer, offset + 8), 0); + assert_eq!(read_u16(buffer, offset + 16), 0); + assert_eq!(read_u16(buffer, offset + 18), 0); + assert_eq!(read_usize(buffer, offset + 24), 0); + } + + fn create_named_directory( + task: &Task, + path: &str, + ) -> Handle { + let name_units = utf16_units(path); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut handle), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&attrs)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + handle + } + + fn open_named_directory(task: &Task, path: &str) -> Handle { + let name_units = utf16_units(path); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut handle), + DIRECTORY_QUERY, + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + handle + } + + fn expected_record_size(name: &str, type_name: &str) -> usize { + size_of::() + + name.encode_utf16().count() * size_of::() + + size_of::() + + type_name.encode_utf16().count() * size_of::() + + size_of::() + } + + fn expected_query_size(entries: &[(&str, &str)]) -> usize { + size_of::() + + entries + .iter() + .map(|(name, type_name)| expected_record_size(name, type_name)) + .sum::() + } + + #[test] + fn open_seeded_root_directory_succeeds() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let name_units = utf16_units(r"\"); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut handle), + DIRECTORY_QUERY, + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + }); + } + + #[test] + fn open_directory_rejects_openlink_attribute() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let name_units = utf16_units(r"\BaseNamedObjects"); + let name = unicode_string(&name_units); + let attrs = object_attributes( + &name, + (ObjectAttributesFlags::CASE_INSENSITIVE | ObjectAttributesFlags::OPENLINK).bits(), + ); + let mut handle = Handle::default(); + + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut handle), + DIRECTORY_QUERY, + Some(const_ptr(&attrs)), + ), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::default()); + }); + } + + #[test] + fn open_directory_distinguishes_missing_leaf_from_missing_parent() { + run_with_test_platform_pointers(|| { + let task = test_task(); + for (path, expected_status) in [ + ( + r"\BaseNamedObjects\DefinitelyMissingLiteBoxDir", + NtStatus::OBJECT_NAME_NOT_FOUND, + ), + ( + r"\KnownDlls\DefinitelyMissingLiteBoxDir", + NtStatus::OBJECT_NAME_NOT_FOUND, + ), + ( + r"\MissingParentLiteBox\Child", + NtStatus::OBJECT_PATH_NOT_FOUND, + ), + ( + r"\DefinitelyMissingLiteBoxDir", + NtStatus::OBJECT_NAME_NOT_FOUND, + ), + ] { + let name_units = utf16_units(path); + let name = unicode_string(&name_units); + let attrs = + object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut handle), + DIRECTORY_QUERY, + Some(const_ptr(&attrs)), + ), + expected_status, + "unexpected status opening {path}", + ); + assert_eq!(handle, Handle::default()); + } + }); + } + + #[test] + fn create_directory_distinguishes_null_object_name_from_empty_name() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let root_units = utf16_units(r"\BaseNamedObjects"); + let root_name = unicode_string(&root_units); + let root_attrs = + object_attributes(&root_name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut root = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut root), + DIRECTORY_TRAVERSE | DIRECTORY_QUERY, + Some(const_ptr(&root_attrs)), + ), + NtStatus::SUCCESS + ); + + let null_name_with_root = ObjectAttributes { + length: size_of::().trunc(), + root_directory: root, + object_name: 0, + attributes: ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + security_descriptor: 0, + security_quality_of_service: 0, + }; + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut handle), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&null_name_with_root)), + Handle::default(), + 0, + ), + NtStatus::OBJECT_NAME_INVALID + ); + assert_eq!(handle, Handle::default()); + + let null_name_without_root = ObjectAttributes { + root_directory: Handle::default(), + ..null_name_with_root + }; + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut handle), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&null_name_without_root)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + assert_ne!(handle, Handle::default()); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + + let empty_name_units: [u16; 0] = []; + let empty_name = unicode_string(&empty_name_units); + let empty_name_with_root = ObjectAttributes { + root_directory: root, + object_name: core::ptr::from_ref(&empty_name) as usize, + ..null_name_with_root + }; + handle = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut handle), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&empty_name_with_root)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + assert_ne!(handle, Handle::default()); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(root), NtStatus::SUCCESS); + }); + } + + #[test] + fn create_and_open_directory_relative_to_root_directory() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let root_units = utf16_units(r"\BaseNamedObjects"); + let root_name = unicode_string(&root_units); + let root_attrs = + object_attributes(&root_name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut root = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut root), + DIRECTORY_TRAVERSE | DIRECTORY_QUERY, + Some(const_ptr(&root_attrs)), + ), + NtStatus::SUCCESS + ); + + let child_units = utf16_units("LiteBoxDirectory"); + let child_name = unicode_string(&child_units); + let child_attrs = ObjectAttributes { + length: size_of::().trunc(), + root_directory: root, + object_name: core::ptr::from_ref(&child_name) as usize, + attributes: ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + security_descriptor: 0, + security_quality_of_service: 0, + }; + let mut created = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut created), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&child_attrs)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut opened), + DIRECTORY_QUERY, + Some(const_ptr(&child_attrs)), + ), + NtStatus::SUCCESS + ); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(created), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(root), NtStatus::SUCCESS); + }); + } + + #[test] + fn directory_lookup_is_case_insensitive_and_case_preserving() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let mixed = create_named_directory(&task, r"\BaseNamedObjects\LiteBoxCaseMixed"); + let lower_open = open_named_directory(&task, r"\basenamedobjects\liteboxcasemixed"); + let trailing_open = open_named_directory(&task, r"\BaseNamedObjects\LiteBoxCaseMixed\"); + let lower_created = + create_named_directory(&task, r"\BaseNamedObjects\liteboxcaselower"); + let upper_open = open_named_directory(&task, r"\BASENAMEDOBJECTS\LITEBOXCASELOWER"); + + let duplicate_units = utf16_units(r"\basenamedobjects\liteboxcasemixed\"); + let duplicate_name = unicode_string(&duplicate_units); + let duplicate_attrs = object_attributes( + &duplicate_name, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + let mut duplicate = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut duplicate), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&duplicate_attrs)), + Handle::default(), + 0, + ), + NtStatus::OBJECT_NAME_COLLISION + ); + assert_eq!(duplicate, Handle::default()); + + let parent = open_named_directory(&task, r"\BaseNamedObjects"); + let mut buffer = [0u8; 512]; + let mut context = 0u32; + let mut return_length = 0u32; + assert_eq!( + task.sys_nt_query_directory_object(DirectoryQueryParameters { + directory_handle: parent, + buffer: mut_ptr(&mut buffer[0]), + buffer_length: buffer.len().trunc(), + return_single_entry: 0, + restart_scan: 1, + context: mut_ptr(&mut context), + return_length: Some(mut_ptr(&mut return_length)), + }), + NtStatus::SUCCESS + ); + + let first_record = read_directory_information(&buffer, 0); + assert_eq!( + first_record, + ParsedDirectoryInformation { + name: "liteboxcaselower".to_string(), + type_name: "Directory".to_string(), + } + ); + let second_offset = size_of::(); + let second_record = read_directory_information(&buffer, second_offset); + assert_eq!( + second_record, + ParsedDirectoryInformation { + name: "LiteBoxCaseMixed".to_string(), + type_name: "Directory".to_string(), + } + ); + assert_zero_directory_information( + &buffer, + second_offset + size_of::(), + ); + assert_eq!(context, 2); + assert_eq!( + return_length as usize, + expected_query_size(&[ + ("liteboxcaselower", "Directory"), + ("LiteBoxCaseMixed", "Directory") + ]) + ); + + assert_eq!(task.sys_nt_close(parent), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(upper_open), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(lower_created), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(trailing_open), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(lower_open), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(mixed), NtStatus::SUCCESS); + }); + } + + #[test] + fn relative_directory_name_requires_root_traverse_access() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let root_units = utf16_units(r"\BaseNamedObjects"); + let root_name = unicode_string(&root_units); + let root_attrs = + object_attributes(&root_name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut root = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut root), + DIRECTORY_QUERY, + Some(const_ptr(&root_attrs)), + ), + NtStatus::SUCCESS + ); + + let child_units = utf16_units("LiteBoxTraverseDenied"); + let child_name = unicode_string(&child_units); + let child_attrs = ObjectAttributes { + length: size_of::().trunc(), + root_directory: root, + object_name: core::ptr::from_ref(&child_name) as usize, + attributes: ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + security_descriptor: 0, + security_quality_of_service: 0, + }; + let mut child = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut child), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&child_attrs)), + Handle::default(), + 0, + ), + NtStatus::ACCESS_DENIED + ); + assert_eq!(child, Handle::default()); + assert_eq!(task.sys_nt_close(root), NtStatus::SUCCESS); + }); + } + + #[test] + fn create_nested_directory_after_parent_exists() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let parent_units = utf16_units(r"\BaseNamedObjects\LiteBoxTreeParent"); + let parent_name = unicode_string(&parent_units); + let parent_attrs = + object_attributes(&parent_name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut parent = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut parent), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&parent_attrs)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + + let child_units = utf16_units(r"\BaseNamedObjects\LiteBoxTreeParent\LiteBoxTreeChild"); + let child_name = unicode_string(&child_units); + let child_attrs = + object_attributes(&child_name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut child = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut child), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&child_attrs)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut opened), + DIRECTORY_QUERY, + Some(const_ptr(&child_attrs)), + ), + NtStatus::SUCCESS + ); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(child), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(parent), NtStatus::SUCCESS); + }); + } + + #[test] + fn create_existing_directory_obeys_openif() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let name_units = utf16_units(r"\BaseNamedObjects\LiteBoxOpenIfDirectory"); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut first = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut first), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&attrs)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + + let mut collision = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut collision), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&attrs)), + Handle::default(), + 0, + ), + NtStatus::OBJECT_NAME_COLLISION + ); + assert_eq!(collision, Handle::default()); + + let openif_attrs = ObjectAttributes { + attributes: (ObjectAttributesFlags::CASE_INSENSITIVE + | ObjectAttributesFlags::OPENIF) + .bits(), + ..attrs + }; + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut opened), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&openif_attrs)), + Handle::default(), + 0, + ), + NtStatus::OBJECT_NAME_EXISTS + ); + assert_ne!(opened, Handle::default()); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(first), NtStatus::SUCCESS); + }); + } + + #[test] + fn query_empty_directory_reports_no_more_entries() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let name_units = utf16_units(r"\BaseNamedObjects"); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut handle), + DIRECTORY_QUERY, + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + + let mut buffer = [0xffu8; 32]; + let mut context = 99u32; + let mut return_length = u32::MAX; + assert_eq!( + task.sys_nt_query_directory_object(DirectoryQueryParameters { + directory_handle: handle, + buffer: mut_ptr(&mut buffer[0]), + buffer_length: buffer.len().trunc(), + return_single_entry: 0, + restart_scan: 1, + context: mut_ptr(&mut context), + return_length: Some(mut_ptr(&mut return_length)), + },), + NtStatus::NO_MORE_ENTRIES + ); + assert_eq!(buffer[0], 0xff); + assert_eq!(context, 0); + assert_eq!(return_length, 0); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + }); + } + + #[test] + fn query_directory_enumerates_children_in_stable_order() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let first = create_named_directory(&task, r"\BaseNamedObjects\LiteBoxEnumB"); + let second = create_named_directory(&task, r"\BaseNamedObjects\LiteBoxEnumA"); + let handle = open_named_directory(&task, r"\BaseNamedObjects"); + let mut buffer = [0u8; 512]; + let mut context = u32::MAX; + let mut return_length = 0u32; + + assert_eq!( + task.sys_nt_query_directory_object(DirectoryQueryParameters { + directory_handle: handle, + buffer: mut_ptr(&mut buffer[0]), + buffer_length: buffer.len().trunc(), + return_single_entry: 0, + restart_scan: 1, + context: mut_ptr(&mut context), + return_length: Some(mut_ptr(&mut return_length)), + },), + NtStatus::SUCCESS + ); + + let first_record = read_directory_information(&buffer, 0); + assert_eq!( + first_record, + ParsedDirectoryInformation { + name: "LiteBoxEnumA".to_string(), + type_name: "Directory".to_string(), + } + ); + let second_offset = size_of::(); + let second_record = read_directory_information(&buffer, second_offset); + assert_eq!( + second_record, + ParsedDirectoryInformation { + name: "LiteBoxEnumB".to_string(), + type_name: "Directory".to_string(), + } + ); + assert_zero_directory_information( + &buffer, + second_offset + size_of::(), + ); + assert_eq!(context, 2); + assert_eq!( + return_length as usize, + expected_query_size(&[ + ("LiteBoxEnumA", "Directory"), + ("LiteBoxEnumB", "Directory") + ]) + ); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(second), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(first), NtStatus::SUCCESS); + }); + } + + #[test] + fn query_directory_single_entry_uses_context_cookie() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let first = create_named_directory(&task, r"\BaseNamedObjects\LiteBoxSingleA"); + let second = create_named_directory(&task, r"\BaseNamedObjects\LiteBoxSingleB"); + let handle = open_named_directory(&task, r"\BaseNamedObjects"); + let mut buffer = [0u8; 256]; + let mut context = 123u32; + let mut return_length = 0u32; + + assert_eq!( + task.sys_nt_query_directory_object(DirectoryQueryParameters { + directory_handle: handle, + buffer: mut_ptr(&mut buffer[0]), + buffer_length: buffer.len().trunc(), + return_single_entry: 1, + restart_scan: 1, + context: mut_ptr(&mut context), + return_length: Some(mut_ptr(&mut return_length)), + },), + NtStatus::MORE_ENTRIES + ); + assert_eq!(context, 1); + assert_eq!( + read_directory_information(&buffer, 0), + ParsedDirectoryInformation { + name: "LiteBoxSingleA".to_string(), + type_name: "Directory".to_string(), + } + ); + + buffer.fill(0); + assert_eq!( + task.sys_nt_query_directory_object(DirectoryQueryParameters { + directory_handle: handle, + buffer: mut_ptr(&mut buffer[0]), + buffer_length: buffer.len().trunc(), + return_single_entry: 1, + restart_scan: 0, + context: mut_ptr(&mut context), + return_length: Some(mut_ptr(&mut return_length)), + },), + NtStatus::SUCCESS + ); + assert_eq!(context, 2); + assert_eq!( + read_directory_information(&buffer, 0), + ParsedDirectoryInformation { + name: "LiteBoxSingleB".to_string(), + type_name: "Directory".to_string(), + } + ); + assert_zero_directory_information(&buffer, size_of::()); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(second), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(first), NtStatus::SUCCESS); + }); + } + + #[test] + fn query_directory_too_small_reports_required_length_without_advancing_context() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let child = create_named_directory(&task, r"\BaseNamedObjects\LiteBoxSmall"); + let handle = open_named_directory(&task, r"\BaseNamedObjects"); + let mut buffer = [0xffu8; 8]; + let mut context = 99u32; + let mut return_length = 0u32; + + assert_eq!( + task.sys_nt_query_directory_object(DirectoryQueryParameters { + directory_handle: handle, + buffer: mut_ptr(&mut buffer[0]), + buffer_length: buffer.len().trunc(), + return_single_entry: 0, + restart_scan: 1, + context: mut_ptr(&mut context), + return_length: Some(mut_ptr(&mut return_length)), + },), + NtStatus::MORE_ENTRIES + ); + assert_eq!(context, 99); + assert_eq!( + return_length as usize, + expected_query_size(&[("LiteBoxSmall", "Directory")]) + ); + assert_eq!(buffer, [0xffu8; 8]); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(child), NtStatus::SUCCESS); + }); + } + + #[test] + fn query_requires_directory_query_access() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let name_units = utf16_units(r"\BaseNamedObjects"); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut handle), + DIRECTORY_TRAVERSE, + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + let mut buffer = 0u8; + let mut context = 0u32; + assert_eq!( + task.sys_nt_query_directory_object(DirectoryQueryParameters { + directory_handle: handle, + buffer: mut_ptr(&mut buffer), + buffer_length: 1, + return_single_entry: 0, + restart_scan: 1, + context: mut_ptr(&mut context), + return_length: None, + },), + NtStatus::ACCESS_DENIED + ); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + }); + } + + #[test] + fn create_probes_output_before_name_resolution() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let name_units = utf16_units(r"\MissingParent\Child"); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + + assert_eq!( + task.sys_nt_create_directory_object( + null_mut_ptr(), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&attrs)), + Handle::default(), + 0, + ), + NtStatus::ACCESS_VIOLATION + ); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[test] + fn host_open_root_directory_status_fidelity() { + use core::ffi::c_void; + + unsafe extern "system" { + fn NtOpenDirectoryObject( + handle: *mut *mut c_void, + access: u32, + attributes: *const ObjectAttributes, + ) -> i32; + fn NtClose(handle: *mut c_void) -> i32; + } + + run_with_test_platform_pointers(|| { + let task = test_task(); + let name_units = utf16_units(r"\"); + let name = unicode_string(&name_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut host_handle = core::ptr::null_mut(); + // SAFETY: The object attributes and output handle point to live test + // stack values for the duration of the host ntdll call. + let host_status = unsafe { + NtOpenDirectoryObject(&raw mut host_handle, DIRECTORY_QUERY, &raw const attrs) + }; + if host_status == NtStatus::SUCCESS.as_raw() && !host_handle.is_null() { + // SAFETY: NtOpenDirectoryObject returned this non-null handle with + // STATUS_SUCCESS, so it is valid to close once here. + unsafe { + NtClose(host_handle); + } + } + + let mut litebox_handle = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut litebox_handle), + DIRECTORY_QUERY, + Some(const_ptr(&attrs)), + ) + .as_raw(), + host_status + ); + if litebox_handle != Handle::default() { + assert_eq!(task.sys_nt_close(litebox_handle), NtStatus::SUCCESS); + } + }); + } +} diff --git a/litebox_shim_windows/src/syscalls/event.rs b/litebox_shim_windows/src/syscalls/event.rs index 7807341a1b..1f52c45617 100644 --- a/litebox_shim_windows/src/syscalls/event.rs +++ b/litebox_shim_windows/src/syscalls/event.rs @@ -16,17 +16,14 @@ use litebox::sync::Mutex; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; -use crate::nt_types::{AccessMask, ObjectAttributes, UnicodeString, read_object_attributes}; +use crate::nt_types::{ + AccessMask, ObjectAttributes, ObjectAttributesFlags, UnicodeString, read_object_attributes, +}; use crate::syscalls::Handle; use crate::{ - ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, - raw_handle_entry, remove_raw_handle, + ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value, raw_handle_entry, }; -const OBJ_CASE_INSENSITIVE: u32 = 0x0000_0040; -const OBJ_OPENIF: u32 = 0x0000_0080; -const OBJ_OPENLINK: u32 = 0x0000_0100; - #[repr(u32)] #[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] pub(crate) enum EventType { @@ -66,26 +63,13 @@ bitflags::bitflags! { impl EventAccess { fn from_desired_access(desired_access: u32) -> Self { - let mut access = Self::from_bits_retain(desired_access); - if desired_access & AccessMask::GENERIC_READ.bits() != 0 { - access.insert(Self::READ); - } - if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { - access.insert(Self::WRITE); - } - if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { - access.insert(Self::EXECUTE); - } - if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { - access.insert(Self::ALL_ACCESS); - } - access.remove(Self::from_bits_retain( - AccessMask::GENERIC_READ.bits() - | AccessMask::GENERIC_WRITE.bits() - | AccessMask::GENERIC_EXECUTE.bits() - | AccessMask::GENERIC_ALL.bits(), - )); - access + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + )) } fn require(self, required: Self) -> Result<(), NtStatus> { @@ -233,7 +217,9 @@ fn read_event_name( if key.is_empty() { return Err(NtStatus::OBJECT_NAME_INVALID); } - if object_attributes.attributes & OBJ_CASE_INSENSITIVE != 0 { + if ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) + .contains(ObjectAttributesFlags::CASE_INSENSITIVE) + { key = key.to_ascii_lowercase(); } Ok(Some(EventName { key })) @@ -250,7 +236,9 @@ fn read_event_object_attributes( return Ok((None, None)); }; let object_attributes = read_object_attributes::(object_attributes_ptr)?; - if object_attributes.attributes & OBJ_OPENLINK != 0 { + if ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) + .contains(ObjectAttributesFlags::OPENLINK) + { return Err(NtStatus::INVALID_PARAMETER); } let event_name = @@ -279,29 +267,17 @@ impl Task { event: Arc>, granted_access: EventAccess, ) -> Result { - let typed = self - .global - .litebox - .descriptor_table_mut() - .insert::>(EventHandleObject { + self.insert_typed_handle::>( + EventHandleObject { event, granted_access, - }); - insert_raw_handle::>( - &self.global.litebox, - &self.process.handles, - typed, + }, drop, ) } pub(crate) fn close_event_handle(&self, handle: Handle) { - remove_raw_handle::>( - &self.global.litebox, - &self.process.handles, - handle, - drop, - ); + self.close_typed_handle::>(handle, drop); } pub(crate) fn close_event(event: EventHandleObject) { @@ -343,7 +319,9 @@ impl Task { let Some(object_attributes) = object_attributes else { return NtStatus::INVALID_PARAMETER; }; - if object_attributes.attributes & OBJ_OPENIF == 0 { + if !ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) + .contains(ObjectAttributesFlags::OPENIF) + { return NtStatus::OBJECT_NAME_COLLISION; } let Ok(handle) = self.insert_event_handle(event, granted_access) else { @@ -553,26 +531,27 @@ impl Task { #[cfg(test)] mod tests { - use alloc::vec::Vec; use core::mem::size_of; + use litebox::utils::TruncateExt as _; use litebox_common_windows::nt_status::NtStatus; use super::*; - use crate::nt_types::ObjectAttributes; - use crate::tests::{const_ptr, mut_ptr, object_attributes, test_task, unicode_string}; + use crate::nt_types::{ObjectAttributes, ObjectAttributesFlags}; + use crate::tests::{ + const_ptr, mut_ptr, object_attributes, test_task, unicode_string, utf16_units, + }; const EVENT_QUERY_STATE: u32 = 0x0001; const EVENT_MODIFY_STATE: u32 = 0x0002; const EVENT_ALL_ACCESS: u32 = 0x001f_0003; fn event_basic_information_size() -> u32 { - u32::try_from(size_of::()) - .expect("EVENT_BASIC_INFORMATION fits in ULONG") + size_of::().trunc() } fn object_attributes_size() -> u32 { - u32::try_from(size_of::()).expect("OBJECT_ATTRIBUTES fits in ULONG") + size_of::().trunc() } #[test] @@ -764,9 +743,9 @@ mod tests { #[test] fn named_event_open_shares_state() { let task = test_task(); - let name_units: Vec = "\\BaseNamedObjects\\LiteBoxEvent".encode_utf16().collect(); + let name_units = utf16_units("\\BaseNamedObjects\\LiteBoxEvent"); let name = unicode_string(&name_units); - let attrs = object_attributes(&name, OBJ_CASE_INSENSITIVE); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); let mut created = Handle::default(); assert_eq!( @@ -839,11 +818,12 @@ mod tests { #[test] fn create_openif_existing_named_event_returns_name_exists() { let task = test_task(); - let name_units: Vec = "\\BaseNamedObjects\\LiteBoxOpenIf".encode_utf16().collect(); + let name_units = utf16_units("\\BaseNamedObjects\\LiteBoxOpenIf"); let name = unicode_string(&name_units); - let attrs = object_attributes(&name, OBJ_CASE_INSENSITIVE); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); let openif_attrs = ObjectAttributes { - attributes: OBJ_CASE_INSENSITIVE | OBJ_OPENIF, + attributes: (ObjectAttributesFlags::CASE_INSENSITIVE | ObjectAttributesFlags::OPENIF) + .bits(), ..attrs }; @@ -1150,12 +1130,13 @@ mod tests { #[test] fn named_open_matches_host_state_sharing() { let unique = 0u8; - let name_units: Vec = - alloc::format!(r"\BaseNamedObjects\LiteBoxEventFidelity{:p}", &unique,) - .encode_utf16() - .collect(); + let name_units = utf16_units(&alloc::format!( + r"\BaseNamedObjects\LiteBoxEventFidelity{:p}", + &unique, + )); let name = unicode_string(&name_units); - let attributes = object_attributes(&name, OBJ_CASE_INSENSITIVE); + let attributes = + object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); let mut host_created = core::ptr::null_mut(); let mut host_opened = core::ptr::null_mut(); diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 51d592ca0a..8fd9104cbf 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -16,8 +16,7 @@ use crate::nt_types::{ }; use crate::syscalls::Handle; use crate::{ - ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, - raw_handle_entry, remove_raw_handle, + ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value, raw_handle_entry, }; const FILE_ATTRIBUTE_READONLY: u32 = 0x0000_0001; @@ -85,10 +84,6 @@ bitflags::bitflags! { const WRITE_ATTRIBUTES = 0x0100; const DELETE = AccessMask::DELETE.bits(); const SYNCHRONIZE = AccessMask::SYNCHRONIZE.bits(); - const GENERIC_ALL = AccessMask::GENERIC_ALL.bits(); - const GENERIC_EXECUTE = AccessMask::GENERIC_EXECUTE.bits(); - const GENERIC_WRITE = AccessMask::GENERIC_WRITE.bits(); - const GENERIC_READ = AccessMask::GENERIC_READ.bits(); const GENERIC_READ_EXPANSION = AccessMask::STANDARD_RIGHTS_READ.bits() | Self::READ_DATA.bits() @@ -145,24 +140,13 @@ bitflags::bitflags! { impl FileAccess { fn from_desired_access(desired_access: u32) -> Self { - let mut access = Self::from_bits_retain(desired_access); - if access.contains(Self::GENERIC_READ) { - access.remove(Self::GENERIC_READ); - access.insert(Self::GENERIC_READ_EXPANSION); - } - if access.contains(Self::GENERIC_WRITE) { - access.remove(Self::GENERIC_WRITE); - access.insert(Self::GENERIC_WRITE_EXPANSION); - } - if access.contains(Self::GENERIC_EXECUTE) { - access.remove(Self::GENERIC_EXECUTE); - access.insert(Self::GENERIC_EXECUTE_EXPANSION); - } - if access.contains(Self::GENERIC_ALL) { - access.remove(Self::GENERIC_ALL); - access.insert(Self::ALL_ACCESS); - } - access + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::GENERIC_READ_EXPANSION.bits(), + Self::GENERIC_WRITE_EXPANSION.bits(), + Self::GENERIC_EXECUTE_EXPANSION.bits(), + Self::ALL_ACCESS.bits(), + )) } fn open_flags( @@ -323,26 +307,11 @@ impl Task { } fn insert_file_handle(&self, file: FileObject) -> Result { - let typed = self - .global - .litebox - .descriptor_table_mut() - .insert::>(file); - insert_raw_handle::>( - &self.global.litebox, - &self.process.handles, - typed, - |file| self.close_file(file), - ) + self.insert_typed_handle::>(file, |file| self.close_file(file)) } pub(crate) fn close_file_handle(&self, handle: Handle) { - remove_raw_handle::>( - &self.global.litebox, - &self.process.handles, - handle, - |file| self.close_file(file), - ); + self.close_typed_handle::>(handle, |file| self.close_file(file)); } pub(crate) fn close_file(&self, file: FileObject) { @@ -755,6 +724,18 @@ fn create_directory_mode(file_attributes: u32) -> Mode { create_mode(file_attributes) | Mode::XUSR } +/// Convert the NT file-name forms we currently support at the object-manager to +/// filesystem seam. +/// +/// Native NT reaches this seam by walking object-manager directories until it +/// reaches a device object, then the device parse routine hands the remaining +/// path to the filesystem driver. LiteBox intentionally uses the Wine-style +/// shortcut here instead: known NT prefixes are recognized as strings and then +/// mapped directly into the sandbox filesystem. Today that includes `\??\`, +/// `\\?\`, any drive-letter prefix, both `\SystemRoot\` and `/SystemRoot/`, +/// `\Device\HarddiskVolume1\`, and `\Device\ConDrv\`. A unified object-manager +/// walk through device objects into the backing filesystem namespace remains +/// outside this file-path mapper. fn absolute_nt_file_name_to_fs_path(name: &str) -> Result { let mut name = name; if let Some(rest) = strip_case_insensitive_prefix(name, "\\??\\") { @@ -912,6 +893,7 @@ mod tests { use super::*; use crate::tests::{ TestFS, TestPlatform, const_ptr, mut_ptr, null_mut_ptr, object_attributes, unicode_string, + utf16_units as utf16, }; use litebox::fs::FileSystem as _; @@ -938,10 +920,6 @@ mod tests { ::run_test_thread(f) } - fn utf16(value: &str) -> std::vec::Vec { - value.encode_utf16().collect() - } - fn open_object_attributes( path: &str, ) -> ( diff --git a/litebox_shim_windows/src/syscalls/iocp.rs b/litebox_shim_windows/src/syscalls/iocp.rs index 57ccac2e66..5e475b60eb 100644 --- a/litebox_shim_windows/src/syscalls/iocp.rs +++ b/litebox_shim_windows/src/syscalls/iocp.rs @@ -12,10 +12,7 @@ use litebox_common_windows::nt_status::NtStatus; use crate::nt_types::{AccessMask, ObjectAttributes, read_object_attributes}; use crate::syscalls::Handle; -use crate::{ - ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, - remove_raw_handle, -}; +use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; bitflags::bitflags! { #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -36,26 +33,13 @@ bitflags::bitflags! { impl IoCompletionAccess { fn from_desired_access(desired_access: u32) -> Self { - let mut access = Self::from_bits_retain(desired_access); - if desired_access & AccessMask::GENERIC_READ.bits() != 0 { - access.insert(Self::READ); - } - if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { - access.insert(Self::WRITE); - } - if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { - access.insert(Self::EXECUTE); - } - if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { - access.insert(Self::ALL_ACCESS); - } - access.remove(Self::from_bits_retain( - AccessMask::GENERIC_READ.bits() - | AccessMask::GENERIC_WRITE.bits() - | AccessMask::GENERIC_EXECUTE.bits() - | AccessMask::GENERIC_ALL.bits(), - )); - access + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + )) } pub(crate) fn require(self, required: Self) -> Result<(), NtStatus> { @@ -123,29 +107,17 @@ impl Task { port: Arc>, granted_access: IoCompletionAccess, ) -> Result { - let typed = self - .global - .litebox - .descriptor_table_mut() - .insert::>(IoCompletionHandleObject { + self.insert_typed_handle::>( + IoCompletionHandleObject { port, granted_access, - }); - insert_raw_handle::>( - &self.global.litebox, - &self.process.handles, - typed, + }, drop, ) } pub(crate) fn close_io_completion_handle(&self, handle: Handle) { - remove_raw_handle::>( - &self.global.litebox, - &self.process.handles, - handle, - drop, - ); + self.close_typed_handle::>(handle, drop); } pub(crate) fn close_io_completion(io_completion: IoCompletionHandleObject) { diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 28ff98bd0c..461e68d5eb 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +pub(crate) mod directory; pub(crate) mod event; pub(crate) mod file; pub(crate) mod iocp; @@ -8,6 +9,7 @@ pub(crate) mod mm; pub(crate) mod nls; pub(crate) mod process; pub(crate) mod registry; +pub(crate) mod symlink; mod sysinfo; pub(crate) mod timer; pub(crate) mod wait_completion_packet; @@ -105,6 +107,48 @@ pub(crate) enum SyscallRequest { event_type: u32, initial_state: u8, }, + NtCreateDirectoryObject { + directory_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, + NtCreateDirectoryObjectEx { + directory_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + shadow_directory_handle: Handle, + flags: u32, + }, + NtOpenDirectoryObject { + directory_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, + NtQueryDirectoryObject { + directory_handle: Handle, + buffer: Platform::RawMutPointer, + buffer_length: u32, + return_single_entry: u8, + restart_scan: u8, + context: Platform::RawMutPointer, + return_length: Option>, + }, + NtCreateSymbolicLinkObject { + link_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + link_target: Platform::RawConstPointer, + }, + NtOpenSymbolicLinkObject { + link_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, + NtQuerySymbolicLinkObject { + link_handle: Handle, + link_target: Platform::RawMutPointer, + returned_length: Option>, + }, NtCreateIoCompletion { io_completion_handle: Platform::RawMutPointer, desired_access: u32, @@ -366,6 +410,48 @@ impl SyscallRequest { event_type, initial_state, })), + NtSysno::NtCreateDirectoryObject => Some(sys_req!(NtCreateDirectoryObject { + directory_handle:*, + desired_access, + object_attributes:*, + })), + NtSysno::NtCreateDirectoryObjectEx => Some(sys_req!(NtCreateDirectoryObjectEx { + directory_handle:*, + desired_access, + object_attributes:*, + shadow_directory_handle:{Handle::from_raw}, + flags, + })), + NtSysno::NtOpenDirectoryObject => Some(sys_req!(NtOpenDirectoryObject { + directory_handle:*, + desired_access, + object_attributes:*, + })), + NtSysno::NtQueryDirectoryObject => Some(sys_req!(NtQueryDirectoryObject { + directory_handle:{Handle::from_raw}, + buffer:*, + buffer_length, + return_single_entry, + restart_scan, + context:*, + return_length:*, + })), + NtSysno::NtCreateSymbolicLinkObject => Some(sys_req!(NtCreateSymbolicLinkObject { + link_handle:*, + desired_access, + object_attributes:*, + link_target:*, + })), + NtSysno::NtOpenSymbolicLinkObject => Some(sys_req!(NtOpenSymbolicLinkObject { + link_handle:*, + desired_access, + object_attributes:*, + })), + NtSysno::NtQuerySymbolicLinkObject => Some(sys_req!(NtQuerySymbolicLinkObject { + link_handle:{Handle::from_raw}, + link_target:*, + returned_length:*, + })), NtSysno::NtCreateIoCompletion => Some(sys_req!(NtCreateIoCompletion { io_completion_handle:*, desired_access, diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 375cb170a0..83ab735168 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -44,9 +44,7 @@ use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::syscalls::Handle; -use crate::{ - ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, raw_handle_entry, remove_raw_handle, -}; +use crate::{ConstPtr, MutPtr, ShimFS, Task, raw_handle_entry}; use crate::nt_types::{AccessMask, ObjectAttributes, UnicodeString, read_object_attributes}; @@ -364,26 +362,15 @@ impl Task { &self, key: RegistryKeyObject, ) -> Result { - let typed = self - .global - .litebox - .descriptor_table_mut() - .insert::>(key); - insert_raw_handle::>( - &self.global.litebox, - &self.process.handles, - typed, - |key| self.close_registry_key(key), - ) + self.insert_typed_handle::>(key, |key| { + self.close_registry_key(key); + }) } pub(crate) fn close_registry_key_handle(&self, handle: Handle) { - remove_raw_handle::>( - &self.global.litebox, - &self.process.handles, - handle, - |key| self.close_registry_key(key), - ); + self.close_typed_handle::>(handle, |key| { + self.close_registry_key(key); + }); } pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { @@ -841,7 +828,7 @@ fn map_read_error(error: ReadError) -> NtStatus { mod tests { use crate::tests::{ TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, object_attributes, test_platform, - unicode_string, + unicode_string, utf16_units as utf16, }; use super::*; @@ -908,10 +895,6 @@ mod tests { fn RegDeleteTreeW(hKey: *mut core::ffi::c_void, lpSubKey: *const u16) -> i32; } - fn utf16(value: &str) -> std::vec::Vec { - value.encode_utf16().collect() - } - fn test_registry() -> (LiteBox, RegistryStore) { let litebox = LiteBox::new(test_platform()); let registry = RegistryStore::new(&litebox); @@ -934,7 +917,7 @@ mod tests { #[cfg(all(target_os = "windows", target_arch = "x86_64"))] fn nul_terminated_utf16(value: &str) -> Vec { - let mut value: Vec = value.encode_utf16().collect(); + let mut value = utf16(value); value.push(0); value } @@ -1125,7 +1108,7 @@ mod tests { value_name, KeyValueInformationClass::Partial, mut_byte_ptr(&mut information), - u32::try_from(information.len()).unwrap(), + information.len().trunc(), mut_ptr(&mut result_length), ) .is_ok() @@ -1228,7 +1211,7 @@ mod tests { value_name, KeyValueInformationClass::Partial, mut_byte_ptr(&mut information), - u32::try_from(information.len()).unwrap(), + information.len().trunc(), mut_ptr(&mut result_length), ) .is_ok() @@ -1241,7 +1224,7 @@ mod tests { value_name, KeyValueInformationClass::Partial, mut_byte_ptr(&mut information), - u32::try_from(information.len()).unwrap(), + information.len().trunc(), mut_ptr(&mut result_length), ) .unwrap_err(), @@ -1264,7 +1247,7 @@ mod tests { value_name, KeyValueInformationClass::Partial, mut_byte_ptr(&mut information), - u32::try_from(information.len()).unwrap(), + information.len().trunc(), mut_ptr(&mut result_length), ) .is_ok() @@ -1279,10 +1262,7 @@ mod tests { KeyValuePartialInformation::read_from_prefix(information).unwrap(); assert_eq!(information.title_index, 0); assert_eq!(information.value_type, RegistryValueType::Sz.into()); - assert_eq!( - information.data_length, - u32::try_from(DEFAULT_ACP_VALUE.len()).unwrap() - ); + assert_eq!(information.data_length, DEFAULT_ACP_VALUE.len().trunc()); assert_eq!(data, DEFAULT_ACP_VALUE); } @@ -1309,7 +1289,7 @@ mod tests { value_name, KeyValueInformationClass::Partial, mut_byte_ptr(&mut information), - u32::try_from(information.len()).unwrap(), + information.len().trunc(), mut_ptr(&mut result_length), ) .unwrap_err(), @@ -1333,7 +1313,7 @@ mod tests { value_name, KeyValueInformationClass::Basic, mut_byte_ptr(&mut basic_information), - u32::try_from(basic_information.len()).unwrap(), + basic_information.len().trunc(), mut_ptr(&mut result_length), ) .is_ok() @@ -1357,7 +1337,7 @@ mod tests { value_name, KeyValueInformationClass::Full, mut_byte_ptr(&mut full_information), - u32::try_from(full_information.len()).unwrap(), + full_information.len().trunc(), mut_ptr(&mut result_length), ) .is_ok() @@ -1365,7 +1345,7 @@ mod tests { let full_information = &full_information[..(result_length as usize)]; let (full_header, full_tail) = KeyValueFullInformation::read_from_prefix(full_information).unwrap(); - let data_offset = usize::try_from(full_header.data_offset).unwrap(); + let data_offset = full_header.data_offset as usize; assert_eq!(full_header.title_index, 0); assert_eq!(full_header.value_type, RegistryValueType::Sz.into()); assert_eq!(full_header.data_length as usize, DEFAULT_OEMCP_VALUE.len()); @@ -1395,7 +1375,7 @@ mod tests { value_name, KeyValueInformationClass::Partial, mut_byte_ptr(&mut information), - u32::try_from(information.len()).unwrap(), + information.len().trunc(), mut_ptr(&mut result_length), ) .unwrap_err(), @@ -1408,7 +1388,7 @@ mod tests { missing_value_name, KeyValueInformationClass::Partial, mut_byte_ptr(&mut information), - u32::try_from(information.len()).unwrap(), + information.len().trunc(), mut_ptr(&mut result_length), ) .unwrap_err(), @@ -1421,7 +1401,7 @@ mod tests { const_ptr(&value_name), 0xffff, mut_byte_ptr(&mut information), - u32::try_from(information.len()).unwrap(), + information.len().trunc(), mut_ptr(&mut result_length), ), NtStatus::INVALID_INFO_CLASS @@ -1433,7 +1413,7 @@ mod tests { value_name, KeyValueInformationClass::Partial, mut_byte_ptr(&mut short_information), - u32::try_from(short_information.len()).unwrap(), + short_information.len().trunc(), mut_ptr(&mut result_length), ) .unwrap_err(), diff --git a/litebox_shim_windows/src/syscalls/symlink.rs b/litebox_shim_windows/src/syscalls/symlink.rs new file mode 100644 index 0000000000..55e3111438 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/symlink.rs @@ -0,0 +1,876 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NT object-manager symbolic-link syscalls. + +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; +use core::marker::PhantomData; +use core::mem::size_of; + +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; +use litebox::utils::TruncateExt as _; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::{AccessMask, ObjectAttributes, ObjectAttributesFlags, UnicodeString}; +use crate::syscalls::directory::ObjectNode; +use crate::syscalls::{Handle, directory::DirectoryName}; +use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; + +const STANDARD_RIGHTS_REQUIRED: u32 = AccessMask::DELETE.bits() + | AccessMask::READ_CONTROL.bits() + | AccessMask::WRITE_DAC.bits() + | AccessMask::WRITE_OWNER.bits(); + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct SymbolicLinkAccess: u32 { + const QUERY = 0x0001; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() | Self::QUERY.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits(); + const EXECUTE = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() | Self::QUERY.bits(); + const ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | Self::QUERY.bits(); + + const _ = !0; + } +} + +impl SymbolicLinkAccess { + fn from_desired_access(desired_access: u32) -> Self { + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + )) + } + + fn require(self, required: Self) -> Result<(), NtStatus> { + if self.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +pub(crate) struct SymbolicLinkSubsystem(PhantomData); + +impl FdEnabledSubsystem for SymbolicLinkSubsystem { + type Entry = SymbolicLinkHandleObject; +} + +impl FdEnabledSubsystemEntry for SymbolicLinkHandleObject {} + +pub(crate) struct SymbolicLinkHandleObject { + link: Arc>, + granted_access: SymbolicLinkAccess, +} + +fn utf16_units(value: &str) -> Result, NtStatus> { + let units: Vec = value.encode_utf16().collect(); + if units + .len() + .checked_mul(size_of::()) + .is_none_or(|len| len > u16::MAX as usize) + { + return Err(NtStatus::NAME_TOO_LONG); + } + Ok(units) +} + +impl Task { + fn symbolic_link_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + self.typed_handle_entry::>(handle) + } + + fn insert_symbolic_link_handle( + &self, + link: Arc>, + granted_access: SymbolicLinkAccess, + ) -> Result { + self.insert_typed_handle::>( + SymbolicLinkHandleObject { + link, + granted_access, + }, + drop, + ) + } + + fn close_symbolic_link_handle(&self, handle: Handle) { + self.close_typed_handle::>(handle, drop); + } + + pub(crate) fn close_symbolic_link(link: SymbolicLinkHandleObject) { + drop(link); + } + + pub(crate) fn sys_nt_create_symbolic_link_object( + &self, + link_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + link_target: ConstPtr, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(link_handle) { + return status; + } + let (object_attributes, link_name) = + match self.read_directory_object_attributes(object_attributes, true) { + Ok((Some(object_attributes), Some(link_name))) => (object_attributes, link_name), + Ok((_, None)) => return NtStatus::OBJECT_NAME_INVALID, + Ok((None, Some(_))) => return NtStatus::INVALID_PARAMETER, + Err(status) => return status, + }; + let target = match link_target.read_at_offset(0) { + Some(target) => match read_symbolic_link_target::(target) { + Ok(target) => target, + Err(status) => return status, + }, + None => return NtStatus::ACCESS_VIOLATION, + }; + let attributes = ObjectAttributesFlags::from_bits_retain(object_attributes.attributes); + if attributes.contains(ObjectAttributesFlags::OPENLINK) { + return NtStatus::INVALID_PARAMETER; + } + + // NT stores the target as an opaque string at creation time. Wine's + // create_symlink only copies the target and ReactOS leaves LinkTargetObject + // null; both defer namespace lookup until the link is traversed. + let granted_access = SymbolicLinkAccess::from_desired_access(desired_access); + self.create_symbolic_link( + link_handle, + granted_access, + link_name, + target, + attributes.contains(ObjectAttributesFlags::OPENIF), + ) + } + + fn create_symbolic_link( + &self, + link_handle: MutPtr, + granted_access: SymbolicLinkAccess, + link_name: DirectoryName, + target: String, + open_if: bool, + ) -> NtStatus { + self.process.directory_namespace.create_symlink( + &link_name.original_path, + target, + |link| { + if !open_if { + return NtStatus::OBJECT_NAME_COLLISION; + } + let Ok(handle) = self.insert_symbolic_link_handle(link, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if link_handle.write_at_offset(0, handle).is_none() { + self.close_symbolic_link_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::OBJECT_NAME_EXISTS + }, + |link| { + let Ok(handle) = self.insert_symbolic_link_handle(link, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if link_handle.write_at_offset(0, handle).is_none() { + self.close_symbolic_link_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + }, + ) + } + + pub(crate) fn sys_nt_open_symbolic_link_object( + &self, + link_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(link_handle) { + return status; + } + let link_name = match self.read_directory_object_attributes(object_attributes, true) { + Ok((Some(_), Some(link_name))) => link_name, + Ok((_, None)) => return NtStatus::OBJECT_NAME_INVALID, + Ok((None, Some(_))) => return NtStatus::INVALID_PARAMETER, + Err(status) => return status, + }; + let link = match self + .process + .directory_namespace + .resolve_symlink(&link_name.original_path, true) + { + Ok(link) => link, + Err(status) => return status, + }; + let Ok(handle) = self.insert_symbolic_link_handle( + link, + SymbolicLinkAccess::from_desired_access(desired_access), + ) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if link_handle.write_at_offset(0, handle).is_none() { + self.close_symbolic_link_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_query_symbolic_link_object( + &self, + link_handle: Handle, + link_target: MutPtr, + returned_length: Option>, + ) -> NtStatus { + let entry = match self.symbolic_link_entry(link_handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + if let Err(status) = + entry.with_entry(|entry| entry.granted_access.require(SymbolicLinkAccess::QUERY)) + { + return status; + } + if let Err(status) = probe_guest_output_preserving_value::(link_target) { + return status; + } + if let Some(returned_length) = returned_length + && let Err(status) = probe_guest_output_preserving_value::(returned_length) + { + return status; + } + + let target = entry.with_entry(|entry| entry.link.symlink_target()); + let target = match target { + Ok(target) => target, + Err(status) => return status, + }; + let units = match utf16_units(&target) { + Ok(units) => units, + Err(status) => return status, + }; + let required_len = units + .len() + .checked_mul(size_of::()) + .ok_or(NtStatus::NAME_TOO_LONG); + let Ok(required_len) = required_len else { + return NtStatus::NAME_TOO_LONG; + }; + let required = match units + .len() + .checked_add(1) + .and_then(|units| units.checked_mul(size_of::())) + .and_then(|bytes| u32::try_from(bytes).ok()) + { + Some(required) if u16::try_from(required).is_ok() => required, + _ => return NtStatus::NAME_TOO_LONG, + }; + let Some(mut unicode) = link_target.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + + if let Some(returned_length) = returned_length + && returned_length.write_at_offset(0, required).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if required > u32::from(unicode.maximum_length) { + return NtStatus::BUFFER_TOO_SMALL; + } + if required != 0 && unicode.buffer == 0 { + return NtStatus::ACCESS_VIOLATION; + } + + let mut output_units = units; + output_units.push(0); + let target_buffer = MutPtr::::from_usize(unicode.buffer); + target_buffer + .write_slice_at_offset(0, &output_units) + .ok_or(NtStatus::ACCESS_VIOLATION) + .map_or_else( + |status| status, + |()| { + unicode.length = required_len.trunc(); + if link_target.write_at_offset(0, unicode).is_none() { + NtStatus::ACCESS_VIOLATION + } else { + NtStatus::SUCCESS + } + }, + ) + } +} + +fn read_symbolic_link_target( + target: UnicodeString, +) -> Result { + // ReactOS rounds odd MaximumLength down before validating this UNICODE_STRING; + // Wine's object-manager tests cover the zero MaximumLength rejection. + let maximum_length = target.maximum_length & !1u16; + if !target.length.is_multiple_of(2) || maximum_length < target.length || maximum_length == 0 { + return Err(NtStatus::INVALID_PARAMETER); + } + + let target = target.read_string::()?; + if target.is_empty() { + return Err(NtStatus::INVALID_PARAMETER); + } + Ok(target) +} + +#[cfg(test)] +mod tests { + use core::mem::size_of_val; + + use litebox::platform::ThreadProvider; + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::{ObjectAttributes, ObjectAttributesFlags, UnicodeString}; + use crate::tests::{ + TestPlatform, const_ptr, mut_ptr, object_attributes, test_task, unicode_string, + utf16_units as test_utf16_units, + }; + + const SYMBOLIC_LINK_QUERY: u32 = 0x0000_0001; + const SYMBOLIC_LINK_ALL_ACCESS: u32 = 0x000f_0001; + const DIRECTORY_QUERY: u32 = 0x0000_0001; + const DIRECTORY_ALL_ACCESS: u32 = 0x000f_000f; + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = crate::tests::test_platform(); + ::run_test_thread(f) + } + + fn link_target(value: &str) -> (Vec, UnicodeString) { + let units = test_utf16_units(value); + let unicode = unicode_string(&units); + (units, unicode) + } + + fn create_link( + task: &Task, + path: &str, + target: &str, + ) -> Handle { + let path_units = test_utf16_units(path); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let (_target_units, target) = link_target(target); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_symbolic_link_object( + mut_ptr(&mut handle), + SYMBOLIC_LINK_ALL_ACCESS, + Some(const_ptr(&attrs)), + const_ptr(&target), + ), + NtStatus::SUCCESS + ); + handle + } + + fn create_directory(task: &Task, path: &str) -> Handle { + let path_units = test_utf16_units(path); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut handle), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&attrs)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + handle + } + + fn open_directory(task: &Task, path: &str) -> Handle { + let path_units = test_utf16_units(path); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut handle), + DIRECTORY_QUERY, + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + handle + } + + fn open_link(task: &Task, path: &str) -> Handle { + let path_units = test_utf16_units(path); + let name = unicode_string(&path_units); + let attrs = object_attributes( + &name, + (ObjectAttributesFlags::CASE_INSENSITIVE | ObjectAttributesFlags::OPENLINK).bits(), + ); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_symbolic_link_object( + mut_ptr(&mut handle), + SYMBOLIC_LINK_QUERY, + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + handle + } + + fn open_link_without_openlink( + task: &Task, + path: &str, + ) -> Handle { + let path_units = test_utf16_units(path); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_symbolic_link_object( + mut_ptr(&mut handle), + SYMBOLIC_LINK_QUERY, + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + handle + } + + fn query_link( + task: &Task, + handle: Handle, + output_units: &mut [u16], + ) -> (UnicodeString, u32) { + let mut target = UnicodeString { + length: u16::MAX, + maximum_length: size_of_val(output_units).trunc(), + padding_0: [0; 4], + buffer: output_units.as_mut_ptr() as usize, + }; + let original_buffer = target.buffer; + let original_maximum_length = target.maximum_length; + let mut returned_length = u32::MAX; + assert_eq!( + task.sys_nt_query_symbolic_link_object( + handle, + mut_ptr(&mut target), + Some(mut_ptr(&mut returned_length)), + ), + NtStatus::SUCCESS + ); + assert_eq!(target.buffer, original_buffer); + assert_eq!(target.maximum_length, original_maximum_length); + assert!(target.length as usize <= size_of_val(output_units)); + assert_eq!(output_units[target.length as usize / 2], 0); + (target, returned_length) + } + + #[test] + fn create_open_and_query_symbolic_link_round_trips_target() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let target = r"\BaseNamedObjects\LiteBoxTarget"; + let created = create_link(&task, r"\BaseNamedObjects\LiteBoxSymlink", target); + let opened = open_link(&task, r"\BaseNamedObjects\LiteBoxSymlink"); + let mut output = alloc::vec![0u16; target.encode_utf16().count() + 1]; + let (target, returned_length) = query_link(&task, opened, &mut output); + assert_eq!(returned_length, u32::from(target.length) + 2); + assert_eq!( + String::from_utf16_lossy(&output[..target.length as usize / 2]), + r"\BaseNamedObjects\LiteBoxTarget" + ); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(created), NtStatus::SUCCESS); + }); + } + + #[test] + fn create_symbolic_link_rejects_empty_target() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let path_units = test_utf16_units(r"\BaseNamedObjects\LiteBoxEmptyTarget"); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let empty_target = unicode_string(&[]); + let mut handle = Handle::default(); + + assert_eq!( + task.sys_nt_create_symbolic_link_object( + mut_ptr(&mut handle), + SYMBOLIC_LINK_ALL_ACCESS, + Some(const_ptr(&attrs)), + const_ptr(&empty_target), + ), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::default()); + }); + } + + #[test] + fn create_symbolic_link_rejects_zero_target_maximum_length() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let path_units = test_utf16_units(r"\BaseNamedObjects\LiteBoxZeroTargetMax"); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let (_target_units, mut target) = link_target(r"\BaseNamedObjects\Target"); + let mut handle = Handle::default(); + target.maximum_length = 0; + + assert_eq!( + task.sys_nt_create_symbolic_link_object( + mut_ptr(&mut handle), + SYMBOLIC_LINK_ALL_ACCESS, + Some(const_ptr(&attrs)), + const_ptr(&target), + ), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::default()); + }); + } + + #[test] + fn create_symbolic_link_rejects_target_maximum_length_shorter_than_length() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let path_units = test_utf16_units(r"\BaseNamedObjects\LiteBoxShortTargetMax"); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let (_target_units, mut target) = link_target(r"\BaseNamedObjects\Target"); + let mut handle = Handle::default(); + target.maximum_length = target.length - 2; + + assert_eq!( + task.sys_nt_create_symbolic_link_object( + mut_ptr(&mut handle), + SYMBOLIC_LINK_ALL_ACCESS, + Some(const_ptr(&attrs)), + const_ptr(&target), + ), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::default()); + }); + } + + #[test] + fn create_symbolic_link_allows_odd_target_maximum_length_after_rounding() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let path_units = test_utf16_units(r"\BaseNamedObjects\LiteBoxOddTargetMax"); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let (_target_units, mut target) = link_target(r"\BaseNamedObjects\Target"); + let mut handle = Handle::default(); + target.maximum_length = target.length + 1; + + assert_eq!( + task.sys_nt_create_symbolic_link_object( + mut_ptr(&mut handle), + SYMBOLIC_LINK_ALL_ACCESS, + Some(const_ptr(&attrs)), + const_ptr(&target), + ), + NtStatus::SUCCESS + ); + assert_ne!(handle, Handle::default()); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + }); + } + + #[test] + fn open_symbolic_link_without_openlink_returns_final_link_itself() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let created = create_link( + &task, + r"\BaseNamedObjects\LiteBoxNoOpenLinkFinal", + r"\BaseNamedObjects\MissingTarget", + ); + let opened = + open_link_without_openlink(&task, r"\BaseNamedObjects\LiteBoxNoOpenLinkFinal"); + let mut output = [0u16; 64]; + let (target, _) = query_link(&task, opened, &mut output); + + assert_eq!( + String::from_utf16_lossy(&output[..target.length as usize / 2]), + r"\BaseNamedObjects\MissingTarget" + ); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(created), NtStatus::SUCCESS); + }); + } + + #[test] + fn open_symbolic_link_openlink_returns_final_link_itself() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let created = create_link( + &task, + r"\BaseNamedObjects\LiteBoxOpenLinkFinal", + r"\BaseNamedObjects\MissingTarget", + ); + let opened = open_link(&task, r"\BaseNamedObjects\LiteBoxOpenLinkFinal"); + let mut output = [0u16; 64]; + let (target, _) = query_link(&task, opened, &mut output); + + assert_eq!( + String::from_utf16_lossy(&output[..target.length as usize / 2]), + r"\BaseNamedObjects\MissingTarget" + ); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(created), NtStatus::SUCCESS); + }); + } + + #[test] + fn directory_open_follows_intermediate_symbolic_link() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let real = create_directory(&task, r"\BaseNamedObjects\LiteBoxRealDir"); + let child = create_directory(&task, r"\BaseNamedObjects\LiteBoxRealDir\Child"); + let link = create_link( + &task, + r"\BaseNamedObjects\LiteBoxDirLink", + r"\BaseNamedObjects\LiteBoxRealDir", + ); + + let opened = open_directory(&task, r"\BaseNamedObjects\LiteBoxDirLink\Child"); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(link), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(child), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(real), NtStatus::SUCCESS); + }); + } + + #[test] + fn directory_create_follows_symlinked_parent() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let real = create_directory(&task, r"\BaseNamedObjects\LiteBoxCreateRealDir"); + let link = create_link( + &task, + r"\BaseNamedObjects\LiteBoxCreateDirLink", + r"\BaseNamedObjects\LiteBoxCreateRealDir", + ); + let created = create_directory(&task, r"\BaseNamedObjects\LiteBoxCreateDirLink\Child"); + let opened = open_directory(&task, r"\BaseNamedObjects\LiteBoxCreateRealDir\Child"); + + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(created), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(link), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(real), NtStatus::SUCCESS); + }); + } + + #[test] + fn dos_device_style_symbolic_link_resolves_through_seeded_directory() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let real = create_directory(&task, r"\BaseNamedObjects\LiteBoxDriveTarget"); + let child = create_directory(&task, r"\BaseNamedObjects\LiteBoxDriveTarget\Child"); + let link = create_link(&task, r"\??\C:", r"\BaseNamedObjects\LiteBoxDriveTarget"); + + let opened = open_directory(&task, r"\??\C:\Child"); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(link), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(child), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(real), NtStatus::SUCCESS); + }); + } + + #[test] + fn open_symbolic_link_rejects_directory_type() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let path_units = test_utf16_units(r"\BaseNamedObjects"); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut handle = Handle::default(); + + assert_eq!( + task.sys_nt_open_symbolic_link_object( + mut_ptr(&mut handle), + SYMBOLIC_LINK_QUERY, + Some(const_ptr(&attrs)), + ), + NtStatus::OBJECT_TYPE_MISMATCH + ); + assert_eq!(handle, Handle::default()); + }); + } + + #[test] + fn create_symbolic_link_obeys_collision_and_openif() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let first = create_link( + &task, + r"\BaseNamedObjects\LiteBoxOpenIfSymlink", + r"\BaseNamedObjects\Target", + ); + let path_units = test_utf16_units(r"\BaseNamedObjects\LiteBoxOpenIfSymlink"); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let (_target_units, target) = link_target(r"\BaseNamedObjects\Target"); + let mut collision = Handle::default(); + + assert_eq!( + task.sys_nt_create_symbolic_link_object( + mut_ptr(&mut collision), + SYMBOLIC_LINK_ALL_ACCESS, + Some(const_ptr(&attrs)), + const_ptr(&target), + ), + NtStatus::OBJECT_NAME_COLLISION + ); + assert_eq!(collision, Handle::default()); + + let openif_attrs = ObjectAttributes { + attributes: (ObjectAttributesFlags::CASE_INSENSITIVE + | ObjectAttributesFlags::OPENIF) + .bits(), + ..attrs + }; + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_create_symbolic_link_object( + mut_ptr(&mut opened), + SYMBOLIC_LINK_ALL_ACCESS, + Some(const_ptr(&openif_attrs)), + const_ptr(&target), + ), + NtStatus::OBJECT_NAME_EXISTS + ); + assert_ne!(opened, Handle::default()); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(first), NtStatus::SUCCESS); + }); + } + + #[test] + fn create_symbolic_link_rejects_existing_directory_type() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let path_units = test_utf16_units(r"\BaseNamedObjects\LiteBoxSymlinkTypeDirectory"); + let name = unicode_string(&path_units); + let attrs = object_attributes(&name, ObjectAttributesFlags::CASE_INSENSITIVE.bits()); + let mut directory = Handle::default(); + assert_eq!( + task.sys_nt_create_directory_object( + mut_ptr(&mut directory), + DIRECTORY_ALL_ACCESS, + Some(const_ptr(&attrs)), + Handle::default(), + 0, + ), + NtStatus::SUCCESS + ); + + let (_target_units, target) = link_target(r"\BaseNamedObjects\Target"); + let mut link = Handle::default(); + assert_eq!( + task.sys_nt_create_symbolic_link_object( + mut_ptr(&mut link), + SYMBOLIC_LINK_ALL_ACCESS, + Some(const_ptr(&attrs)), + const_ptr(&target), + ), + NtStatus::OBJECT_TYPE_MISMATCH + ); + assert_eq!(link, Handle::default()); + assert_eq!(task.sys_nt_close(directory), NtStatus::SUCCESS); + }); + } + + #[test] + fn query_symbolic_link_reports_too_small_without_mutating_output() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let handle = create_link( + &task, + r"\BaseNamedObjects\LiteBoxSmallSymlink", + r"\BaseNamedObjects\LongTarget", + ); + let mut output = [0xeeeeu16; 2]; + let mut target = UnicodeString { + length: 0x1234, + maximum_length: size_of_val(&output).trunc(), + padding_0: [0; 4], + buffer: output.as_mut_ptr() as usize, + }; + let original = target; + let mut returned_length = 0; + + assert_eq!( + task.sys_nt_query_symbolic_link_object( + handle, + mut_ptr(&mut target), + Some(mut_ptr(&mut returned_length)), + ), + NtStatus::BUFFER_TOO_SMALL + ); + assert_eq!(target.length, original.length); + assert_eq!(target.maximum_length, original.maximum_length); + assert_eq!(target.buffer, original.buffer); + assert_eq!(output, [0xeeeeu16; 2]); + assert_eq!( + returned_length, + ((r"\BaseNamedObjects\LongTarget".encode_utf16().count() + 1) * 2).trunc() + ); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + }); + } + + #[test] + fn query_symbolic_link_requires_space_for_trailing_nul() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let target = r"\BaseNamedObjects\ExactLengthTarget"; + let handle = create_link(&task, r"\BaseNamedObjects\LiteBoxExactSymlink", target); + let mut output = alloc::vec![0xeeeeu16; target.encode_utf16().count()]; + let mut target_string = UnicodeString { + length: 0x1234, + maximum_length: size_of_val(output.as_slice()).trunc(), + padding_0: [0; 4], + buffer: output.as_mut_ptr() as usize, + }; + let mut returned_length = 0; + + assert_eq!( + task.sys_nt_query_symbolic_link_object( + handle, + mut_ptr(&mut target_string), + Some(mut_ptr(&mut returned_length)), + ), + NtStatus::BUFFER_TOO_SMALL + ); + assert_eq!( + returned_length, + ((target.encode_utf16().count() + 1) * 2).trunc() + ); + assert!(output.iter().all(|unit| *unit == 0xeeee)); + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + }); + } +} diff --git a/litebox_shim_windows/src/syscalls/timer.rs b/litebox_shim_windows/src/syscalls/timer.rs index 607f99481b..91d97775c0 100644 --- a/litebox_shim_windows/src/syscalls/timer.rs +++ b/litebox_shim_windows/src/syscalls/timer.rs @@ -13,8 +13,7 @@ use litebox_common_windows::nt_status::NtStatus; use crate::nt_types::{AccessMask, ObjectAttributes}; use crate::syscalls::Handle; use crate::{ - ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, - raw_handle_entry, remove_raw_handle, + ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value, raw_handle_entry, }; const TIMER2_ATTRIBUTE_IR_TIMER: u32 = 0x0000_0002; @@ -46,26 +45,13 @@ bitflags::bitflags! { impl TimerAccess { fn from_desired_access(desired_access: u32) -> Self { - let mut access = Self::from_bits_retain(desired_access); - if desired_access & AccessMask::GENERIC_READ.bits() != 0 { - access.insert(Self::READ); - } - if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { - access.insert(Self::WRITE); - } - if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { - access.insert(Self::EXECUTE); - } - if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { - access.insert(Self::ALL_ACCESS); - } - access.remove(Self::from_bits_retain( - AccessMask::GENERIC_READ.bits() - | AccessMask::GENERIC_WRITE.bits() - | AccessMask::GENERIC_EXECUTE.bits() - | AccessMask::GENERIC_ALL.bits(), - )); - access + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + )) } } @@ -162,29 +148,17 @@ impl Task { timer: Arc>, granted_access: TimerAccess, ) -> Result { - let typed = self - .global - .litebox - .descriptor_table_mut() - .insert::>(TimerHandleObject { + self.insert_typed_handle::>( + TimerHandleObject { _timer: timer, granted_access, - }); - insert_raw_handle::>( - &self.global.litebox, - &self.process.handles, - typed, + }, drop, ) } pub(crate) fn close_timer_handle(&self, handle: Handle) { - remove_raw_handle::>( - &self.global.litebox, - &self.process.handles, - handle, - drop, - ); + self.close_typed_handle::>(handle, drop); } pub(crate) fn close_timer(timer: TimerHandleObject) { diff --git a/litebox_shim_windows/src/syscalls/wait_completion_packet.rs b/litebox_shim_windows/src/syscalls/wait_completion_packet.rs index 63b9d7b0ae..80b492536c 100644 --- a/litebox_shim_windows/src/syscalls/wait_completion_packet.rs +++ b/litebox_shim_windows/src/syscalls/wait_completion_packet.rs @@ -16,10 +16,7 @@ use crate::syscalls::Handle; use crate::syscalls::event::{EventAccess, EventSubsystem}; use crate::syscalls::iocp::{IoCompletionAccess, IoCompletionSubsystem}; use crate::syscalls::timer::{TimerAccess, TimerSubsystem}; -use crate::{ - ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, - remove_raw_handle, -}; +use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; const STANDARD_RIGHTS_REQUIRED: u32 = AccessMask::DELETE.bits() | AccessMask::READ_CONTROL.bits() @@ -42,26 +39,13 @@ bitflags::bitflags! { impl WaitCompletionPacketAccess { fn from_desired_access(desired_access: u32) -> Self { - let mut access = Self::from_bits_retain(desired_access); - if desired_access & AccessMask::GENERIC_READ.bits() != 0 { - access.insert(Self::READ); - } - if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { - access.insert(Self::WRITE); - } - if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { - access.insert(Self::EXECUTE); - } - if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { - access.insert(Self::ALL_ACCESS); - } - access.remove(Self::from_bits_retain( - AccessMask::GENERIC_READ.bits() - | AccessMask::GENERIC_WRITE.bits() - | AccessMask::GENERIC_EXECUTE.bits() - | AccessMask::GENERIC_ALL.bits(), - )); - access + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + )) } fn require(self, required: Self) -> Result<(), NtStatus> { @@ -274,29 +258,17 @@ impl Task { packet: Arc>, granted_access: WaitCompletionPacketAccess, ) -> Result { - let typed = self - .global - .litebox - .descriptor_table_mut() - .insert::>(WaitCompletionPacketHandleObject { + self.insert_typed_handle::>( + WaitCompletionPacketHandleObject { packet, granted_access, - }); - insert_raw_handle::>( - &self.global.litebox, - &self.process.handles, - typed, + }, drop, ) } pub(crate) fn close_wait_completion_packet_handle(&self, handle: Handle) { - remove_raw_handle::>( - &self.global.litebox, - &self.process.handles, - handle, - drop, - ); + self.close_typed_handle::>(handle, drop); } pub(crate) fn close_wait_completion_packet( diff --git a/litebox_shim_windows/src/syscalls/worker_factory.rs b/litebox_shim_windows/src/syscalls/worker_factory.rs index 22f62f0a51..0d49dcb94b 100644 --- a/litebox_shim_windows/src/syscalls/worker_factory.rs +++ b/litebox_shim_windows/src/syscalls/worker_factory.rs @@ -15,10 +15,7 @@ use litebox_common_windows::nt_status::NtStatus; use crate::nt_types::{AccessMask, ObjectAttributes, read_object_attributes}; use crate::syscalls::iocp::{IoCompletionAccess, IoCompletionObject, IoCompletionSubsystem}; use crate::syscalls::{Handle, ProcessHandle}; -use crate::{ - ConstPtr, MutPtr, ShimFS, Task, insert_raw_handle, probe_guest_output_preserving_value, - remove_raw_handle, -}; +use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; bitflags::bitflags! { #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -49,26 +46,13 @@ bitflags::bitflags! { impl WorkerFactoryAccess { fn from_desired_access(desired_access: u32) -> Self { - let mut access = Self::from_bits_retain(desired_access); - if desired_access & AccessMask::GENERIC_READ.bits() != 0 { - access.insert(Self::READ); - } - if desired_access & AccessMask::GENERIC_WRITE.bits() != 0 { - access.insert(Self::WRITE); - } - if desired_access & AccessMask::GENERIC_EXECUTE.bits() != 0 { - access.insert(Self::EXECUTE); - } - if desired_access & AccessMask::GENERIC_ALL.bits() != 0 { - access.insert(Self::ALL_ACCESS); - } - access.remove(Self::from_bits_retain( - AccessMask::GENERIC_READ.bits() - | AccessMask::GENERIC_WRITE.bits() - | AccessMask::GENERIC_EXECUTE.bits() - | AccessMask::GENERIC_ALL.bits(), - )); - access + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + )) } fn require(self, required: Self) -> Result<(), NtStatus> { @@ -241,29 +225,17 @@ impl Task { factory: Arc>, granted_access: WorkerFactoryAccess, ) -> Result { - let typed = self - .global - .litebox - .descriptor_table_mut() - .insert::>(WorkerFactoryHandleObject { + self.insert_typed_handle::>( + WorkerFactoryHandleObject { factory, granted_access, - }); - insert_raw_handle::>( - &self.global.litebox, - &self.process.handles, - typed, + }, drop, ) } pub(crate) fn close_worker_factory_handle(&self, handle: Handle) { - remove_raw_handle::>( - &self.global.litebox, - &self.process.handles, - handle, - drop, - ); + self.close_typed_handle::>(handle, drop); } pub(crate) fn close_worker_factory(worker_factory: WorkerFactoryHandleObject) { diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 739fb8d93a..c76ad3e2af 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -5,6 +5,7 @@ extern crate std; use alloc::collections::BTreeMap; use alloc::sync::Arc; +use alloc::vec::Vec; use core::marker::PhantomData; use core::mem::size_of; use core::sync::atomic::{AtomicI32, AtomicU32}; @@ -12,6 +13,7 @@ use litebox::LiteBox; use litebox::fd::RawDescriptorStorage; use litebox::fs::{FileSystem as _, Mode, OFlags}; use litebox::platform::RawConstPointer as _; +use litebox::utils::TruncateExt as _; use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::syscalls::Handle; @@ -50,7 +52,7 @@ pub(crate) fn null_mut_ptr() -> Mu } pub(crate) fn unicode_string(units: &[u16]) -> UnicodeString { - let byte_len = u16::try_from(core::mem::size_of_val(units)).expect("test name fits in USHORT"); + let byte_len = core::mem::size_of_val(units).trunc(); UnicodeString { length: byte_len, maximum_length: byte_len, @@ -59,10 +61,13 @@ pub(crate) fn unicode_string(units: &[u16]) -> UnicodeString { } } +pub(crate) fn utf16_units(value: &str) -> Vec { + value.encode_utf16().collect() +} + pub(crate) fn object_attributes(name: &UnicodeString, attributes: u32) -> ObjectAttributes { ObjectAttributes { - length: u32::try_from(size_of::()) - .expect("OBJECT_ATTRIBUTES fits in ULONG"), + length: size_of::().trunc(), root_directory: Handle::default(), object_name: core::ptr::from_ref(name) as usize, attributes, @@ -134,6 +139,7 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task Task::new(RawDescriptorStorage::new()), + directory_namespace, event_namespace: crate::WindowsEventNamespace::::new(BTreeMap::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), virtual_allocations: crate::WindowsVirtualAllocations::::new( From 049807a19edc9d2fb80a148fc67b765e4d46935f Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 30 Jun 2026 10:33:17 -0700 Subject: [PATCH 075/319] Seed KnownDllPath symbolic link (#986) Predefine `\\KnownDlls\\KnownDllPath` as C:\Windows\System32 so ntdll loader initialization can open and query the known-DLL search path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/syscalls/directory.rs | 21 +++++++++++++++++++ litebox_shim_windows/src/syscalls/symlink.rs | 18 ++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/litebox_shim_windows/src/syscalls/directory.rs b/litebox_shim_windows/src/syscalls/directory.rs index c0eaafd651..853056a618 100644 --- a/litebox_shim_windows/src/syscalls/directory.rs +++ b/litebox_shim_windows/src/syscalls/directory.rs @@ -50,6 +50,11 @@ const SEEDED_DIRECTORY_PATHS: &[&str] = &[ r"\Sessions\BNOLINKS", ]; +// Wine's wineboot and ReactOS SMSS create KnownDllPath so ntdll can open/query +// the DOS path prefix for known DLL lookups during loader initialization. +const SEEDED_SYMLINK_PATHS: &[(&str, &str)] = + &[(r"\KnownDlls\KnownDllPath", r"C:\Windows\System32")]; + bitflags::bitflags! { #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct DirectoryAccess: u32 { @@ -429,6 +434,19 @@ impl DirectoryNamespace { ); } + fn seed_symlink(&self, path: &str, target: &str) { + let status = self.create_symlink( + path, + target.to_string(), + |_| NtStatus::SUCCESS, + |_| NtStatus::SUCCESS, + ); + assert!( + status == NtStatus::SUCCESS, + "seeded NT object symbolic link must have seeded ancestors: {status:?}" + ); + } + fn resolve_tail( &self, tail: &str, @@ -1065,6 +1083,9 @@ pub(crate) fn seed_directory_namespace() for path in SEEDED_DIRECTORY_PATHS { namespace.seed_directory(path); } + for (path, target) in SEEDED_SYMLINK_PATHS { + namespace.seed_symlink(path, target); + } namespace } diff --git a/litebox_shim_windows/src/syscalls/symlink.rs b/litebox_shim_windows/src/syscalls/symlink.rs index 55e3111438..a9c3f115ca 100644 --- a/litebox_shim_windows/src/syscalls/symlink.rs +++ b/litebox_shim_windows/src/syscalls/symlink.rs @@ -502,6 +502,24 @@ mod tests { }); } + #[test] + fn predefined_known_dll_path_symbolic_link_matches_loader_contract() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let opened = open_link(&task, r"\KnownDlls\KnownDllPath"); + let target = r"C:\Windows\System32"; + let mut output = alloc::vec![0u16; target.encode_utf16().count() + 1]; + let (target_string, returned_length) = query_link(&task, opened, &mut output); + + assert_eq!(returned_length, u32::from(target_string.length) + 2); + assert_eq!( + String::from_utf16_lossy(&output[..target_string.length as usize / 2]), + target + ); + assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); + }); + } + #[test] fn create_symbolic_link_rejects_empty_target() { run_with_test_platform_pointers(|| { From f797a65c961bb8571b3c30bcc7419cd366eb8060 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 30 Jun 2026 13:29:34 -0700 Subject: [PATCH 076/319] Handle NtQueryVolumeInformationFile device info (#988) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 16 + litebox_shim_windows/src/syscalls/file.rs | 502 +++++++++++++++++++++- litebox_shim_windows/src/syscalls/mod.rs | 14 + 3 files changed, 530 insertions(+), 2 deletions(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 5ee53e4f92..1ed32598ae 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -881,6 +881,22 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtQueryVolumeInformationFile { + file_handle, + io_status_block, + fs_information, + length, + fs_information_class, + } => { + let status = self.sys_nt_query_volume_information_file( + file_handle, + io_status_block, + fs_information, + length, + fs_information_class, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtOpenKey { key_handle, desired_access, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 8fd9104cbf..76b48ccb48 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -3,6 +3,7 @@ use alloc::string::String; use core::marker::PhantomData; +use core::mem::size_of; use int_enum::IntEnum; use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}; @@ -10,6 +11,7 @@ use litebox::fs::errors::{FileStatusError, MkdirError, OpenError, PathError}; use litebox::fs::{FileType, Mode, OFlags}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::nt_types::{ AccessMask, IoStatusBlock, ObjectAttributes, UnicodeString, read_object_attributes, @@ -31,6 +33,23 @@ const CONDRV_SERVER_DEVICE: &str = "Server"; const CONDRV_REFERENCE_OBJECT: &str = "Reference"; const CONDRV_CONNECT_OBJECT: &str = "Connect"; +// These names and values are Windows ABI constants from WDK headers; Wine's +// regular file/directory branch and ReactOS' filesystem device query path use +// the same FILE_DEVICE_* and FILE_DEVICE_IS_MOUNTED vocabulary. +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum FileDeviceType { + Disk = 0x0000_0007, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct FileDeviceCharacteristics: u32 { + const IS_MOUNTED = 0x0000_0020; + const _ = !0; + } +} + #[repr(usize)] #[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] enum FileCreateInformation { @@ -45,6 +64,25 @@ enum FileCreateInformation { DoesNotExist = 5, } +// TODO: NtSetVolumeInformationFile and sibling query classes +// (FileFsVolumeInformation=1, FileFsSizeInformation=3, FileFsAttributeInformation=5) +// are deferred until a guest exercises them; each needs host-grounded volume +// metadata LiteBox does not model yet. Add the variant and match arm when that boundary lands. +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum FsInformationClass { + FileFsDeviceInformation = 4, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +struct FileFsDeviceInformation { + device_type: u32, + characteristics: u32, +} + +const _: () = assert!(size_of::() == 8); + pub(crate) struct FileObjectSubsystem(PhantomData); impl FdEnabledSubsystem for FileObjectSubsystem { @@ -410,6 +448,87 @@ impl Task { }) } + pub(crate) fn sys_nt_query_volume_information_file( + &self, + file_handle: Handle, + io_status_block: MutPtr, + fs_information: MutPtr, + length: u32, + fs_information_class: u32, + ) -> NtStatus { + let Ok(fs_information_class) = FsInformationClass::try_from(fs_information_class) else { + litebox_util_log::debug!( + fs_information_class = fs_information_class; + "Unsupported NtQueryVolumeInformationFile class" + ); + return NtStatus::INVALID_INFO_CLASS; + }; + + let status = match fs_information_class { + FsInformationClass::FileFsDeviceInformation => self.write_file_fs_device_information( + file_handle, + io_status_block, + fs_information, + length, + ), + }; + + if status == NtStatus::SUCCESS { + litebox_util_log::debug!( + file_handle = file_handle.as_raw(), + length = length, + fs_information_class:? = fs_information_class; + "Handled NtQueryVolumeInformationFile syscall" + ); + } + + status + } + + fn write_file_fs_device_information( + &self, + file_handle: Handle, + io_status_block: MutPtr, + fs_information: MutPtr, + length: u32, + ) -> NtStatus { + if length < u32::try_from(size_of::()).unwrap() { + return NtStatus::INFO_LENGTH_MISMATCH; + } + + let fs_information = + MutPtr::::from_usize(fs_information.as_usize()); + if probe_guest_output_preserving_value::(io_status_block).is_err() + || probe_guest_output_preserving_value::( + fs_information, + ) + .is_err() + { + return NtStatus::ACCESS_VIOLATION; + } + + let Ok(_file) = self.file_entry(file_handle) else { + return NtStatus::INVALID_HANDLE; + }; + + let info = FileFsDeviceInformation { + device_type: FileDeviceType::Disk as u32, + characteristics: FileDeviceCharacteristics::IS_MOUNTED.bits(), + }; + if fs_information.write_at_offset(0, info).is_none() + || io_status_block + .write_at_offset( + 0, + IoStatusBlock::new(NtStatus::SUCCESS, size_of::()), + ) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::SUCCESS + } + // Microsoft Learn documents `NtCreateFile` as the common create/open primitive, // with `NtOpenFile` being its open-existing subset. #[expect( @@ -892,8 +1011,8 @@ fn map_mkdir_error(error: MkdirError) -> NtStatus { mod tests { use super::*; use crate::tests::{ - TestFS, TestPlatform, const_ptr, mut_ptr, null_mut_ptr, object_attributes, unicode_string, - utf16_units as utf16, + TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, null_mut_ptr, object_attributes, + unicode_string, utf16_units as utf16, }; use litebox::fs::FileSystem as _; @@ -967,6 +1086,156 @@ mod tests { (status, handle, io_status) } + fn open_fs_root(task: &Task) -> Handle { + let (_path, _name, attributes) = open_object_attributes("/"); + 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(&attributes)), + mut_ptr(&mut io_status), + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + (FileCreateOptions::DIRECTORY_FILE | FileCreateOptions::SYNCHRONOUS_IO_NONALERT) + .bits(), + ), + NtStatus::SUCCESS + ); + handle + } + + #[test] + fn nt_query_volume_information_file_returns_fs_device_information() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let handle = open_fs_root(&task); + let mut io_status = IoStatusBlock::default(); + let mut output = FileFsDeviceInformation { + device_type: 0, + characteristics: 0, + }; + + assert_eq!( + task.sys_nt_query_volume_information_file( + handle, + mut_ptr(&mut io_status), + mut_byte_ptr(&mut output), + u32::try_from(size_of::()).unwrap(), + FsInformationClass::FileFsDeviceInformation as u32, + ), + NtStatus::SUCCESS + ); + assert_eq!( + FileDeviceType::try_from(output.device_type), + Ok(FileDeviceType::Disk), + "Wine's regular file/directory branch reports FILE_DEVICE_DISK" + ); + assert_eq!( + FileDeviceCharacteristics::from_bits_retain(output.characteristics), + FileDeviceCharacteristics::IS_MOUNTED, + "Wine's regular file/directory branch reports FILE_DEVICE_IS_MOUNTED" + ); + assert_eq!((output.device_type, output.characteristics), (0x7, 0x20)); + assert_eq!(io_status.status, NtStatus::SUCCESS.as_raw()); + assert_eq!(io_status.information, size_of::()); + }); + } + + #[test] + fn nt_query_volume_information_file_leaves_iosb_untouched_on_failures() { + run_with_test_platform_pointers(|| { + let task = crate::tests::test_task(); + let handle = open_fs_root(&task); + let sentinel = IoStatusBlock::new(NtStatus::from_raw(0x1111_1111), 0x2222_2222); + let mut io_status = sentinel; + let mut output = FileFsDeviceInformation { + device_type: 0xcccc_cccc, + characteristics: 0xcccc_cccc, + }; + + assert_eq!( + task.sys_nt_query_volume_information_file( + handle, + mut_ptr(&mut io_status), + mut_byte_ptr(&mut output), + u32::try_from(size_of::()).unwrap() - 1, + FsInformationClass::FileFsDeviceInformation as u32, + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!(io_status.status, sentinel.status); + assert_eq!(io_status.information, sentinel.information); + assert_eq!( + (output.device_type, output.characteristics), + (0xcccc_cccc, 0xcccc_cccc) + ); + + assert_eq!( + task.sys_nt_query_volume_information_file( + handle, + mut_ptr(&mut io_status), + mut_byte_ptr(&mut output), + u32::try_from(size_of::()).unwrap(), + 0xffff, + ), + NtStatus::INVALID_INFO_CLASS + ); + assert_eq!(io_status.status, sentinel.status); + assert_eq!(io_status.information, sentinel.information); + + assert_eq!( + task.sys_nt_query_volume_information_file( + Handle::from_raw(0x1234), + mut_ptr(&mut io_status), + mut_byte_ptr(&mut output), + u32::try_from(size_of::()).unwrap(), + FsInformationClass::FileFsDeviceInformation as u32, + ), + NtStatus::INVALID_HANDLE + ); + assert_eq!(io_status.status, sentinel.status); + assert_eq!(io_status.information, sentinel.information); + + assert_eq!( + task.sys_nt_query_volume_information_file( + Handle::from_raw(0x1234), + mut_ptr(&mut io_status), + mut_byte_ptr(&mut output), + u32::try_from(size_of::()).unwrap() - 1, + FsInformationClass::FileFsDeviceInformation as u32, + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!(io_status.status, sentinel.status); + assert_eq!(io_status.information, sentinel.information); + + assert_eq!( + task.sys_nt_query_volume_information_file( + Handle::from_raw(0x1234), + mut_ptr(&mut io_status), + mut_byte_ptr(&mut output), + u32::try_from(size_of::()).unwrap(), + 0xffff, + ), + NtStatus::INVALID_INFO_CLASS + ); + assert_eq!(io_status.status, sentinel.status); + assert_eq!(io_status.information, sentinel.information); + + assert_eq!( + task.sys_nt_query_volume_information_file( + handle, + null_mut_ptr::(), + mut_byte_ptr(&mut output), + u32::try_from(size_of::()).unwrap(), + FsInformationClass::FileFsDeviceInformation as u32, + ), + NtStatus::ACCESS_VIOLATION + ); + }); + } + #[test] fn nt_open_file_opens_existing_absolute_and_relative_files() { let task = crate::tests::test_task(); @@ -1611,6 +1880,13 @@ mod tests { ShareAccess: u32, OpenOptions: u32, ) -> i32; + fn NtQueryVolumeInformationFile( + FileHandle: *mut c_void, + IoStatusBlock: *mut IoStatusBlock, + FsInformation: *mut c_void, + Length: u32, + FsInformationClass: u32, + ) -> i32; fn NtClose(Handle: *mut c_void) -> i32; } @@ -1636,6 +1912,228 @@ mod tests { } } + fn host_status(status: i32) -> NtStatus { + NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) + } + + #[test] + fn nt_query_volume_information_file_device_information_matches_host_statuses() { + let test_dir = test_tmp_dir( + "nt_query_volume_information_file_device_information_matches_host_statuses", + ); + let _ = std::fs::remove_dir_all(&test_dir); + std::fs::create_dir_all(&test_dir).unwrap(); + let host_file = test_dir.join("existing.txt"); + std::fs::write(&host_file, b"host").unwrap(); + + let host_name_units = utf16(&host_nt_path(&host_file)); + let host_name = unicode_string(&host_name_units); + let host_attributes = host_object_attributes(&host_name); + let mut host_handle = core::ptr::null_mut(); + let mut host_io_status = IoStatusBlock::default(); + // SAFETY: All pointers reference live test locals, and ObjectName is an NT path + // to the temporary file created above. + let host_open = unsafe { + NtOpenFile( + &raw mut host_handle, + FILE_GENERIC_READ, + &raw const host_attributes, + &raw mut host_io_status, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + 0, + ) + }; + assert_eq!(host_open, NtStatus::SUCCESS.as_raw()); + + let mut host_output = FileFsDeviceInformation { + device_type: 0, + characteristics: 0, + }; + let mut host_query_iosb = IoStatusBlock::default(); + // SAFETY: The handle was opened above and output pointers reference live locals. + let host_query = unsafe { + NtQueryVolumeInformationFile( + host_handle, + &raw mut host_query_iosb, + (&raw mut host_output).cast::(), + u32::try_from(size_of::()).unwrap(), + FsInformationClass::FileFsDeviceInformation as u32, + ) + }; + close_host_handle(host_handle); + + assert_eq!(host_status(host_query), NtStatus::SUCCESS); + assert_eq!(host_query_iosb.status, NtStatus::SUCCESS.as_raw()); + assert_eq!( + host_query_iosb.information, + size_of::() + ); + assert_eq!( + FileDeviceType::try_from(host_output.device_type), + Ok(FileDeviceType::Disk) + ); + assert!( + FileDeviceCharacteristics::from_bits_retain(host_output.characteristics) + .contains(FileDeviceCharacteristics::IS_MOUNTED) + ); + + let task = crate::tests::test_task(); + let handle = open_fs_root(&task); + let mut output = FileFsDeviceInformation { + device_type: 0, + characteristics: 0, + }; + let mut io_status = IoStatusBlock::default(); + assert_eq!( + task.sys_nt_query_volume_information_file( + handle, + mut_ptr(&mut io_status), + mut_byte_ptr(&mut output), + u32::try_from(size_of::()).unwrap(), + FsInformationClass::FileFsDeviceInformation as u32, + ), + host_status(host_query) + ); + assert_eq!(io_status.status, host_query_iosb.status); + assert_eq!(io_status.information, host_query_iosb.information); + assert_eq!( + FileDeviceType::try_from(output.device_type), + Ok(FileDeviceType::Disk) + ); + assert_eq!( + FileDeviceCharacteristics::from_bits_retain(output.characteristics), + FileDeviceCharacteristics::IS_MOUNTED + ); + assert_eq!((output.device_type, output.characteristics), (0x7, 0x20)); + + let sentinel = IoStatusBlock::new(NtStatus::from_raw(0x1111_1111), 0x2222_2222); + for (length, class, expected) in [ + ( + u32::try_from(size_of::()).unwrap() - 1, + FsInformationClass::FileFsDeviceInformation as u32, + NtStatus::INFO_LENGTH_MISMATCH, + ), + ( + u32::try_from(size_of::()).unwrap(), + 0xffff, + NtStatus::INVALID_INFO_CLASS, + ), + ] { + let mut host_iosb = sentinel; + let mut host_output = FileFsDeviceInformation { + device_type: 0xcccc_cccc, + characteristics: 0xcccc_cccc, + }; + // SAFETY: `host_handle` is intentionally invalid only in the separate bad-handle + // case below; here all pointers reference live locals. + let host = unsafe { + NtQueryVolumeInformationFile( + core::ptr::null_mut(), + &raw mut host_iosb, + (&raw mut host_output).cast::(), + length, + class, + ) + }; + let mut shim_iosb = sentinel; + let mut shim_output = host_output; + let shim = task.sys_nt_query_volume_information_file( + handle, + mut_ptr(&mut shim_iosb), + mut_byte_ptr(&mut shim_output), + length, + class, + ); + + assert_eq!(shim, expected); + assert_eq!(shim, host_status(host)); + assert_eq!(shim_iosb.status, host_iosb.status); + assert_eq!(shim_iosb.information, host_iosb.information); + } + + let mut shim_iosb = sentinel; + let mut shim_output = FileFsDeviceInformation { + device_type: 0xcccc_cccc, + characteristics: 0xcccc_cccc, + }; + assert_eq!( + task.sys_nt_query_volume_information_file( + Handle::from_raw(0x1234), + mut_ptr(&mut shim_iosb), + mut_byte_ptr(&mut shim_output), + u32::try_from(size_of::()).unwrap(), + FsInformationClass::FileFsDeviceInformation as u32, + ), + NtStatus::INVALID_HANDLE + ); + assert_eq!(shim_iosb.status, sentinel.status); + assert_eq!(shim_iosb.information, sentinel.information); + + let mut host_iosb = sentinel; + let mut host_output = FileFsDeviceInformation { + device_type: 0xcccc_cccc, + characteristics: 0xcccc_cccc, + }; + // SAFETY: The bad handle is deliberately invalid to observe NTSTATUS; the output + // pointers reference live locals and are not retained. + let host_bad_handle = unsafe { + NtQueryVolumeInformationFile( + 0x1234usize as *mut c_void, + &raw mut host_iosb, + (&raw mut host_output).cast::(), + u32::try_from(size_of::()).unwrap(), + FsInformationClass::FileFsDeviceInformation as u32, + ) + }; + assert_eq!(host_status(host_bad_handle), NtStatus::INVALID_HANDLE); + assert_eq!(host_iosb.status, sentinel.status); + assert_eq!(host_iosb.information, sentinel.information); + + for (length, class, expected) in [ + ( + u32::try_from(size_of::()).unwrap() - 1, + FsInformationClass::FileFsDeviceInformation as u32, + NtStatus::INFO_LENGTH_MISMATCH, + ), + ( + u32::try_from(size_of::()).unwrap(), + 0xffff, + NtStatus::INVALID_INFO_CLASS, + ), + ] { + let mut host_iosb = sentinel; + let mut host_output = FileFsDeviceInformation { + device_type: 0xcccc_cccc, + characteristics: 0xcccc_cccc, + }; + // SAFETY: The bad handle is deliberately invalid to observe validation + // precedence; output pointers reference live locals and are not retained. + let host = unsafe { + NtQueryVolumeInformationFile( + 0x1234usize as *mut c_void, + &raw mut host_iosb, + (&raw mut host_output).cast::(), + length, + class, + ) + }; + let mut shim_iosb = sentinel; + let mut shim_output = host_output; + let shim = task.sys_nt_query_volume_information_file( + Handle::from_raw(0x1234), + mut_ptr(&mut shim_iosb), + mut_byte_ptr(&mut shim_output), + length, + class, + ); + + assert_eq!(shim, expected); + assert_eq!(shim, host_status(host)); + assert_eq!(shim_iosb.status, host_iosb.status); + assert_eq!(shim_iosb.information, host_iosb.information); + } + } + #[test] fn nt_open_file_existing_file_matches_host_status_and_information() { let test_dir = diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 461e68d5eb..6d65e72a6a 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -260,6 +260,13 @@ pub(crate) enum SyscallRequest { ea_buffer: Option>, ea_length: u32, }, + NtQueryVolumeInformationFile { + file_handle: Handle, + io_status_block: Platform::RawMutPointer, + fs_information: Platform::RawMutPointer, + length: u32, + fs_information_class: u32, + }, NtOpenKey { key_handle: Platform::RawMutPointer, desired_access: u32, @@ -569,6 +576,13 @@ impl SyscallRequest { ea_buffer:*, ea_length, })), + NtSysno::NtQueryVolumeInformationFile => Some(sys_req!(NtQueryVolumeInformationFile { + file_handle:{Handle::from_raw}, + io_status_block:*, + fs_information:*, + length, + fs_information_class, + })), NtSysno::NtOpenKey => Some(sys_req!(NtOpenKey { key_handle:*, desired_access, From c5cf588c90f1f21386aada619db7a63e8434d94f Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 30 Jun 2026 17:05:42 -0700 Subject: [PATCH 077/319] Add partial support for `NtSetInformationProcess` and `NtSetInformationThread` (#990) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 28 ++ litebox_shim_windows/src/syscalls/mod.rs | 43 +++ litebox_shim_windows/src/syscalls/process.rs | 327 ++++++++++++++++++- litebox_shim_windows/src/syscalls/thread.rs | 327 +++++++++++++++++++ 4 files changed, 722 insertions(+), 3 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/thread.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 1ed32598ae..af6df9e522 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -1039,6 +1039,34 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtSetInformationProcess { + process_handle, + process_information_class, + process_information, + process_information_length, + } => { + let status = Self::sys_nt_set_information_process( + process_handle, + process_information_class, + process_information, + process_information_length, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtSetInformationThread { + thread_handle, + thread_information_class, + thread_information, + thread_information_length, + } => { + let status = Self::sys_nt_set_information_thread( + thread_handle, + thread_information_class, + thread_information, + thread_information_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, source, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 6d65e72a6a..f8853760eb 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -11,6 +11,7 @@ pub(crate) mod process; pub(crate) mod registry; pub(crate) mod symlink; mod sysinfo; +pub(crate) mod thread; pub(crate) mod timer; pub(crate) mod wait_completion_packet; pub(crate) mod worker_factory; @@ -94,6 +95,24 @@ impl ProcessHandle { } } +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(crate) struct ThreadHandle(Handle); + +impl ThreadHandle { + pub(crate) const CURRENT: Self = Self::from_raw(usize::MAX - 1); + + #[must_use] + pub(crate) const fn from_raw(raw: usize) -> Self { + Self(Handle::from_raw(raw)) + } + + #[must_use] + pub(crate) fn is_current(self) -> bool { + self == Self::CURRENT + } +} + #[allow(clippy::enum_variant_names)] #[derive(Debug)] pub(crate) enum SyscallRequest { @@ -334,6 +353,18 @@ pub(crate) enum SyscallRequest { process_information_length: u32, return_length: Option>, }, + NtSetInformationProcess { + process_handle: ProcessHandle, + process_information_class: u32, + process_information: Platform::RawConstPointer, + process_information_length: u32, + }, + NtSetInformationThread { + thread_handle: ThreadHandle, + thread_information_class: u32, + thread_information: Platform::RawConstPointer, + thread_information_length: u32, + }, NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag: u32, source: Platform::RawConstPointer, @@ -650,6 +681,18 @@ impl SyscallRequest { process_information_length, return_length:*, })), + NtSysno::NtSetInformationProcess => Some(sys_req!(NtSetInformationProcess { + process_handle: { ProcessHandle::from_raw }, + process_information_class, + process_information:*, + process_information_length, + })), + NtSysno::NtSetInformationThread => Some(sys_req!(NtSetInformationThread { + thread_handle: { ThreadHandle::from_raw }, + thread_information_class, + thread_information:*, + thread_information_length, + })), NtSysno::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter => Some( sys_req!(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, diff --git a/litebox_shim_windows/src/syscalls/process.rs b/litebox_shim_windows/src/syscalls/process.rs index b9eeafc9d5..0f4c39509b 100644 --- a/litebox_shim_windows/src/syscalls/process.rs +++ b/litebox_shim_windows/src/syscalls/process.rs @@ -3,13 +3,13 @@ use core::sync::atomic::Ordering; use int_enum::IntEnum; -use litebox::platform::RawMutPointer as _; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::syscalls::ProcessHandle; -use crate::{MutPtr, ShimFS, ShimPlatform, Task}; +use crate::{ConstPtr, MutPtr, ShimFS, ShimPlatform, Task}; const ACTIVE_PROCESS_EXIT_STATUS: i32 = 0x0000_0103; const NORMAL_PROCESS_BASE_PRIORITY: i32 = 8; @@ -54,6 +54,12 @@ struct ProcessDefaultHardErrorMode { default_hard_error_mode: u32, } +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable)] +struct ProcessSchedulerSharedDataSlotInformation { + scheduler_shared_data_handle: usize, +} + impl Task { pub(crate) fn sys_nt_query_information_process( &self, @@ -138,6 +144,60 @@ impl Task { status } + pub(crate) fn sys_nt_set_information_process( + process_handle: ProcessHandle, + process_information_class: u32, + process_information: ConstPtr, + process_information_length: u32, + ) -> NtStatus { + let Ok(process_information_class) = + ProcessInformationClass::try_from(process_information_class) + else { + litebox_util_log::debug!( + process_information_class = process_information_class; + "Unsupported NtSetInformationProcess class" + ); + return NtStatus::INVALID_INFO_CLASS; + }; + + let status = match process_information_class { + ProcessInformationClass::SchedulerSharedData => { + Self::set_process_scheduler_shared_data( + process_handle, + process_information, + process_information_length, + ) + } + // TODO: implement additional settable process information classes when a guest + // exercises them. + ProcessInformationClass::BasicInformation + | ProcessInformationClass::DebugPort + | ProcessInformationClass::DefaultHardErrorMode + | ProcessInformationClass::Wow64Information + | ProcessInformationClass::DebugFlags + | ProcessInformationClass::TlsInformation + | ProcessInformationClass::Cookie + | ProcessInformationClass::ConsoleHostProcess + | ProcessInformationClass::ImageInformation => { + litebox_util_log::debug!( + process_information_class:? = process_information_class; + "Unsupported NtSetInformationProcess class" + ); + NtStatus::INVALID_INFO_CLASS + } + }; + + if status == NtStatus::SUCCESS { + litebox_util_log::debug!( + process_information_class:? = process_information_class, + process_information_length = process_information_length; + "Handled NtSetInformationProcess syscall" + ); + } + + status + } + fn write_process_information( process_information: MutPtr, process_information_length: u32, @@ -163,6 +223,33 @@ impl Task { NtStatus::SUCCESS } + fn set_process_scheduler_shared_data( + process_handle: ProcessHandle, + process_information: ConstPtr, + process_information_length: u32, + ) -> NtStatus { + if process_information_length + < size_of::().trunc() + { + return NtStatus::INFO_LENGTH_MISMATCH; + } + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + + let process_information = + ConstPtr::::from_usize( + process_information.as_usize(), + ); + if process_information.read_at_offset(0).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + + // Host 25H2 returns SUCCESS after probing this struct even when the inner scheduler + // shared-data handle is null or bogus; LiteBox has no scheduler-shared-data object to bind. + NtStatus::SUCCESS + } + fn process_basic_information(&self) -> ProcessBasicInformation { ProcessBasicInformation { exit_status: ACTIVE_PROCESS_EXIT_STATUS, @@ -185,18 +272,23 @@ pub(crate) const fn default_process_cookie() -> u32 { #[cfg(test)] mod tests { use super::*; - use crate::tests::{mut_byte_ptr, mut_ptr, null_mut_ptr}; + use crate::tests::{mut_byte_ptr, mut_ptr, null_const_ptr, null_mut_ptr}; use litebox::platform::ThreadProvider; const RETURN_LENGTH_SENTINEL: u32 = 0xaaaa_aaaa; type TestPlatform = crate::tests::TestPlatform; + type TestTask = Task; fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { let _ = crate::tests::test_platform(); ::run_test_thread(f) } + fn const_byte_ptr(value: &T) -> ConstPtr { + ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) + } + #[test] fn nt_query_information_process_validates_arguments() { run_with_test_platform_pointers(|| { @@ -271,6 +363,68 @@ mod tests { }); } + #[test] + fn nt_set_information_process_scheduler_shared_data_validates_arguments() { + run_with_test_platform_pointers(|| { + let information = ProcessSchedulerSharedDataSlotInformation { + scheduler_shared_data_handle: 0, + }; + let information_len: u32 = + size_of::().trunc(); + let bad_handle = ProcessHandle::from_raw(0x1234); + + assert_eq!( + TestTask::sys_nt_set_information_process( + bad_handle, + ProcessInformationClass::SchedulerSharedData as u32, + null_const_ptr::(), + information_len - 1, + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + + assert_eq!( + TestTask::sys_nt_set_information_process( + bad_handle, + 0xffff, + const_byte_ptr(&information), + information_len - 1, + ), + NtStatus::INVALID_INFO_CLASS + ); + + assert_eq!( + TestTask::sys_nt_set_information_process( + bad_handle, + ProcessInformationClass::SchedulerSharedData as u32, + null_const_ptr::(), + information_len, + ), + NtStatus::INVALID_HANDLE + ); + + assert_eq!( + TestTask::sys_nt_set_information_process( + ProcessHandle::CURRENT, + ProcessInformationClass::SchedulerSharedData as u32, + null_const_ptr::(), + information_len, + ), + NtStatus::ACCESS_VIOLATION + ); + + assert_eq!( + TestTask::sys_nt_set_information_process( + ProcessHandle::CURRENT, + ProcessInformationClass::SchedulerSharedData as u32, + const_byte_ptr(&information), + information_len, + ), + NtStatus::SUCCESS + ); + }); + } + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] mod host_fidelity { use core::ffi::c_void; @@ -286,6 +440,12 @@ mod tests { process_information_length: u32, return_length: *mut u32, ) -> i32; + fn NtSetInformationProcess( + process_handle: *mut c_void, + process_information_class: u32, + process_information: *const c_void, + process_information_length: u32, + ) -> i32; } fn empty_basic_information() -> ProcessBasicInformation { @@ -322,6 +482,25 @@ mod tests { NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) } + fn host_nt_set_information_process( + process_handle: *mut c_void, + process_information_class: u32, + process_information: *const c_void, + process_information_length: u32, + ) -> NtStatus { + // SAFETY: The host ntdll call treats these as user-mode input pointers, probes them, + // and does not retain them. Tests pass either valid locals or null to observe NTSTATUS. + let status = unsafe { + NtSetInformationProcess( + process_handle, + process_information_class, + process_information, + process_information_length, + ) + }; + NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) + } + #[test] fn nt_query_information_process_basic_length_mismatch_matches_host() { run_with_test_platform_pointers(|| { @@ -379,5 +558,147 @@ mod tests { assert_eq!(shim_return_length, host_return_length); }); } + + #[test] + fn nt_set_information_process_scheduler_shared_data_matches_host_statuses() { + run_with_test_platform_pointers(|| { + let null_information = ProcessSchedulerSharedDataSlotInformation { + scheduler_shared_data_handle: 0, + }; + let bogus_information = ProcessSchedulerSharedDataSlotInformation { + scheduler_shared_data_handle: 0x1234, + }; + let information_len: u32 = + size_of::().trunc(); + let current_process = usize::MAX as *mut c_void; + let bad_process = 0x1234usize as *mut c_void; + let scheduler_class = ProcessInformationClass::SchedulerSharedData as u32; + let bad_class = 0xffff; + + let supported_status = host_nt_set_information_process( + current_process, + scheduler_class, + core::ptr::from_ref(&null_information).cast::(), + information_len, + ); + + if supported_status != NtStatus::INVALID_INFO_CLASS { + assert_eq!(supported_status, NtStatus::SUCCESS); + + for ( + process_handle, + shim_process_handle, + process_information_class, + host_process_information, + shim_process_information, + process_information_length, + ) in [ + ( + current_process, + ProcessHandle::CURRENT, + scheduler_class, + core::ptr::from_ref(&null_information).cast::(), + const_byte_ptr(&null_information), + information_len, + ), + ( + current_process, + ProcessHandle::CURRENT, + scheduler_class, + core::ptr::from_ref(&bogus_information).cast::(), + const_byte_ptr(&bogus_information), + information_len, + ), + ( + current_process, + ProcessHandle::CURRENT, + scheduler_class, + core::ptr::from_ref(&null_information).cast::(), + const_byte_ptr(&null_information), + information_len - 1, + ), + ( + current_process, + ProcessHandle::CURRENT, + scheduler_class, + core::ptr::null(), + null_const_ptr::(), + information_len, + ), + ( + current_process, + ProcessHandle::CURRENT, + bad_class, + core::ptr::from_ref(&null_information).cast::(), + const_byte_ptr(&null_information), + information_len, + ), + ( + bad_process, + ProcessHandle::from_raw(0x1234), + scheduler_class, + core::ptr::from_ref(&null_information).cast::(), + const_byte_ptr(&null_information), + information_len, + ), + ( + bad_process, + ProcessHandle::from_raw(0x1234), + scheduler_class, + core::ptr::null(), + null_const_ptr::(), + information_len - 1, + ), + ( + bad_process, + ProcessHandle::from_raw(0x1234), + bad_class, + core::ptr::from_ref(&null_information).cast::(), + const_byte_ptr(&null_information), + information_len - 1, + ), + ( + bad_process, + ProcessHandle::from_raw(0x1234), + scheduler_class, + core::ptr::null(), + null_const_ptr::(), + information_len, + ), + ( + current_process, + ProcessHandle::CURRENT, + scheduler_class, + core::ptr::null(), + null_const_ptr::(), + information_len - 1, + ), + ( + current_process, + ProcessHandle::CURRENT, + bad_class, + core::ptr::from_ref(&null_information).cast::(), + const_byte_ptr(&null_information), + information_len - 1, + ), + ] { + let host = host_nt_set_information_process( + process_handle, + process_information_class, + host_process_information, + process_information_length, + ); + let shim = TestTask::sys_nt_set_information_process( + shim_process_handle, + process_information_class, + shim_process_information, + process_information_length, + ); + + assert_eq!(shim, host); + } + } + }); + } } } diff --git a/litebox_shim_windows/src/syscalls/thread.rs b/litebox_shim_windows/src/syscalls/thread.rs new file mode 100644 index 0000000000..54d58eba8b --- /dev/null +++ b/litebox_shim_windows/src/syscalls/thread.rs @@ -0,0 +1,327 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use int_enum::IntEnum; +use litebox::platform::RawConstPointer as _; +use litebox::utils::TruncateExt as _; +use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable}; + +use crate::syscalls::ThreadHandle; +use crate::{ConstPtr, ShimFS, ShimPlatform, Task}; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +enum ThreadInformationClass { + SchedulerSharedDataSlot = 57, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable)] +struct ThreadSchedulerSharedDataSlotInformation { + action: u32, + _padding0: u32, + scheduler_shared_data_handle: usize, + slot: usize, +} + +impl Task { + pub(crate) fn sys_nt_set_information_thread( + thread_handle: ThreadHandle, + thread_information_class: u32, + thread_information: ConstPtr, + thread_information_length: u32, + ) -> NtStatus { + let Ok(thread_information_class) = + ThreadInformationClass::try_from(thread_information_class) + else { + litebox_util_log::debug!( + thread_information_class = thread_information_class; + "Unsupported NtSetInformationThread class" + ); + return NtStatus::INVALID_INFO_CLASS; + }; + + let status = match thread_information_class { + ThreadInformationClass::SchedulerSharedDataSlot => { + Self::set_thread_scheduler_shared_data_slot( + thread_handle, + thread_information, + thread_information_length, + ) + } + }; + + if status == NtStatus::SUCCESS { + litebox_util_log::debug!( + thread_information_class:? = thread_information_class, + thread_information_length = thread_information_length; + "Handled NtSetInformationThread syscall" + ); + } + + status + } + + fn set_thread_scheduler_shared_data_slot( + thread_handle: ThreadHandle, + thread_information: ConstPtr, + thread_information_length: u32, + ) -> NtStatus { + let thread_information = + ConstPtr::::from_usize( + thread_information.as_usize(), + ); + let Some(_thread_information) = thread_information.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + if thread_information_length < size_of::().trunc() + { + return NtStatus::INFO_LENGTH_MISMATCH; + } + if !thread_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + + // The scheduler-shared-data handle is never valid in the sandbox, matching the host + // current-thread path for the observed all-zero slot request. + NtStatus::INVALID_HANDLE + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::null_const_ptr; + use litebox::platform::ThreadProvider; + + type TestPlatform = crate::tests::TestPlatform; + type TestTask = Task; + + fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { + let _ = crate::tests::test_platform(); + ::run_test_thread(f) + } + + fn const_byte_ptr(value: &T) -> ConstPtr { + ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) + } + + #[test] + fn nt_set_information_thread_scheduler_shared_data_slot_validates_arguments() { + run_with_test_platform_pointers(|| { + let information = ThreadSchedulerSharedDataSlotInformation { + action: 0, + _padding0: 0, + scheduler_shared_data_handle: 0, + slot: 0, + }; + let information_len: u32 = + size_of::().trunc(); + let bad_handle = ThreadHandle::from_raw(0x1234); + + assert_eq!( + TestTask::sys_nt_set_information_thread( + bad_handle, + 0xffff, + null_const_ptr::(), + information_len - 1, + ), + NtStatus::INVALID_INFO_CLASS + ); + + assert_eq!( + TestTask::sys_nt_set_information_thread( + bad_handle, + ThreadInformationClass::SchedulerSharedDataSlot as u32, + null_const_ptr::(), + information_len, + ), + NtStatus::ACCESS_VIOLATION + ); + + assert_eq!( + TestTask::sys_nt_set_information_thread( + bad_handle, + ThreadInformationClass::SchedulerSharedDataSlot as u32, + const_byte_ptr(&information), + information_len - 1, + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + + assert_eq!( + TestTask::sys_nt_set_information_thread( + bad_handle, + ThreadInformationClass::SchedulerSharedDataSlot as u32, + const_byte_ptr(&information), + information_len, + ), + NtStatus::INVALID_HANDLE + ); + + assert_eq!( + TestTask::sys_nt_set_information_thread( + ThreadHandle::CURRENT, + ThreadInformationClass::SchedulerSharedDataSlot as u32, + const_byte_ptr(&information), + information_len, + ), + NtStatus::INVALID_HANDLE + ); + }); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + mod host_fidelity { + use core::ffi::c_void; + + use super::*; + + #[link(name = "ntdll")] + unsafe extern "system" { + fn NtSetInformationThread( + thread_handle: *mut c_void, + thread_information_class: u32, + thread_information: *const c_void, + thread_information_length: u32, + ) -> i32; + } + + fn host_nt_set_information_thread( + thread_handle: *mut c_void, + thread_information_class: u32, + thread_information: *const c_void, + thread_information_length: u32, + ) -> NtStatus { + // SAFETY: The host ntdll call treats these as user-mode input pointers, probes them, + // and does not retain them. Tests pass either valid locals or null to observe NTSTATUS. + let status = unsafe { + NtSetInformationThread( + thread_handle, + thread_information_class, + thread_information, + thread_information_length, + ) + }; + NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) + } + + #[test] + fn nt_set_information_thread_scheduler_shared_data_slot_matches_host_statuses() { + run_with_test_platform_pointers(|| { + let information = ThreadSchedulerSharedDataSlotInformation { + action: 0, + _padding0: 0, + scheduler_shared_data_handle: 0, + slot: 0, + }; + let information_len: u32 = + size_of::().trunc(); + let current_thread = (usize::MAX - 1) as *mut c_void; + let bad_thread = 0x1234usize as *mut c_void; + let scheduler_class = ThreadInformationClass::SchedulerSharedDataSlot as u32; + let bad_class = 0xffff; + + if host_nt_set_information_thread( + current_thread, + scheduler_class, + core::ptr::from_ref(&information).cast::(), + information_len, + ) == NtStatus::INVALID_INFO_CLASS + { + return; + } + + for ( + thread_handle, + shim_thread_handle, + thread_information_class, + host_thread_information, + shim_thread_information, + thread_information_length, + ) in [ + ( + current_thread, + ThreadHandle::CURRENT, + scheduler_class, + core::ptr::from_ref(&information).cast::(), + const_byte_ptr(&information), + information_len, + ), + ( + current_thread, + ThreadHandle::CURRENT, + scheduler_class, + core::ptr::from_ref(&information).cast::(), + const_byte_ptr(&information), + information_len - 1, + ), + ( + current_thread, + ThreadHandle::CURRENT, + scheduler_class, + core::ptr::null(), + null_const_ptr::(), + information_len, + ), + ( + current_thread, + ThreadHandle::CURRENT, + bad_class, + core::ptr::null(), + null_const_ptr::(), + information_len, + ), + ( + bad_thread, + ThreadHandle::from_raw(0x1234), + scheduler_class, + core::ptr::from_ref(&information).cast::(), + const_byte_ptr(&information), + information_len, + ), + ( + bad_thread, + ThreadHandle::from_raw(0x1234), + scheduler_class, + core::ptr::null(), + null_const_ptr::(), + information_len, + ), + ( + bad_thread, + ThreadHandle::from_raw(0x1234), + scheduler_class, + core::ptr::from_ref(&information).cast::(), + const_byte_ptr(&information), + information_len - 1, + ), + ( + bad_thread, + ThreadHandle::from_raw(0x1234), + bad_class, + core::ptr::from_ref(&information).cast::(), + const_byte_ptr(&information), + information_len, + ), + ] { + let host = host_nt_set_information_thread( + thread_handle, + thread_information_class, + host_thread_information, + thread_information_length, + ); + let shim = TestTask::sys_nt_set_information_thread( + shim_thread_handle, + thread_information_class, + shim_thread_information, + thread_information_length, + ); + + assert_eq!(shim, host); + } + }); + } + } +} From dd88c90ed9df3921a7175bc7a99273e1f4aab853 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 30 Jun 2026 19:00:28 -0700 Subject: [PATCH 078/319] Add partial support for NtOpenThreadToken (#991) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 30 ++++++++++++++ litebox_shim_windows/src/syscalls/mod.rs | 26 +++++++++++++ litebox_shim_windows/src/syscalls/thread.rs | 43 ++++++++++++++++++++- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index af6df9e522..3de8199688 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -1067,6 +1067,36 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtOpenThreadToken { + thread_handle, + desired_access, + open_as_self, + token_handle, + } => { + let status = Self::sys_nt_open_thread_token( + thread_handle, + desired_access, + open_as_self, + token_handle, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtOpenThreadTokenEx { + thread_handle, + desired_access, + open_as_self, + handle_attributes, + token_handle, + } => { + let status = Self::sys_nt_open_thread_token_ex( + thread_handle, + desired_access, + open_as_self, + handle_attributes, + token_handle, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, source, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index f8853760eb..ced9039d02 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -365,6 +365,19 @@ pub(crate) enum SyscallRequest { thread_information: Platform::RawConstPointer, thread_information_length: u32, }, + NtOpenThreadToken { + thread_handle: ThreadHandle, + desired_access: u32, + open_as_self: u32, + token_handle: Platform::RawMutPointer, + }, + NtOpenThreadTokenEx { + thread_handle: ThreadHandle, + desired_access: u32, + open_as_self: u32, + handle_attributes: u32, + token_handle: Platform::RawMutPointer, + }, NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag: u32, source: Platform::RawConstPointer, @@ -693,6 +706,19 @@ impl SyscallRequest { thread_information:*, thread_information_length, })), + NtSysno::NtOpenThreadToken => Some(sys_req!(NtOpenThreadToken { + thread_handle: { ThreadHandle::from_raw }, + desired_access, + open_as_self, + token_handle:*, + })), + NtSysno::NtOpenThreadTokenEx => Some(sys_req!(NtOpenThreadTokenEx { + thread_handle: { ThreadHandle::from_raw }, + desired_access, + open_as_self, + handle_attributes, + token_handle:*, + })), NtSysno::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter => Some( sys_req!(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, diff --git a/litebox_shim_windows/src/syscalls/thread.rs b/litebox_shim_windows/src/syscalls/thread.rs index 54d58eba8b..2883801ab0 100644 --- a/litebox_shim_windows/src/syscalls/thread.rs +++ b/litebox_shim_windows/src/syscalls/thread.rs @@ -7,8 +7,8 @@ use litebox::utils::TruncateExt as _; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable}; -use crate::syscalls::ThreadHandle; -use crate::{ConstPtr, ShimFS, ShimPlatform, Task}; +use crate::syscalls::{Handle, ThreadHandle}; +use crate::{ConstPtr, MutPtr, ShimFS, ShimPlatform, Task, probe_guest_output_preserving_value}; #[repr(u32)] #[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] @@ -87,6 +87,45 @@ impl Task { // current-thread path for the observed all-zero slot request. NtStatus::INVALID_HANDLE } + + pub(crate) fn sys_nt_open_thread_token( + thread_handle: ThreadHandle, + _desired_access: u32, + _open_as_self: u32, + token_handle: MutPtr, + ) -> NtStatus { + Self::open_thread_token(thread_handle, token_handle) + } + + pub(crate) fn sys_nt_open_thread_token_ex( + thread_handle: ThreadHandle, + _desired_access: u32, + _open_as_self: u32, + _handle_attributes: u32, + token_handle: MutPtr, + ) -> NtStatus { + // TODO: HandleAttributes is outcome-independent while the sandbox has no impersonation + // token. Once a real token subsystem exists it must be validated; host 25H2 returns + // STATUS_INVALID_PARAMETER for attrs=0xffffffff after ImpersonateSelf. + Self::open_thread_token(thread_handle, token_handle) + } + + fn open_thread_token( + thread_handle: ThreadHandle, + token_handle: MutPtr, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(token_handle) { + return status; + } + if !thread_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + + // A thread only has a token while it is actively impersonating (SetThreadToken / + // ImpersonateSelf). Sandbox threads never impersonate, so real host 25H2 returns + // STATUS_NO_TOKEN here as well: this is the host-faithful terminal answer, not a stub. + NtStatus::NO_TOKEN + } } #[cfg(test)] From 4c63b7eedf2ee8682f8cf8cc31f0bb50930d6143 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 30 Jun 2026 21:45:33 -0700 Subject: [PATCH 079/319] Add broker readiness notification channel skeleton (#976) Adds a minimal broker-to-local notification channel skeleton for readiness updates while keeping the control channel strictly paired request/response. The PR defines event readiness notification DTOs, wire codec coverage, channel traits, and Unix-socket notification send/receive support without changing shim behavior or enabling blocking eventfd yet. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_protocol/src/channel.rs | 32 +++++- litebox_broker_protocol/src/message.rs | 22 +++- litebox_broker_protocol/src/wire.rs | 117 +++++++++++++++++++- litebox_broker_protocol/src/wire/event.rs | 4 +- litebox_broker_transport/src/unix_socket.rs | 84 +++++++++++++- 5 files changed, 249 insertions(+), 10 deletions(-) diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index 728053ca03..06a71b46de 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -2,7 +2,8 @@ // Licensed under the MIT license. use crate::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, + BrokerResponse, }; /// Peer identity information supplied by the channel or host layer. @@ -85,3 +86,32 @@ pub trait HostControlChannel { /// Sends one active broker response. fn send_response(&mut self, response: &BrokerResponse) -> Result<(), Self::Error>; } + +/// Local-side receive channel for broker-initiated asynchronous notifications. +/// +/// A notification channel is separate from the control channel so active broker +/// requests remain strictly paired with their responses. The deployment is +/// responsible for binding this channel to the same authenticated broker +/// association as the matching control channel. +pub trait LocalNotificationChannel { + /// Channel-specific error type. + type Error; + + /// Receives one broker notification. + /// + /// Returns `Ok(None)` when the broker closed the channel cleanly before + /// starting another notification frame. + fn recv_notification(&mut self) -> Result, Self::Error>; +} + +/// Host-side send channel for broker-initiated asynchronous notifications. +/// +/// Implementations carry notification frames only; object operation responses +/// continue to use [`HostControlChannel::send_response`]. +pub trait HostNotificationChannel { + /// Channel-specific error type. + type Error; + + /// Sends one broker notification. + fn send_notification(&mut self, notification: &BrokerNotification) -> Result<(), Self::Error>; +} diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index 36c8666370..bb51f177e1 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -4,7 +4,7 @@ use crate::error::ErrorCode; use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, - CreateEventRequest, CreateEventResponse, WaitEventRequest, WaitEventResponse, + CreateEventRequest, CreateEventResponse, ReadinessState, WaitEventRequest, WaitEventResponse, }; use crate::{ObjectHandle, ProtocolVersion}; @@ -84,3 +84,23 @@ pub enum EventResponse { /// Consume operation response. Consume(ConsumeEventResponse), } + +/// Broker-initiated asynchronous notification. +/// +/// Notifications are level-triggered snapshots and may be coalesced or +/// duplicated by a transport. Local waiters must treat them as wakeups to +/// re-check authoritative state, not as ordered state transitions. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BrokerNotification { + /// Readiness changed or should be re-checked for a broker-owned event object. + EventReadiness(EventReadinessNotification), +} + +/// Readiness notification for a broker-owned event object. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EventReadinessNotification { + /// Event object handle. + pub handle: ObjectHandle, + /// Current broker-authoritative readiness snapshot. + pub readiness: ReadinessState, +} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 4e2ac06448..c9bb49a850 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -20,7 +20,8 @@ use thiserror::Error; use crate::error::ErrorCode; use crate::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, + BrokerResponse, EventReadinessNotification, }; use primitive::{Decoder, Encoder}; @@ -38,6 +39,8 @@ const RESPONSE_TAG_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; const RESPONSE_TAG_OBJECT_CLOSED: u8 = 4; +const NOTIFICATION_TAG_EVENT_READINESS: u8 = 0; + /// Error produced while encoding or decoding a broker wire message. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] #[non_exhaustive] @@ -208,6 +211,39 @@ pub fn decode_response(frame: &[u8]) -> Result { Ok(response) } +/// Encodes a broker notification body. +/// +/// Successful encodings are always non-empty because the first byte is the +/// message tag. +pub fn encode_notification(notification: BrokerNotification) -> Vec { + let mut encoder = Encoder::default(); + match notification { + BrokerNotification::EventReadiness(notification) => { + encoder.u8(NOTIFICATION_TAG_EVENT_READINESS); + encoder.handle(notification.handle); + event::encode_readiness(&mut encoder, notification.readiness); + } + } + encoder.finish() +} + +/// Decodes a broker notification body. +pub fn decode_notification(frame: &[u8]) -> Result { + let mut decoder = Decoder::new(frame); + let tag = decoder.u8()?; + let notification = match tag { + NOTIFICATION_TAG_EVENT_READINESS => { + BrokerNotification::EventReadiness(EventReadinessNotification { + handle: decoder.handle()?, + readiness: event::decode_readiness(&mut decoder)?, + }) + } + _ => return Err(WireError::InvalidTag), + }; + decoder.finish()?; + Ok(notification) +} + #[cfg(test)] mod tests { use super::*; @@ -329,6 +365,27 @@ mod tests { } } + #[test] + fn notification_codec_round_trips_all_variants() { + let handle = ObjectHandle(13); + let notifications = [BrokerNotification::EventReadiness( + EventReadinessNotification { + handle, + readiness: ReadinessState { + read_ready: true, + write_ready: false, + }, + }, + )]; + + for notification in notifications { + assert_eq!( + decode_notification(&encode_notification(notification.clone())).unwrap(), + notification + ); + } + } + #[test] fn decode_rejects_malformed_handshake_request_frames() { assert_eq!( @@ -461,6 +518,48 @@ mod tests { assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); } + #[test] + fn decode_rejects_malformed_notification_frames() { + assert_eq!( + decode_notification(&[0xff, 1, 2, 3]), + Err(WireError::InvalidTag) + ); + assert_eq!( + decode_notification(&[NOTIFICATION_TAG_EVENT_READINESS]), + Err(WireError::TruncatedFrame) + ); + + let mut invalid_bool = encode_notification(BrokerNotification::EventReadiness( + EventReadinessNotification { + handle: ObjectHandle(13), + readiness: ReadinessState { + read_ready: true, + write_ready: false, + }, + }, + )); + *invalid_bool.last_mut().unwrap() = 0xff; + assert_eq!( + decode_notification(&invalid_bool), + Err(WireError::InvalidBoolean) + ); + + let mut trailing = encode_notification(BrokerNotification::EventReadiness( + EventReadinessNotification { + handle: ObjectHandle(13), + readiness: ReadinessState { + read_ready: true, + write_ready: false, + }, + }, + )); + trailing.push(0xff); + assert_eq!( + decode_notification(&trailing), + Err(WireError::TrailingBytes) + ); + } + #[test] fn event_add_response_wire_shape_is_pinned() { assert_eq!( @@ -475,4 +574,20 @@ mod tests { [1, 2, 1, 0] ); } + + #[test] + fn event_readiness_notification_wire_shape_is_pinned() { + assert_eq!( + encode_notification(BrokerNotification::EventReadiness( + EventReadinessNotification { + handle: ObjectHandle(13), + readiness: ReadinessState { + read_ready: true, + write_ready: false, + }, + } + )), + [0, 13, 0, 0, 0, 0, 0, 0, 0, 1, 0] + ); + } } diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index 7892f94cd1..0810b03b76 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -114,12 +114,12 @@ pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result) -> Result { +pub(super) fn decode_readiness(decoder: &mut Decoder<'_>) -> Result { Ok(ReadinessState { read_ready: decoder.bool()?, write_ready: decoder.bool()?, diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 877507d5c6..d02fe784d5 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -13,15 +13,17 @@ use std::path::Path; use std::time::{Duration, Instant}; use litebox_broker_protocol::channel::{ - HostControlChannel, HostReceive, LocalControlChannel, PeerCredential, + HostControlChannel, HostNotificationChannel, HostReceive, LocalControlChannel, + LocalNotificationChannel, PeerCredential, }; use litebox_broker_protocol::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, + BrokerResponse, }; use litebox_broker_protocol::wire::{ - WireError, decode_handshake_request, decode_handshake_response, decode_request, - decode_response, encode_handshake_request, encode_handshake_response, encode_request, - encode_response, + WireError, decode_handshake_request, decode_handshake_response, decode_notification, + decode_request, decode_response, encode_handshake_request, encode_handshake_response, + encode_notification, encode_request, encode_response, }; const MAX_FRAME_LEN: usize = 64 * 1024; @@ -67,6 +69,16 @@ pub struct UnixStreamHostControlChannel { stream: UnixStream, } +/// Local-side Unix-domain-socket notification channel for the hosted userland POC. +pub struct UnixStreamLocalNotificationChannel { + stream: UnixStream, +} + +/// Host-side Unix-domain-socket notification channel for the hosted userland POC. +pub struct UnixStreamHostNotificationChannel { + stream: UnixStream, +} + impl UnixStreamHostControlChannel { /// Creates a host control channel from an accepted Unix stream. pub const fn from_accepted(stream: UnixStream) -> Self { @@ -74,6 +86,25 @@ impl UnixStreamHostControlChannel { } } +impl UnixStreamLocalNotificationChannel { + /// Creates a local notification channel from an already-connected Unix stream. + pub const fn from_connected(stream: UnixStream) -> Self { + Self { stream } + } + + /// Connects to a userland broker Unix notification socket. + pub fn connect(path: impl AsRef) -> IoResult { + UnixStream::connect(path).map(Self::from_connected) + } +} + +impl UnixStreamHostNotificationChannel { + /// Creates a host notification channel from an accepted Unix stream. + pub const fn from_accepted(stream: UnixStream) -> Self { + Self { stream } + } +} + impl LocalControlChannel for UnixStreamLocalControlChannel { type Error = Error; @@ -153,6 +184,29 @@ impl HostControlChannel for UnixStreamHostControlChannel { } } +impl LocalNotificationChannel for UnixStreamLocalNotificationChannel { + type Error = Error; + + fn recv_notification(&mut self) -> IoResult> { + match read_frame_with_deadline(&mut self.stream, None)? { + Some(frame) => decode_notification(&frame).map(Some).map_err(wire_error), + None => Ok(None), + } + } +} + +impl HostNotificationChannel for UnixStreamHostNotificationChannel { + type Error = Error; + + fn send_notification(&mut self, notification: &BrokerNotification) -> IoResult<()> { + write_frame_with_deadline( + &mut self.stream, + &encode_notification(notification.clone()), + None, + ) + } +} + fn read_frame_with_deadline( stream: &mut UnixStream, deadline: Option, @@ -383,4 +437,24 @@ mod tests { HostReceive::ProtocolViolation ); } + + #[test] + fn notification_frame_round_trip() { + let (local_stream, host_stream) = UnixStream::pair().unwrap(); + let mut local = UnixStreamLocalNotificationChannel::from_connected(local_stream); + let mut host = UnixStreamHostNotificationChannel::from_accepted(host_stream); + let notification = BrokerNotification::EventReadiness( + litebox_broker_protocol::message::EventReadinessNotification { + handle: litebox_broker_protocol::ObjectHandle(7), + readiness: litebox_broker_protocol::event::ReadinessState { + read_ready: true, + write_ready: false, + }, + }, + ); + + host.send_notification(¬ification).unwrap(); + + assert_eq!(local.recv_notification().unwrap(), Some(notification)); + } } From ea8b16548acc89fb8fb871cc039d04bf709c106e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 1 Jul 2026 10:16:44 -0700 Subject: [PATCH 080/319] Cherry-pick alarm spin overflow fix to ulitebox (#993) Cherry-picks 9bcaa50926c47efe69ae6b1dbfdfff5fd641ef7b (Fix alarm spin test overflow) onto ulitebox. --- litebox_runner_linux_userland/tests/alarm.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/litebox_runner_linux_userland/tests/alarm.c b/litebox_runner_linux_userland/tests/alarm.c index 4938dbd065..c996353adb 100644 --- a/litebox_runner_linux_userland/tests/alarm.c +++ b/litebox_runner_linux_userland/tests/alarm.c @@ -139,11 +139,10 @@ int test_alarm_fires_in_userspace(void) { // Busy-wait (no syscalls) until SIGALRM arrives. // The platform's schedule_interrupt mechanism should interrupt us. - volatile int i = 0; + volatile unsigned long long iterations = 0; while (alarm_count == 0) { - i++; - // Avoid the compiler optimizing this away. - if (i < 0) + iterations++; + if (iterations == 0) break; } @@ -153,7 +152,7 @@ int test_alarm_fires_in_userspace(void) { sa.sa_handler = SIG_DFL; sigaction(SIGALRM, &sa, NULL); - printf("alarm_fires_in_userspace: PASS (loop iterations=%d)\n", i); + printf("alarm_fires_in_userspace: PASS (loop iterations=%llu)\n", iterations); return 0; } From 6b414bb4200afe57b6f51a245027e82313cde5f3 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Wed, 1 Jul 2026 18:59:57 -0700 Subject: [PATCH 081/319] Cherry pick "Update LVBS remap_to_high_canonical layout docs" (#996) Co-authored-by: Sangho Lee --- .../src/mshv/vtl1_mem_layout.rs | 8 +++--- litebox_runner_lvbs/src/main.rs | 28 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/litebox_platform_lvbs/src/mshv/vtl1_mem_layout.rs b/litebox_platform_lvbs/src/mshv/vtl1_mem_layout.rs index f64a2181c7..aac2e3c133 100644 --- a/litebox_platform_lvbs/src/mshv/vtl1_mem_layout.rs +++ b/litebox_platform_lvbs/src/mshv/vtl1_mem_layout.rs @@ -25,10 +25,10 @@ pub const VTL1_PTE_0_PAGE: usize = 5; pub const VTL1_KERNEL_STACK_PAGE: usize = VTL1_PTE_0_PAGE + VSM_SK_PTE_PAGES_COUNT; /// PDPT page for the Phase 1 high-canonical PML4 entry. Placed after the -/// VTL0-reserved special pages (GDT, TSS, PT pages, stack, boot params, -/// cmdline) so that all 8 VTL0 PTE pages remain available for the -/// high-canonical mapping. This page is within the VTL0 identity-mapped -/// 16 MiB region but is otherwise unused memory. +/// VTL0-reserved special pages (GDT, TSS, PT pages, and stack) so that all 8 +/// VTL0 PTE pages remain available for the high-canonical mapping. This page +/// is within the VTL0 identity-mapped 16 MiB region but is otherwise unused +/// memory. pub const VTL1_REMAP_PDPT_PAGE: usize = VTL1_KERNEL_STACK_PAGE + 1; /// PDE page for the Phase 1 high-canonical mapping. PDE entries point to diff --git a/litebox_runner_lvbs/src/main.rs b/litebox_runner_lvbs/src/main.rs index cbca1504cb..abf33655ff 100644 --- a/litebox_runner_lvbs/src/main.rs +++ b/litebox_runner_lvbs/src/main.rs @@ -176,16 +176,16 @@ unsafe fn apply_relocations() { /// │ PML4 (page 2, from VTL0) │ /// │ ┌──────────────────────────────────────────────┐ │ /// │ │ [0] → VTL0 PDPT (page 3) ← identity │ kept (harmless) │ -/// │ │ [256] → new PDPT (page 16) ← high-canon │ Phase 1 adds │ +/// │ │ [256] → new PDPT (page 14) ← high-canon │ Phase 1 adds │ /// │ │ ... │ │ /// │ └──────────────────────────────────────────────┘ │ /// │ │ -/// │ New PDPT (page 16) │ +/// │ New PDPT (page 14) │ /// │ ┌──────────────────────────────────────────────┐ │ -/// │ │ [pdpt_idx] → new PDE (page 17) │ │ +/// │ │ [pdpt_idx] → new PDE (page 15) │ │ /// │ └──────────────────────────────────────────────┘ │ /// │ │ -/// │ New PDE (page 17) │ +/// │ New PDE (page 15) │ /// │ ┌──────────────────────────────────────────────┐ │ /// │ │ [pde+0] → VTL0 PTE page 5 (2 MiB, 4KB pgs) │ reused as-is │ /// │ │ [pde+1] → VTL0 PTE page 6 │ │ @@ -213,7 +213,7 @@ unsafe fn apply_relocations() { /// │ The entire low half [0, 0x7FFF_FFFF_F000) is now available │ /// │ for user-space (TAs / Linux apps). │ /// │ │ -/// │ Reclaim all Phase 1 pages (2–12, 16–17) back to the allocator. │ +/// │ Reclaim all Phase 1 pages (2–12, 14–15) back to the allocator. │ /// └─────────────────────────────────────────────────────────────────────┘ /// ``` /// @@ -229,22 +229,22 @@ unsafe fn apply_relocations() { /// means the existing PTE pages can be **reused as-is** for the /// high-canonical mapping; we only need a new PDPT page and a new PDE page. /// -/// The PDPT and PDE pages are allocated from unused memory after the -/// VTL0-reserved special pages (pages 16–17), preserving all 8 PTE pages -/// for the high-canonical mapping and covering the full 16 MiB. +/// The PDPT and PDE pages are allocated from unused memory after the boot +/// stack page (pages 14–15), preserving all 8 PTE pages for the high-canonical +/// mapping and covering the full 16 MiB. /// /// ## Page table pages used /// /// | page | constant | purpose | /// |------|-----------------------|----------------------------------------| -/// | 16 | `VTL1_REMAP_PDPT_PAGE`| PDPT for the high-canonical PML4 entry | -/// | 17 | `VTL1_REMAP_PDE_PAGE` | PDE pointing to PTE pages 5–12 | +/// | 14 | `VTL1_REMAP_PDPT_PAGE`| PDPT for the high-canonical PML4 entry | +/// | 15 | `VTL1_REMAP_PDE_PAGE` | PDE pointing to PTE pages 5–12 | /// /// ## Algorithm /// /// 1. Compute PML4/PDPT/PDE indices from `memory_base + KERNEL_OFFSET`. -/// 2. Zero and populate a PDPT page (page 16). -/// 3. Zero and populate a PDE page (page 17) pointing to all 8 VTL0 PTE +/// 2. Zero and populate a PDPT page (page 14). +/// 3. Zero and populate a PDE page (page 15) pointing to all 8 VTL0 PTE /// pages 5–12 (4KB page mappings, no huge pages). /// 4. Wire PML4 → PDPT → PDE. /// 5. Flush TLB and jump to `continue_boot` at the high-canonical address. @@ -280,12 +280,12 @@ unsafe fn remap_to_high_canonical() -> ! { let pml4_pa = cr3 & CR3_ADDR_MASK; let pml4_ptr = pml4_pa as *mut u64; - // Set up the PDPT page (page 16) + // Set up the PDPT page. let pdpt_page_pa = memory_base + (VTL1_REMAP_PDPT_PAGE * vtl1_mem_layout::PAGE_SIZE) as u64; let pdpt_ptr = pdpt_page_pa as *mut u64; unsafe { core::ptr::write_bytes(pdpt_ptr, 0, ENTRIES_PER_PT_PAGE) }; - // Set up the PDE page (page 17) + // Set up the PDE page. let pde_page_pa = memory_base + (VTL1_REMAP_PDE_PAGE * vtl1_mem_layout::PAGE_SIZE) as u64; let pde_ptr = pde_page_pa as *mut u64; unsafe { core::ptr::write_bytes(pde_ptr, 0, ENTRIES_PER_PT_PAGE) }; From 77e84fea8b97d137a84e272851baf8801ac0501c Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Wed, 1 Jul 2026 19:06:51 -0700 Subject: [PATCH 082/319] Cherry pick "Add per-session OP-TEE client identity handling" (#997) Co-authored-by: Sangho Lee --- Cargo.lock | 1 - dev_tests/src/ratchet.rs | 4 +- litebox_common_optee/src/lib.rs | 55 ++++++++- litebox_runner_lvbs/Cargo.toml | 1 - litebox_runner_lvbs/src/lib.rs | 50 ++++---- .../src/lib.rs | 15 +-- .../src/tests.rs | 109 ++++++++++++++++-- .../tests/aes-ta-cmds.json | 5 +- .../tests/hello-ta-cmds.json | 5 +- .../tests/kmpp-ta-cmds.json | 6 + .../tests/random-ta-cmds.json | 5 +- litebox_shim_optee/src/lib.rs | 71 ++++++++---- litebox_shim_optee/src/msg_handler.rs | 40 +++++-- litebox_shim_optee/src/session.rs | 53 ++++++++- litebox_shim_optee/src/syscalls/tee.rs | 33 +++++- 15 files changed, 358 insertions(+), 95 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c59fa65cb9..098a78558b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1735,7 +1735,6 @@ dependencies = [ "litebox_shim_optee", "litebox_util_log", "log", - "once_cell", "spin 0.10.0", "x86_64", ] diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index bf83c20de2..2e46b114e6 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -41,10 +41,10 @@ fn ratchet_globals() -> Result<()> { ("litebox_platform_lvbs/", 24), ("litebox_platform_multiplex/", 1), ("litebox_platform_windows_userland/", 8), - ("litebox_runner_lvbs/", 6), + ("litebox_runner_lvbs/", 5), ("litebox_runner_snp/", 2), ("litebox_shim_linux/", 1), - ("litebox_shim_optee/", 4), + ("litebox_shim_optee/", 5), ("litebox_shim_windows/", 1), ], |file| { diff --git a/litebox_common_optee/src/lib.rs b/litebox_common_optee/src/lib.rs index b2cf533cae..aad03495e1 100644 --- a/litebox_common_optee/src/lib.rs +++ b/litebox_common_optee/src/lib.rs @@ -624,6 +624,17 @@ pub struct TeeUuid { } impl TeeUuid { + /// The nil UUID (all zeros, RFC 4122 S4.1.7). + /// + /// Used for anonymous clients (e.g., `TeeLogin::Public`) that carry no + /// REE-derived identity. + pub const NIL: Self = Self { + time_low: 0, + time_mid: 0, + time_hi_and_version: 0, + clock_seq_and_node: [0; 8], + }; + /// Converts a UUID from a 16-byte array in RFC 4122 format (big-endian for numeric fields). /// /// The byte layout is: @@ -750,7 +761,7 @@ pub struct TaHead { pub const TA_HEAD_SECTION_NAME: &str = ".ta_head"; /// `TEE_Identity` from `optee_os/lib/libutee/include/tee_api_types.h`. -#[derive(Clone, Copy, PartialEq, Immutable, IntoBytes)] +#[derive(Clone, Copy, PartialEq, Debug, Immutable, IntoBytes)] #[repr(C)] pub struct TeeIdentity { pub login: TeeLogin, @@ -832,9 +843,11 @@ const TEE_LOGIN_APPLICATION: u32 = 0x4; const TEE_LOGIN_APPLICATION_USER: u32 = 0x5; const TEE_LOGIN_APPLICATION_GROUP: u32 = 0x6; const TEE_LOGIN_TRUSTED_APP: u32 = 0xf000_0000; +// Private OP-TEE login for in-kernel REE clients (`tee_api_defines_extensions.h`). +const TEE_LOGIN_REE_KERNEL: u32 = 0x8000_0000; /// `TEE Login type` from `optee_os/lib/libutee/include/tee_api_defines.h` -#[derive(Clone, Copy, PartialEq, TryFromPrimitive, Immutable, IntoBytes)] +#[derive(Clone, Copy, PartialEq, Debug, TryFromPrimitive, Immutable, IntoBytes)] #[repr(u32)] pub enum TeeLogin { Public = TEE_LOGIN_PUBLIC, @@ -843,6 +856,7 @@ pub enum TeeLogin { Application = TEE_LOGIN_APPLICATION, ApplicationUser = TEE_LOGIN_APPLICATION_USER, ApplicationGroup = TEE_LOGIN_APPLICATION_GROUP, + ReeKernel = TEE_LOGIN_REE_KERNEL, TrustedApp = TEE_LOGIN_TRUSTED_APP, } @@ -1469,6 +1483,10 @@ const OPTEE_MSG_ATTR_TYPE_TMEM_INOUT: u8 = 0xb; // Note: `OPTEE_MSG_ATTR_TYPE_FMEM_*` are aliases of `OPTEE_MSG_ATTR_TYPE_RMEM_*`. // Whether it is RMEM of FMEM depends on the conduit. +/// Meta-parameter marker of the attribute word. Set on the `OpenSession` +/// TA-UUID and client-identity params. +const OPTEE_MSG_ATTR_META: u64 = 1 << 8; + #[non_exhaustive] #[derive(Debug, PartialEq, TryFromPrimitive)] #[repr(u8)] @@ -1492,11 +1510,17 @@ pub enum OpteeMsgAttrType { /// - bit 8 – meta /// - bit 9 – noncontig /// - bits \[63:10\] – reserved (zero) -#[derive(Clone, Copy, Default, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[derive(Clone, Copy, Default, PartialEq, Eq, FromBytes, IntoBytes, Immutable, KnownLayout)] #[repr(transparent)] pub struct OpteeMsgAttr(u64); impl OpteeMsgAttr { + /// The exact attribute word an `OpenSession` meta value parameter must carry + /// (`OPTEE_MSG_ATTR_META | OPTEE_MSG_ATTR_TYPE_VALUE_INPUT`, all other bits + /// zero). See [`OpteeMsgArgs::get_meta_param_value`]. + pub const META_VALUE_INPUT: Self = + Self(OPTEE_MSG_ATTR_META | OPTEE_MSG_ATTR_TYPE_VALUE_INPUT as u64); + /// Returns the attribute type (bits 0–7). #[allow(clippy::cast_possible_truncation)] pub fn attr_type(&self) -> u8 { @@ -1525,6 +1549,10 @@ impl OpteeMsgParam { pub fn attr_type(&self) -> OpteeMsgAttrType { OpteeMsgAttrType::try_from(self.attr.attr_type()).unwrap_or(OpteeMsgAttrType::None) } + /// Returns `true` when the meta bit (bit 8) is set. + pub fn is_meta(&self) -> bool { + self.attr.meta() + } pub fn get_param_tmem(&self) -> Option { if matches!( self.attr.attr_type(), @@ -1757,6 +1785,27 @@ impl OpteeMsgArgs { .ok_or(OpteeSmcReturnCode::EBadCmd)?) } } + + /// Read a value parameter that must be tagged as an `OpenSession` meta parameter. + /// + /// `OpenSession` conveys the TA UUID and client identity in the first two + /// params, each marked exactly [`OpteeMsgAttr::META_VALUE_INPUT`], mirroring + /// OP-TEE OS `get_open_session_meta()`. Plain `get_param_value` ignores + /// these bits, so it must not be used for this. + pub fn get_meta_param_value( + &self, + index: usize, + ) -> Result { + if index >= self.num_params as usize { + return Err(OpteeSmcReturnCode::ENotAvail); + } + let param = &self.params[index]; + if param.attr != OpteeMsgAttr::META_VALUE_INPUT { + return Err(OpteeSmcReturnCode::EBadCmd); + } + param.get_param_value().ok_or(OpteeSmcReturnCode::EBadCmd) + } + pub fn set_param_value( &mut self, index: usize, diff --git a/litebox_runner_lvbs/Cargo.toml b/litebox_runner_lvbs/Cargo.toml index f5fc10f79f..2297c6e3ce 100644 --- a/litebox_runner_lvbs/Cargo.toml +++ b/litebox_runner_lvbs/Cargo.toml @@ -14,7 +14,6 @@ litebox_shim_optee = { path = "../litebox_shim_optee/", version = "0.1.0" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } log = { version = "0.4", default-features = false } spin = { version = "0.10.0", default-features = false, features = ["spin_mutex"] } -once_cell = { version = "1.21.3", default-features = false, features = ["race", "alloc"] } [target.'cfg(target_arch = "x86_64")'.dependencies] x86_64 = { version = "0.15.2", default-features = false, features = ["instructions"] } diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index 6db84928a2..2a351c03bc 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -42,9 +42,8 @@ use litebox_shim_optee::msg_handler::{ decode_ta_request, handle_optee_msg_args, handle_optee_smc_args, packed_msg_args_lock, update_optee_msg_args, }; -use litebox_shim_optee::session::{OpenSessionTarget, SessionManager, TaInstance}; +use litebox_shim_optee::session::{OpenSessionTarget, TaInstance, session_manager}; use litebox_shim_optee::{NormalWorldConstPtr, NormalWorldMutPtr, UserConstPtr}; -use once_cell::race::OnceBox; /// Seed the initial heap regions so the global allocator has enough memory /// for slab-backed allocations (the slab needs >= 2 MB backing pages). @@ -284,12 +283,6 @@ fn optee_smc_handler_entry_inner( Ok(0) } -/// Get the global session manager. -fn session_manager() -> &'static SessionManager { - static SESSION_MANAGER: OnceBox = OnceBox::new(); - SESSION_MANAGER.get_or_init(|| Box::new(SessionManager::new())) -} - /// Switch to the base page table. /// /// This must be called before returning to VTL0 to ensure VTL1 reentry is @@ -523,6 +516,7 @@ fn handle_open_session( msg_args_phys_addr, instance, params, + client_identity, &ta_req_info, ), OpenSessionTarget::NewInstance => open_session_new_instance( @@ -556,6 +550,7 @@ fn open_session_single_instance( msg_args_phys_addr: u64, instance: &TaInstance, params: &[litebox_common_optee::UteeParamOwned], + client_identity: Option, ta_req_info: &litebox_shim_optee::msg_handler::TaRequestInfo, ) -> Result<(), OpteeSmcReturnCode> { let task_pt_id = instance.task_page_table_id(); @@ -566,6 +561,9 @@ fn open_session_single_instance( // Safe to unwrap: session ID has been just created. let runner_session_id = session_token.session_id().unwrap(); + // Record the client identity before running OpenSession + session_manager().set_session_client_identity(runner_session_id, client_identity); + debug_serial_println!( "Reusing single-instance TA: uuid={:?}, task_pt_id={}, session_id={}", ta_uuid, @@ -584,7 +582,7 @@ fn open_session_single_instance( .ok_or(OpteeSmcReturnCode::EBadCmd)? .load_ta_context( params, - Some(runner_session_id), + runner_session_id, UteeEntryFunc::OpenSession as u32, None, ) @@ -682,6 +680,9 @@ fn open_session_single_instance( teardown_ta_page_table(instance.shim(), task_pt_id); }; } else { + // The session id is forgotten (never recycled), so the token's drop + // won't clear the recorded identity. Remove the client identity here. + session_manager().clear_session_client_identity(runner_session_id); session_token.disarm(); } return Err(e); @@ -700,7 +701,7 @@ fn open_session_single_instance( } /// Create a new TA instance for a session. Must be called from within a -/// [`SessionManager::with_ta`] closure. +/// [`litebox_shim_optee::session::SessionManager::with_ta`] closure. /// /// If ldelf loading or OpenSession entry point fails, the page table is torn down. /// Per OP-TEE OS semantics: if OpenSession returns non-success, cleanup happens. @@ -737,19 +738,13 @@ fn open_session_new_instance( // Load ldelf and TA - Box immediately to keep at fixed heap address let shim = litebox_shim_optee::OpteeShimBuilder::new().build(); let loaded_program = Box::new( - shim.load_ldelf( - LDELF_BINARY, - ta_uuid, - Some(ta_bin), - client_identity, - runner_session_id, - ) - .map_err(|_| { - // Safety: We are about to tear down this TA instance; - // no references to user-space memory will be held afterwards. - unsafe { teardown_ta_page_table(&shim, task_pt_id) }; - OpteeSmcReturnCode::ENomem - })?, + shim.load_ldelf(LDELF_BINARY, ta_uuid, Some(ta_bin)) + .map_err(|_| { + // Safety: We are about to tear down this TA instance; + // no references to user-space memory will be held afterwards. + unsafe { teardown_ta_page_table(&shim, task_pt_id) }; + OpteeSmcReturnCode::ENomem + })?, ); let ta_flags = loaded_program.ta_flags; @@ -797,6 +792,9 @@ fn open_session_new_instance( return Ok(()); } + // Record the client identity before running OpenSession + session_manager().set_session_client_identity(runner_session_id, client_identity); + // Load TA context with parameters for OpenSession - pass actual session_id loaded_program.entrypoints.as_ref().ok_or_else(|| { // Safety: We are about to tear down this TA instance; @@ -810,7 +808,7 @@ fn open_session_new_instance( .unwrap() .load_ta_context( params, - Some(runner_session_id), + runner_session_id, UteeEntryFunc::OpenSession as u32, None, ) @@ -979,7 +977,7 @@ fn handle_invoke_command( entrypoints_ref .load_ta_context( params.as_slice(), - Some(session_id), + session_id, UteeEntryFunc::InvokeCommand as u32, Some(cmd_id), ) @@ -1091,7 +1089,7 @@ fn handle_close_session( .unwrap() .load_ta_context( &[], - Some(session_id), + session_id, UteeEntryFunc::CloseSession as u32, None, ) diff --git a/litebox_runner_optee_on_linux_userland/src/lib.rs b/litebox_runner_optee_on_linux_userland/src/lib.rs index ae37334cd4..e2f0ea7d0b 100644 --- a/litebox_runner_optee_on_linux_userland/src/lib.rs +++ b/litebox_runner_optee_on_linux_userland/src/lib.rs @@ -5,7 +5,7 @@ use anyhow::{Context as _, Result}; use clap::Parser; use litebox_common_optee::{TeeUuid, UteeEntryFunc, UteeParamOwned}; use litebox_platform_multiplex::Platform; -use litebox_shim_optee::session::SessionManager; +use litebox_shim_optee::session::session_manager; use std::path::PathBuf; mod tests; @@ -109,21 +109,14 @@ fn run_ta_with_default_commands( ldelf_bin: &[u8], ta_bin: &[u8], ) { - let session_manager = SessionManager::new(); for func_id in [UteeEntryFunc::OpenSession, UteeEntryFunc::CloseSession] { let params = [const { UteeParamOwned::None }; UteeParamOwned::TEE_NUM_PARAMS]; if func_id == UteeEntryFunc::OpenSession { - let session_token = session_manager.try_acquire_open_session_token().unwrap(); + let session_token = session_manager().try_acquire_open_session_token().unwrap(); let session_id = session_token.session_id().unwrap(); let loaded_program = shim - .load_ldelf( - ldelf_bin, - TeeUuid::default(), - Some(ta_bin), - None, - session_id, - ) + .load_ldelf(ldelf_bin, TeeUuid::default(), Some(ta_bin)) .map_err(|_| { panic!("Failed to load ldelf"); }) @@ -140,7 +133,7 @@ fn run_ta_with_default_commands( // loaded binary and heap. In that sense, we can create (and destroy) a stack // for each command freely. let _ = entrypoints - .load_ta_context(params.as_slice(), None, func_id as u32, None) + .load_ta_context(params.as_slice(), session_id, func_id as u32, None) .map_err(|_| { panic!("Failed to load TA context"); }); diff --git a/litebox_runner_optee_on_linux_userland/src/tests.rs b/litebox_runner_optee_on_linux_userland/src/tests.rs index 9bfe30f8ec..645055431e 100644 --- a/litebox_runner_optee_on_linux_userland/src/tests.rs +++ b/litebox_runner_optee_on_linux_userland/src/tests.rs @@ -7,8 +7,10 @@ use litebox::platform::RawConstPointer; use litebox::utils::TruncateExt; -use litebox_common_optee::{TeeParamType, UteeEntryFunc, UteeParamOwned, UteeParams}; -use litebox_shim_optee::session::SessionManager; +use litebox_common_optee::{ + TeeIdentity, TeeLogin, TeeParamType, TeeUuid, UteeEntryFunc, UteeParamOwned, UteeParams, +}; +use litebox_shim_optee::session::session_manager; use litebox_shim_optee::{LoadedProgram, UserConstPtr}; use serde::Deserialize; use std::path::PathBuf; @@ -26,7 +28,9 @@ pub fn run_ta_with_test_commands( serde_json::from_str(&json_str).unwrap() }; let mut ta_info: Option = None; - let session_manager = SessionManager::new(); + // The active session id for the TA. Set at OpenSession and reused for the + // subsequent InvokeCommand entries on the same persistent session. + let mut session_id: Option = None; for cmd in ta_commands { assert!( @@ -50,15 +54,20 @@ pub fn run_ta_with_test_commands( if func_id == UteeEntryFunc::OpenSession { let ta_head = litebox_common_optee::parse_ta_head(ta_bin) .expect("Failed to parse TA header from ta_bin"); - let session_token = session_manager.try_acquire_open_session_token().unwrap(); + let mut session_token = session_manager().try_acquire_open_session_token().unwrap(); + let open_session_id = session_token.session_id().unwrap(); + session_id = Some(open_session_id); + // Emulate the client identity a real REE client would present. + let client_identity = cmd.client_identity.as_ref().map_or( + TeeIdentity { + login: TeeLogin::User, + uuid: TeeUuid::NIL, + }, + ClientIdentityJson::to_tee_identity, + ); + session_manager().set_session_client_identity(open_session_id, Some(client_identity)); let loaded = shim - .load_ldelf( - ldelf_bin, - ta_head.uuid, - Some(ta_bin), - None, - session_token.session_id().unwrap(), - ) + .load_ldelf(ldelf_bin, ta_head.uuid, Some(ta_bin)) .map_err(|_| { panic!("Failed to load TA"); }) @@ -77,17 +86,28 @@ pub fn run_ta_with_test_commands( "ldelf exits with error: return_code={:#x}", ctx.rax ); + // The session persists across all commands, so disarm the token: + // its drop must not recycle the id or clear the client identity. + session_token.disarm(); } if let Some(info) = ta_info.as_mut() { // In OP-TEE TA, each command invocation is like (re)starting the TA with a new stack with // loaded binary and heap. In that sense, we can create (and destroy) a stack // for each command freely. + // `ta_info` is only `Some` after an OpenSession, which also sets + // `session_id`, so this command runs on that established session. + let session_id = session_id.expect("session id set by OpenSession"); let _ = info .entrypoints .as_ref() .unwrap() - .load_ta_context(params.as_slice(), None, func_id as u32, Some(cmd.cmd_id)) + .load_ta_context( + params.as_slice(), + session_id, + func_id as u32, + Some(cmd.cmd_id), + ) .map_err(|_| { panic!("Failed to load TA context"); }); @@ -175,6 +195,71 @@ pub struct TaCommandBase64 { cmd_id: u32, #[serde(default)] args: Vec, + #[serde(default)] + client_identity: Option, +} + +/// Client identity for an `OpenSession`, parsed from the test JSON. +#[derive(Debug, Deserialize)] +struct ClientIdentityJson { + #[serde(default)] + login: ClientLoginJson, + #[serde(default)] + uuid: Option, +} + +/// JSON mirror of [`TeeLogin`]. +#[derive(Debug, Default, Clone, Copy, Deserialize)] +#[serde(rename_all = "snake_case")] +enum ClientLoginJson { + Public, + #[default] + User, + Group, + Application, + ApplicationUser, + ApplicationGroup, + ReeKernel, + TrustedApp, +} + +impl From for TeeLogin { + fn from(login: ClientLoginJson) -> Self { + match login { + ClientLoginJson::Public => TeeLogin::Public, + ClientLoginJson::User => TeeLogin::User, + ClientLoginJson::Group => TeeLogin::Group, + ClientLoginJson::Application => TeeLogin::Application, + ClientLoginJson::ApplicationUser => TeeLogin::ApplicationUser, + ClientLoginJson::ApplicationGroup => TeeLogin::ApplicationGroup, + ClientLoginJson::ReeKernel => TeeLogin::ReeKernel, + ClientLoginJson::TrustedApp => TeeLogin::TrustedApp, + } + } +} + +impl ClientIdentityJson { + fn to_tee_identity(&self) -> TeeIdentity { + let uuid = self + .uuid + .as_deref() + .map_or(TeeUuid::NIL, parse_uuid_or_panic); + TeeIdentity { + login: self.login.into(), + uuid, + } + } +} + +fn parse_uuid_or_panic(s: &str) -> TeeUuid { + let hex: String = s.chars().filter(|&c| c != '-').collect(); + assert_eq!(hex.len(), 32, "client uuid must be 32 hex digits: {s:?}"); + let mut bytes = [0u8; 16]; + for (i, byte) in bytes.iter_mut().enumerate() { + *byte = u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16) + .unwrap_or_else(|_| panic!("invalid hex in client uuid: {s:?}")); + } + TeeUuid::from_bytes(bytes) } #[derive(Debug, Deserialize)] diff --git a/litebox_runner_optee_on_linux_userland/tests/aes-ta-cmds.json b/litebox_runner_optee_on_linux_userland/tests/aes-ta-cmds.json index 8bc266ac0b..a80a3bcd09 100644 --- a/litebox_runner_optee_on_linux_userland/tests/aes-ta-cmds.json +++ b/litebox_runner_optee_on_linux_userland/tests/aes-ta-cmds.json @@ -1,6 +1,9 @@ [ { - "func_id": "open_session" + "func_id": "open_session", + "client_identity": { + "login": "user" + } }, { "func_id": "invoke_command", diff --git a/litebox_runner_optee_on_linux_userland/tests/hello-ta-cmds.json b/litebox_runner_optee_on_linux_userland/tests/hello-ta-cmds.json index a55e89770a..9d87bf1087 100644 --- a/litebox_runner_optee_on_linux_userland/tests/hello-ta-cmds.json +++ b/litebox_runner_optee_on_linux_userland/tests/hello-ta-cmds.json @@ -1,6 +1,9 @@ [ { - "func_id": "open_session" + "func_id": "open_session", + "client_identity": { + "login": "user" + } }, { "func_id": "invoke_command", diff --git a/litebox_runner_optee_on_linux_userland/tests/kmpp-ta-cmds.json b/litebox_runner_optee_on_linux_userland/tests/kmpp-ta-cmds.json index 9eb2e0cee8..88d5e41870 100644 --- a/litebox_runner_optee_on_linux_userland/tests/kmpp-ta-cmds.json +++ b/litebox_runner_optee_on_linux_userland/tests/kmpp-ta-cmds.json @@ -1,6 +1,9 @@ [ { "func_id": "open_session", + "client_identity": { + "login": "user" + }, "args": [ { "param_type": "value_input", @@ -31,6 +34,9 @@ }, { "func_id": "open_session", + "client_identity": { + "login": "user" + }, "args": [ { "param_type": "value_input", diff --git a/litebox_runner_optee_on_linux_userland/tests/random-ta-cmds.json b/litebox_runner_optee_on_linux_userland/tests/random-ta-cmds.json index 166b1d7180..8df2c67dc4 100644 --- a/litebox_runner_optee_on_linux_userland/tests/random-ta-cmds.json +++ b/litebox_runner_optee_on_linux_userland/tests/random-ta-cmds.json @@ -1,6 +1,9 @@ [ { - "func_id": "open_session" + "func_id": "open_session", + "client_identity": { + "login": "user" + } }, { "func_id": "invoke_command", diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index 3f9cdf9235..47daf4b590 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -80,6 +80,7 @@ impl litebox::shim::EnterShim for OpteeShimEntrypoints { return if result.is_ok() { ContinueOperation::Resume } else { + self.task.clear_ta_context(); ContinueOperation::Terminate }; } else if result.is_ok() { @@ -90,6 +91,7 @@ impl litebox::shim::EnterShim for OpteeShimEntrypoints { } // OP-TEE has no signal handling. Kill the TA on any non-PF exception. ctx.rax = (TeeResult::TargetDead as u32) as usize; + self.task.clear_ta_context(); ContinueOperation::Terminate } @@ -228,26 +230,25 @@ pub struct OpteeShim(Arc); impl OpteeShim { /// Load the given `ldelf` binary into memory while making it ready to load the TA binary specified - /// by `ta_uuid` (and optionally `ta_bin`). `client` specifies the one requesting the TA load. + /// by `ta_uuid` (and optionally `ta_bin`). + /// + /// The loaded program is an *instance*: a single instance can serve many + /// sessions. The active session id is supplied per entry via + /// [`OpteeShimEntrypoints::load_ta_context`], and the caller's identity is + /// recorded per session in the session registry via + /// [`session::SessionManager::set_session_client_identity`]. pub fn load_ldelf( &self, ldelf_bin: &[u8], ta_uuid: TeeUuid, ta_bin: Option<&[u8]>, - client: Option, - session_id: u32, ) -> Result { let entrypoints = crate::OpteeShimEntrypoints { _not_send: core::marker::PhantomData, task: Task { global: self.0.clone(), thread: ThreadState::new(), - session_id, ta_app_id: ta_uuid, - client_identity: client.unwrap_or(TeeIdentity { - login: TeeLogin::User, - uuid: TeeUuid::default(), - }), tee_cryp_state_map: TeeCrypStateMap::new(), tee_obj_map: TeeObjMap::new(), ta_handle_map: TaHandleMap::new(), @@ -314,7 +315,7 @@ impl OpteeShimEntrypoints { pub fn load_ta_context( &self, params: &[litebox_common_optee::UteeParamOwned], - session_id: Option, + session_id: u32, func_id: u32, cmd_id: Option, ) -> Result<(), loader::elf::ElfLoaderError> { @@ -324,10 +325,6 @@ impl OpteeShimEntrypoints { self.task.thread.init_state.set(init_state); Ok(()) } - - pub fn get_session_id(&self) -> u32 { - self.task.session_id - } } /// Information about a loaded TA program. @@ -380,9 +377,11 @@ impl Task { if let SyscallRequest::Return { ret } = request { ctx.rax = self.sys_return(ret); + self.clear_ta_context(); return ContinueOperation::Terminate; } else if let SyscallRequest::Panic { code } = request { ctx.rax = self.sys_panic(code); + self.clear_ta_context(); return ContinueOperation::Terminate; } let res: Result<(), TeeResult> = match request { @@ -760,7 +759,7 @@ impl Task { fn load_ta_context( &self, params: &[litebox_common_optee::UteeParamOwned], - session_id: Option, + session_id: u32, func_id: u32, cmd_id: Option, ) -> Result { @@ -792,13 +791,42 @@ impl Task { Ok(ThreadInitState::Ta { cmd_id: cmd_id.unwrap_or(0) as usize, params_address: ta_stack.get_params_address(), - session_id: session_id.unwrap_or(self.session_id) as usize, + session_id: session_id as usize, func_id: func_id as usize, entry_point: self.get_ta_entry_point(), stack_top: ta_stack.get_cur_stack_top(), }) } + /// The session id currently executing in this task (set per entry by + /// [`Self::load_ta_context`], cleared on entry termination by + /// [`Self::clear_ta_context`]). Returns `None` outside a TA entry. + fn current_session_id(&self) -> Option { + match self.thread.init_state.get() { + ThreadInitState::Ta { session_id, .. } => Some(session_id.trunc()), + _ => None, + } + } + + /// The client identity of the session currently executing in this task. + /// Falls back to the anonymous public client outside a TA entry. + fn current_client_identity(&self) -> TeeIdentity { + self.current_session_id().map_or( + TeeIdentity { + login: TeeLogin::Public, + uuid: TeeUuid::NIL, + }, + |session_id| crate::session::session_manager().client_identity(session_id), + ) + } + + /// Clear the per-entry TA execution state once a TA entry has terminated. + fn clear_ta_context(&self) { + if matches!(self.thread.init_state.get(), ThreadInitState::Ta { .. }) { + self.thread.init_state.set(ThreadInitState::None); + } + } + /// Allocate the guest TLS for an OP-TEE TA. /// /// This function is required to overcome the compatibility issue coming from @@ -1305,16 +1333,14 @@ impl TaUuidMap { } } -/// TA/session-related information for the current task +/// Per-instance TA state which can be shared between sessions if it is +/// a single-instance multi-session TA. The active session id is carried +/// per entry (see [`Task::current_session_id`]). struct Task { global: Arc, thread: ThreadState, - /// Session ID - session_id: u32, /// TA UUID ta_app_id: TeeUuid, - /// Client identity (VTL0 process or another TA) - client_identity: TeeIdentity, /// TEE cryptography state map tee_cryp_state_map: TeeCrypStateMap, /// TEE object map @@ -1476,12 +1502,7 @@ mod test_utils { Task { global: self.clone(), thread: ThreadState::new(), - session_id: SessionIdPool::allocate().unwrap(), ta_app_id: TeeUuid::default(), - client_identity: TeeIdentity { - login: TeeLogin::User, - uuid: TeeUuid::default(), - }, tee_cryp_state_map: TeeCrypStateMap::new(), tee_obj_map: TeeObjMap::new(), ta_handle_map: TaHandleMap::new(), diff --git a/litebox_shim_optee/src/msg_handler.rs b/litebox_shim_optee/src/msg_handler.rs index 024df141a9..303129239e 100644 --- a/litebox_shim_optee/src/msg_handler.rs +++ b/litebox_shim_optee/src/msg_handler.rs @@ -436,25 +436,43 @@ pub fn decode_ta_request( let (ta_uuid, client_identity, skip): (Option, Option, usize) = if ta_entry_func == UteeEntryFunc::OpenSession { - // If it is an OpenSession request, extract UUIDs and login from params[0] and params[1] - // Based on observed Linux kernel behavior: + // If it is an OpenSession request, extract the TA UUID, client UUID, + // and login from the two meta params. Wire layout (per the Linux + // OP-TEE driver): // - params[0].a/b = TA UUID (two little-endian u64 values) - // - params[1].a/b = client UUID (two little-endian u64 values) + // - params[1].a/b = client UUID, but only meaningful for the + // user/group/application logins; PUBLIC/REE_KERNEL ignore it and + // report the nil UUID (see the match below). // - params[1].c = client login type (TEE_LOGIN_*) - let param0 = msg_args.get_param_value(0)?; + let param0 = msg_args.get_meta_param_value(0)?; let ta_data = [param0.a, param0.b]; - let param1 = msg_args.get_param_value(1)?; - let client_data = [param1.a, param1.b]; + let param1 = msg_args.get_meta_param_value(1)?; let login: u32 = param1.c.trunc(); - let login = TeeLogin::try_from(login).unwrap_or(TeeLogin::Public); + // Reject unknown login methods + let login = TeeLogin::try_from(login).map_err(|_| OpteeSmcReturnCode::EBadCmd)?; + + // Only the REE-derived user/group/application logins carry a + // meaningful client UUID. PUBLIC and REE_KERNEL clients are anonymous + // and must report the nil UUID (OP-TEE OS memsets it to zero). + // TRUSTED_APP identifies a TA-to-TA caller and is established + // internally, never from a normal-world message. + let client_uuid = match login { + TeeLogin::Public | TeeLogin::ReeKernel => TeeUuid::NIL, + TeeLogin::User + | TeeLogin::Group + | TeeLogin::Application + | TeeLogin::ApplicationUser + | TeeLogin::ApplicationGroup => TeeUuid::from_u64_array([param1.a, param1.b]), + TeeLogin::TrustedApp => return Err(OpteeSmcReturnCode::EBadCmd), + }; // Skip the first two parameters as they convey TA and client UUIDs ( Some(TeeUuid::from_u64_array(ta_data)), Some(TeeIdentity { login, - uuid: TeeUuid::from_u64_array(client_data), + uuid: client_uuid, }), 2, ) @@ -485,6 +503,12 @@ pub fn decode_ta_request( .skip(skip) .enumerate() { + // The meta bit marks the OpenSession TA-UUID/client-identity params, + // which were already consumed via `skip`. A client parameter must not + // carry it (mirrors OP-TEE OS `copy_in_params`). + if param.is_meta() { + return Err(OpteeSmcReturnCode::EBadCmd); + } ta_req_info.params[i] = match param.attr_type() { OpteeMsgAttrType::None => UteeParamOwned::None, OpteeMsgAttrType::ValueInput => { diff --git a/litebox_shim_optee/src/session.rs b/litebox_shim_optee/src/session.rs index 3e462da290..17fdc4edeb 100644 --- a/litebox_shim_optee/src/session.rs +++ b/litebox_shim_optee/src/session.rs @@ -106,12 +106,19 @@ use crate::{LoadedProgram, OpteeShim, SessionIdPool}; use alloc::sync::Arc; use core::sync::atomic::{AtomicBool, Ordering}; use hashbrown::{HashMap, HashSet}; -use litebox_common_optee::{OpteeSmcReturnCode, TaFlags, TeeUuid}; +use litebox_common_optee::{OpteeSmcReturnCode, TaFlags, TeeIdentity, TeeLogin, TeeUuid}; use spin::mutex::SpinMutex; /// Maximum number of concurrent TA instances to avoid out of memory situations. const MAX_TA_INSTANCES: usize = 16; +/// The anonymous public client identity. Used as the fallback when no per-session +/// identity is recorded, matching OP-TEE OS / the Linux driver. +const ANONYMOUS_CLIENT_IDENTITY: TeeIdentity = TeeIdentity { + login: TeeLogin::Public, + uuid: TeeUuid::NIL, +}; + /// A loaded TA instance. /// /// For single-instance TAs one instance is shared across all sessions; the @@ -414,6 +421,10 @@ impl Drop for SessionToken<'_> { if let Some(id) = self.active_session_id.take() { self.manager.active_sessions.lock().remove(&id); if self.owns_id_recycling { + // The session was never published (an OpenSession failure + // path), so the id is recycled here. Drop any client identity + // recorded for it to avoid unnecessary memory leak. + self.manager.clear_session_client_identity(id); recycle_session_id(id); } } @@ -463,6 +474,18 @@ pub struct SessionManager { /// Session ids currently being handled (Invoke/Close). Guards a session /// against concurrent SMC entry by another core that targets the same id. active_sessions: SpinMutex>, + /// Per-session client identity, matching OP-TEE OS's `tee_ta_session.clnt_id`. + /// + /// Populated before the OpenSession entry point runs and removed when + /// the session is unregistered. + session_client_identities: SpinMutex>, +} + +/// Get the global session manager. +pub fn session_manager() -> &'static SessionManager { + static SESSION_MANAGER: once_cell::race::OnceBox = + once_cell::race::OnceBox::new(); + SESSION_MANAGER.get_or_init(|| alloc::boxed::Box::new(SessionManager::new())) } impl SessionManager { @@ -475,6 +498,7 @@ impl SessionManager { single_instance_locks: SpinMutex::new(HashMap::new()), ta_load_lock: AtomicBool::new(false), active_sessions: SpinMutex::new(HashSet::new()), + session_client_identities: SpinMutex::new(HashMap::new()), } } @@ -790,12 +814,39 @@ impl SessionManager { Ok(()) } + /// Record the client identity for `session_id`. + pub fn set_session_client_identity(&self, session_id: u32, identity: Option) { + self.session_client_identities + .lock() + .insert(session_id, identity.unwrap_or(ANONYMOUS_CLIENT_IDENTITY)); + } + + /// The client identity recorded for `session_id`, or the anonymous public + /// client if none was recorded. + pub(crate) fn client_identity(&self, session_id: u32) -> TeeIdentity { + self.session_client_identities + .lock() + .get(&session_id) + .copied() + .unwrap_or(ANONYMOUS_CLIENT_IDENTITY) + } + + /// Drop the recorded client identity for `session_id`. + /// + /// Called directly only on OpenSession rollback paths (the TA's OpenSession + /// failed, so the session is never published). The normal close path calls + /// this indirectly via [`Self::unregister_session`]. + pub fn clear_session_client_identity(&self, session_id: u32) { + self.session_client_identities.lock().remove(&session_id); + } + /// Unregister a session and recycle its session ID. Returns whether /// the session was registered and what flags it had (the latter for /// callers that need to dispatch on `is_single_instance` / /// `is_keep_alive` after removal). pub fn unregister_session(&self, session_id: u32) -> Option { let entry = self.sessions.remove(session_id); + self.clear_session_client_identity(session_id); if entry.is_some() { recycle_session_id(session_id); } diff --git a/litebox_shim_optee/src/syscalls/tee.rs b/litebox_shim_optee/src/syscalls/tee.rs index 1b5945c6ad..088a05936a 100644 --- a/litebox_shim_optee/src/syscalls/tee.rs +++ b/litebox_shim_optee/src/syscalls/tee.rs @@ -95,7 +95,7 @@ impl Task { if prop_buf.len() < core::mem::size_of::() { return Err(TeeResult::ShortBuffer); } - let identity = self.client_identity; + let identity = self.current_client_identity(); prop_buf[..core::mem::size_of::()] .copy_from_slice(identity.as_bytes()); prop_len @@ -106,6 +106,24 @@ impl Task { .ok_or(TeeResult::AccessDenied)?; Ok(()) } + GpdPropertyIndex::ClientEndian => { + const CLIENT_ENDIAN_LITTLE: u32 = 0; + if prop_set != TeePropSet::CurrentClient { + return Err(TeeResult::BadParameters); + } + if prop_buf.len() < core::mem::size_of::() { + return Err(TeeResult::ShortBuffer); + } + prop_buf[..core::mem::size_of::()] + .copy_from_slice(&CLIENT_ENDIAN_LITTLE.to_le_bytes()); + prop_len + .write_at_offset(0, core::mem::size_of::().trunc()) + .ok_or(TeeResult::AccessDenied)?; + prop_type + .write_at_offset(0, UserTaPropType::U32 as u32) + .ok_or(TeeResult::AccessDenied)?; + Ok(()) + } GpdPropertyIndex::CurrentTaUuid => { if prop_set != TeePropSet::CurrentTa { return Err(TeeResult::BadParameters); @@ -149,6 +167,16 @@ impl Task { Err(TeeResult::BadParameters) } } + "gpd.client.endian" => { + if prop_set == TeePropSet::CurrentClient { + index + .write_at_offset(0, GpdPropertyIndex::ClientEndian as u32) + .ok_or(TeeResult::AccessDenied)?; + Ok(()) + } else { + Err(TeeResult::BadParameters) + } + } "gpd.ta.appID" => { if prop_set == TeePropSet::CurrentTa { index @@ -299,6 +327,7 @@ impl Task { #[repr(u32)] pub enum GpdPropertyIndex { ClientIdentity = 0xffff_0000, - CurrentTaUuid = 0xffff_0001, + ClientEndian = 0xffff_0001, + CurrentTaUuid = 0xffff_0002, None = 0xffff_ffff, } From 13c994a6d23b7e89851712158327e0400943a3b2 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Jul 2026 09:38:12 -0700 Subject: [PATCH 083/319] Wire broker readiness notifications (#992) Adds paired broker host serving over control and notification channels, wiring broker-userland and the Linux userland runner through `--broker-control-socket` and `--broker-notification-socket` Unix endpoints. The broker core remains transport-neutral, while the runner now opens and drains the notification stream so future broker-originated wakeups have a real local endpoint. Event mutation readiness still returns through the control response, avoiding duplicate readiness notifications for control-originated changes. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_host/src/lib.rs | 152 ++++++++++++++---- litebox_broker_local/src/lib.rs | 66 +++++++- litebox_broker_userland/src/main.rs | 28 +++- .../tests/notification_runtime.rs | 45 ++++++ .../tests/userland_broker.rs | 64 +++++--- litebox_runner_linux_userland/src/broker.rs | 93 ++++++++--- litebox_runner_linux_userland/src/lib.rs | 33 +++- litebox_runner_linux_userland/tests/run.rs | 85 +++++++--- 8 files changed, 454 insertions(+), 112 deletions(-) create mode 100644 litebox_broker_userland/tests/notification_runtime.rs diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 20fd3cb3d0..905745bb5c 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -14,7 +14,9 @@ extern crate std; use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential}; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; -use litebox_broker_protocol::channel::{HostControlChannel, HostReceive, PeerCredential}; +use litebox_broker_protocol::channel::{ + HostControlChannel, HostNotificationChannel, HostReceive, PeerCredential, +}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse, WaitEventResponse}; use litebox_broker_protocol::message::{ @@ -25,15 +27,24 @@ mod error; pub use error::{BrokerHostError, Result}; -/// Authenticates, negotiates, and serves one broker connection over the control channel. -pub fn serve_connection( +/// Authenticates, negotiates, and serves one broker association over paired +/// control and notification channels. +/// +/// The deployment must bind both channels to the same authenticated peer +/// association. Active requests and responses remain on the control channel; +/// broker-initiated readiness wakeups are sent on the notification channel. +/// Event mutations caused by control requests return readiness in their control +/// response and do not also emit a duplicate notification. +pub fn serve_connection( core: &BrokerCore, - channel: &mut Channel, -) -> Result + control_channel: &mut ControlChannel, + _notification_channel: &mut NotificationChannel, +) -> Result where - Channel: HostControlChannel, + ControlChannel: HostControlChannel, + NotificationChannel: HostNotificationChannel, { - let peer_credential = channel + let peer_credential = control_channel .peer_credential() .map_err(BrokerHostError::Channel)?; let caller_credential = match peer_credential { @@ -43,13 +54,13 @@ where let session = core.create_session(caller_credential)?; loop { - let request = match channel + let request = match control_channel .recv_handshake_request() .map_err(BrokerHostError::Channel)? { HostReceive::Message(request) => request, HostReceive::ProtocolViolation => { - channel + control_channel .send_handshake_response(&BrokerHandshakeResponse::Error( ErrorCode::ProtocolState, )) @@ -69,7 +80,7 @@ where broker_protocol_version: BROKER_PROTOCOL_VERSION, } }; - channel + control_channel .send_handshake_response(&response) .map_err(BrokerHostError::Channel)?; if negotiated { @@ -77,21 +88,14 @@ where } } - serve_request_loop(channel, &session) -} - -fn serve_request_loop( - channel: &mut Channel, - session: &BrokerSession, -) -> Result -where - Channel: HostControlChannel, -{ loop { - let request = match channel.recv_request().map_err(BrokerHostError::Channel)? { + let request = match control_channel + .recv_request() + .map_err(BrokerHostError::Channel)? + { HostReceive::Message(request) => request, HostReceive::ProtocolViolation => { - channel + control_channel .send_response(&BrokerResponse::Error(ErrorCode::ProtocolState)) .map_err(BrokerHostError::Channel)?; return Ok(ConnectionTermination::ProtocolViolation); @@ -99,8 +103,8 @@ where HostReceive::PeerClosed => break, }; - let response = handle_request(session, request); - channel + let response = handle_request(&session, request); + control_channel .send_response(&response) .map_err(BrokerHostError::Channel)?; } @@ -167,8 +171,11 @@ pub enum ConnectionTermination { mod tests { use super::*; use litebox_broker_core::{PolicyEngine, PrincipalRights}; - use litebox_broker_protocol::event::{CreateEventRequest, WaitEventRequest}; - use litebox_broker_protocol::message::BrokerHandshakeRequest; + use litebox_broker_protocol::event::{ + AddEventRequest, ConsumeEventRequest, CreateEventRequest, EventConsumeMode, + WaitEventRequest, + }; + use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerNotification}; use litebox_broker_protocol::{ObjectHandle, ProtocolVersion}; #[test] @@ -183,6 +190,7 @@ mod tests { serve_connection_rejects_active_request_before_negotiation(&broker); serve_connection_rejects_handshake_request_after_negotiation(&broker); serve_connection_returns_channel_error_when_response_send_fails(&broker); + serve_connection_returns_event_readiness_in_control_responses(&broker); active_request_closes_object_reference(&broker); } @@ -198,9 +206,10 @@ mod tests { Ok(HostReceive::PeerClosed), ]), ); + let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel).unwrap(), + serve_connection(broker, &mut channel, &mut notifications).unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -228,9 +237,10 @@ mod tests { ]), std::vec::Vec::from([Ok(HostReceive::PeerClosed)]), ); + let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel).unwrap(), + serve_connection(broker, &mut channel, &mut notifications).unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -251,9 +261,10 @@ mod tests { std::vec::Vec::from([Ok(HostReceive::ProtocolViolation)]), std::vec::Vec::new(), ); + let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel).unwrap(), + serve_connection(broker, &mut channel, &mut notifications).unwrap(), ConnectionTermination::ProtocolViolation ); assert_eq!( @@ -270,9 +281,10 @@ mod tests { }))]), std::vec::Vec::from([Ok(HostReceive::ProtocolViolation)]), ); + let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel).unwrap(), + serve_connection(broker, &mut channel, &mut notifications).unwrap(), ConnectionTermination::ProtocolViolation ); assert_eq!( @@ -295,14 +307,54 @@ mod tests { std::vec::Vec::new(), ); channel.send_error = true; + let mut notifications = FakeHostNotificationChannel::default(); - match serve_connection(broker, &mut channel) { + match serve_connection(broker, &mut channel, &mut notifications) { Err(BrokerHostError::Channel(())) => {} result => panic!("unexpected serve result: {result:?}"), } assert!(channel.handshake_responses.is_empty()); } + fn serve_connection_returns_event_readiness_in_control_responses(broker: &BrokerCore) { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + }))]), + std::vec::Vec::from([Ok(HostReceive::Message(BrokerRequest::Event( + EventRequest::Create(CreateEventRequest { initial_count: 0 }), + )))]), + ); + channel.enqueue_readiness_requests_after_create = true; + let mut notifications = FakeHostNotificationChannel::default(); + + assert_eq!( + serve_connection(broker, &mut channel, &mut notifications).unwrap(), + ConnectionTermination::PeerClosed + ); + assert!(notifications.notifications.is_empty()); + assert_eq!( + &channel.responses[1..], + [ + BrokerResponse::Event(EventResponse::Add(AddEventResponse { + readiness: litebox_broker_protocol::event::ReadinessState { + read_ready: true, + write_ready: true, + }, + })), + BrokerResponse::Event(EventResponse::Consume( + litebox_broker_protocol::event::ConsumeEventResponse { + value: 1, + readiness: litebox_broker_protocol::event::ReadinessState { + read_ready: false, + write_ready: true, + }, + } + )), + ] + ); + } + fn active_request_closes_object_reference(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) @@ -344,6 +396,7 @@ mod tests { requests: std::vec::Vec, ()>>, handshake_responses: std::vec::Vec, responses: std::vec::Vec, + enqueue_readiness_requests_after_create: bool, send_error: bool, } @@ -359,6 +412,7 @@ mod tests { requests, handshake_responses: std::vec::Vec::new(), responses: std::vec::Vec::new(), + enqueue_readiness_requests_after_create: false, send_error: false, } } @@ -409,8 +463,44 @@ mod tests { if self.send_error { return Err(()); } + if self.enqueue_readiness_requests_after_create + && let BrokerResponse::Event(EventResponse::Create(response)) = response + { + self.requests + .push(Ok(HostReceive::Message(BrokerRequest::Event( + EventRequest::Add(AddEventRequest { + handle: response.handle, + value: 1, + }), + )))); + self.requests + .push(Ok(HostReceive::Message(BrokerRequest::Event( + EventRequest::Consume(ConsumeEventRequest { + handle: response.handle, + mode: EventConsumeMode::One, + }), + )))); + self.requests.push(Ok(HostReceive::PeerClosed)); + } self.responses.push(response.clone()); Ok(()) } } + + #[derive(Default)] + struct FakeHostNotificationChannel { + notifications: std::vec::Vec, + } + + impl HostNotificationChannel for FakeHostNotificationChannel { + type Error = (); + + fn send_notification( + &mut self, + notification: &BrokerNotification, + ) -> core::result::Result<(), Self::Error> { + self.notifications.push(notification.clone()); + Ok(()) + } + } } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index f90e481af1..8c2ceecd5c 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Typed broker-local control adapter for broker requests. +//! Typed broker-local adapters for broker requests and notifications. //! //! The local control adapter owns request/response sequencing but does not own a channel. //! Userland, kernel, or ring-buffer deployments can provide channels by //! implementing [`litebox_broker_protocol::channel::LocalControlChannel`]. +//! Notification receive adapters are intentionally separate so active control +//! requests remain strictly paired with their responses. #![no_std] @@ -15,10 +17,11 @@ extern crate std; mod error; mod event; -use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::channel::{LocalControlChannel, LocalNotificationChannel}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse, + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, + BrokerResponse, }; use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; @@ -29,6 +32,11 @@ pub struct BrokerLocal { channel: Channel, } +/// Broker-local receive adapter for broker-initiated asynchronous notifications. +pub struct BrokerNotifications { + channel: Channel, +} + impl BrokerLocal { /// Negotiates the broker protocol over an already-connected control channel. /// @@ -124,14 +132,33 @@ impl BrokerLocal { } } +impl BrokerNotifications { + /// Creates a notification receiver from an already-associated notification channel. + pub const fn new(channel: Channel) -> Self { + Self { channel } + } + + /// Receives the next broker notification. + /// + /// Returns `Ok(None)` when the broker closed the notification channel cleanly. + pub fn recv_notification(&mut self) -> Result, Channel::Error> { + self.channel + .recv_notification() + .map_err(BrokerLocalError::Channel) + } +} + #[cfg(test)] mod tests { use super::*; use core::convert::Infallible; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::ProtocolVersion; + use litebox_broker_protocol::channel::LocalNotificationChannel; use litebox_broker_protocol::event::{CreateEventRequest, CreateEventResponse}; - use litebox_broker_protocol::message::{EventRequest, EventResponse}; + use litebox_broker_protocol::message::{ + EventReadinessNotification, EventRequest, EventResponse, + }; #[test] fn negotiate_returns_active_local_connection() { @@ -219,6 +246,23 @@ mod tests { let _ = BrokerLocal::negotiate(channel); } + #[test] + fn notification_receiver_returns_broker_notifications() { + let notification = BrokerNotification::EventReadiness(EventReadinessNotification { + handle: ObjectHandle(7), + readiness: litebox_broker_protocol::event::ReadinessState { + read_ready: true, + write_ready: false, + }, + }); + let mut receiver = BrokerNotifications::new(FakeNotificationChannel { + notification: Some(notification.clone()), + }); + + assert_eq!(receiver.recv_notification().unwrap(), Some(notification)); + assert_eq!(receiver.recv_notification().unwrap(), None); + } + #[test] fn negotiate_rejects_broker_unsupported_version_response() { let broker_protocol_version = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); @@ -296,4 +340,18 @@ mod tests { Ok(self.response.take()) } } + + struct FakeNotificationChannel { + notification: Option, + } + + impl LocalNotificationChannel for FakeNotificationChannel { + type Error = Infallible; + + fn recv_notification( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(self.notification.take()) + } + } } diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index b6c2ba559b..5363b308bf 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -10,7 +10,9 @@ use std::process::Command; use clap::Parser; use litebox_broker_core::{BrokerCore, PolicyEngine, PrincipalRights}; use litebox_broker_host::serve_connection; -use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; +use litebox_broker_transport::unix_socket::{ + UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, +}; #[derive(Parser, Debug)] struct CliArgs { @@ -27,8 +29,10 @@ fn main() -> Result<(), Box> { let socket_dir = tempfile::Builder::new() .prefix("litebox-broker-userland-") .tempdir()?; - let socket_path = socket_dir.path().join("broker.sock"); - let listener = UnixListener::bind(&socket_path)?; + let control_socket_path = socket_dir.path().join("broker.sock"); + let notification_socket_path = socket_dir.path().join("broker-notification.sock"); + let control_listener = UnixListener::bind(&control_socket_path)?; + let notification_listener = UnixListener::bind(¬ification_socket_path)?; let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( PrincipalRights::all(), ))?; @@ -36,8 +40,10 @@ fn main() -> Result<(), Box> { let mut runner_command = Command::new(&args.runner); runner_command .arg("--unstable") - .arg("--broker-socket") - .arg(&socket_path) + .arg("--broker-control-socket") + .arg(&control_socket_path) + .arg("--broker-notification-socket") + .arg(¬ification_socket_path) .args(&args.runner_arguments); let mut runner = runner_command.spawn()?; let _runner_waiter = std::thread::spawn(move || { @@ -47,13 +53,19 @@ fn main() -> Result<(), Box> { }); loop { - let (stream, _) = listener.accept()?; + let (control_stream, _) = control_listener.accept()?; + let (notification_stream, _) = notification_listener.accept()?; let broker = broker.clone(); if let Err(error) = std::thread::Builder::new() .name("litebox-broker-connection".to_owned()) .spawn(move || { - let mut channel = UnixStreamHostControlChannel::from_accepted(stream); - if let Err(error) = serve_connection(&broker, &mut channel) { + let mut control_channel = + UnixStreamHostControlChannel::from_accepted(control_stream); + let mut notification_channel = + UnixStreamHostNotificationChannel::from_accepted(notification_stream); + if let Err(error) = + serve_connection(&broker, &mut control_channel, &mut notification_channel) + { eprintln!("failed to serve broker connection: {error}"); } }) diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs new file mode 100644 index 0000000000..9fd6fb3aa3 --- /dev/null +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::os::unix::net::UnixStream; + +use litebox_broker_core::{BrokerCore, PolicyEngine, PrincipalRights}; +use litebox_broker_host::{ConnectionTermination, serve_connection}; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::event::ReadinessState; +use litebox_broker_transport::unix_socket::{ + UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, UnixStreamLocalControlChannel, +}; + +#[test] +fn host_serves_control_requests_over_paired_userland_channels() { + let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( + PrincipalRights::all(), + )) + .unwrap(); + let (local_control, host_control) = UnixStream::pair().unwrap(); + let (_local_notification, host_notification) = UnixStream::pair().unwrap(); + + let host_thread = std::thread::spawn(move || { + let mut control = UnixStreamHostControlChannel::from_accepted(host_control); + let mut notification = UnixStreamHostNotificationChannel::from_accepted(host_notification); + serve_connection(&broker, &mut control, &mut notification) + }); + + let mut local = + BrokerLocal::negotiate(UnixStreamLocalControlChannel::from_connected(local_control)) + .unwrap(); + + let handle = local.create_event_with_count(0).unwrap(); + let readiness = ReadinessState { + read_ready: true, + write_ready: true, + }; + assert_eq!(local.add_event(handle, 1).unwrap(), readiness); + + drop(local); + assert_eq!( + host_thread.join().unwrap().unwrap(), + ConnectionTermination::PeerClosed + ); +} diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 6bc055aed4..ee126be51b 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -10,7 +10,9 @@ use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::event::ReadinessState; -use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; +use litebox_broker_transport::unix_socket::{ + UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, +}; const RUNNER_ARGUMENT: &str = "broker-userland-test-runner"; @@ -30,10 +32,11 @@ fn run_parent_test() { // This custom-harness integration test uses its own executable as the broker's // runner. Cargo starts this executable without broker args, so it runs the // parent path here. The broker then starts the same executable with the real - // runner argv (`--unstable --broker-socket `), which runs `run_fake_runner`. - // After the fake runner finishes its broker requests, it terminates the broker - // parent process; this lets the test exercise the long-running broker without a - // test-only shutdown path. + // runner argv (`--unstable --broker-control-socket + // --broker-notification-socket `), which runs `run_fake_runner`. After + // the fake runner finishes its broker requests, it terminates the broker + // parent process; this lets the test exercise the long-running broker + // without a test-only shutdown path. let mut broker = ChildGuard { child: Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")) .arg("--runner") @@ -61,17 +64,24 @@ fn run_fake_runner(args: &[OsString]) { ); assert_eq!( args.get(1).map(OsString::as_os_str), - Some(OsStr::new("--broker-socket")) + Some(OsStr::new("--broker-control-socket")) ); assert_eq!( args.get(3).map(OsString::as_os_str), + Some(OsStr::new("--broker-notification-socket")) + ); + assert_eq!( + args.get(5).map(OsString::as_os_str), Some(OsStr::new(RUNNER_ARGUMENT)) ); - assert_eq!(args.len(), 4, "unexpected runner arguments: {args:?}"); + assert_eq!(args.len(), 6, "unexpected runner arguments: {args:?}"); - let socket_path = args.get(2).unwrap(); - let channel = connect_with_retry(Path::new(socket_path)).unwrap(); - let mut local = BrokerLocal::negotiate(channel).unwrap(); + let control_socket_path = args.get(2).unwrap(); + let notification_socket_path = args.get(4).unwrap(); + let control_channel = connect_control_with_retry(Path::new(control_socket_path)).unwrap(); + let _notification_channel = + connect_notification_with_retry(Path::new(notification_socket_path)).unwrap(); + let mut local = BrokerLocal::negotiate(control_channel).unwrap(); let handle = local.create_event_with_count(0).unwrap(); assert_eq!( @@ -82,13 +92,11 @@ fn run_fake_runner(args: &[OsString]) { } ); - assert_eq!( - local.add_event(handle, 1).unwrap(), - ReadinessState { - read_ready: true, - write_ready: true, - } - ); + let readiness = ReadinessState { + read_ready: true, + write_ready: true, + }; + assert_eq!(local.add_event(handle, 1).unwrap(), readiness); assert_eq!( local.wait_event(handle).unwrap(), @@ -124,7 +132,7 @@ impl Drop for ChildGuard { } } -fn connect_with_retry(socket_path: &Path) -> Result { +fn connect_control_with_retry(socket_path: &Path) -> Result { let deadline = Instant::now() + Duration::from_secs(5); loop { match UnixStreamLocalControlChannel::connect_with_setup_deadline(socket_path, deadline) { @@ -141,3 +149,23 @@ fn connect_with_retry(socket_path: &Path) -> Result Result { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match UnixStreamLocalNotificationChannel::connect(socket_path) { + Ok(channel) => return Ok(channel), + Err(error) if Instant::now() < deadline => { + if error.kind() != ErrorKind::NotFound + && error.kind() != ErrorKind::ConnectionRefused + { + return Err(error); + } + std::thread::sleep(Duration::from_millis(10)); + } + Err(error) => return Err(error), + } + } +} diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 05737cae24..7d36e96be9 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -3,12 +3,15 @@ use std::{ path::Path, + thread::JoinHandle, time::{Duration, Instant}, }; use anyhow::{Context as _, Result}; -use litebox_broker_local::BrokerLocal; -use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; +use litebox_broker_local::{BrokerLocal, BrokerNotifications}; +use litebox_broker_transport::unix_socket::{ + UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, +}; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); const RETRY_DELAY: Duration = Duration::from_millis(20); @@ -16,13 +19,63 @@ type Local = BrokerLocal; pub(crate) struct BrokerConnection { local: Local, + #[expect( + dead_code, + reason = "keeps the notification receiver thread alive while the broker connection is installed" + )] + notification_receiver_thread: JoinHandle<()>, } -pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { - match socket_path { - Some(path) => connect_to_endpoint(path).map(Some), - None => Ok(None), - } +pub(crate) fn connect( + control_socket_path: &Path, + notification_socket_path: &Path, +) -> Result { + let setup_deadline = Instant::now() + SETUP_TIMEOUT; + let control_channel = connect_with_retry( + control_socket_path, + setup_deadline, + "timed out connecting to broker", + |path, deadline| UnixStreamLocalControlChannel::connect_with_setup_deadline(path, deadline), + ) + .with_context(|| { + format!( + "failed to connect to broker at {}", + control_socket_path.display() + ) + })?; + let notification_channel = connect_with_retry( + notification_socket_path, + setup_deadline, + "timed out connecting to broker notifications", + |path, _deadline| UnixStreamLocalNotificationChannel::connect(path), + ) + .with_context(|| { + format!( + "failed to connect to broker notifications at {}", + notification_socket_path.display() + ) + })?; + let local = BrokerLocal::negotiate(control_channel).context("broker negotiation failed")?; + let mut notifications = BrokerNotifications::new(notification_channel); + let notification_thread = std::thread::Builder::new() + .name("litebox-broker-notifications".to_owned()) + .spawn(move || { + loop { + match notifications.recv_notification() { + Ok(Some(_notification)) => {} + Ok(None) => break, + Err(error) => { + eprintln!("failed to receive broker notification: {error}"); + break; + } + } + } + }) + .context("failed to start broker notification receiver")?; + Ok(BrokerConnection { + local, + notification_receiver_thread: notification_thread, + }) } impl BrokerConnection { @@ -31,26 +84,18 @@ impl BrokerConnection { } } -fn connect_to_endpoint(socket_path: &Path) -> Result { - let setup_deadline = Instant::now() + SETUP_TIMEOUT; - let local = connect_with_retry(socket_path, setup_deadline) - .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; - Ok(BrokerConnection { local }) -} - -fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { +fn connect_with_retry( + socket_path: &Path, + setup_deadline: Instant, + timeout_message: &'static str, + mut connect: impl FnMut(&Path, Instant) -> std::io::Result, +) -> Result { loop { - match UnixStreamLocalControlChannel::connect_with_setup_deadline( - socket_path, - setup_deadline, - ) { - Ok(channel) => { - let local = BrokerLocal::negotiate(channel).context("broker negotiation failed")?; - return Ok(local); - } + match connect(socket_path, setup_deadline) { + Ok(channel) => return Ok(channel), Err(error) => { if Instant::now() >= setup_deadline { - return Err(error).context("timed out connecting to broker"); + return Err(error).context(timeout_message); } } } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index efe4406325..76c4e7b5e8 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -81,14 +81,24 @@ pub struct CliArgs { pub program_from_tar: bool, /// Broker-supplied Unix socket path for the local control channel. #[arg( - long = "broker-socket", + long = "broker-control-socket", value_name = "PATH", value_hint = clap::ValueHint::FilePath, hide = true, - requires = "unstable", + requires_all = ["unstable", "broker_notification_socket"], + help_heading = "Unstable Options" + )] + pub broker_control_socket: Option, + /// Broker-supplied Unix socket path for the local notification channel. + #[arg( + long = "broker-notification-socket", + value_name = "PATH", + value_hint = clap::ValueHint::FilePath, + hide = true, + requires_all = ["unstable", "broker_control_socket"], help_heading = "Unstable Options" )] - pub broker_socket: Option, + pub broker_notification_socket: Option, } struct MmappedFile { @@ -213,7 +223,22 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } litebox_platform_multiplex::set_platform(platform); - let broker_connection = broker::connect(cli_args.broker_socket.as_deref())?; + let broker_connection = match ( + cli_args.broker_control_socket.as_deref(), + cli_args.broker_notification_socket.as_deref(), + ) { + (Some(control_socket_path), Some(notification_socket_path)) => Some(broker::connect( + control_socket_path, + notification_socket_path, + )?), + (None, None) => None, + (Some(_), None) => { + anyhow::bail!("broker notification socket is required with broker control socket") + } + (None, Some(_)) => { + anyhow::bail!("broker control socket is required with broker notification socket") + } + }; let shim_builder = if let Some(broker_connection) = broker_connection { litebox_shim_linux::LinuxShimBuilder::new_with_litebox( diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index a5684a90a3..310636b955 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -123,8 +123,17 @@ impl Runner { } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] - fn broker_socket(&mut self, socket_path: &Path) -> &mut Self { - self.command.arg("--broker-socket").arg(socket_path); + fn broker_sockets( + &mut self, + control_socket_path: &Path, + notification_socket_path: &Path, + ) -> &mut Self { + self.command + .arg("--broker-control-socket") + .arg(control_socket_path); + self.command + .arg("--broker-notification-socket") + .arg(notification_socket_path); self } @@ -264,7 +273,8 @@ struct TestBroker { thread: Option>, done_rx: std::sync::mpsc::Receiver<()>, close_object_count_rx: std::sync::mpsc::Receiver, - socket_path: PathBuf, + control_socket_path: PathBuf, + notification_socket_path: PathBuf, } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] @@ -284,54 +294,79 @@ impl TestBroker { .expect("broker test host thread missing") .join() .expect("broker test host panicked"); - let _ = std::fs::remove_file(&self.socket_path); + let _ = std::fs::remove_file(&self.control_socket_path); + let _ = std::fs::remove_file(&self.notification_socket_path); } } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] impl Drop for TestBroker { fn drop(&mut self) { - let _ = std::fs::remove_file(&self.socket_path); + let _ = std::fs::remove_file(&self.control_socket_path); + let _ = std::fs::remove_file(&self.notification_socket_path); } } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] fn spawn_test_broker( - socket_path: &Path, + control_socket_path: &Path, + notification_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, connection_count: usize, ) -> TestBroker { - let _ = std::fs::remove_file(socket_path); + let _ = std::fs::remove_file(control_socket_path); + let _ = std::fs::remove_file(notification_socket_path); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); let (done_tx, done_rx) = std::sync::mpsc::channel(); let (close_object_count_tx, close_object_count_rx) = std::sync::mpsc::channel(); - let server_socket_path = socket_path.to_path_buf(); - let cleanup_socket_path = socket_path.to_path_buf(); + let server_control_socket_path = control_socket_path.to_path_buf(); + let server_notification_socket_path = notification_socket_path.to_path_buf(); + let cleanup_control_socket_path = control_socket_path.to_path_buf(); + let cleanup_notification_socket_path = notification_socket_path.to_path_buf(); let broker_thread = std::thread::spawn(move || { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let listener = std::os::unix::net::UnixListener::bind(&server_socket_path) - .expect("failed to bind broker test socket"); + let control_listener = + std::os::unix::net::UnixListener::bind(&server_control_socket_path) + .expect("failed to bind broker test control socket"); + let notification_listener = + std::os::unix::net::UnixListener::bind(&server_notification_socket_path) + .expect("failed to bind broker test notification socket"); let broker = litebox_broker_core::BrokerCore::new(policy).expect("failed to create broker core"); ready_tx.send(()).expect("failed to report broker ready"); for _ in 0..connection_count { - let (stream, _) = listener + let (control_stream, _) = control_listener .accept() .expect("failed to accept broker local control connection"); - stream + let (notification_stream, _) = notification_listener + .accept() + .expect("failed to accept broker local notification connection"); + control_stream .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test read timeout"); - stream + control_stream .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test write timeout"); + notification_stream + .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) + .expect("failed to configure broker notification test read timeout"); + notification_stream + .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) + .expect("failed to configure broker notification test write timeout"); let mut channel = CountingHostControlChannel { - inner: litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream), + inner: litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(control_stream), close_object_count: 0, }; - let termination = litebox_broker_host::serve_connection(&broker, &mut channel) - .expect("broker host failed"); + let mut notification_channel = + litebox_broker_transport::unix_socket::UnixStreamHostNotificationChannel::from_accepted(notification_stream); + let termination = litebox_broker_host::serve_connection( + &broker, + &mut channel, + &mut notification_channel, + ) + .expect("broker host failed"); assert_eq!( termination, litebox_broker_host::ConnectionTermination::PeerClosed @@ -341,7 +376,8 @@ fn spawn_test_broker( .expect("failed to report broker close-object count"); } })); - let _ = std::fs::remove_file(&server_socket_path); + let _ = std::fs::remove_file(&server_control_socket_path); + let _ = std::fs::remove_file(&server_notification_socket_path); let _ = done_tx.send(()); if let Err(panic) = result { std::panic::resume_unwind(panic); @@ -355,7 +391,8 @@ fn spawn_test_broker( thread: Some(broker_thread), done_rx, close_object_count_rx, - socket_path: cleanup_socket_path, + control_socket_path: cleanup_control_socket_path, + notification_socket_path: cleanup_notification_socket_path, } } @@ -428,9 +465,11 @@ impl fn test_runner_broker_integration_with_rewriter() { let true_path = run_which("true"); let target = common::compile("./tests/eventfd.c", "broker_eventfd_rewriter", false, false); - let socket_path = unique_test_socket_path("runner-broker"); + let control_socket_path = unique_test_socket_path("runner-broker-control"); + let notification_socket_path = unique_test_socket_path("runner-broker-notification"); let broker_thread = spawn_test_broker( - &socket_path, + &control_socket_path, + ¬ification_socket_path, litebox_broker_core::PolicyEngine::with_unauthenticated_rights( litebox_broker_core::PrincipalRights::all(), ), @@ -438,12 +477,12 @@ fn test_runner_broker_integration_with_rewriter() { ); Runner::new(&true_path, "broker_true_rewriter") - .broker_socket(&socket_path) + .broker_sockets(&control_socket_path, ¬ification_socket_path) .run(); assert_eq!(broker_thread.next_close_object_count(), 0); Runner::new(&target, "broker_eventfd_rewriter") - .broker_socket(&socket_path) + .broker_sockets(&control_socket_path, ¬ification_socket_path) .run(); // eventfd.c creates eight eventfd objects; each should release one broker object. assert_eq!(broker_thread.next_close_object_count(), 8); From 9357d84345214164100bc0a980429d4487f50dc4 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 2 Jul 2026 13:08:08 -0700 Subject: [PATCH 084/319] Enable blocking broker-backed eventfd (#1000) Enables broker-backed eventfds to honor the file description's O_NONBLOCK state instead of forcing broker operations to be nonblocking. The local event counter already wakes waiters from add/consume control-response readiness, so blocking read/write now works over the broker-backed path. Adds focused broker integration coverage for clearing O_NONBLOCK, blocking read/write wakeups, and epoll readiness updates. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_runner_linux_userland/tests/eventfd.c | 139 ++++++++++++++++++ litebox_runner_linux_userland/tests/run.rs | 4 +- litebox_shim_linux/src/syscalls/eventfd.rs | 22 +-- 3 files changed, 147 insertions(+), 18 deletions(-) diff --git a/litebox_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c index 39252b1671..4e3926fc2b 100644 --- a/litebox_runner_linux_userland/tests/eventfd.c +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -4,7 +4,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -96,7 +99,132 @@ static int expect_close(int fd) { return close(fd) == 0 ? 0 : 1; } +struct read_thread_args { + int fd; + uint64_t expected; + int result; +}; + +static void *read_thread(void *arg) { + struct read_thread_args *args = arg; + args->result = read_value(args->fd, args->expected); + return NULL; +} + +struct write_thread_args { + int fd; + uint64_t value; + int result; +}; + +static void *write_thread(void *arg) { + struct write_thread_args *args = arg; + args->result = write_value(args->fd, args->value); + return NULL; +} + +static int join_thread(pthread_t thread) { + return pthread_join(thread, NULL) == 0 ? 0 : 1; +} + +static int test_blocking_read_wakeup(void) { + int fd = eventfd(0, EFD_NONBLOCK); + if (fd < 0) { + return 1; + } + if (fcntl(fd, F_SETFL, 0) != 0) { + return 2; + } + + struct read_thread_args args = { + .fd = fd, + .expected = 5, + .result = -1, + }; + pthread_t thread; + if (pthread_create(&thread, NULL, read_thread, &args) != 0) { + return 3; + } + usleep(10000); + if (write_value(fd, 5) != 0) { + return 4; + } + if (join_thread(thread) != 0 || args.result != 0) { + return 5; + } + return expect_close(fd) == 0 ? 0 : 6; +} + +static int test_blocking_write_wakeup(void) { + int fd = eventfd(0, 0); + if (fd < 0) { + return 1; + } + if (write_value(fd, UINT64_MAX - 1) != 0) { + return 2; + } + + struct write_thread_args args = { + .fd = fd, + .value = 1, + .result = -1, + }; + pthread_t thread; + if (pthread_create(&thread, NULL, write_thread, &args) != 0) { + return 3; + } + usleep(10000); + if (read_value(fd, UINT64_MAX - 1) != 0) { + return 4; + } + if (join_thread(thread) != 0 || args.result != 0) { + return 5; + } + if (read_value(fd, 1) != 0) { + return 6; + } + return expect_close(fd) == 0 ? 0 : 7; +} + +static int test_epoll_wakeup(void) { + int fd = eventfd(0, 0); + if (fd < 0) { + return 1; + } + int epoll_fd = epoll_create1(0); + if (epoll_fd < 0) { + return 2; + } + struct epoll_event event = { + .events = EPOLLIN, + .data.fd = fd, + }; + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) != 0) { + return 3; + } + if (write_value(fd, 1) != 0) { + return 4; + } + struct epoll_event ready; + int ready_count = epoll_wait(epoll_fd, &ready, 1, 1000); + if (ready_count != 1 || ready.data.fd != fd || (ready.events & EPOLLIN) == 0) { + return 5; + } + if (read_value(fd, 1) != 0) { + return 6; + } + if (epoll_wait(epoll_fd, &ready, 1, 0) != 0) { + return 7; + } + if (expect_close(epoll_fd) != 0) { + return 8; + } + return expect_close(fd) == 0 ? 0 : 9; +} + int main(void) { + alarm(10); + int fd = eventfd(0, EFD_NONBLOCK); if (fd < 0) { return 10; @@ -327,5 +455,16 @@ int main(void) { return 130; } + if (test_blocking_read_wakeup() != 0) { + return 140; + } + if (test_blocking_write_wakeup() != 0) { + return 141; + } + if (test_epoll_wakeup() != 0) { + return 142; + } + + alarm(0); return 0; } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 310636b955..3ad2bf7504 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -484,8 +484,8 @@ fn test_runner_broker_integration_with_rewriter() { Runner::new(&target, "broker_eventfd_rewriter") .broker_sockets(&control_socket_path, ¬ification_socket_path) .run(); - // eventfd.c creates eight eventfd objects; each should release one broker object. - assert_eq!(broker_thread.next_close_object_count(), 8); + // eventfd.c creates eleven eventfd objects; each should release one broker object. + assert_eq!(broker_thread.next_close_object_count(), 11); broker_thread.join(); } diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 4e926e9e3d..080ff541a3 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -30,9 +30,6 @@ impl FdEnabledSubsystemEntry for EventFile {} /// Backing counter for a Linux eventfd file description. /// -/// New blocking eventfds use the shim-local path. Broker-backed counters stay -/// nonblocking even if file status flags are later changed, until broker -/// readiness notifications can wake local waiters. enum EventFileCounter { ShimLocal { count: Mutex, @@ -71,9 +68,7 @@ impl EventFileCounter counter .read( cx, - // Broker-backed eventfds cannot safely park local waiters - // until broker readiness notifications exist. - true, + nonblock, if semaphore { EventCounterReadMode::One } else { @@ -96,8 +91,7 @@ impl EventFileCounter counter.write(cx, true, value).map_err(Errno::from), + Self::LocalCore(counter) => counter.write(cx, nonblock, value).map_err(Errno::from), } } @@ -215,14 +209,10 @@ impl GlobalState { } let count = u64::from(initval); - let counter = if flags.contains(EfdFlags::NONBLOCK) { - match EventCounter::new(&self.litebox, count) { - Ok(counter) => EventFileCounter::LocalCore(counter), - Err(EventCounterError::Unavailable) => EventFileCounter::shim_local(count), - Err(error) => return Err(error.into()), - } - } else { - EventFileCounter::shim_local(count) + let counter = match EventCounter::new(&self.litebox, count) { + Ok(counter) => EventFileCounter::LocalCore(counter), + Err(EventCounterError::Unavailable) => EventFileCounter::shim_local(count), + Err(error) => return Err(error.into()), }; Ok(EventFile::new(counter, flags)) } From fc4813666848d7725628fadf1b6affdd985be9c8 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 2 Jul 2026 13:19:13 -0700 Subject: [PATCH 085/319] Support section related Windows syscalls (#1001) This PR adds support for `NtOpenSection`, `NtCreateSection`, `NtQuerySection`, `NtMapViewOfSection` and `NtUnmapViewOfSection`. Note LiteBox lacks shared anonymous backing, so a pagefile section is only allowed to be mapped once. For image section, it can be mapped multiple times by copying without copy-on-write mapping support. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_common_windows/src/loader.rs | 15 + litebox_common_windows/src/nt_status.rs | 8 + litebox_shim_windows/src/lib.rs | 184 ++ litebox_shim_windows/src/loader/mod.rs | 1 + litebox_shim_windows/src/loader/pe.rs | 52 + .../src/syscalls/directory.rs | 131 +- litebox_shim_windows/src/syscalls/mm.rs | 8 +- litebox_shim_windows/src/syscalls/mod.rs | 139 +- litebox_shim_windows/src/syscalls/section.rs | 1617 +++++++++++++++++ litebox_shim_windows/src/tests.rs | 2 + 10 files changed, 2151 insertions(+), 6 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/section.rs diff --git a/litebox_common_windows/src/loader.rs b/litebox_common_windows/src/loader.rs index 486b7ae735..0e3f18134d 100644 --- a/litebox_common_windows/src/loader.rs +++ b/litebox_common_windows/src/loader.rs @@ -489,6 +489,21 @@ impl PeParsedFile { self.image.image_base } + #[must_use] + pub fn machine(&self) -> u16 { + self.image.machine + } + + #[must_use] + pub fn characteristics(&self) -> u16 { + self.image.characteristics + } + + #[must_use] + pub fn dll_characteristics(&self) -> u16 { + self.image.dll_characteristics + } + /// Returns whether the image opts into dynamic-base loading. #[must_use] pub fn has_dynamic_base(&self) -> bool { diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index 439cd7fe63..bfeb2249ee 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -159,6 +159,8 @@ impl NtStatus { 0xC0000046 => "STATUS_MUTANT_NOT_OWNED: Mutant not owned", 0xC0000047 => "STATUS_SEMAPHORE_LIMIT_EXCEEDED: Semaphore limit exceeded", 0xC0000048 => "STATUS_PORT_ALREADY_SET: Port already set", + 0xC0000049 => "STATUS_SECTION_NOT_IMAGE: Section not image", + 0xC000004E => "STATUS_SECTION_PROTECTION: Section protection", 0xC000004F => "STATUS_EAS_NOT_SUPPORTED: EAS not supported", 0xC0000050 => "STATUS_EA_TOO_LARGE: EA too large", 0xC0000056 => "STATUS_DELETE_PENDING: Delete pending", @@ -420,6 +422,9 @@ impl NtStatus { /// STATUS_SECTION_TOO_BIG pub const SECTION_TOO_BIG: Self = Self::from_raw(0xC0000040); + /// STATUS_SECTION_NOT_IMAGE + pub const SECTION_NOT_IMAGE: Self = Self::from_raw(0xC0000049); + /// STATUS_PORT_CONNECTION_REFUSED pub const PORT_CONNECTION_REFUSED: Self = Self::from_raw(0xC0000041); @@ -435,6 +440,9 @@ impl NtStatus { /// STATUS_INVALID_PAGE_PROTECTION pub const INVALID_PAGE_PROTECTION: Self = Self::from_raw(0xC0000045); + /// STATUS_SECTION_PROTECTION + pub const SECTION_PROTECTION: Self = Self::from_raw(0xC000004E); + /// STATUS_MUTANT_NOT_OWNED pub const MUTANT_NOT_OWNED: Self = Self::from_raw(0xC0000046); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 3de8199688..a9a8d92476 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -38,6 +38,9 @@ use crate::syscalls::event::{EventHandleObject, EventObject, EventSubsystem}; use crate::syscalls::file::{FileObject, FileObjectSubsystem}; use crate::syscalls::iocp::{IoCompletionHandleObject, IoCompletionSubsystem}; use crate::syscalls::registry::{RegistryKeyObject, RegistryKeySubsystem}; +use crate::syscalls::section::{ + MapViewOfSectionParameters, SectionHandleObject, SectionObject, SectionSubsystem, +}; use crate::syscalls::symlink::{SymbolicLinkHandleObject, SymbolicLinkSubsystem}; use crate::syscalls::timer::{TimerCreateParameters, TimerHandleObject, TimerSubsystem}; use crate::syscalls::wait_completion_packet::{ @@ -90,6 +93,10 @@ pub(crate) type WindowsNlsSectionMappings = litebox::sync::RwLock>; pub(crate) type WindowsVirtualAllocations = litebox::sync::RwLock>; +pub(crate) type WindowsSectionNamespace = + litebox::sync::RwLock>>>; +pub(crate) type WindowsSectionViews = + litebox::sync::RwLock>>; pub(crate) type WindowsEventNamespace = litebox::sync::RwLock>>>; pub(crate) type WindowsDirectoryNamespace = DirectoryNamespace; @@ -103,6 +110,22 @@ pub(crate) struct WindowsVirtualAllocation { pub(crate) pages: rangemap::RangeMap, } +pub(crate) struct WindowsSectionView { + pub(crate) size: usize, + pub(crate) section_offset: usize, + pub(crate) section: Option>>, +} + +impl Clone for WindowsSectionView { + fn clone(&self) -> Self { + Self { + size: self.size, + section_offset: self.section_offset, + section: self.section.clone(), + } + } +} + pub type DefaultFS = WindowsFS; pub type WindowsFS = litebox::fs::layered::FileSystem< @@ -343,6 +366,8 @@ impl WindowsShim { handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), directory_namespace, event_namespace: WindowsEventNamespace::::new(BTreeMap::new()), + section_namespace: WindowsSectionNamespace::::new(BTreeMap::new()), + section_views: WindowsSectionViews::::new(BTreeMap::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), virtual_allocations: load_info.virtual_allocations, system_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), @@ -387,6 +412,8 @@ pub struct Process { handles: WindowsHandleStore, directory_namespace: WindowsDirectoryNamespace, event_namespace: WindowsEventNamespace, + section_namespace: WindowsSectionNamespace, + section_views: WindowsSectionViews, nls_section_mappings: WindowsNlsSectionMappings, virtual_allocations: WindowsVirtualAllocations, system_lcid: AtomicU32, @@ -588,6 +615,15 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtOpenSection { + section_handle, + desired_access, + object_attributes, + } => { + let status = + self.sys_nt_open_section(section_handle, desired_access, object_attributes); + (status, ContinueOperation::Resume) + } SyscallRequest::NtQueryDirectoryObject { directory_handle, buffer, @@ -662,6 +698,50 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtCreateSection { + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + } => { + let status = self.sys_nt_create_section( + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateSectionEx { + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + extended_parameters, + extended_parameter_count, + } => { + let status = self.sys_nt_create_section_ex( + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + extended_parameters, + extended_parameter_count, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtCreateWaitCompletionPacket { wait_completion_packet_handle, desired_access, @@ -1023,6 +1103,22 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtQuerySection { + section_handle, + section_information_class, + section_information, + section_information_length, + return_length, + } => { + let status = self.sys_nt_query_section( + section_handle, + section_information_class, + section_information, + section_information_length, + return_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtQueryInformationProcess { process_handle, process_information_class, @@ -1199,6 +1295,80 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtMapViewOfSection { + section_handle, + process_handle, + base_address, + zero_bits, + commit_size, + section_offset, + view_size, + inherit_disposition, + allocation_type, + page_protection, + } => { + let status = self.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle, + process_handle, + base_address, + zero_bits, + commit_size, + section_offset, + view_size, + inherit_disposition, + allocation_type, + page_protection, + }); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtMapViewOfSectionEx { + section_handle, + process_handle, + base_address, + zero_bits, + commit_size, + section_offset, + view_size, + inherit_disposition, + allocation_type, + page_protection, + extended_parameters, + extended_parameter_count, + } => { + let status = self.sys_nt_map_view_of_section_ex( + MapViewOfSectionParameters { + section_handle, + process_handle, + base_address, + zero_bits, + commit_size, + section_offset, + view_size, + inherit_disposition, + allocation_type, + page_protection, + }, + extended_parameters, + extended_parameter_count, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtUnmapViewOfSection { + process_handle, + base_address, + } => { + let status = self.sys_nt_unmap_view_of_section(process_handle, base_address); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtUnmapViewOfSectionEx { + process_handle, + base_address, + flags, + } => { + let status = + self.sys_nt_unmap_view_of_section_ex(process_handle, base_address, flags); + (status, ContinueOperation::Resume) + } SyscallRequest::NtTerminateProcess { process_handle, exit_status, @@ -1306,6 +1476,14 @@ impl Task { ) { return NtStatus::SUCCESS; } + if remove_raw_handle_by_raw_fd::>( + &self.global.litebox, + &self.process.handles, + raw_fd, + |section| visitor.section(section), + ) { + return NtStatus::SUCCESS; + } NtStatus::INVALID_HANDLE } @@ -1342,6 +1520,8 @@ trait RawHandleVisitor { ); fn worker_factory(&self, worker_factory: WorkerFactoryHandleObject); + + fn section(&self, section: SectionHandleObject); } struct CloseRawHandleVisitor<'task, Platform: ShimPlatform, FS: ShimFS> { @@ -1389,6 +1569,10 @@ impl RawHandleVisitor fn worker_factory(&self, worker_factory: WorkerFactoryHandleObject) { Task::::close_worker_factory(worker_factory); } + + fn section(&self, section: SectionHandleObject) { + Task::::close_section(section); + } } /// The shim entrypoint object passed to the platform. diff --git a/litebox_shim_windows/src/loader/mod.rs b/litebox_shim_windows/src/loader/mod.rs index 3d7b9a32e0..7922fdcdb8 100644 --- a/litebox_shim_windows/src/loader/mod.rs +++ b/litebox_shim_windows/src/loader/mod.rs @@ -4,3 +4,4 @@ mod pe; pub(super) use pe::{PeLoader, WindowsLoadError}; +pub(crate) use pe::{image_section_metadata, load_image_section}; diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 05bcc2b536..4ba683cf64 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -1224,6 +1224,58 @@ fn load_image( load_image_with_writable_sections(fs, path, platform, page_manager, &[]) } +pub(crate) fn load_image_section( + platform: &'static Platform, + fs: Arc, + path: &str, + page_manager: &crate::WindowsPageManager, + virtual_allocations: &crate::WindowsVirtualAllocations, +) -> Result { + let image = load_image(platform, fs, path, page_manager)?; + let mapping = image.mapping; + register_image_virtual_allocation(virtual_allocations, mapping, image.pages); + Ok(mapping) +} + +pub(crate) struct ImageSectionMetadata { + pub(crate) transfer_address: usize, + pub(crate) file_size: u32, + pub(crate) subsystem: u32, + pub(crate) subsystem_major_version: u16, + pub(crate) subsystem_minor_version: u16, + pub(crate) image_characteristics: u16, + pub(crate) dll_characteristics: u16, + pub(crate) machine: u16, +} + +pub(crate) fn image_section_metadata( + fs: Arc, + path: &str, +) -> Result { + let file = PeImageFile::open(fs, path)?; + let parsed = PeParsedFile::parse(&mut &file).map_err(WindowsLoadError::Parse)?; + let file_size = file + .fs + .fd_file_status(&file.fd) + .map_err(PeImageAccessError::FileStatus)? + .size + .try_into() + .map_err(|_| PeImageAccessError::AddressOverflow)?; + Ok(ImageSectionMetadata { + transfer_address: parsed + .image_base() + .checked_add(parsed.entry_point_rva()) + .ok_or(PeImageAccessError::AddressOverflow)?, + file_size, + subsystem: u32::from(parsed.subsystem()), + subsystem_major_version: parsed.major_subsystem_version(), + subsystem_minor_version: parsed.minor_subsystem_version(), + image_characteristics: parsed.characteristics(), + dll_characteristics: parsed.dll_characteristics(), + machine: parsed.machine(), + }) +} + fn load_image_with_writable_sections( fs: Arc, path: &str, diff --git a/litebox_shim_windows/src/syscalls/directory.rs b/litebox_shim_windows/src/syscalls/directory.rs index 853056a618..f992c12319 100644 --- a/litebox_shim_windows/src/syscalls/directory.rs +++ b/litebox_shim_windows/src/syscalls/directory.rs @@ -314,7 +314,10 @@ impl DirectoryNamespace { } } - fn resolve_directory(&self, path: &str) -> Result>, NtStatus> { + pub(super) fn resolve_directory( + &self, + path: &str, + ) -> Result>, NtStatus> { let tail = absolute_path_tail(path)?; let node = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, false)?; if node.is_directory() { @@ -324,6 +327,23 @@ impl DirectoryNamespace { } } + pub(super) fn resolve_object(&self, path: &str) -> Result>, NtStatus> { + let tail = absolute_path_tail(path)?; + self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, true) + } + + pub(super) fn parent_directory_exists(&self, path: &str) -> bool { + let path = trim_trailing_directory_path(path); + if path == r"\" { + return false; + } + let Some(index) = path.rfind('\\') else { + return false; + }; + let parent = if index == 0 { r"\" } else { &path[..index] }; + self.resolve_directory(parent).is_ok() + } + fn create_directory( &self, path: &str, @@ -1223,6 +1243,17 @@ mod tests { handle } + fn object_attributes_with_root( + name: &UnicodeString, + root_directory: Handle, + attributes: u32, + ) -> ObjectAttributes { + ObjectAttributes { + root_directory, + ..object_attributes(name, attributes) + } + } + fn expected_record_size(name: &str, type_name: &str) -> usize { size_of::() + name.encode_utf16().count() * size_of::() @@ -1326,6 +1357,104 @@ mod tests { }); } + #[test] + fn open_section_rejects_empty_known_dlls_with_zeroed_output() { + run_with_test_platform_pointers(|| { + let task = test_task(); + let known_dlls_units = utf16_units(r"\KnownDlls"); + let known_dlls_name = unicode_string(&known_dlls_units); + let known_dlls_attrs = object_attributes( + &known_dlls_name, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + let mut known_dlls = Handle::default(); + assert_eq!( + task.sys_nt_open_directory_object( + mut_ptr(&mut known_dlls), + DIRECTORY_QUERY | DIRECTORY_TRAVERSE, + Some(const_ptr(&known_dlls_attrs)), + ), + NtStatus::SUCCESS + ); + let kernel32_units = utf16_units("KERNEL32.DLL"); + let kernel32 = unicode_string(&kernel32_units); + let attrs = object_attributes_with_root( + &kernel32, + known_dlls, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + let mut handle = Handle::from_raw(0x5555_5555); + + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!(handle, Handle::default()); + + let attrs = object_attributes_with_root( + &kernel32, + known_dlls, + (ObjectAttributesFlags::CASE_INSENSITIVE | ObjectAttributesFlags::OPENLINK).bits(), + ); + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!(handle, Handle::default()); + + let missing_parent_units = utf16_units(r"\MissingLiteBoxParent\KERNEL32.DLL"); + let missing_parent = unicode_string(&missing_parent_units); + let attrs = object_attributes( + &missing_parent, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::OBJECT_PATH_NOT_FOUND + ); + assert_eq!(handle, Handle::default()); + + let attrs = object_attributes( + &known_dlls_name, + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::OBJECT_TYPE_MISMATCH + ); + assert_eq!(handle, Handle::default()); + + let attrs = object_attributes_with_root( + &kernel32, + Handle::from_raw(0x1234), + ObjectAttributesFlags::CASE_INSENSITIVE.bits(), + ); + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, Some(const_ptr(&attrs))), + NtStatus::INVALID_HANDLE + ); + assert_eq!(handle, Handle::default()); + + handle = Handle::from_raw(0x5555_5555); + assert_eq!( + task.sys_nt_open_section(mut_ptr(&mut handle), 0x0d, None), + NtStatus::INVALID_PARAMETER + ); + assert_eq!(handle, Handle::default()); + + assert_eq!( + task.sys_nt_open_section(null_mut_ptr::(), 0x0d, Some(const_ptr(&attrs))), + NtStatus::ACCESS_VIOLATION + ); + + assert_eq!(task.sys_nt_close(known_dlls), NtStatus::SUCCESS); + }); + } + #[test] fn create_directory_distinguishes_null_object_name_from_empty_name() { run_with_test_platform_pointers(|| { diff --git a/litebox_shim_windows/src/syscalls/mm.rs b/litebox_shim_windows/src/syscalls/mm.rs index f67931a198..000e2ea4f6 100644 --- a/litebox_shim_windows/src/syscalls/mm.rs +++ b/litebox_shim_windows/src/syscalls/mm.rs @@ -40,7 +40,7 @@ bitflags::bitflags! { } impl PageProtection { - const BASE_MASK: u32 = 0xff; + pub(super) const BASE_MASK: u32 = 0xff; fn base(self) -> u32 { self.bits() & Self::BASE_MASK @@ -1065,7 +1065,9 @@ fn mark_pages_decommitted( allocation.pages.remove(base..end); } -fn parse_page_protection(protect: u32) -> Option<(PageProtection, MemoryRegionPermissions)> { +pub(super) fn parse_page_protection( + protect: u32, +) -> Option<(PageProtection, MemoryRegionPermissions)> { let protect = PageProtection::from_bits(protect)?; let permissions = page_protect_to_permissions(protect)?; Some((protect, permissions)) @@ -1124,7 +1126,7 @@ fn permissions_to_page_protect(permissions: MemoryRegionPermissions) -> PageProt } } -fn create_pages( +pub(super) fn create_pages( page_manager: &WindowsPageManager, suggested_address: Option>, length: NonZeroPageSize, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index ced9039d02..1e3bda7d5f 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod mm; pub(crate) mod nls; pub(crate) mod process; pub(crate) mod registry; +pub(crate) mod section; pub(crate) mod symlink; mod sysinfo; pub(crate) mod thread; @@ -143,6 +144,11 @@ pub(crate) enum SyscallRequest { desired_access: u32, object_attributes: Option>, }, + NtOpenSection { + section_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + }, NtQueryDirectoryObject { directory_handle: Handle, buffer: Platform::RawMutPointer, @@ -174,6 +180,26 @@ pub(crate) enum SyscallRequest { object_attributes: Option>, number_of_concurrent_threads: u32, }, + NtCreateSection { + section_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + maximum_size: Option>, + section_page_protection: u32, + allocation_attributes: u32, + file_handle: Handle, + }, + NtCreateSectionEx { + section_handle: Platform::RawMutPointer, + desired_access: u32, + object_attributes: Option>, + maximum_size: Option>, + section_page_protection: u32, + allocation_attributes: u32, + file_handle: Handle, + extended_parameters: Option>, + extended_parameter_count: u32, + }, NtCreateWaitCompletionPacket { wait_completion_packet_handle: Platform::RawMutPointer, desired_access: u32, @@ -346,6 +372,13 @@ pub(crate) enum SyscallRequest { system_information_length: u32, return_length: Option>, }, + NtQuerySection { + section_handle: Handle, + section_information_class: u32, + section_information: Platform::RawMutPointer, + section_information_length: usize, + return_length: Option>, + }, NtQueryInformationProcess { process_handle: ProcessHandle, process_information_class: u32, @@ -422,6 +455,41 @@ pub(crate) enum SyscallRequest { memory_information_length: usize, return_length: Option>, }, + NtMapViewOfSection { + section_handle: Handle, + process_handle: ProcessHandle, + base_address: Platform::RawMutPointer, + zero_bits: usize, + commit_size: usize, + section_offset: Option>, + view_size: Platform::RawMutPointer, + inherit_disposition: u32, + allocation_type: u32, + page_protection: u32, + }, + NtMapViewOfSectionEx { + section_handle: Handle, + process_handle: ProcessHandle, + base_address: Platform::RawMutPointer, + zero_bits: usize, + commit_size: usize, + section_offset: Option>, + view_size: Platform::RawMutPointer, + inherit_disposition: u32, + allocation_type: u32, + page_protection: u32, + extended_parameters: Option>, + extended_parameter_count: u32, + }, + NtUnmapViewOfSection { + process_handle: ProcessHandle, + base_address: usize, + }, + NtUnmapViewOfSectionEx { + process_handle: ProcessHandle, + base_address: usize, + flags: u32, + }, NtTerminateProcess { process_handle: ProcessHandle, exit_status: i32, @@ -434,7 +502,7 @@ impl SyscallRequest { pub(crate) fn try_from_raw(pt_regs: &litebox_common_linux::PtRegs) -> Option { macro_rules! sys_req { ($id:ident { $( $field:ident $(:$star:tt)? ),* $(,)? }) => { - sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] [ ]) + sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] [ ]) }; (@[$id:ident] [ $f:ident $(,)? $($field:ident $(:$star:tt)?),* ] [ $n:literal $(,)? $($ns:literal),* ] [ $($tail:tt)* ]) => { sys_req!(@[$id] [ $( $field $(:$star)? ),* ] [ $($ns),* ] [ $($tail)* $f: win_sys_req_arg::(pt_regs, $n)?, ]) @@ -478,8 +546,13 @@ impl SyscallRequest { desired_access, object_attributes:*, })), + NtSysno::NtOpenSection => Some(sys_req!(NtOpenSection { + section_handle:*, + desired_access, + object_attributes:*, + })), NtSysno::NtQueryDirectoryObject => Some(sys_req!(NtQueryDirectoryObject { - directory_handle:{Handle::from_raw}, + directory_handle:{ Handle::from_raw }, buffer:*, buffer_length, return_single_entry, @@ -509,6 +582,26 @@ impl SyscallRequest { object_attributes:*, number_of_concurrent_threads, })), + NtSysno::NtCreateSection => Some(sys_req!(NtCreateSection { + section_handle:*, + desired_access, + object_attributes:*, + maximum_size:*, + section_page_protection, + allocation_attributes, + file_handle:{ Handle::from_raw }, + })), + NtSysno::NtCreateSectionEx => Some(sys_req!(NtCreateSectionEx { + section_handle:*, + desired_access, + object_attributes:*, + maximum_size:*, + section_page_protection, + allocation_attributes, + file_handle:{ Handle::from_raw }, + extended_parameters:*, + extended_parameter_count, + })), NtSysno::NtCreateWaitCompletionPacket => Some(sys_req!( NtCreateWaitCompletionPacket { wait_completion_packet_handle:*, @@ -687,6 +780,13 @@ impl SyscallRequest { system_information_length, return_length:*, })), + NtSysno::NtQuerySection => Some(sys_req!(NtQuerySection { + section_handle: { Handle::from_raw }, + section_information_class, + section_information:*, + section_information_length, + return_length:*, + })), NtSysno::NtQueryInformationProcess => Some(sys_req!(NtQueryInformationProcess { process_handle: { ProcessHandle::from_raw }, process_information_class, @@ -765,6 +865,41 @@ impl SyscallRequest { memory_information_length, return_length:*, })), + NtSysno::NtMapViewOfSection => Some(sys_req!(NtMapViewOfSection { + section_handle: { Handle::from_raw }, + process_handle: { ProcessHandle::from_raw }, + base_address:*, + zero_bits, + commit_size, + section_offset:*, + view_size:*, + inherit_disposition, + allocation_type, + page_protection, + })), + NtSysno::NtMapViewOfSectionEx => Some(sys_req!(NtMapViewOfSectionEx { + section_handle: { Handle::from_raw }, + process_handle: { ProcessHandle::from_raw }, + base_address:*, + zero_bits, + commit_size, + section_offset:*, + view_size:*, + inherit_disposition, + allocation_type, + page_protection, + extended_parameters:*, + extended_parameter_count, + })), + NtSysno::NtUnmapViewOfSection => Some(sys_req!(NtUnmapViewOfSection { + process_handle: { ProcessHandle::from_raw }, + base_address, + })), + NtSysno::NtUnmapViewOfSectionEx => Some(sys_req!(NtUnmapViewOfSectionEx { + process_handle: { ProcessHandle::from_raw }, + base_address, + flags, + })), NtSysno::NtTerminateProcess => Some(sys_req!(NtTerminateProcess { process_handle: { ProcessHandle::from_raw }, exit_status, diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs new file mode 100644 index 0000000000..7d770e83cb --- /dev/null +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -0,0 +1,1617 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::string::String; +use alloc::sync::Arc; +use core::marker::PhantomData; +use core::mem::size_of; +use core::sync::atomic::{AtomicBool, Ordering}; + +use int_enum::IntEnum; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::mm::linux::{CreatePagesFlags, NonZeroPageSize}; +use litebox::platform::page_mgmt::MemoryRegionPermissions; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox_common_windows::nt_status::NtStatus; +use rangemap::RangeMap; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::nt_types::{AccessMask, ObjectAttributes}; +use crate::syscalls::mm::{MemoryType, PageProtection, create_pages, parse_page_protection}; +use crate::syscalls::{Handle, ProcessHandle}; +use crate::{ConstPtr, MutPtr, PAGE_SIZE, ShimFS, ShimPlatform, Task, WindowsSectionView}; + +const VIEW_SHARE: u32 = 1; +const VIEW_UNMAP: u32 = 2; +const MEM_TOP_DOWN: u32 = 0x0010_0000; +const MEM_PHYSICAL: u32 = 0x0040_0000; +const MEM_DIFFERENT_IMAGE_BASE_OK: u32 = 0x0080_0000; +const SUPPORTED_MAP_ALLOCATION_TYPES: u32 = + MEM_TOP_DOWN | MEM_PHYSICAL | MEM_DIFFERENT_IMAGE_BASE_OK; + +enum SectionBacking { + /// LiteBox lacks shared anonymous backing, so a pagefile section is + /// metadata-only until its single allowed view is mapped. Remap after unmap + /// is rejected instead of storing contents in shim memory or a file. + /// + /// Shared write-through backing across concurrent views is the deferred + /// capability (see `TODO(section-subsystem)`); until it lands, a single view + /// is the only observable-faithful case, which is why both the + /// second-concurrent-view and the remap-after-unmap rejects exist. They are + /// one missing feature, not two unrelated limitations. + Pagefile, + ImageFile, +} + +pub(crate) struct SectionSubsystem(PhantomData); + +impl FdEnabledSubsystem for SectionSubsystem { + type Entry = SectionHandleObject; +} + +impl FdEnabledSubsystemEntry for SectionHandleObject {} + +pub(crate) struct SectionHandleObject { + section: Arc>, + granted_access: SectionAccess, +} + +pub(crate) struct SectionObject { + fs_path: Option, + size: usize, + attributes: SectionAllocationAttributes, + protection: PageProtection, + backing: SectionBacking, + pagefile_view_active: AtomicBool, + _platform: PhantomData, +} + +pub(crate) struct MapViewOfSectionParameters { + pub(crate) section_handle: Handle, + pub(crate) process_handle: ProcessHandle, + pub(crate) base_address: MutPtr, + pub(crate) zero_bits: usize, + pub(crate) commit_size: usize, + pub(crate) section_offset: Option>, + pub(crate) view_size: MutPtr, + pub(crate) inherit_disposition: u32, + pub(crate) allocation_type: u32, + pub(crate) page_protection: u32, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct SectionAllocationAttributes: u32 { + const SEC_FILE = 0x0080_0000; + const SEC_IMAGE = 0x0100_0000; + const SEC_RESERVE = 0x0400_0000; + const SEC_COMMIT = 0x0800_0000; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct SectionAccess: u32 { + const QUERY = 0x0001; + const MAP_WRITE = 0x0002; + const MAP_READ = 0x0004; + const MAP_EXECUTE = 0x0008; + const EXTEND_SIZE = 0x0010; + const MAP_EXECUTE_EXPLICIT = 0x0020; + + const GENERIC_READ_EXPANSION = AccessMask::STANDARD_RIGHTS_READ.bits() + | Self::QUERY.bits() + | Self::MAP_READ.bits(); + const GENERIC_WRITE_EXPANSION = AccessMask::STANDARD_RIGHTS_WRITE.bits() + | Self::MAP_WRITE.bits() + | Self::EXTEND_SIZE.bits(); + const GENERIC_EXECUTE_EXPANSION = AccessMask::STANDARD_RIGHTS_EXECUTE.bits() + | Self::MAP_EXECUTE.bits(); + const ALL_ACCESS = AccessMask::STANDARD_RIGHTS_ALL.bits() + | Self::QUERY.bits() + | Self::MAP_WRITE.bits() + | Self::MAP_READ.bits() + | Self::MAP_EXECUTE.bits() + | Self::EXTEND_SIZE.bits(); + const GENERIC_ALL = AccessMask::GENERIC_ALL.bits(); + const GENERIC_EXECUTE = AccessMask::GENERIC_EXECUTE.bits(); + const GENERIC_WRITE = AccessMask::GENERIC_WRITE.bits(); + const GENERIC_READ = AccessMask::GENERIC_READ.bits(); + + const _ = !0; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct UnmapViewOfSectionFlags: u32 { + const _ = 0; + } +} + +impl SectionAccess { + fn from_desired_access(desired_access: u32) -> Self { + let mut access = Self::from_bits_retain(desired_access); + if access.contains(Self::GENERIC_READ) { + access.remove(Self::GENERIC_READ); + access.insert(Self::GENERIC_READ_EXPANSION); + } + if access.contains(Self::GENERIC_WRITE) { + access.remove(Self::GENERIC_WRITE); + access.insert(Self::GENERIC_WRITE_EXPANSION); + } + if access.contains(Self::GENERIC_EXECUTE) { + access.remove(Self::GENERIC_EXECUTE); + access.insert(Self::GENERIC_EXECUTE_EXPANSION); + } + if access.contains(Self::GENERIC_ALL) { + access.remove(Self::GENERIC_ALL); + access.insert(Self::ALL_ACCESS); + } + access + } + + fn require(self, required: Self) -> Result<(), NtStatus> { + if self.contains(required) { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum SectionInformationClass { + Basic = 0, + Image = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SectionBasicInformation { + base_address: usize, + attributes: u32, + _padding: u32, + size: i64, +} + +const _: () = assert!(size_of::() == 24); + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct SectionImageInformation { + transfer_address: usize, + zero_bits: u32, + _padding0: u32, + maximum_stack_size: usize, + committed_stack_size: usize, + subsystem_type: u32, + subsystem_minor_version: u16, + subsystem_major_version: u16, + gp_value: u32, + image_characteristics: u16, + dll_characteristics: u16, + machine: u16, + image_contains_code: u8, + image_flags: u8, + loader_flags: u32, + image_file_size: u32, + checksum: u32, +} + +const _: () = assert!(size_of::() == 64); + +impl Task { + fn section_entry( + &self, + handle: Handle, + ) -> Result>, NtStatus> { + self.typed_handle_entry::>(handle) + } + + fn insert_section_handle( + &self, + section: Arc>, + granted_access: SectionAccess, + ) -> Result { + self.insert_typed_handle::>( + SectionHandleObject { + section, + granted_access, + }, + drop, + ) + } + + pub(crate) fn close_section_handle(&self, handle: Handle) { + self.close_typed_handle::>(handle, drop); + } + + pub(crate) fn close_section(section: SectionHandleObject) { + drop(section); + } + + #[expect( + clippy::too_many_arguments, + reason = "NtCreateSection has seven ABI parameters; keeping ABI args explicit avoids reshuffling" + )] + pub(crate) fn sys_nt_create_section( + &self, + section_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + maximum_size: Option>, + section_page_protection: u32, + allocation_attributes: u32, + file_handle: Handle, + ) -> NtStatus { + // Host ntdll preserves the output handle for pre-creation validation failures such as a + // NULL MaximumSize pagefile section. + if let Err(status) = + crate::probe_guest_output_preserving_value::(section_handle) + { + return status; + } + let granted_access = SectionAccess::from_desired_access(desired_access); + if granted_access.is_empty() { + return NtStatus::ACCESS_DENIED; + } + let Some((protection, _)) = parse_page_protection(section_page_protection) else { + return NtStatus::INVALID_PAGE_PROTECTION; + }; + // NtCreateSection currently supports only pagefile-backed sections. File-backed image + // sections are synthesized by NtOpenSection for KnownDlls; accepting a file handle here + // requires section lifetime/sharing to be keyed by the underlying file object identity. + if !file_handle.is_null() { + litebox_util_log::debug!( + file_handle = file_handle.as_raw(), + allocation_attributes:% = format_args!("{allocation_attributes:#x}"), + section_page_protection:% = format_args!("{section_page_protection:#x}"), + desired_access:% = format_args!("{desired_access:#x}"); + "Unsupported file-backed NtCreateSection" + ); + return NtStatus::INVALID_HANDLE; + } + let allocation_attributes = + SectionAllocationAttributes::from_bits_retain(allocation_attributes); + let supported_create_attributes = + SectionAllocationAttributes::SEC_RESERVE | SectionAllocationAttributes::SEC_COMMIT; + if !allocation_attributes + .difference(supported_create_attributes) + .is_empty() + { + return NtStatus::INVALID_PARAMETER; + } + if !allocation_attributes.intersects(supported_create_attributes) { + return NtStatus::INVALID_PARAMETER; + } + + let Some(maximum_size) = maximum_size else { + return NtStatus::INVALID_PARAMETER_4; + }; + let maximum_size = match maximum_size.read_at_offset(0) { + Some(value) if value > 0 => value, + Some(_) => return NtStatus::INVALID_PARAMETER, + None => return NtStatus::ACCESS_VIOLATION, + }; + let Ok(size) = usize::try_from(maximum_size) else { + return NtStatus::SECTION_TOO_BIG; + }; + let Some(size) = size.checked_next_multiple_of(PAGE_SIZE) else { + return NtStatus::SECTION_TOO_BIG; + }; + if NonZeroPageSize::::new(size).is_none() { + return NtStatus::INVALID_PARAMETER; + } + + let name = match self.read_section_name(object_attributes) { + Ok(name) => name, + Err(status) => return status, + }; + let attributes = if allocation_attributes.contains(SectionAllocationAttributes::SEC_RESERVE) + { + SectionAllocationAttributes::SEC_RESERVE + } else { + SectionAllocationAttributes::SEC_COMMIT + }; + let section = Arc::new(SectionObject { + fs_path: None, + size, + attributes, + protection, + backing: SectionBacking::Pagefile, + pagefile_view_active: AtomicBool::new(false), + _platform: PhantomData, + }); + if let Some(name) = &name { + let status = self.insert_named_section(name, §ion); + if status != NtStatus::SUCCESS { + return status; + } + } + self.publish_section_handle(section_handle, section, granted_access) + } + + #[expect( + clippy::too_many_arguments, + reason = "NtCreateSectionEx extends NtCreateSection with two ABI parameters" + )] + pub(crate) fn sys_nt_create_section_ex( + &self, + section_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + maximum_size: Option>, + section_page_protection: u32, + allocation_attributes: u32, + file_handle: Handle, + extended_parameters: Option>, + extended_parameter_count: u32, + ) -> NtStatus { + if extended_parameters.is_some() || extended_parameter_count != 0 { + return NtStatus::INVALID_PARAMETER; + } + self.sys_nt_create_section( + section_handle, + desired_access, + object_attributes, + maximum_size, + section_page_protection, + allocation_attributes, + file_handle, + ) + } + + pub(crate) fn sys_nt_open_section( + &self, + section_handle: MutPtr, + desired_access: u32, + object_attributes: Option>, + ) -> NtStatus { + // Host ntdll zeroes the output handle before resolving a missing section name. + if section_handle + .write_at_offset(0, Handle::default()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + let granted_access = SectionAccess::from_desired_access(desired_access); + if granted_access.is_empty() { + return NtStatus::ACCESS_DENIED; + } + if object_attributes.is_none() { + return NtStatus::INVALID_PARAMETER; + } + let name = match self.read_required_section_name(object_attributes) { + Ok(name) => name, + Err(status) => return status, + }; + if self + .process + .directory_namespace + .resolve_object(&name) + .is_ok() + { + return NtStatus::OBJECT_TYPE_MISMATCH; + } + if let Some(section) = self.named_section(&name) { + return self.publish_section_handle(section_handle, section, granted_access); + } + let Some(fs_path) = known_dll_section_fs_path(&name) else { + return section_missing_status( + self.process + .directory_namespace + .parent_directory_exists(&name), + ); + }; + let Ok(file_status) = self.fs.file_status(&fs_path) else { + return NtStatus::OBJECT_NAME_NOT_FOUND; + }; + let section = Arc::new(SectionObject { + fs_path: Some(fs_path), + size: file_status.size, + attributes: SectionAllocationAttributes::SEC_FILE + | SectionAllocationAttributes::SEC_IMAGE, + protection: PageProtection::PAGE_EXECUTE_WRITECOPY, + backing: SectionBacking::ImageFile, + pagefile_view_active: AtomicBool::new(false), + _platform: PhantomData, + }); + self.publish_section_handle(section_handle, section, granted_access) + } + + pub(crate) fn sys_nt_query_section( + &self, + section_handle: Handle, + section_information_class: u32, + section_information: MutPtr, + section_information_length: usize, + return_length: Option>, + ) -> NtStatus { + let Ok(information_class) = SectionInformationClass::try_from(section_information_class) + else { + return NtStatus::INVALID_INFO_CLASS; + }; + let entry = match self.section_entry(section_handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + let result = entry.with_entry(|entry| { + entry + .granted_access + .require(SectionAccess::QUERY) + .map(|()| Arc::clone(&entry.section)) + }); + let section = match result { + Ok(section) => section, + Err(status) => return status, + }; + match information_class { + SectionInformationClass::Basic => write_section_basic_information::( + §ion, + section_information, + section_information_length, + return_length, + ), + SectionInformationClass::Image => write_section_image_information::( + §ion, + Arc::clone(&self.fs), + section_information, + section_information_length, + return_length, + ), + } + } + + pub(crate) fn sys_nt_map_view_of_section( + &self, + request: MapViewOfSectionParameters, + ) -> NtStatus { + if !request.process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + let Some(base) = request.base_address.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let Some(requested_view_size) = request.view_size.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let section_offset = match request.section_offset { + Some(section_offset) => match section_offset.read_at_offset(0) { + Some(value) if value >= 0 => usize::try_from(value).unwrap_or(usize::MAX), + Some(_) => return NtStatus::INVALID_PARAMETER, + None => return NtStatus::ACCESS_VIOLATION, + }, + None => 0, + }; + if base != 0 + || request.zero_bits != 0 + || request.commit_size != 0 + || !section_offset.is_multiple_of(PAGE_SIZE) + || !matches!(request.inherit_disposition, VIEW_SHARE | VIEW_UNMAP) + || request.allocation_type & !SUPPORTED_MAP_ALLOCATION_TYPES != 0 + { + return NtStatus::INVALID_PARAMETER; + } + let Some((page_protection, permissions)) = parse_page_protection(request.page_protection) + else { + return NtStatus::INVALID_PAGE_PROTECTION; + }; + let entry = match self.section_entry(request.section_handle) { + Ok(entry) => entry, + Err(status) => return status, + }; + let result = entry.with_entry(|entry| { + entry + .granted_access + .require(required_map_access(page_protection)) + .map(|()| Arc::clone(&entry.section)) + }); + let section = match result { + Ok(section) => section, + Err(status) => return status, + }; + match section.backing { + SectionBacking::Pagefile => self.map_pagefile_section( + request, + §ion, + requested_view_size, + section_offset, + page_protection, + permissions, + ), + SectionBacking::ImageFile => self.map_image_section(request, §ion, page_protection), + } + } + + pub(crate) fn sys_nt_map_view_of_section_ex( + &self, + request: MapViewOfSectionParameters, + extended_parameters: Option>, + extended_parameter_count: u32, + ) -> NtStatus { + if extended_parameters.is_some() || extended_parameter_count != 0 { + // TODO(section-subsystem): model MEM_EXTENDED_PARAMETER address requirements. + return NtStatus::INVALID_PARAMETER; + } + self.sys_nt_map_view_of_section(request) + } + + pub(crate) fn sys_nt_unmap_view_of_section( + &self, + process_handle: ProcessHandle, + base_address: usize, + ) -> NtStatus { + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + let Some((view_base, view)) = self.remove_section_view_for_address(base_address) else { + return NtStatus::NOT_MAPPED_VIEW; + }; + let ptr = MutPtr::::from_usize(view_base); + // SAFETY: Section views are tracked only after this shim successfully creates the pages; + // unmapping consumes the tracked view and removes the exact owned range. + if unsafe { self.global.page_manager.remove_pages(ptr, view.size) }.is_err() { + self.process.section_views.write().insert(view_base, view); + return NtStatus::UNABLE_TO_FREE_VM; + } + self.process.virtual_allocations.write().remove(&view_base); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_unmap_view_of_section_ex( + &self, + process_handle: ProcessHandle, + base_address: usize, + flags: u32, + ) -> NtStatus { + let flags = UnmapViewOfSectionFlags::from_bits_retain(flags); + if !flags.is_empty() { + return NtStatus::INVALID_PARAMETER; + } + self.sys_nt_unmap_view_of_section(process_handle, base_address) + } + + fn publish_section_handle( + &self, + section_handle: MutPtr, + section: Arc>, + granted_access: SectionAccess, + ) -> NtStatus { + let handle = match self.insert_section_handle(section, granted_access) { + Ok(handle) => handle, + Err(status) => return status, + }; + if section_handle.write_at_offset(0, handle).is_none() { + self.close_section_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + fn insert_named_section(&self, name: &str, section: &Arc>) -> NtStatus { + if self + .process + .directory_namespace + .resolve_object(name) + .is_ok() + { + return NtStatus::OBJECT_NAME_COLLISION; + } + if !self + .process + .directory_namespace + .parent_directory_exists(name) + { + return NtStatus::OBJECT_PATH_NOT_FOUND; + } + let key = section_key(name); + let mut namespace = self.process.section_namespace.write(); + if let Some(existing) = namespace.get(&key) + && existing.upgrade().is_some() + { + return NtStatus::OBJECT_NAME_EXISTS; + } + namespace.insert(key, Arc::downgrade(section)); + NtStatus::SUCCESS + } + + fn named_section(&self, name: &str) -> Option>> { + let key = section_key(name); + let mut namespace = self.process.section_namespace.write(); + let section = namespace.get(&key).and_then(alloc::sync::Weak::upgrade); + if section.is_none() { + namespace.remove(&key); + } + section + } + + fn map_pagefile_section( + &self, + request: MapViewOfSectionParameters, + section: &Arc>, + requested_view_size: usize, + section_offset: usize, + page_protection: PageProtection, + permissions: MemoryRegionPermissions, + ) -> NtStatus { + if section_offset > section.size { + return NtStatus::INVALID_VIEW_SIZE; + } + let remaining = section.size - section_offset; + let view_size = if requested_view_size == 0 { + remaining + } else { + requested_view_size + }; + if view_size == 0 || view_size > remaining { + return NtStatus::INVALID_VIEW_SIZE; + } + let Some(mapped_size) = view_size.checked_next_multiple_of(PAGE_SIZE) else { + return NtStatus::INVALID_VIEW_SIZE; + }; + let Some(length) = NonZeroPageSize::::new(mapped_size) else { + return NtStatus::INVALID_VIEW_SIZE; + }; + match section.backing { + SectionBacking::Pagefile => {} + SectionBacking::ImageFile => return NtStatus::INVALID_FILE_FOR_SECTION, + } + if !pagefile_view_protection_is_compatible(section.protection, page_protection) { + litebox_util_log::debug!( + section_protection:% = format_args!("{:#x}", section.protection.bits()), + page_protection:% = format_args!("{:#x}", page_protection.bits()); + "Rejected pagefile section view protection incompatible with section protection" + ); + return NtStatus::SECTION_PROTECTION; + } + if section.pagefile_view_active.swap(true, Ordering::AcqRel) { + litebox_util_log::debug!( + section_size = section.size, + requested_view_size, + section_offset; + "Rejected additional pagefile section view" + ); + // Host 25H2 allows repeated and simultaneous pagefile views. LiteBox + // returns NOT_SUPPORTED until PageManager has first-class shared + // anonymous backing that avoids kernel-side content storage. + return NtStatus::NOT_SUPPORTED; + } + let Ok(mapping) = create_pages( + &self.global.page_manager, + None, + length, + CreatePagesFlags::empty(), + permissions, + |_| Ok(0), + ) else { + section.pagefile_view_active.store(false, Ordering::Release); + return NtStatus::NO_MEMORY; + }; + let base = mapping.as_usize(); + if request.base_address.write_at_offset(0, base).is_none() + || request.view_size.write_at_offset(0, view_size).is_none() + { + let _ = remove_view_pages::(&self.global.page_manager, base, mapped_size); + section.pagefile_view_active.store(false, Ordering::Release); + return NtStatus::ACCESS_VIOLATION; + } + self.process.section_views.write().insert( + base, + WindowsSectionView { + size: mapped_size, + section_offset, + section: Some(Arc::clone(section)), + }, + ); + self.process.virtual_allocations.write().insert( + base, + crate::WindowsVirtualAllocation { + base, + size: mapped_size, + allocation_protect: section.protection, + type_: MemoryType::MEM_MAPPED, + pages: committed_pages(base, mapped_size, page_protection), + }, + ); + NtStatus::SUCCESS + } + + fn map_image_section( + &self, + request: MapViewOfSectionParameters, + section: &SectionObject, + page_protection: PageProtection, + ) -> NtStatus { + let Some(fs_path) = §ion.fs_path else { + return NtStatus::INVALID_FILE_FOR_SECTION; + }; + if required_map_access(page_protection).contains(SectionAccess::MAP_WRITE) { + litebox_util_log::debug!( + page_protection:% = format_args!("{:#x}", page_protection.bits()), + fs_path:% = fs_path; + "Rejected writable image section view" + ); + // Host 25H2 maps SEC_IMAGE with PAGE_READWRITE/PAGE_EXECUTE_READWRITE successfully + // (NtMapViewOfSection returns STATUS_IMAGE_NOT_AT_BASE in the probe). LiteBox rejects + // TODO(section-subsystem): allow this once image mappings support writable + // copy-on-write/shared image pages. + return NtStatus::SECTION_PROTECTION; + } + let mapping = match crate::loader::load_image_section( + self.global.platform, + Arc::clone(&self.fs), + fs_path, + &self.global.page_manager, + &self.process.virtual_allocations, + ) { + Ok(mapping) => mapping, + Err(crate::loader::WindowsLoadError::Access(_)) => { + return NtStatus::OBJECT_NAME_NOT_FOUND; + } + Err(crate::loader::WindowsLoadError::Load(_)) => return NtStatus::NO_MEMORY, + Err(_) => return NtStatus::INVALID_FILE_FOR_SECTION, + }; + if request + .base_address + .write_at_offset(0, mapping.base_addr) + .is_none() + || request + .view_size + .write_at_offset(0, mapping.image_size) + .is_none() + { + let _ = remove_view_pages::( + &self.global.page_manager, + mapping.base_addr, + mapping.mapping_size, + ); + self.process + .virtual_allocations + .write() + .remove(&mapping.base_addr); + return NtStatus::ACCESS_VIOLATION; + } + self.process.section_views.write().insert( + mapping.base_addr, + WindowsSectionView { + size: mapping.mapping_size, + section_offset: 0, + section: None, + }, + ); + NtStatus::SUCCESS + } + + fn remove_section_view_for_address( + &self, + base_address: usize, + ) -> Option<(usize, WindowsSectionView)> { + let mut views = self.process.section_views.write(); + let (&view_base, view) = views.range(..=base_address).next_back()?; + let view = view.clone(); + let view_end = view_base.checked_add(view.size)?; + if base_address < view_end { + views.remove(&view_base); + Some((view_base, view)) + } else { + None + } + } + + fn read_section_name( + &self, + object_attributes: Option>, + ) -> Result, NtStatus> { + let (_, directory_name) = + self.read_directory_object_attributes(object_attributes, false)?; + Ok(directory_name.map(|name| name.original_path)) + } + + fn read_required_section_name( + &self, + object_attributes: Option>, + ) -> Result { + let (_, Some(directory_name)) = + self.read_directory_object_attributes(object_attributes, true)? + else { + return Err(NtStatus::INVALID_PARAMETER); + }; + Ok(directory_name.original_path) + } +} + +fn section_key(path: &str) -> String { + path.to_ascii_lowercase() +} + +fn section_missing_status(parent_exists: bool) -> NtStatus { + if parent_exists { + NtStatus::OBJECT_NAME_NOT_FOUND + } else { + NtStatus::OBJECT_PATH_NOT_FOUND + } +} + +fn known_dll_section_fs_path(object_path: &str) -> Option { + let (dll_name, fs_directory) = + if let Some(rest) = strip_case_insensitive_prefix(object_path, r"\KnownDlls\") { + (rest, "/Windows/System32/") + } else if let Some(rest) = strip_case_insensitive_prefix(object_path, r"\KnownDlls32\") { + (rest, "/Windows/SysWOW64/") + } else { + return None; + }; + if dll_name.contains(['\\', '/']) || !ends_with_ignore_ascii_case(dll_name, ".dll") { + return None; + } + let mut fs_path = String::from(fs_directory); + fs_path.push_str(&dll_name.to_ascii_lowercase()); + Some(fs_path) +} + +fn strip_case_insensitive_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { + value + .get(..prefix.len()) + .is_some_and(|head| head.eq_ignore_ascii_case(prefix)) + .then_some(&value[prefix.len()..]) +} + +fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool { + value + .get(value.len().saturating_sub(suffix.len())..) + .is_some_and(|tail| tail.eq_ignore_ascii_case(suffix)) +} + +fn required_map_access(protection: PageProtection) -> SectionAccess { + let base = protection.bits() & PageProtection::BASE_MASK; + if base == PageProtection::PAGE_NOACCESS.bits() { + SectionAccess::MAP_READ + } else if matches!( + base, + value if value == PageProtection::PAGE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + ) { + SectionAccess::MAP_WRITE + } else if matches!( + base, + value if value == PageProtection::PAGE_EXECUTE.bits() + || value == PageProtection::PAGE_EXECUTE_READ.bits() + || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() + ) { + SectionAccess::MAP_EXECUTE + } else { + SectionAccess::MAP_READ + } +} + +fn pagefile_view_protection_is_compatible( + section_protection: PageProtection, + view_protection: PageProtection, +) -> bool { + let view_base = view_protection.bits() & PageProtection::BASE_MASK; + if view_base == PageProtection::PAGE_NOACCESS.bits() { + return true; + } + + if page_protection_has_read(view_protection) && !page_protection_has_read(section_protection) { + return false; + } + if page_protection_has_direct_write(view_protection) + && !page_protection_has_direct_write(section_protection) + { + return false; + } + if page_protection_has_execute(view_protection) + && !page_protection_has_execute(section_protection) + { + return false; + } + true +} + +fn page_protection_has_read(protection: PageProtection) -> bool { + matches!( + protection.bits() & PageProtection::BASE_MASK, + value if value == PageProtection::PAGE_READONLY.bits() + || value == PageProtection::PAGE_READWRITE.bits() + || value == PageProtection::PAGE_WRITECOPY.bits() + || value == PageProtection::PAGE_EXECUTE_READ.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() + ) +} + +fn page_protection_has_direct_write(protection: PageProtection) -> bool { + matches!( + protection.bits() & PageProtection::BASE_MASK, + value if value == PageProtection::PAGE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + ) +} + +fn page_protection_has_execute(protection: PageProtection) -> bool { + matches!( + protection.bits() & PageProtection::BASE_MASK, + value if value == PageProtection::PAGE_EXECUTE.bits() + || value == PageProtection::PAGE_EXECUTE_READ.bits() + || value == PageProtection::PAGE_EXECUTE_READWRITE.bits() + || value == PageProtection::PAGE_EXECUTE_WRITECOPY.bits() + ) +} + +fn write_section_basic_information( + section: &SectionObject, + section_information: MutPtr, + section_information_length: usize, + return_length: Option>, +) -> NtStatus { + let required_len = size_of::(); + if section_information_length < required_len { + return NtStatus::INFO_LENGTH_MISMATCH; + } + let Ok(size) = i64::try_from(section.size) else { + return NtStatus::SECTION_TOO_BIG; + }; + let info = SectionBasicInformation { + base_address: 0, + attributes: section.attributes.bits(), + _padding: 0, + size, + }; + let output = + MutPtr::::from_usize(section_information.as_usize()); + if output.write_at_offset(0, info).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(return_length) = return_length + && return_length.write_at_offset(0, required_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS +} + +fn write_section_image_information( + section: &SectionObject, + fs: Arc, + section_information: MutPtr, + section_information_length: usize, + return_length: Option>, +) -> NtStatus { + if !matches!(section.backing, SectionBacking::ImageFile) { + return NtStatus::SECTION_NOT_IMAGE; + } + let required_len = size_of::(); + if section_information_length < required_len { + return NtStatus::INFO_LENGTH_MISMATCH; + } + let Some(fs_path) = §ion.fs_path else { + return NtStatus::INVALID_FILE_FOR_SECTION; + }; + let metadata = match crate::loader::image_section_metadata(fs, fs_path) { + Ok(metadata) => metadata, + Err(crate::loader::WindowsLoadError::Access(_)) => return NtStatus::OBJECT_NAME_NOT_FOUND, + Err(_) => return NtStatus::INVALID_FILE_FOR_SECTION, + }; + // Host ntdll reports ReturnLength=64 for SectionImageInformation on x64; the public + // winternl.h layout ends at CheckSum and has no trailing extension fields. + let info = SectionImageInformation { + transfer_address: metadata.transfer_address, + zero_bits: 0, + _padding0: 0, + maximum_stack_size: 0, + committed_stack_size: 0, + subsystem_type: metadata.subsystem, + subsystem_minor_version: metadata.subsystem_minor_version, + subsystem_major_version: metadata.subsystem_major_version, + gp_value: 0, + image_characteristics: metadata.image_characteristics, + dll_characteristics: metadata.dll_characteristics, + machine: metadata.machine, + image_contains_code: 1, + image_flags: 0, + loader_flags: 0, + image_file_size: metadata.file_size, + checksum: 0, + }; + let output = + MutPtr::::from_usize(section_information.as_usize()); + if output.write_at_offset(0, info).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if let Some(return_length) = return_length + && return_length.write_at_offset(0, required_len).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS +} + +fn committed_pages( + base: usize, + size: usize, + protect: PageProtection, +) -> RangeMap { + let mut pages = RangeMap::new(); + if let Some(end) = base.checked_add(size) { + pages.insert(base..end, protect); + } + pages +} + +fn remove_view_pages( + page_manager: &crate::WindowsPageManager, + base: usize, + size: usize, +) -> Result<(), ()> { + let ptr = MutPtr::::from_usize(base); + // SAFETY: The caller passes a section view range created by this module and not yet exposed, + // or a tracked view being rolled back after output write failure. + unsafe { page_manager.remove_pages(ptr, size) }.map_err(|_| ()) +} + +#[cfg(test)] +mod tests { + extern crate std; + + use core::mem::{size_of, size_of_val}; + + use litebox::platform::RawMutPointer as _; + use litebox_common_windows::nt_status::NtStatus; + + use super::*; + use crate::nt_types::{ObjectAttributes, UnicodeString}; + use crate::tests::{TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task}; + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664; + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + const IMAGE_SUBSYSTEM_WINDOWS_CUI: u32 = 3; + + fn wide(value: &str) -> alloc::vec::Vec { + value.encode_utf16().collect() + } + + fn unicode(value: &[u16]) -> UnicodeString { + UnicodeString { + length: u16::try_from(size_of_val(value)).unwrap(), + maximum_length: u16::try_from(size_of_val(value)).unwrap(), + padding_0: [0; 4], + buffer: value.as_ptr() as usize, + } + } + + fn object_attributes(name: &UnicodeString) -> ObjectAttributes { + ObjectAttributes { + length: u32::try_from(size_of::()).unwrap(), + root_directory: Handle::from_raw(0), + object_name: core::ptr::from_ref(name) as usize, + attributes: 0, + security_descriptor: 0, + security_quality_of_service: 0, + } + } + + fn create_pagefile_section( + task: &Task, + access: u32, + size: i64, + protection: PageProtection, + ) -> Handle { + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut handle), + access, + None, + Some(const_ptr(&size)), + protection.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::SUCCESS + ); + handle + } + + fn map_pagefile_section(task: &Task, handle: Handle) -> (usize, usize) { + let mut base = 0usize; + let mut view_size = 0usize; + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::SUCCESS + ); + (base, view_size) + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + fn host_kernel32_image() -> std::vec::Vec { + let system_root = std::env::var_os("SystemRoot").expect("SystemRoot is set on Windows"); + std::fs::read( + std::path::PathBuf::from(system_root) + .join("System32") + .join("kernel32.dll"), + ) + .expect("host kernel32.dll is readable") + } + + #[test] + fn nt_create_section_creates_queryable_pagefile_section() { + let task = test_task(); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2345, + PageProtection::PAGE_READWRITE, + ); + let mut info = SectionBasicInformation { + base_address: usize::MAX, + attributes: u32::MAX, + _padding: u32::MAX, + size: -1, + }; + let mut return_length = 0usize; + + assert_eq!( + task.sys_nt_query_section( + handle, + SectionInformationClass::Basic as u32, + mut_byte_ptr(&mut info), + size_of::(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::SUCCESS + ); + assert_eq!(return_length, size_of::()); + assert_eq!(info.base_address, 0); + assert_eq!( + info.attributes, + SectionAllocationAttributes::SEC_COMMIT.bits() + ); + assert_eq!(info.size, 0x3000); + + let mut too_small = [0xcc; size_of::() - 1]; + let too_small_len = too_small.len(); + return_length = 0x5555_5555; + // Host 25H2 leaves ReturnLength untouched on INFO_LENGTH_MISMATCH + // (Basic len=23 -> ret stays sentinel) and writes 0x18 only on success. + assert_eq!( + task.sys_nt_query_section( + handle, + SectionInformationClass::Basic as u32, + mut_byte_ptr(&mut too_small), + too_small_len, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!(return_length, 0x5555_5555); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[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 name = wide(r"\KnownDlls\kernel32.dll"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut handle), + SectionAccess::QUERY.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + + let mut info = ::new_zeroed(); + let mut return_length = 0usize; + assert_eq!( + task.sys_nt_query_section( + handle, + SectionInformationClass::Image as u32, + mut_byte_ptr(&mut info), + size_of::(), + Some(mut_ptr(&mut return_length)), + ), + NtStatus::SUCCESS + ); + + assert_eq!(return_length, size_of::()); + assert_eq!(info.machine, IMAGE_FILE_MACHINE_AMD64); + assert_eq!(info.subsystem_type, IMAGE_SUBSYSTEM_WINDOWS_CUI); + assert_eq!(info.image_contains_code, 1); + assert_eq!(info.image_file_size, u32::try_from(image.len()).unwrap()); + + let mut too_small = [0xcc; size_of::() - 1]; + let too_small_len = too_small.len(); + return_length = 0x5555_5555; + // Host 25H2 leaves ReturnLength untouched on INFO_LENGTH_MISMATCH + // (Image len=63 -> ret stays sentinel) and writes 0x40 only on success. + assert_eq!( + task.sys_nt_query_section( + handle, + SectionInformationClass::Image as u32, + mut_byte_ptr(&mut too_small), + too_small_len, + Some(mut_ptr(&mut return_length)), + ), + NtStatus::INFO_LENGTH_MISMATCH + ); + assert_eq!(return_length, 0x5555_5555); + } + + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] + #[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 name = wide(r"\KnownDlls\kernel32.dll"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut handle), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + + let mut base = 0usize; + let mut view_size = 0usize; + // Host 25H2 maps SEC_IMAGE with PAGE_READWRITE successfully + // (NtMapViewOfSection returns STATUS_IMAGE_NOT_AT_BASE). LiteBox rejects writable image + // views until image mappings are backed by real shared image pages. + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::SECTION_PROTECTION + ); + assert_eq!(base, 0); + assert_eq!(view_size, 0); + + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_EXECUTE_READ.bits(), + }), + NtStatus::SUCCESS + ); + assert_ne!(base, 0); + assert_ne!(view_size, 0); + } + + #[test] + fn section_output_handles_follow_host_probe_contracts() { + let task = test_task(); + let name = wide(r"\KnownDlls\DefinitelyMissingLiteBoxProbe.dll"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let mut open_handle = Handle::from_raw(0x1111_2222); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut open_handle), + SectionAccess::QUERY.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!(open_handle, Handle::default()); + + let mut create_handle = Handle::from_raw(0x3333_4444); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut create_handle), + SectionAccess::ALL_ACCESS.bits(), + None, + None, + PageProtection::PAGE_READWRITE.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::INVALID_PARAMETER_4 + ); + assert_eq!(create_handle, Handle::from_raw(0x3333_4444)); + } + + #[test] + fn nt_map_view_of_section_maps_writable_pagefile_section() { + let task = test_task(); + let handle = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); + let (base, view_size) = map_pagefile_section(&task, handle); + assert_ne!(base, 0); + assert_eq!(view_size, 0x2000); + + let mapped = MutPtr::::from_usize(base); + assert_eq!(mapped.read_at_offset(0), Some(0)); + assert!(mapped.write_at_offset(0, 0xfeed_cafe).is_some()); + assert_eq!(mapped.read_at_offset(0), Some(0xfeed_cafe)); + + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, base + 0x100), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, base), + NtStatus::NOT_MAPPED_VIEW + ); + } + + #[test] + fn pagefile_map_rejects_protection_incompatible_with_section_protection() { + let task = test_task(); + let readonly = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READONLY, + ); + let execute = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_EXECUTE, + ); + let readwrite = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); + + for (handle, page_protection) in [ + (readonly, PageProtection::PAGE_READWRITE), + (execute, PageProtection::PAGE_READONLY), + (readwrite, PageProtection::PAGE_EXECUTE_READ), + ] { + let mut base = 0usize; + let mut view_size = 0usize; + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: page_protection.bits(), + }), + NtStatus::SECTION_PROTECTION + ); + assert_eq!(base, 0); + assert_eq!(view_size, 0); + } + } + + #[test] + fn pagefile_map_accepts_compatible_noaccess_and_copy_protections() { + let task = test_task(); + // Host 25H2 and ReactOS allow PAGE_WRITECOPY and PAGE_NOACCESS views of + // a PAGE_READONLY pagefile section. + for page_protection in [ + PageProtection::PAGE_WRITECOPY, + PageProtection::PAGE_NOACCESS, + ] { + let readonly = create_pagefile_section( + &task, + SectionAccess::ALL_ACCESS.bits(), + 0x2000, + PageProtection::PAGE_READONLY, + ); + let mut base = 0usize; + let mut view_size = 0usize; + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: readonly, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: page_protection.bits(), + }), + NtStatus::SUCCESS + ); + assert_ne!(base, 0); + assert_eq!(view_size, 0x2000); + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, base), + NtStatus::SUCCESS + ); + } + } + + #[test] + fn pagefile_noaccess_view_requires_map_read_access() { + let task = test_task(); + let handle = create_pagefile_section( + &task, + SectionAccess::QUERY.bits(), + 0x2000, + PageProtection::PAGE_READWRITE, + ); + let mut base = 0usize; + let mut view_size = 0usize; + + // Host 25H2 returns STATUS_ACCESS_DENIED for PAGE_NOACCESS maps unless + // the section handle has SECTION_MAP_READ. + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: handle, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut view_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_NOACCESS.bits(), + }), + NtStatus::ACCESS_DENIED + ); + assert_eq!(base, 0); + assert_eq!(view_size, 0); + } + + #[test] + fn pagefile_section_rejects_additional_views_across_handles_and_unmap() { + let task = test_task(); + let name = wide(r"\BaseNamedObjects\LiteBoxSingleViewSection"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let size = 0x2000i64; + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut handle), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + Some(const_ptr(&size)), + PageProtection::PAGE_READWRITE.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::SUCCESS + ); + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut opened), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + let (first_base, first_size) = map_pagefile_section(&task, handle); + assert_eq!(first_size, 0x2000); + + let mut second_base = 0usize; + let mut second_size = 0usize; + // Host 25H2 permits this second simultaneous pagefile view (STATUS_SUCCESS). LiteBox + // deliberately returns STATUS_NOT_SUPPORTED until shared anonymous backing exists. + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: opened, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut second_base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut second_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::NOT_SUPPORTED + ); + assert_eq!(second_base, 0); + assert_eq!(second_size, 0); + + assert_eq!( + task.sys_nt_unmap_view_of_section(ProcessHandle::CURRENT, first_base), + NtStatus::SUCCESS + ); + second_base = 0; + second_size = 0; + assert_eq!( + task.sys_nt_map_view_of_section(MapViewOfSectionParameters { + section_handle: opened, + process_handle: ProcessHandle::CURRENT, + base_address: mut_ptr(&mut second_base), + zero_bits: 0, + commit_size: 0, + section_offset: None, + view_size: mut_ptr(&mut second_size), + inherit_disposition: VIEW_SHARE, + allocation_type: 0, + page_protection: PageProtection::PAGE_READWRITE.bits(), + }), + NtStatus::NOT_SUPPORTED + ); + assert_eq!(second_base, 0); + assert_eq!(second_size, 0); + } + + #[test] + fn nt_open_section_opens_existing_named_pagefile_section() { + let task = test_task(); + let name = wide(r"\BaseNamedObjects\LiteBoxNamedSection"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let size = 0x1000i64; + let mut created = Handle::default(); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut created), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + Some(const_ptr(&size)), + PageProtection::PAGE_READWRITE.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::SUCCESS + ); + + let mut opened = Handle::default(); + assert_eq!( + task.sys_nt_open_section( + mut_ptr(&mut opened), + SectionAccess::QUERY.bits(), + Some(const_ptr(&attrs)), + ), + NtStatus::SUCCESS + ); + assert_ne!(opened, Handle::default()); + assert_ne!(opened, created); + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index c76ad3e2af..2583ab0396 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -155,6 +155,8 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task::new(RawDescriptorStorage::new()), directory_namespace, event_namespace: crate::WindowsEventNamespace::::new(BTreeMap::new()), + section_namespace: crate::WindowsSectionNamespace::::new(BTreeMap::new()), + section_views: crate::WindowsSectionViews::::new(BTreeMap::new()), nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), virtual_allocations: crate::WindowsVirtualAllocations::::new( BTreeMap::new(), From e1ca2e5107d713f630e541951a9b90e1831d976e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 3 Jul 2026 14:26:30 -0700 Subject: [PATCH 086/319] Dispatch broker event readiness notifications (#1002) Adds a LiteBox broker notification dispatch path and a BrokerHandleRegistry that maps broker object handles to local pollables, so EventReadiness notifications can wake blocked broker-backed event counters. Event counters register/unregister their pollable by handle, and the Linux userland runner now dispatches notification-channel messages through a narrow LiteBox dispatcher instead of only draining them. Includes focused coverage that a broker readiness notification wakes a blocked broker-backed event counter read. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 107 ++++++++++++++ litebox/src/event/counter.rs | 156 +++++++++++++++++++- litebox/src/litebox.rs | 34 ++++- litebox_runner_linux_userland/src/broker.rs | 40 ++--- litebox_runner_linux_userland/src/lib.rs | 16 +- 5 files changed, 317 insertions(+), 36 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 6d51432bf8..66468e8c33 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -1,11 +1,19 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use alloc::{ + sync::{Arc, Weak}, + vec::Vec, +}; + +use hashbrown::HashMap; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode, ReadinessState}; +use crate::event::{Events, polling::Pollee}; +use crate::platform::TimeProvider; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; pub(crate) mod error; @@ -46,6 +54,105 @@ pub(crate) trait BrokerControl: Send + Sync { fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError>; } +pub(crate) struct BrokerHandleRegistry { + handles: Mutex>>, +} + +impl BrokerHandleRegistry { + pub(crate) fn new() -> Self { + Self { + handles: Mutex::new(HashMap::new()), + } + } + + pub(crate) fn register_pollable(&self, handle: ObjectHandle, pollee: &Arc>) { + self.handles + .lock() + .entry(handle) + .or_insert_with(BrokerHandleEntry::new) + .register_pollable(pollee); + } + + pub(crate) fn unregister_pollable(&self, handle: ObjectHandle, pollee: &Arc>) { + let mut handles = self.handles.lock(); + if let Some(entry) = handles.get_mut(&handle) { + entry.unregister_pollable(pollee); + if entry.is_empty() { + handles.remove(&handle); + } + } + } + + pub(crate) fn notify_readiness(&self, handle: ObjectHandle, readiness: ReadinessState) + where + Platform: TimeProvider, + { + let mut handles = self.handles.lock(); + let Some(entry) = handles.get_mut(&handle) else { + return; + }; + entry.prune_stale_pollables(); + if entry.is_empty() { + handles.remove(&handle); + return; + } + + let mut events = Events::empty(); + if readiness.read_ready { + events |= Events::IN; + } + if readiness.write_ready { + events |= Events::OUT; + } + if !events.is_empty() { + entry.notify_pollables(events); + } + } +} + +struct BrokerHandleEntry { + pollables: Vec>>, +} + +impl BrokerHandleEntry { + fn new() -> Self { + Self { + pollables: Vec::new(), + } + } + + fn register_pollable(&mut self, pollee: &Arc>) { + self.pollables.push(Arc::downgrade(pollee)); + } + + fn unregister_pollable(&mut self, pollee: &Arc>) { + self.pollables.retain(|registered| { + registered + .upgrade() + .is_some_and(|registered| !Arc::ptr_eq(®istered, pollee)) + }); + } + + fn prune_stale_pollables(&mut self) { + self.pollables + .retain(|registered| registered.strong_count() > 0); + } + + fn notify_pollables(&self, events: Events) + where + Platform: TimeProvider, + { + for registered in &self.pollables { + if let Some(pollee) = registered.upgrade() { + pollee.notify_observers(events); + } + } + } + + fn is_empty(&self) -> bool { + self.pollables.is_empty() + } +} pub(crate) struct BrokerLocalControl< Platform: RawSyncPrimitivesProvider, Channel: LocalControlChannel + Send, diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 510d8f32fd..60ba5787ba 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -11,7 +11,7 @@ use thiserror::Error; use crate::{ LiteBox, broker::{ - BrokerControl, + BrokerControl, BrokerHandleRegistry, error::{BrokerControlError, BrokerObjectError}, }, event::{ @@ -44,7 +44,8 @@ pub enum EventCounterError { pub struct EventCounter { broker: Arc, handle: ObjectHandle, - pollee: Pollee, + registry: Arc>, + pollee: Arc>, } impl EventCounter @@ -65,10 +66,14 @@ where .create_event_with_count(initial_count) .map_err(BrokerObjectError::from) .map_err(EventCounterError::from)?; + let registry = litebox.broker_handle_registry(); + let pollee = Arc::new(Pollee::new()); + registry.register_pollable(handle, &pollee); Ok(Self { broker, handle, - pollee: Pollee::new(), + registry, + pollee, }) } @@ -136,6 +141,7 @@ where Platform: RawSyncPrimitivesProvider + TimeProvider, { fn drop(&mut self) { + self.registry.unregister_pollable(self.handle, &self.pollee); let _ = self.broker.close_object(self.handle); } } @@ -168,3 +174,147 @@ where events } } + +#[cfg(test)] +mod tests { + extern crate std; + + use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + use alloc::sync::Arc; + use litebox_broker_local::BrokerLocal; + use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::error::ErrorCode; + use litebox_broker_protocol::event::{CreateEventResponse, EventConsumption, ReadinessState}; + use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, + BrokerResponse, EventReadinessNotification, EventRequest, EventResponse, + }; + + use super::*; + use crate::LiteBox; + use crate::event::wait::WaitState; + use crate::platform::mock::MockPlatform; + + #[test] + fn readiness_notification_wakes_blocked_read() { + use std::time::{Duration, Instant}; + + let platform = MockPlatform::new(); + let handle = ObjectHandle(7); + let consume_attempts = Arc::new(AtomicUsize::new(0)); + let read_ready = Arc::new(AtomicBool::new(false)); + let local = BrokerLocal::negotiate(FakeLocalControlChannel { + handle, + consume_attempts: consume_attempts.clone(), + read_ready: read_ready.clone(), + last_request: None, + }) + .unwrap(); + let litebox = LiteBox::new_with_broker_local(platform, local); + let counter = Arc::new(EventCounter::new(&litebox, 0).unwrap()); + + let (result_sender, result_receiver) = std::sync::mpsc::channel(); + { + let counter = counter.clone(); + std::thread::spawn(move || { + result_sender + .send(counter.read( + &WaitState::new(platform).context(), + false, + EventCounterReadMode::One, + )) + .unwrap(); + }); + } + let deadline = Instant::now() + Duration::from_secs(1); + // The second consume attempt happens after the waiter has registered its observer. + while consume_attempts.load(Ordering::SeqCst) < 2 { + assert!(Instant::now() < deadline); + std::thread::yield_now(); + } + read_ready.store(true, Ordering::SeqCst); + litebox.dispatch_broker_notification(BrokerNotification::EventReadiness( + EventReadinessNotification { + handle, + readiness: ReadinessState { + read_ready: true, + write_ready: true, + }, + }, + )); + + assert_eq!( + result_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap() + .unwrap(), + 1 + ); + } + + struct FakeLocalControlChannel { + handle: ObjectHandle, + consume_attempts: Arc, + read_ready: Arc, + last_request: Option, + } + + impl LocalControlChannel for FakeLocalControlChannel { + type Error = core::convert::Infallible; + + fn send_handshake_request( + &mut self, + _request: &BrokerHandshakeRequest, + ) -> core::result::Result<(), Self::Error> { + Ok(()) + } + + fn recv_handshake_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + })) + } + + fn send_request( + &mut self, + request: &BrokerRequest, + ) -> core::result::Result<(), Self::Error> { + self.last_request = Some(request.clone()); + Ok(()) + } + + fn recv_response(&mut self) -> core::result::Result, Self::Error> { + let response = match self.last_request.take().unwrap() { + BrokerRequest::Event(EventRequest::Create(_)) => { + BrokerResponse::Event(EventResponse::Create(CreateEventResponse { + handle: self.handle, + })) + } + BrokerRequest::Event(EventRequest::Consume(request)) + if request.handle == self.handle => + { + self.consume_attempts.fetch_add(1, Ordering::SeqCst); + if self.read_ready.swap(false, Ordering::SeqCst) { + BrokerResponse::Event(EventResponse::Consume(EventConsumption { + value: 1, + readiness: ReadinessState { + read_ready: false, + write_ready: true, + }, + })) + } else { + BrokerResponse::Error(ErrorCode::WouldBlock) + } + } + BrokerRequest::CloseObject(handle) if handle == self.handle => { + BrokerResponse::ObjectClosed + } + request => panic!("unexpected broker request: {request:?}"), + }; + Ok(Some(response)) + } + } +} diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index ffd1b1c2ca..5418917f43 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -7,10 +7,12 @@ use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::message::BrokerNotification; use crate::{ broker, fd::Descriptors, + platform::TimeProvider, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -100,12 +102,11 @@ impl LiteBox { platform, descriptors, broker: broker_control, + broker_handles: Arc::new(broker::BrokerHandleRegistry::new()), }), } } -} -impl LiteBox { /// An explicitly-crate-internal clone method to prevent outside users from cloning the /// [`LiteBox`] object, which could cause confusion as to the intended use. External users must /// only create it via [`Self::new`]. @@ -138,6 +139,34 @@ impl LiteBox { pub(crate) fn broker_control(&self) -> Option> { self.x.broker.clone() } + + pub(crate) fn broker_handle_registry(&self) -> Arc> { + Arc::clone(&self.x.broker_handles) + } + + /// Dispatches one broker notification to the matching local-core object. + pub fn dispatch_broker_notification(&self, notification: BrokerNotification) + where + Platform: TimeProvider, + { + match notification { + BrokerNotification::EventReadiness(notification) => self + .x + .broker_handles + .notify_readiness(notification.handle, notification.readiness), + } + } + + /// Returns a narrow dispatcher for moving broker notification handling into deployment code. + pub fn broker_notification_dispatcher(&self) -> impl Fn(BrokerNotification) + Send + 'static + where + Platform: TimeProvider + 'static, + { + let litebox = self.clone(); + move |notification| { + litebox.dispatch_broker_notification(notification); + } + } } /// The actual body of [`LiteBox`], containing any components that might be shared. @@ -145,4 +174,5 @@ pub(crate) struct LiteBoxX { pub(crate) platform: &'static Platform, descriptors: RwLock>, broker: Option>, + broker_handles: Arc>, } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 7d36e96be9..d6bbc471d6 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -3,33 +3,26 @@ use std::{ path::Path, - thread::JoinHandle, time::{Duration, Instant}, }; use anyhow::{Context as _, Result}; use litebox_broker_local::{BrokerLocal, BrokerNotifications}; +use litebox_broker_protocol::message::BrokerNotification; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, }; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); const RETRY_DELAY: Duration = Duration::from_millis(20); -type Local = BrokerLocal; - -pub(crate) struct BrokerConnection { - local: Local, - #[expect( - dead_code, - reason = "keeps the notification receiver thread alive while the broker connection is installed" - )] - notification_receiver_thread: JoinHandle<()>, -} pub(crate) fn connect( control_socket_path: &Path, notification_socket_path: &Path, -) -> Result { +) -> Result<( + BrokerLocal, + BrokerNotifications, +)> { let setup_deadline = Instant::now() + SETUP_TIMEOUT; let control_channel = connect_with_retry( control_socket_path, @@ -56,13 +49,19 @@ pub(crate) fn connect( ) })?; let local = BrokerLocal::negotiate(control_channel).context("broker negotiation failed")?; - let mut notifications = BrokerNotifications::new(notification_channel); - let notification_thread = std::thread::Builder::new() + Ok((local, BrokerNotifications::new(notification_channel))) +} + +pub(crate) fn start_notification_receiver( + mut notifications: BrokerNotifications, + dispatch_notification: impl Fn(BrokerNotification) + Send + 'static, +) -> Result<()> { + std::thread::Builder::new() .name("litebox-broker-notifications".to_owned()) .spawn(move || { loop { match notifications.recv_notification() { - Ok(Some(_notification)) => {} + Ok(Some(notification)) => dispatch_notification(notification), Ok(None) => break, Err(error) => { eprintln!("failed to receive broker notification: {error}"); @@ -72,16 +71,7 @@ pub(crate) fn connect( } }) .context("failed to start broker notification receiver")?; - Ok(BrokerConnection { - local, - notification_receiver_thread: notification_thread, - }) -} - -impl BrokerConnection { - pub(crate) fn into_local(self) -> Local { - self.local - } + Ok(()) } fn connect_with_retry( diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 76c4e7b5e8..310efe88bb 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -241,12 +241,16 @@ pub fn run(cli_args: CliArgs) -> Result<()> { }; let shim_builder = if let Some(broker_connection) = broker_connection { - litebox_shim_linux::LinuxShimBuilder::new_with_litebox( - litebox::LiteBox::new_with_broker_local( - litebox_platform_multiplex::platform(), - broker_connection.into_local(), - ), - ) + let (broker_local, broker_notifications) = broker_connection; + let litebox = litebox::LiteBox::new_with_broker_local( + litebox_platform_multiplex::platform(), + broker_local, + ); + broker::start_notification_receiver( + broker_notifications, + litebox.broker_notification_dispatcher(), + )?; + litebox_shim_linux::LinuxShimBuilder::new_with_litebox(litebox) } else { litebox_shim_linux::LinuxShimBuilder::new() }; From de95b5de30de3c42f6b2a1fa45570c8b936f694c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 6 Jul 2026 03:04:09 -0700 Subject: [PATCH 087/319] Make Linux eventfd broker-backed (#1005) Makes the Linux eventfd syscall path use the broker-backed EventCounter implementation and removes the shim-local Linux eventfd fallback. Non-broker Linux shim and runner tests now either expect eventfd creation to fail without broker control or use pipes for non-eventfd-specific fd coverage. Broker-backed Linux eventfd coverage remains in the broker runner integration test, including blocking/nonblocking behavior, poll/epoll readiness, sendfile in_fd errno behavior, and EFD_CLOEXEC/FD_CLOEXEC handling. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_runner_linux_userland/tests/eventfd.c | 67 +++++ litebox_runner_linux_userland/tests/execve.c | 13 +- litebox_runner_linux_userland/tests/run.rs | 35 ++- .../tests/sendfile.c | 9 - litebox_shim_linux/src/syscalls/epoll.rs | 129 ++------- litebox_shim_linux/src/syscalls/eventfd.rs | 259 ++---------------- litebox_shim_linux/src/syscalls/tests.rs | 13 +- 7 files changed, 138 insertions(+), 387 deletions(-) diff --git a/litebox_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c index 4e3926fc2b..6f649ddf16 100644 --- a/litebox_runner_linux_userland/tests/eventfd.c +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -10,8 +10,16 @@ #include #include #include +#include +#include #include +#define SENDFILE_DST_PATH "/tmp/lb_eventfd_sendfile_dst" + +static ssize_t sys_sendfile(int out_fd, int in_fd, off_t *offset, size_t count) { + return (ssize_t)syscall(SYS_sendfile, out_fd, in_fd, offset, count); +} + static int expect_eagain_read(int fd) { uint64_t value = 0; errno = 0; @@ -87,6 +95,14 @@ static int expect_nonblock(int fd, int expected) { return ((flags & O_NONBLOCK) != 0) == expected ? 0 : 2; } +static int expect_cloexec(int fd, int expected) { + int flags = fcntl(fd, F_GETFD); + if (flags < 0) { + return 1; + } + return ((flags & FD_CLOEXEC) != 0) == expected ? 0 : 2; +} + static int expect_ebadf_close(int fd) { errno = 0; if (close(fd) != -1) { @@ -222,6 +238,51 @@ static int test_epoll_wakeup(void) { return expect_close(fd) == 0 ? 0 : 9; } +static int test_sendfile_in_fd(void) { + int fd = eventfd(7, 0); + if (fd < 0) { + return 1; + } + int dst = open(SENDFILE_DST_PATH, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (dst < 0) { + return 2; + } + + errno = 0; + if (sys_sendfile(dst, fd, NULL, 4) != -1 || errno != EINVAL) { + return 3; + } + + off_t off = 0; + errno = 0; + if (sys_sendfile(dst, fd, &off, 4) != -1 || errno != ESPIPE) { + return 4; + } + + if (expect_close(dst) != 0) { + return 5; + } + unlink(SENDFILE_DST_PATH); + return expect_close(fd) == 0 ? 0 : 6; +} + +static int test_cloexec_flag(void) { + int fd = eventfd(0, EFD_CLOEXEC); + if (fd < 0) { + return 1; + } + if (expect_cloexec(fd, 1) != 0) { + return 2; + } + if (fcntl(fd, F_SETFD, 0) != 0) { + return 3; + } + if (expect_cloexec(fd, 0) != 0) { + return 4; + } + return expect_close(fd) == 0 ? 0 : 5; +} + int main(void) { alarm(10); @@ -464,6 +525,12 @@ int main(void) { if (test_epoll_wakeup() != 0) { return 142; } + if (test_sendfile_in_fd() != 0) { + return 143; + } + if (test_cloexec_flag() != 0) { + return 144; + } alarm(0); return 0; diff --git a/litebox_runner_linux_userland/tests/execve.c b/litebox_runner_linux_userland/tests/execve.c index 75a322c480..25b62585a9 100644 --- a/litebox_runner_linux_userland/tests/execve.c +++ b/litebox_runner_linux_userland/tests/execve.c @@ -4,7 +4,7 @@ // Test execve behavior: // // Phase 1: -// - Create two eventfds: one with EFD_CLOEXEC, one without. +// - Create two pipe descriptors: one with O_CLOEXEC, one without. // - Exec self, passing their numeric values as argv[1] (cloexec) and argv[2] (keep). // Phase 2 (after exec): // - Verify the CLOEXEC fd is closed (fcntl -> EBADF). @@ -19,7 +19,6 @@ #include #include #include -#include #include #include @@ -39,10 +38,12 @@ int main(int argc, char *argv[], char *envp[]) { if (!phase) { // Phase 1: set up descriptors and exec self. - int fd_clo = eventfd(0, EFD_CLOEXEC); - if (fd_clo < 0) die("eventfd cloexec"); - int fd_keep = eventfd(0, 0); - if (fd_keep < 0) die("eventfd keep"); + int clo_pipe[2]; + if (pipe2(clo_pipe, O_CLOEXEC) != 0) die("pipe2 cloexec"); + int keep_pipe[2]; + if (pipe(keep_pipe) != 0) die("pipe keep"); + int fd_clo = clo_pipe[0]; + int fd_keep = keep_pipe[0]; char clo_buf[32], keep_buf[32]; snprintf(clo_buf, sizeof clo_buf, "%d", fd_clo); diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 3ad2bf7504..737305ba32 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -463,7 +463,15 @@ impl #[cfg(all(target_arch = "x86_64", target_os = "linux"))] #[test] fn test_runner_broker_integration_with_rewriter() { + const HELLO_WORLD_JS: &str = r" +const fs = require('node:fs'); + +const content = 'Hello World!'; +console.log(content); +"; + let true_path = run_which("true"); + let node_path = run_which("node"); let target = common::compile("./tests/eventfd.c", "broker_eventfd_rewriter", false, false); let control_socket_path = unique_test_socket_path("runner-broker-control"); let notification_socket_path = unique_test_socket_path("runner-broker-notification"); @@ -473,7 +481,7 @@ fn test_runner_broker_integration_with_rewriter() { litebox_broker_core::PolicyEngine::with_unauthenticated_rights( litebox_broker_core::PrincipalRights::all(), ), - 2, + 3, ); Runner::new(&true_path, "broker_true_rewriter") @@ -484,30 +492,19 @@ fn test_runner_broker_integration_with_rewriter() { Runner::new(&target, "broker_eventfd_rewriter") .broker_sockets(&control_socket_path, ¬ification_socket_path) .run(); - // eventfd.c creates eleven eventfd objects; each should release one broker object. - assert_eq!(broker_thread.next_close_object_count(), 11); - - broker_thread.join(); -} - -#[cfg(target_arch = "x86_64")] -#[test] -fn test_node_with_rewriter() { - const HELLO_WORLD_JS: &str = r" -const fs = require('node:fs'); - -const content = 'Hello World!'; -console.log(content); -"; + // eventfd.c creates thirteen eventfd objects; each should release one broker object. + assert_eq!(broker_thread.next_close_object_count(), 13); - let node_path = run_which("node"); - Runner::new(&node_path, "hello_node_rewriter") + Runner::new(&node_path, "hello_node_broker_rewriter") + .broker_sockets(&control_socket_path, ¬ification_socket_path) .arg("/out/hello_world.js") .with_fs_path(|out_dir| { - // write the test js file to the output directory std::fs::write(out_dir.join("out/hello_world.js"), HELLO_WORLD_JS).unwrap(); }) .run(); + assert!(broker_thread.next_close_object_count() > 0); + + broker_thread.join(); } #[cfg(target_arch = "x86_64")] diff --git a/litebox_runner_linux_userland/tests/sendfile.c b/litebox_runner_linux_userland/tests/sendfile.c index 5a6dc20dcd..deb541d095 100644 --- a/litebox_runner_linux_userland/tests/sendfile.c +++ b/litebox_runner_linux_userland/tests/sendfile.c @@ -5,7 +5,6 @@ #include "helpers.h" #include -#include #define SRC_PATH "/tmp/lb_sendfile_src" #define DST_PATH "/tmp/lb_sendfile_dst" @@ -340,13 +339,6 @@ static void test_pipe_in_fd(void) { close(pfd[1]); } -static void test_eventfd_in_fd(void) { - int efd = eventfd(7, 0); - if (efd < 0) die("eventfd"); - expect_einval_espipe_in_fd(efd, "eventfd in_fd"); - close(efd); -} - static void test_unix_stream_in_fd(void) { int sv[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) die("socketpair stream"); @@ -382,7 +374,6 @@ int main(void) { test_partial_nonblocking_pipe_error_is_deferred(); test_file_to_pipe_with_offset(); test_pipe_in_fd(); - test_eventfd_in_fd(); test_unix_stream_in_fd(); test_unix_dgram_in_fd(); diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index df1aad275c..5366683dff 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -617,7 +617,7 @@ mod test { use alloc::sync::Arc; use litebox::event::Events; use litebox::event::wait::WaitState; - use litebox_common_linux::{EfdFlags, EpollEvent}; + use litebox_common_linux::EpollEvent; use litebox_platform_multiplex::platform; use super::EpollFile; @@ -632,58 +632,6 @@ mod test { (task, epoll) } - #[test] - fn test_epoll_with_eventfd() { - let (task, epoll) = setup_epoll(); - let eventfd = task - .global - .create_linux_eventfd(0, EfdFlags::CLOEXEC) - .unwrap(); - let typed = task - .global - .litebox - .descriptor_table_mut() - .insert::(eventfd); - let files = Arc::new(FilesState::new(task.files.borrow().fs.clone())); - let Ok(raw_fd) = files.insert_raw_fd(typed) else { - unreachable!() - }; - let descriptor = super::EpollDescriptor::try_from(&files, raw_fd).unwrap(); - epoll - .add_interest( - &task.global, - 10, - &descriptor, - EpollEvent { - events: Events::IN.bits(), - data: 0, - }, - ) - .unwrap(); - - let writer = { - let global = task.global.clone(); - let files = Arc::clone(&files); - std::thread::spawn(move || { - let typed = files - .raw_descriptor_store - .read() - .fd_from_raw_integer::(raw_fd) - .unwrap(); - let _ = global - .litebox - .descriptor_table() - .with_entry(&typed, |entry| { - entry.write(&WaitState::new(platform()).context(), 1) - }); - }) - }; - epoll - .wait(&task.global, &WaitState::new(platform()).context(), 1024) - .unwrap(); - writer.join().unwrap(); - } - #[test] fn test_epoll_with_pipe() { let (task, epoll) = setup_epoll(); @@ -733,23 +681,14 @@ mod test { let task = crate::syscalls::tests::init_platform(None); let mut set = super::PollSet::with_capacity(0); - let eventfd = task - .global - .create_linux_eventfd(0, EfdFlags::empty()) - .unwrap(); - - let typed = task - .global - .litebox - .descriptor_table_mut() - .insert::(eventfd); + let (rfd_u, wfd_u) = task + .sys_pipe2(litebox::fs::OFlags::empty()) + .expect("pipe2 failed"); + let rfd = i32::try_from(rfd_u).unwrap(); + let wfd = i32::try_from(wfd_u).unwrap(); let no_fds = FilesState::new(task.files.borrow().fs.clone()); - let fds = Arc::new(FilesState::new(task.files.borrow().fs.clone())); - let Ok(raw_fd) = fds.insert_raw_fd(typed) else { - unreachable!() - }; - let fd = i32::try_from(raw_fd).unwrap(); - set.add_fd(fd, Events::IN); + let fds = task.files.borrow().clone(); + set.add_fd(rfd, Events::IN); let revents = |set: &super::PollSet| { let revents: std::vec::Vec<_> = set.revents().collect(); @@ -761,36 +700,14 @@ mod test { .unwrap(); assert_eq!(revents(&set), Events::NVAL); - { - let typed = fds - .raw_descriptor_store - .read() - .fd_from_raw_integer::(raw_fd) - .unwrap(); - task.global - .litebox - .descriptor_table() - .with_entry(&typed, |entry| { - entry.write(&WaitState::new(platform()).context(), 1) - }); - } + task.sys_write(wfd, &[1], None).unwrap(); set.wait(&task.global, &WaitState::new(platform()).context(), &fds) .unwrap(); assert_eq!(revents(&set), Events::IN); - { - let typed = fds - .raw_descriptor_store - .read() - .fd_from_raw_integer::(raw_fd) - .unwrap(); - task.global - .litebox - .descriptor_table() - .with_entry(&typed, |entry| { - entry.read(&WaitState::new(platform()).context()) - }); - } + let mut buf = [0; 1]; + assert_eq!(task.sys_read(rfd, &mut buf, None).unwrap(), 1); + assert_eq!(buf, [1]); set.wait( &task.global, &WaitState::new(platform()) @@ -801,27 +718,17 @@ mod test { .unwrap_err(); assert!(revents(&set).is_empty()); - // spawn a thread to write to the eventfd - let global = task.global.clone(); - let fds_for_thread = Arc::clone(&fds); - std::thread::spawn(move || { - let typed = fds_for_thread - .raw_descriptor_store - .read() - .fd_from_raw_integer::(raw_fd) - .unwrap(); - let handle = global - .litebox - .descriptor_table() - .entry_handle(&typed) - .unwrap(); - let _ = - handle.with_entry(|entry| entry.write(&WaitState::new(platform()).context(), 1)); + task.spawn_clone_for_test(move |task| { + std::thread::sleep(core::time::Duration::from_millis(100)); + assert_eq!(task.sys_write(wfd, &[1], None).unwrap(), 1); }); set.wait(&task.global, &WaitState::new(platform()).context(), &fds) .unwrap(); assert_eq!(revents(&set), Events::IN); + + let _ = task.sys_close(rfd); + let _ = task.sys_close(wfd); } #[test] diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 080ff541a3..1852c67591 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -8,15 +8,14 @@ use core::sync::atomic::AtomicU32; use litebox::{ event::{ Events, IOPollable, - counter::{EventCounter, EventCounterError, EventCounterReadMode}, + counter::{EventCounter, EventCounterReadMode}, observer::Observer, - polling::{Pollee, TryOpError}, wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, fs::OFlags, platform::TimeProvider, - sync::{Mutex, RawSyncPrimitivesProvider}, + sync::RawSyncPrimitivesProvider, }; use litebox_common_linux::{EfdFlags, errno::Errno}; @@ -28,140 +27,15 @@ impl FdEnabledSubsystem for EventfdSubsystem { } impl FdEnabledSubsystemEntry for EventFile {} -/// Backing counter for a Linux eventfd file description. -/// -enum EventFileCounter { - ShimLocal { - count: Mutex, - pollee: Pollee, - }, - LocalCore(EventCounter), -} - pub(crate) struct EventFile { - counter: EventFileCounter, + counter: EventCounter, /// File status flags (see [`OFlags::STATUS_FLAGS_MASK`]) status: AtomicU32, semaphore: bool, } -impl EventFileCounter { - fn shim_local(count: u64) -> Self { - Self::ShimLocal { - count: Mutex::new(count), - pollee: Pollee::new(), - } - } - - fn read( - &self, - cx: &WaitContext<'_, Platform>, - nonblock: bool, - semaphore: bool, - ) -> Result { - match self { - Self::ShimLocal { count, pollee } => pollee - .wait(cx, nonblock, Events::IN, || { - Self::try_read_local(count, pollee, semaphore) - }) - .map_err(Errno::from), - Self::LocalCore(counter) => counter - .read( - cx, - nonblock, - if semaphore { - EventCounterReadMode::One - } else { - EventCounterReadMode::All - }, - ) - .map_err(Errno::from), - } - } - - fn write( - &self, - cx: &WaitContext<'_, Platform>, - nonblock: bool, - value: u64, - ) -> Result { - match self { - Self::ShimLocal { count, pollee } => pollee - .wait(cx, nonblock, Events::OUT, || { - Self::try_write_local(count, pollee, value) - }) - .map_err(Errno::from), - Self::LocalCore(counter) => counter.write(cx, nonblock, value).map_err(Errno::from), - } - } - - fn try_read_local( - count: &Mutex, - pollee: &Pollee, - semaphore: bool, - ) -> Result> { - let mut count = count.lock(); - if *count == 0 { - return Err(TryOpError::TryAgain); - } - - let res = if semaphore { 1 } else { *count }; - *count -= res; - - drop(count); - pollee.notify_observers(Events::OUT); - Ok(res) - } - - fn try_write_local( - count: &Mutex, - pollee: &Pollee, - value: u64, - ) -> Result> { - if value == u64::MAX { - return Err(TryOpError::Other(Errno::EINVAL)); - } - - let mut count = count.lock(); - if let Some(new_value) = (*count).checked_add(value) - && new_value != u64::MAX - { - *count = new_value; - drop(count); - pollee.notify_observers(Events::IN); - return Ok(core::mem::size_of::()); - } - - Err(TryOpError::TryAgain) - } - - fn check_io_events(&self) -> Events { - match self { - Self::ShimLocal { count, .. } => { - let count = count.lock(); - let mut events = Events::empty(); - if *count != 0 { - events |= Events::IN; - } - if *count < u64::MAX - 1 { - events |= Events::OUT; - } - events - } - Self::LocalCore(counter) => counter.check_io_events(), - } - } - - fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { - match self { - Self::ShimLocal { pollee, .. } => pollee.register_observer(observer, mask), - Self::LocalCore(counter) => counter.register_observer(observer, mask), - } - } -} - impl EventFile { - fn new(counter: EventFileCounter, flags: EfdFlags) -> Self { + fn new(counter: EventCounter, flags: EfdFlags) -> Self { let mut status = OFlags::RDWR; status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); Self { @@ -172,11 +46,23 @@ impl EventFile { } pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { - self.counter.read(cx, self.is_nonblocking(), self.semaphore) + self.counter + .read( + cx, + self.is_nonblocking(), + if self.semaphore { + EventCounterReadMode::One + } else { + EventCounterReadMode::All + }, + ) + .map_err(Errno::from) } pub(crate) fn write(&self, cx: &WaitContext<'_, Platform>, value: u64) -> Result { - self.counter.write(cx, self.is_nonblocking(), value) + self.counter + .write(cx, self.is_nonblocking(), value) + .map_err(Errno::from) } super::common_functions_for_file_status!(); @@ -209,119 +95,22 @@ impl GlobalState { } let count = u64::from(initval); - let counter = match EventCounter::new(&self.litebox, count) { - Ok(counter) => EventFileCounter::LocalCore(counter), - Err(EventCounterError::Unavailable) => EventFileCounter::shim_local(count), - Err(error) => return Err(error.into()), - }; + let counter = EventCounter::new(&self.litebox, count).map_err(Errno::from)?; Ok(EventFile::new(counter, flags)) } } #[cfg(test)] mod tests { - use litebox::event::wait::WaitState; use litebox_common_linux::{EfdFlags, errno::Errno}; - use litebox_platform_multiplex::platform; - - extern crate std; #[test] - fn test_semaphore_eventfd() { - let _task = crate::syscalls::tests::init_platform(None); - - let eventfd = alloc::sync::Arc::new(super::EventFile::new( - super::EventFileCounter::shim_local(0), - EfdFlags::SEMAPHORE, - )); - let total = 8; - let handles: std::vec::Vec<_> = (0..total) - .map(|_| { - let copied_eventfd = eventfd.clone(); - std::thread::spawn(move || { - copied_eventfd - .read(&WaitState::new(platform()).context()) - .unwrap(); - }) - }) - .collect(); - - std::thread::sleep(core::time::Duration::from_millis(500)); - eventfd - .write(&WaitState::new(platform()).context(), total) - .unwrap(); - for handle in handles { - handle.join().unwrap(); - } - } - - #[test] - fn test_blocking_eventfd() { - let _task = crate::syscalls::tests::init_platform(None); - - let eventfd = alloc::sync::Arc::new(super::EventFile::new( - super::EventFileCounter::shim_local(0), - EfdFlags::empty(), - )); - let copied_eventfd = eventfd.clone(); - std::thread::spawn(move || { - copied_eventfd - .write(&WaitState::new(platform()).context(), 1) - .unwrap(); - // block until the first read finishes - copied_eventfd - .write(&WaitState::new(platform()).context(), u64::MAX - 1) - .unwrap(); - }); - - // block until the first write - let ret = eventfd.read(&WaitState::new(platform()).context()).unwrap(); - assert_eq!(ret, 1); - - // block until the second write - let ret = eventfd.read(&WaitState::new(platform()).context()).unwrap(); - assert_eq!(ret, u64::MAX - 1); - } - - #[test] - fn test_blocking_eventfd_no_race_on_massive_readwrite() { - let _task = crate::syscalls::tests::init_platform(None); - - let eventfd = alloc::sync::Arc::new(super::EventFile::new( - super::EventFileCounter::shim_local(0), - EfdFlags::empty(), - )); - let copied_eventfd = eventfd.clone(); - std::thread::spawn(move || { - for _ in 0..10000 { - copied_eventfd - .write(&WaitState::new(platform()).context(), u64::MAX - 1) - .unwrap(); - } - }); - - for _ in 0..10000 { - let ret = eventfd.read(&WaitState::new(platform()).context()).unwrap(); - assert_eq!(ret, u64::MAX - 1); - } - } - - #[test] - fn test_nonblocking_eventfd_uses_shim_local_without_broker_control() { + fn test_eventfd_requires_broker_control() { let task = crate::syscalls::tests::init_platform(None); - let eventfd = task - .global - .create_linux_eventfd(0, EfdFlags::NONBLOCK) - .unwrap(); - assert_eq!( - eventfd.read(&WaitState::new(platform()).context()), - Err(Errno::EAGAIN) - ); - assert_eq!( - eventfd.write(&WaitState::new(platform()).context(), 1), - Ok(8) - ); - assert_eq!(eventfd.read(&WaitState::new(platform()).context()), Ok(1)); + assert!(matches!( + task.global.create_linux_eventfd(0, EfdFlags::NONBLOCK), + Err(Errno::EIO) + )); } } diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 661e9dfd41..bacf66cf0a 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -86,15 +86,14 @@ fn test_fcntl() { let write_fd = i32::try_from(write_fd).unwrap(); check(write_fd, OFlags::WRONLY | OFlags::NONBLOCK, OFlags::WRONLY); - // Test eventfd - let eventfd = task - .sys_eventfd2( + // Eventfd requires broker control in this shim configuration. + assert_eq!( + task.sys_eventfd2( 0, EfdFlags::CLOEXEC | EfdFlags::SEMAPHORE | EfdFlags::NONBLOCK, - ) - .expect("Failed to create eventfd"); - let eventfd = i32::try_from(eventfd).unwrap(); - check(eventfd, OFlags::RDWR | OFlags::NONBLOCK, OFlags::RDWR); + ), + Err(Errno::EIO) + ); // Test fcntl with DUPFD let fd = task From 33d95bd82a3b396473d3d7fff8376f6e5ddf8b2a Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Mon, 6 Jul 2026 10:58:02 -0700 Subject: [PATCH 088/319] Set Windows process TLS (#1004) Fix `NtSetInformationProcess` to support setting TLS. For now, the Windows shim does not support multi-threading, so the implementation is not complete yet. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 19 +- litebox_shim_windows/src/loader/pe.rs | 2 +- litebox_shim_windows/src/syscalls/mod.rs | 2 +- litebox_shim_windows/src/syscalls/process.rs | 489 +++++++++++++++++-- 4 files changed, 470 insertions(+), 42 deletions(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index a9a8d92476..a5573e77cf 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -153,17 +153,26 @@ where ptr.write_at_offset(0, value) } -fn write_field_at_offset( - base: MutPtr, +fn read_field_at_offset(base: usize, field_offset: usize) -> Option +where + Platform: RawPointerProvider, + Field: zerocopy::FromBytes, +{ + let address = base.checked_add(field_offset)?; + let ptr = ConstPtr::::from_usize(address); + ptr.read_at_offset(0) +} + +fn write_field_at_offset( + base: usize, field_offset: usize, value: Field, ) -> Option<()> where Platform: RawPointerProvider, - Struct: zerocopy::FromBytes + zerocopy::IntoBytes, Field: zerocopy::FromBytes + zerocopy::IntoBytes, { - let address = base.as_usize().checked_add(field_offset)?; + let address = base.checked_add(field_offset)?; let ptr = MutPtr::::from_usize(address); ptr.write_at_offset(0, value) } @@ -1141,7 +1150,7 @@ impl Task { process_information, process_information_length, } => { - let status = Self::sys_nt_set_information_process( + let status = self.sys_nt_set_information_process( process_handle, process_information_class, process_information, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 4ba683cf64..a87d74b033 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -1115,7 +1115,7 @@ where Struct: FromBytes + IntoBytes, Field: FromBytes + IntoBytes, { - crate::write_field_at_offset::(base, field_offset, value) + crate::write_field_at_offset::(base.as_usize(), field_offset, value) .ok_or(PeImageAccessError::MemoryAccess) } diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 1e3bda7d5f..f401a1690d 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -389,7 +389,7 @@ pub(crate) enum SyscallRequest { NtSetInformationProcess { process_handle: ProcessHandle, process_information_class: u32, - process_information: Platform::RawConstPointer, + process_information: Platform::RawMutPointer, process_information_length: u32, }, NtSetInformationThread { diff --git a/litebox_shim_windows/src/syscalls/process.rs b/litebox_shim_windows/src/syscalls/process.rs index 0f4c39509b..2ac2fd3a4e 100644 --- a/litebox_shim_windows/src/syscalls/process.rs +++ b/litebox_shim_windows/src/syscalls/process.rs @@ -1,13 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use core::mem::{offset_of, size_of}; use core::sync::atomic::Ordering; use int_enum::IntEnum; -use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; +use crate::nt_types::ThreadEnvironmentBlock; use crate::syscalls::ProcessHandle; use crate::{ConstPtr, MutPtr, ShimFS, ShimPlatform, Task}; @@ -19,6 +21,7 @@ const GUEST_PARENT_PROCESS_ID: usize = 0; const GUEST_PROCESS_AFFINITY_MASK: usize = 1; const PROCESS_DEBUG_FLAGS_NO_DEBUGGER: u32 = 1; const PROCESS_COOKIE: u32 = 0xdead_beef; +const TEB_TLS_SLOT_COUNT: usize = 64; #[repr(u32)] #[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] @@ -35,6 +38,20 @@ enum ProcessInformationClass { SchedulerSharedData = 112, } +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +enum ProcessTlsOperation { + ReplaceIndex = 0, + ReplaceVector = 1, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct ProcessTlsThreadDataFlags: u32 { + const OLD_DATA_WRITTEN = 0x2; + } +} + #[repr(C)] #[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] struct ProcessBasicInformation { @@ -60,6 +77,174 @@ struct ProcessSchedulerSharedDataSlotInformation { scheduler_shared_data_handle: usize, } +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessTlsInformationHeader { + flags: u32, + operation_type: u32, + thread_data_count: u32, + tls_index: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessTlsInformationExtendedHeader { + header: ProcessTlsInformationHeader, + _reserved: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessTlsThreadDataSimple { + flags: u32, + _padding0: u32, + tls_data: usize, + _reserved: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct ProcessTlsThreadDataExtended { + flags: u32, + _padding0: u32, + new_tls_data: usize, + old_tls_data: usize, + _reserved: usize, +} + +enum ProcessTlsThreadData { + Simple(MutPtr), + Extended(MutPtr), +} + +impl ProcessTlsThreadData { + fn read_tls_data(&self) -> Option { + match self { + Self::Simple(ptr) => crate::read_field_at_offset::( + ptr.as_usize(), + offset_of!(ProcessTlsThreadDataSimple, tls_data), + ), + Self::Extended(ptr) => crate::read_field_at_offset::( + ptr.as_usize(), + offset_of!(ProcessTlsThreadDataExtended, new_tls_data), + ), + } + } + + fn write_tls_data(&self, value: usize) -> Option<()> { + match self { + Self::Simple(ptr) => crate::write_field_at_offset::( + ptr.as_usize(), + offset_of!(ProcessTlsThreadDataSimple, tls_data), + value, + ), + Self::Extended(ptr) => crate::write_field_at_offset::( + ptr.as_usize(), + offset_of!(ProcessTlsThreadDataExtended, old_tls_data), + value, + ), + } + } + + fn write_flags(&self, flags: ProcessTlsThreadDataFlags) -> Option<()> { + match self { + Self::Simple(ptr) => crate::write_field_at_offset::( + ptr.as_usize(), + offset_of!(ProcessTlsThreadDataSimple, flags), + flags.bits(), + ), + Self::Extended(ptr) => crate::write_field_at_offset::( + ptr.as_usize(), + offset_of!(ProcessTlsThreadDataExtended, flags), + flags.bits(), + ), + } + } +} + +#[derive(Clone, Copy, Debug)] +enum ProcessTlsLayout { + Simple, + Extended, +} + +impl ProcessTlsLayout { + fn detect(thread_data_count: u32, process_information_length: u32) -> Result { + let count = thread_data_count as usize; + let extended_len = size_of::() + .checked_add( + count + .checked_mul(size_of::()) + .ok_or(NtStatus::INFO_LENGTH_MISMATCH)?, + ) + .ok_or(NtStatus::INFO_LENGTH_MISMATCH)?; + let simple_len = size_of::() + .checked_add( + count + .checked_mul(size_of::()) + .ok_or(NtStatus::INFO_LENGTH_MISMATCH)?, + ) + .ok_or(NtStatus::INFO_LENGTH_MISMATCH)?; + let provided_len = process_information_length as usize; + + if provided_len >= extended_len { + Ok(Self::Extended) + } else if provided_len >= simple_len { + Ok(Self::Simple) + } else { + Err(NtStatus::INFO_LENGTH_MISMATCH) + } + } + + const fn header_size(self) -> usize { + match self { + Self::Simple => size_of::(), + Self::Extended => size_of::(), + } + } + + const fn entry_size(self) -> usize { + match self { + Self::Simple => size_of::(), + Self::Extended => size_of::(), + } + } + + const fn old_data_offset(self) -> usize { + match self { + Self::Simple => offset_of!(ProcessTlsThreadDataSimple, tls_data), + Self::Extended => offset_of!(ProcessTlsThreadDataExtended, old_tls_data), + } + } + + fn thread_data( + self, + base: MutPtr, + index: usize, + ) -> Option> { + match self { + Self::Simple => { + let arr = MutPtr::::from_usize( + base.as_usize().checked_add( + size_of::() + + index.checked_mul(size_of::())?, + )?, + ); + Some(ProcessTlsThreadData::Simple(arr)) + } + Self::Extended => { + let arr = MutPtr::::from_usize( + base.as_usize().checked_add( + size_of::() + + index.checked_mul(size_of::())?, + )?, + ); + Some(ProcessTlsThreadData::Extended(arr)) + } + } + } +} + impl Task { pub(crate) fn sys_nt_query_information_process( &self, @@ -145,9 +330,10 @@ impl Task { } pub(crate) fn sys_nt_set_information_process( + &self, process_handle: ProcessHandle, process_information_class: u32, - process_information: ConstPtr, + process_information: MutPtr, process_information_length: u32, ) -> NtStatus { let Ok(process_information_class) = @@ -168,6 +354,11 @@ impl Task { process_information_length, ) } + ProcessInformationClass::TlsInformation => self.write_process_tls_information( + process_handle, + process_information, + process_information_length, + ), // TODO: implement additional settable process information classes when a guest // exercises them. ProcessInformationClass::BasicInformation @@ -175,7 +366,6 @@ impl Task { | ProcessInformationClass::DefaultHardErrorMode | ProcessInformationClass::Wow64Information | ProcessInformationClass::DebugFlags - | ProcessInformationClass::TlsInformation | ProcessInformationClass::Cookie | ProcessInformationClass::ConsoleHostProcess | ProcessInformationClass::ImageInformation => { @@ -225,7 +415,7 @@ impl Task { fn set_process_scheduler_shared_data( process_handle: ProcessHandle, - process_information: ConstPtr, + process_information: MutPtr, process_information_length: u32, ) -> NtStatus { if process_information_length @@ -250,6 +440,238 @@ impl Task { NtStatus::SUCCESS } + fn write_process_tls_information( + &self, + process_handle: ProcessHandle, + process_information: MutPtr, + process_information_length: u32, + ) -> NtStatus { + if (process_information_length as usize) < size_of::() { + return NtStatus::INFO_LENGTH_MISMATCH; + } + if !process_handle.is_current() { + return NtStatus::INVALID_HANDLE; + } + + let Some(header) = ConstPtr::::from_usize( + process_information.as_usize(), + ) + .read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let layout = + match ProcessTlsLayout::detect(header.thread_data_count, process_information_length) { + Ok(layout) => layout, + Err(status) => return status, + }; + + litebox_util_log::debug!( + operation_type = header.operation_type, + thread_data_count = header.thread_data_count, + tls_index = header.tls_index, + process_information_length, + layout_header_size = layout.header_size(), + layout_entry_size = layout.entry_size(), + layout_old_data_offset = layout.old_data_offset(); + "Handling ProcessTlsInformation" + ); + + if header.thread_data_count > 1 { + // TODO(multi-thread-tls): PROCESS_TLS_INFORMATION entries are positional per-thread + // data. The shim currently models only the active TEB, so handling multiple entries + // would corrupt prior TLS values by repeatedly writing one TEB's vector. + return NtStatus::NOT_SUPPORTED; + } + + match ProcessTlsOperation::try_from(header.operation_type) { + Ok(ProcessTlsOperation::ReplaceVector) => { + self.replace_tls_vector(process_information, header, layout) + } + Ok(ProcessTlsOperation::ReplaceIndex) => { + self.replace_tls_index(process_information, header, layout) + } + Err(_) => { + litebox_util_log::debug!( + operation_type = header.operation_type, + thread_data_count = header.thread_data_count, + tls_index = header.tls_index; + "Unsupported ProcessTlsInformation operation" + ); + NtStatus::INVALID_INFO_CLASS + } + } + } + + fn replace_tls_vector( + &self, + process_information: MutPtr, + header: ProcessTlsInformationHeader, + layout: ProcessTlsLayout, + ) -> NtStatus { + for index in 0..header.thread_data_count as usize { + let Some(thread_data) = layout.thread_data::(process_information, index) + else { + return NtStatus::INFO_LENGTH_MISMATCH; + }; + let Some(new_tls_data) = thread_data.read_tls_data() else { + return NtStatus::ACCESS_VIOLATION; + }; + // TODO(multi-thread-tls): read ith thread's tls + let teb = MutPtr::::from_usize(self.teb_address); + let Some(old_tls_data) = Self::read_teb_tls_pointer(teb) else { + return NtStatus::ACCESS_VIOLATION; + }; + + litebox_util_log::debug!( + thread_data_index = index, + new_tls_data:% = format_args!("{new_tls_data:#x}"), + old_tls_data:% = format_args!("{old_tls_data:#x}"); + "Replacing process TLS vector" + ); + + if let Err(status) = self.copy_initial_tls_slots(old_tls_data, new_tls_data) { + return status; + } + let old_tls_data_for_guest = self.guest_visible_old_tls_vector(old_tls_data); + + if thread_data.write_tls_data(old_tls_data_for_guest).is_none() + || Self::write_teb_tls_pointer(teb, new_tls_data).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if old_tls_data_for_guest == 0 + && thread_data + .write_flags(ProcessTlsThreadDataFlags::OLD_DATA_WRITTEN) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + } + + NtStatus::SUCCESS + } + + fn replace_tls_index( + &self, + process_information: MutPtr, + header: ProcessTlsInformationHeader, + layout: ProcessTlsLayout, + ) -> NtStatus { + let tls_index = header.tls_index as usize; + if tls_index >= TEB_TLS_SLOT_COUNT { + return NtStatus::INVALID_PARAMETER; + } + + for index in 0..header.thread_data_count as usize { + let Some(thread_data) = layout.thread_data::(process_information, index) + else { + return NtStatus::INFO_LENGTH_MISMATCH; + }; + let Some(new_tls_data) = thread_data.read_tls_data() else { + return NtStatus::ACCESS_VIOLATION; + }; + // TODO(multi-thread-tls): read ith thread's tls + let teb = ConstPtr::::from_usize(self.teb_address); + let Some(tls_array) = Self::read_teb_tls_pointer(teb) else { + return NtStatus::ACCESS_VIOLATION; + }; + if tls_array == 0 { + continue; + } + let tls_slots = MutPtr::::from_usize(tls_array); + let Some(old_tls_data) = tls_slots.read_at_offset(tls_index.cast_signed()) else { + return NtStatus::ACCESS_VIOLATION; + }; + + litebox_util_log::debug!( + thread_data_index = index, + tls_index, + tls_array:% = format_args!("{tls_array:#x}"), + new_tls_data:% = format_args!("{new_tls_data:#x}"), + old_tls_data:% = format_args!("{old_tls_data:#x}"); + "Replacing process TLS index" + ); + + if thread_data.write_tls_data(old_tls_data).is_none() + || tls_slots + .write_at_offset(tls_index.cast_signed(), new_tls_data) + .is_none() + || thread_data + .write_flags(ProcessTlsThreadDataFlags::OLD_DATA_WRITTEN) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + } + + NtStatus::SUCCESS + } + + fn guest_visible_old_tls_vector(&self, old_tls_data: usize) -> usize { + if old_tls_data > 0x10010 + && old_tls_data >= self.teb_address + && old_tls_data + < self + .teb_address + .saturating_add(size_of::()) + { + // The loader frees the returned old vector. The initial vector lives inside + // TEB.tls_slots, so report no heap-backed vector instead of exposing TEB memory. + 0 + } else { + old_tls_data + } + } + + fn copy_initial_tls_slots( + &self, + old_tls_data: usize, + new_tls_data: usize, + ) -> Result<(), NtStatus> { + if old_tls_data <= 0x10010 || new_tls_data <= 0x10010 { + return Ok(()); + } + let initial_tls_slots = self + .teb_address + .checked_add(offset_of!(ThreadEnvironmentBlock, tls_slots)) + .ok_or(NtStatus::INVALID_PARAMETER)?; + if old_tls_data != initial_tls_slots { + return Ok(()); + } + let old_tls_slots = ConstPtr::::from_usize(old_tls_data); + let new_tls_slots = MutPtr::::from_usize(new_tls_data); + for index in 0..TEB_TLS_SLOT_COUNT.cast_signed() { + let slot_value = old_tls_slots + .read_at_offset(index) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + new_tls_slots + .write_at_offset(index, slot_value) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + } + + Ok(()) + } + + fn read_teb_tls_pointer>( + teb: Ptr, + ) -> Option { + crate::read_field_at_offset::( + teb.as_usize(), + offset_of!(ThreadEnvironmentBlock, thread_local_storage_pointer), + ) + } + + fn write_teb_tls_pointer( + teb: MutPtr, + value: usize, + ) -> Option<()> { + crate::write_field_at_offset::( + teb.as_usize(), + offset_of!(ThreadEnvironmentBlock, thread_local_storage_pointer), + value, + ) + } + fn process_basic_information(&self) -> ProcessBasicInformation { ProcessBasicInformation { exit_status: ACTIVE_PROCESS_EXIT_STATUS, @@ -272,23 +694,18 @@ pub(crate) const fn default_process_cookie() -> u32 { #[cfg(test)] mod tests { use super::*; - use crate::tests::{mut_byte_ptr, mut_ptr, null_const_ptr, null_mut_ptr}; + use crate::tests::{mut_byte_ptr, mut_ptr, null_mut_ptr}; use litebox::platform::ThreadProvider; const RETURN_LENGTH_SENTINEL: u32 = 0xaaaa_aaaa; type TestPlatform = crate::tests::TestPlatform; - type TestTask = Task; fn run_with_test_platform_pointers(f: impl FnOnce() -> R) -> R { let _ = crate::tests::test_platform(); ::run_test_thread(f) } - fn const_byte_ptr(value: &T) -> ConstPtr { - ConstPtr::::from_usize(core::ptr::from_ref(value).cast::() as usize) - } - #[test] fn nt_query_information_process_validates_arguments() { run_with_test_platform_pointers(|| { @@ -366,7 +783,8 @@ mod tests { #[test] fn nt_set_information_process_scheduler_shared_data_validates_arguments() { run_with_test_platform_pointers(|| { - let information = ProcessSchedulerSharedDataSlotInformation { + let task = crate::tests::test_task(); + let mut information = ProcessSchedulerSharedDataSlotInformation { scheduler_shared_data_handle: 0, }; let information_len: u32 = @@ -374,50 +792,50 @@ mod tests { let bad_handle = ProcessHandle::from_raw(0x1234); assert_eq!( - TestTask::sys_nt_set_information_process( + task.sys_nt_set_information_process( bad_handle, ProcessInformationClass::SchedulerSharedData as u32, - null_const_ptr::(), + null_mut_ptr::(), information_len - 1, ), NtStatus::INFO_LENGTH_MISMATCH ); assert_eq!( - TestTask::sys_nt_set_information_process( + task.sys_nt_set_information_process( bad_handle, 0xffff, - const_byte_ptr(&information), + mut_byte_ptr(&mut information), information_len - 1, ), NtStatus::INVALID_INFO_CLASS ); assert_eq!( - TestTask::sys_nt_set_information_process( + task.sys_nt_set_information_process( bad_handle, ProcessInformationClass::SchedulerSharedData as u32, - null_const_ptr::(), + null_mut_ptr::(), information_len, ), NtStatus::INVALID_HANDLE ); assert_eq!( - TestTask::sys_nt_set_information_process( + task.sys_nt_set_information_process( ProcessHandle::CURRENT, ProcessInformationClass::SchedulerSharedData as u32, - null_const_ptr::(), + null_mut_ptr::(), information_len, ), NtStatus::ACCESS_VIOLATION ); assert_eq!( - TestTask::sys_nt_set_information_process( + task.sys_nt_set_information_process( ProcessHandle::CURRENT, ProcessInformationClass::SchedulerSharedData as u32, - const_byte_ptr(&information), + mut_byte_ptr(&mut information), information_len, ), NtStatus::SUCCESS @@ -562,10 +980,10 @@ mod tests { #[test] fn nt_set_information_process_scheduler_shared_data_matches_host_statuses() { run_with_test_platform_pointers(|| { - let null_information = ProcessSchedulerSharedDataSlotInformation { + let mut null_information = ProcessSchedulerSharedDataSlotInformation { scheduler_shared_data_handle: 0, }; - let bogus_information = ProcessSchedulerSharedDataSlotInformation { + let mut bogus_information = ProcessSchedulerSharedDataSlotInformation { scheduler_shared_data_handle: 0x1234, }; let information_len: u32 = @@ -598,7 +1016,7 @@ mod tests { ProcessHandle::CURRENT, scheduler_class, core::ptr::from_ref(&null_information).cast::(), - const_byte_ptr(&null_information), + mut_byte_ptr(&mut null_information), information_len, ), ( @@ -606,7 +1024,7 @@ mod tests { ProcessHandle::CURRENT, scheduler_class, core::ptr::from_ref(&bogus_information).cast::(), - const_byte_ptr(&bogus_information), + mut_byte_ptr(&mut bogus_information), information_len, ), ( @@ -614,7 +1032,7 @@ mod tests { ProcessHandle::CURRENT, scheduler_class, core::ptr::from_ref(&null_information).cast::(), - const_byte_ptr(&null_information), + mut_byte_ptr(&mut null_information), information_len - 1, ), ( @@ -622,7 +1040,7 @@ mod tests { ProcessHandle::CURRENT, scheduler_class, core::ptr::null(), - null_const_ptr::(), + null_mut_ptr::(), information_len, ), ( @@ -630,7 +1048,7 @@ mod tests { ProcessHandle::CURRENT, bad_class, core::ptr::from_ref(&null_information).cast::(), - const_byte_ptr(&null_information), + mut_byte_ptr(&mut null_information), information_len, ), ( @@ -638,7 +1056,7 @@ mod tests { ProcessHandle::from_raw(0x1234), scheduler_class, core::ptr::from_ref(&null_information).cast::(), - const_byte_ptr(&null_information), + mut_byte_ptr(&mut null_information), information_len, ), ( @@ -646,7 +1064,7 @@ mod tests { ProcessHandle::from_raw(0x1234), scheduler_class, core::ptr::null(), - null_const_ptr::(), + null_mut_ptr::(), information_len - 1, ), ( @@ -654,7 +1072,7 @@ mod tests { ProcessHandle::from_raw(0x1234), bad_class, core::ptr::from_ref(&null_information).cast::(), - const_byte_ptr(&null_information), + mut_byte_ptr(&mut null_information), information_len - 1, ), ( @@ -662,7 +1080,7 @@ mod tests { ProcessHandle::from_raw(0x1234), scheduler_class, core::ptr::null(), - null_const_ptr::(), + null_mut_ptr::(), information_len, ), ( @@ -670,7 +1088,7 @@ mod tests { ProcessHandle::CURRENT, scheduler_class, core::ptr::null(), - null_const_ptr::(), + null_mut_ptr::(), information_len - 1, ), ( @@ -678,7 +1096,7 @@ mod tests { ProcessHandle::CURRENT, bad_class, core::ptr::from_ref(&null_information).cast::(), - const_byte_ptr(&null_information), + mut_byte_ptr(&mut null_information), information_len - 1, ), ] { @@ -688,7 +1106,8 @@ mod tests { host_process_information, process_information_length, ); - let shim = TestTask::sys_nt_set_information_process( + let task = crate::tests::test_task(); + let shim = task.sys_nt_set_information_process( shim_process_handle, process_information_class, shim_process_information, From 0c96aea10b56fc53e81c15c8a57eeab62ad1c053 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Mon, 6 Jul 2026 16:25:01 -0700 Subject: [PATCH 089/319] Add `NtApphelpCacheControl` handling for the Windows shim (#1008) This PR adds minimal Windows AppHelp cache-control support. Note that this is a cacheless AppHelp implementation with some TODO left. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_common_windows/src/nt_status.rs | 4 + litebox_shim_windows/src/lib.rs | 10 ++ litebox_shim_windows/src/nt_types.rs | 22 ++++ litebox_shim_windows/src/syscalls/apphelp.rs | 121 +++++++++++++++++++ litebox_shim_windows/src/syscalls/mod.rs | 9 ++ 5 files changed, 166 insertions(+) create mode 100644 litebox_shim_windows/src/syscalls/apphelp.rs diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index bfeb2249ee..8aa92d815c 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -204,6 +204,7 @@ impl NtStatus { 0xC0000184 => "STATUS_INVALID_DEVICE_STATE: Invalid device state", 0xC0000201 => "STATUS_NETWORK_OPEN_RESTRICTION: Network open restriction", 0xC0000202 => "STATUS_NO_USER_SESSION_KEY: No user session key", + 0xC0000225 => "STATUS_NOT_FOUND: Not found", 0xC000022D => "STATUS_RETRY: The operation should be retried", 0xC00002DF => "STATUS_SAM_NEED_BOOTKEY_PASSWORD: SAM needs boot key password", 0xC00002E0 => "STATUS_SAM_NEED_BOOTKEY_FLOPPY: SAM needs boot key floppy", @@ -581,6 +582,9 @@ impl NtStatus { /// STATUS_NO_USER_SESSION_KEY pub const NO_USER_SESSION_KEY: Self = Self::from_raw(0xC0000202); + /// STATUS_NOT_FOUND + pub const NOT_FOUND: Self = Self::from_raw(0xC0000225); + /// STATUS_RETRY pub const RETRY: Self = Self::from_raw(0xC000022D); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index a5573e77cf..01ac981d4e 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -986,6 +986,16 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtApphelpCacheControl { + service_class, + service_data, + } => { + let status = syscalls::apphelp::sys_nt_apphelp_cache_control::( + service_class, + service_data, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtOpenKey { key_handle, desired_access, diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index 9438e3b9a3..bf6ffa713d 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -281,6 +281,28 @@ impl UnicodeString { } } +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub(crate) struct AhcServiceLookupCdb { + pub(crate) name: UnicodeString, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] +pub(crate) struct AhcServiceData { + // TODO(ahc-service-data): model the full Win11 AHC_SERVICE_DATA sub-structs + // once their live boundaries are probed; phnt's ntmisc.h layout diverges + // from the observed guest layout before the verified fields below. + pub(crate) reserved_0: [u8; 0xf8], + pub(crate) lookup_cdb: AhcServiceLookupCdb, + pub(crate) reserved_1: [u8; 0x68], + pub(crate) driver_status: i32, + pub(crate) reserved_2: [u8; 4], + pub(crate) params_out: usize, + pub(crate) params_out_size: u32, + pub(crate) reserved_3: [u8; 4], +} + bitflags::bitflags! { /// Packed process flags stored in `PEB.BitField`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/litebox_shim_windows/src/syscalls/apphelp.rs b/litebox_shim_windows/src/syscalls/apphelp.rs new file mode 100644 index 0000000000..6711325113 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/apphelp.rs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::mem::offset_of; +use int_enum::IntEnum; + +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::utils::TruncateExt as _; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::AhcServiceData; +use crate::{MutPtr, ShimPlatform}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] +#[repr(u32)] +pub enum AhcServiceClass { + Lookup = 0, + Remove = 1, + Update = 2, + Clear = 3, + SnapStatistics = 4, + SnapCache = 5, + LookupCdb = 6, + RefreshCdb = 7, + MapQuirks = 8, + HwIdQuery = 9, + InitProcessData = 10, + LookupAndWriteToProcess = 11, +} + +fn handle_lookup_cdb( + service_data: Option>, +) -> NtStatus { + let Some(data_ptr) = service_data else { + return NtStatus::INVALID_PARAMETER; + }; + + let Some(service_data) = data_ptr.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + + if service_data.params_out == 0 || service_data.params_out_size != size_of::().trunc() { + return NtStatus::INVALID_PARAMETER; + } + + match service_data.lookup_cdb.name.read_string::() { + Ok(name) => { + litebox_util_log::debug!( + lookup_cdb_name:% = name, + params_out:% = format_args!("{:#x}", service_data.params_out), + params_out_size = service_data.params_out_size; + "Decoded NtApphelpCacheControl LookupCdb service data" + ); + } + Err(status) => { + litebox_util_log::warn!( + status:? = status, + params_out:% = format_args!("{:#x}", service_data.params_out), + params_out_size = service_data.params_out_size; + "Failed to decode NtApphelpCacheControl LookupCdb name" + ); + } + } + + // TODO: zero seems to indicate no matches. + let params_out = MutPtr::::from_usize(service_data.params_out); + if params_out.write_at_offset(0, 0).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + + if crate::write_field_at_offset::( + data_ptr.as_usize(), + offset_of!(AhcServiceData, driver_status), + NtStatus::SUCCESS.as_raw(), + ) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + NtStatus::SUCCESS +} + +pub(crate) fn sys_nt_apphelp_cache_control( + service_class: u32, + service_data: Option>, +) -> NtStatus { + let Ok(service_class) = AhcServiceClass::try_from(service_class) else { + litebox_util_log::debug!( + service_class, + service_data:% = format_args!("{:#x}", service_data.map_or(0, |ptr| ptr.as_usize())); + "Rejected NtApphelpCacheControl service class" + ); + return NtStatus::INVALID_PARAMETER; + }; + + let status = match service_class { + AhcServiceClass::LookupCdb => handle_lookup_cdb::(service_data), + AhcServiceClass::Lookup | AhcServiceClass::LookupAndWriteToProcess => { + NtStatus::NOT_SUPPORTED + } + AhcServiceClass::Remove + | AhcServiceClass::Update + | AhcServiceClass::Clear + | AhcServiceClass::SnapStatistics + | AhcServiceClass::SnapCache + | AhcServiceClass::RefreshCdb + | AhcServiceClass::MapQuirks + | AhcServiceClass::HwIdQuery + | AhcServiceClass::InitProcessData => NtStatus::NOT_SUPPORTED, + }; + + litebox_util_log::debug!( + service_class:? = service_class, + service_data:% = format_args!("{:#x}", service_data.map_or(0, |ptr| ptr.as_usize())), + status:? = status; + "Handled NtApphelpCacheControl with empty apphelp cache" + ); + + status +} diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index f401a1690d..7d2c8799a5 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +pub(crate) mod apphelp; pub(crate) mod directory; pub(crate) mod event; pub(crate) mod file; @@ -312,6 +313,10 @@ pub(crate) enum SyscallRequest { length: u32, fs_information_class: u32, }, + NtApphelpCacheControl { + service_class: u32, + service_data: Option>, + }, NtOpenKey { key_handle: Platform::RawMutPointer, desired_access: u32, @@ -720,6 +725,10 @@ impl SyscallRequest { length, fs_information_class, })), + NtSysno::NtApphelpCacheControl => Some(sys_req!(NtApphelpCacheControl { + service_class, + service_data:*, + })), NtSysno::NtOpenKey => Some(sys_req!(NtOpenKey { key_handle:*, desired_access, From e20685a4bddc7a88fe2274ac8f999f30b66e4c8a Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 8 Jul 2026 18:01:23 -0700 Subject: [PATCH 090/319] Introduce object manager to Windows shim (#1015) This PR wires the Windows CSR shared section through the shim as a real named section object and maps it during process load. Due to the lack of shared mapping support, it is not actually shared even within one process. It also consolidates Windows named-object handling into a single object-manager namespace. Directory, symbolic link, event, and section objects now live under one namespace tree, with typed object leaves and shared resolution logic. This lets paths like \Windows\SharedSection behave as an object-manager shortcut to \Sessions\0\Windows\SharedSection while keeping a TODO for multi-session resolution. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 173 +++++++-- litebox_shim_windows/src/loader/pe.rs | 197 ++-------- litebox_shim_windows/src/nt_types.rs | 101 +++++- litebox_shim_windows/src/syscalls/event.rs | 97 +++-- litebox_shim_windows/src/syscalls/mod.rs | 4 +- .../{directory.rs => object_manager.rs} | 338 +++++++++++++----- litebox_shim_windows/src/syscalls/section.rs | 249 +++++++++---- litebox_shim_windows/src/syscalls/symlink.rs | 11 +- litebox_shim_windows/src/syscalls/sysinfo.rs | 11 + litebox_shim_windows/src/tests.rs | 51 +-- 10 files changed, 773 insertions(+), 459 deletions(-) rename litebox_shim_windows/src/syscalls/{directory.rs => object_manager.rs} (88%) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 01ac981d4e..b91b797352 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -12,8 +12,7 @@ extern crate alloc; use alloc::collections::BTreeMap; -use alloc::string::String; -use alloc::sync::{Arc, Weak}; +use alloc::sync::Arc; use alloc::vec::Vec; use core::marker::PhantomData; use core::sync::atomic::{AtomicI32, AtomicU32, Ordering}; @@ -31,12 +30,12 @@ use litebox::sync::RawSyncPrimitivesProvider; use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; -use crate::syscalls::directory::{ - DirectoryHandleObject, DirectoryNamespace, DirectoryObjectSubsystem, -}; -use crate::syscalls::event::{EventHandleObject, EventObject, EventSubsystem}; +use crate::syscalls::event::{EventHandleObject, EventSubsystem}; use crate::syscalls::file::{FileObject, FileObjectSubsystem}; use crate::syscalls::iocp::{IoCompletionHandleObject, IoCompletionSubsystem}; +use crate::syscalls::object_manager::{ + DirectoryHandleObject, DirectoryObjectSubsystem, ObjectManager, +}; use crate::syscalls::registry::{RegistryKeyObject, RegistryKeySubsystem}; use crate::syscalls::section::{ MapViewOfSectionParameters, SectionHandleObject, SectionObject, SectionSubsystem, @@ -93,13 +92,9 @@ pub(crate) type WindowsNlsSectionMappings = litebox::sync::RwLock>; pub(crate) type WindowsVirtualAllocations = litebox::sync::RwLock>; -pub(crate) type WindowsSectionNamespace = - litebox::sync::RwLock>>>; pub(crate) type WindowsSectionViews = litebox::sync::RwLock>>; -pub(crate) type WindowsEventNamespace = - litebox::sync::RwLock>>>; -pub(crate) type WindowsDirectoryNamespace = DirectoryNamespace; +pub(crate) type WindowsObjectManager = ObjectManager; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct WindowsVirtualAllocation { @@ -355,6 +350,80 @@ impl WindowsShimBuilder { } } +/// Wine and ReactOS model KUSER_SHARED_DATA as a fixed user page at +/// 0x7FFE0000. Native Windows hosts already provide that page; Non-Windows hosts +/// need LiteBox to create it before guest ntdll reads it during startup. +#[cfg(not(target_os = "windows"))] +const WINDOWS_USER_SHARED_DATA_BASE: usize = 0x7FFE_0000; + +#[cfg(not(target_os = "windows"))] +fn map_windows_user_shared_data( + page_manager: &crate::WindowsPageManager, +) -> Option { + use litebox::mm::linux::{CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize}; + use zerocopy::IntoBytes as _; + let address = NonZeroAddress::new(WINDOWS_USER_SHARED_DATA_BASE)?; + let length = + NonZeroPageSize::new(size_of::().next_multiple_of(PAGE_SIZE))?; + let shared_data = windows_user_shared_data(); + let shared_data_bytes = shared_data.as_bytes(); + // SAFETY: `NOREPLACE` makes the fixed mapping fail instead of replacing any + // existing host or guest mapping at the shared-data address. + unsafe { + page_manager.create_readable_pages( + Some(address), + length, + CreatePagesFlags::FIXED_ADDR | CreatePagesFlags::NOREPLACE, + |ptr| { + ptr.copy_from_slice(0, shared_data_bytes) + .ok_or(MappingError::OutOfMemory)?; + Ok(0) + }, + ) + } + .map(|ptr| ptr.as_usize()) + .ok() +} + +// TODO: This is a temporary placeholder for the Windows shared data page. +// Once we have a proper shared mapping implementation, we can remove this +// and instead map the shared data page from the host into the guest. +#[cfg(not(target_os = "windows"))] +fn windows_user_shared_data() -> nt_types::KUserSharedData { + use zerocopy::FromZeros as _; + let mut shared_data = nt_types::KUserSharedData::new_zeroed(); + shared_data.nt_build_number = u32::from(syscalls::sysinfo::WINDOWS_OS_BUILD_NUMBER); + shared_data.nt_product_type = syscalls::sysinfo::WINDOWS_NT_PRODUCT_WORKSTATION; + shared_data.product_type_is_valid = 1; + shared_data.nt_major_version = u32::from(syscalls::sysinfo::WINDOWS_OS_MAJOR_VERSION); + shared_data.nt_minor_version = u32::from(syscalls::sysinfo::WINDOWS_OS_MINOR_VERSION); + for (index, code_unit) in r"C:\Windows".encode_utf16().enumerate() { + shared_data.nt_system_root[index] = code_unit; + } + + shared_data +} + +fn map_csr_server_shared_memory( + page_manager: &crate::WindowsPageManager, +) -> Option { + let length = litebox::mm::linux::NonZeroPageSize::new( + crate::syscalls::section::WINDOWS_SHARED_SECTION_SIZE, + )?; + // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` leaves address + // selection to the page manager, so this cannot replace an existing mapping. + unsafe { + page_manager.create_writable_pages( + None, + length, + litebox::mm::linux::CreatePagesFlags::empty(), + |_| Ok(0), + ) + } + .map(|mapping| mapping.as_usize()) + .ok() +} + pub struct WindowsShim(Arc>); impl WindowsShim { @@ -366,26 +435,31 @@ impl WindowsShim { argv: Vec, envp: Vec, ) -> Result, loader::WindowsLoadError> { + // TODO: refactor the shared mapping + #[cfg(not(target_os = "windows"))] + let _ = map_windows_user_shared_data::(&self.0.page_manager) + .ok_or(loader::WindowsLoadError::MapSharedMemory)?; + let windows_shared_section_addr = map_csr_server_shared_memory(&self.0.page_manager) + .ok_or(loader::WindowsLoadError::MapSharedMemory)?; + let windows_shared_section = + crate::syscalls::section::load_time_windows_shared_section(windows_shared_section_addr); + let load_info = loader::PeLoader::new(self.0.platform, fs.clone(), &self.0.page_manager) .load(path, &argv, &envp)?; - let directory_namespace = syscalls::directory::seed_directory_namespace(); - let process = Arc::new(Process { - ntdll_mapping: load_info.ntdll_mapping, - peb_address: load_info.environment.peb, - handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), - directory_namespace, - event_namespace: WindowsEventNamespace::::new(BTreeMap::new()), - section_namespace: WindowsSectionNamespace::::new(BTreeMap::new()), - section_views: WindowsSectionViews::::new(BTreeMap::new()), - nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), - virtual_allocations: load_info.virtual_allocations, - system_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), - user_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), - user_ui_language: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), - default_hard_error_mode: AtomicU32::new(0), - cookie: syscalls::process::default_process_cookie(), - exit_code: AtomicI32::new(DEFAULT_PROCESS_EXIT_CODE), - }); + let mut process = + Process::default(Some(load_info.virtual_allocations), windows_shared_section); + process.ntdll_mapping = load_info.ntdll_mapping; + process.peb_address = load_info.environment.peb; + write_field_at_offset::( + process.peb_address, + core::mem::offset_of!( + crate::nt_types::ProcessEnvironmentBlock, + csr_server_read_only_shared_memory_base + ), + windows_shared_section_addr, + ) + .ok_or(loader::WindowsLoadError::MemoryAccess)?; + let process = Arc::new(process); Ok(LoadedProgram { entrypoints: WindowsShimEntrypoints { task: Task { @@ -419,10 +493,11 @@ pub struct Process { ntdll_mapping: Option, peb_address: usize, handles: WindowsHandleStore, - directory_namespace: WindowsDirectoryNamespace, - event_namespace: WindowsEventNamespace, - section_namespace: WindowsSectionNamespace, + object_manager: WindowsObjectManager, section_views: WindowsSectionViews, + // TODO: move this into `GlobalState` once we have a proper shared mapping implementation. + #[expect(dead_code)] + windows_shared_section: Arc>, nls_section_mappings: WindowsNlsSectionMappings, virtual_allocations: WindowsVirtualAllocations, system_lcid: AtomicU32, @@ -443,6 +518,38 @@ impl Process { // TODO: Wait for the NT process object once process lifecycle exists. self.exit_code.load(Ordering::Relaxed) } + + fn default( + virtual_allocations: Option>, + windows_shared_section: Arc>, + ) -> Self { + let object_manager = syscalls::object_manager::seed_object_manager(); + let status = object_manager.create_section( + syscalls::section::WINDOWS_SESSION_SHARED_SECTION_OBJECT, + &windows_shared_section, + ); + assert!( + status == NtStatus::SUCCESS, + "seeded Windows shared section must have seeded ancestors: {status:?}" + ); + Process { + ntdll_mapping: None, + peb_address: 0, + handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), + object_manager, + windows_shared_section, + section_views: WindowsSectionViews::::new(BTreeMap::new()), + nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), + virtual_allocations: virtual_allocations + .unwrap_or_else(|| WindowsVirtualAllocations::::new(BTreeMap::new())), + system_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), + user_lcid: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), + user_ui_language: AtomicU32::new(syscalls::nls::DEFAULT_LOCALE_ID), + default_hard_error_mode: AtomicU32::new(0), + cookie: syscalls::process::default_process_cookie(), + exit_code: AtomicI32::new(DEFAULT_PROCESS_EXIT_CODE), + } + } } struct Task { @@ -643,7 +750,7 @@ impl Task { return_length, } => { let status = self.sys_nt_query_directory_object( - syscalls::directory::DirectoryQueryParameters { + syscalls::object_manager::DirectoryQueryParameters { directory_handle, buffer, buffer_length, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index a87d74b033..8fc050db84 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -43,20 +43,6 @@ const INITIAL_STACK_SIZE: usize = 1024 * 1024; const WINDOWS_SHARED_SECTION_SIZE: usize = 0x1_0000; const CSR_SERVER_DLL_MAX: usize = 4; const BASESRV_SERVERDLL_INDEX: usize = 1; -const WINDOWS_DIRECTORY: &str = r"C:\Windows"; -const WINDOWS_SYSTEM_DIRECTORY: &str = r"C:\Windows\System32"; -const WINDOWS_NAMED_OBJECT_DIRECTORY: &str = r"\BaseNamedObjects"; -const WINDOWS_OS_MAJOR_VERSION: u16 = 10; -const WINDOWS_OS_MINOR_VERSION: u16 = 0; -const WINDOWS_OS_BUILD_NUMBER: u16 = 19041; -const WINDOWS_OS_PLATFORM_WIN32_NT: u32 = 2; -#[cfg(not(target_os = "windows"))] -const WINDOWS_USER_SHARED_DATA_BASE: usize = 0x7FFE_0000; -#[cfg(not(target_os = "windows"))] -const WINDOWS_KUSER_SHARED_DATA_XSTATE_CONFIGURATION_SIZE: usize = 0x348; -#[cfg(not(target_os = "windows"))] -const WINDOWS_NT_PRODUCT_WORKSTATION: u32 = 1; -const WINDOWS_TIME_ZONE_ID_INVALID: u32 = u32::MAX; const WINDOWS_CRITICAL_SECTION_TIMEOUT_100NS: i64 = -150 * 10_000_000; const WINDOWS_HEAP_SEGMENT_RESERVE: u64 = 1024 * 1024; const WINDOWS_HEAP_SEGMENT_COMMIT: u64 = 2 * PAGE_SIZE as u64; @@ -74,94 +60,6 @@ macro_rules! write_static_server_data_field { }; } -/// Layout from Wine `include/ddk/wdm.h` and ReactOS `sdk/include/wine/ddk/wdm.h`. -#[cfg(not(target_os = "windows"))] -#[repr(C)] -#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)] -struct KUserSharedData { - tick_count_low_deprecated: u32, - tick_count_multiplier: u32, - interrupt_time: KSystemTime, - system_time: KSystemTime, - time_zone_bias: KSystemTime, - image_number_low: u16, - image_number_high: u16, - nt_system_root: [u16; 260], - max_stack_trace_depth: u32, - crypto_exponent: u32, - time_zone_id: u32, - large_page_minimum: u32, - ait_sampling_value: u32, - app_compat_flag: u32, - rng_seed_version: u64, - global_validation_run_level: u32, - time_zone_bias_stamp: u32, - nt_build_number: u32, - nt_product_type: u32, - product_type_is_valid: u8, - reserved_0: u8, - native_processor_architecture: u16, - nt_major_version: u32, - nt_minor_version: u32, - processor_features: [u8; 64], - reserved_1: u32, - reserved_3: u32, - time_slip: u32, - alternative_architecture: u32, - boot_id: u32, - system_expiration_date: i64, - suite_mask: u32, - kd_debugger_enabled: u8, - nx_support_policy: u8, - cycles_per_yield: u16, - active_console_id: u32, - dismount_count: u32, - com_plus_package: u32, - last_system_rit_event_tick_count: u32, - number_of_physical_pages: u32, - safe_boot_mode: u8, - virtualization_flags: u8, - padding_2ee: [u8; 2], - shared_data_flags: u32, - data_flags_pad: [u32; 1], - test_ret_instruction: u64, - qpc_frequency: i64, - system_call: u32, - user_cet_available_environments: u32, - system_call_pad: [u64; 2], - tick_count: [u8; 0x10], - cookie: u32, - cookie_pad: [u32; 1], - console_session_foreground_process_id: i64, - time_update_lock: u64, - baseline_system_time_qpc: u64, - baseline_interrupt_time_qpc: u64, - qpc_system_time_increment: u64, - qpc_interrupt_time_increment: u64, - qpc_system_time_increment_shift: u8, - qpc_interrupt_time_increment_shift: u8, - unparked_processor_count: u16, - enclave_feature_mask: [u32; 4], - telemetry_coverage_round: u32, - user_mode_global_logger: [u16; 16], - image_file_execution_options: u32, - lang_generation_count: u32, - active_processor_affinity: u32, - padding_3ac: u32, - interrupt_time_bias: u64, - qpc_bias: u64, - active_processor_count: u32, - active_group_count: u8, - padding_3c5: u8, - qpc_data: u16, - time_zone_bias_effective_start: i64, - time_zone_bias_effective_end: i64, - x_state: [u8; WINDOWS_KUSER_SHARED_DATA_XSTATE_CONFIGURATION_SIZE], - feature_configuration_change_stamp: KSystemTime, - spare: u32, - user_pointer_auth_mask: u64, -} - pub(crate) struct WindowsProcessEnvironment { pub(crate) peb: usize, pub(crate) teb: usize, @@ -211,9 +109,6 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { argv: &[CString], envp: &[CString], ) -> Result, WindowsLoadError> { - #[cfg(not(target_os = "windows"))] - map_windows_user_shared_data::(self.page_manager)?; - let image = load_image(self.platform, self.fs.clone(), path, self.page_manager)?; let application_entry_point = image.mapping.entry_point; let ntdll = load_ntdll(self.platform, self.fs.clone(), self.page_manager)?; @@ -470,16 +365,16 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { peb.process_heaps = process_heaps.address; peb.loader_lock = loader_lock; peb.active_process_affinity_mask = 1; - peb.os_major_version = u32::from(WINDOWS_OS_MAJOR_VERSION); - peb.os_minor_version = u32::from(WINDOWS_OS_MINOR_VERSION); - peb.os_build_number = WINDOWS_OS_BUILD_NUMBER; - peb.os_platform_id = WINDOWS_OS_PLATFORM_WIN32_NT; + 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; - peb.csr_server_read_only_shared_memory_base = read_only_shared_memory_base as u64; + write_guest_value::(peb_ptr, peb)?; let mut teb = ThreadEnvironmentBlock::new_zeroed(); @@ -533,16 +428,20 @@ fn initialize_static_server_data( shared_heap: &mut GuestMemoryAllocator, base_static_server_data: MutPtr, ) -> Result<(), PeImageAccessError> { - let windows_directory = - allocate_guest_unicode_string_from_str::(shared_heap, WINDOWS_DIRECTORY)?; + let windows_directory = allocate_guest_unicode_string_from_str::( + shared_heap, + crate::syscalls::sysinfo::WINDOWS_DIRECTORY, + )?; write_static_server_data_field!( Platform, base_static_server_data, windows_directory, windows_directory, )?; - let windows_system_directory = - allocate_guest_unicode_string_from_str::(shared_heap, WINDOWS_SYSTEM_DIRECTORY)?; + let windows_system_directory = allocate_guest_unicode_string_from_str::( + shared_heap, + crate::syscalls::sysinfo::WINDOWS_SYSTEM_DIRECTORY, + )?; write_static_server_data_field!( Platform, base_static_server_data, @@ -551,7 +450,7 @@ fn initialize_static_server_data( )?; let named_object_directory = allocate_guest_unicode_string_from_str::( shared_heap, - WINDOWS_NAMED_OBJECT_DIRECTORY, + crate::syscalls::sysinfo::WINDOWS_NAMED_OBJECT_DIRECTORY, )?; write_static_server_data_field!( Platform, @@ -563,19 +462,19 @@ fn initialize_static_server_data( Platform, base_static_server_data, windows_major_version, - WINDOWS_OS_MAJOR_VERSION, + crate::syscalls::sysinfo::WINDOWS_OS_MAJOR_VERSION, )?; write_static_server_data_field!( Platform, base_static_server_data, windows_minor_version, - WINDOWS_OS_MINOR_VERSION, + crate::syscalls::sysinfo::WINDOWS_OS_MINOR_VERSION, )?; write_static_server_data_field!( Platform, base_static_server_data, build_number, - WINDOWS_OS_BUILD_NUMBER, + crate::syscalls::sysinfo::WINDOWS_OS_BUILD_NUMBER, )?; let ini_file_mapping = shared_heap @@ -591,7 +490,7 @@ fn initialize_static_server_data( Platform, base_static_server_data, termsrv_client_time_zone_id, - WINDOWS_TIME_ZONE_ID_INVALID, + crate::syscalls::sysinfo::WINDOWS_TIME_ZONE_ID_INVALID, )?; Ok(()) } @@ -754,7 +653,7 @@ struct BaseStaticServerData { f_termsrv_app_install_mode: u8, padding_2: [u8; 3], tzi_termsrv_client_time_zone: TimeZoneInformation, - kt_termsrv_client_bias: KSystemTime, + kt_termsrv_client_bias: crate::nt_types::KSystemTime, termsrv_client_time_zone_id: u32, luid_device_maps_enabled: u8, padding_3: [u8; 3], @@ -881,14 +780,6 @@ struct SystemTime { milliseconds: u16, } -#[repr(C)] -#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)] -struct KSystemTime { - low_part: u32, - high_1_time: i32, - high_2_time: i32, -} - const API_SET_MAPPINGS: &[(&str, &str)] = &[ ("api-ms-win-core-apiquery-l1-1-0", "ntdll.dll"), ("api-ms-win-core-apiquery-l1-1-2", "ntdll.dll"), @@ -1127,52 +1018,6 @@ where crate::write_slice::(address, values).ok_or(PeImageAccessError::MemoryAccess) } -/// Wine and ReactOS model KUSER_SHARED_DATA as a fixed user page at -/// 0x7FFE0000. Native Windows hosts already provide that page; Non-Windows hosts -/// need LiteBox to create it before guest ntdll reads it during startup. -#[cfg(not(target_os = "windows"))] -fn map_windows_user_shared_data( - page_manager: &crate::WindowsPageManager, -) -> Result<(), PeImageAccessError> { - let address = NonZeroAddress::new(WINDOWS_USER_SHARED_DATA_BASE) - .ok_or(PeImageAccessError::AddressOverflow)?; - let length = NonZeroPageSize::new(size_of::().next_multiple_of(PAGE_SIZE)) - .ok_or(PeImageAccessError::AddressOverflow)?; - let shared_data = windows_user_shared_data(); - let shared_data_bytes = shared_data.as_bytes(); - // SAFETY: `NOREPLACE` makes the fixed mapping fail instead of replacing any - // existing host or guest mapping at the shared-data address. - unsafe { - page_manager.create_readable_pages( - Some(address), - length, - CreatePagesFlags::FIXED_ADDR | CreatePagesFlags::NOREPLACE, - |ptr| { - ptr.copy_from_slice(0, shared_data_bytes) - .ok_or(MappingError::OutOfMemory)?; - Ok(0) - }, - ) - } - .map_err(PeImageAccessError::from) - .map(|_| ()) -} - -#[cfg(not(target_os = "windows"))] -fn windows_user_shared_data() -> KUserSharedData { - let mut shared_data = KUserSharedData::new_zeroed(); - shared_data.nt_build_number = u32::from(WINDOWS_OS_BUILD_NUMBER); - shared_data.nt_product_type = WINDOWS_NT_PRODUCT_WORKSTATION; - shared_data.product_type_is_valid = 1; - shared_data.nt_major_version = u32::from(WINDOWS_OS_MAJOR_VERSION); - shared_data.nt_minor_version = u32::from(WINDOWS_OS_MINOR_VERSION); - for (index, code_unit) in WINDOWS_DIRECTORY.encode_utf16().enumerate() { - shared_data.nt_system_root[index] = code_unit; - } - - shared_data -} - struct LoadedNtDll { image: LoadedImage, exports: NtDllExports, @@ -1367,6 +1212,10 @@ pub enum WindowsLoadError { /// Guest ntdll.dll has not been rewritten for LiteBox syscall/GS handling. #[error("guest ntdll.dll must be rewritten for LiteBox before entering its loader")] UnrewrittenNtDll, + #[error("failed to map shared memory")] + MapSharedMemory, + #[error("memory access failed")] + MemoryAccess, } fn is_missing_file_error(error: &WindowsLoadError) -> bool { diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index bf6ffa713d..d8d9ae7438 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -5,7 +5,7 @@ use alloc::string::String; use core::mem::offset_of; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; use litebox_common_windows::nt_status::NtStatus; -use zerocopy::{FromBytes, Immutable, IntoBytes}; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use crate::{ConstPtr, syscalls::Handle}; @@ -781,3 +781,102 @@ const _: [(); 0x1878] = [(); core::mem::size_of::()]; const _: [(); 0x7d0] = [(); core::mem::size_of::()]; const _: [(); 0x4d0] = [(); core::mem::size_of::()]; const _: [(); 0x448] = [(); core::mem::size_of::()]; + +#[repr(C)] +#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)] +pub struct KSystemTime { + pub low_part: u32, + pub high_1_time: i32, + pub high_2_time: i32, +} + +#[cfg(not(target_os = "windows"))] +const WINDOWS_KUSER_SHARED_DATA_XSTATE_CONFIGURATION_SIZE: usize = 0x348; + +/// Layout from Wine `include/ddk/wdm.h` and ReactOS `sdk/include/wine/ddk/wdm.h`. +#[cfg(not(target_os = "windows"))] +#[repr(C)] +#[derive(Clone, Copy, FromBytes, Immutable, IntoBytes, KnownLayout)] +pub struct KUserSharedData { + tick_count_low_deprecated: u32, + tick_count_multiplier: u32, + interrupt_time: KSystemTime, + system_time: KSystemTime, + time_zone_bias: KSystemTime, + image_number_low: u16, + image_number_high: u16, + pub nt_system_root: [u16; 260], + max_stack_trace_depth: u32, + crypto_exponent: u32, + time_zone_id: u32, + large_page_minimum: u32, + ait_sampling_value: u32, + app_compat_flag: u32, + rng_seed_version: u64, + global_validation_run_level: u32, + time_zone_bias_stamp: u32, + pub nt_build_number: u32, + pub nt_product_type: u32, + pub product_type_is_valid: u8, + reserved_0: u8, + native_processor_architecture: u16, + pub nt_major_version: u32, + pub nt_minor_version: u32, + processor_features: [u8; 64], + reserved_1: u32, + reserved_3: u32, + time_slip: u32, + alternative_architecture: u32, + boot_id: u32, + system_expiration_date: i64, + suite_mask: u32, + kd_debugger_enabled: u8, + nx_support_policy: u8, + cycles_per_yield: u16, + active_console_id: u32, + dismount_count: u32, + com_plus_package: u32, + last_system_rit_event_tick_count: u32, + number_of_physical_pages: u32, + safe_boot_mode: u8, + virtualization_flags: u8, + padding_2ee: [u8; 2], + shared_data_flags: u32, + data_flags_pad: [u32; 1], + test_ret_instruction: u64, + qpc_frequency: i64, + system_call: u32, + user_cet_available_environments: u32, + system_call_pad: [u64; 2], + tick_count: [u8; 0x10], + cookie: u32, + cookie_pad: [u32; 1], + console_session_foreground_process_id: i64, + time_update_lock: u64, + baseline_system_time_qpc: u64, + baseline_interrupt_time_qpc: u64, + qpc_system_time_increment: u64, + qpc_interrupt_time_increment: u64, + qpc_system_time_increment_shift: u8, + qpc_interrupt_time_increment_shift: u8, + unparked_processor_count: u16, + enclave_feature_mask: [u32; 4], + telemetry_coverage_round: u32, + user_mode_global_logger: [u16; 16], + image_file_execution_options: u32, + lang_generation_count: u32, + active_processor_affinity: u32, + padding_3ac: u32, + interrupt_time_bias: u64, + qpc_bias: u64, + active_processor_count: u32, + active_group_count: u8, + padding_3c5: u8, + qpc_data: u16, + time_zone_bias_effective_start: i64, + time_zone_bias_effective_end: i64, + x_state: [u8; WINDOWS_KUSER_SHARED_DATA_XSTATE_CONFIGURATION_SIZE], + feature_configuration_change_stamp: KSystemTime, + spare: u32, + user_pointer_auth_mask: u64, +} diff --git a/litebox_shim_windows/src/syscalls/event.rs b/litebox_shim_windows/src/syscalls/event.rs index 1f52c45617..92bf3bdf97 100644 --- a/litebox_shim_windows/src/syscalls/event.rs +++ b/litebox_shim_windows/src/syscalls/event.rs @@ -3,7 +3,6 @@ //! Windows NT event object syscalls. -use alloc::string::String; use alloc::sync::{Arc, Weak}; use core::marker::PhantomData; use core::mem::size_of; @@ -183,7 +182,7 @@ impl IOPollable for EventObject { } struct EventName { - key: String, + original_path: alloc::string::String, } const EVENT_BASIC_INFORMATION_SIZE_U32: u32 = 8; @@ -213,16 +212,11 @@ fn read_event_name( if unicode_string.buffer == 0 { return Err(NtStatus::ACCESS_VIOLATION); } - let mut key = unicode_string.read_string::()?; - if key.is_empty() { + let original_path = unicode_string.read_string::()?; + if original_path.is_empty() { return Err(NtStatus::OBJECT_NAME_INVALID); } - if ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) - .contains(ObjectAttributesFlags::CASE_INSENSITIVE) - { - key = key.to_ascii_lowercase(); - } - Ok(Some(EventName { key })) + Ok(Some(EventName { original_path })) } fn read_event_object_attributes( @@ -307,43 +301,39 @@ impl Task { let granted_access = EventAccess::from_desired_access(desired_access); if let Some(event_name) = event_name { - let mut namespace = self.process.event_namespace.write(); - let existing = - if let Some(event) = namespace.get(&event_name.key).and_then(Weak::upgrade) { - Some(event) - } else { - namespace.remove(&event_name.key); - None - }; - if let Some(event) = existing { - let Some(object_attributes) = object_attributes else { - return NtStatus::INVALID_PARAMETER; - }; - if !ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) - .contains(ObjectAttributesFlags::OPENIF) - { - return NtStatus::OBJECT_NAME_COLLISION; - } - let Ok(handle) = self.insert_event_handle(event, granted_access) else { - return NtStatus::QUOTA_EXCEEDED; - }; - if event_handle.write_at_offset(0, handle).is_none() { - self.close_event_handle(handle); - return NtStatus::ACCESS_VIOLATION; - } - return NtStatus::OBJECT_NAME_EXISTS; - } - let event = Arc::new(EventObject::new(event_type, initial_state != 0)); - let Ok(handle) = self.insert_event_handle(event.clone(), granted_access) else { - return NtStatus::QUOTA_EXCEEDED; - }; - if event_handle.write_at_offset(0, handle).is_none() { - self.close_event_handle(handle); - return NtStatus::ACCESS_VIOLATION; - } - namespace.insert(event_name.key, Arc::downgrade(&event)); - return NtStatus::SUCCESS; + return self.process.object_manager.create_event( + &event_name.original_path, + &event, + |event| { + let Some(object_attributes) = object_attributes else { + return NtStatus::INVALID_PARAMETER; + }; + if !ObjectAttributesFlags::from_bits_retain(object_attributes.attributes) + .contains(ObjectAttributesFlags::OPENIF) + { + return NtStatus::OBJECT_NAME_COLLISION; + } + let Ok(handle) = self.insert_event_handle(event, granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if event_handle.write_at_offset(0, handle).is_none() { + self.close_event_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::OBJECT_NAME_EXISTS + }, + || { + let Ok(handle) = self.insert_event_handle(event.clone(), granted_access) else { + return NtStatus::QUOTA_EXCEEDED; + }; + if event_handle.write_at_offset(0, handle).is_none() { + self.close_event_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + }, + ); } let event = Arc::new(EventObject::new(event_type, initial_state != 0)); @@ -371,14 +361,13 @@ impl Task { Ok((_, None)) => return NtStatus::OBJECT_NAME_INVALID, Err(status) => return status, }; - let event = { - let mut namespace = self.process.event_namespace.write(); - if let Some(event) = namespace.get(&event_name.key).and_then(Weak::upgrade) { - event - } else { - namespace.remove(&event_name.key); - return NtStatus::OBJECT_NAME_NOT_FOUND; - } + let event = match self + .process + .object_manager + .resolve_event(&event_name.original_path) + { + Ok(event) => event, + Err(status) => return status, }; let Ok(handle) = diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 7d2c8799a5..652a44fec2 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -2,17 +2,17 @@ // Licensed under the MIT license. pub(crate) mod apphelp; -pub(crate) mod directory; pub(crate) mod event; pub(crate) mod file; pub(crate) mod iocp; pub(crate) mod mm; pub(crate) mod nls; +pub(crate) mod object_manager; pub(crate) mod process; pub(crate) mod registry; pub(crate) mod section; pub(crate) mod symlink; -mod sysinfo; +pub(crate) mod sysinfo; pub(crate) mod thread; pub(crate) mod timer; pub(crate) mod wait_completion_packet; diff --git a/litebox_shim_windows/src/syscalls/directory.rs b/litebox_shim_windows/src/syscalls/object_manager.rs similarity index 88% rename from litebox_shim_windows/src/syscalls/directory.rs rename to litebox_shim_windows/src/syscalls/object_manager.rs index f992c12319..936966851a 100644 --- a/litebox_shim_windows/src/syscalls/directory.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Windows NT object-manager directory syscalls. +//! Windows NT object manager. use alloc::collections::BTreeMap; use alloc::string::{String, ToString as _}; @@ -21,6 +21,10 @@ use crate::nt_types::{ AccessMask, ObjectAttributes, ObjectAttributesFlags, UnicodeString, read_object_attributes, }; use crate::syscalls::Handle; +use crate::syscalls::event::EventObject; +use crate::syscalls::section::{ + SectionObject, WINDOWS_SESSION_SHARED_SECTION_OBJECT, WINDOWS_SHARED_SECTION_OBJECT, +}; use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; const MAX_SYMLINK_REPARSE_DEPTH: usize = 64; @@ -48,12 +52,20 @@ const SEEDED_DIRECTORY_PATHS: &[&str] = &[ r"\Sessions\0\Windows", r"\Sessions\0\Windows\WindowStations", r"\Sessions\BNOLINKS", + r"\Windows", ]; // Wine's wineboot and ReactOS SMSS create KnownDllPath so ntdll can open/query // the DOS path prefix for known DLL lookups during loader initialization. -const SEEDED_SYMLINK_PATHS: &[(&str, &str)] = - &[(r"\KnownDlls\KnownDllPath", r"C:\Windows\System32")]; +const SEEDED_SYMLINK_PATHS: &[(&str, &str)] = &[ + (r"\KnownDlls\KnownDllPath", r"C:\Windows\System32"), + // TODO(windows-sessions): resolve this through the current session id once + // the shim supports multiple Windows sessions. + ( + WINDOWS_SHARED_SECTION_OBJECT, + WINDOWS_SESSION_SHARED_SECTION_OBJECT, + ), +]; bitflags::bitflags! { #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -120,10 +132,9 @@ pub(super) struct ObjectNode { name: String, parent: Option>>, body: litebox::sync::RwLock>, - _not_send_without_platform: PhantomData, } -pub(crate) struct DirectoryNamespace { +pub(crate) struct ObjectManager { root: Arc>, } @@ -134,6 +145,70 @@ enum NamedObject { Symlink { target: String, }, + Event { + event: Weak>, + }, + Section { + section: Weak>, + }, +} + +pub(super) enum ObjectLeafLookup { + Live(T), + Stale, + TypeMismatch, +} + +impl ObjectLeafLookup { + fn map(self, f: impl FnOnce(T) -> U) -> ObjectLeafLookup { + match self { + Self::Live(object) => ObjectLeafLookup::Live(f(object)), + Self::Stale => ObjectLeafLookup::Stale, + Self::TypeMismatch => ObjectLeafLookup::TypeMismatch, + } + } + + pub(super) fn into_result(self) -> Result { + match self { + Self::Live(object) => Ok(object), + Self::Stale => Err(NtStatus::OBJECT_NAME_NOT_FOUND), + Self::TypeMismatch => Err(NtStatus::OBJECT_TYPE_MISMATCH), + } + } +} + +impl ObjectLeafLookup> { + fn from_weak(object: &Weak) -> Self { + object.upgrade().map_or(Self::Stale, Self::Live) + } +} + +macro_rules! object_leaf_accessors { + ($($vis:vis $method:ident, $lookup:ty, $pattern:pat => $value:expr;)+) => { + $( + $vis fn $method(&self) -> $lookup { + match &*self.body.read() { + $pattern => $value, + _ => ObjectLeafLookup::TypeMismatch, + } + } + )+ + }; +} + +macro_rules! object_node_constructors { + ($($method:ident($($arg:ident: $arg_ty:ty),*) => $body:expr;)+) => { + $( + fn $method( + path: String, + parent: Option>>, + name: String, + $($arg: $arg_ty),* + ) -> Self { + Self::new(path, parent, name, $body) + } + )+ + }; } #[derive(Clone, Debug)] @@ -225,55 +300,49 @@ pub(crate) struct DirectoryQueryParameters { } impl ObjectNode { - fn new_directory( + fn new( path: String, parent: Option>>, name: String, + body: NamedObject, ) -> Self { Self { path, name, parent, - body: litebox::sync::RwLock::::new(NamedObject::Directory { - children: BTreeMap::new(), - }), - _not_send_without_platform: PhantomData, + body: litebox::sync::RwLock::::new(body), } } - fn new_symlink( - path: String, - parent: Option>>, - name: String, - target: String, - ) -> Self { - Self { - path, - name, - parent, - body: litebox::sync::RwLock::::new(NamedObject::Symlink { target }), - _not_send_without_platform: PhantomData, - } + object_node_constructors! { + new_directory() => NamedObject::Directory { children: BTreeMap::new() }; + new_symlink(target: String) => NamedObject::Symlink { target }; + new_event(event: Weak>) => NamedObject::Event { event }; + new_section(section: Weak>) => NamedObject::Section { section }; } fn child(&self, name: &str) -> Option> { - match &*self.body.read() { - NamedObject::Directory { children } => children.get(&ObjectName::new(name)).cloned(), - NamedObject::Symlink { .. } => None, - } + let body = self.body.read(); + let NamedObject::Directory { children } = &*body else { + return None; + }; + children.get(&ObjectName::new(name)).cloned() } fn children_snapshot(&self) -> Result, NtStatus> { - match &*self.body.read() { - NamedObject::Directory { children } => Ok(children - .values() - .map(|child| DirectoryEntrySnapshot { + let body = self.body.read(); + let NamedObject::Directory { children } = &*body else { + return Err(NtStatus::OBJECT_TYPE_MISMATCH); + }; + Ok(children + .values() + .filter_map(|child| { + child.type_name().map(|type_name| DirectoryEntrySnapshot { name: child.name.clone(), - type_name: child.type_name(), + type_name, }) - .collect()), - NamedObject::Symlink { .. } => Err(NtStatus::OBJECT_TYPE_MISMATCH), - } + }) + .collect()) } pub(super) fn is_directory(&self) -> bool { @@ -284,17 +353,19 @@ impl ObjectNode { matches!(&*self.body.read(), NamedObject::Symlink { .. }) } - pub(super) fn symlink_target(&self) -> Result { - match &*self.body.read() { - NamedObject::Symlink { target } => Ok(target.clone()), - NamedObject::Directory { .. } => Err(NtStatus::OBJECT_TYPE_MISMATCH), - } + object_leaf_accessors! { + directory_object, ObjectLeafLookup<()>, NamedObject::Directory { .. } => ObjectLeafLookup::Live(()); + pub(super) symlink_target, ObjectLeafLookup, NamedObject::Symlink { target } => ObjectLeafLookup::Live(target.clone()); + event_object, ObjectLeafLookup>>, NamedObject::Event { event } => ObjectLeafLookup::from_weak(event); + section_object, ObjectLeafLookup>>, NamedObject::Section { section } => ObjectLeafLookup::from_weak(section); } - fn type_name(&self) -> &'static str { + fn type_name(&self) -> Option<&'static str> { match &*self.body.read() { - NamedObject::Directory { .. } => "Directory", - NamedObject::Symlink { .. } => "SymbolicLink", + NamedObject::Directory { .. } => Some("Directory"), + NamedObject::Symlink { .. } => Some("SymbolicLink"), + NamedObject::Event { event } => event.upgrade().map(|_| "Event"), + NamedObject::Section { section } => section.upgrade().map(|_| "Section"), } } @@ -303,7 +374,7 @@ impl ObjectNode { } } -impl DirectoryNamespace { +impl ObjectManager { fn new() -> Self { Self { root: Arc::new(ObjectNode::new_directory( @@ -314,24 +385,6 @@ impl DirectoryNamespace { } } - pub(super) fn resolve_directory( - &self, - path: &str, - ) -> Result>, NtStatus> { - let tail = absolute_path_tail(path)?; - let node = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, false)?; - if node.is_directory() { - Ok(node) - } else { - Err(NtStatus::OBJECT_TYPE_MISMATCH) - } - } - - pub(super) fn resolve_object(&self, path: &str) -> Result>, NtStatus> { - let tail = absolute_path_tail(path)?; - self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, true) - } - pub(super) fn parent_directory_exists(&self, path: &str) -> bool { let path = trim_trailing_directory_path(path); if path == r"\" { @@ -352,8 +405,15 @@ impl DirectoryNamespace { ) -> NtStatus { self.create_child( path, - ObjectNode::is_directory, + |node| { + if node.is_directory() { + ObjectLeafLookup::Live(Arc::clone(node)) + } else { + ObjectLeafLookup::TypeMismatch + } + }, ObjectNode::new_directory, + NtStatus::OBJECT_TYPE_MISMATCH, on_exists, on_created, ) @@ -368,23 +428,65 @@ impl DirectoryNamespace { ) -> NtStatus { self.create_child( path, - ObjectNode::is_symlink, + |node| { + if node.is_symlink() { + ObjectLeafLookup::Live(Arc::clone(node)) + } else { + ObjectLeafLookup::TypeMismatch + } + }, |path, parent, name| ObjectNode::new_symlink(path, parent, name, target), + NtStatus::OBJECT_TYPE_MISMATCH, on_exists, on_created, ) } - fn create_child( + pub(super) fn create_event( &self, path: &str, - existing_matches: impl Fn(&ObjectNode) -> bool, + event: &Arc>, + on_exists: impl FnOnce(Arc>) -> NtStatus, + on_created: impl FnOnce() -> NtStatus, + ) -> NtStatus { + let event = Arc::downgrade(event); + self.create_child( + path, + |node| node.event_object(), + |path, parent, name| ObjectNode::new_event(path, parent, name, event), + NtStatus::OBJECT_TYPE_MISMATCH, + on_exists, + |_| on_created(), + ) + } + + pub(crate) fn create_section( + &self, + path: &str, + section: &Arc>, + ) -> NtStatus { + let section = Arc::downgrade(section); + self.create_child( + path, + |node| node.section_object(), + |path, parent, name| ObjectNode::new_section(path, parent, name, section), + NtStatus::OBJECT_NAME_EXISTS, + |_| NtStatus::OBJECT_NAME_EXISTS, + |_| NtStatus::SUCCESS, + ) + } + + fn create_child( + &self, + path: &str, + existing_object: impl Fn(&Arc>) -> ObjectLeafLookup, construct: impl FnOnce( String, Option>>, String, ) -> ObjectNode, - on_exists: impl FnOnce(Arc>) -> NtStatus, + mismatch_status: NtStatus, + on_exists: impl FnOnce(T) -> NtStatus, on_created: impl FnOnce(Arc>) -> NtStatus, ) -> NtStatus { let tail = match absolute_path_tail(path) { @@ -392,7 +494,11 @@ impl DirectoryNamespace { Err(status) => return status, }; if tail.is_empty() { - return on_exists(Arc::clone(&self.root)); + return match existing_object(&self.root) { + ObjectLeafLookup::Live(object) => on_exists(object), + ObjectLeafLookup::Stale => NtStatus::OBJECT_NAME_NOT_FOUND, + ObjectLeafLookup::TypeMismatch => mismatch_status, + }; } let (parent_tail, leaf_name) = match tail.rsplit_once('\\') { @@ -412,11 +518,14 @@ impl DirectoryNamespace { return NtStatus::OBJECT_TYPE_MISMATCH; }; let leaf_key = ObjectName::new(leaf_name); - if let Some(existing) = children.get(&leaf_key) { - if !existing_matches(existing) { - return NtStatus::OBJECT_TYPE_MISMATCH; + if let Some(existing) = children.get(&leaf_key).cloned() { + match existing_object(&existing) { + ObjectLeafLookup::Live(object) => return on_exists(object), + ObjectLeafLookup::Stale => { + children.remove(&leaf_key); + } + ObjectLeafLookup::TypeMismatch => return mismatch_status, } - return on_exists(Arc::clone(existing)); } let node = Arc::new(construct( @@ -432,18 +541,45 @@ impl DirectoryNamespace { status } + pub(super) fn resolve_directory( + &self, + path: &str, + ) -> Result>, NtStatus> { + self.resolve_object_leaf(path, false, |node| { + node.directory_object().map(|()| Arc::clone(node)) + }) + } + pub(super) fn resolve_symlink( &self, path: &str, open_final_symlink: bool, ) -> Result>, NtStatus> { + self.resolve_object_leaf(path, open_final_symlink, |node| { + node.symlink_target().map(|_| Arc::clone(node)) + }) + } + + pub(super) fn resolve_event(&self, path: &str) -> Result>, NtStatus> { + self.resolve_object_leaf(path, true, |node| node.event_object()) + } + + pub(super) fn resolve_section( + &self, + path: &str, + ) -> Result>, NtStatus> { + self.resolve_object_leaf(path, false, |node| node.section_object()) + } + + fn resolve_object_leaf( + &self, + path: &str, + open_final_symlink: bool, + lookup: impl FnOnce(&Arc>) -> ObjectLeafLookup, + ) -> Result { let tail = absolute_path_tail(path)?; let node = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, open_final_symlink)?; - if node.is_symlink() { - Ok(node) - } else { - Err(NtStatus::OBJECT_TYPE_MISMATCH) - } + lookup(&node).into_result() } fn seed_directory(&self, path: &str) { @@ -509,7 +645,7 @@ impl DirectoryNamespace { if child.is_symlink() && (!final_component || !open_final_symlink) { // This is the lazy-resolution point paired with // NtCreateSymbolicLinkObject storing the target without lookup. - let target = normalize_reparse_target(&child.symlink_target()?)?; + let target = normalize_reparse_target(&child.symlink_target().into_result()?)?; let target_tail = absolute_path_tail(&target)?; let remaining = components.collect::>().join("\\"); let next_tail = if target_tail.is_empty() { @@ -870,7 +1006,7 @@ impl Task { let granted_access = DirectoryAccess::from_desired_access(desired_access); if let Some(directory_name) = directory_name { - return self.process.directory_namespace.create_directory( + return self.process.object_manager.create_directory( &directory_name.original_path, |directory| { let Some(object_attributes) = object_attributes else { @@ -943,7 +1079,7 @@ impl Task { let directory = { match self .process - .directory_namespace + .object_manager .resolve_directory(&directory_name.original_path) { Ok(directory) => directory, @@ -1097,20 +1233,21 @@ impl Task { } } -pub(crate) fn seed_directory_namespace() --> crate::WindowsDirectoryNamespace { - let namespace = DirectoryNamespace::new(); +pub(crate) fn seed_object_manager() +-> crate::WindowsObjectManager { + let object_manager = ObjectManager::new(); for path in SEEDED_DIRECTORY_PATHS { - namespace.seed_directory(path); + object_manager.seed_directory(path); } for (path, target) in SEEDED_SYMLINK_PATHS { - namespace.seed_symlink(path, target); + object_manager.seed_symlink(path, target); } - namespace + object_manager } #[cfg(test)] mod tests { + use alloc::sync::Arc; use core::mem::size_of; use litebox::platform::ThreadProvider; @@ -1119,6 +1256,10 @@ mod tests { use super::*; use crate::nt_types::{ObjectAttributes, ObjectAttributesFlags}; + use crate::syscalls::section::{ + WINDOWS_SESSION_SHARED_SECTION_OBJECT, WINDOWS_SHARED_SECTION_OBJECT, + load_time_windows_shared_section, + }; use crate::tests::{ TestPlatform, const_ptr, mut_ptr, null_mut_ptr, object_attributes, test_task, unicode_string, utf16_units, @@ -1291,6 +1432,31 @@ mod tests { }); } + #[test] + fn windows_shared_section_resolves_to_session_shared_section() { + run_with_test_platform_pointers(|| { + let object_manager = seed_object_manager::(); + let shared_section = load_time_windows_shared_section::(0x10000); + assert_eq!( + object_manager + .create_section(WINDOWS_SESSION_SHARED_SECTION_OBJECT, &shared_section,), + NtStatus::SUCCESS + ); + + let shortcut = object_manager + .resolve_symlink(WINDOWS_SHARED_SECTION_OBJECT, true) + .expect("Windows shared section shortcut is a symbolic link"); + assert_eq!( + shortcut.symlink_target().into_result(), + Ok(WINDOWS_SESSION_SHARED_SECTION_OBJECT.to_string()) + ); + let resolved = object_manager + .resolve_section(WINDOWS_SHARED_SECTION_OBJECT) + .expect("Windows shared section shortcut resolves to session section"); + assert!(Arc::ptr_eq(&resolved, &shared_section)); + }); + } + #[test] fn open_directory_rejects_openlink_attribute() { run_with_test_platform_pointers(|| { diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 7d770e83cb..9e6f1bb026 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -28,6 +28,9 @@ const MEM_PHYSICAL: u32 = 0x0040_0000; const MEM_DIFFERENT_IMAGE_BASE_OK: u32 = 0x0080_0000; const SUPPORTED_MAP_ALLOCATION_TYPES: u32 = MEM_TOP_DOWN | MEM_PHYSICAL | MEM_DIFFERENT_IMAGE_BASE_OK; +pub(crate) const WINDOWS_SHARED_SECTION_OBJECT: &str = r"\Windows\SharedSection"; +pub(crate) const WINDOWS_SESSION_SHARED_SECTION_OBJECT: &str = r"\Sessions\0\Windows\SharedSection"; +pub(crate) const WINDOWS_SHARED_SECTION_SIZE: usize = 0x1_0000; enum SectionBacking { /// LiteBox lacks shared anonymous backing, so a pagefile section is @@ -40,6 +43,13 @@ enum SectionBacking { /// second-concurrent-view and the remap-after-unmap rejects exist. They are /// one missing feature, not two unrelated limitations. Pagefile, + /// CSR shared section is created by kernel and shared across process. For now, + /// we create it in userland for a process during initialization, and thus the first + /// map request would return the pre-mapped address. Subsequent map requests would + /// be rejected as LiteBox lacks shared mapping support. + CsrSharedSection { + base: usize, + }, ImageFile, } @@ -325,7 +335,7 @@ impl Task { _platform: PhantomData, }); if let Some(name) = &name { - let status = self.insert_named_section(name, §ion); + let status = self.process.object_manager.create_section(name, §ion); if status != NtStatus::SUCCESS { return status; } @@ -387,24 +397,26 @@ impl Task { Ok(name) => name, Err(status) => return status, }; - if self - .process - .directory_namespace - .resolve_object(&name) - .is_ok() - { - return NtStatus::OBJECT_TYPE_MISMATCH; - } - if let Some(section) = self.named_section(&name) { - return self.publish_section_handle(section_handle, section, granted_access); + match self.process.object_manager.resolve_section(&name) { + Ok(section) => { + return self.publish_section_handle(section_handle, section, granted_access); + } + Err(NtStatus::OBJECT_NAME_NOT_FOUND | NtStatus::OBJECT_PATH_NOT_FOUND) => {} + Err(status) => return status, } + // TODO: Windows creates one image section per known DLL during boot and lets every process + // map the same section. LiteBox currently lacks a shared image section subsystem, so we create + // a new section for each process that opens a known DLL. let Some(fs_path) = known_dll_section_fs_path(&name) else { return section_missing_status( - self.process - .directory_namespace - .parent_directory_exists(&name), + self.process.object_manager.parent_directory_exists(&name), ); }; + litebox_util_log::debug!( + section_name:% = name, + fs_path:% = fs_path; + "NtOpenSection: creating section for KnownDlls image" + ); let Ok(file_status) = self.fs.file_status(&fs_path) else { return NtStatus::OBJECT_NAME_NOT_FOUND; }; @@ -521,6 +533,13 @@ impl Task { page_protection, permissions, ), + SectionBacking::CsrSharedSection { .. } => self.map_csr_shared_section( + request, + §ion, + requested_view_size, + section_offset, + page_protection, + ), SectionBacking::ImageFile => self.map_image_section(request, §ion, page_protection), } } @@ -549,12 +568,17 @@ impl Task { let Some((view_base, view)) = self.remove_section_view_for_address(base_address) else { return NtStatus::NOT_MAPPED_VIEW; }; - let ptr = MutPtr::::from_usize(view_base); - // SAFETY: Section views are tracked only after this shim successfully creates the pages; - // unmapping consumes the tracked view and removes the exact owned range. - if unsafe { self.global.page_manager.remove_pages(ptr, view.size) }.is_err() { - self.process.section_views.write().insert(view_base, view); - return NtStatus::UNABLE_TO_FREE_VM; + let owns_pages = view.section.as_ref().is_none_or(|section| { + !matches!(section.backing, SectionBacking::CsrSharedSection { .. }) + }); + if owns_pages { + let ptr = MutPtr::::from_usize(view_base); + // SAFETY: Section views are tracked only after this shim successfully creates the pages; + // unmapping consumes the tracked view and removes the exact owned range. + if unsafe { self.global.page_manager.remove_pages(ptr, view.size) }.is_err() { + self.process.section_views.write().insert(view_base, view); + return NtStatus::UNABLE_TO_FREE_VM; + } } self.process.virtual_allocations.write().remove(&view_base); NtStatus::SUCCESS @@ -590,43 +614,6 @@ impl Task { NtStatus::SUCCESS } - fn insert_named_section(&self, name: &str, section: &Arc>) -> NtStatus { - if self - .process - .directory_namespace - .resolve_object(name) - .is_ok() - { - return NtStatus::OBJECT_NAME_COLLISION; - } - if !self - .process - .directory_namespace - .parent_directory_exists(name) - { - return NtStatus::OBJECT_PATH_NOT_FOUND; - } - let key = section_key(name); - let mut namespace = self.process.section_namespace.write(); - if let Some(existing) = namespace.get(&key) - && existing.upgrade().is_some() - { - return NtStatus::OBJECT_NAME_EXISTS; - } - namespace.insert(key, Arc::downgrade(section)); - NtStatus::SUCCESS - } - - fn named_section(&self, name: &str) -> Option>> { - let key = section_key(name); - let mut namespace = self.process.section_namespace.write(); - let section = namespace.get(&key).and_then(alloc::sync::Weak::upgrade); - if section.is_none() { - namespace.remove(&key); - } - section - } - fn map_pagefile_section( &self, request: MapViewOfSectionParameters, @@ -656,7 +643,9 @@ impl Task { }; match section.backing { SectionBacking::Pagefile => {} - SectionBacking::ImageFile => return NtStatus::INVALID_FILE_FOR_SECTION, + SectionBacking::CsrSharedSection { .. } | SectionBacking::ImageFile => { + return NtStatus::INVALID_FILE_FOR_SECTION; + } } if !pagefile_view_protection_is_compatible(section.protection, page_protection) { litebox_util_log::debug!( @@ -718,6 +707,83 @@ impl Task { NtStatus::SUCCESS } + fn map_csr_shared_section( + &self, + request: MapViewOfSectionParameters, + section: &Arc>, + requested_view_size: usize, + section_offset: usize, + page_protection: PageProtection, + ) -> NtStatus { + if section_offset != 0 { + return NtStatus::INVALID_VIEW_SIZE; + } + let view_size = if requested_view_size == 0 { + section.size + } else { + requested_view_size + }; + if view_size == 0 || view_size > section.size { + return NtStatus::INVALID_VIEW_SIZE; + } + let Some(mapped_size) = view_size.checked_next_multiple_of(PAGE_SIZE) else { + return NtStatus::INVALID_VIEW_SIZE; + }; + if mapped_size > WINDOWS_SHARED_SECTION_SIZE { + litebox_util_log::debug!( + section_size = section.size, + requested_view_size, + section_offset; + "Rejected CSR shared section view larger than host limit" + ); + return NtStatus::INVALID_VIEW_SIZE; + } + if !pagefile_view_protection_is_compatible(section.protection, page_protection) { + return NtStatus::SECTION_PROTECTION; + } + if section.pagefile_view_active.swap(true, Ordering::AcqRel) { + litebox_util_log::debug!( + section_size = section.size, + requested_view_size, + section_offset; + "Rejected additional CSR shared section view" + ); + return NtStatus::NOT_SUPPORTED; + } + // TODO: we just return the pre-mapped base address for now, but we should support mapping at a different base address in the future. + let base = match section.backing { + SectionBacking::CsrSharedSection { base } => base, + SectionBacking::Pagefile | SectionBacking::ImageFile => unreachable!(), + }; + if request.base_address.write_at_offset(0, base).is_none() + || request.view_size.write_at_offset(0, view_size).is_none() + { + section.pagefile_view_active.store(false, Ordering::Release); + return NtStatus::ACCESS_VIOLATION; + } + self.process.section_views.write().insert( + base, + WindowsSectionView { + size: mapped_size, + section_offset, + section: Some(Arc::clone(section)), + }, + ); + self.process.virtual_allocations.write().insert( + base, + crate::WindowsVirtualAllocation { + base, + size: mapped_size, + allocation_protect: section.protection, + type_: MemoryType::MEM_MAPPED, + // TODO(section-subsystem): honor per-view CSR protections only after + // the backing is no longer aliased by PEB direct-deref pointers. + pages: committed_pages(base, mapped_size, section.protection), + }, + ); + NtStatus::SUCCESS + } + fn map_image_section( &self, request: MapViewOfSectionParameters, @@ -822,10 +888,6 @@ impl Task { } } -fn section_key(path: &str) -> String { - path.to_ascii_lowercase() -} - fn section_missing_status(parent_exists: bool) -> NtStatus { if parent_exists { NtStatus::OBJECT_NAME_NOT_FOUND @@ -851,11 +913,32 @@ fn known_dll_section_fs_path(object_path: &str) -> Option { Some(fs_path) } +pub(crate) fn load_time_windows_shared_section( + base: usize, +) -> Arc> { + // CSRSS creates this named section for the CSR client/server contract. LiteBox + // synthesizes it from the same static server data shape used for the PEB CSR + // pointers instead of exposing a zeroed generic pagefile section. + Arc::new(SectionObject { + fs_path: None, + size: WINDOWS_SHARED_SECTION_SIZE, + attributes: SectionAllocationAttributes::SEC_COMMIT, + protection: PageProtection::PAGE_READWRITE, + backing: SectionBacking::CsrSharedSection { base }, + pagefile_view_active: AtomicBool::new(false), + _platform: PhantomData, + }) +} + fn strip_case_insensitive_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { - value + if value .get(..prefix.len()) .is_some_and(|head| head.eq_ignore_ascii_case(prefix)) - .then_some(&value[prefix.len()..]) + { + value.get(prefix.len()..) + } else { + None + } } fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool { @@ -1058,11 +1141,12 @@ mod tests { use core::mem::{size_of, size_of_val}; - use litebox::platform::RawMutPointer as _; + use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox_common_windows::nt_status::NtStatus; use super::*; use crate::nt_types::{ObjectAttributes, UnicodeString}; + use crate::syscalls::event::EventType; use crate::tests::{TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task}; #[cfg(all(target_os = "windows", target_arch = "x86_64"))] @@ -1614,4 +1698,39 @@ mod tests { assert_ne!(opened, Handle::default()); assert_ne!(opened, created); } + + #[test] + fn event_and_section_names_collide_in_object_namespace() { + let task = test_task(); + let name = wide(r"\BaseNamedObjects\LiteBoxSharedLeafName"); + let unicode = unicode(&name); + let attrs = object_attributes(&unicode); + let mut event = Handle::default(); + assert_eq!( + task.sys_nt_create_event( + mut_ptr(&mut event), + 0x001f_0003, + Some(const_ptr(&attrs)), + EventType::Notification as u32, + 0, + ), + NtStatus::SUCCESS + ); + + let size = 0x1000i64; + let mut section = Handle::from_raw(0xffff_ffff); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut section), + SectionAccess::ALL_ACCESS.bits(), + Some(const_ptr(&attrs)), + Some(const_ptr(&size)), + PageProtection::PAGE_READWRITE.bits(), + SectionAllocationAttributes::SEC_COMMIT.bits(), + Handle::default(), + ), + NtStatus::OBJECT_NAME_EXISTS + ); + assert_eq!(section, Handle::from_raw(0xffff_ffff)); + } } diff --git a/litebox_shim_windows/src/syscalls/symlink.rs b/litebox_shim_windows/src/syscalls/symlink.rs index a9c3f115ca..1b7fb4afe5 100644 --- a/litebox_shim_windows/src/syscalls/symlink.rs +++ b/litebox_shim_windows/src/syscalls/symlink.rs @@ -15,8 +15,8 @@ use litebox::utils::TruncateExt as _; use litebox_common_windows::nt_status::NtStatus; use crate::nt_types::{AccessMask, ObjectAttributes, ObjectAttributesFlags, UnicodeString}; -use crate::syscalls::directory::ObjectNode; -use crate::syscalls::{Handle, directory::DirectoryName}; +use crate::syscalls::Handle; +use crate::syscalls::object_manager::{DirectoryName, ObjectNode}; use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; const STANDARD_RIGHTS_REQUIRED: u32 = AccessMask::DELETE.bits() @@ -163,7 +163,7 @@ impl Task { target: String, open_if: bool, ) -> NtStatus { - self.process.directory_namespace.create_symlink( + self.process.object_manager.create_symlink( &link_name.original_path, target, |link| { @@ -209,7 +209,7 @@ impl Task { }; let link = match self .process - .directory_namespace + .object_manager .resolve_symlink(&link_name.original_path, true) { Ok(link) => link, @@ -252,8 +252,7 @@ impl Task { return status; } - let target = entry.with_entry(|entry| entry.link.symlink_target()); - let target = match target { + let target = match entry.with_entry(|entry| entry.link.symlink_target().into_result()) { Ok(target) => target, Err(status) => return status, }; diff --git a/litebox_shim_windows/src/syscalls/sysinfo.rs b/litebox_shim_windows/src/syscalls/sysinfo.rs index 902ea224c5..71e57290d2 100644 --- a/litebox_shim_windows/src/syscalls/sysinfo.rs +++ b/litebox_shim_windows/src/syscalls/sysinfo.rs @@ -36,6 +36,17 @@ const SYSTEM_VERIFIER_INFORMATION_LENGTH: u32 = 0x90; const SYSTEM_VERIFIER_INFORMATION_LENGTH_USIZE: usize = 0x90; const X64_SYSTEM_RANGE_START: usize = 0xffff_8000_0000_0000; +pub(crate) const WINDOWS_TIME_ZONE_ID_INVALID: u32 = u32::MAX; +pub(crate) const WINDOWS_OS_MAJOR_VERSION: u16 = 10; +pub(crate) const WINDOWS_OS_MINOR_VERSION: u16 = 0; +pub(crate) const WINDOWS_OS_BUILD_NUMBER: u16 = 19041; +pub(crate) const WINDOWS_OS_PLATFORM_WIN32_NT: u32 = 2; +#[cfg(not(target_os = "windows"))] +pub(crate) const WINDOWS_NT_PRODUCT_WORKSTATION: u32 = 1; +pub(crate) const WINDOWS_DIRECTORY: &str = r"C:\Windows"; +pub(crate) const WINDOWS_SYSTEM_DIRECTORY: &str = r"C:\Windows\System32"; +pub(crate) const WINDOWS_NAMED_OBJECT_DIRECTORY: &str = r"\BaseNamedObjects"; + #[repr(u32)] #[derive(Clone, Copy, Debug, Eq, PartialEq, IntEnum)] enum SystemInformationClass { diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 2583ab0396..89c21357bd 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -3,24 +3,17 @@ extern crate std; -use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; -use core::marker::PhantomData; use core::mem::size_of; -use core::sync::atomic::{AtomicI32, AtomicU32}; use litebox::LiteBox; -use litebox::fd::RawDescriptorStorage; use litebox::fs::{FileSystem as _, Mode, OFlags}; use litebox::platform::RawConstPointer as _; use litebox::utils::TruncateExt as _; use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::syscalls::Handle; -use crate::{ - ConstPtr, DefaultFS, GlobalState, MutPtr, Process, Task, WindowsHandleStore, - WindowsNlsSectionMappings, WindowsPageManager, -}; +use crate::{ConstPtr, DefaultFS, MutPtr, Process, Task, WindowsShim}; #[cfg(target_os = "linux")] pub(crate) type TestPlatform = litebox_platform_linux_userland::LinuxUserland; @@ -96,7 +89,6 @@ pub(crate) fn test_task() -> Task { pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task { let platform = test_platform(); let litebox = LiteBox::new(platform); - let page_manager = WindowsPageManager::::new(&litebox); let mut in_mem = litebox::fs::in_mem::FileSystem::new(&litebox); in_mem.with_root_privileges(|fs| { fs.mkdir( @@ -138,36 +130,19 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task::new(platform); + let fs = Arc::new(shim_builder.default_fs(in_mem, tar_ro)); + let shim = shim_builder.build(); + let WindowsShim(global) = shim; + + let windows_shared_section_base = crate::map_csr_server_shared_memory(&global.page_manager) + .expect("mapping shared memory should succeed"); + let windows_shared_section = + crate::syscalls::section::load_time_windows_shared_section(windows_shared_section_base); + Task { - global: Arc::new(GlobalState { - platform, - registry: crate::syscalls::registry::RegistryStore::new(&litebox), - qpc_boot_instant: litebox::platform::TimeProvider::now(platform), - litebox, - page_manager, - _fs: PhantomData, - }), - process: Arc::new(Process { - ntdll_mapping: None, - peb_address: 0, - handles: WindowsHandleStore::::new(RawDescriptorStorage::new()), - directory_namespace, - event_namespace: crate::WindowsEventNamespace::::new(BTreeMap::new()), - section_namespace: crate::WindowsSectionNamespace::::new(BTreeMap::new()), - section_views: crate::WindowsSectionViews::::new(BTreeMap::new()), - nls_section_mappings: WindowsNlsSectionMappings::::new(BTreeMap::new()), - virtual_allocations: crate::WindowsVirtualAllocations::::new( - BTreeMap::new(), - ), - system_lcid: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), - user_lcid: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), - user_ui_language: AtomicU32::new(crate::syscalls::nls::DEFAULT_LOCALE_ID), - default_hard_error_mode: AtomicU32::new(0), - cookie: crate::syscalls::process::default_process_cookie(), - exit_code: AtomicI32::new(0), - }), + global, + process: Arc::new(Process::default(None, windows_shared_section)), fs, entry_point: 0, stack_top: 0, From 045510dc176c57fe95711aab51ffd1238d126cc0 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Fri, 10 Jul 2026 13:04:29 -0700 Subject: [PATCH 091/319] Cherry pick "Update for Rust 1.97.0" (#1020) Cherry picking PR #1019. It also fixes two places of the Windows shim. --------- Co-authored-by: Sangho Lee --- litebox/src/fs/layered.rs | 6 +++--- litebox/src/fs/tests.rs | 2 +- litebox/src/mm/linux.rs | 4 ++-- litebox_platform_windows_userland/src/lib.rs | 2 +- .../src/lib.rs | 9 ++------- litebox_runner_linux_userland/src/lib.rs | 9 ++------- litebox_shim_optee/src/loader/ta_stack.rs | 17 +++++++---------- litebox_shim_windows/src/lib.rs | 2 +- litebox_shim_windows/src/syscalls/section.rs | 5 ++--- 9 files changed, 21 insertions(+), 35 deletions(-) diff --git a/litebox/src/fs/layered.rs b/litebox/src/fs/layered.rs index f0689af44b..00fb055849 100644 --- a/litebox/src/fs/layered.rs +++ b/litebox/src/fs/layered.rs @@ -157,11 +157,11 @@ impl { - return Err(e)?; + Err(e)?; } Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { assert_ne!(dir, path); - return Err(PathError::MissingComponent)?; + Err(PathError::MissingComponent)?; } Err(FileStatusError::Io) => return Err(MkdirError::Io), } @@ -492,7 +492,7 @@ impl< // remove the tombstone though. tombstone_removal = true; } else { - return Err(PathError::NoSuchFileOrDirectory)?; + Err(PathError::NoSuchFileOrDirectory)?; } } EntryX::Upper { .. } => unreachable!(), diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index a597678730..011aed6e3d 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -1482,7 +1482,7 @@ mod layered { fs.close(&fd).expect("Failed to close upperdir"); // only . and .. - assert!(entries.len() == 2); + assert_eq!(entries.len(), 2); } #[test] diff --git a/litebox/src/mm/linux.rs b/litebox/src/mm/linux.rs index 951519d464..37a7965dae 100644 --- a/litebox/src/mm/linux.rs +++ b/litebox/src/mm/linux.rs @@ -916,8 +916,8 @@ impl + 'static, const ALIGN: usize> Vmem Platform::TASK_ADDR_MIN, Platform::TASK_ADDR_MAX - length.as_usize(), ); - debug_assert!(Platform::TASK_ADDR_MIN % ALIGN == 0); - debug_assert!(Platform::TASK_ADDR_MAX % ALIGN == 0); + debug_assert_eq!(Platform::TASK_ADDR_MIN % ALIGN, 0); + debug_assert_eq!(Platform::TASK_ADDR_MAX % ALIGN, 0); let last_end = self.vmas.last_range_value().map_or(low_limit, |r| r.0.end); if last_end <= high_limit { return Some(high_limit); diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index d8cc6c005b..62336675f5 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -1205,7 +1205,7 @@ fn set_context_to_interrupt_callback( ) { let required_flags = windows_sys::Win32::System::Diagnostics::Debug::CONTEXT_CONTROL_AMD64 | windows_sys::Win32::System::Diagnostics::Debug::CONTEXT_INTEGER_AMD64; - assert!(context.ContextFlags & required_flags == required_flags); + assert_eq!(context.ContextFlags & required_flags, required_flags); context.Rip = interrupt_callback as *const () as usize as u64; context.Rsp = tls.host_sp.get().addr() as u64; context.Rbp = tls.host_bp.get().addr() as u64; diff --git a/litebox_runner_linux_on_windows_userland/src/lib.rs b/litebox_runner_linux_on_windows_userland/src/lib.rs index 658b20ca55..e0d5596c6b 100644 --- a/litebox_runner_linux_on_windows_userland/src/lib.rs +++ b/litebox_runner_linux_on_windows_userland/src/lib.rs @@ -107,13 +107,8 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let envp = if cli_args.forward_environment_variables { envp.into_iter() .chain(std::env::vars().map(|(k, v)| { - std::ffi::CString::new( - k.bytes() - .chain([b'=']) - .chain(v.bytes()) - .collect::>(), - ) - .unwrap() + std::ffi::CString::new(k.bytes().chain(*b"=").chain(v.bytes()).collect::>()) + .unwrap() })) .collect() } else { diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 310efe88bb..9b5495b81f 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -420,13 +420,8 @@ pub fn run(cli_args: CliArgs) -> Result<()> { let envp = if cli_args.forward_environment_variables { envp.into_iter() .chain(std::env::vars().map(|(k, v)| { - std::ffi::CString::new( - k.bytes() - .chain([b'=']) - .chain(v.bytes()) - .collect::>(), - ) - .unwrap() + std::ffi::CString::new(k.bytes().chain(*b"=").chain(v.bytes()).collect::>()) + .unwrap() })) .collect() } else { diff --git a/litebox_shim_optee/src/loader/ta_stack.rs b/litebox_shim_optee/src/loader/ta_stack.rs index 16ab1e9149..a0057a78b7 100644 --- a/litebox_shim_optee/src/loader/ta_stack.rs +++ b/litebox_shim_optee/src/loader/ta_stack.rs @@ -191,17 +191,14 @@ impl TaStack { } match param_type { TeeParamType::MemrefInput | TeeParamType::MemrefInout => { - if let Some(bytes) = bytes { - if len > bytes.len() { - self.pos = self.pos.checked_sub(len - bytes.len())?; - } - self.push_bytes(bytes)?; - self.params - .set_values(self.num_params, self.get_cur_stack_top() as u64, len as u64) - .ok()?; - } else { - return None; + let bytes = bytes?; + if len > bytes.len() { + self.pos = self.pos.checked_sub(len - bytes.len())?; } + self.push_bytes(bytes)?; + self.params + .set_values(self.num_params, self.get_cur_stack_top() as u64, len as u64) + .ok()?; } TeeParamType::MemrefOutput => { self.pos = self.pos.checked_sub(len)?; diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index b91b797352..2e3a098302 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -575,7 +575,7 @@ impl Task { } ctx.rip = self.entry_point; - debug_assert!(self.stack_top % 16 == core::mem::size_of::()); + debug_assert_eq!(self.stack_top % 16, core::mem::size_of::()); ctx.rsp = self.stack_top; ctx.eflags = 0x202; ctx.rcx = self.context; diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 9e6f1bb026..3cf97c2678 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -900,10 +900,9 @@ fn known_dll_section_fs_path(object_path: &str) -> Option { let (dll_name, fs_directory) = if let Some(rest) = strip_case_insensitive_prefix(object_path, r"\KnownDlls\") { (rest, "/Windows/System32/") - } else if let Some(rest) = strip_case_insensitive_prefix(object_path, r"\KnownDlls32\") { - (rest, "/Windows/SysWOW64/") } else { - return None; + let rest = strip_case_insensitive_prefix(object_path, r"\KnownDlls32\")?; + (rest, "/Windows/SysWOW64/") }; if dll_name.contains(['\\', '/']) || !ends_with_ignore_ascii_case(dll_name, ".dll") { return None; From 29126e4528347838ca5a7c1c6a1cfea813b83f26 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Fri, 10 Jul 2026 20:35:49 -0700 Subject: [PATCH 092/319] Cherry pick "Refactor: Use physical pointer abstraction in LVBS" (#1022) Co-authored-by: Sangho Lee --- litebox_common_linux/src/lib.rs | 1 + litebox_common_linux/src/physical_pointers.rs | 533 ++++++++++++++++ litebox_common_linux/src/vmap.rs | 129 ++-- litebox_common_optee/src/lib.rs | 3 +- litebox_platform_linux_userland/src/lib.rs | 19 +- litebox_platform_lvbs/Cargo.toml | 3 +- .../src/arch/x86/mm/paging.rs | 2 - litebox_platform_lvbs/src/lib.rs | 371 +++-------- litebox_platform_lvbs/src/mm/mod.rs | 2 - litebox_platform_lvbs/src/mshv/ringbuffer.rs | 127 +++- litebox_platform_lvbs/src/mshv/vsm.rs | 162 ++--- litebox_platform_multiplex/Cargo.toml | 2 +- litebox_runner_lvbs/src/lib.rs | 29 +- litebox_shim_optee/src/lib.rs | 19 +- litebox_shim_optee/src/msg_handler.rs | 26 +- litebox_shim_optee/src/ptr.rs | 585 ------------------ 16 files changed, 960 insertions(+), 1053 deletions(-) create mode 100644 litebox_common_linux/src/physical_pointers.rs delete mode 100644 litebox_shim_optee/src/ptr.rs diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 5dee9a26f6..f8c4f481bd 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -21,6 +21,7 @@ use crate::signal::SigSet; pub mod errno; pub mod loader; pub mod mm; +pub mod physical_pointers; pub mod signal; pub mod vmap; diff --git a/litebox_common_linux/src/physical_pointers.rs b/litebox_common_linux/src/physical_pointers.rs new file mode 100644 index 0000000000..1bde38e6e5 --- /dev/null +++ b/litebox_common_linux/src/physical_pointers.rs @@ -0,0 +1,533 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Physical Pointer Abstraction with On-demand Mapping +//! +//! This module supports accessing foreign physical addresses (e.g., VTL0 +//! or normal-world physical memory) from LiteBox with on-demand mapping. +//! In the context of LVBS and OP-TEE, accessing physical memory is +//! necessary because VTL0 and VTL1 as well as normal world and secure +//! world exchange data using physical addresses. +//! +//! The safe read/write APIs in this module follow the same safety model as +//! safe wrappers around DMA buffers or shared physical memory. The +//! physical memory is external to Rust's ordinary ownership model and may +//! be changed by hardware or another privilege level. These APIs remain +//! safe because they do not create Rust references into that external +//! memory; they only perform bounded copies between a temporary mapping +//! and memory owned by LiteBox. +//! +//! The safe APIs validate that a physical address is foreign before it is +//! mapped. Accessing LiteBox's own memory through this physical pointer +//! abstraction is prohibited to avoid confused-deputy attacks and to ensure +//! Rust memory safety. In the case of LVBS, LiteBox obtains the physical memory +//! information from VTL0, including the physical memory range assigned to +//! VTL1/LiteBox. Thus, the platform can reject any address that belongs to +//! VTL1's physical memory. +//! +//! Beyond that validation, the platform enforces strict PA/VA separation. On +//! LVBS (see the address-space layout in `litebox_platform_lvbs/src/lib.rs`), +//! VTL1-owned PA is mapped only in the VTL1 kernel VA region, while foreign PA +//! is mapped only in the dedicated direct-map or on-demand vmap VA regions. +//! Those foreign VA ranges are fully disjoint from the VTL1 kernel region where +//! all LiteBox/Rust code and data live, so a raw pointer into a foreign physical +//! mapping cannot alias any Rust reference. + +use crate::vmap::{ + GlobalVmapManager, PhysPageAddr, PhysPageMapInfo, PhysPageMapPermissions, PhysPointerError, + VmapManager, +}; +use core::marker::PhantomData; +use zerocopy::{FromBytes, IntoBytes}; + +/// The concrete [`PhysPageMapInfo`] produced by the `VmapManager` behind a [`GlobalVmapManager`]. +type MapInfoOf = + <>::Manager as VmapManager>::MapInfo; + +/// Allocate a zeroed `Box` on the heap. +/// +/// # Panics +/// +/// Panics if `T` is a zero-sized type, since `alloc_zeroed` with a zero-sized +/// layout is undefined behavior. +fn box_new_zeroed() -> alloc::boxed::Box { + assert!( + core::mem::size_of::() > 0, + "box_new_zeroed does not support zero-sized types" + ); + let layout = core::alloc::Layout::new::(); + // Safety: layout has a non-zero size and correct alignment for T. + let ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }.cast::(); + if ptr.is_null() { + alloc::alloc::handle_alloc_error(layout); + } + // Safety: ptr is a valid, zeroed, properly aligned heap allocation for T. + // T: FromBytes guarantees all-zero is a valid bit pattern. + unsafe { alloc::boxed::Box::from_raw(ptr) } +} + +#[inline] +fn align_down(address: usize, align: usize) -> usize { + address & !(align - 1) +} + +/// Represent a physical pointer to an object with on-demand mapping. +/// +/// Safe methods on this type copy to or from a temporary mapping. They never expose +/// references or slices into the mapped physical memory. +/// +/// Read methods require `T: FromBytes` because external memory may contain any bit pattern. +/// Write methods require `T: IntoBytes` because values are written by copying their byte +/// representation. +/// +/// - `pages`: An array of page-aligned physical addresses. We expect physical addresses in this array are +/// virtually contiguous. +/// - `offset`: The offset within `pages[0]` where the object starts. It should be smaller than `ALIGN`. +/// - `count`: The number of objects of type `T` that can be accessed from this pointer. +/// - `T`: The type of the object being pointed to. `pages` with respect to `offset` should cover enough +/// memory for an object of type `T`. +#[repr(C)] +pub struct PhysMutPtr> { + pages: alloc::boxed::Box<[PhysPageAddr]>, + offset: usize, + count: usize, + _type: PhantomData, + _vmap: PhantomData, +} + +impl PhysMutPtr +where + V: GlobalVmapManager, +{ + /// Compile-time guard rejecting zero-sized types. + /// + /// A physical pointer names a region of foreign memory to copy bytes to or from. + /// ZST has no byte representation and thus has no referent in foreign memory. + const ASSERT_NON_ZST: () = assert!( + core::mem::size_of::() != 0, + "PhysMutPtr does not support zero-sized types" + ); + + /// Create a new `PhysMutPtr` from the given physical page array and offset. + /// + /// All addresses in `pages` should be valid and aligned to `ALIGN`, and `offset` should be + /// smaller than `ALIGN`. Also, `pages` should contain enough pages to cover at least one + /// object of type `T` starting from `offset`. If these conditions are not met, this function + /// returns `Err(PhysPointerError)`. + /// + /// Note: `T` does not need to satisfy `align_of::()` at its location in (foreign) physical + /// memory. This is sound because the foreign `T` is never dereferenced as a Rust reference or + /// via a typed load/store: all access goes through `copy_in`/`copy_out`, which cast the + /// mapped pointer to `*mut u8` and perform a byte-granular, unaligned-safe `memcpy_fallible`. + pub fn new(pages: &[PhysPageAddr], offset: usize) -> Result { + Self::from_boxed(pages.into(), offset) + } + + /// Create a new `PhysMutPtr` from an owned page list, consuming it without copying. + fn from_boxed( + pages: alloc::boxed::Box<[PhysPageAddr]>, + offset: usize, + ) -> Result { + // Force evaluation of the compile-time ZST guard. + let () = Self::ASSERT_NON_ZST; + if offset >= ALIGN { + return Err(PhysPointerError::InvalidBaseOffset(offset, ALIGN)); + } + let size = if pages.is_empty() { + 0 + } else { + pages + .len() + .checked_mul(ALIGN) + .ok_or(PhysPointerError::Overflow)? + - offset + }; + if size < core::mem::size_of::() { + return Err(PhysPointerError::InsufficientPhysicalPages( + size, + core::mem::size_of::(), + )); + } + V::manager().validate_unowned(&pages)?; + Ok(Self { + offset, + count: size / core::mem::size_of::(), + pages, + _type: PhantomData, + _vmap: PhantomData, + }) + } + + /// Create a new `PhysMutPtr` from the given contiguous physical address and length. + /// + /// This is a shortcut for + /// `PhysMutPtr::new([align_down(pa), align_down(pa) + ALIGN, ..., align_up(pa + bytes) - ALIGN], pa % ALIGN)`. + pub fn with_contiguous_pages(pa: usize, bytes: usize) -> Result { + if bytes < core::mem::size_of::() { + return Err(PhysPointerError::InsufficientPhysicalPages( + bytes, + core::mem::size_of::(), + )); + } + let start_page = align_down(pa, ALIGN); + let end_page = pa + .checked_add(bytes) + .and_then(|end| end.checked_next_multiple_of(ALIGN)) + .ok_or(PhysPointerError::Overflow)?; + let span = end_page + .checked_sub(start_page) + .ok_or(PhysPointerError::Overflow)?; + let mut pages = alloc::vec::Vec::with_capacity(span / ALIGN); + let mut current_page = start_page; + while current_page < end_page { + pages.push( + PhysPageAddr::::new(current_page) + .ok_or(PhysPointerError::InvalidPhysicalAddress(current_page))?, + ); + current_page = current_page + .checked_add(ALIGN) + .ok_or(PhysPointerError::Overflow)?; + } + // reuse the allocation + Self::from_boxed(pages.into_boxed_slice(), pa - start_page) + } + + /// Create a new `PhysMutPtr` from the given physical address for a single object. + /// + /// This is a shortcut for `PhysMutPtr::with_contiguous_pages(pa, size_of::())`. + /// + /// Note: This module doesn't provide `as_usize` because LiteBox should not dereference physical addresses directly. + pub fn with_usize(pa: usize) -> Result { + Self::with_contiguous_pages(pa, core::mem::size_of::()) + } + + /// Read the value at the given offset from the physical pointer. + /// + /// Returns an owned copy of the value read from physical memory. + pub fn read_at_offset(&self, count: usize) -> Result, PhysPointerError> + where + T: FromBytes, + { + if count >= self.count { + return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); + } + let guard = self.map_and_get_ptr_guard( + count, + core::mem::size_of::(), + PhysPageMapPermissions::READ, + )?; + let mut boxed = box_new_zeroed::(); + // SAFETY: `boxed` is a freshly allocated `T` and is thus valid for writes + // of `size_of::()` bytes, which is the guard's mapped size. + unsafe { guard.copy_out(core::ptr::from_mut::(boxed.as_mut()).cast::())? }; + Ok(boxed) + } + + /// Read a slice of values at the given offset from the physical pointer. + /// + /// Copies values from physical memory into the caller-provided slice. + pub fn read_slice_at_offset( + &self, + count: usize, + values: &mut [T], + ) -> Result<(), PhysPointerError> + where + T: FromBytes, + { + if count + .checked_add(values.len()) + .is_none_or(|end| end > self.count) + { + return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); + } + if values.is_empty() { + if count >= self.count { + return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); + } + return Ok(()); + } + let guard = self.map_and_get_ptr_guard( + count, + core::mem::size_of_val(values), + PhysPageMapPermissions::READ, + )?; + // SAFETY: `values` is valid for writes of `size_of_val(values)` bytes, which is + // the guard's mapped size. + // + // If `copy_out` fails (e.g., concurrent unmap), `values` can be partially + // overwritten. This is sound because `T: FromBytes` ensures every byte pattern + // is a valid, initialized `T` - there is no element in an undefined state. + unsafe { guard.copy_out(values.as_mut_ptr().cast::())? }; + Ok(()) + } + + /// Write the value at the given offset to the physical pointer. + pub fn write_at_offset(&self, count: usize, value: T) -> Result<(), PhysPointerError> + where + T: IntoBytes, + { + if count >= self.count { + return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); + } + let guard = self.map_and_get_ptr_guard( + count, + core::mem::size_of::(), + PhysPageMapPermissions::READ | PhysPageMapPermissions::WRITE, + )?; + // SAFETY: `value` is valid for reads of `size_of::()` bytes, which is the + // guard's mapped size. + unsafe { guard.copy_in(core::ptr::from_ref(&value).cast::())? }; + Ok(()) + } + + /// Write a slice of values at the given offset to the physical pointer. + pub fn write_slice_at_offset(&self, count: usize, values: &[T]) -> Result<(), PhysPointerError> + where + T: IntoBytes, + { + if count + .checked_add(values.len()) + .is_none_or(|end| end > self.count) + { + return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); + } + if values.is_empty() { + if count >= self.count { + return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); + } + return Ok(()); + } + let guard = self.map_and_get_ptr_guard( + count, + core::mem::size_of_val(values), + PhysPageMapPermissions::READ | PhysPageMapPermissions::WRITE, + )?; + // SAFETY: `values` is valid for reads of `size_of_val(values)` bytes, which is + // the guard's mapped size. + unsafe { guard.copy_in(values.as_ptr().cast::())? }; + Ok(()) + } + + /// This function maps physical pages for the requested data element at a given + /// index and returns a guard that unmaps on drop. + /// + /// It bridges element-level access (used by `read_at_offset`, `write_at_offset`, etc.) + /// with page-level mapping. It determines which physical pages contain the requested + /// element, maps them into virtual memory, and returns a pointer adjusted for + /// the element's position. + /// + /// - `count`: Element index (0-based) within this physical pointer's range. + /// - `size`: Total byte size to map (must cover the data being accessed). + /// - `perms`: Required page permissions (read, write). + /// + /// The returned guard is tied to `self`'s lifetime and releases the mapping when it + /// goes out of scope. + fn map_and_get_ptr_guard( + &self, + count: usize, + size: usize, + perms: PhysPageMapPermissions, + ) -> Result, PhysPointerError> { + let skip = self + .offset + .checked_add( + count + .checked_mul(core::mem::size_of::()) + .ok_or(PhysPointerError::Overflow)?, + ) + .ok_or(PhysPointerError::Overflow)?; + let start = skip / ALIGN; + let end = skip + .checked_add(size) + .ok_or(PhysPointerError::Overflow)? + .div_ceil(ALIGN); + let map_info = self.map_range(start, end, perms)?; + let ptr = map_info.base().wrapping_add(skip % ALIGN).cast::(); + Ok(MappedGuard { + map_info: Some(map_info), + ptr, + size, + _owner: PhantomData, + }) + } + + /// Map the physical pages from `start` to `end` indexes. + fn map_range( + &self, + start: usize, + end: usize, + perms: PhysPageMapPermissions, + ) -> Result, PhysPointerError> { + if start >= end || end > self.pages.len() { + return Err(PhysPointerError::IndexOutOfBounds(end, self.pages.len())); + } + let accept_perms = PhysPageMapPermissions::READ | PhysPageMapPermissions::WRITE; + if perms.bits() & !accept_perms.bits() != 0 { + return Err(PhysPointerError::UnsupportedPermissions(perms.bits())); + } + let sub_pages = &self.pages[start..end]; + // SAFETY: `PhysMutPtr::new` validated these pages as foreign via `validate_unowned`. + // The platform `VmapManager` must map them only in a foreign-memory VA range, disjoint + // from LiteBox-owned Rust objects. This caller never creates Rust references from the + // returned pointer; `MappedGuard` uses it only for fault-tolerant raw byte copies. + unsafe { V::manager().vmap(sub_pages, perms) } + } +} + +/// RAII guard that unmaps physical pages when dropped. +/// +/// Created by `map_and_get_ptr_guard`. Its lifetime is tied to the parent +/// `PhysMutPtr`, and it owns the map info for the duration of the temporary mapping. +/// +/// # Invariant +/// +/// `ptr` points into the live mapping owned by `map_info`, and the `size` bytes starting +/// at `ptr` lie within that mapping. The mapping refers to foreign (non-Rust) physical +/// memory that another core may unmap concurrently, so `ptr` must only ever be accessed +/// through [`Self::copy_in`]/[`Self::copy_out`], which perform fault-tolerant copies. +struct MappedGuard<'a, T, const ALIGN: usize, V: GlobalVmapManager> { + map_info: Option>, + ptr: *mut T, + size: usize, + _owner: PhantomData<&'a PhysMutPtr>, +} + +impl> MappedGuard<'_, T, ALIGN, V> { + /// Copy the `self.size` mapped bytes out into `dst`. + /// + /// This is the only path through which the raw mapped pointer is dereferenced. + /// + /// # Safety + /// + /// `dst` must be valid for writes of `self.size` bytes. + unsafe fn copy_out(&self, dst: *mut u8) -> Result<(), PhysPointerError> { + // Fallible: another core may unmap this page concurrently. + let result = unsafe { + litebox::mm::exception_table::memcpy_fallible(dst, self.ptr.cast::(), self.size) + }; + debug_assert!(result.is_ok(), "fault reading from mapped physical page"); + result.map_err(|_| PhysPointerError::CopyFailed) + } + + /// Copy `self.size` bytes from `src` into the mapped memory. + /// + /// This is the only path through which the raw mapped pointer is dereferenced. + /// + /// # Safety + /// + /// `src` must be valid for reads of `self.size` bytes. + unsafe fn copy_in(&self, src: *const u8) -> Result<(), PhysPointerError> { + // Fallible: another core may unmap this page concurrently. + let result = unsafe { + litebox::mm::exception_table::memcpy_fallible(self.ptr.cast::(), src, self.size) + }; + debug_assert!(result.is_ok(), "fault writing to mapped physical page"); + result.map_err(|_| PhysPointerError::CopyFailed) + } +} + +impl> Drop for MappedGuard<'_, T, ALIGN, V> { + fn drop(&mut self) { + // SAFETY: The platform is expected to handle unmapping safely. Drop cannot + // report errors. If unmapping fails, drop the returned private map_info; + // platform-specific resources that cannot be reclaimed are handled by the + // platform `vunmap` implementation. + if let Some(map_info) = self.map_info.take() { + let _ = unsafe { V::manager().vunmap(map_info) }; + } + } +} + +impl> core::fmt::Debug + for PhysMutPtr +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PhysMutPtr") + .field("pages[0]", &self.pages.first().map_or(0, |p| p.as_usize())) + .field("offset", &self.offset) + .finish_non_exhaustive() + } +} + +/// Represent a physical pointer to a read-only object. This wraps around [`PhysMutPtr`] and +/// exposes only copy-out access. +#[repr(C)] +pub struct PhysConstPtr> { + inner: PhysMutPtr, +} + +impl PhysConstPtr +where + V: GlobalVmapManager, +{ + /// Create a new `PhysConstPtr` from the given physical page array and offset. + /// + /// All addresses in `pages` should be valid and aligned to `ALIGN`, and `offset` should be smaller + /// than `ALIGN`. Also, `pages` should contain enough pages to cover at least one object of + /// type `T` starting from `offset`. If these conditions are not met, this function returns + /// `Err(PhysPointerError)`. + pub fn new(pages: &[PhysPageAddr], offset: usize) -> Result { + Ok(Self { + inner: PhysMutPtr::new(pages, offset)?, + }) + } + + /// Create a new `PhysConstPtr` from the given contiguous physical address and length. + /// + /// This is a shortcut for + /// `PhysConstPtr::new([align_down(pa), align_down(pa) + ALIGN, ..., align_up(pa + bytes) - ALIGN], pa % ALIGN)`. + pub fn with_contiguous_pages(pa: usize, bytes: usize) -> Result { + Ok(Self { + inner: PhysMutPtr::with_contiguous_pages(pa, bytes)?, + }) + } + + /// Create a new `PhysConstPtr` from the given physical address for a single object. + /// + /// This is a shortcut for `PhysConstPtr::with_contiguous_pages(pa, size_of::())`. + /// + /// Note: This module doesn't provide `as_usize` because LiteBox should not dereference physical addresses directly. + pub fn with_usize(pa: usize) -> Result { + Ok(Self { + inner: PhysMutPtr::with_usize(pa)?, + }) + } + + /// Read the value at the given offset from the physical pointer. + /// + /// Returns an owned copy of the value read from physical memory. + pub fn read_at_offset(&self, count: usize) -> Result, PhysPointerError> + where + T: FromBytes, + { + self.inner.read_at_offset(count) + } + + /// Read a slice of values at the given offset from the physical pointer. + /// + /// Copies values from physical memory into the caller-provided slice. + pub fn read_slice_at_offset( + &self, + count: usize, + values: &mut [T], + ) -> Result<(), PhysPointerError> + where + T: FromBytes, + { + self.inner.read_slice_at_offset(count, values) + } +} + +impl> core::fmt::Debug + for PhysConstPtr +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PhysConstPtr") + .field( + "pages[0]", + &self.inner.pages.first().map_or(0, |p| p.as_usize()), + ) + .field("offset", &self.inner.offset) + .finish_non_exhaustive() + } +} diff --git a/litebox_common_linux/src/vmap.rs b/litebox_common_linux/src/vmap.rs index e747ca5a3a..30d161fcd5 100644 --- a/litebox_common_linux/src/vmap.rs +++ b/litebox_common_linux/src/vmap.rs @@ -8,41 +8,60 @@ use thiserror::Error; /// /// `ALIGN`: The page frame size. /// -/// This provider exists to service `litebox_shim_optee::ptr::PhysMutPtr` and -/// `litebox_shim_optee::ptr::PhysConstPtr`. It can benefit other modules which need +/// This provider exists to service [`crate::physical_pointers::PhysMutPtr`] and +/// [`crate::physical_pointers::PhysConstPtr`]. It can benefit other modules which need /// Linux kernel's `vmap()` and `vunmap()` functionalities (e.g., HVCI/HEKI, drivers). -pub trait VmapManager { +/// +/// # Safety +/// +/// Implementors must uphold each unsafe method's contract and keep [`Self::MapInfo`] tied to the +/// mapping it identifies. +pub unsafe trait VmapManager { + /// Implementors use this to carry the virtual mapping and any platform-specific bookkeeping + /// needed for unmapping. + type MapInfo: PhysPageMapInfo; + /// Map the given `PhysPageAddrArray` into virtually contiguous addresses with the given - /// [`PhysPageMapPermissions`] while returning [`PhysPageMapInfo`]. + /// [`PhysPageMapPermissions`] while returning [`Self::MapInfo`]. /// /// This function is analogous to Linux kernel's `vmap()`. /// /// # Safety /// - /// The caller should ensure that `pages` are not in active use by other entities - /// (especially, there should be no read/write or write/write conflicts). - /// Unfortunately, LiteBox itself cannot fully guarantee this and it needs some helps - /// from the caller, hypervisor, or hardware. - /// Multiple LiteBox threads might concurrently call this function with overlapping - /// physical pages, so the implementation should safely handle such cases. + /// The returned pointer is a raw address; creating or holding it does not access memory or + /// create a Rust reference. Any later use of that pointer must satisfy the platform's access + /// requirements for the mapped physical pages. Even when access is logically exclusive, callers + /// must treat the mapped memory like DMA/shared physical memory rather than ordinary Rust-owned + /// RAM. Implementors must not return a VA that aliases LiteBox-owned memory; the returned + /// mapping must live in a platform-defined foreign-memory VA range. unsafe fn vmap( &self, _pages: &PhysPageAddrArray, _perms: PhysPageMapPermissions, - ) -> Result, PhysPointerError> { + ) -> Result { Err(PhysPointerError::UnsupportedOperation) } - /// Unmap the previously mapped virtually contiguous addresses ([`PhysPageMapInfo`]). + /// Unmap the previously mapped virtually contiguous addresses ([`Self::MapInfo`]). /// /// This function is analogous to Linux kernel's `vunmap()`. /// + /// On failure, the unchanged `vmap_info` is returned alongside the error so the caller can + /// retry or otherwise preserve the mapping state. Dropping returned map info is not guaranteed + /// to release platform resources; each implementation owns the retention policy for resources + /// that cannot be safely reclaimed after a failed unmap. + /// /// # Safety /// - /// The caller should ensure that the virtual addresses in `vmap_info` are not in active - /// use by other entities. - unsafe fn vunmap(&self, _vmap_info: PhysPageMapInfo) -> Result<(), PhysPointerError> { - Err(PhysPointerError::UnsupportedOperation) + /// The caller must ensure there are no outstanding raw-pointer uses or Rust references derived + /// from `PhysPageMapInfo::base()`. After a successful call, the virtual mapping is invalid and + /// any platform resources tied to the mapping lifetime have been released or otherwise handled + /// by the implementation. + unsafe fn vunmap( + &self, + vmap_info: Self::MapInfo, + ) -> Result<(), (PhysPointerError, Self::MapInfo)> { + Err((PhysPointerError::UnsupportedOperation, vmap_info)) } /// Validate that the given physical pages are not owned by LiteBox. @@ -50,9 +69,14 @@ pub trait VmapManager { /// Platform is expected to track which physical memory addresses are owned by LiteBox (e.g., VTL1 memory addresses). /// /// Returns `Ok(())` if the physical pages are not owned by LiteBox. Otherwise, returns `Err(PhysPointerError)`. - fn validate_unowned(&self, _pages: &PhysPageAddrArray) -> Result<(), PhysPointerError> { - Ok(()) - } + /// + /// # Invariant + /// + /// The implementor must ensure that, whenever this function returns `Ok(())`, none of the + /// given physical pages may name memory owned by LiteBox/Rust (heap, stack, ...). Callers rely + /// on a successful return to treat the pages as foreign memory and to map them only through + /// platform-defined foreign-memory VA ranges, never through LiteBox-owned VA ranges. + fn validate_unowned(&self, pages: &PhysPageAddrArray) -> Result<(), PhysPointerError>; /// Protect the given physical pages to ensure concurrent read or exclusive write access: /// - Read protection: prevent others from writing to the pages. @@ -60,7 +84,6 @@ pub trait VmapManager { /// - No protection: allow others to read and write the pages. /// /// This function can be implemented using EPT/NPT, TZASC, PMP, or some other hardware mechanisms. - /// If the platform does not support such protection, this function returns `Ok(())` without any action. /// /// Returns `Ok(())` if it successfully protects the pages. If it fails, returns /// `Err(PhysPointerError)`. @@ -72,11 +95,25 @@ pub trait VmapManager { /// The caller should unprotect the pages when they are no longer needed to access them. unsafe fn protect( &self, - _pages: &PhysPageAddrArray, - _perms: PhysPageMapPermissions, - ) -> Result<(), PhysPointerError> { - Ok(()) - } + pages: &PhysPageAddrArray, + perms: PhysPageMapPermissions, + ) -> Result<(), PhysPointerError>; +} + +/// A type-level handle to a platform-global [`VmapManager`]. +/// +/// `PhysMutPtr` and `PhysConstPtr` carry their provider as a type parameter +/// (`PhantomData

`), so they cannot hold a live `&VmapManager`. This trait +/// is the minimum surface that lets such a `PhantomData`-only carrier reach +/// the live manager: each platform implements this on a small unit struct +/// (e.g., `Vmap`) and points `manager()` at its global +/// platform singleton. +pub trait GlobalVmapManager: 'static { + /// The concrete `VmapManager` this marker resolves to. + type Manager: VmapManager + 'static; + + /// Return the global manager instance for this platform. + fn manager() -> &'static Self::Manager; } /// Data structure representing a physical address with page alignment. @@ -90,13 +127,39 @@ pub type PhysPageAddr = litebox::mm::linux::NonZeroAddress = [PhysPageAddr]; -/// Data structure to maintain the mapping information returned by `vmap()`. -#[derive(Clone)] -pub struct PhysPageMapInfo { +/// Mapping information returned by `vmap()`. +/// +/// Implementors use this value to track the virtual mapping and any platform-specific resources +/// tied to it. Callers must pass it back to the same platform's `vunmap()` to explicitly unmap; +/// drop behavior is implementation-specific. +pub trait PhysPageMapInfo { /// Virtual address of the mapped region which is page aligned. - pub base: *mut u8, + fn base(&self) -> *mut u8; /// The size of the mapped region in bytes. - pub size: usize, + fn size(&self) -> usize; +} + +/// A no-op [`PhysPageMapInfo`] for platforms that do not support `vmap()`/`vunmap()`. +#[derive(Debug)] +pub struct NoopPhysPageMapInfo { + base: *mut u8, + size: usize, +} + +impl NoopPhysPageMapInfo { + pub fn new(base: *mut u8, size: usize) -> Self { + Self { base, size } + } +} + +impl PhysPageMapInfo for NoopPhysPageMapInfo { + fn base(&self) -> *mut u8 { + self.base + } + + fn size(&self) -> usize { + self.size + } } bitflags::bitflags! { @@ -146,10 +209,6 @@ impl From for MemoryRegionPermissions { pub enum PhysPointerError { #[error("Physical address {0:#x} is invalid to access")] InvalidPhysicalAddress(usize), - #[error("Physical address {0:#x} is not aligned to {1} bytes")] - UnalignedPhysicalAddress(usize, usize), - #[error("Offset {0:#x} is not aligned to {1} bytes")] - UnalignedOffset(usize, usize), #[error("Base offset {0:#x} is greater than or equal to alignment ({1} bytes)")] InvalidBaseOffset(usize, usize), #[error( @@ -162,8 +221,6 @@ pub enum PhysPointerError { AlreadyMapped(usize), #[error("Physical address {0:#x} is unmapped")] Unmapped(usize), - #[error("No mapping information available")] - NoMappingInfo, #[error("Overflow occurred during calculation")] Overflow, #[error("The operation is unsupported on this platform")] diff --git a/litebox_common_optee/src/lib.rs b/litebox_common_optee/src/lib.rs index aad03495e1..87af051297 100644 --- a/litebox_common_optee/src/lib.rs +++ b/litebox_common_optee/src/lib.rs @@ -2119,7 +2119,7 @@ impl From<&OpteeSmcArgsPage> for OpteeSmcArgs { } /// OP-TEE SMC call arguments. -#[derive(Clone, Copy, Default, FromBytes)] +#[derive(Clone, Copy, Default, FromBytes, IntoBytes, Immutable)] pub struct OpteeSmcArgs { args: [usize; Self::NUM_OPTEE_SMC_ARGS], } @@ -2352,7 +2352,6 @@ impl From for OpteeSmcReturnCode { use litebox_common_linux::vmap::PhysPointerError; match err { PhysPointerError::AlreadyMapped(_) => OpteeSmcReturnCode::EBusy, - PhysPointerError::NoMappingInfo => OpteeSmcReturnCode::ENomem, _ => OpteeSmcReturnCode::EBadAddr, } } diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 770ccaf4ed..003a5591fa 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -2403,7 +2403,24 @@ impl litebox::platform::DerivedKeyProvider for LinuxUserland { /// In general, userland platforms do not support `vmap` and `vunmap` (which are kernel functions). /// We might need to emulate these functions' behaviors using virtual addresses for development or /// testing, or use a kernel module to provide this functionality (if needed). -impl VmapManager for LinuxUserland {} +unsafe impl VmapManager for LinuxUserland { + type MapInfo = litebox_common_linux::vmap::NoopPhysPageMapInfo; + + fn validate_unowned( + &self, + _pages: &litebox_common_linux::vmap::PhysPageAddrArray, + ) -> Result<(), litebox_common_linux::vmap::PhysPointerError> { + Err(litebox_common_linux::vmap::PhysPointerError::UnsupportedOperation) + } + + unsafe fn protect( + &self, + _pages: &litebox_common_linux::vmap::PhysPageAddrArray, + _perms: litebox_common_linux::vmap::PhysPageMapPermissions, + ) -> Result<(), litebox_common_linux::vmap::PhysPointerError> { + Err(litebox_common_linux::vmap::PhysPointerError::UnsupportedOperation) + } +} /// Dummy `VmemPageFaultHandler`. /// diff --git a/litebox_platform_lvbs/Cargo.toml b/litebox_platform_lvbs/Cargo.toml index ee3a988a1f..466891af38 100644 --- a/litebox_platform_lvbs/Cargo.toml +++ b/litebox_platform_lvbs/Cargo.toml @@ -44,8 +44,7 @@ libc = "0.2.177" litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } [features] -default = ["optee_syscall"] -optee_syscall = [] +default = [] linux_syscall = [] devbox = [] diff --git a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs index 4e2866e85d..7a4e5b95dc 100644 --- a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs +++ b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs @@ -686,7 +686,6 @@ impl X64PageTable<'_, M, ALIGN> { /// # Behavior /// - Any existing mapping is treated as an error /// - On error, all pages mapped by this call are unmapped (atomic) - #[cfg(feature = "optee_syscall")] pub(crate) fn map_non_contiguous_phys_frames( &self, frames: &[PhysFrame], @@ -755,7 +754,6 @@ impl X64PageTable<'_, M, ALIGN> { /// /// Note: The caller must already hold the page table lock (`self.inner`). /// This function accepts the locked `MappedPageTable` directly. - #[cfg(feature = "optee_syscall")] fn rollback_mapped_pages( inner: &mut MappedPageTable<'_, FrameMapping>, pages: x86_64::structures::paging::page::PageRangeInclusive, diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index adcee05b7f..8dd2d89663 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -23,10 +23,9 @@ use litebox::{ shim::ContinueOperation, utils::TruncateExt, }; -#[cfg(feature = "optee_syscall")] use litebox_common_linux::vmap::{ - PhysPageAddr, PhysPageAddrArray, PhysPageMapInfo, PhysPageMapPermissions, PhysPointerError, - VmapManager, + GlobalVmapManager, PhysPageAddr, PhysPageAddrArray, PhysPageMapInfo, PhysPageMapPermissions, + PhysPointerError, VmapManager, }; use litebox_common_linux::{PunchthroughSyscall, errno::Errno}; use x86_64::{ @@ -38,7 +37,6 @@ use x86_64::{ }; use zerocopy::{FromBytes, IntoBytes}; -#[cfg(feature = "optee_syscall")] use crate::mm::vmap::vmap_allocator; extern crate alloc; @@ -50,29 +48,26 @@ pub mod mshv; pub mod syscall_entry; -/// Allocate a zeroed `Box` directly on the heap, avoiding stack intermediaries -/// for large types (e.g., 4096-byte `HekiPage`). -/// -/// This is safe because `T: FromBytes` guarantees that all-zero bytes are a valid `T`. -/// -/// # Panics -/// -/// Panics if `T` is a zero-sized type, since `alloc_zeroed` with a zero-sized -/// layout is undefined behavior. -fn box_new_zeroed() -> alloc::boxed::Box { - assert!( - core::mem::size_of::() > 0, - "box_new_zeroed does not support zero-sized types" - ); - let layout = core::alloc::Layout::new::(); - // Safety: layout has a non-zero size and correct alignment for T. - let ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }.cast::(); - if ptr.is_null() { - alloc::alloc::handle_alloc_error(layout); - } - // Safety: ptr is a valid, zeroed, properly aligned heap allocation for T. - // T: FromBytes guarantees all-zero is a valid bit pattern. - unsafe { alloc::boxed::Box::from_raw(ptr) } +/// Mapping info returned by [`LinuxKernel`]'s [`VmapManager::vmap`]. +pub struct LvbsPhysPageMapInfo { + base: *mut u8, + size: usize, +} + +impl LvbsPhysPageMapInfo { + fn new(base: *mut u8, size: usize) -> Self { + Self { base, size } + } +} + +impl PhysPageMapInfo for LvbsPhysPageMapInfo { + fn base(&self) -> *mut u8 { + self.base + } + + fn size(&self) -> usize { + self.size + } } static CPU_MHZ: AtomicU64 = AtomicU64::new(0); @@ -470,6 +465,21 @@ type UserConstPtr = type UserMutPtr = litebox::platform::common_providers::userspace_pointers::UserMutPtr; +/// Type-level marker for the VTL0 physical-pointer provider. +pub enum Vmap {} + +impl GlobalVmapManager for Vmap { + type Manager = crate::host::LvbsLinuxKernel; + fn manager() -> &'static Self::Manager { + crate::platform_low() + } +} + +pub type Vtl0PhysConstPtr = + litebox_common_linux::physical_pointers::PhysConstPtr; +pub type Vtl0PhysMutPtr = + litebox_common_linux::physical_pointers::PhysMutPtr; + impl RawPointerProvider for LinuxKernel { type RawConstPointer = UserConstPtr; type RawMutPointer = UserMutPtr; @@ -643,50 +653,14 @@ impl LinuxKernel { self.vtl1_phys_frame_range } - /// This function maps VTL0 physical page frames containing the physical addresses - /// from `phys_start` to `phys_end` to the VTL1 kernel page table. It internally page aligns - /// the input addresses to ensure the mapped memory area covers the entire input addresses - /// at the page level. It returns a page-aligned address (as `mmap` does) and the length of the mapped memory. - /// - /// Note: VTL0 physical memory is external/remote memory that this Rust binary doesn't own, - /// so mapping it doesn't create aliasing issues within the Rust memory model. - fn map_vtl0_phys_range( - &self, - phys_start: x86_64::PhysAddr, - phys_end: x86_64::PhysAddr, - flags: PageTableFlags, - ) -> Result<(*mut u8, usize), MapToError> { - let frame_range = PhysFrame::range( - PhysFrame::containing_address(phys_start), - PhysFrame::containing_address(phys_end.align_up(Size4KiB::SIZE)), - ); - - // ensure the input address range does not overlap with VTL1 memory - if frame_range.start < self.vtl1_phys_frame_range.end - && self.vtl1_phys_frame_range.start < frame_range.end - { - return Err(MapToError::FrameAllocationFailed); - } - - let flags = flags | PageTableFlags::NO_EXECUTE; - - Ok(( - self.page_table_manager - .current_page_table() - .map_phys_frame_range_direct(frame_range, flags, None)?, - usize::try_from(frame_range.len()).unwrap() * PAGE_SIZE, - )) - } - /// This function unmaps VTL0 pages from the page table. /// /// Allocator does not allocate memory frames for VTL0 pages, so frame deallocation is not needed. /// - /// Note: VTL0 physical memory is external memory not owned by LiteBox (similar to MMIO). - /// LiteBox accesses it by creating a temporary non-shared mapping, copying data to/from a - /// LiteBox-owned buffer, and unmapping immediately. No Rust references are created to the - /// mapped VTL0 memory; all accesses use raw pointer operations (read_volatile / - /// copy_nonoverlapping) to avoid violating Rust's aliasing model. + /// Note: VTL0 physical memory is external memory not owned by LiteBox, similar to DMA/shared + /// physical memory. Physical pointer APIs access it by creating a temporary mapping, copying + /// data to/from a LiteBox-owned buffer with fallible raw-pointer copies, and unmapping + /// immediately. These APIs do not create Rust references to the mapped VTL0 memory. fn unmap_vtl0_pages( &self, page_addr: *const u8, @@ -717,174 +691,6 @@ impl LinuxKernel { } } - /// Map a VTL0 physical range and return a guard that unmaps on drop. - fn map_vtl0_guard( - &self, - phys_addr: x86_64::PhysAddr, - size: u64, - flags: PageTableFlags, - ) -> Option> { - let phys_end = phys_addr - .as_u64() - .checked_add(size) - .and_then(|end| x86_64::PhysAddr::try_new(end).ok())?; - let (page_addr, page_aligned_length) = - self.map_vtl0_phys_range(phys_addr, phys_end, flags).ok()?; - let page_offset: usize = (phys_addr - phys_addr.align_down(Size4KiB::SIZE)).trunc(); - Some(Vtl0MappedGuard { - owner: self, - page_addr, - page_aligned_length, - ptr: page_addr.wrapping_add(page_offset), - size: size.trunc(), - }) - } - - /// This function copies data from VTL0 physical memory to the VTL1 kernel through `Box`. - /// Use this function instead of map/unmap functions to avoid potential TOCTTOU. - /// - /// # Safety - /// - /// The caller must ensure that the `phys_addr` is a valid VTL0 physical address - pub unsafe fn copy_from_vtl0_phys( - &self, - phys_addr: x86_64::PhysAddr, - ) -> Option> { - if core::mem::size_of::() == 0 { - return Some(alloc::boxed::Box::new(T::new_zeroed())); - } - - let src_guard = self.map_vtl0_guard( - phys_addr, - core::mem::size_of::() as u64, - PageTableFlags::PRESENT, - )?; - - let mut boxed = box_new_zeroed::(); - // Use memcpy_fallible instead of ptr::copy_nonoverlapping to handle - // the race where another core running on the same page table unmaps - // this page between map_vtl0_guard and the copy. The mapping is valid - // at this point, so a fault is not expected in the common case. - // TODO: Once VTL0 page-range locking is in place, this fallible copy - // may become unnecessary since the lock would prevent concurrent - // unmapping. It could still serve as a safety net against callers - // that forget to acquire the lock. - let result = unsafe { - litebox::mm::exception_table::memcpy_fallible( - core::ptr::from_mut::(boxed.as_mut()).cast(), - src_guard.ptr, - src_guard.size, - ) - }; - debug_assert!(result.is_ok(), "fault copying from VTL0 mapped page"); - - result.ok().map(|()| boxed) - } - - /// This function copies data from the VTL1 kernel to VTL0 physical memory. - /// Use this function instead of map/unmap functions to avoid potential TOCTTOU. - /// # Safety - /// - /// The caller must ensure that the `phys_addr` is a valid VTL0 physical address - pub unsafe fn copy_to_vtl0_phys( - &self, - phys_addr: x86_64::PhysAddr, - value: &T, - ) -> bool { - if core::mem::size_of::() == 0 { - return true; - } - - let Some(dst_guard) = self.map_vtl0_guard( - phys_addr, - core::mem::size_of::() as u64, - PageTableFlags::PRESENT | PageTableFlags::WRITABLE, - ) else { - return false; - }; - - // Fallible: another core may unmap this page concurrently. - let result = unsafe { - litebox::mm::exception_table::memcpy_fallible( - dst_guard.ptr, - core::ptr::from_ref::(value).cast::(), - dst_guard.size, - ) - }; - debug_assert!(result.is_ok(), "fault copying to VTL0 mapped page"); - result.is_ok() - } - - /// This function copies a slice from the VTL1 kernel to VTL0 physical memory. - /// Use this function instead of map/unmap functions to avoid potential TOCTTOU. - /// - /// # Safety - /// - /// The caller must ensure that the `phys_addr` is a valid VTL0 physical address. - pub unsafe fn copy_slice_to_vtl0_phys( - &self, - phys_addr: x86_64::PhysAddr, - value: &[T], - ) -> bool { - if core::mem::size_of_val(value) == 0 { - return true; - } - - let Some(dst_guard) = self.map_vtl0_guard( - phys_addr, - core::mem::size_of_val(value) as u64, - PageTableFlags::PRESENT | PageTableFlags::WRITABLE, - ) else { - return false; - }; - - // Fallible: another core may unmap this page concurrently. - let result = unsafe { - litebox::mm::exception_table::memcpy_fallible( - dst_guard.ptr, - value.as_ptr().cast::(), - dst_guard.size, - ) - }; - debug_assert!(result.is_ok(), "fault copying to VTL0 mapped page"); - result.is_ok() - } - - /// This function copies a slice from VTL0 physical memory to the VTL1 kernel. - /// Use this function instead of map/unmap functions to avoid potential TOCTTOU. - /// - /// # Safety - /// - /// The caller must ensure that the `phys_addr` is a valid VTL0 physical address. - pub unsafe fn copy_slice_from_vtl0_phys( - &self, - phys_addr: x86_64::PhysAddr, - buf: &mut [T], - ) -> bool { - if core::mem::size_of_val(buf) == 0 { - return true; - } - - let Some(src_guard) = self.map_vtl0_guard( - phys_addr, - core::mem::size_of_val(buf) as u64, - PageTableFlags::PRESENT, - ) else { - return false; - }; - - // Fallible: another core may unmap this page concurrently. - let result = unsafe { - litebox::mm::exception_table::memcpy_fallible( - buf.as_mut_ptr().cast::(), - src_guard.ptr, - src_guard.size, - ) - }; - debug_assert!(result.is_ok(), "fault copying from VTL0 mapped page"); - result.is_ok() - } - /// Create a new task page table for VTL1 user space and returns its ID. /// /// The kernel address space is duplicated from the base page table, @@ -965,26 +771,6 @@ impl LinuxKernel { } } -/// RAII guard that unmaps VTL0 physical pages when dropped. -struct Vtl0MappedGuard<'a, Host: HostInterface> { - owner: &'a LinuxKernel, - page_addr: *mut u8, - page_aligned_length: usize, - ptr: *mut u8, - size: usize, -} - -impl Drop for Vtl0MappedGuard<'_, Host> { - fn drop(&mut self) { - assert!( - self.owner - .unmap_vtl0_pages(self.page_addr, self.page_aligned_length) - .is_ok(), - "Failed to unmap VTL0 pages" - ); - } -} - impl RawMutexProvider for LinuxKernel { type RawMutex = RawMutex; } @@ -1355,7 +1141,6 @@ impl litebox::platform::SystemInfoProvider for LinuxKernel< } } -#[cfg(feature = "optee_syscall")] /// Checks whether the given physical addresses are contiguous with respect to ALIGN. fn is_contiguous(addrs: &[PhysPageAddr]) -> bool { for window in addrs.windows(2) { @@ -1372,13 +1157,14 @@ fn is_contiguous(addrs: &[PhysPageAddr]) -> bool { true } -#[cfg(feature = "optee_syscall")] -impl VmapManager for LinuxKernel { +unsafe impl VmapManager for LinuxKernel { + type MapInfo = LvbsPhysPageMapInfo; + unsafe fn vmap( &self, pages: &PhysPageAddrArray, perms: PhysPageMapPermissions, - ) -> Result, PhysPointerError> { + ) -> Result { if pages.is_empty() { return Err(PhysPointerError::InvalidPhysicalAddress(0)); } @@ -1387,14 +1173,28 @@ impl VmapManager for LinuxKernel unimplemented!("ALIGN other than 4KiB is not supported yet"); } + self.validate_unowned(pages)?; + + // Reject duplicates early as an API-level validation. The page-table implementation also + // rejects duplicate/shared mappings, but this keeps the error local to the input array. + if !is_contiguous(pages) { + let mut seen = hashbrown::HashSet::with_capacity(pages.len()); + for page in pages { + if !seen.insert(page.as_usize()) { + return Err(PhysPointerError::DuplicatePhysicalAddress(page.as_usize())); + } + } + } + // VTL0 memory must never be executable from VTL1 (DEP). let mut flags = PageTableFlags::PRESENT | PageTableFlags::NO_EXECUTE; if perms.contains(PhysPageMapPermissions::WRITE) { flags |= PageTableFlags::WRITABLE; } - // If pages are contiguous, use `map_phys_frame_range_direct` which is efficient and - // doesn't require vmap VA space. + // `validate_unowned` rejects VTL1-owned PA before callers reach `vmap`, so these pages + // are foreign. Contiguous foreign PA uses the foreign direct-map VA range; non-contiguous + // foreign PA uses the vmap VA range. Neither range aliases VTL1-owned Rust memory. if is_contiguous(pages) { let phys_start = x86_64::PhysAddr::new(pages[0].as_usize() as u64); let phys_end = x86_64::PhysAddr::new( @@ -1415,10 +1215,7 @@ impl VmapManager for LinuxKernel .current_page_table() .map_phys_frame_range_direct(frame_range, flags, None) { - Ok(page_addr) => Ok(PhysPageMapInfo { - base: page_addr, - size: pages.len() * ALIGN, - }), + Ok(page_addr) => Ok(LvbsPhysPageMapInfo::new(page_addr, pages.len() * ALIGN)), Err(MapToError::PageAlreadyMapped(_)) => { Err(PhysPointerError::AlreadyMapped(pages[0].as_usize())) } @@ -1430,16 +1227,6 @@ impl VmapManager for LinuxKernel ), } } else { - // Reject duplicate page addresses - { - let mut seen = hashbrown::HashSet::with_capacity(pages.len()); - for page in pages { - if !seen.insert(page.as_usize()) { - return Err(PhysPointerError::DuplicatePhysicalAddress(page.as_usize())); - } - } - } - let frames: alloc::vec::Vec> = pages .iter() .map(|p| PhysFrame::containing_address(x86_64::PhysAddr::new(p.as_usize() as u64))) @@ -1464,10 +1251,7 @@ impl VmapManager for LinuxKernel .current_page_table() .map_non_contiguous_phys_frames(&frames, base_va, flags) { - Ok(page_addr) => Ok(PhysPageMapInfo { - base: page_addr, - size: pages.len() * ALIGN, - }), + Ok(page_addr) => Ok(LvbsPhysPageMapInfo::new(page_addr, pages.len() * ALIGN)), Err(e) => { let _ = vmap_allocator().unregister_allocation(base_va); match e { @@ -1486,24 +1270,37 @@ impl VmapManager for LinuxKernel } } - unsafe fn vunmap(&self, vmap_info: PhysPageMapInfo) -> Result<(), PhysPointerError> { + unsafe fn vunmap( + &self, + vmap_info: Self::MapInfo, + ) -> Result<(), (PhysPointerError, Self::MapInfo)> { if ALIGN != PAGE_SIZE { unimplemented!("ALIGN other than 4KiB is not supported yet"); } - let base_va = x86_64::VirtAddr::new(vmap_info.base as u64); + let base = vmap_info.base(); + let size = vmap_info.size(); + let base_va = x86_64::VirtAddr::new(base as u64); // Unmap the page table entries first. Only release the VA range back // to the allocator when unmapping succeeds; if it fails, stale PTE // entries remain and recycling the VA would cause collisions. - self.unmap_vtl0_pages(vmap_info.base, vmap_info.size) - .map_err(|_| PhysPointerError::Unmapped(vmap_info.base as usize))?; + if self.unmap_vtl0_pages(base, size).is_err() { + return Err((PhysPointerError::Unmapped(base as usize), vmap_info)); + } - if crate::mm::vmap::is_vmap_address(base_va) { - crate::mm::vmap::vmap_allocator() + // PTEs are already cleared at this point, so the mapping is functionally gone + // and a retry would only re-fail against empty page-table entries. If the VA + // allocator's bookkeeping is inconsistent, surface it via `debug_assert!`. The + // VA region is leaked but cannot be safely recycled. + let unregister_ok = !crate::mm::vmap::is_vmap_address(base_va) + || crate::mm::vmap::vmap_allocator() .unregister_allocation(base_va) - .ok_or(PhysPointerError::Unmapped(vmap_info.base as usize))?; - } + .is_some(); + debug_assert!( + unregister_ok, + "vmap allocator unregister failed at {base_va:?}", + ); Ok(()) } diff --git a/litebox_platform_lvbs/src/mm/mod.rs b/litebox_platform_lvbs/src/mm/mod.rs index df04d4209c..a96d5fd128 100644 --- a/litebox_platform_lvbs/src/mm/mod.rs +++ b/litebox_platform_lvbs/src/mm/mod.rs @@ -6,7 +6,6 @@ use crate::arch::{PhysAddr, VirtAddr}; pub(crate) mod pgtable; -#[cfg(feature = "optee_syscall")] pub(crate) mod vmap; #[cfg(test)] @@ -56,7 +55,6 @@ pub trait MemoryProvider { fn pa_to_va_direct(pa: PhysAddr) -> VirtAddr { let pa = pa.as_u64() & !Self::PRIVATE_PTE_MASK; let va = VirtAddr::new_truncate(pa + Self::GVA_OFFSET.as_u64()); - #[cfg(feature = "optee_syscall")] assert!( va.as_u64() < crate::VMAP_START as u64, "VA {va:#x} is out of range for direct mapping" diff --git a/litebox_platform_lvbs/src/mshv/ringbuffer.rs b/litebox_platform_lvbs/src/mshv/ringbuffer.rs index 92a75da26e..355ffc3d90 100644 --- a/litebox_platform_lvbs/src/mshv/ringbuffer.rs +++ b/litebox_platform_lvbs/src/mshv/ringbuffer.rs @@ -3,7 +3,11 @@ //! RingBuffer implementation and functions +use crate::Vtl0PhysMutPtr; use core::fmt; +use litebox::mm::linux::PAGE_SIZE; +use litebox::utils::TruncateExt; +use litebox_common_linux::vmap::PhysPageAddr; use spin::{Mutex, Once}; use x86_64::PhysAddr; @@ -11,47 +15,120 @@ pub struct RingBuffer { rb_pa: PhysAddr, write_offset: usize, size: usize, + // True iff `rb_pa` is page-aligned and `size` is a non-zero page multiple, + // i.e. wraparound can be collapsed into a single non-contiguous mapping. + // Pages themselves are derived from `rb_pa + idx * PAGE_SIZE` since the ring + // is physically contiguous. + fast_path_eligible: bool, } impl RingBuffer { pub fn new(phys_addr: PhysAddr, requested_size: usize) -> Self { + let pa: usize = phys_addr.as_u64().trunc(); + let fast_path_eligible = requested_size > 0 + && requested_size.is_multiple_of(PAGE_SIZE) + && pa.is_multiple_of(PAGE_SIZE); RingBuffer { rb_pa: phys_addr, write_offset: 0, size: requested_size, + fast_path_eligible, } } pub fn write(&mut self, buf: &[u8]) { - // If the input buffer is longer than the ring buffer, fill the whole ring buffer with - // the final [ring buffer size] values from the input buffer - if buf.len() >= self.size { - let single_slice = &buf[(buf.len() - self.size)..]; - unsafe { - crate::platform_low().copy_slice_to_vtl0_phys(self.rb_pa, single_slice); - } - self.write_offset = 0; + if self.size == 0 || buf.is_empty() { return; } - - // Otherwise, calculate if wraparound needed - let space_remaining: usize = self.size - self.write_offset; - if buf.len() > space_remaining { - let first_slice = &buf[..space_remaining]; - let wraparound_slice = &buf[space_remaining..]; - unsafe { - crate::platform_low() - .copy_slice_to_vtl0_phys(self.rb_pa + self.write_offset as u64, first_slice); - crate::platform_low().copy_slice_to_vtl0_phys(self.rb_pa, wraparound_slice); - } + self.write_offset = if self.fast_path_eligible { + write_fast(self.rb_pa, self.size, self.write_offset, buf) } else { - unsafe { - crate::platform_low() - .copy_slice_to_vtl0_phys(self.rb_pa + self.write_offset as u64, buf); - } - } - self.write_offset = (self.write_offset + buf.len()) % self.size; + write_slow(self.rb_pa, self.size, self.write_offset, buf) + }; + } +} + +#[inline] +fn advance_offset(size: usize, write_offset: usize, len: usize) -> usize { + if len >= size { + 0 + } else { + (write_offset + len) % size + } +} + +/// Fast path for a page-aligned, page-sized ring buffer. Wraparound becomes a +/// single virtually-contiguous, physically non-contiguous mapping by emitting +/// the wrap span as `[rb_pa + (start_page + i) % page_count * PAGE_SIZE]`. +/// Returns the new write offset after attempting the write. +fn write_fast(rb_pa: PhysAddr, size: usize, write_offset: usize, buf: &[u8]) -> usize { + const MAX_SPAN_PAGES: usize = 16; + + // Inputs longer than the buffer overwrite the whole ring with the trailing bytes. + let (buf, start) = if buf.len() >= size { + (&buf[(buf.len() - size)..], 0) + } else { + (buf, write_offset) + }; + + let page_count = size / PAGE_SIZE; + let start_page = start / PAGE_SIZE; + let in_page_offset = start % PAGE_SIZE; + let span_pages = (in_page_offset + buf.len()).div_ceil(PAGE_SIZE); + // `span_pages > page_count`: the wrap *revisits* the start page, so the span + // would map the same physical page twice and vmap rejects the duplicate. + // `span_pages > MAX_SPAN_PAGES`: the span is too long for `span` below. + if span_pages > page_count || span_pages > MAX_SPAN_PAGES { + return write_slow(rb_pa, size, write_offset, buf); + } + let rb_pa: usize = rb_pa.as_u64().trunc(); + let mut span: arrayvec::ArrayVec, MAX_SPAN_PAGES> = + arrayvec::ArrayVec::new(); + for i in 0..span_pages { + let page_idx = (start_page + i) % page_count; + let Some(addr) = page_idx + .checked_mul(PAGE_SIZE) + .and_then(|off| rb_pa.checked_add(off)) + .and_then(PhysPageAddr::::new) + else { + return write_offset; + }; + span.push(addr); + } + + let Ok(ptr) = Vtl0PhysMutPtr::::new(&span, in_page_offset) else { + return advance_offset(size, write_offset, buf.len()); + }; + let _ = ptr.write_slice_at_offset(0, buf); + advance_offset(size, write_offset, buf.len()) +} + +/// Slow path used when `rb_pa` or `size` is not page-aligned/page-multiple. +/// Wraparound issues two map/unmap cycles. Returns the new write offset +/// after attempting the write. +fn write_slow(rb_pa: PhysAddr, size: usize, write_offset: usize, buf: &[u8]) -> usize { + let write_slice = |pa: PhysAddr, slice: &[u8]| -> bool { + Vtl0PhysMutPtr::::with_contiguous_pages(pa.as_u64().trunc(), slice.len()) + .and_then(|ptr| ptr.write_slice_at_offset(0, slice)) + .is_ok() + }; + + if buf.len() >= size { + let single_slice = &buf[(buf.len() - size)..]; + let _ = write_slice(rb_pa, single_slice); + return advance_offset(size, write_offset, buf.len()); + } + + let space_remaining = size - write_offset; + if buf.len() > space_remaining { + let first_slice = &buf[..space_remaining]; + let wraparound_slice = &buf[space_remaining..]; + let _ = write_slice(rb_pa + write_offset as u64, first_slice); + let _ = write_slice(rb_pa, wraparound_slice); + } else { + let _ = write_slice(rb_pa + write_offset as u64, buf); } + advance_offset(size, write_offset, buf.len()) } static RINGBUFFER_ONCE: Once> = Once::new(); diff --git a/litebox_platform_lvbs/src/mshv/vsm.rs b/litebox_platform_lvbs/src/mshv/vsm.rs index e2184c2f0d..29cc0442fa 100644 --- a/litebox_platform_lvbs/src/mshv/vsm.rs +++ b/litebox_platform_lvbs/src/mshv/vsm.rs @@ -7,7 +7,7 @@ use crate::mshv::mem_integrity::parse_modinfo; use crate::mshv::ringbuffer::set_ringbuffer; use crate::{ - debug_serial_println, + Vtl0PhysConstPtr, Vtl0PhysMutPtr, debug_serial_println, host::{ PRK_LEN, bootparam::get_vtl1_memory_info, @@ -49,7 +49,7 @@ use core::{ }; use hashbrown::{HashMap, HashSet}; use litebox::utils::TruncateExt; -use litebox_common_linux::errno::Errno; +use litebox_common_linux::{errno::Errno, vmap::PhysPageAddr}; use spin::Once; use thiserror::Error; use x86_64::{ @@ -57,13 +57,9 @@ use x86_64::{ structures::paging::{PageSize, PhysFrame, Size4KiB, frame::PhysFrameRange}, }; use x509_cert::{Certificate, der::Decode}; -use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout}; +use zerocopy::{FromBytes, FromZeros, IntoBytes}; use zeroize::Zeroizing; -#[derive(Copy, Clone, FromBytes, Immutable, KnownLayout)] -#[repr(align(4096))] -struct AlignedPage([u8; PAGE_SIZE]); - // For now, we do not validate large kernel modules due to the VTL1's memory size limitation. const MODULE_VALIDATION_MAX_SIZE: usize = 64 * 1024 * 1024; @@ -128,11 +124,13 @@ pub fn mshv_vsm_boot_aps(cpu_online_mask_pfn: u64) -> Result { .and_then(|pa| PhysAddr::try_new(pa).ok()) .ok_or(VsmError::InvalidPhysicalAddress)?; - let Some(cpu_mask) = (unsafe { - crate::platform_low().copy_from_vtl0_phys::(cpu_online_mask_page_addr) - }) else { - return Err(VsmError::CpuOnlineMaskCopyFailed); - }; + let cpu_mask_ptr = Vtl0PhysConstPtr::::with_usize( + cpu_online_mask_page_addr.as_u64().trunc(), + ) + .map_err(|_| VsmError::CpuOnlineMaskCopyFailed)?; + let cpu_mask = cpu_mask_ptr + .read_at_offset(0) + .map_err(|_| VsmError::CpuOnlineMaskCopyFailed)?; #[cfg(debug_assertions)] { @@ -848,23 +846,27 @@ fn copy_heki_patch_from_vtl0(patch_pa_0: u64, patch_pa_1: u64) -> Result(patch_pa_0) } + let ptr = Vtl0PhysConstPtr::::with_usize(patch_pa_0.as_u64().trunc()) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + ptr.read_at_offset(0) .map(|boxed| *boxed) - .ok_or(VsmError::Vtl0CopyFailed) + .map_err(|_| VsmError::Vtl0CopyFailed) } else { let mut heki_patch = HekiPatch::new_zeroed(); let heki_patch_bytes = heki_patch.as_mut_bytes(); - unsafe { - if !crate::platform_low().copy_slice_from_vtl0_phys( - patch_pa_0, - heki_patch_bytes.get_unchecked_mut(..bytes_in_first_page), - ) || !crate::platform_low().copy_slice_from_vtl0_phys( - patch_pa_1, - heki_patch_bytes.get_unchecked_mut(bytes_in_first_page..), - ) { - return Err(VsmError::Vtl0CopyFailed); - } - } + let pages = [ + PhysPageAddr::::new(patch_pa_0.align_down(Size4KiB::SIZE).as_u64().trunc()) + .ok_or(VsmError::Vtl0CopyFailed)?, + PhysPageAddr::::new(patch_pa_1.as_u64().trunc()) + .ok_or(VsmError::Vtl0CopyFailed)?, + ]; + let ptr = Vtl0PhysConstPtr::::new( + &pages, + (patch_pa_0 - patch_pa_0.align_down(Size4KiB::SIZE)).trunc(), + ) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + ptr.read_slice_at_offset(0, heki_patch_bytes) + .map_err(|_| VsmError::Vtl0CopyFailed)?; Ok(heki_patch) }?; @@ -882,32 +884,45 @@ fn apply_vtl0_text_patch(heki_patch: HekiPatch) -> Result<(), VsmError> { let heki_patch_pa_0 = PhysAddr::new(heki_patch.pa[0]); let heki_patch_pa_1 = PhysAddr::new(heki_patch.pa[1]); - let patch_target_page_offset: usize = - (heki_patch_pa_0 - heki_patch_pa_0.align_down(Size4KiB::SIZE)).trunc(); - let bytes_in_first_page = PAGE_SIZE - patch_target_page_offset; + let patch = &heki_patch.code[..usize::from(heki_patch.size)]; + if patch.is_empty() { + return Ok(()); + } if heki_patch_pa_1.is_null() || (heki_patch_pa_0.align_up(Size4KiB::SIZE) == heki_patch_pa_1.align_down(Size4KiB::SIZE)) { - if !unsafe { - crate::platform_low().copy_slice_to_vtl0_phys( - heki_patch_pa_0, - &heki_patch.code[..usize::from(heki_patch.size)], - ) - } { - return Err(VsmError::Vtl0CopyFailed); - } + // Single contiguous span: either fits in one page (pa_1 null) or pa_1 is the + // adjacent next page. `HekiPatch::is_valid` enforces this; assert in debug builds. + debug_assert!( + !heki_patch_pa_1.is_null() + || heki_patch_pa_0.as_u64() + patch.len() as u64 + <= heki_patch_pa_0.align_down(Size4KiB::SIZE).as_u64() + Size4KiB::SIZE, + "patch crosses page boundary but pa_1 is null" + ); + let ptr = Vtl0PhysMutPtr::::with_contiguous_pages( + heki_patch_pa_0.as_u64().trunc(), + patch.len(), + ) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + ptr.write_slice_at_offset(0, patch) + .map_err(|_| VsmError::Vtl0CopyFailed)?; } else { - let (patch_first, patch_second) = - heki_patch.code[..usize::from(heki_patch.size)].split_at(bytes_in_first_page); - - unsafe { - if !crate::platform_low().copy_slice_to_vtl0_phys(heki_patch_pa_0, patch_first) - || !crate::platform_low().copy_slice_to_vtl0_phys(heki_patch_pa_1, patch_second) - { - return Err(VsmError::Vtl0CopyFailed); - } - } + let pages = [ + PhysPageAddr::::new( + heki_patch_pa_0.align_down(Size4KiB::SIZE).as_u64().trunc(), + ) + .ok_or(VsmError::Vtl0CopyFailed)?, + PhysPageAddr::::new(heki_patch_pa_1.as_u64().trunc()) + .ok_or(VsmError::Vtl0CopyFailed)?, + ]; + let ptr = Vtl0PhysMutPtr::::new( + &pages, + (heki_patch_pa_0 - heki_patch_pa_0.align_down(Size4KiB::SIZE)).trunc(), + ) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + ptr.write_slice_at_offset(0, patch) + .map_err(|_| VsmError::Vtl0CopyFailed)?; } Ok(()) } @@ -945,12 +960,14 @@ fn mshv_vsm_set_platform_root_key(key_pa: u64) -> Result { let key_pa = PhysAddr::try_new(key_pa).map_err(|_| VsmError::InvalidPhysicalAddress)?; let mut keybuf = Zeroizing::new([0u8; PRK_LEN]); - if unsafe { crate::platform_low().copy_slice_from_vtl0_phys(key_pa, &mut *keybuf) } { - set_platform_root_key(&*keybuf); - Ok(0) - } else { - Err(VsmError::Vtl0CopyFailed) - } + let key_ptr = + Vtl0PhysConstPtr::::with_contiguous_pages(key_pa.as_u64().trunc(), PRK_LEN) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + key_ptr + .read_slice_at_offset(0, &mut *keybuf) + .map_err(|_| VsmError::Vtl0CopyFailed)?; + set_platform_root_key(&*keybuf); + Ok(0) } /// VSM function dispatcher @@ -1373,7 +1390,9 @@ fn copy_heki_pages_from_vtl0(pa: u64, nranges: u64) -> Option> { if visited_pages.contains(&cur_pa.as_u64()) { return None; } - let heki_page = (unsafe { crate::platform_low().copy_from_vtl0_phys::(cur_pa) })?; + let ptr = + Vtl0PhysConstPtr::::with_usize(cur_pa.as_u64().trunc()).ok()?; + let heki_page = ptr.read_at_offset(0).ok()?; if !heki_page.is_valid() { return None; } @@ -1644,28 +1663,25 @@ impl MemoryContainer { phys_start: PhysAddr, phys_end: PhysAddr, ) -> Result<(), MemoryContainerError> { - let mut bytes_to_copy: usize = (phys_end - phys_start).trunc(); - let mut phys_cur = phys_start; - - while phys_cur < phys_end { - let phys_aligned = phys_cur.align_down(Size4KiB::SIZE); - let Some(page) = - (unsafe { crate::platform_low().copy_from_vtl0_phys::(phys_aligned) }) - else { - return Err(MemoryContainerError::CopyFromVtl0Failed); - }; + let bytes_to_copy: usize = (phys_end - phys_start).trunc(); + if bytes_to_copy == 0 { + return Ok(()); + } - let src_offset: usize = (phys_cur - phys_aligned).trunc(); - let src_len = core::cmp::min(bytes_to_copy, PAGE_SIZE - src_offset); - let src = &page.0[src_offset..src_offset + src_len]; + let ptr = Vtl0PhysConstPtr::::with_contiguous_pages( + phys_start.as_u64().trunc(), + bytes_to_copy, + ) + .map_err(|_| MemoryContainerError::CopyFromVtl0Failed)?; - self.buf.extend_from_slice(src); - phys_cur = phys_cur - .as_u64() - .checked_add(src_len as u64) - .and_then(|next| PhysAddr::try_new(next).ok()) - .ok_or(MemoryContainerError::Overflow)?; - bytes_to_copy -= src_len; + let old_len = self.buf.len(); + self.buf.resize(old_len + bytes_to_copy, 0); + if ptr + .read_slice_at_offset(0, &mut self.buf[old_len..]) + .is_err() + { + self.buf.truncate(old_len); + return Err(MemoryContainerError::CopyFromVtl0Failed); } Ok(()) } diff --git a/litebox_platform_multiplex/Cargo.toml b/litebox_platform_multiplex/Cargo.toml index 1c48c3a738..1099d334e2 100644 --- a/litebox_platform_multiplex/Cargo.toml +++ b/litebox_platform_multiplex/Cargo.toml @@ -21,7 +21,7 @@ platform_linux_snp = ["dep:litebox_platform_linux_kernel"] platform_linux_userland_with_linux_syscall = ["platform_linux_userland", "litebox_platform_linux_userland/linux_syscall"] platform_linux_userland_with_optee_syscall = ["platform_linux_userland", "litebox_platform_linux_userland/optee_syscall"] platform_lvbs_with_linux_syscall = ["platform_lvbs", "litebox_platform_lvbs/linux_syscall"] -platform_lvbs_with_optee_syscall = ["platform_lvbs", "litebox_platform_lvbs/optee_syscall"] +platform_lvbs_with_optee_syscall = ["platform_lvbs"] [lints] workspace = true diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index 2a351c03bc..f9513b7edf 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -275,10 +275,10 @@ fn optee_smc_handler_entry_inner( // Write back the SMC arguments page to normal world memory. // All OP-TEE return codes (success or error) are delivered via smc_args.args[0]. - let mut smc_args_ptr = NormalWorldMutPtr::::with_usize(smc_args_addr) + let smc_args_ptr = NormalWorldMutPtr::::with_usize(smc_args_addr) .map_err(|_| litebox_common_linux::errno::Errno::EINVAL)?; - // SAFETY: The SMC args are written back to normal world memory. - unsafe { smc_args_ptr.write_at_offset(0, smc_args_updated) } + smc_args_ptr + .write_at_offset(0, smc_args_updated) .map_err(|_| litebox_common_linux::errno::Errno::EFAULT)?; Ok(0) } @@ -435,13 +435,12 @@ fn optee_smc_handler(smc_args_addr: usize) -> OpteeSmcArgs { args }; - let Ok(mut smc_args_ptr) = + let Ok(smc_args_ptr) = NormalWorldConstPtr::::with_usize(smc_args_addr) else { return make_error_response(OpteeSmcReturnCode::EBadAddr); }; - // SAFETY: The SMC args are read from normal world memory into an owned copy. - let Ok(mut smc_args) = (unsafe { smc_args_ptr.read_at_offset(0) }) else { + let Ok(mut smc_args) = smc_args_ptr.read_at_offset(0) else { return make_error_response(OpteeSmcReturnCode::EBadAddr); }; let Ok(smc_result) = handle_optee_smc_args(&mut smc_args) else { @@ -1213,13 +1212,11 @@ fn write_msg_args_to_normal_world( let mut blob = vec![0u8; msg_args_size]; msg_args.serialize(&mut blob)?; - let mut ptr = NormalWorldMutPtr::::with_contiguous_pages( + let ptr = NormalWorldMutPtr::::with_contiguous_pages( msg_args_phys_addr.trunc(), msg_args_size, )?; - // SAFETY: Writing msg_args back to normal world memory at a valid physical address. - // The blob contains the serialized variable-length optee_msg_arg structure(s). - unsafe { ptr.write_slice_at_offset(0, &blob) }?; + ptr.write_slice_at_offset(0, &blob)?; Ok(()) } @@ -1239,15 +1236,13 @@ fn write_non_ta_msg_args_to_normal_world( let mut blob = vec![0u8; msg_args_size]; msg_args.serialize(&mut blob)?; - let mut ptr = NormalWorldMutPtr::::with_contiguous_pages( + let ptr = NormalWorldMutPtr::::with_contiguous_pages( msg_args_phys_addr.trunc(), msg_args_size, )?; - // SAFETY: Writing msg_args back to normal world memory at a valid physical address. - // The blob contains the serialized variable-length optee_msg_arg structure(s). // Serialize the packed-page write. See `packed_msg_args_lock`. let _packed_guard = packed_msg_args_lock(); - unsafe { ptr.write_slice_at_offset(0, &blob) }?; + ptr.write_slice_at_offset(0, &blob)?; Ok(()) } @@ -1273,10 +1268,8 @@ fn write_rpc_args_to_normal_world( let rpc_pa: usize = >::trunc(msg_args_phys_addr) .checked_add(msg_args_size) .ok_or(OpteeSmcReturnCode::EBadAddr)?; // RPC args are placed right after the main msg_args blob - let mut ptr = NormalWorldMutPtr::::with_contiguous_pages(rpc_pa, rpc_args_size)?; - // SAFETY: Writing rpc_args back to normal world memory at a valid physical address. - // The blob contains the serialized variable-length optee_msg_arg structure(s). - unsafe { ptr.write_slice_at_offset(0, &blob) }?; + let ptr = NormalWorldMutPtr::::with_contiguous_pages(rpc_pa, rpc_args_size)?; + ptr.write_slice_at_offset(0, &blob)?; Ok(()) } diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index 47daf4b590..14473f7422 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -22,7 +22,7 @@ use litebox::{ shim::ContinueOperation, utils::{ReinterpretUnsignedExt, TruncateExt}, }; -use litebox_common_linux::{MapFlags, ProtFlags, errno::Errno}; +use litebox_common_linux::{MapFlags, ProtFlags, errno::Errno, vmap::GlobalVmapManager}; use litebox_common_optee::{ LdelfArg, LdelfSyscallRequest, SyscallRequest, TaFlags, TeeAlgorithm, TeeAlgorithmClass, TeeAttributeType, TeeCrypStateHandle, TeeHandleFlag, TeeIdentity, TeeLogin, TeeObjHandle, @@ -35,7 +35,6 @@ pub mod session; pub(crate) mod syscalls; pub mod msg_handler; -pub mod ptr; // Re-export session management types for convenience pub use session::{OpenSessionTarget, SessionManager, SessionToken, TaInstance}; @@ -1489,8 +1488,20 @@ impl SessionIdPool { } } -pub type NormalWorldConstPtr = crate::ptr::PhysConstPtr; -pub type NormalWorldMutPtr = crate::ptr::PhysMutPtr; +/// Type-level marker for the normal-world physical-pointer provider. +pub enum Vmap {} + +impl GlobalVmapManager for Vmap { + type Manager = litebox_platform_multiplex::Platform; + fn manager() -> &'static Self::Manager { + litebox_platform_multiplex::platform() + } +} + +pub type NormalWorldConstPtr = + litebox_common_linux::physical_pointers::PhysConstPtr; +pub type NormalWorldMutPtr = + litebox_common_linux::physical_pointers::PhysMutPtr; #[cfg(test)] mod test_utils { diff --git a/litebox_shim_optee/src/msg_handler.rs b/litebox_shim_optee/src/msg_handler.rs index 303129239e..f989e20239 100644 --- a/litebox_shim_optee/src/msg_handler.rs +++ b/litebox_shim_optee/src/msg_handler.rs @@ -181,10 +181,11 @@ pub fn read_optee_msg_args_from_phys( let mut blob = alloc::vec![0u8; copy_size]; - let mut blob_ptr = + let blob_ptr = NormalWorldConstPtr::::with_contiguous_pages(phys_addr, copy_size) .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; - unsafe { blob_ptr.read_slice_at_offset(0, &mut blob) } + blob_ptr + .read_slice_at_offset(0, &mut blob) .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; parse_optee_msg_args(&blob, has_rpc_arg) @@ -751,11 +752,8 @@ impl ShmInfo { { return Err(OpteeSmcReturnCode::EBadAddr); } - let mut ptr = NormalWorldConstPtr::::new(&self.page_addrs, self.page_offset)?; - // SAFETY: bounds validated above; copy lands in a buffer owned by LiteBox to avoid TOCTOU issues. - unsafe { - ptr.read_slice_at_offset(offset, buffer)?; - } + let ptr = NormalWorldConstPtr::::new(&self.page_addrs, self.page_offset)?; + ptr.read_slice_at_offset(offset, buffer)?; Ok(()) } @@ -766,11 +764,8 @@ impl ShmInfo { if buffer.len() > self.len { return Err(OpteeSmcReturnCode::EBadAddr); } - let mut ptr = NormalWorldMutPtr::::new(&self.page_addrs, self.page_offset)?; - // SAFETY: bounds validated above; data comes from a buffer owned by LiteBox. - unsafe { - ptr.write_slice_at_offset(0, buffer)?; - } + let ptr = NormalWorldMutPtr::::new(&self.page_addrs, self.page_offset)?; + ptr.write_slice_at_offset(0, buffer)?; Ok(()) } } @@ -847,10 +842,11 @@ impl ShmRefMap { return Err(OpteeSmcReturnCode::EBadAddr); } visited_pages_data.insert(cur_addr); - let mut cur_ptr = NormalWorldConstPtr::::with_usize(cur_addr) + let cur_ptr = NormalWorldConstPtr::::with_usize(cur_addr) + .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; + let pages_data = cur_ptr + .read_at_offset(0) .map_err(|_| OpteeSmcReturnCode::EBadAddr)?; - let pages_data = - unsafe { cur_ptr.read_at_offset(0) }.map_err(|_| OpteeSmcReturnCode::EBadAddr)?; let pages_len_before = pages.len(); for page in &pages_data.pages_list { if *page == 0 || pages.len() == num_pages { diff --git a/litebox_shim_optee/src/ptr.rs b/litebox_shim_optee/src/ptr.rs deleted file mode 100644 index 06a492506e..0000000000 --- a/litebox_shim_optee/src/ptr.rs +++ /dev/null @@ -1,585 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Physical Pointer Abstraction with On-demand Mapping -//! -//! This module adds supports for accessing physical addresses (e.g., VTL0 or -//! normal-world physical memory) from LiteBox with on-demand mapping. -//! In the context of LVBS and OP-TEE, accessing physical memory is necessary -//! because VTL0 and VTL1 as well as normal world and secure world do not share -//! the same virtual address space, but they still have to share data through memory. -//! VTL1 and secure world receive physical addresses from VTL0 and normal world, -//! respectively, and they need to read from or write to those addresses. -//! -//! To simplify all these, we could persistently map the entire VTL0/normal-world -//! physical memory into VTL1/secure-world address space at once and just access them -//! through corresponding virtual addresses. However, this module does not take these -//! approaches due to scalability (e.g., how to deal with a system with terabytes of -//! physical memory?) and security concerns (e.g., data corruption or information -//! leakage due to concurrent or persistent access). -//! -//! Instead, the approach this module takes is to map the required physical memory -//! region on-demand when accessing them while using a LiteBox-owned buffer to copy -//! data to/from those regions. This way, this module can ensure that data must be -//! copied into LiteBox-owned memory before being used while avoiding any unknown -//! side effects due to persistent memory mapping. -//! -//! Considerations: -//! -//! Ideally, this module should be able to validate whether a given physical address -//! is okay to access or even exists in the first place. For example, accessing -//! LiteBox's own memory with this physical pointer abstraction must be prohibited to -//! prevent the Boomerang attack and any other undefined memory access. Also, some -//! device memory is mapped to certain physical address ranges and LiteBox should not -//! touch them without in-depth knowledge. However, this is a bit tricky because, in -//! many cases, LiteBox does not directly interact with the underlying hardware or -//! BIOS/UEFI such that it does not have complete knowledge of the physical memory -//! layout. In the case of LVBS, LiteBox obtains the physical memory information -//! from VTL0 including the total physical memory size and the memory range assigned -//! to VTL1/LiteBox. Thus, this module can at least confirm a given physical address -//! does not belong to VTL1's physical memory. -//! -//! This module should allow byte-level access while transparently handling page -//! mapping and data access across page boundaries. This could become complicated -//! when we consider multiple page sizes (e.g., 4 KiB, 2 MiB, 1 GiB). Also, -//! unaligned access is a matter to be considered. -//! -//! In addition, often times, this physical pointer abstraction is involved with -//! a list of physical addresses (i.e., scatter-gather list). For example, in -//! the worse case, a two-byte value can span across two non-contiguous physical -//! pages (the last byte of the first page and the first byte of the second page). -//! Thus, to enhance the performance, we may need to consider mapping multiple pages -//! at once, copy data from/to them, and unmap them later. -//! -//! When this module needs to access data across physical page boundaries, it assumes -//! that those physical pages are virtually contiguous in VTL0 or normal-world address -//! space. Otherwise, this module could end up with accessing misordered data. This is -//! best-effort assumption and ensuring this is the caller's responsibility (e.g., even -//! if this module always requires a list of physical addresses, the caller might -//! provide a wrong list by mistake or intentionally). - -// TODO: Since the below `PhysMutPtr` and `PhysConstPtr` are not OP-TEE specific, -// we can move them to a different crate (e.g., `litebox`) if needed. - -use litebox_common_linux::vmap::{ - PhysPageAddr, PhysPageMapInfo, PhysPageMapPermissions, PhysPointerError, VmapManager, -}; -use litebox_platform_multiplex::platform; -use zerocopy::FromBytes; - -/// Allocate a zeroed `Box` on the heap. -/// -/// # Panics -/// -/// Panics if `T` is a zero-sized type, since `alloc_zeroed` with a zero-sized -/// layout is undefined behavior. -fn box_new_zeroed() -> alloc::boxed::Box { - assert!( - core::mem::size_of::() > 0, - "box_new_zeroed does not support zero-sized types" - ); - let layout = core::alloc::Layout::new::(); - // Safety: layout has a non-zero size and correct alignment for T. - let ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }.cast::(); - if ptr.is_null() { - alloc::alloc::handle_alloc_error(layout); - } - // Safety: ptr is a valid, zeroed, properly aligned heap allocation for T. - // T: FromBytes guarantees all-zero is a valid bit pattern. - unsafe { alloc::boxed::Box::from_raw(ptr) } -} - -#[inline] -fn align_down(address: usize, align: usize) -> usize { - address & !(align - 1) -} - -/// Represent a physical pointer to an object with on-demand mapping. -/// - `pages`: An array of page-aligned physical addresses. We expect physical addresses in this array are -/// virtually contiguous. -/// - `offset`: The offset within `pages[0]` where the object starts. It should be smaller than `ALIGN`. -/// - `count`: The number of objects of type `T` that can be accessed from this pointer. -/// - `map_info`: The mapping information of the currently mapped physical pages, if any. -/// - `T`: The type of the object being pointed to. `pages` with respect to `offset` should cover enough -/// memory for an object of type `T`. -#[derive(Clone)] -#[repr(C)] -pub struct PhysMutPtr { - pages: alloc::boxed::Box<[PhysPageAddr]>, - offset: usize, - count: usize, - map_info: Option>, - _type: core::marker::PhantomData, -} - -impl PhysMutPtr { - /// Create a new `PhysMutPtr` from the given physical page array and offset. - /// - /// All addresses in `pages` should be valid and aligned to `ALIGN`, and `offset` should be - /// smaller than `ALIGN`. Also, `pages` should contain enough pages to cover at least one - /// object of type `T` starting from `offset`. If these conditions are not met, this function - /// returns `Err(PhysPointerError)`. - pub fn new(pages: &[PhysPageAddr], offset: usize) -> Result { - if offset >= ALIGN { - return Err(PhysPointerError::InvalidBaseOffset(offset, ALIGN)); - } - let size = if pages.is_empty() { - 0 - } else { - pages - .len() - .checked_mul(ALIGN) - .ok_or(PhysPointerError::Overflow)? - - offset - }; - if size < core::mem::size_of::() { - return Err(PhysPointerError::InsufficientPhysicalPages( - size, - core::mem::size_of::(), - )); - } - platform().validate_unowned(pages)?; - Ok(Self { - pages: pages.into(), - offset, - count: size / core::mem::size_of::(), - map_info: None, - _type: core::marker::PhantomData, - }) - } - - /// Create a new `PhysMutPtr` from the given contiguous physical address and length. - /// - /// This is a shortcut for - /// `PhysMutPtr::new([align_down(pa), align_down(pa) + ALIGN, ..., align_up(pa + bytes) - ALIGN], pa % ALIGN)`. - /// This function assumes that `pa`, ..., `pa+bytes` are both physically and virtually contiguous. If not, - /// later accesses through `PhysMutPtr` may read/write data in a wrong order. - pub fn with_contiguous_pages(pa: usize, bytes: usize) -> Result { - if bytes < core::mem::size_of::() { - return Err(PhysPointerError::InsufficientPhysicalPages( - bytes, - core::mem::size_of::(), - )); - } - let start_page = align_down(pa, ALIGN); - let end_page = pa - .checked_add(bytes) - .and_then(|end| end.checked_next_multiple_of(ALIGN)) - .ok_or(PhysPointerError::Overflow)?; - let span = end_page - .checked_sub(start_page) - .ok_or(PhysPointerError::Overflow)?; - let mut pages = alloc::vec::Vec::with_capacity(span / ALIGN); - let mut current_page = start_page; - while current_page < end_page { - pages.push( - PhysPageAddr::::new(current_page) - .ok_or(PhysPointerError::InvalidPhysicalAddress(current_page))?, - ); - current_page = current_page - .checked_add(ALIGN) - .ok_or(PhysPointerError::Overflow)?; - } - Self::new(&pages, pa - start_page) - } - - /// Create a new `PhysMutPtr` from the given physical address for a single object. - /// - /// This is a shortcut for `PhysMutPtr::with_contiguous_pages(pa, size_of::())`. - /// - /// Note: This module doesn't provide `as_usize` because LiteBox should not dereference physical addresses directly. - pub fn with_usize(pa: usize) -> Result { - Self::with_contiguous_pages(pa, core::mem::size_of::()) - } - - /// Read the value at the given offset from the physical pointer. - /// - /// # Safety - /// - /// The caller should be aware that the given physical address might be concurrently written by - /// other entities (e.g., the normal world kernel) if there is no extra security mechanism - /// in place (e.g., by the hypervisor or hardware). That is, it might read corrupt data. - /// `FromBytes` is required to ensure T is valid for any bit pattern from untrusted physical memory. - pub unsafe fn read_at_offset( - &mut self, - count: usize, - ) -> Result, PhysPointerError> - where - T: FromBytes, - { - if count >= self.count { - return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); - } - let guard = unsafe { - self.map_and_get_ptr_guard( - count, - core::mem::size_of::(), - PhysPageMapPermissions::READ, - )? - }; - let mut boxed = box_new_zeroed::(); - // Fallible: another core may unmap this page concurrently. - let result = unsafe { - litebox::mm::exception_table::memcpy_fallible( - core::ptr::from_mut::(boxed.as_mut()).cast::(), - guard.ptr.cast::(), - guard.size, - ) - }; - debug_assert!(result.is_ok(), "fault reading from mapped physical page"); - result.map_err(|_| PhysPointerError::CopyFailed)?; - Ok(boxed) - } - - /// Read a slice of values at the given offset from the physical pointer. - /// - /// # Safety - /// - /// The caller should be aware that the given physical address might be concurrently written by - /// other entities (e.g., the normal world kernel) if there is no extra security mechanism - /// in place (e.g., by the hypervisor or hardware). That is, it might read corrupt data. - /// `FromBytes` is required to ensure T is valid for any bit pattern from untrusted physical memory. - pub unsafe fn read_slice_at_offset( - &mut self, - count: usize, - values: &mut [T], - ) -> Result<(), PhysPointerError> - where - T: FromBytes, - { - if count - .checked_add(values.len()) - .is_none_or(|end| end > self.count) - { - return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); - } - let guard = unsafe { - self.map_and_get_ptr_guard( - count, - core::mem::size_of_val(values), - PhysPageMapPermissions::READ, - )? - }; - // Fallible: another core may unmap this page concurrently. - let result = unsafe { - litebox::mm::exception_table::memcpy_fallible( - values.as_mut_ptr().cast::(), - guard.ptr.cast::(), - guard.size, - ) - }; - debug_assert!(result.is_ok(), "fault reading from mapped physical page"); - result.map_err(|_| PhysPointerError::CopyFailed)?; - Ok(()) - } - - /// Write the value at the given offset to the physical pointer. - /// - /// # Safety - /// - /// The caller should be aware that the given physical address might be concurrently written by - /// other entities (e.g., the normal world kernel) if there is no extra security mechanism - /// in place (e.g., by the hypervisor or hardware). That is, data it writes might be overwritten. - pub unsafe fn write_at_offset( - &mut self, - count: usize, - value: T, - ) -> Result<(), PhysPointerError> { - if count >= self.count { - return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); - } - let guard = unsafe { - self.map_and_get_ptr_guard( - count, - core::mem::size_of::(), - PhysPageMapPermissions::READ | PhysPageMapPermissions::WRITE, - )? - }; - // Fallible: another core may unmap this page concurrently. - let result = unsafe { - litebox::mm::exception_table::memcpy_fallible( - guard.ptr.cast::(), - core::ptr::from_ref(&value).cast::(), - guard.size, - ) - }; - debug_assert!(result.is_ok(), "fault writing to mapped physical page"); - result.map_err(|_| PhysPointerError::CopyFailed)?; - Ok(()) - } - - /// Write a slice of values at the given offset to the physical pointer. - /// - /// # Safety - /// - /// The caller should be aware that the given physical address might be concurrently written by - /// other entities (e.g., the normal world kernel) if there is no extra security mechanism - /// in place (e.g., by the hypervisor or hardware). That is, data it writes might be overwritten. - pub unsafe fn write_slice_at_offset( - &mut self, - count: usize, - values: &[T], - ) -> Result<(), PhysPointerError> { - if count - .checked_add(values.len()) - .is_none_or(|end| end > self.count) - { - return Err(PhysPointerError::IndexOutOfBounds(count, self.count)); - } - let guard = unsafe { - self.map_and_get_ptr_guard( - count, - core::mem::size_of_val(values), - PhysPageMapPermissions::READ | PhysPageMapPermissions::WRITE, - )? - }; - // Fallible: another core may unmap this page concurrently. - let result = unsafe { - litebox::mm::exception_table::memcpy_fallible( - guard.ptr.cast::(), - values.as_ptr().cast::(), - guard.size, - ) - }; - debug_assert!(result.is_ok(), "fault writing to mapped physical page"); - result.map_err(|_| PhysPointerError::CopyFailed)?; - Ok(()) - } - - /// This function maps physical pages for the requested data element at a given - /// index and returns a guard that unmaps on drop. - /// - /// It bridges element-level access (used by `read_at_offset`, `write_at_offset`, etc.) - /// with page-level mapping. It determines which physical pages contain the requested - /// element, maps them into virtual memory, and returns a pointer adjusted for - /// the element's position. - /// - /// - `count`: Element index (0-based) within this physical pointer's range. - /// - `size`: Total byte size to map (must cover the data being accessed). - /// - `perms`: Required page permissions (read, write). - /// - /// # Safety - /// - /// Same as [`Self::map_range`]. The returned guard borrows `self` mutably, ensuring - /// the mapping is released when the guard goes out of scope. - unsafe fn map_and_get_ptr_guard( - &mut self, - count: usize, - size: usize, - perms: PhysPageMapPermissions, - ) -> Result, PhysPointerError> { - let skip = self - .offset - .checked_add( - count - .checked_mul(core::mem::size_of::()) - .ok_or(PhysPointerError::Overflow)?, - ) - .ok_or(PhysPointerError::Overflow)?; - let start = skip / ALIGN; - let end = skip - .checked_add(size) - .ok_or(PhysPointerError::Overflow)? - .div_ceil(ALIGN); - unsafe { - self.map_range(start, end, perms)?; - } - let map_info = self - .map_info - .as_ref() - .ok_or(PhysPointerError::NoMappingInfo)?; - let ptr = map_info.base.wrapping_add(skip % ALIGN).cast::(); - let _ = map_info; - Ok(MappedGuard { - owner: self, - ptr, - size, - }) - } - - /// Map the physical pages from `start` to `end` indexes. - /// - /// # Safety - /// - /// This function assumes that the underlying platform safely handles concurrent mapping/unmapping - /// requests for the same physical pages. - unsafe fn map_range( - &mut self, - start: usize, - end: usize, - perms: PhysPageMapPermissions, - ) -> Result<(), PhysPointerError> { - if start >= end || end > self.pages.len() { - return Err(PhysPointerError::IndexOutOfBounds(end, self.pages.len())); - } - let accept_perms = PhysPageMapPermissions::READ | PhysPageMapPermissions::WRITE; - if perms.bits() & !accept_perms.bits() != 0 { - return Err(PhysPointerError::UnsupportedPermissions(perms.bits())); - } - if self.map_info.is_none() { - let sub_pages = &self.pages[start..end]; - unsafe { - self.map_info = Some(platform().vmap(sub_pages, perms)?); - } - Ok(()) - } else { - Err(PhysPointerError::AlreadyMapped( - self.pages.first().map_or(0, |p| p.as_usize()), - )) - } - } - - /// Unmap the physical pages if mapped. - /// - /// # Safety - /// - /// This function assumes that the underlying platform safely handles concurrent mapping/unmapping - /// requests for the same physical pages. - unsafe fn unmap(&mut self) -> Result<(), PhysPointerError> { - if let Some(map_info) = self.map_info.take() { - unsafe { - platform().vunmap(map_info)?; - } - Ok(()) - } else { - Err(PhysPointerError::Unmapped( - self.pages.first().map_or(0, |p| p.as_usize()), - )) - } - } -} - -/// RAII guard that unmaps physical pages when dropped. -/// -/// Created by `map_and_get_ptr_guard`. Holds a mutable borrow on the parent -/// `PhysMutPtr` and provides the mapped base pointer for the duration of the mapping. -struct MappedGuard<'a, T: Clone, const ALIGN: usize> { - owner: &'a mut PhysMutPtr, - ptr: *mut T, - size: usize, -} - -impl Drop for MappedGuard<'_, T, ALIGN> { - fn drop(&mut self) { - // SAFETY: The platform is expected to handle unmapping safely, including - // the case where pages were never mapped (returns Unmapped error, ignored). - let result = unsafe { self.owner.unmap() }; - debug_assert!( - result.is_ok() || matches!(result, Err(PhysPointerError::Unmapped(_))), - "unexpected error during unmap in drop: {result:?}", - ); - } -} - -impl Drop for PhysMutPtr { - fn drop(&mut self) { - // SAFETY: The platform is expected to handle unmapping safely, including - // the case where pages were never mapped (returns Unmapped error, ignored). - let result = unsafe { self.unmap() }; - debug_assert!( - result.is_ok() || matches!(result, Err(PhysPointerError::Unmapped(_))), - "unexpected error during unmap in drop: {result:?}", - ); - } -} - -impl core::fmt::Debug for PhysMutPtr { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("PhysMutPtr") - .field("pages[0]", &self.pages.first().map_or(0, |p| p.as_usize())) - .field("offset", &self.offset) - .finish_non_exhaustive() - } -} - -/// Represent a physical pointer to a read-only object. This wraps around [`PhysMutPtr`] and -/// exposes only read access. -#[derive(Clone)] -#[repr(C)] -pub struct PhysConstPtr { - inner: PhysMutPtr, -} - -impl PhysConstPtr { - /// Create a new `PhysConstPtr` from the given physical page array and offset. - /// - /// All addresses in `pages` should be valid and aligned to `ALIGN`, and `offset` should be smaller - /// than `ALIGN`. Also, `pages` should contain enough pages to cover at least one object of - /// type `T` starting from `offset`. If these conditions are not met, this function returns - /// `Err(PhysPointerError)`. - pub fn new(pages: &[PhysPageAddr], offset: usize) -> Result { - Ok(Self { - inner: PhysMutPtr::new(pages, offset)?, - }) - } - - /// Create a new `PhysConstPtr` from the given contiguous physical address and length. - /// - /// This is a shortcut for - /// `PhysConstPtr::new([align_down(pa), align_down(pa) + ALIGN, ..., align_up(pa + bytes) - ALIGN], pa % ALIGN)`. - /// This function assumes that `pa`, ..., `pa+bytes` are both physically and virtually contiguous. If not, - /// later accesses through `PhysConstPtr` may read data in a wrong order. - pub fn with_contiguous_pages(pa: usize, bytes: usize) -> Result { - Ok(Self { - inner: PhysMutPtr::with_contiguous_pages(pa, bytes)?, - }) - } - - /// Create a new `PhysConstPtr` from the given physical address for a single object. - /// - /// This is a shortcut for `PhysConstPtr::with_contiguous_pages(pa, size_of::())`. - /// - /// Note: This module doesn't provide `as_usize` because LiteBox should not dereference physical addresses directly. - pub fn with_usize(pa: usize) -> Result { - Ok(Self { - inner: PhysMutPtr::with_usize(pa)?, - }) - } - - /// Read the value at the given offset from the physical pointer. - /// - /// # Safety - /// - /// The caller should be aware that the given physical address might be concurrently written by - /// other entities (e.g., the normal world kernel) if there is no extra security mechanism - /// in place (e.g., by the hypervisor or hardware). That is, it might read corrupt data. - pub unsafe fn read_at_offset( - &mut self, - count: usize, - ) -> Result, PhysPointerError> - where - T: FromBytes, - { - unsafe { self.inner.read_at_offset(count) } - } - - /// Read a slice of values at the given offset from the physical pointer. - /// - /// # Safety - /// - /// The caller should be aware that the given physical address might be concurrently written by - /// other entities (e.g., the normal world kernel) if there is no extra security mechanism - /// in place (e.g., by the hypervisor or hardware). That is, it might read corrupt data. - pub unsafe fn read_slice_at_offset( - &mut self, - count: usize, - values: &mut [T], - ) -> Result<(), PhysPointerError> - where - T: FromBytes, - { - unsafe { self.inner.read_slice_at_offset(count, values) } - } -} - -impl core::fmt::Debug for PhysConstPtr { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("PhysConstPtr") - .field( - "pages[0]", - &self.inner.pages.first().map_or(0, |p| p.as_usize()), - ) - .field("offset", &self.inner.offset) - .finish_non_exhaustive() - } -} From 9ce5b2ea8002568b8ed73a99e58fbf87346c2ac8 Mon Sep 17 00:00:00 2001 From: Will Portnoy Date: Sun, 12 Jul 2026 13:03:32 -0700 Subject: [PATCH 093/319] Load ELF interpreter high so a low-loaded ET_EXEC's brk heap isn't capped (#1023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of #985 (squash commit `f5ad1f20`) onto `ulitebox`. Dynamically-linked ET_EXEC (non-PIE) binaries load at their low canonical vaddrs and grow their glibc brk heap upward from there, but the ELF interpreter was reserved at a fixed low offset directly above the main image — capping the heap, so a heap-heavy process (e.g. a bundled node) hit ENOMEM the moment it crossed that offset. This loads the interpreter at the top of the address space instead, mirroring how the kernel places ld.so for an ET_EXEC main and leaving the whole gap above the main image free for the heap; it is a no-op for PIE mains, whose interpreter already loads high. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/loader/elf.rs | 204 ++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 3 deletions(-) diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index d867676a84..fef7fa45ab 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -25,6 +25,7 @@ use crate::{ShimFS, Task}; struct ElfFile<'a, FS: ShimFS> { task: &'a Task, fd: i32, + load_high: bool, } impl<'a, FS: ShimFS> ElfFile<'a, FS> { @@ -32,7 +33,11 @@ impl<'a, FS: ShimFS> ElfFile<'a, FS> { let fd = task .sys_open(path, OFlags::RDONLY, Mode::empty())? .reinterpret_as_signed(); - Ok(ElfFile { task, fd }) + Ok(ElfFile { + task, + fd, + load_high: false, + }) } } @@ -75,10 +80,22 @@ impl litebox_common_linux::loader::MapMemory for ElfFile<'_, FS> { // Allocate a mapping large enough that even if it's maximally misaligned we can // still fit `len` bytes. let mapping_len = len + (align.max(PAGE_SIZE) - PAGE_SIZE); + let hint = if self.load_high { + // Reserve the interpreter top-down by passing no hint: LiteBox's + // `get_unmmaped_area` then runs its top-down search and returns + // the highest free slot (see `litebox/src/mm/linux.rs`), which is + // where we want `ld.so` so the low ET_EXEC brk heap below stays + // uncapped. This needs no explicit `TASK_ADDR_MAX` arithmetic and + // no reserve-once bookkeeping, and it does not rely on any + // platform honoring an out-of-range hint. + 0 + } else { + super::DEFAULT_LOW_ADDR + }; let mapping_ptr = self .task .sys_mmap( - super::DEFAULT_LOW_ADDR, + hint, mapping_len, litebox_common_linux::ProtFlags::PROT_NONE, litebox_common_linux::MapFlags::MAP_ANONYMOUS @@ -225,7 +242,11 @@ impl<'a, FS: ShimFS> ElfLoader<'a, FS> { // Parse the interpreter ELF file, if any. let interp = if let Some(interp_name) = main.parsed.interp(&mut &main.file)? { // e.g., /lib64/ld-linux-x86-64.so.2 - Some(FileAndParsed::new(task, interp_name)?) + let mut interp = FileAndParsed::new(task, interp_name)?; + // Linux places the ET_EXEC interpreter high so brk can grow above + // the fixed-address main image without hitting ld.so. + interp.file.load_high = true; + Some(interp) } else { None }; @@ -317,3 +338,180 @@ impl From for litebox_common_linux::errno::Errno { } } } + +#[cfg(test)] +mod tests { + extern crate std; + + use alloc::vec::Vec; + + use litebox::{ + fs::{Mode, OFlags}, + platform::PageManagementProvider, + }; + use litebox_platform_multiplex::Platform; + + use super::*; + + const ELF_HEADER_SIZE: usize = 64; + const ELF_HEADER_SIZE_U16: u16 = 64; + const PROGRAM_HEADER_SIZE_U16: u16 = 56; + const ET_EXEC: u16 = 2; + const ET_DYN: u16 = 3; + const EM_X86_64: u16 = 62; + const PT_LOAD: u32 = 1; + const PT_INTERP: u32 = 3; + const PF_X: u32 = 1; + const PF_R: u32 = 4; + const EXEC_LOAD_ADDR: u64 = 0x400000; + const INTERP_PATH_OFFSET: usize = 0x200; + const INTERP_PATH: &[u8] = b"/ld.so\0"; + + #[derive(Clone, Copy)] + struct ProgramHeader { + typ: u32, + flags: u32, + offset: u64, + vaddr: u64, + filesz: u64, + memsz: u64, + align: u64, + } + + fn push_u16(buf: &mut Vec, value: u16) { + buf.extend_from_slice(&value.to_le_bytes()); + } + + fn push_u32(buf: &mut Vec, value: u32) { + buf.extend_from_slice(&value.to_le_bytes()); + } + + fn push_u64(buf: &mut Vec, value: u64) { + buf.extend_from_slice(&value.to_le_bytes()); + } + + fn append_elf_header(buf: &mut Vec, elf_type: u16, entry: u64, phnum: u16) { + buf.extend_from_slice(b"\x7fELF"); + buf.extend_from_slice(&[2, 1, 1, 0]); + buf.extend_from_slice(&[0; 8]); + push_u16(buf, elf_type); + push_u16(buf, EM_X86_64); + push_u32(buf, 1); + push_u64(buf, entry); + push_u64(buf, u64::from(ELF_HEADER_SIZE_U16)); + push_u64(buf, 0); + push_u32(buf, 0); + push_u16(buf, ELF_HEADER_SIZE_U16); + push_u16(buf, PROGRAM_HEADER_SIZE_U16); + push_u16(buf, phnum); + push_u16(buf, 0); + push_u16(buf, 0); + push_u16(buf, 0); + assert_eq!(buf.len(), ELF_HEADER_SIZE); + } + + fn append_program_header(buf: &mut Vec, ph: ProgramHeader) { + push_u32(buf, ph.typ); + push_u32(buf, ph.flags); + push_u64(buf, ph.offset); + push_u64(buf, ph.vaddr); + push_u64(buf, ph.vaddr); + push_u64(buf, ph.filesz); + push_u64(buf, ph.memsz); + push_u64(buf, ph.align); + } + + fn minimal_elf(elf_type: u16, interp: Option<&[u8]>) -> Vec { + let phnum = if interp.is_some() { 2 } else { 1 }; + let page_size = u64::try_from(PAGE_SIZE).expect("PAGE_SIZE fits u64"); + let entry = if elf_type == ET_EXEC { + EXEC_LOAD_ADDR + } else { + 0 + }; + let mut buf = Vec::new(); + append_elf_header(&mut buf, elf_type, entry, phnum); + append_program_header( + &mut buf, + ProgramHeader { + typ: PT_LOAD, + flags: PF_R | PF_X, + offset: 0, + vaddr: if elf_type == ET_EXEC { + EXEC_LOAD_ADDR + } else { + 0 + }, + filesz: page_size, + memsz: page_size, + align: page_size, + }, + ); + if let Some(interp) = interp { + append_program_header( + &mut buf, + ProgramHeader { + typ: PT_INTERP, + flags: PF_R, + offset: u64::try_from(INTERP_PATH_OFFSET).expect("offset fits u64"), + vaddr: 0, + filesz: u64::try_from(interp.len()).expect("interpreter path length fits u64"), + memsz: u64::try_from(interp.len()).expect("interpreter path length fits u64"), + align: 1, + }, + ); + } + buf.resize(PAGE_SIZE, 0); + if let Some(interp) = interp { + buf[INTERP_PATH_OFFSET..INTERP_PATH_OFFSET + interp.len()].copy_from_slice(interp); + } + buf + } + + fn write_file(task: &Task, path: &str, data: &[u8]) { + let fd = task + .sys_open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("failed to create test ELF"); + let fd = i32::try_from(fd).expect("fd fits i32"); + task.sys_write(fd, data, None) + .expect("failed to write test ELF"); + task.sys_close(fd).expect("failed to close test ELF"); + } + + #[test] + fn et_exec_interpreter_loads_top_down_above_low_heap() { + let task = crate::syscalls::tests::init_platform(None); + write_file(&task, "/main", &minimal_elf(ET_EXEC, Some(INTERP_PATH))); + write_file(&task, "/ld.so", &minimal_elf(ET_DYN, None)); + + let mut loader = ElfLoader::new(&task, "/main").expect("loader should parse test ELFs"); + let main = loader + .main + .load_mapped(task.global.platform) + .expect("main should load"); + assert_eq!(main.base_addr, 0); + + let interp = loader + .interp + .as_mut() + .expect("test main should have PT_INTERP") + .load_mapped(task.global.platform) + .expect("interpreter should load"); + + // The interpreter must land high — via the top-down search — so the + // low ET_EXEC brk heap below it is not capped. The exact address is + // not asserted: `get_unmmaped_area` returns the highest free gap, and + // host mappings seeded into the userland VMA tree can sit near the top + // and push that gap below the very top slot (see `mm/linux.rs`). Assert + // the invariant that matters — placement in the high half of the + // address space, far above the low-heap region — not one exact slot. + let addr_max = >::TASK_ADDR_MAX; + assert!( + interp.base_addr >= addr_max / 2, + "ET_EXEC interpreter loaded at {:#x}, near the low-heap region {:#x} rather than top-down high (>= {:#x})", + interp.base_addr, + crate::loader::DEFAULT_LOW_ADDR, + addr_max / 2, + ); + } +} From afce07094154355ddc007a50062e6e6417fe3b07 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 14 Jul 2026 14:29:03 -0700 Subject: [PATCH 094/319] Refactor device ConDrv handling (#1027) Refactor console-driver (\Device\ConDrv) support in the Windows shim so console objects are modeled as first-class NT file devices routed through the object manager and adds `NtDeviceIoControlFile` so guest console setup IOCTLs are handled. Previously ConDrv paths were resolved by ad-hoc string prefix matching; this PR also unifies path resolution. --- litebox_shim_windows/src/lib.rs | 26 + litebox_shim_windows/src/loader/pe.rs | 36 +- litebox_shim_windows/src/syscalls/condrv.rs | 306 ++++++++++ litebox_shim_windows/src/syscalls/event.rs | 16 + litebox_shim_windows/src/syscalls/file.rs | 566 ++++++++++++------ .../src/syscalls/file_path.rs | 254 ++++++++ litebox_shim_windows/src/syscalls/mod.rs | 26 + .../src/syscalls/object_manager.rs | 201 +++++-- litebox_shim_windows/src/syscalls/symlink.rs | 6 +- 9 files changed, 1181 insertions(+), 256 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/condrv.rs create mode 100644 litebox_shim_windows/src/syscalls/file_path.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 2e3a098302..5c1d5b2f88 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -1093,6 +1093,32 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtDeviceIoControlFile { + file_handle, + event, + apc_routine, + apc_context, + io_status_block, + io_control_code, + input_buffer, + input_buffer_length, + output_buffer, + output_buffer_length, + } => { + let status = self.sys_nt_device_io_control_file( + file_handle, + event, + apc_routine, + apc_context, + io_status_block, + io_control_code, + input_buffer, + input_buffer_length, + output_buffer, + output_buffer_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtApphelpCacheControl { service_class, service_data, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 8fc050db84..c859f81066 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -35,7 +35,7 @@ use crate::syscalls::process::{INITIAL_PROCESS_ID, INITIAL_THREAD_ID}; use crate::{MutPtr, ShimFS}; const NTDLL_WRITABLE_SECTIONS: &[&[u8]] = &[b".mrdata"]; -const NTDLL_PATHS: &[&str] = &["/Windows/System32/ntdll.dll", "/windows/system32/ntdll.dll"]; +const NTDLL_PATH: &str = "/Windows/System32/ntdll.dll"; const RUNTIME_FUNCTION_ENTRY_SIZE: usize = 12; const ZERO_CHUNK: [u8; PAGE_SIZE] = [0; PAGE_SIZE]; const FILE_CHUNK_BYTES: usize = 64 * 1024; @@ -1038,26 +1038,24 @@ fn load_ntdll( fs: Arc, page_manager: &crate::WindowsPageManager, ) -> Result, WindowsLoadError> { - for path in NTDLL_PATHS { - match load_image_with_writable_sections( - fs.clone(), - path, - platform, - page_manager, - NTDLL_WRITABLE_SECTIONS, - ) { - Ok(image) => { - let exports = ntdll_exports::(&image)?; - litebox_util_log::debug!(path:% = path; "Loaded guest ntdll.dll"); - return Ok(Some(LoadedNtDll { image, exports })); - } - Err(error) if is_missing_file_error(&error) => {} - Err(error) => return Err(error), + match load_image_with_writable_sections( + fs, + NTDLL_PATH, + platform, + page_manager, + NTDLL_WRITABLE_SECTIONS, + ) { + Ok(image) => { + let exports = ntdll_exports::(&image)?; + litebox_util_log::debug!(path:% = NTDLL_PATH; "Loaded guest ntdll.dll"); + Ok(Some(LoadedNtDll { image, exports })) + } + Err(error) if is_missing_file_error(&error) => { + litebox_util_log::debug!("Guest ntdll.dll was not found in the initial filesystem"); + Ok(None) } + Err(error) => Err(error), } - - litebox_util_log::debug!("Guest ntdll.dll was not found in the initial filesystem"); - Ok(None) } fn load_image( diff --git a/litebox_shim_windows/src/syscalls/condrv.rs b/litebox_shim_windows/src/syscalls/condrv.rs new file mode 100644 index 0000000000..d56d66eacd --- /dev/null +++ b/litebox_shim_windows/src/syscalls/condrv.rs @@ -0,0 +1,306 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows console driver support. + +use core::mem::size_of; + +use int_enum::IntEnum; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::nt_types::IoStatusBlock; +use crate::{ConstPtr, MutPtr}; + +const FILE_DEVICE_CONSOLE: u32 = 0x50; +const CD_SERVER_EA_NAME: &[u8] = b"server"; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +pub(crate) enum CondrvObject { + Input = 0, + Output = 1, + Server = 2, + Reference = 3, + Connect = 4, +} + +impl CondrvObject { + pub(crate) fn from_device_name(name: &str) -> Result { + match Self::from_component(name) { + Some(object @ (Self::Input | Self::Output | Self::Server)) => Ok(object), + Some(Self::Reference) => Err(NtStatus::INVALID_HANDLE), + Some(Self::Connect) => Err(NtStatus::OBJECT_TYPE_MISMATCH), + None => Err(NtStatus::OBJECT_NAME_NOT_FOUND), + } + } + + fn from_component(name: &str) -> Option { + if name.eq_ignore_ascii_case("Input") { + Some(Self::Input) + } else if name.eq_ignore_ascii_case("Output") { + Some(Self::Output) + } else if name.eq_ignore_ascii_case("Server") { + Some(Self::Server) + } else if name.eq_ignore_ascii_case("Reference") { + Some(Self::Reference) + } else if name.eq_ignore_ascii_case("Connect") { + Some(Self::Connect) + } else { + None + } + } + + pub(crate) fn relative_child(self, name: &str) -> Result { + let name = name.strip_prefix('\\').ok_or(NtStatus::NOT_FOUND)?; + let child = Self::from_component(name).ok_or(NtStatus::NOT_FOUND)?; + + match (self, child) { + (Self::Server, Self::Server | Self::Reference) + | (Self::Reference, Self::Server | Self::Connect | Self::Input | Self::Output) + | (Self::Input | Self::Output, Self::Server | Self::Input | Self::Output) => Ok(child), + (Self::Server, Self::Input | Self::Output) => Err(NtStatus::INVALID_DEVICE_STATE), + (Self::Reference | Self::Input | Self::Output, Self::Reference) => { + Err(NtStatus::OBJECT_TYPE_MISMATCH) + } + (Self::Server | Self::Input | Self::Output, Self::Connect) | (Self::Connect, _) => { + Err(NtStatus::INVALID_HANDLE) + } + } + } + + pub(crate) fn handle_path(self) -> &'static str { + match self { + Self::Input => "/dev/stdin", + Self::Output => "/dev/stdout", + Self::Server => r"\Device\ConDrv\Server", + Self::Reference => r"\Device\ConDrv\Reference", + Self::Connect => r"\Device\ConDrv\Connect", + } + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum IoControlMethod { + Buffered = 0, + InDirect = 1, + OutDirect = 2, + Neither = 3, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct IoControlAccess: u32 { + const ANY = 0; + const READ = 1; + const WRITE = 2; + const _ = !0; + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum ConsoleIoControlFunction { + LaunchServer = 13, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct FileFullEaInformation { + next_entry_offset: u32, + flags: u8, + ea_name_length: u8, + ea_value_length: u16, +} + +#[cfg(test)] +pub(crate) fn ea_buffer(name: &[u8], value_length: usize) -> alloc::vec::Vec { + let header = FileFullEaInformation { + next_entry_offset: 0, + flags: 0, + ea_name_length: u8::try_from(name.len()).unwrap(), + ea_value_length: u16::try_from(value_length).unwrap(), + }; + let mut buffer = alloc::vec::Vec::new(); + buffer.extend_from_slice(header.as_bytes()); + buffer.extend_from_slice(name); + buffer.push(0); + buffer.resize(buffer.len() + value_length, 0); + buffer +} + +pub(crate) fn validate_connect_server_ea( + ea_buffer: Option>, + ea_length: u32, +) -> Result<(), NtStatus> { + let Some(ea_buffer) = ea_buffer else { + return Err(NtStatus::EAS_NOT_SUPPORTED); + }; + let ea_length = ea_length as usize; + let Some(entry) = ConstPtr::::from_usize(ea_buffer.as_usize()) + .read_at_offset(0) + else { + return Err(NtStatus::ACCESS_VIOLATION); + }; + + let name_offset = size_of::(); + let name_length = entry.ea_name_length as usize; + let value_length = entry.ea_value_length as usize; + let value_offset = name_offset + .checked_add(name_length) + .and_then(|offset| offset.checked_add(1)) + .ok_or(NtStatus::EAS_NOT_SUPPORTED)?; + let entry_length = value_offset + .checked_add(value_length) + .ok_or(NtStatus::EAS_NOT_SUPPORTED)?; + if entry_length > ea_length { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + + let Some(name_address) = ea_buffer.as_usize().checked_add(name_offset) else { + return Err(NtStatus::EAS_NOT_SUPPORTED); + }; + let Some(name_with_nul) = + ConstPtr::::from_usize(name_address).to_owned_slice(name_length + 1) + else { + return Err(NtStatus::ACCESS_VIOLATION); + }; + let Some((&0, name)) = name_with_nul.split_last() else { + return Err(NtStatus::EAS_NOT_SUPPORTED); + }; + if !name.eq_ignore_ascii_case(CD_SERVER_EA_NAME) { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + + let Some(value_address) = ea_buffer.as_usize().checked_add(value_offset) else { + return Err(NtStatus::EAS_NOT_SUPPORTED); + }; + // The ConDrv "server" EA value format is undocumented; probe the declared payload without + // interpreting it until its semantics are understood. + if ConstPtr::::from_usize(value_address) + .to_owned_slice(value_length) + .is_none() + { + return Err(NtStatus::ACCESS_VIOLATION); + } + + Ok(()) +} + +pub(crate) fn handle_ioctl( + condrv_object: CondrvObject, + io_status_block: MutPtr, + io_control_code: u32, + input_buffer: Option>, + input_buffer_length: u32, + output_buffer: Option>, + output_buffer_length: u32, +) -> NtStatus { + let device_type = io_control_code >> 16; + let access = IoControlAccess::from_bits_retain((io_control_code >> 14) & 0x3); + let function = (io_control_code >> 2) & 0xfff; + let method = IoControlMethod::try_from(io_control_code & 0x3); + + if device_type != FILE_DEVICE_CONSOLE || method != Ok(IoControlMethod::Neither) { + litebox_util_log::debug!( + condrv_object:? = condrv_object, + io_control_code:% = format_args!("{io_control_code:#x}"); + "Unsupported ConDrv IOCTL shape" + ); + return complete_ioctl::(io_status_block, NtStatus::NOT_SUPPORTED, 0); + } + + let Ok(function) = ConsoleIoControlFunction::try_from(function) else { + litebox_util_log::debug!( + condrv_object:? = condrv_object, + io_control_code:% = format_args!("{io_control_code:#x}"); + "Unsupported ConDrv IOCTL function" + ); + return complete_ioctl::(io_status_block, NtStatus::NOT_SUPPORTED, 0); + }; + + match (condrv_object, function) { + (CondrvObject::Server, ConsoleIoControlFunction::LaunchServer) + if access.is_empty() + && input_buffer.is_some() + && input_buffer_length != 0 + && output_buffer.is_none() + && output_buffer_length == 0 => + { + if input_buffer + .and_then(|input_buffer| input_buffer.read_at_offset(0)) + .is_none() + { + return complete_ioctl::(io_status_block, NtStatus::ACCESS_VIOLATION, 0); + } + complete_ioctl::(io_status_block, NtStatus::SUCCESS, 0) + } + _ => { + litebox_util_log::debug!( + condrv_object:? = condrv_object, + function:? = function, + io_control_code:% = format_args!("{io_control_code:#x}"); + "Unsupported ConDrv IOCTL for object" + ); + complete_ioctl::(io_status_block, NtStatus::NOT_SUPPORTED, 0) + } + } +} + +pub(crate) fn complete_ioctl( + io_status_block: MutPtr, + status: NtStatus, + information: usize, +) -> NtStatus { + if io_status_block + .write_at_offset(0, IoStatusBlock::new(status, information)) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + status +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn relative_children_match_host_parse_contexts() { + use CondrvObject::{Connect, Input, Output, Reference, Server}; + + for (parent, name, expected) in [ + (Server, r"\Server", Ok(Server)), + (Server, r"\Reference", Ok(Reference)), + (Server, r"\Connect", Err(NtStatus::INVALID_HANDLE)), + (Server, r"\Input", Err(NtStatus::INVALID_DEVICE_STATE)), + (Server, r"\Output", Err(NtStatus::INVALID_DEVICE_STATE)), + (Reference, r"\Server", Ok(Server)), + (Reference, r"\Connect", Ok(Connect)), + (Reference, r"\Input", Ok(Input)), + (Reference, r"\Output", Ok(Output)), + ( + Reference, + r"\Reference", + Err(NtStatus::OBJECT_TYPE_MISMATCH), + ), + (Input, r"\Server", Ok(Server)), + (Input, r"\Input", Ok(Input)), + (Input, r"\Output", Ok(Output)), + (Input, r"\Reference", Err(NtStatus::OBJECT_TYPE_MISMATCH)), + (Input, r"\Connect", Err(NtStatus::INVALID_HANDLE)), + (Output, r"\Server", Ok(Server)), + (Output, r"\Input", Ok(Input)), + (Output, r"\Output", Ok(Output)), + (Output, r"\Reference", Err(NtStatus::OBJECT_TYPE_MISMATCH)), + (Output, r"\Connect", Err(NtStatus::INVALID_HANDLE)), + ] { + assert_eq!(parent.relative_child(name), expected, "{parent:?} + {name}"); + } + + assert_eq!(Server.relative_child("Reference"), Err(NtStatus::NOT_FOUND)); + assert_eq!(Server.relative_child(r"\Missing"), Err(NtStatus::NOT_FOUND)); + } +} diff --git a/litebox_shim_windows/src/syscalls/event.rs b/litebox_shim_windows/src/syscalls/event.rs index 92bf3bdf97..09c5c0554e 100644 --- a/litebox_shim_windows/src/syscalls/event.rs +++ b/litebox_shim_windows/src/syscalls/event.rs @@ -399,6 +399,22 @@ impl Task { } } + pub(crate) fn set_event(&self, event_handle: Handle) -> NtStatus { + match self.modify_event(event_handle, None, |event| Ok(event.set())) { + Ok(()) => NtStatus::SUCCESS, + Err(status) => status, + } + } + + pub(crate) fn clear_event(&self, event_handle: Handle) -> Result<(), NtStatus> { + self.modify_event(event_handle, None, |event| Ok(event.clear())) + } + + pub(crate) fn check_event_modify_access(&self, event_handle: Handle) -> Result<(), NtStatus> { + let entry = self.event_entry(event_handle)?; + entry.with_entry(|entry| entry.granted_access.require(EventAccess::MODIFY_STATE)) + } + pub(crate) fn sys_nt_reset_event( &self, event_handle: Handle, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 76b48ccb48..9bb5457fcd 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -17,6 +17,8 @@ use crate::nt_types::{ AccessMask, IoStatusBlock, ObjectAttributes, UnicodeString, read_object_attributes, }; use crate::syscalls::Handle; +use crate::syscalls::condrv::{self, CondrvObject}; +use crate::syscalls::file_path::{FilePathResolver, FilePathRoot, FileTarget}; use crate::{ ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value, raw_handle_entry, }; @@ -27,12 +29,6 @@ const FILE_SHARE_READ: u32 = 0x0000_0001; const FILE_SHARE_WRITE: u32 = 0x0000_0002; const FILE_SHARE_DELETE: u32 = 0x0000_0004; -const CONDRV_INPUT_OBJECT: &str = "Input"; -const CONDRV_OUTPUT_OBJECT: &str = "Output"; -const CONDRV_SERVER_DEVICE: &str = "Server"; -const CONDRV_REFERENCE_OBJECT: &str = "Reference"; -const CONDRV_CONNECT_OBJECT: &str = "Connect"; - // These names and values are Windows ABI constants from WDK headers; Wine's // regular file/directory branch and ReactOS' filesystem device query path use // the same FILE_DEVICE_* and FILE_DEVICE_IS_MOUNTED vocabulary. @@ -93,13 +89,44 @@ impl FdEnabledSubsystemEntry for FileObject {} pub(crate) struct FileObject { path: String, - fd: TypedFd, + backing: FileObjectBacking, granted_access: FileAccess, share_access: FileShareAccess, - is_directory: bool, create_options: FileCreateOptions, } +enum FileObjectBacking { + Filesystem { + fd: TypedFd, + is_directory: bool, + }, + CondrvStream { + object: CondrvObject, + fd: TypedFd, + }, + CondrvControl(CondrvObject), +} + +impl FileObject { + fn condrv_object(&self) -> Option { + match self.backing { + FileObjectBacking::CondrvStream { object, .. } + | FileObjectBacking::CondrvControl(object) => Some(object), + FileObjectBacking::Filesystem { .. } => None, + } + } + + fn is_directory(&self) -> bool { + matches!( + self.backing, + FileObjectBacking::Filesystem { + is_directory: true, + .. + } + ) + } +} + bitflags::bitflags! { /// File object `ACCESS_MASK` rights accepted by `NtOpenFile`/`NtCreateFile`. /// @@ -353,16 +380,24 @@ impl Task { } pub(crate) fn close_file(&self, file: FileObject) { - let _ = self.fs.close(&file.fd); - if file - .create_options - .contains(FileCreateOptions::DELETE_ON_CLOSE) - { - if file.is_directory { - let _ = self.fs.rmdir(&file.path); - } else { - let _ = self.fs.unlink(&file.path); + match file.backing { + FileObjectBacking::Filesystem { fd, is_directory } => { + let _ = self.fs.close(&fd); + if file + .create_options + .contains(FileCreateOptions::DELETE_ON_CLOSE) + { + if is_directory { + let _ = self.fs.rmdir(&file.path); + } else { + let _ = self.fs.unlink(&file.path); + } + } } + FileObjectBacking::CondrvStream { fd, .. } => { + let _ = self.fs.close(&fd); + } + FileObjectBacking::CondrvControl(_) => {} } } @@ -485,6 +520,76 @@ impl Task { status } + #[expect( + clippy::too_many_arguments, + reason = "NtDeviceIoControlFile has ten ABI parameters; keeping them explicit preserves syscall ordering" + )] + pub(crate) fn sys_nt_device_io_control_file( + &self, + file_handle: Handle, + event: Handle, + apc_routine: Option>, + apc_context: Option>, + io_status_block: MutPtr, + io_control_code: u32, + input_buffer: Option>, + input_buffer_length: u32, + output_buffer: Option>, + output_buffer_length: u32, + ) -> NtStatus { + if let Err(status) = + probe_guest_output_preserving_value::(io_status_block) + { + return status; + } + if !event.is_null() + && let Err(status) = self.check_event_modify_access(event) + { + return status; + } + + let condrv_object = match self.file_entry(file_handle) { + Ok(entry) => entry.with_entry(FileObject::condrv_object), + Err(status) => return status, + }; + if !event.is_null() + && let Err(status) = self.clear_event(event) + { + return status; + } + let Some(condrv_object) = condrv_object else { + litebox_util_log::debug!( + file_handle = file_handle.as_raw(), + io_control_code:% = format_args!("{io_control_code:#x}"); + "Unsupported NtDeviceIoControlFile for non-ConDrv file handle" + ); + return NtStatus::INVALID_DEVICE_REQUEST; + }; + if apc_routine.is_some() || apc_context.is_some() { + litebox_util_log::debug!( + file_handle = file_handle.as_raw(), + apc_context = apc_context.map_or(0, |context| context.as_usize()); + "Ignoring NtDeviceIoControlFile APC completion arguments for synchronous completion" + ); + } + let status = condrv::handle_ioctl::( + condrv_object, + io_status_block, + io_control_code, + input_buffer, + input_buffer_length, + output_buffer, + output_buffer_length, + ); + if !event.is_null() { + let event_status = self.set_event(event); + if event_status != NtStatus::SUCCESS { + return event_status; + } + } + status + } + fn write_file_fs_device_information( &self, file_handle: Handle, @@ -553,17 +658,50 @@ impl Task { if object_attributes.object_name == 0 { return Err(NtStatus::INVALID_PARAMETER); } - if ea_buffer.is_some() || ea_length != 0 { - return Err(NtStatus::EAS_NOT_SUPPORTED); - } let desired_access = FileAccess::from_desired_access(desired_access); let create_options = FileCreateOptions::from_bits_retain(create_options); validate_create_options(desired_access, create_disposition, create_options)?; let share_access = FileShareAccess::from_share_access(share_access)?; - let path = self.object_attributes_to_fs_path(object_attributes)?; - self.check_file_sharing(&path, desired_access, share_access)?; + let (file, information) = match self.object_attributes_to_file_target(object_attributes)? { + FileTarget::Filesystem(path) => { + if ea_buffer.is_some() || ea_length != 0 { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + self.open_filesystem_target( + path, + desired_access, + share_access, + create_disposition, + create_options, + file_attributes, + ) + } + FileTarget::Condrv(object) => self.open_condrv_target( + object, + desired_access, + share_access, + create_disposition, + create_options, + file_attributes, + ea_buffer, + ea_length, + ), + }?; + let handle = self.insert_file_handle(file)?; + Ok((handle, information)) + } + fn open_filesystem_target( + &self, + path: String, + desired_access: FileAccess, + share_access: FileShareAccess, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, + file_attributes: u32, + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + self.check_file_sharing(&path, desired_access, share_access)?; if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { return self.open_or_create_directory( &path, @@ -575,7 +713,88 @@ impl Task { ); } - let existed_before_open = self.fs.file_status(&path).is_ok(); + let (fd, is_directory, information) = self.open_backing_fd( + &path, + desired_access, + create_disposition, + create_options, + file_attributes, + )?; + Ok(( + FileObject { + path, + backing: FileObjectBacking::Filesystem { fd, is_directory }, + granted_access: desired_access, + share_access, + create_options, + }, + information, + )) + } + + #[expect( + clippy::too_many_arguments, + reason = "ConDrv creation validates the parsed NtCreateFile fields at the device boundary" + )] + fn open_condrv_target( + &self, + object: CondrvObject, + desired_access: FileAccess, + share_access: FileShareAccess, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, + file_attributes: u32, + ea_buffer: Option>, + ea_length: u32, + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + if object == CondrvObject::Connect { + condrv::validate_connect_server_ea::(ea_buffer, ea_length)?; + } else if ea_buffer.is_some() || ea_length != 0 { + return Err(NtStatus::EAS_NOT_SUPPORTED); + } + if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { + return Err(NtStatus::NOT_A_DIRECTORY); + } + + let path = String::from(object.handle_path()); + self.check_file_sharing(&path, desired_access, share_access)?; + let (backing, information) = match object { + CondrvObject::Input | CondrvObject::Output => { + let (fd, _, information) = self.open_backing_fd( + &path, + desired_access, + create_disposition, + create_options, + file_attributes, + )?; + (FileObjectBacking::CondrvStream { object, fd }, information) + } + CondrvObject::Server | CondrvObject::Reference | CondrvObject::Connect => ( + FileObjectBacking::CondrvControl(object), + FileCreateInformation::Opened, + ), + }; + Ok(( + FileObject { + path, + backing, + granted_access: desired_access, + share_access, + create_options, + }, + information, + )) + } + + fn open_backing_fd( + &self, + path: &str, + desired_access: FileAccess, + create_disposition: CreateDisposition, + create_options: FileCreateOptions, + file_attributes: u32, + ) -> Result<(TypedFd, bool, FileCreateInformation), NtStatus> { + let existed_before_open = self.fs.file_status(path).is_ok(); if create_disposition == CreateDisposition::Supersede && existed_before_open && !desired_access.contains(FileAccess::DELETE) @@ -585,7 +804,7 @@ impl Task { let flags = desired_access.open_flags(create_disposition, create_options); let fd = self .fs - .open(&path, flags, create_mode(file_attributes)) + .open(path, flags, create_mode(file_attributes)) .map_err(|error| map_open_error(error, create_disposition))?; let file_status = match self.fs.fd_file_status(&fd) { Ok(file_status) => file_status, @@ -601,15 +820,11 @@ impl Task { return Err(NtStatus::OBJECT_TYPE_MISMATCH); } let information = create_disposition.success_information(existed_before_open); - let handle = self.insert_file_handle(FileObject { - path, + Ok(( fd, - granted_access: desired_access, - share_access, - is_directory: file_status.file_type == FileType::Directory, - create_options, - })?; - Ok((handle, information)) + file_status.file_type == FileType::Directory, + information, + )) } fn open_or_create_directory( @@ -620,7 +835,7 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, file_attributes: u32, - ) -> Result<(Handle, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { if matches!( create_disposition, CreateDisposition::Supersede @@ -662,37 +877,48 @@ impl Task { .open(path, flags, Mode::empty()) .map_err(|error| map_open_error(error, create_disposition))?; let information = create_disposition.success_information(existed_before_open); - let handle = self.insert_file_handle(FileObject { - path: String::from(path), - fd, - granted_access: desired_access, - share_access, - is_directory: true, - create_options, - })?; - Ok((handle, information)) + Ok(( + FileObject { + path: String::from(path), + backing: FileObjectBacking::Filesystem { + fd, + is_directory: true, + }, + granted_access: desired_access, + share_access, + create_options, + }, + information, + )) } - fn object_attributes_to_fs_path( + fn object_attributes_to_file_target( &self, object_attributes: ObjectAttributes, - ) -> Result { + ) -> Result { let object_name_ptr = ConstPtr::::from_usize(object_attributes.object_name); let object_name = object_name_ptr .read_at_offset(0) .ok_or(NtStatus::ACCESS_VIOLATION)?; let object_name = object_name.read_string::()?; - if object_attributes.root_directory.is_null() || is_absolute_windows_path(&object_name) { - return absolute_nt_file_name_to_fs_path(&object_name); + let resolver = FilePathResolver::new(&self.process.object_manager); + if object_attributes.root_directory.is_null() { + return resolver.resolve(FilePathRoot::Namespace, &object_name); } let root_file = self.file_entry(object_attributes.root_directory)?; root_file.with_entry(|root_file| { - if !root_file.is_directory { - return Err(NtStatus::NOT_A_DIRECTORY); + if let Some(parent) = root_file.condrv_object() { + return resolver.resolve(FilePathRoot::Condrv(parent), &object_name); } - relative_nt_file_name_to_fs_path(&root_file.path, &object_name) + resolver.resolve( + FilePathRoot::Filesystem { + path: &root_file.path, + is_directory: root_file.is_directory(), + }, + &object_name, + ) }) } @@ -843,120 +1069,6 @@ fn create_directory_mode(file_attributes: u32) -> Mode { create_mode(file_attributes) | Mode::XUSR } -/// Convert the NT file-name forms we currently support at the object-manager to -/// filesystem seam. -/// -/// Native NT reaches this seam by walking object-manager directories until it -/// reaches a device object, then the device parse routine hands the remaining -/// path to the filesystem driver. LiteBox intentionally uses the Wine-style -/// shortcut here instead: known NT prefixes are recognized as strings and then -/// mapped directly into the sandbox filesystem. Today that includes `\??\`, -/// `\\?\`, any drive-letter prefix, both `\SystemRoot\` and `/SystemRoot/`, -/// `\Device\HarddiskVolume1\`, and `\Device\ConDrv\`. A unified object-manager -/// walk through device objects into the backing filesystem namespace remains -/// outside this file-path mapper. -fn absolute_nt_file_name_to_fs_path(name: &str) -> Result { - let mut name = name; - if let Some(rest) = strip_case_insensitive_prefix(name, "\\??\\") { - name = rest; - } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\\\?\\") { - name = rest; - } - - if name.len() >= 3 && name.as_bytes()[1] == b':' && matches!(name.as_bytes()[2], b'\\' | b'/') { - name = &name[2..]; - } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\SystemRoot\\") { - return join_absolute_components("/Windows", rest); - } else if let Some(rest) = strip_case_insensitive_prefix(name, "/SystemRoot/") { - return join_absolute_components("/Windows", rest); - } else if let Some(rest) = strip_case_insensitive_prefix(name, "\\Device\\HarddiskVolume1\\") { - return join_absolute_components("/", rest); - } else if let Some(device_name) = strip_case_insensitive_prefix(name, "\\Device\\ConDrv\\") { - return condrv_device_file(device_name).ok_or(NtStatus::OBJECT_NAME_NOT_FOUND); - } - - let path = name.trim_start_matches(['\\', '/']); - join_absolute_components("/", path) -} - -fn relative_nt_file_name_to_fs_path(root_path: &str, name: &str) -> Result { - if is_absolute_windows_path(name) { - return absolute_nt_file_name_to_fs_path(name); - } - join_absolute_components(root_path, name) -} - -fn join_absolute_components(root_path: &str, components: &str) -> Result { - let mut path = String::from(root_path.trim_end_matches('/')); - if path.is_empty() { - path.push('/'); - } - for component in components.split(['\\', '/']) { - if component.is_empty() || component == "." { - continue; - } - if component == ".." { - return Err(NtStatus::INVALID_PARAMETER); - } - if !path.ends_with('/') { - path.push('/'); - } - append_windows_component(&mut path, component); - } - Ok(path) -} - -fn condrv_device_file(device_name: &str) -> Option { - if device_name.eq_ignore_ascii_case(CONDRV_INPUT_OBJECT) { - return Some(String::from("/dev/stdin")); - } - if device_name.eq_ignore_ascii_case(CONDRV_OUTPUT_OBJECT) { - return Some(String::from("/dev/stdout")); - } - if device_name.eq_ignore_ascii_case(CONDRV_SERVER_DEVICE) - || device_name.eq_ignore_ascii_case(CONDRV_REFERENCE_OBJECT) - || device_name.eq_ignore_ascii_case(CONDRV_CONNECT_OBJECT) - { - return Some(String::from("/dev/null")); - } - None -} - -fn append_windows_component(path: &mut String, component: &str) { - if component.eq_ignore_ascii_case("Windows") { - path.push_str("Windows"); - } else if component.eq_ignore_ascii_case("System32") { - path.push_str("System32"); - } else if ends_with_ignore_ascii_case(component, ".dll") - || ends_with_ignore_ascii_case(component, ".nls") - { - path.push_str(&component.to_ascii_lowercase()); - } else { - path.push_str(component); - } -} - -fn strip_case_insensitive_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { - value - .get(..prefix.len()) - .is_some_and(|head| head.eq_ignore_ascii_case(prefix)) - .then(|| &value[prefix.len()..]) -} - -fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool { - value - .get(value.len().saturating_sub(suffix.len())..) - .is_some_and(|tail| tail.eq_ignore_ascii_case(suffix)) -} - -fn is_absolute_windows_path(name: &str) -> bool { - name.starts_with(['\\', '/']) - || name - .as_bytes() - .get(1..3) - .is_some_and(|bytes| bytes[0] == b':' && matches!(bytes[1], b'\\' | b'/')) -} - fn map_open_error(error: OpenError, create_disposition: CreateDisposition) -> NtStatus { match error { OpenError::PathError(error) => match error { @@ -1105,6 +1217,97 @@ mod tests { handle } + fn open_condrv_server(task: &Task) -> Handle { + let (_server_path, _server_name, server_attributes) = + open_object_attributes(r"\Device\ConDrv\Server"); + let mut io_status = IoStatusBlock::default(); + task.do_nt_create_file( + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + server_attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0 + } + + fn open_condrv_reference(task: &Task, server_handle: Handle) -> Handle { + let (_reference_path, _reference_name, mut reference_attributes) = + open_object_attributes(r"\Reference"); + reference_attributes.root_directory = server_handle; + let mut io_status = IoStatusBlock::default(); + task.do_nt_create_file( + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + reference_attributes, + mut_ptr(&mut io_status), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + CreateDisposition::Open, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ) + .unwrap() + .0 + } + + #[test] + fn nt_create_file_follows_condrv_server_reference_connect_sequence() { + let task = crate::tests::test_task(); + let server_handle = open_condrv_server(&task); + let reference_handle = open_condrv_reference(&task, server_handle); + let (_connect_path, _connect_name, mut connect_attributes) = + open_object_attributes(r"\Connect"); + connect_attributes.root_directory = reference_handle; + let ea = condrv::ea_buffer(b"server", 1340); + let mut connect_handle = Handle::default(); + let mut io_status = IoStatusBlock::default(); + + assert_eq!( + task.file_entry(server_handle) + .unwrap() + .with_entry(FileObject::condrv_object), + Some(CondrvObject::Server) + ); + assert_eq!( + task.file_entry(reference_handle) + .unwrap() + .with_entry(FileObject::condrv_object), + Some(CondrvObject::Reference) + ); + assert_eq!( + task.sys_nt_create_file( + mut_ptr(&mut connect_handle), + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + Some(const_ptr(&connect_attributes)), + mut_ptr(&mut io_status), + None, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + Some(const_ptr(&ea[0])), + u32::try_from(ea.len()).unwrap(), + ), + NtStatus::SUCCESS + ); + assert_eq!( + task.file_entry(connect_handle) + .unwrap() + .with_entry(FileObject::condrv_object), + Some(CondrvObject::Connect) + ); + + assert_eq!(task.sys_nt_close(connect_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(reference_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(server_handle), NtStatus::SUCCESS); + } + #[test] fn nt_query_volume_information_file_returns_fs_device_information() { run_with_test_platform_pointers(|| { @@ -1245,7 +1448,8 @@ mod tests { .unwrap(); create_existing_file(&task, "/tmp/dir/child.txt", b"child"); - let (_path, _name, attributes) = open_object_attributes("\\tmp\\dir-file-root.txt"); + let (_path, _name, attributes) = + open_object_attributes(r"\Device\HarddiskVolume1\tmp\dir-file-root.txt"); let mut handle = Handle::default(); let mut io_status = IoStatusBlock::default(); assert_eq!( @@ -1265,7 +1469,8 @@ mod tests { usize::from(FileCreateInformation::Opened) ); - let (_path, _name, directory_attributes) = open_object_attributes("\\tmp\\dir"); + let (_path, _name, directory_attributes) = + open_object_attributes(r"\Device\HarddiskVolume1\tmp\dir"); let directory_handle = task .do_nt_create_file( FILE_GENERIC_READ, @@ -1821,37 +2026,6 @@ mod tests { assert_eq!(handle, original_handle); } - #[test] - fn nt_create_file_maps_dos_paths_into_the_sandbox_fs() { - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\??\C:\Windows\System32\ntdll.dll").unwrap(), - "/Windows/System32/ntdll.dll" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\??\c:\windows\system32\KERNEL32.DLL").unwrap(), - "/Windows/System32/kernel32.dll" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path( - r"\Device\HarddiskVolume1\Windows\System32\c_1252.NLS" - ) - .unwrap(), - "/Windows/System32/c_1252.nls" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\SystemRoot\System32\kernel32.dll").unwrap(), - "/Windows/System32/kernel32.dll" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\Device\ConDrv\Output").unwrap(), - "/dev/stdout" - ); - assert_eq!( - absolute_nt_file_name_to_fs_path(r"\Device\ConDrv\Connect").unwrap(), - "/dev/null" - ); - } - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] mod host_fidelity { use super::*; diff --git a/litebox_shim_windows/src/syscalls/file_path.rs b/litebox_shim_windows/src/syscalls/file_path.rs new file mode 100644 index 0000000000..5ac9e7a39c --- /dev/null +++ b/litebox_shim_windows/src/syscalls/file_path.rs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::string::String; + +use litebox_common_windows::nt_status::NtStatus; + +use crate::syscalls::condrv::CondrvObject; +use crate::syscalls::object_manager::{FileDeviceObject, ObjectManager}; + +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum FileTarget { + Filesystem(String), + Condrv(CondrvObject), +} + +pub(crate) enum FilePathRoot<'a> { + Namespace, + Filesystem { path: &'a str, is_directory: bool }, + Condrv(CondrvObject), +} + +pub(crate) struct FilePathResolver<'a, Platform: crate::ShimPlatform> { + object_manager: &'a ObjectManager, +} + +impl<'a, Platform: crate::ShimPlatform> FilePathResolver<'a, Platform> { + pub(crate) fn new(object_manager: &'a ObjectManager) -> Self { + Self { object_manager } + } + + pub(crate) fn resolve( + &self, + root: FilePathRoot<'_>, + name: &str, + ) -> Result { + match root { + FilePathRoot::Condrv(parent) => parent.relative_child(name).map(FileTarget::Condrv), + FilePathRoot::Namespace => self.resolve_absolute(name), + FilePathRoot::Filesystem { .. } if is_absolute_windows_path(name) => { + self.resolve_absolute(name) + } + FilePathRoot::Filesystem { + is_directory: false, + .. + } => Err(NtStatus::NOT_A_DIRECTORY), + FilePathRoot::Filesystem { path, .. } => { + join_absolute_components(path, name).map(FileTarget::Filesystem) + } + } + } + + fn resolve_absolute(&self, name: &str) -> Result { + if name.starts_with('/') { + return join_absolute_components("/", name).map(FileTarget::Filesystem); + } + if !is_absolute_windows_path(name) { + return Err(NtStatus::OBJECT_PATH_SYNTAX_BAD); + } + + let object_path = absolute_windows_file_name_to_object_path(name); + let (device, remaining) = self.object_manager.resolve_file_device(&object_path)?; + file_device_path_to_file_target(device, &remaining) + } +} + +fn absolute_windows_file_name_to_object_path(name: &str) -> String { + if let Some(rest) = strip_case_insensitive_prefix(name, "\\\\?\\") { + return alloc::format!(r"\??\{}", normalize_file_name_separators(rest)); + } + if name.starts_with('\\') { + return normalize_file_name_separators(name); + } + alloc::format!(r"\??\{}", normalize_file_name_separators(name)) +} + +fn normalize_file_name_separators(name: &str) -> String { + name.replace('/', "\\") +} + +fn file_device_path_to_file_target( + device: FileDeviceObject, + remaining: &str, +) -> Result { + match device { + FileDeviceObject::Filesystem { root_path } => { + join_absolute_components(&root_path, remaining).map(FileTarget::Filesystem) + } + FileDeviceObject::ConsoleDriver => { + CondrvObject::from_device_name(remaining).map(FileTarget::Condrv) + } + } +} + +fn join_absolute_components(root_path: &str, components: &str) -> Result { + let mut path = String::from(root_path.trim_end_matches('/')); + if path.is_empty() { + path.push('/'); + } + for component in components.split(['\\', '/']) { + if component.is_empty() || component == "." { + continue; + } + if component == ".." { + return Err(NtStatus::INVALID_PARAMETER); + } + if !path.ends_with('/') { + path.push('/'); + } + path.push_str(component); + } + Ok(path) +} + +fn strip_case_insensitive_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { + value + .get(..prefix.len()) + .is_some_and(|head| head.eq_ignore_ascii_case(prefix)) + .then(|| &value[prefix.len()..]) +} + +fn is_absolute_windows_path(name: &str) -> bool { + name.starts_with(['\\', '/']) || is_absolute_windows_drive_path(name) +} + +fn is_absolute_windows_drive_path(name: &str) -> bool { + name.as_bytes() + .get(1..3) + .is_some_and(|bytes| bytes[0] == b':' && matches!(bytes[1], b'\\' | b'/')) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn namespace_paths_resolve_through_the_object_manager() { + let task = crate::tests::test_task(); + let resolver = FilePathResolver::new(&task.process.object_manager); + let resolve = |name| resolver.resolve(FilePathRoot::Namespace, name); + let resolve_path = |name| { + resolve(name).map(|target| match target { + FileTarget::Filesystem(path) => path, + FileTarget::Condrv(object) => String::from(object.handle_path()), + }) + }; + + assert_eq!( + resolve_path(r"\??\C:\Windows\System32\ntdll.dll").unwrap(), + "/Windows/System32/ntdll.dll" + ); + assert_eq!( + resolve_path(r"\??\c:\windows\system32\KERNEL32.DLL").unwrap(), + "/windows/system32/KERNEL32.DLL" + ); + assert_eq!( + resolve_path(r"\Device\HarddiskVolume1\Windows\System32\c_1252.NLS").unwrap(), + "/Windows/System32/c_1252.NLS" + ); + assert_eq!( + resolve_path(r"\SystemRoot\System32\kernel32.dll").unwrap(), + "/Windows/System32/kernel32.dll" + ); + assert_eq!( + resolve_path(r"\Device\ConDrv\Output").unwrap(), + "/dev/stdout" + ); + assert_eq!( + resolve_path(r"\Device\ConDrv\Reference"), + Err(NtStatus::INVALID_HANDLE) + ); + assert_eq!( + resolve_path(r"\Device\ConDrv\Connect"), + Err(NtStatus::OBJECT_TYPE_MISMATCH) + ); + assert_eq!( + resolve(r"\Missing\file.txt"), + Err(NtStatus::OBJECT_PATH_NOT_FOUND) + ); + assert_eq!( + resolve("/tmp/compatibility-path.txt"), + Ok(FileTarget::Filesystem(String::from( + "/tmp/compatibility-path.txt" + ))) + ); + assert_eq!( + resolve("/SystemRoot/not-an-object-path"), + Ok(FileTarget::Filesystem(String::from( + "/SystemRoot/not-an-object-path" + ))) + ); + assert_eq!( + resolve("relative.txt"), + Err(NtStatus::OBJECT_PATH_SYNTAX_BAD) + ); + } + + #[test] + fn root_kind_controls_relative_path_resolution() { + let task = crate::tests::test_task(); + let resolver = FilePathResolver::new(&task.process.object_manager); + + assert_eq!( + resolver.resolve( + FilePathRoot::Filesystem { + path: "/tmp/root", + is_directory: true, + }, + r"child\file.txt", + ), + Ok(FileTarget::Filesystem(String::from( + "/tmp/root/child/file.txt" + ))) + ); + assert_eq!( + resolver.resolve( + FilePathRoot::Filesystem { + path: "/tmp/root", + is_directory: true, + }, + r"C:\Windows\System32\ntdll.dll", + ), + Ok(FileTarget::Filesystem(String::from( + "/Windows/System32/ntdll.dll" + ))) + ); + assert_eq!( + resolver.resolve( + FilePathRoot::Filesystem { + path: "/tmp/root", + is_directory: true, + }, + r"MixedCase\File.TXT", + ), + Ok(FileTarget::Filesystem(String::from( + "/tmp/root/MixedCase/File.TXT" + ))) + ); + assert_eq!( + resolver.resolve( + FilePathRoot::Filesystem { + path: "/tmp/root.txt", + is_directory: false, + }, + "child.txt", + ), + Err(NtStatus::NOT_A_DIRECTORY) + ); + assert_eq!( + resolver.resolve(FilePathRoot::Condrv(CondrvObject::Reference), r"\Connect"), + Ok(FileTarget::Condrv(CondrvObject::Connect)) + ); + } +} diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 652a44fec2..09a92765c7 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -2,8 +2,10 @@ // Licensed under the MIT license. pub(crate) mod apphelp; +pub(crate) mod condrv; pub(crate) mod event; pub(crate) mod file; +pub(crate) mod file_path; pub(crate) mod iocp; pub(crate) mod mm; pub(crate) mod nls; @@ -313,6 +315,18 @@ pub(crate) enum SyscallRequest { length: u32, fs_information_class: u32, }, + NtDeviceIoControlFile { + file_handle: Handle, + event: Handle, + apc_routine: Option>, + apc_context: Option>, + io_status_block: Platform::RawMutPointer, + io_control_code: u32, + input_buffer: Option>, + input_buffer_length: u32, + output_buffer: Option>, + output_buffer_length: u32, + }, NtApphelpCacheControl { service_class: u32, service_data: Option>, @@ -725,6 +739,18 @@ impl SyscallRequest { length, fs_information_class, })), + NtSysno::NtDeviceIoControlFile => Some(sys_req!(NtDeviceIoControlFile { + file_handle:{Handle::from_raw}, + event:{Handle::from_raw}, + apc_routine:*, + apc_context:*, + io_status_block:*, + io_control_code, + input_buffer:*, + input_buffer_length, + output_buffer:*, + output_buffer_length, + })), NtSysno::NtApphelpCacheControl => Some(sys_req!(NtApphelpCacheControl { service_class, service_data:*, diff --git a/litebox_shim_windows/src/syscalls/object_manager.rs b/litebox_shim_windows/src/syscalls/object_manager.rs index 936966851a..09b4d1bb6c 100644 --- a/litebox_shim_windows/src/syscalls/object_manager.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -58,6 +58,8 @@ const SEEDED_DIRECTORY_PATHS: &[&str] = &[ // Wine's wineboot and ReactOS SMSS create KnownDllPath so ntdll can open/query // the DOS path prefix for known DLL lookups during loader initialization. const SEEDED_SYMLINK_PATHS: &[(&str, &str)] = &[ + (r"\??\C:", r"\Device\HarddiskVolume1"), + (r"\SystemRoot", r"\Device\HarddiskVolume1\Windows"), (r"\KnownDlls\KnownDllPath", r"C:\Windows\System32"), // TODO(windows-sessions): resolve this through the current session id once // the shim supports multiple Windows sessions. @@ -138,6 +140,12 @@ pub(crate) struct ObjectManager { root: Arc>, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum FileDeviceObject { + Filesystem { root_path: String }, + ConsoleDriver, +} + enum NamedObject { Directory { children: BTreeMap>>, @@ -151,6 +159,9 @@ enum NamedObject { Section { section: Weak>, }, + FileDevice { + device: FileDeviceObject, + }, } pub(super) enum ObjectLeafLookup { @@ -319,6 +330,7 @@ impl ObjectNode { new_symlink(target: String) => NamedObject::Symlink { target }; new_event(event: Weak>) => NamedObject::Event { event }; new_section(section: Weak>) => NamedObject::Section { section }; + new_file_device(device: FileDeviceObject) => NamedObject::FileDevice { device }; } fn child(&self, name: &str) -> Option> { @@ -353,11 +365,16 @@ impl ObjectNode { matches!(&*self.body.read(), NamedObject::Symlink { .. }) } + fn is_file_device(&self) -> bool { + matches!(&*self.body.read(), NamedObject::FileDevice { .. }) + } + object_leaf_accessors! { directory_object, ObjectLeafLookup<()>, NamedObject::Directory { .. } => ObjectLeafLookup::Live(()); pub(super) symlink_target, ObjectLeafLookup, NamedObject::Symlink { target } => ObjectLeafLookup::Live(target.clone()); event_object, ObjectLeafLookup>>, NamedObject::Event { event } => ObjectLeafLookup::from_weak(event); section_object, ObjectLeafLookup>>, NamedObject::Section { section } => ObjectLeafLookup::from_weak(section); + file_device_object, ObjectLeafLookup, NamedObject::FileDevice { device } => ObjectLeafLookup::Live(device.clone()); } fn type_name(&self) -> Option<&'static str> { @@ -366,6 +383,7 @@ impl ObjectNode { NamedObject::Symlink { .. } => Some("SymbolicLink"), NamedObject::Event { event } => event.upgrade().map(|_| "Event"), NamedObject::Section { section } => section.upgrade().map(|_| "Section"), + NamedObject::FileDevice { .. } => Some("Device"), } } @@ -476,6 +494,17 @@ impl ObjectManager { ) } + fn create_file_device(&self, path: &str, device: FileDeviceObject) -> NtStatus { + self.create_child( + path, + |node| node.file_device_object(), + |path, parent, name| ObjectNode::new_file_device(path, parent, name, device), + NtStatus::OBJECT_TYPE_MISMATCH, + |_| NtStatus::OBJECT_NAME_EXISTS, + |_| NtStatus::SUCCESS, + ) + } + fn create_child( &self, path: &str, @@ -509,8 +538,11 @@ impl ObjectManager { return NtStatus::OBJECT_NAME_INVALID; } - let parent = match self.resolve_tail(parent_tail, NtStatus::OBJECT_PATH_NOT_FOUND, false) { - Ok(parent) => parent, + let parent = match self.resolve_tail(parent_tail, NtStatus::OBJECT_PATH_NOT_FOUND, true) { + Ok((parent, remaining)) if remaining.is_empty() => parent, + Ok((_, remaining)) => { + return unresolved_tail_status(&remaining, NtStatus::OBJECT_PATH_NOT_FOUND); + } Err(status) => return status, }; let mut body = parent.body.write(); @@ -545,7 +577,7 @@ impl ObjectManager { &self, path: &str, ) -> Result>, NtStatus> { - self.resolve_object_leaf(path, false, |node| { + self.resolve_object_leaf(path, true, |node| { node.directory_object().map(|()| Arc::clone(node)) }) } @@ -553,32 +585,58 @@ impl ObjectManager { pub(super) fn resolve_symlink( &self, path: &str, - open_final_symlink: bool, + follow_final_symlink: bool, ) -> Result>, NtStatus> { - self.resolve_object_leaf(path, open_final_symlink, |node| { + self.resolve_object_leaf(path, follow_final_symlink, |node| { node.symlink_target().map(|_| Arc::clone(node)) }) } pub(super) fn resolve_event(&self, path: &str) -> Result>, NtStatus> { - self.resolve_object_leaf(path, true, |node| node.event_object()) + self.resolve_object_leaf(path, false, |node| node.event_object()) } pub(super) fn resolve_section( &self, path: &str, ) -> Result>, NtStatus> { - self.resolve_object_leaf(path, false, |node| node.section_object()) + self.resolve_object_leaf(path, true, |node| node.section_object()) + } + + pub(crate) fn resolve_file_device( + &self, + path: &str, + ) -> Result<(FileDeviceObject, String), NtStatus> { + let tail = absolute_path_tail(path)?; + let (node, remaining) = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, true)?; + if node.is_file_device() { + return Ok((node.file_device_object().into_result()?, remaining)); + } + if remaining.is_empty() { + Err(NtStatus::OBJECT_TYPE_MISMATCH) + } else { + Err(unresolved_tail_status( + &remaining, + NtStatus::OBJECT_NAME_NOT_FOUND, + )) + } } fn resolve_object_leaf( &self, path: &str, - open_final_symlink: bool, + follow_final_symlink: bool, lookup: impl FnOnce(&Arc>) -> ObjectLeafLookup, ) -> Result { let tail = absolute_path_tail(path)?; - let node = self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, open_final_symlink)?; + let (node, remaining) = + self.resolve_tail(tail, NtStatus::OBJECT_NAME_NOT_FOUND, follow_final_symlink)?; + if !remaining.is_empty() { + return Err(unresolved_tail_status( + &remaining, + NtStatus::OBJECT_NAME_NOT_FOUND, + )); + } lookup(&node).into_result() } @@ -603,18 +661,32 @@ impl ObjectManager { ); } + fn seed_file_device(&self, path: &str, device: FileDeviceObject) { + let status = self.create_file_device(path, device); + assert!( + status == NtStatus::SUCCESS, + "seeded NT file device must have seeded ancestors: {status:?}" + ); + } + fn resolve_tail( &self, tail: &str, final_missing_status: NtStatus, - open_final_symlink: bool, - ) -> Result>, NtStatus> { + follow_final_symlink: bool, + ) -> Result<(Arc>, String), NtStatus> { let mut tail = tail.to_string(); for _ in 0..=MAX_SYMLINK_REPARSE_DEPTH { - match self.resolve_tail_once(&tail, final_missing_status, open_final_symlink)? { - TailResolution::Resolved(node) => return Ok(node), - TailResolution::Reparse(next_tail) => tail = next_tail, + let (node, remaining) = match self.resolve_tail_once(&tail) { + Ok(resolution) => resolution, + Err(NtStatus::OBJECT_NAME_NOT_FOUND) => return Err(final_missing_status), + Err(status) => return Err(status), + }; + if node.is_symlink() && (!remaining.is_empty() || follow_final_symlink) { + tail = reparse_tail(&node, &remaining)?; + continue; } + return Ok((node, remaining)); } Err(NtStatus::NAME_TOO_LONG) } @@ -622,11 +694,9 @@ impl ObjectManager { fn resolve_tail_once( &self, tail: &str, - final_missing_status: NtStatus, - open_final_symlink: bool, - ) -> Result, NtStatus> { + ) -> Result<(Arc>, String), NtStatus> { if tail.is_empty() { - return Ok(TailResolution::Resolved(Arc::clone(&self.root))); + return Ok((Arc::clone(&self.root), String::new())); } let mut current = Arc::clone(&self.root); @@ -637,35 +707,46 @@ impl ObjectManager { } let final_component = components.peek().is_none(); let missing_status = if final_component { - final_missing_status + NtStatus::OBJECT_NAME_NOT_FOUND } else { NtStatus::OBJECT_PATH_NOT_FOUND }; let child = current.child(component).ok_or(missing_status)?; - if child.is_symlink() && (!final_component || !open_final_symlink) { - // This is the lazy-resolution point paired with - // NtCreateSymbolicLinkObject storing the target without lookup. - let target = normalize_reparse_target(&child.symlink_target().into_result()?)?; - let target_tail = absolute_path_tail(&target)?; - let remaining = components.collect::>().join("\\"); - let next_tail = if target_tail.is_empty() { - remaining - } else if remaining.is_empty() { - target_tail.to_string() - } else { - alloc::format!("{target_tail}\\{remaining}") - }; - return Ok(TailResolution::Reparse(next_tail)); + if !child.is_directory() { + return Ok((child, components.collect::>().join("\\"))); } current = child; } - Ok(TailResolution::Resolved(current)) + Ok((current, String::new())) } } -enum TailResolution { - Resolved(Arc>), - Reparse(String), +fn reparse_tail( + node: &ObjectNode, + remaining: &str, +) -> Result { + // This is the lazy-resolution point paired with NtCreateSymbolicLinkObject + // storing the target without lookup. + let target = normalize_reparse_target(&node.symlink_target().into_result()?)?; + let target_tail = absolute_path_tail(&target)?; + if target_tail.is_empty() { + Ok(remaining.to_string()) + } else if remaining.is_empty() { + Ok(target_tail.to_string()) + } else { + Ok(alloc::format!("{target_tail}\\{remaining}")) + } +} + +fn unresolved_tail_status(remaining: &str, final_missing_status: NtStatus) -> NtStatus { + debug_assert!(!remaining.is_empty()); + if remaining.split('\\').any(str::is_empty) { + NtStatus::OBJECT_NAME_INVALID + } else if remaining.contains('\\') { + NtStatus::OBJECT_PATH_NOT_FOUND + } else { + final_missing_status + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1239,6 +1320,13 @@ pub(crate) fn seed_object_manager() for path in SEEDED_DIRECTORY_PATHS { object_manager.seed_directory(path); } + object_manager.seed_file_device( + r"\Device\HarddiskVolume1", + FileDeviceObject::Filesystem { + root_path: "/".to_string(), + }, + ); + object_manager.seed_file_device(r"\Device\ConDrv", FileDeviceObject::ConsoleDriver); for (path, target) in SEEDED_SYMLINK_PATHS { object_manager.seed_symlink(path, target); } @@ -1444,7 +1532,7 @@ mod tests { ); let shortcut = object_manager - .resolve_symlink(WINDOWS_SHARED_SECTION_OBJECT, true) + .resolve_symlink(WINDOWS_SHARED_SECTION_OBJECT, false) .expect("Windows shared section shortcut is a symbolic link"); assert_eq!( shortcut.symlink_target().into_result(), @@ -1457,6 +1545,43 @@ mod tests { }); } + #[test] + fn seeded_file_devices_resolve_through_object_manager() { + let object_manager = seed_object_manager::(); + + assert_eq!( + object_manager.resolve_file_device(r"\Device\HarddiskVolume1\Windows"), + Ok(( + FileDeviceObject::Filesystem { + root_path: "/".to_string(), + }, + "Windows".to_string(), + )) + ); + assert_eq!( + object_manager.resolve_file_device(r"\??\C:\Windows\System32"), + Ok(( + FileDeviceObject::Filesystem { + root_path: "/".to_string(), + }, + r"Windows\System32".to_string(), + )) + ); + assert_eq!( + object_manager.resolve_file_device(r"\SystemRoot\System32"), + Ok(( + FileDeviceObject::Filesystem { + root_path: "/".to_string(), + }, + r"Windows\System32".to_string(), + )) + ); + assert_eq!( + object_manager.resolve_file_device(r"\Device\ConDrv\Output"), + Ok((FileDeviceObject::ConsoleDriver, "Output".to_string())) + ); + } + #[test] fn open_directory_rejects_openlink_attribute() { run_with_test_platform_pointers(|| { diff --git a/litebox_shim_windows/src/syscalls/symlink.rs b/litebox_shim_windows/src/syscalls/symlink.rs index 1b7fb4afe5..08aadc6c52 100644 --- a/litebox_shim_windows/src/syscalls/symlink.rs +++ b/litebox_shim_windows/src/syscalls/symlink.rs @@ -210,7 +210,7 @@ impl Task { let link = match self .process .object_manager - .resolve_symlink(&link_name.original_path, true) + .resolve_symlink(&link_name.original_path, false) { Ok(link) => link, Err(status) => return status, @@ -706,9 +706,9 @@ mod tests { let task = test_task(); let real = create_directory(&task, r"\BaseNamedObjects\LiteBoxDriveTarget"); let child = create_directory(&task, r"\BaseNamedObjects\LiteBoxDriveTarget\Child"); - let link = create_link(&task, r"\??\C:", r"\BaseNamedObjects\LiteBoxDriveTarget"); + let link = create_link(&task, r"\??\Z:", r"\BaseNamedObjects\LiteBoxDriveTarget"); - let opened = open_directory(&task, r"\??\C:\Child"); + let opened = open_directory(&task, r"\??\Z:\Child"); assert_eq!(task.sys_nt_close(opened), NtStatus::SUCCESS); assert_eq!(task.sys_nt_close(link), NtStatus::SUCCESS); assert_eq!(task.sys_nt_close(child), NtStatus::SUCCESS); From 15c58354aa1a0574199133630d51290bf2c96356 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Thu, 16 Jul 2026 08:16:42 -0700 Subject: [PATCH 095/319] Cherry pick "Add OP-TEE ShortBuffer required-size handling" (#1030) Co-authored-by: Sangho Lee --- litebox_shim_optee/src/lib.rs | 56 +++++++++++++++------- litebox_shim_optee/src/syscalls/cryp.rs | 62 ++++++++++++------------- litebox_shim_optee/src/syscalls/tee.rs | 33 ++++++++----- 3 files changed, 92 insertions(+), 59 deletions(-) diff --git a/litebox_shim_optee/src/lib.rs b/litebox_shim_optee/src/lib.rs index 14473f7422..3fbb0a9878 100644 --- a/litebox_shim_optee/src/lib.rs +++ b/litebox_shim_optee/src/lib.rs @@ -465,23 +465,35 @@ impl Task { ret_orig, } => { if let Some(mut params_copied) = params.read_at_offset(0) { - self.sys_invoke_ta_command( + let result = self.sys_invoke_ta_command( ta_sess_id, cancel_req_to, cmd_id, &mut params_copied, ret_orig, - ) - .and_then(|cleanup| { - if !params_copied.needs_copy_back() - || params.write_at_offset(0, params_copied).is_some() - { - Ok(()) - } else { - cleanup.run(self); - Err(TeeResult::AccessDenied) + ); + match result { + Ok(cleanup) => { + if !params_copied.needs_copy_back() + || params.write_at_offset(0, params_copied).is_some() + { + Ok(()) + } else { + cleanup.run(self); + Err(TeeResult::AccessDenied) + } + } + Err(TeeResult::ShortBuffer) => { + if !params_copied.needs_copy_back() + || params.write_at_offset(0, params_copied).is_some() + { + Err(TeeResult::ShortBuffer) + } else { + Err(TeeResult::AccessDenied) + } } - }) + Err(error) => Err(error), + } } else { Err(TeeResult::BadParameters) } @@ -949,11 +961,23 @@ where { let mut length: usize = length.trunc(); let mut kernel_buf = vec![0u8; length]; - syscall_fn(task, state, &src_slice, &mut kernel_buf, &mut length).and_then(|()| { - let _ = dst_len.write_at_offset(0, length as u64); - dst.copy_from_slice(0, &kernel_buf[..length]) - .ok_or(TeeResult::OutOfMemory) - }) + let result = syscall_fn(task, state, &src_slice, &mut kernel_buf, &mut length); + match result { + Ok(()) => { + dst.copy_from_slice(0, &kernel_buf[..length]) + .ok_or(TeeResult::OutOfMemory)?; + dst_len + .write_at_offset(0, length as u64) + .ok_or(TeeResult::AccessDenied) + } + Err(TeeResult::ShortBuffer) => { + dst_len + .write_at_offset(0, length as u64) + .ok_or(TeeResult::AccessDenied)?; + Err(TeeResult::ShortBuffer) + } + Err(error) => Err(error), + } } else { Err(TeeResult::BadParameters) } diff --git a/litebox_shim_optee/src/syscalls/cryp.rs b/litebox_shim_optee/src/syscalls/cryp.rs index 8845ee4829..60394b0eae 100644 --- a/litebox_shim_optee/src/syscalls/cryp.rs +++ b/litebox_shim_optee/src/syscalls/cryp.rs @@ -167,43 +167,43 @@ impl Task { last_block: bool, ) -> Result<(), TeeResult> { let tee_cryp_state_map = &self.tee_cryp_state_map; + let Some(mut map) = tee_cryp_state_map.get_mut(state) else { + return Err(TeeResult::BadParameters); + }; if dst_slice.len() < src_slice.len() { + *dst_len = src_slice.len(); return Err(TeeResult::ShortBuffer); } - if let Some(mut map) = tee_cryp_state_map.get_mut(state) { - // Check last_block before applying the cipher so we don't mutate - // dst_slice and then return an error. - if last_block { - #[cfg(debug_assertions)] - todo!("support algorithms which have a certain finalization logic"); - #[cfg(not(debug_assertions))] - return Err(TeeResult::NotSupported); - } - if let Some(state_entry) = map.get_mut(&state) - && let Some(cipher) = state_entry.get_mut_cipher() - { - dst_slice[..src_slice.len()].copy_from_slice(src_slice); - match cipher { - Cipher::Aes128Ctr(aes128ctr) => { - aes128ctr.apply_keystream(&mut dst_slice[..src_slice.len()]); - } - Cipher::Aes192Ctr(aes192ctr) => { - aes192ctr.apply_keystream(&mut dst_slice[..src_slice.len()]); - } - Cipher::Aes256Ctr(aes256ctr) => { - aes256ctr.apply_keystream(&mut dst_slice[..src_slice.len()]); - } + // Check last_block before applying the cipher so we don't mutate + // dst_slice and then return an error. + if last_block { + #[cfg(debug_assertions)] + todo!("support algorithms which have a certain finalization logic"); + #[cfg(not(debug_assertions))] + return Err(TeeResult::NotSupported); + } + if let Some(state_entry) = map.get_mut(&state) + && let Some(cipher) = state_entry.get_mut_cipher() + { + dst_slice[..src_slice.len()].copy_from_slice(src_slice); + match cipher { + Cipher::Aes128Ctr(aes128ctr) => { + aes128ctr.apply_keystream(&mut dst_slice[..src_slice.len()]); + } + Cipher::Aes192Ctr(aes192ctr) => { + aes192ctr.apply_keystream(&mut dst_slice[..src_slice.len()]); + } + Cipher::Aes256Ctr(aes256ctr) => { + aes256ctr.apply_keystream(&mut dst_slice[..src_slice.len()]); } - *dst_len = src_slice.len(); - Ok(()) - } else { - #[cfg(debug_assertions)] - todo!("handle unimplemented cipher"); - #[cfg(not(debug_assertions))] - Err(TeeResult::NotImplemented) } + *dst_len = src_slice.len(); + Ok(()) } else { - Err(TeeResult::BadParameters) + #[cfg(debug_assertions)] + todo!("handle unimplemented cipher"); + #[cfg(not(debug_assertions))] + Err(TeeResult::NotImplemented) } } diff --git a/litebox_shim_optee/src/syscalls/tee.rs b/litebox_shim_optee/src/syscalls/tee.rs index 088a05936a..84009cd604 100644 --- a/litebox_shim_optee/src/syscalls/tee.rs +++ b/litebox_shim_optee/src/syscalls/tee.rs @@ -92,7 +92,13 @@ impl Task { if prop_set != TeePropSet::CurrentClient { return Err(TeeResult::BadParameters); } + prop_type + .write_at_offset(0, UserTaPropType::Identity as u32) + .ok_or(TeeResult::AccessDenied)?; if prop_buf.len() < core::mem::size_of::() { + prop_len + .write_at_offset(0, core::mem::size_of::().trunc()) + .ok_or(TeeResult::AccessDenied)?; return Err(TeeResult::ShortBuffer); } let identity = self.current_client_identity(); @@ -101,9 +107,6 @@ impl Task { prop_len .write_at_offset(0, core::mem::size_of::().trunc()) .ok_or(TeeResult::AccessDenied)?; - prop_type - .write_at_offset(0, UserTaPropType::Identity as u32) - .ok_or(TeeResult::AccessDenied)?; Ok(()) } GpdPropertyIndex::ClientEndian => { @@ -111,7 +114,13 @@ impl Task { if prop_set != TeePropSet::CurrentClient { return Err(TeeResult::BadParameters); } + prop_type + .write_at_offset(0, UserTaPropType::U32 as u32) + .ok_or(TeeResult::AccessDenied)?; if prop_buf.len() < core::mem::size_of::() { + prop_len + .write_at_offset(0, core::mem::size_of::().trunc()) + .ok_or(TeeResult::AccessDenied)?; return Err(TeeResult::ShortBuffer); } prop_buf[..core::mem::size_of::()] @@ -119,16 +128,19 @@ impl Task { prop_len .write_at_offset(0, core::mem::size_of::().trunc()) .ok_or(TeeResult::AccessDenied)?; - prop_type - .write_at_offset(0, UserTaPropType::U32 as u32) - .ok_or(TeeResult::AccessDenied)?; Ok(()) } GpdPropertyIndex::CurrentTaUuid => { if prop_set != TeePropSet::CurrentTa { return Err(TeeResult::BadParameters); } + prop_type + .write_at_offset(0, UserTaPropType::Uuid as u32) + .ok_or(TeeResult::AccessDenied)?; if prop_buf.len() < core::mem::size_of::() { + prop_len + .write_at_offset(0, core::mem::size_of::().trunc()) + .ok_or(TeeResult::AccessDenied)?; return Err(TeeResult::ShortBuffer); } let ta_uuid = self.ta_app_id; @@ -136,9 +148,6 @@ impl Task { prop_len .write_at_offset(0, core::mem::size_of::().trunc()) .ok_or(TeeResult::AccessDenied)?; - prop_type - .write_at_offset(0, UserTaPropType::Uuid as u32) - .ok_or(TeeResult::AccessDenied)?; Ok(()) } GpdPropertyIndex::None => Err(TeeResult::BadParameters), @@ -252,11 +261,11 @@ impl Task { ) -> Result { // `cancel_req_to` is a timeout value. Ignore it for now. if let Some(pta) = self.pta_for_session(ta_sess_id) { - let cleanup = pta.invoke_command(self, cmd_id, params)?; + let result = pta.invoke_command(self, cmd_id, params); // Best-effort write-back of the return origin, matching OP-TEE OS // (`syscall_invoke_ta_command`): the copy result is ignored. - let _ = ret_orig.write_at_offset(0, TeeOrigin::Tee); - Ok(cleanup) + let _ = ret_orig.write_at_offset(0, TeeOrigin::TrustedApp); + result } else { #[cfg(debug_assertions)] todo!("support inter TA interaction"); From db57f43e2312a180baf5c16a7501037a16c40e93 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 16 Jul 2026 17:30:03 -0700 Subject: [PATCH 096/319] Windows shim support for NtConnectPort (#1034) Implement the CSR `NtConnectPort` startup handshake so Windows guests can connect to `\Windows\ApiPort`, receive CSR connection data, and map their client section. CSR client and CSRSS bases currently alias one mapping; distinct virtual mappings over shared backing remain TODO. Also, only BASESRV static-data slot 1 is populated; CSRSRV (0), CONSRV (2), USERSRV (3) remain TODO. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 194 ++++---- litebox_shim_windows/src/loader/pe.rs | 9 +- litebox_shim_windows/src/syscalls/lpc.rs | 425 ++++++++++++++++++ litebox_shim_windows/src/syscalls/mod.rs | 25 ++ .../src/syscalls/object_manager.rs | 35 ++ litebox_shim_windows/src/syscalls/section.rs | 120 ++++- litebox_shim_windows/src/syscalls/sysinfo.rs | 2 +- litebox_shim_windows/src/tests.rs | 21 +- 8 files changed, 689 insertions(+), 142 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/lpc.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 5c1d5b2f88..f620b373fb 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -33,6 +33,7 @@ use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; use crate::syscalls::event::{EventHandleObject, EventSubsystem}; use crate::syscalls::file::{FileObject, FileObjectSubsystem}; use crate::syscalls::iocp::{IoCompletionHandleObject, IoCompletionSubsystem}; +use crate::syscalls::lpc::{LpcPortHandleObject, LpcPortSubsystem}; use crate::syscalls::object_manager::{ DirectoryHandleObject, DirectoryObjectSubsystem, ObjectManager, }; @@ -404,26 +405,6 @@ fn windows_user_shared_data() -> nt_types::KUserSharedData { shared_data } -fn map_csr_server_shared_memory( - page_manager: &crate::WindowsPageManager, -) -> Option { - let length = litebox::mm::linux::NonZeroPageSize::new( - crate::syscalls::section::WINDOWS_SHARED_SECTION_SIZE, - )?; - // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` leaves address - // selection to the page manager, so this cannot replace an existing mapping. - unsafe { - page_manager.create_writable_pages( - None, - length, - litebox::mm::linux::CreatePagesFlags::empty(), - |_| Ok(0), - ) - } - .map(|mapping| mapping.as_usize()) - .ok() -} - pub struct WindowsShim(Arc>); impl WindowsShim { @@ -439,26 +420,16 @@ impl WindowsShim { #[cfg(not(target_os = "windows"))] let _ = map_windows_user_shared_data::(&self.0.page_manager) .ok_or(loader::WindowsLoadError::MapSharedMemory)?; - let windows_shared_section_addr = map_csr_server_shared_memory(&self.0.page_manager) - .ok_or(loader::WindowsLoadError::MapSharedMemory)?; - let windows_shared_section = - crate::syscalls::section::load_time_windows_shared_section(windows_shared_section_addr); - 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. + let windows_shared_section = crate::syscalls::section::load_time_windows_shared_section( + load_info.environment.windows_shared_section, + ); let mut process = Process::default(Some(load_info.virtual_allocations), windows_shared_section); process.ntdll_mapping = load_info.ntdll_mapping; process.peb_address = load_info.environment.peb; - write_field_at_offset::( - process.peb_address, - core::mem::offset_of!( - crate::nt_types::ProcessEnvironmentBlock, - csr_server_read_only_shared_memory_base - ), - windows_shared_section_addr, - ) - .ok_or(loader::WindowsLoadError::MemoryAccess)?; let process = Arc::new(process); Ok(LoadedProgram { entrypoints: WindowsShimEntrypoints { @@ -658,9 +629,18 @@ impl Task { fn handle_syscall_request(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { let Some(req) = SyscallRequest::::try_from_raw(ctx) else { - litebox_util_log::debug!( - syscall:? = NtSysno::from_raw(ctx.orig_rax); - "Unsupported Windows syscall" + let caller = ConstPtr::::from_usize(ctx.rsp) + .read_at_offset(0) + .unwrap_or_default(); + litebox_util_log::error!( + syscall:? = NtSysno::from_raw(ctx.orig_rax), + rip:% = format_args!("{:#x}", ctx.rip), + caller:% = format_args!("{caller:#x}"), + arg0:% = format_args!("{:#x}", ctx.r10), + arg1:% = format_args!("{:#x}", ctx.rdx), + arg2:% = format_args!("{:#x}", ctx.r8), + arg3:% = format_args!("{:#x}", ctx.r9); + "Unsupported Windows syscall; terminating Windows guest" ); return ContinueOperation::Terminate; }; @@ -814,6 +794,34 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtConnectPort { + port_handle, + port_name, + security_qos, + client_view, + server_view, + max_message_length, + connection_information, + connection_information_length, + } => { + let status = self.sys_nt_connect_port(syscalls::lpc::ConnectPortParameters { + port_handle, + port_name, + security_qos, + client_view, + server_view, + max_message_length, + connection_information, + connection_information_length, + }); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtSecureConnectPort => { + litebox_util_log::debug!( + "Rejected NtSecureConnectPort; only the CSR NtConnectPort subset is modeled" + ); + (NtStatus::NOT_SUPPORTED, ContinueOperation::Resume) + } SyscallRequest::NtCreateSection { section_handle, desired_access, @@ -1556,86 +1564,34 @@ impl Task { raw_fd: usize, visitor: impl RawHandleVisitor, ) -> NtStatus { - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |file| visitor.file(file), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |key| visitor.registry_key(key), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |event| visitor.event(event), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |directory| visitor.directory(directory), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |link| visitor.symbolic_link(link), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |io_completion| visitor.io_completion(io_completion), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |timer| visitor.timer(timer), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |wait_completion_packet| visitor.wait_completion_packet(wait_completion_packet), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |worker_factory| visitor.worker_factory(worker_factory), - ) { - return NtStatus::SUCCESS; - } - if remove_raw_handle_by_raw_fd::>( - &self.global.litebox, - &self.process.handles, - raw_fd, - |section| visitor.section(section), - ) { - return NtStatus::SUCCESS; + macro_rules! try_close { + ($subsystem:ty, $visit:ident) => { + if remove_raw_handle_by_raw_fd::( + &self.global.litebox, + &self.process.handles, + raw_fd, + |entry| visitor.$visit(entry), + ) { + return NtStatus::SUCCESS; + } + }; } + + try_close!(FileObjectSubsystem, file); + try_close!(RegistryKeySubsystem, registry_key); + try_close!(EventSubsystem, event); + try_close!(DirectoryObjectSubsystem, directory); + try_close!(SymbolicLinkSubsystem, symbolic_link); + try_close!(IoCompletionSubsystem, io_completion); + try_close!(LpcPortSubsystem, lpc_port); + try_close!(TimerSubsystem, timer); + try_close!( + WaitCompletionPacketSubsystem, + wait_completion_packet + ); + try_close!(WorkerFactorySubsystem, worker_factory); + try_close!(SectionSubsystem, section); + NtStatus::INVALID_HANDLE } @@ -1664,6 +1620,8 @@ trait RawHandleVisitor { fn io_completion(&self, io_completion: IoCompletionHandleObject); + fn lpc_port(&self, lpc_port: LpcPortHandleObject); + fn timer(&self, timer: TimerHandleObject); fn wait_completion_packet( @@ -1707,6 +1665,10 @@ impl RawHandleVisitor Task::::close_io_completion(io_completion); } + fn lpc_port(&self, lpc_port: LpcPortHandleObject) { + Task::::close_lpc_port(lpc_port); + } + fn timer(&self, timer: TimerHandleObject) { Task::::close_timer(timer); } diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index c859f81066..6fbe76a340 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -64,6 +64,7 @@ pub(crate) struct WindowsProcessEnvironment { pub(crate) peb: usize, pub(crate) teb: usize, pub(crate) context: usize, + pub(crate) windows_shared_section: usize, } pub(crate) struct PeLoadInfo { @@ -374,6 +375,9 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { 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)?; @@ -402,6 +406,7 @@ impl<'a, Platform: crate::ShimPlatform, FS: ShimFS> PeLoader<'a, Platform, FS> { peb: peb_ptr, teb: teb_ptr, context: ctx_ptr, + windows_shared_section: read_only_shared_memory_base, }) } } @@ -411,6 +416,8 @@ fn initialize_windows_static_server_data( ) -> Result { let read_only_static_server_data = shared_heap.allocate_array::(CSR_SERVER_DLL_MAX)?; + // TODO(csr-server-dlls): populate CSRSRV (0), CONSRV (2), and USERSRV (3) + // when their shared static server data is modeled. let client_base_static_server_data = shared_heap.allocate::()?; initialize_static_server_data::(shared_heap, client_base_static_server_data)?; @@ -2004,7 +2011,7 @@ mod tests { fn dump_api_set_namespace(api_set_map: ApiSetNamespace, bytes: &[u8], label: &str) { std::println!("{label}"); - std::println!("API_SET_NAMESPACE len={:#x}", bytes.len(),); + std::println!("API_SET_NAMESPACE len={:#x}", bytes.len()); std::println!(" version: {:#010x}", api_set_map.version); std::println!(" size: {:#010x}", api_set_map.size); std::println!(" flags: {:#010x}", api_set_map.flags); diff --git a/litebox_shim_windows/src/syscalls/lpc.rs b/litebox_shim_windows/src/syscalls/lpc.rs new file mode 100644 index 0000000000..d91173b937 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/lpc.rs @@ -0,0 +1,425 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::string::String; +use core::marker::PhantomData; +use core::mem::size_of; + +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::utils::TruncateExt as _; +use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use super::Handle; +use crate::nt_types::{ProcessEnvironmentBlock, ThreadEnvironmentBlock, UnicodeString}; +use crate::{ConstPtr, MutPtr, ShimFS, ShimPlatform, Task, probe_guest_output_preserving_value}; + +const CSR_MAX_MESSAGE_LENGTH: u32 = 0x148; +const CSR_SERVER_PROCESS_ID: usize = 1; +// TODO(csr-server-dll-names): report names once the CSR connect contract models them. +const CSR_NUMBER_OF_SERVER_DLL_NAMES: u32 = 0; + +pub(crate) struct LpcPortSubsystem(PhantomData); + +impl FdEnabledSubsystem for LpcPortSubsystem { + type Entry = LpcPortHandleObject; +} + +impl FdEnabledSubsystemEntry for LpcPortHandleObject {} + +pub(crate) struct LpcPortHandleObject { + _port_name: String, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +pub(crate) struct SecurityQualityOfService { + length: u32, + impersonation_level: u32, + context_tracking_mode: u8, + effective_only: u8, + padding: [u8; 2], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +pub(crate) struct PortView { + length: u32, + padding: u32, + section_handle: Handle, + section_offset: u64, + view_size: usize, + view_base: usize, + view_remote_base: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +pub(crate) struct RemotePortView { + length: u32, + padding: u32, + view_size: usize, + view_base: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct CsrApiConnectInfo { + shared_section_base: usize, + shared_static_server_data: usize, + shared_section_heap: usize, + debug_flags: u32, + size_of_peb_data: u32, + size_of_teb_data: u32, + number_of_server_dll_names: u32, + server_process_id: usize, +} + +pub(crate) struct ConnectPortParameters { + pub(crate) port_handle: MutPtr, + pub(crate) port_name: ConstPtr, + pub(crate) security_qos: ConstPtr, + pub(crate) client_view: Option>, + pub(crate) server_view: Option>, + pub(crate) max_message_length: Option>, + pub(crate) connection_information: Option>, + pub(crate) connection_information_length: Option>, +} + +impl Task { + pub(crate) fn sys_nt_connect_port(&self, params: ConnectPortParameters) -> NtStatus { + if params.security_qos.read_at_offset(0).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + let port_name = match params + .port_name + .read_at_offset(0) + .ok_or(NtStatus::ACCESS_VIOLATION) + .and_then(UnicodeString::read_string::) + { + Ok(name) => name, + Err(status) => return status, + }; + if let Err(status) = self.process.object_manager.resolve_port(&port_name) { + return status; + } + + let Some(client_view) = params.client_view else { + return NtStatus::INVALID_PARAMETER; + }; + let Some(connection_information) = params.connection_information else { + return NtStatus::INVALID_PARAMETER; + }; + let Some(connection_information_length) = params.connection_information_length else { + return NtStatus::INVALID_PARAMETER; + }; + + let Some(client_view_value) = client_view.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + if client_view_value.length as usize != size_of::() + || client_view_value.section_offset != 0 + || client_view_value.view_size == 0 + { + return NtStatus::INVALID_PARAMETER; + } + let server_view_value = match params.server_view { + Some(server_view) => match server_view.read_at_offset(0) { + Some(view) if view.length as usize == size_of::() => Some(view), + Some(_) => return NtStatus::INVALID_PARAMETER, + None => return NtStatus::ACCESS_VIOLATION, + }, + None => None, + }; + + let connection_info_len = match connection_information_length.read_at_offset(0) { + Some(length) => length as usize, + None => return NtStatus::ACCESS_VIOLATION, + }; + if connection_info_len != size_of::() { + return NtStatus::INFO_LENGTH_MISMATCH; + } + + if let Err(status) = probe_lpc_outputs::( + params.port_handle, + client_view, + params.server_view, + params.max_message_length, + connection_information, + connection_information_length, + connection_info_len, + ) { + return status; + } + + let Some(connect_info) = self.csr_api_connect_info() else { + return NtStatus::ACCESS_VIOLATION; + }; + let mapped_view = match self.map_client_port_section( + client_view_value.section_handle, + client_view_value.view_size, + ) { + Ok(mapped_view) => mapped_view, + Err(status) => return status, + }; + let port = LpcPortHandleObject { + _port_name: port_name.clone(), + }; + let handle = match self.insert_typed_handle::>(port, drop) { + Ok(handle) => handle, + Err(status) => { + self.rollback_pagefile_section_view(mapped_view.base); + return status; + } + }; + + let mut written_client_view = client_view_value; + written_client_view.view_size = mapped_view.view_size; + written_client_view.view_base = mapped_view.base; + written_client_view.view_remote_base = mapped_view.base; + + let write_failed = client_view + .write_at_offset(0, written_client_view) + .is_none() + || params + .max_message_length + .is_some_and(|ptr| ptr.write_at_offset(0, CSR_MAX_MESSAGE_LENGTH).is_none()) + || connection_information + .write_slice_at_offset(0, connect_info.as_bytes()) + .is_none() + || connection_information_length + .write_at_offset(0, size_of::().trunc()) + .is_none() + || params.port_handle.write_at_offset(0, handle).is_none(); + if write_failed { + self.close_lpc_port_handle(handle); + self.rollback_pagefile_section_view(mapped_view.base); + return NtStatus::ACCESS_VIOLATION; + } + + if let (Some(server_view), Some(mut server_view_value)) = + (params.server_view, server_view_value) + { + server_view_value.view_size = mapped_view.mapped_size; + server_view_value.view_base = mapped_view.base; + if server_view.write_at_offset(0, server_view_value).is_none() { + self.close_lpc_port_handle(handle); + self.rollback_pagefile_section_view(mapped_view.base); + return NtStatus::ACCESS_VIOLATION; + } + } + + litebox_util_log::debug!( + port_name:% = port_name, + handle:% = format_args!("{:#x}", handle.as_raw()), + client_view_base:% = format_args!("{:#x}", mapped_view.base), + client_view_size = mapped_view.view_size; + "Handled NtConnectPort for CSR API port" + ); + NtStatus::SUCCESS + } + + pub(crate) fn close_lpc_port_handle(&self, handle: Handle) { + self.close_typed_handle::>(handle, drop); + } + + pub(crate) fn close_lpc_port(port: LpcPortHandleObject) { + drop(port); + } + + fn csr_api_connect_info(&self) -> Option { + let read_only_shared_memory_base = crate::read_field_at_offset::( + self.process.peb_address, + core::mem::offset_of!(ProcessEnvironmentBlock, read_only_shared_memory_base), + )?; + let read_only_static_server_data = crate::read_field_at_offset::( + self.process.peb_address, + core::mem::offset_of!(ProcessEnvironmentBlock, read_only_static_server_data), + )?; + Some(CsrApiConnectInfo { + shared_section_base: read_only_shared_memory_base, + shared_static_server_data: read_only_static_server_data, + shared_section_heap: read_only_shared_memory_base, + debug_flags: 0, + size_of_peb_data: size_of::().trunc(), + size_of_teb_data: size_of::().trunc(), + number_of_server_dll_names: CSR_NUMBER_OF_SERVER_DLL_NAMES, + server_process_id: CSR_SERVER_PROCESS_ID, + }) + } +} + +fn probe_lpc_outputs( + port_handle: MutPtr, + client_view: MutPtr, + server_view: Option>, + max_message_length: Option>, + connection_information: MutPtr, + connection_information_length: MutPtr, + connection_information_len: usize, +) -> Result<(), NtStatus> { + probe_guest_output_preserving_value::(port_handle)?; + probe_guest_output_preserving_value::(client_view)?; + if let Some(server_view) = server_view { + probe_guest_output_preserving_value::(server_view)?; + } + if let Some(max_message_length) = max_message_length { + probe_guest_output_preserving_value::(max_message_length)?; + } + probe_guest_byte_buffer_preserving::( + connection_information, + connection_information_len, + size_of::(), + )?; + probe_guest_output_preserving_value::(connection_information_length) +} + +fn probe_guest_byte_buffer_preserving( + ptr: MutPtr, + len: usize, + max_len: usize, +) -> Result<(), NtStatus> { + if len > max_len { + return Err(NtStatus::INFO_LENGTH_MISMATCH); + } + let bytes = ptr.to_owned_slice(len).ok_or(NtStatus::ACCESS_VIOLATION)?; + ptr.write_slice_at_offset(0, bytes.as_ref()) + .ok_or(NtStatus::ACCESS_VIOLATION) +} + +#[cfg(test)] +mod tests { + use zerocopy::FromZeros as _; + + use super::*; + use crate::syscalls::mm::PageProtection; + use crate::syscalls::object_manager::WINDOWS_API_PORT; + use crate::tests::{ + TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task, unicode_string, + utf16_units, + }; + + const SECTION_MAP_WRITE: u32 = 0x0002; + const SECTION_MAP_READ: u32 = 0x0004; + const SEC_COMMIT: u32 = 0x0800_0000; + + fn task_with_peb(peb: &mut ProcessEnvironmentBlock) -> Task { + let mut task = test_task(); + alloc::sync::Arc::get_mut(&mut task.process) + .expect("test task has a unique process reference") + .peb_address = core::ptr::from_mut(peb) as usize; + task + } + + fn security_qos() -> SecurityQualityOfService { + SecurityQualityOfService { + length: size_of::().trunc(), + impersonation_level: 2, + context_tracking_mode: 0, + effective_only: 1, + padding: [0; 2], + } + } + + fn api_port_name(value: &str) -> (alloc::vec::Vec, UnicodeString) { + let units = utf16_units(value); + let unicode = unicode_string(&units); + (units, unicode) + } + + fn empty_connect_info() -> CsrApiConnectInfo { + CsrApiConnectInfo { + shared_section_base: 0, + shared_static_server_data: 0, + shared_section_heap: 0, + debug_flags: 0, + size_of_peb_data: 0, + size_of_teb_data: 0, + number_of_server_dll_names: 0, + server_process_id: 0, + } + } + + fn create_client_section(task: &Task, access: u32) -> Handle { + let mut handle = Handle::default(); + let size = i64::try_from(crate::PAGE_SIZE).expect("test section size fits in i64"); + assert_eq!( + task.sys_nt_create_section( + mut_ptr(&mut handle), + access, + None, + Some(const_ptr(&size)), + PageProtection::PAGE_READWRITE.bits(), + SEC_COMMIT, + Handle::default(), + ), + NtStatus::SUCCESS + ); + handle + } + + #[test] + fn nt_connect_port_fills_csr_info_and_maps_client_section() { + let mut peb = ProcessEnvironmentBlock::new_zeroed(); + peb.read_only_shared_memory_base = 0x7000_0000; + peb.read_only_static_server_data = 0x7000_1000; + peb.csr_server_read_only_shared_memory_base = 0x7100_0000; + let task = task_with_peb(&mut peb); + let (_name_units, name) = api_port_name(WINDOWS_API_PORT); + let qos = security_qos(); + let section_handle = create_client_section(&task, SECTION_MAP_READ | SECTION_MAP_WRITE); + let mut handle = Handle::default(); + let mut client_view = PortView { + length: size_of::().trunc(), + padding: 0, + section_handle, + section_offset: 0, + view_size: crate::PAGE_SIZE, + view_base: 0, + view_remote_base: 0, + }; + let mut max_message_length = 0u32; + let mut connection_info = empty_connect_info(); + let mut connection_info_len = size_of::().trunc(); + + assert_eq!( + task.sys_nt_connect_port(ConnectPortParameters { + port_handle: mut_ptr(&mut handle), + port_name: const_ptr(&name), + security_qos: const_ptr(&qos), + client_view: Some(mut_ptr(&mut client_view)), + server_view: None, + max_message_length: Some(mut_ptr(&mut max_message_length)), + connection_information: Some(mut_byte_ptr(&mut connection_info)), + connection_information_length: Some(mut_ptr(&mut connection_info_len)), + }), + NtStatus::SUCCESS + ); + + assert!(!handle.is_null()); + assert_ne!(client_view.view_base, 0); + assert_eq!(client_view.view_remote_base, client_view.view_base); + assert_eq!(max_message_length, CSR_MAX_MESSAGE_LENGTH); + assert_eq!( + connection_info.shared_section_base, + peb.read_only_shared_memory_base + ); + assert_eq!( + connection_info.shared_static_server_data, + peb.read_only_static_server_data + ); + assert_ne!( + u64::try_from(connection_info.shared_section_base).unwrap(), + peb.csr_server_read_only_shared_memory_base + ); + assert_eq!( + connection_info.size_of_peb_data, + size_of::().trunc() + ); + assert_eq!( + connection_info.size_of_teb_data, + size_of::().trunc() + ); + } +} diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 09a92765c7..0ee7646687 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -7,6 +7,7 @@ pub(crate) mod event; pub(crate) mod file; pub(crate) mod file_path; pub(crate) mod iocp; +pub(crate) mod lpc; pub(crate) mod mm; pub(crate) mod nls; pub(crate) mod object_manager; @@ -183,6 +184,19 @@ pub(crate) enum SyscallRequest { object_attributes: Option>, number_of_concurrent_threads: u32, }, + NtConnectPort { + port_handle: Platform::RawMutPointer, + port_name: Platform::RawConstPointer, + security_qos: Platform::RawConstPointer, + client_view: Option>, + server_view: Option>, + max_message_length: Option>, + connection_information: Option>, + connection_information_length: Option>, + }, + /// `NtSecureConnectPort` carries SID and server-view semantics that are + /// deliberately outside the current CSR `NtConnectPort` subset. + NtSecureConnectPort, NtCreateSection { section_handle: Platform::RawMutPointer, desired_access: u32, @@ -601,6 +615,17 @@ impl SyscallRequest { object_attributes:*, number_of_concurrent_threads, })), + NtSysno::NtConnectPort => Some(sys_req!(NtConnectPort { + port_handle:*, + port_name:*, + security_qos:*, + client_view:*, + server_view:*, + max_message_length:*, + connection_information:*, + connection_information_length:*, + })), + NtSysno::NtSecureConnectPort => Some(SyscallRequest::NtSecureConnectPort), NtSysno::NtCreateSection => Some(sys_req!(NtCreateSection { section_handle:*, desired_access, diff --git a/litebox_shim_windows/src/syscalls/object_manager.rs b/litebox_shim_windows/src/syscalls/object_manager.rs index 09b4d1bb6c..2492f175f1 100644 --- a/litebox_shim_windows/src/syscalls/object_manager.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -28,6 +28,7 @@ use crate::syscalls::section::{ use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; const MAX_SYMLINK_REPARSE_DEPTH: usize = 64; +pub(crate) const WINDOWS_API_PORT: &str = r"\Windows\ApiPort"; const STANDARD_RIGHTS_REQUIRED: u32 = AccessMask::DELETE.bits() | AccessMask::READ_CONTROL.bits() | AccessMask::WRITE_DAC.bits() @@ -162,6 +163,7 @@ enum NamedObject { FileDevice { device: FileDeviceObject, }, + Port, } pub(super) enum ObjectLeafLookup { @@ -331,6 +333,7 @@ impl ObjectNode { new_event(event: Weak>) => NamedObject::Event { event }; new_section(section: Weak>) => NamedObject::Section { section }; new_file_device(device: FileDeviceObject) => NamedObject::FileDevice { device }; + new_port() => NamedObject::Port; } fn child(&self, name: &str) -> Option> { @@ -375,6 +378,7 @@ impl ObjectNode { event_object, ObjectLeafLookup>>, NamedObject::Event { event } => ObjectLeafLookup::from_weak(event); section_object, ObjectLeafLookup>>, NamedObject::Section { section } => ObjectLeafLookup::from_weak(section); file_device_object, ObjectLeafLookup, NamedObject::FileDevice { device } => ObjectLeafLookup::Live(device.clone()); + port_object, ObjectLeafLookup<()>, NamedObject::Port => ObjectLeafLookup::Live(()); } fn type_name(&self) -> Option<&'static str> { @@ -384,6 +388,7 @@ impl ObjectNode { NamedObject::Event { event } => event.upgrade().map(|_| "Event"), NamedObject::Section { section } => section.upgrade().map(|_| "Section"), NamedObject::FileDevice { .. } => Some("Device"), + NamedObject::Port => Some("Port"), } } @@ -505,6 +510,17 @@ impl ObjectManager { ) } + fn create_port(&self, path: &str) -> NtStatus { + self.create_child( + path, + |node| node.port_object(), + ObjectNode::new_port, + NtStatus::OBJECT_TYPE_MISMATCH, + |()| NtStatus::OBJECT_NAME_EXISTS, + |_| NtStatus::SUCCESS, + ) + } + fn create_child( &self, path: &str, @@ -622,6 +638,16 @@ impl ObjectManager { } } + pub(crate) fn resolve_port(&self, path: &str) -> Result<(), NtStatus> { + self.resolve_object_leaf(path, false, |node| { + if node.path == path { + node.port_object() + } else { + ObjectLeafLookup::Stale + } + }) + } + fn resolve_object_leaf( &self, path: &str, @@ -669,6 +695,14 @@ impl ObjectManager { ); } + fn seed_port(&self, path: &str) { + let status = self.create_port(path); + assert!( + status == NtStatus::SUCCESS, + "seeded NT port must have seeded ancestors: {status:?}" + ); + } + fn resolve_tail( &self, tail: &str, @@ -1327,6 +1361,7 @@ pub(crate) fn seed_object_manager() }, ); object_manager.seed_file_device(r"\Device\ConDrv", FileDeviceObject::ConsoleDriver); + object_manager.seed_port(WINDOWS_API_PORT); for (path, target) in SEEDED_SYMLINK_PATHS { object_manager.seed_symlink(path, target); } diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 3cf97c2678..dccd92f312 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -89,6 +89,12 @@ pub(crate) struct MapViewOfSectionParameters { pub(crate) page_protection: u32, } +pub(super) struct MappedPagefileSectionView { + pub(super) base: usize, + pub(super) mapped_size: usize, + pub(super) view_size: usize, +} + bitflags::bitflags! { #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct SectionAllocationAttributes: u32 { @@ -623,8 +629,66 @@ impl Task { page_protection: PageProtection, permissions: MemoryRegionPermissions, ) -> NtStatus { + let mapped_view = match self.map_pagefile_section_view( + section, + requested_view_size, + section_offset, + page_protection, + permissions, + ) { + Ok(mapped_view) => mapped_view, + Err(status) => return status, + }; + if request + .base_address + .write_at_offset(0, mapped_view.base) + .is_none() + || request + .view_size + .write_at_offset(0, mapped_view.view_size) + .is_none() + { + self.rollback_pagefile_section_view(mapped_view.base); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(super) fn map_client_port_section( + &self, + section_handle: Handle, + requested_view_size: usize, + ) -> Result { + let entry = self.section_entry(section_handle)?; + let section = entry.with_entry(|entry| { + entry + .granted_access + .require(SectionAccess::MAP_READ | SectionAccess::MAP_WRITE) + .map(|()| Arc::clone(&entry.section)) + })?; + let page_protection = PageProtection::PAGE_READWRITE; + let Some((_, permissions)) = parse_page_protection(page_protection.bits()) else { + return Err(NtStatus::INVALID_PAGE_PROTECTION); + }; + self.map_pagefile_section_view( + §ion, + requested_view_size, + 0, + page_protection, + permissions, + ) + } + + fn map_pagefile_section_view( + &self, + section: &Arc>, + requested_view_size: usize, + section_offset: usize, + page_protection: PageProtection, + permissions: MemoryRegionPermissions, + ) -> Result { if section_offset > section.size { - return NtStatus::INVALID_VIEW_SIZE; + return Err(NtStatus::INVALID_VIEW_SIZE); } let remaining = section.size - section_offset; let view_size = if requested_view_size == 0 { @@ -633,18 +697,17 @@ impl Task { requested_view_size }; if view_size == 0 || view_size > remaining { - return NtStatus::INVALID_VIEW_SIZE; + return Err(NtStatus::INVALID_VIEW_SIZE); } - let Some(mapped_size) = view_size.checked_next_multiple_of(PAGE_SIZE) else { - return NtStatus::INVALID_VIEW_SIZE; - }; - let Some(length) = NonZeroPageSize::::new(mapped_size) else { - return NtStatus::INVALID_VIEW_SIZE; - }; + let mapped_size = view_size + .checked_next_multiple_of(PAGE_SIZE) + .ok_or(NtStatus::INVALID_VIEW_SIZE)?; + let length = + NonZeroPageSize::::new(mapped_size).ok_or(NtStatus::INVALID_VIEW_SIZE)?; match section.backing { SectionBacking::Pagefile => {} SectionBacking::CsrSharedSection { .. } | SectionBacking::ImageFile => { - return NtStatus::INVALID_FILE_FOR_SECTION; + return Err(NtStatus::INVALID_FILE_FOR_SECTION); } } if !pagefile_view_protection_is_compatible(section.protection, page_protection) { @@ -653,7 +716,7 @@ impl Task { page_protection:% = format_args!("{:#x}", page_protection.bits()); "Rejected pagefile section view protection incompatible with section protection" ); - return NtStatus::SECTION_PROTECTION; + return Err(NtStatus::SECTION_PROTECTION); } if section.pagefile_view_active.swap(true, Ordering::AcqRel) { litebox_util_log::debug!( @@ -665,27 +728,21 @@ impl Task { // Host 25H2 allows repeated and simultaneous pagefile views. LiteBox // returns NOT_SUPPORTED until PageManager has first-class shared // anonymous backing that avoids kernel-side content storage. - return NtStatus::NOT_SUPPORTED; + return Err(NtStatus::NOT_SUPPORTED); } - let Ok(mapping) = create_pages( + let mapping = create_pages( &self.global.page_manager, None, length, CreatePagesFlags::empty(), permissions, |_| Ok(0), - ) else { + ) + .map_err(|_| { section.pagefile_view_active.store(false, Ordering::Release); - return NtStatus::NO_MEMORY; - }; + NtStatus::NO_MEMORY + })?; let base = mapping.as_usize(); - if request.base_address.write_at_offset(0, base).is_none() - || request.view_size.write_at_offset(0, view_size).is_none() - { - let _ = remove_view_pages::(&self.global.page_manager, base, mapped_size); - section.pagefile_view_active.store(false, Ordering::Release); - return NtStatus::ACCESS_VIOLATION; - } self.process.section_views.write().insert( base, WindowsSectionView { @@ -704,7 +761,24 @@ impl Task { pages: committed_pages(base, mapped_size, page_protection), }, ); - NtStatus::SUCCESS + Ok(MappedPagefileSectionView { + base, + mapped_size, + view_size, + }) + } + + pub(super) fn rollback_pagefile_section_view(&self, base_address: usize) { + let Some((view_base, view)) = self.remove_section_view_for_address(base_address) else { + return; + }; + if let Some(section) = &view.section + && matches!(section.backing, SectionBacking::Pagefile) + { + section.pagefile_view_active.store(false, Ordering::Release); + } + let _ = remove_view_pages::(&self.global.page_manager, view_base, view.size); + self.process.virtual_allocations.write().remove(&view_base); } fn map_csr_shared_section( diff --git a/litebox_shim_windows/src/syscalls/sysinfo.rs b/litebox_shim_windows/src/syscalls/sysinfo.rs index 71e57290d2..84f65f8763 100644 --- a/litebox_shim_windows/src/syscalls/sysinfo.rs +++ b/litebox_shim_windows/src/syscalls/sysinfo.rs @@ -777,7 +777,7 @@ mod tests { extern crate std; const QPC_SLEEP_DURATION: Duration = Duration::from_millis(25); - const QPC_SLEEP_TOLERANCE: Duration = Duration::from_millis(10); + const QPC_SLEEP_TOLERANCE: Duration = Duration::from_millis(15); type TestPlatform = crate::tests::TestPlatform; type TestTask = Task; diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 89c21357bd..9726560c11 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -82,6 +82,25 @@ pub(crate) fn test_platform() -> &'static TestPlatform { }) } +fn map_csr_server_shared_memory( + page_manager: &crate::WindowsPageManager, +) -> Option { + let length = litebox::mm::linux::NonZeroPageSize::new( + crate::syscalls::section::WINDOWS_SHARED_SECTION_SIZE, + )?; + // SAFETY: address selection is left to the page manager, so this cannot replace a mapping. + unsafe { + page_manager.create_writable_pages( + None, + length, + litebox::mm::linux::CreatePagesFlags::empty(), + |_| Ok(0), + ) + } + .map(|mapping| mapping.as_usize()) + .ok() +} + pub(crate) fn test_task() -> Task { test_task_with_nls_files(&[]) } @@ -135,7 +154,7 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task Date: Fri, 17 Jul 2026 08:53:54 -0700 Subject: [PATCH 097/319] Cherry pick "Use canonical address for cleanup" (#1035) Co-authored-by: Sangho Lee --- litebox_platform_lvbs/src/arch/x86/mm/paging.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs index 7a4e5b95dc..5ce73b30df 100644 --- a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs +++ b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs @@ -42,9 +42,6 @@ const PML4_SHIFT: u32 = 39; /// Mask for a 9-bit page-table index (512 entries per table). const PML4_INDEX_MASK: u64 = 0x1FF; -/// Number of bytes of virtual address space covered by one PML4 slot (512 GiB). -const PML4_SLOT_SIZE: u64 = 1 << PML4_SHIFT; - /// PML4 index of the first VTL1-kernel slot (`PA + KERNEL_OFFSET`). /// /// Only slots `>= KERNEL_PML4_START` are safe to share between page tables: @@ -308,10 +305,9 @@ impl X64PageTable<'_, M, ALIGN> { // `0 ..= KERNEL_PML4_START * PML4_SLOT_SIZE - 1`. The kernel region at // and above `KERNEL_PML4_START` is base-owned/shared. let start = Page::::from_start_address(VirtAddr::new(0)).unwrap(); - let end = Page::::containing_address(VirtAddr::new( - KERNEL_PML4_START as u64 * PML4_SLOT_SIZE - 1, - )); + let end = Page::::containing_address(VirtAddr::new(crate::KERNEL_OFFSET - 1)); // Safety: The page table is being destroyed and will not be reused. + // This function crosses the non-canonical hole. unsafe { self.inner .lock() From 159feec8d0b5eca66ca044f4ef2df13589d5e1c1 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Fri, 17 Jul 2026 10:48:29 -0700 Subject: [PATCH 098/319] Cherry pick "Harden LVBS/HEKI module/kexec validation against TOCTOU and confused deputy" (#1036) Co-authored-by: Sangho Lee --- dev_tests/src/ratchet.rs | 2 +- litebox_common_linux/src/vmap.rs | 19 +- litebox_platform_lvbs/src/lib.rs | 37 +- litebox_platform_lvbs/src/mshv/error.rs | 4 + litebox_platform_lvbs/src/mshv/mod.rs | 65 +++ litebox_platform_lvbs/src/mshv/ringbuffer.rs | 13 +- litebox_platform_lvbs/src/mshv/vsm.rs | 552 +++++++++++++++---- 7 files changed, 556 insertions(+), 136 deletions(-) diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 2e46b114e6..03aa8e6fb2 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -38,7 +38,7 @@ fn ratchet_globals() -> Result<()> { ("litebox/", 9), ("litebox_platform_linux_kernel/", 6), ("litebox_platform_linux_userland/", 5), - ("litebox_platform_lvbs/", 24), + ("litebox_platform_lvbs/", 25), ("litebox_platform_multiplex/", 1), ("litebox_platform_windows_userland/", 8), ("litebox_runner_lvbs/", 5), diff --git a/litebox_common_linux/src/vmap.rs b/litebox_common_linux/src/vmap.rs index 30d161fcd5..4218ce6b7b 100644 --- a/litebox_common_linux/src/vmap.rs +++ b/litebox_common_linux/src/vmap.rs @@ -42,6 +42,21 @@ pub unsafe trait VmapManager { Err(PhysPointerError::UnsupportedOperation) } + /// Map pages while bypassing platform-defined ordinary access checks. Ordinary [`Self::vmap`] + /// may delegate here after performing those checks. The default is deny. + /// + /// # Safety + /// + /// In addition to [`Self::vmap`]'s raw-mapping requirements, the caller must independently + /// authorize bypassing the omitted platform checks. + unsafe fn vmap_privileged( + &self, + _pages: &PhysPageAddrArray, + _perms: PhysPageMapPermissions, + ) -> Result { + Err(PhysPointerError::UnsupportedOperation) + } + /// Unmap the previously mapped virtually contiguous addresses ([`Self::MapInfo`]). /// /// This function is analogous to Linux kernel's `vunmap()`. @@ -78,7 +93,7 @@ pub unsafe trait VmapManager { /// platform-defined foreign-memory VA ranges, never through LiteBox-owned VA ranges. fn validate_unowned(&self, pages: &PhysPageAddrArray) -> Result<(), PhysPointerError>; - /// Protect the given physical pages to ensure concurrent read or exclusive write access: + /// Protect the given physical pages according to `perms`: /// - Read protection: prevent others from writing to the pages. /// - Read/write protection: prevent others from reading or writing to the pages. /// - No protection: allow others to read and write the pages. @@ -92,7 +107,7 @@ pub unsafe trait VmapManager { /// /// This function relies on hypercalls or other privileged hardware features and assumes those features /// are safe to use. - /// The caller should unprotect the pages when they are no longer needed to access them. + /// Callers should restore ordinary access when protection is no longer needed. unsafe fn protect( &self, pages: &PhysPageAddrArray, diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index 8dd2d89663..0ff8e438b9 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -48,15 +48,21 @@ pub mod mshv; pub mod syscall_entry; -/// Mapping info returned by [`LinuxKernel`]'s [`VmapManager::vmap`]. +/// Mapping metadata. Ordinary writable mappings retain an opaque protected-frame access guard for +/// the mapping's lifetime. pub struct LvbsPhysPageMapInfo { base: *mut u8, size: usize, + protected_frame_access: Option>, } impl LvbsPhysPageMapInfo { fn new(base: *mut u8, size: usize) -> Self { - Self { base, size } + Self { + base, + size, + protected_frame_access: None, + } } } @@ -475,11 +481,6 @@ impl GlobalVmapManager for Vmap { } } -pub type Vtl0PhysConstPtr = - litebox_common_linux::physical_pointers::PhysConstPtr; -pub type Vtl0PhysMutPtr = - litebox_common_linux::physical_pointers::PhysMutPtr; - impl RawPointerProvider for LinuxKernel { type RawConstPointer = UserConstPtr; type RawMutPointer = UserMutPtr; @@ -1164,6 +1165,26 @@ unsafe impl VmapManager for Linu &self, pages: &PhysPageAddrArray, perms: PhysPageMapPermissions, + ) -> Result { + let protected_frame_access = if perms.contains(PhysPageMapPermissions::WRITE) { + // This shared guard spans map/copy/unmap. It permits concurrent foreign-memory writes + // but does not support re-entry into a VTL protection change. + Some(crate::mshv::vsm::protected_frame_registry().acquire_access_guard(pages)?) + } else { + None + }; + // SAFETY: ordinary writable mappings were checked against protected and in-flight frames; + // the guard is retained through map, access, and unmap. `vmap_privileged` provides the + // shared raw mapping implementation. + let mut map_info = unsafe { self.vmap_privileged(pages, perms)? }; + map_info.protected_frame_access = protected_frame_access; + Ok(map_info) + } + + unsafe fn vmap_privileged( + &self, + pages: &PhysPageAddrArray, + perms: PhysPageMapPermissions, ) -> Result { if pages.is_empty() { return Err(PhysPointerError::InvalidPhysicalAddress(0)); @@ -1342,7 +1363,7 @@ unsafe impl VmapManager for Linu } let mem_attr = if perms.contains(PhysPageMapPermissions::WRITE) { - // VTL1 wants to write data to the pages, preventing VTL0 from reading/executing the pages. + // VTL1 needs writable access, so deny VTL0 all access. crate::mshv::heki::MemAttr::empty() } else if perms.contains(PhysPageMapPermissions::READ) { // VTL1 wants to read data from the pages, preventing VTL0 from writing to the pages. diff --git a/litebox_platform_lvbs/src/mshv/error.rs b/litebox_platform_lvbs/src/mshv/error.rs index bd40d50b28..a9614205c2 100644 --- a/litebox_platform_lvbs/src/mshv/error.rs +++ b/litebox_platform_lvbs/src/mshv/error.rs @@ -83,6 +83,9 @@ pub enum VsmError { #[error("invalid module token")] ModuleTokenInvalid, + #[error("physical frames overlap already-protected or reserved memory")] + ProtectedFrameOverlap, + // Kernel Symbol Table Errors #[error("no kernel symbol table found")] KernelSymbolTableNotFound, @@ -212,6 +215,7 @@ impl From for Errno { | VsmError::ModuleMemoryTypeInvalid | VsmError::ModuleRelocationInvalid | VsmError::ModuleTokenInvalid + | VsmError::ProtectedFrameOverlap | VsmError::KexecTypeInvalid | VsmError::KexecImageSegmentsInvalid | VsmError::SymbolTableEmpty diff --git a/litebox_platform_lvbs/src/mshv/mod.rs b/litebox_platform_lvbs/src/mshv/mod.rs index 40dd206c4a..11cb3ed602 100644 --- a/litebox_platform_lvbs/src/mshv/mod.rs +++ b/litebox_platform_lvbs/src/mshv/mod.rs @@ -15,6 +15,71 @@ pub mod vsm_intercept; pub mod vtl1_mem_layout; pub mod vtl_switch; +use litebox_common_linux::vmap::{ + GlobalVmapManager, PhysPageAddrArray, PhysPageMapPermissions, PhysPointerError, VmapManager, +}; + +/// Provider for MSHV operations authorized to modify protected VTL0 frames. +struct PrivilegedVmap; + +impl GlobalVmapManager for PrivilegedVmap { + type Manager = PrivilegedVmap; + + fn manager() -> &'static Self::Manager { + &PrivilegedVmap + } +} + +unsafe impl VmapManager for PrivilegedVmap { + type MapInfo = crate::LvbsPhysPageMapInfo; + + unsafe fn vmap( + &self, + pages: &PhysPageAddrArray, + perms: PhysPageMapPermissions, + ) -> Result { + // SAFETY: callers uphold the raw mapping contract. This provider is used only for + // independently authorized HEKI patch and ring-buffer writes. + unsafe { crate::platform_low().vmap_privileged(pages, perms) } + } + + unsafe fn vunmap( + &self, + map_info: Self::MapInfo, + ) -> Result<(), (PhysPointerError, Self::MapInfo)> { + // SAFETY: `map_info` came from the same LVBS mapper and has no outstanding uses beyond the + // physical-pointer guard that is dropping it. + unsafe { + >::vunmap( + crate::platform_low(), + map_info, + ) + } + } + + fn validate_unowned(&self, pages: &PhysPageAddrArray) -> Result<(), PhysPointerError> { + crate::platform_low().validate_unowned(pages) + } + + unsafe fn protect( + &self, + pages: &PhysPageAddrArray, + perms: PhysPageMapPermissions, + ) -> Result<(), PhysPointerError> { + // SAFETY: callers uphold `VmapManager::protect`; this forwards unchanged to LVBS. + unsafe { crate::platform_low().protect(pages, perms) } + } +} + +type Vtl0PhysConstPtr = + litebox_common_linux::physical_pointers::PhysConstPtr; + +/// Mutable VTL0 pointer reserved for validated HEKI text patching and the fixed-address log ring +/// buffer. It bypasses ordinary protected-frame access checks and synchronization. Do not use it for other +/// VTL0 destinations that could enable confused-deputy writes. +type PrivilegedVtl0PhysMutPtr = + litebox_common_linux::physical_pointers::PhysMutPtr; + use crate::arch::MAX_CORES; use crate::mshv::vtl1_mem_layout::PAGE_SIZE; use modular_bitfield::prelude::*; diff --git a/litebox_platform_lvbs/src/mshv/ringbuffer.rs b/litebox_platform_lvbs/src/mshv/ringbuffer.rs index 355ffc3d90..573bde849c 100644 --- a/litebox_platform_lvbs/src/mshv/ringbuffer.rs +++ b/litebox_platform_lvbs/src/mshv/ringbuffer.rs @@ -3,7 +3,7 @@ //! RingBuffer implementation and functions -use crate::Vtl0PhysMutPtr; +use super::PrivilegedVtl0PhysMutPtr; use core::fmt; use litebox::mm::linux::PAGE_SIZE; use litebox::utils::TruncateExt; @@ -96,7 +96,7 @@ fn write_fast(rb_pa: PhysAddr, size: usize, write_offset: usize, buf: &[u8]) -> span.push(addr); } - let Ok(ptr) = Vtl0PhysMutPtr::::new(&span, in_page_offset) else { + let Ok(ptr) = PrivilegedVtl0PhysMutPtr::::new(&span, in_page_offset) else { return advance_offset(size, write_offset, buf.len()); }; let _ = ptr.write_slice_at_offset(0, buf); @@ -108,9 +108,12 @@ fn write_fast(rb_pa: PhysAddr, size: usize, write_offset: usize, buf: &[u8]) -> /// after attempting the write. fn write_slow(rb_pa: PhysAddr, size: usize, write_offset: usize, buf: &[u8]) -> usize { let write_slice = |pa: PhysAddr, slice: &[u8]| -> bool { - Vtl0PhysMutPtr::::with_contiguous_pages(pa.as_u64().trunc(), slice.len()) - .and_then(|ptr| ptr.write_slice_at_offset(0, slice)) - .is_ok() + PrivilegedVtl0PhysMutPtr::::with_contiguous_pages( + pa.as_u64().trunc(), + slice.len(), + ) + .and_then(|ptr| ptr.write_slice_at_offset(0, slice)) + .is_ok() }; if buf.len() >= size { diff --git a/litebox_platform_lvbs/src/mshv/vsm.rs b/litebox_platform_lvbs/src/mshv/vsm.rs index 29cc0442fa..7cb97c8e3a 100644 --- a/litebox_platform_lvbs/src/mshv/vsm.rs +++ b/litebox_platform_lvbs/src/mshv/vsm.rs @@ -6,8 +6,9 @@ #[cfg(debug_assertions)] use crate::mshv::mem_integrity::parse_modinfo; use crate::mshv::ringbuffer::set_ringbuffer; +use crate::mshv::{PrivilegedVtl0PhysMutPtr, Vtl0PhysConstPtr}; use crate::{ - Vtl0PhysConstPtr, Vtl0PhysMutPtr, debug_serial_println, + debug_serial_println, host::{ PRK_LEN, bootparam::get_vtl1_memory_info, @@ -41,6 +42,7 @@ use crate::{ vtl1_mem_layout::{PAGE_SHIFT, PAGE_SIZE}, }, }; + use alloc::{boxed::Box, ffi::CString, string::String, vec::Vec}; use core::{ mem, @@ -50,7 +52,8 @@ use core::{ use hashbrown::{HashMap, HashSet}; use litebox::utils::TruncateExt; use litebox_common_linux::{errno::Errno, vmap::PhysPageAddr}; -use spin::Once; +use rangemap::RangeSet; +use spin::{Once, rwlock::RwLock as SpinRwLock}; use thiserror::Error; use x86_64::{ PhysAddr, VirtAddr, @@ -465,6 +468,131 @@ pub fn mshv_vsm_load_kdata(pa: u64, nranges: u64) -> Result { // TODO: save blocklist hashes } +/// RAII reservation over VTL0 physical frames, shared by module load and kexec validation. +/// On drop without `commit`, every newly reserved range is restored to VTL0 read/write, +/// non-executable access. +struct FrameReservation { + owned_ranges: Vec>, + owned_frames: RangeSet, + committed: bool, +} + +#[derive(Debug, PartialEq, Eq)] +enum ReservationStatus { + New, + AlreadyOwned, +} + +impl FrameReservation { + fn new() -> Self { + Self { + owned_ranges: Vec::new(), + owned_frames: RangeSet::new(), + committed: false, + } + } + + fn classify( + owned: &RangeSet, + registry: &ProtectedFrameUpdateGuard<'_>, + range: Range, + ) -> Result { + if owned.gaps(&range).next().is_none() { + return Ok(ReservationStatus::AlreadyOwned); + } + if owned.overlaps(&range) || registry.overlaps(&range) { + Err(VsmError::ProtectedFrameOverlap) + } else { + Ok(ReservationStatus::New) + } + } + + /// Reserve `frames`. Ranges fully owned before this call are accepted idempotently. Overlap + /// within this batch, partial overlap with prior ownership, and overlap with VTL1, protected + /// frames, or another reservation are rejected. + /// + /// Validation and insertion are atomic under exclusive registry access. On rejection, only + /// claims added by this call are rolled back. + fn reserve( + &mut self, + frames: impl IntoIterator>, + ) -> Result, VsmError> { + let vtl1 = crate::platform_low().vtl1_phys_frame_range(); + let vtl1_start = vtl1.start.start_address().as_u64(); + let vtl1_end = vtl1.end.start_address().as_u64(); + + protected_frame_registry().with_exclusive(|protected| { + // Idempotence applies only to ranges owned before this call. + let owned_before = self.owned_frames.clone(); + let mut seen = RangeSet::new(); + let mut statuses = Vec::new(); + // Frames this call adds, so a later overlap rolls back only them. + let rollback_from = self.owned_ranges.len(); + for phys_frame_range in frames { + let start = phys_frame_range.start.start_address().as_u64(); + let end = phys_frame_range.end.start_address().as_u64(); + if start >= end { + statuses.push(ReservationStatus::AlreadyOwned); + continue; + } + // `protected` holds existing non-writable frames, this reservation's earlier + // claims, and any other concurrent reservation's in-flight claims. + let range = start..end; + let status = if seen.overlaps(&range) || (start < vtl1_end && vtl1_start < end) { + Err(VsmError::ProtectedFrameOverlap) + } else { + Self::classify(&owned_before, protected, range.clone()) + }; + let status = match status { + Ok(status) => status, + Err(error) => { + for undo in &self.owned_ranges[rollback_from..] { + let range = undo.start.start_address().as_u64() + ..undo.end.start_address().as_u64(); + protected.remove(range.clone()); + self.owned_frames.remove(range); + } + self.owned_ranges.truncate(rollback_from); + return Err(error); + } + }; + seen.insert(range.clone()); + if status == ReservationStatus::AlreadyOwned { + statuses.push(status); + continue; + } + protected.insert(range.clone()); + self.owned_frames.insert(range); + self.owned_ranges.push(phys_frame_range); + statuses.push(status); + } + Ok(statuses) + }) + } + + /// Mark the reserved frames as committed; drop becomes a no-op. + fn commit(&mut self) { + self.committed = true; + } +} + +impl Drop for FrameReservation { + fn drop(&mut self) { + if self.committed { + return; + } + // Rollback: restore every newly reserved range to VTL0 read/write, non-executable access. + // Drop cannot report failure, so debug builds assert it. + for &phys_frame_range in &self.owned_ranges { + let result = unprotect_physical_memory_range(phys_frame_range); + debug_assert!( + result.is_ok(), + "Failed to restore VTL0 read/write access for reserved frames" + ); + } + } +} + /// VSM function for validating a guest kernel module and applying specified protection to its memory ranges after validation. /// `pa` and `nranges` specify a memory area containing the information about the kernel module to validate or protect. /// `flags` controls the validation process (unused for now). @@ -528,6 +656,17 @@ pub fn mshv_vsm_validate_guest_module(pa: u64, nranges: u64, _flags: u64) -> Res } } + // Reject overlap and reserve this module's frames. Legitimate module frames are never shared. + let mut frame_guard = FrameReservation::new(); + let _ = frame_guard.reserve(module_memory_metadata.iter().map(|r| r.phys_frame_range))?; + + // Freeze frames that require immutable copy/validation to avoid TOCTOU. + for mod_mem_range in &module_memory_metadata { + if !mod_mem_type_to_mem_attr(mod_mem_range.mod_mem_type).contains(MemAttr::MEM_ATTR_WRITE) { + protect_physical_memory_range(mod_mem_range.phys_frame_range, MemAttr::MEM_ATTR_READ)?; + } + } + module_as_elf .write_bytes_from_heki_range() .map_err(|_| VsmError::Vtl0CopyFailed)?; @@ -559,7 +698,21 @@ pub fn mshv_vsm_validate_guest_module(pa: u64, nranges: u64, _flags: u64) -> Res return Err(VsmError::ModuleRelocationInvalid); } - // pre-computed patch data for a module + // Both read-only and executable frames have been frozen above. + // Thus, only promote executable frames to RX. + for mod_mem_range in &module_memory_metadata { + if matches!( + mod_mem_range.mod_mem_type, + ModMemType::Text | ModMemType::InitText + ) { + protect_physical_memory_range( + mod_mem_range.phys_frame_range, + mod_mem_type_to_mem_attr(mod_mem_range.mod_mem_type), + )?; + } + } + + // Commit the module's pre-computed patch data (transactional). if !patch_info_for_module.is_empty() { let patch_info_buf = &patch_info_for_module[..]; crate::platform_low() @@ -569,14 +722,8 @@ pub fn mshv_vsm_validate_guest_module(pa: u64, nranges: u64, _flags: u64) -> Res .map_err(|_| VsmError::Vtl0CopyFailed)?; } - // once a module is verified and validated, change the permission of its memory ranges based on their types - for mod_mem_range in &module_memory_metadata { - protect_physical_memory_range( - mod_mem_range.phys_frame_range, - mod_mem_type_to_mem_attr(mod_mem_range.mod_mem_type), - )?; - } - + // Fully validated and committed: disarm the guard and register the module. + frame_guard.commit(); // register the module memory in the global map and obtain a unique token for it let token = crate::platform_low() .vtl0_kernel_info @@ -600,33 +747,49 @@ pub fn mshv_vsm_free_guest_module_init(token: i64) -> Result { return Err(VsmError::ModuleTokenInvalid); } + let mut result: Result<(), VsmError> = Ok(()); if let Some(entry) = crate::platform_low() .vtl0_kernel_info .module_memory_metadata .iter_entry(token) { for mod_mem_range in entry.iter_mem_ranges() { - match mod_mem_range.mod_mem_type { + let range_result = match mod_mem_range.mod_mem_type { ModMemType::InitText | ModMemType::InitData | ModMemType::InitRoData => { - // make this memory range readable, writable, and non-executable after initialization to let the VTL0 kernel free it - protect_physical_memory_range( - mod_mem_range.phys_frame_range, - MemAttr::MEM_ATTR_READ | MemAttr::MEM_ATTR_WRITE, - )?; + unprotect_physical_memory_range(mod_mem_range.phys_frame_range) } ModMemType::RoAfterInit => { // make this memory range read-only after initialization protect_physical_memory_range( mod_mem_range.phys_frame_range, MemAttr::MEM_ATTR_READ, - )?; + ) } - _ => {} + _ => Ok(()), + }; + if range_result.is_err() { + result = range_result; + break; } } } - Ok(0) + // Drop the init ranges from the module's metadata regardless of failures. This is intentional + // since hypercalls shouldn't fail and avoiding double release is more important. + let freed_init_patch_targets = crate::platform_low() + .vtl0_kernel_info + .module_memory_metadata + .remove_init_ranges(token); + // Remove the precomputed patches targeting those freed init frames so a stale init patch cannot + // later be applied to recycled frames (no patch-after-free). + if !freed_init_patch_targets.is_empty() { + crate::platform_low() + .vtl0_kernel_info + .precomputed_patches + .remove_patch_data(&freed_init_patch_targets); + } + + result.map(|()| 0) } /// VSM function for supporting the unloading of a guest kernel module. @@ -647,12 +810,8 @@ pub fn mshv_vsm_unload_guest_module(token: i64) -> Result { .module_memory_metadata .iter_entry(token) { - // make the memory ranges of a module readable, writable, and non-executable to let the VTL0 kernel unload the module for mod_mem_range in entry.iter_mem_ranges() { - protect_physical_memory_range( - mod_mem_range.phys_frame_range, - MemAttr::MEM_ATTR_READ | MemAttr::MEM_ATTR_WRITE, - )?; + unprotect_physical_memory_range(mod_mem_range.phys_frame_range)?; } } @@ -708,10 +867,7 @@ pub fn mshv_vsm_kexec_validate(pa: u64, nranges: u64, crash: u64) -> Result Result Result KEXEC_SEGMENT_MAX as u64 { return Err(VsmError::KexecImageSegmentsInvalid); } + let mut segment_ranges = Vec::new(); for i in 0..usize::try_from(kimage.nr_segments).unwrap_or(0) { let va = kimage.segment[i].buf; let pa = kimage.segment[i].mem; if let Some(epa) = pa.checked_add(kimage.segment[i].memsz) { - kexec_memory_metadata.insert_memory_range(KexecMemoryRange::new(va, pa, epa)?); + segment_ranges.push(KexecMemoryRange::new(va, pa, epa)?); } else { return Err(VsmError::KexecSegmentRangeInvalid); } } + let reservation_statuses = + frame_guard.reserve(segment_ranges.iter().map(|r| r.phys_frame_range))?; + for (segment_range, status) in segment_ranges.into_iter().zip(reservation_statuses) { + if status == ReservationStatus::New { + protect_physical_memory_range( + segment_range.phys_frame_range, + MemAttr::MEM_ATTR_READ, + )?; + kexec_memory_metadata.insert_memory_range(segment_range); + } + } } - // write protect the kexec memory ranges first to avoid the race condition during verification - for kexec_mem_range in &kexec_memory_metadata { - protect_physical_memory_range(kexec_mem_range.phys_frame_range, MemAttr::MEM_ATTR_READ)?; - } - - // verify the signature of kexec blob - let kexec_kernel_blob_data = &kexec_kernel_blob[..]; - - if let Err(result) = verify_kernel_pe_signature(kexec_kernel_blob_data, certs) { - for kexec_mem_range in &kexec_memory_metadata { - protect_physical_memory_range( - kexec_mem_range.phys_frame_range, - MemAttr::MEM_ATTR_READ | MemAttr::MEM_ATTR_WRITE, - )?; - } + // verify the signature of the kexec blob + if let Err(result) = verify_kernel_pe_signature(&kexec_kernel_blob[..], certs) { return Err(VsmError::SignatureVerificationFailed(result)); } + frame_guard.commit(); // register the protected kexec memory ranges to support possible invalidation in the future kexec_metadata_ref.register_memory(kexec_memory_metadata); @@ -877,8 +1041,8 @@ fn copy_heki_patch_from_vtl0(patch_pa_0: u64, patch_pa_1: u64) -> Result Result<(), VsmError> { // `HekiPatch::is_valid` already validated both physical addresses. let heki_patch_pa_0 = PhysAddr::new(heki_patch.pa[0]); @@ -900,7 +1064,8 @@ fn apply_vtl0_text_patch(heki_patch: HekiPatch) -> Result<(), VsmError> { <= heki_patch_pa_0.align_down(Size4KiB::SIZE).as_u64() + Size4KiB::SIZE, "patch crosses page boundary but pa_1 is null" ); - let ptr = Vtl0PhysMutPtr::::with_contiguous_pages( + // The patch was validated against VTL1's precomputed HEKI patch data. + let ptr = PrivilegedVtl0PhysMutPtr::::with_contiguous_pages( heki_patch_pa_0.as_u64().trunc(), patch.len(), ) @@ -916,7 +1081,8 @@ fn apply_vtl0_text_patch(heki_patch: HekiPatch) -> Result<(), VsmError> { PhysPageAddr::::new(heki_patch_pa_1.as_u64().trunc()) .ok_or(VsmError::Vtl0CopyFailed)?, ]; - let ptr = Vtl0PhysMutPtr::::new( + // The patch was validated against VTL1's precomputed HEKI patch data. + let ptr = PrivilegedVtl0PhysMutPtr::::new( &pages, (heki_patch_pa_0 - heki_patch_pa_0.align_down(Size4KiB::SIZE)).trunc(), ) @@ -928,6 +1094,10 @@ fn apply_vtl0_text_patch(heki_patch: HekiPatch) -> Result<(), VsmError> { } fn mshv_vsm_allocate_ringbuffer_memory(phys_addr: u64, size: usize) -> Result { + if crate::platform_low().vtl0_kernel_info.check_end_of_boot() { + return Err(VsmError::OperationAfterEndOfBoot("ring buffer allocation")); + } + let end = phys_addr .checked_add(size as u64) .ok_or(VsmError::IntegerOverflow) @@ -1333,6 +1503,45 @@ impl ModuleMemoryMetadataMap { map.remove(&key).is_some() } + /// Drop a module's freed init ranges from its metadata after [`mshv_vsm_free_guest_module_init`] + /// hands them back to VTL0, so a later free/unload does not re-release them. + /// + /// It also returns patch targets that fell within this freed init frames. These patch targets + /// are no longer valid (i.e., potential patch-after-free) and thus their corresponding + /// precomputed patches should be removed (we can't remove them here due to locks). + fn remove_init_ranges(&self, key: i64) -> Vec { + let is_init = |t| { + matches!( + t, + ModMemType::InitText | ModMemType::InitData | ModMemType::InitRoData + ) + }; + let mut map = self.inner.lock(); + let Some(metadata) = map.get_mut(&key) else { + return Vec::new(); + }; + let init_ranges: Vec> = metadata + .ranges + .iter() + .filter(|r| is_init(r.mod_mem_type)) + .map(|r| r.phys_frame_range) + .collect(); + metadata.ranges.retain(|r| !is_init(r.mod_mem_type)); + let mut freed_patch_targets = Vec::new(); + metadata.patch_targets.retain(|&pa| { + let freed = init_ranges + .iter() + .any(|fr| fr.start.start_address() <= pa && fr.end.start_address() > pa); + if freed { + freed_patch_targets.push(pa); + false + } else { + true + } + }); + freed_patch_targets + } + /// Return the addresses of patch targets belonging to a module identified by `key` pub(crate) fn get_patch_targets(&self, key: i64) -> Option> { let guard = self.inner.lock(); @@ -1411,61 +1620,167 @@ fn copy_heki_pages_from_vtl0(pa: u64, nranges: u64) -> Option> { Some(heki_pages) } -/// Protects a VTL0 physical memory range from potentially compromised VTL0 by restricting its -/// access permissions using VTL protection mask (e.g., kernel code integrity). +/// Registry of VTL0 frames that are non-writable to VTL0 or reserved by in-flight module or kexec +/// validation. Ordinary writable mappings retain shared access for their lifetime; reservations and +/// VTL0 protection updates use exclusive access. Privileged HEKI and ring-buffer mappings bypass +/// the registry. +pub(crate) struct ProtectedFrameRegistry { + frames: SpinRwLock>, +} + +/// Opaque guard that holds shared registry access for an ordinary writable mapping, blocking +/// exclusive protection and reservation updates until dropped. +pub(crate) struct ProtectedFrameAccessGuard<'a> { + _guard: spin::rwlock::RwLockReadGuard<'a, RangeSet>, +} + +struct ProtectedFrameUpdateGuard<'a> { + guard: spin::rwlock::RwLockWriteGuard<'a, RangeSet>, +} + +impl ProtectedFrameUpdateGuard<'_> { + fn overlaps(&self, range: &Range) -> bool { + self.guard.overlaps(range) + } + + fn insert(&mut self, range: Range) { + self.guard.insert(range); + } + + fn remove(&mut self, range: Range) { + self.guard.remove(range); + } + + fn record_protection(&mut self, phys_frame_range: PhysFrameRange, protect: bool) { + let start = phys_frame_range.start.start_address().as_u64(); + let end = phys_frame_range.end.start_address().as_u64(); + if start >= end { + return; + } + if protect { + self.insert(start..end); + } else { + self.remove(start..end); + } + } +} + +impl ProtectedFrameRegistry { + fn new() -> Self { + Self { + frames: SpinRwLock::new(RangeSet::new()), + } + } + + /// Validates that no requested page is registered as protected or reserved and returns a shared + /// guard that prevents protection or reservation updates until dropped. + pub(crate) fn acquire_access_guard( + &self, + pages: &litebox_common_linux::vmap::PhysPageAddrArray, + ) -> Result, litebox_common_linux::vmap::PhysPointerError> { + let guard = self.frames.read(); + for page in pages { + let start = page.as_usize() as u64; + let end = start + .checked_add(ALIGN as u64) + .ok_or(litebox_common_linux::vmap::PhysPointerError::Overflow)?; + if guard.overlaps(&(start..end)) { + return Err( + litebox_common_linux::vmap::PhysPointerError::InvalidPhysicalAddress( + page.as_usize(), + ), + ); + } + } + Ok(ProtectedFrameAccessGuard { _guard: guard }) + } + + /// Runs `f` with exclusive registry access. + fn with_exclusive(&self, f: impl FnOnce(&mut ProtectedFrameUpdateGuard<'_>) -> R) -> R { + f(&mut ProtectedFrameUpdateGuard { + guard: self.frames.write(), + }) + } +} + +pub(crate) fn protected_frame_registry() -> &'static ProtectedFrameRegistry { + static REGISTRY: Once = Once::new(); + REGISTRY.call_once(ProtectedFrameRegistry::new) +} + +/// Protect a VTL0 physical memory range using VTL protection mask (e.g., kernel code integrity). +/// +/// The registry tracks non-writable VTL0 ranges and temporary validation reservations. +/// See [`protected_frame_registry`]. /// /// If the requested range overlaps with VTL1 working memory, the VTL1 portion is silently /// skipped and only the remaining VTL0 portions are protected. If the range falls entirely /// within VTL1, this function returns `Ok(())` without issuing a hypercall. /// -/// `phys_frame_range` specifies the physical frame range to protect (must belong to VTL0). +/// `phys_frame_range` specifies the range whose VTL0 permissions are updated; VTL1 working-memory +/// portions are ignored. /// `mem_attr` specifies the memory attributes (VTL0's allowed access) to be applied. pub(crate) fn protect_physical_memory_range( phys_frame_range: PhysFrameRange, mem_attr: MemAttr, ) -> Result<(), VsmError> { + let protect = !mem_attr.contains(MemAttr::MEM_ATTR_WRITE); let vtl1_range = crate::platform_low().vtl1_phys_frame_range(); - // Fast path: no overlap with VTL1 — protect the entire range directly. - let overlaps_vtl1 = - phys_frame_range.start < vtl1_range.end && vtl1_range.start < phys_frame_range.end; - - if !overlaps_vtl1 { - let pa = phys_frame_range.start.start_address().as_u64(); - let num_pages = phys_frame_range.count() as u64; - hv_modify_vtl_protection_mask(pa, num_pages, mem_attr_to_hv_page_prot_flags(mem_attr)) - .map_err(VsmError::HypercallFailed)?; - return Ok(()); - } - // Range fully within VTL1 — nothing to protect for VTL0. if phys_frame_range.start >= vtl1_range.start && phys_frame_range.end <= vtl1_range.end { return Ok(()); } - // Partial overlap: split into the portions before and after VTL1, skipping VTL1 pages. - let sub_ranges: [PhysFrameRange; 2] = { - let before = PhysFrame::range( - phys_frame_range.start, - core::cmp::min(phys_frame_range.end, vtl1_range.start), - ); - let after = PhysFrame::range( - core::cmp::max(phys_frame_range.start, vtl1_range.end), - phys_frame_range.end, - ); - [before, after] - }; + // Fast path: no overlap with VTL1 — protect the entire range directly. + let overlaps_vtl1 = + phys_frame_range.start < vtl1_range.end && vtl1_range.start < phys_frame_range.end; - for sub_range in sub_ranges { - if sub_range.start >= sub_range.end { - continue; + protected_frame_registry().with_exclusive(|protected| { + if !overlaps_vtl1 { + let pa = phys_frame_range.start.start_address().as_u64(); + let num_pages = phys_frame_range.count() as u64; + hv_modify_vtl_protection_mask(pa, num_pages, mem_attr_to_hv_page_prot_flags(mem_attr)) + .map_err(VsmError::HypercallFailed)?; + protected.record_protection(phys_frame_range, protect); + return Ok(()); } - let pa = sub_range.start.start_address().as_u64(); - let num_pages = sub_range.count() as u64; - hv_modify_vtl_protection_mask(pa, num_pages, mem_attr_to_hv_page_prot_flags(mem_attr)) - .map_err(VsmError::HypercallFailed)?; - } - Ok(()) + + // Partial overlap: split into the portions before and after VTL1, skipping VTL1 pages. + let sub_ranges: [PhysFrameRange; 2] = { + let before = PhysFrame::range( + phys_frame_range.start, + core::cmp::min(phys_frame_range.end, vtl1_range.start), + ); + let after = PhysFrame::range( + core::cmp::max(phys_frame_range.start, vtl1_range.end), + phys_frame_range.end, + ); + [before, after] + }; + + for sub_range in sub_ranges { + if sub_range.start >= sub_range.end { + continue; + } + let pa = sub_range.start.start_address().as_u64(); + let num_pages = sub_range.count() as u64; + hv_modify_vtl_protection_mask(pa, num_pages, mem_attr_to_hv_page_prot_flags(mem_attr)) + .map_err(VsmError::HypercallFailed)?; + protected.record_protection(sub_range, protect); + } + Ok(()) + }) +} + +/// Restore VTL0 read/write access while leaving execution disabled, and removes the registry entry. +fn unprotect_physical_memory_range( + phys_frame_range: PhysFrameRange, +) -> Result<(), VsmError> { + protect_physical_memory_range( + phys_frame_range, + MemAttr::MEM_ATTR_READ | MemAttr::MEM_ATTR_WRITE, + ) } /// This function is a variant of [`protect_physical_memory_range`] to protect a VTL1 physical memory range. @@ -1899,7 +2214,8 @@ impl PatchDataMap { if patch_info_buf.len() < core::mem::size_of::() { return Err(PatchDataMapError::InvalidHekiPatchInfo); } - let mut inner = self.inner.write(); + + let mut parsed: Vec<(PhysAddr, HekiPatch)> = Vec::new(); // the buffer looks like below: // [`HekiPatchInfo`, [`HekiPatch`, ...], `HekiPatchInfo`, [`HekiPatch`, ...], ...] @@ -1935,54 +2251,50 @@ impl PatchDataMap { let patch_target_pa_0 = PhysAddr::new(patch.pa[0]); let patch_target_pa_1 = PhysAddr::new(patch.pa[1]); - if let Some(ref mut mod_mem_meta) = module_memory_metadata { - for mod_mem_range in &**mod_mem_meta { + // The second page is used as an additional key when a patch straddles two physical + // pages (see `validate_text_poke_bp_batch`). + let straddles_second_page = !patch_target_pa_1.is_null() + && patch_target_pa_0 + .as_u64() + .checked_add(1) + .and_then(|next| PhysAddr::try_new(next).ok()) + .is_some_and(|next| next.is_aligned(Size4KiB::SIZE)); + + if let Some(ref mod_mem_meta) = module_memory_metadata { + // Only accept patch targets within the module's executable ranges. + let in_executable_range = mod_mem_meta.iter().any(|mod_mem_range| { let in_range = |pa: PhysAddr| { mod_mem_range.phys_frame_range.start.start_address() <= pa && mod_mem_range.phys_frame_range.end.start_address() > pa }; - if matches!( + matches!( mod_mem_range.mod_mem_type, ModMemType::Text | ModMemType::InitText ) && in_range(patch_target_pa_0) && (patch_target_pa_1.is_null() || in_range(patch_target_pa_1)) - { - mod_mem_meta.insert_patch_target(patch_target_pa_0); - inner.insert(patch_target_pa_0, patch); - - // If the first byte of a patch target is in the first (physical) page while the remaining bytes - // are in the second page, we use the second page as an additional key for the patch to deal with - // Step 2 of `text_poke_bp_batch` where we only know the second to last bytes of the patch such - // that cannot know the address of the first page. Details are in `validate_text_poke_bp_batch`. - if !patch_target_pa_1.is_null() - && patch_target_pa_0 - .as_u64() - .checked_add(1) - .and_then(|next| PhysAddr::try_new(next).ok()) - .is_some_and(|next| next.is_aligned(Size4KiB::SIZE)) - { - mod_mem_meta.insert_patch_target(patch_target_pa_1); - inner.insert(patch_target_pa_1, patch); - } - break; - } - } - } else { - inner.insert(patch_target_pa_0, patch); - if !patch_target_pa_1.is_null() - && patch_target_pa_0 - .as_u64() - .checked_add(1) - .and_then(|next| PhysAddr::try_new(next).ok()) - .is_some_and(|next| next.is_aligned(Size4KiB::SIZE)) - { - inner.insert(patch_target_pa_1, patch); + }); + if !in_executable_range { + continue; } } + + parsed.push((patch_target_pa_0, patch)); + if straddles_second_page { + parsed.push((patch_target_pa_1, patch)); + } } index = patches_end; } + // Commit every parsed patch and record its targets for later unload cleanup. + let mut inner = self.inner.write(); + for (target, patch) in parsed { + inner.insert(target, patch); + if let Some(ref mut mod_mem_meta) = module_memory_metadata { + mod_mem_meta.insert_patch_target(target); + } + } + Ok(()) } } From 2fd502f9a8567a36b329e1def80d6361836f1bd9 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 17 Jul 2026 12:00:57 -0700 Subject: [PATCH 099/319] Bump bitflags to 2.13.1 in ulitebox (#1038) Cherry-pick f05d72d698a150289641911425063e95f8de1baa onto `ulitebox`. --- Cargo.lock | 40 ++++++++++++------------ litebox/Cargo.toml | 2 +- litebox_platform_linux_kernel/Cargo.toml | 2 +- litebox_shim_linux/Cargo.toml | 2 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 098a78558b..fc2e643e37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,7 +163,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "cexpr", "clang-sys", "itertools", @@ -211,9 +211,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bitvec" @@ -1439,7 +1439,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "libc", "plain", "redox_syscall", @@ -1456,7 +1456,7 @@ name = "litebox" version = "0.1.0" dependencies = [ "arrayvec", - "bitflags 2.11.0", + "bitflags 2.13.1", "buddy_system_allocator", "either", "hashbrown", @@ -1480,7 +1480,7 @@ dependencies = [ name = "litebox_broker_core" version = "0.1.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "hashbrown", "litebox_broker_protocol", "spin 0.9.8", @@ -1537,7 +1537,7 @@ name = "litebox_common_linux" version = "0.1.0" dependencies = [ "bitfield", - "bitflags 2.11.0", + "bitflags 2.13.1", "cfg-if", "elf", "int-enum", @@ -1551,7 +1551,7 @@ dependencies = [ name = "litebox_common_optee" version = "0.1.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "elf", "litebox", "litebox_common_linux", @@ -1592,7 +1592,7 @@ version = "0.1.0" dependencies = [ "arrayvec", "bindgen", - "bitflags 2.11.0", + "bitflags 2.13.1", "litebox", "litebox_common_linux", "litebox_util_log", @@ -1629,7 +1629,7 @@ dependencies = [ "aligned-vec", "arrayvec", "authenticode", - "bitflags 2.11.0", + "bitflags 2.13.1", "cms", "const-oid", "digest", @@ -1812,7 +1812,7 @@ name = "litebox_shim_linux" version = "0.1.0" dependencies = [ "arrayvec", - "bitflags 2.11.0", + "bitflags 2.13.1", "bitvec", "libc", "litebox", @@ -1859,7 +1859,7 @@ dependencies = [ name = "litebox_shim_windows" version = "0.1.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "int-enum", "litebox", "litebox_common_linux", @@ -2203,7 +2203,7 @@ version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "cfg-if", "foreign-types", "libc", @@ -2469,7 +2469,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", ] [[package]] @@ -2498,7 +2498,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", ] [[package]] @@ -2611,7 +2611,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -2678,7 +2678,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "core-foundation", "core-foundation-sys", "libc", @@ -3074,7 +3074,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac9ee8b664c9f1740cd813fea422116f8ba29997bb7c878d1940424889802897" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "log", "num-traits", ] @@ -3216,7 +3216,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -3810,7 +3810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f042214de98141e9c8706e8192b73f56494087cc55ebec28ce10f26c5c364ae" dependencies = [ "bit_field", - "bitflags 2.11.0", + "bitflags 2.13.1", "rustversion", "volatile", ] diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 6e8c3ecfe7..610efd9fa8 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] arrayvec = { version = "0.7.6", default-features = false, optional = true } -bitflags = "2.6.0" +bitflags = "2.13.1" either = { version = "1.13.0", default-features = false } hashbrown = "0.15.2" smallvec = "1.13.2" diff --git a/litebox_platform_linux_kernel/Cargo.toml b/litebox_platform_linux_kernel/Cargo.toml index 858cb655e5..b134214965 100644 --- a/litebox_platform_linux_kernel/Cargo.toml +++ b/litebox_platform_linux_kernel/Cargo.toml @@ -9,7 +9,7 @@ edition = "2024" bindgen = "0.71.0" [dependencies] -bitflags = "2.9.0" +bitflags = "2.13.1" litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } paste = "1.0.15" diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index 8d6e231542..b58332a1e9 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] arrayvec = { version = "0.7.6", default-features = false } bitvec = { version = "1.0.1", default-features = false, features = ["alloc"] } -bitflags = "2.9.0" +bitflags = "2.13.1" litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } litebox_platform_multiplex = { path = "../litebox_platform_multiplex/", version = "0.1.0", default-features = false } From c49392761e761706ab7718661db24c9f3c00a84f Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Fri, 17 Jul 2026 13:27:23 -0700 Subject: [PATCH 100/319] Cherry pick "Enable concurrent mappings of shared normal-world/VTL0 physical pages" (#1039) Co-authored-by: Sangho Lee --- dev_tests/src/ratchet.rs | 2 +- .../src/arch/x86/mm/paging.rs | 35 +--- litebox_platform_lvbs/src/lib.rs | 166 ++++++---------- litebox_platform_lvbs/src/mm/mod.rs | 22 --- litebox_platform_lvbs/src/mm/vmap.rs | 184 +++++------------- litebox_runner_lvbs/src/lib.rs | 5 +- litebox_shim_optee/src/msg_handler.rs | 50 ++--- 7 files changed, 125 insertions(+), 339 deletions(-) diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 03aa8e6fb2..b1ec3e9a45 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -44,7 +44,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_runner_lvbs/", 5), ("litebox_runner_snp/", 2), ("litebox_shim_linux/", 1), - ("litebox_shim_optee/", 5), + ("litebox_shim_optee/", 4), ("litebox_shim_windows/", 1), ], |file| { diff --git a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs index 5ce73b30df..165f29584d 100644 --- a/litebox_platform_lvbs/src/arch/x86/mm/paging.rs +++ b/litebox_platform_lvbs/src/arch/x86/mm/paging.rs @@ -558,42 +558,13 @@ impl X64PageTable<'_, M, ALIGN> { frame_range: PhysFrameRange, flags: PageTableFlags, exec_ranges: Option<&[Range]>, - ) -> Result<*mut u8, MapToError> { - self.map_phys_frame_range_with(frame_range, flags, exec_ranges, M::pa_to_va) - } - - /// Map physical frame range to the page table using the direct-map offset - /// ([`MemoryProvider::pa_to_va_direct`], i.e., `PA + GVA_OFFSET`). - /// - /// Use this for VTL0 / external physical memory that should be accessible - /// through the direct-map region. - pub(crate) fn map_phys_frame_range_direct( - &self, - frame_range: PhysFrameRange, - flags: PageTableFlags, - exec_ranges: Option<&[Range]>, - ) -> Result<*mut u8, MapToError> { - self.map_phys_frame_range_with(frame_range, flags, exec_ranges, M::pa_to_va_direct) - } - - /// Common implementation for [`Self::map_phys_frame_range`] and - /// [`Self::map_phys_frame_range_direct`]. - /// - /// `pa_to_va` selects how physical addresses are translated to virtual - /// addresses — either via `KERNEL_OFFSET` or `GVA_OFFSET`. - fn map_phys_frame_range_with( - &self, - frame_range: PhysFrameRange, - flags: PageTableFlags, - exec_ranges: Option<&[Range]>, - pa_to_va: fn(PhysAddr) -> VirtAddr, ) -> Result<*mut u8, MapToError> { let mut allocator = PageTableAllocator::::new(); let mut inner = self.inner.lock(); for target_frame in frame_range { let page: Page = - Page::containing_address(pa_to_va(target_frame.start_address())); + Page::containing_address(M::pa_to_va(target_frame.start_address())); match inner.translate(page.start_address()) { TranslateResult::Mapped { @@ -655,12 +626,12 @@ impl X64PageTable<'_, M, ALIGN> { } let start_page = - Page::::containing_address(pa_to_va(frame_range.start.start_address())); + Page::::containing_address(M::pa_to_va(frame_range.start.start_address())); let count = (frame_range.end.start_address() - frame_range.start.start_address()) / Size4KiB::SIZE; flush_tlb_range(start_page, count.trunc()); - Ok(pa_to_va(frame_range.start.start_address()).as_mut_ptr()) + Ok(M::pa_to_va(frame_range.start.start_address()).as_mut_ptr()) } /// Map non-contiguous physical frames to virtually contiguous addresses. diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index 0ff8e438b9..6726a9f67f 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -24,7 +24,7 @@ use litebox::{ utils::TruncateExt, }; use litebox_common_linux::vmap::{ - GlobalVmapManager, PhysPageAddr, PhysPageAddrArray, PhysPageMapInfo, PhysPageMapPermissions, + GlobalVmapManager, PhysPageAddrArray, PhysPageMapInfo, PhysPageMapPermissions, PhysPointerError, VmapManager, }; use litebox_common_linux::{PunchthroughSyscall, errno::Errno}; @@ -98,7 +98,7 @@ pub const BASE_PAGE_TABLE_ID: usize = 0; // 0xFFFF_C000_0000_0000 ├─────────────────────────────────┤ // │ Direct map region (64 TiB) │ // │ VA = PA + GVA_OFFSET │ -// │ VTL0 memory mapped on demand │ +// │ Currently unused │ // │ │ // │ ┄ ┄ ┄ ┄ ┄ ┄ ┄ ┄ ┄ ┄ ┄ ┄ ┄ ┄ │ // │ VTL1 PA range = unmapped gap │ @@ -112,12 +112,13 @@ pub const BASE_PAGE_TABLE_ID: usize = 0; // │ mmap / TA memory │ // 0x0000_0000_0001_0000 └─────────────────────────────────┘ ← USER_ADDR_MIN // -// The 64 TiB direct map reservation ensures that any physical address -// up to 64 TiB can be mapped via the simple PA + GVA_OFFSET formula -// without colliding with the vmap region. A 1 TiB guard gap between -// the direct map and the vmap region catches stray accesses. -// VTL1 memory is never mapped in the direct map; it lives exclusively -// in the VTL1 kernel region at KERNEL_OFFSET. +// The 64 TiB direct map region is reserved for possible future use (e.g., device +// drivers, persistent mapping). Foreign physical memory currently uses private +// mappings in the vmap region instead. If direct mapping is restored, physical +// addresses up to 64 TiB can use the PA + GVA_OFFSET formula without colliding +// with vmap. A 1 TiB guard gap catches stray accesses. VTL1 memory must never +// be mapped in the direct map; it lives exclusively in the VTL1 kernel region +// at KERNEL_OFFSET. // // The VTL1 kernel region at the top of the address space maps the // entire VTL1 kernel via PA + KERNEL_OFFSET. A 1 TiB guard gap @@ -1142,22 +1143,6 @@ impl litebox::platform::SystemInfoProvider for LinuxKernel< } } -/// Checks whether the given physical addresses are contiguous with respect to ALIGN. -fn is_contiguous(addrs: &[PhysPageAddr]) -> bool { - for window in addrs.windows(2) { - let first = window[0].as_usize(); - let second = window[1].as_usize(); - if let Some(expected) = first.checked_add(ALIGN) { - if second != expected { - return false; - } - } else { - return false; - } - } - true -} - unsafe impl VmapManager for LinuxKernel { type MapInfo = LvbsPhysPageMapInfo; @@ -1198,7 +1183,8 @@ unsafe impl VmapManager for Linu // Reject duplicates early as an API-level validation. The page-table implementation also // rejects duplicate/shared mappings, but this keeps the error local to the input array. - if !is_contiguous(pages) { + // A single page can never collide with itself, so skip the set allocation. + if pages.len() > 1 { let mut seen = hashbrown::HashSet::with_capacity(pages.len()); for page in pages { if !seen.insert(page.as_usize()) { @@ -1213,79 +1199,54 @@ unsafe impl VmapManager for Linu flags |= PageTableFlags::WRITABLE; } - // `validate_unowned` rejects VTL1-owned PA before callers reach `vmap`, so these pages - // are foreign. Contiguous foreign PA uses the foreign direct-map VA range; non-contiguous - // foreign PA uses the vmap VA range. Neither range aliases VTL1-owned Rust memory. - if is_contiguous(pages) { - let phys_start = x86_64::PhysAddr::new(pages[0].as_usize() as u64); - let phys_end = x86_64::PhysAddr::new( - pages - .last() - .unwrap() - .as_usize() - .checked_add(ALIGN) - .ok_or(PhysPointerError::Overflow)? as u64, - ); - let frame_range = PhysFrame::range( - PhysFrame::::containing_address(phys_start), - PhysFrame::::containing_address(phys_end), - ); - - match self - .page_table_manager - .current_page_table() - .map_phys_frame_range_direct(frame_range, flags, None) - { - Ok(page_addr) => Ok(LvbsPhysPageMapInfo::new(page_addr, pages.len() * ALIGN)), - Err(MapToError::PageAlreadyMapped(_)) => { - Err(PhysPointerError::AlreadyMapped(pages[0].as_usize())) + // Always allocate a fresh, private virtual address window for the mapping. This lets + // multiple cores map the same physical frame(s) concurrently at distinct VAs (used only for + // transient data copy in/out via raw pointers), so a core unmapping its window never + // disturbs another core's access to the same frame. + // + // `validate_unowned` rejects VTL1-owned PA before callers reach `vmap`, so these pages are + // foreign and the vmap VA range never aliases VTL1-owned Rust memory. + let frames: alloc::vec::Vec> = pages + .iter() + .map(|p| { + let address = p.as_usize(); + x86_64::PhysAddr::try_new(address as u64) + .map(PhysFrame::containing_address) + .map_err(|_| PhysPointerError::InvalidPhysicalAddress(address)) + }) + .collect::>()?; + + let base_va = vmap_allocator() + .allocate_va(frames.len()) + .map_err(|e| match e { + crate::mm::vmap::VmapAllocError::VaSpaceExhausted => { + PhysPointerError::VaSpaceExhausted } - Err(MapToError::FrameAllocationFailed) => { - Err(PhysPointerError::FrameAllocationFailed) + // `pages` was checked non-empty above and `frames` is built 1:1 from it, so the + // allocator cannot report an empty input here. + crate::mm::vmap::VmapAllocError::EmptyInput => { + unreachable!("frames is derived 1:1 from a non-empty pages slice") } - Err(MapToError::ParentEntryHugePage) => Err( - PhysPointerError::InvalidPhysicalAddress(pages[0].as_usize()), - ), - } - } else { - let frames: alloc::vec::Vec> = pages - .iter() - .map(|p| PhysFrame::containing_address(x86_64::PhysAddr::new(p.as_usize() as u64))) - .collect(); - - let base_va = vmap_allocator() - .allocate_va_and_register_map(&frames) - .map_err(|e| match e { - crate::mm::vmap::VmapAllocError::EmptyInput => { - PhysPointerError::InvalidPhysicalAddress(0) - } - crate::mm::vmap::VmapAllocError::DuplicateMapping => { - PhysPointerError::AlreadyMapped(pages[0].as_usize()) - } - crate::mm::vmap::VmapAllocError::VaSpaceExhausted => { - PhysPointerError::VaSpaceExhausted - } - })?; + })?; - match self - .page_table_manager - .current_page_table() - .map_non_contiguous_phys_frames(&frames, base_va, flags) - { - Ok(page_addr) => Ok(LvbsPhysPageMapInfo::new(page_addr, pages.len() * ALIGN)), - Err(e) => { - let _ = vmap_allocator().unregister_allocation(base_va); - match e { - MapToError::PageAlreadyMapped(_) => { - Err(PhysPointerError::AlreadyMapped(pages[0].as_usize())) - } - MapToError::FrameAllocationFailed => { - Err(PhysPointerError::FrameAllocationFailed) - } - MapToError::ParentEntryHugePage => Err( - PhysPointerError::InvalidPhysicalAddress(pages[0].as_usize()), - ), + match self + .page_table_manager + .current_page_table() + .map_non_contiguous_phys_frames(&frames, base_va, flags) + { + Ok(page_addr) => Ok(LvbsPhysPageMapInfo::new(page_addr, pages.len() * ALIGN)), + Err(e) => { + vmap_allocator().free_va(base_va, frames.len()); + match e { + MapToError::PageAlreadyMapped(_) => { + Err(PhysPointerError::AlreadyMapped(pages[0].as_usize())) + } + MapToError::FrameAllocationFailed => { + Err(PhysPointerError::FrameAllocationFailed) } + MapToError::ParentEntryHugePage => Err( + PhysPointerError::InvalidPhysicalAddress(pages[0].as_usize()), + ), } } } @@ -1311,17 +1272,12 @@ unsafe impl VmapManager for Linu } // PTEs are already cleared at this point, so the mapping is functionally gone - // and a retry would only re-fail against empty page-table entries. If the VA - // allocator's bookkeeping is inconsistent, surface it via `debug_assert!`. The - // VA region is leaked but cannot be safely recycled. - let unregister_ok = !crate::mm::vmap::is_vmap_address(base_va) - || crate::mm::vmap::vmap_allocator() - .unregister_allocation(base_va) - .is_some(); - debug_assert!( - unregister_ok, - "vmap allocator unregister failed at {base_va:?}", - ); + // and a retry would only re-fail against empty page-table entries. Return the VA + // range to the allocator. `vmap_info` is consumed by value and never cloned, so this + // range is freed exactly once. + if crate::mm::vmap::is_vmap_address(base_va) { + crate::mm::vmap::vmap_allocator().free_va(base_va, size / ALIGN); + } Ok(()) } diff --git a/litebox_platform_lvbs/src/mm/mod.rs b/litebox_platform_lvbs/src/mm/mod.rs index a96d5fd128..fd1fc4427c 100644 --- a/litebox_platform_lvbs/src/mm/mod.rs +++ b/litebox_platform_lvbs/src/mm/mod.rs @@ -40,28 +40,6 @@ pub trait MemoryProvider { /// The caller must ensure that the memory range is valid and not used by any others. unsafe fn mem_fill_pages(start: usize, size: usize); - /// Obtain physical address (PA) of a page given its direct-map VA. - /// - /// The direct map covers all physical memory via `VA = PA + GVA_OFFSET`. - /// Use this for VTL0 / external physical memory. - fn va_to_pa_direct(va: VirtAddr) -> PhysAddr { - PhysAddr::new_truncate(va - Self::GVA_OFFSET) - } - - /// Obtain the direct-map virtual address (VA) of a page given its PA. - /// - /// The direct map covers all physical memory via `VA = PA + GVA_OFFSET`. - /// Use this for VTL0 / external physical memory. - fn pa_to_va_direct(pa: PhysAddr) -> VirtAddr { - let pa = pa.as_u64() & !Self::PRIVATE_PTE_MASK; - let va = VirtAddr::new_truncate(pa + Self::GVA_OFFSET.as_u64()); - assert!( - va.as_u64() < crate::VMAP_START as u64, - "VA {va:#x} is out of range for direct mapping" - ); - va - } - /// Obtain physical address (PA) of a page given its kernel VA. /// /// The VTL1 kernel region maps kernel memory via `VA = PA + KERNEL_OFFSET`. diff --git a/litebox_platform_lvbs/src/mm/vmap.rs b/litebox_platform_lvbs/src/mm/vmap.rs index 3f04477a6e..2416bc87fc 100644 --- a/litebox_platform_lvbs/src/mm/vmap.rs +++ b/litebox_platform_lvbs/src/mm/vmap.rs @@ -5,16 +5,16 @@ //! //! This module provides functionality similar to Linux kernel's `vmap()` and `vunmap()`: //! - Reserves a virtual address region for vmap mappings -//! - Maintains PA→VA mappings using HashMap for duplicate detection and cleanup +//! - Tracks allocations by base virtual address for cleanup +//! +//! The same physical frame may be mapped at multiple virtual addresses simultaneously, so no +//! PA→VA uniqueness is enforced: each mapping is a private, transient window. -use alloc::boxed::Box; -use hashbrown::HashMap; use litebox::utils::TruncateExt; use rangemap::RangeSet; use spin::Once; use spin::mutex::SpinMutex; use x86_64::VirtAddr; -use x86_64::structures::paging::{PhysFrame, Size4KiB}; use crate::mshv::vtl1_mem_layout::PAGE_SIZE; @@ -24,9 +24,6 @@ pub enum VmapAllocError { /// The input frame slice was empty. #[error("empty frame slice")] EmptyInput, - /// At least one physical frame is already mapped. - #[error("physical frame already mapped")] - DuplicateMapping, /// The vmap virtual address region has no contiguous range large enough. #[error("vmap virtual address space exhausted")] VaSpaceExhausted, @@ -41,26 +38,19 @@ const VMAP_END_VPN: usize = VMAP_END / PAGE_SIZE; /// Number of unmapped guard pages appended after each vmap allocation. const GUARD_PAGES: usize = 1; -/// Information about a single vmap allocation. -#[derive(Clone, Debug)] -struct VmapAllocation { - /// Physical frames of the mapped pages (in order). - frames: Box<[PhysFrame]>, -} - /// Inner state for the vmap region allocator. /// -/// Uses a bump allocator with a `RangeSet` free list for virtual page numbers -/// and HashMap for maintaining mappings between physical and virtual addresses. +/// Uses a bump allocator with a `RangeSet` free list for virtual page numbers. +/// +/// The same physical frame may be mapped at multiple virtual addresses simultaneously: each +/// mapping is a private, transient window (used only to copy data in/out). The allocator only +/// tracks free VA ranges; the caller owns the page count for each live mapping (it is recoverable +/// from the mapping info) and passes it back on teardown. struct VmapRegionAllocatorInner { /// Next available virtual page number for allocation (bump allocator). next_vpn: usize, /// Free set of previously allocated and freed VPN ranges (auto-coalescing). free_set: RangeSet, - /// Map from physical frame to virtual address. - pa_to_va_map: HashMap, VirtAddr>, - /// Allocation metadata indexed by starting virtual address. - allocations: HashMap, } impl VmapRegionAllocatorInner { @@ -69,8 +59,6 @@ impl VmapRegionAllocatorInner { Self { next_vpn: VMAP_START_VPN, free_set: RangeSet::new(), - pa_to_va_map: HashMap::new(), - allocations: HashMap::new(), } } @@ -134,7 +122,7 @@ pub fn is_vmap_address(va: VirtAddr) -> bool { (VMAP_START..VMAP_END).contains(&va.as_u64().trunc()) } -/// Vmap region allocator that manages virtual address allocation and PA↔VA mappings. +/// Vmap region allocator that manages virtual address allocation for transient physical mappings. pub struct VmapRegionAllocator { inner: SpinMutex, } @@ -146,72 +134,30 @@ impl VmapRegionAllocator { } } - /// Atomically allocates VA range, registers mappings, and records allocation. - /// - /// This ensures consistency: either the entire operation succeeds or nothing changes. + /// Allocates a fresh VA range covering `num_pages` mapped pages (plus trailing guard pages). /// /// # Errors /// - /// - [`VmapAllocError::EmptyInput`] — `frames` is empty. - /// - [`VmapAllocError::DuplicateMapping`] — a physical frame is already mapped. + /// - [`VmapAllocError::EmptyInput`] — `num_pages` is zero. /// - [`VmapAllocError::VaSpaceExhausted`] — no contiguous VA range is available. - pub fn allocate_va_and_register_map( - &self, - frames: &[PhysFrame], - ) -> Result { - if frames.is_empty() { + pub fn allocate_va(&self, num_pages: usize) -> Result { + if num_pages == 0 { return Err(VmapAllocError::EmptyInput); } - let mut inner = self.inner.lock(); - - // Check for duplicate PA mappings before allocating - for frame in frames { - if inner.pa_to_va_map.contains_key(frame) { - return Err(VmapAllocError::DuplicateMapping); - } - } - - let base_va = inner - .allocate_va_range(frames.len()) - .ok_or(VmapAllocError::VaSpaceExhausted)?; - let end_va = base_va + (frames.len() as u64) * (PAGE_SIZE as u64); - - for (va, &frame) in (base_va.as_u64()..end_va.as_u64()) - .step_by(PAGE_SIZE) - .map(VirtAddr::new) - .zip(frames.iter()) - { - inner.pa_to_va_map.insert(frame, va); - } - - inner.allocations.insert( - base_va, - VmapAllocation { - frames: frames.into(), - }, - ); - - Ok(base_va) + self.inner + .lock() + .allocate_va_range(num_pages) + .ok_or(VmapAllocError::VaSpaceExhausted) } - /// Unregisters all mappings for an allocation starting at the given virtual address - /// and returns its VA range to the free list. + /// Returns a `num_pages`-page VA range starting at `base_va` to the free list. /// - /// This is used both for normal `vunmap` teardown and to roll back a failed - /// page-table mapping after `allocate_va_and_register_map` succeeds. - /// - /// Returns the number of pages that were unmapped, or `None` if no allocation was found. - pub fn unregister_allocation(&self, base_va: VirtAddr) -> Option { - let mut inner = self.inner.lock(); - let allocation = inner.allocations.remove(&base_va)?; - for frame in &allocation.frames { - inner.pa_to_va_map.remove(frame); - } - - inner.free_va_range(base_va, allocation.frames.len()); - - Some(allocation.frames.len()) + /// This is used both for normal `vunmap` teardown and to roll back a failed page-table mapping + /// after [`Self::allocate_va`] succeeds. `base_va`/`num_pages` must match a value pair from a + /// prior `allocate_va`; mapping-info move semantics guarantee each range is freed at most once. + pub fn free_va(&self, base_va: VirtAddr, num_pages: usize) { + self.inner.lock().free_va_range(base_va, num_pages); } } @@ -224,7 +170,6 @@ pub fn vmap_allocator() -> &'static VmapRegionAllocator { #[cfg(test)] mod tests { use super::*; - use x86_64::PhysAddr; #[test] fn test_allocate_va_range() { @@ -268,94 +213,53 @@ mod tests { } #[test] - fn test_allocate_va_and_register_map() { + fn test_allocate_va() { let allocator = VmapRegionAllocator::new(); - let frames = alloc::vec![ - PhysFrame::::containing_address(PhysAddr::new(0x1000)), - PhysFrame::::containing_address(PhysAddr::new(0x3000)), - PhysFrame::::containing_address(PhysAddr::new(0x5000)), - ]; - - // Allocate and register - let base_va = allocator.allocate_va_and_register_map(&frames); + // Allocate a 3-page range + let base_va = allocator.allocate_va(3); assert!(base_va.is_ok()); - let base_va = base_va.unwrap(); - assert_eq!(base_va.as_u64(), VMAP_START as u64); - - // Duplicate PA should fail with DuplicateMapping - let duplicate = allocator - .allocate_va_and_register_map(&[PhysFrame::containing_address(PhysAddr::new(0x1000))]); - assert!(matches!(duplicate, Err(VmapAllocError::DuplicateMapping))); + assert_eq!(base_va.unwrap().as_u64(), VMAP_START as u64); - // Empty input should fail with EmptyInput + // Zero pages should fail with EmptyInput assert!(matches!( - allocator.allocate_va_and_register_map(&[]), + allocator.allocate_va(0), Err(VmapAllocError::EmptyInput) )); } #[test] - fn test_rollback_via_unregister() { + fn test_rollback_via_free() { let allocator = VmapRegionAllocator::new(); - let frames = alloc::vec![ - PhysFrame::::containing_address(PhysAddr::new(0x1000)), - PhysFrame::::containing_address(PhysAddr::new(0x2000)), - ]; - - let base_va = allocator.allocate_va_and_register_map(&frames).unwrap(); + let base_va = allocator.allocate_va(2).unwrap(); - // Simulate rollback by unregistering immediately - let count = allocator.unregister_allocation(base_va); - assert_eq!(count, Some(2)); + // Simulate rollback by freeing immediately + allocator.free_va(base_va, 2); - // Mappings should be gone — re-registering the same PAs must succeed - let new_va = allocator.allocate_va_and_register_map(&frames).unwrap(); + // The VA range should be gone — re-allocating must succeed and reuse it + let new_va = allocator.allocate_va(2).unwrap(); assert_eq!(new_va, base_va); } #[test] - fn test_unregister_allocation() { + fn test_free_va() { let allocator = VmapRegionAllocator::new(); - let frames = alloc::vec![ - PhysFrame::::containing_address(PhysAddr::new(0x1000)), - PhysFrame::::containing_address(PhysAddr::new(0x3000)), - PhysFrame::::containing_address(PhysAddr::new(0x5000)), - ]; + let base_va = allocator.allocate_va(3).unwrap(); - let base_va = allocator.allocate_va_and_register_map(&frames).unwrap(); - - // Unregister - let num_pages = allocator.unregister_allocation(base_va); - assert_eq!(num_pages, Some(3)); - - // Mappings should be gone — re-registering the same PAs must succeed - // and reuse the freed VA range - let new_va = allocator.allocate_va_and_register_map(&frames).unwrap(); + // Free, then re-allocating the same size must reuse the freed VA range + allocator.free_va(base_va, 3); + let new_va = allocator.allocate_va(3).unwrap(); assert_eq!(new_va, base_va); - - // Unregistering an unknown VA returns None - assert_eq!( - allocator.unregister_allocation(VirtAddr::new(VMAP_END as u64 - 0x1000)), - None - ); } #[test] fn test_guard_page_gap() { let allocator = VmapRegionAllocator::new(); - let frames_a = alloc::vec![PhysFrame::::containing_address(PhysAddr::new( - 0x1000 - )),]; - let frames_b = alloc::vec![PhysFrame::::containing_address(PhysAddr::new( - 0x2000 - )),]; - - let va_a = allocator.allocate_va_and_register_map(&frames_a).unwrap(); - let va_b = allocator.allocate_va_and_register_map(&frames_b).unwrap(); + let va_a = allocator.allocate_va(1).unwrap(); + let va_b = allocator.allocate_va(1).unwrap(); // Allocations should be separated by at least GUARD_PAGES unmapped pages let gap_pages = (va_b.as_u64() - va_a.as_u64()) / PAGE_SIZE as u64; diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index f9513b7edf..14687571d3 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -39,8 +39,7 @@ use litebox_platform_lvbs::{ }; use litebox_platform_multiplex::Platform; use litebox_shim_optee::msg_handler::{ - decode_ta_request, handle_optee_msg_args, handle_optee_smc_args, packed_msg_args_lock, - update_optee_msg_args, + decode_ta_request, handle_optee_msg_args, handle_optee_smc_args, update_optee_msg_args, }; use litebox_shim_optee::session::{OpenSessionTarget, TaInstance, session_manager}; use litebox_shim_optee::{NormalWorldConstPtr, NormalWorldMutPtr, UserConstPtr}; @@ -1240,8 +1239,6 @@ fn write_non_ta_msg_args_to_normal_world( msg_args_phys_addr.trunc(), msg_args_size, )?; - // Serialize the packed-page write. See `packed_msg_args_lock`. - let _packed_guard = packed_msg_args_lock(); ptr.write_slice_at_offset(0, &blob)?; Ok(()) } diff --git a/litebox_shim_optee/src/msg_handler.rs b/litebox_shim_optee/src/msg_handler.rs index f989e20239..3f73043b5b 100644 --- a/litebox_shim_optee/src/msg_handler.rs +++ b/litebox_shim_optee/src/msg_handler.rs @@ -142,6 +142,18 @@ fn parse_optee_msg_args( /// the main one at offset `optee_msg_args_total_size(num_params)` (the *actual* `num_params`, /// not `MAX_ARG_PARAM_COUNT`). This matches the Linux driver's layout. /// +/// # Concurrency +/// +/// This read is intentionally not serialized against a concurrent write-back on the same 4 KiB +/// frame. The Linux OP-TEE driver packs multiple `optee_msg_arg`s into sub-page slots and hands +/// out one slot per in-flight call (bitmap under `shm_arg_cache.mutex`), so concurrent cores touch +/// disjoint slots. Each access maps the frame into its own private, transient VA window (see the +/// `vmap`-based `PhysMutPtr`), so no page-table conflict arises either. A malicious normal-world +/// kernel can provide overlapped slots, but this only results in copying invalid `optee_msg_arg` +/// and/or corrupting normal-world memory - the malicious kernel can easily do these even without +/// slot overlaps. Our fallible memcpy with `FromBytes` ensures this copy-in does not result in +/// Rust safety/soundness issues. +/// /// VTL0 physical memory layout at `phys_addr`: /// /// ```text @@ -208,12 +220,7 @@ pub fn handle_optee_smc_args( OpteeSmcFunction::CallWithArg => { let msg_args_addr = smc.optee_msg_args_phys_addr()?; let msg_args_addr: usize = msg_args_addr.trunc(); - // Serialize the packed-page read against a concurrent write-back. See - // `packed_msg_args_lock`. - let (msg_args, _) = { - let _packed_guard = packed_msg_args_lock(); - read_optee_msg_args_from_phys(msg_args_addr, false)? - }; + let (msg_args, _) = read_optee_msg_args_from_phys(msg_args_addr, false)?; Ok(OpteeSmcResult::CallWithArg { msg_args, rpc_args: None, @@ -223,10 +230,7 @@ pub fn handle_optee_smc_args( OpteeSmcFunction::CallWithRpcArg => { let msg_args_addr = smc.optee_msg_args_phys_addr()?; let msg_args_addr: usize = msg_args_addr.trunc(); - let (msg_args, rpc_args) = { - let _packed_guard = packed_msg_args_lock(); - read_optee_msg_args_from_phys(msg_args_addr, true)? - }; + let (msg_args, rpc_args) = read_optee_msg_args_from_phys(msg_args_addr, true)?; Ok(OpteeSmcResult::CallWithArg { msg_args, rpc_args, @@ -246,12 +250,7 @@ pub fn handle_optee_smc_args( main_max + optee_msg_args_total_size(OpteeRpcArgs::MAX_RPC_ARG_PARAM_COUNT.trunc()); let mut blob = alloc::vec![0u8; copy_size]; - // Serialize the packed-page read against a concurrent write-back. See - // `packed_msg_args_lock`. - { - let _packed_guard = packed_msg_args_lock(); - shm_info.read_at(offset, &mut blob)?; - } + shm_info.read_at(offset, &mut blob)?; let (msg_args, rpc_args) = parse_optee_msg_args(&blob, true)?; // Compute the physical address of `OpteeMsgArgs` @@ -397,25 +396,6 @@ pub struct TaRequestInfo { pub out_shm_info: [Option>; UteeParamOwned::TEE_NUM_PARAMS], } -/// Acquire the lock serializing packed-`OpteeMsgArgs` page access on the base page table. -/// -/// The OP-TEE driver packs multiple requests into sub-page slots of one frame which can be -/// concurrently access by multiple cores which are on the base page table. Since LiteBox -/// currently doesn't support shared mapping, it uses this lock to serialize the concurrent -/// access. Note that cores on different task page tables (i.e., instances) do not need to -/// acquire this lock since they maintain their own mappings. -/// -/// Hold the guard only across the packed-page read/write. -/// -/// TODO: This is a temporary mitigation. It should be replaced by a more fundamental -/// approach such as shared mapping support, physical address range reservation, and/or -/// sub-page access control. -#[must_use] -pub fn packed_msg_args_lock() -> spin::mutex::SpinMutexGuard<'static, ()> { - static PACKED_MSG_ARGS_LOCK: spin::mutex::SpinMutex<()> = spin::mutex::SpinMutex::new(()); - PACKED_MSG_ARGS_LOCK.lock() -} - /// This function decodes a TA request contained in `OpteeMsgArgs`. /// /// It copies the entire parameter data from the normal world shared memory into the secure world's From 759546aba0be77a1f16da5d37609b8d16ba01c1e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 18 Jul 2026 08:30:45 -0700 Subject: [PATCH 101/319] Simplify broker principal rights (#1041) This PR replaces per-object principal rights with one object-rights set for each principal. It keeps the static policy surface simple before adding more broker object types. --- litebox_broker_core/src/lib.rs | 2 +- litebox_broker_core/src/policy.rs | 48 ++++--------------- litebox_broker_core/src/session.rs | 19 ++------ litebox_broker_host/src/lib.rs | 4 +- litebox_broker_userland/src/main.rs | 4 +- .../tests/notification_runtime.rs | 4 +- litebox_runner_linux_userland/tests/run.rs | 2 +- 7 files changed, 20 insertions(+), 63 deletions(-) diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 95aac9b09f..bf0713985e 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -31,7 +31,7 @@ use litebox_broker_protocol::ObjectHandle; use spin::rwlock::RwLock; pub use error::BrokerError; -pub use policy::{PolicyEngine, PolicyProfile, PrincipalRights}; +pub use policy::{PolicyEngine, PolicyProfile}; use session::ObjectReference; pub use session::{BrokerSession, CallerCredential, ObjectRights}; diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index b5c921fcf2..27d9da65f6 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::session::ObjectKind; use crate::{BrokerError, CallerCredential, ObjectRights}; /// Configured broker policy. @@ -13,33 +12,10 @@ pub enum PolicyProfile { /// Static rights for known broker principals. Static { /// Rights for the unauthenticated principal used by the initial POC. - unauthenticated: PrincipalRights, + unauthenticated: ObjectRights, }, } -/// Rights granted to one broker principal. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub struct PrincipalRights { - /// Rights for event objects. - pub event: ObjectRights, -} - -impl PrincipalRights { - /// Grants all currently supported object rights. - pub const fn all() -> Self { - Self { - event: ObjectRights::WAIT.union(ObjectRights::WRITE), - } - } - - fn object_rights(self, object_kind: ObjectKind) -> ObjectRights { - match object_kind { - ObjectKind::Event => self.event, - } - } -} - /// Broker policy decision and audit component. /// /// This initial engine is a placeholder static policy surface for the broker @@ -62,22 +38,20 @@ impl PolicyEngine { } /// Creates a policy engine with rights for the unauthenticated principal. - pub const fn with_unauthenticated_rights(unauthenticated: PrincipalRights) -> Self { + pub const fn with_unauthenticated_rights(unauthenticated: ObjectRights) -> Self { Self::new(PolicyProfile::Static { unauthenticated }) } pub(crate) fn principal_object_rights( &self, caller_credential: CallerCredential, - object_kind: ObjectKind, ) -> Result { - let principal_rights = match (self.profile, caller_credential) { + let rights = match (self.profile, caller_credential) { (PolicyProfile::Static { unauthenticated }, CallerCredential::Unauthenticated) => { unauthenticated } (PolicyProfile::DefaultDeny, _) => return Err(BrokerError::PolicyDenied), }; - let rights = principal_rights.object_rights(object_kind); if rights.is_empty() { return Err(BrokerError::PolicyDenied); } @@ -97,34 +71,30 @@ mod tests { #[test] fn static_policy_allows_configured_principal_rights() { - let policy = PolicyEngine::with_unauthenticated_rights(PrincipalRights::all()); + let policy = PolicyEngine::with_unauthenticated_rights(ObjectRights::all()); assert_eq!( - policy.principal_object_rights(CallerCredential::Unauthenticated, ObjectKind::Event), + policy.principal_object_rights(CallerCredential::Unauthenticated), Ok(ObjectRights::WAIT | ObjectRights::WRITE) ); } #[test] fn static_policy_returns_configured_principal_rights() { - let policy = PolicyEngine::with_unauthenticated_rights(PrincipalRights { - event: ObjectRights::WAIT, - }); + let policy = PolicyEngine::with_unauthenticated_rights(ObjectRights::WAIT); assert_eq!( - policy.principal_object_rights(CallerCredential::Unauthenticated, ObjectKind::Event), + policy.principal_object_rights(CallerCredential::Unauthenticated), Ok(ObjectRights::WAIT) ); } #[test] fn empty_principal_rights_deny_object_authorization() { - let policy = PolicyEngine::with_unauthenticated_rights(PrincipalRights { - event: ObjectRights::empty(), - }); + let policy = PolicyEngine::with_unauthenticated_rights(ObjectRights::empty()); assert_eq!( - policy.principal_object_rights(CallerCredential::Unauthenticated, ObjectKind::Event), + policy.principal_object_rights(CallerCredential::Unauthenticated), Err(BrokerError::PolicyDenied) ); } diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index c4e15396b3..12d189759b 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -48,19 +48,6 @@ pub(crate) enum ObjectEntry { Event(EventObject), } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum ObjectKind { - Event, -} - -impl ObjectEntry { - fn kind(self) -> ObjectKind { - match self { - Self::Event(_) => ObjectKind::Event, - } - } -} - /// Broker-owned authority token for one authenticated caller session. /// /// User mode does not choose this value. The broker entry layer authenticates @@ -92,7 +79,7 @@ impl BrokerSession { let rights = self .core .policy - .principal_object_rights(self.caller_credential, object.kind())?; + .principal_object_rights(self.caller_credential)?; let mut references = self.core.references.write(); if references.len() >= self.core.limits.max_references { return Err(BrokerError::ResourceExhausted); @@ -178,7 +165,7 @@ impl Drop for BrokerSession { #[cfg(test)] mod tests { use crate::{ - BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine, PrincipalRights, + BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, ObjectRights, PolicyEngine, }; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::event::{EventConsumeMode, EventConsumption, ReadinessState}; @@ -186,7 +173,7 @@ mod tests { #[test] fn object_reference_lifecycle_uses_public_core_constructor_once() { let broker = BrokerCore::new_with_limits( - PolicyEngine::with_unauthenticated_rights(PrincipalRights::all()), + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), BrokerCoreLimits::new(1), ) .unwrap(); diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 905745bb5c..2ed201a4c1 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -170,7 +170,7 @@ pub enum ConnectionTermination { #[cfg(test)] mod tests { use super::*; - use litebox_broker_core::{PolicyEngine, PrincipalRights}; + use litebox_broker_core::{ObjectRights, PolicyEngine}; use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, CreateEventRequest, EventConsumeMode, WaitEventRequest, @@ -181,7 +181,7 @@ mod tests { #[test] fn host_request_handling_uses_one_broker_core() { let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( - PrincipalRights::all(), + ObjectRights::all(), )) .unwrap(); diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 5363b308bf..cfd0cebb36 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -8,7 +8,7 @@ use std::path::PathBuf; use std::process::Command; use clap::Parser; -use litebox_broker_core::{BrokerCore, PolicyEngine, PrincipalRights}; +use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::serve_connection; use litebox_broker_transport::unix_socket::{ UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, @@ -34,7 +34,7 @@ fn main() -> Result<(), Box> { let control_listener = UnixListener::bind(&control_socket_path)?; let notification_listener = UnixListener::bind(¬ification_socket_path)?; let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( - PrincipalRights::all(), + ObjectRights::all(), ))?; let mut runner_command = Command::new(&args.runner); diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index 9fd6fb3aa3..3f669f59a0 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -3,7 +3,7 @@ use std::os::unix::net::UnixStream; -use litebox_broker_core::{BrokerCore, PolicyEngine, PrincipalRights}; +use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{ConnectionTermination, serve_connection}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::event::ReadinessState; @@ -14,7 +14,7 @@ use litebox_broker_transport::unix_socket::{ #[test] fn host_serves_control_requests_over_paired_userland_channels() { let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( - PrincipalRights::all(), + ObjectRights::all(), )) .unwrap(); let (local_control, host_control) = UnixStream::pair().unwrap(); diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 737305ba32..895dcf22bc 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -479,7 +479,7 @@ console.log(content); &control_socket_path, ¬ification_socket_path, litebox_broker_core::PolicyEngine::with_unauthenticated_rights( - litebox_broker_core::PrincipalRights::all(), + litebox_broker_core::ObjectRights::all(), ), 3, ); From 585cbe7030adcf4d6f2e4062d7169eae5160808e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 18 Jul 2026 09:01:16 -0700 Subject: [PATCH 102/319] Unify broker readiness representation (#1042) This PR gives broker-backed objects one shared readiness flag representation across control responses and asynchronous notifications. Existing event counters are migrated to the object-neutral format. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 76 +++++----- litebox/src/event/counter.rs | 38 ++--- litebox/src/litebox.rs | 2 +- litebox_broker_core/src/event.rs | 35 ++--- litebox_broker_core/src/session.rs | 18 +-- litebox_broker_host/src/lib.rs | 11 +- litebox_broker_local/src/event.rs | 7 +- litebox_broker_local/src/lib.rs | 12 +- litebox_broker_protocol/src/event.rs | 16 +-- litebox_broker_protocol/src/lib.rs | 1 + litebox_broker_protocol/src/message.rs | 15 +- litebox_broker_protocol/src/readiness.rs | 28 ++++ litebox_broker_protocol/src/wire.rs | 131 +++++++----------- litebox_broker_protocol/src/wire/event.rs | 28 ++-- litebox_broker_protocol/src/wire/primitive.rs | 21 ++- litebox_broker_transport/src/unix_socket.rs | 9 +- .../tests/notification_runtime.rs | 7 +- .../tests/userland_broker.rs | 20 +-- 18 files changed, 201 insertions(+), 274 deletions(-) create mode 100644 litebox_broker_protocol/src/readiness.rs diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 66468e8c33..5334ae9587 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -10,7 +10,8 @@ use hashbrown::HashMap; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; -use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode, ReadinessState}; +use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode}; +use litebox_broker_protocol::readiness::ReadinessFlags; use crate::event::{Events, polling::Pollee}; use crate::platform::TimeProvider; @@ -37,13 +38,13 @@ pub(crate) trait BrokerControl: Send + Sync { fn wait_event( &self, handle: ObjectHandle, - ) -> core::result::Result; + ) -> core::result::Result; fn add_event( &self, handle: ObjectHandle, value: u64, - ) -> core::result::Result; + ) -> core::result::Result; fn consume_event( &self, @@ -77,35 +78,38 @@ impl BrokerHandleRegistry { let mut handles = self.handles.lock(); if let Some(entry) = handles.get_mut(&handle) { entry.unregister_pollable(pollee); - if entry.is_empty() { + if entry.pollables.is_empty() { handles.remove(&handle); } } } - pub(crate) fn notify_readiness(&self, handle: ObjectHandle, readiness: ReadinessState) + pub(crate) fn notify_readiness(&self, handle: ObjectHandle, readiness: ReadinessFlags) where Platform: TimeProvider, { - let mut handles = self.handles.lock(); - let Some(entry) = handles.get_mut(&handle) else { - return; - }; - entry.prune_stale_pollables(); - if entry.is_empty() { - handles.remove(&handle); + let events = readiness_events(readiness); + if events.is_empty() { return; } - - let mut events = Events::empty(); - if readiness.read_ready { - events |= Events::IN; - } - if readiness.write_ready { - events |= Events::OUT; - } - if !events.is_empty() { - entry.notify_pollables(events); + let pollables = { + let mut handles = self.handles.lock(); + let Some(entry) = handles.get_mut(&handle) else { + return; + }; + entry.prune_stale_pollables(); + let pollables = entry + .pollables + .iter() + .filter_map(Weak::upgrade) + .collect::>(); + if entry.pollables.is_empty() { + handles.remove(&handle); + } + pollables + }; + for pollee in pollables { + pollee.notify_observers(events); } } } @@ -137,21 +141,6 @@ impl BrokerHandleEntry { self.pollables .retain(|registered| registered.strong_count() > 0); } - - fn notify_pollables(&self, events: Events) - where - Platform: TimeProvider, - { - for registered in &self.pollables { - if let Some(pollee) = registered.upgrade() { - pollee.notify_observers(events); - } - } - } - - fn is_empty(&self) -> bool { - self.pollables.is_empty() - } } pub(crate) struct BrokerLocalControl< Platform: RawSyncPrimitivesProvider, @@ -187,7 +176,7 @@ where fn wait_event( &self, handle: ObjectHandle, - ) -> core::result::Result { + ) -> core::result::Result { Ok(self.local.lock().wait_event(handle)?) } @@ -195,7 +184,7 @@ where &self, handle: ObjectHandle, value: u64, - ) -> core::result::Result { + ) -> core::result::Result { Ok(self.local.lock().add_event(handle, value)?) } @@ -211,3 +200,12 @@ where Ok(self.local.lock().close_object(handle)?) } } + +pub(crate) fn readiness_events(readiness: ReadinessFlags) -> Events { + let mut events = Events::empty(); + events.set(Events::IN, readiness.0 & ReadinessFlags::READ.0 != 0); + events.set(Events::OUT, readiness.0 & ReadinessFlags::WRITE.0 != 0); + events.set(Events::HUP, readiness.0 & ReadinessFlags::HANGUP.0 != 0); + events.set(Events::ERR, readiness.0 & ReadinessFlags::ERROR.0 != 0); + events +} diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 60ba5787ba..b346e1d36d 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -4,8 +4,9 @@ use alloc::sync::Arc; use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::event::ConsumeEventResponse; pub use litebox_broker_protocol::event::EventConsumeMode as EventCounterReadMode; -use litebox_broker_protocol::event::{ConsumeEventResponse, ReadinessState}; +use litebox_broker_protocol::readiness::ReadinessFlags; use thiserror::Error; use crate::{ @@ -13,6 +14,7 @@ use crate::{ broker::{ BrokerControl, BrokerHandleRegistry, error::{BrokerControlError, BrokerObjectError}, + readiness_events, }, event::{ Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, @@ -86,7 +88,7 @@ where ) -> Result> { self.pollee.wait(cx, nonblock, Events::IN, || { let response = self.consume(mode)?; - if response.readiness.write_ready { + if response.readiness.0 & ReadinessFlags::WRITE.0 != 0 { self.pollee.notify_observers(Events::OUT); } Ok(response.value) @@ -105,7 +107,7 @@ where } self.pollee.wait(cx, nonblock, Events::OUT, || { let readiness = self.add(value)?; - if value != 0 && readiness.read_ready { + if value != 0 && readiness.0 & ReadinessFlags::READ.0 != 0 { self.pollee.notify_observers(Events::IN); } Ok(core::mem::size_of::()) @@ -121,7 +123,7 @@ where .map_err(|error| self.broker_request_error(error)) } - fn add(&self, value: u64) -> Result { + fn add(&self, value: u64) -> Result { self.broker .add_event(self.handle, value) .map_err(|error| self.broker_request_error(error)) @@ -164,14 +166,7 @@ where Err(BrokerObjectError::WouldBlock) => return Events::empty(), Err(_) => return Events::ERR, }; - let mut events = Events::empty(); - if readiness.read_ready { - events |= Events::IN; - } - if readiness.write_ready { - events |= Events::OUT; - } - events + readiness_events(readiness) } } @@ -185,11 +180,12 @@ mod tests { use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::error::ErrorCode; - use litebox_broker_protocol::event::{CreateEventResponse, EventConsumption, ReadinessState}; + use litebox_broker_protocol::event::{CreateEventResponse, EventConsumption}; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, - BrokerResponse, EventReadinessNotification, EventRequest, EventResponse, + BrokerResponse, EventRequest, EventResponse, ReadinessNotification, }; + use litebox_broker_protocol::readiness::ReadinessFlags; use super::*; use crate::LiteBox; @@ -234,13 +230,10 @@ mod tests { std::thread::yield_now(); } read_ready.store(true, Ordering::SeqCst); - litebox.dispatch_broker_notification(BrokerNotification::EventReadiness( - EventReadinessNotification { + litebox.dispatch_broker_notification(BrokerNotification::Readiness( + ReadinessNotification { handle, - readiness: ReadinessState { - read_ready: true, - write_ready: true, - }, + readiness: ReadinessFlags::READ | ReadinessFlags::WRITE, }, )); @@ -300,10 +293,7 @@ mod tests { if self.read_ready.swap(false, Ordering::SeqCst) { BrokerResponse::Event(EventResponse::Consume(EventConsumption { value: 1, - readiness: ReadinessState { - read_ready: false, - write_ready: true, - }, + readiness: ReadinessFlags::WRITE, })) } else { BrokerResponse::Error(ErrorCode::WouldBlock) diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 5418917f43..637c6301b4 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -150,7 +150,7 @@ impl LiteBox { Platform: TimeProvider, { match notification { - BrokerNotification::EventReadiness(notification) => self + BrokerNotification::Readiness(notification) => self .x .broker_handles .notify_readiness(notification.handle, notification.readiness), diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index db72026e3a..62218d305e 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -6,7 +6,8 @@ use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result}; use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::event::{EventConsumeMode, EventConsumption, ReadinessState}; +use litebox_broker_protocol::event::{EventConsumeMode, EventConsumption}; +use litebox_broker_protocol::readiness::ReadinessFlags; pub(crate) const MAX_EVENT_COUNT: u64 = u64::MAX - 1; @@ -24,18 +25,15 @@ pub fn create(session: &BrokerSession, initial_count: u64) -> Result Result { +pub fn wait(session: &BrokerSession, handle: ObjectHandle) -> Result { let required_rights = ObjectRights::WAIT; session.with_authorized_object(handle, required_rights, |object| match object { - ObjectEntry::Event(event) => Ok(ReadinessState { - read_ready: event.count > 0, - write_ready: event.count < MAX_EVENT_COUNT, - }), + ObjectEntry::Event(event) => Ok(event.readiness()), }) } /// Adds readiness credits to a broker-owned event object. -pub fn add(session: &BrokerSession, handle: ObjectHandle, value: u64) -> Result { +pub fn add(session: &BrokerSession, handle: ObjectHandle, value: u64) -> Result { let required_rights = ObjectRights::WRITE; session.with_authorized_object_mut(handle, required_rights, |object| match object { ObjectEntry::Event(event) => event.add(value), @@ -64,16 +62,13 @@ impl EventObject { Self { count } } - fn add(&mut self, value: u64) -> Result { + fn add(&mut self, value: u64) -> Result { self.count = self .count .checked_add(value) .filter(|count| *count <= MAX_EVENT_COUNT) .ok_or(BrokerError::WouldBlock)?; - Ok(ReadinessState { - read_ready: self.count > 0, - write_ready: self.count < MAX_EVENT_COUNT, - }) + Ok(self.readiness()) } fn consume(&mut self, mode: EventConsumeMode) -> Result { @@ -88,10 +83,18 @@ impl EventObject { self.count -= value; Ok(EventConsumption { value, - readiness: ReadinessState { - read_ready: self.count > 0, - write_ready: self.count < MAX_EVENT_COUNT, - }, + readiness: self.readiness(), }) } + + fn readiness(self) -> ReadinessFlags { + let mut readiness = ReadinessFlags::default(); + if self.count > 0 { + readiness = readiness | ReadinessFlags::READ; + } + if self.count < MAX_EVENT_COUNT { + readiness = readiness | ReadinessFlags::WRITE; + } + readiness + } } diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index 12d189759b..cea80b3c81 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -168,7 +168,8 @@ mod tests { BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, ObjectRights, PolicyEngine, }; use litebox_broker_protocol::ObjectHandle; - use litebox_broker_protocol::event::{EventConsumeMode, EventConsumption, ReadinessState}; + use litebox_broker_protocol::event::{EventConsumeMode, EventConsumption}; + use litebox_broker_protocol::readiness::ReadinessFlags; #[test] fn object_reference_lifecycle_uses_public_core_constructor_once() { @@ -199,26 +200,17 @@ mod tests { assert_eq!( crate::event::wait(&session, handle), - Ok(ReadinessState { - read_ready: false, - write_ready: true, - }) + Ok(ReadinessFlags::WRITE) ); assert_eq!( crate::event::add(&session, handle, 1), - Ok(ReadinessState { - read_ready: true, - write_ready: true, - }) + Ok(ReadinessFlags::READ | ReadinessFlags::WRITE) ); assert_eq!( crate::event::consume(&session, handle, EventConsumeMode::One), Ok(EventConsumption { value: 1, - readiness: ReadinessState { - read_ready: false, - write_ready: true, - }, + readiness: ReadinessFlags::WRITE, }) ); assert_eq!( diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 2ed201a4c1..56ff111c8d 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -337,18 +337,13 @@ mod tests { &channel.responses[1..], [ BrokerResponse::Event(EventResponse::Add(AddEventResponse { - readiness: litebox_broker_protocol::event::ReadinessState { - read_ready: true, - write_ready: true, - }, + readiness: litebox_broker_protocol::readiness::ReadinessFlags::READ + | litebox_broker_protocol::readiness::ReadinessFlags::WRITE, })), BrokerResponse::Event(EventResponse::Consume( litebox_broker_protocol::event::ConsumeEventResponse { value: 1, - readiness: litebox_broker_protocol::event::ReadinessState { - read_ready: false, - write_ready: true, - }, + readiness: litebox_broker_protocol::readiness::ReadinessFlags::WRITE, } )), ] diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 829391965c..431cd4e17e 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -5,11 +5,12 @@ use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, - EventConsumeMode, ReadinessState, WaitEventRequest, + EventConsumeMode, WaitEventRequest, }; use litebox_broker_protocol::message::{ BrokerRequest, BrokerResponse, EventRequest, EventResponse, }; +use litebox_broker_protocol::readiness::ReadinessFlags; use crate::{BrokerLocal, BrokerLocalError, Result}; @@ -38,7 +39,7 @@ impl BrokerLocal { /// /// Panics if the broker reports an unrecoverable error or returns a protocol /// response that does not match the issued event request. - pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { + pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { let response = self.request_event(EventRequest::Wait(WaitEventRequest { handle }))?; match response { EventResponse::Wait(response) => Ok(response.readiness), @@ -56,7 +57,7 @@ impl BrokerLocal { &mut self, handle: ObjectHandle, value: u64, - ) -> Result { + ) -> Result { let response = self.request_event(EventRequest::Add(AddEventRequest { handle, value }))?; match response { EventResponse::Add(response) => Ok(response.readiness), diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 8c2ceecd5c..11f165f857 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -156,9 +156,8 @@ mod tests { use litebox_broker_protocol::ProtocolVersion; use litebox_broker_protocol::channel::LocalNotificationChannel; use litebox_broker_protocol::event::{CreateEventRequest, CreateEventResponse}; - use litebox_broker_protocol::message::{ - EventReadinessNotification, EventRequest, EventResponse, - }; + use litebox_broker_protocol::message::{EventRequest, EventResponse, ReadinessNotification}; + use litebox_broker_protocol::readiness::ReadinessFlags; #[test] fn negotiate_returns_active_local_connection() { @@ -248,12 +247,9 @@ mod tests { #[test] fn notification_receiver_returns_broker_notifications() { - let notification = BrokerNotification::EventReadiness(EventReadinessNotification { + let notification = BrokerNotification::Readiness(ReadinessNotification { handle: ObjectHandle(7), - readiness: litebox_broker_protocol::event::ReadinessState { - read_ready: true, - write_ready: false, - }, + readiness: ReadinessFlags::READ, }); let mut receiver = BrokerNotifications::new(FakeNotificationChannel { notification: Some(notification.clone()), diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs index 1cf506d661..b54df3eeff 100644 --- a/litebox_broker_protocol/src/event.rs +++ b/litebox_broker_protocol/src/event.rs @@ -2,15 +2,7 @@ // Licensed under the MIT license. use crate::ObjectHandle; - -/// Broker-authoritative readiness state for one object. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct ReadinessState { - /// Whether an event read/consume operation can complete without blocking. - pub read_ready: bool, - /// Whether an event write/add operation can complete without blocking. - pub write_ready: bool, -} +use crate::readiness::ReadinessFlags; /// How a broker event consume operation should remove readiness credits. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -46,7 +38,7 @@ pub struct WaitEventRequest { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WaitEventResponse { /// Current readiness state. - pub readiness: ReadinessState, + pub readiness: ReadinessFlags, } /// Request to add readiness credits to an event. @@ -62,7 +54,7 @@ pub struct AddEventRequest { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct AddEventResponse { /// Readiness state after adding credits. - pub readiness: ReadinessState, + pub readiness: ReadinessFlags, } /// Request to consume readiness credits from an event. @@ -80,7 +72,7 @@ pub struct EventConsumption { /// Number of readiness credits consumed. pub value: u64, /// Readiness state after consuming credits. - pub readiness: ReadinessState, + pub readiness: ReadinessFlags, } /// Response to an event consume request. diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index de47dd864d..b8c684a94a 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -16,6 +16,7 @@ pub mod channel; pub mod error; pub mod event; pub mod message; +pub mod readiness; pub mod wire; /// Opaque broker object reference handle. diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index bb51f177e1..b46c802999 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -4,8 +4,9 @@ use crate::error::ErrorCode; use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, - CreateEventRequest, CreateEventResponse, ReadinessState, WaitEventRequest, WaitEventResponse, + CreateEventRequest, CreateEventResponse, WaitEventRequest, WaitEventResponse, }; +use crate::readiness::ReadinessFlags; use crate::{ObjectHandle, ProtocolVersion}; /// Broker handshake request sent before the control channel is active. @@ -92,15 +93,15 @@ pub enum EventResponse { /// re-check authoritative state, not as ordered state transitions. #[derive(Clone, Debug, PartialEq, Eq)] pub enum BrokerNotification { - /// Readiness changed or should be re-checked for a broker-owned event object. - EventReadiness(EventReadinessNotification), + /// Readiness changed or should be re-checked for a broker-owned object. + Readiness(ReadinessNotification), } -/// Readiness notification for a broker-owned event object. +/// Readiness notification for a broker-owned object. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct EventReadinessNotification { - /// Event object handle. +pub struct ReadinessNotification { + /// Broker object handle. pub handle: ObjectHandle, /// Current broker-authoritative readiness snapshot. - pub readiness: ReadinessState, + pub readiness: ReadinessFlags, } diff --git a/litebox_broker_protocol/src/readiness.rs b/litebox_broker_protocol/src/readiness.rs new file mode 100644 index 0000000000..c552703e04 --- /dev/null +++ b/litebox_broker_protocol/src/readiness.rs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// ABI-neutral broker object readiness flags. +/// +/// Unknown bits are preserved so protocol peers can ignore readiness kinds they +/// do not understand. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReadinessFlags(pub u32); + +impl ReadinessFlags { + /// Data can be read without blocking. + pub const READ: Self = Self(1 << 0); + /// Data can be written without blocking. + pub const WRITE: Self = Self(1 << 1); + /// The peer closed its write side. + pub const HANGUP: Self = Self(1 << 2); + /// The object is in an error state. + pub const ERROR: Self = Self(1 << 3); +} + +impl core::ops::BitOr for ReadinessFlags { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index c9bb49a850..7efc2caabe 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -21,8 +21,9 @@ use thiserror::Error; use crate::error::ErrorCode; use crate::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, - BrokerResponse, EventReadinessNotification, + BrokerResponse, ReadinessNotification, }; +use crate::readiness::ReadinessFlags; use primitive::{Decoder, Encoder}; @@ -39,7 +40,7 @@ const RESPONSE_TAG_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; const RESPONSE_TAG_OBJECT_CLOSED: u8 = 4; -const NOTIFICATION_TAG_EVENT_READINESS: u8 = 0; +const NOTIFICATION_TAG_READINESS: u8 = 0; /// Error produced while encoding or decoding a broker wire message. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] @@ -49,8 +50,6 @@ pub enum WireError { TruncatedFrame, #[error("trailing broker wire bytes")] TrailingBytes, - #[error("invalid broker wire boolean")] - InvalidBoolean, #[error("invalid broker wire tag")] InvalidTag, #[error("broker wire message is not valid in this protocol phase")] @@ -218,10 +217,10 @@ pub fn decode_response(frame: &[u8]) -> Result { pub fn encode_notification(notification: BrokerNotification) -> Vec { let mut encoder = Encoder::default(); match notification { - BrokerNotification::EventReadiness(notification) => { - encoder.u8(NOTIFICATION_TAG_EVENT_READINESS); + BrokerNotification::Readiness(notification) => { + encoder.u8(NOTIFICATION_TAG_READINESS); encoder.handle(notification.handle); - event::encode_readiness(&mut encoder, notification.readiness); + encoder.u32(notification.readiness.0); } } encoder.finish() @@ -232,12 +231,10 @@ pub fn decode_notification(frame: &[u8]) -> Result { - BrokerNotification::EventReadiness(EventReadinessNotification { - handle: decoder.handle()?, - readiness: event::decode_readiness(&mut decoder)?, - }) - } + NOTIFICATION_TAG_READINESS => BrokerNotification::Readiness(ReadinessNotification { + handle: decoder.handle()?, + readiness: ReadinessFlags(decoder.u32()?), + }), _ => return Err(WireError::InvalidTag), }; decoder.finish()?; @@ -249,7 +246,7 @@ mod tests { use super::*; use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, - CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, WaitEventRequest, + CreateEventResponse, EventConsumeMode, EventConsumption, WaitEventRequest, WaitEventResponse, }; use crate::message::{EventRequest, EventResponse}; @@ -328,29 +325,17 @@ mod tests { BrokerResponse::ObjectClosed, BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })), BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { - readiness: ReadinessState { - read_ready: true, - write_ready: false, - }, + readiness: ReadinessFlags::READ, })), BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { - readiness: ReadinessState { - read_ready: false, - write_ready: true, - }, + readiness: ReadinessFlags::WRITE, })), BrokerResponse::Event(EventResponse::Add(AddEventResponse { - readiness: ReadinessState { - read_ready: true, - write_ready: true, - }, + readiness: ReadinessFlags::READ | ReadinessFlags::WRITE, })), BrokerResponse::Event(EventResponse::Consume(EventConsumption { value: 3, - readiness: ReadinessState { - read_ready: false, - write_ready: true, - }, + readiness: ReadinessFlags::WRITE, })), BrokerResponse::Error(ErrorCode::PolicyDenied), BrokerResponse::Error(ErrorCode::WouldBlock), @@ -368,15 +353,10 @@ mod tests { #[test] fn notification_codec_round_trips_all_variants() { let handle = ObjectHandle(13); - let notifications = [BrokerNotification::EventReadiness( - EventReadinessNotification { - handle, - readiness: ReadinessState { - read_ready: true, - write_ready: false, - }, - }, - )]; + let notifications = [BrokerNotification::Readiness(ReadinessNotification { + handle, + readiness: ReadinessFlags::READ | ReadinessFlags::HANGUP, + })]; for notification in notifications { assert_eq!( @@ -498,22 +478,21 @@ mod tests { ); assert_eq!( decode_response(&[1, 1, 0xff]), - Err(WireError::InvalidBoolean) + Err(WireError::TruncatedFrame) ); assert_eq!( decode_response(&[2, 0xff, 0xff]), Err(WireError::InvalidTag) ); - let mut invalid_bool = [1, 2, 2, 0]; - assert_eq!( - decode_response(&invalid_bool), - Err(WireError::InvalidBoolean) - ); + let truncated = [1, 2, 2, 0]; + assert_eq!(decode_response(&truncated), Err(WireError::TruncatedFrame)); - invalid_bool[2] = 1; - invalid_bool[3] = 1; - let mut frame = invalid_bool.to_vec(); + let mut frame = encode_response(BrokerResponse::Event(EventResponse::Add( + AddEventResponse { + readiness: ReadinessFlags::READ | ReadinessFlags::WRITE, + }, + ))); frame.push(0xff); assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); } @@ -525,34 +504,26 @@ mod tests { Err(WireError::InvalidTag) ); assert_eq!( - decode_notification(&[NOTIFICATION_TAG_EVENT_READINESS]), + decode_notification(&[NOTIFICATION_TAG_READINESS]), Err(WireError::TruncatedFrame) ); - let mut invalid_bool = encode_notification(BrokerNotification::EventReadiness( - EventReadinessNotification { + let mut truncated = + encode_notification(BrokerNotification::Readiness(ReadinessNotification { handle: ObjectHandle(13), - readiness: ReadinessState { - read_ready: true, - write_ready: false, - }, - }, - )); - *invalid_bool.last_mut().unwrap() = 0xff; + readiness: ReadinessFlags::READ, + })); + truncated.pop(); assert_eq!( - decode_notification(&invalid_bool), - Err(WireError::InvalidBoolean) + decode_notification(&truncated), + Err(WireError::TruncatedFrame) ); - let mut trailing = encode_notification(BrokerNotification::EventReadiness( - EventReadinessNotification { + let mut trailing = + encode_notification(BrokerNotification::Readiness(ReadinessNotification { handle: ObjectHandle(13), - readiness: ReadinessState { - read_ready: true, - write_ready: false, - }, - }, - )); + readiness: ReadinessFlags::READ, + })); trailing.push(0xff); assert_eq!( decode_notification(&trailing), @@ -565,29 +536,21 @@ mod tests { assert_eq!( encode_response(BrokerResponse::Event(EventResponse::Add( AddEventResponse { - readiness: ReadinessState { - read_ready: true, - write_ready: false, - }, + readiness: ReadinessFlags::READ, } ))), - [1, 2, 1, 0] + [1, 2, 1, 0, 0, 0] ); } #[test] - fn event_readiness_notification_wire_shape_is_pinned() { + fn readiness_notification_wire_shape_is_pinned() { assert_eq!( - encode_notification(BrokerNotification::EventReadiness( - EventReadinessNotification { - handle: ObjectHandle(13), - readiness: ReadinessState { - read_ready: true, - write_ready: false, - }, - } - )), - [0, 13, 0, 0, 0, 0, 0, 0, 0, 1, 0] + encode_notification(BrokerNotification::Readiness(ReadinessNotification { + handle: ObjectHandle(13), + readiness: ReadinessFlags::READ | ReadinessFlags::HANGUP, + })), + [0, 13, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0] ); } } diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index 0810b03b76..6010a81b0e 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -3,10 +3,10 @@ use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, - CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, WaitEventRequest, - WaitEventResponse, + CreateEventResponse, EventConsumeMode, EventConsumption, WaitEventRequest, WaitEventResponse, }; use crate::message::{EventRequest, EventResponse}; +use crate::readiness::ReadinessFlags; use super::WireError; use super::primitive::{Decoder, Encoder}; @@ -79,16 +79,16 @@ pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventRespon } EventResponse::Wait(response) => { encoder.u8(EVENT_RESPONSE_TAG_WAITED); - encode_readiness(encoder, response.readiness); + encoder.u32(response.readiness.0); } EventResponse::Add(response) => { encoder.u8(EVENT_RESPONSE_TAG_ADDED); - encode_readiness(encoder, response.readiness); + encoder.u32(response.readiness.0); } EventResponse::Consume(response) => { encoder.u8(EVENT_RESPONSE_TAG_CONSUMED); encoder.u64(response.value); - encode_readiness(encoder, response.readiness); + encoder.u32(response.readiness.0); } } } @@ -99,14 +99,14 @@ pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result EventResponse::Wait(WaitEventResponse { - readiness: decode_readiness(decoder)?, + readiness: ReadinessFlags(decoder.u32()?), }), EVENT_RESPONSE_TAG_ADDED => EventResponse::Add(AddEventResponse { - readiness: decode_readiness(decoder)?, + readiness: ReadinessFlags(decoder.u32()?), }), EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(EventConsumption { value: decoder.u64()?, - readiness: decode_readiness(decoder)?, + readiness: ReadinessFlags(decoder.u32()?), }), _ => return Err(WireError::InvalidTag), }; @@ -114,18 +114,6 @@ pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result) -> Result { - Ok(ReadinessState { - read_ready: decoder.bool()?, - write_ready: decoder.bool()?, - }) -} - fn encode_consume_mode(encoder: &mut Encoder, mode: EventConsumeMode) { match mode { EventConsumeMode::All => { diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs index b038804306..617f411f7f 100644 --- a/litebox_broker_protocol/src/wire/primitive.rs +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -17,10 +17,6 @@ impl Encoder { self.bytes } - pub(super) fn bool(&mut self, value: bool) { - self.u8(u8::from(value)); - } - pub(super) fn u8(&mut self, value: u8) { self.bytes.push(value); } @@ -29,6 +25,10 @@ impl Encoder { self.bytes.extend_from_slice(&value.to_le_bytes()); } + pub(super) fn u32(&mut self, value: u32) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + pub(super) fn u64(&mut self, value: u64) { self.bytes.extend_from_slice(&value.to_le_bytes()); } @@ -60,14 +60,6 @@ impl<'a> Decoder<'a> { } } - pub(super) fn bool(&mut self) -> Result { - match self.u8()? { - 0 => Ok(false), - 1 => Ok(true), - _ => Err(WireError::InvalidBoolean), - } - } - pub(super) fn u8(&mut self) -> Result { let bytes = self.take(1)?; Ok(bytes[0]) @@ -78,6 +70,11 @@ impl<'a> Decoder<'a> { Ok(u16::from_le_bytes([bytes[0], bytes[1]])) } + pub(super) fn u32(&mut self) -> Result { + let bytes = self.take(4)?; + Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) + } + pub(super) fn u64(&mut self) -> Result { let bytes = self.take(8)?; Ok(u64::from_le_bytes([ diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index d02fe784d5..1b5f956a39 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -443,13 +443,10 @@ mod tests { let (local_stream, host_stream) = UnixStream::pair().unwrap(); let mut local = UnixStreamLocalNotificationChannel::from_connected(local_stream); let mut host = UnixStreamHostNotificationChannel::from_accepted(host_stream); - let notification = BrokerNotification::EventReadiness( - litebox_broker_protocol::message::EventReadinessNotification { + let notification = BrokerNotification::Readiness( + litebox_broker_protocol::message::ReadinessNotification { handle: litebox_broker_protocol::ObjectHandle(7), - readiness: litebox_broker_protocol::event::ReadinessState { - read_ready: true, - write_ready: false, - }, + readiness: litebox_broker_protocol::readiness::ReadinessFlags::READ, }, ); diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index 3f669f59a0..37b309e2de 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -6,7 +6,7 @@ use std::os::unix::net::UnixStream; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{ConnectionTermination, serve_connection}; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::event::ReadinessState; +use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_transport::unix_socket::{ UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, UnixStreamLocalControlChannel, }; @@ -31,10 +31,7 @@ fn host_serves_control_requests_over_paired_userland_channels() { .unwrap(); let handle = local.create_event_with_count(0).unwrap(); - let readiness = ReadinessState { - read_ready: true, - write_ready: true, - }; + let readiness = ReadinessFlags::READ | ReadinessFlags::WRITE; assert_eq!(local.add_event(handle, 1).unwrap(), readiness); drop(local); diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index ee126be51b..9c5b9492e1 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -9,7 +9,7 @@ use std::process::{Child, Command}; use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::event::ReadinessState; +use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, }; @@ -84,26 +84,14 @@ fn run_fake_runner(args: &[OsString]) { let mut local = BrokerLocal::negotiate(control_channel).unwrap(); let handle = local.create_event_with_count(0).unwrap(); - assert_eq!( - local.wait_event(handle).unwrap(), - ReadinessState { - read_ready: false, - write_ready: true, - } - ); + assert_eq!(local.wait_event(handle).unwrap(), ReadinessFlags::WRITE); - let readiness = ReadinessState { - read_ready: true, - write_ready: true, - }; + let readiness = ReadinessFlags::READ | ReadinessFlags::WRITE; assert_eq!(local.add_event(handle, 1).unwrap(), readiness); assert_eq!( local.wait_event(handle).unwrap(), - ReadinessState { - read_ready: true, - write_ready: true, - } + ReadinessFlags::READ | ReadinessFlags::WRITE ); drop(local); From 1b76b096397a3e1f80c19cea89406a77b6a0f367 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 18 Jul 2026 13:48:32 -0700 Subject: [PATCH 103/319] Handle broker connection loss (#1043) This PR handles broker connection loss by invalidating the local control channel and waking affected waiters with an error. Notification-channel termination cancels pending control I/O so teardown cannot hang. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/mod.rs | 156 +++++++++------- litebox/src/event/counter.rs | 186 ++++++++++++++++++-- litebox/src/litebox.rs | 51 ++++-- litebox_broker_transport/src/unix_socket.rs | 70 ++++++++ litebox_runner_linux_userland/src/broker.rs | 32 +++- litebox_runner_linux_userland/src/lib.rs | 4 +- 6 files changed, 390 insertions(+), 109 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 5334ae9587..42d040bad7 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -53,35 +53,31 @@ pub(crate) trait BrokerControl: Send + Sync { ) -> core::result::Result; fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError>; + + fn fail_connection(&self); } -pub(crate) struct BrokerHandleRegistry { - handles: Mutex>>, +pub(crate) struct BrokerPollableRegistry { + pollables: Mutex>>>, } -impl BrokerHandleRegistry { +impl BrokerPollableRegistry { pub(crate) fn new() -> Self { Self { - handles: Mutex::new(HashMap::new()), + pollables: Mutex::new(HashMap::new()), } } pub(crate) fn register_pollable(&self, handle: ObjectHandle, pollee: &Arc>) { - self.handles - .lock() - .entry(handle) - .or_insert_with(BrokerHandleEntry::new) - .register_pollable(pollee); + let previous = self.pollables.lock().insert(handle, Arc::downgrade(pollee)); + assert!( + previous.is_none(), + "broker handle already has a registered pollable" + ); } - pub(crate) fn unregister_pollable(&self, handle: ObjectHandle, pollee: &Arc>) { - let mut handles = self.handles.lock(); - if let Some(entry) = handles.get_mut(&handle) { - entry.unregister_pollable(pollee); - if entry.pollables.is_empty() { - handles.remove(&handle); - } - } + pub(crate) fn unregister_pollable(&self, handle: ObjectHandle) { + self.pollables.lock().remove(&handle); } pub(crate) fn notify_readiness(&self, handle: ObjectHandle, readiness: ReadinessFlags) @@ -92,92 +88,108 @@ impl BrokerHandleRegistry { if events.is_empty() { return; } - let pollables = { - let mut handles = self.handles.lock(); - let Some(entry) = handles.get_mut(&handle) else { - return; - }; - entry.prune_stale_pollables(); - let pollables = entry - .pollables - .iter() - .filter_map(Weak::upgrade) - .collect::>(); - if entry.pollables.is_empty() { - handles.remove(&handle); + let pollee = { + let mut pollables = self.pollables.lock(); + let pollee = pollables.get(&handle).and_then(Weak::upgrade); + if pollee.is_none() { + pollables.remove(&handle); } - pollables + pollee }; - for pollee in pollables { + if let Some(pollee) = pollee { pollee.notify_observers(events); } } -} - -struct BrokerHandleEntry { - pollables: Vec>>, -} -impl BrokerHandleEntry { - fn new() -> Self { - Self { - pollables: Vec::new(), + fn notify_all(&self, events: Events) + where + Platform: TimeProvider, + { + let pollables = { + let mut pollables = Vec::new(); + self.pollables.lock().retain(|_, registered| { + let Some(pollee) = registered.upgrade() else { + return false; + }; + pollables.push(pollee); + true + }); + pollables + }; + for pollee in pollables { + pollee.notify_observers(events); } } - - fn register_pollable(&mut self, pollee: &Arc>) { - self.pollables.push(Arc::downgrade(pollee)); - } - - fn unregister_pollable(&mut self, pollee: &Arc>) { - self.pollables.retain(|registered| { - registered - .upgrade() - .is_some_and(|registered| !Arc::ptr_eq(®istered, pollee)) - }); - } - - fn prune_stale_pollables(&mut self) { - self.pollables - .retain(|registered| registered.strong_count() > 0); - } } + pub(crate) struct BrokerLocalControl< Platform: RawSyncPrimitivesProvider, Channel: LocalControlChannel + Send, > { - local: Mutex>, + local: Mutex>>, + pollable_registry: Arc>, } impl BrokerLocalControl where - Platform: RawSyncPrimitivesProvider, + Platform: RawSyncPrimitivesProvider + TimeProvider, Channel: LocalControlChannel + Send, { - pub(crate) const fn new(local: BrokerLocal) -> Self { + pub(crate) fn new( + local: BrokerLocal, + pollable_registry: Arc>, + ) -> Self { Self { - local: Mutex::new(local), + local: Mutex::new(Some(local)), + pollable_registry, } } + + fn request( + &self, + request: impl FnOnce( + &mut BrokerLocal, + ) -> litebox_broker_local::Result, + ) -> core::result::Result { + let (result, failed_connection) = { + let mut local = self.local.lock(); + let Some(connection) = local.as_mut() else { + return Err(BrokerControlError::Transport); + }; + let result = request(connection).map_err(BrokerControlError::from); + let failed_connection = if matches!(result.as_ref(), Err(BrokerControlError::Transport)) + { + local.take() + } else { + None + }; + (result, failed_connection) + }; + if let Some(connection) = failed_connection { + drop(connection); + self.pollable_registry.notify_all(Events::ERR); + } + result + } } impl BrokerControl for BrokerLocalControl where - Platform: RawSyncPrimitivesProvider, + Platform: RawSyncPrimitivesProvider + TimeProvider, Channel: LocalControlChannel + Send, { fn create_event_with_count( &self, initial_count: u64, ) -> core::result::Result { - Ok(self.local.lock().create_event_with_count(initial_count)?) + self.request(|local| local.create_event_with_count(initial_count)) } fn wait_event( &self, handle: ObjectHandle, ) -> core::result::Result { - Ok(self.local.lock().wait_event(handle)?) + self.request(|local| local.wait_event(handle)) } fn add_event( @@ -185,7 +197,7 @@ where handle: ObjectHandle, value: u64, ) -> core::result::Result { - Ok(self.local.lock().add_event(handle, value)?) + self.request(|local| local.add_event(handle, value)) } fn consume_event( @@ -193,11 +205,19 @@ where handle: ObjectHandle, mode: EventConsumeMode, ) -> core::result::Result { - Ok(self.local.lock().consume_event(handle, mode)?) + self.request(|local| local.consume_event(handle, mode)) } fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError> { - Ok(self.local.lock().close_object(handle)?) + self.request(|local| local.close_object(handle)) + } + + fn fail_connection(&self) { + let connection = self.local.lock().take(); + if let Some(connection) = connection { + drop(connection); + self.pollable_registry.notify_all(Events::ERR); + } } } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index b346e1d36d..4ebd50e604 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -12,7 +12,7 @@ use thiserror::Error; use crate::{ LiteBox, broker::{ - BrokerControl, BrokerHandleRegistry, + BrokerControl, BrokerPollableRegistry, error::{BrokerControlError, BrokerObjectError}, readiness_events, }, @@ -46,7 +46,7 @@ pub enum EventCounterError { pub struct EventCounter { broker: Arc, handle: ObjectHandle, - registry: Arc>, + pollable_registry: Arc>, pollee: Arc>, } @@ -68,13 +68,13 @@ where .create_event_with_count(initial_count) .map_err(BrokerObjectError::from) .map_err(EventCounterError::from)?; - let registry = litebox.broker_handle_registry(); + let pollable_registry = litebox.broker_pollable_registry(); let pollee = Arc::new(Pollee::new()); - registry.register_pollable(handle, &pollee); + pollable_registry.register_pollable(handle, &pollee); Ok(Self { broker, handle, - registry, + pollable_registry, pollee, }) } @@ -143,7 +143,7 @@ where Platform: RawSyncPrimitivesProvider + TimeProvider, { fn drop(&mut self) { - self.registry.unregister_pollable(self.handle, &self.pollee); + self.pollable_registry.unregister_pollable(self.handle); let _ = self.broker.close_object(self.handle); } } @@ -200,10 +200,13 @@ mod tests { let handle = ObjectHandle(7); let consume_attempts = Arc::new(AtomicUsize::new(0)); let read_ready = Arc::new(AtomicBool::new(false)); + let request_count = Arc::new(AtomicUsize::new(0)); let local = BrokerLocal::negotiate(FakeLocalControlChannel { - handle, + next_handle: handle.0, consume_attempts: consume_attempts.clone(), read_ready: read_ready.clone(), + request_count, + fail_requests: Arc::new(AtomicBool::new(false)), last_request: None, }) .unwrap(); @@ -246,15 +249,159 @@ mod tests { ); } + #[test] + fn broker_association_failure_wakes_blocked_read() { + use std::time::{Duration, Instant}; + + let platform = MockPlatform::new(); + let handle = ObjectHandle(7); + let consume_attempts = Arc::new(AtomicUsize::new(0)); + let request_count = Arc::new(AtomicUsize::new(0)); + let local = BrokerLocal::negotiate(FakeLocalControlChannel { + next_handle: handle.0, + consume_attempts: Arc::clone(&consume_attempts), + read_ready: Arc::new(AtomicBool::new(false)), + request_count: Arc::clone(&request_count), + fail_requests: Arc::new(AtomicBool::new(false)), + last_request: None, + }) + .unwrap(); + let litebox = Arc::new(LiteBox::new_with_broker_local(platform, local)); + let counter = Arc::new(EventCounter::new(&litebox, 0).unwrap()); + + let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); + let read_counter = Arc::clone(&counter); + let reader = std::thread::spawn(move || { + result_sender + .send(read_counter.read( + &WaitState::new(platform).context(), + false, + EventCounterReadMode::One, + )) + .unwrap(); + }); + let deadline = Instant::now() + Duration::from_secs(1); + while consume_attempts.load(Ordering::SeqCst) < 2 { + assert!(Instant::now() < deadline); + std::thread::yield_now(); + } + + litebox.broker_failure_dispatcher()(); + + assert!(matches!( + result_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + Err(TryOpError::Other(EventCounterError::Io)) + )); + reader.join().unwrap(); + assert_eq!(request_count.load(Ordering::SeqCst), 3); + assert_eq!(counter.check_io_events(), Events::ERR); + assert_eq!(request_count.load(Ordering::SeqCst), 3); + } + + #[test] + fn control_transport_failure_notifies_all_event_counters() { + let platform = MockPlatform::new(); + let handle = ObjectHandle(7); + let request_count = Arc::new(AtomicUsize::new(0)); + let fail_requests = Arc::new(AtomicBool::new(false)); + let local = BrokerLocal::negotiate(FakeLocalControlChannel { + next_handle: handle.0, + consume_attempts: Arc::new(AtomicUsize::new(0)), + read_ready: Arc::new(AtomicBool::new(false)), + request_count: Arc::clone(&request_count), + fail_requests: Arc::clone(&fail_requests), + last_request: None, + }) + .unwrap(); + let litebox = LiteBox::new_with_broker_local(platform, local); + let first = EventCounter::new(&litebox, 0).unwrap(); + let second = EventCounter::new(&litebox, 0).unwrap(); + let first_observer = Arc::new(ErrorObserver(AtomicBool::new(false))); + let first_observer_dyn: Arc> = first_observer.clone(); + first.register_observer(Arc::downgrade(&first_observer_dyn), Events::ERR); + let second_observer = Arc::new(ErrorObserver(AtomicBool::new(false))); + let second_observer_dyn: Arc> = second_observer.clone(); + second.register_observer(Arc::downgrade(&second_observer_dyn), Events::ERR); + + fail_requests.store(true, Ordering::SeqCst); + + assert_eq!(first.check_io_events(), Events::ERR); + assert!(first_observer.0.load(Ordering::SeqCst)); + assert!(second_observer.0.load(Ordering::SeqCst)); + assert_eq!(request_count.load(Ordering::SeqCst), 3); + assert_eq!(second.check_io_events(), Events::ERR); + assert_eq!(request_count.load(Ordering::SeqCst), 3); + } + + #[test] + fn broker_dispatchers_follow_objects_that_outlive_litebox() { + let platform = MockPlatform::new(); + let handle = ObjectHandle(7); + let request_count = Arc::new(AtomicUsize::new(0)); + let local = BrokerLocal::negotiate(FakeLocalControlChannel { + next_handle: handle.0, + consume_attempts: Arc::new(AtomicUsize::new(0)), + read_ready: Arc::new(AtomicBool::new(false)), + request_count: Arc::clone(&request_count), + fail_requests: Arc::new(AtomicBool::new(false)), + last_request: None, + }) + .unwrap(); + let litebox = LiteBox::new_with_broker_local(platform, local); + let counter = EventCounter::new(&litebox, 0).unwrap(); + let read_observer = Arc::new(ReadObserver(AtomicBool::new(false))); + let read_observer_dyn: Arc> = read_observer.clone(); + counter.register_observer(Arc::downgrade(&read_observer_dyn), Events::IN); + let litebox_weak = Arc::downgrade(&litebox.x); + let dispatch_notification = litebox.broker_notification_dispatcher(); + let dispatch_failure = litebox.broker_failure_dispatcher(); + + drop(litebox); + + assert!(litebox_weak.upgrade().is_none()); + dispatch_notification(BrokerNotification::Readiness(ReadinessNotification { + handle, + readiness: ReadinessFlags::READ, + })); + assert!(read_observer.0.load(Ordering::SeqCst)); + dispatch_failure(); + assert_eq!(counter.check_io_events(), Events::ERR); + assert_eq!(request_count.load(Ordering::SeqCst), 1); + } + + struct ErrorObserver(AtomicBool); + + impl Observer for ErrorObserver { + fn on_events(&self, events: &Events) { + if events.contains(Events::ERR) { + self.0.store(true, Ordering::SeqCst); + } + } + } + + struct ReadObserver(AtomicBool); + + impl Observer for ReadObserver { + fn on_events(&self, events: &Events) { + if events.contains(Events::IN) { + self.0.store(true, Ordering::SeqCst); + } + } + } + struct FakeLocalControlChannel { - handle: ObjectHandle, + next_handle: u64, consume_attempts: Arc, read_ready: Arc, + request_count: Arc, + fail_requests: Arc, last_request: Option, } impl LocalControlChannel for FakeLocalControlChannel { - type Error = core::convert::Infallible; + type Error = (); fn send_handshake_request( &mut self, @@ -276,19 +423,22 @@ mod tests { request: &BrokerRequest, ) -> core::result::Result<(), Self::Error> { self.last_request = Some(request.clone()); + self.request_count.fetch_add(1, Ordering::SeqCst); Ok(()) } fn recv_response(&mut self) -> core::result::Result, Self::Error> { + if self.fail_requests.load(Ordering::SeqCst) { + self.last_request.take(); + return Err(()); + } let response = match self.last_request.take().unwrap() { BrokerRequest::Event(EventRequest::Create(_)) => { - BrokerResponse::Event(EventResponse::Create(CreateEventResponse { - handle: self.handle, - })) + let handle = ObjectHandle(self.next_handle); + self.next_handle += 1; + BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })) } - BrokerRequest::Event(EventRequest::Consume(request)) - if request.handle == self.handle => - { + BrokerRequest::Event(EventRequest::Consume(_)) => { self.consume_attempts.fetch_add(1, Ordering::SeqCst); if self.read_ready.swap(false, Ordering::SeqCst) { BrokerResponse::Event(EventResponse::Consume(EventConsumption { @@ -299,10 +449,10 @@ mod tests { BrokerResponse::Error(ErrorCode::WouldBlock) } } - BrokerRequest::CloseObject(handle) if handle == self.handle => { - BrokerResponse::ObjectClosed + BrokerRequest::CloseObject(_) => BrokerResponse::ObjectClosed, + request @ BrokerRequest::Event(_) => { + panic!("unexpected broker request: {request:?}") } - request => panic!("unexpected broker request: {request:?}"), }; Ok(Some(response)) } diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 637c6301b4..36838a8db7 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -36,7 +36,11 @@ impl LiteBox { /// If the `enforce_singleton_litebox_instance` compilation feature has been enabled, and more /// than one instance is made, will panic. pub fn new(platform: &'static Platform) -> Self { - Self::new_inner(platform, None) + Self::new_inner( + platform, + None, + Arc::new(broker::BrokerPollableRegistry::new()), + ) } /// Create a new [`LiteBox`] instance with a negotiated broker-local control adapter installed. @@ -45,19 +49,21 @@ impl LiteBox { broker_local: BrokerLocal, ) -> Self where + Platform: TimeProvider, Channel: LocalControlChannel + Send + 'static, { - Self::new_inner( - platform, - Some(Arc::new( - broker::BrokerLocalControl::::new(broker_local), - )), - ) + let broker_pollables = Arc::new(broker::BrokerPollableRegistry::new()); + let broker_control = Arc::new(broker::BrokerLocalControl::::new( + broker_local, + Arc::clone(&broker_pollables), + )); + Self::new_inner(platform, Some(broker_control), broker_pollables) } fn new_inner( platform: &'static Platform, broker_control: Option>, + broker_pollables: Arc>, ) -> Self { // This check ensures that there is exactly one `LiteBox` instance in the process. // @@ -102,7 +108,7 @@ impl LiteBox { platform, descriptors, broker: broker_control, - broker_handles: Arc::new(broker::BrokerHandleRegistry::new()), + broker_pollables, }), } } @@ -140,8 +146,8 @@ impl LiteBox { self.x.broker.clone() } - pub(crate) fn broker_handle_registry(&self) -> Arc> { - Arc::clone(&self.x.broker_handles) + pub(crate) fn broker_pollable_registry(&self) -> Arc> { + Arc::clone(&self.x.broker_pollables) } /// Dispatches one broker notification to the matching local-core object. @@ -152,7 +158,7 @@ impl LiteBox { match notification { BrokerNotification::Readiness(notification) => self .x - .broker_handles + .broker_pollables .notify_readiness(notification.handle, notification.readiness), } } @@ -162,9 +168,26 @@ impl LiteBox { where Platform: TimeProvider + 'static, { - let litebox = self.clone(); + let broker_pollables = Arc::downgrade(&self.x.broker_pollables); move |notification| { - litebox.dispatch_broker_notification(notification); + if let Some(broker_pollables) = broker_pollables.upgrade() { + match notification { + BrokerNotification::Readiness(notification) => { + broker_pollables + .notify_readiness(notification.handle, notification.readiness); + } + } + } + } + } + + /// Returns a dispatcher that fails all broker-backed objects when the association closes. + pub fn broker_failure_dispatcher(&self) -> impl Fn() + Send + 'static { + let broker = self.x.broker.as_ref().map(Arc::downgrade); + move || { + if let Some(broker) = broker.as_ref().and_then(alloc::sync::Weak::upgrade) { + broker.fail_connection(); + } } } } @@ -174,5 +197,5 @@ pub(crate) struct LiteBoxX { pub(crate) platform: &'static Platform, descriptors: RwLock>, broker: Option>, - broker_handles: Arc>, + broker_pollables: Arc>, } diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 1b5f956a39..2f3c5ee250 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -8,6 +8,7 @@ //! no_std protocol, local, core, and host crates. use std::io::{Error, ErrorKind, Read, Result as IoResult, Write}; +use std::net::Shutdown; use std::os::unix::net::UnixStream; use std::path::Path; use std::time::{Duration, Instant}; @@ -34,6 +35,11 @@ pub struct UnixStreamLocalControlChannel { setup_deadline: Option, } +/// Independently owned handle for interrupting local control-channel I/O. +pub struct UnixStreamLocalControlCancellation { + stream: UnixStream, +} + impl UnixStreamLocalControlChannel { /// Creates a local control channel from an already-connected Unix stream. pub const fn from_connected(stream: UnixStream) -> Self { @@ -62,6 +68,29 @@ impl UnixStreamLocalControlChannel { setup_deadline: Some(deadline), }) } + + /// Creates a handle that can interrupt pending control-channel I/O. + pub fn cancellation_handle(&self) -> IoResult { + self.stream + .try_clone() + .map(|stream| UnixStreamLocalControlCancellation { stream }) + } +} + +impl UnixStreamLocalControlCancellation { + /// Shuts down the control stream, unblocking pending reads or writes. + pub fn cancel(&self) -> IoResult<()> { + match self.stream.shutdown(Shutdown::Both) { + Err(error) if error.kind() == ErrorKind::NotConnected => Ok(()), + result => result, + } + } +} + +impl Drop for UnixStreamLocalControlChannel { + fn drop(&mut self) { + let _ = self.stream.shutdown(Shutdown::Both); + } } /// Host-side Unix-domain-socket control channel for the hosted userland POC. @@ -403,6 +432,47 @@ mod tests { ); } + #[test] + fn local_control_cancellation_unblocks_response_read() { + let (local_stream, _host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); + let cancellation = channel.cancellation_handle().unwrap(); + let completed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(1); + let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); + let reader_completed = completed.clone(); + let reader = std::thread::spawn(move || { + started_sender.send(()).unwrap(); + result_sender.send(channel.recv_response()).unwrap(); + reader_completed.store(true, std::sync::atomic::Ordering::Release); + }); + + started_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + std::thread::sleep(Duration::from_millis(50)); + assert!(!completed.load(std::sync::atomic::Ordering::Acquire)); + cancellation.cancel().unwrap(); + + assert!(result_receiver.recv_timeout(Duration::from_secs(1)).is_ok()); + reader.join().unwrap(); + } + + #[test] + fn dropping_local_control_closes_connection_with_cancellation_clone() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let channel = UnixStreamLocalControlChannel::from_connected(local_stream); + let _cancellation = channel.cancellation_handle().unwrap(); + host_stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + + drop(channel); + + let mut byte = [0]; + assert_eq!(host_stream.read(&mut byte).unwrap(), 0); + } + #[test] fn host_reports_wrong_phase_request_frames_as_protocol_violations() { let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index d6bbc471d6..c17b75d2c8 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -10,7 +10,8 @@ use anyhow::{Context as _, Result}; use litebox_broker_local::{BrokerLocal, BrokerNotifications}; use litebox_broker_protocol::message::BrokerNotification; use litebox_broker_transport::unix_socket::{ - UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, + UnixStreamLocalControlCancellation, UnixStreamLocalControlChannel, + UnixStreamLocalNotificationChannel, }; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -22,6 +23,7 @@ pub(crate) fn connect( ) -> Result<( BrokerLocal, BrokerNotifications, + UnixStreamLocalControlCancellation, )> { let setup_deadline = Instant::now() + SETUP_TIMEOUT; let control_channel = connect_with_retry( @@ -48,26 +50,40 @@ pub(crate) fn connect( notification_socket_path.display() ) })?; + let control_cancellation = control_channel + .cancellation_handle() + .context("failed to create broker control cancellation handle")?; let local = BrokerLocal::negotiate(control_channel).context("broker negotiation failed")?; - Ok((local, BrokerNotifications::new(notification_channel))) + Ok(( + local, + BrokerNotifications::new(notification_channel), + control_cancellation, + )) } pub(crate) fn start_notification_receiver( mut notifications: BrokerNotifications, + control_cancellation: UnixStreamLocalControlCancellation, dispatch_notification: impl Fn(BrokerNotification) + Send + 'static, + dispatch_failure: impl Fn() + Send + 'static, ) -> Result<()> { std::thread::Builder::new() .name("litebox-broker-notifications".to_owned()) .spawn(move || { - loop { + let receive_error = loop { match notifications.recv_notification() { Ok(Some(notification)) => dispatch_notification(notification), - Ok(None) => break, - Err(error) => { - eprintln!("failed to receive broker notification: {error}"); - break; - } + Ok(None) => break None, + Err(error) => break Some(error), } + }; + let cancellation_error = control_cancellation.cancel().err(); + dispatch_failure(); + if let Some(error) = receive_error { + eprintln!("failed to receive broker notification: {error}"); + } + if let Some(error) = cancellation_error { + eprintln!("failed to cancel broker control channel: {error}"); } }) .context("failed to start broker notification receiver")?; diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 9b5495b81f..0fc0eee0a7 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -241,14 +241,16 @@ pub fn run(cli_args: CliArgs) -> Result<()> { }; let shim_builder = if let Some(broker_connection) = broker_connection { - let (broker_local, broker_notifications) = broker_connection; + let (broker_local, broker_notifications, broker_control_cancellation) = broker_connection; let litebox = litebox::LiteBox::new_with_broker_local( litebox_platform_multiplex::platform(), broker_local, ); broker::start_notification_receiver( broker_notifications, + broker_control_cancellation, litebox.broker_notification_dispatcher(), + litebox.broker_failure_dispatcher(), )?; litebox_shim_linux::LinuxShimBuilder::new_with_litebox(litebox) } else { From 2d1d82a461309ce0f1ac1647826c1be66e42e0ee Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 18 Jul 2026 14:12:38 -0700 Subject: [PATCH 104/319] Fix pipe edge-case semantics (#1044) This PR fixes in-process pipe behavior for blocking writes, zero-length writes after peer closure, and operations on the wrong endpoint. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/pipes.rs | 64 ++++++++++++++++++--------- litebox_common_linux/src/errno/mod.rs | 6 +-- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index 8b685d7f9d..bc298ea293 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -403,12 +403,12 @@ impl WriteEnd WriteEnd total_written += written, + Err(_) if total_written != 0 => return Ok(total_written), + Err(error) => return Err(PipeError::from(error)), + } + } + Ok(total_written) } common_functions_for_channel!(); @@ -628,6 +638,24 @@ mod tests { extern crate std; + #[test] + fn local_zero_length_write_succeeds_after_reader_closes() { + let platform = crate::platform::mock::MockPlatform::new(); + let litebox = crate::LiteBox::new(platform); + let pipes = super::Pipes::new(&litebox); + let (writer, reader) = pipes.create_pipe(2, super::Flags::empty(), None); + + pipes.close(&reader).unwrap(); + + assert_eq!( + pipes + .write(&WaitState::new(platform).context(), &writer, &[]) + .unwrap(), + 0 + ); + pipes.close(&writer).unwrap(); + } + #[test] fn test_blocking_channel() { let platform = crate::platform::mock::MockPlatform::new(); @@ -639,15 +667,11 @@ mod tests { std::thread::scope(|scope| { scope.spawn(move || { let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - let mut i = 0; - while i < data.len() { - let ret = pipes - .write(&WaitState::new(platform).context(), &prod, &data[i..]) - .unwrap(); - i += ret; - } + let written = pipes + .write(&WaitState::new(platform).context(), &prod, &data) + .unwrap(); + assert_eq!(written, data.len()); pipes.close(&prod).unwrap(); - assert_eq!(i, data.len()); }); let mut buf = [0; 10]; diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index ca932167dd..4a828008f1 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -576,8 +576,8 @@ impl From for Errno { impl From for Errno { fn from(value: litebox::pipes::errors::ReadError) -> Self { match value { - litebox::pipes::errors::ReadError::ClosedFd => Errno::EBADFD, - litebox::pipes::errors::ReadError::NotForReading => Errno::EINVAL, + litebox::pipes::errors::ReadError::ClosedFd + | litebox::pipes::errors::ReadError::NotForReading => Errno::EBADF, litebox::pipes::errors::ReadError::WouldBlock => Errno::EWOULDBLOCK, _ => todo!(), } @@ -589,7 +589,7 @@ impl From for Errno { match value { litebox::pipes::errors::WriteError::ClosedFd => Errno::EBADF, litebox::pipes::errors::WriteError::ReadEndClosed => Errno::EPIPE, - litebox::pipes::errors::WriteError::NotForWriting => Errno::EINVAL, + litebox::pipes::errors::WriteError::NotForWriting => Errno::EBADF, litebox::pipes::errors::WriteError::WouldBlock => Errno::EWOULDBLOCK, _ => todo!(), } From 3107ce8316212e249a5654d41c8f00e7d89ffc21 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Sat, 18 Jul 2026 14:41:01 -0700 Subject: [PATCH 105/319] Model connected ConDrv stream objects (#1046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Model the ConDrv `Server` → `Reference` → `Connect` flow and resolve connected console children with native-observed status behavior. Remaining work: - Decode the full undocumented ConDrv handshake payload. - Implement active screen-buffer switching and native ConDrv share-access exceptions. - Defer shared-console ownership until LiteBox supports multiple guest processes. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 4 + litebox_shim_windows/src/syscalls/condrv.rs | 166 ++++++-- litebox_shim_windows/src/syscalls/file.rs | 401 ++++++++++++++++++-- 3 files changed, 524 insertions(+), 47 deletions(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index f620b373fb..961ae6261f 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -464,6 +464,7 @@ pub struct Process { ntdll_mapping: Option, peb_address: usize, handles: WindowsHandleStore, + condrv_console: syscalls::condrv::CondrvConsole, object_manager: WindowsObjectManager, section_views: WindowsSectionViews, // TODO: move this into `GlobalState` once we have a proper shared mapping implementation. @@ -507,6 +508,9 @@ impl Process { ntdll_mapping: None, peb_address: 0, handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), + // TODO(condrv-shared-console): move console ownership to shared state or a broker when + // LiteBox supports AttachConsole/IOCTL_CONDRV_BIND_PID across guest processes. + condrv_console: syscalls::condrv::CondrvConsole::new(), object_manager, windows_shared_section, section_views: WindowsSectionViews::::new(BTreeMap::new()), diff --git a/litebox_shim_windows/src/syscalls/condrv.rs b/litebox_shim_windows/src/syscalls/condrv.rs index d56d66eacd..956a1accd1 100644 --- a/litebox_shim_windows/src/syscalls/condrv.rs +++ b/litebox_shim_windows/src/syscalls/condrv.rs @@ -3,6 +3,7 @@ //! Windows console driver support. +use alloc::sync::Arc; use core::mem::size_of; use int_enum::IntEnum; @@ -21,15 +22,92 @@ const CD_SERVER_EA_NAME: &[u8] = b"server"; pub(crate) enum CondrvObject { Input = 0, Output = 1, - Server = 2, - Reference = 3, - Connect = 4, + CurrentInput = 2, + CurrentOutput = 3, + ScreenBuffer = 4, + Server = 5, + Reference = 6, + Connect = 7, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum CondrvStreamDirection { + Input, + Output, +} + +pub(crate) struct CondrvStreamObject { + id: u64, +} + +struct CondrvConsoleState { + next_object_id: u64, + bound_input: Arc, + active_output: Arc, +} + +pub(crate) struct CondrvConsole { + state: litebox::sync::Mutex, +} + +impl CondrvStreamObject { + pub(crate) fn id(&self) -> u64 { + self.id + } +} + +impl CondrvConsole { + pub(crate) fn new() -> Self { + let bound_input = Arc::new(CondrvStreamObject { id: 1 }); + let active_output = Arc::new(CondrvStreamObject { id: 2 }); + Self { + state: litebox::sync::Mutex::new(CondrvConsoleState { + next_object_id: 3, + bound_input, + active_output, + }), + } + } + + pub(crate) fn open_stream( + &self, + endpoint: CondrvObject, + ) -> Result, NtStatus> { + let mut state = self.state.lock(); + match endpoint { + CondrvObject::CurrentInput => Ok(Arc::clone(&state.bound_input)), + // TODO(condrv-activate-buffer): update this pointer when LiteBox implements and + // host-validates the ConDrv activate-buffer IOCTL. + CondrvObject::CurrentOutput => Ok(Arc::clone(&state.active_output)), + CondrvObject::Input | CondrvObject::Output | CondrvObject::ScreenBuffer => { + state.allocate_object() + } + CondrvObject::Server | CondrvObject::Reference | CondrvObject::Connect => { + Err(NtStatus::OBJECT_TYPE_MISMATCH) + } + } + } +} + +impl CondrvConsoleState { + fn allocate_object(&mut self) -> Result, NtStatus> { + let id = self.next_object_id; + self.next_object_id = id.checked_add(1).ok_or(NtStatus::QUOTA_EXCEEDED)?; + Ok(Arc::new(CondrvStreamObject { id })) + } } impl CondrvObject { pub(crate) fn from_device_name(name: &str) -> Result { match Self::from_component(name) { - Some(object @ (Self::Input | Self::Output | Self::Server)) => Ok(object), + Some( + object @ (Self::Input + | Self::Output + | Self::CurrentInput + | Self::CurrentOutput + | Self::ScreenBuffer + | Self::Server), + ) => Ok(object), Some(Self::Reference) => Err(NtStatus::INVALID_HANDLE), Some(Self::Connect) => Err(NtStatus::OBJECT_TYPE_MISMATCH), None => Err(NtStatus::OBJECT_NAME_NOT_FOUND), @@ -41,6 +119,12 @@ impl CondrvObject { Some(Self::Input) } else if name.eq_ignore_ascii_case("Output") { Some(Self::Output) + } else if name.eq_ignore_ascii_case("CurrentIn") { + Some(Self::CurrentInput) + } else if name.eq_ignore_ascii_case("CurrentOut") { + Some(Self::CurrentOutput) + } else if name.eq_ignore_ascii_case("ScreenBuffer") { + Some(Self::ScreenBuffer) } else if name.eq_ignore_ascii_case("Server") { Some(Self::Server) } else if name.eq_ignore_ascii_case("Reference") { @@ -56,29 +140,52 @@ impl CondrvObject { let name = name.strip_prefix('\\').ok_or(NtStatus::NOT_FOUND)?; let child = Self::from_component(name).ok_or(NtStatus::NOT_FOUND)?; - match (self, child) { - (Self::Server, Self::Server | Self::Reference) - | (Self::Reference, Self::Server | Self::Connect | Self::Input | Self::Output) - | (Self::Input | Self::Output, Self::Server | Self::Input | Self::Output) => Ok(child), - (Self::Server, Self::Input | Self::Output) => Err(NtStatus::INVALID_DEVICE_STATE), - (Self::Reference | Self::Input | Self::Output, Self::Reference) => { - Err(NtStatus::OBJECT_TYPE_MISMATCH) + match child { + Self::Server => Ok(child), + Self::Reference => match self { + Self::Server | Self::Connect => Ok(child), + _ => Err(NtStatus::OBJECT_TYPE_MISMATCH), + }, + Self::Connect => { + if self == Self::Reference { + Ok(child) + } else { + Err(NtStatus::INVALID_HANDLE) + } } - (Self::Server | Self::Input | Self::Output, Self::Connect) | (Self::Connect, _) => { - Err(NtStatus::INVALID_HANDLE) + Self::Input + | Self::Output + | Self::CurrentInput + | Self::CurrentOutput + | Self::ScreenBuffer => { + if self == Self::Server { + Err(NtStatus::INVALID_DEVICE_STATE) + } else { + Ok(child) + } } } } pub(crate) fn handle_path(self) -> &'static str { match self { - Self::Input => "/dev/stdin", - Self::Output => "/dev/stdout", + Self::Input | Self::CurrentInput => "/dev/stdin", + Self::Output | Self::CurrentOutput | Self::ScreenBuffer => "/dev/stdout", Self::Server => r"\Device\ConDrv\Server", Self::Reference => r"\Device\ConDrv\Reference", Self::Connect => r"\Device\ConDrv\Connect", } } + + pub(crate) fn stream_direction(self) -> Option { + match self { + Self::Input | Self::CurrentInput => Some(CondrvStreamDirection::Input), + Self::Output | Self::CurrentOutput | Self::ScreenBuffer => { + Some(CondrvStreamDirection::Output) + } + Self::Server | Self::Reference | Self::Connect => None, + } + } } #[repr(u32)] @@ -177,13 +284,17 @@ pub(crate) fn validate_connect_server_ea( let Some(value_address) = ea_buffer.as_usize().checked_add(value_offset) else { return Err(NtStatus::EAS_NOT_SUPPORTED); }; - // The ConDrv "server" EA value format is undocumented; probe the declared payload without - // interpreting it until its semantics are understood. - if ConstPtr::::from_usize(value_address) - .to_owned_slice(value_length) - .is_none() - { + // Windows rejects a structurally valid but zeroed server handshake with + // STATUS_PIPE_DISCONNECTED. + // TODO(condrv-handshake): fully decode the undocumented server payload; the current subset + // only pins the native all-zero rejection and validates its readable extent. + let Some(value) = + ConstPtr::::from_usize(value_address).to_owned_slice(value_length) + else { return Err(NtStatus::ACCESS_VIOLATION); + }; + if value.iter().all(|byte| *byte == 0) { + return Err(NtStatus::PIPE_DISCONNECTED); } Ok(()) @@ -269,7 +380,9 @@ mod tests { #[test] fn relative_children_match_host_parse_contexts() { - use CondrvObject::{Connect, Input, Output, Reference, Server}; + use CondrvObject::{ + Connect, CurrentInput, CurrentOutput, Input, Output, Reference, ScreenBuffer, Server, + }; for (parent, name, expected) in [ (Server, r"\Server", Ok(Server)), @@ -296,6 +409,15 @@ mod tests { (Output, r"\Output", Ok(Output)), (Output, r"\Reference", Err(NtStatus::OBJECT_TYPE_MISMATCH)), (Output, r"\Connect", Err(NtStatus::INVALID_HANDLE)), + (Connect, r"\Input", Ok(Input)), + (Connect, r"\Output", Ok(Output)), + (Connect, r"\CurrentIn", Ok(CurrentInput)), + (Connect, r"\CurrentOut", Ok(CurrentOutput)), + (Connect, r"\ScreenBuffer", Ok(ScreenBuffer)), + (Connect, r"\Server", Ok(Server)), + (Connect, r"\Reference", Ok(Reference)), + (Connect, r"\Connect", Err(NtStatus::INVALID_HANDLE)), + (Connect, r"\Bogus", Err(NtStatus::NOT_FOUND)), ] { assert_eq!(parent.relative_child(name), expected, "{parent:?} + {name}"); } diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 9bb5457fcd..5f12101ab4 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -2,6 +2,7 @@ // Licensed under the MIT license. use alloc::string::String; +use alloc::sync::Arc; use core::marker::PhantomData; use core::mem::size_of; @@ -17,7 +18,7 @@ use crate::nt_types::{ AccessMask, IoStatusBlock, ObjectAttributes, UnicodeString, read_object_attributes, }; use crate::syscalls::Handle; -use crate::syscalls::condrv::{self, CondrvObject}; +use crate::syscalls::condrv::{self, CondrvObject, CondrvStreamDirection, CondrvStreamObject}; use crate::syscalls::file_path::{FilePathResolver, FilePathRoot, FileTarget}; use crate::{ ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value, raw_handle_entry, @@ -102,11 +103,29 @@ enum FileObjectBacking { }, CondrvStream { object: CondrvObject, + stream_object: Arc, fd: TypedFd, }, CondrvControl(CondrvObject), } +#[derive(Clone, Copy)] +enum FileSharingIdentity<'a> { + Path(&'a str), + // TODO(condrv-share-access): native CONIN$/CONOUT$ permits multiple share-access-zero opens + // of the same bound object; determine which ConDrv opens ignore sharing before enforcing it. + CondrvObject(u64), +} + +impl FileSharingIdentity<'_> { + 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), + } + } +} + impl FileObject { fn condrv_object(&self) -> Option { match self.backing { @@ -116,6 +135,13 @@ impl FileObject { } } + fn condrv_stream_object_id(&self) -> Option { + match &self.backing { + FileObjectBacking::CondrvStream { stream_object, .. } => Some(stream_object.id()), + FileObjectBacking::Filesystem { .. } | FileObjectBacking::CondrvControl(_) => None, + } + } + fn is_directory(&self) -> bool { matches!( self.backing, @@ -683,7 +709,6 @@ impl Task { share_access, create_disposition, create_options, - file_attributes, ea_buffer, ea_length, ), @@ -701,7 +726,11 @@ impl Task { create_options: FileCreateOptions, file_attributes: u32, ) -> Result<(FileObject, FileCreateInformation), NtStatus> { - self.check_file_sharing(&path, desired_access, share_access)?; + self.check_file_sharing( + FileSharingIdentity::Path(&path), + desired_access, + share_access, + )?; if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { return self.open_or_create_directory( &path, @@ -718,7 +747,7 @@ impl Task { desired_access, create_disposition, create_options, - file_attributes, + create_mode(file_attributes), )?; Ok(( FileObject { @@ -743,7 +772,6 @@ impl Task { share_access: FileShareAccess, create_disposition: CreateDisposition, create_options: FileCreateOptions, - file_attributes: u32, ea_buffer: Option>, ea_length: u32, ) -> Result<(FileObject, FileCreateInformation), NtStatus> { @@ -757,22 +785,42 @@ impl Task { } let path = String::from(object.handle_path()); - self.check_file_sharing(&path, desired_access, share_access)?; - let (backing, information) = match object { - CondrvObject::Input | CondrvObject::Output => { - let (fd, _, information) = self.open_backing_fd( - &path, - desired_access, - create_disposition, - create_options, - file_attributes, - )?; - (FileObjectBacking::CondrvStream { object, fd }, information) - } - CondrvObject::Server | CondrvObject::Reference | CondrvObject::Connect => ( + let (backing, information) = if let Some(direction) = object.stream_direction() { + let stream_object = self.process.condrv_console.open_stream(object)?; + self.check_file_sharing( + FileSharingIdentity::CondrvObject(stream_object.id()), + desired_access, + share_access, + )?; + let backing_access = match direction { + CondrvStreamDirection::Input => FileAccess::READ_DATA, + CondrvStreamDirection::Output => FileAccess::WRITE_DATA, + }; + let (fd, _, information) = self.open_backing_fd( + &path, + backing_access, + create_disposition, + create_options, + Mode::empty(), + )?; + ( + FileObjectBacking::CondrvStream { + object, + stream_object, + fd, + }, + information, + ) + } else { + self.check_file_sharing( + FileSharingIdentity::Path(&path), + desired_access, + share_access, + )?; + ( FileObjectBacking::CondrvControl(object), FileCreateInformation::Opened, - ), + ) }; Ok(( FileObject { @@ -792,7 +840,7 @@ impl Task { desired_access: FileAccess, create_disposition: CreateDisposition, create_options: FileCreateOptions, - file_attributes: u32, + mode: Mode, ) -> Result<(TypedFd, bool, FileCreateInformation), NtStatus> { let existed_before_open = self.fs.file_status(path).is_ok(); if create_disposition == CreateDisposition::Supersede @@ -804,7 +852,7 @@ impl Task { let flags = desired_access.open_flags(create_disposition, create_options); let fd = self .fs - .open(path, flags, create_mode(file_attributes)) + .open(path, flags, mode) .map_err(|error| map_open_error(error, create_disposition))?; let file_status = match self.fs.fd_file_status(&fd) { Ok(file_status) => file_status, @@ -924,7 +972,7 @@ impl Task { fn check_file_sharing( &self, - path: &str, + identity: FileSharingIdentity<'_>, desired_access: FileAccess, share_access: FileShareAccess, ) -> Result<(), NtStatus> { @@ -942,7 +990,7 @@ impl Task { continue; }; let conflicts = entry.with_entry(|file| { - file.path == path + identity.matches(file) && (desired_access.conflicts_with_share(file.share_access) || file.granted_access.conflicts_with_share(share_access)) }); @@ -1256,8 +1304,34 @@ mod tests { .0 } + fn open_condrv_child( + task: &Task, + root: Handle, + name: &str, + desired_access: u32, + ) -> (NtStatus, Handle) { + let (_path, _name, mut attributes) = open_object_attributes(name); + attributes.root_directory = root; + let mut handle = Handle::default(); + let mut io_status = IoStatusBlock::default(); + let status = task.sys_nt_create_file( + mut_ptr(&mut handle), + desired_access, + Some(const_ptr(&attributes)), + mut_ptr(&mut io_status), + None, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ); + (status, handle) + } + #[test] - fn nt_create_file_follows_condrv_server_reference_connect_sequence() { + fn nt_create_file_follows_condrv_connection_through_standard_streams() { let task = crate::tests::test_task(); let server_handle = open_condrv_server(&task); let reference_handle = open_condrv_reference(&task, server_handle); @@ -1280,6 +1354,46 @@ mod tests { .with_entry(FileObject::condrv_object), Some(CondrvObject::Reference) ); + + assert_eq!( + task.sys_nt_create_file( + mut_ptr(&mut connect_handle), + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + Some(const_ptr(&connect_attributes)), + mut_ptr(&mut io_status), + None, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + None, + 0, + ), + NtStatus::EAS_NOT_SUPPORTED + ); + assert!(connect_handle.is_null()); + + assert_eq!( + task.sys_nt_create_file( + mut_ptr(&mut connect_handle), + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + Some(const_ptr(&connect_attributes)), + mut_ptr(&mut io_status), + None, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + Some(const_ptr(&ea[0])), + u32::try_from(ea.len()).unwrap(), + ), + NtStatus::PIPE_DISCONNECTED + ); + assert!(connect_handle.is_null()); + + assert_eq!(task.sys_nt_close(server_handle), NtStatus::SUCCESS); + let mut ea = ea; + *ea.last_mut().unwrap() = 1; assert_eq!( task.sys_nt_create_file( mut_ptr(&mut connect_handle), @@ -1303,9 +1417,147 @@ mod tests { Some(CondrvObject::Connect) ); + assert_eq!(connect_handle, server_handle); + let (input_status, input_handle) = + open_condrv_child(&task, connect_handle, r"\Input", FILE_GENERIC_READ); + let (output_status, output_handle) = + open_condrv_child(&task, connect_handle, r"\Output", FILE_GENERIC_WRITE); + let (current_input_status, current_input_handle) = + open_condrv_child(&task, connect_handle, r"\CurrentIn", FILE_GENERIC_READ); + let (current_output_status, current_output_handle) = + open_condrv_child(&task, connect_handle, r"\CurrentOut", FILE_GENERIC_WRITE); + let (screen_buffer_status, screen_buffer_handle) = + open_condrv_child(&task, connect_handle, r"\ScreenBuffer", FILE_GENERIC_WRITE); + assert_eq!(input_status, NtStatus::SUCCESS); + assert_eq!(output_status, NtStatus::SUCCESS); + assert_eq!(current_input_status, NtStatus::SUCCESS); + assert_eq!(current_output_status, NtStatus::SUCCESS); + assert_eq!(screen_buffer_status, NtStatus::SUCCESS); + let stream_identity = |handle| { + task.file_entry(handle).unwrap().with_entry(|file| { + ( + file.condrv_object().unwrap(), + file.condrv_stream_object_id().unwrap(), + ) + }) + }; + let input_identity = stream_identity(input_handle); + let output_identity = stream_identity(output_handle); + let current_input_identity = stream_identity(current_input_handle); + let current_output_identity = stream_identity(current_output_handle); + let screen_buffer_identity = stream_identity(screen_buffer_handle); + assert_eq!(current_input_identity.0, CondrvObject::CurrentInput); + assert_eq!(current_output_identity.0, CondrvObject::CurrentOutput); + assert_eq!(screen_buffer_identity.0, CondrvObject::ScreenBuffer); + assert_ne!(input_identity.1, current_input_identity.1); + assert_ne!(output_identity.1, current_output_identity.1); + assert_ne!(output_identity.1, screen_buffer_identity.1); + assert_ne!(current_output_identity.1, screen_buffer_identity.1); + for handle in [output_handle, current_output_handle, screen_buffer_handle] { + assert_eq!( + task.file_entry(handle) + .unwrap() + .with_entry(|file| file.path.clone()), + "/dev/stdout" + ); + } + + for (path, desired_access, expected_object, expected_bound_id) in [ + ( + r"\Device\ConDrv\CurrentIn", + FILE_GENERIC_READ, + CondrvObject::CurrentInput, + Some(current_input_identity.1), + ), + ( + r"\Device\ConDrv\CurrentOut", + FILE_GENERIC_WRITE, + CondrvObject::CurrentOutput, + Some(current_output_identity.1), + ), + ( + r"\Device\ConDrv\ScreenBuffer", + FILE_GENERIC_WRITE, + CondrvObject::ScreenBuffer, + None, + ), + ] { + let (status, handle, _) = create_file(&task, path, desired_access, FILE_OPEN); + assert_eq!(status, NtStatus::SUCCESS, "{path}"); + let identity = stream_identity(handle); + assert_eq!(identity.0, expected_object, "{path}"); + if let Some(expected_bound_id) = expected_bound_id { + assert_eq!(identity.1, expected_bound_id, "{path}"); + } else { + assert_ne!(identity.1, screen_buffer_identity.1, "{path}"); + } + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + } + + assert_eq!(task.sys_nt_close(current_output_handle), NtStatus::SUCCESS); + let mut exclusive_handles = alloc::vec::Vec::new(); + for path in [ + r"\Device\ConDrv\Output", + r"\Device\ConDrv\CurrentOut", + r"\Device\ConDrv\ScreenBuffer", + r"\Device\ConDrv\ScreenBuffer", + ] { + let (_path, _name, attributes) = open_object_attributes(path); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_file( + mut_ptr(&mut handle), + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + Some(const_ptr(&attributes)), + mut_ptr(&mut io_status), + None, + 0, + 0, + FILE_OPEN, + FileCreateOptions::NON_DIRECTORY_FILE.bits(), + None, + 0, + ), + NtStatus::SUCCESS, + "{path}" + ); + exclusive_handles.push(handle); + } + let exclusive_identities: alloc::vec::Vec<_> = exclusive_handles + .iter() + .map(|handle| stream_identity(*handle)) + .collect(); + assert_eq!(exclusive_identities[0].0, CondrvObject::Output); + assert_eq!(exclusive_identities[1].0, CondrvObject::CurrentOutput); + assert_eq!( + exclusive_identities[1].1, current_output_identity.1, + "CurrentOut must reference the active output object" + ); + assert_ne!(exclusive_identities[0].1, exclusive_identities[1].1); + assert_ne!(exclusive_identities[2].1, exclusive_identities[3].1); + let closed_screen_buffer_id = exclusive_identities[2].1; + for handle in exclusive_handles { + assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); + } + let (status, reopened_screen_buffer, _) = create_file( + &task, + r"\Device\ConDrv\ScreenBuffer", + FILE_GENERIC_WRITE, + FILE_OPEN, + ); + assert_eq!(status, NtStatus::SUCCESS); + assert_ne!( + stream_identity(reopened_screen_buffer).1, + closed_screen_buffer_id + ); + assert_eq!(task.sys_nt_close(reopened_screen_buffer), NtStatus::SUCCESS); + + assert_eq!(task.sys_nt_close(screen_buffer_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(current_input_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(output_handle), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(input_handle), NtStatus::SUCCESS); assert_eq!(task.sys_nt_close(connect_handle), NtStatus::SUCCESS); assert_eq!(task.sys_nt_close(reference_handle), NtStatus::SUCCESS); - assert_eq!(task.sys_nt_close(server_handle), NtStatus::SUCCESS); } #[test] @@ -2029,10 +2281,12 @@ mod tests { #[cfg(all(target_os = "windows", target_arch = "x86_64"))] mod host_fidelity { use super::*; + use crate::nt_types::{ProcessEnvironmentBlock, RtlUserProcessParameters}; use core::ffi::c_void; #[link(name = "ntdll")] unsafe extern "system" { + fn RtlGetCurrentPeb() -> *const ProcessEnvironmentBlock; fn NtCreateFile( FileHandle: *mut *mut c_void, DesiredAccess: u32, @@ -2064,6 +2318,12 @@ mod tests { fn NtClose(Handle: *mut c_void) -> i32; } + #[link(name = "kernel32")] + unsafe extern "system" { + fn AllocConsole() -> i32; + fn GetLastError() -> u32; + } + fn host_nt_path(path: &std::path::Path) -> std::string::String { std::format!(r"\??\{}", path.display()) } @@ -2090,6 +2350,97 @@ mod tests { NtStatus::from_raw(u32::from_ne_bytes(status.to_ne_bytes())) } + fn host_create_file(root: Handle, name: &str) -> (NtStatus, *mut c_void) { + let path = utf16(name); + let name = unicode_string(&path); + let mut attributes = host_object_attributes(&name); + attributes.root_directory = root; + let mut handle = core::ptr::null_mut(); + let mut io_status = IoStatusBlock::default(); + // SAFETY: All pointers reference live local typed values for the call. + let status = unsafe { + NtCreateFile( + &raw mut handle, + FILE_GENERIC_READ | FILE_GENERIC_WRITE, + &raw const attributes, + &raw mut io_status, + core::ptr::null(), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, + FileCreateOptions::SYNCHRONOUS_IO_NONALERT.bits(), + core::ptr::null(), + 0, + ) + }; + (host_status(status), handle) + } + + #[test] + fn connected_console_child_matrix_matches_host() { + // SAFETY: RtlGetCurrentPeb returns the live typed PEB for this process. + let mut peb = unsafe { &*RtlGetCurrentPeb() }; + // SAFETY: The current process owns a live RTL_USER_PROCESS_PARAMETERS block. + let mut process_parameters = + unsafe { &*(peb.process_parameters as *const RtlUserProcessParameters) }; + let console_handle = process_parameters.console_handle; + // ReactOS and Wine model detached/new/no-window console states as null or + // the reserved pseudo-handles -1 through -4. + if console_handle == 0 || console_handle >= usize::MAX - 3 { + // SAFETY: The test process has no connected console, so AllocConsole may attach one. + let allocated = unsafe { AllocConsole() }; + assert_ne!( + allocated, + 0, + "AllocConsole failed with Win32 error {}", + // SAFETY: GetLastError has no preconditions. + unsafe { GetLastError() } + ); + // AllocConsole updates the live process parameters. + // SAFETY: RtlGetCurrentPeb returns the live typed PEB for this process. + peb = unsafe { &*RtlGetCurrentPeb() }; + // SAFETY: The PEB owns a live RTL_USER_PROCESS_PARAMETERS block. + process_parameters = + unsafe { &*(peb.process_parameters as *const RtlUserProcessParameters) }; + } + assert_ne!( + process_parameters.console_handle, 0, + "console handle remained null after ensuring a console" + ); + let console_handle = Handle::from_raw(process_parameters.console_handle); + + let success = [NtStatus::SUCCESS]; + let screen_buffer = [NtStatus::SUCCESS, NtStatus::INVALID_PARAMETER]; + let invalid_handle = [NtStatus::INVALID_HANDLE]; + let not_found = [NtStatus::NOT_FOUND]; + for (name, expected) in [ + (r"\Input", success.as_slice()), + (r"\Output", success.as_slice()), + (r"\CurrentIn", success.as_slice()), + (r"\CurrentOut", success.as_slice()), + // Headless and pseudoconsole hosts may not support creating a bound legacy + // screen buffer even though their connected root supports CurrentOut. + (r"\ScreenBuffer", screen_buffer.as_slice()), + (r"\Server", success.as_slice()), + (r"\Reference", success.as_slice()), + (r"\Connect", invalid_handle.as_slice()), + (r"\Bogus", not_found.as_slice()), + ] { + let (status, handle) = host_create_file(console_handle, name); + assert!( + expected.contains(&status), + "{name:?} under console handle {:#x}: expected one of {expected:?}, got {status:?}", + console_handle.as_raw(), + ); + if status == NtStatus::SUCCESS { + assert!(!handle.is_null()); + close_host_handle(handle); + } else { + assert!(handle.is_null()); + } + } + } + #[test] fn nt_query_volume_information_file_device_information_matches_host_statuses() { let test_dir = test_tmp_dir( From 7bbd90281663f45a47ebbe5619643e83209da0ab Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 18 Jul 2026 15:59:34 -0700 Subject: [PATCH 106/319] Add broker-backed pipes (#1045) This PR adds broker-backed pipes, with the broker owning pipe state and endpoint lifetime. The existing in-process pipe remains available when no broker is configured. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 14 +- litebox/src/broker/mod.rs | 49 +- litebox/src/event/counter.rs | 7 +- litebox/src/pipes.rs | 562 +++++++++++++++++- litebox_broker_core/src/error.rs | 6 + litebox_broker_core/src/event.rs | 16 +- litebox_broker_core/src/lib.rs | 31 +- litebox_broker_core/src/pipe.rs | 226 +++++++ litebox_broker_core/src/session.rs | 159 ++++- litebox_broker_host/src/lib.rs | 57 +- litebox_broker_local/src/event.rs | 20 +- litebox_broker_local/src/lib.rs | 34 +- litebox_broker_local/src/pipe.rs | 84 +++ litebox_broker_protocol/src/error.rs | 8 + litebox_broker_protocol/src/event.rs | 14 - litebox_broker_protocol/src/lib.rs | 1 + litebox_broker_protocol/src/message.rs | 40 +- litebox_broker_protocol/src/pipe.rs | 62 ++ litebox_broker_protocol/src/wire.rs | 79 ++- litebox_broker_protocol/src/wire/event.rs | 26 +- litebox_broker_protocol/src/wire/pipe.rs | 91 +++ litebox_broker_protocol/src/wire/primitive.rs | 15 + .../tests/userland_broker.rs | 7 +- litebox_common_linux/src/errno/mod.rs | 22 + .../tests/pipe_broker.c | 268 +++++++++ litebox_runner_linux_userland/tests/run.rs | 16 +- litebox_shim_linux/src/syscalls/epoll.rs | 9 +- litebox_shim_linux/src/syscalls/pipe.rs | 4 +- 28 files changed, 1776 insertions(+), 151 deletions(-) create mode 100644 litebox_broker_core/src/pipe.rs create mode 100644 litebox_broker_local/src/pipe.rs create mode 100644 litebox_broker_protocol/src/pipe.rs create mode 100644 litebox_broker_protocol/src/wire/pipe.rs create mode 100644 litebox_runner_linux_userland/tests/pipe_broker.c diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index c5497262bb..d5280f075e 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -28,10 +28,14 @@ pub(crate) enum BrokerObjectError { InvalidObject, #[error("broker object operation would block")] WouldBlock, + #[error("broker object peer is closed")] + PeerClosed, #[error("broker object resource exhausted")] ResourceExhausted, #[error("broker object permission denied")] PermissionDenied, + #[error("broker memory allocation failed")] + OutOfMemory, } impl From for BrokerObjectError { @@ -48,8 +52,10 @@ impl From for BrokerObjectError { match error { ErrorCode::InvalidRights | ErrorCode::UnknownObject => Self::InvalidObject, ErrorCode::WouldBlock => Self::WouldBlock, + ErrorCode::PeerClosed => Self::PeerClosed, ErrorCode::ResourceExhausted => Self::ResourceExhausted, ErrorCode::PolicyDenied => Self::PermissionDenied, + ErrorCode::OutOfMemory => Self::OutOfMemory, ErrorCode::UnsupportedVersion | ErrorCode::MalformedRequest | ErrorCode::ProtocolState @@ -82,9 +88,13 @@ impl From for EventCounterError { fn from(error: BrokerObjectError) -> Self { match error { BrokerObjectError::WouldBlock => Self::WouldBlock, - BrokerObjectError::ResourceExhausted => Self::ResourceExhausted, + BrokerObjectError::ResourceExhausted | BrokerObjectError::OutOfMemory => { + Self::ResourceExhausted + } BrokerObjectError::PermissionDenied => Self::PermissionDenied, - BrokerObjectError::Control | BrokerObjectError::InvalidObject => Self::Io, + BrokerObjectError::Control + | BrokerObjectError::InvalidObject + | BrokerObjectError::PeerClosed => Self::Io, } } } diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 42d040bad7..7999cc2517 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -11,6 +11,7 @@ use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode}; +use litebox_broker_protocol::pipe::CreatePipeResponse; use litebox_broker_protocol::readiness::ReadinessFlags; use crate::event::{Events, polling::Pollee}; @@ -35,7 +36,7 @@ pub(crate) trait BrokerControl: Send + Sync { initial_count: u64, ) -> core::result::Result; - fn wait_event( + fn check_readiness( &self, handle: ObjectHandle, ) -> core::result::Result; @@ -52,6 +53,24 @@ pub(crate) trait BrokerControl: Send + Sync { mode: EventConsumeMode, ) -> core::result::Result; + fn create_pipe( + &self, + capacity: u64, + atomic_write_size: u64, + ) -> core::result::Result; + + fn read_pipe( + &self, + handle: ObjectHandle, + length: u32, + ) -> core::result::Result, BrokerControlError>; + + fn write_pipe( + &self, + handle: ObjectHandle, + data: &[u8], + ) -> core::result::Result; + fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError>; fn fail_connection(&self); @@ -185,11 +204,11 @@ where self.request(|local| local.create_event_with_count(initial_count)) } - fn wait_event( + fn check_readiness( &self, handle: ObjectHandle, ) -> core::result::Result { - self.request(|local| local.wait_event(handle)) + self.request(|local| local.check_readiness(handle)) } fn add_event( @@ -208,6 +227,30 @@ where self.request(|local| local.consume_event(handle, mode)) } + fn create_pipe( + &self, + capacity: u64, + atomic_write_size: u64, + ) -> core::result::Result { + self.request(|local| local.create_pipe(capacity, atomic_write_size)) + } + + fn read_pipe( + &self, + handle: ObjectHandle, + length: u32, + ) -> core::result::Result, BrokerControlError> { + self.request(|local| local.read_pipe(handle, length)) + } + + fn write_pipe( + &self, + handle: ObjectHandle, + data: &[u8], + ) -> core::result::Result { + self.request(|local| local.write_pipe(handle, data)) + } + fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError> { self.request(|local| local.close_object(handle)) } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 4ebd50e604..6873cf56c5 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -159,7 +159,7 @@ where fn check_io_events(&self) -> Events { let readiness = match self .broker - .wait_event(self.handle) + .check_readiness(self.handle) .map_err(|error| self.broker_request_error(error)) { Ok(readiness) => readiness, @@ -450,7 +450,10 @@ mod tests { } } BrokerRequest::CloseObject(_) => BrokerResponse::ObjectClosed, - request @ BrokerRequest::Event(_) => { + BrokerRequest::CheckReadiness(_) => { + BrokerResponse::Readiness(ReadinessFlags::WRITE) + } + request @ (BrokerRequest::Event(_) | BrokerRequest::Pipe(_)) => { panic!("unexpected broker request: {request:?}") } }; diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index bc298ea293..8a3d9c1e99 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -12,6 +12,10 @@ use core::{ }; use alloc::sync::{Arc, Weak}; +use either::Either; +use litebox_broker_protocol::{ + ObjectHandle, pipe::MAX_PIPE_TRANSFER_SIZE, readiness::ReadinessFlags, +}; use ringbuf::{ HeapCons, HeapProd, HeapRb, traits::{Consumer as _, Observer as _, Producer as _, Split as _}, @@ -20,6 +24,11 @@ use thiserror::Error; use crate::{ LiteBox, + broker::{ + BrokerControl, BrokerPollableRegistry, + error::{BrokerControlError, BrokerObjectError}, + readiness_events, + }, event::{ Events, IOPollable, observer::Observer, @@ -65,15 +74,31 @@ impl Pipes { capacity: usize, flags: Flags, atomic_slice_guarantee_size: Option, - ) -> (PipeFd, PipeFd) { - let (sender, receiver) = - new_pipe::(capacity, OFlags::from(flags), atomic_slice_guarantee_size); - let sender = PipeEnd::Sender(sender); - let receiver = PipeEnd::Receiver(receiver); + ) -> Result<(PipeFd, PipeFd), errors::CreateError> { + let (sender, receiver) = if let Some(broker) = self.litebox.broker_control() { + let (sender, receiver) = new_broker_pipe( + broker, + self.litebox.broker_pollable_registry(), + capacity, + OFlags::from(flags), + atomic_slice_guarantee_size, + )?; + ( + PipeEnd::BrokerSender(sender), + PipeEnd::BrokerReceiver(receiver), + ) + } else { + let (sender, receiver) = new_pipe::( + capacity, + OFlags::from(flags), + atomic_slice_guarantee_size, + ); + (PipeEnd::Sender(sender), PipeEnd::Receiver(receiver)) + }; let mut dt = self.litebox.descriptor_table_mut(); let sender = dt.insert(sender); let receiver = dt.insert(receiver); - (sender, receiver) + Ok((sender, receiver)) } /// Close the pipe at `fd`. @@ -99,11 +124,17 @@ impl Pipes { ) -> Result { let dt = self.litebox.descriptor_table(); let p = match &dt.get_entry(fd).ok_or(errors::ReadError::ClosedFd)?.entry { - PipeEnd::Receiver(p) => Arc::clone(p), - PipeEnd::Sender(_) => return Err(errors::ReadError::NotForReading), + PipeEnd::Receiver(p) => Either::Left(Arc::clone(p)), + PipeEnd::BrokerReceiver(p) => Either::Right(Arc::clone(p)), + PipeEnd::Sender(_) | PipeEnd::BrokerSender(_) => { + return Err(errors::ReadError::NotForReading); + } }; drop(dt); - p.read(cx, buf).map_err(From::from) + match p { + Either::Left(p) => p.read(cx, buf).map_err(From::from), + Either::Right(p) => p.read(cx, buf).map_err(From::from), + } } /// Write the values in `buf` into the pipe, returning the number of elements written. @@ -117,11 +148,17 @@ impl Pipes { ) -> Result { let dt = self.litebox.descriptor_table(); let p = match &dt.get_entry(fd).ok_or(errors::WriteError::ClosedFd)?.entry { - PipeEnd::Sender(p) => Arc::clone(p), - PipeEnd::Receiver(_) => return Err(errors::WriteError::NotForWriting), + PipeEnd::Sender(p) => Either::Left(Arc::clone(p)), + PipeEnd::BrokerSender(p) => Either::Right(Arc::clone(p)), + PipeEnd::Receiver(_) | PipeEnd::BrokerReceiver(_) => { + return Err(errors::WriteError::NotForWriting); + } }; drop(dt); - p.write(cx, buf).map_err(From::from) + match p { + Either::Left(p) => p.write(cx, buf).map_err(From::from), + Either::Right(p) => p.write(cx, buf).map_err(From::from), + } } /// Whether the provided FD points to a reader or a writer end. @@ -131,8 +168,8 @@ impl Pipes { ) -> Result { let dt = self.litebox.descriptor_table(); match dt.get_entry(fd).ok_or(errors::ClosedError::ClosedFd)?.entry { - PipeEnd::Sender(_) => Ok(HalfPipeType::SenderHalf), - PipeEnd::Receiver(_) => Ok(HalfPipeType::ReceiverHalf), + PipeEnd::Sender(_) | PipeEnd::BrokerSender(_) => Ok(HalfPipeType::SenderHalf), + PipeEnd::Receiver(_) | PipeEnd::BrokerReceiver(_) => Ok(HalfPipeType::ReceiverHalf), } } @@ -142,6 +179,7 @@ impl Pipes { let oflags = match &dt.get_entry(fd).ok_or(errors::ClosedError::ClosedFd)?.entry { PipeEnd::Receiver(p) => p.get_status(), PipeEnd::Sender(p) => p.get_status(), + PipeEnd::BrokerReceiver(p) | PipeEnd::BrokerSender(p) => p.get_status(), }; Ok(Flags::from_oflags_truncate(oflags)) } @@ -159,6 +197,9 @@ impl Pipes { match &dt.get_entry(fd).ok_or(errors::ClosedError::ClosedFd)?.entry { PipeEnd::Receiver(p) => p.set_status(OFlags::from(mask), on), PipeEnd::Sender(p) => p.set_status(OFlags::from(mask), on), + PipeEnd::BrokerReceiver(p) | PipeEnd::BrokerSender(p) => { + p.set_status(OFlags::from(mask), on); + } } Ok(()) } @@ -173,6 +214,7 @@ impl Pipes { match &dt.get_entry(fd).ok_or(errors::ClosedError::ClosedFd)?.entry { PipeEnd::Receiver(p) => Ok(f(p)), PipeEnd::Sender(p) => Ok(f(p)), + PipeEnd::BrokerReceiver(p) | PipeEnd::BrokerSender(p) => Ok(f(p)), } } } @@ -187,6 +229,8 @@ pub enum HalfPipeType { enum PipeEnd { Receiver(Arc>), Sender(Arc>), + BrokerReceiver(Arc>), + BrokerSender(Arc>), } bitflags::bitflags! { @@ -226,6 +270,20 @@ pub mod errors { use thiserror::Error; + /// Possible errors from [`Pipes::create_pipe`]. + #[non_exhaustive] + #[derive(Error, Debug)] + pub enum CreateError { + #[error("pipe resource exhausted")] + ResourceExhausted, + #[error("pipe memory allocation failed")] + OutOfMemory, + #[error("pipe permission denied")] + PermissionDenied, + #[error("pipe broker I/O failed")] + Io, + } + /// Possible errors from [`Pipes::close`] #[non_exhaustive] #[derive(Error, Debug)] @@ -243,6 +301,8 @@ pub mod errors { WouldBlock, #[error("wait error")] WaitError(WaitError), + #[error("pipe I/O failed")] + Io, } /// Possible errors from [`Pipes::write`] @@ -259,6 +319,8 @@ pub mod errors { WouldBlock, #[error("wait error")] WaitError(WaitError), + #[error("pipe I/O failed")] + Io, } /// Possible errors from functions that always succeed unless the descriptor is closed. @@ -269,6 +331,236 @@ pub mod errors { } } +struct BrokerPipeEnd { + broker: Arc, + handle: ObjectHandle, + pollable_registry: Arc>, + pollee: Arc>, + peer: Weak, + endpoint_type: HalfPipeType, + status: AtomicU32, +} + +#[expect( + clippy::type_complexity, + reason = "a type alias would not make the two pipe endpoint result clearer" +)] +fn new_broker_pipe( + broker: Arc, + pollable_registry: Arc>, + capacity: usize, + flags: OFlags, + atomic_slice_guarantee_size: Option, +) -> Result<(Arc>, Arc>), errors::CreateError> { + let atomic_write_size = atomic_slice_guarantee_size + .map(NonZeroUsize::get) + .unwrap_or_default(); + if atomic_write_size > MAX_PIPE_TRANSFER_SIZE as usize { + return Err(errors::CreateError::ResourceExhausted); + } + let response = broker + .create_pipe( + capacity + .try_into() + .map_err(|_| errors::CreateError::ResourceExhausted)?, + atomic_write_size + .try_into() + .map_err(|_| errors::CreateError::ResourceExhausted)?, + ) + .map_err(BrokerObjectError::from) + .map_err(errors::CreateError::from)?; + + let mut writer = Arc::new(BrokerPipeEnd { + broker: Arc::clone(&broker), + handle: response.write_handle, + pollable_registry: Arc::clone(&pollable_registry), + pollee: Arc::new(Pollee::new()), + peer: Weak::new(), + endpoint_type: HalfPipeType::SenderHalf, + status: AtomicU32::new((flags | OFlags::WRONLY).bits()), + }); + let reader = Arc::new_cyclic(|weak_reader| { + Arc::get_mut(&mut writer) + .expect("new pipe writer must be uniquely owned") + .peer = weak_reader.clone(); + BrokerPipeEnd { + broker, + handle: response.read_handle, + pollable_registry: Arc::clone(&pollable_registry), + pollee: Arc::new(Pollee::new()), + peer: Arc::downgrade(&writer), + endpoint_type: HalfPipeType::ReceiverHalf, + status: AtomicU32::new((flags | OFlags::RDONLY).bits()), + } + }); + + pollable_registry.register_pollable(response.write_handle, &writer.pollee); + pollable_registry.register_pollable(response.read_handle, &reader.pollee); + Ok((writer, reader)) +} + +impl BrokerPipeEnd { + fn get_status(&self) -> OFlags { + OFlags::from_bits(self.status.load(Relaxed)).unwrap() & OFlags::STATUS_FLAGS_MASK + } + + fn set_status(&self, mask: OFlags, on: bool) { + if on { + self.status.fetch_or(mask.bits(), Relaxed); + } else { + self.status.fetch_and(mask.complement().bits(), Relaxed); + } + } + + fn read(&self, cx: &WaitContext<'_, Platform>, buf: &mut [u8]) -> Result { + let length = buf.len().min(MAX_PIPE_TRANSFER_SIZE as usize); + if length == 0 { + return Ok(0); + } + let request_length = length + .try_into() + .expect("pipe transfer limit must fit in u32"); + + self.pollee + .wait( + cx, + self.get_status().contains(OFlags::NONBLOCK), + Events::IN, + || { + let data = self + .broker + .read_pipe(self.handle, request_length) + .map_err(|error| self.broker_request_error(error))?; + if data.len() > length { + return Err(TryOpError::Other(PipeError::Io)); + } + buf[..data.len()].copy_from_slice(&data); + if !data.is_empty() + && let Some(peer) = self.peer.upgrade() + { + peer.pollee.notify_observers(Events::OUT); + } + Ok(data.len()) + }, + ) + .map_err(PipeError::from) + } + + fn write(&self, cx: &WaitContext<'_, Platform>, buf: &[u8]) -> Result { + if buf.is_empty() { + return Ok(0); + } + let nonblock = self.get_status().contains(OFlags::NONBLOCK); + if nonblock { + let data = &buf[..buf.len().min(MAX_PIPE_TRANSFER_SIZE as usize)]; + return self + .pollee + .wait(cx, nonblock, Events::OUT, || self.try_write(data)) + .map_err(PipeError::from); + } + + let mut total_written = 0; + while total_written < buf.len() { + let end = total_written + .saturating_add(MAX_PIPE_TRANSFER_SIZE as usize) + .min(buf.len()); + let data = &buf[total_written..end]; + match self + .pollee + .wait(cx, false, Events::OUT, || self.try_write(data)) + { + Ok(written) => total_written += written, + Err(_) if total_written != 0 => return Ok(total_written), + Err(error) => return Err(PipeError::from(error)), + } + } + Ok(total_written) + } + + fn try_write(&self, data: &[u8]) -> Result> { + let written = self + .broker + .write_pipe(self.handle, data) + .map_err(|error| self.broker_request_error(error))?; + if written > data.len() || (written == 0 && !data.is_empty()) { + return Err(TryOpError::Other(PipeError::Io)); + } + if written != 0 + && let Some(peer) = self.peer.upgrade() + { + peer.pollee.notify_observers(Events::IN); + } + Ok(written) + } + + fn broker_request_error(&self, error: BrokerControlError) -> BrokerObjectError { + let error = error.into(); + if error != BrokerObjectError::WouldBlock { + self.pollee.notify_observers(Events::ERR); + } + error + } + + fn readiness(&self) -> Result { + self.broker.check_readiness(self.handle) + } +} + +impl IOPollable for BrokerPipeEnd { + fn register_observer(&self, observer: Weak>, filter: Events) { + self.pollee.register_observer(observer, filter); + } + + fn check_io_events(&self) -> Events { + match self.readiness() { + Ok(readiness) => readiness_events(readiness), + Err(_) => Events::ERR, + } + } +} + +impl Drop for BrokerPipeEnd { + fn drop(&mut self) { + self.pollable_registry.unregister_pollable(self.handle); + let _ = self.broker.close_object(self.handle); + if let Some(peer) = self.peer.upgrade() { + let event = match self.endpoint_type { + HalfPipeType::SenderHalf => Events::HUP, + HalfPipeType::ReceiverHalf => Events::ERR, + }; + peer.pollee.notify_observers(event); + } + } +} + +impl From for TryOpError { + fn from(error: BrokerObjectError) -> Self { + match error { + BrokerObjectError::WouldBlock => TryOpError::TryAgain, + BrokerObjectError::PeerClosed => TryOpError::Other(PipeError::PeerShutdown), + BrokerObjectError::Control + | BrokerObjectError::InvalidObject + | BrokerObjectError::ResourceExhausted + | BrokerObjectError::PermissionDenied + | BrokerObjectError::OutOfMemory => TryOpError::Other(PipeError::Io), + } + } +} + +impl From for errors::CreateError { + fn from(error: BrokerObjectError) -> Self { + match error { + BrokerObjectError::ResourceExhausted => Self::ResourceExhausted, + BrokerObjectError::OutOfMemory => Self::OutOfMemory, + BrokerObjectError::PermissionDenied => Self::PermissionDenied, + BrokerObjectError::Control + | BrokerObjectError::InvalidObject + | BrokerObjectError::WouldBlock + | BrokerObjectError::PeerClosed => Self::Io, + } + } +} + struct EndPointer { rb: Mutex, pollee: Pollee, @@ -351,15 +643,15 @@ enum PipeError { WouldBlock, #[error("wait error")] WaitError(WaitError), + #[error("pipe I/O failed")] + Io, } impl From for errors::ReadError { fn from(err: PipeError) -> Self { match err { PipeError::ThisEndShutdown => errors::ReadError::ClosedFd, - PipeError::PeerShutdown => { - unreachable!("unreachable for now; see documentation of `read`") - } + PipeError::PeerShutdown | PipeError::Io => errors::ReadError::Io, PipeError::WouldBlock => errors::ReadError::WouldBlock, PipeError::WaitError(e) => errors::ReadError::WaitError(e), } @@ -372,6 +664,7 @@ impl From for errors::WriteError { PipeError::PeerShutdown => errors::WriteError::ReadEndClosed, PipeError::WouldBlock => errors::WriteError::WouldBlock, PipeError::WaitError(e) => errors::WriteError::WaitError(e), + PipeError::Io => errors::WriteError::Io, } } } @@ -631,19 +924,163 @@ fn new_pipe( #[cfg(test)] mod tests { + use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + use alloc::sync::Arc; + use litebox_broker_local::BrokerLocal; + use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::error::ErrorCode; + use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, + BrokerResponse, PipeRequest, PipeResponse, ReadinessNotification, + }; + use litebox_broker_protocol::pipe::CreatePipeResponse; + use litebox_broker_protocol::readiness::ReadinessFlags; + use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; + use crate::{ - event::wait::WaitState, + event::{Events, observer::Observer, wait::WaitState}, pipes::errors::{ReadError, WriteError}, }; extern crate std; + #[test] + fn broker_control_failure_notifies_all_pipe_observers() { + let platform = crate::platform::mock::MockPlatform::new(); + let request_count = Arc::new(AtomicUsize::new(0)); + let force_transport = Arc::new(AtomicBool::new(false)); + let local = BrokerLocal::negotiate(FailingPipeChannel { + last_request: None, + request_count: Arc::clone(&request_count), + read_failure: ReadFailure::Transport, + force_transport, + }) + .unwrap(); + let litebox = crate::LiteBox::new_with_broker_local(platform, local); + let pipes = super::Pipes::new(&litebox); + let (writer, reader) = pipes.create_pipe(2, super::Flags::empty(), None).unwrap(); + let writer_observer = Arc::new(ErrorObserver(AtomicBool::new(false))); + let writer_observer_dyn: Arc> = writer_observer.clone(); + pipes + .with_iopollable(&writer, |pollable| { + pollable.register_observer(Arc::downgrade(&writer_observer_dyn), Events::ERR); + }) + .unwrap(); + let reader_observer = Arc::new(ErrorObserver(AtomicBool::new(false))); + let reader_observer_dyn: Arc> = reader_observer.clone(); + pipes + .with_iopollable(&reader, |pollable| { + pollable.register_observer(Arc::downgrade(&reader_observer_dyn), Events::ERR); + }) + .unwrap(); + + let mut value = 0; + assert!(matches!( + pipes.read( + &WaitState::new(platform).context(), + &reader, + core::slice::from_mut(&mut value), + ), + Err(ReadError::Io) + )); + assert_eq!(request_count.load(Ordering::SeqCst), 2); + assert!(writer_observer.0.load(Ordering::SeqCst)); + assert!(reader_observer.0.load(Ordering::SeqCst)); + } + + #[test] + fn broker_failure_wakes_blocked_pipe_read() { + let platform = crate::platform::mock::MockPlatform::new(); + let request_count = Arc::new(AtomicUsize::new(0)); + let force_transport = Arc::new(AtomicBool::new(false)); + let local = BrokerLocal::negotiate(FailingPipeChannel { + last_request: None, + request_count: Arc::clone(&request_count), + read_failure: ReadFailure::WouldBlock, + force_transport: Arc::clone(&force_transport), + }) + .unwrap(); + let litebox = Arc::new(crate::LiteBox::new_with_broker_local(platform, local)); + let pipes = super::Pipes::new(&litebox); + let (writer, reader) = pipes.create_pipe(2, super::Flags::empty(), None).unwrap(); + + let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); + let read_litebox = Arc::clone(&litebox); + let read_thread = std::thread::spawn(move || { + let pipes = super::Pipes::new(&read_litebox); + let mut value = 0; + result_sender + .send(pipes.read( + &WaitState::new(platform).context(), + &reader, + core::slice::from_mut(&mut value), + )) + .unwrap(); + }); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1); + let mut setup_completed = true; + while request_count.load(Ordering::SeqCst) < 3 { + if std::time::Instant::now() >= deadline { + setup_completed = false; + force_transport.store(true, Ordering::SeqCst); + let _ = pipes.close(&writer); + break; + } + std::thread::yield_now(); + } + + if setup_completed { + litebox.dispatch_broker_notification(BrokerNotification::Readiness( + ReadinessNotification { + handle: ObjectHandle(1), + readiness: ReadinessFlags::READ, + }, + )); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1); + while request_count.load(Ordering::SeqCst) < 4 { + if std::time::Instant::now() >= deadline { + setup_completed = false; + force_transport.store(true, Ordering::SeqCst); + let _ = pipes.close(&writer); + break; + } + std::thread::yield_now(); + } + } + + if setup_completed { + litebox.broker_failure_dispatcher()(); + } + + let initial_read_result = result_receiver.recv_timeout(std::time::Duration::from_secs(1)); + let woke_without_cleanup = initial_read_result.is_ok(); + let mut read_result = initial_read_result.ok(); + if read_result.is_none() { + force_transport.store(true, Ordering::SeqCst); + let _ = pipes.close(&writer); + read_result = result_receiver + .recv_timeout(std::time::Duration::from_secs(1)) + .ok(); + } + if read_result.is_some() { + read_thread.join().unwrap(); + } else { + std::process::abort(); + } + + assert!(setup_completed); + assert!(woke_without_cleanup); + assert!(matches!(read_result, Some(Err(ReadError::Io)))); + assert_eq!(request_count.load(Ordering::SeqCst), 4); + } + #[test] fn local_zero_length_write_succeeds_after_reader_closes() { let platform = crate::platform::mock::MockPlatform::new(); let litebox = crate::LiteBox::new(platform); let pipes = super::Pipes::new(&litebox); - let (writer, reader) = pipes.create_pipe(2, super::Flags::empty(), None); + let (writer, reader) = pipes.create_pipe(2, super::Flags::empty(), None).unwrap(); pipes.close(&reader).unwrap(); @@ -656,13 +1093,94 @@ mod tests { pipes.close(&writer).unwrap(); } + struct ErrorObserver(AtomicBool); + + impl Observer for ErrorObserver { + fn on_events(&self, events: &Events) { + if events.contains(Events::ERR) { + self.0.store(true, Ordering::SeqCst); + } + } + } + + #[derive(Debug)] + struct FailingPipeChannel { + last_request: Option, + request_count: Arc, + read_failure: ReadFailure, + force_transport: Arc, + } + + #[derive(Clone, Copy, Debug)] + enum ReadFailure { + Transport, + WouldBlock, + } + + impl LocalControlChannel for FailingPipeChannel { + type Error = (); + + fn send_handshake_request( + &mut self, + _request: &BrokerHandshakeRequest, + ) -> core::result::Result<(), Self::Error> { + Ok(()) + } + + fn recv_handshake_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + })) + } + + fn send_request( + &mut self, + request: &BrokerRequest, + ) -> core::result::Result<(), Self::Error> { + self.last_request = Some(request.clone()); + self.request_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + fn recv_response(&mut self) -> core::result::Result, Self::Error> { + match self.last_request.take().unwrap() { + BrokerRequest::Pipe(PipeRequest::Create(_)) => Ok(Some(BrokerResponse::Pipe( + PipeResponse::Create(CreatePipeResponse { + read_handle: ObjectHandle(1), + write_handle: ObjectHandle(2), + }), + ))), + BrokerRequest::Pipe(PipeRequest::Read(_)) + if self.force_transport.load(Ordering::SeqCst) => + { + Err(()) + } + BrokerRequest::Pipe(PipeRequest::Read(_)) => match self.read_failure { + ReadFailure::Transport => Err(()), + ReadFailure::WouldBlock => { + Ok(Some(BrokerResponse::Error(ErrorCode::WouldBlock))) + } + }, + BrokerRequest::CloseObject(_) => Ok(Some(BrokerResponse::ObjectClosed)), + BrokerRequest::CheckReadiness(_) => { + Ok(Some(BrokerResponse::Readiness(ReadinessFlags::default()))) + } + request @ (BrokerRequest::Pipe(_) | BrokerRequest::Event(_)) => { + panic!("unexpected broker request: {request:?}") + } + } + } + } + #[test] fn test_blocking_channel() { let platform = crate::platform::mock::MockPlatform::new(); let litebox = &crate::LiteBox::new(platform); let pipes = &super::Pipes::new(litebox); - let (prod, cons) = pipes.create_pipe(2, super::Flags::empty(), None); + let (prod, cons) = pipes.create_pipe(2, super::Flags::empty(), None).unwrap(); std::thread::scope(|scope| { scope.spawn(move || { @@ -696,7 +1214,9 @@ mod tests { let litebox = &crate::LiteBox::new(platform); let pipes = &super::Pipes::new(litebox); - let (prod, cons) = pipes.create_pipe(2, super::Flags::NON_BLOCKING, None); + let (prod, cons) = pipes + .create_pipe(2, super::Flags::NON_BLOCKING, None) + .unwrap(); std::thread::scope(|scope| { scope.spawn(move || { diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index cd610a999f..7748d0be0e 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -21,6 +21,10 @@ pub enum BrokerError { BrokerCoreAlreadyExists, #[error("broker operation would block")] WouldBlock, + #[error("broker object peer is closed")] + PeerClosed, + #[error("broker memory allocation failed")] + OutOfMemory, #[error("unsupported broker operation")] UnsupportedOperation, } @@ -34,6 +38,8 @@ impl From for ErrorCode { BrokerError::ResourceExhausted => Self::ResourceExhausted, BrokerError::BrokerCoreAlreadyExists => Self::Internal, BrokerError::WouldBlock => Self::WouldBlock, + BrokerError::PeerClosed => Self::PeerClosed, + BrokerError::OutOfMemory => Self::OutOfMemory, BrokerError::UnsupportedOperation => Self::UnsupportedOperation, } } diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 62218d305e..41b7f31f51 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -20,23 +20,12 @@ pub fn create(session: &BrokerSession, initial_count: u64) -> Result Result { - let required_rights = ObjectRights::WAIT; - session.with_authorized_object(handle, required_rights, |object| match object { - ObjectEntry::Event(event) => Ok(event.readiness()), - }) -} - /// Adds readiness credits to a broker-owned event object. pub fn add(session: &BrokerSession, handle: ObjectHandle, value: u64) -> Result { let required_rights = ObjectRights::WRITE; session.with_authorized_object_mut(handle, required_rights, |object| match object { ObjectEntry::Event(event) => event.add(value), + ObjectEntry::Pipe(_) => Err(BrokerError::InvalidRights), }) } @@ -49,6 +38,7 @@ pub fn consume( let required_rights = ObjectRights::WAIT; session.with_authorized_object_mut(handle, required_rights, |object| match object { ObjectEntry::Event(event) => event.consume(mode), + ObjectEntry::Pipe(_) => Err(BrokerError::InvalidRights), }) } @@ -87,7 +77,7 @@ impl EventObject { }) } - fn readiness(self) -> ReadinessFlags { + pub(crate) fn readiness(self) -> ReadinessFlags { let mut readiness = ReadinessFlags::default(); if self.count > 0 { readiness = readiness | ReadinessFlags::READ; diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index bf0713985e..03edc37c93 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -20,11 +20,12 @@ extern crate std; mod error; pub mod event; +pub mod pipe; mod policy; mod session; use alloc::sync::Arc; -use core::sync::atomic::{AtomicBool, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use hashbrown::HashMap; use litebox_broker_protocol::ObjectHandle; @@ -46,17 +47,23 @@ pub type Result = core::result::Result; pub struct BrokerCoreLimits { /// Maximum live object references. pub max_references: usize, + /// Maximum total capacity in bytes reserved by live pipes. + pub max_total_pipe_capacity: usize, } impl BrokerCoreLimits { /// Conservative default limits for initial broker deployments. pub const DEFAULT: Self = Self { max_references: 4096, + max_total_pipe_capacity: 64 * 1024 * 1024, }; /// Creates a broker core limit set. - pub const fn new(max_references: usize) -> Self { - Self { max_references } + pub const fn new(max_references: usize, max_total_pipe_capacity: usize) -> Self { + Self { + max_references, + max_total_pipe_capacity, + } } } @@ -78,6 +85,7 @@ pub struct BrokerCore { pub(crate) next_session_id: Arc>, pub(crate) next_reference_handle: Arc>, pub(crate) references: Arc>>, + pub(crate) reserved_pipe_capacity: Arc, } static BROKER_CORE_CREATED: AtomicBool = AtomicBool::new(false); @@ -100,6 +108,7 @@ impl BrokerCore { next_session_id: Arc::new(RwLock::new(1)), next_reference_handle: Arc::new(RwLock::new(1)), references: Arc::new(RwLock::new(HashMap::new())), + reserved_pipe_capacity: Arc::new(AtomicUsize::new(0)), }) } @@ -113,6 +122,22 @@ impl BrokerCore { Ok(handle) } + pub(crate) fn allocate_reference_handle_pair(&self) -> Result<(ObjectHandle, ObjectHandle)> { + let mut next_reference_handle = self.next_reference_handle.write(); + let first = ObjectHandle(*next_reference_handle); + let second = ObjectHandle( + first + .0 + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?, + ); + *next_reference_handle = second + .0 + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?; + Ok((first, second)) + } + /// Allocates broker authority state for one authenticated caller session. pub fn create_session(&self, caller_credential: CallerCredential) -> Result { let mut next_session_id = self.next_session_id.write(); diff --git a/litebox_broker_core/src/pipe.rs b/litebox_broker_core/src/pipe.rs new file mode 100644 index 0000000000..4d834ca90f --- /dev/null +++ b/litebox_broker_core/src/pipe.rs @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Broker-owned byte pipe operations. + +use alloc::{collections::VecDeque, sync::Arc, vec::Vec}; +use core::sync::atomic::{AtomicUsize, Ordering}; + +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::pipe::MAX_PIPE_TRANSFER_SIZE; +use litebox_broker_protocol::readiness::ReadinessFlags; +use spin::rwlock::RwLock; + +use crate::session::{ObjectEntry, ObjectRights}; +use crate::{BrokerError, BrokerSession, Result}; + +/// Maximum capacity accepted by the control-path pipe prototype. +pub const MAX_PIPE_CAPACITY: usize = 1024 * 1024; + +/// Creates a broker-owned pipe and returns its read and write endpoint handles. +/// +pub fn create( + session: &BrokerSession, + capacity: u64, + atomic_write_size: u64, +) -> Result<(ObjectHandle, ObjectHandle)> { + let capacity = usize::try_from(capacity).map_err(|_| BrokerError::ResourceExhausted)?; + let atomic_write_size = + usize::try_from(atomic_write_size).map_err(|_| BrokerError::ResourceExhausted)?; + if capacity == 0 + || capacity > MAX_PIPE_CAPACITY + || atomic_write_size > capacity + || atomic_write_size > MAX_PIPE_TRANSFER_SIZE as usize + { + return Err(BrokerError::ResourceExhausted); + } + + let capacity_reservation = PipeCapacityReservation::new(session, capacity)?; + let mut data = VecDeque::new(); + data.try_reserve_exact(capacity) + .map_err(|_| BrokerError::OutOfMemory)?; + let state = Arc::new(RwLock::new(PipeState { + data, + capacity, + atomic_write_size, + read_open: true, + write_open: true, + _capacity_reservation: capacity_reservation, + })); + session.create_object_reference_pair( + ObjectEntry::Pipe(PipeObject::reader(Arc::clone(&state))), + ObjectEntry::Pipe(PipeObject::writer(state)), + ) +} + +/// Reads up to `length` bytes from a broker-owned pipe. +pub fn read(session: &BrokerSession, handle: ObjectHandle, length: u32) -> Result> { + if length > MAX_PIPE_TRANSFER_SIZE { + return Err(BrokerError::ResourceExhausted); + } + session.with_authorized_object(handle, ObjectRights::WAIT, |object| match object { + ObjectEntry::Pipe(pipe) => pipe.read(length as usize), + ObjectEntry::Event(_) => Err(BrokerError::InvalidRights), + }) +} + +/// Writes bytes to a broker-owned pipe. +pub fn write(session: &BrokerSession, handle: ObjectHandle, data: &[u8]) -> Result { + if data.len() > MAX_PIPE_TRANSFER_SIZE as usize { + return Err(BrokerError::ResourceExhausted); + } + session.with_authorized_object(handle, ObjectRights::WRITE, |object| match object { + ObjectEntry::Pipe(pipe) => pipe.write(data), + ObjectEntry::Event(_) => Err(BrokerError::InvalidRights), + }) +} + +pub(crate) struct PipeObject { + state: Arc>, + endpoint: PipeEndpoint, +} + +impl PipeObject { + fn reader(state: Arc>) -> Self { + Self { + state, + endpoint: PipeEndpoint::Read, + } + } + + fn writer(state: Arc>) -> Self { + Self { + state, + endpoint: PipeEndpoint::Write, + } + } + + fn read(&self, length: usize) -> Result> { + if !matches!(self.endpoint, PipeEndpoint::Read) { + return Err(BrokerError::InvalidRights); + } + if length == 0 { + return Ok(Vec::new()); + } + + let mut state = self.state.write(); + if state.data.is_empty() { + return if state.write_open { + Err(BrokerError::WouldBlock) + } else { + Ok(Vec::new()) + }; + } + + let read_len = length.min(state.data.len()); + let mut data = Vec::new(); + data.try_reserve_exact(read_len) + .map_err(|_| BrokerError::OutOfMemory)?; + data.extend(state.data.drain(..read_len)); + Ok(data) + } + + fn write(&self, data: &[u8]) -> Result { + if !matches!(self.endpoint, PipeEndpoint::Write) { + return Err(BrokerError::InvalidRights); + } + if data.is_empty() { + return Ok(0); + } + let mut state = self.state.write(); + if !state.read_open { + return Err(BrokerError::PeerClosed); + } + + let available = state.capacity - state.data.len(); + if available == 0 || (data.len() <= state.atomic_write_size && available < data.len()) { + return Err(BrokerError::WouldBlock); + } + + let write_len = available.min(data.len()); + state.data.extend(&data[..write_len]); + Ok(write_len) + } + + pub(crate) fn readiness(&self) -> ReadinessFlags { + let state = self.state.read(); + match self.endpoint { + PipeEndpoint::Read => { + let mut readiness = ReadinessFlags::default(); + if !state.data.is_empty() { + readiness = readiness | ReadinessFlags::READ; + } + if !state.write_open { + readiness = readiness | ReadinessFlags::HANGUP; + } + readiness + } + PipeEndpoint::Write => { + let mut readiness = ReadinessFlags::default(); + if state.data.len() < state.capacity { + readiness = readiness | ReadinessFlags::WRITE; + } + if !state.read_open { + readiness = readiness | ReadinessFlags::ERROR; + } + readiness + } + } + } +} + +impl Drop for PipeObject { + fn drop(&mut self) { + let mut state = self.state.write(); + match self.endpoint { + PipeEndpoint::Read => state.read_open = false, + PipeEndpoint::Write => state.write_open = false, + } + } +} + +enum PipeEndpoint { + Read, + Write, +} + +struct PipeCapacityReservation { + reserved_capacity: Arc, + capacity: usize, +} + +impl PipeCapacityReservation { + fn new(session: &BrokerSession, capacity: usize) -> Result { + let reserved_capacity = Arc::clone(&session.core.reserved_pipe_capacity); + reserved_capacity + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |reserved| { + reserved + .checked_add(capacity) + .filter(|total| *total <= session.core.limits.max_total_pipe_capacity) + }) + .map_err(|_| BrokerError::ResourceExhausted)?; + Ok(Self { + reserved_capacity, + capacity, + }) + } +} + +impl Drop for PipeCapacityReservation { + fn drop(&mut self) { + self.reserved_capacity + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |reserved| { + reserved.checked_sub(self.capacity) + }) + .expect("reserved pipe capacity must include every live pipe"); + } +} + +struct PipeState { + data: VecDeque, + capacity: usize, + atomic_write_size: usize, + read_open: bool, + write_open: bool, + _capacity_reservation: PipeCapacityReservation, +} diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index cea80b3c81..e64cf8bda3 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -4,9 +4,11 @@ use alloc::sync::Arc; use crate::event::EventObject; +use crate::pipe::PipeObject; use crate::{BrokerCore, BrokerError, Result}; use hashbrown::HashMap; use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::readiness::ReadinessFlags; use spin::rwlock::RwLock; /// Caller identity information supplied by the broker entry layer. @@ -43,9 +45,9 @@ pub(crate) struct ObjectReference { pub(crate) rights: ObjectRights, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum ObjectEntry { Event(EventObject), + Pipe(PipeObject), } /// Broker-owned authority token for one authenticated caller session. @@ -97,6 +99,37 @@ impl BrokerSession { Ok(handle) } + pub(crate) fn create_object_reference_pair( + &self, + first: ObjectEntry, + second: ObjectEntry, + ) -> Result<(ObjectHandle, ObjectHandle)> { + let rights = self + .core + .policy + .principal_object_rights(self.caller_credential)?; + let mut references = self.core.references.write(); + if references + .len() + .checked_add(2) + .is_none_or(|count| count > self.core.limits.max_references) + { + return Err(BrokerError::ResourceExhausted); + } + let (first_handle, second_handle) = self.core.allocate_reference_handle_pair()?; + for (handle, object) in [(first_handle, first), (second_handle, second)] { + references.insert( + handle, + ObjectReference { + object: Arc::new(RwLock::new(object)), + session_id: self.session_id, + rights, + }, + ); + } + Ok((first_handle, second_handle)) + } + pub(crate) fn with_authorized_object( &self, handle: ObjectHandle, @@ -125,6 +158,16 @@ impl BrokerSession { f(&mut object) } + /// Returns the current readiness of a broker-owned object. + pub fn check_readiness(&self, handle: ObjectHandle) -> Result { + self.with_authorized_object(handle, ObjectRights::WAIT, |object| { + Ok(match object { + ObjectEntry::Event(event) => event.readiness(), + ObjectEntry::Pipe(pipe) => pipe.readiness(), + }) + }) + } + fn authorize_use_object( &self, references: &HashMap, @@ -164,6 +207,8 @@ impl Drop for BrokerSession { #[cfg(test)] mod tests { + use core::sync::atomic::Ordering; + use crate::{ BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, ObjectRights, PolicyEngine, }; @@ -175,9 +220,20 @@ mod tests { fn object_reference_lifecycle_uses_public_core_constructor_once() { let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), - BrokerCoreLimits::new(1), + BrokerCoreLimits::new(2, 4), ) .unwrap(); + + check_event_reference_lifecycle(&broker); + check_session_drop_releases_references(&broker); + check_pipe_lifecycle(&broker); + check_pipe_reader_closure(&broker); + check_pair_handle_exhaustion(&broker); + + assert!(broker.references.read().is_empty()); + } + + fn check_event_reference_lifecycle(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); @@ -189,7 +245,7 @@ mod tests { assert_ne!(unknown_handle, handle); assert_eq!( - crate::event::wait(&session, unknown_handle), + session.check_readiness(unknown_handle), Err(BrokerError::UnknownObject) ); @@ -198,10 +254,7 @@ mod tests { Err(BrokerError::UnknownObject) ); - assert_eq!( - crate::event::wait(&session, handle), - Ok(ReadinessFlags::WRITE) - ); + assert_eq!(session.check_readiness(handle), Ok(ReadinessFlags::WRITE)); assert_eq!( crate::event::add(&session, handle, 1), Ok(ReadinessFlags::READ | ReadinessFlags::WRITE) @@ -213,10 +266,17 @@ mod tests { readiness: ReadinessFlags::WRITE, }) ); + let second_handle = crate::event::create(&session, 0).unwrap(); assert_eq!( crate::event::create(&session, 0), Err(BrokerError::ResourceExhausted) ); + assert_eq!( + crate::pipe::create(&session, 4, 2), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + assert_eq!(session.close_object_reference(second_handle), Ok(())); assert_eq!(session.close_object_reference(handle), Ok(())); { @@ -227,7 +287,9 @@ mod tests { session.close_object_reference(handle), Err(BrokerError::UnknownObject) ); + } + fn check_session_drop_releases_references(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); @@ -243,19 +305,96 @@ mod tests { let references = broker.references.read(); assert!(references.is_empty()); } + } + fn check_pipe_lifecycle(broker: &BrokerCore) { + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + assert_eq!( + crate::pipe::create(&session, 5, 2), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + let (reader, writer) = crate::pipe::create(&session, 4, 2).unwrap(); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 4); + assert_eq!( + session.check_readiness(reader), + Ok(ReadinessFlags::default()) + ); + assert_eq!( + crate::pipe::read(&session, reader, 1), + Err(BrokerError::WouldBlock) + ); + assert_eq!(crate::pipe::write(&session, writer, &[1, 2]), Ok(2)); + assert_eq!(crate::pipe::write(&session, writer, &[3, 4, 5]), Ok(2)); + assert_eq!( + crate::pipe::write(&session, writer, &[5]), + Err(BrokerError::WouldBlock) + ); + assert_eq!( + crate::pipe::read(&session, reader, 3), + Ok(std::vec::Vec::from([1, 2, 3])) + ); + assert_eq!(crate::pipe::write(&session, writer, &[5, 6]), Ok(2)); + assert_eq!(session.close_object_reference(writer), Ok(())); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 4); + assert_eq!( + session.check_readiness(reader), + Ok(ReadinessFlags::READ | ReadinessFlags::HANGUP) + ); + assert_eq!( + crate::pipe::read(&session, reader, 4), + Ok(std::vec::Vec::from([4, 5, 6])) + ); + assert_eq!( + crate::pipe::read(&session, reader, 1), + Ok(std::vec::Vec::new()) + ); + assert_eq!(session.close_object_reference(reader), Ok(())); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + } + + fn check_pipe_reader_closure(broker: &BrokerCore) { + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (reader, writer) = crate::pipe::create(&session, 4, 2).unwrap(); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 4); + assert_eq!(session.close_object_reference(reader), Ok(())); + assert_eq!(crate::pipe::write(&session, writer, &[]), Ok(0)); + assert_eq!( + crate::pipe::write(&session, writer, &[1]), + Err(BrokerError::PeerClosed) + ); + assert_eq!( + session.check_readiness(writer), + Ok(ReadinessFlags::WRITE | ReadinessFlags::ERROR) + ); + assert_eq!(session.close_object_reference(writer), Ok(())); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + } + + fn check_pair_handle_exhaustion(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); { let mut next_reference_handle = broker.next_reference_handle.write(); - *next_reference_handle = u64::MAX; + *next_reference_handle = u64::MAX - 1; } + assert_eq!( + crate::pipe::create(&session, 4, 2), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(*broker.next_reference_handle.read(), u64::MAX - 1); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + let handle = crate::event::create(&session, 0).unwrap(); + assert_eq!(handle, ObjectHandle(u64::MAX - 1)); + assert_eq!(session.close_object_reference(handle), Ok(())); assert_eq!( crate::event::create(&session, 0), Err(BrokerError::ResourceExhausted) ); - let references = broker.references.read(); - assert!(references.is_empty()); } } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 56ff111c8d..2cfd154a88 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -18,10 +18,12 @@ use litebox_broker_protocol::channel::{ HostControlChannel, HostNotificationChannel, HostReceive, PeerCredential, }; use litebox_broker_protocol::error::ErrorCode; -use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse, WaitEventResponse}; +use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse}; use litebox_broker_protocol::message::{ BrokerHandshakeResponse, BrokerRequest, BrokerResponse, EventRequest, EventResponse, + PipeRequest, PipeResponse, }; +use litebox_broker_protocol::pipe::{CreatePipeResponse, ReadPipeResponse, WritePipeResponse}; mod error; @@ -118,7 +120,46 @@ fn handle_request(session: &BrokerSession, request: BrokerRequest) -> BrokerResp Ok(()) => BrokerResponse::ObjectClosed, Err(error) => BrokerResponse::Error(error.into()), }, + BrokerRequest::CheckReadiness(handle) => match session.check_readiness(handle) { + Ok(readiness) => BrokerResponse::Readiness(readiness), + Err(error) => BrokerResponse::Error(error.into()), + }, BrokerRequest::Event(request) => handle_event_request(session, request), + BrokerRequest::Pipe(request) => handle_pipe_request(session, request), + } +} + +fn handle_pipe_request(session: &BrokerSession, request: PipeRequest) -> BrokerResponse { + let response = match request { + PipeRequest::Create(request) => { + litebox_broker_core::pipe::create(session, request.capacity, request.atomic_write_size) + .map(|(read_handle, write_handle)| { + PipeResponse::Create(CreatePipeResponse { + read_handle, + write_handle, + }) + }) + } + PipeRequest::Read(request) => { + litebox_broker_core::pipe::read(session, request.handle, request.length) + .map(|data| PipeResponse::Read(ReadPipeResponse { data })) + } + PipeRequest::Write(request) => { + litebox_broker_core::pipe::write(session, request.handle, &request.data).and_then( + |written| { + Ok(PipeResponse::Write(WritePipeResponse { + written: written + .try_into() + .map_err(|_| litebox_broker_core::BrokerError::ResourceExhausted)?, + })) + }, + ) + } + }; + + match response { + Ok(response) => BrokerResponse::Pipe(response), + Err(error) => BrokerResponse::Error(error.into()), } } @@ -132,14 +173,6 @@ fn handle_event_request(session: &BrokerSession, request: EventRequest) -> Broke Err(error) => BrokerResponse::Error(error.into()), } } - EventRequest::Wait(request) => { - match litebox_broker_core::event::wait(session, request.handle) { - Ok(readiness) => { - BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { readiness })) - } - Err(error) => BrokerResponse::Error(error.into()), - } - } EventRequest::Add(request) => { match litebox_broker_core::event::add(session, request.handle, request.value) { Ok(readiness) => { @@ -173,7 +206,6 @@ mod tests { use litebox_broker_core::{ObjectRights, PolicyEngine}; use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, CreateEventRequest, EventConsumeMode, - WaitEventRequest, }; use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerNotification}; use litebox_broker_protocol::{ObjectHandle, ProtocolVersion}; @@ -370,10 +402,7 @@ mod tests { BrokerResponse::ObjectClosed ); assert_eq!( - handle_request( - &session, - BrokerRequest::Event(EventRequest::Wait(WaitEventRequest { handle })) - ), + handle_request(&session, BrokerRequest::CheckReadiness(handle)), BrokerResponse::Error(ErrorCode::UnknownObject) ); assert_eq!( diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 431cd4e17e..0985ff6b0b 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -5,7 +5,7 @@ use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, - EventConsumeMode, WaitEventRequest, + EventConsumeMode, }; use litebox_broker_protocol::message::{ BrokerRequest, BrokerResponse, EventRequest, EventResponse, @@ -33,20 +33,6 @@ impl BrokerLocal { } } - /// Checks whether an event wait would complete now. - /// - /// # Panics - /// - /// Panics if the broker reports an unrecoverable error or returns a protocol - /// response that does not match the issued event request. - pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { - let response = self.request_event(EventRequest::Wait(WaitEventRequest { handle }))?; - match response { - EventResponse::Wait(response) => Ok(response.readiness), - response => panic!("broker returned unexpected event response: {response:?}"), - } - } - /// Adds readiness credits to a broker-owned event object. /// /// # Panics @@ -88,7 +74,9 @@ impl BrokerLocal { match self.request(BrokerRequest::Event(request))? { BrokerResponse::Event(response) => Ok(response), BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response @ BrokerResponse::ObjectClosed => { + response @ (BrokerResponse::ObjectClosed + | BrokerResponse::Readiness(_) + | BrokerResponse::Pipe(_)) => { panic!("broker returned unexpected event response: {response:?}"); } } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 11f165f857..9f652c1595 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -11,11 +11,14 @@ #![no_std] +extern crate alloc; + #[cfg(test)] extern crate std; mod error; mod event; +mod pipe; use litebox_broker_protocol::channel::{LocalControlChannel, LocalNotificationChannel}; use litebox_broker_protocol::error::ErrorCode; @@ -23,6 +26,7 @@ use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, BrokerResponse, }; +use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; pub use error::{BrokerLocalError, Result}; @@ -103,7 +107,9 @@ impl BrokerLocal { | ErrorCode::UnknownObject | ErrorCode::InvalidRights | ErrorCode::ResourceExhausted - | ErrorCode::WouldBlock => Err(BrokerLocalError::Broker(error)), + | ErrorCode::WouldBlock + | ErrorCode::PeerClosed + | ErrorCode::OutOfMemory => Err(BrokerLocalError::Broker(error)), ErrorCode::UnsupportedVersion | ErrorCode::MalformedRequest | ErrorCode::ProtocolState @@ -111,7 +117,27 @@ impl BrokerLocal { | ErrorCode::Internal => panic!("broker returned unrecoverable error: {error}"), _ => panic!("broker returned unsupported error: {error}"), }, - response @ (BrokerResponse::Event(_) | BrokerResponse::ObjectClosed) => Ok(response), + response @ (BrokerResponse::Event(_) + | BrokerResponse::Pipe(_) + | BrokerResponse::ObjectClosed + | BrokerResponse::Readiness(_)) => Ok(response), + } + } + + /// Checks the current readiness of a broker-owned object. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a + /// response that does not match the issued readiness request. + pub fn check_readiness( + &mut self, + handle: ObjectHandle, + ) -> Result { + match self.request(BrokerRequest::CheckReadiness(handle))? { + BrokerResponse::Readiness(readiness) => Ok(readiness), + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response => panic!("broker returned unexpected readiness response: {response:?}"), } } @@ -125,7 +151,9 @@ impl BrokerLocal { match self.request(BrokerRequest::CloseObject(handle))? { BrokerResponse::ObjectClosed => Ok(()), BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response @ BrokerResponse::Event(_) => { + response @ (BrokerResponse::Event(_) + | BrokerResponse::Pipe(_) + | BrokerResponse::Readiness(_)) => { panic!("broker returned unexpected close response: {response:?}"); } } diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs new file mode 100644 index 0000000000..ed21ad73e1 --- /dev/null +++ b/litebox_broker_local/src/pipe.rs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::vec::Vec; + +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse, PipeRequest, PipeResponse}; +use litebox_broker_protocol::pipe::{ + CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, WritePipeRequest, +}; + +use crate::{BrokerLocal, BrokerLocalError, Result}; + +impl BrokerLocal { + /// Creates a broker-owned byte pipe. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a + /// response that does not match the issued pipe request. + pub fn create_pipe( + &mut self, + capacity: u64, + atomic_write_size: u64, + ) -> Result { + match self.request_pipe(PipeRequest::Create(CreatePipeRequest { + capacity, + atomic_write_size, + }))? { + PipeResponse::Create(response) => Ok(response), + response => panic!("broker returned unexpected pipe response: {response:?}"), + } + } + + /// Reads bytes from a broker-owned pipe. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a + /// response that does not match the issued pipe request. + pub fn read_pipe( + &mut self, + handle: ObjectHandle, + length: u32, + ) -> Result, Channel::Error> { + match self.request_pipe(PipeRequest::Read(ReadPipeRequest { handle, length }))? { + PipeResponse::Read(response) => Ok(response.data), + response => panic!("broker returned unexpected pipe response: {response:?}"), + } + } + + /// Writes bytes to a broker-owned pipe. + /// + /// # Panics + /// + /// Panics if the broker reports an unrecoverable error or returns a + /// response that does not match the issued pipe request. + pub fn write_pipe( + &mut self, + handle: ObjectHandle, + data: &[u8], + ) -> Result { + match self.request_pipe(PipeRequest::Write(WritePipeRequest { + handle, + data: data.to_vec(), + }))? { + PipeResponse::Write(response) => Ok(response.written as usize), + response => panic!("broker returned unexpected pipe response: {response:?}"), + } + } + + fn request_pipe(&mut self, request: PipeRequest) -> Result { + match self.request(BrokerRequest::Pipe(request))? { + BrokerResponse::Pipe(response) => Ok(response), + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response @ (BrokerResponse::ObjectClosed + | BrokerResponse::Readiness(_) + | BrokerResponse::Event(_)) => { + panic!("broker returned unexpected pipe response: {response:?}"); + } + } + } +} diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index ae94cd0e0a..891ea3924c 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -27,6 +27,10 @@ pub enum ErrorCode { ResourceExhausted, #[error("broker operation would block")] WouldBlock, + #[error("broker object peer is closed")] + PeerClosed, + #[error("broker memory allocation failed")] + OutOfMemory, } impl ErrorCode { @@ -48,6 +52,8 @@ impl ErrorCode { 8 => Some(Self::InvalidRights), 9 => Some(Self::ResourceExhausted), 10 => Some(Self::WouldBlock), + 11 => Some(Self::PeerClosed), + 12 => Some(Self::OutOfMemory), _ => None, } } @@ -65,6 +71,8 @@ impl ErrorCode { Self::InvalidRights => 8, Self::ResourceExhausted => 9, Self::WouldBlock => 10, + Self::PeerClosed => 11, + Self::OutOfMemory => 12, } } } diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs index b54df3eeff..08535da2a7 100644 --- a/litebox_broker_protocol/src/event.rs +++ b/litebox_broker_protocol/src/event.rs @@ -27,20 +27,6 @@ pub struct CreateEventResponse { pub handle: ObjectHandle, } -/// Request to check whether an event wait would complete now. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WaitEventRequest { - /// Event handle. - pub handle: ObjectHandle, -} - -/// Response to an event wait request. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WaitEventResponse { - /// Current readiness state. - pub readiness: ReadinessFlags, -} - /// Request to add readiness credits to an event. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct AddEventRequest { diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index b8c684a94a..582960e22f 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -16,6 +16,7 @@ pub mod channel; pub mod error; pub mod event; pub mod message; +pub mod pipe; pub mod readiness; pub mod wire; diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index b46c802999..78afa826be 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -4,7 +4,11 @@ use crate::error::ErrorCode; use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, - CreateEventRequest, CreateEventResponse, WaitEventRequest, WaitEventResponse, + CreateEventRequest, CreateEventResponse, +}; +use crate::pipe::{ + CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, + WritePipeResponse, }; use crate::readiness::ReadinessFlags; use crate::{ObjectHandle, ProtocolVersion}; @@ -21,8 +25,12 @@ pub struct BrokerHandshakeRequest { pub enum BrokerRequest { /// Close one broker object reference. CloseObject(ObjectHandle), + /// Check the current readiness of a broker-owned object. + CheckReadiness(ObjectHandle), /// Event object request family. Event(EventRequest), + /// Pipe object request family. + Pipe(PipeRequest), } /// Broker handshake response sent before the control channel is active. @@ -54,21 +62,34 @@ pub enum BrokerHandshakeResponse { pub enum EventRequest { /// Create a broker-owned event object. Create(CreateEventRequest), - /// Check whether an event wait would complete now. - Wait(WaitEventRequest), /// Add readiness credits to an event. Add(AddEventRequest), /// Consume readiness credits from an event. Consume(ConsumeEventRequest), } +/// Broker-owned pipe object request. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PipeRequest { + /// Create a broker-owned byte pipe. + Create(CreatePipeRequest), + /// Read bytes from a pipe. + Read(ReadPipeRequest), + /// Write bytes to a pipe. + Write(WritePipeRequest), +} + /// Broker response sent over an active control channel. #[derive(Clone, Debug, PartialEq, Eq)] pub enum BrokerResponse { /// Object close operation completed. ObjectClosed, + /// Current readiness of a broker-owned object. + Readiness(ReadinessFlags), /// Event object response family. Event(EventResponse), + /// Pipe object response family. + Pipe(PipeResponse), /// Operation failed with an ABI-neutral broker error. Error(ErrorCode), } @@ -78,14 +99,23 @@ pub enum BrokerResponse { pub enum EventResponse { /// Create operation response. Create(CreateEventResponse), - /// Wait operation response. - Wait(WaitEventResponse), /// Add operation response. Add(AddEventResponse), /// Consume operation response. Consume(ConsumeEventResponse), } +/// Broker-owned pipe object response. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PipeResponse { + /// Create operation response. + Create(CreatePipeResponse), + /// Read operation response. + Read(ReadPipeResponse), + /// Write operation response. + Write(WritePipeResponse), +} + /// Broker-initiated asynchronous notification. /// /// Notifications are level-triggered snapshots and may be coalesced or diff --git a/litebox_broker_protocol/src/pipe.rs b/litebox_broker_protocol/src/pipe.rs new file mode 100644 index 0000000000..8e812a8b03 --- /dev/null +++ b/litebox_broker_protocol/src/pipe.rs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::vec::Vec; + +use crate::ObjectHandle; + +/// Maximum pipe payload carried by one control-path request or response. +/// +/// This leaves room for the broker envelope and operation metadata within the +/// smallest currently supported transport frame. +pub const MAX_PIPE_TRANSFER_SIZE: u32 = 32 * 1024; + +/// Request to create a broker-owned byte pipe. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CreatePipeRequest { + /// Maximum number of buffered bytes. + pub capacity: u64, + /// Maximum write size that must be accepted atomically. + pub atomic_write_size: u64, +} + +/// Response to a pipe create request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CreatePipeResponse { + /// Handle for the read endpoint. + pub read_handle: ObjectHandle, + /// Handle for the write endpoint. + pub write_handle: ObjectHandle, +} + +/// Request to read bytes from a pipe endpoint. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ReadPipeRequest { + /// Read endpoint handle. + pub handle: ObjectHandle, + /// Maximum number of bytes to return. + pub length: u32, +} + +/// Response containing bytes read from a pipe. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReadPipeResponse { + /// Bytes removed from the pipe. + pub data: Vec, +} + +/// Request to write bytes to a pipe endpoint. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct WritePipeRequest { + /// Write endpoint handle. + pub handle: ObjectHandle, + /// Bytes to append to the pipe. + pub data: Vec, +} + +/// Response describing a completed pipe write. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WritePipeResponse { + /// Number of bytes appended to the pipe. + pub written: u32, +} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 7efc2caabe..00935d94b4 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -28,17 +28,22 @@ use crate::readiness::ReadinessFlags; use primitive::{Decoder, Encoder}; mod event; +mod pipe; mod primitive; const REQUEST_TAG_NEGOTIATE: u8 = 0; const REQUEST_TAG_EVENT: u8 = 1; const REQUEST_TAG_CLOSE_OBJECT: u8 = 2; +const REQUEST_TAG_PIPE: u8 = 3; +const REQUEST_TAG_CHECK_READINESS: u8 = 4; const RESPONSE_TAG_NEGOTIATED: u8 = 0; const RESPONSE_TAG_EVENT: u8 = 1; const RESPONSE_TAG_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; const RESPONSE_TAG_OBJECT_CLOSED: u8 = 4; +const RESPONSE_TAG_PIPE: u8 = 5; +const RESPONSE_TAG_READINESS: u8 = 6; const NOTIFICATION_TAG_READINESS: u8 = 0; @@ -77,7 +82,12 @@ pub fn decode_handshake_request(frame: &[u8]) -> Result BrokerHandshakeRequest { protocol_version: decoder.protocol_version()?, }, - REQUEST_TAG_EVENT | REQUEST_TAG_CLOSE_OBJECT => return Err(WireError::WrongMessagePhase), + REQUEST_TAG_EVENT + | REQUEST_TAG_CLOSE_OBJECT + | REQUEST_TAG_PIPE + | REQUEST_TAG_CHECK_READINESS => { + return Err(WireError::WrongMessagePhase); + } _ => return Err(WireError::InvalidTag), }; decoder.finish()?; @@ -95,10 +105,18 @@ pub fn encode_request(request: BrokerRequest) -> Vec { encoder.u8(REQUEST_TAG_CLOSE_OBJECT); encoder.handle(handle); } + BrokerRequest::CheckReadiness(handle) => { + encoder.u8(REQUEST_TAG_CHECK_READINESS); + encoder.handle(handle); + } BrokerRequest::Event(request) => { encoder.u8(REQUEST_TAG_EVENT); event::encode_event_request(&mut encoder, request); } + BrokerRequest::Pipe(request) => { + encoder.u8(REQUEST_TAG_PIPE); + pipe::encode_pipe_request(&mut encoder, request); + } } encoder.finish() } @@ -110,7 +128,9 @@ pub fn decode_request(frame: &[u8]) -> Result { let request = match tag { REQUEST_TAG_NEGOTIATE => return Err(WireError::WrongMessagePhase), REQUEST_TAG_CLOSE_OBJECT => BrokerRequest::CloseObject(decoder.handle()?), + REQUEST_TAG_CHECK_READINESS => BrokerRequest::CheckReadiness(decoder.handle()?), REQUEST_TAG_EVENT => BrokerRequest::Event(event::decode_event_request(&mut decoder)?), + REQUEST_TAG_PIPE => BrokerRequest::Pipe(pipe::decode_pipe_request(&mut decoder)?), _ => return Err(WireError::InvalidTag), }; decoder.finish()?; @@ -152,7 +172,10 @@ pub fn decode_handshake_response(frame: &[u8]) -> Result BrokerHandshakeResponse::Negotiated { broker_protocol_version: decoder.protocol_version()?, }, - RESPONSE_TAG_EVENT | RESPONSE_TAG_OBJECT_CLOSED => { + RESPONSE_TAG_EVENT + | RESPONSE_TAG_OBJECT_CLOSED + | RESPONSE_TAG_PIPE + | RESPONSE_TAG_READINESS => { return Err(WireError::WrongMessagePhase); } RESPONSE_TAG_VERSION_MISMATCH => BrokerHandshakeResponse::VersionMismatch { @@ -178,10 +201,18 @@ pub fn encode_response(response: BrokerResponse) -> Vec { BrokerResponse::ObjectClosed => { encoder.u8(RESPONSE_TAG_OBJECT_CLOSED); } + BrokerResponse::Readiness(readiness) => { + encoder.u8(RESPONSE_TAG_READINESS); + encoder.u32(readiness.0); + } BrokerResponse::Event(response) => { encoder.u8(RESPONSE_TAG_EVENT); event::encode_event_response(&mut encoder, response); } + BrokerResponse::Pipe(response) => { + encoder.u8(RESPONSE_TAG_PIPE); + pipe::encode_pipe_response(&mut encoder, response); + } BrokerResponse::Error(error) => { encoder.u8(RESPONSE_TAG_ERROR); encoder.u16(error.as_raw()); @@ -199,11 +230,13 @@ pub fn decode_response(frame: &[u8]) -> Result { return Err(WireError::WrongMessagePhase); } RESPONSE_TAG_EVENT => BrokerResponse::Event(event::decode_event_response(&mut decoder)?), + RESPONSE_TAG_PIPE => BrokerResponse::Pipe(pipe::decode_pipe_response(&mut decoder)?), RESPONSE_TAG_ERROR => { let error = ErrorCode::from_raw(decoder.u16()?).ok_or(WireError::InvalidTag)?; BrokerResponse::Error(error) } RESPONSE_TAG_OBJECT_CLOSED => BrokerResponse::ObjectClosed, + RESPONSE_TAG_READINESS => BrokerResponse::Readiness(ReadinessFlags(decoder.u32()?)), _ => return Err(WireError::InvalidTag), }; decoder.finish()?; @@ -246,10 +279,13 @@ mod tests { use super::*; use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, - CreateEventResponse, EventConsumeMode, EventConsumption, WaitEventRequest, - WaitEventResponse, + CreateEventResponse, EventConsumeMode, EventConsumption, + }; + use crate::message::{EventRequest, EventResponse, PipeRequest, PipeResponse}; + use crate::pipe::{ + CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, + WritePipeResponse, }; - use crate::message::{EventRequest, EventResponse}; use crate::{ObjectHandle, ProtocolVersion}; #[test] @@ -271,13 +307,13 @@ mod tests { let handle = ObjectHandle(13); let requests = [ BrokerRequest::CloseObject(handle), + BrokerRequest::CheckReadiness(handle), BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, })), BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 7, })), - BrokerRequest::Event(EventRequest::Wait(WaitEventRequest { handle })), BrokerRequest::Event(EventRequest::Add(AddEventRequest { handle, value: 3 })), BrokerRequest::Event(EventRequest::Consume(ConsumeEventRequest { handle, @@ -287,6 +323,15 @@ mod tests { handle, mode: EventConsumeMode::One, })), + BrokerRequest::Pipe(PipeRequest::Create(CreatePipeRequest { + capacity: 4096, + atomic_write_size: 512, + })), + BrokerRequest::Pipe(PipeRequest::Read(ReadPipeRequest { handle, length: 32 })), + BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { + handle, + data: Vec::from([1, 2, 3]), + })), ]; for request in requests { @@ -323,13 +368,9 @@ mod tests { let handle = ObjectHandle(13); let responses = [ BrokerResponse::ObjectClosed, + BrokerResponse::Readiness(ReadinessFlags::READ), + BrokerResponse::Readiness(ReadinessFlags::WRITE), BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })), - BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { - readiness: ReadinessFlags::READ, - })), - BrokerResponse::Event(EventResponse::Wait(WaitEventResponse { - readiness: ReadinessFlags::WRITE, - })), BrokerResponse::Event(EventResponse::Add(AddEventResponse { readiness: ReadinessFlags::READ | ReadinessFlags::WRITE, })), @@ -337,8 +378,18 @@ mod tests { value: 3, readiness: ReadinessFlags::WRITE, })), + BrokerResponse::Pipe(PipeResponse::Create(CreatePipeResponse { + read_handle: handle, + write_handle: ObjectHandle(14), + })), + BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { + data: Vec::from([1, 2, 3]), + })), + BrokerResponse::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })), BrokerResponse::Error(ErrorCode::PolicyDenied), BrokerResponse::Error(ErrorCode::WouldBlock), + BrokerResponse::Error(ErrorCode::PeerClosed), + BrokerResponse::Error(ErrorCode::OutOfMemory), BrokerResponse::Error(ErrorCode::Internal), ]; @@ -477,7 +528,7 @@ mod tests { Err(WireError::WrongMessagePhase) ); assert_eq!( - decode_response(&[1, 1, 0xff]), + decode_response(&[RESPONSE_TAG_READINESS, 0xff]), Err(WireError::TruncatedFrame) ); assert_eq!( @@ -539,7 +590,7 @@ mod tests { readiness: ReadinessFlags::READ, } ))), - [1, 2, 1, 0, 0, 0] + [1, 1, 1, 0, 0, 0] ); } diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index 6010a81b0e..8babbcb9b0 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -3,7 +3,7 @@ use crate::event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, - CreateEventResponse, EventConsumeMode, EventConsumption, WaitEventRequest, WaitEventResponse, + CreateEventResponse, EventConsumeMode, EventConsumption, }; use crate::message::{EventRequest, EventResponse}; use crate::readiness::ReadinessFlags; @@ -14,14 +14,12 @@ use super::primitive::{Decoder, Encoder}; // Event operation tags live with the event family. Future event operations // should add tags here; unrelated object families should get their own module. const EVENT_REQUEST_TAG_CREATE: u8 = 0; -const EVENT_REQUEST_TAG_WAIT: u8 = 1; -const EVENT_REQUEST_TAG_ADD: u8 = 2; -const EVENT_REQUEST_TAG_CONSUME: u8 = 3; +const EVENT_REQUEST_TAG_ADD: u8 = 1; +const EVENT_REQUEST_TAG_CONSUME: u8 = 2; const EVENT_RESPONSE_TAG_CREATED: u8 = 0; -const EVENT_RESPONSE_TAG_WAITED: u8 = 1; -const EVENT_RESPONSE_TAG_ADDED: u8 = 2; -const EVENT_RESPONSE_TAG_CONSUMED: u8 = 3; +const EVENT_RESPONSE_TAG_ADDED: u8 = 1; +const EVENT_RESPONSE_TAG_CONSUMED: u8 = 2; const EVENT_CONSUME_MODE_TAG_ALL: u8 = 1; const EVENT_CONSUME_MODE_TAG_ONE: u8 = 2; @@ -32,10 +30,6 @@ pub(super) fn encode_event_request(encoder: &mut Encoder, request: EventRequest) encoder.u8(EVENT_REQUEST_TAG_CREATE); encoder.u64(request.initial_count); } - EventRequest::Wait(request) => { - encoder.u8(EVENT_REQUEST_TAG_WAIT); - encoder.handle(request.handle); - } EventRequest::Add(request) => { encoder.u8(EVENT_REQUEST_TAG_ADD); encoder.handle(request.handle); @@ -54,9 +48,6 @@ pub(super) fn decode_event_request(decoder: &mut Decoder<'_>) -> Result EventRequest::Create(CreateEventRequest { initial_count: decoder.u64()?, }), - EVENT_REQUEST_TAG_WAIT => EventRequest::Wait(WaitEventRequest { - handle: decoder.handle()?, - }), EVENT_REQUEST_TAG_ADD => EventRequest::Add(AddEventRequest { handle: decoder.handle()?, value: decoder.u64()?, @@ -77,10 +68,6 @@ pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventRespon encoder.u8(EVENT_RESPONSE_TAG_CREATED); encoder.handle(response.handle); } - EventResponse::Wait(response) => { - encoder.u8(EVENT_RESPONSE_TAG_WAITED); - encoder.u32(response.readiness.0); - } EventResponse::Add(response) => { encoder.u8(EVENT_RESPONSE_TAG_ADDED); encoder.u32(response.readiness.0); @@ -98,9 +85,6 @@ pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result EventResponse::Create(CreateEventResponse { handle: decoder.handle()?, }), - EVENT_RESPONSE_TAG_WAITED => EventResponse::Wait(WaitEventResponse { - readiness: ReadinessFlags(decoder.u32()?), - }), EVENT_RESPONSE_TAG_ADDED => EventResponse::Add(AddEventResponse { readiness: ReadinessFlags(decoder.u32()?), }), diff --git a/litebox_broker_protocol/src/wire/pipe.rs b/litebox_broker_protocol/src/wire/pipe.rs new file mode 100644 index 0000000000..1ef8e0fee3 --- /dev/null +++ b/litebox_broker_protocol/src/wire/pipe.rs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::message::{PipeRequest, PipeResponse}; +use crate::pipe::{ + CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, + WritePipeResponse, +}; + +use super::WireError; +use super::primitive::{Decoder, Encoder}; + +const PIPE_REQUEST_TAG_CREATE: u8 = 0; +const PIPE_REQUEST_TAG_READ: u8 = 1; +const PIPE_REQUEST_TAG_WRITE: u8 = 2; + +const PIPE_RESPONSE_TAG_CREATED: u8 = 0; +const PIPE_RESPONSE_TAG_READ: u8 = 1; +const PIPE_RESPONSE_TAG_WRITTEN: u8 = 2; + +pub(super) fn encode_pipe_request(encoder: &mut Encoder, request: PipeRequest) { + match request { + PipeRequest::Create(request) => { + encoder.u8(PIPE_REQUEST_TAG_CREATE); + encoder.u64(request.capacity); + encoder.u64(request.atomic_write_size); + } + PipeRequest::Read(request) => { + encoder.u8(PIPE_REQUEST_TAG_READ); + encoder.handle(request.handle); + encoder.u32(request.length); + } + PipeRequest::Write(request) => { + encoder.u8(PIPE_REQUEST_TAG_WRITE); + encoder.handle(request.handle); + encoder.bytes(&request.data); + } + } +} + +pub(super) fn decode_pipe_request(decoder: &mut Decoder<'_>) -> Result { + match decoder.u8()? { + PIPE_REQUEST_TAG_CREATE => Ok(PipeRequest::Create(CreatePipeRequest { + capacity: decoder.u64()?, + atomic_write_size: decoder.u64()?, + })), + PIPE_REQUEST_TAG_READ => Ok(PipeRequest::Read(ReadPipeRequest { + handle: decoder.handle()?, + length: decoder.u32()?, + })), + PIPE_REQUEST_TAG_WRITE => Ok(PipeRequest::Write(WritePipeRequest { + handle: decoder.handle()?, + data: decoder.bytes()?, + })), + _ => Err(WireError::InvalidTag), + } +} + +pub(super) fn encode_pipe_response(encoder: &mut Encoder, response: PipeResponse) { + match response { + PipeResponse::Create(response) => { + encoder.u8(PIPE_RESPONSE_TAG_CREATED); + encoder.handle(response.read_handle); + encoder.handle(response.write_handle); + } + PipeResponse::Read(response) => { + encoder.u8(PIPE_RESPONSE_TAG_READ); + encoder.bytes(&response.data); + } + PipeResponse::Write(response) => { + encoder.u8(PIPE_RESPONSE_TAG_WRITTEN); + encoder.u32(response.written); + } + } +} + +pub(super) fn decode_pipe_response(decoder: &mut Decoder<'_>) -> Result { + match decoder.u8()? { + PIPE_RESPONSE_TAG_CREATED => Ok(PipeResponse::Create(CreatePipeResponse { + read_handle: decoder.handle()?, + write_handle: decoder.handle()?, + })), + PIPE_RESPONSE_TAG_READ => Ok(PipeResponse::Read(ReadPipeResponse { + data: decoder.bytes()?, + })), + PIPE_RESPONSE_TAG_WRITTEN => Ok(PipeResponse::Write(WritePipeResponse { + written: decoder.u32()?, + })), + _ => Err(WireError::InvalidTag), + } +} diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs index 617f411f7f..e06e73dc9a 100644 --- a/litebox_broker_protocol/src/wire/primitive.rs +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -33,6 +33,16 @@ impl Encoder { self.bytes.extend_from_slice(&value.to_le_bytes()); } + pub(super) fn bytes(&mut self, value: &[u8]) { + self.u64( + value + .len() + .try_into() + .expect("broker byte payload length exceeds u64"), + ); + self.bytes.extend_from_slice(value); + } + pub(super) fn protocol_version(&mut self, version: ProtocolVersion) { self.u16(version.0); } @@ -82,6 +92,11 @@ impl<'a> Decoder<'a> { ])) } + pub(super) fn bytes(&mut self) -> Result, WireError> { + let len = usize::try_from(self.u64()?).map_err(|_| WireError::OffsetOverflow)?; + Ok(self.take(len)?.to_vec()) + } + pub(super) fn protocol_version(&mut self) -> Result { Ok(ProtocolVersion(self.u16()?)) } diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 9c5b9492e1..89b89b6c1e 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -84,13 +84,16 @@ fn run_fake_runner(args: &[OsString]) { let mut local = BrokerLocal::negotiate(control_channel).unwrap(); let handle = local.create_event_with_count(0).unwrap(); - assert_eq!(local.wait_event(handle).unwrap(), ReadinessFlags::WRITE); + assert_eq!( + local.check_readiness(handle).unwrap(), + ReadinessFlags::WRITE + ); let readiness = ReadinessFlags::READ | ReadinessFlags::WRITE; assert_eq!(local.add_event(handle, 1).unwrap(), readiness); assert_eq!( - local.wait_event(handle).unwrap(), + local.check_readiness(handle).unwrap(), ReadinessFlags::READ | ReadinessFlags::WRITE ); drop(local); diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 4a828008f1..0022c6dcf4 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -579,6 +579,11 @@ impl From for Errno { litebox::pipes::errors::ReadError::ClosedFd | litebox::pipes::errors::ReadError::NotForReading => Errno::EBADF, litebox::pipes::errors::ReadError::WouldBlock => Errno::EWOULDBLOCK, + litebox::pipes::errors::ReadError::WaitError(e) => match e { + litebox::event::wait::WaitError::Interrupted => Errno::EINTR, + litebox::event::wait::WaitError::TimedOut => Errno::ETIMEDOUT, + }, + litebox::pipes::errors::ReadError::Io => Errno::EIO, _ => todo!(), } } @@ -591,6 +596,23 @@ impl From for Errno { litebox::pipes::errors::WriteError::ReadEndClosed => Errno::EPIPE, litebox::pipes::errors::WriteError::NotForWriting => Errno::EBADF, litebox::pipes::errors::WriteError::WouldBlock => Errno::EWOULDBLOCK, + litebox::pipes::errors::WriteError::WaitError(e) => match e { + litebox::event::wait::WaitError::Interrupted => Errno::EINTR, + litebox::event::wait::WaitError::TimedOut => Errno::ETIMEDOUT, + }, + litebox::pipes::errors::WriteError::Io => Errno::EIO, + _ => todo!(), + } + } +} + +impl From for Errno { + fn from(value: litebox::pipes::errors::CreateError) -> Self { + match value { + litebox::pipes::errors::CreateError::ResourceExhausted => Errno::ENFILE, + litebox::pipes::errors::CreateError::OutOfMemory => Errno::ENOMEM, + litebox::pipes::errors::CreateError::PermissionDenied => Errno::EACCES, + litebox::pipes::errors::CreateError::Io => Errno::EIO, _ => todo!(), } } diff --git a/litebox_runner_linux_userland/tests/pipe_broker.c b/litebox_runner_linux_userland/tests/pipe_broker.c new file mode 100644 index 0000000000..35e640968f --- /dev/null +++ b/litebox_runner_linux_userland/tests/pipe_broker.c @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BLOCKING_TEST_DELAY_US 50000 +#define OPERATION_TIMEOUT_SECONDS 2 +#define THREAD_JOIN_TIMEOUT_SECONDS 2 + +struct io_thread_args { + int fd; + unsigned char value; + int write; + int result; + atomic_int started; + atomic_int completed; +}; + +static void *io_thread(void *arg) { + struct io_thread_args *args = arg; + unsigned char value = args->value; + atomic_store_explicit(&args->started, 1, memory_order_release); + ssize_t result = args->write ? write(args->fd, &value, 1) + : read(args->fd, &value, 1); + args->result = result == 1 && (args->write || value == args->value) ? 0 : 1; + atomic_store_explicit(&args->completed, 1, memory_order_release); + return NULL; +} + +static int poll_events(int fd, short events, short expected) { + struct pollfd poll_fd = { + .fd = fd, + .events = events, + }; + int ready = poll(&poll_fd, 1, 0); + return ready >= 0 && poll_fd.revents == expected ? 0 : 1; +} + +static int join_thread(pthread_t thread, int wake_fd) { + struct timespec deadline; + if (clock_gettime(CLOCK_REALTIME, &deadline) == 0) { + deadline.tv_sec += THREAD_JOIN_TIMEOUT_SECONDS; + if (pthread_timedjoin_np(thread, NULL, &deadline) == 0) { + return 0; + } + } + close(wake_fd); + _exit(1); +} + +static int test_nonblocking_and_lifecycle(void) { + int fds[2]; + if (pipe2(fds, O_NONBLOCK | O_CLOEXEC) != 0) { + return 1; + } + int read_status = fcntl(fds[0], F_GETFL); + int write_status = fcntl(fds[1], F_GETFL); + int read_descriptor_flags = fcntl(fds[0], F_GETFD); + int write_descriptor_flags = fcntl(fds[1], F_GETFD); + if (read_status < 0 || write_status < 0 || read_descriptor_flags < 0 || + write_descriptor_flags < 0 || (read_status & O_NONBLOCK) == 0 || + (write_status & O_NONBLOCK) == 0 || + (read_descriptor_flags & FD_CLOEXEC) == 0 || + (write_descriptor_flags & FD_CLOEXEC) == 0) { + return 2; + } + if (poll_events(fds[0], POLLIN, 0) != 0 || + poll_events(fds[1], POLLOUT, POLLOUT) != 0) { + return 3; + } + + unsigned char data[3] = {1, 2, 3}; + unsigned char output[3] = {0}; + errno = 0; + if (read(fds[1], output, 0) != -1 || errno != EBADF) { + return 4; + } + errno = 0; + if (write(fds[0], data, 0) != -1 || errno != EBADF) { + return 4; + } + errno = 0; + if (read(fds[0], output, sizeof(output)) != -1 || errno != EAGAIN) { + return 5; + } + if (write(fds[1], data, sizeof(data)) != sizeof(data) || + poll_events(fds[0], POLLIN, POLLIN) != 0 || + read(fds[0], output, sizeof(output)) != sizeof(output) || + memcmp(data, output, sizeof(data)) != 0) { + return 6; + } + + int duplicate = dup(fds[1]); + if (duplicate < 0 || close(fds[1]) != 0 || + write(duplicate, data, sizeof(data)) != sizeof(data) || + read(fds[0], output, sizeof(output)) != sizeof(output)) { + return 7; + } + return close(duplicate) == 0 && close(fds[0]) == 0 ? 0 : 8; +} + +static int test_blocking_read_wakeup(void) { + int fds[2]; + if (pipe(fds) != 0) { + return 1; + } + struct io_thread_args args = { + .fd = fds[0], + .value = 42, + .write = 0, + .result = -1, + }; + atomic_init(&args.started, 0); + atomic_init(&args.completed, 0); + pthread_t thread; + if (pthread_create(&thread, NULL, io_thread, &args) != 0) { + return 2; + } + alarm(OPERATION_TIMEOUT_SECONDS); + while (!atomic_load_explicit(&args.started, memory_order_acquire)) { + sched_yield(); + } + alarm(0); + usleep(BLOCKING_TEST_DELAY_US); + if (atomic_load_explicit(&args.completed, memory_order_acquire)) { + pthread_join(thread, NULL); + return 3; + } + unsigned char value = 42; + alarm(OPERATION_TIMEOUT_SECONDS); + ssize_t wake_result = write(fds[1], &value, 1); + alarm(0); + int join_result = join_thread(thread, fds[1]); + if (wake_result != 1 || join_result != 0 || args.result != 0) { + return 3; + } + + unsigned char input[65536]; + unsigned char output[65536]; + memset(input, 0x5a, sizeof(input)); + alarm(OPERATION_TIMEOUT_SECONDS); + ssize_t large_write_result = write(fds[1], input, sizeof(input)); + alarm(0); + if (large_write_result != sizeof(input)) { + return 4; + } + size_t read_size = 0; + alarm(OPERATION_TIMEOUT_SECONDS); + while (read_size < sizeof(output)) { + ssize_t size = + read(fds[0], output + read_size, sizeof(output) - read_size); + if (size <= 0) { + return 5; + } + read_size += (size_t)size; + } + alarm(0); + if (memcmp(input, output, sizeof(input)) != 0) { + return 6; + } + return close(fds[0]) == 0 && close(fds[1]) == 0 ? 0 : 7; +} + +static int test_blocking_write_wakeup(void) { + int fds[2]; + if (pipe2(fds, O_NONBLOCK) != 0) { + return 1; + } + unsigned char data[4096] = {0}; + size_t total_written = 0; + ssize_t write_result; + alarm(OPERATION_TIMEOUT_SECONDS); + while ((write_result = write(fds[1], data, sizeof(data))) == sizeof(data)) { + total_written += sizeof(data); + if (total_written > 65536) { + return 2; + } + } + alarm(0); + if (total_written != 65536 || write_result != -1 || errno != EAGAIN || + fcntl(fds[1], F_SETFL, 0) != 0) { + return 2; + } + + struct io_thread_args args = { + .fd = fds[1], + .value = 7, + .write = 1, + .result = -1, + }; + atomic_init(&args.started, 0); + atomic_init(&args.completed, 0); + pthread_t thread; + if (pthread_create(&thread, NULL, io_thread, &args) != 0) { + return 3; + } + alarm(OPERATION_TIMEOUT_SECONDS); + while (!atomic_load_explicit(&args.started, memory_order_acquire)) { + sched_yield(); + } + alarm(0); + usleep(BLOCKING_TEST_DELAY_US); + if (atomic_load_explicit(&args.completed, memory_order_acquire)) { + pthread_join(thread, NULL); + return 4; + } + unsigned char value; + alarm(OPERATION_TIMEOUT_SECONDS); + ssize_t wake_result = read(fds[0], &value, 1); + alarm(0); + int join_result = join_thread(thread, fds[0]); + if (wake_result != 1 || join_result != 0 || args.result != 0) { + return 4; + } + return close(fds[0]) == 0 && close(fds[1]) == 0 ? 0 : 5; +} + +static int test_closed_peers(void) { + int fds[2]; + unsigned char value = 1; + if (pipe(fds) != 0 || close(fds[1]) != 0 || + poll_events(fds[0], POLLIN, POLLHUP) != 0 || + read(fds[0], &value, 1) != 0 || close(fds[0]) != 0) { + return 1; + } + + if (signal(SIGPIPE, SIG_IGN) == SIG_ERR || pipe(fds) != 0 || + close(fds[0]) != 0 || + poll_events(fds[1], POLLOUT, POLLOUT | POLLERR) != 0) { + return 2; + } + if (write(fds[1], &value, 0) != 0) { + return 3; + } + errno = 0; + if (write(fds[1], &value, 1) != -1 || errno != EPIPE) { + return 4; + } + return close(fds[1]) == 0 ? 0 : 5; +} + +int main(void) { + int result = test_nonblocking_and_lifecycle(); + if (result != 0) { + return 10 + result; + } + result = test_blocking_read_wakeup(); + if (result != 0) { + return 20 + result; + } + result = test_blocking_write_wakeup(); + if (result != 0) { + return 30 + result; + } + result = test_closed_peers(); + return result == 0 ? 0 : 40 + result; +} diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 895dcf22bc..a0fac43672 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -10,7 +10,7 @@ use std::{ }; const BROKER_HELPER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); -const BROKER_ONLY_C_TESTS: &[&str] = &["eventfd.c"]; +const BROKER_ONLY_C_TESTS: &[&str] = &["eventfd.c", "pipe_broker.c"]; #[must_use] struct Runner { @@ -473,6 +473,12 @@ console.log(content); let true_path = run_which("true"); let node_path = run_which("node"); let target = common::compile("./tests/eventfd.c", "broker_eventfd_rewriter", false, false); + let pipe_target = common::compile( + "./tests/pipe_broker.c", + "broker_pipe_rewriter", + false, + false, + ); let control_socket_path = unique_test_socket_path("runner-broker-control"); let notification_socket_path = unique_test_socket_path("runner-broker-notification"); let broker_thread = spawn_test_broker( @@ -481,7 +487,7 @@ console.log(content); litebox_broker_core::PolicyEngine::with_unauthenticated_rights( litebox_broker_core::ObjectRights::all(), ), - 3, + 4, ); Runner::new(&true_path, "broker_true_rewriter") @@ -495,6 +501,12 @@ console.log(content); // eventfd.c creates thirteen eventfd objects; each should release one broker object. assert_eq!(broker_thread.next_close_object_count(), 13); + Runner::new(&pipe_target, "broker_pipe_rewriter") + .broker_sockets(&control_socket_path, ¬ification_socket_path) + .run(); + // pipe_broker.c creates five pipes; each endpoint owns one broker object. + assert_eq!(broker_thread.next_close_object_count(), 10); + Runner::new(&node_path, "hello_node_broker_rewriter") .broker_sockets(&control_socket_path, ¬ification_socket_path) .arg("/out/hello_world.js") diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 5366683dff..4cf57cba2e 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -635,10 +635,11 @@ mod test { #[test] fn test_epoll_with_pipe() { let (task, epoll) = setup_epoll(); - let (producer, consumer) = - task.global - .pipes - .create_pipe(2, litebox::pipes::Flags::empty(), None); + let (producer, consumer) = task + .global + .pipes + .create_pipe(2, litebox::pipes::Flags::empty(), None) + .unwrap(); let consumer = Arc::new(consumer); let reader = super::EpollDescriptor::Pipe(Arc::clone(&consumer)); epoll diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs index 90273c164d..06bc92cdd4 100644 --- a/litebox_shim_linux/src/syscalls/pipe.rs +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -19,7 +19,7 @@ use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; use crate::{GlobalState, Platform, ShimFS}; -const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; +const DEFAULT_PIPE_BUF_SIZE: usize = 64 * 1024; /// Status flags for Linux pipe file descriptions. /// @@ -58,7 +58,7 @@ impl GlobalState { pipe_flags, // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. NonZero::new(4096), - ); + )?; let initial_status = OFlags::from(pipe_flags); { From a9530c34d0ac76e6412893dcad4d98f7842664e4 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sun, 19 Jul 2026 11:12:53 -0700 Subject: [PATCH 107/319] Add broker shared-memory primitives (#1049) This PR adds a transport-neutral byte-copy shared-memory interface. It includes a Linux memfd implementation with immutable-size seals and synchronized access. It does not change broker protocol or pipe behavior. --- Cargo.lock | 2 + litebox_broker_protocol/src/lib.rs | 1 + litebox_broker_protocol/src/shared_memory.rs | 40 +++ litebox_broker_transport/Cargo.toml | 9 + litebox_broker_transport/src/lib.rs | 6 + litebox_broker_transport/src/shared_memory.rs | 239 ++++++++++++++++++ litebox_broker_userland/Cargo.toml | 2 +- litebox_runner_linux_userland/Cargo.toml | 2 +- 8 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 litebox_broker_protocol/src/shared_memory.rs create mode 100644 litebox_broker_transport/src/shared_memory.rs diff --git a/Cargo.lock b/Cargo.lock index fc2e643e37..702f5c2df1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1515,7 +1515,9 @@ dependencies = [ name = "litebox_broker_transport" version = "0.1.0" dependencies = [ + "libc", "litebox_broker_protocol", + "rustix", ] [[package]] diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 582960e22f..07ee191085 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -18,6 +18,7 @@ pub mod event; pub mod message; pub mod pipe; pub mod readiness; +pub mod shared_memory; pub mod wire; /// Opaque broker object reference handle. diff --git a/litebox_broker_protocol/src/shared_memory.rs b/litebox_broker_protocol/src/shared_memory.rs new file mode 100644 index 0000000000..db44b96759 --- /dev/null +++ b/litebox_broker_protocol/src/shared_memory.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Transport-neutral shared-memory resources. + +use thiserror::Error; + +/// Error accessing a shared-memory resource. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum SharedMemoryError { + /// The requested byte range is outside the shared-memory resource. + #[error("shared-memory range is out of bounds")] + InvalidRange, +} + +/// Byte-copy access to a shared-memory resource. +/// +/// A value may own a distinct shared-memory object or identify a region in a +/// larger shared-memory arena. Implementations must keep the backing resource +/// alive and make concurrent calls within the local process safe without +/// exposing Rust references into memory writable by another process. +/// +/// Coordination with other processes is the responsibility of the protocol +/// using the shared memory. +pub trait SharedMemory: Send + Sync + 'static { + /// Returns the mapped resource length in bytes. + fn len(&self) -> usize; + + /// Returns whether the resource is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Copies bytes from shared memory into `destination`. + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError>; + + /// Copies bytes from `source` into shared memory. + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError>; +} diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml index da5909d569..31cf39c20a 100644 --- a/litebox_broker_transport/Cargo.toml +++ b/litebox_broker_transport/Cargo.toml @@ -3,8 +3,17 @@ name = "litebox_broker_transport" version = "0.1.0" edition = "2024" +[features] +std = [] +linux-shared-memory = ["std", "dep:libc", "dep:rustix", "rustix/fs"] +unix = ["std"] + [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +[target.'cfg(target_os = "linux")'.dependencies] +libc = { version = "0.2.177", default-features = false, optional = true } +rustix = { version = "1.1.2", default-features = false, features = ["std"], optional = true } + [lints] workspace = true diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs index 9603907abc..111c84ebd1 100644 --- a/litebox_broker_transport/src/lib.rs +++ b/litebox_broker_transport/src/lib.rs @@ -1,10 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +#![cfg_attr(not(feature = "std"), no_std)] + //! Broker transport implementations. //! //! Transports own hosted or platform-specific framing and I/O. Portable broker //! protocol messages, local-side adapters, host-side request handling, and core //! authority state live in separate crates. +#[cfg(all(feature = "linux-shared-memory", target_os = "linux"))] +pub mod shared_memory; + +#[cfg(all(feature = "unix", unix))] pub mod unix_socket; diff --git a/litebox_broker_transport/src/shared_memory.rs b/litebox_broker_transport/src/shared_memory.rs new file mode 100644 index 0000000000..d194d1536e --- /dev/null +++ b/litebox_broker_transport/src/shared_memory.rs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Reusable Linux memfd-backed shared memory. + +use std::io::{Error, Result as IoResult}; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; +use std::ptr::NonNull; +use std::sync::Mutex; + +use rustix::fs::{ + MemfdFlags, SealFlags, fcntl_add_seals, fcntl_get_seals, fstat, ftruncate, memfd_create, +}; + +use litebox_broker_protocol::shared_memory::{SharedMemory, SharedMemoryError}; + +const REQUIRED_MEMFD_SEALS: SealFlags = SealFlags::from_bits_retain( + SealFlags::GROW.bits() | SealFlags::SHRINK.bits() | SealFlags::SEAL.bits(), +); + +/// Linux memfd-backed shared memory usable by broker transports. +pub struct MemfdSharedMemory { + fd: OwnedFd, + mapping: Mutex, +} + +struct MappedRegion { + address: NonNull, + length: usize, +} + +// SAFETY: `MappedRegion` exclusively owns its mapping, and all byte access is +// serialized by the enclosing `Mutex`. +unsafe impl Send for MappedRegion {} + +impl MemfdSharedMemory { + /// Creates and maps a sealed memfd with `length` bytes. + pub fn create(length: usize) -> IoResult { + if length == 0 { + return Err(invalid_data("shared memory cannot be empty")); + } + let fd = memfd_create( + "litebox-broker-shm", + MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING, + )?; + ftruncate( + &fd, + length + .try_into() + .map_err(|_| invalid_data("shared-memory length exceeds u64"))?, + )?; + fcntl_add_seals(&fd, REQUIRED_MEMFD_SEALS)?; + Self::map(fd, length) + } + + /// Validates and maps a received memfd with `expected_length` bytes. + /// + /// The descriptor must have the expected nonzero size sealed against + /// changes. + pub fn from_received_fd(fd: OwnedFd, expected_length: usize) -> IoResult { + if expected_length == 0 { + return Err(invalid_data("shared memory cannot be empty")); + } + // Verify the size seals before reading the size so it cannot change + // between validation and mapping. + let seals = fcntl_get_seals(&fd)?; + if !seals.contains(REQUIRED_MEMFD_SEALS) { + return Err(invalid_data("shared-memory size is not sealed")); + } + let length = usize::try_from(fstat(&fd)?.st_size) + .map_err(|_| invalid_data("invalid shared-memory length"))?; + if length != expected_length { + return Err(invalid_data( + "shared-memory length does not match expected size", + )); + } + Self::map(fd, length) + } + + fn map(fd: OwnedFd, length: usize) -> IoResult { + if length > isize::MAX as usize { + return Err(invalid_data( + "shared-memory length exceeds pointer offset range", + )); + } + // SAFETY: `fd` refers to a file at least `length` bytes long. The + // returned mapping is checked against `MAP_FAILED` and owned by + // `MappedRegion`. + let address = unsafe { + libc::mmap( + std::ptr::null_mut(), + length, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd.as_raw_fd(), + 0, + ) + }; + if address == libc::MAP_FAILED { + return Err(Error::last_os_error()); + } + let address = + NonNull::new(address.cast()).ok_or_else(|| invalid_data("mmap returned null"))?; + Ok(Self { + fd, + mapping: Mutex::new(MappedRegion { address, length }), + }) + } +} + +impl AsFd for MemfdSharedMemory { + fn as_fd(&self) -> BorrowedFd<'_> { + self.fd.as_fd() + } +} + +impl SharedMemory for MemfdSharedMemory { + fn len(&self) -> usize { + self.mapping + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .length + } + + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + let mapping = self + .mapping + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + offset + .checked_add(destination.len()) + .filter(|end| *end <= mapping.length) + .ok_or(SharedMemoryError::InvalidRange)?; + // SAFETY: The range was checked against the live mapping, + // `destination` is valid for its full length, and no Rust reference is + // created for the byte-addressed shared mapping. + unsafe { + libc::memcpy( + destination.as_mut_ptr().cast(), + mapping.address.as_ptr().add(offset).cast(), + destination.len(), + ); + } + Ok(()) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let mapping = self + .mapping + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + offset + .checked_add(source.len()) + .filter(|end| *end <= mapping.length) + .ok_or(SharedMemoryError::InvalidRange)?; + // SAFETY: The range was checked against the live mapping, `source` is + // valid for its full length, and no Rust reference is created for the + // byte-addressed shared mapping. + unsafe { + libc::memcpy( + mapping.address.as_ptr().add(offset).cast(), + source.as_ptr().cast(), + source.len(), + ); + } + Ok(()) + } +} + +impl Drop for MappedRegion { + fn drop(&mut self) { + // SAFETY: `address` and `length` describe the mapping exclusively owned + // by this value, and it is unmapped exactly once here. + let result = unsafe { libc::munmap(self.address.as_ptr().cast(), self.length) }; + debug_assert_eq!(result, 0, "failed to unmap broker shared memory"); + } +} + +fn invalid_data(message: &'static str) -> Error { + Error::new(std::io::ErrorKind::InvalidData, message) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mappings_share_bytes_and_validate_ranges() { + let first = MemfdSharedMemory::create(64).unwrap(); + let second = + MemfdSharedMemory::from_received_fd(first.as_fd().try_clone_to_owned().unwrap(), 64) + .unwrap(); + + first.write(0, &[1, 2, 3]).unwrap(); + let mut data = [0; 3]; + second.read(0, &mut data).unwrap(); + assert_eq!(data, [1, 2, 3]); + + assert_eq!( + second.write(63, &[1, 2]), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.read(usize::MAX, &mut data), + Err(SharedMemoryError::InvalidRange) + ); + } + + #[test] + fn rejects_unsealed_mismatched_and_oversized_mappings() { + let fd = memfd_create("litebox-broker-shm-test", MemfdFlags::CLOEXEC).unwrap(); + ftruncate(&fd, 1).unwrap(); + assert_eq!( + MemfdSharedMemory::from_received_fd(fd, 1) + .err() + .expect("unsealed memfd should fail") + .kind(), + std::io::ErrorKind::InvalidData + ); + + let memory = MemfdSharedMemory::create(64).unwrap(); + assert_eq!( + MemfdSharedMemory::from_received_fd(memory.as_fd().try_clone_to_owned().unwrap(), 32,) + .err() + .expect("mismatched memfd size should fail") + .kind(), + std::io::ErrorKind::InvalidData + ); + + let fd = memfd_create("litebox-broker-shm-test", MemfdFlags::CLOEXEC).unwrap(); + assert_eq!( + MemfdSharedMemory::map(fd, isize::MAX as usize + 1) + .err() + .expect("oversized mapping should fail") + .kind(), + std::io::ErrorKind::InvalidData + ); + } +} diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index e9253548ff..bcb95def82 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" clap = { version = "4.5.33", features = ["derive"] } litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0", features = ["unix"] } tempfile = { version = "3", default-features = false } [[bin]] diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 25c47f8a3d..6ead05a5ac 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -10,7 +10,7 @@ libc = { version = "0.2.169", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } -litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } +litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport", features = ["unix"] } 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_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_linux_userland"] } From 8019c72a2e0dfe8c23e5017d96f355d159231c8c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 21 Jul 2026 08:51:00 -0700 Subject: [PATCH 108/319] Add Unix memfd transfer helpers (#1055) ## Summary Add `send_memfd` and `receive_memfd` to the existing Linux shared-memory transport for exchanging one exact-size sealed memfd over a connected Unix stream. The exchange rejects malformed ancillary data, applies close-on-exec, validates the expected size, and enforces absolute setup deadlines without altering socket timeouts. ## Validation - `cargo nextest run -p litebox_broker_protocol -p litebox_broker_transport --all-features` - strict clippy for protocol and transport across all targets/features - protocol and transport no-std/feature checks - `cargo check --all-targets` --- dev_tests/src/ratchet.rs | 1 + litebox_broker_protocol/src/shared_memory.rs | 15 +- litebox_broker_transport/Cargo.toml | 2 +- litebox_broker_transport/src/shared_memory.rs | 308 ++++++++++++++++++ 4 files changed, 320 insertions(+), 6 deletions(-) diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index b1ec3e9a45..c4aa6d2851 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -72,6 +72,7 @@ fn ratchet_maybe_uninit() -> Result<()> { &[ ("dev_tests/", 1), ("litebox/", 1), + ("litebox_broker_transport/", 3), ("litebox_platform_linux_userland/", 2), ], |file| { diff --git a/litebox_broker_protocol/src/shared_memory.rs b/litebox_broker_protocol/src/shared_memory.rs index db44b96759..96b67df2db 100644 --- a/litebox_broker_protocol/src/shared_memory.rs +++ b/litebox_broker_protocol/src/shared_memory.rs @@ -17,14 +17,19 @@ pub enum SharedMemoryError { /// Byte-copy access to a shared-memory resource. /// /// A value may own a distinct shared-memory object or identify a region in a -/// larger shared-memory arena. Implementations must keep the backing resource -/// alive and make concurrent calls within the local process safe without -/// exposing Rust references into memory writable by another process. +/// larger shared-memory resource. Each endpoint has its own implementation, and +/// peers may use different implementation types, such as user and kernel +/// mappings of the same physical memory. Implementations must keep the backing +/// resource alive and make concurrent local calls safe without exposing Rust +/// references into memory writable by the peer. /// -/// Coordination with other processes is the responsibility of the protocol -/// using the shared memory. +/// Establishing the shared resource and coordinating access between endpoints +/// are responsibilities of the deployment and protocol using the shared +/// memory. pub trait SharedMemory: Send + Sync + 'static { /// Returns the mapped resource length in bytes. + /// + /// The length must remain stable for the lifetime of the resource. fn len(&self) -> usize; /// Returns whether the resource is empty. diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml index 31cf39c20a..151e4bfea5 100644 --- a/litebox_broker_transport/Cargo.toml +++ b/litebox_broker_transport/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [features] std = [] -linux-shared-memory = ["std", "dep:libc", "dep:rustix", "rustix/fs"] +linux-shared-memory = ["std", "dep:libc", "dep:rustix", "rustix/fs", "rustix/net"] unix = ["std"] [dependencies] diff --git a/litebox_broker_transport/src/shared_memory.rs b/litebox_broker_transport/src/shared_memory.rs index d194d1536e..58f5b057c2 100644 --- a/litebox_broker_transport/src/shared_memory.rs +++ b/litebox_broker_transport/src/shared_memory.rs @@ -4,13 +4,21 @@ //! Reusable Linux memfd-backed shared memory. use std::io::{Error, Result as IoResult}; +use std::io::{ErrorKind, IoSlice, IoSliceMut}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; +use std::os::unix::net::UnixStream; use std::ptr::NonNull; use std::sync::Mutex; +use std::time::{Duration, Instant}; use rustix::fs::{ MemfdFlags, SealFlags, fcntl_add_seals, fcntl_get_seals, fstat, ftruncate, memfd_create, }; +use rustix::io::Errno; +use rustix::net::{ + RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags, SendAncillaryBuffer, + SendAncillaryMessage, SendFlags, +}; use litebox_broker_protocol::shared_memory::{SharedMemory, SharedMemoryError}; @@ -167,6 +175,172 @@ impl SharedMemory for MemfdSharedMemory { } } +/// Sends one memfd-backed shared-memory resource over an exclusively owned +/// connected Unix stream. +/// +/// `deadline` bounds setup I/O without leaving a changed socket timeout behind. +pub fn send_memfd( + stream: &mut UnixStream, + memory: &MemfdSharedMemory, + deadline: Option, +) -> IoResult<()> { + with_write_deadline(stream, deadline, |stream, deadline| { + send_fd(stream, memory.as_fd(), deadline) + }) +} + +/// Receives, validates, and maps one memfd-backed shared-memory resource. +/// +/// `expected_length` supplies the trusted expected size. `deadline` bounds +/// setup I/O without leaving a changed socket timeout behind. +pub fn receive_memfd( + stream: &mut UnixStream, + expected_length: usize, + deadline: Option, +) -> IoResult { + let fd = with_read_deadline(stream, deadline, receive_fd)?; + MemfdSharedMemory::from_received_fd(fd, expected_length) +} + +fn send_fd(stream: &mut UnixStream, fd: BorrowedFd<'_>, deadline: Option) -> IoResult<()> { + // Unix streams require an ordinary data byte to carry ancillary data. + let carrier = [0]; + let io = [IoSlice::new(&carrier)]; + let fds = [fd]; + let mut control_space = [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))]; + let mut control = SendAncillaryBuffer::new(&mut control_space); + assert!( + control.push(SendAncillaryMessage::ScmRights(&fds)), + "SCM_RIGHTS control buffer is correctly sized" + ); + loop { + refresh_write_deadline(stream, deadline)?; + match rustix::net::sendmsg(stream.as_fd(), &io, &mut control, SendFlags::NOSIGNAL) { + Ok(1) => return Ok(()), + Ok(0) => { + return Err(Error::new( + ErrorKind::WriteZero, + "failed to send shared-memory descriptor", + )); + } + Ok(_) => return Err(invalid_data("oversized shared-memory setup write")), + Err(Errno::INTR) => {} + Err(error) => return Err(error.into()), + } + } +} + +fn receive_fd(stream: &mut UnixStream, deadline: Option) -> IoResult { + let mut carrier = [0]; + let mut io = [IoSliceMut::new(&mut carrier)]; + let mut control_space = [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(4))]; + let mut control = RecvAncillaryBuffer::new(&mut control_space); + let received = loop { + refresh_read_deadline(stream, deadline)?; + match rustix::net::recvmsg( + stream.as_fd(), + &mut io, + &mut control, + RecvFlags::CMSG_CLOEXEC, + ) { + Ok(received) => break received, + Err(Errno::INTR) => {} + Err(error) => return Err(error.into()), + } + }; + + let mut received_fds = Vec::new(); + let mut unexpected_control_message = false; + for message in control.drain() { + match message { + RecvAncillaryMessage::ScmRights(fds) => received_fds.extend(fds), + _ => unexpected_control_message = true, + } + } + + if received.bytes == 0 { + return Err(Error::new( + ErrorKind::UnexpectedEof, + "broker closed during shared-memory setup", + )); + } + if received.bytes != carrier.len() + || received + .flags + .intersects(ReturnFlags::TRUNC | ReturnFlags::CTRUNC) + || unexpected_control_message + || received_fds.len() != 1 + { + return Err(invalid_data( + "shared-memory setup contained invalid descriptor data", + )); + } + Ok(received_fds + .pop() + .expect("exactly one received descriptor was validated")) +} + +fn with_read_deadline( + stream: &mut UnixStream, + deadline: Option, + operation: impl FnOnce(&mut UnixStream, Option) -> IoResult, +) -> IoResult { + let Some(_) = deadline else { + return operation(stream, None); + }; + let previous = stream.read_timeout()?; + let result = operation(stream, deadline); + combine_result_with_restore(result, stream.set_read_timeout(previous)) +} + +fn with_write_deadline( + stream: &mut UnixStream, + deadline: Option, + operation: impl FnOnce(&mut UnixStream, Option) -> IoResult, +) -> IoResult { + let Some(_) = deadline else { + return operation(stream, None); + }; + let previous = stream.write_timeout()?; + let result = operation(stream, deadline); + combine_result_with_restore(result, stream.set_write_timeout(previous)) +} + +fn refresh_read_deadline(stream: &UnixStream, deadline: Option) -> IoResult<()> { + if let Some(deadline) = deadline { + stream.set_read_timeout(Some(io_timeout_for_deadline(deadline)?))?; + } + Ok(()) +} + +fn refresh_write_deadline(stream: &UnixStream, deadline: Option) -> IoResult<()> { + if let Some(deadline) = deadline { + stream.set_write_timeout(Some(io_timeout_for_deadline(deadline)?))?; + } + Ok(()) +} + +fn combine_result_with_restore( + result: IoResult, + restore: IoResult<()>, +) -> IoResult { + match (result, restore) { + (Ok(output), Ok(())) => Ok(output), + (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error), + (Err(operation), Err(restore)) => Err(Error::new( + operation.kind(), + format!("{operation}; additionally failed to restore socket timeout: {restore}"), + )), + } +} + +fn io_timeout_for_deadline(deadline: Instant) -> IoResult { + deadline + .checked_duration_since(Instant::now()) + .filter(|timeout| !timeout.is_zero()) + .ok_or_else(|| Error::new(ErrorKind::TimedOut, "shared-memory setup deadline expired")) +} + impl Drop for MappedRegion { fn drop(&mut self) { // SAFETY: `address` and `length` describe the mapping exclusively owned @@ -183,6 +357,8 @@ fn invalid_data(message: &'static str) -> Error { #[cfg(test)] mod tests { use super::*; + use rustix::io::FdFlags; + use std::io::Write; #[test] fn mappings_share_bytes_and_validate_ranges() { @@ -236,4 +412,136 @@ mod tests { std::io::ErrorKind::InvalidData ); } + + #[test] + fn transfers_exact_size_memory_with_close_on_exec() { + let length = 24; + let memory = MemfdSharedMemory::create(length).unwrap(); + memory.write(16, &[1, 2, 3]).unwrap(); + let (mut local_stream, mut host_stream) = UnixStream::pair().unwrap(); + + send_memfd(&mut host_stream, &memory, None).unwrap(); + let mapped_memory = receive_memfd(&mut local_stream, length, None).unwrap(); + let mut bytes = [0; 3]; + mapped_memory.read(16, &mut bytes).unwrap(); + assert_eq!(bytes, [1, 2, 3]); + let flags = rustix::io::fcntl_getfd(mapped_memory.as_fd()).unwrap(); + assert!(flags.contains(FdFlags::CLOEXEC)); + } + + #[test] + fn rejects_missing_multiple_and_truncated_descriptors() { + let length = 8; + + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + sender.write_all(&[0]).unwrap(); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("missing descriptor must be rejected") + .kind(), + ErrorKind::InvalidData + ); + + let memory = MemfdSharedMemory::create(length).unwrap(); + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + send_test_fds(&mut sender, &[memory.as_fd(), memory.as_fd()]); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("multiple descriptors must be rejected") + .kind(), + ErrorKind::InvalidData + ); + + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + let fd = memory.as_fd(); + send_test_fds(&mut sender, &[fd, fd, fd, fd, fd]); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("truncated descriptors must be rejected") + .kind(), + ErrorKind::InvalidData + ); + } + + #[test] + fn rejects_wrong_size_and_unsealed_memory() { + let length = 8; + + let wrong_size = MemfdSharedMemory::create(7).unwrap(); + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + send_memfd(&mut sender, &wrong_size, None).unwrap(); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("wrong shared-memory size must be rejected") + .kind(), + ErrorKind::InvalidData + ); + + let unsealed = memfd_create("unsealed-transfer-test", MemfdFlags::CLOEXEC).unwrap(); + ftruncate(&unsealed, length.try_into().unwrap()).unwrap(); + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + send_test_fds(&mut sender, &[unsealed.as_fd()]); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("unsealed shared memory must be rejected") + .kind(), + ErrorKind::InvalidData + ); + } + + #[test] + fn reports_eof_and_expired_deadline() { + let length = 8; + let (mut receiver, sender) = UnixStream::pair().unwrap(); + drop(sender); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("setup EOF must be reported") + .kind(), + ErrorKind::UnexpectedEof + ); + + let (mut receiver, _sender) = UnixStream::pair().unwrap(); + let previous_timeout = Some(Duration::from_secs(2)); + receiver.set_read_timeout(previous_timeout).unwrap(); + let expired = Instant::now().checked_sub(Duration::from_secs(1)).unwrap(); + assert_eq!( + receive_memfd(&mut receiver, length, Some(expired)) + .err() + .expect("expired setup deadline must be rejected") + .kind(), + ErrorKind::TimedOut + ); + assert_eq!(receiver.read_timeout().unwrap(), previous_timeout); + + let memory = MemfdSharedMemory::create(length).unwrap(); + let (_receiver, mut sender) = UnixStream::pair().unwrap(); + sender.set_write_timeout(previous_timeout).unwrap(); + assert_eq!( + send_memfd(&mut sender, &memory, Some(expired)) + .expect_err("expired send deadline must be rejected") + .kind(), + ErrorKind::TimedOut + ); + assert_eq!(sender.write_timeout().unwrap(), previous_timeout); + } + + fn send_test_fds(stream: &mut UnixStream, fds: &[BorrowedFd<'_>]) { + let carrier = [0]; + let io = [IoSlice::new(&carrier)]; + let mut control_space = + [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(8))]; + let mut control = SendAncillaryBuffer::new(&mut control_space); + assert!(control.push(SendAncillaryMessage::ScmRights(fds))); + assert_eq!( + rustix::net::sendmsg(stream.as_fd(), &io, &mut control, SendFlags::NOSIGNAL).unwrap(), + 1 + ); + } } From 6e1e1aa1bc50e04b1918b782bf3839ff81abc144 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Tue, 21 Jul 2026 10:25:11 -0700 Subject: [PATCH 109/319] Implement NtDuplicateObject for Windows Shim (#1057) This PR implements current-process handle duplication across supported Windows object types. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_common_windows/src/nt_status.rs | 4 + litebox_shim_windows/src/lib.rs | 403 ++++++++++++++-- litebox_shim_windows/src/nt_types.rs | 1 + litebox_shim_windows/src/syscalls/event.rs | 74 +-- litebox_shim_windows/src/syscalls/file.rs | 162 ++++++- litebox_shim_windows/src/syscalls/iocp.rs | 27 +- litebox_shim_windows/src/syscalls/lpc.rs | 15 +- litebox_shim_windows/src/syscalls/mod.rs | 18 + .../src/syscalls/object_manager.rs | 43 +- litebox_shim_windows/src/syscalls/registry.rs | 58 ++- litebox_shim_windows/src/syscalls/section.rs | 72 +-- litebox_shim_windows/src/syscalls/symlink.rs | 40 +- litebox_shim_windows/src/syscalls/timer.rs | 51 +- .../src/syscalls/wait_completion_packet.rs | 118 ++--- .../src/syscalls/worker_factory.rs | 104 ++-- litebox_shim_windows/src/tests.rs | 450 ++++++++++++++++++ 16 files changed, 1224 insertions(+), 416 deletions(-) diff --git a/litebox_common_windows/src/nt_status.rs b/litebox_common_windows/src/nt_status.rs index 8aa92d815c..dd9d10f576 100644 --- a/litebox_common_windows/src/nt_status.rs +++ b/litebox_common_windows/src/nt_status.rs @@ -206,6 +206,7 @@ impl NtStatus { 0xC0000202 => "STATUS_NO_USER_SESSION_KEY: No user session key", 0xC0000225 => "STATUS_NOT_FOUND: Not found", 0xC000022D => "STATUS_RETRY: The operation should be retried", + 0xC0000235 => "STATUS_HANDLE_NOT_CLOSABLE: Handle not closable", 0xC00002DF => "STATUS_SAM_NEED_BOOTKEY_PASSWORD: SAM needs boot key password", 0xC00002E0 => "STATUS_SAM_NEED_BOOTKEY_FLOPPY: SAM needs boot key floppy", 0xC0000282 => "STATUS_RANGE_LIST_CONFLICT: Range list conflict", @@ -588,6 +589,9 @@ impl NtStatus { /// STATUS_RETRY pub const RETRY: Self = Self::from_raw(0xC000022D); + /// Handle not closable + pub const HANDLE_NOT_CLOSABLE: Self = Self::from_raw(0xC0000235); + /// STATUS_SAM_NEED_BOOTKEY_PASSWORD pub const SAM_NEED_BOOTKEY_PASSWORD: Self = Self::from_raw(0xC00002DF); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 961ae6261f..49306bf8e0 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -89,6 +89,54 @@ pub(crate) type MutPtr = pub(crate) type WindowsPageManager = PageManager; pub(crate) type WindowsHandleStore = litebox::sync::RwLock; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct DuplicateOptions: u32 { + const CLOSE_SOURCE = 0x0000_0001; + const SAME_ACCESS = 0x0000_0002; + const SAME_ATTRIBUTES = 0x0000_0004; + + const _ = !0; + } +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] + struct HandleAttributes: u32 { + const PROTECT_FROM_CLOSE = 0x0000_0001; + const INHERIT = 0x0000_0002; + const AUDIT_OBJECT_CLOSE = 0x0000_0004; + + const _ = !0; + } +} + +#[derive(Clone, Copy, Default)] +struct WindowsHandleMetadata { + granted_access: u32, + attributes: HandleAttributes, +} + +pub(crate) trait WindowsHandleSubsystem: litebox::fd::FdEnabledSubsystem { + fn normalize_desired_access(desired_access: u32) -> u32; + + fn resolve_duplicate_access( + _entry: &Self::Entry, + desired_access: u32, + ) -> Result { + let maximum_allowed = desired_access & nt_types::AccessMask::MAXIMUM_ALLOWED.bits() != 0; + let explicit_access = desired_access & !nt_types::AccessMask::MAXIMUM_ALLOWED.bits(); + let normalized = Self::normalize_desired_access(explicit_access); + Ok(if maximum_allowed { + // TODO(dacl-access-check): Derive this grant from the caller's token and the + // object's security descriptor instead of assuming a single trust context. + normalized | Self::normalize_desired_access(nt_types::AccessMask::GENERIC_ALL.bits()) + } else { + normalized + }) + } +} pub(crate) type WindowsNlsSectionMappings = litebox::sync::RwLock>; pub(crate) type WindowsVirtualAllocations = @@ -572,42 +620,115 @@ impl Task { &self, handle: syscalls::Handle, ) -> Result, NtStatus> + where + Subsystem: litebox::fd::FdEnabledSubsystem, + { + let typed = self.typed_handle::(handle)?; + self.global + .litebox + .descriptor_table() + .entry_handle(&typed) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn typed_handle_entry_with_access( + &self, + handle: syscalls::Handle, + required_access: u32, + ) -> Result, NtStatus> + where + Subsystem: WindowsHandleSubsystem, + { + let typed = self.typed_handle::(handle)?; + self.require_typed_handle_access(&typed, required_access)?; + self.global + .litebox + .descriptor_table() + .entry_handle(&typed) + .ok_or(NtStatus::INVALID_HANDLE) + } + + fn typed_handle( + &self, + handle: syscalls::Handle, + ) -> Result>, NtStatus> where Subsystem: litebox::fd::FdEnabledSubsystem, { let Some(raw_fd) = handle.raw_fd() else { return Err(NtStatus::INVALID_HANDLE); }; - let typed = { - let handles = self.process.handles.read(); - match handles.fd_from_raw_integer::(raw_fd) { - Ok(typed) => typed, - Err(litebox::fd::ErrRawIntFd::NotFound) => return Err(NtStatus::INVALID_HANDLE), - Err(litebox::fd::ErrRawIntFd::InvalidSubsystem) => { - return Err(NtStatus::OBJECT_TYPE_MISMATCH); - } - } - }; + let handles = self.process.handles.read(); + match handles.fd_from_raw_integer::(raw_fd) { + Ok(typed) => Ok(typed), + Err(litebox::fd::ErrRawIntFd::NotFound) => Err(NtStatus::INVALID_HANDLE), + Err(litebox::fd::ErrRawIntFd::InvalidSubsystem) => Err(NtStatus::OBJECT_TYPE_MISMATCH), + } + } + + fn typed_handle_metadata( + &self, + typed: &litebox::fd::TypedFd, + ) -> Result + where + Subsystem: WindowsHandleSubsystem, + { self.global .litebox .descriptor_table() - .entry_handle(&typed) - .ok_or(NtStatus::INVALID_HANDLE) + .with_metadata::(typed, |metadata| *metadata) + .map_err(|_| NtStatus::INVALID_HANDLE) + } + + pub(crate) fn require_handle_access( + &self, + handle: syscalls::Handle, + required_access: u32, + ) -> Result<(), NtStatus> + where + Subsystem: WindowsHandleSubsystem, + { + let typed = self.typed_handle::(handle)?; + self.require_typed_handle_access(&typed, required_access) + } + + pub(crate) fn require_typed_handle_access( + &self, + typed: &litebox::fd::TypedFd, + required_access: u32, + ) -> Result<(), NtStatus> + where + Subsystem: WindowsHandleSubsystem, + { + if self.typed_handle_metadata(typed)?.granted_access & required_access == required_access { + Ok(()) + } else { + Err(NtStatus::ACCESS_DENIED) + } } fn insert_typed_handle( &self, entry: Subsystem::Entry, + granted_access: u32, cleanup_entry: impl FnOnce(Subsystem::Entry), ) -> Result where - Subsystem: litebox::fd::FdEnabledSubsystem, + Subsystem: WindowsHandleSubsystem, { - let typed = self - .global - .litebox - .descriptor_table_mut() - .insert::(entry); + let typed = { + let mut descriptors = self.global.litebox.descriptor_table_mut(); + let typed = descriptors.insert::(entry); + let old = descriptors.set_fd_metadata( + &typed, + WindowsHandleMetadata { + granted_access, + attributes: HandleAttributes::empty(), + }, + ); + debug_assert!(old.is_none()); + typed + }; insert_raw_handle::( &self.global.litebox, &self.process.handles, @@ -657,6 +778,26 @@ impl Task { let status = self.sys_nt_close(handle); (status, ContinueOperation::Resume) } + SyscallRequest::NtDuplicateObject { + source_process_handle, + source_handle, + target_process_handle, + target_handle, + desired_access, + handle_attributes, + options, + } => { + let status = self.sys_nt_duplicate_object( + source_process_handle, + source_handle, + target_process_handle, + target_handle, + desired_access, + handle_attributes, + options, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtCreateEvent { event_handle, desired_access, @@ -1557,26 +1698,197 @@ impl Task { } pub(crate) fn sys_nt_close(&self, handle: syscalls::Handle) -> NtStatus { - let Some(raw_fd) = handle.raw_fd() else { + self.close_handle(handle, CloseRawHandleVisitor { task: self }) + } + + #[expect( + clippy::too_many_arguments, + reason = "matches the native NtDuplicateObject contract" + )] + pub(crate) fn sys_nt_duplicate_object( + &self, + source_process_handle: syscalls::ProcessHandle, + source_handle: syscalls::Handle, + target_process_handle: syscalls::ProcessHandle, + target_handle: Option>, + desired_access: u32, + handle_attributes: u32, + options: u32, + ) -> NtStatus { + let options = DuplicateOptions::from_bits_retain(options); + if let Some(target_handle) = target_handle + && target_handle + .write_at_offset(0, syscalls::Handle::default()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if !source_process_handle.is_current() { + // TODO(duplicate-object-cross-process): resolve process handles once LiteBox supports + // multiple guest processes and per-process handle tables. + return NtStatus::INVALID_HANDLE; + } + + let status = self.duplicate_object( + source_handle, + target_process_handle, + target_handle, + desired_access, + handle_attributes, + options, + ); + + if options.contains(DuplicateOptions::CLOSE_SOURCE) { + let _ = self.sys_nt_close(source_handle); + } + status + } + + fn duplicate_object( + &self, + source_handle: syscalls::Handle, + target_process_handle: syscalls::ProcessHandle, + target_handle: Option>, + desired_access: u32, + handle_attributes: u32, + options: DuplicateOptions, + ) -> NtStatus { + if target_process_handle.is_null() { + return if options.contains(DuplicateOptions::CLOSE_SOURCE) { + NtStatus::SUCCESS + } else { + NtStatus::INVALID_PARAMETER + }; + } + if !target_process_handle.is_current() { + // TODO(duplicate-object-cross-process): insert into the target process handle table. return NtStatus::INVALID_HANDLE; + } + let duplicate = match self.duplicate_handle( + source_handle, + desired_access, + handle_attributes, + options, + ) { + Ok(handle) => handle, + Err(status) => return status, }; - self.close_raw_fd(raw_fd, CloseRawHandleVisitor { task: self }) + if let Some(target_handle) = target_handle + && target_handle.write_at_offset(0, duplicate).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + fn duplicate_handle( + &self, + source_handle: syscalls::Handle, + desired_access: u32, + handle_attributes: u32, + options: DuplicateOptions, + ) -> Result { + macro_rules! try_duplicate { + ($subsystem:ty) => { + if let Some(result) = self.try_duplicate_handle::<$subsystem>( + source_handle, + desired_access, + handle_attributes, + options, + ) { + return result; + } + }; + } + + try_duplicate!(FileObjectSubsystem); + try_duplicate!(RegistryKeySubsystem); + try_duplicate!(EventSubsystem); + try_duplicate!(DirectoryObjectSubsystem); + try_duplicate!(SymbolicLinkSubsystem); + try_duplicate!(IoCompletionSubsystem); + try_duplicate!(LpcPortSubsystem); + try_duplicate!(TimerSubsystem); + try_duplicate!(WaitCompletionPacketSubsystem); + try_duplicate!(WorkerFactorySubsystem); + try_duplicate!(SectionSubsystem); + + Err(NtStatus::INVALID_HANDLE) } - fn close_raw_fd( + fn try_duplicate_handle( &self, - raw_fd: usize, + source_handle: syscalls::Handle, + desired_access: u32, + handle_attributes: u32, + options: DuplicateOptions, + ) -> Option> + where + Subsystem: WindowsHandleSubsystem, + { + let typed = match self.typed_handle::(source_handle) { + Ok(typed) => typed, + Err(NtStatus::OBJECT_TYPE_MISMATCH) => return None, + Err(status) => return Some(Err(status)), + }; + let source_metadata = match self.typed_handle_metadata(&typed) { + Ok(metadata) => metadata, + Err(status) => return Some(Err(status)), + }; + + let source_access = source_metadata.granted_access; + let duplicate_access = if options.contains(DuplicateOptions::SAME_ACCESS) { + source_access + } else { + let descriptors = self.global.litebox.descriptor_table(); + match descriptors.with_entry(&typed, |entry| { + Subsystem::resolve_duplicate_access(entry, desired_access) + }) { + Some(Ok(access)) => access, + Some(Err(status)) => return Some(Err(status)), + None => return Some(Err(NtStatus::INVALID_HANDLE)), + } + }; + let duplicate_attributes = if options.contains(DuplicateOptions::SAME_ATTRIBUTES) { + source_metadata.attributes + } else { + HandleAttributes::from_bits_retain(handle_attributes) + }; + + let duplicate = { + let mut descriptors = self.global.litebox.descriptor_table_mut(); + let Some(duplicate) = descriptors.duplicate(&typed) else { + return Some(Err(NtStatus::INVALID_HANDLE)); + }; + let old = descriptors.set_fd_metadata( + &duplicate, + WindowsHandleMetadata { + granted_access: duplicate_access, + attributes: duplicate_attributes, + }, + ); + debug_assert!(old.is_none()); + duplicate + }; + Some(insert_raw_handle::( + &self.global.litebox, + &self.process.handles, + duplicate, + drop, + )) + } + + fn close_handle( + &self, + handle: syscalls::Handle, visitor: impl RawHandleVisitor, ) -> NtStatus { macro_rules! try_close { ($subsystem:ty, $visit:ident) => { - if remove_raw_handle_by_raw_fd::( - &self.global.litebox, - &self.process.handles, - raw_fd, - |entry| visitor.$visit(entry), - ) { - return NtStatus::SUCCESS; + if let Some(status) = + self.try_close_handle::<$subsystem>(handle, |entry| visitor.$visit(entry)) + { + return status; } }; } @@ -1599,6 +1911,39 @@ impl Task { NtStatus::INVALID_HANDLE } + fn try_close_handle( + &self, + handle: syscalls::Handle, + cleanup_entry: impl FnOnce(Subsystem::Entry), + ) -> Option + where + Subsystem: WindowsHandleSubsystem, + { + let typed = match self.typed_handle::(handle) { + Ok(typed) => typed, + Err(NtStatus::OBJECT_TYPE_MISMATCH) => return None, + Err(status) => return Some(status), + }; + let metadata = match self.typed_handle_metadata(&typed) { + Ok(metadata) => metadata, + Err(status) => return Some(status), + }; + if metadata + .attributes + .contains(HandleAttributes::PROTECT_FROM_CLOSE) + { + return Some(NtStatus::HANDLE_NOT_CLOSABLE); + } + + remove_raw_handle::( + &self.global.litebox, + &self.process.handles, + handle, + cleanup_entry, + ); + Some(NtStatus::SUCCESS) + } + fn handle_interrupt_request( &self, _ctx: &mut litebox_common_linux::PtRegs, diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index d8d9ae7438..5665286f1d 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -143,6 +143,7 @@ bitflags::bitflags! { const WRITE_DAC = 0x0004_0000; const WRITE_OWNER = 0x0008_0000; const SYNCHRONIZE = 0x0010_0000; + const MAXIMUM_ALLOWED = 0x0200_0000; const STANDARD_RIGHTS_READ = Self::READ_CONTROL.bits(); const STANDARD_RIGHTS_WRITE = Self::READ_CONTROL.bits(); const STANDARD_RIGHTS_EXECUTE = Self::READ_CONTROL.bits(); diff --git a/litebox_shim_windows/src/syscalls/event.rs b/litebox_shim_windows/src/syscalls/event.rs index 09c5c0554e..4696108b3c 100644 --- a/litebox_shim_windows/src/syscalls/event.rs +++ b/litebox_shim_windows/src/syscalls/event.rs @@ -19,9 +19,7 @@ use crate::nt_types::{ AccessMask, ObjectAttributes, ObjectAttributesFlags, UnicodeString, read_object_attributes, }; use crate::syscalls::Handle; -use crate::{ - ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value, raw_handle_entry, -}; +use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; #[repr(u32)] #[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] @@ -70,14 +68,6 @@ impl EventAccess { Self::ALL_ACCESS.bits(), )) } - - fn require(self, required: Self) -> Result<(), NtStatus> { - if self.contains(required) { - Ok(()) - } else { - Err(NtStatus::ACCESS_DENIED) - } - } } pub(crate) struct EventSubsystem(PhantomData); @@ -88,9 +78,14 @@ impl FdEnabledSubsystem for EventSubsystem FdEnabledSubsystemEntry for EventHandleObject {} +impl crate::WindowsHandleSubsystem for EventSubsystem { + fn normalize_desired_access(desired_access: u32) -> u32 { + EventAccess::from_desired_access(desired_access).bits() + } +} + pub(crate) struct EventHandleObject { event: Arc>, - granted_access: EventAccess, } pub(crate) struct EventObject { @@ -158,10 +153,6 @@ impl EventObject { } impl EventHandleObject { - pub(crate) fn require_access(&self, required: EventAccess) -> Result<(), NtStatus> { - self.granted_access.require(required) - } - pub(crate) fn is_signaled(&self) -> bool { self.event.is_signaled() } @@ -244,28 +235,14 @@ fn read_event_object_attributes( } impl Task { - fn event_entry( - &self, - handle: Handle, - ) -> Result>, NtStatus> { - raw_handle_entry::>( - &self.global.litebox, - &self.process.handles, - handle, - ) - .ok_or(NtStatus::INVALID_HANDLE) - } - fn insert_event_handle( &self, event: Arc>, granted_access: EventAccess, ) -> Result { self.insert_typed_handle::>( - EventHandleObject { - event, - granted_access, - }, + EventHandleObject { event }, + granted_access.bits(), drop, ) } @@ -411,8 +388,10 @@ impl Task { } pub(crate) fn check_event_modify_access(&self, event_handle: Handle) -> Result<(), NtStatus> { - let entry = self.event_entry(event_handle)?; - entry.with_entry(|entry| entry.granted_access.require(EventAccess::MODIFY_STATE)) + self.require_handle_access::>( + event_handle, + EventAccess::MODIFY_STATE.bits(), + ) } pub(crate) fn sys_nt_reset_event( @@ -488,19 +467,14 @@ impl Task { return status; } - let Ok(entry) = self.event_entry(event_handle) else { - return NtStatus::INVALID_HANDLE; - }; - let query = entry.with_entry(|entry| { - entry - .granted_access - .require(EventAccess::QUERY_STATE) - .map(|()| entry.event.query()) - }); - let info = match query { - Ok(info) => info, + let entry = match self.typed_handle_entry_with_access::>( + event_handle, + EventAccess::QUERY_STATE.bits(), + ) { + Ok(entry) => entry, Err(status) => return status, }; + let info = entry.with_entry(|entry| entry.event.query()); if event_information.write_at_offset(0, info).is_none() { return NtStatus::ACCESS_VIOLATION; } @@ -520,11 +494,11 @@ impl Task { previous_state: Option>, op: impl FnOnce(&EventObject) -> Result, ) -> Result<(), NtStatus> { - let entry = self.event_entry(event_handle)?; - let previous = entry.with_entry(|entry| { - entry.granted_access.require(EventAccess::MODIFY_STATE)?; - op(&entry.event) - })?; + let entry = self.typed_handle_entry_with_access::>( + event_handle, + EventAccess::MODIFY_STATE.bits(), + )?; + let previous = entry.with_entry(|entry| op(&entry.event))?; if let Some(previous_state) = previous_state && previous_state.write_at_offset(0, previous).is_none() { diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 5f12101ab4..237ba6b333 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -88,10 +88,32 @@ impl FdEnabledSubsystem for FileObjectSubsystem { impl FdEnabledSubsystemEntry for FileObject {} +impl crate::WindowsHandleSubsystem for FileObjectSubsystem { + fn normalize_desired_access(desired_access: u32) -> u32 { + FileAccess::from_desired_access(desired_access).bits() + } + + fn resolve_duplicate_access(entry: &Self::Entry, desired_access: u32) -> Result { + let maximum_allowed = desired_access & AccessMask::MAXIMUM_ALLOWED.bits() != 0; + let explicit_access = + FileAccess::from_desired_access(desired_access & !AccessMask::MAXIMUM_ALLOWED.bits()); + if !entry.create_time_access.contains(explicit_access) { + return Err(NtStatus::ACCESS_DENIED); + } + Ok(if maximum_allowed { + // TODO(dacl-access-check): Replace this original-open ceiling with a token and + // security-descriptor access check when the shim models DACLs. + entry.create_time_access.bits() + } else { + explicit_access.bits() + }) + } +} + pub(crate) struct FileObject { path: String, backing: FileObjectBacking, - granted_access: FileAccess, + create_time_access: FileAccess, share_access: FileShareAccess, create_options: FileCreateOptions, } @@ -398,7 +420,10 @@ impl Task { } fn insert_file_handle(&self, file: FileObject) -> Result { - self.insert_typed_handle::>(file, |file| self.close_file(file)) + let granted_access = file.create_time_access.bits(); + self.insert_typed_handle::>(file, granted_access, |file| { + self.close_file(file); + }) } pub(crate) fn close_file_handle(&self, handle: Handle) { @@ -753,7 +778,7 @@ impl Task { FileObject { path, backing: FileObjectBacking::Filesystem { fd, is_directory }, - granted_access: desired_access, + create_time_access: desired_access, share_access, create_options, }, @@ -826,7 +851,7 @@ impl Task { FileObject { path, backing, - granted_access: desired_access, + create_time_access: desired_access, share_access, create_options, }, @@ -932,7 +957,7 @@ impl Task { fd, is_directory: true, }, - granted_access: desired_access, + create_time_access: desired_access, share_access, create_options, }, @@ -992,7 +1017,7 @@ impl Task { let conflicts = entry.with_entry(|file| { identity.matches(file) && (desired_access.conflicts_with_share(file.share_access) - || file.granted_access.conflicts_with_share(share_access)) + || file.create_time_access.conflicts_with_share(share_access)) }); if conflicts { return Err(NtStatus::SHARING_VIOLATION); @@ -1330,6 +1355,58 @@ mod tests { (status, handle) } + #[test] + fn nt_duplicate_object_rejects_file_access_escalation() { + let task = crate::tests::test_task(); + create_existing_file(&task, "/tmp/duplicate-read-only.txt", b"data"); + let (status, source, _) = create_file( + &task, + "/tmp/duplicate-read-only.txt", + FILE_GENERIC_READ, + FILE_OPEN, + ); + assert_eq!(status, NtStatus::SUCCESS); + + let mut write_duplicate = Handle::default(); + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::CURRENT, + Some(mut_ptr(&mut write_duplicate)), + FileAccess::WRITE_DATA.bits(), + 0, + 0, + ), + NtStatus::ACCESS_DENIED + ); + assert!(write_duplicate.is_null()); + + let mut maximum_duplicate = Handle::default(); + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::CURRENT, + Some(mut_ptr(&mut maximum_duplicate)), + AccessMask::MAXIMUM_ALLOWED.bits(), + 0, + 0, + ), + NtStatus::SUCCESS + ); + assert_eq!( + task.typed_handle::>(maximum_duplicate) + .and_then(|typed| { + task.typed_handle_metadata(&typed) + .map(|metadata| metadata.granted_access) + }), + Ok(FileAccess::from_desired_access(FILE_GENERIC_READ).bits()) + ); + assert_eq!(task.sys_nt_close(source), NtStatus::SUCCESS); + assert_eq!(task.sys_nt_close(maximum_duplicate), NtStatus::SUCCESS); + } + #[test] fn nt_create_file_follows_condrv_connection_through_standard_streams() { let task = crate::tests::test_task(); @@ -2315,6 +2392,15 @@ mod tests { Length: u32, FsInformationClass: u32, ) -> i32; + fn NtDuplicateObject( + SourceProcessHandle: *mut c_void, + SourceHandle: *mut c_void, + TargetProcessHandle: *mut c_void, + TargetHandle: *mut c_void, + DesiredAccess: u32, + HandleAttributes: u32, + Options: u32, + ) -> i32; fn NtClose(Handle: *mut c_void) -> i32; } @@ -2706,6 +2792,70 @@ mod tests { assert_eq!(host_io_status.information, litebox_io_status.information); } + #[test] + fn nt_duplicate_object_file_access_matrix_matches_host() { + let test_dir = test_tmp_dir("nt_duplicate_object_file_access_matrix_matches_host"); + let _ = std::fs::remove_dir_all(&test_dir); + std::fs::create_dir_all(&test_dir).unwrap(); + let host_file = test_dir.join("read-only-source.txt"); + std::fs::write(&host_file, b"host").unwrap(); + + let host_name_units = utf16(&host_nt_path(&host_file)); + let host_name = unicode_string(&host_name_units); + let host_attributes = host_object_attributes(&host_name); + let mut host_source = core::ptr::null_mut(); + let mut host_io_status = IoStatusBlock::default(); + // SAFETY: All pointers reference live locals and ObjectName names the test file. + assert_eq!( + unsafe { + NtOpenFile( + &raw mut host_source, + FILE_GENERIC_READ, + &raw const host_attributes, + &raw mut host_io_status, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + 0, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + let mut host_write_duplicate: *mut c_void = core::ptr::null_mut(); + // SAFETY: The process pseudo-handles and source handle are valid; output is a local. + assert_eq!( + unsafe { + NtDuplicateObject( + usize::MAX as *mut c_void, + host_source, + usize::MAX as *mut c_void, + (&raw mut host_write_duplicate).cast(), + FileAccess::WRITE_DATA.bits(), + 0, + 0, + ) + }, + NtStatus::ACCESS_DENIED.as_raw() + ); + assert!(host_write_duplicate.is_null()); + let mut host_maximum_duplicate: *mut c_void = core::ptr::null_mut(); + // SAFETY: The process pseudo-handles and source handle are valid; output is a local. + assert_eq!( + unsafe { + NtDuplicateObject( + usize::MAX as *mut c_void, + host_source, + usize::MAX as *mut c_void, + (&raw mut host_maximum_duplicate).cast(), + AccessMask::MAXIMUM_ALLOWED.bits(), + 0, + 0, + ) + }, + NtStatus::SUCCESS.as_raw() + ); + close_host_handle(host_source); + close_host_handle(host_maximum_duplicate); + } + #[test] fn nt_create_file_supersede_missing_matches_host_status_and_information() { let test_dir = test_tmp_dir( diff --git a/litebox_shim_windows/src/syscalls/iocp.rs b/litebox_shim_windows/src/syscalls/iocp.rs index 5e475b60eb..c4b8a008af 100644 --- a/litebox_shim_windows/src/syscalls/iocp.rs +++ b/litebox_shim_windows/src/syscalls/iocp.rs @@ -41,14 +41,6 @@ impl IoCompletionAccess { Self::ALL_ACCESS.bits(), )) } - - pub(crate) fn require(self, required: Self) -> Result<(), NtStatus> { - if self.contains(required) { - Ok(()) - } else { - Err(NtStatus::ACCESS_DENIED) - } - } } pub(crate) struct IoCompletionSubsystem(PhantomData); @@ -59,9 +51,16 @@ impl FdEnabledSubsystem for IoCompletionSubsystem impl FdEnabledSubsystemEntry for IoCompletionHandleObject {} +impl crate::WindowsHandleSubsystem + for IoCompletionSubsystem +{ + fn normalize_desired_access(desired_access: u32) -> u32 { + IoCompletionAccess::from_desired_access(desired_access).bits() + } +} + pub(crate) struct IoCompletionHandleObject { port: Arc>, - granted_access: IoCompletionAccess, } pub(crate) struct IoCompletionObject { @@ -82,10 +81,6 @@ impl IoCompletionHandleObject { pub(crate) fn port(&self) -> Arc> { self.port.clone() } - - pub(crate) fn require_access(&self, required: IoCompletionAccess) -> Result<(), NtStatus> { - self.granted_access.require(required) - } } fn validate_io_completion_object_attributes( @@ -108,10 +103,8 @@ impl Task { granted_access: IoCompletionAccess, ) -> Result { self.insert_typed_handle::>( - IoCompletionHandleObject { - port, - granted_access, - }, + IoCompletionHandleObject { port }, + granted_access.bits(), drop, ) } diff --git a/litebox_shim_windows/src/syscalls/lpc.rs b/litebox_shim_windows/src/syscalls/lpc.rs index d91173b937..e9e11c94f3 100644 --- a/litebox_shim_windows/src/syscalls/lpc.rs +++ b/litebox_shim_windows/src/syscalls/lpc.rs @@ -28,6 +28,19 @@ impl FdEnabledSubsystem for LpcPortSubsystem { impl FdEnabledSubsystemEntry for LpcPortHandleObject {} +impl crate::WindowsHandleSubsystem for LpcPortSubsystem { + fn normalize_desired_access(desired_access: u32) -> u32 { + desired_access + } + + fn resolve_duplicate_access( + _entry: &Self::Entry, + desired_access: u32, + ) -> Result { + Ok(desired_access & !crate::nt_types::AccessMask::MAXIMUM_ALLOWED.bits()) + } +} + pub(crate) struct LpcPortHandleObject { _port_name: String, } @@ -166,7 +179,7 @@ impl Task { let port = LpcPortHandleObject { _port_name: port_name.clone(), }; - let handle = match self.insert_typed_handle::>(port, drop) { + let handle = match self.insert_typed_handle::>(port, 0, drop) { Ok(handle) => handle, Err(status) => { self.rollback_pagefile_section_view(mapped_view.base); diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 0ee7646687..a2bf811de6 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -124,6 +124,15 @@ pub(crate) enum SyscallRequest { NtClose { handle: Handle, }, + NtDuplicateObject { + source_process_handle: ProcessHandle, + source_handle: Handle, + target_process_handle: ProcessHandle, + target_handle: Option>, + desired_access: u32, + handle_attributes: u32, + options: u32, + }, NtCreateEvent { event_handle: Platform::RawMutPointer, desired_access: u32, @@ -555,6 +564,15 @@ impl SyscallRequest { NtSysno::NtClose => Some(sys_req!(NtClose { handle: { Handle::from_raw }, })), + NtSysno::NtDuplicateObject => Some(sys_req!(NtDuplicateObject { + source_process_handle: { ProcessHandle::from_raw }, + source_handle: { Handle::from_raw }, + target_process_handle: { ProcessHandle::from_raw }, + target_handle:*, + desired_access, + handle_attributes, + options, + })), NtSysno::NtCreateEvent => Some(sys_req!(NtCreateEvent { event_handle:*, desired_access, diff --git a/litebox_shim_windows/src/syscalls/object_manager.rs b/litebox_shim_windows/src/syscalls/object_manager.rs index 2492f175f1..5e79356db8 100644 --- a/litebox_shim_windows/src/syscalls/object_manager.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -107,14 +107,6 @@ impl DirectoryAccess { Self::ALL_ACCESS.bits(), )) } - - fn require(self, required: Self) -> Result<(), NtStatus> { - if self.contains(required) { - Ok(()) - } else { - Err(NtStatus::ACCESS_DENIED) - } - } } pub(crate) struct DirectoryObjectSubsystem(PhantomData); @@ -125,9 +117,16 @@ impl FdEnabledSubsystem for DirectoryObjectSubsys impl FdEnabledSubsystemEntry for DirectoryHandleObject {} +impl crate::WindowsHandleSubsystem + for DirectoryObjectSubsystem +{ + fn normalize_desired_access(desired_access: u32) -> u32 { + DirectoryAccess::from_desired_access(desired_access).bits() + } +} + pub(crate) struct DirectoryHandleObject { directory: Arc>, - granted_access: DirectoryAccess, } pub(super) struct ObjectNode { @@ -998,11 +997,11 @@ impl Task { &self, handle: Handle, ) -> Result>, NtStatus> { - let entry = self.directory_entry(handle)?; - entry.with_entry(|entry| { - entry.granted_access.require(DirectoryAccess::TRAVERSE)?; - Ok(Arc::clone(&entry.directory)) - }) + let entry = self.typed_handle_entry_with_access::>( + handle, + DirectoryAccess::TRAVERSE.bits(), + )?; + Ok(entry.with_entry(|entry| Arc::clone(&entry.directory))) } pub(super) fn read_directory_object_attributes( @@ -1072,10 +1071,8 @@ impl Task { granted_access: DirectoryAccess, ) -> Result { self.insert_typed_handle::>( - DirectoryHandleObject { - directory, - granted_access, - }, + DirectoryHandleObject { directory }, + granted_access.bits(), drop, ) } @@ -1223,15 +1220,13 @@ impl Task { &self, params: DirectoryQueryParameters, ) -> NtStatus { - let entry = match self.directory_entry(params.directory_handle) { + let entry = match self.typed_handle_entry_with_access::>( + params.directory_handle, + DirectoryAccess::QUERY.bits(), + ) { Ok(entry) => entry, Err(status) => return status, }; - if let Err(status) = - entry.with_entry(|entry| entry.granted_access.require(DirectoryAccess::QUERY)) - { - return status; - } let directory = entry.with_entry(|entry| Arc::clone(&entry.directory)); let entries = match directory.children_snapshot() { Ok(entries) => entries, diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 83ab735168..9c2f409acd 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -62,10 +62,17 @@ impl FdEnabledSubsystem for RegistryKeySubsystem< impl FdEnabledSubsystemEntry for RegistryKeyObject {} +impl crate::WindowsHandleSubsystem + for RegistryKeySubsystem +{ + fn normalize_desired_access(desired_access: u32) -> u32 { + RegistryKeyAccess::from_desired_access(desired_access).bits() + } +} + pub(crate) struct RegistryKeyObject { path: String, fd: TypedFd>, - granted_access: RegistryKeyAccess, } pub(crate) struct RegistryStore { @@ -132,6 +139,18 @@ bitflags::bitflags! { } } +impl RegistryKeyAccess { + fn from_desired_access(desired_access: u32) -> Self { + Self::from_bits_retain(AccessMask::expand_generic_access( + desired_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + )) + } +} + impl From for OFlags { fn from(desired_access: RegistryKeyAccess) -> Self { let wants_read = desired_access.intersects(RegistryKeyAccess::FS_READ_ACCESS); @@ -146,16 +165,6 @@ impl From for OFlags { } } -impl RegistryKeyAccess { - fn can_query_value(self) -> bool { - const QUERY_ACCESS_BITS: u32 = RegistryKeyAccess::QUERY_VALUE.bits() - | AccessMask::GENERIC_READ.bits() - | AccessMask::GENERIC_EXECUTE.bits() - | AccessMask::GENERIC_ALL.bits(); - self.intersects(Self::from_bits_retain(QUERY_ACCESS_BITS)) - } -} - /// System-defined `REG_*` value types stored in `KEY_VALUE_*_INFORMATION::Type`. #[repr(u32)] #[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] @@ -361,10 +370,15 @@ impl Task { fn insert_registry_key_handle( &self, key: RegistryKeyObject, + granted_access: RegistryKeyAccess, ) -> Result { - self.insert_typed_handle::>(key, |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) { @@ -426,7 +440,7 @@ impl Task { .with_entry(|root_key| relative_nt_key_name_to_fs_path(&root_key.path, &key_name))? }; - let desired_access = RegistryKeyAccess::from_bits_retain(desired_access); + let desired_access = RegistryKeyAccess::from_desired_access(desired_access); let fd = self .global .registry @@ -443,11 +457,7 @@ impl Task { ); } })?; - self.insert_registry_key_handle(RegistryKeyObject { - path, - fd, - granted_access: desired_access, - }) + self.insert_registry_key_handle(RegistryKeyObject { path, fd }, desired_access) } pub(crate) fn sys_nt_query_value_key( @@ -493,12 +503,12 @@ impl Task { length: u32, result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = self.registry_key_entry(key_handle)?; + let key = self.typed_handle_entry_with_access::>( + key_handle, + RegistryKeyAccess::QUERY_VALUE.bits(), + )?; let value_name = value_name.read_string::()?; let value = key.with_entry(|key| { - if !key.granted_access.can_query_value() { - return Err(NtStatus::ACCESS_DENIED); - } // TODO: Open the value relative to `key.fd` once the FS has an openat-style API. self.global .registry diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index dccd92f312..86d9cb40a3 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -61,9 +61,14 @@ impl FdEnabledSubsystem for SectionSubsystem { impl FdEnabledSubsystemEntry for SectionHandleObject {} +impl crate::WindowsHandleSubsystem for SectionSubsystem { + fn normalize_desired_access(desired_access: u32) -> u32 { + SectionAccess::from_desired_access(desired_access).bits() + } +} + pub(crate) struct SectionHandleObject { section: Arc>, - granted_access: SectionAccess, } pub(crate) struct SectionObject { @@ -166,14 +171,6 @@ impl SectionAccess { } access } - - fn require(self, required: Self) -> Result<(), NtStatus> { - if self.contains(required) { - Ok(()) - } else { - Err(NtStatus::ACCESS_DENIED) - } - } } #[repr(u32)] @@ -219,23 +216,14 @@ struct SectionImageInformation { const _: () = assert!(size_of::() == 64); impl Task { - fn section_entry( - &self, - handle: Handle, - ) -> Result>, NtStatus> { - self.typed_handle_entry::>(handle) - } - fn insert_section_handle( &self, section: Arc>, granted_access: SectionAccess, ) -> Result { self.insert_typed_handle::>( - SectionHandleObject { - section, - granted_access, - }, + SectionHandleObject { section }, + granted_access.bits(), drop, ) } @@ -451,20 +439,14 @@ impl Task { else { return NtStatus::INVALID_INFO_CLASS; }; - let entry = match self.section_entry(section_handle) { + let entry = match self.typed_handle_entry_with_access::>( + section_handle, + SectionAccess::QUERY.bits(), + ) { Ok(entry) => entry, Err(status) => return status, }; - let result = entry.with_entry(|entry| { - entry - .granted_access - .require(SectionAccess::QUERY) - .map(|()| Arc::clone(&entry.section)) - }); - let section = match result { - Ok(section) => section, - Err(status) => return status, - }; + let section = entry.with_entry(|entry| Arc::clone(&entry.section)); match information_class { SectionInformationClass::Basic => write_section_basic_information::( §ion, @@ -516,20 +498,14 @@ impl Task { else { return NtStatus::INVALID_PAGE_PROTECTION; }; - let entry = match self.section_entry(request.section_handle) { + let entry = match self.typed_handle_entry_with_access::>( + request.section_handle, + required_map_access(page_protection).bits(), + ) { Ok(entry) => entry, Err(status) => return status, }; - let result = entry.with_entry(|entry| { - entry - .granted_access - .require(required_map_access(page_protection)) - .map(|()| Arc::clone(&entry.section)) - }); - let section = match result { - Ok(section) => section, - Err(status) => return status, - }; + let section = entry.with_entry(|entry| Arc::clone(&entry.section)); match section.backing { SectionBacking::Pagefile => self.map_pagefile_section( request, @@ -659,13 +635,11 @@ impl Task { section_handle: Handle, requested_view_size: usize, ) -> Result { - let entry = self.section_entry(section_handle)?; - let section = entry.with_entry(|entry| { - entry - .granted_access - .require(SectionAccess::MAP_READ | SectionAccess::MAP_WRITE) - .map(|()| Arc::clone(&entry.section)) - })?; + let entry = self.typed_handle_entry_with_access::>( + section_handle, + (SectionAccess::MAP_READ | SectionAccess::MAP_WRITE).bits(), + )?; + let section = entry.with_entry(|entry| Arc::clone(&entry.section)); let page_protection = PageProtection::PAGE_READWRITE; let Some((_, permissions)) = parse_page_protection(page_protection.bits()) else { return Err(NtStatus::INVALID_PAGE_PROTECTION); diff --git a/litebox_shim_windows/src/syscalls/symlink.rs b/litebox_shim_windows/src/syscalls/symlink.rs index 08aadc6c52..38b86d9755 100644 --- a/litebox_shim_windows/src/syscalls/symlink.rs +++ b/litebox_shim_windows/src/syscalls/symlink.rs @@ -48,14 +48,6 @@ impl SymbolicLinkAccess { Self::ALL_ACCESS.bits(), )) } - - fn require(self, required: Self) -> Result<(), NtStatus> { - if self.contains(required) { - Ok(()) - } else { - Err(NtStatus::ACCESS_DENIED) - } - } } pub(crate) struct SymbolicLinkSubsystem(PhantomData); @@ -66,9 +58,16 @@ impl FdEnabledSubsystem for SymbolicLinkSubsystem impl FdEnabledSubsystemEntry for SymbolicLinkHandleObject {} +impl crate::WindowsHandleSubsystem + for SymbolicLinkSubsystem +{ + fn normalize_desired_access(desired_access: u32) -> u32 { + SymbolicLinkAccess::from_desired_access(desired_access).bits() + } +} + pub(crate) struct SymbolicLinkHandleObject { link: Arc>, - granted_access: SymbolicLinkAccess, } fn utf16_units(value: &str) -> Result, NtStatus> { @@ -84,23 +83,14 @@ fn utf16_units(value: &str) -> Result, NtStatus> { } impl Task { - fn symbolic_link_entry( - &self, - handle: Handle, - ) -> Result>, NtStatus> { - self.typed_handle_entry::>(handle) - } - fn insert_symbolic_link_handle( &self, link: Arc>, granted_access: SymbolicLinkAccess, ) -> Result { self.insert_typed_handle::>( - SymbolicLinkHandleObject { - link, - granted_access, - }, + SymbolicLinkHandleObject { link }, + granted_access.bits(), drop, ) } @@ -234,15 +224,13 @@ impl Task { link_target: MutPtr, returned_length: Option>, ) -> NtStatus { - let entry = match self.symbolic_link_entry(link_handle) { + let entry = match self.typed_handle_entry_with_access::>( + link_handle, + SymbolicLinkAccess::QUERY.bits(), + ) { Ok(entry) => entry, Err(status) => return status, }; - if let Err(status) = - entry.with_entry(|entry| entry.granted_access.require(SymbolicLinkAccess::QUERY)) - { - return status; - } if let Err(status) = probe_guest_output_preserving_value::(link_target) { return status; } diff --git a/litebox_shim_windows/src/syscalls/timer.rs b/litebox_shim_windows/src/syscalls/timer.rs index 91d97775c0..0befe6ab04 100644 --- a/litebox_shim_windows/src/syscalls/timer.rs +++ b/litebox_shim_windows/src/syscalls/timer.rs @@ -12,9 +12,7 @@ use litebox_common_windows::nt_status::NtStatus; use crate::nt_types::{AccessMask, ObjectAttributes}; use crate::syscalls::Handle; -use crate::{ - ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value, raw_handle_entry, -}; +use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; const TIMER2_ATTRIBUTE_IR_TIMER: u32 = 0x0000_0002; const TIMER2_ATTRIBUTE_HIGH_RESOLUTION: u32 = 0x0000_0004; @@ -74,19 +72,14 @@ impl FdEnabledSubsystem for TimerSubsystem FdEnabledSubsystemEntry for TimerHandleObject {} -pub(crate) struct TimerHandleObject { - _timer: Arc>, - granted_access: TimerAccess, +impl crate::WindowsHandleSubsystem for TimerSubsystem { + fn normalize_desired_access(desired_access: u32) -> u32 { + TimerAccess::from_desired_access(desired_access).bits() + } } -impl TimerHandleObject { - pub(crate) fn require_access(&self, required: TimerAccess) -> Result<(), NtStatus> { - if self.granted_access.contains(required) { - Ok(()) - } else { - Err(NtStatus::ACCESS_DENIED) - } - } +pub(crate) struct TimerHandleObject { + _timer: Arc>, } pub(crate) struct TimerObject { @@ -131,28 +124,14 @@ fn validate_timer2_after_output( } impl Task { - fn timer_entry( - &self, - handle: Handle, - ) -> Result>, NtStatus> { - raw_handle_entry::>( - &self.global.litebox, - &self.process.handles, - handle, - ) - .ok_or(NtStatus::INVALID_HANDLE) - } - fn insert_timer_handle( &self, timer: Arc>, granted_access: TimerAccess, ) -> Result { self.insert_typed_handle::>( - TimerHandleObject { - _timer: timer, - granted_access, - }, + TimerHandleObject { _timer: timer }, + granted_access.bits(), drop, ) } @@ -201,16 +180,12 @@ impl Task { period: Option>, parameters: Option>, ) -> NtStatus { - let timer = match self.timer_entry(timer_handle) { - Ok(timer) => timer, - Err(status) => return status, - }; - if let Err(status) = - timer.with_entry(|timer| timer.require_access(TimerAccess::MODIFY_STATE)) - { + if let Err(status) = self.require_handle_access::>( + timer_handle, + TimerAccess::MODIFY_STATE.bits(), + ) { return status; } - let _due_time = match due_time { Some(due_time) => match due_time.read_at_offset(0) { Some(due_time) => Some(due_time), diff --git a/litebox_shim_windows/src/syscalls/wait_completion_packet.rs b/litebox_shim_windows/src/syscalls/wait_completion_packet.rs index 80b492536c..9b9d4da1fb 100644 --- a/litebox_shim_windows/src/syscalls/wait_completion_packet.rs +++ b/litebox_shim_windows/src/syscalls/wait_completion_packet.rs @@ -13,9 +13,9 @@ use litebox_common_windows::nt_status::NtStatus; use crate::nt_types::{AccessMask, ObjectAttributes, read_object_attributes}; use crate::syscalls::Handle; -use crate::syscalls::event::{EventAccess, EventSubsystem}; +use crate::syscalls::event::{EventHandleObject, EventSubsystem}; use crate::syscalls::iocp::{IoCompletionAccess, IoCompletionSubsystem}; -use crate::syscalls::timer::{TimerAccess, TimerSubsystem}; +use crate::syscalls::timer::TimerSubsystem; use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; const STANDARD_RIGHTS_REQUIRED: u32 = AccessMask::DELETE.bits() @@ -47,14 +47,6 @@ impl WaitCompletionPacketAccess { Self::ALL_ACCESS.bits(), )) } - - fn require(self, required: Self) -> Result<(), NtStatus> { - if self.contains(required) { - Ok(()) - } else { - Err(NtStatus::ACCESS_DENIED) - } - } } pub(crate) struct WaitCompletionPacketSubsystem(PhantomData); @@ -68,9 +60,16 @@ impl FdEnabledSubsystemEntry { } +impl crate::WindowsHandleSubsystem + for WaitCompletionPacketSubsystem +{ + fn normalize_desired_access(desired_access: u32) -> u32 { + WaitCompletionPacketAccess::from_desired_access(desired_access).bits() + } +} + pub(crate) struct WaitCompletionPacketHandleObject { packet: Arc>, - granted_access: WaitCompletionPacketAccess, } pub(crate) struct WaitCompletionPacketObject { @@ -129,6 +128,7 @@ impl Task { } } }; + self.require_typed_handle_access(&typed, WaitCompletionPacketAccess::SET_STATE.bits())?; self.global .litebox .descriptor_table() @@ -152,6 +152,7 @@ impl Task { Err(ErrRawIntFd::InvalidSubsystem) => return Err(NtStatus::OBJECT_TYPE_MISMATCH), } }; + self.require_typed_handle_access(&typed, WaitCompletionPacketAccess::SET_STATE.bits())?; self.global .litebox .descriptor_table() @@ -163,22 +164,16 @@ impl Task { &self, handle: Handle, ) -> Result<(), NtStatus> { - let Some(raw_fd) = handle.raw_fd() else { - return Err(NtStatus::OBJECT_TYPE_MISMATCH); - }; - let typed = { - let handles = self.process.handles.read(); - match handles.fd_from_raw_integer::>(raw_fd) { - Ok(typed) => typed, - Err(ErrRawIntFd::NotFound | ErrRawIntFd::InvalidSubsystem) => { - return Err(NtStatus::OBJECT_TYPE_MISMATCH); - } + self.require_handle_access::>( + handle, + IoCompletionAccess::MODIFY_STATE.bits(), + ) + .map_err(|status| match status { + NtStatus::INVALID_HANDLE | NtStatus::OBJECT_TYPE_MISMATCH => { + NtStatus::OBJECT_TYPE_MISMATCH } - }; - let Some(entry) = self.global.litebox.descriptor_table().entry_handle(&typed) else { - return Err(NtStatus::OBJECT_TYPE_MISMATCH); - }; - entry.with_entry(|entry| entry.require_access(IoCompletionAccess::MODIFY_STATE)) + status => status, + }) } fn target_object_signaled_for_wait_completion_packet( @@ -214,43 +209,30 @@ impl Task { let Some(entry) = self.global.litebox.descriptor_table().entry_handle(&typed) else { return Err(NtStatus::ACCESS_DENIED); }; - entry - .with_entry(|entry| { - entry - .require_access(EventAccess::from_bits_retain( - AccessMask::SYNCHRONIZE.bits(), - )) - .map(|()| entry.is_signaled()) - }) - .map(Some) + self.require_typed_handle_access::>( + &typed, + AccessMask::SYNCHRONIZE.bits(), + )?; + Ok(Some(entry.with_entry(EventHandleObject::is_signaled))) } fn timer_signaled_for_wait_completion_packet( &self, raw_fd: usize, ) -> Result, NtStatus> { - let typed = { - let handles = self.process.handles.read(); - match handles.fd_from_raw_integer::>(raw_fd) { - Ok(typed) => typed, - Err(ErrRawIntFd::NotFound) => return Err(NtStatus::ACCESS_DENIED), - Err(ErrRawIntFd::InvalidSubsystem) => return Ok(None), - } - }; - let Some(entry) = self.global.litebox.descriptor_table().entry_handle(&typed) else { - return Err(NtStatus::ACCESS_DENIED); - }; + let handle = Handle::from_raw_fd(raw_fd).ok_or(NtStatus::ACCESS_DENIED)?; + match self.require_handle_access::>( + handle, + AccessMask::SYNCHRONIZE.bits(), + ) { + Ok(()) => {} + Err(NtStatus::OBJECT_TYPE_MISMATCH) => return Ok(None), + Err(NtStatus::INVALID_HANDLE) => return Err(NtStatus::ACCESS_DENIED), + Err(status) => return Err(status), + } // TODO: return the timer object's real signaled state after NtSetTimer2 models // due-time expiration and periodic re-signaling. - entry - .with_entry(|entry| { - entry - .require_access(TimerAccess::from_bits_retain( - AccessMask::SYNCHRONIZE.bits(), - )) - .map(|()| false) - }) - .map(Some) + Ok(Some(false)) } fn insert_wait_completion_packet_handle( @@ -259,10 +241,8 @@ impl Task { granted_access: WaitCompletionPacketAccess, ) -> Result { self.insert_typed_handle::>( - WaitCompletionPacketHandleObject { - packet, - granted_access, - }, + WaitCompletionPacketHandleObject { packet }, + granted_access.bits(), drop, ) } @@ -320,15 +300,7 @@ impl Task { Ok(entry) => entry, Err(status) => return status, }; - let packet = match entry.with_entry(|entry| { - entry - .granted_access - .require(WaitCompletionPacketAccess::SET_STATE) - .map(|()| entry.packet.clone()) - }) { - Ok(packet) => packet, - Err(status) => return status, - }; + let packet = entry.with_entry(|entry| entry.packet.clone()); if let Err(status) = self.validate_io_completion_for_wait_completion_packet(params.io_completion_handle) @@ -381,15 +353,7 @@ impl Task { Ok(entry) => entry, Err(status) => return status, }; - let packet = match entry.with_entry(|entry| { - entry - .granted_access - .require(WaitCompletionPacketAccess::SET_STATE) - .map(|()| Arc::clone(&entry.packet)) - }) { - Ok(packet) => packet, - Err(status) => return status, - }; + let packet = entry.with_entry(|entry| Arc::clone(&entry.packet)); let mut association = packet.association.lock(); let Some(current_association) = *association else { diff --git a/litebox_shim_windows/src/syscalls/worker_factory.rs b/litebox_shim_windows/src/syscalls/worker_factory.rs index 0d49dcb94b..7c986dd838 100644 --- a/litebox_shim_windows/src/syscalls/worker_factory.rs +++ b/litebox_shim_windows/src/syscalls/worker_factory.rs @@ -8,12 +8,14 @@ use core::marker::PhantomData; use core::mem::size_of; use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use litebox::fd::{ErrRawIntFd, FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox_common_windows::nt_status::NtStatus; use crate::nt_types::{AccessMask, ObjectAttributes, read_object_attributes}; -use crate::syscalls::iocp::{IoCompletionAccess, IoCompletionObject, IoCompletionSubsystem}; +use crate::syscalls::iocp::{ + IoCompletionAccess, IoCompletionHandleObject, IoCompletionObject, IoCompletionSubsystem, +}; use crate::syscalls::{Handle, ProcessHandle}; use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; @@ -54,14 +56,6 @@ impl WorkerFactoryAccess { Self::ALL_ACCESS.bits(), )) } - - fn require(self, required: Self) -> Result<(), NtStatus> { - if self.contains(required) { - Ok(()) - } else { - Err(NtStatus::ACCESS_DENIED) - } - } } #[repr(u32)] @@ -96,9 +90,16 @@ impl FdEnabledSubsystemEntry { } +impl crate::WindowsHandleSubsystem + for WorkerFactorySubsystem +{ + fn normalize_desired_access(desired_access: u32) -> u32 { + WorkerFactoryAccess::from_desired_access(desired_access).bits() + } +} + pub(crate) struct WorkerFactoryHandleObject { factory: Arc>, - granted_access: WorkerFactoryAccess, } pub(crate) struct WorkerFactoryObject { @@ -152,55 +153,15 @@ fn commit_worker_factory_shutdown( } impl Task { - fn worker_factory_entry( - &self, - handle: Handle, - ) -> Result>, NtStatus> - { - let Some(raw_fd) = handle.raw_fd() else { - return Err(NtStatus::INVALID_HANDLE); - }; - let typed = { - let handles = self.process.handles.read(); - match handles.fd_from_raw_integer::>(raw_fd) { - Ok(typed) => typed, - Err(ErrRawIntFd::NotFound) => return Err(NtStatus::INVALID_HANDLE), - Err(ErrRawIntFd::InvalidSubsystem) => { - return Err(NtStatus::OBJECT_TYPE_MISMATCH); - } - } - }; - self.global - .litebox - .descriptor_table() - .entry_handle(&typed) - .ok_or(NtStatus::INVALID_HANDLE) - } - fn io_completion_port( &self, handle: Handle, ) -> Result>, NtStatus> { - let Some(raw_fd) = handle.raw_fd() else { - return Err(NtStatus::INVALID_HANDLE); - }; - let typed = { - let handles = self.process.handles.read(); - match handles.fd_from_raw_integer::>(raw_fd) { - Ok(typed) => typed, - Err(ErrRawIntFd::NotFound) => return Err(NtStatus::INVALID_HANDLE), - Err(ErrRawIntFd::InvalidSubsystem) => { - return Err(NtStatus::OBJECT_TYPE_MISMATCH); - } - } - }; - let Some(entry) = self.global.litebox.descriptor_table().entry_handle(&typed) else { - return Err(NtStatus::INVALID_HANDLE); - }; - entry.with_entry(|entry| { - entry.require_access(IoCompletionAccess::MODIFY_STATE)?; - Ok(entry.port()) - }) + let entry = self.typed_handle_entry_with_access::>( + handle, + IoCompletionAccess::MODIFY_STATE.bits(), + )?; + Ok(entry.with_entry(IoCompletionHandleObject::port)) } fn validate_worker_process_handle( @@ -226,10 +187,8 @@ impl Task { granted_access: WorkerFactoryAccess, ) -> Result { self.insert_typed_handle::>( - WorkerFactoryHandleObject { - factory, - granted_access, - }, + WorkerFactoryHandleObject { factory }, + granted_access.bits(), drop, ) } @@ -323,15 +282,15 @@ impl Task { .expect("ULONG input is four bytes"), ); - let entry = match self.worker_factory_entry(handle) { + let entry = match self.typed_handle_entry_with_access::>( + handle, + WorkerFactoryAccess::SET_INFORMATION.bits(), + ) { Ok(entry) => entry, Err(status) => return status, }; entry .with_entry(|entry| { - entry - .granted_access - .require(WorkerFactoryAccess::SET_INFORMATION)?; // TODO: enforce these limits against real worker creation/drain behavior // once worker threads are modeled; today they are only recorded. match information_class { @@ -380,19 +339,14 @@ impl Task { { return status; } - let entry = match self.worker_factory_entry(handle) { + let entry = match self.typed_handle_entry_with_access::>( + handle, + WorkerFactoryAccess::SHUTDOWN.bits(), + ) { Ok(entry) => entry, Err(status) => return status, }; - let factory = match entry.with_entry(|entry| { - entry - .granted_access - .require(WorkerFactoryAccess::SHUTDOWN)?; - Ok(Arc::clone(&entry.factory)) - }) { - Ok(factory) => factory, - Err(status) => return status, - }; + let factory = entry.with_entry(|entry| Arc::clone(&entry.factory)); // TODO: report the actual pending worker count and wake/release workers once worker // threads are modeled; the current subset has no workers to drain. commit_worker_factory_shutdown(&factory, pending_worker_count) @@ -595,7 +549,7 @@ mod tests { NtStatus::SUCCESS ); let factory = task - .worker_factory_entry(worker_factory) + .typed_handle_entry::>(worker_factory) .expect("worker factory handle is valid") .with_entry(|entry| Arc::clone(&entry.factory)); assert!(!factory.shutdown.load(Ordering::Relaxed)); diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 9726560c11..4c8a42eddf 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -169,3 +169,453 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task, desired_access: u32) -> Handle { + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_create_event(mut_ptr(&mut handle), desired_access, None, 0, 0,), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + handle +} + +#[test] +fn nt_duplicate_object_preserves_identity_with_independent_access() { + let task = test_task(); + let source = create_event(&task, SYNCHRONIZE); + let mut duplicate = Handle::default(); + + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::CURRENT, + Some(mut_ptr(&mut duplicate)), + EVENT_MODIFY_STATE, + 0, + 0, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_ne!(source, duplicate); + assert_eq!( + task.sys_nt_set_event(source, None), + litebox_common_windows::nt_status::NtStatus::ACCESS_DENIED + ); + assert_eq!( + task.sys_nt_set_event(duplicate, None), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(source), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_set_event(duplicate, None), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(duplicate), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); +} + +#[test] +fn nt_duplicate_object_can_atomically_replace_the_source_handle() { + let task = test_task(); + let source = create_event(&task, EVENT_MODIFY_STATE); + let mut duplicate = Handle::default(); + + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::CURRENT, + Some(mut_ptr(&mut duplicate)), + 0, + 0, + DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(source), + litebox_common_windows::nt_status::NtStatus::INVALID_HANDLE + ); + assert_eq!( + task.sys_nt_set_event(duplicate, None), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(duplicate), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); +} + +#[test] +fn nt_duplicate_object_closes_source_even_when_duplication_fails() { + let task = test_task(); + let source = create_event(&task, EVENT_MODIFY_STATE); + let mut duplicate = Handle::from_raw(0x7777); + + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::from_raw(0x1234), + Some(mut_ptr(&mut duplicate)), + 0, + 0, + DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS, + ), + litebox_common_windows::nt_status::NtStatus::INVALID_HANDLE + ); + assert_eq!( + task.sys_nt_close(source), + litebox_common_windows::nt_status::NtStatus::INVALID_HANDLE + ); + assert!(duplicate.is_null()); +} + +#[test] +fn nt_duplicate_object_supports_close_only_calls() { + let task = test_task(); + let source = create_event(&task, EVENT_MODIFY_STATE); + + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::from_raw(0), + None, + 0, + 0, + 0, + ), + litebox_common_windows::nt_status::NtStatus::INVALID_PARAMETER + ); + assert_eq!( + task.sys_nt_set_event(source, None), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::from_raw(0), + None, + 0, + 0, + DUPLICATE_CLOSE_SOURCE, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(source), + litebox_common_windows::nt_status::NtStatus::INVALID_HANDLE + ); +} + +#[test] +fn nt_duplicate_object_null_output_retains_inaccessible_duplicate() { + let task = test_task(); + let source = create_event(&task, EVENT_MODIFY_STATE); + let handles_before = task.process.handles.read().iter_alive().count(); + + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::CURRENT, + None, + 0, + 0, + DUPLICATE_SAME_ACCESS, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.process.handles.read().iter_alive().count(), + handles_before + 1 + ); + assert_eq!( + task.sys_nt_close(source), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.process.handles.read().iter_alive().count(), + handles_before + ); +} + +#[test] +fn nt_duplicate_object_ignores_unknown_flags_and_copies_attributes() { + let task = test_task(); + let source = create_event(&task, EVENT_MODIFY_STATE); + let mut first_duplicate = Handle::default(); + let mut second_duplicate = Handle::default(); + let mut unprotected_duplicate = Handle::default(); + + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::CURRENT, + Some(mut_ptr(&mut first_duplicate)), + 0, + 0x8000_0001, + DUPLICATE_SAME_ACCESS | 0x8000_0000, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + first_duplicate, + crate::syscalls::ProcessHandle::CURRENT, + Some(mut_ptr(&mut second_duplicate)), + 0, + 0x4000_0000, + DUPLICATE_SAME_ACCESS | DUPLICATE_SAME_ATTRIBUTES, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + first_duplicate, + crate::syscalls::ProcessHandle::CURRENT, + Some(mut_ptr(&mut unprotected_duplicate)), + 0, + 0, + DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(source), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(first_duplicate), + litebox_common_windows::nt_status::NtStatus::HANDLE_NOT_CLOSABLE + ); + assert_eq!( + task.sys_nt_close(second_duplicate), + litebox_common_windows::nt_status::NtStatus::HANDLE_NOT_CLOSABLE + ); + assert_eq!( + task.sys_nt_close(unprotected_duplicate), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); +} + +#[test] +fn nt_duplicate_object_grants_maximum_allowed_access() { + let task = test_task(); + let source = create_event(&task, SYNCHRONIZE); + let mut duplicate = Handle::default(); + + assert_eq!( + task.sys_nt_duplicate_object( + crate::syscalls::ProcessHandle::CURRENT, + source, + crate::syscalls::ProcessHandle::CURRENT, + Some(mut_ptr(&mut duplicate)), + MAXIMUM_ALLOWED, + 0, + 0, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_set_event(source, None), + litebox_common_windows::nt_status::NtStatus::ACCESS_DENIED + ); + assert_eq!( + task.sys_nt_set_event(duplicate, None), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(source), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_close(duplicate), + litebox_common_windows::nt_status::NtStatus::SUCCESS + ); +} + +#[cfg(target_os = "windows")] +#[test] +fn host_nt_duplicate_object_failure_and_access_matrix() { + use core::ffi::c_void; + + #[link(name = "kernel32")] + unsafe extern "system" { + fn CreateEventW( + event_attributes: *const c_void, + manual_reset: i32, + initial_state: i32, + name: *const u16, + ) -> *mut c_void; + } + + #[link(name = "ntdll")] + unsafe extern "system" { + fn NtClose(handle: *mut c_void) -> i32; + fn NtSetEvent(handle: *mut c_void, previous_state: *mut i32) -> i32; + fn NtDuplicateObject( + source_process_handle: *mut c_void, + source_handle: *mut c_void, + target_process_handle: *mut c_void, + target_handle: *mut c_void, + desired_access: u32, + handle_attributes: u32, + options: u32, + ) -> i32; + } + + // SAFETY: All pointers are either documented pseudo-handles, null, or valid local outputs. + unsafe { + let source = CreateEventW(core::ptr::null(), 0, 0, core::ptr::null()); + assert!(!source.is_null()); + let mut duplicate: *mut c_void = core::ptr::null_mut(); + assert_eq!( + NtDuplicateObject( + usize::MAX as *mut c_void, + source, + 0x1234usize as *mut c_void, + (&raw mut duplicate).cast(), + 0, + 0, + DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS, + ), + litebox_common_windows::nt_status::NtStatus::INVALID_HANDLE.as_raw() + ); + assert_eq!( + NtClose(source), + litebox_common_windows::nt_status::NtStatus::INVALID_HANDLE.as_raw() + ); + assert!(duplicate.is_null()); + + let source = CreateEventW(core::ptr::null(), 0, 0, core::ptr::null()); + assert!(!source.is_null()); + let mut duplicate = usize::MAX as *mut c_void; + assert_eq!( + NtDuplicateObject( + usize::MAX as *mut c_void, + source, + core::ptr::null_mut(), + (&raw mut duplicate).cast(), + 0, + 0, + DUPLICATE_SAME_ACCESS, + ), + litebox_common_windows::nt_status::NtStatus::INVALID_PARAMETER.as_raw() + ); + assert!(duplicate.is_null()); + assert_eq!( + NtClose(source), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + + let source = CreateEventW(core::ptr::null(), 0, 0, core::ptr::null()); + assert!(!source.is_null()); + assert_eq!( + NtDuplicateObject( + usize::MAX as *mut c_void, + source, + usize::MAX as *mut c_void, + core::ptr::null_mut(), + 0, + 0, + DUPLICATE_SAME_ACCESS, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + assert_eq!( + NtClose(source), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + + let source = CreateEventW(core::ptr::null(), 0, 0, core::ptr::null()); + assert!(!source.is_null()); + assert_eq!( + NtDuplicateObject( + usize::MAX as *mut c_void, + source, + usize::MAX as *mut c_void, + core::ptr::dangling_mut::(), + 0, + 0, + DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS, + ), + litebox_common_windows::nt_status::NtStatus::ACCESS_VIOLATION.as_raw() + ); + assert_eq!( + NtSetEvent(source, core::ptr::null_mut()), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + assert_eq!( + NtClose(source), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + + let source = CreateEventW(core::ptr::null(), 0, 0, core::ptr::null()); + assert!(!source.is_null()); + let mut reduced: *mut c_void = core::ptr::null_mut(); + assert_eq!( + NtDuplicateObject( + usize::MAX as *mut c_void, + source, + usize::MAX as *mut c_void, + (&raw mut reduced).cast(), + SYNCHRONIZE, + 0, + 0, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + let mut expanded: *mut c_void = core::ptr::null_mut(); + assert_eq!( + NtDuplicateObject( + usize::MAX as *mut c_void, + reduced, + usize::MAX as *mut c_void, + (&raw mut expanded).cast(), + EVENT_MODIFY_STATE, + 0, + 0, + ), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + assert_eq!( + NtSetEvent(reduced, core::ptr::null_mut()), + litebox_common_windows::nt_status::NtStatus::ACCESS_DENIED.as_raw() + ); + assert_eq!( + NtSetEvent(expanded, core::ptr::null_mut()), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + assert_eq!( + NtClose(source), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + assert_eq!( + NtClose(reduced), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + assert_eq!( + NtClose(expanded), + litebox_common_windows::nt_status::NtStatus::SUCCESS.as_raw() + ); + } +} From e028ac58dc9de23292e51aab2d3e22d30dfcd901 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 21 Jul 2026 13:29:24 -0700 Subject: [PATCH 110/319] Use shared memory for broker pipes (#1052) Each broker connection establishes one sealed memfd after protocol negotiation and reuses it at offset zero for serialized pipe transfers. Pipe requests carry transfer lengths and responses carry byte counts; the host and local adapters stage data through shared memory while BrokerCore remains authoritative for pipe state. --- litebox/src/event/counter.rs | 103 ++-- litebox/src/pipes.rs | 62 ++- litebox_broker_host/src/lib.rs | 496 ++++++++++++++++-- litebox_broker_local/src/lib.rs | 223 ++++++-- litebox_broker_local/src/pipe.rs | 266 +++++++++- litebox_broker_protocol/src/pipe.rs | 23 +- litebox_broker_protocol/src/wire.rs | 9 +- litebox_broker_protocol/src/wire/pipe.rs | 8 +- litebox_broker_protocol/src/wire/primitive.rs | 15 - litebox_broker_transport/src/unix_socket.rs | 22 + litebox_broker_userland/Cargo.toml | 4 +- litebox_broker_userland/src/main.rs | 17 +- .../tests/notification_runtime.rs | 23 +- .../tests/userland_broker.rs | 24 +- litebox_runner_linux_userland/Cargo.toml | 2 +- litebox_runner_linux_userland/src/broker.rs | 9 +- litebox_runner_linux_userland/tests/run.rs | 7 + 17 files changed, 1105 insertions(+), 208 deletions(-) diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 6873cf56c5..c81e478d69 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -201,14 +201,17 @@ mod tests { let consume_attempts = Arc::new(AtomicUsize::new(0)); let read_ready = Arc::new(AtomicBool::new(false)); let request_count = Arc::new(AtomicUsize::new(0)); - let local = BrokerLocal::negotiate(FakeLocalControlChannel { - next_handle: handle.0, - consume_attempts: consume_attempts.clone(), - read_ready: read_ready.clone(), - request_count, - fail_requests: Arc::new(AtomicBool::new(false)), - last_request: None, - }) + let local = BrokerLocal::negotiate( + FakeLocalControlChannel { + next_handle: handle.0, + consume_attempts: consume_attempts.clone(), + read_ready: read_ready.clone(), + request_count, + fail_requests: Arc::new(AtomicBool::new(false)), + last_request: None, + }, + |_| Ok(Arc::new(NoopSharedMemory)), + ) .unwrap(); let litebox = LiteBox::new_with_broker_local(platform, local); let counter = Arc::new(EventCounter::new(&litebox, 0).unwrap()); @@ -257,14 +260,17 @@ mod tests { let handle = ObjectHandle(7); let consume_attempts = Arc::new(AtomicUsize::new(0)); let request_count = Arc::new(AtomicUsize::new(0)); - let local = BrokerLocal::negotiate(FakeLocalControlChannel { - next_handle: handle.0, - consume_attempts: Arc::clone(&consume_attempts), - read_ready: Arc::new(AtomicBool::new(false)), - request_count: Arc::clone(&request_count), - fail_requests: Arc::new(AtomicBool::new(false)), - last_request: None, - }) + let local = BrokerLocal::negotiate( + FakeLocalControlChannel { + next_handle: handle.0, + consume_attempts: Arc::clone(&consume_attempts), + read_ready: Arc::new(AtomicBool::new(false)), + request_count: Arc::clone(&request_count), + fail_requests: Arc::new(AtomicBool::new(false)), + last_request: None, + }, + |_| Ok(Arc::new(NoopSharedMemory)), + ) .unwrap(); let litebox = Arc::new(LiteBox::new_with_broker_local(platform, local)); let counter = Arc::new(EventCounter::new(&litebox, 0).unwrap()); @@ -306,14 +312,17 @@ mod tests { let handle = ObjectHandle(7); let request_count = Arc::new(AtomicUsize::new(0)); let fail_requests = Arc::new(AtomicBool::new(false)); - let local = BrokerLocal::negotiate(FakeLocalControlChannel { - next_handle: handle.0, - consume_attempts: Arc::new(AtomicUsize::new(0)), - read_ready: Arc::new(AtomicBool::new(false)), - request_count: Arc::clone(&request_count), - fail_requests: Arc::clone(&fail_requests), - last_request: None, - }) + let local = BrokerLocal::negotiate( + FakeLocalControlChannel { + next_handle: handle.0, + consume_attempts: Arc::new(AtomicUsize::new(0)), + read_ready: Arc::new(AtomicBool::new(false)), + request_count: Arc::clone(&request_count), + fail_requests: Arc::clone(&fail_requests), + last_request: None, + }, + |_| Ok(Arc::new(NoopSharedMemory)), + ) .unwrap(); let litebox = LiteBox::new_with_broker_local(platform, local); let first = EventCounter::new(&litebox, 0).unwrap(); @@ -340,14 +349,17 @@ mod tests { let platform = MockPlatform::new(); let handle = ObjectHandle(7); let request_count = Arc::new(AtomicUsize::new(0)); - let local = BrokerLocal::negotiate(FakeLocalControlChannel { - next_handle: handle.0, - consume_attempts: Arc::new(AtomicUsize::new(0)), - read_ready: Arc::new(AtomicBool::new(false)), - request_count: Arc::clone(&request_count), - fail_requests: Arc::new(AtomicBool::new(false)), - last_request: None, - }) + let local = BrokerLocal::negotiate( + FakeLocalControlChannel { + next_handle: handle.0, + consume_attempts: Arc::new(AtomicUsize::new(0)), + read_ready: Arc::new(AtomicBool::new(false)), + request_count: Arc::clone(&request_count), + fail_requests: Arc::new(AtomicBool::new(false)), + last_request: None, + }, + |_| Ok(Arc::new(NoopSharedMemory)), + ) .unwrap(); let litebox = LiteBox::new_with_broker_local(platform, local); let counter = EventCounter::new(&litebox, 0).unwrap(); @@ -400,6 +412,33 @@ mod tests { last_request: Option, } + struct NoopSharedMemory; + + impl litebox_broker_protocol::shared_memory::SharedMemory for NoopSharedMemory { + fn len(&self) -> usize { + litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE + } + + fn read( + &self, + _offset: usize, + destination: &mut [u8], + ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + { + destination.fill(0); + Ok(()) + } + + fn write( + &self, + _offset: usize, + _source: &[u8], + ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + { + Ok(()) + } + } + impl LocalControlChannel for FakeLocalControlChannel { type Error = (); diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index 8a3d9c1e99..71bd086c14 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -932,7 +932,7 @@ mod tests { use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, - BrokerResponse, PipeRequest, PipeResponse, ReadinessNotification, + BrokerResponse, PipeRequest, ReadinessNotification, }; use litebox_broker_protocol::pipe::CreatePipeResponse; use litebox_broker_protocol::readiness::ReadinessFlags; @@ -950,12 +950,15 @@ mod tests { let platform = crate::platform::mock::MockPlatform::new(); let request_count = Arc::new(AtomicUsize::new(0)); let force_transport = Arc::new(AtomicBool::new(false)); - let local = BrokerLocal::negotiate(FailingPipeChannel { - last_request: None, - request_count: Arc::clone(&request_count), - read_failure: ReadFailure::Transport, - force_transport, - }) + let local = BrokerLocal::negotiate( + FailingPipeChannel { + last_request: None, + request_count: Arc::clone(&request_count), + read_failure: ReadFailure::Transport, + force_transport, + }, + |_| Ok(Arc::new(NoopSharedMemory)), + ) .unwrap(); let litebox = crate::LiteBox::new_with_broker_local(platform, local); let pipes = super::Pipes::new(&litebox); @@ -994,12 +997,15 @@ mod tests { let platform = crate::platform::mock::MockPlatform::new(); let request_count = Arc::new(AtomicUsize::new(0)); let force_transport = Arc::new(AtomicBool::new(false)); - let local = BrokerLocal::negotiate(FailingPipeChannel { - last_request: None, - request_count: Arc::clone(&request_count), - read_failure: ReadFailure::WouldBlock, - force_transport: Arc::clone(&force_transport), - }) + let local = BrokerLocal::negotiate( + FailingPipeChannel { + last_request: None, + request_count: Arc::clone(&request_count), + read_failure: ReadFailure::WouldBlock, + force_transport: Arc::clone(&force_transport), + }, + |_| Ok(Arc::new(NoopSharedMemory)), + ) .unwrap(); let litebox = Arc::new(crate::LiteBox::new_with_broker_local(platform, local)); let pipes = super::Pipes::new(&litebox); @@ -1111,6 +1117,34 @@ mod tests { force_transport: Arc, } + #[derive(Clone, Copy)] + struct NoopSharedMemory; + + impl litebox_broker_protocol::shared_memory::SharedMemory for NoopSharedMemory { + fn len(&self) -> usize { + litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE + } + + fn read( + &self, + _offset: usize, + destination: &mut [u8], + ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + { + destination.fill(0); + Ok(()) + } + + fn write( + &self, + _offset: usize, + _source: &[u8], + ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + { + Ok(()) + } + } + #[derive(Clone, Copy, Debug)] enum ReadFailure { Transport, @@ -1147,7 +1181,7 @@ mod tests { fn recv_response(&mut self) -> core::result::Result, Self::Error> { match self.last_request.take().unwrap() { BrokerRequest::Pipe(PipeRequest::Create(_)) => Ok(Some(BrokerResponse::Pipe( - PipeResponse::Create(CreatePipeResponse { + litebox_broker_protocol::message::PipeResponse::Create(CreatePipeResponse { read_handle: ObjectHandle(1), write_handle: ObjectHandle(2), }), diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 2cfd154a88..4006152d7f 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -9,9 +9,13 @@ #![no_std] +extern crate alloc; + #[cfg(test)] extern crate std; +use alloc::vec::Vec; + use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential}; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; use litebox_broker_protocol::channel::{ @@ -23,7 +27,10 @@ use litebox_broker_protocol::message::{ BrokerHandshakeResponse, BrokerRequest, BrokerResponse, EventRequest, EventResponse, PipeRequest, PipeResponse, }; -use litebox_broker_protocol::pipe::{CreatePipeResponse, ReadPipeResponse, WritePipeResponse}; +use litebox_broker_protocol::pipe::{ + CreatePipeResponse, PIPE_TRANSFER_BUFFER_SIZE, ReadPipeResponse, WritePipeResponse, +}; +use litebox_broker_protocol::shared_memory::SharedMemory; mod error; @@ -37,15 +44,24 @@ pub use error::{BrokerHostError, Result}; /// broker-initiated readiness wakeups are sent on the notification channel. /// Event mutations caused by control requests return readiness in their control /// response and do not also emit a duplicate notification. +/// +/// `shared_memory` belongs to this association and is reused at offset zero for +/// serialized pipe transfers. `send_shared_memory` runs after version +/// negotiation and before active requests begin. pub fn serve_connection( core: &BrokerCore, control_channel: &mut ControlChannel, _notification_channel: &mut NotificationChannel, + shared_memory: &dyn SharedMemory, + send_shared_memory: impl FnOnce(&mut ControlChannel) -> core::result::Result<(), ChannelError>, ) -> Result where ControlChannel: HostControlChannel, NotificationChannel: HostNotificationChannel, { + if shared_memory.len() != PIPE_TRANSFER_BUFFER_SIZE { + return Err(BrokerHostError::Broker(ErrorCode::Internal)); + } let peer_credential = control_channel .peer_credential() .map_err(BrokerHostError::Channel)?; @@ -54,7 +70,6 @@ where _ => return Err(BrokerHostError::Broker(ErrorCode::PolicyDenied)), }; let session = core.create_session(caller_credential)?; - loop { let request = match control_channel .recv_handshake_request() @@ -86,6 +101,7 @@ where .send_handshake_response(&response) .map_err(BrokerHostError::Channel)?; if negotiated { + send_shared_memory(control_channel).map_err(BrokerHostError::Channel)?; break; } } @@ -105,7 +121,8 @@ where HostReceive::PeerClosed => break, }; - let response = handle_request(&session, request); + let response = complete_request(handle_request(&session, request, shared_memory)) + .map_err(BrokerHostError::Broker)?; control_channel .send_response(&response) .map_err(BrokerHostError::Channel)?; @@ -114,23 +131,55 @@ where Ok(ConnectionTermination::PeerClosed) } -fn handle_request(session: &BrokerSession, request: BrokerRequest) -> BrokerResponse { +type RequestResult = core::result::Result; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RequestFailure { + /// Send an error response and continue serving the association. + Respond(ErrorCode), + /// Terminate the association without sending a response. + Abort(ErrorCode), +} + +fn complete_request( + result: RequestResult, +) -> core::result::Result { + match result { + Ok(response) => Ok(response), + Err(RequestFailure::Respond(error)) => Ok(BrokerResponse::Error(error)), + Err(RequestFailure::Abort(error)) => Err(error), + } +} + +fn handle_request( + session: &BrokerSession, + request: BrokerRequest, + shared_memory: &dyn SharedMemory, +) -> RequestResult { match request { - BrokerRequest::CloseObject(handle) => match session.close_object_reference(handle) { - Ok(()) => BrokerResponse::ObjectClosed, - Err(error) => BrokerResponse::Error(error.into()), - }, - BrokerRequest::CheckReadiness(handle) => match session.check_readiness(handle) { - Ok(readiness) => BrokerResponse::Readiness(readiness), - Err(error) => BrokerResponse::Error(error.into()), - }, - BrokerRequest::Event(request) => handle_event_request(session, request), - BrokerRequest::Pipe(request) => handle_pipe_request(session, request), + BrokerRequest::CloseObject(handle) => session + .close_object_reference(handle) + .map(|()| BrokerResponse::ObjectClosed) + .map_err(|error| RequestFailure::Respond(error.into())), + BrokerRequest::CheckReadiness(handle) => session + .check_readiness(handle) + .map(BrokerResponse::Readiness) + .map_err(|error| RequestFailure::Respond(error.into())), + BrokerRequest::Event(request) => { + handle_event_request(session, request).map(BrokerResponse::Event) + } + BrokerRequest::Pipe(request) => { + handle_pipe_request(session, request, shared_memory).map(BrokerResponse::Pipe) + } } } -fn handle_pipe_request(session: &BrokerSession, request: PipeRequest) -> BrokerResponse { - let response = match request { +fn handle_pipe_request( + session: &BrokerSession, + request: PipeRequest, + shared_memory: &dyn SharedMemory, +) -> RequestResult { + match request { PipeRequest::Create(request) => { litebox_broker_core::pipe::create(session, request.capacity, request.atomic_write_size) .map(|(read_handle, write_handle)| { @@ -139,53 +188,69 @@ fn handle_pipe_request(session: &BrokerSession, request: PipeRequest) -> BrokerR write_handle, }) }) + .map_err(|error| RequestFailure::Respond(error.into())) } PipeRequest::Read(request) => { - litebox_broker_core::pipe::read(session, request.handle, request.length) - .map(|data| PipeResponse::Read(ReadPipeResponse { data })) + if request.length as usize > PIPE_TRANSFER_BUFFER_SIZE { + return Err(RequestFailure::Respond(ErrorCode::MalformedRequest)); + } + let data = litebox_broker_core::pipe::read(session, request.handle, request.length) + .map_err(|error| RequestFailure::Respond(error.into()))?; + shared_memory + .write(0, &data) + .map_err(|_| RequestFailure::Abort(ErrorCode::Internal))?; + Ok(PipeResponse::Read(ReadPipeResponse { + read: data + .len() + .try_into() + .map_err(|_| RequestFailure::Abort(ErrorCode::ResourceExhausted))?, + })) } PipeRequest::Write(request) => { - litebox_broker_core::pipe::write(session, request.handle, &request.data).and_then( - |written| { + if request.length as usize > PIPE_TRANSFER_BUFFER_SIZE { + return Err(RequestFailure::Respond(ErrorCode::MalformedRequest)); + } + let length = request.length as usize; + let mut data = Vec::new(); + if data.try_reserve_exact(length).is_err() { + return Err(RequestFailure::Respond(ErrorCode::OutOfMemory)); + } + data.resize(length, 0); + shared_memory + .read(0, &mut data) + .map_err(|_| RequestFailure::Abort(ErrorCode::Internal))?; + litebox_broker_core::pipe::write(session, request.handle, &data) + .map_err(|error| RequestFailure::Respond(error.into())) + .and_then(|written| { Ok(PipeResponse::Write(WritePipeResponse { written: written .try_into() - .map_err(|_| litebox_broker_core::BrokerError::ResourceExhausted)?, + .map_err(|_| RequestFailure::Abort(ErrorCode::ResourceExhausted))?, })) - }, - ) + }) } - }; - - match response { - Ok(response) => BrokerResponse::Pipe(response), - Err(error) => BrokerResponse::Error(error.into()), } } -fn handle_event_request(session: &BrokerSession, request: EventRequest) -> BrokerResponse { +fn handle_event_request( + session: &BrokerSession, + request: EventRequest, +) -> RequestResult { match request { EventRequest::Create(request) => { - match litebox_broker_core::event::create(session, request.initial_count) { - Ok(handle) => { - BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })) - } - Err(error) => BrokerResponse::Error(error.into()), - } + litebox_broker_core::event::create(session, request.initial_count) + .map(|handle| EventResponse::Create(CreateEventResponse { handle })) + .map_err(|error| RequestFailure::Respond(error.into())) } EventRequest::Add(request) => { - match litebox_broker_core::event::add(session, request.handle, request.value) { - Ok(readiness) => { - BrokerResponse::Event(EventResponse::Add(AddEventResponse { readiness })) - } - Err(error) => BrokerResponse::Error(error.into()), - } + litebox_broker_core::event::add(session, request.handle, request.value) + .map(|readiness| EventResponse::Add(AddEventResponse { readiness })) + .map_err(|error| RequestFailure::Respond(error.into())) } EventRequest::Consume(request) => { - match litebox_broker_core::event::consume(session, request.handle, request.mode) { - Ok(consumption) => BrokerResponse::Event(EventResponse::Consume(consumption)), - Err(error) => BrokerResponse::Error(error.into()), - } + litebox_broker_core::event::consume(session, request.handle, request.mode) + .map(EventResponse::Consume) + .map_err(|error| RequestFailure::Respond(error.into())) } } } @@ -203,12 +268,17 @@ pub enum ConnectionTermination { #[cfg(test)] mod tests { use super::*; + use core::cell::Cell; use litebox_broker_core::{ObjectRights, PolicyEngine}; + use litebox_broker_protocol::channel::HostControlChannel; use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, CreateEventRequest, EventConsumeMode, }; use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerNotification}; + use litebox_broker_protocol::pipe::{CreatePipeRequest, ReadPipeRequest, WritePipeRequest}; + use litebox_broker_protocol::shared_memory::SharedMemoryError; use litebox_broker_protocol::{ObjectHandle, ProtocolVersion}; + use std::sync::{Arc, Mutex}; #[test] fn host_request_handling_uses_one_broker_core() { @@ -219,11 +289,15 @@ mod tests { serve_connection_negotiates_routes_one_request_and_returns_peer_closed(&broker); serve_connection_retries_after_version_mismatch(&broker); + serve_connection_skips_setup_after_version_mismatch(&broker); serve_connection_rejects_active_request_before_negotiation(&broker); serve_connection_rejects_handshake_request_after_negotiation(&broker); serve_connection_returns_channel_error_when_response_send_fails(&broker); serve_connection_returns_event_readiness_in_control_responses(&broker); + serve_connection_continues_after_recoverable_request_failure(&broker); + serve_connection_aborts_without_response_on_shared_memory_failure(&broker); active_request_closes_object_reference(&broker); + association_shared_memory_stages_pipe_data(&broker); } fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { @@ -241,7 +315,14 @@ mod tests { let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel, &mut notifications).unwrap(), + serve_connection( + broker, + &mut channel, + &mut notifications, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + |_| Ok(()), + ) + .unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -272,7 +353,14 @@ mod tests { let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel, &mut notifications).unwrap(), + serve_connection( + broker, + &mut channel, + &mut notifications, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + |_| Ok(()), + ) + .unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -288,6 +376,42 @@ mod tests { ); } + fn serve_connection_skips_setup_after_version_mismatch(broker: &BrokerCore) { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([ + Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: ProtocolVersion(BROKER_PROTOCOL_VERSION.0 - 1), + })), + Ok(HostReceive::PeerClosed), + ]), + std::vec::Vec::new(), + ); + let mut notifications = FakeHostNotificationChannel::default(); + let setup_called = Cell::new(false); + + assert_eq!( + serve_connection( + broker, + &mut channel, + &mut notifications, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + |_| { + setup_called.set(true); + Ok(()) + }, + ) + .unwrap(), + ConnectionTermination::PeerClosed + ); + assert_eq!( + channel.handshake_responses, + [BrokerHandshakeResponse::VersionMismatch { + broker_protocol_version: BROKER_PROTOCOL_VERSION + }] + ); + assert!(!setup_called.get()); + } + fn serve_connection_rejects_active_request_before_negotiation(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::ProtocolViolation)]), @@ -296,7 +420,14 @@ mod tests { let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel, &mut notifications).unwrap(), + serve_connection( + broker, + &mut channel, + &mut notifications, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + |_| Ok(()), + ) + .unwrap(), ConnectionTermination::ProtocolViolation ); assert_eq!( @@ -316,7 +447,14 @@ mod tests { let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel, &mut notifications).unwrap(), + serve_connection( + broker, + &mut channel, + &mut notifications, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + |_| Ok(()), + ) + .unwrap(), ConnectionTermination::ProtocolViolation ); assert_eq!( @@ -341,7 +479,13 @@ mod tests { channel.send_error = true; let mut notifications = FakeHostNotificationChannel::default(); - match serve_connection(broker, &mut channel, &mut notifications) { + match serve_connection( + broker, + &mut channel, + &mut notifications, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + |_| Ok(()), + ) { Err(BrokerHostError::Channel(())) => {} result => panic!("unexpected serve result: {result:?}"), } @@ -361,7 +505,14 @@ mod tests { let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( - serve_connection(broker, &mut channel, &mut notifications).unwrap(), + serve_connection( + broker, + &mut channel, + &mut notifications, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + |_| Ok(()), + ) + .unwrap(), ConnectionTermination::PeerClosed ); assert!(notifications.notifications.is_empty()); @@ -382,11 +533,84 @@ mod tests { ); } + fn serve_connection_continues_after_recoverable_request_failure(broker: &BrokerCore) { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + }))]), + std::vec::Vec::from([ + Ok(HostReceive::Message(BrokerRequest::Pipe( + PipeRequest::Read(ReadPipeRequest { + handle: ObjectHandle(u64::MAX), + length: 1, + }), + ))), + Ok(HostReceive::Message(BrokerRequest::Event( + EventRequest::Create(CreateEventRequest { initial_count: 0 }), + ))), + Ok(HostReceive::PeerClosed), + ]), + ); + let mut notifications = FakeHostNotificationChannel::default(); + + assert_eq!( + serve_connection( + broker, + &mut channel, + &mut notifications, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + |_| Ok(()), + ) + .unwrap(), + ConnectionTermination::PeerClosed + ); + assert_eq!( + channel.responses[0], + BrokerResponse::Error(ErrorCode::UnknownObject) + ); + assert!(matches!( + channel.responses[1], + BrokerResponse::Event(EventResponse::Create(_)) + )); + } + + fn serve_connection_aborts_without_response_on_shared_memory_failure(broker: &BrokerCore) { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + }))]), + std::vec::Vec::from([Ok(HostReceive::Message(BrokerRequest::Pipe( + PipeRequest::Create(CreatePipeRequest { + capacity: 64, + atomic_write_size: 16, + }), + )))]), + ); + channel.enqueue_write_request_after_pipe_create = true; + let mut notifications = FakeHostNotificationChannel::default(); + + assert!(matches!( + serve_connection( + broker, + &mut channel, + &mut notifications, + &FailingSharedMemory, + |_| Ok(()), + ), + Err(BrokerHostError::Broker(ErrorCode::Internal)) + )); + assert_eq!(channel.responses.len(), 1); + assert!(matches!( + channel.responses[0], + BrokerResponse::Pipe(PipeResponse::Create(_)) + )); + } + fn active_request_closes_object_reference(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); - let response = handle_request( + let response = handle_test_request( &session, BrokerRequest::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, @@ -398,15 +622,15 @@ mod tests { let handle = response.handle; assert_eq!( - handle_request(&session, BrokerRequest::CloseObject(handle)), + handle_test_request(&session, BrokerRequest::CloseObject(handle)), BrokerResponse::ObjectClosed ); assert_eq!( - handle_request(&session, BrokerRequest::CheckReadiness(handle)), + handle_test_request(&session, BrokerRequest::CheckReadiness(handle)), BrokerResponse::Error(ErrorCode::UnknownObject) ); assert_eq!( - handle_request( + handle_test_request( &session, BrokerRequest::CloseObject(ObjectHandle(handle.0 + 1)) ), @@ -414,6 +638,83 @@ mod tests { ); } + fn association_shared_memory_stages_pipe_data(broker: &BrokerCore) { + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let memory = TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE); + let created = handle_test_request_with_memory( + &session, + BrokerRequest::Pipe(PipeRequest::Create(CreatePipeRequest { + capacity: 64, + atomic_write_size: 16, + })), + &memory, + ); + let BrokerResponse::Pipe(PipeResponse::Create(response)) = created else { + panic!("expected successful pipe creation"); + }; + + memory.write(0, &[1, 2, 3]).unwrap(); + let write = handle_test_request_with_memory( + &session, + BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { + handle: response.write_handle, + length: 3, + })), + &memory, + ); + assert_eq!( + write, + BrokerResponse::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })) + ); + + let read = handle_test_request_with_memory( + &session, + BrokerRequest::Pipe(PipeRequest::Read(ReadPipeRequest { + handle: response.read_handle, + length: 3, + })), + &memory, + ); + assert_eq!( + read, + BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { read: 3 })) + ); + let mut data = [0; 3]; + memory.read(0, &mut data).unwrap(); + assert_eq!(data, [1, 2, 3]); + + let invalid_range = handle_test_request_with_memory( + &session, + BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { + handle: response.write_handle, + length: u32::try_from(PIPE_TRANSFER_BUFFER_SIZE).unwrap() + 1, + })), + &memory, + ); + assert_eq!( + invalid_range, + BrokerResponse::Error(ErrorCode::MalformedRequest) + ); + } + + fn handle_test_request(session: &BrokerSession, request: BrokerRequest) -> BrokerResponse { + handle_test_request_with_memory( + session, + request, + &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + ) + } + + fn handle_test_request_with_memory( + session: &BrokerSession, + request: BrokerRequest, + shared_memory: &dyn SharedMemory, + ) -> BrokerResponse { + complete_request(handle_request(session, request, shared_memory)).unwrap() + } + struct FakeHostControlChannel { handshake_requests: std::vec::Vec, ()>>, @@ -421,6 +722,7 @@ mod tests { handshake_responses: std::vec::Vec, responses: std::vec::Vec, enqueue_readiness_requests_after_create: bool, + enqueue_write_request_after_pipe_create: bool, send_error: bool, } @@ -437,6 +739,7 @@ mod tests { handshake_responses: std::vec::Vec::new(), responses: std::vec::Vec::new(), enqueue_readiness_requests_after_create: false, + enqueue_write_request_after_pipe_create: false, send_error: false, } } @@ -506,11 +809,94 @@ mod tests { )))); self.requests.push(Ok(HostReceive::PeerClosed)); } + if self.enqueue_write_request_after_pipe_create + && let BrokerResponse::Pipe(PipeResponse::Create(response)) = response + { + self.requests + .push(Ok(HostReceive::Message(BrokerRequest::Pipe( + PipeRequest::Write(WritePipeRequest { + handle: response.write_handle, + length: 1, + }), + )))); + self.requests.push(Ok(HostReceive::PeerClosed)); + } self.responses.push(response.clone()); Ok(()) } } + #[derive(Clone)] + struct TestSharedMemory(Arc>>); + + impl TestSharedMemory { + fn new(length: usize) -> Self { + Self(Arc::new(Mutex::new(std::vec![0; length]))) + } + } + + impl SharedMemory for TestSharedMemory { + fn len(&self) -> usize { + self.0.lock().unwrap().len() + } + + fn read( + &self, + offset: usize, + destination: &mut [u8], + ) -> core::result::Result<(), SharedMemoryError> { + let memory = self.0.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let source = memory + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } + + fn write( + &self, + offset: usize, + source: &[u8], + ) -> core::result::Result<(), SharedMemoryError> { + let mut memory = self.0.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let destination = memory + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } + } + + struct FailingSharedMemory; + + impl SharedMemory for FailingSharedMemory { + fn len(&self) -> usize { + PIPE_TRANSFER_BUFFER_SIZE + } + + fn read( + &self, + _offset: usize, + _destination: &mut [u8], + ) -> core::result::Result<(), SharedMemoryError> { + Err(SharedMemoryError::InvalidRange) + } + + fn write( + &self, + _offset: usize, + _source: &[u8], + ) -> core::result::Result<(), SharedMemoryError> { + Err(SharedMemoryError::InvalidRange) + } + } + #[derive(Default)] struct FakeHostNotificationChannel { notifications: std::vec::Vec, diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 9f652c1595..fc2552c447 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -20,20 +20,28 @@ mod error; mod event; mod pipe; +use alloc::sync::Arc; + use litebox_broker_protocol::channel::{LocalControlChannel, LocalNotificationChannel}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, BrokerResponse, }; +use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_protocol::shared_memory::SharedMemory; use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; pub use error::{BrokerLocalError, Result}; /// Typed broker-local control adapter for broker operations. +/// +/// The shared memory belongs to the broker association and is reused for each +/// serialized pipe transfer. pub struct BrokerLocal { channel: Channel, + shared_memory: Arc, } /// Broker-local receive adapter for broker-initiated asynchronous notifications. @@ -42,13 +50,23 @@ pub struct BrokerNotifications { } impl BrokerLocal { - /// Negotiates the broker protocol over an already-connected control channel. + /// Negotiates the broker protocol, then establishes the association shared + /// memory before active requests begin. /// /// # Panics /// - /// Panics if the broker reports an unrecoverable error or returns a protocol - /// response that does not match the negotiation request. - pub fn negotiate(mut channel: Channel) -> Result { + /// Panics if the broker reports an unrecoverable error, returns a protocol + /// response that does not match the negotiation request, or setup returns + /// shared memory with an invalid size. + pub fn negotiate( + mut channel: Channel, + receive_shared_memory: impl FnOnce( + &mut Channel, + ) -> core::result::Result< + Arc, + Channel::Error, + >, + ) -> Result { let requested = BROKER_PROTOCOL_VERSION; let request = BrokerHandshakeRequest { protocol_version: requested, @@ -68,7 +86,17 @@ impl BrokerLocal { requested, broker_protocol_version, "broker returned unexpected negotiation response: {response:?}" ); - Ok(Self { channel }) + let shared_memory = + receive_shared_memory(&mut channel).map_err(BrokerLocalError::Channel)?; + assert_eq!( + shared_memory.len(), + PIPE_TRANSFER_BUFFER_SIZE, + "broker association shared memory has an invalid size" + ); + Ok(Self { + channel, + shared_memory, + }) } BrokerHandshakeResponse::VersionMismatch { .. } => { Err(BrokerLocalError::Broker(ErrorCode::UnsupportedVersion)) @@ -92,7 +120,10 @@ impl BrokerLocal { /// /// Panics if the broker reports an unrecoverable error or returns a protocol /// response that does not match an active request. - pub fn request(&mut self, request: BrokerRequest) -> Result { + pub(crate) fn request( + &mut self, + request: BrokerRequest, + ) -> Result { self.channel .send_request(&request) .map_err(BrokerLocalError::Channel)?; @@ -179,23 +210,31 @@ impl BrokerNotifications { #[cfg(test)] mod tests { use super::*; + use core::cell::Cell; use core::convert::Infallible; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::ProtocolVersion; use litebox_broker_protocol::channel::LocalNotificationChannel; - use litebox_broker_protocol::event::{CreateEventRequest, CreateEventResponse}; - use litebox_broker_protocol::message::{EventRequest, EventResponse, ReadinessNotification}; + use litebox_broker_protocol::message::ReadinessNotification; use litebox_broker_protocol::readiness::ReadinessFlags; #[test] - fn negotiate_returns_active_local_connection() { + fn negotiate_runs_setup_after_response_before_active_requests() { let channel = FakeControlChannel::new( Some(BrokerHandshakeResponse::Negotiated { broker_protocol_version: BROKER_PROTOCOL_VERSION, }), None, ); - let local = BrokerLocal::negotiate(channel).unwrap(); + let setup_calls = Cell::new(0); + let local = BrokerLocal::negotiate(channel, |channel| { + assert!(channel.sent_handshake_request.is_some()); + assert!(channel.handshake_response.is_none()); + assert!(channel.sent_request.is_none()); + setup_calls.set(setup_calls.get() + 1); + Ok(noop_shared_memory()) + }) + .unwrap(); assert_eq!( local.channel.sent_handshake_request, @@ -203,20 +242,7 @@ mod tests { protocol_version: BROKER_PROTOCOL_VERSION }) ); - } - - #[test] - fn active_request_sends_event_request() { - let handle = ObjectHandle(7); - let request = BrokerRequest::Event(EventRequest::Create(CreateEventRequest { - initial_count: 0, - })); - let response = BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })); - let channel = FakeControlChannel::new(None, Some(response.clone())); - let mut local = BrokerLocal { channel }; - - assert_eq!(local.request(request.clone()).unwrap(), response); - assert_eq!(local.channel.sent_request, Some(request)); + assert_eq!(setup_calls.get(), 1); } #[test] @@ -225,7 +251,10 @@ mod tests { let request = BrokerRequest::CloseObject(handle); let response = BrokerResponse::ObjectClosed; let channel = FakeControlChannel::new(None, Some(response.clone())); - let mut local = BrokerLocal { channel }; + let mut local = BrokerLocal { + channel, + shared_memory: noop_shared_memory(), + }; assert!(local.close_object(handle).is_ok()); assert_eq!(local.channel.sent_request, Some(request)); @@ -233,15 +262,15 @@ mod tests { #[test] fn active_request_returns_recoverable_broker_error() { - let request = BrokerRequest::Event(EventRequest::Create(CreateEventRequest { - initial_count: 0, - })); let channel = FakeControlChannel::new(None, Some(BrokerResponse::Error(ErrorCode::WouldBlock))); - let mut local = BrokerLocal { channel }; + let mut local = BrokerLocal { + channel, + shared_memory: noop_shared_memory(), + }; assert!(matches!( - local.request(request), + local.create_event_with_count(0), Err(BrokerLocalError::Broker(ErrorCode::WouldBlock)) )); } @@ -249,19 +278,18 @@ mod tests { #[test] #[should_panic(expected = "broker returned unrecoverable error")] fn active_request_panics_on_unrecoverable_broker_error() { - let request = BrokerRequest::Event(EventRequest::Create(CreateEventRequest { - initial_count: 0, - })); let channel = FakeControlChannel::new(None, Some(BrokerResponse::Error(ErrorCode::Internal))); - let mut local = BrokerLocal { channel }; + let mut local = BrokerLocal { + channel, + shared_memory: noop_shared_memory(), + }; - let _ = local.request(request); + let _ = local.create_event_with_count(0); } #[test] - #[should_panic(expected = "broker returned unexpected negotiation response")] - fn negotiate_rejects_broker_different_version_response() { + fn negotiate_rejects_broker_different_version_without_setup() { let broker_protocol_version = ProtocolVersion(BROKER_PROTOCOL_VERSION.0 + 1); let channel = FakeControlChannel::new( Some(BrokerHandshakeResponse::Negotiated { @@ -269,8 +297,16 @@ mod tests { }), None, ); + let setup_called = Cell::new(false); - let _ = BrokerLocal::negotiate(channel); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _ = BrokerLocal::negotiate(channel, |_| { + setup_called.set(true); + Ok(noop_shared_memory()) + }); + })); + assert_panic_contains(result, "broker returned unexpected negotiation response"); + assert!(!setup_called.get()); } #[test] @@ -297,21 +333,82 @@ mod tests { None, ); + let setup_called = Cell::new(false); assert!(matches!( - BrokerLocal::negotiate(channel), + BrokerLocal::negotiate(channel, |_| { + setup_called.set(true); + Ok(noop_shared_memory()) + }), Err(BrokerLocalError::Broker(ErrorCode::UnsupportedVersion)) )); + assert!(!setup_called.get()); } #[test] - #[should_panic(expected = "broker returned unrecoverable error")] - fn negotiate_panics_on_unrecoverable_broker_error() { + fn negotiate_skips_setup_before_panicking_on_unrecoverable_broker_error() { let channel = FakeControlChannel::new( Some(BrokerHandshakeResponse::Error(ErrorCode::Internal)), None, ); + let setup_called = Cell::new(false); - let _ = BrokerLocal::negotiate(channel); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _ = BrokerLocal::negotiate(channel, |_| { + setup_called.set(true); + Ok(noop_shared_memory()) + }); + })); + assert_panic_contains(result, "broker returned unrecoverable error"); + assert!(!setup_called.get()); + } + + #[test] + fn negotiate_propagates_shared_memory_receive_error() { + let channel = FakeControlChannel::new( + Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }), + None, + ); + + assert!(matches!( + BrokerLocal::negotiate(channel, |_| Err(FakeChannelError::SharedMemoryReceive)), + Err(BrokerLocalError::Channel( + FakeChannelError::SharedMemoryReceive + )) + )); + } + + #[test] + #[should_panic(expected = "broker association shared memory has an invalid size")] + fn negotiate_rejects_invalid_shared_memory_size() { + let channel = FakeControlChannel::new( + Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }), + None, + ); + + let _ = BrokerLocal::negotiate(channel, |_| { + Ok(Arc::new(NoopSharedMemory { + length: PIPE_TRANSFER_BUFFER_SIZE - 1, + }) as Arc) + }); + } + + fn assert_panic_contains(result: std::thread::Result<()>, expected: &str) { + let panic = result.expect_err("operation did not panic"); + let message = if let Some(message) = panic.downcast_ref::<&str>() { + *message + } else if let Some(message) = panic.downcast_ref::() { + message.as_str() + } else { + panic!("unexpected panic payload"); + }; + assert!( + message.contains(expected), + "panic message did not contain {expected:?}: {message}" + ); } struct FakeControlChannel { @@ -321,6 +418,46 @@ mod tests { response: Option, } + #[derive(Debug, PartialEq, Eq)] + enum FakeChannelError { + SharedMemoryReceive, + } + + struct NoopSharedMemory { + length: usize, + } + + impl SharedMemory for NoopSharedMemory { + fn len(&self) -> usize { + self.length + } + + fn read( + &self, + _offset: usize, + destination: &mut [u8], + ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + { + destination.fill(0); + Ok(()) + } + + fn write( + &self, + _offset: usize, + _source: &[u8], + ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + { + Ok(()) + } + } + + fn noop_shared_memory() -> Arc { + Arc::new(NoopSharedMemory { + length: PIPE_TRANSFER_BUFFER_SIZE, + }) + } + impl FakeControlChannel { const fn new( handshake_response: Option, @@ -336,7 +473,7 @@ mod tests { } impl LocalControlChannel for FakeControlChannel { - type Error = Infallible; + type Error = FakeChannelError; fn send_handshake_request( &mut self, diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs index ed21ad73e1..888af95b89 100644 --- a/litebox_broker_local/src/pipe.rs +++ b/litebox_broker_local/src/pipe.rs @@ -7,7 +7,8 @@ use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse, PipeRequest, PipeResponse}; use litebox_broker_protocol::pipe::{ - CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, WritePipeRequest, + CreatePipeRequest, CreatePipeResponse, PIPE_TRANSFER_BUFFER_SIZE, ReadPipeRequest, + WritePipeRequest, }; use crate::{BrokerLocal, BrokerLocalError, Result}; @@ -24,13 +25,14 @@ impl BrokerLocal { capacity: u64, atomic_write_size: u64, ) -> Result { - match self.request_pipe(PipeRequest::Create(CreatePipeRequest { + let response = self.request_pipe(PipeRequest::Create(CreatePipeRequest { capacity, atomic_write_size, - }))? { - PipeResponse::Create(response) => Ok(response), - response => panic!("broker returned unexpected pipe response: {response:?}"), - } + }))?; + let PipeResponse::Create(response) = response else { + panic!("broker returned unexpected pipe create response: {response:?}"); + }; + Ok(response) } /// Reads bytes from a broker-owned pipe. @@ -44,10 +46,30 @@ impl BrokerLocal { handle: ObjectHandle, length: u32, ) -> Result, Channel::Error> { - match self.request_pipe(PipeRequest::Read(ReadPipeRequest { handle, length }))? { - PipeResponse::Read(response) => Ok(response.data), - response => panic!("broker returned unexpected pipe response: {response:?}"), + if length as usize > PIPE_TRANSFER_BUFFER_SIZE { + return Err(BrokerLocalError::Broker( + litebox_broker_protocol::error::ErrorCode::ResourceExhausted, + )); } + let mut data = Vec::new(); + data.try_reserve_exact(length as usize).map_err(|_| { + BrokerLocalError::Broker(litebox_broker_protocol::error::ErrorCode::OutOfMemory) + })?; + data.resize(length as usize, 0); + let response = self.request_pipe(PipeRequest::Read(ReadPipeRequest { handle, length }))?; + let PipeResponse::Read(response) = response else { + panic!("broker returned unexpected pipe read response: {response:?}"); + }; + assert!( + response.read <= length, + "broker returned oversized pipe read" + ); + let read = response.read as usize; + data.truncate(read); + self.shared_memory + .read(0, &mut data) + .expect("validated shared pipe read range must be accessible"); + Ok(data) } /// Writes bytes to a broker-owned pipe. @@ -61,13 +83,30 @@ impl BrokerLocal { handle: ObjectHandle, data: &[u8], ) -> Result { - match self.request_pipe(PipeRequest::Write(WritePipeRequest { - handle, - data: data.to_vec(), - }))? { - PipeResponse::Write(response) => Ok(response.written as usize), - response => panic!("broker returned unexpected pipe response: {response:?}"), + if data.len() > PIPE_TRANSFER_BUFFER_SIZE { + return Err(BrokerLocalError::Broker( + litebox_broker_protocol::error::ErrorCode::ResourceExhausted, + )); } + self.shared_memory + .write(0, data) + .expect("validated shared pipe write range must be accessible"); + let response = self.request_pipe(PipeRequest::Write(WritePipeRequest { + handle, + length: data + .len() + .try_into() + .expect("shared pipe transfer length must fit in u32"), + }))?; + let PipeResponse::Write(response) = response else { + panic!("broker returned unexpected pipe write response: {response:?}"); + }; + let written = response.written as usize; + assert!( + written <= data.len(), + "broker returned oversized shared pipe write" + ); + Ok(written) } fn request_pipe(&mut self, request: PipeRequest) -> Result { @@ -82,3 +121,200 @@ impl BrokerLocal { } } } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::sync::Arc; + use core::convert::Infallible; + use std::collections::VecDeque; + use std::sync::Mutex; + + use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; + use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerHandshakeResponse}; + use litebox_broker_protocol::pipe::{ReadPipeResponse, WritePipeResponse}; + use litebox_broker_protocol::shared_memory::{SharedMemory, SharedMemoryError}; + + #[test] + fn pipe_uses_attached_shared_memory_for_data_operations() { + let read_handle = ObjectHandle(1); + let write_handle = ObjectHandle(2); + let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); + let channel = ScriptedChannel::new([ + BrokerResponse::Pipe(PipeResponse::Create(CreatePipeResponse { + read_handle, + write_handle, + })), + BrokerResponse::Pipe(PipeResponse::Write(WritePipeResponse { written: 2 })), + BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2 })), + ]); + let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory.clone())).unwrap(); + + local.create_pipe(64, 16).unwrap(); + assert_eq!(local.write_pipe(write_handle, &[1, 2, 3]).unwrap(), 2); + let mut staged = [0; 3]; + memory.read(0, &mut staged).unwrap(); + assert_eq!(staged, [1, 2, 3]); + + memory.write(0, &[4, 5, 6]).unwrap(); + assert_eq!(local.read_pipe(read_handle, 3).unwrap(), [4, 5]); + assert_eq!( + local.channel.sent_requests, + [ + BrokerRequest::Pipe(PipeRequest::Create(CreatePipeRequest { + capacity: 64, + atomic_write_size: 16, + })), + BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { + handle: write_handle, + length: 3, + })), + BrokerRequest::Pipe(PipeRequest::Read(ReadPipeRequest { + handle: read_handle, + length: 3, + })), + ] + ); + } + + #[test] + fn pipe_rejects_oversized_transfers_before_request() { + let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); + let channel = ScriptedChannel::new([]); + let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + let oversized_length = PIPE_TRANSFER_BUFFER_SIZE + 1; + + assert!(matches!( + local.read_pipe(ObjectHandle(1), u32::try_from(oversized_length).unwrap()), + Err(BrokerLocalError::Broker( + litebox_broker_protocol::error::ErrorCode::ResourceExhausted + )) + )); + assert!(matches!( + local.write_pipe(ObjectHandle(2), &std::vec![0; oversized_length]), + Err(BrokerLocalError::Broker( + litebox_broker_protocol::error::ErrorCode::ResourceExhausted + )) + )); + assert!(local.channel.sent_requests.is_empty()); + } + + #[test] + #[should_panic(expected = "broker returned oversized pipe read")] + fn read_pipe_rejects_oversized_response() { + let channel = + ScriptedChannel::new([BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { + read: 2, + }))]); + let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); + let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + + let _ = local.read_pipe(ObjectHandle(1), 1); + } + + #[test] + #[should_panic(expected = "broker returned oversized shared pipe write")] + fn write_pipe_rejects_oversized_response() { + let channel = ScriptedChannel::new([BrokerResponse::Pipe(PipeResponse::Write( + WritePipeResponse { written: 2 }, + ))]); + let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); + let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + + let _ = local.write_pipe(ObjectHandle(1), &[0]); + } + + #[derive(Clone)] + struct TestSharedMemory(Arc>>); + + impl TestSharedMemory { + fn new(length: usize) -> Self { + Self(Arc::new(Mutex::new(std::vec![0; length]))) + } + } + + impl SharedMemory for TestSharedMemory { + fn len(&self) -> usize { + self.0.lock().unwrap().len() + } + + fn read( + &self, + offset: usize, + destination: &mut [u8], + ) -> core::result::Result<(), SharedMemoryError> { + let memory = self.0.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let source = memory + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } + + fn write( + &self, + offset: usize, + source: &[u8], + ) -> core::result::Result<(), SharedMemoryError> { + let mut memory = self.0.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let destination = memory + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } + } + + struct ScriptedChannel { + responses: VecDeque, + sent_requests: Vec, + } + + impl ScriptedChannel { + fn new(responses: impl IntoIterator) -> Self { + Self { + responses: responses.into_iter().collect(), + sent_requests: Vec::new(), + } + } + } + + impl LocalControlChannel for ScriptedChannel { + type Error = 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, + })) + } + + fn send_request( + &mut self, + request: &BrokerRequest, + ) -> core::result::Result<(), Self::Error> { + self.sent_requests.push(request.clone()); + Ok(()) + } + + fn recv_response(&mut self) -> core::result::Result, Self::Error> { + Ok(self.responses.pop_front()) + } + } +} diff --git a/litebox_broker_protocol/src/pipe.rs b/litebox_broker_protocol/src/pipe.rs index 8e812a8b03..512d2e2741 100644 --- a/litebox_broker_protocol/src/pipe.rs +++ b/litebox_broker_protocol/src/pipe.rs @@ -1,16 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use alloc::vec::Vec; - use crate::ObjectHandle; -/// Maximum pipe payload carried by one control-path request or response. +/// Maximum pipe transfer described by one control-path request or response. /// /// This leaves room for the broker envelope and operation metadata within the /// smallest currently supported transport frame. pub const MAX_PIPE_TRANSFER_SIZE: u32 = 32 * 1024; +/// Association shared-memory size required for broker pipe transfers. +pub const PIPE_TRANSFER_BUFFER_SIZE: usize = MAX_PIPE_TRANSFER_SIZE as usize; + /// Request to create a broker-owned byte pipe. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CreatePipeRequest { @@ -38,20 +39,20 @@ pub struct ReadPipeRequest { pub length: u32, } -/// Response containing bytes read from a pipe. -#[derive(Clone, Debug, PartialEq, Eq)] +/// Response describing bytes read into shared memory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ReadPipeResponse { - /// Bytes removed from the pipe. - pub data: Vec, + /// Number of bytes placed in the read region. + pub read: u32, } -/// Request to write bytes to a pipe endpoint. -#[derive(Clone, Debug, PartialEq, Eq)] +/// Request to write bytes staged in shared memory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct WritePipeRequest { /// Write endpoint handle. pub handle: ObjectHandle, - /// Bytes to append to the pipe. - pub data: Vec, + /// Number of staged bytes to write. + pub length: u32, } /// Response describing a completed pipe write. diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 00935d94b4..12c3492e71 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -328,10 +328,7 @@ mod tests { atomic_write_size: 512, })), BrokerRequest::Pipe(PipeRequest::Read(ReadPipeRequest { handle, length: 32 })), - BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { - handle, - data: Vec::from([1, 2, 3]), - })), + BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { handle, length: 3 })), ]; for request in requests { @@ -382,9 +379,7 @@ mod tests { read_handle: handle, write_handle: ObjectHandle(14), })), - BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { - data: Vec::from([1, 2, 3]), - })), + BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { read: 3 })), BrokerResponse::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })), BrokerResponse::Error(ErrorCode::PolicyDenied), BrokerResponse::Error(ErrorCode::WouldBlock), diff --git a/litebox_broker_protocol/src/wire/pipe.rs b/litebox_broker_protocol/src/wire/pipe.rs index 1ef8e0fee3..1d6a86bdd2 100644 --- a/litebox_broker_protocol/src/wire/pipe.rs +++ b/litebox_broker_protocol/src/wire/pipe.rs @@ -33,7 +33,7 @@ pub(super) fn encode_pipe_request(encoder: &mut Encoder, request: PipeRequest) { PipeRequest::Write(request) => { encoder.u8(PIPE_REQUEST_TAG_WRITE); encoder.handle(request.handle); - encoder.bytes(&request.data); + encoder.u32(request.length); } } } @@ -50,7 +50,7 @@ pub(super) fn decode_pipe_request(decoder: &mut Decoder<'_>) -> Result Ok(PipeRequest::Write(WritePipeRequest { handle: decoder.handle()?, - data: decoder.bytes()?, + length: decoder.u32()?, })), _ => Err(WireError::InvalidTag), } @@ -65,7 +65,7 @@ pub(super) fn encode_pipe_response(encoder: &mut Encoder, response: PipeResponse } PipeResponse::Read(response) => { encoder.u8(PIPE_RESPONSE_TAG_READ); - encoder.bytes(&response.data); + encoder.u32(response.read); } PipeResponse::Write(response) => { encoder.u8(PIPE_RESPONSE_TAG_WRITTEN); @@ -81,7 +81,7 @@ pub(super) fn decode_pipe_response(decoder: &mut Decoder<'_>) -> Result Ok(PipeResponse::Read(ReadPipeResponse { - data: decoder.bytes()?, + read: decoder.u32()?, })), PIPE_RESPONSE_TAG_WRITTEN => Ok(PipeResponse::Write(WritePipeResponse { written: decoder.u32()?, diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs index e06e73dc9a..617f411f7f 100644 --- a/litebox_broker_protocol/src/wire/primitive.rs +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -33,16 +33,6 @@ impl Encoder { self.bytes.extend_from_slice(&value.to_le_bytes()); } - pub(super) fn bytes(&mut self, value: &[u8]) { - self.u64( - value - .len() - .try_into() - .expect("broker byte payload length exceeds u64"), - ); - self.bytes.extend_from_slice(value); - } - pub(super) fn protocol_version(&mut self, version: ProtocolVersion) { self.u16(version.0); } @@ -92,11 +82,6 @@ impl<'a> Decoder<'a> { ])) } - pub(super) fn bytes(&mut self) -> Result, WireError> { - let len = usize::try_from(self.u64()?).map_err(|_| WireError::OffsetOverflow)?; - Ok(self.take(len)?.to_vec()) - } - pub(super) fn protocol_version(&mut self) -> Result { Ok(ProtocolVersion(self.u16()?)) } diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 2f3c5ee250..fdc585930b 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -13,6 +13,8 @@ use std::os::unix::net::UnixStream; use std::path::Path; use std::time::{Duration, Instant}; +#[cfg(all(feature = "linux-shared-memory", target_os = "linux"))] +use crate::shared_memory::MemfdSharedMemory; use litebox_broker_protocol::channel::{ HostControlChannel, HostNotificationChannel, HostReceive, LocalControlChannel, LocalNotificationChannel, PeerCredential, @@ -75,6 +77,16 @@ impl UnixStreamLocalControlChannel { .try_clone() .map(|stream| UnixStreamLocalControlCancellation { stream }) } + + /// Receives the memfd associated with this control channel. + #[cfg(all(feature = "linux-shared-memory", target_os = "linux"))] + pub fn receive_memfd( + &mut self, + expected_len: usize, + deadline: Option, + ) -> IoResult { + crate::shared_memory::receive_memfd(&mut self.stream, expected_len, deadline) + } } impl UnixStreamLocalControlCancellation { @@ -113,6 +125,16 @@ impl UnixStreamHostControlChannel { pub const fn from_accepted(stream: UnixStream) -> Self { Self { stream } } + + /// Sends the memfd associated with this control channel. + #[cfg(all(feature = "linux-shared-memory", target_os = "linux"))] + pub fn send_memfd( + &mut self, + shared_memory: &MemfdSharedMemory, + deadline: Option, + ) -> IoResult<()> { + crate::shared_memory::send_memfd(&mut self.stream, shared_memory, deadline) + } } impl UnixStreamLocalNotificationChannel { diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index bcb95def82..6cceb029cf 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -7,7 +7,8 @@ edition = "2024" clap = { version = "4.5.33", features = ["derive"] } litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0", features = ["unix"] } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0", features = ["linux-shared-memory", "unix"] } tempfile = { version = "3", default-features = false } [[bin]] @@ -22,7 +23,6 @@ harness = false [dev-dependencies] libc = { version = "0.2.169", default-features = false } litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0" } -litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index cfd0cebb36..ad9aa1c1c3 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -6,14 +6,19 @@ use std::ffi::OsString; use std::os::unix::net::UnixListener; use std::path::PathBuf; use std::process::Command; +use std::time::{Duration, Instant}; use clap::Parser; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::serve_connection; +use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; +use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, }; +const SETUP_TIMEOUT: Duration = Duration::from_secs(5); + #[derive(Parser, Debug)] struct CliArgs { /// Local runner executable to launch. @@ -54,6 +59,8 @@ fn main() -> Result<(), Box> { loop { let (control_stream, _) = control_listener.accept()?; + let setup_deadline = Instant::now() + SETUP_TIMEOUT; + let shared_memory = MemfdSharedMemory::create(PIPE_TRANSFER_BUFFER_SIZE)?; let (notification_stream, _) = notification_listener.accept()?; let broker = broker.clone(); if let Err(error) = std::thread::Builder::new() @@ -63,9 +70,13 @@ fn main() -> Result<(), Box> { UnixStreamHostControlChannel::from_accepted(control_stream); let mut notification_channel = UnixStreamHostNotificationChannel::from_accepted(notification_stream); - if let Err(error) = - serve_connection(&broker, &mut control_channel, &mut notification_channel) - { + if let Err(error) = serve_connection( + &broker, + &mut control_channel, + &mut notification_channel, + &shared_memory, + |channel| channel.send_memfd(&shared_memory, Some(setup_deadline)), + ) { eprintln!("failed to serve broker connection: {error}"); } }) diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index 37b309e2de..5b96813c13 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -2,11 +2,14 @@ // Licensed under the MIT license. use std::os::unix::net::UnixStream; +use std::sync::Arc; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{ConnectionTermination, serve_connection}; use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, UnixStreamLocalControlChannel, }; @@ -19,16 +22,28 @@ fn host_serves_control_requests_over_paired_userland_channels() { .unwrap(); let (local_control, host_control) = UnixStream::pair().unwrap(); let (_local_notification, host_notification) = UnixStream::pair().unwrap(); + let host_shared_memory = MemfdSharedMemory::create(PIPE_TRANSFER_BUFFER_SIZE).unwrap(); let host_thread = std::thread::spawn(move || { let mut control = UnixStreamHostControlChannel::from_accepted(host_control); let mut notification = UnixStreamHostNotificationChannel::from_accepted(host_notification); - serve_connection(&broker, &mut control, &mut notification) + serve_connection( + &broker, + &mut control, + &mut notification, + &host_shared_memory, + |channel| channel.send_memfd(&host_shared_memory, None), + ) }); - let mut local = - BrokerLocal::negotiate(UnixStreamLocalControlChannel::from_connected(local_control)) - .unwrap(); + let mut local = BrokerLocal::negotiate( + UnixStreamLocalControlChannel::from_connected(local_control), + |channel| { + let shared_memory = channel.receive_memfd(PIPE_TRANSFER_BUFFER_SIZE, None)?; + Ok(Arc::new(shared_memory)) + }, + ) + .unwrap(); let handle = local.create_event_with_count(0).unwrap(); let readiness = ReadinessFlags::READ | ReadinessFlags::WRITE; diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 89b89b6c1e..1ec8a13799 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -6,9 +6,11 @@ use std::io::{Error, ErrorKind, Result}; use std::os::unix::process::ExitStatusExt; use std::path::Path; use std::process::{Child, Command}; +use std::sync::Arc; use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, @@ -81,7 +83,14 @@ fn run_fake_runner(args: &[OsString]) { let control_channel = connect_control_with_retry(Path::new(control_socket_path)).unwrap(); let _notification_channel = connect_notification_with_retry(Path::new(notification_socket_path)).unwrap(); - let mut local = BrokerLocal::negotiate(control_channel).unwrap(); + let mut local = BrokerLocal::negotiate(control_channel, |channel| { + let shared_memory = channel.receive_memfd( + PIPE_TRANSFER_BUFFER_SIZE, + Some(Instant::now() + Duration::from_secs(5)), + )?; + Ok(Arc::new(shared_memory)) + }) + .unwrap(); let handle = local.create_event_with_count(0).unwrap(); assert_eq!( @@ -96,6 +105,19 @@ fn run_fake_runner(args: &[OsString]) { local.check_readiness(handle).unwrap(), ReadinessFlags::READ | ReadinessFlags::WRITE ); + + let pipe = local.create_pipe(64, 16).unwrap(); + let data = b"shared pipe data"; + assert_eq!( + local.write_pipe(pipe.write_handle, data).unwrap(), + data.len() + ); + assert_eq!( + local + .read_pipe(pipe.read_handle, data.len().try_into().unwrap()) + .unwrap(), + data + ); drop(local); // SAFETY: `getppid` takes no pointer arguments and has no Rust-side aliasing requirements. diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 6ead05a5ac..27d2bdb3c2 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -10,7 +10,7 @@ libc = { version = "0.2.169", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } -litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport", features = ["unix"] } +litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport", features = ["linux-shared-memory", "unix"] } 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_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_linux_userland"] } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index c17b75d2c8..150425b647 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -3,12 +3,14 @@ use std::{ path::Path, + sync::Arc, time::{Duration, Instant}, }; use anyhow::{Context as _, Result}; use litebox_broker_local::{BrokerLocal, BrokerNotifications}; use litebox_broker_protocol::message::BrokerNotification; +use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlCancellation, UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, @@ -53,7 +55,12 @@ pub(crate) fn connect( let control_cancellation = control_channel .cancellation_handle() .context("failed to create broker control cancellation handle")?; - let local = BrokerLocal::negotiate(control_channel).context("broker negotiation failed")?; + let local = BrokerLocal::negotiate(control_channel, |channel| { + let shared_memory = + channel.receive_memfd(PIPE_TRANSFER_BUFFER_SIZE, Some(setup_deadline))?; + Ok(Arc::new(shared_memory)) + }) + .context("broker negotiation failed")?; Ok(( local, BrokerNotifications::new(notification_channel), diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index a0fac43672..54abc5d4ca 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -340,6 +340,11 @@ fn spawn_test_broker( let (control_stream, _) = control_listener .accept() .expect("failed to accept broker local control connection"); + let shared_memory = + litebox_broker_transport::shared_memory::MemfdSharedMemory::create( + litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE, + ) + .expect("failed to create broker test shared memory"); let (notification_stream, _) = notification_listener .accept() .expect("failed to accept broker local notification connection"); @@ -365,6 +370,8 @@ fn spawn_test_broker( &broker, &mut channel, &mut notification_channel, + &shared_memory, + |channel| channel.inner.send_memfd(&shared_memory, None), ) .expect("broker host failed"); assert_eq!( From f85610392e71f2e5a6168fa2ae9e89a3e2286a88 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 21 Jul 2026 15:39:21 -0700 Subject: [PATCH 111/319] Authenticate Unix broker channels (#1058) Authenticate the broker control and notification sockets with Linux peer credentials before serving the spawned runner. Both channels must belong to the same live child process before the host grants broker rights through the transport-neutral host-guaranteed identity. Setup acceptance, handshake I/O, and shared-memory transfer share one deadline, and the broker serves and reaps one runner association. --- Cargo.lock | 1 - litebox_broker_core/src/policy.rs | 27 +- litebox_broker_core/src/session.rs | 2 + litebox_broker_host/src/lib.rs | 1 + litebox_broker_protocol/src/channel.rs | 3 + litebox_broker_transport/Cargo.toml | 3 +- litebox_broker_transport/src/lib.rs | 7 +- litebox_broker_transport/src/shared_memory.rs | 68 +---- litebox_broker_transport/src/unix_io.rs | 73 ++++++ litebox_broker_transport/src/unix_socket.rs | 239 +++++++++++++----- litebox_broker_userland/Cargo.toml | 3 +- litebox_broker_userland/src/main.rs | 148 ++++++++--- .../tests/userland_broker.rs | 16 +- litebox_runner_linux_userland/Cargo.toml | 2 +- litebox_runner_linux_userland/src/lib.rs | 33 +-- litebox_runner_linux_userland/tests/run.rs | 12 +- 16 files changed, 436 insertions(+), 202 deletions(-) create mode 100644 litebox_broker_transport/src/unix_io.rs diff --git a/Cargo.lock b/Cargo.lock index 702f5c2df1..8587bd2dea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1525,7 +1525,6 @@ name = "litebox_broker_userland" version = "0.1.0" dependencies = [ "clap", - "libc", "litebox_broker_core", "litebox_broker_host", "litebox_broker_local", diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index 27d9da65f6..4110910603 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -14,6 +14,11 @@ pub enum PolicyProfile { /// Rights for the unauthenticated principal used by the initial POC. unauthenticated: ObjectRights, }, + /// Static rights for a principal authenticated by the broker entry layer. + HostGuaranteed { + /// Rights granted to the host-guaranteed principal. + rights: ObjectRights, + }, } /// Broker policy decision and audit component. @@ -42,6 +47,11 @@ impl PolicyEngine { Self::new(PolicyProfile::Static { unauthenticated }) } + /// Creates a policy engine with rights for a host-guaranteed principal. + pub const fn with_host_guaranteed_rights(rights: ObjectRights) -> Self { + Self::new(PolicyProfile::HostGuaranteed { rights }) + } + pub(crate) fn principal_object_rights( &self, caller_credential: CallerCredential, @@ -50,7 +60,8 @@ impl PolicyEngine { (PolicyProfile::Static { unauthenticated }, CallerCredential::Unauthenticated) => { unauthenticated } - (PolicyProfile::DefaultDeny, _) => return Err(BrokerError::PolicyDenied), + (PolicyProfile::HostGuaranteed { rights }, CallerCredential::HostGuaranteed) => rights, + _ => return Err(BrokerError::PolicyDenied), }; if rights.is_empty() { return Err(BrokerError::PolicyDenied); @@ -89,6 +100,20 @@ mod tests { ); } + #[test] + fn host_guaranteed_policy_returns_configured_principal_rights() { + let policy = PolicyEngine::with_host_guaranteed_rights(ObjectRights::WAIT); + + assert_eq!( + policy.principal_object_rights(CallerCredential::HostGuaranteed), + Ok(ObjectRights::WAIT) + ); + assert_eq!( + policy.principal_object_rights(CallerCredential::Unauthenticated), + Err(BrokerError::PolicyDenied) + ); + } + #[test] fn empty_principal_rights_deny_object_authorization() { let policy = PolicyEngine::with_unauthenticated_rights(ObjectRights::empty()); diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index e64cf8bda3..b673ef4cac 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -19,6 +19,8 @@ use spin::rwlock::RwLock; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum CallerCredential { + /// The trusted broker entry layer authenticated and bound the caller. + HostGuaranteed, /// Explicit deployment mode for the initial unauthenticated userland POC. Unauthenticated, } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 4006152d7f..01f1e1ffc7 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -66,6 +66,7 @@ where .peer_credential() .map_err(BrokerHostError::Channel)?; let caller_credential = match peer_credential { + PeerCredential::HostGuaranteed => CallerCredential::HostGuaranteed, PeerCredential::Unauthenticated => CallerCredential::Unauthenticated, _ => return Err(BrokerHostError::Broker(ErrorCode::PolicyDenied)), }; diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index 06a71b46de..8b8cc51981 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -14,6 +14,9 @@ use crate::message::{ #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum PeerCredential { + /// The trusted host or deployment authenticated and bound the peer before + /// constructing the channel. + HostGuaranteed, /// Explicit deployment mode for the initial unauthenticated userland POC. /// /// Channels that are expected to authenticate peers must return an error diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml index 151e4bfea5..3fc4be1087 100644 --- a/litebox_broker_transport/Cargo.toml +++ b/litebox_broker_transport/Cargo.toml @@ -5,8 +5,7 @@ edition = "2024" [features] std = [] -linux-shared-memory = ["std", "dep:libc", "dep:rustix", "rustix/fs", "rustix/net"] -unix = ["std"] +linux-userland = ["std", "dep:libc", "dep:rustix", "rustix/fs", "rustix/net"] [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs index 111c84ebd1..8d2f7a045c 100644 --- a/litebox_broker_transport/src/lib.rs +++ b/litebox_broker_transport/src/lib.rs @@ -9,8 +9,11 @@ //! protocol messages, local-side adapters, host-side request handling, and core //! authority state live in separate crates. -#[cfg(all(feature = "linux-shared-memory", target_os = "linux"))] +#[cfg(all(feature = "linux-userland", target_os = "linux"))] pub mod shared_memory; -#[cfg(all(feature = "unix", unix))] +#[cfg(all(feature = "linux-userland", target_os = "linux"))] pub mod unix_socket; + +#[cfg(all(feature = "linux-userland", target_os = "linux"))] +mod unix_io; diff --git a/litebox_broker_transport/src/shared_memory.rs b/litebox_broker_transport/src/shared_memory.rs index 58f5b057c2..2c826cf0f8 100644 --- a/litebox_broker_transport/src/shared_memory.rs +++ b/litebox_broker_transport/src/shared_memory.rs @@ -9,7 +9,7 @@ use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; use std::os::unix::net::UnixStream; use std::ptr::NonNull; use std::sync::Mutex; -use std::time::{Duration, Instant}; +use std::time::Instant; use rustix::fs::{ MemfdFlags, SealFlags, fcntl_add_seals, fcntl_get_seals, fstat, ftruncate, memfd_create, @@ -22,6 +22,10 @@ use rustix::net::{ use litebox_broker_protocol::shared_memory::{SharedMemory, SharedMemoryError}; +use crate::unix_io::{ + refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, +}; + const REQUIRED_MEMFD_SEALS: SealFlags = SealFlags::from_bits_retain( SealFlags::GROW.bits() | SealFlags::SHRINK.bits() | SealFlags::SEAL.bits(), ); @@ -280,67 +284,6 @@ fn receive_fd(stream: &mut UnixStream, deadline: Option) -> IoResult( - stream: &mut UnixStream, - deadline: Option, - operation: impl FnOnce(&mut UnixStream, Option) -> IoResult, -) -> IoResult { - let Some(_) = deadline else { - return operation(stream, None); - }; - let previous = stream.read_timeout()?; - let result = operation(stream, deadline); - combine_result_with_restore(result, stream.set_read_timeout(previous)) -} - -fn with_write_deadline( - stream: &mut UnixStream, - deadline: Option, - operation: impl FnOnce(&mut UnixStream, Option) -> IoResult, -) -> IoResult { - let Some(_) = deadline else { - return operation(stream, None); - }; - let previous = stream.write_timeout()?; - let result = operation(stream, deadline); - combine_result_with_restore(result, stream.set_write_timeout(previous)) -} - -fn refresh_read_deadline(stream: &UnixStream, deadline: Option) -> IoResult<()> { - if let Some(deadline) = deadline { - stream.set_read_timeout(Some(io_timeout_for_deadline(deadline)?))?; - } - Ok(()) -} - -fn refresh_write_deadline(stream: &UnixStream, deadline: Option) -> IoResult<()> { - if let Some(deadline) = deadline { - stream.set_write_timeout(Some(io_timeout_for_deadline(deadline)?))?; - } - Ok(()) -} - -fn combine_result_with_restore( - result: IoResult, - restore: IoResult<()>, -) -> IoResult { - match (result, restore) { - (Ok(output), Ok(())) => Ok(output), - (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error), - (Err(operation), Err(restore)) => Err(Error::new( - operation.kind(), - format!("{operation}; additionally failed to restore socket timeout: {restore}"), - )), - } -} - -fn io_timeout_for_deadline(deadline: Instant) -> IoResult { - deadline - .checked_duration_since(Instant::now()) - .filter(|timeout| !timeout.is_zero()) - .ok_or_else(|| Error::new(ErrorKind::TimedOut, "shared-memory setup deadline expired")) -} - impl Drop for MappedRegion { fn drop(&mut self) { // SAFETY: `address` and `length` describe the mapping exclusively owned @@ -359,6 +302,7 @@ mod tests { use super::*; use rustix::io::FdFlags; use std::io::Write; + use std::time::Duration; #[test] fn mappings_share_bytes_and_validate_ranges() { diff --git a/litebox_broker_transport/src/unix_io.rs b/litebox_broker_transport/src/unix_io.rs new file mode 100644 index 0000000000..6d3f573a70 --- /dev/null +++ b/litebox_broker_transport/src/unix_io.rs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::io::{Error, ErrorKind, Result as IoResult}; +use std::os::unix::net::UnixStream; +use std::time::{Duration, Instant}; + +pub(crate) fn with_read_deadline( + stream: &mut UnixStream, + deadline: Option, + operation: impl FnOnce(&mut UnixStream, Option) -> IoResult, +) -> IoResult { + let Some(_) = deadline else { + return operation(stream, None); + }; + let previous = stream.read_timeout()?; + let result = operation(stream, deadline); + combine_result_with_restore(result, stream.set_read_timeout(previous)) +} + +pub(crate) fn with_write_deadline( + stream: &mut UnixStream, + deadline: Option, + operation: impl FnOnce(&mut UnixStream, Option) -> IoResult, +) -> IoResult { + let Some(_) = deadline else { + return operation(stream, None); + }; + let previous = stream.write_timeout()?; + let result = operation(stream, deadline); + combine_result_with_restore(result, stream.set_write_timeout(previous)) +} + +pub(crate) fn refresh_read_deadline( + stream: &UnixStream, + deadline: Option, +) -> IoResult<()> { + if let Some(deadline) = deadline { + stream.set_read_timeout(Some(io_timeout_for_deadline(deadline)?))?; + } + Ok(()) +} + +pub(crate) fn refresh_write_deadline( + stream: &UnixStream, + deadline: Option, +) -> IoResult<()> { + if let Some(deadline) = deadline { + stream.set_write_timeout(Some(io_timeout_for_deadline(deadline)?))?; + } + Ok(()) +} + +fn combine_result_with_restore( + result: IoResult, + restore: IoResult<()>, +) -> IoResult { + match (result, restore) { + (Ok(output), Ok(())) => Ok(output), + (Err(error), Ok(())) | (Ok(_), Err(error)) => Err(error), + (Err(operation), Err(restore)) => Err(Error::new( + operation.kind(), + format!("{operation}; additionally failed to restore socket timeout: {restore}"), + )), + } +} + +fn io_timeout_for_deadline(deadline: Instant) -> IoResult { + deadline + .checked_duration_since(Instant::now()) + .filter(|timeout| !timeout.is_zero()) + .ok_or_else(|| Error::new(ErrorKind::TimedOut, "broker setup deadline expired")) +} diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index fdc585930b..66d908ee9d 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -11,10 +11,12 @@ use std::io::{Error, ErrorKind, Read, Result as IoResult, Write}; use std::net::Shutdown; use std::os::unix::net::UnixStream; use std::path::Path; -use std::time::{Duration, Instant}; +use std::time::Instant; -#[cfg(all(feature = "linux-shared-memory", target_os = "linux"))] use crate::shared_memory::MemfdSharedMemory; +use crate::unix_io::{ + refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, +}; use litebox_broker_protocol::channel::{ HostControlChannel, HostNotificationChannel, HostReceive, LocalControlChannel, LocalNotificationChannel, PeerCredential, @@ -31,6 +33,34 @@ use litebox_broker_protocol::wire::{ const MAX_FRAME_LEN: usize = 64 * 1024; +/// Validates that a connected Unix socket belongs to `expected_process_id`. +pub fn validate_peer_process(stream: &UnixStream, expected_process_id: u32) -> IoResult<()> { + if peer_process_id(stream)? != expected_process_id { + return Err(Error::new( + ErrorKind::PermissionDenied, + "Unix socket peer is not the expected process", + )); + } + Ok(()) +} + +/// Validates that two connected Unix sockets belong to the same process. +pub fn validate_same_peer_process(first: &UnixStream, second: &UnixStream) -> IoResult<()> { + if peer_process_id(first)? != peer_process_id(second)? { + return Err(Error::new( + ErrorKind::PermissionDenied, + "Unix sockets belong to different peer processes", + )); + } + Ok(()) +} + +fn peer_process_id(stream: &UnixStream) -> IoResult { + let credentials = rustix::net::sockopt::socket_peercred(stream)?; + u32::try_from(credentials.pid.as_raw_pid()) + .map_err(|_| invalid_data("Unix peer process ID is invalid")) +} + /// Local-side Unix-domain-socket control channel for the hosted userland POC. pub struct UnixStreamLocalControlChannel { stream: UnixStream, @@ -79,7 +109,6 @@ impl UnixStreamLocalControlChannel { } /// Receives the memfd associated with this control channel. - #[cfg(all(feature = "linux-shared-memory", target_os = "linux"))] pub fn receive_memfd( &mut self, expected_len: usize, @@ -108,6 +137,8 @@ impl Drop for UnixStreamLocalControlChannel { /// Host-side Unix-domain-socket control channel for the hosted userland POC. pub struct UnixStreamHostControlChannel { stream: UnixStream, + peer_credential: PeerCredential, + setup_deadline: Option, } /// Local-side Unix-domain-socket notification channel for the hosted userland POC. @@ -123,11 +154,24 @@ pub struct UnixStreamHostNotificationChannel { impl UnixStreamHostControlChannel { /// Creates a host control channel from an accepted Unix stream. pub const fn from_accepted(stream: UnixStream) -> Self { - Self { stream } + Self { + stream, + peer_credential: PeerCredential::Unauthenticated, + setup_deadline: None, + } + } + + /// Creates a host control channel after the deployment has authenticated + /// and bound the accepted peer. `setup_deadline` bounds handshake I/O. + pub const fn from_host_guaranteed(stream: UnixStream, setup_deadline: Instant) -> Self { + Self { + stream, + peer_credential: PeerCredential::HostGuaranteed, + setup_deadline: Some(setup_deadline), + } } /// Sends the memfd associated with this control channel. - #[cfg(all(feature = "linux-shared-memory", target_os = "linux"))] pub fn send_memfd( &mut self, shared_memory: &MemfdSharedMemory, @@ -166,10 +210,7 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { fn recv_handshake_response(&mut self) -> IoResult> { let frame = read_frame_with_deadline(&mut self.stream, self.setup_deadline)?; - if self.setup_deadline.take().is_some() { - self.stream.set_read_timeout(None)?; - self.stream.set_write_timeout(None)?; - } + self.setup_deadline = None; match frame { Some(frame) => decode_handshake_response(&frame) .map(Some) @@ -195,13 +236,11 @@ impl HostControlChannel for UnixStreamHostControlChannel { type Error = Error; fn peer_credential(&self) -> IoResult { - // TODO(broker): replace the PoC placeholder with Unix peer credential extraction - // before this channel is used as an authenticated deployment boundary. - Ok(PeerCredential::Unauthenticated) + Ok(self.peer_credential) } fn recv_handshake_request(&mut self) -> IoResult> { - let Some(frame) = read_frame_with_deadline(&mut self.stream, None)? else { + let Some(frame) = read_frame_with_deadline(&mut self.stream, self.setup_deadline)? else { return Ok(HostReceive::PeerClosed); }; match decode_handshake_request(&frame) { @@ -215,8 +254,12 @@ impl HostControlChannel for UnixStreamHostControlChannel { write_frame_with_deadline( &mut self.stream, &encode_handshake_response(response.clone()), - None, - ) + self.setup_deadline, + )?; + if matches!(response, BrokerHandshakeResponse::Negotiated { .. }) { + self.setup_deadline = None; + } + Ok(()) } fn recv_request(&mut self) -> IoResult> { @@ -262,36 +305,38 @@ fn read_frame_with_deadline( stream: &mut UnixStream, deadline: Option, ) -> IoResult>> { - let mut len_buf = [0; 4]; - let mut read = 0; - while read < len_buf.len() { - refresh_stream_io_deadline(stream, deadline)?; - match stream.read(&mut len_buf[read..]) { - Ok(0) if read == 0 => return Ok(None), - Ok(0) => return Err(invalid_data("truncated broker frame length")), - Ok(len) => read += len, - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) => return Err(error), + with_read_deadline(stream, deadline, |stream, deadline| { + let mut len_buf = [0; 4]; + let mut read = 0; + while read < len_buf.len() { + refresh_read_deadline(stream, deadline)?; + match stream.read(&mut len_buf[read..]) { + Ok(0) if read == 0 => return Ok(None), + Ok(0) => return Err(invalid_data("truncated broker frame length")), + Ok(len) => read += len, + Err(error) if error.kind() == ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } } - } - let len = u32::from_le_bytes(len_buf) as usize; - if len == 0 || len > MAX_FRAME_LEN { - return Err(invalid_data("invalid broker frame length")); - } + let len = u32::from_le_bytes(len_buf) as usize; + if len == 0 || len > MAX_FRAME_LEN { + return Err(invalid_data("invalid broker frame length")); + } - let mut frame = vec![0; len]; - let mut read = 0; - while read < frame.len() { - refresh_stream_io_deadline(stream, deadline)?; - match stream.read(&mut frame[read..]) { - Ok(0) => return Err(invalid_data("truncated broker frame")), - Ok(len) => read += len, - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) => return Err(error), + let mut frame = vec![0; len]; + let mut read = 0; + while read < frame.len() { + refresh_read_deadline(stream, deadline)?; + match stream.read(&mut frame[read..]) { + Ok(0) => return Err(invalid_data("truncated broker frame")), + Ok(len) => read += len, + Err(error) if error.kind() == ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } } - } - Ok(Some(frame)) + Ok(Some(frame)) + }) } fn write_frame_with_deadline( @@ -299,12 +344,14 @@ fn write_frame_with_deadline( frame: &[u8], deadline: Option, ) -> IoResult<()> { - if frame.is_empty() || frame.len() > MAX_FRAME_LEN { - return Err(invalid_data("invalid broker frame length")); - } - let len = u32::try_from(frame.len()).map_err(|_| invalid_data("broker frame too large"))?; - write_all_with_deadline(stream, &len.to_le_bytes(), deadline)?; - write_all_with_deadline(stream, frame, deadline) + with_write_deadline(stream, deadline, |stream, deadline| { + if frame.is_empty() || frame.len() > MAX_FRAME_LEN { + return Err(invalid_data("invalid broker frame length")); + } + let len = u32::try_from(frame.len()).map_err(|_| invalid_data("broker frame too large"))?; + write_all_with_deadline(stream, &len.to_le_bytes(), deadline)?; + write_all_with_deadline(stream, frame, deadline) + }) } fn write_all_with_deadline( @@ -313,7 +360,7 @@ fn write_all_with_deadline( deadline: Option, ) -> IoResult<()> { while !buffer.is_empty() { - refresh_stream_io_deadline(stream, deadline)?; + refresh_write_deadline(stream, deadline)?; match stream.write(buffer) { Ok(0) => { return Err(Error::new( @@ -329,23 +376,6 @@ fn write_all_with_deadline( Ok(()) } -fn refresh_stream_io_deadline(stream: &UnixStream, deadline: Option) -> IoResult<()> { - if let Some(deadline) = deadline { - let timeout = io_timeout_for_deadline(deadline)?; - stream.set_read_timeout(Some(timeout))?; - stream.set_write_timeout(Some(timeout))?; - } - Ok(()) -} - -fn io_timeout_for_deadline(deadline: Instant) -> IoResult { - let timeout = deadline - .checked_duration_since(Instant::now()) - .filter(|timeout| !timeout.is_zero()) - .ok_or_else(|| Error::new(ErrorKind::TimedOut, "broker I/O deadline expired"))?; - Ok(timeout) -} - fn invalid_data(message: &'static str) -> Error { Error::new(ErrorKind::InvalidData, message) } @@ -360,6 +390,22 @@ fn wire_error(error: WireError) -> Error { #[cfg(test)] mod tests { use super::*; + use std::time::Duration; + + #[test] + fn linux_peer_validation_identifies_connected_process() { + let (first, second) = UnixStream::pair().unwrap(); + + validate_peer_process(&first, std::process::id()).unwrap(); + validate_same_peer_process(&first, &second).unwrap(); + let unexpected_process_id = std::process::id().checked_add(1).unwrap(); + assert_eq!( + validate_peer_process(&first, unexpected_process_id) + .unwrap_err() + .kind(), + ErrorKind::PermissionDenied + ); + } #[test] fn frame_round_trip() { @@ -454,6 +500,69 @@ mod tests { ); } + #[test] + fn host_handshake_request_read_setup_deadline_is_wall_clock() { + let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamHostControlChannel::from_host_guaranteed( + host_stream, + Instant::now() + Duration::from_millis(50), + ); + + let reader = std::thread::spawn(move || channel.recv_handshake_request().unwrap_err()); + local_stream.write_all(&8u32.to_le_bytes()).unwrap(); + for _ in 0..8 { + std::thread::sleep(Duration::from_millis(20)); + if local_stream.write_all(&[0]).is_err() { + break; + } + } + + let error = reader.join().expect("timeout reader panicked"); + assert!( + matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut), + "unexpected timeout error kind: {error:?}" + ); + } + + #[test] + fn negotiated_host_handshake_restores_active_timeouts() { + let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); + let active_read_timeout = Some(Duration::from_secs(2)); + let active_write_timeout = Some(Duration::from_secs(3)); + host_stream.set_read_timeout(active_read_timeout).unwrap(); + host_stream.set_write_timeout(active_write_timeout).unwrap(); + let mut channel = UnixStreamHostControlChannel::from_host_guaranteed( + host_stream, + Instant::now() + Duration::from_secs(1), + ); + let request = BrokerHandshakeRequest { + protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }; + + write_frame_with_deadline( + &mut local_stream, + &encode_handshake_request(request.clone()), + None, + ) + .unwrap(); + assert_eq!( + channel.recv_handshake_request().unwrap(), + HostReceive::Message(request) + ); + channel + .send_handshake_response(&BrokerHandshakeResponse::Negotiated { + broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }) + .unwrap(); + + assert_eq!(channel.setup_deadline, None); + assert_eq!(channel.stream.read_timeout().unwrap(), active_read_timeout); + assert_eq!( + channel.stream.write_timeout().unwrap(), + active_write_timeout + ); + } + #[test] fn local_control_cancellation_unblocks_response_read() { let (local_stream, _host_stream) = UnixStream::pair().unwrap(); diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index 6cceb029cf..def0db4077 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -8,7 +8,7 @@ clap = { version = "4.5.33", features = ["derive"] } litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0", features = ["linux-shared-memory", "unix"] } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0", features = ["linux-userland"] } tempfile = { version = "3", default-features = false } [[bin]] @@ -21,7 +21,6 @@ path = "tests/userland_broker.rs" harness = false [dev-dependencies] -libc = { version = "0.2.169", default-features = false } litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0" } [lints] diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index ad9aa1c1c3..5d13e25638 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -1,23 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use std::cell::Cell; use std::error::Error; use std::ffi::OsString; -use std::os::unix::net::UnixListener; +use std::io::{Error as IoError, ErrorKind, Result as IoResult}; +use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; -use std::process::Command; +use std::process::{Child, Command}; use std::time::{Duration, Instant}; use clap::Parser; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; -use litebox_broker_host::serve_connection; +use litebox_broker_host::{ConnectionTermination, serve_connection}; use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ - UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, + UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, validate_peer_process, }; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); +const ACCEPT_RETRY_DELAY: Duration = Duration::from_millis(10); #[derive(Parser, Debug)] struct CliArgs { @@ -38,7 +41,9 @@ fn main() -> Result<(), Box> { let notification_socket_path = socket_dir.path().join("broker-notification.sock"); let control_listener = UnixListener::bind(&control_socket_path)?; let notification_listener = UnixListener::bind(¬ification_socket_path)?; - let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( + control_listener.set_nonblocking(true)?; + notification_listener.set_nonblocking(true)?; + let broker = BrokerCore::new(PolicyEngine::with_host_guaranteed_rights( ObjectRights::all(), ))?; @@ -51,37 +56,112 @@ fn main() -> Result<(), Box> { .arg(¬ification_socket_path) .args(&args.runner_arguments); let mut runner = runner_command.spawn()?; - let _runner_waiter = std::thread::spawn(move || { - if let Err(error) = runner.wait() { - eprintln!("failed to wait for local runner: {error}"); - } - }); + let runner_process_id = runner.id(); + + let association_result = serve_runner( + &broker, + &control_listener, + ¬ification_listener, + &mut runner, + runner_process_id, + ); + if association_result.is_err() { + let _ = runner.kill(); + } + let runner_status = runner.wait()?; + association_result?; + if !runner_status.success() { + return Err(IoError::other(format!("runner exited with {runner_status}")).into()); + } + Ok(()) +} + +fn serve_runner( + broker: &BrokerCore, + control_listener: &UnixListener, + notification_listener: &UnixListener, + runner: &mut Child, + runner_process_id: u32, +) -> Result<(), Box> { + let setup_deadline = Instant::now() + SETUP_TIMEOUT; + let control_stream = accept_runner_stream( + control_listener, + runner, + runner_process_id, + setup_deadline, + "control", + )?; + let notification_stream = accept_runner_stream( + notification_listener, + runner, + runner_process_id, + setup_deadline, + "notification", + )?; + let shared_memory = MemfdSharedMemory::create(PIPE_TRANSFER_BUFFER_SIZE)?; + let mut control_channel = + UnixStreamHostControlChannel::from_host_guaranteed(control_stream, setup_deadline); + let mut notification_channel = + UnixStreamHostNotificationChannel::from_accepted(notification_stream); + let setup_completed = Cell::new(false); + let termination = serve_connection( + broker, + &mut control_channel, + &mut notification_channel, + &shared_memory, + |channel| { + channel.send_memfd(&shared_memory, Some(setup_deadline))?; + setup_completed.set(true); + Ok(()) + }, + )?; + if termination != ConnectionTermination::PeerClosed { + return Err(IoError::new( + ErrorKind::InvalidData, + "runner violated the broker protocol", + ) + .into()); + } + if !setup_completed.get() { + return Err(IoError::new( + ErrorKind::UnexpectedEof, + "runner closed before completing broker setup", + ) + .into()); + } + Ok(()) +} +fn accept_runner_stream( + listener: &UnixListener, + runner: &mut Child, + runner_process_id: u32, + deadline: Instant, + channel_name: &'static str, +) -> IoResult { loop { - let (control_stream, _) = control_listener.accept()?; - let setup_deadline = Instant::now() + SETUP_TIMEOUT; - let shared_memory = MemfdSharedMemory::create(PIPE_TRANSFER_BUFFER_SIZE)?; - let (notification_stream, _) = notification_listener.accept()?; - let broker = broker.clone(); - if let Err(error) = std::thread::Builder::new() - .name("litebox-broker-connection".to_owned()) - .spawn(move || { - let mut control_channel = - UnixStreamHostControlChannel::from_accepted(control_stream); - let mut notification_channel = - UnixStreamHostNotificationChannel::from_accepted(notification_stream); - if let Err(error) = serve_connection( - &broker, - &mut control_channel, - &mut notification_channel, - &shared_memory, - |channel| channel.send_memfd(&shared_memory, Some(setup_deadline)), - ) { - eprintln!("failed to serve broker connection: {error}"); - } - }) - { - eprintln!("failed to spawn broker connection handler: {error}"); + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(IoError::new( + ErrorKind::TimedOut, + format!("timed out waiting for runner {channel_name} channel"), + )); + } + if let Some(status) = runner.try_wait()? { + return Err(IoError::new( + ErrorKind::BrokenPipe, + format!("runner exited with {status} before connecting its {channel_name} channel"), + )); + } + + match listener.accept() { + Ok((stream, _)) => { + validate_peer_process(&stream, runner_process_id)?; + return Ok(stream); + } + Err(error) if error.kind() == ErrorKind::WouldBlock => {} + Err(error) => return Err(error), } + std::thread::sleep(remaining.min(ACCEPT_RETRY_DELAY)); } } diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 1ec8a13799..f869816a53 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -2,8 +2,7 @@ // Licensed under the MIT license. use std::ffi::{OsStr, OsString}; -use std::io::{Error, ErrorKind, Result}; -use std::os::unix::process::ExitStatusExt; +use std::io::{ErrorKind, Result}; use std::path::Path; use std::process::{Child, Command}; use std::sync::Arc; @@ -51,7 +50,7 @@ fn run_parent_test() { let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { if let Some(status) = broker.child.try_wait().unwrap() { - assert_eq!(status.signal(), Some(libc::SIGTERM)); + assert!(status.success(), "broker failed with {status}"); return; } std::thread::sleep(Duration::from_millis(10)); @@ -119,17 +118,6 @@ fn run_fake_runner(args: &[OsString]) { data ); drop(local); - - // SAFETY: `getppid` takes no pointer arguments and has no Rust-side aliasing requirements. - let broker_pid = unsafe { libc::getppid() }; - // SAFETY: `broker_pid` is the runner's parent process and `SIGTERM` is a valid signal number. - let kill_result = unsafe { libc::kill(broker_pid, libc::SIGTERM) }; - assert_eq!( - kill_result, - 0, - "failed to stop broker: {}", - Error::last_os_error() - ); } struct ChildGuard { diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 27d2bdb3c2..e88c47617b 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -10,7 +10,7 @@ libc = { version = "0.2.169", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } -litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport", features = ["linux-shared-memory", "unix"] } +litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport", features = ["linux-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_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_linux_userland"] } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 0fc0eee0a7..80d754de83 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -159,6 +159,23 @@ pub fn run(cli_args: CliArgs) -> Result<()> { ); } + let broker_connection = match ( + cli_args.broker_control_socket.as_deref(), + cli_args.broker_notification_socket.as_deref(), + ) { + (Some(control_socket_path), Some(notification_socket_path)) => Some(broker::connect( + control_socket_path, + notification_socket_path, + )?), + (None, None) => None, + (Some(_), None) => { + anyhow::bail!("broker notification socket is required with broker control socket") + } + (None, Some(_)) => { + anyhow::bail!("broker control socket is required with broker notification socket") + } + }; + let mut cow_eligible_regions: Vec = Vec::new(); // When --program-from-tar is set, the program binary is already in the tar file, @@ -223,22 +240,6 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } litebox_platform_multiplex::set_platform(platform); - let broker_connection = match ( - cli_args.broker_control_socket.as_deref(), - cli_args.broker_notification_socket.as_deref(), - ) { - (Some(control_socket_path), Some(notification_socket_path)) => Some(broker::connect( - control_socket_path, - notification_socket_path, - )?), - (None, None) => None, - (Some(_), None) => { - anyhow::bail!("broker notification socket is required with broker control socket") - } - (None, Some(_)) => { - anyhow::bail!("broker control socket is required with broker notification socket") - } - }; let shim_builder = if let Some(broker_connection) = broker_connection { let (broker_local, broker_notifications, broker_control_cancellation) = broker_connection; diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 54abc5d4ca..2c5cd0574f 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -348,6 +348,11 @@ fn spawn_test_broker( let (notification_stream, _) = notification_listener .accept() .expect("failed to accept broker local notification connection"); + litebox_broker_transport::unix_socket::validate_same_peer_process( + &control_stream, + ¬ification_stream, + ) + .expect("broker channels must belong to the same runner process"); control_stream .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test read timeout"); @@ -361,7 +366,10 @@ fn spawn_test_broker( .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker notification test write timeout"); let mut channel = CountingHostControlChannel { - inner: litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(control_stream), + inner: litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_host_guaranteed( + control_stream, + std::time::Instant::now() + BROKER_HELPER_TIMEOUT, + ), close_object_count: 0, }; let mut notification_channel = @@ -491,7 +499,7 @@ console.log(content); let broker_thread = spawn_test_broker( &control_socket_path, ¬ification_socket_path, - litebox_broker_core::PolicyEngine::with_unauthenticated_rights( + litebox_broker_core::PolicyEngine::with_host_guaranteed_rights( litebox_broker_core::ObjectRights::all(), ), 4, From d9e06ccd75de344472ddf994295f7c5cf5d8f698 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 21 Jul 2026 19:35:21 -0700 Subject: [PATCH 112/319] Tag broker control requests (#1060) Adds association-scoped `RequestId`s to active broker requests and responses while leaving handshake and notification messages unchanged. The local endpoint allocates non-wrapping IDs and rejects mismatched responses, while the serial host loop echoes each request ID. Uncorrelated protocol violations terminate the association instead of fabricating a response. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 11 +- litebox/src/broker/mod.rs | 14 +- litebox/src/event/counter.rs | 31 +- litebox/src/pipes.rs | 37 +- litebox_broker_host/src/lib.rs | 191 ++++++----- litebox_broker_local/src/error.rs | 10 + litebox_broker_local/src/event.rs | 14 +- litebox_broker_local/src/lib.rs | 160 +++++++-- litebox_broker_local/src/pipe.rs | 68 ++-- litebox_broker_protocol/src/lib.rs | 5 + litebox_broker_protocol/src/message.rs | 28 +- litebox_broker_protocol/src/wire.rs | 320 ++++++++++++------ litebox_broker_protocol/src/wire/primitive.rs | 10 +- litebox_broker_transport/src/unix_socket.rs | 13 +- litebox_runner_linux_userland/tests/run.rs | 5 +- 15 files changed, 611 insertions(+), 306 deletions(-) diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index d5280f075e..13ed357ebc 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -10,8 +10,8 @@ use crate::event::{counter::EventCounterError, polling::TryOpError}; /// Error returned by the deployment-provided broker control path. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] pub(crate) enum BrokerControlError { - #[error("broker control transport failed")] - Transport, + #[error("broker control association failed")] + AssociationFailed, #[error("broker returned operation error: {0}")] Broker(#[source] ErrorCode), } @@ -41,7 +41,7 @@ pub(crate) enum BrokerObjectError { impl From for BrokerObjectError { fn from(error: BrokerControlError) -> Self { match error { - BrokerControlError::Transport => Self::Control, + BrokerControlError::AssociationFailed => Self::Control, BrokerControlError::Broker(error) => error.into(), } } @@ -69,7 +69,10 @@ impl From for BrokerObjectError { impl From> for BrokerControlError { fn from(error: BrokerLocalError) -> Self { match error { - BrokerLocalError::Channel(_) | BrokerLocalError::ChannelClosed => Self::Transport, + BrokerLocalError::Channel(_) + | BrokerLocalError::ChannelClosed + | BrokerLocalError::RequestIdExhausted + | BrokerLocalError::UnexpectedResponseId { .. } => Self::AssociationFailed, BrokerLocalError::Broker(error) => Self::Broker(error), } } diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 7999cc2517..96a603fbf8 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -173,15 +173,15 @@ where let (result, failed_connection) = { let mut local = self.local.lock(); let Some(connection) = local.as_mut() else { - return Err(BrokerControlError::Transport); + return Err(BrokerControlError::AssociationFailed); }; let result = request(connection).map_err(BrokerControlError::from); - let failed_connection = if matches!(result.as_ref(), Err(BrokerControlError::Transport)) - { - local.take() - } else { - None - }; + let failed_connection = + if matches!(result.as_ref(), Err(BrokerControlError::AssociationFailed)) { + local.take() + } else { + None + }; (result, failed_connection) }; if let Some(connection) = failed_connection { diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index c81e478d69..de9f4760d9 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -182,8 +182,9 @@ mod tests { use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{CreateEventResponse, EventConsumption}; use litebox_broker_protocol::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, - BrokerResponse, EventRequest, EventResponse, ReadinessNotification, + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, + BrokerRequest, BrokerResponse, BrokerResult, EventRequest, EventResponse, + ReadinessNotification, }; use litebox_broker_protocol::readiness::ReadinessFlags; @@ -471,32 +472,36 @@ mod tests { self.last_request.take(); return Err(()); } - let response = match self.last_request.take().unwrap() { - BrokerRequest::Event(EventRequest::Create(_)) => { + let request = self.last_request.take().unwrap(); + let result = match request.operation { + BrokerOperation::Event(EventRequest::Create(_)) => { let handle = ObjectHandle(self.next_handle); self.next_handle += 1; - BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })) + BrokerResult::Event(EventResponse::Create(CreateEventResponse { handle })) } - BrokerRequest::Event(EventRequest::Consume(_)) => { + BrokerOperation::Event(EventRequest::Consume(_)) => { self.consume_attempts.fetch_add(1, Ordering::SeqCst); if self.read_ready.swap(false, Ordering::SeqCst) { - BrokerResponse::Event(EventResponse::Consume(EventConsumption { + BrokerResult::Event(EventResponse::Consume(EventConsumption { value: 1, readiness: ReadinessFlags::WRITE, })) } else { - BrokerResponse::Error(ErrorCode::WouldBlock) + BrokerResult::Error(ErrorCode::WouldBlock) } } - BrokerRequest::CloseObject(_) => BrokerResponse::ObjectClosed, - BrokerRequest::CheckReadiness(_) => { - BrokerResponse::Readiness(ReadinessFlags::WRITE) + BrokerOperation::CloseObject(_) => BrokerResult::ObjectClosed, + BrokerOperation::CheckReadiness(_) => { + BrokerResult::Readiness(ReadinessFlags::WRITE) } - request @ (BrokerRequest::Event(_) | BrokerRequest::Pipe(_)) => { + request @ (BrokerOperation::Event(_) | BrokerOperation::Pipe(_)) => { panic!("unexpected broker request: {request:?}") } }; - Ok(Some(response)) + Ok(Some(BrokerResponse { + request_id: request.request_id, + result, + })) } } } diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index 71bd086c14..35972ea931 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -931,8 +931,8 @@ mod tests { use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, - BrokerResponse, PipeRequest, ReadinessNotification, + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, + BrokerRequest, BrokerResponse, BrokerResult, PipeRequest, ReadinessNotification, }; use litebox_broker_protocol::pipe::CreatePipeResponse; use litebox_broker_protocol::readiness::ReadinessFlags; @@ -1179,32 +1179,35 @@ mod tests { } fn recv_response(&mut self) -> core::result::Result, Self::Error> { - match self.last_request.take().unwrap() { - BrokerRequest::Pipe(PipeRequest::Create(_)) => Ok(Some(BrokerResponse::Pipe( + let request = self.last_request.take().unwrap(); + let result = match request.operation { + BrokerOperation::Pipe(PipeRequest::Create(_)) => BrokerResult::Pipe( litebox_broker_protocol::message::PipeResponse::Create(CreatePipeResponse { read_handle: ObjectHandle(1), write_handle: ObjectHandle(2), }), - ))), - BrokerRequest::Pipe(PipeRequest::Read(_)) + ), + BrokerOperation::Pipe(PipeRequest::Read(_)) if self.force_transport.load(Ordering::SeqCst) => { - Err(()) + return Err(()); } - BrokerRequest::Pipe(PipeRequest::Read(_)) => match self.read_failure { - ReadFailure::Transport => Err(()), - ReadFailure::WouldBlock => { - Ok(Some(BrokerResponse::Error(ErrorCode::WouldBlock))) - } + BrokerOperation::Pipe(PipeRequest::Read(_)) => match self.read_failure { + ReadFailure::Transport => return Err(()), + ReadFailure::WouldBlock => BrokerResult::Error(ErrorCode::WouldBlock), }, - BrokerRequest::CloseObject(_) => Ok(Some(BrokerResponse::ObjectClosed)), - BrokerRequest::CheckReadiness(_) => { - Ok(Some(BrokerResponse::Readiness(ReadinessFlags::default()))) + BrokerOperation::CloseObject(_) => BrokerResult::ObjectClosed, + BrokerOperation::CheckReadiness(_) => { + BrokerResult::Readiness(ReadinessFlags::default()) } - request @ (BrokerRequest::Pipe(_) | BrokerRequest::Event(_)) => { + request @ (BrokerOperation::Pipe(_) | BrokerOperation::Event(_)) => { panic!("unexpected broker request: {request:?}") } - } + }; + Ok(Some(BrokerResponse { + request_id: request.request_id, + result, + })) } } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 01f1e1ffc7..cad4d1a24a 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -24,8 +24,8 @@ use litebox_broker_protocol::channel::{ use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse}; use litebox_broker_protocol::message::{ - BrokerHandshakeResponse, BrokerRequest, BrokerResponse, EventRequest, EventResponse, - PipeRequest, PipeResponse, + BrokerHandshakeResponse, BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, + EventRequest, EventResponse, PipeRequest, PipeResponse, }; use litebox_broker_protocol::pipe::{ CreatePipeResponse, PIPE_TRANSFER_BUFFER_SIZE, ReadPipeResponse, WritePipeResponse, @@ -114,18 +114,19 @@ where { HostReceive::Message(request) => request, HostReceive::ProtocolViolation => { - control_channel - .send_response(&BrokerResponse::Error(ErrorCode::ProtocolState)) - .map_err(BrokerHostError::Channel)?; return Ok(ConnectionTermination::ProtocolViolation); } HostReceive::PeerClosed => break, }; - let response = complete_request(handle_request(&session, request, shared_memory)) + let BrokerRequest { + request_id, + operation, + } = request; + let result = complete_request(handle_request(&session, operation, shared_memory)) .map_err(BrokerHostError::Broker)?; control_channel - .send_response(&response) + .send_response(&BrokerResponse { request_id, result }) .map_err(BrokerHostError::Channel)?; } @@ -143,34 +144,34 @@ enum RequestFailure { } fn complete_request( - result: RequestResult, -) -> core::result::Result { + result: RequestResult, +) -> core::result::Result { match result { Ok(response) => Ok(response), - Err(RequestFailure::Respond(error)) => Ok(BrokerResponse::Error(error)), + Err(RequestFailure::Respond(error)) => Ok(BrokerResult::Error(error)), Err(RequestFailure::Abort(error)) => Err(error), } } fn handle_request( session: &BrokerSession, - request: BrokerRequest, + operation: BrokerOperation, shared_memory: &dyn SharedMemory, -) -> RequestResult { - match request { - BrokerRequest::CloseObject(handle) => session +) -> RequestResult { + match operation { + BrokerOperation::CloseObject(handle) => session .close_object_reference(handle) - .map(|()| BrokerResponse::ObjectClosed) + .map(|()| BrokerResult::ObjectClosed) .map_err(|error| RequestFailure::Respond(error.into())), - BrokerRequest::CheckReadiness(handle) => session + BrokerOperation::CheckReadiness(handle) => session .check_readiness(handle) - .map(BrokerResponse::Readiness) + .map(BrokerResult::Readiness) .map_err(|error| RequestFailure::Respond(error.into())), - BrokerRequest::Event(request) => { - handle_event_request(session, request).map(BrokerResponse::Event) + BrokerOperation::Event(request) => { + handle_event_request(session, request).map(BrokerResult::Event) } - BrokerRequest::Pipe(request) => { - handle_pipe_request(session, request, shared_memory).map(BrokerResponse::Pipe) + BrokerOperation::Pipe(request) => { + handle_pipe_request(session, request, shared_memory).map(BrokerResult::Pipe) } } } @@ -262,7 +263,7 @@ fn handle_event_request( pub enum ConnectionTermination { /// The peer cleanly closed the channel. PeerClosed, - /// The broker sent a protocol-state error before closing the channel. + /// The peer violated the protocol. ProtocolViolation, } @@ -278,7 +279,7 @@ mod tests { use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerNotification}; use litebox_broker_protocol::pipe::{CreatePipeRequest, ReadPipeRequest, WritePipeRequest}; use litebox_broker_protocol::shared_memory::SharedMemoryError; - use litebox_broker_protocol::{ObjectHandle, ProtocolVersion}; + use litebox_broker_protocol::{ObjectHandle, ProtocolVersion, RequestId}; use std::sync::{Arc, Mutex}; #[test] @@ -307,12 +308,13 @@ mod tests { protocol_version: BROKER_PROTOCOL_VERSION, }))]), std::vec::Vec::from([ - Ok(HostReceive::Message(BrokerRequest::Event( + Ok(HostReceive::Message(BrokerOperation::Event( EventRequest::Create(CreateEventRequest { initial_count: 0 }), ))), Ok(HostReceive::PeerClosed), ]), ); + channel.next_request_id = 41; let mut notifications = FakeHostNotificationChannel::default(); assert_eq!( @@ -332,11 +334,12 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION } ); - let handle = match &channel.responses[0] { - BrokerResponse::Event(EventResponse::Create(response)) => response.handle, + let handle = match &channel.results[0] { + BrokerResult::Event(EventResponse::Create(response)) => response.handle, response => panic!("unexpected response: {response:?}"), }; assert_ne!(handle.0, 0); + assert_eq!(channel.response_ids, [RequestId(41)]); } fn serve_connection_retries_after_version_mismatch(broker: &BrokerCore) { @@ -435,7 +438,7 @@ mod tests { channel.handshake_responses, [BrokerHandshakeResponse::Error(ErrorCode::ProtocolState)] ); - assert!(channel.responses.is_empty()); + assert!(channel.results.is_empty()); } fn serve_connection_rejects_handshake_request_after_negotiation(broker: &BrokerCore) { @@ -464,10 +467,7 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION }] ); - assert_eq!( - channel.responses, - [BrokerResponse::Error(ErrorCode::ProtocolState)] - ); + assert!(channel.results.is_empty()); } fn serve_connection_returns_channel_error_when_response_send_fails(broker: &BrokerCore) { @@ -498,7 +498,7 @@ mod tests { std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, }))]), - std::vec::Vec::from([Ok(HostReceive::Message(BrokerRequest::Event( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerOperation::Event( EventRequest::Create(CreateEventRequest { initial_count: 0 }), )))]), ); @@ -518,13 +518,13 @@ mod tests { ); assert!(notifications.notifications.is_empty()); assert_eq!( - &channel.responses[1..], + &channel.results[1..], [ - BrokerResponse::Event(EventResponse::Add(AddEventResponse { + BrokerResult::Event(EventResponse::Add(AddEventResponse { readiness: litebox_broker_protocol::readiness::ReadinessFlags::READ | litebox_broker_protocol::readiness::ReadinessFlags::WRITE, })), - BrokerResponse::Event(EventResponse::Consume( + BrokerResult::Event(EventResponse::Consume( litebox_broker_protocol::event::ConsumeEventResponse { value: 1, readiness: litebox_broker_protocol::readiness::ReadinessFlags::WRITE, @@ -540,13 +540,13 @@ mod tests { protocol_version: BROKER_PROTOCOL_VERSION, }))]), std::vec::Vec::from([ - Ok(HostReceive::Message(BrokerRequest::Pipe( + Ok(HostReceive::Message(BrokerOperation::Pipe( PipeRequest::Read(ReadPipeRequest { handle: ObjectHandle(u64::MAX), length: 1, }), ))), - Ok(HostReceive::Message(BrokerRequest::Event( + Ok(HostReceive::Message(BrokerOperation::Event( EventRequest::Create(CreateEventRequest { initial_count: 0 }), ))), Ok(HostReceive::PeerClosed), @@ -566,13 +566,14 @@ mod tests { ConnectionTermination::PeerClosed ); assert_eq!( - channel.responses[0], - BrokerResponse::Error(ErrorCode::UnknownObject) + channel.results[0], + BrokerResult::Error(ErrorCode::UnknownObject) ); assert!(matches!( - channel.responses[1], - BrokerResponse::Event(EventResponse::Create(_)) + channel.results[1], + BrokerResult::Event(EventResponse::Create(_)) )); + assert_eq!(channel.response_ids, [RequestId(0), RequestId(1)]); } fn serve_connection_aborts_without_response_on_shared_memory_failure(broker: &BrokerCore) { @@ -580,7 +581,7 @@ mod tests { std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, }))]), - std::vec::Vec::from([Ok(HostReceive::Message(BrokerRequest::Pipe( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerOperation::Pipe( PipeRequest::Create(CreatePipeRequest { capacity: 64, atomic_write_size: 16, @@ -600,10 +601,10 @@ mod tests { ), Err(BrokerHostError::Broker(ErrorCode::Internal)) )); - assert_eq!(channel.responses.len(), 1); + assert_eq!(channel.results.len(), 1); assert!(matches!( - channel.responses[0], - BrokerResponse::Pipe(PipeResponse::Create(_)) + channel.results[0], + BrokerResult::Pipe(PipeResponse::Create(_)) )); } @@ -613,29 +614,29 @@ mod tests { .unwrap(); let response = handle_test_request( &session, - BrokerRequest::Event(EventRequest::Create(CreateEventRequest { + BrokerOperation::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, })), ); - let BrokerResponse::Event(EventResponse::Create(response)) = response else { + let BrokerResult::Event(EventResponse::Create(response)) = response else { panic!("unexpected create response: {response:?}"); }; let handle = response.handle; assert_eq!( - handle_test_request(&session, BrokerRequest::CloseObject(handle)), - BrokerResponse::ObjectClosed + handle_test_request(&session, BrokerOperation::CloseObject(handle)), + BrokerResult::ObjectClosed ); assert_eq!( - handle_test_request(&session, BrokerRequest::CheckReadiness(handle)), - BrokerResponse::Error(ErrorCode::UnknownObject) + handle_test_request(&session, BrokerOperation::CheckReadiness(handle)), + BrokerResult::Error(ErrorCode::UnknownObject) ); assert_eq!( handle_test_request( &session, - BrokerRequest::CloseObject(ObjectHandle(handle.0 + 1)) + BrokerOperation::CloseObject(ObjectHandle(handle.0 + 1)) ), - BrokerResponse::Error(ErrorCode::UnknownObject) + BrokerResult::Error(ErrorCode::UnknownObject) ); } @@ -646,20 +647,20 @@ mod tests { let memory = TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE); let created = handle_test_request_with_memory( &session, - BrokerRequest::Pipe(PipeRequest::Create(CreatePipeRequest { + BrokerOperation::Pipe(PipeRequest::Create(CreatePipeRequest { capacity: 64, atomic_write_size: 16, })), &memory, ); - let BrokerResponse::Pipe(PipeResponse::Create(response)) = created else { + let BrokerResult::Pipe(PipeResponse::Create(response)) = created else { panic!("expected successful pipe creation"); }; memory.write(0, &[1, 2, 3]).unwrap(); let write = handle_test_request_with_memory( &session, - BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { + BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle: response.write_handle, length: 3, })), @@ -667,12 +668,12 @@ mod tests { ); assert_eq!( write, - BrokerResponse::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })) + BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })) ); let read = handle_test_request_with_memory( &session, - BrokerRequest::Pipe(PipeRequest::Read(ReadPipeRequest { + BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { handle: response.read_handle, length: 3, })), @@ -680,7 +681,7 @@ mod tests { ); assert_eq!( read, - BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { read: 3 })) + BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 3 })) ); let mut data = [0; 3]; memory.read(0, &mut data).unwrap(); @@ -688,7 +689,7 @@ mod tests { let invalid_range = handle_test_request_with_memory( &session, - BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { + BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle: response.write_handle, length: u32::try_from(PIPE_TRANSFER_BUFFER_SIZE).unwrap() + 1, })), @@ -696,32 +697,34 @@ mod tests { ); assert_eq!( invalid_range, - BrokerResponse::Error(ErrorCode::MalformedRequest) + BrokerResult::Error(ErrorCode::MalformedRequest) ); } - fn handle_test_request(session: &BrokerSession, request: BrokerRequest) -> BrokerResponse { + fn handle_test_request(session: &BrokerSession, operation: BrokerOperation) -> BrokerResult { handle_test_request_with_memory( session, - request, + operation, &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), ) } fn handle_test_request_with_memory( session: &BrokerSession, - request: BrokerRequest, + operation: BrokerOperation, shared_memory: &dyn SharedMemory, - ) -> BrokerResponse { - complete_request(handle_request(session, request, shared_memory)).unwrap() + ) -> BrokerResult { + complete_request(handle_request(session, operation, shared_memory)).unwrap() } struct FakeHostControlChannel { handshake_requests: std::vec::Vec, ()>>, - requests: std::vec::Vec, ()>>, + operations: std::vec::Vec, ()>>, handshake_responses: std::vec::Vec, - responses: std::vec::Vec, + results: std::vec::Vec, + response_ids: std::vec::Vec, + next_request_id: u64, enqueue_readiness_requests_after_create: bool, enqueue_write_request_after_pipe_create: bool, send_error: bool, @@ -732,13 +735,15 @@ mod tests { handshake_requests: std::vec::Vec< core::result::Result, ()>, >, - requests: std::vec::Vec, ()>>, + operations: std::vec::Vec, ()>>, ) -> Self { Self { handshake_requests, - requests, + operations, handshake_responses: std::vec::Vec::new(), - responses: std::vec::Vec::new(), + results: std::vec::Vec::new(), + response_ids: std::vec::Vec::new(), + next_request_id: 0, enqueue_readiness_requests_after_create: false, enqueue_write_request_after_pipe_create: false, send_error: false, @@ -777,11 +782,23 @@ mod tests { fn recv_request( &mut self, ) -> core::result::Result, Self::Error> { - if self.requests.is_empty() { - Ok(HostReceive::PeerClosed) + let received = if self.operations.is_empty() { + HostReceive::PeerClosed } else { - self.requests.remove(0) - } + self.operations.remove(0)? + }; + Ok(match received { + HostReceive::Message(request) => { + let request_id = RequestId(self.next_request_id); + self.next_request_id += 1; + HostReceive::Message(BrokerRequest { + request_id, + operation: request, + }) + } + HostReceive::ProtocolViolation => HostReceive::ProtocolViolation, + HostReceive::PeerClosed => HostReceive::PeerClosed, + }) } fn send_response( @@ -791,38 +808,40 @@ mod tests { if self.send_error { return Err(()); } + let result = &response.result; if self.enqueue_readiness_requests_after_create - && let BrokerResponse::Event(EventResponse::Create(response)) = response + && let BrokerResult::Event(EventResponse::Create(response)) = result { - self.requests - .push(Ok(HostReceive::Message(BrokerRequest::Event( + self.operations + .push(Ok(HostReceive::Message(BrokerOperation::Event( EventRequest::Add(AddEventRequest { handle: response.handle, value: 1, }), )))); - self.requests - .push(Ok(HostReceive::Message(BrokerRequest::Event( + self.operations + .push(Ok(HostReceive::Message(BrokerOperation::Event( EventRequest::Consume(ConsumeEventRequest { handle: response.handle, mode: EventConsumeMode::One, }), )))); - self.requests.push(Ok(HostReceive::PeerClosed)); + self.operations.push(Ok(HostReceive::PeerClosed)); } if self.enqueue_write_request_after_pipe_create - && let BrokerResponse::Pipe(PipeResponse::Create(response)) = response + && let BrokerResult::Pipe(PipeResponse::Create(response)) = result { - self.requests - .push(Ok(HostReceive::Message(BrokerRequest::Pipe( + self.operations + .push(Ok(HostReceive::Message(BrokerOperation::Pipe( PipeRequest::Write(WritePipeRequest { handle: response.write_handle, length: 1, }), )))); - self.requests.push(Ok(HostReceive::PeerClosed)); + self.operations.push(Ok(HostReceive::PeerClosed)); } - self.responses.push(response.clone()); + self.results.push(result.clone()); + self.response_ids.push(response.request_id); Ok(()) } } diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 812fc38aab..b75661a478 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use litebox_broker_protocol::RequestId; use litebox_broker_protocol::error::ErrorCode; use thiserror::Error; @@ -11,6 +12,15 @@ pub enum BrokerLocalError { Channel(#[source] E), #[error("broker closed the channel")] ChannelClosed, + #[error("broker request identifiers are exhausted")] + RequestIdExhausted, + #[error("broker returned response ID {actual:?} for request {expected:?}")] + UnexpectedResponseId { + /// Request identifier sent by the local endpoint. + expected: RequestId, + /// Request identifier returned by the broker. + actual: RequestId, + }, #[error("broker rejected request: {0}")] Broker(#[source] ErrorCode), } diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 0985ff6b0b..9819ee19c5 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -8,7 +8,7 @@ use litebox_broker_protocol::event::{ EventConsumeMode, }; use litebox_broker_protocol::message::{ - BrokerRequest, BrokerResponse, EventRequest, EventResponse, + BrokerOperation, BrokerResult, EventRequest, EventResponse, }; use litebox_broker_protocol::readiness::ReadinessFlags; @@ -71,12 +71,12 @@ impl BrokerLocal { } fn request_event(&mut self, request: EventRequest) -> Result { - match self.request(BrokerRequest::Event(request))? { - BrokerResponse::Event(response) => Ok(response), - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response @ (BrokerResponse::ObjectClosed - | BrokerResponse::Readiness(_) - | BrokerResponse::Pipe(_)) => { + match self.request(BrokerOperation::Event(request))? { + BrokerResult::Event(response) => Ok(response), + BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), + response @ (BrokerResult::ObjectClosed + | BrokerResult::Readiness(_) + | BrokerResult::Pipe(_)) => { panic!("broker returned unexpected event response: {response:?}"); } } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index fc2552c447..b2a22d77ea 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -25,13 +25,13 @@ use alloc::sync::Arc; use litebox_broker_protocol::channel::{LocalControlChannel, LocalNotificationChannel}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, - BrokerResponse, + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, + BrokerRequest, BrokerResponse, BrokerResult, }; use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::shared_memory::SharedMemory; -use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; +use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle, RequestId}; pub use error::{BrokerLocalError, Result}; @@ -42,6 +42,7 @@ pub use error::{BrokerLocalError, Result}; pub struct BrokerLocal { channel: Channel, shared_memory: Arc, + next_request_id: u64, } /// Broker-local receive adapter for broker-initiated asynchronous notifications. @@ -96,6 +97,7 @@ impl BrokerLocal { Ok(Self { channel, shared_memory, + next_request_id: 0, }) } BrokerHandshakeResponse::VersionMismatch { .. } => { @@ -122,18 +124,35 @@ impl BrokerLocal { /// response that does not match an active request. pub(crate) fn request( &mut self, - request: BrokerRequest, - ) -> Result { + operation: BrokerOperation, + ) -> Result { + let request_id = RequestId(self.next_request_id); + self.next_request_id = self + .next_request_id + .checked_add(1) + .ok_or(BrokerLocalError::RequestIdExhausted)?; self.channel - .send_request(&request) + .send_request(&BrokerRequest { + request_id, + operation, + }) .map_err(BrokerLocalError::Channel)?; - match self + let BrokerResponse { + request_id: response_id, + result, + } = self .channel .recv_response() .map_err(BrokerLocalError::Channel)? - .ok_or(BrokerLocalError::ChannelClosed)? - { - BrokerResponse::Error(error) => match error { + .ok_or(BrokerLocalError::ChannelClosed)?; + if response_id != request_id { + return Err(BrokerLocalError::UnexpectedResponseId { + expected: request_id, + actual: response_id, + }); + } + match result { + BrokerResult::Error(error) => match error { ErrorCode::PolicyDenied | ErrorCode::UnknownObject | ErrorCode::InvalidRights @@ -148,10 +167,10 @@ impl BrokerLocal { | ErrorCode::Internal => panic!("broker returned unrecoverable error: {error}"), _ => panic!("broker returned unsupported error: {error}"), }, - response @ (BrokerResponse::Event(_) - | BrokerResponse::Pipe(_) - | BrokerResponse::ObjectClosed - | BrokerResponse::Readiness(_)) => Ok(response), + result @ (BrokerResult::Event(_) + | BrokerResult::Pipe(_) + | BrokerResult::ObjectClosed + | BrokerResult::Readiness(_)) => Ok(result), } } @@ -165,9 +184,9 @@ impl BrokerLocal { &mut self, handle: ObjectHandle, ) -> Result { - match self.request(BrokerRequest::CheckReadiness(handle))? { - BrokerResponse::Readiness(readiness) => Ok(readiness), - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + match self.request(BrokerOperation::CheckReadiness(handle))? { + BrokerResult::Readiness(readiness) => Ok(readiness), + BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), response => panic!("broker returned unexpected readiness response: {response:?}"), } } @@ -179,12 +198,12 @@ impl BrokerLocal { /// Panics if the broker reports an unrecoverable error or returns a protocol /// response that does not match an object close request. pub fn close_object(&mut self, handle: ObjectHandle) -> Result<(), Channel::Error> { - match self.request(BrokerRequest::CloseObject(handle))? { - BrokerResponse::ObjectClosed => Ok(()), - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response @ (BrokerResponse::Event(_) - | BrokerResponse::Pipe(_) - | BrokerResponse::Readiness(_)) => { + match self.request(BrokerOperation::CloseObject(handle))? { + BrokerResult::ObjectClosed => Ok(()), + BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), + response @ (BrokerResult::Event(_) + | BrokerResult::Pipe(_) + | BrokerResult::Readiness(_)) => { panic!("broker returned unexpected close response: {response:?}"); } } @@ -248,25 +267,92 @@ mod tests { #[test] fn close_object_sends_close_object_request() { let handle = ObjectHandle(7); - let request = BrokerRequest::CloseObject(handle); - let response = BrokerResponse::ObjectClosed; + let request = BrokerOperation::CloseObject(handle); + let response = BrokerResult::ObjectClosed; let channel = FakeControlChannel::new(None, Some(response.clone())); let mut local = BrokerLocal { channel, shared_memory: noop_shared_memory(), + next_request_id: 0, }; assert!(local.close_object(handle).is_ok()); - assert_eq!(local.channel.sent_request, Some(request)); + assert_eq!( + local.channel.sent_request, + Some(BrokerRequest { + request_id: RequestId(0), + operation: request, + }) + ); + } + + #[test] + fn active_requests_use_monotonic_identifiers() { + let handle = ObjectHandle(7); + let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); + let mut local = BrokerLocal { + channel, + shared_memory: noop_shared_memory(), + next_request_id: 0, + }; + + local.close_object(handle).unwrap(); + assert_eq!( + local.channel.sent_request.as_ref().unwrap().request_id, + RequestId(0) + ); + + local.channel.response = Some(BrokerResult::ObjectClosed); + local.close_object(handle).unwrap(); + assert_eq!( + local.channel.sent_request.as_ref().unwrap().request_id, + RequestId(1) + ); + } + + #[test] + fn active_request_rejects_mismatched_response_identifier() { + let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); + let mut local = BrokerLocal { + channel, + shared_memory: noop_shared_memory(), + next_request_id: 0, + }; + local.channel.response_id = Some(RequestId(9)); + + assert!(matches!( + local.close_object(ObjectHandle(7)), + Err(BrokerLocalError::UnexpectedResponseId { + expected: RequestId(0), + actual: RequestId(9), + }) + )); + } + + #[test] + fn active_request_identifier_exhaustion_does_not_wrap() { + let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); + let mut local = BrokerLocal { + channel, + shared_memory: noop_shared_memory(), + next_request_id: u64::MAX, + }; + + assert!(matches!( + local.close_object(ObjectHandle(7)), + Err(BrokerLocalError::RequestIdExhausted) + )); + assert!(local.channel.sent_request.is_none()); } #[test] fn active_request_returns_recoverable_broker_error() { let channel = - FakeControlChannel::new(None, Some(BrokerResponse::Error(ErrorCode::WouldBlock))); + FakeControlChannel::new(None, Some(BrokerResult::Error(ErrorCode::WouldBlock))); let mut local = BrokerLocal { channel, shared_memory: noop_shared_memory(), + next_request_id: 0, }; assert!(matches!( @@ -278,11 +364,11 @@ mod tests { #[test] #[should_panic(expected = "broker returned unrecoverable error")] fn active_request_panics_on_unrecoverable_broker_error() { - let channel = - FakeControlChannel::new(None, Some(BrokerResponse::Error(ErrorCode::Internal))); + let channel = FakeControlChannel::new(None, Some(BrokerResult::Error(ErrorCode::Internal))); let mut local = BrokerLocal { channel, shared_memory: noop_shared_memory(), + next_request_id: 0, }; let _ = local.create_event_with_count(0); @@ -415,7 +501,8 @@ mod tests { sent_handshake_request: Option, sent_request: Option, handshake_response: Option, - response: Option, + response: Option, + response_id: Option, } #[derive(Debug, PartialEq, Eq)] @@ -461,13 +548,14 @@ mod tests { impl FakeControlChannel { const fn new( handshake_response: Option, - response: Option, + response: Option, ) -> Self { Self { sent_handshake_request: None, sent_request: None, handshake_response, response, + response_id: None, } } } @@ -498,7 +586,15 @@ mod tests { } fn recv_response(&mut self) -> core::result::Result, Self::Error> { - Ok(self.response.take()) + Ok(self.response.take().map(|result| BrokerResponse { + request_id: self.response_id.unwrap_or_else(|| { + self.sent_request + .as_ref() + .expect("response requires a sent request") + .request_id + }), + result, + })) } } diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs index 888af95b89..ec284c91dd 100644 --- a/litebox_broker_local/src/pipe.rs +++ b/litebox_broker_local/src/pipe.rs @@ -5,7 +5,7 @@ use alloc::vec::Vec; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; -use litebox_broker_protocol::message::{BrokerRequest, BrokerResponse, PipeRequest, PipeResponse}; +use litebox_broker_protocol::message::{BrokerOperation, BrokerResult, PipeRequest, PipeResponse}; use litebox_broker_protocol::pipe::{ CreatePipeRequest, CreatePipeResponse, PIPE_TRANSFER_BUFFER_SIZE, ReadPipeRequest, WritePipeRequest, @@ -110,12 +110,12 @@ impl BrokerLocal { } fn request_pipe(&mut self, request: PipeRequest) -> Result { - match self.request(BrokerRequest::Pipe(request))? { - BrokerResponse::Pipe(response) => Ok(response), - BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), - response @ (BrokerResponse::ObjectClosed - | BrokerResponse::Readiness(_) - | BrokerResponse::Event(_)) => { + match self.request(BrokerOperation::Pipe(request))? { + BrokerResult::Pipe(response) => Ok(response), + BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), + response @ (BrokerResult::ObjectClosed + | BrokerResult::Readiness(_) + | BrokerResult::Event(_)) => { panic!("broker returned unexpected pipe response: {response:?}"); } } @@ -130,11 +130,14 @@ mod tests { use std::collections::VecDeque; use std::sync::Mutex; - use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; use litebox_broker_protocol::channel::LocalControlChannel; - use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerHandshakeResponse}; + use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, + BrokerResponse, BrokerResult, + }; use litebox_broker_protocol::pipe::{ReadPipeResponse, WritePipeResponse}; use litebox_broker_protocol::shared_memory::{SharedMemory, SharedMemoryError}; + use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, RequestId}; #[test] fn pipe_uses_attached_shared_memory_for_data_operations() { @@ -142,12 +145,12 @@ mod tests { let write_handle = ObjectHandle(2); let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); let channel = ScriptedChannel::new([ - BrokerResponse::Pipe(PipeResponse::Create(CreatePipeResponse { + BrokerResult::Pipe(PipeResponse::Create(CreatePipeResponse { read_handle, write_handle, })), - BrokerResponse::Pipe(PipeResponse::Write(WritePipeResponse { written: 2 })), - BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2 })), + BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { written: 2 })), + BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2 })), ]); let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory.clone())).unwrap(); @@ -160,17 +163,17 @@ mod tests { memory.write(0, &[4, 5, 6]).unwrap(); assert_eq!(local.read_pipe(read_handle, 3).unwrap(), [4, 5]); assert_eq!( - local.channel.sent_requests, + local.channel.sent_operations, [ - BrokerRequest::Pipe(PipeRequest::Create(CreatePipeRequest { + BrokerOperation::Pipe(PipeRequest::Create(CreatePipeRequest { capacity: 64, atomic_write_size: 16, })), - BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { + BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle: write_handle, length: 3, })), - BrokerRequest::Pipe(PipeRequest::Read(ReadPipeRequest { + BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { handle: read_handle, length: 3, })), @@ -197,14 +200,14 @@ mod tests { litebox_broker_protocol::error::ErrorCode::ResourceExhausted )) )); - assert!(local.channel.sent_requests.is_empty()); + assert!(local.channel.sent_operations.is_empty()); } #[test] #[should_panic(expected = "broker returned oversized pipe read")] fn read_pipe_rejects_oversized_response() { let channel = - ScriptedChannel::new([BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { + ScriptedChannel::new([BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2, }))]); let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); @@ -216,9 +219,10 @@ mod tests { #[test] #[should_panic(expected = "broker returned oversized shared pipe write")] fn write_pipe_rejects_oversized_response() { - let channel = ScriptedChannel::new([BrokerResponse::Pipe(PipeResponse::Write( - WritePipeResponse { written: 2 }, - ))]); + let channel = + ScriptedChannel::new([BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { + written: 2, + }))]); let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); @@ -273,15 +277,17 @@ mod tests { } struct ScriptedChannel { - responses: VecDeque, - sent_requests: Vec, + results: VecDeque, + sent_operations: Vec, + last_request_id: Option, } impl ScriptedChannel { - fn new(responses: impl IntoIterator) -> Self { + fn new(results: impl IntoIterator) -> Self { Self { - responses: responses.into_iter().collect(), - sent_requests: Vec::new(), + results: results.into_iter().collect(), + sent_operations: Vec::new(), + last_request_id: None, } } } @@ -309,12 +315,18 @@ mod tests { &mut self, request: &BrokerRequest, ) -> core::result::Result<(), Self::Error> { - self.sent_requests.push(request.clone()); + self.sent_operations.push(request.operation.clone()); + self.last_request_id = Some(request.request_id); Ok(()) } fn recv_response(&mut self) -> core::result::Result, Self::Error> { - Ok(self.responses.pop_front()) + Ok(self.results.pop_front().map(|result| BrokerResponse { + request_id: self + .last_request_id + .expect("response requires a sent request"), + result, + })) } } } diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 07ee191085..d347a98b4b 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -26,6 +26,11 @@ pub mod wire; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ObjectHandle(pub u64); +/// Association-scoped broker request identifier. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RequestId(pub u64); + /// Broker protocol version. #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index 78afa826be..e7a59defa3 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -11,7 +11,7 @@ use crate::pipe::{ WritePipeResponse, }; use crate::readiness::ReadinessFlags; -use crate::{ObjectHandle, ProtocolVersion}; +use crate::{ObjectHandle, ProtocolVersion, RequestId}; /// Broker handshake request sent before the control channel is active. #[derive(Clone, Debug, PartialEq, Eq)] @@ -20,9 +20,9 @@ pub struct BrokerHandshakeRequest { pub protocol_version: ProtocolVersion, } -/// Broker request sent over an active control channel. +/// Operation requested over an active broker control channel. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum BrokerRequest { +pub enum BrokerOperation { /// Close one broker object reference. CloseObject(ObjectHandle), /// Check the current readiness of a broker-owned object. @@ -33,6 +33,15 @@ pub enum BrokerRequest { Pipe(PipeRequest), } +/// Request sent over an active broker control channel. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BrokerRequest { + /// Correlation identifier allocated by the local endpoint. + pub request_id: RequestId, + /// Requested broker operation. + pub operation: BrokerOperation, +} + /// Broker handshake response sent before the control channel is active. #[derive(Clone, Debug, PartialEq, Eq)] pub enum BrokerHandshakeResponse { @@ -79,9 +88,9 @@ pub enum PipeRequest { Write(WritePipeRequest), } -/// Broker response sent over an active control channel. +/// Result returned for an active broker operation. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum BrokerResponse { +pub enum BrokerResult { /// Object close operation completed. ObjectClosed, /// Current readiness of a broker-owned object. @@ -94,6 +103,15 @@ pub enum BrokerResponse { Error(ErrorCode), } +/// Response sent over an active broker control channel. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BrokerResponse { + /// Correlation identifier copied from the request. + pub request_id: RequestId, + /// Result of the requested broker operation. + pub result: BrokerResult, +} + /// Broker-owned event object response. #[derive(Clone, Debug, PartialEq, Eq)] pub enum EventResponse { diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 12c3492e71..16ccf84691 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -20,8 +20,8 @@ use thiserror::Error; use crate::error::ErrorCode; use crate::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, - BrokerResponse, ReadinessNotification, + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, + BrokerRequest, BrokerResponse, BrokerResult, ReadinessNotification, }; use crate::readiness::ReadinessFlags; @@ -39,11 +39,12 @@ const REQUEST_TAG_CHECK_READINESS: u8 = 4; const RESPONSE_TAG_NEGOTIATED: u8 = 0; const RESPONSE_TAG_EVENT: u8 = 1; -const RESPONSE_TAG_ERROR: u8 = 2; +const RESPONSE_TAG_HANDSHAKE_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; const RESPONSE_TAG_OBJECT_CLOSED: u8 = 4; const RESPONSE_TAG_PIPE: u8 = 5; const RESPONSE_TAG_READINESS: u8 = 6; +const RESPONSE_TAG_ERROR: u8 = 7; const NOTIFICATION_TAG_READINESS: u8 = 0; @@ -100,21 +101,29 @@ pub fn decode_handshake_request(frame: &[u8]) -> Result Vec { let mut encoder = Encoder::default(); - match request { - BrokerRequest::CloseObject(handle) => { + let BrokerRequest { + request_id, + operation, + } = request; + match operation { + BrokerOperation::CloseObject(handle) => { encoder.u8(REQUEST_TAG_CLOSE_OBJECT); + encoder.request_id(request_id); encoder.handle(handle); } - BrokerRequest::CheckReadiness(handle) => { + BrokerOperation::CheckReadiness(handle) => { encoder.u8(REQUEST_TAG_CHECK_READINESS); + encoder.request_id(request_id); encoder.handle(handle); } - BrokerRequest::Event(request) => { + BrokerOperation::Event(request) => { encoder.u8(REQUEST_TAG_EVENT); + encoder.request_id(request_id); event::encode_event_request(&mut encoder, request); } - BrokerRequest::Pipe(request) => { + BrokerOperation::Pipe(request) => { encoder.u8(REQUEST_TAG_PIPE); + encoder.request_id(request_id); pipe::encode_pipe_request(&mut encoder, request); } } @@ -125,16 +134,27 @@ pub fn encode_request(request: BrokerRequest) -> Vec { pub fn decode_request(frame: &[u8]) -> Result { let mut decoder = Decoder::new(frame); let tag = decoder.u8()?; - let request = match tag { + match tag { REQUEST_TAG_NEGOTIATE => return Err(WireError::WrongMessagePhase), - REQUEST_TAG_CLOSE_OBJECT => BrokerRequest::CloseObject(decoder.handle()?), - REQUEST_TAG_CHECK_READINESS => BrokerRequest::CheckReadiness(decoder.handle()?), - REQUEST_TAG_EVENT => BrokerRequest::Event(event::decode_event_request(&mut decoder)?), - REQUEST_TAG_PIPE => BrokerRequest::Pipe(pipe::decode_pipe_request(&mut decoder)?), + REQUEST_TAG_CLOSE_OBJECT + | REQUEST_TAG_CHECK_READINESS + | REQUEST_TAG_EVENT + | REQUEST_TAG_PIPE => {} _ => return Err(WireError::InvalidTag), + } + let request_id = decoder.request_id()?; + let operation = match tag { + REQUEST_TAG_CLOSE_OBJECT => BrokerOperation::CloseObject(decoder.handle()?), + REQUEST_TAG_CHECK_READINESS => BrokerOperation::CheckReadiness(decoder.handle()?), + REQUEST_TAG_EVENT => BrokerOperation::Event(event::decode_event_request(&mut decoder)?), + REQUEST_TAG_PIPE => BrokerOperation::Pipe(pipe::decode_pipe_request(&mut decoder)?), + _ => unreachable!("active request tag was validated"), }; decoder.finish()?; - Ok(request) + Ok(BrokerRequest { + request_id, + operation, + }) } /// Encodes a broker handshake response body. @@ -157,7 +177,7 @@ pub fn encode_handshake_response(response: BrokerHandshakeResponse) -> Vec { encoder.protocol_version(broker_protocol_version); } BrokerHandshakeResponse::Error(error) => { - encoder.u8(RESPONSE_TAG_ERROR); + encoder.u8(RESPONSE_TAG_HANDSHAKE_ERROR); encoder.u16(error.as_raw()); } } @@ -175,13 +195,14 @@ pub fn decode_handshake_response(frame: &[u8]) -> Result { + | RESPONSE_TAG_READINESS + | RESPONSE_TAG_ERROR => { return Err(WireError::WrongMessagePhase); } RESPONSE_TAG_VERSION_MISMATCH => BrokerHandshakeResponse::VersionMismatch { broker_protocol_version: decoder.protocol_version()?, }, - RESPONSE_TAG_ERROR => { + RESPONSE_TAG_HANDSHAKE_ERROR => { let error = ErrorCode::from_raw(decoder.u16()?).ok_or(WireError::InvalidTag)?; BrokerHandshakeResponse::Error(error) } @@ -197,24 +218,30 @@ pub fn decode_handshake_response(frame: &[u8]) -> Result Vec { let mut encoder = Encoder::default(); - match response { - BrokerResponse::ObjectClosed => { + let BrokerResponse { request_id, result } = response; + match result { + BrokerResult::ObjectClosed => { encoder.u8(RESPONSE_TAG_OBJECT_CLOSED); + encoder.request_id(request_id); } - BrokerResponse::Readiness(readiness) => { + BrokerResult::Readiness(readiness) => { encoder.u8(RESPONSE_TAG_READINESS); + encoder.request_id(request_id); encoder.u32(readiness.0); } - BrokerResponse::Event(response) => { + BrokerResult::Event(response) => { encoder.u8(RESPONSE_TAG_EVENT); + encoder.request_id(request_id); event::encode_event_response(&mut encoder, response); } - BrokerResponse::Pipe(response) => { + BrokerResult::Pipe(response) => { encoder.u8(RESPONSE_TAG_PIPE); + encoder.request_id(request_id); pipe::encode_pipe_response(&mut encoder, response); } - BrokerResponse::Error(error) => { + BrokerResult::Error(error) => { encoder.u8(RESPONSE_TAG_ERROR); + encoder.request_id(request_id); encoder.u16(error.as_raw()); } } @@ -225,22 +252,31 @@ pub fn encode_response(response: BrokerResponse) -> Vec { pub fn decode_response(frame: &[u8]) -> Result { let mut decoder = Decoder::new(frame); let tag = decoder.u8()?; - let response = match tag { - RESPONSE_TAG_NEGOTIATED | RESPONSE_TAG_VERSION_MISMATCH => { + match tag { + RESPONSE_TAG_NEGOTIATED | RESPONSE_TAG_HANDSHAKE_ERROR | RESPONSE_TAG_VERSION_MISMATCH => { return Err(WireError::WrongMessagePhase); } - RESPONSE_TAG_EVENT => BrokerResponse::Event(event::decode_event_response(&mut decoder)?), - RESPONSE_TAG_PIPE => BrokerResponse::Pipe(pipe::decode_pipe_response(&mut decoder)?), + RESPONSE_TAG_EVENT + | RESPONSE_TAG_OBJECT_CLOSED + | RESPONSE_TAG_PIPE + | RESPONSE_TAG_READINESS + | RESPONSE_TAG_ERROR => {} + _ => return Err(WireError::InvalidTag), + } + let request_id = decoder.request_id()?; + let result = match tag { + RESPONSE_TAG_EVENT => BrokerResult::Event(event::decode_event_response(&mut decoder)?), + RESPONSE_TAG_PIPE => BrokerResult::Pipe(pipe::decode_pipe_response(&mut decoder)?), RESPONSE_TAG_ERROR => { let error = ErrorCode::from_raw(decoder.u16()?).ok_or(WireError::InvalidTag)?; - BrokerResponse::Error(error) + BrokerResult::Error(error) } - RESPONSE_TAG_OBJECT_CLOSED => BrokerResponse::ObjectClosed, - RESPONSE_TAG_READINESS => BrokerResponse::Readiness(ReadinessFlags(decoder.u32()?)), - _ => return Err(WireError::InvalidTag), + RESPONSE_TAG_OBJECT_CLOSED => BrokerResult::ObjectClosed, + RESPONSE_TAG_READINESS => BrokerResult::Readiness(ReadinessFlags(decoder.u32()?)), + _ => unreachable!("active response tag was validated"), }; decoder.finish()?; - Ok(response) + Ok(BrokerResponse { request_id, result }) } /// Encodes a broker notification body. @@ -286,7 +322,9 @@ mod tests { CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, WritePipeResponse, }; - use crate::{ObjectHandle, ProtocolVersion}; + use crate::{ObjectHandle, ProtocolVersion, RequestId}; + + const TEST_REQUEST_ID: RequestId = RequestId(0x0102_0304_0506_0708); #[test] fn handshake_request_codec_round_trips_all_variants() { @@ -305,33 +343,51 @@ mod tests { #[test] fn request_codec_round_trips_all_variants() { let handle = ObjectHandle(13); - let requests = [ - BrokerRequest::CloseObject(handle), - BrokerRequest::CheckReadiness(handle), - BrokerRequest::Event(EventRequest::Create(CreateEventRequest { + let operations = [ + BrokerOperation::CloseObject(handle), + BrokerOperation::CheckReadiness(handle), + BrokerOperation::Event(EventRequest::Create(CreateEventRequest { initial_count: 0, })), - BrokerRequest::Event(EventRequest::Create(CreateEventRequest { + BrokerOperation::Event(EventRequest::Create(CreateEventRequest { initial_count: 7, })), - BrokerRequest::Event(EventRequest::Add(AddEventRequest { handle, value: 3 })), - BrokerRequest::Event(EventRequest::Consume(ConsumeEventRequest { + BrokerOperation::Event(EventRequest::Add(AddEventRequest { handle, value: 3 })), + BrokerOperation::Event(EventRequest::Consume(ConsumeEventRequest { handle, mode: EventConsumeMode::All, })), - BrokerRequest::Event(EventRequest::Consume(ConsumeEventRequest { + BrokerOperation::Event(EventRequest::Consume(ConsumeEventRequest { handle, mode: EventConsumeMode::One, })), - BrokerRequest::Pipe(PipeRequest::Create(CreatePipeRequest { + BrokerOperation::Pipe(PipeRequest::Create(CreatePipeRequest { capacity: 4096, atomic_write_size: 512, })), - BrokerRequest::Pipe(PipeRequest::Read(ReadPipeRequest { handle, length: 32 })), - BrokerRequest::Pipe(PipeRequest::Write(WritePipeRequest { handle, length: 3 })), + BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { handle, length: 32 })), + BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle, length: 3 })), ]; - for request in requests { + for operation in operations { + let request = BrokerRequest { + request_id: TEST_REQUEST_ID, + operation, + }; + assert_eq!( + decode_request(&encode_request(request.clone())).unwrap(), + request + ); + } + } + + #[test] + fn request_codec_round_trips_identifier_bounds() { + for request_id in [RequestId(0), RequestId(u64::MAX)] { + let request = BrokerRequest { + request_id, + operation: BrokerOperation::CloseObject(ObjectHandle(13)), + }; assert_eq!( decode_request(&encode_request(request.clone())).unwrap(), request @@ -363,32 +419,50 @@ mod tests { #[test] fn response_codec_round_trips_all_variants() { let handle = ObjectHandle(13); - let responses = [ - BrokerResponse::ObjectClosed, - BrokerResponse::Readiness(ReadinessFlags::READ), - BrokerResponse::Readiness(ReadinessFlags::WRITE), - BrokerResponse::Event(EventResponse::Create(CreateEventResponse { handle })), - BrokerResponse::Event(EventResponse::Add(AddEventResponse { + let results = [ + BrokerResult::ObjectClosed, + BrokerResult::Readiness(ReadinessFlags::READ), + BrokerResult::Readiness(ReadinessFlags::WRITE), + BrokerResult::Event(EventResponse::Create(CreateEventResponse { handle })), + BrokerResult::Event(EventResponse::Add(AddEventResponse { readiness: ReadinessFlags::READ | ReadinessFlags::WRITE, })), - BrokerResponse::Event(EventResponse::Consume(EventConsumption { + BrokerResult::Event(EventResponse::Consume(EventConsumption { value: 3, readiness: ReadinessFlags::WRITE, })), - BrokerResponse::Pipe(PipeResponse::Create(CreatePipeResponse { + BrokerResult::Pipe(PipeResponse::Create(CreatePipeResponse { read_handle: handle, write_handle: ObjectHandle(14), })), - BrokerResponse::Pipe(PipeResponse::Read(ReadPipeResponse { read: 3 })), - BrokerResponse::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })), - BrokerResponse::Error(ErrorCode::PolicyDenied), - BrokerResponse::Error(ErrorCode::WouldBlock), - BrokerResponse::Error(ErrorCode::PeerClosed), - BrokerResponse::Error(ErrorCode::OutOfMemory), - BrokerResponse::Error(ErrorCode::Internal), + BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 3 })), + BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })), + BrokerResult::Error(ErrorCode::PolicyDenied), + BrokerResult::Error(ErrorCode::WouldBlock), + BrokerResult::Error(ErrorCode::PeerClosed), + BrokerResult::Error(ErrorCode::OutOfMemory), + BrokerResult::Error(ErrorCode::Internal), ]; - for response in responses { + for result in results { + let response = BrokerResponse { + request_id: TEST_REQUEST_ID, + result, + }; + assert_eq!( + decode_response(&encode_response(response.clone())).unwrap(), + response + ); + } + } + + #[test] + fn response_codec_round_trips_identifier_bounds() { + for request_id in [RequestId(0), RequestId(u64::MAX)] { + let response = BrokerResponse { + request_id, + result: BrokerResult::ObjectClosed, + }; assert_eq!( decode_response(&encode_response(response.clone())).unwrap(), response @@ -423,15 +497,19 @@ mod tests { Err(WireError::TruncatedFrame) ); assert_eq!( - decode_handshake_request(&encode_request(BrokerRequest::Event(EventRequest::Create( - CreateEventRequest { initial_count: 0 }, - )))), + decode_handshake_request(&encode_request(BrokerRequest { + request_id: TEST_REQUEST_ID, + operation: BrokerOperation::Event(EventRequest::Create(CreateEventRequest { + initial_count: 0, + })), + })), Err(WireError::WrongMessagePhase) ); assert_eq!( - decode_handshake_request(&encode_request(BrokerRequest::CloseObject(ObjectHandle( - 13 - )))), + decode_handshake_request(&encode_request(BrokerRequest { + request_id: TEST_REQUEST_ID, + operation: BrokerOperation::CloseObject(ObjectHandle(13)), + })), Err(WireError::WrongMessagePhase) ); let mut frame = encode_handshake_request(BrokerHandshakeRequest { @@ -453,20 +531,28 @@ mod tests { })), Err(WireError::WrongMessagePhase) ); - let mut unknown_consume_mode = encode_request(BrokerRequest::Event(EventRequest::Consume( - ConsumeEventRequest { + assert_eq!( + decode_request(&[REQUEST_TAG_EVENT, 0, 0, 0, 0, 0, 0, 0]), + Err(WireError::TruncatedFrame) + ); + let mut unknown_consume_mode = encode_request(BrokerRequest { + request_id: TEST_REQUEST_ID, + operation: BrokerOperation::Event(EventRequest::Consume(ConsumeEventRequest { handle: ObjectHandle(13), mode: EventConsumeMode::All, - }, - ))); + })), + }); *unknown_consume_mode.last_mut().unwrap() = 0xff; assert_eq!( decode_request(&unknown_consume_mode), Err(WireError::InvalidTag) ); - let mut frame = encode_request(BrokerRequest::Event(EventRequest::Create( - CreateEventRequest { initial_count: 0 }, - ))); + let mut frame = encode_request(BrokerRequest { + request_id: TEST_REQUEST_ID, + operation: BrokerOperation::Event(EventRequest::Create(CreateEventRequest { + initial_count: 0, + })), + }); frame.push(0xff); assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); } @@ -486,15 +572,26 @@ mod tests { Err(WireError::InvalidTag) ); assert_eq!( - decode_handshake_response(&encode_response(BrokerResponse::Event( - EventResponse::Create(CreateEventResponse { + decode_handshake_response(&encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Event(EventResponse::Create(CreateEventResponse { handle: ObjectHandle(13), - }), - ))), + })), + })), Err(WireError::WrongMessagePhase) ); assert_eq!( - decode_handshake_response(&encode_response(BrokerResponse::ObjectClosed)), + decode_handshake_response(&encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::ObjectClosed, + })), + Err(WireError::WrongMessagePhase) + ); + assert_eq!( + decode_handshake_response(&encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Error(ErrorCode::WouldBlock), + })), Err(WireError::WrongMessagePhase) ); @@ -514,31 +611,38 @@ mod tests { decode_response(&[0xff, 1, 2, 3]), Err(WireError::InvalidTag) ); - assert_eq!( - decode_response(&encode_handshake_response( - BrokerHandshakeResponse::Negotiated { - broker_protocol_version: ProtocolVersion(1), - }, - )), - Err(WireError::WrongMessagePhase) - ); + for response in [ + BrokerHandshakeResponse::Negotiated { + broker_protocol_version: ProtocolVersion(1), + }, + BrokerHandshakeResponse::VersionMismatch { + broker_protocol_version: ProtocolVersion(1), + }, + BrokerHandshakeResponse::Error(ErrorCode::PolicyDenied), + ] { + assert_eq!( + decode_response(&encode_handshake_response(response)), + Err(WireError::WrongMessagePhase) + ); + } assert_eq!( decode_response(&[RESPONSE_TAG_READINESS, 0xff]), Err(WireError::TruncatedFrame) ); - assert_eq!( - decode_response(&[2, 0xff, 0xff]), - Err(WireError::InvalidTag) - ); + let mut invalid_error = Vec::from([RESPONSE_TAG_ERROR]); + invalid_error.extend_from_slice(&TEST_REQUEST_ID.0.to_le_bytes()); + invalid_error.extend_from_slice(&u16::MAX.to_le_bytes()); + assert_eq!(decode_response(&invalid_error), Err(WireError::InvalidTag)); - let truncated = [1, 2, 2, 0]; + let truncated = [RESPONSE_TAG_EVENT, 2, 2, 0]; assert_eq!(decode_response(&truncated), Err(WireError::TruncatedFrame)); - let mut frame = encode_response(BrokerResponse::Event(EventResponse::Add( - AddEventResponse { + let mut frame = encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Event(EventResponse::Add(AddEventResponse { readiness: ReadinessFlags::READ | ReadinessFlags::WRITE, - }, - ))); + })), + }); frame.push(0xff); assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); } @@ -577,15 +681,29 @@ mod tests { ); } + #[test] + fn event_create_request_wire_shape_is_pinned() { + assert_eq!( + encode_request(BrokerRequest { + request_id: RequestId(13), + operation: BrokerOperation::Event(EventRequest::Create(CreateEventRequest { + initial_count: 7, + })), + }), + [1, 13, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0] + ); + } + #[test] fn event_add_response_wire_shape_is_pinned() { assert_eq!( - encode_response(BrokerResponse::Event(EventResponse::Add( - AddEventResponse { + encode_response(BrokerResponse { + request_id: RequestId(13), + result: BrokerResult::Event(EventResponse::Add(AddEventResponse { readiness: ReadinessFlags::READ, - } - ))), - [1, 1, 1, 0, 0, 0] + })), + }), + [1, 13, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0] ); } diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs index 617f411f7f..83a9d00b92 100644 --- a/litebox_broker_protocol/src/wire/primitive.rs +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -3,7 +3,7 @@ use alloc::vec::Vec; -use crate::{ObjectHandle, ProtocolVersion}; +use crate::{ObjectHandle, ProtocolVersion, RequestId}; use super::WireError; @@ -40,6 +40,10 @@ impl Encoder { pub(super) fn handle(&mut self, handle: ObjectHandle) { self.u64(handle.0); } + + pub(super) fn request_id(&mut self, request_id: RequestId) { + self.u64(request_id.0); + } } pub(super) struct Decoder<'a> { @@ -90,6 +94,10 @@ impl<'a> Decoder<'a> { Ok(ObjectHandle(self.u64()?)) } + pub(super) fn request_id(&mut self) -> Result { + Ok(RequestId(self.u64()?)) + } + fn take(&mut self, len: usize) -> Result<&'a [u8], WireError> { let end = self .offset diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 66d908ee9d..1220007de8 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -390,6 +390,8 @@ fn wire_error(error: WireError) -> Error { #[cfg(test)] mod tests { use super::*; + use litebox_broker_protocol::RequestId; + use litebox_broker_protocol::message::{BrokerOperation, BrokerRequest}; use std::time::Duration; #[test] @@ -610,11 +612,14 @@ mod tests { let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); write_frame_with_deadline( &mut peer_stream, - &encode_request(BrokerRequest::Event( - litebox_broker_protocol::message::EventRequest::Create( - litebox_broker_protocol::event::CreateEventRequest { initial_count: 0 }, + &encode_request(BrokerRequest { + request_id: RequestId(0), + operation: BrokerOperation::Event( + litebox_broker_protocol::message::EventRequest::Create( + litebox_broker_protocol::event::CreateEventRequest { initial_count: 0 }, + ), ), - )), + }), None, ) .unwrap(); diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 2c5cd0574f..41eeddeae8 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -459,7 +459,10 @@ impl if matches!( &request, litebox_broker_protocol::channel::HostReceive::Message( - litebox_broker_protocol::message::BrokerRequest::CloseObject(_) + litebox_broker_protocol::message::BrokerRequest { + operation: litebox_broker_protocol::message::BrokerOperation::CloseObject(_), + .. + } ) ) { self.close_object_count += 1; From 6b186e37cd24410018df1b752ad43cd836f44168 Mon Sep 17 00:00:00 2001 From: Will Portnoy Date: Wed, 22 Jul 2026 11:45:06 -0700 Subject: [PATCH 113/319] fix(net): accept advisory socket options (IP_TOS, SO_RCVBUF, SO_SNDBUF) (cherry-pick of #1054) (#1062) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of #1054 (commit `886f8278`) onto `ulitebox`. --- setsockopt returned EOPNOTSUPP for IP_TOS, SO_RCVBUF and SO_SNDBUF, which Node/libuv treat as fatal — Socket.setTypeOfService and TLS buffer sizing raise uncaught exceptions that tear down the connection. These options are advisory hints, so accept them silently and keep the fixed internal buffer size that getsockopt already reports, for both INET and UNIX sockets. This matches how unprivileged sockets behave on native Linux. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/syscalls/net.rs | 23 ++++++++++++++++++++--- litebox_shim_linux/src/syscalls/unix.rs | 11 +++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index 997ccb632b..bc341ac070 100644 --- a/litebox_shim_linux/src/syscalls/net.rs +++ b/litebox_shim_linux/src/syscalls/net.rs @@ -385,7 +385,15 @@ impl GlobalState { match optname { SocketOptionName::IP(ip) => match ip { - litebox_common_linux::IpOption::TOS => return Err(Errno::EOPNOTSUPP), + // IP_TOS is an advisory traffic-class hint. Accept it (we don't + // propagate the bit anywhere) instead of returning EOPNOTSUPP, + // which Node's Socket.setTypeOfService treats as fatal and + // cascades into tearing down the connection. Log at debug so the + // accepted-but-ignored option stays visible. + litebox_common_linux::IpOption::TOS => { + litebox_util_log::debug!("accepting and ignoring setsockopt(IP_TOS)"); + return Ok(()); + } }, SocketOptionName::Socket(so) => match so { // handled by `setsockopt_common` @@ -395,8 +403,17 @@ impl GlobalState { | SocketOption::REUSEADDR | SocketOption::BROADCAST | SocketOption::KEEPALIVE => unreachable!(), - // We use fixed buffer size for now - SocketOption::RCVBUF | SocketOption::SNDBUF => return Err(Errno::EOPNOTSUPP), + // SO_RCVBUF / SO_SNDBUF are advisory hints. Accept them and keep + // the fixed internal buffer size that getsockopt reports, instead + // of returning EOPNOTSUPP (which Node's TLS socket path treats as + // fatal). Log at debug so the accepted-but-ignored option stays + // visible. + SocketOption::RCVBUF | SocketOption::SNDBUF => { + litebox_util_log::debug!( + "accepting and ignoring setsockopt(SO_RCVBUF/SO_SNDBUF); using fixed buffer size" + ); + return Ok(()); + } // Socket does not support these options SocketOption::TYPE | SocketOption::PEERCRED | SocketOption::ERROR => { return Err(Errno::ENOPROTOOPT); diff --git a/litebox_shim_linux/src/syscalls/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index 5ae3222bbb..f6edd53b30 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -1513,8 +1513,15 @@ impl UnixSocket { SocketOption::TYPE | SocketOption::PEERCRED | SocketOption::ERROR => { Err(Errno::ENOPROTOOPT) } - // We use fixed buffer size for now - SocketOption::RCVBUF | SocketOption::SNDBUF => Err(Errno::EOPNOTSUPP), + // SO_RCVBUF / SO_SNDBUF are advisory hints. Accept them and keep + // the fixed internal buffer size, instead of returning EOPNOTSUPP. + // Log at debug so the accepted-but-ignored option stays visible. + SocketOption::RCVBUF | SocketOption::SNDBUF => { + litebox_util_log::debug!( + "accepting and ignoring setsockopt(SO_RCVBUF/SO_SNDBUF) on unix socket; using fixed buffer size" + ); + Ok(()) + } }, SocketOptionName::TCP(_) => Err(Errno::EOPNOTSUPP), } From 77b7b11d299b3bbb0fae9e7020ea481d6ca060f0 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 22 Jul 2026 12:05:56 -0700 Subject: [PATCH 114/319] Implement Windows WNF state lifecycle syscalls (#1063) This PR adds a shim-global WNF state store and wires query, create, update, delete-data, delete-name, and state-name-information syscalls. Explicit SID scopes, DACL enforcement, privileged lifetimes, temporary-name cleanup, and subscriber notifications remain deferred. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 118 +++- litebox_shim_windows/src/syscalls/mod.rs | 83 +++ .../src/syscalls/object_manager.rs | 26 +- litebox_shim_windows/src/syscalls/wnf.rs | 559 ++++++++++++++++++ litebox_shim_windows/src/tests.rs | 171 ------ 5 files changed, 763 insertions(+), 194 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/wnf.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 49306bf8e0..f7de8819d0 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -247,6 +247,26 @@ where .ok_or(NtStatus::ACCESS_VIOLATION) } +pub(crate) fn probe_guest_output_buffer( + buffer: MutPtr, + buffer_length: usize, +) -> Result<(), NtStatus> +where + Platform: RawPointerProvider, +{ + if buffer_length == 0 { + return Ok(()); + } + probe_guest_output_preserving_value::(buffer)?; + let last_offset = isize::try_from(buffer_length - 1).map_err(|_| NtStatus::ACCESS_VIOLATION)?; + let value = buffer + .read_at_offset(last_offset) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + buffer + .write_at_offset(last_offset, value) + .ok_or(NtStatus::ACCESS_VIOLATION) +} + fn set_guest_teb(platform: &Platform, teb_address: usize) -> bool where Platform: PunchthroughProvider + RawPointerProvider, @@ -391,6 +411,9 @@ impl WindowsShimBuilder { platform: self.platform, page_manager: PageManager::new(&self.litebox), registry: syscalls::registry::RegistryStore::new(&self.litebox), + wnf_states: syscalls::wnf::WnfStateStore::new( + syscalls::wnf::WnfStateStoreData::default(), + ), qpc_boot_instant: TimeProvider::now(self.platform), litebox: self.litebox, _fs: PhantomData, @@ -502,6 +525,7 @@ struct GlobalState { platform: &'static Platform, page_manager: WindowsPageManager, registry: syscalls::registry::RegistryStore, + wnf_states: syscalls::wnf::WnfStateStore, qpc_boot_instant: ::Instant, litebox: LiteBox, _fs: PhantomData, @@ -516,7 +540,10 @@ pub struct Process { object_manager: WindowsObjectManager, section_views: WindowsSectionViews, // TODO: move this into `GlobalState` once we have a proper shared mapping implementation. - #[expect(dead_code)] + #[expect( + dead_code, + reason = "keeps alive the section registered weakly in the object namespace" + )] windows_shared_section: Arc>, nls_section_mappings: WindowsNlsSectionMappings, virtual_allocations: WindowsVirtualAllocations, @@ -1408,6 +1435,95 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtQueryWnfStateData { + state_name, + type_id, + explicit_scope, + change_stamp, + buffer, + buffer_size, + } => { + let status = self.sys_nt_query_wnf_state_data( + state_name, + type_id, + explicit_scope, + change_stamp, + buffer, + buffer_size, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtCreateWnfStateName { + state_name, + name_lifetime, + data_scope, + persist_data, + type_id, + maximum_state_size, + security_descriptor, + } => { + let status = self.sys_nt_create_wnf_state_name( + syscalls::wnf::WnfCreateStateNameParameters { + state_name, + name_lifetime, + data_scope, + persist_data, + type_id, + maximum_state_size, + security_descriptor, + }, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtUpdateWnfStateData { + state_name, + buffer, + buffer_size, + type_id, + explicit_scope, + matching_change_stamp, + check_stamp, + } => { + let status = self.sys_nt_update_wnf_state_data( + syscalls::wnf::WnfUpdateStateDataParameters { + state_name, + buffer, + buffer_size, + type_id, + explicit_scope, + matching_change_stamp, + check_stamp, + }, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtDeleteWnfStateData { + state_name, + explicit_scope, + } => { + let status = self.sys_nt_delete_wnf_state_data(state_name, explicit_scope); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtDeleteWnfStateName { state_name } => { + let status = self.sys_nt_delete_wnf_state_name(state_name); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryWnfStateNameInformation { + state_name, + name_information_class, + explicit_scope, + buffer, + buffer_size, + } => { + let status = self.sys_nt_query_wnf_state_name_information( + state_name, + name_information_class, + explicit_scope, + buffer, + buffer_size, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtQuerySection { section_handle, section_information_class, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index a2bf811de6..72a52e1f45 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -19,6 +19,7 @@ pub(crate) mod sysinfo; pub(crate) mod thread; pub(crate) mod timer; pub(crate) mod wait_completion_packet; +pub(crate) mod wnf; pub(crate) mod worker_factory; use litebox::platform::{RawConstPointer as _, RawPointerProvider}; @@ -414,6 +415,46 @@ pub(crate) enum SyscallRequest { system_information_length: u32, return_length: Option>, }, + NtQueryWnfStateData { + state_name: Platform::RawConstPointer, + type_id: Option>, + explicit_scope: Option>, + change_stamp: Platform::RawMutPointer, + buffer: Platform::RawMutPointer, + buffer_size: Platform::RawMutPointer, + }, + NtCreateWnfStateName { + state_name: Platform::RawMutPointer, + name_lifetime: u32, + data_scope: u32, + persist_data: u8, + type_id: Option>, + maximum_state_size: u32, + security_descriptor: Platform::RawConstPointer, + }, + NtUpdateWnfStateData { + state_name: Platform::RawConstPointer, + buffer: Option>, + buffer_size: u32, + type_id: Option>, + explicit_scope: Option>, + matching_change_stamp: u32, + check_stamp: i32, + }, + NtDeleteWnfStateData { + state_name: Platform::RawConstPointer, + explicit_scope: Option>, + }, + NtDeleteWnfStateName { + state_name: Platform::RawConstPointer, + }, + NtQueryWnfStateNameInformation { + state_name: Platform::RawConstPointer, + name_information_class: u32, + explicit_scope: Option>, + buffer: Platform::RawMutPointer, + buffer_size: u32, + }, NtQuerySection { section_handle: Handle, section_information_class: u32, @@ -858,6 +899,48 @@ impl SyscallRequest { system_information_length, return_length:*, })), + NtSysno::NtQueryWnfStateData => Some(sys_req!(NtQueryWnfStateData { + state_name:*, + type_id:*, + explicit_scope:*, + change_stamp:*, + buffer:*, + buffer_size:*, + })), + NtSysno::NtCreateWnfStateName => Some(sys_req!(NtCreateWnfStateName { + state_name:*, + name_lifetime, + data_scope, + persist_data, + type_id:*, + maximum_state_size, + security_descriptor:*, + })), + NtSysno::NtUpdateWnfStateData => Some(sys_req!(NtUpdateWnfStateData { + state_name:*, + buffer:*, + buffer_size, + type_id:*, + explicit_scope:*, + matching_change_stamp, + check_stamp, + })), + NtSysno::NtDeleteWnfStateData => Some(sys_req!(NtDeleteWnfStateData { + state_name:*, + explicit_scope:*, + })), + NtSysno::NtDeleteWnfStateName => Some(sys_req!(NtDeleteWnfStateName { + state_name:*, + })), + NtSysno::NtQueryWnfStateNameInformation => { + Some(sys_req!(NtQueryWnfStateNameInformation { + state_name:*, + name_information_class, + explicit_scope:*, + buffer:*, + buffer_size, + })) + } NtSysno::NtQuerySection => Some(sys_req!(NtQuerySection { section_handle: { Handle::from_raw }, section_information_class, diff --git a/litebox_shim_windows/src/syscalls/object_manager.rs b/litebox_shim_windows/src/syscalls/object_manager.rs index 5e79356db8..14a454c09c 100644 --- a/litebox_shim_windows/src/syscalls/object_manager.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -25,7 +25,9 @@ use crate::syscalls::event::EventObject; use crate::syscalls::section::{ SectionObject, WINDOWS_SESSION_SHARED_SECTION_OBJECT, WINDOWS_SHARED_SECTION_OBJECT, }; -use crate::{ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_preserving_value}; +use crate::{ + ConstPtr, MutPtr, ShimFS, Task, probe_guest_output_buffer, probe_guest_output_preserving_value, +}; const MAX_SYMLINK_REPARSE_DEPTH: usize = 64; pub(crate) const WINDOWS_API_PORT: &str = r"\Windows\ApiPort"; @@ -964,26 +966,6 @@ fn write_directory_records( Ok(()) } -fn probe_output_buffer( - buffer: MutPtr, - buffer_length: usize, -) -> Result<(), NtStatus> { - if buffer_length == 0 { - return Ok(()); - } - let value = buffer.read_at_offset(0).ok_or(NtStatus::ACCESS_VIOLATION)?; - buffer - .write_at_offset(0, value) - .ok_or(NtStatus::ACCESS_VIOLATION)?; - let last_offset = byte_offset(buffer_length - 1)?; - let value = buffer - .read_at_offset(last_offset) - .ok_or(NtStatus::ACCESS_VIOLATION)?; - buffer - .write_at_offset(last_offset, value) - .ok_or(NtStatus::ACCESS_VIOLATION) -} - impl Task { fn directory_entry( &self, @@ -1233,7 +1215,7 @@ impl Task { Err(status) => return status, }; let buffer_length = params.buffer_length as usize; - if let Err(status) = probe_output_buffer::(params.buffer, buffer_length) { + if let Err(status) = probe_guest_output_buffer::(params.buffer, buffer_length) { return status; } diff --git a/litebox_shim_windows/src/syscalls/wnf.rs b/litebox_shim_windows/src/syscalls/wnf.rs new file mode 100644 index 0000000000..8b4f96db4c --- /dev/null +++ b/litebox_shim_windows/src/syscalls/wnf.rs @@ -0,0 +1,559 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use int_enum::IntEnum; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::utils::TruncateExt as _; +use litebox_common_windows::nt_status::NtStatus; + +use crate::nt_types::Guid; +use crate::{ + ConstPtr, MutPtr, ShimFS, ShimPlatform, Task, probe_guest_output_buffer, + probe_guest_output_preserving_value, +}; + +const MAXIMUM_STATE_SIZE: u32 = 0x1000; +const STATE_NAME_XOR_KEY: u64 = 0x41c6_4e6d_a3bc_0074; +const MAXIMUM_UNIQUE_ID: u32 = 0x001f_ffff; +const STATE_NAME_INFORMATION_SIZE: u32 = 4; +const INITIAL_CHANGE_STAMP: u32 = 0; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum WnfStateNameLifetime { + WellKnown = 0, + Permanent = 1, + Persistent = 2, + Temporary = 3, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum WnfDataScope { + System = 0, + Session = 1, + User = 2, + Process = 3, + Machine = 4, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum WnfStateNameInformation { + Exists = 0, + SubscribersPresent = 1, + IsQuiescent = 2, +} + +#[derive(Clone)] +pub(crate) struct WnfStateData { + change_stamp: u32, + type_id: Option, + data: Vec, + maximum_state_size: u32, + lifetime: WnfStateNameLifetime, +} + +#[derive(Default)] +pub(crate) struct WnfStateStoreData { + next_unique_id: u32, + states: BTreeMap, +} + +pub(crate) type WnfStateStore = litebox::sync::RwLock; + +pub(crate) struct WnfCreateStateNameParameters { + pub(crate) state_name: MutPtr, + pub(crate) name_lifetime: u32, + pub(crate) data_scope: u32, + pub(crate) persist_data: u8, + pub(crate) type_id: Option>, + pub(crate) maximum_state_size: u32, + pub(crate) security_descriptor: ConstPtr, +} + +pub(crate) struct WnfUpdateStateDataParameters { + pub(crate) state_name: ConstPtr, + pub(crate) buffer: Option>, + pub(crate) buffer_size: u32, + pub(crate) type_id: Option>, + pub(crate) explicit_scope: Option>, + pub(crate) matching_change_stamp: u32, + pub(crate) check_stamp: i32, +} + +impl Task { + pub(crate) fn sys_nt_create_wnf_state_name( + &self, + params: WnfCreateStateNameParameters, + ) -> NtStatus { + if probe_guest_output_preserving_value::(params.state_name).is_err() { + return NtStatus::ACCESS_VIOLATION; + } + let type_id = match read_type_id::(params.type_id) { + Ok(type_id) => type_id, + Err(status) => return status, + }; + if params.security_descriptor.read_at_offset(0).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + let Ok(lifetime) = WnfStateNameLifetime::try_from(params.name_lifetime) else { + return NtStatus::INVALID_PARAMETER; + }; + let Ok(data_scope) = WnfDataScope::try_from(params.data_scope) else { + return NtStatus::INVALID_PARAMETER; + }; + if params.maximum_state_size > MAXIMUM_STATE_SIZE { + return NtStatus::INVALID_PARAMETER; + } + match lifetime { + WnfStateNameLifetime::WellKnown => return NtStatus::INVALID_PARAMETER, + WnfStateNameLifetime::Permanent | WnfStateNameLifetime::Persistent => { + // TODO(wnf-create-privilege): Allow privileged lifetimes once guest token + // privileges are modeled. + return NtStatus::PRIVILEGE_NOT_HELD; + } + WnfStateNameLifetime::Temporary => {} + } + if data_scope == WnfDataScope::Process || params.persist_data != 0 { + return NtStatus::INVALID_PARAMETER; + } + + // TODO(wnf-security-descriptor): Enforce the supplied DACL once guest tokens and WNF + // access checks are modeled. + let state_name = { + let mut store = self.global.wnf_states.write(); + let Some(unique_id) = store.next_unique_id.checked_add(1) else { + return NtStatus::NO_MEMORY; + }; + if unique_id > MAXIMUM_UNIQUE_ID { + return NtStatus::NO_MEMORY; + } + store.next_unique_id = unique_id; + let state_name = encode_state_name(lifetime, data_scope, false, unique_id); + let state = WnfStateData { + change_stamp: INITIAL_CHANGE_STAMP, + type_id, + data: Vec::new(), + maximum_state_size: params.maximum_state_size, + lifetime, + }; + // TODO(wnf-temporary-lifetime): Remove temporary names when their creating guest + // process exits once process lifecycle is modeled. + store.states.insert(state_name, state); + state_name + }; + if params.state_name.write_at_offset(0, state_name).is_none() { + let mut store = self.global.wnf_states.write(); + if store + .states + .get(&state_name) + .is_some_and(|current| current.change_stamp == INITIAL_CHANGE_STAMP) + { + store.states.remove(&state_name); + } + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_update_wnf_state_data( + &self, + params: WnfUpdateStateDataParameters, + ) -> NtStatus { + let Some(state_name) = params.state_name.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let type_id = match read_type_id::(params.type_id) { + Ok(type_id) => type_id, + Err(status) => return status, + }; + if params.explicit_scope.is_some() { + // TODO(wnf-explicit-scope): Key state data by the explicit SID once scoped WNF + // state access is modeled. + return NtStatus::INVALID_PARAMETER; + } + let data = if params.buffer_size == 0 { + Vec::new() + } else { + let Some(buffer) = params.buffer else { + return NtStatus::ACCESS_VIOLATION; + }; + let Some(data) = buffer.to_owned_slice(params.buffer_size as usize) else { + return NtStatus::ACCESS_VIOLATION; + }; + Vec::from(data) + }; + + let mut store = self.global.wnf_states.write(); + let Some(state) = store.states.get_mut(&state_name) else { + return NtStatus::OBJECT_NAME_NOT_FOUND; + }; + if !type_id_matches(state.type_id, type_id) || params.buffer_size > state.maximum_state_size + { + return NtStatus::INVALID_PARAMETER; + } + if params.check_stamp != 0 && params.matching_change_stamp != state.change_stamp { + return NtStatus::UNSUCCESSFUL; + } + state.change_stamp = state.change_stamp.wrapping_add(1); + state.data = data; + // TODO(wnf-notify): Deliver successful updates to subscribers when WNF subscriptions are + // modeled. + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_delete_wnf_state_data( + &self, + state_name: ConstPtr, + explicit_scope: Option>, + ) -> NtStatus { + let Some(state_name) = state_name.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + if explicit_scope.is_some() { + // TODO(wnf-explicit-scope): Delete only the selected SID-scoped data instance once + // scoped WNF state access is modeled. + return NtStatus::INVALID_PARAMETER; + } + let mut store = self.global.wnf_states.write(); + let Some(state) = store.states.get_mut(&state_name) else { + return NtStatus::OBJECT_NAME_NOT_FOUND; + }; + state.change_stamp = 0; + state.data.clear(); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_delete_wnf_state_name( + &self, + state_name: ConstPtr, + ) -> NtStatus { + let Some(state_name) = state_name.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let mut store = self.global.wnf_states.write(); + let Some(state) = store.states.get(&state_name) else { + return NtStatus::OBJECT_NAME_NOT_FOUND; + }; + if state.lifetime == WnfStateNameLifetime::WellKnown { + return NtStatus::INVALID_PARAMETER; + } + store.states.remove(&state_name); + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_query_wnf_state_name_information( + &self, + state_name: ConstPtr, + name_information_class: u32, + explicit_scope: Option>, + buffer: MutPtr, + buffer_size: u32, + ) -> NtStatus { + let Some(state_name) = state_name.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let Ok(information_class) = WnfStateNameInformation::try_from(name_information_class) + else { + return NtStatus::INVALID_INFO_CLASS; + }; + if buffer_size != STATE_NAME_INFORMATION_SIZE { + return NtStatus::INVALID_PARAMETER; + } + if explicit_scope.is_some() { + // TODO(wnf-explicit-scope): Resolve the selected SID-scoped state instance once scoped + // WNF state access is modeled. + return NtStatus::INVALID_PARAMETER; + } + if probe_guest_output_preserving_value::(buffer).is_err() { + return NtStatus::ACCESS_VIOLATION; + } + + let store = self.global.wnf_states.read(); + let exists = store.states.contains_key(&state_name); + let value = match information_class { + WnfStateNameInformation::Exists => u32::from(exists), + WnfStateNameInformation::SubscribersPresent => { + if !exists { + return NtStatus::OBJECT_NAME_NOT_FOUND; + } + // TODO(wnf-notify): Report registered subscribers once WNF subscriptions are + // modeled. + 0 + } + WnfStateNameInformation::IsQuiescent => { + if !exists { + return NtStatus::OBJECT_NAME_NOT_FOUND; + } + 1 + } + }; + buffer + .write_at_offset(0, value) + .map_or(NtStatus::ACCESS_VIOLATION, |()| NtStatus::SUCCESS) + } + + pub(crate) fn sys_nt_query_wnf_state_data( + &self, + state_name: ConstPtr, + type_id: Option>, + explicit_scope: Option>, + change_stamp: MutPtr, + buffer: MutPtr, + buffer_size: MutPtr, + ) -> NtStatus { + let Some(state_name) = state_name.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let type_id = match type_id { + Some(type_id) => match type_id.read_at_offset(0) { + Some(type_id) => Some(type_id), + None => return NtStatus::ACCESS_VIOLATION, + }, + None => None, + }; + let Some(available_size) = buffer_size.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + let outputs_valid = probe_guest_output_preserving_value::(change_stamp) + .is_ok() + && probe_guest_output_preserving_value::(buffer_size).is_ok() + && probe_guest_output_buffer::(buffer, available_size as usize).is_ok(); + if !outputs_valid { + return NtStatus::ACCESS_VIOLATION; + } + if explicit_scope.is_some() { + // TODO(wnf-explicit-scope): Key state data by the explicit SID once scoped WNF state + // creation and security checks are modeled. + litebox_util_log::debug!( + state_name:% = format_args!("{state_name:#x}"); + "Explicit-scope WNF state queries are not supported" + ); + return NtStatus::INVALID_PARAMETER; + } + + let state = { + let store = self.global.wnf_states.read(); + store.states.get(&state_name).cloned() + }; + let Some(state) = state else { + return NtStatus::OBJECT_NAME_NOT_FOUND; + }; + if !type_id_matches(state.type_id, type_id) { + return NtStatus::INVALID_PARAMETER; + } + + let required_size = state.data.len().trunc(); + let status = if available_size < required_size { + NtStatus::BUFFER_TOO_SMALL + } else { + if !state.data.is_empty() && buffer.write_slice_at_offset(0, &state.data).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + }; + if change_stamp + .write_at_offset(0, state.change_stamp) + .is_none() + || buffer_size.write_at_offset(0, required_size).is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + status + } +} + +fn read_type_id( + type_id: Option>, +) -> Result, NtStatus> { + type_id + .map(|type_id| type_id.read_at_offset(0).ok_or(NtStatus::ACCESS_VIOLATION)) + .transpose() +} + +fn type_id_matches(expected: Option, supplied: Option) -> bool { + expected.is_none() + || matches!((expected, supplied), (Some(expected), Some(supplied)) if expected.data == supplied.data) +} + +fn encode_state_name( + lifetime: WnfStateNameLifetime, + data_scope: WnfDataScope, + persist_data: bool, + unique_id: u32, +) -> u64 { + let clear = 1 + | ((lifetime as u64) << 4) + | ((data_scope as u64) << 6) + | (u64::from(persist_data) << 10) + | (u64::from(unique_id) << 11); + clear ^ STATE_NAME_XOR_KEY +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{TestFS, TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task}; + + const SECURITY_DESCRIPTOR_REVISION: u8 = 1; + + fn create_state( + task: &Task, + type_id: Option, + maximum_state_size: u32, + ) -> u64 { + let mut state_name = 0; + assert_eq!( + task.sys_nt_create_wnf_state_name(WnfCreateStateNameParameters { + state_name: mut_ptr(&mut state_name), + name_lifetime: WnfStateNameLifetime::Temporary as u32, + data_scope: WnfDataScope::Machine as u32, + persist_data: 0, + type_id: type_id.as_ref().map(const_ptr), + maximum_state_size, + security_descriptor: const_ptr(&SECURITY_DESCRIPTOR_REVISION), + }), + NtStatus::SUCCESS + ); + state_name + } + + fn update_state( + task: &Task, + state_name: u64, + data: &[u8], + type_id: Option<&Guid>, + matching_change_stamp: u32, + check_stamp: i32, + ) -> NtStatus { + task.sys_nt_update_wnf_state_data(WnfUpdateStateDataParameters { + state_name: const_ptr(&state_name), + buffer: data.first().map(const_ptr), + buffer_size: u32::try_from(data.len()).expect("test payload length fits in u32"), + type_id: type_id.map(const_ptr), + explicit_scope: None, + matching_change_stamp, + check_stamp, + }) + } + + #[test] + fn delete_data_resets_state_and_delete_name_removes_it() { + let task = test_task(); + let state_name = create_state(&task, None, 4); + assert_eq!( + update_state(&task, state_name, &[1, 2], None, 0, 0), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_delete_wnf_state_data(const_ptr(&state_name), None), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_delete_wnf_state_data(const_ptr(&state_name), None), + NtStatus::SUCCESS + ); + + let mut change_stamp = 99; + let mut buffer = [0xaau8; 2]; + let mut buffer_size = 2; + assert_eq!( + task.sys_nt_query_wnf_state_data( + const_ptr(&state_name), + None, + None, + mut_ptr(&mut change_stamp), + mut_byte_ptr(&mut buffer), + mut_ptr(&mut buffer_size), + ), + NtStatus::SUCCESS + ); + assert_eq!(change_stamp, 0); + assert_eq!(buffer_size, 0); + assert_eq!(buffer, [0xaa; 2]); + + assert_eq!( + task.sys_nt_delete_wnf_state_name(const_ptr(&state_name)), + NtStatus::SUCCESS + ); + assert_eq!( + task.sys_nt_delete_wnf_state_name(const_ptr(&state_name)), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!( + task.sys_nt_query_wnf_state_data( + const_ptr(&state_name), + None, + None, + mut_ptr(&mut change_stamp), + mut_byte_ptr(&mut buffer), + mut_ptr(&mut buffer_size), + ), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + } + + #[test] + fn state_name_information_reports_native_boolean_contract() { + let task = test_task(); + let state_name = create_state(&task, None, 4); + for (class, expected) in [ + (WnfStateNameInformation::Exists, 1), + (WnfStateNameInformation::SubscribersPresent, 0), + (WnfStateNameInformation::IsQuiescent, 1), + ] { + let mut value = u32::MAX; + assert_eq!( + task.sys_nt_query_wnf_state_name_information( + const_ptr(&state_name), + class as u32, + None, + mut_ptr(&mut value), + STATE_NAME_INFORMATION_SIZE, + ), + NtStatus::SUCCESS + ); + assert_eq!(value, expected); + } + + assert_eq!( + task.sys_nt_delete_wnf_state_name(const_ptr(&state_name)), + NtStatus::SUCCESS + ); + let mut value = u32::MAX; + assert_eq!( + task.sys_nt_query_wnf_state_name_information( + const_ptr(&state_name), + WnfStateNameInformation::Exists as u32, + None, + mut_ptr(&mut value), + STATE_NAME_INFORMATION_SIZE, + ), + NtStatus::SUCCESS + ); + assert_eq!(value, 0); + assert_eq!( + task.sys_nt_query_wnf_state_name_information( + const_ptr(&state_name), + WnfStateNameInformation::SubscribersPresent as u32, + None, + mut_ptr(&mut value), + STATE_NAME_INFORMATION_SIZE, + ), + NtStatus::OBJECT_NAME_NOT_FOUND + ); + assert_eq!( + task.sys_nt_query_wnf_state_name_information( + const_ptr(&state_name), + 3, + None, + mut_ptr(&mut value), + STATE_NAME_INFORMATION_SIZE, + ), + NtStatus::INVALID_INFO_CLASS + ); + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 4c8a42eddf..53f2ad296f 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -174,8 +174,6 @@ const EVENT_MODIFY_STATE: u32 = 0x0002; const SYNCHRONIZE: u32 = 0x0010_0000; const DUPLICATE_CLOSE_SOURCE: u32 = 0x0000_0001; const DUPLICATE_SAME_ACCESS: u32 = 0x0000_0002; -const DUPLICATE_SAME_ATTRIBUTES: u32 = 0x0000_0004; -const MAXIMUM_ALLOWED: u32 = 0x0200_0000; fn create_event(task: &Task, desired_access: u32) -> Handle { let mut handle = Handle::default(); @@ -284,175 +282,6 @@ fn nt_duplicate_object_closes_source_even_when_duplication_fails() { assert!(duplicate.is_null()); } -#[test] -fn nt_duplicate_object_supports_close_only_calls() { - let task = test_task(); - let source = create_event(&task, EVENT_MODIFY_STATE); - - assert_eq!( - task.sys_nt_duplicate_object( - crate::syscalls::ProcessHandle::CURRENT, - source, - crate::syscalls::ProcessHandle::from_raw(0), - None, - 0, - 0, - 0, - ), - litebox_common_windows::nt_status::NtStatus::INVALID_PARAMETER - ); - assert_eq!( - task.sys_nt_set_event(source, None), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_duplicate_object( - crate::syscalls::ProcessHandle::CURRENT, - source, - crate::syscalls::ProcessHandle::from_raw(0), - None, - 0, - 0, - DUPLICATE_CLOSE_SOURCE, - ), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_close(source), - litebox_common_windows::nt_status::NtStatus::INVALID_HANDLE - ); -} - -#[test] -fn nt_duplicate_object_null_output_retains_inaccessible_duplicate() { - let task = test_task(); - let source = create_event(&task, EVENT_MODIFY_STATE); - let handles_before = task.process.handles.read().iter_alive().count(); - - assert_eq!( - task.sys_nt_duplicate_object( - crate::syscalls::ProcessHandle::CURRENT, - source, - crate::syscalls::ProcessHandle::CURRENT, - None, - 0, - 0, - DUPLICATE_SAME_ACCESS, - ), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.process.handles.read().iter_alive().count(), - handles_before + 1 - ); - assert_eq!( - task.sys_nt_close(source), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.process.handles.read().iter_alive().count(), - handles_before - ); -} - -#[test] -fn nt_duplicate_object_ignores_unknown_flags_and_copies_attributes() { - let task = test_task(); - let source = create_event(&task, EVENT_MODIFY_STATE); - let mut first_duplicate = Handle::default(); - let mut second_duplicate = Handle::default(); - let mut unprotected_duplicate = Handle::default(); - - assert_eq!( - task.sys_nt_duplicate_object( - crate::syscalls::ProcessHandle::CURRENT, - source, - crate::syscalls::ProcessHandle::CURRENT, - Some(mut_ptr(&mut first_duplicate)), - 0, - 0x8000_0001, - DUPLICATE_SAME_ACCESS | 0x8000_0000, - ), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_duplicate_object( - crate::syscalls::ProcessHandle::CURRENT, - first_duplicate, - crate::syscalls::ProcessHandle::CURRENT, - Some(mut_ptr(&mut second_duplicate)), - 0, - 0x4000_0000, - DUPLICATE_SAME_ACCESS | DUPLICATE_SAME_ATTRIBUTES, - ), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_duplicate_object( - crate::syscalls::ProcessHandle::CURRENT, - first_duplicate, - crate::syscalls::ProcessHandle::CURRENT, - Some(mut_ptr(&mut unprotected_duplicate)), - 0, - 0, - DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS, - ), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_close(source), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_close(first_duplicate), - litebox_common_windows::nt_status::NtStatus::HANDLE_NOT_CLOSABLE - ); - assert_eq!( - task.sys_nt_close(second_duplicate), - litebox_common_windows::nt_status::NtStatus::HANDLE_NOT_CLOSABLE - ); - assert_eq!( - task.sys_nt_close(unprotected_duplicate), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); -} - -#[test] -fn nt_duplicate_object_grants_maximum_allowed_access() { - let task = test_task(); - let source = create_event(&task, SYNCHRONIZE); - let mut duplicate = Handle::default(); - - assert_eq!( - task.sys_nt_duplicate_object( - crate::syscalls::ProcessHandle::CURRENT, - source, - crate::syscalls::ProcessHandle::CURRENT, - Some(mut_ptr(&mut duplicate)), - MAXIMUM_ALLOWED, - 0, - 0, - ), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_set_event(source, None), - litebox_common_windows::nt_status::NtStatus::ACCESS_DENIED - ); - assert_eq!( - task.sys_nt_set_event(duplicate, None), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_close(source), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); - assert_eq!( - task.sys_nt_close(duplicate), - litebox_common_windows::nt_status::NtStatus::SUCCESS - ); -} - #[cfg(target_os = "windows")] #[test] fn host_nt_duplicate_object_failure_and_access_matrix() { From b5cf9574096d9c4b5664acb6bf41508ea6f3201b Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 22 Jul 2026 13:13:28 -0700 Subject: [PATCH 115/319] Multiplex broker control calls (#1061) Allows multiple local threads to issue broker calls concurrently while preserving serial host execution. It unifies local setup and active operations under a phase-aware `LocalControlChannel`, bounds each Unix association to 64 published calls, serializes complete request frames, and uses one response dispatcher to correlate out-of-order replies by request ID and wake only the matching caller. Offset-zero shared-memory payload transfers remain serialized, while fatal control or notification failures cross-cancel the association, wake all pending callers, and fail broker-backed pollables closed. It also prevents Unix control activation before successful negotiation. --- litebox/src/broker/mod.rs | 38 +- litebox/src/event/counter.rs | 47 +- litebox/src/litebox.rs | 2 +- litebox/src/pipes.rs | 29 +- litebox_broker_local/src/event.rs | 8 +- litebox_broker_local/src/lib.rs | 223 ++-- litebox_broker_local/src/pipe.rs | 85 +- litebox_broker_protocol/src/channel.rs | 19 +- litebox_broker_transport/src/unix_socket.rs | 1075 ++++++++++++++++- .../tests/notification_runtime.rs | 3 +- .../tests/userland_broker.rs | 3 +- litebox_runner_linux_userland/src/broker.rs | 307 ++++- litebox_runner_linux_userland/src/lib.rs | 7 +- 13 files changed, 1585 insertions(+), 261 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 96a603fbf8..cbd0677413 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -143,49 +143,42 @@ impl BrokerPollableRegistry { pub(crate) struct BrokerLocalControl< Platform: RawSyncPrimitivesProvider, - Channel: LocalControlChannel + Send, + Channel: LocalControlChannel + Send + Sync, > { - local: Mutex>>, + local: Mutex>>>, pollable_registry: Arc>, } impl BrokerLocalControl where Platform: RawSyncPrimitivesProvider + TimeProvider, - Channel: LocalControlChannel + Send, + Channel: LocalControlChannel + Send + Sync, { pub(crate) fn new( local: BrokerLocal, pollable_registry: Arc>, ) -> Self { Self { - local: Mutex::new(Some(local)), + local: Mutex::new(Some(Arc::new(local))), pollable_registry, } } fn request( &self, - request: impl FnOnce( - &mut BrokerLocal, - ) -> litebox_broker_local::Result, + request: impl FnOnce(&BrokerLocal) -> litebox_broker_local::Result, ) -> core::result::Result { - let (result, failed_connection) = { - let mut local = self.local.lock(); - let Some(connection) = local.as_mut() else { + let connection = { + let local = self.local.lock(); + let Some(connection) = local.as_ref() else { return Err(BrokerControlError::AssociationFailed); }; - let result = request(connection).map_err(BrokerControlError::from); - let failed_connection = - if matches!(result.as_ref(), Err(BrokerControlError::AssociationFailed)) { - local.take() - } else { - None - }; - (result, failed_connection) + Arc::clone(connection) }; - if let Some(connection) = failed_connection { - drop(connection); + let result = request(&connection).map_err(BrokerControlError::from); + if matches!(result.as_ref(), Err(BrokerControlError::AssociationFailed)) + && self.local.lock().take().is_some() + { self.pollable_registry.notify_all(Events::ERR); } result @@ -195,7 +188,7 @@ where impl BrokerControl for BrokerLocalControl where Platform: RawSyncPrimitivesProvider + TimeProvider, - Channel: LocalControlChannel + Send, + Channel: LocalControlChannel + Send + Sync, { fn create_event_with_count( &self, @@ -257,8 +250,7 @@ where fn fail_connection(&self) { let connection = self.local.lock().take(); - if let Some(connection) = connection { - drop(connection); + if connection.is_some() { self.pollable_registry.notify_all(Events::ERR); } } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index de9f4760d9..f0da3fc514 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -174,7 +174,7 @@ where mod tests { extern crate std; - use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; @@ -204,12 +204,11 @@ mod tests { let request_count = Arc::new(AtomicUsize::new(0)); let local = BrokerLocal::negotiate( FakeLocalControlChannel { - next_handle: handle.0, + next_handle: AtomicU64::new(handle.0), consume_attempts: consume_attempts.clone(), read_ready: read_ready.clone(), request_count, fail_requests: Arc::new(AtomicBool::new(false)), - last_request: None, }, |_| Ok(Arc::new(NoopSharedMemory)), ) @@ -263,12 +262,11 @@ mod tests { let request_count = Arc::new(AtomicUsize::new(0)); let local = BrokerLocal::negotiate( FakeLocalControlChannel { - next_handle: handle.0, + next_handle: AtomicU64::new(handle.0), consume_attempts: Arc::clone(&consume_attempts), read_ready: Arc::new(AtomicBool::new(false)), request_count: Arc::clone(&request_count), fail_requests: Arc::new(AtomicBool::new(false)), - last_request: None, }, |_| Ok(Arc::new(NoopSharedMemory)), ) @@ -315,12 +313,11 @@ mod tests { let fail_requests = Arc::new(AtomicBool::new(false)); let local = BrokerLocal::negotiate( FakeLocalControlChannel { - next_handle: handle.0, + next_handle: AtomicU64::new(handle.0), consume_attempts: Arc::new(AtomicUsize::new(0)), read_ready: Arc::new(AtomicBool::new(false)), request_count: Arc::clone(&request_count), fail_requests: Arc::clone(&fail_requests), - last_request: None, }, |_| Ok(Arc::new(NoopSharedMemory)), ) @@ -352,12 +349,11 @@ mod tests { let request_count = Arc::new(AtomicUsize::new(0)); let local = BrokerLocal::negotiate( FakeLocalControlChannel { - next_handle: handle.0, + next_handle: AtomicU64::new(handle.0), consume_attempts: Arc::new(AtomicUsize::new(0)), read_ready: Arc::new(AtomicBool::new(false)), request_count: Arc::clone(&request_count), fail_requests: Arc::new(AtomicBool::new(false)), - last_request: None, }, |_| Ok(Arc::new(NoopSharedMemory)), ) @@ -405,12 +401,11 @@ mod tests { } struct FakeLocalControlChannel { - next_handle: u64, + next_handle: AtomicU64, consume_attempts: Arc, read_ready: Arc, request_count: Arc, fail_requests: Arc, - last_request: Option, } struct NoopSharedMemory; @@ -457,26 +452,17 @@ mod tests { broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, })) } - - fn send_request( - &mut self, - request: &BrokerRequest, - ) -> core::result::Result<(), Self::Error> { - self.last_request = Some(request.clone()); + fn call( + &self, + request: BrokerRequest, + ) -> core::result::Result { self.request_count.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - - fn recv_response(&mut self) -> core::result::Result, Self::Error> { if self.fail_requests.load(Ordering::SeqCst) { - self.last_request.take(); return Err(()); } - let request = self.last_request.take().unwrap(); let result = match request.operation { BrokerOperation::Event(EventRequest::Create(_)) => { - let handle = ObjectHandle(self.next_handle); - self.next_handle += 1; + let handle = ObjectHandle(self.next_handle.fetch_add(1, Ordering::SeqCst)); BrokerResult::Event(EventResponse::Create(CreateEventResponse { handle })) } BrokerOperation::Event(EventRequest::Consume(_)) => { @@ -498,10 +484,17 @@ mod tests { panic!("unexpected broker request: {request:?}") } }; - Ok(Some(BrokerResponse { + Ok(BrokerResponse { request_id: request.request_id, result, - })) + }) + } + + fn with_serialized_payload( + &self, + transfer: impl FnOnce() -> T, + ) -> core::result::Result { + Ok(transfer()) } } } diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 36838a8db7..c34f4a120e 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -50,7 +50,7 @@ impl LiteBox { ) -> Self where Platform: TimeProvider, - Channel: LocalControlChannel + Send + 'static, + Channel: LocalControlChannel + Send + Sync + 'static, { let broker_pollables = Arc::new(broker::BrokerPollableRegistry::new()); let broker_control = Arc::new(broker::BrokerLocalControl::::new( diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index 35972ea931..bd21130ed6 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -952,7 +952,6 @@ mod tests { let force_transport = Arc::new(AtomicBool::new(false)); let local = BrokerLocal::negotiate( FailingPipeChannel { - last_request: None, request_count: Arc::clone(&request_count), read_failure: ReadFailure::Transport, force_transport, @@ -999,7 +998,6 @@ mod tests { let force_transport = Arc::new(AtomicBool::new(false)); let local = BrokerLocal::negotiate( FailingPipeChannel { - last_request: None, request_count: Arc::clone(&request_count), read_failure: ReadFailure::WouldBlock, force_transport: Arc::clone(&force_transport), @@ -1111,7 +1109,6 @@ mod tests { #[derive(Debug)] struct FailingPipeChannel { - last_request: Option, request_count: Arc, read_failure: ReadFailure, force_transport: Arc, @@ -1168,18 +1165,11 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION, })) } - - fn send_request( - &mut self, - request: &BrokerRequest, - ) -> core::result::Result<(), Self::Error> { - self.last_request = Some(request.clone()); + fn call( + &self, + request: BrokerRequest, + ) -> core::result::Result { self.request_count.fetch_add(1, Ordering::SeqCst); - Ok(()) - } - - fn recv_response(&mut self) -> core::result::Result, Self::Error> { - let request = self.last_request.take().unwrap(); let result = match request.operation { BrokerOperation::Pipe(PipeRequest::Create(_)) => BrokerResult::Pipe( litebox_broker_protocol::message::PipeResponse::Create(CreatePipeResponse { @@ -1204,10 +1194,17 @@ mod tests { panic!("unexpected broker request: {request:?}") } }; - Ok(Some(BrokerResponse { + Ok(BrokerResponse { request_id: request.request_id, result, - })) + }) + } + + fn with_serialized_payload( + &self, + transfer: impl FnOnce() -> T, + ) -> core::result::Result { + Ok(transfer()) } } diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 9819ee19c5..6ddc48af80 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -22,7 +22,7 @@ impl BrokerLocal { /// Panics if the broker reports an unrecoverable error or returns a protocol /// response that does not match the issued event request. pub fn create_event_with_count( - &mut self, + &self, initial_count: u64, ) -> Result { let response = @@ -40,7 +40,7 @@ impl BrokerLocal { /// Panics if the broker reports an unrecoverable error or returns a protocol /// response that does not match the issued event request. pub fn add_event( - &mut self, + &self, handle: ObjectHandle, value: u64, ) -> Result { @@ -58,7 +58,7 @@ impl BrokerLocal { /// Panics if the broker reports an unrecoverable error or returns a protocol /// response that does not match the issued event request. pub fn consume_event( - &mut self, + &self, handle: ObjectHandle, mode: EventConsumeMode, ) -> Result { @@ -70,7 +70,7 @@ impl BrokerLocal { } } - fn request_event(&mut self, request: EventRequest) -> Result { + fn request_event(&self, request: EventRequest) -> Result { match self.request(BrokerOperation::Event(request))? { BrokerResult::Event(response) => Ok(response), BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index b2a22d77ea..6a36e19fc9 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -3,9 +3,9 @@ //! Typed broker-local adapters for broker requests and notifications. //! -//! The local control adapter owns request/response sequencing but does not own a channel. -//! Userland, kernel, or ring-buffer deployments can provide channels by -//! implementing [`litebox_broker_protocol::channel::LocalControlChannel`]. +//! The local control adapter owns request identifiers but does not own transport +//! sequencing. Userland, kernel, or ring-buffer deployments provide control +//! channels by implementing [`litebox_broker_protocol::channel::LocalControlChannel`]. //! Notification receive adapters are intentionally separate so active control //! requests remain strictly paired with their responses. @@ -21,6 +21,7 @@ mod event; mod pipe; use alloc::sync::Arc; +use core::sync::atomic::{AtomicU64, Ordering}; use litebox_broker_protocol::channel::{LocalControlChannel, LocalNotificationChannel}; use litebox_broker_protocol::error::ErrorCode; @@ -42,7 +43,7 @@ pub use error::{BrokerLocalError, Result}; pub struct BrokerLocal { channel: Channel, shared_memory: Arc, - next_request_id: u64, + next_request_id: AtomicU64, } /// Broker-local receive adapter for broker-initiated asynchronous notifications. @@ -61,12 +62,9 @@ impl BrokerLocal { /// shared memory with an invalid size. pub fn negotiate( mut channel: Channel, - receive_shared_memory: impl FnOnce( + activate: impl FnOnce( &mut Channel, - ) -> core::result::Result< - Arc, - Channel::Error, - >, + ) -> core::result::Result, Channel::Error>, ) -> Result { let requested = BROKER_PROTOCOL_VERSION; let request = BrokerHandshakeRequest { @@ -87,8 +85,7 @@ impl BrokerLocal { requested, broker_protocol_version, "broker returned unexpected negotiation response: {response:?}" ); - let shared_memory = - receive_shared_memory(&mut channel).map_err(BrokerLocalError::Channel)?; + let shared_memory = activate(&mut channel).map_err(BrokerLocalError::Channel)?; assert_eq!( shared_memory.len(), PIPE_TRANSFER_BUFFER_SIZE, @@ -97,7 +94,7 @@ impl BrokerLocal { Ok(Self { channel, shared_memory, - next_request_id: 0, + next_request_id: AtomicU64::new(0), }) } BrokerHandshakeResponse::VersionMismatch { .. } => { @@ -123,28 +120,26 @@ impl BrokerLocal { /// Panics if the broker reports an unrecoverable error or returns a protocol /// response that does not match an active request. pub(crate) fn request( - &mut self, + &self, operation: BrokerOperation, ) -> Result { - let request_id = RequestId(self.next_request_id); - self.next_request_id = self + let request_id = self .next_request_id - .checked_add(1) - .ok_or(BrokerLocalError::RequestIdExhausted)?; - self.channel - .send_request(&BrokerRequest { - request_id, - operation, + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |request_id| { + request_id.checked_add(1) }) - .map_err(BrokerLocalError::Channel)?; + .map(RequestId) + .map_err(|_| BrokerLocalError::RequestIdExhausted)?; let BrokerResponse { request_id: response_id, result, } = self .channel - .recv_response() - .map_err(BrokerLocalError::Channel)? - .ok_or(BrokerLocalError::ChannelClosed)?; + .call(BrokerRequest { + request_id, + operation, + }) + .map_err(BrokerLocalError::Channel)?; if response_id != request_id { return Err(BrokerLocalError::UnexpectedResponseId { expected: request_id, @@ -180,10 +175,7 @@ impl BrokerLocal { /// /// Panics if the broker reports an unrecoverable error or returns a /// response that does not match the issued readiness request. - pub fn check_readiness( - &mut self, - handle: ObjectHandle, - ) -> Result { + pub fn check_readiness(&self, handle: ObjectHandle) -> Result { match self.request(BrokerOperation::CheckReadiness(handle))? { BrokerResult::Readiness(readiness) => Ok(readiness), BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), @@ -197,7 +189,7 @@ impl BrokerLocal { /// /// Panics if the broker reports an unrecoverable error or returns a protocol /// response that does not match an object close request. - pub fn close_object(&mut self, handle: ObjectHandle) -> Result<(), Channel::Error> { + pub fn close_object(&self, handle: ObjectHandle) -> Result<(), Channel::Error> { match self.request(BrokerOperation::CloseObject(handle))? { BrokerResult::ObjectClosed => Ok(()), BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), @@ -229,13 +221,14 @@ impl BrokerNotifications { #[cfg(test)] mod tests { use super::*; - use core::cell::Cell; + use core::cell::{Cell, RefCell}; use core::convert::Infallible; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::ProtocolVersion; use litebox_broker_protocol::channel::LocalNotificationChannel; use litebox_broker_protocol::message::ReadinessNotification; use litebox_broker_protocol::readiness::ReadinessFlags; + use std::sync::Mutex; #[test] fn negotiate_runs_setup_after_response_before_active_requests() { @@ -249,7 +242,7 @@ mod tests { let local = BrokerLocal::negotiate(channel, |channel| { assert!(channel.sent_handshake_request.is_some()); assert!(channel.handshake_response.is_none()); - assert!(channel.sent_request.is_none()); + assert!(channel.sent_request.borrow().is_none()); setup_calls.set(setup_calls.get() + 1); Ok(noop_shared_memory()) }) @@ -270,15 +263,15 @@ mod tests { let request = BrokerOperation::CloseObject(handle); let response = BrokerResult::ObjectClosed; let channel = FakeControlChannel::new(None, Some(response.clone())); - let mut local = BrokerLocal { + let local = BrokerLocal { channel, shared_memory: noop_shared_memory(), - next_request_id: 0, + next_request_id: AtomicU64::new(0), }; assert!(local.close_object(handle).is_ok()); assert_eq!( - local.channel.sent_request, + local.channel.sent_request.borrow().clone(), Some(BrokerRequest { request_id: RequestId(0), operation: request, @@ -290,35 +283,74 @@ mod tests { fn active_requests_use_monotonic_identifiers() { let handle = ObjectHandle(7); let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); - let mut local = BrokerLocal { + let local = BrokerLocal { channel, shared_memory: noop_shared_memory(), - next_request_id: 0, + next_request_id: AtomicU64::new(0), }; local.close_object(handle).unwrap(); assert_eq!( - local.channel.sent_request.as_ref().unwrap().request_id, + local + .channel + .sent_request + .borrow() + .as_ref() + .unwrap() + .request_id, RequestId(0) ); - local.channel.response = Some(BrokerResult::ObjectClosed); + *local.channel.response.borrow_mut() = Some(BrokerResult::ObjectClosed); local.close_object(handle).unwrap(); assert_eq!( - local.channel.sent_request.as_ref().unwrap().request_id, + local + .channel + .sent_request + .borrow() + .as_ref() + .unwrap() + .request_id, RequestId(1) ); } + #[test] + fn concurrent_active_requests_use_distinct_identifiers() { + let local = Arc::new(BrokerLocal { + channel: ConcurrentCallChannel { + request_ids: Mutex::new(std::vec::Vec::new()), + }, + shared_memory: noop_shared_memory(), + next_request_id: AtomicU64::new(0), + }); + let callers = (0..16) + .map(|handle| { + let local = Arc::clone(&local); + std::thread::spawn(move || local.close_object(ObjectHandle(handle))) + }) + .collect::>(); + + for caller in callers { + caller.join().unwrap().unwrap(); + } + let mut request_ids = local.channel.request_ids.lock().unwrap().clone(); + request_ids.sort(); + assert_eq!( + request_ids, + (0..16).map(RequestId).collect::>() + ); + } + #[test] fn active_request_rejects_mismatched_response_identifier() { let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); - let mut local = BrokerLocal { + let local = BrokerLocal { channel, shared_memory: noop_shared_memory(), - next_request_id: 0, + next_request_id: AtomicU64::new(0), }; - local.channel.response_id = Some(RequestId(9)); + local.channel.response_id.set(Some(RequestId(9))); assert!(matches!( local.close_object(ObjectHandle(7)), @@ -332,27 +364,27 @@ mod tests { #[test] fn active_request_identifier_exhaustion_does_not_wrap() { let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); - let mut local = BrokerLocal { + let local = BrokerLocal { channel, shared_memory: noop_shared_memory(), - next_request_id: u64::MAX, + next_request_id: AtomicU64::new(u64::MAX), }; assert!(matches!( local.close_object(ObjectHandle(7)), Err(BrokerLocalError::RequestIdExhausted) )); - assert!(local.channel.sent_request.is_none()); + assert!(local.channel.sent_request.borrow().is_none()); } #[test] fn active_request_returns_recoverable_broker_error() { let channel = FakeControlChannel::new(None, Some(BrokerResult::Error(ErrorCode::WouldBlock))); - let mut local = BrokerLocal { + let local = BrokerLocal { channel, shared_memory: noop_shared_memory(), - next_request_id: 0, + next_request_id: AtomicU64::new(0), }; assert!(matches!( @@ -365,10 +397,10 @@ mod tests { #[should_panic(expected = "broker returned unrecoverable error")] fn active_request_panics_on_unrecoverable_broker_error() { let channel = FakeControlChannel::new(None, Some(BrokerResult::Error(ErrorCode::Internal))); - let mut local = BrokerLocal { + let local = BrokerLocal { channel, shared_memory: noop_shared_memory(), - next_request_id: 0, + next_request_id: AtomicU64::new(0), }; let _ = local.create_event_with_count(0); @@ -458,7 +490,9 @@ mod tests { ); assert!(matches!( - BrokerLocal::negotiate(channel, |_| Err(FakeChannelError::SharedMemoryReceive)), + BrokerLocal::::negotiate(channel, |_| { + Err(FakeChannelError::SharedMemoryReceive) + }), Err(BrokerLocalError::Channel( FakeChannelError::SharedMemoryReceive )) @@ -499,10 +533,10 @@ mod tests { struct FakeControlChannel { sent_handshake_request: Option, - sent_request: Option, + sent_request: RefCell>, handshake_response: Option, - response: Option, - response_id: Option, + response: RefCell>, + response_id: Cell>, } #[derive(Debug, PartialEq, Eq)] @@ -552,10 +586,10 @@ mod tests { ) -> Self { Self { sent_handshake_request: None, - sent_request: None, + sent_request: RefCell::new(None), handshake_response, - response, - response_id: None, + response: RefCell::new(response), + response_id: Cell::new(None), } } } @@ -576,25 +610,33 @@ mod tests { ) -> core::result::Result, Self::Error> { Ok(self.handshake_response.take()) } - - fn send_request( - &mut self, - request: &BrokerRequest, - ) -> core::result::Result<(), Self::Error> { - self.sent_request = Some(request.clone()); - Ok(()) - } - - fn recv_response(&mut self) -> core::result::Result, Self::Error> { - Ok(self.response.take().map(|result| BrokerResponse { - request_id: self.response_id.unwrap_or_else(|| { + fn call( + &self, + request: BrokerRequest, + ) -> core::result::Result { + *self.sent_request.borrow_mut() = Some(request); + let result = self + .response + .borrow_mut() + .take() + .expect("response requires a scripted result"); + Ok(BrokerResponse { + request_id: self.response_id.get().unwrap_or_else(|| { self.sent_request + .borrow() .as_ref() .expect("response requires a sent request") .request_id }), result, - })) + }) + } + + fn with_serialized_payload( + &self, + transfer: impl FnOnce() -> T, + ) -> core::result::Result { + Ok(transfer()) } } @@ -602,6 +644,47 @@ mod tests { notification: Option, } + struct ConcurrentCallChannel { + request_ids: Mutex>, + } + + impl LocalControlChannel for ConcurrentCallChannel { + type Error = Infallible; + + fn send_handshake_request( + &mut self, + _request: &BrokerHandshakeRequest, + ) -> core::result::Result<(), Self::Error> { + Ok(()) + } + + fn recv_handshake_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + })) + } + + fn call( + &self, + request: BrokerRequest, + ) -> core::result::Result { + self.request_ids.lock().unwrap().push(request.request_id); + Ok(BrokerResponse { + request_id: request.request_id, + result: BrokerResult::ObjectClosed, + }) + } + + fn with_serialized_payload( + &self, + transfer: impl FnOnce() -> T, + ) -> core::result::Result { + Ok(transfer()) + } + } + impl LocalNotificationChannel for FakeNotificationChannel { type Error = Infallible; diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs index ec284c91dd..5559aa34f9 100644 --- a/litebox_broker_local/src/pipe.rs +++ b/litebox_broker_local/src/pipe.rs @@ -21,7 +21,7 @@ impl BrokerLocal { /// Panics if the broker reports an unrecoverable error or returns a /// response that does not match the issued pipe request. pub fn create_pipe( - &mut self, + &self, capacity: u64, atomic_write_size: u64, ) -> Result { @@ -41,8 +41,14 @@ impl BrokerLocal { /// /// Panics if the broker reports an unrecoverable error or returns a /// response that does not match the issued pipe request. - pub fn read_pipe( - &mut self, + pub fn read_pipe(&self, handle: ObjectHandle, length: u32) -> Result, Channel::Error> { + self.channel + .with_serialized_payload(|| self.read_pipe_serialized(handle, length)) + .map_err(BrokerLocalError::Channel)? + } + + fn read_pipe_serialized( + &self, handle: ObjectHandle, length: u32, ) -> Result, Channel::Error> { @@ -78,8 +84,14 @@ impl BrokerLocal { /// /// Panics if the broker reports an unrecoverable error or returns a /// response that does not match the issued pipe request. - pub fn write_pipe( - &mut self, + pub fn write_pipe(&self, handle: ObjectHandle, data: &[u8]) -> Result { + self.channel + .with_serialized_payload(|| self.write_pipe_serialized(handle, data)) + .map_err(BrokerLocalError::Channel)? + } + + fn write_pipe_serialized( + &self, handle: ObjectHandle, data: &[u8], ) -> Result { @@ -109,7 +121,7 @@ impl BrokerLocal { Ok(written) } - fn request_pipe(&mut self, request: PipeRequest) -> Result { + fn request_pipe(&self, request: PipeRequest) -> Result { match self.request(BrokerOperation::Pipe(request))? { BrokerResult::Pipe(response) => Ok(response), BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), @@ -126,10 +138,11 @@ impl BrokerLocal { mod tests { use super::*; use alloc::sync::Arc; - use core::convert::Infallible; + use core::{cell::RefCell, convert::Infallible}; use std::collections::VecDeque; use std::sync::Mutex; + use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, @@ -137,7 +150,6 @@ mod tests { }; use litebox_broker_protocol::pipe::{ReadPipeResponse, WritePipeResponse}; use litebox_broker_protocol::shared_memory::{SharedMemory, SharedMemoryError}; - use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, RequestId}; #[test] fn pipe_uses_attached_shared_memory_for_data_operations() { @@ -152,7 +164,7 @@ mod tests { BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { written: 2 })), BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2 })), ]); - let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory.clone())).unwrap(); + let local = BrokerLocal::negotiate(channel, |_| Ok(memory.clone())).unwrap(); local.create_pipe(64, 16).unwrap(); assert_eq!(local.write_pipe(write_handle, &[1, 2, 3]).unwrap(), 2); @@ -163,8 +175,8 @@ mod tests { memory.write(0, &[4, 5, 6]).unwrap(); assert_eq!(local.read_pipe(read_handle, 3).unwrap(), [4, 5]); assert_eq!( - local.channel.sent_operations, - [ + local.channel.sent_operations.borrow().as_slice(), + &[ BrokerOperation::Pipe(PipeRequest::Create(CreatePipeRequest { capacity: 64, atomic_write_size: 16, @@ -185,7 +197,7 @@ mod tests { fn pipe_rejects_oversized_transfers_before_request() { let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); let channel = ScriptedChannel::new([]); - let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); let oversized_length = PIPE_TRANSFER_BUFFER_SIZE + 1; assert!(matches!( @@ -200,7 +212,7 @@ mod tests { litebox_broker_protocol::error::ErrorCode::ResourceExhausted )) )); - assert!(local.channel.sent_operations.is_empty()); + assert!(local.channel.sent_operations.borrow().is_empty()); } #[test] @@ -211,7 +223,7 @@ mod tests { read: 2, }))]); let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); - let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); let _ = local.read_pipe(ObjectHandle(1), 1); } @@ -224,7 +236,7 @@ mod tests { written: 2, }))]); let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); - let mut local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); let _ = local.write_pipe(ObjectHandle(1), &[0]); } @@ -277,17 +289,15 @@ mod tests { } struct ScriptedChannel { - results: VecDeque, - sent_operations: Vec, - last_request_id: Option, + results: RefCell>, + sent_operations: RefCell>, } impl ScriptedChannel { fn new(results: impl IntoIterator) -> Self { Self { - results: results.into_iter().collect(), - sent_operations: Vec::new(), - last_request_id: None, + results: RefCell::new(results.into_iter().collect()), + sent_operations: RefCell::new(Vec::new()), } } } @@ -310,23 +320,26 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION, })) } - - fn send_request( - &mut self, - request: &BrokerRequest, - ) -> core::result::Result<(), Self::Error> { - self.sent_operations.push(request.operation.clone()); - self.last_request_id = Some(request.request_id); - Ok(()) + fn call( + &self, + request: BrokerRequest, + ) -> core::result::Result { + self.sent_operations.borrow_mut().push(request.operation); + Ok(BrokerResponse { + request_id: request.request_id, + result: self + .results + .borrow_mut() + .pop_front() + .expect("response requires a scripted result"), + }) } - fn recv_response(&mut self) -> core::result::Result, Self::Error> { - Ok(self.results.pop_front().map(|result| BrokerResponse { - request_id: self - .last_request_id - .expect("response requires a sent request"), - result, - })) + fn with_serialized_payload( + &self, + transfer: impl FnOnce() -> T, + ) -> core::result::Result { + Ok(transfer()) } } } diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index 8b8cc51981..6566a3a188 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -37,7 +37,7 @@ pub enum HostReceive { PeerClosed, } -/// Local-side control channel for broker authority calls. +/// Local-side control channel for broker association setup and active calls. pub trait LocalControlChannel { /// Channel-specific error type. type Error; @@ -54,14 +54,19 @@ pub trait LocalControlChannel { /// starting another response frame. fn recv_handshake_response(&mut self) -> Result, Self::Error>; - /// Sends one active broker request. - fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error>; + /// Publishes one request and waits for its correlated response. + /// + /// Calls may execute concurrently, and each pending request must have a + /// distinct identifier. If a valid active call returns a channel error, the + /// association is considered failed: every concurrent or future call must + /// return an error rather than remain blocked. + fn call(&self, request: BrokerRequest) -> Result; - /// Receives one active broker response. + /// Serializes one complete shared-memory payload transfer. /// - /// Returns `Ok(None)` when the broker closed the channel cleanly before - /// starting another response frame. - fn recv_response(&mut self) -> Result, Self::Error>; + /// The closure must run exactly once while no other payload transfer using + /// the same association shared memory is active. + fn with_serialized_payload(&self, transfer: impl FnOnce() -> T) -> Result; } /// Host-side control channel for broker authority calls. diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 1220007de8..cc8bc236c8 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -6,17 +6,25 @@ //! This module deliberately uses `std` because Unix-domain sockets and `std::io` //! framing are hosted userland concerns. Portable broker interfaces live in the //! no_std protocol, local, core, and host crates. +//! +//! After setup, each caller thread registers its request and writes its complete +//! frame while holding the shared writer mutex; there is no local request worker. +//! One response-dispatcher thread exclusively reads responses, correlates them by +//! request ID, and wakes the matching callers. use std::io::{Error, ErrorKind, Read, Result as IoResult, Write}; use std::net::Shutdown; use std::os::unix::net::UnixStream; use std::path::Path; +use std::sync::{Arc, Condvar, Mutex}; use std::time::Instant; +use std::{collections::HashMap, thread}; use crate::shared_memory::MemfdSharedMemory; use crate::unix_io::{ refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, }; +use litebox_broker_protocol::RequestId; use litebox_broker_protocol::channel::{ HostControlChannel, HostNotificationChannel, HostReceive, LocalControlChannel, LocalNotificationChannel, PeerCredential, @@ -32,6 +40,8 @@ use litebox_broker_protocol::wire::{ }; const MAX_FRAME_LEN: usize = 64 * 1024; +/// Maximum number of active calls waiting for broker responses. +pub const MAX_PENDING_CALLS: usize = 64; /// Validates that a connected Unix socket belongs to `expected_process_id`. pub fn validate_peer_process(stream: &UnixStream, expected_process_id: u32) -> IoResult<()> { @@ -63,21 +73,45 @@ fn peer_process_id(stream: &UnixStream) -> IoResult { /// Local-side Unix-domain-socket control channel for the hosted userland POC. pub struct UnixStreamLocalControlChannel { + state: UnixStreamLocalControlState, +} + +enum UnixStreamLocalControlState { + Setup(UnixStreamLocalSetup), + Active(UnixStreamLocalActive), + Failed, +} + +struct UnixStreamLocalSetup { stream: UnixStream, setup_deadline: Option, + negotiated: bool, } /// Independently owned handle for interrupting local control-channel I/O. pub struct UnixStreamLocalControlCancellation { stream: UnixStream, + pending_calls: Arc, + association_failure: Arc, +} + +struct UnixStreamLocalActive { + request_stream: Mutex, + shutdown_stream: UnixStream, + pending_calls: Arc, + payload_transfer: Mutex<()>, + association_failure: Arc, } impl UnixStreamLocalControlChannel { /// Creates a local control channel from an already-connected Unix stream. pub const fn from_connected(stream: UnixStream) -> Self { Self { - stream, - setup_deadline: None, + state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { + stream, + setup_deadline: None, + negotiated: false, + }), } } @@ -96,41 +130,113 @@ impl UnixStreamLocalControlChannel { deadline: Instant, ) -> IoResult { UnixStream::connect(path).map(|stream| Self { - stream, - setup_deadline: Some(deadline), + state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { + stream, + setup_deadline: Some(deadline), + negotiated: false, + }), }) } - /// Creates a handle that can interrupt pending control-channel I/O. - pub fn cancellation_handle(&self) -> IoResult { - self.stream - .try_clone() - .map(|stream| UnixStreamLocalControlCancellation { stream }) - } - /// Receives the memfd associated with this control channel. pub fn receive_memfd( &mut self, expected_len: usize, deadline: Option, ) -> IoResult { - crate::shared_memory::receive_memfd(&mut self.stream, expected_len, deadline) + let UnixStreamLocalControlState::Setup(setup) = &mut self.state else { + return Err(invalid_data("broker control setup already completed")); + }; + crate::shared_memory::receive_memfd(&mut setup.stream, expected_len, deadline) + } + + /// Completes setup and starts the active response pump. + pub fn activate( + &mut self, + association_failure: impl Fn() + Send + Sync + 'static, + ) -> IoResult { + let UnixStreamLocalControlState::Setup(setup) = &self.state else { + return Err(invalid_data("broker control channel already active")); + }; + if !setup.negotiated { + return Err(invalid_data( + "broker control channel activated before negotiation completed", + )); + } + let UnixStreamLocalControlState::Setup(setup) = + core::mem::replace(&mut self.state, UnixStreamLocalControlState::Failed) + else { + unreachable!("broker control setup state disappeared"); + }; + + let response_stream = setup.stream; + let request_stream = response_stream.try_clone()?; + let shutdown_stream = response_stream.try_clone()?; + let cancellation_stream = response_stream.try_clone()?; + let response_cancellation = response_stream.try_clone()?; + let pending_calls = Arc::new(PendingCalls::new()); + let association_failure: Arc = Arc::new(association_failure); + let response_pending_calls = Arc::clone(&pending_calls); + let response_failure = Arc::clone(&association_failure); + thread::Builder::new() + .name("litebox-broker-responses".to_owned()) + .spawn(move || { + dispatch_responses( + response_stream, + response_cancellation, + response_pending_calls, + response_failure, + ); + })?; + + self.state = UnixStreamLocalControlState::Active(UnixStreamLocalActive { + request_stream: Mutex::new(request_stream), + shutdown_stream, + pending_calls: Arc::clone(&pending_calls), + payload_transfer: Mutex::new(()), + association_failure: Arc::clone(&association_failure), + }); + Ok(UnixStreamLocalControlCancellation { + stream: cancellation_stream, + pending_calls, + association_failure, + }) } } impl UnixStreamLocalControlCancellation { /// Shuts down the control stream, unblocking pending reads or writes. pub fn cancel(&self) -> IoResult<()> { - match self.stream.shutdown(Shutdown::Both) { - Err(error) if error.kind() == ErrorKind::NotConnected => Ok(()), - result => result, - } + fail_active_channel( + &self.pending_calls, + &self.stream, + self.association_failure.as_ref(), + Error::new(ErrorKind::ConnectionAborted, "broker association cancelled"), + ) } } impl Drop for UnixStreamLocalControlChannel { fn drop(&mut self) { - let _ = self.stream.shutdown(Shutdown::Both); + let UnixStreamLocalControlState::Active(active) = &self.state else { + return; + }; + let _ = fail_active_channel( + &active.pending_calls, + &active.shutdown_stream, + active.association_failure.as_ref(), + Error::new( + ErrorKind::ConnectionAborted, + "broker active channel dropped", + ), + ); + } +} + +fn shutdown(stream: &UnixStream) -> IoResult<()> { + match stream.shutdown(Shutdown::Both) { + Err(error) if error.kind() == ErrorKind::NotConnected => Ok(()), + result => result, } } @@ -146,6 +252,11 @@ pub struct UnixStreamLocalNotificationChannel { stream: UnixStream, } +/// Independently owned handle for interrupting local notification-channel I/O. +pub struct UnixStreamLocalNotificationCancellation { + stream: UnixStream, +} + /// Host-side Unix-domain-socket notification channel for the hosted userland POC. pub struct UnixStreamHostNotificationChannel { stream: UnixStream, @@ -191,6 +302,26 @@ impl UnixStreamLocalNotificationChannel { pub fn connect(path: impl AsRef) -> IoResult { UnixStream::connect(path).map(Self::from_connected) } + + /// Creates a handle that can interrupt pending notification-channel I/O. + pub fn cancellation_handle(&self) -> IoResult { + self.stream + .try_clone() + .map(|stream| UnixStreamLocalNotificationCancellation { stream }) + } +} + +impl UnixStreamLocalNotificationCancellation { + /// Shuts down the notification stream, unblocking pending reads. + pub fn cancel(&self) -> IoResult<()> { + shutdown(&self.stream) + } +} + +impl Drop for UnixStreamLocalNotificationChannel { + fn drop(&mut self) { + let _ = shutdown(&self.stream); + } } impl UnixStreamHostNotificationChannel { @@ -204,31 +335,68 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { type Error = Error; fn send_handshake_request(&mut self, request: &BrokerHandshakeRequest) -> IoResult<()> { + let UnixStreamLocalControlState::Setup(setup) = &mut self.state else { + return Err(invalid_data("broker control channel is already active")); + }; let frame = encode_handshake_request(request.clone()); - write_frame_with_deadline(&mut self.stream, &frame, self.setup_deadline) + write_frame_with_deadline(&mut setup.stream, &frame, setup.setup_deadline) } fn recv_handshake_response(&mut self) -> IoResult> { - let frame = read_frame_with_deadline(&mut self.stream, self.setup_deadline)?; - self.setup_deadline = None; + let UnixStreamLocalControlState::Setup(setup) = &mut self.state else { + return Err(invalid_data("broker control channel is already active")); + }; + let frame = read_frame_with_deadline(&mut setup.stream, setup.setup_deadline)?; + setup.setup_deadline = None; match frame { - Some(frame) => decode_handshake_response(&frame) - .map(Some) - .map_err(wire_error), + Some(frame) => { + let response = decode_handshake_response(&frame).map_err(wire_error)?; + setup.negotiated = matches!(&response, BrokerHandshakeResponse::Negotiated { .. }); + Ok(Some(response)) + } None => Ok(None), } } - fn send_request(&mut self, request: &BrokerRequest) -> IoResult<()> { - let frame = encode_request(request.clone()); - write_frame_with_deadline(&mut self.stream, &frame, None) + fn call(&self, request: BrokerRequest) -> IoResult { + let UnixStreamLocalControlState::Active(active) = &self.state else { + return Err(invalid_data("broker control channel is not active")); + }; + let request_id = request.request_id; + let pending_call = active.pending_calls.register(request_id)?; + let request_frame = encode_request(request); + + let write_result = { + let mut request_stream = active + .request_stream + .lock() + .expect("broker request writer mutex poisoned"); + match active.pending_calls.current_failure() { + Some(error) => Err(copy_io_error(&error)), + None => write_frame_with_deadline(&mut request_stream, &request_frame, None), + } + }; + if let Err(error) = write_result { + let _ = fail_active_channel( + &active.pending_calls, + &active.shutdown_stream, + active.association_failure.as_ref(), + error, + ); + } + + pending_call.wait() } - fn recv_response(&mut self) -> IoResult> { - match read_frame_with_deadline(&mut self.stream, None)? { - Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), - None => Ok(None), - } + fn with_serialized_payload(&self, transfer: impl FnOnce() -> T) -> IoResult { + let UnixStreamLocalControlState::Active(active) = &self.state else { + return Err(invalid_data("broker control channel is not active")); + }; + let _transfer = active + .payload_transfer + .lock() + .expect("broker payload-transfer mutex poisoned"); + Ok(transfer()) } } @@ -301,6 +469,217 @@ impl HostNotificationChannel for UnixStreamHostNotificationChannel { } } +struct PendingCalls { + state: Mutex, + capacity_available: Condvar, +} + +struct PendingCallState { + calls: HashMap>, + failure: Option>, +} + +struct PendingCall { + result: Mutex>, + result_ready: Condvar, +} + +enum PendingCallResult { + Response(BrokerResponse), + Failure(Arc), +} + +impl PendingCall { + fn new() -> Self { + Self { + result: Mutex::new(None), + result_ready: Condvar::new(), + } + } + + fn resolve(&self, result: PendingCallResult) { + let mut stored = self + .result + .lock() + .expect("broker pending-call result mutex poisoned"); + assert!(stored.is_none(), "broker pending call already resolved"); + *stored = Some(result); + self.result_ready.notify_one(); + } + + fn wait(&self) -> IoResult { + let mut result = self + .result + .lock() + .expect("broker pending-call result mutex poisoned"); + loop { + if let Some(result) = result.take() { + return match result { + PendingCallResult::Response(response) => Ok(response), + PendingCallResult::Failure(error) => Err(copy_io_error(&error)), + }; + } + result = self + .result_ready + .wait(result) + .expect("broker pending-call result mutex poisoned"); + } + } +} + +impl PendingCalls { + fn new() -> Self { + Self { + state: Mutex::new(PendingCallState { + calls: HashMap::new(), + failure: None, + }), + capacity_available: Condvar::new(), + } + } + + fn register(&self, request_id: RequestId) -> IoResult> { + let pending_call = Arc::new(PendingCall::new()); + let mut state = self.state.lock().expect("broker pending mutex poisoned"); + while state.calls.len() == MAX_PENDING_CALLS && state.failure.is_none() { + state = self + .capacity_available + .wait(state) + .expect("broker pending mutex poisoned"); + } + if let Some(error) = state.failure.as_ref() { + return Err(copy_io_error(error)); + } + match state.calls.entry(request_id) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(Arc::clone(&pending_call)); + } + std::collections::hash_map::Entry::Occupied(_) => { + return Err(invalid_data("duplicate broker request ID")); + } + } + Ok(pending_call) + } + + fn complete(&self, response: BrokerResponse) -> IoResult<()> { + let pending_call = { + let mut state = self.state.lock().expect("broker pending mutex poisoned"); + if let Some(error) = state.failure.as_ref() { + return Err(copy_io_error(error)); + } + let Some(pending_call) = state.calls.remove(&response.request_id) else { + return Err(invalid_data("broker returned an unknown response ID")); + }; + self.capacity_available.notify_one(); + pending_call + }; + pending_call.resolve(PendingCallResult::Response(response)); + Ok(()) + } + + fn record_failure(&self, error: Arc) -> bool { + let pending_calls = { + let mut state = self.state.lock().expect("broker pending mutex poisoned"); + if state.failure.is_some() { + return false; + } + state.failure = Some(Arc::clone(&error)); + let pending_calls = core::mem::take(&mut state.calls); + self.capacity_available.notify_all(); + pending_calls + }; + for pending_call in pending_calls.into_values() { + pending_call.resolve(PendingCallResult::Failure(Arc::clone(&error))); + } + true + } + + fn current_failure(&self) -> Option> { + self.state + .lock() + .expect("broker pending mutex poisoned") + .failure + .as_ref() + .map(Arc::clone) + } +} + +fn dispatch_responses( + mut response_stream: UnixStream, + response_cancellation: UnixStream, + pending_calls: Arc, + association_failure: Arc, +) { + loop { + let response = match read_frame_with_deadline(&mut response_stream, None) { + Ok(Some(frame)) => match decode_response(&frame).map_err(wire_error) { + Ok(response) => response, + Err(error) => { + let _ = fail_active_channel( + &pending_calls, + &response_cancellation, + association_failure.as_ref(), + error, + ); + return; + } + }, + Ok(None) => { + let _ = fail_active_channel( + &pending_calls, + &response_cancellation, + association_failure.as_ref(), + Error::new( + ErrorKind::UnexpectedEof, + "broker closed the active control channel", + ), + ); + return; + } + Err(error) => { + let _ = fail_active_channel( + &pending_calls, + &response_cancellation, + association_failure.as_ref(), + error, + ); + return; + } + }; + + if let Err(error) = pending_calls.complete(response) { + let _ = fail_active_channel( + &pending_calls, + &response_cancellation, + association_failure.as_ref(), + error, + ); + return; + } + } +} + +fn fail_active_channel( + pending_calls: &PendingCalls, + shutdown_stream: &UnixStream, + association_failure: &(dyn Fn() + Send + Sync), + error: Error, +) -> IoResult<()> { + let first_failure = pending_calls.record_failure(Arc::new(error)); + let shutdown_result = shutdown(shutdown_stream); + if first_failure { + association_failure(); + } + shutdown_result +} + +fn copy_io_error(error: &Error) -> Error { + match error.raw_os_error() { + Some(code) => Error::from_raw_os_error(code), + None => Error::new(error.kind(), error.to_string()), + } +} + fn read_frame_with_deadline( stream: &mut UnixStream, deadline: Option, @@ -390,10 +769,50 @@ fn wire_error(error: WireError) -> Error { #[cfg(test)] mod tests { use super::*; - use litebox_broker_protocol::RequestId; - use litebox_broker_protocol::message::{BrokerOperation, BrokerRequest}; + use litebox_broker_protocol::message::{ + BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, + }; + use litebox_broker_protocol::{ObjectHandle, RequestId}; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Barrier, mpsc}; use std::time::Duration; + fn activate_test_channel( + stream: UnixStream, + association_failure: impl Fn() + Send + Sync + 'static, + ) -> ( + UnixStreamLocalControlChannel, + UnixStreamLocalControlCancellation, + ) { + let mut channel = UnixStreamLocalControlChannel { + state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { + stream, + setup_deadline: None, + negotiated: true, + }), + }; + let cancellation = channel.activate(association_failure).unwrap(); + (channel, cancellation) + } + + fn activate_counting_failure_channel( + stream: UnixStream, + ) -> ( + UnixStreamLocalControlChannel, + UnixStreamLocalControlCancellation, + Arc, + mpsc::Receiver<()>, + ) { + let failure_count = Arc::new(AtomicUsize::new(0)); + let response_failure_count = Arc::clone(&failure_count); + let (failure_sender, failure_receiver) = mpsc::channel(); + let (channel, cancellation) = activate_test_channel(stream, move || { + response_failure_count.fetch_add(1, Ordering::SeqCst); + failure_sender.send(()).unwrap(); + }); + (channel, cancellation, failure_count, failure_receiver) + } + #[test] fn linux_peer_validation_identifies_connected_process() { let (first, second) = UnixStream::pair().unwrap(); @@ -434,6 +853,536 @@ mod tests { ); } + #[test] + fn local_control_channel_enforces_setup_and_active_phases() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); + assert_eq!( + channel + .call(BrokerRequest { + request_id: RequestId(0), + operation: BrokerOperation::CloseObject(ObjectHandle(1)), + }) + .unwrap_err() + .kind(), + ErrorKind::InvalidData + ); + + assert_eq!( + channel.activate(|| {}).err().unwrap().kind(), + ErrorKind::InvalidData + ); + let handshake_request = BrokerHandshakeRequest { + protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }; + channel.send_handshake_request(&handshake_request).unwrap(); + assert_eq!( + decode_handshake_request( + &read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap() + ) + .unwrap(), + handshake_request + ); + write_frame_with_deadline( + &mut host_stream, + &encode_handshake_response(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }), + None, + ) + .unwrap(); + assert!(matches!( + channel.recv_handshake_response().unwrap(), + Some(BrokerHandshakeResponse::Negotiated { .. }) + )); + + let _cancellation = channel.activate(|| {}).unwrap(); + + assert_eq!( + channel.activate(|| {}).err().unwrap().kind(), + ErrorKind::InvalidData + ); + assert_eq!( + channel + .send_handshake_request(&BrokerHandshakeRequest { + protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }) + .unwrap_err() + .kind(), + ErrorKind::InvalidData + ); + + let channel = Arc::new(channel); + let call_channel = Arc::clone(&channel); + let call = thread::spawn(move || { + call_channel.call(BrokerRequest { + request_id: RequestId(1), + operation: BrokerOperation::CloseObject(ObjectHandle(1)), + }) + }); + let request = decode_request( + &read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(), + ) + .unwrap(); + write_frame_with_deadline( + &mut host_stream, + &encode_response(BrokerResponse { + request_id: request.request_id, + result: BrokerResult::ObjectClosed, + }), + None, + ) + .unwrap(); + assert_eq!(call.join().unwrap().unwrap().request_id, RequestId(1)); + } + + #[test] + fn active_channel_matches_out_of_order_responses() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let (channel, _cancellation) = activate_test_channel(local_stream, || {}); + let channel = Arc::new(channel); + + let first_channel = Arc::clone(&channel); + let first = thread::spawn(move || { + first_channel.call(BrokerRequest { + request_id: RequestId(3), + operation: BrokerOperation::CloseObject(ObjectHandle(3)), + }) + }); + let second_channel = Arc::clone(&channel); + let second = thread::spawn(move || { + second_channel.call(BrokerRequest { + request_id: RequestId(7), + operation: BrokerOperation::CloseObject(ObjectHandle(7)), + }) + }); + + let first_request = decode_request( + &read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(), + ) + .unwrap(); + let second_request = decode_request( + &read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(), + ) + .unwrap(); + let mut request_ids = [first_request.request_id, second_request.request_id]; + request_ids.sort(); + assert_eq!(request_ids, [RequestId(3), RequestId(7)]); + + write_frame_with_deadline( + &mut host_stream, + &encode_response(BrokerResponse { + request_id: second_request.request_id, + result: BrokerResult::ObjectClosed, + }), + None, + ) + .unwrap(); + write_frame_with_deadline( + &mut host_stream, + &encode_response(BrokerResponse { + request_id: first_request.request_id, + result: BrokerResult::ObjectClosed, + }), + None, + ) + .unwrap(); + + assert_eq!(first.join().unwrap().unwrap().request_id, RequestId(3)); + assert_eq!(second.join().unwrap().unwrap().request_id, RequestId(7)); + } + + #[test] + fn active_channel_serializes_shared_payload_transfers() { + let (local_stream, _host_stream) = UnixStream::pair().unwrap(); + let (channel, _cancellation) = activate_test_channel(local_stream, || {}); + let channel = Arc::new(channel); + let (first_entered_sender, first_entered_receiver) = mpsc::sync_channel(1); + let (release_first_sender, release_first_receiver) = mpsc::sync_channel(1); + let first_channel = Arc::clone(&channel); + let first = thread::spawn(move || { + first_channel + .with_serialized_payload(|| { + first_entered_sender.send(()).unwrap(); + release_first_receiver.recv().unwrap(); + }) + .unwrap(); + }); + first_entered_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + + let (second_started_sender, second_started_receiver) = mpsc::sync_channel(1); + let (second_entered_sender, second_entered_receiver) = mpsc::sync_channel(1); + let second = thread::spawn(move || { + second_started_sender.send(()).unwrap(); + channel + .with_serialized_payload(|| second_entered_sender.send(()).unwrap()) + .unwrap(); + }); + second_started_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + let entered_before_release = second_entered_receiver + .recv_timeout(Duration::from_millis(100)) + .is_ok(); + + release_first_sender.send(()).unwrap(); + if !entered_before_release { + second_entered_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + } + first.join().unwrap(); + second.join().unwrap(); + assert!(!entered_before_release); + } + + #[test] + fn active_channel_bounds_pending_calls_before_publication() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + host_stream + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let (channel, cancellation) = activate_test_channel(local_stream, || {}); + let channel = Arc::new(channel); + let call_start = Arc::new(Barrier::new(MAX_PENDING_CALLS + 2)); + let callers = (0..=MAX_PENDING_CALLS) + .map(|request_id| { + let channel = Arc::clone(&channel); + let call_start = Arc::clone(&call_start); + thread::spawn(move || { + call_start.wait(); + channel.call(BrokerRequest { + request_id: RequestId(request_id as u64), + operation: BrokerOperation::CloseObject(ObjectHandle(request_id as u64)), + }) + }) + }) + .collect::>(); + + call_start.wait(); + let mut published_request_ids = Vec::with_capacity(MAX_PENDING_CALLS); + for _ in 0..MAX_PENDING_CALLS { + let frame = read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(); + published_request_ids.push(decode_request(&frame).unwrap().request_id); + } + host_stream + .set_read_timeout(Some(Duration::from_millis(100))) + .unwrap(); + let error = read_frame_with_deadline(&mut host_stream, None).unwrap_err(); + assert!(matches!( + error.kind(), + ErrorKind::WouldBlock | ErrorKind::TimedOut + )); + + write_frame_with_deadline( + &mut host_stream, + &encode_response(BrokerResponse { + request_id: published_request_ids[0], + result: BrokerResult::ObjectClosed, + }), + None, + ) + .unwrap(); + host_stream + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let released_request = decode_request( + &read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert!(!published_request_ids.contains(&released_request.request_id)); + + cancellation.cancel().unwrap(); + let mut completed = 0; + let mut failed = 0; + for caller in callers { + match caller.join().unwrap() { + Ok(_) => completed += 1, + Err(_) => failed += 1, + } + } + assert_eq!(completed, 1); + assert_eq!(failed, MAX_PENDING_CALLS); + } + + #[test] + fn unknown_response_identifier_fails_all_pending_calls() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let (channel, _cancellation, failure_count, failure_receiver) = + activate_counting_failure_channel(local_stream); + let channel = Arc::new(channel); + let callers = [1, 2].map(|request_id| { + let channel = Arc::clone(&channel); + thread::spawn(move || { + channel.call(BrokerRequest { + request_id: RequestId(request_id), + operation: BrokerOperation::CloseObject(ObjectHandle(request_id)), + }) + }) + }); + + for _ in 0..callers.len() { + let frame = read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(); + decode_request(&frame).unwrap(); + } + write_frame_with_deadline( + &mut host_stream, + &encode_response(BrokerResponse { + request_id: RequestId(99), + result: BrokerResult::ObjectClosed, + }), + None, + ) + .unwrap(); + + for caller in callers { + assert_eq!( + caller.join().unwrap().unwrap_err().kind(), + ErrorKind::InvalidData + ); + } + failure_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_eq!(failure_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn malformed_response_fails_all_pending_calls() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let (channel, _cancellation, failure_count, failure_receiver) = + activate_counting_failure_channel(local_stream); + let channel = Arc::new(channel); + let callers = [1, 2].map(|request_id| { + let channel = Arc::clone(&channel); + thread::spawn(move || { + channel.call(BrokerRequest { + request_id: RequestId(request_id), + operation: BrokerOperation::CloseObject(ObjectHandle(request_id)), + }) + }) + }); + + for _ in 0..callers.len() { + let frame = read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(); + decode_request(&frame).unwrap(); + } + write_frame_with_deadline(&mut host_stream, &[u8::MAX], None).unwrap(); + + for caller in callers { + assert_eq!( + caller.join().unwrap().unwrap_err().kind(), + ErrorKind::InvalidData + ); + } + failure_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_eq!(failure_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn response_eof_fails_all_pending_calls() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let (channel, _cancellation, failure_count, failure_receiver) = + activate_counting_failure_channel(local_stream); + let channel = Arc::new(channel); + let callers = [1, 2].map(|request_id| { + let channel = Arc::clone(&channel); + thread::spawn(move || { + channel.call(BrokerRequest { + request_id: RequestId(request_id), + operation: BrokerOperation::CloseObject(ObjectHandle(request_id)), + }) + }) + }); + + for _ in 0..callers.len() { + let frame = read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(); + decode_request(&frame).unwrap(); + } + drop(host_stream); + + for caller in callers { + assert_eq!( + caller.join().unwrap().unwrap_err().kind(), + ErrorKind::UnexpectedEof + ); + } + failure_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_eq!(failure_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn request_write_failure_fails_existing_pending_calls() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let (channel, _cancellation, failure_count, failure_receiver) = + activate_counting_failure_channel(local_stream); + let channel = Arc::new(channel); + let pending_callers = [1, 2].map(|request_id| { + let channel = Arc::clone(&channel); + thread::spawn(move || { + channel.call(BrokerRequest { + request_id: RequestId(request_id), + operation: BrokerOperation::CloseObject(ObjectHandle(request_id)), + }) + }) + }); + for _ in 0..pending_callers.len() { + let frame = read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(); + decode_request(&frame).unwrap(); + } + + host_stream.shutdown(Shutdown::Read).unwrap(); + let failing_channel = Arc::clone(&channel); + let failing_caller = thread::spawn(move || { + failing_channel.call(BrokerRequest { + request_id: RequestId(3), + operation: BrokerOperation::CloseObject(ObjectHandle(3)), + }) + }); + + assert!(failing_caller.join().unwrap().is_err()); + for caller in pending_callers { + assert!(caller.join().unwrap().is_err()); + } + failure_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_eq!(failure_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn duplicate_response_identifier_fails_other_pending_calls() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let (channel, _cancellation, failure_count, failure_receiver) = + activate_counting_failure_channel(local_stream); + let channel = Arc::new(channel); + let first_channel = Arc::clone(&channel); + let first = thread::spawn(move || { + first_channel.call(BrokerRequest { + request_id: RequestId(1), + operation: BrokerOperation::CloseObject(ObjectHandle(1)), + }) + }); + let second_channel = Arc::clone(&channel); + let second = thread::spawn(move || { + second_channel.call(BrokerRequest { + request_id: RequestId(2), + operation: BrokerOperation::CloseObject(ObjectHandle(2)), + }) + }); + + for _ in 0..2 { + let frame = read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(); + decode_request(&frame).unwrap(); + } + let response = encode_response(BrokerResponse { + request_id: RequestId(1), + result: BrokerResult::ObjectClosed, + }); + write_frame_with_deadline(&mut host_stream, &response, None).unwrap(); + write_frame_with_deadline(&mut host_stream, &response, None).unwrap(); + + assert_eq!(first.join().unwrap().unwrap().request_id, RequestId(1)); + assert_eq!( + second.join().unwrap().unwrap_err().kind(), + ErrorKind::InvalidData + ); + failure_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_eq!(failure_count.load(Ordering::SeqCst), 1); + } + + #[test] + fn completed_call_wins_over_later_association_failure() { + let pending = PendingCalls::new(); + let request_id = RequestId(1); + let pending_call = pending.register(request_id).unwrap(); + pending + .complete(BrokerResponse { + request_id, + result: BrokerResult::ObjectClosed, + }) + .unwrap(); + pending.record_failure(Arc::new(Error::new( + ErrorKind::ConnectionAborted, + "test failure", + ))); + + assert_eq!(pending_call.wait().unwrap().request_id, request_id); + } + + #[test] + fn duplicate_pending_registration_preserves_the_original_call() { + let pending = PendingCalls::new(); + let request_id = RequestId(1); + let pending_call = pending.register(request_id).unwrap(); + assert_eq!( + pending.register(request_id).err().unwrap().kind(), + ErrorKind::InvalidData + ); + pending + .complete(BrokerResponse { + request_id, + result: BrokerResult::ObjectClosed, + }) + .unwrap(); + + assert_eq!(pending_call.wait().unwrap().request_id, request_id); + } + + #[test] + fn association_failure_wins_before_completion() { + let pending = PendingCalls::new(); + let request_id = RequestId(1); + let pending_call = pending.register(request_id).unwrap(); + pending.record_failure(Arc::new(Error::new( + ErrorKind::ConnectionAborted, + "test failure", + ))); + + assert!( + pending + .complete(BrokerResponse { + request_id, + result: BrokerResult::ObjectClosed, + }) + .is_err() + ); + assert_eq!( + pending_call.wait().unwrap_err().kind(), + ErrorKind::ConnectionAborted + ); + } + #[test] fn malformed_frames_are_invalid() { let (mut writer, mut reader) = UnixStream::pair().unwrap(); @@ -482,8 +1431,11 @@ mod tests { fn local_handshake_response_read_setup_deadline_is_wall_clock() { let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); let mut channel = UnixStreamLocalControlChannel { - stream: local_stream, - setup_deadline: Some(Instant::now() + Duration::from_millis(50)), + state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { + stream: local_stream, + setup_deadline: Some(Instant::now() + Duration::from_millis(50)), + negotiated: false, + }), }; let reader = std::thread::spawn(move || channel.recv_handshake_response().unwrap_err()); @@ -566,36 +1518,49 @@ mod tests { } #[test] - fn local_control_cancellation_unblocks_response_read() { - let (local_stream, _host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); - let cancellation = channel.cancellation_handle().unwrap(); - let completed = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); - let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(1); - let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); - let reader_completed = completed.clone(); - let reader = std::thread::spawn(move || { - started_sender.send(()).unwrap(); - result_sender.send(channel.recv_response()).unwrap(); - reader_completed.store(true, std::sync::atomic::Ordering::Release); + fn local_control_cancellation_unblocks_pending_call() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + host_stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let (channel, cancellation) = activate_test_channel(local_stream, || {}); + let (result_sender, result_receiver) = mpsc::sync_channel(1); + let caller = std::thread::spawn(move || { + result_sender + .send(channel.call(BrokerRequest { + request_id: RequestId(0), + operation: BrokerOperation::CloseObject(litebox_broker_protocol::ObjectHandle( + 1, + )), + })) + .unwrap(); }); - started_receiver - .recv_timeout(Duration::from_secs(1)) + let frame = read_frame_with_deadline(&mut host_stream, None) + .unwrap() .unwrap(); - std::thread::sleep(Duration::from_millis(50)); - assert!(!completed.load(std::sync::atomic::Ordering::Acquire)); + decode_request(&frame).unwrap(); + assert!(matches!( + result_receiver.try_recv(), + Err(mpsc::TryRecvError::Empty) + )); cancellation.cancel().unwrap(); - assert!(result_receiver.recv_timeout(Duration::from_secs(1)).is_ok()); - reader.join().unwrap(); + assert_eq!( + result_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap() + .unwrap_err() + .kind(), + ErrorKind::ConnectionAborted + ); + caller.join().unwrap(); } #[test] fn dropping_local_control_closes_connection_with_cancellation_clone() { let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let channel = UnixStreamLocalControlChannel::from_connected(local_stream); - let _cancellation = channel.cancellation_handle().unwrap(); + let (channel, _cancellation) = activate_test_channel(local_stream, || {}); host_stream .set_read_timeout(Some(Duration::from_secs(1))) .unwrap(); diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index 5b96813c13..ebb681c98f 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -36,10 +36,11 @@ fn host_serves_control_requests_over_paired_userland_channels() { ) }); - let mut local = BrokerLocal::negotiate( + let local = BrokerLocal::negotiate( UnixStreamLocalControlChannel::from_connected(local_control), |channel| { let shared_memory = channel.receive_memfd(PIPE_TRANSFER_BUFFER_SIZE, None)?; + let _cancellation = channel.activate(|| {})?; Ok(Arc::new(shared_memory)) }, ) diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index f869816a53..f6a95f72b9 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -82,11 +82,12 @@ fn run_fake_runner(args: &[OsString]) { let control_channel = connect_control_with_retry(Path::new(control_socket_path)).unwrap(); let _notification_channel = connect_notification_with_retry(Path::new(notification_socket_path)).unwrap(); - let mut local = BrokerLocal::negotiate(control_channel, |channel| { + let local = BrokerLocal::negotiate(control_channel, |channel| { let shared_memory = channel.receive_memfd( PIPE_TRANSFER_BUFFER_SIZE, Some(Instant::now() + Duration::from_secs(5)), )?; + let _cancellation = channel.activate(|| {})?; Ok(Arc::new(shared_memory)) }) .unwrap(); diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 150425b647..8f3adafd21 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -3,7 +3,10 @@ use std::{ path::Path, - sync::Arc, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, time::{Duration, Instant}, }; @@ -13,7 +16,7 @@ use litebox_broker_protocol::message::BrokerNotification; use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlCancellation, UnixStreamLocalControlChannel, - UnixStreamLocalNotificationChannel, + UnixStreamLocalNotificationCancellation, UnixStreamLocalNotificationChannel, }; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -25,7 +28,7 @@ pub(crate) fn connect( ) -> Result<( BrokerLocal, BrokerNotifications, - UnixStreamLocalControlCancellation, + Arc, )> { let setup_deadline = Instant::now() + SETUP_TIMEOUT; let control_channel = connect_with_retry( @@ -52,27 +55,40 @@ pub(crate) fn connect( notification_socket_path.display() ) })?; - let control_cancellation = control_channel + let notification_cancellation_handle = notification_channel .cancellation_handle() - .context("failed to create broker control cancellation handle")?; - let local = BrokerLocal::negotiate(control_channel, |channel| { - let shared_memory = - channel.receive_memfd(PIPE_TRANSFER_BUFFER_SIZE, Some(setup_deadline))?; - Ok(Arc::new(shared_memory)) + .context("failed to create broker notification cancellation handle")?; + let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( + notification_cancellation_handle, + )); + let local = BrokerLocal::negotiate(control_channel, { + let association_coordinator = Arc::clone(&association_coordinator); + move |channel| { + let shared_memory = + channel.receive_memfd(PIPE_TRANSFER_BUFFER_SIZE, Some(setup_deadline))?; + let weak_association_coordinator = Arc::downgrade(&association_coordinator); + let control_cancellation_handle = channel.activate(move || { + if let Some(association_coordinator) = weak_association_coordinator.upgrade() { + association_coordinator.report_failure(); + } + })?; + association_coordinator + .install_control_cancellation_handle(control_cancellation_handle)?; + Ok(Arc::new(shared_memory)) + } }) .context("broker negotiation failed")?; Ok(( local, BrokerNotifications::new(notification_channel), - control_cancellation, + association_coordinator, )) } pub(crate) fn start_notification_receiver( mut notifications: BrokerNotifications, - control_cancellation: UnixStreamLocalControlCancellation, + association_coordinator: Arc, dispatch_notification: impl Fn(BrokerNotification) + Send + 'static, - dispatch_failure: impl Fn() + Send + 'static, ) -> Result<()> { std::thread::Builder::new() .name("litebox-broker-notifications".to_owned()) @@ -84,19 +100,99 @@ pub(crate) fn start_notification_receiver( Err(error) => break Some(error), } }; - let cancellation_error = control_cancellation.cancel().err(); - dispatch_failure(); + association_coordinator.report_failure(); if let Some(error) = receive_error { eprintln!("failed to receive broker notification: {error}"); } - if let Some(error) = cancellation_error { - eprintln!("failed to cancel broker control channel: {error}"); - } }) .context("failed to start broker notification receiver")?; Ok(()) } +pub(crate) struct BrokerAssociationFailureCoordinator { + failed: AtomicBool, + control_cancellation_handle: Mutex>, + notification_cancellation_handle: UnixStreamLocalNotificationCancellation, + dispatch_failure: Mutex>>, +} + +impl BrokerAssociationFailureCoordinator { + fn new(notification_cancellation_handle: UnixStreamLocalNotificationCancellation) -> Self { + Self { + failed: AtomicBool::new(false), + control_cancellation_handle: Mutex::new(None), + notification_cancellation_handle, + dispatch_failure: Mutex::new(None), + } + } + + fn install_control_cancellation_handle( + &self, + control_cancellation_handle: UnixStreamLocalControlCancellation, + ) -> std::io::Result<()> { + let mut installed = self + .control_cancellation_handle + .lock() + .expect("broker control cancellation mutex poisoned"); + assert!( + installed.is_none(), + "broker control cancellation already installed" + ); + if self.failed.load(Ordering::Acquire) { + control_cancellation_handle.cancel()?; + return Err(std::io::Error::new( + std::io::ErrorKind::ConnectionAborted, + "broker association failed during activation", + )); + } + *installed = Some(control_cancellation_handle); + Ok(()) + } + + pub(crate) fn install_dispatch(&self, dispatch_failure: impl FnOnce() + Send + 'static) { + let mut installed = self + .dispatch_failure + .lock() + .expect("broker failure dispatch mutex poisoned"); + assert!( + installed.is_none(), + "broker failure dispatch already installed" + ); + if self.failed.load(Ordering::Acquire) { + drop(installed); + dispatch_failure(); + } else { + *installed = Some(Box::new(dispatch_failure)); + } + } + + fn report_failure(&self) { + if self.failed.swap(true, Ordering::AcqRel) { + return; + } + if let Some(cancellation_handle) = self + .control_cancellation_handle + .lock() + .expect("broker control cancellation mutex poisoned") + .as_ref() + && let Err(error) = cancellation_handle.cancel() + { + eprintln!("failed to cancel broker control channel: {error}"); + } + if let Err(error) = self.notification_cancellation_handle.cancel() { + eprintln!("failed to cancel broker notification channel: {error}"); + } + let dispatch_failure = self + .dispatch_failure + .lock() + .expect("broker failure dispatch mutex poisoned") + .take(); + if let Some(dispatch_failure) = dispatch_failure { + dispatch_failure(); + } + } +} + fn connect_with_retry( socket_path: &Path, setup_deadline: Instant, @@ -116,3 +212,180 @@ fn connect_with_retry( std::thread::sleep(RETRY_DELAY.min(remaining)); } } + +#[cfg(test)] +mod tests { + use super::*; + use litebox_broker_protocol::channel::{HostControlChannel, HostReceive, LocalControlChannel}; + use litebox_broker_protocol::message::{BrokerOperation, BrokerRequest}; + use litebox_broker_protocol::{ObjectHandle, RequestId}; + use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; + use std::io::{ErrorKind, Read}; + use std::os::unix::net::UnixStream; + use std::sync::mpsc; + + fn negotiate_control_pair( + local_stream: UnixStream, + host_stream: UnixStream, + ) -> (UnixStreamLocalControlChannel, UnixStreamHostControlChannel) { + let mut local = UnixStreamLocalControlChannel::from_connected(local_stream); + let mut host = UnixStreamHostControlChannel::from_accepted(host_stream); + let request = litebox_broker_protocol::message::BrokerHandshakeRequest { + protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }; + local.send_handshake_request(&request).unwrap(); + assert_eq!( + host.recv_handshake_request().unwrap(), + HostReceive::Message(request) + ); + host.send_handshake_response( + &litebox_broker_protocol::message::BrokerHandshakeResponse::Negotiated { + broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }, + ) + .unwrap(); + assert!(matches!( + local.recv_handshake_response().unwrap(), + Some(litebox_broker_protocol::message::BrokerHandshakeResponse::Negotiated { .. }) + )); + (local, host) + } + + fn activate_control_channel( + channel: &mut UnixStreamLocalControlChannel, + association_coordinator: &Arc, + ) -> UnixStreamLocalControlCancellation { + let weak_association_coordinator = Arc::downgrade(association_coordinator); + channel + .activate(move || { + if let Some(association_coordinator) = weak_association_coordinator.upgrade() { + association_coordinator.report_failure(); + } + }) + .unwrap() + } + + #[test] + fn control_failure_cancels_notifications() { + let (local_control, host_control) = UnixStream::pair().unwrap(); + let (mut active_channel, host_control) = + negotiate_control_pair(local_control, host_control); + let (local_notification, mut host_notification) = UnixStream::pair().unwrap(); + host_notification + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let notification_channel = + UnixStreamLocalNotificationChannel::from_connected(local_notification); + let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( + notification_channel.cancellation_handle().unwrap(), + )); + let control_cancellation_handle = + activate_control_channel(&mut active_channel, &association_coordinator); + association_coordinator + .install_control_cancellation_handle(control_cancellation_handle) + .unwrap(); + let (failure_sender, failure_receiver) = mpsc::sync_channel(1); + association_coordinator.install_dispatch(move || failure_sender.send(()).unwrap()); + + drop(host_control); + + failure_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + let mut byte = [0]; + assert_eq!(host_notification.read(&mut byte).unwrap(), 0); + drop(active_channel); + drop(notification_channel); + } + + #[test] + fn failure_before_installation_cancels_control_and_dispatches_failure() { + let (local_control, host_control) = UnixStream::pair().unwrap(); + host_control + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let (mut active_channel, mut host_control) = + negotiate_control_pair(local_control, host_control); + let (local_notification, mut host_notification) = UnixStream::pair().unwrap(); + host_notification + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + let notification_channel = + UnixStreamLocalNotificationChannel::from_connected(local_notification); + let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( + notification_channel.cancellation_handle().unwrap(), + )); + let control_cancellation_handle = + activate_control_channel(&mut active_channel, &association_coordinator); + + association_coordinator.report_failure(); + + assert_eq!( + association_coordinator + .install_control_cancellation_handle(control_cancellation_handle) + .unwrap_err() + .kind(), + ErrorKind::ConnectionAborted + ); + let (failure_sender, failure_receiver) = mpsc::sync_channel(1); + association_coordinator.install_dispatch(move || failure_sender.send(()).unwrap()); + failure_receiver.try_recv().unwrap(); + assert_eq!( + host_control.recv_request().unwrap(), + HostReceive::PeerClosed + ); + let mut byte = [0]; + assert_eq!(host_notification.read(&mut byte).unwrap(), 0); + drop(active_channel); + drop(notification_channel); + } + + #[test] + fn notification_failure_cancels_control() { + let (local_control, host_control) = UnixStream::pair().unwrap(); + let (mut active_channel, mut host_control) = + negotiate_control_pair(local_control, host_control); + let (local_notification, host_notification) = UnixStream::pair().unwrap(); + let notification_channel = + UnixStreamLocalNotificationChannel::from_connected(local_notification); + let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( + notification_channel.cancellation_handle().unwrap(), + )); + let control_cancellation_handle = + activate_control_channel(&mut active_channel, &association_coordinator); + association_coordinator + .install_control_cancellation_handle(control_cancellation_handle) + .unwrap(); + let (failure_sender, failure_receiver) = mpsc::sync_channel(1); + association_coordinator.install_dispatch(move || failure_sender.send(()).unwrap()); + start_notification_receiver( + BrokerNotifications::new(notification_channel), + Arc::clone(&association_coordinator), + |_| {}, + ) + .unwrap(); + let active_channel = Arc::new(active_channel); + let pending_channel = Arc::clone(&active_channel); + let pending_call = std::thread::spawn(move || { + pending_channel.call(BrokerRequest { + request_id: RequestId(1), + operation: BrokerOperation::CloseObject(ObjectHandle(1)), + }) + }); + assert!(matches!( + host_control.recv_request().unwrap(), + HostReceive::Message(_) + )); + + drop(host_notification); + + failure_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert!(pending_call.join().unwrap().is_err()); + assert_eq!( + host_control.recv_request().unwrap(), + HostReceive::PeerClosed + ); + } +} diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 80d754de83..0e4d11a72b 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -242,16 +242,17 @@ pub fn run(cli_args: CliArgs) -> Result<()> { litebox_platform_multiplex::set_platform(platform); let shim_builder = if let Some(broker_connection) = broker_connection { - let (broker_local, broker_notifications, broker_control_cancellation) = broker_connection; + let (broker_local, broker_notifications, broker_association_coordinator) = + broker_connection; let litebox = litebox::LiteBox::new_with_broker_local( litebox_platform_multiplex::platform(), broker_local, ); + broker_association_coordinator.install_dispatch(litebox.broker_failure_dispatcher()); broker::start_notification_receiver( broker_notifications, - broker_control_cancellation, + broker_association_coordinator, litebox.broker_notification_dispatcher(), - litebox.broker_failure_dispatcher(), )?; litebox_shim_linux::LinuxShimBuilder::new_with_litebox(litebox) } else { From b0ca9f623ddc00ee0ee852b5b91f73ba5727afe4 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 22 Jul 2026 15:03:04 -0700 Subject: [PATCH 116/319] Add broker shared-buffer slots (#1065) Adds a checked 16-slot association shared-buffer pool with 32 KiB per slot and wires the full 512 KiB mapping through Linux-userland broker setup. Pipe payloads remain serialized through slot zero. Slot leasing and wire protocol changes are intentionally deferred. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/event/counter.rs | 2 +- litebox/src/pipes.rs | 2 +- litebox_broker_host/src/error.rs | 2 + litebox_broker_host/src/lib.rs | 169 ++++++---- litebox_broker_local/src/lib.rs | 42 +-- litebox_broker_local/src/pipe.rs | 41 ++- litebox_broker_protocol/src/lib.rs | 3 + litebox_broker_protocol/src/pipe.rs | 3 - litebox_broker_protocol/src/shared_memory.rs | 289 ++++++++++++++++++ litebox_broker_transport/src/shared_memory.rs | 39 ++- litebox_broker_userland/src/main.rs | 11 +- .../tests/notification_runtime.rs | 14 +- .../tests/userland_broker.rs | 4 +- litebox_runner_linux_userland/src/broker.rs | 4 +- litebox_runner_linux_userland/tests/run.rs | 11 +- 15 files changed, 516 insertions(+), 120 deletions(-) diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index f0da3fc514..5859da3437 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -412,7 +412,7 @@ mod tests { impl litebox_broker_protocol::shared_memory::SharedMemory for NoopSharedMemory { fn len(&self) -> usize { - litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE + litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE } fn read( diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index bd21130ed6..8388d83a97 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -1119,7 +1119,7 @@ mod tests { impl litebox_broker_protocol::shared_memory::SharedMemory for NoopSharedMemory { fn len(&self) -> usize { - litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE + litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE } fn read( diff --git a/litebox_broker_host/src/error.rs b/litebox_broker_host/src/error.rs index 1338637827..c52813d084 100644 --- a/litebox_broker_host/src/error.rs +++ b/litebox_broker_host/src/error.rs @@ -13,6 +13,8 @@ pub enum BrokerHostError { Channel(#[source] E), #[error("broker setup failed: {0}")] Broker(#[source] ErrorCode), + #[error("broker association shared-buffer layout does not match the protocol layout")] + SharedBufferLayoutMismatch, } impl From for BrokerHostError { diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index cad4d1a24a..7c85d18814 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -28,9 +28,11 @@ use litebox_broker_protocol::message::{ EventRequest, EventResponse, PipeRequest, PipeResponse, }; use litebox_broker_protocol::pipe::{ - CreatePipeResponse, PIPE_TRANSFER_BUFFER_SIZE, ReadPipeResponse, WritePipeResponse, + CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE, ReadPipeResponse, WritePipeResponse, +}; +use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_LAYOUT, SharedBufferPool, SharedBufferSlotIndex, SharedMemory, }; -use litebox_broker_protocol::shared_memory::SharedMemory; mod error; @@ -45,23 +47,25 @@ pub use error::{BrokerHostError, Result}; /// Event mutations caused by control requests return readiness in their control /// response and do not also emit a duplicate notification. /// -/// `shared_memory` belongs to this association and is reused at offset zero for -/// serialized pipe transfers. `send_shared_memory` runs after version +/// `shared_buffers` belongs to this association. Pipe transfers currently reuse +/// slot zero serially. `send_shared_memory` runs after version /// negotiation and before active requests begin. -pub fn serve_connection( +pub fn serve_connection( core: &BrokerCore, control_channel: &mut ControlChannel, _notification_channel: &mut NotificationChannel, - shared_memory: &dyn SharedMemory, + shared_buffers: &SharedBufferPool, send_shared_memory: impl FnOnce(&mut ControlChannel) -> core::result::Result<(), ChannelError>, ) -> Result where ControlChannel: HostControlChannel, NotificationChannel: HostNotificationChannel, + Memory: SharedMemory, { - if shared_memory.len() != PIPE_TRANSFER_BUFFER_SIZE { - return Err(BrokerHostError::Broker(ErrorCode::Internal)); + if shared_buffers.layout() != SHARED_BUFFER_LAYOUT { + return Err(BrokerHostError::SharedBufferLayoutMismatch); } + let peer_credential = control_channel .peer_credential() .map_err(BrokerHostError::Channel)?; @@ -123,7 +127,7 @@ where request_id, operation, } = request; - let result = complete_request(handle_request(&session, operation, shared_memory)) + let result = complete_request(handle_request(&session, operation, shared_buffers)) .map_err(BrokerHostError::Broker)?; control_channel .send_response(&BrokerResponse { request_id, result }) @@ -153,10 +157,10 @@ fn complete_request( } } -fn handle_request( +fn handle_request( session: &BrokerSession, operation: BrokerOperation, - shared_memory: &dyn SharedMemory, + shared_buffers: &SharedBufferPool, ) -> RequestResult { match operation { BrokerOperation::CloseObject(handle) => session @@ -171,16 +175,18 @@ fn handle_request( handle_event_request(session, request).map(BrokerResult::Event) } BrokerOperation::Pipe(request) => { - handle_pipe_request(session, request, shared_memory).map(BrokerResult::Pipe) + handle_pipe_request(session, request, shared_buffers).map(BrokerResult::Pipe) } } } -fn handle_pipe_request( +fn handle_pipe_request( session: &BrokerSession, request: PipeRequest, - shared_memory: &dyn SharedMemory, + shared_buffers: &SharedBufferPool, ) -> RequestResult { + const SERIALIZED_PIPE_SLOT: SharedBufferSlotIndex = SharedBufferSlotIndex(0); + match request { PipeRequest::Create(request) => { litebox_broker_core::pipe::create(session, request.capacity, request.atomic_write_size) @@ -193,13 +199,13 @@ fn handle_pipe_request( .map_err(|error| RequestFailure::Respond(error.into())) } PipeRequest::Read(request) => { - if request.length as usize > PIPE_TRANSFER_BUFFER_SIZE { + if request.length > MAX_PIPE_TRANSFER_SIZE { return Err(RequestFailure::Respond(ErrorCode::MalformedRequest)); } let data = litebox_broker_core::pipe::read(session, request.handle, request.length) .map_err(|error| RequestFailure::Respond(error.into()))?; - shared_memory - .write(0, &data) + shared_buffers + .write(SERIALIZED_PIPE_SLOT, &data) .map_err(|_| RequestFailure::Abort(ErrorCode::Internal))?; Ok(PipeResponse::Read(ReadPipeResponse { read: data @@ -209,7 +215,7 @@ fn handle_pipe_request( })) } PipeRequest::Write(request) => { - if request.length as usize > PIPE_TRANSFER_BUFFER_SIZE { + if request.length > MAX_PIPE_TRANSFER_SIZE { return Err(RequestFailure::Respond(ErrorCode::MalformedRequest)); } let length = request.length as usize; @@ -218,8 +224,8 @@ fn handle_pipe_request( return Err(RequestFailure::Respond(ErrorCode::OutOfMemory)); } data.resize(length, 0); - shared_memory - .read(0, &mut data) + shared_buffers + .read(SERIALIZED_PIPE_SLOT, &mut data) .map_err(|_| RequestFailure::Abort(ErrorCode::Internal))?; litebox_broker_core::pipe::write(session, request.handle, &data) .map_err(|error| RequestFailure::Respond(error.into())) @@ -278,7 +284,9 @@ mod tests { }; use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerNotification}; use litebox_broker_protocol::pipe::{CreatePipeRequest, ReadPipeRequest, WritePipeRequest}; - use litebox_broker_protocol::shared_memory::SharedMemoryError; + use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, SharedMemoryError, + }; use litebox_broker_protocol::{ObjectHandle, ProtocolVersion, RequestId}; use std::sync::{Arc, Mutex}; @@ -298,8 +306,9 @@ mod tests { serve_connection_returns_event_readiness_in_control_responses(&broker); serve_connection_continues_after_recoverable_request_failure(&broker); serve_connection_aborts_without_response_on_shared_memory_failure(&broker); + serve_connection_rejects_incompatible_shared_buffer_layout(&broker); active_request_closes_object_reference(&broker); - association_shared_memory_stages_pipe_data(&broker); + association_shared_buffer_slot_zero_stages_pipe_data(&broker); } fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { @@ -322,7 +331,7 @@ mod tests { broker, &mut channel, &mut notifications, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + &test_shared_buffers(), |_| Ok(()), ) .unwrap(), @@ -361,7 +370,7 @@ mod tests { broker, &mut channel, &mut notifications, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + &test_shared_buffers(), |_| Ok(()), ) .unwrap(), @@ -398,7 +407,7 @@ mod tests { broker, &mut channel, &mut notifications, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + &test_shared_buffers(), |_| { setup_called.set(true); Ok(()) @@ -428,7 +437,7 @@ mod tests { broker, &mut channel, &mut notifications, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + &test_shared_buffers(), |_| Ok(()), ) .unwrap(), @@ -455,7 +464,7 @@ mod tests { broker, &mut channel, &mut notifications, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + &test_shared_buffers(), |_| Ok(()), ) .unwrap(), @@ -484,7 +493,7 @@ mod tests { broker, &mut channel, &mut notifications, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + &test_shared_buffers(), |_| Ok(()), ) { Err(BrokerHostError::Channel(())) => {} @@ -510,7 +519,7 @@ mod tests { broker, &mut channel, &mut notifications, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + &test_shared_buffers(), |_| Ok(()), ) .unwrap(), @@ -559,7 +568,7 @@ mod tests { broker, &mut channel, &mut notifications, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), + &test_shared_buffers(), |_| Ok(()), ) .unwrap(), @@ -596,7 +605,7 @@ mod tests { broker, &mut channel, &mut notifications, - &FailingSharedMemory, + &SharedBufferPool::new(FailingSharedMemory, SHARED_BUFFER_LAYOUT).unwrap(), |_| Ok(()), ), Err(BrokerHostError::Broker(ErrorCode::Internal)) @@ -608,6 +617,43 @@ mod tests { )); } + fn serve_connection_rejects_incompatible_shared_buffer_layout(broker: &BrokerCore) { + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + }))]), + std::vec::Vec::new(), + ); + let mut notifications = FakeHostNotificationChannel::default(); + let incompatible_layout = litebox_broker_protocol::shared_memory::SharedBufferLayout::new( + u32::try_from(SHARED_BUFFER_POOL_SIZE).unwrap(), + 1, + ) + .unwrap(); + let shared_buffers = SharedBufferPool::new( + TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE), + incompatible_layout, + ) + .unwrap(); + let setup_called = Cell::new(false); + + assert!(matches!( + serve_connection( + broker, + &mut channel, + &mut notifications, + &shared_buffers, + |_| { + setup_called.set(true); + Ok(()) + }, + ), + Err(BrokerHostError::SharedBufferLayoutMismatch) + )); + assert!(!setup_called.get()); + assert!(channel.handshake_responses.is_empty()); + } + fn active_request_closes_object_reference(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) @@ -640,60 +686,73 @@ mod tests { ); } - fn association_shared_memory_stages_pipe_data(broker: &BrokerCore) { + fn association_shared_buffer_slot_zero_stages_pipe_data(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); - let memory = TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE); - let created = handle_test_request_with_memory( + let memory = TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE); + let shared_buffers = SharedBufferPool::new(memory.clone(), SHARED_BUFFER_LAYOUT).unwrap(); + shared_buffers + .write(SharedBufferSlotIndex(1), &[9]) + .unwrap(); + let created = handle_test_request_with_buffers( &session, BrokerOperation::Pipe(PipeRequest::Create(CreatePipeRequest { capacity: 64, atomic_write_size: 16, })), - &memory, + &shared_buffers, ); let BrokerResult::Pipe(PipeResponse::Create(response)) = created else { panic!("expected successful pipe creation"); }; - memory.write(0, &[1, 2, 3]).unwrap(); - let write = handle_test_request_with_memory( + shared_buffers + .write(SharedBufferSlotIndex(0), &[1, 2, 3]) + .unwrap(); + let write = handle_test_request_with_buffers( &session, BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle: response.write_handle, length: 3, })), - &memory, + &shared_buffers, ); assert_eq!( write, BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })) ); - let read = handle_test_request_with_memory( + let read = handle_test_request_with_buffers( &session, BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { handle: response.read_handle, length: 3, })), - &memory, + &shared_buffers, ); assert_eq!( read, BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 3 })) ); let mut data = [0; 3]; - memory.read(0, &mut data).unwrap(); + shared_buffers + .read(SharedBufferSlotIndex(0), &mut data) + .unwrap(); assert_eq!(data, [1, 2, 3]); + let mut second_slot = [0]; + shared_buffers + .read(SharedBufferSlotIndex(1), &mut second_slot) + .unwrap(); + assert_eq!(second_slot, [9]); - let invalid_range = handle_test_request_with_memory( + let invalid_range = handle_test_request_with_buffers( &session, BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle: response.write_handle, - length: u32::try_from(PIPE_TRANSFER_BUFFER_SIZE).unwrap() + 1, + length: MAX_PIPE_TRANSFER_SIZE + 1, })), - &memory, + &shared_buffers, ); assert_eq!( invalid_range, @@ -702,19 +761,23 @@ mod tests { } fn handle_test_request(session: &BrokerSession, operation: BrokerOperation) -> BrokerResult { - handle_test_request_with_memory( - session, - operation, - &TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE), - ) + handle_test_request_with_buffers(session, operation, &test_shared_buffers()) } - fn handle_test_request_with_memory( + fn handle_test_request_with_buffers( session: &BrokerSession, operation: BrokerOperation, - shared_memory: &dyn SharedMemory, + shared_buffers: &SharedBufferPool, ) -> BrokerResult { - complete_request(handle_request(session, operation, shared_memory)).unwrap() + complete_request(handle_request(session, operation, shared_buffers)).unwrap() + } + + fn test_shared_buffers() -> SharedBufferPool { + SharedBufferPool::new( + TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE), + SHARED_BUFFER_LAYOUT, + ) + .unwrap() } struct FakeHostControlChannel { @@ -897,7 +960,7 @@ mod tests { impl SharedMemory for FailingSharedMemory { fn len(&self) -> usize { - PIPE_TRANSFER_BUFFER_SIZE + SHARED_BUFFER_POOL_SIZE } fn read( diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 6a36e19fc9..d7d6b83fdb 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -29,20 +29,21 @@ use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, }; -use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_protocol::readiness::ReadinessFlags; -use litebox_broker_protocol::shared_memory::SharedMemory; +use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_LAYOUT, SharedBufferPool, SharedMemory, +}; use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle, RequestId}; pub use error::{BrokerLocalError, Result}; /// Typed broker-local control adapter for broker operations. /// -/// The shared memory belongs to the broker association and is reused for each -/// serialized pipe transfer. +/// The shared-buffer pool belongs to the broker association. Pipe transfers +/// currently reuse slot zero under the channel's serialization scope. pub struct BrokerLocal { channel: Channel, - shared_memory: Arc, + shared_buffers: SharedBufferPool>, next_request_id: AtomicU64, } @@ -86,14 +87,11 @@ impl BrokerLocal { "broker returned unexpected negotiation response: {response:?}" ); let shared_memory = activate(&mut channel).map_err(BrokerLocalError::Channel)?; - assert_eq!( - shared_memory.len(), - PIPE_TRANSFER_BUFFER_SIZE, - "broker association shared memory has an invalid size" - ); + let shared_buffers = SharedBufferPool::new(shared_memory, SHARED_BUFFER_LAYOUT) + .expect("broker association shared memory has an invalid size"); Ok(Self { channel, - shared_memory, + shared_buffers, next_request_id: AtomicU64::new(0), }) } @@ -265,7 +263,7 @@ mod tests { let channel = FakeControlChannel::new(None, Some(response.clone())); let local = BrokerLocal { channel, - shared_memory: noop_shared_memory(), + shared_buffers: noop_shared_buffers(), next_request_id: AtomicU64::new(0), }; @@ -285,7 +283,7 @@ mod tests { let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); let local = BrokerLocal { channel, - shared_memory: noop_shared_memory(), + shared_buffers: noop_shared_buffers(), next_request_id: AtomicU64::new(0), }; @@ -321,7 +319,7 @@ mod tests { channel: ConcurrentCallChannel { request_ids: Mutex::new(std::vec::Vec::new()), }, - shared_memory: noop_shared_memory(), + shared_buffers: noop_shared_buffers(), next_request_id: AtomicU64::new(0), }); let callers = (0..16) @@ -347,7 +345,7 @@ mod tests { let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); let local = BrokerLocal { channel, - shared_memory: noop_shared_memory(), + shared_buffers: noop_shared_buffers(), next_request_id: AtomicU64::new(0), }; local.channel.response_id.set(Some(RequestId(9))); @@ -366,7 +364,7 @@ mod tests { let channel = FakeControlChannel::new(None, Some(BrokerResult::ObjectClosed)); let local = BrokerLocal { channel, - shared_memory: noop_shared_memory(), + shared_buffers: noop_shared_buffers(), next_request_id: AtomicU64::new(u64::MAX), }; @@ -383,7 +381,7 @@ mod tests { FakeControlChannel::new(None, Some(BrokerResult::Error(ErrorCode::WouldBlock))); let local = BrokerLocal { channel, - shared_memory: noop_shared_memory(), + shared_buffers: noop_shared_buffers(), next_request_id: AtomicU64::new(0), }; @@ -399,7 +397,7 @@ mod tests { let channel = FakeControlChannel::new(None, Some(BrokerResult::Error(ErrorCode::Internal))); let local = BrokerLocal { channel, - shared_memory: noop_shared_memory(), + shared_buffers: noop_shared_buffers(), next_request_id: AtomicU64::new(0), }; @@ -511,7 +509,7 @@ mod tests { let _ = BrokerLocal::negotiate(channel, |_| { Ok(Arc::new(NoopSharedMemory { - length: PIPE_TRANSFER_BUFFER_SIZE - 1, + length: litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE - 1, }) as Arc) }); } @@ -575,10 +573,14 @@ mod tests { fn noop_shared_memory() -> Arc { Arc::new(NoopSharedMemory { - length: PIPE_TRANSFER_BUFFER_SIZE, + length: litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE, }) } + fn noop_shared_buffers() -> SharedBufferPool> { + SharedBufferPool::new(noop_shared_memory(), SHARED_BUFFER_LAYOUT).unwrap() + } + impl FakeControlChannel { const fn new( handshake_response: Option, diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs index 5559aa34f9..716764040f 100644 --- a/litebox_broker_local/src/pipe.rs +++ b/litebox_broker_local/src/pipe.rs @@ -7,12 +7,15 @@ use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::message::{BrokerOperation, BrokerResult, PipeRequest, PipeResponse}; use litebox_broker_protocol::pipe::{ - CreatePipeRequest, CreatePipeResponse, PIPE_TRANSFER_BUFFER_SIZE, ReadPipeRequest, + CreatePipeRequest, CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE, ReadPipeRequest, WritePipeRequest, }; +use litebox_broker_protocol::shared_memory::SharedBufferSlotIndex; use crate::{BrokerLocal, BrokerLocalError, Result}; +const SERIALIZED_PIPE_SLOT: SharedBufferSlotIndex = SharedBufferSlotIndex(0); + impl BrokerLocal { /// Creates a broker-owned byte pipe. /// @@ -52,7 +55,7 @@ impl BrokerLocal { handle: ObjectHandle, length: u32, ) -> Result, Channel::Error> { - if length as usize > PIPE_TRANSFER_BUFFER_SIZE { + if length > MAX_PIPE_TRANSFER_SIZE { return Err(BrokerLocalError::Broker( litebox_broker_protocol::error::ErrorCode::ResourceExhausted, )); @@ -72,8 +75,8 @@ impl BrokerLocal { ); let read = response.read as usize; data.truncate(read); - self.shared_memory - .read(0, &mut data) + self.shared_buffers + .read(SERIALIZED_PIPE_SLOT, &mut data) .expect("validated shared pipe read range must be accessible"); Ok(data) } @@ -95,13 +98,13 @@ impl BrokerLocal { handle: ObjectHandle, data: &[u8], ) -> Result { - if data.len() > PIPE_TRANSFER_BUFFER_SIZE { + if data.len() > MAX_PIPE_TRANSFER_SIZE as usize { return Err(BrokerLocalError::Broker( litebox_broker_protocol::error::ErrorCode::ResourceExhausted, )); } - self.shared_memory - .write(0, data) + self.shared_buffers + .write(SERIALIZED_PIPE_SLOT, data) .expect("validated shared pipe write range must be accessible"); let response = self.request_pipe(PipeRequest::Write(WritePipeRequest { handle, @@ -149,13 +152,15 @@ mod tests { BrokerResponse, BrokerResult, }; use litebox_broker_protocol::pipe::{ReadPipeResponse, WritePipeResponse}; - use litebox_broker_protocol::shared_memory::{SharedMemory, SharedMemoryError}; + use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, SharedMemory, SharedMemoryError, + }; #[test] - fn pipe_uses_attached_shared_memory_for_data_operations() { + fn pipe_uses_slot_zero_for_serialized_data_operations() { let read_handle = ObjectHandle(1); let write_handle = ObjectHandle(2); - let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); + let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); let channel = ScriptedChannel::new([ BrokerResult::Pipe(PipeResponse::Create(CreatePipeResponse { read_handle, @@ -165,6 +170,9 @@ mod tests { BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2 })), ]); let local = BrokerLocal::negotiate(channel, |_| Ok(memory.clone())).unwrap(); + memory + .write(SHARED_BUFFER_SLOT_SIZE as usize, &[9]) + .unwrap(); local.create_pipe(64, 16).unwrap(); assert_eq!(local.write_pipe(write_handle, &[1, 2, 3]).unwrap(), 2); @@ -174,6 +182,11 @@ mod tests { memory.write(0, &[4, 5, 6]).unwrap(); assert_eq!(local.read_pipe(read_handle, 3).unwrap(), [4, 5]); + let mut second_slot = [0]; + memory + .read(SHARED_BUFFER_SLOT_SIZE as usize, &mut second_slot) + .unwrap(); + assert_eq!(second_slot, [9]); assert_eq!( local.channel.sent_operations.borrow().as_slice(), &[ @@ -195,10 +208,10 @@ mod tests { #[test] fn pipe_rejects_oversized_transfers_before_request() { - let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); + let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); let channel = ScriptedChannel::new([]); let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); - let oversized_length = PIPE_TRANSFER_BUFFER_SIZE + 1; + let oversized_length = MAX_PIPE_TRANSFER_SIZE as usize + 1; assert!(matches!( local.read_pipe(ObjectHandle(1), u32::try_from(oversized_length).unwrap()), @@ -222,7 +235,7 @@ mod tests { ScriptedChannel::new([BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2, }))]); - let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); + let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); let _ = local.read_pipe(ObjectHandle(1), 1); @@ -235,7 +248,7 @@ mod tests { ScriptedChannel::new([BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { written: 2, }))]); - let memory = Arc::new(TestSharedMemory::new(PIPE_TRANSFER_BUFFER_SIZE)); + let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); let _ = local.write_pipe(ObjectHandle(1), &[0]); diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index d347a98b4b..6c6d1bb700 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -12,6 +12,9 @@ extern crate alloc; +#[cfg(test)] +extern crate std; + pub mod channel; pub mod error; pub mod event; diff --git a/litebox_broker_protocol/src/pipe.rs b/litebox_broker_protocol/src/pipe.rs index 512d2e2741..4323bfea49 100644 --- a/litebox_broker_protocol/src/pipe.rs +++ b/litebox_broker_protocol/src/pipe.rs @@ -9,9 +9,6 @@ use crate::ObjectHandle; /// smallest currently supported transport frame. pub const MAX_PIPE_TRANSFER_SIZE: u32 = 32 * 1024; -/// Association shared-memory size required for broker pipe transfers. -pub const PIPE_TRANSFER_BUFFER_SIZE: usize = MAX_PIPE_TRANSFER_SIZE as usize; - /// Request to create a broker-owned byte pipe. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CreatePipeRequest { diff --git a/litebox_broker_protocol/src/shared_memory.rs b/litebox_broker_protocol/src/shared_memory.rs index 96b67df2db..12fc40d70a 100644 --- a/litebox_broker_protocol/src/shared_memory.rs +++ b/litebox_broker_protocol/src/shared_memory.rs @@ -3,8 +3,29 @@ //! Transport-neutral shared-memory resources. +use alloc::sync::Arc; +use core::ops::Range; + use thiserror::Error; +use crate::pipe::MAX_PIPE_TRANSFER_SIZE; + +/// Size of each association shared-buffer slot. +pub const SHARED_BUFFER_SLOT_SIZE: u32 = MAX_PIPE_TRANSFER_SIZE; + +/// Number of slots in one association shared-buffer pool. +pub const SHARED_BUFFER_SLOT_COUNT: u32 = 16; + +/// Fixed layout of one association shared-buffer pool. +pub const SHARED_BUFFER_LAYOUT: SharedBufferLayout = + match SharedBufferLayout::new(SHARED_BUFFER_SLOT_SIZE, SHARED_BUFFER_SLOT_COUNT) { + Ok(layout) => layout, + Err(_) => panic!("broker shared-buffer constants must form a valid layout"), + }; + +/// Exact shared-memory size required for one association shared-buffer pool. +pub const SHARED_BUFFER_POOL_SIZE: usize = SHARED_BUFFER_LAYOUT.total_len(); + /// Error accessing a shared-memory resource. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] #[non_exhaustive] @@ -43,3 +64,271 @@ pub trait SharedMemory: Send + Sync + 'static { /// Copies bytes from `source` into shared memory. fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError>; } + +impl SharedMemory for Arc { + fn len(&self) -> usize { + (**self).len() + } + + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + (**self).read(offset, destination) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + (**self).write(offset, source) + } +} + +/// Error validating or accessing a fixed-slot shared-buffer pool. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum SharedBufferError { + /// The layout has no slots, has empty slots, or exceeds the addressable range. + #[error("invalid shared-buffer layout")] + InvalidLayout, + /// The backing shared-memory length does not exactly match the layout. + #[error("shared-memory length does not match the shared-buffer layout")] + MemoryLengthMismatch, + /// The requested slot does not exist in the layout. + #[error("shared-buffer slot is out of bounds")] + InvalidSlot, + /// The requested byte range does not fit in one slot. + #[error("shared-buffer range exceeds the slot size")] + RangeExceedsSlot, + /// The backing shared-memory access failed. + #[error("shared-memory access failed: {0}")] + SharedMemory(#[from] SharedMemoryError), +} + +/// Immutable fixed-slot layout for an association shared-buffer pool. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SharedBufferLayout { + slot_size: u32, + slot_count: u32, + total_len: usize, +} + +impl SharedBufferLayout { + /// Creates a checked fixed-slot layout. + pub const fn new(slot_size: u32, slot_count: u32) -> Result { + if slot_size == 0 || slot_count == 0 { + return Err(SharedBufferError::InvalidLayout); + } + let Some(total_len) = (slot_size as usize).checked_mul(slot_count as usize) else { + return Err(SharedBufferError::InvalidLayout); + }; + if total_len > isize::MAX as usize { + return Err(SharedBufferError::InvalidLayout); + } + Ok(Self { + slot_size, + slot_count, + total_len, + }) + } + + /// Returns the size of each slot in bytes. + pub const fn slot_size(self) -> u32 { + self.slot_size + } + + /// Returns the number of slots. + pub const fn slot_count(self) -> u32 { + self.slot_count + } + + /// Returns the exact backing-memory length required by this layout. + pub const fn total_len(self) -> usize { + self.total_len + } + + /// Returns the shared-memory range for a prefix of one slot. + pub fn range( + self, + slot: SharedBufferSlotIndex, + length: usize, + ) -> Result, SharedBufferError> { + if slot.0 >= self.slot_count { + return Err(SharedBufferError::InvalidSlot); + } + if length > self.slot_size as usize { + return Err(SharedBufferError::RangeExceedsSlot); + } + let offset = (slot.0 as usize) + .checked_mul(self.slot_size as usize) + .ok_or(SharedBufferError::InvalidLayout)?; + let end = offset + .checked_add(length) + .ok_or(SharedBufferError::RangeExceedsSlot)?; + Ok(offset..end) + } +} + +/// Index of one fixed shared-buffer slot. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SharedBufferSlotIndex(pub u32); + +/// A shared-memory resource viewed as a checked fixed-slot buffer pool. +/// +/// Slot ownership and reuse remain responsibilities of the protocol using the +/// pool. Accessors copy bytes and never expose references into peer-writable +/// memory. +pub struct SharedBufferPool { + memory: Memory, + layout: SharedBufferLayout, +} + +impl SharedBufferPool { + /// Attaches a layout to an exact-size shared-memory resource. + pub fn new(memory: Memory, layout: SharedBufferLayout) -> Result { + if memory.len() != layout.total_len() { + return Err(SharedBufferError::MemoryLengthMismatch); + } + Ok(Self { memory, layout }) + } + + /// Returns the fixed-slot layout. + pub const fn layout(&self) -> SharedBufferLayout { + self.layout + } + + /// Returns the backing shared-memory resource. + pub const fn memory(&self) -> &Memory { + &self.memory + } + + /// Copies bytes from the start of `slot` into `destination`. + pub fn read( + &self, + slot: SharedBufferSlotIndex, + destination: &mut [u8], + ) -> Result<(), SharedBufferError> { + let range = self.layout.range(slot, destination.len())?; + self.memory.read(range.start, destination)?; + Ok(()) + } + + /// Copies `source` into the start of `slot`. + pub fn write( + &self, + slot: SharedBufferSlotIndex, + source: &[u8], + ) -> Result<(), SharedBufferError> { + let range = self.layout.range(slot, source.len())?; + self.memory.write(range.start, source)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + use alloc::vec::Vec; + use std::sync::Mutex; + + #[test] + fn association_layout_has_expected_size() { + assert_eq!(SHARED_BUFFER_LAYOUT.slot_size(), 32 * 1024); + assert_eq!(SHARED_BUFFER_LAYOUT.slot_count(), 16); + assert_eq!(SHARED_BUFFER_POOL_SIZE, 512 * 1024); + } + + #[test] + fn layout_rejects_empty_and_overflowing_configurations() { + assert_eq!( + SharedBufferLayout::new(0, 1), + Err(SharedBufferError::InvalidLayout) + ); + assert_eq!( + SharedBufferLayout::new(1, 0), + Err(SharedBufferError::InvalidLayout) + ); + assert_eq!( + SharedBufferLayout::new(u32::MAX, u32::MAX), + Err(SharedBufferError::InvalidLayout) + ); + } + + #[test] + fn layout_derives_disjoint_slot_ranges() { + let layout = SharedBufferLayout::new(8, 3).unwrap(); + + assert_eq!(layout.range(SharedBufferSlotIndex(0), 8), Ok(0..8)); + assert_eq!(layout.range(SharedBufferSlotIndex(1), 8), Ok(8..16)); + assert_eq!(layout.range(SharedBufferSlotIndex(2), 8), Ok(16..24)); + assert_eq!( + layout.range(SharedBufferSlotIndex(3), 0), + Err(SharedBufferError::InvalidSlot) + ); + assert_eq!( + layout.range(SharedBufferSlotIndex(0), 9), + Err(SharedBufferError::RangeExceedsSlot) + ); + } + + #[test] + fn pool_checks_backing_length_and_slot_boundaries() { + let layout = SharedBufferLayout::new(8, 3).unwrap(); + assert!(matches!( + SharedBufferPool::new(TestSharedMemory::new(23), layout), + Err(SharedBufferError::MemoryLengthMismatch) + )); + let memory = Arc::new(TestSharedMemory::new(layout.total_len())); + let pool = SharedBufferPool::new(Arc::clone(&memory), layout).unwrap(); + + pool.write(SharedBufferSlotIndex(0), &[1, 2, 3]).unwrap(); + pool.write(SharedBufferSlotIndex(2), &[4, 5]).unwrap(); + let mut first = [0; 3]; + pool.read(SharedBufferSlotIndex(0), &mut first).unwrap(); + assert_eq!(first, [1, 2, 3]); + assert_eq!(&memory.bytes()[8..16], &[0; 8]); + assert_eq!( + pool.write(SharedBufferSlotIndex(2), &[0; 9]), + Err(SharedBufferError::RangeExceedsSlot) + ); + } + + struct TestSharedMemory(Mutex>); + + impl TestSharedMemory { + fn new(length: usize) -> Self { + Self(Mutex::new(vec![0; length])) + } + + fn bytes(&self) -> Vec { + self.0.lock().unwrap().clone() + } + } + + impl SharedMemory for TestSharedMemory { + fn len(&self) -> usize { + self.0.lock().unwrap().len() + } + + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + let memory = self.0.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let source = memory + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let mut memory = self.0.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let destination = memory + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } + } +} diff --git a/litebox_broker_transport/src/shared_memory.rs b/litebox_broker_transport/src/shared_memory.rs index 2c826cf0f8..b81de5eb72 100644 --- a/litebox_broker_transport/src/shared_memory.rs +++ b/litebox_broker_transport/src/shared_memory.rs @@ -300,6 +300,9 @@ fn invalid_data(message: &'static str) -> Error { #[cfg(test)] mod tests { use super::*; + use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, SharedBufferSlotIndex, + }; use rustix::io::FdFlags; use std::io::Write; use std::time::Duration; @@ -358,18 +361,30 @@ mod tests { } #[test] - fn transfers_exact_size_memory_with_close_on_exec() { - let length = 24; - let memory = MemfdSharedMemory::create(length).unwrap(); - memory.write(16, &[1, 2, 3]).unwrap(); + fn transfers_exact_pool_with_shared_visibility_and_close_on_exec() { + let memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE).unwrap(); + let pool = SharedBufferPool::new(memory, SHARED_BUFFER_LAYOUT).unwrap(); + for index in 0..SHARED_BUFFER_LAYOUT.slot_count() { + pool.write( + SharedBufferSlotIndex(index), + &[u8::try_from(index).unwrap()], + ) + .unwrap(); + } let (mut local_stream, mut host_stream) = UnixStream::pair().unwrap(); - send_memfd(&mut host_stream, &memory, None).unwrap(); - let mapped_memory = receive_memfd(&mut local_stream, length, None).unwrap(); - let mut bytes = [0; 3]; - mapped_memory.read(16, &mut bytes).unwrap(); - assert_eq!(bytes, [1, 2, 3]); - let flags = rustix::io::fcntl_getfd(mapped_memory.as_fd()).unwrap(); + send_memfd(&mut host_stream, pool.memory(), None).unwrap(); + let mapped_memory = + receive_memfd(&mut local_stream, SHARED_BUFFER_POOL_SIZE, None).unwrap(); + let mapped_pool = SharedBufferPool::new(mapped_memory, SHARED_BUFFER_LAYOUT).unwrap(); + for index in 0..SHARED_BUFFER_LAYOUT.slot_count() { + let mut byte = [0]; + mapped_pool + .read(SharedBufferSlotIndex(index), &mut byte) + .unwrap(); + assert_eq!(byte, [u8::try_from(index).unwrap()]); + } + let flags = rustix::io::fcntl_getfd(mapped_pool.memory().as_fd()).unwrap(); assert!(flags.contains(FdFlags::CLOEXEC)); } @@ -412,9 +427,9 @@ mod tests { #[test] fn rejects_wrong_size_and_unsealed_memory() { - let length = 8; + let length = SHARED_BUFFER_POOL_SIZE; - let wrong_size = MemfdSharedMemory::create(7).unwrap(); + let wrong_size = MemfdSharedMemory::create(length - 1).unwrap(); let (mut receiver, mut sender) = UnixStream::pair().unwrap(); send_memfd(&mut sender, &wrong_size, None).unwrap(); assert_eq!( diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 5d13e25638..f0252c4d42 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -13,7 +13,9 @@ use std::time::{Duration, Instant}; use clap::Parser; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{ConnectionTermination, serve_connection}; -use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; +use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, +}; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, validate_peer_process, @@ -98,7 +100,8 @@ fn serve_runner( setup_deadline, "notification", )?; - let shared_memory = MemfdSharedMemory::create(PIPE_TRANSFER_BUFFER_SIZE)?; + let shared_memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE)?; + let shared_buffers = SharedBufferPool::new(shared_memory, SHARED_BUFFER_LAYOUT)?; let mut control_channel = UnixStreamHostControlChannel::from_host_guaranteed(control_stream, setup_deadline); let mut notification_channel = @@ -108,9 +111,9 @@ fn serve_runner( broker, &mut control_channel, &mut notification_channel, - &shared_memory, + &shared_buffers, |channel| { - channel.send_memfd(&shared_memory, Some(setup_deadline))?; + channel.send_memfd(shared_buffers.memory(), Some(setup_deadline))?; setup_completed.set(true); Ok(()) }, diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index ebb681c98f..8771845f6e 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -7,8 +7,10 @@ use std::sync::Arc; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{ConnectionTermination, serve_connection}; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, +}; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, UnixStreamLocalControlChannel, @@ -22,7 +24,9 @@ fn host_serves_control_requests_over_paired_userland_channels() { .unwrap(); let (local_control, host_control) = UnixStream::pair().unwrap(); let (_local_notification, host_notification) = UnixStream::pair().unwrap(); - let host_shared_memory = MemfdSharedMemory::create(PIPE_TRANSFER_BUFFER_SIZE).unwrap(); + let host_shared_memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE).unwrap(); + let host_shared_buffers = + SharedBufferPool::new(host_shared_memory, SHARED_BUFFER_LAYOUT).unwrap(); let host_thread = std::thread::spawn(move || { let mut control = UnixStreamHostControlChannel::from_accepted(host_control); @@ -31,15 +35,15 @@ fn host_serves_control_requests_over_paired_userland_channels() { &broker, &mut control, &mut notification, - &host_shared_memory, - |channel| channel.send_memfd(&host_shared_memory, None), + &host_shared_buffers, + |channel| channel.send_memfd(host_shared_buffers.memory(), None), ) }); let local = BrokerLocal::negotiate( UnixStreamLocalControlChannel::from_connected(local_control), |channel| { - let shared_memory = channel.receive_memfd(PIPE_TRANSFER_BUFFER_SIZE, None)?; + let shared_memory = channel.receive_memfd(SHARED_BUFFER_POOL_SIZE, None)?; let _cancellation = channel.activate(|| {})?; Ok(Arc::new(shared_memory)) }, diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index f6a95f72b9..c9507d96d7 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -9,8 +9,8 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, }; @@ -84,7 +84,7 @@ fn run_fake_runner(args: &[OsString]) { connect_notification_with_retry(Path::new(notification_socket_path)).unwrap(); let local = BrokerLocal::negotiate(control_channel, |channel| { let shared_memory = channel.receive_memfd( - PIPE_TRANSFER_BUFFER_SIZE, + SHARED_BUFFER_POOL_SIZE, Some(Instant::now() + Duration::from_secs(5)), )?; let _cancellation = channel.activate(|| {})?; diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 8f3adafd21..11ae005ad2 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -13,7 +13,7 @@ use std::{ use anyhow::{Context as _, Result}; use litebox_broker_local::{BrokerLocal, BrokerNotifications}; use litebox_broker_protocol::message::BrokerNotification; -use litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE; +use litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlCancellation, UnixStreamLocalControlChannel, UnixStreamLocalNotificationCancellation, UnixStreamLocalNotificationChannel, @@ -65,7 +65,7 @@ pub(crate) fn connect( let association_coordinator = Arc::clone(&association_coordinator); move |channel| { let shared_memory = - channel.receive_memfd(PIPE_TRANSFER_BUFFER_SIZE, Some(setup_deadline))?; + channel.receive_memfd(SHARED_BUFFER_POOL_SIZE, Some(setup_deadline))?; let weak_association_coordinator = Arc::downgrade(&association_coordinator); let control_cancellation_handle = channel.activate(move || { if let Some(association_coordinator) = weak_association_coordinator.upgrade() { diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 41eeddeae8..6681e45285 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -342,9 +342,14 @@ fn spawn_test_broker( .expect("failed to accept broker local control connection"); let shared_memory = litebox_broker_transport::shared_memory::MemfdSharedMemory::create( - litebox_broker_protocol::pipe::PIPE_TRANSFER_BUFFER_SIZE, + litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE, ) .expect("failed to create broker test shared memory"); + let shared_buffers = litebox_broker_protocol::shared_memory::SharedBufferPool::new( + shared_memory, + litebox_broker_protocol::shared_memory::SHARED_BUFFER_LAYOUT, + ) + .expect("failed to attach broker test shared-buffer layout"); let (notification_stream, _) = notification_listener .accept() .expect("failed to accept broker local notification connection"); @@ -378,8 +383,8 @@ fn spawn_test_broker( &broker, &mut channel, &mut notification_channel, - &shared_memory, - |channel| channel.inner.send_memfd(&shared_memory, None), + &shared_buffers, + |channel| channel.inner.send_memfd(shared_buffers.memory(), None), ) .expect("broker host failed"); assert_eq!( From 106e6250b0c38545f2c6dd4a7c12a204f40bb332 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Wed, 22 Jul 2026 16:57:21 -0700 Subject: [PATCH 117/319] Cherry pick "Hoist VSM/HEKI types and errors into `litebox_common_lvbs`" (#1069) Co-authored-by: Sangho Lee --- Cargo.lock | 15 + Cargo.toml | 2 + litebox_common_lvbs/Cargo.toml | 18 + litebox_common_lvbs/src/lib.rs | 842 ++++++++++++++++++ litebox_platform_lvbs/Cargo.toml | 2 + litebox_platform_lvbs/src/arch/x86/mod.rs | 4 - litebox_platform_lvbs/src/host/linux.rs | 86 +- litebox_platform_lvbs/src/host/lvbs_impl.rs | 4 +- litebox_platform_lvbs/src/host/mod.rs | 2 +- .../src/host/per_cpu_variables.rs | 3 +- litebox_platform_lvbs/src/lib.rs | 7 +- litebox_platform_lvbs/src/mshv/error.rs | 234 ----- litebox_platform_lvbs/src/mshv/heki.rs | 386 +------- litebox_platform_lvbs/src/mshv/hvcall.rs | 48 +- litebox_platform_lvbs/src/mshv/hvcall_mm.rs | 5 +- litebox_platform_lvbs/src/mshv/hvcall_vp.rs | 3 +- .../src/mshv/mem_integrity.rs | 40 +- litebox_platform_lvbs/src/mshv/mod.rs | 60 +- litebox_platform_lvbs/src/mshv/vsm.rs | 22 +- .../src/mshv/vsm_intercept.rs | 4 +- litebox_platform_lvbs/src/mshv/vtl_switch.rs | 6 +- litebox_runner_lvbs/Cargo.toml | 1 + litebox_runner_lvbs/src/lib.rs | 3 +- 23 files changed, 925 insertions(+), 872 deletions(-) create mode 100644 litebox_common_lvbs/Cargo.toml create mode 100644 litebox_common_lvbs/src/lib.rs delete mode 100644 litebox_platform_lvbs/src/mshv/error.rs diff --git a/Cargo.lock b/Cargo.lock index 8587bd2dea..616362318a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1548,6 +1548,19 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "litebox_common_lvbs" +version = "0.1.0" +dependencies = [ + "bitflags 2.13.1", + "litebox", + "litebox_common_linux", + "num_enum", + "thiserror", + "x86_64", + "zerocopy", +] + [[package]] name = "litebox_common_optee" version = "0.1.0" @@ -1639,6 +1652,7 @@ dependencies = [ "libc", "litebox", "litebox_common_linux", + "litebox_common_lvbs", "litebox_util_log", "modular-bitfield", "num_enum", @@ -1730,6 +1744,7 @@ dependencies = [ "arrayvec", "litebox", "litebox_common_linux", + "litebox_common_lvbs", "litebox_common_optee", "litebox_platform_lvbs", "litebox_platform_multiplex", diff --git a/Cargo.toml b/Cargo.toml index 26ae2d6760..7556df176e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", + "litebox_common_lvbs", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", "litebox_platform_windows_userland", @@ -45,6 +46,7 @@ default-members = [ "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", + "litebox_common_lvbs", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", "litebox_platform_windows_userland", diff --git a/litebox_common_lvbs/Cargo.toml b/litebox_common_lvbs/Cargo.toml new file mode 100644 index 0000000000..b3ac14e949 --- /dev/null +++ b/litebox_common_lvbs/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "litebox_common_lvbs" +version = "0.1.0" +edition = "2024" + +[dependencies] +bitflags = "2.9.0" +litebox = { path = "../litebox/", version = "0.1.0" } +litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } +num_enum = { version = "0.7.3", default-features = false } +thiserror = { version = "2.0.6", default-features = false } +zerocopy = { version = "0.8", default-features = false, features = ["derive"] } + +[target.'cfg(target_arch = "x86_64")'.dependencies] +x86_64 = { version = "0.15.2", default-features = false, features = ["instructions"] } + +[lints] +workspace = true diff --git a/litebox_common_lvbs/src/lib.rs b/litebox_common_lvbs/src/lib.rs new file mode 100644 index 0000000000..daf0ef858a --- /dev/null +++ b/litebox_common_lvbs/src/lib.rs @@ -0,0 +1,842 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Shared VSM/HEKI wire types and constants for the LVBS platform, service, and runner. + +#![cfg(target_arch = "x86_64")] +#![no_std] + +extern crate alloc; + +use core::mem; +use litebox::utils::TruncateExt; +use litebox_common_linux::errno::Errno; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use thiserror::Error; +use x86_64::{ + PhysAddr, VirtAddr, + structures::paging::{PageSize, Size4KiB}, +}; +use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout}; + +pub const PAGE_SIZE: usize = 4096; +pub const PAGE_SHIFT: usize = 12; + +/// Length of the Platform Root Key in bytes. +pub const PRK_LEN: usize = 32; + +/// Maximum number of CPU cores addressable through the VTL0 `cpu_online_mask` +/// ABI. Bounds how many bits of the mask VTL1 will honor when booting APs. +pub const MAX_CORES: usize = 128; + +/// VTL call parameters (`param[0]`: function ID, `param[1..4]`: parameters) +pub const NUM_VTLCALL_PARAMS: usize = 4; + +pub const VSM_VTL_CALL_FUNC_ID_ENABLE_APS_VTL: u32 = 0x1_ffe0; +pub const VSM_VTL_CALL_FUNC_ID_BOOT_APS: u32 = 0x1_ffe1; +pub const VSM_VTL_CALL_FUNC_ID_LOCK_REGS: u32 = 0x1_ffe2; +pub const VSM_VTL_CALL_FUNC_ID_SIGNAL_END_OF_BOOT: u32 = 0x1_ffe3; +pub const VSM_VTL_CALL_FUNC_ID_PROTECT_MEMORY: u32 = 0x1_ffe4; +pub const VSM_VTL_CALL_FUNC_ID_LOAD_KDATA: u32 = 0x1_ffe5; +pub const VSM_VTL_CALL_FUNC_ID_VALIDATE_MODULE: u32 = 0x1_ffe6; +pub const VSM_VTL_CALL_FUNC_ID_FREE_MODULE_INIT: u32 = 0x1_ffe7; +pub const VSM_VTL_CALL_FUNC_ID_UNLOAD_MODULE: u32 = 0x1_ffe8; +pub const VSM_VTL_CALL_FUNC_ID_COPY_SECONDARY_KEY: u32 = 0x1_ffe9; +pub const VSM_VTL_CALL_FUNC_ID_KEXEC_VALIDATE: u32 = 0x1_ffea; +pub const VSM_VTL_CALL_FUNC_ID_PATCH_TEXT: u32 = 0x1_ffeb; +pub const VSM_VTL_CALL_FUNC_ID_ALLOCATE_RINGBUFFER_MEMORY: u32 = 0x1_ffec; + +// This VSM function ID for setting the platform root key is subject to change +pub const VSM_VTL_CALL_FUNC_ID_SET_PLATFORM_ROOT_KEY: u32 = 0x1_ffed; + +// This VSM function ID for OP-TEE messages is subject to change +pub const VSM_VTL_CALL_FUNC_ID_OPTEE_MESSAGE: u32 = 0x1_fff0; + +/// VSM Functions +#[derive(Debug, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum VsmFunction { + // VSM/Heki functions + EnableAPsVtl = VSM_VTL_CALL_FUNC_ID_ENABLE_APS_VTL, + BootAPs = VSM_VTL_CALL_FUNC_ID_BOOT_APS, + LockRegs = VSM_VTL_CALL_FUNC_ID_LOCK_REGS, + SignalEndOfBoot = VSM_VTL_CALL_FUNC_ID_SIGNAL_END_OF_BOOT, + ProtectMemory = VSM_VTL_CALL_FUNC_ID_PROTECT_MEMORY, + LoadKData = VSM_VTL_CALL_FUNC_ID_LOAD_KDATA, + ValidateModule = VSM_VTL_CALL_FUNC_ID_VALIDATE_MODULE, + FreeModuleInit = VSM_VTL_CALL_FUNC_ID_FREE_MODULE_INIT, + UnloadModule = VSM_VTL_CALL_FUNC_ID_UNLOAD_MODULE, + CopySecondaryKey = VSM_VTL_CALL_FUNC_ID_COPY_SECONDARY_KEY, + KexecValidate = VSM_VTL_CALL_FUNC_ID_KEXEC_VALIDATE, + PatchText = VSM_VTL_CALL_FUNC_ID_PATCH_TEXT, + OpteeMessage = VSM_VTL_CALL_FUNC_ID_OPTEE_MESSAGE, + AllocateRingbufferMemory = VSM_VTL_CALL_FUNC_ID_ALLOCATE_RINGBUFFER_MEMORY, + SetPlatformRootKey = VSM_VTL_CALL_FUNC_ID_SET_PLATFORM_ROOT_KEY, +} + +// `HV_STATUS_*` constants used as discriminants for `HypervCallError`. +pub const HV_STATUS_INVALID_HYPERCALL_CODE: u32 = 2; +pub const HV_STATUS_INVALID_HYPERCALL_INPUT: u32 = 3; +pub const HV_STATUS_INVALID_ALIGNMENT: u32 = 4; +pub const HV_STATUS_INVALID_PARAMETER: u32 = 5; +pub const HV_STATUS_ACCESS_DENIED: u32 = 6; +pub const HV_STATUS_OPERATION_DENIED: u32 = 8; +pub const HV_STATUS_INSUFFICIENT_MEMORY: u32 = 11; +pub const HV_STATUS_INVALID_PORT_ID: u32 = 17; +pub const HV_STATUS_INVALID_CONNECTION_ID: u32 = 18; +pub const HV_STATUS_INSUFFICIENT_BUFFERS: u32 = 19; +pub const HV_STATUS_TIME_OUT: u32 = 120; +pub const HV_STATUS_VTL_ALREADY_ENABLED: u32 = 134; + +/// Errors for Hyper-V hypercalls. +#[derive(Debug, Error, TryFromPrimitive, IntoPrimitive)] +#[non_exhaustive] +#[repr(u32)] +pub enum HypervCallError { + #[error("invalid hypercall code")] + InvalidCode = HV_STATUS_INVALID_HYPERCALL_CODE, + #[error("invalid hypercall input")] + InvalidInput = HV_STATUS_INVALID_HYPERCALL_INPUT, + #[error("invalid alignment")] + InvalidAlignment = HV_STATUS_INVALID_ALIGNMENT, + #[error("invalid parameter")] + InvalidParameter = HV_STATUS_INVALID_PARAMETER, + #[error("access denied")] + AccessDenied = HV_STATUS_ACCESS_DENIED, + #[error("operation denied")] + OperationDenied = HV_STATUS_OPERATION_DENIED, + #[error("insufficient memory")] + InsufficientMemory = HV_STATUS_INSUFFICIENT_MEMORY, + #[error("invalid port ID")] + InvalidPortID = HV_STATUS_INVALID_PORT_ID, + #[error("invalid connection ID")] + InvalidConnectionID = HV_STATUS_INVALID_CONNECTION_ID, + #[error("insufficient buffers")] + InsufficientBuffers = HV_STATUS_INSUFFICIENT_BUFFERS, + #[error("timeout")] + TimeOut = HV_STATUS_TIME_OUT, + #[error("VTL already enabled")] + AlreadyEnabled = HV_STATUS_VTL_ALREADY_ENABLED, + #[error("unknown hypercall error")] + Unknown = 0xffff_ffff, +} + +/// Errors for module signature verification. +#[derive(Debug, Error, PartialEq)] +#[non_exhaustive] +pub enum VerificationError { + #[error("signature not found in module")] + SignatureNotFound, + #[error("invalid signature format")] + InvalidSignature, + #[error("invalid certificate")] + InvalidCertificate, + #[error("signature authentication failed")] + AuthenticationFailed, + #[error("failed to parse signature data")] + ParseFailed, + #[error("unsupported signature algorithm")] + Unsupported, +} + +impl From for Errno { + fn from(e: VerificationError) -> Self { + match e { + VerificationError::AuthenticationFailed => Errno::EKEYREJECTED, + VerificationError::SignatureNotFound => Errno::ENODATA, + VerificationError::Unsupported => Errno::ENOPKG, + VerificationError::InvalidCertificate => Errno::ENOKEY, + VerificationError::InvalidSignature | VerificationError::ParseFailed => Errno::ELIBBAD, + } + } +} + +/// Errors for Virtual Secure Mode (VSM) operations. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum VsmError { + // Boot/AP Initialization Errors + #[error("failed to copy boot signal page from VTL0")] + BootSignalPageCopyFailed, + + #[error("failed to initialize AP: {0:?}")] + ApInitFailed(HypervCallError), + + #[error("failed to copy boot signal page to VTL0")] + BootSignalWriteFailed, + + #[error("failed to copy cpu_online_mask from VTL0")] + CpuOnlineMaskCopyFailed, + + #[error("code page offset overflow when computing VTL return address")] + CodePageOffsetOverflow, + + #[error("integer overflow while processing VTL0-controlled range data")] + IntegerOverflow, + + // End-of-Boot Restriction Errors + #[error("{0} not allowed after end of boot")] + OperationAfterEndOfBoot(&'static str), + + // Address Validation Errors + #[error("invalid input address")] + InvalidInputAddress, + + #[error("address must be page-aligned")] + AddressNotPageAligned, + + #[error("invalid physical address")] + InvalidPhysicalAddress, + + // Memory/Data Errors + #[error("invalid memory attributes")] + MemoryAttributeInvalid, + + #[error("failed to copy HEKI pages from VTL0")] + HekiPagesCopyFailed, + + #[error("invalid kernel data type")] + KernelDataTypeInvalid, + + #[error("invalid module memory type")] + ModuleMemoryTypeInvalid, + + // Certificate Errors + #[error("system certificates not loaded")] + SystemCertificatesNotLoaded, + + #[error("no system certificate found in kernel data")] + SystemCertificatesNotFound, + + #[error("no valid system certificates parsed")] + SystemCertificatesInvalid, + + #[error("invalid DER certificate data (expected {expected} bytes, got {actual})")] + CertificateDerLengthInvalid { expected: usize, actual: usize }, + + #[error("failed to parse certificate")] + CertificateParseFailed, + + // Module Validation Errors + #[error("module ELF size ({size} bytes) exceeds maximum allowed ({max} bytes)")] + ModuleElfSizeExceeded { size: usize, max: usize }, + + #[error("found unexpected relocations in loaded module")] + ModuleRelocationInvalid, + + #[error("invalid module token")] + ModuleTokenInvalid, + + #[error("physical frames overlap already-protected or reserved memory")] + ProtectedFrameOverlap, + + // Kernel Symbol Table Errors + #[error("no kernel symbol table found")] + KernelSymbolTableNotFound, + + // Kexec Errors + #[error("invalid kexec type")] + KexecTypeInvalid, + + #[error("invalid kexec image segments")] + KexecImageSegmentsInvalid, + + #[error("invalid kexec segment memory range")] + KexecSegmentRangeInvalid, + + // Patch Errors + #[error("precomputed patch data not found")] + PrecomputedPatchNotFound, + + #[error("text patch validation failed")] + TextPatchSuspicious, + + // Unsupported Operation Errors + #[error("{0} is not supported")] + OperationNotSupported(&'static str), + + // VTL0 Memory Copy Errors + #[error("failed to copy data from/to VTL0")] + Vtl0CopyFailed, + + // Hypercall Errors + #[error("hypercall failed: {0:?}")] + HypercallFailed(HypervCallError), + + // Signature Verification Errors + #[error("signature verification failed: {0:?}")] + SignatureVerificationFailed(VerificationError), + + // Data Parsing Errors + #[error("buffer too small for {0}")] + BufferTooSmall(&'static str), + + // Address/Memory Range Errors + #[error("invalid virtual address")] + InvalidVirtualAddress, + + #[error("discontiguous memory range")] + DiscontiguousMemoryRange, + + // Symbol Table Errors + #[error("symbol table data empty")] + SymbolTableEmpty, + + #[error("symbol table data out of range")] + SymbolTableOutOfRange, + + #[error("symbol table length not aligned to symbol size")] + SymbolTableLengthInvalid, + + #[error("failed to parse symbol at offset {0:#x}")] + SymbolParseFailed(usize), + + #[error("symbol name offset out of bounds")] + SymbolNameOffsetInvalid, + + #[error("symbol name missing NUL terminator")] + SymbolNameNoTerminator, + + #[error("symbol name exceeds maximum length")] + SymbolNameTooLong, + + #[error("symbol name contains invalid UTF-8")] + SymbolNameInvalidUtf8, +} + +impl From for VsmError { + fn from(e: VerificationError) -> Self { + VsmError::SignatureVerificationFailed(e) + } +} + +impl From for Errno { + fn from(e: VsmError) -> Self { + match e { + // Address/pointer errors and memory copy failures - memory access fault + VsmError::InvalidInputAddress + | VsmError::InvalidPhysicalAddress + | VsmError::InvalidVirtualAddress + | VsmError::DiscontiguousMemoryRange + | VsmError::BootSignalPageCopyFailed + | VsmError::BootSignalWriteFailed + | VsmError::CpuOnlineMaskCopyFailed + | VsmError::HekiPagesCopyFailed + | VsmError::Vtl0CopyFailed => Errno::EFAULT, + + // Not found errors + VsmError::SystemCertificatesNotFound + | VsmError::KernelSymbolTableNotFound + | VsmError::PrecomputedPatchNotFound => Errno::ENOENT, + + // Operation not permitted after end of boot + VsmError::OperationAfterEndOfBoot(_) => Errno::EPERM, + + // Unsupported operation + VsmError::OperationNotSupported(_) => Errno::ENOTSUP, + + // Security/verification failures - access denied + VsmError::TextPatchSuspicious + | VsmError::SystemCertificatesInvalid + | VsmError::SystemCertificatesNotLoaded => Errno::EACCES, + + // Size/range errors + VsmError::BufferTooSmall(_) + | VsmError::KexecSegmentRangeInvalid + | VsmError::ModuleElfSizeExceeded { .. } + | VsmError::CodePageOffsetOverflow + | VsmError::IntegerOverflow + | VsmError::SymbolNameTooLong + | VsmError::SymbolTableOutOfRange => Errno::ERANGE, + + // Init/hardware failures - I/O error + VsmError::ApInitFailed(_) | VsmError::HypercallFailed(_) => Errno::EIO, + + // True format/validation errors - invalid argument + VsmError::AddressNotPageAligned + | VsmError::MemoryAttributeInvalid + | VsmError::KernelDataTypeInvalid + | VsmError::ModuleMemoryTypeInvalid + | VsmError::ModuleRelocationInvalid + | VsmError::ModuleTokenInvalid + | VsmError::ProtectedFrameOverlap + | VsmError::KexecTypeInvalid + | VsmError::KexecImageSegmentsInvalid + | VsmError::SymbolTableEmpty + | VsmError::SymbolTableLengthInvalid + | VsmError::SymbolParseFailed(_) + | VsmError::SymbolNameOffsetInvalid + | VsmError::SymbolNameInvalidUtf8 + | VsmError::SymbolNameNoTerminator + | VsmError::CertificateDerLengthInvalid { .. } + | VsmError::CertificateParseFailed => Errno::EINVAL, + + // Signature verification failures delegate to VerificationError's Errno mapping + VsmError::SignatureVerificationFailed(e) => Errno::from(e), + } + } +} + +/// `list_head` from [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/include/linux/types.h#L190) +/// Pointer fields stored as u64 since we don't dereference them. +#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct ListHead { + pub next: u64, + pub prev: u64, +} + +#[allow(non_camel_case_types)] +pub type __be32 = u32; + +#[repr(u8)] +pub enum PkeyIdType { + PkeyIdPgp = 0, + PkeyIdX509 = 1, + PkeyIdPkcs7 = 2, +} + +/// `module_signature` from [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/include/linux/module_signature.h#L33) +#[repr(C)] +#[derive(Debug, Clone, Copy, FromBytes, Immutable, KnownLayout)] +pub struct ModuleSignature { + pub algo: u8, + pub hash: u8, + pub id_type: u8, + pub signer_len: u8, + pub key_id_len: u8, + _pad: [u8; 3], + sig_len: __be32, +} + +impl ModuleSignature { + pub fn sig_len(&self) -> u32 { + u32::from_be(self.sig_len) + } + + /// Currently, Linux kernel only supports PKCS#7 signatures for module signing and thus `id_type` is always `PkeyIdType::PkeyIdPkcs7`. + /// Other fields except for `sig_len` are set to zero. + pub fn is_valid(&self) -> bool { + self.sig_len() > 0 + && self.algo == 0 + && self.hash == 0 + && self.id_type == PkeyIdType::PkeyIdPkcs7 as u8 + && self.signer_len == 0 + && self.key_id_len == 0 + } +} + +/// `kexec_segment` from [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/include/linux/kexec.h#L82) +#[repr(C)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct KexecSegment { + /// Pointer to buffer (stored as u64 since we don't dereference it) + pub buf: u64, + pub bufsz: u64, + pub mem: u64, + pub memsz: u64, +} + +/// `kimage` from [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/include/linux/kexec.h#L296) +/// Note that this is a part of the original `kimage` structure. It only contains some fields that +/// we need for our use case, such as `nr_segments` and `segment`, and +/// are not affected by the kernel build configurations like `CONFIG_KEXEC_FILE` and `CONFIG_IMA_KEXEC`. +#[repr(C)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] +pub struct Kimage { + head: u64, + /// Pointer fields stored as u64 since we don't dereference them + entry: u64, + last_entry: u64, + start: u64, + control_code_page: u64, // struct page* + swap_page: u64, // struct page* + vmcoreinfo_page: u64, // struct page* + vmcoreinfo_data_copy: u64, + pub nr_segments: u64, + pub segment: [KexecSegment; KEXEC_SEGMENT_MAX], + // we do not need the rest of the fields for now +} +pub const KEXEC_SEGMENT_MAX: usize = 16; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, PartialEq)] + pub struct MemAttr: u64 { + const MEM_ATTR_READ = 1 << 0; + const MEM_ATTR_WRITE = 1 << 1; + const MEM_ATTR_EXEC = 1 << 2; + const MEM_ATTR_IMMUTABLE = 1 << 3; + + const _ = !0; + } +} + +#[derive(Default, Debug, TryFromPrimitive, PartialEq)] +#[repr(u64)] +pub enum HekiKdataType { + SystemCerts = 0, + RevocationCerts = 1, + BlocklistHashes = 2, + KernelInfo = 3, + KernelData = 4, + PatchInfo = 5, + KexecTrampoline = 6, + #[default] + Unknown = 0xffff_ffff_ffff_ffff, +} + +#[derive(Default, Debug, TryFromPrimitive, PartialEq)] +#[repr(u64)] +pub enum HekiKexecType { + KexecImage = 0, + KexecKernelBlob = 1, + KexecPages = 2, + #[default] + Unknown = 0xffff_ffff_ffff_ffff, +} + +#[derive(Clone, Copy, Default, Debug, TryFromPrimitive, PartialEq)] +#[repr(u64)] +pub enum ModMemType { + Text = 0, + Data = 1, + RoData = 2, + RoAfterInit = 3, + InitText = 4, + InitData = 5, + InitRoData = 6, + ElfBuffer = 7, + Patch = 8, + #[default] + Unknown = 0xffff_ffff_ffff_ffff, +} + +/// Maps a module memory-type to the corresponding [`MemAttr`] permission set. +pub fn mod_mem_type_to_mem_attr(mod_mem_type: ModMemType) -> MemAttr { + let mut mem_attr = MemAttr::empty(); + + match mod_mem_type { + ModMemType::Text | ModMemType::InitText => { + mem_attr.set(MemAttr::MEM_ATTR_READ, true); + mem_attr.set(MemAttr::MEM_ATTR_EXEC, true); + } + ModMemType::Data | ModMemType::RoAfterInit | ModMemType::InitData => { + mem_attr.set(MemAttr::MEM_ATTR_READ, true); + mem_attr.set(MemAttr::MEM_ATTR_WRITE, true); + } + ModMemType::RoData | ModMemType::InitRoData => { + mem_attr.set(MemAttr::MEM_ATTR_READ, true); + } + _ => {} + } + + mem_attr +} + +/// `HekiRange` is a generic container for various types of memory ranges. +/// It has an `attributes` field which can be interpreted differently based on the context like +/// `MemAttr`, `KdataType`, `ModMemType`, or `KexecType`. +#[derive(Default, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C, packed)] +pub struct HekiRange { + pub va: u64, + pub pa: u64, + pub epa: u64, + pub attributes: u64, +} + +impl HekiRange { + #[inline] + pub fn is_aligned(&self, align: U) -> bool + where + U: Into + Copy, + { + let va = self.va; + let pa = self.pa; + let epa = self.epa; + + VirtAddr::new(va).is_aligned(align) + && PhysAddr::new(pa).is_aligned(align) + && PhysAddr::new(epa).is_aligned(align) + } + + #[inline] + pub fn mem_attr(&self) -> Option { + let attr = self.attributes; + MemAttr::from_bits(attr) + } + + #[inline] + pub fn mod_mem_type(&self) -> ModMemType { + let attr = self.attributes; + ModMemType::try_from(attr).unwrap_or(ModMemType::Unknown) + } + + #[inline] + pub fn heki_kdata_type(&self) -> HekiKdataType { + let attr = self.attributes; + HekiKdataType::try_from(attr).unwrap_or(HekiKdataType::Unknown) + } + + #[inline] + pub fn heki_kexec_type(&self) -> HekiKexecType { + let attr = self.attributes; + HekiKexecType::try_from(attr).unwrap_or(HekiKexecType::Unknown) + } + + pub fn is_valid(&self) -> bool { + let va = self.va; + let pa = self.pa; + let epa = self.epa; + let Ok(pa) = PhysAddr::try_new(pa) else { + return false; + }; + let Ok(epa) = PhysAddr::try_new(epa) else { + return false; + }; + !(VirtAddr::try_new(va).is_err() + || epa < pa + || (self.mem_attr().is_none() + && self.heki_kdata_type() == HekiKdataType::Unknown + && self.heki_kexec_type() == HekiKexecType::Unknown + && self.mod_mem_type() == ModMemType::Unknown)) + } +} + +impl core::fmt::Debug for HekiRange { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let va = self.va; + let pa = self.pa; + let epa = self.epa; + let attr = self.attributes; + f.debug_struct("HekiRange") + .field("va", &format_args!("{va:#x}")) + .field("pa", &format_args!("{pa:#x}")) + .field("epa", &format_args!("{epa:#x}")) + .field("attr", &format_args!("{attr:#x}")) + .field("type", &format_args!("{:?}", self.heki_kdata_type())) + .field("size", &format_args!("{:?}", self.epa - self.pa)) + .finish() + } +} + +#[expect(clippy::cast_possible_truncation)] +pub const HEKI_MAX_RANGES: usize = + ((PAGE_SIZE as u32 - u64::BITS * 3 / 8) / core::mem::size_of::() as u32) as usize; + +#[derive(Clone, Copy, FromBytes, Immutable, KnownLayout)] +#[repr(align(4096))] +#[repr(C)] +pub struct HekiPage { + /// Pointer to next page (stored as u64 since we don't dereference it) + pub next: u64, + pub next_pa: u64, + pub nranges: u64, + pub ranges: [HekiRange; HEKI_MAX_RANGES], + pad: u64, +} + +impl HekiPage { + pub fn new() -> Self { + // Safety: all fields are valid when zeroed (u64 zeros, array of zeroed HekiRange) + Self::new_zeroed() + } + + pub fn is_valid(&self) -> bool { + if PhysAddr::try_new(self.next_pa) + .ok() + .is_none_or(|next_pa| self.next_pa != 0 && !next_pa.is_aligned(Size4KiB::SIZE)) + { + return false; + } + let Some(nranges) = usize::try_from(self.nranges) + .ok() + .filter(|&n| (1..=HEKI_MAX_RANGES).contains(&n)) + else { + return false; + }; + for heki_range in &self.ranges[..nranges] { + if !heki_range.is_valid() { + return false; + } + } + true + } +} + +impl Default for HekiPage { + fn default() -> Self { + Self::new_zeroed() + } +} + +impl HekiPage { + /// Returns an iterator over the valid `HekiRange`s in this page. + pub fn iter(&self) -> core::slice::Iter<'_, HekiRange> { + self.ranges[..usize::try_from(self.nranges).unwrap_or(0)].iter() + } +} + +impl<'a> IntoIterator for &'a HekiPage { + type Item = &'a HekiRange; + type IntoIter = core::slice::Iter<'a, HekiRange>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +#[derive(Default, Clone, Copy, Debug, FromBytes, IntoBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct HekiPatch { + pub pa: [u64; 2], + pub size: u8, + pub code: [u8; POKE_MAX_OPCODE_SIZE], + _padding: [u8; 2], +} +pub const POKE_MAX_OPCODE_SIZE: usize = 5; + +impl HekiPatch { + /// Creates a new `HekiPatch` with a given buffer. Returns `None` if any field is invalid. + pub fn try_from_bytes(bytes: &[u8]) -> Option { + let patch = Self::read_from_bytes(bytes).ok()?; + if patch.is_valid() { Some(patch) } else { None } + } + + pub fn is_valid(&self) -> bool { + let Some(pa_0) = PhysAddr::try_new(self.pa[0]) + .ok() + .filter(|&pa| !pa.is_null()) + else { + return false; + }; + let Some(pa_1) = PhysAddr::try_new(self.pa[1]) + .ok() + .filter(|&pa| pa.is_null() || pa.is_aligned(Size4KiB::SIZE)) + else { + return false; + }; + let bytes_in_first_page = if pa_0.is_aligned(Size4KiB::SIZE) { + core::cmp::min(PAGE_SIZE, usize::from(self.size)) + } else { + core::cmp::min( + (pa_0.align_up(Size4KiB::SIZE) - pa_0).trunc(), + usize::from(self.size), + ) + }; + + !(self.size == 0 + || usize::from(self.size) > POKE_MAX_OPCODE_SIZE + || (pa_0 == pa_1) + || (bytes_in_first_page < usize::from(self.size) && pa_1.is_null()) + || (bytes_in_first_page == usize::from(self.size) && !pa_1.is_null())) + } +} + +#[derive(Default, Clone, Copy, Debug, PartialEq)] +#[repr(u32)] +pub enum HekiPatchType { + JumpLabel = 0, + #[default] + Unknown = 0xffff_ffff, +} + +#[derive(Clone, Copy, Debug, FromBytes, Immutable, KnownLayout)] +#[repr(C)] +pub struct HekiPatchInfo { + /// Patch type stored as u32 for zerocopy compatibility (see `HekiPatchType`) + pub typ_: u32, + list: ListHead, + /// *const `struct module` (stored as u64 since we don't dereference it) + mod_: u64, + pub patch_index: u64, + pub max_patch_count: u64, + // pub patch: [HekiPatch; *] +} + +impl HekiPatchInfo { + /// Creates a new `HekiPatchInfo` with a given buffer. Returns `None` if any field is invalid. + pub fn try_from_bytes(bytes: &[u8]) -> Option { + let info = Self::read_from_bytes(bytes).ok()?; + if info.is_valid() { Some(info) } else { None } + } + + pub fn is_valid(&self) -> bool { + !(self.typ_ != HekiPatchType::JumpLabel as u32 + || self.patch_index == 0 + || self.patch_index > self.max_patch_count) + } +} + +#[repr(C)] +#[allow(clippy::struct_field_names)] +// TODO: Account for kernel config changing the size and meaning of the field members +pub struct HekiKernelSymbol { + pub value_offset: core::ffi::c_int, + pub name_offset: core::ffi::c_int, + pub namespace_offset: core::ffi::c_int, +} + +impl HekiKernelSymbol { + pub const KSYM_LEN: usize = mem::size_of::(); + pub const KSY_NAME_LEN: usize = 512; + + /// # Panics + /// + /// Panics if the input buffer is not aligned to `HekiKernelSymbol`. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < Self::KSYM_LEN { + return Err(VsmError::BufferTooSmall("HekiKernelSymbol")); + } + + #[allow(clippy::cast_ptr_alignment)] + let ksym_ptr = bytes.as_ptr().cast::(); + assert!(ksym_ptr.is_aligned(), "ksym_ptr is not aligned"); + + // SAFETY: Casting from vtl0 buffer that contained the struct + unsafe { + Ok(HekiKernelSymbol { + value_offset: (*ksym_ptr).value_offset, + name_offset: (*ksym_ptr).name_offset, + namespace_offset: (*ksym_ptr).namespace_offset, + }) + } + } +} + +#[repr(C)] +#[allow(clippy::struct_field_names)] +pub struct HekiKernelInfo { + pub ksymtab_start: *const HekiKernelSymbol, + pub ksymtab_end: *const HekiKernelSymbol, + pub ksymtab_gpl_start: *const HekiKernelSymbol, + pub ksymtab_gpl_end: *const HekiKernelSymbol, + // Skip unused arch info +} + +impl HekiKernelInfo { + const KINFO_LEN: usize = mem::size_of::(); + + /// # Panics + /// + /// Panics if the input buffer is not aligned to `HekiKernelInfo`. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < Self::KINFO_LEN { + return Err(VsmError::BufferTooSmall("HekiKernelInfo")); + } + + #[allow(clippy::cast_ptr_alignment)] + let kinfo_ptr = bytes.as_ptr().cast::(); + assert!(kinfo_ptr.is_aligned(), "kinfo_ptr is not aligned"); + + // SAFETY: Casting from vtl0 buffer that contained the struct + unsafe { + Ok(HekiKernelInfo { + ksymtab_start: (*kinfo_ptr).ksymtab_start, + ksymtab_end: (*kinfo_ptr).ksymtab_end, + ksymtab_gpl_start: (*kinfo_ptr).ksymtab_gpl_start, + ksymtab_gpl_end: (*kinfo_ptr).ksymtab_gpl_end, + }) + } + } +} diff --git a/litebox_platform_lvbs/Cargo.toml b/litebox_platform_lvbs/Cargo.toml index 466891af38..a4d9cf5600 100644 --- a/litebox_platform_lvbs/Cargo.toml +++ b/litebox_platform_lvbs/Cargo.toml @@ -8,6 +8,8 @@ edition = "2024" bitflags = "2.9.0" litebox = { path = "../litebox/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } +litebox_common_lvbs = { path = "../litebox_common_lvbs/", version = "0.1.0" } +litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } spin = { version = "0.10.0", default-features = false, features = [ "spin_mutex", "once", diff --git a/litebox_platform_lvbs/src/arch/x86/mod.rs b/litebox_platform_lvbs/src/arch/x86/mod.rs index 26b7446581..262296b0b8 100644 --- a/litebox_platform_lvbs/src/arch/x86/mod.rs +++ b/litebox_platform_lvbs/src/arch/x86/mod.rs @@ -42,10 +42,6 @@ pub fn enable_fsgsbase() { } } -/// The maximum number of supported CPU cores. It depends on the number of VCPUs that -/// Hyper-V supports. We set it to 128 for now. -pub const MAX_CORES: usize = 128; - /// Enable CPU extended states such as XMM and instructions to use and manage them /// such as SSE and XSAVE /// diff --git a/litebox_platform_lvbs/src/host/linux.rs b/litebox_platform_lvbs/src/host/linux.rs index 44dcb6cd7a..58c8811b36 100644 --- a/litebox_platform_lvbs/src/host/linux.rs +++ b/litebox_platform_lvbs/src/host/linux.rs @@ -3,8 +3,8 @@ //! Linux Structs -use crate::arch::MAX_CORES; -use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; +use litebox_common_lvbs::MAX_CORES; +use zerocopy::{FromBytes, Immutable, KnownLayout}; /// Context saved when entering the kernel /// @@ -91,85 +91,3 @@ impl CpuMask { } } } - -#[allow(non_camel_case_types)] -pub type __be32 = u32; - -#[repr(u8)] -pub enum PkeyIdType { - PkeyIdPgp = 0, - PkeyIdX509 = 1, - PkeyIdPkcs7 = 2, -} - -/// `module_signature` from [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/include/linux/module_signature.h#L33) -#[repr(C)] -#[derive(Debug, Clone, Copy, FromBytes, Immutable, KnownLayout)] -pub struct ModuleSignature { - pub algo: u8, - pub hash: u8, - pub id_type: u8, - pub signer_len: u8, - pub key_id_len: u8, - _pad: [u8; 3], - sig_len: __be32, -} - -impl ModuleSignature { - pub fn sig_len(&self) -> u32 { - u32::from_be(self.sig_len) - } - - /// Currently, Linux kernel only supports PKCS#7 signatures for module signing and thus `id_type` is always `PkeyIdType::PkeyIdPkcs7`. - /// Other fields except for `sig_len` are set to zero. - pub fn is_valid(&self) -> bool { - self.sig_len() > 0 - && self.algo == 0 - && self.hash == 0 - && self.id_type == PkeyIdType::PkeyIdPkcs7 as u8 - && self.signer_len == 0 - && self.key_id_len == 0 - } -} - -/// `kexec_segment` from [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/include/linux/kexec.h#L82) -#[repr(C)] -#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] -pub struct KexecSegment { - /// Pointer to buffer (stored as u64 since we don't dereference it) - pub buf: u64, - pub bufsz: u64, - pub mem: u64, - pub memsz: u64, -} - -/// `kimage` from [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/include/linux/kexec.h#L296) -/// Note that this is a part of the original `kimage` structure. It only contains some fields that -/// we need for our use case, such as `nr_segments` and `segment`, and -/// are not affected by the kernel build configurations like `CONFIG_KEXEC_FILE` and `CONFIG_IMA_KEXEC`. -#[repr(C)] -#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] -pub struct Kimage { - head: u64, - /// Pointer fields stored as u64 since we don't dereference them - entry: u64, - last_entry: u64, - start: u64, - control_code_page: u64, // struct page* - swap_page: u64, // struct page* - vmcoreinfo_page: u64, // struct page* - vmcoreinfo_data_copy: u64, - pub nr_segments: u64, - pub segment: [KexecSegment; KEXEC_SEGMENT_MAX], - // we do not need the rest of the fields for now -} -pub const KEXEC_SEGMENT_MAX: usize = 16; - -/// `list_head` from [Linux](https://elixir.bootlin.com/linux/v6.6.85/source/include/linux/types.h#L190) -/// Pointer fields stored as u64 since we don't dereference them. -#[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable, KnownLayout)] -#[repr(C)] -pub struct ListHead { - pub next: u64, - pub prev: u64, -} diff --git a/litebox_platform_lvbs/src/host/lvbs_impl.rs b/litebox_platform_lvbs/src/host/lvbs_impl.rs index 2e0c79fc7b..5c0bc70c5d 100644 --- a/litebox_platform_lvbs/src/host/lvbs_impl.rs +++ b/litebox_platform_lvbs/src/host/lvbs_impl.rs @@ -8,6 +8,7 @@ use crate::{ host::per_cpu_variables::with_per_cpu_variables, }; use digest::Digest; +use litebox_common_lvbs::PRK_LEN; use rand_core::{RngCore, SeedableRng}; use zeroize::Zeroizing; @@ -174,9 +175,6 @@ impl LvbsCrng { } } -/// Length of the Platform Root Key in bytes. -pub(crate) const PRK_LEN: usize = 32; - static PRK_ONCE: spin::Once<[u8; PRK_LEN]> = spin::Once::new(); // Do not expose a raw PRK getter (i.e., no `get_platform_root_key`). diff --git a/litebox_platform_lvbs/src/host/mod.rs b/litebox_platform_lvbs/src/host/mod.rs index 197836bc71..41466f7fff 100644 --- a/litebox_platform_lvbs/src/host/mod.rs +++ b/litebox_platform_lvbs/src/host/mod.rs @@ -8,7 +8,7 @@ pub mod lvbs_impl; pub mod per_cpu_variables; pub use lvbs_impl::LvbsLinuxKernel; -pub(crate) use lvbs_impl::{PRK_LEN, set_platform_root_key}; +pub(crate) use lvbs_impl::set_platform_root_key; #[cfg(test)] pub mod mock; diff --git a/litebox_platform_lvbs/src/host/per_cpu_variables.rs b/litebox_platform_lvbs/src/host/per_cpu_variables.rs index fc618cf3e9..e0c577c1b9 100644 --- a/litebox_platform_lvbs/src/host/per_cpu_variables.rs +++ b/litebox_platform_lvbs/src/host/per_cpu_variables.rs @@ -4,7 +4,7 @@ //! Per-CPU VTL1 kernel variables use crate::{ - arch::{MAX_CORES, gdt, instrs::rdmsr}, + arch::{gdt, instrs::rdmsr}, mshv::{ HV_REGISTER_VP_INDEX, HvMessage, HvMessagePage, HvVpAssistPage, vsm::ControlRegMap, vtl_switch::VtlState, vtl1_mem_layout::PAGE_SIZE, @@ -16,6 +16,7 @@ use core::cell::{Cell, UnsafeCell}; use core::mem::offset_of; use litebox::utils::TruncateExt; use litebox_common_linux::{rdgsbase, wrgsbase}; +use litebox_common_lvbs::MAX_CORES; use x86_64::VirtAddr; pub const DOUBLE_FAULT_STACK_SIZE: usize = 2 * PAGE_SIZE; diff --git a/litebox_platform_lvbs/src/lib.rs b/litebox_platform_lvbs/src/lib.rs index 6726a9f67f..99180f853c 100644 --- a/litebox_platform_lvbs/src/lib.rs +++ b/litebox_platform_lvbs/src/lib.rs @@ -1320,13 +1320,14 @@ unsafe impl VmapManager for Linu let mem_attr = if perms.contains(PhysPageMapPermissions::WRITE) { // VTL1 needs writable access, so deny VTL0 all access. - crate::mshv::heki::MemAttr::empty() + litebox_common_lvbs::MemAttr::empty() } else if perms.contains(PhysPageMapPermissions::READ) { // VTL1 wants to read data from the pages, preventing VTL0 from writing to the pages. - crate::mshv::heki::MemAttr::MEM_ATTR_READ | crate::mshv::heki::MemAttr::MEM_ATTR_EXEC + litebox_common_lvbs::MemAttr::MEM_ATTR_READ + | litebox_common_lvbs::MemAttr::MEM_ATTR_EXEC } else { // VTL1 no longer protects the pages. - crate::mshv::heki::MemAttr::all() + litebox_common_lvbs::MemAttr::all() }; for range in range_set.iter() { diff --git a/litebox_platform_lvbs/src/mshv/error.rs b/litebox_platform_lvbs/src/mshv/error.rs deleted file mode 100644 index a9614205c2..0000000000 --- a/litebox_platform_lvbs/src/mshv/error.rs +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Error types for VSM operations - -use crate::mshv::{hvcall::HypervCallError, mem_integrity::VerificationError}; -use litebox_common_linux::errno::Errno; -use thiserror::Error; - -/// Errors for Virtual Secure Mode (VSM) operations. -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum VsmError { - // Boot/AP Initialization Errors - #[error("failed to copy boot signal page from VTL0")] - BootSignalPageCopyFailed, - - #[error("failed to initialize AP: {0:?}")] - ApInitFailed(HypervCallError), - - #[error("failed to copy boot signal page to VTL0")] - BootSignalWriteFailed, - - #[error("failed to copy cpu_online_mask from VTL0")] - CpuOnlineMaskCopyFailed, - - #[error("code page offset overflow when computing VTL return address")] - CodePageOffsetOverflow, - - #[error("integer overflow while processing VTL0-controlled range data")] - IntegerOverflow, - - // End-of-Boot Restriction Errors - #[error("{0} not allowed after end of boot")] - OperationAfterEndOfBoot(&'static str), - - // Address Validation Errors - #[error("invalid input address")] - InvalidInputAddress, - - #[error("address must be page-aligned")] - AddressNotPageAligned, - - #[error("invalid physical address")] - InvalidPhysicalAddress, - - // Memory/Data Errors - #[error("invalid memory attributes")] - MemoryAttributeInvalid, - - #[error("failed to copy HEKI pages from VTL0")] - HekiPagesCopyFailed, - - #[error("invalid kernel data type")] - KernelDataTypeInvalid, - - #[error("invalid module memory type")] - ModuleMemoryTypeInvalid, - - // Certificate Errors - #[error("system certificates not loaded")] - SystemCertificatesNotLoaded, - - #[error("no system certificate found in kernel data")] - SystemCertificatesNotFound, - - #[error("no valid system certificates parsed")] - SystemCertificatesInvalid, - - #[error("invalid DER certificate data (expected {expected} bytes, got {actual})")] - CertificateDerLengthInvalid { expected: usize, actual: usize }, - - #[error("failed to parse certificate")] - CertificateParseFailed, - - // Module Validation Errors - #[error("module ELF size ({size} bytes) exceeds maximum allowed ({max} bytes)")] - ModuleElfSizeExceeded { size: usize, max: usize }, - - #[error("found unexpected relocations in loaded module")] - ModuleRelocationInvalid, - - #[error("invalid module token")] - ModuleTokenInvalid, - - #[error("physical frames overlap already-protected or reserved memory")] - ProtectedFrameOverlap, - - // Kernel Symbol Table Errors - #[error("no kernel symbol table found")] - KernelSymbolTableNotFound, - - // Kexec Errors - #[error("invalid kexec type")] - KexecTypeInvalid, - - #[error("invalid kexec image segments")] - KexecImageSegmentsInvalid, - - #[error("invalid kexec segment memory range")] - KexecSegmentRangeInvalid, - - // Patch Errors - #[error("precomputed patch data not found")] - PrecomputedPatchNotFound, - - #[error("text patch validation failed")] - TextPatchSuspicious, - - // Unsupported Operation Errors - #[error("{0} is not supported")] - OperationNotSupported(&'static str), - - // VTL0 Memory Copy Errors - #[error("failed to copy data from/to VTL0")] - Vtl0CopyFailed, - - // Hypercall Errors - #[error("hypercall failed: {0:?}")] - HypercallFailed(HypervCallError), - - // Signature Verification Errors - #[error("signature verification failed: {0:?}")] - SignatureVerificationFailed(VerificationError), - - // Data Parsing Errors - #[error("buffer too small for {0}")] - BufferTooSmall(&'static str), - - // Address/Memory Range Errors - #[error("invalid virtual address")] - InvalidVirtualAddress, - - #[error("discontiguous memory range")] - DiscontiguousMemoryRange, - - // Symbol Table Errors - #[error("symbol table data empty")] - SymbolTableEmpty, - - #[error("symbol table data out of range")] - SymbolTableOutOfRange, - - #[error("symbol table length not aligned to symbol size")] - SymbolTableLengthInvalid, - - #[error("failed to parse symbol at offset {0:#x}")] - SymbolParseFailed(usize), - - #[error("symbol name offset out of bounds")] - SymbolNameOffsetInvalid, - - #[error("symbol name missing NUL terminator")] - SymbolNameNoTerminator, - - #[error("symbol name exceeds maximum length")] - SymbolNameTooLong, - - #[error("symbol name contains invalid UTF-8")] - SymbolNameInvalidUtf8, -} - -impl From for VsmError { - fn from(e: VerificationError) -> Self { - VsmError::SignatureVerificationFailed(e) - } -} - -impl From for Errno { - fn from(e: VsmError) -> Self { - match e { - // Address/pointer errors and memory copy failures - memory access fault - VsmError::InvalidInputAddress - | VsmError::InvalidPhysicalAddress - | VsmError::InvalidVirtualAddress - | VsmError::DiscontiguousMemoryRange - | VsmError::BootSignalPageCopyFailed - | VsmError::BootSignalWriteFailed - | VsmError::CpuOnlineMaskCopyFailed - | VsmError::HekiPagesCopyFailed - | VsmError::Vtl0CopyFailed => Errno::EFAULT, - - // Not found errors - VsmError::SystemCertificatesNotFound - | VsmError::KernelSymbolTableNotFound - | VsmError::PrecomputedPatchNotFound => Errno::ENOENT, - - // Operation not permitted after end of boot - VsmError::OperationAfterEndOfBoot(_) => Errno::EPERM, - - // Unsupported operation - VsmError::OperationNotSupported(_) => Errno::ENOTSUP, - - // Security/verification failures - access denied - VsmError::TextPatchSuspicious - | VsmError::SystemCertificatesInvalid - | VsmError::SystemCertificatesNotLoaded => Errno::EACCES, - - // Size/range errors - VsmError::BufferTooSmall(_) - | VsmError::KexecSegmentRangeInvalid - | VsmError::ModuleElfSizeExceeded { .. } - | VsmError::CodePageOffsetOverflow - | VsmError::IntegerOverflow - | VsmError::SymbolNameTooLong - | VsmError::SymbolTableOutOfRange => Errno::ERANGE, - - // Init/hardware failures - I/O error - VsmError::ApInitFailed(_) | VsmError::HypercallFailed(_) => Errno::EIO, - - // True format/validation errors - invalid argument - VsmError::AddressNotPageAligned - | VsmError::MemoryAttributeInvalid - | VsmError::KernelDataTypeInvalid - | VsmError::ModuleMemoryTypeInvalid - | VsmError::ModuleRelocationInvalid - | VsmError::ModuleTokenInvalid - | VsmError::ProtectedFrameOverlap - | VsmError::KexecTypeInvalid - | VsmError::KexecImageSegmentsInvalid - | VsmError::SymbolTableEmpty - | VsmError::SymbolTableLengthInvalid - | VsmError::SymbolParseFailed(_) - | VsmError::SymbolNameOffsetInvalid - | VsmError::SymbolNameInvalidUtf8 - | VsmError::SymbolNameNoTerminator - | VsmError::CertificateDerLengthInvalid { .. } - | VsmError::CertificateParseFailed => Errno::EINVAL, - - // Signature verification failures delegate to VerificationError's Errno mapping - VsmError::SignatureVerificationFailed(e) => Errno::from(e), - } - } -} diff --git a/litebox_platform_lvbs/src/mshv/heki.rs b/litebox_platform_lvbs/src/mshv/heki.rs index b4911fc8f6..c1615d9c1d 100644 --- a/litebox_platform_lvbs/src/mshv/heki.rs +++ b/litebox_platform_lvbs/src/mshv/heki.rs @@ -1,30 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::{ - host::linux::ListHead, - mshv::{HvPageProtFlags, error::VsmError, vtl1_mem_layout::PAGE_SIZE}, -}; -use core::mem; -use litebox::utils::TruncateExt; -use num_enum::TryFromPrimitive; -use x86_64::{ - PhysAddr, VirtAddr, - structures::paging::{PageSize, Size4KiB}, -}; -use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout}; +//! Platform-coupled HEKI helpers. +//! +//! The wire types, enums, and constants have been hoisted into +//! [`litebox_common_lvbs`]. What remains here are helpers that depend on +//! platform-specific types (e.g. [`HvPageProtFlags`]). -bitflags::bitflags! { - #[derive(Clone, Copy, Debug, PartialEq)] - pub struct MemAttr: u64 { - const MEM_ATTR_READ = 1 << 0; - const MEM_ATTR_WRITE = 1 << 1; - const MEM_ATTR_EXEC = 1 << 2; - const MEM_ATTR_IMMUTABLE = 1 << 3; - - const _ = !0; - } -} +use crate::mshv::HvPageProtFlags; +use litebox_common_lvbs::MemAttr; pub(crate) fn mem_attr_to_hv_page_prot_flags(attr: MemAttr) -> HvPageProtFlags { let mut flags = HvPageProtFlags::empty(); @@ -42,359 +26,3 @@ pub(crate) fn mem_attr_to_hv_page_prot_flags(attr: MemAttr) -> HvPageProtFlags { flags } - -#[derive(Default, Debug, TryFromPrimitive, PartialEq)] -#[repr(u64)] -pub enum HekiKdataType { - SystemCerts = 0, - RevocationCerts = 1, - BlocklistHashes = 2, - KernelInfo = 3, - KernelData = 4, - PatchInfo = 5, - KexecTrampoline = 6, - #[default] - Unknown = 0xffff_ffff_ffff_ffff, -} - -#[derive(Default, Debug, TryFromPrimitive, PartialEq)] -#[repr(u64)] -pub enum HekiKexecType { - KexecImage = 0, - KexecKernelBlob = 1, - KexecPages = 2, - #[default] - Unknown = 0xffff_ffff_ffff_ffff, -} - -#[derive(Clone, Copy, Default, Debug, TryFromPrimitive, PartialEq)] -#[repr(u64)] -pub enum ModMemType { - Text = 0, - Data = 1, - RoData = 2, - RoAfterInit = 3, - InitText = 4, - InitData = 5, - InitRoData = 6, - ElfBuffer = 7, - Patch = 8, - #[default] - Unknown = 0xffff_ffff_ffff_ffff, -} - -pub(crate) fn mod_mem_type_to_mem_attr(mod_mem_type: ModMemType) -> MemAttr { - let mut mem_attr = MemAttr::empty(); - - match mod_mem_type { - ModMemType::Text | ModMemType::InitText => { - mem_attr.set(MemAttr::MEM_ATTR_READ, true); - mem_attr.set(MemAttr::MEM_ATTR_EXEC, true); - } - ModMemType::Data | ModMemType::RoAfterInit | ModMemType::InitData => { - mem_attr.set(MemAttr::MEM_ATTR_READ, true); - mem_attr.set(MemAttr::MEM_ATTR_WRITE, true); - } - ModMemType::RoData | ModMemType::InitRoData => { - mem_attr.set(MemAttr::MEM_ATTR_READ, true); - } - _ => {} - } - - mem_attr -} - -/// `HekiRange` is a generic container for various types of memory ranges. -/// It has an `attributes` field which can be interpreted differently based on the context like -/// `MemAttr`, `KdataType`, `ModMemType`, or `KexecType`. -#[derive(Default, Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)] -#[repr(C, packed)] -pub struct HekiRange { - pub va: u64, - pub pa: u64, - pub epa: u64, - pub attributes: u64, -} - -impl HekiRange { - #[inline] - pub fn is_aligned(&self, align: U) -> bool - where - U: Into + Copy, - { - let va = self.va; - let pa = self.pa; - let epa = self.epa; - - VirtAddr::new(va).is_aligned(align) - && PhysAddr::new(pa).is_aligned(align) - && PhysAddr::new(epa).is_aligned(align) - } - - #[inline] - pub fn mem_attr(&self) -> Option { - let attr = self.attributes; - MemAttr::from_bits(attr) - } - - #[inline] - pub fn mod_mem_type(&self) -> ModMemType { - let attr = self.attributes; - ModMemType::try_from(attr).unwrap_or(ModMemType::Unknown) - } - - #[inline] - pub fn heki_kdata_type(&self) -> HekiKdataType { - let attr = self.attributes; - HekiKdataType::try_from(attr).unwrap_or(HekiKdataType::Unknown) - } - - #[inline] - pub fn heki_kexec_type(&self) -> HekiKexecType { - let attr = self.attributes; - HekiKexecType::try_from(attr).unwrap_or(HekiKexecType::Unknown) - } - - pub fn is_valid(&self) -> bool { - let va = self.va; - let pa = self.pa; - let epa = self.epa; - let Ok(pa) = PhysAddr::try_new(pa) else { - return false; - }; - let Ok(epa) = PhysAddr::try_new(epa) else { - return false; - }; - !(VirtAddr::try_new(va).is_err() - || epa < pa - || (self.mem_attr().is_none() - && self.heki_kdata_type() == HekiKdataType::Unknown - && self.heki_kexec_type() == HekiKexecType::Unknown - && self.mod_mem_type() == ModMemType::Unknown)) - } -} - -impl core::fmt::Debug for HekiRange { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let va = self.va; - let pa = self.pa; - let epa = self.epa; - let attr = self.attributes; - f.debug_struct("HekiRange") - .field("va", &format_args!("{va:#x}")) - .field("pa", &format_args!("{pa:#x}")) - .field("epa", &format_args!("{epa:#x}")) - .field("attr", &format_args!("{attr:#x}")) - .field("type", &format_args!("{:?}", self.heki_kdata_type())) - .field("size", &format_args!("{:?}", self.epa - self.pa)) - .finish() - } -} - -#[expect(clippy::cast_possible_truncation)] -pub const HEKI_MAX_RANGES: usize = - ((PAGE_SIZE as u32 - u64::BITS * 3 / 8) / core::mem::size_of::() as u32) as usize; - -#[derive(Clone, Copy, FromBytes, Immutable, KnownLayout)] -#[repr(align(4096))] -#[repr(C)] -pub struct HekiPage { - /// Pointer to next page (stored as u64 since we don't dereference it) - pub next: u64, - pub next_pa: u64, - pub nranges: u64, - pub ranges: [HekiRange; HEKI_MAX_RANGES], - pad: u64, -} - -impl HekiPage { - pub fn new() -> Self { - // Safety: all fields are valid when zeroed (u64 zeros, array of zeroed HekiRange) - Self::new_zeroed() - } - - pub fn is_valid(&self) -> bool { - if PhysAddr::try_new(self.next_pa) - .ok() - .is_none_or(|next_pa| self.next_pa != 0 && !next_pa.is_aligned(Size4KiB::SIZE)) - { - return false; - } - let Some(nranges) = usize::try_from(self.nranges) - .ok() - .filter(|&n| (1..=HEKI_MAX_RANGES).contains(&n)) - else { - return false; - }; - for heki_range in &self.ranges[..nranges] { - if !heki_range.is_valid() { - return false; - } - } - true - } -} - -impl Default for HekiPage { - fn default() -> Self { - Self::new_zeroed() - } -} - -impl<'a> IntoIterator for &'a HekiPage { - type Item = &'a HekiRange; - type IntoIter = core::slice::Iter<'a, HekiRange>; - - fn into_iter(self) -> Self::IntoIter { - self.ranges[..usize::try_from(self.nranges).unwrap_or(0)].iter() - } -} - -#[derive(Default, Clone, Copy, Debug, FromBytes, IntoBytes, Immutable, KnownLayout)] -#[repr(C)] -pub struct HekiPatch { - pub pa: [u64; 2], - pub size: u8, - pub code: [u8; POKE_MAX_OPCODE_SIZE], - _padding: [u8; 2], -} -pub const POKE_MAX_OPCODE_SIZE: usize = 5; - -impl HekiPatch { - /// Creates a new `HekiPatch` with a given buffer. Returns `None` if any field is invalid. - pub fn try_from_bytes(bytes: &[u8]) -> Option { - let patch = Self::read_from_bytes(bytes).ok()?; - if patch.is_valid() { Some(patch) } else { None } - } - - pub fn is_valid(&self) -> bool { - let Some(pa_0) = PhysAddr::try_new(self.pa[0]) - .ok() - .filter(|&pa| !pa.is_null()) - else { - return false; - }; - let Some(pa_1) = PhysAddr::try_new(self.pa[1]) - .ok() - .filter(|&pa| pa.is_null() || pa.is_aligned(Size4KiB::SIZE)) - else { - return false; - }; - let bytes_in_first_page = if pa_0.is_aligned(Size4KiB::SIZE) { - core::cmp::min(PAGE_SIZE, usize::from(self.size)) - } else { - core::cmp::min( - (pa_0.align_up(Size4KiB::SIZE) - pa_0).trunc(), - usize::from(self.size), - ) - }; - - !(self.size == 0 - || usize::from(self.size) > POKE_MAX_OPCODE_SIZE - || (pa_0 == pa_1) - || (bytes_in_first_page < usize::from(self.size) && pa_1.is_null()) - || (bytes_in_first_page == usize::from(self.size) && !pa_1.is_null())) - } -} - -#[derive(Default, Clone, Copy, Debug, PartialEq)] -#[repr(u32)] -pub enum HekiPatchType { - JumpLabel = 0, - #[default] - Unknown = 0xffff_ffff, -} - -#[derive(Clone, Copy, Debug, FromBytes, Immutable, KnownLayout)] -#[repr(C)] -pub struct HekiPatchInfo { - /// Patch type stored as u32 for zerocopy compatibility (see `HekiPatchType`) - pub typ_: u32, - list: ListHead, - /// *const `struct module` (stored as u64 since we don't dereference it) - mod_: u64, - pub patch_index: u64, - pub max_patch_count: u64, - // pub patch: [HekiPatch; *] -} - -impl HekiPatchInfo { - /// Creates a new `HekiPatchInfo` with a given buffer. Returns `None` if any field is invalid. - pub fn try_from_bytes(bytes: &[u8]) -> Option { - let info = Self::read_from_bytes(bytes).ok()?; - if info.is_valid() { Some(info) } else { None } - } - - pub fn is_valid(&self) -> bool { - !(self.typ_ != HekiPatchType::JumpLabel as u32 - || self.patch_index == 0 - || self.patch_index > self.max_patch_count) - } -} - -#[repr(C)] -#[allow(clippy::struct_field_names)] -// TODO: Account for kernel config changing the size and meaning of the field members -pub struct HekiKernelSymbol { - pub value_offset: core::ffi::c_int, - pub name_offset: core::ffi::c_int, - pub namespace_offset: core::ffi::c_int, -} - -impl HekiKernelSymbol { - pub const KSYM_LEN: usize = mem::size_of::(); - pub const KSY_NAME_LEN: usize = 512; - - pub fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() < Self::KSYM_LEN { - return Err(VsmError::BufferTooSmall("HekiKernelSymbol")); - } - - #[allow(clippy::cast_ptr_alignment)] - let ksym_ptr = bytes.as_ptr().cast::(); - assert!(ksym_ptr.is_aligned(), "ksym_ptr is not aligned"); - - // SAFETY: Casting from vtl0 buffer that contained the struct - unsafe { - Ok(HekiKernelSymbol { - value_offset: (*ksym_ptr).value_offset, - name_offset: (*ksym_ptr).name_offset, - namespace_offset: (*ksym_ptr).namespace_offset, - }) - } - } -} - -#[repr(C)] -#[allow(clippy::struct_field_names)] -pub struct HekiKernelInfo { - pub ksymtab_start: *const HekiKernelSymbol, - pub ksymtab_end: *const HekiKernelSymbol, - pub ksymtab_gpl_start: *const HekiKernelSymbol, - pub ksymtab_gpl_end: *const HekiKernelSymbol, - // Skip unused arch info -} - -impl HekiKernelInfo { - const KINFO_LEN: usize = mem::size_of::(); - - pub fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() < Self::KINFO_LEN { - return Err(VsmError::BufferTooSmall("HekiKernelInfo")); - } - - #[allow(clippy::cast_ptr_alignment)] - let kinfo_ptr = bytes.as_ptr().cast::(); - assert!(kinfo_ptr.is_aligned(), "kinfo_ptr is not aligned"); - - // SAFETY: Casting from vtl0 buffer that contained the struct - unsafe { - Ok(HekiKernelInfo { - ksymtab_start: (*kinfo_ptr).ksymtab_start, - ksymtab_end: (*kinfo_ptr).ksymtab_end, - ksymtab_gpl_start: (*kinfo_ptr).ksymtab_gpl_start, - ksymtab_gpl_end: (*kinfo_ptr).ksymtab_gpl_end, - }) - } - } -} diff --git a/litebox_platform_lvbs/src/mshv/hvcall.rs b/litebox_platform_lvbs/src/mshv/hvcall.rs index b6f0d4a3f2..1b1de38141 100644 --- a/litebox_platform_lvbs/src/mshv/hvcall.rs +++ b/litebox_platform_lvbs/src/mshv/hvcall.rs @@ -11,21 +11,16 @@ use crate::{ mshv::{ HV_HYPERCALL_REP_COMP_MASK, HV_HYPERCALL_REP_COMP_OFFSET, HV_HYPERCALL_REP_START_MASK, HV_HYPERCALL_REP_START_OFFSET, HV_HYPERCALL_RESULT_MASK, HV_HYPERCALL_VARHEAD_OFFSET, - HV_STATUS_ACCESS_DENIED, HV_STATUS_INSUFFICIENT_BUFFERS, HV_STATUS_INSUFFICIENT_MEMORY, - HV_STATUS_INVALID_ALIGNMENT, HV_STATUS_INVALID_CONNECTION_ID, - HV_STATUS_INVALID_HYPERCALL_CODE, HV_STATUS_INVALID_HYPERCALL_INPUT, - HV_STATUS_INVALID_PARAMETER, HV_STATUS_INVALID_PORT_ID, HV_STATUS_OPERATION_DENIED, - HV_STATUS_SUCCESS, HV_STATUS_TIME_OUT, HV_STATUS_VTL_ALREADY_ENABLED, - HV_X64_MSR_GUEST_OS_ID, HV_X64_MSR_HYPERCALL, HV_X64_MSR_HYPERCALL_ENABLE, - HV_X64_MSR_SCONTROL, HV_X64_MSR_SCONTROL_ENABLE, HV_X64_MSR_SIMP, HV_X64_MSR_SIMP_ENABLE, - HV_X64_MSR_SINT0, HV_X64_MSR_VP_ASSIST_PAGE, HV_X64_MSR_VP_ASSIST_PAGE_ENABLE, - HYPERV_CPUID_IMPLEMENT_LIMITS, HYPERV_CPUID_INTERFACE, + HV_STATUS_SUCCESS, HV_X64_MSR_GUEST_OS_ID, HV_X64_MSR_HYPERCALL, + HV_X64_MSR_HYPERCALL_ENABLE, HV_X64_MSR_SCONTROL, HV_X64_MSR_SCONTROL_ENABLE, + HV_X64_MSR_SIMP, HV_X64_MSR_SIMP_ENABLE, HV_X64_MSR_SINT0, HV_X64_MSR_VP_ASSIST_PAGE, + HV_X64_MSR_VP_ASSIST_PAGE_ENABLE, HYPERV_CPUID_IMPLEMENT_LIMITS, HYPERV_CPUID_INTERFACE, HYPERV_CPUID_VENDOR_AND_MAX_FUNCTIONS, HYPERV_HYPERVISOR_PRESENT_BIT, HYPERVISOR_CALLBACK_VECTOR, HvSynicSint, vsm, }, }; use core::arch::asm; -use num_enum::{IntoPrimitive, TryFromPrimitive}; +use litebox_common_lvbs::HypervCallError; use thiserror::Error; #[cfg(debug_assertions)] @@ -281,36 +276,3 @@ pub enum HypervError { #[error("unknown Hyper-V error")] Unknown, } - -/// Errors for Hyper-V hypercalls. -#[derive(Debug, Error, TryFromPrimitive, IntoPrimitive)] -#[non_exhaustive] -#[repr(u32)] -pub enum HypervCallError { - #[error("invalid hypercall code")] - InvalidCode = HV_STATUS_INVALID_HYPERCALL_CODE, - #[error("invalid hypercall input")] - InvalidInput = HV_STATUS_INVALID_HYPERCALL_INPUT, - #[error("invalid alignment")] - InvalidAlignment = HV_STATUS_INVALID_ALIGNMENT, - #[error("invalid parameter")] - InvalidParameter = HV_STATUS_INVALID_PARAMETER, - #[error("access denied")] - AccessDenied = HV_STATUS_ACCESS_DENIED, - #[error("operation denied")] - OperationDenied = HV_STATUS_OPERATION_DENIED, - #[error("insufficient memory")] - InsufficientMemory = HV_STATUS_INSUFFICIENT_MEMORY, - #[error("invalid port ID")] - InvalidPortID = HV_STATUS_INVALID_PORT_ID, - #[error("invalid connection ID")] - InvalidConnectionID = HV_STATUS_INVALID_CONNECTION_ID, - #[error("insufficient buffers")] - InsufficientBuffers = HV_STATUS_INSUFFICIENT_BUFFERS, - #[error("timeout")] - TimeOut = HV_STATUS_TIME_OUT, - #[error("VTL already enabled")] - AlreadyEnabled = HV_STATUS_VTL_ALREADY_ENABLED, - #[error("unknown hypercall error")] - Unknown = 0xffff_ffff, -} diff --git a/litebox_platform_lvbs/src/mshv/hvcall_mm.rs b/litebox_platform_lvbs/src/mshv/hvcall_mm.rs index 3955dec8dc..d3183381e0 100644 --- a/litebox_platform_lvbs/src/mshv/hvcall_mm.rs +++ b/litebox_platform_lvbs/src/mshv/hvcall_mm.rs @@ -15,12 +15,11 @@ use crate::{ host::per_cpu_variables::with_per_cpu_variables, mshv::{ HV_PARTITION_ID_SELF, HVCALL_MODIFY_VTL_PROTECTION_MASK, HvInputModifyVtlProtectionMask, - HvInputVtl, HvPageProtFlags, - hvcall::{HypervCallError, hv_do_rep_hypercall}, - vtl1_mem_layout::PAGE_SHIFT, + HvInputVtl, HvPageProtFlags, hvcall::hv_do_rep_hypercall, vtl1_mem_layout::PAGE_SHIFT, }, }; use litebox::utils::TruncateExt; +use litebox_common_lvbs::HypervCallError; /// Hyper-V Hypercall to prevent lower VTLs (i.e., VTL0) from accessing a specified range of /// guest physical memory pages with a given protection flag. diff --git a/litebox_platform_lvbs/src/mshv/hvcall_vp.rs b/litebox_platform_lvbs/src/mshv/hvcall_vp.rs index 4dc03aadc5..7e4f23bb23 100644 --- a/litebox_platform_lvbs/src/mshv/hvcall_vp.rs +++ b/litebox_platform_lvbs/src/mshv/hvcall_vp.rs @@ -14,13 +14,14 @@ use crate::{ HV_PARTITION_ID_SELF, HV_VP_INDEX_SELF, HV_VTL_NORMAL, HV_VTL_SECURE, HVCALL_ENABLE_VP_VTL, HVCALL_GET_VP_REGISTERS, HVCALL_SET_VP_REGISTERS, HvEnableVpVtl, HvGetVpRegistersInput, HvGetVpRegistersOutput, HvInputVtl, HvSetVpRegistersInput, SegmentRegisterAttributeFlags, - hvcall::{HypervCallError, hv_do_hypercall, hv_do_rep_hypercall}, + hvcall::{hv_do_hypercall, hv_do_rep_hypercall}, vtl1_mem_layout::{ PAGE_SIZE, VTL1_KERNEL_STACK_PAGE, VTL1_TSS_PAGE, get_address_of_special_page, }, }, serial_println, }; +use litebox_common_lvbs::HypervCallError; use x86_64::{ PrivilegeLevel, structures::{gdt::SegmentSelector, tss::TaskStateSegment}, diff --git a/litebox_platform_lvbs/src/mshv/mem_integrity.rs b/litebox_platform_lvbs/src/mshv/mem_integrity.rs index f4ac3aef11..323b97d52a 100644 --- a/litebox_platform_lvbs/src/mshv/mem_integrity.rs +++ b/litebox_platform_lvbs/src/mshv/mem_integrity.rs @@ -3,13 +3,7 @@ //! Functions for checking the memory integrity of VTL0 kernel image and modules -use crate::{ - host::linux::ModuleSignature, - mshv::{ - heki::{HekiPatch, POKE_MAX_OPCODE_SIZE}, - vsm::ModuleMemory, - }, -}; +use crate::mshv::vsm::ModuleMemory; use alloc::{vec, vec::Vec}; use authenticode::{AttributeCertificateIterator, AuthenticodeSignature, authenticode_digest}; use cms::{content_info::ContentInfo, signed_data::SignedData}; @@ -25,7 +19,7 @@ use elf::{ string_table::StringTable, symbol::Symbol, }; -use litebox_common_linux::errno::Errno; +use litebox_common_lvbs::{HekiPatch, ModuleSignature, POKE_MAX_OPCODE_SIZE, VerificationError}; use object::read::pe::PeFile64; use rangemap::set::RangeSet; use rsa::{RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs1v15::Signature, signature::Verifier}; @@ -766,33 +760,3 @@ pub enum KernelElfError { #[error("unsupported relocation type")] UnsupportedRelocation, } - -/// Errors for module signature verification. -#[derive(Debug, Error, PartialEq)] -#[non_exhaustive] -pub enum VerificationError { - #[error("signature not found in module")] - SignatureNotFound, - #[error("invalid signature format")] - InvalidSignature, - #[error("invalid certificate")] - InvalidCertificate, - #[error("signature authentication failed")] - AuthenticationFailed, - #[error("failed to parse signature data")] - ParseFailed, - #[error("unsupported signature algorithm")] - Unsupported, -} - -impl From for Errno { - fn from(e: VerificationError) -> Self { - match e { - VerificationError::AuthenticationFailed => Errno::EKEYREJECTED, - VerificationError::SignatureNotFound => Errno::ENODATA, - VerificationError::Unsupported => Errno::ENOPKG, - VerificationError::InvalidCertificate => Errno::ENOKEY, - VerificationError::InvalidSignature | VerificationError::ParseFailed => Errno::ELIBBAD, - } - } -} diff --git a/litebox_platform_lvbs/src/mshv/mod.rs b/litebox_platform_lvbs/src/mshv/mod.rs index 11cb3ed602..bbeb928642 100644 --- a/litebox_platform_lvbs/src/mshv/mod.rs +++ b/litebox_platform_lvbs/src/mshv/mod.rs @@ -3,7 +3,6 @@ //! Hyper-V-specific code -pub mod error; pub(crate) mod heki; pub mod hvcall; pub(crate) mod hvcall_mm; @@ -80,8 +79,8 @@ type Vtl0PhysConstPtr = type PrivilegedVtl0PhysMutPtr = litebox_common_linux::physical_pointers::PhysMutPtr; -use crate::arch::MAX_CORES; use crate::mshv::vtl1_mem_layout::PAGE_SIZE; +use litebox_common_lvbs::MAX_CORES; use modular_bitfield::prelude::*; use modular_bitfield::specifiers::{B3, B4, B7, B8, B16, B31, B32, B45, B51, B62}; use num_enum::{IntoPrimitive, TryFromPrimitive}; @@ -95,18 +94,6 @@ pub const HV_HYPERCALL_VARHEAD_OFFSET: u64 = 17; pub const HV_REGISTER_VP_INDEX: u32 = 0x_4000_0002; pub const HV_STATUS_SUCCESS: u32 = 0; -pub const HV_STATUS_INVALID_HYPERCALL_CODE: u32 = 2; -pub const HV_STATUS_INVALID_HYPERCALL_INPUT: u32 = 3; -pub const HV_STATUS_INVALID_ALIGNMENT: u32 = 4; -pub const HV_STATUS_INVALID_PARAMETER: u32 = 5; -pub const HV_STATUS_ACCESS_DENIED: u32 = 6; -pub const HV_STATUS_OPERATION_DENIED: u32 = 8; -pub const HV_STATUS_INSUFFICIENT_MEMORY: u32 = 11; -pub const HV_STATUS_INVALID_PORT_ID: u32 = 17; -pub const HV_STATUS_INVALID_CONNECTION_ID: u32 = 18; -pub const HV_STATUS_INSUFFICIENT_BUFFERS: u32 = 19; -pub const HV_STATUS_TIME_OUT: u32 = 120; -pub const HV_STATUS_VTL_ALREADY_ENABLED: u32 = 134; pub const HV_X64_MSR_GUEST_OS_ID: u32 = 0x_4000_0000; pub const HV_X64_MSR_HYPERCALL: u32 = 0x_4000_0001; @@ -176,51 +163,6 @@ pub const HV_REGISTER_CR_INTERCEPT_CR0_MASK: u32 = 0x000e_0001; pub const HV_REGISTER_CR_INTERCEPT_CR4_MASK: u32 = 0x000e_0002; pub const HV_REGISTER_PENDING_EVENT0: u32 = 0x0001_0004; -/// VTL call parameters (`param[0]`: function ID, `param[1..4]`: parameters) -pub const NUM_VTLCALL_PARAMS: usize = 4; - -pub const VSM_VTL_CALL_FUNC_ID_ENABLE_APS_VTL: u32 = 0x1_ffe0; -pub const VSM_VTL_CALL_FUNC_ID_BOOT_APS: u32 = 0x1_ffe1; -pub const VSM_VTL_CALL_FUNC_ID_LOCK_REGS: u32 = 0x1_ffe2; -pub const VSM_VTL_CALL_FUNC_ID_SIGNAL_END_OF_BOOT: u32 = 0x1_ffe3; -pub const VSM_VTL_CALL_FUNC_ID_PROTECT_MEMORY: u32 = 0x1_ffe4; -pub const VSM_VTL_CALL_FUNC_ID_LOAD_KDATA: u32 = 0x1_ffe5; -pub const VSM_VTL_CALL_FUNC_ID_VALIDATE_MODULE: u32 = 0x1_ffe6; -pub const VSM_VTL_CALL_FUNC_ID_FREE_MODULE_INIT: u32 = 0x1_ffe7; -pub const VSM_VTL_CALL_FUNC_ID_UNLOAD_MODULE: u32 = 0x1_ffe8; -pub const VSM_VTL_CALL_FUNC_ID_COPY_SECONDARY_KEY: u32 = 0x1_ffe9; -pub const VSM_VTL_CALL_FUNC_ID_KEXEC_VALIDATE: u32 = 0x1_ffea; -pub const VSM_VTL_CALL_FUNC_ID_PATCH_TEXT: u32 = 0x1_ffeb; -pub const VSM_VTL_CALL_FUNC_ID_ALLOCATE_RINGBUFFER_MEMORY: u32 = 0x1_ffec; - -// This VSM function ID for setting the platform root key is subject to change -pub const VSM_VTL_CALL_FUNC_ID_SET_PLATFORM_ROOT_KEY: u32 = 0x1_ffed; - -// This VSM function ID for OP-TEE messages is subject to change -pub const VSM_VTL_CALL_FUNC_ID_OPTEE_MESSAGE: u32 = 0x1_fff0; - -/// VSM Functions -#[derive(Debug, PartialEq, TryFromPrimitive)] -#[repr(u32)] -pub enum VsmFunction { - // VSM/Heki functions - EnableAPsVtl = VSM_VTL_CALL_FUNC_ID_ENABLE_APS_VTL, - BootAPs = VSM_VTL_CALL_FUNC_ID_BOOT_APS, - LockRegs = VSM_VTL_CALL_FUNC_ID_LOCK_REGS, - SignalEndOfBoot = VSM_VTL_CALL_FUNC_ID_SIGNAL_END_OF_BOOT, - ProtectMemory = VSM_VTL_CALL_FUNC_ID_PROTECT_MEMORY, - LoadKData = VSM_VTL_CALL_FUNC_ID_LOAD_KDATA, - ValidateModule = VSM_VTL_CALL_FUNC_ID_VALIDATE_MODULE, - FreeModuleInit = VSM_VTL_CALL_FUNC_ID_FREE_MODULE_INIT, - UnloadModule = VSM_VTL_CALL_FUNC_ID_UNLOAD_MODULE, - CopySecondaryKey = VSM_VTL_CALL_FUNC_ID_COPY_SECONDARY_KEY, - KexecValidate = VSM_VTL_CALL_FUNC_ID_KEXEC_VALIDATE, - PatchText = VSM_VTL_CALL_FUNC_ID_PATCH_TEXT, - OpteeMessage = VSM_VTL_CALL_FUNC_ID_OPTEE_MESSAGE, - AllocateRingbufferMemory = VSM_VTL_CALL_FUNC_ID_ALLOCATE_RINGBUFFER_MEMORY, - SetPlatformRootKey = VSM_VTL_CALL_FUNC_ID_SET_PLATFORM_ROOT_KEY, -} - pub const MSR_EFER: u32 = 0xc000_0080; pub const MSR_STAR: u32 = 0xc000_0081; pub const MSR_LSTAR: u32 = 0xc000_0082; diff --git a/litebox_platform_lvbs/src/mshv/vsm.rs b/litebox_platform_lvbs/src/mshv/vsm.rs index 7cb97c8e3a..ba8c80f4f7 100644 --- a/litebox_platform_lvbs/src/mshv/vsm.rs +++ b/litebox_platform_lvbs/src/mshv/vsm.rs @@ -10,10 +10,7 @@ use crate::mshv::{PrivilegedVtl0PhysMutPtr, Vtl0PhysConstPtr}; use crate::{ debug_serial_println, host::{ - PRK_LEN, - bootparam::get_vtl1_memory_info, - linux::{CpuMask, KEXEC_SEGMENT_MAX, Kimage}, - per_cpu_variables::with_per_cpu_variables, + bootparam::get_vtl1_memory_info, linux::CpuMask, per_cpu_variables::with_per_cpu_variables, set_platform_root_key, }, mshv::{ @@ -23,15 +20,9 @@ use crate::{ HV_X64_REGISTER_CR4, HV_X64_REGISTER_CSTAR, HV_X64_REGISTER_EFER, HV_X64_REGISTER_LSTAR, HV_X64_REGISTER_SFMASK, HV_X64_REGISTER_STAR, HV_X64_REGISTER_SYSENTER_CS, HV_X64_REGISTER_SYSENTER_EIP, HV_X64_REGISTER_SYSENTER_ESP, HvCrInterceptControlFlags, - HvPageProtFlags, HvRegisterVsmPartitionConfig, HvRegisterVsmVpSecureVtlConfig, VsmFunction, - X86Cr0Flags, X86Cr4Flags, - error::VsmError, - heki::{ - HekiKdataType, HekiKernelInfo, HekiKernelSymbol, HekiKexecType, HekiPage, HekiPatch, - HekiPatchInfo, HekiRange, MemAttr, ModMemType, mem_attr_to_hv_page_prot_flags, - mod_mem_type_to_mem_attr, - }, - hvcall::HypervCallError, + HvPageProtFlags, HvRegisterVsmPartitionConfig, HvRegisterVsmVpSecureVtlConfig, X86Cr0Flags, + X86Cr4Flags, + heki::mem_attr_to_hv_page_prot_flags, hvcall_mm::hv_modify_vtl_protection_mask, hvcall_vp::{hvcall_get_vp_vtl0_registers, hvcall_set_vp_registers, init_vtl_ap}, mem_integrity::{ @@ -42,6 +33,11 @@ use crate::{ vtl1_mem_layout::{PAGE_SHIFT, PAGE_SIZE}, }, }; +use litebox_common_lvbs::{ + HekiKdataType, HekiKernelInfo, HekiKernelSymbol, HekiKexecType, HekiPage, HekiPatch, + HekiPatchInfo, HekiRange, HypervCallError, KEXEC_SEGMENT_MAX, Kimage, MemAttr, ModMemType, + PRK_LEN, VsmError, VsmFunction, mod_mem_type_to_mem_attr, +}; use alloc::{boxed::Box, ffi::CString, string::String, vec::Vec}; use core::{ diff --git a/litebox_platform_lvbs/src/mshv/vsm_intercept.rs b/litebox_platform_lvbs/src/mshv/vsm_intercept.rs index 4a88b7df6b..f54903c30a 100644 --- a/litebox_platform_lvbs/src/mshv/vsm_intercept.rs +++ b/litebox_platform_lvbs/src/mshv/vsm_intercept.rs @@ -12,10 +12,10 @@ use crate::{ HV_X64_REGISTER_TR, HvInterceptMessage, HvInterceptMessageHeader, HvMessageType, HvMsrInterceptMessage, HvPendingExceptionEvent, MSR_CSTAR, MSR_EFER, MSR_IA32_APICBASE, MSR_IA32_SYSENTER_CS, MSR_IA32_SYSENTER_EIP, MSR_IA32_SYSENTER_ESP, MSR_LSTAR, MSR_STAR, - MSR_SYSCALL_MASK, X86Cr0Flags, X86Cr4Flags, hvcall::HypervCallError, - hvcall_vp::hvcall_set_vp_vtl0_registers, + MSR_SYSCALL_MASK, X86Cr0Flags, X86Cr4Flags, hvcall_vp::hvcall_set_vp_vtl0_registers, }, }; +use litebox_common_lvbs::HypervCallError; use num_enum::TryFromPrimitive; /// A list of MSR indexes that VSM prevents VTL0 from writing to. diff --git a/litebox_platform_lvbs/src/mshv/vtl_switch.rs b/litebox_platform_lvbs/src/mshv/vtl_switch.rs index cc55ab182d..51ea5b265c 100644 --- a/litebox_platform_lvbs/src/mshv/vtl_switch.rs +++ b/litebox_platform_lvbs/src/mshv/vtl_switch.rs @@ -9,12 +9,12 @@ use crate::host::{ }; use crate::mshv::{ HV_FLUSH_EX_VP_SET_BANKS, HV_REGISTER_VSM_CODEPAGE_OFFSETS, HvRegisterVsmCodePageOffsets, - NUM_VTLCALL_PARAMS, VTL_ENTRY_REASON_INTERRUPT, VTL_ENTRY_REASON_LOWER_VTL_CALL, - VTL_ENTRY_REASON_RESERVED, error::VsmError, hvcall_vp::hvcall_get_vp_registers, - vsm_intercept::vsm_handle_intercept, + VTL_ENTRY_REASON_INTERRUPT, VTL_ENTRY_REASON_LOWER_VTL_CALL, VTL_ENTRY_REASON_RESERVED, + hvcall_vp::hvcall_get_vp_registers, vsm_intercept::vsm_handle_intercept, }; use core::sync::atomic::{AtomicU64, Ordering}; use litebox::utils::{ReinterpretUnsignedExt, TruncateExt}; +use litebox_common_lvbs::{NUM_VTLCALL_PARAMS, VsmError}; use num_enum::TryFromPrimitive; /// Bitmask of VPs currently executing VTL1 code. diff --git a/litebox_runner_lvbs/Cargo.toml b/litebox_runner_lvbs/Cargo.toml index 2297c6e3ce..10b9b8807c 100644 --- a/litebox_runner_lvbs/Cargo.toml +++ b/litebox_runner_lvbs/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" arrayvec = { version = "0.7.6", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } litebox_platform_lvbs = { version = "0.1.0", path = "../litebox_platform_lvbs", default-features = false } +litebox_common_lvbs = { version = "0.1.0", path = "../litebox_common_lvbs" } litebox_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_lvbs"] } litebox_common_optee = { path = "../litebox_common_optee/", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } diff --git a/litebox_runner_lvbs/src/lib.rs b/litebox_runner_lvbs/src/lib.rs index 14687571d3..c7b50b14f0 100644 --- a/litebox_runner_lvbs/src/lib.rs +++ b/litebox_runner_lvbs/src/lib.rs @@ -13,6 +13,7 @@ use litebox::{ utils::{ReinterpretSignedExt, TruncateExt}, }; use litebox_common_linux::errno::Errno; +use litebox_common_lvbs::{NUM_VTLCALL_PARAMS, VsmFunction}; use litebox_common_optee::{ OpteeMessageCommand, OpteeMsgArgs, OpteeRpcArgs, OpteeSmcArgs, OpteeSmcResult, OpteeSmcReturnCode, TeeOrigin, TeeResult, UteeEntryFunc, UteeParams, optee_msg_args_total_size, @@ -23,7 +24,7 @@ use litebox_platform_lvbs::{ host::{bootparam::get_vtl1_memory_info, per_cpu_variables}, mm::MemoryProvider, mshv::{ - NUM_VTLCALL_PARAMS, VsmFunction, hvcall, + hvcall, vsm::vsm_dispatch, vsm_intercept::raise_vtl0_gp_fault, vtl_switch::{vtl_switch, vtl_switch_init}, From c7e478363a8220e53637ca3d44a8fadf4b58d1ef Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 22 Jul 2026 17:28:39 -0700 Subject: [PATCH 118/319] Allocate broker shared-buffer slots (#1068) Adds operation-scoped shared-buffer descriptors and a FIFO allocator for the 16 local slots. Local callers reserve a slot until response consumption, and the host validates slot use with non-wrapping request IDs. This removes association-wide payload serialization; host execution remains serial until the next PR. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac86dd2d-0280-4d7b-87a8-5654ad1503a6 --- litebox/src/broker/mod.rs | 325 ++++++++++++++++- litebox/src/broker/shared_buffer.rs | 339 ++++++++++++++++++ litebox/src/event/counter.rs | 7 - litebox/src/pipes.rs | 7 - litebox_broker_host/src/lib.rs | 212 +++++++++-- litebox_broker_local/src/lib.rs | 18 +- litebox_broker_local/src/pipe.rs | 152 ++++---- litebox_broker_protocol/src/channel.rs | 6 - litebox_broker_protocol/src/pipe.rs | 9 +- litebox_broker_protocol/src/shared_memory.rs | 12 + litebox_broker_protocol/src/wire.rs | 17 +- litebox_broker_protocol/src/wire/pipe.rs | 23 +- litebox_broker_transport/src/unix_socket.rs | 59 --- .../tests/userland_broker.rs | 28 +- 14 files changed, 985 insertions(+), 229 deletions(-) create mode 100644 litebox/src/broker/shared_buffer.rs diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index cbd0677413..a9d78b8f18 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -10,8 +10,9 @@ use hashbrown::HashMap; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode}; -use litebox_broker_protocol::pipe::CreatePipeResponse; +use litebox_broker_protocol::pipe::{CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE}; use litebox_broker_protocol::readiness::ReadinessFlags; use crate::event::{Events, polling::Pollee}; @@ -19,7 +20,9 @@ use crate::platform::TimeProvider; use crate::sync::{Mutex, RawSyncPrimitivesProvider}; pub(crate) mod error; +mod shared_buffer; use error::BrokerControlError; +use shared_buffer::{SlotAllocator, SlotLease}; /// Local-core access to the negotiated broker control channel. /// @@ -147,6 +150,7 @@ pub(crate) struct BrokerLocalControl< > { local: Mutex>>>, pollable_registry: Arc>, + slot_allocator: SlotAllocator, } impl BrokerLocalControl @@ -161,6 +165,7 @@ where Self { local: Mutex::new(Some(Arc::new(local))), pollable_registry, + slot_allocator: SlotAllocator::new(), } } @@ -176,13 +181,28 @@ where Arc::clone(connection) }; let result = request(&connection).map_err(BrokerControlError::from); - if matches!(result.as_ref(), Err(BrokerControlError::AssociationFailed)) - && self.local.lock().take().is_some() - { - self.pollable_registry.notify_all(Events::ERR); + if matches!(result.as_ref(), Err(BrokerControlError::AssociationFailed)) { + self.fail_association(); } result } + + fn acquire_shared_buffer( + &self, + length: u32, + ) -> core::result::Result, BrokerControlError> { + self.slot_allocator.acquire(length).map_err(|_| { + self.fail_association(); + BrokerControlError::AssociationFailed + }) + } + + fn fail_association(&self) { + self.slot_allocator.fail(); + if self.local.lock().take().is_some() { + self.pollable_registry.notify_all(Events::ERR); + } + } } impl BrokerControl for BrokerLocalControl @@ -233,7 +253,17 @@ where handle: ObjectHandle, length: u32, ) -> core::result::Result, BrokerControlError> { - self.request(|local| local.read_pipe(handle, length)) + if length > MAX_PIPE_TRANSFER_SIZE { + return Err(BrokerControlError::Broker(ErrorCode::ResourceExhausted)); + } + let mut data = Vec::new(); + data.try_reserve_exact(length as usize) + .map_err(|_| BrokerControlError::Broker(ErrorCode::OutOfMemory))?; + data.resize(length as usize, 0); + let lease = self.acquire_shared_buffer(length)?; + let read = self.request(|local| local.read_pipe(handle, lease.descriptor(), &mut data))?; + data.truncate(read); + Ok(data) } fn write_pipe( @@ -241,7 +271,13 @@ where handle: ObjectHandle, data: &[u8], ) -> core::result::Result { - self.request(|local| local.write_pipe(handle, data)) + if data.len() > MAX_PIPE_TRANSFER_SIZE as usize { + return Err(BrokerControlError::Broker(ErrorCode::ResourceExhausted)); + } + let length = u32::try_from(data.len()) + .expect("validated shared pipe transfer length must fit in u32"); + let lease = self.acquire_shared_buffer(length)?; + self.request(|local| local.write_pipe(handle, lease.descriptor(), data)) } fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError> { @@ -249,10 +285,7 @@ where } fn fail_connection(&self) { - let connection = self.local.lock().take(); - if connection.is_some() { - self.pollable_registry.notify_all(Events::ERR); - } + self.fail_association(); } } @@ -264,3 +297,273 @@ pub(crate) fn readiness_events(readiness: ReadinessFlags) -> Events { events.set(Events::ERR, readiness.0 & ReadinessFlags::ERROR.0 != 0); events } + +#[cfg(test)] +mod tests { + extern crate std; + + use super::*; + use core::convert::Infallible; + use std::sync::{Arc as StdArc, Condvar as StdCondvar, Mutex as StdMutex, mpsc}; + use std::time::Duration; + + use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; + use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, + BrokerResponse, BrokerResult, PipeRequest, PipeResponse, + }; + use litebox_broker_protocol::pipe::{ReadPipeResponse, WritePipeResponse}; + use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, SharedBufferDescriptor, SharedMemory, + SharedMemoryError, + }; + + use crate::platform::mock::MockPlatform; + + #[test] + fn concurrent_pipe_writes_use_distinct_shared_buffer_leases() { + let memory = TestSharedMemory::new(); + let (observed_sender, observed_receiver) = mpsc::sync_channel(2); + let release = StdArc::new((StdMutex::new(false), StdCondvar::new())); + let channel = ConcurrentPipeChannel { + memory: memory.clone(), + observed_sender, + release: StdArc::clone(&release), + }; + let local = + BrokerLocal::negotiate(channel, |_| Ok(Arc::new(memory) as Arc)) + .unwrap(); + let control = Arc::new(BrokerLocalControl::::new( + local, + Arc::new(BrokerPollableRegistry::new()), + )); + let first_control = Arc::clone(&control); + let first = std::thread::spawn(move || first_control.write_pipe(ObjectHandle(1), b"first")); + let second_control = Arc::clone(&control); + let second = + std::thread::spawn(move || second_control.write_pipe(ObjectHandle(2), b"second")); + + let first_observed = observed_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + let second_observed = observed_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_ne!( + first_observed.0.slot_index, second_observed.0.slot_index, + "simultaneous payload calls reused one slot" + ); + let mut payloads = [first_observed.1, second_observed.1]; + payloads.sort(); + assert_eq!(payloads, [b"first".to_vec(), b"second".to_vec()]); + + let (released, available) = &*release; + *released.lock().unwrap() = true; + available.notify_all(); + assert_eq!(first.join().unwrap().unwrap(), 5); + assert_eq!(second.join().unwrap().unwrap(), 6); + } + + #[test] + fn concurrent_pipe_reads_retain_distinct_shared_buffer_data() { + let memory = TestSharedMemory::new(); + let (observed_sender, observed_receiver) = mpsc::sync_channel(2); + let release = StdArc::new((StdMutex::new(false), StdCondvar::new())); + let channel = ConcurrentPipeReadChannel { + memory: memory.clone(), + observed_sender, + release: StdArc::clone(&release), + }; + let local = + BrokerLocal::negotiate(channel, |_| Ok(Arc::new(memory) as Arc)) + .unwrap(); + let control = Arc::new(BrokerLocalControl::::new( + local, + Arc::new(BrokerPollableRegistry::new()), + )); + let first_control = Arc::clone(&control); + let first = std::thread::spawn(move || first_control.read_pipe(ObjectHandle(1), 3)); + let second_control = Arc::clone(&control); + let second = std::thread::spawn(move || second_control.read_pipe(ObjectHandle(2), 3)); + + let first_buffer = observed_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + let second_buffer = observed_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + assert_ne!( + first_buffer.slot_index, second_buffer.slot_index, + "simultaneous payload calls reused one slot" + ); + + let (released, available) = &*release; + *released.lock().unwrap() = true; + available.notify_all(); + assert_eq!(first.join().unwrap().unwrap(), [1; 3]); + assert_eq!(second.join().unwrap().unwrap(), [2; 3]); + } + + #[derive(Clone)] + struct TestSharedMemory(StdArc>>); + + impl TestSharedMemory { + fn new() -> Self { + Self(StdArc::new(StdMutex::new(std::vec![ + 0; + SHARED_BUFFER_POOL_SIZE + ]))) + } + } + + impl SharedMemory for TestSharedMemory { + fn len(&self) -> usize { + self.0.lock().unwrap().len() + } + + fn read( + &self, + offset: usize, + destination: &mut [u8], + ) -> core::result::Result<(), SharedMemoryError> { + let memory = self.0.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice( + memory + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?, + ); + Ok(()) + } + + fn write( + &self, + offset: usize, + source: &[u8], + ) -> core::result::Result<(), SharedMemoryError> { + let mut memory = self.0.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + memory + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)? + .copy_from_slice(source); + Ok(()) + } + } + + struct ConcurrentPipeChannel { + memory: TestSharedMemory, + observed_sender: mpsc::SyncSender<(SharedBufferDescriptor, std::vec::Vec)>, + release: StdArc<(StdMutex, StdCondvar)>, + } + + struct ConcurrentPipeReadChannel { + memory: TestSharedMemory, + observed_sender: mpsc::SyncSender, + release: StdArc<(StdMutex, StdCondvar)>, + } + + impl LocalControlChannel for ConcurrentPipeChannel { + type Error = 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, + })) + } + + fn call( + &self, + request: BrokerRequest, + ) -> core::result::Result { + let BrokerOperation::Pipe(PipeRequest::Write(write)) = request.operation else { + panic!("unexpected broker request"); + }; + let mut payload = std::vec![0; write.buffer.length as usize]; + self.memory + .read( + write.buffer.slot_index.0 as usize * SHARED_BUFFER_SLOT_SIZE as usize, + &mut payload, + ) + .unwrap(); + self.observed_sender.send((write.buffer, payload)).unwrap(); + let (released, available) = &*self.release; + let mut released = released.lock().unwrap(); + while !*released { + released = available.wait(released).unwrap(); + } + Ok(BrokerResponse { + request_id: request.request_id, + result: BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { + written: write.buffer.length, + })), + }) + } + } + + impl LocalControlChannel for ConcurrentPipeReadChannel { + type Error = 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, + })) + } + + fn call( + &self, + request: BrokerRequest, + ) -> core::result::Result { + let BrokerOperation::Pipe(PipeRequest::Read(read)) = request.operation else { + panic!("unexpected broker request"); + }; + let payload = std::vec![ + u8::try_from(read.handle.0).unwrap(); + read.buffer.length as usize + ]; + self.memory + .write( + read.buffer.slot_index.0 as usize * SHARED_BUFFER_SLOT_SIZE as usize, + &payload, + ) + .unwrap(); + self.observed_sender.send(read.buffer).unwrap(); + let (released, available) = &*self.release; + let mut released = released.lock().unwrap(); + while !*released { + released = available.wait(released).unwrap(); + } + Ok(BrokerResponse { + request_id: request.request_id, + result: BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { + read: read.buffer.length, + })), + }) + } + } +} diff --git a/litebox/src/broker/shared_buffer.rs b/litebox/src/broker/shared_buffer.rs new file mode 100644 index 0000000000..c4361f0e73 --- /dev/null +++ b/litebox/src/broker/shared_buffer.rs @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::collections::VecDeque; +use alloc::sync::Arc; +use core::sync::atomic::Ordering::{Acquire, Release}; + +use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_SLOT_COUNT, SharedBufferDescriptor, SharedBufferSlotIndex, +}; + +use crate::platform::RawMutex as _; +use crate::sync::{Mutex, RawSyncPrimitivesProvider}; + +const ALLOCATED_SLOT_MASK: u64 = (1 << SHARED_BUFFER_SLOT_COUNT) - 1; + +pub(super) struct SlotAllocator { + state: Mutex>, +} + +struct AllocatorState { + allocated_slots: u64, + next_slot: u32, + failed: bool, + waiters: VecDeque>>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) struct AcquireError; + +pub(super) struct SlotLease<'a, Platform: RawSyncPrimitivesProvider> { + allocator: &'a SlotAllocator, + descriptor: SharedBufferDescriptor, +} + +struct SlotWaiter { + length: u32, + result: Mutex>>, + completion: Platform::RawMutex, +} + +impl SlotAllocator { + pub(super) fn new() -> Self { + Self { + state: Mutex::new(AllocatorState { + allocated_slots: 0, + next_slot: 0, + failed: false, + waiters: VecDeque::new(), + }), + } + } + + pub(super) fn acquire(&self, length: u32) -> Result, AcquireError> { + { + let mut state = self.state.lock(); + if state.failed { + return Err(AcquireError); + } + if state.waiters.is_empty() + && let Some(descriptor) = state.allocate(length) + { + return Ok(SlotLease { + allocator: self, + descriptor, + }); + } + } + + let waiter = Arc::new(SlotWaiter::new(length)); + { + let mut state = self.state.lock(); + if state.failed { + return Err(AcquireError); + } + if state.waiters.is_empty() + && let Some(descriptor) = state.allocate(length) + { + return Ok(SlotLease { + allocator: self, + descriptor, + }); + } + state.waiters.push_back(Arc::clone(&waiter)); + } + + let descriptor = waiter.wait()?; + Ok(SlotLease { + allocator: self, + descriptor, + }) + } + + pub(super) fn fail(&self) -> bool { + let waiters = { + let mut state = self.state.lock(); + if state.failed { + return false; + } + state.failed = true; + core::mem::take(&mut state.waiters) + }; + for waiter in waiters { + waiter.resolve(Err(AcquireError)); + } + true + } + + fn release(&self, slot_index: SharedBufferSlotIndex) { + let mut state = self.state.lock(); + let slot_mask = 1 << slot_index.0; + assert_ne!( + state.allocated_slots & slot_mask, + 0, + "shared-buffer slot released without an active lease" + ); + state.allocated_slots &= !slot_mask; + if state.failed { + return; + } + let Some(waiter) = state.waiters.pop_front() else { + return; + }; + let descriptor = state + .allocate(waiter.length) + .expect("released slot was not available"); + drop(state); + waiter.resolve(Ok(descriptor)); + } + + #[cfg(test)] + fn waiter_count(&self) -> usize { + self.state.lock().waiters.len() + } +} + +impl SlotWaiter { + fn new(length: u32) -> Self { + Self { + length, + result: Mutex::new(None), + completion: Platform::RawMutex::INIT, + } + } + + fn resolve(&self, result: Result) { + let mut stored = self.result.lock(); + assert!(stored.is_none(), "shared-buffer waiter already resolved"); + *stored = Some(result); + drop(stored); + self.completion.underlying_atomic().fetch_add(1, Release); + self.completion.wake_one(); + } + + fn wait(&self) -> Result { + loop { + let mut result = self.result.lock(); + if let Some(result) = result.take() { + return result; + } + let observed = self.completion.underlying_atomic().load(Acquire); + drop(result); + let _ = self.completion.block(observed); + } + } +} + +impl SlotLease<'_, Platform> { + pub(super) const fn descriptor(&self) -> SharedBufferDescriptor { + self.descriptor + } +} + +impl Drop for SlotLease<'_, Platform> { + fn drop(&mut self) { + self.allocator.release(self.descriptor.slot_index); + } +} + +impl AllocatorState { + fn allocate(&mut self, length: u32) -> Option { + let slot_index = self.next_free_slot()?; + self.allocated_slots |= 1 << slot_index; + self.next_slot = (slot_index + 1) % SHARED_BUFFER_SLOT_COUNT; + Some(SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(slot_index), + length, + }) + } + + fn next_free_slot(&self) -> Option { + let available_slots = !self.allocated_slots & ALLOCATED_SLOT_MASK; + if available_slots == 0 { + return None; + } + let available_slots_after_next = available_slots & (u64::MAX << self.next_slot); + Some(if available_slots_after_next == 0 { + available_slots.trailing_zeros() + } else { + available_slots_after_next.trailing_zeros() + }) + } +} + +#[cfg(test)] +mod tests { + extern crate std; + + use super::*; + use alloc::sync::Arc; + use alloc::vec::Vec; + use std::sync::mpsc; + use std::time::Duration; + + use crate::platform::mock::MockPlatform; + + #[test] + fn leases_use_distinct_slots_and_reuse_released_slots() { + let allocator = SlotAllocator::::new(); + let mut leases = (0..SHARED_BUFFER_SLOT_COUNT) + .map(|_| allocator.acquire(7).unwrap()) + .collect::>(); + + for (index, lease) in leases.iter().enumerate() { + assert_eq!(lease.descriptor().slot_index.0 as usize, index); + assert_eq!(lease.descriptor().length, 7); + } + + drop(leases.remove(0)); + let reused = allocator.acquire(9).unwrap(); + assert_eq!(reused.descriptor().slot_index, SharedBufferSlotIndex(0)); + } + + #[test] + fn exhausted_allocator_wakes_one_waiter_on_release() { + let allocator = Arc::new(SlotAllocator::::new()); + let mut leases = (0..SHARED_BUFFER_SLOT_COUNT) + .map(|_| allocator.acquire(1).unwrap()) + .collect::>(); + let waiter_allocator = Arc::clone(&allocator); + let (sender, receiver) = mpsc::sync_channel(1); + let waiter = std::thread::spawn(move || { + let lease = waiter_allocator.acquire(1).unwrap(); + sender.send(lease.descriptor()).unwrap(); + }); + while allocator.waiter_count() == 0 { + std::thread::yield_now(); + } + assert!(matches!( + receiver.try_recv(), + Err(mpsc::TryRecvError::Empty) + )); + + drop(leases.remove(0)); + assert_eq!( + receiver.recv_timeout(Duration::from_secs(1)).unwrap(), + SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(0), + length: 1, + } + ); + waiter.join().unwrap(); + } + + #[test] + fn exhausted_allocator_serves_waiters_in_arrival_order() { + let allocator = Arc::new(SlotAllocator::::new()); + let mut leases = (0..SHARED_BUFFER_SLOT_COUNT) + .map(|_| allocator.acquire(1).unwrap()) + .collect::>(); + + let first_allocator = Arc::clone(&allocator); + let (first_acquired_sender, first_acquired_receiver) = mpsc::sync_channel(1); + let (release_first_sender, release_first_receiver) = mpsc::sync_channel(1); + let first = std::thread::spawn(move || { + let lease = first_allocator.acquire(1).unwrap(); + first_acquired_sender.send(lease.descriptor()).unwrap(); + release_first_receiver.recv().unwrap(); + }); + while allocator.waiter_count() != 1 { + std::thread::yield_now(); + } + + let second_allocator = Arc::clone(&allocator); + let (second_acquired_sender, second_acquired_receiver) = mpsc::sync_channel(1); + let second = std::thread::spawn(move || { + let lease = second_allocator.acquire(1).unwrap(); + second_acquired_sender.send(lease.descriptor()).unwrap(); + }); + while allocator.waiter_count() != 2 { + std::thread::yield_now(); + } + + drop(leases.remove(0)); + assert_eq!( + first_acquired_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap() + .slot_index, + SharedBufferSlotIndex(0) + ); + assert!(matches!( + second_acquired_receiver.try_recv(), + Err(mpsc::TryRecvError::Empty) + )); + + release_first_sender.send(()).unwrap(); + assert_eq!( + second_acquired_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap() + .slot_index, + SharedBufferSlotIndex(0) + ); + first.join().unwrap(); + second.join().unwrap(); + } + + #[test] + fn association_failure_wakes_waiters_and_prevents_new_leases() { + let allocator = Arc::new(SlotAllocator::::new()); + let _leases = (0..SHARED_BUFFER_SLOT_COUNT) + .map(|_| allocator.acquire(1).unwrap()) + .collect::>(); + let waiter_allocator = Arc::clone(&allocator); + let (sender, receiver) = mpsc::sync_channel(1); + let waiter = std::thread::spawn(move || { + sender.send(waiter_allocator.acquire(1).is_err()).unwrap(); + }); + while allocator.waiter_count() == 0 { + std::thread::yield_now(); + } + + assert!(allocator.fail()); + assert!(receiver.recv_timeout(Duration::from_secs(1)).unwrap()); + assert!(allocator.acquire(1).is_err()); + waiter.join().unwrap(); + } +} diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 5859da3437..5d178be6c6 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -489,12 +489,5 @@ mod tests { result, }) } - - fn with_serialized_payload( - &self, - transfer: impl FnOnce() -> T, - ) -> core::result::Result { - Ok(transfer()) - } } } diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index 8388d83a97..d436d694d7 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -1199,13 +1199,6 @@ mod tests { result, }) } - - fn with_serialized_payload( - &self, - transfer: impl FnOnce() -> T, - ) -> core::result::Result { - Ok(transfer()) - } } #[test] diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 7c85d18814..e3303196f0 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -17,7 +17,6 @@ extern crate std; use alloc::vec::Vec; use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential}; -use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; use litebox_broker_protocol::channel::{ HostControlChannel, HostNotificationChannel, HostReceive, PeerCredential, }; @@ -31,8 +30,10 @@ use litebox_broker_protocol::pipe::{ CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE, ReadPipeResponse, WritePipeResponse, }; use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_LAYOUT, SharedBufferPool, SharedBufferSlotIndex, SharedMemory, + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_SLOT_COUNT, SharedBufferDescriptor, SharedBufferPool, + SharedBufferSlotIndex, SharedMemory, }; +use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, RequestId}; mod error; @@ -47,9 +48,9 @@ pub use error::{BrokerHostError, Result}; /// Event mutations caused by control requests return readiness in their control /// response and do not also emit a duplicate notification. /// -/// `shared_buffers` belongs to this association. Pipe transfers currently reuse -/// slot zero serially. `send_shared_memory` runs after version -/// negotiation and before active requests begin. +/// `shared_buffers` belongs to this association. Payload descriptors are +/// validated against trusted per-slot claim state. `send_shared_memory` runs +/// after version negotiation and before active requests begin. pub fn serve_connection( core: &BrokerCore, control_channel: &mut ControlChannel, @@ -111,6 +112,7 @@ where } } + let mut shared_buffer_usage = SharedBufferUsage::new(); loop { let request = match control_channel .recv_request() @@ -127,11 +129,27 @@ where request_id, operation, } = request; + let buffer_descriptor = match &operation { + BrokerOperation::Pipe(PipeRequest::Read(request)) => Some(request.buffer), + BrokerOperation::Pipe(PipeRequest::Write(request)) => Some(request.buffer), + BrokerOperation::CloseObject(_) + | BrokerOperation::CheckReadiness(_) + | BrokerOperation::Event(_) + | BrokerOperation::Pipe(PipeRequest::Create(_)) => None, + }; + if let Some(descriptor) = buffer_descriptor { + shared_buffer_usage + .begin(request_id, descriptor, shared_buffers.layout()) + .map_err(BrokerHostError::Broker)?; + } let result = complete_request(handle_request(&session, operation, shared_buffers)) .map_err(BrokerHostError::Broker)?; control_channel .send_response(&BrokerResponse { request_id, result }) .map_err(BrokerHostError::Channel)?; + if let Some(descriptor) = buffer_descriptor { + shared_buffer_usage.end(request_id, descriptor.slot_index); + } } Ok(ConnectionTermination::PeerClosed) @@ -147,6 +165,61 @@ enum RequestFailure { Abort(ErrorCode), } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SharedBufferSlotState { + Unused, + Idle(RequestId), + Active(RequestId), +} + +struct SharedBufferUsage { + slots: [SharedBufferSlotState; SHARED_BUFFER_SLOT_COUNT as usize], +} + +impl SharedBufferUsage { + const fn new() -> Self { + Self { + slots: [SharedBufferSlotState::Unused; SHARED_BUFFER_SLOT_COUNT as usize], + } + } + + fn begin( + &mut self, + request_id: RequestId, + descriptor: SharedBufferDescriptor, + layout: litebox_broker_protocol::shared_memory::SharedBufferLayout, + ) -> core::result::Result<(), ErrorCode> { + if layout + .range(descriptor.slot_index, descriptor.length as usize) + .is_err() + { + return Err(ErrorCode::MalformedRequest); + } + let slot = &mut self.slots[descriptor.slot_index.0 as usize]; + // A local lease spans response consumption, so honest reuse of this slot + // always carries a newer, non-wrapping request ID. + match *slot { + SharedBufferSlotState::Unused => {} + SharedBufferSlotState::Idle(last_request_id) if request_id > last_request_id => {} + SharedBufferSlotState::Idle(_) | SharedBufferSlotState::Active(_) => { + return Err(ErrorCode::MalformedRequest); + } + } + *slot = SharedBufferSlotState::Active(request_id); + Ok(()) + } + + fn end(&mut self, request_id: RequestId, slot_index: SharedBufferSlotIndex) { + let slot = &mut self.slots[slot_index.0 as usize]; + assert_eq!( + *slot, + SharedBufferSlotState::Active(request_id), + "shared-buffer slot state changed before response emission" + ); + *slot = SharedBufferSlotState::Idle(request_id); + } +} + fn complete_request( result: RequestResult, ) -> core::result::Result { @@ -185,8 +258,6 @@ fn handle_pipe_request( request: PipeRequest, shared_buffers: &SharedBufferPool, ) -> RequestResult { - const SERIALIZED_PIPE_SLOT: SharedBufferSlotIndex = SharedBufferSlotIndex(0); - match request { PipeRequest::Create(request) => { litebox_broker_core::pipe::create(session, request.capacity, request.atomic_write_size) @@ -199,13 +270,14 @@ fn handle_pipe_request( .map_err(|error| RequestFailure::Respond(error.into())) } PipeRequest::Read(request) => { - if request.length > MAX_PIPE_TRANSFER_SIZE { - return Err(RequestFailure::Respond(ErrorCode::MalformedRequest)); + if request.buffer.length > MAX_PIPE_TRANSFER_SIZE { + return Err(RequestFailure::Abort(ErrorCode::MalformedRequest)); } - let data = litebox_broker_core::pipe::read(session, request.handle, request.length) - .map_err(|error| RequestFailure::Respond(error.into()))?; + let data = + litebox_broker_core::pipe::read(session, request.handle, request.buffer.length) + .map_err(|error| RequestFailure::Respond(error.into()))?; shared_buffers - .write(SERIALIZED_PIPE_SLOT, &data) + .write(request.buffer.slot_index, &data) .map_err(|_| RequestFailure::Abort(ErrorCode::Internal))?; Ok(PipeResponse::Read(ReadPipeResponse { read: data @@ -215,17 +287,17 @@ fn handle_pipe_request( })) } PipeRequest::Write(request) => { - if request.length > MAX_PIPE_TRANSFER_SIZE { - return Err(RequestFailure::Respond(ErrorCode::MalformedRequest)); + if request.buffer.length > MAX_PIPE_TRANSFER_SIZE { + return Err(RequestFailure::Abort(ErrorCode::MalformedRequest)); } - let length = request.length as usize; + let length = request.buffer.length as usize; let mut data = Vec::new(); if data.try_reserve_exact(length).is_err() { return Err(RequestFailure::Respond(ErrorCode::OutOfMemory)); } data.resize(length, 0); shared_buffers - .read(SERIALIZED_PIPE_SLOT, &mut data) + .read(request.buffer.slot_index, &mut data) .map_err(|_| RequestFailure::Abort(ErrorCode::Internal))?; litebox_broker_core::pipe::write(session, request.handle, &data) .map_err(|error| RequestFailure::Respond(error.into())) @@ -285,7 +357,8 @@ mod tests { use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerNotification}; use litebox_broker_protocol::pipe::{CreatePipeRequest, ReadPipeRequest, WritePipeRequest}; use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, SharedMemoryError, + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, + SharedBufferDescriptor, SharedBufferPool, SharedMemoryError, }; use litebox_broker_protocol::{ObjectHandle, ProtocolVersion, RequestId}; use std::sync::{Arc, Mutex}; @@ -305,10 +378,12 @@ mod tests { serve_connection_returns_channel_error_when_response_send_fails(&broker); serve_connection_returns_event_readiness_in_control_responses(&broker); serve_connection_continues_after_recoverable_request_failure(&broker); + serve_connection_aborts_on_stale_shared_buffer_request(&broker); serve_connection_aborts_without_response_on_shared_memory_failure(&broker); serve_connection_rejects_incompatible_shared_buffer_layout(&broker); active_request_closes_object_reference(&broker); - association_shared_buffer_slot_zero_stages_pipe_data(&broker); + association_shared_buffer_descriptors_stage_pipe_data(&broker); + shared_buffer_usage_rejects_invalid_descriptors(); } fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { @@ -552,7 +627,7 @@ mod tests { Ok(HostReceive::Message(BrokerOperation::Pipe( PipeRequest::Read(ReadPipeRequest { handle: ObjectHandle(u64::MAX), - length: 1, + buffer: descriptor(0, 1), }), ))), Ok(HostReceive::Message(BrokerOperation::Event( @@ -585,6 +660,40 @@ mod tests { assert_eq!(channel.response_ids, [RequestId(0), RequestId(1)]); } + fn serve_connection_aborts_on_stale_shared_buffer_request(broker: &BrokerCore) { + let stale_request = BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { + handle: ObjectHandle(u64::MAX), + buffer: descriptor(0, 1), + })); + let mut channel = FakeHostControlChannel::new( + std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + }))]), + std::vec::Vec::from([ + Ok(HostReceive::Message(stale_request.clone())), + Ok(HostReceive::Message(stale_request)), + ]), + ); + channel.request_id_step = 0; + let mut notifications = FakeHostNotificationChannel::default(); + + assert!(matches!( + serve_connection( + broker, + &mut channel, + &mut notifications, + &test_shared_buffers(), + |_| Ok(()), + ), + Err(BrokerHostError::Broker(ErrorCode::MalformedRequest)) + )); + assert_eq!( + channel.results, + [BrokerResult::Error(ErrorCode::UnknownObject)] + ); + assert_eq!(channel.response_ids, [RequestId(0)]); + } + fn serve_connection_aborts_without_response_on_shared_memory_failure(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { @@ -686,7 +795,7 @@ mod tests { ); } - fn association_shared_buffer_slot_zero_stages_pipe_data(broker: &BrokerCore) { + fn association_shared_buffer_descriptors_stage_pipe_data(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); @@ -708,13 +817,13 @@ mod tests { }; shared_buffers - .write(SharedBufferSlotIndex(0), &[1, 2, 3]) + .write(SharedBufferSlotIndex(2), &[1, 2, 3]) .unwrap(); let write = handle_test_request_with_buffers( &session, BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle: response.write_handle, - length: 3, + buffer: descriptor(2, 3), })), &shared_buffers, ); @@ -727,7 +836,7 @@ mod tests { &session, BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { handle: response.read_handle, - length: 3, + buffer: descriptor(4, 3), })), &shared_buffers, ); @@ -737,7 +846,7 @@ mod tests { ); let mut data = [0; 3]; shared_buffers - .read(SharedBufferSlotIndex(0), &mut data) + .read(SharedBufferSlotIndex(4), &mut data) .unwrap(); assert_eq!(data, [1, 2, 3]); let mut second_slot = [0]; @@ -745,21 +854,52 @@ mod tests { .read(SharedBufferSlotIndex(1), &mut second_slot) .unwrap(); assert_eq!(second_slot, [9]); + } - let invalid_range = handle_test_request_with_buffers( - &session, - BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { - handle: response.write_handle, - length: MAX_PIPE_TRANSFER_SIZE + 1, - })), - &shared_buffers, + fn shared_buffer_usage_rejects_invalid_descriptors() { + let mut usage = SharedBufferUsage::new(); + usage + .begin(RequestId(1), descriptor(0, 3), SHARED_BUFFER_LAYOUT) + .unwrap(); + assert_eq!( + usage.begin(RequestId(2), descriptor(0, 3), SHARED_BUFFER_LAYOUT), + Err(ErrorCode::MalformedRequest) + ); + usage.end(RequestId(1), SharedBufferSlotIndex(0)); + assert_eq!( + usage.begin(RequestId(1), descriptor(0, 3), SHARED_BUFFER_LAYOUT), + Err(ErrorCode::MalformedRequest) + ); + assert_eq!( + usage.begin(RequestId(0), descriptor(0, 3), SHARED_BUFFER_LAYOUT), + Err(ErrorCode::MalformedRequest) + ); + assert!( + usage + .begin(RequestId(3), descriptor(0, 3), SHARED_BUFFER_LAYOUT) + .is_ok() + ); + assert_eq!( + usage.begin(RequestId(2), descriptor(16, 3), SHARED_BUFFER_LAYOUT), + Err(ErrorCode::MalformedRequest) ); assert_eq!( - invalid_range, - BrokerResult::Error(ErrorCode::MalformedRequest) + usage.begin( + RequestId(2), + descriptor(1, SHARED_BUFFER_SLOT_SIZE + 1), + SHARED_BUFFER_LAYOUT + ), + Err(ErrorCode::MalformedRequest) ); } + const fn descriptor(slot: u32, length: u32) -> SharedBufferDescriptor { + SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(slot), + length, + } + } + fn handle_test_request(session: &BrokerSession, operation: BrokerOperation) -> BrokerResult { handle_test_request_with_buffers(session, operation, &test_shared_buffers()) } @@ -788,6 +928,7 @@ mod tests { results: std::vec::Vec, response_ids: std::vec::Vec, next_request_id: u64, + request_id_step: u64, enqueue_readiness_requests_after_create: bool, enqueue_write_request_after_pipe_create: bool, send_error: bool, @@ -807,6 +948,7 @@ mod tests { results: std::vec::Vec::new(), response_ids: std::vec::Vec::new(), next_request_id: 0, + request_id_step: 1, enqueue_readiness_requests_after_create: false, enqueue_write_request_after_pipe_create: false, send_error: false, @@ -853,7 +995,7 @@ mod tests { Ok(match received { HostReceive::Message(request) => { let request_id = RequestId(self.next_request_id); - self.next_request_id += 1; + self.next_request_id += self.request_id_step; HostReceive::Message(BrokerRequest { request_id, operation: request, @@ -898,7 +1040,7 @@ mod tests { .push(Ok(HostReceive::Message(BrokerOperation::Pipe( PipeRequest::Write(WritePipeRequest { handle: response.write_handle, - length: 1, + buffer: descriptor(0, 1), }), )))); self.operations.push(Ok(HostReceive::PeerClosed)); diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index d7d6b83fdb..360c9e0642 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -39,8 +39,8 @@ pub use error::{BrokerLocalError, Result}; /// Typed broker-local control adapter for broker operations. /// -/// The shared-buffer pool belongs to the broker association. Pipe transfers -/// currently reuse slot zero under the channel's serialization scope. +/// The shared-buffer pool belongs to the broker association. Payload request +/// descriptors identify operation-scoped slots managed by the caller. pub struct BrokerLocal { channel: Channel, shared_buffers: SharedBufferPool>, @@ -633,13 +633,6 @@ mod tests { result, }) } - - fn with_serialized_payload( - &self, - transfer: impl FnOnce() -> T, - ) -> core::result::Result { - Ok(transfer()) - } } struct FakeNotificationChannel { @@ -678,13 +671,6 @@ mod tests { result: BrokerResult::ObjectClosed, }) } - - fn with_serialized_payload( - &self, - transfer: impl FnOnce() -> T, - ) -> core::result::Result { - Ok(transfer()) - } } impl LocalNotificationChannel for FakeNotificationChannel { diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs index 716764040f..da0688dd6e 100644 --- a/litebox_broker_local/src/pipe.rs +++ b/litebox_broker_local/src/pipe.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use alloc::vec::Vec; - use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::message::{BrokerOperation, BrokerResult, PipeRequest, PipeResponse}; @@ -10,19 +8,18 @@ use litebox_broker_protocol::pipe::{ CreatePipeRequest, CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE, ReadPipeRequest, WritePipeRequest, }; -use litebox_broker_protocol::shared_memory::SharedBufferSlotIndex; +use litebox_broker_protocol::shared_memory::SharedBufferDescriptor; use crate::{BrokerLocal, BrokerLocalError, Result}; -const SERIALIZED_PIPE_SLOT: SharedBufferSlotIndex = SharedBufferSlotIndex(0); - impl BrokerLocal { /// Creates a broker-owned byte pipe. /// /// # Panics /// /// Panics if the broker reports an unrecoverable error or returns a - /// response that does not match the issued pipe request. + /// response that does not match the issued pipe request, or if `buffer` is + /// not a valid lease whose length matches `destination`. pub fn create_pipe( &self, capacity: u64, @@ -38,81 +35,83 @@ impl BrokerLocal { Ok(response) } - /// Reads bytes from a broker-owned pipe. + /// Reads bytes from a broker-owned pipe into an operation-scoped shared + /// buffer lease. + /// + /// The caller must retain exclusive ownership of the descriptor's slot + /// until this method returns. /// /// # Panics /// /// Panics if the broker reports an unrecoverable error or returns a - /// response that does not match the issued pipe request. - pub fn read_pipe(&self, handle: ObjectHandle, length: u32) -> Result, Channel::Error> { - self.channel - .with_serialized_payload(|| self.read_pipe_serialized(handle, length)) - .map_err(BrokerLocalError::Channel)? - } - - fn read_pipe_serialized( + /// response that does not match the issued pipe request, or if `buffer` is + /// not a valid lease whose length matches `data`. + pub fn read_pipe( &self, handle: ObjectHandle, - length: u32, - ) -> Result, Channel::Error> { - if length > MAX_PIPE_TRANSFER_SIZE { + buffer: SharedBufferDescriptor, + destination: &mut [u8], + ) -> Result { + if buffer.length > MAX_PIPE_TRANSFER_SIZE { return Err(BrokerLocalError::Broker( litebox_broker_protocol::error::ErrorCode::ResourceExhausted, )); } - let mut data = Vec::new(); - data.try_reserve_exact(length as usize).map_err(|_| { - BrokerLocalError::Broker(litebox_broker_protocol::error::ErrorCode::OutOfMemory) - })?; - data.resize(length as usize, 0); - let response = self.request_pipe(PipeRequest::Read(ReadPipeRequest { handle, length }))?; + assert_eq!( + destination.len(), + buffer.length as usize, + "shared pipe read destination must match its descriptor" + ); + self.shared_buffers + .layout() + .range(buffer.slot_index, destination.len()) + .expect("shared pipe read descriptor must identify a valid slot range"); + let response = self.request_pipe(PipeRequest::Read(ReadPipeRequest { handle, buffer }))?; let PipeResponse::Read(response) = response else { panic!("broker returned unexpected pipe read response: {response:?}"); }; assert!( - response.read <= length, + response.read <= buffer.length, "broker returned oversized pipe read" ); let read = response.read as usize; - data.truncate(read); self.shared_buffers - .read(SERIALIZED_PIPE_SLOT, &mut data) + .read(buffer.slot_index, &mut destination[..read]) .expect("validated shared pipe read range must be accessible"); - Ok(data) + Ok(read) } - /// Writes bytes to a broker-owned pipe. + /// Writes bytes to a broker-owned pipe from an operation-scoped shared + /// buffer lease. + /// + /// The caller must retain exclusive ownership of the descriptor's slot + /// until this method returns. /// /// # Panics /// /// Panics if the broker reports an unrecoverable error or returns a /// response that does not match the issued pipe request. - pub fn write_pipe(&self, handle: ObjectHandle, data: &[u8]) -> Result { - self.channel - .with_serialized_payload(|| self.write_pipe_serialized(handle, data)) - .map_err(BrokerLocalError::Channel)? - } - - fn write_pipe_serialized( + pub fn write_pipe( &self, handle: ObjectHandle, + buffer: SharedBufferDescriptor, data: &[u8], ) -> Result { - if data.len() > MAX_PIPE_TRANSFER_SIZE as usize { + if buffer.length > MAX_PIPE_TRANSFER_SIZE { return Err(BrokerLocalError::Broker( litebox_broker_protocol::error::ErrorCode::ResourceExhausted, )); } + assert_eq!( + data.len(), + buffer.length as usize, + "shared pipe write data must match its descriptor" + ); self.shared_buffers - .write(SERIALIZED_PIPE_SLOT, data) + .write(buffer.slot_index, data) .expect("validated shared pipe write range must be accessible"); - let response = self.request_pipe(PipeRequest::Write(WritePipeRequest { - handle, - length: data - .len() - .try_into() - .expect("shared pipe transfer length must fit in u32"), - }))?; + let response = + self.request_pipe(PipeRequest::Write(WritePipeRequest { handle, buffer }))?; let PipeResponse::Write(response) = response else { panic!("broker returned unexpected pipe write response: {response:?}"); }; @@ -141,6 +140,7 @@ impl BrokerLocal { mod tests { use super::*; use alloc::sync::Arc; + use alloc::vec::Vec; use core::{cell::RefCell, convert::Infallible}; use std::collections::VecDeque; use std::sync::Mutex; @@ -153,11 +153,12 @@ mod tests { }; use litebox_broker_protocol::pipe::{ReadPipeResponse, WritePipeResponse}; use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, SharedMemory, SharedMemoryError, + SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, SharedBufferSlotIndex, SharedMemory, + SharedMemoryError, }; #[test] - fn pipe_uses_slot_zero_for_serialized_data_operations() { + fn pipe_uses_the_descriptor_slot_for_data_operations() { let read_handle = ObjectHandle(1); let write_handle = ObjectHandle(2); let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); @@ -170,23 +171,31 @@ mod tests { BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2 })), ]); let local = BrokerLocal::negotiate(channel, |_| Ok(memory.clone())).unwrap(); - memory - .write(SHARED_BUFFER_SLOT_SIZE as usize, &[9]) - .unwrap(); + let write_buffer = descriptor(2, 3); + let read_buffer = descriptor(4, 3); local.create_pipe(64, 16).unwrap(); - assert_eq!(local.write_pipe(write_handle, &[1, 2, 3]).unwrap(), 2); + assert_eq!( + local + .write_pipe(write_handle, write_buffer, &[1, 2, 3]) + .unwrap(), + 2 + ); let mut staged = [0; 3]; - memory.read(0, &mut staged).unwrap(); + memory + .read(2 * SHARED_BUFFER_SLOT_SIZE as usize, &mut staged) + .unwrap(); assert_eq!(staged, [1, 2, 3]); - memory.write(0, &[4, 5, 6]).unwrap(); - assert_eq!(local.read_pipe(read_handle, 3).unwrap(), [4, 5]); - let mut second_slot = [0]; memory - .read(SHARED_BUFFER_SLOT_SIZE as usize, &mut second_slot) + .write(4 * SHARED_BUFFER_SLOT_SIZE as usize, &[4, 5, 6]) .unwrap(); - assert_eq!(second_slot, [9]); + let mut read_data = [0; 3]; + let read = local + .read_pipe(read_handle, read_buffer, &mut read_data) + .unwrap(); + assert_eq!(read, 2); + assert_eq!(&read_data[..read], &[4, 5]); assert_eq!( local.channel.sent_operations.borrow().as_slice(), &[ @@ -196,11 +205,11 @@ mod tests { })), BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle: write_handle, - length: 3, + buffer: write_buffer, })), BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { handle: read_handle, - length: 3, + buffer: read_buffer, })), ] ); @@ -211,16 +220,16 @@ mod tests { let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); let channel = ScriptedChannel::new([]); let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); - let oversized_length = MAX_PIPE_TRANSFER_SIZE as usize + 1; + let oversized = descriptor(0, MAX_PIPE_TRANSFER_SIZE + 1); assert!(matches!( - local.read_pipe(ObjectHandle(1), u32::try_from(oversized_length).unwrap()), + local.read_pipe(ObjectHandle(1), oversized, &mut []), Err(BrokerLocalError::Broker( litebox_broker_protocol::error::ErrorCode::ResourceExhausted )) )); assert!(matches!( - local.write_pipe(ObjectHandle(2), &std::vec![0; oversized_length]), + local.write_pipe(ObjectHandle(2), oversized, &[]), Err(BrokerLocalError::Broker( litebox_broker_protocol::error::ErrorCode::ResourceExhausted )) @@ -237,8 +246,9 @@ mod tests { }))]); let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + let mut destination = [0]; - let _ = local.read_pipe(ObjectHandle(1), 1); + let _ = local.read_pipe(ObjectHandle(1), descriptor(0, 1), &mut destination); } #[test] @@ -251,7 +261,14 @@ mod tests { let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); - let _ = local.write_pipe(ObjectHandle(1), &[0]); + let _ = local.write_pipe(ObjectHandle(1), descriptor(0, 1), &[0]); + } + + const fn descriptor(slot: u32, length: u32) -> SharedBufferDescriptor { + SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(slot), + length, + } } #[derive(Clone)] @@ -347,12 +364,5 @@ mod tests { .expect("response requires a scripted result"), }) } - - fn with_serialized_payload( - &self, - transfer: impl FnOnce() -> T, - ) -> core::result::Result { - Ok(transfer()) - } } } diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index 6566a3a188..9e02fd116f 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -61,12 +61,6 @@ pub trait LocalControlChannel { /// association is considered failed: every concurrent or future call must /// return an error rather than remain blocked. fn call(&self, request: BrokerRequest) -> Result; - - /// Serializes one complete shared-memory payload transfer. - /// - /// The closure must run exactly once while no other payload transfer using - /// the same association shared memory is active. - fn with_serialized_payload(&self, transfer: impl FnOnce() -> T) -> Result; } /// Host-side control channel for broker authority calls. diff --git a/litebox_broker_protocol/src/pipe.rs b/litebox_broker_protocol/src/pipe.rs index 4323bfea49..5369f179e2 100644 --- a/litebox_broker_protocol/src/pipe.rs +++ b/litebox_broker_protocol/src/pipe.rs @@ -2,6 +2,7 @@ // Licensed under the MIT license. use crate::ObjectHandle; +use crate::shared_memory::SharedBufferDescriptor; /// Maximum pipe transfer described by one control-path request or response. /// @@ -32,8 +33,8 @@ pub struct CreatePipeResponse { pub struct ReadPipeRequest { /// Read endpoint handle. pub handle: ObjectHandle, - /// Maximum number of bytes to return. - pub length: u32, + /// Leased shared-buffer region to receive the bytes. + pub buffer: SharedBufferDescriptor, } /// Response describing bytes read into shared memory. @@ -48,8 +49,8 @@ pub struct ReadPipeResponse { pub struct WritePipeRequest { /// Write endpoint handle. pub handle: ObjectHandle, - /// Number of staged bytes to write. - pub length: u32, + /// Leased shared-buffer region containing the staged bytes. + pub buffer: SharedBufferDescriptor, } /// Response describing a completed pipe write. diff --git a/litebox_broker_protocol/src/shared_memory.rs b/litebox_broker_protocol/src/shared_memory.rs index 12fc40d70a..bed7da4c4b 100644 --- a/litebox_broker_protocol/src/shared_memory.rs +++ b/litebox_broker_protocol/src/shared_memory.rs @@ -169,6 +169,18 @@ impl SharedBufferLayout { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SharedBufferSlotIndex(pub u32); +/// Identifies one operation-scoped region in the association shared-buffer pool. +/// +/// The slot offset is derived from the trusted association layout and is never +/// supplied by the peer. The request variant determines the transfer direction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SharedBufferDescriptor { + /// Slot used by this operation. + pub slot_index: SharedBufferSlotIndex, + /// Number of bytes used from the start of the slot. + pub length: u32, +} + /// A shared-memory resource viewed as a checked fixed-slot buffer pool. /// /// Slot ownership and reuse remain responsibilities of the protocol using the diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 16ccf84691..6b92818484 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -322,6 +322,7 @@ mod tests { CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, WritePipeResponse, }; + use crate::shared_memory::{SharedBufferDescriptor, SharedBufferSlotIndex}; use crate::{ObjectHandle, ProtocolVersion, RequestId}; const TEST_REQUEST_ID: RequestId = RequestId(0x0102_0304_0506_0708); @@ -365,8 +366,20 @@ mod tests { capacity: 4096, atomic_write_size: 512, })), - BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { handle, length: 32 })), - BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { handle, length: 3 })), + BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { + handle, + buffer: SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(2), + length: 32, + }, + })), + BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { + handle, + buffer: SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(15), + length: 3, + }, + })), ]; for operation in operations { diff --git a/litebox_broker_protocol/src/wire/pipe.rs b/litebox_broker_protocol/src/wire/pipe.rs index 1d6a86bdd2..6ef2520a5b 100644 --- a/litebox_broker_protocol/src/wire/pipe.rs +++ b/litebox_broker_protocol/src/wire/pipe.rs @@ -6,6 +6,7 @@ use crate::pipe::{ CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, WritePipeResponse, }; +use crate::shared_memory::{SharedBufferDescriptor, SharedBufferSlotIndex}; use super::WireError; use super::primitive::{Decoder, Encoder}; @@ -28,12 +29,12 @@ pub(super) fn encode_pipe_request(encoder: &mut Encoder, request: PipeRequest) { PipeRequest::Read(request) => { encoder.u8(PIPE_REQUEST_TAG_READ); encoder.handle(request.handle); - encoder.u32(request.length); + encode_shared_buffer_descriptor(encoder, request.buffer); } PipeRequest::Write(request) => { encoder.u8(PIPE_REQUEST_TAG_WRITE); encoder.handle(request.handle); - encoder.u32(request.length); + encode_shared_buffer_descriptor(encoder, request.buffer); } } } @@ -46,16 +47,30 @@ pub(super) fn decode_pipe_request(decoder: &mut Decoder<'_>) -> Result Ok(PipeRequest::Read(ReadPipeRequest { handle: decoder.handle()?, - length: decoder.u32()?, + buffer: decode_shared_buffer_descriptor(decoder)?, })), PIPE_REQUEST_TAG_WRITE => Ok(PipeRequest::Write(WritePipeRequest { handle: decoder.handle()?, - length: decoder.u32()?, + buffer: decode_shared_buffer_descriptor(decoder)?, })), _ => Err(WireError::InvalidTag), } } +fn encode_shared_buffer_descriptor(encoder: &mut Encoder, descriptor: SharedBufferDescriptor) { + encoder.u32(descriptor.slot_index.0); + encoder.u32(descriptor.length); +} + +fn decode_shared_buffer_descriptor( + decoder: &mut Decoder<'_>, +) -> Result { + Ok(SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(decoder.u32()?), + length: decoder.u32()?, + }) +} + pub(super) fn encode_pipe_response(encoder: &mut Encoder, response: PipeResponse) { match response { PipeResponse::Create(response) => { diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index cc8bc236c8..24e93627d2 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -99,7 +99,6 @@ struct UnixStreamLocalActive { request_stream: Mutex, shutdown_stream: UnixStream, pending_calls: Arc, - payload_transfer: Mutex<()>, association_failure: Arc, } @@ -193,7 +192,6 @@ impl UnixStreamLocalControlChannel { request_stream: Mutex::new(request_stream), shutdown_stream, pending_calls: Arc::clone(&pending_calls), - payload_transfer: Mutex::new(()), association_failure: Arc::clone(&association_failure), }); Ok(UnixStreamLocalControlCancellation { @@ -387,17 +385,6 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { pending_call.wait() } - - fn with_serialized_payload(&self, transfer: impl FnOnce() -> T) -> IoResult { - let UnixStreamLocalControlState::Active(active) = &self.state else { - return Err(invalid_data("broker control channel is not active")); - }; - let _transfer = active - .payload_transfer - .lock() - .expect("broker payload-transfer mutex poisoned"); - Ok(transfer()) - } } impl HostControlChannel for UnixStreamHostControlChannel { @@ -1000,52 +987,6 @@ mod tests { assert_eq!(second.join().unwrap().unwrap().request_id, RequestId(7)); } - #[test] - fn active_channel_serializes_shared_payload_transfers() { - let (local_stream, _host_stream) = UnixStream::pair().unwrap(); - let (channel, _cancellation) = activate_test_channel(local_stream, || {}); - let channel = Arc::new(channel); - let (first_entered_sender, first_entered_receiver) = mpsc::sync_channel(1); - let (release_first_sender, release_first_receiver) = mpsc::sync_channel(1); - let first_channel = Arc::clone(&channel); - let first = thread::spawn(move || { - first_channel - .with_serialized_payload(|| { - first_entered_sender.send(()).unwrap(); - release_first_receiver.recv().unwrap(); - }) - .unwrap(); - }); - first_entered_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - - let (second_started_sender, second_started_receiver) = mpsc::sync_channel(1); - let (second_entered_sender, second_entered_receiver) = mpsc::sync_channel(1); - let second = thread::spawn(move || { - second_started_sender.send(()).unwrap(); - channel - .with_serialized_payload(|| second_entered_sender.send(()).unwrap()) - .unwrap(); - }); - second_started_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - let entered_before_release = second_entered_receiver - .recv_timeout(Duration::from_millis(100)) - .is_ok(); - - release_first_sender.send(()).unwrap(); - if !entered_before_release { - second_entered_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - } - first.join().unwrap(); - second.join().unwrap(); - assert!(!entered_before_release); - } - #[test] fn active_channel_bounds_pending_calls_before_publication() { let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index c9507d96d7..1df0839abf 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -10,7 +10,9 @@ use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::readiness::ReadinessFlags; -use litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE; +use litebox_broker_protocol::shared_memory::{ + SHARED_BUFFER_POOL_SIZE, SharedBufferDescriptor, SharedBufferSlotIndex, +}; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, }; @@ -108,16 +110,28 @@ fn run_fake_runner(args: &[OsString]) { let pipe = local.create_pipe(64, 16).unwrap(); let data = b"shared pipe data"; - assert_eq!( - local.write_pipe(pipe.write_handle, data).unwrap(), - data.len() - ); + let write_buffer = SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(0), + length: data.len().try_into().unwrap(), + }; assert_eq!( local - .read_pipe(pipe.read_handle, data.len().try_into().unwrap()) + .write_pipe(pipe.write_handle, write_buffer, data) .unwrap(), - data + data.len() ); + let mut received = [0; 16]; + let read = local + .read_pipe( + pipe.read_handle, + SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(1), + length: received.len().try_into().unwrap(), + }, + &mut received, + ) + .unwrap(); + assert_eq!(&received[..read], data); drop(local); } From c5606f7f22b326313bd4514552968c28282eec5e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 22 Jul 2026 19:59:05 -0700 Subject: [PATCH 119/319] Execute broker requests concurrently (#1070) Adds a portable host association for concurrent request execution and splits active Unix control into request, response, and shutdown handles. The userland broker dispatches a bounded 64-request queue across eight workers, serializes complete response frames in completion order, and preserves shared-buffer slot validation. Reader, worker, and writer failures converge on fail-closed socket shutdown and bounded cleanup. --------- Copilot-Session: ac86dd2d-0280-4d7b-87a8-5654ad1503a6 --- Cargo.lock | 1 + litebox_broker_host/Cargo.toml | 1 + litebox_broker_host/src/lib.rs | 409 +++++++++++++++--- litebox_broker_transport/src/unix_socket.rs | 140 +++++- litebox_broker_userland/src/main.rs | 258 +++++++++-- .../tests/userland_broker.rs | 40 +- 6 files changed, 756 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 616362318a..d48bb2d0f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,6 +1493,7 @@ version = "0.1.0" dependencies = [ "litebox_broker_core", "litebox_broker_protocol", + "spin 0.9.8", "thiserror", ] diff --git a/litebox_broker_host/Cargo.toml b/litebox_broker_host/Cargo.toml index 3457368fdd..03a7c344ae 100644 --- a/litebox_broker_host/Cargo.toml +++ b/litebox_broker_host/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +spin = { version = "0.9.8", default-features = false, features = ["spin_mutex"] } thiserror = { version = "2.0.6", default-features = false } [lints] diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index e3303196f0..d40ed162e0 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -34,33 +34,110 @@ use litebox_broker_protocol::shared_memory::{ SharedBufferSlotIndex, SharedMemory, }; use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, RequestId}; +use spin::mutex::SpinMutex; mod error; pub use error::{BrokerHostError, Result}; -/// Authenticates, negotiates, and serves one broker association over paired -/// control and notification channels. +/// Negotiated active association, or a terminal outcome reached during setup. +pub type ConnectionSetup<'a, Memory> = + core::result::Result, ConnectionTermination>; + +/// Active portable broker association. /// -/// The deployment must bind both channels to the same authenticated peer -/// association. Active requests and responses remain on the control channel; -/// broker-initiated readiness wakeups are sent on the notification channel. -/// Event mutations caused by control requests return readiness in their control -/// response and do not also emit a duplicate notification. +/// Deployments may share this value across bounded workers. Each request is +/// executed independently, while shared-buffer usage is synchronized and +/// released immediately before publishing the response. +pub struct BrokerHostAssociation<'a, Memory: SharedMemory> { + session: BrokerSession, + shared_buffers: &'a SharedBufferPool, + state: SpinMutex, +} + +struct AssociationState { + failed: bool, + shared_buffer_usage: SharedBufferUsage, +} + +impl BrokerHostAssociation<'_, Memory> { + /// Executes one active request and emits its response. + /// + /// Any fatal broker or response-channel error permanently fails this + /// association. Recoverable broker operation errors are emitted normally in + /// the correlated response. + pub fn execute_request( + &self, + request: BrokerRequest, + send_response: impl FnOnce(&BrokerResponse) -> core::result::Result<(), ChannelError>, + ) -> Result<(), ChannelError> { + let BrokerRequest { + request_id, + operation, + } = request; + let buffer_descriptor = match &operation { + BrokerOperation::Pipe(PipeRequest::Read(request)) => Some(request.buffer), + BrokerOperation::Pipe(PipeRequest::Write(request)) => Some(request.buffer), + BrokerOperation::CloseObject(_) + | BrokerOperation::CheckReadiness(_) + | BrokerOperation::Event(_) + | BrokerOperation::Pipe(PipeRequest::Create(_)) => None, + }; + + { + let mut state = self.state.lock(); + if state.failed { + return Err(BrokerHostError::Broker(ErrorCode::Internal)); + } + if let Some(descriptor) = buffer_descriptor + && let Err(error) = state.shared_buffer_usage.begin( + request_id, + descriptor, + self.shared_buffers.layout(), + ) + { + state.failed = true; + return Err(BrokerHostError::Broker(error)); + } + } + + let result = match complete_request(handle_request( + &self.session, + operation, + self.shared_buffers, + )) { + Ok(result) => result, + Err(error) => { + self.state.lock().failed = true; + return Err(BrokerHostError::Broker(error)); + } + }; + if let Some(descriptor) = buffer_descriptor { + self.state + .lock() + .shared_buffer_usage + .end(request_id, descriptor.slot_index); + } + if let Err(error) = send_response(&BrokerResponse { request_id, result }) { + self.state.lock().failed = true; + return Err(BrokerHostError::Channel(error)); + } + Ok(()) + } +} + +/// Authenticates and negotiates one broker control connection. /// -/// `shared_buffers` belongs to this association. Payload descriptors are -/// validated against trusted per-slot claim state. `send_shared_memory` runs -/// after version negotiation and before active requests begin. -pub fn serve_connection( +/// `send_shared_memory` runs after version negotiation and before the active +/// association is returned. +pub fn setup_connection<'a, ControlChannel, Memory, ChannelError>( core: &BrokerCore, control_channel: &mut ControlChannel, - _notification_channel: &mut NotificationChannel, - shared_buffers: &SharedBufferPool, + shared_buffers: &'a SharedBufferPool, send_shared_memory: impl FnOnce(&mut ControlChannel) -> core::result::Result<(), ChannelError>, -) -> Result +) -> Result, ChannelError> where ControlChannel: HostControlChannel, - NotificationChannel: HostNotificationChannel, Memory: SharedMemory, { if shared_buffers.layout() != SHARED_BUFFER_LAYOUT { @@ -88,9 +165,11 @@ where ErrorCode::ProtocolState, )) .map_err(BrokerHostError::Channel)?; - return Ok(ConnectionTermination::ProtocolViolation); + return Ok(Err(ConnectionTermination::ProtocolViolation)); + } + HostReceive::PeerClosed => { + return Ok(Err(ConnectionTermination::PeerClosed)); } - HostReceive::PeerClosed => return Ok(ConnectionTermination::PeerClosed), }; let negotiated = request.protocol_version == BROKER_PROTOCOL_VERSION; @@ -108,11 +187,47 @@ where .map_err(BrokerHostError::Channel)?; if negotiated { send_shared_memory(control_channel).map_err(BrokerHostError::Channel)?; - break; + return Ok(Ok(BrokerHostAssociation { + session, + shared_buffers, + state: SpinMutex::new(AssociationState { + failed: false, + shared_buffer_usage: SharedBufferUsage::new(), + }), + })); } } +} - let mut shared_buffer_usage = SharedBufferUsage::new(); +/// Authenticates, negotiates, and serves one broker association over paired +/// control and notification channels. +/// +/// The deployment must bind both channels to the same authenticated peer +/// association. Active requests and responses remain on the control channel; +/// broker-initiated readiness wakeups are sent on the notification channel. +/// Event mutations caused by control requests return readiness in their control +/// response and do not also emit a duplicate notification. +/// +/// `shared_buffers` belongs to this association. Payload descriptors are +/// validated against trusted per-slot claim state. `send_shared_memory` runs +/// after version negotiation and before active requests begin. +pub fn serve_connection( + core: &BrokerCore, + control_channel: &mut ControlChannel, + _notification_channel: &mut NotificationChannel, + shared_buffers: &SharedBufferPool, + send_shared_memory: impl FnOnce(&mut ControlChannel) -> core::result::Result<(), ChannelError>, +) -> Result +where + ControlChannel: HostControlChannel, + NotificationChannel: HostNotificationChannel, + Memory: SharedMemory, +{ + let association = + match setup_connection(core, control_channel, shared_buffers, send_shared_memory)? { + Ok(association) => association, + Err(termination) => return Ok(termination), + }; loop { let request = match control_channel .recv_request() @@ -124,32 +239,7 @@ where } HostReceive::PeerClosed => break, }; - - let BrokerRequest { - request_id, - operation, - } = request; - let buffer_descriptor = match &operation { - BrokerOperation::Pipe(PipeRequest::Read(request)) => Some(request.buffer), - BrokerOperation::Pipe(PipeRequest::Write(request)) => Some(request.buffer), - BrokerOperation::CloseObject(_) - | BrokerOperation::CheckReadiness(_) - | BrokerOperation::Event(_) - | BrokerOperation::Pipe(PipeRequest::Create(_)) => None, - }; - if let Some(descriptor) = buffer_descriptor { - shared_buffer_usage - .begin(request_id, descriptor, shared_buffers.layout()) - .map_err(BrokerHostError::Broker)?; - } - let result = complete_request(handle_request(&session, operation, shared_buffers)) - .map_err(BrokerHostError::Broker)?; - control_channel - .send_response(&BrokerResponse { request_id, result }) - .map_err(BrokerHostError::Channel)?; - if let Some(descriptor) = buffer_descriptor { - shared_buffer_usage.end(request_id, descriptor.slot_index); - } + association.execute_request(request, |response| control_channel.send_response(response))?; } Ok(ConnectionTermination::PeerClosed) @@ -361,7 +451,8 @@ mod tests { SharedBufferDescriptor, SharedBufferPool, SharedMemoryError, }; use litebox_broker_protocol::{ObjectHandle, ProtocolVersion, RequestId}; - use std::sync::{Arc, Mutex}; + use std::sync::{Arc, Condvar, Mutex, mpsc}; + use std::time::Duration; #[test] fn host_request_handling_uses_one_broker_core() { @@ -384,6 +475,9 @@ mod tests { active_request_closes_object_reference(&broker); association_shared_buffer_descriptors_stage_pipe_data(&broker); shared_buffer_usage_rejects_invalid_descriptors(); + association_executes_distinct_slots_concurrently(&broker); + association_allows_slot_reuse_during_response_emission(&broker); + association_allows_out_of_order_responses(&broker); } fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { @@ -559,9 +653,11 @@ mod tests { std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, }))]), - std::vec::Vec::new(), + std::vec::Vec::from([Ok(HostReceive::Message(BrokerOperation::Event( + EventRequest::Create(CreateEventRequest { initial_count: 0 }), + )))]), ); - channel.send_error = true; + channel.response_send_error = true; let mut notifications = FakeHostNotificationChannel::default(); match serve_connection( @@ -574,7 +670,8 @@ mod tests { Err(BrokerHostError::Channel(())) => {} result => panic!("unexpected serve result: {result:?}"), } - assert!(channel.handshake_responses.is_empty()); + assert_eq!(channel.handshake_responses.len(), 1); + assert!(channel.results.is_empty()); } fn serve_connection_returns_event_readiness_in_control_responses(broker: &BrokerCore) { @@ -893,6 +990,181 @@ mod tests { ); } + fn association_executes_distinct_slots_concurrently(broker: &BrokerCore) { + let release = Arc::new((Mutex::new(false), Condvar::new())); + let (entered_sender, entered_receiver) = mpsc::sync_channel(2); + let memory = BlockingReadSharedMemory { + memory: TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE), + entered_sender, + release: Arc::clone(&release), + }; + let shared_buffers = SharedBufferPool::new(memory, SHARED_BUFFER_LAYOUT).unwrap(); + let association = test_association(broker, &shared_buffers); + let (_, first_write_handle) = + litebox_broker_core::pipe::create(&association.session, 64, 16).unwrap(); + let (_, second_write_handle) = + litebox_broker_core::pipe::create(&association.session, 64, 16).unwrap(); + + std::thread::scope(|scope| { + let first_association = &association; + let first = scope.spawn(move || { + first_association + .execute_request(write_request(1, 0, first_write_handle), |_| Ok::<_, ()>(())) + }); + let second_association = &association; + let second = scope.spawn(move || { + second_association.execute_request(write_request(2, 1, second_write_handle), |_| { + Ok::<_, ()>(()) + }) + }); + + let entered = [ + entered_receiver.recv_timeout(Duration::from_secs(1)), + entered_receiver.recv_timeout(Duration::from_secs(1)), + ]; + let (released, available) = &*release; + *released.lock().unwrap() = true; + available.notify_all(); + first.join().unwrap().unwrap(); + second.join().unwrap().unwrap(); + let [first_offset, second_offset] = entered.map(|result| result.unwrap()); + assert_ne!(first_offset, second_offset); + }); + } + + fn association_allows_slot_reuse_during_response_emission(broker: &BrokerCore) { + let shared_buffers = test_shared_buffers(); + let association = test_association(broker, &shared_buffers); + let release = Arc::new((Mutex::new(false), Condvar::new())); + let (started_sender, started_receiver) = mpsc::sync_channel(1); + + std::thread::scope(|scope| { + let worker_release = Arc::clone(&release); + let first_association = &association; + let first = scope.spawn(move || { + first_association.execute_request(read_request(1, 0), |_| { + started_sender.send(()).unwrap(); + wait_for_release(&worker_release); + Ok::<_, ()>(()) + }) + }); + started_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + + association + .execute_request(read_request(2, 0), |_| Ok::<_, ()>(())) + .unwrap(); + let (released, available) = &*release; + *released.lock().unwrap() = true; + available.notify_all(); + first.join().unwrap().unwrap(); + }); + } + + fn association_allows_out_of_order_responses(broker: &BrokerCore) { + let shared_buffers = test_shared_buffers(); + let association = test_association(broker, &shared_buffers); + let release = Arc::new((Mutex::new(false), Condvar::new())); + let (first_started_sender, first_started_receiver) = mpsc::sync_channel(1); + let (response_sender, response_receiver) = mpsc::sync_channel(2); + + std::thread::scope(|scope| { + let first_association = &association; + let first_release = Arc::clone(&release); + let first_response_sender = response_sender.clone(); + let first = scope.spawn(move || { + first_association.execute_request(event_create_request(1), |response| { + first_started_sender.send(()).unwrap(); + wait_for_release(&first_release); + first_response_sender.send(response.request_id).unwrap(); + Ok::<_, ()>(()) + }) + }); + first_started_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + + let second_association = &association; + let second = scope.spawn(move || { + second_association.execute_request(event_create_request(2), |response| { + response_sender.send(response.request_id).unwrap(); + Ok::<_, ()>(()) + }) + }); + assert_eq!( + response_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + RequestId(2) + ); + let (released, available) = &*release; + *released.lock().unwrap() = true; + available.notify_all(); + assert_eq!( + response_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + RequestId(1) + ); + first.join().unwrap().unwrap(); + second.join().unwrap().unwrap(); + }); + } + + fn test_association<'a, Memory: SharedMemory>( + broker: &BrokerCore, + shared_buffers: &'a SharedBufferPool, + ) -> BrokerHostAssociation<'a, Memory> { + BrokerHostAssociation { + session: broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(), + shared_buffers, + state: SpinMutex::new(AssociationState { + failed: false, + shared_buffer_usage: SharedBufferUsage::new(), + }), + } + } + + fn read_request(request_id: u64, slot_index: u32) -> BrokerRequest { + BrokerRequest { + request_id: RequestId(request_id), + operation: BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { + handle: ObjectHandle(u64::MAX), + buffer: descriptor(slot_index, 1), + })), + } + } + + fn write_request(request_id: u64, slot_index: u32, handle: ObjectHandle) -> BrokerRequest { + BrokerRequest { + request_id: RequestId(request_id), + operation: BrokerOperation::Pipe(PipeRequest::Write(WritePipeRequest { + handle, + buffer: descriptor(slot_index, 1), + })), + } + } + + fn event_create_request(request_id: u64) -> BrokerRequest { + BrokerRequest { + request_id: RequestId(request_id), + operation: BrokerOperation::Event(EventRequest::Create(CreateEventRequest { + initial_count: 0, + })), + } + } + + fn wait_for_release(release: &(Mutex, Condvar)) { + let (released, available) = release; + let mut released = released.lock().unwrap(); + while !*released { + released = available.wait(released).unwrap(); + } + } + const fn descriptor(slot: u32, length: u32) -> SharedBufferDescriptor { SharedBufferDescriptor { slot_index: SharedBufferSlotIndex(slot), @@ -931,7 +1203,7 @@ mod tests { request_id_step: u64, enqueue_readiness_requests_after_create: bool, enqueue_write_request_after_pipe_create: bool, - send_error: bool, + response_send_error: bool, } impl FakeHostControlChannel { @@ -951,7 +1223,7 @@ mod tests { request_id_step: 1, enqueue_readiness_requests_after_create: false, enqueue_write_request_after_pipe_create: false, - send_error: false, + response_send_error: false, } } } @@ -977,9 +1249,6 @@ mod tests { &mut self, response: &BrokerHandshakeResponse, ) -> core::result::Result<(), Self::Error> { - if self.send_error { - return Err(()); - } self.handshake_responses.push(response.clone()); Ok(()) } @@ -1010,7 +1279,7 @@ mod tests { &mut self, response: &BrokerResponse, ) -> core::result::Result<(), Self::Error> { - if self.send_error { + if self.response_send_error { return Err(()); } let result = &response.result; @@ -1098,6 +1367,36 @@ mod tests { } } + struct BlockingReadSharedMemory { + memory: TestSharedMemory, + entered_sender: mpsc::SyncSender, + release: Arc<(Mutex, Condvar)>, + } + + impl SharedMemory for BlockingReadSharedMemory { + fn len(&self) -> usize { + self.memory.len() + } + + fn read( + &self, + offset: usize, + destination: &mut [u8], + ) -> core::result::Result<(), SharedMemoryError> { + self.entered_sender.send(offset).unwrap(); + wait_for_release(&self.release); + self.memory.read(offset, destination) + } + + fn write( + &self, + offset: usize, + source: &[u8], + ) -> core::result::Result<(), SharedMemoryError> { + self.memory.write(offset, source) + } + } + struct FailingSharedMemory; impl SharedMemory for FailingSharedMemory { diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 24e93627d2..646ec4ac18 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -243,6 +243,23 @@ pub struct UnixStreamHostControlChannel { stream: UnixStream, peer_credential: PeerCredential, setup_deadline: Option, + negotiated: bool, +} + +/// Request-reading half of an active host control channel. +pub struct UnixStreamHostRequestSource { + stream: UnixStream, +} + +/// Shared response-writing half of an active host control channel. +#[derive(Clone)] +pub struct UnixStreamHostResponseSink { + stream: Arc>, +} + +/// Handle that interrupts all active host control-channel I/O. +pub struct UnixStreamHostControlShutdown { + stream: UnixStream, } /// Local-side Unix-domain-socket notification channel for the hosted userland POC. @@ -267,6 +284,7 @@ impl UnixStreamHostControlChannel { stream, peer_credential: PeerCredential::Unauthenticated, setup_deadline: None, + negotiated: false, } } @@ -277,6 +295,7 @@ impl UnixStreamHostControlChannel { stream, peer_credential: PeerCredential::HostGuaranteed, setup_deadline: Some(setup_deadline), + negotiated: false, } } @@ -288,6 +307,43 @@ impl UnixStreamHostControlChannel { ) -> IoResult<()> { crate::shared_memory::send_memfd(&mut self.stream, shared_memory, deadline) } + + /// Consumes a negotiated setup channel into independently usable active + /// request, response, and shutdown handles. + pub fn into_active( + self, + ) -> IoResult<( + UnixStreamHostRequestSource, + UnixStreamHostResponseSink, + UnixStreamHostControlShutdown, + )> { + if !self.negotiated { + return Err(invalid_data( + "broker host control channel activated before negotiation completed", + )); + } + let response_stream = self.stream.try_clone()?; + let shutdown_stream = self.stream.try_clone()?; + Ok(( + UnixStreamHostRequestSource { + stream: self.stream, + }, + UnixStreamHostResponseSink { + stream: Arc::new(Mutex::new(response_stream)), + }, + UnixStreamHostControlShutdown { + stream: shutdown_stream, + }, + )) + } +} + +impl UnixStreamHostControlShutdown { + /// Shuts down the active control socket without waiting for the response + /// writer mutex. + pub fn shutdown(&self) -> IoResult<()> { + shutdown(&self.stream) + } } impl UnixStreamLocalNotificationChannel { @@ -411,7 +467,8 @@ impl HostControlChannel for UnixStreamHostControlChannel { &encode_handshake_response(response.clone()), self.setup_deadline, )?; - if matches!(response, BrokerHandshakeResponse::Negotiated { .. }) { + self.negotiated = matches!(response, BrokerHandshakeResponse::Negotiated { .. }); + if self.negotiated { self.setup_deadline = None; } Ok(()) @@ -433,6 +490,31 @@ impl HostControlChannel for UnixStreamHostControlChannel { } } +impl UnixStreamHostRequestSource { + /// Receives one active broker request. + pub fn recv_request(&mut self) -> IoResult> { + let Some(frame) = read_frame_with_deadline(&mut self.stream, None)? else { + return Ok(HostReceive::PeerClosed); + }; + match decode_request(&frame) { + Ok(request) => Ok(HostReceive::Message(request)), + Err(WireError::WrongMessagePhase) => Ok(HostReceive::ProtocolViolation), + Err(error) => Err(wire_error(error)), + } + } +} + +impl UnixStreamHostResponseSink { + /// Serializes and sends one complete active broker response. + pub fn send_response(&self, response: &BrokerResponse) -> IoResult<()> { + let mut stream = self + .stream + .lock() + .map_err(|_| Error::other("broker response writer mutex poisoned"))?; + write_frame_with_deadline(&mut stream, &encode_response(response.clone()), None) + } +} + impl LocalNotificationChannel for UnixStreamLocalNotificationChannel { type Error = Error; @@ -1458,6 +1540,62 @@ mod tests { ); } + #[test] + fn host_control_requires_negotiation_before_active_split() { + let (_peer_stream, host_stream) = UnixStream::pair().unwrap(); + let channel = UnixStreamHostControlChannel::from_accepted(host_stream); + + assert!(channel.into_active().is_err()); + } + + #[test] + fn concurrent_host_response_sinks_write_complete_frames() { + let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); + channel + .send_handshake_response(&BrokerHandshakeResponse::Negotiated { + broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }) + .unwrap(); + let handshake = read_frame_with_deadline(&mut peer_stream, None) + .unwrap() + .unwrap(); + assert!(matches!( + decode_handshake_response(&handshake).unwrap(), + BrokerHandshakeResponse::Negotiated { .. } + )); + let (_request_source, response_sink, _shutdown) = channel.into_active().unwrap(); + let first_sink = response_sink.clone(); + let first = std::thread::spawn(move || { + first_sink + .send_response(&BrokerResponse { + request_id: RequestId(1), + result: BrokerResult::ObjectClosed, + }) + .unwrap(); + }); + let second = std::thread::spawn(move || { + response_sink + .send_response(&BrokerResponse { + request_id: RequestId(2), + result: BrokerResult::ObjectClosed, + }) + .unwrap(); + }); + + let mut response_ids = [RequestId(0); 2]; + for response_id in &mut response_ids { + let frame = read_frame_with_deadline(&mut peer_stream, None) + .unwrap() + .unwrap(); + *response_id = decode_response(&frame).unwrap().request_id; + } + response_ids.sort(); + assert_eq!(response_ids, [RequestId(1), RequestId(2)]); + first.join().unwrap(); + second.join().unwrap(); + } + #[test] fn local_control_cancellation_unblocks_pending_call() { let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index f0252c4d42..4569b63077 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -1,28 +1,37 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use std::cell::Cell; use std::error::Error; use std::ffi::OsString; use std::io::{Error as IoError, ErrorKind, Result as IoResult}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; use std::process::{Child, Command}; +use std::sync::mpsc::{Receiver, SyncSender, sync_channel}; +use std::sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, +}; use std::time::{Duration, Instant}; use clap::Parser; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; -use litebox_broker_host::{ConnectionTermination, serve_connection}; +use litebox_broker_host::{BrokerHostAssociation, ConnectionTermination, setup_connection}; +use litebox_broker_protocol::channel::HostReceive; +use litebox_broker_protocol::message::BrokerRequest; use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, SharedMemory, }; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ - UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, validate_peer_process, + UnixStreamHostControlChannel, UnixStreamHostControlShutdown, UnixStreamHostNotificationChannel, + UnixStreamHostRequestSource, UnixStreamHostResponseSink, validate_peer_process, }; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); const ACCEPT_RETRY_DELAY: Duration = Duration::from_millis(10); +const REQUEST_QUEUE_CAPACITY: usize = 64; +const WORKER_COUNT: usize = 8; #[derive(Parser, Debug)] struct CliArgs { @@ -104,35 +113,189 @@ fn serve_runner( let shared_buffers = SharedBufferPool::new(shared_memory, SHARED_BUFFER_LAYOUT)?; let mut control_channel = UnixStreamHostControlChannel::from_host_guaranteed(control_stream, setup_deadline); - let mut notification_channel = + let _notification_channel = UnixStreamHostNotificationChannel::from_accepted(notification_stream); - let setup_completed = Cell::new(false); - let termination = serve_connection( - broker, - &mut control_channel, - &mut notification_channel, - &shared_buffers, - |channel| { + let association = + match setup_connection(broker, &mut control_channel, &shared_buffers, |channel| { channel.send_memfd(shared_buffers.memory(), Some(setup_deadline))?; - setup_completed.set(true); Ok(()) - }, - )?; - if termination != ConnectionTermination::PeerClosed { - return Err(IoError::new( - ErrorKind::InvalidData, - "runner violated the broker protocol", - ) - .into()); + })? { + Ok(association) => association, + Err(ConnectionTermination::PeerClosed) => { + return Err(IoError::new( + ErrorKind::UnexpectedEof, + "runner closed before completing broker setup", + ) + .into()); + } + Err(ConnectionTermination::ProtocolViolation) => { + return Err(IoError::new( + ErrorKind::InvalidData, + "runner violated the broker protocol during setup", + ) + .into()); + } + Err(_) => { + return Err(IoError::new( + ErrorKind::InvalidData, + "runner ended broker setup unexpectedly", + ) + .into()); + } + }; + let (request_source, response_sink, shutdown) = control_channel.into_active()?; + dispatch_requests(association, request_source, response_sink, shutdown)?; + Ok(()) +} + +fn dispatch_requests( + association: BrokerHostAssociation<'_, Memory>, + mut request_source: UnixStreamHostRequestSource, + response_sink: UnixStreamHostResponseSink, + shutdown: UnixStreamHostControlShutdown, +) -> IoResult<()> { + let association = Arc::new(association); + let failure = Arc::new(HostAssociationFailure::new(shutdown)); + let (request_sender, request_receiver) = sync_channel(REQUEST_QUEUE_CAPACITY); + let request_receiver = Arc::new(Mutex::new(request_receiver)); + + std::thread::scope(|scope| { + let mut workers = Vec::with_capacity(WORKER_COUNT); + for worker_id in 0..WORKER_COUNT { + let association = Arc::clone(&association); + let request_receiver = Arc::clone(&request_receiver); + let response_sink = response_sink.clone(); + let worker_failure = Arc::clone(&failure); + match std::thread::Builder::new() + .name(format!("litebox-broker-worker-{worker_id}")) + .spawn_scoped(scope, move || { + run_worker( + &association, + &request_receiver, + &response_sink, + &worker_failure, + ); + }) { + Ok(worker) => workers.push(worker), + Err(error) => { + failure.report(error); + break; + } + } + } + + read_requests(&mut request_source, request_sender, &failure); + for worker in workers { + if worker.join().is_err() { + failure.report(IoError::other("broker request worker panicked")); + } + } + }); + + match failure.take_error() { + Some(error) => Err(error), + None => Ok(()), } - if !setup_completed.get() { - return Err(IoError::new( - ErrorKind::UnexpectedEof, - "runner closed before completing broker setup", - ) - .into()); +} + +fn read_requests( + request_source: &mut UnixStreamHostRequestSource, + request_sender: SyncSender, + failure: &HostAssociationFailure, +) { + loop { + if failure.failed() { + break; + } + match request_source.recv_request() { + Ok(HostReceive::Message(request)) => { + if request_sender.send(request).is_err() { + failure.report(IoError::new( + ErrorKind::BrokenPipe, + "broker request workers stopped", + )); + break; + } + } + Ok(HostReceive::ProtocolViolation) => { + failure.report(IoError::new( + ErrorKind::InvalidData, + "runner sent a request for the wrong protocol phase", + )); + break; + } + Ok(HostReceive::PeerClosed) => break, + Err(error) => { + failure.report(error); + break; + } + } + } +} + +fn run_worker( + association: &BrokerHostAssociation<'_, Memory>, + request_receiver: &Mutex>, + response_sink: &UnixStreamHostResponseSink, + failure: &HostAssociationFailure, +) { + loop { + let request = request_receiver + .lock() + .expect("broker request receiver mutex poisoned") + .recv(); + let Ok(request) = request else { + break; + }; + if failure.failed() { + continue; + } + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + association.execute_request(request, |response| response_sink.send_response(response)) + })) { + Ok(Ok(())) => {} + Ok(Err(error)) => failure.report(IoError::other(error)), + Err(_) => failure.report(IoError::other("broker request worker panicked")), + } + } +} + +struct HostAssociationFailure { + failed: AtomicBool, + error: Mutex>, + shutdown: UnixStreamHostControlShutdown, +} + +impl HostAssociationFailure { + const fn new(shutdown: UnixStreamHostControlShutdown) -> Self { + Self { + failed: AtomicBool::new(false), + error: Mutex::new(None), + shutdown, + } + } + + fn failed(&self) -> bool { + self.failed.load(Ordering::Acquire) + } + + fn report(&self, error: IoError) { + if self.failed.swap(true, Ordering::AcqRel) { + return; + } + *self + .error + .lock() + .expect("broker association failure mutex poisoned") = Some(error); + let _ = self.shutdown.shutdown(); + } + + fn take_error(&self) -> Option { + self.error + .lock() + .expect("broker association failure mutex poisoned") + .take() } - Ok(()) } fn accept_runner_stream( @@ -168,3 +331,42 @@ fn accept_runner_stream( std::thread::sleep(remaining.min(ACCEPT_RETRY_DELAY)); } } + +#[cfg(test)] +mod tests { + use super::*; + use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; + use litebox_broker_protocol::channel::HostControlChannel; + use litebox_broker_protocol::message::BrokerHandshakeResponse; + + #[test] + fn first_failure_is_preserved_and_unblocks_request_reading() { + let (peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut control_channel = UnixStreamHostControlChannel::from_accepted(host_stream); + control_channel + .send_handshake_response(&BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }) + .unwrap(); + let (mut request_source, _response_sink, shutdown) = control_channel.into_active().unwrap(); + let failure = HostAssociationFailure::new(shutdown); + let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); + let reader = std::thread::spawn(move || { + result_sender.send(request_source.recv_request()).unwrap(); + }); + + failure.report(IoError::new(ErrorKind::TimedOut, "first failure")); + failure.report(IoError::other("second failure")); + let receive_result = result_receiver.recv_timeout(Duration::from_secs(1)); + drop(peer_stream); + reader.join().unwrap(); + + assert!(matches!( + receive_result.unwrap(), + Ok(HostReceive::PeerClosed) | Err(_) + )); + let error = failure.take_error().unwrap(); + assert_eq!(error.kind(), ErrorKind::TimedOut); + assert_eq!(error.to_string(), "first failure"); + } +} diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 1df0839abf..17e9c946ae 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -84,15 +84,37 @@ fn run_fake_runner(args: &[OsString]) { let control_channel = connect_control_with_retry(Path::new(control_socket_path)).unwrap(); let _notification_channel = connect_notification_with_retry(Path::new(notification_socket_path)).unwrap(); - let local = BrokerLocal::negotiate(control_channel, |channel| { - let shared_memory = channel.receive_memfd( - SHARED_BUFFER_POOL_SIZE, - Some(Instant::now() + Duration::from_secs(5)), - )?; - let _cancellation = channel.activate(|| {})?; - Ok(Arc::new(shared_memory)) - }) - .unwrap(); + let local = Arc::new( + BrokerLocal::negotiate(control_channel, |channel| { + let shared_memory = channel.receive_memfd( + SHARED_BUFFER_POOL_SIZE, + Some(Instant::now() + Duration::from_secs(5)), + )?; + let _cancellation = channel.activate(|| {})?; + Ok(Arc::new(shared_memory)) + }) + .unwrap(), + ); + + let start = Arc::new(std::sync::Barrier::new(17)); + let callers = (0..16) + .map(|initial_count| { + let local = Arc::clone(&local); + let start = Arc::clone(&start); + std::thread::spawn(move || { + start.wait(); + local.create_event_with_count(initial_count).unwrap() + }) + }) + .collect::>(); + start.wait(); + let mut concurrent_handles = callers + .into_iter() + .map(|caller| caller.join().unwrap()) + .collect::>(); + concurrent_handles.sort(); + concurrent_handles.dedup(); + assert_eq!(concurrent_handles.len(), 16); let handle = local.create_event_with_count(0).unwrap(); assert_eq!( From 40e2178a170daaac708ea1f66637d68ad3d2c25c Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 22 Jul 2026 20:31:15 -0700 Subject: [PATCH 120/319] Implement Windows process token (#1071) This PR adds typed current-process token handles for `NtOpenProcessToken`, `NtOpenProcessTokenEx` and `NtQueryInformationToken`, including close and duplicate support. Cross-process tokens and remaining token information classes are deferred. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 136 +++++- litebox_shim_windows/src/loader/pe.rs | 11 +- litebox_shim_windows/src/nt_types.rs | 7 + litebox_shim_windows/src/syscalls/mod.rs | 37 ++ litebox_shim_windows/src/syscalls/token.rs | 502 +++++++++++++++++++++ 5 files changed, 675 insertions(+), 18 deletions(-) create mode 100644 litebox_shim_windows/src/syscalls/token.rs diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index f7de8819d0..4a644dabf8 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -43,6 +43,7 @@ use crate::syscalls::section::{ }; use crate::syscalls::symlink::{SymbolicLinkHandleObject, SymbolicLinkSubsystem}; use crate::syscalls::timer::{TimerCreateParameters, TimerHandleObject, TimerSubsystem}; +use crate::syscalls::token::{TokenHandleObject, TokenObject, TokenSubsystem}; use crate::syscalls::wait_completion_packet::{ WaitCompletionPacketAssociateParameters, WaitCompletionPacketHandleObject, WaitCompletionPacketSubsystem, @@ -107,11 +108,43 @@ bitflags::bitflags! { const PROTECT_FROM_CLOSE = 0x0000_0001; const INHERIT = 0x0000_0002; const AUDIT_OBJECT_CLOSE = 0x0000_0004; + const HANDLE_BEHAVIOR_ATTRIBUTES = Self::PROTECT_FROM_CLOSE.bits() + | Self::INHERIT.bits() + | Self::AUDIT_OBJECT_CLOSE.bits(); const _ = !0; } } +impl HandleAttributes { + fn from_token_open_attributes(attributes: u32) -> Option { + const OBJ_EXCLUSIVE: u32 = 0x20; + const OBJ_OPENLINK: u32 = 0x100; + + // NtOpenProcessTokenEx accepts and ignores unrelated object attributes, but native + // Windows rejects the two attributes that cannot apply to opening an existing token. + if attributes & (OBJ_EXCLUSIVE | OBJ_OPENLINK) != 0 { + return None; + } + Some(Self::from_bits_retain( + attributes & Self::HANDLE_BEHAVIOR_ATTRIBUTES.bits(), + )) + } + + fn from_duplicate_attributes(attributes: u32) -> Option { + const OBJ_EXCLUSIVE: u32 = 0x20; + + // Native NtDuplicateObject accepts and ignores unrelated object attributes, but an + // exclusive duplicate is invalid because the object already has an open handle. + if attributes & OBJ_EXCLUSIVE != 0 { + return None; + } + Some(Self::from_bits_retain( + attributes & Self::HANDLE_BEHAVIOR_ATTRIBUTES.bits(), + )) + } +} + #[derive(Clone, Copy, Default)] struct WindowsHandleMetadata { granted_access: u32, @@ -536,6 +569,7 @@ pub struct Process { ntdll_mapping: Option, peb_address: usize, handles: WindowsHandleStore, + token: Arc, condrv_console: syscalls::condrv::CondrvConsole, object_manager: WindowsObjectManager, section_views: WindowsSectionViews, @@ -583,6 +617,7 @@ impl Process { ntdll_mapping: None, peb_address: 0, handles: WindowsHandleStore::::new(litebox::fd::RawDescriptorStorage::new()), + token: Arc::new(TokenObject::primary()), // TODO(condrv-shared-console): move console ownership to shared state or a broker when // LiteBox supports AttachConsole/IOCTL_CONDRV_BIND_PID across guest processes. condrv_console: syscalls::condrv::CondrvConsole::new(), @@ -740,6 +775,24 @@ impl Task { granted_access: u32, cleanup_entry: impl FnOnce(Subsystem::Entry), ) -> Result + where + Subsystem: WindowsHandleSubsystem, + { + self.insert_typed_handle_with_attributes::( + entry, + granted_access, + HandleAttributes::empty(), + cleanup_entry, + ) + } + + fn insert_typed_handle_with_attributes( + &self, + entry: Subsystem::Entry, + granted_access: u32, + attributes: HandleAttributes, + cleanup_entry: impl FnOnce(Subsystem::Entry), + ) -> Result where Subsystem: WindowsHandleSubsystem, { @@ -750,7 +803,7 @@ impl Task { &typed, WindowsHandleMetadata { granted_access, - attributes: HandleAttributes::empty(), + attributes, }, ); debug_assert!(old.is_none()); @@ -1614,6 +1667,45 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtOpenProcessToken { + process_handle, + desired_access, + token_handle, + } => { + let status = + self.sys_nt_open_process_token(process_handle, desired_access, token_handle); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtOpenProcessTokenEx { + process_handle, + desired_access, + handle_attributes, + token_handle, + } => { + let status = self.sys_nt_open_process_token_ex( + process_handle, + desired_access, + handle_attributes, + token_handle, + ); + (status, ContinueOperation::Resume) + } + SyscallRequest::NtQueryInformationToken { + token_handle, + token_information_class, + token_information, + token_information_length, + return_length, + } => { + let status = self.sys_nt_query_information_token( + token_handle, + token_information_class, + token_information, + token_information_length, + return_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, source, @@ -1892,6 +1984,7 @@ impl Task { if let Some(target_handle) = target_handle && target_handle.write_at_offset(0, duplicate).is_none() { + let _ = self.remove_handle(duplicate, CloseRawHandleVisitor { task: self }, false); return NtStatus::ACCESS_VIOLATION; } NtStatus::SUCCESS @@ -1928,6 +2021,7 @@ impl Task { try_duplicate!(WaitCompletionPacketSubsystem); try_duplicate!(WorkerFactorySubsystem); try_duplicate!(SectionSubsystem); + try_duplicate!(TokenSubsystem); Err(NtStatus::INVALID_HANDLE) } @@ -1968,7 +2062,11 @@ impl Task { let duplicate_attributes = if options.contains(DuplicateOptions::SAME_ATTRIBUTES) { source_metadata.attributes } else { - HandleAttributes::from_bits_retain(handle_attributes) + let Some(attributes) = HandleAttributes::from_duplicate_attributes(handle_attributes) + else { + return Some(Err(NtStatus::INVALID_PARAMETER)); + }; + attributes }; let duplicate = { @@ -1998,12 +2096,23 @@ impl Task { &self, handle: syscalls::Handle, visitor: impl RawHandleVisitor, + ) -> NtStatus { + self.remove_handle(handle, visitor, true) + } + + fn remove_handle( + &self, + handle: syscalls::Handle, + visitor: impl RawHandleVisitor, + enforce_protect_from_close: bool, ) -> NtStatus { macro_rules! try_close { ($subsystem:ty, $visit:ident) => { - if let Some(status) = - self.try_close_handle::<$subsystem>(handle, |entry| visitor.$visit(entry)) - { + if let Some(status) = self.try_remove_handle::<$subsystem>( + handle, + enforce_protect_from_close, + |entry| visitor.$visit(entry), + ) { return status; } }; @@ -2023,13 +2132,15 @@ impl Task { ); try_close!(WorkerFactorySubsystem, worker_factory); try_close!(SectionSubsystem, section); + try_close!(TokenSubsystem, token); NtStatus::INVALID_HANDLE } - fn try_close_handle( + fn try_remove_handle( &self, handle: syscalls::Handle, + enforce_protect_from_close: bool, cleanup_entry: impl FnOnce(Subsystem::Entry), ) -> Option where @@ -2044,9 +2155,10 @@ impl Task { Ok(metadata) => metadata, Err(status) => return Some(status), }; - if metadata - .attributes - .contains(HandleAttributes::PROTECT_FROM_CLOSE) + if enforce_protect_from_close + && metadata + .attributes + .contains(HandleAttributes::PROTECT_FROM_CLOSE) { return Some(NtStatus::HANDLE_NOT_CLOSABLE); } @@ -2097,6 +2209,8 @@ trait RawHandleVisitor { fn worker_factory(&self, worker_factory: WorkerFactoryHandleObject); fn section(&self, section: SectionHandleObject); + + fn token(&self, token: TokenHandleObject); } struct CloseRawHandleVisitor<'task, Platform: ShimPlatform, FS: ShimFS> { @@ -2152,6 +2266,10 @@ impl RawHandleVisitor fn section(&self, section: SectionHandleObject) { Task::::close_section(section); } + + fn token(&self, token: TokenHandleObject) { + Task::::close_token(token); + } } /// The shim entrypoint object passed to the platform. diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 6fbe76a340..745c49888a 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -27,8 +27,8 @@ use thiserror::Error; use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout}; use crate::nt_types::{ - ClientId, PebBitField, ProcessEnvironmentBlock, RtlUserProcFlags, RtlUserProcessParameters, - ThreadEnvironmentBlock, UnicodeString, X64Context, + ClientId, Luid, PebBitField, ProcessEnvironmentBlock, RtlUserProcFlags, + RtlUserProcessParameters, ThreadEnvironmentBlock, UnicodeString, X64Context, }; use crate::syscalls::mm::{MemoryType, PageProtection}; use crate::syscalls::process::{INITIAL_PROCESS_ID, INITIAL_THREAD_ID}; @@ -755,13 +755,6 @@ struct NlsUserInfo { ul_cache_update_count: u32, } -#[repr(C)] -#[derive(FromBytes, IntoBytes)] -struct Luid { - low_part: u32, - high_part: i32, -} - #[repr(C)] #[derive(FromBytes, IntoBytes)] struct TimeZoneInformation { diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index 5665286f1d..a9edac5c4b 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -19,6 +19,13 @@ const USER_MODE_CODE_SELECTOR: u16 = 0x33; const USER_MODE_STACK_SELECTOR: u16 = 0x2b; const INITIAL_CONTEXT_EFLAGS: u32 = 0x200; +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +pub(crate) struct Luid { + pub(crate) low_part: u32, + pub(crate) high_part: i32, +} + #[repr(C)] #[derive(Clone, Copy, Debug, FromBytes, IntoBytes, Immutable)] pub struct X64Context { diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 72a52e1f45..60c1e3a85e 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -18,6 +18,7 @@ pub(crate) mod symlink; pub(crate) mod sysinfo; pub(crate) mod thread; pub(crate) mod timer; +pub(crate) mod token; pub(crate) mod wait_completion_packet; pub(crate) mod wnf; pub(crate) mod worker_factory; @@ -494,6 +495,24 @@ pub(crate) enum SyscallRequest { handle_attributes: u32, token_handle: Platform::RawMutPointer, }, + NtOpenProcessToken { + process_handle: ProcessHandle, + desired_access: u32, + token_handle: Platform::RawMutPointer, + }, + NtOpenProcessTokenEx { + process_handle: ProcessHandle, + desired_access: u32, + handle_attributes: u32, + token_handle: Platform::RawMutPointer, + }, + NtQueryInformationToken { + token_handle: Handle, + token_information_class: u32, + token_information: Platform::RawMutPointer, + token_information_length: u32, + return_length: Platform::RawMutPointer, + }, NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag: u32, source: Platform::RawConstPointer, @@ -980,6 +999,24 @@ impl SyscallRequest { handle_attributes, token_handle:*, })), + NtSysno::NtOpenProcessToken => Some(sys_req!(NtOpenProcessToken { + process_handle: { ProcessHandle::from_raw }, + desired_access, + token_handle:*, + })), + NtSysno::NtOpenProcessTokenEx => Some(sys_req!(NtOpenProcessTokenEx { + process_handle: { ProcessHandle::from_raw }, + desired_access, + handle_attributes, + token_handle:*, + })), + NtSysno::NtQueryInformationToken => Some(sys_req!(NtQueryInformationToken { + token_handle: { Handle::from_raw }, + token_information_class, + token_information:*, + token_information_length, + return_length:*, + })), NtSysno::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter => Some( sys_req!(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, diff --git a/litebox_shim_windows/src/syscalls/token.rs b/litebox_shim_windows/src/syscalls/token.rs new file mode 100644 index 0000000000..f23f7bee19 --- /dev/null +++ b/litebox_shim_windows/src/syscalls/token.rs @@ -0,0 +1,502 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows NT access-token syscalls. + +use alloc::sync::Arc; +use core::mem::size_of; + +use int_enum::IntEnum; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; +use litebox::utils::TruncateExt as _; +use litebox_common_windows::nt_status::NtStatus; +use zerocopy::{FromBytes, Immutable, IntoBytes}; + +use crate::nt_types::{AccessMask, Luid}; +use crate::syscalls::{Handle, ProcessHandle}; +use crate::{ + HandleAttributes, MutPtr, ShimFS, Task, WindowsHandleSubsystem, probe_guest_output_buffer, + probe_guest_output_preserving_value, +}; + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub(crate) struct TokenAccess: u32 { + const ASSIGN_PRIMARY = 0x0001; + const DUPLICATE = 0x0002; + const IMPERSONATE = 0x0004; + const QUERY = 0x0008; + const QUERY_SOURCE = 0x0010; + const ADJUST_PRIVILEGES = 0x0020; + const ADJUST_GROUPS = 0x0040; + const ADJUST_DEFAULT = 0x0080; + const ADJUST_SESSION_ID = 0x0100; + + const READ = AccessMask::STANDARD_RIGHTS_READ.bits() | Self::QUERY.bits(); + const WRITE = AccessMask::STANDARD_RIGHTS_WRITE.bits() + | Self::ADJUST_PRIVILEGES.bits() + | Self::ADJUST_GROUPS.bits() + | Self::ADJUST_DEFAULT.bits(); + const EXECUTE = AccessMask::STANDARD_RIGHTS_EXECUTE.bits(); + const ALL_ACCESS = AccessMask::DELETE.bits() + | AccessMask::READ_CONTROL.bits() + | AccessMask::WRITE_DAC.bits() + | AccessMask::WRITE_OWNER.bits() + | Self::ASSIGN_PRIMARY.bits() + | Self::DUPLICATE.bits() + | Self::IMPERSONATE.bits() + | Self::QUERY.bits() + | Self::QUERY_SOURCE.bits() + | Self::ADJUST_PRIVILEGES.bits() + | Self::ADJUST_GROUPS.bits() + | Self::ADJUST_DEFAULT.bits() + | Self::ADJUST_SESSION_ID.bits(); + + const _ = !0; + } +} + +impl TokenAccess { + fn from_desired_access(desired_access: u32) -> Self { + let maximum_allowed = desired_access & AccessMask::MAXIMUM_ALLOWED.bits() != 0; + let explicit_access = desired_access & !AccessMask::MAXIMUM_ALLOWED.bits(); + let normalized = AccessMask::expand_generic_access( + explicit_access, + Self::READ.bits(), + Self::WRITE.bits(), + Self::EXECUTE.bits(), + Self::ALL_ACCESS.bits(), + ); + Self::from_bits_retain(if maximum_allowed { + normalized | Self::ALL_ACCESS.bits() + } else { + normalized + }) + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +pub(crate) enum TokenInformationClass { + User = 1, + Groups = 2, + Privileges = 3, + Owner = 4, + PrimaryGroup = 5, + DefaultDacl = 6, + Source = 7, + Type = 8, + ImpersonationLevel = 9, + Statistics = 10, + RestrictedSids = 11, + SessionId = 12, + GroupsAndPrivileges = 13, + SessionReference = 14, + SandBoxInert = 15, + AuditPolicy = 16, + Origin = 17, + ElevationType = 18, + LinkedToken = 19, + Elevation = 20, + HasRestrictions = 21, + AccessInformation = 22, + VirtualizationAllowed = 23, + VirtualizationEnabled = 24, + IntegrityLevel = 25, + UiAccess = 26, + MandatoryPolicy = 27, + LogonSid = 28, + IsAppContainer = 29, + Capabilities = 30, + AppContainerSid = 31, + AppContainerNumber = 32, + UserClaimAttributes = 33, + DeviceClaimAttributes = 34, + RestrictedUserClaimAttributes = 35, + RestrictedDeviceClaimAttributes = 36, + DeviceGroups = 37, + RestrictedDeviceGroups = 38, + SecurityAttributes = 39, + IsRestricted = 40, + ProcessTrustLevel = 41, + PrivateNameSpace = 42, + SingletonAttributes = 43, + BnoIsolation = 44, + ChildProcessFlags = 45, + IsLessPrivilegedAppContainer = 46, + IsSandboxed = 47, + IsAppSilo = 48, + LoggingInformation = 49, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +pub(crate) struct SidAndAttributes { + sid: usize, + attributes: u32, + padding: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +pub(crate) struct TokenUser { + user: SidAndAttributes, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +pub(crate) struct Sid { + revision: u8, + sub_authority_count: u8, + identifier_authority: [u8; 6], + sub_authority: [u32; 1], +} + +#[repr(C, packed(4))] +#[derive(Immutable, IntoBytes)] +struct TokenUserInformation { + user: TokenUser, + sid: Sid, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +pub(crate) struct TokenPrivileges { + privilege_count: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +pub(crate) struct TokenStatistics { + token_id: Luid, + authentication_id: Luid, + expiration_time: i64, + token_type: u32, + impersonation_level: u32, + dynamic_charged: u32, + dynamic_available: u32, + group_count: u32, + privilege_count: u32, + modified_id: Luid, +} + +const TOKEN_TYPE_PRIMARY: u32 = 1; +const SECURITY_ANONYMOUS: u32 = 0; + +// TODO(token-luid-allocation): Allocate these from sandbox-wide state once multiple token objects +// or token mutation are supported. +const PRIMARY_TOKEN_ID: Luid = Luid { + low_part: 1, + high_part: 0, +}; + +const PRIMARY_TOKEN_MODIFIED_ID: Luid = Luid { + low_part: 2, + high_part: 0, +}; + +const SYSTEM_LUID: Luid = Luid { + low_part: 0x3e7, + high_part: 0, +}; + +const LOCAL_SYSTEM_SID: Sid = Sid { + revision: 1, + sub_authority_count: 1, + identifier_authority: [0, 0, 0, 0, 0, 5], + sub_authority: [18], +}; + +pub(crate) struct TokenObject { + user: Sid, + statistics: TokenStatistics, +} + +impl TokenObject { + pub(crate) const fn primary() -> Self { + Self { + user: LOCAL_SYSTEM_SID, + statistics: TokenStatistics { + token_id: PRIMARY_TOKEN_ID, + authentication_id: SYSTEM_LUID, + expiration_time: i64::MAX, + token_type: TOKEN_TYPE_PRIMARY, + impersonation_level: SECURITY_ANONYMOUS, + dynamic_charged: 0, + dynamic_available: 0, + group_count: 0, + privilege_count: 0, + modified_id: PRIMARY_TOKEN_MODIFIED_ID, + }, + } + } +} + +pub(crate) struct TokenHandleObject { + token: Arc, +} + +pub(crate) struct TokenSubsystem; + +impl FdEnabledSubsystem for TokenSubsystem { + type Entry = TokenHandleObject; +} + +impl FdEnabledSubsystemEntry for TokenHandleObject {} + +impl WindowsHandleSubsystem for TokenSubsystem { + fn normalize_desired_access(desired_access: u32) -> u32 { + TokenAccess::from_desired_access(desired_access).bits() + } +} + +impl Task { + pub(crate) fn sys_nt_open_process_token( + &self, + process_handle: ProcessHandle, + desired_access: u32, + token_handle: MutPtr, + ) -> NtStatus { + self.open_process_token(process_handle, desired_access, 0, token_handle) + } + + pub(crate) fn sys_nt_open_process_token_ex( + &self, + process_handle: ProcessHandle, + desired_access: u32, + handle_attributes: u32, + token_handle: MutPtr, + ) -> NtStatus { + self.open_process_token( + process_handle, + desired_access, + handle_attributes, + token_handle, + ) + } + + fn open_process_token( + &self, + process_handle: ProcessHandle, + desired_access: u32, + handle_attributes: u32, + token_handle: MutPtr, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(token_handle) { + return status; + } + let Some(attributes) = HandleAttributes::from_token_open_attributes(handle_attributes) + else { + return NtStatus::INVALID_PARAMETER; + }; + if !process_handle.is_current() { + // TODO(token-cross-process): Resolve real process handles once the sandbox supports + // multiple guest processes and per-process primary tokens. + return NtStatus::INVALID_HANDLE; + } + + let handle = match self.insert_typed_handle_with_attributes::( + TokenHandleObject { + token: self.process.token.clone(), + }, + TokenAccess::from_desired_access(desired_access).bits(), + attributes, + drop, + ) { + Ok(handle) => handle, + Err(status) => return status, + }; + if token_handle.write_at_offset(0, handle).is_none() { + self.close_token_handle(handle); + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn sys_nt_query_information_token( + &self, + token_handle: Handle, + token_information_class: u32, + token_information: MutPtr, + token_information_length: u32, + return_length: MutPtr, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(return_length) { + return status; + } + let Ok(class) = TokenInformationClass::try_from(token_information_class) else { + return NtStatus::INVALID_INFO_CLASS; + }; + if let Err(status) = probe_guest_output_buffer::( + token_information, + token_information_length as usize, + ) { + return status; + } + + let entry = match self.typed_handle_entry_with_access::( + token_handle, + TokenAccess::QUERY.bits(), + ) { + Ok(entry) => entry, + Err(status) => return status, + }; + + match class { + TokenInformationClass::User => entry.with_entry(|entry| { + Self::write_token_information_value( + token_information, + token_information_length, + return_length, + || { + let sid_address = token_information + .as_usize() + .checked_add(size_of::()) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + Ok(TokenUserInformation { + user: TokenUser { + user: SidAndAttributes { + sid: sid_address, + attributes: 0, + padding: 0, + }, + }, + sid: entry.token.user, + }) + }, + ) + }), + TokenInformationClass::Privileges => Self::write_token_information_value( + token_information, + token_information_length, + return_length, + || Ok(TokenPrivileges { privilege_count: 0 }), + ), + TokenInformationClass::Statistics => entry.with_entry(|entry| { + Self::write_token_information_value( + token_information, + token_information_length, + return_length, + || Ok(entry.token.statistics), + ) + }), + _ => { + // TODO(token-model): Add each information class when its backing token state is + // modeled; do not synthesize security-sensitive token data. + NtStatus::NOT_IMPLEMENTED + } + } + } + + fn write_token_information_value( + token_information: MutPtr, + token_information_length: u32, + return_length: MutPtr, + build_information: impl FnOnce() -> Result, + ) -> NtStatus { + let required_length = size_of::().trunc(); + if return_length.write_at_offset(0, required_length).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + if token_information_length < required_length { + return NtStatus::BUFFER_TOO_SMALL; + } + let information = match build_information() { + Ok(information) => information, + Err(status) => return status, + }; + if token_information + .write_slice_at_offset(0, information.as_bytes()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + NtStatus::SUCCESS + } + + pub(crate) fn close_token_handle(&self, handle: Handle) { + self.close_typed_handle::(handle, drop); + } + + pub(crate) fn close_token(token: TokenHandleObject) { + drop(token); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::{mut_byte_ptr, mut_ptr, null_mut_ptr, test_task}; + + #[repr(C)] + #[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] + struct TokenUserBuffer { + user: TokenUser, + sid: Sid, + padding: u32, + } + + #[test] + fn open_and_query_process_token_identity() { + let task = test_task(); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_process_token( + ProcessHandle::CURRENT, + TokenAccess::QUERY.bits(), + mut_ptr(&mut handle), + ), + NtStatus::SUCCESS + ); + + let mut required_length = 0; + assert_eq!( + task.sys_nt_query_information_token( + handle, + TokenInformationClass::User as u32, + null_mut_ptr(), + 0, + mut_ptr(&mut required_length), + ), + NtStatus::BUFFER_TOO_SMALL + ); + assert_eq!( + required_length as usize, + size_of::() + size_of::() + ); + + let mut output = TokenUserBuffer { + user: TokenUser { + user: SidAndAttributes { + sid: 0, + attributes: u32::MAX, + padding: 0, + }, + }, + sid: Sid { + revision: 0, + sub_authority_count: 0, + identifier_authority: [0; 6], + sub_authority: [0], + }, + padding: 0, + }; + assert_eq!( + task.sys_nt_query_information_token( + handle, + TokenInformationClass::User as u32, + mut_byte_ptr(&mut output), + required_length, + mut_ptr(&mut required_length), + ), + NtStatus::SUCCESS + ); + assert_eq!( + output.user.user.sid, + core::ptr::from_ref(&output.sid) as usize + ); + assert_eq!(output.user.user.attributes, 0); + assert_eq!(output.sid, LOCAL_SYSTEM_SID); + } +} From 816e73c4aa184d647f0a3e67c99f182d80ad300e Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 23 Jul 2026 12:02:19 -0700 Subject: [PATCH 121/319] Implement NtQuerySecurityAttributesToken for Windows Shim (#1073) This PR adds basic support for `NtQuerySecurityAttributesToken` without attribute mutation and duplicate-token copying. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 18 + litebox_shim_windows/src/syscalls/mod.rs | 18 + litebox_shim_windows/src/syscalls/token.rs | 455 ++++++++++++++++++++- 3 files changed, 486 insertions(+), 5 deletions(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 4a644dabf8..0764e836bb 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -1706,6 +1706,24 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtQuerySecurityAttributesToken { + token_handle, + attributes, + number_of_attributes, + buffer, + length, + return_length, + } => { + let status = self.sys_nt_query_security_attributes_token( + token_handle, + attributes, + number_of_attributes, + buffer, + length, + return_length, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, source, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index 60c1e3a85e..cfe18d48d0 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -513,6 +513,14 @@ pub(crate) enum SyscallRequest { token_information_length: u32, return_length: Platform::RawMutPointer, }, + NtQuerySecurityAttributesToken { + token_handle: Handle, + attributes: Platform::RawConstPointer, + number_of_attributes: u32, + buffer: Platform::RawMutPointer, + length: u32, + return_length: Platform::RawMutPointer, + }, NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag: u32, source: Platform::RawConstPointer, @@ -1017,6 +1025,16 @@ impl SyscallRequest { token_information_length, return_length:*, })), + NtSysno::NtQuerySecurityAttributesToken => { + Some(sys_req!(NtQuerySecurityAttributesToken { + token_handle: { Handle::from_raw }, + attributes:*, + number_of_attributes, + buffer:*, + length, + return_length:*, + })) + } NtSysno::NtConvertBetweenAuxiliaryCounterAndPerformanceCounter => Some( sys_req!(NtConvertBetweenAuxiliaryCounterAndPerformanceCounter { flag, diff --git a/litebox_shim_windows/src/syscalls/token.rs b/litebox_shim_windows/src/syscalls/token.rs index f23f7bee19..48be754a54 100644 --- a/litebox_shim_windows/src/syscalls/token.rs +++ b/litebox_shim_windows/src/syscalls/token.rs @@ -3,7 +3,10 @@ //! Windows NT access-token syscalls. +use alloc::boxed::Box; use alloc::sync::Arc; +use alloc::vec::Vec; +use core::borrow::Borrow; use core::mem::size_of; use int_enum::IntEnum; @@ -13,11 +16,11 @@ use litebox::utils::TruncateExt as _; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; -use crate::nt_types::{AccessMask, Luid}; +use crate::nt_types::{AccessMask, Luid, UnicodeString}; use crate::syscalls::{Handle, ProcessHandle}; use crate::{ - HandleAttributes, MutPtr, ShimFS, Task, WindowsHandleSubsystem, probe_guest_output_buffer, - probe_guest_output_preserving_value, + ConstPtr, HandleAttributes, MutPtr, ShimFS, Task, WindowsHandleSubsystem, + probe_guest_output_buffer, probe_guest_output_preserving_value, }; bitflags::bitflags! { @@ -130,6 +133,66 @@ pub(crate) enum TokenInformationClass { LoggingInformation = 49, } +#[repr(u16)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum TokenSecurityAttributeValueType { + Invalid = 0x00, + Int64 = 0x01, + Uint64 = 0x02, + String = 0x03, + Fqbn = 0x04, + Sid = 0x05, + Boolean = 0x06, + OctetString = 0x10, +} + +#[repr(u16)] +#[derive(Clone, Copy, Debug, Eq, IntEnum, PartialEq)] +enum TokenSecurityAttributesInformationVersion { + V1 = 1, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + struct TokenSecurityAttributeFlags: u32 { + const NON_INHERITABLE = 0x0001; + const VALUE_CASE_SENSITIVE = 0x0002; + const USE_FOR_DENY_ONLY = 0x0004; + const DISABLED_BY_DEFAULT = 0x0008; + const DISABLED = 0x0010; + const MANDATORY = 0x0020; + const COMPARE_IGNORE = 0x0040; + const CUSTOM = 0xffff_0000; + } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, FromBytes, Immutable, IntoBytes)] +struct TokenSecurityAttributeV1 { + name: UnicodeString, + value_type: u16, + reserved: u16, + flags: u32, + value_count: u32, + padding: u32, + values: usize, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] +struct TokenSecurityAttributesInformation { + version: u16, + reserved: u16, + attribute_count: u32, + attribute_v1: usize, +} + +struct TokenSecurityAttribute { + name: Box<[u16]>, + value_type: TokenSecurityAttributeValueType, + flags: TokenSecurityAttributeFlags, +} + #[repr(C)] #[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] pub(crate) struct SidAndAttributes { @@ -211,10 +274,11 @@ const LOCAL_SYSTEM_SID: Sid = Sid { pub(crate) struct TokenObject { user: Sid, statistics: TokenStatistics, + security_attributes: Box<[TokenSecurityAttribute]>, } impl TokenObject { - pub(crate) const fn primary() -> Self { + pub(crate) fn primary() -> Self { Self { user: LOCAL_SYSTEM_SID, statistics: TokenStatistics { @@ -229,6 +293,7 @@ impl TokenObject { privilege_count: 0, modified_id: PRIMARY_TOKEN_MODIFIED_ID, }, + security_attributes: Box::new([]), } } } @@ -252,6 +317,10 @@ impl WindowsHandleSubsystem for TokenSubsystem { } impl Task { + const CURRENT_PROCESS_TOKEN: Handle = Handle::from_raw(usize::MAX - 3); + const CURRENT_THREAD_TOKEN: Handle = Handle::from_raw(usize::MAX - 4); + const CURRENT_THREAD_EFFECTIVE_TOKEN: Handle = Handle::from_raw(usize::MAX - 5); + pub(crate) fn sys_nt_open_process_token( &self, process_handle: ProcessHandle, @@ -381,6 +450,14 @@ impl Task { || Ok(entry.token.statistics), ) }), + TokenInformationClass::SecurityAttributes => entry.with_entry(|entry| { + Self::write_token_security_attributes( + &entry.token.security_attributes, + token_information, + token_information_length, + return_length, + ) + }), _ => { // TODO(token-model): Add each information class when its backing token state is // modeled; do not synthesize security-sensitive token data. @@ -389,6 +466,221 @@ impl Task { } } + pub(crate) fn sys_nt_query_security_attributes_token( + &self, + token_handle: Handle, + attributes: ConstPtr, + number_of_attributes: u32, + buffer: MutPtr, + length: u32, + return_length: MutPtr, + ) -> NtStatus { + if let Err(status) = probe_guest_output_preserving_value::(return_length) { + return status; + } + if number_of_attributes != 0 && attributes.as_usize() == 0 { + return NtStatus::INVALID_PARAMETER; + } + + let requested_names = + match Self::read_security_attribute_names(attributes, number_of_attributes) { + Ok(names) => names, + Err(status) => return status, + }; + + let query_attributes = |security_attributes: &[TokenSecurityAttribute]| { + let selected = + match Self::select_security_attributes(security_attributes, &requested_names) { + Ok(selected) => selected, + Err(status) => { + if return_length.write_at_offset(0, 0).is_none() { + return NtStatus::ACCESS_VIOLATION; + } + return status; + } + }; + Self::write_token_security_attributes(&selected, buffer, length, return_length) + }; + + if token_handle == Self::CURRENT_PROCESS_TOKEN + || token_handle == Self::CURRENT_THREAD_EFFECTIVE_TOKEN + { + return query_attributes(&self.process.token.security_attributes); + } + if token_handle == Self::CURRENT_THREAD_TOKEN { + return NtStatus::NO_TOKEN; + } + + let entry = match self.typed_handle_entry_with_access::( + token_handle, + TokenAccess::QUERY.bits(), + ) { + Ok(entry) => entry, + Err(status) => return status, + }; + entry.with_entry(|entry| query_attributes(&entry.token.security_attributes)) + } + + fn read_security_attribute_names( + attributes: ConstPtr, + number_of_attributes: u32, + ) -> Result, NtStatus> { + let mut names = Vec::new(); + for index in 0..number_of_attributes as usize { + let name = attributes + .read_at_offset(index.cast_signed()) + .ok_or(NtStatus::ACCESS_VIOLATION)? + .read_string::()?; + names.push(name); + } + Ok(names) + } + + fn select_security_attributes<'a>( + security_attributes: &'a [TokenSecurityAttribute], + requested_names: &[alloc::string::String], + ) -> Result, NtStatus> { + if requested_names.is_empty() { + return Ok(security_attributes.iter().collect()); + } + + let mut selected = Vec::new(); + for requested_name in requested_names { + // TODO(token-security-attribute-casefold): Use Windows invariant Unicode + // case-folding once non-ASCII attribute names are modeled. + let Some(attribute) = security_attributes.iter().find(|attribute| { + requested_name + .encode_utf16() + .eq(attribute.name.iter().copied()) + || requested_name.eq_ignore_ascii_case( + &alloc::string::String::from_utf16_lossy(&attribute.name), + ) + }) else { + return Err(NtStatus::NOT_FOUND); + }; + selected.push(attribute); + } + Ok(selected) + } + + fn write_token_security_attributes>( + security_attributes: &[S], + buffer: MutPtr, + length: u32, + return_length: MutPtr, + ) -> NtStatus { + let Some(attribute_bytes) = + size_of::().checked_mul(security_attributes.len()) + else { + return NtStatus::INVALID_PARAMETER; + }; + let Some(mut required_length) = + size_of::().checked_add(attribute_bytes) + else { + return NtStatus::INVALID_PARAMETER; + }; + for attribute in security_attributes { + let attribute = attribute.borrow(); + let Some(name_bytes) = attribute.name.len().checked_mul(size_of::()) else { + return NtStatus::INVALID_PARAMETER; + }; + required_length = match required_length.checked_add(name_bytes) { + Some(length) => length, + None => return NtStatus::INVALID_PARAMETER, + }; + } + let Ok(required_length_u32) = u32::try_from(required_length) else { + return NtStatus::INVALID_PARAMETER; + }; + if return_length + .write_at_offset(0, required_length_u32) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if length < required_length_u32 { + return NtStatus::BUFFER_TOO_SMALL; + } + if let Err(status) = probe_guest_output_buffer::(buffer, required_length) { + return status; + } + + let attribute_v1 = if security_attributes.is_empty() { + 0 + } else { + match buffer + .as_usize() + .checked_add(size_of::()) + { + Some(address) => address, + None => return NtStatus::ACCESS_VIOLATION, + } + }; + let information = TokenSecurityAttributesInformation { + version: TokenSecurityAttributesInformationVersion::V1 as u16, + reserved: 0, + attribute_count: security_attributes.len().trunc(), + attribute_v1, + }; + if buffer + .write_slice_at_offset(0, information.as_bytes()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + + let mut name_offset = size_of::() + attribute_bytes; + let attribute_buffer = + MutPtr::::from_usize(attribute_v1); + for (index, attribute) in security_attributes.iter().enumerate() { + let attribute = attribute.borrow(); + let Some(name_length) = attribute.name.len().checked_mul(size_of::()) else { + return NtStatus::INVALID_PARAMETER; + }; + let Ok(name_length_u16) = u16::try_from(name_length) else { + return NtStatus::INVALID_PARAMETER; + }; + let Some(name_address) = buffer.as_usize().checked_add(name_offset) else { + return NtStatus::ACCESS_VIOLATION; + }; + let information = TokenSecurityAttributeV1 { + name: UnicodeString { + length: name_length_u16, + maximum_length: name_length_u16, + padding_0: [0; 4], + buffer: name_address, + }, + value_type: attribute.value_type as u16, + reserved: 0, + flags: attribute.flags.bits(), + value_count: 0, + padding: 0, + values: 0, + }; + if attribute_buffer + .write_at_offset(index.cast_signed(), information) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if buffer + .write_slice_at_offset(name_offset.cast_signed(), attribute.name.as_bytes()) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + name_offset += name_length; + } + + // TODO(token-security-attribute-values): Store and serialize typed V1 values when + // NtCreateTokenEx or NtSetInformationToken can populate token security attributes. + // TODO(token-security-attribute-set): Implement TokenSecurityAttributes mutation with + // SeTcbPrivilege enforcement when NtSetInformationToken is added. + // TODO(token-security-attribute-duplicate): Deep-copy attributes when NtDuplicateToken + // creates distinct token objects. + NtStatus::SUCCESS + } + fn write_token_information_value( token_information: MutPtr, token_information_length: u32, @@ -427,7 +719,10 @@ impl Task { #[cfg(test)] mod tests { use super::*; - use crate::tests::{mut_byte_ptr, mut_ptr, null_mut_ptr, test_task}; + use crate::tests::{ + const_ptr, mut_byte_ptr, mut_ptr, null_const_ptr, null_mut_ptr, test_task, unicode_string, + utf16_units, + }; #[repr(C)] #[derive(Clone, Copy, Debug, Eq, FromBytes, Immutable, IntoBytes, PartialEq)] @@ -499,4 +794,154 @@ mod tests { assert_eq!(output.user.user.attributes, 0); assert_eq!(output.sid, LOCAL_SYSTEM_SID); } + + #[test] + fn named_security_attribute_queries_are_case_insensitive() { + let mut task = test_task(); + let process = Arc::get_mut(&mut task.process).expect("test task must own its process"); + let token = Arc::get_mut(&mut process.token).expect("test process must own its token"); + token.security_attributes = alloc::vec![TokenSecurityAttribute { + name: utf16_units("LITEBOX://TestAttribute").into_boxed_slice(), + value_type: TokenSecurityAttributeValueType::Uint64, + flags: TokenSecurityAttributeFlags::MANDATORY, + }] + .into_boxed_slice(); + + let requested_name = utf16_units("litebox://testattribute"); + let requested_name = unicode_string(&requested_name); + let mut output = [0_u8; 128]; + let mut return_length = 0; + assert_eq!( + task.sys_nt_query_security_attributes_token( + Task::::CURRENT_PROCESS_TOKEN, + const_ptr(&requested_name), + 1, + mut_byte_ptr(&mut output), + output.len().trunc(), + mut_ptr(&mut return_length), + ), + NtStatus::SUCCESS + ); + + let output_address = output.as_ptr() as usize; + let information = TokenSecurityAttributesInformation::read_from_prefix(&output) + .expect("valid header") + .0; + assert_eq!(information.version, 1); + assert_eq!(information.attribute_count, 1); + assert_eq!( + information.attribute_v1, + output_address + size_of::() + ); + let attribute = TokenSecurityAttributeV1::read_from_prefix( + &output[size_of::()..], + ) + .expect("valid attribute") + .0; + assert_eq!( + attribute.value_type, + TokenSecurityAttributeValueType::Uint64 as u16 + ); + assert_eq!( + attribute.flags, + TokenSecurityAttributeFlags::MANDATORY.bits() + ); + assert_eq!(attribute.value_count, 0); + assert_eq!(attribute.values, 0); + assert_eq!( + attribute.name.buffer, + output_address + + size_of::() + + size_of::() + ); + } + + #[test] + fn named_security_attribute_query_reports_missing_names() { + let task = test_task(); + let requested_name = utf16_units("LITEBOX://Missing"); + let requested_name = unicode_string(&requested_name); + let mut return_length = u32::MAX; + + assert_eq!( + task.sys_nt_query_security_attributes_token( + Task::::CURRENT_PROCESS_TOKEN, + const_ptr(&requested_name), + 1, + null_mut_ptr(), + 0, + mut_ptr(&mut return_length), + ), + NtStatus::NOT_FOUND + ); + assert_eq!(return_length, 0); + } + + #[test] + fn query_security_attributes_enforces_query_access() { + let task = test_task(); + let mut handle = Handle::default(); + assert_eq!( + task.sys_nt_open_process_token( + ProcessHandle::CURRENT, + TokenAccess::DUPLICATE.bits(), + mut_ptr(&mut handle), + ), + NtStatus::SUCCESS + ); + let mut output = TokenSecurityAttributesInformation { + version: 0, + reserved: 0, + attribute_count: 0, + attribute_v1: 0, + }; + let mut return_length = 0; + + assert_eq!( + task.sys_nt_query_security_attributes_token( + handle, + null_const_ptr(), + 0, + mut_byte_ptr(&mut output), + size_of::().trunc(), + mut_ptr(&mut return_length), + ), + NtStatus::ACCESS_DENIED + ); + } + + #[test] + fn query_security_attributes_handles_token_pseudo_handles() { + let task = test_task(); + let mut output = TokenSecurityAttributesInformation { + version: 0, + reserved: 0, + attribute_count: 0, + attribute_v1: 0, + }; + let mut return_length = 0; + + assert_eq!( + task.sys_nt_query_security_attributes_token( + Task::::CURRENT_THREAD_TOKEN, + null_const_ptr(), + 0, + mut_byte_ptr(&mut output), + size_of::().trunc(), + mut_ptr(&mut return_length), + ), + NtStatus::NO_TOKEN + ); + assert_eq!( + task.sys_nt_query_security_attributes_token( + Task::::CURRENT_THREAD_EFFECTIVE_TOKEN, + null_const_ptr(), + 0, + mut_byte_ptr(&mut output), + size_of::().trunc(), + mut_ptr(&mut return_length), + ), + NtStatus::SUCCESS + ); + } } From 5a459f57a7f4c9c7025981371cc4ab724a3a4104 Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Thu, 23 Jul 2026 12:56:46 -0700 Subject: [PATCH 122/319] ulitebox fixes due to recent merge #806: Replace removed punchthrough APIs with the arch-specific provider. #995: Migrate device filesystem setup to the mounted backend API. #1040: Migrate Tar-RO filesystem setup to the resolver-backed backend. --- .../src/lib.rs | 3 +- litebox_runner_windows_userland/src/lib.rs | 3 +- litebox_shim_windows/src/lib.rs | 72 +++++++++---------- litebox_shim_windows/src/syscalls/registry.rs | 21 ++++-- litebox_shim_windows/src/tests.rs | 4 +- 5 files changed, 52 insertions(+), 51 deletions(-) diff --git a/litebox_runner_windows_on_linux_userland/src/lib.rs b/litebox_runner_windows_on_linux_userland/src/lib.rs index 83022065a5..a0e55e2695 100644 --- a/litebox_runner_windows_on_linux_userland/src/lib.rs +++ b/litebox_runner_windows_on_linux_userland/src/lib.rs @@ -89,8 +89,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { .expect("/tmp chown cannot fail on a fresh in-memory file system"); }); - let tar_ro = litebox::fs::tar_ro::FileSystem::new(litebox, tar_data.into()); - shim_builder.default_fs(in_mem, tar_ro) + shim_builder.default_fs(in_mem, tar_data.into()) }; let initial_file_system = std::sync::Arc::new(initial_file_system); diff --git a/litebox_runner_windows_userland/src/lib.rs b/litebox_runner_windows_userland/src/lib.rs index dd206a3dd7..84b58f4c83 100644 --- a/litebox_runner_windows_userland/src/lib.rs +++ b/litebox_runner_windows_userland/src/lib.rs @@ -89,8 +89,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { .expect("/tmp chown cannot fail on a fresh in-memory file system"); }); - let tar_ro = litebox::fs::tar_ro::FileSystem::new(litebox, tar_data.into()); - shim_builder.default_fs(in_mem, tar_ro) + shim_builder.default_fs(in_mem, tar_data.into()) }; let initial_file_system = std::sync::Arc::new(initial_file_system); diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 0764e836bb..9b6c773504 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -11,6 +11,7 @@ extern crate alloc; +use alloc::borrow::Cow; use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; @@ -21,7 +22,7 @@ use litebox_common_windows::nt_status::NtStatus; use litebox::LiteBox; use litebox::mm::PageManager; use litebox::platform::{ - CrngProvider, PageManagementProvider, PunchthroughProvider, PunchthroughToken, + ArchSpecificProvider, ArchSpecificRegister, CrngProvider, PageManagementProvider, RawConstPointer as _, RawMutPointer as _, RawPointerProvider, StdioProvider, SystemInfoProvider, TimeProvider, }; @@ -67,6 +68,7 @@ pub trait ShimPlatform: RawSyncPrimitivesProvider + RawPointerProvider + PageManagementProvider + + ArchSpecificProvider + SystemInfoProvider + TimeProvider + 'static @@ -77,6 +79,7 @@ impl ShimPlatform for T where T: RawSyncPrimitivesProvider + RawPointerProvider + PageManagementProvider + + ArchSpecificProvider + SystemInfoProvider + TimeProvider + 'static @@ -210,8 +213,8 @@ pub type WindowsFS = litebox::fs::layered::FileSystem< litebox::fs::in_mem::FileSystem, litebox::fs::layered::FileSystem< Platform, - litebox::fs::devices::FileSystem, - litebox::fs::tar_ro::FileSystem, + litebox::fs::resolver::Resolver, + litebox::fs::resolver::Resolver, >, >; @@ -300,21 +303,10 @@ where .ok_or(NtStatus::ACCESS_VIOLATION) } -fn set_guest_teb(platform: &Platform, teb_address: usize) -> bool -where - Platform: PunchthroughProvider + RawPointerProvider, - ::PunchthroughToken<'static>: PunchthroughToken< - Punchthrough = litebox_common_linux::PunchthroughSyscall<'static, Platform>, - >, -{ - let punchthrough: litebox_common_linux::PunchthroughSyscall<'static, Platform> = - litebox_common_linux::PunchthroughSyscall::SetFsBase { addr: teb_address }; - let Some(token) = platform.get_punchthrough_token_for(punchthrough) else { - litebox_util_log::warn!(teb:% = format_args!("{teb_address:#x}"); "Failed to get punchthrough token for Windows TEB base"); - return false; - }; - - if let Err(error) = token.execute() { +fn set_guest_teb(platform: &Platform, teb_address: usize) -> bool { + if let Err(error) = + platform.set_arch_specific_register(&ArchSpecificRegister::FsBase, teb_address) + { litebox_util_log::warn!(error:? = error, teb:% = format_args!("{teb_address:#x}"); "Failed to set Windows TEB base"); return false; } @@ -430,12 +422,12 @@ impl WindowsShimBuilder { pub fn default_fs( &self, in_mem_fs: litebox::fs::in_mem::FileSystem, - tar_ro_fs: litebox::fs::tar_ro::FileSystem, + tar_data: Cow<'static, [u8]>, ) -> DefaultFS where Platform: CrngProvider + StdioProvider, { - default_fs(&self.litebox, in_mem_fs, tar_ro_fs) + default_fs(&self.litebox, in_mem_fs, tar_data) } #[must_use] @@ -648,13 +640,7 @@ struct Task { } impl Task { - fn init(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation - where - Platform: PunchthroughProvider, - ::PunchthroughToken<'static>: PunchthroughToken< - Punchthrough = litebox_common_linux::PunchthroughSyscall<'static, Platform>, - >, - { + fn init(&self, ctx: &mut litebox_common_linux::PtRegs) -> ContinueOperation { if !set_guest_teb(self.global.platform, self.teb_address) { return ContinueOperation::Terminate; } @@ -2296,14 +2282,7 @@ pub struct WindowsShimEntrypoints { _not_send: PhantomData<*const ()>, } -impl EnterShim for WindowsShimEntrypoints -where - Platform: ShimPlatform + PunchthroughProvider, - ::PunchthroughToken<'static>: PunchthroughToken< - Punchthrough = litebox_common_linux::PunchthroughSyscall<'static, Platform>, - >, - FS: ShimFS, -{ +impl EnterShim for WindowsShimEntrypoints { type ExecutionContext = litebox_common_linux::PtRegs; fn init(&self, ctx: &mut Self::ExecutionContext) -> ContinueOperation { @@ -2345,19 +2324,36 @@ pub struct LoadedProgram { fn default_fs( litebox: &LiteBox, in_mem_fs: litebox::fs::in_mem::FileSystem, - tar_ro_fs: litebox::fs::tar_ro::FileSystem, + tar_data: Cow<'static, [u8]>, ) -> WindowsFS where Platform: ShimPlatform + CrngProvider + StdioProvider, { - let devices = litebox::fs::devices::FileSystem::new(litebox); + let devices = litebox::fs::resolver::Resolver::new( + litebox, + litebox::fs::composer::Composer::builder() + .mount("/dev", |allocator| { + litebox::fs::devices::Devices::new(litebox, allocator) + }) + .build() + .unwrap(), + ); + let tar_ro = litebox::fs::resolver::Resolver::new( + litebox, + litebox::fs::composer::Composer::builder() + .mount("/", |allocator| { + litebox::fs::tar_ro::TarRo::new(tar_data, allocator) + }) + .build() + .unwrap(), + ); litebox::fs::layered::FileSystem::new( litebox, in_mem_fs, litebox::fs::layered::FileSystem::new( litebox, devices, - tar_ro_fs, + tar_ro, litebox::fs::layered::LayeringSemantics::LowerLayerReadOnly, ), litebox::fs::layered::LayeringSemantics::LowerLayerWritableFiles, diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 9c2f409acd..a3500c4614 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -51,7 +51,7 @@ use crate::nt_types::{AccessMask, ObjectAttributes, UnicodeString, read_object_a type RegistryFileSystem = litebox::fs::layered::FileSystem< Platform, litebox::fs::in_mem::FileSystem, - litebox::fs::tar_ro::FileSystem, + litebox::fs::resolver::Resolver, >; pub(crate) struct RegistryKeySubsystem(PhantomData); @@ -287,14 +287,23 @@ impl RegistryStore { } }); + let tar_ro = litebox::fs::resolver::Resolver::new( + litebox, + litebox::fs::composer::Composer::builder() + .mount("/", |allocator| { + litebox::fs::tar_ro::TarRo::new( + // TODO: Replace with tar file provided by the user + litebox::fs::tar_ro::EMPTY_TAR_FILE.into(), + allocator, + ) + }) + .build() + .unwrap(), + ); let fs = litebox::fs::layered::FileSystem::new( litebox, in_mem, - litebox::fs::tar_ro::FileSystem::new( - litebox, - // TODO: Replace with tar file provided by the user - litebox::fs::tar_ro::EMPTY_TAR_FILE.into(), - ), + tar_ro, litebox::fs::layered::LayeringSemantics::LowerLayerReadOnly, ); diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 53f2ad296f..38a2250f55 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -147,10 +147,8 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task::new(platform); - let fs = Arc::new(shim_builder.default_fs(in_mem, tar_ro)); + let fs = Arc::new(shim_builder.default_fs(in_mem, litebox::fs::tar_ro::EMPTY_TAR_FILE.into())); let shim = shim_builder.build(); let WindowsShim(global) = shim; From a3fed4a43aa76c3da9fddd394ffe9fb81afffcee Mon Sep 17 00:00:00 2001 From: Jay Bosamiya Date: Thu, 23 Jul 2026 13:07:48 -0700 Subject: [PATCH 123/319] Ignore pending insta snapshots --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7ef4aa8dac..24f99cb1df 100644 --- a/.gitignore +++ b/.gitignore @@ -106,6 +106,9 @@ target/ # These are backup files generated by rustfmt **/*.rs.bk +# Pending snapshots generated by insta +*.snap.new + # MSVC Windows builds of rustc generate these, which store debugging information *.pdb From c7c5dc333b0b3409abf06f0e93006fd0719662e6 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 23 Jul 2026 15:12:14 -0700 Subject: [PATCH 124/319] Add broker shared control ring primitives (#1072) Adds hostile-peer-safe shared control ring primitives: one exact memfd mapping with independent 64-slot SPSC request and response rings. Producers release-publish non-wrapping sequences; consumers copy and validate owned snapshots; producers validate shared reclamation heads. Adds process-shared futex wait/wake over wrapping hint epochs. `Empty` and `Full` return race-free wait epochs, publication commits state before advancing its epoch, and role-bound endpoints avoid exposing raw mapping access. Production requests and responses remain socket-framed; control ring integration is deferred to the next PR. --------- Copilot-Session: ac86dd2d-0280-4d7b-87a8-5654ad1503a6 --- litebox_broker_protocol/src/shared_memory.rs | 43 + litebox_broker_transport/Cargo.toml | 9 +- litebox_broker_transport/src/control_ring.rs | 1543 +++++++++++++++++ litebox_broker_transport/src/lib.rs | 7 + litebox_broker_transport/src/shared_memory.rs | 333 +++- 5 files changed, 1933 insertions(+), 2 deletions(-) create mode 100644 litebox_broker_transport/src/control_ring.rs diff --git a/litebox_broker_protocol/src/shared_memory.rs b/litebox_broker_protocol/src/shared_memory.rs index bed7da4c4b..eaf243aca9 100644 --- a/litebox_broker_protocol/src/shared_memory.rs +++ b/litebox_broker_protocol/src/shared_memory.rs @@ -33,6 +33,9 @@ pub enum SharedMemoryError { /// The requested byte range is outside the shared-memory resource. #[error("shared-memory range is out of bounds")] InvalidRange, + /// An atomic access is not naturally aligned. + #[error("shared-memory atomic access is not naturally aligned")] + UnalignedAtomic, } /// Byte-copy access to a shared-memory resource. @@ -65,6 +68,46 @@ pub trait SharedMemory: Send + Sync + 'static { fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError>; } +/// Ordered atomic access to shared-memory synchronization values. +/// +/// Implementations must provide naturally aligned, indivisible, system-visible +/// operations over coherent shared memory. Atomic values must not also be +/// accessed through [`SharedMemory::read`] or [`SharedMemory::write`] by a +/// conforming endpoint. +pub trait AtomicSharedMemory: SharedMemory { + /// Atomically loads a naturally aligned native-endian `u32` with acquire + /// ordering. + fn load_u32_acquire(&self, offset: usize) -> Result; + + /// Atomically increments a naturally aligned native-endian `u32` with + /// release ordering and returns its previous value. + fn fetch_add_u32_release(&self, offset: usize, value: u32) -> Result; + + /// Atomically loads a naturally aligned native-endian `u64` with acquire + /// ordering. + fn load_u64_acquire(&self, offset: usize) -> Result; + + /// Atomically stores a naturally aligned native-endian `u64` with release + /// ordering. + /// + /// On error, the value must not have been stored. + fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError>; + + /// Atomically release-stores a native-endian `u64`, then release-adds to a + /// native-endian `u32`, returning the previous `u32`. + /// + /// Both values must be naturally aligned and occupy non-overlapping ranges. + /// Implementations must validate both accesses before storing either value. + /// On error, neither value may have been modified. + fn store_u64_and_fetch_add_u32_release( + &self, + store_offset: usize, + value: u64, + add_offset: usize, + add_value: u32, + ) -> Result; +} + impl SharedMemory for Arc { fn len(&self) -> usize { (**self).len() diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml index 3fc4be1087..fa423ee624 100644 --- a/litebox_broker_transport/Cargo.toml +++ b/litebox_broker_transport/Cargo.toml @@ -5,7 +5,14 @@ edition = "2024" [features] std = [] -linux-userland = ["std", "dep:libc", "dep:rustix", "rustix/fs", "rustix/net"] +linux-userland = [ + "std", + "dep:libc", + "dep:rustix", + "rustix/fs", + "rustix/net", + "rustix/thread", +] [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } diff --git a/litebox_broker_transport/src/control_ring.rs b/litebox_broker_transport/src/control_ring.rs new file mode 100644 index 0000000000..be33978f19 --- /dev/null +++ b/litebox_broker_transport/src/control_ring.rs @@ -0,0 +1,1543 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Hostile-peer-safe shared control-ring state machines. + +use alloc::sync::Arc; +use core::mem::size_of; +use core::ops::Range; +#[cfg(test)] +use core::sync::atomic::fence; + +#[cfg(test)] +use litebox_broker_protocol::shared_memory::SharedMemory; +use litebox_broker_protocol::shared_memory::{AtomicSharedMemory, SharedMemoryError}; + +/// Size of one shared control-ring slot. +pub const CONTROL_RING_SLOT_SIZE: usize = 4096; + +/// Size of the fixed metadata at the start of a control-ring slot. +pub const CONTROL_RING_SLOT_HEADER_SIZE: usize = 16; + +/// Maximum encoded request or response size in one control-ring slot. +pub const CONTROL_RING_PAYLOAD_CAPACITY: usize = + CONTROL_RING_SLOT_SIZE - CONTROL_RING_SLOT_HEADER_SIZE; + +/// Number of slots in each direction of the shared control ring. +pub const CONTROL_RING_SLOT_COUNT: u64 = 64; + +/// Exact shared-memory size required for both control-ring directions. +pub const CONTROL_RING_MEMORY_SIZE: usize = + CONTROL_RING_DATA_SIZE + CONTROL_RING_SYNC_DIRECTION_SIZE * 2; + +// The fixed count is representable by `usize` on every supported target. +#[allow(clippy::cast_possible_truncation)] +const CONTROL_RING_DIRECTION_SIZE: usize = + CONTROL_RING_SLOT_SIZE * CONTROL_RING_SLOT_COUNT as usize; +const CONTROL_RING_DATA_SIZE: usize = CONTROL_RING_DIRECTION_SIZE * 2; +const CONTROL_RING_SYNC_DIRECTION_SIZE: usize = 16; +const PRODUCER_EPOCH_OFFSET: usize = 0; +const CONSUMER_EPOCH_OFFSET: usize = 4; +const CONSUMER_HEAD_OFFSET: usize = 8; + +/// One direction in the shared control-ring mapping. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ControlRingDirection { + /// Local-to-broker request ring. + Requests, + /// Broker-to-local response ring. + Responses, +} + +impl ControlRingDirection { + fn slot_range(self, slot: u64) -> Range { + debug_assert!(slot < CONTROL_RING_SLOT_COUNT); + let slot = usize::try_from(slot).expect("control-ring slot index is bounded"); + let direction_offset = match self { + Self::Requests => 0, + Self::Responses => CONTROL_RING_DIRECTION_SIZE, + }; + let start = direction_offset + slot * CONTROL_RING_SLOT_SIZE; + start..start + CONTROL_RING_SLOT_SIZE + } + + /// Returns the atomic `u32` epoch incremented when the producer publishes + /// work for this direction. + pub const fn producer_epoch_offset(self) -> usize { + self.sync_offset() + PRODUCER_EPOCH_OFFSET + } + + /// Returns the atomic `u32` epoch incremented when the consumer publishes + /// progress for this direction. + pub const fn consumer_epoch_offset(self) -> usize { + self.sync_offset() + CONSUMER_EPOCH_OFFSET + } + + /// Returns the atomic `u64` consumer-head offset for this direction. + pub const fn consumer_head_offset(self) -> usize { + self.sync_offset() + CONSUMER_HEAD_OFFSET + } + + const fn sync_offset(self) -> usize { + CONTROL_RING_DATA_SIZE + + match self { + Self::Requests => 0, + Self::Responses => CONTROL_RING_SYNC_DIRECTION_SIZE, + } + } +} + +/// Error validating or accessing shared control-ring state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ControlRingError { + /// The backing shared-memory length is not the exact control-ring size. + MemoryLengthMismatch { + /// Required mapping length. + expected: usize, + /// Actual mapping length. + actual: usize, + }, + /// An empty encoded envelope cannot be written to the ring. + EmptyPayload, + /// The encoded envelope does not fit in one ring slot. + PayloadTooLarge { + /// Supplied envelope length. + length: usize, + }, + /// A peer progress counter moved backward. + CounterRegressed { + /// Last accepted counter value. + previous: u64, + /// Newly received counter value. + received: u64, + }, + /// A peer progress counter violates the locally known ring window. + CounterOutOfRange { + /// Trusted local counter bounding the received value. + local: u64, + /// Newly received counter value. + received: u64, + }, + /// The non-wrapping absolute slot sequence is exhausted. + CounterExhausted, + /// A copied slot does not carry the expected absolute sequence. + UnexpectedSequence { + /// Sequence derived from trusted endpoint-local state. + expected: u64, + /// Sequence copied from the shared slot. + actual: u64, + }, + /// A copied slot has nonzero reserved metadata. + NonzeroReserved { + /// Reserved value copied from the shared slot. + value: u32, + }, + /// A copied slot has a zero or oversized payload length. + InvalidPayloadLength { + /// Length copied from the shared slot. + length: u32, + }, + /// The backing shared-memory access failed. + SharedMemory(SharedMemoryError), +} + +impl From for ControlRingError { + fn from(error: SharedMemoryError) -> Self { + Self::SharedMemory(error) + } +} + +/// Result of a nonblocking control-ring write. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ControlRingWriteStatus { + /// The payload and metadata were copied and the sequence was published. + Written, + /// The producer cannot reuse a slot until the peer acknowledges progress. + Full { + /// Consumer epoch sampled before the final full check. + wait_epoch: u32, + }, +} + +/// Result of a nonblocking control-ring read. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ControlRingReadStatus { + /// One complete slot was copied, validated, and decoded. + Message(Message), + /// The next slot still carries its previous sequence. + Empty { + /// Producer epoch sampled before the final empty check. + wait_epoch: u32, + }, +} + +/// Error copying, validating, or decoding one control-ring slot. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ControlRingReadError { + /// Ring metadata or shared-memory access is invalid. + Ring(ControlRingError), + /// The owned encoded envelope was rejected by its protocol decoder. + Decode(DecodeError), +} + +/// Exact-size shared memory containing request and response control rings. +pub struct ControlRing { + memory: Memory, +} + +impl ControlRing { + /// Attaches to an exact-size shared control-ring mapping. + pub fn new(memory: Memory) -> Result { + let actual = memory.len(); + if actual != CONTROL_RING_MEMORY_SIZE { + return Err(ControlRingError::MemoryLengthMismatch { + expected: CONTROL_RING_MEMORY_SIZE, + actual, + }); + } + Ok(Self { memory }) + } + + /// Returns the copy-only shared-memory resource. + pub const fn memory(&self) -> &Memory { + &self.memory + } + + /// Consumes the mapping into the local request producer and response + /// consumer. + pub fn into_local(self) -> (ControlRingProducer, ControlRingConsumer) { + self.into_endpoints( + ControlRingDirection::Requests, + ControlRingDirection::Responses, + ) + } + + /// Consumes the mapping into the broker response producer and request + /// consumer. + pub fn into_broker(self) -> (ControlRingProducer, ControlRingConsumer) { + self.into_endpoints( + ControlRingDirection::Responses, + ControlRingDirection::Requests, + ) + } + + fn into_endpoints( + self, + producer_direction: ControlRingDirection, + consumer_direction: ControlRingDirection, + ) -> (ControlRingProducer, ControlRingConsumer) { + let ring = Arc::new(self); + ( + ControlRingProducer::new(Arc::clone(&ring), producer_direction), + ControlRingConsumer::new(ring, consumer_direction), + ) + } + + fn write_slot( + &self, + direction: ControlRingDirection, + position: u64, + metadata: [u8; CONTROL_RING_SLOT_HEADER_SIZE - size_of::()], + payload: &[u8], + sequence: u64, + ) -> Result<(), ControlRingError> { + let slot = position % CONTROL_RING_SLOT_COUNT; + let range = direction.slot_range(slot); + self.memory + .write(range.start + CONTROL_RING_SLOT_HEADER_SIZE, payload)?; + self.memory + .write(range.start + size_of::(), &metadata)?; + self.memory.store_u64_and_fetch_add_u32_release( + range.start, + sequence.to_le(), + direction.producer_epoch_offset(), + 1, + )?; + Ok(()) + } + + fn load_sequence( + &self, + direction: ControlRingDirection, + position: u64, + ) -> Result { + let slot = position % CONTROL_RING_SLOT_COUNT; + let range = direction.slot_range(slot); + Ok(u64::from_le(self.memory.load_u64_acquire(range.start)?)) + } + + fn read_slot_body( + &self, + direction: ControlRingDirection, + position: u64, + body: &mut [u8], + ) -> Result<(), ControlRingError> { + let slot = position % CONTROL_RING_SLOT_COUNT; + let range = direction.slot_range(slot); + self.memory.read(range.start + size_of::(), body)?; + Ok(()) + } + + fn load_producer_epoch( + &self, + direction: ControlRingDirection, + ) -> Result { + Ok(self + .memory + .load_u32_acquire(direction.producer_epoch_offset())?) + } + + fn load_consumer_epoch( + &self, + direction: ControlRingDirection, + ) -> Result { + Ok(self + .memory + .load_u32_acquire(direction.consumer_epoch_offset())?) + } + + fn load_consumer_head(&self, direction: ControlRingDirection) -> Result { + Ok(u64::from_le( + self.memory + .load_u64_acquire(direction.consumer_head_offset())?, + )) + } +} + +/// Trusted endpoint-local state for one control-ring producer. +pub struct ControlRingProducer { + ring: Arc>, + direction: ControlRingDirection, + tail: u64, + acknowledged_head: u64, +} + +impl ControlRingProducer { + fn new(ring: Arc>, direction: ControlRingDirection) -> Self { + Self { + ring, + direction, + tail: 0, + acknowledged_head: 0, + } + } + + /// Returns the ring direction written by this producer. + pub const fn direction(&self) -> ControlRingDirection { + self.direction + } + + #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] + pub(crate) fn memory(&self) -> &Memory { + self.ring.memory() + } + + fn refresh_head(&mut self) -> Result<(), ControlRingError> { + let head = self.ring.load_consumer_head(self.direction)?; + if head < self.acknowledged_head { + return Err(ControlRingError::CounterRegressed { + previous: self.acknowledged_head, + received: head, + }); + } + if head > self.tail { + return Err(ControlRingError::CounterOutOfRange { + local: self.tail, + received: head, + }); + } + self.acknowledged_head = head; + Ok(()) + } + + /// Copies an encoded envelope and its header into the next available slot. + pub fn try_write( + &mut self, + payload: &[u8], + ) -> Result { + if payload.is_empty() { + return Err(ControlRingError::EmptyPayload); + } + if payload.len() > CONTROL_RING_PAYLOAD_CAPACITY { + return Err(ControlRingError::PayloadTooLarge { + length: payload.len(), + }); + } + if self.tail == u64::MAX { + return Err(ControlRingError::CounterExhausted); + } + if self.tail - self.acknowledged_head == CONTROL_RING_SLOT_COUNT { + let wait_epoch = self.ring.load_consumer_epoch(self.direction)?; + self.refresh_head()?; + if self.tail - self.acknowledged_head == CONTROL_RING_SLOT_COUNT { + return Ok(ControlRingWriteStatus::Full { wait_epoch }); + } + } + + let sequence = self.tail + 1; + let length = + u32::try_from(payload.len()).map_err(|_| ControlRingError::PayloadTooLarge { + length: payload.len(), + })?; + let mut metadata = [0; CONTROL_RING_SLOT_HEADER_SIZE - size_of::()]; + metadata[..size_of::()].copy_from_slice(&length.to_le_bytes()); + self.ring + .write_slot(self.direction, self.tail, metadata, payload, sequence)?; + self.tail = sequence; + Ok(ControlRingWriteStatus::Written) + } +} + +/// Trusted endpoint-local state for one control-ring consumer. +pub struct ControlRingConsumer { + ring: Arc>, + direction: ControlRingDirection, + head: u64, + published_head: u64, +} + +impl ControlRingConsumer { + fn new(ring: Arc>, direction: ControlRingDirection) -> Self { + Self { + ring, + direction, + head: 0, + published_head: 0, + } + } + + /// Returns the ring direction read by this consumer. + pub const fn direction(&self) -> ControlRingDirection { + self.direction + } + + #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] + pub(crate) fn memory(&self) -> &Memory { + self.ring.memory() + } + + /// Publishes newly consumed slots and advances the consumer wake epoch. + pub fn publish_head(&mut self) -> Result<(), ControlRingError> { + if self.head == self.published_head { + return Ok(()); + } + self.ring.memory.store_u64_and_fetch_add_u32_release( + self.direction.consumer_head_offset(), + self.head.to_le(), + self.direction.consumer_epoch_offset(), + 1, + )?; + self.published_head = self.head; + Ok(()) + } + + /// Polls, copies, validates, and decodes one peer-published slot. + /// + /// The decoder receives only an owned snapshot of the exact encoded + /// envelope. It must reject malformed message tags, phases, and trailing + /// bytes. The trusted head advances only after successful decoding. + pub fn try_read( + &mut self, + decode: impl FnOnce(&[u8]) -> Result, + ) -> Result, ControlRingReadError> { + let expected_sequence = self.head.checked_add(1).ok_or(ControlRingReadError::Ring( + ControlRingError::CounterExhausted, + ))?; + let wait_epoch = self + .ring + .load_producer_epoch(self.direction) + .map_err(ControlRingReadError::Ring)?; + let actual_sequence = self + .ring + .load_sequence(self.direction, self.head) + .map_err(ControlRingReadError::Ring)?; + let stale_sequence = expected_sequence.saturating_sub(CONTROL_RING_SLOT_COUNT); + if actual_sequence == stale_sequence { + return Ok(ControlRingReadStatus::Empty { wait_epoch }); + } + if actual_sequence != expected_sequence { + return Err(ControlRingReadError::Ring( + ControlRingError::UnexpectedSequence { + expected: expected_sequence, + actual: actual_sequence, + }, + )); + } + let mut image = [0; CONTROL_RING_SLOT_SIZE]; + image[..size_of::()].copy_from_slice(&actual_sequence.to_le_bytes()); + self.ring + .read_slot_body(self.direction, self.head, &mut image[size_of::()..]) + .map_err(ControlRingReadError::Ring)?; + let verified_sequence = self + .ring + .load_sequence(self.direction, self.head) + .map_err(ControlRingReadError::Ring)?; + if verified_sequence != expected_sequence { + return Err(ControlRingReadError::Ring( + ControlRingError::UnexpectedSequence { + expected: expected_sequence, + actual: verified_sequence, + }, + )); + } + let reserved = u32::from_le_bytes([image[12], image[13], image[14], image[15]]); + if reserved != 0 { + return Err(ControlRingReadError::Ring( + ControlRingError::NonzeroReserved { value: reserved }, + )); + } + let length = u32::from_le_bytes([image[8], image[9], image[10], image[11]]); + let length_usize = length as usize; + if length_usize == 0 || length_usize > CONTROL_RING_PAYLOAD_CAPACITY { + return Err(ControlRingReadError::Ring( + ControlRingError::InvalidPayloadLength { length }, + )); + } + let payload = + &image[CONTROL_RING_SLOT_HEADER_SIZE..CONTROL_RING_SLOT_HEADER_SIZE + length_usize]; + let message = decode(payload).map_err(ControlRingReadError::Decode)?; + self.head = expected_sequence; + Ok(ControlRingReadStatus::Message(message)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::mem::align_of; + use litebox_broker_protocol::RequestId; + use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerOperation, BrokerRequest, + }; + use litebox_broker_protocol::wire::{ + WireError, decode_request, encode_handshake_request, encode_request, + }; + use litebox_broker_protocol::{ObjectHandle, ProtocolVersion}; + use std::sync::Mutex; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::vec; + use std::vec::Vec; + + const SEQUENCE_RANGE: core::ops::Range = 0..8; + const LENGTH_RANGE: core::ops::Range = 8..12; + const RESERVED_RANGE: core::ops::Range = 12..16; + + #[test] + fn mapping_requires_the_exact_control_ring_size() { + assert!(matches!( + ControlRing::new(TestMemory::new(CONTROL_RING_MEMORY_SIZE - 1)), + Err(ControlRingError::MemoryLengthMismatch { + expected: CONTROL_RING_MEMORY_SIZE, + actual + }) if actual == CONTROL_RING_MEMORY_SIZE - 1 + )); + assert!(matches!( + ControlRing::new(TestMemory::new(CONTROL_RING_MEMORY_SIZE + 1)), + Err(ControlRingError::MemoryLengthMismatch { + expected: CONTROL_RING_MEMORY_SIZE, + actual + }) if actual == CONTROL_RING_MEMORY_SIZE + 1 + )); + assert!(ControlRing::new(TestMemory::new(CONTROL_RING_MEMORY_SIZE)).is_ok()); + } + + #[test] + fn endpoint_roles_bind_opposite_directions_to_one_mapping() { + let (local_producer, local_consumer) = test_ring().into_local(); + assert_eq!(local_producer.direction(), ControlRingDirection::Requests); + assert_eq!(local_consumer.direction(), ControlRingDirection::Responses); + assert!(Arc::ptr_eq(&local_producer.ring, &local_consumer.ring)); + + let (broker_producer, broker_consumer) = test_ring().into_broker(); + assert_eq!(broker_producer.direction(), ControlRingDirection::Responses); + assert_eq!(broker_consumer.direction(), ControlRingDirection::Requests); + assert!(Arc::ptr_eq(&broker_producer.ring, &broker_consumer.ring)); + } + + #[test] + fn request_and_response_rings_publish_independently() { + let ring = Arc::new(test_ring()); + let mut request_producer = + ControlRingProducer::new(Arc::clone(&ring), ControlRingDirection::Requests); + let mut request_consumer = + ControlRingConsumer::new(Arc::clone(&ring), ControlRingDirection::Requests); + let mut response_producer = + ControlRingProducer::new(Arc::clone(&ring), ControlRingDirection::Responses); + let mut response_consumer = ControlRingConsumer::new(ring, ControlRingDirection::Responses); + + assert_eq!( + request_producer.try_write(&[1, 2, 3]), + Ok(ControlRingWriteStatus::Written) + ); + assert_eq!( + response_producer.try_write(&[4, 5]), + Ok(ControlRingWriteStatus::Written) + ); + + assert_eq!( + request_consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![1, 2, 3])) + ); + assert_eq!( + response_consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![4, 5])) + ); + assert_eq!( + request_consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { wait_epoch: 1 }) + ); + assert_eq!( + response_consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { wait_epoch: 1 }) + ); + } + + #[test] + fn payload_length_bounds_are_enforced() { + let (mut producer, mut consumer) = test_endpoints(); + + assert_eq!(producer.try_write(&[]), Err(ControlRingError::EmptyPayload)); + assert_eq!( + producer.try_write(&[0; CONTROL_RING_PAYLOAD_CAPACITY + 1]), + Err(ControlRingError::PayloadTooLarge { + length: CONTROL_RING_PAYLOAD_CAPACITY + 1 + }) + ); + assert_eq!( + producer.try_write(&[7; CONTROL_RING_PAYLOAD_CAPACITY]), + Ok(ControlRingWriteStatus::Written) + ); + + assert_eq!( + consumer.try_read(|payload| Ok::<_, ()>(payload.len())), + Ok(ControlRingReadStatus::Message( + CONTROL_RING_PAYLOAD_CAPACITY + )) + ); + } + + #[test] + fn producer_publishes_sequence_after_payload_and_metadata() { + let (mut producer, _) = test_endpoints(); + + assert_eq!( + producer.try_write(&[1, 2, 3]), + Ok(ControlRingWriteStatus::Written) + ); + assert_eq!( + producer.memory().write_log(), + vec![ + (CONTROL_RING_SLOT_HEADER_SIZE, 3), + ( + size_of::(), + CONTROL_RING_SLOT_HEADER_SIZE - size_of::() + ), + (0, size_of::()), + ( + ControlRingDirection::Requests.producer_epoch_offset(), + size_of::() + ), + ] + ); + } + + #[test] + fn failed_payload_metadata_or_atomic_publication_does_not_publish_progress() { + for failed_write in [1, 2, 3] { + let ring = ControlRing::new(FailingWriteMemory::new()).unwrap(); + let (mut producer, mut consumer) = ring.into_endpoints( + ControlRingDirection::Requests, + ControlRingDirection::Requests, + ); + producer.try_write(&[7]).unwrap(); + producer.memory().fail_after(failed_write); + assert_eq!( + producer.try_write(&[1, 2, 3]), + Err(ControlRingError::SharedMemory( + SharedMemoryError::InvalidRange + )) + ); + assert_eq!(producer.tail, 1); + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![7])) + ); + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { wait_epoch: 1 }) + ); + + assert_eq!( + producer.try_write(&[1, 2, 3]), + Ok(ControlRingWriteStatus::Written) + ); + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![1, 2, 3])) + ); + } + } + + #[test] + fn shorter_reused_payload_does_not_expose_stale_trailing_bytes() { + let (mut producer, mut consumer) = test_endpoints(); + + producer.try_write(&[7; 100]).unwrap(); + for _ in 1..CONTROL_RING_SLOT_COUNT { + producer.try_write(&[8]).unwrap(); + } + assert_eq!( + consumer.try_read(|payload| Ok::<_, ()>(payload.len())), + Ok(ControlRingReadStatus::Message(100)) + ); + for _ in 1..CONTROL_RING_SLOT_COUNT { + consumer.try_read(owned_bytes).unwrap(); + } + consumer.publish_head().unwrap(); + + producer.try_write(&[9]).unwrap(); + assert_eq!( + &producer.memory().bytes() + [CONTROL_RING_SLOT_HEADER_SIZE + 1..CONTROL_RING_SLOT_HEADER_SIZE + 100], + &[7; 99] + ); + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![9])) + ); + } + + #[test] + fn shared_epochs_publish_and_acknowledge_a_full_batch() { + let (mut producer, mut consumer) = test_endpoints(); + + for value in 0..CONTROL_RING_SLOT_COUNT { + let value = u8::try_from(value).unwrap(); + assert_eq!( + producer.try_write(&[value]), + Ok(ControlRingWriteStatus::Written) + ); + } + assert_eq!( + producer.try_write(&[0xff]), + Ok(ControlRingWriteStatus::Full { wait_epoch: 0 }) + ); + + for value in 0..CONTROL_RING_SLOT_COUNT { + let value = u8::try_from(value).unwrap(); + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![value])) + ); + } + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { + wait_epoch: u32::try_from(CONTROL_RING_SLOT_COUNT).unwrap() + }) + ); + assert_eq!( + producer.try_write(&[0xff]), + Ok(ControlRingWriteStatus::Full { wait_epoch: 0 }) + ); + + consumer.publish_head().unwrap(); + assert_eq!( + producer.try_write(&[0xff]), + Ok(ControlRingWriteStatus::Written) + ); + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![0xff])) + ); + } + + #[test] + fn wakeup_epochs_wrap_without_controlling_ring_progress() { + let (mut producer, mut consumer) = test_endpoints(); + + producer + .memory() + .fetch_add_u32_release( + ControlRingDirection::Requests.producer_epoch_offset(), + u32::MAX, + ) + .unwrap(); + producer.try_write(&[7]).unwrap(); + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![7])) + ); + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { wait_epoch: 0 }) + ); + + consumer + .memory() + .fetch_add_u32_release( + ControlRingDirection::Requests.consumer_epoch_offset(), + u32::MAX, + ) + .unwrap(); + consumer.publish_head().unwrap(); + assert_eq!( + producer + .memory() + .load_u32_acquire(ControlRingDirection::Requests.consumer_epoch_offset()), + Ok(0) + ); + assert_eq!(producer.refresh_head(), Ok(())); + } + + #[test] + fn unchanged_consumer_head_does_not_advance_the_epoch() { + let (mut producer, mut consumer) = test_endpoints(); + consumer.publish_head().unwrap(); + assert_eq!( + consumer + .memory() + .load_u32_acquire(ControlRingDirection::Requests.consumer_epoch_offset()), + Ok(0) + ); + + producer.try_write(&[7]).unwrap(); + consumer.try_read(owned_bytes).unwrap(); + consumer.publish_head().unwrap(); + consumer.publish_head().unwrap(); + assert_eq!( + consumer + .memory() + .load_u32_acquire(ControlRingDirection::Requests.consumer_epoch_offset()), + Ok(1) + ); + } + + #[test] + fn producer_rejects_regressed_or_future_shared_heads() { + let (mut producer, _) = test_endpoints(); + producer.try_write(&[1]).unwrap(); + producer.try_write(&[2]).unwrap(); + + store_test_consumer_head(producer.memory(), 1); + assert_eq!(producer.refresh_head(), Ok(())); + assert_eq!(producer.refresh_head(), Ok(())); + store_test_consumer_head(producer.memory(), 0); + assert_eq!( + producer.refresh_head(), + Err(ControlRingError::CounterRegressed { + previous: 1, + received: 0 + }) + ); + store_test_consumer_head(producer.memory(), 3); + assert_eq!( + producer.refresh_head(), + Err(ControlRingError::CounterOutOfRange { + local: 2, + received: 3 + }) + ); + } + + #[test] + fn consumer_rejects_hostile_slot_metadata() { + fn assert_rejected(image: &[u8; CONTROL_RING_SLOT_SIZE], expected: ControlRingError) { + let ring = test_ring(); + install_slot(ring.memory(), image); + let (_, mut consumer) = ring.into_endpoints( + ControlRingDirection::Responses, + ControlRingDirection::Requests, + ); + assert_eq!( + consumer.try_read(owned_bytes), + Err(ControlRingReadError::Ring(expected)) + ); + assert_eq!(consumer.head, 0); + } + + assert_rejected( + &raw_slot(2, 1, 0, &[1]), + ControlRingError::UnexpectedSequence { + expected: 1, + actual: 2, + }, + ); + assert_rejected( + &raw_slot(1, 1, 7, &[1]), + ControlRingError::NonzeroReserved { value: 7 }, + ); + assert_rejected( + &raw_slot(1, 0, 0, &[]), + ControlRingError::InvalidPayloadLength { length: 0 }, + ); + let oversized = u32::try_from(CONTROL_RING_PAYLOAD_CAPACITY + 1).unwrap(); + assert_rejected( + &raw_slot(1, oversized, 0, &[]), + ControlRingError::InvalidPayloadLength { length: oversized }, + ); + } + + #[test] + fn consumer_treats_the_previous_slot_sequence_as_empty() { + let (_, mut consumer) = test_endpoints(); + + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { wait_epoch: 0 }) + ); + install_slot(consumer.memory(), &raw_slot(1, 1, 0, &[7])); + consumer.head = CONTROL_RING_SLOT_COUNT; + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { wait_epoch: 0 }) + ); + } + + #[test] + fn active_decoder_rejects_truncated_trailing_and_wrong_phase_slots() { + let request = BrokerRequest { + request_id: RequestId(13), + operation: BrokerOperation::CloseObject(ObjectHandle(17)), + }; + let encoded = encode_request(request.clone()); + let frames = [ + ( + encoded[..encoded.len() - 1].to_vec(), + WireError::TruncatedFrame, + ), + ( + { + let mut trailing = encoded.clone(); + trailing.push(0xff); + trailing + }, + WireError::TrailingBytes, + ), + ( + encode_handshake_request(BrokerHandshakeRequest { + protocol_version: ProtocolVersion(1), + }), + WireError::WrongMessagePhase, + ), + ]; + + for (frame, expected) in frames { + let (mut producer, mut consumer) = test_endpoints(); + producer.try_write(&frame).unwrap(); + assert_eq!( + consumer.try_read(decode_request), + Err(ControlRingReadError::Decode(expected)) + ); + assert_eq!(consumer.head, 0); + } + + let (mut producer, mut consumer) = test_endpoints(); + producer.try_write(&encoded).unwrap(); + assert_eq!( + consumer.try_read(decode_request), + Ok(ControlRingReadStatus::Message(request)) + ); + } + + #[test] + fn decoder_observes_only_the_owned_slot_snapshot() { + let (mut producer, mut consumer) = test_endpoints(); + producer.try_write(&[1, 2, 3]).unwrap(); + let memory = producer.memory(); + + assert_eq!( + consumer.try_read(|payload| { + memory + .write(CONTROL_RING_SLOT_HEADER_SIZE, &[9, 9, 9]) + .unwrap(); + Ok::<_, ()>(payload.to_vec()) + }), + Ok(ControlRingReadStatus::Message(vec![1, 2, 3])) + ); + assert_eq!( + &memory.bytes()[CONTROL_RING_SLOT_HEADER_SIZE..CONTROL_RING_SLOT_HEADER_SIZE + 3], + &[9, 9, 9] + ); + } + + #[test] + fn torn_length_snapshot_is_rejected_before_payload_slicing() { + let memory = TearingMemory::with_torn_length(&raw_slot(1, 1, 0, &[7])); + let ring = ControlRing::new(memory).unwrap(); + let (_, mut consumer) = ring.into_endpoints( + ControlRingDirection::Responses, + ControlRingDirection::Requests, + ); + + assert_eq!( + consumer.try_read(owned_bytes), + Err(ControlRingReadError::Ring( + ControlRingError::InvalidPayloadLength { + length: 0xffff_0001 + } + )) + ); + assert_eq!(consumer.head, 0); + } + + #[test] + fn sequence_change_during_body_copy_is_rejected() { + let memory = TearingMemory::with_changed_sequence(&raw_slot(1, 1, 0, &[7])); + let ring = ControlRing::new(memory).unwrap(); + let (_, mut consumer) = ring.into_endpoints( + ControlRingDirection::Responses, + ControlRingDirection::Requests, + ); + + assert_eq!( + consumer.try_read(owned_bytes), + Err(ControlRingReadError::Ring( + ControlRingError::UnexpectedSequence { + expected: 1, + actual: 2, + } + )) + ); + assert_eq!(consumer.head, 0); + } + + #[test] + fn terminal_sequence_is_used_once_without_wrapping() { + let (mut producer, mut consumer) = test_endpoints(); + producer.tail = u64::MAX - 1; + producer.acknowledged_head = u64::MAX - 1; + + assert_eq!( + producer.try_write(&[7]), + Ok(ControlRingWriteStatus::Written) + ); + let writes = producer.memory().write_count.load(Ordering::Relaxed); + assert_eq!( + producer.try_write(&[8]), + Err(ControlRingError::CounterExhausted) + ); + assert_eq!( + producer.memory().write_count.load(Ordering::Relaxed), + writes + ); + + consumer.head = u64::MAX - 1; + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![7])) + ); + assert_eq!(consumer.head, u64::MAX); + assert_eq!( + consumer.try_read(owned_bytes), + Err(ControlRingReadError::Ring( + ControlRingError::CounterExhausted + )) + ); + } + + fn test_ring() -> ControlRing { + ControlRing::new(TestMemory::new(CONTROL_RING_MEMORY_SIZE)).unwrap() + } + + fn test_endpoints() -> ( + ControlRingProducer, + ControlRingConsumer, + ) { + test_ring().into_endpoints( + ControlRingDirection::Requests, + ControlRingDirection::Requests, + ) + } + + fn owned_bytes(payload: &[u8]) -> Result, ()> { + if payload.is_empty() { + Err(()) + } else { + Ok(payload.to_vec()) + } + } + + fn raw_slot( + sequence: u64, + length: u32, + reserved: u32, + payload: &[u8], + ) -> [u8; CONTROL_RING_SLOT_SIZE] { + let mut image = [0; CONTROL_RING_SLOT_SIZE]; + image[SEQUENCE_RANGE].copy_from_slice(&sequence.to_le_bytes()); + image[LENGTH_RANGE].copy_from_slice(&length.to_le_bytes()); + image[RESERVED_RANGE].copy_from_slice(&reserved.to_le_bytes()); + image[CONTROL_RING_SLOT_HEADER_SIZE..CONTROL_RING_SLOT_HEADER_SIZE + payload.len()] + .copy_from_slice(payload); + image + } + + fn install_slot(memory: &TestMemory, image: &[u8; CONTROL_RING_SLOT_SIZE]) { + memory + .write(size_of::(), &image[size_of::()..]) + .unwrap(); + let sequence = u64::from_le_bytes(image[..size_of::()].try_into().unwrap()); + memory.store_u64_release(0, sequence.to_le()).unwrap(); + } + + fn store_test_consumer_head(memory: &TestMemory, head: u64) { + memory + .store_u64_release( + ControlRingDirection::Requests.consumer_head_offset(), + head.to_le(), + ) + .unwrap(); + } + + struct TestMemory { + bytes: Mutex>, + write_log: Mutex>, + write_count: AtomicUsize, + } + + impl TestMemory { + fn new(length: usize) -> Self { + Self { + bytes: Mutex::new(vec![0; length]), + write_log: Mutex::new(Vec::new()), + write_count: AtomicUsize::new(0), + } + } + + fn bytes(&self) -> Vec { + self.bytes.lock().unwrap().clone() + } + + fn write_log(&self) -> Vec<(usize, usize)> { + self.write_log.lock().unwrap().clone() + } + } + + impl SharedMemory for TestMemory { + fn len(&self) -> usize { + self.bytes.lock().unwrap().len() + } + + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + let bytes = self.bytes.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice( + bytes + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?, + ); + Ok(()) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let mut bytes = self.bytes.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + bytes + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)? + .copy_from_slice(source); + self.write_log.lock().unwrap().push((offset, source.len())); + self.write_count.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + } + + impl AtomicSharedMemory for TestMemory { + fn load_u32_acquire(&self, offset: usize) -> Result { + test_load_u32_acquire(&self.bytes, offset) + } + + fn fetch_add_u32_release( + &self, + offset: usize, + value: u32, + ) -> Result { + let previous = test_fetch_add_u32_release(&self.bytes, offset, value)?; + self.write_log + .lock() + .unwrap() + .push((offset, size_of::())); + self.write_count.fetch_add(1, Ordering::Relaxed); + Ok(previous) + } + + fn load_u64_acquire(&self, offset: usize) -> Result { + test_load_u64_acquire(&self.bytes, offset) + } + + fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError> { + test_store_u64_release(&self.bytes, offset, value)?; + self.write_log + .lock() + .unwrap() + .push((offset, size_of::())); + self.write_count.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + fn store_u64_and_fetch_add_u32_release( + &self, + store_offset: usize, + value: u64, + add_offset: usize, + add_value: u32, + ) -> Result { + let previous = test_store_u64_and_fetch_add_u32_release( + &self.bytes, + store_offset, + value, + add_offset, + add_value, + )?; + self.write_log.lock().unwrap().extend([ + (store_offset, size_of::()), + (add_offset, size_of::()), + ]); + self.write_count.fetch_add(2, Ordering::Relaxed); + Ok(previous) + } + } + + struct FailingWriteMemory { + bytes: Mutex>, + write_count: AtomicUsize, + fail_on_write: AtomicUsize, + } + + impl FailingWriteMemory { + fn new() -> Self { + Self { + bytes: Mutex::new(vec![0; CONTROL_RING_MEMORY_SIZE]), + write_count: AtomicUsize::new(0), + fail_on_write: AtomicUsize::new(usize::MAX), + } + } + + fn fail_after(&self, writes: usize) { + let current = self.write_count.load(Ordering::Relaxed); + self.fail_on_write + .store(current + writes, Ordering::Relaxed); + } + } + + impl SharedMemory for FailingWriteMemory { + fn len(&self) -> usize { + self.bytes.lock().unwrap().len() + } + + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + let bytes = self.bytes.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice( + bytes + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?, + ); + Ok(()) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let call = self.write_count.fetch_add(1, Ordering::Relaxed) + 1; + let mut bytes = self.bytes.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let destination = bytes + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + if call == self.fail_on_write.load(Ordering::Relaxed) { + let partial = source.len().div_ceil(2); + destination[..partial].copy_from_slice(&source[..partial]); + return Err(SharedMemoryError::InvalidRange); + } + destination.copy_from_slice(source); + Ok(()) + } + } + + impl AtomicSharedMemory for FailingWriteMemory { + fn load_u32_acquire(&self, offset: usize) -> Result { + test_load_u32_acquire(&self.bytes, offset) + } + + fn fetch_add_u32_release( + &self, + offset: usize, + value: u32, + ) -> Result { + test_fetch_add_u32_release(&self.bytes, offset, value) + } + + fn load_u64_acquire(&self, offset: usize) -> Result { + test_load_u64_acquire(&self.bytes, offset) + } + + fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError> { + if !offset.is_multiple_of(align_of::()) { + return Err(SharedMemoryError::UnalignedAtomic); + } + let call = self.write_count.fetch_add(1, Ordering::Relaxed) + 1; + if call == self.fail_on_write.load(Ordering::Relaxed) { + return Err(SharedMemoryError::InvalidRange); + } + test_store_u64_release(&self.bytes, offset, value) + } + + fn store_u64_and_fetch_add_u32_release( + &self, + store_offset: usize, + value: u64, + add_offset: usize, + add_value: u32, + ) -> Result { + let call = self.write_count.fetch_add(1, Ordering::Relaxed) + 1; + if call == self.fail_on_write.load(Ordering::Relaxed) { + return Err(SharedMemoryError::InvalidRange); + } + test_store_u64_and_fetch_add_u32_release( + &self.bytes, + store_offset, + value, + add_offset, + add_value, + ) + } + } + + enum Tear { + Length, + Sequence, + } + + struct TearingMemory { + bytes: Mutex>, + tear: Tear, + } + + impl TearingMemory { + fn with_torn_length(first_slot: &[u8; CONTROL_RING_SLOT_SIZE]) -> Self { + Self::new(first_slot, Tear::Length) + } + + fn with_changed_sequence(first_slot: &[u8; CONTROL_RING_SLOT_SIZE]) -> Self { + Self::new(first_slot, Tear::Sequence) + } + + fn new(first_slot: &[u8; CONTROL_RING_SLOT_SIZE], tear: Tear) -> Self { + let mut bytes = vec![0; CONTROL_RING_MEMORY_SIZE]; + bytes[..CONTROL_RING_SLOT_SIZE].copy_from_slice(first_slot); + Self { + bytes: Mutex::new(bytes), + tear, + } + } + } + + impl SharedMemory for TearingMemory { + fn len(&self) -> usize { + self.bytes.lock().unwrap().len() + } + + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + let mut bytes = self.bytes.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + bytes + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + + match self.tear { + Tear::Length => { + destination[..2].copy_from_slice(&bytes[offset..offset + 2]); + bytes[offset + 2..offset + 4].fill(0xff); + destination[2..].copy_from_slice(&bytes[offset + 2..end]); + } + Tear::Sequence => { + destination.copy_from_slice(&bytes[offset..end]); + bytes[..size_of::()].copy_from_slice(&2_u64.to_le_bytes()); + } + } + Ok(()) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let mut bytes = self.bytes.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + bytes + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)? + .copy_from_slice(source); + Ok(()) + } + } + + impl AtomicSharedMemory for TearingMemory { + fn load_u32_acquire(&self, offset: usize) -> Result { + test_load_u32_acquire(&self.bytes, offset) + } + + fn fetch_add_u32_release( + &self, + offset: usize, + value: u32, + ) -> Result { + test_fetch_add_u32_release(&self.bytes, offset, value) + } + + fn load_u64_acquire(&self, offset: usize) -> Result { + test_load_u64_acquire(&self.bytes, offset) + } + + fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError> { + test_store_u64_release(&self.bytes, offset, value) + } + + fn store_u64_and_fetch_add_u32_release( + &self, + store_offset: usize, + value: u64, + add_offset: usize, + add_value: u32, + ) -> Result { + test_store_u64_and_fetch_add_u32_release( + &self.bytes, + store_offset, + value, + add_offset, + add_value, + ) + } + } + + fn test_load_u32_acquire( + bytes: &Mutex>, + offset: usize, + ) -> Result { + if !offset.is_multiple_of(align_of::()) { + return Err(SharedMemoryError::UnalignedAtomic); + } + let bytes = bytes.lock().unwrap(); + let end = offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + let value = u32::from_ne_bytes( + bytes + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)? + .try_into() + .unwrap(), + ); + fence(Ordering::Acquire); + Ok(value) + } + + fn test_fetch_add_u32_release( + bytes: &Mutex>, + offset: usize, + value: u32, + ) -> Result { + if !offset.is_multiple_of(align_of::()) { + return Err(SharedMemoryError::UnalignedAtomic); + } + fence(Ordering::Release); + let mut bytes = bytes.lock().unwrap(); + let end = offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + let destination = bytes + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + let previous = u32::from_ne_bytes(destination.try_into().unwrap()); + destination.copy_from_slice(&previous.wrapping_add(value).to_ne_bytes()); + Ok(previous) + } + + fn test_load_u64_acquire( + bytes: &Mutex>, + offset: usize, + ) -> Result { + if !offset.is_multiple_of(align_of::()) { + return Err(SharedMemoryError::UnalignedAtomic); + } + let bytes = bytes.lock().unwrap(); + let end = offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + let value = u64::from_ne_bytes( + bytes + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)? + .try_into() + .unwrap(), + ); + fence(Ordering::Acquire); + Ok(value) + } + + fn test_store_u64_release( + bytes: &Mutex>, + offset: usize, + value: u64, + ) -> Result<(), SharedMemoryError> { + if !offset.is_multiple_of(align_of::()) { + return Err(SharedMemoryError::UnalignedAtomic); + } + fence(Ordering::Release); + let mut bytes = bytes.lock().unwrap(); + let end = offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + bytes + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)? + .copy_from_slice(&value.to_ne_bytes()); + Ok(()) + } + + fn test_store_u64_and_fetch_add_u32_release( + bytes: &Mutex>, + store_offset: usize, + value: u64, + add_offset: usize, + add_value: u32, + ) -> Result { + if !store_offset.is_multiple_of(align_of::()) + || !add_offset.is_multiple_of(align_of::()) + { + return Err(SharedMemoryError::UnalignedAtomic); + } + let store_end = store_offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + let add_end = add_offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + if store_offset < add_end && add_offset < store_end { + return Err(SharedMemoryError::InvalidRange); + } + let mut bytes = bytes.lock().unwrap(); + if store_end > bytes.len() || add_end > bytes.len() { + return Err(SharedMemoryError::InvalidRange); + } + let previous = u32::from_ne_bytes( + bytes[add_offset..add_end] + .try_into() + .expect("checked u32 range"), + ); + fence(Ordering::Release); + bytes[store_offset..store_end].copy_from_slice(&value.to_ne_bytes()); + bytes[add_offset..add_end].copy_from_slice(&previous.wrapping_add(add_value).to_ne_bytes()); + Ok(previous) + } +} diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs index 8d2f7a045c..b33c8ed3ac 100644 --- a/litebox_broker_transport/src/lib.rs +++ b/litebox_broker_transport/src/lib.rs @@ -9,6 +9,13 @@ //! protocol messages, local-side adapters, host-side request handling, and core //! authority state live in separate crates. +extern crate alloc; + +#[cfg(test)] +extern crate std; + +pub mod control_ring; + #[cfg(all(feature = "linux-userland", target_os = "linux"))] pub mod shared_memory; diff --git a/litebox_broker_transport/src/shared_memory.rs b/litebox_broker_transport/src/shared_memory.rs index b81de5eb72..d6f357835f 100644 --- a/litebox_broker_transport/src/shared_memory.rs +++ b/litebox_broker_transport/src/shared_memory.rs @@ -5,6 +5,7 @@ use std::io::{Error, Result as IoResult}; use std::io::{ErrorKind, IoSlice, IoSliceMut}; +use std::mem::{align_of, size_of}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; use std::os::unix::net::UnixStream; use std::ptr::NonNull; @@ -19,9 +20,13 @@ use rustix::net::{ RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags, SendAncillaryBuffer, SendAncillaryMessage, SendFlags, }; +use rustix::thread::futex; -use litebox_broker_protocol::shared_memory::{SharedMemory, SharedMemoryError}; +use litebox_broker_protocol::shared_memory::{AtomicSharedMemory, SharedMemory, SharedMemoryError}; +use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; + +use crate::control_ring::{ControlRingConsumer, ControlRingProducer}; use crate::unix_io::{ refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, }; @@ -45,6 +50,85 @@ struct MappedRegion { // serialized by the enclosing `Mutex`. unsafe impl Send for MappedRegion {} +fn atomic_u64_at( + memory: &MemfdSharedMemory, + offset: usize, +) -> Result<&AtomicU64, SharedMemoryError> { + let address = { + let mapping = memory + .mapping + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let byte_address = + shared_address(&mapping, offset, size_of::(), align_of::())?; + // The runtime check above establishes the stronger alignment. + #[allow(clippy::cast_ptr_alignment)] + let address = byte_address.cast::(); + address + }; + // SAFETY: The pointer is valid and aligned for a `u64`. Control-ring + // sequence words are accessed atomically by conforming endpoints, and + // `memory` keeps the immutable mapping alive for the returned reference. + Ok(unsafe { AtomicU64::from_ptr(address) }) +} + +fn atomic_u32_at( + memory: &MemfdSharedMemory, + offset: usize, +) -> Result<&AtomicU32, SharedMemoryError> { + let address = { + let mapping = memory + .mapping + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let byte_address = + shared_address(&mapping, offset, size_of::(), align_of::())?; + // The runtime check above establishes the stronger alignment. + #[allow(clippy::cast_ptr_alignment)] + let address = byte_address.cast::(); + address + }; + // SAFETY: The pointer is valid and aligned for a `u32`. Control-ring epoch + // words are accessed atomically by conforming endpoints and the futex + // syscall, and `memory` keeps the immutable mapping alive for the returned + // reference. + Ok(unsafe { AtomicU32::from_ptr(address) }) +} + +fn shared_address( + mapping: &MappedRegion, + offset: usize, + size: usize, + alignment: usize, +) -> Result<*mut u8, SharedMemoryError> { + offset + .checked_add(size) + .filter(|end| *end <= mapping.length) + .ok_or(SharedMemoryError::InvalidRange)?; + // SAFETY: The checked offset is inside the live mapping. + let byte_address = unsafe { mapping.address.as_ptr().add(offset) }; + if !byte_address.addr().is_multiple_of(alignment) { + return Err(SharedMemoryError::UnalignedAtomic); + } + Ok(byte_address) +} + +fn validate_nonoverlapping_atomic_ranges( + store_offset: usize, + add_offset: usize, +) -> Result<(), SharedMemoryError> { + let store_end = store_offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + let add_end = add_offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + if store_offset < add_end && add_offset < store_end { + return Err(SharedMemoryError::InvalidRange); + } + Ok(()) +} + impl MemfdSharedMemory { /// Creates and maps a sealed memfd with `length` bytes. pub fn create(length: usize) -> IoResult { @@ -118,6 +202,61 @@ impl MemfdSharedMemory { mapping: Mutex::new(MappedRegion { address, length }), }) } + + /// Waits while a shared atomic `u32` still equals `expected`. + /// + /// A value change or signal interruption is reported as a successful, + /// possibly spurious wakeup. The caller must recheck its wait condition. + fn futex_wait_u32(&self, offset: usize, expected: u32) -> IoResult<()> { + let word = atomic_u32_at(self, offset) + .map_err(|error| Error::new(ErrorKind::InvalidInput, error))?; + match futex::wait(word, futex::Flags::empty(), expected, None) { + Ok(()) | Err(Errno::AGAIN | Errno::INTR) => Ok(()), + Err(error) => Err(error.into()), + } + } + + /// Wakes one waiter blocked on a shared atomic `u32`. + fn futex_wake_u32(&self, offset: usize) -> IoResult<()> { + let word = atomic_u32_at(self, offset) + .map_err(|error| Error::new(ErrorKind::InvalidInput, error))?; + futex::wake(word, futex::Flags::empty(), 1)?; + Ok(()) + } +} + +impl ControlRingProducer { + /// Waits for consumer progress after + /// [`Full`](crate::control_ring::ControlRingWriteStatus::Full). + /// + /// The caller must retry the write after this possibly spurious wakeup. + pub fn wait_for_capacity(&self, wait_epoch: u32) -> IoResult<()> { + self.memory() + .futex_wait_u32(self.direction().consumer_epoch_offset(), wait_epoch) + } + + /// Wakes the consumer after publishing one or more messages. + pub fn wake_consumer(&self) -> IoResult<()> { + self.memory() + .futex_wake_u32(self.direction().producer_epoch_offset()) + } +} + +impl ControlRingConsumer { + /// Waits for producer progress after + /// [`Empty`](crate::control_ring::ControlRingReadStatus::Empty). + /// + /// The caller must retry the read after this possibly spurious wakeup. + pub fn wait_for_message(&self, wait_epoch: u32) -> IoResult<()> { + self.memory() + .futex_wait_u32(self.direction().producer_epoch_offset(), wait_epoch) + } + + /// Wakes the producer after publishing newly consumed slots. + pub fn wake_producer(&self) -> IoResult<()> { + self.memory() + .futex_wake_u32(self.direction().consumer_epoch_offset()) + } } impl AsFd for MemfdSharedMemory { @@ -179,6 +318,39 @@ impl SharedMemory for MemfdSharedMemory { } } +impl AtomicSharedMemory for MemfdSharedMemory { + fn load_u32_acquire(&self, offset: usize) -> Result { + Ok(atomic_u32_at(self, offset)?.load(Ordering::Acquire)) + } + + fn fetch_add_u32_release(&self, offset: usize, value: u32) -> Result { + Ok(atomic_u32_at(self, offset)?.fetch_add(value, Ordering::Release)) + } + + fn load_u64_acquire(&self, offset: usize) -> Result { + Ok(atomic_u64_at(self, offset)?.load(Ordering::Acquire)) + } + + fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError> { + atomic_u64_at(self, offset)?.store(value, Ordering::Release); + Ok(()) + } + + fn store_u64_and_fetch_add_u32_release( + &self, + store_offset: usize, + value: u64, + add_offset: usize, + add_value: u32, + ) -> Result { + validate_nonoverlapping_atomic_ranges(store_offset, add_offset)?; + let stored = atomic_u64_at(self, store_offset)?; + let added = atomic_u32_at(self, add_offset)?; + stored.store(value, Ordering::Release); + Ok(added.fetch_add(add_value, Ordering::Release)) + } +} + /// Sends one memfd-backed shared-memory resource over an exclusively owned /// connected Unix stream. /// @@ -300,11 +472,17 @@ fn invalid_data(message: &'static str) -> Error { #[cfg(test)] mod tests { use super::*; + use crate::control_ring::{ + CONTROL_RING_MEMORY_SIZE, CONTROL_RING_SLOT_COUNT, ControlRing, ControlRingReadStatus, + ControlRingWriteStatus, + }; use litebox_broker_protocol::shared_memory::{ SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, SharedBufferSlotIndex, }; use rustix::io::FdFlags; use std::io::Write; + use std::sync::{Arc, Barrier}; + use std::thread; use std::time::Duration; #[test] @@ -329,6 +507,63 @@ mod tests { ); } + #[test] + fn mappings_share_ordered_atomic_values_and_validate_alignment() { + let first = MemfdSharedMemory::create(64).unwrap(); + let second = + MemfdSharedMemory::from_received_fd(first.as_fd().try_clone_to_owned().unwrap(), 64) + .unwrap(); + + first.store_u64_release(8, 0x0102_0304_0506_0708).unwrap(); + assert_eq!(second.load_u64_acquire(8), Ok(0x0102_0304_0506_0708)); + assert_eq!(first.fetch_add_u32_release(4, 3), Ok(0)); + assert_eq!(second.load_u32_acquire(4), Ok(3)); + assert_eq!( + first.store_u64_and_fetch_add_u32_release(8, 0x1112_1314_1516_1718, 4, 2), + Ok(3) + ); + assert_eq!(second.load_u64_acquire(8), Ok(0x1112_1314_1516_1718)); + assert_eq!(second.load_u32_acquire(4), Ok(5)); + assert_eq!( + first.store_u64_and_fetch_add_u32_release(8, 0, 64, 1), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + first.store_u64_and_fetch_add_u32_release(8, 0, 12, 1), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!(second.load_u64_acquire(8), Ok(0x1112_1314_1516_1718)); + second.futex_wait_u32(4, 0).unwrap(); + assert_eq!( + second.load_u64_acquire(1), + Err(SharedMemoryError::UnalignedAtomic) + ); + assert_eq!( + second.store_u64_release(1, 0), + Err(SharedMemoryError::UnalignedAtomic) + ); + assert_eq!( + second.load_u64_acquire(64), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.load_u32_acquire(1), + Err(SharedMemoryError::UnalignedAtomic) + ); + assert_eq!( + second.fetch_add_u32_release(64, 1), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.futex_wait_u32(64, 0).unwrap_err().kind(), + ErrorKind::InvalidInput + ); + assert_eq!( + second.futex_wake_u32(1).unwrap_err().kind(), + ErrorKind::InvalidInput + ); + } + #[test] fn rejects_unsealed_mismatched_and_oversized_mappings() { let fd = memfd_create("litebox-broker-shm-test", MemfdFlags::CLOEXEC).unwrap(); @@ -388,6 +623,102 @@ mod tests { assert!(flags.contains(FdFlags::CLOEXEC)); } + #[test] + fn transfers_exact_sealed_control_ring_mapping() { + let memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let ring = ControlRing::new(memory).unwrap(); + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + + send_memfd(&mut sender, ring.memory(), None).unwrap(); + let mapped = receive_memfd(&mut receiver, CONTROL_RING_MEMORY_SIZE, None).unwrap(); + let mapped_ring = ControlRing::new(mapped).unwrap(); + ring.memory().write(13, &[1, 2, 3]).unwrap(); + let mut bytes = [0; 3]; + mapped_ring.memory().read(13, &mut bytes).unwrap(); + assert_eq!(bytes, [1, 2, 3]); + + let flags = rustix::io::fcntl_getfd(mapped_ring.memory().as_fd()).unwrap(); + assert!(flags.contains(FdFlags::CLOEXEC)); + let seals = fcntl_get_seals(mapped_ring.memory().as_fd()).unwrap(); + assert!(seals.contains(REQUIRED_MEMFD_SEALS)); + assert!(!seals.contains(SealFlags::WRITE)); + } + + #[test] + fn shared_futex_wakeup_prevents_missed_cross_mapping_work() { + let local_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let broker_memory = MemfdSharedMemory::from_received_fd( + local_memory.as_fd().try_clone_to_owned().unwrap(), + CONTROL_RING_MEMORY_SIZE, + ) + .unwrap(); + let (mut producer, _) = ControlRing::new(local_memory).unwrap().into_local(); + let (_, mut consumer) = ControlRing::new(broker_memory).unwrap().into_broker(); + let empty_checked = Arc::new(Barrier::new(2)); + let broker_empty_checked = Arc::clone(&empty_checked); + + let broker = thread::spawn(move || { + let ControlRingReadStatus::Empty { + wait_epoch: producer_epoch, + } = consumer + .try_read(|payload| Ok::<_, ()>(payload[0])) + .unwrap() + else { + panic!("request ring should initially be empty"); + }; + broker_empty_checked.wait(); + consumer.wait_for_message(producer_epoch).unwrap(); + for expected in 0..CONTROL_RING_SLOT_COUNT { + let expected = u8::try_from(expected).unwrap(); + assert_eq!( + consumer.try_read(|payload| Ok::<_, ()>(payload[0])), + Ok(ControlRingReadStatus::Message(expected)) + ); + } + + consumer.publish_head().unwrap(); + consumer.wake_producer().unwrap(); + match consumer + .try_read(|payload| Ok::<_, ()>(payload[0])) + .unwrap() + { + ControlRingReadStatus::Empty { wait_epoch } => { + consumer.wait_for_message(wait_epoch).unwrap(); + assert_eq!( + consumer.try_read(|payload| Ok::<_, ()>(payload[0])), + Ok(ControlRingReadStatus::Message(0xff)) + ); + } + ControlRingReadStatus::Message(value) => assert_eq!(value, 0xff), + } + }); + + empty_checked.wait(); + for value in 0..CONTROL_RING_SLOT_COUNT { + let value = u8::try_from(value).unwrap(); + assert_eq!( + producer.try_write(&[value]), + Ok(ControlRingWriteStatus::Written) + ); + } + let ControlRingWriteStatus::Full { + wait_epoch: consumer_epoch, + } = producer.try_write(&[0xff]).unwrap() + else { + panic!("request ring should be full"); + }; + producer.wake_consumer().unwrap(); + + producer.wait_for_capacity(consumer_epoch).unwrap(); + assert_eq!( + producer.try_write(&[0xff]), + Ok(ControlRingWriteStatus::Written) + ); + producer.wake_consumer().unwrap(); + + broker.join().unwrap(); + } + #[test] fn rejects_missing_multiple_and_truncated_descriptors() { let length = 8; From 57a1131faec1238a36de830ae5685efc01c90d45 Mon Sep 17 00:00:00 2001 From: "Jay Bosamiya (Microsoft)" Date: Thu, 23 Jul 2026 15:31:47 -0700 Subject: [PATCH 125/319] Normalize objdump snapshot output (#1078) This allows more consistent results across different machines with differing toolchains of objdump, which could otherwise have spurious failures. Primarily only impacts whitespace changes, but also normalizes out the comments --- .../tests/snapshot_tests.rs | 71 +- .../snapshot_tests__hello-aarch64-diff.snap | 26 +- .../snapshots/snapshot_tests__hello-diff.snap | 2250 ++++++++--------- 3 files changed, 1192 insertions(+), 1155 deletions(-) diff --git a/litebox_syscall_rewriter/tests/snapshot_tests.rs b/litebox_syscall_rewriter/tests/snapshot_tests.rs index 2afc255a17..2506c39de6 100644 --- a/litebox_syscall_rewriter/tests/snapshot_tests.rs +++ b/litebox_syscall_rewriter/tests/snapshot_tests.rs @@ -17,12 +17,17 @@ fn objdump(objdump_cmd: &str, binary: &[u8]) -> String { .output() .unwrap(); - String::from_utf8_lossy(&output.stdout) + let mut lines = String::from_utf8_lossy(&output.stdout) .lines() .filter(|l| !l.contains("/tmp/")) .map(|line| normalize_objdump_line(line, trampoline_range.as_ref())) - .collect::>() - .join("\n") + .collect::>(); + let first_content = lines + .iter() + .position(|line| !line.is_empty()) + .unwrap_or(lines.len()); + lines.drain(..first_content); + lines.join("\n") } /// Return the first objdump-like command that exists on the host from @@ -57,9 +62,6 @@ fn trampoline_range(binary: &[u8]) -> Option> { } fn normalize_objdump_line(line: &str, trampoline_range: Option<&std::ops::Range>) -> String { - let Some(trampoline_range) = trampoline_range else { - return line.trim_end().to_owned(); - }; let Some((address, rest)) = line.split_once(':') else { return line.trim_end().to_owned(); }; @@ -71,19 +73,54 @@ fn normalize_objdump_line(line: &str, trampoline_range: Option<&std::ops::Range< // trampoline base so the snapshot is independent of the trampoline's exact // address. Other branches (and same-mnemonic branches that stay in the // original code) are left untouched. - for (i, token) in tokens.iter().enumerate() { - if !matches!(*token, "jmp" | "b" | "bl") { - continue; - } - if let Some(target) = tokens - .get(i + 1) - .and_then(|t| u64::from_str_radix(t.trim_start_matches("0x"), 16).ok()) - && trampoline_range.contains(&target) - { - let offset = target - trampoline_range.start; - return format!("{address}:\t"); + if let Some(trampoline_range) = trampoline_range { + for (i, token) in tokens.iter().enumerate() { + if !matches!(*token, "jmp" | "b" | "bl") { + continue; + } + if let Some(target) = tokens + .get(i + 1) + .and_then(|t| u64::from_str_radix(t.trim_start_matches("0x"), 16).ok()) + && trampoline_range.contains(&target) + { + let offset = target - trampoline_range.start; + return format!("{address}:\t"); + } } } + + // GNU and LLVM objdump differ in whitespace, capitalization, comments, + // and some numeric formatting. Keep snapshots focused on instructions + // rather than the disassembler that happened to be available. + let code_len = tokens + .iter() + .take_while(|token| { + matches!(token.len(), 2 | 8) && token.bytes().all(|byte| byte.is_ascii_hexdigit()) + }) + .count(); + if code_len != 0 && code_len < tokens.len() { + let machine_code = tokens[..code_len].join(" ").to_ascii_lowercase(); + let instruction = tokens[code_len..] + .iter() + .take_while(|token| !matches!(**token, "//" | "#")) + .map(|token| { + let token = token.to_ascii_lowercase(); + if token == "#0" { + "#0x0".to_owned() + } else if let Some(value) = token.strip_prefix("0x") + && value.bytes().all(|byte| byte.is_ascii_hexdigit()) + { + value.to_owned() + } else { + token + } + }) + .collect::>() + .join(" ") + .replace(", ", ","); + return format!("{address}:\t{machine_code}\t{instruction}"); + } + line.trim_end().to_owned() } diff --git a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-aarch64-diff.snap b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-aarch64-diff.snap index 2f1dd63c73..fe098d0a27 100644 --- a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-aarch64-diff.snap +++ b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-aarch64-diff.snap @@ -4,26 +4,26 @@ expression: diff --- --- original +++ rewritten -@@ -4,15 +4,15 @@ +@@ -1,15 +1,15 @@ Disassembly of section .text: 0000000000400110 <_start>: -- 400110: d51bd045 msr tpidr_el0, x5 -- 400114: d53bd049 mrs x9, tpidr_el0 +- 400110: d51bd045 msr tpidr_el0,x5 +- 400114: d53bd049 mrs x9,tpidr_el0 + 400110: + 400114: - 400118: d2800808 mov x8, #0x40 // #64 - 40011c: d2800020 mov x0, #0x1 // #1 - 400120: 910003e1 mov x1, sp - 400124: d28001c2 mov x2, #0xe // #14 -- 400128: d4000001 svc #0x0 + 400118: d2800808 mov x8,#0x40 + 40011c: d2800020 mov x0,#0x1 + 400120: 910003e1 mov x1,sp + 400124: d28001c2 mov x2,#0xe +- 400128: d4000001 svc #0x0 + 400128: - 40012c: d2801588 mov x8, #0xac // #172 -- 400130: d4000001 svc #0x0 + 40012c: d2801588 mov x8,#0xac +- 400130: d4000001 svc #0x0 + 400130: - 400134: d2800ba8 mov x8, #0x5d // #93 - 400138: d2800000 mov x0, #0x0 // #0 -- 40013c: d4000001 svc #0x0 + 400134: d2800ba8 mov x8,#0x5d + 400138: d2800000 mov x0,#0x0 +- 40013c: d4000001 svc #0x0 \ No newline at end of file + 40013c: \ No newline at end of file diff --git a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap index 5c41cdaaad..9f91e9b7fa 100644 --- a/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap +++ b/litebox_syscall_rewriter/tests/snapshots/snapshot_tests__hello-diff.snap @@ -4,1288 +4,1288 @@ expression: diff --- --- original +++ rewritten -@@ -131,8 +131,9 @@ - 401217: 48 c7 85 50 ff ff ff movq $0x20,-0xb0(%rbp) +@@ -128,8 +128,9 @@ + 401217: 48 c7 85 50 ff ff ff movq $0x20,-0xb0(%rbp) 40121e: 20 00 00 00 - 401222: bf 01 00 00 00 mov $0x1,%edi -- 401227: b8 0e 00 00 00 mov $0xe,%eax -- 40122c: 0f 05 syscall + 401222: bf 01 00 00 00 mov $0x1,%edi +- 401227: b8 0e 00 00 00 mov $0xe,%eax +- 40122c: 0f 05 syscall + 401227: -+ 40122c: 90 nop -+ 40122d: 90 nop - 40122e: 8b 05 0c fc 0a 00 mov 0xafc0c(%rip),%eax # 4b0e40 - 401234: 83 f8 01 cmp $0x1,%eax - 401237: 75 77 jne 4012b0 -@@ -1133,9 +1134,8 @@ - 401e6c: 74 12 je 401e80 <__libc_start_call_main+0x90> - 401e6e: ba 3c 00 00 00 mov $0x3c,%edx - 401e73: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) -- 401e78: 31 ff xor %edi,%edi -- 401e7a: 89 d0 mov %edx,%eax -- 401e7c: 0f 05 syscall ++ 40122c: 90 nop ++ 40122d: 90 nop + 40122e: 8b 05 0c fc 0a 00 mov 0xafc0c(%rip),%eax + 401234: 83 f8 01 cmp $0x1,%eax + 401237: 75 77 jne 4012b0 +@@ -1130,9 +1131,8 @@ + 401e6c: 74 12 je 401e80 <__libc_start_call_main+0x90> + 401e6e: ba 3c 00 00 00 mov $0x3c,%edx + 401e73: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) +- 401e78: 31 ff xor %edi,%edi +- 401e7a: 89 d0 mov %edx,%eax +- 401e7c: 0f 05 syscall + 401e78: -+ 401e7d: 90 nop - 401e7e: eb f8 jmp 401e78 <__libc_start_call_main+0x88> - 401e80: 31 c0 xor %eax,%eax - 401e82: eb d4 jmp 401e58 <__libc_start_call_main+0x68> -@@ -3117,8 +3117,9 @@ - 403ed9: 74 11 je 403eec <__libc_start_main+0x13c> - 403edb: be 01 00 00 00 mov $0x1,%esi - 403ee0: bf 01 50 00 00 mov $0x5001,%edi -- 403ee5: b8 9e 00 00 00 mov $0x9e,%eax -- 403eea: 0f 05 syscall ++ 401e7d: 90 nop + 401e7e: eb f8 jmp 401e78 <__libc_start_call_main+0x88> + 401e80: 31 c0 xor %eax,%eax + 401e82: eb d4 jmp 401e58 <__libc_start_call_main+0x68> +@@ -3114,8 +3114,9 @@ + 403ed9: 74 11 je 403eec <__libc_start_main+0x13c> + 403edb: be 01 00 00 00 mov $0x1,%esi + 403ee0: bf 01 50 00 00 mov $0x5001,%edi +- 403ee5: b8 9e 00 00 00 mov $0x9e,%eax +- 403eea: 0f 05 syscall + 403ee5: -+ 403eea: 90 nop -+ 403eeb: 90 nop - 403eec: 44 89 ef mov %r13d,%edi - 403eef: e8 9c d4 01 00 call 421390 <_dl_cet_setup_features> - 403ef4: 48 8b 15 0d 52 0a 00 mov 0xa520d(%rip),%rdx # 4a9108 <_dl_random> -@@ -3441,18 +3442,22 @@ - 4043c5: 48 89 46 08 mov %rax,0x8(%rsi) - 4043c9: b8 9e 00 00 00 mov $0x9e,%eax - 4043ce: 48 89 36 mov %rsi,(%rsi) -- 4043d1: 48 89 76 10 mov %rsi,0x10(%rsi) -- 4043d5: 0f 05 syscall ++ 403eea: 90 nop ++ 403eeb: 90 nop + 403eec: 44 89 ef mov %r13d,%edi + 403eef: e8 9c d4 01 00 call 421390 <_dl_cet_setup_features> + 403ef4: 48 8b 15 0d 52 0a 00 mov 0xa520d(%rip),%rdx +@@ -3438,18 +3439,22 @@ + 4043c5: 48 89 46 08 mov %rax,0x8(%rsi) + 4043c9: b8 9e 00 00 00 mov $0x9e,%eax + 4043ce: 48 89 36 mov %rsi,(%rsi) +- 4043d1: 48 89 76 10 mov %rsi,0x10(%rsi) +- 4043d5: 0f 05 syscall + 4043d1: -+ 4043d6: 90 nop - 4043d7: 85 c0 test %eax,%eax - 4043d9: 74 24 je 4043ff <__libc_setup_tls+0x1df> - 4043db: ba 2d 00 00 00 mov $0x2d,%edx - 4043e0: bf 02 00 00 00 mov $0x2,%edi - 4043e5: b8 01 00 00 00 mov $0x1,%eax -- 4043ea: 48 8d 35 c7 d1 07 00 lea 0x7d1c7(%rip),%rsi # 4815b8 -- 4043f1: 0f 05 syscall ++ 4043d6: 90 nop + 4043d7: 85 c0 test %eax,%eax + 4043d9: 74 24 je 4043ff <__libc_setup_tls+0x1df> + 4043db: ba 2d 00 00 00 mov $0x2d,%edx + 4043e0: bf 02 00 00 00 mov $0x2,%edi + 4043e5: b8 01 00 00 00 mov $0x1,%eax +- 4043ea: 48 8d 35 c7 d1 07 00 lea 0x7d1c7(%rip),%rsi +- 4043f1: 0f 05 syscall + 4043ea: -+ 4043ef: 90 nop -+ 4043f0: 90 nop -+ 4043f1: 90 nop -+ 4043f2: 90 nop - 4043f3: bf 7f 00 00 00 mov $0x7f,%edi -- 4043f8: b8 e7 00 00 00 mov $0xe7,%eax -- 4043fd: 0f 05 syscall ++ 4043ef: 90 nop ++ 4043f0: 90 nop ++ 4043f1: 90 nop ++ 4043f2: 90 nop + 4043f3: bf 7f 00 00 00 mov $0x7f,%edi +- 4043f8: b8 e7 00 00 00 mov $0xe7,%eax +- 4043fd: 0f 05 syscall + 4043f8: -+ 4043fd: 90 nop -+ 4043fe: 90 nop - 4043ff: e8 dc ba 01 00 call 41fee0 <__tls_init_tp> - 404404: 48 8b 45 c8 mov -0x38(%rbp),%rax - 404408: 4d 89 ae 78 04 00 00 mov %r13,0x478(%r14) -@@ -3492,11 +3497,15 @@ - 4044b0: ba 2d 00 00 00 mov $0x2d,%edx - 4044b5: bf 02 00 00 00 mov $0x2,%edi - 4044ba: b8 01 00 00 00 mov $0x1,%eax -- 4044bf: 48 8d 35 f2 d0 07 00 lea 0x7d0f2(%rip),%rsi # 4815b8 -- 4044c6: 0f 05 syscall ++ 4043fd: 90 nop ++ 4043fe: 90 nop + 4043ff: e8 dc ba 01 00 call 41fee0 <__tls_init_tp> + 404404: 48 8b 45 c8 mov -0x38(%rbp),%rax + 404408: 4d 89 ae 78 04 00 00 mov %r13,0x478(%r14) +@@ -3489,11 +3494,15 @@ + 4044b0: ba 2d 00 00 00 mov $0x2d,%edx + 4044b5: bf 02 00 00 00 mov $0x2,%edi + 4044ba: b8 01 00 00 00 mov $0x1,%eax +- 4044bf: 48 8d 35 f2 d0 07 00 lea 0x7d0f2(%rip),%rsi +- 4044c6: 0f 05 syscall + 4044bf: -+ 4044c4: 90 nop -+ 4044c5: 90 nop -+ 4044c6: 90 nop -+ 4044c7: 90 nop - 4044c8: bf 7f 00 00 00 mov $0x7f,%edi -- 4044cd: b8 e7 00 00 00 mov $0xe7,%eax -- 4044d2: 0f 05 syscall ++ 4044c4: 90 nop ++ 4044c5: 90 nop ++ 4044c6: 90 nop ++ 4044c7: 90 nop + 4044c8: bf 7f 00 00 00 mov $0x7f,%edi +- 4044cd: b8 e7 00 00 00 mov $0xe7,%eax +- 4044d2: 0f 05 syscall + 4044cd: -+ 4044d2: 90 nop -+ 4044d3: 90 nop - 4044d4: e9 70 fe ff ff jmp 404349 <__libc_setup_tls+0x129> - 4044d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) ++ 4044d2: 90 nop ++ 4044d3: 90 nop + 4044d4: e9 70 fe ff ff jmp 404349 <__libc_setup_tls+0x129> + 4044d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) -@@ -9234,8 +9243,7 @@ - 40a3dc: 0f 1f 40 00 nopl 0x0(%rax) - 40a3e0: 48 8b b5 f0 fe ff ff mov -0x110(%rbp),%rsi - 40a3e7: bf 02 00 00 00 mov $0x2,%edi -- 40a3ec: 44 89 c8 mov %r9d,%eax -- 40a3ef: 0f 05 syscall +@@ -9231,8 +9240,7 @@ + 40a3dc: 0f 1f 40 00 nopl 0x0(%rax) + 40a3e0: 48 8b b5 f0 fe ff ff mov -0x110(%rbp),%rsi + 40a3e7: bf 02 00 00 00 mov $0x2,%edi +- 40a3ec: 44 89 c8 mov %r9d,%eax +- 40a3ef: 0f 05 syscall + 40a3ec: - 40a3f1: 48 83 f8 fc cmp $0xfffffffffffffffc,%rax - 40a3f5: 74 e9 je 40a3e0 <__libc_message_impl+0x150> - 40a3f7: 45 31 c9 xor %r9d,%r9d -@@ -9372,8 +9380,9 @@ - 40a5c7: 45 31 d2 xor %r10d,%r10d - 40a5ca: ba 02 00 00 00 mov $0x2,%edx - 40a5cf: be 80 00 00 00 mov $0x80,%esi -- 40a5d4: b8 ca 00 00 00 mov $0xca,%eax -- 40a5d9: 0f 05 syscall + 40a3f1: 48 83 f8 fc cmp $0xfffffffffffffffc,%rax + 40a3f5: 74 e9 je 40a3e0 <__libc_message_impl+0x150> + 40a3f7: 45 31 c9 xor %r9d,%r9d +@@ -9369,8 +9377,9 @@ + 40a5c7: 45 31 d2 xor %r10d,%r10d + 40a5ca: ba 02 00 00 00 mov $0x2,%edx + 40a5cf: be 80 00 00 00 mov $0x80,%esi +- 40a5d4: b8 ca 00 00 00 mov $0xca,%eax +- 40a5d9: 0f 05 syscall + 40a5d4: -+ 40a5d9: 90 nop -+ 40a5da: 90 nop - 40a5db: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 40a5e1: 76 d8 jbe 40a5bb <__lll_lock_wait_private+0xb> - 40a5e3: 83 f8 f5 cmp $0xfffffff5,%eax -@@ -9405,8 +9414,8 @@ - 40a62d: 45 31 d2 xor %r10d,%r10d - 40a630: ba 02 00 00 00 mov $0x2,%edx - 40a635: b8 ca 00 00 00 mov $0xca,%eax -- 40a63a: 40 80 f6 80 xor $0x80,%sil -- 40a63e: 0f 05 syscall ++ 40a5d9: 90 nop ++ 40a5da: 90 nop + 40a5db: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 40a5e1: 76 d8 jbe 40a5bb <__lll_lock_wait_private+0xb> + 40a5e3: 83 f8 f5 cmp $0xfffffff5,%eax +@@ -9402,8 +9411,8 @@ + 40a62d: 45 31 d2 xor %r10d,%r10d + 40a630: ba 02 00 00 00 mov $0x2,%edx + 40a635: b8 ca 00 00 00 mov $0xca,%eax +- 40a63a: 40 80 f6 80 xor $0x80,%sil +- 40a63e: 0f 05 syscall + 40a63a: -+ 40a63f: 90 nop - 40a640: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 40a646: 76 d6 jbe 40a61e <__lll_lock_wait+0xe> - 40a648: 83 f8 f5 cmp $0xfffffff5,%eax -@@ -9426,8 +9435,9 @@ - 40a674: 45 31 d2 xor %r10d,%r10d - 40a677: ba 01 00 00 00 mov $0x1,%edx - 40a67c: be 81 00 00 00 mov $0x81,%esi -- 40a681: b8 ca 00 00 00 mov $0xca,%eax -- 40a686: 0f 05 syscall ++ 40a63f: 90 nop + 40a640: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 40a646: 76 d6 jbe 40a61e <__lll_lock_wait+0xe> + 40a648: 83 f8 f5 cmp $0xfffffff5,%eax +@@ -9423,8 +9432,9 @@ + 40a674: 45 31 d2 xor %r10d,%r10d + 40a677: ba 01 00 00 00 mov $0x1,%edx + 40a67c: be 81 00 00 00 mov $0x81,%esi +- 40a681: b8 ca 00 00 00 mov $0xca,%eax +- 40a686: 0f 05 syscall + 40a681: -+ 40a686: 90 nop -+ 40a687: 90 nop - 40a688: c3 ret - 40a689: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) ++ 40a686: 90 nop ++ 40a687: 90 nop + 40a688: c3 ret + 40a689: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) -@@ -9436,8 +9446,9 @@ - 40a694: 40 80 f6 81 xor $0x81,%sil - 40a698: 45 31 d2 xor %r10d,%r10d - 40a69b: ba 01 00 00 00 mov $0x1,%edx -- 40a6a0: b8 ca 00 00 00 mov $0xca,%eax -- 40a6a5: 0f 05 syscall +@@ -9433,8 +9443,9 @@ + 40a694: 40 80 f6 81 xor $0x81,%sil + 40a698: 45 31 d2 xor %r10d,%r10d + 40a69b: ba 01 00 00 00 mov $0x1,%edx +- 40a6a0: b8 ca 00 00 00 mov $0xca,%eax +- 40a6a5: 0f 05 syscall + 40a6a0: -+ 40a6a5: 90 nop -+ 40a6a6: 90 nop - 40a6a7: c3 ret - 40a6a8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) ++ 40a6a5: 90 nop ++ 40a6a6: 90 nop + 40a6a7: c3 ret + 40a6a8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1) 40a6af: 00 -@@ -10840,8 +10851,9 @@ - 40bbd5: 48 89 45 e8 mov %rax,-0x18(%rbp) - 40bbd9: 31 c0 xor %eax,%eax - 40bbdb: c6 05 3e 4c 0a 00 01 movb $0x1,0xa4c3e(%rip) # 4b0820 <__malloc_initialized> -- 40bbe2: b8 3e 01 00 00 mov $0x13e,%eax -- 40bbe7: 0f 05 syscall +@@ -10837,8 +10848,9 @@ + 40bbd5: 48 89 45 e8 mov %rax,-0x18(%rbp) + 40bbd9: 31 c0 xor %eax,%eax + 40bbdb: c6 05 3e 4c 0a 00 01 movb $0x1,0xa4c3e(%rip) +- 40bbe2: b8 3e 01 00 00 mov $0x13e,%eax +- 40bbe7: 0f 05 syscall + 40bbe2: -+ 40bbe7: 90 nop -+ 40bbe8: 90 nop - 40bbe9: 48 8d 5d d0 lea -0x30(%rbp),%rbx - 40bbed: 48 83 f8 08 cmp $0x8,%rax - 40bbf1: 74 4e je 40bc41 -@@ -23532,8 +23544,9 @@ - 4181dc: 5d pop %rbp - 4181dd: c3 ret - 4181de: 66 90 xchg %ax,%ax -- 4181e0: b8 e4 00 00 00 mov $0xe4,%eax -- 4181e5: 0f 05 syscall ++ 40bbe7: 90 nop ++ 40bbe8: 90 nop + 40bbe9: 48 8d 5d d0 lea -0x30(%rbp),%rbx + 40bbed: 48 83 f8 08 cmp $0x8,%rax + 40bbf1: 74 4e je 40bc41 +@@ -23529,8 +23541,9 @@ + 4181dc: 5d pop %rbp + 4181dd: c3 ret + 4181de: 66 90 xchg %ax,%ax +- 4181e0: b8 e4 00 00 00 mov $0xe4,%eax +- 4181e5: 0f 05 syscall + 4181e0: -+ 4181e5: 90 nop -+ 4181e6: 90 nop - 4181e7: 85 c0 test %eax,%eax - 4181e9: 75 1d jne 418208 <__clock_gettime+0x48> - 4181eb: 31 c0 xor %eax,%eax -@@ -23566,8 +23579,10 @@ - 418242: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) - 418248: f4 hlt - 418249: 89 d0 mov %edx,%eax -- 41824b: 0f 05 syscall -- 41824d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax ++ 4181e5: 90 nop ++ 4181e6: 90 nop + 4181e7: 85 c0 test %eax,%eax + 4181e9: 75 1d jne 418208 <__clock_gettime+0x48> + 4181eb: 31 c0 xor %eax,%eax +@@ -23563,8 +23576,10 @@ + 418242: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) + 418248: f4 hlt + 418249: 89 d0 mov %edx,%eax +- 41824b: 0f 05 syscall +- 41824d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 41824b: -+ 418250: 90 nop -+ 418251: 90 nop -+ 418252: 90 nop - 418253: 76 f3 jbe 418248 <_exit+0x18> - 418255: f7 d8 neg %eax - 418257: 64 89 06 mov %eax,%fs:(%rsi) -@@ -23576,8 +23591,9 @@ ++ 418250: 90 nop ++ 418251: 90 nop ++ 418252: 90 nop + 418253: 76 f3 jbe 418248 <_exit+0x18> + 418255: f7 d8 neg %eax + 418257: 64 89 06 mov %eax,%fs:(%rsi) +@@ -23573,8 +23588,9 @@ 0000000000418260 <__fstat>: - 418260: f3 0f 1e fa endbr64 -- 418264: b8 05 00 00 00 mov $0x5,%eax -- 418269: 0f 05 syscall + 418260: f3 0f 1e fa endbr64 +- 418264: b8 05 00 00 00 mov $0x5,%eax +- 418269: 0f 05 syscall + 418264: -+ 418269: 90 nop -+ 41826a: 90 nop - 41826b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 418271: 77 05 ja 418278 <__fstat+0x18> - 418273: c3 ret -@@ -23591,8 +23607,9 @@ ++ 418269: 90 nop ++ 41826a: 90 nop + 41826b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 418271: 77 05 ja 418278 <__fstat+0x18> + 418273: c3 ret +@@ -23588,8 +23604,9 @@ 0000000000418290 <__close_nocancel>: - 418290: f3 0f 1e fa endbr64 -- 418294: b8 03 00 00 00 mov $0x3,%eax -- 418299: 0f 05 syscall + 418290: f3 0f 1e fa endbr64 +- 418294: b8 03 00 00 00 mov $0x3,%eax +- 418299: 0f 05 syscall + 418294: -+ 418299: 90 nop -+ 41829a: 90 nop - 41829b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 4182a1: 77 05 ja 4182a8 <__close_nocancel+0x18> - 4182a3: c3 ret -@@ -23621,8 +23638,9 @@ - 4182f2: 48 89 45 c0 mov %rax,-0x40(%rbp) - 4182f6: 83 fe 09 cmp $0x9,%esi - 4182f9: 74 25 je 418320 <__fcntl64_nocancel+0x60> -- 4182fb: b8 48 00 00 00 mov $0x48,%eax -- 418300: 0f 05 syscall ++ 418299: 90 nop ++ 41829a: 90 nop + 41829b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 4182a1: 77 05 ja 4182a8 <__close_nocancel+0x18> + 4182a3: c3 ret +@@ -23618,8 +23635,9 @@ + 4182f2: 48 89 45 c0 mov %rax,-0x40(%rbp) + 4182f6: 83 fe 09 cmp $0x9,%esi + 4182f9: 74 25 je 418320 <__fcntl64_nocancel+0x60> +- 4182fb: b8 48 00 00 00 mov $0x48,%eax +- 418300: 0f 05 syscall + 4182fb: -+ 418300: 90 nop -+ 418301: 90 nop - 418302: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 418308: 77 3e ja 418348 <__fcntl64_nocancel+0x88> - 41830a: 48 8b 55 c8 mov -0x38(%rbp),%rdx -@@ -23634,8 +23652,9 @@ - 41831b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) - 418320: 48 8d 55 a8 lea -0x58(%rbp),%rdx - 418324: be 10 00 00 00 mov $0x10,%esi -- 418329: b8 48 00 00 00 mov $0x48,%eax -- 41832e: 0f 05 syscall ++ 418300: 90 nop ++ 418301: 90 nop + 418302: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 418308: 77 3e ja 418348 <__fcntl64_nocancel+0x88> + 41830a: 48 8b 55 c8 mov -0x38(%rbp),%rdx +@@ -23631,8 +23649,9 @@ + 41831b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) + 418320: 48 8d 55 a8 lea -0x58(%rbp),%rdx + 418324: be 10 00 00 00 mov $0x10,%esi +- 418329: b8 48 00 00 00 mov $0x48,%eax +- 41832e: 0f 05 syscall + 418329: -+ 41832e: 90 nop -+ 41832f: 90 nop - 418330: 3d 00 f0 ff ff cmp $0xfffff000,%eax - 418335: 77 11 ja 418348 <__fcntl64_nocancel+0x88> - 418337: 83 7d a8 02 cmpl $0x2,-0x58(%rbp) -@@ -23662,8 +23681,9 @@ - 418379: 31 c0 xor %eax,%eax - 41837b: 83 fe 09 cmp $0x9,%esi - 41837e: 74 20 je 4183a0 <__fcntl64_nocancel_adjusted+0x40> -- 418380: b8 48 00 00 00 mov $0x48,%eax -- 418385: 0f 05 syscall ++ 41832e: 90 nop ++ 41832f: 90 nop + 418330: 3d 00 f0 ff ff cmp $0xfffff000,%eax + 418335: 77 11 ja 418348 <__fcntl64_nocancel+0x88> + 418337: 83 7d a8 02 cmpl $0x2,-0x58(%rbp) +@@ -23659,8 +23678,9 @@ + 418379: 31 c0 xor %eax,%eax + 41837b: 83 fe 09 cmp $0x9,%esi + 41837e: 74 20 je 4183a0 <__fcntl64_nocancel_adjusted+0x40> +- 418380: b8 48 00 00 00 mov $0x48,%eax +- 418385: 0f 05 syscall + 418380: -+ 418385: 90 nop -+ 418386: 90 nop - 418387: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 41838d: 77 39 ja 4183c8 <__fcntl64_nocancel_adjusted+0x68> - 41838f: 48 8b 55 f8 mov -0x8(%rbp),%rdx -@@ -23674,8 +23694,9 @@ - 41839f: c3 ret - 4183a0: 48 8d 55 f0 lea -0x10(%rbp),%rdx - 4183a4: be 10 00 00 00 mov $0x10,%esi -- 4183a9: b8 48 00 00 00 mov $0x48,%eax -- 4183ae: 0f 05 syscall ++ 418385: 90 nop ++ 418386: 90 nop + 418387: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 41838d: 77 39 ja 4183c8 <__fcntl64_nocancel_adjusted+0x68> + 41838f: 48 8b 55 f8 mov -0x8(%rbp),%rdx +@@ -23671,8 +23691,9 @@ + 41839f: c3 ret + 4183a0: 48 8d 55 f0 lea -0x10(%rbp),%rdx + 4183a4: be 10 00 00 00 mov $0x10,%esi +- 4183a9: b8 48 00 00 00 mov $0x48,%eax +- 4183ae: 0f 05 syscall + 4183a9: -+ 4183ae: 90 nop -+ 4183af: 90 nop - 4183b0: 3d 00 f0 ff ff cmp $0xfffff000,%eax - 4183b5: 77 11 ja 4183c8 <__fcntl64_nocancel_adjusted+0x68> - 4183b7: 83 7d f0 02 cmpl $0x2,-0x10(%rbp) -@@ -23711,8 +23732,9 @@ - 418413: 89 f2 mov %esi,%edx - 418415: b8 01 01 00 00 mov $0x101,%eax - 41841a: 48 89 fe mov %rdi,%rsi -- 41841d: bf 9c ff ff ff mov $0xffffff9c,%edi -- 418422: 0f 05 syscall ++ 4183ae: 90 nop ++ 4183af: 90 nop + 4183b0: 3d 00 f0 ff ff cmp $0xfffff000,%eax + 4183b5: 77 11 ja 4183c8 <__fcntl64_nocancel_adjusted+0x68> + 4183b7: 83 7d f0 02 cmpl $0x2,-0x10(%rbp) +@@ -23708,8 +23729,9 @@ + 418413: 89 f2 mov %esi,%edx + 418415: b8 01 01 00 00 mov $0x101,%eax + 41841a: 48 89 fe mov %rdi,%rsi +- 41841d: bf 9c ff ff ff mov $0xffffff9c,%edi +- 418422: 0f 05 syscall + 41841d: -+ 418422: 90 nop -+ 418423: 90 nop - 418424: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 41842a: 77 34 ja 418460 <__open64_nocancel+0x80> - 41842c: 48 8b 55 c8 mov -0x38(%rbp),%rdx -@@ -23740,9 +23762,10 @@ ++ 418422: 90 nop ++ 418423: 90 nop + 418424: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 41842a: 77 34 ja 418460 <__open64_nocancel+0x80> + 41842c: 48 8b 55 c8 mov -0x38(%rbp),%rdx +@@ -23737,9 +23759,10 @@ 41847f: 00 0000000000418480 <__read_nocancel>: -- 418480: f3 0f 1e fa endbr64 -- 418484: 31 c0 xor %eax,%eax -- 418486: 0f 05 syscall +- 418480: f3 0f 1e fa endbr64 +- 418484: 31 c0 xor %eax,%eax +- 418486: 0f 05 syscall + 418480: -+ 418485: 90 nop -+ 418486: 90 nop -+ 418487: 90 nop - 418488: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 41848e: 77 08 ja 418498 <__read_nocancel+0x18> - 418490: c3 ret -@@ -23756,8 +23779,9 @@ ++ 418485: 90 nop ++ 418486: 90 nop ++ 418487: 90 nop + 418488: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 41848e: 77 08 ja 418498 <__read_nocancel+0x18> + 418490: c3 ret +@@ -23753,8 +23776,9 @@ 00000000004184b0 <__brk>: - 4184b0: f3 0f 1e fa endbr64 -- 4184b4: b8 0c 00 00 00 mov $0xc,%eax -- 4184b9: 0f 05 syscall + 4184b0: f3 0f 1e fa endbr64 +- 4184b4: b8 0c 00 00 00 mov $0xc,%eax +- 4184b9: 0f 05 syscall + 4184b4: -+ 4184b9: 90 nop -+ 4184ba: 90 nop - 4184bb: 48 89 05 96 83 09 00 mov %rax,0x98396(%rip) # 4b0858 <__curbrk> - 4184c2: 48 39 f8 cmp %rdi,%rax - 4184c5: 72 09 jb 4184d0 <__brk+0x20> -@@ -23922,8 +23946,9 @@ - 418714: 48 89 45 f8 mov %rax,-0x8(%rbp) - 418718: 31 c0 xor %eax,%eax - 41871a: 48 8d 95 f0 ef ff ff lea -0x1010(%rbp),%rdx -- 418721: b8 cc 00 00 00 mov $0xcc,%eax -- 418726: 0f 05 syscall ++ 4184b9: 90 nop ++ 4184ba: 90 nop + 4184bb: 48 89 05 96 83 09 00 mov %rax,0x98396(%rip) + 4184c2: 48 39 f8 cmp %rdi,%rax + 4184c5: 72 09 jb 4184d0 <__brk+0x20> +@@ -23919,8 +23943,9 @@ + 418714: 48 89 45 f8 mov %rax,-0x8(%rbp) + 418718: 31 c0 xor %eax,%eax + 41871a: 48 8d 95 f0 ef ff ff lea -0x1010(%rbp),%rdx +- 418721: b8 cc 00 00 00 mov $0xcc,%eax +- 418726: 0f 05 syscall + 418721: -+ 418726: 90 nop -+ 418727: 90 nop - 418728: 85 c0 test %eax,%eax - 41872a: 7f 24 jg 418750 <__get_nprocs_sched+0x60> - 41872c: 83 f8 ea cmp $0xffffffea,%eax -@@ -24231,8 +24256,9 @@ ++ 418726: 90 nop ++ 418727: 90 nop + 418728: 85 c0 test %eax,%eax + 41872a: 7f 24 jg 418750 <__get_nprocs_sched+0x60> + 41872c: 83 f8 ea cmp $0xffffffea,%eax +@@ -24228,8 +24253,9 @@ 0000000000418b40 <__madvise>: - 418b40: f3 0f 1e fa endbr64 -- 418b44: b8 1c 00 00 00 mov $0x1c,%eax -- 418b49: 0f 05 syscall + 418b40: f3 0f 1e fa endbr64 +- 418b44: b8 1c 00 00 00 mov $0x1c,%eax +- 418b49: 0f 05 syscall + 418b44: -+ 418b49: 90 nop -+ 418b4a: 90 nop - 418b4b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 418b51: 73 01 jae 418b54 <__madvise+0x14> - 418b53: c3 ret -@@ -24259,8 +24285,9 @@ - 418b8d: 74 41 je 418bd0 <__mmap64+0x60> - 418b8f: 45 89 e2 mov %r12d,%r10d - 418b92: 48 89 df mov %rbx,%rdi -- 418b95: b8 09 00 00 00 mov $0x9,%eax -- 418b9a: 0f 05 syscall ++ 418b49: 90 nop ++ 418b4a: 90 nop + 418b4b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 418b51: 73 01 jae 418b54 <__madvise+0x14> + 418b53: c3 ret +@@ -24256,8 +24282,9 @@ + 418b8d: 74 41 je 418bd0 <__mmap64+0x60> + 418b8f: 45 89 e2 mov %r12d,%r10d + 418b92: 48 89 df mov %rbx,%rdi +- 418b95: b8 09 00 00 00 mov $0x9,%eax +- 418b9a: 0f 05 syscall + 418b95: -+ 418b9a: 90 nop -+ 418b9b: 90 nop - 418b9c: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 418ba2: 77 6c ja 418c10 <__mmap64+0xa0> - 418ba4: 5b pop %rbx -@@ -24284,8 +24311,8 @@ - 418be8: 45 89 e2 mov %r12d,%r10d - 418beb: 31 ff xor %edi,%edi - 418bed: b8 09 00 00 00 mov $0x9,%eax -- 418bf2: 41 83 ca 40 or $0x40,%r10d -- 418bf6: 0f 05 syscall ++ 418b9a: 90 nop ++ 418b9b: 90 nop + 418b9c: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 418ba2: 77 6c ja 418c10 <__mmap64+0xa0> + 418ba4: 5b pop %rbx +@@ -24281,8 +24308,8 @@ + 418be8: 45 89 e2 mov %r12d,%r10d + 418beb: 31 ff xor %edi,%edi + 418bed: b8 09 00 00 00 mov $0x9,%eax +- 418bf2: 41 83 ca 40 or $0x40,%r10d +- 418bf6: 0f 05 syscall + 418bf2: -+ 418bf7: 90 nop - 418bf8: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 418bfe: 76 a4 jbe 418ba4 <__mmap64+0x34> - 418c00: 48 c7 c1 c0 ff ff ff mov $0xffffffffffffffc0,%rcx -@@ -24303,8 +24330,9 @@ ++ 418bf7: 90 nop + 418bf8: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 418bfe: 76 a4 jbe 418ba4 <__mmap64+0x34> + 418c00: 48 c7 c1 c0 ff ff ff mov $0xffffffffffffffc0,%rcx +@@ -24300,8 +24327,9 @@ 0000000000418c30 <__mprotect>: - 418c30: f3 0f 1e fa endbr64 -- 418c34: b8 0a 00 00 00 mov $0xa,%eax -- 418c39: 0f 05 syscall + 418c30: f3 0f 1e fa endbr64 +- 418c34: b8 0a 00 00 00 mov $0xa,%eax +- 418c39: 0f 05 syscall + 418c34: -+ 418c39: 90 nop -+ 418c3a: 90 nop - 418c3b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 418c41: 73 01 jae 418c44 <__mprotect+0x14> - 418c43: c3 ret -@@ -24319,8 +24347,9 @@ ++ 418c39: 90 nop ++ 418c3a: 90 nop + 418c3b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 418c41: 73 01 jae 418c44 <__mprotect+0x14> + 418c43: c3 ret +@@ -24316,8 +24344,9 @@ 0000000000418c60 <__munmap>: - 418c60: f3 0f 1e fa endbr64 -- 418c64: b8 0b 00 00 00 mov $0xb,%eax -- 418c69: 0f 05 syscall + 418c60: f3 0f 1e fa endbr64 +- 418c64: b8 0b 00 00 00 mov $0xb,%eax +- 418c69: 0f 05 syscall + 418c64: -+ 418c69: 90 nop -+ 418c6a: 90 nop - 418c6b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 418c71: 73 01 jae 418c74 <__munmap+0x14> - 418c73: c3 ret -@@ -24396,8 +24425,9 @@ - 418d42: 83 e1 02 and $0x2,%ecx - 418d45: 75 29 jne 418d70 <__mremap+0x50> - 418d47: 45 31 c0 xor %r8d,%r8d -- 418d4a: b8 19 00 00 00 mov $0x19,%eax -- 418d4f: 0f 05 syscall ++ 418c69: 90 nop ++ 418c6a: 90 nop + 418c6b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 418c71: 73 01 jae 418c74 <__munmap+0x14> + 418c73: c3 ret +@@ -24393,8 +24422,9 @@ + 418d42: 83 e1 02 and $0x2,%ecx + 418d45: 75 29 jne 418d70 <__mremap+0x50> + 418d47: 45 31 c0 xor %r8d,%r8d +- 418d4a: b8 19 00 00 00 mov $0x19,%eax +- 418d4f: 0f 05 syscall + 418d4a: -+ 418d4f: 90 nop -+ 418d50: 90 nop - 418d51: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 418d57: 77 37 ja 418d90 <__mremap+0x70> - 418d59: 48 8b 55 c8 mov -0x38(%rbp),%rdx -@@ -24463,8 +24493,9 @@ - 418e1e: 48 89 da mov %rbx,%rdx - 418e21: 31 f6 xor %esi,%esi - 418e23: bf 41 4d 56 53 mov $0x53564d41,%edi -- 418e28: b8 9d 00 00 00 mov $0x9d,%eax -- 418e2d: 0f 05 syscall ++ 418d4f: 90 nop ++ 418d50: 90 nop + 418d51: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 418d57: 77 37 ja 418d90 <__mremap+0x70> + 418d59: 48 8b 55 c8 mov -0x38(%rbp),%rdx +@@ -24460,8 +24490,9 @@ + 418e1e: 48 89 da mov %rbx,%rdx + 418e21: 31 f6 xor %esi,%esi + 418e23: bf 41 4d 56 53 mov $0x53564d41,%edi +- 418e28: b8 9d 00 00 00 mov $0x9d,%eax +- 418e2d: 0f 05 syscall + 418e28: -+ 418e2d: 90 nop -+ 418e2e: 90 nop - 418e2f: 83 f8 ea cmp $0xffffffea,%eax - 418e32: 75 a4 jne 418dd8 <__set_vma_name+0x28> - 418e34: c7 05 5e 1c 09 00 00 movl $0x0,0x91c5e(%rip) # 4aaa9c -@@ -24477,8 +24508,9 @@ ++ 418e2d: 90 nop ++ 418e2e: 90 nop + 418e2f: 83 f8 ea cmp $0xffffffea,%eax + 418e32: 75 a4 jne 418dd8 <__set_vma_name+0x28> + 418e34: c7 05 5e 1c 09 00 00 movl $0x0,0x91c5e(%rip) +@@ -24474,8 +24505,9 @@ 0000000000418e50 <__sysinfo>: - 418e50: f3 0f 1e fa endbr64 -- 418e54: b8 63 00 00 00 mov $0x63,%eax -- 418e59: 0f 05 syscall + 418e50: f3 0f 1e fa endbr64 +- 418e54: b8 63 00 00 00 mov $0x63,%eax +- 418e59: 0f 05 syscall + 418e54: -+ 418e59: 90 nop -+ 418e5a: 90 nop - 418e5b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 418e61: 73 01 jae 418e64 <__sysinfo+0x14> - 418e63: c3 ret -@@ -29948,8 +29980,7 @@ - 41e488: b8 0b 01 00 00 mov $0x10b,%eax - 41e48d: 48 8d 35 0d 17 06 00 lea 0x6170d(%rip),%rsi # 47fba1 <__PRETTY_FUNCTION__.20+0x37e> - 41e494: 48 8d 9d e0 ef ff ff lea -0x1020(%rbp),%rbx -- 41e49b: 48 89 da mov %rbx,%rdx -- 41e49e: 0f 05 syscall ++ 418e59: 90 nop ++ 418e5a: 90 nop + 418e5b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 418e61: 73 01 jae 418e64 <__sysinfo+0x14> + 418e63: c3 ret +@@ -29945,8 +29977,7 @@ + 41e488: b8 0b 01 00 00 mov $0x10b,%eax + 41e48d: 48 8d 35 0d 17 06 00 lea 0x6170d(%rip),%rsi + 41e494: 48 8d 9d e0 ef ff ff lea -0x1020(%rbp),%rbx +- 41e49b: 48 89 da mov %rbx,%rdx +- 41e49e: 0f 05 syscall + 41e49b: - 41e4a0: 85 c0 test %eax,%eax - 41e4a2: 7e 5c jle 41e500 <_dl_get_origin+0xa0> - 41e4a4: 0f b6 95 e0 ef ff ff movzbl -0x1020(%rbp),%edx -@@ -30115,8 +30146,9 @@ - 41e6d2: 8b bd a8 f6 ff ff mov -0x958(%rbp),%edi - 41e6d8: 48 63 d3 movslq %ebx,%rdx - 41e6db: 48 8d b5 d0 f6 ff ff lea -0x930(%rbp),%rsi -- 41e6e2: b8 14 00 00 00 mov $0x14,%eax -- 41e6e7: 0f 05 syscall + 41e4a0: 85 c0 test %eax,%eax + 41e4a2: 7e 5c jle 41e500 <_dl_get_origin+0xa0> + 41e4a4: 0f b6 95 e0 ef ff ff movzbl -0x1020(%rbp),%edx +@@ -30112,8 +30143,9 @@ + 41e6d2: 8b bd a8 f6 ff ff mov -0x958(%rbp),%edi + 41e6d8: 48 63 d3 movslq %ebx,%rdx + 41e6db: 48 8d b5 d0 f6 ff ff lea -0x930(%rbp),%rsi +- 41e6e2: b8 14 00 00 00 mov $0x14,%eax +- 41e6e7: 0f 05 syscall + 41e6e2: -+ 41e6e7: 90 nop -+ 41e6e8: 90 nop - 41e6e9: 48 81 c4 38 09 00 00 add $0x938,%rsp - 41e6f0: 5b pop %rbx - 41e6f1: 41 5c pop %r12 -@@ -31674,8 +31706,9 @@ - 41ff19: 48 89 42 08 mov %rax,0x8(%rdx) - 41ff1d: 48 89 05 ec 09 09 00 mov %rax,0x909ec(%rip) # 4b0910 <_dl_stack_user> - 41ff24: 48 8d bb d0 02 00 00 lea 0x2d0(%rbx),%rdi -- 41ff2b: b8 da 00 00 00 mov $0xda,%eax -- 41ff30: 0f 05 syscall ++ 41e6e7: 90 nop ++ 41e6e8: 90 nop + 41e6e9: 48 81 c4 38 09 00 00 add $0x938,%rsp + 41e6f0: 5b pop %rbx + 41e6f1: 41 5c pop %r12 +@@ -31671,8 +31703,9 @@ + 41ff19: 48 89 42 08 mov %rax,0x8(%rdx) + 41ff1d: 48 89 05 ec 09 09 00 mov %rax,0x909ec(%rip) + 41ff24: 48 8d bb d0 02 00 00 lea 0x2d0(%rbx),%rdi +- 41ff2b: b8 da 00 00 00 mov $0xda,%eax +- 41ff30: 0f 05 syscall + 41ff2b: -+ 41ff30: 90 nop -+ 41ff31: 90 nop - 41ff32: 89 83 d0 02 00 00 mov %eax,0x2d0(%rbx) - 41ff38: 48 8d 83 10 03 00 00 lea 0x310(%rbx),%rax - 41ff3f: 64 48 89 04 25 10 05 mov %rax,%fs:0x510 -@@ -31692,8 +31725,11 @@ - 41ff77: b8 11 01 00 00 mov $0x111,%eax - 41ff7c: 66 48 0f 6e c7 movq %rdi,%xmm0 - 41ff81: 66 0f 6c c0 punpcklqdq %xmm0,%xmm0 -- 41ff85: 0f 11 83 d8 02 00 00 movups %xmm0,0x2d8(%rbx) -- 41ff8c: 0f 05 syscall ++ 41ff30: 90 nop ++ 41ff31: 90 nop + 41ff32: 89 83 d0 02 00 00 mov %eax,0x2d0(%rbx) + 41ff38: 48 8d 83 10 03 00 00 lea 0x310(%rbx),%rax + 41ff3f: 64 48 89 04 25 10 05 mov %rax,%fs:0x510 +@@ -31689,8 +31722,11 @@ + 41ff77: b8 11 01 00 00 mov $0x111,%eax + 41ff7c: 66 48 0f 6e c7 movq %rdi,%xmm0 + 41ff81: 66 0f 6c c0 punpcklqdq %xmm0,%xmm0 +- 41ff85: 0f 11 83 d8 02 00 00 movups %xmm0,0x2d8(%rbx) +- 41ff8c: 0f 05 syscall + 41ff85: -+ 41ff8a: 90 nop -+ 41ff8b: 90 nop -+ 41ff8c: 90 nop -+ 41ff8d: 90 nop - 41ff8e: 31 d2 xor %edx,%edx - 41ff90: 48 8d 75 ec lea -0x14(%rbp),%rsi - 41ff94: bf 28 00 00 00 mov $0x28,%edi -@@ -31719,8 +31755,9 @@ - 41ffed: 31 d2 xor %edx,%edx - 41ffef: be 20 00 00 00 mov $0x20,%esi - 41fff4: 48 89 df mov %rbx,%rdi -- 41fff7: b8 4e 01 00 00 mov $0x14e,%eax -- 41fffc: 0f 05 syscall ++ 41ff8a: 90 nop ++ 41ff8b: 90 nop ++ 41ff8c: 90 nop ++ 41ff8d: 90 nop + 41ff8e: 31 d2 xor %edx,%edx + 41ff90: 48 8d 75 ec lea -0x14(%rbp),%rsi + 41ff94: bf 28 00 00 00 mov $0x28,%edi +@@ -31716,8 +31752,9 @@ + 41ffed: 31 d2 xor %edx,%edx + 41ffef: be 20 00 00 00 mov $0x20,%esi + 41fff4: 48 89 df mov %rbx,%rdi +- 41fff7: b8 4e 01 00 00 mov $0x14e,%eax +- 41fffc: 0f 05 syscall + 41fff7: -+ 41fffc: 90 nop -+ 41fffd: 90 nop - 41fffe: 3d 00 f0 ff ff cmp $0xfffff000,%eax - 420003: 77 a7 ja 41ffac <__tls_init_tp+0xcc> - 420005: c7 05 11 7b 08 00 20 movl $0x20,0x87b11(%rip) # 4a7b20 <__rseq_size> -@@ -33086,8 +33123,9 @@ - 421339: 0f 84 d9 fe ff ff je 421218 <_dl_cet_open_check+0x158> - 42133f: be 01 00 00 00 mov $0x1,%esi - 421344: bf 02 50 00 00 mov $0x5002,%edi -- 421349: b8 9e 00 00 00 mov $0x9e,%eax -- 42134e: 0f 05 syscall ++ 41fffc: 90 nop ++ 41fffd: 90 nop + 41fffe: 3d 00 f0 ff ff cmp $0xfffff000,%eax + 420003: 77 a7 ja 41ffac <__tls_init_tp+0xcc> + 420005: c7 05 11 7b 08 00 20 movl $0x20,0x87b11(%rip) +@@ -33083,8 +33120,9 @@ + 421339: 0f 84 d9 fe ff ff je 421218 <_dl_cet_open_check+0x158> + 42133f: be 01 00 00 00 mov $0x1,%esi + 421344: bf 02 50 00 00 mov $0x5002,%edi +- 421349: b8 9e 00 00 00 mov $0x9e,%eax +- 42134e: 0f 05 syscall + 421349: -+ 42134e: 90 nop -+ 42134f: 90 nop - 421350: 89 c7 mov %eax,%edi - 421352: 85 c0 test %eax,%eax - 421354: 75 24 jne 42137a <_dl_cet_open_check+0x2ba> -@@ -33117,8 +33155,8 @@ - 42139e: bf 05 50 00 00 mov $0x5005,%edi - 4213a3: 89 d0 mov %edx,%eax - 4213a5: 48 89 e5 mov %rsp,%rbp -- 4213a8: 48 8d 75 f8 lea -0x8(%rbp),%rsi -- 4213ac: 0f 05 syscall ++ 42134e: 90 nop ++ 42134f: 90 nop + 421350: 89 c7 mov %eax,%edi + 421352: 85 c0 test %eax,%eax + 421354: 75 24 jne 42137a <_dl_cet_open_check+0x2ba> +@@ -33114,8 +33152,8 @@ + 42139e: bf 05 50 00 00 mov $0x5005,%edi + 4213a3: 89 d0 mov %edx,%eax + 4213a5: 48 89 e5 mov %rsp,%rbp +- 4213a8: 48 8d 75 f8 lea -0x8(%rbp),%rsi +- 4213ac: 0f 05 syscall + 4213a8: -+ 4213ad: 90 nop - 4213ae: 48 85 c0 test %rax,%rax - 4213b1: 74 15 je 4213c8 <_dl_cet_setup_features+0x38> - 4213b3: 31 c0 xor %eax,%eax -@@ -33141,9 +33179,11 @@ - 4213ec: a8 0c test $0xc,%al - 4213ee: 74 10 je 421400 <_dl_cet_setup_features+0x70> - 4213f0: 48 c7 c6 ff ff ff ff mov $0xffffffffffffffff,%rsi -- 4213f7: bf 03 50 00 00 mov $0x5003,%edi -- 4213fc: 89 d0 mov %edx,%eax -- 4213fe: 0f 05 syscall ++ 4213ad: 90 nop + 4213ae: 48 85 c0 test %rax,%rax + 4213b1: 74 15 je 4213c8 <_dl_cet_setup_features+0x38> + 4213b3: 31 c0 xor %eax,%eax +@@ -33138,9 +33176,11 @@ + 4213ec: a8 0c test $0xc,%al + 4213ee: 74 10 je 421400 <_dl_cet_setup_features+0x70> + 4213f0: 48 c7 c6 ff ff ff ff mov $0xffffffffffffffff,%rsi +- 4213f7: bf 03 50 00 00 mov $0x5003,%edi +- 4213fc: 89 d0 mov %edx,%eax +- 4213fe: 0f 05 syscall + 4213f7: -+ 4213fc: 90 nop -+ 4213fd: 90 nop -+ 4213fe: 90 nop -+ 4213ff: 90 nop - 421400: b8 02 00 00 00 mov $0x2,%eax - 421405: eb ae jmp 4213b5 <_dl_cet_setup_features+0x25> - 421407: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) -@@ -33172,13 +33212,13 @@ - 421446: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) ++ 4213fc: 90 nop ++ 4213fd: 90 nop ++ 4213fe: 90 nop ++ 4213ff: 90 nop + 421400: b8 02 00 00 00 mov $0x2,%eax + 421405: eb ae jmp 4213b5 <_dl_cet_setup_features+0x25> + 421407: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1) +@@ -33169,13 +33209,13 @@ + 421446: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 42144d: 00 00 00 - 421450: be 0c 00 00 00 mov $0xc,%esi -- 421455: 31 ff xor %edi,%edi -- 421457: 89 f0 mov %esi,%eax -- 421459: 0f 05 syscall + 421450: be 0c 00 00 00 mov $0xc,%esi +- 421455: 31 ff xor %edi,%edi +- 421457: 89 f0 mov %esi,%eax +- 421459: 0f 05 syscall + 421455: -+ 42145a: 90 nop - 42145b: 48 89 c2 mov %rax,%rdx -- 42145e: 48 8d 3c 18 lea (%rax,%rbx,1),%rdi -- 421462: 89 f0 mov %esi,%eax -- 421464: 0f 05 syscall ++ 42145a: 90 nop + 42145b: 48 89 c2 mov %rax,%rdx +- 42145e: 48 8d 3c 18 lea (%rax,%rbx,1),%rdi +- 421462: 89 f0 mov %esi,%eax +- 421464: 0f 05 syscall + 42145e: -+ 421463: 90 nop -+ 421464: 90 nop -+ 421465: 90 nop - 421466: 48 39 c2 cmp %rax,%rdx - 421469: 75 cd jne 421438 <_dl_early_allocate+0x28> - 42146b: 45 31 c9 xor %r9d,%r9d -@@ -33187,8 +33227,9 @@ - 421479: 31 ff xor %edi,%edi - 42147b: 41 ba 22 00 00 00 mov $0x22,%r10d - 421481: 48 89 de mov %rbx,%rsi -- 421484: b8 09 00 00 00 mov $0x9,%eax -- 421489: 0f 05 syscall ++ 421463: 90 nop ++ 421464: 90 nop ++ 421465: 90 nop + 421466: 48 39 c2 cmp %rax,%rdx + 421469: 75 cd jne 421438 <_dl_early_allocate+0x28> + 42146b: 45 31 c9 xor %r9d,%r9d +@@ -33184,8 +33224,9 @@ + 421479: 31 ff xor %edi,%edi + 42147b: 41 ba 22 00 00 00 mov $0x22,%r10d + 421481: 48 89 de mov %rbx,%rsi +- 421484: b8 09 00 00 00 mov $0x9,%eax +- 421489: 0f 05 syscall + 421484: -+ 421489: 90 nop -+ 42148a: 90 nop - 42148b: 31 d2 xor %edx,%edx - 42148d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 421493: 48 8b 5d f8 mov -0x8(%rbp),%rbx -@@ -69741,8 +69782,9 @@ - 444c0d: 41 ba 08 00 00 00 mov $0x8,%r10d - 444c13: 4c 89 f2 mov %r14,%rdx - 444c16: 48 8d 35 b3 0a 04 00 lea 0x40ab3(%rip),%rsi # 4856d0 -- 444c1d: b8 0e 00 00 00 mov $0xe,%eax -- 444c22: 0f 05 syscall ++ 421489: 90 nop ++ 42148a: 90 nop + 42148b: 31 d2 xor %edx,%edx + 42148d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 421493: 48 8b 5d f8 mov -0x8(%rbp),%rbx +@@ -69738,8 +69779,9 @@ + 444c0d: 41 ba 08 00 00 00 mov $0x8,%r10d + 444c13: 4c 89 f2 mov %r14,%rdx + 444c16: 48 8d 35 b3 0a 04 00 lea 0x40ab3(%rip),%rsi +- 444c1d: b8 0e 00 00 00 mov $0xe,%eax +- 444c22: 0f 05 syscall + 444c1d: -+ 444c22: 90 nop -+ 444c23: 90 nop - 444c24: 31 c0 xor %eax,%eax - 444c26: 4c 8d a3 04 09 00 00 lea 0x904(%rbx),%r12 - 444c2d: ba 01 00 00 00 mov $0x1,%edx -@@ -69759,8 +69801,9 @@ - 444c5e: 31 d2 xor %edx,%edx - 444c60: 4c 89 f6 mov %r14,%rsi - 444c63: bf 02 00 00 00 mov $0x2,%edi -- 444c68: b8 0e 00 00 00 mov $0xe,%eax -- 444c6d: 0f 05 syscall ++ 444c22: 90 nop ++ 444c23: 90 nop + 444c24: 31 c0 xor %eax,%eax + 444c26: 4c 8d a3 04 09 00 00 lea 0x904(%rbx),%r12 + 444c2d: ba 01 00 00 00 mov $0x1,%edx +@@ -69756,8 +69798,9 @@ + 444c5e: 31 d2 xor %edx,%edx + 444c60: 4c 89 f6 mov %r14,%rsi + 444c63: bf 02 00 00 00 mov $0x2,%edi +- 444c68: b8 0e 00 00 00 mov $0xe,%eax +- 444c6d: 0f 05 syscall + 444c68: -+ 444c6d: 90 nop -+ 444c6e: 90 nop - 444c6f: 48 8b 45 d8 mov -0x28(%rbp),%rax - 444c73: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax ++ 444c6d: 90 nop ++ 444c6e: 90 nop + 444c6f: 48 8b 45 d8 mov -0x28(%rbp),%rax + 444c73: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax 444c7a: 00 00 -@@ -69779,23 +69822,26 @@ - 444ca3: 44 89 ea mov %r13d,%edx - 444ca6: 89 c7 mov %eax,%edi - 444ca8: 89 de mov %ebx,%esi -- 444caa: b8 ea 00 00 00 mov $0xea,%eax -- 444caf: 0f 05 syscall +@@ -69776,23 +69819,26 @@ + 444ca3: 44 89 ea mov %r13d,%edx + 444ca6: 89 c7 mov %eax,%edi + 444ca8: 89 de mov %ebx,%esi +- 444caa: b8 ea 00 00 00 mov $0xea,%eax +- 444caf: 0f 05 syscall + 444caa: -+ 444caf: 90 nop -+ 444cb0: 90 nop - 444cb1: 3d 00 f0 ff ff cmp $0xfffff000,%eax - 444cb6: 76 8f jbe 444c47 <__pthread_kill_internal+0x77> - 444cb8: 89 c3 mov %eax,%ebx - 444cba: f7 db neg %ebx - 444cbc: eb 8b jmp 444c49 <__pthread_kill_internal+0x79> - 444cbe: 66 90 xchg %ax,%ax -- 444cc0: b8 ba 00 00 00 mov $0xba,%eax -- 444cc5: 0f 05 syscall ++ 444caf: 90 nop ++ 444cb0: 90 nop + 444cb1: 3d 00 f0 ff ff cmp $0xfffff000,%eax + 444cb6: 76 8f jbe 444c47 <__pthread_kill_internal+0x77> + 444cb8: 89 c3 mov %eax,%ebx + 444cba: f7 db neg %ebx + 444cbc: eb 8b jmp 444c49 <__pthread_kill_internal+0x79> + 444cbe: 66 90 xchg %ax,%ax +- 444cc0: b8 ba 00 00 00 mov $0xba,%eax +- 444cc5: 0f 05 syscall + 444cc0: -+ 444cc5: 90 nop -+ 444cc6: 90 nop - 444cc7: 89 c3 mov %eax,%ebx - 444cc9: e8 82 6e 01 00 call 45bb50 <__getpid> - 444cce: 44 89 ea mov %r13d,%edx - 444cd1: 89 de mov %ebx,%esi - 444cd3: 89 c7 mov %eax,%edi -- 444cd5: b8 ea 00 00 00 mov $0xea,%eax -- 444cda: 0f 05 syscall ++ 444cc5: 90 nop ++ 444cc6: 90 nop + 444cc7: 89 c3 mov %eax,%ebx + 444cc9: e8 82 6e 01 00 call 45bb50 <__getpid> + 444cce: 44 89 ea mov %r13d,%edx + 444cd1: 89 de mov %ebx,%esi + 444cd3: 89 c7 mov %eax,%edi +- 444cd5: b8 ea 00 00 00 mov $0xea,%eax +- 444cda: 0f 05 syscall + 444cd5: -+ 444cda: 90 nop -+ 444cdb: 90 nop - 444cdc: 89 c3 mov %eax,%ebx - 444cde: f7 db neg %ebx - 444ce0: 3d 00 f0 ff ff cmp $0xfffff000,%eax -@@ -69843,8 +69889,11 @@ - 444d71: 31 ff xor %edi,%edi - 444d73: b8 0e 00 00 00 mov $0xe,%eax - 444d78: 4c 89 fa mov %r15,%rdx -- 444d7b: 48 8d 35 4e 09 04 00 lea 0x4094e(%rip),%rsi # 4856d0 -- 444d82: 0f 05 syscall ++ 444cda: 90 nop ++ 444cdb: 90 nop + 444cdc: 89 c3 mov %eax,%ebx + 444cde: f7 db neg %ebx + 444ce0: 3d 00 f0 ff ff cmp $0xfffff000,%eax +@@ -69840,8 +69886,11 @@ + 444d71: 31 ff xor %edi,%edi + 444d73: b8 0e 00 00 00 mov $0xe,%eax + 444d78: 4c 89 fa mov %r15,%rdx +- 444d7b: 48 8d 35 4e 09 04 00 lea 0x4094e(%rip),%rsi +- 444d82: 0f 05 syscall + 444d7b: -+ 444d80: 90 nop -+ 444d81: 90 nop -+ 444d82: 90 nop -+ 444d83: 90 nop - 444d84: 31 c0 xor %eax,%eax - 444d86: 4c 8d ab 04 09 00 00 lea 0x904(%rbx),%r13 - 444d8d: ba 01 00 00 00 mov $0x1,%edx -@@ -69861,8 +69910,9 @@ - 444dbf: 31 d2 xor %edx,%edx - 444dc1: 4c 89 fe mov %r15,%rsi - 444dc4: bf 02 00 00 00 mov $0x2,%edi -- 444dc9: b8 0e 00 00 00 mov $0xe,%eax -- 444dce: 0f 05 syscall ++ 444d80: 90 nop ++ 444d81: 90 nop ++ 444d82: 90 nop ++ 444d83: 90 nop + 444d84: 31 c0 xor %eax,%eax + 444d86: 4c 8d ab 04 09 00 00 lea 0x904(%rbx),%r13 + 444d8d: ba 01 00 00 00 mov $0x1,%edx +@@ -69858,8 +69907,9 @@ + 444dbf: 31 d2 xor %edx,%edx + 444dc1: 4c 89 fe mov %r15,%rsi + 444dc4: bf 02 00 00 00 mov $0x2,%edi +- 444dc9: b8 0e 00 00 00 mov $0xe,%eax +- 444dce: 0f 05 syscall + 444dc9: -+ 444dce: 90 nop -+ 444dcf: 90 nop - 444dd0: 48 8b 45 c8 mov -0x38(%rbp),%rax - 444dd4: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax ++ 444dce: 90 nop ++ 444dcf: 90 nop + 444dd0: 48 8b 45 c8 mov -0x38(%rbp),%rax + 444dd4: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax 444ddb: 00 00 -@@ -69882,22 +69932,25 @@ - 444e03: 44 89 e2 mov %r12d,%edx - 444e06: 89 c7 mov %eax,%edi - 444e08: 89 de mov %ebx,%esi -- 444e0a: b8 ea 00 00 00 mov $0xea,%eax -- 444e0f: 0f 05 syscall +@@ -69879,22 +69929,25 @@ + 444e03: 44 89 e2 mov %r12d,%edx + 444e06: 89 c7 mov %eax,%edi + 444e08: 89 de mov %ebx,%esi +- 444e0a: b8 ea 00 00 00 mov $0xea,%eax +- 444e0f: 0f 05 syscall + 444e0a: -+ 444e0f: 90 nop -+ 444e10: 90 nop - 444e11: 3d 00 f0 ff ff cmp $0xfffff000,%eax - 444e16: 76 8f jbe 444da7 <__pthread_kill+0x87> - 444e18: 41 89 c6 mov %eax,%r14d - 444e1b: 41 f7 de neg %r14d - 444e1e: eb 8a jmp 444daa <__pthread_kill+0x8a> -- 444e20: b8 ba 00 00 00 mov $0xba,%eax -- 444e25: 0f 05 syscall ++ 444e0f: 90 nop ++ 444e10: 90 nop + 444e11: 3d 00 f0 ff ff cmp $0xfffff000,%eax + 444e16: 76 8f jbe 444da7 <__pthread_kill+0x87> + 444e18: 41 89 c6 mov %eax,%r14d + 444e1b: 41 f7 de neg %r14d + 444e1e: eb 8a jmp 444daa <__pthread_kill+0x8a> +- 444e20: b8 ba 00 00 00 mov $0xba,%eax +- 444e25: 0f 05 syscall + 444e20: -+ 444e25: 90 nop -+ 444e26: 90 nop - 444e27: 89 c3 mov %eax,%ebx - 444e29: e8 22 6d 01 00 call 45bb50 <__getpid> - 444e2e: 44 89 e2 mov %r12d,%edx - 444e31: 89 de mov %ebx,%esi - 444e33: 89 c7 mov %eax,%edi -- 444e35: b8 ea 00 00 00 mov $0xea,%eax -- 444e3a: 0f 05 syscall ++ 444e25: 90 nop ++ 444e26: 90 nop + 444e27: 89 c3 mov %eax,%ebx + 444e29: e8 22 6d 01 00 call 45bb50 <__getpid> + 444e2e: 44 89 e2 mov %r12d,%edx + 444e31: 89 de mov %ebx,%esi + 444e33: 89 c7 mov %eax,%edi +- 444e35: b8 ea 00 00 00 mov $0xea,%eax +- 444e3a: 0f 05 syscall + 444e35: -+ 444e3a: 90 nop -+ 444e3b: 90 nop - 444e3c: 41 89 c6 mov %eax,%r14d - 444e3f: 41 f7 de neg %r14d - 444e42: 3d 00 f0 ff ff cmp $0xfffff000,%eax -@@ -70102,8 +70155,10 @@ - 445101: 48 89 df mov %rbx,%rdi - 445104: 44 89 f0 mov %r14d,%eax - 445107: f7 d6 not %esi -- 445109: 81 e6 80 00 00 00 and $0x80,%esi -- 44510f: 0f 05 syscall ++ 444e3a: 90 nop ++ 444e3b: 90 nop + 444e3c: 41 89 c6 mov %eax,%r14d + 444e3f: 41 f7 de neg %r14d + 444e42: 3d 00 f0 ff ff cmp $0xfffff000,%eax +@@ -70099,8 +70152,10 @@ + 445101: 48 89 df mov %rbx,%rdi + 445104: 44 89 f0 mov %r14d,%eax + 445107: f7 d6 not %esi +- 445109: 81 e6 80 00 00 00 and $0x80,%esi +- 44510f: 0f 05 syscall + 445109: -+ 44510e: 90 nop -+ 44510f: 90 nop -+ 445110: 90 nop - 445111: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 445117: 76 b7 jbe 4450d0 <__pthread_mutex_lock_full+0x1a0> - 445119: 83 f8 f5 cmp $0xfffffff5,%eax -@@ -70220,8 +70275,9 @@ - 4452df: 45 31 d2 xor %r10d,%r10d - 4452e2: 31 f6 xor %esi,%esi - 4452e4: 48 89 df mov %rbx,%rdi -- 4452e7: b8 ca 00 00 00 mov $0xca,%eax -- 4452ec: 0f 05 syscall ++ 44510e: 90 nop ++ 44510f: 90 nop ++ 445110: 90 nop + 445111: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 445117: 76 b7 jbe 4450d0 <__pthread_mutex_lock_full+0x1a0> + 445119: 83 f8 f5 cmp $0xfffffff5,%eax +@@ -70217,8 +70272,9 @@ + 4452df: 45 31 d2 xor %r10d,%r10d + 4452e2: 31 f6 xor %esi,%esi + 4452e4: 48 89 df mov %rbx,%rdi +- 4452e7: b8 ca 00 00 00 mov $0xca,%eax +- 4452ec: 0f 05 syscall + 4452e7: -+ 4452ec: 90 nop -+ 4452ed: 90 nop - 4452ee: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 4452f4: 0f 87 4e 02 00 00 ja 445548 <__pthread_mutex_lock_full+0x618> - 4452fa: 8b 13 mov (%rbx),%edx -@@ -70339,8 +70395,9 @@ - 4454fa: 31 d2 xor %edx,%edx - 4454fc: 48 89 df mov %rbx,%rdi - 4454ff: be 07 00 00 00 mov $0x7,%esi -- 445504: b8 ca 00 00 00 mov $0xca,%eax -- 445509: 0f 05 syscall ++ 4452ec: 90 nop ++ 4452ed: 90 nop + 4452ee: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 4452f4: 0f 87 4e 02 00 00 ja 445548 <__pthread_mutex_lock_full+0x618> + 4452fa: 8b 13 mov (%rbx),%edx +@@ -70336,8 +70392,9 @@ + 4454fa: 31 d2 xor %edx,%edx + 4454fc: 48 89 df mov %rbx,%rdi + 4454ff: be 07 00 00 00 mov $0x7,%esi +- 445504: b8 ca 00 00 00 mov $0xca,%eax +- 445509: 0f 05 syscall + 445504: -+ 445509: 90 nop -+ 44550a: 90 nop - 44550b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 445511: 0f 86 71 ff ff ff jbe 445488 <__pthread_mutex_lock_full+0x558> - 445517: 83 f8 92 cmp $0xffffff92,%eax -@@ -70720,8 +70777,8 @@ - 445aa1: 4c 89 c7 mov %r8,%rdi - 445aa4: b8 ca 00 00 00 mov $0xca,%eax - 445aa9: 81 e6 80 00 00 00 and $0x80,%esi -- 445aaf: 40 80 f6 81 xor $0x81,%sil -- 445ab3: 0f 05 syscall ++ 445509: 90 nop ++ 44550a: 90 nop + 44550b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 445511: 0f 86 71 ff ff ff jbe 445488 <__pthread_mutex_lock_full+0x558> + 445517: 83 f8 92 cmp $0xffffff92,%eax +@@ -70717,8 +70774,8 @@ + 445aa1: 4c 89 c7 mov %r8,%rdi + 445aa4: b8 ca 00 00 00 mov $0xca,%eax + 445aa9: 81 e6 80 00 00 00 and $0x80,%esi +- 445aaf: 40 80 f6 81 xor $0x81,%sil +- 445ab3: 0f 05 syscall + 445aaf: -+ 445ab4: 90 nop - 445ab5: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 445abb: 0f 87 0e 02 00 00 ja 445ccf <__pthread_mutex_unlock_full+0x3bf> - 445ac1: 90 nop -@@ -70863,8 +70920,9 @@ - 445cf3: ba 01 00 00 00 mov $0x1,%edx - 445cf8: be 01 00 00 00 mov $0x1,%esi - 445cfd: 4c 89 c7 mov %r8,%rdi -- 445d00: b8 ca 00 00 00 mov $0xca,%eax -- 445d05: 0f 05 syscall ++ 445ab4: 90 nop + 445ab5: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 445abb: 0f 87 0e 02 00 00 ja 445ccf <__pthread_mutex_unlock_full+0x3bf> + 445ac1: 90 nop +@@ -70860,8 +70917,9 @@ + 445cf3: ba 01 00 00 00 mov $0x1,%edx + 445cf8: be 01 00 00 00 mov $0x1,%esi + 445cfd: 4c 89 c7 mov %r8,%rdi +- 445d00: b8 ca 00 00 00 mov $0xca,%eax +- 445d05: 0f 05 syscall + 445d00: -+ 445d05: 90 nop -+ 445d06: 90 nop - 445d07: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 445d0d: 0f 86 36 fd ff ff jbe 445a49 <__pthread_mutex_unlock_full+0x139> - 445d13: 83 c0 16 add $0x16,%eax -@@ -70875,8 +70933,9 @@ - 445d24: 45 31 d2 xor %r10d,%r10d - 445d27: 31 d2 xor %edx,%edx - 445d29: 4c 89 c7 mov %r8,%rdi -- 445d2c: b8 ca 00 00 00 mov $0xca,%eax -- 445d31: 0f 05 syscall ++ 445d05: 90 nop ++ 445d06: 90 nop + 445d07: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 445d0d: 0f 86 36 fd ff ff jbe 445a49 <__pthread_mutex_unlock_full+0x139> + 445d13: 83 c0 16 add $0x16,%eax +@@ -70872,8 +70930,9 @@ + 445d24: 45 31 d2 xor %r10d,%r10d + 445d27: 31 d2 xor %edx,%edx + 445d29: 4c 89 c7 mov %r8,%rdi +- 445d2c: b8 ca 00 00 00 mov $0xca,%eax +- 445d31: 0f 05 syscall + 445d2c: -+ 445d31: 90 nop -+ 445d32: 90 nop - 445d33: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 445d39: 0f 86 f8 fd ff ff jbe 445b37 <__pthread_mutex_unlock_full+0x227> - 445d3f: 83 f8 92 cmp $0xffffff92,%eax -@@ -71093,8 +71152,9 @@ - 446007: 45 31 d2 xor %r10d,%r10d - 44600a: be 80 00 00 00 mov $0x80,%esi - 44600f: 48 89 df mov %rbx,%rdi -- 446012: b8 ca 00 00 00 mov $0xca,%eax -- 446017: 0f 05 syscall ++ 445d31: 90 nop ++ 445d32: 90 nop + 445d33: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 445d39: 0f 86 f8 fd ff ff jbe 445b37 <__pthread_mutex_unlock_full+0x227> + 445d3f: 83 f8 92 cmp $0xffffff92,%eax +@@ -71090,8 +71149,9 @@ + 446007: 45 31 d2 xor %r10d,%r10d + 44600a: be 80 00 00 00 mov $0x80,%esi + 44600f: 48 89 df mov %rbx,%rdi +- 446012: b8 ca 00 00 00 mov $0xca,%eax +- 446017: 0f 05 syscall + 446012: -+ 446017: 90 nop -+ 446018: 90 nop - 446019: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 44601f: 76 a1 jbe 445fc2 <__pthread_once_slow+0x22> - 446021: 83 f8 f5 cmp $0xfffffff5,%eax -@@ -71130,8 +71190,9 @@ - 4460a5: be 81 00 00 00 mov $0x81,%esi - 4460aa: c7 03 02 00 00 00 movl $0x2,(%rbx) - 4460b0: 48 89 df mov %rbx,%rdi -- 4460b3: b8 ca 00 00 00 mov $0xca,%eax -- 4460b8: 0f 05 syscall ++ 446017: 90 nop ++ 446018: 90 nop + 446019: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 44601f: 76 a1 jbe 445fc2 <__pthread_once_slow+0x22> + 446021: 83 f8 f5 cmp $0xfffffff5,%eax +@@ -71127,8 +71187,9 @@ + 4460a5: be 81 00 00 00 mov $0x81,%esi + 4460aa: c7 03 02 00 00 00 movl $0x2,(%rbx) + 4460b0: 48 89 df mov %rbx,%rdi +- 4460b3: b8 ca 00 00 00 mov $0xca,%eax +- 4460b8: 0f 05 syscall + 4460b3: -+ 4460b8: 90 nop -+ 4460b9: 90 nop - 4460ba: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 4460c0: 0f 86 02 ff ff ff jbe 445fc8 <__pthread_once_slow+0x28> - 4460c6: 83 c0 16 add $0x16,%eax -@@ -71173,8 +71234,9 @@ - 44613a: 45 31 d2 xor %r10d,%r10d - 44613d: ba ff ff ff 7f mov $0x7fffffff,%edx - 446142: be 81 00 00 00 mov $0x81,%esi -- 446147: b8 ca 00 00 00 mov $0xca,%eax -- 44614c: 0f 05 syscall ++ 4460b8: 90 nop ++ 4460b9: 90 nop + 4460ba: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 4460c0: 0f 86 02 ff ff ff jbe 445fc8 <__pthread_once_slow+0x28> + 4460c6: 83 c0 16 add $0x16,%eax +@@ -71170,8 +71231,9 @@ + 44613a: 45 31 d2 xor %r10d,%r10d + 44613d: ba ff ff ff 7f mov $0x7fffffff,%edx + 446142: be 81 00 00 00 mov $0x81,%esi +- 446147: b8 ca 00 00 00 mov $0xca,%eax +- 44614c: 0f 05 syscall + 446147: -+ 44614c: 90 nop -+ 44614d: 90 nop - 44614e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 446154: 77 0a ja 446160 - 446156: c3 ret -@@ -71316,8 +71378,8 @@ - 4462dc: 40 0f 95 c6 setne %sil - 4462e0: 45 31 d2 xor %r10d,%r10d - 4462e3: c1 e6 07 shl $0x7,%esi -- 4462e6: 40 80 f6 81 xor $0x81,%sil -- 4462ea: 0f 05 syscall ++ 44614c: 90 nop ++ 44614d: 90 nop + 44614e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 446154: 77 0a ja 446160 + 446156: c3 ret +@@ -71313,8 +71375,8 @@ + 4462dc: 40 0f 95 c6 setne %sil + 4462e0: 45 31 d2 xor %r10d,%r10d + 4462e3: c1 e6 07 shl $0x7,%esi +- 4462e6: 40 80 f6 81 xor $0x81,%sil +- 4462ea: 0f 05 syscall + 4462e6: -+ 4462eb: 90 nop - 4462ec: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 4462f2: 0f 86 2e ff ff ff jbe 446226 <___pthread_rwlock_rdlock+0x46> - 4462f8: 83 c0 16 add $0x16,%eax -@@ -71420,8 +71482,9 @@ - 44642f: ba ff ff ff 7f mov $0x7fffffff,%edx - 446434: 4c 89 c7 mov %r8,%rdi - 446437: 40 80 f6 81 xor $0x81,%sil -- 44643b: b8 ca 00 00 00 mov $0xca,%eax -- 446440: 0f 05 syscall ++ 4462eb: 90 nop + 4462ec: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 4462f2: 0f 86 2e ff ff ff jbe 446226 <___pthread_rwlock_rdlock+0x46> + 4462f8: 83 c0 16 add $0x16,%eax +@@ -71417,8 +71479,9 @@ + 44642f: ba ff ff ff 7f mov $0x7fffffff,%edx + 446434: 4c 89 c7 mov %r8,%rdi + 446437: 40 80 f6 81 xor $0x81,%sil +- 44643b: b8 ca 00 00 00 mov $0xca,%eax +- 446440: 0f 05 syscall + 44643b: -+ 446440: 90 nop -+ 446441: 90 nop - 446442: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 446448: 0f 87 da 00 00 00 ja 446528 <___pthread_rwlock_unlock+0x158> - 44644e: 5b pop %rbx -@@ -71446,8 +71509,8 @@ - 446482: 45 31 d2 xor %r10d,%r10d - 446485: ba ff ff ff 7f mov $0x7fffffff,%edx - 44648a: b8 ca 00 00 00 mov $0xca,%eax -- 44648f: 40 80 f6 81 xor $0x81,%sil -- 446493: 0f 05 syscall ++ 446440: 90 nop ++ 446441: 90 nop + 446442: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 446448: 0f 87 da 00 00 00 ja 446528 <___pthread_rwlock_unlock+0x158> + 44644e: 5b pop %rbx +@@ -71443,8 +71506,8 @@ + 446482: 45 31 d2 xor %r10d,%r10d + 446485: ba ff ff ff 7f mov $0x7fffffff,%edx + 44648a: b8 ca 00 00 00 mov $0xca,%eax +- 44648f: 40 80 f6 81 xor $0x81,%sil +- 446493: 0f 05 syscall + 44648f: -+ 446494: 90 nop - 446495: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 44649b: 76 83 jbe 446420 <___pthread_rwlock_unlock+0x50> - 44649d: 83 c0 16 add $0x16,%eax -@@ -71487,8 +71550,9 @@ - 446509: ba 01 00 00 00 mov $0x1,%edx - 44650e: 48 89 df mov %rbx,%rdi - 446511: 40 80 f6 81 xor $0x81,%sil -- 446515: b8 ca 00 00 00 mov $0xca,%eax -- 44651a: 0f 05 syscall ++ 446494: 90 nop + 446495: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 44649b: 76 83 jbe 446420 <___pthread_rwlock_unlock+0x50> + 44649d: 83 c0 16 add $0x16,%eax +@@ -71484,8 +71547,9 @@ + 446509: ba 01 00 00 00 mov $0x1,%edx + 44650e: 48 89 df mov %rbx,%rdi + 446511: 40 80 f6 81 xor $0x81,%sil +- 446515: b8 ca 00 00 00 mov $0xca,%eax +- 44651a: 0f 05 syscall + 446515: -+ 44651a: 90 nop -+ 44651b: 90 nop - 44651c: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 446522: 0f 86 26 ff ff ff jbe 44644e <___pthread_rwlock_unlock+0x7e> - 446528: 83 c0 16 add $0x16,%eax -@@ -71515,8 +71579,8 @@ - 44656f: 45 31 d2 xor %r10d,%r10d - 446572: ba ff ff ff 7f mov $0x7fffffff,%edx - 446577: b8 ca 00 00 00 mov $0xca,%eax -- 44657c: 40 80 f6 81 xor $0x81,%sil -- 446580: 0f 05 syscall ++ 44651a: 90 nop ++ 44651b: 90 nop + 44651c: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 446522: 0f 86 26 ff ff ff jbe 44644e <___pthread_rwlock_unlock+0x7e> + 446528: 83 c0 16 add $0x16,%eax +@@ -71512,8 +71576,8 @@ + 44656f: 45 31 d2 xor %r10d,%r10d + 446572: ba ff ff ff 7f mov $0x7fffffff,%edx + 446577: b8 ca 00 00 00 mov $0xca,%eax +- 44657c: 40 80 f6 81 xor $0x81,%sil +- 446580: 0f 05 syscall + 44657c: -+ 446581: 90 nop - 446582: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 446588: 0f 86 6c ff ff ff jbe 4464fa <___pthread_rwlock_unlock+0x12a> - 44658e: 83 c0 16 add $0x16,%eax -@@ -71736,8 +71800,9 @@ - 44684d: ba 01 00 00 00 mov $0x1,%edx - 446852: 4c 89 e7 mov %r12,%rdi - 446855: 40 80 f6 81 xor $0x81,%sil -- 446859: b8 ca 00 00 00 mov $0xca,%eax -- 44685e: 0f 05 syscall ++ 446581: 90 nop + 446582: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 446588: 0f 86 6c ff ff ff jbe 4464fa <___pthread_rwlock_unlock+0x12a> + 44658e: 83 c0 16 add $0x16,%eax +@@ -71733,8 +71797,9 @@ + 44684d: ba 01 00 00 00 mov $0x1,%edx + 446852: 4c 89 e7 mov %r12,%rdi + 446855: 40 80 f6 81 xor $0x81,%sil +- 446859: b8 ca 00 00 00 mov $0xca,%eax +- 44685e: 0f 05 syscall + 446859: -+ 44685e: 90 nop -+ 44685f: 90 nop - 446860: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 446866: 0f 87 f9 00 00 00 ja 446965 <___pthread_rwlock_wrlock+0x3c5> - 44686c: 41 83 e0 04 and $0x4,%r8d -@@ -71747,8 +71812,9 @@ - 446878: ba ff ff ff 7f mov $0x7fffffff,%edx - 44687d: 48 89 df mov %rbx,%rdi - 446880: 40 80 f6 81 xor $0x81,%sil -- 446884: b8 ca 00 00 00 mov $0xca,%eax -- 446889: 0f 05 syscall ++ 44685e: 90 nop ++ 44685f: 90 nop + 446860: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 446866: 0f 87 f9 00 00 00 ja 446965 <___pthread_rwlock_wrlock+0x3c5> + 44686c: 41 83 e0 04 and $0x4,%r8d +@@ -71744,8 +71809,9 @@ + 446878: ba ff ff ff 7f mov $0x7fffffff,%edx + 44687d: 48 89 df mov %rbx,%rdi + 446880: 40 80 f6 81 xor $0x81,%sil +- 446884: b8 ca 00 00 00 mov $0xca,%eax +- 446889: 0f 05 syscall + 446884: -+ 446889: 90 nop -+ 44688a: 90 nop - 44688b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 446891: 0f 87 c1 00 00 00 ja 446958 <___pthread_rwlock_wrlock+0x3b8> - 446897: 41 b8 6e 00 00 00 mov $0x6e,%r8d -@@ -71786,8 +71852,9 @@ - 44691c: ba 01 00 00 00 mov $0x1,%edx - 446921: 4c 89 e7 mov %r12,%rdi - 446924: 40 80 f6 81 xor $0x81,%sil -- 446928: b8 ca 00 00 00 mov $0xca,%eax -- 44692d: 0f 05 syscall ++ 446889: 90 nop ++ 44688a: 90 nop + 44688b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 446891: 0f 87 c1 00 00 00 ja 446958 <___pthread_rwlock_wrlock+0x3b8> + 446897: 41 b8 6e 00 00 00 mov $0x6e,%r8d +@@ -71783,8 +71849,9 @@ + 44691c: ba 01 00 00 00 mov $0x1,%edx + 446921: 4c 89 e7 mov %r12,%rdi + 446924: 40 80 f6 81 xor $0x81,%sil +- 446928: b8 ca 00 00 00 mov $0xca,%eax +- 44692d: 0f 05 syscall + 446928: -+ 44692d: 90 nop -+ 44692e: 90 nop - 44692f: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 446935: 0f 86 e8 fc ff ff jbe 446623 <___pthread_rwlock_wrlock+0x83> - 44693b: 83 c0 16 add $0x16,%eax -@@ -71852,8 +71919,9 @@ - 446a05: 48 89 f0 mov %rsi,%rax - 446a08: 48 89 c6 mov %rax,%rsi - 446a0b: 41 ba 08 00 00 00 mov $0x8,%r10d -- 446a11: b8 0e 00 00 00 mov $0xe,%eax -- 446a16: 0f 05 syscall ++ 44692d: 90 nop ++ 44692e: 90 nop + 44692f: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 446935: 0f 86 e8 fc ff ff jbe 446623 <___pthread_rwlock_wrlock+0x83> + 44693b: 83 c0 16 add $0x16,%eax +@@ -71849,8 +71916,9 @@ + 446a05: 48 89 f0 mov %rsi,%rax + 446a08: 48 89 c6 mov %rax,%rsi + 446a0b: 41 ba 08 00 00 00 mov $0x8,%r10d +- 446a11: b8 0e 00 00 00 mov $0xe,%eax +- 446a16: 0f 05 syscall + 446a11: -+ 446a16: 90 nop -+ 446a17: 90 nop - 446a18: 89 c2 mov %eax,%edx - 446a1a: f7 da neg %edx - 446a1c: 3d 00 f0 ff ff cmp $0xfffff000,%eax -@@ -93239,8 +93307,9 @@ - 45ba24: b8 ff ff ff 7f mov $0x7fffffff,%eax - 45ba29: 48 39 c2 cmp %rax,%rdx - 45ba2c: 48 0f 47 d0 cmova %rax,%rdx -- 45ba30: b8 d9 00 00 00 mov $0xd9,%eax -- 45ba35: 0f 05 syscall ++ 446a16: 90 nop ++ 446a17: 90 nop + 446a18: 89 c2 mov %eax,%edx + 446a1a: f7 da neg %edx + 446a1c: 3d 00 f0 ff ff cmp $0xfffff000,%eax +@@ -93236,8 +93304,9 @@ + 45ba24: b8 ff ff ff 7f mov $0x7fffffff,%eax + 45ba29: 48 39 c2 cmp %rax,%rdx + 45ba2c: 48 0f 47 d0 cmova %rax,%rdx +- 45ba30: b8 d9 00 00 00 mov $0xd9,%eax +- 45ba35: 0f 05 syscall + 45ba30: -+ 45ba35: 90 nop -+ 45ba36: 90 nop - 45ba37: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45ba3d: 77 01 ja 45ba40 <__getdents+0x20> - 45ba3f: c3 ret -@@ -93332,8 +93401,9 @@ ++ 45ba35: 90 nop ++ 45ba36: 90 nop + 45ba37: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45ba3d: 77 01 ja 45ba40 <__getdents+0x20> + 45ba3f: c3 ret +@@ -93329,8 +93398,9 @@ 000000000045bb50 <__getpid>: - 45bb50: f3 0f 1e fa endbr64 -- 45bb54: b8 27 00 00 00 mov $0x27,%eax -- 45bb59: 0f 05 syscall + 45bb50: f3 0f 1e fa endbr64 +- 45bb54: b8 27 00 00 00 mov $0x27,%eax +- 45bb59: 0f 05 syscall + 45bb54: -+ 45bb59: 90 nop -+ 45bb5a: 90 nop - 45bb5b: c3 ret - 45bb5c: 0f 1f 40 00 nopl 0x0(%rax) ++ 45bb59: 90 nop ++ 45bb5a: 90 nop + 45bb5b: c3 ret + 45bb5c: 0f 1f 40 00 nopl 0x0(%rax) -@@ -93365,8 +93435,9 @@ +@@ -93362,8 +93432,9 @@ 000000000045bba0 <__sched_getparam>: - 45bba0: f3 0f 1e fa endbr64 -- 45bba4: b8 8f 00 00 00 mov $0x8f,%eax -- 45bba9: 0f 05 syscall + 45bba0: f3 0f 1e fa endbr64 +- 45bba4: b8 8f 00 00 00 mov $0x8f,%eax +- 45bba9: 0f 05 syscall + 45bba4: -+ 45bba9: 90 nop -+ 45bbaa: 90 nop - 45bbab: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 45bbb1: 73 01 jae 45bbb4 <__sched_getparam+0x14> - 45bbb3: c3 ret -@@ -93381,8 +93452,9 @@ ++ 45bba9: 90 nop ++ 45bbaa: 90 nop + 45bbab: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 45bbb1: 73 01 jae 45bbb4 <__sched_getparam+0x14> + 45bbb3: c3 ret +@@ -93378,8 +93449,9 @@ 000000000045bbd0 <__sched_getscheduler>: - 45bbd0: f3 0f 1e fa endbr64 -- 45bbd4: b8 91 00 00 00 mov $0x91,%eax -- 45bbd9: 0f 05 syscall + 45bbd0: f3 0f 1e fa endbr64 +- 45bbd4: b8 91 00 00 00 mov $0x91,%eax +- 45bbd9: 0f 05 syscall + 45bbd4: -+ 45bbd9: 90 nop -+ 45bbda: 90 nop - 45bbdb: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 45bbe1: 73 01 jae 45bbe4 <__sched_getscheduler+0x14> - 45bbe3: c3 ret -@@ -93397,8 +93469,9 @@ ++ 45bbd9: 90 nop ++ 45bbda: 90 nop + 45bbdb: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 45bbe1: 73 01 jae 45bbe4 <__sched_getscheduler+0x14> + 45bbe3: c3 ret +@@ -93394,8 +93466,9 @@ 000000000045bc00 <__sched_get_priority_max>: - 45bc00: f3 0f 1e fa endbr64 -- 45bc04: b8 92 00 00 00 mov $0x92,%eax -- 45bc09: 0f 05 syscall + 45bc00: f3 0f 1e fa endbr64 +- 45bc04: b8 92 00 00 00 mov $0x92,%eax +- 45bc09: 0f 05 syscall + 45bc04: -+ 45bc09: 90 nop -+ 45bc0a: 90 nop - 45bc0b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 45bc11: 73 01 jae 45bc14 <__sched_get_priority_max+0x14> - 45bc13: c3 ret -@@ -93413,8 +93486,9 @@ ++ 45bc09: 90 nop ++ 45bc0a: 90 nop + 45bc0b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 45bc11: 73 01 jae 45bc14 <__sched_get_priority_max+0x14> + 45bc13: c3 ret +@@ -93410,8 +93483,9 @@ 000000000045bc30 <__sched_get_priority_min>: - 45bc30: f3 0f 1e fa endbr64 -- 45bc34: b8 93 00 00 00 mov $0x93,%eax -- 45bc39: 0f 05 syscall + 45bc30: f3 0f 1e fa endbr64 +- 45bc34: b8 93 00 00 00 mov $0x93,%eax +- 45bc39: 0f 05 syscall + 45bc34: -+ 45bc39: 90 nop -+ 45bc3a: 90 nop - 45bc3b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 45bc41: 73 01 jae 45bc44 <__sched_get_priority_min+0x14> - 45bc43: c3 ret -@@ -93429,8 +93503,9 @@ ++ 45bc39: 90 nop ++ 45bc3a: 90 nop + 45bc3b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 45bc41: 73 01 jae 45bc44 <__sched_get_priority_min+0x14> + 45bc43: c3 ret +@@ -93426,8 +93500,9 @@ 000000000045bc60 <__sched_setscheduler>: - 45bc60: f3 0f 1e fa endbr64 -- 45bc64: b8 90 00 00 00 mov $0x90,%eax -- 45bc69: 0f 05 syscall + 45bc60: f3 0f 1e fa endbr64 +- 45bc64: b8 90 00 00 00 mov $0x90,%eax +- 45bc69: 0f 05 syscall + 45bc64: -+ 45bc69: 90 nop -+ 45bc6a: 90 nop - 45bc6b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax - 45bc71: 73 01 jae 45bc74 <__sched_setscheduler+0x14> - 45bc73: c3 ret -@@ -93477,8 +93552,9 @@ - 45bd00: 48 89 85 08 ff ff ff mov %rax,-0xf8(%rbp) - 45bd07: 0f 84 21 04 00 00 je 45c12e <__getcwd+0x49e> - 45bd0d: 48 8b bd 08 ff ff ff mov -0xf8(%rbp),%rdi -- 45bd14: b8 4f 00 00 00 mov $0x4f,%eax -- 45bd19: 0f 05 syscall ++ 45bc69: 90 nop ++ 45bc6a: 90 nop + 45bc6b: 48 3d 01 f0 ff ff cmp $0xfffffffffffff001,%rax + 45bc71: 73 01 jae 45bc74 <__sched_setscheduler+0x14> + 45bc73: c3 ret +@@ -93474,8 +93549,9 @@ + 45bd00: 48 89 85 08 ff ff ff mov %rax,-0xf8(%rbp) + 45bd07: 0f 84 21 04 00 00 je 45c12e <__getcwd+0x49e> + 45bd0d: 48 8b bd 08 ff ff ff mov -0xf8(%rbp),%rdi +- 45bd14: b8 4f 00 00 00 mov $0x4f,%eax +- 45bd19: 0f 05 syscall + 45bd14: -+ 45bd19: 90 nop -+ 45bd1a: 90 nop - 45bd1b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45bd21: 0f 87 85 05 00 00 ja 45c2ac <__getcwd+0x61c> - 45bd27: 85 c0 test %eax,%eax -@@ -93914,8 +93990,9 @@ ++ 45bd19: 90 nop ++ 45bd1a: 90 nop + 45bd1b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45bd21: 0f 87 85 05 00 00 ja 45c2ac <__getcwd+0x61c> + 45bd27: 85 c0 test %eax,%eax +@@ -93911,8 +93987,9 @@ 000000000045c510 <__libc_lseek>: - 45c510: f3 0f 1e fa endbr64 -- 45c514: b8 08 00 00 00 mov $0x8,%eax -- 45c519: 0f 05 syscall + 45c510: f3 0f 1e fa endbr64 +- 45c514: b8 08 00 00 00 mov $0x8,%eax +- 45c519: 0f 05 syscall + 45c514: -+ 45c519: 90 nop -+ 45c51a: 90 nop - 45c51b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c521: 77 05 ja 45c528 <__libc_lseek+0x18> - 45c523: c3 ret -@@ -93962,8 +94039,9 @@ - 45c5a4: 89 da mov %ebx,%edx - 45c5a6: 4c 89 e6 mov %r12,%rsi - 45c5a9: bf 9c ff ff ff mov $0xffffff9c,%edi -- 45c5ae: b8 01 01 00 00 mov $0x101,%eax -- 45c5b3: 0f 05 syscall ++ 45c519: 90 nop ++ 45c51a: 90 nop + 45c51b: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c521: 77 05 ja 45c528 <__libc_lseek+0x18> + 45c523: c3 ret +@@ -93959,8 +94036,9 @@ + 45c5a4: 89 da mov %ebx,%edx + 45c5a6: 4c 89 e6 mov %r12,%rsi + 45c5a9: bf 9c ff ff ff mov $0xffffff9c,%edi +- 45c5ae: b8 01 01 00 00 mov $0x101,%eax +- 45c5b3: 0f 05 syscall + 45c5ae: -+ 45c5b3: 90 nop -+ 45c5b4: 90 nop - 45c5b5: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c5bb: 0f 87 7f 00 00 00 ja 45c640 <__libc_open+0xe0> - 45c5c1: 48 8b 55 b8 mov -0x48(%rbp),%rdx -@@ -93991,8 +94069,9 @@ - 45c613: 4c 89 e6 mov %r12,%rsi - 45c616: 41 89 c0 mov %eax,%r8d - 45c619: bf 9c ff ff ff mov $0xffffff9c,%edi -- 45c61e: b8 01 01 00 00 mov $0x101,%eax -- 45c623: 0f 05 syscall ++ 45c5b3: 90 nop ++ 45c5b4: 90 nop + 45c5b5: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c5bb: 0f 87 7f 00 00 00 ja 45c640 <__libc_open+0xe0> + 45c5c1: 48 8b 55 b8 mov -0x48(%rbp),%rdx +@@ -93988,8 +94066,9 @@ + 45c613: 4c 89 e6 mov %r12,%rsi + 45c616: 41 89 c0 mov %eax,%r8d + 45c619: bf 9c ff ff ff mov $0xffffff9c,%edi +- 45c61e: b8 01 01 00 00 mov $0x101,%eax +- 45c623: 0f 05 syscall + 45c61e: -+ 45c623: 90 nop -+ 45c624: 90 nop - 45c625: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c62b: 77 33 ja 45c660 <__libc_open+0x100> - 45c62d: 44 89 c7 mov %r8d,%edi -@@ -94036,8 +94115,9 @@ - 45c6b0: 74 36 je 45c6e8 <__libc_openat64+0x68> - 45c6b2: 80 3d df e3 04 00 00 cmpb $0x0,0x4e3df(%rip) # 4aaa98 <__libc_single_threaded> - 45c6b9: 74 51 je 45c70c <__libc_openat64+0x8c> -- 45c6bb: b8 01 01 00 00 mov $0x101,%eax -- 45c6c0: 0f 05 syscall ++ 45c623: 90 nop ++ 45c624: 90 nop + 45c625: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c62b: 77 33 ja 45c660 <__libc_open+0x100> + 45c62d: 44 89 c7 mov %r8d,%edi +@@ -94033,8 +94112,9 @@ + 45c6b0: 74 36 je 45c6e8 <__libc_openat64+0x68> + 45c6b2: 80 3d df e3 04 00 00 cmpb $0x0,0x4e3df(%rip) + 45c6b9: 74 51 je 45c70c <__libc_openat64+0x8c> +- 45c6bb: b8 01 01 00 00 mov $0x101,%eax +- 45c6c0: 0f 05 syscall + 45c6bb: -+ 45c6c0: 90 nop -+ 45c6c1: 90 nop - 45c6c2: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c6c8: 0f 87 8a 00 00 00 ja 45c758 <__libc_openat64+0xd8> - 45c6ce: 48 8b 55 c8 mov -0x38(%rbp),%rdx -@@ -94065,8 +94145,9 @@ - 45c726: 41 89 c0 mov %eax,%r8d - 45c729: 48 8b 75 a0 mov -0x60(%rbp),%rsi - 45c72d: 8b 7d a8 mov -0x58(%rbp),%edi -- 45c730: b8 01 01 00 00 mov $0x101,%eax -- 45c735: 0f 05 syscall ++ 45c6c0: 90 nop ++ 45c6c1: 90 nop + 45c6c2: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c6c8: 0f 87 8a 00 00 00 ja 45c758 <__libc_openat64+0xd8> + 45c6ce: 48 8b 55 c8 mov -0x38(%rbp),%rdx +@@ -94062,8 +94142,9 @@ + 45c726: 41 89 c0 mov %eax,%r8d + 45c729: 48 8b 75 a0 mov -0x60(%rbp),%rsi + 45c72d: 8b 7d a8 mov -0x58(%rbp),%edi +- 45c730: b8 01 01 00 00 mov $0x101,%eax +- 45c735: 0f 05 syscall + 45c730: -+ 45c735: 90 nop -+ 45c736: 90 nop - 45c737: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c73d: 77 31 ja 45c770 <__libc_openat64+0xf0> - 45c73f: 44 89 c7 mov %r8d,%edi -@@ -94095,8 +94176,10 @@ - 45c794: 80 3d fd e2 04 00 00 cmpb $0x0,0x4e2fd(%rip) # 4aaa98 <__libc_single_threaded> - 45c79b: 74 13 je 45c7b0 <__libc_read+0x20> - 45c79d: 31 c0 xor %eax,%eax -- 45c79f: 0f 05 syscall -- 45c7a1: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax ++ 45c735: 90 nop ++ 45c736: 90 nop + 45c737: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c73d: 77 31 ja 45c770 <__libc_openat64+0xf0> + 45c73f: 44 89 c7 mov %r8d,%edi +@@ -94092,8 +94173,10 @@ + 45c794: 80 3d fd e2 04 00 00 cmpb $0x0,0x4e2fd(%rip) + 45c79b: 74 13 je 45c7b0 <__libc_read+0x20> + 45c79d: 31 c0 xor %eax,%eax +- 45c79f: 0f 05 syscall +- 45c7a1: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c79f: -+ 45c7a4: 90 nop -+ 45c7a5: 90 nop -+ 45c7a6: 90 nop - 45c7a7: 77 4f ja 45c7f8 <__libc_read+0x68> - 45c7a9: c3 ret - 45c7aa: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) -@@ -94110,9 +94193,9 @@ - 45c7c8: 48 8b 55 e8 mov -0x18(%rbp),%rdx - 45c7cc: 48 8b 75 f0 mov -0x10(%rbp),%rsi - 45c7d0: 41 89 c0 mov %eax,%r8d -- 45c7d3: 8b 7d f8 mov -0x8(%rbp),%edi -- 45c7d6: 31 c0 xor %eax,%eax -- 45c7d8: 0f 05 syscall ++ 45c7a4: 90 nop ++ 45c7a5: 90 nop ++ 45c7a6: 90 nop + 45c7a7: 77 4f ja 45c7f8 <__libc_read+0x68> + 45c7a9: c3 ret + 45c7aa: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) +@@ -94107,9 +94190,9 @@ + 45c7c8: 48 8b 55 e8 mov -0x18(%rbp),%rdx + 45c7cc: 48 8b 75 f0 mov -0x10(%rbp),%rsi + 45c7d0: 41 89 c0 mov %eax,%r8d +- 45c7d3: 8b 7d f8 mov -0x8(%rbp),%edi +- 45c7d6: 31 c0 xor %eax,%eax +- 45c7d8: 0f 05 syscall + 45c7d3: -+ 45c7d8: 90 nop -+ 45c7d9: 90 nop - 45c7da: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c7e0: 77 2e ja 45c810 <__libc_read+0x80> - 45c7e2: 44 89 c7 mov %r8d,%edi -@@ -94151,8 +94234,9 @@ - 45c850: f3 0f 1e fa endbr64 - 45c854: 80 3d 3d e2 04 00 00 cmpb $0x0,0x4e23d(%rip) # 4aaa98 <__libc_single_threaded> - 45c85b: 74 13 je 45c870 <__libc_write+0x20> -- 45c85d: b8 01 00 00 00 mov $0x1,%eax -- 45c862: 0f 05 syscall ++ 45c7d8: 90 nop ++ 45c7d9: 90 nop + 45c7da: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c7e0: 77 2e ja 45c810 <__libc_read+0x80> + 45c7e2: 44 89 c7 mov %r8d,%edi +@@ -94148,8 +94231,9 @@ + 45c850: f3 0f 1e fa endbr64 + 45c854: 80 3d 3d e2 04 00 00 cmpb $0x0,0x4e23d(%rip) + 45c85b: 74 13 je 45c870 <__libc_write+0x20> +- 45c85d: b8 01 00 00 00 mov $0x1,%eax +- 45c862: 0f 05 syscall + 45c85d: -+ 45c862: 90 nop -+ 45c863: 90 nop - 45c864: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c86a: 77 54 ja 45c8c0 <__libc_write+0x70> - 45c86c: c3 ret -@@ -94168,8 +94252,9 @@ - 45c88c: 48 8b 75 f0 mov -0x10(%rbp),%rsi - 45c890: 41 89 c0 mov %eax,%r8d - 45c893: 8b 7d f8 mov -0x8(%rbp),%edi -- 45c896: b8 01 00 00 00 mov $0x1,%eax -- 45c89b: 0f 05 syscall ++ 45c862: 90 nop ++ 45c863: 90 nop + 45c864: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c86a: 77 54 ja 45c8c0 <__libc_write+0x70> + 45c86c: c3 ret +@@ -94165,8 +94249,9 @@ + 45c88c: 48 8b 75 f0 mov -0x10(%rbp),%rsi + 45c890: 41 89 c0 mov %eax,%r8d + 45c893: 8b 7d f8 mov -0x8(%rbp),%edi +- 45c896: b8 01 00 00 00 mov $0x1,%eax +- 45c89b: 0f 05 syscall + 45c896: -+ 45c89b: 90 nop -+ 45c89c: 90 nop - 45c89d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c8a3: 77 33 ja 45c8d8 <__libc_write+0x88> - 45c8a5: 44 89 c7 mov %r8d,%edi -@@ -94210,8 +94295,9 @@ - 45c919: f7 d0 not %eax - 45c91b: a9 00 00 41 00 test $0x410000,%eax - 45c920: 74 26 je 45c948 <__openat64_nocancel+0x58> -- 45c922: b8 01 01 00 00 mov $0x101,%eax -- 45c927: 0f 05 syscall ++ 45c89b: 90 nop ++ 45c89c: 90 nop + 45c89d: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c8a3: 77 33 ja 45c8d8 <__libc_write+0x88> + 45c8a5: 44 89 c7 mov %r8d,%edi +@@ -94207,8 +94292,9 @@ + 45c919: f7 d0 not %eax + 45c91b: a9 00 00 41 00 test $0x410000,%eax + 45c920: 74 26 je 45c948 <__openat64_nocancel+0x58> +- 45c922: b8 01 01 00 00 mov $0x101,%eax +- 45c927: 0f 05 syscall + 45c922: -+ 45c927: 90 nop -+ 45c928: 90 nop - 45c929: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c92f: 77 37 ja 45c968 <__openat64_nocancel+0x78> - 45c931: 48 8b 55 c8 mov -0x38(%rbp),%rdx -@@ -94239,8 +94325,9 @@ ++ 45c927: 90 nop ++ 45c928: 90 nop + 45c929: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c92f: 77 37 ja 45c968 <__openat64_nocancel+0x78> + 45c931: 48 8b 55 c8 mov -0x38(%rbp),%rdx +@@ -94236,8 +94322,9 @@ 000000000045c980 <__pread64_nocancel>: - 45c980: f3 0f 1e fa endbr64 - 45c984: 49 89 ca mov %rcx,%r10 -- 45c987: b8 11 00 00 00 mov $0x11,%eax -- 45c98c: 0f 05 syscall + 45c980: f3 0f 1e fa endbr64 + 45c984: 49 89 ca mov %rcx,%r10 +- 45c987: b8 11 00 00 00 mov $0x11,%eax +- 45c98c: 0f 05 syscall + 45c987: -+ 45c98c: 90 nop -+ 45c98d: 90 nop - 45c98e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c994: 77 0a ja 45c9a0 <__pread64_nocancel+0x20> - 45c996: c3 ret -@@ -94257,8 +94344,9 @@ ++ 45c98c: 90 nop ++ 45c98d: 90 nop + 45c98e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c994: 77 0a ja 45c9a0 <__pread64_nocancel+0x20> + 45c996: c3 ret +@@ -94254,8 +94341,9 @@ 000000000045c9c0 <__write_nocancel>: - 45c9c0: f3 0f 1e fa endbr64 -- 45c9c4: b8 01 00 00 00 mov $0x1,%eax -- 45c9c9: 0f 05 syscall + 45c9c0: f3 0f 1e fa endbr64 +- 45c9c4: b8 01 00 00 00 mov $0x1,%eax +- 45c9c9: 0f 05 syscall + 45c9c4: -+ 45c9c9: 90 nop -+ 45c9ca: 90 nop - 45c9cb: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45c9d1: 77 05 ja 45c9d8 <__write_nocancel+0x18> - 45c9d3: c3 ret -@@ -94282,8 +94370,9 @@ - 45ca0d: 48 89 45 f8 mov %rax,-0x8(%rbp) - 45ca11: 31 c0 xor %eax,%eax - 45ca13: 48 8d 55 d0 lea -0x30(%rbp),%rdx -- 45ca17: b8 10 00 00 00 mov $0x10,%eax -- 45ca1c: 0f 05 syscall ++ 45c9c9: 90 nop ++ 45c9ca: 90 nop + 45c9cb: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45c9d1: 77 05 ja 45c9d8 <__write_nocancel+0x18> + 45c9d3: c3 ret +@@ -94279,8 +94367,9 @@ + 45ca0d: 48 89 45 f8 mov %rax,-0x8(%rbp) + 45ca11: 31 c0 xor %eax,%eax + 45ca13: 48 8d 55 d0 lea -0x30(%rbp),%rdx +- 45ca17: b8 10 00 00 00 mov $0x10,%eax +- 45ca1c: 0f 05 syscall + 45ca17: -+ 45ca1c: 90 nop -+ 45ca1d: 90 nop - 45ca1e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45ca24: 77 6a ja 45ca90 <__tcgetattr+0xa0> - 45ca26: 89 c2 mov %eax,%edx -@@ -94329,9 +94418,11 @@ - 45cab4: 49 89 f2 mov %rsi,%r10 - 45cab7: 31 d2 xor %edx,%edx - 45cab9: 89 fe mov %edi,%esi -- 45cabb: b8 2e 01 00 00 mov $0x12e,%eax -- 45cac0: 31 ff xor %edi,%edi -- 45cac2: 0f 05 syscall ++ 45ca1c: 90 nop ++ 45ca1d: 90 nop + 45ca1e: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45ca24: 77 6a ja 45ca90 <__tcgetattr+0xa0> + 45ca26: 89 c2 mov %eax,%edx +@@ -94326,9 +94415,11 @@ + 45cab4: 49 89 f2 mov %rsi,%r10 + 45cab7: 31 d2 xor %edx,%edx + 45cab9: 89 fe mov %edi,%esi +- 45cabb: b8 2e 01 00 00 mov $0x12e,%eax +- 45cac0: 31 ff xor %edi,%edi +- 45cac2: 0f 05 syscall + 45cabb: -+ 45cac0: 90 nop -+ 45cac1: 90 nop -+ 45cac2: 90 nop -+ 45cac3: 90 nop - 45cac4: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 45caca: 77 04 ja 45cad0 <__GI___getrlimit+0x20> - 45cacc: c3 ret -@@ -97928,8 +98019,9 @@ - 45ff97: 64 48 8b 04 25 10 00 mov %fs:0x10,%rax ++ 45cac0: 90 nop ++ 45cac1: 90 nop ++ 45cac2: 90 nop ++ 45cac3: 90 nop + 45cac4: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 45caca: 77 04 ja 45cad0 <__gi___getrlimit+0x20> + 45cacc: c3 ret +@@ -97925,8 +98016,9 @@ + 45ff97: 64 48 8b 04 25 10 00 mov %fs:0x10,%rax 45ff9e: 00 00 - 45ffa0: 48 8d 78 1c lea 0x1c(%rax),%rdi -- 45ffa4: b8 ca 00 00 00 mov $0xca,%eax -- 45ffa9: 0f 05 syscall + 45ffa0: 48 8d 78 1c lea 0x1c(%rax),%rdi +- 45ffa4: b8 ca 00 00 00 mov $0xca,%eax +- 45ffa9: 0f 05 syscall + 45ffa4: -+ 45ffa9: 90 nop -+ 45ffaa: 90 nop - 45ffab: 48 8d 3d 6e ab 04 00 lea 0x4ab6e(%rip),%rdi # 4aab20 <_dl_load_lock> - 45ffb2: 44 89 8d 44 ff ff ff mov %r9d,-0xbc(%rbp) - 45ffb9: 4c 89 85 48 ff ff ff mov %r8,-0xb8(%rbp) -@@ -100864,8 +100956,7 @@ - 463062: 45 31 d2 xor %r10d,%r10d - 463065: ba 02 00 00 00 mov $0x2,%edx - 46306a: be 80 00 00 00 mov $0x80,%esi -- 46306f: 44 89 c8 mov %r9d,%eax -- 463072: 0f 05 syscall ++ 45ffa9: 90 nop ++ 45ffaa: 90 nop + 45ffab: 48 8d 3d 6e ab 04 00 lea 0x4ab6e(%rip),%rdi + 45ffb2: 44 89 8d 44 ff ff ff mov %r9d,-0xbc(%rbp) + 45ffb9: 4c 89 85 48 ff ff ff mov %r8,-0xb8(%rbp) +@@ -100861,8 +100953,7 @@ + 463062: 45 31 d2 xor %r10d,%r10d + 463065: ba 02 00 00 00 mov $0x2,%edx + 46306a: be 80 00 00 00 mov $0x80,%esi +- 46306f: 44 89 c8 mov %r9d,%eax +- 463072: 0f 05 syscall + 46306f: - 463074: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 46307a: 76 dc jbe 463058 <__thread_gscope_wait+0x88> - 46307c: 83 f8 f5 cmp $0xfffffff5,%eax -@@ -100904,8 +100995,7 @@ - 463102: 45 31 d2 xor %r10d,%r10d - 463105: ba 02 00 00 00 mov $0x2,%edx - 46310a: be 80 00 00 00 mov $0x80,%esi -- 46310f: 44 89 c8 mov %r9d,%eax -- 463112: 0f 05 syscall + 463074: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 46307a: 76 dc jbe 463058 <__thread_gscope_wait+0x88> + 46307c: 83 f8 f5 cmp $0xfffffff5,%eax +@@ -100901,8 +100992,7 @@ + 463102: 45 31 d2 xor %r10d,%r10d + 463105: ba 02 00 00 00 mov $0x2,%edx + 46310a: be 80 00 00 00 mov $0x80,%esi +- 46310f: 44 89 c8 mov %r9d,%eax +- 463112: 0f 05 syscall + 46310f: - 463114: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 46311a: 76 dc jbe 4630f8 <__thread_gscope_wait+0x128> - 46311c: 83 f8 f5 cmp $0xfffffff5,%eax -@@ -104731,8 +104821,11 @@ - 4669cc: 0f 1f 40 00 nopl 0x0(%rax) + 463114: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 46311a: 76 dc jbe 4630f8 <__thread_gscope_wait+0x128> + 46311c: 83 f8 f5 cmp $0xfffffff5,%eax +@@ -104728,8 +104818,11 @@ + 4669cc: 0f 1f 40 00 nopl 0x0(%rax) 00000000004669d0 <__restore_rt>: -- 4669d0: 48 c7 c0 0f 00 00 00 mov $0xf,%rax -- 4669d7: 0f 05 syscall +- 4669d0: 48 c7 c0 0f 00 00 00 mov $0xf,%rax +- 4669d7: 0f 05 syscall + 4669d0: -+ 4669d5: 90 nop -+ 4669d6: 90 nop -+ 4669d7: 90 nop -+ 4669d8: 90 nop - 4669d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) ++ 4669d5: 90 nop ++ 4669d6: 90 nop ++ 4669d7: 90 nop ++ 4669d8: 90 nop + 4669d9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax) 00000000004669e0 <__libc_sigaction>: -@@ -104776,8 +104869,9 @@ - 466a9f: 0f 11 b5 38 ff ff ff movups %xmm6,-0xc8(%rbp) - 466aa6: 0f 11 bd 48 ff ff ff movups %xmm7,-0xb8(%rbp) - 466aad: 41 ba 08 00 00 00 mov $0x8,%r10d -- 466ab3: b8 0d 00 00 00 mov $0xd,%eax -- 466ab8: 0f 05 syscall +@@ -104773,8 +104866,9 @@ + 466a9f: 0f 11 b5 38 ff ff ff movups %xmm6,-0xc8(%rbp) + 466aa6: 0f 11 bd 48 ff ff ff movups %xmm7,-0xb8(%rbp) + 466aad: 41 ba 08 00 00 00 mov $0x8,%r10d +- 466ab3: b8 0d 00 00 00 mov $0xd,%eax +- 466ab8: 0f 05 syscall + 466ab3: -+ 466ab8: 90 nop -+ 466ab9: 90 nop - 466aba: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 466ac0: 0f 87 ba 00 00 00 ja 466b80 <__libc_sigaction+0x1a0> - 466ac6: 89 c2 mov %eax,%edx -@@ -111544,8 +111638,7 @@ - 46cb11: 45 31 d2 xor %r10d,%r10d - 46cb14: 89 ca mov %ecx,%edx - 46cb16: be 80 00 00 00 mov $0x80,%esi -- 46cb1b: 44 89 c0 mov %r8d,%eax -- 46cb1e: 0f 05 syscall ++ 466ab8: 90 nop ++ 466ab9: 90 nop + 466aba: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 466ac0: 0f 87 ba 00 00 00 ja 466b80 <__libc_sigaction+0x1a0> + 466ac6: 89 c2 mov %eax,%edx +@@ -111541,8 +111635,7 @@ + 46cb11: 45 31 d2 xor %r10d,%r10d + 46cb14: 89 ca mov %ecx,%edx + 46cb16: be 80 00 00 00 mov $0x80,%esi +- 46cb1b: 44 89 c0 mov %r8d,%eax +- 46cb1e: 0f 05 syscall + 46cb1b: - 46cb20: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax - 46cb26: 77 0d ja 46cb35 <__pthread_disable_asynccancel+0x65> - 46cb28: 8b 0f mov (%rdi),%ecx -@@ -111701,8 +111794,7 @@ - 46ccd5: 81 f6 00 01 00 00 xor $0x100,%esi - 46ccdb: 40 80 ce 89 or $0x89,%sil - 46ccdf: 44 31 c6 xor %r8d,%esi -- 46cce2: 45 31 c0 xor %r8d,%r8d -- 46cce5: 0f 05 syscall + 46cb20: 48 3d 00 f0 ff ff cmp $0xfffffffffffff000,%rax + 46cb26: 77 0d ja 46cb35 <__pthread_disable_asynccancel+0x65> + 46cb28: 8b 0f mov (%rdi),%ecx +@@ -111698,8 +111791,7 @@ + 46ccd5: 81 f6 00 01 00 00 xor $0x100,%esi + 46ccdb: 40 80 ce 89 or $0x89,%sil + 46ccdf: 44 31 c6 xor %r8d,%esi +- 46cce2: 45 31 c0 xor %r8d,%r8d +- 46cce5: 0f 05 syscall + 46cce2: - 46cce7: 85 c0 test %eax,%eax - 46cce9: 7f 27 jg 46cd12 <__futex_abstimed_wait64+0x62> - 46cceb: 83 f8 ea cmp $0xffffffea,%eax -@@ -111756,8 +111848,9 @@ - 46cd83: 45 31 c0 xor %r8d,%r8d - 46cd86: 49 89 ca mov %rcx,%r10 - 46cd89: 44 89 e2 mov %r12d,%edx -- 46cd8c: b8 ca 00 00 00 mov $0xca,%eax -- 46cd91: 0f 05 syscall + 46cce7: 85 c0 test %eax,%eax + 46cce9: 7f 27 jg 46cd12 <__futex_abstimed_wait64+0x62> + 46cceb: 83 f8 ea cmp $0xffffffea,%eax +@@ -111753,8 +111845,9 @@ + 46cd83: 45 31 c0 xor %r8d,%r8d + 46cd86: 49 89 ca mov %rcx,%r10 + 46cd89: 44 89 e2 mov %r12d,%edx +- 46cd8c: b8 ca 00 00 00 mov $0xca,%eax +- 46cd91: 0f 05 syscall + 46cd8c: -+ 46cd91: 90 nop -+ 46cd92: 90 nop - 46cd93: 48 89 c3 mov %rax,%rbx - 46cd96: 89 d8 mov %ebx,%eax - 46cd98: 85 db test %ebx,%ebx -@@ -111802,8 +111895,9 @@ - 46ce0d: 48 8b 7d d0 mov -0x30(%rbp),%rdi - 46ce11: 41 b9 ff ff ff ff mov $0xffffffff,%r9d - 46ce17: 44 89 e2 mov %r12d,%edx -- 46ce1a: b8 ca 00 00 00 mov $0xca,%eax -- 46ce1f: 0f 05 syscall ++ 46cd91: 90 nop ++ 46cd92: 90 nop + 46cd93: 48 89 c3 mov %rax,%rbx + 46cd96: 89 d8 mov %ebx,%eax + 46cd98: 85 db test %ebx,%ebx +@@ -111799,8 +111892,9 @@ + 46ce0d: 48 8b 7d d0 mov -0x30(%rbp),%rdi + 46ce11: 41 b9 ff ff ff ff mov $0xffffffff,%r9d + 46ce17: 44 89 e2 mov %r12d,%edx +- 46ce1a: b8 ca 00 00 00 mov $0xca,%eax +- 46ce1f: 0f 05 syscall + 46ce1a: -+ 46ce1f: 90 nop -+ 46ce20: 90 nop - 46ce21: 44 89 ef mov %r13d,%edi - 46ce24: 48 89 c3 mov %rax,%rbx - 46ce27: e8 a4 fc ff ff call 46cad0 <__pthread_disable_asynccancel> -@@ -111827,8 +111921,9 @@ - 46ce66: 48 85 d2 test %rdx,%rdx - 46ce69: 0f 45 f1 cmovne %ecx,%esi - 46ce6c: 31 d2 xor %edx,%edx -- 46ce6e: b8 ca 00 00 00 mov $0xca,%eax -- 46ce73: 0f 05 syscall ++ 46ce1f: 90 nop ++ 46ce20: 90 nop + 46ce21: 44 89 ef mov %r13d,%edi + 46ce24: 48 89 c3 mov %rax,%rbx + 46ce27: e8 a4 fc ff ff call 46cad0 <__pthread_disable_asynccancel> +@@ -111824,8 +111918,9 @@ + 46ce66: 48 85 d2 test %rdx,%rdx + 46ce69: 0f 45 f1 cmovne %ecx,%esi + 46ce6c: 31 d2 xor %edx,%edx +- 46ce6e: b8 ca 00 00 00 mov $0xca,%eax +- 46ce73: 0f 05 syscall + 46ce6e: -+ 46ce73: 90 nop -+ 46ce74: 90 nop - 46ce75: 83 f8 da cmp $0xffffffda,%eax - 46ce78: 74 26 je 46cea0 <__futex_lock_pi64+0x50> - 46ce7a: 83 f8 92 cmp $0xffffff92,%eax -@@ -114436,8 +114531,9 @@ ++ 46ce73: 90 nop ++ 46ce74: 90 nop + 46ce75: 83 f8 da cmp $0xffffffda,%eax + 46ce78: 74 26 je 46cea0 <__futex_lock_pi64+0x50> + 46ce7a: 83 f8 92 cmp $0xffffff92,%eax +@@ -114433,8 +114528,9 @@ 000000000046f340 <__GI___fstatat>: - 46f340: f3 0f 1e fa endbr64 - 46f344: 41 89 ca mov %ecx,%r10d -- 46f347: b8 06 01 00 00 mov $0x106,%eax -- 46f34c: 0f 05 syscall + 46f340: f3 0f 1e fa endbr64 + 46f344: 41 89 ca mov %ecx,%r10d +- 46f347: b8 06 01 00 00 mov $0x106,%eax +- 46f34c: 0f 05 syscall + 46f347: -+ 46f34c: 90 nop -+ 46f34d: 90 nop - 46f34e: 3d 00 f0 ff ff cmp $0xfffff000,%eax - 46f353: 77 0b ja 46f360 <__GI___fstatat+0x20> - 46f355: 31 c0 xor %eax,%eax -@@ -117945,8 +118041,9 @@ - 47296c: 64 48 8b 04 25 10 00 mov %fs:0x10,%rax ++ 46f34c: 90 nop ++ 46f34d: 90 nop + 46f34e: 3d 00 f0 ff ff cmp $0xfffff000,%eax + 46f353: 77 0b ja 46f360 <__gi___fstatat+0x20> + 46f355: 31 c0 xor %eax,%eax +@@ -117942,8 +118038,9 @@ + 47296c: 64 48 8b 04 25 10 00 mov %fs:0x10,%rax 472973: 00 00 - 472975: 48 8d 78 1c lea 0x1c(%rax),%rdi -- 472979: b8 ca 00 00 00 mov $0xca,%eax -- 47297e: 0f 05 syscall + 472975: 48 8d 78 1c lea 0x1c(%rax),%rdi +- 472979: b8 ca 00 00 00 mov $0xca,%eax +- 47297e: 0f 05 syscall + 472979: -+ 47297e: 90 nop -+ 47297f: 90 nop - 472980: eb 8c jmp 47290e <_dl_fixup+0x10e> - 472982: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) - 472988: 31 c0 xor %eax,%eax -@@ -122353,8 +122450,9 @@ - 476c07: 64 48 8b 04 25 10 00 mov %fs:0x10,%rax ++ 47297e: 90 nop ++ 47297f: 90 nop + 472980: eb 8c jmp 47290e <_dl_fixup+0x10e> + 472982: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) + 472988: 31 c0 xor %eax,%eax +@@ -122350,8 +122447,9 @@ + 476c07: 64 48 8b 04 25 10 00 mov %fs:0x10,%rax 476c0e: 00 00 - 476c10: 48 8d 78 1c lea 0x1c(%rax),%rdi -- 476c14: b8 ca 00 00 00 mov $0xca,%eax -- 476c19: 0f 05 syscall + 476c10: 48 8d 78 1c lea 0x1c(%rax),%rdi +- 476c14: b8 ca 00 00 00 mov $0xca,%eax +- 476c19: 0f 05 syscall + 476c14: -+ 476c19: 90 nop -+ 476c1a: 90 nop - 476c1b: 48 83 7d 98 00 cmpq $0x0,-0x68(%rbp) - 476c20: 48 8b 4d b0 mov -0x50(%rbp),%rcx - 476c24: 0f 84 ae fd ff ff je 4769d8 <_dl_vsym+0xb8> -@@ -122513,8 +122611,9 @@ - 476e41: 64 48 8b 04 25 10 00 mov %fs:0x10,%rax ++ 476c19: 90 nop ++ 476c1a: 90 nop + 476c1b: 48 83 7d 98 00 cmpq $0x0,-0x68(%rbp) + 476c20: 48 8b 4d b0 mov -0x50(%rbp),%rcx + 476c24: 0f 84 ae fd ff ff je 4769d8 <_dl_vsym+0xb8> +@@ -122510,8 +122608,9 @@ + 476e41: 64 48 8b 04 25 10 00 mov %fs:0x10,%rax 476e48: 00 00 - 476e4a: 48 8d 78 1c lea 0x1c(%rax),%rdi -- 476e4e: b8 ca 00 00 00 mov $0xca,%eax -- 476e53: 0f 05 syscall + 476e4a: 48 8d 78 1c lea 0x1c(%rax),%rdi +- 476e4e: b8 ca 00 00 00 mov $0xca,%eax +- 476e53: 0f 05 syscall + 476e4e: -+ 476e53: 90 nop -+ 476e54: 90 nop - 476e55: 48 83 7d 98 00 cmpq $0x0,-0x68(%rbp) - 476e5a: 48 8b 4d b0 mov -0x50(%rbp),%rcx - 476e5e: 0f 84 3c fe ff ff je 476ca0 <_dl_sym+0x60> ++ 476e53: 90 nop ++ 476e54: 90 nop + 476e55: 48 83 7d 98 00 cmpq $0x0,-0x68(%rbp) + 476e5a: 48 8b 4d b0 mov -0x50(%rbp),%rcx + 476e5e: 0f 84 3c fe ff ff je 476ca0 <_dl_sym+0x60> From 6deaedf9b6b12d1a0f8ec497f22e0d271d544b50 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 23 Jul 2026 16:01:00 -0700 Subject: [PATCH 126/319] Add a dummy handler for NtTestAlert to Windows shim (#1079) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 8 ++++++++ litebox_shim_windows/src/syscalls/mod.rs | 2 ++ 2 files changed, 10 insertions(+) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 9b6c773504..22c0dbebdb 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -1900,6 +1900,14 @@ impl Task { (NtStatus::SUCCESS, ContinueOperation::Terminate) } } + SyscallRequest::NtTestAlert => { + // TODO(apc-model): Deliver queued user-mode APCs once thread alert and APC state + // are modeled. + litebox_util_log::debug!( + "NtTestAlert is a no-op; user-mode APC delivery is not yet modeled" + ); + (NtStatus::SUCCESS, ContinueOperation::Resume) + } SyscallRequest::NtManageHotPatch => { (NtStatus::NOT_IMPLEMENTED, ContinueOperation::Resume) } diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index cfe18d48d0..cc59ebdc96 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -604,6 +604,7 @@ pub(crate) enum SyscallRequest { process_handle: ProcessHandle, exit_status: i32, }, + NtTestAlert, /// TODO: not supported yet NtManageHotPatch, } @@ -1120,6 +1121,7 @@ impl SyscallRequest { process_handle: { ProcessHandle::from_raw }, exit_status, })), + NtSysno::NtTestAlert => Some(SyscallRequest::NtTestAlert), NtSysno::NtManageHotPatch => Some(SyscallRequest::NtManageHotPatch), _ => None, } From ce7ecced86e331d8a77b240cd903a6c6b8e5a951 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 23 Jul 2026 21:19:46 -0700 Subject: [PATCH 127/319] Move broker control traffic to shared-memory rings (#1080) Active broker requests and responses now use paired shared-memory control rings, preserving request-ID multiplexing and bounded host dispatch. Setup transfers and validates an exact-size ring memfd, while the authenticated Unix socket remains for setup, liveness, cancellation, and teardown. Futex-backed waits and 128-byte slots keep the ring efficient and bounded by the protocol's maximum encoded message size. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac86dd2d-0280-4d7b-87a8-5654ad1503a6 --- litebox_broker_host/src/lib.rs | 295 +-- litebox_broker_protocol/src/channel.rs | 14 +- litebox_broker_protocol/src/pipe.rs | 6 +- litebox_broker_protocol/src/wire.rs | 23 +- litebox_broker_transport/src/control_ring.rs | 72 +- litebox_broker_transport/src/shared_memory.rs | 15 +- litebox_broker_transport/src/unix_socket.rs | 1777 +++++++++-------- litebox_broker_userland/src/main.rs | 76 +- .../tests/notification_runtime.rs | 46 +- .../tests/userland_broker.rs | 13 +- litebox_runner_linux_userland/src/broker.rs | 80 +- litebox_runner_linux_userland/tests/run.rs | 128 +- 12 files changed, 1384 insertions(+), 1161 deletions(-) diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index d40ed162e0..e5029fc712 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -17,9 +17,7 @@ extern crate std; use alloc::vec::Vec; use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential}; -use litebox_broker_protocol::channel::{ - HostControlChannel, HostNotificationChannel, HostReceive, PeerCredential, -}; +use litebox_broker_protocol::channel::{HostReceive, HostSetupChannel, PeerCredential}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse}; use litebox_broker_protocol::message::{ @@ -130,21 +128,21 @@ impl BrokerHostAssociation<'_, Memory> { /// /// `send_shared_memory` runs after version negotiation and before the active /// association is returned. -pub fn setup_connection<'a, ControlChannel, Memory, ChannelError>( +pub fn setup_connection<'a, SetupChannel, Memory, ChannelError>( core: &BrokerCore, - control_channel: &mut ControlChannel, + setup_channel: &mut SetupChannel, shared_buffers: &'a SharedBufferPool, - send_shared_memory: impl FnOnce(&mut ControlChannel) -> core::result::Result<(), ChannelError>, + send_shared_memory: impl FnOnce(&mut SetupChannel) -> core::result::Result<(), ChannelError>, ) -> Result, ChannelError> where - ControlChannel: HostControlChannel, + SetupChannel: HostSetupChannel, Memory: SharedMemory, { if shared_buffers.layout() != SHARED_BUFFER_LAYOUT { return Err(BrokerHostError::SharedBufferLayoutMismatch); } - let peer_credential = control_channel + let peer_credential = setup_channel .peer_credential() .map_err(BrokerHostError::Channel)?; let caller_credential = match peer_credential { @@ -154,13 +152,13 @@ where }; let session = core.create_session(caller_credential)?; loop { - let request = match control_channel + let request = match setup_channel .recv_handshake_request() .map_err(BrokerHostError::Channel)? { HostReceive::Message(request) => request, HostReceive::ProtocolViolation => { - control_channel + setup_channel .send_handshake_response(&BrokerHandshakeResponse::Error( ErrorCode::ProtocolState, )) @@ -182,11 +180,11 @@ where broker_protocol_version: BROKER_PROTOCOL_VERSION, } }; - control_channel + setup_channel .send_handshake_response(&response) .map_err(BrokerHostError::Channel)?; if negotiated { - send_shared_memory(control_channel).map_err(BrokerHostError::Channel)?; + send_shared_memory(setup_channel).map_err(BrokerHostError::Channel)?; return Ok(Ok(BrokerHostAssociation { session, shared_buffers, @@ -199,52 +197,6 @@ where } } -/// Authenticates, negotiates, and serves one broker association over paired -/// control and notification channels. -/// -/// The deployment must bind both channels to the same authenticated peer -/// association. Active requests and responses remain on the control channel; -/// broker-initiated readiness wakeups are sent on the notification channel. -/// Event mutations caused by control requests return readiness in their control -/// response and do not also emit a duplicate notification. -/// -/// `shared_buffers` belongs to this association. Payload descriptors are -/// validated against trusted per-slot claim state. `send_shared_memory` runs -/// after version negotiation and before active requests begin. -pub fn serve_connection( - core: &BrokerCore, - control_channel: &mut ControlChannel, - _notification_channel: &mut NotificationChannel, - shared_buffers: &SharedBufferPool, - send_shared_memory: impl FnOnce(&mut ControlChannel) -> core::result::Result<(), ChannelError>, -) -> Result -where - ControlChannel: HostControlChannel, - NotificationChannel: HostNotificationChannel, - Memory: SharedMemory, -{ - let association = - match setup_connection(core, control_channel, shared_buffers, send_shared_memory)? { - Ok(association) => association, - Err(termination) => return Ok(termination), - }; - loop { - let request = match control_channel - .recv_request() - .map_err(BrokerHostError::Channel)? - { - HostReceive::Message(request) => request, - HostReceive::ProtocolViolation => { - return Ok(ConnectionTermination::ProtocolViolation); - } - HostReceive::PeerClosed => break, - }; - association.execute_request(request, |response| control_channel.send_response(response))?; - } - - Ok(ConnectionTermination::PeerClosed) -} - type RequestResult = core::result::Result; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -440,11 +392,10 @@ mod tests { use super::*; use core::cell::Cell; use litebox_broker_core::{ObjectRights, PolicyEngine}; - use litebox_broker_protocol::channel::HostControlChannel; use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, CreateEventRequest, EventConsumeMode, }; - use litebox_broker_protocol::message::{BrokerHandshakeRequest, BrokerNotification}; + use litebox_broker_protocol::message::BrokerHandshakeRequest; use litebox_broker_protocol::pipe::{CreatePipeRequest, ReadPipeRequest, WritePipeRequest}; use litebox_broker_protocol::shared_memory::{ SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, @@ -461,17 +412,17 @@ mod tests { )) .unwrap(); - serve_connection_negotiates_routes_one_request_and_returns_peer_closed(&broker); - serve_connection_retries_after_version_mismatch(&broker); - serve_connection_skips_setup_after_version_mismatch(&broker); - serve_connection_rejects_active_request_before_negotiation(&broker); - serve_connection_rejects_handshake_request_after_negotiation(&broker); - serve_connection_returns_channel_error_when_response_send_fails(&broker); - serve_connection_returns_event_readiness_in_control_responses(&broker); - serve_connection_continues_after_recoverable_request_failure(&broker); - serve_connection_aborts_on_stale_shared_buffer_request(&broker); - serve_connection_aborts_without_response_on_shared_memory_failure(&broker); - serve_connection_rejects_incompatible_shared_buffer_layout(&broker); + test_channel_negotiates_routes_one_request_and_returns_peer_closed(&broker); + test_channel_retries_after_version_mismatch(&broker); + test_channel_skips_setup_after_version_mismatch(&broker); + test_channel_rejects_active_request_before_negotiation(&broker); + test_channel_rejects_handshake_request_after_negotiation(&broker); + test_channel_returns_channel_error_when_response_send_fails(&broker); + test_channel_returns_event_readiness_in_control_responses(&broker); + test_channel_continues_after_recoverable_request_failure(&broker); + test_channel_aborts_on_stale_shared_buffer_request(&broker); + test_channel_aborts_without_response_on_shared_memory_failure(&broker); + test_channel_rejects_incompatible_shared_buffer_layout(&broker); active_request_closes_object_reference(&broker); association_shared_buffer_descriptors_stage_pipe_data(&broker); shared_buffer_usage_rejects_invalid_descriptors(); @@ -480,7 +431,7 @@ mod tests { association_allows_out_of_order_responses(&broker); } - fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { + fn test_channel_negotiates_routes_one_request_and_returns_peer_closed(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, @@ -493,17 +444,8 @@ mod tests { ]), ); channel.next_request_id = 41; - let mut notifications = FakeHostNotificationChannel::default(); - assert_eq!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| Ok(()), - ) - .unwrap(), + serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| Ok(())).unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -520,7 +462,7 @@ mod tests { assert_eq!(channel.response_ids, [RequestId(41)]); } - fn serve_connection_retries_after_version_mismatch(broker: &BrokerCore) { + fn test_channel_retries_after_version_mismatch(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([ Ok(HostReceive::Message(BrokerHandshakeRequest { @@ -532,17 +474,8 @@ mod tests { ]), std::vec::Vec::from([Ok(HostReceive::PeerClosed)]), ); - let mut notifications = FakeHostNotificationChannel::default(); - assert_eq!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| Ok(()), - ) - .unwrap(), + serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| Ok(())).unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -558,7 +491,7 @@ mod tests { ); } - fn serve_connection_skips_setup_after_version_mismatch(broker: &BrokerCore) { + fn test_channel_skips_setup_after_version_mismatch(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([ Ok(HostReceive::Message(BrokerHandshakeRequest { @@ -568,20 +501,13 @@ mod tests { ]), std::vec::Vec::new(), ); - let mut notifications = FakeHostNotificationChannel::default(); let setup_called = Cell::new(false); assert_eq!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| { - setup_called.set(true); - Ok(()) - }, - ) + serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| { + setup_called.set(true); + Ok(()) + }) .unwrap(), ConnectionTermination::PeerClosed ); @@ -594,22 +520,13 @@ mod tests { assert!(!setup_called.get()); } - fn serve_connection_rejects_active_request_before_negotiation(broker: &BrokerCore) { + fn test_channel_rejects_active_request_before_negotiation(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::ProtocolViolation)]), std::vec::Vec::new(), ); - let mut notifications = FakeHostNotificationChannel::default(); - assert_eq!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| Ok(()), - ) - .unwrap(), + serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| Ok(())).unwrap(), ConnectionTermination::ProtocolViolation ); assert_eq!( @@ -619,24 +536,15 @@ mod tests { assert!(channel.results.is_empty()); } - fn serve_connection_rejects_handshake_request_after_negotiation(broker: &BrokerCore) { + fn test_channel_rejects_handshake_request_after_negotiation(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, }))]), std::vec::Vec::from([Ok(HostReceive::ProtocolViolation)]), ); - let mut notifications = FakeHostNotificationChannel::default(); - assert_eq!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| Ok(()), - ) - .unwrap(), + serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| Ok(())).unwrap(), ConnectionTermination::ProtocolViolation ); assert_eq!( @@ -648,7 +556,7 @@ mod tests { assert!(channel.results.is_empty()); } - fn serve_connection_returns_channel_error_when_response_send_fails(broker: &BrokerCore) { + fn test_channel_returns_channel_error_when_response_send_fails(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, @@ -658,15 +566,7 @@ mod tests { )))]), ); channel.response_send_error = true; - let mut notifications = FakeHostNotificationChannel::default(); - - match serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| Ok(()), - ) { + match serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| Ok(())) { Err(BrokerHostError::Channel(())) => {} result => panic!("unexpected serve result: {result:?}"), } @@ -674,7 +574,7 @@ mod tests { assert!(channel.results.is_empty()); } - fn serve_connection_returns_event_readiness_in_control_responses(broker: &BrokerCore) { + fn test_channel_returns_event_readiness_in_control_responses(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, @@ -684,20 +584,10 @@ mod tests { )))]), ); channel.enqueue_readiness_requests_after_create = true; - let mut notifications = FakeHostNotificationChannel::default(); - assert_eq!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| Ok(()), - ) - .unwrap(), + serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| Ok(())).unwrap(), ConnectionTermination::PeerClosed ); - assert!(notifications.notifications.is_empty()); assert_eq!( &channel.results[1..], [ @@ -715,7 +605,7 @@ mod tests { ); } - fn serve_connection_continues_after_recoverable_request_failure(broker: &BrokerCore) { + fn test_channel_continues_after_recoverable_request_failure(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, @@ -733,17 +623,8 @@ mod tests { Ok(HostReceive::PeerClosed), ]), ); - let mut notifications = FakeHostNotificationChannel::default(); - assert_eq!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| Ok(()), - ) - .unwrap(), + serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| Ok(())).unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -757,7 +638,7 @@ mod tests { assert_eq!(channel.response_ids, [RequestId(0), RequestId(1)]); } - fn serve_connection_aborts_on_stale_shared_buffer_request(broker: &BrokerCore) { + fn test_channel_aborts_on_stale_shared_buffer_request(broker: &BrokerCore) { let stale_request = BrokerOperation::Pipe(PipeRequest::Read(ReadPipeRequest { handle: ObjectHandle(u64::MAX), buffer: descriptor(0, 1), @@ -772,16 +653,8 @@ mod tests { ]), ); channel.request_id_step = 0; - let mut notifications = FakeHostNotificationChannel::default(); - assert!(matches!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &test_shared_buffers(), - |_| Ok(()), - ), + serve_test_channel(broker, &mut channel, &test_shared_buffers(), |_| Ok(())), Err(BrokerHostError::Broker(ErrorCode::MalformedRequest)) )); assert_eq!( @@ -791,7 +664,7 @@ mod tests { assert_eq!(channel.response_ids, [RequestId(0)]); } - fn serve_connection_aborts_without_response_on_shared_memory_failure(broker: &BrokerCore) { + fn test_channel_aborts_without_response_on_shared_memory_failure(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, @@ -804,13 +677,10 @@ mod tests { )))]), ); channel.enqueue_write_request_after_pipe_create = true; - let mut notifications = FakeHostNotificationChannel::default(); - assert!(matches!( - serve_connection( + serve_test_channel( broker, &mut channel, - &mut notifications, &SharedBufferPool::new(FailingSharedMemory, SHARED_BUFFER_LAYOUT).unwrap(), |_| Ok(()), ), @@ -823,14 +693,13 @@ mod tests { )); } - fn serve_connection_rejects_incompatible_shared_buffer_layout(broker: &BrokerCore) { + fn test_channel_rejects_incompatible_shared_buffer_layout(broker: &BrokerCore) { let mut channel = FakeHostControlChannel::new( std::vec::Vec::from([Ok(HostReceive::Message(BrokerHandshakeRequest { protocol_version: BROKER_PROTOCOL_VERSION, }))]), std::vec::Vec::new(), ); - let mut notifications = FakeHostNotificationChannel::default(); let incompatible_layout = litebox_broker_protocol::shared_memory::SharedBufferLayout::new( u32::try_from(SHARED_BUFFER_POOL_SIZE).unwrap(), 1, @@ -844,16 +713,10 @@ mod tests { let setup_called = Cell::new(false); assert!(matches!( - serve_connection( - broker, - &mut channel, - &mut notifications, - &shared_buffers, - |_| { - setup_called.set(true); - Ok(()) - }, - ), + serve_test_channel(broker, &mut channel, &shared_buffers, |_| { + setup_called.set(true); + Ok(()) + }), Err(BrokerHostError::SharedBufferLayoutMismatch) )); assert!(!setup_called.get()); @@ -1192,6 +1055,34 @@ mod tests { .unwrap() } + fn serve_test_channel( + broker: &BrokerCore, + control_channel: &mut FakeHostControlChannel, + shared_buffers: &SharedBufferPool, + send_shared_memory: impl FnOnce(&mut FakeHostControlChannel) -> core::result::Result<(), ()>, + ) -> Result { + let association = + match setup_connection(broker, control_channel, shared_buffers, send_shared_memory)? { + Ok(association) => association, + Err(termination) => return Ok(termination), + }; + loop { + let request = match control_channel + .recv_request() + .map_err(BrokerHostError::Channel)? + { + HostReceive::Message(request) => request, + HostReceive::ProtocolViolation => { + return Ok(ConnectionTermination::ProtocolViolation); + } + HostReceive::PeerClosed => break, + }; + association + .execute_request(request, |response| control_channel.send_response(response))?; + } + Ok(ConnectionTermination::PeerClosed) + } + struct FakeHostControlChannel { handshake_requests: std::vec::Vec, ()>>, @@ -1228,7 +1119,7 @@ mod tests { } } - impl HostControlChannel for FakeHostControlChannel { + impl HostSetupChannel for FakeHostControlChannel { type Error = (); fn peer_credential(&self) -> core::result::Result { @@ -1252,10 +1143,10 @@ mod tests { self.handshake_responses.push(response.clone()); Ok(()) } + } - fn recv_request( - &mut self, - ) -> core::result::Result, Self::Error> { + impl FakeHostControlChannel { + fn recv_request(&mut self) -> core::result::Result, ()> { let received = if self.operations.is_empty() { HostReceive::PeerClosed } else { @@ -1275,10 +1166,7 @@ mod tests { }) } - fn send_response( - &mut self, - response: &BrokerResponse, - ) -> core::result::Result<(), Self::Error> { + fn send_response(&mut self, response: &BrokerResponse) -> core::result::Result<(), ()> { if self.response_send_error { return Err(()); } @@ -1420,21 +1308,4 @@ mod tests { Err(SharedMemoryError::InvalidRange) } } - - #[derive(Default)] - struct FakeHostNotificationChannel { - notifications: std::vec::Vec, - } - - impl HostNotificationChannel for FakeHostNotificationChannel { - type Error = (); - - fn send_notification( - &mut self, - notification: &BrokerNotification, - ) -> core::result::Result<(), Self::Error> { - self.notifications.push(notification.clone()); - Ok(()) - } - } } diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index 9e02fd116f..784ae62d02 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -20,7 +20,7 @@ pub enum PeerCredential { /// Explicit deployment mode for the initial unauthenticated userland POC. /// /// Channels that are expected to authenticate peers must return an error - /// from [`HostControlChannel::peer_credential`] when authentication is + /// from [`HostSetupChannel::peer_credential`] when authentication is /// unavailable or fails; this variant is only for deployments that /// deliberately choose unauthenticated operation. Unauthenticated, @@ -63,8 +63,8 @@ pub trait LocalControlChannel { fn call(&self, request: BrokerRequest) -> Result; } -/// Host-side control channel for broker authority calls. -pub trait HostControlChannel { +/// Host-side channel for broker association setup. +pub trait HostSetupChannel { /// Channel-specific error type. type Error; @@ -81,12 +81,6 @@ pub trait HostControlChannel { &mut self, response: &BrokerHandshakeResponse, ) -> Result<(), Self::Error>; - - /// Receives one active broker request. - fn recv_request(&mut self) -> Result, Self::Error>; - - /// Sends one active broker response. - fn send_response(&mut self, response: &BrokerResponse) -> Result<(), Self::Error>; } /// Local-side receive channel for broker-initiated asynchronous notifications. @@ -109,7 +103,7 @@ pub trait LocalNotificationChannel { /// Host-side send channel for broker-initiated asynchronous notifications. /// /// Implementations carry notification frames only; object operation responses -/// continue to use [`HostControlChannel::send_response`]. +/// remain on the active control transport. pub trait HostNotificationChannel { /// Channel-specific error type. type Error; diff --git a/litebox_broker_protocol/src/pipe.rs b/litebox_broker_protocol/src/pipe.rs index 5369f179e2..81dae150bc 100644 --- a/litebox_broker_protocol/src/pipe.rs +++ b/litebox_broker_protocol/src/pipe.rs @@ -4,10 +4,10 @@ use crate::ObjectHandle; use crate::shared_memory::SharedBufferDescriptor; -/// Maximum pipe transfer described by one control-path request or response. +/// Maximum pipe bytes transferred by one broker request. /// -/// This leaves room for the broker envelope and operation metadata within the -/// smallest currently supported transport frame. +/// Each association shared-buffer slot has this size. Larger blocking writes +/// are split across requests, while reads may return at most this amount. pub const MAX_PIPE_TRANSFER_SIZE: u32 = 32 * 1024; /// Request to create a broker-owned byte pipe. diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 6b92818484..75c2e7385d 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -48,6 +48,9 @@ const RESPONSE_TAG_ERROR: u8 = 7; const NOTIFICATION_TAG_READINESS: u8 = 0; +/// Maximum byte length of any encoded active request or response. +pub const MAX_ENCODED_ACTIVE_MESSAGE_SIZE: usize = 26; + /// Error produced while encoding or decoding a broker wire message. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] #[non_exhaustive] @@ -381,17 +384,19 @@ mod tests { }, })), ]; + let mut maximum_encoded_size = 0; for operation in operations { let request = BrokerRequest { request_id: TEST_REQUEST_ID, operation, }; - assert_eq!( - decode_request(&encode_request(request.clone())).unwrap(), - request - ); + let encoded = encode_request(request.clone()); + maximum_encoded_size = maximum_encoded_size.max(encoded.len()); + assert!(encoded.len() <= MAX_ENCODED_ACTIVE_MESSAGE_SIZE); + assert_eq!(decode_request(&encoded).unwrap(), request); } + assert_eq!(maximum_encoded_size, MAX_ENCODED_ACTIVE_MESSAGE_SIZE); } #[test] @@ -456,17 +461,19 @@ mod tests { BrokerResult::Error(ErrorCode::OutOfMemory), BrokerResult::Error(ErrorCode::Internal), ]; + let mut maximum_encoded_size = 0; for result in results { let response = BrokerResponse { request_id: TEST_REQUEST_ID, result, }; - assert_eq!( - decode_response(&encode_response(response.clone())).unwrap(), - response - ); + let encoded = encode_response(response.clone()); + maximum_encoded_size = maximum_encoded_size.max(encoded.len()); + assert!(encoded.len() <= MAX_ENCODED_ACTIVE_MESSAGE_SIZE); + assert_eq!(decode_response(&encoded).unwrap(), response); } + assert_eq!(maximum_encoded_size, MAX_ENCODED_ACTIVE_MESSAGE_SIZE); } #[test] diff --git a/litebox_broker_transport/src/control_ring.rs b/litebox_broker_transport/src/control_ring.rs index be33978f19..0855b60c04 100644 --- a/litebox_broker_transport/src/control_ring.rs +++ b/litebox_broker_transport/src/control_ring.rs @@ -14,7 +14,7 @@ use litebox_broker_protocol::shared_memory::SharedMemory; use litebox_broker_protocol::shared_memory::{AtomicSharedMemory, SharedMemoryError}; /// Size of one shared control-ring slot. -pub const CONTROL_RING_SLOT_SIZE: usize = 4096; +pub const CONTROL_RING_SLOT_SIZE: usize = 128; /// Size of the fixed metadata at the start of a control-ring slot. pub const CONTROL_RING_SLOT_HEADER_SIZE: usize = 16; @@ -23,6 +23,11 @@ pub const CONTROL_RING_SLOT_HEADER_SIZE: usize = 16; pub const CONTROL_RING_PAYLOAD_CAPACITY: usize = CONTROL_RING_SLOT_SIZE - CONTROL_RING_SLOT_HEADER_SIZE; +const _: () = assert!( + CONTROL_RING_PAYLOAD_CAPACITY >= litebox_broker_protocol::wire::MAX_ENCODED_ACTIVE_MESSAGE_SIZE +); +const _: () = assert!(CONTROL_RING_SLOT_SIZE.is_multiple_of(size_of::())); + /// Number of slots in each direction of the shared control ring. pub const CONTROL_RING_SLOT_COUNT: u64 = 64; @@ -313,6 +318,55 @@ pub struct ControlRingProducer { acknowledged_head: u64, } +/// Cloneable, narrow handle for interrupting a wait on one ring endpoint. +/// +/// This handle intentionally exposes neither the backing memory nor endpoint +/// state. Hosted transports use it to make liveness and cancellation events +/// visible to a thread blocked in an OS-specific wait. +#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] +pub(crate) struct ControlRingWakeHandle { + ring: Arc>, + wait_epoch: ControlRingWaitEpoch, +} + +#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] +#[derive(Clone, Copy)] +enum ControlRingWaitEpoch { + Producer(ControlRingDirection), + Consumer(ControlRingDirection), +} + +#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] +impl ControlRingWaitEpoch { + const fn offset(self) -> usize { + match self { + Self::Producer(direction) => direction.producer_epoch_offset(), + Self::Consumer(direction) => direction.consumer_epoch_offset(), + } + } +} + +#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] +impl Clone for ControlRingWakeHandle { + fn clone(&self) -> Self { + Self { + ring: Arc::clone(&self.ring), + wait_epoch: self.wait_epoch, + } + } +} + +#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] +impl ControlRingWakeHandle { + pub(crate) const fn wait_epoch_offset(&self) -> usize { + self.wait_epoch.offset() + } + + pub(crate) fn memory(&self) -> &Memory { + self.ring.memory() + } +} + impl ControlRingProducer { fn new(ring: Arc>, direction: ControlRingDirection) -> Self { Self { @@ -328,6 +382,14 @@ impl ControlRingProducer { self.direction } + #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] + pub(crate) fn wake_handle(&self) -> ControlRingWakeHandle { + ControlRingWakeHandle { + ring: Arc::clone(&self.ring), + wait_epoch: ControlRingWaitEpoch::Consumer(self.direction), + } + } + #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] pub(crate) fn memory(&self) -> &Memory { self.ring.memory() @@ -412,6 +474,14 @@ impl ControlRingConsumer { self.direction } + #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] + pub(crate) fn wake_handle(&self) -> ControlRingWakeHandle { + ControlRingWakeHandle { + ring: Arc::clone(&self.ring), + wait_epoch: ControlRingWaitEpoch::Producer(self.direction), + } + } + #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] pub(crate) fn memory(&self) -> &Memory { self.ring.memory() diff --git a/litebox_broker_transport/src/shared_memory.rs b/litebox_broker_transport/src/shared_memory.rs index d6f357835f..f59f688965 100644 --- a/litebox_broker_transport/src/shared_memory.rs +++ b/litebox_broker_transport/src/shared_memory.rs @@ -26,7 +26,7 @@ use litebox_broker_protocol::shared_memory::{AtomicSharedMemory, SharedMemory, S use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; -use crate::control_ring::{ControlRingConsumer, ControlRingProducer}; +use crate::control_ring::{ControlRingConsumer, ControlRingProducer, ControlRingWakeHandle}; use crate::unix_io::{ refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, }; @@ -259,6 +259,19 @@ impl ControlRingConsumer { } } +impl ControlRingWakeHandle { + /// Changes and wakes the epoch observed by this endpoint's wait operation. + /// + /// Incrementing before waking closes the race where cancellation happens + /// after a ring operation samples its epoch but before it enters futex wait. + pub(crate) fn interrupt_wait(&self) -> IoResult<()> { + self.memory() + .fetch_add_u32_release(self.wait_epoch_offset(), 1) + .map_err(|error| Error::new(ErrorKind::InvalidInput, error))?; + self.memory().futex_wake_u32(self.wait_epoch_offset()) + } +} + impl AsFd for MemfdSharedMemory { fn as_fd(&self) -> BorrowedFd<'_> { self.fd.as_fd() diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 646ec4ac18..96aa9580e9 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -7,10 +7,8 @@ //! framing are hosted userland concerns. Portable broker interfaces live in the //! no_std protocol, local, core, and host crates. //! -//! After setup, each caller thread registers its request and writes its complete -//! frame while holding the shared writer mutex; there is no local request worker. -//! One response-dispatcher thread exclusively reads responses, correlates them by -//! request ID, and wakes the matching callers. +//! After setup, the authenticated socket is retained only for liveness and +//! cancellation. Active requests and responses use a shared control ring. use std::io::{Error, ErrorKind, Read, Result as IoResult, Write}; use std::net::Shutdown; @@ -20,13 +18,17 @@ use std::sync::{Arc, Condvar, Mutex}; use std::time::Instant; use std::{collections::HashMap, thread}; +use crate::control_ring::{ + ControlRing, ControlRingConsumer, ControlRingError, ControlRingProducer, ControlRingReadError, + ControlRingReadStatus, ControlRingWakeHandle, ControlRingWriteStatus, +}; use crate::shared_memory::MemfdSharedMemory; use crate::unix_io::{ refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, }; use litebox_broker_protocol::RequestId; use litebox_broker_protocol::channel::{ - HostControlChannel, HostNotificationChannel, HostReceive, LocalControlChannel, + HostNotificationChannel, HostReceive, HostSetupChannel, LocalControlChannel, LocalNotificationChannel, PeerCredential, }; use litebox_broker_protocol::message::{ @@ -40,6 +42,7 @@ use litebox_broker_protocol::wire::{ }; const MAX_FRAME_LEN: usize = 64 * 1024; +const CONTROL_RING_READY: &[u8] = b"litebox-control-ring-ready-v1"; /// Maximum number of active calls waiting for broker responses. pub const MAX_PENDING_CALLS: usize = 64; @@ -90,16 +93,20 @@ struct UnixStreamLocalSetup { /// Independently owned handle for interrupting local control-channel I/O. pub struct UnixStreamLocalControlCancellation { - stream: UnixStream, - pending_calls: Arc, - association_failure: Arc, + failure_coordinator: Arc, } struct UnixStreamLocalActive { - request_stream: Mutex, - shutdown_stream: UnixStream, + request_producer: Mutex>, + failure_coordinator: Arc, +} + +struct LocalActiveFailureCoordinator { + stream: UnixStream, pending_calls: Arc, association_failure: Arc, + request_wait: ControlRingWakeHandle, + response_wait: ControlRingWakeHandle, } impl UnixStreamLocalControlChannel { @@ -149,9 +156,13 @@ impl UnixStreamLocalControlChannel { crate::shared_memory::receive_memfd(&mut setup.stream, expected_len, deadline) } - /// Completes setup and starts the active response pump. + /// Completes setup and starts the active control-ring response pump. + /// + /// The ring must be the validated control-ring memfd received during this + /// setup exchange. pub fn activate( &mut self, + ring: ControlRing, association_failure: impl Fn() + Send + Sync + 'static, ) -> IoResult { let UnixStreamLocalControlState::Setup(setup) = &self.state else { @@ -168,36 +179,63 @@ impl UnixStreamLocalControlChannel { unreachable!("broker control setup state disappeared"); }; - let response_stream = setup.stream; - let request_stream = response_stream.try_clone()?; - let shutdown_stream = response_stream.try_clone()?; - let cancellation_stream = response_stream.try_clone()?; - let response_cancellation = response_stream.try_clone()?; + let mut monitor_stream = setup.stream; + write_frame_with_deadline( + &mut monitor_stream, + CONTROL_RING_READY, + setup.setup_deadline, + )?; + let Some(ready) = read_frame_with_deadline(&mut monitor_stream, setup.setup_deadline)? + else { + return Err(Error::new( + ErrorKind::UnexpectedEof, + "broker closed before control-ring setup acknowledgement", + )); + }; + if ready != CONTROL_RING_READY { + return Err(invalid_data( + "broker sent an invalid control-ring setup acknowledgement", + )); + } + + let shutdown_stream = monitor_stream.try_clone()?; + let (request_producer, response_consumer) = ring.into_local(); let pending_calls = Arc::new(PendingCalls::new()); let association_failure: Arc = Arc::new(association_failure); - let response_pending_calls = Arc::clone(&pending_calls); - let response_failure = Arc::clone(&association_failure); - thread::Builder::new() + let failure_coordinator = Arc::new(LocalActiveFailureCoordinator { + stream: shutdown_stream, + pending_calls: Arc::clone(&pending_calls), + association_failure, + request_wait: request_producer.wake_handle(), + response_wait: response_consumer.wake_handle(), + }); + let response_failure_coordinator = Arc::clone(&failure_coordinator); + if let Err(error) = thread::Builder::new() .name("litebox-broker-responses".to_owned()) .spawn(move || { - dispatch_responses( - response_stream, - response_cancellation, - response_pending_calls, - response_failure, - ); - })?; + dispatch_responses(response_consumer, response_failure_coordinator); + }) + { + let _ = failure_coordinator.fail(error); + return Err(Error::other("failed to start broker response pump")); + } + let monitor_failure_coordinator = Arc::clone(&failure_coordinator); + if let Err(error) = thread::Builder::new() + .name("litebox-broker-liveness".to_owned()) + .spawn(move || { + monitor_local_socket(&mut monitor_stream, &monitor_failure_coordinator); + }) + { + let _ = failure_coordinator.fail(error); + return Err(Error::other("failed to start broker liveness monitor")); + } self.state = UnixStreamLocalControlState::Active(UnixStreamLocalActive { - request_stream: Mutex::new(request_stream), - shutdown_stream, - pending_calls: Arc::clone(&pending_calls), - association_failure: Arc::clone(&association_failure), + request_producer: Mutex::new(request_producer), + failure_coordinator: Arc::clone(&failure_coordinator), }); Ok(UnixStreamLocalControlCancellation { - stream: cancellation_stream, - pending_calls, - association_failure, + failure_coordinator, }) } } @@ -205,12 +243,10 @@ impl UnixStreamLocalControlChannel { impl UnixStreamLocalControlCancellation { /// Shuts down the control stream, unblocking pending reads or writes. pub fn cancel(&self) -> IoResult<()> { - fail_active_channel( - &self.pending_calls, - &self.stream, - self.association_failure.as_ref(), - Error::new(ErrorKind::ConnectionAborted, "broker association cancelled"), - ) + self.failure_coordinator.fail(Error::new( + ErrorKind::ConnectionAborted, + "broker association cancelled", + )) } } @@ -219,15 +255,10 @@ impl Drop for UnixStreamLocalControlChannel { let UnixStreamLocalControlState::Active(active) = &self.state else { return; }; - let _ = fail_active_channel( - &active.pending_calls, - &active.shutdown_stream, - active.association_failure.as_ref(), - Error::new( - ErrorKind::ConnectionAborted, - "broker active channel dropped", - ), - ); + let _ = active.failure_coordinator.fail(Error::new( + ErrorKind::ConnectionAborted, + "broker active channel dropped", + )); } } @@ -248,18 +279,33 @@ pub struct UnixStreamHostControlChannel { /// Request-reading half of an active host control channel. pub struct UnixStreamHostRequestSource { - stream: UnixStream, + consumer: ControlRingConsumer, + active: Arc, } /// Shared response-writing half of an active host control channel. #[derive(Clone)] pub struct UnixStreamHostResponseSink { - stream: Arc>, + producer: Arc>>, + active: Arc, } -/// Handle that interrupts all active host control-channel I/O. +/// RAII guard that interrupts all active host control-channel I/O when dropped. pub struct UnixStreamHostControlShutdown { + active: Arc, +} + +struct HostActiveState { stream: UnixStream, + status: Mutex, + request_wait: ControlRingWakeHandle, + response_wait: ControlRingWakeHandle, +} + +enum HostActiveStatus { + Live, + PeerClosed, + Failed(Arc), } /// Local-side Unix-domain-socket notification channel for the hosted userland POC. @@ -311,7 +357,8 @@ impl UnixStreamHostControlChannel { /// Consumes a negotiated setup channel into independently usable active /// request, response, and shutdown handles. pub fn into_active( - self, + mut self, + ring: ControlRing, ) -> IoResult<( UnixStreamHostRequestSource, UnixStreamHostResponseSink, @@ -322,18 +369,41 @@ impl UnixStreamHostControlChannel { "broker host control channel activated before negotiation completed", )); } - let response_stream = self.stream.try_clone()?; + let Some(ready) = read_frame_with_deadline(&mut self.stream, self.setup_deadline)? else { + return Err(Error::new( + ErrorKind::UnexpectedEof, + "runner closed before control-ring setup acknowledgement", + )); + }; + if ready != CONTROL_RING_READY { + return Err(invalid_data( + "runner sent an invalid control-ring setup acknowledgement", + )); + } + write_frame_with_deadline(&mut self.stream, CONTROL_RING_READY, self.setup_deadline)?; + let shutdown_stream = self.stream.try_clone()?; + let (response_producer, request_consumer) = ring.into_broker(); + let active = Arc::new(HostActiveState { + stream: shutdown_stream, + status: Mutex::new(HostActiveStatus::Live), + request_wait: request_consumer.wake_handle(), + response_wait: response_producer.wake_handle(), + }); + let monitor_active = Arc::clone(&active); + thread::Builder::new() + .name("litebox-runner-liveness".to_owned()) + .spawn(move || monitor_host_socket(&mut self.stream, &monitor_active))?; Ok(( UnixStreamHostRequestSource { - stream: self.stream, + consumer: request_consumer, + active: Arc::clone(&active), }, UnixStreamHostResponseSink { - stream: Arc::new(Mutex::new(response_stream)), - }, - UnixStreamHostControlShutdown { - stream: shutdown_stream, + producer: Arc::new(Mutex::new(response_producer)), + active: Arc::clone(&active), }, + UnixStreamHostControlShutdown { active }, )) } } @@ -342,7 +412,19 @@ impl UnixStreamHostControlShutdown { /// Shuts down the active control socket without waiting for the response /// writer mutex. pub fn shutdown(&self) -> IoResult<()> { - shutdown(&self.stream) + self.active.fail(Error::new( + ErrorKind::ConnectionAborted, + "broker host control channel shut down", + )) + } +} + +impl Drop for UnixStreamHostControlShutdown { + fn drop(&mut self) { + let _ = self.active.fail(Error::new( + ErrorKind::ConnectionAborted, + "broker host control shutdown guard dropped", + )); } } @@ -401,7 +483,6 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { return Err(invalid_data("broker control channel is already active")); }; let frame = read_frame_with_deadline(&mut setup.stream, setup.setup_deadline)?; - setup.setup_deadline = None; match frame { Some(frame) => { let response = decode_handshake_response(&frame).map_err(wire_error)?; @@ -417,33 +498,47 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { return Err(invalid_data("broker control channel is not active")); }; let request_id = request.request_id; - let pending_call = active.pending_calls.register(request_id)?; + let pending_call = active + .failure_coordinator + .pending_calls + .register(request_id)?; let request_frame = encode_request(request); let write_result = { - let mut request_stream = active - .request_stream + let mut producer = active + .request_producer .lock() .expect("broker request writer mutex poisoned"); - match active.pending_calls.current_failure() { - Some(error) => Err(copy_io_error(&error)), - None => write_frame_with_deadline(&mut request_stream, &request_frame, None), + loop { + let write_status = active + .failure_coordinator + .pending_calls + .run_if_live(|| producer.try_write(&request_frame).map_err(Error::from)); + match write_status { + Ok(ControlRingWriteStatus::Written) => { + if let Err(error) = producer.wake_consumer() { + break Err(error); + } + break Ok(()); + } + Ok(ControlRingWriteStatus::Full { wait_epoch }) => { + if let Err(error) = producer.wait_for_capacity(wait_epoch) { + break Err(error); + } + } + Err(error) => break Err(error), + } } }; if let Err(error) = write_result { - let _ = fail_active_channel( - &active.pending_calls, - &active.shutdown_stream, - active.association_failure.as_ref(), - error, - ); + let _ = active.failure_coordinator.fail(error); } pending_call.wait() } } -impl HostControlChannel for UnixStreamHostControlChannel { +impl HostSetupChannel for UnixStreamHostControlChannel { type Error = Error; fn peer_credential(&self) -> IoResult { @@ -468,38 +563,48 @@ impl HostControlChannel for UnixStreamHostControlChannel { self.setup_deadline, )?; self.negotiated = matches!(response, BrokerHandshakeResponse::Negotiated { .. }); - if self.negotiated { - self.setup_deadline = None; - } Ok(()) } - - fn recv_request(&mut self) -> IoResult> { - let Some(frame) = read_frame_with_deadline(&mut self.stream, None)? else { - return Ok(HostReceive::PeerClosed); - }; - match decode_request(&frame) { - Ok(request) => Ok(HostReceive::Message(request)), - Err(WireError::WrongMessagePhase) => Ok(HostReceive::ProtocolViolation), - Err(error) => Err(wire_error(error)), - } - } - - fn send_response(&mut self, response: &BrokerResponse) -> IoResult<()> { - write_frame_with_deadline(&mut self.stream, &encode_response(response.clone()), None) - } } impl UnixStreamHostRequestSource { /// Receives one active broker request. pub fn recv_request(&mut self) -> IoResult> { - let Some(frame) = read_frame_with_deadline(&mut self.stream, None)? else { - return Ok(HostReceive::PeerClosed); - }; - match decode_request(&frame) { - Ok(request) => Ok(HostReceive::Message(request)), - Err(WireError::WrongMessagePhase) => Ok(HostReceive::ProtocolViolation), - Err(error) => Err(wire_error(error)), + loop { + if let Some(error) = self.active.request_failure() { + return Err(error); + } + match self.consumer.try_read(decode_request) { + Ok(ControlRingReadStatus::Message(request)) => { + self.active.acknowledge_request(&mut self.consumer)?; + return Ok(HostReceive::Message(request)); + } + Ok(ControlRingReadStatus::Empty { wait_epoch }) => { + if let Some(terminal) = self.active.request_terminal_result() { + return terminal; + } + if let Err(error) = self.consumer.wait_for_message(wait_epoch) { + let result = Err(copy_io_error(&error)); + let _ = self.active.fail(error); + return result; + } + } + Err(ControlRingReadError::Decode(WireError::WrongMessagePhase)) => { + return Ok(HostReceive::ProtocolViolation); + } + Err(ControlRingReadError::Decode(error)) => { + let error = wire_error(error); + let result = Err(copy_io_error(&error)); + let _ = self.active.fail(error); + return result; + } + Err(ControlRingReadError::Ring(error)) => { + let error = Error::from(error); + let result = Err(copy_io_error(&error)); + let _ = self.active.fail(error); + return result; + } + } } } } @@ -507,11 +612,23 @@ impl UnixStreamHostRequestSource { impl UnixStreamHostResponseSink { /// Serializes and sends one complete active broker response. pub fn send_response(&self, response: &BrokerResponse) -> IoResult<()> { - let mut stream = self - .stream + let frame = encode_response(response.clone()); + let mut producer = self + .producer .lock() .map_err(|_| Error::other("broker response writer mutex poisoned"))?; - write_frame_with_deadline(&mut stream, &encode_response(response.clone()), None) + loop { + match self.active.try_publish_response(&mut producer, &frame)? { + ControlRingWriteStatus::Written => return Ok(()), + ControlRingWriteStatus::Full { wait_epoch } => { + if let Err(error) = producer.wait_for_capacity(wait_epoch) { + let result = Err(copy_io_error(&error)); + let _ = self.active.fail(error); + return result; + } + } + } + } } } @@ -671,75 +788,237 @@ impl PendingCalls { .as_ref() .map(Arc::clone) } + + /// Runs a nonblocking publication while excluding failure recording. + fn run_if_live(&self, operation: impl FnOnce() -> IoResult) -> IoResult { + let state = self.state.lock().expect("broker pending mutex poisoned"); + if let Some(error) = state.failure.as_ref() { + return Err(copy_io_error(error)); + } + operation() + } } -fn dispatch_responses( - mut response_stream: UnixStream, - response_cancellation: UnixStream, - pending_calls: Arc, - association_failure: Arc, +impl LocalActiveFailureCoordinator { + fn fail(&self, error: Error) -> IoResult<()> { + let first_failure = self.pending_calls.record_failure(Arc::new(error)); + let request_wake = self.request_wait.interrupt_wait(); + let response_wake = self.response_wait.interrupt_wait(); + let shutdown_result = shutdown(&self.stream); + if first_failure { + (self.association_failure)(); + } + request_wake.and(response_wake).and(shutdown_result) + } +} + +impl HostActiveState { + fn acknowledge_request( + &self, + consumer: &mut ControlRingConsumer, + ) -> IoResult<()> { + let result = { + let status = self + .status + .lock() + .expect("broker host active-state mutex poisoned"); + if let HostActiveStatus::Failed(error) = &*status { + return Err(copy_io_error(error)); + } + consumer + .publish_head() + .map_err(Error::from) + .and_then(|()| consumer.wake_producer()) + }; + if let Err(error) = result { + let result = Err(copy_io_error(&error)); + let _ = self.fail(error); + return result; + } + Ok(()) + } + + fn fail(&self, error: Error) -> IoResult<()> { + { + let mut status = self + .status + .lock() + .expect("broker host active-state mutex poisoned"); + if matches!(*status, HostActiveStatus::Live) { + *status = HostActiveStatus::Failed(Arc::new(error)); + } + } + let request_wake = self.request_wait.interrupt_wait(); + let response_wake = self.response_wait.interrupt_wait(); + request_wake.and(response_wake).and(shutdown(&self.stream)) + } + + fn peer_closed(&self) { + { + let mut status = self + .status + .lock() + .expect("broker host active-state mutex poisoned"); + if matches!(*status, HostActiveStatus::Live) { + *status = HostActiveStatus::PeerClosed; + } + } + let _ = self.request_wait.interrupt_wait(); + let _ = self.response_wait.interrupt_wait(); + } + + fn request_terminal_result(&self) -> Option>> { + match &*self + .status + .lock() + .expect("broker host active-state mutex poisoned") + { + HostActiveStatus::Live => None, + HostActiveStatus::PeerClosed => Some(Ok(HostReceive::PeerClosed)), + HostActiveStatus::Failed(error) => Some(Err(copy_io_error(error))), + } + } + + fn request_failure(&self) -> Option { + match &*self + .status + .lock() + .expect("broker host active-state mutex poisoned") + { + HostActiveStatus::Failed(error) => Some(copy_io_error(error)), + HostActiveStatus::Live | HostActiveStatus::PeerClosed => None, + } + } + + fn try_publish_response( + &self, + producer: &mut ControlRingProducer, + frame: &[u8], + ) -> IoResult { + let result = { + let status = self + .status + .lock() + .expect("broker host active-state mutex poisoned"); + match &*status { + HostActiveStatus::Live => {} + HostActiveStatus::PeerClosed => { + return Err(Error::new( + ErrorKind::BrokenPipe, + "runner closed the active control channel", + )); + } + HostActiveStatus::Failed(error) => return Err(copy_io_error(error)), + } + producer + .try_write(frame) + .map_err(Error::from) + .and_then(|write_status| { + if matches!(write_status, ControlRingWriteStatus::Written) { + producer.wake_consumer()?; + } + Ok(write_status) + }) + }; + if let Err(error) = result { + let result = Err(copy_io_error(&error)); + let _ = self.fail(error); + return result; + } + result + } +} + +fn monitor_local_socket( + stream: &mut UnixStream, + failure_coordinator: &LocalActiveFailureCoordinator, ) { + let error = monitor_socket(stream, "broker"); + let _ = failure_coordinator.fail(error); +} + +fn monitor_host_socket(stream: &mut UnixStream, active: &HostActiveState) { + let mut byte = [0]; loop { - let response = match read_frame_with_deadline(&mut response_stream, None) { - Ok(Some(frame)) => match decode_response(&frame).map_err(wire_error) { - Ok(response) => response, - Err(error) => { - let _ = fail_active_channel( - &pending_calls, - &response_cancellation, - association_failure.as_ref(), - error, - ); - return; - } - }, - Ok(None) => { - let _ = fail_active_channel( - &pending_calls, - &response_cancellation, - association_failure.as_ref(), - Error::new( - ErrorKind::UnexpectedEof, - "broker closed the active control channel", - ), - ); + match stream.read(&mut byte) { + Ok(0) => { + active.peer_closed(); + return; + } + Ok(_) => { + let _ = active.fail(invalid_data( + "runner sent unexpected active control-socket data", + )); return; } + Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) => { - let _ = fail_active_channel( - &pending_calls, - &response_cancellation, - association_failure.as_ref(), - error, - ); + let _ = active.fail(error); return; } - }; + } + } +} - if let Err(error) = pending_calls.complete(response) { - let _ = fail_active_channel( - &pending_calls, - &response_cancellation, - association_failure.as_ref(), - error, - ); - return; +fn monitor_socket(stream: &mut UnixStream, peer: &'static str) -> Error { + let mut byte = [0]; + loop { + match stream.read(&mut byte) { + Ok(0) => { + return Error::new( + ErrorKind::UnexpectedEof, + format!("{peer} closed the active control channel"), + ); + } + Ok(_) => { + return invalid_data("peer sent unexpected active control-socket data"); + } + Err(error) if error.kind() == ErrorKind::Interrupted => {} + Err(error) => return error, } } } -fn fail_active_channel( - pending_calls: &PendingCalls, - shutdown_stream: &UnixStream, - association_failure: &(dyn Fn() + Send + Sync), - error: Error, -) -> IoResult<()> { - let first_failure = pending_calls.record_failure(Arc::new(error)); - let shutdown_result = shutdown(shutdown_stream); - if first_failure { - association_failure(); +fn dispatch_responses( + mut consumer: ControlRingConsumer, + failure_coordinator: Arc, +) { + loop { + match consumer.try_read(decode_response) { + Ok(ControlRingReadStatus::Message(response)) => { + if let Err(error) = consumer + .publish_head() + .map_err(Error::from) + .and_then(|()| consumer.wake_producer()) + .and_then(|()| failure_coordinator.pending_calls.complete(response)) + { + let _ = failure_coordinator.fail(error); + return; + } + } + Ok(ControlRingReadStatus::Empty { wait_epoch }) => { + if failure_coordinator + .pending_calls + .current_failure() + .is_some() + { + return; + } + if let Err(error) = consumer.wait_for_message(wait_epoch) { + let _ = failure_coordinator.fail(error); + return; + } + } + Err(ControlRingReadError::Ring(error)) => { + let _ = failure_coordinator.fail(Error::from(error)); + return; + } + Err(ControlRingReadError::Decode(error)) => { + let _ = failure_coordinator.fail(wire_error(error)); + return; + } + } } - shutdown_result } fn copy_io_error(error: &Error) -> Error { @@ -835,51 +1114,193 @@ fn wire_error(error: WireError) -> Error { ) } +impl From for Error { + fn from(error: ControlRingError) -> Self { + Self::new( + ErrorKind::InvalidData, + format!("invalid broker control ring: {error:?}"), + ) + } +} + #[cfg(test)] -mod tests { +mod control_ring_tests { use super::*; + use crate::control_ring::{ + CONTROL_RING_MEMORY_SIZE, CONTROL_RING_SLOT_COUNT, ControlRingProducer, + }; + use litebox_broker_protocol::channel::LocalControlChannel; use litebox_broker_protocol::message::{ BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, }; + use litebox_broker_protocol::wire::{ + decode_request, decode_response, encode_handshake_request, encode_response, + }; use litebox_broker_protocol::{ObjectHandle, RequestId}; + use std::io::{Read, Write}; + use std::os::fd::AsFd; + use std::sync::Barrier; use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::{Barrier, mpsc}; use std::time::Duration; - fn activate_test_channel( - stream: UnixStream, - association_failure: impl Fn() + Send + Sync + 'static, - ) -> ( - UnixStreamLocalControlChannel, - UnixStreamLocalControlCancellation, + type Producer = ControlRingProducer; + type Consumer = ControlRingConsumer; + + fn ring_pair() -> ( + ControlRing, + ControlRing, ) { - let mut channel = UnixStreamLocalControlChannel { + let first = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let second = MemfdSharedMemory::from_received_fd( + first.as_fd().try_clone_to_owned().unwrap(), + CONTROL_RING_MEMORY_SIZE, + ) + .unwrap(); + ( + ControlRing::new(first).unwrap(), + ControlRing::new(second).unwrap(), + ) + } + + fn negotiated_local(stream: UnixStream) -> UnixStreamLocalControlChannel { + UnixStreamLocalControlChannel { state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { stream, - setup_deadline: None, + setup_deadline: Some(Instant::now() + Duration::from_secs(2)), negotiated: true, }), - }; - let cancellation = channel.activate(association_failure).unwrap(); - (channel, cancellation) + } } - fn activate_counting_failure_channel( - stream: UnixStream, + fn activate_local( + association_failure: impl Fn() + Send + Sync + 'static, ) -> ( UnixStreamLocalControlChannel, UnixStreamLocalControlCancellation, - Arc, - mpsc::Receiver<()>, + Producer, + Consumer, + UnixStream, + ) { + let (local_stream, peer_stream) = UnixStream::pair().unwrap(); + let mut ack_stream = peer_stream.try_clone().unwrap(); + let acknowledgement = thread::spawn(move || { + assert_eq!( + read_frame_with_deadline(&mut ack_stream, None) + .unwrap() + .unwrap(), + CONTROL_RING_READY + ); + write_frame_with_deadline(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); + }); + let (local_ring, broker_ring) = ring_pair(); + let mut channel = negotiated_local(local_stream); + let cancellation = channel.activate(local_ring, association_failure).unwrap(); + acknowledgement.join().unwrap(); + let (response_producer, request_consumer) = broker_ring.into_broker(); + ( + channel, + cancellation, + response_producer, + request_consumer, + peer_stream, + ) + } + + fn split_host() -> ( + UnixStreamHostRequestSource, + UnixStreamHostResponseSink, + UnixStreamHostControlShutdown, + Producer, + Consumer, + UnixStream, ) { - let failure_count = Arc::new(AtomicUsize::new(0)); - let response_failure_count = Arc::clone(&failure_count); - let (failure_sender, failure_receiver) = mpsc::channel(); - let (channel, cancellation) = activate_test_channel(stream, move || { - response_failure_count.fetch_add(1, Ordering::SeqCst); - failure_sender.send(()).unwrap(); + let (peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut ack_stream = peer_stream.try_clone().unwrap(); + let acknowledgement = thread::spawn(move || { + write_frame_with_deadline(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); + assert_eq!( + read_frame_with_deadline(&mut ack_stream, None) + .unwrap() + .unwrap(), + CONTROL_RING_READY + ); }); - (channel, cancellation, failure_count, failure_receiver) + let (local_ring, host_ring) = ring_pair(); + let channel = UnixStreamHostControlChannel { + stream: host_stream, + peer_credential: PeerCredential::HostGuaranteed, + setup_deadline: Some(Instant::now() + Duration::from_secs(2)), + negotiated: true, + }; + let (source, sink, shutdown) = channel.into_active(host_ring).unwrap(); + acknowledgement.join().unwrap(); + let (request_producer, response_consumer) = local_ring.into_local(); + ( + source, + sink, + shutdown, + request_producer, + response_consumer, + peer_stream, + ) + } + + fn read_request(consumer: &mut Consumer) -> BrokerRequest { + loop { + match consumer.try_read(decode_request).unwrap() { + ControlRingReadStatus::Message(request) => { + consumer.publish_head().unwrap(); + consumer.wake_producer().unwrap(); + return request; + } + ControlRingReadStatus::Empty { wait_epoch } => { + consumer.wait_for_message(wait_epoch).unwrap(); + } + } + } + } + + fn read_response(consumer: &mut Consumer) -> BrokerResponse { + loop { + match consumer.try_read(decode_response).unwrap() { + ControlRingReadStatus::Message(response) => { + consumer.publish_head().unwrap(); + consumer.wake_producer().unwrap(); + return response; + } + ControlRingReadStatus::Empty { wait_epoch } => { + consumer.wait_for_message(wait_epoch).unwrap(); + } + } + } + } + + fn write_payload(producer: &mut Producer, payload: &[u8]) { + loop { + match producer.try_write(payload).unwrap() { + ControlRingWriteStatus::Written => { + producer.wake_consumer().unwrap(); + return; + } + ControlRingWriteStatus::Full { wait_epoch } => { + producer.wait_for_capacity(wait_epoch).unwrap(); + } + } + } + } + + fn request(id: u64) -> BrokerRequest { + BrokerRequest { + request_id: RequestId(id), + operation: BrokerOperation::CloseObject(ObjectHandle(id)), + } + } + + fn response(id: RequestId) -> BrokerResponse { + BrokerResponse { + request_id: id, + result: BrokerResult::ObjectClosed, + } } #[test] @@ -898,49 +1319,69 @@ mod tests { } #[test] - fn frame_round_trip() { + fn setup_frames_round_trip_and_reject_invalid_boundaries() { let (mut writer, mut reader) = UnixStream::pair().unwrap(); write_frame_with_deadline(&mut writer, &[1, 2, 3], None).unwrap(); - assert_eq!( read_frame_with_deadline(&mut reader, None) .unwrap() .unwrap(), [1, 2, 3] ); - } - #[test] - fn clean_eof_before_frame_is_close() { let (writer, mut reader) = UnixStream::pair().unwrap(); drop(writer); - assert!( read_frame_with_deadline(&mut reader, None) .unwrap() .is_none() ); - } - #[test] - fn local_control_channel_enforces_setup_and_active_phases() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); - assert_eq!( - channel - .call(BrokerRequest { - request_id: RequestId(0), - operation: BrokerOperation::CloseObject(ObjectHandle(1)), - }) + for frame_prefix in [ + vec![1, 0], + 0u32.to_le_bytes().to_vec(), + u32::try_from(MAX_FRAME_LEN + 1) + .unwrap() + .to_le_bytes() + .to_vec(), + ] { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&frame_prefix).unwrap(); + drop(writer); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + ErrorKind::InvalidData + ); + } + + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&4u32.to_le_bytes()).unwrap(); + writer.write_all(&[1, 2]).unwrap(); + drop(writer); + assert_eq!( + read_frame_with_deadline(&mut reader, None) .unwrap_err() .kind(), ErrorKind::InvalidData ); + } + #[test] + fn local_control_channel_enforces_setup_and_active_phases() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); + assert_eq!( + channel.call(request(0)).unwrap_err().kind(), + ErrorKind::InvalidData + ); + let (ring, _) = ring_pair(); assert_eq!( - channel.activate(|| {}).err().unwrap().kind(), + channel.activate(ring, || {}).err().unwrap().kind(), ErrorKind::InvalidData ); + let handshake_request = BrokerHandshakeRequest { protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, }; @@ -967,741 +1408,495 @@ mod tests { Some(BrokerHandshakeResponse::Negotiated { .. }) )); - let _cancellation = channel.activate(|| {}).unwrap(); + let acknowledgement = thread::spawn(move || { + assert_eq!( + read_frame_with_deadline(&mut host_stream, None) + .unwrap() + .unwrap(), + CONTROL_RING_READY + ); + write_frame_with_deadline(&mut host_stream, CONTROL_RING_READY, None).unwrap(); + host_stream + }); + let (ring, _) = ring_pair(); + let _cancellation = channel.activate(ring, || {}).unwrap(); + let mut host_stream = acknowledgement.join().unwrap(); + let (ring, _) = ring_pair(); assert_eq!( - channel.activate(|| {}).err().unwrap().kind(), + channel.activate(ring, || {}).err().unwrap().kind(), ErrorKind::InvalidData ); assert_eq!( channel - .send_handshake_request(&BrokerHandshakeRequest { - protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, - }) + .send_handshake_request(&handshake_request) .unwrap_err() .kind(), ErrorKind::InvalidData ); - let channel = Arc::new(channel); - let call_channel = Arc::clone(&channel); - let call = thread::spawn(move || { - call_channel.call(BrokerRequest { - request_id: RequestId(1), - operation: BrokerOperation::CloseObject(ObjectHandle(1)), - }) - }); - let request = decode_request( - &read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(), - ) - .unwrap(); - write_frame_with_deadline( - &mut host_stream, - &encode_response(BrokerResponse { - request_id: request.request_id, - result: BrokerResult::ObjectClosed, - }), - None, - ) - .unwrap(); - assert_eq!(call.join().unwrap().unwrap().request_id, RequestId(1)); + host_stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + drop(channel); + let mut byte = [0]; + assert_eq!(host_stream.read(&mut byte).unwrap(), 0); } #[test] - fn active_channel_matches_out_of_order_responses() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let (channel, _cancellation) = activate_test_channel(local_stream, || {}); - let channel = Arc::new(channel); - - let first_channel = Arc::clone(&channel); - let first = thread::spawn(move || { - first_channel.call(BrokerRequest { - request_id: RequestId(3), - operation: BrokerOperation::CloseObject(ObjectHandle(3)), - }) - }); - let second_channel = Arc::clone(&channel); - let second = thread::spawn(move || { - second_channel.call(BrokerRequest { - request_id: RequestId(7), - operation: BrokerOperation::CloseObject(ObjectHandle(7)), - }) - }); + fn two_way_ready_ack_activates_ring_transport() { + let (local_stream, host_stream) = UnixStream::pair().unwrap(); + let (local_ring, host_ring) = ring_pair(); + let mut local = negotiated_local(local_stream); + let host = UnixStreamHostControlChannel { + stream: host_stream, + peer_credential: PeerCredential::HostGuaranteed, + setup_deadline: Some(Instant::now() + Duration::from_secs(2)), + negotiated: true, + }; + let host_active = thread::spawn(move || host.into_active(host_ring).unwrap()); + let _cancellation = local.activate(local_ring, || {}).unwrap(); + let (mut source, sink, _shutdown) = host_active.join().unwrap(); - let first_request = decode_request( - &read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(), - ) - .unwrap(); - let second_request = decode_request( - &read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(), - ) - .unwrap(); - let mut request_ids = [first_request.request_id, second_request.request_id]; - request_ids.sort(); - assert_eq!(request_ids, [RequestId(3), RequestId(7)]); + let caller = thread::spawn(move || local.call(request(7))); + let HostReceive::Message(received) = source.recv_request().unwrap() else { + panic!("expected ring request"); + }; + sink.send_response(&response(received.request_id)).unwrap(); + assert_eq!(caller.join().unwrap().unwrap().request_id, RequestId(7)); + } - write_frame_with_deadline( - &mut host_stream, - &encode_response(BrokerResponse { - request_id: second_request.request_id, - result: BrokerResult::ObjectClosed, - }), - None, - ) - .unwrap(); - write_frame_with_deadline( - &mut host_stream, - &encode_response(BrokerResponse { - request_id: first_request.request_id, - result: BrokerResult::ObjectClosed, - }), - None, - ) - .unwrap(); + #[test] + fn local_matches_out_of_order_ring_responses_without_socket_frames() { + let (channel, _cancellation, mut responses, mut requests, mut peer) = activate_local(|| {}); + peer.set_read_timeout(Some(Duration::from_millis(100))) + .unwrap(); + let channel = Arc::new(channel); + let calls = [3, 7].map(|id| { + let channel = Arc::clone(&channel); + thread::spawn(move || channel.call(request(id))) + }); + let first = read_request(&mut requests); + let second = read_request(&mut requests); - assert_eq!(first.join().unwrap().unwrap().request_id, RequestId(3)); - assert_eq!(second.join().unwrap().unwrap().request_id, RequestId(7)); + write_payload( + &mut responses, + &encode_response(response(second.request_id)), + ); + write_payload(&mut responses, &encode_response(response(first.request_id))); + for call in calls { + assert!(call.join().unwrap().is_ok()); + } + let mut byte = [0]; + assert!(matches!( + peer.read(&mut byte).unwrap_err().kind(), + ErrorKind::WouldBlock | ErrorKind::TimedOut + )); } #[test] - fn active_channel_bounds_pending_calls_before_publication() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - host_stream - .set_read_timeout(Some(Duration::from_secs(2))) - .unwrap(); - let (channel, cancellation) = activate_test_channel(local_stream, || {}); + fn pending_capacity_blocks_before_sixty_fifth_publication() { + let (channel, cancellation, mut responses, mut requests, _peer) = activate_local(|| {}); let channel = Arc::new(channel); - let call_start = Arc::new(Barrier::new(MAX_PENDING_CALLS + 2)); + let start = Arc::new(Barrier::new(MAX_PENDING_CALLS + 2)); let callers = (0..=MAX_PENDING_CALLS) - .map(|request_id| { + .map(|id| { let channel = Arc::clone(&channel); - let call_start = Arc::clone(&call_start); + let start = Arc::clone(&start); thread::spawn(move || { - call_start.wait(); - channel.call(BrokerRequest { - request_id: RequestId(request_id as u64), - operation: BrokerOperation::CloseObject(ObjectHandle(request_id as u64)), - }) + start.wait(); + channel.call(request(id as u64)) }) }) .collect::>(); + start.wait(); - call_start.wait(); - let mut published_request_ids = Vec::with_capacity(MAX_PENDING_CALLS); + let mut published = Vec::new(); for _ in 0..MAX_PENDING_CALLS { - let frame = read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(); - published_request_ids.push(decode_request(&frame).unwrap().request_id); + published.push(read_request(&mut requests).request_id); } - host_stream - .set_read_timeout(Some(Duration::from_millis(100))) - .unwrap(); - let error = read_frame_with_deadline(&mut host_stream, None).unwrap_err(); - assert!(matches!( - error.kind(), - ErrorKind::WouldBlock | ErrorKind::TimedOut - )); - - write_frame_with_deadline( - &mut host_stream, - &encode_response(BrokerResponse { - request_id: published_request_ids[0], - result: BrokerResult::ObjectClosed, - }), - None, - ) - .unwrap(); - host_stream - .set_read_timeout(Some(Duration::from_secs(2))) - .unwrap(); - let released_request = decode_request( - &read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(), - ) - .unwrap(); - assert!(!published_request_ids.contains(&released_request.request_id)); + write_payload(&mut responses, &encode_response(response(published[0]))); + let released = read_request(&mut requests).request_id; + assert!(!published.contains(&released)); cancellation.cancel().unwrap(); - let mut completed = 0; - let mut failed = 0; - for caller in callers { - match caller.join().unwrap() { - Ok(_) => completed += 1, - Err(_) => failed += 1, - } - } + let completed = callers + .into_iter() + .map(|caller| usize::from(caller.join().unwrap().is_ok())) + .sum::(); assert_eq!(completed, 1); - assert_eq!(failed, MAX_PENDING_CALLS); } #[test] - fn unknown_response_identifier_fails_all_pending_calls() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let (channel, _cancellation, failure_count, failure_receiver) = - activate_counting_failure_channel(local_stream); - let channel = Arc::new(channel); - let callers = [1, 2].map(|request_id| { - let channel = Arc::clone(&channel); - thread::spawn(move || { - channel.call(BrokerRequest { - request_id: RequestId(request_id), - operation: BrokerOperation::CloseObject(ObjectHandle(request_id)), - }) - }) - }); - - for _ in 0..callers.len() { - let frame = read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(); - decode_request(&frame).unwrap(); - } - write_frame_with_deadline( - &mut host_stream, - &encode_response(BrokerResponse { - request_id: RequestId(99), - result: BrokerResult::ObjectClosed, - }), - None, - ) - .unwrap(); + fn unknown_duplicate_and_malformed_responses_fail_closed() { + for payload_kind in 0..3 { + let failures = Arc::new(AtomicUsize::new(0)); + let callback_failures = Arc::clone(&failures); + let (channel, _cancellation, mut responses, mut requests, _peer) = + activate_local(move || { + callback_failures.fetch_add(1, Ordering::SeqCst); + }); + let channel = Arc::new(channel); + let calls = [1, 2].map(|id| { + let channel = Arc::clone(&channel); + thread::spawn(move || channel.call(request(id))) + }); + read_request(&mut requests); + read_request(&mut requests); + + match payload_kind { + 0 => write_payload(&mut responses, &encode_response(response(RequestId(99)))), + 1 => { + let duplicate = encode_response(response(RequestId(1))); + write_payload(&mut responses, &duplicate); + write_payload(&mut responses, &duplicate); + } + _ => write_payload(&mut responses, &[u8::MAX]), + } - for caller in callers { - assert_eq!( - caller.join().unwrap().unwrap_err().kind(), - ErrorKind::InvalidData - ); + let results = calls.map(|call| call.join().unwrap()); + let error_count = results.iter().filter(|result| result.is_err()).count(); + assert_eq!(error_count, if payload_kind == 1 { 1 } else { 2 }); + assert_eq!(failures.load(Ordering::SeqCst), 1); } - failure_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - assert_eq!(failure_count.load(Ordering::SeqCst), 1); } #[test] - fn malformed_response_fails_all_pending_calls() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let (channel, _cancellation, failure_count, failure_receiver) = - activate_counting_failure_channel(local_stream); - let channel = Arc::new(channel); - let callers = [1, 2].map(|request_id| { - let channel = Arc::clone(&channel); - thread::spawn(move || { - channel.call(BrokerRequest { - request_id: RequestId(request_id), - operation: BrokerOperation::CloseObject(ObjectHandle(request_id)), - }) - }) - }); - - for _ in 0..callers.len() { - let frame = read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(); - decode_request(&frame).unwrap(); - } - write_frame_with_deadline(&mut host_stream, &[u8::MAX], None).unwrap(); - - for caller in callers { - assert_eq!( - caller.join().unwrap().unwrap_err().kind(), - ErrorKind::InvalidData - ); + fn local_socket_eof_and_cancellation_wake_pending_calls() { + for close_peer in [false, true] { + let (channel, cancellation, _responses, mut requests, peer) = activate_local(|| {}); + let caller = thread::spawn(move || channel.call(request(1))); + read_request(&mut requests); + if close_peer { + drop(peer); + } else { + cancellation.cancel().unwrap(); + } + assert!(caller.join().unwrap().is_err()); } - failure_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - assert_eq!(failure_count.load(Ordering::SeqCst), 1); } #[test] - fn response_eof_fails_all_pending_calls() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let (channel, _cancellation, failure_count, failure_receiver) = - activate_counting_failure_channel(local_stream); - let channel = Arc::new(channel); - let callers = [1, 2].map(|request_id| { - let channel = Arc::clone(&channel); - thread::spawn(move || { - channel.call(BrokerRequest { - request_id: RequestId(request_id), - operation: BrokerOperation::CloseObject(ObjectHandle(request_id)), - }) + fn host_split_decodes_requests_and_cloned_sinks_publish_complete_responses() { + let (mut source, sink, _shutdown, mut requests, mut responses, _peer) = split_host(); + write_payload(&mut requests, &encode_request(request(1))); + assert!(matches!( + source.recv_request().unwrap(), + HostReceive::Message(BrokerRequest { + request_id: RequestId(1), + .. }) - }); + )); - for _ in 0..callers.len() { - let frame = read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(); - decode_request(&frame).unwrap(); - } - drop(host_stream); + let first = sink.clone(); + let writer = thread::spawn(move || first.send_response(&response(RequestId(3)))); + sink.send_response(&response(RequestId(7))).unwrap(); + writer.join().unwrap().unwrap(); + let mut ids = [ + read_response(&mut responses).request_id, + read_response(&mut responses).request_id, + ]; + ids.sort(); + assert_eq!(ids, [RequestId(3), RequestId(7)]); + } - for caller in callers { - assert_eq!( - caller.join().unwrap().unwrap_err().kind(), - ErrorKind::UnexpectedEof - ); - } - failure_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - assert_eq!(failure_count.load(Ordering::SeqCst), 1); + #[test] + fn host_clean_close_wakes_request_wait_as_peer_closed() { + let (mut source, _sink, _shutdown, _requests, _responses, peer) = split_host(); + let receiver = thread::spawn(move || source.recv_request()); + drop(peer); + assert_eq!(receiver.join().unwrap().unwrap(), HostReceive::PeerClosed); } #[test] - fn request_write_failure_fails_existing_pending_calls() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let (channel, _cancellation, failure_count, failure_receiver) = - activate_counting_failure_channel(local_stream); - let channel = Arc::new(channel); - let pending_callers = [1, 2].map(|request_id| { - let channel = Arc::clone(&channel); - thread::spawn(move || { - channel.call(BrokerRequest { - request_id: RequestId(request_id), - operation: BrokerOperation::CloseObject(ObjectHandle(request_id)), - }) - }) - }); - for _ in 0..pending_callers.len() { - let frame = read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(); - decode_request(&frame).unwrap(); - } + fn host_failure_preempts_queued_and_decoded_requests_but_peer_close_drains() { + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + write_payload(&mut requests, &encode_request(request(1))); + source + .active + .fail(Error::new(ErrorKind::TimedOut, "test failure")) + .unwrap(); + assert_eq!( + source.recv_request().unwrap_err().kind(), + ErrorKind::TimedOut + ); + + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + write_payload(&mut requests, &encode_request(request(2))); + assert!(matches!( + source.consumer.try_read(decode_request).unwrap(), + ControlRingReadStatus::Message(_) + )); + source + .active + .fail(Error::new(ErrorKind::TimedOut, "test failure")) + .unwrap(); + assert_eq!( + source + .active + .acknowledge_request(&mut source.consumer) + .unwrap_err() + .kind(), + ErrorKind::TimedOut + ); - host_stream.shutdown(Shutdown::Read).unwrap(); - let failing_channel = Arc::clone(&channel); - let failing_caller = thread::spawn(move || { - failing_channel.call(BrokerRequest { + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + write_payload(&mut requests, &encode_request(request(3))); + source.active.peer_closed(); + assert!(matches!( + source.recv_request().unwrap(), + HostReceive::Message(BrokerRequest { request_id: RequestId(3), - operation: BrokerOperation::CloseObject(ObjectHandle(3)), + .. }) - }); - - assert!(failing_caller.join().unwrap().is_err()); - for caller in pending_callers { - assert!(caller.join().unwrap().is_err()); - } - failure_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - assert_eq!(failure_count.load(Ordering::SeqCst), 1); + )); + assert_eq!(source.recv_request().unwrap(), HostReceive::PeerClosed); } #[test] - fn duplicate_response_identifier_fails_other_pending_calls() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let (channel, _cancellation, failure_count, failure_receiver) = - activate_counting_failure_channel(local_stream); - let channel = Arc::new(channel); - let first_channel = Arc::clone(&channel); - let first = thread::spawn(move || { - first_channel.call(BrokerRequest { - request_id: RequestId(1), - operation: BrokerOperation::CloseObject(ObjectHandle(1)), - }) - }); - let second_channel = Arc::clone(&channel); - let second = thread::spawn(move || { - second_channel.call(BrokerRequest { - request_id: RequestId(2), - operation: BrokerOperation::CloseObject(ObjectHandle(2)), - }) - }); + fn dropping_host_shutdown_guard_wakes_request_wait_and_closes_socket() { + let (mut source, sink, shutdown, _requests, _responses, mut peer) = split_host(); + peer.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); + let receiver = thread::spawn(move || source.recv_request()); - for _ in 0..2 { - let frame = read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(); - decode_request(&frame).unwrap(); - } - let response = encode_response(BrokerResponse { - request_id: RequestId(1), - result: BrokerResult::ObjectClosed, - }); - write_frame_with_deadline(&mut host_stream, &response, None).unwrap(); - write_frame_with_deadline(&mut host_stream, &response, None).unwrap(); + drop(sink); + drop(shutdown); - assert_eq!(first.join().unwrap().unwrap().request_id, RequestId(1)); assert_eq!( - second.join().unwrap().unwrap_err().kind(), - ErrorKind::InvalidData + receiver.join().unwrap().unwrap_err().kind(), + ErrorKind::ConnectionAborted ); - failure_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - assert_eq!(failure_count.load(Ordering::SeqCst), 1); - } - - #[test] - fn completed_call_wins_over_later_association_failure() { - let pending = PendingCalls::new(); - let request_id = RequestId(1); - let pending_call = pending.register(request_id).unwrap(); - pending - .complete(BrokerResponse { - request_id, - result: BrokerResult::ObjectClosed, - }) - .unwrap(); - pending.record_failure(Arc::new(Error::new( - ErrorKind::ConnectionAborted, - "test failure", - ))); - - assert_eq!(pending_call.wait().unwrap().request_id, request_id); + let mut byte = [0]; + assert_eq!(peer.read(&mut byte).unwrap(), 0); } #[test] - fn duplicate_pending_registration_preserves_the_original_call() { - let pending = PendingCalls::new(); - let request_id = RequestId(1); - let pending_call = pending.register(request_id).unwrap(); + fn host_close_wakes_response_producer_blocked_on_full_ring() { + let (_source, sink, _shutdown, _requests, _responses, peer) = split_host(); + for id in 0..CONTROL_RING_SLOT_COUNT { + sink.send_response(&response(RequestId(id))).unwrap(); + } + let blocked_sink = sink.clone(); + let blocked = thread::spawn(move || blocked_sink.send_response(&response(RequestId(99)))); + thread::sleep(Duration::from_millis(20)); + drop(peer); assert_eq!( - pending.register(request_id).err().unwrap().kind(), - ErrorKind::InvalidData + blocked.join().unwrap().unwrap_err().kind(), + ErrorKind::BrokenPipe ); - pending - .complete(BrokerResponse { - request_id, - result: BrokerResult::ObjectClosed, - }) - .unwrap(); - - assert_eq!(pending_call.wait().unwrap().request_id, request_id); } #[test] - fn association_failure_wins_before_completion() { - let pending = PendingCalls::new(); - let request_id = RequestId(1); - let pending_call = pending.register(request_id).unwrap(); - pending.record_failure(Arc::new(Error::new( - ErrorKind::ConnectionAborted, - "test failure", - ))); - - assert!( - pending - .complete(BrokerResponse { - request_id, - result: BrokerResult::ObjectClosed, - }) - .is_err() + fn host_reports_wrong_phase_ring_message_as_protocol_violation() { + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + write_payload( + &mut requests, + &encode_handshake_request(BrokerHandshakeRequest { + protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }), ); assert_eq!( - pending_call.wait().unwrap_err().kind(), - ErrorKind::ConnectionAborted + source.recv_request().unwrap(), + HostReceive::ProtocolViolation ); } #[test] - fn malformed_frames_are_invalid() { - let (mut writer, mut reader) = UnixStream::pair().unwrap(); - writer.write_all(&[1, 0]).unwrap(); - drop(writer); + fn malformed_host_ring_request_is_fatal_invalid_data() { + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + write_payload(&mut requests, &[u8::MAX]); assert_eq!( - read_frame_with_deadline(&mut reader, None) - .unwrap_err() - .kind(), + source.recv_request().unwrap_err().kind(), ErrorKind::InvalidData ); + } - let (mut writer, mut reader) = UnixStream::pair().unwrap(); - writer.write_all(&0u32.to_le_bytes()).unwrap(); + #[test] + fn host_setup_rejects_active_frames_and_requires_negotiation() { + let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); + write_frame_with_deadline(&mut peer_stream, &encode_request(request(0)), None).unwrap(); assert_eq!( - read_frame_with_deadline(&mut reader, None) - .unwrap_err() - .kind(), - ErrorKind::InvalidData + channel.recv_handshake_request().unwrap(), + HostReceive::ProtocolViolation ); - let (mut writer, mut reader) = UnixStream::pair().unwrap(); - writer - .write_all(&u32::try_from(MAX_FRAME_LEN + 1).unwrap().to_le_bytes()) - .unwrap(); - assert_eq!( - read_frame_with_deadline(&mut reader, None) - .unwrap_err() - .kind(), - ErrorKind::InvalidData - ); + let (_peer_stream, host_stream) = UnixStream::pair().unwrap(); + let channel = UnixStreamHostControlChannel::from_accepted(host_stream); + let (ring, _) = ring_pair(); + let Err(error) = channel.into_active(ring) else { + panic!("host control channel activated before negotiation"); + }; + assert_eq!(error.kind(), ErrorKind::InvalidData); + } - let (mut writer, mut reader) = UnixStream::pair().unwrap(); - writer.write_all(&4u32.to_le_bytes()).unwrap(); - writer.write_all(&[1, 2]).unwrap(); - drop(writer); - assert_eq!( - read_frame_with_deadline(&mut reader, None) - .unwrap_err() - .kind(), - ErrorKind::InvalidData - ); + #[test] + fn ready_ack_uses_absolute_setup_deadline() { + let (local_stream, _peer) = UnixStream::pair().unwrap(); + let (ring, _) = ring_pair(); + let mut local = UnixStreamLocalControlChannel { + state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { + stream: local_stream, + setup_deadline: Some(Instant::now() + Duration::from_millis(30)), + negotiated: true, + }), + }; + let Err(error) = local.activate(ring, || {}) else { + panic!("activation unexpectedly succeeded"); + }; + assert!(matches!( + error.kind(), + ErrorKind::WouldBlock | ErrorKind::TimedOut + )); } #[test] - fn local_handshake_response_read_setup_deadline_is_wall_clock() { + fn handshake_reads_use_absolute_setup_deadlines() { let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamLocalControlChannel { + let mut local = UnixStreamLocalControlChannel { state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { stream: local_stream, setup_deadline: Some(Instant::now() + Duration::from_millis(50)), negotiated: false, }), }; - - let reader = std::thread::spawn(move || channel.recv_handshake_response().unwrap_err()); + let local_reader = thread::spawn(move || local.recv_handshake_response().unwrap_err()); host_stream.write_all(&8u32.to_le_bytes()).unwrap(); for _ in 0..8 { - std::thread::sleep(Duration::from_millis(20)); + thread::sleep(Duration::from_millis(20)); if host_stream.write_all(&[0]).is_err() { break; } } - - let error = reader.join().expect("timeout reader panicked"); + let error = local_reader.join().unwrap(); assert!( matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut), - "unexpected timeout error kind: {error:?}" + "unexpected local timeout error: {error:?}" ); - } - #[test] - fn host_handshake_request_read_setup_deadline_is_wall_clock() { let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamHostControlChannel::from_host_guaranteed( + let mut host = UnixStreamHostControlChannel::from_host_guaranteed( host_stream, Instant::now() + Duration::from_millis(50), ); - - let reader = std::thread::spawn(move || channel.recv_handshake_request().unwrap_err()); + let host_reader = thread::spawn(move || host.recv_handshake_request().unwrap_err()); local_stream.write_all(&8u32.to_le_bytes()).unwrap(); for _ in 0..8 { - std::thread::sleep(Duration::from_millis(20)); + thread::sleep(Duration::from_millis(20)); if local_stream.write_all(&[0]).is_err() { break; } } - - let error = reader.join().expect("timeout reader panicked"); + let error = host_reader.join().unwrap(); assert!( matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut), - "unexpected timeout error kind: {error:?}" + "unexpected host timeout error: {error:?}" ); } #[test] - fn negotiated_host_handshake_restores_active_timeouts() { - let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); - let active_read_timeout = Some(Duration::from_secs(2)); - let active_write_timeout = Some(Duration::from_secs(3)); - host_stream.set_read_timeout(active_read_timeout).unwrap(); - host_stream.set_write_timeout(active_write_timeout).unwrap(); - let mut channel = UnixStreamHostControlChannel::from_host_guaranteed( - host_stream, - Instant::now() + Duration::from_secs(1), + fn notification_frame_round_trips() { + let (local_stream, host_stream) = UnixStream::pair().unwrap(); + let mut local = UnixStreamLocalNotificationChannel::from_connected(local_stream); + let mut host = UnixStreamHostNotificationChannel::from_accepted(host_stream); + let notification = BrokerNotification::Readiness( + litebox_broker_protocol::message::ReadinessNotification { + handle: ObjectHandle(7), + readiness: litebox_broker_protocol::readiness::ReadinessFlags::READ, + }, ); - let request = BrokerHandshakeRequest { - protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, - }; - write_frame_with_deadline( - &mut local_stream, - &encode_handshake_request(request.clone()), - None, - ) - .unwrap(); - assert_eq!( - channel.recv_handshake_request().unwrap(), - HostReceive::Message(request) - ); - channel - .send_handshake_response(&BrokerHandshakeResponse::Negotiated { - broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, - }) - .unwrap(); + host.send_notification(¬ification).unwrap(); - assert_eq!(channel.setup_deadline, None); - assert_eq!(channel.stream.read_timeout().unwrap(), active_read_timeout); - assert_eq!( - channel.stream.write_timeout().unwrap(), - active_write_timeout - ); + assert_eq!(local.recv_notification().unwrap(), Some(notification)); } #[test] - fn host_control_requires_negotiation_before_active_split() { - let (_peer_stream, host_stream) = UnixStream::pair().unwrap(); - let channel = UnixStreamHostControlChannel::from_accepted(host_stream); + fn completed_call_wins_over_later_failure_and_failure_wins_before_completion() { + let pending = PendingCalls::new(); + let completed = pending.register(RequestId(1)).unwrap(); + pending.complete(response(RequestId(1))).unwrap(); + pending.record_failure(Arc::new(Error::new( + ErrorKind::ConnectionAborted, + "test failure", + ))); + assert_eq!(completed.wait().unwrap().request_id, RequestId(1)); - assert!(channel.into_active().is_err()); + let pending = PendingCalls::new(); + let failed = pending.register(RequestId(2)).unwrap(); + pending.record_failure(Arc::new(Error::new( + ErrorKind::ConnectionAborted, + "test failure", + ))); + assert!(pending.complete(response(RequestId(2))).is_err()); + assert_eq!( + failed.wait().unwrap_err().kind(), + ErrorKind::ConnectionAborted + ); } #[test] - fn concurrent_host_response_sinks_write_complete_frames() { - let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); - channel - .send_handshake_response(&BrokerHandshakeResponse::Negotiated { - broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, - }) - .unwrap(); - let handshake = read_frame_with_deadline(&mut peer_stream, None) - .unwrap() - .unwrap(); - assert!(matches!( - decode_handshake_response(&handshake).unwrap(), - BrokerHandshakeResponse::Negotiated { .. } - )); - let (_request_source, response_sink, _shutdown) = channel.into_active().unwrap(); - let first_sink = response_sink.clone(); - let first = std::thread::spawn(move || { - first_sink - .send_response(&BrokerResponse { - request_id: RequestId(1), - result: BrokerResult::ObjectClosed, + fn failure_recording_waits_for_in_progress_publication() { + let pending = Arc::new(PendingCalls::new()); + let pending_call = pending.register(RequestId(1)).unwrap(); + let publication_state = Arc::new(AtomicUsize::new(0)); + let (publication_started, wait_for_publication) = std::sync::mpsc::sync_channel(0); + let (release_publication, publication_released) = std::sync::mpsc::sync_channel(0); + let publisher_pending = Arc::clone(&pending); + let publisher_state = Arc::clone(&publication_state); + let publisher = thread::spawn(move || { + publisher_pending + .run_if_live(|| { + publisher_state.store(1, Ordering::Release); + publication_started.send(()).unwrap(); + publication_released.recv().unwrap(); + publisher_state.store(2, Ordering::Release); + Ok(()) }) .unwrap(); }); - let second = std::thread::spawn(move || { - response_sink - .send_response(&BrokerResponse { - request_id: RequestId(2), - result: BrokerResult::ObjectClosed, - }) - .unwrap(); - }); - - let mut response_ids = [RequestId(0); 2]; - for response_id in &mut response_ids { - let frame = read_frame_with_deadline(&mut peer_stream, None) - .unwrap() - .unwrap(); - *response_id = decode_response(&frame).unwrap().request_id; - } - response_ids.sort(); - assert_eq!(response_ids, [RequestId(1), RequestId(2)]); - first.join().unwrap(); - second.join().unwrap(); - } - - #[test] - fn local_control_cancellation_unblocks_pending_call() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - host_stream - .set_read_timeout(Some(Duration::from_secs(1))) - .unwrap(); - let (channel, cancellation) = activate_test_channel(local_stream, || {}); - let (result_sender, result_receiver) = mpsc::sync_channel(1); - let caller = std::thread::spawn(move || { - result_sender - .send(channel.call(BrokerRequest { - request_id: RequestId(0), - operation: BrokerOperation::CloseObject(litebox_broker_protocol::ObjectHandle( - 1, - )), - })) - .unwrap(); + wait_for_publication.recv().unwrap(); + + let (failure_started, wait_for_failure) = std::sync::mpsc::sync_channel(0); + let (failure_recorded, wait_for_recording) = std::sync::mpsc::sync_channel(0); + let failure_pending = Arc::clone(&pending); + let failure_state = Arc::clone(&publication_state); + let failure = thread::spawn(move || { + failure_started.send(()).unwrap(); + failure_pending.record_failure(Arc::new(Error::new( + ErrorKind::ConnectionAborted, + "test failure", + ))); + assert_eq!(failure_state.load(Ordering::Acquire), 2); + failure_recorded.send(()).unwrap(); }); - - let frame = read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(); - decode_request(&frame).unwrap(); + wait_for_failure.recv().unwrap(); assert!(matches!( - result_receiver.try_recv(), - Err(mpsc::TryRecvError::Empty) + wait_for_recording.recv_timeout(Duration::from_millis(20)), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) )); - cancellation.cancel().unwrap(); - - assert_eq!( - result_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap() - .unwrap_err() - .kind(), - ErrorKind::ConnectionAborted - ); - caller.join().unwrap(); - } - #[test] - fn dropping_local_control_closes_connection_with_cancellation_clone() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let (channel, _cancellation) = activate_test_channel(local_stream, || {}); - host_stream - .set_read_timeout(Some(Duration::from_secs(1))) + release_publication.send(()).unwrap(); + publisher.join().unwrap(); + wait_for_recording + .recv_timeout(Duration::from_secs(1)) .unwrap(); - - drop(channel); - - let mut byte = [0]; - assert_eq!(host_stream.read(&mut byte).unwrap(), 0); - } - - #[test] - fn host_reports_wrong_phase_request_frames_as_protocol_violations() { - let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); - write_frame_with_deadline( - &mut peer_stream, - &encode_request(BrokerRequest { - request_id: RequestId(0), - operation: BrokerOperation::Event( - litebox_broker_protocol::message::EventRequest::Create( - litebox_broker_protocol::event::CreateEventRequest { initial_count: 0 }, - ), - ), - }), - None, - ) - .unwrap(); - assert_eq!( - channel.recv_handshake_request().unwrap(), - HostReceive::ProtocolViolation - ); - - let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); - write_frame_with_deadline( - &mut peer_stream, - &encode_handshake_request(BrokerHandshakeRequest { - protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, - }), - None, - ) - .unwrap(); + failure.join().unwrap(); assert_eq!( - channel.recv_request().unwrap(), - HostReceive::ProtocolViolation + pending_call.wait().unwrap_err().kind(), + ErrorKind::ConnectionAborted ); } #[test] - fn notification_frame_round_trip() { - let (local_stream, host_stream) = UnixStream::pair().unwrap(); - let mut local = UnixStreamLocalNotificationChannel::from_connected(local_stream); - let mut host = UnixStreamHostNotificationChannel::from_accepted(host_stream); - let notification = BrokerNotification::Readiness( - litebox_broker_protocol::message::ReadinessNotification { - handle: litebox_broker_protocol::ObjectHandle(7), - readiness: litebox_broker_protocol::readiness::ReadinessFlags::READ, - }, - ); - - host.send_notification(¬ification).unwrap(); - - assert_eq!(local.recv_notification().unwrap(), Some(notification)); + fn duplicate_pending_registration_preserves_original() { + let pending = PendingCalls::new(); + let original = pending.register(RequestId(1)).unwrap(); + let Err(error) = pending.register(RequestId(1)) else { + panic!("duplicate registration unexpectedly succeeded"); + }; + assert_eq!(error.kind(), ErrorKind::InvalidData); + pending.complete(response(RequestId(1))).unwrap(); + assert_eq!(original.wait().unwrap().request_id, RequestId(1)); } } diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 4569b63077..6137ff1edb 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -22,6 +22,7 @@ use litebox_broker_protocol::message::BrokerRequest; use litebox_broker_protocol::shared_memory::{ SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, SharedMemory, }; +use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ UnixStreamHostControlChannel, UnixStreamHostControlShutdown, UnixStreamHostNotificationChannel, @@ -111,6 +112,9 @@ fn serve_runner( )?; let shared_memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE)?; let shared_buffers = SharedBufferPool::new(shared_memory, SHARED_BUFFER_LAYOUT)?; + let control_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE)?; + let control_ring = ControlRing::new(control_memory) + .map_err(|error| IoError::other(format!("failed to create control ring: {error:?}")))?; let mut control_channel = UnixStreamHostControlChannel::from_host_guaranteed(control_stream, setup_deadline); let _notification_channel = @@ -118,6 +122,7 @@ fn serve_runner( let association = match setup_connection(broker, &mut control_channel, &shared_buffers, |channel| { channel.send_memfd(shared_buffers.memory(), Some(setup_deadline))?; + channel.send_memfd(control_ring.memory(), Some(setup_deadline))?; Ok(()) })? { Ok(association) => association, @@ -143,7 +148,7 @@ fn serve_runner( .into()); } }; - let (request_source, response_sink, shutdown) = control_channel.into_active()?; + let (request_source, response_sink, shutdown) = control_channel.into_active(control_ring)?; dispatch_requests(association, request_source, response_sink, shutdown)?; Ok(()) } @@ -155,7 +160,7 @@ fn dispatch_requests( shutdown: UnixStreamHostControlShutdown, ) -> IoResult<()> { let association = Arc::new(association); - let failure = Arc::new(HostAssociationFailure::new(shutdown)); + let failure_coordinator = Arc::new(HostAssociationFailureCoordinator::new(shutdown)); let (request_sender, request_receiver) = sync_channel(REQUEST_QUEUE_CAPACITY); let request_receiver = Arc::new(Mutex::new(request_receiver)); @@ -165,7 +170,7 @@ fn dispatch_requests( let association = Arc::clone(&association); let request_receiver = Arc::clone(&request_receiver); let response_sink = response_sink.clone(); - let worker_failure = Arc::clone(&failure); + let worker_failure_coordinator = Arc::clone(&failure_coordinator); match std::thread::Builder::new() .name(format!("litebox-broker-worker-{worker_id}")) .spawn_scoped(scope, move || { @@ -173,26 +178,26 @@ fn dispatch_requests( &association, &request_receiver, &response_sink, - &worker_failure, + &worker_failure_coordinator, ); }) { Ok(worker) => workers.push(worker), Err(error) => { - failure.report(error); + failure_coordinator.report(error); break; } } } - read_requests(&mut request_source, request_sender, &failure); + read_requests(&mut request_source, request_sender, &failure_coordinator); for worker in workers { if worker.join().is_err() { - failure.report(IoError::other("broker request worker panicked")); + failure_coordinator.report(IoError::other("broker request worker panicked")); } } }); - match failure.take_error() { + match failure_coordinator.take_error() { Some(error) => Err(error), None => Ok(()), } @@ -201,16 +206,16 @@ fn dispatch_requests( fn read_requests( request_source: &mut UnixStreamHostRequestSource, request_sender: SyncSender, - failure: &HostAssociationFailure, + failure_coordinator: &HostAssociationFailureCoordinator, ) { loop { - if failure.failed() { + if failure_coordinator.failed() { break; } match request_source.recv_request() { Ok(HostReceive::Message(request)) => { if request_sender.send(request).is_err() { - failure.report(IoError::new( + failure_coordinator.report(IoError::new( ErrorKind::BrokenPipe, "broker request workers stopped", )); @@ -218,7 +223,7 @@ fn read_requests( } } Ok(HostReceive::ProtocolViolation) => { - failure.report(IoError::new( + failure_coordinator.report(IoError::new( ErrorKind::InvalidData, "runner sent a request for the wrong protocol phase", )); @@ -226,7 +231,7 @@ fn read_requests( } Ok(HostReceive::PeerClosed) => break, Err(error) => { - failure.report(error); + failure_coordinator.report(error); break; } } @@ -237,7 +242,7 @@ fn run_worker( association: &BrokerHostAssociation<'_, Memory>, request_receiver: &Mutex>, response_sink: &UnixStreamHostResponseSink, - failure: &HostAssociationFailure, + failure_coordinator: &HostAssociationFailureCoordinator, ) { loop { let request = request_receiver @@ -247,26 +252,28 @@ fn run_worker( let Ok(request) = request else { break; }; - if failure.failed() { + if failure_coordinator.failed() { continue; } match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { association.execute_request(request, |response| response_sink.send_response(response)) })) { Ok(Ok(())) => {} - Ok(Err(error)) => failure.report(IoError::other(error)), - Err(_) => failure.report(IoError::other("broker request worker panicked")), + Ok(Err(error)) => failure_coordinator.report(IoError::other(error)), + Err(_) => { + failure_coordinator.report(IoError::other("broker request worker panicked")); + } } } } -struct HostAssociationFailure { +struct HostAssociationFailureCoordinator { failed: AtomicBool, error: Mutex>, shutdown: UnixStreamHostControlShutdown, } -impl HostAssociationFailure { +impl HostAssociationFailureCoordinator { const fn new(shutdown: UnixStreamHostControlShutdown) -> Self { Self { failed: AtomicBool::new(false), @@ -336,36 +343,53 @@ fn accept_runner_stream( mod tests { use super::*; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; - use litebox_broker_protocol::channel::HostControlChannel; + use litebox_broker_protocol::channel::{HostSetupChannel, LocalControlChannel}; use litebox_broker_protocol::message::BrokerHandshakeResponse; + use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; + use std::os::fd::AsFd; #[test] fn first_failure_is_preserved_and_unblocks_request_reading() { let (peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut local_channel = UnixStreamLocalControlChannel::from_connected(peer_stream); let mut control_channel = UnixStreamHostControlChannel::from_accepted(host_stream); control_channel .send_handshake_response(&BrokerHandshakeResponse::Negotiated { broker_protocol_version: BROKER_PROTOCOL_VERSION, }) .unwrap(); - let (mut request_source, _response_sink, shutdown) = control_channel.into_active().unwrap(); - let failure = HostAssociationFailure::new(shutdown); + local_channel.recv_handshake_response().unwrap().unwrap(); + let local_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let host_memory = MemfdSharedMemory::from_received_fd( + local_memory.as_fd().try_clone_to_owned().unwrap(), + CONTROL_RING_MEMORY_SIZE, + ) + .unwrap(); + let local_ring = ControlRing::new(local_memory).unwrap(); + let host_ring = ControlRing::new(host_memory).unwrap(); + let local_activation = std::thread::spawn(move || { + let cancellation = local_channel.activate(local_ring, || {}).unwrap(); + (local_channel, cancellation) + }); + let (mut request_source, _response_sink, shutdown) = + control_channel.into_active(host_ring).unwrap(); + let (_local_channel, _cancellation) = local_activation.join().unwrap(); + let failure_coordinator = HostAssociationFailureCoordinator::new(shutdown); let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); let reader = std::thread::spawn(move || { result_sender.send(request_source.recv_request()).unwrap(); }); - failure.report(IoError::new(ErrorKind::TimedOut, "first failure")); - failure.report(IoError::other("second failure")); + failure_coordinator.report(IoError::new(ErrorKind::TimedOut, "first failure")); + failure_coordinator.report(IoError::other("second failure")); let receive_result = result_receiver.recv_timeout(Duration::from_secs(1)); - drop(peer_stream); reader.join().unwrap(); assert!(matches!( receive_result.unwrap(), Ok(HostReceive::PeerClosed) | Err(_) )); - let error = failure.take_error().unwrap(); + let error = failure_coordinator.take_error().unwrap(); assert_eq!(error.kind(), ErrorKind::TimedOut); assert_eq!(error.to_string(), "first failure"); } diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index 8771845f6e..424ce4d15a 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -5,12 +5,14 @@ use std::os::unix::net::UnixStream; use std::sync::Arc; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; -use litebox_broker_host::{ConnectionTermination, serve_connection}; +use litebox_broker_host::{ConnectionTermination, setup_connection}; use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::channel::HostReceive; use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::shared_memory::{ SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, }; +use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, UnixStreamLocalControlChannel, @@ -27,24 +29,46 @@ fn host_serves_control_requests_over_paired_userland_channels() { let host_shared_memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE).unwrap(); let host_shared_buffers = SharedBufferPool::new(host_shared_memory, SHARED_BUFFER_LAYOUT).unwrap(); + let host_control_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let host_control_ring = ControlRing::new(host_control_memory).unwrap(); let host_thread = std::thread::spawn(move || { let mut control = UnixStreamHostControlChannel::from_accepted(host_control); - let mut notification = UnixStreamHostNotificationChannel::from_accepted(host_notification); - serve_connection( - &broker, - &mut control, - &mut notification, - &host_shared_buffers, - |channel| channel.send_memfd(host_shared_buffers.memory(), None), - ) + let _notification = UnixStreamHostNotificationChannel::from_accepted(host_notification); + let association = + setup_connection(&broker, &mut control, &host_shared_buffers, |channel| { + channel.send_memfd(host_shared_buffers.memory(), None)?; + channel.send_memfd(host_control_ring.memory(), None) + }) + .unwrap() + .unwrap(); + let (mut request_source, response_sink, _shutdown) = + control.into_active(host_control_ring).unwrap(); + loop { + match request_source.recv_request().unwrap() { + HostReceive::Message(request) => association + .execute_request(request, |response| response_sink.send_response(response)) + .unwrap(), + HostReceive::PeerClosed => return ConnectionTermination::PeerClosed, + HostReceive::ProtocolViolation => { + return ConnectionTermination::ProtocolViolation; + } + } + } }); let local = BrokerLocal::negotiate( UnixStreamLocalControlChannel::from_connected(local_control), |channel| { let shared_memory = channel.receive_memfd(SHARED_BUFFER_POOL_SIZE, None)?; - let _cancellation = channel.activate(|| {})?; + let control_memory = channel.receive_memfd(CONTROL_RING_MEMORY_SIZE, None)?; + let control_ring = ControlRing::new(control_memory).map_err(|error| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid test control ring: {error:?}"), + ) + })?; + let _cancellation = channel.activate(control_ring, || {})?; Ok(Arc::new(shared_memory)) }, ) @@ -56,7 +80,7 @@ fn host_serves_control_requests_over_paired_userland_channels() { drop(local); assert_eq!( - host_thread.join().unwrap().unwrap(), + host_thread.join().unwrap(), ConnectionTermination::PeerClosed ); } diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 17e9c946ae..4434d73aa6 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -13,6 +13,7 @@ use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::shared_memory::{ SHARED_BUFFER_POOL_SIZE, SharedBufferDescriptor, SharedBufferSlotIndex, }; +use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, }; @@ -90,7 +91,17 @@ fn run_fake_runner(args: &[OsString]) { SHARED_BUFFER_POOL_SIZE, Some(Instant::now() + Duration::from_secs(5)), )?; - let _cancellation = channel.activate(|| {})?; + let control_memory = channel.receive_memfd( + CONTROL_RING_MEMORY_SIZE, + Some(Instant::now() + Duration::from_secs(5)), + )?; + let control_ring = ControlRing::new(control_memory).map_err(|error| { + std::io::Error::new( + ErrorKind::InvalidData, + format!("invalid test control ring: {error:?}"), + ) + })?; + let _cancellation = channel.activate(control_ring, || {})?; Ok(Arc::new(shared_memory)) }) .unwrap(), diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 11ae005ad2..786f0e7d95 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -14,6 +14,7 @@ use anyhow::{Context as _, Result}; use litebox_broker_local::{BrokerLocal, BrokerNotifications}; use litebox_broker_protocol::message::BrokerNotification; use litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE; +use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; use litebox_broker_transport::unix_socket::{ UnixStreamLocalControlCancellation, UnixStreamLocalControlChannel, UnixStreamLocalNotificationCancellation, UnixStreamLocalNotificationChannel, @@ -66,8 +67,16 @@ pub(crate) fn connect( move |channel| { let shared_memory = channel.receive_memfd(SHARED_BUFFER_POOL_SIZE, Some(setup_deadline))?; + let control_memory = + channel.receive_memfd(CONTROL_RING_MEMORY_SIZE, Some(setup_deadline))?; + let control_ring = ControlRing::new(control_memory).map_err(|error| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid broker control ring: {error:?}"), + ) + })?; let weak_association_coordinator = Arc::downgrade(&association_coordinator); - let control_cancellation_handle = channel.activate(move || { + let control_cancellation_handle = channel.activate(control_ring, move || { if let Some(association_coordinator) = weak_association_coordinator.upgrade() { association_coordinator.report_failure(); } @@ -216,11 +225,16 @@ fn connect_with_retry( #[cfg(test)] mod tests { use super::*; - use litebox_broker_protocol::channel::{HostControlChannel, HostReceive, LocalControlChannel}; + use litebox_broker_protocol::channel::{HostReceive, HostSetupChannel, LocalControlChannel}; use litebox_broker_protocol::message::{BrokerOperation, BrokerRequest}; use litebox_broker_protocol::{ObjectHandle, RequestId}; - use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; + use litebox_broker_transport::shared_memory::MemfdSharedMemory; + use litebox_broker_transport::unix_socket::{ + UnixStreamHostControlChannel, UnixStreamHostControlShutdown, UnixStreamHostRequestSource, + UnixStreamHostResponseSink, + }; use std::io::{ErrorKind, Read}; + use std::os::fd::AsFd; use std::os::unix::net::UnixStream; use std::sync::mpsc; @@ -253,16 +267,34 @@ mod tests { fn activate_control_channel( channel: &mut UnixStreamLocalControlChannel, + host_channel: UnixStreamHostControlChannel, association_coordinator: &Arc, - ) -> UnixStreamLocalControlCancellation { + ) -> ( + UnixStreamLocalControlCancellation, + UnixStreamHostRequestSource, + UnixStreamHostResponseSink, + UnixStreamHostControlShutdown, + ) { + let local_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let host_memory = MemfdSharedMemory::from_received_fd( + local_memory.as_fd().try_clone_to_owned().unwrap(), + CONTROL_RING_MEMORY_SIZE, + ) + .unwrap(); + let local_ring = ControlRing::new(local_memory).unwrap(); + let host_ring = ControlRing::new(host_memory).unwrap(); + let host_activation = + std::thread::spawn(move || host_channel.into_active(host_ring).unwrap()); let weak_association_coordinator = Arc::downgrade(association_coordinator); - channel - .activate(move || { + let cancellation = channel + .activate(local_ring, move || { if let Some(association_coordinator) = weak_association_coordinator.upgrade() { association_coordinator.report_failure(); } }) - .unwrap() + .unwrap(); + let (request_source, response_sink, shutdown) = host_activation.join().unwrap(); + (cancellation, request_source, response_sink, shutdown) } #[test] @@ -279,15 +311,17 @@ mod tests { let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( notification_channel.cancellation_handle().unwrap(), )); - let control_cancellation_handle = - activate_control_channel(&mut active_channel, &association_coordinator); + let (control_cancellation_handle, host_request_source, host_response_sink, host_shutdown) = + activate_control_channel(&mut active_channel, host_control, &association_coordinator); association_coordinator .install_control_cancellation_handle(control_cancellation_handle) .unwrap(); let (failure_sender, failure_receiver) = mpsc::sync_channel(1); association_coordinator.install_dispatch(move || failure_sender.send(()).unwrap()); - drop(host_control); + host_shutdown.shutdown().unwrap(); + drop(host_request_source); + drop(host_response_sink); failure_receiver .recv_timeout(Duration::from_secs(1)) @@ -304,7 +338,7 @@ mod tests { host_control .set_read_timeout(Some(Duration::from_secs(1))) .unwrap(); - let (mut active_channel, mut host_control) = + let (mut active_channel, host_control) = negotiate_control_pair(local_control, host_control); let (local_notification, mut host_notification) = UnixStream::pair().unwrap(); host_notification @@ -315,8 +349,12 @@ mod tests { let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( notification_channel.cancellation_handle().unwrap(), )); - let control_cancellation_handle = - activate_control_channel(&mut active_channel, &association_coordinator); + let ( + control_cancellation_handle, + mut host_request_source, + _host_response_sink, + _host_shutdown, + ) = activate_control_channel(&mut active_channel, host_control, &association_coordinator); association_coordinator.report_failure(); @@ -331,7 +369,7 @@ mod tests { association_coordinator.install_dispatch(move || failure_sender.send(()).unwrap()); failure_receiver.try_recv().unwrap(); assert_eq!( - host_control.recv_request().unwrap(), + host_request_source.recv_request().unwrap(), HostReceive::PeerClosed ); let mut byte = [0]; @@ -343,7 +381,7 @@ mod tests { #[test] fn notification_failure_cancels_control() { let (local_control, host_control) = UnixStream::pair().unwrap(); - let (mut active_channel, mut host_control) = + let (mut active_channel, host_control) = negotiate_control_pair(local_control, host_control); let (local_notification, host_notification) = UnixStream::pair().unwrap(); let notification_channel = @@ -351,8 +389,12 @@ mod tests { let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( notification_channel.cancellation_handle().unwrap(), )); - let control_cancellation_handle = - activate_control_channel(&mut active_channel, &association_coordinator); + let ( + control_cancellation_handle, + mut host_request_source, + _host_response_sink, + _host_shutdown, + ) = activate_control_channel(&mut active_channel, host_control, &association_coordinator); association_coordinator .install_control_cancellation_handle(control_cancellation_handle) .unwrap(); @@ -373,7 +415,7 @@ mod tests { }) }); assert!(matches!( - host_control.recv_request().unwrap(), + host_request_source.recv_request().unwrap(), HostReceive::Message(_) )); @@ -384,7 +426,7 @@ mod tests { .unwrap(); assert!(pending_call.join().unwrap().is_err()); assert_eq!( - host_control.recv_request().unwrap(), + host_request_source.recv_request().unwrap(), HostReceive::PeerClosed ); } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 0df90dc67f..c605800040 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -388,6 +388,14 @@ fn spawn_test_broker( litebox_broker_protocol::shared_memory::SHARED_BUFFER_LAYOUT, ) .expect("failed to attach broker test shared-buffer layout"); + let control_memory = + litebox_broker_transport::shared_memory::MemfdSharedMemory::create( + litebox_broker_transport::control_ring::CONTROL_RING_MEMORY_SIZE, + ) + .expect("failed to create broker test control ring"); + let control_ring = + litebox_broker_transport::control_ring::ControlRing::new(control_memory) + .expect("failed to attach broker test control ring"); let (notification_stream, _) = notification_listener .accept() .expect("failed to accept broker local notification connection"); @@ -408,29 +416,60 @@ fn spawn_test_broker( notification_stream .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker notification test write timeout"); - let mut channel = CountingHostControlChannel { - inner: litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_host_guaranteed( + let mut channel = + litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_host_guaranteed( control_stream, std::time::Instant::now() + BROKER_HELPER_TIMEOUT, - ), - close_object_count: 0, - }; - let mut notification_channel = + ); + let _notification_channel = litebox_broker_transport::unix_socket::UnixStreamHostNotificationChannel::from_accepted(notification_stream); - let termination = litebox_broker_host::serve_connection( + let association = litebox_broker_host::setup_connection( &broker, &mut channel, - &mut notification_channel, &shared_buffers, - |channel| channel.inner.send_memfd(shared_buffers.memory(), None), + |channel| { + channel.send_memfd(shared_buffers.memory(), None)?; + channel.send_memfd(control_ring.memory(), None) + }, ) - .expect("broker host failed"); + .expect("broker host setup failed") + .expect("broker setup terminated before activation"); + let (mut request_source, response_sink, _shutdown) = channel + .into_active(control_ring) + .expect("failed to activate broker test control ring"); + let mut close_object_count = 0; + let termination = loop { + match request_source + .recv_request() + .expect("failed to receive broker test request") + { + litebox_broker_protocol::channel::HostReceive::Message(request) => { + if matches!( + &request.operation, + litebox_broker_protocol::message::BrokerOperation::CloseObject(_) + ) { + close_object_count += 1; + } + association + .execute_request(request, |response| { + response_sink.send_response(response) + }) + .expect("failed to execute broker test request"); + } + litebox_broker_protocol::channel::HostReceive::PeerClosed => { + break litebox_broker_host::ConnectionTermination::PeerClosed; + } + litebox_broker_protocol::channel::HostReceive::ProtocolViolation => { + break litebox_broker_host::ConnectionTermination::ProtocolViolation; + } + } + }; assert_eq!( termination, litebox_broker_host::ConnectionTermination::PeerClosed ); close_object_count_tx - .send(channel.close_object_count) + .send(close_object_count) .expect("failed to report broker close-object count"); } })); @@ -454,73 +493,6 @@ fn spawn_test_broker( } } -#[cfg(all(target_arch = "x86_64", target_os = "linux"))] -struct CountingHostControlChannel { - inner: Channel, - close_object_count: usize, -} - -#[cfg(all(target_arch = "x86_64", target_os = "linux"))] -impl - litebox_broker_protocol::channel::HostControlChannel for CountingHostControlChannel -{ - type Error = Channel::Error; - - fn peer_credential( - &self, - ) -> Result { - self.inner.peer_credential() - } - - fn recv_handshake_request( - &mut self, - ) -> Result< - litebox_broker_protocol::channel::HostReceive< - litebox_broker_protocol::message::BrokerHandshakeRequest, - >, - Self::Error, - > { - self.inner.recv_handshake_request() - } - - fn send_handshake_response( - &mut self, - response: &litebox_broker_protocol::message::BrokerHandshakeResponse, - ) -> Result<(), Self::Error> { - self.inner.send_handshake_response(response) - } - - fn recv_request( - &mut self, - ) -> Result< - litebox_broker_protocol::channel::HostReceive< - litebox_broker_protocol::message::BrokerRequest, - >, - Self::Error, - > { - let request = self.inner.recv_request()?; - if matches!( - &request, - litebox_broker_protocol::channel::HostReceive::Message( - litebox_broker_protocol::message::BrokerRequest { - operation: litebox_broker_protocol::message::BrokerOperation::CloseObject(_), - .. - } - ) - ) { - self.close_object_count += 1; - } - Ok(request) - } - - fn send_response( - &mut self, - response: &litebox_broker_protocol::message::BrokerResponse, - ) -> Result<(), Self::Error> { - self.inner.send_response(response) - } -} - #[cfg(all(target_arch = "x86_64", target_os = "linux"))] #[test] fn test_runner_broker_integration_with_rewriter() { From d69a4af4c8faf744c11907296a18987ad8e76347 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Thu, 23 Jul 2026 23:53:51 -0700 Subject: [PATCH 128/319] Implement NtContinue context restoration (#1082) This PR adds support for `NtContinue` syscall, but restoring floating-point, XSTATE, and debug-register state remain as TODO. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_windows/src/lib.rs | 86 ++++++++++++++++++++++-- litebox_shim_windows/src/nt_types.rs | 27 +++++--- litebox_shim_windows/src/syscalls/mod.rs | 9 +++ 3 files changed, 109 insertions(+), 13 deletions(-) diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 22c0dbebdb..dbcbfdab40 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -28,6 +28,7 @@ use litebox::platform::{ }; use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; use litebox::sync::RawSyncPrimitivesProvider; +use litebox::utils::TruncateExt as _; use litebox_common_windows::NtSysno; use litebox_common_windows::loader::{MappingInfo, PAGE_SIZE}; @@ -1886,6 +1887,13 @@ impl Task { self.sys_nt_unmap_view_of_section_ex(process_handle, base_address, flags); (status, ContinueOperation::Resume) } + SyscallRequest::NtContinue { + context, + test_alert, + } => match Self::sys_nt_continue(ctx, context, test_alert) { + Ok(()) => return ContinueOperation::Resume, + Err(status) => (status, ContinueOperation::Resume), + }, SyscallRequest::NtTerminateProcess { process_handle, exit_status, @@ -1901,11 +1909,7 @@ impl Task { } } SyscallRequest::NtTestAlert => { - // TODO(apc-model): Deliver queued user-mode APCs once thread alert and APC state - // are modeled. - litebox_util_log::debug!( - "NtTestAlert is a no-op; user-mode APC delivery is not yet modeled" - ); + Self::test_alert(); (NtStatus::SUCCESS, ContinueOperation::Resume) } SyscallRequest::NtManageHotPatch => { @@ -1917,6 +1921,78 @@ impl Task { op } + fn sys_nt_continue( + ctx: &mut litebox_common_linux::PtRegs, + context: ConstPtr, + test_alert: bool, + ) -> Result<(), NtStatus> { + if context.as_usize() == 0 { + return Err(NtStatus::ACCESS_VIOLATION); + } + let context = context + .read_at_offset(0) + .ok_or(NtStatus::ACCESS_VIOLATION)?; + + if test_alert { + Self::test_alert(); + } + + let context_flags = nt_types::ContextFlags::from_bits_retain(context.context_flags); + + if context_flags.contains(nt_types::ContextFlags::CONTROL) { + ctx.rip = context.rip.trunc(); + ctx.rsp = context.rsp.trunc(); + ctx.eflags = context.e_flags as usize; + ctx.cs = context.seg_cs as usize; + ctx.ss = context.seg_ss as usize; + } + + if context_flags.contains(nt_types::ContextFlags::INTEGER) { + ctx.rax = context.rax.trunc(); + ctx.rbx = context.rbx.trunc(); + ctx.rcx = context.rcx.trunc(); + ctx.rdx = context.rdx.trunc(); + ctx.rsi = context.rsi.trunc(); + ctx.rdi = context.rdi.trunc(); + ctx.rbp = context.rbp.trunc(); + ctx.r8 = context.r8.trunc(); + ctx.r9 = context.r9.trunc(); + ctx.r10 = context.r10.trunc(); + ctx.r11 = context.r11.trunc(); + ctx.r12 = context.r12.trunc(); + ctx.r13 = context.r13.trunc(); + ctx.r14 = context.r14.trunc(); + ctx.r15 = context.r15.trunc(); + } + + // TODO(context-model): Restore floating-point, extended, and debug-register state. + if context_flags.contains(nt_types::ContextFlags::FLOATING_POINT) { + litebox_util_log::warn!( + "NtContinue requested floating-point state, which is not yet restored" + ); + } + if context_flags.contains(nt_types::ContextFlags::XSTATE) { + litebox_util_log::warn!( + "NtContinue requested extended state, which is not yet restored" + ); + } + if context_flags.contains(nt_types::ContextFlags::DEBUG_REGISTERS) { + litebox_util_log::warn!( + "NtContinue requested debug-register state, which is not yet restored" + ); + } + + Ok(()) + } + + fn test_alert() { + // TODO(apc-model): Deliver queued user-mode APCs once thread alert and APC state + // are modeled. + litebox_util_log::debug!( + "NtTestAlert is a no-op; user-mode APC delivery is not yet modeled" + ); + } + pub(crate) fn sys_nt_close(&self, handle: syscalls::Handle) -> NtStatus { self.close_handle(handle, CloseRawHandleVisitor { task: self }) } diff --git a/litebox_shim_windows/src/nt_types.rs b/litebox_shim_windows/src/nt_types.rs index a9edac5c4b..1a0211eb83 100644 --- a/litebox_shim_windows/src/nt_types.rs +++ b/litebox_shim_windows/src/nt_types.rs @@ -9,10 +9,20 @@ use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use crate::{ConstPtr, syscalls::Handle}; -pub const X64_CONTEXT_CONTROL: u32 = 0x0010_0001; -pub const X64_CONTEXT_INTEGER: u32 = 0x0010_0002; -pub const X64_CONTEXT_FLOATING_POINT: u32 = 0x0010_0008; -pub const X64_CONTEXT_DEBUG_REGISTERS: u32 = 0x0010_0010; +bitflags::bitflags! { + /// Flags carried in `CONTEXT.ContextFlags`, selecting which register groups + /// a `CONTEXT` structure describes. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ContextFlags: u32 { + const CONTROL = 0x0010_0001; + const INTEGER = 0x0010_0002; + const FLOATING_POINT = 0x0010_0008; + const DEBUG_REGISTERS = 0x0010_0010; + const XSTATE = 0x0010_0040; + + const _ = !0; + } +} const INITIAL_CONTEXT_MXCSR: u32 = 0x1f80; const USER_MODE_CODE_SELECTOR: u16 = 0x33; @@ -124,10 +134,11 @@ impl X64Context { peb: usize, ) -> X64Context { X64Context { - context_flags: X64_CONTEXT_CONTROL - | X64_CONTEXT_INTEGER - | X64_CONTEXT_FLOATING_POINT - | X64_CONTEXT_DEBUG_REGISTERS, + context_flags: ContextFlags::CONTROL + .union(ContextFlags::INTEGER) + .union(ContextFlags::FLOATING_POINT) + .union(ContextFlags::DEBUG_REGISTERS) + .bits(), mx_csr: INITIAL_CONTEXT_MXCSR, seg_cs: USER_MODE_CODE_SELECTOR, seg_ss: USER_MODE_STACK_SELECTOR, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index cc59ebdc96..bf850e9f32 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -600,6 +600,11 @@ pub(crate) enum SyscallRequest { base_address: usize, flags: u32, }, + /// Restores the selected portions of a thread context and resumes execution. + NtContinue { + context: Platform::RawConstPointer, + test_alert: bool, + }, NtTerminateProcess { process_handle: ProcessHandle, exit_status: i32, @@ -1117,6 +1122,10 @@ impl SyscallRequest { base_address, flags, })), + NtSysno::NtContinue => Some(sys_req!(NtContinue { + context:*, + test_alert: { |value: u8| value != 0 }, + })), NtSysno::NtTerminateProcess => Some(sys_req!(NtTerminateProcess { process_handle: { ProcessHandle::from_raw }, exit_status, From 3fd0b0d963fbf17e7d812ba7ab61eb47fa41ae25 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 24 Jul 2026 10:00:42 -0700 Subject: [PATCH 129/319] Add a shared-memory broker notification ring (#1081) Extend the exact shared control mapping with an independent 64-slot broker-to-local notification ring. Make ring geometry direction-aware, expose role-bound endpoints, and pin the maximum notification wire size; production notifications remain on the Unix socket until the next migration PR. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac86dd2d-0280-4d7b-87a8-5654ad1503a6 --- litebox_broker_protocol/src/wire.rs | 13 +- litebox_broker_transport/src/control_ring.rs | 276 +++++++++++++++--- litebox_broker_transport/src/shared_memory.rs | 10 +- litebox_broker_transport/src/unix_socket.rs | 24 +- 4 files changed, 274 insertions(+), 49 deletions(-) diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 75c2e7385d..494faff7f1 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -51,6 +51,9 @@ const NOTIFICATION_TAG_READINESS: u8 = 0; /// Maximum byte length of any encoded active request or response. pub const MAX_ENCODED_ACTIVE_MESSAGE_SIZE: usize = 26; +/// Maximum byte length of any encoded broker notification. +pub const MAX_ENCODED_NOTIFICATION_SIZE: usize = 13; + /// Error produced while encoding or decoding a broker wire message. #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] #[non_exhaustive] @@ -497,13 +500,15 @@ mod tests { handle, readiness: ReadinessFlags::READ | ReadinessFlags::HANGUP, })]; + let mut maximum_encoded_size = 0; for notification in notifications { - assert_eq!( - decode_notification(&encode_notification(notification.clone())).unwrap(), - notification - ); + let encoded = encode_notification(notification.clone()); + maximum_encoded_size = maximum_encoded_size.max(encoded.len()); + assert!(encoded.len() <= MAX_ENCODED_NOTIFICATION_SIZE); + assert_eq!(decode_notification(&encoded).unwrap(), notification); } + assert_eq!(maximum_encoded_size, MAX_ENCODED_NOTIFICATION_SIZE); } #[test] diff --git a/litebox_broker_transport/src/control_ring.rs b/litebox_broker_transport/src/control_ring.rs index 0855b60c04..baa0d63564 100644 --- a/litebox_broker_transport/src/control_ring.rs +++ b/litebox_broker_transport/src/control_ring.rs @@ -19,27 +19,38 @@ pub const CONTROL_RING_SLOT_SIZE: usize = 128; /// Size of the fixed metadata at the start of a control-ring slot. pub const CONTROL_RING_SLOT_HEADER_SIZE: usize = 16; -/// Maximum encoded request or response size in one control-ring slot. +/// Maximum encoded control message size in one ring slot. pub const CONTROL_RING_PAYLOAD_CAPACITY: usize = CONTROL_RING_SLOT_SIZE - CONTROL_RING_SLOT_HEADER_SIZE; const _: () = assert!( CONTROL_RING_PAYLOAD_CAPACITY >= litebox_broker_protocol::wire::MAX_ENCODED_ACTIVE_MESSAGE_SIZE ); +const _: () = assert!( + CONTROL_RING_PAYLOAD_CAPACITY >= litebox_broker_protocol::wire::MAX_ENCODED_NOTIFICATION_SIZE +); const _: () = assert!(CONTROL_RING_SLOT_SIZE.is_multiple_of(size_of::())); -/// Number of slots in each direction of the shared control ring. +/// Number of slots in each request or response direction. pub const CONTROL_RING_SLOT_COUNT: u64 = 64; -/// Exact shared-memory size required for both control-ring directions. +/// Number of slots in the broker-to-local notification direction. +pub const CONTROL_RING_NOTIFICATION_SLOT_COUNT: u64 = 64; + +/// Exact shared-memory size required for all association control directions. pub const CONTROL_RING_MEMORY_SIZE: usize = - CONTROL_RING_DATA_SIZE + CONTROL_RING_SYNC_DIRECTION_SIZE * 2; + CONTROL_RING_DATA_SIZE + CONTROL_RING_SYNC_DIRECTION_SIZE * 3; // The fixed count is representable by `usize` on every supported target. #[allow(clippy::cast_possible_truncation)] const CONTROL_RING_DIRECTION_SIZE: usize = CONTROL_RING_SLOT_SIZE * CONTROL_RING_SLOT_COUNT as usize; -const CONTROL_RING_DATA_SIZE: usize = CONTROL_RING_DIRECTION_SIZE * 2; +// The fixed count is representable by `usize` on every supported target. +#[allow(clippy::cast_possible_truncation)] +const CONTROL_RING_NOTIFICATION_DIRECTION_SIZE: usize = + CONTROL_RING_SLOT_SIZE * CONTROL_RING_NOTIFICATION_SLOT_COUNT as usize; +const CONTROL_RING_DATA_SIZE: usize = + CONTROL_RING_DIRECTION_SIZE * 2 + CONTROL_RING_NOTIFICATION_DIRECTION_SIZE; const CONTROL_RING_SYNC_DIRECTION_SIZE: usize = 16; const PRODUCER_EPOCH_OFFSET: usize = 0; const CONSUMER_EPOCH_OFFSET: usize = 4; @@ -52,20 +63,31 @@ pub enum ControlRingDirection { Requests, /// Broker-to-local response ring. Responses, + /// Broker-to-local asynchronous notification ring. + Notifications, } impl ControlRingDirection { fn slot_range(self, slot: u64) -> Range { - debug_assert!(slot < CONTROL_RING_SLOT_COUNT); + debug_assert!(slot < self.slot_count()); let slot = usize::try_from(slot).expect("control-ring slot index is bounded"); - let direction_offset = match self { + let data_offset = match self { Self::Requests => 0, Self::Responses => CONTROL_RING_DIRECTION_SIZE, + Self::Notifications => CONTROL_RING_DIRECTION_SIZE * 2, }; - let start = direction_offset + slot * CONTROL_RING_SLOT_SIZE; + let start = data_offset + slot * CONTROL_RING_SLOT_SIZE; start..start + CONTROL_RING_SLOT_SIZE } + /// Returns the number of slots in this direction. + pub const fn slot_count(self) -> u64 { + match self { + Self::Requests | Self::Responses => CONTROL_RING_SLOT_COUNT, + Self::Notifications => CONTROL_RING_NOTIFICATION_SLOT_COUNT, + } + } + /// Returns the atomic `u32` epoch incremented when the producer publishes /// work for this direction. pub const fn producer_epoch_offset(self) -> usize { @@ -88,6 +110,7 @@ impl ControlRingDirection { + match self { Self::Requests => 0, Self::Responses => CONTROL_RING_SYNC_DIRECTION_SIZE, + Self::Notifications => CONTROL_RING_SYNC_DIRECTION_SIZE * 2, } } } @@ -186,11 +209,31 @@ pub enum ControlRingReadError { Decode(DecodeError), } -/// Exact-size shared memory containing request and response control rings. +/// Exact-size shared memory containing request, response, and notification rings. pub struct ControlRing { memory: Memory, } +/// Role-bound association ring endpoints owned by the local peer. +pub struct LocalControlRingEndpoints { + /// Local-to-broker request producer. + pub request_producer: ControlRingProducer, + /// Broker-to-local response consumer. + pub response_consumer: ControlRingConsumer, + /// Broker-to-local notification consumer. + pub notification_consumer: ControlRingConsumer, +} + +/// Role-bound association ring endpoints owned by the broker peer. +pub struct BrokerControlRingEndpoints { + /// Local-to-broker request consumer. + pub request_consumer: ControlRingConsumer, + /// Broker-to-local response producer. + pub response_producer: ControlRingProducer, + /// Broker-to-local notification producer. + pub notification_producer: ControlRingProducer, +} + impl ControlRing { /// Attaches to an exact-size shared control-ring mapping. pub fn new(memory: Memory) -> Result { @@ -209,24 +252,45 @@ impl ControlRing { &self.memory } - /// Consumes the mapping into the local request producer and response - /// consumer. - pub fn into_local(self) -> (ControlRingProducer, ControlRingConsumer) { - self.into_endpoints( - ControlRingDirection::Requests, - ControlRingDirection::Responses, - ) + /// Consumes the mapping into endpoints owned by the local peer. + pub fn into_local(self) -> LocalControlRingEndpoints { + let ring = Arc::new(self); + LocalControlRingEndpoints { + request_producer: ControlRingProducer::new( + Arc::clone(&ring), + ControlRingDirection::Requests, + ), + response_consumer: ControlRingConsumer::new( + Arc::clone(&ring), + ControlRingDirection::Responses, + ), + notification_consumer: ControlRingConsumer::new( + ring, + ControlRingDirection::Notifications, + ), + } } - /// Consumes the mapping into the broker response producer and request - /// consumer. - pub fn into_broker(self) -> (ControlRingProducer, ControlRingConsumer) { - self.into_endpoints( - ControlRingDirection::Responses, - ControlRingDirection::Requests, - ) + /// Consumes the mapping into endpoints owned by the broker peer. + pub fn into_broker(self) -> BrokerControlRingEndpoints { + let ring = Arc::new(self); + BrokerControlRingEndpoints { + request_consumer: ControlRingConsumer::new( + Arc::clone(&ring), + ControlRingDirection::Requests, + ), + response_producer: ControlRingProducer::new( + Arc::clone(&ring), + ControlRingDirection::Responses, + ), + notification_producer: ControlRingProducer::new( + ring, + ControlRingDirection::Notifications, + ), + } } + #[cfg(test)] fn into_endpoints( self, producer_direction: ControlRingDirection, @@ -247,7 +311,7 @@ impl ControlRing { payload: &[u8], sequence: u64, ) -> Result<(), ControlRingError> { - let slot = position % CONTROL_RING_SLOT_COUNT; + let slot = position % direction.slot_count(); let range = direction.slot_range(slot); self.memory .write(range.start + CONTROL_RING_SLOT_HEADER_SIZE, payload)?; @@ -267,7 +331,7 @@ impl ControlRing { direction: ControlRingDirection, position: u64, ) -> Result { - let slot = position % CONTROL_RING_SLOT_COUNT; + let slot = position % direction.slot_count(); let range = direction.slot_range(slot); Ok(u64::from_le(self.memory.load_u64_acquire(range.start)?)) } @@ -278,7 +342,7 @@ impl ControlRing { position: u64, body: &mut [u8], ) -> Result<(), ControlRingError> { - let slot = position % CONTROL_RING_SLOT_COUNT; + let slot = position % direction.slot_count(); let range = direction.slot_range(slot); self.memory.read(range.start + size_of::(), body)?; Ok(()) @@ -429,10 +493,11 @@ impl ControlRingProducer { if self.tail == u64::MAX { return Err(ControlRingError::CounterExhausted); } - if self.tail - self.acknowledged_head == CONTROL_RING_SLOT_COUNT { + let slot_count = self.direction.slot_count(); + if self.tail - self.acknowledged_head == slot_count { let wait_epoch = self.ring.load_consumer_epoch(self.direction)?; self.refresh_head()?; - if self.tail - self.acknowledged_head == CONTROL_RING_SLOT_COUNT { + if self.tail - self.acknowledged_head == slot_count { return Ok(ControlRingWriteStatus::Full { wait_epoch }); } } @@ -522,7 +587,7 @@ impl ControlRingConsumer { .ring .load_sequence(self.direction, self.head) .map_err(ControlRingReadError::Ring)?; - let stale_sequence = expected_sequence.saturating_sub(CONTROL_RING_SLOT_COUNT); + let stale_sequence = expected_sequence.saturating_sub(self.direction.slot_count()); if actual_sequence == stale_sequence { return Ok(ControlRingReadStatus::Empty { wait_epoch }); } @@ -595,6 +660,7 @@ mod tests { #[test] fn mapping_requires_the_exact_control_ring_size() { + assert_eq!(CONTROL_RING_MEMORY_SIZE, 24_624); assert!(matches!( ControlRing::new(TestMemory::new(CONTROL_RING_MEMORY_SIZE - 1)), Err(ControlRingError::MemoryLengthMismatch { @@ -612,21 +678,100 @@ mod tests { assert!(ControlRing::new(TestMemory::new(CONTROL_RING_MEMORY_SIZE)).is_ok()); } + #[test] + fn directions_occupy_disjoint_data_and_sync_ranges() { + let directions = [ + ControlRingDirection::Requests, + ControlRingDirection::Responses, + ControlRingDirection::Notifications, + ]; + let mut next_data_offset = 0; + for direction in directions { + let first = direction.slot_range(0); + let last = direction.slot_range(direction.slot_count() - 1); + assert_eq!(first.start, next_data_offset); + assert_eq!( + last.end - first.start, + usize::try_from(direction.slot_count()).unwrap() * CONTROL_RING_SLOT_SIZE + ); + next_data_offset = last.end; + } + assert_eq!(next_data_offset, CONTROL_RING_DATA_SIZE); + + for (index, direction) in directions.into_iter().enumerate() { + assert_eq!( + direction.producer_epoch_offset(), + CONTROL_RING_DATA_SIZE + index * CONTROL_RING_SYNC_DIRECTION_SIZE + ); + assert_eq!( + direction.consumer_epoch_offset(), + direction.producer_epoch_offset() + size_of::() + ); + assert_eq!( + direction.consumer_head_offset(), + direction.producer_epoch_offset() + size_of::() + ); + assert!( + direction + .consumer_head_offset() + .is_multiple_of(align_of::()) + ); + } + assert_eq!( + ControlRingDirection::Notifications.consumer_head_offset() + size_of::(), + CONTROL_RING_MEMORY_SIZE + ); + } + #[test] fn endpoint_roles_bind_opposite_directions_to_one_mapping() { - let (local_producer, local_consumer) = test_ring().into_local(); - assert_eq!(local_producer.direction(), ControlRingDirection::Requests); - assert_eq!(local_consumer.direction(), ControlRingDirection::Responses); - assert!(Arc::ptr_eq(&local_producer.ring, &local_consumer.ring)); + let local = test_ring().into_local(); + assert_eq!( + local.request_producer.direction(), + ControlRingDirection::Requests + ); + assert_eq!( + local.response_consumer.direction(), + ControlRingDirection::Responses + ); + assert_eq!( + local.notification_consumer.direction(), + ControlRingDirection::Notifications + ); + assert!(Arc::ptr_eq( + &local.request_producer.ring, + &local.response_consumer.ring + )); + assert!(Arc::ptr_eq( + &local.request_producer.ring, + &local.notification_consumer.ring + )); - let (broker_producer, broker_consumer) = test_ring().into_broker(); - assert_eq!(broker_producer.direction(), ControlRingDirection::Responses); - assert_eq!(broker_consumer.direction(), ControlRingDirection::Requests); - assert!(Arc::ptr_eq(&broker_producer.ring, &broker_consumer.ring)); + let broker = test_ring().into_broker(); + assert_eq!( + broker.request_consumer.direction(), + ControlRingDirection::Requests + ); + assert_eq!( + broker.response_producer.direction(), + ControlRingDirection::Responses + ); + assert_eq!( + broker.notification_producer.direction(), + ControlRingDirection::Notifications + ); + assert!(Arc::ptr_eq( + &broker.request_consumer.ring, + &broker.response_producer.ring + )); + assert!(Arc::ptr_eq( + &broker.request_consumer.ring, + &broker.notification_producer.ring + )); } #[test] - fn request_and_response_rings_publish_independently() { + fn request_response_and_notification_rings_publish_independently() { let ring = Arc::new(test_ring()); let mut request_producer = ControlRingProducer::new(Arc::clone(&ring), ControlRingDirection::Requests); @@ -634,7 +779,12 @@ mod tests { ControlRingConsumer::new(Arc::clone(&ring), ControlRingDirection::Requests); let mut response_producer = ControlRingProducer::new(Arc::clone(&ring), ControlRingDirection::Responses); - let mut response_consumer = ControlRingConsumer::new(ring, ControlRingDirection::Responses); + let mut response_consumer = + ControlRingConsumer::new(Arc::clone(&ring), ControlRingDirection::Responses); + let mut notification_producer = + ControlRingProducer::new(Arc::clone(&ring), ControlRingDirection::Notifications); + let mut notification_consumer = + ControlRingConsumer::new(ring, ControlRingDirection::Notifications); assert_eq!( request_producer.try_write(&[1, 2, 3]), @@ -644,6 +794,10 @@ mod tests { response_producer.try_write(&[4, 5]), Ok(ControlRingWriteStatus::Written) ); + assert_eq!( + notification_producer.try_write(&[6]), + Ok(ControlRingWriteStatus::Written) + ); assert_eq!( request_consumer.try_read(owned_bytes), @@ -653,6 +807,10 @@ mod tests { response_consumer.try_read(owned_bytes), Ok(ControlRingReadStatus::Message(vec![4, 5])) ); + assert_eq!( + notification_consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![6])) + ); assert_eq!( request_consumer.try_read(owned_bytes), Ok(ControlRingReadStatus::Empty { wait_epoch: 1 }) @@ -661,6 +819,10 @@ mod tests { response_consumer.try_read(owned_bytes), Ok(ControlRingReadStatus::Empty { wait_epoch: 1 }) ); + assert_eq!( + notification_consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { wait_epoch: 1 }) + ); } #[test] @@ -687,6 +849,42 @@ mod tests { ); } + #[test] + fn each_direction_enforces_its_own_capacity() { + for direction in [ + ControlRingDirection::Requests, + ControlRingDirection::Responses, + ControlRingDirection::Notifications, + ] { + let (mut producer, mut consumer) = test_ring().into_endpoints(direction, direction); + for value in 0..direction.slot_count() { + assert_eq!( + producer.try_write(&[u8::try_from(value % 256).unwrap()]), + Ok(ControlRingWriteStatus::Written) + ); + } + assert_eq!( + producer.try_write(&[0xff]), + Ok(ControlRingWriteStatus::Full { wait_epoch: 0 }) + ); + + for value in 0..direction.slot_count() { + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Message(vec![ + u8::try_from(value % 256).unwrap() + ])) + ); + } + assert_eq!( + consumer.try_read(owned_bytes), + Ok(ControlRingReadStatus::Empty { + wait_epoch: u32::try_from(direction.slot_count()).unwrap() + }) + ); + } + } + #[test] fn producer_publishes_sequence_after_payload_and_metadata() { let (mut producer, _) = test_endpoints(); diff --git a/litebox_broker_transport/src/shared_memory.rs b/litebox_broker_transport/src/shared_memory.rs index f59f688965..a9b913cf34 100644 --- a/litebox_broker_transport/src/shared_memory.rs +++ b/litebox_broker_transport/src/shared_memory.rs @@ -665,8 +665,14 @@ mod tests { CONTROL_RING_MEMORY_SIZE, ) .unwrap(); - let (mut producer, _) = ControlRing::new(local_memory).unwrap().into_local(); - let (_, mut consumer) = ControlRing::new(broker_memory).unwrap().into_broker(); + let mut producer = ControlRing::new(local_memory) + .unwrap() + .into_local() + .request_producer; + let mut consumer = ControlRing::new(broker_memory) + .unwrap() + .into_broker() + .request_consumer; let empty_checked = Arc::new(Barrier::new(2)); let broker_empty_checked = Arc::clone(&empty_checked); diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 96aa9580e9..ffe7c1c146 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -199,7 +199,11 @@ impl UnixStreamLocalControlChannel { } let shutdown_stream = monitor_stream.try_clone()?; - let (request_producer, response_consumer) = ring.into_local(); + let crate::control_ring::LocalControlRingEndpoints { + request_producer, + response_consumer, + notification_consumer: _, + } = ring.into_local(); let pending_calls = Arc::new(PendingCalls::new()); let association_failure: Arc = Arc::new(association_failure); let failure_coordinator = Arc::new(LocalActiveFailureCoordinator { @@ -383,7 +387,11 @@ impl UnixStreamHostControlChannel { write_frame_with_deadline(&mut self.stream, CONTROL_RING_READY, self.setup_deadline)?; let shutdown_stream = self.stream.try_clone()?; - let (response_producer, request_consumer) = ring.into_broker(); + let crate::control_ring::BrokerControlRingEndpoints { + request_consumer, + response_producer, + notification_producer: _, + } = ring.into_broker(); let active = Arc::new(HostActiveState { stream: shutdown_stream, status: Mutex::new(HostActiveStatus::Live), @@ -1196,7 +1204,11 @@ mod control_ring_tests { let mut channel = negotiated_local(local_stream); let cancellation = channel.activate(local_ring, association_failure).unwrap(); acknowledgement.join().unwrap(); - let (response_producer, request_consumer) = broker_ring.into_broker(); + let crate::control_ring::BrokerControlRingEndpoints { + request_consumer, + response_producer, + notification_producer: _, + } = broker_ring.into_broker(); ( channel, cancellation, @@ -1234,7 +1246,11 @@ mod control_ring_tests { }; let (source, sink, shutdown) = channel.into_active(host_ring).unwrap(); acknowledgement.join().unwrap(); - let (request_producer, response_consumer) = local_ring.into_local(); + let crate::control_ring::LocalControlRingEndpoints { + request_producer, + response_consumer, + notification_consumer: _, + } = local_ring.into_local(); ( source, sink, From daa95c3a8cd432053153b09e5e1fb9c37caef0d2 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 24 Jul 2026 13:52:08 -0700 Subject: [PATCH 130/319] Move broker notifications to shared memory (#1083) Route broker-to-local notifications through the association shared-memory notification ring and share the existing control socket's liveness and shutdown across all ring directions. Remove the dedicated notification socket listener, runner argument, connection retry, credential pairing, and shutdown path while keeping the portable notification channel contracts. Split the portable local channel into `LocalSetupChannel` for association setup and `LocalCallChannel` for active calls, mirroring the host side. `UnixStreamLocalSetupChannel::into_active` consumes a negotiated setup channel into the call, notification, and shutdown handles, retiring the failed placeholder state and the runtime phase errors it guarded. `BrokerLocal::negotiate` returns the endpoints activation produced alongside the association, so deployments no longer route the notification receiver out through a captured option. Setup-phase types are named `UnixStream*` and active ring-backed endpoints `UnixControlRing*`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ac86dd2d-0280-4d7b-87a8-5654ad1503a6 Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- litebox/src/broker/mod.rs | 36 +- litebox/src/event/counter.rs | 35 +- litebox/src/litebox.rs | 4 +- litebox/src/pipes.rs | 17 +- litebox_broker_local/src/event.rs | 4 +- litebox_broker_local/src/lib.rs | 102 +- litebox_broker_local/src/pipe.rs | 25 +- litebox_broker_protocol/src/channel.rs | 21 +- litebox_broker_transport/src/unix_socket.rs | 970 +++++++++--------- litebox_broker_userland/src/main.rs | 67 +- .../tests/notification_runtime.rs | 42 +- .../tests/userland_broker.rs | 84 +- litebox_runner_linux_userland/src/broker.rs | 279 +++-- litebox_runner_linux_userland/src/lib.rs | 30 +- litebox_runner_linux_userland/tests/run.rs | 51 +- 15 files changed, 879 insertions(+), 888 deletions(-) diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index a9d78b8f18..d4ca737b5e 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -9,7 +9,7 @@ use alloc::{ use hashbrown::HashMap; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::channel::LocalCallChannel; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode}; use litebox_broker_protocol::pipe::{CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE}; @@ -146,7 +146,7 @@ impl BrokerPollableRegistry { pub(crate) struct BrokerLocalControl< Platform: RawSyncPrimitivesProvider, - Channel: LocalControlChannel + Send + Sync, + Channel: LocalCallChannel + Send + Sync, > { local: Mutex>>>, pollable_registry: Arc>, @@ -156,7 +156,7 @@ pub(crate) struct BrokerLocalControl< impl BrokerLocalControl where Platform: RawSyncPrimitivesProvider + TimeProvider, - Channel: LocalControlChannel + Send + Sync, + Channel: LocalCallChannel + Send + Sync, { pub(crate) fn new( local: BrokerLocal, @@ -208,7 +208,7 @@ where impl BrokerControl for BrokerLocalControl where Platform: RawSyncPrimitivesProvider + TimeProvider, - Channel: LocalControlChannel + Send + Sync, + Channel: LocalCallChannel + Send + Sync, { fn create_event_with_count( &self, @@ -308,7 +308,7 @@ mod tests { use std::time::Duration; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; - use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, PipeRequest, PipeResponse, @@ -331,9 +331,10 @@ mod tests { observed_sender, release: StdArc::clone(&release), }; - let local = - BrokerLocal::negotiate(channel, |_| Ok(Arc::new(memory) as Arc)) - .unwrap(); + let (local, ()) = BrokerLocal::negotiate(channel, |channel| { + Ok((channel, Arc::new(memory) as Arc, ())) + }) + .unwrap(); let control = Arc::new(BrokerLocalControl::::new( local, Arc::new(BrokerPollableRegistry::new()), @@ -375,9 +376,10 @@ mod tests { observed_sender, release: StdArc::clone(&release), }; - let local = - BrokerLocal::negotiate(channel, |_| Ok(Arc::new(memory) as Arc)) - .unwrap(); + let (local, ()) = BrokerLocal::negotiate(channel, |channel| { + Ok((channel, Arc::new(memory) as Arc, ())) + }) + .unwrap(); let control = Arc::new(BrokerLocalControl::::new( local, Arc::new(BrokerPollableRegistry::new()), @@ -468,7 +470,7 @@ mod tests { release: StdArc<(StdMutex, StdCondvar)>, } - impl LocalControlChannel for ConcurrentPipeChannel { + impl LocalSetupChannel for ConcurrentPipeChannel { type Error = Infallible; fn send_handshake_request( @@ -486,6 +488,10 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION, })) } + } + + impl LocalCallChannel for ConcurrentPipeChannel { + type Error = Infallible; fn call( &self, @@ -516,7 +522,7 @@ mod tests { } } - impl LocalControlChannel for ConcurrentPipeReadChannel { + impl LocalSetupChannel for ConcurrentPipeReadChannel { type Error = Infallible; fn send_handshake_request( @@ -534,6 +540,10 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION, })) } + } + + impl LocalCallChannel for ConcurrentPipeReadChannel { + type Error = Infallible; fn call( &self, diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 5d178be6c6..17b11b13d5 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -178,7 +178,7 @@ mod tests { use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; - use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{CreateEventResponse, EventConsumption}; use litebox_broker_protocol::message::{ @@ -202,15 +202,15 @@ mod tests { let consume_attempts = Arc::new(AtomicUsize::new(0)); let read_ready = Arc::new(AtomicBool::new(false)); let request_count = Arc::new(AtomicUsize::new(0)); - let local = BrokerLocal::negotiate( - FakeLocalControlChannel { + let (local, ()) = BrokerLocal::negotiate( + FakeLocalChannel { next_handle: AtomicU64::new(handle.0), consume_attempts: consume_attempts.clone(), read_ready: read_ready.clone(), request_count, fail_requests: Arc::new(AtomicBool::new(false)), }, - |_| Ok(Arc::new(NoopSharedMemory)), + |channel| Ok((channel, Arc::new(NoopSharedMemory), ())), ) .unwrap(); let litebox = LiteBox::new_with_broker_local(platform, local); @@ -260,15 +260,15 @@ mod tests { let handle = ObjectHandle(7); let consume_attempts = Arc::new(AtomicUsize::new(0)); let request_count = Arc::new(AtomicUsize::new(0)); - let local = BrokerLocal::negotiate( - FakeLocalControlChannel { + let (local, ()) = BrokerLocal::negotiate( + FakeLocalChannel { next_handle: AtomicU64::new(handle.0), consume_attempts: Arc::clone(&consume_attempts), read_ready: Arc::new(AtomicBool::new(false)), request_count: Arc::clone(&request_count), fail_requests: Arc::new(AtomicBool::new(false)), }, - |_| Ok(Arc::new(NoopSharedMemory)), + |channel| Ok((channel, Arc::new(NoopSharedMemory), ())), ) .unwrap(); let litebox = Arc::new(LiteBox::new_with_broker_local(platform, local)); @@ -311,15 +311,15 @@ mod tests { let handle = ObjectHandle(7); let request_count = Arc::new(AtomicUsize::new(0)); let fail_requests = Arc::new(AtomicBool::new(false)); - let local = BrokerLocal::negotiate( - FakeLocalControlChannel { + let (local, ()) = BrokerLocal::negotiate( + FakeLocalChannel { next_handle: AtomicU64::new(handle.0), consume_attempts: Arc::new(AtomicUsize::new(0)), read_ready: Arc::new(AtomicBool::new(false)), request_count: Arc::clone(&request_count), fail_requests: Arc::clone(&fail_requests), }, - |_| Ok(Arc::new(NoopSharedMemory)), + |channel| Ok((channel, Arc::new(NoopSharedMemory), ())), ) .unwrap(); let litebox = LiteBox::new_with_broker_local(platform, local); @@ -347,15 +347,15 @@ mod tests { let platform = MockPlatform::new(); let handle = ObjectHandle(7); let request_count = Arc::new(AtomicUsize::new(0)); - let local = BrokerLocal::negotiate( - FakeLocalControlChannel { + let (local, ()) = BrokerLocal::negotiate( + FakeLocalChannel { next_handle: AtomicU64::new(handle.0), consume_attempts: Arc::new(AtomicUsize::new(0)), read_ready: Arc::new(AtomicBool::new(false)), request_count: Arc::clone(&request_count), fail_requests: Arc::new(AtomicBool::new(false)), }, - |_| Ok(Arc::new(NoopSharedMemory)), + |channel| Ok((channel, Arc::new(NoopSharedMemory), ())), ) .unwrap(); let litebox = LiteBox::new_with_broker_local(platform, local); @@ -400,7 +400,7 @@ mod tests { } } - struct FakeLocalControlChannel { + struct FakeLocalChannel { next_handle: AtomicU64, consume_attempts: Arc, read_ready: Arc, @@ -435,7 +435,7 @@ mod tests { } } - impl LocalControlChannel for FakeLocalControlChannel { + impl LocalSetupChannel for FakeLocalChannel { type Error = (); fn send_handshake_request( @@ -452,6 +452,11 @@ mod tests { broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, })) } + } + + impl LocalCallChannel for FakeLocalChannel { + type Error = (); + fn call( &self, request: BrokerRequest, diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index c34f4a120e..989364956e 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -6,7 +6,7 @@ use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::channel::LocalCallChannel; use litebox_broker_protocol::message::BrokerNotification; use crate::{ @@ -50,7 +50,7 @@ impl LiteBox { ) -> Self where Platform: TimeProvider, - Channel: LocalControlChannel + Send + Sync + 'static, + Channel: LocalCallChannel + Send + Sync + 'static, { let broker_pollables = Arc::new(broker::BrokerPollableRegistry::new()); let broker_control = Arc::new(broker::BrokerLocalControl::::new( diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index d436d694d7..d49359203c 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -928,7 +928,7 @@ mod tests { use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; - use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, @@ -950,13 +950,13 @@ mod tests { let platform = crate::platform::mock::MockPlatform::new(); let request_count = Arc::new(AtomicUsize::new(0)); let force_transport = Arc::new(AtomicBool::new(false)); - let local = BrokerLocal::negotiate( + let (local, ()) = BrokerLocal::negotiate( FailingPipeChannel { request_count: Arc::clone(&request_count), read_failure: ReadFailure::Transport, force_transport, }, - |_| Ok(Arc::new(NoopSharedMemory)), + |channel| Ok((channel, Arc::new(NoopSharedMemory), ())), ) .unwrap(); let litebox = crate::LiteBox::new_with_broker_local(platform, local); @@ -996,13 +996,13 @@ mod tests { let platform = crate::platform::mock::MockPlatform::new(); let request_count = Arc::new(AtomicUsize::new(0)); let force_transport = Arc::new(AtomicBool::new(false)); - let local = BrokerLocal::negotiate( + let (local, ()) = BrokerLocal::negotiate( FailingPipeChannel { request_count: Arc::clone(&request_count), read_failure: ReadFailure::WouldBlock, force_transport: Arc::clone(&force_transport), }, - |_| Ok(Arc::new(NoopSharedMemory)), + |channel| Ok((channel, Arc::new(NoopSharedMemory), ())), ) .unwrap(); let litebox = Arc::new(crate::LiteBox::new_with_broker_local(platform, local)); @@ -1148,7 +1148,7 @@ mod tests { WouldBlock, } - impl LocalControlChannel for FailingPipeChannel { + impl LocalSetupChannel for FailingPipeChannel { type Error = (); fn send_handshake_request( @@ -1165,6 +1165,11 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION, })) } + } + + impl LocalCallChannel for FailingPipeChannel { + type Error = (); + fn call( &self, request: BrokerRequest, diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 6ddc48af80..da42351e78 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -2,7 +2,7 @@ // Licensed under the MIT license. use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::channel::LocalCallChannel; use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, EventConsumeMode, @@ -14,7 +14,7 @@ use litebox_broker_protocol::readiness::ReadinessFlags; use crate::{BrokerLocal, BrokerLocalError, Result}; -impl BrokerLocal { +impl BrokerLocal { /// Creates a broker-owned event object with initial readiness credits. /// /// # Panics diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 360c9e0642..0165a42a54 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -4,8 +4,10 @@ //! Typed broker-local adapters for broker requests and notifications. //! //! The local control adapter owns request identifiers but does not own transport -//! sequencing. Userland, kernel, or ring-buffer deployments provide control -//! channels by implementing [`litebox_broker_protocol::channel::LocalControlChannel`]. +//! sequencing. Userland, kernel, or ring-buffer deployments provide channels by +//! implementing [`litebox_broker_protocol::channel::LocalSetupChannel`] for +//! association setup and +//! [`litebox_broker_protocol::channel::LocalCallChannel`] for active calls. //! Notification receive adapters are intentionally separate so active control //! requests remain strictly paired with their responses. @@ -23,7 +25,9 @@ mod pipe; use alloc::sync::Arc; use core::sync::atomic::{AtomicU64, Ordering}; -use litebox_broker_protocol::channel::{LocalControlChannel, LocalNotificationChannel}; +use litebox_broker_protocol::channel::{ + LocalCallChannel, LocalNotificationChannel, LocalSetupChannel, +}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, @@ -41,7 +45,7 @@ pub use error::{BrokerLocalError, Result}; /// /// The shared-buffer pool belongs to the broker association. Payload request /// descriptors identify operation-scoped slots managed by the caller. -pub struct BrokerLocal { +pub struct BrokerLocal { channel: Channel, shared_buffers: SharedBufferPool>, next_request_id: AtomicU64, @@ -52,29 +56,38 @@ pub struct BrokerNotifications { channel: Channel, } -impl BrokerLocal { - /// Negotiates the broker protocol, then establishes the association shared - /// memory before active requests begin. +impl BrokerLocal { + /// Negotiates the broker protocol on `setup`, then consumes it into the + /// active call channel and association shared memory before active requests + /// begin. + /// + /// `activate` owns every deployment-specific setup step that must complete + /// after negotiation, such as receiving shared memory and starting the + /// active transport. Any additional endpoints activation produces, such as + /// a notification receiver, are returned to the caller as `Activated`. /// /// # Panics /// /// Panics if the broker reports an unrecoverable error, returns a protocol /// response that does not match the negotiation request, or setup returns /// shared memory with an invalid size. - pub fn negotiate( - mut channel: Channel, + pub fn negotiate, Activated>( + mut setup: Setup, activate: impl FnOnce( - &mut Channel, - ) -> core::result::Result, Channel::Error>, - ) -> Result { + Setup, + ) -> core::result::Result< + (Channel, Arc, Activated), + Channel::Error, + >, + ) -> Result<(Self, Activated), Channel::Error> { let requested = BROKER_PROTOCOL_VERSION; let request = BrokerHandshakeRequest { protocol_version: requested, }; - channel + setup .send_handshake_request(&request) .map_err(BrokerLocalError::Channel)?; - match channel + match setup .recv_handshake_response() .map_err(BrokerLocalError::Channel)? .ok_or(BrokerLocalError::ChannelClosed)? @@ -86,14 +99,18 @@ impl BrokerLocal { requested, broker_protocol_version, "broker returned unexpected negotiation response: {response:?}" ); - let shared_memory = activate(&mut channel).map_err(BrokerLocalError::Channel)?; + let (channel, shared_memory, activated) = + activate(setup).map_err(BrokerLocalError::Channel)?; let shared_buffers = SharedBufferPool::new(shared_memory, SHARED_BUFFER_LAYOUT) .expect("broker association shared memory has an invalid size"); - Ok(Self { - channel, - shared_buffers, - next_request_id: AtomicU64::new(0), - }) + Ok(( + Self { + channel, + shared_buffers, + next_request_id: AtomicU64::new(0), + }, + activated, + )) } BrokerHandshakeResponse::VersionMismatch { .. } => { Err(BrokerLocalError::Broker(ErrorCode::UnsupportedVersion)) @@ -237,12 +254,12 @@ mod tests { None, ); let setup_calls = Cell::new(0); - let local = BrokerLocal::negotiate(channel, |channel| { + let (local, ()) = BrokerLocal::negotiate(channel, |channel| { assert!(channel.sent_handshake_request.is_some()); assert!(channel.handshake_response.is_none()); assert!(channel.sent_request.borrow().is_none()); setup_calls.set(setup_calls.get() + 1); - Ok(noop_shared_memory()) + Ok((channel, noop_shared_memory(), ())) }) .unwrap(); @@ -416,9 +433,9 @@ mod tests { let setup_called = Cell::new(false); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let _ = BrokerLocal::negotiate(channel, |_| { + let _ = BrokerLocal::negotiate(channel, |channel| { setup_called.set(true); - Ok(noop_shared_memory()) + Ok((channel, noop_shared_memory(), ())) }); })); assert_panic_contains(result, "broker returned unexpected negotiation response"); @@ -451,9 +468,9 @@ mod tests { let setup_called = Cell::new(false); assert!(matches!( - BrokerLocal::negotiate(channel, |_| { + BrokerLocal::negotiate(channel, |channel| { setup_called.set(true); - Ok(noop_shared_memory()) + Ok((channel, noop_shared_memory(), ())) }), Err(BrokerLocalError::Broker(ErrorCode::UnsupportedVersion)) )); @@ -469,9 +486,9 @@ mod tests { let setup_called = Cell::new(false); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let _ = BrokerLocal::negotiate(channel, |_| { + let _ = BrokerLocal::negotiate(channel, |channel| { setup_called.set(true); - Ok(noop_shared_memory()) + Ok((channel, noop_shared_memory(), ())) }); })); assert_panic_contains(result, "broker returned unrecoverable error"); @@ -489,7 +506,9 @@ mod tests { assert!(matches!( BrokerLocal::::negotiate(channel, |_| { - Err(FakeChannelError::SharedMemoryReceive) + Err::<(FakeControlChannel, Arc, ()), _>( + FakeChannelError::SharedMemoryReceive, + ) }), Err(BrokerLocalError::Channel( FakeChannelError::SharedMemoryReceive @@ -507,10 +526,14 @@ mod tests { None, ); - let _ = BrokerLocal::negotiate(channel, |_| { - Ok(Arc::new(NoopSharedMemory { - length: litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE - 1, - }) as Arc) + let _ = BrokerLocal::negotiate(channel, |channel| { + Ok(( + channel, + Arc::new(NoopSharedMemory { + length: litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE - 1, + }) as Arc, + (), + )) }); } @@ -596,7 +619,7 @@ mod tests { } } - impl LocalControlChannel for FakeControlChannel { + impl LocalSetupChannel for FakeControlChannel { type Error = FakeChannelError; fn send_handshake_request( @@ -612,6 +635,11 @@ mod tests { ) -> core::result::Result, Self::Error> { Ok(self.handshake_response.take()) } + } + + impl LocalCallChannel for FakeControlChannel { + type Error = FakeChannelError; + fn call( &self, request: BrokerRequest, @@ -643,7 +671,7 @@ mod tests { request_ids: Mutex>, } - impl LocalControlChannel for ConcurrentCallChannel { + impl LocalSetupChannel for ConcurrentCallChannel { type Error = Infallible; fn send_handshake_request( @@ -660,6 +688,10 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION, })) } + } + + impl LocalCallChannel for ConcurrentCallChannel { + type Error = Infallible; fn call( &self, diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs index da0688dd6e..10eed91915 100644 --- a/litebox_broker_local/src/pipe.rs +++ b/litebox_broker_local/src/pipe.rs @@ -2,7 +2,7 @@ // Licensed under the MIT license. use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::LocalControlChannel; +use litebox_broker_protocol::channel::LocalCallChannel; use litebox_broker_protocol::message::{BrokerOperation, BrokerResult, PipeRequest, PipeResponse}; use litebox_broker_protocol::pipe::{ CreatePipeRequest, CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE, ReadPipeRequest, @@ -12,7 +12,7 @@ use litebox_broker_protocol::shared_memory::SharedBufferDescriptor; use crate::{BrokerLocal, BrokerLocalError, Result}; -impl BrokerLocal { +impl BrokerLocal { /// Creates a broker-owned byte pipe. /// /// # Panics @@ -146,7 +146,7 @@ mod tests { use std::sync::Mutex; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; - use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, @@ -170,7 +170,8 @@ mod tests { BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { written: 2 })), BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 2 })), ]); - let local = BrokerLocal::negotiate(channel, |_| Ok(memory.clone())).unwrap(); + let (local, ()) = + BrokerLocal::negotiate(channel, |channel| Ok((channel, memory.clone(), ()))).unwrap(); let write_buffer = descriptor(2, 3); let read_buffer = descriptor(4, 3); @@ -219,7 +220,8 @@ mod tests { fn pipe_rejects_oversized_transfers_before_request() { let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); let channel = ScriptedChannel::new([]); - let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + let (local, ()) = + BrokerLocal::negotiate(channel, |channel| Ok((channel, memory, ()))).unwrap(); let oversized = descriptor(0, MAX_PIPE_TRANSFER_SIZE + 1); assert!(matches!( @@ -245,7 +247,8 @@ mod tests { read: 2, }))]); let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); - let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + let (local, ()) = + BrokerLocal::negotiate(channel, |channel| Ok((channel, memory, ()))).unwrap(); let mut destination = [0]; let _ = local.read_pipe(ObjectHandle(1), descriptor(0, 1), &mut destination); @@ -259,7 +262,8 @@ mod tests { written: 2, }))]); let memory = Arc::new(TestSharedMemory::new(SHARED_BUFFER_POOL_SIZE)); - let local = BrokerLocal::negotiate(channel, |_| Ok(memory)).unwrap(); + let (local, ()) = + BrokerLocal::negotiate(channel, |channel| Ok((channel, memory, ()))).unwrap(); let _ = local.write_pipe(ObjectHandle(1), descriptor(0, 1), &[0]); } @@ -332,7 +336,7 @@ mod tests { } } - impl LocalControlChannel for ScriptedChannel { + impl LocalSetupChannel for ScriptedChannel { type Error = Infallible; fn send_handshake_request( @@ -350,6 +354,11 @@ mod tests { broker_protocol_version: BROKER_PROTOCOL_VERSION, })) } + } + + impl LocalCallChannel for ScriptedChannel { + type Error = Infallible; + fn call( &self, request: BrokerRequest, diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index 784ae62d02..85750d3636 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -37,8 +37,11 @@ pub enum HostReceive { PeerClosed, } -/// Local-side control channel for broker association setup and active calls. -pub trait LocalControlChannel { +/// Local-side channel for broker association setup. +/// +/// Setup ends when the deployment consumes this channel into an active +/// [`LocalCallChannel`], so handshake and active call state cannot overlap. +pub trait LocalSetupChannel { /// Channel-specific error type. type Error; @@ -53,6 +56,12 @@ pub trait LocalControlChannel { /// Returns `Ok(None)` when the broker closed the channel cleanly before /// starting another response frame. fn recv_handshake_response(&mut self) -> Result, Self::Error>; +} + +/// Local-side channel for active broker calls. +pub trait LocalCallChannel { + /// Channel-specific error type. + type Error; /// Publishes one request and waits for its correlated response. /// @@ -85,10 +94,10 @@ pub trait HostSetupChannel { /// Local-side receive channel for broker-initiated asynchronous notifications. /// -/// A notification channel is separate from the control channel so active broker -/// requests remain strictly paired with their responses. The deployment is -/// responsible for binding this channel to the same authenticated broker -/// association as the matching control channel. +/// The notification path is logically separate from request and response +/// traffic so active broker requests remain strictly paired with their +/// responses. A deployment may carry notifications in the same authenticated +/// association as its control path. pub trait LocalNotificationChannel { /// Channel-specific error type. type Error; diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index ffe7c1c146..91bd020d75 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -8,7 +8,8 @@ //! no_std protocol, local, core, and host crates. //! //! After setup, the authenticated socket is retained only for liveness and -//! cancellation. Active requests and responses use a shared control ring. +//! fail-closed shutdown. Active requests, responses, and notifications use +//! shared control rings. use std::io::{Error, ErrorKind, Read, Result as IoResult, Write}; use std::net::Shutdown; @@ -28,8 +29,8 @@ use crate::unix_io::{ }; use litebox_broker_protocol::RequestId; use litebox_broker_protocol::channel::{ - HostNotificationChannel, HostReceive, HostSetupChannel, LocalControlChannel, - LocalNotificationChannel, PeerCredential, + HostNotificationChannel, HostReceive, HostSetupChannel, LocalCallChannel, + LocalNotificationChannel, LocalSetupChannel, PeerCredential, }; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, @@ -41,7 +42,7 @@ use litebox_broker_protocol::wire::{ encode_notification, encode_request, encode_response, }; -const MAX_FRAME_LEN: usize = 64 * 1024; +const MAX_SETUP_FRAME_LEN: usize = 64 * 1024; const CONTROL_RING_READY: &[u8] = b"litebox-control-ring-ready-v1"; /// Maximum number of active calls waiting for broker responses. pub const MAX_PENDING_CALLS: usize = 64; @@ -57,67 +58,55 @@ pub fn validate_peer_process(stream: &UnixStream, expected_process_id: u32) -> I Ok(()) } -/// Validates that two connected Unix sockets belong to the same process. -pub fn validate_same_peer_process(first: &UnixStream, second: &UnixStream) -> IoResult<()> { - if peer_process_id(first)? != peer_process_id(second)? { - return Err(Error::new( - ErrorKind::PermissionDenied, - "Unix sockets belong to different peer processes", - )); - } - Ok(()) -} - fn peer_process_id(stream: &UnixStream) -> IoResult { let credentials = rustix::net::sockopt::socket_peercred(stream)?; u32::try_from(credentials.pid.as_raw_pid()) .map_err(|_| invalid_data("Unix peer process ID is invalid")) } -/// Local-side Unix-domain-socket control channel for the hosted userland POC. -pub struct UnixStreamLocalControlChannel { - state: UnixStreamLocalControlState, -} - -enum UnixStreamLocalControlState { - Setup(UnixStreamLocalSetup), - Active(UnixStreamLocalActive), - Failed, -} - -struct UnixStreamLocalSetup { +/// Local-side broker association setup channel over a Unix stream. +pub struct UnixStreamLocalSetupChannel { stream: UnixStream, setup_deadline: Option, negotiated: bool, } -/// Independently owned handle for interrupting local control-channel I/O. -pub struct UnixStreamLocalControlCancellation { - failure_coordinator: Arc, +/// Call-issuing endpoint of an active local control-ring association. +pub struct UnixControlRingLocalCallChannel { + association: Arc, } -struct UnixStreamLocalActive { - request_producer: Mutex>, - failure_coordinator: Arc, +/// Independently owned handle for interrupting all local active-ring I/O. +pub struct UnixControlRingLocalShutdown { + association: Arc, } -struct LocalActiveFailureCoordinator { - stream: UnixStream, +/// State shared by every activated local endpoint of one association: the +/// request producer, the setup socket used for liveness and teardown, pending +/// call tracking, and the wake handles of all three ring directions. +struct LocalRingAssociation { + request_producer: Mutex>, + control_stream: UnixStream, pending_calls: Arc, - association_failure: Arc, - request_wait: ControlRingWakeHandle, - response_wait: ControlRingWakeHandle, + on_failure: Arc, + request_wake: ControlRingWakeHandle, + response_wake: ControlRingWakeHandle, + notification_wake: ControlRingWakeHandle, } -impl UnixStreamLocalControlChannel { - /// Creates a local control channel from an already-connected Unix stream. +/// Local notification receiver for a shared-ring Unix broker association. +pub struct UnixControlRingLocalNotificationChannel { + consumer: ControlRingConsumer, + association: Arc, +} + +impl UnixStreamLocalSetupChannel { + /// Creates a local setup channel from an already-connected Unix stream. pub const fn from_connected(stream: UnixStream) -> Self { Self { - state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { - stream, - setup_deadline: None, - negotiated: false, - }), + stream, + setup_deadline: None, + negotiated: false, } } @@ -136,57 +125,45 @@ impl UnixStreamLocalControlChannel { deadline: Instant, ) -> IoResult { UnixStream::connect(path).map(|stream| Self { - state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { - stream, - setup_deadline: Some(deadline), - negotiated: false, - }), + stream, + setup_deadline: Some(deadline), + negotiated: false, }) } - /// Receives the memfd associated with this control channel. + /// Receives one memfd offered by the broker during setup. pub fn receive_memfd( &mut self, expected_len: usize, deadline: Option, ) -> IoResult { - let UnixStreamLocalControlState::Setup(setup) = &mut self.state else { - return Err(invalid_data("broker control setup already completed")); - }; - crate::shared_memory::receive_memfd(&mut setup.stream, expected_len, deadline) + crate::shared_memory::receive_memfd(&mut self.stream, expected_len, deadline) } - /// Completes setup and starts the active control-ring response pump. + /// Consumes a negotiated setup channel into independently usable active + /// call, notification, and shutdown handles, starting the response + /// dispatcher and liveness monitor. /// /// The ring must be the validated control-ring memfd received during this /// setup exchange. - pub fn activate( - &mut self, + pub fn into_active( + self, ring: ControlRing, - association_failure: impl Fn() + Send + Sync + 'static, - ) -> IoResult { - let UnixStreamLocalControlState::Setup(setup) = &self.state else { - return Err(invalid_data("broker control channel already active")); - }; - if !setup.negotiated { + on_failure: impl Fn() + Send + Sync + 'static, + ) -> IoResult<( + UnixControlRingLocalCallChannel, + UnixControlRingLocalNotificationChannel, + UnixControlRingLocalShutdown, + )> { + if !self.negotiated { return Err(invalid_data( - "broker control channel activated before negotiation completed", + "broker local setup channel activated before negotiation completed", )); } - let UnixStreamLocalControlState::Setup(setup) = - core::mem::replace(&mut self.state, UnixStreamLocalControlState::Failed) - else { - unreachable!("broker control setup state disappeared"); - }; - let mut monitor_stream = setup.stream; - write_frame_with_deadline( - &mut monitor_stream, - CONTROL_RING_READY, - setup.setup_deadline, - )?; - let Some(ready) = read_frame_with_deadline(&mut monitor_stream, setup.setup_deadline)? - else { + let mut setup_stream = self.stream; + write_setup_frame(&mut setup_stream, CONTROL_RING_READY, self.setup_deadline)?; + let Some(ready) = read_setup_frame(&mut setup_stream, self.setup_deadline)? else { return Err(Error::new( ErrorKind::UnexpectedEof, "broker closed before control-ring setup acknowledgement", @@ -198,137 +175,134 @@ impl UnixStreamLocalControlChannel { )); } - let shutdown_stream = monitor_stream.try_clone()?; + let shutdown_stream = setup_stream.try_clone()?; let crate::control_ring::LocalControlRingEndpoints { request_producer, response_consumer, - notification_consumer: _, + notification_consumer, } = ring.into_local(); let pending_calls = Arc::new(PendingCalls::new()); - let association_failure: Arc = Arc::new(association_failure); - let failure_coordinator = Arc::new(LocalActiveFailureCoordinator { - stream: shutdown_stream, + let on_failure: Arc = Arc::new(on_failure); + let association = Arc::new(LocalRingAssociation { + request_wake: request_producer.wake_handle(), + request_producer: Mutex::new(request_producer), + control_stream: shutdown_stream, pending_calls: Arc::clone(&pending_calls), - association_failure, - request_wait: request_producer.wake_handle(), - response_wait: response_consumer.wake_handle(), + on_failure, + response_wake: response_consumer.wake_handle(), + notification_wake: notification_consumer.wake_handle(), }); - let response_failure_coordinator = Arc::clone(&failure_coordinator); + let response_association = Arc::clone(&association); if let Err(error) = thread::Builder::new() .name("litebox-broker-responses".to_owned()) .spawn(move || { - dispatch_responses(response_consumer, response_failure_coordinator); + dispatch_responses(response_consumer, response_association); }) { - let _ = failure_coordinator.fail(error); - return Err(Error::other("failed to start broker response pump")); + let _ = association.fail(error); + return Err(Error::other("failed to start broker response dispatcher")); } - let monitor_failure_coordinator = Arc::clone(&failure_coordinator); + let monitor_association = Arc::clone(&association); if let Err(error) = thread::Builder::new() .name("litebox-broker-liveness".to_owned()) .spawn(move || { - monitor_local_socket(&mut monitor_stream, &monitor_failure_coordinator); + monitor_local_socket(&mut setup_stream, &monitor_association); }) { - let _ = failure_coordinator.fail(error); + let _ = association.fail(error); return Err(Error::other("failed to start broker liveness monitor")); } - self.state = UnixStreamLocalControlState::Active(UnixStreamLocalActive { - request_producer: Mutex::new(request_producer), - failure_coordinator: Arc::clone(&failure_coordinator), - }); - Ok(UnixStreamLocalControlCancellation { - failure_coordinator, - }) + Ok(( + UnixControlRingLocalCallChannel { + association: Arc::clone(&association), + }, + UnixControlRingLocalNotificationChannel { + consumer: notification_consumer, + association: Arc::clone(&association), + }, + UnixControlRingLocalShutdown { association }, + )) } } -impl UnixStreamLocalControlCancellation { - /// Shuts down the control stream, unblocking pending reads or writes. - pub fn cancel(&self) -> IoResult<()> { - self.failure_coordinator.fail(Error::new( +impl UnixControlRingLocalShutdown { + /// Shuts down the active association, unblocking ring and socket waits. + pub fn shutdown(&self) -> IoResult<()> { + self.association.fail(Error::new( ErrorKind::ConnectionAborted, - "broker association cancelled", + "broker local association shut down", )) } } -impl Drop for UnixStreamLocalControlChannel { +impl Drop for UnixControlRingLocalCallChannel { fn drop(&mut self) { - let UnixStreamLocalControlState::Active(active) = &self.state else { - return; - }; - let _ = active.failure_coordinator.fail(Error::new( + let _ = self.association.fail(Error::new( ErrorKind::ConnectionAborted, - "broker active channel dropped", + "broker local call channel dropped", )); } } -fn shutdown(stream: &UnixStream) -> IoResult<()> { +fn shutdown_socket(stream: &UnixStream) -> IoResult<()> { match stream.shutdown(Shutdown::Both) { Err(error) if error.kind() == ErrorKind::NotConnected => Ok(()), result => result, } } -/// Host-side Unix-domain-socket control channel for the hosted userland POC. -pub struct UnixStreamHostControlChannel { +/// Host-side broker association setup channel over a Unix stream. +pub struct UnixStreamHostSetupChannel { stream: UnixStream, peer_credential: PeerCredential, setup_deadline: Option, negotiated: bool, } -/// Request-reading half of an active host control channel. -pub struct UnixStreamHostRequestSource { +/// Request-reading endpoint of an active host control-ring association. +pub struct UnixControlRingHostRequestSource { consumer: ControlRingConsumer, - active: Arc, + association: Arc, } -/// Shared response-writing half of an active host control channel. +/// Shared response-writing endpoint of an active host control-ring association. #[derive(Clone)] -pub struct UnixStreamHostResponseSink { +pub struct UnixControlRingHostResponseSink { producer: Arc>>, - active: Arc, + association: Arc, } -/// RAII guard that interrupts all active host control-channel I/O when dropped. -pub struct UnixStreamHostControlShutdown { - active: Arc, +/// RAII guard that interrupts all active host ring I/O when dropped. +pub struct UnixControlRingHostShutdown { + association: Arc, } -struct HostActiveState { - stream: UnixStream, - status: Mutex, - request_wait: ControlRingWakeHandle, - response_wait: ControlRingWakeHandle, +/// State shared by every activated host endpoint of one association: the setup +/// socket used for liveness and teardown, terminal status, and the wake handles +/// of all three ring directions. +struct HostRingAssociation { + control_stream: UnixStream, + status: Mutex, + request_wake: ControlRingWakeHandle, + response_wake: ControlRingWakeHandle, + notification_wake: ControlRingWakeHandle, } -enum HostActiveStatus { +enum HostAssociationStatus { Live, PeerClosed, Failed(Arc), } -/// Local-side Unix-domain-socket notification channel for the hosted userland POC. -pub struct UnixStreamLocalNotificationChannel { - stream: UnixStream, -} - -/// Independently owned handle for interrupting local notification-channel I/O. -pub struct UnixStreamLocalNotificationCancellation { - stream: UnixStream, -} - -/// Host-side Unix-domain-socket notification channel for the hosted userland POC. -pub struct UnixStreamHostNotificationChannel { - stream: UnixStream, +/// Host notification sender for a shared-ring Unix broker association. +pub struct UnixControlRingHostNotificationChannel { + producer: ControlRingProducer, + association: Arc, } -impl UnixStreamHostControlChannel { - /// Creates a host control channel from an accepted Unix stream. +impl UnixStreamHostSetupChannel { + /// Creates a host setup channel from an accepted Unix stream. pub const fn from_accepted(stream: UnixStream) -> Self { Self { stream, @@ -338,7 +312,7 @@ impl UnixStreamHostControlChannel { } } - /// Creates a host control channel after the deployment has authenticated + /// Creates a host setup channel after the deployment has authenticated /// and bound the accepted peer. `setup_deadline` bounds handshake I/O. pub const fn from_host_guaranteed(stream: UnixStream, setup_deadline: Instant) -> Self { Self { @@ -349,7 +323,7 @@ impl UnixStreamHostControlChannel { } } - /// Sends the memfd associated with this control channel. + /// Sends a memfd during association setup. pub fn send_memfd( &mut self, shared_memory: &MemfdSharedMemory, @@ -359,21 +333,22 @@ impl UnixStreamHostControlChannel { } /// Consumes a negotiated setup channel into independently usable active - /// request, response, and shutdown handles. + /// request, response, notification, and shutdown handles. pub fn into_active( mut self, ring: ControlRing, ) -> IoResult<( - UnixStreamHostRequestSource, - UnixStreamHostResponseSink, - UnixStreamHostControlShutdown, + UnixControlRingHostRequestSource, + UnixControlRingHostResponseSink, + UnixControlRingHostNotificationChannel, + UnixControlRingHostShutdown, )> { if !self.negotiated { return Err(invalid_data( - "broker host control channel activated before negotiation completed", + "broker host setup channel activated before negotiation completed", )); } - let Some(ready) = read_frame_with_deadline(&mut self.stream, self.setup_deadline)? else { + let Some(ready) = read_setup_frame(&mut self.stream, self.setup_deadline)? else { return Err(Error::new( ErrorKind::UnexpectedEof, "runner closed before control-ring setup acknowledgement", @@ -384,142 +359,99 @@ impl UnixStreamHostControlChannel { "runner sent an invalid control-ring setup acknowledgement", )); } - write_frame_with_deadline(&mut self.stream, CONTROL_RING_READY, self.setup_deadline)?; + write_setup_frame(&mut self.stream, CONTROL_RING_READY, self.setup_deadline)?; let shutdown_stream = self.stream.try_clone()?; let crate::control_ring::BrokerControlRingEndpoints { request_consumer, response_producer, - notification_producer: _, + notification_producer, } = ring.into_broker(); - let active = Arc::new(HostActiveState { - stream: shutdown_stream, - status: Mutex::new(HostActiveStatus::Live), - request_wait: request_consumer.wake_handle(), - response_wait: response_producer.wake_handle(), + let association = Arc::new(HostRingAssociation { + control_stream: shutdown_stream, + status: Mutex::new(HostAssociationStatus::Live), + request_wake: request_consumer.wake_handle(), + response_wake: response_producer.wake_handle(), + notification_wake: notification_producer.wake_handle(), }); - let monitor_active = Arc::clone(&active); + let monitor_association = Arc::clone(&association); thread::Builder::new() .name("litebox-runner-liveness".to_owned()) - .spawn(move || monitor_host_socket(&mut self.stream, &monitor_active))?; + .spawn(move || monitor_host_socket(&mut self.stream, &monitor_association))?; Ok(( - UnixStreamHostRequestSource { + UnixControlRingHostRequestSource { consumer: request_consumer, - active: Arc::clone(&active), + association: Arc::clone(&association), }, - UnixStreamHostResponseSink { + UnixControlRingHostResponseSink { producer: Arc::new(Mutex::new(response_producer)), - active: Arc::clone(&active), + association: Arc::clone(&association), }, - UnixStreamHostControlShutdown { active }, + UnixControlRingHostNotificationChannel { + producer: notification_producer, + association: Arc::clone(&association), + }, + UnixControlRingHostShutdown { association }, )) } } -impl UnixStreamHostControlShutdown { - /// Shuts down the active control socket without waiting for the response - /// writer mutex. +impl UnixControlRingHostShutdown { + /// Shuts down the active association without waiting for a ring lock. pub fn shutdown(&self) -> IoResult<()> { - self.active.fail(Error::new( + self.association.fail(Error::new( ErrorKind::ConnectionAborted, - "broker host control channel shut down", + "broker host association shut down", )) } } -impl Drop for UnixStreamHostControlShutdown { +impl Drop for UnixControlRingHostShutdown { fn drop(&mut self) { - let _ = self.active.fail(Error::new( + let _ = self.association.fail(Error::new( ErrorKind::ConnectionAborted, - "broker host control shutdown guard dropped", + "broker host association shutdown guard dropped", )); } } -impl UnixStreamLocalNotificationChannel { - /// Creates a local notification channel from an already-connected Unix stream. - pub const fn from_connected(stream: UnixStream) -> Self { - Self { stream } - } - - /// Connects to a userland broker Unix notification socket. - pub fn connect(path: impl AsRef) -> IoResult { - UnixStream::connect(path).map(Self::from_connected) - } - - /// Creates a handle that can interrupt pending notification-channel I/O. - pub fn cancellation_handle(&self) -> IoResult { - self.stream - .try_clone() - .map(|stream| UnixStreamLocalNotificationCancellation { stream }) - } -} - -impl UnixStreamLocalNotificationCancellation { - /// Shuts down the notification stream, unblocking pending reads. - pub fn cancel(&self) -> IoResult<()> { - shutdown(&self.stream) - } -} - -impl Drop for UnixStreamLocalNotificationChannel { - fn drop(&mut self) { - let _ = shutdown(&self.stream); - } -} - -impl UnixStreamHostNotificationChannel { - /// Creates a host notification channel from an accepted Unix stream. - pub const fn from_accepted(stream: UnixStream) -> Self { - Self { stream } - } -} - -impl LocalControlChannel for UnixStreamLocalControlChannel { +impl LocalSetupChannel for UnixStreamLocalSetupChannel { type Error = Error; fn send_handshake_request(&mut self, request: &BrokerHandshakeRequest) -> IoResult<()> { - let UnixStreamLocalControlState::Setup(setup) = &mut self.state else { - return Err(invalid_data("broker control channel is already active")); - }; let frame = encode_handshake_request(request.clone()); - write_frame_with_deadline(&mut setup.stream, &frame, setup.setup_deadline) + write_setup_frame(&mut self.stream, &frame, self.setup_deadline) } fn recv_handshake_response(&mut self) -> IoResult> { - let UnixStreamLocalControlState::Setup(setup) = &mut self.state else { - return Err(invalid_data("broker control channel is already active")); - }; - let frame = read_frame_with_deadline(&mut setup.stream, setup.setup_deadline)?; + let frame = read_setup_frame(&mut self.stream, self.setup_deadline)?; match frame { Some(frame) => { let response = decode_handshake_response(&frame).map_err(wire_error)?; - setup.negotiated = matches!(&response, BrokerHandshakeResponse::Negotiated { .. }); + self.negotiated = matches!(&response, BrokerHandshakeResponse::Negotiated { .. }); Ok(Some(response)) } None => Ok(None), } } +} + +impl LocalCallChannel for UnixControlRingLocalCallChannel { + type Error = Error; fn call(&self, request: BrokerRequest) -> IoResult { - let UnixStreamLocalControlState::Active(active) = &self.state else { - return Err(invalid_data("broker control channel is not active")); - }; + let association = &self.association; let request_id = request.request_id; - let pending_call = active - .failure_coordinator - .pending_calls - .register(request_id)?; + let pending_call = association.pending_calls.register(request_id)?; let request_frame = encode_request(request); let write_result = { - let mut producer = active + let mut producer = association .request_producer .lock() .expect("broker request writer mutex poisoned"); loop { - let write_status = active - .failure_coordinator + let write_status = association .pending_calls .run_if_live(|| producer.try_write(&request_frame).map_err(Error::from)); match write_status { @@ -539,14 +471,14 @@ impl LocalControlChannel for UnixStreamLocalControlChannel { } }; if let Err(error) = write_result { - let _ = active.failure_coordinator.fail(error); + let _ = association.fail(error); } pending_call.wait() } } -impl HostSetupChannel for UnixStreamHostControlChannel { +impl HostSetupChannel for UnixStreamHostSetupChannel { type Error = Error; fn peer_credential(&self) -> IoResult { @@ -554,7 +486,7 @@ impl HostSetupChannel for UnixStreamHostControlChannel { } fn recv_handshake_request(&mut self) -> IoResult> { - let Some(frame) = read_frame_with_deadline(&mut self.stream, self.setup_deadline)? else { + let Some(frame) = read_setup_frame(&mut self.stream, self.setup_deadline)? else { return Ok(HostReceive::PeerClosed); }; match decode_handshake_request(&frame) { @@ -565,7 +497,7 @@ impl HostSetupChannel for UnixStreamHostControlChannel { } fn send_handshake_response(&mut self, response: &BrokerHandshakeResponse) -> IoResult<()> { - write_frame_with_deadline( + write_setup_frame( &mut self.stream, &encode_handshake_response(response.clone()), self.setup_deadline, @@ -575,25 +507,25 @@ impl HostSetupChannel for UnixStreamHostControlChannel { } } -impl UnixStreamHostRequestSource { +impl UnixControlRingHostRequestSource { /// Receives one active broker request. pub fn recv_request(&mut self) -> IoResult> { loop { - if let Some(error) = self.active.request_failure() { + if let Some(error) = self.association.current_failure() { return Err(error); } match self.consumer.try_read(decode_request) { Ok(ControlRingReadStatus::Message(request)) => { - self.active.acknowledge_request(&mut self.consumer)?; + self.association.acknowledge_request(&mut self.consumer)?; return Ok(HostReceive::Message(request)); } Ok(ControlRingReadStatus::Empty { wait_epoch }) => { - if let Some(terminal) = self.active.request_terminal_result() { + if let Some(terminal) = self.association.request_terminal_result() { return terminal; } if let Err(error) = self.consumer.wait_for_message(wait_epoch) { let result = Err(copy_io_error(&error)); - let _ = self.active.fail(error); + let _ = self.association.fail(error); return result; } } @@ -603,13 +535,13 @@ impl UnixStreamHostRequestSource { Err(ControlRingReadError::Decode(error)) => { let error = wire_error(error); let result = Err(copy_io_error(&error)); - let _ = self.active.fail(error); + let _ = self.association.fail(error); return result; } Err(ControlRingReadError::Ring(error)) => { let error = Error::from(error); let result = Err(copy_io_error(&error)); - let _ = self.active.fail(error); + let _ = self.association.fail(error); return result; } } @@ -617,7 +549,7 @@ impl UnixStreamHostRequestSource { } } -impl UnixStreamHostResponseSink { +impl UnixControlRingHostResponseSink { /// Serializes and sends one complete active broker response. pub fn send_response(&self, response: &BrokerResponse) -> IoResult<()> { let frame = encode_response(response.clone()); @@ -626,12 +558,12 @@ impl UnixStreamHostResponseSink { .lock() .map_err(|_| Error::other("broker response writer mutex poisoned"))?; loop { - match self.active.try_publish_response(&mut producer, &frame)? { + match self.association.try_publish(&mut producer, &frame)? { ControlRingWriteStatus::Written => return Ok(()), ControlRingWriteStatus::Full { wait_epoch } => { if let Err(error) = producer.wait_for_capacity(wait_epoch) { let result = Err(copy_io_error(&error)); - let _ = self.active.fail(error); + let _ = self.association.fail(error); return result; } } @@ -640,35 +572,73 @@ impl UnixStreamHostResponseSink { } } -impl LocalNotificationChannel for UnixStreamLocalNotificationChannel { +impl LocalNotificationChannel for UnixControlRingLocalNotificationChannel { type Error = Error; fn recv_notification(&mut self) -> IoResult> { - match read_frame_with_deadline(&mut self.stream, None)? { - Some(frame) => decode_notification(&frame).map(Some).map_err(wire_error), - None => Ok(None), + loop { + if let Some(error) = self.association.pending_calls.current_failure() { + return Err(copy_io_error(&error)); + } + match self.consumer.try_read(decode_notification) { + Ok(ControlRingReadStatus::Message(notification)) => { + self.association + .acknowledge_notification(&mut self.consumer)?; + return Ok(Some(notification)); + } + Ok(ControlRingReadStatus::Empty { wait_epoch }) => { + if let Some(error) = self.association.pending_calls.current_failure() { + return Err(copy_io_error(&error)); + } + if let Err(error) = self.consumer.wait_for_message(wait_epoch) { + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } + Err(ControlRingReadError::Ring(error)) => { + let error = Error::from(error); + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + Err(ControlRingReadError::Decode(error)) => { + let error = wire_error(error); + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } } } } -impl HostNotificationChannel for UnixStreamHostNotificationChannel { +impl HostNotificationChannel for UnixControlRingHostNotificationChannel { type Error = Error; fn send_notification(&mut self, notification: &BrokerNotification) -> IoResult<()> { - write_frame_with_deadline( - &mut self.stream, - &encode_notification(notification.clone()), - None, - ) + let frame = encode_notification(notification.clone()); + loop { + match self.association.try_publish(&mut self.producer, &frame)? { + ControlRingWriteStatus::Written => return Ok(()), + ControlRingWriteStatus::Full { wait_epoch } => { + if let Err(error) = self.producer.wait_for_capacity(wait_epoch) { + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } + } + } } } struct PendingCalls { - state: Mutex, + state: Mutex, capacity_available: Condvar, } -struct PendingCallState { +struct PendingCallsState { calls: HashMap>, failure: Option>, } @@ -724,7 +694,7 @@ impl PendingCall { impl PendingCalls { fn new() -> Self { Self { - state: Mutex::new(PendingCallState { + state: Mutex::new(PendingCallsState { calls: HashMap::new(), failure: None, }), @@ -807,20 +777,42 @@ impl PendingCalls { } } -impl LocalActiveFailureCoordinator { +impl LocalRingAssociation { + fn acknowledge_notification( + &self, + consumer: &mut ControlRingConsumer, + ) -> IoResult<()> { + let result = self.pending_calls.run_if_live(|| { + consumer + .publish_head() + .map_err(Error::from) + .and_then(|()| consumer.wake_producer()) + }); + if let Err(error) = result { + let result = Err(copy_io_error(&error)); + let _ = self.fail(error); + return result; + } + Ok(()) + } + fn fail(&self, error: Error) -> IoResult<()> { let first_failure = self.pending_calls.record_failure(Arc::new(error)); - let request_wake = self.request_wait.interrupt_wait(); - let response_wake = self.response_wait.interrupt_wait(); - let shutdown_result = shutdown(&self.stream); + let request_wake = self.request_wake.interrupt_wait(); + let response_wake = self.response_wake.interrupt_wait(); + let notification_wake = self.notification_wake.interrupt_wait(); + let shutdown_result = shutdown_socket(&self.control_stream); if first_failure { - (self.association_failure)(); + (self.on_failure)(); } - request_wake.and(response_wake).and(shutdown_result) + request_wake + .and(response_wake) + .and(notification_wake) + .and(shutdown_result) } } -impl HostActiveState { +impl HostRingAssociation { fn acknowledge_request( &self, consumer: &mut ControlRingConsumer, @@ -829,8 +821,8 @@ impl HostActiveState { let status = self .status .lock() - .expect("broker host active-state mutex poisoned"); - if let HostActiveStatus::Failed(error) = &*status { + .expect("broker host association mutex poisoned"); + if let HostAssociationStatus::Failed(error) = &*status { return Err(copy_io_error(error)); } consumer @@ -851,14 +843,18 @@ impl HostActiveState { let mut status = self .status .lock() - .expect("broker host active-state mutex poisoned"); - if matches!(*status, HostActiveStatus::Live) { - *status = HostActiveStatus::Failed(Arc::new(error)); + .expect("broker host association mutex poisoned"); + if matches!(*status, HostAssociationStatus::Live) { + *status = HostAssociationStatus::Failed(Arc::new(error)); } } - let request_wake = self.request_wait.interrupt_wait(); - let response_wake = self.response_wait.interrupt_wait(); - request_wake.and(response_wake).and(shutdown(&self.stream)) + let request_wake = self.request_wake.interrupt_wait(); + let response_wake = self.response_wake.interrupt_wait(); + let notification_wake = self.notification_wake.interrupt_wait(); + request_wake + .and(response_wake) + .and(notification_wake) + .and(shutdown_socket(&self.control_stream)) } fn peer_closed(&self) { @@ -866,39 +862,40 @@ impl HostActiveState { let mut status = self .status .lock() - .expect("broker host active-state mutex poisoned"); - if matches!(*status, HostActiveStatus::Live) { - *status = HostActiveStatus::PeerClosed; + .expect("broker host association mutex poisoned"); + if matches!(*status, HostAssociationStatus::Live) { + *status = HostAssociationStatus::PeerClosed; } } - let _ = self.request_wait.interrupt_wait(); - let _ = self.response_wait.interrupt_wait(); + let _ = self.request_wake.interrupt_wait(); + let _ = self.response_wake.interrupt_wait(); + let _ = self.notification_wake.interrupt_wait(); } fn request_terminal_result(&self) -> Option>> { match &*self .status .lock() - .expect("broker host active-state mutex poisoned") + .expect("broker host association mutex poisoned") { - HostActiveStatus::Live => None, - HostActiveStatus::PeerClosed => Some(Ok(HostReceive::PeerClosed)), - HostActiveStatus::Failed(error) => Some(Err(copy_io_error(error))), + HostAssociationStatus::Live => None, + HostAssociationStatus::PeerClosed => Some(Ok(HostReceive::PeerClosed)), + HostAssociationStatus::Failed(error) => Some(Err(copy_io_error(error))), } } - fn request_failure(&self) -> Option { + fn current_failure(&self) -> Option { match &*self .status .lock() - .expect("broker host active-state mutex poisoned") + .expect("broker host association mutex poisoned") { - HostActiveStatus::Failed(error) => Some(copy_io_error(error)), - HostActiveStatus::Live | HostActiveStatus::PeerClosed => None, + HostAssociationStatus::Failed(error) => Some(copy_io_error(error)), + HostAssociationStatus::Live | HostAssociationStatus::PeerClosed => None, } } - fn try_publish_response( + fn try_publish( &self, producer: &mut ControlRingProducer, frame: &[u8], @@ -907,16 +904,16 @@ impl HostActiveState { let status = self .status .lock() - .expect("broker host active-state mutex poisoned"); + .expect("broker host association mutex poisoned"); match &*status { - HostActiveStatus::Live => {} - HostActiveStatus::PeerClosed => { + HostAssociationStatus::Live => {} + HostAssociationStatus::PeerClosed => { return Err(Error::new( ErrorKind::BrokenPipe, - "runner closed the active control channel", + "runner closed the active broker association", )); } - HostActiveStatus::Failed(error) => return Err(copy_io_error(error)), + HostAssociationStatus::Failed(error) => return Err(copy_io_error(error)), } producer .try_write(frame) @@ -937,45 +934,42 @@ impl HostActiveState { } } -fn monitor_local_socket( - stream: &mut UnixStream, - failure_coordinator: &LocalActiveFailureCoordinator, -) { - let error = monitor_socket(stream, "broker"); - let _ = failure_coordinator.fail(error); +fn monitor_local_socket(stream: &mut UnixStream, association: &LocalRingAssociation) { + let error = wait_for_socket_termination(stream, "broker"); + let _ = association.fail(error); } -fn monitor_host_socket(stream: &mut UnixStream, active: &HostActiveState) { +fn monitor_host_socket(stream: &mut UnixStream, association: &HostRingAssociation) { let mut byte = [0]; loop { match stream.read(&mut byte) { Ok(0) => { - active.peer_closed(); + association.peer_closed(); return; } Ok(_) => { - let _ = active.fail(invalid_data( + let _ = association.fail(invalid_data( "runner sent unexpected active control-socket data", )); return; } Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) => { - let _ = active.fail(error); + let _ = association.fail(error); return; } } } } -fn monitor_socket(stream: &mut UnixStream, peer: &'static str) -> Error { +fn wait_for_socket_termination(stream: &mut UnixStream, peer: &'static str) -> Error { let mut byte = [0]; loop { match stream.read(&mut byte) { Ok(0) => { return Error::new( ErrorKind::UnexpectedEof, - format!("{peer} closed the active control channel"), + format!("{peer} closed the active broker association"), ); } Ok(_) => { @@ -989,7 +983,7 @@ fn monitor_socket(stream: &mut UnixStream, peer: &'static str) -> Error { fn dispatch_responses( mut consumer: ControlRingConsumer, - failure_coordinator: Arc, + association: Arc, ) { loop { match consumer.try_read(decode_response) { @@ -998,31 +992,27 @@ fn dispatch_responses( .publish_head() .map_err(Error::from) .and_then(|()| consumer.wake_producer()) - .and_then(|()| failure_coordinator.pending_calls.complete(response)) + .and_then(|()| association.pending_calls.complete(response)) { - let _ = failure_coordinator.fail(error); + let _ = association.fail(error); return; } } Ok(ControlRingReadStatus::Empty { wait_epoch }) => { - if failure_coordinator - .pending_calls - .current_failure() - .is_some() - { + if association.pending_calls.current_failure().is_some() { return; } if let Err(error) = consumer.wait_for_message(wait_epoch) { - let _ = failure_coordinator.fail(error); + let _ = association.fail(error); return; } } Err(ControlRingReadError::Ring(error)) => { - let _ = failure_coordinator.fail(Error::from(error)); + let _ = association.fail(Error::from(error)); return; } Err(ControlRingReadError::Decode(error)) => { - let _ = failure_coordinator.fail(wire_error(error)); + let _ = association.fail(wire_error(error)); return; } } @@ -1036,7 +1026,7 @@ fn copy_io_error(error: &Error) -> Error { } } -fn read_frame_with_deadline( +fn read_setup_frame( stream: &mut UnixStream, deadline: Option, ) -> IoResult>> { @@ -1047,7 +1037,7 @@ fn read_frame_with_deadline( refresh_read_deadline(stream, deadline)?; match stream.read(&mut len_buf[read..]) { Ok(0) if read == 0 => return Ok(None), - Ok(0) => return Err(invalid_data("truncated broker frame length")), + Ok(0) => return Err(invalid_data("truncated broker setup frame length")), Ok(len) => read += len, Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) => return Err(error), @@ -1055,8 +1045,8 @@ fn read_frame_with_deadline( } let len = u32::from_le_bytes(len_buf) as usize; - if len == 0 || len > MAX_FRAME_LEN { - return Err(invalid_data("invalid broker frame length")); + if len == 0 || len > MAX_SETUP_FRAME_LEN { + return Err(invalid_data("invalid broker setup frame length")); } let mut frame = vec![0; len]; @@ -1064,7 +1054,7 @@ fn read_frame_with_deadline( while read < frame.len() { refresh_read_deadline(stream, deadline)?; match stream.read(&mut frame[read..]) { - Ok(0) => return Err(invalid_data("truncated broker frame")), + Ok(0) => return Err(invalid_data("truncated broker setup frame")), Ok(len) => read += len, Err(error) if error.kind() == ErrorKind::Interrupted => {} Err(error) => return Err(error), @@ -1074,16 +1064,17 @@ fn read_frame_with_deadline( }) } -fn write_frame_with_deadline( +fn write_setup_frame( stream: &mut UnixStream, frame: &[u8], deadline: Option, ) -> IoResult<()> { with_write_deadline(stream, deadline, |stream, deadline| { - if frame.is_empty() || frame.len() > MAX_FRAME_LEN { - return Err(invalid_data("invalid broker frame length")); + if frame.is_empty() || frame.len() > MAX_SETUP_FRAME_LEN { + return Err(invalid_data("invalid broker setup frame length")); } - let len = u32::try_from(frame.len()).map_err(|_| invalid_data("broker frame too large"))?; + let len = + u32::try_from(frame.len()).map_err(|_| invalid_data("broker setup frame too large"))?; write_all_with_deadline(stream, &len.to_le_bytes(), deadline)?; write_all_with_deadline(stream, frame, deadline) }) @@ -1100,7 +1091,7 @@ fn write_all_with_deadline( Ok(0) => { return Err(Error::new( ErrorKind::WriteZero, - "failed to write broker frame", + "failed to write broker setup frame", )); } Ok(written) => buffer = &buffer[written..], @@ -1137,7 +1128,7 @@ mod control_ring_tests { use crate::control_ring::{ CONTROL_RING_MEMORY_SIZE, CONTROL_RING_SLOT_COUNT, ControlRingProducer, }; - use litebox_broker_protocol::channel::LocalControlChannel; + use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::message::{ BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, }; @@ -1170,21 +1161,19 @@ mod control_ring_tests { ) } - fn negotiated_local(stream: UnixStream) -> UnixStreamLocalControlChannel { - UnixStreamLocalControlChannel { - state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { - stream, - setup_deadline: Some(Instant::now() + Duration::from_secs(2)), - negotiated: true, - }), + fn negotiated_local(stream: UnixStream) -> UnixStreamLocalSetupChannel { + UnixStreamLocalSetupChannel { + stream, + setup_deadline: Some(Instant::now() + Duration::from_secs(2)), + negotiated: true, } } fn activate_local( - association_failure: impl Fn() + Send + Sync + 'static, + on_failure: impl Fn() + Send + Sync + 'static, ) -> ( - UnixStreamLocalControlChannel, - UnixStreamLocalControlCancellation, + UnixControlRingLocalCallChannel, + UnixControlRingLocalShutdown, Producer, Consumer, UnixStream, @@ -1193,16 +1182,15 @@ mod control_ring_tests { let mut ack_stream = peer_stream.try_clone().unwrap(); let acknowledgement = thread::spawn(move || { assert_eq!( - read_frame_with_deadline(&mut ack_stream, None) - .unwrap() - .unwrap(), + read_setup_frame(&mut ack_stream, None).unwrap().unwrap(), CONTROL_RING_READY ); - write_frame_with_deadline(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); + write_setup_frame(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); }); let (local_ring, broker_ring) = ring_pair(); - let mut channel = negotiated_local(local_stream); - let cancellation = channel.activate(local_ring, association_failure).unwrap(); + let setup = negotiated_local(local_stream); + let (channel, _notifications, shutdown) = + setup.into_active(local_ring, on_failure).unwrap(); acknowledgement.join().unwrap(); let crate::control_ring::BrokerControlRingEndpoints { request_consumer, @@ -1211,17 +1199,17 @@ mod control_ring_tests { } = broker_ring.into_broker(); ( channel, - cancellation, + shutdown, response_producer, request_consumer, peer_stream, ) } - fn split_host() -> ( - UnixStreamHostRequestSource, - UnixStreamHostResponseSink, - UnixStreamHostControlShutdown, + fn activate_host() -> ( + UnixControlRingHostRequestSource, + UnixControlRingHostResponseSink, + UnixControlRingHostShutdown, Producer, Consumer, UnixStream, @@ -1229,22 +1217,20 @@ mod control_ring_tests { let (peer_stream, host_stream) = UnixStream::pair().unwrap(); let mut ack_stream = peer_stream.try_clone().unwrap(); let acknowledgement = thread::spawn(move || { - write_frame_with_deadline(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); + write_setup_frame(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); assert_eq!( - read_frame_with_deadline(&mut ack_stream, None) - .unwrap() - .unwrap(), + read_setup_frame(&mut ack_stream, None).unwrap().unwrap(), CONTROL_RING_READY ); }); let (local_ring, host_ring) = ring_pair(); - let channel = UnixStreamHostControlChannel { + let channel = UnixStreamHostSetupChannel { stream: host_stream, peer_credential: PeerCredential::HostGuaranteed, setup_deadline: Some(Instant::now() + Duration::from_secs(2)), negotiated: true, }; - let (source, sink, shutdown) = channel.into_active(host_ring).unwrap(); + let (source, sink, _notifications, shutdown) = channel.into_active(host_ring).unwrap(); acknowledgement.join().unwrap(); let crate::control_ring::LocalControlRingEndpoints { request_producer, @@ -1261,6 +1247,33 @@ mod control_ring_tests { ) } + fn notification_channel_pair() -> ( + UnixControlRingLocalCallChannel, + UnixControlRingLocalNotificationChannel, + UnixControlRingHostNotificationChannel, + UnixControlRingHostShutdown, + ) { + let (local_stream, host_stream) = UnixStream::pair().unwrap(); + let (local_ring, host_ring) = ring_pair(); + let local_setup = negotiated_local(local_stream); + let host_control = UnixStreamHostSetupChannel { + stream: host_stream, + peer_credential: PeerCredential::HostGuaranteed, + setup_deadline: Some(Instant::now() + Duration::from_secs(2)), + negotiated: true, + }; + let host_active = thread::spawn(move || host_control.into_active(host_ring).unwrap()); + let (local_call, local_notifications, _local_shutdown) = + local_setup.into_active(local_ring, || {}).unwrap(); + let (_source, _sink, host_notifications, shutdown) = host_active.join().unwrap(); + ( + local_call, + local_notifications, + host_notifications, + shutdown, + ) + } + fn read_request(consumer: &mut Consumer) -> BrokerRequest { loop { match consumer.try_read(decode_request).unwrap() { @@ -1321,10 +1334,9 @@ mod control_ring_tests { #[test] fn linux_peer_validation_identifies_connected_process() { - let (first, second) = UnixStream::pair().unwrap(); + let (first, _second) = UnixStream::pair().unwrap(); validate_peer_process(&first, std::process::id()).unwrap(); - validate_same_peer_process(&first, &second).unwrap(); let unexpected_process_id = std::process::id().checked_add(1).unwrap(); assert_eq!( validate_peer_process(&first, unexpected_process_id) @@ -1337,26 +1349,20 @@ mod control_ring_tests { #[test] fn setup_frames_round_trip_and_reject_invalid_boundaries() { let (mut writer, mut reader) = UnixStream::pair().unwrap(); - write_frame_with_deadline(&mut writer, &[1, 2, 3], None).unwrap(); + write_setup_frame(&mut writer, &[1, 2, 3], None).unwrap(); assert_eq!( - read_frame_with_deadline(&mut reader, None) - .unwrap() - .unwrap(), + read_setup_frame(&mut reader, None).unwrap().unwrap(), [1, 2, 3] ); let (writer, mut reader) = UnixStream::pair().unwrap(); drop(writer); - assert!( - read_frame_with_deadline(&mut reader, None) - .unwrap() - .is_none() - ); + assert!(read_setup_frame(&mut reader, None).unwrap().is_none()); for frame_prefix in [ vec![1, 0], 0u32.to_le_bytes().to_vec(), - u32::try_from(MAX_FRAME_LEN + 1) + u32::try_from(MAX_SETUP_FRAME_LEN + 1) .unwrap() .to_le_bytes() .to_vec(), @@ -1365,9 +1371,7 @@ mod control_ring_tests { writer.write_all(&frame_prefix).unwrap(); drop(writer); assert_eq!( - read_frame_with_deadline(&mut reader, None) - .unwrap_err() - .kind(), + read_setup_frame(&mut reader, None).unwrap_err().kind(), ErrorKind::InvalidData ); } @@ -1377,41 +1381,37 @@ mod control_ring_tests { writer.write_all(&[1, 2]).unwrap(); drop(writer); assert_eq!( - read_frame_with_deadline(&mut reader, None) - .unwrap_err() - .kind(), + read_setup_frame(&mut reader, None).unwrap_err().kind(), ErrorKind::InvalidData ); } #[test] - fn local_control_channel_enforces_setup_and_active_phases() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); - assert_eq!( - channel.call(request(0)).unwrap_err().kind(), - ErrorKind::InvalidData - ); + fn local_setup_rejects_activation_before_negotiation() { + let (local_stream, _host_stream) = UnixStream::pair().unwrap(); + let setup = UnixStreamLocalSetupChannel::from_connected(local_stream); let (ring, _) = ring_pair(); - assert_eq!( - channel.activate(ring, || {}).err().unwrap().kind(), - ErrorKind::InvalidData - ); + let Err(error) = setup.into_active(ring, || {}) else { + panic!("local setup channel activated before negotiation"); + }; + assert_eq!(error.kind(), ErrorKind::InvalidData); + } + + #[test] + fn local_setup_negotiates_then_activates_and_closes_on_drop() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let mut setup = UnixStreamLocalSetupChannel::from_connected(local_stream); let handshake_request = BrokerHandshakeRequest { protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, }; - channel.send_handshake_request(&handshake_request).unwrap(); + setup.send_handshake_request(&handshake_request).unwrap(); assert_eq!( - decode_handshake_request( - &read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap() - ) - .unwrap(), + decode_handshake_request(&read_setup_frame(&mut host_stream, None).unwrap().unwrap()) + .unwrap(), handshake_request ); - write_frame_with_deadline( + write_setup_frame( &mut host_stream, &encode_handshake_response(BrokerHandshakeResponse::Negotiated { broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, @@ -1420,41 +1420,26 @@ mod control_ring_tests { ) .unwrap(); assert!(matches!( - channel.recv_handshake_response().unwrap(), + setup.recv_handshake_response().unwrap(), Some(BrokerHandshakeResponse::Negotiated { .. }) )); let acknowledgement = thread::spawn(move || { assert_eq!( - read_frame_with_deadline(&mut host_stream, None) - .unwrap() - .unwrap(), + read_setup_frame(&mut host_stream, None).unwrap().unwrap(), CONTROL_RING_READY ); - write_frame_with_deadline(&mut host_stream, CONTROL_RING_READY, None).unwrap(); + write_setup_frame(&mut host_stream, CONTROL_RING_READY, None).unwrap(); host_stream }); let (ring, _) = ring_pair(); - let _cancellation = channel.activate(ring, || {}).unwrap(); + let (call_channel, _notifications, _shutdown) = setup.into_active(ring, || {}).unwrap(); let mut host_stream = acknowledgement.join().unwrap(); - let (ring, _) = ring_pair(); - assert_eq!( - channel.activate(ring, || {}).err().unwrap().kind(), - ErrorKind::InvalidData - ); - assert_eq!( - channel - .send_handshake_request(&handshake_request) - .unwrap_err() - .kind(), - ErrorKind::InvalidData - ); - host_stream .set_read_timeout(Some(Duration::from_secs(1))) .unwrap(); - drop(channel); + drop(call_channel); let mut byte = [0]; assert_eq!(host_stream.read(&mut byte).unwrap(), 0); } @@ -1463,16 +1448,17 @@ mod control_ring_tests { fn two_way_ready_ack_activates_ring_transport() { let (local_stream, host_stream) = UnixStream::pair().unwrap(); let (local_ring, host_ring) = ring_pair(); - let mut local = negotiated_local(local_stream); - let host = UnixStreamHostControlChannel { + let local_setup = negotiated_local(local_stream); + let host = UnixStreamHostSetupChannel { stream: host_stream, peer_credential: PeerCredential::HostGuaranteed, setup_deadline: Some(Instant::now() + Duration::from_secs(2)), negotiated: true, }; let host_active = thread::spawn(move || host.into_active(host_ring).unwrap()); - let _cancellation = local.activate(local_ring, || {}).unwrap(); - let (mut source, sink, _shutdown) = host_active.join().unwrap(); + let (local, _local_notifications, _local_shutdown) = + local_setup.into_active(local_ring, || {}).unwrap(); + let (mut source, sink, _host_notifications, _shutdown) = host_active.join().unwrap(); let caller = thread::spawn(move || local.call(request(7))); let HostReceive::Message(received) = source.recv_request().unwrap() else { @@ -1484,7 +1470,7 @@ mod control_ring_tests { #[test] fn local_matches_out_of_order_ring_responses_without_socket_frames() { - let (channel, _cancellation, mut responses, mut requests, mut peer) = activate_local(|| {}); + let (channel, _shutdown, mut responses, mut requests, mut peer) = activate_local(|| {}); peer.set_read_timeout(Some(Duration::from_millis(100))) .unwrap(); let channel = Arc::new(channel); @@ -1512,7 +1498,7 @@ mod control_ring_tests { #[test] fn pending_capacity_blocks_before_sixty_fifth_publication() { - let (channel, cancellation, mut responses, mut requests, _peer) = activate_local(|| {}); + let (channel, shutdown, mut responses, mut requests, _peer) = activate_local(|| {}); let channel = Arc::new(channel); let start = Arc::new(Barrier::new(MAX_PENDING_CALLS + 2)); let callers = (0..=MAX_PENDING_CALLS) @@ -1535,7 +1521,7 @@ mod control_ring_tests { let released = read_request(&mut requests).request_id; assert!(!published.contains(&released)); - cancellation.cancel().unwrap(); + shutdown.shutdown().unwrap(); let completed = callers .into_iter() .map(|caller| usize::from(caller.join().unwrap().is_ok())) @@ -1548,7 +1534,7 @@ mod control_ring_tests { for payload_kind in 0..3 { let failures = Arc::new(AtomicUsize::new(0)); let callback_failures = Arc::clone(&failures); - let (channel, _cancellation, mut responses, mut requests, _peer) = + let (channel, _shutdown, mut responses, mut requests, _peer) = activate_local(move || { callback_failures.fetch_add(1, Ordering::SeqCst); }); @@ -1578,23 +1564,23 @@ mod control_ring_tests { } #[test] - fn local_socket_eof_and_cancellation_wake_pending_calls() { + fn local_socket_eof_and_shutdown_wake_pending_calls() { for close_peer in [false, true] { - let (channel, cancellation, _responses, mut requests, peer) = activate_local(|| {}); + let (channel, shutdown, _responses, mut requests, peer) = activate_local(|| {}); let caller = thread::spawn(move || channel.call(request(1))); read_request(&mut requests); if close_peer { drop(peer); } else { - cancellation.cancel().unwrap(); + shutdown.shutdown().unwrap(); } assert!(caller.join().unwrap().is_err()); } } #[test] - fn host_split_decodes_requests_and_cloned_sinks_publish_complete_responses() { - let (mut source, sink, _shutdown, mut requests, mut responses, _peer) = split_host(); + fn host_activation_decodes_requests_and_cloned_sinks_publish_complete_responses() { + let (mut source, sink, _shutdown, mut requests, mut responses, _peer) = activate_host(); write_payload(&mut requests, &encode_request(request(1))); assert!(matches!( source.recv_request().unwrap(), @@ -1618,7 +1604,7 @@ mod control_ring_tests { #[test] fn host_clean_close_wakes_request_wait_as_peer_closed() { - let (mut source, _sink, _shutdown, _requests, _responses, peer) = split_host(); + let (mut source, _sink, _shutdown, _requests, _responses, peer) = activate_host(); let receiver = thread::spawn(move || source.recv_request()); drop(peer); assert_eq!(receiver.join().unwrap().unwrap(), HostReceive::PeerClosed); @@ -1626,10 +1612,10 @@ mod control_ring_tests { #[test] fn host_failure_preempts_queued_and_decoded_requests_but_peer_close_drains() { - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); write_payload(&mut requests, &encode_request(request(1))); source - .active + .association .fail(Error::new(ErrorKind::TimedOut, "test failure")) .unwrap(); assert_eq!( @@ -1637,28 +1623,28 @@ mod control_ring_tests { ErrorKind::TimedOut ); - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); write_payload(&mut requests, &encode_request(request(2))); assert!(matches!( source.consumer.try_read(decode_request).unwrap(), ControlRingReadStatus::Message(_) )); source - .active + .association .fail(Error::new(ErrorKind::TimedOut, "test failure")) .unwrap(); assert_eq!( source - .active + .association .acknowledge_request(&mut source.consumer) .unwrap_err() .kind(), ErrorKind::TimedOut ); - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); write_payload(&mut requests, &encode_request(request(3))); - source.active.peer_closed(); + source.association.peer_closed(); assert!(matches!( source.recv_request().unwrap(), HostReceive::Message(BrokerRequest { @@ -1671,7 +1657,7 @@ mod control_ring_tests { #[test] fn dropping_host_shutdown_guard_wakes_request_wait_and_closes_socket() { - let (mut source, sink, shutdown, _requests, _responses, mut peer) = split_host(); + let (mut source, sink, shutdown, _requests, _responses, mut peer) = activate_host(); peer.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); let receiver = thread::spawn(move || source.recv_request()); @@ -1688,7 +1674,7 @@ mod control_ring_tests { #[test] fn host_close_wakes_response_producer_blocked_on_full_ring() { - let (_source, sink, _shutdown, _requests, _responses, peer) = split_host(); + let (_source, sink, _shutdown, _requests, _responses, peer) = activate_host(); for id in 0..CONTROL_RING_SLOT_COUNT { sink.send_response(&response(RequestId(id))).unwrap(); } @@ -1704,7 +1690,7 @@ mod control_ring_tests { #[test] fn host_reports_wrong_phase_ring_message_as_protocol_violation() { - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); write_payload( &mut requests, &encode_handshake_request(BrokerHandshakeRequest { @@ -1719,7 +1705,7 @@ mod control_ring_tests { #[test] fn malformed_host_ring_request_is_fatal_invalid_data() { - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = split_host(); + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); write_payload(&mut requests, &[u8::MAX]); assert_eq!( source.recv_request().unwrap_err().kind(), @@ -1730,15 +1716,15 @@ mod control_ring_tests { #[test] fn host_setup_rejects_active_frames_and_requires_negotiation() { let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); - write_frame_with_deadline(&mut peer_stream, &encode_request(request(0)), None).unwrap(); + let mut channel = UnixStreamHostSetupChannel::from_accepted(host_stream); + write_setup_frame(&mut peer_stream, &encode_request(request(0)), None).unwrap(); assert_eq!( channel.recv_handshake_request().unwrap(), HostReceive::ProtocolViolation ); let (_peer_stream, host_stream) = UnixStream::pair().unwrap(); - let channel = UnixStreamHostControlChannel::from_accepted(host_stream); + let channel = UnixStreamHostSetupChannel::from_accepted(host_stream); let (ring, _) = ring_pair(); let Err(error) = channel.into_active(ring) else { panic!("host control channel activated before negotiation"); @@ -1750,14 +1736,12 @@ mod control_ring_tests { fn ready_ack_uses_absolute_setup_deadline() { let (local_stream, _peer) = UnixStream::pair().unwrap(); let (ring, _) = ring_pair(); - let mut local = UnixStreamLocalControlChannel { - state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { - stream: local_stream, - setup_deadline: Some(Instant::now() + Duration::from_millis(30)), - negotiated: true, - }), + let local = UnixStreamLocalSetupChannel { + stream: local_stream, + setup_deadline: Some(Instant::now() + Duration::from_millis(30)), + negotiated: true, }; - let Err(error) = local.activate(ring, || {}) else { + let Err(error) = local.into_active(ring, || {}) else { panic!("activation unexpectedly succeeded"); }; assert!(matches!( @@ -1769,12 +1753,10 @@ mod control_ring_tests { #[test] fn handshake_reads_use_absolute_setup_deadlines() { let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); - let mut local = UnixStreamLocalControlChannel { - state: UnixStreamLocalControlState::Setup(UnixStreamLocalSetup { - stream: local_stream, - setup_deadline: Some(Instant::now() + Duration::from_millis(50)), - negotiated: false, - }), + let mut local = UnixStreamLocalSetupChannel { + stream: local_stream, + setup_deadline: Some(Instant::now() + Duration::from_millis(50)), + negotiated: false, }; let local_reader = thread::spawn(move || local.recv_handshake_response().unwrap_err()); host_stream.write_all(&8u32.to_le_bytes()).unwrap(); @@ -1791,7 +1773,7 @@ mod control_ring_tests { ); let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); - let mut host = UnixStreamHostControlChannel::from_host_guaranteed( + let mut host = UnixStreamHostSetupChannel::from_host_guaranteed( host_stream, Instant::now() + Duration::from_millis(50), ); @@ -1811,10 +1793,8 @@ mod control_ring_tests { } #[test] - fn notification_frame_round_trips() { - let (local_stream, host_stream) = UnixStream::pair().unwrap(); - let mut local = UnixStreamLocalNotificationChannel::from_connected(local_stream); - let mut host = UnixStreamHostNotificationChannel::from_accepted(host_stream); + fn notification_ring_round_trips() { + let (_control, mut local, mut host, _shutdown) = notification_channel_pair(); let notification = BrokerNotification::Readiness( litebox_broker_protocol::message::ReadinessNotification { handle: ObjectHandle(7), @@ -1822,9 +1802,81 @@ mod control_ring_tests { }, ); + let receiver = thread::spawn(move || local.recv_notification()); + thread::sleep(Duration::from_millis(20)); host.send_notification(¬ification).unwrap(); - assert_eq!(local.recv_notification().unwrap(), Some(notification)); + assert_eq!(receiver.join().unwrap().unwrap(), Some(notification)); + } + + #[test] + fn full_notification_ring_wakes_after_consumer_progress() { + let (_control, mut local, mut host, _shutdown) = notification_channel_pair(); + let notification = BrokerNotification::Readiness( + litebox_broker_protocol::message::ReadinessNotification { + handle: ObjectHandle(7), + readiness: litebox_broker_protocol::readiness::ReadinessFlags::READ, + }, + ); + for _ in 0..crate::control_ring::CONTROL_RING_NOTIFICATION_SLOT_COUNT { + host.send_notification(¬ification).unwrap(); + } + + let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(1); + let (done_sender, done_receiver) = std::sync::mpsc::sync_channel(1); + let writer = thread::spawn(move || { + started_sender.send(()).unwrap(); + host.send_notification(¬ification).unwrap(); + done_sender.send(()).unwrap(); + }); + started_receiver.recv().unwrap(); + assert!( + done_receiver + .recv_timeout(Duration::from_millis(20)) + .is_err() + ); + + assert!(matches!( + local.recv_notification().unwrap(), + Some(BrokerNotification::Readiness(_)) + )); + done_receiver.recv_timeout(Duration::from_secs(1)).unwrap(); + writer.join().unwrap(); + } + + #[test] + fn association_shutdown_interrupts_notification_wait() { + let (_control, mut local, _host, shutdown) = notification_channel_pair(); + let receiver = thread::spawn(move || local.recv_notification()); + + shutdown.shutdown().unwrap(); + + assert_eq!( + receiver.join().unwrap().unwrap_err().kind(), + ErrorKind::UnexpectedEof + ); + } + + #[test] + fn malformed_notification_fails_the_association() { + let (control, mut local, mut host, _shutdown) = notification_channel_pair(); + assert_eq!( + host.producer.try_write(&[0xff]).unwrap(), + ControlRingWriteStatus::Written + ); + host.producer.wake_consumer().unwrap(); + + assert_eq!( + local.recv_notification().unwrap_err().kind(), + ErrorKind::InvalidData + ); + assert!( + control + .association + .pending_calls + .current_failure() + .is_some() + ); } #[test] diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 6137ff1edb..89f76d39c5 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -25,8 +25,8 @@ use litebox_broker_protocol::shared_memory::{ use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ - UnixStreamHostControlChannel, UnixStreamHostControlShutdown, UnixStreamHostNotificationChannel, - UnixStreamHostRequestSource, UnixStreamHostResponseSink, validate_peer_process, + UnixControlRingHostRequestSource, UnixControlRingHostResponseSink, UnixControlRingHostShutdown, + UnixStreamHostSetupChannel, validate_peer_process, }; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -50,11 +50,8 @@ fn main() -> Result<(), Box> { .prefix("litebox-broker-userland-") .tempdir()?; let control_socket_path = socket_dir.path().join("broker.sock"); - let notification_socket_path = socket_dir.path().join("broker-notification.sock"); let control_listener = UnixListener::bind(&control_socket_path)?; - let notification_listener = UnixListener::bind(¬ification_socket_path)?; control_listener.set_nonblocking(true)?; - notification_listener.set_nonblocking(true)?; let broker = BrokerCore::new(PolicyEngine::with_host_guaranteed_rights( ObjectRights::all(), ))?; @@ -64,19 +61,12 @@ fn main() -> Result<(), Box> { .arg("--unstable") .arg("--broker-control-socket") .arg(&control_socket_path) - .arg("--broker-notification-socket") - .arg(¬ification_socket_path) .args(&args.runner_arguments); let mut runner = runner_command.spawn()?; let runner_process_id = runner.id(); - let association_result = serve_runner( - &broker, - &control_listener, - ¬ification_listener, - &mut runner, - runner_process_id, - ); + let association_result = + serve_runner(&broker, &control_listener, &mut runner, runner_process_id); if association_result.is_err() { let _ = runner.kill(); } @@ -91,7 +81,6 @@ fn main() -> Result<(), Box> { fn serve_runner( broker: &BrokerCore, control_listener: &UnixListener, - notification_listener: &UnixListener, runner: &mut Child, runner_process_id: u32, ) -> Result<(), Box> { @@ -103,22 +92,13 @@ fn serve_runner( setup_deadline, "control", )?; - let notification_stream = accept_runner_stream( - notification_listener, - runner, - runner_process_id, - setup_deadline, - "notification", - )?; let shared_memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE)?; let shared_buffers = SharedBufferPool::new(shared_memory, SHARED_BUFFER_LAYOUT)?; let control_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE)?; let control_ring = ControlRing::new(control_memory) .map_err(|error| IoError::other(format!("failed to create control ring: {error:?}")))?; let mut control_channel = - UnixStreamHostControlChannel::from_host_guaranteed(control_stream, setup_deadline); - let _notification_channel = - UnixStreamHostNotificationChannel::from_accepted(notification_stream); + UnixStreamHostSetupChannel::from_host_guaranteed(control_stream, setup_deadline); let association = match setup_connection(broker, &mut control_channel, &shared_buffers, |channel| { channel.send_memfd(shared_buffers.memory(), Some(setup_deadline))?; @@ -148,16 +128,17 @@ fn serve_runner( .into()); } }; - let (request_source, response_sink, shutdown) = control_channel.into_active(control_ring)?; + let (request_source, response_sink, _notification_channel, shutdown) = + control_channel.into_active(control_ring)?; dispatch_requests(association, request_source, response_sink, shutdown)?; Ok(()) } fn dispatch_requests( association: BrokerHostAssociation<'_, Memory>, - mut request_source: UnixStreamHostRequestSource, - response_sink: UnixStreamHostResponseSink, - shutdown: UnixStreamHostControlShutdown, + mut request_source: UnixControlRingHostRequestSource, + response_sink: UnixControlRingHostResponseSink, + shutdown: UnixControlRingHostShutdown, ) -> IoResult<()> { let association = Arc::new(association); let failure_coordinator = Arc::new(HostAssociationFailureCoordinator::new(shutdown)); @@ -204,7 +185,7 @@ fn dispatch_requests( } fn read_requests( - request_source: &mut UnixStreamHostRequestSource, + request_source: &mut UnixControlRingHostRequestSource, request_sender: SyncSender, failure_coordinator: &HostAssociationFailureCoordinator, ) { @@ -241,7 +222,7 @@ fn read_requests( fn run_worker( association: &BrokerHostAssociation<'_, Memory>, request_receiver: &Mutex>, - response_sink: &UnixStreamHostResponseSink, + response_sink: &UnixControlRingHostResponseSink, failure_coordinator: &HostAssociationFailureCoordinator, ) { loop { @@ -270,11 +251,11 @@ fn run_worker( struct HostAssociationFailureCoordinator { failed: AtomicBool, error: Mutex>, - shutdown: UnixStreamHostControlShutdown, + shutdown: UnixControlRingHostShutdown, } impl HostAssociationFailureCoordinator { - const fn new(shutdown: UnixStreamHostControlShutdown) -> Self { + const fn new(shutdown: UnixControlRingHostShutdown) -> Self { Self { failed: AtomicBool::new(false), error: Mutex::new(None), @@ -343,22 +324,22 @@ fn accept_runner_stream( mod tests { use super::*; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; - use litebox_broker_protocol::channel::{HostSetupChannel, LocalControlChannel}; + use litebox_broker_protocol::channel::{HostSetupChannel, LocalSetupChannel}; use litebox_broker_protocol::message::BrokerHandshakeResponse; - use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; + use litebox_broker_transport::unix_socket::UnixStreamLocalSetupChannel; use std::os::fd::AsFd; #[test] fn first_failure_is_preserved_and_unblocks_request_reading() { let (peer_stream, host_stream) = UnixStream::pair().unwrap(); - let mut local_channel = UnixStreamLocalControlChannel::from_connected(peer_stream); - let mut control_channel = UnixStreamHostControlChannel::from_accepted(host_stream); + let mut local_setup = UnixStreamLocalSetupChannel::from_connected(peer_stream); + let mut control_channel = UnixStreamHostSetupChannel::from_accepted(host_stream); control_channel .send_handshake_response(&BrokerHandshakeResponse::Negotiated { broker_protocol_version: BROKER_PROTOCOL_VERSION, }) .unwrap(); - local_channel.recv_handshake_response().unwrap().unwrap(); + local_setup.recv_handshake_response().unwrap().unwrap(); let local_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); let host_memory = MemfdSharedMemory::from_received_fd( local_memory.as_fd().try_clone_to_owned().unwrap(), @@ -367,13 +348,11 @@ mod tests { .unwrap(); let local_ring = ControlRing::new(local_memory).unwrap(); let host_ring = ControlRing::new(host_memory).unwrap(); - let local_activation = std::thread::spawn(move || { - let cancellation = local_channel.activate(local_ring, || {}).unwrap(); - (local_channel, cancellation) - }); - let (mut request_source, _response_sink, shutdown) = + let local_activation = + std::thread::spawn(move || local_setup.into_active(local_ring, || {}).unwrap()); + let (mut request_source, _response_sink, _notifications, shutdown) = control_channel.into_active(host_ring).unwrap(); - let (_local_channel, _cancellation) = local_activation.join().unwrap(); + let (_local_call, _local_notifications, _local_shutdown) = local_activation.join().unwrap(); let failure_coordinator = HostAssociationFailureCoordinator::new(shutdown); let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); let reader = std::thread::spawn(move || { diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index 424ce4d15a..393be1e740 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -6,8 +6,10 @@ use std::sync::Arc; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{ConnectionTermination, setup_connection}; -use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::channel::HostReceive; +use litebox_broker_local::{BrokerLocal, BrokerNotifications}; +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::channel::{HostNotificationChannel, HostReceive}; +use litebox_broker_protocol::message::{BrokerNotification, ReadinessNotification}; use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::shared_memory::{ SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, @@ -15,26 +17,29 @@ use litebox_broker_protocol::shared_memory::{ use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ - UnixStreamHostControlChannel, UnixStreamHostNotificationChannel, UnixStreamLocalControlChannel, + UnixStreamHostSetupChannel, UnixStreamLocalSetupChannel, }; #[test] -fn host_serves_control_requests_over_paired_userland_channels() { +fn host_serves_control_requests_and_notifications_over_shared_rings() { let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( ObjectRights::all(), )) .unwrap(); let (local_control, host_control) = UnixStream::pair().unwrap(); - let (_local_notification, host_notification) = UnixStream::pair().unwrap(); let host_shared_memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE).unwrap(); let host_shared_buffers = SharedBufferPool::new(host_shared_memory, SHARED_BUFFER_LAYOUT).unwrap(); let host_control_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); let host_control_ring = ControlRing::new(host_control_memory).unwrap(); + let notification = BrokerNotification::Readiness(ReadinessNotification { + handle: ObjectHandle(7), + readiness: ReadinessFlags::READ, + }); + let host_notification = notification.clone(); let host_thread = std::thread::spawn(move || { - let mut control = UnixStreamHostControlChannel::from_accepted(host_control); - let _notification = UnixStreamHostNotificationChannel::from_accepted(host_notification); + let mut control = UnixStreamHostSetupChannel::from_accepted(host_control); let association = setup_connection(&broker, &mut control, &host_shared_buffers, |channel| { channel.send_memfd(host_shared_buffers.memory(), None)?; @@ -42,8 +47,9 @@ fn host_serves_control_requests_over_paired_userland_channels() { }) .unwrap() .unwrap(); - let (mut request_source, response_sink, _shutdown) = + let (mut request_source, response_sink, mut notifications, _shutdown) = control.into_active(host_control_ring).unwrap(); + notifications.send_notification(&host_notification).unwrap(); loop { match request_source.recv_request().unwrap() { HostReceive::Message(request) => association @@ -57,22 +63,28 @@ fn host_serves_control_requests_over_paired_userland_channels() { } }); - let local = BrokerLocal::negotiate( - UnixStreamLocalControlChannel::from_connected(local_control), - |channel| { - let shared_memory = channel.receive_memfd(SHARED_BUFFER_POOL_SIZE, None)?; - let control_memory = channel.receive_memfd(CONTROL_RING_MEMORY_SIZE, None)?; + let (local, notification_channel) = BrokerLocal::negotiate( + UnixStreamLocalSetupChannel::from_connected(local_control), + |mut setup| { + let shared_memory = setup.receive_memfd(SHARED_BUFFER_POOL_SIZE, None)?; + let control_memory = setup.receive_memfd(CONTROL_RING_MEMORY_SIZE, None)?; let control_ring = ControlRing::new(control_memory).map_err(|error| { std::io::Error::new( std::io::ErrorKind::InvalidData, format!("invalid test control ring: {error:?}"), ) })?; - let _cancellation = channel.activate(control_ring, || {})?; - Ok(Arc::new(shared_memory)) + let (call_channel, notifications, _shutdown) = + setup.into_active(control_ring, || {})?; + Ok((call_channel, Arc::new(shared_memory), notifications)) }, ) .unwrap(); + let mut notifications = BrokerNotifications::new(notification_channel); + assert_eq!( + notifications.recv_notification().unwrap(), + Some(notification) + ); let handle = local.create_event_with_count(0).unwrap(); let readiness = ReadinessFlags::READ | ReadinessFlags::WRITE; diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 4434d73aa6..f034a2a9cb 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -14,9 +14,7 @@ use litebox_broker_protocol::shared_memory::{ SHARED_BUFFER_POOL_SIZE, SharedBufferDescriptor, SharedBufferSlotIndex, }; use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; -use litebox_broker_transport::unix_socket::{ - UnixStreamLocalControlChannel, UnixStreamLocalNotificationChannel, -}; +use litebox_broker_transport::unix_socket::UnixStreamLocalSetupChannel; const RUNNER_ARGUMENT: &str = "broker-userland-test-runner"; @@ -36,8 +34,8 @@ fn run_parent_test() { // This custom-harness integration test uses its own executable as the broker's // runner. Cargo starts this executable without broker args, so it runs the // parent path here. The broker then starts the same executable with the real - // runner argv (`--unstable --broker-control-socket - // --broker-notification-socket `), which runs `run_fake_runner`. After + // runner argv (`--unstable --broker-control-socket `), which runs + // `run_fake_runner`. After // the fake runner finishes its broker requests, it terminates the broker // parent process; this lets the test exercise the long-running broker // without a test-only shutdown path. @@ -72,40 +70,32 @@ fn run_fake_runner(args: &[OsString]) { ); assert_eq!( args.get(3).map(OsString::as_os_str), - Some(OsStr::new("--broker-notification-socket")) - ); - assert_eq!( - args.get(5).map(OsString::as_os_str), Some(OsStr::new(RUNNER_ARGUMENT)) ); - assert_eq!(args.len(), 6, "unexpected runner arguments: {args:?}"); + assert_eq!(args.len(), 4, "unexpected runner arguments: {args:?}"); let control_socket_path = args.get(2).unwrap(); - let notification_socket_path = args.get(4).unwrap(); - let control_channel = connect_control_with_retry(Path::new(control_socket_path)).unwrap(); - let _notification_channel = - connect_notification_with_retry(Path::new(notification_socket_path)).unwrap(); - let local = Arc::new( - BrokerLocal::negotiate(control_channel, |channel| { - let shared_memory = channel.receive_memfd( - SHARED_BUFFER_POOL_SIZE, - Some(Instant::now() + Duration::from_secs(5)), - )?; - let control_memory = channel.receive_memfd( - CONTROL_RING_MEMORY_SIZE, - Some(Instant::now() + Duration::from_secs(5)), - )?; - let control_ring = ControlRing::new(control_memory).map_err(|error| { - std::io::Error::new( - ErrorKind::InvalidData, - format!("invalid test control ring: {error:?}"), - ) - })?; - let _cancellation = channel.activate(control_ring, || {})?; - Ok(Arc::new(shared_memory)) - }) - .unwrap(), - ); + let setup_channel = connect_control_with_retry(Path::new(control_socket_path)).unwrap(); + let (local, ()) = BrokerLocal::negotiate(setup_channel, |mut setup| { + let shared_memory = setup.receive_memfd( + SHARED_BUFFER_POOL_SIZE, + Some(Instant::now() + Duration::from_secs(5)), + )?; + let control_memory = setup.receive_memfd( + CONTROL_RING_MEMORY_SIZE, + Some(Instant::now() + Duration::from_secs(5)), + )?; + let control_ring = ControlRing::new(control_memory).map_err(|error| { + std::io::Error::new( + ErrorKind::InvalidData, + format!("invalid test control ring: {error:?}"), + ) + })?; + let (call_channel, _notifications, _shutdown) = setup.into_active(control_ring, || {})?; + Ok((call_channel, Arc::new(shared_memory), ())) + }) + .unwrap(); + let local = Arc::new(local); let start = Arc::new(std::sync::Barrier::new(17)); let callers = (0..16) @@ -181,30 +171,10 @@ impl Drop for ChildGuard { } } -fn connect_control_with_retry(socket_path: &Path) -> Result { - let deadline = Instant::now() + Duration::from_secs(5); - loop { - match UnixStreamLocalControlChannel::connect_with_setup_deadline(socket_path, deadline) { - Ok(channel) => return Ok(channel), - Err(error) if Instant::now() < deadline => { - if error.kind() != ErrorKind::NotFound - && error.kind() != ErrorKind::ConnectionRefused - { - return Err(error); - } - std::thread::sleep(Duration::from_millis(10)); - } - Err(error) => return Err(error), - } - } -} - -fn connect_notification_with_retry( - socket_path: &Path, -) -> Result { +fn connect_control_with_retry(socket_path: &Path) -> Result { let deadline = Instant::now() + Duration::from_secs(5); loop { - match UnixStreamLocalNotificationChannel::connect(socket_path) { + match UnixStreamLocalSetupChannel::connect_with_setup_deadline(socket_path, deadline) { Ok(channel) => return Ok(channel), Err(error) if Instant::now() < deadline => { if error.kind() != ErrorKind::NotFound diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 786f0e7d95..35d5370f47 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -16,8 +16,8 @@ use litebox_broker_protocol::message::BrokerNotification; use litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; use litebox_broker_transport::unix_socket::{ - UnixStreamLocalControlCancellation, UnixStreamLocalControlChannel, - UnixStreamLocalNotificationCancellation, UnixStreamLocalNotificationChannel, + UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, + UnixControlRingLocalShutdown, UnixStreamLocalSetupChannel, }; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -25,18 +25,17 @@ const RETRY_DELAY: Duration = Duration::from_millis(20); pub(crate) fn connect( control_socket_path: &Path, - notification_socket_path: &Path, ) -> Result<( - BrokerLocal, - BrokerNotifications, + BrokerLocal, + BrokerNotifications, Arc, )> { let setup_deadline = Instant::now() + SETUP_TIMEOUT; - let control_channel = connect_with_retry( + let setup_channel = connect_with_retry( control_socket_path, setup_deadline, "timed out connecting to broker", - |path, deadline| UnixStreamLocalControlChannel::connect_with_setup_deadline(path, deadline), + |path, deadline| UnixStreamLocalSetupChannel::connect_with_setup_deadline(path, deadline), ) .with_context(|| { format!( @@ -44,47 +43,25 @@ pub(crate) fn connect( control_socket_path.display() ) })?; - let notification_channel = connect_with_retry( - notification_socket_path, - setup_deadline, - "timed out connecting to broker notifications", - |path, _deadline| UnixStreamLocalNotificationChannel::connect(path), - ) - .with_context(|| { - format!( - "failed to connect to broker notifications at {}", - notification_socket_path.display() - ) - })?; - let notification_cancellation_handle = notification_channel - .cancellation_handle() - .context("failed to create broker notification cancellation handle")?; - let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( - notification_cancellation_handle, - )); - let local = BrokerLocal::negotiate(control_channel, { - let association_coordinator = Arc::clone(&association_coordinator); - move |channel| { - let shared_memory = - channel.receive_memfd(SHARED_BUFFER_POOL_SIZE, Some(setup_deadline))?; - let control_memory = - channel.receive_memfd(CONTROL_RING_MEMORY_SIZE, Some(setup_deadline))?; - let control_ring = ControlRing::new(control_memory).map_err(|error| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("invalid broker control ring: {error:?}"), - ) - })?; - let weak_association_coordinator = Arc::downgrade(&association_coordinator); - let control_cancellation_handle = channel.activate(control_ring, move || { + let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new()); + let (local, notification_channel) = BrokerLocal::negotiate(setup_channel, |mut setup| { + let shared_memory = setup.receive_memfd(SHARED_BUFFER_POOL_SIZE, Some(setup_deadline))?; + let control_memory = setup.receive_memfd(CONTROL_RING_MEMORY_SIZE, Some(setup_deadline))?; + let control_ring = ControlRing::new(control_memory).map_err(|error| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid broker control ring: {error:?}"), + ) + })?; + let weak_association_coordinator = Arc::downgrade(&association_coordinator); + let (call_channel, notification_channel, association_shutdown) = + setup.into_active(control_ring, move || { if let Some(association_coordinator) = weak_association_coordinator.upgrade() { association_coordinator.report_failure(); } })?; - association_coordinator - .install_control_cancellation_handle(control_cancellation_handle)?; - Ok(Arc::new(shared_memory)) - } + association_coordinator.install_shutdown(association_shutdown)?; + Ok((call_channel, Arc::new(shared_memory), notification_channel)) }) .context("broker negotiation failed")?; Ok(( @@ -95,7 +72,7 @@ pub(crate) fn connect( } pub(crate) fn start_notification_receiver( - mut notifications: BrokerNotifications, + mut notifications: BrokerNotifications, association_coordinator: Arc, dispatch_notification: impl Fn(BrokerNotification) + Send + 'static, ) -> Result<()> { @@ -120,41 +97,36 @@ pub(crate) fn start_notification_receiver( pub(crate) struct BrokerAssociationFailureCoordinator { failed: AtomicBool, - control_cancellation_handle: Mutex>, - notification_cancellation_handle: UnixStreamLocalNotificationCancellation, + shutdown: Mutex>, dispatch_failure: Mutex>>, } impl BrokerAssociationFailureCoordinator { - fn new(notification_cancellation_handle: UnixStreamLocalNotificationCancellation) -> Self { + fn new() -> Self { Self { failed: AtomicBool::new(false), - control_cancellation_handle: Mutex::new(None), - notification_cancellation_handle, + shutdown: Mutex::new(None), dispatch_failure: Mutex::new(None), } } - fn install_control_cancellation_handle( - &self, - control_cancellation_handle: UnixStreamLocalControlCancellation, - ) -> std::io::Result<()> { + fn install_shutdown(&self, shutdown: UnixControlRingLocalShutdown) -> std::io::Result<()> { let mut installed = self - .control_cancellation_handle + .shutdown .lock() - .expect("broker control cancellation mutex poisoned"); + .expect("broker association shutdown mutex poisoned"); assert!( installed.is_none(), - "broker control cancellation already installed" + "broker association shutdown already installed" ); if self.failed.load(Ordering::Acquire) { - control_cancellation_handle.cancel()?; + shutdown.shutdown()?; return Err(std::io::Error::new( std::io::ErrorKind::ConnectionAborted, "broker association failed during activation", )); } - *installed = Some(control_cancellation_handle); + *installed = Some(shutdown); Ok(()) } @@ -179,17 +151,14 @@ impl BrokerAssociationFailureCoordinator { if self.failed.swap(true, Ordering::AcqRel) { return; } - if let Some(cancellation_handle) = self - .control_cancellation_handle + if let Some(shutdown_handle) = self + .shutdown .lock() - .expect("broker control cancellation mutex poisoned") + .expect("broker association shutdown mutex poisoned") .as_ref() - && let Err(error) = cancellation_handle.cancel() + && let Err(error) = shutdown_handle.shutdown() { - eprintln!("failed to cancel broker control channel: {error}"); - } - if let Err(error) = self.notification_cancellation_handle.cancel() { - eprintln!("failed to cancel broker notification channel: {error}"); + eprintln!("failed to shut down broker association: {error}"); } let dispatch_failure = self .dispatch_failure @@ -225,15 +194,20 @@ fn connect_with_retry( #[cfg(test)] mod tests { use super::*; - use litebox_broker_protocol::channel::{HostReceive, HostSetupChannel, LocalControlChannel}; - use litebox_broker_protocol::message::{BrokerOperation, BrokerRequest}; - use litebox_broker_protocol::{ObjectHandle, RequestId}; + use litebox_broker_protocol::ObjectHandle; + use litebox_broker_protocol::channel::{ + HostNotificationChannel, HostReceive, HostSetupChannel, LocalSetupChannel, + }; + use litebox_broker_protocol::message::{BrokerNotification, ReadinessNotification}; + use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ - UnixStreamHostControlChannel, UnixStreamHostControlShutdown, UnixStreamHostRequestSource, - UnixStreamHostResponseSink, + UnixControlRingHostNotificationChannel, UnixControlRingHostRequestSource, + UnixControlRingHostResponseSink, UnixControlRingHostShutdown, + UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, + UnixControlRingLocalShutdown, UnixStreamHostSetupChannel, UnixStreamLocalSetupChannel, }; - use std::io::{ErrorKind, Read}; + use std::io::ErrorKind; use std::os::fd::AsFd; use std::os::unix::net::UnixStream; use std::sync::mpsc; @@ -241,9 +215,9 @@ mod tests { fn negotiate_control_pair( local_stream: UnixStream, host_stream: UnixStream, - ) -> (UnixStreamLocalControlChannel, UnixStreamHostControlChannel) { - let mut local = UnixStreamLocalControlChannel::from_connected(local_stream); - let mut host = UnixStreamHostControlChannel::from_accepted(host_stream); + ) -> (UnixStreamLocalSetupChannel, UnixStreamHostSetupChannel) { + let mut local = UnixStreamLocalSetupChannel::from_connected(local_stream); + let mut host = UnixStreamHostSetupChannel::from_accepted(host_stream); let request = litebox_broker_protocol::message::BrokerHandshakeRequest { protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, }; @@ -266,14 +240,17 @@ mod tests { } fn activate_control_channel( - channel: &mut UnixStreamLocalControlChannel, - host_channel: UnixStreamHostControlChannel, + setup: UnixStreamLocalSetupChannel, + host_channel: UnixStreamHostSetupChannel, association_coordinator: &Arc, ) -> ( - UnixStreamLocalControlCancellation, - UnixStreamHostRequestSource, - UnixStreamHostResponseSink, - UnixStreamHostControlShutdown, + UnixControlRingLocalCallChannel, + UnixControlRingLocalShutdown, + UnixControlRingLocalNotificationChannel, + UnixControlRingHostRequestSource, + UnixControlRingHostResponseSink, + UnixControlRingHostNotificationChannel, + UnixControlRingHostShutdown, ) { let local_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); let host_memory = MemfdSharedMemory::from_received_fd( @@ -286,38 +263,51 @@ mod tests { let host_activation = std::thread::spawn(move || host_channel.into_active(host_ring).unwrap()); let weak_association_coordinator = Arc::downgrade(association_coordinator); - let cancellation = channel - .activate(local_ring, move || { + let (local_call, local_notifications, local_shutdown) = setup + .into_active(local_ring, move || { if let Some(association_coordinator) = weak_association_coordinator.upgrade() { association_coordinator.report_failure(); } }) .unwrap(); - let (request_source, response_sink, shutdown) = host_activation.join().unwrap(); - (cancellation, request_source, response_sink, shutdown) + let (request_source, response_sink, host_notifications, host_shutdown) = + host_activation.join().unwrap(); + ( + local_call, + local_shutdown, + local_notifications, + request_source, + response_sink, + host_notifications, + host_shutdown, + ) } #[test] fn control_failure_cancels_notifications() { let (local_control, host_control) = UnixStream::pair().unwrap(); - let (mut active_channel, host_control) = - negotiate_control_pair(local_control, host_control); - let (local_notification, mut host_notification) = UnixStream::pair().unwrap(); - host_notification - .set_read_timeout(Some(Duration::from_secs(1))) - .unwrap(); - let notification_channel = - UnixStreamLocalNotificationChannel::from_connected(local_notification); - let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( - notification_channel.cancellation_handle().unwrap(), - )); - let (control_cancellation_handle, host_request_source, host_response_sink, host_shutdown) = - activate_control_channel(&mut active_channel, host_control, &association_coordinator); + let (local_setup, host_control) = negotiate_control_pair(local_control, host_control); + let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new()); + let ( + active_channel, + association_shutdown, + notification_channel, + host_request_source, + host_response_sink, + _host_notifications, + host_shutdown, + ) = activate_control_channel(local_setup, host_control, &association_coordinator); association_coordinator - .install_control_cancellation_handle(control_cancellation_handle) + .install_shutdown(association_shutdown) .unwrap(); let (failure_sender, failure_receiver) = mpsc::sync_channel(1); association_coordinator.install_dispatch(move || failure_sender.send(()).unwrap()); + start_notification_receiver( + BrokerNotifications::new(notification_channel), + Arc::clone(&association_coordinator), + |_| {}, + ) + .unwrap(); host_shutdown.shutdown().unwrap(); drop(host_request_source); @@ -326,41 +316,32 @@ mod tests { failure_receiver .recv_timeout(Duration::from_secs(1)) .unwrap(); - let mut byte = [0]; - assert_eq!(host_notification.read(&mut byte).unwrap(), 0); drop(active_channel); - drop(notification_channel); } #[test] - fn failure_before_installation_cancels_control_and_dispatches_failure() { + fn failure_before_installation_cancels_association_and_dispatches_failure() { let (local_control, host_control) = UnixStream::pair().unwrap(); host_control .set_read_timeout(Some(Duration::from_secs(1))) .unwrap(); - let (mut active_channel, host_control) = - negotiate_control_pair(local_control, host_control); - let (local_notification, mut host_notification) = UnixStream::pair().unwrap(); - host_notification - .set_read_timeout(Some(Duration::from_secs(1))) - .unwrap(); - let notification_channel = - UnixStreamLocalNotificationChannel::from_connected(local_notification); - let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( - notification_channel.cancellation_handle().unwrap(), - )); + let (local_setup, host_control) = negotiate_control_pair(local_control, host_control); + let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new()); let ( - control_cancellation_handle, + active_channel, + association_shutdown, + _notification_channel, mut host_request_source, _host_response_sink, + _host_notifications, _host_shutdown, - ) = activate_control_channel(&mut active_channel, host_control, &association_coordinator); + ) = activate_control_channel(local_setup, host_control, &association_coordinator); association_coordinator.report_failure(); assert_eq!( association_coordinator - .install_control_cancellation_handle(control_cancellation_handle) + .install_shutdown(association_shutdown) .unwrap_err() .kind(), ErrorKind::ConnectionAborted @@ -372,62 +353,48 @@ mod tests { host_request_source.recv_request().unwrap(), HostReceive::PeerClosed ); - let mut byte = [0]; - assert_eq!(host_notification.read(&mut byte).unwrap(), 0); drop(active_channel); - drop(notification_channel); } #[test] - fn notification_failure_cancels_control() { + fn notification_receiver_dispatches_ring_message() { let (local_control, host_control) = UnixStream::pair().unwrap(); - let (mut active_channel, host_control) = - negotiate_control_pair(local_control, host_control); - let (local_notification, host_notification) = UnixStream::pair().unwrap(); - let notification_channel = - UnixStreamLocalNotificationChannel::from_connected(local_notification); - let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new( - notification_channel.cancellation_handle().unwrap(), - )); + let (local_setup, host_control) = negotiate_control_pair(local_control, host_control); + let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new()); let ( - control_cancellation_handle, - mut host_request_source, + active_channel, + association_shutdown, + notification_channel, + _host_request_source, _host_response_sink, - _host_shutdown, - ) = activate_control_channel(&mut active_channel, host_control, &association_coordinator); + mut host_notifications, + host_shutdown, + ) = activate_control_channel(local_setup, host_control, &association_coordinator); association_coordinator - .install_control_cancellation_handle(control_cancellation_handle) + .install_shutdown(association_shutdown) .unwrap(); - let (failure_sender, failure_receiver) = mpsc::sync_channel(1); - association_coordinator.install_dispatch(move || failure_sender.send(()).unwrap()); + association_coordinator.install_dispatch(|| {}); + let (notification_sender, notification_receiver) = mpsc::sync_channel(1); start_notification_receiver( BrokerNotifications::new(notification_channel), Arc::clone(&association_coordinator), - |_| {}, + move |notification| notification_sender.send(notification).unwrap(), ) .unwrap(); - let active_channel = Arc::new(active_channel); - let pending_channel = Arc::clone(&active_channel); - let pending_call = std::thread::spawn(move || { - pending_channel.call(BrokerRequest { - request_id: RequestId(1), - operation: BrokerOperation::CloseObject(ObjectHandle(1)), - }) + let notification = BrokerNotification::Readiness(ReadinessNotification { + handle: ObjectHandle(7), + readiness: ReadinessFlags::READ, }); - assert!(matches!( - host_request_source.recv_request().unwrap(), - HostReceive::Message(_) - )); - drop(host_notification); + host_notifications.send_notification(¬ification).unwrap(); - failure_receiver - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - assert!(pending_call.join().unwrap().is_err()); assert_eq!( - host_request_source.recv_request().unwrap(), - HostReceive::PeerClosed + notification_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + notification ); + host_shutdown.shutdown().unwrap(); + drop(active_channel); } } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index f700c479ff..37bd14bf94 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -85,20 +85,10 @@ pub struct CliArgs { value_name = "PATH", value_hint = clap::ValueHint::FilePath, hide = true, - requires_all = ["unstable", "broker_notification_socket"], + requires = "unstable", help_heading = "Unstable Options" )] pub broker_control_socket: Option, - /// Broker-supplied Unix socket path for the local notification channel. - #[arg( - long = "broker-notification-socket", - value_name = "PATH", - value_hint = clap::ValueHint::FilePath, - hide = true, - requires_all = ["unstable", "broker_control_socket"], - help_heading = "Unstable Options" - )] - pub broker_notification_socket: Option, } struct MmappedFile { @@ -159,21 +149,9 @@ pub fn run(cli_args: CliArgs) -> Result<()> { ); } - let broker_connection = match ( - cli_args.broker_control_socket.as_deref(), - cli_args.broker_notification_socket.as_deref(), - ) { - (Some(control_socket_path), Some(notification_socket_path)) => Some(broker::connect( - control_socket_path, - notification_socket_path, - )?), - (None, None) => None, - (Some(_), None) => { - anyhow::bail!("broker notification socket is required with broker control socket") - } - (None, Some(_)) => { - anyhow::bail!("broker control socket is required with broker notification socket") - } + let broker_connection = match cli_args.broker_control_socket.as_deref() { + Some(control_socket_path) => Some(broker::connect(control_socket_path)?), + None => None, }; let mut cow_eligible_regions: Vec = Vec::new(); diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index c605800040..a97414d62c 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -119,17 +119,10 @@ impl Runner { } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] - fn broker_sockets( - &mut self, - control_socket_path: &Path, - notification_socket_path: &Path, - ) -> &mut Self { + fn broker_socket(&mut self, control_socket_path: &Path) -> &mut Self { self.command .arg("--broker-control-socket") .arg(control_socket_path); - self.command - .arg("--broker-notification-socket") - .arg(notification_socket_path); self } @@ -312,7 +305,6 @@ struct TestBroker { done_rx: std::sync::mpsc::Receiver<()>, close_object_count_rx: std::sync::mpsc::Receiver, control_socket_path: PathBuf, - notification_socket_path: PathBuf, } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] @@ -333,7 +325,6 @@ impl TestBroker { .join() .expect("broker test host panicked"); let _ = std::fs::remove_file(&self.control_socket_path); - let _ = std::fs::remove_file(&self.notification_socket_path); } } @@ -341,35 +332,27 @@ impl TestBroker { impl Drop for TestBroker { fn drop(&mut self) { let _ = std::fs::remove_file(&self.control_socket_path); - let _ = std::fs::remove_file(&self.notification_socket_path); } } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] fn spawn_test_broker( control_socket_path: &Path, - notification_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, connection_count: usize, ) -> TestBroker { let _ = std::fs::remove_file(control_socket_path); - let _ = std::fs::remove_file(notification_socket_path); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); let (done_tx, done_rx) = std::sync::mpsc::channel(); let (close_object_count_tx, close_object_count_rx) = std::sync::mpsc::channel(); let server_control_socket_path = control_socket_path.to_path_buf(); - let server_notification_socket_path = notification_socket_path.to_path_buf(); let cleanup_control_socket_path = control_socket_path.to_path_buf(); - let cleanup_notification_socket_path = notification_socket_path.to_path_buf(); let broker_thread = std::thread::spawn(move || { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let control_listener = std::os::unix::net::UnixListener::bind(&server_control_socket_path) .expect("failed to bind broker test control socket"); - let notification_listener = - std::os::unix::net::UnixListener::bind(&server_notification_socket_path) - .expect("failed to bind broker test notification socket"); let broker = litebox_broker_core::BrokerCore::new(policy).expect("failed to create broker core"); ready_tx.send(()).expect("failed to report broker ready"); @@ -396,33 +379,17 @@ fn spawn_test_broker( let control_ring = litebox_broker_transport::control_ring::ControlRing::new(control_memory) .expect("failed to attach broker test control ring"); - let (notification_stream, _) = notification_listener - .accept() - .expect("failed to accept broker local notification connection"); - litebox_broker_transport::unix_socket::validate_same_peer_process( - &control_stream, - ¬ification_stream, - ) - .expect("broker channels must belong to the same runner process"); control_stream .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test read timeout"); control_stream .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test write timeout"); - notification_stream - .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) - .expect("failed to configure broker notification test read timeout"); - notification_stream - .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) - .expect("failed to configure broker notification test write timeout"); let mut channel = - litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_host_guaranteed( + litebox_broker_transport::unix_socket::UnixStreamHostSetupChannel::from_host_guaranteed( control_stream, std::time::Instant::now() + BROKER_HELPER_TIMEOUT, ); - let _notification_channel = - litebox_broker_transport::unix_socket::UnixStreamHostNotificationChannel::from_accepted(notification_stream); let association = litebox_broker_host::setup_connection( &broker, &mut channel, @@ -434,7 +401,7 @@ fn spawn_test_broker( ) .expect("broker host setup failed") .expect("broker setup terminated before activation"); - let (mut request_source, response_sink, _shutdown) = channel + let (mut request_source, response_sink, _notifications, _shutdown) = channel .into_active(control_ring) .expect("failed to activate broker test control ring"); let mut close_object_count = 0; @@ -474,7 +441,6 @@ fn spawn_test_broker( } })); let _ = std::fs::remove_file(&server_control_socket_path); - let _ = std::fs::remove_file(&server_notification_socket_path); let _ = done_tx.send(()); if let Err(panic) = result { std::panic::resume_unwind(panic); @@ -489,7 +455,6 @@ fn spawn_test_broker( done_rx, close_object_count_rx, control_socket_path: cleanup_control_socket_path, - notification_socket_path: cleanup_notification_socket_path, } } @@ -513,10 +478,8 @@ console.log(content); false, ); let control_socket_path = unique_test_socket_path("runner-broker-control"); - let notification_socket_path = unique_test_socket_path("runner-broker-notification"); let broker_thread = spawn_test_broker( &control_socket_path, - ¬ification_socket_path, litebox_broker_core::PolicyEngine::with_host_guaranteed_rights( litebox_broker_core::ObjectRights::all(), ), @@ -524,24 +487,24 @@ console.log(content); ); Runner::new(&true_path, "broker_true_rewriter") - .broker_sockets(&control_socket_path, ¬ification_socket_path) + .broker_socket(&control_socket_path) .run(); assert_eq!(broker_thread.next_close_object_count(), 0); Runner::new(&target, "broker_eventfd_rewriter") - .broker_sockets(&control_socket_path, ¬ification_socket_path) + .broker_socket(&control_socket_path) .run(); // eventfd.c creates thirteen eventfd objects; each should release one broker object. assert_eq!(broker_thread.next_close_object_count(), 13); Runner::new(&pipe_target, "broker_pipe_rewriter") - .broker_sockets(&control_socket_path, ¬ification_socket_path) + .broker_socket(&control_socket_path) .run(); // pipe_broker.c creates five pipes; each endpoint owns one broker object. assert_eq!(broker_thread.next_close_object_count(), 10); Runner::new(&node_path, "hello_node_broker_rewriter") - .broker_sockets(&control_socket_path, ¬ification_socket_path) + .broker_socket(&control_socket_path) .arg("/out/hello_world.js") .with_fs_path(|out_dir| { std::fs::write(out_dir.join("out/hello_world.js"), HELLO_WORLD_JS).unwrap(); From bcc8628fcc86aec6e407441a78a035cde19ce42b Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 24 Jul 2026 14:56:46 -0700 Subject: [PATCH 131/319] Implement synchronous NtWriteFile support (#1086) Add basic support for `NtWriteFile`. Now we can run a hello-world PE successfully. Remaining TODOs: - Implement asynchronous/APC completion. - Support byte-range lock keys. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1 - litebox_runner_windows_userland/Cargo.toml | 1 - litebox_runner_windows_userland/tests/run.rs | 142 +++++++++++-------- litebox_shim_windows/src/lib.rs | 24 ++++ litebox_shim_windows/src/syscalls/file.rs | 133 ++++++++++++++++- litebox_shim_windows/src/syscalls/mod.rs | 22 +++ 6 files changed, 258 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9453e19e07..adfa49ad25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1791,7 +1791,6 @@ dependencies = [ "clap", "litebox", "litebox_common_linux", - "litebox_common_windows", "litebox_platform_windows_userland", "litebox_shim_windows", "litebox_syscall_rewriter", diff --git a/litebox_runner_windows_userland/Cargo.toml b/litebox_runner_windows_userland/Cargo.toml index 8f6fcdc10d..8e13d4a347 100644 --- a/litebox_runner_windows_userland/Cargo.toml +++ b/litebox_runner_windows_userland/Cargo.toml @@ -14,7 +14,6 @@ litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } [dev-dependencies] -litebox_common_windows = { version = "0.1.0", path = "../litebox_common_windows" } litebox_syscall_rewriter = { version = "0.1.0", path = "../litebox_syscall_rewriter" } tar = "0.4" diff --git a/litebox_runner_windows_userland/tests/run.rs b/litebox_runner_windows_userland/tests/run.rs index d79f2b1018..68247df615 100644 --- a/litebox_runner_windows_userland/tests/run.rs +++ b/litebox_runner_windows_userland/tests/run.rs @@ -3,30 +3,20 @@ #![cfg(all(target_os = "windows", target_arch = "x86_64"))] +/// Runs a hello-world guest PE end to end. #[test] -fn loads_minimal_pe_without_imports() { - let test_dir = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("no_import"); +fn run_hello_world_pe() { + let test_dir = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("kernel32_import"); let _ = std::fs::remove_dir_all(&test_dir); std::fs::create_dir_all(&test_dir).unwrap(); - let pe_path = build_no_import_pe(&test_dir); + let pe_path = build_kernel32_import_pe(&test_dir); println!( - "Built rewritten no-import PE fixture at `{}`", + "Built rewritten kernel32-import PE fixture at `{}`", pe_path.display() ); - for dll_name in ["ntdll.dll", "kernel32.dll", "kernelbase.dll"] { - let dll_path = build_rewritten_system_dll(&test_dir, dll_name); - println!( - "Built rewritten {dll_name} fixture at `{}`", - dll_path.display() - ); - } - // ntdll's NLS init opens these locale tables before reaching the test's - // `NtTerminateProcess` syscall; copy them verbatim from the host. - for nls_name in ["c_1252.nls", "c_437.nls", "c_10000.nls", "locale.nls"] { - let nls_path = copy_host_system32_file(&test_dir, nls_name); - println!("Copied {nls_name} fixture at `{}`", nls_path.display()); - } - let tar_path = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("no_import.tar"); + stage_system_fixtures(&test_dir); + let tar_path = + std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("kernel32_import.tar"); create_tar_with_dir(&test_dir, &tar_path); let mut command = @@ -36,7 +26,7 @@ fn loads_minimal_pe_without_imports() { command.args([ "--initial-files", tar_path.to_str().unwrap(), - "/no_import.exe", + "/kernel32_import.exe", ]); println!("Running `{command:?}`"); let output = command @@ -44,29 +34,40 @@ fn loads_minimal_pe_without_imports() { .expect("failed to run litebox_runner_windows_userland"); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); - let reached_unsupported_syscall = stdout.contains("Unsupported Windows syscall") - || stderr.contains("Unsupported Windows syscall"); assert!( - output.status.success() || reached_unsupported_syscall, - "runner failed to load no-import PE; status {:?}\nstdout:\n{}\nstderr:\n{}", + output.status.success(), + "runner failed to run kernel32-import PE; status {:?}\nstdout:\n{}\nstderr:\n{}", output.status.code(), stdout, stderr ); + assert!( + stdout.contains("hello world\n"), + "guest output was not captured\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); } -fn build_no_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { - let source_path = test_dir.join("no_import.rs"); - let raw_exe_path = test_dir.join("no_import.raw.exe"); - let exe_path = test_dir.join("no_import.exe"); - let syscall_number = litebox_common_windows::NtSysno::NtTerminateProcess.as_raw(); - println!("Using LiteBox NtTerminateProcess sysno `{syscall_number:#x}`"); - std::fs::write( - &source_path, - minimal_pe_with_nt_terminate_process_syscall_source(syscall_number), - ) - .unwrap(); +/// Stages the guest system DLLs and locale tables the PE fixture needs. +fn stage_system_fixtures(test_dir: &std::path::Path) { + for dll_name in ["ntdll.dll", "kernel32.dll", "kernelbase.dll"] { + let dll_path = build_rewritten_system_dll(test_dir, dll_name); + println!( + "Built rewritten {dll_name} fixture at `{}`", + dll_path.display() + ); + } + for nls_name in ["c_1252.nls", "c_437.nls", "c_10000.nls", "locale.nls"] { + let nls_path = copy_host_system32_file(test_dir, nls_name); + println!("Copied {nls_name} fixture at `{}`", nls_path.display()); + } +} + +fn build_kernel32_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { + let source_path = test_dir.join("kernel32_import.rs"); + let raw_exe_path = test_dir.join("kernel32_import.raw.exe"); + let exe_path = test_dir.join("kernel32_import.exe"); + std::fs::write(&source_path, KERNEL32_IMPORT_PE_SOURCE).unwrap(); let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); let output = std::process::Command::new(rustc) @@ -76,6 +77,10 @@ fn build_no_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { "-C", "panic=abort", "-C", + "opt-level=1", + "-l", + "dylib=kernel32", + "-C", "link-arg=/ENTRY:mainCRTStartup", "-C", "link-arg=/SUBSYSTEM:CONSOLE", @@ -85,51 +90,66 @@ fn build_no_import_pe(test_dir: &std::path::Path) -> std::path::PathBuf { raw_exe_path.to_str().unwrap(), ]) .output() - .expect("failed to run rustc for the no-import Windows PE fixture"); + .expect("failed to run rustc for the kernel32-import Windows PE fixture"); assert!( output.status.success(), - "failed to build no-import Windows PE fixture\nstdout:\n{}\nstderr:\n{}", + "failed to build kernel32-import Windows PE fixture\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let rewritten = - litebox_syscall_rewriter::rewrite_binary(&std::fs::read(raw_exe_path).unwrap(), None) - .expect("failed to rewrite no-import Windows PE fixture"); + litebox_syscall_rewriter::rewrite_binary(&std::fs::read(&raw_exe_path).unwrap(), None) + .expect("failed to rewrite kernel32-import Windows PE fixture"); std::fs::write(&exe_path, rewritten).unwrap(); + // Keep the unrewritten build out of the fixture tar. + std::fs::remove_file(&raw_exe_path).unwrap(); exe_path } -fn minimal_pe_with_nt_terminate_process_syscall_source(syscall_number: u32) -> String { - format!( - r#" +/// `STD_OUTPUT_HANDLE` is `(DWORD)-11`. +const KERNEL32_IMPORT_PE_SOURCE: &str = r#" #![no_std] #![no_main] +#[link(name = "kernel32")] +unsafe extern "system" { + fn GetStdHandle(std_handle: u32) -> usize; + fn WriteFile( + file: usize, + buffer: *const u8, + length: u32, + written: *mut u32, + overlapped: usize, + ) -> i32; + fn ExitProcess(exit_code: u32) -> !; +} + #[unsafe(no_mangle)] -pub unsafe extern "system" fn mainCRTStartup() -> ! {{ - unsafe {{ - core::arch::asm!( - "mov rcx, -1", - "xor edx, edx", - "mov r10, rcx", - "mov eax, {syscall_number:#x}", - "syscall", - options(noreturn), +pub unsafe extern "system" fn mainCRTStartup() -> ! { + unsafe { + let MESSAGE: &[u8] = b"hello world\n"; + let stdout = GetStdHandle(0xffff_fff5); + let mut written = 0u32; + let ok = WriteFile( + stdout, + MESSAGE.as_ptr(), + MESSAGE.len() as u32, + &raw mut written, + 0, ); - }} -}} + ExitProcess(u32::from(ok == 0 || written as usize != MESSAGE.len())); + } +} #[panic_handler] -fn panic(_info: &core::panic::PanicInfo<'_>) -> ! {{ - loop {{ +fn panic(_info: &core::panic::PanicInfo<'_>) -> ! { + loop { core::hint::spin_loop(); - }} -}} -"# - ) + } } +"#; fn build_rewritten_system_dll(test_dir: &std::path::Path, dll_name: &str) -> std::path::PathBuf { let dll_path = fixture_system32_path(test_dir, dll_name); @@ -170,13 +190,13 @@ fn host_system32_file_path(file_name: &str) -> std::path::PathBuf { } fn create_tar_with_dir(test_dir: &std::path::Path, tar_path: &std::path::Path) { - let output_file = std::fs::File::create(tar_path) - .expect("failed to create tar for the no-import Windows PE fixture"); + let output_file = + std::fs::File::create(tar_path).expect("failed to create tar for the Windows PE fixture"); let mut builder = tar::Builder::new(output_file); append_regular_files_to_ustar(&mut builder, test_dir, test_dir); builder .finish() - .expect("failed to finalize tar for the no-import Windows PE fixture"); + .expect("failed to finalize tar for the Windows PE fixture"); } fn append_regular_files_to_ustar( diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index dbcbfdab40..6178538e16 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -1297,6 +1297,30 @@ impl Task { ); (status, ContinueOperation::Resume) } + SyscallRequest::NtWriteFile { + file_handle, + event, + apc_routine, + apc_context, + io_status_block, + buffer, + length, + byte_offset, + key, + } => { + let status = self.sys_nt_write_file( + file_handle, + event, + apc_routine, + apc_context, + io_status_block, + buffer, + length, + byte_offset, + key, + ); + (status, ContinueOperation::Resume) + } SyscallRequest::NtQueryVolumeInformationFile { file_handle, io_status_block, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 237ba6b333..bdbd5b036a 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -8,8 +8,8 @@ use core::mem::size_of; use int_enum::IntEnum; use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}; -use litebox::fs::errors::{FileStatusError, MkdirError, OpenError, PathError}; -use litebox::fs::{FileType, Mode, OFlags}; +use litebox::fs::errors::{FileStatusError, MkdirError, OpenError, PathError, WriteError}; +use litebox::fs::{FileType, Mode, OFlags, SeekWhence}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -30,6 +30,12 @@ const FILE_SHARE_READ: u32 = 0x0000_0001; const FILE_SHARE_WRITE: u32 = 0x0000_0002; const FILE_SHARE_DELETE: u32 = 0x0000_0004; +/// Append at the current end of file +const FILE_WRITE_TO_END_OF_FILE: i64 = -1; + +/// Use the file object's current position +const FILE_USE_FILE_POINTER_POSITION: i64 = -2; + // These names and values are Windows ABI constants from WDK headers; Wine's // regular file/directory branch and ReactOS' filesystem device query path use // the same FILE_DEVICE_* and FILE_DEVICE_IS_MOUNTED vocabulary. @@ -534,6 +540,129 @@ impl Task { }) } + #[expect( + clippy::too_many_arguments, + reason = "NtWriteFile has nine ABI parameters; keeping them explicit preserves syscall ordering" + )] + pub(crate) fn sys_nt_write_file( + &self, + file_handle: Handle, + event: Handle, + apc_routine: Option>, + apc_context: Option>, + io_status_block: MutPtr, + buffer: ConstPtr, + length: u32, + byte_offset: Option>, + key: Option>, + ) -> NtStatus { + if probe_guest_output_preserving_value::(io_status_block).is_err() + { + return NtStatus::ACCESS_VIOLATION; + } + let Some(buffer) = buffer.to_owned_slice(length as usize) else { + return NtStatus::ACCESS_VIOLATION; + }; + if !event.is_null() + && let Err(status) = self.check_event_modify_access(event) + { + return status; + } + let offset = match byte_offset { + Some(byte_offset) => match byte_offset.read_at_offset(0) { + Some(FILE_USE_FILE_POINTER_POSITION) => None, + Some(FILE_WRITE_TO_END_OF_FILE) => { + let file = match self.file_entry(file_handle) { + Ok(file) => file, + Err(status) => return status, + }; + match file.with_entry(|file| self.fs.file_status(&file.path)) { + Ok(status) => Some(status.size), + Err(error) => return map_file_status_error(error), + } + } + Some(offset) if offset >= 0 => match usize::try_from(offset) { + Ok(offset) => Some(offset), + Err(_) => return NtStatus::INVALID_PARAMETER, + }, + Some(_) => return NtStatus::INVALID_PARAMETER, + None => return NtStatus::ACCESS_VIOLATION, + }, + None => None, + }; + if let Some(key) = key { + let Some(key) = key.read_at_offset(0) else { + return NtStatus::ACCESS_VIOLATION; + }; + litebox_util_log::debug!( + file_handle = file_handle.as_raw(), + key = key; + "Ignoring NtWriteFile byte-range lock key; byte-range locking is not supported yet" + ); + } + + let file = match self.file_entry(file_handle) { + Ok(file) => file, + Err(status) => return status, + }; + if !event.is_null() + && let Err(status) = self.clear_event(event) + { + return status; + } + if apc_routine.is_some() || apc_context.is_some() { + litebox_util_log::debug!( + file_handle = file_handle.as_raw(); + "Ignoring NtWriteFile APC completion arguments for synchronous completion" + ); + } + let result = file.with_entry(|file| match &file.backing { + FileObjectBacking::Filesystem { fd, is_directory } => { + if *is_directory { + return Err(WriteError::NotAFile); + } + let written = self.fs.write(fd, &buffer, offset)?; + // A positional write leaves the backing file offset untouched, but NT advances a + // synchronous file object's position past the end of every write, including + // explicit-offset and append writes. Asynchronous handles keep their position. + if let Some(offset) = offset + && file + .create_options + .intersects(FileCreateOptions::SYNCHRONOUS_IO) + { + let _ = self.fs.seek( + fd, + (offset + written).cast_signed(), + SeekWhence::RelativeToBeginning, + ); + } + Ok(written) + } + FileObjectBacking::CondrvStream { fd, .. } => self.fs.write(fd, &buffer, offset), + FileObjectBacking::CondrvControl(_) => Err(WriteError::NotAFile), + }); + let (status, information) = match result { + Ok(written) => (NtStatus::SUCCESS, written), + Err(WriteError::ClosedFd) => (NtStatus::INVALID_HANDLE, 0), + Err(WriteError::NotForWriting) => (NtStatus::ACCESS_DENIED, 0), + Err(WriteError::NotAFile) => (NtStatus::INVALID_DEVICE_REQUEST, 0), + Err(_) => (NtStatus::UNSUCCESSFUL, 0), + }; + if io_status_block + .write_at_offset(0, IoStatusBlock::new(status, information)) + .is_none() + { + return NtStatus::ACCESS_VIOLATION; + } + if !event.is_null() { + let event_status = self.set_event(event); + if event_status != NtStatus::SUCCESS { + return event_status; + } + } + status + } + pub(crate) fn sys_nt_query_volume_information_file( &self, file_handle: Handle, diff --git a/litebox_shim_windows/src/syscalls/mod.rs b/litebox_shim_windows/src/syscalls/mod.rs index bf850e9f32..a78a929fcd 100644 --- a/litebox_shim_windows/src/syscalls/mod.rs +++ b/litebox_shim_windows/src/syscalls/mod.rs @@ -333,6 +333,17 @@ pub(crate) enum SyscallRequest { ea_buffer: Option>, ea_length: u32, }, + NtWriteFile { + file_handle: Handle, + event: Handle, + apc_routine: Option>, + apc_context: Option>, + io_status_block: Platform::RawMutPointer, + buffer: Platform::RawConstPointer, + length: u32, + byte_offset: Option>, + key: Option>, + }, NtQueryVolumeInformationFile { file_handle: Handle, io_status_block: Platform::RawMutPointer, @@ -849,6 +860,17 @@ impl SyscallRequest { ea_buffer:*, ea_length, })), + NtSysno::NtWriteFile => Some(sys_req!(NtWriteFile { + file_handle:{Handle::from_raw}, + event:{Handle::from_raw}, + apc_routine:*, + apc_context:*, + io_status_block:*, + buffer:*, + length, + byte_offset:*, + key:*, + })), NtSysno::NtQueryVolumeInformationFile => Some(sys_req!(NtQueryVolumeInformationFile { file_handle:{Handle::from_raw}, io_status_block:*, From f3a40f74deff610dbfc708f8f9ea5e21117cafcc Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 25 Jul 2026 14:39:53 -0700 Subject: [PATCH 132/319] Publish broker readiness notifications (#1085) The association notification ring has been wired end to end since broker notifications moved to shared memory, but nothing published on it, so readiness that no local request causes had no way to reach a local waiter. This adds bounded coalescing readiness publication to the portable host adapter: sources record the authoritative flags of an object without ever waiting for notification-ring capacity, and one dedicated publisher owns the ring producer, so only that publisher can block when a local endpoint stops draining. The userland broker host supplies the wake primitive and runs that publisher on its own thread per association. No production source publishes yet. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- Cargo.lock | 1 + litebox_broker_host/Cargo.toml | 1 + litebox_broker_host/src/lib.rs | 1 + litebox_broker_host/src/readiness.rs | 851 ++++++++++++++++++ litebox_broker_userland/src/lib.rs | 10 + litebox_broker_userland/src/main.rs | 519 ++++++++++- litebox_broker_userland/src/readiness.rs | 318 +++++++ .../tests/notification_runtime.rs | 255 +++++- 8 files changed, 1907 insertions(+), 49 deletions(-) create mode 100644 litebox_broker_host/src/readiness.rs create mode 100644 litebox_broker_userland/src/lib.rs create mode 100644 litebox_broker_userland/src/readiness.rs diff --git a/Cargo.lock b/Cargo.lock index adfa49ad25..0d93506717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1470,6 +1470,7 @@ dependencies = [ name = "litebox_broker_host" version = "0.1.0" dependencies = [ + "hashbrown", "litebox_broker_core", "litebox_broker_protocol", "spin 0.9.8", diff --git a/litebox_broker_host/Cargo.toml b/litebox_broker_host/Cargo.toml index 03a7c344ae..b046ec1acf 100644 --- a/litebox_broker_host/Cargo.toml +++ b/litebox_broker_host/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +hashbrown = "0.15.2" litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } spin = { version = "0.9.8", default-features = false, features = ["spin_mutex"] } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index e5029fc712..48c8c7f8c9 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -35,6 +35,7 @@ use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, RequestId}; use spin::mutex::SpinMutex; mod error; +pub mod readiness; pub use error::{BrokerHostError, Result}; diff --git a/litebox_broker_host/src/readiness.rs b/litebox_broker_host/src/readiness.rs new file mode 100644 index 0000000000..0b2aa0269a --- /dev/null +++ b/litebox_broker_host/src/readiness.rs @@ -0,0 +1,851 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Bounded, coalescing readiness publication state for one broker association. +//! +//! Backend readiness sources and the notification transport have very different +//! blocking behavior. A source discovers a readiness change while holding +//! backend state and must never wait for notification-ring capacity; the +//! notification ring is a bounded shared region whose producer blocks when the +//! local endpoint stops draining it. +//! +//! [`ReadinessPublisher`] separates the two. Sources call [`publish`] to record +//! the authoritative flags for an object, which only updates in-memory state. +//! [`publish_readiness`] owns the notification channel and repeatedly claims +//! pending updates, sends them, and reports completion. +//! +//! Because [`BrokerNotification::Readiness`] is a hint to re-check state rather +//! than an ordered transition, repeated updates for one handle collapse into a +//! single notification carrying the newest flags. Updates that arrive while a +//! notification is in flight are not lost: each entry carries a generation that +//! identifies its newest change, and a claim is confirmed only while the +//! generation it was taken from is still current. +//! +//! This type is transport-neutral and performs no transport I/O, so it holds no +//! thread, timer, or wake primitive and never waits for notification-ring +//! capacity. Deployments own those and wake their publisher whenever +//! [`publish`] reports [`PublishOutcome::Queued`]. +//! +//! [`publish`]: ReadinessPublisher::publish +//! [`BrokerNotification::Readiness`]: litebox_broker_protocol::message::BrokerNotification::Readiness + +use alloc::collections::VecDeque; + +use hashbrown::HashMap; +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::channel::HostNotificationChannel; +use litebox_broker_protocol::message::{BrokerNotification, ReadinessNotification}; +use litebox_broker_protocol::readiness::ReadinessFlags; +use spin::mutex::SpinMutex; +use thiserror::Error; + +/// Maximum number of objects one association tracks readiness for. +/// +/// This matches the default broker-core reference limit, so a source that +/// retires an object as its backend resource is released stays well inside it. +/// It exists so that a source which does not, or a deployment that raises the +/// core limit, cannot grow publication state without limit. +pub const MAX_TRACKED_READINESS_OBJECTS: usize = 4096; + +/// Error returned when readiness state cannot record an update. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] +#[non_exhaustive] +pub enum ReadinessPublishError { + /// The association already tracks [`MAX_TRACKED_READINESS_OBJECTS`] objects. + #[error("broker association already tracks the maximum number of readiness objects")] + TooManyObjects, +} + +/// Outcome of recording one readiness update. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PublishOutcome { + /// The update queued work the publisher may not know about yet, so the + /// deployment must wake its publisher. + Queued, + /// The update needs no wake: it either matched the newest recorded state or + /// joined a notification that is already queued or in flight, which carries + /// the newest flags when it is published. + Coalesced, + /// The publisher is closed and the update was discarded. + Closed, +} + +/// One readiness notification claimed for publication. +/// +/// The claim borrows the publisher it came from, so it cannot be confirmed +/// against a different one, and it is not `Copy`, so it cannot be replayed. +/// Dropping it without calling [`confirm`] requeues the update it carried, so +/// a send that fails or unwinds leaves the object publishable again. The +/// requeue is skipped when the update is no longer the current one to send: +/// retirement, closure, and a newer recorded change all supersede it. +/// +/// [`confirm`]: Self::confirm +#[derive(Debug)] +pub(crate) struct PendingReadiness<'publisher> { + publisher: &'publisher ReadinessPublisher, + notification: ReadinessNotification, + generation: u64, +} + +impl PendingReadiness<'_> { + /// Notification to send on the association notification channel. + pub(crate) const fn notification(&self) -> ReadinessNotification { + self.notification + } + + /// Reports that the notification reached the notification channel. + /// + /// The pending mark is cleared only when the object still holds the + /// generation the claim was taken from. Any change recorded while the + /// notification was in flight leaves the object pending, including a change + /// that returned to the published flags, because the local endpoint samples + /// authoritative state independently and may have observed the intermediate + /// value. + pub(crate) fn confirm(self) { + let mut state = self.publisher.state.lock(); + if let Some(entry) = state.entries.get_mut(&self.notification.handle) + && entry.generation == self.generation + { + entry.dirty = false; + } + drop(state); + core::mem::forget(self); + } +} + +impl Drop for PendingReadiness<'_> { + fn drop(&mut self) { + let mut state = self.publisher.state.lock(); + // A newer change has already requeued the object, and a retired or + // closed publisher has nothing left to publish. The queued check is + // defensive rather than load bearing, so no test can distinguish it: a + // matching generation means no publication since this claim was taken, + // and only publication or a requeue can queue the object, so a matching + // generation already implies the object is unqueued. + if let Some(entry) = state.entries.get_mut(&self.notification.handle) + && entry.generation == self.generation + && !entry.queued + { + entry.queued = true; + state.queue.push_back(self.notification.handle); + } + } +} + +/// Coalescing readiness publication state shared by backend sources and one +/// notification publisher. +#[derive(Debug)] +pub struct ReadinessPublisher { + state: SpinMutex, +} + +#[derive(Debug)] +struct PublisherState { + entries: HashMap, + queue: VecDeque, + /// Generation to hand to the next recorded change. + /// + /// Generations are allocated publisher-wide rather than per object so that + /// an object re-registered under a recycled handle cannot reuse a value a + /// still-unconfirmed claim was taken from. Values are skipped freely; only + /// their distinctness matters, which holds unless the counter wraps all the + /// way back to a value a claim is still holding. That needs `2^64` recorded + /// changes to elapse while one send is in flight, so it is treated as + /// unreachable. + next_generation: u64, + closed: bool, +} + +#[derive(Debug)] +struct ReadinessEntry { + /// Newest authoritative flags recorded by a backend source. + readiness: ReadinessFlags, + /// Identifies the recorded change, and is replaced whenever one arrives. + /// + /// Confirmation compares this rather than the published flags because a + /// notification only tells the local endpoint to re-check; the endpoint + /// then samples authoritative state itself and may observe a value the + /// publisher never sent. Flags that change and return to the published + /// value are therefore still a change the endpoint must be told about. + generation: u64, + /// A change has not yet been confirmed as published. + dirty: bool, + /// The handle currently sits in `queue`. + queued: bool, +} + +impl Default for ReadinessPublisher { + fn default() -> Self { + Self::new() + } +} + +impl ReadinessPublisher { + /// Creates empty readiness publication state. + #[must_use] + pub fn new() -> Self { + Self { + state: SpinMutex::new(PublisherState { + entries: HashMap::new(), + queue: VecDeque::new(), + next_generation: 0, + closed: false, + }), + } + } + + /// Records the authoritative readiness of one object. + /// + /// This never waits for notification-ring capacity, so backend sources may + /// call it while holding their own state. The caller must wake the + /// publisher when the outcome is [`PublishOutcome::Queued`]. + pub fn publish( + &self, + handle: ObjectHandle, + readiness: ReadinessFlags, + ) -> Result { + let mut state = self.state.lock(); + if state.closed { + return Ok(PublishOutcome::Closed); + } + let generation = state.next_generation; + if let Some(entry) = state.entries.get_mut(&handle) { + if entry.readiness == readiness { + // Either the local endpoint already knows this value or a + // claim carrying it is queued or in flight. + return Ok(PublishOutcome::Coalesced); + } + entry.readiness = readiness; + entry.generation = generation; + entry.dirty = true; + let already_queued = entry.queued; + entry.queued = true; + state.next_generation = generation.wrapping_add(1); + if already_queued { + return Ok(PublishOutcome::Coalesced); + } + } else { + if state.entries.len() >= MAX_TRACKED_READINESS_OBJECTS { + return Err(ReadinessPublishError::TooManyObjects); + } + state.entries.insert( + handle, + ReadinessEntry { + readiness, + generation, + dirty: true, + queued: true, + }, + ); + state.next_generation = generation.wrapping_add(1); + } + state.queue.push_back(handle); + Ok(PublishOutcome::Queued) + } + + /// Claims the next pending readiness notification, if any. + /// + /// The claim leaves the object marked pending until it is confirmed, so a + /// change recorded while it is in flight is republished rather than lost. + /// A claim that is dropped unconfirmed requeues the object, so a failed or + /// unwinding send leaves the update publishable by a later publisher. + #[must_use] + pub(crate) fn take_pending(&self) -> Option> { + let mut state = self.state.lock(); + while let Some(handle) = state.queue.pop_front() { + let Some(entry) = state.entries.get_mut(&handle) else { + continue; + }; + entry.queued = false; + if !entry.dirty { + continue; + } + let notification = ReadinessNotification { + handle, + readiness: entry.readiness, + }; + let generation = entry.generation; + drop(state); + return Some(PendingReadiness { + publisher: self, + notification, + generation, + }); + } + None + } + + /// Drops readiness state for an object whose backend resource is retired. + /// + /// Notifications already claimed for the object stay valid to send. A + /// notification that arrives after retirement is ignored by the local + /// endpoint, or, if the handle has since been recycled, is treated as a + /// spurious hint to re-check the new object. + pub fn retire(&self, handle: ObjectHandle) { + let mut state = self.state.lock(); + if state.entries.remove(&handle).is_some() { + state.queue.retain(|queued| *queued != handle); + } + } + + /// Closes publication permanently and discards pending state. + /// + /// Later updates are discarded and no further claim is produced, so a + /// publisher woken during association teardown observes [`is_closed`] and + /// stops. + /// + /// [`is_closed`]: Self::is_closed + pub fn close(&self) { + let mut state = self.state.lock(); + state.closed = true; + state.entries.clear(); + state.queue.clear(); + } + + /// Reports whether publication is closed. + #[must_use] + pub fn is_closed(&self) -> bool { + self.state.lock().closed + } + + /// Number of objects with recorded readiness state. + #[must_use] + pub fn tracked_objects(&self) -> usize { + self.state.lock().entries.len() + } + + /// Number of queue slots holding work, which must never exceed the number + /// of tracked objects. + #[cfg(test)] + fn queued_updates(&self) -> usize { + self.state.lock().queue.len() + } +} + +/// Publishes coalesced readiness updates until publication closes. +/// +/// This is the single owner of `channel`. Sending may block when the local +/// endpoint stops draining the notification transport; backend sources calling +/// [`ReadinessPublisher::publish`] are unaffected because they never touch the +/// channel. +/// +/// `wait_for_work` parks the caller while nothing is pending. It must return +/// once [`ReadinessPublisher::publish`] reports [`PublishOutcome::Queued`] or +/// [`ReadinessPublisher::close`] runs, so association teardown always ends the +/// loop. Deployments that must also interrupt an in-progress send do so through +/// their transport, which fails the blocked send. +/// +/// Returns `Ok(())` once publication is closed and no claim is outstanding. A +/// failed send returns its error with the claimed update left publishable, so +/// resuming on a replacement channel does not lose the notification; the +/// requeue raises no wake of its own, so a deployment that resumes must call +/// this again rather than wait for one. +pub fn publish_readiness( + publisher: &ReadinessPublisher, + channel: &mut Channel, + mut wait_for_work: impl FnMut(), +) -> Result<(), Channel::Error> { + loop { + if let Some(pending) = publisher.take_pending() { + channel.send_notification(&BrokerNotification::Readiness(pending.notification()))?; + pending.confirm(); + continue; + } + if publisher.is_closed() { + return Ok(()); + } + wait_for_work(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Deadline for every test wait, so a regression fails instead of hanging. + const TEST_TIMEOUT: core::time::Duration = core::time::Duration::from_secs(10); + const HANDLE: ObjectHandle = ObjectHandle(7); + const OTHER_HANDLE: ObjectHandle = ObjectHandle(9); + + fn publish(publisher: &ReadinessPublisher, handle: ObjectHandle, readiness: ReadinessFlags) { + publisher.publish(handle, readiness).unwrap(); + } + + fn drain(publisher: &ReadinessPublisher) -> alloc::vec::Vec { + let mut drained = alloc::vec::Vec::new(); + while let Some(pending) = publisher.take_pending() { + drained.push(pending.notification()); + pending.confirm(); + } + drained + } + + #[test] + fn repeating_known_readiness_publishes_nothing() { + let publisher = ReadinessPublisher::new(); + assert_eq!( + publisher.publish(HANDLE, ReadinessFlags::READ).unwrap(), + PublishOutcome::Queued + ); + drain(&publisher); + + assert_eq!( + publisher.publish(HANDLE, ReadinessFlags::READ).unwrap(), + PublishOutcome::Coalesced + ); + + assert!(publisher.take_pending().is_none()); + } + + #[test] + fn queued_updates_collapse_to_the_newest_flags() { + let publisher = ReadinessPublisher::new(); + + publish(&publisher, HANDLE, ReadinessFlags::READ); + assert_eq!( + publisher + .publish(HANDLE, ReadinessFlags::READ | ReadinessFlags::WRITE) + .unwrap(), + PublishOutcome::Coalesced + ); + publish(&publisher, HANDLE, ReadinessFlags::HANGUP); + + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::HANGUP, + }] + ); + } + + #[test] + fn updates_during_publication_are_republished() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + + let pending = publisher.take_pending().unwrap(); + assert_eq!( + publisher.publish(HANDLE, ReadinessFlags::WRITE).unwrap(), + PublishOutcome::Queued + ); + pending.confirm(); + + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::WRITE, + }] + ); + } + + #[test] + fn readiness_that_returns_to_the_claimed_value_is_still_republished() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + let pending = publisher.take_pending().unwrap(); + + // The local endpoint re-checks authoritative state on its own after a + // notification, so it may sample the intermediate value. Returning to + // the claimed value is therefore still a change it must be told about, + // which is why confirmation compares generations and not flags. + publish(&publisher, HANDLE, ReadinessFlags::WRITE); + publish(&publisher, HANDLE, ReadinessFlags::READ); + pending.confirm(); + + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::READ, + }] + ); + } + + #[test] + fn retiring_an_object_drops_its_queued_update() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + publish(&publisher, OTHER_HANDLE, ReadinessFlags::WRITE); + + publisher.retire(HANDLE); + + assert_eq!(publisher.tracked_objects(), 1); + // The queued slot goes with the object; leaving it behind would let a + // local grow the queue without bound by cycling objects. + assert_eq!(publisher.queued_updates(), 1); + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: OTHER_HANDLE, + readiness: ReadinessFlags::WRITE, + }] + ); + } + + #[test] + fn confirming_a_retired_object_does_not_resurrect_it() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + let pending = publisher.take_pending().unwrap(); + + publisher.retire(HANDLE); + pending.confirm(); + + assert_eq!(publisher.tracked_objects(), 0); + assert!(publisher.take_pending().is_none()); + } + + #[test] + fn a_reused_handle_starts_from_unknown_readiness() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + drain(&publisher); + + publisher.retire(HANDLE); + + assert_eq!( + publisher.publish(HANDLE, ReadinessFlags::READ).unwrap(), + PublishOutcome::Queued + ); + } + + #[test] + fn confirming_a_stale_claim_keeps_the_new_update() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + let stale = publisher.take_pending().unwrap(); + + // The object is retired and the handle is recycled for a new object + // while the first claim is still in flight. + publisher.retire(HANDLE); + publish(&publisher, HANDLE, ReadinessFlags::WRITE); + stale.confirm(); + + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::WRITE, + }] + ); + } + + #[test] + fn closing_discards_state_and_later_updates() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + + publisher.close(); + + assert!(publisher.is_closed()); + assert!(publisher.take_pending().is_none()); + assert_eq!( + publisher.publish(HANDLE, ReadinessFlags::WRITE).unwrap(), + PublishOutcome::Closed + ); + assert_eq!(publisher.tracked_objects(), 0); + } + + #[test] + fn tracking_is_bounded() { + let publisher = ReadinessPublisher::new(); + for index in 0..MAX_TRACKED_READINESS_OBJECTS { + publish(&publisher, ObjectHandle(index as u64), ReadinessFlags::READ); + } + + assert_eq!( + publisher.publish(ObjectHandle(u64::MAX), ReadinessFlags::READ), + Err(ReadinessPublishError::TooManyObjects) + ); + + // Retiring an object makes room again. + publisher.retire(ObjectHandle(0)); + assert_eq!( + publisher.publish(ObjectHandle(u64::MAX), ReadinessFlags::READ), + Ok(PublishOutcome::Queued) + ); + } + + #[test] + fn repeated_updates_queue_one_claim_per_object() { + let publisher = ReadinessPublisher::new(); + for round in 1..=8u32 { + for handle in 0..4u64 { + publish(&publisher, ObjectHandle(handle), ReadinessFlags(round)); + } + } + + let drained = drain(&publisher); + + assert_eq!(drained.len(), 4); + assert!( + drained + .iter() + .all(|notification| notification.readiness == ReadinessFlags(8)) + ); + } + + /// Notification channel that hands each notification to the test thread and + /// then blocks until the test releases it, modelling a full ring. + struct GatedChannel { + sent: std::sync::mpsc::SyncSender, + release: std::sync::mpsc::Receiver<()>, + } + + impl HostNotificationChannel for GatedChannel { + type Error = &'static str; + + fn send_notification( + &mut self, + notification: &BrokerNotification, + ) -> Result<(), Self::Error> { + self.sent + .send(notification.clone()) + .map_err(|_| "test receiver dropped")?; + self.release.recv().map_err(|_| "test releaser dropped") + } + } + + struct FailingChannel; + + impl HostNotificationChannel for FailingChannel { + type Error = &'static str; + + fn send_notification( + &mut self, + _notification: &BrokerNotification, + ) -> Result<(), Self::Error> { + Err("notification channel failed") + } + } + + struct PanickingChannel; + + impl HostNotificationChannel for PanickingChannel { + type Error = &'static str; + + fn send_notification( + &mut self, + _notification: &BrokerNotification, + ) -> Result<(), Self::Error> { + panic!("notification channel panicked") + } + } + + fn readiness_of(notification: &BrokerNotification) -> ReadinessNotification { + let BrokerNotification::Readiness(readiness) = notification; + *readiness + } + + /// Runs publication on its own thread and reports its result through a + /// channel, so a loop that never ends fails a test on the deadline instead + /// of blocking it forever. + fn spawn_publication( + publisher: std::sync::Arc, + mut channel: GatedChannel, + mut wait_for_work: impl FnMut() + Send + 'static, + ) -> std::sync::mpsc::Receiver> { + let (finished, finish) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let _ = finished.send(publish_readiness( + &publisher, + &mut channel, + &mut wait_for_work, + )); + }); + finish + } + + fn expect_publication_ended(finish: &std::sync::mpsc::Receiver>) { + finish + .recv_timeout(TEST_TIMEOUT) + .expect("publication must end") + .expect("publication must end without a channel error"); + } + + #[test] + fn queued_updates_publish_in_order_until_the_waiter_closes() { + let publisher = std::sync::Arc::new(ReadinessPublisher::new()); + publish(&publisher, HANDLE, ReadinessFlags::READ); + publish(&publisher, OTHER_HANDLE, ReadinessFlags::WRITE); + let (sent, received) = std::sync::mpsc::sync_channel(4); + let (releaser, release) = std::sync::mpsc::channel(); + for _ in 0..2 { + releaser.send(()).unwrap(); + } + let channel = GatedChannel { sent, release }; + let closing = std::sync::Arc::clone(&publisher); + + let finish = spawn_publication(publisher, channel, move || closing.close()); + expect_publication_ended(&finish); + + let drained: alloc::vec::Vec<_> = received.try_iter().map(|n| readiness_of(&n)).collect(); + assert_eq!(drained.len(), 2); + assert_eq!(drained[0].handle, HANDLE); + assert_eq!(drained[1].handle, OTHER_HANDLE); + } + + #[test] + fn closing_ends_a_parked_publisher() { + let publisher = std::sync::Arc::new(ReadinessPublisher::new()); + let (sent, _received) = std::sync::mpsc::sync_channel(1); + let (_releaser, release) = std::sync::mpsc::channel(); + let channel = GatedChannel { sent, release }; + let parked = std::sync::Arc::clone(&publisher); + let (parked_sender, parked_receiver) = std::sync::mpsc::channel(); + + let finish = spawn_publication(parked, channel, move || { + // Reporting from inside the wait is what proves the publisher + // reached it, so closing is what ends it rather than a queue it + // already found closed. + let _ = parked_sender.send(()); + std::thread::sleep(core::time::Duration::from_millis(1)); + }); + parked_receiver + .recv_timeout(TEST_TIMEOUT) + .expect("publication must park before it is closed"); + publisher.close(); + + expect_publication_ended(&finish); + } + + #[test] + fn updates_recorded_while_a_send_blocks_are_published_afterwards() { + let publisher = std::sync::Arc::new(ReadinessPublisher::new()); + publish(&publisher, HANDLE, ReadinessFlags::READ); + let (sent, received) = std::sync::mpsc::sync_channel(0); + let (releaser, release) = std::sync::mpsc::channel(); + let channel = GatedChannel { sent, release }; + let publishing = std::sync::Arc::clone(&publisher); + let finish = spawn_publication(publishing, channel, || { + std::thread::sleep(core::time::Duration::from_millis(1)); + }); + + // The first notification is now in flight and cannot be confirmed yet. + assert_eq!( + readiness_of(&received.recv_timeout(TEST_TIMEOUT).unwrap()).readiness, + ReadinessFlags::READ + ); + assert_eq!( + publisher.publish(HANDLE, ReadinessFlags::HANGUP).unwrap(), + PublishOutcome::Queued + ); + releaser.send(()).unwrap(); + + assert_eq!( + readiness_of(&received.recv_timeout(TEST_TIMEOUT).unwrap()).readiness, + ReadinessFlags::HANGUP + ); + releaser.send(()).unwrap(); + publisher.close(); + expect_publication_ended(&finish); + } + + #[test] + fn a_failed_send_stops_publication_and_reports_the_channel_error() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + + let error = publish_readiness(&publisher, &mut FailingChannel, || { + unreachable!("a failed send must not park the publisher") + }) + .unwrap_err(); + + assert_eq!(error, "notification channel failed"); + + // The claim was dropped by the failed send rather than confirmed, so + // the update it carried is still publishable on a replacement channel. + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::READ, + }] + ); + } + + #[test] + fn an_abandoned_claim_returns_its_update_to_the_queue() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + + drop(publisher.take_pending().unwrap()); + + // Republishing the same flags coalesces, so nothing else can rescue the + // update if abandoning the claim strands it. + assert_eq!( + publisher.publish(HANDLE, ReadinessFlags::READ).unwrap(), + PublishOutcome::Coalesced + ); + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::READ, + }] + ); + } + + #[test] + fn abandoning_a_stale_claim_requeues_nothing() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + let stale = publisher.take_pending().unwrap(); + + publish(&publisher, HANDLE, ReadinessFlags::WRITE); + let newer = publisher.take_pending().unwrap(); + + // Taking the newer update leaves the object unqueued, so its generation + // is all that marks the older claim as superseded. Without that + // comparison the older claim requeues an object whose current update is + // already in flight, publishing the same readiness twice. + assert_eq!(publisher.queued_updates(), 0); + drop(stale); + assert_eq!(publisher.queued_updates(), 0); + + // A change recorded while that newer claim is still out queues the + // object again, and abandoning the claim must not queue it a second + // time. + publish(&publisher, HANDLE, ReadinessFlags::READ); + assert_eq!(publisher.queued_updates(), 1); + drop(newer); + assert_eq!(publisher.queued_updates(), 1); + + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::READ, + }] + ); + } + + #[test] + fn a_panicking_send_returns_its_update_to_the_queue() { + let publisher = ReadinessPublisher::new(); + publish(&publisher, HANDLE, ReadinessFlags::READ); + + let previous_hook = std::panic::take_hook(); + std::panic::set_hook(alloc::boxed::Box::new(|_| {})); + let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _ = publish_readiness(&publisher, &mut PanickingChannel, || { + unreachable!("a panicking send must not park the publisher") + }); + })); + std::panic::set_hook(previous_hook); + + assert!(unwound.is_err()); + + // Unwinding past the claim runs its drop, which must leave the update + // publishable rather than stranded. + assert_eq!( + drain(&publisher), + [ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::READ, + }] + ); + } +} diff --git a/litebox_broker_userland/src/lib.rs b/litebox_broker_userland/src/lib.rs new file mode 100644 index 0000000000..aad4690c33 --- /dev/null +++ b/litebox_broker_userland/src/lib.rs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Linux-userland broker host deployment support. +//! +//! The broker host binary in this crate owns process, socket, and thread +//! policy. This library holds the deployment pieces that are useful outside +//! `main`, so integration tests can drive the same code the binary runs. + +pub mod readiness; diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 89f76d39c5..a9c36445c6 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -25,9 +25,11 @@ use litebox_broker_protocol::shared_memory::{ use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ - UnixControlRingHostRequestSource, UnixControlRingHostResponseSink, UnixControlRingHostShutdown, - UnixStreamHostSetupChannel, validate_peer_process, + UnixControlRingHostNotificationChannel, UnixControlRingHostRequestSource, + UnixControlRingHostResponseSink, UnixControlRingHostShutdown, UnixStreamHostSetupChannel, + validate_peer_process, }; +use litebox_broker_userland::readiness::ReadinessPublisherRuntime; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); const ACCEPT_RETRY_DELAY: Duration = Duration::from_millis(10); @@ -128,16 +130,123 @@ fn serve_runner( .into()); } }; - let (request_source, response_sink, _notification_channel, shutdown) = + let (request_source, response_sink, notification_channel, shutdown) = control_channel.into_active(control_ring)?; - dispatch_requests(association, request_source, response_sink, shutdown)?; + dispatch_requests( + association, + Arc::new(ReadinessPublisherRuntime::new()), + request_source, + response_sink, + notification_channel, + shutdown, + )?; Ok(()) } +/// Records the first failure of an association and ends its transport. +/// +/// Every thread serving an association reports through this, and the endpoints +/// they block on are released by ending the transport, so it is what the +/// teardown guards below reach for. +struct HostAssociationFailureCoordinator { + failed: AtomicBool, + error: Mutex>, + shutdown: UnixControlRingHostShutdown, +} + +impl HostAssociationFailureCoordinator { + const fn new(shutdown: UnixControlRingHostShutdown) -> Self { + Self { + failed: AtomicBool::new(false), + error: Mutex::new(None), + shutdown, + } + } + + fn failed(&self) -> bool { + self.failed.load(Ordering::Acquire) + } + + fn report(&self, error: IoError) { + if self.failed.swap(true, Ordering::AcqRel) { + return; + } + *self + .error + .lock() + .expect("broker association failure mutex poisoned") = Some(error); + let _ = self.shutdown.shutdown(); + } + + /// Ends the association transport without recording a failure. + /// + /// Teardown uses this to release endpoints blocked on the control ring + /// without turning a shutdown that reported nothing into a reported error. + fn shutdown(&self) { + let _ = self.shutdown.shutdown(); + } + + fn take_error(&self) -> Option { + self.error + .lock() + .expect("broker association failure mutex poisoned") + .take() + } +} + +/// Fails the association if readiness publication unwinds. +/// +/// The request reader owns association termination but does not depend on the +/// publisher, so an unwinding publisher would otherwise leave a live +/// association with no notification source. The join that turns that panic into +/// a reported failure is reached only once the reader has returned, and a peer +/// that is waiting for a readiness change it will never be told about does not +/// return it. Failing the association here ends that wait instead. +struct PublisherPanicGuard<'association> { + failure_coordinator: &'association HostAssociationFailureCoordinator, +} + +impl Drop for PublisherPanicGuard<'_> { + fn drop(&mut self) { + if std::thread::panicking() { + self.failure_coordinator + .report(IoError::other("broker readiness publisher panicked")); + } + } +} + +/// Ends readiness publication when an association scope ends for any reason. +/// +/// The publisher is a scoped thread, so the scope joins it before propagating a +/// panic out of the association, and both states it can rest in have to end for +/// that join to complete. Closing publication returns a publisher parked for +/// work, and ending the transport returns one blocked on notification capacity +/// that a local endpoint stopped draining. The failure coordinator owns the +/// association until `dispatch_requests` returns, which is after that join, so +/// an unwind cannot leave ending the transport to dropping it. +struct ReadinessPublicationGuard<'association> { + readiness: &'association ReadinessPublisherRuntime, + failure_coordinator: &'association HostAssociationFailureCoordinator, +} + +impl Drop for ReadinessPublicationGuard<'_> { + fn drop(&mut self) { + self.readiness.close(); + self.failure_coordinator.shutdown(); + } +} + +/// Serves one association until it ends, then reports its first failure. +/// +/// `readiness` is created by the caller rather than here so readiness sources +/// can record into the same runtime this publishes from. Nothing publishes into +/// it in production yet; the Linux network reactor is its first source. fn dispatch_requests( association: BrokerHostAssociation<'_, Memory>, + readiness: Arc, mut request_source: UnixControlRingHostRequestSource, response_sink: UnixControlRingHostResponseSink, + mut notification_channel: UnixControlRingHostNotificationChannel, shutdown: UnixControlRingHostShutdown, ) -> IoResult<()> { let association = Arc::new(association); @@ -146,6 +255,40 @@ fn dispatch_requests( let request_receiver = Arc::new(Mutex::new(request_receiver)); std::thread::scope(|scope| { + let publisher_readiness = Arc::clone(&readiness); + let publisher_failure_coordinator = Arc::clone(&failure_coordinator); + let publisher = std::thread::Builder::new() + .name("litebox-broker-notifier".to_owned()) + .spawn_scoped(scope, move || { + let _panicking = PublisherPanicGuard { + failure_coordinator: &publisher_failure_coordinator, + }; + // The request reader owns association termination. A failing + // notification transport fails the association, so a reader + // still running observes and reports the same error, and a peer + // that closed cleanly is not a failure at all. Reporting here + // would turn a clean shutdown into a reported error. A failure + // that first appears once the reader has returned is dropped + // deliberately, because the association is already over. + let _ = publisher_readiness.run(&mut notification_channel); + }); + let publisher = match publisher { + Ok(publisher) => Some(publisher), + Err(error) => { + failure_coordinator.report(error); + None + } + }; + + // Publication must end on every exit, including an unwind: the scope + // joins the publisher before it propagates a panic, and a publisher + // still parked or still blocked on ring capacity would never return, + // hanging teardown instead. + let publication = ReadinessPublicationGuard { + readiness: &readiness, + failure_coordinator: &failure_coordinator, + }; + let mut workers = Vec::with_capacity(WORKER_COUNT); for worker_id in 0..WORKER_COUNT { let association = Arc::clone(&association); @@ -176,6 +319,20 @@ fn dispatch_requests( failure_coordinator.report(IoError::other("broker request worker panicked")); } } + // Readiness publication lives exactly as long as the association. The + // request reader returns only once the association is over, but workers + // keep draining already-queued requests after that, so publication must + // outlive them or a late readiness change would be discarded. Ending it + // here rather than leaving it to the scope orders it before the join + // that observes a panicking publisher, and dropping the guard is what + // ends both states the publisher can rest in without depending on the + // reader having failed the association already. + drop(publication); + if let Some(publisher) = publisher + && publisher.join().is_err() + { + failure_coordinator.report(IoError::other("broker readiness publisher panicked")); + } }); match failure_coordinator.take_error() { @@ -248,44 +405,6 @@ fn run_worker( } } -struct HostAssociationFailureCoordinator { - failed: AtomicBool, - error: Mutex>, - shutdown: UnixControlRingHostShutdown, -} - -impl HostAssociationFailureCoordinator { - const fn new(shutdown: UnixControlRingHostShutdown) -> Self { - Self { - failed: AtomicBool::new(false), - error: Mutex::new(None), - shutdown, - } - } - - fn failed(&self) -> bool { - self.failed.load(Ordering::Acquire) - } - - fn report(&self, error: IoError) { - if self.failed.swap(true, Ordering::AcqRel) { - return; - } - *self - .error - .lock() - .expect("broker association failure mutex poisoned") = Some(error); - let _ = self.shutdown.shutdown(); - } - - fn take_error(&self) -> Option { - self.error - .lock() - .expect("broker association failure mutex poisoned") - .take() - } -} - fn accept_runner_stream( listener: &UnixListener, runner: &mut Child, @@ -326,11 +445,27 @@ mod tests { use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; use litebox_broker_protocol::channel::{HostSetupChannel, LocalSetupChannel}; use litebox_broker_protocol::message::BrokerHandshakeResponse; - use litebox_broker_transport::unix_socket::UnixStreamLocalSetupChannel; + use litebox_broker_transport::unix_socket::{ + UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, + UnixControlRingLocalShutdown, UnixStreamLocalSetupChannel, + }; use std::os::fd::AsFd; - #[test] - fn first_failure_is_preserved_and_unblocks_request_reading() { + /// One live host association: the endpoints teardown acts on, and the rest + /// held open so the association stays up for the duration of a test. + struct LiveAssociation { + request_source: UnixControlRingHostRequestSource, + notifications: UnixControlRingHostNotificationChannel, + shutdown: UnixControlRingHostShutdown, + _response_sink: UnixControlRingHostResponseSink, + _local: ( + UnixControlRingLocalCallChannel, + UnixControlRingLocalNotificationChannel, + UnixControlRingLocalShutdown, + ), + } + + fn live_association() -> LiveAssociation { let (peer_stream, host_stream) = UnixStream::pair().unwrap(); let mut local_setup = UnixStreamLocalSetupChannel::from_connected(peer_stream); let mut control_channel = UnixStreamHostSetupChannel::from_accepted(host_stream); @@ -350,10 +485,231 @@ mod tests { let host_ring = ControlRing::new(host_memory).unwrap(); let local_activation = std::thread::spawn(move || local_setup.into_active(local_ring, || {}).unwrap()); - let (mut request_source, _response_sink, _notifications, shutdown) = + let (request_source, response_sink, notifications, shutdown) = control_channel.into_active(host_ring).unwrap(); - let (_local_call, _local_notifications, _local_shutdown) = local_activation.join().unwrap(); - let failure_coordinator = HostAssociationFailureCoordinator::new(shutdown); + LiveAssociation { + request_source, + notifications, + shutdown, + _response_sink: response_sink, + _local: local_activation.join().unwrap(), + } + } + + /// A notification channel that accepts every send and keeps nothing. + struct DiscardingChannel; + + impl litebox_broker_protocol::channel::HostNotificationChannel for DiscardingChannel { + type Error = IoError; + + fn send_notification( + &mut self, + _notification: &litebox_broker_protocol::message::BrokerNotification, + ) -> IoResult<()> { + Ok(()) + } + } + + /// Negotiates the local half of an association served by [`spawn_dispatch`]. + fn negotiate_local( + stream: UnixStream, + ) -> ( + litebox_broker_local::BrokerLocal, + UnixControlRingLocalNotificationChannel, + ) { + litebox_broker_local::BrokerLocal::negotiate( + UnixStreamLocalSetupChannel::from_connected(stream), + |mut setup| { + let shared_memory = setup.receive_memfd(SHARED_BUFFER_POOL_SIZE, None)?; + let control_memory = setup.receive_memfd(CONTROL_RING_MEMORY_SIZE, None)?; + let control_ring = ControlRing::new(control_memory).map_err(|error| { + IoError::new( + ErrorKind::InvalidData, + format!("invalid test control ring: {error:?}"), + ) + })?; + let (call_channel, notifications, _shutdown) = + setup.into_active(control_ring, || {})?; + Ok((call_channel, Arc::new(shared_memory), notifications)) + }, + ) + .unwrap() + } + + /// One association served by `dispatch_requests` exactly as production + /// serves it. + /// + /// The guard tests above cover what the teardown guards do; only this + /// covers that `dispatch_requests` installs them and starts a publisher at + /// all, because its single production caller is unreachable from a test. + /// Dispatch starts only once the local half has finished negotiating, so a + /// publisher that fails immediately cannot race activation. + fn spawn_dispatch( + readiness: Arc, + ) -> ( + litebox_broker_local::BrokerLocal, + UnixControlRingLocalNotificationChannel, + Receiver>, + std::thread::JoinHandle<()>, + ) { + let (local_stream, host_stream) = UnixStream::pair().unwrap(); + let (outcome_sender, outcome) = sync_channel(1); + let (start, started) = sync_channel(1); + let host = std::thread::spawn(move || { + let broker = BrokerCore::new(PolicyEngine::with_host_guaranteed_rights( + ObjectRights::all(), + )) + .unwrap(); + let shared_memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE).unwrap(); + let shared_buffers = + SharedBufferPool::new(shared_memory, SHARED_BUFFER_LAYOUT).unwrap(); + let control_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let control_ring = ControlRing::new(control_memory).unwrap(); + let mut control = UnixStreamHostSetupChannel::from_host_guaranteed( + host_stream, + Instant::now() + SETUP_TIMEOUT, + ); + let association = setup_connection(&broker, &mut control, &shared_buffers, |channel| { + channel.send_memfd(shared_buffers.memory(), None)?; + channel.send_memfd(control_ring.memory(), None) + }) + .unwrap() + .unwrap(); + let (request_source, response_sink, notifications, shutdown) = + control.into_active(control_ring).unwrap(); + started.recv().unwrap(); + outcome_sender + .send(dispatch_requests( + association, + readiness, + request_source, + response_sink, + notifications, + shutdown, + )) + .unwrap(); + }); + let (local, notifications) = negotiate_local(local_stream); + start.send(()).unwrap(); + (local, notifications, outcome, host) + } + + #[test] + fn publication_guard_ends_a_parked_publisher() { + let association = live_association(); + let failure_coordinator = HostAssociationFailureCoordinator::new(association.shutdown); + let readiness = Arc::new(ReadinessPublisherRuntime::new()); + let publishing = Arc::clone(&readiness); + let (finished, finish) = sync_channel(1); + let publisher = std::thread::spawn(move || { + finished + .send(publishing.run(&mut DiscardingChannel)) + .unwrap(); + }); + + // The publisher parks on an empty queue, so only closing publication + // ends it. An unwind past the explicit close leaves the guard as the + // only thing that can, and the scope joins the publisher before it + // propagates the panic. + std::thread::sleep(Duration::from_millis(20)); + drop(ReadinessPublicationGuard { + readiness: &readiness, + failure_coordinator: &failure_coordinator, + }); + + finish + .recv_timeout(SETUP_TIMEOUT) + .expect("dropping the guard must end the parked publisher") + .unwrap(); + publisher.join().unwrap(); + } + + #[test] + fn publication_guard_ends_a_capacity_blocked_publisher() { + use litebox_broker_protocol::ObjectHandle; + use litebox_broker_protocol::readiness::ReadinessFlags; + use litebox_broker_transport::control_ring::CONTROL_RING_NOTIFICATION_SLOT_COUNT; + + let association = live_association(); + let mut notifications = association.notifications; + let failure_coordinator = HostAssociationFailureCoordinator::new(association.shutdown); + let readiness = Arc::new(ReadinessPublisherRuntime::new()); + + // The local endpoint never drains, so the ring fills and the publisher + // ends up blocked on capacity rather than parked for work. Closing + // publication cannot reach it there, and an unwind reaches the scope + // join before anything else ends the transport. + for handle in 0..CONTROL_RING_NOTIFICATION_SLOT_COUNT * 3 { + readiness + .publish(ObjectHandle(handle), ReadinessFlags::READ) + .unwrap(); + } + let publishing = Arc::clone(&readiness); + let (finished, finish) = sync_channel(1); + let publisher = std::thread::spawn(move || { + finished.send(publishing.run(&mut notifications)).unwrap(); + }); + std::thread::sleep(Duration::from_millis(20)); + + drop(ReadinessPublicationGuard { + readiness: &readiness, + failure_coordinator: &failure_coordinator, + }); + + let outcome = finish + .recv_timeout(SETUP_TIMEOUT) + .expect("dropping the guard must end a publisher blocked on capacity"); + publisher.join().unwrap(); + assert_eq!( + outcome + .expect_err("ending the transport must fail the blocked send") + .kind(), + ErrorKind::ConnectionAborted + ); + assert!( + failure_coordinator.take_error().is_none(), + "ending the transport during teardown must not report a failure" + ); + } + + #[test] + fn a_panicking_publisher_ends_a_blocked_request_reader() { + let association = live_association(); + let mut request_source = association.request_source; + let failure_coordinator = + Arc::new(HostAssociationFailureCoordinator::new(association.shutdown)); + let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); + let reader = std::thread::spawn(move || { + result_sender.send(request_source.recv_request()).unwrap(); + }); + + // The peer sends nothing and never closes, so the reader returns only + // if the publisher's unwind fails the association. + let publisher_failure_coordinator = Arc::clone(&failure_coordinator); + let publisher = std::thread::spawn(move || { + let _panicking = PublisherPanicGuard { + failure_coordinator: &publisher_failure_coordinator, + }; + panic!("readiness publication panicked"); + }); + + let receive_result = result_receiver + .recv_timeout(SETUP_TIMEOUT) + .expect("a panicking publisher must end a blocked request reader"); + assert!(matches!( + receive_result, + Ok(HostReceive::PeerClosed) | Err(_) + )); + reader.join().unwrap(); + assert!(publisher.join().is_err()); + assert!(failure_coordinator.take_error().is_some()); + } + + #[test] + fn first_failure_is_preserved_and_unblocks_request_reading() { + let association = live_association(); + let mut request_source = association.request_source; + let failure_coordinator = HostAssociationFailureCoordinator::new(association.shutdown); let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(1); let reader = std::thread::spawn(move || { result_sender.send(request_source.recv_request()).unwrap(); @@ -372,4 +728,71 @@ mod tests { assert_eq!(error.kind(), ErrorKind::TimedOut); assert_eq!(error.to_string(), "first failure"); } + + #[test] + fn dispatching_requests_publishes_readiness_until_the_association_ends() { + use litebox_broker_protocol::ObjectHandle; + use litebox_broker_protocol::message::{BrokerNotification, ReadinessNotification}; + use litebox_broker_protocol::readiness::ReadinessFlags; + + const HANDLE: ObjectHandle = ObjectHandle(11); + let expected = ReadinessFlags::READ | ReadinessFlags::WRITE; + let readiness = Arc::new(ReadinessPublisherRuntime::new()); + let (local, mut notifications, outcome, host) = spawn_dispatch(Arc::clone(&readiness)); + + readiness.publish(HANDLE, expected).unwrap(); + + // The receive has no deadline of its own, so a publisher that dispatch + // never started has to fail the test rather than hang it. + let (notified, notifications_seen) = sync_channel(1); + let receiver = std::thread::spawn(move || { + use litebox_broker_protocol::channel::LocalNotificationChannel; + + let notification = notifications.recv_notification().unwrap(); + notified.send(notification).unwrap(); + notifications + }); + let notification = notifications_seen + .recv_timeout(SETUP_TIMEOUT) + .expect("dispatch must publish readiness recorded in its runtime"); + assert_eq!( + notification, + Some(BrokerNotification::Readiness(ReadinessNotification { + handle: HANDLE, + readiness: expected, + })) + ); + let notifications = receiver.join().unwrap(); + + // A publisher parked for work outlives a clean local close unless + // dispatch ends publication, so this deadline covers that too. + drop(local); + drop(notifications); + outcome + .recv_timeout(SETUP_TIMEOUT) + .expect("a clean local close must end dispatch") + .unwrap(); + host.join().unwrap(); + } + + #[test] + fn dispatching_requests_fails_when_its_readiness_publisher_panics() { + // Publication is one-shot, so a runtime that has already run makes the + // publisher thread panic as soon as dispatch starts it. + let readiness = Arc::new(ReadinessPublisherRuntime::new()); + readiness.close(); + readiness.run(&mut DiscardingChannel).unwrap(); + + // The local half stays connected and idle, so nothing but the panic can + // release the request reader that owns association termination. + let (local, _notifications, outcome, host) = spawn_dispatch(Arc::clone(&readiness)); + + let error = outcome + .recv_timeout(SETUP_TIMEOUT) + .expect("a panicking publisher must end dispatch") + .expect_err("a panicking publisher must fail the association"); + assert_eq!(error.to_string(), "broker readiness publisher panicked"); + drop(local); + host.join().unwrap(); + } } diff --git a/litebox_broker_userland/src/readiness.rs b/litebox_broker_userland/src/readiness.rs new file mode 100644 index 0000000000..b567a2d7de --- /dev/null +++ b/litebox_broker_userland/src/readiness.rs @@ -0,0 +1,318 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Threaded readiness publication for the Linux-userland broker host. +//! +//! [`litebox_broker_host::readiness`] owns the portable coalescing state and +//! the publication loop but deliberately holds no wake primitive, so a +//! deployment supplies one. This module pairs that state with a condition +//! variable and the thread-facing API the broker host binary needs. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Condvar, Mutex}; + +use litebox_broker_host::readiness::{ + PublishOutcome, ReadinessPublishError, ReadinessPublisher, publish_readiness, +}; +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::channel::HostNotificationChannel; +use litebox_broker_protocol::readiness::ReadinessFlags; + +/// Readiness publication state plus the wake primitive its publisher parks on. +/// +/// Backend readiness sources share this value and call [`publish`] and +/// [`retire`]; neither waits for notification transport capacity. Exactly one +/// thread calls [`run`], which owns the association notification channel for +/// the lifetime of the association. +/// +/// [`publish`]: Self::publish +/// [`retire`]: Self::retire +/// [`run`]: Self::run +#[derive(Debug)] +pub struct ReadinessPublisherRuntime { + publisher: ReadinessPublisher, + signaled: Mutex, + work_available: Condvar, + running: AtomicBool, +} + +impl Default for ReadinessPublisherRuntime { + fn default() -> Self { + Self::new() + } +} + +impl ReadinessPublisherRuntime { + /// Creates idle readiness publication state. + #[must_use] + pub fn new() -> Self { + Self { + publisher: ReadinessPublisher::new(), + signaled: Mutex::new(false), + work_available: Condvar::new(), + running: AtomicBool::new(false), + } + } + + /// Records the authoritative readiness of one broker object. + /// + /// Updates are coalesced per object, so a source may call this as often as + /// its backend state changes. An update recorded after [`close`] is + /// discarded rather than reported, because the association it would reach + /// is already over. Returns an error only when the association already + /// tracks the maximum number of objects. + /// + /// [`close`]: Self::close + pub fn publish( + &self, + handle: ObjectHandle, + readiness: ReadinessFlags, + ) -> Result<(), ReadinessPublishError> { + if self.publisher.publish(handle, readiness)? == PublishOutcome::Queued { + self.signal(); + } + Ok(()) + } + + /// Drops readiness state for an object whose backend resource is retired. + pub fn retire(&self, handle: ObjectHandle) { + self.publisher.retire(handle); + } + + /// Publishes readiness notifications until the runtime is closed. + /// + /// The caller must be the only owner of `channel`. Sending blocks while the + /// local endpoint leaves the notification transport full; that is confined + /// to this thread by design, and association teardown fails the blocked + /// send through the transport. + /// + /// # Panics + /// + /// Panics if publication has already run. Closure is terminal, so a runtime + /// serves exactly one publisher, and a second one would park on a wake that + /// only ever releases one waiter. That is a silent hang, so the second + /// caller is rejected loudly instead. A returned error is terminal for the + /// same reason: the portable loop leaves the failed update publishable, but + /// no later publisher can send it, so this closes publication rather than + /// leave sources recording into state nothing will drain. Resuming on a + /// replacement channel means a new runtime, which starts empty, so the + /// failed update is lost at this layer. + pub fn run( + &self, + channel: &mut Channel, + ) -> Result<(), Channel::Error> { + assert!( + !self.running.swap(true, Ordering::Relaxed), + "readiness publication must run exactly once per runtime" + ); + let outcome = publish_readiness(&self.publisher, channel, || self.wait_for_work()); + if outcome.is_err() { + self.publisher.close(); + } + outcome + } + + /// Closes publication and wakes a publisher parked for work so [`run`] + /// returns. + /// + /// A publisher already inside a blocking send is not woken by this; the + /// notification transport ends that send when the association fails or its + /// peer closes. + /// + /// [`run`]: Self::run + pub fn close(&self) { + self.publisher.close(); + self.signal(); + } + + fn signal(&self) { + *self + .signaled + .lock() + .expect("broker readiness publisher mutex poisoned") = true; + self.work_available.notify_one(); + } + + fn wait_for_work(&self) { + let mut signaled = self + .signaled + .lock() + .expect("broker readiness publisher mutex poisoned"); + while !*signaled { + signaled = self + .work_available + .wait(signaled) + .expect("broker readiness publisher mutex poisoned"); + } + *signaled = false; + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::sync::mpsc::{Receiver, Sender, channel}; + use std::time::Duration; + + use litebox_broker_protocol::message::{BrokerNotification, ReadinessNotification}; + + use super::*; + + const HANDLE: ObjectHandle = ObjectHandle(3); + const OTHER_HANDLE: ObjectHandle = ObjectHandle(5); + const TEST_TIMEOUT: Duration = Duration::from_secs(5); + + struct RecordingChannel { + sent: Sender, + } + + impl HostNotificationChannel for RecordingChannel { + type Error = &'static str; + + fn send_notification( + &mut self, + notification: &BrokerNotification, + ) -> Result<(), Self::Error> { + let BrokerNotification::Readiness(readiness) = notification; + self.sent.send(*readiness).map_err(|_| "receiver dropped") + } + } + + /// Runs a publisher on its own thread and reports its result through a + /// channel, so a publisher that never ends fails a test on the deadline + /// instead of hanging its join. + fn spawn_publisher( + runtime: &Arc, + sent: Sender, + ) -> Receiver> { + let publishing = Arc::clone(runtime); + let (finished, finish) = channel(); + std::thread::spawn(move || { + let mut channel = RecordingChannel { sent }; + let _ = finished.send(publishing.run(&mut channel)); + }); + finish + } + + fn expect_publication_ended(finish: &Receiver>) { + finish + .recv_timeout(TEST_TIMEOUT) + .expect("publication must end") + .expect("publication must end without a channel error"); + } + + #[test] + fn a_parked_publisher_wakes_for_a_later_readiness_update() { + let runtime = Arc::new(ReadinessPublisherRuntime::new()); + let (sent, received) = channel(); + let finish = spawn_publisher(&runtime, sent); + + // The publisher parks first, so this update must wake it. + std::thread::sleep(Duration::from_millis(20)); + runtime.publish(HANDLE, ReadinessFlags::READ).unwrap(); + + assert_eq!( + received.recv_timeout(TEST_TIMEOUT).unwrap(), + ReadinessNotification { + handle: HANDLE, + readiness: ReadinessFlags::READ, + } + ); + runtime.close(); + expect_publication_ended(&finish); + } + + use litebox_broker_host::readiness::MAX_TRACKED_READINESS_OBJECTS; + + struct FailingChannel; + + impl HostNotificationChannel for FailingChannel { + type Error = &'static str; + + fn send_notification( + &mut self, + _notification: &BrokerNotification, + ) -> Result<(), Self::Error> { + Err("notification channel failed") + } + } + + #[test] + fn a_failed_publication_closes_the_runtime() { + let runtime = ReadinessPublisherRuntime::new(); + runtime.publish(HANDLE, ReadinessFlags::READ).unwrap(); + + runtime.run(&mut FailingChannel).unwrap_err(); + + // Publication cannot run again, so a closed runtime must discard later + // updates rather than let sources fill tracking state to its limit. + for handle in 0..=MAX_TRACKED_READINESS_OBJECTS as u64 { + runtime + .publish(ObjectHandle(handle), ReadinessFlags::READ) + .unwrap(); + } + } + + #[test] + #[should_panic(expected = "exactly once")] + fn publication_runs_at_most_once() { + let runtime = ReadinessPublisherRuntime::new(); + let (sent, _received) = channel(); + let mut channel = RecordingChannel { sent }; + + // Closure is terminal, so the first run returns at once and the second + // would otherwise park on a wake that can never come. + runtime.close(); + runtime.run(&mut channel).unwrap(); + + let _ = runtime.run(&mut channel); + } + + #[test] + fn closing_wakes_a_publisher_waiting_for_work() { + let runtime = Arc::new(ReadinessPublisherRuntime::new()); + let waiting = Arc::clone(&runtime); + let (wake_sender, wakes) = channel(); + std::thread::spawn(move || { + waiting.wait_for_work(); + let _ = wake_sender.send(()); + }); + + // The latch is sticky, so this must end the wait whether it lands + // before the waiter parks or after it, which is why the test needs no + // rendezvous with a park it cannot observe. + runtime.close(); + + wakes + .recv_timeout(TEST_TIMEOUT) + .expect("closing must end a wait for work"); + } + + #[test] + fn retired_objects_stop_being_published() { + let runtime = Arc::new(ReadinessPublisherRuntime::new()); + let (sent, received) = channel(); + + // The retired handle is queued ahead of the surviving one, so it would + // arrive first if retirement had not dropped its queued update. The + // publisher starts only once both calls have run, which keeps it from + // draining the queue before retirement. + runtime.publish(HANDLE, ReadinessFlags::READ).unwrap(); + runtime.retire(HANDLE); + runtime.publish(OTHER_HANDLE, ReadinessFlags::READ).unwrap(); + + let finish = spawn_publisher(&runtime, sent); + + assert_eq!( + received.recv_timeout(TEST_TIMEOUT).unwrap(), + ReadinessNotification { + handle: OTHER_HANDLE, + readiness: ReadinessFlags::READ, + }, + "a retired object must not be published" + ); + runtime.close(); + expect_publication_ended(&finish); + assert!(received.try_iter().next().is_none()); + } +} diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index 393be1e740..4dba303632 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -3,6 +3,9 @@ use std::os::unix::net::UnixStream; use std::sync::Arc; +use std::sync::mpsc::{Receiver, channel}; +use std::thread::JoinHandle; +use std::time::Duration; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{ConnectionTermination, setup_connection}; @@ -14,11 +17,107 @@ use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::shared_memory::{ SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, }; -use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; +use litebox_broker_transport::control_ring::{ + CONTROL_RING_MEMORY_SIZE, CONTROL_RING_NOTIFICATION_SLOT_COUNT, ControlRing, +}; use litebox_broker_transport::shared_memory::MemfdSharedMemory; use litebox_broker_transport::unix_socket::{ + UnixControlRingHostNotificationChannel, UnixControlRingHostShutdown, + UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, UnixStreamHostSetupChannel, UnixStreamLocalSetupChannel, }; +use litebox_broker_userland::readiness::ReadinessPublisherRuntime; + +/// Long enough that a hung wakeup fails the test instead of hanging CI. +const TEST_TIMEOUT: Duration = Duration::from_secs(10); +/// Long enough for the local endpoint to reach its blocking receive. +const BLOCK_DELAY: Duration = Duration::from_millis(50); +/// More objects than the notification ring holds, so publication must block. +const OVERSUBSCRIBED_OBJECT_COUNT: u64 = CONTROL_RING_NOTIFICATION_SLOT_COUNT * 3; + +/// Runs one host association whose only traffic is readiness notifications. +/// +/// The reader thread mirrors production: it is the endpoint that observes local +/// termination and interrupts every ring wait of the association. +fn spawn_host( + stream: UnixStream, + host: impl FnOnce(UnixControlRingHostNotificationChannel, UnixControlRingHostShutdown) + + Send + + 'static, +) -> JoinHandle<()> { + std::thread::spawn(move || { + let broker = BrokerCore::new(PolicyEngine::with_unauthenticated_rights( + ObjectRights::all(), + )) + .unwrap(); + let shared_memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE).unwrap(); + let shared_buffers = SharedBufferPool::new(shared_memory, SHARED_BUFFER_LAYOUT).unwrap(); + let control_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let control_ring = ControlRing::new(control_memory).unwrap(); + let mut control = UnixStreamHostSetupChannel::from_accepted(stream); + let association = setup_connection(&broker, &mut control, &shared_buffers, |channel| { + channel.send_memfd(shared_buffers.memory(), None)?; + channel.send_memfd(control_ring.memory(), None) + }) + .unwrap() + .unwrap(); + let (mut request_source, response_sink, notifications, shutdown) = + control.into_active(control_ring).unwrap(); + std::thread::scope(|scope| { + scope.spawn(|| { + while let Ok(HostReceive::Message(request)) = request_source.recv_request() { + association + .execute_request(request, |response| response_sink.send_response(response)) + .unwrap(); + } + }); + host(notifications, shutdown); + }); + }) +} + +/// Negotiates the local half of an association created by [`spawn_host`]. +fn negotiate_local( + stream: UnixStream, +) -> ( + BrokerLocal, + BrokerNotifications, +) { + let (local, notifications) = BrokerLocal::negotiate( + UnixStreamLocalSetupChannel::from_connected(stream), + |mut setup| { + let shared_memory = setup.receive_memfd(SHARED_BUFFER_POOL_SIZE, None)?; + let control_memory = setup.receive_memfd(CONTROL_RING_MEMORY_SIZE, None)?; + let control_ring = ControlRing::new(control_memory).map_err(|error| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid test control ring: {error:?}"), + ) + })?; + let (call_channel, notifications, _shutdown) = + setup.into_active(control_ring, || {})?; + Ok((call_channel, Arc::new(shared_memory), notifications)) + }, + ) + .unwrap(); + (local, BrokerNotifications::new(notifications)) +} + +/// Receives a publisher's outcome under the test deadline, so publication that +/// never ends fails the test instead of hanging its join. +fn expect_publication_ended(outcomes: &Receiver>) { + outcomes + .recv_timeout(TEST_TIMEOUT) + .expect("publication must end") + .expect("publication must end without a transport error"); +} + +fn readiness_of(notification: Option) -> ReadinessNotification { + let Some(BrokerNotification::Readiness(readiness)) = notification else { + panic!("expected a readiness notification, got {notification:?}"); + }; + readiness +} #[test] fn host_serves_control_requests_and_notifications_over_shared_rings() { @@ -96,3 +195,157 @@ fn host_serves_control_requests_and_notifications_over_shared_rings() { ConnectionTermination::PeerClosed ); } + +#[test] +fn a_host_readiness_source_wakes_a_blocked_local_receiver() { + const HANDLE: ObjectHandle = ObjectHandle(11); + let readiness = ReadinessFlags::READ | ReadinessFlags::WRITE; + let (local_control, host_control) = UnixStream::pair().unwrap(); + let (finish_sender, finish_receiver) = channel::<()>(); + let (outcome_sender, outcome_receiver) = channel(); + + let host = spawn_host(host_control, move |mut notifications, _shutdown| { + let runtime = Arc::new(ReadinessPublisherRuntime::new()); + let publishing = Arc::clone(&runtime); + let publisher = std::thread::spawn(move || publishing.run(&mut notifications)); + + // The local endpoint is blocked in its notification receive by now, so + // this update is the only thing that can wake it. + std::thread::sleep(BLOCK_DELAY); + runtime.publish(HANDLE, readiness).unwrap(); + + let _ = finish_receiver.recv(); + runtime.close(); + // Reporting the publisher's outcome rather than joining here keeps a + // close that stops waking it a test failure instead of a hang. + outcome_sender.send(publisher.join().unwrap()).unwrap(); + }); + + let (local, notifications) = negotiate_local(local_control); + let (notification_sender, notification_receiver) = channel(); + let receiver_thread = std::thread::spawn(move || { + let mut notifications = notifications; + // Receiving on a helper thread keeps a wake that never arrives a test + // failure rather than a hang: the notification wait has no deadline of + // its own. + let notification = readiness_of(notifications.recv_notification().unwrap()); + notification_sender.send(notification).unwrap(); + notifications + }); + + assert_eq!( + notification_receiver + .recv_timeout(TEST_TIMEOUT) + .expect("a host readiness source must wake the blocked local receiver"), + ReadinessNotification { + handle: HANDLE, + readiness, + } + ); + let _notifications = receiver_thread.join().unwrap(); + drop(finish_sender); + drop(local); + expect_publication_ended(&outcome_receiver); + host.join().unwrap(); +} + +#[test] +fn a_full_notification_ring_does_not_block_readiness_sources() { + let (local_control, host_control) = UnixStream::pair().unwrap(); + let (published_sender, published_receiver) = channel::<()>(); + let (finish_sender, finish_receiver) = channel::<()>(); + let (outcome_sender, outcome_receiver) = channel(); + + let host = spawn_host(host_control, move |mut notifications, _shutdown| { + let runtime = Arc::new(ReadinessPublisherRuntime::new()); + let publishing = Arc::clone(&runtime); + let publisher = std::thread::spawn(move || publishing.run(&mut notifications)); + + // The local endpoint drains nothing until it sees this signal, so the + // ring fills and the publisher blocks while the source runs to + // completion. + for handle in 0..OVERSUBSCRIBED_OBJECT_COUNT { + runtime + .publish(ObjectHandle(handle), ReadinessFlags::READ) + .unwrap(); + } + published_sender.send(()).unwrap(); + + let _ = finish_receiver.recv(); + runtime.close(); + outcome_sender.send(publisher.join().unwrap()).unwrap(); + }); + + let (local, notifications) = negotiate_local(local_control); + published_receiver.recv_timeout(TEST_TIMEOUT).unwrap(); + + // Draining on a helper thread keeps an update that is wrongly coalesced + // away a test failure rather than a hang, because only this thread can + // release the host closure. + let (drained_sender, drained_receiver) = channel(); + let drain = std::thread::spawn(move || { + let mut notifications = notifications; + let drained: Vec<_> = (0..OVERSUBSCRIBED_OBJECT_COUNT) + .map(|_| readiness_of(notifications.recv_notification().unwrap())) + .collect(); + drained_sender.send(drained).unwrap(); + notifications + }); + let drained = drained_receiver + .recv_timeout(TEST_TIMEOUT) + .expect("a full ring must still deliver every published update"); + let _notifications = drain.join().unwrap(); + + for (handle, notification) in drained.into_iter().enumerate() { + assert_eq!( + notification, + ReadinessNotification { + handle: ObjectHandle(handle as u64), + readiness: ReadinessFlags::READ, + } + ); + } + + drop(finish_sender); + drop(local); + expect_publication_ended(&outcome_receiver); + host.join().unwrap(); +} + +#[test] +fn a_clean_local_close_ends_a_publisher_blocked_on_a_full_ring() { + let (local_control, host_control) = UnixStream::pair().unwrap(); + let (published_sender, published_receiver) = channel::<()>(); + let (outcome_sender, outcome_receiver) = channel(); + + let host = spawn_host(host_control, move |mut notifications, _shutdown| { + let runtime = Arc::new(ReadinessPublisherRuntime::new()); + let publishing = Arc::clone(&runtime); + let publisher = std::thread::spawn(move || publishing.run(&mut notifications)); + + for handle in 0..OVERSUBSCRIBED_OBJECT_COUNT { + runtime + .publish(ObjectHandle(handle), ReadinessFlags::READ) + .unwrap(); + } + published_sender.send(()).unwrap(); + + // The publisher is blocked on notification-ring capacity that the local + // endpoint will never make available. Only teardown can end it. + outcome_sender.send(publisher.join().unwrap()).unwrap(); + }); + + let (local, notifications) = negotiate_local(local_control); + published_receiver.recv_timeout(TEST_TIMEOUT).unwrap(); + drop(notifications); + drop(local); + + let outcome = outcome_receiver.recv_timeout(TEST_TIMEOUT).unwrap(); + let error = outcome.expect_err("a closed association must end the blocked publisher"); + assert_eq!( + error.kind(), + std::io::ErrorKind::BrokenPipe, + "a closed peer must end publication as a closure rather than a transport failure, got {error:?}" + ); + host.join().unwrap(); +} From 3f38fc83097908589fdea2852389e59e4a7d0713 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sun, 26 Jul 2026 19:02:05 -0700 Subject: [PATCH 133/319] Split generic and Linux broker transports (#1092) Keep peer-visible contracts in `litebox_broker_protocol`, move runtime channel/shared-memory interfaces and the portable control ring into an unconditionally `no_std` `litebox_broker_transport`, and isolate memfd, futex, Unix framing, authentication, liveness, and both local and host endpoints in `litebox_broker_transport_linux_userland`. The existing Linux runner and broker executable select that concrete transport, while generic transport now builds unchanged for kernel targets. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- .github/workflows/ci.yml | 9 +- Cargo.lock | 14 + Cargo.toml | 2 + dev_tests/src/ratchet.rs | 3 +- litebox/Cargo.toml | 1 + litebox/src/broker/mod.rs | 10 +- litebox/src/broker/shared_buffer.rs | 2 +- litebox/src/event/counter.rs | 10 +- litebox/src/litebox.rs | 2 +- litebox/src/pipes.rs | 10 +- litebox_broker_host/Cargo.toml | 1 + litebox_broker_host/src/lib.rs | 32 +- litebox_broker_host/src/readiness.rs | 2 +- litebox_broker_local/Cargo.toml | 1 + litebox_broker_local/src/event.rs | 2 +- litebox_broker_local/src/lib.rs | 39 +- litebox_broker_local/src/pipe.rs | 12 +- litebox_broker_protocol/src/lib.rs | 14 +- litebox_broker_protocol/src/pipe.rs | 9 +- litebox_broker_protocol/src/shared_buffer.rs | 171 ++ litebox_broker_protocol/src/shared_memory.rs | 389 ---- litebox_broker_protocol/src/wire.rs | 2 +- litebox_broker_protocol/src/wire/pipe.rs | 2 +- litebox_broker_transport/Cargo.toml | 16 +- .../src/channel.rs | 9 +- litebox_broker_transport/src/control_ring.rs | 398 +++- litebox_broker_transport/src/lib.rs | 35 +- litebox_broker_transport/src/shared_memory.rs | 989 ++------- litebox_broker_transport/src/unix_socket.rs | 1970 ----------------- .../Cargo.toml | 15 + .../src/lib.rs | 28 + .../src/memfd.rs | 1068 +++++++++ .../src/setup.rs | 191 ++ .../src/unix_io.rs | 2 +- .../src/unix_socket/host.rs | 996 +++++++++ .../src/unix_socket/local.rs | 1174 ++++++++++ .../src/unix_socket/mod.rs | 27 + litebox_broker_userland/Cargo.toml | 3 +- litebox_broker_userland/src/lib.rs | 10 +- litebox_broker_userland/src/main.rs | 19 +- litebox_broker_userland/src/readiness.rs | 2 +- .../tests/notification_runtime.rs | 17 +- .../tests/userland_broker.rs | 4 +- litebox_platform_linux_userland/src/lib.rs | 131 +- litebox_runner_linux_userland/Cargo.toml | 3 +- litebox_runner_linux_userland/src/broker.rs | 97 +- litebox_runner_linux_userland/src/lib.rs | 18 +- litebox_runner_linux_userland/tests/run.rs | 25 +- 48 files changed, 4532 insertions(+), 3454 deletions(-) create mode 100644 litebox_broker_protocol/src/shared_buffer.rs delete mode 100644 litebox_broker_protocol/src/shared_memory.rs rename {litebox_broker_protocol => litebox_broker_transport}/src/channel.rs (92%) delete mode 100644 litebox_broker_transport/src/unix_socket.rs create mode 100644 litebox_broker_transport_linux_userland/Cargo.toml create mode 100644 litebox_broker_transport_linux_userland/src/lib.rs create mode 100644 litebox_broker_transport_linux_userland/src/memfd.rs create mode 100644 litebox_broker_transport_linux_userland/src/setup.rs rename {litebox_broker_transport => litebox_broker_transport_linux_userland}/src/unix_io.rs (96%) create mode 100644 litebox_broker_transport_linux_userland/src/unix_socket/host.rs create mode 100644 litebox_broker_transport_linux_userland/src/unix_socket/local.rs create mode 100644 litebox_broker_transport_linux_userland/src/unix_socket/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a9fc78953..355f42b9da 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -255,9 +255,10 @@ jobs: # - `litebox_platform_windows_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # - # - `litebox_broker_transport` is allowed to have `std` access, - # since it owns hosted concrete broker transport implementations, - # including the current Unix-domain-socket control channel. + # - `litebox_broker_transport_linux_userland` is allowed to have + # `std` access, since it is the Linux-userland binding that owns + # both the local and the broker/host endpoints of the current + # Unix-domain-socket control channel. # # - `litebox_broker_userland` is allowed to have `std` access, # since it is the hosted userland broker executable. @@ -317,7 +318,7 @@ jobs: # can safely use std. find . -type f -name 'Cargo.toml' \ -not -path './Cargo.toml' \ - -not -path './litebox_broker_transport/Cargo.toml' \ + -not -path './litebox_broker_transport_linux_userland/Cargo.toml' \ -not -path './litebox_broker_userland/Cargo.toml' \ -not -path './litebox_platform_linux_userland/Cargo.toml' \ -not -path './litebox_platform_windows_userland/Cargo.toml' \ diff --git a/Cargo.lock b/Cargo.lock index 0d93506717..b021700c13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1441,6 +1441,7 @@ dependencies = [ "hashbrown", "litebox_broker_local", "litebox_broker_protocol", + "litebox_broker_transport", "litebox_util_log", "rangemap", "ringbuf", @@ -1473,6 +1474,7 @@ dependencies = [ "hashbrown", "litebox_broker_core", "litebox_broker_protocol", + "litebox_broker_transport", "spin 0.9.8", "thiserror", ] @@ -1482,6 +1484,7 @@ name = "litebox_broker_local" version = "0.1.0" dependencies = [ "litebox_broker_protocol", + "litebox_broker_transport", "thiserror", ] @@ -1495,9 +1498,18 @@ dependencies = [ [[package]] name = "litebox_broker_transport" version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", + "thiserror", +] + +[[package]] +name = "litebox_broker_transport_linux_userland" +version = "0.1.0" dependencies = [ "libc", "litebox_broker_protocol", + "litebox_broker_transport", "rustix", ] @@ -1511,6 +1523,7 @@ dependencies = [ "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", + "litebox_broker_transport_linux_userland", "tempfile", ] @@ -1704,6 +1717,7 @@ dependencies = [ "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", + "litebox_broker_transport_linux_userland", "litebox_common_linux", "litebox_platform_linux_userland", "litebox_shim_linux", diff --git a/Cargo.toml b/Cargo.toml index 7556df176e..e301e1634b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "litebox_broker_protocol", "litebox_broker_host", "litebox_broker_transport", + "litebox_broker_transport_linux_userland", "litebox_broker_userland", "litebox_common_linux", "litebox_common_windows", @@ -42,6 +43,7 @@ default-members = [ "litebox_broker_protocol", "litebox_broker_host", "litebox_broker_transport", + "litebox_broker_transport_linux_userland", "litebox_broker_userland", "litebox_common_linux", "litebox_common_windows", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 63320c5ec0..1e605978c3 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -35,6 +35,7 @@ fn ratchet_globals() -> Result<()> { &[ ("dev_bench/", 1), ("litebox_broker_core/", 1), + ("litebox_broker_transport_linux_userland/", 1), ("litebox/", 9), ("litebox_platform_linux_kernel/", 6), ("litebox_platform_linux_userland/", 5), @@ -72,7 +73,7 @@ fn ratchet_maybe_uninit() -> Result<()> { &[ ("dev_tests/", 1), ("litebox/", 1), - ("litebox_broker_transport/", 3), + ("litebox_broker_transport_linux_userland/", 3), ("litebox_platform_linux_userland/", 2), ], |file| { diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 610efd9fa8..5898dc9ce5 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -22,6 +22,7 @@ slabmalloc = { git = "https://github.com/gz/rust-slabmalloc.git", rev = "19480b2 litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } +litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.60.2", features = [ diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index d4ca737b5e..72dd93b924 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -9,11 +9,11 @@ use alloc::{ use hashbrown::HashMap; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::LocalCallChannel; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode}; use litebox_broker_protocol::pipe::{CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE}; use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_transport::channel::LocalCallChannel; use crate::event::{Events, polling::Pollee}; use crate::platform::TimeProvider; @@ -308,16 +308,16 @@ mod tests { use std::time::Duration; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; - use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, PipeRequest, PipeResponse, }; use litebox_broker_protocol::pipe::{ReadPipeResponse, WritePipeResponse}; - use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, SharedBufferDescriptor, SharedMemory, - SharedMemoryError, + use litebox_broker_protocol::shared_buffer::{ + SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, SharedBufferDescriptor, }; + use litebox_broker_transport::channel::{LocalCallChannel, LocalSetupChannel}; + use litebox_broker_transport::shared_memory::{SharedMemory, SharedMemoryError}; use crate::platform::mock::MockPlatform; diff --git a/litebox/src/broker/shared_buffer.rs b/litebox/src/broker/shared_buffer.rs index c4361f0e73..94dda307a9 100644 --- a/litebox/src/broker/shared_buffer.rs +++ b/litebox/src/broker/shared_buffer.rs @@ -5,7 +5,7 @@ use alloc::collections::VecDeque; use alloc::sync::Arc; use core::sync::atomic::Ordering::{Acquire, Release}; -use litebox_broker_protocol::shared_memory::{ +use litebox_broker_protocol::shared_buffer::{ SHARED_BUFFER_SLOT_COUNT, SharedBufferDescriptor, SharedBufferSlotIndex, }; diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 17b11b13d5..f16025454a 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -178,7 +178,6 @@ mod tests { use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; - use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{CreateEventResponse, EventConsumption}; use litebox_broker_protocol::message::{ @@ -187,6 +186,7 @@ mod tests { ReadinessNotification, }; use litebox_broker_protocol::readiness::ReadinessFlags; + use litebox_broker_transport::channel::{LocalCallChannel, LocalSetupChannel}; use super::*; use crate::LiteBox; @@ -410,16 +410,16 @@ mod tests { struct NoopSharedMemory; - impl litebox_broker_protocol::shared_memory::SharedMemory for NoopSharedMemory { + impl litebox_broker_transport::shared_memory::SharedMemory for NoopSharedMemory { fn len(&self) -> usize { - litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE + litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE } fn read( &self, _offset: usize, destination: &mut [u8], - ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + ) -> core::result::Result<(), litebox_broker_transport::shared_memory::SharedMemoryError> { destination.fill(0); Ok(()) @@ -429,7 +429,7 @@ mod tests { &self, _offset: usize, _source: &[u8], - ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + ) -> core::result::Result<(), litebox_broker_transport::shared_memory::SharedMemoryError> { Ok(()) } diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 989364956e..b58831b3f2 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -6,8 +6,8 @@ use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::channel::LocalCallChannel; use litebox_broker_protocol::message::BrokerNotification; +use litebox_broker_transport::channel::LocalCallChannel; use crate::{ broker, diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index d49359203c..41bafef6a0 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -928,7 +928,6 @@ mod tests { use alloc::sync::Arc; use litebox_broker_local::BrokerLocal; - use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, @@ -937,6 +936,7 @@ mod tests { use litebox_broker_protocol::pipe::CreatePipeResponse; use litebox_broker_protocol::readiness::ReadinessFlags; use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; + use litebox_broker_transport::channel::{LocalCallChannel, LocalSetupChannel}; use crate::{ event::{Events, observer::Observer, wait::WaitState}, @@ -1117,16 +1117,16 @@ mod tests { #[derive(Clone, Copy)] struct NoopSharedMemory; - impl litebox_broker_protocol::shared_memory::SharedMemory for NoopSharedMemory { + impl litebox_broker_transport::shared_memory::SharedMemory for NoopSharedMemory { fn len(&self) -> usize { - litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE + litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE } fn read( &self, _offset: usize, destination: &mut [u8], - ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + ) -> core::result::Result<(), litebox_broker_transport::shared_memory::SharedMemoryError> { destination.fill(0); Ok(()) @@ -1136,7 +1136,7 @@ mod tests { &self, _offset: usize, _source: &[u8], - ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + ) -> core::result::Result<(), litebox_broker_transport::shared_memory::SharedMemoryError> { Ok(()) } diff --git a/litebox_broker_host/Cargo.toml b/litebox_broker_host/Cargo.toml index b046ec1acf..baa3e6f58f 100644 --- a/litebox_broker_host/Cargo.toml +++ b/litebox_broker_host/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" hashbrown = "0.15.2" litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } spin = { version = "0.9.8", default-features = false, features = ["spin_mutex"] } thiserror = { version = "2.0.6", default-features = false } diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 48c8c7f8c9..c746db0bf2 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -1,11 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Channel-neutral broker-side protocol/core adapter. +//! Portable host endpoint for broker associations. //! -//! This crate wires `litebox_broker_core` to any implementation of the neutral -//! host-side control-channel trait. Concrete channels live in separate crates such as -//! `litebox_broker_transport`. +//! This crate is the trusted counterpart to `litebox_broker_local`. The local +//! endpoint turns in-sandbox object operations into broker requests; this host +//! endpoint authenticates the peer during association setup, creates its +//! `litebox_broker_core` session, validates its shared-buffer use, dispatches +//! requests to the core, and returns correlated responses. It also coordinates +//! broker-to-local readiness notifications. +//! +//! The endpoint is channel-neutral. Deployments provide host channels through +//! `litebox_broker_transport`; concrete bindings such as +//! `litebox_broker_transport_linux_userland` decide how messages move. #![no_std] @@ -17,7 +24,6 @@ extern crate std; use alloc::vec::Vec; use litebox_broker_core::{BrokerCore, BrokerSession, CallerCredential}; -use litebox_broker_protocol::channel::{HostReceive, HostSetupChannel, PeerCredential}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse}; use litebox_broker_protocol::message::{ @@ -27,11 +33,12 @@ use litebox_broker_protocol::message::{ use litebox_broker_protocol::pipe::{ CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE, ReadPipeResponse, WritePipeResponse, }; -use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_LAYOUT, SHARED_BUFFER_SLOT_COUNT, SharedBufferDescriptor, SharedBufferPool, - SharedBufferSlotIndex, SharedMemory, +use litebox_broker_protocol::shared_buffer::{ + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_SLOT_COUNT, SharedBufferDescriptor, SharedBufferSlotIndex, }; use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, RequestId}; +use litebox_broker_transport::channel::{HostReceive, HostSetupChannel, PeerCredential}; +use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory}; use spin::mutex::SpinMutex; mod error; @@ -230,7 +237,7 @@ impl SharedBufferUsage { &mut self, request_id: RequestId, descriptor: SharedBufferDescriptor, - layout: litebox_broker_protocol::shared_memory::SharedBufferLayout, + layout: litebox_broker_protocol::shared_buffer::SharedBufferLayout, ) -> core::result::Result<(), ErrorCode> { if layout .range(descriptor.slot_index, descriptor.length as usize) @@ -398,11 +405,12 @@ mod tests { }; use litebox_broker_protocol::message::BrokerHandshakeRequest; use litebox_broker_protocol::pipe::{CreatePipeRequest, ReadPipeRequest, WritePipeRequest}; - use litebox_broker_protocol::shared_memory::{ + use litebox_broker_protocol::shared_buffer::{ SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, - SharedBufferDescriptor, SharedBufferPool, SharedMemoryError, + SharedBufferDescriptor, }; use litebox_broker_protocol::{ObjectHandle, ProtocolVersion, RequestId}; + use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemoryError}; use std::sync::{Arc, Condvar, Mutex, mpsc}; use std::time::Duration; @@ -701,7 +709,7 @@ mod tests { }))]), std::vec::Vec::new(), ); - let incompatible_layout = litebox_broker_protocol::shared_memory::SharedBufferLayout::new( + let incompatible_layout = litebox_broker_protocol::shared_buffer::SharedBufferLayout::new( u32::try_from(SHARED_BUFFER_POOL_SIZE).unwrap(), 1, ) diff --git a/litebox_broker_host/src/readiness.rs b/litebox_broker_host/src/readiness.rs index 0b2aa0269a..d0a51b5449 100644 --- a/litebox_broker_host/src/readiness.rs +++ b/litebox_broker_host/src/readiness.rs @@ -33,9 +33,9 @@ use alloc::collections::VecDeque; use hashbrown::HashMap; use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::HostNotificationChannel; use litebox_broker_protocol::message::{BrokerNotification, ReadinessNotification}; use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_transport::channel::HostNotificationChannel; use spin::mutex::SpinMutex; use thiserror::Error; diff --git a/litebox_broker_local/Cargo.toml b/litebox_broker_local/Cargo.toml index 40d00c9ce9..40256f00b9 100644 --- a/litebox_broker_local/Cargo.toml +++ b/litebox_broker_local/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } thiserror = { version = "2.0.6", default-features = false } [lints] diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index da42351e78..b2b0d30c32 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -2,7 +2,6 @@ // Licensed under the MIT license. use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::LocalCallChannel; use litebox_broker_protocol::event::{ AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CreateEventRequest, EventConsumeMode, @@ -11,6 +10,7 @@ use litebox_broker_protocol::message::{ BrokerOperation, BrokerResult, EventRequest, EventResponse, }; use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_transport::channel::LocalCallChannel; use crate::{BrokerLocal, BrokerLocalError, Result}; diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 0165a42a54..636cd9d358 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -1,15 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Typed broker-local adapters for broker requests and notifications. +//! Portable local endpoint for broker associations. //! -//! The local control adapter owns request identifiers but does not own transport -//! sequencing. Userland, kernel, or ring-buffer deployments provide channels by -//! implementing [`litebox_broker_protocol::channel::LocalSetupChannel`] for -//! association setup and -//! [`litebox_broker_protocol::channel::LocalCallChannel`] for active calls. -//! Notification receive adapters are intentionally separate so active control -//! requests remain strictly paired with their responses. +//! This crate is the in-sandbox counterpart to `litebox_broker_host`. It +//! negotiates an association, turns typed object operations into broker +//! requests, manages access to the association's shared buffers, assigns request +//! identifiers, and verifies that responses are correctly correlated. A +//! separate notification adapter receives broker-to-local readiness updates. +//! +//! The endpoint is channel-neutral. Deployments provide local channels through +//! `litebox_broker_transport`; concrete bindings such as +//! `litebox_broker_transport_linux_userland` decide how messages move. #![no_std] @@ -25,19 +27,18 @@ mod pipe; use alloc::sync::Arc; use core::sync::atomic::{AtomicU64, Ordering}; -use litebox_broker_protocol::channel::{ - LocalCallChannel, LocalNotificationChannel, LocalSetupChannel, -}; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, }; use litebox_broker_protocol::readiness::ReadinessFlags; -use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_LAYOUT, SharedBufferPool, SharedMemory, -}; +use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_LAYOUT; use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle, RequestId}; +use litebox_broker_transport::channel::{ + LocalCallChannel, LocalNotificationChannel, LocalSetupChannel, +}; +use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory}; pub use error::{BrokerLocalError, Result}; @@ -240,9 +241,9 @@ mod tests { use core::convert::Infallible; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::ProtocolVersion; - use litebox_broker_protocol::channel::LocalNotificationChannel; use litebox_broker_protocol::message::ReadinessNotification; use litebox_broker_protocol::readiness::ReadinessFlags; + use litebox_broker_transport::channel::LocalNotificationChannel; use std::sync::Mutex; #[test] @@ -530,7 +531,7 @@ mod tests { Ok(( channel, Arc::new(NoopSharedMemory { - length: litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE - 1, + length: litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE - 1, }) as Arc, (), )) @@ -578,7 +579,7 @@ mod tests { &self, _offset: usize, destination: &mut [u8], - ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + ) -> core::result::Result<(), litebox_broker_transport::shared_memory::SharedMemoryError> { destination.fill(0); Ok(()) @@ -588,7 +589,7 @@ mod tests { &self, _offset: usize, _source: &[u8], - ) -> core::result::Result<(), litebox_broker_protocol::shared_memory::SharedMemoryError> + ) -> core::result::Result<(), litebox_broker_transport::shared_memory::SharedMemoryError> { Ok(()) } @@ -596,7 +597,7 @@ mod tests { fn noop_shared_memory() -> Arc { Arc::new(NoopSharedMemory { - length: litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE, + length: litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE, }) } diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs index 10eed91915..1b00071135 100644 --- a/litebox_broker_local/src/pipe.rs +++ b/litebox_broker_local/src/pipe.rs @@ -2,13 +2,13 @@ // Licensed under the MIT license. use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::LocalCallChannel; use litebox_broker_protocol::message::{BrokerOperation, BrokerResult, PipeRequest, PipeResponse}; use litebox_broker_protocol::pipe::{ CreatePipeRequest, CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE, ReadPipeRequest, WritePipeRequest, }; -use litebox_broker_protocol::shared_memory::SharedBufferDescriptor; +use litebox_broker_protocol::shared_buffer::SharedBufferDescriptor; +use litebox_broker_transport::channel::LocalCallChannel; use crate::{BrokerLocal, BrokerLocalError, Result}; @@ -146,16 +146,16 @@ mod tests { use std::sync::Mutex; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; - use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, }; use litebox_broker_protocol::pipe::{ReadPipeResponse, WritePipeResponse}; - use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, SharedBufferSlotIndex, SharedMemory, - SharedMemoryError, + use litebox_broker_protocol::shared_buffer::{ + SHARED_BUFFER_POOL_SIZE, SHARED_BUFFER_SLOT_SIZE, SharedBufferSlotIndex, }; + use litebox_broker_transport::channel::{LocalCallChannel, LocalSetupChannel}; + use litebox_broker_transport::shared_memory::{SharedMemory, SharedMemoryError}; #[test] fn pipe_uses_the_descriptor_slot_for_data_operations() { diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 6c6d1bb700..e0a17ebd0b 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Shared broker protocol types and channel contracts. +//! Shared broker protocol contracts. //! -//! This crate describes broker-visible opaque handles, errors, versions, -//! request/response messages, and the transport-neutral control-channel -//! contracts used to carry them. It does not know whether messages move over -//! Unix sockets, shared rings, kernel traps, or another IPC mechanism. +//! This crate describes what broker peers agree on: opaque handles, errors, +//! versions, handshake/request/response/notification messages, the shared-buffer +//! layout those messages reference, and the wire codecs that encode them. It +//! does not describe how messages move; runtime channel and shared-memory +//! interfaces live in `litebox_broker_transport`. #![no_std] @@ -15,13 +16,12 @@ extern crate alloc; #[cfg(test)] extern crate std; -pub mod channel; pub mod error; pub mod event; pub mod message; pub mod pipe; pub mod readiness; -pub mod shared_memory; +pub mod shared_buffer; pub mod wire; /// Opaque broker object reference handle. diff --git a/litebox_broker_protocol/src/pipe.rs b/litebox_broker_protocol/src/pipe.rs index 81dae150bc..ad17216792 100644 --- a/litebox_broker_protocol/src/pipe.rs +++ b/litebox_broker_protocol/src/pipe.rs @@ -2,13 +2,14 @@ // Licensed under the MIT license. use crate::ObjectHandle; -use crate::shared_memory::SharedBufferDescriptor; +use crate::shared_buffer::{SHARED_BUFFER_SLOT_SIZE, SharedBufferDescriptor}; /// Maximum pipe bytes transferred by one broker request. /// -/// Each association shared-buffer slot has this size. Larger blocking writes -/// are split across requests, while reads may return at most this amount. -pub const MAX_PIPE_TRANSFER_SIZE: u32 = 32 * 1024; +/// One transfer occupies at most one association shared-buffer slot. Larger +/// blocking writes are split across requests, while reads may return at most +/// this amount. +pub const MAX_PIPE_TRANSFER_SIZE: u32 = SHARED_BUFFER_SLOT_SIZE; /// Request to create a broker-owned byte pipe. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/litebox_broker_protocol/src/shared_buffer.rs b/litebox_broker_protocol/src/shared_buffer.rs new file mode 100644 index 0000000000..5004b4fc73 --- /dev/null +++ b/litebox_broker_protocol/src/shared_buffer.rs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Peer-visible association shared-buffer layout. +//! +//! Both peers agree on this fixed-slot layout before any payload moves, so the +//! slot geometry and the descriptors that name one slot are part of the +//! protocol contract. Attaching real memory to the layout and copying bytes +//! through it are runtime transport concerns that live in +//! `litebox_broker_transport`. + +use core::ops::Range; + +use thiserror::Error; + +/// Size of each association shared-buffer slot. +pub const SHARED_BUFFER_SLOT_SIZE: u32 = 32 * 1024; + +/// Number of slots in one association shared-buffer pool. +pub const SHARED_BUFFER_SLOT_COUNT: u32 = 16; + +/// Fixed layout of one association shared-buffer pool. +pub const SHARED_BUFFER_LAYOUT: SharedBufferLayout = + match SharedBufferLayout::new(SHARED_BUFFER_SLOT_SIZE, SHARED_BUFFER_SLOT_COUNT) { + Ok(layout) => layout, + Err(_) => panic!("broker shared-buffer constants must form a valid layout"), + }; + +/// Exact shared-memory size required for one association shared-buffer pool. +pub const SHARED_BUFFER_POOL_SIZE: usize = SHARED_BUFFER_LAYOUT.total_len(); + +/// Error validating a fixed-slot shared-buffer layout or one of its ranges. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum SharedBufferLayoutError { + /// The layout has no slots, has empty slots, or exceeds the addressable range. + #[error("invalid shared-buffer layout")] + InvalidLayout, + /// The requested slot does not exist in the layout. + #[error("shared-buffer slot is out of bounds")] + InvalidSlot, + /// The requested byte range does not fit in one slot. + #[error("shared-buffer range exceeds the slot size")] + RangeExceedsSlot, +} + +/// Immutable fixed-slot layout for an association shared-buffer pool. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SharedBufferLayout { + slot_size: u32, + slot_count: u32, + total_len: usize, +} + +impl SharedBufferLayout { + /// Creates a checked fixed-slot layout. + pub const fn new(slot_size: u32, slot_count: u32) -> Result { + if slot_size == 0 || slot_count == 0 { + return Err(SharedBufferLayoutError::InvalidLayout); + } + let Some(total_len) = (slot_size as usize).checked_mul(slot_count as usize) else { + return Err(SharedBufferLayoutError::InvalidLayout); + }; + if total_len > isize::MAX as usize { + return Err(SharedBufferLayoutError::InvalidLayout); + } + Ok(Self { + slot_size, + slot_count, + total_len, + }) + } + + /// Returns the size of each slot in bytes. + pub const fn slot_size(self) -> u32 { + self.slot_size + } + + /// Returns the number of slots. + pub const fn slot_count(self) -> u32 { + self.slot_count + } + + /// Returns the exact backing-memory length required by this layout. + pub const fn total_len(self) -> usize { + self.total_len + } + + /// Returns the shared-memory range for a prefix of one slot. + pub fn range( + self, + slot: SharedBufferSlotIndex, + length: usize, + ) -> Result, SharedBufferLayoutError> { + if slot.0 >= self.slot_count { + return Err(SharedBufferLayoutError::InvalidSlot); + } + if length > self.slot_size as usize { + return Err(SharedBufferLayoutError::RangeExceedsSlot); + } + let offset = (slot.0 as usize) + .checked_mul(self.slot_size as usize) + .ok_or(SharedBufferLayoutError::InvalidLayout)?; + let end = offset + .checked_add(length) + .ok_or(SharedBufferLayoutError::RangeExceedsSlot)?; + Ok(offset..end) + } +} + +/// Index of one fixed shared-buffer slot. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SharedBufferSlotIndex(pub u32); + +/// Identifies one operation-scoped region in the association shared-buffer pool. +/// +/// The slot offset is derived from the trusted association layout and is never +/// supplied by the peer. The request variant determines the transfer direction. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SharedBufferDescriptor { + /// Slot used by this operation. + pub slot_index: SharedBufferSlotIndex, + /// Number of bytes used from the start of the slot. + pub length: u32, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn association_layout_has_expected_size() { + assert_eq!(SHARED_BUFFER_LAYOUT.slot_size(), 32 * 1024); + assert_eq!(SHARED_BUFFER_LAYOUT.slot_count(), 16); + assert_eq!(SHARED_BUFFER_POOL_SIZE, 512 * 1024); + } + + #[test] + fn layout_rejects_empty_and_overflowing_configurations() { + assert_eq!( + SharedBufferLayout::new(0, 1), + Err(SharedBufferLayoutError::InvalidLayout) + ); + assert_eq!( + SharedBufferLayout::new(1, 0), + Err(SharedBufferLayoutError::InvalidLayout) + ); + assert_eq!( + SharedBufferLayout::new(u32::MAX, u32::MAX), + Err(SharedBufferLayoutError::InvalidLayout) + ); + } + + #[test] + fn layout_derives_disjoint_slot_ranges() { + let layout = SharedBufferLayout::new(8, 3).unwrap(); + + assert_eq!(layout.range(SharedBufferSlotIndex(0), 8), Ok(0..8)); + assert_eq!(layout.range(SharedBufferSlotIndex(1), 8), Ok(8..16)); + assert_eq!(layout.range(SharedBufferSlotIndex(2), 8), Ok(16..24)); + assert_eq!( + layout.range(SharedBufferSlotIndex(3), 0), + Err(SharedBufferLayoutError::InvalidSlot) + ); + assert_eq!( + layout.range(SharedBufferSlotIndex(0), 9), + Err(SharedBufferLayoutError::RangeExceedsSlot) + ); + } +} diff --git a/litebox_broker_protocol/src/shared_memory.rs b/litebox_broker_protocol/src/shared_memory.rs deleted file mode 100644 index eaf243aca9..0000000000 --- a/litebox_broker_protocol/src/shared_memory.rs +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Transport-neutral shared-memory resources. - -use alloc::sync::Arc; -use core::ops::Range; - -use thiserror::Error; - -use crate::pipe::MAX_PIPE_TRANSFER_SIZE; - -/// Size of each association shared-buffer slot. -pub const SHARED_BUFFER_SLOT_SIZE: u32 = MAX_PIPE_TRANSFER_SIZE; - -/// Number of slots in one association shared-buffer pool. -pub const SHARED_BUFFER_SLOT_COUNT: u32 = 16; - -/// Fixed layout of one association shared-buffer pool. -pub const SHARED_BUFFER_LAYOUT: SharedBufferLayout = - match SharedBufferLayout::new(SHARED_BUFFER_SLOT_SIZE, SHARED_BUFFER_SLOT_COUNT) { - Ok(layout) => layout, - Err(_) => panic!("broker shared-buffer constants must form a valid layout"), - }; - -/// Exact shared-memory size required for one association shared-buffer pool. -pub const SHARED_BUFFER_POOL_SIZE: usize = SHARED_BUFFER_LAYOUT.total_len(); - -/// Error accessing a shared-memory resource. -#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum SharedMemoryError { - /// The requested byte range is outside the shared-memory resource. - #[error("shared-memory range is out of bounds")] - InvalidRange, - /// An atomic access is not naturally aligned. - #[error("shared-memory atomic access is not naturally aligned")] - UnalignedAtomic, -} - -/// Byte-copy access to a shared-memory resource. -/// -/// A value may own a distinct shared-memory object or identify a region in a -/// larger shared-memory resource. Each endpoint has its own implementation, and -/// peers may use different implementation types, such as user and kernel -/// mappings of the same physical memory. Implementations must keep the backing -/// resource alive and make concurrent local calls safe without exposing Rust -/// references into memory writable by the peer. -/// -/// Establishing the shared resource and coordinating access between endpoints -/// are responsibilities of the deployment and protocol using the shared -/// memory. -pub trait SharedMemory: Send + Sync + 'static { - /// Returns the mapped resource length in bytes. - /// - /// The length must remain stable for the lifetime of the resource. - fn len(&self) -> usize; - - /// Returns whether the resource is empty. - fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Copies bytes from shared memory into `destination`. - fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError>; - - /// Copies bytes from `source` into shared memory. - fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError>; -} - -/// Ordered atomic access to shared-memory synchronization values. -/// -/// Implementations must provide naturally aligned, indivisible, system-visible -/// operations over coherent shared memory. Atomic values must not also be -/// accessed through [`SharedMemory::read`] or [`SharedMemory::write`] by a -/// conforming endpoint. -pub trait AtomicSharedMemory: SharedMemory { - /// Atomically loads a naturally aligned native-endian `u32` with acquire - /// ordering. - fn load_u32_acquire(&self, offset: usize) -> Result; - - /// Atomically increments a naturally aligned native-endian `u32` with - /// release ordering and returns its previous value. - fn fetch_add_u32_release(&self, offset: usize, value: u32) -> Result; - - /// Atomically loads a naturally aligned native-endian `u64` with acquire - /// ordering. - fn load_u64_acquire(&self, offset: usize) -> Result; - - /// Atomically stores a naturally aligned native-endian `u64` with release - /// ordering. - /// - /// On error, the value must not have been stored. - fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError>; - - /// Atomically release-stores a native-endian `u64`, then release-adds to a - /// native-endian `u32`, returning the previous `u32`. - /// - /// Both values must be naturally aligned and occupy non-overlapping ranges. - /// Implementations must validate both accesses before storing either value. - /// On error, neither value may have been modified. - fn store_u64_and_fetch_add_u32_release( - &self, - store_offset: usize, - value: u64, - add_offset: usize, - add_value: u32, - ) -> Result; -} - -impl SharedMemory for Arc { - fn len(&self) -> usize { - (**self).len() - } - - fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { - (**self).read(offset, destination) - } - - fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { - (**self).write(offset, source) - } -} - -/// Error validating or accessing a fixed-slot shared-buffer pool. -#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum SharedBufferError { - /// The layout has no slots, has empty slots, or exceeds the addressable range. - #[error("invalid shared-buffer layout")] - InvalidLayout, - /// The backing shared-memory length does not exactly match the layout. - #[error("shared-memory length does not match the shared-buffer layout")] - MemoryLengthMismatch, - /// The requested slot does not exist in the layout. - #[error("shared-buffer slot is out of bounds")] - InvalidSlot, - /// The requested byte range does not fit in one slot. - #[error("shared-buffer range exceeds the slot size")] - RangeExceedsSlot, - /// The backing shared-memory access failed. - #[error("shared-memory access failed: {0}")] - SharedMemory(#[from] SharedMemoryError), -} - -/// Immutable fixed-slot layout for an association shared-buffer pool. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct SharedBufferLayout { - slot_size: u32, - slot_count: u32, - total_len: usize, -} - -impl SharedBufferLayout { - /// Creates a checked fixed-slot layout. - pub const fn new(slot_size: u32, slot_count: u32) -> Result { - if slot_size == 0 || slot_count == 0 { - return Err(SharedBufferError::InvalidLayout); - } - let Some(total_len) = (slot_size as usize).checked_mul(slot_count as usize) else { - return Err(SharedBufferError::InvalidLayout); - }; - if total_len > isize::MAX as usize { - return Err(SharedBufferError::InvalidLayout); - } - Ok(Self { - slot_size, - slot_count, - total_len, - }) - } - - /// Returns the size of each slot in bytes. - pub const fn slot_size(self) -> u32 { - self.slot_size - } - - /// Returns the number of slots. - pub const fn slot_count(self) -> u32 { - self.slot_count - } - - /// Returns the exact backing-memory length required by this layout. - pub const fn total_len(self) -> usize { - self.total_len - } - - /// Returns the shared-memory range for a prefix of one slot. - pub fn range( - self, - slot: SharedBufferSlotIndex, - length: usize, - ) -> Result, SharedBufferError> { - if slot.0 >= self.slot_count { - return Err(SharedBufferError::InvalidSlot); - } - if length > self.slot_size as usize { - return Err(SharedBufferError::RangeExceedsSlot); - } - let offset = (slot.0 as usize) - .checked_mul(self.slot_size as usize) - .ok_or(SharedBufferError::InvalidLayout)?; - let end = offset - .checked_add(length) - .ok_or(SharedBufferError::RangeExceedsSlot)?; - Ok(offset..end) - } -} - -/// Index of one fixed shared-buffer slot. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SharedBufferSlotIndex(pub u32); - -/// Identifies one operation-scoped region in the association shared-buffer pool. -/// -/// The slot offset is derived from the trusted association layout and is never -/// supplied by the peer. The request variant determines the transfer direction. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct SharedBufferDescriptor { - /// Slot used by this operation. - pub slot_index: SharedBufferSlotIndex, - /// Number of bytes used from the start of the slot. - pub length: u32, -} - -/// A shared-memory resource viewed as a checked fixed-slot buffer pool. -/// -/// Slot ownership and reuse remain responsibilities of the protocol using the -/// pool. Accessors copy bytes and never expose references into peer-writable -/// memory. -pub struct SharedBufferPool { - memory: Memory, - layout: SharedBufferLayout, -} - -impl SharedBufferPool { - /// Attaches a layout to an exact-size shared-memory resource. - pub fn new(memory: Memory, layout: SharedBufferLayout) -> Result { - if memory.len() != layout.total_len() { - return Err(SharedBufferError::MemoryLengthMismatch); - } - Ok(Self { memory, layout }) - } - - /// Returns the fixed-slot layout. - pub const fn layout(&self) -> SharedBufferLayout { - self.layout - } - - /// Returns the backing shared-memory resource. - pub const fn memory(&self) -> &Memory { - &self.memory - } - - /// Copies bytes from the start of `slot` into `destination`. - pub fn read( - &self, - slot: SharedBufferSlotIndex, - destination: &mut [u8], - ) -> Result<(), SharedBufferError> { - let range = self.layout.range(slot, destination.len())?; - self.memory.read(range.start, destination)?; - Ok(()) - } - - /// Copies `source` into the start of `slot`. - pub fn write( - &self, - slot: SharedBufferSlotIndex, - source: &[u8], - ) -> Result<(), SharedBufferError> { - let range = self.layout.range(slot, source.len())?; - self.memory.write(range.start, source)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloc::vec; - use alloc::vec::Vec; - use std::sync::Mutex; - - #[test] - fn association_layout_has_expected_size() { - assert_eq!(SHARED_BUFFER_LAYOUT.slot_size(), 32 * 1024); - assert_eq!(SHARED_BUFFER_LAYOUT.slot_count(), 16); - assert_eq!(SHARED_BUFFER_POOL_SIZE, 512 * 1024); - } - - #[test] - fn layout_rejects_empty_and_overflowing_configurations() { - assert_eq!( - SharedBufferLayout::new(0, 1), - Err(SharedBufferError::InvalidLayout) - ); - assert_eq!( - SharedBufferLayout::new(1, 0), - Err(SharedBufferError::InvalidLayout) - ); - assert_eq!( - SharedBufferLayout::new(u32::MAX, u32::MAX), - Err(SharedBufferError::InvalidLayout) - ); - } - - #[test] - fn layout_derives_disjoint_slot_ranges() { - let layout = SharedBufferLayout::new(8, 3).unwrap(); - - assert_eq!(layout.range(SharedBufferSlotIndex(0), 8), Ok(0..8)); - assert_eq!(layout.range(SharedBufferSlotIndex(1), 8), Ok(8..16)); - assert_eq!(layout.range(SharedBufferSlotIndex(2), 8), Ok(16..24)); - assert_eq!( - layout.range(SharedBufferSlotIndex(3), 0), - Err(SharedBufferError::InvalidSlot) - ); - assert_eq!( - layout.range(SharedBufferSlotIndex(0), 9), - Err(SharedBufferError::RangeExceedsSlot) - ); - } - - #[test] - fn pool_checks_backing_length_and_slot_boundaries() { - let layout = SharedBufferLayout::new(8, 3).unwrap(); - assert!(matches!( - SharedBufferPool::new(TestSharedMemory::new(23), layout), - Err(SharedBufferError::MemoryLengthMismatch) - )); - let memory = Arc::new(TestSharedMemory::new(layout.total_len())); - let pool = SharedBufferPool::new(Arc::clone(&memory), layout).unwrap(); - - pool.write(SharedBufferSlotIndex(0), &[1, 2, 3]).unwrap(); - pool.write(SharedBufferSlotIndex(2), &[4, 5]).unwrap(); - let mut first = [0; 3]; - pool.read(SharedBufferSlotIndex(0), &mut first).unwrap(); - assert_eq!(first, [1, 2, 3]); - assert_eq!(&memory.bytes()[8..16], &[0; 8]); - assert_eq!( - pool.write(SharedBufferSlotIndex(2), &[0; 9]), - Err(SharedBufferError::RangeExceedsSlot) - ); - } - - struct TestSharedMemory(Mutex>); - - impl TestSharedMemory { - fn new(length: usize) -> Self { - Self(Mutex::new(vec![0; length])) - } - - fn bytes(&self) -> Vec { - self.0.lock().unwrap().clone() - } - } - - impl SharedMemory for TestSharedMemory { - fn len(&self) -> usize { - self.0.lock().unwrap().len() - } - - fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { - let memory = self.0.lock().unwrap(); - let end = offset - .checked_add(destination.len()) - .ok_or(SharedMemoryError::InvalidRange)?; - let source = memory - .get(offset..end) - .ok_or(SharedMemoryError::InvalidRange)?; - destination.copy_from_slice(source); - Ok(()) - } - - fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { - let mut memory = self.0.lock().unwrap(); - let end = offset - .checked_add(source.len()) - .ok_or(SharedMemoryError::InvalidRange)?; - let destination = memory - .get_mut(offset..end) - .ok_or(SharedMemoryError::InvalidRange)?; - destination.copy_from_slice(source); - Ok(()) - } - } -} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 494faff7f1..f48390ee92 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -328,7 +328,7 @@ mod tests { CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, WritePipeResponse, }; - use crate::shared_memory::{SharedBufferDescriptor, SharedBufferSlotIndex}; + use crate::shared_buffer::{SharedBufferDescriptor, SharedBufferSlotIndex}; use crate::{ObjectHandle, ProtocolVersion, RequestId}; const TEST_REQUEST_ID: RequestId = RequestId(0x0102_0304_0506_0708); diff --git a/litebox_broker_protocol/src/wire/pipe.rs b/litebox_broker_protocol/src/wire/pipe.rs index 6ef2520a5b..a09beba030 100644 --- a/litebox_broker_protocol/src/wire/pipe.rs +++ b/litebox_broker_protocol/src/wire/pipe.rs @@ -6,7 +6,7 @@ use crate::pipe::{ CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, WritePipeResponse, }; -use crate::shared_memory::{SharedBufferDescriptor, SharedBufferSlotIndex}; +use crate::shared_buffer::{SharedBufferDescriptor, SharedBufferSlotIndex}; use super::WireError; use super::primitive::{Decoder, Encoder}; diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml index fa423ee624..e67b374568 100644 --- a/litebox_broker_transport/Cargo.toml +++ b/litebox_broker_transport/Cargo.toml @@ -3,23 +3,9 @@ name = "litebox_broker_transport" version = "0.1.0" edition = "2024" -[features] -std = [] -linux-userland = [ - "std", - "dep:libc", - "dep:rustix", - "rustix/fs", - "rustix/net", - "rustix/thread", -] - [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } - -[target.'cfg(target_os = "linux")'.dependencies] -libc = { version = "0.2.177", default-features = false, optional = true } -rustix = { version = "1.1.2", default-features = false, features = ["std"], optional = true } +thiserror = { version = "2.0.6", default-features = false } [lints] workspace = true diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_transport/src/channel.rs similarity index 92% rename from litebox_broker_protocol/src/channel.rs rename to litebox_broker_transport/src/channel.rs index 85750d3636..54f9861685 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_transport/src/channel.rs @@ -1,7 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::message::{ +//! Runtime control-channel contracts for broker associations. +//! +//! These traits describe how an association moves protocol messages between the +//! local endpoint and the broker host. They are transport-neutral: an +//! implementation may use Unix sockets, shared rings, kernel traps, or another +//! IPC mechanism. + +use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, BrokerResponse, }; diff --git a/litebox_broker_transport/src/control_ring.rs b/litebox_broker_transport/src/control_ring.rs index baa0d63564..e8b0a31004 100644 --- a/litebox_broker_transport/src/control_ring.rs +++ b/litebox_broker_transport/src/control_ring.rs @@ -2,20 +2,25 @@ // Licensed under the MIT license. //! Hostile-peer-safe shared control-ring state machines. +//! +//! The memory layout in this module is a transport ABI shared by both +//! control-ring endpoints. Layout changes must also change the versioned +//! activation token so endpoints with incompatible ring layouts fail setup +//! rather than interpreting the same shared memory differently. use alloc::sync::Arc; use core::mem::size_of; use core::ops::Range; -#[cfg(test)] -use core::sync::atomic::fence; +use core::sync::atomic::{Ordering, fence}; -#[cfg(test)] -use litebox_broker_protocol::shared_memory::SharedMemory; -use litebox_broker_protocol::shared_memory::{AtomicSharedMemory, SharedMemoryError}; +use crate::shared_memory::{ControlRingMemory, SharedMemoryError}; /// Size of one shared control-ring slot. pub const CONTROL_RING_SLOT_SIZE: usize = 128; +/// Versioned token exchanged before endpoints activate this control-ring ABI. +pub const CONTROL_RING_READY: &[u8] = b"litebox-control-ring-ready-v1"; + /// Size of the fixed metadata at the start of a control-ring slot. pub const CONTROL_RING_SLOT_HEADER_SIZE: usize = 16; @@ -56,6 +61,81 @@ const PRODUCER_EPOCH_OFFSET: usize = 0; const CONSUMER_EPOCH_OFFSET: usize = 4; const CONSUMER_HEAD_OFFSET: usize = 8; +/// Returns whether the control-ring ABI permits a byte-copy range. +/// +/// Concrete shared-memory implementations use this together with +/// [`memory_permits_u32`] and [`memory_permits_u64`] to keep their own byte and +/// typed-word accesses disjoint. A peer can bypass these checks through its +/// backing-resource alias, so implementations must remain memory-safe under +/// arbitrary peer writes. +pub const fn memory_permits_byte_range(offset: usize, length: usize) -> bool { + let Some(end) = offset.checked_add(length) else { + return false; + }; + if end > CONTROL_RING_MEMORY_SIZE { + return false; + } + if length == 0 { + return true; + } + direction_permits_byte_range(offset, length, 0, CONTROL_RING_SLOT_COUNT) + || direction_permits_byte_range( + offset, + length, + CONTROL_RING_DIRECTION_SIZE, + CONTROL_RING_SLOT_COUNT, + ) + || direction_permits_byte_range( + offset, + length, + CONTROL_RING_DIRECTION_SIZE * 2, + CONTROL_RING_NOTIFICATION_SLOT_COUNT, + ) +} + +/// Returns whether the control-ring ABI permits a `u32` access at `offset`. +pub const fn memory_permits_u32(offset: usize) -> bool { + let Some(relative) = offset.checked_sub(CONTROL_RING_DATA_SIZE) else { + return false; + }; + relative < CONTROL_RING_SYNC_DIRECTION_SIZE * 3 + && matches!( + relative % CONTROL_RING_SYNC_DIRECTION_SIZE, + PRODUCER_EPOCH_OFFSET | CONSUMER_EPOCH_OFFSET + ) +} + +/// Returns whether the control-ring ABI permits a `u64` access at `offset`. +pub const fn memory_permits_u64(offset: usize) -> bool { + if offset < CONTROL_RING_DATA_SIZE { + return offset.is_multiple_of(CONTROL_RING_SLOT_SIZE); + } + let relative = offset - CONTROL_RING_DATA_SIZE; + relative < CONTROL_RING_SYNC_DIRECTION_SIZE * 3 + && relative % CONTROL_RING_SYNC_DIRECTION_SIZE == CONSUMER_HEAD_OFFSET +} + +const fn direction_permits_byte_range( + offset: usize, + length: usize, + direction_start: usize, + slot_count: u64, +) -> bool { + let Some(relative) = offset.checked_sub(direction_start) else { + return false; + }; + #[allow(clippy::cast_possible_truncation)] + let direction_size = CONTROL_RING_SLOT_SIZE * slot_count as usize; + if relative >= direction_size { + return false; + } + let slot_offset = relative % CONTROL_RING_SLOT_SIZE; + let Some(slot_end) = slot_offset.checked_add(length) else { + return false; + }; + slot_offset >= size_of::() && slot_end <= CONTROL_RING_SLOT_SIZE +} + /// One direction in the shared control-ring mapping. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ControlRingDirection { @@ -88,19 +168,19 @@ impl ControlRingDirection { } } - /// Returns the atomic `u32` epoch incremented when the producer publishes - /// work for this direction. + /// Returns the `u32` epoch incremented when the producer publishes work for + /// this direction. pub const fn producer_epoch_offset(self) -> usize { self.sync_offset() + PRODUCER_EPOCH_OFFSET } - /// Returns the atomic `u32` epoch incremented when the consumer publishes - /// progress for this direction. + /// Returns the `u32` epoch incremented when the consumer publishes progress + /// for this direction. pub const fn consumer_epoch_offset(self) -> usize { self.sync_offset() + CONSUMER_EPOCH_OFFSET } - /// Returns the atomic `u64` consumer-head offset for this direction. + /// Returns the `u64` consumer-head offset for this direction. pub const fn consumer_head_offset(self) -> usize { self.sync_offset() + CONSUMER_HEAD_OFFSET } @@ -210,12 +290,12 @@ pub enum ControlRingReadError { } /// Exact-size shared memory containing request, response, and notification rings. -pub struct ControlRing { +pub struct ControlRing { memory: Memory, } /// Role-bound association ring endpoints owned by the local peer. -pub struct LocalControlRingEndpoints { +pub struct LocalControlRingEndpoints { /// Local-to-broker request producer. pub request_producer: ControlRingProducer, /// Broker-to-local response consumer. @@ -225,7 +305,7 @@ pub struct LocalControlRingEndpoints { } /// Role-bound association ring endpoints owned by the broker peer. -pub struct BrokerControlRingEndpoints { +pub struct BrokerControlRingEndpoints { /// Local-to-broker request consumer. pub request_consumer: ControlRingConsumer, /// Broker-to-local response producer. @@ -234,7 +314,7 @@ pub struct BrokerControlRingEndpoints { pub notification_producer: ControlRingProducer, } -impl ControlRing { +impl ControlRing { /// Attaches to an exact-size shared control-ring mapping. pub fn new(memory: Memory) -> Result { let actual = memory.len(); @@ -317,11 +397,10 @@ impl ControlRing { .write(range.start + CONTROL_RING_SLOT_HEADER_SIZE, payload)?; self.memory .write(range.start + size_of::(), &metadata)?; - self.memory.store_u64_and_fetch_add_u32_release( + self.memory.store_u64_and_increment_u32_release( range.start, sequence.to_le(), direction.producer_epoch_offset(), - 1, )?; Ok(()) } @@ -375,7 +454,7 @@ impl ControlRing { } /// Trusted endpoint-local state for one control-ring producer. -pub struct ControlRingProducer { +pub struct ControlRingProducer { ring: Arc>, direction: ControlRingDirection, tail: u64, @@ -385,22 +464,51 @@ pub struct ControlRingProducer { /// Cloneable, narrow handle for interrupting a wait on one ring endpoint. /// /// This handle intentionally exposes neither the backing memory nor endpoint -/// state. Hosted transports use it to make liveness and cancellation events -/// visible to a thread blocked in an OS-specific wait. -#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] -pub(crate) struct ControlRingWakeHandle { +/// state. Concrete transports use it to interrupt a ring wait provided by +/// [`WaitableSharedMemory`] when liveness or cancellation state changes. It is +/// public so that transport bindings outside this crate, such as the endpoints +/// in `litebox_broker_transport_linux_userland`, can interrupt ring waits +/// without gaining access to ring memory. +pub struct ControlRingWakeHandle { ring: Arc>, wait_epoch: ControlRingWaitEpoch, } -#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] +/// Shared-memory wait and wake support for control-ring epoch words. +/// +/// The ring state machines themselves are nonblocking: they report +/// [`Full`](ControlRingWriteStatus::Full) or [`Empty`](ControlRingReadStatus::Empty) +/// together with the epoch that was sampled before the final check. Shared +/// memory that can also block and wake threads on those epoch words implements +/// this trait, which lets ring endpoints offer blocking waits without knowing +/// whether the backing memory uses a futex, a kernel event, or something else. +/// +/// Implementations must publish and observe epoch changes through the same +/// coherent shared memory the ring uses, so a wake that follows an epoch change +/// can never be missed by a waiter that sampled the previous epoch. +pub trait WaitableSharedMemory: ControlRingMemory { + /// Error reported by blocking operations on this shared memory. + type Error; + + /// Converts a shared-memory access failure into [`Self::Error`]. + fn wait_access_error(error: SharedMemoryError) -> Self::Error; + + /// Waits while the naturally aligned `u32` at `offset` equals `expected`. + /// + /// A value change or an interruption must be reported as a successful, + /// possibly spurious wakeup, so callers must recheck their wait condition. + fn wait_while_equal(&self, offset: usize, expected: u32) -> Result<(), Self::Error>; + + /// Wakes one waiter blocked on the naturally aligned `u32` at `offset`. + fn wake_one(&self, offset: usize) -> Result<(), Self::Error>; +} + #[derive(Clone, Copy)] enum ControlRingWaitEpoch { Producer(ControlRingDirection), Consumer(ControlRingDirection), } -#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] impl ControlRingWaitEpoch { const fn offset(self) -> usize { match self { @@ -410,8 +518,7 @@ impl ControlRingWaitEpoch { } } -#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] -impl Clone for ControlRingWakeHandle { +impl Clone for ControlRingWakeHandle { fn clone(&self) -> Self { Self { ring: Arc::clone(&self.ring), @@ -420,8 +527,7 @@ impl Clone for ControlRingWakeHandle { } } -#[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] -impl ControlRingWakeHandle { +impl ControlRingWakeHandle { pub(crate) const fn wait_epoch_offset(&self) -> usize { self.wait_epoch.offset() } @@ -431,7 +537,20 @@ impl ControlRingWakeHandle { } } -impl ControlRingProducer { +impl ControlRingWakeHandle { + /// Changes and wakes the epoch observed by this endpoint's wait operation. + /// + /// Incrementing before waking closes the race where cancellation happens + /// after a ring operation samples its epoch but before it starts to wait. + pub fn interrupt_wait(&self) -> Result<(), Memory::Error> { + self.memory() + .increment_u32_release(self.wait_epoch_offset()) + .map_err(Memory::wait_access_error)?; + self.memory().wake_one(self.wait_epoch_offset()) + } +} + +impl ControlRingProducer { fn new(ring: Arc>, direction: ControlRingDirection) -> Self { Self { ring, @@ -446,15 +565,15 @@ impl ControlRingProducer { self.direction } - #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] - pub(crate) fn wake_handle(&self) -> ControlRingWakeHandle { + /// Returns a handle that can interrupt a wait on this producer's ring + /// direction. + pub fn wake_handle(&self) -> ControlRingWakeHandle { ControlRingWakeHandle { ring: Arc::clone(&self.ring), wait_epoch: ControlRingWaitEpoch::Consumer(self.direction), } } - #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] pub(crate) fn memory(&self) -> &Memory { self.ring.memory() } @@ -516,15 +635,31 @@ impl ControlRingProducer { } } +impl ControlRingProducer { + /// Waits for consumer progress after [`ControlRingWriteStatus::Full`]. + /// + /// The caller must retry the write after this possibly spurious wakeup. + pub fn wait_for_capacity(&mut self, wait_epoch: u32) -> Result<(), Memory::Error> { + self.memory() + .wait_while_equal(self.direction().consumer_epoch_offset(), wait_epoch) + } + + /// Wakes the consumer after publishing one or more messages. + pub fn wake_consumer(&self) -> Result<(), Memory::Error> { + self.memory() + .wake_one(self.direction().producer_epoch_offset()) + } +} + /// Trusted endpoint-local state for one control-ring consumer. -pub struct ControlRingConsumer { +pub struct ControlRingConsumer { ring: Arc>, direction: ControlRingDirection, head: u64, published_head: u64, } -impl ControlRingConsumer { +impl ControlRingConsumer { fn new(ring: Arc>, direction: ControlRingDirection) -> Self { Self { ring, @@ -539,15 +674,15 @@ impl ControlRingConsumer { self.direction } - #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] - pub(crate) fn wake_handle(&self) -> ControlRingWakeHandle { + /// Returns a handle that can interrupt a wait on this consumer's ring + /// direction. + pub fn wake_handle(&self) -> ControlRingWakeHandle { ControlRingWakeHandle { ring: Arc::clone(&self.ring), wait_epoch: ControlRingWaitEpoch::Producer(self.direction), } } - #[cfg(any(test, all(feature = "linux-userland", target_os = "linux")))] pub(crate) fn memory(&self) -> &Memory { self.ring.memory() } @@ -557,11 +692,10 @@ impl ControlRingConsumer { if self.head == self.published_head { return Ok(()); } - self.ring.memory.store_u64_and_fetch_add_u32_release( + self.ring.memory.store_u64_and_increment_u32_release( self.direction.consumer_head_offset(), self.head.to_le(), self.direction.consumer_epoch_offset(), - 1, )?; self.published_head = self.head; Ok(()) @@ -604,6 +738,9 @@ impl ControlRingConsumer { self.ring .read_slot_body(self.direction, self.head, &mut image[size_of::()..]) .map_err(ControlRingReadError::Ring)?; + // Keep every slot-body load before the sequence recheck so a hostile + // producer cannot pass validation with a body read after publication. + fence(Ordering::Acquire); let verified_sequence = self .ring .load_sequence(self.direction, self.head) @@ -637,9 +774,26 @@ impl ControlRingConsumer { } } +impl ControlRingConsumer { + /// Waits for producer progress after [`ControlRingReadStatus::Empty`]. + /// + /// The caller must retry the read after this possibly spurious wakeup. + pub fn wait_for_message(&mut self, wait_epoch: u32) -> Result<(), Memory::Error> { + self.memory() + .wait_while_equal(self.direction().producer_epoch_offset(), wait_epoch) + } + + /// Wakes the producer after publishing newly consumed slots. + pub fn wake_producer(&self) -> Result<(), Memory::Error> { + self.memory() + .wake_one(self.direction().consumer_epoch_offset()) + } +} + #[cfg(test)] mod tests { use super::*; + use crate::shared_memory::SharedMemory; use core::mem::align_of; use litebox_broker_protocol::RequestId; use litebox_broker_protocol::message::{ @@ -678,6 +832,30 @@ mod tests { assert!(ControlRing::new(TestMemory::new(CONTROL_RING_MEMORY_SIZE)).is_ok()); } + #[test] + fn memory_layout_separates_byte_and_word_regions() { + assert!(memory_permits_u64(0)); + assert!(!memory_permits_u32(0)); + assert!(!memory_permits_byte_range(0, 1)); + assert!(memory_permits_byte_range( + size_of::(), + CONTROL_RING_SLOT_SIZE - size_of::() + )); + assert!(!memory_permits_byte_range( + size_of::(), + CONTROL_RING_SLOT_SIZE - size_of::() + 1 + )); + + let sync_start = CONTROL_RING_DATA_SIZE; + assert!(memory_permits_u32(sync_start + PRODUCER_EPOCH_OFFSET)); + assert!(memory_permits_u32(sync_start + CONSUMER_EPOCH_OFFSET)); + assert!(memory_permits_u64(sync_start + CONSUMER_HEAD_OFFSET)); + assert!(!memory_permits_byte_range(sync_start, 1)); + assert!(!memory_permits_u32(CONTROL_RING_MEMORY_SIZE)); + assert!(!memory_permits_u64(CONTROL_RING_MEMORY_SIZE)); + assert!(!memory_permits_byte_range(CONTROL_RING_MEMORY_SIZE, 1)); + } + #[test] fn directions_occupy_disjoint_data_and_sync_ranges() { let directions = [ @@ -911,7 +1089,7 @@ mod tests { } #[test] - fn failed_payload_metadata_or_atomic_publication_does_not_publish_progress() { + fn failed_payload_metadata_or_word_publication_does_not_publish_progress() { for failed_write in [1, 2, 3] { let ring = ControlRing::new(FailingWriteMemory::new()).unwrap(); let (mut producer, mut consumer) = ring.into_endpoints( @@ -1025,13 +1203,10 @@ mod tests { fn wakeup_epochs_wrap_without_controlling_ring_progress() { let (mut producer, mut consumer) = test_endpoints(); - producer - .memory() - .fetch_add_u32_release( - ControlRingDirection::Requests.producer_epoch_offset(), - u32::MAX, - ) - .unwrap(); + producer.memory().store_u32_for_test( + ControlRingDirection::Requests.producer_epoch_offset(), + u32::MAX, + ); producer.try_write(&[7]).unwrap(); assert_eq!( consumer.try_read(owned_bytes), @@ -1042,13 +1217,10 @@ mod tests { Ok(ControlRingReadStatus::Empty { wait_epoch: 0 }) ); - consumer - .memory() - .fetch_add_u32_release( - ControlRingDirection::Requests.consumer_epoch_offset(), - u32::MAX, - ) - .unwrap(); + consumer.memory().store_u32_for_test( + ControlRingDirection::Requests.consumer_epoch_offset(), + u32::MAX, + ); consumer.publish_head().unwrap(); assert_eq!( producer @@ -1381,6 +1553,11 @@ mod tests { fn write_log(&self) -> Vec<(usize, usize)> { self.write_log.lock().unwrap().clone() } + + fn store_u32_for_test(&self, offset: usize, value: u32) { + self.bytes.lock().unwrap()[offset..offset + size_of::()] + .copy_from_slice(&value.to_ne_bytes()); + } } impl SharedMemory for TestMemory { @@ -1416,23 +1593,19 @@ mod tests { } } - impl AtomicSharedMemory for TestMemory { + impl ControlRingMemory for TestMemory { fn load_u32_acquire(&self, offset: usize) -> Result { test_load_u32_acquire(&self.bytes, offset) } - fn fetch_add_u32_release( - &self, - offset: usize, - value: u32, - ) -> Result { - let previous = test_fetch_add_u32_release(&self.bytes, offset, value)?; + fn increment_u32_release(&self, offset: usize) -> Result<(), SharedMemoryError> { + test_increment_u32_release(&self.bytes, offset)?; self.write_log .lock() .unwrap() .push((offset, size_of::())); self.write_count.fetch_add(1, Ordering::Relaxed); - Ok(previous) + Ok(()) } fn load_u64_acquire(&self, offset: usize) -> Result { @@ -1449,26 +1622,24 @@ mod tests { Ok(()) } - fn store_u64_and_fetch_add_u32_release( + fn store_u64_and_increment_u32_release( &self, store_offset: usize, value: u64, - add_offset: usize, - add_value: u32, - ) -> Result { - let previous = test_store_u64_and_fetch_add_u32_release( + increment_offset: usize, + ) -> Result<(), SharedMemoryError> { + test_store_u64_and_increment_u32_release( &self.bytes, store_offset, value, - add_offset, - add_value, + increment_offset, )?; self.write_log.lock().unwrap().extend([ (store_offset, size_of::()), - (add_offset, size_of::()), + (increment_offset, size_of::()), ]); self.write_count.fetch_add(2, Ordering::Relaxed); - Ok(previous) + Ok(()) } } @@ -1531,17 +1702,13 @@ mod tests { } } - impl AtomicSharedMemory for FailingWriteMemory { + impl ControlRingMemory for FailingWriteMemory { fn load_u32_acquire(&self, offset: usize) -> Result { test_load_u32_acquire(&self.bytes, offset) } - fn fetch_add_u32_release( - &self, - offset: usize, - value: u32, - ) -> Result { - test_fetch_add_u32_release(&self.bytes, offset, value) + fn increment_u32_release(&self, offset: usize) -> Result<(), SharedMemoryError> { + test_increment_u32_release(&self.bytes, offset) } fn load_u64_acquire(&self, offset: usize) -> Result { @@ -1550,7 +1717,7 @@ mod tests { fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError> { if !offset.is_multiple_of(align_of::()) { - return Err(SharedMemoryError::UnalignedAtomic); + return Err(SharedMemoryError::UnalignedWord); } let call = self.write_count.fetch_add(1, Ordering::Relaxed) + 1; if call == self.fail_on_write.load(Ordering::Relaxed) { @@ -1559,23 +1726,21 @@ mod tests { test_store_u64_release(&self.bytes, offset, value) } - fn store_u64_and_fetch_add_u32_release( + fn store_u64_and_increment_u32_release( &self, store_offset: usize, value: u64, - add_offset: usize, - add_value: u32, - ) -> Result { + increment_offset: usize, + ) -> Result<(), SharedMemoryError> { let call = self.write_count.fetch_add(1, Ordering::Relaxed) + 1; if call == self.fail_on_write.load(Ordering::Relaxed) { return Err(SharedMemoryError::InvalidRange); } - test_store_u64_and_fetch_add_u32_release( + test_store_u64_and_increment_u32_release( &self.bytes, store_offset, value, - add_offset, - add_value, + increment_offset, ) } } @@ -1650,17 +1815,13 @@ mod tests { } } - impl AtomicSharedMemory for TearingMemory { + impl ControlRingMemory for TearingMemory { fn load_u32_acquire(&self, offset: usize) -> Result { test_load_u32_acquire(&self.bytes, offset) } - fn fetch_add_u32_release( - &self, - offset: usize, - value: u32, - ) -> Result { - test_fetch_add_u32_release(&self.bytes, offset, value) + fn increment_u32_release(&self, offset: usize) -> Result<(), SharedMemoryError> { + test_increment_u32_release(&self.bytes, offset) } fn load_u64_acquire(&self, offset: usize) -> Result { @@ -1671,19 +1832,17 @@ mod tests { test_store_u64_release(&self.bytes, offset, value) } - fn store_u64_and_fetch_add_u32_release( + fn store_u64_and_increment_u32_release( &self, store_offset: usize, value: u64, - add_offset: usize, - add_value: u32, - ) -> Result { - test_store_u64_and_fetch_add_u32_release( + increment_offset: usize, + ) -> Result<(), SharedMemoryError> { + test_store_u64_and_increment_u32_release( &self.bytes, store_offset, value, - add_offset, - add_value, + increment_offset, ) } } @@ -1693,7 +1852,7 @@ mod tests { offset: usize, ) -> Result { if !offset.is_multiple_of(align_of::()) { - return Err(SharedMemoryError::UnalignedAtomic); + return Err(SharedMemoryError::UnalignedWord); } let bytes = bytes.lock().unwrap(); let end = offset @@ -1710,13 +1869,12 @@ mod tests { Ok(value) } - fn test_fetch_add_u32_release( + fn test_increment_u32_release( bytes: &Mutex>, offset: usize, - value: u32, - ) -> Result { + ) -> Result<(), SharedMemoryError> { if !offset.is_multiple_of(align_of::()) { - return Err(SharedMemoryError::UnalignedAtomic); + return Err(SharedMemoryError::UnalignedWord); } fence(Ordering::Release); let mut bytes = bytes.lock().unwrap(); @@ -1727,8 +1885,8 @@ mod tests { .get_mut(offset..end) .ok_or(SharedMemoryError::InvalidRange)?; let previous = u32::from_ne_bytes(destination.try_into().unwrap()); - destination.copy_from_slice(&previous.wrapping_add(value).to_ne_bytes()); - Ok(previous) + destination.copy_from_slice(&previous.wrapping_add(1).to_ne_bytes()); + Ok(()) } fn test_load_u64_acquire( @@ -1736,7 +1894,7 @@ mod tests { offset: usize, ) -> Result { if !offset.is_multiple_of(align_of::()) { - return Err(SharedMemoryError::UnalignedAtomic); + return Err(SharedMemoryError::UnalignedWord); } let bytes = bytes.lock().unwrap(); let end = offset @@ -1759,7 +1917,7 @@ mod tests { value: u64, ) -> Result<(), SharedMemoryError> { if !offset.is_multiple_of(align_of::()) { - return Err(SharedMemoryError::UnalignedAtomic); + return Err(SharedMemoryError::UnalignedWord); } fence(Ordering::Release); let mut bytes = bytes.lock().unwrap(); @@ -1773,39 +1931,39 @@ mod tests { Ok(()) } - fn test_store_u64_and_fetch_add_u32_release( + fn test_store_u64_and_increment_u32_release( bytes: &Mutex>, store_offset: usize, value: u64, - add_offset: usize, - add_value: u32, - ) -> Result { + increment_offset: usize, + ) -> Result<(), SharedMemoryError> { if !store_offset.is_multiple_of(align_of::()) - || !add_offset.is_multiple_of(align_of::()) + || !increment_offset.is_multiple_of(align_of::()) { - return Err(SharedMemoryError::UnalignedAtomic); + return Err(SharedMemoryError::UnalignedWord); } let store_end = store_offset .checked_add(size_of::()) .ok_or(SharedMemoryError::InvalidRange)?; - let add_end = add_offset + let increment_end = increment_offset .checked_add(size_of::()) .ok_or(SharedMemoryError::InvalidRange)?; - if store_offset < add_end && add_offset < store_end { + if store_offset < increment_end && increment_offset < store_end { return Err(SharedMemoryError::InvalidRange); } let mut bytes = bytes.lock().unwrap(); - if store_end > bytes.len() || add_end > bytes.len() { + if store_end > bytes.len() || increment_end > bytes.len() { return Err(SharedMemoryError::InvalidRange); } let previous = u32::from_ne_bytes( - bytes[add_offset..add_end] + bytes[increment_offset..increment_end] .try_into() .expect("checked u32 range"), ); fence(Ordering::Release); bytes[store_offset..store_end].copy_from_slice(&value.to_ne_bytes()); - bytes[add_offset..add_end].copy_from_slice(&previous.wrapping_add(add_value).to_ne_bytes()); - Ok(previous) + bytes[increment_offset..increment_end] + .copy_from_slice(&previous.wrapping_add(1).to_ne_bytes()); + Ok(()) } } diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs index b33c8ed3ac..7a76ef5727 100644 --- a/litebox_broker_transport/src/lib.rs +++ b/litebox_broker_transport/src/lib.rs @@ -1,26 +1,33 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#![cfg_attr(not(feature = "std"), no_std)] - -//! Broker transport implementations. +//! Runtime broker transport interfaces and portable transport mechanisms. +//! +//! This crate owns what a broker association needs in order to *move* messages +//! once both peers agree on the protocol: the local-side and host-side channel +//! contracts, the runtime shared-memory interfaces and the checked +//! shared-buffer pool built on them, and the portable shared control-ring state +//! machines. //! -//! Transports own hosted or platform-specific framing and I/O. Portable broker -//! protocol messages, local-side adapters, host-side request handling, and core -//! authority state live in separate crates. +//! Nothing here is tied to an operating system or to hosted userland. The crate +//! is unconditionally `no_std`, so a kernel deployment can use the same +//! interfaces and control rings. Concrete association bindings, such as the +//! Unix-domain-socket and memfd endpoints in +//! `litebox_broker_transport_linux_userland`, live in separate crates, and a +//! deployment may implement the channel traits directly instead of using the +//! control ring. +//! +//! Peer-visible message and layout contracts live in `litebox_broker_protocol`, +//! while the portable local-side, host-side, and core authority adapters live in +//! their own crates. + +#![no_std] extern crate alloc; #[cfg(test)] extern crate std; +pub mod channel; pub mod control_ring; - -#[cfg(all(feature = "linux-userland", target_os = "linux"))] pub mod shared_memory; - -#[cfg(all(feature = "linux-userland", target_os = "linux"))] -pub mod unix_socket; - -#[cfg(all(feature = "linux-userland", target_os = "linux"))] -mod unix_io; diff --git a/litebox_broker_transport/src/shared_memory.rs b/litebox_broker_transport/src/shared_memory.rs index a9b913cf34..df15ba0dd4 100644 --- a/litebox_broker_transport/src/shared_memory.rs +++ b/litebox_broker_transport/src/shared_memory.rs @@ -1,856 +1,273 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Reusable Linux memfd-backed shared memory. - -use std::io::{Error, Result as IoResult}; -use std::io::{ErrorKind, IoSlice, IoSliceMut}; -use std::mem::{align_of, size_of}; -use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; -use std::os::unix::net::UnixStream; -use std::ptr::NonNull; -use std::sync::Mutex; -use std::time::Instant; - -use rustix::fs::{ - MemfdFlags, SealFlags, fcntl_add_seals, fcntl_get_seals, fstat, ftruncate, memfd_create, -}; -use rustix::io::Errno; -use rustix::net::{ - RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags, SendAncillaryBuffer, - SendAncillaryMessage, SendFlags, -}; -use rustix::thread::futex; +//! Runtime shared-memory access for broker transports. +//! +//! [`SharedMemory`] and [`ControlRingMemory`] abstract a concrete shared-memory +//! resource. [`SharedBufferPool`] applies the peer-visible fixed-slot layout +//! from [`litebox_broker_protocol::shared_buffer`] and bounds-checks each slot +//! access. -use litebox_broker_protocol::shared_memory::{AtomicSharedMemory, SharedMemory, SharedMemoryError}; +use alloc::sync::Arc; -use std::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use thiserror::Error; -use crate::control_ring::{ControlRingConsumer, ControlRingProducer, ControlRingWakeHandle}; -use crate::unix_io::{ - refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, +use litebox_broker_protocol::shared_buffer::{ + SharedBufferLayout, SharedBufferLayoutError, SharedBufferSlotIndex, }; -const REQUIRED_MEMFD_SEALS: SealFlags = SealFlags::from_bits_retain( - SealFlags::GROW.bits() | SealFlags::SHRINK.bits() | SealFlags::SEAL.bits(), -); - -/// Linux memfd-backed shared memory usable by broker transports. -pub struct MemfdSharedMemory { - fd: OwnedFd, - mapping: Mutex, +/// Error accessing a shared-memory resource. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum SharedMemoryError { + /// The requested byte range is outside the shared-memory resource. + #[error("shared-memory range is out of bounds")] + InvalidRange, + /// A typed word access is not naturally aligned. + #[error("shared-memory word access is not naturally aligned")] + UnalignedWord, + /// The backing resource could not complete an otherwise valid access. + #[error("shared-memory backing resource access failed")] + AccessFailed, } -struct MappedRegion { - address: NonNull, - length: usize, -} - -// SAFETY: `MappedRegion` exclusively owns its mapping, and all byte access is -// serialized by the enclosing `Mutex`. -unsafe impl Send for MappedRegion {} - -fn atomic_u64_at( - memory: &MemfdSharedMemory, - offset: usize, -) -> Result<&AtomicU64, SharedMemoryError> { - let address = { - let mapping = memory - .mapping - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - let byte_address = - shared_address(&mapping, offset, size_of::(), align_of::())?; - // The runtime check above establishes the stronger alignment. - #[allow(clippy::cast_ptr_alignment)] - let address = byte_address.cast::(); - address - }; - // SAFETY: The pointer is valid and aligned for a `u64`. Control-ring - // sequence words are accessed atomically by conforming endpoints, and - // `memory` keeps the immutable mapping alive for the returned reference. - Ok(unsafe { AtomicU64::from_ptr(address) }) -} - -fn atomic_u32_at( - memory: &MemfdSharedMemory, - offset: usize, -) -> Result<&AtomicU32, SharedMemoryError> { - let address = { - let mapping = memory - .mapping - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - let byte_address = - shared_address(&mapping, offset, size_of::(), align_of::())?; - // The runtime check above establishes the stronger alignment. - #[allow(clippy::cast_ptr_alignment)] - let address = byte_address.cast::(); - address - }; - // SAFETY: The pointer is valid and aligned for a `u32`. Control-ring epoch - // words are accessed atomically by conforming endpoints and the futex - // syscall, and `memory` keeps the immutable mapping alive for the returned - // reference. - Ok(unsafe { AtomicU32::from_ptr(address) }) -} - -fn shared_address( - mapping: &MappedRegion, - offset: usize, - size: usize, - alignment: usize, -) -> Result<*mut u8, SharedMemoryError> { - offset - .checked_add(size) - .filter(|end| *end <= mapping.length) - .ok_or(SharedMemoryError::InvalidRange)?; - // SAFETY: The checked offset is inside the live mapping. - let byte_address = unsafe { mapping.address.as_ptr().add(offset) }; - if !byte_address.addr().is_multiple_of(alignment) { - return Err(SharedMemoryError::UnalignedAtomic); - } - Ok(byte_address) -} - -fn validate_nonoverlapping_atomic_ranges( - store_offset: usize, - add_offset: usize, -) -> Result<(), SharedMemoryError> { - let store_end = store_offset - .checked_add(size_of::()) - .ok_or(SharedMemoryError::InvalidRange)?; - let add_end = add_offset - .checked_add(size_of::()) - .ok_or(SharedMemoryError::InvalidRange)?; - if store_offset < add_end && add_offset < store_end { - return Err(SharedMemoryError::InvalidRange); - } - Ok(()) -} - -impl MemfdSharedMemory { - /// Creates and maps a sealed memfd with `length` bytes. - pub fn create(length: usize) -> IoResult { - if length == 0 { - return Err(invalid_data("shared memory cannot be empty")); - } - let fd = memfd_create( - "litebox-broker-shm", - MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING, - )?; - ftruncate( - &fd, - length - .try_into() - .map_err(|_| invalid_data("shared-memory length exceeds u64"))?, - )?; - fcntl_add_seals(&fd, REQUIRED_MEMFD_SEALS)?; - Self::map(fd, length) - } - - /// Validates and maps a received memfd with `expected_length` bytes. +/// Byte-copy access to a shared-memory mapping. +/// +/// A value may own a distinct shared-memory object or identify a region within +/// a larger resource. Each endpoint has its own value, and peers may use +/// different implementation types, such as user and kernel mappings of the same +/// physical memory. Implementations must keep the backing resource alive, make +/// concurrent local calls safe, and never expose Rust references into memory +/// writable by a peer. +/// +/// A peer may access the same bytes concurrently, even if doing so violates the +/// higher-level protocol. Implementations must keep such access memory-safe; +/// callers that require a coherent snapshot must validate it separately. +/// +/// The concrete transport establishes and shares the resource. The protocol +/// using it determines which endpoint may access each byte range. +pub trait SharedMemory: Send + Sync + 'static { + /// Returns the mapped resource length in bytes. /// - /// The descriptor must have the expected nonzero size sealed against - /// changes. - pub fn from_received_fd(fd: OwnedFd, expected_length: usize) -> IoResult { - if expected_length == 0 { - return Err(invalid_data("shared memory cannot be empty")); - } - // Verify the size seals before reading the size so it cannot change - // between validation and mapping. - let seals = fcntl_get_seals(&fd)?; - if !seals.contains(REQUIRED_MEMFD_SEALS) { - return Err(invalid_data("shared-memory size is not sealed")); - } - let length = usize::try_from(fstat(&fd)?.st_size) - .map_err(|_| invalid_data("invalid shared-memory length"))?; - if length != expected_length { - return Err(invalid_data( - "shared-memory length does not match expected size", - )); - } - Self::map(fd, length) - } + /// The length must remain stable for the lifetime of the resource. + fn len(&self) -> usize; - fn map(fd: OwnedFd, length: usize) -> IoResult { - if length > isize::MAX as usize { - return Err(invalid_data( - "shared-memory length exceeds pointer offset range", - )); - } - // SAFETY: `fd` refers to a file at least `length` bytes long. The - // returned mapping is checked against `MAP_FAILED` and owned by - // `MappedRegion`. - let address = unsafe { - libc::mmap( - std::ptr::null_mut(), - length, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_SHARED, - fd.as_raw_fd(), - 0, - ) - }; - if address == libc::MAP_FAILED { - return Err(Error::last_os_error()); - } - let address = - NonNull::new(address.cast()).ok_or_else(|| invalid_data("mmap returned null"))?; - Ok(Self { - fd, - mapping: Mutex::new(MappedRegion { address, length }), - }) + /// Returns whether the resource is empty. + fn is_empty(&self) -> bool { + self.len() == 0 } - /// Waits while a shared atomic `u32` still equals `expected`. + /// Copies bytes from shared memory at `offset` into `destination`. /// - /// A value change or signal interruption is reported as a successful, - /// possibly spurious wakeup. The caller must recheck its wait condition. - fn futex_wait_u32(&self, offset: usize, expected: u32) -> IoResult<()> { - let word = atomic_u32_at(self, offset) - .map_err(|error| Error::new(ErrorKind::InvalidInput, error))?; - match futex::wait(word, futex::Flags::empty(), expected, None) { - Ok(()) | Err(Errno::AGAIN | Errno::INTR) => Ok(()), - Err(error) => Err(error.into()), - } - } + /// The entire range must be validated before any bytes are copied. + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError>; - /// Wakes one waiter blocked on a shared atomic `u32`. - fn futex_wake_u32(&self, offset: usize) -> IoResult<()> { - let word = atomic_u32_at(self, offset) - .map_err(|error| Error::new(ErrorKind::InvalidInput, error))?; - futex::wake(word, futex::Flags::empty(), 1)?; - Ok(()) - } -} - -impl ControlRingProducer { - /// Waits for consumer progress after - /// [`Full`](crate::control_ring::ControlRingWriteStatus::Full). + /// Copies `source` into shared memory at `offset`. /// - /// The caller must retry the write after this possibly spurious wakeup. - pub fn wait_for_capacity(&self, wait_epoch: u32) -> IoResult<()> { - self.memory() - .futex_wait_u32(self.direction().consumer_epoch_offset(), wait_epoch) - } - - /// Wakes the consumer after publishing one or more messages. - pub fn wake_consumer(&self) -> IoResult<()> { - self.memory() - .futex_wake_u32(self.direction().producer_epoch_offset()) - } + /// The entire range must be validated before any bytes are copied. + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError>; } -impl ControlRingConsumer { - /// Waits for producer progress after - /// [`Empty`](crate::control_ring::ControlRingReadStatus::Empty). - /// - /// The caller must retry the read after this possibly spurious wakeup. - pub fn wait_for_message(&self, wait_epoch: u32) -> IoResult<()> { - self.memory() - .futex_wait_u32(self.direction().producer_epoch_offset(), wait_epoch) - } +/// Ordered access to shared control-ring synchronization values. +/// +/// A peer may modify any backing byte through an uncontrolled alias, including +/// with non-atomic or mixed-width accesses. Implementations must therefore +/// perform these operations without creating Rust references into peer-writable +/// memory. Reads return untrusted snapshots that may contain any bit pattern; +/// the control ring validates them against trusted endpoint-local state. +/// +/// Word operations must be indivisible and ordered between conforming +/// endpoints. An uncontrolled peer alias may bypass those guarantees, but must +/// not compromise the implementation's Rust memory safety. +/// +/// Safe implementations must enforce disjoint byte, `u32`, and `u64` access +/// regions for their own APIs. The `u32` increment must be indivisible between +/// conforming endpoints and must wrap on overflow. +pub trait ControlRingMemory: SharedMemory { + /// Reads a naturally aligned native-endian `u32` with acquire semantics. + fn load_u32_acquire(&self, offset: usize) -> Result; - /// Wakes the producer after publishing newly consumed slots. - pub fn wake_producer(&self) -> IoResult<()> { - self.memory() - .futex_wake_u32(self.direction().consumer_epoch_offset()) - } -} + /// Indivisibly increments a naturally aligned native-endian `u32` with + /// release semantics, wrapping on overflow. + fn increment_u32_release(&self, offset: usize) -> Result<(), SharedMemoryError>; + + /// Reads a naturally aligned native-endian `u64` with acquire semantics. + fn load_u64_acquire(&self, offset: usize) -> Result; -impl ControlRingWakeHandle { - /// Changes and wakes the epoch observed by this endpoint's wait operation. + /// Writes a naturally aligned native-endian `u64` with release semantics. /// - /// Incrementing before waking closes the race where cancellation happens - /// after a ring operation samples its epoch but before it enters futex wait. - pub(crate) fn interrupt_wait(&self) -> IoResult<()> { - self.memory() - .fetch_add_u32_release(self.wait_epoch_offset(), 1) - .map_err(|error| Error::new(ErrorKind::InvalidInput, error))?; - self.memory().futex_wake_u32(self.wait_epoch_offset()) - } -} + /// On error, the value must not have been stored. + fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError>; -impl AsFd for MemfdSharedMemory { - fn as_fd(&self) -> BorrowedFd<'_> { - self.fd.as_fd() - } + /// Release-writes a native-endian `u64`, then indivisibly increments a + /// native-endian `u32` with release semantics. + /// + /// Both values must be naturally aligned and occupy non-overlapping ranges. + /// Implementations must validate both accesses before writing either value. + /// The two operations are ordered but are not one indivisible transaction, + /// so a backing-resource failure may leave only the `u64` written. + fn store_u64_and_increment_u32_release( + &self, + store_offset: usize, + value: u64, + increment_offset: usize, + ) -> Result<(), SharedMemoryError>; } -impl SharedMemory for MemfdSharedMemory { +impl SharedMemory for Arc { fn len(&self) -> usize { - self.mapping - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .length + (**self).len() } fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { - let mapping = self - .mapping - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - offset - .checked_add(destination.len()) - .filter(|end| *end <= mapping.length) - .ok_or(SharedMemoryError::InvalidRange)?; - // SAFETY: The range was checked against the live mapping, - // `destination` is valid for its full length, and no Rust reference is - // created for the byte-addressed shared mapping. - unsafe { - libc::memcpy( - destination.as_mut_ptr().cast(), - mapping.address.as_ptr().add(offset).cast(), - destination.len(), - ); - } - Ok(()) + (**self).read(offset, destination) } fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { - let mapping = self - .mapping - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - offset - .checked_add(source.len()) - .filter(|end| *end <= mapping.length) - .ok_or(SharedMemoryError::InvalidRange)?; - // SAFETY: The range was checked against the live mapping, `source` is - // valid for its full length, and no Rust reference is created for the - // byte-addressed shared mapping. - unsafe { - libc::memcpy( - mapping.address.as_ptr().add(offset).cast(), - source.as_ptr().cast(), - source.len(), - ); - } - Ok(()) - } -} - -impl AtomicSharedMemory for MemfdSharedMemory { - fn load_u32_acquire(&self, offset: usize) -> Result { - Ok(atomic_u32_at(self, offset)?.load(Ordering::Acquire)) - } - - fn fetch_add_u32_release(&self, offset: usize, value: u32) -> Result { - Ok(atomic_u32_at(self, offset)?.fetch_add(value, Ordering::Release)) - } - - fn load_u64_acquire(&self, offset: usize) -> Result { - Ok(atomic_u64_at(self, offset)?.load(Ordering::Acquire)) - } - - fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError> { - atomic_u64_at(self, offset)?.store(value, Ordering::Release); - Ok(()) - } - - fn store_u64_and_fetch_add_u32_release( - &self, - store_offset: usize, - value: u64, - add_offset: usize, - add_value: u32, - ) -> Result { - validate_nonoverlapping_atomic_ranges(store_offset, add_offset)?; - let stored = atomic_u64_at(self, store_offset)?; - let added = atomic_u32_at(self, add_offset)?; - stored.store(value, Ordering::Release); - Ok(added.fetch_add(add_value, Ordering::Release)) + (**self).write(offset, source) } } -/// Sends one memfd-backed shared-memory resource over an exclusively owned -/// connected Unix stream. -/// -/// `deadline` bounds setup I/O without leaving a changed socket timeout behind. -pub fn send_memfd( - stream: &mut UnixStream, - memory: &MemfdSharedMemory, - deadline: Option, -) -> IoResult<()> { - with_write_deadline(stream, deadline, |stream, deadline| { - send_fd(stream, memory.as_fd(), deadline) - }) +/// Error validating or accessing a fixed-slot shared-buffer pool. +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum SharedBufferError { + /// The requested slot or byte range is invalid for the layout. + #[error("invalid shared-buffer layout access: {0}")] + Layout(#[from] SharedBufferLayoutError), + /// The backing shared-memory length does not exactly match the layout. + #[error("shared-memory length does not match the shared-buffer layout")] + MemoryLengthMismatch, + /// The backing shared-memory access failed. + #[error("shared-memory access failed: {0}")] + SharedMemory(#[from] SharedMemoryError), } -/// Receives, validates, and maps one memfd-backed shared-memory resource. +/// A shared-memory resource with bounds-checked fixed-slot access. /// -/// `expected_length` supplies the trusted expected size. `deadline` bounds -/// setup I/O without leaving a changed socket timeout behind. -pub fn receive_memfd( - stream: &mut UnixStream, - expected_length: usize, - deadline: Option, -) -> IoResult { - let fd = with_read_deadline(stream, deadline, receive_fd)?; - MemfdSharedMemory::from_received_fd(fd, expected_length) +/// Construction validates that the backing memory has the layout's exact size. +/// Each read or write validates its slot and byte count before deriving an +/// offset and copying data. The pool does not allocate, lease, or synchronize +/// slots; the protocol using it owns those responsibilities. +pub struct SharedBufferPool { + memory: Memory, + layout: SharedBufferLayout, } -fn send_fd(stream: &mut UnixStream, fd: BorrowedFd<'_>, deadline: Option) -> IoResult<()> { - // Unix streams require an ordinary data byte to carry ancillary data. - let carrier = [0]; - let io = [IoSlice::new(&carrier)]; - let fds = [fd]; - let mut control_space = [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))]; - let mut control = SendAncillaryBuffer::new(&mut control_space); - assert!( - control.push(SendAncillaryMessage::ScmRights(&fds)), - "SCM_RIGHTS control buffer is correctly sized" - ); - loop { - refresh_write_deadline(stream, deadline)?; - match rustix::net::sendmsg(stream.as_fd(), &io, &mut control, SendFlags::NOSIGNAL) { - Ok(1) => return Ok(()), - Ok(0) => { - return Err(Error::new( - ErrorKind::WriteZero, - "failed to send shared-memory descriptor", - )); - } - Ok(_) => return Err(invalid_data("oversized shared-memory setup write")), - Err(Errno::INTR) => {} - Err(error) => return Err(error.into()), +impl SharedBufferPool { + /// Creates a fixed-slot view over an exact-size shared-memory resource. + pub fn new(memory: Memory, layout: SharedBufferLayout) -> Result { + if memory.len() != layout.total_len() { + return Err(SharedBufferError::MemoryLengthMismatch); } + Ok(Self { memory, layout }) } -} -fn receive_fd(stream: &mut UnixStream, deadline: Option) -> IoResult { - let mut carrier = [0]; - let mut io = [IoSliceMut::new(&mut carrier)]; - let mut control_space = [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(4))]; - let mut control = RecvAncillaryBuffer::new(&mut control_space); - let received = loop { - refresh_read_deadline(stream, deadline)?; - match rustix::net::recvmsg( - stream.as_fd(), - &mut io, - &mut control, - RecvFlags::CMSG_CLOEXEC, - ) { - Ok(received) => break received, - Err(Errno::INTR) => {} - Err(error) => return Err(error.into()), - } - }; - - let mut received_fds = Vec::new(); - let mut unexpected_control_message = false; - for message in control.drain() { - match message { - RecvAncillaryMessage::ScmRights(fds) => received_fds.extend(fds), - _ => unexpected_control_message = true, - } + /// Returns the fixed-slot layout. + pub const fn layout(&self) -> SharedBufferLayout { + self.layout } - if received.bytes == 0 { - return Err(Error::new( - ErrorKind::UnexpectedEof, - "broker closed during shared-memory setup", - )); - } - if received.bytes != carrier.len() - || received - .flags - .intersects(ReturnFlags::TRUNC | ReturnFlags::CTRUNC) - || unexpected_control_message - || received_fds.len() != 1 - { - return Err(invalid_data( - "shared-memory setup contained invalid descriptor data", - )); + /// Returns the backing shared-memory resource. + /// + /// Direct access is not constrained by the pool's fixed-slot layout. + pub const fn memory(&self) -> &Memory { + &self.memory } - Ok(received_fds - .pop() - .expect("exactly one received descriptor was validated")) -} -impl Drop for MappedRegion { - fn drop(&mut self) { - // SAFETY: `address` and `length` describe the mapping exclusively owned - // by this value, and it is unmapped exactly once here. - let result = unsafe { libc::munmap(self.address.as_ptr().cast(), self.length) }; - debug_assert_eq!(result, 0, "failed to unmap broker shared memory"); + /// Copies bytes from the start of `slot` into `destination`. + pub fn read( + &self, + slot: SharedBufferSlotIndex, + destination: &mut [u8], + ) -> Result<(), SharedBufferError> { + let range = self.layout.range(slot, destination.len())?; + self.memory.read(range.start, destination)?; + Ok(()) } -} -fn invalid_data(message: &'static str) -> Error { - Error::new(std::io::ErrorKind::InvalidData, message) + /// Copies `source` into the start of `slot`. + pub fn write( + &self, + slot: SharedBufferSlotIndex, + source: &[u8], + ) -> Result<(), SharedBufferError> { + let range = self.layout.range(slot, source.len())?; + self.memory.write(range.start, source)?; + Ok(()) + } } #[cfg(test)] mod tests { use super::*; - use crate::control_ring::{ - CONTROL_RING_MEMORY_SIZE, CONTROL_RING_SLOT_COUNT, ControlRing, ControlRingReadStatus, - ControlRingWriteStatus, - }; - use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, SharedBufferSlotIndex, - }; - use rustix::io::FdFlags; - use std::io::Write; - use std::sync::{Arc, Barrier}; - use std::thread; - use std::time::Duration; + use alloc::vec; + use alloc::vec::Vec; + use std::sync::Mutex; #[test] - fn mappings_share_bytes_and_validate_ranges() { - let first = MemfdSharedMemory::create(64).unwrap(); - let second = - MemfdSharedMemory::from_received_fd(first.as_fd().try_clone_to_owned().unwrap(), 64) - .unwrap(); - - first.write(0, &[1, 2, 3]).unwrap(); - let mut data = [0; 3]; - second.read(0, &mut data).unwrap(); - assert_eq!(data, [1, 2, 3]); - - assert_eq!( - second.write(63, &[1, 2]), - Err(SharedMemoryError::InvalidRange) - ); - assert_eq!( - second.read(usize::MAX, &mut data), - Err(SharedMemoryError::InvalidRange) - ); - } - - #[test] - fn mappings_share_ordered_atomic_values_and_validate_alignment() { - let first = MemfdSharedMemory::create(64).unwrap(); - let second = - MemfdSharedMemory::from_received_fd(first.as_fd().try_clone_to_owned().unwrap(), 64) - .unwrap(); - - first.store_u64_release(8, 0x0102_0304_0506_0708).unwrap(); - assert_eq!(second.load_u64_acquire(8), Ok(0x0102_0304_0506_0708)); - assert_eq!(first.fetch_add_u32_release(4, 3), Ok(0)); - assert_eq!(second.load_u32_acquire(4), Ok(3)); - assert_eq!( - first.store_u64_and_fetch_add_u32_release(8, 0x1112_1314_1516_1718, 4, 2), - Ok(3) - ); - assert_eq!(second.load_u64_acquire(8), Ok(0x1112_1314_1516_1718)); - assert_eq!(second.load_u32_acquire(4), Ok(5)); - assert_eq!( - first.store_u64_and_fetch_add_u32_release(8, 0, 64, 1), - Err(SharedMemoryError::InvalidRange) - ); - assert_eq!( - first.store_u64_and_fetch_add_u32_release(8, 0, 12, 1), - Err(SharedMemoryError::InvalidRange) - ); - assert_eq!(second.load_u64_acquire(8), Ok(0x1112_1314_1516_1718)); - second.futex_wait_u32(4, 0).unwrap(); - assert_eq!( - second.load_u64_acquire(1), - Err(SharedMemoryError::UnalignedAtomic) - ); - assert_eq!( - second.store_u64_release(1, 0), - Err(SharedMemoryError::UnalignedAtomic) - ); - assert_eq!( - second.load_u64_acquire(64), - Err(SharedMemoryError::InvalidRange) - ); - assert_eq!( - second.load_u32_acquire(1), - Err(SharedMemoryError::UnalignedAtomic) - ); - assert_eq!( - second.fetch_add_u32_release(64, 1), - Err(SharedMemoryError::InvalidRange) - ); - assert_eq!( - second.futex_wait_u32(64, 0).unwrap_err().kind(), - ErrorKind::InvalidInput - ); + fn pool_checks_backing_length_and_slot_boundaries() { + let layout = SharedBufferLayout::new(8, 3).unwrap(); + assert!(matches!( + SharedBufferPool::new(TestSharedMemory::new(23), layout), + Err(SharedBufferError::MemoryLengthMismatch) + )); + let memory = Arc::new(TestSharedMemory::new(layout.total_len())); + let pool = SharedBufferPool::new(Arc::clone(&memory), layout).unwrap(); + + pool.write(SharedBufferSlotIndex(0), &[1, 2, 3]).unwrap(); + pool.write(SharedBufferSlotIndex(2), &[4, 5]).unwrap(); + let mut first = [0; 3]; + pool.read(SharedBufferSlotIndex(0), &mut first).unwrap(); + assert_eq!(first, [1, 2, 3]); + assert_eq!(&memory.bytes()[8..16], &[0; 8]); assert_eq!( - second.futex_wake_u32(1).unwrap_err().kind(), - ErrorKind::InvalidInput + pool.write(SharedBufferSlotIndex(2), &[0; 9]), + Err(SharedBufferError::Layout( + SharedBufferLayoutError::RangeExceedsSlot + )) ); } - #[test] - fn rejects_unsealed_mismatched_and_oversized_mappings() { - let fd = memfd_create("litebox-broker-shm-test", MemfdFlags::CLOEXEC).unwrap(); - ftruncate(&fd, 1).unwrap(); - assert_eq!( - MemfdSharedMemory::from_received_fd(fd, 1) - .err() - .expect("unsealed memfd should fail") - .kind(), - std::io::ErrorKind::InvalidData - ); + struct TestSharedMemory(Mutex>); - let memory = MemfdSharedMemory::create(64).unwrap(); - assert_eq!( - MemfdSharedMemory::from_received_fd(memory.as_fd().try_clone_to_owned().unwrap(), 32,) - .err() - .expect("mismatched memfd size should fail") - .kind(), - std::io::ErrorKind::InvalidData - ); - - let fd = memfd_create("litebox-broker-shm-test", MemfdFlags::CLOEXEC).unwrap(); - assert_eq!( - MemfdSharedMemory::map(fd, isize::MAX as usize + 1) - .err() - .expect("oversized mapping should fail") - .kind(), - std::io::ErrorKind::InvalidData - ); - } - - #[test] - fn transfers_exact_pool_with_shared_visibility_and_close_on_exec() { - let memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE).unwrap(); - let pool = SharedBufferPool::new(memory, SHARED_BUFFER_LAYOUT).unwrap(); - for index in 0..SHARED_BUFFER_LAYOUT.slot_count() { - pool.write( - SharedBufferSlotIndex(index), - &[u8::try_from(index).unwrap()], - ) - .unwrap(); + impl TestSharedMemory { + fn new(length: usize) -> Self { + Self(Mutex::new(vec![0; length])) } - let (mut local_stream, mut host_stream) = UnixStream::pair().unwrap(); - - send_memfd(&mut host_stream, pool.memory(), None).unwrap(); - let mapped_memory = - receive_memfd(&mut local_stream, SHARED_BUFFER_POOL_SIZE, None).unwrap(); - let mapped_pool = SharedBufferPool::new(mapped_memory, SHARED_BUFFER_LAYOUT).unwrap(); - for index in 0..SHARED_BUFFER_LAYOUT.slot_count() { - let mut byte = [0]; - mapped_pool - .read(SharedBufferSlotIndex(index), &mut byte) - .unwrap(); - assert_eq!(byte, [u8::try_from(index).unwrap()]); - } - let flags = rustix::io::fcntl_getfd(mapped_pool.memory().as_fd()).unwrap(); - assert!(flags.contains(FdFlags::CLOEXEC)); - } - #[test] - fn transfers_exact_sealed_control_ring_mapping() { - let memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); - let ring = ControlRing::new(memory).unwrap(); - let (mut receiver, mut sender) = UnixStream::pair().unwrap(); - - send_memfd(&mut sender, ring.memory(), None).unwrap(); - let mapped = receive_memfd(&mut receiver, CONTROL_RING_MEMORY_SIZE, None).unwrap(); - let mapped_ring = ControlRing::new(mapped).unwrap(); - ring.memory().write(13, &[1, 2, 3]).unwrap(); - let mut bytes = [0; 3]; - mapped_ring.memory().read(13, &mut bytes).unwrap(); - assert_eq!(bytes, [1, 2, 3]); - - let flags = rustix::io::fcntl_getfd(mapped_ring.memory().as_fd()).unwrap(); - assert!(flags.contains(FdFlags::CLOEXEC)); - let seals = fcntl_get_seals(mapped_ring.memory().as_fd()).unwrap(); - assert!(seals.contains(REQUIRED_MEMFD_SEALS)); - assert!(!seals.contains(SealFlags::WRITE)); - } - - #[test] - fn shared_futex_wakeup_prevents_missed_cross_mapping_work() { - let local_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); - let broker_memory = MemfdSharedMemory::from_received_fd( - local_memory.as_fd().try_clone_to_owned().unwrap(), - CONTROL_RING_MEMORY_SIZE, - ) - .unwrap(); - let mut producer = ControlRing::new(local_memory) - .unwrap() - .into_local() - .request_producer; - let mut consumer = ControlRing::new(broker_memory) - .unwrap() - .into_broker() - .request_consumer; - let empty_checked = Arc::new(Barrier::new(2)); - let broker_empty_checked = Arc::clone(&empty_checked); - - let broker = thread::spawn(move || { - let ControlRingReadStatus::Empty { - wait_epoch: producer_epoch, - } = consumer - .try_read(|payload| Ok::<_, ()>(payload[0])) - .unwrap() - else { - panic!("request ring should initially be empty"); - }; - broker_empty_checked.wait(); - consumer.wait_for_message(producer_epoch).unwrap(); - for expected in 0..CONTROL_RING_SLOT_COUNT { - let expected = u8::try_from(expected).unwrap(); - assert_eq!( - consumer.try_read(|payload| Ok::<_, ()>(payload[0])), - Ok(ControlRingReadStatus::Message(expected)) - ); - } - - consumer.publish_head().unwrap(); - consumer.wake_producer().unwrap(); - match consumer - .try_read(|payload| Ok::<_, ()>(payload[0])) - .unwrap() - { - ControlRingReadStatus::Empty { wait_epoch } => { - consumer.wait_for_message(wait_epoch).unwrap(); - assert_eq!( - consumer.try_read(|payload| Ok::<_, ()>(payload[0])), - Ok(ControlRingReadStatus::Message(0xff)) - ); - } - ControlRingReadStatus::Message(value) => assert_eq!(value, 0xff), - } - }); - - empty_checked.wait(); - for value in 0..CONTROL_RING_SLOT_COUNT { - let value = u8::try_from(value).unwrap(); - assert_eq!( - producer.try_write(&[value]), - Ok(ControlRingWriteStatus::Written) - ); + fn bytes(&self) -> Vec { + self.0.lock().unwrap().clone() } - let ControlRingWriteStatus::Full { - wait_epoch: consumer_epoch, - } = producer.try_write(&[0xff]).unwrap() - else { - panic!("request ring should be full"); - }; - producer.wake_consumer().unwrap(); - - producer.wait_for_capacity(consumer_epoch).unwrap(); - assert_eq!( - producer.try_write(&[0xff]), - Ok(ControlRingWriteStatus::Written) - ); - producer.wake_consumer().unwrap(); - - broker.join().unwrap(); - } - - #[test] - fn rejects_missing_multiple_and_truncated_descriptors() { - let length = 8; - - let (mut receiver, mut sender) = UnixStream::pair().unwrap(); - sender.write_all(&[0]).unwrap(); - assert_eq!( - receive_memfd(&mut receiver, length, None) - .err() - .expect("missing descriptor must be rejected") - .kind(), - ErrorKind::InvalidData - ); - - let memory = MemfdSharedMemory::create(length).unwrap(); - let (mut receiver, mut sender) = UnixStream::pair().unwrap(); - send_test_fds(&mut sender, &[memory.as_fd(), memory.as_fd()]); - assert_eq!( - receive_memfd(&mut receiver, length, None) - .err() - .expect("multiple descriptors must be rejected") - .kind(), - ErrorKind::InvalidData - ); - - let (mut receiver, mut sender) = UnixStream::pair().unwrap(); - let fd = memory.as_fd(); - send_test_fds(&mut sender, &[fd, fd, fd, fd, fd]); - assert_eq!( - receive_memfd(&mut receiver, length, None) - .err() - .expect("truncated descriptors must be rejected") - .kind(), - ErrorKind::InvalidData - ); - } - - #[test] - fn rejects_wrong_size_and_unsealed_memory() { - let length = SHARED_BUFFER_POOL_SIZE; - - let wrong_size = MemfdSharedMemory::create(length - 1).unwrap(); - let (mut receiver, mut sender) = UnixStream::pair().unwrap(); - send_memfd(&mut sender, &wrong_size, None).unwrap(); - assert_eq!( - receive_memfd(&mut receiver, length, None) - .err() - .expect("wrong shared-memory size must be rejected") - .kind(), - ErrorKind::InvalidData - ); - - let unsealed = memfd_create("unsealed-transfer-test", MemfdFlags::CLOEXEC).unwrap(); - ftruncate(&unsealed, length.try_into().unwrap()).unwrap(); - let (mut receiver, mut sender) = UnixStream::pair().unwrap(); - send_test_fds(&mut sender, &[unsealed.as_fd()]); - assert_eq!( - receive_memfd(&mut receiver, length, None) - .err() - .expect("unsealed shared memory must be rejected") - .kind(), - ErrorKind::InvalidData - ); } - #[test] - fn reports_eof_and_expired_deadline() { - let length = 8; - let (mut receiver, sender) = UnixStream::pair().unwrap(); - drop(sender); - assert_eq!( - receive_memfd(&mut receiver, length, None) - .err() - .expect("setup EOF must be reported") - .kind(), - ErrorKind::UnexpectedEof - ); - - let (mut receiver, _sender) = UnixStream::pair().unwrap(); - let previous_timeout = Some(Duration::from_secs(2)); - receiver.set_read_timeout(previous_timeout).unwrap(); - let expired = Instant::now().checked_sub(Duration::from_secs(1)).unwrap(); - assert_eq!( - receive_memfd(&mut receiver, length, Some(expired)) - .err() - .expect("expired setup deadline must be rejected") - .kind(), - ErrorKind::TimedOut - ); - assert_eq!(receiver.read_timeout().unwrap(), previous_timeout); + impl SharedMemory for TestSharedMemory { + fn len(&self) -> usize { + self.0.lock().unwrap().len() + } - let memory = MemfdSharedMemory::create(length).unwrap(); - let (_receiver, mut sender) = UnixStream::pair().unwrap(); - sender.set_write_timeout(previous_timeout).unwrap(); - assert_eq!( - send_memfd(&mut sender, &memory, Some(expired)) - .expect_err("expired send deadline must be rejected") - .kind(), - ErrorKind::TimedOut - ); - assert_eq!(sender.write_timeout().unwrap(), previous_timeout); - } + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + let memory = self.0.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let source = memory + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } - fn send_test_fds(stream: &mut UnixStream, fds: &[BorrowedFd<'_>]) { - let carrier = [0]; - let io = [IoSlice::new(&carrier)]; - let mut control_space = - [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(8))]; - let mut control = SendAncillaryBuffer::new(&mut control_space); - assert!(control.push(SendAncillaryMessage::ScmRights(fds))); - assert_eq!( - rustix::net::sendmsg(stream.as_fd(), &io, &mut control, SendFlags::NOSIGNAL).unwrap(), - 1 - ); + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let mut memory = self.0.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let destination = memory + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } } } diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs deleted file mode 100644 index 91bd020d75..0000000000 --- a/litebox_broker_transport/src/unix_socket.rs +++ /dev/null @@ -1,1970 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Unix-domain-socket broker channel for hosted userland deployments. -//! -//! This module deliberately uses `std` because Unix-domain sockets and `std::io` -//! framing are hosted userland concerns. Portable broker interfaces live in the -//! no_std protocol, local, core, and host crates. -//! -//! After setup, the authenticated socket is retained only for liveness and -//! fail-closed shutdown. Active requests, responses, and notifications use -//! shared control rings. - -use std::io::{Error, ErrorKind, Read, Result as IoResult, Write}; -use std::net::Shutdown; -use std::os::unix::net::UnixStream; -use std::path::Path; -use std::sync::{Arc, Condvar, Mutex}; -use std::time::Instant; -use std::{collections::HashMap, thread}; - -use crate::control_ring::{ - ControlRing, ControlRingConsumer, ControlRingError, ControlRingProducer, ControlRingReadError, - ControlRingReadStatus, ControlRingWakeHandle, ControlRingWriteStatus, -}; -use crate::shared_memory::MemfdSharedMemory; -use crate::unix_io::{ - refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, -}; -use litebox_broker_protocol::RequestId; -use litebox_broker_protocol::channel::{ - HostNotificationChannel, HostReceive, HostSetupChannel, LocalCallChannel, - LocalNotificationChannel, LocalSetupChannel, PeerCredential, -}; -use litebox_broker_protocol::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, - BrokerResponse, -}; -use litebox_broker_protocol::wire::{ - WireError, decode_handshake_request, decode_handshake_response, decode_notification, - decode_request, decode_response, encode_handshake_request, encode_handshake_response, - encode_notification, encode_request, encode_response, -}; - -const MAX_SETUP_FRAME_LEN: usize = 64 * 1024; -const CONTROL_RING_READY: &[u8] = b"litebox-control-ring-ready-v1"; -/// Maximum number of active calls waiting for broker responses. -pub const MAX_PENDING_CALLS: usize = 64; - -/// Validates that a connected Unix socket belongs to `expected_process_id`. -pub fn validate_peer_process(stream: &UnixStream, expected_process_id: u32) -> IoResult<()> { - if peer_process_id(stream)? != expected_process_id { - return Err(Error::new( - ErrorKind::PermissionDenied, - "Unix socket peer is not the expected process", - )); - } - Ok(()) -} - -fn peer_process_id(stream: &UnixStream) -> IoResult { - let credentials = rustix::net::sockopt::socket_peercred(stream)?; - u32::try_from(credentials.pid.as_raw_pid()) - .map_err(|_| invalid_data("Unix peer process ID is invalid")) -} - -/// Local-side broker association setup channel over a Unix stream. -pub struct UnixStreamLocalSetupChannel { - stream: UnixStream, - setup_deadline: Option, - negotiated: bool, -} - -/// Call-issuing endpoint of an active local control-ring association. -pub struct UnixControlRingLocalCallChannel { - association: Arc, -} - -/// Independently owned handle for interrupting all local active-ring I/O. -pub struct UnixControlRingLocalShutdown { - association: Arc, -} - -/// State shared by every activated local endpoint of one association: the -/// request producer, the setup socket used for liveness and teardown, pending -/// call tracking, and the wake handles of all three ring directions. -struct LocalRingAssociation { - request_producer: Mutex>, - control_stream: UnixStream, - pending_calls: Arc, - on_failure: Arc, - request_wake: ControlRingWakeHandle, - response_wake: ControlRingWakeHandle, - notification_wake: ControlRingWakeHandle, -} - -/// Local notification receiver for a shared-ring Unix broker association. -pub struct UnixControlRingLocalNotificationChannel { - consumer: ControlRingConsumer, - association: Arc, -} - -impl UnixStreamLocalSetupChannel { - /// Creates a local setup channel from an already-connected Unix stream. - pub const fn from_connected(stream: UnixStream) -> Self { - Self { - stream, - setup_deadline: None, - negotiated: false, - } - } - - /// Connects to a userland broker Unix socket. - pub fn connect(path: impl AsRef) -> IoResult { - UnixStream::connect(path).map(Self::from_connected) - } - - /// Connects to a userland broker Unix socket with a deadline for setup I/O. - /// - /// TODO: `UnixStream` does not expose a connect timeout, so this - /// deadline currently covers setup I/O after the initial connect - /// succeeds, but not a blocking connect call. - pub fn connect_with_setup_deadline( - path: impl AsRef, - deadline: Instant, - ) -> IoResult { - UnixStream::connect(path).map(|stream| Self { - stream, - setup_deadline: Some(deadline), - negotiated: false, - }) - } - - /// Receives one memfd offered by the broker during setup. - pub fn receive_memfd( - &mut self, - expected_len: usize, - deadline: Option, - ) -> IoResult { - crate::shared_memory::receive_memfd(&mut self.stream, expected_len, deadline) - } - - /// Consumes a negotiated setup channel into independently usable active - /// call, notification, and shutdown handles, starting the response - /// dispatcher and liveness monitor. - /// - /// The ring must be the validated control-ring memfd received during this - /// setup exchange. - pub fn into_active( - self, - ring: ControlRing, - on_failure: impl Fn() + Send + Sync + 'static, - ) -> IoResult<( - UnixControlRingLocalCallChannel, - UnixControlRingLocalNotificationChannel, - UnixControlRingLocalShutdown, - )> { - if !self.negotiated { - return Err(invalid_data( - "broker local setup channel activated before negotiation completed", - )); - } - - let mut setup_stream = self.stream; - write_setup_frame(&mut setup_stream, CONTROL_RING_READY, self.setup_deadline)?; - let Some(ready) = read_setup_frame(&mut setup_stream, self.setup_deadline)? else { - return Err(Error::new( - ErrorKind::UnexpectedEof, - "broker closed before control-ring setup acknowledgement", - )); - }; - if ready != CONTROL_RING_READY { - return Err(invalid_data( - "broker sent an invalid control-ring setup acknowledgement", - )); - } - - let shutdown_stream = setup_stream.try_clone()?; - let crate::control_ring::LocalControlRingEndpoints { - request_producer, - response_consumer, - notification_consumer, - } = ring.into_local(); - let pending_calls = Arc::new(PendingCalls::new()); - let on_failure: Arc = Arc::new(on_failure); - let association = Arc::new(LocalRingAssociation { - request_wake: request_producer.wake_handle(), - request_producer: Mutex::new(request_producer), - control_stream: shutdown_stream, - pending_calls: Arc::clone(&pending_calls), - on_failure, - response_wake: response_consumer.wake_handle(), - notification_wake: notification_consumer.wake_handle(), - }); - let response_association = Arc::clone(&association); - if let Err(error) = thread::Builder::new() - .name("litebox-broker-responses".to_owned()) - .spawn(move || { - dispatch_responses(response_consumer, response_association); - }) - { - let _ = association.fail(error); - return Err(Error::other("failed to start broker response dispatcher")); - } - let monitor_association = Arc::clone(&association); - if let Err(error) = thread::Builder::new() - .name("litebox-broker-liveness".to_owned()) - .spawn(move || { - monitor_local_socket(&mut setup_stream, &monitor_association); - }) - { - let _ = association.fail(error); - return Err(Error::other("failed to start broker liveness monitor")); - } - - Ok(( - UnixControlRingLocalCallChannel { - association: Arc::clone(&association), - }, - UnixControlRingLocalNotificationChannel { - consumer: notification_consumer, - association: Arc::clone(&association), - }, - UnixControlRingLocalShutdown { association }, - )) - } -} - -impl UnixControlRingLocalShutdown { - /// Shuts down the active association, unblocking ring and socket waits. - pub fn shutdown(&self) -> IoResult<()> { - self.association.fail(Error::new( - ErrorKind::ConnectionAborted, - "broker local association shut down", - )) - } -} - -impl Drop for UnixControlRingLocalCallChannel { - fn drop(&mut self) { - let _ = self.association.fail(Error::new( - ErrorKind::ConnectionAborted, - "broker local call channel dropped", - )); - } -} - -fn shutdown_socket(stream: &UnixStream) -> IoResult<()> { - match stream.shutdown(Shutdown::Both) { - Err(error) if error.kind() == ErrorKind::NotConnected => Ok(()), - result => result, - } -} - -/// Host-side broker association setup channel over a Unix stream. -pub struct UnixStreamHostSetupChannel { - stream: UnixStream, - peer_credential: PeerCredential, - setup_deadline: Option, - negotiated: bool, -} - -/// Request-reading endpoint of an active host control-ring association. -pub struct UnixControlRingHostRequestSource { - consumer: ControlRingConsumer, - association: Arc, -} - -/// Shared response-writing endpoint of an active host control-ring association. -#[derive(Clone)] -pub struct UnixControlRingHostResponseSink { - producer: Arc>>, - association: Arc, -} - -/// RAII guard that interrupts all active host ring I/O when dropped. -pub struct UnixControlRingHostShutdown { - association: Arc, -} - -/// State shared by every activated host endpoint of one association: the setup -/// socket used for liveness and teardown, terminal status, and the wake handles -/// of all three ring directions. -struct HostRingAssociation { - control_stream: UnixStream, - status: Mutex, - request_wake: ControlRingWakeHandle, - response_wake: ControlRingWakeHandle, - notification_wake: ControlRingWakeHandle, -} - -enum HostAssociationStatus { - Live, - PeerClosed, - Failed(Arc), -} - -/// Host notification sender for a shared-ring Unix broker association. -pub struct UnixControlRingHostNotificationChannel { - producer: ControlRingProducer, - association: Arc, -} - -impl UnixStreamHostSetupChannel { - /// Creates a host setup channel from an accepted Unix stream. - pub const fn from_accepted(stream: UnixStream) -> Self { - Self { - stream, - peer_credential: PeerCredential::Unauthenticated, - setup_deadline: None, - negotiated: false, - } - } - - /// Creates a host setup channel after the deployment has authenticated - /// and bound the accepted peer. `setup_deadline` bounds handshake I/O. - pub const fn from_host_guaranteed(stream: UnixStream, setup_deadline: Instant) -> Self { - Self { - stream, - peer_credential: PeerCredential::HostGuaranteed, - setup_deadline: Some(setup_deadline), - negotiated: false, - } - } - - /// Sends a memfd during association setup. - pub fn send_memfd( - &mut self, - shared_memory: &MemfdSharedMemory, - deadline: Option, - ) -> IoResult<()> { - crate::shared_memory::send_memfd(&mut self.stream, shared_memory, deadline) - } - - /// Consumes a negotiated setup channel into independently usable active - /// request, response, notification, and shutdown handles. - pub fn into_active( - mut self, - ring: ControlRing, - ) -> IoResult<( - UnixControlRingHostRequestSource, - UnixControlRingHostResponseSink, - UnixControlRingHostNotificationChannel, - UnixControlRingHostShutdown, - )> { - if !self.negotiated { - return Err(invalid_data( - "broker host setup channel activated before negotiation completed", - )); - } - let Some(ready) = read_setup_frame(&mut self.stream, self.setup_deadline)? else { - return Err(Error::new( - ErrorKind::UnexpectedEof, - "runner closed before control-ring setup acknowledgement", - )); - }; - if ready != CONTROL_RING_READY { - return Err(invalid_data( - "runner sent an invalid control-ring setup acknowledgement", - )); - } - write_setup_frame(&mut self.stream, CONTROL_RING_READY, self.setup_deadline)?; - - let shutdown_stream = self.stream.try_clone()?; - let crate::control_ring::BrokerControlRingEndpoints { - request_consumer, - response_producer, - notification_producer, - } = ring.into_broker(); - let association = Arc::new(HostRingAssociation { - control_stream: shutdown_stream, - status: Mutex::new(HostAssociationStatus::Live), - request_wake: request_consumer.wake_handle(), - response_wake: response_producer.wake_handle(), - notification_wake: notification_producer.wake_handle(), - }); - let monitor_association = Arc::clone(&association); - thread::Builder::new() - .name("litebox-runner-liveness".to_owned()) - .spawn(move || monitor_host_socket(&mut self.stream, &monitor_association))?; - Ok(( - UnixControlRingHostRequestSource { - consumer: request_consumer, - association: Arc::clone(&association), - }, - UnixControlRingHostResponseSink { - producer: Arc::new(Mutex::new(response_producer)), - association: Arc::clone(&association), - }, - UnixControlRingHostNotificationChannel { - producer: notification_producer, - association: Arc::clone(&association), - }, - UnixControlRingHostShutdown { association }, - )) - } -} - -impl UnixControlRingHostShutdown { - /// Shuts down the active association without waiting for a ring lock. - pub fn shutdown(&self) -> IoResult<()> { - self.association.fail(Error::new( - ErrorKind::ConnectionAborted, - "broker host association shut down", - )) - } -} - -impl Drop for UnixControlRingHostShutdown { - fn drop(&mut self) { - let _ = self.association.fail(Error::new( - ErrorKind::ConnectionAborted, - "broker host association shutdown guard dropped", - )); - } -} - -impl LocalSetupChannel for UnixStreamLocalSetupChannel { - type Error = Error; - - fn send_handshake_request(&mut self, request: &BrokerHandshakeRequest) -> IoResult<()> { - let frame = encode_handshake_request(request.clone()); - write_setup_frame(&mut self.stream, &frame, self.setup_deadline) - } - - fn recv_handshake_response(&mut self) -> IoResult> { - let frame = read_setup_frame(&mut self.stream, self.setup_deadline)?; - match frame { - Some(frame) => { - let response = decode_handshake_response(&frame).map_err(wire_error)?; - self.negotiated = matches!(&response, BrokerHandshakeResponse::Negotiated { .. }); - Ok(Some(response)) - } - None => Ok(None), - } - } -} - -impl LocalCallChannel for UnixControlRingLocalCallChannel { - type Error = Error; - - fn call(&self, request: BrokerRequest) -> IoResult { - let association = &self.association; - let request_id = request.request_id; - let pending_call = association.pending_calls.register(request_id)?; - let request_frame = encode_request(request); - - let write_result = { - let mut producer = association - .request_producer - .lock() - .expect("broker request writer mutex poisoned"); - loop { - let write_status = association - .pending_calls - .run_if_live(|| producer.try_write(&request_frame).map_err(Error::from)); - match write_status { - Ok(ControlRingWriteStatus::Written) => { - if let Err(error) = producer.wake_consumer() { - break Err(error); - } - break Ok(()); - } - Ok(ControlRingWriteStatus::Full { wait_epoch }) => { - if let Err(error) = producer.wait_for_capacity(wait_epoch) { - break Err(error); - } - } - Err(error) => break Err(error), - } - } - }; - if let Err(error) = write_result { - let _ = association.fail(error); - } - - pending_call.wait() - } -} - -impl HostSetupChannel for UnixStreamHostSetupChannel { - type Error = Error; - - fn peer_credential(&self) -> IoResult { - Ok(self.peer_credential) - } - - fn recv_handshake_request(&mut self) -> IoResult> { - let Some(frame) = read_setup_frame(&mut self.stream, self.setup_deadline)? else { - return Ok(HostReceive::PeerClosed); - }; - match decode_handshake_request(&frame) { - Ok(request) => Ok(HostReceive::Message(request)), - Err(WireError::WrongMessagePhase) => Ok(HostReceive::ProtocolViolation), - Err(error) => Err(wire_error(error)), - } - } - - fn send_handshake_response(&mut self, response: &BrokerHandshakeResponse) -> IoResult<()> { - write_setup_frame( - &mut self.stream, - &encode_handshake_response(response.clone()), - self.setup_deadline, - )?; - self.negotiated = matches!(response, BrokerHandshakeResponse::Negotiated { .. }); - Ok(()) - } -} - -impl UnixControlRingHostRequestSource { - /// Receives one active broker request. - pub fn recv_request(&mut self) -> IoResult> { - loop { - if let Some(error) = self.association.current_failure() { - return Err(error); - } - match self.consumer.try_read(decode_request) { - Ok(ControlRingReadStatus::Message(request)) => { - self.association.acknowledge_request(&mut self.consumer)?; - return Ok(HostReceive::Message(request)); - } - Ok(ControlRingReadStatus::Empty { wait_epoch }) => { - if let Some(terminal) = self.association.request_terminal_result() { - return terminal; - } - if let Err(error) = self.consumer.wait_for_message(wait_epoch) { - let result = Err(copy_io_error(&error)); - let _ = self.association.fail(error); - return result; - } - } - Err(ControlRingReadError::Decode(WireError::WrongMessagePhase)) => { - return Ok(HostReceive::ProtocolViolation); - } - Err(ControlRingReadError::Decode(error)) => { - let error = wire_error(error); - let result = Err(copy_io_error(&error)); - let _ = self.association.fail(error); - return result; - } - Err(ControlRingReadError::Ring(error)) => { - let error = Error::from(error); - let result = Err(copy_io_error(&error)); - let _ = self.association.fail(error); - return result; - } - } - } - } -} - -impl UnixControlRingHostResponseSink { - /// Serializes and sends one complete active broker response. - pub fn send_response(&self, response: &BrokerResponse) -> IoResult<()> { - let frame = encode_response(response.clone()); - let mut producer = self - .producer - .lock() - .map_err(|_| Error::other("broker response writer mutex poisoned"))?; - loop { - match self.association.try_publish(&mut producer, &frame)? { - ControlRingWriteStatus::Written => return Ok(()), - ControlRingWriteStatus::Full { wait_epoch } => { - if let Err(error) = producer.wait_for_capacity(wait_epoch) { - let result = Err(copy_io_error(&error)); - let _ = self.association.fail(error); - return result; - } - } - } - } - } -} - -impl LocalNotificationChannel for UnixControlRingLocalNotificationChannel { - type Error = Error; - - fn recv_notification(&mut self) -> IoResult> { - loop { - if let Some(error) = self.association.pending_calls.current_failure() { - return Err(copy_io_error(&error)); - } - match self.consumer.try_read(decode_notification) { - Ok(ControlRingReadStatus::Message(notification)) => { - self.association - .acknowledge_notification(&mut self.consumer)?; - return Ok(Some(notification)); - } - Ok(ControlRingReadStatus::Empty { wait_epoch }) => { - if let Some(error) = self.association.pending_calls.current_failure() { - return Err(copy_io_error(&error)); - } - if let Err(error) = self.consumer.wait_for_message(wait_epoch) { - let result = Err(copy_io_error(&error)); - let _ = self.association.fail(error); - return result; - } - } - Err(ControlRingReadError::Ring(error)) => { - let error = Error::from(error); - let result = Err(copy_io_error(&error)); - let _ = self.association.fail(error); - return result; - } - Err(ControlRingReadError::Decode(error)) => { - let error = wire_error(error); - let result = Err(copy_io_error(&error)); - let _ = self.association.fail(error); - return result; - } - } - } - } -} - -impl HostNotificationChannel for UnixControlRingHostNotificationChannel { - type Error = Error; - - fn send_notification(&mut self, notification: &BrokerNotification) -> IoResult<()> { - let frame = encode_notification(notification.clone()); - loop { - match self.association.try_publish(&mut self.producer, &frame)? { - ControlRingWriteStatus::Written => return Ok(()), - ControlRingWriteStatus::Full { wait_epoch } => { - if let Err(error) = self.producer.wait_for_capacity(wait_epoch) { - let result = Err(copy_io_error(&error)); - let _ = self.association.fail(error); - return result; - } - } - } - } - } -} - -struct PendingCalls { - state: Mutex, - capacity_available: Condvar, -} - -struct PendingCallsState { - calls: HashMap>, - failure: Option>, -} - -struct PendingCall { - result: Mutex>, - result_ready: Condvar, -} - -enum PendingCallResult { - Response(BrokerResponse), - Failure(Arc), -} - -impl PendingCall { - fn new() -> Self { - Self { - result: Mutex::new(None), - result_ready: Condvar::new(), - } - } - - fn resolve(&self, result: PendingCallResult) { - let mut stored = self - .result - .lock() - .expect("broker pending-call result mutex poisoned"); - assert!(stored.is_none(), "broker pending call already resolved"); - *stored = Some(result); - self.result_ready.notify_one(); - } - - fn wait(&self) -> IoResult { - let mut result = self - .result - .lock() - .expect("broker pending-call result mutex poisoned"); - loop { - if let Some(result) = result.take() { - return match result { - PendingCallResult::Response(response) => Ok(response), - PendingCallResult::Failure(error) => Err(copy_io_error(&error)), - }; - } - result = self - .result_ready - .wait(result) - .expect("broker pending-call result mutex poisoned"); - } - } -} - -impl PendingCalls { - fn new() -> Self { - Self { - state: Mutex::new(PendingCallsState { - calls: HashMap::new(), - failure: None, - }), - capacity_available: Condvar::new(), - } - } - - fn register(&self, request_id: RequestId) -> IoResult> { - let pending_call = Arc::new(PendingCall::new()); - let mut state = self.state.lock().expect("broker pending mutex poisoned"); - while state.calls.len() == MAX_PENDING_CALLS && state.failure.is_none() { - state = self - .capacity_available - .wait(state) - .expect("broker pending mutex poisoned"); - } - if let Some(error) = state.failure.as_ref() { - return Err(copy_io_error(error)); - } - match state.calls.entry(request_id) { - std::collections::hash_map::Entry::Vacant(entry) => { - entry.insert(Arc::clone(&pending_call)); - } - std::collections::hash_map::Entry::Occupied(_) => { - return Err(invalid_data("duplicate broker request ID")); - } - } - Ok(pending_call) - } - - fn complete(&self, response: BrokerResponse) -> IoResult<()> { - let pending_call = { - let mut state = self.state.lock().expect("broker pending mutex poisoned"); - if let Some(error) = state.failure.as_ref() { - return Err(copy_io_error(error)); - } - let Some(pending_call) = state.calls.remove(&response.request_id) else { - return Err(invalid_data("broker returned an unknown response ID")); - }; - self.capacity_available.notify_one(); - pending_call - }; - pending_call.resolve(PendingCallResult::Response(response)); - Ok(()) - } - - fn record_failure(&self, error: Arc) -> bool { - let pending_calls = { - let mut state = self.state.lock().expect("broker pending mutex poisoned"); - if state.failure.is_some() { - return false; - } - state.failure = Some(Arc::clone(&error)); - let pending_calls = core::mem::take(&mut state.calls); - self.capacity_available.notify_all(); - pending_calls - }; - for pending_call in pending_calls.into_values() { - pending_call.resolve(PendingCallResult::Failure(Arc::clone(&error))); - } - true - } - - fn current_failure(&self) -> Option> { - self.state - .lock() - .expect("broker pending mutex poisoned") - .failure - .as_ref() - .map(Arc::clone) - } - - /// Runs a nonblocking publication while excluding failure recording. - fn run_if_live(&self, operation: impl FnOnce() -> IoResult) -> IoResult { - let state = self.state.lock().expect("broker pending mutex poisoned"); - if let Some(error) = state.failure.as_ref() { - return Err(copy_io_error(error)); - } - operation() - } -} - -impl LocalRingAssociation { - fn acknowledge_notification( - &self, - consumer: &mut ControlRingConsumer, - ) -> IoResult<()> { - let result = self.pending_calls.run_if_live(|| { - consumer - .publish_head() - .map_err(Error::from) - .and_then(|()| consumer.wake_producer()) - }); - if let Err(error) = result { - let result = Err(copy_io_error(&error)); - let _ = self.fail(error); - return result; - } - Ok(()) - } - - fn fail(&self, error: Error) -> IoResult<()> { - let first_failure = self.pending_calls.record_failure(Arc::new(error)); - let request_wake = self.request_wake.interrupt_wait(); - let response_wake = self.response_wake.interrupt_wait(); - let notification_wake = self.notification_wake.interrupt_wait(); - let shutdown_result = shutdown_socket(&self.control_stream); - if first_failure { - (self.on_failure)(); - } - request_wake - .and(response_wake) - .and(notification_wake) - .and(shutdown_result) - } -} - -impl HostRingAssociation { - fn acknowledge_request( - &self, - consumer: &mut ControlRingConsumer, - ) -> IoResult<()> { - let result = { - let status = self - .status - .lock() - .expect("broker host association mutex poisoned"); - if let HostAssociationStatus::Failed(error) = &*status { - return Err(copy_io_error(error)); - } - consumer - .publish_head() - .map_err(Error::from) - .and_then(|()| consumer.wake_producer()) - }; - if let Err(error) = result { - let result = Err(copy_io_error(&error)); - let _ = self.fail(error); - return result; - } - Ok(()) - } - - fn fail(&self, error: Error) -> IoResult<()> { - { - let mut status = self - .status - .lock() - .expect("broker host association mutex poisoned"); - if matches!(*status, HostAssociationStatus::Live) { - *status = HostAssociationStatus::Failed(Arc::new(error)); - } - } - let request_wake = self.request_wake.interrupt_wait(); - let response_wake = self.response_wake.interrupt_wait(); - let notification_wake = self.notification_wake.interrupt_wait(); - request_wake - .and(response_wake) - .and(notification_wake) - .and(shutdown_socket(&self.control_stream)) - } - - fn peer_closed(&self) { - { - let mut status = self - .status - .lock() - .expect("broker host association mutex poisoned"); - if matches!(*status, HostAssociationStatus::Live) { - *status = HostAssociationStatus::PeerClosed; - } - } - let _ = self.request_wake.interrupt_wait(); - let _ = self.response_wake.interrupt_wait(); - let _ = self.notification_wake.interrupt_wait(); - } - - fn request_terminal_result(&self) -> Option>> { - match &*self - .status - .lock() - .expect("broker host association mutex poisoned") - { - HostAssociationStatus::Live => None, - HostAssociationStatus::PeerClosed => Some(Ok(HostReceive::PeerClosed)), - HostAssociationStatus::Failed(error) => Some(Err(copy_io_error(error))), - } - } - - fn current_failure(&self) -> Option { - match &*self - .status - .lock() - .expect("broker host association mutex poisoned") - { - HostAssociationStatus::Failed(error) => Some(copy_io_error(error)), - HostAssociationStatus::Live | HostAssociationStatus::PeerClosed => None, - } - } - - fn try_publish( - &self, - producer: &mut ControlRingProducer, - frame: &[u8], - ) -> IoResult { - let result = { - let status = self - .status - .lock() - .expect("broker host association mutex poisoned"); - match &*status { - HostAssociationStatus::Live => {} - HostAssociationStatus::PeerClosed => { - return Err(Error::new( - ErrorKind::BrokenPipe, - "runner closed the active broker association", - )); - } - HostAssociationStatus::Failed(error) => return Err(copy_io_error(error)), - } - producer - .try_write(frame) - .map_err(Error::from) - .and_then(|write_status| { - if matches!(write_status, ControlRingWriteStatus::Written) { - producer.wake_consumer()?; - } - Ok(write_status) - }) - }; - if let Err(error) = result { - let result = Err(copy_io_error(&error)); - let _ = self.fail(error); - return result; - } - result - } -} - -fn monitor_local_socket(stream: &mut UnixStream, association: &LocalRingAssociation) { - let error = wait_for_socket_termination(stream, "broker"); - let _ = association.fail(error); -} - -fn monitor_host_socket(stream: &mut UnixStream, association: &HostRingAssociation) { - let mut byte = [0]; - loop { - match stream.read(&mut byte) { - Ok(0) => { - association.peer_closed(); - return; - } - Ok(_) => { - let _ = association.fail(invalid_data( - "runner sent unexpected active control-socket data", - )); - return; - } - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) => { - let _ = association.fail(error); - return; - } - } - } -} - -fn wait_for_socket_termination(stream: &mut UnixStream, peer: &'static str) -> Error { - let mut byte = [0]; - loop { - match stream.read(&mut byte) { - Ok(0) => { - return Error::new( - ErrorKind::UnexpectedEof, - format!("{peer} closed the active broker association"), - ); - } - Ok(_) => { - return invalid_data("peer sent unexpected active control-socket data"); - } - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) => return error, - } - } -} - -fn dispatch_responses( - mut consumer: ControlRingConsumer, - association: Arc, -) { - loop { - match consumer.try_read(decode_response) { - Ok(ControlRingReadStatus::Message(response)) => { - if let Err(error) = consumer - .publish_head() - .map_err(Error::from) - .and_then(|()| consumer.wake_producer()) - .and_then(|()| association.pending_calls.complete(response)) - { - let _ = association.fail(error); - return; - } - } - Ok(ControlRingReadStatus::Empty { wait_epoch }) => { - if association.pending_calls.current_failure().is_some() { - return; - } - if let Err(error) = consumer.wait_for_message(wait_epoch) { - let _ = association.fail(error); - return; - } - } - Err(ControlRingReadError::Ring(error)) => { - let _ = association.fail(Error::from(error)); - return; - } - Err(ControlRingReadError::Decode(error)) => { - let _ = association.fail(wire_error(error)); - return; - } - } - } -} - -fn copy_io_error(error: &Error) -> Error { - match error.raw_os_error() { - Some(code) => Error::from_raw_os_error(code), - None => Error::new(error.kind(), error.to_string()), - } -} - -fn read_setup_frame( - stream: &mut UnixStream, - deadline: Option, -) -> IoResult>> { - with_read_deadline(stream, deadline, |stream, deadline| { - let mut len_buf = [0; 4]; - let mut read = 0; - while read < len_buf.len() { - refresh_read_deadline(stream, deadline)?; - match stream.read(&mut len_buf[read..]) { - Ok(0) if read == 0 => return Ok(None), - Ok(0) => return Err(invalid_data("truncated broker setup frame length")), - Ok(len) => read += len, - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) => return Err(error), - } - } - - let len = u32::from_le_bytes(len_buf) as usize; - if len == 0 || len > MAX_SETUP_FRAME_LEN { - return Err(invalid_data("invalid broker setup frame length")); - } - - let mut frame = vec![0; len]; - let mut read = 0; - while read < frame.len() { - refresh_read_deadline(stream, deadline)?; - match stream.read(&mut frame[read..]) { - Ok(0) => return Err(invalid_data("truncated broker setup frame")), - Ok(len) => read += len, - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) => return Err(error), - } - } - Ok(Some(frame)) - }) -} - -fn write_setup_frame( - stream: &mut UnixStream, - frame: &[u8], - deadline: Option, -) -> IoResult<()> { - with_write_deadline(stream, deadline, |stream, deadline| { - if frame.is_empty() || frame.len() > MAX_SETUP_FRAME_LEN { - return Err(invalid_data("invalid broker setup frame length")); - } - let len = - u32::try_from(frame.len()).map_err(|_| invalid_data("broker setup frame too large"))?; - write_all_with_deadline(stream, &len.to_le_bytes(), deadline)?; - write_all_with_deadline(stream, frame, deadline) - }) -} - -fn write_all_with_deadline( - stream: &mut UnixStream, - mut buffer: &[u8], - deadline: Option, -) -> IoResult<()> { - while !buffer.is_empty() { - refresh_write_deadline(stream, deadline)?; - match stream.write(buffer) { - Ok(0) => { - return Err(Error::new( - ErrorKind::WriteZero, - "failed to write broker setup frame", - )); - } - Ok(written) => buffer = &buffer[written..], - Err(error) if error.kind() == ErrorKind::Interrupted => {} - Err(error) => return Err(error), - } - } - Ok(()) -} - -fn invalid_data(message: &'static str) -> Error { - Error::new(ErrorKind::InvalidData, message) -} - -fn wire_error(error: WireError) -> Error { - Error::new( - ErrorKind::InvalidData, - format!("invalid broker wire message: {error}"), - ) -} - -impl From for Error { - fn from(error: ControlRingError) -> Self { - Self::new( - ErrorKind::InvalidData, - format!("invalid broker control ring: {error:?}"), - ) - } -} - -#[cfg(test)] -mod control_ring_tests { - use super::*; - use crate::control_ring::{ - CONTROL_RING_MEMORY_SIZE, CONTROL_RING_SLOT_COUNT, ControlRingProducer, - }; - use litebox_broker_protocol::channel::{LocalCallChannel, LocalSetupChannel}; - use litebox_broker_protocol::message::{ - BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, - }; - use litebox_broker_protocol::wire::{ - decode_request, decode_response, encode_handshake_request, encode_response, - }; - use litebox_broker_protocol::{ObjectHandle, RequestId}; - use std::io::{Read, Write}; - use std::os::fd::AsFd; - use std::sync::Barrier; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::time::Duration; - - type Producer = ControlRingProducer; - type Consumer = ControlRingConsumer; - - fn ring_pair() -> ( - ControlRing, - ControlRing, - ) { - let first = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); - let second = MemfdSharedMemory::from_received_fd( - first.as_fd().try_clone_to_owned().unwrap(), - CONTROL_RING_MEMORY_SIZE, - ) - .unwrap(); - ( - ControlRing::new(first).unwrap(), - ControlRing::new(second).unwrap(), - ) - } - - fn negotiated_local(stream: UnixStream) -> UnixStreamLocalSetupChannel { - UnixStreamLocalSetupChannel { - stream, - setup_deadline: Some(Instant::now() + Duration::from_secs(2)), - negotiated: true, - } - } - - fn activate_local( - on_failure: impl Fn() + Send + Sync + 'static, - ) -> ( - UnixControlRingLocalCallChannel, - UnixControlRingLocalShutdown, - Producer, - Consumer, - UnixStream, - ) { - let (local_stream, peer_stream) = UnixStream::pair().unwrap(); - let mut ack_stream = peer_stream.try_clone().unwrap(); - let acknowledgement = thread::spawn(move || { - assert_eq!( - read_setup_frame(&mut ack_stream, None).unwrap().unwrap(), - CONTROL_RING_READY - ); - write_setup_frame(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); - }); - let (local_ring, broker_ring) = ring_pair(); - let setup = negotiated_local(local_stream); - let (channel, _notifications, shutdown) = - setup.into_active(local_ring, on_failure).unwrap(); - acknowledgement.join().unwrap(); - let crate::control_ring::BrokerControlRingEndpoints { - request_consumer, - response_producer, - notification_producer: _, - } = broker_ring.into_broker(); - ( - channel, - shutdown, - response_producer, - request_consumer, - peer_stream, - ) - } - - fn activate_host() -> ( - UnixControlRingHostRequestSource, - UnixControlRingHostResponseSink, - UnixControlRingHostShutdown, - Producer, - Consumer, - UnixStream, - ) { - let (peer_stream, host_stream) = UnixStream::pair().unwrap(); - let mut ack_stream = peer_stream.try_clone().unwrap(); - let acknowledgement = thread::spawn(move || { - write_setup_frame(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); - assert_eq!( - read_setup_frame(&mut ack_stream, None).unwrap().unwrap(), - CONTROL_RING_READY - ); - }); - let (local_ring, host_ring) = ring_pair(); - let channel = UnixStreamHostSetupChannel { - stream: host_stream, - peer_credential: PeerCredential::HostGuaranteed, - setup_deadline: Some(Instant::now() + Duration::from_secs(2)), - negotiated: true, - }; - let (source, sink, _notifications, shutdown) = channel.into_active(host_ring).unwrap(); - acknowledgement.join().unwrap(); - let crate::control_ring::LocalControlRingEndpoints { - request_producer, - response_consumer, - notification_consumer: _, - } = local_ring.into_local(); - ( - source, - sink, - shutdown, - request_producer, - response_consumer, - peer_stream, - ) - } - - fn notification_channel_pair() -> ( - UnixControlRingLocalCallChannel, - UnixControlRingLocalNotificationChannel, - UnixControlRingHostNotificationChannel, - UnixControlRingHostShutdown, - ) { - let (local_stream, host_stream) = UnixStream::pair().unwrap(); - let (local_ring, host_ring) = ring_pair(); - let local_setup = negotiated_local(local_stream); - let host_control = UnixStreamHostSetupChannel { - stream: host_stream, - peer_credential: PeerCredential::HostGuaranteed, - setup_deadline: Some(Instant::now() + Duration::from_secs(2)), - negotiated: true, - }; - let host_active = thread::spawn(move || host_control.into_active(host_ring).unwrap()); - let (local_call, local_notifications, _local_shutdown) = - local_setup.into_active(local_ring, || {}).unwrap(); - let (_source, _sink, host_notifications, shutdown) = host_active.join().unwrap(); - ( - local_call, - local_notifications, - host_notifications, - shutdown, - ) - } - - fn read_request(consumer: &mut Consumer) -> BrokerRequest { - loop { - match consumer.try_read(decode_request).unwrap() { - ControlRingReadStatus::Message(request) => { - consumer.publish_head().unwrap(); - consumer.wake_producer().unwrap(); - return request; - } - ControlRingReadStatus::Empty { wait_epoch } => { - consumer.wait_for_message(wait_epoch).unwrap(); - } - } - } - } - - fn read_response(consumer: &mut Consumer) -> BrokerResponse { - loop { - match consumer.try_read(decode_response).unwrap() { - ControlRingReadStatus::Message(response) => { - consumer.publish_head().unwrap(); - consumer.wake_producer().unwrap(); - return response; - } - ControlRingReadStatus::Empty { wait_epoch } => { - consumer.wait_for_message(wait_epoch).unwrap(); - } - } - } - } - - fn write_payload(producer: &mut Producer, payload: &[u8]) { - loop { - match producer.try_write(payload).unwrap() { - ControlRingWriteStatus::Written => { - producer.wake_consumer().unwrap(); - return; - } - ControlRingWriteStatus::Full { wait_epoch } => { - producer.wait_for_capacity(wait_epoch).unwrap(); - } - } - } - } - - fn request(id: u64) -> BrokerRequest { - BrokerRequest { - request_id: RequestId(id), - operation: BrokerOperation::CloseObject(ObjectHandle(id)), - } - } - - fn response(id: RequestId) -> BrokerResponse { - BrokerResponse { - request_id: id, - result: BrokerResult::ObjectClosed, - } - } - - #[test] - fn linux_peer_validation_identifies_connected_process() { - let (first, _second) = UnixStream::pair().unwrap(); - - validate_peer_process(&first, std::process::id()).unwrap(); - let unexpected_process_id = std::process::id().checked_add(1).unwrap(); - assert_eq!( - validate_peer_process(&first, unexpected_process_id) - .unwrap_err() - .kind(), - ErrorKind::PermissionDenied - ); - } - - #[test] - fn setup_frames_round_trip_and_reject_invalid_boundaries() { - let (mut writer, mut reader) = UnixStream::pair().unwrap(); - write_setup_frame(&mut writer, &[1, 2, 3], None).unwrap(); - assert_eq!( - read_setup_frame(&mut reader, None).unwrap().unwrap(), - [1, 2, 3] - ); - - let (writer, mut reader) = UnixStream::pair().unwrap(); - drop(writer); - assert!(read_setup_frame(&mut reader, None).unwrap().is_none()); - - for frame_prefix in [ - vec![1, 0], - 0u32.to_le_bytes().to_vec(), - u32::try_from(MAX_SETUP_FRAME_LEN + 1) - .unwrap() - .to_le_bytes() - .to_vec(), - ] { - let (mut writer, mut reader) = UnixStream::pair().unwrap(); - writer.write_all(&frame_prefix).unwrap(); - drop(writer); - assert_eq!( - read_setup_frame(&mut reader, None).unwrap_err().kind(), - ErrorKind::InvalidData - ); - } - - let (mut writer, mut reader) = UnixStream::pair().unwrap(); - writer.write_all(&4u32.to_le_bytes()).unwrap(); - writer.write_all(&[1, 2]).unwrap(); - drop(writer); - assert_eq!( - read_setup_frame(&mut reader, None).unwrap_err().kind(), - ErrorKind::InvalidData - ); - } - - #[test] - fn local_setup_rejects_activation_before_negotiation() { - let (local_stream, _host_stream) = UnixStream::pair().unwrap(); - let setup = UnixStreamLocalSetupChannel::from_connected(local_stream); - let (ring, _) = ring_pair(); - let Err(error) = setup.into_active(ring, || {}) else { - panic!("local setup channel activated before negotiation"); - }; - assert_eq!(error.kind(), ErrorKind::InvalidData); - } - - #[test] - fn local_setup_negotiates_then_activates_and_closes_on_drop() { - let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); - let mut setup = UnixStreamLocalSetupChannel::from_connected(local_stream); - - let handshake_request = BrokerHandshakeRequest { - protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, - }; - setup.send_handshake_request(&handshake_request).unwrap(); - assert_eq!( - decode_handshake_request(&read_setup_frame(&mut host_stream, None).unwrap().unwrap()) - .unwrap(), - handshake_request - ); - write_setup_frame( - &mut host_stream, - &encode_handshake_response(BrokerHandshakeResponse::Negotiated { - broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, - }), - None, - ) - .unwrap(); - assert!(matches!( - setup.recv_handshake_response().unwrap(), - Some(BrokerHandshakeResponse::Negotiated { .. }) - )); - - let acknowledgement = thread::spawn(move || { - assert_eq!( - read_setup_frame(&mut host_stream, None).unwrap().unwrap(), - CONTROL_RING_READY - ); - write_setup_frame(&mut host_stream, CONTROL_RING_READY, None).unwrap(); - host_stream - }); - let (ring, _) = ring_pair(); - let (call_channel, _notifications, _shutdown) = setup.into_active(ring, || {}).unwrap(); - let mut host_stream = acknowledgement.join().unwrap(); - - host_stream - .set_read_timeout(Some(Duration::from_secs(1))) - .unwrap(); - drop(call_channel); - let mut byte = [0]; - assert_eq!(host_stream.read(&mut byte).unwrap(), 0); - } - - #[test] - fn two_way_ready_ack_activates_ring_transport() { - let (local_stream, host_stream) = UnixStream::pair().unwrap(); - let (local_ring, host_ring) = ring_pair(); - let local_setup = negotiated_local(local_stream); - let host = UnixStreamHostSetupChannel { - stream: host_stream, - peer_credential: PeerCredential::HostGuaranteed, - setup_deadline: Some(Instant::now() + Duration::from_secs(2)), - negotiated: true, - }; - let host_active = thread::spawn(move || host.into_active(host_ring).unwrap()); - let (local, _local_notifications, _local_shutdown) = - local_setup.into_active(local_ring, || {}).unwrap(); - let (mut source, sink, _host_notifications, _shutdown) = host_active.join().unwrap(); - - let caller = thread::spawn(move || local.call(request(7))); - let HostReceive::Message(received) = source.recv_request().unwrap() else { - panic!("expected ring request"); - }; - sink.send_response(&response(received.request_id)).unwrap(); - assert_eq!(caller.join().unwrap().unwrap().request_id, RequestId(7)); - } - - #[test] - fn local_matches_out_of_order_ring_responses_without_socket_frames() { - let (channel, _shutdown, mut responses, mut requests, mut peer) = activate_local(|| {}); - peer.set_read_timeout(Some(Duration::from_millis(100))) - .unwrap(); - let channel = Arc::new(channel); - let calls = [3, 7].map(|id| { - let channel = Arc::clone(&channel); - thread::spawn(move || channel.call(request(id))) - }); - let first = read_request(&mut requests); - let second = read_request(&mut requests); - - write_payload( - &mut responses, - &encode_response(response(second.request_id)), - ); - write_payload(&mut responses, &encode_response(response(first.request_id))); - for call in calls { - assert!(call.join().unwrap().is_ok()); - } - let mut byte = [0]; - assert!(matches!( - peer.read(&mut byte).unwrap_err().kind(), - ErrorKind::WouldBlock | ErrorKind::TimedOut - )); - } - - #[test] - fn pending_capacity_blocks_before_sixty_fifth_publication() { - let (channel, shutdown, mut responses, mut requests, _peer) = activate_local(|| {}); - let channel = Arc::new(channel); - let start = Arc::new(Barrier::new(MAX_PENDING_CALLS + 2)); - let callers = (0..=MAX_PENDING_CALLS) - .map(|id| { - let channel = Arc::clone(&channel); - let start = Arc::clone(&start); - thread::spawn(move || { - start.wait(); - channel.call(request(id as u64)) - }) - }) - .collect::>(); - start.wait(); - - let mut published = Vec::new(); - for _ in 0..MAX_PENDING_CALLS { - published.push(read_request(&mut requests).request_id); - } - write_payload(&mut responses, &encode_response(response(published[0]))); - let released = read_request(&mut requests).request_id; - assert!(!published.contains(&released)); - - shutdown.shutdown().unwrap(); - let completed = callers - .into_iter() - .map(|caller| usize::from(caller.join().unwrap().is_ok())) - .sum::(); - assert_eq!(completed, 1); - } - - #[test] - fn unknown_duplicate_and_malformed_responses_fail_closed() { - for payload_kind in 0..3 { - let failures = Arc::new(AtomicUsize::new(0)); - let callback_failures = Arc::clone(&failures); - let (channel, _shutdown, mut responses, mut requests, _peer) = - activate_local(move || { - callback_failures.fetch_add(1, Ordering::SeqCst); - }); - let channel = Arc::new(channel); - let calls = [1, 2].map(|id| { - let channel = Arc::clone(&channel); - thread::spawn(move || channel.call(request(id))) - }); - read_request(&mut requests); - read_request(&mut requests); - - match payload_kind { - 0 => write_payload(&mut responses, &encode_response(response(RequestId(99)))), - 1 => { - let duplicate = encode_response(response(RequestId(1))); - write_payload(&mut responses, &duplicate); - write_payload(&mut responses, &duplicate); - } - _ => write_payload(&mut responses, &[u8::MAX]), - } - - let results = calls.map(|call| call.join().unwrap()); - let error_count = results.iter().filter(|result| result.is_err()).count(); - assert_eq!(error_count, if payload_kind == 1 { 1 } else { 2 }); - assert_eq!(failures.load(Ordering::SeqCst), 1); - } - } - - #[test] - fn local_socket_eof_and_shutdown_wake_pending_calls() { - for close_peer in [false, true] { - let (channel, shutdown, _responses, mut requests, peer) = activate_local(|| {}); - let caller = thread::spawn(move || channel.call(request(1))); - read_request(&mut requests); - if close_peer { - drop(peer); - } else { - shutdown.shutdown().unwrap(); - } - assert!(caller.join().unwrap().is_err()); - } - } - - #[test] - fn host_activation_decodes_requests_and_cloned_sinks_publish_complete_responses() { - let (mut source, sink, _shutdown, mut requests, mut responses, _peer) = activate_host(); - write_payload(&mut requests, &encode_request(request(1))); - assert!(matches!( - source.recv_request().unwrap(), - HostReceive::Message(BrokerRequest { - request_id: RequestId(1), - .. - }) - )); - - let first = sink.clone(); - let writer = thread::spawn(move || first.send_response(&response(RequestId(3)))); - sink.send_response(&response(RequestId(7))).unwrap(); - writer.join().unwrap().unwrap(); - let mut ids = [ - read_response(&mut responses).request_id, - read_response(&mut responses).request_id, - ]; - ids.sort(); - assert_eq!(ids, [RequestId(3), RequestId(7)]); - } - - #[test] - fn host_clean_close_wakes_request_wait_as_peer_closed() { - let (mut source, _sink, _shutdown, _requests, _responses, peer) = activate_host(); - let receiver = thread::spawn(move || source.recv_request()); - drop(peer); - assert_eq!(receiver.join().unwrap().unwrap(), HostReceive::PeerClosed); - } - - #[test] - fn host_failure_preempts_queued_and_decoded_requests_but_peer_close_drains() { - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); - write_payload(&mut requests, &encode_request(request(1))); - source - .association - .fail(Error::new(ErrorKind::TimedOut, "test failure")) - .unwrap(); - assert_eq!( - source.recv_request().unwrap_err().kind(), - ErrorKind::TimedOut - ); - - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); - write_payload(&mut requests, &encode_request(request(2))); - assert!(matches!( - source.consumer.try_read(decode_request).unwrap(), - ControlRingReadStatus::Message(_) - )); - source - .association - .fail(Error::new(ErrorKind::TimedOut, "test failure")) - .unwrap(); - assert_eq!( - source - .association - .acknowledge_request(&mut source.consumer) - .unwrap_err() - .kind(), - ErrorKind::TimedOut - ); - - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); - write_payload(&mut requests, &encode_request(request(3))); - source.association.peer_closed(); - assert!(matches!( - source.recv_request().unwrap(), - HostReceive::Message(BrokerRequest { - request_id: RequestId(3), - .. - }) - )); - assert_eq!(source.recv_request().unwrap(), HostReceive::PeerClosed); - } - - #[test] - fn dropping_host_shutdown_guard_wakes_request_wait_and_closes_socket() { - let (mut source, sink, shutdown, _requests, _responses, mut peer) = activate_host(); - peer.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); - let receiver = thread::spawn(move || source.recv_request()); - - drop(sink); - drop(shutdown); - - assert_eq!( - receiver.join().unwrap().unwrap_err().kind(), - ErrorKind::ConnectionAborted - ); - let mut byte = [0]; - assert_eq!(peer.read(&mut byte).unwrap(), 0); - } - - #[test] - fn host_close_wakes_response_producer_blocked_on_full_ring() { - let (_source, sink, _shutdown, _requests, _responses, peer) = activate_host(); - for id in 0..CONTROL_RING_SLOT_COUNT { - sink.send_response(&response(RequestId(id))).unwrap(); - } - let blocked_sink = sink.clone(); - let blocked = thread::spawn(move || blocked_sink.send_response(&response(RequestId(99)))); - thread::sleep(Duration::from_millis(20)); - drop(peer); - assert_eq!( - blocked.join().unwrap().unwrap_err().kind(), - ErrorKind::BrokenPipe - ); - } - - #[test] - fn host_reports_wrong_phase_ring_message_as_protocol_violation() { - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); - write_payload( - &mut requests, - &encode_handshake_request(BrokerHandshakeRequest { - protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, - }), - ); - assert_eq!( - source.recv_request().unwrap(), - HostReceive::ProtocolViolation - ); - } - - #[test] - fn malformed_host_ring_request_is_fatal_invalid_data() { - let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); - write_payload(&mut requests, &[u8::MAX]); - assert_eq!( - source.recv_request().unwrap_err().kind(), - ErrorKind::InvalidData - ); - } - - #[test] - fn host_setup_rejects_active_frames_and_requires_negotiation() { - let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamHostSetupChannel::from_accepted(host_stream); - write_setup_frame(&mut peer_stream, &encode_request(request(0)), None).unwrap(); - assert_eq!( - channel.recv_handshake_request().unwrap(), - HostReceive::ProtocolViolation - ); - - let (_peer_stream, host_stream) = UnixStream::pair().unwrap(); - let channel = UnixStreamHostSetupChannel::from_accepted(host_stream); - let (ring, _) = ring_pair(); - let Err(error) = channel.into_active(ring) else { - panic!("host control channel activated before negotiation"); - }; - assert_eq!(error.kind(), ErrorKind::InvalidData); - } - - #[test] - fn ready_ack_uses_absolute_setup_deadline() { - let (local_stream, _peer) = UnixStream::pair().unwrap(); - let (ring, _) = ring_pair(); - let local = UnixStreamLocalSetupChannel { - stream: local_stream, - setup_deadline: Some(Instant::now() + Duration::from_millis(30)), - negotiated: true, - }; - let Err(error) = local.into_active(ring, || {}) else { - panic!("activation unexpectedly succeeded"); - }; - assert!(matches!( - error.kind(), - ErrorKind::WouldBlock | ErrorKind::TimedOut - )); - } - - #[test] - fn handshake_reads_use_absolute_setup_deadlines() { - let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); - let mut local = UnixStreamLocalSetupChannel { - stream: local_stream, - setup_deadline: Some(Instant::now() + Duration::from_millis(50)), - negotiated: false, - }; - let local_reader = thread::spawn(move || local.recv_handshake_response().unwrap_err()); - host_stream.write_all(&8u32.to_le_bytes()).unwrap(); - for _ in 0..8 { - thread::sleep(Duration::from_millis(20)); - if host_stream.write_all(&[0]).is_err() { - break; - } - } - let error = local_reader.join().unwrap(); - assert!( - matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut), - "unexpected local timeout error: {error:?}" - ); - - let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); - let mut host = UnixStreamHostSetupChannel::from_host_guaranteed( - host_stream, - Instant::now() + Duration::from_millis(50), - ); - let host_reader = thread::spawn(move || host.recv_handshake_request().unwrap_err()); - local_stream.write_all(&8u32.to_le_bytes()).unwrap(); - for _ in 0..8 { - thread::sleep(Duration::from_millis(20)); - if local_stream.write_all(&[0]).is_err() { - break; - } - } - let error = host_reader.join().unwrap(); - assert!( - matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut), - "unexpected host timeout error: {error:?}" - ); - } - - #[test] - fn notification_ring_round_trips() { - let (_control, mut local, mut host, _shutdown) = notification_channel_pair(); - let notification = BrokerNotification::Readiness( - litebox_broker_protocol::message::ReadinessNotification { - handle: ObjectHandle(7), - readiness: litebox_broker_protocol::readiness::ReadinessFlags::READ, - }, - ); - - let receiver = thread::spawn(move || local.recv_notification()); - thread::sleep(Duration::from_millis(20)); - host.send_notification(¬ification).unwrap(); - - assert_eq!(receiver.join().unwrap().unwrap(), Some(notification)); - } - - #[test] - fn full_notification_ring_wakes_after_consumer_progress() { - let (_control, mut local, mut host, _shutdown) = notification_channel_pair(); - let notification = BrokerNotification::Readiness( - litebox_broker_protocol::message::ReadinessNotification { - handle: ObjectHandle(7), - readiness: litebox_broker_protocol::readiness::ReadinessFlags::READ, - }, - ); - for _ in 0..crate::control_ring::CONTROL_RING_NOTIFICATION_SLOT_COUNT { - host.send_notification(¬ification).unwrap(); - } - - let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(1); - let (done_sender, done_receiver) = std::sync::mpsc::sync_channel(1); - let writer = thread::spawn(move || { - started_sender.send(()).unwrap(); - host.send_notification(¬ification).unwrap(); - done_sender.send(()).unwrap(); - }); - started_receiver.recv().unwrap(); - assert!( - done_receiver - .recv_timeout(Duration::from_millis(20)) - .is_err() - ); - - assert!(matches!( - local.recv_notification().unwrap(), - Some(BrokerNotification::Readiness(_)) - )); - done_receiver.recv_timeout(Duration::from_secs(1)).unwrap(); - writer.join().unwrap(); - } - - #[test] - fn association_shutdown_interrupts_notification_wait() { - let (_control, mut local, _host, shutdown) = notification_channel_pair(); - let receiver = thread::spawn(move || local.recv_notification()); - - shutdown.shutdown().unwrap(); - - assert_eq!( - receiver.join().unwrap().unwrap_err().kind(), - ErrorKind::UnexpectedEof - ); - } - - #[test] - fn malformed_notification_fails_the_association() { - let (control, mut local, mut host, _shutdown) = notification_channel_pair(); - assert_eq!( - host.producer.try_write(&[0xff]).unwrap(), - ControlRingWriteStatus::Written - ); - host.producer.wake_consumer().unwrap(); - - assert_eq!( - local.recv_notification().unwrap_err().kind(), - ErrorKind::InvalidData - ); - assert!( - control - .association - .pending_calls - .current_failure() - .is_some() - ); - } - - #[test] - fn completed_call_wins_over_later_failure_and_failure_wins_before_completion() { - let pending = PendingCalls::new(); - let completed = pending.register(RequestId(1)).unwrap(); - pending.complete(response(RequestId(1))).unwrap(); - pending.record_failure(Arc::new(Error::new( - ErrorKind::ConnectionAborted, - "test failure", - ))); - assert_eq!(completed.wait().unwrap().request_id, RequestId(1)); - - let pending = PendingCalls::new(); - let failed = pending.register(RequestId(2)).unwrap(); - pending.record_failure(Arc::new(Error::new( - ErrorKind::ConnectionAborted, - "test failure", - ))); - assert!(pending.complete(response(RequestId(2))).is_err()); - assert_eq!( - failed.wait().unwrap_err().kind(), - ErrorKind::ConnectionAborted - ); - } - - #[test] - fn failure_recording_waits_for_in_progress_publication() { - let pending = Arc::new(PendingCalls::new()); - let pending_call = pending.register(RequestId(1)).unwrap(); - let publication_state = Arc::new(AtomicUsize::new(0)); - let (publication_started, wait_for_publication) = std::sync::mpsc::sync_channel(0); - let (release_publication, publication_released) = std::sync::mpsc::sync_channel(0); - let publisher_pending = Arc::clone(&pending); - let publisher_state = Arc::clone(&publication_state); - let publisher = thread::spawn(move || { - publisher_pending - .run_if_live(|| { - publisher_state.store(1, Ordering::Release); - publication_started.send(()).unwrap(); - publication_released.recv().unwrap(); - publisher_state.store(2, Ordering::Release); - Ok(()) - }) - .unwrap(); - }); - wait_for_publication.recv().unwrap(); - - let (failure_started, wait_for_failure) = std::sync::mpsc::sync_channel(0); - let (failure_recorded, wait_for_recording) = std::sync::mpsc::sync_channel(0); - let failure_pending = Arc::clone(&pending); - let failure_state = Arc::clone(&publication_state); - let failure = thread::spawn(move || { - failure_started.send(()).unwrap(); - failure_pending.record_failure(Arc::new(Error::new( - ErrorKind::ConnectionAborted, - "test failure", - ))); - assert_eq!(failure_state.load(Ordering::Acquire), 2); - failure_recorded.send(()).unwrap(); - }); - wait_for_failure.recv().unwrap(); - assert!(matches!( - wait_for_recording.recv_timeout(Duration::from_millis(20)), - Err(std::sync::mpsc::RecvTimeoutError::Timeout) - )); - - release_publication.send(()).unwrap(); - publisher.join().unwrap(); - wait_for_recording - .recv_timeout(Duration::from_secs(1)) - .unwrap(); - failure.join().unwrap(); - assert_eq!( - pending_call.wait().unwrap_err().kind(), - ErrorKind::ConnectionAborted - ); - } - - #[test] - fn duplicate_pending_registration_preserves_original() { - let pending = PendingCalls::new(); - let original = pending.register(RequestId(1)).unwrap(); - let Err(error) = pending.register(RequestId(1)) else { - panic!("duplicate registration unexpectedly succeeded"); - }; - assert_eq!(error.kind(), ErrorKind::InvalidData); - pending.complete(response(RequestId(1))).unwrap(); - assert_eq!(original.wait().unwrap().request_id, RequestId(1)); - } -} diff --git a/litebox_broker_transport_linux_userland/Cargo.toml b/litebox_broker_transport_linux_userland/Cargo.toml new file mode 100644 index 0000000000..554822f15a --- /dev/null +++ b/litebox_broker_transport_linux_userland/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "litebox_broker_transport_linux_userland" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } + +[target.'cfg(target_os = "linux")'.dependencies] +libc = { version = "0.2.177", default-features = false } +rustix = { version = "1.1.2", default-features = false, features = ["std", "event", "fs", "mm", "net"] } + +[lints] +workspace = true diff --git a/litebox_broker_transport_linux_userland/src/lib.rs b/litebox_broker_transport_linux_userland/src/lib.rs new file mode 100644 index 0000000000..024487f319 --- /dev/null +++ b/litebox_broker_transport_linux_userland/src/lib.rs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Linux-userland broker association transport. +//! +//! This crate binds a broker association to Linux userland. It owns both sides +//! of the hosted deployment: the local (guest-side) endpoints a runner uses and +//! the host (broker-side) endpoints the broker uses, together with the +//! Linux-specific machinery they share, namely memfd-backed shared memory, +//! futex waits, Unix-domain-socket setup framing, descriptor transfer, peer +//! authentication, and liveness monitoring. +//! +//! The crate deliberately uses `std` because Unix-domain sockets and `std::io` +//! framing are hosted userland concerns. Everything portable stays out of it: +//! peer-visible messages and layouts live in `litebox_broker_protocol`, runtime +//! channel interfaces, shared-memory interfaces, and the control-ring state +//! machines live in `litebox_broker_transport`, and the local, host, and core +//! authority adapters live in their own OS-neutral crates. + +#![cfg(target_os = "linux")] + +mod setup; + +pub mod memfd; + +mod unix_io; + +pub mod unix_socket; diff --git a/litebox_broker_transport_linux_userland/src/memfd.rs b/litebox_broker_transport_linux_userland/src/memfd.rs new file mode 100644 index 0000000000..44d539e47d --- /dev/null +++ b/litebox_broker_transport_linux_userland/src/memfd.rs @@ -0,0 +1,1068 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Linux memfd-backed broker association memory. +//! +//! One sealed memfd mapping backs an association shared-buffer pool or a shared +//! control ring. The mapping implements the portable shared-memory interfaces in +//! `litebox_broker_transport`, and its futex support lets portable control-ring +//! endpoints block and wake without knowing anything about Linux. +//! +//! Rust never dereferences the peer-writable mapping. Byte and word access uses +//! positional descriptor I/O into private buffers; the mapping exists only to +//! provide checked addresses to the kernel's futex operations. + +use std::io::{Error, Result as IoResult}; +use std::io::{ErrorKind, IoSlice, IoSliceMut}; +use std::mem::{align_of, size_of}; +use std::os::fd::{AsFd, BorrowedFd, OwnedFd}; +use std::os::unix::net::UnixStream; +use std::ptr::NonNull; +use std::time::Instant; + +use rustix::fs::{ + MemfdFlags, SealFlags, fcntl_add_seals, fcntl_get_seals, fstat, ftruncate, memfd_create, +}; +use rustix::io::{Errno, pread, pwrite}; +use rustix::mm::{MapFlags, ProtFlags, mmap, munmap}; +use rustix::net::{ + RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags, SendAncillaryBuffer, + SendAncillaryMessage, SendFlags, +}; + +use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; +use litebox_broker_transport::control_ring::{ + CONTROL_RING_MEMORY_SIZE, WaitableSharedMemory, memory_permits_byte_range, memory_permits_u32, + memory_permits_u64, +}; +use litebox_broker_transport::shared_memory::{ControlRingMemory, SharedMemory, SharedMemoryError}; + +use crate::unix_io::{ + refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, +}; + +const REQUIRED_MEMFD_SEALS: SealFlags = SealFlags::from_bits_retain( + SealFlags::GROW.bits() | SealFlags::SHRINK.bits() | SealFlags::SEAL.bits(), +); +const _: () = assert!(SHARED_BUFFER_POOL_SIZE != CONTROL_RING_MEMORY_SIZE); + +/// Linux memfd-backed shared memory usable by broker transports. +pub struct MemfdSharedMemory { + fd: OwnedFd, + mapping: MappedRegion, + policy: MemoryAccessPolicy, +} + +struct MappedRegion { + address: NonNull, + length: usize, +} + +/// Restricts each memfd to one non-overlapping portable access model. +/// +/// Shared-buffer memfds permit only byte copies. Control-ring memfds permit +/// byte and typed-word operations only at offsets defined by the ring ABI. +#[derive(Clone, Copy)] +enum MemoryAccessPolicy { + Bytes, + ControlRing, +} + +impl MemoryAccessPolicy { + const fn for_length(length: usize) -> Self { + if length == CONTROL_RING_MEMORY_SIZE { + Self::ControlRing + } else { + Self::Bytes + } + } + + const fn permits_byte_range(self, offset: usize, length: usize) -> bool { + match self { + Self::Bytes => true, + Self::ControlRing => memory_permits_byte_range(offset, length), + } + } + + const fn permits_u32(self, offset: usize) -> bool { + match self { + Self::Bytes => false, + Self::ControlRing => memory_permits_u32(offset), + } + } + + const fn permits_u64(self, offset: usize) -> bool { + match self { + Self::Bytes => false, + Self::ControlRing => memory_permits_u64(offset), + } + } +} + +// SAFETY: Moving or sharing this owner does not move or invalidate its OS +// mapping. Its metadata is immutable, and mapped contents are never +// dereferenced by Rust. +unsafe impl Send for MappedRegion {} +// SAFETY: See the `Send` justification. Only checked raw futex addresses are +// derived from the mapping and passed to the kernel. +unsafe impl Sync for MappedRegion {} + +fn validate_u64_offset(memory: &MemfdSharedMemory, offset: usize) -> Result<(), SharedMemoryError> { + if !memory.policy.permits_u64(offset) { + return Err(SharedMemoryError::InvalidRange); + } + checked_range(&memory.mapping, offset, size_of::(), align_of::()) +} + +fn checked_u32_address( + memory: &MemfdSharedMemory, + offset: usize, +) -> Result<*mut u32, SharedMemoryError> { + if !memory.policy.permits_u32(offset) { + return Err(SharedMemoryError::InvalidRange); + } + let byte_address = + shared_address(&memory.mapping, offset, size_of::(), align_of::())?; + // The runtime check above establishes the required alignment. + #[allow(clippy::cast_ptr_alignment)] + Ok(byte_address.cast::()) +} + +fn checked_range( + mapping: &MappedRegion, + offset: usize, + size: usize, + alignment: usize, +) -> Result<(), SharedMemoryError> { + offset + .checked_add(size) + .filter(|end| *end <= mapping.length) + .ok_or(SharedMemoryError::InvalidRange)?; + if !offset.is_multiple_of(alignment) { + return Err(SharedMemoryError::UnalignedWord); + } + Ok(()) +} + +fn shared_address( + mapping: &MappedRegion, + offset: usize, + size: usize, + alignment: usize, +) -> Result<*mut u8, SharedMemoryError> { + checked_range(mapping, offset, size, alignment)?; + Ok(mapping.address.as_ptr().wrapping_add(offset)) +} + +fn validate_nonoverlapping_word_ranges( + store_offset: usize, + increment_offset: usize, +) -> Result<(), SharedMemoryError> { + let store_end = store_offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + let increment_end = increment_offset + .checked_add(size_of::()) + .ok_or(SharedMemoryError::InvalidRange)?; + if store_offset < increment_end && increment_offset < store_end { + return Err(SharedMemoryError::InvalidRange); + } + Ok(()) +} + +fn read_exact_at( + memory: &MemfdSharedMemory, + offset: usize, + destination: &mut [u8], +) -> Result<(), SharedMemoryError> { + let mut completed = 0; + while completed < destination.len() { + let file_offset = + u64::try_from(offset + completed).map_err(|_| SharedMemoryError::InvalidRange)?; + match pread(&memory.fd, &mut destination[completed..], file_offset) { + Ok(0) => return Err(SharedMemoryError::AccessFailed), + Ok(read) => completed += read, + Err(Errno::INTR) => {} + Err(_) => return Err(SharedMemoryError::AccessFailed), + } + } + Ok(()) +} + +fn write_all_at( + memory: &MemfdSharedMemory, + offset: usize, + source: &[u8], +) -> Result<(), SharedMemoryError> { + let mut completed = 0; + while completed < source.len() { + let file_offset = + u64::try_from(offset + completed).map_err(|_| SharedMemoryError::InvalidRange)?; + match pwrite(&memory.fd, &source[completed..], file_offset) { + Ok(0) => return Err(SharedMemoryError::AccessFailed), + Ok(written) => completed += written, + Err(Errno::INTR) => {} + Err(_) => return Err(SharedMemoryError::AccessFailed), + } + } + Ok(()) +} + +const FUTEX_INCREMENT_OPERATION: libc::c_int = + (libc::FUTEX_OP_ADD << 28) | (libc::FUTEX_OP_CMP_EQ << 24) | (1 << 12); +const FUTEX_WAIT_RECHECK_TIMEOUT: libc::timespec = libc::timespec { + tv_sec: 0, + tv_nsec: 100_000_000, +}; + +// The rustix futex API requires `AtomicU32` references. Raw syscalls keep Rust +// references out of memory that a peer can modify through an uncontrolled fd. +fn futex_increment(address: *mut u32) -> IoResult<()> { + // SAFETY: `address` is aligned and lies within the live shared mapping. + // FUTEX_WAKE_OP atomically increments it in the kernel, so Rust never forms + // an atomic reference that a peer could invalidate through another alias. + let result = unsafe { + libc::syscall( + libc::SYS_futex, + address, + libc::FUTEX_WAKE_OP, + 0, + 0, + address, + FUTEX_INCREMENT_OPERATION, + ) + }; + if result == -1 { + Err(Error::last_os_error()) + } else { + Ok(()) + } +} + +fn futex_wait(address: *mut u32, expected: u32) -> IoResult<()> { + let timeout = FUTEX_WAIT_RECHECK_TIMEOUT; + // SAFETY: `address` is aligned and lies within the live shared mapping. The + // timeout and the other unused pointer are valid for the syscall. + let result = unsafe { + libc::syscall( + libc::SYS_futex, + address, + libc::FUTEX_WAIT, + expected, + &raw const timeout, + std::ptr::null::(), + 0, + ) + }; + if result == -1 { + Err(Error::last_os_error()) + } else { + Ok(()) + } +} + +fn futex_wake_one(address: *mut u32) -> IoResult<()> { + // SAFETY: `address` is aligned and lies within the live shared mapping. + let result = unsafe { + libc::syscall( + libc::SYS_futex, + address, + libc::FUTEX_WAKE, + 1, + std::ptr::null::(), + std::ptr::null::(), + 0, + ) + }; + if result == -1 { + Err(Error::last_os_error()) + } else { + Ok(()) + } +} + +impl MemfdSharedMemory { + /// Creates and maps a sealed memfd with `length` bytes. + pub fn create(length: usize) -> IoResult { + if length == 0 { + return Err(invalid_data("shared memory cannot be empty")); + } + let fd = memfd_create( + "litebox-broker-shm", + MemfdFlags::CLOEXEC | MemfdFlags::ALLOW_SEALING, + )?; + ftruncate( + &fd, + length + .try_into() + .map_err(|_| invalid_data("shared-memory length exceeds u64"))?, + )?; + fcntl_add_seals(&fd, REQUIRED_MEMFD_SEALS)?; + Self::map(fd, length) + } + + /// Validates and maps a received memfd with `expected_length` bytes. + /// + /// The descriptor must have the expected nonzero size sealed against + /// changes. + pub fn from_received_fd(fd: OwnedFd, expected_length: usize) -> IoResult { + if expected_length == 0 { + return Err(invalid_data("shared memory cannot be empty")); + } + // Verify the size seals before reading the size so it cannot change + // between validation and mapping. + let seals = fcntl_get_seals(&fd)?; + if !seals.contains(REQUIRED_MEMFD_SEALS) { + return Err(invalid_data("shared-memory size is not sealed")); + } + let length = usize::try_from(fstat(&fd)?.st_size) + .map_err(|_| invalid_data("invalid shared-memory length"))?; + if length != expected_length { + return Err(invalid_data( + "shared-memory length does not match expected size", + )); + } + Self::map(fd, length) + } + + fn map(fd: OwnedFd, length: usize) -> IoResult { + if length > isize::MAX as usize { + return Err(invalid_data( + "shared-memory length exceeds pointer offset range", + )); + } + // SAFETY: `fd` refers to a file at least `length` bytes long. A null + // address lets the kernel choose the mapping location, and + // `MappedRegion` owns the returned mapping. + let address = unsafe { + mmap( + std::ptr::null_mut(), + length, + ProtFlags::READ | ProtFlags::WRITE, + MapFlags::SHARED, + &fd, + 0, + ) + }?; + let address = + NonNull::new(address.cast()).ok_or_else(|| invalid_data("mmap returned null"))?; + Ok(Self { + fd, + mapping: MappedRegion { address, length }, + policy: MemoryAccessPolicy::for_length(length), + }) + } +} + +impl WaitableSharedMemory for MemfdSharedMemory { + type Error = Error; + + fn wait_access_error(error: SharedMemoryError) -> Error { + Error::new(ErrorKind::InvalidInput, error) + } + + /// Waits while a shared `u32` still equals `expected`. + /// + /// A value change or signal interruption is reported as a successful, + /// possibly spurious wakeup. A bounded wait also lets callers recheck + /// trusted cancellation state if a hostile peer restores the sampled shared + /// value after cancellation. The caller must recheck its wait condition. + fn wait_while_equal(&self, offset: usize, expected: u32) -> IoResult<()> { + let address = checked_u32_address(self, offset).map_err(Self::wait_access_error)?; + match futex_wait(address, expected) { + Ok(()) => Ok(()), + Err(error) + if matches!( + error.raw_os_error(), + Some(libc::EAGAIN | libc::EINTR | libc::ETIMEDOUT) + ) => + { + Ok(()) + } + Err(error) => Err(error), + } + } + + /// Wakes one waiter blocked on a shared `u32`. + fn wake_one(&self, offset: usize) -> IoResult<()> { + let address = checked_u32_address(self, offset).map_err(Self::wait_access_error)?; + futex_wake_one(address) + } +} + +impl AsFd for MemfdSharedMemory { + fn as_fd(&self) -> BorrowedFd<'_> { + self.fd.as_fd() + } +} + +impl SharedMemory for MemfdSharedMemory { + fn len(&self) -> usize { + self.mapping.length + } + + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + checked_range(&self.mapping, offset, destination.len(), 1)?; + if !self.policy.permits_byte_range(offset, destination.len()) { + return Err(SharedMemoryError::InvalidRange); + } + read_exact_at(self, offset, destination) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + checked_range(&self.mapping, offset, source.len(), 1)?; + if !self.policy.permits_byte_range(offset, source.len()) { + return Err(SharedMemoryError::InvalidRange); + } + write_all_at(self, offset, source) + } +} + +impl ControlRingMemory for MemfdSharedMemory { + fn load_u32_acquire(&self, offset: usize) -> Result { + checked_u32_address(self, offset)?; + let mut bytes = [0; size_of::()]; + read_exact_at(self, offset, &mut bytes)?; + Ok(u32::from_ne_bytes(bytes)) + } + + fn increment_u32_release(&self, offset: usize) -> Result<(), SharedMemoryError> { + let address = checked_u32_address(self, offset)?; + futex_increment(address).map_err(|_| SharedMemoryError::AccessFailed) + } + + fn load_u64_acquire(&self, offset: usize) -> Result { + validate_u64_offset(self, offset)?; + let mut bytes = [0; size_of::()]; + read_exact_at(self, offset, &mut bytes)?; + Ok(u64::from_ne_bytes(bytes)) + } + + fn store_u64_release(&self, offset: usize, value: u64) -> Result<(), SharedMemoryError> { + validate_u64_offset(self, offset)?; + write_all_at(self, offset, &value.to_ne_bytes()) + } + + fn store_u64_and_increment_u32_release( + &self, + store_offset: usize, + value: u64, + increment_offset: usize, + ) -> Result<(), SharedMemoryError> { + validate_nonoverlapping_word_ranges(store_offset, increment_offset)?; + validate_u64_offset(self, store_offset)?; + let increment_address = checked_u32_address(self, increment_offset)?; + write_all_at(self, store_offset, &value.to_ne_bytes())?; + futex_increment(increment_address).map_err(|_| SharedMemoryError::AccessFailed) + } +} + +/// Sends one memfd-backed shared-memory resource over an exclusively owned +/// connected Unix stream. +/// +/// `deadline` bounds setup I/O without leaving a changed socket timeout behind. +pub fn send_memfd( + stream: &mut UnixStream, + memory: &MemfdSharedMemory, + deadline: Option, +) -> IoResult<()> { + with_write_deadline(stream, deadline, |stream, deadline| { + send_fd(stream, memory.fd.as_fd(), deadline) + }) +} + +/// Receives, validates, and maps one memfd-backed shared-memory resource. +/// +/// `expected_length` supplies the trusted expected size. `deadline` bounds +/// setup I/O without leaving a changed socket timeout behind. +pub fn receive_memfd( + stream: &mut UnixStream, + expected_length: usize, + deadline: Option, +) -> IoResult { + let fd = with_read_deadline(stream, deadline, receive_fd)?; + MemfdSharedMemory::from_received_fd(fd, expected_length) +} + +fn send_fd(stream: &mut UnixStream, fd: BorrowedFd<'_>, deadline: Option) -> IoResult<()> { + // Unix streams require an ordinary data byte to carry ancillary data. + let carrier = [0]; + let io = [IoSlice::new(&carrier)]; + let fds = [fd]; + let mut control_space = [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))]; + let mut control = SendAncillaryBuffer::new(&mut control_space); + assert!( + control.push(SendAncillaryMessage::ScmRights(&fds)), + "SCM_RIGHTS control buffer is correctly sized" + ); + loop { + refresh_write_deadline(stream, deadline)?; + match rustix::net::sendmsg(stream.as_fd(), &io, &mut control, SendFlags::NOSIGNAL) { + Ok(1) => return Ok(()), + Ok(0) => { + return Err(Error::new( + ErrorKind::WriteZero, + "failed to send shared-memory descriptor", + )); + } + Ok(_) => return Err(invalid_data("oversized shared-memory setup write")), + Err(Errno::INTR) => {} + Err(error) => return Err(error.into()), + } + } +} + +fn receive_fd(stream: &mut UnixStream, deadline: Option) -> IoResult { + let mut carrier = [0]; + let mut io = [IoSliceMut::new(&mut carrier)]; + let mut control_space = [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(4))]; + let mut control = RecvAncillaryBuffer::new(&mut control_space); + let received = loop { + refresh_read_deadline(stream, deadline)?; + match rustix::net::recvmsg( + stream.as_fd(), + &mut io, + &mut control, + RecvFlags::CMSG_CLOEXEC, + ) { + Ok(received) => break received, + Err(Errno::INTR) => {} + Err(error) => return Err(error.into()), + } + }; + + let mut received_fds = Vec::new(); + let mut unexpected_control_message = false; + for message in control.drain() { + match message { + RecvAncillaryMessage::ScmRights(fds) => received_fds.extend(fds), + _ => unexpected_control_message = true, + } + } + + if received.bytes == 0 { + return Err(Error::new( + ErrorKind::UnexpectedEof, + "broker closed during shared-memory setup", + )); + } + if received.bytes != carrier.len() + || received + .flags + .intersects(ReturnFlags::TRUNC | ReturnFlags::CTRUNC) + || unexpected_control_message + || received_fds.len() != 1 + { + return Err(invalid_data( + "shared-memory setup contained invalid descriptor data", + )); + } + Ok(received_fds + .pop() + .expect("exactly one received descriptor was validated")) +} + +impl Drop for MappedRegion { + fn drop(&mut self) { + // SAFETY: `address` and `length` describe the mapping exclusively owned + // by this value, and it is unmapped exactly once here. + let result = unsafe { munmap(self.address.as_ptr().cast(), self.length) }; + debug_assert!(result.is_ok(), "failed to unmap broker shared memory"); + } +} + +fn invalid_data(message: &'static str) -> Error { + Error::new(std::io::ErrorKind::InvalidData, message) +} + +#[cfg(test)] +mod tests { + use super::*; + use litebox_broker_protocol::shared_buffer::{ + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferSlotIndex, + }; + use litebox_broker_transport::control_ring::{ + CONTROL_RING_MEMORY_SIZE, CONTROL_RING_SLOT_COUNT, ControlRing, ControlRingReadStatus, + ControlRingWriteStatus, + }; + use litebox_broker_transport::shared_memory::SharedBufferPool; + use rustix::io::FdFlags; + use std::io::Write; + use std::sync::{Arc, Barrier}; + use std::thread; + use std::time::Duration; + + #[test] + fn mappings_share_bytes_and_validate_ranges() { + let first = MemfdSharedMemory::create(64).unwrap(); + let second = + MemfdSharedMemory::from_received_fd(first.fd.as_fd().try_clone_to_owned().unwrap(), 64) + .unwrap(); + + first.write(0, &[1, 2, 3]).unwrap(); + let mut data = [0; 3]; + second.read(0, &mut data).unwrap(); + assert_eq!(data, [1, 2, 3]); + + assert_eq!( + second.write(63, &[1, 2]), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.read(usize::MAX, &mut data), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.load_u64_acquire(0), + Err(SharedMemoryError::InvalidRange) + ); + } + + #[test] + fn mappings_preserve_disjoint_partial_word_writes() { + let first = MemfdSharedMemory::create(10).unwrap(); + let second = + MemfdSharedMemory::from_received_fd(first.fd.as_fd().try_clone_to_owned().unwrap(), 10) + .unwrap(); + let start = Barrier::new(3); + thread::scope(|scope| { + scope.spawn(|| { + start.wait(); + first.write(0, &[1; 4]).unwrap(); + }); + scope.spawn(|| { + start.wait(); + second.write(4, &[2; 6]).unwrap(); + }); + start.wait(); + }); + + let mut bytes = [0; 10]; + first.read(0, &mut bytes).unwrap(); + assert_eq!(bytes, [1, 1, 1, 1, 2, 2, 2, 2, 2, 2]); + } + + #[test] + fn mappings_enforce_control_ring_typed_access() { + let first = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let second = MemfdSharedMemory::from_received_fd( + first.fd.as_fd().try_clone_to_owned().unwrap(), + CONTROL_RING_MEMORY_SIZE, + ) + .unwrap(); + let sequence_offset = (0..CONTROL_RING_MEMORY_SIZE) + .find(|offset| memory_permits_u64(*offset)) + .unwrap(); + let epoch_offset = (0..CONTROL_RING_MEMORY_SIZE) + .find(|offset| memory_permits_u32(*offset)) + .unwrap(); + let body_offset = (0..CONTROL_RING_MEMORY_SIZE) + .find(|offset| memory_permits_byte_range(*offset, 1)) + .unwrap(); + + first + .store_u64_release(sequence_offset, 0x0102_0304_0506_0708) + .unwrap(); + assert_eq!( + second.load_u64_acquire(sequence_offset), + Ok(0x0102_0304_0506_0708) + ); + assert_eq!(first.increment_u32_release(epoch_offset), Ok(())); + assert_eq!(second.load_u32_acquire(epoch_offset), Ok(1)); + assert_eq!( + first.store_u64_and_increment_u32_release( + sequence_offset, + 0x1112_1314_1516_1718, + epoch_offset, + ), + Ok(()) + ); + assert_eq!( + second.load_u64_acquire(sequence_offset), + Ok(0x1112_1314_1516_1718) + ); + assert_eq!(second.load_u32_acquire(epoch_offset), Ok(2)); + second.write(body_offset, &[7]).unwrap(); + let mut byte = [0]; + first.read(body_offset, &mut byte).unwrap(); + assert_eq!(byte, [7]); + + assert_eq!( + second.read(sequence_offset, &mut byte), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.write(epoch_offset, &[0]), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.load_u32_acquire(sequence_offset), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.load_u64_acquire(body_offset), + Err(SharedMemoryError::InvalidRange) + ); + second.wait_while_equal(epoch_offset, 0).unwrap(); + assert_eq!( + second.store_u64_and_increment_u32_release(sequence_offset, 0, sequence_offset), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second.load_u64_acquire(CONTROL_RING_MEMORY_SIZE), + Err(SharedMemoryError::InvalidRange) + ); + assert_eq!( + second + .wait_while_equal(sequence_offset, 0) + .unwrap_err() + .kind(), + ErrorKind::InvalidInput + ); + assert_eq!( + second.wake_one(body_offset).unwrap_err().kind(), + ErrorKind::InvalidInput + ); + } + + #[test] + fn futex_wait_returns_without_peer_cooperation() { + let memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let epoch_offset = (0..CONTROL_RING_MEMORY_SIZE) + .find(|offset| memory_permits_u32(*offset)) + .unwrap(); + let start = Instant::now(); + + memory.wait_while_equal(epoch_offset, 0).unwrap(); + + assert!(start.elapsed() < Duration::from_secs(1)); + } + + #[test] + fn peer_descriptor_writes_are_read_as_untrusted_snapshots() { + let memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let peer_fd = memory.as_fd().try_clone_to_owned().unwrap(); + let sequence_offset = (0..CONTROL_RING_MEMORY_SIZE) + .find(|offset| memory_permits_u64(*offset)) + .unwrap(); + let epoch_offset = (0..CONTROL_RING_MEMORY_SIZE) + .find(|offset| memory_permits_u32(*offset)) + .unwrap(); + + let mut expected_sequence = [0; size_of::()]; + expected_sequence[1..4].copy_from_slice(&[0xaa, 0xbb, 0xcc]); + assert_eq!( + rustix::io::pwrite( + &peer_fd, + &expected_sequence[1..4], + u64::try_from(sequence_offset).unwrap() + 1, + ), + Ok(3) + ); + assert_eq!( + memory.load_u64_acquire(sequence_offset), + Ok(u64::from_ne_bytes(expected_sequence)) + ); + + assert_eq!( + rustix::io::pwrite( + &peer_fd, + &u32::MAX.to_ne_bytes(), + u64::try_from(epoch_offset).unwrap(), + ), + Ok(size_of::()) + ); + memory.increment_u32_release(epoch_offset).unwrap(); + assert_eq!(memory.load_u32_acquire(epoch_offset), Ok(0)); + } + + #[test] + fn rejects_unsealed_mismatched_and_oversized_mappings() { + let fd = memfd_create("litebox-broker-shm-test", MemfdFlags::CLOEXEC).unwrap(); + ftruncate(&fd, 1).unwrap(); + assert_eq!( + MemfdSharedMemory::from_received_fd(fd, 1) + .err() + .expect("unsealed memfd should fail") + .kind(), + std::io::ErrorKind::InvalidData + ); + + let memory = MemfdSharedMemory::create(64).unwrap(); + assert_eq!( + MemfdSharedMemory::from_received_fd( + memory.fd.as_fd().try_clone_to_owned().unwrap(), + 32, + ) + .err() + .expect("mismatched memfd size should fail") + .kind(), + std::io::ErrorKind::InvalidData + ); + + let fd = memfd_create("litebox-broker-shm-test", MemfdFlags::CLOEXEC).unwrap(); + assert_eq!( + MemfdSharedMemory::map(fd, isize::MAX as usize + 1) + .err() + .expect("oversized mapping should fail") + .kind(), + std::io::ErrorKind::InvalidData + ); + } + + #[test] + fn transfers_exact_pool_with_shared_visibility_and_close_on_exec() { + let memory = MemfdSharedMemory::create(SHARED_BUFFER_POOL_SIZE).unwrap(); + let pool = SharedBufferPool::new(memory, SHARED_BUFFER_LAYOUT).unwrap(); + for index in 0..SHARED_BUFFER_LAYOUT.slot_count() { + pool.write( + SharedBufferSlotIndex(index), + &[u8::try_from(index).unwrap()], + ) + .unwrap(); + } + let (mut local_stream, mut host_stream) = UnixStream::pair().unwrap(); + + send_memfd(&mut host_stream, pool.memory(), None).unwrap(); + let mapped_memory = + receive_memfd(&mut local_stream, SHARED_BUFFER_POOL_SIZE, None).unwrap(); + let mapped_pool = SharedBufferPool::new(mapped_memory, SHARED_BUFFER_LAYOUT).unwrap(); + for index in 0..SHARED_BUFFER_LAYOUT.slot_count() { + let mut byte = [0]; + mapped_pool + .read(SharedBufferSlotIndex(index), &mut byte) + .unwrap(); + assert_eq!(byte, [u8::try_from(index).unwrap()]); + } + let flags = rustix::io::fcntl_getfd(mapped_pool.memory().fd.as_fd()).unwrap(); + assert!(flags.contains(FdFlags::CLOEXEC)); + } + + #[test] + fn transfers_exact_sealed_control_ring_mapping() { + let memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let ring = ControlRing::new(memory).unwrap(); + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + + send_memfd(&mut sender, ring.memory(), None).unwrap(); + let mapped = receive_memfd(&mut receiver, CONTROL_RING_MEMORY_SIZE, None).unwrap(); + let mapped_ring = ControlRing::new(mapped).unwrap(); + ring.memory().write(13, &[1, 2, 3]).unwrap(); + let mut bytes = [0; 3]; + mapped_ring.memory().read(13, &mut bytes).unwrap(); + assert_eq!(bytes, [1, 2, 3]); + + let flags = rustix::io::fcntl_getfd(mapped_ring.memory().fd.as_fd()).unwrap(); + assert!(flags.contains(FdFlags::CLOEXEC)); + let seals = fcntl_get_seals(mapped_ring.memory().fd.as_fd()).unwrap(); + assert!(seals.contains(REQUIRED_MEMFD_SEALS)); + assert!(!seals.contains(SealFlags::WRITE)); + } + + #[test] + fn shared_futex_wakeup_prevents_missed_cross_mapping_work() { + let local_memory = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let broker_memory = MemfdSharedMemory::from_received_fd( + local_memory.fd.as_fd().try_clone_to_owned().unwrap(), + CONTROL_RING_MEMORY_SIZE, + ) + .unwrap(); + let mut producer = ControlRing::new(local_memory) + .unwrap() + .into_local() + .request_producer; + let mut consumer = ControlRing::new(broker_memory) + .unwrap() + .into_broker() + .request_consumer; + let empty_checked = Arc::new(Barrier::new(2)); + let broker_empty_checked = Arc::clone(&empty_checked); + + let broker = thread::spawn(move || { + let ControlRingReadStatus::Empty { + wait_epoch: producer_epoch, + } = consumer + .try_read(|payload| Ok::<_, ()>(payload[0])) + .unwrap() + else { + panic!("request ring should initially be empty"); + }; + broker_empty_checked.wait(); + consumer.wait_for_message(producer_epoch).unwrap(); + for expected in 0..CONTROL_RING_SLOT_COUNT { + let expected = u8::try_from(expected).unwrap(); + loop { + match consumer + .try_read(|payload| Ok::<_, ()>(payload[0])) + .unwrap() + { + ControlRingReadStatus::Message(value) => { + assert_eq!(value, expected); + break; + } + ControlRingReadStatus::Empty { wait_epoch } => { + consumer.wait_for_message(wait_epoch).unwrap(); + } + } + } + } + + consumer.publish_head().unwrap(); + consumer.wake_producer().unwrap(); + match consumer + .try_read(|payload| Ok::<_, ()>(payload[0])) + .unwrap() + { + ControlRingReadStatus::Empty { wait_epoch } => { + consumer.wait_for_message(wait_epoch).unwrap(); + assert_eq!( + consumer.try_read(|payload| Ok::<_, ()>(payload[0])), + Ok(ControlRingReadStatus::Message(0xff)) + ); + } + ControlRingReadStatus::Message(value) => assert_eq!(value, 0xff), + } + }); + + empty_checked.wait(); + for value in 0..CONTROL_RING_SLOT_COUNT { + let value = u8::try_from(value).unwrap(); + assert_eq!( + producer.try_write(&[value]), + Ok(ControlRingWriteStatus::Written) + ); + } + let ControlRingWriteStatus::Full { + wait_epoch: consumer_epoch, + } = producer.try_write(&[0xff]).unwrap() + else { + panic!("request ring should be full"); + }; + producer.wake_consumer().unwrap(); + + producer.wait_for_capacity(consumer_epoch).unwrap(); + assert_eq!( + producer.try_write(&[0xff]), + Ok(ControlRingWriteStatus::Written) + ); + producer.wake_consumer().unwrap(); + + broker.join().unwrap(); + } + + #[test] + fn rejects_missing_multiple_and_truncated_descriptors() { + let length = 8; + + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + sender.write_all(&[0]).unwrap(); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("missing descriptor must be rejected") + .kind(), + ErrorKind::InvalidData + ); + + let memory = MemfdSharedMemory::create(length).unwrap(); + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + send_test_fds(&mut sender, &[memory.fd.as_fd(), memory.fd.as_fd()]); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("multiple descriptors must be rejected") + .kind(), + ErrorKind::InvalidData + ); + + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + let fd = memory.fd.as_fd(); + send_test_fds(&mut sender, &[fd, fd, fd, fd, fd]); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("truncated descriptors must be rejected") + .kind(), + ErrorKind::InvalidData + ); + } + + #[test] + fn rejects_wrong_size_and_unsealed_memory() { + let length = SHARED_BUFFER_POOL_SIZE; + + let wrong_size = MemfdSharedMemory::create(length - 1).unwrap(); + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + send_memfd(&mut sender, &wrong_size, None).unwrap(); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("wrong shared-memory size must be rejected") + .kind(), + ErrorKind::InvalidData + ); + + let unsealed = memfd_create("unsealed-transfer-test", MemfdFlags::CLOEXEC).unwrap(); + ftruncate(&unsealed, length.try_into().unwrap()).unwrap(); + let (mut receiver, mut sender) = UnixStream::pair().unwrap(); + send_test_fds(&mut sender, &[unsealed.as_fd()]); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("unsealed shared memory must be rejected") + .kind(), + ErrorKind::InvalidData + ); + } + + #[test] + fn reports_eof_and_expired_deadline() { + let length = 8; + let (mut receiver, sender) = UnixStream::pair().unwrap(); + drop(sender); + assert_eq!( + receive_memfd(&mut receiver, length, None) + .err() + .expect("setup EOF must be reported") + .kind(), + ErrorKind::UnexpectedEof + ); + + let (mut receiver, _sender) = UnixStream::pair().unwrap(); + let previous_timeout = Some(Duration::from_secs(2)); + receiver.set_read_timeout(previous_timeout).unwrap(); + let expired = Instant::now().checked_sub(Duration::from_secs(1)).unwrap(); + assert_eq!( + receive_memfd(&mut receiver, length, Some(expired)) + .err() + .expect("expired setup deadline must be rejected") + .kind(), + ErrorKind::TimedOut + ); + assert_eq!(receiver.read_timeout().unwrap(), previous_timeout); + + let memory = MemfdSharedMemory::create(length).unwrap(); + let (_receiver, mut sender) = UnixStream::pair().unwrap(); + sender.set_write_timeout(previous_timeout).unwrap(); + assert_eq!( + send_memfd(&mut sender, &memory, Some(expired)) + .expect_err("expired send deadline must be rejected") + .kind(), + ErrorKind::TimedOut + ); + assert_eq!(sender.write_timeout().unwrap(), previous_timeout); + } + + fn send_test_fds(stream: &mut UnixStream, fds: &[BorrowedFd<'_>]) { + let carrier = [0]; + let io = [IoSlice::new(&carrier)]; + let mut control_space = + [std::mem::MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(8))]; + let mut control = SendAncillaryBuffer::new(&mut control_space); + assert!(control.push(SendAncillaryMessage::ScmRights(fds))); + assert_eq!( + rustix::net::sendmsg(stream.as_fd(), &io, &mut control, SendFlags::NOSIGNAL).unwrap(), + 1 + ); + } +} diff --git a/litebox_broker_transport_linux_userland/src/setup.rs b/litebox_broker_transport_linux_userland/src/setup.rs new file mode 100644 index 0000000000..6be265ba89 --- /dev/null +++ b/litebox_broker_transport_linux_userland/src/setup.rs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Unix setup framing and failure helpers shared by both association sides. +//! +//! The local and host endpoints in [`crate::unix_socket`] must frame setup +//! traffic and report failures identically, so this crate-private module is the +//! single source of that security-sensitive framing instead of each endpoint +//! reimplementing it. +//! +//! Setup framing is a property of this Linux-userland binding, not of the +//! broker wire protocol; portable messages live in `litebox_broker_protocol`. + +use std::io::{Error, ErrorKind, Read, Result as IoResult, Write}; +use std::net::Shutdown; +use std::os::unix::net::UnixStream; +use std::time::Instant; + +use litebox_broker_protocol::wire::WireError; +use litebox_broker_transport::control_ring::ControlRingError; + +use crate::unix_io::{ + refresh_read_deadline, refresh_write_deadline, with_read_deadline, with_write_deadline, +}; + +/// Largest setup frame either endpoint accepts or produces. +const MAX_SETUP_FRAME_LEN: usize = 64 * 1024; + +/// Reads one length-prefixed setup frame, bounded by `deadline`. +/// +/// Returns `Ok(None)` when the peer closed cleanly on a frame boundary. +pub(crate) fn read_setup_frame( + stream: &mut UnixStream, + deadline: Option, +) -> IoResult>> { + with_read_deadline(stream, deadline, |stream, deadline| { + let mut len_buf = [0; 4]; + let mut read = 0; + while read < len_buf.len() { + refresh_read_deadline(stream, deadline)?; + match stream.read(&mut len_buf[read..]) { + Ok(0) if read == 0 => return Ok(None), + Ok(0) => return Err(invalid_data("truncated broker setup frame length")), + Ok(len) => read += len, + Err(error) if error.kind() == ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + + let len = u32::from_le_bytes(len_buf) as usize; + if len == 0 || len > MAX_SETUP_FRAME_LEN { + return Err(invalid_data("invalid broker setup frame length")); + } + + let mut frame = vec![0; len]; + let mut read = 0; + while read < frame.len() { + refresh_read_deadline(stream, deadline)?; + match stream.read(&mut frame[read..]) { + Ok(0) => return Err(invalid_data("truncated broker setup frame")), + Ok(len) => read += len, + Err(error) if error.kind() == ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + Ok(Some(frame)) + }) +} + +/// Writes one length-prefixed setup frame, bounded by `deadline`. +pub(crate) fn write_setup_frame( + stream: &mut UnixStream, + frame: &[u8], + deadline: Option, +) -> IoResult<()> { + with_write_deadline(stream, deadline, |stream, deadline| { + if frame.is_empty() || frame.len() > MAX_SETUP_FRAME_LEN { + return Err(invalid_data("invalid broker setup frame length")); + } + let len = + u32::try_from(frame.len()).map_err(|_| invalid_data("broker setup frame too large"))?; + write_all_with_deadline(stream, &len.to_le_bytes(), deadline)?; + write_all_with_deadline(stream, frame, deadline) + }) +} + +fn write_all_with_deadline( + stream: &mut UnixStream, + mut buffer: &[u8], + deadline: Option, +) -> IoResult<()> { + while !buffer.is_empty() { + refresh_write_deadline(stream, deadline)?; + match stream.write(buffer) { + Ok(0) => { + return Err(Error::new( + ErrorKind::WriteZero, + "failed to write broker setup frame", + )); + } + Ok(written) => buffer = &buffer[written..], + Err(error) if error.kind() == ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + Ok(()) +} + +/// Shuts down both directions of an association socket, tolerating a peer that +/// already disconnected. +pub(crate) fn shutdown_socket(stream: &UnixStream) -> IoResult<()> { + match stream.shutdown(Shutdown::Both) { + Err(error) if error.kind() == ErrorKind::NotConnected => Ok(()), + result => result, + } +} + +/// Builds the fail-closed error both endpoints report for malformed input. +pub(crate) fn invalid_data(message: &'static str) -> Error { + Error::new(ErrorKind::InvalidData, message) +} + +/// Maps a decode failure to the fail-closed error both endpoints report. +pub(crate) fn wire_error(error: WireError) -> Error { + Error::new( + ErrorKind::InvalidData, + format!("invalid broker wire message: {error}"), + ) +} + +/// Clones an error so one recorded terminal failure can be reported to every +/// waiter of an association. +pub(crate) fn copy_io_error(error: &Error) -> Error { + match error.raw_os_error() { + Some(code) => Error::from_raw_os_error(code), + None => Error::new(error.kind(), error.to_string()), + } +} + +/// Maps a control-ring failure to the fail-closed error both endpoints report. +pub(crate) fn ring_error(error: ControlRingError) -> Error { + Error::new( + ErrorKind::InvalidData, + format!("invalid broker control ring: {error:?}"), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn setup_frames_round_trip_and_reject_invalid_boundaries() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + write_setup_frame(&mut writer, &[1, 2, 3], None).unwrap(); + assert_eq!( + read_setup_frame(&mut reader, None).unwrap().unwrap(), + [1, 2, 3] + ); + + let (writer, mut reader) = UnixStream::pair().unwrap(); + drop(writer); + assert!(read_setup_frame(&mut reader, None).unwrap().is_none()); + + for frame_prefix in [ + vec![1, 0], + 0u32.to_le_bytes().to_vec(), + u32::try_from(MAX_SETUP_FRAME_LEN + 1) + .unwrap() + .to_le_bytes() + .to_vec(), + ] { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&frame_prefix).unwrap(); + drop(writer); + assert_eq!( + read_setup_frame(&mut reader, None).unwrap_err().kind(), + ErrorKind::InvalidData + ); + } + + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&4u32.to_le_bytes()).unwrap(); + writer.write_all(&[1, 2]).unwrap(); + drop(writer); + assert_eq!( + read_setup_frame(&mut reader, None).unwrap_err().kind(), + ErrorKind::InvalidData + ); + } +} diff --git a/litebox_broker_transport/src/unix_io.rs b/litebox_broker_transport_linux_userland/src/unix_io.rs similarity index 96% rename from litebox_broker_transport/src/unix_io.rs rename to litebox_broker_transport_linux_userland/src/unix_io.rs index 6d3f573a70..3be27f7b5f 100644 --- a/litebox_broker_transport/src/unix_io.rs +++ b/litebox_broker_transport_linux_userland/src/unix_io.rs @@ -65,7 +65,7 @@ fn combine_result_with_restore( } } -fn io_timeout_for_deadline(deadline: Instant) -> IoResult { +pub(crate) fn io_timeout_for_deadline(deadline: Instant) -> IoResult { deadline .checked_duration_since(Instant::now()) .filter(|timeout| !timeout.is_zero()) diff --git a/litebox_broker_transport_linux_userland/src/unix_socket/host.rs b/litebox_broker_transport_linux_userland/src/unix_socket/host.rs new file mode 100644 index 0000000000..fdc210210d --- /dev/null +++ b/litebox_broker_transport_linux_userland/src/unix_socket/host.rs @@ -0,0 +1,996 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Host (broker-side) endpoints of a Unix-domain-socket broker association. +//! +//! The matching local endpoints live in the sibling `local` module, and both +//! sides share the crate-private `setup` framing. Portable broker interfaces +//! live in the no_std protocol, transport, local, core, and host crates. + +use std::io::{Error, ErrorKind, Read, Result as IoResult}; +use std::mem::size_of; +use std::os::fd::AsRawFd; +use std::os::unix::net::UnixStream; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Instant; + +use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, + BrokerResponse, +}; +use litebox_broker_protocol::wire::{ + WireError, decode_handshake_request, decode_request, encode_handshake_response, + encode_notification, encode_response, +}; +use litebox_broker_transport::channel::{ + HostNotificationChannel, HostReceive, HostSetupChannel, PeerCredential, +}; +use litebox_broker_transport::control_ring::{ + CONTROL_RING_READY, ControlRing, ControlRingConsumer, ControlRingProducer, + ControlRingReadError, ControlRingReadStatus, ControlRingWakeHandle, ControlRingWriteStatus, +}; + +use crate::memfd::MemfdSharedMemory; +use crate::setup::{ + copy_io_error, invalid_data, read_setup_frame, ring_error, shutdown_socket, wire_error, + write_setup_frame, +}; + +/// Validates that a connected Unix socket belongs to `expected_process_id`. +pub fn validate_peer_process(stream: &UnixStream, expected_process_id: u32) -> IoResult<()> { + if peer_process_id(stream)? != expected_process_id { + return Err(Error::new( + ErrorKind::PermissionDenied, + "Unix socket peer is not the expected process", + )); + } + Ok(()) +} + +fn peer_process_id(stream: &UnixStream) -> IoResult { + let expected_length = size_of::(); + let mut credentials = libc::ucred { + pid: 0, + uid: 0, + gid: 0, + }; + let mut actual_length = + libc::socklen_t::try_from(expected_length).expect("Linux ucred size fits socklen_t"); + // SAFETY: `stream` supplies a live socket descriptor, `credentials` is + // writable for `actual_length` bytes, and `actual_length` itself is a valid + // writable socklen_t. + let result = unsafe { + libc::getsockopt( + stream.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_PEERCRED, + std::ptr::from_mut(&mut credentials).cast(), + &raw mut actual_length, + ) + }; + if result != 0 { + return Err(Error::last_os_error()); + } + if actual_length as usize != expected_length { + return Err(invalid_data( + "Unix peer credentials have an unexpected size", + )); + } + validate_peer_process_id(credentials.pid) +} + +fn validate_peer_process_id(process_id: i32) -> IoResult { + match u32::try_from(process_id) { + Ok(process_id) if process_id != 0 => Ok(process_id), + _ => Err(Error::new( + ErrorKind::PermissionDenied, + "Unix socket peer process ID is unavailable", + )), + } +} + +/// Host-side broker association setup channel over a Unix stream. +pub struct UnixStreamHostSetupChannel { + stream: UnixStream, + peer_credential: PeerCredential, + setup_deadline: Option, + negotiated: bool, +} + +/// Request-reading endpoint of an active host control-ring association. +pub struct UnixControlRingHostRequestSource { + consumer: ControlRingConsumer, + association: Arc, +} + +/// Shared response-writing endpoint of an active host control-ring association. +#[derive(Clone)] +pub struct UnixControlRingHostResponseSink { + producer: Arc>>, + association: Arc, +} + +/// RAII guard that interrupts all active host ring I/O when dropped. +pub struct UnixControlRingHostShutdown { + association: Arc, +} + +/// State shared by every activated host endpoint of one association: the setup +/// socket used for liveness and teardown, terminal status, and the wake handles +/// of all three ring directions. +struct HostRingAssociation { + control_stream: UnixStream, + status: Mutex, + request_wake: ControlRingWakeHandle, + response_wake: ControlRingWakeHandle, + notification_wake: ControlRingWakeHandle, +} + +enum HostAssociationStatus { + Live, + PeerClosed, + Failed(Arc), +} + +/// Host notification sender for a shared-ring Unix broker association. +pub struct UnixControlRingHostNotificationChannel { + producer: ControlRingProducer, + association: Arc, +} + +impl UnixStreamHostSetupChannel { + /// Creates a host setup channel from an accepted Unix stream. + pub const fn from_accepted(stream: UnixStream) -> Self { + Self { + stream, + peer_credential: PeerCredential::Unauthenticated, + setup_deadline: None, + negotiated: false, + } + } + + /// Creates a host setup channel after the deployment has authenticated + /// and bound the accepted peer. `setup_deadline` bounds handshake I/O. + pub const fn from_host_guaranteed(stream: UnixStream, setup_deadline: Instant) -> Self { + Self { + stream, + peer_credential: PeerCredential::HostGuaranteed, + setup_deadline: Some(setup_deadline), + negotiated: false, + } + } + + /// Sends a memfd during association setup. + pub fn send_memfd( + &mut self, + shared_memory: &MemfdSharedMemory, + deadline: Option, + ) -> IoResult<()> { + crate::memfd::send_memfd(&mut self.stream, shared_memory, deadline) + } + + /// Consumes a negotiated setup channel into independently usable active + /// request, response, notification, and shutdown handles. + pub fn into_active( + mut self, + ring: ControlRing, + ) -> IoResult<( + UnixControlRingHostRequestSource, + UnixControlRingHostResponseSink, + UnixControlRingHostNotificationChannel, + UnixControlRingHostShutdown, + )> { + if !self.negotiated { + return Err(invalid_data( + "broker host setup channel activated before negotiation completed", + )); + } + let Some(ready) = read_setup_frame(&mut self.stream, self.setup_deadline)? else { + return Err(Error::new( + ErrorKind::UnexpectedEof, + "runner closed before control-ring setup acknowledgement", + )); + }; + if ready != CONTROL_RING_READY { + return Err(invalid_data( + "runner sent an invalid control-ring setup acknowledgement", + )); + } + write_setup_frame(&mut self.stream, CONTROL_RING_READY, self.setup_deadline)?; + + let shutdown_stream = self.stream.try_clone()?; + let litebox_broker_transport::control_ring::BrokerControlRingEndpoints { + request_consumer, + response_producer, + notification_producer, + } = ring.into_broker(); + let association = Arc::new(HostRingAssociation { + control_stream: shutdown_stream, + status: Mutex::new(HostAssociationStatus::Live), + request_wake: request_consumer.wake_handle(), + response_wake: response_producer.wake_handle(), + notification_wake: notification_producer.wake_handle(), + }); + let monitor_association = Arc::clone(&association); + thread::Builder::new() + .name("litebox-runner-liveness".to_owned()) + .spawn(move || monitor_host_socket(&mut self.stream, &monitor_association))?; + Ok(( + UnixControlRingHostRequestSource { + consumer: request_consumer, + association: Arc::clone(&association), + }, + UnixControlRingHostResponseSink { + producer: Arc::new(Mutex::new(response_producer)), + association: Arc::clone(&association), + }, + UnixControlRingHostNotificationChannel { + producer: notification_producer, + association: Arc::clone(&association), + }, + UnixControlRingHostShutdown { association }, + )) + } +} + +impl UnixControlRingHostShutdown { + /// Shuts down the active association without waiting for a ring lock. + pub fn shutdown(&self) -> IoResult<()> { + self.association.fail(Error::new( + ErrorKind::ConnectionAborted, + "broker host association shut down", + )) + } +} + +impl Drop for UnixControlRingHostShutdown { + fn drop(&mut self) { + let _ = self.association.fail(Error::new( + ErrorKind::ConnectionAborted, + "broker host association shutdown guard dropped", + )); + } +} + +impl HostSetupChannel for UnixStreamHostSetupChannel { + type Error = Error; + + fn peer_credential(&self) -> IoResult { + Ok(self.peer_credential) + } + + fn recv_handshake_request(&mut self) -> IoResult> { + let Some(frame) = read_setup_frame(&mut self.stream, self.setup_deadline)? else { + return Ok(HostReceive::PeerClosed); + }; + match decode_handshake_request(&frame) { + Ok(request) => Ok(HostReceive::Message(request)), + Err(WireError::WrongMessagePhase) => Ok(HostReceive::ProtocolViolation), + Err(error) => Err(wire_error(error)), + } + } + + fn send_handshake_response(&mut self, response: &BrokerHandshakeResponse) -> IoResult<()> { + write_setup_frame( + &mut self.stream, + &encode_handshake_response(response.clone()), + self.setup_deadline, + )?; + self.negotiated = matches!(response, BrokerHandshakeResponse::Negotiated { .. }); + Ok(()) + } +} + +impl UnixControlRingHostRequestSource { + /// Receives one active broker request. + pub fn recv_request(&mut self) -> IoResult> { + loop { + if let Some(error) = self.association.current_failure() { + return Err(error); + } + match self.consumer.try_read(decode_request) { + Ok(ControlRingReadStatus::Message(request)) => { + self.association.acknowledge_request(&mut self.consumer)?; + return Ok(HostReceive::Message(request)); + } + Ok(ControlRingReadStatus::Empty { wait_epoch }) => { + if let Some(terminal) = self.association.request_terminal_result() { + return terminal; + } + if let Err(error) = self.consumer.wait_for_message(wait_epoch) { + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } + Err(ControlRingReadError::Decode(WireError::WrongMessagePhase)) => { + return Ok(HostReceive::ProtocolViolation); + } + Err(ControlRingReadError::Decode(error)) => { + let error = wire_error(error); + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + Err(ControlRingReadError::Ring(error)) => { + let error = ring_error(error); + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } + } + } +} + +impl UnixControlRingHostResponseSink { + /// Serializes and sends one complete active broker response. + pub fn send_response(&self, response: &BrokerResponse) -> IoResult<()> { + let frame = encode_response(response.clone()); + let mut producer = self + .producer + .lock() + .map_err(|_| Error::other("broker response writer mutex poisoned"))?; + loop { + match self.association.try_publish(&mut producer, &frame)? { + ControlRingWriteStatus::Written => return Ok(()), + ControlRingWriteStatus::Full { wait_epoch } => { + if let Err(error) = producer.wait_for_capacity(wait_epoch) { + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } + } + } + } +} + +impl HostNotificationChannel for UnixControlRingHostNotificationChannel { + type Error = Error; + + fn send_notification(&mut self, notification: &BrokerNotification) -> IoResult<()> { + let frame = encode_notification(notification.clone()); + loop { + match self.association.try_publish(&mut self.producer, &frame)? { + ControlRingWriteStatus::Written => return Ok(()), + ControlRingWriteStatus::Full { wait_epoch } => { + if let Err(error) = self.producer.wait_for_capacity(wait_epoch) { + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } + } + } + } +} + +impl HostRingAssociation { + fn acknowledge_request( + &self, + consumer: &mut ControlRingConsumer, + ) -> IoResult<()> { + let result = { + let status = self + .status + .lock() + .expect("broker host association mutex poisoned"); + if let HostAssociationStatus::Failed(error) = &*status { + return Err(copy_io_error(error)); + } + consumer + .publish_head() + .map_err(ring_error) + .and_then(|()| consumer.wake_producer()) + }; + if let Err(error) = result { + let result = Err(copy_io_error(&error)); + let _ = self.fail(error); + return result; + } + Ok(()) + } + + fn fail(&self, error: Error) -> IoResult<()> { + { + let mut status = self + .status + .lock() + .expect("broker host association mutex poisoned"); + if matches!(*status, HostAssociationStatus::Live) { + *status = HostAssociationStatus::Failed(Arc::new(error)); + } + } + let request_wake = self.request_wake.interrupt_wait(); + let response_wake = self.response_wake.interrupt_wait(); + let notification_wake = self.notification_wake.interrupt_wait(); + request_wake + .and(response_wake) + .and(notification_wake) + .and(shutdown_socket(&self.control_stream)) + } + + fn peer_closed(&self) { + { + let mut status = self + .status + .lock() + .expect("broker host association mutex poisoned"); + if matches!(*status, HostAssociationStatus::Live) { + *status = HostAssociationStatus::PeerClosed; + } + } + let _ = self.request_wake.interrupt_wait(); + let _ = self.response_wake.interrupt_wait(); + let _ = self.notification_wake.interrupt_wait(); + } + + fn request_terminal_result(&self) -> Option>> { + match &*self + .status + .lock() + .expect("broker host association mutex poisoned") + { + HostAssociationStatus::Live => None, + HostAssociationStatus::PeerClosed => Some(Ok(HostReceive::PeerClosed)), + HostAssociationStatus::Failed(error) => Some(Err(copy_io_error(error))), + } + } + + fn current_failure(&self) -> Option { + match &*self + .status + .lock() + .expect("broker host association mutex poisoned") + { + HostAssociationStatus::Failed(error) => Some(copy_io_error(error)), + HostAssociationStatus::Live | HostAssociationStatus::PeerClosed => None, + } + } + + fn try_publish( + &self, + producer: &mut ControlRingProducer, + frame: &[u8], + ) -> IoResult { + let result = { + let status = self + .status + .lock() + .expect("broker host association mutex poisoned"); + match &*status { + HostAssociationStatus::Live => {} + HostAssociationStatus::PeerClosed => { + return Err(Error::new( + ErrorKind::BrokenPipe, + "runner closed the active broker association", + )); + } + HostAssociationStatus::Failed(error) => return Err(copy_io_error(error)), + } + producer + .try_write(frame) + .map_err(ring_error) + .and_then(|write_status| { + if matches!(write_status, ControlRingWriteStatus::Written) { + producer.wake_consumer()?; + } + Ok(write_status) + }) + }; + if let Err(error) = result { + let result = Err(copy_io_error(&error)); + let _ = self.fail(error); + return result; + } + result + } +} + +fn monitor_host_socket(stream: &mut UnixStream, association: &HostRingAssociation) { + let mut byte = [0]; + loop { + match stream.read(&mut byte) { + Ok(0) => { + association.peer_closed(); + return; + } + Ok(_) => { + let _ = association.fail(invalid_data( + "runner sent unexpected active control-socket data", + )); + return; + } + Err(error) if error.kind() == ErrorKind::Interrupted => {} + Err(error) => { + let _ = association.fail(error); + return; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use litebox_broker_protocol::message::{ + BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, ReadinessNotification, + }; + use litebox_broker_protocol::readiness::ReadinessFlags; + use litebox_broker_protocol::wire::{ + decode_response, encode_handshake_request, encode_request, + }; + use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle, RequestId}; + use litebox_broker_transport::channel::{ + LocalCallChannel, LocalNotificationChannel, LocalSetupChannel, + }; + use litebox_broker_transport::control_ring::{ + CONTROL_RING_MEMORY_SIZE, CONTROL_RING_NOTIFICATION_SLOT_COUNT, CONTROL_RING_SLOT_COUNT, + LocalControlRingEndpoints, + }; + + use crate::unix_socket::local::{ + UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, + UnixStreamLocalSetupChannel, + }; + use std::io::Write; + use std::os::fd::AsFd; + use std::time::Duration; + + type Producer = ControlRingProducer; + type Consumer = ControlRingConsumer; + + fn ring_pair() -> ( + ControlRing, + ControlRing, + ) { + let first = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let second = MemfdSharedMemory::from_received_fd( + first.as_fd().try_clone_to_owned().unwrap(), + CONTROL_RING_MEMORY_SIZE, + ) + .unwrap(); + ( + ControlRing::new(first).unwrap(), + ControlRing::new(second).unwrap(), + ) + } + + fn negotiated_host(stream: UnixStream) -> UnixStreamHostSetupChannel { + UnixStreamHostSetupChannel { + stream, + peer_credential: PeerCredential::HostGuaranteed, + setup_deadline: Some(Instant::now() + Duration::from_secs(2)), + negotiated: true, + } + } + + /// Negotiates one real association over a socket pair, so the local half + /// reaches its negotiated state through the same handshake production uses. + fn negotiated_pair() -> (UnixStreamLocalSetupChannel, UnixStreamHostSetupChannel) { + let (local_stream, host_stream) = UnixStream::pair().unwrap(); + let mut local = UnixStreamLocalSetupChannel::from_connected(local_stream); + let mut host = UnixStreamHostSetupChannel::from_host_guaranteed( + host_stream, + Instant::now() + Duration::from_secs(2), + ); + local + .send_handshake_request(&BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + }) + .unwrap(); + assert!(matches!( + host.recv_handshake_request().unwrap(), + HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + }) + )); + host.send_handshake_response(&BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + }) + .unwrap(); + assert!(matches!( + local.recv_handshake_response().unwrap(), + Some(BrokerHandshakeResponse::Negotiated { .. }) + )); + (local, host) + } + + fn activate_host() -> ( + UnixControlRingHostRequestSource, + UnixControlRingHostResponseSink, + UnixControlRingHostShutdown, + Producer, + Consumer, + UnixStream, + ) { + let (peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut ack_stream = peer_stream.try_clone().unwrap(); + let acknowledgement = thread::spawn(move || { + write_setup_frame(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); + assert_eq!( + read_setup_frame(&mut ack_stream, None).unwrap().unwrap(), + CONTROL_RING_READY + ); + }); + let (local_ring, host_ring) = ring_pair(); + let channel = negotiated_host(host_stream); + let (source, sink, _notifications, shutdown) = channel.into_active(host_ring).unwrap(); + acknowledgement.join().unwrap(); + let LocalControlRingEndpoints { + request_producer, + response_consumer, + notification_consumer: _, + } = local_ring.into_local(); + ( + source, + sink, + shutdown, + request_producer, + response_consumer, + peer_stream, + ) + } + + fn notification_channel_pair() -> ( + UnixControlRingLocalCallChannel, + UnixControlRingLocalNotificationChannel, + UnixControlRingHostNotificationChannel, + UnixControlRingHostShutdown, + ) { + let (local_setup, host_control) = negotiated_pair(); + let (local_ring, host_ring) = ring_pair(); + let host_active = thread::spawn(move || host_control.into_active(host_ring).unwrap()); + let (local_call, local_notifications, _local_shutdown) = + local_setup.into_active(local_ring, || {}).unwrap(); + let (_source, _sink, host_notifications, shutdown) = host_active.join().unwrap(); + ( + local_call, + local_notifications, + host_notifications, + shutdown, + ) + } + + fn read_response(consumer: &mut Consumer) -> BrokerResponse { + loop { + match consumer.try_read(decode_response).unwrap() { + ControlRingReadStatus::Message(response) => { + consumer.publish_head().unwrap(); + consumer.wake_producer().unwrap(); + return response; + } + ControlRingReadStatus::Empty { wait_epoch } => { + consumer.wait_for_message(wait_epoch).unwrap(); + } + } + } + } + + fn write_payload(producer: &mut Producer, payload: &[u8]) { + loop { + match producer.try_write(payload).unwrap() { + ControlRingWriteStatus::Written => { + producer.wake_consumer().unwrap(); + return; + } + ControlRingWriteStatus::Full { wait_epoch } => { + producer.wait_for_capacity(wait_epoch).unwrap(); + } + } + } + } + + fn request(id: u64) -> BrokerRequest { + BrokerRequest { + request_id: RequestId(id), + operation: BrokerOperation::CloseObject(ObjectHandle(id)), + } + } + + fn response(id: RequestId) -> BrokerResponse { + BrokerResponse { + request_id: id, + result: BrokerResult::ObjectClosed, + } + } + + #[test] + fn linux_peer_validation_identifies_connected_process() { + let (first, _second) = UnixStream::pair().unwrap(); + + validate_peer_process(&first, std::process::id()).unwrap(); + let unexpected_process_id = std::process::id().checked_add(1).unwrap(); + assert_eq!( + validate_peer_process(&first, unexpected_process_id) + .unwrap_err() + .kind(), + ErrorKind::PermissionDenied + ); + } + + #[test] + fn linux_peer_validation_rejects_unavailable_process_ids() { + for process_id in [i32::MIN, -1, 0] { + assert_eq!( + validate_peer_process_id(process_id).unwrap_err().kind(), + ErrorKind::PermissionDenied + ); + } + assert_eq!(validate_peer_process_id(1).unwrap(), 1); + } + + #[test] + fn two_way_ready_ack_activates_ring_transport() { + let (local_setup, host) = negotiated_pair(); + let (local_ring, host_ring) = ring_pair(); + let host_active = thread::spawn(move || host.into_active(host_ring).unwrap()); + let (local, _local_notifications, _local_shutdown) = + local_setup.into_active(local_ring, || {}).unwrap(); + let (mut source, sink, _host_notifications, _shutdown) = host_active.join().unwrap(); + + let caller = thread::spawn(move || local.call(request(7))); + let HostReceive::Message(received) = source.recv_request().unwrap() else { + panic!("expected ring request"); + }; + sink.send_response(&response(received.request_id)).unwrap(); + assert_eq!(caller.join().unwrap().unwrap().request_id, RequestId(7)); + } + + #[test] + fn host_activation_decodes_requests_and_cloned_sinks_publish_complete_responses() { + let (mut source, sink, _shutdown, mut requests, mut responses, _peer) = activate_host(); + write_payload(&mut requests, &encode_request(request(1))); + assert!(matches!( + source.recv_request().unwrap(), + HostReceive::Message(BrokerRequest { + request_id: RequestId(1), + .. + }) + )); + + let first = sink.clone(); + let writer = thread::spawn(move || first.send_response(&response(RequestId(3)))); + sink.send_response(&response(RequestId(7))).unwrap(); + writer.join().unwrap().unwrap(); + let mut ids = [ + read_response(&mut responses).request_id, + read_response(&mut responses).request_id, + ]; + ids.sort(); + assert_eq!(ids, [RequestId(3), RequestId(7)]); + } + + #[test] + fn host_clean_close_wakes_request_wait_as_peer_closed() { + let (mut source, _sink, _shutdown, _requests, _responses, peer) = activate_host(); + let receiver = thread::spawn(move || source.recv_request()); + drop(peer); + assert_eq!(receiver.join().unwrap().unwrap(), HostReceive::PeerClosed); + } + + #[test] + fn host_failure_preempts_queued_and_decoded_requests_but_peer_close_drains() { + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); + write_payload(&mut requests, &encode_request(request(1))); + source + .association + .fail(Error::new(ErrorKind::TimedOut, "test failure")) + .unwrap(); + assert_eq!( + source.recv_request().unwrap_err().kind(), + ErrorKind::TimedOut + ); + + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); + write_payload(&mut requests, &encode_request(request(2))); + assert!(matches!( + source.consumer.try_read(decode_request).unwrap(), + ControlRingReadStatus::Message(_) + )); + source + .association + .fail(Error::new(ErrorKind::TimedOut, "test failure")) + .unwrap(); + assert_eq!( + source + .association + .acknowledge_request(&mut source.consumer) + .unwrap_err() + .kind(), + ErrorKind::TimedOut + ); + + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); + write_payload(&mut requests, &encode_request(request(3))); + source.association.peer_closed(); + assert!(matches!( + source.recv_request().unwrap(), + HostReceive::Message(BrokerRequest { + request_id: RequestId(3), + .. + }) + )); + assert_eq!(source.recv_request().unwrap(), HostReceive::PeerClosed); + } + + #[test] + fn dropping_host_shutdown_guard_wakes_request_wait_and_closes_socket() { + let (mut source, sink, shutdown, _requests, _responses, mut peer) = activate_host(); + peer.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); + let receiver = thread::spawn(move || source.recv_request()); + + drop(sink); + drop(shutdown); + + assert_eq!( + receiver.join().unwrap().unwrap_err().kind(), + ErrorKind::ConnectionAborted + ); + let mut byte = [0]; + assert_eq!(peer.read(&mut byte).unwrap(), 0); + } + + #[test] + fn host_close_wakes_response_producer_blocked_on_full_ring() { + let (_source, sink, _shutdown, _requests, _responses, peer) = activate_host(); + for id in 0..CONTROL_RING_SLOT_COUNT { + sink.send_response(&response(RequestId(id))).unwrap(); + } + let blocked_sink = sink.clone(); + let blocked = thread::spawn(move || blocked_sink.send_response(&response(RequestId(99)))); + thread::sleep(Duration::from_millis(20)); + drop(peer); + assert_eq!( + blocked.join().unwrap().unwrap_err().kind(), + ErrorKind::BrokenPipe + ); + } + + #[test] + fn host_reports_wrong_phase_ring_message_as_protocol_violation() { + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); + write_payload( + &mut requests, + &encode_handshake_request(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + }), + ); + assert_eq!( + source.recv_request().unwrap(), + HostReceive::ProtocolViolation + ); + } + + #[test] + fn malformed_host_ring_request_is_fatal_invalid_data() { + let (mut source, _sink, _shutdown, mut requests, _responses, _peer) = activate_host(); + write_payload(&mut requests, &[u8::MAX]); + assert_eq!( + source.recv_request().unwrap_err().kind(), + ErrorKind::InvalidData + ); + } + + #[test] + fn host_setup_rejects_active_frames_and_requires_negotiation() { + let (mut peer_stream, host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamHostSetupChannel::from_accepted(host_stream); + write_setup_frame(&mut peer_stream, &encode_request(request(0)), None).unwrap(); + assert_eq!( + channel.recv_handshake_request().unwrap(), + HostReceive::ProtocolViolation + ); + + let (_peer_stream, host_stream) = UnixStream::pair().unwrap(); + let channel = UnixStreamHostSetupChannel::from_accepted(host_stream); + let (ring, _) = ring_pair(); + let Err(error) = channel.into_active(ring) else { + panic!("host control channel activated before negotiation"); + }; + assert_eq!(error.kind(), ErrorKind::InvalidData); + } + + #[test] + fn host_handshake_reads_use_absolute_setup_deadlines() { + let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); + let mut host = UnixStreamHostSetupChannel::from_host_guaranteed( + host_stream, + Instant::now() + Duration::from_millis(50), + ); + let host_reader = thread::spawn(move || host.recv_handshake_request().unwrap_err()); + local_stream.write_all(&8u32.to_le_bytes()).unwrap(); + for _ in 0..8 { + thread::sleep(Duration::from_millis(20)); + if local_stream.write_all(&[0]).is_err() { + break; + } + } + let error = host_reader.join().unwrap(); + assert!( + matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut), + "unexpected host timeout error: {error:?}" + ); + } + + #[test] + fn notification_ring_round_trips() { + let (_control, mut local, mut host, _shutdown) = notification_channel_pair(); + let notification = BrokerNotification::Readiness(ReadinessNotification { + handle: ObjectHandle(7), + readiness: ReadinessFlags::READ, + }); + + let receiver = thread::spawn(move || local.recv_notification()); + thread::sleep(Duration::from_millis(20)); + host.send_notification(¬ification).unwrap(); + + assert_eq!(receiver.join().unwrap().unwrap(), Some(notification)); + } + + #[test] + fn full_notification_ring_wakes_after_consumer_progress() { + let (_control, mut local, mut host, _shutdown) = notification_channel_pair(); + let notification = BrokerNotification::Readiness(ReadinessNotification { + handle: ObjectHandle(7), + readiness: ReadinessFlags::READ, + }); + for _ in 0..CONTROL_RING_NOTIFICATION_SLOT_COUNT { + host.send_notification(¬ification).unwrap(); + } + + let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(1); + let (done_sender, done_receiver) = std::sync::mpsc::sync_channel(1); + let writer = thread::spawn(move || { + started_sender.send(()).unwrap(); + host.send_notification(¬ification).unwrap(); + done_sender.send(()).unwrap(); + }); + started_receiver.recv().unwrap(); + assert!( + done_receiver + .recv_timeout(Duration::from_millis(20)) + .is_err() + ); + + assert!(matches!( + local.recv_notification().unwrap(), + Some(BrokerNotification::Readiness(_)) + )); + done_receiver.recv_timeout(Duration::from_secs(1)).unwrap(); + writer.join().unwrap(); + } + + #[test] + fn association_shutdown_interrupts_notification_wait() { + let (_control, mut local, _host, shutdown) = notification_channel_pair(); + let receiver = thread::spawn(move || local.recv_notification()); + + shutdown.shutdown().unwrap(); + + assert_eq!( + receiver.join().unwrap().unwrap_err().kind(), + ErrorKind::UnexpectedEof + ); + } + + #[test] + fn malformed_notification_fails_the_association() { + let (control, mut local, mut host, _shutdown) = notification_channel_pair(); + assert_eq!( + host.producer.try_write(&[0xff]).unwrap(), + ControlRingWriteStatus::Written + ); + host.producer.wake_consumer().unwrap(); + + assert_eq!( + local.recv_notification().unwrap_err().kind(), + ErrorKind::InvalidData + ); + // The local association is failed, so it refuses further calls without + // ever reaching the ring. + assert!(control.call(request(1)).is_err()); + } +} diff --git a/litebox_broker_transport_linux_userland/src/unix_socket/local.rs b/litebox_broker_transport_linux_userland/src/unix_socket/local.rs new file mode 100644 index 0000000000..6b090a1eff --- /dev/null +++ b/litebox_broker_transport_linux_userland/src/unix_socket/local.rs @@ -0,0 +1,1174 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Local (guest-side) endpoints of a Unix-domain-socket broker association. +//! +//! The matching host endpoints live in the sibling `host` module, and both +//! sides share the crate-private `setup` framing. Portable broker interfaces +//! live in the no_std protocol, transport, local, core, and host crates. + +use std::io::{Error, ErrorKind, Read, Result as IoResult}; +use std::os::fd::{AsFd, BorrowedFd, OwnedFd}; +use std::os::unix::net::UnixStream; +use std::path::Path; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::{Duration, Instant}; +use std::{collections::HashMap, thread}; + +use rustix::event::{PollFd, PollFlags, Timespec, poll}; +use rustix::io::Errno; +use rustix::net::{ + AddressFamily, SocketAddrUnix, SocketFlags, SocketType, connect, socket_with, sockopt, +}; + +use litebox_broker_protocol::RequestId; +use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerNotification, BrokerRequest, + BrokerResponse, +}; +use litebox_broker_protocol::wire::{ + decode_handshake_response, decode_notification, decode_response, encode_handshake_request, + encode_request, +}; +use litebox_broker_transport::channel::{ + LocalCallChannel, LocalNotificationChannel, LocalSetupChannel, +}; +use litebox_broker_transport::control_ring::{ + CONTROL_RING_READY, ControlRing, ControlRingConsumer, ControlRingProducer, + ControlRingReadError, ControlRingReadStatus, ControlRingWakeHandle, ControlRingWriteStatus, +}; + +use crate::memfd::MemfdSharedMemory; +use crate::setup::{ + copy_io_error, invalid_data, read_setup_frame, ring_error, shutdown_socket, wire_error, + write_setup_frame, +}; +use crate::unix_io::io_timeout_for_deadline; + +const CONNECT_RETRY_DELAY: Duration = Duration::from_millis(10); + +/// Maximum number of active calls waiting for broker responses. +pub const MAX_PENDING_CALLS: usize = 64; + +/// Local-side broker association setup channel over a Unix stream. +pub struct UnixStreamLocalSetupChannel { + stream: UnixStream, + setup_deadline: Option, + negotiated: bool, +} + +/// Call-issuing endpoint of an active local control-ring association. +pub struct UnixControlRingLocalCallChannel { + association: Arc, +} + +/// Independently owned handle for interrupting all local active-ring I/O. +pub struct UnixControlRingLocalShutdown { + association: Arc, +} + +/// State shared by every activated local endpoint of one association: the +/// request producer, the setup socket used for liveness and teardown, pending +/// call tracking, and the wake handles of all three ring directions. +struct LocalRingAssociation { + request_producer: Mutex>, + control_stream: UnixStream, + pending_calls: Arc, + on_failure: Arc, + request_wake: ControlRingWakeHandle, + response_wake: ControlRingWakeHandle, + notification_wake: ControlRingWakeHandle, +} + +/// Local notification receiver for a shared-ring Unix broker association. +pub struct UnixControlRingLocalNotificationChannel { + consumer: ControlRingConsumer, + association: Arc, +} + +impl UnixStreamLocalSetupChannel { + /// Creates a local setup channel from an already-connected Unix stream. + pub const fn from_connected(stream: UnixStream) -> Self { + Self { + stream, + setup_deadline: None, + negotiated: false, + } + } + + /// Connects to a userland broker Unix socket. + pub fn connect(path: impl AsRef) -> IoResult { + UnixStream::connect(path).map(Self::from_connected) + } + + /// Connects to a userland broker Unix socket with an absolute deadline for + /// the connection and subsequent setup I/O. + pub fn connect_with_setup_deadline( + path: impl AsRef, + deadline: Instant, + ) -> IoResult { + connect_with_deadline(path.as_ref(), deadline).map(|stream| Self { + stream, + setup_deadline: Some(deadline), + negotiated: false, + }) + } + + /// Receives one memfd offered by the broker during setup. + pub fn receive_memfd( + &mut self, + expected_len: usize, + deadline: Option, + ) -> IoResult { + crate::memfd::receive_memfd(&mut self.stream, expected_len, deadline) + } + + /// Consumes a negotiated setup channel into independently usable active + /// call, notification, and shutdown handles, starting the response + /// dispatcher and liveness monitor. + /// + /// The ring must be the validated control-ring memfd received during this + /// setup exchange. + pub fn into_active( + self, + ring: ControlRing, + on_failure: impl Fn() + Send + Sync + 'static, + ) -> IoResult<( + UnixControlRingLocalCallChannel, + UnixControlRingLocalNotificationChannel, + UnixControlRingLocalShutdown, + )> { + if !self.negotiated { + return Err(invalid_data( + "broker local setup channel activated before negotiation completed", + )); + } + + let mut setup_stream = self.stream; + write_setup_frame(&mut setup_stream, CONTROL_RING_READY, self.setup_deadline)?; + let Some(ready) = read_setup_frame(&mut setup_stream, self.setup_deadline)? else { + return Err(Error::new( + ErrorKind::UnexpectedEof, + "broker closed before control-ring setup acknowledgement", + )); + }; + if ready != CONTROL_RING_READY { + return Err(invalid_data( + "broker sent an invalid control-ring setup acknowledgement", + )); + } + + let shutdown_stream = setup_stream.try_clone()?; + let litebox_broker_transport::control_ring::LocalControlRingEndpoints { + request_producer, + response_consumer, + notification_consumer, + } = ring.into_local(); + let pending_calls = Arc::new(PendingCalls::new()); + let on_failure: Arc = Arc::new(on_failure); + let association = Arc::new(LocalRingAssociation { + request_wake: request_producer.wake_handle(), + request_producer: Mutex::new(request_producer), + control_stream: shutdown_stream, + pending_calls: Arc::clone(&pending_calls), + on_failure, + response_wake: response_consumer.wake_handle(), + notification_wake: notification_consumer.wake_handle(), + }); + let response_association = Arc::clone(&association); + if let Err(error) = thread::Builder::new() + .name("litebox-broker-responses".to_owned()) + .spawn(move || { + dispatch_responses(response_consumer, response_association); + }) + { + let _ = association.fail(error); + return Err(Error::other("failed to start broker response dispatcher")); + } + let monitor_association = Arc::clone(&association); + if let Err(error) = thread::Builder::new() + .name("litebox-broker-liveness".to_owned()) + .spawn(move || { + monitor_local_socket(&mut setup_stream, &monitor_association); + }) + { + let _ = association.fail(error); + return Err(Error::other("failed to start broker liveness monitor")); + } + + Ok(( + UnixControlRingLocalCallChannel { + association: Arc::clone(&association), + }, + UnixControlRingLocalNotificationChannel { + consumer: notification_consumer, + association: Arc::clone(&association), + }, + UnixControlRingLocalShutdown { association }, + )) + } +} + +fn connect_with_deadline(path: &Path, deadline: Instant) -> IoResult { + io_timeout_for_deadline(deadline)?; + let address = SocketAddrUnix::new(path)?; + let socket = socket_with( + AddressFamily::UNIX, + SocketType::STREAM, + SocketFlags::CLOEXEC | SocketFlags::NONBLOCK, + None, + )?; + + loop { + let remaining = io_timeout_for_deadline(deadline)?; + match connect(&socket, &address) { + Ok(()) | Err(Errno::ISCONN) => break, + Err(Errno::INTR) => {} + // Linux reports a full Unix-domain listen queue as EAGAIN without + // starting a connection. Polling this socket would falsely report + // it writable with no SO_ERROR, so retry connect instead. + Err(Errno::AGAIN) => thread::sleep(CONNECT_RETRY_DELAY.min(remaining)), + Err(Errno::INPROGRESS | Errno::ALREADY) => { + wait_for_nonblocking_connect(&socket, deadline)?; + break; + } + Err(error) => return Err(error.into()), + } + } + + let stream = UnixStream::from(socket); + stream.set_nonblocking(false)?; + io_timeout_for_deadline(deadline)?; + Ok(stream) +} + +fn wait_for_nonblocking_connect(socket: &OwnedFd, deadline: Instant) -> IoResult<()> { + loop { + let remaining = io_timeout_for_deadline(deadline)?; + let timeout = Timespec::try_from(remaining).map_err(|_| { + Error::new( + ErrorKind::InvalidInput, + "broker setup deadline is too distant", + ) + })?; + let mut poll_fd = [PollFd::new(socket, PollFlags::OUT)]; + match poll(&mut poll_fd, Some(&timeout)) { + Ok(0) | Err(Errno::INTR) => {} + Ok(_) => match sockopt::socket_error(socket)? { + Ok(()) => return Ok(()), + Err(error) => return Err(error.into()), + }, + Err(error) => return Err(error.into()), + } + } +} + +impl UnixControlRingLocalShutdown { + /// Shuts down the active association, unblocking ring and socket waits. + pub fn shutdown(&self) -> IoResult<()> { + self.association.fail(Error::new( + ErrorKind::ConnectionAborted, + "broker local association shut down", + )) + } +} + +impl AsFd for UnixControlRingLocalShutdown { + fn as_fd(&self) -> BorrowedFd<'_> { + self.association.control_stream.as_fd() + } +} + +impl Drop for UnixControlRingLocalCallChannel { + fn drop(&mut self) { + let _ = self.association.fail(Error::new( + ErrorKind::ConnectionAborted, + "broker local call channel dropped", + )); + } +} + +impl LocalSetupChannel for UnixStreamLocalSetupChannel { + type Error = Error; + + fn send_handshake_request(&mut self, request: &BrokerHandshakeRequest) -> IoResult<()> { + let frame = encode_handshake_request(request.clone()); + write_setup_frame(&mut self.stream, &frame, self.setup_deadline) + } + + fn recv_handshake_response(&mut self) -> IoResult> { + let frame = read_setup_frame(&mut self.stream, self.setup_deadline)?; + match frame { + Some(frame) => { + let response = decode_handshake_response(&frame).map_err(wire_error)?; + self.negotiated = matches!(&response, BrokerHandshakeResponse::Negotiated { .. }); + Ok(Some(response)) + } + None => Ok(None), + } + } +} + +impl LocalCallChannel for UnixControlRingLocalCallChannel { + type Error = Error; + + fn call(&self, request: BrokerRequest) -> IoResult { + let association = &self.association; + let request_id = request.request_id; + let pending_call = association.pending_calls.register(request_id)?; + let request_frame = encode_request(request); + + let write_result = { + let mut producer = association + .request_producer + .lock() + .expect("broker request writer mutex poisoned"); + loop { + let write_status = association + .pending_calls + .run_if_live(|| producer.try_write(&request_frame).map_err(ring_error)); + match write_status { + Ok(ControlRingWriteStatus::Written) => { + if let Err(error) = producer.wake_consumer() { + break Err(error); + } + break Ok(()); + } + Ok(ControlRingWriteStatus::Full { wait_epoch }) => { + if let Err(error) = producer.wait_for_capacity(wait_epoch) { + break Err(error); + } + } + Err(error) => break Err(error), + } + } + }; + if let Err(error) = write_result { + let _ = association.fail(error); + } + + pending_call.wait() + } +} + +impl LocalNotificationChannel for UnixControlRingLocalNotificationChannel { + type Error = Error; + + fn recv_notification(&mut self) -> IoResult> { + loop { + if let Some(error) = self.association.pending_calls.current_failure() { + return Err(copy_io_error(&error)); + } + match self.consumer.try_read(decode_notification) { + Ok(ControlRingReadStatus::Message(notification)) => { + self.association + .acknowledge_notification(&mut self.consumer)?; + return Ok(Some(notification)); + } + Ok(ControlRingReadStatus::Empty { wait_epoch }) => { + if let Some(error) = self.association.pending_calls.current_failure() { + return Err(copy_io_error(&error)); + } + if let Err(error) = self.consumer.wait_for_message(wait_epoch) { + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } + Err(ControlRingReadError::Ring(error)) => { + let error = ring_error(error); + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + Err(ControlRingReadError::Decode(error)) => { + let error = wire_error(error); + let result = Err(copy_io_error(&error)); + let _ = self.association.fail(error); + return result; + } + } + } + } +} + +struct PendingCalls { + state: Mutex, + capacity_available: Condvar, +} + +struct PendingCallsState { + calls: HashMap>, + failure: Option>, +} + +struct PendingCall { + result: Mutex>, + result_ready: Condvar, +} + +enum PendingCallResult { + Response(BrokerResponse), + Failure(Arc), +} + +impl PendingCall { + fn new() -> Self { + Self { + result: Mutex::new(None), + result_ready: Condvar::new(), + } + } + + fn resolve(&self, result: PendingCallResult) { + let mut stored = self + .result + .lock() + .expect("broker pending-call result mutex poisoned"); + assert!(stored.is_none(), "broker pending call already resolved"); + *stored = Some(result); + self.result_ready.notify_one(); + } + + fn wait(&self) -> IoResult { + let mut result = self + .result + .lock() + .expect("broker pending-call result mutex poisoned"); + loop { + if let Some(result) = result.take() { + return match result { + PendingCallResult::Response(response) => Ok(response), + PendingCallResult::Failure(error) => Err(copy_io_error(&error)), + }; + } + result = self + .result_ready + .wait(result) + .expect("broker pending-call result mutex poisoned"); + } + } +} + +impl PendingCalls { + fn new() -> Self { + Self { + state: Mutex::new(PendingCallsState { + calls: HashMap::new(), + failure: None, + }), + capacity_available: Condvar::new(), + } + } + + fn register(&self, request_id: RequestId) -> IoResult> { + let pending_call = Arc::new(PendingCall::new()); + let mut state = self.state.lock().expect("broker pending mutex poisoned"); + while state.calls.len() == MAX_PENDING_CALLS && state.failure.is_none() { + state = self + .capacity_available + .wait(state) + .expect("broker pending mutex poisoned"); + } + if let Some(error) = state.failure.as_ref() { + return Err(copy_io_error(error)); + } + match state.calls.entry(request_id) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(Arc::clone(&pending_call)); + } + std::collections::hash_map::Entry::Occupied(_) => { + return Err(invalid_data("duplicate broker request ID")); + } + } + Ok(pending_call) + } + + fn complete(&self, response: BrokerResponse) -> IoResult<()> { + let pending_call = { + let mut state = self.state.lock().expect("broker pending mutex poisoned"); + if let Some(error) = state.failure.as_ref() { + return Err(copy_io_error(error)); + } + let Some(pending_call) = state.calls.remove(&response.request_id) else { + return Err(invalid_data("broker returned an unknown response ID")); + }; + self.capacity_available.notify_one(); + pending_call + }; + pending_call.resolve(PendingCallResult::Response(response)); + Ok(()) + } + + fn record_failure(&self, error: Arc) -> bool { + let pending_calls = { + let mut state = self.state.lock().expect("broker pending mutex poisoned"); + if state.failure.is_some() { + return false; + } + state.failure = Some(Arc::clone(&error)); + let pending_calls = core::mem::take(&mut state.calls); + self.capacity_available.notify_all(); + pending_calls + }; + for pending_call in pending_calls.into_values() { + pending_call.resolve(PendingCallResult::Failure(Arc::clone(&error))); + } + true + } + + fn current_failure(&self) -> Option> { + self.state + .lock() + .expect("broker pending mutex poisoned") + .failure + .as_ref() + .map(Arc::clone) + } + + /// Runs a nonblocking publication while excluding failure recording. + fn run_if_live(&self, operation: impl FnOnce() -> IoResult) -> IoResult { + let state = self.state.lock().expect("broker pending mutex poisoned"); + if let Some(error) = state.failure.as_ref() { + return Err(copy_io_error(error)); + } + operation() + } +} + +impl LocalRingAssociation { + fn acknowledge_notification( + &self, + consumer: &mut ControlRingConsumer, + ) -> IoResult<()> { + let result = self.pending_calls.run_if_live(|| { + consumer + .publish_head() + .map_err(ring_error) + .and_then(|()| consumer.wake_producer()) + }); + if let Err(error) = result { + let result = Err(copy_io_error(&error)); + let _ = self.fail(error); + return result; + } + Ok(()) + } + + fn fail(&self, error: Error) -> IoResult<()> { + let first_failure = self.pending_calls.record_failure(Arc::new(error)); + let request_wake = self.request_wake.interrupt_wait(); + let response_wake = self.response_wake.interrupt_wait(); + let notification_wake = self.notification_wake.interrupt_wait(); + let shutdown_result = shutdown_socket(&self.control_stream); + if first_failure { + (self.on_failure)(); + } + request_wake + .and(response_wake) + .and(notification_wake) + .and(shutdown_result) + } +} + +fn monitor_local_socket(stream: &mut UnixStream, association: &LocalRingAssociation) { + let error = wait_for_socket_termination(stream, "broker"); + let _ = association.fail(error); +} + +fn wait_for_socket_termination(stream: &mut UnixStream, peer: &'static str) -> Error { + let mut byte = [0]; + loop { + match stream.read(&mut byte) { + Ok(0) => { + return Error::new( + ErrorKind::UnexpectedEof, + format!("{peer} closed the active broker association"), + ); + } + Ok(_) => { + return invalid_data("peer sent unexpected active control-socket data"); + } + Err(error) if error.kind() == ErrorKind::Interrupted => {} + Err(error) => return error, + } + } +} + +fn dispatch_responses( + mut consumer: ControlRingConsumer, + association: Arc, +) { + loop { + match consumer.try_read(decode_response) { + Ok(ControlRingReadStatus::Message(response)) => { + if let Err(error) = consumer + .publish_head() + .map_err(ring_error) + .and_then(|()| consumer.wake_producer()) + .and_then(|()| association.pending_calls.complete(response)) + { + let _ = association.fail(error); + return; + } + } + Ok(ControlRingReadStatus::Empty { wait_epoch }) => { + if association.pending_calls.current_failure().is_some() { + return; + } + if let Err(error) = consumer.wait_for_message(wait_epoch) { + let _ = association.fail(error); + return; + } + } + Err(ControlRingReadError::Ring(error)) => { + let _ = association.fail(ring_error(error)); + return; + } + Err(ControlRingReadError::Decode(error)) => { + let _ = association.fail(wire_error(error)); + return; + } + } + } +} + +#[cfg(test)] +mod control_ring_tests { + use super::*; + use litebox_broker_protocol::message::{ + BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, + }; + use litebox_broker_protocol::wire::{ + decode_handshake_request, decode_request, encode_handshake_response, encode_response, + }; + use litebox_broker_protocol::{ObjectHandle, RequestId}; + use litebox_broker_transport::channel::{LocalCallChannel, LocalSetupChannel}; + use litebox_broker_transport::control_ring::CONTROL_RING_MEMORY_SIZE; + use rustix::fs::{OFlags, fcntl_getfl}; + use std::io::{Read, Write}; + use std::os::fd::AsFd; + use std::os::unix::net::UnixListener; + use std::path::PathBuf; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Barrier, mpsc}; + + type Producer = ControlRingProducer; + type Consumer = ControlRingConsumer; + + struct TestSocketPath(PathBuf); + + impl TestSocketPath { + fn new() -> Self { + static NEXT_PATH: AtomicUsize = AtomicUsize::new(0); + Self(std::env::temp_dir().join(format!( + "litebox-broker-connect-{}-{}", + std::process::id(), + NEXT_PATH.fetch_add(1, Ordering::Relaxed) + ))) + } + + fn as_path(&self) -> &Path { + &self.0 + } + } + + impl Drop for TestSocketPath { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } + } + + fn saturated_listener() -> (TestSocketPath, UnixListener, Vec) { + let path = TestSocketPath::new(); + let listener = UnixListener::bind(path.as_path()).unwrap(); + rustix::net::listen(&listener, 0).unwrap(); + let address = SocketAddrUnix::new(path.as_path()).unwrap(); + let mut queued = Vec::new(); + for _ in 0..1024 { + let socket = socket_with( + AddressFamily::UNIX, + SocketType::STREAM, + SocketFlags::CLOEXEC | SocketFlags::NONBLOCK, + None, + ) + .unwrap(); + match connect(&socket, &address) { + Ok(()) => queued.push(socket), + Err(Errno::AGAIN) => return (path, listener, queued), + Err(error) => panic!("unexpected queue-filling connect error: {error}"), + } + } + panic!("failed to saturate Unix listener queue"); + } + + fn ring_pair() -> ( + ControlRing, + ControlRing, + ) { + let first = MemfdSharedMemory::create(CONTROL_RING_MEMORY_SIZE).unwrap(); + let second = MemfdSharedMemory::from_received_fd( + first.as_fd().try_clone_to_owned().unwrap(), + CONTROL_RING_MEMORY_SIZE, + ) + .unwrap(); + ( + ControlRing::new(first).unwrap(), + ControlRing::new(second).unwrap(), + ) + } + + fn negotiated_local(stream: UnixStream) -> UnixStreamLocalSetupChannel { + UnixStreamLocalSetupChannel { + stream, + setup_deadline: Some(Instant::now() + Duration::from_secs(2)), + negotiated: true, + } + } + + fn activate_local( + on_failure: impl Fn() + Send + Sync + 'static, + ) -> ( + UnixControlRingLocalCallChannel, + UnixControlRingLocalShutdown, + Producer, + Consumer, + UnixStream, + ) { + let (local_stream, peer_stream) = UnixStream::pair().unwrap(); + let mut ack_stream = peer_stream.try_clone().unwrap(); + let acknowledgement = thread::spawn(move || { + assert_eq!( + read_setup_frame(&mut ack_stream, None).unwrap().unwrap(), + CONTROL_RING_READY + ); + write_setup_frame(&mut ack_stream, CONTROL_RING_READY, None).unwrap(); + }); + let (local_ring, broker_ring) = ring_pair(); + let setup = negotiated_local(local_stream); + let (channel, _notifications, shutdown) = + setup.into_active(local_ring, on_failure).unwrap(); + acknowledgement.join().unwrap(); + let litebox_broker_transport::control_ring::BrokerControlRingEndpoints { + request_consumer, + response_producer, + notification_producer: _, + } = broker_ring.into_broker(); + ( + channel, + shutdown, + response_producer, + request_consumer, + peer_stream, + ) + } + + fn read_request(consumer: &mut Consumer) -> BrokerRequest { + loop { + match consumer.try_read(decode_request).unwrap() { + ControlRingReadStatus::Message(request) => { + consumer.publish_head().unwrap(); + consumer.wake_producer().unwrap(); + return request; + } + ControlRingReadStatus::Empty { wait_epoch } => { + consumer.wait_for_message(wait_epoch).unwrap(); + } + } + } + } + + fn write_payload(producer: &mut Producer, payload: &[u8]) { + loop { + match producer.try_write(payload).unwrap() { + ControlRingWriteStatus::Written => { + producer.wake_consumer().unwrap(); + return; + } + ControlRingWriteStatus::Full { wait_epoch } => { + producer.wait_for_capacity(wait_epoch).unwrap(); + } + } + } + } + + fn request(id: u64) -> BrokerRequest { + BrokerRequest { + request_id: RequestId(id), + operation: BrokerOperation::CloseObject(ObjectHandle(id)), + } + } + + fn response(id: RequestId) -> BrokerResponse { + BrokerResponse { + request_id: id, + result: BrokerResult::ObjectClosed, + } + } + + #[test] + fn local_setup_rejects_activation_before_negotiation() { + let (local_stream, _host_stream) = UnixStream::pair().unwrap(); + let setup = UnixStreamLocalSetupChannel::from_connected(local_stream); + let (ring, _) = ring_pair(); + let Err(error) = setup.into_active(ring, || {}) else { + panic!("local setup channel activated before negotiation"); + }; + assert_eq!(error.kind(), ErrorKind::InvalidData); + } + + #[test] + fn local_setup_negotiates_then_activates_and_closes_on_drop() { + let (local_stream, mut host_stream) = UnixStream::pair().unwrap(); + let mut setup = UnixStreamLocalSetupChannel::from_connected(local_stream); + + let handshake_request = BrokerHandshakeRequest { + protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }; + setup.send_handshake_request(&handshake_request).unwrap(); + assert_eq!( + decode_handshake_request(&read_setup_frame(&mut host_stream, None).unwrap().unwrap()) + .unwrap(), + handshake_request + ); + write_setup_frame( + &mut host_stream, + &encode_handshake_response(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: litebox_broker_protocol::BROKER_PROTOCOL_VERSION, + }), + None, + ) + .unwrap(); + assert!(matches!( + setup.recv_handshake_response().unwrap(), + Some(BrokerHandshakeResponse::Negotiated { .. }) + )); + + let acknowledgement = thread::spawn(move || { + assert_eq!( + read_setup_frame(&mut host_stream, None).unwrap().unwrap(), + CONTROL_RING_READY + ); + write_setup_frame(&mut host_stream, CONTROL_RING_READY, None).unwrap(); + host_stream + }); + let (ring, _) = ring_pair(); + let (call_channel, _notifications, _shutdown) = setup.into_active(ring, || {}).unwrap(); + let mut host_stream = acknowledgement.join().unwrap(); + + host_stream + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); + drop(call_channel); + let mut byte = [0]; + assert_eq!(host_stream.read(&mut byte).unwrap(), 0); + } + + #[test] + fn local_matches_out_of_order_ring_responses_without_socket_frames() { + let (channel, _shutdown, mut responses, mut requests, mut peer) = activate_local(|| {}); + peer.set_read_timeout(Some(Duration::from_millis(100))) + .unwrap(); + let channel = Arc::new(channel); + let calls = [3, 7].map(|id| { + let channel = Arc::clone(&channel); + thread::spawn(move || channel.call(request(id))) + }); + let first = read_request(&mut requests); + let second = read_request(&mut requests); + + write_payload( + &mut responses, + &encode_response(response(second.request_id)), + ); + write_payload(&mut responses, &encode_response(response(first.request_id))); + for call in calls { + assert!(call.join().unwrap().is_ok()); + } + let mut byte = [0]; + assert!(matches!( + peer.read(&mut byte).unwrap_err().kind(), + ErrorKind::WouldBlock | ErrorKind::TimedOut + )); + } + + #[test] + fn pending_capacity_blocks_before_sixty_fifth_publication() { + let (channel, shutdown, mut responses, mut requests, _peer) = activate_local(|| {}); + let channel = Arc::new(channel); + let start = Arc::new(Barrier::new(MAX_PENDING_CALLS + 2)); + let callers = (0..=MAX_PENDING_CALLS) + .map(|id| { + let channel = Arc::clone(&channel); + let start = Arc::clone(&start); + thread::spawn(move || { + start.wait(); + channel.call(request(id as u64)) + }) + }) + .collect::>(); + start.wait(); + + let mut published = Vec::new(); + for _ in 0..MAX_PENDING_CALLS { + published.push(read_request(&mut requests).request_id); + } + write_payload(&mut responses, &encode_response(response(published[0]))); + let released = read_request(&mut requests).request_id; + assert!(!published.contains(&released)); + + shutdown.shutdown().unwrap(); + let completed = callers + .into_iter() + .map(|caller| usize::from(caller.join().unwrap().is_ok())) + .sum::(); + assert_eq!(completed, 1); + } + + #[test] + fn unknown_duplicate_and_malformed_responses_fail_closed() { + for payload_kind in 0..3 { + let failures = Arc::new(AtomicUsize::new(0)); + let callback_failures = Arc::clone(&failures); + let (failure_reported, wait_for_failure) = mpsc::channel(); + let (channel, _shutdown, mut responses, mut requests, _peer) = + activate_local(move || { + callback_failures.fetch_add(1, Ordering::SeqCst); + failure_reported.send(()).unwrap(); + }); + let channel = Arc::new(channel); + let calls = [1, 2].map(|id| { + let channel = Arc::clone(&channel); + thread::spawn(move || channel.call(request(id))) + }); + read_request(&mut requests); + read_request(&mut requests); + + match payload_kind { + 0 => write_payload(&mut responses, &encode_response(response(RequestId(99)))), + 1 => { + let duplicate = encode_response(response(RequestId(1))); + write_payload(&mut responses, &duplicate); + write_payload(&mut responses, &duplicate); + } + _ => write_payload(&mut responses, &[u8::MAX]), + } + + let results = calls.map(|call| call.join().unwrap()); + let error_count = results.iter().filter(|result| result.is_err()).count(); + assert_eq!(error_count, if payload_kind == 1 { 1 } else { 2 }); + wait_for_failure + .recv_timeout(Duration::from_secs(1)) + .expect("failure callback was not invoked"); + assert_eq!(failures.load(Ordering::SeqCst), 1); + } + } + + #[test] + fn local_socket_eof_and_shutdown_wake_pending_calls() { + for close_peer in [false, true] { + let (channel, shutdown, _responses, mut requests, peer) = activate_local(|| {}); + let caller = thread::spawn(move || channel.call(request(1))); + read_request(&mut requests); + if close_peer { + drop(peer); + } else { + shutdown.shutdown().unwrap(); + } + assert!(caller.join().unwrap().is_err()); + } + } + + #[test] + fn ready_ack_uses_absolute_setup_deadline() { + let (local_stream, _peer) = UnixStream::pair().unwrap(); + let (ring, _) = ring_pair(); + let local = UnixStreamLocalSetupChannel { + stream: local_stream, + setup_deadline: Some(Instant::now() + Duration::from_millis(30)), + negotiated: true, + }; + let Err(error) = local.into_active(ring, || {}) else { + panic!("activation unexpectedly succeeded"); + }; + assert!(matches!( + error.kind(), + ErrorKind::WouldBlock | ErrorKind::TimedOut + )); + } + + #[test] + fn initial_connect_uses_absolute_setup_deadline() { + let (path, listener, _queued) = saturated_listener(); + let deadline = Instant::now() + Duration::from_millis(50); + let (result_sender, result_receiver) = std::sync::mpsc::sync_channel(0); + let connector = thread::spawn(move || { + result_sender + .send(UnixStreamLocalSetupChannel::connect_with_setup_deadline( + path.as_path(), + deadline, + )) + .unwrap(); + }); + + let result = match result_receiver.recv_timeout(Duration::from_secs(1)) { + Ok(result) => result, + Err(error) => { + // Release a queue slot so a regressed blocking connect can + // finish instead of leaving the test process stuck. + listener.accept().unwrap(); + let _ = result_receiver.recv_timeout(Duration::from_secs(1)); + connector.join().unwrap(); + panic!("connect did not honor its setup deadline: {error}"); + } + }; + connector.join().unwrap(); + let Err(error) = result else { + panic!("connect unexpectedly succeeded while the listen queue was full"); + }; + assert_eq!(error.kind(), ErrorKind::TimedOut); + } + + #[test] + fn initial_connect_retries_a_full_queue_and_restores_blocking_mode() { + let (path, listener, _queued) = saturated_listener(); + let deadline = Instant::now() + Duration::from_secs(2); + let connector = thread::spawn(move || { + UnixStreamLocalSetupChannel::connect_with_setup_deadline(path.as_path(), deadline) + }); + thread::sleep(Duration::from_millis(30)); + let _accepted = listener.accept().unwrap(); + + let channel = connector.join().unwrap().unwrap(); + assert!( + !fcntl_getfl(&channel.stream) + .unwrap() + .contains(OFlags::NONBLOCK) + ); + assert_eq!(channel.setup_deadline, Some(deadline)); + } + + #[test] + fn expired_setup_deadline_prevents_connect() { + let path = TestSocketPath::new(); + let Err(error) = UnixStreamLocalSetupChannel::connect_with_setup_deadline( + path.as_path(), + Instant::now(), + ) else { + panic!("connect unexpectedly accepted an expired setup deadline"); + }; + assert_eq!(error.kind(), ErrorKind::TimedOut); + } + + #[test] + fn handshake_reads_use_absolute_setup_deadlines() { + let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); + let mut local = UnixStreamLocalSetupChannel { + stream: local_stream, + setup_deadline: Some(Instant::now() + Duration::from_millis(50)), + negotiated: false, + }; + let local_reader = thread::spawn(move || local.recv_handshake_response().unwrap_err()); + host_stream.write_all(&8u32.to_le_bytes()).unwrap(); + for _ in 0..8 { + thread::sleep(Duration::from_millis(20)); + if host_stream.write_all(&[0]).is_err() { + break; + } + } + let error = local_reader.join().unwrap(); + assert!( + matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut), + "unexpected local timeout error: {error:?}" + ); + } + + #[test] + fn completed_call_wins_over_later_failure_and_failure_wins_before_completion() { + let pending = PendingCalls::new(); + let completed = pending.register(RequestId(1)).unwrap(); + pending.complete(response(RequestId(1))).unwrap(); + pending.record_failure(Arc::new(Error::new( + ErrorKind::ConnectionAborted, + "test failure", + ))); + assert_eq!(completed.wait().unwrap().request_id, RequestId(1)); + + let pending = PendingCalls::new(); + let failed = pending.register(RequestId(2)).unwrap(); + pending.record_failure(Arc::new(Error::new( + ErrorKind::ConnectionAborted, + "test failure", + ))); + assert!(pending.complete(response(RequestId(2))).is_err()); + assert_eq!( + failed.wait().unwrap_err().kind(), + ErrorKind::ConnectionAborted + ); + } + + #[test] + fn failure_recording_waits_for_in_progress_publication() { + let pending = Arc::new(PendingCalls::new()); + let pending_call = pending.register(RequestId(1)).unwrap(); + let publication_state = Arc::new(AtomicUsize::new(0)); + let (publication_started, wait_for_publication) = std::sync::mpsc::sync_channel(0); + let (release_publication, publication_released) = std::sync::mpsc::sync_channel(0); + let publisher_pending = Arc::clone(&pending); + let publisher_state = Arc::clone(&publication_state); + let publisher = thread::spawn(move || { + publisher_pending + .run_if_live(|| { + publisher_state.store(1, Ordering::Release); + publication_started.send(()).unwrap(); + publication_released.recv().unwrap(); + publisher_state.store(2, Ordering::Release); + Ok(()) + }) + .unwrap(); + }); + wait_for_publication.recv().unwrap(); + + let (failure_started, wait_for_failure) = std::sync::mpsc::sync_channel(0); + let (failure_recorded, wait_for_recording) = std::sync::mpsc::sync_channel(0); + let failure_pending = Arc::clone(&pending); + let failure_state = Arc::clone(&publication_state); + let failure = thread::spawn(move || { + failure_started.send(()).unwrap(); + failure_pending.record_failure(Arc::new(Error::new( + ErrorKind::ConnectionAborted, + "test failure", + ))); + assert_eq!(failure_state.load(Ordering::Acquire), 2); + failure_recorded.send(()).unwrap(); + }); + wait_for_failure.recv().unwrap(); + assert!(matches!( + wait_for_recording.recv_timeout(Duration::from_millis(20)), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) + )); + + release_publication.send(()).unwrap(); + publisher.join().unwrap(); + wait_for_recording + .recv_timeout(Duration::from_secs(1)) + .unwrap(); + failure.join().unwrap(); + assert_eq!( + pending_call.wait().unwrap_err().kind(), + ErrorKind::ConnectionAborted + ); + } + + #[test] + fn duplicate_pending_registration_preserves_original() { + let pending = PendingCalls::new(); + let original = pending.register(RequestId(1)).unwrap(); + let Err(error) = pending.register(RequestId(1)) else { + panic!("duplicate registration unexpectedly succeeded"); + }; + assert_eq!(error.kind(), ErrorKind::InvalidData); + pending.complete(response(RequestId(1))).unwrap(); + assert_eq!(original.wait().unwrap().request_id, RequestId(1)); + } +} diff --git a/litebox_broker_transport_linux_userland/src/unix_socket/mod.rs b/litebox_broker_transport_linux_userland/src/unix_socket/mod.rs new file mode 100644 index 0000000000..43407e1409 --- /dev/null +++ b/litebox_broker_transport_linux_userland/src/unix_socket/mod.rs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Unix-domain-socket broker endpoints for hosted userland deployments. +//! +//! Both sides of one association live here: the local (guest-side) endpoints a +//! runner activates and the host (broker-side) endpoints the broker activates. +//! Keeping them in one module lets their shared, security-sensitive setup +//! framing stay private to this crate. +//! +//! Setup negotiates the association over the Unix stream and transfers the +//! memfds backing the shared buffers and the control ring. After setup, the +//! authenticated socket is retained only for liveness and fail-closed shutdown: +//! active requests, responses, and notifications use the shared control rings. + +mod host; +mod local; + +pub use host::{ + UnixControlRingHostNotificationChannel, UnixControlRingHostRequestSource, + UnixControlRingHostResponseSink, UnixControlRingHostShutdown, UnixStreamHostSetupChannel, + validate_peer_process, +}; +pub use local::{ + MAX_PENDING_CALLS, UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, + UnixControlRingLocalShutdown, UnixStreamLocalSetupChannel, +}; diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index def0db4077..ec27699bea 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -8,7 +8,8 @@ clap = { version = "4.5.33", features = ["derive"] } litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0", features = ["linux-userland"] } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } +litebox_broker_transport_linux_userland = { path = "../litebox_broker_transport_linux_userland", version = "0.1.0" } tempfile = { version = "3", default-features = false } [[bin]] diff --git a/litebox_broker_userland/src/lib.rs b/litebox_broker_userland/src/lib.rs index aad4690c33..ba442723f6 100644 --- a/litebox_broker_userland/src/lib.rs +++ b/litebox_broker_userland/src/lib.rs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Linux-userland broker host deployment support. +//! Support for the Linux-userland broker process. //! -//! The broker host binary in this crate owns process, socket, and thread -//! policy. This library holds the deployment pieces that are useful outside -//! `main`, so integration tests can drive the same code the binary runs. +//! The broker executable composes `litebox_broker_core` and +//! `litebox_broker_host` with the Linux host endpoints from +//! `litebox_broker_transport_linux_userland`. It owns runner process lifecycle, +//! socket setup, and worker threads. This library exposes components shared by +//! the executable and its integration tests, currently readiness publication. pub mod readiness; diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index a9c36445c6..f2fa3dcce8 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -17,14 +17,13 @@ use std::time::{Duration, Instant}; use clap::Parser; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{BrokerHostAssociation, ConnectionTermination, setup_connection}; -use litebox_broker_protocol::channel::HostReceive; use litebox_broker_protocol::message::BrokerRequest; -use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, SharedMemory, -}; +use litebox_broker_protocol::shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}; +use litebox_broker_transport::channel::HostReceive; use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; -use litebox_broker_transport::shared_memory::MemfdSharedMemory; -use litebox_broker_transport::unix_socket::{ +use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory}; +use litebox_broker_transport_linux_userland::memfd::MemfdSharedMemory; +use litebox_broker_transport_linux_userland::unix_socket::{ UnixControlRingHostNotificationChannel, UnixControlRingHostRequestSource, UnixControlRingHostResponseSink, UnixControlRingHostShutdown, UnixStreamHostSetupChannel, validate_peer_process, @@ -443,9 +442,9 @@ fn accept_runner_stream( mod tests { use super::*; use litebox_broker_protocol::BROKER_PROTOCOL_VERSION; - use litebox_broker_protocol::channel::{HostSetupChannel, LocalSetupChannel}; use litebox_broker_protocol::message::BrokerHandshakeResponse; - use litebox_broker_transport::unix_socket::{ + use litebox_broker_transport::channel::{HostSetupChannel, LocalSetupChannel}; + use litebox_broker_transport_linux_userland::unix_socket::{ UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, UnixControlRingLocalShutdown, UnixStreamLocalSetupChannel, }; @@ -499,7 +498,7 @@ mod tests { /// A notification channel that accepts every send and keeps nothing. struct DiscardingChannel; - impl litebox_broker_protocol::channel::HostNotificationChannel for DiscardingChannel { + impl litebox_broker_transport::channel::HostNotificationChannel for DiscardingChannel { type Error = IoError; fn send_notification( @@ -746,7 +745,7 @@ mod tests { // never started has to fail the test rather than hang it. let (notified, notifications_seen) = sync_channel(1); let receiver = std::thread::spawn(move || { - use litebox_broker_protocol::channel::LocalNotificationChannel; + use litebox_broker_transport::channel::LocalNotificationChannel; let notification = notifications.recv_notification().unwrap(); notified.send(notification).unwrap(); diff --git a/litebox_broker_userland/src/readiness.rs b/litebox_broker_userland/src/readiness.rs index b567a2d7de..0a01033381 100644 --- a/litebox_broker_userland/src/readiness.rs +++ b/litebox_broker_userland/src/readiness.rs @@ -15,8 +15,8 @@ use litebox_broker_host::readiness::{ PublishOutcome, ReadinessPublishError, ReadinessPublisher, publish_readiness, }; use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::HostNotificationChannel; use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_transport::channel::HostNotificationChannel; /// Readiness publication state plus the wake primitive its publisher parks on. /// diff --git a/litebox_broker_userland/tests/notification_runtime.rs b/litebox_broker_userland/tests/notification_runtime.rs index 4dba303632..567b9b7311 100644 --- a/litebox_broker_userland/tests/notification_runtime.rs +++ b/litebox_broker_userland/tests/notification_runtime.rs @@ -11,20 +11,21 @@ use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_host::{ConnectionTermination, setup_connection}; use litebox_broker_local::{BrokerLocal, BrokerNotifications}; use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::channel::{HostNotificationChannel, HostReceive}; use litebox_broker_protocol::message::{BrokerNotification, ReadinessNotification}; use litebox_broker_protocol::readiness::ReadinessFlags; -use litebox_broker_protocol::shared_memory::{ - SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferPool, -}; +use litebox_broker_protocol::shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}; +use litebox_broker_transport::channel::{HostNotificationChannel, HostReceive}; use litebox_broker_transport::control_ring::{ CONTROL_RING_MEMORY_SIZE, CONTROL_RING_NOTIFICATION_SLOT_COUNT, ControlRing, }; -use litebox_broker_transport::shared_memory::MemfdSharedMemory; -use litebox_broker_transport::unix_socket::{ - UnixControlRingHostNotificationChannel, UnixControlRingHostShutdown, +use litebox_broker_transport::shared_memory::SharedBufferPool; +use litebox_broker_transport_linux_userland::memfd::MemfdSharedMemory; +use litebox_broker_transport_linux_userland::unix_socket::{ + UnixControlRingHostNotificationChannel, UnixControlRingHostShutdown, UnixStreamHostSetupChannel, +}; +use litebox_broker_transport_linux_userland::unix_socket::{ UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, - UnixStreamHostSetupChannel, UnixStreamLocalSetupChannel, + UnixStreamLocalSetupChannel, }; use litebox_broker_userland::readiness::ReadinessPublisherRuntime; diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index f034a2a9cb..944c2c5943 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -10,11 +10,11 @@ use std::time::{Duration, Instant}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::readiness::ReadinessFlags; -use litebox_broker_protocol::shared_memory::{ +use litebox_broker_protocol::shared_buffer::{ SHARED_BUFFER_POOL_SIZE, SharedBufferDescriptor, SharedBufferSlotIndex, }; use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; -use litebox_broker_transport::unix_socket::UnixStreamLocalSetupChannel; +use litebox_broker_transport_linux_userland::unix_socket::UnixStreamLocalSetupChannel; const RUNNER_ARGUMENT: &str = "broker-userland-test-runner"; diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 6323152f15..9c295267cd 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -422,13 +422,19 @@ impl LinuxUserland { clippy::missing_panics_doc, reason = "the seccomp filter rules are hardcoded and not expected to fail" )] - pub fn enable_seccomp_filter() { + /// Installs the runner seccomp filter. + /// + /// Broker transport exceptions are restricted to the supplied descriptors. + pub fn enable_seccomp_filter( + positional_io_fds: &[std::os::fd::RawFd], + shutdown_fds: &[std::os::fd::RawFd], + ) { use seccompiler::{ BpfProgram, SeccompAction, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, SeccompFilter, SeccompRule, }; - let rules = vec![ + let mut rules = vec![ // TUN and terminal (libc::SYS_read, vec![]), (libc::SYS_write, vec![]), @@ -515,6 +521,63 @@ impl LinuxUserland { ), (libc::SYS_close, vec![]), ]; + if !positional_io_fds.is_empty() { + // Broker shared memory uses positional descriptor I/O. + let fd_rules = || { + positional_io_fds + .iter() + .map(|fd| { + SeccompRule::new(vec![ + SeccompCondition::new( + 0, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + u64::from( + u32::try_from(*fd) + .expect("positional I/O descriptor must be valid"), + ), + ) + .unwrap(), + ]) + .unwrap() + }) + .collect() + }; + rules.push((libc::SYS_pread64, fd_rules())); + rules.push((libc::SYS_pwrite64, fd_rules())); + } + if !shutdown_fds.is_empty() { + // Association failure shuts down the control socket in both + // directions to interrupt local and peer liveness waits. + let shutdown_rules = shutdown_fds + .iter() + .map(|fd| { + SeccompRule::new(vec![ + SeccompCondition::new( + 0, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + u64::from( + u32::try_from(*fd).expect("shutdown descriptor must be valid"), + ), + ) + .unwrap(), + SeccompCondition::new( + 1, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + u64::from( + u32::try_from(libc::SHUT_RDWR) + .expect("SHUT_RDWR must be non-negative"), + ), + ) + .unwrap(), + ]) + .unwrap() + }) + .collect(); + rules.push((libc::SYS_shutdown, shutdown_rules)); + } let rule_map: std::collections::BTreeMap> = rules.into_iter().collect(); let filter = SeccompFilter::new( @@ -2446,6 +2509,9 @@ impl litebox::mm::linux::VmemPageFaultHandler for LinuxUserland { #[cfg(test)] mod tests { use core::sync::atomic::AtomicU32; + use std::net::Shutdown; + use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + use std::os::unix::net::UnixStream; use std::thread::sleep; use litebox::{fs::OFlags, platform::RawMutex}; @@ -2490,8 +2556,67 @@ mod tests { #[test] fn test_seccomp_filter() { + fn test_memfd(name: &std::ffi::CStr) -> OwnedFd { + // SAFETY: `name` is a valid C string and the returned descriptor is + // transferred immediately into `OwnedFd`. + let fd = unsafe { libc::memfd_create(name.as_ptr(), libc::MFD_CLOEXEC) }; + assert!(fd >= 0); + // SAFETY: `fd` was just returned as an owned descriptor. + unsafe { OwnedFd::from_raw_fd(fd) } + } + let _platform: &LinuxUserland = LinuxUserland::new(None); - LinuxUserland::enable_seccomp_filter(); + let allowed = test_memfd(c"seccomp-allowed-positional-io"); + let denied = test_memfd(c"seccomp-denied-positional-io"); + let (allowed_shutdown, _allowed_peer) = UnixStream::pair().unwrap(); + let (denied_shutdown, _denied_peer) = UnixStream::pair().unwrap(); + LinuxUserland::enable_seccomp_filter( + &[allowed.as_raw_fd()], + &[allowed_shutdown.as_raw_fd()], + ); + + let written = [7_u8]; + // SAFETY: The buffers are valid for their lengths, and both descriptors + // remain open for the calls. + assert_eq!( + unsafe { + libc::pwrite( + allowed.as_raw_fd(), + written.as_ptr().cast(), + written.len(), + 0, + ) + }, + 1 + ); + let mut read = [0_u8]; + // SAFETY: See the `pwrite` call above. + assert_eq!( + unsafe { libc::pread(allowed.as_raw_fd(), read.as_mut_ptr().cast(), read.len(), 0,) }, + 1 + ); + assert_eq!(read, written); + // SAFETY: See the allowed `pwrite` call above. + assert_eq!( + unsafe { + libc::pwrite( + denied.as_raw_fd(), + written.as_ptr().cast(), + written.len(), + 0, + ) + }, + -1 + ); + assert_eq!( + std::io::Error::last_os_error().raw_os_error(), + Some(libc::EINVAL) + ); + let error = allowed_shutdown.shutdown(Shutdown::Write).unwrap_err(); + assert_eq!(error.raw_os_error(), Some(libc::EINVAL)); + allowed_shutdown.shutdown(Shutdown::Both).unwrap(); + let error = denied_shutdown.shutdown(Shutdown::Both).unwrap_err(); + assert_eq!(error.raw_os_error(), Some(libc::EINVAL)); let pathname = c"/tmp/test_seccomp"; let mkdir_res = unsafe { diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index da1d018eb0..bf1119f748 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -10,7 +10,8 @@ libc = { version = "0.2.169", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } -litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport", features = ["linux-userland"] } +litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } +litebox_broker_transport_linux_userland = { version = "0.1.0", path = "../litebox_broker_transport_linux_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_linux = { version = "0.1.0", path = "../litebox_shim_linux" } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 35d5370f47..e89805a811 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -2,6 +2,7 @@ // Licensed under the MIT license. use std::{ + os::fd::{AsFd, AsRawFd, RawFd}, path::Path, sync::{ Arc, Mutex, @@ -13,9 +14,9 @@ use std::{ use anyhow::{Context as _, Result}; use litebox_broker_local::{BrokerLocal, BrokerNotifications}; use litebox_broker_protocol::message::BrokerNotification; -use litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE; +use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport::control_ring::{CONTROL_RING_MEMORY_SIZE, ControlRing}; -use litebox_broker_transport::unix_socket::{ +use litebox_broker_transport_linux_userland::unix_socket::{ UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, UnixControlRingLocalShutdown, UnixStreamLocalSetupChannel, }; @@ -23,13 +24,15 @@ use litebox_broker_transport::unix_socket::{ const SETUP_TIMEOUT: Duration = Duration::from_secs(5); const RETRY_DELAY: Duration = Duration::from_millis(20); -pub(crate) fn connect( - control_socket_path: &Path, -) -> Result<( - BrokerLocal, - BrokerNotifications, - Arc, -)> { +pub(crate) struct BrokerConnection { + pub(crate) local: BrokerLocal, + pub(crate) notifications: BrokerNotifications, + pub(crate) coordinator: Arc, + pub(crate) positional_io_fds: [RawFd; 2], + pub(crate) shutdown_fd: RawFd, +} + +pub(crate) fn connect(control_socket_path: &Path) -> Result { let setup_deadline = Instant::now() + SETUP_TIMEOUT; let setup_channel = connect_with_retry( control_socket_path, @@ -44,31 +47,45 @@ pub(crate) fn connect( ) })?; let association_coordinator = Arc::new(BrokerAssociationFailureCoordinator::new()); - let (local, notification_channel) = BrokerLocal::negotiate(setup_channel, |mut setup| { - let shared_memory = setup.receive_memfd(SHARED_BUFFER_POOL_SIZE, Some(setup_deadline))?; - let control_memory = setup.receive_memfd(CONTROL_RING_MEMORY_SIZE, Some(setup_deadline))?; - let control_ring = ControlRing::new(control_memory).map_err(|error| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("invalid broker control ring: {error:?}"), - ) - })?; - let weak_association_coordinator = Arc::downgrade(&association_coordinator); - let (call_channel, notification_channel, association_shutdown) = - setup.into_active(control_ring, move || { - if let Some(association_coordinator) = weak_association_coordinator.upgrade() { - association_coordinator.report_failure(); - } + let (local, (notification_channel, positional_io_fds, shutdown_fd)) = + BrokerLocal::negotiate(setup_channel, |mut setup| { + let shared_memory = + setup.receive_memfd(SHARED_BUFFER_POOL_SIZE, Some(setup_deadline))?; + let control_memory = + setup.receive_memfd(CONTROL_RING_MEMORY_SIZE, Some(setup_deadline))?; + let positional_io_fds = [ + shared_memory.as_fd().as_raw_fd(), + control_memory.as_fd().as_raw_fd(), + ]; + let control_ring = ControlRing::new(control_memory).map_err(|error| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid broker control ring: {error:?}"), + ) })?; - association_coordinator.install_shutdown(association_shutdown)?; - Ok((call_channel, Arc::new(shared_memory), notification_channel)) - }) - .context("broker negotiation failed")?; - Ok(( + let weak_association_coordinator = Arc::downgrade(&association_coordinator); + let (call_channel, notification_channel, association_shutdown) = + setup.into_active(control_ring, move || { + if let Some(association_coordinator) = weak_association_coordinator.upgrade() { + association_coordinator.report_failure(); + } + })?; + let shutdown_fd = association_shutdown.as_fd().as_raw_fd(); + association_coordinator.install_shutdown(association_shutdown)?; + Ok(( + call_channel, + Arc::new(shared_memory), + (notification_channel, positional_io_fds, shutdown_fd), + )) + }) + .context("broker negotiation failed")?; + Ok(BrokerConnection { local, - BrokerNotifications::new(notification_channel), - association_coordinator, - )) + notifications: BrokerNotifications::new(notification_channel), + coordinator: association_coordinator, + positional_io_fds, + shutdown_fd, + }) } pub(crate) fn start_notification_receiver( @@ -195,17 +212,19 @@ fn connect_with_retry( mod tests { use super::*; use litebox_broker_protocol::ObjectHandle; - use litebox_broker_protocol::channel::{ - HostNotificationChannel, HostReceive, HostSetupChannel, LocalSetupChannel, - }; use litebox_broker_protocol::message::{BrokerNotification, ReadinessNotification}; use litebox_broker_protocol::readiness::ReadinessFlags; - use litebox_broker_transport::shared_memory::MemfdSharedMemory; - use litebox_broker_transport::unix_socket::{ + use litebox_broker_transport::channel::{ + HostNotificationChannel, HostReceive, HostSetupChannel, LocalSetupChannel, + }; + use litebox_broker_transport_linux_userland::memfd::MemfdSharedMemory; + use litebox_broker_transport_linux_userland::unix_socket::{ UnixControlRingHostNotificationChannel, UnixControlRingHostRequestSource, - UnixControlRingHostResponseSink, UnixControlRingHostShutdown, + UnixControlRingHostResponseSink, UnixControlRingHostShutdown, UnixStreamHostSetupChannel, + }; + use litebox_broker_transport_linux_userland::unix_socket::{ UnixControlRingLocalCallChannel, UnixControlRingLocalNotificationChannel, - UnixControlRingLocalShutdown, UnixStreamHostSetupChannel, UnixStreamLocalSetupChannel, + UnixControlRingLocalShutdown, UnixStreamLocalSetupChannel, }; use std::io::ErrorKind; use std::os::fd::AsFd; diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 37bd14bf94..a985c1353b 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -217,9 +217,18 @@ pub fn run(cli_args: CliArgs) -> Result<()> { platform.register_cow_region(file.data, file.abs_path); } + let mut broker_positional_io_fds = Vec::new(); + let mut broker_shutdown_fds = Vec::new(); let shim_builder = if let Some(broker_connection) = broker_connection { - let (broker_local, broker_notifications, broker_association_coordinator) = - broker_connection; + let broker::BrokerConnection { + local: broker_local, + notifications: broker_notifications, + coordinator: broker_association_coordinator, + positional_io_fds, + shutdown_fd, + } = broker_connection; + broker_positional_io_fds.extend(positional_io_fds); + broker_shutdown_fds.push(shutdown_fd); let litebox = litebox::LiteBox::new_with_broker_local(platform, broker_local); broker_association_coordinator.install_dispatch(litebox.broker_failure_dispatcher()); broker::start_notification_receiver( @@ -401,7 +410,10 @@ pub fn run(cli_args: CliArgs) -> Result<()> { }; #[cfg(target_arch = "x86_64")] - litebox_platform_linux_userland::LinuxUserland::enable_seccomp_filter(); + litebox_platform_linux_userland::LinuxUserland::enable_seccomp_filter( + &broker_positional_io_fds, + &broker_shutdown_fds, + ); let program = shim.load_program(initial_file_system, task_params, prog_path, argv, envp)?; diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index a97414d62c..28fc01dd7a 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -362,17 +362,18 @@ fn spawn_test_broker( .accept() .expect("failed to accept broker local control connection"); let shared_memory = - litebox_broker_transport::shared_memory::MemfdSharedMemory::create( - litebox_broker_protocol::shared_memory::SHARED_BUFFER_POOL_SIZE, + litebox_broker_transport_linux_userland::memfd::MemfdSharedMemory::create( + litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE, ) .expect("failed to create broker test shared memory"); - let shared_buffers = litebox_broker_protocol::shared_memory::SharedBufferPool::new( - shared_memory, - litebox_broker_protocol::shared_memory::SHARED_BUFFER_LAYOUT, - ) - .expect("failed to attach broker test shared-buffer layout"); + let shared_buffers = + litebox_broker_transport::shared_memory::SharedBufferPool::new( + shared_memory, + litebox_broker_protocol::shared_buffer::SHARED_BUFFER_LAYOUT, + ) + .expect("failed to attach broker test shared-buffer layout"); let control_memory = - litebox_broker_transport::shared_memory::MemfdSharedMemory::create( + litebox_broker_transport_linux_userland::memfd::MemfdSharedMemory::create( litebox_broker_transport::control_ring::CONTROL_RING_MEMORY_SIZE, ) .expect("failed to create broker test control ring"); @@ -386,7 +387,7 @@ fn spawn_test_broker( .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test write timeout"); let mut channel = - litebox_broker_transport::unix_socket::UnixStreamHostSetupChannel::from_host_guaranteed( + litebox_broker_transport_linux_userland::unix_socket::UnixStreamHostSetupChannel::from_host_guaranteed( control_stream, std::time::Instant::now() + BROKER_HELPER_TIMEOUT, ); @@ -410,7 +411,7 @@ fn spawn_test_broker( .recv_request() .expect("failed to receive broker test request") { - litebox_broker_protocol::channel::HostReceive::Message(request) => { + litebox_broker_transport::channel::HostReceive::Message(request) => { if matches!( &request.operation, litebox_broker_protocol::message::BrokerOperation::CloseObject(_) @@ -423,10 +424,10 @@ fn spawn_test_broker( }) .expect("failed to execute broker test request"); } - litebox_broker_protocol::channel::HostReceive::PeerClosed => { + litebox_broker_transport::channel::HostReceive::PeerClosed => { break litebox_broker_host::ConnectionTermination::PeerClosed; } - litebox_broker_protocol::channel::HostReceive::ProtocolViolation => { + litebox_broker_transport::channel::HostReceive::ProtocolViolation => { break litebox_broker_host::ConnectionTermination::ProtocolViolation; } } From c48965b46a0d7b1d3398f480d6c17a1068a7a33c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sun, 26 Jul 2026 20:18:11 -0700 Subject: [PATCH 134/319] Define broker socket contracts (#1094) This PR defines portable, typed IPv4/TCP broker socket contracts and bounded wire codecs, including shared-buffer descriptors for send and receive. It separates ordinary network outcomes from broker failures and models connection and receive states without exposing platform ABI values or host descriptors. The broker host validates shared-buffer leases but continues to reject socket operations until socket authority is implemented. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b5a1a347-37a8-4246-8bbc-306590921475 --- litebox/src/broker/mod.rs | 8 +- litebox/src/event/counter.rs | 8 +- litebox/src/pipes.rs | 4 +- litebox_broker_host/src/lib.rs | 25 +- litebox_broker_local/src/event.rs | 3 +- litebox_broker_local/src/lib.rs | 2 + litebox_broker_local/src/pipe.rs | 3 +- litebox_broker_protocol/src/lib.rs | 1 + litebox_broker_protocol/src/message.rs | 52 +++ litebox_broker_protocol/src/readiness.rs | 20 ++ litebox_broker_protocol/src/socket.rs | 339 ++++++++++++++++++ litebox_broker_protocol/src/wire.rs | 389 ++++++++++++++++++++- litebox_broker_protocol/src/wire/event.rs | 18 +- litebox_broker_protocol/src/wire/pipe.rs | 12 +- litebox_broker_protocol/src/wire/socket.rs | 272 ++++++++++++++ 15 files changed, 1122 insertions(+), 34 deletions(-) create mode 100644 litebox_broker_protocol/src/socket.rs create mode 100644 litebox_broker_protocol/src/wire/socket.rs diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 72dd93b924..b8bb0603ec 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -291,10 +291,10 @@ where pub(crate) fn readiness_events(readiness: ReadinessFlags) -> Events { let mut events = Events::empty(); - events.set(Events::IN, readiness.0 & ReadinessFlags::READ.0 != 0); - events.set(Events::OUT, readiness.0 & ReadinessFlags::WRITE.0 != 0); - events.set(Events::HUP, readiness.0 & ReadinessFlags::HANGUP.0 != 0); - events.set(Events::ERR, readiness.0 & ReadinessFlags::ERROR.0 != 0); + events.set(Events::IN, readiness.contains(ReadinessFlags::READ)); + events.set(Events::OUT, readiness.contains(ReadinessFlags::WRITE)); + events.set(Events::HUP, readiness.contains(ReadinessFlags::HANGUP)); + events.set(Events::ERR, readiness.contains(ReadinessFlags::ERROR)); events } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index f16025454a..347f04853e 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -88,7 +88,7 @@ where ) -> Result> { self.pollee.wait(cx, nonblock, Events::IN, || { let response = self.consume(mode)?; - if response.readiness.0 & ReadinessFlags::WRITE.0 != 0 { + if response.readiness.contains(ReadinessFlags::WRITE) { self.pollee.notify_observers(Events::OUT); } Ok(response.value) @@ -107,7 +107,7 @@ where } self.pollee.wait(cx, nonblock, Events::OUT, || { let readiness = self.add(value)?; - if value != 0 && readiness.0 & ReadinessFlags::READ.0 != 0 { + if value != 0 && readiness.contains(ReadinessFlags::READ) { self.pollee.notify_observers(Events::IN); } Ok(core::mem::size_of::()) @@ -485,7 +485,9 @@ mod tests { BrokerOperation::CheckReadiness(_) => { BrokerResult::Readiness(ReadinessFlags::WRITE) } - request @ (BrokerOperation::Event(_) | BrokerOperation::Pipe(_)) => { + request @ (BrokerOperation::Event(_) + | BrokerOperation::Pipe(_) + | BrokerOperation::Socket(_)) => { panic!("unexpected broker request: {request:?}") } }; diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index 41bafef6a0..7cc30e21a5 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -1195,7 +1195,9 @@ mod tests { BrokerOperation::CheckReadiness(_) => { BrokerResult::Readiness(ReadinessFlags::default()) } - request @ (BrokerOperation::Pipe(_) | BrokerOperation::Event(_)) => { + request @ (BrokerOperation::Pipe(_) + | BrokerOperation::Event(_) + | BrokerOperation::Socket(_)) => { panic!("unexpected broker request: {request:?}") } }; diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index c746db0bf2..cab39262a3 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -28,7 +28,7 @@ use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{AddEventResponse, CreateEventResponse}; use litebox_broker_protocol::message::{ BrokerHandshakeResponse, BrokerOperation, BrokerRequest, BrokerResponse, BrokerResult, - EventRequest, EventResponse, PipeRequest, PipeResponse, + EventRequest, EventResponse, PipeRequest, PipeResponse, SocketRequest, }; use litebox_broker_protocol::pipe::{ CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE, ReadPipeResponse, WritePipeResponse, @@ -84,10 +84,18 @@ impl BrokerHostAssociation<'_, Memory> { let buffer_descriptor = match &operation { BrokerOperation::Pipe(PipeRequest::Read(request)) => Some(request.buffer), BrokerOperation::Pipe(PipeRequest::Write(request)) => Some(request.buffer), + BrokerOperation::Socket(SocketRequest::Send(request)) => Some(request.buffer), + BrokerOperation::Socket(SocketRequest::Receive(request)) => Some(request.buffer), BrokerOperation::CloseObject(_) | BrokerOperation::CheckReadiness(_) | BrokerOperation::Event(_) - | BrokerOperation::Pipe(PipeRequest::Create(_)) => None, + | BrokerOperation::Pipe(PipeRequest::Create(_)) + | BrokerOperation::Socket( + SocketRequest::Create(_) + | SocketRequest::Connect(_) + | SocketRequest::Shutdown(_) + | SocketRequest::Status(_), + ) => None, }; { @@ -300,6 +308,19 @@ fn handle_request( BrokerOperation::Pipe(request) => { handle_pipe_request(session, request, shared_buffers).map(BrokerResult::Pipe) } + // The socket protocol is defined before the broker implements it, so + // the operations decode and their shared-buffer leases are validated, + // but no socket object exists to act on one yet. + // + // `UnsupportedOperation` is deliberately in the local endpoint's fatal + // group alongside `MalformedRequest` and `ProtocolState`: it means the + // local sent an operation this broker never serves, which cannot happen + // unless the two sides disagree about the protocol. No local code + // constructs a socket request today, so this is unreachable; reporting + // a recoverable code instead would let a future wiring mistake look + // like an ordinary runtime failure rather than the contract violation + // it is. + BrokerOperation::Socket(_) => Err(RequestFailure::Respond(ErrorCode::UnsupportedOperation)), } } diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index b2b0d30c32..ca1f5e16ee 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -76,7 +76,8 @@ impl BrokerLocal { BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), response @ (BrokerResult::ObjectClosed | BrokerResult::Readiness(_) - | BrokerResult::Pipe(_)) => { + | BrokerResult::Pipe(_) + | BrokerResult::Socket(_)) => { panic!("broker returned unexpected event response: {response:?}"); } } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 636cd9d358..8fb951049a 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -180,6 +180,7 @@ impl BrokerLocal { }, result @ (BrokerResult::Event(_) | BrokerResult::Pipe(_) + | BrokerResult::Socket(_) | BrokerResult::ObjectClosed | BrokerResult::Readiness(_)) => Ok(result), } @@ -211,6 +212,7 @@ impl BrokerLocal { BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), response @ (BrokerResult::Event(_) | BrokerResult::Pipe(_) + | BrokerResult::Socket(_) | BrokerResult::Readiness(_)) => { panic!("broker returned unexpected close response: {response:?}"); } diff --git a/litebox_broker_local/src/pipe.rs b/litebox_broker_local/src/pipe.rs index 1b00071135..ac68055b4b 100644 --- a/litebox_broker_local/src/pipe.rs +++ b/litebox_broker_local/src/pipe.rs @@ -129,7 +129,8 @@ impl BrokerLocal { BrokerResult::Error(error) => Err(BrokerLocalError::Broker(error)), response @ (BrokerResult::ObjectClosed | BrokerResult::Readiness(_) - | BrokerResult::Event(_)) => { + | BrokerResult::Event(_) + | BrokerResult::Socket(_)) => { panic!("broker returned unexpected pipe response: {response:?}"); } } diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index e0a17ebd0b..95f2e2ebbc 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -22,6 +22,7 @@ pub mod message; pub mod pipe; pub mod readiness; pub mod shared_buffer; +pub mod socket; pub mod wire; /// Opaque broker object reference handle. diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index e7a59defa3..3d475fca3e 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -11,6 +11,11 @@ use crate::pipe::{ WritePipeResponse, }; use crate::readiness::ReadinessFlags; +use crate::socket::{ + ConnectSocketRequest, ConnectSocketResponse, CreateSocketRequest, CreateSocketResponse, + ReceiveSocketRequest, ReceiveSocketResponse, SendSocketRequest, SendSocketResponse, + ShutdownSocketRequest, SocketError, SocketStatusRequest, SocketStatusResponse, +}; use crate::{ObjectHandle, ProtocolVersion, RequestId}; /// Broker handshake request sent before the control channel is active. @@ -31,6 +36,8 @@ pub enum BrokerOperation { Event(EventRequest), /// Pipe object request family. Pipe(PipeRequest), + /// Socket object request family. + Socket(SocketRequest), } /// Request sent over an active broker control channel. @@ -88,6 +95,23 @@ pub enum PipeRequest { Write(WritePipeRequest), } +/// Broker-owned socket object request. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SocketRequest { + /// Create a broker-owned socket. + Create(CreateSocketRequest), + /// Connect a socket to a remote address. + Connect(ConnectSocketRequest), + /// Send bytes staged in shared memory. + Send(SendSocketRequest), + /// Receive bytes into shared memory. + Receive(ReceiveSocketRequest), + /// Shut down one or both directions. + Shutdown(ShutdownSocketRequest), + /// Read a socket's connection state. + Status(SocketStatusRequest), +} + /// Result returned for an active broker operation. #[derive(Clone, Debug, PartialEq, Eq)] pub enum BrokerResult { @@ -99,6 +123,8 @@ pub enum BrokerResult { Event(EventResponse), /// Pipe object response family. Pipe(PipeResponse), + /// Socket object response family. + Socket(SocketResponse), /// Operation failed with an ABI-neutral broker error. Error(ErrorCode), } @@ -134,6 +160,32 @@ pub enum PipeResponse { Write(WritePipeResponse), } +/// Broker-owned socket object response. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SocketResponse { + /// Create operation response. + Create(CreateSocketResponse), + /// Connect operation response. + Connect(ConnectSocketResponse), + /// Send operation response. + Send(SendSocketResponse), + /// Receive operation response. + Receive(ReceiveSocketResponse), + /// Shutdown operation completed. + Shutdown, + /// Status operation response. + Status(SocketStatusResponse), + /// A non-connect host network operation failed. + /// + /// Connect and status responses carry terminal failures in + /// [`SocketConnectionStatus`] so repeated status requests remain + /// idempotent. Broker and request-validation failures use + /// [`BrokerResult::Error`] instead. + /// + /// [`SocketConnectionStatus`]: crate::socket::SocketConnectionStatus + Failed(SocketError), +} + /// Broker-initiated asynchronous notification. /// /// Notifications are level-triggered snapshots and may be coalesced or diff --git a/litebox_broker_protocol/src/readiness.rs b/litebox_broker_protocol/src/readiness.rs index c552703e04..be00b1b60d 100644 --- a/litebox_broker_protocol/src/readiness.rs +++ b/litebox_broker_protocol/src/readiness.rs @@ -17,6 +17,12 @@ impl ReadinessFlags { pub const HANGUP: Self = Self(1 << 2); /// The object is in an error state. pub const ERROR: Self = Self(1 << 3); + + /// Returns whether every flag in `other` is set. + #[must_use] + pub const fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } } impl core::ops::BitOr for ReadinessFlags { @@ -26,3 +32,17 @@ impl core::ops::BitOr for ReadinessFlags { Self(self.0 | rhs.0) } } + +#[cfg(test)] +mod tests { + use super::ReadinessFlags; + + #[test] + fn contains_requires_every_requested_flag() { + let readiness = ReadinessFlags::READ | ReadinessFlags::WRITE; + assert!(readiness.contains(ReadinessFlags::READ)); + assert!(readiness.contains(ReadinessFlags::WRITE)); + assert!(readiness.contains(ReadinessFlags::READ | ReadinessFlags::WRITE)); + assert!(!readiness.contains(ReadinessFlags::ERROR)); + } +} diff --git a/litebox_broker_protocol/src/socket.rs b/litebox_broker_protocol/src/socket.rs new file mode 100644 index 0000000000..2ecc39944e --- /dev/null +++ b/litebox_broker_protocol/src/socket.rs @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Typed values for broker-mediated network sockets. +//! +//! These describe what a local endpoint may ask the broker to do with a socket, +//! never how the broker does it: no descriptor, poll registration, or platform +//! error appears here. Every value is a closed set rather than a passthrough +//! integer, so a local endpoint cannot name an address family, type, protocol, +//! or flag the broker has not agreed to support. + +use crate::ObjectHandle; +use crate::shared_buffer::SharedBufferDescriptor; +use thiserror::Error; + +/// Address family of a broker socket. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum AddressFamily { + /// IPv4. + Ipv4, +} + +/// Communication semantics of a broker socket. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum SocketType { + /// Reliable ordered byte stream. + Stream, +} + +/// IP protocol carried by a broker socket. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum IpProtocol { + /// TCP. + Tcp, +} + +/// Which directions of a socket to shut down. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ShutdownMode { + /// Further receives return end of stream. + Read, + /// The peer sees end of stream. + Write, + /// Both directions. + Both, +} + +/// IPv4 address in network byte order. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Ipv4Address(pub [u8; 4]); + +/// Transport port in host byte order. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Port(pub u16); + +/// IPv4 socket address. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SocketAddressV4 { + /// IPv4 address. + pub address: Ipv4Address, + /// Transport port. + pub port: Port, +} + +/// Flags for a send operation. +/// +/// Send and receive flags are separate types because their underlying flag sets +/// are disjoint: a receive-only flag such as [`ReceiveFlags::PEEK`] is +/// meaningless on a send, and one shared type would let a local endpoint name it +/// there. +/// +/// Like [`ReceiveFlags`] and unlike [`ReadinessFlags`], which preserves bits a +/// peer may not understand, this is bounded: flags are forwarded to a host +/// socket operation, so a bit the broker does not recognize must never reach +/// one. [`Self::SUPPORTED`] defines the bound as part of the ABI, and the broker +/// rejects anything outside it rather than masking it away, which would silently +/// perform an operation the caller did not ask for. +/// +/// No send flag is supported yet, so any nonzero value is rejected. +/// +/// [`ReadinessFlags`]: crate::readiness::ReadinessFlags +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SendFlags(pub u32); + +impl SendFlags { + /// No flags. + pub const NONE: Self = Self(0); + + /// Every send flag this protocol version defines. + pub const SUPPORTED: Self = Self(0); + + /// Returns whether any bit outside [`Self::SUPPORTED`] is set. + #[must_use] + pub const fn has_unsupported_bits(self) -> bool { + self.0 & !Self::SUPPORTED.0 != 0 + } +} + +/// Flags for a receive operation. +/// +/// See [`SendFlags`] for why the two directions are separate types and why both +/// are bounded rather than passthrough. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReceiveFlags(pub u32); + +impl ReceiveFlags { + /// No flags. + pub const NONE: Self = Self(0); + /// Return data without consuming it. + pub const PEEK: Self = Self(1 << 0); + + /// Every receive flag this protocol version defines. + pub const SUPPORTED: Self = Self(Self::PEEK.0); + + /// Returns whether any bit outside [`Self::SUPPORTED`] is set. + #[must_use] + pub const fn has_unsupported_bits(self) -> bool { + self.0 & !Self::SUPPORTED.0 != 0 + } + + /// Returns whether every flag in `other` is set. + #[must_use] + pub const fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } +} + +/// Reason a socket operation failed. +/// +/// This is a bounded restatement of the network failures a host stack reports, +/// kept separate from [`ErrorCode`] because those describe how the broker +/// handled a request, not what a remote peer or network did. Keeping them apart +/// also means adding a network failure never widens the error type every broker +/// operation can return. +/// +/// [`ErrorCode`]: crate::error::ErrorCode +#[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] +#[non_exhaustive] +pub enum SocketError { + /// The peer refused the connection. + #[error("connection refused")] + ConnectionRefused, + /// The connection was reset by the peer. + #[error("connection reset")] + ConnectionReset, + /// The connection was aborted before it completed. + #[error("connection aborted")] + ConnectionAborted, + /// No route to the network. + #[error("network unreachable")] + NetworkUnreachable, + /// No route to the host. + #[error("host unreachable")] + HostUnreachable, + /// The connection attempt timed out. + #[error("connection timed out")] + TimedOut, + /// The address is already in use. + #[error("address already in use")] + AddressInUse, + /// The address is not available on this host. + #[error("address not available")] + AddressNotAvailable, + /// The policy engine refused the destination. + #[error("socket policy denied the operation")] + PolicyDenied, + /// The host stack failed in a way this protocol does not distinguish. + #[error("other socket error")] + Other, +} + +impl SocketError { + /// Raw socket error values are part of the broker wire ABI. + /// + /// Value `0` is unassigned so a zero-filled value never represents a + /// concrete network failure. + pub const fn from_raw(raw: u8) -> Option { + match raw { + 1 => Some(Self::ConnectionRefused), + 2 => Some(Self::ConnectionReset), + 3 => Some(Self::ConnectionAborted), + 4 => Some(Self::NetworkUnreachable), + 5 => Some(Self::HostUnreachable), + 6 => Some(Self::TimedOut), + 7 => Some(Self::AddressInUse), + 8 => Some(Self::AddressNotAvailable), + 9 => Some(Self::PolicyDenied), + 10 => Some(Self::Other), + _ => None, + } + } + + /// Returns the raw broker wire ABI value. + pub const fn as_raw(self) -> u8 { + match self { + Self::ConnectionRefused => 1, + Self::ConnectionReset => 2, + Self::ConnectionAborted => 3, + Self::NetworkUnreachable => 4, + Self::HostUnreachable => 5, + Self::TimedOut => 6, + Self::AddressInUse => 7, + Self::AddressNotAvailable => 8, + Self::PolicyDenied => 9, + Self::Other => 10, + } + } +} + +/// Request to create a broker-owned socket. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CreateSocketRequest { + /// Address family. + pub address_family: AddressFamily, + /// Communication semantics. + pub socket_type: SocketType, + /// Transport protocol. + pub protocol: IpProtocol, +} + +/// Response to a socket create request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CreateSocketResponse { + /// Handle naming the new socket. + pub handle: ObjectHandle, +} + +/// Request to connect a socket to a remote address. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ConnectSocketRequest { + /// Socket handle. + pub handle: ObjectHandle, + /// Remote address to connect to. + pub address: SocketAddressV4, +} + +/// Response to a socket connect request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ConnectSocketResponse { + /// Connection state after the nonblocking attempt. + pub status: SocketConnectionStatus, +} + +/// Broker-authoritative socket connection state. +/// +/// The broker only performs non-blocking operations, so a connect that cannot +/// complete immediately reports [`Self::Connecting`] rather than waiting. The +/// caller waits for write readiness and then reads the authoritative state with +/// a status request. +/// +/// Connected and failed states are terminal and idempotent: repeated status +/// requests return the same state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum SocketConnectionStatus { + /// No connection attempt has started. + Unconnected, + /// The connection is still being established. + Connecting, + /// The connection is established. + Connected, + /// The connection attempt failed. + Failed(SocketError), +} + +/// Request to send bytes staged in shared memory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SendSocketRequest { + /// Socket handle. + pub handle: ObjectHandle, + /// Leased shared-buffer region containing the staged bytes. + pub buffer: SharedBufferDescriptor, + /// Send flags. + pub flags: SendFlags, +} + +/// Response describing a completed send. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SendSocketResponse { + /// Number of bytes accepted by the socket. + pub sent: u32, +} + +/// Request to receive bytes into shared memory. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ReceiveSocketRequest { + /// Socket handle. + pub handle: ObjectHandle, + /// Leased shared-buffer region to receive the bytes. + pub buffer: SharedBufferDescriptor, + /// Receive flags. + pub flags: ReceiveFlags, +} + +/// Response to a socket receive request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ReceiveSocketResponse { + /// Number of bytes placed in the receive region. + /// + /// Zero is the successful result of a zero-length request. A socket with + /// nothing to read yet reports [`ErrorCode::WouldBlock`] instead. + /// + /// [`ErrorCode::WouldBlock`]: crate::error::ErrorCode::WouldBlock + Received(u32), + /// The socket's receive direction reached end of stream. + EndOfStream, +} + +/// Request to shut down one or both directions of a socket. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ShutdownSocketRequest { + /// Socket handle. + pub handle: ObjectHandle, + /// Directions to shut down. + pub mode: ShutdownMode, +} + +/// Request for a socket's connection state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SocketStatusRequest { + /// Socket handle. + pub handle: ObjectHandle, +} + +/// Response describing a socket's broker-authoritative connection state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SocketStatusResponse { + /// Current connection state. + pub status: SocketConnectionStatus, +} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index f48390ee92..72c061c1f5 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -30,12 +30,14 @@ use primitive::{Decoder, Encoder}; mod event; mod pipe; mod primitive; +mod socket; const REQUEST_TAG_NEGOTIATE: u8 = 0; const REQUEST_TAG_EVENT: u8 = 1; const REQUEST_TAG_CLOSE_OBJECT: u8 = 2; const REQUEST_TAG_PIPE: u8 = 3; const REQUEST_TAG_CHECK_READINESS: u8 = 4; +const REQUEST_TAG_SOCKET: u8 = 5; const RESPONSE_TAG_NEGOTIATED: u8 = 0; const RESPONSE_TAG_EVENT: u8 = 1; @@ -45,11 +47,12 @@ const RESPONSE_TAG_OBJECT_CLOSED: u8 = 4; const RESPONSE_TAG_PIPE: u8 = 5; const RESPONSE_TAG_READINESS: u8 = 6; const RESPONSE_TAG_ERROR: u8 = 7; +const RESPONSE_TAG_SOCKET: u8 = 8; const NOTIFICATION_TAG_READINESS: u8 = 0; /// Maximum byte length of any encoded active request or response. -pub const MAX_ENCODED_ACTIVE_MESSAGE_SIZE: usize = 26; +pub const MAX_ENCODED_ACTIVE_MESSAGE_SIZE: usize = 30; /// Maximum byte length of any encoded broker notification. pub const MAX_ENCODED_NOTIFICATION_SIZE: usize = 13; @@ -92,7 +95,8 @@ pub fn decode_handshake_request(frame: &[u8]) -> Result { + | REQUEST_TAG_CHECK_READINESS + | REQUEST_TAG_SOCKET => { return Err(WireError::WrongMessagePhase); } _ => return Err(WireError::InvalidTag), @@ -132,6 +136,11 @@ pub fn encode_request(request: BrokerRequest) -> Vec { encoder.request_id(request_id); pipe::encode_pipe_request(&mut encoder, request); } + BrokerOperation::Socket(request) => { + encoder.u8(REQUEST_TAG_SOCKET); + encoder.request_id(request_id); + socket::encode_socket_request(&mut encoder, request); + } } encoder.finish() } @@ -145,7 +154,8 @@ pub fn decode_request(frame: &[u8]) -> Result { REQUEST_TAG_CLOSE_OBJECT | REQUEST_TAG_CHECK_READINESS | REQUEST_TAG_EVENT - | REQUEST_TAG_PIPE => {} + | REQUEST_TAG_PIPE + | REQUEST_TAG_SOCKET => {} _ => return Err(WireError::InvalidTag), } let request_id = decoder.request_id()?; @@ -154,6 +164,7 @@ pub fn decode_request(frame: &[u8]) -> Result { REQUEST_TAG_CHECK_READINESS => BrokerOperation::CheckReadiness(decoder.handle()?), REQUEST_TAG_EVENT => BrokerOperation::Event(event::decode_event_request(&mut decoder)?), REQUEST_TAG_PIPE => BrokerOperation::Pipe(pipe::decode_pipe_request(&mut decoder)?), + REQUEST_TAG_SOCKET => BrokerOperation::Socket(socket::decode_socket_request(&mut decoder)?), _ => unreachable!("active request tag was validated"), }; decoder.finish()?; @@ -202,7 +213,8 @@ pub fn decode_handshake_response(frame: &[u8]) -> Result { + | RESPONSE_TAG_ERROR + | RESPONSE_TAG_SOCKET => { return Err(WireError::WrongMessagePhase); } RESPONSE_TAG_VERSION_MISMATCH => BrokerHandshakeResponse::VersionMismatch { @@ -245,6 +257,11 @@ pub fn encode_response(response: BrokerResponse) -> Vec { encoder.request_id(request_id); pipe::encode_pipe_response(&mut encoder, response); } + BrokerResult::Socket(response) => { + encoder.u8(RESPONSE_TAG_SOCKET); + encoder.request_id(request_id); + socket::encode_socket_response(&mut encoder, response); + } BrokerResult::Error(error) => { encoder.u8(RESPONSE_TAG_ERROR); encoder.request_id(request_id); @@ -266,13 +283,15 @@ pub fn decode_response(frame: &[u8]) -> Result { | RESPONSE_TAG_OBJECT_CLOSED | RESPONSE_TAG_PIPE | RESPONSE_TAG_READINESS - | RESPONSE_TAG_ERROR => {} + | RESPONSE_TAG_ERROR + | RESPONSE_TAG_SOCKET => {} _ => return Err(WireError::InvalidTag), } let request_id = decoder.request_id()?; let result = match tag { RESPONSE_TAG_EVENT => BrokerResult::Event(event::decode_event_response(&mut decoder)?), RESPONSE_TAG_PIPE => BrokerResult::Pipe(pipe::decode_pipe_response(&mut decoder)?), + RESPONSE_TAG_SOCKET => BrokerResult::Socket(socket::decode_socket_response(&mut decoder)?), RESPONSE_TAG_ERROR => { let error = ErrorCode::from_raw(decoder.u16()?).ok_or(WireError::InvalidTag)?; BrokerResult::Error(error) @@ -323,12 +342,21 @@ mod tests { AddEventRequest, AddEventResponse, ConsumeEventRequest, CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, }; - use crate::message::{EventRequest, EventResponse, PipeRequest, PipeResponse}; + use crate::message::{ + EventRequest, EventResponse, PipeRequest, PipeResponse, SocketRequest, SocketResponse, + }; use crate::pipe::{ CreatePipeRequest, CreatePipeResponse, ReadPipeRequest, ReadPipeResponse, WritePipeRequest, WritePipeResponse, }; use crate::shared_buffer::{SharedBufferDescriptor, SharedBufferSlotIndex}; + use crate::socket::{ + AddressFamily, ConnectSocketRequest, ConnectSocketResponse, CreateSocketRequest, + CreateSocketResponse, IpProtocol, Ipv4Address, Port, ReceiveFlags, ReceiveSocketRequest, + ReceiveSocketResponse, SendFlags, SendSocketRequest, SendSocketResponse, ShutdownMode, + ShutdownSocketRequest, SocketAddressV4, SocketConnectionStatus, SocketError, + SocketStatusRequest, SocketStatusResponse, SocketType, + }; use crate::{ObjectHandle, ProtocolVersion, RequestId}; const TEST_REQUEST_ID: RequestId = RequestId(0x0102_0304_0506_0708); @@ -386,6 +414,47 @@ mod tests { length: 3, }, })), + BrokerOperation::Socket(SocketRequest::Create(CreateSocketRequest { + address_family: AddressFamily::Ipv4, + socket_type: SocketType::Stream, + protocol: IpProtocol::Tcp, + })), + BrokerOperation::Socket(SocketRequest::Connect(ConnectSocketRequest { + handle, + address: SocketAddressV4 { + address: Ipv4Address([203, 0, 113, 7]), + port: Port(443), + }, + })), + BrokerOperation::Socket(SocketRequest::Send(SendSocketRequest { + handle, + buffer: SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(15), + length: 3, + }, + flags: SendFlags::NONE, + })), + BrokerOperation::Socket(SocketRequest::Receive(ReceiveSocketRequest { + handle, + buffer: SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(15), + length: 3, + }, + flags: ReceiveFlags::PEEK, + })), + BrokerOperation::Socket(SocketRequest::Shutdown(ShutdownSocketRequest { + handle, + mode: ShutdownMode::Read, + })), + BrokerOperation::Socket(SocketRequest::Shutdown(ShutdownSocketRequest { + handle, + mode: ShutdownMode::Write, + })), + BrokerOperation::Socket(SocketRequest::Shutdown(ShutdownSocketRequest { + handle, + mode: ShutdownMode::Both, + })), + BrokerOperation::Socket(SocketRequest::Status(SocketStatusRequest { handle })), ]; let mut maximum_encoded_size = 0; @@ -398,10 +467,95 @@ mod tests { maximum_encoded_size = maximum_encoded_size.max(encoded.len()); assert!(encoded.len() <= MAX_ENCODED_ACTIVE_MESSAGE_SIZE); assert_eq!(decode_request(&encoded).unwrap(), request); + // Every active tag must be reported as a phase violation during + // the handshake, not as an unknown tag: only the former is turned + // into a clean protocol-violation shutdown by the transport. + assert_eq!( + decode_handshake_request(&encoded), + Err(WireError::WrongMessagePhase) + ); } assert_eq!(maximum_encoded_size, MAX_ENCODED_ACTIVE_MESSAGE_SIZE); } + #[test] + fn flag_bits_round_trip_unmasked() { + // The codec carries flags verbatim so the core can reject unsupported + // bits; masking them here would hide them from that check, and a + // dropped field would make an unsupported flag look like none at all. + let handle = ObjectHandle(13); + let buffer = SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(15), + length: 3, + }; + let unsupported = 0x8000_0001; + for operation in [ + BrokerOperation::Socket(SocketRequest::Send(SendSocketRequest { + handle, + buffer, + flags: SendFlags(unsupported), + })), + BrokerOperation::Socket(SocketRequest::Receive(ReceiveSocketRequest { + handle, + buffer, + flags: ReceiveFlags(unsupported), + })), + ] { + let request = BrokerRequest { + request_id: TEST_REQUEST_ID, + operation, + }; + assert_eq!( + decode_request(&encode_request(request.clone())).unwrap(), + request + ); + } + + assert!(SendFlags(unsupported).has_unsupported_bits()); + assert!(ReceiveFlags(unsupported).has_unsupported_bits()); + // No send flag exists yet, so every bit is unsupported there. + assert!(SendFlags(ReceiveFlags::PEEK.0).has_unsupported_bits()); + assert!(!SendFlags::NONE.has_unsupported_bits()); + assert!(!ReceiveFlags::PEEK.has_unsupported_bits()); + assert!(ReceiveFlags::PEEK.contains(ReceiveFlags::PEEK)); + assert!(!ReceiveFlags::NONE.contains(ReceiveFlags::PEEK)); + } + + #[test] + fn socket_error_codec_round_trips_all_variants() { + for error in [ + SocketError::ConnectionRefused, + SocketError::ConnectionReset, + SocketError::ConnectionAborted, + SocketError::NetworkUnreachable, + SocketError::HostUnreachable, + SocketError::TimedOut, + SocketError::AddressInUse, + SocketError::AddressNotAvailable, + SocketError::PolicyDenied, + SocketError::Other, + ] { + for socket_response in [ + SocketResponse::Failed(error), + SocketResponse::Connect(ConnectSocketResponse { + status: SocketConnectionStatus::Failed(error), + }), + SocketResponse::Status(SocketStatusResponse { + status: SocketConnectionStatus::Failed(error), + }), + ] { + let response = BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Socket(socket_response), + }; + assert_eq!( + decode_response(&encode_response(response.clone())).unwrap(), + response + ); + } + } + } + #[test] fn request_codec_round_trips_identifier_bounds() { for request_id in [RequestId(0), RequestId(u64::MAX)] { @@ -458,6 +612,34 @@ mod tests { })), BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { read: 3 })), BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { written: 3 })), + BrokerResult::Socket(SocketResponse::Create(CreateSocketResponse { handle })), + BrokerResult::Socket(SocketResponse::Status(SocketStatusResponse { + status: SocketConnectionStatus::Unconnected, + })), + BrokerResult::Socket(SocketResponse::Connect(ConnectSocketResponse { + status: SocketConnectionStatus::Connecting, + })), + BrokerResult::Socket(SocketResponse::Connect(ConnectSocketResponse { + status: SocketConnectionStatus::Connected, + })), + BrokerResult::Socket(SocketResponse::Connect(ConnectSocketResponse { + status: SocketConnectionStatus::Failed(SocketError::ConnectionRefused), + })), + BrokerResult::Socket(SocketResponse::Send(SendSocketResponse { sent: 3 })), + BrokerResult::Socket(SocketResponse::Receive(ReceiveSocketResponse::Received(3))), + BrokerResult::Socket(SocketResponse::Receive(ReceiveSocketResponse::Received(0))), + BrokerResult::Socket(SocketResponse::Receive(ReceiveSocketResponse::EndOfStream)), + BrokerResult::Socket(SocketResponse::Shutdown), + BrokerResult::Socket(SocketResponse::Status(SocketStatusResponse { + status: SocketConnectionStatus::Connecting, + })), + BrokerResult::Socket(SocketResponse::Status(SocketStatusResponse { + status: SocketConnectionStatus::Connected, + })), + BrokerResult::Socket(SocketResponse::Status(SocketStatusResponse { + status: SocketConnectionStatus::Failed(SocketError::TimedOut), + })), + BrokerResult::Socket(SocketResponse::Failed(SocketError::ConnectionReset)), BrokerResult::Error(ErrorCode::PolicyDenied), BrokerResult::Error(ErrorCode::WouldBlock), BrokerResult::Error(ErrorCode::PeerClosed), @@ -475,8 +657,14 @@ mod tests { maximum_encoded_size = maximum_encoded_size.max(encoded.len()); assert!(encoded.len() <= MAX_ENCODED_ACTIVE_MESSAGE_SIZE); assert_eq!(decode_response(&encoded).unwrap(), response); + assert_eq!( + decode_handshake_response(&encoded), + Err(WireError::WrongMessagePhase) + ); } - assert_eq!(maximum_encoded_size, MAX_ENCODED_ACTIVE_MESSAGE_SIZE); + // Requests bind the shared limit, so responses only have to fit under + // it. The request test asserts the limit is reached and therefore tight. + assert!(maximum_encoded_size <= MAX_ENCODED_ACTIVE_MESSAGE_SIZE); } #[test] @@ -582,6 +770,163 @@ mod tests { assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); } + #[test] + fn decode_rejects_malformed_socket_request_frames() { + let mut unknown_operation = Vec::from([REQUEST_TAG_SOCKET]); + unknown_operation.extend_from_slice(&TEST_REQUEST_ID.0.to_le_bytes()); + unknown_operation.push(0xff); + assert_eq!( + decode_request(&unknown_operation), + Err(WireError::InvalidTag) + ); + + // A socket envelope carrying no family tag at all. + assert_eq!( + decode_request(&unknown_operation[..unknown_operation.len() - 1]), + Err(WireError::TruncatedFrame) + ); + + // Address family, type, and protocol are the last three bytes of a + // create frame, so each unknown tag is rejected on its own. + let create = encode_request(BrokerRequest { + request_id: TEST_REQUEST_ID, + operation: BrokerOperation::Socket(SocketRequest::Create(CreateSocketRequest { + address_family: AddressFamily::Ipv4, + socket_type: SocketType::Stream, + protocol: IpProtocol::Tcp, + })), + }); + for offset in 1..=3 { + let mut frame = create.clone(); + let index = frame.len() - offset; + frame[index] = 0xff; + assert_eq!(decode_request(&frame), Err(WireError::InvalidTag)); + } + assert_eq!( + decode_request(&create[..create.len() - 1]), + Err(WireError::TruncatedFrame) + ); + + let mut unknown_shutdown_mode = encode_request(BrokerRequest { + request_id: TEST_REQUEST_ID, + operation: BrokerOperation::Socket(SocketRequest::Shutdown(ShutdownSocketRequest { + handle: ObjectHandle(9), + mode: ShutdownMode::Both, + })), + }); + *unknown_shutdown_mode.last_mut().unwrap() = 0xff; + assert_eq!( + decode_request(&unknown_shutdown_mode), + Err(WireError::InvalidTag) + ); + + let connect = encode_request(BrokerRequest { + request_id: TEST_REQUEST_ID, + operation: BrokerOperation::Socket(SocketRequest::Connect(ConnectSocketRequest { + handle: ObjectHandle(9), + address: SocketAddressV4 { + address: Ipv4Address([203, 0, 113, 7]), + port: Port(443), + }, + })), + }); + assert_eq!( + decode_request(&connect[..connect.len() - 1]), + Err(WireError::TruncatedFrame) + ); + + let mut trailing = connect; + trailing.push(0); + assert_eq!(decode_request(&trailing), Err(WireError::TrailingBytes)); + } + + #[test] + fn decode_rejects_malformed_socket_response_frames() { + let mut unknown_response = Vec::from([RESPONSE_TAG_SOCKET]); + unknown_response.extend_from_slice(&TEST_REQUEST_ID.0.to_le_bytes()); + unknown_response.push(0xff); + assert_eq!( + decode_response(&unknown_response), + Err(WireError::InvalidTag) + ); + assert_eq!( + decode_response(&unknown_response[..unknown_response.len() - 1]), + Err(WireError::TruncatedFrame) + ); + + let mut unknown_connect_status = encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Socket(SocketResponse::Connect(ConnectSocketResponse { + status: SocketConnectionStatus::Connecting, + })), + }); + *unknown_connect_status.last_mut().unwrap() = 0xff; + assert_eq!( + decode_response(&unknown_connect_status), + Err(WireError::InvalidTag) + ); + + let mut unknown_status = encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Socket(SocketResponse::Status(SocketStatusResponse { + status: SocketConnectionStatus::Connected, + })), + }); + *unknown_status.last_mut().unwrap() = 0xff; + assert_eq!(decode_response(&unknown_status), Err(WireError::InvalidTag)); + + let socket_error = encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Socket(SocketResponse::Failed(SocketError::TimedOut)), + }); + for raw in [0, 11, u8::MAX] { + let mut unknown_socket_error = socket_error.clone(); + *unknown_socket_error.last_mut().unwrap() = raw; + assert_eq!( + decode_response(&unknown_socket_error), + Err(WireError::InvalidTag) + ); + } + assert_eq!( + decode_response(&socket_error[..socket_error.len() - 1]), + Err(WireError::TruncatedFrame) + ); + + let failed_connection = encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Socket(SocketResponse::Connect(ConnectSocketResponse { + status: SocketConnectionStatus::Failed(SocketError::ConnectionRefused), + })), + }); + assert_eq!( + decode_response(&failed_connection[..failed_connection.len() - 1]), + Err(WireError::TruncatedFrame) + ); + + let received = encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Socket(SocketResponse::Receive(ReceiveSocketResponse::Received( + 4096, + ))), + }); + assert_eq!( + decode_response(&received[..received.len() - 1]), + Err(WireError::TruncatedFrame) + ); + + let mut unknown_receive_result = encode_response(BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::Socket(SocketResponse::Receive( + ReceiveSocketResponse::EndOfStream, + )), + }); + *unknown_receive_result.last_mut().unwrap() = 0xff; + assert_eq!( + decode_response(&unknown_receive_result), + Err(WireError::InvalidTag) + ); + } + #[test] fn decode_rejects_malformed_handshake_response_frames() { assert_eq!( @@ -719,6 +1064,36 @@ mod tests { ); } + #[test] + fn socket_connect_request_wire_shape_is_pinned() { + assert_eq!( + encode_request(BrokerRequest { + request_id: RequestId(13), + operation: BrokerOperation::Socket(SocketRequest::Connect(ConnectSocketRequest { + handle: ObjectHandle(9), + address: SocketAddressV4 { + address: Ipv4Address([203, 0, 113, 7]), + port: Port(443), + }, + })), + }), + [ + 5, 13, 0, 0, 0, 0, 0, 0, 0, 1, 9, 0, 0, 0, 0, 0, 0, 0, 203, 0, 113, 7, 187, 1 + ] + ); + } + + #[test] + fn socket_failure_response_wire_shape_is_pinned() { + assert_eq!( + encode_response(BrokerResponse { + request_id: RequestId(13), + result: BrokerResult::Socket(SocketResponse::Failed(SocketError::ConnectionReset,)), + }), + [8, 13, 0, 0, 0, 0, 0, 0, 0, 6, 2] + ); + } + #[test] fn event_add_response_wire_shape_is_pinned() { assert_eq!( diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs index 8babbcb9b0..0fba093389 100644 --- a/litebox_broker_protocol/src/wire/event.rs +++ b/litebox_broker_protocol/src/wire/event.rs @@ -17,9 +17,9 @@ const EVENT_REQUEST_TAG_CREATE: u8 = 0; const EVENT_REQUEST_TAG_ADD: u8 = 1; const EVENT_REQUEST_TAG_CONSUME: u8 = 2; -const EVENT_RESPONSE_TAG_CREATED: u8 = 0; -const EVENT_RESPONSE_TAG_ADDED: u8 = 1; -const EVENT_RESPONSE_TAG_CONSUMED: u8 = 2; +const EVENT_RESPONSE_TAG_CREATE: u8 = 0; +const EVENT_RESPONSE_TAG_ADD: u8 = 1; +const EVENT_RESPONSE_TAG_CONSUME: u8 = 2; const EVENT_CONSUME_MODE_TAG_ALL: u8 = 1; const EVENT_CONSUME_MODE_TAG_ONE: u8 = 2; @@ -65,15 +65,15 @@ pub(super) fn decode_event_request(decoder: &mut Decoder<'_>) -> Result { - encoder.u8(EVENT_RESPONSE_TAG_CREATED); + encoder.u8(EVENT_RESPONSE_TAG_CREATE); encoder.handle(response.handle); } EventResponse::Add(response) => { - encoder.u8(EVENT_RESPONSE_TAG_ADDED); + encoder.u8(EVENT_RESPONSE_TAG_ADD); encoder.u32(response.readiness.0); } EventResponse::Consume(response) => { - encoder.u8(EVENT_RESPONSE_TAG_CONSUMED); + encoder.u8(EVENT_RESPONSE_TAG_CONSUME); encoder.u64(response.value); encoder.u32(response.readiness.0); } @@ -82,13 +82,13 @@ pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventRespon pub(super) fn decode_event_response(decoder: &mut Decoder<'_>) -> Result { let response = match decoder.u8()? { - EVENT_RESPONSE_TAG_CREATED => EventResponse::Create(CreateEventResponse { + EVENT_RESPONSE_TAG_CREATE => EventResponse::Create(CreateEventResponse { handle: decoder.handle()?, }), - EVENT_RESPONSE_TAG_ADDED => EventResponse::Add(AddEventResponse { + EVENT_RESPONSE_TAG_ADD => EventResponse::Add(AddEventResponse { readiness: ReadinessFlags(decoder.u32()?), }), - EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(EventConsumption { + EVENT_RESPONSE_TAG_CONSUME => EventResponse::Consume(EventConsumption { value: decoder.u64()?, readiness: ReadinessFlags(decoder.u32()?), }), diff --git a/litebox_broker_protocol/src/wire/pipe.rs b/litebox_broker_protocol/src/wire/pipe.rs index a09beba030..a74d1eebe5 100644 --- a/litebox_broker_protocol/src/wire/pipe.rs +++ b/litebox_broker_protocol/src/wire/pipe.rs @@ -15,9 +15,9 @@ const PIPE_REQUEST_TAG_CREATE: u8 = 0; const PIPE_REQUEST_TAG_READ: u8 = 1; const PIPE_REQUEST_TAG_WRITE: u8 = 2; -const PIPE_RESPONSE_TAG_CREATED: u8 = 0; +const PIPE_RESPONSE_TAG_CREATE: u8 = 0; const PIPE_RESPONSE_TAG_READ: u8 = 1; -const PIPE_RESPONSE_TAG_WRITTEN: u8 = 2; +const PIPE_RESPONSE_TAG_WRITE: u8 = 2; pub(super) fn encode_pipe_request(encoder: &mut Encoder, request: PipeRequest) { match request { @@ -74,7 +74,7 @@ fn decode_shared_buffer_descriptor( pub(super) fn encode_pipe_response(encoder: &mut Encoder, response: PipeResponse) { match response { PipeResponse::Create(response) => { - encoder.u8(PIPE_RESPONSE_TAG_CREATED); + encoder.u8(PIPE_RESPONSE_TAG_CREATE); encoder.handle(response.read_handle); encoder.handle(response.write_handle); } @@ -83,7 +83,7 @@ pub(super) fn encode_pipe_response(encoder: &mut Encoder, response: PipeResponse encoder.u32(response.read); } PipeResponse::Write(response) => { - encoder.u8(PIPE_RESPONSE_TAG_WRITTEN); + encoder.u8(PIPE_RESPONSE_TAG_WRITE); encoder.u32(response.written); } } @@ -91,14 +91,14 @@ pub(super) fn encode_pipe_response(encoder: &mut Encoder, response: PipeResponse pub(super) fn decode_pipe_response(decoder: &mut Decoder<'_>) -> Result { match decoder.u8()? { - PIPE_RESPONSE_TAG_CREATED => Ok(PipeResponse::Create(CreatePipeResponse { + PIPE_RESPONSE_TAG_CREATE => Ok(PipeResponse::Create(CreatePipeResponse { read_handle: decoder.handle()?, write_handle: decoder.handle()?, })), PIPE_RESPONSE_TAG_READ => Ok(PipeResponse::Read(ReadPipeResponse { read: decoder.u32()?, })), - PIPE_RESPONSE_TAG_WRITTEN => Ok(PipeResponse::Write(WritePipeResponse { + PIPE_RESPONSE_TAG_WRITE => Ok(PipeResponse::Write(WritePipeResponse { written: decoder.u32()?, })), _ => Err(WireError::InvalidTag), diff --git a/litebox_broker_protocol/src/wire/socket.rs b/litebox_broker_protocol/src/wire/socket.rs new file mode 100644 index 0000000000..031ef1cd89 --- /dev/null +++ b/litebox_broker_protocol/src/wire/socket.rs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::message::{SocketRequest, SocketResponse}; +use crate::shared_buffer::{SharedBufferDescriptor, SharedBufferSlotIndex}; +use crate::socket::{ + AddressFamily, ConnectSocketRequest, ConnectSocketResponse, CreateSocketRequest, + CreateSocketResponse, IpProtocol, Ipv4Address, Port, ReceiveFlags, ReceiveSocketRequest, + ReceiveSocketResponse, SendFlags, SendSocketRequest, SendSocketResponse, ShutdownMode, + ShutdownSocketRequest, SocketAddressV4, SocketConnectionStatus, SocketError, + SocketStatusRequest, SocketStatusResponse, SocketType, +}; + +use super::WireError; +use super::primitive::{Decoder, Encoder}; + +const SOCKET_REQUEST_TAG_CREATE: u8 = 0; +const SOCKET_REQUEST_TAG_CONNECT: u8 = 1; +const SOCKET_REQUEST_TAG_SEND: u8 = 2; +const SOCKET_REQUEST_TAG_RECEIVE: u8 = 3; +const SOCKET_REQUEST_TAG_SHUTDOWN: u8 = 4; +const SOCKET_REQUEST_TAG_STATUS: u8 = 5; + +const SOCKET_RESPONSE_TAG_CREATE: u8 = 0; +const SOCKET_RESPONSE_TAG_CONNECT: u8 = 1; +const SOCKET_RESPONSE_TAG_SEND: u8 = 2; +const SOCKET_RESPONSE_TAG_RECEIVE: u8 = 3; +const SOCKET_RESPONSE_TAG_SHUTDOWN: u8 = 4; +const SOCKET_RESPONSE_TAG_STATUS: u8 = 5; +const SOCKET_RESPONSE_TAG_FAILED: u8 = 6; + +const ADDRESS_FAMILY_TAG_IPV4: u8 = 0; + +const TYPE_TAG_STREAM: u8 = 0; + +const IP_PROTOCOL_TAG_TCP: u8 = 0; + +const SHUTDOWN_TAG_READ: u8 = 0; +const SHUTDOWN_TAG_WRITE: u8 = 1; +const SHUTDOWN_TAG_BOTH: u8 = 2; + +const CONNECTION_STATUS_TAG_UNCONNECTED: u8 = 0; +const CONNECTION_STATUS_TAG_CONNECTING: u8 = 1; +const CONNECTION_STATUS_TAG_CONNECTED: u8 = 2; +const CONNECTION_STATUS_TAG_FAILED: u8 = 3; + +const RECEIVE_RESPONSE_TAG_RECEIVED: u8 = 0; +const RECEIVE_RESPONSE_TAG_END_OF_STREAM: u8 = 1; + +pub(super) fn encode_socket_request(encoder: &mut Encoder, request: SocketRequest) { + match request { + SocketRequest::Create(request) => { + encoder.u8(SOCKET_REQUEST_TAG_CREATE); + encoder.u8(match request.address_family { + AddressFamily::Ipv4 => ADDRESS_FAMILY_TAG_IPV4, + }); + encoder.u8(match request.socket_type { + SocketType::Stream => TYPE_TAG_STREAM, + }); + encoder.u8(match request.protocol { + IpProtocol::Tcp => IP_PROTOCOL_TAG_TCP, + }); + } + SocketRequest::Connect(request) => { + encoder.u8(SOCKET_REQUEST_TAG_CONNECT); + encoder.handle(request.handle); + encode_address(encoder, request.address); + } + SocketRequest::Send(request) => { + encoder.u8(SOCKET_REQUEST_TAG_SEND); + encoder.handle(request.handle); + encode_shared_buffer_descriptor(encoder, request.buffer); + encoder.u32(request.flags.0); + } + SocketRequest::Receive(request) => { + encoder.u8(SOCKET_REQUEST_TAG_RECEIVE); + encoder.handle(request.handle); + encode_shared_buffer_descriptor(encoder, request.buffer); + encoder.u32(request.flags.0); + } + SocketRequest::Shutdown(request) => { + encoder.u8(SOCKET_REQUEST_TAG_SHUTDOWN); + encoder.handle(request.handle); + encoder.u8(match request.mode { + ShutdownMode::Read => SHUTDOWN_TAG_READ, + ShutdownMode::Write => SHUTDOWN_TAG_WRITE, + ShutdownMode::Both => SHUTDOWN_TAG_BOTH, + }); + } + SocketRequest::Status(request) => { + encoder.u8(SOCKET_REQUEST_TAG_STATUS); + encoder.handle(request.handle); + } + } +} + +pub(super) fn decode_socket_request(decoder: &mut Decoder<'_>) -> Result { + match decoder.u8()? { + SOCKET_REQUEST_TAG_CREATE => Ok(SocketRequest::Create(CreateSocketRequest { + address_family: match decoder.u8()? { + ADDRESS_FAMILY_TAG_IPV4 => AddressFamily::Ipv4, + _ => return Err(WireError::InvalidTag), + }, + socket_type: match decoder.u8()? { + TYPE_TAG_STREAM => SocketType::Stream, + _ => return Err(WireError::InvalidTag), + }, + protocol: match decoder.u8()? { + IP_PROTOCOL_TAG_TCP => IpProtocol::Tcp, + _ => return Err(WireError::InvalidTag), + }, + })), + SOCKET_REQUEST_TAG_CONNECT => Ok(SocketRequest::Connect(ConnectSocketRequest { + handle: decoder.handle()?, + address: decode_address(decoder)?, + })), + SOCKET_REQUEST_TAG_SEND => Ok(SocketRequest::Send(SendSocketRequest { + handle: decoder.handle()?, + buffer: decode_shared_buffer_descriptor(decoder)?, + flags: SendFlags(decoder.u32()?), + })), + SOCKET_REQUEST_TAG_RECEIVE => Ok(SocketRequest::Receive(ReceiveSocketRequest { + handle: decoder.handle()?, + buffer: decode_shared_buffer_descriptor(decoder)?, + flags: ReceiveFlags(decoder.u32()?), + })), + SOCKET_REQUEST_TAG_SHUTDOWN => Ok(SocketRequest::Shutdown(ShutdownSocketRequest { + handle: decoder.handle()?, + mode: match decoder.u8()? { + SHUTDOWN_TAG_READ => ShutdownMode::Read, + SHUTDOWN_TAG_WRITE => ShutdownMode::Write, + SHUTDOWN_TAG_BOTH => ShutdownMode::Both, + _ => return Err(WireError::InvalidTag), + }, + })), + SOCKET_REQUEST_TAG_STATUS => Ok(SocketRequest::Status(SocketStatusRequest { + handle: decoder.handle()?, + })), + _ => Err(WireError::InvalidTag), + } +} + +pub(super) fn encode_socket_response(encoder: &mut Encoder, response: SocketResponse) { + match response { + SocketResponse::Create(response) => { + encoder.u8(SOCKET_RESPONSE_TAG_CREATE); + encoder.handle(response.handle); + } + SocketResponse::Connect(response) => { + encoder.u8(SOCKET_RESPONSE_TAG_CONNECT); + encode_connection_status(encoder, response.status); + } + SocketResponse::Send(response) => { + encoder.u8(SOCKET_RESPONSE_TAG_SEND); + encoder.u32(response.sent); + } + SocketResponse::Receive(response) => { + encoder.u8(SOCKET_RESPONSE_TAG_RECEIVE); + match response { + ReceiveSocketResponse::Received(received) => { + encoder.u8(RECEIVE_RESPONSE_TAG_RECEIVED); + encoder.u32(received); + } + ReceiveSocketResponse::EndOfStream => { + encoder.u8(RECEIVE_RESPONSE_TAG_END_OF_STREAM); + } + } + } + SocketResponse::Shutdown => encoder.u8(SOCKET_RESPONSE_TAG_SHUTDOWN), + SocketResponse::Status(response) => { + encoder.u8(SOCKET_RESPONSE_TAG_STATUS); + encode_connection_status(encoder, response.status); + } + SocketResponse::Failed(error) => { + encoder.u8(SOCKET_RESPONSE_TAG_FAILED); + encode_socket_error(encoder, error); + } + } +} + +pub(super) fn decode_socket_response( + decoder: &mut Decoder<'_>, +) -> Result { + match decoder.u8()? { + SOCKET_RESPONSE_TAG_CREATE => Ok(SocketResponse::Create(CreateSocketResponse { + handle: decoder.handle()?, + })), + SOCKET_RESPONSE_TAG_CONNECT => Ok(SocketResponse::Connect(ConnectSocketResponse { + status: decode_connection_status(decoder)?, + })), + SOCKET_RESPONSE_TAG_SEND => Ok(SocketResponse::Send(SendSocketResponse { + sent: decoder.u32()?, + })), + SOCKET_RESPONSE_TAG_RECEIVE => Ok(SocketResponse::Receive(match decoder.u8()? { + RECEIVE_RESPONSE_TAG_RECEIVED => ReceiveSocketResponse::Received(decoder.u32()?), + RECEIVE_RESPONSE_TAG_END_OF_STREAM => ReceiveSocketResponse::EndOfStream, + _ => return Err(WireError::InvalidTag), + })), + SOCKET_RESPONSE_TAG_SHUTDOWN => Ok(SocketResponse::Shutdown), + SOCKET_RESPONSE_TAG_STATUS => Ok(SocketResponse::Status(SocketStatusResponse { + status: decode_connection_status(decoder)?, + })), + SOCKET_RESPONSE_TAG_FAILED => Ok(SocketResponse::Failed(decode_socket_error(decoder)?)), + _ => Err(WireError::InvalidTag), + } +} + +fn encode_connection_status(encoder: &mut Encoder, status: SocketConnectionStatus) { + match status { + SocketConnectionStatus::Unconnected => encoder.u8(CONNECTION_STATUS_TAG_UNCONNECTED), + SocketConnectionStatus::Connecting => encoder.u8(CONNECTION_STATUS_TAG_CONNECTING), + SocketConnectionStatus::Connected => encoder.u8(CONNECTION_STATUS_TAG_CONNECTED), + SocketConnectionStatus::Failed(error) => { + encoder.u8(CONNECTION_STATUS_TAG_FAILED); + encode_socket_error(encoder, error); + } + } +} + +fn decode_connection_status( + decoder: &mut Decoder<'_>, +) -> Result { + match decoder.u8()? { + CONNECTION_STATUS_TAG_UNCONNECTED => Ok(SocketConnectionStatus::Unconnected), + CONNECTION_STATUS_TAG_CONNECTING => Ok(SocketConnectionStatus::Connecting), + CONNECTION_STATUS_TAG_CONNECTED => Ok(SocketConnectionStatus::Connected), + CONNECTION_STATUS_TAG_FAILED => Ok(SocketConnectionStatus::Failed(decode_socket_error( + decoder, + )?)), + _ => Err(WireError::InvalidTag), + } +} + +fn encode_socket_error(encoder: &mut Encoder, error: SocketError) { + encoder.u8(error.as_raw()); +} + +fn decode_socket_error(decoder: &mut Decoder<'_>) -> Result { + SocketError::from_raw(decoder.u8()?).ok_or(WireError::InvalidTag) +} + +fn encode_address(encoder: &mut Encoder, address: SocketAddressV4) { + for octet in address.address.0 { + encoder.u8(octet); + } + encoder.u16(address.port.0); +} + +fn decode_address(decoder: &mut Decoder<'_>) -> Result { + let mut octets = [0; 4]; + for octet in &mut octets { + *octet = decoder.u8()?; + } + Ok(SocketAddressV4 { + address: Ipv4Address(octets), + port: Port(decoder.u16()?), + }) +} + +fn encode_shared_buffer_descriptor(encoder: &mut Encoder, descriptor: SharedBufferDescriptor) { + encoder.u32(descriptor.slot_index.0); + encoder.u32(descriptor.length); +} + +fn decode_shared_buffer_descriptor( + decoder: &mut Decoder<'_>, +) -> Result { + Ok(SharedBufferDescriptor { + slot_index: SharedBufferSlotIndex(decoder.u32()?), + length: decoder.u32()?, + }) +} From ec1369458f0cc207bcb4ce792925d20bc56d3807 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 27 Jul 2026 10:40:39 -0700 Subject: [PATCH 135/319] Drop broker objects outside reference locks (#1095) This PR moves broker object destruction outside the global reference table and per-session index locks for both explicit close and session teardown. A per-session handle index keeps explicit close O(1) and teardown O(n) while ensuring objects are dropped only after both locks are released. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_core/Cargo.toml | 2 +- litebox_broker_core/src/error.rs | 4 +- litebox_broker_core/src/lib.rs | 5 - litebox_broker_core/src/session.rs | 209 +++++++++++++++++++++++++---- 4 files changed, 185 insertions(+), 35 deletions(-) diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index 7f28f63ddb..020e0e23bd 100644 --- a/litebox_broker_core/Cargo.toml +++ b/litebox_broker_core/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" bitflags = { version = "2.9.0", default-features = false } hashbrown = "0.15.2" litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -spin = { version = "0.9.8", default-features = false, features = ["rwlock"] } +spin = { version = "0.9.8", default-features = false, features = ["rwlock", "spin_mutex"] } thiserror = { version = "2.0.6", default-features = false } [lints] diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index 7748d0be0e..493f1c4c4e 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -21,6 +21,8 @@ pub enum BrokerError { BrokerCoreAlreadyExists, #[error("broker operation would block")] WouldBlock, + #[error("broker authority state is inconsistent")] + Internal, #[error("broker object peer is closed")] PeerClosed, #[error("broker memory allocation failed")] @@ -36,7 +38,7 @@ impl From for ErrorCode { BrokerError::UnknownObject => Self::UnknownObject, BrokerError::InvalidRights => Self::InvalidRights, BrokerError::ResourceExhausted => Self::ResourceExhausted, - BrokerError::BrokerCoreAlreadyExists => Self::Internal, + BrokerError::BrokerCoreAlreadyExists | BrokerError::Internal => Self::Internal, BrokerError::WouldBlock => Self::WouldBlock, BrokerError::PeerClosed => Self::PeerClosed, BrokerError::OutOfMemory => Self::OutOfMemory, diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 03edc37c93..76df5158f3 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -151,9 +151,4 @@ impl BrokerCore { caller_credential, )) } - - pub(crate) fn close_session(&self, session_id: session::SessionId) { - let mut references = self.references.write(); - references.retain(|_, reference| reference.session_id != session_id); - } } diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index b673ef4cac..c8870fe9ed 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use alloc::sync::Arc; +use alloc::{sync::Arc, vec::Vec}; use crate::event::EventObject; use crate::pipe::PipeObject; @@ -9,7 +9,7 @@ use crate::{BrokerCore, BrokerError, Result}; use hashbrown::HashMap; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::readiness::ReadinessFlags; -use spin::rwlock::RwLock; +use spin::{Mutex, rwlock::RwLock}; /// Caller identity information supplied by the broker entry layer. /// @@ -45,6 +45,7 @@ pub(crate) struct ObjectReference { pub(crate) object: Arc>, pub(crate) session_id: SessionId, pub(crate) rights: ObjectRights, + session_reference_index: usize, } pub(crate) enum ObjectEntry { @@ -63,6 +64,8 @@ pub struct BrokerSession { pub(crate) session_id: SessionId, /// Broker-entry-authenticated caller credential for this session. pub(crate) caller_credential: CallerCredential, + /// Handles of the live object references owned by this session. + reference_handles: Mutex>, } impl BrokerSession { @@ -76,6 +79,7 @@ impl BrokerSession { core, session_id, caller_credential, + reference_handles: Mutex::new(Vec::new()), } } @@ -84,18 +88,24 @@ impl BrokerSession { .core .policy .principal_object_rights(self.caller_credential)?; + let mut reference_handles = self.reference_handles.lock(); + reference_handles + .try_reserve(1) + .map_err(|_| BrokerError::OutOfMemory)?; let mut references = self.core.references.write(); if references.len() >= self.core.limits.max_references { return Err(BrokerError::ResourceExhausted); } let handle = self.core.allocate_reference_handle()?; - references.insert( + if references.contains_key(&handle) { + return Err(BrokerError::Internal); + } + self.insert_object_reference( + &mut references, + &mut reference_handles, handle, - ObjectReference { - object: Arc::new(RwLock::new(object)), - session_id: self.session_id, - rights, - }, + object, + rights, ); Ok(handle) @@ -110,6 +120,10 @@ impl BrokerSession { .core .policy .principal_object_rights(self.caller_credential)?; + let mut reference_handles = self.reference_handles.lock(); + reference_handles + .try_reserve(2) + .map_err(|_| BrokerError::OutOfMemory)?; let mut references = self.core.references.write(); if references .len() @@ -119,19 +133,45 @@ impl BrokerSession { return Err(BrokerError::ResourceExhausted); } let (first_handle, second_handle) = self.core.allocate_reference_handle_pair()?; + if first_handle == second_handle + || references.contains_key(&first_handle) + || references.contains_key(&second_handle) + { + return Err(BrokerError::Internal); + } for (handle, object) in [(first_handle, first), (second_handle, second)] { - references.insert( + self.insert_object_reference( + &mut references, + &mut reference_handles, handle, - ObjectReference { - object: Arc::new(RwLock::new(object)), - session_id: self.session_id, - rights, - }, + object, + rights, ); } Ok((first_handle, second_handle)) } + fn insert_object_reference( + &self, + references: &mut HashMap, + reference_handles: &mut Vec, + handle: ObjectHandle, + object: ObjectEntry, + rights: ObjectRights, + ) { + let session_reference_index = reference_handles.len(); + references.insert( + handle, + ObjectReference { + object: Arc::new(RwLock::new(object)), + session_id: self.session_id, + rights, + session_reference_index, + }, + ); + reference_handles.push(handle); + } + pub(crate) fn with_authorized_object( &self, handle: ObjectHandle, @@ -190,20 +230,91 @@ impl BrokerSession { /// Closes one object reference owned by this session. /// /// The underlying object is released when this was the last live reference. + /// Destruction happens after releasing the process-wide reference-table + /// lock, so an object may safely release platform resources. pub fn close_object_reference(&self, handle: ObjectHandle) -> Result<()> { + let reference = self.remove_object_reference(handle)?; + drop(reference); + Ok(()) + } + + fn remove_object_reference(&self, handle: ObjectHandle) -> Result { + let mut reference_handles = self.reference_handles.lock(); let mut references = self.core.references.write(); - let reference = references.get(&handle).ok_or(BrokerError::UnknownObject)?; - if reference.session_id != self.session_id { - return Err(BrokerError::UnknownObject); + let reference = references + .remove(&handle) + .ok_or(BrokerError::UnknownObject)?; + let index = reference.session_reference_index; + // Keep fallible validation in a nested scope so `?` and early returns + // reach the shared rollback below instead of dropping the removed + // reference while either reference-index lock is held. + let removal_result = (|| { + if reference.session_id != self.session_id { + return Err(BrokerError::UnknownObject); + } + if reference_handles.get(index) != Some(&handle) { + return Err(BrokerError::Internal); + } + let last_index = reference_handles + .len() + .checked_sub(1) + .ok_or(BrokerError::Internal)?; + if index != last_index { + let moved_handle = *reference_handles + .get(last_index) + .ok_or(BrokerError::Internal)?; + let moved_reference = references + .get_mut(&moved_handle) + .ok_or(BrokerError::Internal)?; + if moved_reference.session_id != self.session_id + || moved_reference.session_reference_index != last_index + { + return Err(BrokerError::Internal); + } + moved_reference.session_reference_index = index; + } + reference_handles.swap_remove(index); + Ok(()) + })(); + + if let Err(error) = removal_result { + let replaced_reference = references.insert(handle, reference); + drop(references); + drop(reference_handles); + if replaced_reference.is_some() { + return Err(BrokerError::Internal); + } + return Err(error); } - references.remove(&handle); - Ok(()) + Ok(reference) } } impl Drop for BrokerSession { fn drop(&mut self) { - self.core.close_session(self.session_id); + loop { + let Some(handle) = self.reference_handles.lock().pop() else { + break; + }; + // Do not restore an inconsistent handle: retrying it forever would + // prevent later valid references from being released. + let reference = { + let mut references = self.core.references.write(); + let Some(reference) = references.get(&handle) else { + continue; + }; + if reference.session_id != self.session_id { + continue; + } + let Some(reference) = references.remove(&handle) else { + continue; + }; + reference + }; + // Object destruction may release platform resources and must never + // run while either reference index lock is held. + drop(reference); + } } } @@ -230,6 +341,8 @@ mod tests { check_session_drop_releases_references(&broker); check_pipe_lifecycle(&broker); check_pipe_reader_closure(&broker); + check_corrupt_index_fails_without_mutation(&broker); + check_corrupt_index_does_not_break_teardown(&broker); check_pair_handle_exhaustion(&broker); assert!(broker.references.read().is_empty()); @@ -278,27 +391,26 @@ mod tests { Err(BrokerError::ResourceExhausted) ); assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); - assert_eq!(session.close_object_reference(second_handle), Ok(())); - + // Closing the older handle exercises swap-removing a non-last entry. assert_eq!(session.close_object_reference(handle), Ok(())); - { - let references = broker.references.read(); - assert!(references.is_empty()); - } assert_eq!( session.close_object_reference(handle), Err(BrokerError::UnknownObject) ); + assert_eq!(session.close_object_reference(second_handle), Ok(())); + assert!(broker.references.read().is_empty()); } fn check_session_drop_releases_references(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) .unwrap(); - let _handle = crate::event::create(&session, 0).unwrap(); + let first = crate::event::create(&session, 0).unwrap(); + let second = crate::event::create(&session, 0).unwrap(); + assert_ne!(first, second); { let references = broker.references.read(); - assert_eq!(references.len(), 1); + assert_eq!(references.len(), 2); } drop(session); @@ -377,6 +489,47 @@ mod tests { assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); } + fn check_corrupt_index_fails_without_mutation(broker: &BrokerCore) { + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let older = crate::event::create(&session, 0).unwrap(); + let newer = crate::event::create(&session, 0).unwrap(); + { + let mut references = broker.references.write(); + references.get_mut(&older).unwrap().session_reference_index = usize::MAX; + } + + assert_eq!( + session.close_object_reference(older), + Err(BrokerError::Internal) + ); + { + let mut references = broker.references.write(); + references.get_mut(&older).unwrap().session_reference_index = 0; + } + assert_eq!(session.close_object_reference(older), Ok(())); + assert_eq!(session.close_object_reference(newer), Ok(())); + } + + fn check_corrupt_index_does_not_break_teardown(broker: &BrokerCore) { + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let _older = crate::event::create(&session, 0).unwrap(); + let newer = crate::event::create(&session, 0).unwrap(); + broker + .references + .write() + .get_mut(&newer) + .unwrap() + .session_reference_index = usize::MAX; + + drop(session); + + assert!(broker.references.read().is_empty()); + } + fn check_pair_handle_exhaustion(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) From 014f354381296b1e5481558036b98a675ed30417 Mon Sep 17 00:00:00 2001 From: dywongcloud <274333120+dywongcloud@users.noreply.github.com> Date: Mon, 27 Jul 2026 18:42:10 +0000 Subject: [PATCH 136/319] Bound broker resource budgets per session `BrokerCoreLimits` was global to the broker core, but one broker process serves every client association. A single malicious or buggy local could spend the whole reference budget or the whole pipe-capacity budget and permanently deny object and pipe creation to every other session. The budget was only returned when the offending objects closed or the session was torn down, so a local that simply stopped making progress pinned it indefinitely. Give every budget a per-session quota alongside its global ceiling, so the ceiling stays a backstop while no single session can reach it: - `BrokerCoreLimits` gains `max_session_references` and `max_session_pipe_capacity`. `new` keeps its signature and derives both as a quarter of the matching ceiling, so a caller that has not thought about quotas still gets a core no one session can exhaust; `with_session_quotas` states them explicitly. - `create_object_reference` and `create_object_reference_pair` charge the session against `reference_handles`, which already is the session's live reference list, so the quota is enforced against the very state that releases it and the two cannot drift apart. The check runs before the core-wide table is locked, so a session at its quota does not make every other session contend for the shared lock. - Pipe capacity is charged to a per-session counter and to the core-wide counter through the same RAII guard, so a reservation refused by the ceiling releases the session charge on the way out, and both are credited back when the pipe's last endpoint goes away. The counter is shared with the reservation because a pipe object can outlive the session that created it. Ownership enforcement was already correct, so this is an availability fix only. Bounding the control ring's `wait_for_capacity` is deliberately not part of it: any threshold that evicts a hostile local eventually evicts a slow honest one, and a pinned budget is now bounded by the quota anyway. --- litebox_broker_core/src/lib.rs | 112 ++++++++++++-- litebox_broker_core/src/pipe.rs | 46 +++++- litebox_broker_core/src/session.rs | 230 ++++++++++++++++++++++++++++- 3 files changed, 371 insertions(+), 17 deletions(-) diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 76df5158f3..54116cbc48 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -39,30 +39,74 @@ pub use session::{BrokerSession, CallerCredential, ObjectRights}; /// BrokerCore result type. pub type Result = core::result::Result; +/// Number of equal shares a global ceiling is split into when a limit set does +/// not state its per-session quota explicitly. +/// +/// A session may hold one share, so several sessions must each spend their +/// whole quota before a global ceiling is reached, and no single session can +/// reach one on its own. +const DEFAULT_SESSION_QUOTA_SHARES: usize = 4; + /// Resource limits for broker-owned authority state. /// -/// These limits are global to the broker core, not per session. +/// Every budget has two limits. The global ceiling bounds what all sessions +/// hold together and keeps the broker process bounded. The per-session quota +/// bounds what any one session holds, so a malicious or malfunctioning session +/// cannot spend the whole ceiling and deny object and pipe creation to every +/// other session served by the same broker core. Both are enforced on every +/// allocation, with the global ceiling acting as the backstop. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub struct BrokerCoreLimits { - /// Maximum live object references. + /// Maximum live object references across all sessions. pub max_references: usize, - /// Maximum total capacity in bytes reserved by live pipes. + /// Maximum total capacity in bytes reserved by live pipes across all + /// sessions. pub max_total_pipe_capacity: usize, + /// Maximum live object references held by any one session. + pub max_session_references: usize, + /// Maximum capacity in bytes reserved by the live pipes of any one session. + pub max_session_pipe_capacity: usize, } impl BrokerCoreLimits { /// Conservative default limits for initial broker deployments. - pub const DEFAULT: Self = Self { - max_references: 4096, - max_total_pipe_capacity: 64 * 1024 * 1024, - }; - - /// Creates a broker core limit set. + pub const DEFAULT: Self = Self::new(4096, 64 * 1024 * 1024); + + /// Creates a broker core limit set that gives each session an equal share + /// of the global ceilings. + /// + /// Each session may hold up to a quarter of each ceiling, rounded up, so a + /// caller that has not thought about per-session quotas still gets a core + /// no single session can exhaust. Use + /// [`BrokerCoreLimits::with_session_quotas`] to state the quotas + /// explicitly. pub const fn new(max_references: usize, max_total_pipe_capacity: usize) -> Self { Self { max_references, max_total_pipe_capacity, + max_session_references: max_references.div_ceil(DEFAULT_SESSION_QUOTA_SHARES), + max_session_pipe_capacity: max_total_pipe_capacity + .div_ceil(DEFAULT_SESSION_QUOTA_SHARES), + } + } + + /// Returns these limits with explicit per-session quotas. + /// + /// A quota above its global ceiling is accepted rather than rejected: the + /// ceiling is still enforced, so the effective quota is whichever of the + /// two is smaller. + #[must_use] + pub const fn with_session_quotas( + self, + max_session_references: usize, + max_session_pipe_capacity: usize, + ) -> Self { + Self { + max_references: self.max_references, + max_total_pipe_capacity: self.max_total_pipe_capacity, + max_session_references, + max_session_pipe_capacity, } } } @@ -152,3 +196,53 @@ impl BrokerCore { )) } } + +#[cfg(test)] +mod tests { + use super::{BrokerCoreLimits, DEFAULT_SESSION_QUOTA_SHARES}; + + #[test] + fn default_limits_bound_what_one_session_can_hold() { + let limits = BrokerCoreLimits::DEFAULT; + + assert_eq!(limits.max_references, 4096); + assert_eq!(limits.max_total_pipe_capacity, 64 * 1024 * 1024); + assert_eq!( + limits.max_session_references, + limits.max_references / DEFAULT_SESSION_QUOTA_SHARES + ); + assert_eq!( + limits.max_session_pipe_capacity, + limits.max_total_pipe_capacity / DEFAULT_SESSION_QUOTA_SHARES + ); + // No single session can reach a global ceiling on its own. + assert!(limits.max_session_references < limits.max_references); + assert!(limits.max_session_pipe_capacity < limits.max_total_pipe_capacity); + } + + #[test] + fn derived_quotas_never_round_a_usable_ceiling_down_to_nothing() { + let limits = BrokerCoreLimits::new(1, 1); + + assert_eq!(limits.max_session_references, 1); + assert_eq!(limits.max_session_pipe_capacity, 1); + } + + #[test] + fn derived_quotas_stay_zero_for_a_ceiling_of_zero() { + let limits = BrokerCoreLimits::new(0, 0); + + assert_eq!(limits.max_session_references, 0); + assert_eq!(limits.max_session_pipe_capacity, 0); + } + + #[test] + fn explicit_quotas_replace_the_derived_ones_and_keep_the_ceilings() { + let limits = BrokerCoreLimits::new(4096, 64 * 1024 * 1024).with_session_quotas(7, 9); + + assert_eq!(limits.max_references, 4096); + assert_eq!(limits.max_total_pipe_capacity, 64 * 1024 * 1024); + assert_eq!(limits.max_session_references, 7); + assert_eq!(limits.max_session_pipe_capacity, 9); + } +} diff --git a/litebox_broker_core/src/pipe.rs b/litebox_broker_core/src/pipe.rs index 4d834ca90f..5586a9d1dc 100644 --- a/litebox_broker_core/src/pipe.rs +++ b/litebox_broker_core/src/pipe.rs @@ -184,19 +184,19 @@ enum PipeEndpoint { Write, } -struct PipeCapacityReservation { +/// Capacity charged to one counter for as long as this value is alive. +struct CapacityCharge { reserved_capacity: Arc, capacity: usize, } -impl PipeCapacityReservation { - fn new(session: &BrokerSession, capacity: usize) -> Result { - let reserved_capacity = Arc::clone(&session.core.reserved_pipe_capacity); +impl CapacityCharge { + fn new(reserved_capacity: Arc, capacity: usize, limit: usize) -> Result { reserved_capacity .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |reserved| { reserved .checked_add(capacity) - .filter(|total| *total <= session.core.limits.max_total_pipe_capacity) + .filter(|total| *total <= limit) }) .map_err(|_| BrokerError::ResourceExhausted)?; Ok(Self { @@ -206,7 +206,7 @@ impl PipeCapacityReservation { } } -impl Drop for PipeCapacityReservation { +impl Drop for CapacityCharge { fn drop(&mut self) { self.reserved_capacity .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |reserved| { @@ -216,6 +216,40 @@ impl Drop for PipeCapacityReservation { } } +/// Both capacity budgets one live pipe holds. +/// +/// The per-session quota keeps one session from reserving the whole core-wide +/// ceiling; the core-wide ceiling remains the backstop across all sessions. +/// Both charges are released together when the pipe's shared state is dropped, +/// which happens once the last reference to either endpoint is closed or its +/// owning session is torn down. +struct PipeCapacityReservation { + _session_charge: CapacityCharge, + _core_charge: CapacityCharge, +} + +impl PipeCapacityReservation { + fn new(session: &BrokerSession, capacity: usize) -> Result { + // Charge the session first: a session already at its quota is rejected + // without touching the shared counter, and the `?` below releases this + // charge if the core-wide ceiling then rejects the pipe. + let session_charge = CapacityCharge::new( + Arc::clone(&session.reserved_pipe_capacity), + capacity, + session.core.limits.max_session_pipe_capacity, + )?; + let core_charge = CapacityCharge::new( + Arc::clone(&session.core.reserved_pipe_capacity), + capacity, + session.core.limits.max_total_pipe_capacity, + )?; + Ok(Self { + _session_charge: session_charge, + _core_charge: core_charge, + }) + } +} + struct PipeState { data: VecDeque, capacity: usize, diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index c8870fe9ed..6bf9e6a006 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -2,6 +2,7 @@ // Licensed under the MIT license. use alloc::{sync::Arc, vec::Vec}; +use core::sync::atomic::AtomicUsize; use crate::event::EventObject; use crate::pipe::PipeObject; @@ -57,7 +58,8 @@ pub(crate) enum ObjectEntry { /// /// User mode does not choose this value. The broker entry layer authenticates /// the caller, then BrokerCore assigns this identity for all operations received -/// on that session. Dropping the session releases all object references it owns. +/// on that session. Dropping the session releases all object references it owns, +/// and with them the share of the core-wide budgets they held. pub struct BrokerSession { pub(crate) core: BrokerCore, /// Broker-assigned session identity. @@ -65,7 +67,18 @@ pub struct BrokerSession { /// Broker-entry-authenticated caller credential for this session. pub(crate) caller_credential: CallerCredential, /// Handles of the live object references owned by this session. + /// + /// The length of this vector is the session's live reference count, so the + /// per-session reference quota is enforced against the very state that + /// releases it and the two cannot drift apart. reference_handles: Mutex>, + /// Capacity in bytes reserved by this session's live pipes. + /// + /// Reservations share ownership of this counter because a pipe object may + /// outlive the session that created it: another worker can hold the + /// object's `Arc` across session teardown, and the release must still find + /// a live counter to credit. + pub(crate) reserved_pipe_capacity: Arc, } impl BrokerSession { @@ -80,6 +93,7 @@ impl BrokerSession { session_id, caller_credential, reference_handles: Mutex::new(Vec::new()), + reserved_pipe_capacity: Arc::new(AtomicUsize::new(0)), } } @@ -89,6 +103,12 @@ impl BrokerSession { .policy .principal_object_rights(self.caller_credential)?; let mut reference_handles = self.reference_handles.lock(); + // Charge the session before the core-wide table is locked, so a session + // that is already at its quota cannot make every other session contend + // for the shared lock to be told the core is full. + if reference_handles.len() >= self.core.limits.max_session_references { + return Err(BrokerError::ResourceExhausted); + } reference_handles .try_reserve(1) .map_err(|_| BrokerError::OutOfMemory)?; @@ -121,6 +141,13 @@ impl BrokerSession { .policy .principal_object_rights(self.caller_credential)?; let mut reference_handles = self.reference_handles.lock(); + if reference_handles + .len() + .checked_add(2) + .is_none_or(|count| count > self.core.limits.max_session_references) + { + return Err(BrokerError::ResourceExhausted); + } reference_handles .try_reserve(2) .map_err(|_| BrokerError::OutOfMemory)?; @@ -320,6 +347,7 @@ impl Drop for BrokerSession { #[cfg(test)] mod tests { + use alloc::sync::Arc; use core::sync::atomic::Ordering; use crate::{ @@ -329,11 +357,28 @@ mod tests { use litebox_broker_protocol::event::{EventConsumeMode, EventConsumption}; use litebox_broker_protocol::readiness::ReadinessFlags; + /// Core-wide ceilings used by every check below. + /// + /// They are exactly twice the per-session quotas, so two sessions spending + /// their full quota reach the ceilings and a third session is refused by + /// the backstop rather than by its own quota. + const TEST_MAX_REFERENCES: usize = 4; + const TEST_MAX_TOTAL_PIPE_CAPACITY: usize = 8; + /// Per-session quotas used by every check below. + /// + /// Two references is the smallest quota that still admits a pipe, whose two + /// endpoints are created together. + const TEST_MAX_SESSION_REFERENCES: usize = 2; + const TEST_MAX_SESSION_PIPE_CAPACITY: usize = 4; + /// `TEST_MAX_SESSION_PIPE_CAPACITY` in the width `pipe::create` accepts. + const TEST_SESSION_PIPE_CAPACITY_REQUEST: u64 = 4; + #[test] fn object_reference_lifecycle_uses_public_core_constructor_once() { let broker = BrokerCore::new_with_limits( PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), - BrokerCoreLimits::new(2, 4), + BrokerCoreLimits::new(TEST_MAX_REFERENCES, TEST_MAX_TOTAL_PIPE_CAPACITY) + .with_session_quotas(TEST_MAX_SESSION_REFERENCES, TEST_MAX_SESSION_PIPE_CAPACITY), ) .unwrap(); @@ -343,9 +388,16 @@ mod tests { check_pipe_reader_closure(&broker); check_corrupt_index_fails_without_mutation(&broker); check_corrupt_index_does_not_break_teardown(&broker); + check_reference_quota_is_per_session(&broker); + check_pipe_capacity_quota_is_per_session(&broker); + check_session_drop_releases_quotas(&broker); + check_pipe_capacity_is_released_when_endpoints_are_refused(&broker); + // Handle allocation is exhausted for the rest of the process once this + // check runs, so it must stay last. check_pair_handle_exhaustion(&broker); assert!(broker.references.read().is_empty()); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); } fn check_event_reference_lifecycle(broker: &BrokerCore) { @@ -530,6 +582,180 @@ mod tests { assert!(broker.references.read().is_empty()); } + /// One session spending its whole reference quota must not stop another + /// session from creating objects. This is the regression test for the + /// core-wide budgets being reachable by a single session. + fn check_reference_quota_is_per_session(broker: &BrokerCore) { + let greedy = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let neighbor = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + + let greedy_first = crate::event::create(&greedy, 0).unwrap(); + let greedy_second = crate::event::create(&greedy, 0).unwrap(); + // The greedy session stops at its own quota while the core-wide + // ceiling still has room for every other session's share. + assert_eq!( + crate::event::create(&greedy, 0), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(broker.references.read().len(), TEST_MAX_SESSION_REFERENCES); + + let neighbor_first = crate::event::create(&neighbor, 0).unwrap(); + let neighbor_second = crate::event::create(&neighbor, 0).unwrap(); + assert_eq!(broker.references.read().len(), TEST_MAX_REFERENCES); + + // With every quota spent, the core-wide ceiling is the backstop. + let latecomer = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + assert_eq!( + crate::event::create(&latecomer, 0), + Err(BrokerError::ResourceExhausted) + ); + + // Closing a reference returns quota to the session that held it. + assert_eq!(greedy.close_object_reference(greedy_first), Ok(())); + let greedy_third = crate::event::create(&greedy, 0).unwrap(); + + assert_eq!(greedy.close_object_reference(greedy_second), Ok(())); + assert_eq!(greedy.close_object_reference(greedy_third), Ok(())); + assert_eq!(neighbor.close_object_reference(neighbor_first), Ok(())); + assert_eq!(neighbor.close_object_reference(neighbor_second), Ok(())); + assert!(broker.references.read().is_empty()); + } + + /// The same isolation must hold for reserved pipe capacity, which is + /// tracked in a counter rather than in the reference table. + fn check_pipe_capacity_quota_is_per_session(broker: &BrokerCore) { + let greedy = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let neighbor = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + + let (greedy_reader, greedy_writer) = + crate::pipe::create(&greedy, TEST_SESSION_PIPE_CAPACITY_REQUEST, 2).unwrap(); + assert_eq!( + greedy.reserved_pipe_capacity.load(Ordering::Relaxed), + TEST_MAX_SESSION_PIPE_CAPACITY + ); + assert_eq!( + broker.reserved_pipe_capacity.load(Ordering::Relaxed), + TEST_MAX_SESSION_PIPE_CAPACITY + ); + // The greedy session is refused by its own quota, without charging the + // core-wide counter it would otherwise have consumed. + assert_eq!( + crate::pipe::create(&greedy, 1, 1), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!( + broker.reserved_pipe_capacity.load(Ordering::Relaxed), + TEST_MAX_SESSION_PIPE_CAPACITY + ); + + let (neighbor_reader, neighbor_writer) = + crate::pipe::create(&neighbor, TEST_SESSION_PIPE_CAPACITY_REQUEST, 2).unwrap(); + assert_eq!( + broker.reserved_pipe_capacity.load(Ordering::Relaxed), + TEST_MAX_TOTAL_PIPE_CAPACITY + ); + + // The core-wide ceiling backstops a session that is within its quota, + // and the refused reservation leaves no charge behind on either counter. + let latecomer = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + assert_eq!( + crate::pipe::create(&latecomer, 1, 1), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(latecomer.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + assert_eq!( + broker.reserved_pipe_capacity.load(Ordering::Relaxed), + TEST_MAX_TOTAL_PIPE_CAPACITY + ); + + // Capacity is held until both endpoints are gone, then credited back to + // the session and to the core together. + assert_eq!(greedy.close_object_reference(greedy_reader), Ok(())); + assert_eq!( + greedy.reserved_pipe_capacity.load(Ordering::Relaxed), + TEST_MAX_SESSION_PIPE_CAPACITY + ); + assert_eq!(greedy.close_object_reference(greedy_writer), Ok(())); + assert_eq!(greedy.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + assert_eq!( + broker.reserved_pipe_capacity.load(Ordering::Relaxed), + TEST_MAX_SESSION_PIPE_CAPACITY + ); + + assert_eq!(neighbor.close_object_reference(neighbor_reader), Ok(())); + assert_eq!(neighbor.close_object_reference(neighbor_writer), Ok(())); + assert_eq!(neighbor.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + assert!(broker.references.read().is_empty()); + } + + /// Tearing a session down must return everything it held, so a session that + /// stops making progress cannot pin a share of the core-wide budgets past + /// its own lifetime. + fn check_session_drop_releases_quotas(broker: &BrokerCore) { + // Twice, so the second session proves the first really gave the + // core-wide budgets back rather than merely stopping using them. + for _ in 0..2 { + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let (reader, writer) = + crate::pipe::create(&session, TEST_SESSION_PIPE_CAPACITY_REQUEST, 2).unwrap(); + assert_ne!(reader, writer); + assert_eq!(broker.references.read().len(), 2); + assert_eq!( + broker.reserved_pipe_capacity.load(Ordering::Relaxed), + TEST_MAX_SESSION_PIPE_CAPACITY + ); + // Outlives the session, so the per-session charge stays observable + // across teardown. + let session_capacity = Arc::clone(&session.reserved_pipe_capacity); + assert_eq!( + session_capacity.load(Ordering::Relaxed), + TEST_MAX_SESSION_PIPE_CAPACITY + ); + + drop(session); + + assert!(broker.references.read().is_empty()); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + assert_eq!(session_capacity.load(Ordering::Relaxed), 0); + } + } + + /// Pipe capacity is reserved before the endpoint references exist, so a + /// session that is at its reference quota must get the reservation back. + fn check_pipe_capacity_is_released_when_endpoints_are_refused(broker: &BrokerCore) { + let session = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let first = crate::event::create(&session, 0).unwrap(); + let second = crate::event::create(&session, 0).unwrap(); + + assert_eq!( + crate::pipe::create(&session, TEST_SESSION_PIPE_CAPACITY_REQUEST, 2), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(session.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + + assert_eq!(session.close_object_reference(first), Ok(())); + assert_eq!(session.close_object_reference(second), Ok(())); + assert!(broker.references.read().is_empty()); + } + fn check_pair_handle_exhaustion(broker: &BrokerCore) { let session = broker .create_session(CallerCredential::Unauthenticated) From c0e24593f93f972ea2b098cb80db30bdf7e88e25 Mon Sep 17 00:00:00 2001 From: dywongcloud <274333120+dywongcloud@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:58:34 +0000 Subject: [PATCH 137/319] Pin the quota invariants review found unguarded Follow-up to the per-session quotas, all from reviewing that change: - Restore coverage of the core-wide ceiling on the reference-pair path. Charging the session first short-circuits it everywhere else in the suite, so that branch could be deleted outright and every test stayed green. The new assertion catches a session well inside its own quota being refused by the ceiling, which is also the only place `with_session_quotas`'s "the smaller of the two binds" contract is exercised on the pair path. - Record why `first` and `second` must stay parameters in `create_object_reference_pair`: they are dropped after every lock guard in the body, which is what keeps a failure return from destroying pipe endpoints under a reference-index lock. - Record that `insert_object_reference` must stay infallible and that `ObjectReference` must never gain a destructor, since the discarded insert result would be dropped under both index locks. - State the counter invariant on `PipeCapacityReservation`: the two charges are not jointly atomic and need not be, because the only transient is over-counting. - Scope the identity lock in `create_session`, which session construction now allocates under. - Give `with_session_quotas` an example, which also documents how a knowingly single-session deployment opts back into the whole core. - Correct `MAX_TRACKED_READINESS_OBJECTS`'s comment: an association is one session, so it now sits above the default per-session quota rather than matching the core-wide reference limit. --- litebox_broker_core/src/lib.rs | 30 ++++++++++++++++++++++------ litebox_broker_core/src/pipe.rs | 7 +++++++ litebox_broker_core/src/session.rs | 20 +++++++++++++++++++ litebox_broker_host/src/readiness.rs | 9 +++++---- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 54116cbc48..483ef8b3e3 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -95,7 +95,19 @@ impl BrokerCoreLimits { /// /// A quota above its global ceiling is accepted rather than rejected: the /// ceiling is still enforced, so the effective quota is whichever of the - /// two is smaller. + /// two is smaller. A deployment that knowingly serves one session can + /// therefore hand it the whole core by raising each quota to its ceiling. + /// + /// ``` + /// use litebox_broker_core::BrokerCoreLimits; + /// + /// let shared = BrokerCoreLimits::DEFAULT; + /// assert_eq!(shared.max_session_references, 1024); + /// + /// let single_tenant = + /// shared.with_session_quotas(shared.max_references, shared.max_total_pipe_capacity); + /// assert_eq!(single_tenant.max_session_references, shared.max_references); + /// ``` #[must_use] pub const fn with_session_quotas( self, @@ -184,11 +196,17 @@ impl BrokerCore { /// Allocates broker authority state for one authenticated caller session. pub fn create_session(&self, caller_credential: CallerCredential) -> Result { - let mut next_session_id = self.next_session_id.write(); - let session_id = *next_session_id; - *next_session_id = session_id - .checked_add(1) - .ok_or(BrokerError::ResourceExhausted)?; + // Release the identity lock before building the session: session + // construction allocates the per-session capacity counter, and every + // session creation contends for this lock. + let session_id = { + let mut next_session_id = self.next_session_id.write(); + let session_id = *next_session_id; + *next_session_id = session_id + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted)?; + session_id + }; Ok(BrokerSession::new( self.clone(), session::SessionId(session_id), diff --git a/litebox_broker_core/src/pipe.rs b/litebox_broker_core/src/pipe.rs index 5586a9d1dc..e7f544a54c 100644 --- a/litebox_broker_core/src/pipe.rs +++ b/litebox_broker_core/src/pipe.rs @@ -223,6 +223,13 @@ impl Drop for CapacityCharge { /// Both charges are released together when the pipe's shared state is dropped, /// which happens once the last reference to either endpoint is closed or its /// owning session is torn down. +/// +/// Each counter is at every instant at least the total capacity of the live +/// pipes it covers, because a charge is taken before the pipe state that owns +/// it exists and released after that state is destroyed. The two charges are +/// therefore not jointly atomic, and do not need to be: the only transient a +/// concurrent creator can observe is over-counting, which can refuse it +/// slightly too early but can never admit it too late. struct PipeCapacityReservation { _session_charge: CapacityCharge, _core_charge: CapacityCharge, diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index 6bf9e6a006..976c888d5c 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -136,6 +136,11 @@ impl BrokerSession { first: ObjectEntry, second: ObjectEntry, ) -> Result<(ObjectHandle, ObjectHandle)> { + // `first` and `second` are parameters, so they are dropped after every + // lock guard declared in this body. That is what makes each failure + // return below destroy the pipe endpoints, which take the pipe-state + // lock, only once both reference-index locks are released. Do not + // rebind them to locals declared after either guard. let rights = self .core .policy @@ -187,6 +192,12 @@ impl BrokerSession { rights: ObjectRights, ) { let session_reference_index = reference_handles.len(); + // Both callers check `contains_key` first, so this always replaces + // nothing. Anything dropped here would be destroyed while both the + // session's reference-handle lock and the core-wide reference lock are + // held, so `ObjectReference` must never gain a destructor that takes + // either. Keeping this insert infallible is also what lets the caller's + // pair loop run without a rollback path. references.insert( handle, ObjectReference { @@ -615,6 +626,15 @@ mod tests { crate::event::create(&latecomer, 0), Err(BrokerError::ResourceExhausted) ); + // Same backstop on the pair path, which admits two references at once. + // No pipe is alive here, so the capacity ceiling cannot short-circuit + // it, and the latecomer is well inside its own quota. + assert_eq!( + crate::pipe::create(&latecomer, 1, 1), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(latecomer.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); // Closing a reference returns quota to the session that held it. assert_eq!(greedy.close_object_reference(greedy_first), Ok(())); diff --git a/litebox_broker_host/src/readiness.rs b/litebox_broker_host/src/readiness.rs index d0a51b5449..f38e774080 100644 --- a/litebox_broker_host/src/readiness.rs +++ b/litebox_broker_host/src/readiness.rs @@ -41,10 +41,11 @@ use thiserror::Error; /// Maximum number of objects one association tracks readiness for. /// -/// This matches the default broker-core reference limit, so a source that -/// retires an object as its backend resource is released stays well inside it. -/// It exists so that a source which does not, or a deployment that raises the -/// core limit, cannot grow publication state without limit. +/// One association is one broker-core session, so this sits above the default +/// per-session reference quota rather than matching it exactly, and a source +/// that retires an object as its backend resource is released stays well +/// inside it. It exists so that a source which does not, or a deployment that +/// raises the core limits, cannot grow publication state without limit. pub const MAX_TRACKED_READINESS_OBJECTS: usize = 4096; /// Error returned when readiness state cannot record an update. From ebf77adffa82cf7d9425c4aa605ec93d52953ec6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 23:16:07 +0000 Subject: [PATCH 138/319] Add secure Next.js/TypeScript BD operating system dashboard Converts the standalone Tencent Cloud NA BD Operating System HTML dashboard into a self-contained Next.js 16 + TypeScript application under tencent-bd-dashboard/, unrelated to the litebox codebase. Implements the PRD's four requirements: - Account pipeline data persisted in SQLite (Drizzle ORM) instead of localStorage. - Service catalog sync against the Tencent Cloud Billing API (DescribeProducts), with a from-scratch TC3-HMAC-SHA256 signer. - Locale routing for en/zh-Hans/zh-Hant with safe auto-translation: deterministic script conversion between Simplified and Traditional, and machine translation across the English/Chinese boundary via the TMT API, both tracked with per-translation origin/status/staleness so machine output is never presented as reviewed. - A single Zod + Drizzle schema (src/domain) as the source of truth for every persisted and submitted value. Also ports all six original dashboard tabs (service catalog, account pipeline with a per-account research workspace, development SOP, next-step board, entry playbooks, weekly CEO review) plus authentication, session management, CSRF protection, rate limiting, RBAC, an audit log, and an admin panel, none of which the original client-only HTML file had. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UXp2bUcbFjHSeQMTbw6RVo --- tencent-bd-dashboard/.env.example | 50 + tencent-bd-dashboard/.gitignore | 30 + tencent-bd-dashboard/README.md | 172 + tencent-bd-dashboard/drizzle.config.ts | 16 + .../drizzle/0000_slow_trish_tilby.sql | 277 + .../drizzle/meta/0000_snapshot.json | 2030 +++ .../drizzle/meta/_journal.json | 13 + tencent-bd-dashboard/next.config.ts | 27 + tencent-bd-dashboard/package-lock.json | 3949 +++++ tencent-bd-dashboard/package.json | 40 + .../scripts/generate-zh-hant.ts | 60 + tencent-bd-dashboard/scripts/verify.ts | 112 + .../(app)/accounts/AccountsClient.tsx | 225 + .../(app)/accounts/[id]/ResearchForm.tsx | 130 + .../app/[locale]/(app)/accounts/[id]/page.tsx | 43 + .../src/app/[locale]/(app)/accounts/page.tsx | 73 + .../app/[locale]/(app)/admin/AdminClient.tsx | 366 + .../src/app/[locale]/(app)/admin/page.tsx | 67 + .../app/[locale]/(app)/board/BoardClient.tsx | 216 + .../src/app/[locale]/(app)/board/page.tsx | 56 + .../src/app/[locale]/(app)/layout.tsx | 61 + .../src/app/[locale]/(app)/page.tsx | 11 + .../(app)/playbooks/PlaybooksClient.tsx | 132 + .../src/app/[locale]/(app)/playbooks/page.tsx | 47 + .../(app)/products/ProductsClient.tsx | 704 + .../src/app/[locale]/(app)/products/page.tsx | 194 + .../app/[locale]/(app)/sop/SopChecklist.tsx | 81 + .../src/app/[locale]/(app)/sop/page.tsx | 38 + .../[locale]/(app)/weekly/WeeklyClient.tsx | 91 + .../src/app/[locale]/(app)/weekly/page.tsx | 32 + .../src/app/[locale]/error.tsx | 34 + .../src/app/[locale]/layout.tsx | 51 + .../src/app/[locale]/login/LoginForm.tsx | 45 + .../src/app/[locale]/login/page.tsx | 38 + .../src/app/[locale]/not-found.tsx | 12 + tencent-bd-dashboard/src/app/globals.css | 668 + tencent-bd-dashboard/src/app/icon.svg | 4 + .../src/components/CsrfField.tsx | 22 + .../src/components/FilterForm.tsx | 47 + .../src/components/LocaleSwitcher.tsx | 45 + tencent-bd-dashboard/src/components/Modal.tsx | 51 + .../src/components/NavTabs.tsx | 42 + .../src/components/SignOutForm.tsx | 15 + .../src/components/SubmitButton.tsx | 26 + .../src/components/TranslateField.tsx | 88 + .../src/components/fields.tsx | 90 + tencent-bd-dashboard/src/db/client.ts | 72 + tencent-bd-dashboard/src/db/migrate.ts | 13 + tencent-bd-dashboard/src/db/reset.ts | 13 + tencent-bd-dashboard/src/db/schema.ts | 635 + tencent-bd-dashboard/src/db/seed-corpus.ts | 314 + .../src/db/seed-data/accounts.json | 21 + .../src/db/seed-data/motions.json | 132 + .../src/db/seed-data/products.json | 13396 ++++++++++++++++ .../src/db/seed-data/sop.json | 12 + .../src/db/seed-data/todos.json | 42 + tencent-bd-dashboard/src/db/seed.ts | 73 + tencent-bd-dashboard/src/domain/enums.ts | 166 + .../src/domain/review-questions.ts | 33 + tencent-bd-dashboard/src/domain/schemas.ts | 445 + .../src/i18n/messages/en.json | 494 + .../src/i18n/messages/zh-Hans.json | 494 + .../src/i18n/messages/zh-Hant.json | 494 + tencent-bd-dashboard/src/i18n/navigation.ts | 12 + tencent-bd-dashboard/src/i18n/request.ts | 45 + tencent-bd-dashboard/src/i18n/routing.ts | 24 + tencent-bd-dashboard/src/lib/auth/guard.ts | 191 + .../src/lib/auth/password-policy.ts | 96 + tencent-bd-dashboard/src/lib/auth/rbac.ts | 93 + tencent-bd-dashboard/src/lib/auth/session.ts | 224 + tencent-bd-dashboard/src/lib/env.ts | 101 + .../src/lib/security/audit.ts | 143 + .../src/lib/security/crypto.ts | 180 + .../src/lib/security/csrf-constants.ts | 8 + tencent-bd-dashboard/src/lib/security/csrf.ts | 116 + .../src/lib/security/rate-limit.ts | 166 + .../src/lib/security/request.ts | 95 + tencent-bd-dashboard/src/proxy.ts | 209 + .../src/server/actions/accounts.ts | 99 + .../src/server/actions/admin.ts | 129 + .../src/server/actions/auth.ts | 227 + .../src/server/actions/board.ts | 182 + .../src/server/actions/data-export.ts | 109 + .../src/server/actions/products.ts | 228 + .../src/server/actions/read.ts | 54 + .../src/server/actions/review.ts | 33 + .../src/server/actions/shared.ts | 27 + .../src/server/actions/translation.ts | 92 + .../src/server/catalog/sync.ts | 123 + .../src/server/data/accounts.ts | 115 + tencent-bd-dashboard/src/server/data/board.ts | 145 + .../src/server/data/products.ts | 192 + .../src/server/data/review.ts | 34 + .../src/server/data/translations.ts | 97 + tencent-bd-dashboard/src/server/data/users.ts | 74 + .../src/server/tencent/catalog-client.ts | 97 + .../src/server/tencent/signer.ts | 157 + .../src/server/tencent/translate-client.ts | 62 + .../src/server/translation/fields.ts | 44 + .../src/server/translation/service.ts | 150 + .../src/server/translation/source.ts | 50 + tencent-bd-dashboard/tsconfig.json | 46 + 102 files changed, 31739 insertions(+) create mode 100644 tencent-bd-dashboard/.env.example create mode 100644 tencent-bd-dashboard/.gitignore create mode 100644 tencent-bd-dashboard/README.md create mode 100644 tencent-bd-dashboard/drizzle.config.ts create mode 100644 tencent-bd-dashboard/drizzle/0000_slow_trish_tilby.sql create mode 100644 tencent-bd-dashboard/drizzle/meta/0000_snapshot.json create mode 100644 tencent-bd-dashboard/drizzle/meta/_journal.json create mode 100644 tencent-bd-dashboard/next.config.ts create mode 100644 tencent-bd-dashboard/package-lock.json create mode 100644 tencent-bd-dashboard/package.json create mode 100644 tencent-bd-dashboard/scripts/generate-zh-hant.ts create mode 100644 tencent-bd-dashboard/scripts/verify.ts create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/accounts/AccountsClient.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/accounts/[id]/ResearchForm.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/accounts/[id]/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/accounts/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/admin/AdminClient.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/admin/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/board/BoardClient.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/board/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/layout.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/playbooks/PlaybooksClient.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/playbooks/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/products/ProductsClient.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/products/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/sop/SopChecklist.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/sop/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/weekly/WeeklyClient.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/(app)/weekly/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/error.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/layout.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/login/LoginForm.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/login/page.tsx create mode 100644 tencent-bd-dashboard/src/app/[locale]/not-found.tsx create mode 100644 tencent-bd-dashboard/src/app/globals.css create mode 100644 tencent-bd-dashboard/src/app/icon.svg create mode 100644 tencent-bd-dashboard/src/components/CsrfField.tsx create mode 100644 tencent-bd-dashboard/src/components/FilterForm.tsx create mode 100644 tencent-bd-dashboard/src/components/LocaleSwitcher.tsx create mode 100644 tencent-bd-dashboard/src/components/Modal.tsx create mode 100644 tencent-bd-dashboard/src/components/NavTabs.tsx create mode 100644 tencent-bd-dashboard/src/components/SignOutForm.tsx create mode 100644 tencent-bd-dashboard/src/components/SubmitButton.tsx create mode 100644 tencent-bd-dashboard/src/components/TranslateField.tsx create mode 100644 tencent-bd-dashboard/src/components/fields.tsx create mode 100644 tencent-bd-dashboard/src/db/client.ts create mode 100644 tencent-bd-dashboard/src/db/migrate.ts create mode 100644 tencent-bd-dashboard/src/db/reset.ts create mode 100644 tencent-bd-dashboard/src/db/schema.ts create mode 100644 tencent-bd-dashboard/src/db/seed-corpus.ts create mode 100644 tencent-bd-dashboard/src/db/seed-data/accounts.json create mode 100644 tencent-bd-dashboard/src/db/seed-data/motions.json create mode 100644 tencent-bd-dashboard/src/db/seed-data/products.json create mode 100644 tencent-bd-dashboard/src/db/seed-data/sop.json create mode 100644 tencent-bd-dashboard/src/db/seed-data/todos.json create mode 100644 tencent-bd-dashboard/src/db/seed.ts create mode 100644 tencent-bd-dashboard/src/domain/enums.ts create mode 100644 tencent-bd-dashboard/src/domain/review-questions.ts create mode 100644 tencent-bd-dashboard/src/domain/schemas.ts create mode 100644 tencent-bd-dashboard/src/i18n/messages/en.json create mode 100644 tencent-bd-dashboard/src/i18n/messages/zh-Hans.json create mode 100644 tencent-bd-dashboard/src/i18n/messages/zh-Hant.json create mode 100644 tencent-bd-dashboard/src/i18n/navigation.ts create mode 100644 tencent-bd-dashboard/src/i18n/request.ts create mode 100644 tencent-bd-dashboard/src/i18n/routing.ts create mode 100644 tencent-bd-dashboard/src/lib/auth/guard.ts create mode 100644 tencent-bd-dashboard/src/lib/auth/password-policy.ts create mode 100644 tencent-bd-dashboard/src/lib/auth/rbac.ts create mode 100644 tencent-bd-dashboard/src/lib/auth/session.ts create mode 100644 tencent-bd-dashboard/src/lib/env.ts create mode 100644 tencent-bd-dashboard/src/lib/security/audit.ts create mode 100644 tencent-bd-dashboard/src/lib/security/crypto.ts create mode 100644 tencent-bd-dashboard/src/lib/security/csrf-constants.ts create mode 100644 tencent-bd-dashboard/src/lib/security/csrf.ts create mode 100644 tencent-bd-dashboard/src/lib/security/rate-limit.ts create mode 100644 tencent-bd-dashboard/src/lib/security/request.ts create mode 100644 tencent-bd-dashboard/src/proxy.ts create mode 100644 tencent-bd-dashboard/src/server/actions/accounts.ts create mode 100644 tencent-bd-dashboard/src/server/actions/admin.ts create mode 100644 tencent-bd-dashboard/src/server/actions/auth.ts create mode 100644 tencent-bd-dashboard/src/server/actions/board.ts create mode 100644 tencent-bd-dashboard/src/server/actions/data-export.ts create mode 100644 tencent-bd-dashboard/src/server/actions/products.ts create mode 100644 tencent-bd-dashboard/src/server/actions/read.ts create mode 100644 tencent-bd-dashboard/src/server/actions/review.ts create mode 100644 tencent-bd-dashboard/src/server/actions/shared.ts create mode 100644 tencent-bd-dashboard/src/server/actions/translation.ts create mode 100644 tencent-bd-dashboard/src/server/catalog/sync.ts create mode 100644 tencent-bd-dashboard/src/server/data/accounts.ts create mode 100644 tencent-bd-dashboard/src/server/data/board.ts create mode 100644 tencent-bd-dashboard/src/server/data/products.ts create mode 100644 tencent-bd-dashboard/src/server/data/review.ts create mode 100644 tencent-bd-dashboard/src/server/data/translations.ts create mode 100644 tencent-bd-dashboard/src/server/data/users.ts create mode 100644 tencent-bd-dashboard/src/server/tencent/catalog-client.ts create mode 100644 tencent-bd-dashboard/src/server/tencent/signer.ts create mode 100644 tencent-bd-dashboard/src/server/tencent/translate-client.ts create mode 100644 tencent-bd-dashboard/src/server/translation/fields.ts create mode 100644 tencent-bd-dashboard/src/server/translation/service.ts create mode 100644 tencent-bd-dashboard/src/server/translation/source.ts create mode 100644 tencent-bd-dashboard/tsconfig.json diff --git a/tencent-bd-dashboard/.env.example b/tencent-bd-dashboard/.env.example new file mode 100644 index 0000000000..f2b61d0841 --- /dev/null +++ b/tencent-bd-dashboard/.env.example @@ -0,0 +1,50 @@ +# --------------------------------------------------------------------------- +# Tencent Cloud NA BD Operating System - environment configuration +# Copy to .env.local and fill in. .env.local is git-ignored. +# --------------------------------------------------------------------------- + +# --- Required in every environment ------------------------------------------ + +# 32+ byte secret used to derive session-token and CSRF-token HMAC keys. +# Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" +APP_SECRET= + +# Absolute or project-relative path to the SQLite database file. +DATABASE_PATH=./data/bd-os.db + +# --- Required in production only --------------------------------------------- + +# Canonical public origin, e.g. https://bd.example.com +# Used for strict same-origin checks on every mutating request. +APP_ORIGIN=http://localhost:3000 + +# --- Bootstrap administrator (consumed once, on first `db:seed`) -------------- + +BOOTSTRAP_ADMIN_EMAIL=admin@example.com +# Must satisfy the password policy: >= 12 chars, upper, lower, digit, symbol. +BOOTSTRAP_ADMIN_PASSWORD= + +# --- Tencent Cloud upstream APIs (optional) ---------------------------------- +# When unset, the service catalog serves the bundled local corpus and the +# translation service falls back to deterministic script conversion only. +# These are read server-side only and are never exposed to the browser. + +TENCENTCLOUD_SECRET_ID= +TENCENTCLOUD_SECRET_KEY= + +# Region used for both the product catalog and machine-translation endpoints. +TENCENTCLOUD_REGION=ap-singapore + +# Machine-translation project id (0 is the default project). +TENCENTCLOUD_TMT_PROJECT_ID=0 + +# --- Tuning (all optional, defaults shown) ------------------------------------ + +# Session idle timeout and absolute lifetime, in seconds. +SESSION_IDLE_TTL_SECONDS=3600 +SESSION_ABSOLUTE_TTL_SECONDS=43200 + +# Failed sign-in attempts before an account is temporarily locked, and the +# lockout duration in seconds. +LOGIN_MAX_ATTEMPTS=5 +LOGIN_LOCKOUT_SECONDS=900 diff --git a/tencent-bd-dashboard/.gitignore b/tencent-bd-dashboard/.gitignore new file mode 100644 index 0000000000..edd434693d --- /dev/null +++ b/tencent-bd-dashboard/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +/node_modules + +# Next.js build output +/.next/ +/out/ + +# Local environment (contains APP_SECRET and other credentials) +.env +.env.local +.env*.local + +# SQLite database and its WAL/SHM sidecar files +/data/ +*.db +*.db-journal +*.db-wal +*.db-shm + +# TypeScript incremental build info +*.tsbuildinfo +next-env.d.ts + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS +.DS_Store diff --git a/tencent-bd-dashboard/README.md b/tencent-bd-dashboard/README.md new file mode 100644 index 0000000000..801a42afe0 --- /dev/null +++ b/tencent-bd-dashboard/README.md @@ -0,0 +1,172 @@ +# Tencent Cloud NA BD Operating System + +A secure Next.js + TypeScript port of the original single-file HTML BD +dashboard, rebuilt around the four requirements in the PRD: + +1. **Account Pipelines** — stored in a SQLite database (not `localStorage`). +2. **Service Catalog Search / 产品情报库** — consumes an upstream Tencent + Cloud product API. +3. **Locales / Translation** — safe auto-translation between English and + Chinese (Simplified and Traditional). +4. **Typesafe schema** — a single Drizzle schema, inferred TypeScript types, + and Zod validation at every input boundary. + +It keeps all six tabs from the original dashboard (Service Catalog, Account +Pipeline, Development SOP, Next-Step Board, Entry Playbooks, Weekly CEO +Review) plus authentication, role-based access control, and an admin panel — +none of which the source HTML file had, since it ran entirely client-side +with no backend. + +## Quick start + +```bash +npm install +cp .env.example .env.local # then fill in APP_SECRET and bootstrap admin credentials +npm run db:seed # migrates the DB and loads the 181-product corpus +npm run dev # http://localhost:3000 +``` + +Generate `APP_SECRET`: + +```bash +node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))" +``` + +Sign in with the `BOOTSTRAP_ADMIN_EMAIL` / `BOOTSTRAP_ADMIN_PASSWORD` you set +in `.env.local` before running `db:seed` (the password must satisfy the +policy — 12+ characters with upper, lower, digit, and symbol). + +## Scripts + +| Command | Purpose | +|---|---| +| `npm run dev` | Development server | +| `npm run build` | Production build (fails on type errors) | +| `npm start` | Run the production build | +| `npm run typecheck` | `tsc --noEmit` | +| `npm run db:migrate` | Apply pending Drizzle migrations | +| `npm run db:seed` | Migrate + load the initial corpus + create the bootstrap admin | +| `npm run db:reset` | Reload the initial corpus (users/sessions/audit log untouched) | + +## Architecture + +``` +src/ + domain/ Enum vocabularies + Zod schemas (the "typesafe schema" layer) + db/ Drizzle schema, SQLite client, migrations, seed corpus + lib/ + auth/ Sessions, RBAC, the requireRead/requireMutation guard, password policy + security/ scrypt hashing, CSRF, rate limiting, audit log, request context + env.ts Validated environment configuration + i18n/ next-intl routing + message catalogs (en, zh-Hans, zh-Hant) + server/ + data/ Query layer (one module per entity) + actions/ Server Actions -- the only mutation surface + tencent/ TC3-HMAC-SHA256 signer + Tencent Cloud API clients + catalog/ Upstream product-catalog sync + translation/ Script conversion (zh-Hans <-> zh-Hant) + machine translation (en <-> zh) + components/ Shared client/server UI primitives + app/[locale]/ Routes (App Router, locale-prefixed throughout) +``` + +### Security model + +- **Auth**: opaque server-side sessions (random token, hashed at rest), + scrypt password hashing, account lockout after repeated failures, + idle + absolute session timeouts, session revocation on password change. +- **CSRF**: signed double-submit tokens bound to the session id, minted once + at login (Next.js forbids writing cookies during a page render — only + Server Actions/Route Handlers may — so the token is never re-minted on a + GET). +- **Origin checks**: every mutation independently verifies the `Origin` + header against an allowlist, on top of the CSRF token. +- **Rate limiting**: persisted, fixed-window, per-bucket (login, mutation, + translation, catalog sync, export), so it survives a restart and holds + across workers. +- **RBAC**: three roles (`admin`, `editor`, `viewer`) mapped to named + permissions, checked identically at the Server Action layer for every + request — client-side tab hiding is a convenience, never the boundary. +- **Audit log**: append-only record of every auth event and business + mutation. +- **Headers**: a per-request CSP nonce, `X-Frame-Options`, + `Permissions-Policy` (fully denied), `Strict-Transport-Security`, and + `Cross-Origin-Opener/Resource-Policy`, all set in `src/proxy.ts` + (Next.js 16 renamed the `middleware.ts` convention to `proxy.ts`). + +**Deployment note**: `Strict-Transport-Security` is sent whenever +`NODE_ENV=production`. Serve this behind real TLS (terminate HTTPS in front +of it) — testing the production build over plain HTTP in a browser that +caches HSTS for `localhost` will cause the browser to force-upgrade +subsequent requests to HTTPS and fail to connect. `npm run dev` does not set +this header. + +### Data model + +SQLite via `better-sqlite3` + Drizzle ORM. Every enum column is typed against +the closed vocabularies in `src/domain/enums.ts` and validated at every +write boundary with the matching Zod schema in `src/domain/schemas.ts` — the +same source of truth Drizzle's inferred `Product`/`Account`/etc. types come +from, so the persisted shape, the compile-time type, and the runtime +validator cannot drift from one another. + +Evidence rows (`product_evidence`) are normalised out of the product row +into their own table, so the Evidence Studio's "which claims may I actually +state to a customer" question is a query, not a document scan. + +### Upstream Tencent Cloud integration + +Two live API integrations, both behind `TENCENTCLOUD_SECRET_ID`/ +`TENCENTCLOUD_SECRET_KEY` (unset by default — the app works fully on the +bundled local corpus without them): + +- **Catalog sync** (`src/server/catalog/sync.ts`): calls the Billing API's + `DescribeProducts` action (the one stable, generally-available Tencent + Cloud API that returns a product code/name catalog — there is no public + "marketing catalog" API). A sync only attaches an upstream code to an + existing product by name match, or creates a minimal stub row; it never + overwrites BD intelligence a rep has already written. +- **Machine translation** (`src/server/translation/service.ts`): calls the + TMT `TextTranslate` action for any `en <-> zh` pair. Simplified/Traditional + conversion never calls this API — it is done locally and deterministically + with `opencc-js`. + +Both go through `src/server/tencent/signer.ts`, a from-scratch implementation +of Tencent Cloud's TC3-HMAC-SHA256 request signature. + +### Translation safety + +The Evidence Studio's whole discipline — never state an unsupported claim — +extends to translation: every stored translation carries an `origin` +(`human` / `machine` / `script-conversion`) and a `status` +(`draft` / `needs-review` / `approved`), plus a hash of the source text it +was produced from. A source edit invalidates the translation instead of +silently serving it stale, and the UI marks machine output as unreviewed +until an admin approves it in **Administration → Translations**. + +## Known scope trims + +Built to be genuinely complete against the PRD and a faithful, secure port +of all six original tabs — a few original features were deliberately left +out or simplified given the size of the source app, noted here rather than +silently dropped: + +- **Bulk JSON import** (the original's "导入更新" button): export is fully + implemented; a validated bulk-import counterpart was scoped out as + lower-value relative to its implementation cost (it would need its own + comprehensive per-row Zod schema mirroring every entity). `db:reset` + covers "restore the known-good corpus." +- **CSV export**: only the whole-database JSON export is implemented. +- **Kanban drag-and-drop**: the Next-Step Board moves a card between columns + via a "move to" select rather than pointer drag-and-drop — same data model + (`position` column), simpler and keyboard-accessible; a drag interaction + could be layered on later without a schema change. +- **ESLint**: Next.js 16 removed the built-in `next lint`/`eslint` build + integration; static checking here is `npm run typecheck` (strict mode, + `noUncheckedIndexedAccess`) rather than a separate lint config. + +## Environment variables + +See `.env.example` for the full list with descriptions. Only `APP_SECRET` is +required to start the app; everything else has a safe default or degrades +gracefully (no Tencent credentials → local corpus + script-conversion-only +translation; no `APP_ORIGIN` in development → localhost is allowed). diff --git a/tencent-bd-dashboard/drizzle.config.ts b/tencent-bd-dashboard/drizzle.config.ts new file mode 100644 index 0000000000..4315ecb9cf --- /dev/null +++ b/tencent-bd-dashboard/drizzle.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'drizzle-kit'; + +/** + * Drizzle Kit is used to generate SQL migrations from `src/db/schema.ts`. + * Runtime migration is applied by `src/db/migrate.ts`, not by this config. + */ +export default defineConfig({ + dialect: 'sqlite', + schema: './src/db/schema.ts', + out: './drizzle', + dbCredentials: { + url: process.env.DATABASE_PATH ?? './data/bd-os.db', + }, + strict: true, + verbose: true, +}); diff --git a/tencent-bd-dashboard/drizzle/0000_slow_trish_tilby.sql b/tencent-bd-dashboard/drizzle/0000_slow_trish_tilby.sql new file mode 100644 index 0000000000..847a7a730a --- /dev/null +++ b/tencent-bd-dashboard/drizzle/0000_slow_trish_tilby.sql @@ -0,0 +1,277 @@ +CREATE TABLE `account_research` ( + `account_id` integer PRIMARY KEY NOT NULL, + `company_snapshot` text DEFAULT '' NOT NULL, + `recent_news` text DEFAULT '' NOT NULL, + `funding` text DEFAULT '' NOT NULL, + `stack` text DEFAULT '' NOT NULL, + `hiring` text DEFAULT '' NOT NULL, + `vendors` text DEFAULT '' NOT NULL, + `triggers` text DEFAULT '' NOT NULL, + `pains` text DEFAULT '' NOT NULL, + `gaps` text DEFAULT '' NOT NULL, + `why_account` text DEFAULT '' NOT NULL, + `why_now` text DEFAULT '' NOT NULL, + `primary_pain` text DEFAULT '' NOT NULL, + `wedge` text DEFAULT '' NOT NULL, + `buyer` text DEFAULT '' NOT NULL, + `proof` text DEFAULT '' NOT NULL, + `expansion` text DEFAULT '' NOT NULL, + `validation_step` text DEFAULT '' NOT NULL, + `final_thesis` text DEFAULT '' NOT NULL, + `economic_buyer` text DEFAULT '' NOT NULL, + `technical_buyer` text DEFAULT '' NOT NULL, + `champion` text DEFAULT '' NOT NULL, + `users_influence` text DEFAULT '' NOT NULL, + `blockers` text DEFAULT '' NOT NULL, + `procurement` text DEFAULT '' NOT NULL, + `warm_path` text DEFAULT '' NOT NULL, + `sources` text DEFAULT '' NOT NULL, + `confidence` text DEFAULT '' NOT NULL, + `last_verified` text DEFAULT '' NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `accounts` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `company` text NOT NULL, + `region` text DEFAULT '' NOT NULL, + `industry` text DEFAULT '' NOT NULL, + `size` text DEFAULT 'Scale-up' NOT NULL, + `incumbent` text DEFAULT '' NOT NULL, + `products` text DEFAULT '' NOT NULL, + `pain` text DEFAULT '' NOT NULL, + `trigger` text DEFAULT '' NOT NULL, + `contact` text DEFAULT '' NOT NULL, + `path` text DEFAULT '' NOT NULL, + `stage` text DEFAULT 'Target' NOT NULL, + `fit` text DEFAULT 'Medium' NOT NULL, + `next_action` text DEFAULT '' NOT NULL, + `due_on` text DEFAULT '' NOT NULL, + `notes` text DEFAULT '' NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +CREATE INDEX `accounts_stage_idx` ON `accounts` (`stage`);--> statement-breakpoint +CREATE INDEX `accounts_company_idx` ON `accounts` (`company`);--> statement-breakpoint +CREATE TABLE `audit_log` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `actor_user_id` integer, + `actor_email` text DEFAULT '' NOT NULL, + `action` text NOT NULL, + `entity_type` text DEFAULT '' NOT NULL, + `entity_id` text DEFAULT '' NOT NULL, + `metadata` text DEFAULT '{}' NOT NULL, + `ip_hash` text DEFAULT '' NOT NULL, + `outcome` text DEFAULT 'success' NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`actor_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE INDEX `audit_actor_idx` ON `audit_log` (`actor_user_id`);--> statement-breakpoint +CREATE INDEX `audit_created_idx` ON `audit_log` (`created_at`);--> statement-breakpoint +CREATE INDEX `audit_entity_idx` ON `audit_log` (`entity_type`,`entity_id`);--> statement-breakpoint +CREATE TABLE `catalog_sync_runs` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `started_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `finished_at` integer, + `status` text DEFAULT 'running' NOT NULL, + `source_kind` text DEFAULT 'local-corpus' NOT NULL, + `products_seen` integer DEFAULT 0 NOT NULL, + `products_created` integer DEFAULT 0 NOT NULL, + `products_updated` integer DEFAULT 0 NOT NULL, + `error` text DEFAULT '' NOT NULL, + `triggered_by` integer, + FOREIGN KEY (`triggered_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE INDEX `sync_started_idx` ON `catalog_sync_runs` (`started_at`);--> statement-breakpoint +CREATE TABLE `motions` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `opportunity` text NOT NULL, + `trigger_signals` text DEFAULT '' NOT NULL, + `icp` text DEFAULT '' NOT NULL, + `discovery_questions` text DEFAULT '' NOT NULL, + `wedge_product` text DEFAULT '' NOT NULL, + `poc_strategy` text DEFAULT '' NOT NULL, + `success_metrics` text DEFAULT '' NOT NULL, + `expansion_path` text DEFAULT '' NOT NULL, + `risks` text DEFAULT '' NOT NULL, + `evidence_required` text DEFAULT '' NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +CREATE TABLE `product_evidence` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `product_id` integer NOT NULL, + `level` text DEFAULT 'Assumption' NOT NULL, + `statement` text DEFAULT '' NOT NULL, + `can_say` text DEFAULT 'No' NOT NULL, + `source` text DEFAULT '' NOT NULL, + `confidence` text DEFAULT 'Low' NOT NULL, + `verified_on` text DEFAULT '' NOT NULL, + `notes` text DEFAULT '' NOT NULL, + `position` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`product_id`) REFERENCES `products`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `evidence_product_idx` ON `product_evidence` (`product_id`,`position`);--> statement-breakpoint +CREATE INDEX `evidence_can_say_idx` ON `product_evidence` (`can_say`);--> statement-breakpoint +CREATE TABLE `products` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `category` text DEFAULT '' NOT NULL, + `product` text NOT NULL, + `description` text DEFAULT '' NOT NULL, + `competitors` text DEFAULT '' NOT NULL, + `use_cases` text DEFAULT '' NOT NULL, + `industries` text DEFAULT '' NOT NULL, + `company_size` text DEFAULT '' NOT NULL, + `customer_gate` text DEFAULT '' NOT NULL, + `pain_points` text DEFAULT '' NOT NULL, + `strengths` text DEFAULT '' NOT NULL, + `weaknesses` text DEFAULT '' NOT NULL, + `product_entry` text DEFAULT '' NOT NULL, + `bd_motion` text DEFAULT '' NOT NULL, + `notes` text DEFAULT '' NOT NULL, + `source` text DEFAULT '' NOT NULL, + `priority` text DEFAULT 'P3' NOT NULL, + `knowledge` text DEFAULT 'To Learn' NOT NULL, + `confidence` text DEFAULT 'Hypothesis' NOT NULL, + `status` text DEFAULT 'Not Reviewed' NOT NULL, + `owner` text DEFAULT '' NOT NULL, + `commercial_category` text DEFAULT 'Standalone / Other' NOT NULL, + `sell_mode` text DEFAULT 'Standalone' NOT NULL, + `solution_story` text DEFAULT '' NOT NULL, + `primary_competitor` text DEFAULT '' NOT NULL, + `reference_customers` text DEFAULT '' NOT NULL, + `tencent_edge` text DEFAULT '' NOT NULL, + `messaging_technical` text DEFAULT '' NOT NULL, + `messaging_business` text DEFAULT '' NOT NULL, + `messaging_executive` text DEFAULT '' NOT NULL, + `messaging_safe_claim` text DEFAULT '' NOT NULL, + `discovery` text DEFAULT '' NOT NULL, + `buying_signals` text DEFAULT '' NOT NULL, + `red_flags` text DEFAULT '' NOT NULL, + `proof_required` text DEFAULT '' NOT NULL, + `objections` text DEFAULT '' NOT NULL, + `replacements` text DEFAULT '' NOT NULL, + `learning_checklist` text DEFAULT '' NOT NULL, + `score_demand` integer DEFAULT 3 NOT NULL, + `score_right_to_win` integer DEFAULT 3 NOT NULL, + `score_entry` integer DEFAULT 3 NOT NULL, + `score_poc` integer DEFAULT 3 NOT NULL, + `score_expansion` integer DEFAULT 3 NOT NULL, + `score_revenue` integer DEFAULT 3 NOT NULL, + `score_rationale` text DEFAULT '' NOT NULL, + `score_validation` text DEFAULT '' NOT NULL, + `upstream_code` text, + `upstream_synced_at` integer, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `products_upstream_code_unique` ON `products` (`upstream_code`);--> statement-breakpoint +CREATE INDEX `products_priority_idx` ON `products` (`priority`);--> statement-breakpoint +CREATE INDEX `products_category_idx` ON `products` (`category`);--> statement-breakpoint +CREATE INDEX `products_commercial_idx` ON `products` (`commercial_category`);--> statement-breakpoint +CREATE INDEX `products_status_idx` ON `products` (`status`);--> statement-breakpoint +CREATE TABLE `rate_limits` ( + `key` text PRIMARY KEY NOT NULL, + `count` integer DEFAULT 0 NOT NULL, + `window_started_at` integer NOT NULL, + `expires_at` integer NOT NULL +); +--> statement-breakpoint +CREATE INDEX `rate_limits_expiry_idx` ON `rate_limits` (`expires_at`);--> statement-breakpoint +CREATE TABLE `review_answers` ( + `question_index` integer NOT NULL, + `user_id` integer NOT NULL, + `answer` text DEFAULT '' NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + PRIMARY KEY(`user_id`, `question_index`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `review_user_idx` ON `review_answers` (`user_id`);--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `token_hash` text NOT NULL, + `user_agent_hash` text DEFAULT '' NOT NULL, + `ip_hash` text DEFAULT '' NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `last_seen_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `absolute_expires_at` integer NOT NULL, + `revoked_at` integer, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `sessions_token_hash_unique` ON `sessions` (`token_hash`);--> statement-breakpoint +CREATE INDEX `sessions_user_idx` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE INDEX `sessions_expiry_idx` ON `sessions` (`absolute_expires_at`);--> statement-breakpoint +CREATE TABLE `sop_steps` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `body` text DEFAULT '' NOT NULL, + `position` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +CREATE INDEX `sop_position_idx` ON `sop_steps` (`position`);--> statement-breakpoint +CREATE TABLE `todos` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `title` text NOT NULL, + `detail` text DEFAULT '' NOT NULL, + `owner` text DEFAULT 'Me' NOT NULL, + `priority` text DEFAULT 'P2' NOT NULL, + `status` text DEFAULT 'Backlog' NOT NULL, + `due_on` text DEFAULT '' NOT NULL, + `link` text DEFAULT '' NOT NULL, + `position` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +CREATE INDEX `todos_status_idx` ON `todos` (`status`,`position`);--> statement-breakpoint +CREATE TABLE `translations` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `entity_type` text NOT NULL, + `entity_id` integer NOT NULL, + `field` text NOT NULL, + `locale` text NOT NULL, + `value` text NOT NULL, + `source_locale` text NOT NULL, + `source_hash` text NOT NULL, + `origin` text DEFAULT 'machine' NOT NULL, + `status` text DEFAULT 'needs-review' NOT NULL, + `reviewed_by` integer, + `reviewed_at` integer, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`reviewed_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `translations_target_unique` ON `translations` (`entity_type`,`entity_id`,`field`,`locale`);--> statement-breakpoint +CREATE INDEX `translations_status_idx` ON `translations` (`status`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `email` text NOT NULL, + `display_name` text DEFAULT '' NOT NULL, + `password_hash` text NOT NULL, + `role` text DEFAULT 'viewer' NOT NULL, + `locale` text DEFAULT 'en' NOT NULL, + `failed_login_count` integer DEFAULT 0 NOT NULL, + `locked_until` integer, + `last_login_at` integer, + `is_active` integer DEFAULT true NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`); \ No newline at end of file diff --git a/tencent-bd-dashboard/drizzle/meta/0000_snapshot.json b/tencent-bd-dashboard/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000000..42c48ad05e --- /dev/null +++ b/tencent-bd-dashboard/drizzle/meta/0000_snapshot.json @@ -0,0 +1,2030 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "695d4e40-9637-420a-b6c2-d9b4be6debb6", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "account_research": { + "name": "account_research", + "columns": { + "account_id": { + "name": "account_id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "company_snapshot": { + "name": "company_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "recent_news": { + "name": "recent_news", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "funding": { + "name": "funding", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "stack": { + "name": "stack", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "hiring": { + "name": "hiring", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "vendors": { + "name": "vendors", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "triggers": { + "name": "triggers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pains": { + "name": "pains", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "gaps": { + "name": "gaps", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "why_account": { + "name": "why_account", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "why_now": { + "name": "why_now", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "primary_pain": { + "name": "primary_pain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "wedge": { + "name": "wedge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "buyer": { + "name": "buyer", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "proof": { + "name": "proof", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "expansion": { + "name": "expansion", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "validation_step": { + "name": "validation_step", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "final_thesis": { + "name": "final_thesis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "economic_buyer": { + "name": "economic_buyer", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "technical_buyer": { + "name": "technical_buyer", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "champion": { + "name": "champion", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "users_influence": { + "name": "users_influence", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "blockers": { + "name": "blockers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "procurement": { + "name": "procurement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "warm_path": { + "name": "warm_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "last_verified": { + "name": "last_verified", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "account_research_account_id_accounts_id_fk": { + "name": "account_research_account_id_accounts_id_fk", + "tableFrom": "account_research", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "company": { + "name": "company", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "industry": { + "name": "industry", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Scale-up'" + }, + "incumbent": { + "name": "incumbent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "products": { + "name": "products", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pain": { + "name": "pain", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Target'" + }, + "fit": { + "name": "fit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Medium'" + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "due_on": { + "name": "due_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "accounts_stage_idx": { + "name": "accounts_stage_idx", + "columns": [ + "stage" + ], + "isUnique": false + }, + "accounts_company_idx": { + "name": "accounts_company_idx", + "columns": [ + "company" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_log": { + "name": "audit_log", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{}'" + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'success'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "audit_actor_idx": { + "name": "audit_actor_idx", + "columns": [ + "actor_user_id" + ], + "isUnique": false + }, + "audit_created_idx": { + "name": "audit_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "audit_entity_idx": { + "name": "audit_entity_idx", + "columns": [ + "entity_type", + "entity_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_log_actor_user_id_users_id_fk": { + "name": "audit_log_actor_user_id_users_id_fk", + "tableFrom": "audit_log", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "catalog_sync_runs": { + "name": "catalog_sync_runs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'running'" + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local-corpus'" + }, + "products_seen": { + "name": "products_seen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "products_created": { + "name": "products_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "products_updated": { + "name": "products_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "triggered_by": { + "name": "triggered_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sync_started_idx": { + "name": "sync_started_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "catalog_sync_runs_triggered_by_users_id_fk": { + "name": "catalog_sync_runs_triggered_by_users_id_fk", + "tableFrom": "catalog_sync_runs", + "tableTo": "users", + "columnsFrom": [ + "triggered_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "motions": { + "name": "motions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "opportunity": { + "name": "opportunity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "trigger_signals": { + "name": "trigger_signals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "icp": { + "name": "icp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "discovery_questions": { + "name": "discovery_questions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "wedge_product": { + "name": "wedge_product", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "poc_strategy": { + "name": "poc_strategy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "success_metrics": { + "name": "success_metrics", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "expansion_path": { + "name": "expansion_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "risks": { + "name": "risks", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "evidence_required": { + "name": "evidence_required", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "product_evidence": { + "name": "product_evidence", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Assumption'" + }, + "statement": { + "name": "statement", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "can_say": { + "name": "can_say", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'No'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Low'" + }, + "verified_on": { + "name": "verified_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "evidence_product_idx": { + "name": "evidence_product_idx", + "columns": [ + "product_id", + "position" + ], + "isUnique": false + }, + "evidence_can_say_idx": { + "name": "evidence_can_say_idx", + "columns": [ + "can_say" + ], + "isUnique": false + } + }, + "foreignKeys": { + "product_evidence_product_id_products_id_fk": { + "name": "product_evidence_product_id_products_id_fk", + "tableFrom": "product_evidence", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "products": { + "name": "products", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "competitors": { + "name": "competitors", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "use_cases": { + "name": "use_cases", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "industries": { + "name": "industries", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "company_size": { + "name": "company_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "customer_gate": { + "name": "customer_gate", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "pain_points": { + "name": "pain_points", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "strengths": { + "name": "strengths", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "weaknesses": { + "name": "weaknesses", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "product_entry": { + "name": "product_entry", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "bd_motion": { + "name": "bd_motion", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'P3'" + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'To Learn'" + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Hypothesis'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Not Reviewed'" + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "commercial_category": { + "name": "commercial_category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Standalone / Other'" + }, + "sell_mode": { + "name": "sell_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Standalone'" + }, + "solution_story": { + "name": "solution_story", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "primary_competitor": { + "name": "primary_competitor", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "reference_customers": { + "name": "reference_customers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "tencent_edge": { + "name": "tencent_edge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "messaging_technical": { + "name": "messaging_technical", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "messaging_business": { + "name": "messaging_business", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "messaging_executive": { + "name": "messaging_executive", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "messaging_safe_claim": { + "name": "messaging_safe_claim", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "discovery": { + "name": "discovery", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "buying_signals": { + "name": "buying_signals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "red_flags": { + "name": "red_flags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "proof_required": { + "name": "proof_required", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "objections": { + "name": "objections", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "replacements": { + "name": "replacements", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "learning_checklist": { + "name": "learning_checklist", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "score_demand": { + "name": "score_demand", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "score_right_to_win": { + "name": "score_right_to_win", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "score_entry": { + "name": "score_entry", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "score_poc": { + "name": "score_poc", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "score_expansion": { + "name": "score_expansion", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "score_revenue": { + "name": "score_revenue", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "score_rationale": { + "name": "score_rationale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "score_validation": { + "name": "score_validation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "upstream_code": { + "name": "upstream_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upstream_synced_at": { + "name": "upstream_synced_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "products_upstream_code_unique": { + "name": "products_upstream_code_unique", + "columns": [ + "upstream_code" + ], + "isUnique": true + }, + "products_priority_idx": { + "name": "products_priority_idx", + "columns": [ + "priority" + ], + "isUnique": false + }, + "products_category_idx": { + "name": "products_category_idx", + "columns": [ + "category" + ], + "isUnique": false + }, + "products_commercial_idx": { + "name": "products_commercial_idx", + "columns": [ + "commercial_category" + ], + "isUnique": false + }, + "products_status_idx": { + "name": "products_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "rate_limits": { + "name": "rate_limits", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "window_started_at": { + "name": "window_started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "rate_limits_expiry_idx": { + "name": "rate_limits_expiry_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "review_answers": { + "name": "review_answers", + "columns": { + "question_index": { + "name": "question_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "answer": { + "name": "answer", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "review_user_idx": { + "name": "review_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "review_answers_user_id_users_id_fk": { + "name": "review_answers_user_id_users_id_fk", + "tableFrom": "review_answers", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "review_answers_user_id_question_index_pk": { + "columns": [ + "user_id", + "question_index" + ], + "name": "review_answers_user_id_question_index_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent_hash": { + "name": "user_agent_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "absolute_expires_at": { + "name": "absolute_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_token_hash_unique": { + "name": "sessions_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + }, + "sessions_user_idx": { + "name": "sessions_user_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expiry_idx": { + "name": "sessions_expiry_idx", + "columns": [ + "absolute_expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sop_steps": { + "name": "sop_steps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "sop_position_idx": { + "name": "sop_position_idx", + "columns": [ + "position" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "todos": { + "name": "todos", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Me'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'P2'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Backlog'" + }, + "due_on": { + "name": "due_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "todos_status_idx": { + "name": "todos_status_idx", + "columns": [ + "status", + "position" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "translations": { + "name": "translations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "field": { + "name": "field", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_locale": { + "name": "source_locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'machine'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'needs-review'" + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "translations_target_unique": { + "name": "translations_target_unique", + "columns": [ + "entity_type", + "entity_id", + "field", + "locale" + ], + "isUnique": true + }, + "translations_status_idx": { + "name": "translations_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "translations_reviewed_by_users_id_fk": { + "name": "translations_reviewed_by_users_id_fk", + "tableFrom": "translations", + "tableTo": "users", + "columnsFrom": [ + "reviewed_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'viewer'" + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "failed_login_count": { + "name": "failed_login_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_login_at": { + "name": "last_login_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/tencent-bd-dashboard/drizzle/meta/_journal.json b/tencent-bd-dashboard/drizzle/meta/_journal.json new file mode 100644 index 0000000000..1522792ab3 --- /dev/null +++ b/tencent-bd-dashboard/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1785450181395, + "tag": "0000_slow_trish_tilby", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/tencent-bd-dashboard/next.config.ts b/tencent-bd-dashboard/next.config.ts new file mode 100644 index 0000000000..6278cbeff5 --- /dev/null +++ b/tencent-bd-dashboard/next.config.ts @@ -0,0 +1,27 @@ +import type { NextConfig } from 'next'; +import createNextIntlPlugin from 'next-intl/plugin'; + +const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts'); + +const nextConfig: NextConfig = { + reactStrictMode: true, + + // Never leak framework version or stack details to clients. + poweredByHeader: false, + + // Fail the production build on type errors rather than shipping them. + // (Next.js 16 dropped the built-in `next lint` / `eslint` build integration; + // static analysis here is `npm run typecheck`, run in CI alongside this.) + typescript: { ignoreBuildErrors: false }, + + // better-sqlite3 is a native addon: keep it external to the server bundle. + serverExternalPackages: ['better-sqlite3'], + + experimental: { + // Server Actions are the only mutation surface; bound the request body so a + // single action call cannot be used to exhaust memory. + serverActions: { bodySizeLimit: '1mb' }, + }, +}; + +export default withNextIntl(nextConfig); diff --git a/tencent-bd-dashboard/package-lock.json b/tencent-bd-dashboard/package-lock.json new file mode 100644 index 0000000000..302adbb225 --- /dev/null +++ b/tencent-bd-dashboard/package-lock.json @@ -0,0 +1,3949 @@ +{ + "name": "tencent-bd-dashboard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tencent-bd-dashboard", + "version": "1.0.0", + "dependencies": { + "better-sqlite3": "^11.8.1", + "drizzle-orm": "^0.38.4", + "next": "^16.2.12", + "next-intl": "^4.3.4", + "opencc-js": "^1.0.5", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "server-only": "^0.0.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.7", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", + "cross-env": "^10.1.0", + "drizzle-kit": "^0.30.2", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + }, + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.15", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.15.tgz", + "integrity": "sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.11" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "license": "MIT" + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.13.tgz", + "integrity": "sha512-kHEAFOkeJSPNi7c5PaKaRjxcBrJwzzt81ifUu+8uve1EDW/VJl83KsxmqgqNZLzcFEhSliZGvx3+pk/RH0IOmg==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.12.tgz", + "integrity": "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.12.tgz", + "integrity": "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.12.tgz", + "integrity": "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.12.tgz", + "integrity": "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.12.tgz", + "integrity": "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.12.tgz", + "integrity": "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.12.tgz", + "integrity": "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.12.tgz", + "integrity": "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.12.tgz", + "integrity": "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@schummar/icu-type-parser": { + "version": "1.21.5", + "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", + "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", + "license": "MIT" + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.47.tgz", + "integrity": "sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.47.tgz", + "integrity": "sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.47.tgz", + "integrity": "sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.47.tgz", + "integrity": "sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.47.tgz", + "integrity": "sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.47.tgz", + "integrity": "sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.47.tgz", + "integrity": "sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.47.tgz", + "integrity": "sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.47.tgz", + "integrity": "sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.47.tgz", + "integrity": "sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.47.tgz", + "integrity": "sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.47.tgz", + "integrity": "sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.8.tgz", + "integrity": "sha512-zAgkquC2WYF0PIc6XbNYkA2uuxxFavzgmX61R+dHDUa558V8Ejf8ozTZFR6QzM24RWu4kBcRkhJ5kpz77j9fnQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/drizzle-kit": { + "version": "0.30.6", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.6.tgz", + "integrity": "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0", + "gel": "^2.0.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.38.4.tgz", + "integrity": "sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gel": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gel/-/gel-2.2.0.tgz", + "integrity": "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@petamoriken/float16": "^3.8.7", + "debug": "^4.3.4", + "env-paths": "^3.0.0", + "semver": "^7.6.2", + "shell-quote": "^1.8.1", + "which": "^4.0.0" + }, + "bin": { + "gel": "dist/cli.mjs" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/icu-minify": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.13.4.tgz", + "integrity": "sha512-yK6HyPLGlQjqm8fTKtnBpM77z7vl7JdDBN2EXLvmgAu/b7XaOHWZb73M3ISl9ahBTehBv7RYeqqWSHfk1v2YcA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/icu-messageformat-parser": "^3.4.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/intl-messageformat": { + "version": "11.2.12", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.12.tgz", + "integrity": "sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.15" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next": { + "version": "16.2.12", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.12.tgz", + "integrity": "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.12", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.12", + "@next/swc-darwin-x64": "16.2.12", + "@next/swc-linux-arm64-gnu": "16.2.12", + "@next/swc-linux-arm64-musl": "16.2.12", + "@next/swc-linux-x64-gnu": "16.2.12", + "@next/swc-linux-x64-musl": "16.2.12", + "@next/swc-win32-arm64-msvc": "16.2.12", + "@next/swc-win32-x64-msvc": "16.2.12", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-intl": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.13.4.tgz", + "integrity": "sha512-jhPAT0u0lahIK6E4gVdZAehugWCosBhLG8sV7xMzgSVoJpxHObP+Fiu+z2FfkEW0XPPtr7uEXoUlLEfhxhNMTg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.8.1", + "@parcel/watcher": "^2.4.1", + "@swc/core": "^1.15.2", + "icu-minify": "^4.13.4", + "negotiator": "^1.0.0", + "next-intl-swc-plugin-extractor": "^4.13.4", + "po-parser": "^2.1.1", + "use-intl": "^4.13.4" + }, + "peerDependencies": { + "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/next-intl-swc-plugin-extractor": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.13.4.tgz", + "integrity": "sha512-uN1+NMUYbG6YkO3q+rjc2bvAPX9nQ23owemvHJAyW0pRbQjVDwvNhmrV5qaak0oQc/9okbK17KLT49AoMGhVEQ==", + "license": "MIT" + }, + "node_modules/next-intl/node_modules/@swc/core": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.47.tgz", + "integrity": "sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.47", + "@swc/core-darwin-x64": "1.15.47", + "@swc/core-linux-arm-gnueabihf": "1.15.47", + "@swc/core-linux-arm64-gnu": "1.15.47", + "@swc/core-linux-arm64-musl": "1.15.47", + "@swc/core-linux-ppc64-gnu": "1.15.47", + "@swc/core-linux-s390x-gnu": "1.15.47", + "@swc/core-linux-x64-gnu": "1.15.47", + "@swc/core-linux-x64-musl": "1.15.47", + "@swc/core-win32-arm64-msvc": "1.15.47", + "@swc/core-win32-ia32-msvc": "1.15.47", + "@swc/core-win32-x64-msvc": "1.15.47" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opencc-js": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.4.1.tgz", + "integrity": "sha512-2lPLcrg7cnh1ATSduOxhnk/jq6KlR6w+5Ihl4wF7Z5e5Rva4vxTyugV5rZ7EiUCOZl+EDOLetIix+BhgYDCC+A==", + "license": "MIT AND Apache-2.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/po-parser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", + "integrity": "sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/use-intl": { + "version": "4.13.4", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.13.4.tgz", + "integrity": "sha512-wRhU5zyPNgu845++EJ8ckQsi89b22QUop7NlGxNXpsnKSwEJr7WErAkdAYeVQgFTmDWsa8e2NI1e14XbWz9Ecw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^3.1.0", + "@schummar/icu-type-parser": "1.21.5", + "icu-minify": "^4.13.4", + "intl-messageformat": "^11.1.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/tencent-bd-dashboard/package.json b/tencent-bd-dashboard/package.json new file mode 100644 index 0000000000..8d4d11302e --- /dev/null +++ b/tencent-bd-dashboard/package.json @@ -0,0 +1,40 @@ +{ + "name": "tencent-bd-dashboard", + "version": "1.0.0", + "private": true, + "description": "Tencent Cloud NA BD Operating System - secure Next.js dashboard", + "scripts": { + "dev": "next dev --port 3000", + "build": "next build", + "start": "next start --port 3000", + "typecheck": "tsc --noEmit", + "db:migrate": "cross-env NODE_OPTIONS=--conditions=react-server tsx --env-file-if-exists=./.env.local --env-file-if-exists=./.env src/db/migrate.ts", + "db:seed": "cross-env NODE_OPTIONS=--conditions=react-server tsx --env-file-if-exists=./.env.local --env-file-if-exists=./.env src/db/seed.ts", + "db:reset": "cross-env NODE_OPTIONS=--conditions=react-server tsx --env-file-if-exists=./.env.local --env-file-if-exists=./.env src/db/reset.ts", + "verify": "cross-env NODE_OPTIONS=--conditions=react-server tsx --env-file-if-exists=./.env.local --env-file-if-exists=./.env scripts/verify.ts" + }, + "dependencies": { + "better-sqlite3": "^11.8.1", + "drizzle-orm": "^0.38.4", + "next": "^16.2.12", + "next-intl": "^4.3.4", + "opencc-js": "^1.0.5", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "server-only": "^0.0.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.7", + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", + "cross-env": "^10.1.0", + "drizzle-kit": "^0.30.2", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + }, + "engines": { + "node": ">=22.9.0" + } +} diff --git a/tencent-bd-dashboard/scripts/generate-zh-hant.ts b/tencent-bd-dashboard/scripts/generate-zh-hant.ts new file mode 100644 index 0000000000..e7cb1cbc3a --- /dev/null +++ b/tencent-bd-dashboard/scripts/generate-zh-hant.ts @@ -0,0 +1,60 @@ +/** + * Generate `src/i18n/messages/zh-Hant.json` from the Simplified catalog. + * + * The Traditional catalog is derived, never hand-maintained: keeping two + * Chinese catalogs in sync by hand guarantees they drift, and the difference + * between them is a mechanical script conversion, not a translation. + * + * Conversion is `cn -> tw` (character level). The `twp` variant additionally + * substitutes Taiwan vocabulary, which changes word choice rather than script; + * that is a translation decision and is deliberately left to a human reviewer. + * + * Run: npx tsx scripts/generate-zh-hant.ts + */ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import * as OpenCC from 'opencc-js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const messagesDir = resolve(here, '../src/i18n/messages'); + +const convert = OpenCC.Converter({ from: 'cn', to: 'tw' }); + +type Json = string | number | boolean | null | Json[] | { [key: string]: Json }; + +/** + * Walk the catalog, converting only string leaves. + * + * Keys are ASCII identifiers referenced from code and must survive untouched -- + * converting a key would silently break every lookup that uses it. + */ +function convertTree(value: Json): Json { + if (typeof value === 'string') return convert(value); + if (Array.isArray(value)) return value.map(convertTree); + if (value !== null && typeof value === 'object') { + return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, convertTree(v)])); + } + return value; +} + +const sourcePath = resolve(messagesDir, 'zh-Hans.json'); +const targetPath = resolve(messagesDir, 'zh-Hant.json'); + +const source = JSON.parse(readFileSync(sourcePath, 'utf8')) as Json; +const converted = convertTree(source); + +writeFileSync(targetPath, `${JSON.stringify(converted, null, 2)}\n`, 'utf8'); + +function countLeaves(value: Json): number { + if (typeof value === 'string') return 1; + if (Array.isArray(value)) return value.reduce((n, v) => n + countLeaves(v), 0); + if (value !== null && typeof value === 'object') { + return Object.values(value).reduce((n, v) => n + countLeaves(v), 0); + } + return 0; +} + +console.log(`Wrote ${targetPath} (${countLeaves(converted)} strings converted cn -> tw)`); diff --git a/tencent-bd-dashboard/scripts/verify.ts b/tencent-bd-dashboard/scripts/verify.ts new file mode 100644 index 0000000000..77a3d912c1 --- /dev/null +++ b/tencent-bd-dashboard/scripts/verify.ts @@ -0,0 +1,112 @@ +/** + * Lightweight smoke test: migrate a throwaway database, load the seed + * corpus, and assert a handful of invariants that would catch the most + * likely regressions (a broken migration, a seed script that silently loads + * zero rows, a password hash that does not round-trip). + * + * Deliberately not a test framework: this project has no `*.test.ts` files + * and does not depend on one. This script exercises real code against a + * real (temporary) SQLite database -- the same kind of check as running the + * app itself, just scripted and assertion-based rather than clicked through + * by hand. + * + * Run: npm run verify + */ + +import { existsSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +process.env.DATABASE_PATH = join(mkdtempSync(join(tmpdir(), 'bd-os-verify-')), 'verify.db'); + +async function main(): Promise { + const failures: string[] = []; + const check = (label: string, ok: boolean) => { + console.log(`${ok ? 'PASS' : 'FAIL'} - ${label}`); + if (!ok) failures.push(label); + }; + + const { migrate } = await import('drizzle-orm/better-sqlite3/migrator'); + const { db, sqlite } = await import('../src/db/client'); + const { applySeedCorpus } = await import('../src/db/seed-corpus'); + const { products, accounts, todos, sopSteps, motions } = await import('../src/db/schema'); + const { hashPassword, verifyPassword } = await import('../src/lib/security/crypto'); + const { check: checkPasswordPolicy } = await import('../src/lib/auth/password-policy'); + const { productSchema, accountSchema } = await import('../src/domain/schemas'); + + migrate(db, { migrationsFolder: join(__dirname, '..', 'drizzle') }); + check('migrations applied without throwing', true); + + const counts = applySeedCorpus(); + check(`seed loaded 181 products (got ${counts.products})`, counts.products === 181); + check(`seed loaded at least 1 account (got ${counts.accounts})`, counts.accounts >= 1); + + const productRows = db.select().from(products).all(); + check('products table row count matches reported count', productRows.length === counts.products); + check('every product has a non-empty name', productRows.every((p) => p.product.trim() !== '')); + check( + 'every product has a valid priority (P1/P2/P3)', + productRows.every((p) => ['P1', 'P2', 'P3'].includes(p.priority)), + ); + + const accountRows = db.select().from(accounts).all(); + check('accounts table has rows', accountRows.length > 0); + + const todoRows = db.select().from(todos).all(); + check('todos seeded', todoRows.length > 0); + + const sopRows = db.select().from(sopSteps).all(); + check('SOP checklist seeded', sopRows.length > 0); + + const motionRows = db.select().from(motions).all(); + check('sales motions seeded', motionRows.length === 10); + + // Password hashing round-trip. + const password = 'Correct-Horse-Battery-Staple-9!'; + const hash = await hashPassword(password); + check('scrypt hash verifies against its own password', await verifyPassword(password, hash)); + check('scrypt hash rejects a wrong password', !(await verifyPassword('wrong-password-entirely', hash))); + check('password policy accepts a strong password', checkPasswordPolicy(password).ok); + check('password policy rejects a common weak password', !checkPasswordPolicy('password123').ok); + + // Zod schemas reject malformed input at the same boundary the Server Actions use. + const badProduct = productSchema.safeParse({ category: '', product: '', priority: 'P9' }); + check('productSchema rejects an empty name and invalid priority', !badProduct.success); + + const goodAccount = accountSchema.safeParse({ + company: 'Test Co', + region: '', + industry: '', + size: 'Scale-up', + incumbent: '', + products: '', + pain: '', + trigger: '', + contact: '', + path: '', + stage: 'Target', + fit: 'Medium', + nextAction: '', + dueOn: '', + notes: '', + }); + check('accountSchema accepts a minimal valid account', goodAccount.success); + + sqlite.close(); + const dbPath = process.env.DATABASE_PATH!; + if (existsSync(dbPath)) rmSync(join(dbPath, '..'), { recursive: true, force: true }); + + if (failures.length > 0) { + console.error(`\n${failures.length} check(s) failed:`); + for (const f of failures) console.error(` - ${f}`); + process.exitCode = 1; + return; + } + + console.log('\nAll checks passed.'); +} + +main().catch((error: unknown) => { + console.error('verify script crashed:', error); + process.exitCode = 1; +}); diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/AccountsClient.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/AccountsClient.tsx new file mode 100644 index 0000000000..b4524fa8a1 --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/AccountsClient.tsx @@ -0,0 +1,225 @@ +'use client'; + +import { useActionState, useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; + +import { ACCOUNT_FITS, ACCOUNT_SIZES, ACCOUNT_STAGES } from '@/domain/enums'; +import type { Account } from '@/db/schema'; +import { CSRF_FIELD } from '@/lib/security/csrf-constants'; +import { Modal } from '@/components/Modal'; +import { SelectField, TextAreaField, TextField } from '@/components/fields'; +import { Link } from '@/i18n/navigation'; +import { SubmitButton } from '@/components/SubmitButton'; +import { removeAccount, saveAccount } from '@/server/actions/accounts'; +import type { FormActionState } from '@/server/actions/shared'; + +const initialState: FormActionState = {}; + +function fitTone(fit: string): string { + if (fit === 'High') return 'success'; + if (fit === 'Medium') return 'warn'; + return 'neutral'; +} + +export function AccountsClient({ + items, + total, + page, + pageSize, + canWrite, + canDelete, + csrfToken, +}: { + items: Account[]; + total: number; + page: number; + pageSize: number; + canWrite: boolean; + canDelete: boolean; + csrfToken: string; +}) { + const t = useTranslations('accounts'); + const tCommon = useTranslations('common'); + const [modal, setModal] = useState<{ type: 'create' } | { type: 'edit'; account: Account } | null>(null); + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + + return ( + <> + {canWrite ? ( +

+ ) : null} + +
+ + + + + + + + + + + + + + + + + + + + {items.map((account) => ( + + + + + + + + + + + + + + + + ))} + {items.length === 0 ? ( + + + + ) : null} + +
{t('colCompany')}{t('colRegion')}{t('colIndustry')}{t('colSize')}{t('colIncumbent')}{t('colProducts')}{t('colPain')}{t('colTrigger')}{t('colStage')}{t('colFit')}{t('colNext')}{t('colDue')}{tCommon('actions')}
+ {account.company} + {account.region}{account.industry}{account.size}{truncate(account.incumbent)}{truncate(account.products)}{truncate(account.pain)}{truncate(account.trigger)} + + {account.stage} + + + + {account.fit} + + {truncate(account.nextAction)}{account.dueOn || '-'} +
+ {canWrite ? ( + + ) : null} + + {t('openResearch')} + + {canDelete ? ( +
{ + await removeAccount(formData); + }} + onSubmit={(event) => { + if (!window.confirm(tCommon('confirmDelete'))) event.preventDefault(); + }} + > + + + +
+ ) : null} +
+
+ {tCommon('emptyState')} +
+
+ + {totalPages > 1 ? ( +
+ {tCommon('showing', { count: total === 0 ? 0 : (page - 1) * pageSize + 1, total })} + + {page} / {totalPages} + +
+ ) : null} + + {modal ? ( + setModal(null)} + /> + ) : null} + + ); +} + +function truncate(value: string, max = 80): string { + const trimmed = value.trim(); + if (trimmed.length <= max) return trimmed || '-'; + return `${trimmed.slice(0, max)}...`; +} + +function AccountFormModal({ account, csrfToken, onClose }: { account?: Account; csrfToken: string; onClose: () => void }) { + const t = useTranslations('accounts'); + const tCommon = useTranslations('common'); + const [state, formAction] = useActionState(saveAccount, initialState); + + useEffect(() => { + if (state.success) onClose(); + }, [state.success, onClose]); + + return ( + +
+

{account ? t('editTitle') : t('createTitle')}

+ +
+ + {state.message ? ( +

+ {state.message} +

+ ) : null} + +
+ + {account ? : null} + +
+ + + + + + + + + + + + + + + + +
+
+ +
+ + + {tCommon('save')} + +
+
+ ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/[id]/ResearchForm.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/[id]/ResearchForm.tsx new file mode 100644 index 0000000000..e45e5bc621 --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/[id]/ResearchForm.tsx @@ -0,0 +1,130 @@ +'use client'; + +import { useActionState, useState } from 'react'; +import { useTranslations } from 'next-intl'; + +import type { AccountResearch } from '@/db/schema'; +import { CSRF_FIELD } from '@/lib/security/csrf-constants'; +import { TextAreaField, TextField } from '@/components/fields'; +import { SubmitButton } from '@/components/SubmitButton'; +import { saveResearch } from '@/server/actions/accounts'; +import type { FormActionState } from '@/server/actions/shared'; + +const initialState: FormActionState = {}; + +const TABS = ['research', 'thesis', 'buying', 'evidence'] as const; +type Tab = (typeof TABS)[number]; + +export function ResearchForm({ + accountId, + research, + canWrite, + csrfToken, +}: { + accountId: number; + research: AccountResearch | undefined; + canWrite: boolean; + csrfToken: string; +}) { + const t = useTranslations('research'); + const tCommon = useTranslations('common'); + const [tab, setTab] = useState('research'); + const [state, formAction] = useActionState(saveResearch, initialState); + + const r = research; + + return ( +
+ + + + {state.message ? ( +

+ {state.message} +

+ ) : null} + {state.success ? ( +

+ {tCommon('saved')} +

+ ) : null} + +
+ {TABS.map((key) => ( + + ))} +
+ +
+
+ + + + + + + + + +
+
+ +
+
+ {t('thesisFormulaTitle')} +

{t('thesisFormulaBody')}

+
+
+ + + + + + + + + +
+
+ +
+
+ + + + + + + +
+
+ +
+
+ + + +
+
+ + {canWrite ? ( +
+ + {t('save')} + +
+ ) : null} +
+ ); +} + +function capitalize(value: T): string { + return value.charAt(0).toUpperCase() + value.slice(1); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/[id]/page.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/[id]/page.tsx new file mode 100644 index 0000000000..66e365614f --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/[id]/page.tsx @@ -0,0 +1,43 @@ +import { getTranslations } from 'next-intl/server'; +import { notFound } from 'next/navigation'; + +import { can } from '@/lib/auth/rbac'; +import * as session from '@/lib/auth/session'; +import { readToken } from '@/lib/security/csrf'; +import { Link } from '@/i18n/navigation'; +import { getAccount, getAccountResearch } from '@/server/data/accounts'; + +import { ResearchForm } from './ResearchForm'; + +export default async function AccountResearchPage({ params }: { params: Promise<{ id: string }> }) { + const active = await session.current(); + if (!active) return null; + + const { id } = await params; + const accountId = Number(id); + if (!Number.isInteger(accountId) || accountId <= 0) notFound(); + + const account = getAccount(accountId); + if (!account) notFound(); + + const research = getAccountResearch(accountId); + const t = await getTranslations('research'); + const canWrite = can(active.user.role, 'account.write'); + const csrfToken = await readToken(active.sessionId); + + return ( +
+
+
+ + {'<- '} + {t('backToPipeline')} + +

{account.company}

+
+
+ + +
+ ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/page.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/page.tsx new file mode 100644 index 0000000000..e7f885e502 --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/accounts/page.tsx @@ -0,0 +1,73 @@ +import { getTranslations } from 'next-intl/server'; + +import { ACCOUNT_STAGES } from '@/domain/enums'; +import { accountFilterSchema } from '@/domain/schemas'; +import { can } from '@/lib/auth/rbac'; +import * as session from '@/lib/auth/session'; +import { readToken } from '@/lib/security/csrf'; +import { FilterForm } from '@/components/FilterForm'; +import { listAccounts } from '@/server/data/accounts'; + +import { AccountsClient } from './AccountsClient'; + +export default async function AccountsPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const active = await session.current(); + if (!active) return null; + + const raw = await searchParams; + const filter = accountFilterSchema.parse({ q: raw.q, stage: raw.stage, page: raw.page }); + + const [t, tCommon, list] = await Promise.all([ + getTranslations('accounts'), + getTranslations('common'), + Promise.resolve(listAccounts(filter)), + ]); + + const canWrite = can(active.user.role, 'account.write'); + const canDelete = can(active.user.role, 'account.delete'); + const csrfToken = await readToken(active.sessionId); + + return ( + <> +
+
+

{t('bannerWorkspaceTitle')}

+

{t('bannerWorkspaceBody')}

+
+
+

{t('bannerThesisTitle')}

+

{t('bannerThesisBody')}

+
+
+ + + + + + + + + + ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/admin/AdminClient.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/admin/AdminClient.tsx new file mode 100644 index 0000000000..fe989bf2b7 --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/admin/AdminClient.tsx @@ -0,0 +1,366 @@ +'use client'; + +import { useActionState, useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; + +import type { Locale, UserRole } from '@/domain/enums'; +import { CSRF_FIELD } from '@/lib/security/csrf-constants'; +import { SelectField, TextField } from '@/components/fields'; +import { SubmitButton } from '@/components/SubmitButton'; +import { resetToInitialCorpus, createUserAccount, updateUserAccount } from '@/server/actions/admin'; +import { approveTranslation } from '@/server/actions/translation'; +import { exportAllData } from '@/server/actions/data-export'; +import type { FormActionState } from '@/server/actions/shared'; + +const initialState: FormActionState = {}; + +interface UserRow { + id: number; + email: string; + displayName: string; + role: UserRole; + isActive: boolean; + lastLoginAt: string | null; + createdAt: string; +} + +interface PendingTranslation { + id: number; + entityType: string; + entityId: number; + field: string; + locale: Locale; + value: string; + origin: string; +} + +interface AuditEntry { + id: number; + action: string; + actorEmail: string; + entityType: string; + entityId: string; + outcome: string; + createdAt: string; +} + +export function AdminClient({ + currentUserId, + users, + roles, + pendingTranslations, + auditEntries, + lastSync, + hasTencentCredentials, + csrfToken, +}: { + currentUserId: number; + users: UserRow[]; + roles: readonly UserRole[]; + pendingTranslations: PendingTranslation[]; + auditEntries: AuditEntry[]; + lastSync: { status: string; finishedAt: string | null; sourceKind: string; error: string } | null; + hasTencentCredentials: boolean; + csrfToken: string; +}) { + const t = useTranslations('admin'); + const tProducts = useTranslations('products'); + + return ( +
+ + +
+

{t('sync')}

+

+ {lastSync + ? `${lastSync.status} - ${tProducts('syncLast', { when: lastSync.finishedAt ? new Date(lastSync.finishedAt).toLocaleString() : '-', source: lastSync.sourceKind })}` + : tProducts('syncNever')} + {!hasTencentCredentials ? ` - ${tProducts('syncUnavailable')}` : ''} +

+ {lastSync?.error ?

{lastSync.error}

: null} +
+ + {pendingTranslations.length > 0 ? : null} + + + + +
+ ); +} + +// --------------------------------------------------------------------------- +// Users +// --------------------------------------------------------------------------- + +function UsersSection({ + currentUserId, + users, + roles, + csrfToken, +}: { + currentUserId: number; + users: UserRow[]; + roles: readonly UserRole[]; + csrfToken: string; +}) { + const t = useTranslations('admin'); + const tCommon = useTranslations('common'); + const [createState, createAction] = useActionState(createUserAccount, initialState); + const [showCreate, setShowCreate] = useState(false); + + useEffect(() => { + if (createState.success) setShowCreate(false); + }, [createState.success]); + + return ( +
+
+

{t('users')}

+ +
+ + {showCreate ? ( +
+ + + + +
+ + {tCommon('save')} + +
+ {createState.message ? {createState.message} : null} + + ) : null} + +
+ + + + + + + + + + + + + {users.map((user) => ( + + ))} + +
{t('colEmail')}{t('colName')}{t('colRole')}{t('colStatus')}{t('colLastLogin')}{tCommon('actions')}
+
+
+ ); +} + +function UserRowItem({ + user, + roles, + isSelf, + csrfToken, +}: { + user: UserRow; + roles: readonly UserRole[]; + isSelf: boolean; + csrfToken: string; +}) { + const t = useTranslations('admin'); + const tCommon = useTranslations('common'); + const [state, formAction] = useActionState(updateUserAccount, initialState); + + return ( + + {user.email} + {user.displayName} + +
+ + + + + + {tCommon('save')} + {state.message ? {state.message} : null} +
+ + {user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : '-'} + + + ); +} + +// --------------------------------------------------------------------------- +// Translation review +// --------------------------------------------------------------------------- + +function TranslationReviewSection({ items, csrfToken }: { items: PendingTranslation[]; csrfToken: string }) { + const t = useTranslations('translation'); + return ( +
+

{t('reviewQueue')}

+
+ {items.map((item) => ( + + ))} +
+
+ ); +} + +function TranslationReviewItem({ item, csrfToken }: { item: PendingTranslation; csrfToken: string }) { + const t = useTranslations('translation'); + + return ( +
{ + await approveTranslation(formData); + }} + className="row" + style={{ borderBottom: '1px solid var(--line)', paddingBottom: 8 }} + > + + + +
+ + {item.entityType} #{item.entityId} - {item.field} - {item.locale} - {item.origin} + +

{item.value}

+
+ + +
+ ); +} + +// --------------------------------------------------------------------------- +// Data export / reset +// --------------------------------------------------------------------------- + +function DataSection({ csrfToken }: { csrfToken: string }) { + const tCommon = useTranslations('common'); + const [exporting, setExporting] = useState(false); + const [resetState, resetAction] = useActionState(resetToInitialCorpus, initialState); + + async function handleExport() { + setExporting(true); + try { + const formData = new FormData(); + formData.set(CSRF_FIELD, csrfToken); + const result = await exportAllData(formData); + if (!result.ok || !result.data) { + window.alert(result.message ?? 'Export failed.'); + return; + } + const blob = new Blob([JSON.stringify(result.data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `bd-os-export-${new Date().toISOString().slice(0, 10)}.json`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } finally { + setExporting(false); + } + } + + return ( +
+
+ +
+ +
{ + if (!window.confirm(tCommon('resetConfirm'))) event.preventDefault(); + }} + > + + + {tCommon('reset')} + {resetState.errors?.confirm ? {resetState.errors.confirm[0]} : null} + {resetState.message ? {resetState.message} : null} + {resetState.success ? {tCommon('saved')} : null} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Audit log +// --------------------------------------------------------------------------- + +function AuditSection({ entries }: { entries: AuditEntry[] }) { + const t = useTranslations('admin'); + + return ( +
+

{t('audit')}

+
+ + + + + + + + + + + + {entries.map((entry) => ( + + + + + + + + ))} + {entries.length === 0 ? ( + + + + ) : null} + +
{t('auditWhen')}{t('auditAction')}{t('auditActor')}{t('auditEntity')}{t('auditOutcome')}
{new Date(entry.createdAt).toLocaleString()}{entry.action}{entry.actorEmail || '-'} + {entry.entityType} {entry.entityId} + + + {entry.outcome} + +
+ {t('auditEmpty')} +
+
+
+ ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/admin/page.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/admin/page.tsx new file mode 100644 index 0000000000..f1da245ef5 --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/admin/page.tsx @@ -0,0 +1,67 @@ +import { getTranslations } from 'next-intl/server'; +import { redirect } from 'next/navigation'; + +import { USER_ROLES } from '@/domain/enums'; +import * as session from '@/lib/auth/session'; +import { readToken } from '@/lib/security/csrf'; +import { hasTencentCredentials } from '@/lib/env'; +import { listUsers } from '@/server/data/users'; +import { listNeedingReview } from '@/server/data/translations'; +import { getLastSyncRun } from '@/server/catalog/sync'; +import { loadAuditLog } from '@/server/actions/data-export'; + +import { AdminClient } from './AdminClient'; + +export default async function AdminPage({ params }: { params: Promise<{ locale: string }> }) { + const active = await session.current(); + if (!active) return null; + + const { locale } = await params; + if (active.user.role !== 'admin') { + redirect(`/${locale}/products`); + } + + const t = await getTranslations('admin'); + const users = listUsers(); + const pendingTranslations = listNeedingReview(); + const lastSync = getLastSyncRun(); + const auditResult = await loadAuditLog(); + const csrfToken = await readToken(active.sessionId); + + return ( +
+

{t('heading')}

+ + ({ + id: u.id, + email: u.email, + displayName: u.displayName, + role: u.role, + isActive: u.isActive, + lastLoginAt: u.lastLoginAt?.toISOString() ?? null, + createdAt: u.createdAt.toISOString(), + }))} + roles={USER_ROLES} + pendingTranslations={pendingTranslations.map((tr) => ({ + id: tr.id, + entityType: tr.entityType, + entityId: tr.entityId, + field: tr.field, + locale: tr.locale, + value: tr.value, + origin: tr.origin, + }))} + auditEntries={auditResult.entries ?? []} + lastSync={ + lastSync + ? { status: lastSync.status, finishedAt: lastSync.finishedAt?.toISOString() ?? null, sourceKind: lastSync.sourceKind, error: lastSync.error } + : null + } + hasTencentCredentials={hasTencentCredentials} + csrfToken={csrfToken} + /> +
+ ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/board/BoardClient.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/board/BoardClient.tsx new file mode 100644 index 0000000000..113c049a04 --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/board/BoardClient.tsx @@ -0,0 +1,216 @@ +'use client'; + +import { useActionState, useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; + +import { TODO_OWNERS, TODO_PRIORITIES, TODO_STATUSES } from '@/domain/enums'; +import type { TodoStatus } from '@/domain/enums'; +import type { Todo } from '@/db/schema'; +import { CSRF_FIELD } from '@/lib/security/csrf-constants'; +import { Modal } from '@/components/Modal'; +import { SelectField, TextAreaField, TextField } from '@/components/fields'; +import { SubmitButton } from '@/components/SubmitButton'; +import { moveTodoCard, removeTodo, saveTodo } from '@/server/actions/board'; +import type { FormActionState } from '@/server/actions/shared'; + +const initialState: FormActionState = {}; + +const COLUMN_LABELS: Record = { + Backlog: 'colBacklog', + 'This Week': 'colWeek', + 'In Progress': 'colProgress', + Done: 'colDone', +}; + +export function BoardClient({ items, canWrite, csrfToken }: { items: Todo[]; canWrite: boolean; csrfToken: string }) { + const t = useTranslations('board'); + const tCommon = useTranslations('common'); + const [modal, setModal] = useState<{ type: 'create' } | { type: 'edit'; todo: Todo } | null>(null); + + const byStatus = Object.fromEntries(TODO_STATUSES.map((status) => [status, items.filter((i) => i.status === status)])) as Record< + TodoStatus, + Todo[] + >; + + return ( + <> + {canWrite ? ( +
+ +
+ ) : null} + +
+ {TODO_STATUSES.map((status) => ( +
+

{t(COLUMN_LABELS[status] as 'colBacklog')}

+ {byStatus[status].map((todo) => ( + setModal({ type: 'edit', todo })} + /> + ))} + {byStatus[status].length === 0 ?

{tCommon('emptyState')}

: null} +
+ ))} +
+ + {modal ? setModal(null)} /> : null} + + ); +} + +function isOverdue(dueOn: string): boolean { + if (!dueOn) return false; + return dueOn < new Date().toISOString().slice(0, 10); +} + +function TodoCard({ + todo, + canWrite, + csrfToken, + onEdit, +}: { + todo: Todo; + canWrite: boolean; + csrfToken: string; + onEdit: () => void; +}) { + const t = useTranslations('board'); + const tCommon = useTranslations('common'); + + return ( +
+ ); +} + +function MoveSelect({ todo, csrfToken }: { todo: Todo; csrfToken: string }) { + return ( +
{ + await moveTodoCard(formData); + }} + > + + + {/* Appends to the end of the target column; within-column reordering is + not exposed in this UI -- `position` still exists for a future drag + interaction to use without a data model change. */} + + +
+ ); +} + +function TodoFormModal({ todo, csrfToken, onClose }: { todo?: Todo; csrfToken: string; onClose: () => void }) { + const t = useTranslations('board'); + const tCommon = useTranslations('common'); + const [state, formAction] = useActionState(saveTodo, initialState); + + useEffect(() => { + if (state.success) onClose(); + }, [state.success, onClose]); + + return ( + +
+

{todo ? t('editTitle') : t('createTitle')}

+ +
+ + {state.message ? ( +

+ {state.message} +

+ ) : null} + +
+ + {todo ? : null} + +
+ + + + + + + +
+
+ +
+ + + {tCommon('save')} + +
+
+ ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/board/page.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/board/page.tsx new file mode 100644 index 0000000000..25b1eac48b --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/board/page.tsx @@ -0,0 +1,56 @@ +import { getTranslations } from 'next-intl/server'; + +import { TODO_OWNERS, TODO_PRIORITIES } from '@/domain/enums'; +import { todoFilterSchema } from '@/domain/schemas'; +import { can } from '@/lib/auth/rbac'; +import * as session from '@/lib/auth/session'; +import { readToken } from '@/lib/security/csrf'; +import { FilterForm } from '@/components/FilterForm'; +import { listTodos } from '@/server/data/board'; + +import { BoardClient } from './BoardClient'; + +export default async function BoardPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const active = await session.current(); + if (!active) return null; + + const raw = await searchParams; + const filter = todoFilterSchema.parse({ owner: raw.owner, priority: raw.priority }); + + const [t, tCommon] = await Promise.all([getTranslations('board'), getTranslations('common')]); + const todos = listTodos(filter); + const canWrite = can(active.user.role, 'board.write'); + const csrfToken = await readToken(active.sessionId); + + return ( + <> + + + + + + + + + ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/layout.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/layout.tsx new file mode 100644 index 0000000000..53304a9994 --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/layout.tsx @@ -0,0 +1,61 @@ +import { getTranslations } from 'next-intl/server'; +import { redirect } from 'next/navigation'; + +import { permissionsFor } from '@/lib/auth/rbac'; +import * as session from '@/lib/auth/session'; +import { LocaleSwitcher } from '@/components/LocaleSwitcher'; +import { NavTabs } from '@/components/NavTabs'; +import { SignOutForm } from '@/components/SignOutForm'; + +/** + * Chrome for every authenticated dashboard route: header, tab strip, and the + * `
` content wrapper. + * + * This is a second, independent authentication check on top of the redirect + * middleware already performs -- see the note in `src/middleware.ts` on why + * the Edge layer cannot itself validate a session. This is the check that + * actually grants or denies the page. + */ +export default async function AppLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const active = await session.current(); + + if (!active) { + redirect(`/${locale}/login`); + } + + const t = await getTranslations('app'); + const permissions = permissionsFor(active.user.role); + + return ( + <> +
+
+
+

{t('title')}

+
{t('subtitle')}
+
+
+ + {t('signedInAs', { name: active.user.displayName || active.user.email })} + {' · '} + {t('role')}: {active.user.role} + + + +
+
+
+ + + +
{children}
+ + ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/page.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/page.tsx new file mode 100644 index 0000000000..e11321fbdd --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from '@/i18n/navigation'; + +/** The dashboard's root path has no content of its own; the catalog is home. */ +export default async function DashboardRoot({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + redirect({ href: '/products', locale }); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/playbooks/PlaybooksClient.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/playbooks/PlaybooksClient.tsx new file mode 100644 index 0000000000..39636eb18d --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/playbooks/PlaybooksClient.tsx @@ -0,0 +1,132 @@ +'use client'; + +import { useActionState, useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; + +import type { Motion } from '@/db/schema'; +import { CSRF_FIELD } from '@/lib/security/csrf-constants'; +import { Modal } from '@/components/Modal'; +import { TextAreaField, TextField } from '@/components/fields'; +import { SubmitButton } from '@/components/SubmitButton'; +import { removeMotion, saveMotion } from '@/server/actions/board'; +import type { FormActionState } from '@/server/actions/shared'; + +const initialState: FormActionState = {}; + +export function PlaybooksClient({ items, canWrite, csrfToken }: { items: Motion[]; canWrite: boolean; csrfToken: string }) { + const t = useTranslations('playbooks'); + const tCommon = useTranslations('common'); + const [modal, setModal] = useState<{ type: 'create' } | { type: 'edit'; motion: Motion } | null>(null); + + return ( + <> +
+ {canWrite ? ( + + ) : null} +
+ +
+ {items.map((motion) => ( +
+

{motion.opportunity}

+

+ {t('fieldTriggerSignals')}: {motion.triggerSignals || '-'} +

+

+ {t('fieldIcp')}: {motion.icp || '-'} +

+

+ {t('fieldWedgeProduct')}: {motion.wedgeProduct || '-'} +

+ {canWrite ? ( +
+ +
{ + await removeMotion(formData); + }} + onSubmit={(event) => { + if (!window.confirm(tCommon('confirmDelete'))) event.preventDefault(); + }} + > + + + +
+
+ ) : null} +
+ ))} + {items.length === 0 ?

{tCommon('emptyState')}

: null} +
+ + {modal ? ( + setModal(null)} /> + ) : null} + + ); +} + +function MotionFormModal({ motion, csrfToken, onClose }: { motion?: Motion; csrfToken: string; onClose: () => void }) { + const t = useTranslations('playbooks'); + const tCommon = useTranslations('common'); + const [state, formAction] = useActionState(saveMotion, initialState); + + useEffect(() => { + if (state.success) onClose(); + }, [state.success, onClose]); + + return ( + +
+
+

{motion ? t('editTitle') : t('createTitle')}

+

{t('subtitle')}

+
+ +
+ + {state.message ? ( +

+ {state.message} +

+ ) : null} + +
+ + {motion ? : null} + +
+ + + + + + + + + + +
+
+ +
+ + + {tCommon('save')} + +
+
+ ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/playbooks/page.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/playbooks/page.tsx new file mode 100644 index 0000000000..9925fa92af --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/playbooks/page.tsx @@ -0,0 +1,47 @@ +import { getTranslations } from 'next-intl/server'; + +import { can } from '@/lib/auth/rbac'; +import * as session from '@/lib/auth/session'; +import { readToken } from '@/lib/security/csrf'; +import { FilterForm } from '@/components/FilterForm'; +import { listMotions } from '@/server/data/board'; + +import { PlaybooksClient } from './PlaybooksClient'; + +export default async function PlaybooksPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const active = await session.current(); + if (!active) return null; + + const { q } = await searchParams; + const query = typeof q === 'string' ? q : ''; + + const t = await getTranslations('playbooks'); + const motions = listMotions(query); + const canWrite = can(active.user.role, 'playbook.write'); + const csrfToken = await readToken(active.sessionId); + + return ( + <> +
+
+

{t('bannerLibraryTitle')}

+

{t('bannerLibraryBody')}

+
+
+

{t('bannerUsageTitle')}

+

{t('bannerUsageBody')}

+
+
+ + + + + + + + ); +} diff --git a/tencent-bd-dashboard/src/app/[locale]/(app)/products/ProductsClient.tsx b/tencent-bd-dashboard/src/app/[locale]/(app)/products/ProductsClient.tsx new file mode 100644 index 0000000000..5ceaf80b8a --- /dev/null +++ b/tencent-bd-dashboard/src/app/[locale]/(app)/products/ProductsClient.tsx @@ -0,0 +1,704 @@ +'use client'; + +import { useActionState, useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; + +import { + KNOWLEDGE_LEVELS, + PRIORITIES, + PRIORITY_SCORE_MAX, + PRIORITY_SCORE_MIN, + CONFIDENCE_LEVELS, + PRODUCT_STATUSES, + COMMERCIAL_CATEGORIES, + SELL_MODES, + EVIDENCE_LEVELS, + CAN_SAY_OPTIONS, + EVIDENCE_CONFIDENCE, +} from '@/domain/enums'; +import type { Product, ProductEvidence } from '@/db/schema'; +import { CSRF_FIELD } from '@/lib/security/csrf-constants'; +import { Link } from '@/i18n/navigation'; +import { Modal } from '@/components/Modal'; +import { SelectField, TextAreaField, TextField } from '@/components/fields'; +import { SubmitButton } from '@/components/SubmitButton'; +import { TranslateField } from '@/components/TranslateField'; +import { + removeProduct, + runCatalogSync, + saveEvidenceStudio, + savePriorityScore, + saveProduct, + saveProductStory, +} from '@/server/actions/products'; +import { loadProductEvidence } from '@/server/actions/read'; +import type { FormActionState } from '@/server/actions/shared'; + +type ModalState = + | { type: 'create' } + | { type: 'edit'; product: Product } + | { type: 'story'; product: Product } + | { type: 'score'; product: Product } + | { type: 'evidence'; product: Product }; + +const initialState: FormActionState = {}; + +function priorityTone(priority: string): string { + if (priority === 'P1') return 'danger'; + if (priority === 'P2') return 'warn'; + return 'brand'; +} + +export function ProductsClient({ + items, + total, + page, + pageSize, + canWrite, + canDelete, + canSync, + csrfToken, + syncStatus, + hasTencentCredentials, + filterQuery, +}: { + items: Product[]; + total: number; + page: number; + pageSize: number; + canWrite: boolean; + canDelete: boolean; + canSync: boolean; + csrfToken: string; + syncStatus: { status: string; finishedAt: string | null; sourceKind: string } | null; + hasTencentCredentials: boolean; + filterQuery: Record; +}) { + const t = useTranslations('products'); + const tCommon = useTranslations('common'); + const [modal, setModal] = useState(null); + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + + return ( + <> +
+
+ {canWrite ? ( + + ) : null} + {canSync ? : null} +
+ + {syncStatus + ? t('syncLast', { when: syncStatus.finishedAt ? new Date(syncStatus.finishedAt).toLocaleString() : '-', source: syncStatus.sourceKind }) + : t('syncNever')} + {!hasTencentCredentials ? ` · ${t('syncUnavailable')}` : ''} + +
+ +
+ + + + + + + + + + + + + + + + + + {items.map((product) => ( + + + + + + + + + + + + + + ))} + {items.length === 0 ? ( + + + + ) : null} + +
{t('colPriority')}{t('colCommercial')}{t('colProduct')}{t('colCategory')}{t('colStory')}{t('colCompetitors')}{t('colPainPoints')}{t('colKnowledge')}{t('colConfidence')}{t('colStatus')}{tCommon('actions')}
+ + {product.priority} + + {product.commercialCategory}{product.product}{product.category}{truncate(product.solutionStory, 90)}{truncate(product.competitors, 80)}{truncate(product.painPoints, 80)}{product.knowledge}{product.confidence}{product.status} +
+ {canWrite ? ( + <> + + + + + + ) : null} + {canDelete ? : null} +
+
+ {tCommon('emptyState')} +
+
+ + + + {modal?.type === 'create' || modal?.type === 'edit' ? ( + setModal(null)} + /> + ) : null} + {modal?.type === 'story' ? ( + setModal(null)} /> + ) : null} + {modal?.type === 'score' ? ( + setModal(null)} /> + ) : null} + {modal?.type === 'evidence' ? ( + setModal(null)} /> + ) : null} + + ); +} + +function truncate(value: string, max: number): string { + const trimmed = value.trim(); + if (trimmed.length <= max) return trimmed || '-'; + return `${trimmed.slice(0, max)}...`; +} + +function Pagination({ + page, + totalPages, + total, + pageSize, + filterQuery, + tCommon, +}: { + page: number; + totalPages: number; + total: number; + pageSize: number; + filterQuery: Record; + tCommon: ReturnType; +}) { + if (totalPages <= 1) return null; + + // Built from server-known state (the parsed filter, passed down as a prop) + // rather than `window.location`, which does not exist during the initial + // server render of this client component. + const queryFor = (p: number) => ({ ...filterQuery, page: String(p) }); + + return ( +
+ {tCommon('showing', { count: total === 0 ? 0 : (page - 1) * pageSize + 1, total })} +
+ {page > 1 ? ( + + {tCommon('previousPage')} + + ) : ( + + {tCommon('previousPage')} + + )} + + {page} / {totalPages} + + {page < totalPages ? ( + + {tCommon('nextPage')} + + ) : ( + + {tCommon('nextPage')} + + )} +
+
+ ); +} + +function DeleteProductButton({ id, csrfToken, label }: { id: number; csrfToken: string; label: string }) { + const tCommon = useTranslations('common'); + return ( +
{ + await removeProduct(formData); + }} + onSubmit={(event) => { + if (!window.confirm(tCommon('confirmDelete'))) event.preventDefault(); + }} + > + + + +
+ ); +} + +function SyncButton({ csrfToken, t }: { csrfToken: string; t: ReturnType }) { + const [state, formAction] = useActionState(runCatalogSync, initialState); + return ( +
+ + {t('syncCatalog')} + {state.message ? ( + + {state.message} + + ) : null} +
+ ); +} + +// --------------------------------------------------------------------------- +// Create / edit product +// --------------------------------------------------------------------------- + +function ProductFormModal({ product, csrfToken, onClose }: { product?: Product; csrfToken: string; onClose: () => void }) { + const t = useTranslations('products'); + const tCommon = useTranslations('common'); + const [state, formAction] = useActionState(saveProduct, initialState); + + useEffect(() => { + if (state.success) onClose(); + }, [state.success, onClose]); + + return ( + +
+

{product ? t('editTitle') : t('createTitle')}

+ +
+ + {state.message ? ( +

+ {state.message} +

+ ) : null} + +
+ + {product ? : null} + +
+ + + + + + + + +
+ + {product ? ( + + ) : null} +
+ + + + + + + + + + + +
+
+ +
+ + + {tCommon('save')} + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Solution story +// --------------------------------------------------------------------------- + +function StoryModal({ product, csrfToken, onClose }: { product: Product; csrfToken: string; onClose: () => void }) { + const t = useTranslations('story'); + const tCommon = useTranslations('common'); + const [state, formAction] = useActionState(saveProductStory, initialState); + + useEffect(() => { + if (state.success) onClose(); + }, [state.success, onClose]); + + return ( + +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+ + {state.message ? ( +

+ {state.message} +

+ ) : null} + +
+ + +
+ + + + + + +
+ + +
+
+
+ {t('formulaTitle')} +

{t('formulaBody')}

+
+
+ +
+ + + {t('save')} + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Priority scorecard +// --------------------------------------------------------------------------- + +const SCORE_DIMENSIONS = [ + { key: 'demand', field: 'scoreDemand' as const }, + { key: 'rightToWin', field: 'scoreRightToWin' as const }, + { key: 'entry', field: 'scoreEntry' as const }, + { key: 'poc', field: 'scorePoc' as const }, + { key: 'expansion', field: 'scoreExpansion' as const }, + { key: 'revenue', field: 'scoreRevenue' as const }, +]; + +function ScoreModal({ product, csrfToken, onClose }: { product: Product; csrfToken: string; onClose: () => void }) { + const t = useTranslations('priority'); + const tCommon = useTranslations('common'); + const [state, formAction] = useActionState(savePriorityScore, initialState); + const [scores, setScores] = useState>( + Object.fromEntries(SCORE_DIMENSIONS.map((d) => [d.key, product[d.field]])), + ); + + useEffect(() => { + if (state.success) onClose(); + }, [state.success, onClose]); + + const total = Object.values(scores).reduce((sum, v) => sum + v, 0); + const max = SCORE_DIMENSIONS.length * PRIORITY_SCORE_MAX; + const recommendation = total >= max * 0.8 ? 'P1' : total >= max * 0.55 ? 'P2' : 'P3'; + + return ( + +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+ + {state.message ? ( +

+ {state.message} +

+ ) : null} + +
+ + + +
+ {SCORE_DIMENSIONS.map((d) => ( + + ))} +
+ +
+
+
{t('total')}
+ + {t('totalHint')} + +
+ + {total} / {max} + + + {recommendation} + +
+ +
+ + +
+
+ +
+ + + {t('save')} + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Evidence Studio +// --------------------------------------------------------------------------- + +type EvidenceRowDraft = Pick< + ProductEvidence, + 'level' | 'statement' | 'canSay' | 'source' | 'confidence' | 'verifiedOn' | 'notes' +>; + +function blankEvidenceRow(): EvidenceRowDraft { + return { level: 'Assumption', statement: '', canSay: 'No', source: '', confidence: 'Low', verifiedOn: '', notes: '' }; +} + +function EvidenceModal({ product, csrfToken, onClose }: { product: Product; csrfToken: string; onClose: () => void }) { + const t = useTranslations('evidence'); + const tCommon = useTranslations('common'); + const [state, formAction] = useActionState(saveEvidenceStudio, initialState); + const [rows, setRows] = useState(null); + const [loadError, setLoadError] = useState(null); + + useEffect(() => { + let cancelled = false; + loadProductEvidence(product.id).then((result) => { + if (cancelled) return; + if (result.ok && result.rows) { + setRows( + result.rows.map((r) => ({ + level: r.level, + statement: r.statement, + canSay: r.canSay, + source: r.source, + confidence: r.confidence, + verifiedOn: r.verifiedOn, + notes: r.notes, + })), + ); + } else { + setLoadError(result.message ?? 'Failed to load evidence.'); + } + }); + return () => { + cancelled = true; + }; + // Runs once per product id; loadProductEvidence is a stable server action reference. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [product.id]); + + useEffect(() => { + if (state.success) onClose(); + }, [state.success, onClose]); + + const verifiedCount = (rows ?? []).filter((r) => r.canSay === 'Yes').length; + + return ( + +
+
+

{t('title')}

+

{t('subtitle')}

+
+ +
+ + {state.message ? ( +

+ {state.message} +

+ ) : null} + +
+
+ {t('guideFactTitle')} +

{t('guideFactBody')}

+
+
+ {t('guideInferenceTitle')} +

{t('guideInferenceBody')}

+
+
+ {t('guideOutcomeTitle')} +

{t('guideOutcomeBody')}

+
+
+ + {rows === null ? ( +

{loadError ?? tCommon('loading')}

+ ) : ( +
+ + + + +
+
+

{t('tableTitle')}

+ {t('summary', { verified: verifiedCount, total: rows.length })} +
+

{t('tableHint')}

+ + {rows.map((row, index) => ( +
+ +
+
{todo.title}
+
+ + {todo.owner} + + + {todo.priority} + + {todo.dueOn ? ( + + {todo.dueOn} {isOverdue(todo.dueOn) ? `(${t('overdue')})` : ''} + + ) : null} +
+ {todo.detail ?

{todo.detail}

: null} + {todo.link ? ( +

+ + {todo.link} + +

+ ) : null} + + {canWrite ? ( +
+ + +
{ + await removeTodo(formData); + }} + onSubmit={(event) => { + if (!window.confirm(tCommon('confirmDelete'))) event.preventDefault(); + }} + > + + + +
+
+ ) : null} +