diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 705dc01fd..9f8e1e073 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,7 @@ jobs: # is the PR this job starts covering it. AARCH64_CRATES: >- -p litebox + -p litebox_broker_core -p litebox_broker_userland -p litebox_common_linux -p litebox_egress_proxy @@ -318,6 +319,9 @@ jobs: # `std` access, since it owns hosted Linux broker resources such as # sockets and the epoll reactor. # + # - `litebox_broker_platform_windows_userland` is allowed to have + # `std` access, since it owns hosted Windows broker resources. + # # - `litebox_broker_userland` is allowed to have `std` access, # since it is the hosted userland broker executable. # @@ -387,6 +391,7 @@ jobs: -not -path './Cargo.toml' \ -not -path './litebox_broker_local_userland/Cargo.toml' \ -not -path './litebox_broker_platform_linux_userland/Cargo.toml' \ + -not -path './litebox_broker_platform_windows_userland/Cargo.toml' \ -not -path './litebox_broker_transport_linux_userland/Cargo.toml' \ -not -path './litebox_broker_transport_windows_userland/Cargo.toml' \ -not -path './litebox_broker_userland/Cargo.toml' \ diff --git a/Cargo.lock b/Cargo.lock index ea8b1557b..9606139a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1449,7 +1449,6 @@ dependencies = [ "bitflags", "buddy_system_allocator", "hashbrown", - "litebox_broker_core", "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", @@ -1459,7 +1458,6 @@ dependencies = [ "slabmalloc", "smallvec", "spin 0.9.8", - "tempfile", "thiserror", "windows-sys 0.60.2", "zerocopy", @@ -1476,6 +1474,7 @@ dependencies = [ "litebox_util_log", "spin 0.9.8", "tar-no-std", + "tempfile", "thiserror", ] @@ -1523,10 +1522,19 @@ dependencies = [ "rustix", ] +[[package]] +name = "litebox_broker_platform_windows_userland" +version = "0.1.0" +dependencies = [ + "litebox_platform", + "windows-sys 0.60.2", +] + [[package]] name = "litebox_broker_protocol" version = "0.1.0" dependencies = [ + "bitflags", "thiserror", ] @@ -1567,6 +1575,7 @@ dependencies = [ "litebox_broker_host", "litebox_broker_local", "litebox_broker_platform_linux_userland", + "litebox_broker_platform_windows_userland", "litebox_broker_protocol", "litebox_broker_transport", "litebox_broker_transport_linux_userland", @@ -1587,6 +1596,7 @@ dependencies = [ "elf", "int-enum", "litebox", + "litebox_broker_protocol", "syscalls", "thiserror", "zerocopy", @@ -1795,7 +1805,6 @@ dependencies = [ "litebox_shim_linux", "litebox_syscall_rewriter", "litebox_util_log", - "memmap2", "sha2", "tracing-subscriber", "walkdir", @@ -1863,6 +1872,7 @@ dependencies = [ "anyhow", "clap", "litebox", + "litebox_broker_local_userland", "litebox_common_linux", "litebox_common_windows", "litebox_platform_linux_userland", @@ -1885,7 +1895,6 @@ dependencies = [ "litebox_shim_windows", "litebox_syscall_rewriter", "litebox_util_log", - "memmap2", "tar", "tracing-subscriber", ] @@ -1925,7 +1934,6 @@ dependencies = [ "bitvec", "libc", "litebox", - "litebox_broker_core", "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", @@ -1938,9 +1946,7 @@ dependencies = [ "object", "once_cell", "ringbuf", - "spin 0.9.8", "syscalls", - "tempfile", "thiserror", "zerocopy", ] @@ -1979,6 +1985,11 @@ dependencies = [ "bitflags", "int-enum", "litebox", + "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", + "litebox_broker_protocol", + "litebox_broker_transport", "litebox_common_linux", "litebox_common_windows", "litebox_platform", @@ -2072,15 +2083,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memmap2" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" -dependencies = [ - "libc", -] - [[package]] name = "minimal-lexical" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 8ad47a0e9..c51df626a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "litebox_broker_local_userland", "litebox_broker_core", "litebox_broker_platform_linux_userland", + "litebox_broker_platform_windows_userland", "litebox_broker_protocol", "litebox_broker_host", "litebox_broker_transport", @@ -48,6 +49,7 @@ default-members = [ "litebox_broker_local_userland", "litebox_broker_core", "litebox_broker_platform_linux_userland", + "litebox_broker_platform_windows_userland", "litebox_broker_protocol", "litebox_broker_host", "litebox_broker_transport", diff --git a/dev_bench/unixbench/run_unixbench.py b/dev_bench/unixbench/run_unixbench.py index 0fe02d66c..4c7d8bb48 100644 --- a/dev_bench/unixbench/run_unixbench.py +++ b/dev_bench/unixbench/run_unixbench.py @@ -438,6 +438,7 @@ def run_litebox( cmd = [ str(broker_path), + "--fs-initial-files", str(tar_path), "--runner", str(runner_path), "--env", "LD_LIBRARY_PATH=/lib64:/lib32:/lib", "--env", "HOME=/", @@ -491,6 +492,7 @@ def run_litebox_windows( broker_path = runner_path.with_name("litebox-broker-userland.exe") cmd = [ str(broker_path), + "--fs-initial-files", str(tar_path), "--runner", str(runner_path), "--env", "LD_LIBRARY_PATH=/lib64:/lib32:/lib", "--env", "HOME=/", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index ca27ed956..4aabd15ab 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -46,7 +46,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_platform_windows_userland/", 8), ("litebox_runner_lvbs/", 6), ("litebox_runner_snp/", 2), - ("litebox_shim_linux/", 2), + ("litebox_shim_linux/", 1), ("litebox_shim_optee/", 6), ("litebox_shim_windows/", 1), ("litebox_runner_windows_userland/", 2), diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 520ca728e..6a0996212 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -15,7 +15,6 @@ 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_core = { version = "0.1.0", path = "../litebox_broker_core" } 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" } @@ -37,6 +36,3 @@ enforce_singleton_litebox_instance = [] [lints] workspace = true - -[dev-dependencies] -tempfile = "3" diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index e8177f2ac..11f995740 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -12,6 +12,10 @@ use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::event::{ConsumeEventResponse, EventConsumeMode}; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileOpenFlags, FileSeekWhence, + FileStatus, FileUser, MAX_FILE_TRANSFER_SIZE, +}; use litebox_broker_protocol::pipe::{CreatePipeResponse, MAX_PIPE_TRANSFER_SIZE}; use litebox_broker_protocol::random::MAX_RANDOM_TRANSFER_SIZE; use litebox_broker_protocol::readiness::ReadinessFlags; @@ -180,6 +184,96 @@ pub(crate) trait BrokerControl: Send + Sync { data: &[u8], ) -> core::result::Result; + fn open_file( + &self, + path: &str, + user: FileUser, + access: FileAccessMode, + flags: FileOpenFlags, + mode: FileMode, + ) -> core::result::Result, BrokerControlError>; + + fn read_file( + &self, + handle: ObjectHandle, + data: &mut [u8], + offset: Option, + ) -> core::result::Result, BrokerControlError>; + + fn write_file( + &self, + handle: ObjectHandle, + data: &[u8], + offset: Option, + ) -> core::result::Result, BrokerControlError>; + + fn seek_file( + &self, + handle: ObjectHandle, + offset: i64, + whence: FileSeekWhence, + ) -> core::result::Result, BrokerControlError>; + + fn truncate_file( + &self, + handle: ObjectHandle, + length: u64, + reset_offset: bool, + ) -> core::result::Result, BrokerControlError>; + + fn read_directory( + &self, + handle: ObjectHandle, + ) -> core::result::Result< + core::result::Result, FileError>, + BrokerControlError, + >; + + fn path_file_status( + &self, + path: &str, + user: FileUser, + ) -> core::result::Result, BrokerControlError>; + + fn handle_file_status( + &self, + handle: ObjectHandle, + ) -> core::result::Result, BrokerControlError>; + + fn chmod_file( + &self, + path: &str, + user: FileUser, + mode: FileMode, + ) -> core::result::Result, BrokerControlError>; + + fn chown_file( + &self, + path: &str, + acting_user: FileUser, + user: Option, + group: Option, + ) -> core::result::Result, BrokerControlError>; + + fn unlink_file( + &self, + path: &str, + user: FileUser, + ) -> core::result::Result, BrokerControlError>; + + fn mkdir_file( + &self, + path: &str, + user: FileUser, + mode: FileMode, + ) -> core::result::Result, BrokerControlError>; + + fn rmdir_file( + &self, + path: &str, + user: FileUser, + ) -> core::result::Result, BrokerControlError>; + fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError>; fn fail_connection(&self); @@ -617,6 +711,159 @@ where self.request(|local| local.write_pipe(handle, lease.descriptor(), data)) } + fn open_file( + &self, + path: &str, + user: FileUser, + access: FileAccessMode, + flags: FileOpenFlags, + mode: FileMode, + ) -> core::result::Result, BrokerControlError> + { + let length = file_transfer_length(path.len())?; + let lease = self.acquire_shared_buffer(length)?; + self.request(|local| local.open_file(lease.descriptor(), path, user, access, flags, mode)) + } + + fn read_file( + &self, + handle: ObjectHandle, + data: &mut [u8], + offset: Option, + ) -> core::result::Result, BrokerControlError> { + let length = file_transfer_length(data.len().min(MAX_FILE_TRANSFER_SIZE as usize))?; + let data = &mut data[..length as usize]; + let lease = self.acquire_shared_buffer(length)?; + self.request(|local| local.read_file(handle, lease.descriptor(), data, offset)) + } + + fn write_file( + &self, + handle: ObjectHandle, + data: &[u8], + offset: Option, + ) -> core::result::Result, BrokerControlError> { + let length = file_transfer_length(data.len().min(MAX_FILE_TRANSFER_SIZE as usize))?; + let data = &data[..length as usize]; + let lease = self.acquire_shared_buffer(length)?; + self.request(|local| local.write_file(handle, lease.descriptor(), data, offset)) + } + + fn seek_file( + &self, + handle: ObjectHandle, + offset: i64, + whence: FileSeekWhence, + ) -> core::result::Result, BrokerControlError> { + self.request(|local| local.seek_file(handle, offset, whence)) + } + + fn truncate_file( + &self, + handle: ObjectHandle, + length: u64, + reset_offset: bool, + ) -> core::result::Result, BrokerControlError> { + self.request(|local| local.truncate_file(handle, length, reset_offset)) + } + + fn read_directory( + &self, + handle: ObjectHandle, + ) -> core::result::Result< + core::result::Result, FileError>, + BrokerControlError, + > { + let mut entries = Vec::new(); + let mut start_index = 0; + loop { + let lease = self.acquire_shared_buffer(MAX_FILE_TRANSFER_SIZE)?; + let response = self + .request(|local| local.read_directory(handle, lease.descriptor(), start_index))?; + let (mut chunk, next_index) = match response { + Ok(response) => response, + Err(error) => return Ok(Err(error)), + }; + entries + .try_reserve(chunk.len()) + .map_err(|_| BrokerControlError::Broker(ErrorCode::OutOfMemory))?; + entries.append(&mut chunk); + let Some(next_index) = next_index else { + return Ok(Ok(entries)); + }; + assert!( + next_index > start_index, + "broker returned a non-advancing file directory continuation" + ); + start_index = next_index; + } + } + + fn path_file_status( + &self, + path: &str, + user: FileUser, + ) -> core::result::Result, BrokerControlError> { + let lease = self.acquire_shared_buffer(file_transfer_length(path.len())?)?; + self.request(|local| local.path_file_status(lease.descriptor(), path, user)) + } + + fn handle_file_status( + &self, + handle: ObjectHandle, + ) -> core::result::Result, BrokerControlError> { + self.request(|local| local.handle_file_status(handle)) + } + + fn chmod_file( + &self, + path: &str, + user: FileUser, + mode: FileMode, + ) -> core::result::Result, BrokerControlError> { + let lease = self.acquire_shared_buffer(file_transfer_length(path.len())?)?; + self.request(|local| local.chmod_file(lease.descriptor(), path, user, mode)) + } + + fn chown_file( + &self, + path: &str, + acting_user: FileUser, + user: Option, + group: Option, + ) -> core::result::Result, BrokerControlError> { + let lease = self.acquire_shared_buffer(file_transfer_length(path.len())?)?; + self.request(|local| local.chown_file(lease.descriptor(), path, acting_user, user, group)) + } + + fn unlink_file( + &self, + path: &str, + user: FileUser, + ) -> core::result::Result, BrokerControlError> { + let lease = self.acquire_shared_buffer(file_transfer_length(path.len())?)?; + self.request(|local| local.unlink_file(lease.descriptor(), path, user)) + } + + fn mkdir_file( + &self, + path: &str, + user: FileUser, + mode: FileMode, + ) -> core::result::Result, BrokerControlError> { + let lease = self.acquire_shared_buffer(file_transfer_length(path.len())?)?; + self.request(|local| local.mkdir_file(lease.descriptor(), path, user, mode)) + } + + fn rmdir_file( + &self, + path: &str, + user: FileUser, + ) -> core::result::Result, BrokerControlError> { + let lease = self.acquire_shared_buffer(file_transfer_length(path.len())?)?; + self.request(|local| local.rmdir_file(lease.descriptor(), path, user)) + } + fn close_object(&self, handle: ObjectHandle) -> core::result::Result<(), BrokerControlError> { self.request(|local| local.close_object(handle)) } @@ -626,6 +873,13 @@ where } } +fn file_transfer_length(length: usize) -> core::result::Result { + if length > MAX_FILE_TRANSFER_SIZE as usize { + return Err(BrokerControlError::Broker(ErrorCode::ResourceExhausted)); + } + Ok(u32::try_from(length).expect("validated file transfer length must fit in u32")) +} + pub(crate) fn readiness_events(readiness: ReadinessFlags) -> Events { let mut events = Events::empty(); events.set(Events::IN, readiness.contains(ReadinessFlags::READ)); diff --git a/litebox/src/fs/errors.rs b/litebox/src/fs/errors.rs index 459caaea2..49184f5c6 100644 --- a/litebox/src/fs/errors.rs +++ b/litebox/src/fs/errors.rs @@ -1,20 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Possible errors from [`Resolver`] - -#[expect( - unused_imports, - reason = "used for doc string links to work out, but not for code" -)] -use super::resolver::Resolver; +//! Errors from LiteBox file operations. +use litebox_broker_protocol::fs::FileMode; use thiserror::Error; // XXX(jayb): We probably need to introduce a notion of `Stale` to many/most of these errors, in // order to more correctly support network-attached file systems. -/// Possible errors from [`Resolver::open`] +/// Possible errors from [`crate::LiteBox::open_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum OpenError { @@ -34,12 +29,12 @@ pub enum OpenError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::close`] +/// Possible errors from [`crate::LiteBox::close_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum CloseError {} -/// Possible errors from [`Resolver::read`] +/// Possible errors from [`crate::LiteBox::read_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum ReadError { @@ -53,7 +48,7 @@ pub enum ReadError { Io, } -/// Possible errors from [`Resolver::write`] +/// Possible errors from [`crate::LiteBox::write_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum WriteError { @@ -67,7 +62,7 @@ pub enum WriteError { Io, } -/// Possible errors from [`Resolver::seek`] +/// Possible errors from [`crate::LiteBox::seek_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum SeekError { @@ -83,7 +78,7 @@ pub enum SeekError { Io, } -/// Possible errors from [`Resolver::truncate`] +/// Possible errors from [`crate::LiteBox::truncate_file`]. #[derive(Error, Debug)] pub enum TruncateError { #[error("fd has been closed already")] @@ -98,7 +93,7 @@ pub enum TruncateError { Io, } -/// Possible errors from [`Resolver::chmod`] +/// Possible errors from [`crate::LiteBox::chmod_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum ChmodError { @@ -115,7 +110,7 @@ pub enum ChmodError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::chown`] +/// Possible errors from [`crate::LiteBox::chown_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum ChownError { @@ -132,7 +127,7 @@ pub enum ChownError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::unlink`] +/// Possible errors from [`crate::LiteBox::unlink_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum UnlinkError { @@ -148,7 +143,7 @@ pub enum UnlinkError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::mkdir`] +/// Possible errors from [`crate::LiteBox::mkdir_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum MkdirError { @@ -164,7 +159,7 @@ pub enum MkdirError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::rmdir`] +/// Possible errors from [`crate::LiteBox::rmdir_file`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum RmdirError { @@ -186,7 +181,7 @@ pub enum RmdirError { PathError(#[from] PathError), } -/// Possible errors from [`Resolver::read_dir`] +/// Possible errors from [`crate::LiteBox::read_file_directory`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum ReadDirError { @@ -198,7 +193,7 @@ pub enum ReadDirError { Io, } -/// Possible errors from [`Resolver::file_status`] +/// Possible errors from [`crate::LiteBox::path_file_status`] and [`crate::LiteBox::file_status`]. #[non_exhaustive] #[derive(Error, Debug)] pub enum FileStatusError { @@ -210,16 +205,6 @@ pub enum FileStatusError { PathError(#[from] PathError), } -/// Possible errors from a backend walk -#[non_exhaustive] -#[derive(Error, Debug)] -pub enum WalkError { - #[error("I/O error")] - Io, - #[error(transparent)] - PathError(#[from] PathError), -} - /// Possible errors in any file-system function due to path errors. #[derive(Error, Debug)] pub enum PathError { @@ -230,7 +215,7 @@ pub enum PathError { #[cfg(debug_assertions)] dir: alloc::string::String, #[cfg(debug_assertions)] - perms: crate::fs::Mode, + perms: FileMode, }, #[error("invalid characters, not permitted by underlying file system")] InvalidPathname, diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs new file mode 100644 index 000000000..5f1d93bb4 --- /dev/null +++ b/litebox/src/fs/file.rs @@ -0,0 +1,514 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Guest file operations backed by broker-owned file objects. + +use alloc::string::{String, ToString}; +use alloc::sync::Arc; +use alloc::vec; +use alloc::vec::Vec; + +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::error::ErrorCode; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileDirectoryEntry, FileError, FileMode as Mode, FileOpenFlags, + FileSeekWhence as SeekWhence, FileStatus, FileUser as UserInfo, +}; + +use crate::path::Arg; +use crate::{LiteBox, sync}; + +use super::errors::{ + ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError, + ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, +}; + +impl LiteBox { + fn broker_file(&self, fd: &FileFd) -> Option> { + self.descriptor_table() + .with_entry(fd, |entry| Arc::clone(&entry.entry)) + } + + fn broker_path(context: &Context, path: impl Arg) -> Result { + Ok(context.resolve(path)?.to_string()) + } + + /// Opens a file. + /// + /// `access` and `flags` use the architecture-independent broker contract. + /// The `mode` is only significant when creating a file. + pub fn open_file( + &self, + context: &Context, + path: impl Arg, + access: FileAccessMode, + flags: FileOpenFlags, + mode: Mode, + ) -> Result { + let path = Self::broker_path(context, path)?; + if FileOpenFlags::from_bits(flags.bits()).is_none() { + return Err(OpenError::AccessNotAllowed); + } + let broker = self.broker_control().ok_or(OpenError::Io)?; + let handle = broker + .open_file( + &path, + context.acting_user(), + access, + flags, + mode & Mode::SUPPORTED, + ) + .map_err(|_| OpenError::Io)? + .map_err(open_error)?; + Ok(self + .descriptor_table_mut() + .insert(Arc::new(BrokerFile { broker, handle }))) + } + + /// Close the file at `fd`. + /// + /// Future operations on the `fd` will start to return `ClosedFd` errors. + pub fn close_file(&self, fd: &FileFd) -> Result<(), CloseError> { + let mut descriptors = self.descriptor_table_mut(); + let removed = descriptors.remove(fd); + drop(descriptors); + drop(removed); + Ok(()) + } + + /// Read from a file descriptor at `offset` into a buffer. + pub fn read_file( + &self, + fd: &FileFd, + buf: &mut [u8], + offset: Option, + ) -> Result { + let file = self.broker_file(fd).ok_or(ReadError::ClosedFd)?; + file.broker + .read_file( + file.handle, + buf, + offset + .map(u64::try_from) + .transpose() + .map_err(|_| ReadError::Io)?, + ) + .map_err(|error| broker_fd_error(error, ReadError::ClosedFd, ReadError::Io))? + .map_err(read_error) + } + + /// Write from a buffer to a file descriptor at `offset`. + pub fn write_file( + &self, + fd: &FileFd, + buf: &[u8], + offset: Option, + ) -> Result { + let file = self.broker_file(fd).ok_or(WriteError::ClosedFd)?; + file.broker + .write_file( + file.handle, + buf, + offset + .map(u64::try_from) + .transpose() + .map_err(|_| WriteError::Io)?, + ) + .map_err(|error| broker_fd_error(error, WriteError::ClosedFd, WriteError::Io))? + .map_err(write_error) + } + + /// Reposition the read/write file offset. + pub fn seek_file( + &self, + fd: &FileFd, + offset: isize, + whence: SeekWhence, + ) -> Result { + let file = self.broker_file(fd).ok_or(SeekError::ClosedFd)?; + let offset = i64::try_from(offset).map_err(|_| SeekError::InvalidOffset)?; + let offset = file + .broker + .seek_file(file.handle, offset, whence) + .map_err(|error| broker_fd_error(error, SeekError::ClosedFd, SeekError::Io))? + .map_err(seek_error)?; + usize::try_from(offset).map_err(|_| SeekError::InvalidOffset) + } + + /// Truncate the file to the specified length. + pub fn truncate_file( + &self, + fd: &FileFd, + length: usize, + reset_offset: bool, + ) -> Result<(), TruncateError> { + let file = self.broker_file(fd).ok_or(TruncateError::ClosedFd)?; + file.broker + .truncate_file( + file.handle, + u64::try_from(length).map_err(|_| TruncateError::Io)?, + reset_offset, + ) + .map_err(|error| broker_fd_error(error, TruncateError::ClosedFd, TruncateError::Io))? + .map_err(truncate_error) + } + + /// Change the permissions of a file. + pub fn chmod_file( + &self, + context: &Context, + path: impl Arg, + mode: Mode, + ) -> Result<(), ChmodError> { + let path = Self::broker_path(context, path)?; + self.broker_control() + .ok_or(ChmodError::Io)? + .chmod_file(&path, context.acting_user(), mode & Mode::SUPPORTED) + .map_err(|_| ChmodError::Io)? + .map_err(chmod_error) + } + + /// Change the owner of a file. + pub fn chown_file( + &self, + context: &Context, + path: impl Arg, + user: Option, + group: Option, + ) -> Result<(), ChownError> { + let path = Self::broker_path(context, path)?; + self.broker_control() + .ok_or(ChownError::Io)? + .chown_file(&path, context.acting_user(), user, group) + .map_err(|_| ChownError::Io)? + .map_err(chown_error) + } + + /// Unlink a file. + pub fn unlink_file(&self, context: &Context, path: impl Arg) -> Result<(), UnlinkError> { + let path = Self::broker_path(context, path)?; + self.broker_control() + .ok_or(UnlinkError::Io)? + .unlink_file(&path, context.acting_user()) + .map_err(|_| UnlinkError::Io)? + .map_err(unlink_error) + } + + /// Create a new directory. + pub fn mkdir_file( + &self, + context: &Context, + path: impl Arg, + mode: Mode, + ) -> Result<(), MkdirError> { + let path = Self::broker_path(context, path)?; + self.broker_control() + .ok_or(MkdirError::Io)? + .mkdir_file(&path, context.acting_user(), mode & Mode::SUPPORTED) + .map_err(|_| MkdirError::Io)? + .map_err(mkdir_error) + } + + /// Remove a directory. + pub fn rmdir_file(&self, context: &Context, path: impl Arg) -> Result<(), RmdirError> { + let path = Self::broker_path(context, path)?; + self.broker_control() + .ok_or(RmdirError::Io)? + .rmdir_file(&path, context.acting_user()) + .map_err(|_| RmdirError::Io)? + .map_err(rmdir_error) + } + + /// Read directory entries from a directory file descriptor. + pub fn read_file_directory( + &self, + fd: &FileFd, + ) -> Result, ReadDirError> { + let file = self.broker_file(fd).ok_or(ReadDirError::ClosedFd)?; + file.broker + .read_directory(file.handle) + .map_err(|error| broker_fd_error(error, ReadDirError::ClosedFd, ReadDirError::Io))? + .map_err(read_dir_error) + } + + /// Obtain the status of a path. + pub fn path_file_status( + &self, + context: &Context, + path: impl Arg, + ) -> Result { + let path = Self::broker_path(context, path)?; + self.broker_control() + .ok_or(FileStatusError::Io)? + .path_file_status(&path, context.acting_user()) + .map_err(|_| FileStatusError::Io)? + .map_err(file_status_error) + } + + /// Equivalent to [`Self::path_file_status`], but on an open `fd`. + pub fn file_status(&self, fd: &FileFd) -> Result { + let file = self.broker_file(fd).ok_or(FileStatusError::ClosedFd)?; + file.broker + .handle_file_status(file.handle) + .map_err(|error| { + broker_fd_error(error, FileStatusError::ClosedFd, FileStatusError::Io) + })? + .map_err(file_status_error) + } +} + +/// Caller-owned filesystem context containing a working directory and acting user. +#[derive(Clone, Debug)] +pub struct Context { + cwd: Arc, + user_info: UserInfo, +} + +impl Context { + /// The user that operations on this context act as. + #[must_use] + pub fn acting_user(&self) -> UserInfo { + self.user_info + } + + /// Set the user that operations on this context act as. + pub fn set_acting_user(&mut self, user: UserInfo) { + self.user_info = user; + } + + /// The current working directory. + #[must_use] + pub fn cwd(&self) -> &ResolvedPath { + &self.cwd + } + + /// Set the current working directory. + pub fn set_cwd(&mut self, cwd: ResolvedPath) { + self.cwd = Arc::new(cwd); + } + + /// A new default context, anchored at `/` for a non-root user. + #[must_use] + pub fn new() -> Self { + Self { + cwd: Arc::new(ResolvedPath { components: vec![] }), + user_info: UserInfo { + user: 1000, + group: 1000, + }, + } + } + + /// Resolve `path` against the current context. + pub fn resolve(&self, path: impl Arg) -> Result { + let mut components = if path.as_rust_str()?.starts_with('/') { + vec![] + } else { + self.cwd.components.clone() + }; + for component in path.components()? { + match component { + "" | "." => {} + ".." => { + let _ = components.pop(); + } + _ => components.push(component.into()), + } + } + Ok(ResolvedPath { components }) + } +} + +impl Default for Context { + fn default() -> Self { + Self::new() + } +} + +/// Absolute normalized path, created from [`Context::resolve`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedPath { + components: Vec, +} + +impl core::fmt::Display for ResolvedPath { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for component in &self.components { + write!(formatter, "/{component}")?; + } + if self.components.is_empty() { + formatter.write_str("/")?; + } + Ok(()) + } +} + +/// Guest-side reference to a broker-owned file and the subsystem type for [`FileFd`]. +/// +/// The broker object is closed when its last descriptor or in-flight operation releases its +/// reference. +pub struct BrokerFile { + broker: Arc, + handle: ObjectHandle, +} + +impl Drop for BrokerFile { + fn drop(&mut self) { + let _ = self.broker.close_object(self.handle); + } +} + +fn broker_fd_error(error: crate::broker::error::BrokerControlError, closed: T, io: T) -> T { + match error { + crate::broker::error::BrokerControlError::Broker( + ErrorCode::UnknownObject | ErrorCode::InvalidRights, + ) => closed, + crate::broker::error::BrokerControlError::AssociationFailed + | crate::broker::error::BrokerControlError::Broker(_) => io, + } +} + +// TODO: Define canonical per-operation protocol errors so these conversions can be removed without +// broadening every LiteBox file API to the full set of `FileError` variants. +fn path_error(error: FileError) -> Option { + match error { + FileError::NoSuchFileOrDirectory => Some(PathError::NoSuchFileOrDirectory), + FileError::NoSearchPermissions => Some(PathError::NoSearchPerms { + #[cfg(debug_assertions)] + dir: String::new(), + #[cfg(debug_assertions)] + perms: Mode::empty(), + }), + FileError::InvalidPathname => Some(PathError::InvalidPathname), + FileError::MissingComponent => Some(PathError::MissingComponent), + FileError::ComponentNotDirectory => Some(PathError::ComponentNotADirectory), + _ => None, + } +} + +fn open_error(error: FileError) -> OpenError { + if let Some(error) = path_error(error) { + return error.into(); + } + match error { + FileError::AccessNotAllowed => OpenError::AccessNotAllowed, + FileError::NoWritePermissions => OpenError::NoWritePerms, + FileError::ReadOnlyFs => OpenError::ReadOnlyFileSystem, + FileError::AlreadyExists => OpenError::AlreadyExists, + FileError::IsDirectory => OpenError::TruncateError(TruncateError::IsDirectory), + FileError::NotForWriting => OpenError::TruncateError(TruncateError::NotForWriting), + FileError::IsTerminalDevice => OpenError::TruncateError(TruncateError::IsTerminalDevice), + _ => OpenError::Io, + } +} + +fn read_error(error: FileError) -> ReadError { + match error { + FileError::NotFile => ReadError::NotAFile, + FileError::NotForReading => ReadError::NotForReading, + _ => ReadError::Io, + } +} + +fn write_error(error: FileError) -> WriteError { + match error { + FileError::NotFile => WriteError::NotAFile, + FileError::NotForWriting => WriteError::NotForWriting, + _ => WriteError::Io, + } +} + +fn seek_error(error: FileError) -> SeekError { + match error { + FileError::NotFile => SeekError::NotAFile, + FileError::InvalidOffset => SeekError::InvalidOffset, + FileError::NonSeekable => SeekError::NonSeekable, + _ => SeekError::Io, + } +} + +fn truncate_error(error: FileError) -> TruncateError { + match error { + FileError::IsDirectory => TruncateError::IsDirectory, + FileError::NotForWriting => TruncateError::NotForWriting, + FileError::IsTerminalDevice => TruncateError::IsTerminalDevice, + _ => TruncateError::Io, + } +} + +fn chmod_error(error: FileError) -> ChmodError { + if let Some(error) = path_error(error) { + return error.into(); + } + match error { + FileError::NotOwner => ChmodError::NotTheOwner, + FileError::ReadOnlyFs => ChmodError::ReadOnlyFileSystem, + _ => ChmodError::Io, + } +} + +fn chown_error(error: FileError) -> ChownError { + if let Some(error) = path_error(error) { + return error.into(); + } + match error { + FileError::NotOwner => ChownError::NotTheOwner, + FileError::ReadOnlyFs => ChownError::ReadOnlyFileSystem, + _ => ChownError::Io, + } +} + +fn unlink_error(error: FileError) -> UnlinkError { + if let Some(error) = path_error(error) { + return error.into(); + } + match error { + FileError::NoWritePermissions => UnlinkError::NoWritePerms, + FileError::IsDirectory => UnlinkError::IsADirectory, + FileError::ReadOnlyFs => UnlinkError::ReadOnlyFileSystem, + _ => UnlinkError::Io, + } +} + +fn mkdir_error(error: FileError) -> MkdirError { + if let Some(error) = path_error(error) { + return error.into(); + } + match error { + FileError::NoWritePermissions => MkdirError::NoWritePerms, + FileError::AlreadyExists => MkdirError::AlreadyExists, + FileError::ReadOnlyFs => MkdirError::ReadOnlyFileSystem, + _ => MkdirError::Io, + } +} + +fn rmdir_error(error: FileError) -> RmdirError { + if let Some(error) = path_error(error) { + return error.into(); + } + match error { + FileError::NoWritePermissions => RmdirError::NoWritePerms, + FileError::Busy => RmdirError::Busy, + FileError::NotEmpty => RmdirError::NotEmpty, + FileError::NotDirectory => RmdirError::NotADirectory, + FileError::ReadOnlyFs => RmdirError::ReadOnlyFileSystem, + _ => RmdirError::Io, + } +} + +fn read_dir_error(error: FileError) -> ReadDirError { + match error { + FileError::NotDirectory => ReadDirError::NotADirectory, + _ => ReadDirError::Io, + } +} + +fn file_status_error(error: FileError) -> FileStatusError { + path_error(error).map_or(FileStatusError::Io, Into::into) +} + +crate::fd::enable_fds_for_subsystem! { + BrokerFile; + Arc; + -> FileFd; +} diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index c59d72682..2b9d40bf7 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -1,315 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Guest-facing filesystem facade. +//! Guest-facing file operations and local descriptor integration. //! //! Filesystem resolution and backend implementations live in `litebox_broker_core`. This module -//! retains LiteBox's guest values, descriptor integration, and compatibility module paths. - -use bitflags::bitflags; - -use core::ffi::c_uint; -use core::num::NonZeroUsize; +//! retains caller context and local descriptors. Shared file values are defined in +//! [`litebox_broker_protocol::fs`]. pub mod errors; -pub mod resolver; - -// TODO: Remove these implementation-facing compatibility modules once LiteBox uses the broker -// file APIs exclusively. They temporarily preserve local filesystem construction while resolver -// and backend ownership moves into broker core. -#[doc(hidden)] -pub mod backend { - pub use litebox_broker_core::fs::backend::*; -} - -#[doc(hidden)] -pub mod composer { - pub use litebox_broker_core::fs::composer::*; -} - -#[doc(hidden)] -pub mod devices { - pub use litebox_broker_core::fs::devices::*; -} - -#[doc(hidden)] -pub mod in_mem { - pub use litebox_broker_core::fs::in_mem::{InMem, InMemDirHandle, InMemFileHandle}; - - /// A node used to pre-populate an [`InMem`] backend, via [`InMem::new_initialized`]. - pub enum InitialNode { - /// A directory. - Directory { - /// Permission bits for the directory. - mode: super::Mode, - /// Owning user and group. - owner: super::UserInfo, - }, - /// A regular file, along with its contents. - File { - /// Permission bits for the file. - mode: super::Mode, - /// Owning user and group. - owner: super::UserInfo, - /// The file's contents. - /// - /// Borrowed data is kept borrowed until the first write to the file, which makes this - /// the cheap way to set up large read-heavy files (such as executables). - data: alloc::borrow::Cow<'static, [u8]>, - }, - } - - impl From for litebox_broker_core::fs::in_mem::InitialNode { - fn from(node: InitialNode) -> Self { - match node { - InitialNode::Directory { mode, owner } => Self::Directory { - mode: litebox_broker_core::fs::Mode::from_bits_retain(mode.bits()), - owner: litebox_broker_core::fs::UserInfo { - user: owner.user, - group: owner.group, - }, - }, - InitialNode::File { mode, owner, data } => Self::File { - mode: litebox_broker_core::fs::Mode::from_bits_retain(mode.bits()), - owner: litebox_broker_core::fs::UserInfo { - user: owner.user, - group: owner.group, - }, - data, - }, - } - } - } -} - -#[doc(hidden)] -#[cfg(test)] -pub(crate) mod inode_allocator { - pub(crate) use litebox_broker_core::fs::inode_allocator::*; -} - -#[doc(hidden)] -pub mod nine_p { - pub use litebox_broker_core::fs::nine_p::*; -} - -#[doc(hidden)] -pub mod overlay { - pub use litebox_broker_core::fs::overlay::*; -} - -#[doc(hidden)] -pub mod tar_ro { - pub use litebox_broker_core::fs::tar_ro::*; -} - -#[cfg(test)] -mod tests; - -#[cfg(all(test, target_os = "linux"))] -#[path = "nine_p/tests.rs"] -mod nine_p_tests; - -bitflags! { - /// `S_I*` constants for open, ... - #[repr(transparent)] - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] - pub struct Mode: c_uint { - /// `S_IRWXU`: user (file owner) has read, write, and execute permission - const RWXU = 0o00700; - /// `S_IRUSR`: user has read permission - const RUSR = 0o00400; - /// `S_IWUSR`: user has write permission - const WUSR = 0o00200; - /// `S_IXUSR`: user has execute permission - const XUSR = 0o00100; - /// `S_IRWXG`: group has read, write, and execute permission - const RWXG = 0o00070; - /// `S_IRGRP`: group has read permission - const RGRP = 0o00040; - /// `S_IWGRP`: group has write permission - const WGRP = 0o00020; - /// `S_IXGRP`: group has execute permission - const XGRP = 0o00010; - /// `S_IRWXO`: others have read, write, and execute permission - const RWXO = 0o00007; - /// `S_IROTH`: others have read permission - const ROTH = 0o00004; - /// `S_IWOTH`: others have write permission - const WOTH = 0o00002; - /// `S_IXOTH`: others have execute permission - const XOTH = 0o00001; - /// `S_ISUID`: set-user-ID bit - const SUID = 0o0004000; - /// `S_ISGID`: set-group-ID bit (see inode(7)). - const SGID = 0o0002000; - /// `S_ISVTX`: sticky bit (see inode(7)). - const SVTX = 0o0001000; - /// - const _ = !0; - } -} - -/// Types of files on a file-system. -/// -/// See [`resolver::Resolver::file_status`]. -#[derive(Debug, PartialEq, Eq, Clone)] -#[non_exhaustive] -pub enum FileType { - RegularFile, - Directory, - CharacterDevice, -} - -bitflags! { - /// `O_*` constants for use with open, ... - #[repr(transparent)] - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] - pub struct OFlags: c_uint { - /// `O_RDONLY`: read-only - const RDONLY = 0x0; - /// `O_WRONLY`: write-only - const WRONLY = 0x1; - /// `O_RDWR`: read/write. - /// - /// This is not equal to `RDONLY | WRONLY`. It's a distinct flag. - const RDWR = 0x2; - /// `O_APPEND`: append mode - const APPEND = 0x400; - /// `O_ASYNC`: signal-driven I/O - const ASYNC = 0x2000; - /// `O_CLOEXEC`: close-on-exec flag - const CLOEXEC = 0x80000; - /// `O_CREAT`: if path does not exist, create it as a regular file - const CREAT = 0x40; - /// `O_DIRECT`: try to minimize cache effects of I/O for this file - #[cfg(target_arch = "x86_64")] - const DIRECT = 0x4000; - #[cfg(target_arch = "aarch64")] - const DIRECT = 0x10000; - /// `O_DIRECTORY`: fail if not a directory - #[cfg(target_arch = "x86_64")] - const DIRECTORY = 0x10000; - #[cfg(target_arch = "aarch64")] - const DIRECTORY = 0x4000; - /// `O_DSYNC`: write operations on the file will complete according to the requirements of - /// synchronized I/O *data* integrity completion. - const DSYNC = 0x1000; - /// `O_EXCL`: exclusive use - const EXCL = 0x80; - /// `O_LARGEFILE`: allow large file support - #[cfg(target_arch = "x86_64")] - const LARGEFILE = 0x8000; - #[cfg(target_arch = "aarch64")] - const LARGEFILE = 0x20000; - /// `O_NOATIME`: do not update access time - const NOATIME = 0x40000; - /// `O_NOCTTY`: do not assign controlling terminal - const NOCTTY = 0x100; - /// `O_NOFOLLOW`: fail if the path does not point to a regular file - #[cfg(target_arch = "x86_64")] - const NOFOLLOW = 0x20000; - #[cfg(target_arch = "aarch64")] - const NOFOLLOW = 0x8000; - /// `O_NDELAY`: non-blocking mode (same as NONBLOCK) - const NDELAY = 0x800; - /// `O_NONBLOCK`: non-blocking mode (same as NDELAY) - const NONBLOCK = 0x800; - /// `O_PATH`: open a file descriptor for path resolution only - const PATH = 0x200000; - /// `O_SYNC`: write operations on the file will complete according to the requirements of - /// synchronized I/O file integrity completion (by contrast with the synchronized I/O data - /// integrity completion provided by `O_DSYNC`.) - const SYNC = 0x101000; - /// `O_TMPFILE`: create an unnamed temporary file - #[cfg(target_arch = "x86_64")] - const TMPFILE = 0x410000; - #[cfg(target_arch = "aarch64")] - const TMPFILE = 0x404000; - /// `O_TRUNC`: truncate the file to zero length - const TRUNC = 0x200; - /// - const _ = !0; - - /// All file status flags + access modes - const STATUS_FLAGS_MASK = Self::APPEND.bits() - | Self::NONBLOCK.bits() - | Self::DSYNC.bits() - | Self::ASYNC.bits() - | Self::DIRECT.bits() - | Self::LARGEFILE.bits() - | Self::NOATIME.bits() - | Self::SYNC.bits() - | Self::PATH.bits() - | Self::RDONLY.bits() - | Self::WRONLY.bits() - | Self::RDWR.bits(); - } -} - -/// The `whence` directive to [`resolver::Resolver::seek`] -#[derive(Copy, Clone)] -pub enum SeekWhence { - /// The file offset is set to `offset` bytes. - RelativeToBeginning, - /// The file offset is set to its current location plus `offset` bytes. - RelativeToCurrentOffset, - /// The file offset is set to the size of the file plus `offset` bytes. - RelativeToEnd, -} - -/// The status of a file/directory/... on the file-system, inspired by `stat(3type)`. -/// -/// This is explicitly a non-exhaustive struct with public members. As LiteBox evolves, more -/// elements might be added to this struct, allowing file systems to provide richer information -/// about the status of files. However, users of LiteBox must not depend on the completeness or even -/// layout of this particular type. -#[non_exhaustive] -pub struct FileStatus { - /// File type - pub file_type: FileType, - /// Permissions for the file - pub mode: Mode, - /// Size of the file, in bytes. This value considered informative if this is a regular file. - pub size: usize, - /// Owner of the file - pub owner: UserInfo, - /// Information about this particular node - pub node_info: NodeInfo, - /// Block size for file system I/O - pub blksize: usize, -} - -/// User information -#[derive(Clone, Copy, Debug)] -pub struct UserInfo { - /// User ID for the owner - pub user: u16, - /// Group ID for the owner - pub group: u16, -} - -/// Device/Inode information -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -pub struct NodeInfo { - /// Device number - pub dev: usize, - /// Inode number - pub ino: usize, - /// Device that is being referred to (will be `Some(...)` only if special file) - pub rdev: Option, -} - -/// Directory entries returned by [`resolver::Resolver::read_dir`] -#[derive(Debug)] -#[non_exhaustive] -pub struct DirEntry { - pub name: alloc::string::String, - pub file_type: FileType, - pub ino_info: Option, -} +mod file; -impl UserInfo { - /// The root user - pub const ROOT: Self = Self { user: 0, group: 0 }; -} +pub use file::{BrokerFile, Context, FileFd}; diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/resolver.rs deleted file mode 100644 index ff89b754a..000000000 --- a/litebox/src/fs/resolver.rs +++ /dev/null @@ -1,649 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Guest filesystem facade backed directly by the broker-core filesystem engine. - -use alloc::string::{String, ToString}; -use alloc::sync::Arc; -use alloc::vec; -use alloc::vec::Vec; - -use litebox_broker_core::fs as broker_fs; -use litebox_broker_core::fs::backend::DeviceIo; -use litebox_broker_core::fs::resolver::{Resolver as BrokerResolver, ResolverEntry}; - -use crate::path::Arg; -use crate::{LiteBox, fd::TypedFd, sync}; - -use super::errors::{ - ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError, - ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, -}; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, SeekWhence, UserInfo}; - -/// The guest-facing filesystem entry point. -pub struct Resolver< - Platform: sync::RawSyncPrimitivesProvider, - Backend: broker_fs::backend::Backend + 'static, -> { - litebox: LiteBox, - engine: BrokerResolver, -} - -impl - Resolver -{ - /// Construct a new resolver over `backend`. - #[must_use] - pub fn new(litebox: &LiteBox, backend: Backend) -> Self { - Self { - litebox: litebox.clone(), - engine: BrokerResolver::new(backend), - } - } -} - -impl DeviceIo for LiteBox { - fn read_stdin(&self, output: &mut [u8]) -> Result { - LiteBox::read_stdio(self, output).map_err(|_| broker_fs::errors::ReadError::Io) - } - - fn write_stdio( - &self, - stream: litebox_broker_protocol::stdio::StdioOutputStream, - input: &[u8], - ) -> Result { - LiteBox::write_stdio(self, stream, input).map_err(|_| broker_fs::errors::WriteError::Io) - } - - fn fill_random(&self, output: &mut [u8]) -> Result<(), broker_fs::errors::ReadError> { - LiteBox::fill_random(self, output).map_err(|_| broker_fs::errors::ReadError::Io) - } -} - -/// Per-call resolution context. The user may hold and mutate this as they wish. -/// -/// This struct is deliberately cheap to clone. -// NOTE(jayb): I generally dislike getters/setters for fields of a data-like struct (e.g., see -// acting_user and set_acting_user here), but I'm putting these here since I am not yet convinced -// that we won't need more things in the context, nor am I convinced that we might not need the -// ability to lock down how contexts are made/used. In some sense, I am forcing some chokepoints -// here. In the future, we might flatten these out and just allow access to the fields directly. -#[derive(Clone, Debug)] -pub struct Context { - /// Current working directory. - cwd: Arc, - /// Effective user for permission checks. - user_info: UserInfo, -} - -impl Context { - /// The user that operations on this context act as. - #[must_use] - pub fn acting_user(&self) -> UserInfo { - self.user_info - } - - /// Set the user that operations on this context act as. - pub fn set_acting_user(&mut self, user: UserInfo) { - self.user_info = user; - } - - /// The current working directory. - #[must_use] - pub fn cwd(&self) -> &ResolvedPath { - &self.cwd - } - - /// Set the current working directory. - pub fn set_cwd(&mut self, cwd: ResolvedPath) { - self.cwd = Arc::new(cwd); - } - - /// A new default context, anchored at `/` for a non-root user. - pub fn new() -> Context { - Self { - cwd: Arc::new(ResolvedPath { components: vec![] }), - user_info: UserInfo { - user: 1000, - group: 1000, - }, - } - } - - /// Resolve `path` against the current context. - // XXX(jayb): if/when we support chroot, we might need to tweak this to not allow "escaping" - // outside the chrooted part. - // XXX(jayb): since we are migrating all resolution into the resolver, we probably don't need - // `Arg` anymore, so could get rid of it in the future. - pub fn resolve(&self, path: impl Arg) -> Result { - let mut components = if path.as_rust_str()?.starts_with('/') { - vec![] - } else { - self.cwd.components.clone() - }; - for component in path.components()? { - match component { - "" | "." => {} - ".." => { - let _ = components.pop(); - } - _ => { - components.push(component.into()); - } - } - } - Ok(ResolvedPath { components }) - } -} - -impl Default for Context { - fn default() -> Self { - Self::new() - } -} - -/// Absolute normalized path, must only be created from [`Context::resolve`]. -/// -/// Note that a resolved path does not imply that it exists within the file system, merely that it -/// is an absolute normalized path. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ResolvedPath { - // Note: an empty path is equivalent to `/`. - components: Vec, -} - -impl core::fmt::Display for ResolvedPath { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - for component in &self.components { - write!(f, "/{component}")?; - } - if self.components.is_empty() { - f.write_str("/")?; - } - Ok(()) - } -} - -impl - Resolver -{ - /// Opens a file. - /// - /// The `mode` is only significant when creating a file. - pub fn open( - &self, - context: &Context, - path: impl Arg, - flags: OFlags, - mode: Mode, - ) -> Result, OpenError> { - let path = context.resolve(path)?.to_string(); - let entry = self - .engine - .open( - broker_user_info(context.acting_user()), - &path, - broker_open_flags(flags), - broker_mode(mode), - ) - .map_err(guest_open_error)?; - Ok(self.litebox.descriptor_table_mut().insert(entry)) - } - - /// Close the file at `fd`. - /// - /// Future operations on the `fd` will start to return `ClosedFd` errors. - pub fn close(&self, fd: &TypedFd) -> Result<(), CloseError> { - let mut descriptors = self.litebox.descriptor_table_mut(); - let removed = descriptors.remove(fd); - drop(descriptors); - // Some backends might block while closing an fd, so release the descriptor-table lock - // before dropping the backend handle. - drop(removed); - Ok(()) - } - - /// Read from a file descriptor at `offset` into a buffer. - /// - /// If `offset` is None, the read will start at the current file offset and update the file - /// offset to the end of the read. - /// If `offset` is Some, the file offset is not changed. - /// - /// # Panics - /// - /// Panics if the updated file offset would overflow `usize`. - pub fn read( - &self, - fd: &TypedFd, - buf: &mut [u8], - offset: Option, - ) -> Result { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(ReadError::ClosedFd)?; - let mut entry = entry.get_entry_mut(); - // XXX(jayb): This deliberately preserves the current descriptor-entry lock across backend - // I/O. A later PR can introduce a smaller position/append serialization primitive. - self.engine - .read(&self.litebox, &mut entry.entry, buf, offset) - .map_err(guest_read_error) - } - - /// Write from a buffer to a file descriptor at `offset`. - /// - /// If `offset` is None, the write will start at the current file offset and update the file - /// offset to the end of the write. - /// If `offset` is Some, the file offset is not changed. - /// - /// # Panics - /// - /// Panics if the updated file offset would overflow `usize`. - pub fn write( - &self, - fd: &TypedFd, - buf: &[u8], - offset: Option, - ) -> Result { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(WriteError::ClosedFd)?; - let mut entry = entry.get_entry_mut(); - // XXX(jayb): This deliberately preserves the current descriptor-entry lock across backend - // I/O. A later PR can introduce a smaller position/append serialization primitive. - self.engine - .write(&self.litebox, &mut entry.entry, buf, offset) - .map_err(guest_write_error) - } - - /// Reposition the read/write file offset, by changing it to `offset` relative to `whence`. - /// - /// Returns the resulting offset (in bytes from start of file) on success. - pub fn seek( - &self, - fd: &TypedFd, - offset: isize, - whence: SeekWhence, - ) -> Result { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(SeekError::ClosedFd)?; - let mut entry = entry.get_entry_mut(); - self.engine - .seek(&mut entry.entry, offset, broker_seek_whence(whence)) - .map_err(guest_seek_error) - } - - /// Truncate the file to the specified length. - /// - /// If shorter than existing size, extra data is lost. If longer than existing size, resize by - /// adding `\0`s. - /// - /// If `reset_offset` is true, the offset is reset to zero; otherwise, it remains unchanged. - pub fn truncate( - &self, - fd: &TypedFd, - length: usize, - reset_offset: bool, - ) -> Result<(), TruncateError> { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(TruncateError::ClosedFd)?; - let mut entry = entry.get_entry_mut(); - self.engine - .truncate(&mut entry.entry, length, reset_offset) - .map_err(guest_truncate_error) - } - - /// Change the permissions of a file. - pub fn chmod(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), ChmodError> { - let path = context.resolve(path)?.to_string(); - self.engine - .chmod( - broker_user_info(context.acting_user()), - &path, - broker_mode(mode), - ) - .map_err(guest_chmod_error) - } - - /// Change the owner of a file. - pub fn chown( - &self, - context: &Context, - path: impl Arg, - user: Option, - group: Option, - ) -> Result<(), ChownError> { - let path = context.resolve(path)?.to_string(); - self.engine - .chown(broker_user_info(context.acting_user()), &path, user, group) - .map_err(guest_chown_error) - } - - /// Unlink a file. - pub fn unlink(&self, context: &Context, path: impl Arg) -> Result<(), UnlinkError> { - let path = context.resolve(path)?.to_string(); - self.engine - .unlink(broker_user_info(context.acting_user()), &path) - .map_err(guest_unlink_error) - } - - /// Create a new directory. - pub fn mkdir(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), MkdirError> { - let path = context.resolve(path)?.to_string(); - self.engine - .mkdir( - broker_user_info(context.acting_user()), - &path, - broker_mode(mode), - ) - .map_err(guest_mkdir_error) - } - - /// Remove a directory. - pub fn rmdir(&self, context: &Context, path: impl Arg) -> Result<(), RmdirError> { - let path = context.resolve(path)?.to_string(); - self.engine - .rmdir(broker_user_info(context.acting_user()), &path) - .map_err(guest_rmdir_error) - } - - /// Read directory entries from a directory file descriptor. - /// - /// Returns a list of file/directory names including synthesized `.` and `..` entries. - pub fn read_dir(&self, fd: &TypedFd) -> Result, ReadDirError> { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(ReadDirError::ClosedFd)?; - let entry = entry.get_entry(); - self.engine - .read_dir(&entry.entry) - .map_err(guest_read_dir_error) - .map(guest_directory_entries) - } - - /// Obtain the status of a path. - pub fn file_status( - &self, - context: &Context, - path: impl Arg, - ) -> Result { - let path = context.resolve(path)?.to_string(); - self.engine - .file_status(broker_user_info(context.acting_user()), &path) - .map_err(guest_file_status_error) - .map(guest_file_status) - } - - /// Equivalent to [`Self::file_status`], but on an open `fd`. - pub fn fd_file_status(&self, fd: &TypedFd) -> Result { - let entry = self - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(FileStatusError::ClosedFd)?; - let entry = entry.get_entry(); - self.engine - .handle_status(&entry.entry) - .map_err(guest_file_status_error) - .map(guest_file_status) - } - - /// Get static backing data for a file, if available and supported. - /// - /// This method returns the (entire) underlying static byte slice if the file's contents are - /// backed by borrowed static data (e.g., set up via [`super::in_mem::InitialNode::File`]). - /// - /// Returns `None` if no static backing data is available/supported. - pub fn get_static_backing_data(&self, fd: &TypedFd) -> Option<&'static [u8]> { - let entry = self.litebox.descriptor_table().entry_handle(fd)?; - let entry = entry.get_entry(); - self.engine.get_static_backing_data(&entry.entry) - } -} - -// TODO: Remove most of the guest/core conversion helpers below once LiteBox uses the broker file -// APIs exclusively. They temporarily preserve LiteBox's guest-facing types while this facade calls -// the broker-core engine directly. -fn broker_mode(mode: Mode) -> broker_fs::Mode { - broker_fs::Mode::from_bits_retain(mode.bits()) -} - -fn guest_mode(mode: broker_fs::Mode) -> Mode { - Mode::from_bits_retain(mode.bits()) -} - -fn broker_open_flags(flags: OFlags) -> broker_fs::OFlags { - broker_fs::OFlags::from_bits_retain(flags.bits()) -} - -fn broker_user_info(user: UserInfo) -> broker_fs::UserInfo { - broker_fs::UserInfo { - user: user.user, - group: user.group, - } -} - -fn guest_user_info(user: broker_fs::UserInfo) -> UserInfo { - UserInfo { - user: user.user, - group: user.group, - } -} - -fn broker_seek_whence(whence: SeekWhence) -> broker_fs::SeekWhence { - match whence { - SeekWhence::RelativeToBeginning => broker_fs::SeekWhence::RelativeToBeginning, - SeekWhence::RelativeToCurrentOffset => broker_fs::SeekWhence::RelativeToCurrentOffset, - SeekWhence::RelativeToEnd => broker_fs::SeekWhence::RelativeToEnd, - } -} - -fn guest_file_type(file_type: broker_fs::FileType) -> FileType { - match file_type { - broker_fs::FileType::RegularFile => FileType::RegularFile, - broker_fs::FileType::Directory => FileType::Directory, - broker_fs::FileType::CharacterDevice => FileType::CharacterDevice, - } -} - -fn guest_node_info(node: broker_fs::NodeInfo) -> NodeInfo { - NodeInfo { - dev: node.dev, - ino: node.ino, - rdev: node.rdev, - } -} - -fn guest_file_status(status: broker_fs::FileStatus) -> FileStatus { - FileStatus { - file_type: guest_file_type(status.file_type), - mode: guest_mode(status.mode), - size: status.size, - owner: guest_user_info(status.owner), - node_info: guest_node_info(status.node_info), - blksize: status.blksize, - } -} - -fn guest_directory_entries(entries: Vec) -> Vec { - entries - .into_iter() - .map(|entry| DirEntry { - name: entry.name, - file_type: guest_file_type(entry.file_type), - ino_info: entry.ino_info.map(guest_node_info), - }) - .collect() -} - -fn guest_path_error(error: broker_fs::errors::PathError) -> PathError { - match error { - broker_fs::errors::PathError::NoSuchFileOrDirectory => PathError::NoSuchFileOrDirectory, - broker_fs::errors::PathError::NoSearchPerms { - #[cfg(debug_assertions)] - dir, - #[cfg(debug_assertions)] - perms, - } => PathError::NoSearchPerms { - #[cfg(debug_assertions)] - dir, - #[cfg(debug_assertions)] - perms: guest_mode(perms), - }, - broker_fs::errors::PathError::InvalidPathname => PathError::InvalidPathname, - broker_fs::errors::PathError::MissingComponent => PathError::MissingComponent, - broker_fs::errors::PathError::ComponentNotADirectory => PathError::ComponentNotADirectory, - } -} - -fn guest_open_error(error: broker_fs::errors::OpenError) -> OpenError { - match error { - broker_fs::errors::OpenError::AccessNotAllowed => OpenError::AccessNotAllowed, - broker_fs::errors::OpenError::NoWritePerms => OpenError::NoWritePerms, - broker_fs::errors::OpenError::ReadOnlyFileSystem => OpenError::ReadOnlyFileSystem, - broker_fs::errors::OpenError::AlreadyExists => OpenError::AlreadyExists, - broker_fs::errors::OpenError::TruncateError(error) => { - OpenError::TruncateError(guest_truncate_error(error)) - } - broker_fs::errors::OpenError::Io => OpenError::Io, - broker_fs::errors::OpenError::PathError(error) => { - OpenError::PathError(guest_path_error(error)) - } - } -} - -fn guest_read_error(error: broker_fs::errors::ReadError) -> ReadError { - match error { - broker_fs::errors::ReadError::ClosedFd => ReadError::ClosedFd, - broker_fs::errors::ReadError::NotAFile => ReadError::NotAFile, - broker_fs::errors::ReadError::NotForReading => ReadError::NotForReading, - broker_fs::errors::ReadError::Io => ReadError::Io, - } -} - -fn guest_write_error(error: broker_fs::errors::WriteError) -> WriteError { - match error { - broker_fs::errors::WriteError::ClosedFd => WriteError::ClosedFd, - broker_fs::errors::WriteError::NotAFile => WriteError::NotAFile, - broker_fs::errors::WriteError::NotForWriting => WriteError::NotForWriting, - broker_fs::errors::WriteError::Io => WriteError::Io, - } -} - -fn guest_seek_error(error: broker_fs::errors::SeekError) -> SeekError { - match error { - broker_fs::errors::SeekError::ClosedFd => SeekError::ClosedFd, - broker_fs::errors::SeekError::NotAFile => SeekError::NotAFile, - broker_fs::errors::SeekError::InvalidOffset => SeekError::InvalidOffset, - broker_fs::errors::SeekError::NonSeekable => SeekError::NonSeekable, - broker_fs::errors::SeekError::Io => SeekError::Io, - } -} - -fn guest_truncate_error(error: broker_fs::errors::TruncateError) -> TruncateError { - match error { - broker_fs::errors::TruncateError::ClosedFd => TruncateError::ClosedFd, - broker_fs::errors::TruncateError::IsDirectory => TruncateError::IsDirectory, - broker_fs::errors::TruncateError::NotForWriting => TruncateError::NotForWriting, - broker_fs::errors::TruncateError::IsTerminalDevice => TruncateError::IsTerminalDevice, - broker_fs::errors::TruncateError::Io => TruncateError::Io, - } -} - -fn guest_chmod_error(error: broker_fs::errors::ChmodError) -> ChmodError { - match error { - broker_fs::errors::ChmodError::NotTheOwner => ChmodError::NotTheOwner, - broker_fs::errors::ChmodError::ReadOnlyFileSystem => ChmodError::ReadOnlyFileSystem, - broker_fs::errors::ChmodError::Io => ChmodError::Io, - broker_fs::errors::ChmodError::PathError(error) => { - ChmodError::PathError(guest_path_error(error)) - } - } -} - -fn guest_chown_error(error: broker_fs::errors::ChownError) -> ChownError { - match error { - broker_fs::errors::ChownError::NotTheOwner => ChownError::NotTheOwner, - broker_fs::errors::ChownError::ReadOnlyFileSystem => ChownError::ReadOnlyFileSystem, - broker_fs::errors::ChownError::Io => ChownError::Io, - broker_fs::errors::ChownError::PathError(error) => { - ChownError::PathError(guest_path_error(error)) - } - } -} - -fn guest_unlink_error(error: broker_fs::errors::UnlinkError) -> UnlinkError { - match error { - broker_fs::errors::UnlinkError::NoWritePerms => UnlinkError::NoWritePerms, - broker_fs::errors::UnlinkError::IsADirectory => UnlinkError::IsADirectory, - broker_fs::errors::UnlinkError::ReadOnlyFileSystem => UnlinkError::ReadOnlyFileSystem, - broker_fs::errors::UnlinkError::Io => UnlinkError::Io, - broker_fs::errors::UnlinkError::PathError(error) => { - UnlinkError::PathError(guest_path_error(error)) - } - } -} - -fn guest_mkdir_error(error: broker_fs::errors::MkdirError) -> MkdirError { - match error { - broker_fs::errors::MkdirError::NoWritePerms => MkdirError::NoWritePerms, - broker_fs::errors::MkdirError::AlreadyExists => MkdirError::AlreadyExists, - broker_fs::errors::MkdirError::ReadOnlyFileSystem => MkdirError::ReadOnlyFileSystem, - broker_fs::errors::MkdirError::Io => MkdirError::Io, - broker_fs::errors::MkdirError::PathError(error) => { - MkdirError::PathError(guest_path_error(error)) - } - } -} - -fn guest_rmdir_error(error: broker_fs::errors::RmdirError) -> RmdirError { - match error { - broker_fs::errors::RmdirError::NoWritePerms => RmdirError::NoWritePerms, - broker_fs::errors::RmdirError::Busy => RmdirError::Busy, - broker_fs::errors::RmdirError::NotEmpty => RmdirError::NotEmpty, - broker_fs::errors::RmdirError::NotADirectory => RmdirError::NotADirectory, - broker_fs::errors::RmdirError::ReadOnlyFileSystem => RmdirError::ReadOnlyFileSystem, - broker_fs::errors::RmdirError::Io => RmdirError::Io, - broker_fs::errors::RmdirError::PathError(error) => { - RmdirError::PathError(guest_path_error(error)) - } - } -} - -fn guest_read_dir_error(error: broker_fs::errors::ReadDirError) -> ReadDirError { - match error { - broker_fs::errors::ReadDirError::ClosedFd => ReadDirError::ClosedFd, - broker_fs::errors::ReadDirError::NotADirectory => ReadDirError::NotADirectory, - broker_fs::errors::ReadDirError::Io => ReadDirError::Io, - } -} - -fn guest_file_status_error(error: broker_fs::errors::FileStatusError) -> FileStatusError { - match error { - broker_fs::errors::FileStatusError::ClosedFd => FileStatusError::ClosedFd, - broker_fs::errors::FileStatusError::Io => FileStatusError::Io, - broker_fs::errors::FileStatusError::PathError(error) => { - FileStatusError::PathError(guest_path_error(error)) - } - } -} - -crate::fd::enable_fds_for_subsystem! { - @ Platform: { sync::RawSyncPrimitivesProvider }, Backend: { broker_fs::backend::Backend + 'static }; - Resolver; - @ Backend: { broker_fs::backend::Backend + 'static }; - ResolverEntry; - -> ResolverFd; -} diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs deleted file mode 100644 index 542f634bc..000000000 --- a/litebox/src/fs/tests.rs +++ /dev/null @@ -1,2293 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -fn tar_ro_fs( - litebox: &crate::LiteBox, - tar_data: alloc::borrow::Cow<'static, [u8]>, -) -> crate::fs::resolver::Resolver { - crate::fs::resolver::Resolver::new( - litebox, - crate::fs::tar_ro::TarRo::new( - tar_data, - crate::fs::inode_allocator::InodeAllocator::standalone(), - ), - ) -} - -type InMemFs = crate::fs::resolver::Resolver< - crate::platform::mock::MockPlatform, - crate::fs::in_mem::InMem, ->; - -fn in_mem_fs(litebox: &crate::LiteBox) -> InMemFs { - crate::fs::resolver::Resolver::new( - litebox, - crate::fs::in_mem::InMem::new(crate::fs::inode_allocator::InodeAllocator::standalone()), - ) -} - -/// Run `f` with the acting user set to root. -fn with_root_privileges< - Platform: crate::sync::RawSyncPrimitivesProvider, - B: crate::fs::backend::Backend, ->( - fs: &mut crate::fs::resolver::Resolver, - context: &crate::fs::resolver::Context, - f: impl FnOnce(&mut crate::fs::resolver::Resolver, &crate::fs::resolver::Context), -) { - let root = crate::fs::UserInfo::ROOT; - with_user(fs, context, root.user, root.group, f); -} - -/// Run `f` with the acting user set to `user`/`group`, so that tests can exercise operations -/// whose outcome depends on the acting user. -fn with_user( - fs: &mut crate::fs::resolver::Resolver, - context: &crate::fs::resolver::Context, - user: u16, - group: u16, - f: impl FnOnce(&mut crate::fs::resolver::Resolver, &crate::fs::resolver::Context), -) { - let mut context = context.clone(); - context.set_acting_user(crate::fs::UserInfo { user, group }); - f(fs, &context); -} - -type OverlayFs = crate::fs::resolver::Resolver< - crate::platform::mock::MockPlatform, - crate::fs::overlay::Overlay, ->; - -/// An overlay of `upper` over a tar-backed lower layer. -fn overlay_fs( - litebox: &crate::LiteBox, - upper: crate::fs::in_mem::InMem, - tar_data: alloc::borrow::Cow<'static, [u8]>, -) -> OverlayFs { - crate::fs::resolver::Resolver::new( - litebox, - crate::fs::overlay::Overlay::new( - upper, - crate::fs::tar_ro::TarRo::new( - tar_data, - crate::fs::inode_allocator::InodeAllocator::standalone(), - ), - crate::fs::inode_allocator::InodeAllocator::standalone(), - ), - ) -} - -mod in_mem { - use crate::LiteBox; - use crate::fs::{Mode, OFlags}; - use crate::platform::mock::MockPlatform; - use alloc::vec; - use alloc::vec::Vec; - extern crate std; - - use super::{with_root_privileges, with_user}; - - #[test] - fn root_file_creation_and_deletion() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - - with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { - // Test file creation - let path = "/testfile"; - let fd = fs - .open(ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - - fs.close(&fd).expect("Failed to close file"); - - // Test file deletion - fs.unlink(ctx, path).expect("Failed to unlink file"); - assert!( - fs.open(ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), - "File should not exist" - ); - }); - } - - #[test] - fn root_file_read_write() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - - with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { - // Create and write to a file - let path = "/testfile"; - let fd = fs - .open(ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - let data = b"Hello, world!"; - fs.write(&fd, data, None).expect("Failed to write to file"); - fs.close(&fd).expect("Failed to close file"); - - // Read from the file - let fd = fs - .open(ctx, path, OFlags::RDONLY, Mode::RWXU) - .expect("Failed to open file"); - let mut buffer = vec![0; data.len()]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, data.len()); - assert_eq!(&buffer, data); - fs.close(&fd).expect("Failed to close file"); - }); - } - - #[test] - fn write_only_open_does_not_require_read_permission() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); - - let path = "/tmp/write_only"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) - .expect("Failed to create write-only file"); - fs.write(&fd, b"x", None).expect("Failed to write file"); - - let mut buffer = [0]; - assert!(matches!( - fs.read(&fd, &mut buffer, None), - Err(crate::fs::errors::ReadError::NotForReading) - )); - fs.close(&fd).expect("Failed to close file"); - - assert!(matches!( - fs.open(&ctx, path, OFlags::RDONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::AccessNotAllowed) - )); - } - - #[test] - fn newly_created_file_does_not_require_its_own_permissions() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); - - let path = "/tmp/zero_mode"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) - .expect("Failed to create zero-mode file"); - fs.write(&fd, b"x", None).expect("Failed to write file"); - fs.close(&fd).expect("Failed to close file"); - - let status = fs.file_status(&ctx, path).expect("Failed to stat file"); - assert_eq!(status.mode, Mode::empty()); - assert!(matches!( - fs.open(&ctx, path, OFlags::WRONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::AccessNotAllowed) - )); - } - - #[test] - fn root_directory_creation_and_removal() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - - with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { - // Test directory creation - let path = "/testdir"; - fs.mkdir(ctx, path, Mode::RWXU) - .expect("Failed to create directory"); - - // Test directory removal - fs.rmdir(ctx, path).expect("Failed to remove directory"); - assert!( - fs.open(ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), - "Directory should not exist" - ); - }); - } - - #[test] - fn file_creation_and_deletion() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); - - // Test file creation - let path = "/tmp/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - - fs.close(&fd).expect("Failed to close file"); - - // Test file deletion - fs.unlink(&ctx, path).expect("Failed to unlink file"); - assert!( - fs.open(&ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), - "File should not exist" - ); - } - - #[test] - fn file_read_write() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); - - // Create and write to a file - let path = "/tmp/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - let data = b"Hello, world!"; - fs.write(&fd, data, None).expect("Failed to write to file"); - fs.write(&fd, &data[2..], Some(2)) - .expect("Failed to write to file with offset"); - fs.close(&fd).expect("Failed to close file"); - - // Read from the file - let fd = fs - .open(&ctx, path, OFlags::RDONLY, Mode::RWXU) - .expect("Failed to open file"); - let mut buffer = vec![0; data.len()]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - let bytes_read2 = fs - .read(&fd, &mut buffer[2..], Some(2)) - .expect("Failed to read from file with offset"); - assert_eq!(bytes_read, data.len()); - assert_eq!(bytes_read2, data.len() - 2); - assert_eq!(&buffer, data); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn directory_creation_and_removal() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - // Make `/tmp` and set up with reasonable privs so normal users can do things in there. - fs.mkdir(ctx, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /tmp"); - }); - - // Test directory creation - let path = "/tmp/testdir"; - fs.mkdir(&ctx, path, Mode::RWXU) - .expect("Failed to create directory"); - - // Test directory removal - fs.rmdir(&ctx, path).expect("Failed to remove directory"); - assert!( - fs.open(&ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), - "Directory should not exist" - ); - } - - #[test] - fn read_dir_empty() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - - with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { - let fd = fs - .open(ctx, "/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open root directory"); - let entries = fs - .read_dir(&fd) - .expect("Failed to read directory") - .iter() - .map(|e| e.name.clone()) - .collect::>(); - assert_eq!( - entries, - vec![".", ".."], - "Root directory should contain . and .." - ); - fs.close(&fd).expect("Failed to close directory"); - }); - } - - #[test] - fn read_dir_with_files_and_dirs() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - - with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { - // Create a directory structure - fs.mkdir(ctx, "/testdir", Mode::RWXU) - .expect("Failed to create directory"); - let fd1 = fs - .open( - ctx, - "/testfile1", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file1"); - fs.close(&fd1).expect("Failed to close file1"); - let fd2 = fs - .open( - ctx, - "/testfile2", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file2"); - fs.close(&fd2).expect("Failed to close file2"); - - // Read root directory - let fd = fs - .open(ctx, "/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open root directory"); - let entries = fs.read_dir(&fd).expect("Failed to read directory"); - fs.close(&fd).expect("Failed to close directory"); - - // Should have 5 entries: ., .., testdir, testfile1, testfile2 - assert_eq!(entries.len(), 5); - - let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); - names.sort_unstable(); - assert_eq!(names, vec![".", "..", "testdir", "testfile1", "testfile2"]); - - // Check file types - for entry in &entries { - match entry.name.as_str() { - "testdir" | "." | ".." => { - assert_eq!(entry.file_type, crate::fs::FileType::Directory); - } - "testfile1" | "testfile2" => { - assert_eq!(entry.file_type, crate::fs::FileType::RegularFile); - } - _ => panic!("Unexpected entry: {}", entry.name), - } - if entry.name != "." && entry.name != ".." { - assert!(entry.ino_info.is_some(), "Inode info should be present"); - } else { - // TODO(jayb): Re-enable this assertion once the resolver fills in - // inode information for the synthesized `.` and `..` entries. - } - } - - // Read the subdirectory (should be empty) - let fd = fs - .open(ctx, "/testdir", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open subdirectory"); - let entries = fs - .read_dir(&fd) - .expect("Failed to read subdirectory") - .iter() - .map(|e| e.name.clone()) - .collect::>(); - assert!(entries.len() == 2, "Subdirectory should contain . and .."); - fs.close(&fd).expect("Failed to close subdirectory"); - }); - } - - #[test] - fn read_dir_file_not_directory() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - - with_root_privileges(&mut super::in_mem_fs(&litebox), &ctx, |fs, ctx| { - // Create a file - let fd = fs - .open(ctx, "/testfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.close(&fd).expect("Failed to close file"); - - // Try to read_dir on the file (should fail) - let fd = fs - .open(ctx, "/testfile", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file"); - let result = fs.read_dir(&fd); - fs.close(&fd).expect("Failed to close file"); - - assert!(matches!( - result, - Err(crate::fs::errors::ReadDirError::NotADirectory) - )); - }); - } - - #[test] - fn parent_dir_write_permissions_are_enforced() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - // A root-owned 0755 directory, holding a file and a directory to try to remove. - fs.mkdir( - ctx, - "/rootdir", - Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, - ) - .expect("Failed to create directory"); - let fd = fs - .open( - ctx, - "/rootdir/file", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file"); - fs.close(&fd).expect("Failed to close file"); - fs.mkdir(ctx, "/rootdir/sub", Mode::RWXU) - .expect("Failed to create subdirectory"); - - // A world-writable directory, for the positive case. - fs.mkdir(ctx, "/opendir", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create directory"); - }); - - with_user(&mut fs, &ctx, 1000, 1000, |fs, ctx| { - assert!(matches!( - fs.open( - ctx, - "/rootdir/new", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU - ), - Err(crate::fs::errors::OpenError::NoWritePerms) - )); - assert!(matches!( - fs.mkdir(ctx, "/rootdir/newdir", Mode::RWXU), - Err(crate::fs::errors::MkdirError::NoWritePerms) - )); - assert!(matches!( - fs.unlink(ctx, "/rootdir/file"), - Err(crate::fs::errors::UnlinkError::NoWritePerms) - )); - assert!(matches!( - fs.rmdir(ctx, "/rootdir/sub"), - Err(crate::fs::errors::RmdirError::NoWritePerms) - )); - - // The same operations succeed in a directory the user may write. - let fd = fs - .open( - ctx, - "/opendir/new", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file"); - fs.close(&fd).expect("Failed to close file"); - fs.mkdir(ctx, "/opendir/newdir", Mode::RWXU) - .expect("Failed to create directory"); - fs.unlink(ctx, "/opendir/new") - .expect("Failed to unlink file"); - fs.rmdir(ctx, "/opendir/newdir") - .expect("Failed to remove directory"); - }); - } - - #[test] - fn chown_test() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - // Create a test file as root - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - let path = "/testfile"; - let fd = fs - .open(ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.close(&fd).expect("Failed to close file"); - - // First chown to 1000:1000 as root (should succeed) - fs.chown(ctx, path, Some(1000), Some(1000)) - .expect("Failed to chown as root"); - }); - - // Switch to user 1000 and test that owner can chown (should succeed) - let path = "/testfile"; - with_user(&mut fs, &ctx, 1000, 1000, |fs, ctx| { - fs.chown(ctx, path, Some(123), Some(456)) - .expect("Failed to chown as owner"); - }); - - // Switch to a different user and test that non-owner cannot chown (should fail) - with_user(&mut fs, &ctx, 500, 500, |fs, ctx| { - match fs.chown(ctx, path, Some(789), Some(101)) { - Err(crate::fs::errors::ChownError::NotTheOwner) => { - // Expected behavior - } - Ok(()) => panic!("Non-owner should not be able to chown"), - Err(e) => panic!("Unexpected error: {e:?}"), - } - }); - - // Test chown on non-existent file (should fail) - match fs.chown(&ctx, "/nonexistent", Some(123), Some(456)) { - Err(crate::fs::errors::ChownError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory, - )) => { - // Expected behavior - } - Ok(()) => panic!("Should not be able to chown non-existent file"), - Err(e) => panic!("Unexpected error: {e:?}"), - } - - // Test partial chown (change only user, leave group unchanged) - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chown(ctx, path, Some(999), None) - .expect("Failed to chown user only"); - }); - - // Test partial chown (change only group, leave user unchanged) - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chown(ctx, path, None, Some(888)) - .expect("Failed to chown group only"); - }); - } - - #[test] - fn o_directory_flag_tests() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - // Create test directory and file - fs.mkdir(&ctx, "/testdir", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create directory"); - - let fd = fs - .open( - &ctx, - "/testfile", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file"); - fs.close(&fd).expect("Failed to close file"); - - // Test O_DIRECTORY on a directory (should succeed) - let fd = fs - .open( - &ctx, - "/testdir", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("Failed to open directory with O_DIRECTORY"); - fs.close(&fd).expect("Failed to close directory"); - - // Test O_DIRECTORY on a regular file (should fail) - assert!(matches!( - fs.open( - &ctx, - "/testfile", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) - )); - - // Test O_DIRECTORY on non-existent path (should fail) - assert!(matches!( - fs.open( - &ctx, - "/nonexistent", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )) - )); - - // Test O_DIRECTORY with O_CREAT on non-existent path - // According to the implementation, O_DIRECTORY should be ignored when O_CREAT is specified - let fd = fs - .open( - &ctx, - "/newfile", - OFlags::CREAT | OFlags::WRONLY | OFlags::DIRECTORY, - Mode::RWXU, - ) - .expect("Failed to create file with O_CREAT | O_DIRECTORY"); - fs.close(&fd).expect("Failed to close file"); - - // Verify it created a regular file, not a directory - let stat = fs - .file_status(&ctx, "/newfile") - .expect("Failed to get file status"); - assert_eq!(stat.file_type, crate::fs::FileType::RegularFile); - - // TODO(jayb): Restore coverage of `O_RDWR | O_DIRECTORY` once `OpenError` can report - // `EISDIR`; see the matching TODO in `InMem::owned_dir_at`. The legacy in-memory file - // system used to accept such an open, which Linux rejects. - } - - #[test] - fn o_excl_flag_tests() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - // Test O_CREAT | O_EXCL on non-existent file (should succeed) - let fd = fs - .open( - &ctx, - "/newfile", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create new file with O_CREAT | O_EXCL"); - - // Write some data to verify file was created - fs.write(&fd, b"test data", None) - .expect("Failed to write to new file"); - fs.close(&fd).expect("Failed to close new file"); - - // Test O_CREAT | O_EXCL on existing file (should fail) - assert!(matches!( - fs.open( - &ctx, - "/newfile", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ), - Err(crate::fs::errors::OpenError::AlreadyExists) - )); - - // Test O_EXCL without O_CREAT (should be ignored and succeed) - let fd = fs - .open( - &ctx, - "/newfile", - OFlags::EXCL | OFlags::RDONLY, - Mode::empty(), - ) - .expect("Failed to open existing file with O_EXCL (without O_CREAT)"); - - // Verify we can read the data - let mut buffer = vec![0; 9]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"test data"); - fs.close(&fd).expect("Failed to close file"); - - // Test O_CREAT without O_EXCL on existing file (should succeed) - let fd = fs - .open(&ctx, "/newfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to open existing file with O_CREAT (without O_EXCL)"); - fs.close(&fd).expect("Failed to close file"); - - // Test O_CREAT | O_EXCL on directory (should fail) - fs.mkdir(&ctx, "/testdir", Mode::RWXU) - .expect("Failed to create directory"); - assert!(matches!( - fs.open( - &ctx, - "/testdir", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ), - Err(crate::fs::errors::OpenError::AlreadyExists) - )); - } - - #[test] - fn open_with_trunc() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - // Create a file and write some initial content - let path = "/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - let initial_data = b"Hello, world! This is initial content."; - fs.write(&fd, initial_data, None) - .expect("Failed to write initial content"); - fs.close(&fd).expect("Failed to close file"); - - // Verify initial content was written - let fd = fs - .open(&ctx, path, OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file for reading"); - let mut buffer = vec![0; initial_data.len()]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read initial content"); - assert_eq!(bytes_read, initial_data.len()); - assert_eq!(&buffer, initial_data); - fs.close(&fd).expect("Failed to close file"); - - // Test O_TRUNC with O_WRONLY - should truncate file - let fd = fs - .open(&ctx, path, OFlags::WRONLY | OFlags::TRUNC, Mode::empty()) - .expect("Failed to open file with O_TRUNC | O_WRONLY"); - - // Write new content to the truncated file - let new_data = b"New content"; - fs.write(&fd, new_data, None) - .expect("Failed to write new content"); - fs.close(&fd).expect("Failed to close file"); - - // Verify the file was truncated and contains only new content - let fd = fs - .open(&ctx, path, OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file for verification"); - let mut buffer = vec![0; initial_data.len()]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read after truncation"); - assert_eq!(bytes_read, new_data.len()); - assert_eq!(&buffer[..bytes_read], new_data); - fs.close(&fd).expect("Failed to close file"); - - // Test O_TRUNC with O_RDWR - should also truncate - fs.write( - &fs.open(&ctx, path, OFlags::WRONLY, Mode::empty()).unwrap(), - b"More content to truncate", - None, - ) - .unwrap(); - fs.close(&fs.open(&ctx, path, OFlags::WRONLY, Mode::empty()).unwrap()) - .unwrap(); - - let fd = fs - .open(&ctx, path, OFlags::RDWR | OFlags::TRUNC, Mode::empty()) - .expect("Failed to open file with O_TRUNC | O_RDWR"); - - // File should be empty after truncation - let mut buffer = vec![0; 100]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from truncated file"); - assert_eq!(bytes_read, 0); - - // Write and read back to verify it works - let test_data = b"After RDWR truncation"; - fs.write(&fd, test_data, None) - .expect("Failed to write after RDWR truncation"); - - fs.seek(&fd, 0, crate::fs::SeekWhence::RelativeToBeginning) - .expect("Failed to seek to beginning"); - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read after write"); - assert_eq!(bytes_read, test_data.len()); - assert_eq!(&buffer[..bytes_read], test_data); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn write_position_after_seek() { - use crate::fs::SeekWhence; - - let ctx = crate::fs::resolver::Context::new(); - - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - // Allow regular user to create in root for this focused test - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("chmod / failed"); - }); - - let fd = fs - .open( - &ctx, - "/posfile", - OFlags::CREAT | OFlags::RDWR, - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("open failed"); - - // 1. First positional write; position should advance by 6. - fs.write(&fd, b"abcdef", None).expect("first write failed"); - - // 2. Rewind to beginning. - fs.seek(&fd, 0, SeekWhence::RelativeToBeginning) - .expect("seek failed"); - - // 3. Another positional write should write from start - fs.write(&fd, b"X", None).expect("overwrite failed"); - - // The file offset should now be at 2. - assert_eq!( - fs.seek(&fd, 0, SeekWhence::RelativeToCurrentOffset) - .expect("seek failed"), - 1 - ); - - // Read back whole file to verify content and length. - fs.seek(&fd, 0, SeekWhence::RelativeToBeginning) - .expect("seek failed"); - let mut buf = [0u8; 16]; - let n = fs.read(&fd, &mut buf, None).expect("read failed"); - assert_eq!(n, 6, "file length should be 6 after writes"); - assert_eq!(&buf[..n], b"Xbcdef", "file content mismatch"); - - // Extra: another append to verify continued correct advancement. - fs.write(&fd, b"12", None).expect("second append failed"); - fs.seek(&fd, 0, SeekWhence::RelativeToBeginning) - .expect("seek 2 failed"); - let mut buf2 = [0u8; 16]; - let n2 = fs.read(&fd, &mut buf2, None).expect("read 2 failed"); - assert_eq!(n2, 8); - assert_eq!(&buf2[..n2], b"Xbcdef12"); - - fs.close(&fd).expect("close failed"); - } - - #[test] - fn o_append_flag_basic() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - // Create a file and write some initial content - let path = "/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - let initial_data = b"Hello"; - fs.write(&fd, initial_data, None) - .expect("Failed to write initial content"); - fs.close(&fd).expect("Failed to close file"); - - // Re-open with O_APPEND and write more data - let fd = fs - .open(&ctx, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) - .expect("Failed to open file with O_APPEND"); - let append_data = b" World"; - fs.write(&fd, append_data, None) - .expect("Failed to append data"); - fs.close(&fd).expect("Failed to close file"); - - // Verify the file contains both pieces of data concatenated - let fd = fs - .open(&ctx, path, OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file for reading"); - let mut buffer = vec![0; 11]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 11); - assert_eq!(&buffer[..bytes_read], b"Hello World"); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn o_append_flag_seek_ignored_for_write() { - use crate::fs::SeekWhence; - - let ctx = crate::fs::resolver::Context::new(); - - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - // Create a file and write some initial content - let path = "/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.write(&fd, b"ABCDEF", None) - .expect("Failed to write initial content"); - fs.close(&fd).expect("Failed to close file"); - - // Re-open with O_APPEND - let fd = fs - .open(&ctx, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) - .expect("Failed to open file with O_APPEND"); - - // Seek to beginning - this should succeed but writes should still append - fs.seek(&fd, 0, SeekWhence::RelativeToBeginning) - .expect("Failed to seek to beginning"); - - // Write some data - it should go to the end despite the seek - fs.write(&fd, b"123", None) - .expect("Failed to write after seek"); - fs.close(&fd).expect("Failed to close file"); - - // Verify the file content: original data followed by appended data - let fd = fs - .open(&ctx, path, OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file for reading"); - let mut buffer = vec![0; 20]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 9); - assert_eq!(&buffer[..bytes_read], b"ABCDEF123"); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn o_append_flag_with_rdwr() { - use crate::fs::SeekWhence; - - let ctx = crate::fs::resolver::Context::new(); - - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - // Create a file with initial content - let path = "/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.write(&fd, b"Hello", None) - .expect("Failed to write initial content"); - fs.close(&fd).expect("Failed to close file"); - - // Re-open with O_RDWR | O_APPEND - let fd = fs - .open(&ctx, path, OFlags::RDWR | OFlags::APPEND, Mode::empty()) - .expect("Failed to open file with O_RDWR | O_APPEND"); - - // Read should work normally from the beginning - let mut buffer = vec![0; 10]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 5); - assert_eq!(&buffer[..bytes_read], b"Hello"); - - // Seek to beginning - write should still append despite position being at 0 - fs.seek(&fd, 0, SeekWhence::RelativeToBeginning) - .expect("Seek failed"); - - // Write should append to end, ignoring the current position - fs.write(&fd, b" World", None) - .expect("Failed to write with append"); - - // Seek to beginning and read the whole file - fs.seek(&fd, 0, SeekWhence::RelativeToBeginning) - .expect("Seek failed"); - let mut buffer = vec![0; 20]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 11); - assert_eq!(&buffer[..bytes_read], b"Hello World"); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn o_append_pwrite_ignores_append_mode() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - // Create a file with initial content - let path = "/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.write(&fd, b"ABCDEF", None) - .expect("Failed to write initial content"); - fs.close(&fd).expect("Failed to close file"); - - // Re-open with O_APPEND - let fd = fs - .open(&ctx, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) - .expect("Failed to open file with O_APPEND"); - - // pwrite (write with explicit offset) should ignore O_APPEND per POSIX - fs.write(&fd, b"XX", Some(2)).expect("Failed to pwrite"); - fs.close(&fd).expect("Failed to close file"); - - // Verify the file content: XX should be at position 2, not appended - let fd = fs - .open(&ctx, path, OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file for reading"); - let mut buffer = vec![0; 10]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 6); - assert_eq!(&buffer[..bytes_read], b"ABXXEF"); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn o_append_with_trunc() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let mut fs = super::in_mem_fs(&litebox); - - with_root_privileges(&mut fs, &ctx, |fs, ctx| { - fs.chmod(ctx, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to chmod /"); - }); - - // Create a file with initial content - let path = "/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - fs.write(&fd, b"Original content", None) - .expect("Failed to write initial content"); - fs.close(&fd).expect("Failed to close file"); - - // Re-open with O_TRUNC | O_APPEND - let fd = fs - .open( - &ctx, - path, - OFlags::WRONLY | OFlags::TRUNC | OFlags::APPEND, - Mode::empty(), - ) - .expect("Failed to open file with O_TRUNC | O_APPEND"); - - // File should be truncated, then write should append (to empty file) - fs.write(&fd, b"New", None) - .expect("Failed to write after truncation"); - fs.write(&fd, b"Content", None) - .expect("Failed to write second chunk"); - fs.close(&fd).expect("Failed to close file"); - - // Verify the file content - let fd = fs - .open(&ctx, path, OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file for reading"); - let mut buffer = vec![0; 20]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(bytes_read, 10); - assert_eq!(&buffer[..bytes_read], b"NewContent"); - fs.close(&fd).expect("Failed to close file"); - } -} - -mod tar_ro { - use crate::LiteBox; - use crate::fs::{Mode, OFlags}; - use crate::platform::mock::MockPlatform; - use alloc::vec; - use alloc::vec::Vec; - extern crate std; - - const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); - - #[test] - fn file_read() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - let fd = fs - .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) - .expect("Failed to open file"); - let mut buffer = vec![0; 1024]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"testfoo\n"); - fs.close(&fd).expect("Failed to close file"); - let fd = fs - .open(&ctx, "bar/baz", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file"); - let mut buffer = vec![0; 1024]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"test bar baz\n"); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn dir_and_nonexist_checks() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - assert!(matches!( - fs.open(&ctx, "bar/ba", OFlags::RDONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )), - )); - let fd = fs - .open(&ctx, "bar", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open dir"); - fs.close(&fd).expect("Failed to close dir"); - } - - #[test] - fn o_directory_flag_tests() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - - // Test O_DIRECTORY on a directory (should succeed) - let fd = fs - .open( - &ctx, - "bar", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("Failed to open directory with O_DIRECTORY"); - fs.close(&fd).expect("Failed to close directory"); - - // Test O_DIRECTORY on a regular file (should fail) - assert!(matches!( - fs.open( - &ctx, - "foo", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) - )); - - // Test O_DIRECTORY on non-existent path (should fail) - assert!(matches!( - fs.open( - &ctx, - "nonexistent", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )) - )); - - // Test O_DIRECTORY on nested file (should fail) - assert!(matches!( - fs.open( - &ctx, - "bar/baz", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) - )); - } - - #[test] - fn write_or_truncate_open_of_directory_fails() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - - for flags in [OFlags::WRONLY, OFlags::RDWR, OFlags::TRUNC] { - assert!(matches!( - fs.open(&ctx, "bar", flags, Mode::empty()), - Err(crate::fs::errors::OpenError::ReadOnlyFileSystem) - )); - } - } - - #[test] - fn read_dir_subdirectory() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - - // Read root directory - let fd = fs - .open(&ctx, "/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open root directory"); - let entries = fs.read_dir(&fd).expect("Failed to read root directory"); - fs.close(&fd).expect("Failed to close root directory"); - - // Should have 4 entries: ., .., bar, foo - assert_eq!(entries.len(), 4); - - let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); - names.sort_unstable(); - assert_eq!(names, vec![".", "..", "bar", "foo"]); - - // Check file types - for entry in &entries { - match entry.name.as_str() { - "foo" => { - assert_eq!(entry.file_type, crate::fs::FileType::RegularFile); - } - "bar" | "." | ".." => assert_eq!(entry.file_type, crate::fs::FileType::Directory), - _ => panic!("Unexpected entry: {}", entry.name), - } - if entry.name != "." && entry.name != ".." { - assert!(entry.ino_info.is_some(), "Inode info should be present"); - } else { - // TODO(jayb): Re-enable this assertion once Composer handles `.` and `..` inode - // information better. - } - } - - // Read `bar` directory - let fd = fs - .open(&ctx, "bar", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open bar directory"); - let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); - fs.close(&fd).expect("Failed to close bar directory"); - - // Should have 3 entry: ., .., baz (file) - assert_eq!(entries.len(), 3); - assert_eq!(entries[2].name, "baz"); - assert_eq!(entries[2].file_type, crate::fs::FileType::RegularFile); - } - - #[test] - fn read_dir_file_not_directory() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = super::tar_ro_fs(&litebox, TEST_TAR_FILE.into()); - - let fd = fs - .open(&ctx, "foo", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open foo file"); - let result = fs.read_dir(&fd); - fs.close(&fd).expect("Failed to close foo file"); - - assert!(matches!( - result, - Err(crate::fs::errors::ReadDirError::NotADirectory) - )); - } -} - -mod overlay { - use crate::LiteBox; - use crate::fs::in_mem::{InMem, InitialNode}; - use crate::fs::{FileType, Mode, OFlags, UserInfo}; - use crate::platform::mock::MockPlatform; - use alloc::vec; - use alloc::vec::Vec; - extern crate std; - - const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); - - /// The user these tests act as, and so the owner of anything they are set up as having created. - const ACTING_USER: UserInfo = UserInfo { - user: 1000, - group: 1000, - }; - const ALL_PERMS: Mode = Mode::RWXU.union(Mode::RWXG).union(Mode::RWXO); - - /// An upper backend whose root is writable by the acting user, holding `entries`. - /// - /// The overlay directs every mutation to the upper backend, so its root has to allow writes for - /// anything to be created. - fn upper( - entries: impl IntoIterator, - ) -> InMem { - InMem::new_initialized( - [( - "/", - InitialNode::Directory { - mode: ALL_PERMS, - owner: UserInfo::ROOT, - }, - )] - .into_iter() - .chain(entries), - ) - } - - fn overlay_fs(litebox: &LiteBox, upper: InMem) -> super::OverlayFs { - super::overlay_fs(litebox, upper, TEST_TAR_FILE.into()) - } - - #[test] - fn file_read_from_lower() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - let fd = fs - .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) - .expect("Failed to open file"); - let mut buffer = vec![0; 1024]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"testfoo\n"); - let stat = fs.fd_file_status(&fd).expect("Failed to fd file stat"); - assert_eq!(stat.file_type, FileType::RegularFile); - assert_eq!(stat.mode, Mode::from_bits(0o644).unwrap()); - fs.close(&fd).expect("Failed to close file"); - - let stat = fs.file_status(&ctx, "bar").expect("Failed to file stat"); - assert_eq!(stat.file_type, FileType::Directory); - assert_eq!(stat.mode, Mode::from_bits(0o777).unwrap()); - - let fd = fs - .open(&ctx, "bar/baz", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open file"); - let mut buffer = vec![0; 1024]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"test bar baz\n"); - let stat = fs.fd_file_status(&fd).expect("Failed to fd file stat"); - assert_eq!(stat.file_type, FileType::RegularFile); - assert_eq!(stat.mode, Mode::from_bits(0o644).unwrap()); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn dir_and_nonexist_checks() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - assert!(matches!( - fs.open(&ctx, "bar/ba", OFlags::RDONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )), - )); - let fd = fs - .open(&ctx, "bar", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open dir"); - fs.close(&fd).expect("Failed to close dir"); - } - - /// Check that for the same file, even though it started as a lower file, writing to it copies - /// it up and redirects handles already open on it, so every descriptor sees the update. - #[test] - fn file_read_write_copy_up() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - let fd1 = fs - .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) - .expect("Failed to open file"); - let fd2 = fs - .open(&ctx, "foo", OFlags::WRONLY, Mode::RWXU) - .expect("Failed to open file"); - - let mut buffer = vec![0; 1024]; - - let bytes_read = fs - .read(&fd1, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"testfoo\n"); - - fs.write(&fd2, b"share", None) - .expect("Failed to write to file"); - - fs.seek(&fd1, 0, crate::fs::SeekWhence::RelativeToBeginning) - .expect("Failed to seek to start"); - let bytes_read = fs - .read(&fd1, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"shareoo\n"); - - fs.close(&fd1).expect("Failed to close file"); - fs.close(&fd2).expect("Failed to close file"); - } - - /// Similar to [`file_read_write_copy_up`] but also confirm that file positions have been - /// maintained. - #[test] - fn file_read_write_copy_up_keeps_position() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - let fd1 = fs - .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) - .expect("Failed to open file"); - let fd2 = fs - .open(&ctx, "foo", OFlags::WRONLY, Mode::RWXU) - .expect("Failed to open file"); - - let mut buffer = vec![0; 4]; - - let bytes_read = fs - .read(&fd1, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"test"); - - fs.write(&fd2, b"share", None) - .expect("Failed to write to file"); - - let bytes_read = fs - .read(&fd1, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"eoo\n"); - - fs.close(&fd1).expect("Failed to close file"); - fs.close(&fd2).expect("Failed to close file"); - } - - #[test] - fn file_deletion() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - let fd = fs - .open(&ctx, "foo", OFlags::RDONLY, Mode::RWXU) - .expect("Failed to open file"); - - let mut buffer = vec![0; 4]; - - // The file exists, and is readable - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"test"); - - // Then we delete it - fs.unlink(&ctx, "foo").unwrap(); - - // This should not really impact the readability; file is fine. - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"foo\n"); - - // But if we close and attempt to re-open, it should not exist - fs.close(&fd).expect("Failed to close file"); - assert!(matches!( - fs.open(&ctx, "foo", OFlags::RDONLY, Mode::empty()), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )), - )); - } - - #[test] - fn o_directory_flag_tests() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs( - &litebox, - upper([ - ( - "/upperdir", - InitialNode::Directory { - mode: ALL_PERMS, - owner: ACTING_USER, - }, - ), - ( - "/upperfile", - InitialNode::File { - mode: Mode::RWXU, - owner: ACTING_USER, - data: alloc::borrow::Cow::Borrowed(b""), - }, - ), - ]), - ); - - // Test O_DIRECTORY on directory from lower layer (tar) - let fd = fs - .open( - &ctx, - "bar", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("Failed to open lower layer directory with O_DIRECTORY"); - fs.close(&fd).expect("Failed to close directory"); - - // Test O_DIRECTORY on directory from upper layer (in_mem) - let fd = fs - .open( - &ctx, - "/upperdir", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("Failed to open upper layer directory with O_DIRECTORY"); - fs.close(&fd).expect("Failed to close directory"); - - // Test O_DIRECTORY on file from lower layer (should fail) - assert!(matches!( - fs.open( - &ctx, - "foo", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) - )); - - // Test O_DIRECTORY on file from upper layer (should fail) - assert!(matches!( - fs.open( - &ctx, - "/upperfile", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) - )); - - // Test O_DIRECTORY on nested file from lower layer (should fail) - assert!(matches!( - fs.open( - &ctx, - "bar/baz", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::ComponentNotADirectory - )) - )); - - // Test O_DIRECTORY on non-existent path (should fail) - assert!(matches!( - fs.open( - &ctx, - "nonexistent", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty() - ), - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )) - )); - } - - #[test] - // Regression test for #250: a file that already exists in the lower layer should not be - // shadowed by an attempt to create a file. - fn file_create_exist_in_lower() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - let fd = fs - .open(&ctx, "foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) - .expect("Failed to open file"); - let mut buffer = vec![0; 4]; - - // The file exists, and is readable - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from file"); - assert_eq!(&buffer[..bytes_read], b"test"); - } - - #[test] - fn read_dir_from_lower_layer() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // Read bar subdirectory - let fd = fs - .open(&ctx, "bar", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open bar directory"); - let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); - fs.close(&fd).expect("Failed to close bar directory"); - - // Should have 3 entries: ., .., baz (file) - assert_eq!(entries.len(), 3); - assert_eq!(entries[2].name, "baz"); - assert_eq!(entries[2].file_type, crate::fs::FileType::RegularFile); - assert!( - entries[2].ino_info.is_some(), - "Inode info should be present" - ); - } - - #[test] - fn read_dir_from_upper_layer() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs( - &litebox, - upper([ - ( - "/upperdir", - InitialNode::Directory { - mode: ALL_PERMS, - owner: ACTING_USER, - }, - ), - ( - "/upperfile", - InitialNode::File { - mode: Mode::RWXU, - owner: ACTING_USER, - data: alloc::borrow::Cow::Borrowed(b""), - }, - ), - ]), - ); - - // Read root directory (should contain entries from both layers) - let fd = fs - .open(&ctx, "/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open root directory"); - let entries = fs.read_dir(&fd).expect("Failed to read root directory"); - fs.close(&fd).expect("Failed to close root directory"); - - // Should have 6 entries: ., .., bar, foo (from lower), upperdir, upperfile (from upper) - assert_eq!(entries.len(), 6); - - let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); - names.sort_unstable(); - assert_eq!( - names, - vec![".", "..", "bar", "foo", "upperdir", "upperfile"] - ); - - // Check file types - for entry in &entries { - match entry.name.as_str() { - "foo" | "upperfile" => { - assert_eq!(entry.file_type, crate::fs::FileType::RegularFile); - } - "bar" | "upperdir" | "." | ".." => { - assert_eq!(entry.file_type, crate::fs::FileType::Directory); - } - _ => panic!("Unexpected entry: {}", entry.name), - } - if entry.name != "." && entry.name != ".." { - assert!(entry.ino_info.is_some(), "Inode info should be present"); - } else { - // TODO(jayb): Re-enable this assertion once the resolver fills in - // inode information for the synthesized `.` and `..` entries. - } - } - - // Read upperdir directory (should be from upper layer) - let fd = fs - .open(&ctx, "/upperdir", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open upperdir"); - let entries = fs.read_dir(&fd).expect("Failed to read upperdir"); - fs.close(&fd).expect("Failed to close upperdir"); - - // only . and .. - assert_eq!(entries.len(), 2); - } - - #[test] - fn o_excl_tests() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // Test O_CREAT | O_EXCL on file that exists in lower layer (should fail) - // "foo" exists in the tar file - assert!(matches!( - fs.open( - &ctx, - "foo", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ), - Err(crate::fs::errors::OpenError::AlreadyExists) - )); - - // Test O_CREAT | O_EXCL on file that doesn't exist anywhere (should succeed) - let fd = fs - .open( - &ctx, - "/newfile", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create new file with O_CREAT | O_EXCL"); - - fs.write(&fd, b"overlay test", None) - .expect("Failed to write to new file"); - fs.close(&fd).expect("Failed to close new file"); - - // Test O_CREAT | O_EXCL on file that now exists in upper layer (should fail) - assert!(matches!( - fs.open( - &ctx, - "/newfile", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ), - Err(crate::fs::errors::OpenError::AlreadyExists) - )); - - // Test O_CREAT | O_EXCL on directory that exists in lower layer (should fail) - // "bar" is a directory in the tar file - assert!(matches!( - fs.open( - &ctx, - "bar", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ), - Err(crate::fs::errors::OpenError::AlreadyExists) - )); - - // Test O_CREAT | O_EXCL on file that was deleted (tombstoned) should succeed - // First delete a file from lower layer - fs.unlink(&ctx, "foo") - .expect("Failed to unlink lower layer file"); - - // Now try to create it with O_EXCL (should succeed since it's tombstoned) - let fd = fs - .open( - &ctx, - "foo", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create file over tombstone with O_CREAT | O_EXCL"); - - fs.write(&fd, b"new foo content", None) - .expect("Failed to write to recreated file"); - fs.close(&fd).expect("Failed to close recreated file"); - - // Verify the new content - let fd = fs - .open(&ctx, "foo", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open recreated file"); - let mut buffer = vec![0; 15]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from recreated file"); - assert_eq!(&buffer[..bytes_read], b"new foo content"); - fs.close(&fd).expect("Failed to close recreated file"); - - // Test O_CREAT | O_EXCL behavior with existing upper layer file - // Create a file in upper layer first - let fd = fs - .open( - &ctx, - "/upper_only_file", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("Failed to create upper layer file"); - fs.write(&fd, b"upper content", None) - .expect("Failed to write to upper layer file"); - fs.close(&fd).expect("Failed to close upper layer file"); - - // Now try O_CREAT | O_EXCL on the same file (should fail) - assert!(matches!( - fs.open( - &ctx, - "/upper_only_file", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, - Mode::RWXU, - ), - Err(crate::fs::errors::OpenError::AlreadyExists) - )); - } - - #[test] - fn dir_creation_inside_lower_existing_dir() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // Create the directory /bar/test (where /bar already exists inside the tar file) - fs.mkdir(&ctx, "/bar/test", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create /bar/test directory"); - - // Verify the directory was created - let stat = fs - .file_status(&ctx, "/bar/test") - .expect("Failed to get status of /bar/test"); - assert_eq!(stat.file_type, FileType::Directory); - - // Verify we can open the directory - let fd = fs - .open(&ctx, "/bar/test", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open /bar/test directory"); - let entries = fs - .read_dir(&fd) - .expect("Failed to read /bar/test directory"); - fs.close(&fd).expect("Failed to close directory"); - - // Should contain only . and .. entries - assert_eq!(entries.len(), 2); - let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); - names.sort_unstable(); - assert_eq!(names, vec![".", ".."]); - } - - #[test] - fn file_creation_materializes_ancestor_dirs() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // Open bar/test for writing (where bar exists in lower layer but test doesn't exist) - // This should create ancestor directories and allow file creation - let fd = fs - .open(&ctx, "bar/test", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to open bar/test for writing"); - - // Write data to the file - let data = b"Hello from nested file!"; - fs.write(&fd, data, None) - .expect("Failed to write to bar/test"); - fs.close(&fd).expect("Failed to close file"); - - // Read the file back - let fd = fs - .open(&ctx, "bar/test", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open bar/test for reading"); - let mut buffer = vec![0; 1024]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from bar/test"); - assert_eq!(&buffer[..bytes_read], data); - fs.close(&fd).expect("Failed to close file"); - - // Verify the file exists and has correct type - let stat = fs - .file_status(&ctx, "bar/test") - .expect("Failed to get status of bar/test"); - assert_eq!(stat.file_type, FileType::RegularFile); - } - - #[test] - fn file_modification_materializes_ancestor_dirs() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // Open bar/baz for writing (both bar and baz exist in lower layer) - // This copies up the ancestor directories and allows the file to be modified - let fd = fs - .open(&ctx, "bar/baz", OFlags::WRONLY, Mode::RWXU) - .expect("Failed to open bar/baz for writing"); - - // Write new data to the file (overwriting existing content) - let data = b"Modified content!"; - fs.write(&fd, data, None) - .expect("Failed to write to bar/baz"); - fs.close(&fd).expect("Failed to close file"); - - // Read the file back to verify it was modified - let fd = fs - .open(&ctx, "bar/baz", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open bar/baz for reading"); - let mut buffer = vec![0; 1024]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read from bar/baz"); - - assert_eq!(&buffer[..bytes_read], data); - fs.close(&fd).expect("Failed to close file"); - - // Verify the file still exists and has correct type - let stat = fs - .file_status(&ctx, "bar/baz") - .expect("Failed to get status of bar/baz"); - assert_eq!(stat.file_type, FileType::RegularFile); - } - - #[test] - fn open_with_trunc() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // Open with O_TRUNC should copy the file up into the upper backend, empty - let fd = fs - .open(&ctx, "foo", OFlags::RDWR | OFlags::TRUNC, Mode::empty()) - .expect("Failed to open file with O_TRUNC"); - - // File should be truncated (empty) - let mut buffer = vec![0; 1024]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read file"); - assert_eq!(bytes_read, 0); - - // Write new content - fs.write(&fd, b"new content", None) - .expect("Failed to write to file"); - fs.close(&fd).expect("Failed to close file"); - - // Verify the content persists - let fd = fs - .open(&ctx, "foo", OFlags::RDONLY, Mode::empty()) - .expect("Failed to reopen file"); - let mut buffer = vec![0; 1024]; - let bytes_read = fs - .read(&fd, &mut buffer, None) - .expect("Failed to read file"); - assert_eq!(&buffer[..bytes_read], b"new content"); - fs.close(&fd).expect("Failed to close file"); - } - - #[test] - fn rmdir_upper_only_directory() { - use crate::fs::errors::{PathError, RmdirError}; - - let ctx = crate::fs::resolver::Context::new(); - - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // Create an empty directory only in upper layer - fs.mkdir(&ctx, "/upper_empty", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("mkdir upper_empty failed"); - - // Remove it - fs.rmdir(&ctx, "/upper_empty") - .expect("rmdir upper_empty should succeed"); - - // Verify it no longer exists - assert!(matches!( - fs.file_status(&ctx, "/upper_empty"), - Err(crate::fs::errors::FileStatusError::PathError( - PathError::NoSuchFileOrDirectory - )) - )); - - // Second removal should yield NoSuchFileOrDirectory (path error) - assert!(matches!( - fs.rmdir(&ctx, "/upper_empty"), - Err(RmdirError::PathError(PathError::NoSuchFileOrDirectory)) - )); - } - - #[test] - fn rmdir_upper_directory_not_empty_then_empty() { - use crate::fs::errors::{PathError, RmdirError}; - - let ctx = crate::fs::resolver::Context::new(); - - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - fs.mkdir(&ctx, "/upper_dir", Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("mkdir upper_dir failed"); - - // Create a file inside making directory non-empty - let fd = fs - .open( - &ctx, - "/upper_dir/file", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU | Mode::RWXG, - ) - .expect("create file in upper_dir failed"); - fs.close(&fd).unwrap(); - - // Attempt to remove while non-empty - assert!(matches!( - fs.rmdir(&ctx, "/upper_dir"), - Err(RmdirError::NotEmpty) - )); - - // Remove inner file - fs.unlink(&ctx, "/upper_dir/file") - .expect("unlink inner failed"); - - // Now should succeed - fs.rmdir(&ctx, "/upper_dir") - .expect("rmdir upper_dir should succeed"); - - // Confirm gone - assert!(matches!( - fs.file_status(&ctx, "/upper_dir"), - Err(crate::fs::errors::FileStatusError::PathError( - PathError::NoSuchFileOrDirectory - )) - )); - } - - #[test] - fn rmdir_lower_directory_non_empty() { - use crate::fs::errors::RmdirError; - - let ctx = crate::fs::resolver::Context::new(); - - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // "bar" exists in lower layer and contains "baz" (non-empty) - assert!(matches!(fs.rmdir(&ctx, "bar"), Err(RmdirError::NotEmpty))); - } - - #[test] - fn rmdir_not_a_directory() { - use crate::fs::errors::RmdirError; - - let ctx = crate::fs::resolver::Context::new(); - - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - // Create a regular file (upper only) - let fd = fs - .open( - &ctx, - "/regular_file", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU | Mode::RWXG, - ) - .expect("create file failed"); - fs.close(&fd).unwrap(); - - // rmdir should fail with NotADirectory - assert!(matches!( - fs.rmdir(&ctx, "/regular_file"), - Err(RmdirError::NotADirectory) - )); - } - - #[test] - fn copy_up_does_not_deadlock() { - use std::sync::mpsc; - use std::thread; - use std::time::Duration; - - let ctx = crate::fs::resolver::Context::new(); - - let litebox = LiteBox::new(MockPlatform::new()); - let fs = overlay_fs(&litebox, upper([])); - - fs.file_status(&ctx, "foo").expect("Failed to stat foo"); - - // Writing to the lower-layer file triggers copy-up. Run it on a worker thread. - let (tx, rx) = mpsc::channel(); - thread::spawn(move || { - let fd = fs - .open(&ctx, "foo", OFlags::WRONLY, Mode::RWXU) - .expect("Failed to open file for writing"); - fs.write(&fd, b"x", None).expect("Failed to write to file"); - fs.close(&fd).expect("Failed to close file"); - let _ = tx.send(()); - }); - - rx.recv_timeout(Duration::from_secs(2)) - .expect("copy-up deadlocked"); - } -} - -mod stdio { - use crate::LiteBox; - use crate::fs::devices::Devices; - use crate::fs::errors::{ReadError, WriteError}; - use crate::fs::resolver::Resolver; - use crate::fs::{Mode, OFlags}; - use crate::platform::mock::MockPlatform; - use alloc::vec; - extern crate std; - - #[test] - fn stdio_requires_broker() { - let ctx = crate::fs::resolver::Context::new(); - let platform = MockPlatform::new(); - let litebox = LiteBox::new(platform); - let fs = Resolver::new( - &litebox, - crate::fs::composer::Composer::builder() - .mount("/dev", Devices::new) - .build() - .unwrap(), - ); - - let fd_stdout = fs - .open(&ctx, "/dev/stdout", OFlags::WRONLY, Mode::empty()) - .expect("Failed to open /dev/stdout"); - assert!(matches!(fs.write(&fd_stdout, b"", None), Ok(0))); - assert!(matches!( - fs.write(&fd_stdout, b"Hello, stdout!", None), - Err(WriteError::Io) - )); - fs.close(&fd_stdout).expect("Failed to close /dev/stdout"); - - let fd_stderr = fs - .open(&ctx, "/dev/stderr", OFlags::WRONLY, Mode::empty()) - .expect("Failed to open /dev/stderr"); - assert!(matches!(fs.write(&fd_stderr, b"", None), Ok(0))); - assert!(matches!( - fs.write(&fd_stderr, b"Hello, stderr!", None), - Err(WriteError::Io) - )); - fs.close(&fd_stderr).expect("Failed to close /dev/stderr"); - - let fd_stdin = fs - .open(&ctx, "/dev/stdin", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open /dev/stdin"); - assert!(matches!(fs.read(&fd_stdin, &mut [], None), Ok(0))); - let mut buffer = vec![0; 13]; - assert!(matches!( - fs.read(&fd_stdin, &mut buffer, None), - Err(ReadError::Io) - )); - fs.close(&fd_stdin).expect("Failed to close /dev/stdin"); - } - - #[test] - fn non_dev_path_fails() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = Resolver::new( - &litebox, - crate::fs::composer::Composer::builder() - .mount("/dev", Devices::new) - .build() - .unwrap(), - ); - - // Attempt to open a non-/dev/* path - let result = fs.open(&ctx, "foo", OFlags::RDONLY, Mode::empty()); - assert!(matches!( - result, - Err(crate::fs::errors::OpenError::PathError( - crate::fs::errors::PathError::NoSuchFileOrDirectory - )) - )); - } -} - -mod composed_stdio { - use crate::LiteBox; - use crate::fs::composer::Composer; - use crate::fs::devices::Devices; - use crate::fs::errors::{ReadError, WriteError}; - use crate::fs::in_mem::{InMem, InitialNode}; - use crate::fs::resolver::Resolver; - use crate::fs::{Mode, OFlags, UserInfo}; - use crate::platform::mock::MockPlatform; - use alloc::vec; - extern crate std; - - type ComposedFs = Resolver; - - fn composed_fs(litebox: &LiteBox) -> ComposedFs { - Resolver::new( - litebox, - Composer::builder() - .mount("/", |_| { - InMem::::new_initialized([( - "/", - InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: UserInfo::ROOT, - }, - )]) - }) - .mount("/dev", Devices::new) - .build() - .unwrap(), - ) - } - - #[test] - fn stdio_requires_broker() { - let ctx = crate::fs::resolver::Context::new(); - let platform = MockPlatform::new(); - let litebox = LiteBox::new(platform); - let fs = composed_fs(&litebox); - - let fd_stdout = fs - .open(&ctx, "/dev/stdout", OFlags::WRONLY, Mode::empty()) - .expect("Failed to open /dev/stdout"); - assert!(matches!(fs.write(&fd_stdout, b"", None), Ok(0))); - assert!(matches!( - fs.write(&fd_stdout, b"Hello, composed stdout!", None), - Err(WriteError::Io) - )); - fs.close(&fd_stdout).expect("Failed to close /dev/stdout"); - - let fd_stderr = fs - .open(&ctx, "/dev/stderr", OFlags::WRONLY, Mode::empty()) - .expect("Failed to open /dev/stderr"); - assert!(matches!(fs.write(&fd_stderr, b"", None), Ok(0))); - assert!(matches!( - fs.write(&fd_stderr, b"Hello, composed stderr!", None), - Err(WriteError::Io) - )); - fs.close(&fd_stderr).expect("Failed to close /dev/stderr"); - - let fd_stdin = fs - .open(&ctx, "/dev/stdin", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open /dev/stdin"); - assert!(matches!(fs.read(&fd_stdin, &mut [], None), Ok(0))); - let mut buffer = vec![0; 1024]; - assert!(matches!( - fs.read(&fd_stdin, &mut buffer, None), - Err(ReadError::Io) - )); - fs.close(&fd_stdin).expect("Failed to close /dev/stdin"); - } - - #[test] - fn write_to_non_dev() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = LiteBox::new(MockPlatform::new()); - let fs = composed_fs(&litebox); - - // Test file creation - let path = "/testfile"; - let fd = fs - .open(&ctx, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("Failed to create file"); - - fs.close(&fd).expect("Failed to close file"); - - // Test file deletion - fs.unlink(&ctx, path).expect("Failed to unlink file"); - assert!( - fs.open(&ctx, path, OFlags::RDONLY, Mode::RWXU).is_err(), - "File should not exist" - ); - } -} diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index a9581abab..072f7cbbb 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -5,7 +5,7 @@ use core::{ num::NonZeroUsize, - sync::atomic::{AtomicU32, Ordering::Relaxed}, + sync::atomic::{AtomicBool, Ordering::Relaxed}, }; use alloc::sync::{Arc, Weak}; @@ -28,7 +28,6 @@ use crate::{ polling::{Pollee, TryOpError}, wait::{WaitContext, WaitError}, }, - fs::OFlags, sync::RawSyncPrimitivesProvider, }; @@ -75,7 +74,7 @@ impl Pipes { broker, self.litebox.broker_pollable_registry(), capacity, - OFlags::from(flags), + flags, atomic_slice_guarantee_size, )?; let mut dt = self.litebox.descriptor_table_mut(); @@ -160,7 +159,11 @@ impl Pipes { .ok_or(errors::ClosedError::ClosedFd)? .entry .0; - Ok(Flags::from_oflags_truncate(p.get_status())) + Ok(if p.non_blocking.load(Relaxed) { + Flags::NON_BLOCKING + } else { + Flags::empty() + }) } /// Update the flags set on the pipe at `fd`. @@ -178,7 +181,9 @@ impl Pipes { .ok_or(errors::ClosedError::ClosedFd)? .entry .0; - p.set_status(OFlags::from(mask), on); + if mask.contains(Flags::NON_BLOCKING) { + p.non_blocking.store(on, Relaxed); + } Ok(()) } @@ -218,21 +223,6 @@ bitflags::bitflags! { } } -impl Flags { - fn from_oflags_truncate(oflags: OFlags) -> Self { - let mut flags = Flags::empty(); - flags.set(Flags::NON_BLOCKING, oflags.contains(OFlags::NONBLOCK)); - flags - } -} -impl From for OFlags { - fn from(flags: Flags) -> Self { - let mut oflags = OFlags::empty(); - oflags.set(OFlags::NONBLOCK, flags.contains(Flags::NON_BLOCKING)); - oflags - } -} - pub mod errors { use crate::event::wait::WaitError; @@ -356,7 +346,7 @@ struct BrokerPipeEnd { pollee: Arc>, peer: Weak, endpoint_type: HalfPipeType, - status: AtomicU32, + non_blocking: AtomicBool, } #[expect( @@ -367,7 +357,7 @@ fn new_broker_pipe( broker: Arc, pollable_registry: Arc>, capacity: usize, - flags: OFlags, + flags: Flags, atomic_slice_guarantee_size: Option, ) -> Result<(Arc>, Arc>), errors::CreateError> { let atomic_write_size = atomic_slice_guarantee_size @@ -395,7 +385,7 @@ fn new_broker_pipe( pollee: Arc::new(Pollee::new()), peer: Weak::new(), endpoint_type: HalfPipeType::SenderHalf, - status: AtomicU32::new((flags | OFlags::WRONLY).bits()), + non_blocking: AtomicBool::new(flags.contains(Flags::NON_BLOCKING)), }); let reader = Arc::new_cyclic(|weak_reader| { Arc::get_mut(&mut writer) @@ -408,7 +398,7 @@ fn new_broker_pipe( pollee: Arc::new(Pollee::new()), peer: Arc::downgrade(&writer), endpoint_type: HalfPipeType::ReceiverHalf, - status: AtomicU32::new((flags | OFlags::RDONLY).bits()), + non_blocking: AtomicBool::new(flags.contains(Flags::NON_BLOCKING)), } }); @@ -418,18 +408,6 @@ fn new_broker_pipe( } 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 { @@ -440,27 +418,22 @@ impl BrokerPipeEnd .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()) - }, - ) + .wait(cx, self.non_blocking.load(Relaxed), 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) } @@ -468,7 +441,7 @@ impl BrokerPipeEnd if buf.is_empty() { return Ok(0); } - let nonblock = self.get_status().contains(OFlags::NONBLOCK); + let nonblock = self.non_blocking.load(Relaxed); if nonblock { let data = &buf[..buf.len().min(MAX_PIPE_TRANSFER_SIZE as usize)]; return self diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index 178011dd5..2716e9a3a 100644 --- a/litebox_broker_core/Cargo.toml +++ b/litebox_broker_core/Cargo.toml @@ -15,3 +15,7 @@ thiserror = { version = "2.0.6", default-features = false } [lints] workspace = true + +[dev-dependencies] +# Temporary directories exported by the `diod` server the 9P tests run against. +tempfile = "3" diff --git a/litebox_broker_core/src/fs/composer.rs b/litebox_broker_core/src/fs/composer.rs index 2baa830ad..48a6ef16c 100644 --- a/litebox_broker_core/src/fs/composer.rs +++ b/litebox_broker_core/src/fs/composer.rs @@ -271,7 +271,7 @@ impl Composer { .virtual_dirs .iter() .find(|dir| dir.path == path) - .map(|dir| dir.node_info.clone()) + .map(|dir| dir.node_info) .expect("virtual directory is precomputed"); FileStatus { file_type: FileType::Directory, diff --git a/litebox_broker_core/src/fs/devices.rs b/litebox_broker_core/src/fs/devices.rs index 220ed3724..487b63010 100644 --- a/litebox_broker_core/src/fs/devices.rs +++ b/litebox_broker_core/src/fs/devices.rs @@ -23,11 +23,11 @@ use super::inode_allocator::InodeAllocator; use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; /// Block size for stdio devices -const STDIO_BLOCK_SIZE: usize = 1024; +const STDIO_BLOCK_SIZE: u64 = 1024; /// Block size for null device -const NULL_BLOCK_SIZE: usize = 0x1000; +const NULL_BLOCK_SIZE: u64 = 0x1000; /// Block size for /dev/urandom -const URANDOM_BLOCK_SIZE: usize = 0x1000; +const URANDOM_BLOCK_SIZE: u64 = 0x1000; /// Constant node information for all 3 stdio devices: /// ```console @@ -40,21 +40,21 @@ const URANDOM_BLOCK_SIZE: usize = 0x1000; const STDIO_NODE_INFO: NodeInfo = NodeInfo { dev: 64, ino: 9, - rdev: core::num::NonZeroUsize::new(34822), + rdev: core::num::NonZeroU64::new(34822), }; /// Node info for /dev/null const NULL_NODE_INFO: NodeInfo = NodeInfo { dev: 5, ino: 4, // major=1, minor=3 - rdev: core::num::NonZeroUsize::new(0x103), + rdev: core::num::NonZeroU64::new(0x103), }; /// Node info for /dev/urandom const URANDOM_NODE_INFO: NodeInfo = NodeInfo { dev: 5, ino: 8, // major=1, minor=9 - rdev: core::num::NonZeroUsize::new(0x109), + rdev: core::num::NonZeroU64::new(0x109), }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -316,7 +316,7 @@ impl Backend for Devices { mode: Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, size: super::DEFAULT_DIRECTORY_SIZE, owner: UserInfo::ROOT, - node_info: self.root_inode.clone(), + node_info: self.root_inode, blksize: super::DEFAULT_DIRECTORY_SIZE, }) } diff --git a/litebox_broker_core/src/fs/in_mem.rs b/litebox_broker_core/src/fs/in_mem.rs index 828700983..8922da989 100644 --- a/litebox_broker_core/src/fs/in_mem.rs +++ b/litebox_broker_core/src/fs/in_mem.rs @@ -388,8 +388,8 @@ impl super::backend::Backend for InMe .iter() .map(|(name, child)| { let (file_type, node_info) = match child { - Node::File(file) => (FileType::RegularFile, file.read().node_info.clone()), - Node::Dir(dir) => (FileType::Directory, dir.read().node_info.clone()), + Node::File(file) => (FileType::RegularFile, file.read().node_info), + Node::Dir(dir) => (FileType::Directory, dir.read().node_info), }; DirEntry { name: name.clone(), @@ -466,9 +466,9 @@ impl super::backend::Backend for InMe Ok(FileStatus { file_type: FileType::RegularFile, mode: file.perms.mode, - size: file.data.len(), + size: u64::try_from(file.data.len()).map_err(|_| FileStatusError::Io)?, owner: file.perms.userinfo, - node_info: file.node_info.clone(), + node_info: file.node_info, blksize: BLOCK_SIZE, }) } @@ -479,7 +479,7 @@ impl super::backend::Backend for InMe mode: dir.perms.mode, size: super::DEFAULT_DIRECTORY_SIZE, owner: dir.perms.userinfo, - node_info: dir.node_info.clone(), + node_info: dir.node_info, blksize: BLOCK_SIZE, }) } @@ -649,7 +649,7 @@ fn assert_supported_oflags(flags: super::OFlags) { /// Block size for file system I/O operations // TODO(jayb): Determine appropriate block size -const BLOCK_SIZE: usize = 0; +const BLOCK_SIZE: u64 = 0; enum Node { File(FileNode), diff --git a/litebox_broker_core/src/fs/inode_allocator.rs b/litebox_broker_core/src/fs/inode_allocator.rs index 9540807be..3e96043ab 100644 --- a/litebox_broker_core/src/fs/inode_allocator.rs +++ b/litebox_broker_core/src/fs/inode_allocator.rs @@ -53,20 +53,20 @@ impl InodeAllocator { Self::for_device(STANDALONE_DEVICE_ID) } - /// Allocate a fresh `NodeInfo` for a new entry on this backend. + /// Allocate a fresh [`NodeInfo`] for a new entry on this backend. #[must_use] pub fn next(&self) -> NodeInfo { let ino = self.counter.fetch_add(1, Ordering::Relaxed); NodeInfo { dev: self.device_id(), - ino: ino.try_into().unwrap(), + ino, rdev: None, } } /// The device id this allocator hands out. #[must_use] - pub fn device_id(&self) -> usize { - self.device_id.try_into().unwrap() + pub fn device_id(&self) -> u64 { + self.device_id } } diff --git a/litebox_broker_core/src/fs/mod.rs b/litebox_broker_core/src/fs/mod.rs index 1917f9a0b..08c221696 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -11,7 +11,6 @@ use bitflags::bitflags; use core::ffi::c_uint; -use core::num::NonZeroUsize; pub mod backend; pub mod composer; @@ -25,63 +24,19 @@ pub mod overlay; pub mod resolver; mod service; pub mod tar_ro; +#[cfg(test)] +mod tests; +pub use litebox_broker_protocol::fs::{ + FileDirectoryEntry as DirEntry, FileMode as Mode, FileNodeInfo as NodeInfo, + FileSeekWhence as SeekWhence, FileStatus, FileType, FileUser as UserInfo, +}; pub(crate) use service::File; pub use service::{ FileResult, FileService, UnsupportedFileService, chmod, chown, handle_status, mkdir, open, path_status, read, read_directory, rmdir, seek, truncate, unlink, write, }; -bitflags! { - /// `S_I*` constants for open, ... - #[repr(transparent)] - #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] - pub struct Mode: c_uint { - /// `S_IRWXU`: user (file owner) has read, write, and execute permission - const RWXU = 0o00700; - /// `S_IRUSR`: user has read permission - const RUSR = 0o00400; - /// `S_IWUSR`: user has write permission - const WUSR = 0o00200; - /// `S_IXUSR`: user has execute permission - const XUSR = 0o00100; - /// `S_IRWXG`: group has read, write, and execute permission - const RWXG = 0o00070; - /// `S_IRGRP`: group has read permission - const RGRP = 0o00040; - /// `S_IWGRP`: group has write permission - const WGRP = 0o00020; - /// `S_IXGRP`: group has execute permission - const XGRP = 0o00010; - /// `S_IRWXO`: others have read, write, and execute permission - const RWXO = 0o00007; - /// `S_IROTH`: others have read permission - const ROTH = 0o00004; - /// `S_IWOTH`: others have write permission - const WOTH = 0o00002; - /// `S_IXOTH`: others have execute permission - const XOTH = 0o00001; - /// `S_ISUID`: set-user-ID bit - const SUID = 0o0004000; - /// `S_ISGID`: set-group-ID bit (see inode(7)). - const SGID = 0o0002000; - /// `S_ISVTX`: sticky bit (see inode(7)). - const SVTX = 0o0001000; - /// - const _ = !0; - } -} - -/// Types of files on a file-system. -/// -/// See [`resolver::Resolver::file_status`]. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum FileType { - RegularFile, - Directory, - CharacterDevice, -} - bitflags! { /// `O_*` constants for use with open, ... #[repr(transparent)] @@ -168,72 +123,5 @@ bitflags! { } } -/// The `whence` directive to [`resolver::Resolver::seek`] -#[derive(Copy, Clone)] -pub enum SeekWhence { - /// The file offset is set to `offset` bytes. - RelativeToBeginning, - /// The file offset is set to its current location plus `offset` bytes. - RelativeToCurrentOffset, - /// The file offset is set to the size of the file plus `offset` bytes. - RelativeToEnd, -} - -/// The status of a file/directory/... on the file-system, inspired by `stat(3type)`. -/// -/// This is explicitly a non-exhaustive struct with public members. As LiteBox evolves, more -/// elements might be added to this struct, allowing file systems to provide richer information -/// about the status of files. However, users of LiteBox must not depend on the completeness or even -/// layout of this particular type. -#[non_exhaustive] -pub struct FileStatus { - /// File type - pub file_type: FileType, - /// Permissions for the file - pub mode: Mode, - /// Size of the file, in bytes. This value considered informative if this is a regular file. - pub size: usize, - /// Owner of the file - pub owner: UserInfo, - /// Information about this particular node - pub node_info: NodeInfo, - /// Block size for file system I/O - pub blksize: usize, -} - -/// User information -#[derive(Clone, Copy, Debug)] -pub struct UserInfo { - /// User ID for the owner - pub user: u16, - /// Group ID for the owner - pub group: u16, -} - -/// Device/Inode information -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -pub struct NodeInfo { - /// Device number - pub dev: usize, - /// Inode number - pub ino: usize, - /// Device that is being referred to (will be `Some(...)` only if special file) - pub rdev: Option, -} - -/// Directory entries returned by [`resolver::Resolver::read_dir`] -#[derive(Debug)] -#[non_exhaustive] -pub struct DirEntry { - pub name: alloc::string::String, - pub file_type: FileType, - pub ino_info: Option, -} - -impl UserInfo { - /// The root user - pub const ROOT: Self = Self { user: 0, group: 0 }; -} - /// The size reported as the size of a directory. -const DEFAULT_DIRECTORY_SIZE: usize = 4096; +const DEFAULT_DIRECTORY_SIZE: u64 = 4096; diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index c751fabe3..38834b2fb 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -11,7 +11,7 @@ use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; -use core::num::NonZeroUsize; +use core::num::NonZeroU64; use core::sync::atomic::{AtomicBool, Ordering}; use thiserror::Error; @@ -31,6 +31,8 @@ use litebox_platform::sync; mod client; mod fcall; mod id_pool; +#[cfg(all(test, target_os = "linux"))] +mod tests; pub mod transport; @@ -53,7 +55,7 @@ pub struct NineP>, /// Device id reported in every [`NodeInfo`](super::NodeInfo) from this backend; inode numbers /// come from the server's qids instead. - device_id: usize, + device_id: u64, /// Whether `unlinkat` is supported by the server unlinkat_supported: AtomicBool, } @@ -440,18 +442,16 @@ where // have the resolver handle only cases where it is not handled by the backend? !matches!(&*entry.name, b"." | b"..") }) - .map(|entry| { - Ok(super::DirEntry { - name: String::from_utf8_lossy(&entry.name).into_owned(), - file_type: qid_type_to_file_type(entry.qid.typ), - ino_info: Some(super::NodeInfo { - dev: self.device_id, - ino: usize::try_from(entry.qid.path).map_err(|_| Error::InvalidResponse)?, - rdev: None, - }), - }) + .map(|entry| super::DirEntry { + name: String::from_utf8_lossy(&entry.name).into_owned(), + file_type: qid_type_to_file_type(entry.qid.typ), + ino_info: Some(super::NodeInfo { + dev: self.device_id, + ino: entry.qid.path, + rdev: None, + }), }) - .collect::>()?) + .collect()) } fn read( @@ -525,7 +525,7 @@ where fid, name, fcall::LOpenFlags::O_RDWR, - metadata.mode.bits(), + metadata.mode.bits().into(), u32::from(metadata.owner.group), )?; Ok(FileHandle::from_typed::(NinePFileHandle { @@ -544,7 +544,7 @@ where self.client.mkdir( &dir.fid.fid, name, - metadata.mode.bits(), + metadata.mode.bits().into(), u32::from(metadata.owner.group), )?; // `Tmkdir` only reports the new directory's qid, so a walk is needed to address it. @@ -576,7 +576,7 @@ where HandleRef::Dir(h) => &h.get_typed::().fid, }; let stat = fcall::SetAttr { - mode: mode.bits(), + mode: mode.bits().into(), ..Default::default() }; Ok(self @@ -715,38 +715,36 @@ fn qid_type_to_file_type(qid_type: fcall::QidType) -> super::FileType { /// filesystem as. fn rgetattr_to_file_status( attr: &fcall::Rgetattr, - device_id: usize, + device_id: u64, ) -> Result { let file_type = qid_type_to_file_type(attr.qid.typ); if attr.valid.contains(fcall::GetattrMask::BASIC) { Ok(super::FileStatus { file_type, - mode: super::Mode::from_bits_truncate(attr.stat.mode), - size: usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)?, + mode: super::Mode::from_u32_bits_truncate(attr.stat.mode), + size: attr.stat.size, owner: super::UserInfo { user: u16::try_from(attr.stat.uid).map_err(|_| Error::InvalidResponse)?, group: u16::try_from(attr.stat.gid).map_err(|_| Error::InvalidResponse)?, }, node_info: super::NodeInfo { dev: device_id, - ino: usize::try_from(attr.qid.path).map_err(|_| Error::InvalidResponse)?, - rdev: NonZeroUsize::new( - usize::try_from(attr.stat.rdev).map_err(|_| Error::InvalidResponse)?, - ), + ino: attr.qid.path, + rdev: NonZeroU64::new(attr.stat.rdev), }, - blksize: usize::try_from(attr.stat.blksize).map_err(|_| Error::InvalidResponse)?, + blksize: attr.stat.blksize, }) } else { Ok(super::FileStatus { file_type, mode: if attr.valid.contains(fcall::GetattrMask::MODE) { - super::Mode::from_bits_truncate(attr.stat.mode) + super::Mode::from_u32_bits_truncate(attr.stat.mode) } else { super::Mode::empty() }, size: if attr.valid.contains(fcall::GetattrMask::SIZE) { - usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)? + attr.stat.size } else { 0 }, @@ -764,17 +762,15 @@ fn rgetattr_to_file_status( }, node_info: super::NodeInfo { dev: device_id, - ino: usize::try_from(attr.qid.path).map_err(|_| Error::InvalidResponse)?, + ino: attr.qid.path, rdev: if attr.valid.contains(fcall::GetattrMask::RDEV) { - NonZeroUsize::new( - usize::try_from(attr.stat.rdev).map_err(|_| Error::InvalidResponse)?, - ) + NonZeroU64::new(attr.stat.rdev) } else { None }, }, blksize: if attr.valid.contains(fcall::GetattrMask::BLOCKS) { - usize::try_from(attr.stat.blksize).map_err(|_| Error::InvalidResponse)? + attr.stat.blksize } else { 0 }, diff --git a/litebox/src/fs/nine_p/tests.rs b/litebox_broker_core/src/fs/nine_p/tests.rs similarity index 68% rename from litebox/src/fs/nine_p/tests.rs rename to litebox_broker_core/src/fs/nine_p/tests.rs index a46c43d14..3c11b82da 100644 --- a/litebox/src/fs/nine_p/tests.rs +++ b/litebox_broker_core/src/fs/nine_p/tests.rs @@ -1,31 +1,42 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -extern crate std; +//! 9P filesystem semantics, exercised against a real `diod` server. +//! +//! These tests drive the broker-core resolver over the [`NineP`] backend, so they cover the +//! client's protocol handling and the resolver semantics layered on it. They need `diod` +//! installed (`apt install diod`). use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::io::{Read as _, Write as _}; use std::net::{TcpListener, TcpStream}; use std::path::Path; +use crate::fs::backend::NoDeviceIo; use crate::fs::errors::{ FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; use crate::fs::inode_allocator::InodeAllocator; use crate::fs::resolver::Resolver; -use crate::fs::{Mode, OFlags}; -use crate::platform::mock::MockPlatform; +use crate::fs::{FileType, Mode, OFlags, SeekWhence}; +use crate::test_platform::TestPlatform; -use super::nine_p::{NineP, transport}; +use super::{NineP, transport}; -type NinePFs = Resolver>; +/// A resolver over a 9P backend reached through `T`. +type NinePFs = Resolver>; + +const USER: crate::fs::UserInfo = crate::fs::UserInfo { + user: 1000, + group: 1000, +}; /// Attach to `server` over `transport`, building the backend the tests resolve paths through. fn attach( transport: T, server: &DiodServer, -) -> NineP { +) -> NineP { let aname = server.export_path().to_str().unwrap(); let username = std::env::var("USER") .or_else(|_| std::env::var("LOGNAME")) @@ -40,7 +51,7 @@ fn attach( .expect("failed to create 9P filesystem") } -/// A wrapper around `TcpStream` that implements the litebox 9P transport traits. +/// A wrapper around `TcpStream` that implements the 9P transport traits. struct TcpTransport { stream: TcpStream, } @@ -194,12 +205,9 @@ impl Drop for DiodServer { // Helper: create a connected 9P filesystem // --------------------------------------------------------------------------- -fn connect_9p( - litebox: &crate::LiteBox, - server: &DiodServer, -) -> NinePFs { +fn connect_9p(server: &DiodServer) -> NinePFs { let transport = TcpTransport::connect(&server.addr()); - Resolver::new(litebox, attach(transport, server)) + Resolver::new(attach(transport, server)) } // --------------------------------------------------------------------------- @@ -208,15 +216,13 @@ fn connect_9p( #[test] fn test_nine_p_create_and_read_file() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Create a file and write to it - let fd = fs + let mut fd = fs .open( - &ctx, + USER, "/hello.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, @@ -224,10 +230,12 @@ fn test_nine_p_create_and_read_file() { .expect("failed to create file via 9P"); let data = b"Hello from litebox 9P!"; - let written = fs.write(&fd, data, None).expect("failed to write via 9P"); + let written = fs + .write(&NoDeviceIo, &mut fd, data, None) + .expect("failed to write via 9P"); assert_eq!(written, data.len()); - fs.close(&fd).expect("failed to close file"); + drop(fd); // Verify the file exists on the host let host_path = server.export_path().join("hello.txt"); @@ -236,48 +244,49 @@ fn test_nine_p_create_and_read_file() { assert_eq!(host_content, "Hello from litebox 9P!"); // Read the file back through 9P - let fd = fs - .open(&ctx, "/hello.txt", OFlags::RDONLY, Mode::empty()) + let mut fd = fs + .open(USER, "/hello.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading via 9P"); let mut buf = alloc::vec![0u8; 256]; - let bytes_read = fs.read(&fd, &mut buf, None).expect("failed to read via 9P"); + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buf, None) + .expect("failed to read via 9P"); assert_eq!(&buf[..bytes_read], data); - fs.close(&fd).expect("failed to close file"); + drop(fd); } #[test] fn test_nine_p_mkdir_and_readdir() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Create directories - fs.mkdir(&ctx, "/subdir", Mode::RWXU) + fs.mkdir(USER, "/subdir", Mode::RWXU) .expect("failed to mkdir via 9P"); - fs.mkdir(&ctx, "/subdir/nested", Mode::RWXU) + fs.mkdir(USER, "/subdir/nested", Mode::RWXU) .expect("failed to mkdir nested via 9P"); // Create a file inside the subdirectory - let fd = fs + let mut fd = fs .open( - &ctx, + USER, "/subdir/file.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("failed to create file in subdir"); - fs.write(&fd, b"nested content", None).unwrap(); - fs.close(&fd).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"nested content", None) + .unwrap(); + drop(fd); // Read the root directory let fd = fs - .open(&ctx, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open(USER, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) .expect("failed to open root dir"); let entries = fs.read_dir(&fd).expect("failed to readdir root"); - fs.close(&fd).unwrap(); + drop(fd); let names: alloc::vec::Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); assert!( @@ -288,14 +297,14 @@ fn test_nine_p_mkdir_and_readdir() { // Read the subdirectory let fd = fs .open( - &ctx, + USER, "/subdir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .expect("failed to open subdir"); let entries = fs.read_dir(&fd).expect("failed to readdir subdir"); - fs.close(&fd).unwrap(); + drop(fd); let names: alloc::vec::Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); assert!( @@ -310,36 +319,34 @@ fn test_nine_p_mkdir_and_readdir() { #[test] fn test_nine_p_unlink_and_rmdir() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Create a file, then delete it let fd = fs .open( - &ctx, + USER, "/to_delete.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("failed to create file"); - fs.close(&fd).unwrap(); + drop(fd); - fs.unlink(&ctx, "/to_delete.txt") + fs.unlink(USER, "/to_delete.txt") .expect("failed to unlink file via 9P"); // Verify the file is gone assert!( - fs.open(&ctx, "/to_delete.txt", OFlags::RDONLY, Mode::empty()) + fs.open(USER, "/to_delete.txt", OFlags::RDONLY, Mode::empty()) .is_err(), "file should no longer exist" ); // Create a directory, then remove it - fs.mkdir(&ctx, "/to_remove", Mode::RWXU) + fs.mkdir(USER, "/to_remove", Mode::RWXU) .expect("failed to mkdir"); - fs.rmdir(&ctx, "/to_remove") + fs.rmdir(USER, "/to_remove") .expect("failed to rmdir via 9P"); // Verify the directory is gone on the host @@ -351,107 +358,104 @@ fn test_nine_p_unlink_and_rmdir() { #[test] fn test_nine_p_file_status() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Create a file with known content - let fd = fs + let mut fd = fs .open( - &ctx, + USER, "/status_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("failed to create file"); let data = b"1234567890"; - fs.write(&fd, data, None).unwrap(); - fs.close(&fd).unwrap(); + fs.write(&NoDeviceIo, &mut fd, data, None).unwrap(); + drop(fd); // Check file_status via path let status = fs - .file_status(&ctx, "/status_test.txt") + .file_status(USER, "/status_test.txt") .expect("failed to stat file"); assert_eq!( status.file_type, - crate::fs::FileType::RegularFile, + FileType::RegularFile, "should be a regular file" ); assert_eq!(status.size, 10, "file size should be 10 bytes"); // Check directory status - fs.mkdir(&ctx, "/stat_dir", Mode::RWXU).unwrap(); + fs.mkdir(USER, "/stat_dir", Mode::RWXU).unwrap(); let status = fs - .file_status(&ctx, "/stat_dir") + .file_status(USER, "/stat_dir") .expect("failed to stat dir"); assert_eq!( status.file_type, - crate::fs::FileType::Directory, + FileType::Directory, "should be a directory" ); } #[test] fn test_nine_p_seek_and_partial_read() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Write a file with known content - let fd = fs + let mut fd = fs .open( - &ctx, + USER, "/seek_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("failed to create file"); - fs.write(&fd, b"ABCDEFGHIJ", None).unwrap(); - fs.close(&fd).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"ABCDEFGHIJ", None).unwrap(); + drop(fd); // Open for reading and seek - let fd = fs - .open(&ctx, "/seek_test.txt", OFlags::RDONLY, Mode::empty()) + let mut fd = fs + .open(USER, "/seek_test.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading"); // Seek to offset 5 let pos = fs - .seek(&fd, 5, crate::fs::SeekWhence::RelativeToBeginning) + .seek(&mut fd, 5, SeekWhence::RelativeToBeginning) .expect("failed to seek"); assert_eq!(pos, 5); // Read from offset 5 → should get "FGHIJ" let mut buf = alloc::vec![0u8; 10]; - let n = fs.read(&fd, &mut buf, None).expect("failed to read"); + let n = fs + .read(&NoDeviceIo, &mut fd, &mut buf, None) + .expect("failed to read"); assert_eq!(&buf[..n], b"FGHIJ"); - fs.close(&fd).unwrap(); + drop(fd); } #[test] fn test_nine_p_truncate() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Write a file - let fd = fs + let mut fd = fs .open( - &ctx, + USER, "/trunc_test.txt", OFlags::CREAT | OFlags::RDWR, Mode::RWXU, ) .expect("failed to create file"); - fs.write(&fd, b"Hello, World!", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"Hello, World!", None) + .unwrap(); // Truncate to 5 bytes - fs.truncate(&fd, 5, true) + fs.truncate(&mut fd, 5, true) .expect("failed to truncate via 9P"); - fs.close(&fd).unwrap(); + drop(fd); // Verify on host let content = std::fs::read_to_string(server.export_path().join("trunc_test.txt")).unwrap(); @@ -460,8 +464,6 @@ fn test_nine_p_truncate() { #[test] fn test_nine_p_host_files_visible() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); // Pre-populate some files on the host side @@ -473,28 +475,28 @@ fn test_nine_p_host_files_visible() { ) .unwrap(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Read file created on the host through 9P - let fd = fs - .open(&ctx, "/host_file.txt", OFlags::RDONLY, Mode::empty()) + let mut fd = fs + .open(USER, "/host_file.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open host file via 9P"); let mut buf = alloc::vec![0u8; 256]; - let n = fs.read(&fd, &mut buf, None).unwrap(); + let n = fs.read(&NoDeviceIo, &mut fd, &mut buf, None).unwrap(); assert_eq!(&buf[..n], b"from host"); - fs.close(&fd).unwrap(); + drop(fd); // List host directory through 9P let fd = fs .open( - &ctx, + USER, "/host_dir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .expect("failed to open host dir via 9P"); let entries = fs.read_dir(&fd).unwrap(); - fs.close(&fd).unwrap(); + drop(fd); let names: alloc::vec::Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); assert!( @@ -560,16 +562,9 @@ impl transport::Write for BrokenTransport { /// `allowed_writes` must be >= 2 for the filesystem to be constructed /// successfully. Any FS operation after construction will consume one /// additional write. -fn connect_9p_broken( - litebox: &crate::LiteBox, - server: &DiodServer, - allowed_writes: usize, -) -> NinePFs { +fn connect_9p_broken(server: &DiodServer, allowed_writes: usize) -> NinePFs { let tcp = TcpTransport::connect(&server.addr()); - Resolver::new( - litebox, - attach(BrokenTransport::new(tcp, allowed_writes), server), - ) + Resolver::new(attach(BrokenTransport::new(tcp, allowed_writes), server)) } // --------------------------------------------------------------------------- @@ -580,107 +575,82 @@ fn connect_9p_broken( /// breaks after the filesystem has been attached. #[test] fn test_nine_p_broken_open() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); // 2 writes: version + attach. The next write (open's walk) will fail. - let fs = connect_9p_broken(&litebox, &server, 2); + let fs = connect_9p_broken(&server, 2); - let result = fs.open(&ctx, "/anything.txt", OFlags::RDONLY, Mode::empty()); + let result = fs.open(USER, "/anything.txt", OFlags::RDONLY, Mode::empty()); assert!(matches!(result, Err(OpenError::Io))); } /// Creating a file should fail when the connection is broken. #[test] fn test_nine_p_broken_create() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p_broken(&litebox, &server, 2); + let fs = connect_9p_broken(&server, 2); - let result = fs.open(&ctx, "/new.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU); + let result = fs.open(USER, "/new.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU); assert!(matches!(result, Err(OpenError::Io))); } /// Reading from an fd obtained before the break should fail. #[test] fn test_nine_p_broken_read() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - // Pre-create a file via normal connection - { - let fs = connect_9p(&litebox, &server); - let fd = fs - .open( - &ctx, - "/read_me.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .unwrap(); - fs.write(&fd, b"data", None).unwrap(); - fs.close(&fd).unwrap(); - } + std::fs::write(server.export_path().join("read_me.txt"), b"data").unwrap(); // 4 writes: version + attach + walk + lopen. Then read will fail. - let fs = connect_9p_broken(&litebox, &server, 4); - let fd = fs - .open(&ctx, "/read_me.txt", OFlags::RDONLY, Mode::empty()) + let fs = connect_9p_broken(&server, 4); + let mut fd = fs + .open(USER, "/read_me.txt", OFlags::RDONLY, Mode::empty()) .expect("open should succeed before break"); let mut buf = alloc::vec![0u8; 64]; - let result = fs.read(&fd, &mut buf, None); + let result = fs.read(&NoDeviceIo, &mut fd, &mut buf, None); assert!(matches!(result, Err(ReadError::Io))); } /// Writing to an fd obtained before the break should fail. #[test] fn test_nine_p_broken_write() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); // 5 writes: version + attach + walk (which reports the file as missing) + the clone of the // parent directory's fid + create. Then write will fail. - let fs = connect_9p_broken(&litebox, &server, 5); - let fd = fs + let fs = connect_9p_broken(&server, 5); + let mut fd = fs .open( - &ctx, + USER, "/write_me.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("create should succeed before break"); - let result = fs.write(&fd, b"data", None); + let result = fs.write(&NoDeviceIo, &mut fd, b"data", None); assert!(matches!(result, Err(WriteError::Io))); } /// mkdir should fail when the connection is broken. #[test] fn test_nine_p_broken_mkdir() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p_broken(&litebox, &server, 2); + let fs = connect_9p_broken(&server, 2); - let result = fs.mkdir(&ctx, "/broken_dir", Mode::RWXU); + let result = fs.mkdir(USER, "/broken_dir", Mode::RWXU); assert!(matches!(result, Err(MkdirError::Io))); } /// readdir should fail when the connection breaks during the directory read. #[test] fn test_nine_p_broken_readdir() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); // 4 writes: version + attach + walk + lopen for the directory. - let fs = connect_9p_broken(&litebox, &server, 4); + let fs = connect_9p_broken(&server, 4); let fd = fs - .open(&ctx, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open(USER, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) .expect("open dir should succeed before break"); let result = fs.read_dir(&fd); @@ -690,120 +660,68 @@ fn test_nine_p_broken_readdir() { /// unlink should fail when the connection is broken. #[test] fn test_nine_p_broken_unlink() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - // Pre-create a file - { - let fs = connect_9p(&litebox, &server); - let fd = fs - .open( - &ctx, - "/to_unlink.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .unwrap(); - fs.close(&fd).unwrap(); - } + std::fs::write(server.export_path().join("to_unlink.txt"), b"").unwrap(); - let fs = connect_9p_broken(&litebox, &server, 2); - let result = fs.unlink(&ctx, "/to_unlink.txt"); + let fs = connect_9p_broken(&server, 2); + let result = fs.unlink(USER, "/to_unlink.txt"); assert!(matches!(result, Err(UnlinkError::Io))); } /// rmdir should fail when the connection is broken. #[test] fn test_nine_p_broken_rmdir() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - // Pre-create a directory - { - let fs = connect_9p(&litebox, &server); - fs.mkdir(&ctx, "/to_rmdir", Mode::RWXU).unwrap(); - } + std::fs::create_dir(server.export_path().join("to_rmdir")).unwrap(); - let fs = connect_9p_broken(&litebox, &server, 2); - let result = fs.rmdir(&ctx, "/to_rmdir"); + let fs = connect_9p_broken(&server, 2); + let result = fs.rmdir(USER, "/to_rmdir"); assert!(matches!(result, Err(RmdirError::Io))); } /// file_status should fail when the connection is broken. #[test] fn test_nine_p_broken_file_status() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p_broken(&litebox, &server, 2); + let fs = connect_9p_broken(&server, 2); - let result = fs.file_status(&ctx, "/"); + let result = fs.file_status(USER, "/"); assert!(matches!(result, Err(FileStatusError::Io))); } /// truncate should fail when the connection breaks after open. #[test] fn test_nine_p_broken_truncate() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - // Pre-create a file - { - let fs = connect_9p(&litebox, &server); - let fd = fs - .open( - &ctx, - "/to_trunc.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .unwrap(); - fs.write(&fd, b"some data", None).unwrap(); - fs.close(&fd).unwrap(); - } + std::fs::write(server.export_path().join("to_trunc.txt"), b"some data").unwrap(); // 4 writes: version + attach + walk + lopen. Then truncate will fail. - let fs = connect_9p_broken(&litebox, &server, 4); - let fd = fs - .open(&ctx, "/to_trunc.txt", OFlags::RDWR, Mode::empty()) + let fs = connect_9p_broken(&server, 4); + let mut fd = fs + .open(USER, "/to_trunc.txt", OFlags::RDWR, Mode::empty()) .expect("open should succeed before break"); - let result = fs.truncate(&fd, 0, true); + let result = fs.truncate(&mut fd, 0, true); assert!(matches!(result, Err(TruncateError::Io))); } /// seek (RelativeToEnd, which requires a getattr) should fail when broken. #[test] fn test_nine_p_broken_seek() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - // Pre-create a file - { - let fs = connect_9p(&litebox, &server); - let fd = fs - .open( - &ctx, - "/to_seek.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .unwrap(); - fs.write(&fd, b"data", None).unwrap(); - fs.close(&fd).unwrap(); - } + std::fs::write(server.export_path().join("to_seek.txt"), b"data").unwrap(); // 4 writes: version + attach + walk + lopen. Then the getattr for seek will fail. - let fs = connect_9p_broken(&litebox, &server, 4); - let fd = fs - .open(&ctx, "/to_seek.txt", OFlags::RDONLY, Mode::empty()) + let fs = connect_9p_broken(&server, 4); + let mut fd = fs + .open(USER, "/to_seek.txt", OFlags::RDONLY, Mode::empty()) .expect("open should succeed before break"); - let result = fs.seek(&fd, -1, crate::fs::SeekWhence::RelativeToEnd); + let result = fs.seek(&mut fd, -1, SeekWhence::RelativeToEnd); assert!(matches!(result, Err(SeekError::Io))); } @@ -811,71 +729,62 @@ fn test_nine_p_broken_seek() { fn test_nine_p_deep_path_walk() { use core::fmt::Write as _; - let ctx = crate::fs::resolver::Context::new(); - - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Create a path deeper than MAXWELEM (13) to exercise walk_chunked let mut path = std::string::String::new(); for i in 0..20 { path.push('/'); write!(path, "d{i}").unwrap(); - fs.mkdir(&ctx, &*path, Mode::RWXU) + fs.mkdir(USER, &path, Mode::RWXU) .expect("failed to mkdir deep path component"); } // Create a file at the bottom let file_path = path.clone() + "/deep_file.txt"; - let fd = fs - .open( - &ctx, - &*file_path, - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) + let mut fd = fs + .open(USER, &file_path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("failed to create file in deep path"); - fs.write(&fd, b"deep content", None).unwrap(); - fs.close(&fd).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"deep content", None) + .unwrap(); + drop(fd); // Read it back - let fd = fs - .open(&ctx, &*file_path, OFlags::RDONLY, Mode::empty()) + let mut fd = fs + .open(USER, &file_path, OFlags::RDONLY, Mode::empty()) .expect("failed to open file in deep path"); let mut buf = alloc::vec![0u8; 64]; - let n = fs.read(&fd, &mut buf, None).unwrap(); + let n = fs.read(&NoDeviceIo, &mut fd, &mut buf, None).unwrap(); assert_eq!(&buf[..n], b"deep content"); - fs.close(&fd).unwrap(); + drop(fd); // Verify file_status works through the deep path let status = fs - .file_status(&ctx, &*file_path) + .file_status(USER, &file_path) .expect("failed to stat deep file"); - assert_eq!(status.file_type, crate::fs::FileType::RegularFile); + assert_eq!(status.file_type, FileType::RegularFile); assert_eq!(status.size, 12); } #[test] fn test_nine_p_chmod() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Create a file let fd = fs .open( - &ctx, + USER, "/chmod_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("failed to create file"); - fs.close(&fd).unwrap(); + drop(fd); // Change permissions to read-only for user - fs.chmod(&ctx, "/chmod_test.txt", Mode::RUSR) + fs.chmod(USER, "/chmod_test.txt", Mode::RUSR) .expect("chmod failed"); // Verify via host filesystem @@ -890,7 +799,7 @@ fn test_nine_p_chmod() { // Also verify via 9P file_status let status = fs - .file_status(&ctx, "/chmod_test.txt") + .file_status(USER, "/chmod_test.txt") .expect("file_status failed"); assert!(status.mode.contains(Mode::RUSR), "mode should contain RUSR"); assert!( @@ -901,30 +810,28 @@ fn test_nine_p_chmod() { #[test] fn test_nine_p_chown() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Create a file let fd = fs .open( - &ctx, + USER, "/chown_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("failed to create file"); - fs.close(&fd).unwrap(); + drop(fd); // Get current ownership let status_before = fs - .file_status(&ctx, "/chown_test.txt") + .file_status(USER, "/chown_test.txt") .expect("file_status failed"); // Change group to the same value (chown to a different uid/gid requires root) fs.chown( - &ctx, + USER, "/chown_test.txt", Some(status_before.owner.user), Some(status_before.owner.group), @@ -933,59 +840,54 @@ fn test_nine_p_chown() { // Verify ownership hasn't changed let status_after = fs - .file_status(&ctx, "/chown_test.txt") + .file_status(USER, "/chown_test.txt") .expect("file_status failed after chown"); assert_eq!(status_after.owner.user, status_before.owner.user); assert_eq!(status_after.owner.group, status_before.owner.group); } #[test] -fn test_nine_p_fd_file_status() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); +fn test_nine_p_handle_status() { let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // Create a file with known content - let fd = fs + let mut fd = fs .open( - &ctx, + USER, "/fd_stat_test.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("failed to create file"); - fs.write(&fd, b"hello fd_stat", None).unwrap(); - fs.close(&fd).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"hello fd_stat", None) + .unwrap(); + drop(fd); - // Open the file and check fd_file_status + // Open the file and check the open-handle status let fd = fs - .open(&ctx, "/fd_stat_test.txt", OFlags::RDONLY, Mode::empty()) + .open(USER, "/fd_stat_test.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open file"); - let status = fs.fd_file_status(&fd).expect("fd_file_status failed"); - assert_eq!(status.file_type, crate::fs::FileType::RegularFile); + let status = fs.handle_status(&fd).expect("handle status failed"); + assert_eq!(status.file_type, FileType::RegularFile); assert_eq!(status.size, 13, "file size should be 13 bytes"); - // Also check fd_file_status on a directory - fs.close(&fd).unwrap(); + // Also check the handle status on a directory + drop(fd); let fd = fs - .open(&ctx, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open(USER, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) .expect("failed to open root dir"); - let status = fs - .fd_file_status(&fd) - .expect("fd_file_status on dir failed"); - assert_eq!(status.file_type, crate::fs::FileType::Directory); - fs.close(&fd).unwrap(); + let status = fs.handle_status(&fd).expect("handle status on dir failed"); + assert_eq!(status.file_type, FileType::Directory); + drop(fd); } #[test] fn test_nine_p_large_read_write() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); // The msize is 65536 and IOHDRSZ is 24, so the max per-message payload // is 65512 bytes. Write data larger than that to verify the client @@ -995,9 +897,9 @@ fn test_nine_p_large_read_write() { .map(|i: usize| u8::try_from(i % 251).unwrap()) .collect(); - let fd = fs + let mut fd = fs .open( - &ctx, + USER, "/large_test.bin", OFlags::CREAT | OFlags::RDWR, Mode::RWXU, @@ -1007,24 +909,26 @@ fn test_nine_p_large_read_write() { // Write in a loop (the client caps each write to msize - IOHDRSZ) let mut written = 0; while written < data.len() { - let n = fs.write(&fd, &data[written..], None).expect("write failed"); + let n = fs + .write(&NoDeviceIo, &mut fd, &data[written..], None) + .expect("write failed"); assert!(n > 0, "write should make progress"); written += n; } assert_eq!(written, data.len()); - fs.close(&fd).unwrap(); + drop(fd); // Read it all back - let fd = fs - .open(&ctx, "/large_test.bin", OFlags::RDONLY, Mode::empty()) + let mut fd = fs + .open(USER, "/large_test.bin", OFlags::RDONLY, Mode::empty()) .expect("failed to open file for reading"); let mut read_buf = alloc::vec![0u8; data_size]; let mut total_read = 0; while total_read < data.len() { let n = fs - .read(&fd, &mut read_buf[total_read..], None) + .read(&NoDeviceIo, &mut fd, &mut read_buf[total_read..], None) .expect("read failed"); if n == 0 { break; @@ -1034,19 +938,17 @@ fn test_nine_p_large_read_write() { assert_eq!(total_read, data.len()); assert_eq!(read_buf, data); - fs.close(&fd).unwrap(); + drop(fd); } #[test] fn test_nine_p_explicit_offset_read_write() { - let ctx = crate::fs::resolver::Context::new(); - let litebox = crate::LiteBox::new(MockPlatform::new()); let server = DiodServer::start(); - let fs = connect_9p(&litebox, &server); + let fs = connect_9p(&server); - let fd = fs + let mut fd = fs .open( - &ctx, + USER, "/offset_test.txt", OFlags::CREAT | OFlags::RDWR, Mode::RWXU, @@ -1054,19 +956,19 @@ fn test_nine_p_explicit_offset_read_write() { .expect("failed to create file"); // Write "AAAAAAAAAA" at offset 0 using implicit offset - fs.write(&fd, b"AAAAAAAAAA", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"AAAAAAAAAA", None).unwrap(); // Write "BBBBB" at explicit offset 5 — should NOT change the fd offset let n = fs - .write(&fd, b"BBBBB", Some(5)) + .write(&NoDeviceIo, &mut fd, b"BBBBB", Some(5)) .expect("explicit offset write failed"); assert_eq!(n, 5); // The fd offset should still be 10 (from the first write), not 10 // Write "C" using implicit offset — should go at offset 10 - fs.write(&fd, b"C", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"C", None).unwrap(); - fs.close(&fd).unwrap(); + drop(fd); // Verify the final file content on host: "AAAAABBBBBC" let host_content = @@ -1074,14 +976,14 @@ fn test_nine_p_explicit_offset_read_write() { assert_eq!(host_content, "AAAAABBBBBC"); // Now test explicit offset reads - let fd = fs - .open(&ctx, "/offset_test.txt", OFlags::RDONLY, Mode::empty()) + let mut fd = fs + .open(USER, "/offset_test.txt", OFlags::RDONLY, Mode::empty()) .expect("failed to open for reading"); // Read 5 bytes at explicit offset 5 → "BBBBB" let mut buf = alloc::vec![0u8; 5]; let n = fs - .read(&fd, &mut buf, Some(5)) + .read(&NoDeviceIo, &mut fd, &mut buf, Some(5)) .expect("explicit offset read failed"); assert_eq!(n, 5); assert_eq!(&buf[..n], b"BBBBB"); @@ -1089,8 +991,10 @@ fn test_nine_p_explicit_offset_read_write() { // fd offset should still be 0 (explicit offset doesn't change it) // Read using implicit offset → should start at 0 let mut buf = alloc::vec![0u8; 11]; - let n = fs.read(&fd, &mut buf, None).expect("implicit read failed"); + let n = fs + .read(&NoDeviceIo, &mut fd, &mut buf, None) + .expect("implicit read failed"); assert_eq!(&buf[..n], b"AAAAABBBBBC"); - fs.close(&fd).unwrap(); + drop(fd); } diff --git a/litebox_broker_core/src/fs/overlay.rs b/litebox_broker_core/src/fs/overlay.rs index 96ff68f93..9e29b669d 100644 --- a/litebox_broker_core/src/fs/overlay.rs +++ b/litebox_broker_core/src/fs/overlay.rs @@ -285,7 +285,7 @@ impl Overlay { if let Ok(upper_status) = self.upper.status(HandleRef::File(&upper)) { self.bind_copy_up( layer, - status.node_info.clone(), + status.node_info, upper_status.node_info, Some(&upper), ); @@ -532,12 +532,11 @@ impl Overlay { node: NodeInfo, ) -> NodeInfo { let rdev = node.rdev; - ids.entry(layer_node(layer, node)) + *ids.entry(layer_node(layer, node)) .or_insert_with(|| NodeInfo { rdev, ..self.alloc.next() }) - .clone() } /// `status` as reported by `layer`, with its node identity replaced by the overlay's own. @@ -560,7 +559,7 @@ impl Overlay { ) { let mut state = self.state.lock(); let id = self.map_node(&mut state.ids, Some(layer), lower); - state.ids.insert(layer_node(None, upper), id.clone()); + state.ids.insert(layer_node(None, upper), id); if let Some(file) = upper_file { state.copied_up.insert(id, file.clone()); } @@ -645,7 +644,7 @@ impl Overlay { entry.lower.get_or_insert(layer); if !entry.upper && entry.lower == Some(layer) { // This layer owns the entry, so its node is the one callers see. - entry.entry.ino_info = lower_node.clone().map(|node| { + entry.entry.ino_info = lower_node.map(|node| { self.map_node(&mut self.state.lock().ids, Some(layer), node) }); } @@ -656,7 +655,7 @@ impl Overlay { entry.lower_directories[layer] = true; // Several layers describe one logical directory; the one already resolved // above owns the identity, and this layer's node adopts it. - if let (Some(node), Some(id)) = (lower_node, entry.entry.ino_info.clone()) { + if let (Some(node), Some(id)) = (lower_node, entry.entry.ino_info) { self.state .lock() .ids diff --git a/litebox_broker_core/src/fs/resolver.rs b/litebox_broker_core/src/fs/resolver.rs index 472746c13..a0f222982 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -623,10 +623,13 @@ impl Resolver 0, SeekBehavior::PositionBased if entry.append_mode && offset.is_none() => { - self.backend - .status(HandleRef::File(file)) - .map_err(|_| WriteError::Io)? - .size + usize::try_from( + self.backend + .status(HandleRef::File(file)) + .map_err(|_| WriteError::Io)? + .size, + ) + .map_err(|_| WriteError::Io)? } SeekBehavior::PositionBased => offset.unwrap_or(entry.position), }; @@ -696,6 +699,7 @@ impl Resolver 0, SeekWhence::RelativeToCurrentOffset => entry.position, diff --git a/litebox_broker_core/src/fs/service.rs b/litebox_broker_core/src/fs/service.rs index bf251d763..c526a8690 100644 --- a/litebox_broker_core/src/fs/service.rs +++ b/litebox_broker_core/src/fs/service.rs @@ -8,20 +8,19 @@ use core::any::Any; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, - FileSeekWhence, FileStatus as ProtocolFileStatus, FileType as ProtocolFileType, FileUser, - MAX_FILE_TRANSFER_SIZE, + FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileOpenFlags, FileSeekWhence, + FileStatus, FileUser, MAX_FILE_TRANSFER_SIZE, }; use litebox_broker_protocol::stdio::{MAX_STDIO_TRANSFER_SIZE, StdioOutputStream}; use litebox_platform::sync::{RawSyncPrimitivesProvider, RwLock}; +use super::OFlags; use super::backend::DeviceIo; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; use super::resolver::{Resolver, ResolverEntry}; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, SeekWhence, UserInfo}; use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result}; @@ -47,7 +46,7 @@ impl File { mod private { use super::{ BrokerError, BrokerSession, File, FileAccessMode, FileDirectoryEntry, FileMode, - FileOpenFlags, FileSeekWhence, FileUser, ProtocolFileStatus, ServiceResult, Vec, + FileOpenFlags, FileSeekWhence, FileStatus, FileUser, ServiceResult, Vec, }; pub trait Service: Send + Sync { @@ -115,7 +114,7 @@ mod private { &self, _session: &BrokerSession, _file: &File, - ) -> ServiceResult { + ) -> ServiceResult { Err(BrokerError::UnsupportedOperation) } @@ -124,7 +123,7 @@ mod private { _session: &BrokerSession, _path: &str, _user: FileUser, - ) -> ServiceResult { + ) -> ServiceResult { Err(BrokerError::UnsupportedOperation) } @@ -208,7 +207,7 @@ where mode: FileMode, ) -> ServiceResult { let flags = open_flags(access, flags)?; - let entry = match Resolver::open(self, user_info(user), path, flags, file_mode(mode)) { + let entry = match Resolver::open(self, user, path, flags, mode & FileMode::SUPPORTED) { Ok(entry) => entry, Err(error) => return Ok(Err(file_open_error(error))), }; @@ -295,7 +294,6 @@ where let Ok(offset) = isize::try_from(offset) else { return Ok(Err(FileError::InvalidOffset)); }; - let whence = seek_whence(whence)?; let state = file.state::>>()?; let entry = state.read(); let seek = if entry.uses_position() { @@ -364,26 +362,19 @@ where Ok(entries) => entries, Err(error) => return Ok(Err(file_read_directory_error(error))), }; - let entries = entries - .into_iter() - .map(directory_entry) - .collect::>>()?; Ok(Ok(entries)) } - fn handle_status( - &self, - _session: &BrokerSession, - file: &File, - ) -> ServiceResult { + fn handle_status(&self, _session: &BrokerSession, file: &File) -> ServiceResult { let entry = file .state::>>()? .read(); - let status = match Resolver::handle_status(self, &entry) { + let mut status = match Resolver::handle_status(self, &entry) { Ok(status) => status, Err(error) => return Ok(Err(file_status_error(error))), }; - Ok(Ok(file_status(status)?)) + mask_status_mode(&mut status); + Ok(Ok(status)) } fn path_status( @@ -391,12 +382,13 @@ where _session: &BrokerSession, path: &str, user: FileUser, - ) -> ServiceResult { - let status = match Resolver::file_status(self, user_info(user), path) { + ) -> ServiceResult { + let mut status = match Resolver::file_status(self, user, path) { Ok(status) => status, Err(error) => return Ok(Err(file_status_error(error))), }; - Ok(Ok(file_status(status)?)) + mask_status_mode(&mut status); + Ok(Ok(status)) } fn chmod( @@ -406,7 +398,7 @@ where user: FileUser, mode: FileMode, ) -> ServiceResult<()> { - Ok(Resolver::chmod(self, user_info(user), path, file_mode(mode)).map_err(file_chmod_error)) + Ok(Resolver::chmod(self, user, path, mode & FileMode::SUPPORTED).map_err(file_chmod_error)) } fn chown( @@ -417,14 +409,11 @@ where user: Option, group: Option, ) -> ServiceResult<()> { - Ok( - Resolver::chown(self, user_info(acting_user), path, user, group) - .map_err(file_chown_error), - ) + Ok(Resolver::chown(self, acting_user, path, user, group).map_err(file_chown_error)) } fn unlink(&self, _session: &BrokerSession, path: &str, user: FileUser) -> ServiceResult<()> { - Ok(Resolver::unlink(self, user_info(user), path).map_err(file_unlink_error)) + Ok(Resolver::unlink(self, user, path).map_err(file_unlink_error)) } fn mkdir( @@ -434,11 +423,11 @@ where user: FileUser, mode: FileMode, ) -> ServiceResult<()> { - Ok(Resolver::mkdir(self, user_info(user), path, file_mode(mode)).map_err(file_mkdir_error)) + Ok(Resolver::mkdir(self, user, path, mode & FileMode::SUPPORTED).map_err(file_mkdir_error)) } fn rmdir(&self, _session: &BrokerSession, path: &str, user: FileUser) -> ServiceResult<()> { - Ok(Resolver::rmdir(self, user_info(user), path).map_err(file_rmdir_error)) + Ok(Resolver::rmdir(self, user, path).map_err(file_rmdir_error)) } } @@ -533,7 +522,7 @@ pub fn read_directory( pub fn handle_status( session: &BrokerSession, handle: ObjectHandle, -) -> Result> { +) -> Result> { let file = file_with_any_rights(session, handle, ObjectRights::WAIT | ObjectRights::WRITE)?; session.core.fs.handle_status(session, &file) } @@ -543,7 +532,7 @@ pub fn path_status( session: &BrokerSession, path: &str, user: FileUser, -) -> Result> { +) -> Result> { authorize(session, ObjectRights::WAIT)?; if let Err(error) = validate_path(path) { return Ok(Err(error)); @@ -714,13 +703,6 @@ impl DeviceIo for SessionDeviceIo<'_> { } } -const fn user_info(user: FileUser) -> UserInfo { - UserInfo { - user: user.user, - group: user.group, - } -} - fn open_flags(access: FileAccessMode, flags: FileOpenFlags) -> Result { let mut output = match access { FileAccessMode::ReadOnly => OFlags::RDONLY, @@ -747,63 +729,12 @@ fn open_flags(access: FileAccessMode, flags: FileOpenFlags) -> Result { Ok(output) } -fn file_mode(mode: FileMode) -> Mode { - Mode::from_bits_retain(u32::from(mode.bits())) -} - -fn seek_whence(whence: FileSeekWhence) -> Result { - Ok(match whence { - FileSeekWhence::Beginning => SeekWhence::RelativeToBeginning, - FileSeekWhence::Current => SeekWhence::RelativeToCurrentOffset, - FileSeekWhence::End => SeekWhence::RelativeToEnd, - _ => return Err(BrokerError::UnsupportedOperation), - }) -} - -fn file_status(status: FileStatus) -> Result { - let mode_bits = u16::try_from(status.mode.bits() & u32::from(FileMode::SUPPORTED.bits())) - .map_err(|_| BrokerError::Internal)?; - let mode = FileMode::from_bits(mode_bits).ok_or(BrokerError::Internal)?; - Ok(ProtocolFileStatus { - file_type: file_type(status.file_type), - mode, - size: u64::try_from(status.size).map_err(|_| BrokerError::Internal)?, - owner: FileUser { - user: status.owner.user, - group: status.owner.group, - }, - node_info: node_info(status.node_info)?, - block_size: u64::try_from(status.blksize).map_err(|_| BrokerError::Internal)?, - }) -} - -fn directory_entry(entry: DirEntry) -> Result { - Ok(FileDirectoryEntry { - name: entry.name, - file_type: file_type(entry.file_type), - node_info: entry.ino_info.map(node_info).transpose()?, - }) -} - -const fn file_type(file_type: FileType) -> ProtocolFileType { - match file_type { - FileType::RegularFile => ProtocolFileType::RegularFile, - FileType::Directory => ProtocolFileType::Directory, - FileType::CharacterDevice => ProtocolFileType::CharacterDevice, - } -} - -fn node_info(node_info: NodeInfo) -> Result { - Ok(FileNodeInfo { - dev: u64::try_from(node_info.dev).map_err(|_| BrokerError::Internal)?, - ino: u64::try_from(node_info.ino).map_err(|_| BrokerError::Internal)?, - rdev: node_info - .rdev - .map(|rdev| u64::try_from(rdev.get()).map_err(|_| BrokerError::Internal)) - .transpose()?, - }) +fn mask_status_mode(status: &mut FileStatus) { + status.mode &= FileMode::SUPPORTED; } +// TODO: Define canonical per-operation protocol errors so these engine-to-protocol conversions can +// be removed while retaining operation-specific error sets. fn file_path_error(error: PathError) -> FileError { match error { PathError::NoSuchFileOrDirectory => FileError::NoSuchFileOrDirectory, @@ -927,23 +858,28 @@ fn file_status_error(error: FileStatusError) -> FileError { #[cfg(test)] mod tests { use super::*; + use core::num::NonZeroU64; + use litebox_broker_protocol::fs::{FileNodeInfo, FileType}; #[test] fn file_status_excludes_object_type_mode_bits() { - let status = file_status(FileStatus { + let mut status = FileStatus { file_type: FileType::RegularFile, - mode: Mode::from_bits_retain(0o100644), - size: 1, - owner: UserInfo::ROOT, - node_info: NodeInfo { - dev: 2, - ino: 3, - rdev: None, + mode: FileMode::from_bits_retain(0o100644), + size: u64::MAX, + owner: FileUser::ROOT, + node_info: FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX - 1, + rdev: NonZeroU64::new(u64::MAX), }, - blksize: 4096, - }) - .unwrap(); + blksize: u64::MAX, + }; + let mut expected = status; + expected.mode = FileMode::from_bits(0o644).unwrap(); + + mask_status_mode(&mut status); - assert_eq!(status.mode, FileMode::from_bits(0o644).unwrap()); + assert_eq!(status, expected); } } diff --git a/litebox_broker_core/src/fs/tar_ro.rs b/litebox_broker_core/src/fs/tar_ro.rs index 4450e63b6..95aef5cce 100644 --- a/litebox_broker_core/src/fs/tar_ro.rs +++ b/litebox_broker_core/src/fs/tar_ro.rs @@ -43,7 +43,7 @@ use super::{ /// Block size for file system I/O operations // TODO(jayb): Determine appropriate block size -const BLOCK_SIZE: usize = 0; +const BLOCK_SIZE: u64 = 0; /// A [`super::backend::Backend`] that stores all files in-memory, via a read-only `.tar` file. pub struct TarRo { @@ -185,14 +185,12 @@ impl super::backend::Backend for TarRo { .iter() .map(|(name, child)| { let (file_type, node_info) = match *child { - IndexedChild::File(idx) => ( - FileType::RegularFile, - self.tar_index.files[idx].node_info.clone(), - ), - IndexedChild::Dir(idx) => ( - FileType::Directory, - self.tar_index.dirs[idx].node_info.clone(), - ), + IndexedChild::File(idx) => { + (FileType::RegularFile, self.tar_index.files[idx].node_info) + } + IndexedChild::Dir(idx) => { + (FileType::Directory, self.tar_index.dirs[idx].node_info) + } }; DirEntry { name: name.clone(), @@ -247,9 +245,10 @@ impl super::backend::Backend for TarRo { Ok(super::FileStatus { file_type: FileType::RegularFile, mode: file.mode, - size: file.data_range.len(), + size: u64::try_from(file.data_range.len()) + .map_err(|_| super::errors::FileStatusError::Io)?, owner: file.owner, - node_info: file.node_info.clone(), + node_info: file.node_info, blksize: BLOCK_SIZE, }) } @@ -260,7 +259,7 @@ impl super::backend::Backend for TarRo { mode: DEFAULT_DIR_MODE, size: super::DEFAULT_DIRECTORY_SIZE, owner: dir.owner.unwrap_or(DEFAULT_DIRECTORY_OWNER), - node_info: dir.node_info.clone(), + node_info: dir.node_info, blksize: BLOCK_SIZE, }) } diff --git a/litebox/src/fs/test.tar b/litebox_broker_core/src/fs/test.tar similarity index 100% rename from litebox/src/fs/test.tar rename to litebox_broker_core/src/fs/test.tar diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs new file mode 100644 index 000000000..dfdfa1401 --- /dev/null +++ b/litebox_broker_core/src/fs/tests.rs @@ -0,0 +1,1986 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Filesystem semantics for the broker-core resolver and its backends. +//! +//! These tests drive the resolver and the backends directly. There is deliberately no broker +//! session, transport, or guest descriptor table involved: the semantics under test are owned by +//! broker core. + +use alloc::borrow::Cow; + +use litebox_broker_protocol::stdio::StdioOutputStream; + +use super::backend::{DeviceIo, NoDeviceIo}; +use super::errors::{ReadError, WriteError}; +use super::in_mem::InMem; +use super::inode_allocator::InodeAllocator; +use super::overlay::Overlay; +use super::resolver::{Resolver, ResolverEntry}; +use super::tar_ro::TarRo; +use super::{FileType, Mode, OFlags, SeekWhence, UserInfo}; +use crate::test_platform::TestPlatform; + +const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); +const USER: UserInfo = UserInfo { + user: 1000, + group: 1000, +}; +const ROOT: UserInfo = UserInfo::ROOT; + +struct UnservicedStdio; + +impl DeviceIo for UnservicedStdio { + fn read_stdin(&self, output: &mut [u8]) -> Result { + if output.is_empty() { + return Ok(0); + } + Err(ReadError::Io) + } + + fn write_stdio(&self, _stream: StdioOutputStream, input: &[u8]) -> Result { + if input.is_empty() { + return Ok(0); + } + Err(WriteError::Io) + } + + fn fill_random(&self, _output: &mut [u8]) -> Result<(), ReadError> { + Err(ReadError::Io) + } +} + +fn in_mem_fs() -> Resolver> { + Resolver::new(InMem::::new(InodeAllocator::standalone())) +} + +fn tar_ro_fs(tar_data: Cow<'static, [u8]>) -> Resolver { + Resolver::new(TarRo::new(tar_data, InodeAllocator::standalone())) +} + +/// An overlay of `upper` over a tar-backed lower layer. +fn overlay_fs( + upper: InMem, + tar_data: Cow<'static, [u8]>, +) -> Resolver> { + Resolver::new(Overlay::::new( + upper, + TarRo::new(tar_data, InodeAllocator::standalone()), + InodeAllocator::standalone(), + )) +} + +mod in_mem { + use super::{ + FileType, InMem, Mode, NoDeviceIo, OFlags, ROOT, Resolver, ResolverEntry, SeekWhence, + TestPlatform, USER, UserInfo, in_mem_fs, + }; + use crate::fs::errors::{ + ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, + UnlinkError, + }; + use alloc::vec; + use alloc::vec::Vec; + + type InMemFs = Resolver>; + type InMemEntry = ResolverEntry>; + + /// Create `/tmp` as root, so that the unprivileged user can create entries in it. + fn world_writable_tmp(fs: &InMemFs) { + fs.mkdir(ROOT, "/tmp", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to create /tmp"); + } + + /// Make the root directory world-writable, so tests can create entries directly in it. + fn world_writable_root(fs: &InMemFs) { + fs.chmod(ROOT, "/", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to chmod /"); + } + + #[test] + fn root_file_creation_and_deletion() { + let fs = in_mem_fs(); + + // Test file creation + let path = "/testfile"; + let fd = fs + .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + drop(fd); + + // Test file deletion + fs.unlink(ROOT, path).expect("Failed to unlink file"); + assert!( + fs.open(ROOT, path, OFlags::RDONLY, Mode::RWXU).is_err(), + "File should not exist" + ); + } + + #[test] + fn root_file_read_write() { + let fs = in_mem_fs(); + + // Create and write to a file + let path = "/testfile"; + let mut fd = fs + .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + let data = b"Hello, world!"; + fs.write(&NoDeviceIo, &mut fd, data, None) + .expect("Failed to write to file"); + drop(fd); + + // Read from the file + let mut fd = fs + .open(ROOT, path, OFlags::RDONLY, Mode::RWXU) + .expect("Failed to open file"); + let mut buffer = vec![0; data.len()]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(bytes_read, data.len()); + assert_eq!(&buffer, data); + } + + #[test] + fn write_only_open_does_not_require_read_permission() { + let fs = in_mem_fs(); + world_writable_tmp(&fs); + + let path = "/tmp/write_only"; + let mut fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) + .expect("Failed to create write-only file"); + fs.write(&NoDeviceIo, &mut fd, b"x", None) + .expect("Failed to write file"); + + let mut buffer = [0]; + assert!(matches!( + fs.read(&NoDeviceIo, &mut fd, &mut buffer, None), + Err(ReadError::NotForReading) + )); + drop(fd); + + assert!(matches!( + fs.open(USER, path, OFlags::RDONLY, Mode::empty()), + Err(OpenError::AccessNotAllowed) + )); + } + + #[test] + fn newly_created_file_does_not_require_its_own_permissions() { + let fs = in_mem_fs(); + world_writable_tmp(&fs); + + let path = "/tmp/zero_mode"; + let mut fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) + .expect("Failed to create zero-mode file"); + fs.write(&NoDeviceIo, &mut fd, b"x", None) + .expect("Failed to write file"); + drop(fd); + + let status = fs.file_status(USER, path).expect("Failed to stat file"); + assert_eq!(status.mode, Mode::empty()); + assert!(matches!( + fs.open(USER, path, OFlags::WRONLY, Mode::empty()), + Err(OpenError::AccessNotAllowed) + )); + } + + #[test] + fn root_directory_creation_and_removal() { + let fs = in_mem_fs(); + + // Test directory creation + let path = "/testdir"; + fs.mkdir(ROOT, path, Mode::RWXU) + .expect("Failed to create directory"); + + // Test directory removal + fs.rmdir(ROOT, path).expect("Failed to remove directory"); + assert!( + fs.open(ROOT, path, OFlags::RDONLY, Mode::RWXU).is_err(), + "Directory should not exist" + ); + } + + #[test] + fn file_creation_and_deletion() { + let fs = in_mem_fs(); + world_writable_tmp(&fs); + + // Test file creation + let path = "/tmp/testfile"; + let fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + drop(fd); + + // Test file deletion + fs.unlink(USER, path).expect("Failed to unlink file"); + assert!( + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), + "File should not exist" + ); + } + + #[test] + fn file_read_write() { + let fs = in_mem_fs(); + world_writable_tmp(&fs); + + // Create and write to a file + let path = "/tmp/testfile"; + let mut fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + let data = b"Hello, world!"; + fs.write(&NoDeviceIo, &mut fd, data, None) + .expect("Failed to write to file"); + fs.write(&NoDeviceIo, &mut fd, &data[2..], Some(2)) + .expect("Failed to write to file with offset"); + drop(fd); + + // Read from the file + let mut fd = fs + .open(USER, path, OFlags::RDONLY, Mode::RWXU) + .expect("Failed to open file"); + let mut buffer = vec![0; data.len()]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + let bytes_read2 = fs + .read(&NoDeviceIo, &mut fd, &mut buffer[2..], Some(2)) + .expect("Failed to read from file with offset"); + assert_eq!(bytes_read, data.len()); + assert_eq!(bytes_read2, data.len() - 2); + assert_eq!(&buffer, data); + } + + #[test] + fn directory_creation_and_removal() { + let fs = in_mem_fs(); + world_writable_tmp(&fs); + + // Test directory creation + let path = "/tmp/testdir"; + fs.mkdir(USER, path, Mode::RWXU) + .expect("Failed to create directory"); + + // Test directory removal + fs.rmdir(USER, path).expect("Failed to remove directory"); + assert!( + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), + "Directory should not exist" + ); + } + + #[test] + fn read_dir_empty() { + let fs = in_mem_fs(); + + let fd = fs + .open(ROOT, "/", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open root directory"); + let entries = fs + .read_dir(&fd) + .expect("Failed to read directory") + .iter() + .map(|e| e.name.clone()) + .collect::>(); + assert_eq!( + entries, + vec![".", ".."], + "Root directory should contain . and .." + ); + } + + #[test] + fn read_dir_with_files_and_dirs() { + let fs = in_mem_fs(); + + // Create a directory structure + fs.mkdir(ROOT, "/testdir", Mode::RWXU) + .expect("Failed to create directory"); + let fd1 = fs + .open( + ROOT, + "/testfile1", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file1"); + drop(fd1); + let fd2 = fs + .open( + ROOT, + "/testfile2", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file2"); + drop(fd2); + + // Read root directory + let fd = fs + .open(ROOT, "/", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open root directory"); + let entries = fs.read_dir(&fd).expect("Failed to read directory"); + drop(fd); + + // Should have 5 entries: ., .., testdir, testfile1, testfile2 + assert_eq!(entries.len(), 5); + + let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); + names.sort_unstable(); + assert_eq!(names, vec![".", "..", "testdir", "testfile1", "testfile2"]); + + // Check file types + for entry in &entries { + match entry.name.as_str() { + "testdir" | "." | ".." => { + assert_eq!(entry.file_type, FileType::Directory); + } + "testfile1" | "testfile2" => { + assert_eq!(entry.file_type, FileType::RegularFile); + } + _ => panic!("Unexpected entry: {}", entry.name), + } + if entry.name != "." && entry.name != ".." { + assert!(entry.ino_info.is_some(), "Inode info should be present"); + } else { + // TODO(jayb): Re-enable this assertion once the resolver fills in + // inode information for the synthesized `.` and `..` entries. + } + } + + // Read the subdirectory (should be empty) + let fd = fs + .open(ROOT, "/testdir", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open subdirectory"); + let entries = fs + .read_dir(&fd) + .expect("Failed to read subdirectory") + .iter() + .map(|e| e.name.clone()) + .collect::>(); + assert!(entries.len() == 2, "Subdirectory should contain . and .."); + } + + #[test] + fn read_dir_file_not_directory() { + let fs = in_mem_fs(); + + // Create a file + let fd = fs + .open( + ROOT, + "/testfile", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file"); + drop(fd); + + // Try to read_dir on the file (should fail) + let fd = fs + .open(ROOT, "/testfile", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file"); + assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); + } + + #[test] + fn parent_dir_write_permissions_are_enforced() { + let fs = in_mem_fs(); + + // A root-owned 0755 directory, holding a file and a directory to try to remove. + fs.mkdir( + ROOT, + "/rootdir", + Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, + ) + .expect("Failed to create directory"); + let fd = fs + .open( + ROOT, + "/rootdir/file", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file"); + drop(fd); + fs.mkdir(ROOT, "/rootdir/sub", Mode::RWXU) + .expect("Failed to create subdirectory"); + + // A world-writable directory, for the positive case. + fs.mkdir(ROOT, "/opendir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to create directory"); + + assert!(matches!( + fs.open( + USER, + "/rootdir/new", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU + ), + Err(OpenError::NoWritePerms) + )); + assert!(matches!( + fs.mkdir(USER, "/rootdir/newdir", Mode::RWXU), + Err(MkdirError::NoWritePerms) + )); + assert!(matches!( + fs.unlink(USER, "/rootdir/file"), + Err(UnlinkError::NoWritePerms) + )); + assert!(matches!( + fs.rmdir(USER, "/rootdir/sub"), + Err(RmdirError::NoWritePerms) + )); + + // The same operations succeed in a directory the user may write. + let fd = fs + .open( + USER, + "/opendir/new", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file"); + drop(fd); + fs.mkdir(USER, "/opendir/newdir", Mode::RWXU) + .expect("Failed to create directory"); + fs.unlink(USER, "/opendir/new") + .expect("Failed to unlink file"); + fs.rmdir(USER, "/opendir/newdir") + .expect("Failed to remove directory"); + } + + #[test] + fn chown_test() { + let fs = in_mem_fs(); + + // Create a test file as root + let path = "/testfile"; + let fd = fs + .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + drop(fd); + + // First chown to 1000:1000 as root (should succeed) + fs.chown(ROOT, path, Some(1000), Some(1000)) + .expect("Failed to chown as root"); + + // The owner may chown (should succeed) + fs.chown(USER, path, Some(123), Some(456)) + .expect("Failed to chown as owner"); + + // A different user may not chown (should fail) + let other = UserInfo { + user: 500, + group: 500, + }; + match fs.chown(other, path, Some(789), Some(101)) { + Err(ChownError::NotTheOwner) => { + // Expected behavior + } + Ok(()) => panic!("Non-owner should not be able to chown"), + Err(e) => panic!("Unexpected error: {e:?}"), + } + + // Test chown on non-existent file (should fail) + match fs.chown(USER, "/nonexistent", Some(123), Some(456)) { + Err(ChownError::PathError(PathError::NoSuchFileOrDirectory)) => { + // Expected behavior + } + Ok(()) => panic!("Should not be able to chown non-existent file"), + Err(e) => panic!("Unexpected error: {e:?}"), + } + + // Test partial chown (change only user, leave group unchanged) + fs.chown(ROOT, path, Some(999), None) + .expect("Failed to chown user only"); + + // Test partial chown (change only group, leave user unchanged) + fs.chown(ROOT, path, None, Some(888)) + .expect("Failed to chown group only"); + } + + #[test] + fn o_directory_flag_tests() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + // Create test directory and file + fs.mkdir(USER, "/testdir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to create directory"); + + let fd = fs + .open( + USER, + "/testfile", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file"); + drop(fd); + + // Test O_DIRECTORY on a directory (should succeed) + let fd = fs + .open( + USER, + "/testdir", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) + .expect("Failed to open directory with O_DIRECTORY"); + drop(fd); + + // Test O_DIRECTORY on a regular file (should fail) + assert!(matches!( + fs.open( + USER, + "/testfile", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::ComponentNotADirectory)) + )); + + // Test O_DIRECTORY on non-existent path (should fail) + assert!(matches!( + fs.open( + USER, + "/nonexistent", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) + )); + + // Test O_DIRECTORY with O_CREAT on non-existent path + // According to the implementation, O_DIRECTORY should be ignored when O_CREAT is specified + let fd = fs + .open( + USER, + "/newfile", + OFlags::CREAT | OFlags::WRONLY | OFlags::DIRECTORY, + Mode::RWXU, + ) + .expect("Failed to create file with O_CREAT | O_DIRECTORY"); + drop(fd); + + // Verify it created a regular file, not a directory + let stat = fs + .file_status(USER, "/newfile") + .expect("Failed to get file status"); + assert_eq!(stat.file_type, FileType::RegularFile); + + // TODO(jayb): Restore coverage of `O_RDWR | O_DIRECTORY` once `OpenError` can report + // `EISDIR`; see the matching TODO in `InMem::owned_dir_at`. The legacy in-memory file + // system used to accept such an open, which Linux rejects. + } + + #[test] + fn o_excl_flag_tests() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + // Test O_CREAT | O_EXCL on non-existent file (should succeed) + let mut fd = fs + .open( + USER, + "/newfile", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create new file with O_CREAT | O_EXCL"); + + // Write some data to verify file was created + fs.write(&NoDeviceIo, &mut fd, b"test data", None) + .expect("Failed to write to new file"); + drop(fd); + + // Test O_CREAT | O_EXCL on existing file (should fail) + assert!(matches!( + fs.open( + USER, + "/newfile", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ), + Err(OpenError::AlreadyExists) + )); + + // Test O_EXCL without O_CREAT (should be ignored and succeed) + let mut fd = fs + .open( + USER, + "/newfile", + OFlags::EXCL | OFlags::RDONLY, + Mode::empty(), + ) + .expect("Failed to open existing file with O_EXCL (without O_CREAT)"); + + // Verify we can read the data + let mut buffer = vec![0; 9]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"test data"); + drop(fd); + + // Test O_CREAT without O_EXCL on existing file (should succeed) + let fd = fs + .open(USER, "/newfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to open existing file with O_CREAT (without O_EXCL)"); + drop(fd); + + // Test O_CREAT | O_EXCL on directory (should fail) + fs.mkdir(USER, "/testdir", Mode::RWXU) + .expect("Failed to create directory"); + assert!(matches!( + fs.open( + USER, + "/testdir", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ), + Err(OpenError::AlreadyExists) + )); + } + + #[test] + fn open_with_trunc() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + // Create a file and write some initial content + let path = "/testfile"; + let mut fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + let initial_data = b"Hello, world! This is initial content."; + fs.write(&NoDeviceIo, &mut fd, initial_data, None) + .expect("Failed to write initial content"); + drop(fd); + + // Verify initial content was written + let mut fd = fs + .open(USER, path, OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file for reading"); + let mut buffer = vec![0; initial_data.len()]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read initial content"); + assert_eq!(bytes_read, initial_data.len()); + assert_eq!(&buffer, initial_data); + drop(fd); + + // Test O_TRUNC with O_WRONLY - should truncate file + let mut fd = fs + .open(USER, path, OFlags::WRONLY | OFlags::TRUNC, Mode::empty()) + .expect("Failed to open file with O_TRUNC | O_WRONLY"); + + // Write new content to the truncated file + let new_data = b"New content"; + fs.write(&NoDeviceIo, &mut fd, new_data, None) + .expect("Failed to write new content"); + drop(fd); + + // Verify the file was truncated and contains only new content + let mut fd = fs + .open(USER, path, OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file for verification"); + let mut buffer = vec![0; initial_data.len()]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read after truncation"); + assert_eq!(bytes_read, new_data.len()); + assert_eq!(&buffer[..bytes_read], new_data); + drop(fd); + + // Test O_TRUNC with O_RDWR - should also truncate + let mut fd = fs + .open(USER, path, OFlags::WRONLY, Mode::empty()) + .expect("Failed to open file for writing"); + fs.write(&NoDeviceIo, &mut fd, b"More content to truncate", None) + .expect("Failed to write more content"); + drop(fd); + + let mut fd = fs + .open(USER, path, OFlags::RDWR | OFlags::TRUNC, Mode::empty()) + .expect("Failed to open file with O_TRUNC | O_RDWR"); + + // File should be empty after truncation + let mut buffer = vec![0; 100]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from truncated file"); + assert_eq!(bytes_read, 0); + + // Write and read back to verify it works + let test_data = b"After RDWR truncation"; + fs.write(&NoDeviceIo, &mut fd, test_data, None) + .expect("Failed to write after RDWR truncation"); + + fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) + .expect("Failed to seek to beginning"); + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read after write"); + assert_eq!(bytes_read, test_data.len()); + assert_eq!(&buffer[..bytes_read], test_data); + } + + #[test] + fn write_position_after_seek() { + let fs = in_mem_fs(); + // Allow regular user to create in root for this focused test + world_writable_root(&fs); + + let mut fd = fs + .open( + USER, + "/posfile", + OFlags::CREAT | OFlags::RDWR, + Mode::RWXU | Mode::RWXG | Mode::RWXO, + ) + .expect("open failed"); + + // 1. First positional write; position should advance by 6. + fs.write(&NoDeviceIo, &mut fd, b"abcdef", None) + .expect("first write failed"); + + // 2. Rewind to beginning. + fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) + .expect("seek failed"); + + // 3. Another positional write should write from start + fs.write(&NoDeviceIo, &mut fd, b"X", None) + .expect("overwrite failed"); + + // The file offset should now be at 1. + assert_eq!( + fs.seek(&mut fd, 0, SeekWhence::RelativeToCurrentOffset) + .expect("seek failed"), + 1 + ); + + // Read back whole file to verify content and length. + fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) + .expect("seek failed"); + let mut buf = [0u8; 16]; + let n = fs + .read(&NoDeviceIo, &mut fd, &mut buf, None) + .expect("read failed"); + assert_eq!(n, 6, "file length should be 6 after writes"); + assert_eq!(&buf[..n], b"Xbcdef", "file content mismatch"); + + // Extra: another append to verify continued correct advancement. + fs.write(&NoDeviceIo, &mut fd, b"12", None) + .expect("second append failed"); + fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) + .expect("seek 2 failed"); + let mut buf2 = [0u8; 16]; + let n2 = fs + .read(&NoDeviceIo, &mut fd, &mut buf2, None) + .expect("read 2 failed"); + assert_eq!(n2, 8); + assert_eq!(&buf2[..n2], b"Xbcdef12"); + } + + /// Create `path` holding `data`, as the unprivileged user. + fn create_with_content(fs: &InMemFs, path: &str, data: &[u8]) { + let mut fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + fs.write(&NoDeviceIo, &mut fd, data, None) + .expect("Failed to write initial content"); + } + + /// Read the whole of `fd` from its current position. + fn read_all(fs: &InMemFs, fd: &mut InMemEntry) -> Vec { + let mut buffer = vec![0; 64]; + let bytes_read = fs + .read(&NoDeviceIo, fd, &mut buffer, None) + .expect("Failed to read from file"); + buffer.truncate(bytes_read); + buffer + } + + #[test] + fn o_append_flag_basic() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + // Create a file and write some initial content + let path = "/testfile"; + create_with_content(&fs, path, b"Hello"); + + // Re-open with O_APPEND and write more data + let mut fd = fs + .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .expect("Failed to open file with O_APPEND"); + fs.write(&NoDeviceIo, &mut fd, b" World", None) + .expect("Failed to append data"); + drop(fd); + + // Verify the file contains both pieces of data concatenated + let mut fd = fs + .open(USER, path, OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file for reading"); + assert_eq!(read_all(&fs, &mut fd), b"Hello World"); + } + + #[test] + fn o_append_flag_seek_ignored_for_write() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + // Create a file and write some initial content + let path = "/testfile"; + create_with_content(&fs, path, b"ABCDEF"); + + // Re-open with O_APPEND + let mut fd = fs + .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .expect("Failed to open file with O_APPEND"); + + // Seek to beginning - this should succeed but writes should still append + fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) + .expect("Failed to seek to beginning"); + + // Write some data - it should go to the end despite the seek + fs.write(&NoDeviceIo, &mut fd, b"123", None) + .expect("Failed to write after seek"); + drop(fd); + + // Verify the file content: original data followed by appended data + let mut fd = fs + .open(USER, path, OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file for reading"); + assert_eq!(read_all(&fs, &mut fd), b"ABCDEF123"); + } + + #[test] + fn o_append_flag_with_rdwr() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + // Create a file with initial content + let path = "/testfile"; + create_with_content(&fs, path, b"Hello"); + + // Re-open with O_RDWR | O_APPEND + let mut fd = fs + .open(USER, path, OFlags::RDWR | OFlags::APPEND, Mode::empty()) + .expect("Failed to open file with O_RDWR | O_APPEND"); + + // Read should work normally from the beginning + assert_eq!(read_all(&fs, &mut fd), b"Hello"); + + // Seek to beginning - write should still append despite position being at 0 + fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) + .expect("Seek failed"); + + // Write should append to end, ignoring the current position + fs.write(&NoDeviceIo, &mut fd, b" World", None) + .expect("Failed to write with append"); + + // Seek to beginning and read the whole file + fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) + .expect("Seek failed"); + assert_eq!(read_all(&fs, &mut fd), b"Hello World"); + } + + #[test] + fn o_append_pwrite_ignores_append_mode() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + // Create a file with initial content + let path = "/testfile"; + create_with_content(&fs, path, b"ABCDEF"); + + // Re-open with O_APPEND + let mut fd = fs + .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .expect("Failed to open file with O_APPEND"); + + // pwrite (write with explicit offset) should ignore O_APPEND per POSIX + fs.write(&NoDeviceIo, &mut fd, b"XX", Some(2)) + .expect("Failed to pwrite"); + drop(fd); + + // Verify the file content: XX should be at position 2, not appended + let mut fd = fs + .open(USER, path, OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file for reading"); + assert_eq!(read_all(&fs, &mut fd), b"ABXXEF"); + } + + #[test] + fn o_append_with_trunc() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + // Create a file with initial content + let path = "/testfile"; + create_with_content(&fs, path, b"Original content"); + + // Re-open with O_TRUNC | O_APPEND + let mut fd = fs + .open( + USER, + path, + OFlags::WRONLY | OFlags::TRUNC | OFlags::APPEND, + Mode::empty(), + ) + .expect("Failed to open file with O_TRUNC | O_APPEND"); + + // File should be truncated, then write should append (to empty file) + fs.write(&NoDeviceIo, &mut fd, b"New", None) + .expect("Failed to write after truncation"); + fs.write(&NoDeviceIo, &mut fd, b"Content", None) + .expect("Failed to write second chunk"); + drop(fd); + + // Verify the file content + let mut fd = fs + .open(USER, path, OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file for reading"); + assert_eq!(read_all(&fs, &mut fd), b"NewContent"); + } +} + +mod tar_ro { + use super::{FileType, Mode, NoDeviceIo, OFlags, TEST_TAR_FILE, USER, tar_ro_fs}; + use crate::fs::errors::{OpenError, PathError, ReadDirError}; + use alloc::vec; + use alloc::vec::Vec; + + #[test] + fn file_read() { + let fs = tar_ro_fs(TEST_TAR_FILE.into()); + let mut fd = fs + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .expect("Failed to open file"); + let mut buffer = vec![0; 1024]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"testfoo\n"); + drop(fd); + + let mut fd = fs + .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file"); + let mut buffer = vec![0; 1024]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"test bar baz\n"); + } + + #[test] + fn dir_and_nonexist_checks() { + let fs = tar_ro_fs(TEST_TAR_FILE.into()); + assert!(matches!( + fs.open(USER, "bar/ba", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), + )); + fs.open(USER, "bar", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open dir"); + } + + #[test] + fn o_directory_flag_tests() { + let fs = tar_ro_fs(TEST_TAR_FILE.into()); + + // Test O_DIRECTORY on a directory (should succeed) + fs.open( + USER, + "bar", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) + .expect("Failed to open directory with O_DIRECTORY"); + + // Test O_DIRECTORY on a regular file (should fail) + assert!(matches!( + fs.open( + USER, + "foo", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::ComponentNotADirectory)) + )); + + // Test O_DIRECTORY on non-existent path (should fail) + assert!(matches!( + fs.open( + USER, + "nonexistent", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) + )); + + // Test O_DIRECTORY on nested file (should fail) + assert!(matches!( + fs.open( + USER, + "bar/baz", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::ComponentNotADirectory)) + )); + } + + #[test] + fn write_or_truncate_open_of_directory_fails() { + let fs = tar_ro_fs(TEST_TAR_FILE.into()); + + for flags in [OFlags::WRONLY, OFlags::RDWR, OFlags::TRUNC] { + assert!(matches!( + fs.open(USER, "bar", flags, Mode::empty()), + Err(OpenError::ReadOnlyFileSystem) + )); + } + } + + #[test] + fn read_dir_subdirectory() { + let fs = tar_ro_fs(TEST_TAR_FILE.into()); + + // Read root directory + let fd = fs + .open(USER, "/", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open root directory"); + let entries = fs.read_dir(&fd).expect("Failed to read root directory"); + drop(fd); + + // Should have 4 entries: ., .., bar, foo + assert_eq!(entries.len(), 4); + + let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); + names.sort_unstable(); + assert_eq!(names, vec![".", "..", "bar", "foo"]); + + // Check file types + for entry in &entries { + match entry.name.as_str() { + "foo" => { + assert_eq!(entry.file_type, FileType::RegularFile); + } + "bar" | "." | ".." => assert_eq!(entry.file_type, FileType::Directory), + _ => panic!("Unexpected entry: {}", entry.name), + } + if entry.name != "." && entry.name != ".." { + assert!(entry.ino_info.is_some(), "Inode info should be present"); + } else { + // TODO(jayb): Re-enable this assertion once Composer handles `.` and `..` inode + // information better. + } + } + + // Read `bar` directory + let fd = fs + .open(USER, "bar", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open bar directory"); + let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); + + // Should have 3 entries: ., .., baz (file) + assert_eq!(entries.len(), 3); + assert_eq!(entries[2].name, "baz"); + assert_eq!(entries[2].file_type, FileType::RegularFile); + } + + #[test] + fn read_dir_file_not_directory() { + let fs = tar_ro_fs(TEST_TAR_FILE.into()); + + let fd = fs + .open(USER, "foo", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open foo file"); + assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); + } +} + +mod overlay { + use super::{ + FileType, Mode, NoDeviceIo, OFlags, Overlay, Resolver, SeekWhence, TEST_TAR_FILE, + TestPlatform, USER, UserInfo, + }; + use crate::fs::errors::{FileStatusError, OpenError, PathError, RmdirError}; + use crate::fs::in_mem::{InMem, InitialNode}; + use alloc::vec; + use alloc::vec::Vec; + extern crate std; + + /// The user these tests act as, and so the owner of anything they are set up as having created. + const ACTING_USER: UserInfo = USER; + const ALL_PERMS: Mode = Mode::RWXU.union(Mode::RWXG).union(Mode::RWXO); + + /// An upper backend whose root is writable by the acting user, holding `entries`. + /// + /// The overlay directs every mutation to the upper backend, so its root has to allow writes for + /// anything to be created. + fn upper( + entries: impl IntoIterator, + ) -> InMem { + InMem::new_initialized( + [( + "/", + InitialNode::Directory { + mode: ALL_PERMS, + owner: UserInfo::ROOT, + }, + )] + .into_iter() + .chain(entries), + ) + } + + fn overlay_fs(upper: InMem) -> Resolver> { + super::overlay_fs(upper, TEST_TAR_FILE.into()) + } + + #[test] + fn file_read_from_lower() { + let fs = overlay_fs(upper([])); + let mut fd = fs + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .expect("Failed to open file"); + let mut buffer = vec![0; 1024]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"testfoo\n"); + let stat = fs.handle_status(&fd).expect("Failed to handle stat"); + assert_eq!(stat.file_type, FileType::RegularFile); + assert_eq!(stat.mode, Mode::from_bits(0o644).unwrap()); + drop(fd); + + let stat = fs.file_status(USER, "bar").expect("Failed to file stat"); + assert_eq!(stat.file_type, FileType::Directory); + assert_eq!(stat.mode, Mode::from_bits(0o777).unwrap()); + + let mut fd = fs + .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open file"); + let mut buffer = vec![0; 1024]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"test bar baz\n"); + let stat = fs.handle_status(&fd).expect("Failed to handle stat"); + assert_eq!(stat.file_type, FileType::RegularFile); + assert_eq!(stat.mode, Mode::from_bits(0o644).unwrap()); + } + + #[test] + fn dir_and_nonexist_checks() { + let fs = overlay_fs(upper([])); + assert!(matches!( + fs.open(USER, "bar/ba", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), + )); + fs.open(USER, "bar", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open dir"); + } + + /// Check that for the same file, even though it started as a lower file, writing to it copies + /// it up and redirects handles already open on it, so every descriptor sees the update. + #[test] + fn file_read_write_copy_up() { + let fs = overlay_fs(upper([])); + let mut fd1 = fs + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .expect("Failed to open file"); + let mut fd2 = fs + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) + .expect("Failed to open file"); + + let mut buffer = vec![0; 1024]; + + let bytes_read = fs + .read(&NoDeviceIo, &mut fd1, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"testfoo\n"); + + fs.write(&NoDeviceIo, &mut fd2, b"share", None) + .expect("Failed to write to file"); + + fs.seek(&mut fd1, 0, SeekWhence::RelativeToBeginning) + .expect("Failed to seek to start"); + let bytes_read = fs + .read(&NoDeviceIo, &mut fd1, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"shareoo\n"); + } + + /// Similar to [`file_read_write_copy_up`] but also confirm that file positions have been + /// maintained. + #[test] + fn file_read_write_copy_up_keeps_position() { + let fs = overlay_fs(upper([])); + let mut fd1 = fs + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .expect("Failed to open file"); + let mut fd2 = fs + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) + .expect("Failed to open file"); + + let mut buffer = vec![0; 4]; + + let bytes_read = fs + .read(&NoDeviceIo, &mut fd1, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"test"); + + fs.write(&NoDeviceIo, &mut fd2, b"share", None) + .expect("Failed to write to file"); + + let bytes_read = fs + .read(&NoDeviceIo, &mut fd1, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"eoo\n"); + } + + #[test] + fn file_deletion() { + let fs = overlay_fs(upper([])); + let mut fd = fs + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .expect("Failed to open file"); + + let mut buffer = vec![0; 4]; + + // The file exists, and is readable + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"test"); + + // Then we delete it + fs.unlink(USER, "foo").unwrap(); + + // This should not really impact the readability; file is fine. + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"foo\n"); + + // But if we close and attempt to re-open, it should not exist + drop(fd); + assert!(matches!( + fs.open(USER, "foo", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), + )); + } + + #[test] + fn o_directory_flag_tests() { + let fs = overlay_fs(upper([ + ( + "/upperdir", + InitialNode::Directory { + mode: ALL_PERMS, + owner: ACTING_USER, + }, + ), + ( + "/upperfile", + InitialNode::File { + mode: Mode::RWXU, + owner: ACTING_USER, + data: alloc::borrow::Cow::Borrowed(b""), + }, + ), + ])); + + // Test O_DIRECTORY on directory from lower layer (tar) + fs.open( + USER, + "bar", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) + .expect("Failed to open lower layer directory with O_DIRECTORY"); + + // Test O_DIRECTORY on directory from upper layer (in_mem) + fs.open( + USER, + "/upperdir", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), + ) + .expect("Failed to open upper layer directory with O_DIRECTORY"); + + // Test O_DIRECTORY on file from lower layer (should fail) + assert!(matches!( + fs.open( + USER, + "foo", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::ComponentNotADirectory)) + )); + + // Test O_DIRECTORY on file from upper layer (should fail) + assert!(matches!( + fs.open( + USER, + "/upperfile", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::ComponentNotADirectory)) + )); + + // Test O_DIRECTORY on nested file from lower layer (should fail) + assert!(matches!( + fs.open( + USER, + "bar/baz", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::ComponentNotADirectory)) + )); + + // Test O_DIRECTORY on non-existent path (should fail) + assert!(matches!( + fs.open( + USER, + "nonexistent", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty() + ), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) + )); + } + + #[test] + // Regression test for #250: a file that already exists in the lower layer should not be + // shadowed by an attempt to create a file. + fn file_create_exist_in_lower() { + let fs = overlay_fs(upper([])); + let mut fd = fs + .open(USER, "foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) + .expect("Failed to open file"); + let mut buffer = vec![0; 4]; + + // The file exists, and is readable + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"test"); + } + + #[test] + fn read_dir_from_lower_layer() { + let fs = overlay_fs(upper([])); + + // Read bar subdirectory + let fd = fs + .open(USER, "bar", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open bar directory"); + let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); + + // Should have 3 entries: ., .., baz (file) + assert_eq!(entries.len(), 3); + assert_eq!(entries[2].name, "baz"); + assert_eq!(entries[2].file_type, FileType::RegularFile); + assert!( + entries[2].ino_info.is_some(), + "Inode info should be present" + ); + } + + #[test] + fn read_dir_from_upper_layer() { + let fs = overlay_fs(upper([ + ( + "/upperdir", + InitialNode::Directory { + mode: ALL_PERMS, + owner: ACTING_USER, + }, + ), + ( + "/upperfile", + InitialNode::File { + mode: Mode::RWXU, + owner: ACTING_USER, + data: alloc::borrow::Cow::Borrowed(b""), + }, + ), + ])); + + // Read root directory (should contain entries from both layers) + let fd = fs + .open(USER, "/", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open root directory"); + let entries = fs.read_dir(&fd).expect("Failed to read root directory"); + drop(fd); + + // Should have 6 entries: ., .., bar, foo (from lower), upperdir, upperfile (from upper) + assert_eq!(entries.len(), 6); + + let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); + names.sort_unstable(); + assert_eq!( + names, + vec![".", "..", "bar", "foo", "upperdir", "upperfile"] + ); + + // Check file types + for entry in &entries { + match entry.name.as_str() { + "foo" | "upperfile" => { + assert_eq!(entry.file_type, FileType::RegularFile); + } + "bar" | "upperdir" | "." | ".." => { + assert_eq!(entry.file_type, FileType::Directory); + } + _ => panic!("Unexpected entry: {}", entry.name), + } + if entry.name != "." && entry.name != ".." { + assert!(entry.ino_info.is_some(), "Inode info should be present"); + } else { + // TODO(jayb): Re-enable this assertion once the resolver fills in + // inode information for the synthesized `.` and `..` entries. + } + } + + // Read upperdir directory (should be from upper layer) + let fd = fs + .open(USER, "/upperdir", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open upperdir"); + let entries = fs.read_dir(&fd).expect("Failed to read upperdir"); + + // only . and .. + assert_eq!(entries.len(), 2); + } + + #[test] + fn o_excl_tests() { + let fs = overlay_fs(upper([])); + + // Test O_CREAT | O_EXCL on file that exists in lower layer (should fail) + // "foo" exists in the tar file + assert!(matches!( + fs.open( + USER, + "foo", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ), + Err(OpenError::AlreadyExists) + )); + + // Test O_CREAT | O_EXCL on file that doesn't exist anywhere (should succeed) + let mut fd = fs + .open( + USER, + "/newfile", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create new file with O_CREAT | O_EXCL"); + + fs.write(&NoDeviceIo, &mut fd, b"overlay test", None) + .expect("Failed to write to new file"); + drop(fd); + + // Test O_CREAT | O_EXCL on file that now exists in upper layer (should fail) + assert!(matches!( + fs.open( + USER, + "/newfile", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ), + Err(OpenError::AlreadyExists) + )); + + // Test O_CREAT | O_EXCL on directory that exists in lower layer (should fail) + // "bar" is a directory in the tar file + assert!(matches!( + fs.open( + USER, + "bar", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ), + Err(OpenError::AlreadyExists) + )); + + // Test O_CREAT | O_EXCL on file that was deleted (tombstoned) should succeed + // First delete a file from lower layer + fs.unlink(USER, "foo") + .expect("Failed to unlink lower layer file"); + + // Now try to create it with O_EXCL (should succeed since it's tombstoned) + let mut fd = fs + .open( + USER, + "foo", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create file over tombstone with O_CREAT | O_EXCL"); + + fs.write(&NoDeviceIo, &mut fd, b"new foo content", None) + .expect("Failed to write to recreated file"); + drop(fd); + + // Verify the new content + let mut fd = fs + .open(USER, "foo", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open recreated file"); + let mut buffer = vec![0; 15]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from recreated file"); + assert_eq!(&buffer[..bytes_read], b"new foo content"); + drop(fd); + + // Test O_CREAT | O_EXCL behavior with existing upper layer file + // Create a file in upper layer first + let mut fd = fs + .open( + USER, + "/upper_only_file", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU, + ) + .expect("Failed to create upper layer file"); + fs.write(&NoDeviceIo, &mut fd, b"upper content", None) + .expect("Failed to write to upper layer file"); + drop(fd); + + // Now try O_CREAT | O_EXCL on the same file (should fail) + assert!(matches!( + fs.open( + USER, + "/upper_only_file", + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RWXU, + ), + Err(OpenError::AlreadyExists) + )); + } + + #[test] + fn dir_creation_inside_lower_existing_dir() { + let fs = overlay_fs(upper([])); + + // Create the directory /bar/test (where /bar already exists inside the tar file) + fs.mkdir(USER, "/bar/test", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("Failed to create /bar/test directory"); + + // Verify the directory was created + let stat = fs + .file_status(USER, "/bar/test") + .expect("Failed to get status of /bar/test"); + assert_eq!(stat.file_type, FileType::Directory); + + // Verify we can open the directory + let fd = fs + .open(USER, "/bar/test", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open /bar/test directory"); + let entries = fs + .read_dir(&fd) + .expect("Failed to read /bar/test directory"); + + // Should contain only . and .. entries + assert_eq!(entries.len(), 2); + let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); + names.sort_unstable(); + assert_eq!(names, vec![".", ".."]); + } + + #[test] + fn file_creation_materializes_ancestor_dirs() { + let fs = overlay_fs(upper([])); + + // Open bar/test for writing (where bar exists in lower layer but test doesn't exist) + // This should create ancestor directories and allow file creation + let mut fd = fs + .open(USER, "bar/test", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to open bar/test for writing"); + + // Write data to the file + let data = b"Hello from nested file!"; + fs.write(&NoDeviceIo, &mut fd, data, None) + .expect("Failed to write to bar/test"); + drop(fd); + + // Read the file back + let mut fd = fs + .open(USER, "bar/test", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open bar/test for reading"); + let mut buffer = vec![0; 1024]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from bar/test"); + assert_eq!(&buffer[..bytes_read], data); + drop(fd); + + // Verify the file exists and has correct type + let stat = fs + .file_status(USER, "bar/test") + .expect("Failed to get status of bar/test"); + assert_eq!(stat.file_type, FileType::RegularFile); + } + + #[test] + fn file_modification_materializes_ancestor_dirs() { + let fs = overlay_fs(upper([])); + + // Open bar/baz for writing (both bar and baz exist in lower layer) + // This copies up the ancestor directories and allows the file to be modified + let mut fd = fs + .open(USER, "bar/baz", OFlags::WRONLY, Mode::RWXU) + .expect("Failed to open bar/baz for writing"); + + // Write new data to the file (overwriting existing content) + let data = b"Modified content!"; + fs.write(&NoDeviceIo, &mut fd, data, None) + .expect("Failed to write to bar/baz"); + drop(fd); + + // Read the file back to verify it was modified + let mut fd = fs + .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open bar/baz for reading"); + let mut buffer = vec![0; 1024]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read from bar/baz"); + + assert_eq!(&buffer[..bytes_read], data); + drop(fd); + + // Verify the file still exists and has correct type + let stat = fs + .file_status(USER, "bar/baz") + .expect("Failed to get status of bar/baz"); + assert_eq!(stat.file_type, FileType::RegularFile); + } + + #[test] + fn open_with_trunc() { + let fs = overlay_fs(upper([])); + + // Open with O_TRUNC should copy the file up into the upper backend, empty + let mut fd = fs + .open(USER, "foo", OFlags::RDWR | OFlags::TRUNC, Mode::empty()) + .expect("Failed to open file with O_TRUNC"); + + // File should be truncated (empty) + let mut buffer = vec![0; 1024]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read file"); + assert_eq!(bytes_read, 0); + + // Write new content + fs.write(&NoDeviceIo, &mut fd, b"new content", None) + .expect("Failed to write to file"); + drop(fd); + + // Verify the content persists + let mut fd = fs + .open(USER, "foo", OFlags::RDONLY, Mode::empty()) + .expect("Failed to reopen file"); + let mut buffer = vec![0; 1024]; + let bytes_read = fs + .read(&NoDeviceIo, &mut fd, &mut buffer, None) + .expect("Failed to read file"); + assert_eq!(&buffer[..bytes_read], b"new content"); + } + + #[test] + fn rmdir_upper_only_directory() { + let fs = overlay_fs(upper([])); + + // Create an empty directory only in upper layer + fs.mkdir(USER, "/upper_empty", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("mkdir upper_empty failed"); + + // Remove it + fs.rmdir(USER, "/upper_empty") + .expect("rmdir upper_empty should succeed"); + + // Verify it no longer exists + assert!(matches!( + fs.file_status(USER, "/upper_empty"), + Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) + )); + + // Second removal should yield NoSuchFileOrDirectory (path error) + assert!(matches!( + fs.rmdir(USER, "/upper_empty"), + Err(RmdirError::PathError(PathError::NoSuchFileOrDirectory)) + )); + } + + #[test] + fn rmdir_upper_directory_not_empty_then_empty() { + let fs = overlay_fs(upper([])); + + fs.mkdir(USER, "/upper_dir", Mode::RWXU | Mode::RWXG | Mode::RWXO) + .expect("mkdir upper_dir failed"); + + // Create a file inside making directory non-empty + let fd = fs + .open( + USER, + "/upper_dir/file", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU | Mode::RWXG, + ) + .expect("create file in upper_dir failed"); + drop(fd); + + // Attempt to remove while non-empty + assert!(matches!( + fs.rmdir(USER, "/upper_dir"), + Err(RmdirError::NotEmpty) + )); + + // Remove inner file + fs.unlink(USER, "/upper_dir/file") + .expect("unlink inner failed"); + + // Now should succeed + fs.rmdir(USER, "/upper_dir") + .expect("rmdir upper_dir should succeed"); + + // Confirm gone + assert!(matches!( + fs.file_status(USER, "/upper_dir"), + Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) + )); + } + + #[test] + fn rmdir_lower_directory_non_empty() { + let fs = overlay_fs(upper([])); + + // "bar" exists in lower layer and contains "baz" (non-empty) + assert!(matches!(fs.rmdir(USER, "bar"), Err(RmdirError::NotEmpty))); + } + + #[test] + fn rmdir_not_a_directory() { + let fs = overlay_fs(upper([])); + + // Create a regular file (upper only) + let fd = fs + .open( + USER, + "/regular_file", + OFlags::CREAT | OFlags::WRONLY, + Mode::RWXU | Mode::RWXG, + ) + .expect("create file failed"); + drop(fd); + + // rmdir should fail with NotADirectory + assert!(matches!( + fs.rmdir(USER, "/regular_file"), + Err(RmdirError::NotADirectory) + )); + } + + #[test] + fn copy_up_does_not_deadlock() { + use std::sync::mpsc; + use std::thread; + use std::time::Duration; + + let fs = overlay_fs(upper([])); + + fs.file_status(USER, "foo").expect("Failed to stat foo"); + + // Writing to the lower-layer file triggers copy-up. Run it on a worker thread. + let (tx, rx) = mpsc::channel(); + thread::spawn(move || { + let mut fd = fs + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) + .expect("Failed to open file for writing"); + fs.write(&NoDeviceIo, &mut fd, b"x", None) + .expect("Failed to write to file"); + drop(fd); + let _ = tx.send(()); + }); + + rx.recv_timeout(Duration::from_secs(2)) + .expect("copy-up deadlocked"); + } +} + +mod devices { + use super::{Mode, OFlags, Resolver, TestPlatform, USER, UnservicedStdio}; + use crate::fs::composer::Composer; + use crate::fs::devices::Devices; + use crate::fs::errors::{OpenError, PathError, ReadError, WriteError}; + use alloc::vec; + + fn devices_fs() -> Resolver { + Resolver::new( + Composer::builder() + .mount("/dev", Devices::new) + .build() + .unwrap(), + ) + } + + /// Stdio devices hold no data of their own: every non-empty transfer needs the session's + /// device I/O, and fails when the session cannot service it. + #[test] + fn stdio_requires_broker() { + let fs = devices_fs(); + let stdio = UnservicedStdio; + + let mut fd_stdout = fs + .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stdout"); + assert!(matches!(fs.write(&stdio, &mut fd_stdout, b"", None), Ok(0))); + assert!(matches!( + fs.write(&stdio, &mut fd_stdout, b"Hello, stdout!", None), + Err(WriteError::Io) + )); + drop(fd_stdout); + + let mut fd_stderr = fs + .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stderr"); + assert!(matches!(fs.write(&stdio, &mut fd_stderr, b"", None), Ok(0))); + assert!(matches!( + fs.write(&stdio, &mut fd_stderr, b"Hello, stderr!", None), + Err(WriteError::Io) + )); + drop(fd_stderr); + + let mut fd_stdin = fs + .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open /dev/stdin"); + assert!(matches!( + fs.read(&stdio, &mut fd_stdin, &mut [], None), + Ok(0) + )); + let mut buffer = vec![0; 13]; + assert!(matches!( + fs.read(&stdio, &mut fd_stdin, &mut buffer, None), + Err(ReadError::Io) + )); + } + + #[test] + fn non_dev_path_fails() { + let fs = devices_fs(); + + // Attempt to open a non-/dev/* path + assert!(matches!( + fs.open(USER, "foo", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) + )); + } +} + +mod composed { + use super::{InMem, Mode, OFlags, Resolver, TestPlatform, USER, UnservicedStdio, UserInfo}; + use crate::fs::composer::Composer; + use crate::fs::devices::Devices; + use crate::fs::errors::{ReadError, WriteError}; + use crate::fs::in_mem::InitialNode; + use alloc::vec; + + fn composed_fs() -> Resolver { + Resolver::new( + Composer::builder() + .mount("/", |_| { + InMem::::new_initialized([( + "/", + InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner: UserInfo::ROOT, + }, + )]) + }) + .mount("/dev", Devices::new) + .build() + .unwrap(), + ) + } + + #[test] + fn stdio_requires_broker() { + let fs = composed_fs(); + let stdio = UnservicedStdio; + + let mut fd_stdout = fs + .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stdout"); + assert!(matches!(fs.write(&stdio, &mut fd_stdout, b"", None), Ok(0))); + assert!(matches!( + fs.write(&stdio, &mut fd_stdout, b"Hello, composed stdout!", None), + Err(WriteError::Io) + )); + drop(fd_stdout); + + let mut fd_stderr = fs + .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stderr"); + assert!(matches!(fs.write(&stdio, &mut fd_stderr, b"", None), Ok(0))); + assert!(matches!( + fs.write(&stdio, &mut fd_stderr, b"Hello, composed stderr!", None), + Err(WriteError::Io) + )); + drop(fd_stderr); + + let mut fd_stdin = fs + .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open /dev/stdin"); + assert!(matches!( + fs.read(&stdio, &mut fd_stdin, &mut [], None), + Ok(0) + )); + let mut buffer = vec![0; 1024]; + assert!(matches!( + fs.read(&stdio, &mut fd_stdin, &mut buffer, None), + Err(ReadError::Io) + )); + } + + #[test] + fn write_to_non_dev() { + let fs = composed_fs(); + + // Test file creation + let path = "/testfile"; + let fd = fs + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .expect("Failed to create file"); + drop(fd); + + // Test file deletion + fs.unlink(USER, path).expect("Failed to unlink file"); + assert!( + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), + "File should not exist" + ); + } +} diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index c9b6962a2..085cb2680 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -28,6 +28,8 @@ pub mod readiness; mod session; pub mod socket; pub mod stdio; +#[cfg(test)] +mod test_platform; use alloc::sync::Arc; use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index 77a0a3478..a3e8be623 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -618,10 +618,10 @@ impl Drop for BrokerSession { #[cfg(test)] mod tests { - use core::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; - use core::time::Duration; + use core::sync::atomic::{AtomicUsize, Ordering}; use super::{SessionReferences, release_pending_reference}; + use crate::test_platform::TestPlatform; use crate::{ BrokerCore, BrokerCoreLimits, BrokerError, CallerCredential, ObjectRights, PolicyEngine, SocketPolicy, @@ -632,13 +632,7 @@ mod tests { FileAccessMode, FileError, FileMode, FileOpenFlags, FileSeekWhence, FileType, FileUser, }; use litebox_broker_protocol::readiness::ReadinessFlags; - use litebox_platform::sync::{ - ImmediatelyWokenUp, RawMutex, RawMutexProvider, UnblockedOrTimedOut, - }; - use std::{ - sync::{Arc, Condvar, Mutex}, - vec::Vec, - }; + use std::{sync::Arc, vec::Vec}; const TEST_MAX_REFERENCES: usize = 4; const TEST_MAX_PIPE_CAPACITY: usize = 8; @@ -646,70 +640,6 @@ mod tests { const TEST_MAX_PIPE_CAPACITY_PER_SESSION: usize = 4; const ROOT: FileUser = FileUser { user: 0, group: 0 }; - struct TestRawMutex { - state: AtomicU32, - waiters: Mutex<()>, - wake: Condvar, - } - - impl RawMutex for TestRawMutex { - const INIT: Self = Self { - state: AtomicU32::new(0), - waiters: Mutex::new(()), - wake: Condvar::new(), - }; - - fn underlying_atomic(&self) -> &AtomicU32 { - &self.state - } - - fn wake_many(&self, count: usize) -> usize { - let _waiters = self.waiters.lock().unwrap(); - self.wake.notify_all(); - count - } - - fn block(&self, expected: u32) -> Result<(), ImmediatelyWokenUp> { - let waiters = self.waiters.lock().unwrap(); - if self.state.load(Ordering::Acquire) != expected { - return Err(ImmediatelyWokenUp); - } - let _waiters = self - .wake - .wait_while(waiters, |()| self.state.load(Ordering::Acquire) == expected) - .unwrap(); - Ok(()) - } - - fn block_or_timeout( - &self, - expected: u32, - timeout: Duration, - ) -> Result { - let waiters = self.waiters.lock().unwrap(); - if self.state.load(Ordering::Acquire) != expected { - return Err(ImmediatelyWokenUp); - } - let (_waiters, result) = self - .wake - .wait_timeout_while(waiters, timeout, |()| { - self.state.load(Ordering::Acquire) == expected - }) - .unwrap(); - Ok(if result.timed_out() { - UnblockedOrTimedOut::TimedOut - } else { - UnblockedOrTimedOut::Unblocked - }) - } - } - - struct TestSync; - - impl RawMutexProvider for TestSync { - type RawMutex = TestRawMutex; - } - #[test] fn pending_reference_release_checks_both_counters() { let core_pending_references = AtomicUsize::new(1); @@ -816,7 +746,7 @@ mod tests { assert_eq!(crate::fs::write(&source, file, b"abcdef", None), Ok(Ok(6))); assert_eq!( - crate::fs::seek(&source, file, 0, FileSeekWhence::Beginning), + crate::fs::seek(&source, file, 0, FileSeekWhence::RelativeToBeginning), Ok(Ok(0)) ); @@ -885,7 +815,7 @@ mod tests { assert_eq!(status.size, 6); assert_eq!(source.close_object_reference(file), Ok(())); assert_eq!( - crate::fs::seek(&target, duplicate, 0, FileSeekWhence::Beginning), + crate::fs::seek(&target, duplicate, 0, FileSeekWhence::RelativeToBeginning), Ok(Ok(0)) ); assert_eq!( @@ -952,7 +882,7 @@ mod tests { fn object_reference_lifecycle_uses_public_core_constructor_once() { let socket_provider = Arc::new(crate::socket::tests::TestSocketProvider::default()); let fs = crate::fs::composer::Composer::builder() - .mount("/", crate::fs::in_mem::InMem::::new) + .mount("/", crate::fs::in_mem::InMem::::new) .mount("/dev", crate::fs::devices::Devices::new) .build() .unwrap(); @@ -972,7 +902,7 @@ mod tests { socket_provider.clone(), Arc::new(crate::random::TestRandomProvider), Arc::new(crate::stdio::UnsupportedStdioProvider), - Arc::new(crate::fs::resolver::Resolver::::new(fs)), + Arc::new(crate::fs::resolver::Resolver::::new(fs)), ) .unwrap(); diff --git a/litebox_broker_core/src/test_platform.rs b/litebox_broker_core/src/test_platform.rs new file mode 100644 index 000000000..cf1a20da9 --- /dev/null +++ b/litebox_broker_core/src/test_platform.rs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Synchronization platform shared by broker-core unit tests. + +use core::sync::atomic::{AtomicU32, Ordering}; +use core::time::Duration; + +use litebox_platform::sync::{ImmediatelyWokenUp, RawMutex, RawMutexProvider, UnblockedOrTimedOut}; +use std::sync::{Condvar, Mutex}; + +/// A [`RawMutex`] built on the host's condition variables. +pub(crate) struct TestRawMutex { + state: AtomicU32, + waiters: Mutex<()>, + wake: Condvar, +} + +impl RawMutex for TestRawMutex { + const INIT: Self = Self { + state: AtomicU32::new(0), + waiters: Mutex::new(()), + wake: Condvar::new(), + }; + + fn underlying_atomic(&self) -> &AtomicU32 { + &self.state + } + + fn wake_many(&self, count: usize) -> usize { + let _waiters = self.waiters.lock().unwrap(); + self.wake.notify_all(); + count + } + + fn block(&self, expected: u32) -> Result<(), ImmediatelyWokenUp> { + let waiters = self.waiters.lock().unwrap(); + if self.state.load(Ordering::Acquire) != expected { + return Err(ImmediatelyWokenUp); + } + let _waiters = self + .wake + .wait_while(waiters, |()| self.state.load(Ordering::Acquire) == expected) + .unwrap(); + Ok(()) + } + + fn block_or_timeout( + &self, + expected: u32, + timeout: Duration, + ) -> Result { + let waiters = self.waiters.lock().unwrap(); + if self.state.load(Ordering::Acquire) != expected { + return Err(ImmediatelyWokenUp); + } + let (_waiters, result) = self + .wake + .wait_timeout_while(waiters, timeout, |()| { + self.state.load(Ordering::Acquire) == expected + }) + .unwrap(); + Ok(if result.timed_out() { + UnblockedOrTimedOut::TimedOut + } else { + UnblockedOrTimedOut::Unblocked + }) + } +} + +/// The platform broker-core tests instantiate platform-generic types with. +pub(crate) struct TestPlatform; + +impl RawMutexProvider for TestPlatform { + type RawMutex = TestRawMutex; +} diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 19adbcfea..7c8e0b59a 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -1491,7 +1491,7 @@ mod tests { BrokerOperation::File(FileRequest::Seek(SeekFileRequest { handle: opened.handle, offset: 0, - whence: FileSeekWhence::Beginning, + whence: FileSeekWhence::RelativeToBeginning, })), &shared_buffers, ), diff --git a/litebox_broker_local/src/fs.rs b/litebox_broker_local/src/fs.rs index d82108c37..1049d5dda 100644 --- a/litebox_broker_local/src/fs.rs +++ b/litebox_broker_local/src/fs.rs @@ -467,12 +467,12 @@ mod tests { ino: 2, rdev: None, }, - block_size: 4096, + blksize: 4096, }; let entries = [FileDirectoryEntry { name: "file".into(), file_type: FileType::RegularFile, - node_info: None, + ino_info: None, }]; let directory_payload = encode_directory_entries(&entries).unwrap(); let channel = ScriptedChannel::new([ @@ -532,7 +532,7 @@ mod tests { assert_eq!(output, [4, 5]); assert_eq!( local - .seek_file(handle, 0, FileSeekWhence::Beginning) + .seek_file(handle, 0, FileSeekWhence::RelativeToBeginning) .unwrap(), Ok(0) ); diff --git a/litebox_broker_platform_windows_userland/Cargo.toml b/litebox_broker_platform_windows_userland/Cargo.toml new file mode 100644 index 000000000..64325a6a5 --- /dev/null +++ b/litebox_broker_platform_windows_userland/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "litebox_broker_platform_windows_userland" +version = "0.1.0" +edition = "2024" + +[target.'cfg(windows)'.dependencies] +litebox_platform = { path = "../litebox_platform", version = "0.1.0" } +windows-sys = { version = "0.60.2", features = [ + "Win32_Foundation", + "Win32_System_Threading", +] } + +[features] +lock_tracing = ["litebox_platform/lock_tracing"] + +[lints] +workspace = true diff --git a/litebox_broker_platform_windows_userland/src/lib.rs b/litebox_broker_platform_windows_userland/src/lib.rs new file mode 100644 index 000000000..aefeaad28 --- /dev/null +++ b/litebox_broker_platform_windows_userland/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows-userland implementations of trusted broker platform capabilities. +//! +//! Association transport belongs to `litebox_broker_transport_windows_userland`. +//! This crate instead owns broker-side operating-system resources that must +//! never cross into the local process. + +#![cfg(windows)] + +mod sync; + +pub use sync::WindowsSyncPrimitivesProvider; diff --git a/litebox_broker_platform_windows_userland/src/sync.rs b/litebox_broker_platform_windows_userland/src/sync.rs new file mode 100644 index 000000000..95d52219a --- /dev/null +++ b/litebox_broker_platform_windows_userland/src/sync.rs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Windows-userland broker synchronization primitives. + +use core::ffi::c_void; +use core::sync::atomic::AtomicU32; +use core::time::Duration; + +use litebox_platform::sync::{ + ImmediatelyWokenUp, RawMutex as RawMutexTrait, RawMutexProvider, UnblockedOrTimedOut, +}; +use windows_sys::Win32::Foundation::{ERROR_TIMEOUT, GetLastError}; +use windows_sys::Win32::System::Threading::{ + INFINITE, WaitOnAddress, WakeByAddressAll, WakeByAddressSingle, +}; + +/// Blocking synchronization primitives for a Windows-userland broker. +#[derive(Clone, Copy, Debug, Default)] +pub struct WindowsSyncPrimitivesProvider; + +impl RawMutexProvider for WindowsSyncPrimitivesProvider { + type RawMutex = WindowsRawMutex; +} + +/// Raw blocking mutex used by the Windows-userland broker. +pub struct WindowsRawMutex { + state: AtomicU32, +} + +impl WindowsRawMutex { + const fn new() -> Self { + Self { + state: AtomicU32::new(0), + } + } + + fn block_or_maybe_timeout( + &self, + expected: u32, + timeout: Option, + ) -> UnblockedOrTimedOut { + let timeout_ms = timeout.map_or(INFINITE, |timeout| { + u32::try_from(timeout.as_millis().min(u128::from(INFINITE - 1))).unwrap() + }); + // SAFETY: Both pointers remain valid for the call and identify equally sized u32 values. + let unblocked = unsafe { + WaitOnAddress( + (&raw const self.state).cast::(), + (&raw const expected).cast::(), + size_of::(), + timeout_ms, + ) != 0 + }; + if unblocked { + UnblockedOrTimedOut::Unblocked + } else { + // SAFETY: GetLastError has no preconditions. + match unsafe { GetLastError() } { + ERROR_TIMEOUT => UnblockedOrTimedOut::TimedOut, + error => panic!("WaitOnAddress failed with error {error}"), + } + } + } +} + +impl RawMutexTrait for WindowsRawMutex { + const INIT: Self = Self::new(); + + fn underlying_atomic(&self) -> &AtomicU32 { + &self.state + } + + fn wake_many(&self, count: usize) -> usize { + assert!(count > 0, "wake count must be nonzero"); + let address = core::ptr::from_ref(&self.state).cast::(); + // SAFETY: `address` points to the aligned AtomicU32 used by waiters and stays valid here. + unsafe { + if count == 1 { + WakeByAddressSingle(address); + } else if count >= i32::MAX as usize { + WakeByAddressAll(address); + } else { + for _ in 0..count { + WakeByAddressSingle(address); + } + } + } + 0 + } + + fn block(&self, expected: u32) -> Result<(), ImmediatelyWokenUp> { + match self.block_or_maybe_timeout(expected, None) { + UnblockedOrTimedOut::Unblocked => Ok(()), + UnblockedOrTimedOut::TimedOut => unreachable!(), + } + } + + fn block_or_timeout( + &self, + expected: u32, + timeout: Duration, + ) -> Result { + Ok(self.block_or_maybe_timeout(expected, Some(timeout))) + } +} diff --git a/litebox_broker_protocol/Cargo.toml b/litebox_broker_protocol/Cargo.toml index 9cfecae7e..e1c47bbcc 100644 --- a/litebox_broker_protocol/Cargo.toml +++ b/litebox_broker_protocol/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +bitflags = { version = "2.9.0", default-features = false } thiserror = { version = "2.0.6", default-features = false } [lints] diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index 918e4ba12..45c800e63 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -5,7 +5,9 @@ use alloc::string::String; use alloc::vec::Vec; +use core::num::NonZeroU64; +use bitflags::bitflags; use thiserror::Error; use crate::ObjectHandle; @@ -28,6 +30,11 @@ pub struct FileUser { pub group: u16, } +impl FileUser { + /// The root user. + pub const ROOT: Self = Self { user: 0, group: 0 }; +} + /// File object kind. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] @@ -41,14 +48,14 @@ pub enum FileType { } /// Device and inode identity. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct FileNodeInfo { /// Device number. pub dev: u64, /// Inode number. pub ino: u64, - /// Referenced device number for special files. - pub rdev: Option, + /// Nonzero referenced device number for special files. + pub rdev: Option, } /// Status returned for a fs object. @@ -65,7 +72,7 @@ pub struct FileStatus { /// Device and inode identity. pub node_info: FileNodeInfo, /// Preferred fs I/O block size. - pub block_size: u64, + pub blksize: u64, } /// One directory entry. @@ -76,7 +83,7 @@ pub struct FileDirectoryEntry { /// Entry kind. pub file_type: FileType, /// Optional device and inode identity. - pub node_info: Option, + pub ino_info: Option, } /// File operation failure that is meaningful to the guest ABI. @@ -129,14 +136,13 @@ pub enum FileError { /// Seek origin. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] pub enum FileSeekWhence { /// Offset from the beginning of the file. - Beginning, + RelativeToBeginning, /// Offset from the file's current position. - Current, + RelativeToCurrentOffset, /// Offset from the end of the file. - End, + RelativeToEnd, } /// Access mode requested when opening a fs object. @@ -151,29 +157,56 @@ pub enum FileAccessMode { ReadWrite, } -/// ABI-neutral fs permission and special mode bits. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct FileMode(u16); - -impl FileMode { - /// Every permission and special mode bit this protocol version defines. - pub const SUPPORTED: Self = Self(0o7777); - - /// Creates a mode when every bit is defined by this protocol version. - #[must_use] - pub const fn from_bits(bits: u16) -> Option { - if bits & !Self::SUPPORTED.0 == 0 { - Some(Self(bits)) - } else { - None - } +bitflags! { + /// ABI-neutral fs permission and special mode bits. + #[repr(transparent)] + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] + pub struct FileMode: u16 { + /// User (file owner) has read, write, and execute permission. + const RWXU = 0o00700; + /// User has read permission. + const RUSR = 0o00400; + /// User has write permission. + const WUSR = 0o00200; + /// User has execute permission. + const XUSR = 0o00100; + /// Group has read, write, and execute permission. + const RWXG = 0o00070; + /// Group has read permission. + const RGRP = 0o00040; + /// Group has write permission. + const WGRP = 0o00020; + /// Group has execute permission. + const XGRP = 0o00010; + /// Others have read, write, and execute permission. + const RWXO = 0o00007; + /// Others have read permission. + const ROTH = 0o00004; + /// Others have write permission. + const WOTH = 0o00002; + /// Others have execute permission. + const XOTH = 0o00001; + /// Set-user-ID bit. + const SUID = 0o0004000; + /// Set-group-ID bit. + const SGID = 0o0002000; + /// Sticky bit. + const SVTX = 0o0001000; + /// Every permission and special mode bit this protocol version defines. + const SUPPORTED = 0o0007777; } +} - /// Returns the stable protocol bits. +impl FileMode { + /// Creates a mode from wider mode bits, discarding bits not defined by this protocol. #[must_use] - pub const fn bits(self) -> u16 { - self.0 + #[expect( + clippy::cast_possible_truncation, + reason = "masking with u16-backed supported bits guarantees the result fits in u16" + )] + pub fn from_u32_bits_truncate(bits: u32) -> Self { + let bits = (bits & u32::from(Self::SUPPORTED.bits())) as u16; + Self::from_bits_retain(bits) } } @@ -208,7 +241,6 @@ impl FileOpenFlags { pub const APPEND: Self = Self(1 << 8); /// Open only for path-based operations. pub const PATH: Self = Self(1 << 9); - /// Every open flag this protocol version defines. pub const SUPPORTED: Self = Self( Self::CREATE.0 @@ -547,14 +579,16 @@ pub fn try_decode_directory_entries( name.push_str(encoded_name); let file_type = file_type_from_raw(decoder.u8()?).ok_or(DirectoryPayloadError::Malformed)?; - let node_info = match decoder.u8()? { + let ino_info = match decoder.u8()? { 0 => None, 1 => { let dev = decoder.u64()?; let ino = decoder.u64()?; let rdev = match decoder.u8()? { 0 => None, - 1 => Some(decoder.u64()?), + 1 => Some( + NonZeroU64::new(decoder.u64()?).ok_or(DirectoryPayloadError::Malformed)?, + ), _ => return Err(DirectoryPayloadError::Malformed.into()), }; Some(FileNodeInfo { dev, ino, rdev }) @@ -564,7 +598,7 @@ pub fn try_decode_directory_entries( entries.push(FileDirectoryEntry { name, file_type, - node_info, + ino_info, }); } if decoder.offset != payload.len() { @@ -598,7 +632,7 @@ fn encoded_directory_entry_length( .checked_add(entry.name.len()) .and_then(|length| length.checked_add(2)) .and_then(|length| { - entry.node_info.map_or(Some(length), |node_info| { + entry.ino_info.map_or(Some(length), |node_info| { length .checked_add(size_of::() * 2 + 1) .and_then(|length| { @@ -648,7 +682,7 @@ fn encode_directory_entry( output.extend_from_slice(&name_len.to_le_bytes()); output.extend_from_slice(name); output.push(file_type_raw(entry.file_type)); - match entry.node_info { + match entry.ino_info { Some(node_info) => { output.push(1); output.extend_from_slice(&node_info.dev.to_le_bytes()); @@ -656,7 +690,7 @@ fn encode_directory_entry( match node_info.rdev { Some(rdev) => { output.push(1); - output.extend_from_slice(&rdev.to_le_bytes()); + output.extend_from_slice(&rdev.get().to_le_bytes()); } None => output.push(0), } @@ -711,12 +745,12 @@ mod tests { FileDirectoryEntry { name: ".".into(), file_type: FileType::Directory, - node_info: None, + ino_info: None, }, FileDirectoryEntry { name: "regular".into(), file_type: FileType::RegularFile, - node_info: Some(FileNodeInfo { + ino_info: Some(FileNodeInfo { dev: 2, ino: 3, rdev: None, @@ -725,10 +759,10 @@ mod tests { FileDirectoryEntry { name: "device".into(), file_type: FileType::CharacterDevice, - node_info: Some(FileNodeInfo { + ino_info: Some(FileNodeInfo { dev: 5, ino: 7, - rdev: Some(11), + rdev: NonZeroU64::new(11), }), }, ]; @@ -742,10 +776,10 @@ mod tests { let payload = encode_directory_entries(&[FileDirectoryEntry { name: "x".into(), file_type: FileType::CharacterDevice, - node_info: Some(FileNodeInfo { + ino_info: Some(FileNodeInfo { dev: 2, ino: 3, - rdev: Some(5), + rdev: NonZeroU64::new(5), }), }]) .unwrap(); @@ -764,17 +798,17 @@ mod tests { FileDirectoryEntry { name: "first".into(), file_type: FileType::RegularFile, - node_info: None, + ino_info: None, }, FileDirectoryEntry { name: "second".into(), file_type: FileType::Directory, - node_info: None, + ino_info: None, }, FileDirectoryEntry { name: "third".into(), file_type: FileType::CharacterDevice, - node_info: None, + ino_info: None, }, ]; let first_two_length = encode_directory_entries(&entries[..2]).unwrap().len(); @@ -800,7 +834,7 @@ mod tests { let valid = encode_directory_entries(&[FileDirectoryEntry { name: "entry".into(), file_type: FileType::RegularFile, - node_info: None, + ino_info: None, }]) .unwrap(); @@ -834,7 +868,7 @@ mod tests { let maximum_entry = FileDirectoryEntry { name: "x".repeat(maximum_name_length), file_type: FileType::RegularFile, - node_info: None, + ino_info: None, }; let payload = encode_directory_entries(core::slice::from_ref(&maximum_entry)).unwrap(); assert_eq!(payload.len(), MAX_FILE_TRANSFER_SIZE as usize); @@ -843,7 +877,7 @@ mod tests { let oversized_entry = FileDirectoryEntry { name: "x".repeat(maximum_name_length + 1), file_type: FileType::RegularFile, - node_info: None, + ino_info: None, }; assert_eq!( encode_directory_entries(&[oversized_entry]), diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 682230de7..648b40beb 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -465,6 +465,7 @@ mod tests { }; use crate::{ObjectHandle, ProtocolVersion, RequestId}; use core::net::{Ipv4Addr, SocketAddrV4}; + use core::num::NonZeroU64; const TEST_REQUEST_ID: RequestId = RequestId(0x0102_0304_0506_0708); @@ -635,7 +636,7 @@ mod tests { BrokerOperation::File(FileRequest::Seek(SeekFileRequest { handle, offset: -19, - whence: FileSeekWhence::Current, + whence: FileSeekWhence::RelativeToCurrentOffset, })), BrokerOperation::File(FileRequest::Truncate(TruncateFileRequest { handle, @@ -1096,9 +1097,9 @@ mod tests { node_info: FileNodeInfo { dev: u64::MAX, ino: u64::MAX, - rdev: Some(u64::MAX), + rdev: Some(NonZeroU64::MAX), }, - block_size: u64::MAX, + blksize: u64::MAX, })), BrokerResult::File(FileResponse::HandleStatus(FileStatus { file_type: FileType::CharacterDevice, @@ -1111,9 +1112,9 @@ mod tests { node_info: FileNodeInfo { dev: u64::MAX, ino: u64::MAX, - rdev: Some(u64::MAX), + rdev: Some(NonZeroU64::MAX), }, - block_size: u64::MAX, + blksize: u64::MAX, })), BrokerResult::File(FileResponse::Chmod), BrokerResult::File(FileResponse::Chown), @@ -1308,7 +1309,7 @@ mod tests { operation: BrokerOperation::File(FileRequest::Seek(SeekFileRequest { handle: ObjectHandle(13), offset: 0, - whence: FileSeekWhence::Beginning, + whence: FileSeekWhence::RelativeToBeginning, })), }); *invalid_whence.last_mut().unwrap() = 0xff; @@ -1346,7 +1347,7 @@ mod tests { ino: 7, rdev: None, }, - block_size: 4096, + blksize: 4096, })), }; let status = encode_response(status); diff --git a/litebox_broker_protocol/src/wire/fs.rs b/litebox_broker_protocol/src/wire/fs.rs index aa33dd8f3..9c526d207 100644 --- a/litebox_broker_protocol/src/wire/fs.rs +++ b/litebox_broker_protocol/src/wire/fs.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use core::num::NonZeroU64; + use crate::fs::{ ChmodFileRequest, ChownFileRequest, FileAccessMode, FileError, FileMode, FileNodeInfo, FileOpenFlags, FileSeekWhence, FileStatus, FileUser, HandleFileStatusRequest, MkdirFileRequest, @@ -418,17 +420,17 @@ fn decode_bool(decoder: &mut Decoder<'_>) -> Result { fn encode_whence(encoder: &mut Encoder, whence: FileSeekWhence) { encoder.u8(match whence { - FileSeekWhence::Beginning => 0, - FileSeekWhence::Current => 1, - FileSeekWhence::End => 2, + FileSeekWhence::RelativeToBeginning => 0, + FileSeekWhence::RelativeToCurrentOffset => 1, + FileSeekWhence::RelativeToEnd => 2, }); } fn decode_whence(decoder: &mut Decoder<'_>) -> Result { match decoder.u8()? { - 0 => Ok(FileSeekWhence::Beginning), - 1 => Ok(FileSeekWhence::Current), - 2 => Ok(FileSeekWhence::End), + 0 => Ok(FileSeekWhence::RelativeToBeginning), + 1 => Ok(FileSeekWhence::RelativeToCurrentOffset), + 2 => Ok(FileSeekWhence::RelativeToEnd), _ => Err(WireError::InvalidTag), } } @@ -440,8 +442,8 @@ fn encode_status(encoder: &mut Encoder, status: FileStatus) { encode_user(encoder, status.owner); encoder.u64(status.node_info.dev); encoder.u64(status.node_info.ino); - encode_optional_u64(encoder, status.node_info.rdev); - encoder.u64(status.block_size); + encode_optional_u64(encoder, status.node_info.rdev.map(NonZeroU64::get)); + encoder.u64(status.blksize); } fn decode_status(decoder: &mut Decoder<'_>) -> Result { @@ -451,14 +453,16 @@ fn decode_status(decoder: &mut Decoder<'_>) -> Result { let owner = decode_user(decoder)?; let dev = decoder.u64()?; let ino = decoder.u64()?; - let rdev = decode_optional_u64(decoder)?; - let block_size = decoder.u64()?; + let rdev = decode_optional_u64(decoder)? + .map(|value| NonZeroU64::new(value).ok_or(WireError::InvalidTag)) + .transpose()?; + let blksize = decoder.u64()?; Ok(FileStatus { file_type, mode, size, owner, node_info: FileNodeInfo { dev, ino, rdev }, - block_size, + blksize, }) } diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index 942a00bbc..5a3cac711 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -20,6 +20,7 @@ litebox_runner_linux_userland = { path = "../litebox_runner_linux_userland", ver tempfile = { version = "3", default-features = false } [target.'cfg(all(windows, target_arch = "x86_64"))'.dependencies] +litebox_broker_platform_windows_userland = { path = "../litebox_broker_platform_windows_userland", version = "0.1.0" } litebox_broker_transport_windows_userland = { path = "../litebox_broker_transport_windows_userland", version = "0.1.0" } litebox_runner_windows_userland = { path = "../litebox_runner_windows_userland", version = "0.1.0" } @@ -27,8 +28,10 @@ litebox_runner_windows_userland = { path = "../litebox_runner_windows_userland", lock_tracing = [ "litebox_platform/lock_tracing", "litebox_broker_platform_linux_userland/lock_tracing", + "litebox_broker_platform_windows_userland/lock_tracing", "litebox_runner_linux_userland/lock_tracing", ] +aarch64_virtualize_x18 = ["litebox_runner_linux_userland/aarch64_virtualize_x18"] [[bin]] name = "litebox-broker-userland" diff --git a/litebox_broker_userland/src/builder.rs b/litebox_broker_userland/src/builder.rs index 93a664a74..e4268b421 100644 --- a/litebox_broker_userland/src/builder.rs +++ b/litebox_broker_userland/src/builder.rs @@ -14,6 +14,7 @@ use std::fmt; use std::sync::Arc; +use litebox_broker_core::fs::{FileService, UnsupportedFileService}; use litebox_broker_core::socket::SocketProvider; use litebox_broker_core::{BrokerCore, BrokerCoreLimits, BrokerError, PolicyEngine}; @@ -76,6 +77,7 @@ impl From for BrokerBuildError { pub struct BrokerCoreBuilder { policy: PolicyEngine, limits: BrokerCoreLimits, + fs: Arc, } impl BrokerCoreBuilder { @@ -86,6 +88,7 @@ impl BrokerCoreBuilder { Self { policy, limits: BrokerCoreLimits::DEFAULT, + fs: Arc::new(UnsupportedFileService), } } @@ -96,6 +99,13 @@ impl BrokerCoreBuilder { self } + /// Installs the broker-authoritative file service. + #[must_use] + pub fn with_file_service(mut self, fs: Arc) -> Self { + self.fs = fs; + self + } + /// Applies platform setup and constructs the [`BrokerCore`]. /// /// A broker process may construct only one broker core for its process @@ -116,7 +126,7 @@ impl BrokerCoreBuilder { socket_provider, Arc::new(UserlandRandomProvider), Arc::new(UserlandStdioProvider::new()?), - Arc::new(litebox_broker_core::fs::UnsupportedFileService), + self.fs, )?; Ok(broker) } diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index 156e7b5d2..b96bdb6cf 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -14,6 +14,7 @@ use std::time::{Duration, Instant}; use clap::Parser as _; use litebox_broker_core::socket::HOST_GATEWAY_IPV4_ADDRESS; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; +use litebox_broker_platform_linux_userland::LinuxSyncPrimitivesProvider; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport_linux_userland::memfd::MemfdSharedMemory; use litebox_broker_transport_linux_userland::unix_socket::{ @@ -50,7 +51,10 @@ pub(super) fn run(mut args: super::CliArgs) -> Result<(), Box> { let policy = PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()).with_socket_policy( configured_socket_policy(&args.allow_tcp_destination, &args.allow_udp_destination)?, ); - let build_broker = || BrokerCoreBuilder::new(policy).build(); + let fs = super::create_file_service::( + args.fs_initial_files.as_deref(), + )?; + let build_broker = || BrokerCoreBuilder::new(policy).with_file_service(fs).build(); let broker = if args.in_process_runner { litebox_platform_linux_userland::with_guest_signals_blocked(build_broker)? } else { diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 7c2c9527e..ac64645a5 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -1,22 +1,32 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use std::borrow::Cow; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::io::{Error as IoError, ErrorKind, Result as IoResult}; use std::net::Ipv4Addr; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use std::str::FromStr; +use std::sync::Arc; use std::thread::JoinHandle; use std::time::{Duration, Instant}; use clap::Parser; +use litebox_broker_core::fs::FileService; +use litebox_broker_core::fs::composer::Composer; +use litebox_broker_core::fs::in_mem::{InMem, InitialNode}; +use litebox_broker_core::fs::overlay::Overlay; +use litebox_broker_core::fs::resolver::Resolver; +use litebox_broker_core::fs::tar_ro::{EMPTY_TAR_FILE, TarRo}; use litebox_broker_core::{ CallerCredential, DestinationPortRange, DestinationRule, Ipv4Cidr, SocketPolicy, SocketPolicyError, }; +use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use litebox_broker_protocol::socket::{Ipv4Address, Port}; +use litebox_platform::sync::RawSyncPrimitivesProvider; #[cfg(target_os = "linux")] mod linux; @@ -70,6 +80,7 @@ impl FromStr for AllowedDestination { } #[derive(Parser, Debug)] +#[allow(clippy::struct_excessive_bools)] struct CliArgs { /// Permit HTTP and HTTPS proxy requests to a hostname and destination ports. #[cfg(target_os = "linux")] @@ -105,11 +116,54 @@ struct CliArgs { conflicts_with = "in_process_runner" )] runner: Option, - /// Arguments to pass to the local runner. + /// Tar archive to mount as the broker-owned initial file system. + #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] + fs_initial_files: Option, + /// Opaque arguments to pass to the local runner without interpretation. #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true, value_hint = clap::ValueHint::CommandWithArguments)] runner_arguments: Vec, } +fn create_file_service(initial_files: Option<&Path>) -> IoResult> +where + Platform: RawSyncPrimitivesProvider, +{ + let writable_directory = |owner| InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner, + }; + let entries = vec![ + ("/tmp".to_owned(), writable_directory(UserInfo::ROOT)), + ("/registry".to_owned(), writable_directory(UserInfo::ROOT)), + ]; + + let tar_data = match initial_files { + Some(path) => { + if path.extension().and_then(|extension| extension.to_str()) != Some("tar") { + return Err(IoError::new( + ErrorKind::InvalidInput, + format!("expected a .tar file, found {}", path.display()), + )); + } + Cow::Owned(std::fs::read(path)?) + } + None => Cow::Borrowed(EMPTY_TAR_FILE), + }; + let in_mem = InMem::::new_initialized(entries); + let backend = Composer::builder() + .mount_nestable("/", |allocators| { + Overlay::::new( + in_mem, + TarRo::new(tar_data, allocators.next()), + allocators.next(), + ) + }) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .map_err(|_| IoError::other("failed to construct broker file service"))?; + Ok(Arc::new(Resolver::::new(backend))) +} + fn runner_command_arguments( args: &CliArgs, control_channel: &OsStr, diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index 8f5ef0919..cf8eb18a2 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -13,6 +13,7 @@ use std::time::Instant; use clap::Parser as _; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; +use litebox_broker_platform_windows_userland::WindowsSyncPrimitivesProvider; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport_windows_userland::named_pipe::{ WindowsNamedPipeHostSetupChannel, WindowsNamedPipeListener, WindowsNamedPipeStream, @@ -26,12 +27,15 @@ use super::{SETUP_TIMEOUT, configured_socket_policy}; pub(super) fn run(args: super::CliArgs) -> Result<(), Box> { let control_pipe = unique_control_pipe_name(); let control_listener = WindowsNamedPipeListener::bind(&control_pipe)?; - let broker = BrokerCoreBuilder::new( - PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()).with_socket_policy( - configured_socket_policy(&args.allow_tcp_destination, &args.allow_udp_destination)?, - ), - ) - .build()?; + let policy = PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()).with_socket_policy( + configured_socket_policy(&args.allow_tcp_destination, &args.allow_udp_destination)?, + ); + let fs = super::create_file_service::( + args.fs_initial_files.as_deref(), + )?; + let broker = BrokerCoreBuilder::new(policy) + .with_file_service(fs) + .build()?; if args.in_process_runner { debug_assert!(args.unstable); diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index f2180b31c..50dd990f8 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -42,10 +42,11 @@ fn run_parent_test() { // 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 test_executable = std::env::current_exe().unwrap(); let mut event_command = Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")); event_command .arg("--runner") - .arg(std::env::current_exe().unwrap()) + .arg(&test_executable) .arg(RUNNER_ARGUMENT); wait_for_broker(event_command); @@ -73,7 +74,7 @@ fn run_parent_test() { .arg("--allow-udp-destination") .arg(format!("{gateway}/32:{udp_port}")) .arg("--runner") - .arg(std::env::current_exe().unwrap()) + .arg(test_executable) .arg(NETWORK_RUNNER_ARGUMENT) .arg(tcp_port.to_string()) .arg(udp_port.to_string()); diff --git a/litebox_common_linux/Cargo.toml b/litebox_common_linux/Cargo.toml index 1606bad73..bc1208b28 100644 --- a/litebox_common_linux/Cargo.toml +++ b/litebox_common_linux/Cargo.toml @@ -8,6 +8,7 @@ bitflags = "2.9.0" cfg-if = "1.0.0" elf = { version = "0.8.0", default-features = false } litebox = { path = "../litebox/", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } thiserror = { version = "2.0.6", default-features = false } int-enum = "1.2.0" syscalls = { version = "0.6", default-features = false } diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 9d57b82de..18a7c4ada 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -338,6 +338,8 @@ impl From for Errno { fn from(value: litebox::fs::errors::FileStatusError) -> Self { match value { litebox::fs::errors::FileStatusError::PathError(path_error) => path_error.into(), + litebox::fs::errors::FileStatusError::ClosedFd => Errno::EBADF, + litebox::fs::errors::FileStatusError::Io => Errno::EIO, _ => unimplemented!(), } } diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index c29e43cf8..1eaabda6e 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -6,13 +6,11 @@ #![no_std] #![allow(non_camel_case_types)] -use core::ffi::c_char; +use core::ffi::{c_char, c_uint}; use core::time::Duration; use int_enum::IntEnum; -use litebox::{ - fs::OFlags, - utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, -}; +use litebox::utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}; +use litebox_broker_protocol::fs::{FileMode, FileNodeInfo, FileStatus, FileType, FileUser}; use syscalls::Sysno; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -60,6 +58,103 @@ pub const CLOCK_MONOTONIC_COARSE: i32 = 6; /// the current working directory. pub const AT_FDCWD: i32 = -100; +bitflags::bitflags! { + /// `O_*` constants for use with open, ... + #[repr(transparent)] + #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] + pub struct OFlags: c_uint { + /// `O_RDONLY`: read-only + const RDONLY = 0x0; + /// `O_WRONLY`: write-only + const WRONLY = 0x1; + /// `O_RDWR`: read/write. + /// + /// This is not equal to `RDONLY | WRONLY`. It's a distinct flag. + const RDWR = 0x2; + /// `O_APPEND`: append mode + const APPEND = 0x400; + /// `O_ASYNC`: signal-driven I/O + const ASYNC = 0x2000; + /// `O_CLOEXEC`: close-on-exec flag + const CLOEXEC = 0x80000; + /// `O_CREAT`: if path does not exist, create it as a regular file + const CREAT = 0x40; + /// `O_DIRECT`: try to minimize cache effects of I/O + #[cfg(target_arch = "x86_64")] + const DIRECT = 0x4000; + #[cfg(target_arch = "aarch64")] + const DIRECT = 0x10000; + /// `O_DIRECTORY`: fail if not a directory + #[cfg(target_arch = "x86_64")] + const DIRECTORY = 0x10000; + #[cfg(target_arch = "aarch64")] + const DIRECTORY = 0x4000; + /// `O_DSYNC`: write operations on the file will complete according to the requirements of + /// synchronized I/O *data* integrity completion. + const DSYNC = 0x1000; + /// `O_EXCL`: exclusive use + const EXCL = 0x80; + /// `O_LARGEFILE`: allow large file support + #[cfg(target_arch = "x86_64")] + const LARGEFILE = 0x8000; + #[cfg(target_arch = "aarch64")] + const LARGEFILE = 0x20000; + /// `O_NOATIME`: do not update access time + const NOATIME = 0x40000; + /// `O_NOCTTY`: do not assign controlling terminal + const NOCTTY = 0x100; + /// `O_NOFOLLOW`: fail if the path does not point to a regular file + #[cfg(target_arch = "x86_64")] + const NOFOLLOW = 0x20000; + #[cfg(target_arch = "aarch64")] + const NOFOLLOW = 0x8000; + /// `O_NDELAY`: non-blocking mode (same as NONBLOCK) + const NDELAY = 0x800; + /// `O_NONBLOCK`: non-blocking mode (same as NDELAY) + const NONBLOCK = 0x800; + /// `O_PATH`: open a file descriptor for path resolution only + const PATH = 0x200000; + /// `O_SYNC`: write operations on the file will complete according to the requirements of + /// synchronized I/O file integrity completion (by contrast with the synchronized I/O data + /// integrity completion provided by `O_DSYNC`.) + const SYNC = 0x101000; + /// `O_TMPFILE`: create an unnamed temporary file + #[cfg(target_arch = "x86_64")] + const TMPFILE = 0x410000; + #[cfg(target_arch = "aarch64")] + const TMPFILE = 0x404000; + /// `O_TRUNC`: truncate the file to zero length + const TRUNC = 0x200; + /// + const _ = !0; + + /// All file status flags + access modes + const STATUS_FLAGS_MASK = Self::APPEND.bits() + | Self::NONBLOCK.bits() + | Self::DSYNC.bits() + | Self::ASYNC.bits() + | Self::DIRECT.bits() + | Self::LARGEFILE.bits() + | Self::NOATIME.bits() + | Self::SYNC.bits() + | Self::PATH.bits() + | Self::RDONLY.bits() + | Self::WRONLY.bits() + | Self::RDWR.bits(); + } +} + +impl From for OFlags { + fn from(flags: litebox::pipes::Flags) -> Self { + let mut oflags = OFlags::empty(); + oflags.set( + OFlags::NONBLOCK, + flags.contains(litebox::pipes::Flags::NON_BLOCKING), + ); + oflags + } +} + /// Encoding for ioctl commands. pub mod ioctl { /// The number of bits allocated for the ioctl command number field. @@ -250,12 +345,12 @@ pub enum InodeType { Socket = 0o140000, } -impl From for InodeType { - fn from(value: litebox::fs::FileType) -> Self { +impl From for InodeType { + fn from(value: FileType) -> Self { match value { - litebox::fs::FileType::RegularFile => InodeType::File, - litebox::fs::FileType::Directory => InodeType::Dir, - litebox::fs::FileType::CharacterDevice => InodeType::CharDevice, + FileType::RegularFile => InodeType::File, + FileType::Directory => InodeType::Dir, + FileType::CharacterDevice => InodeType::CharDevice, _ => unimplemented!(), } } @@ -281,12 +376,12 @@ pub enum DirentType { Socket = 12, } -impl From for DirentType { - fn from(value: litebox::fs::FileType) -> Self { +impl From for DirentType { + fn from(value: FileType) -> Self { match value { - litebox::fs::FileType::RegularFile => DirentType::Regular, - litebox::fs::FileType::Directory => DirentType::Directory, - litebox::fs::FileType::CharacterDevice => DirentType::CharDevice, + FileType::RegularFile => DirentType::Regular, + FileType::Directory => DirentType::Directory, + FileType::CharacterDevice => DirentType::CharDevice, _ => unimplemented!(), } } @@ -368,42 +463,43 @@ pub struct IoReadVec { /// `iovec` struct for both read and write pub type IoVec = IoReadVec; -impl From for FileStat { - fn from(value: litebox::fs::FileStatus) -> Self { +impl TryFrom for FileStat { + type Error = errno::Errno; + + fn try_from(value: FileStatus) -> Result { // TODO: add more fields - let litebox::fs::FileStatus { + let FileStatus { file_type, mode, size, - owner: litebox::fs::UserInfo { user, group }, - node_info: litebox::fs::NodeInfo { dev, ino, rdev }, - blksize, + owner: FileUser { user, group }, + node_info: FileNodeInfo { dev, ino, rdev }, + blksize: block_size, .. } = value; - Self { - st_dev: <_>::try_from(dev).unwrap(), - st_ino: <_>::try_from(ino).unwrap(), + // Linux exposes signed sizes even where the Rust ABI struct uses `usize`. + let size = i64::try_from(size).map_err(|_| errno::Errno::EOVERFLOW)?; + #[cfg(target_arch = "x86_64")] + let block_size = i64::try_from(block_size).map_err(|_| errno::Errno::EOVERFLOW)?; + Ok(Self { + st_dev: dev, + st_ino: ino, st_nlink: 1, - st_mode: (mode.bits() | InodeType::from(file_type) as u32).trunc(), + st_mode: (u32::from(mode.bits()) | InodeType::from(file_type) as u32).trunc(), st_uid: <_>::from(user), st_gid: <_>::from(group), - st_rdev: rdev - .map(|r| <_>::try_from(r.get()).unwrap()) - .unwrap_or_default(), + st_rdev: rdev.map_or(0, core::num::NonZeroU64::get), #[cfg(target_arch = "x86_64")] - #[allow(clippy::cast_possible_wrap)] - st_size: size, + st_size: usize::try_from(size).map_err(|_| errno::Errno::EOVERFLOW)?, #[cfg(target_arch = "aarch64")] - #[allow(clippy::cast_possible_wrap)] - st_size: size as i64, + st_size: size, #[cfg(target_arch = "x86_64")] - st_blksize: blksize, + st_blksize: usize::try_from(block_size).map_err(|_| errno::Errno::EOVERFLOW)?, #[cfg(target_arch = "aarch64")] - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - st_blksize: blksize as i32, + st_blksize: i32::try_from(block_size).map_err(|_| errno::Errno::EOVERFLOW)?, st_blocks: 0, ..Default::default() - } + }) } } @@ -509,35 +605,36 @@ fn dev_minor(dev: u64) -> u32 { ((dev & 0xff) | ((dev >> 12) & !0xff)).trunc() } -impl From for Statx { - fn from(value: litebox::fs::FileStatus) -> Self { - let litebox::fs::FileStatus { +impl TryFrom for Statx { + type Error = errno::Errno; + + fn try_from(value: FileStatus) -> Result { + let FileStatus { file_type, mode, size, - owner: litebox::fs::UserInfo { user, group }, - node_info: litebox::fs::NodeInfo { dev, ino, rdev }, - blksize, + owner: FileUser { user, group }, + node_info: FileNodeInfo { dev, ino, rdev }, + blksize: block_size, .. } = value; - let dev = dev as u64; - let rdev = rdev.map_or(0u64, |r| r.get() as u64); - Self { + let rdev = rdev.map_or(0, core::num::NonZeroU64::get); + Ok(Self { stx_mask: StatxMask::STATX_BASIC_FILLED.bits(), - stx_blksize: blksize.trunc(), + stx_blksize: u32::try_from(block_size).map_err(|_| errno::Errno::EOVERFLOW)?, stx_nlink: 1, stx_uid: u32::from(user), stx_gid: u32::from(group), - stx_mode: (mode.bits() | InodeType::from(file_type) as u32).trunc(), - stx_ino: ino as u64, - stx_size: size as u64, + stx_mode: (u32::from(mode.bits()) | InodeType::from(file_type) as u32).trunc(), + stx_ino: ino, + stx_size: size, stx_blocks: 0, stx_rdev_major: dev_major(rdev), stx_rdev_minor: dev_minor(rdev), stx_dev_major: dev_major(dev), stx_dev_minor: dev_minor(dev), ..Default::default() - } + }) } } @@ -680,8 +777,8 @@ bitflags::bitflags! { #[derive(Debug, Clone, Copy)] pub struct EfdFlags: core::ffi::c_uint { const SEMAPHORE = 1; - const CLOEXEC = litebox::fs::OFlags::CLOEXEC.bits(); - const NONBLOCK = litebox::fs::OFlags::NONBLOCK.bits(); + const CLOEXEC = OFlags::CLOEXEC.bits(); + const NONBLOCK = OFlags::NONBLOCK.bits(); /// const _ = !0; } @@ -1432,7 +1529,7 @@ pub struct RobustListHead { bitflags::bitflags! { #[derive(Debug)] pub struct EpollCreateFlags: core::ffi::c_uint { - const EPOLL_CLOEXEC = litebox::fs::OFlags::CLOEXEC.bits(); + const EPOLL_CLOEXEC = OFlags::CLOEXEC.bits(); /// const _ = !0; } @@ -2115,7 +2212,7 @@ pub enum SyscallRequest { Dup { oldfd: i32, newfd: Option, - flags: Option, + flags: Option, }, Socket { domain: u32, @@ -2276,8 +2373,8 @@ pub enum SyscallRequest { Openat { dirfd: i32, pathname: UserPtr, - flags: litebox::fs::OFlags, - mode: litebox::fs::Mode, + flags: OFlags, + mode: FileMode, }, Ftruncate { fd: i32, @@ -2306,7 +2403,7 @@ pub enum SyscallRequest { }, Pipe2 { pipefd: UserPtrMut, - flags: litebox::fs::OFlags, + flags: OFlags, }, Clone { args: CloneArgs, @@ -2621,7 +2718,7 @@ impl SyscallRequest { }, Sysno::faccessat2 => sys_req!(Faccessat { dirfd, pathname:*, mode, flags }), #[cfg(target_arch = "x86_64")] - Sysno::pipe => sys_req!(Pipe2 { pipefd:*, flags: { litebox::fs::OFlags::empty() } }), + Sysno::pipe => sys_req!(Pipe2 { pipefd:*, flags: { OFlags::empty() } }), Sysno::pipe2 => sys_req!(Pipe2 { pipefd:* ,flags }), Sysno::madvise => sys_req!(Madvise { addr:*, length, behavior:? }), Sysno::dup => SyscallRequest::Dup { @@ -2866,9 +2963,7 @@ impl SyscallRequest { SyscallRequest::Openat { dirfd: AT_FDCWD, pathname: ctx.sys_req_ptr(0), - flags: litebox::fs::OFlags::CREAT - | litebox::fs::OFlags::WRONLY - | litebox::fs::OFlags::TRUNC, + flags: OFlags::CREAT | OFlags::WRONLY | OFlags::TRUNC, mode: ctx.sys_req_arg(1), } } @@ -3415,8 +3510,8 @@ reinterpret_truncated_from_usize_for! { MapFlags, MRemapFlags, AccessFlags, - litebox::fs::Mode, - litebox::fs::OFlags, + FileMode, + OFlags, AtFlags, SockFlags, SendFlags, diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 13170228b..e97c865f9 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -14,14 +14,13 @@ use std::sync::atomic::{AtomicI32, AtomicU32, Ordering}; use std::time::Duration; use std::unimplemented; -use litebox::fs::OFlags; use litebox::platform::RawConstPointer as _; use litebox::platform::page_mgmt::{ CowAllocationError, FixedAddressBehavior, MemoryRegionPermissions, }; use litebox::shim::ContinueOperation; use litebox::utils::{ReinterpretSignedExt, ReinterpretUnsignedExt as _, TruncateExt}; -use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, vmap::VmapManager}; +use litebox_common_linux::{MRemapFlags, MapFlags, OFlags, ProtFlags, vmap::VmapManager}; use litebox_platform::sync::{ ImmediatelyWokenUp, RawMutex as RawMutexTrait, RawMutexProvider, UnblockedOrTimedOut, WaitWakerProvider, @@ -2862,7 +2861,7 @@ mod tests { use std::os::unix::net::UnixStream; use std::thread::sleep; - use litebox::fs::OFlags; + use litebox_common_linux::OFlags; use litebox_platform::sync::RawMutex; use crate::LinuxUserland; diff --git a/litebox_runner_linux_on_windows_userland/src/lib.rs b/litebox_runner_linux_on_windows_userland/src/lib.rs index e047d4fb2..50be8fd6c 100644 --- a/litebox_runner_linux_on_windows_userland/src/lib.rs +++ b/litebox_runner_linux_on_windows_userland/src/lib.rs @@ -7,7 +7,7 @@ extern crate alloc; -use anyhow::{Result, anyhow}; +use anyhow::{Context as _, Result}; use clap::Parser; use litebox_broker_local_userland as broker; use litebox_platform_windows_userland::WindowsUserland as Platform; @@ -49,8 +49,9 @@ pub struct CliArgs { /// /// All ELF binaries should be pre-rewritten with the syscall rewriter /// (e.g., via `litebox-packager`). + /// This may be omitted when the broker was configured with `--fs-initial-files`. #[arg(long = "initial-files", value_name = "PATH_TO_TAR", value_hint = clap::ValueHint::FilePath)] - pub initial_files: PathBuf, + pub initial_files: Option, } /// Run Linux programs with LiteBox on unmodified Windows @@ -71,54 +72,26 @@ pub fn run(cli_args: CliArgs) -> Result<()> { ) .init(); - let tar_file = &cli_args.initial_files; - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - anyhow::bail!("Expected a .tar file, found {}", tar_file.display()); - } - let tar_data = std::fs::read(tar_file) - .map_err(|e| anyhow!("Could not read tar file at {}: {}", tar_file.display(), e))?; - let platform = Platform::new(); - let broker_connection = cli_args + let control_pipe = cli_args .broker_control_channel .as_deref() - .map(broker::connect) - .transpose()?; - let shim_builder = if let Some(broker_connection) = broker_connection { - let broker::BrokerConnection { - local, - notifications, - } = broker_connection; - let litebox = litebox::LiteBox::new_with_broker_local(platform, local); - broker::start_notification_receiver( - notifications, - litebox.broker_notification_dispatcher(), - litebox.broker_failure_dispatcher(), - )?; - litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox) - } else { - litebox_shim_linux::LinuxShimBuilder::new(platform) - }; + .context("file operations require --broker-control-channel")?; + let broker::BrokerConnection { + local, + notifications, + } = broker::connect(control_pipe)?; + let litebox = litebox::LiteBox::new_with_broker_local(platform, local); + broker::start_notification_receiver( + notifications, + litebox.broker_notification_dispatcher(), + litebox.broker_failure_dispatcher(), + )?; + let shim_builder = litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox); // The program path is a Unix-style path inside the tar archive. let prog_path = &cli_args.program_and_arguments[0]; - let initial_file_system = { - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo { - user: 1000, - group: 1000, - }, - }, - )]); - - shim_builder.default_fs(in_mem, tar_data.into()) - }; - let initial_file_system = std::sync::Arc::new(initial_file_system); - let shim = shim_builder.build(); let argv = cli_args .program_and_arguments @@ -142,13 +115,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { }; let program = shim - .load_program( - initial_file_system, - platform.init_task(), - prog_path, - argv, - envp, - ) + .load_program(platform.init_task(), prog_path, argv, envp) .unwrap(); unsafe { litebox_platform_windows_userland::run_thread( diff --git a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs deleted file mode 100644 index 07a8ea88d..000000000 --- a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#![cfg(all(target_os = "windows", target_arch = "x86_64"))] - -use std::ffi::CString; - -use litebox::fs::{Mode, OFlags}; -use litebox_platform_windows_userland::WindowsUserland as Platform; - -pub struct TestLauncher { - platform: &'static Platform, - shim_builder: litebox_shim_linux::LinuxShimBuilder, - fs: litebox_shim_linux::DefaultFS, - context: litebox::fs::resolver::Context, -} - -impl TestLauncher { - pub fn init_platform( - tar_data: &'static [u8], - initial_dirs: &[&str], - initial_files: &[&str], - ) -> Self { - let platform = Platform::new(); - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let tar_data = if tar_data.is_empty() { - litebox::fs::tar_ro::EMPTY_TAR_FILE.into() - } else { - tar_data.into() - }; - let fs = shim_builder.default_fs(in_mem, tar_data); - let mut this = Self { - platform, - shim_builder, - fs, - context: litebox::fs::resolver::Context::new(), - }; - - for each in initial_dirs { - this.install_dir(each); - } - for each in initial_files { - let data = std::fs::read(each).unwrap(); - this.install_file(data, each); - } - - this - } - - pub fn install_dir(&mut self, path: &str) { - self.fs - .mkdir(&self.context, path, Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create directory"); - } - - pub fn install_file(&mut self, contents: Vec, out: &str) { - let fd = self - .fs - .open( - &self.context, - out, - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXG | Mode::RWXO | Mode::RWXU, - ) - .unwrap(); - self.fs.write(&fd, &contents, None).unwrap(); - self.fs.close(&fd).unwrap(); - } - - pub fn test_load_exec_common(self, executable_path: &str) { - let fs = std::sync::Arc::new(self.fs); - let argv = vec![ - CString::new(executable_path).unwrap(), - CString::new("hello").unwrap(), - ]; - let envp = vec![CString::new("PATH=/bin").unwrap()]; - let shim = self.shim_builder.build(); - let program = shim - .load_program(fs, self.platform.init_task(), executable_path, argv, envp) - .unwrap(); - unsafe { - litebox_platform_windows_userland::run_thread( - program.entrypoints, - &mut litebox_common_linux::PtRegs::default(), - ); - } - assert_eq!(program.process.wait(), 0); - } -} diff --git a/litebox_runner_linux_on_windows_userland/tests/loader.rs b/litebox_runner_linux_on_windows_userland/tests/loader.rs index bb79fd60a..ff04c5bd5 100644 --- a/litebox_runner_linux_on_windows_userland/tests/loader.rs +++ b/litebox_runner_linux_on_windows_userland/tests/loader.rs @@ -9,8 +9,6 @@ #![cfg(all(target_os = "windows", target_arch = "x86_64"))] -mod common; - #[expect( unused, reason = "This code snippet is just used to illustrate the source code of the `hello_exec_nolibc` test." @@ -156,25 +154,21 @@ fn test_static_linked_prog_with_rewriter() { test_dir.push("tests/test-bins"); let prog_name = "hello_world_static"; - let prog_name_hooked = format!("{prog_name}.hooked"); let path = test_dir.join(prog_name); let executable_data = litebox_syscall_rewriter::rewrite_binary(&std::fs::read(path).unwrap(), None).unwrap(); - let executable_path = format!("/{prog_name_hooked}"); - - let mut launcher = common::TestLauncher::init_platform(&[], &[], &[]); - launcher.install_file(executable_data, &executable_path); - launcher.test_load_exec_common(&executable_path); + let (broker, runner) = build_windows_broker(); + run_prog_with_windows_broker(&broker, &runner, prog_name, &[], Some(&executable_data)); } #[test] fn test_programs_with_windows_broker() { let (broker, runner) = build_windows_broker(); - run_prog_with_windows_broker(&broker, &runner, "hello_world_static", &[]); - run_prog_with_windows_broker(&broker, &runner, "pipe_broker", &[]); - run_prog_with_windows_broker(&broker, &runner, "hello_world_dyn", &DYNAMIC_LIBS); - run_prog_with_windows_broker(&broker, &runner, "hello_thread", &DYNAMIC_LIBS); + run_prog_with_windows_broker(&broker, &runner, "hello_world_static", &[], None); + run_prog_with_windows_broker(&broker, &runner, "pipe_broker", &[], None); + run_prog_with_windows_broker(&broker, &runner, "hello_world_dyn", &DYNAMIC_LIBS, None); + run_prog_with_windows_broker(&broker, &runner, "hello_thread", &DYNAMIC_LIBS, None); } const DYNAMIC_LIBS: [(&str, &str); 2] = [ @@ -219,13 +213,23 @@ fn run_prog_with_windows_broker( runner: &std::path::Path, exec_name: &str, libs: &[(&str, &str)], + pre_rewritten_exec: Option<&[u8]>, ) { let test_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/test-bins"); - let tar_path = - std::path::Path::new(env!("OUT_DIR")).join(format!("broker_{exec_name}_rootfs.tar")); + let variant = if pre_rewritten_exec.is_some() { + "_pre_rewritten" + } else { + "" + }; + let tar_path = std::path::Path::new(env!("OUT_DIR")) + .join(format!("broker_{exec_name}{variant}_rootfs.tar")); let mut tar = tar::Builder::new(std::fs::File::create(&tar_path).unwrap()); let exec_path = format!("bin/{exec_name}.hooked"); - append_rewritten_file(&mut tar, &test_dir.join(exec_name), &exec_path); + if let Some(executable) = pre_rewritten_exec { + append_file(&mut tar, executable, &exec_path); + } else { + append_rewritten_file(&mut tar, &test_dir.join(exec_name), &exec_path); + } for (file, prefix) in libs { append_rewritten_file( &mut tar, @@ -242,10 +246,12 @@ fn run_prog_with_windows_broker( } arguments.extend([ "--initial-files".into(), - tar_path.into_os_string(), + tar_path.clone().into_os_string(), format!("/{exec_path}").into(), ]); let status = std::process::Command::new(broker) + .arg("--fs-initial-files") + .arg(&tar_path) .arg("--runner") .arg(runner) .args(arguments) @@ -261,15 +267,19 @@ fn append_rewritten_file( ) { let rewritten = litebox_syscall_rewriter::rewrite_binary(&std::fs::read(source).unwrap(), None).unwrap(); + append_file(tar, &rewritten, archive_path); +} + +fn append_file(tar: &mut tar::Builder, contents: &[u8], archive_path: &str) { let mut header = tar::Header::new_ustar(); - header.set_size(rewritten.len() as u64); + header.set_size(contents.len() as u64); header.set_mode(0o755); header.set_uid(0); header.set_gid(0); header.set_mtime(0); header.set_entry_type(tar::EntryType::Regular); header.set_cksum(); - tar.append_data(&mut header, archive_path, rewritten.as_slice()) + tar.append_data(&mut header, archive_path, contents) .unwrap(); } @@ -344,11 +354,6 @@ fn run_dynamic_linked_prog_with_rewriter( tar.finish().unwrap(); println!("Tar file created at: {}", tar_target_file.to_str().unwrap()); - let binary_path = std::env::var("NEXTEST_BIN_EXE_litebox_runner_linux_on_windows_userland") - .unwrap_or_else(|_| { - env!("CARGO_BIN_EXE_litebox_runner_linux_on_windows_userland").to_string() - }); - // The program path refers to the tar-internal path. let prog_tar_path = format!("/bin/{prog_name_hooked}"); @@ -364,9 +369,14 @@ fn run_dynamic_linked_prog_with_rewriter( ]; args.push(&prog_tar_path); args.extend_from_slice(cmd_args); - - let mut command = std::process::Command::new(&binary_path); - command.args(&args); + let (broker, runner) = build_windows_broker(); + let mut command = std::process::Command::new(broker); + command + .arg("--fs-initial-files") + .arg(&tar_target_file) + .arg("--runner") + .arg(runner) + .args(&args); println!("Running `{command:?}`"); let status = command .status() diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 2c44200da..0589bc789 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -16,7 +16,6 @@ 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" } litebox_syscall_rewriter = { version = "0.1.0", path = "../litebox_syscall_rewriter" } -memmap2 = "0.9.8" tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = ["backend_tracing"] } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 8db2640e7..bfb1658f2 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -3,15 +3,14 @@ use anyhow::{Context as _, Result, anyhow}; use clap::Parser; -use litebox::fs::Mode; use litebox_platform_linux_userland::LinuxUserland as Platform; -use memmap2::Mmap; use std::os::linux::fs::MetadataExt as _; use std::path::{Path, PathBuf}; use litebox_broker_local_userland as broker; - -extern crate alloc; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileMode as Mode, FileOpenFlags, FileType, FileUser as UserInfo, +}; // 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. @@ -74,7 +73,7 @@ pub struct CliArgs { /// This is used by `litebox-packager` to create fully self-contained tar bundles. #[arg( long = "program-from-tar", - requires_all = ["unstable", "initial_files"], + requires = "unstable", conflicts_with = "rewrite_syscalls", help_heading = "Unstable Options" )] @@ -100,30 +99,6 @@ pub struct CliArgs { pub broker_proxy_url: Option, } -struct MmappedFile { - data: &'static [u8], - abs_path: PathBuf, -} - -fn mmapped_file(path: impl AsRef) -> Result { - let path = path.as_ref(); - let abs_path = std::path::absolute(path) - .map_err(|e| anyhow!("Could not get absolute path for {}: {}", path.display(), e))?; - let file = std::fs::File::open(&abs_path)?; - let data = { - // SAFETY: We assume that the file given to us is not going to change _externally_ while in - // the middle of execution. Since we are mapping it as read-only and mapping it only once, - // we are not planning to change it either. With both these in mind, this call is safe. - // - // We need to leak the `Mmap` object, so that it stays alive until the end of the program, - // rather than being unmapped at function finish (i.e., to get the `'static` lifetime). - Box::leak(Box::new(unsafe { Mmap::map(&file) }.map_err(|e| { - anyhow!("Could not read tar file at {}: {}", path.display(), e) - })?)) - }; - Ok(MmappedFile { data, abs_path }) -} - /// Run Linux programs with LiteBox on unmodified Linux /// /// # Panics @@ -164,114 +139,53 @@ pub fn run(cli_args: CliArgs) -> Result { ); } - let broker_connection = match cli_args.broker_control_channel.as_deref() { - Some(control_socket_path) => { - Some(litebox_platform_linux_userland::with_guest_signals_blocked( - || broker::connect(control_socket_path), - )?) - } - None => None, - }; - - let mut cow_eligible_regions: Vec = Vec::new(); - - // When --program-from-tar is set, the program binary is already in the tar file, - // so we skip reading it from the host filesystem and skip extracting ancestor modes. - #[allow(clippy::type_complexity)] - let (ancestor_modes_and_users, prog_data): ( - Vec<(litebox::fs::Mode, u32)>, - Option>, - ) = if cli_args.program_from_tar { - (Vec::new(), None) + let prog = if cli_args.program_from_tar { + PathBuf::from(&cli_args.program_and_arguments[0]) } else { - let prog = std::path::absolute(Path::new(&cli_args.program_and_arguments[0])).unwrap(); - if !prog.exists() { - let mut msg = format!("program not found on host filesystem: {}", prog.display()); - if cli_args.initial_files.is_some() { - msg.push_str( - "\nhint: if the program is inside the tar archive, \ - add --program-from-tar", - ); - } - anyhow::bail!(msg); - } - let ancestors: Vec<_> = prog.ancestors().collect(); - let modes: Vec<_> = ancestors - .into_iter() - .rev() - .skip(1) - .map(|path| { - let metadata = path.metadata().unwrap(); - ( - litebox::fs::Mode::from_bits(metadata.st_mode()).unwrap(), - metadata.st_uid(), - ) - }) - .collect(); - let file = mmapped_file(&prog)?; - let data = if cli_args.rewrite_syscalls { - #[cfg(target_arch = "aarch64")] - let rewritten = litebox_syscall_rewriter::hook_syscalls_in_elf_with_options( - file.data, - None, - litebox_syscall_rewriter::RewriteOptions::new( - litebox_syscall_rewriter::TargetHost::Linux, - cfg!(feature = "aarch64_virtualize_x18"), - ), + std::path::absolute(Path::new(&cli_args.program_and_arguments[0])).with_context(|| { + format!( + "could not resolve program path {}", + cli_args.program_and_arguments[0] ) - .with_context(|| format!("failed to rewrite {}", prog.display()))?; - #[cfg(not(target_arch = "aarch64"))] - let rewritten = litebox_syscall_rewriter::hook_syscalls_in_elf(file.data, None) - .with_context(|| format!("failed to rewrite {}", prog.display()))?; - rewritten.into() - } else { - let data = file.data.into(); - cow_eligible_regions.push(file); - data - }; - (modes, Some(data)) - }; - let tar_data: &'static [u8] = if let Some(tar_file) = cli_args.initial_files.as_ref() { - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - anyhow::bail!("Expected a .tar file, found {}", tar_file.display()); - } - mmapped_file(tar_file)?.data - } else { - litebox::fs::tar_ro::EMPTY_TAR_FILE + })? }; + let host_program = (!cli_args.program_from_tar) + .then(|| prepare_host_program(&cli_args, &prog)) + .transpose()?; // TODO(jb): Clean up platform initialization once we have https://github.com/MSRSSP/litebox/issues/24 let platform = Platform::new(); - for file in cow_eligible_regions { - 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::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()); - litebox_platform_linux_userland::with_guest_signals_blocked(|| { - broker::start_notification_receiver( - broker_notifications, - broker_association_coordinator, - litebox.broker_notification_dispatcher(), - ) - })?; - litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox) - } else { - litebox_shim_linux::LinuxShimBuilder::new(platform) - }; + let control_socket_path = cli_args + .broker_control_channel + .as_deref() + .context("file operations require --broker-control-channel")?; + let broker::BrokerConnection { + local: broker_local, + notifications: broker_notifications, + coordinator: broker_association_coordinator, + positional_io_fds, + shutdown_fd, + } = litebox_platform_linux_userland::with_guest_signals_blocked(|| { + broker::connect(control_socket_path) + })?; + 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()); + litebox_platform_linux_userland::with_guest_signals_blocked(|| { + broker::start_notification_receiver( + broker_notifications, + broker_association_coordinator, + litebox.broker_notification_dispatcher(), + ) + })?; + let shim_builder = litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox); + if let Some(host_program) = host_program { + stage_host_program(shim_builder.litebox(), host_program)?; + } // SAFETY: `gettid` takes no pointer arguments and has no Rust-side aliasing requirements. let tid = unsafe { libc::syscall(libc::SYS_gettid) } .try_into() @@ -286,91 +200,6 @@ pub fn run(cli_args: CliArgs) -> Result { gid: u32::from(DEFAULT_GUEST_GID), egid: u32::from(DEFAULT_GUEST_GID), }; - let initial_file_system = { - // The in-memory layer is pre-populated at construction, which lets us set up root-owned - // directories and files without ever acting as root at runtime. - // - // A host uid of 0 anywhere along the path means the entry stays root-owned; as soon as a - // path component belongs to a non-root host user, that component and everything below it - // is owned by the guest user. - let owner_of = |parent_host_user: u32, host_user: u32| { - if parent_host_user == 0 && host_user == 0 { - litebox::fs::UserInfo::ROOT - } else { - litebox::fs::UserInfo { - user: DEFAULT_GUEST_UID, - group: DEFAULT_GUEST_GID, - } - } - }; - let mut entries: Vec<(String, litebox::fs::in_mem::InitialNode)> = Vec::new(); - - // When loading the program from the tar, we don't need to create ancestor - // directories or write the program binary into the in-memory FS -- the program - // is already in the tar layer. - 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 mut prev_user = 0; - for (path, &mode_and_user) in ancestors - .into_iter() - .skip(1) - .rev() - .skip(1) - .zip(&ancestor_modes_and_users) - { - entries.push(( - path.to_str().unwrap().to_owned(), - litebox::fs::in_mem::InitialNode::Directory { - mode: mode_and_user.0, - owner: owner_of(prev_user, mode_and_user.1), - }, - )); - prev_user = mode_and_user.1; - } - let last = ancestor_modes_and_users.last().ok_or_else(|| { - anyhow!("program path has no ancestor directories (is it the root path?)") - })?; - entries.push(( - prog.to_str().unwrap().to_owned(), - litebox::fs::in_mem::InitialNode::File { - mode: last.0, - owner: owner_of(prev_user, last.1), - data: prog_data, - }, - )); - } - - let tmp_mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; - if let Some((_, node)) = entries.iter_mut().find(|(path, _)| path == "/tmp") { - // `/tmp` is an ancestor of the program, so it keeps the owner derived above and only - // has its mode widened. - let litebox::fs::in_mem::InitialNode::Directory { mode, .. } = node else { - unreachable!("ancestors are always directories") - }; - *mode = tmp_mode; - } else { - entries.push(( - "/tmp".to_owned(), - litebox::fs::in_mem::InitialNode::Directory { - mode: tmp_mode, - owner: litebox::fs::UserInfo::ROOT, - }, - )); - } - - let in_mem = litebox::fs::in_mem::InMem::new_initialized(entries); - shim_builder.default_fs(in_mem, tar_data.into()) - }; - - // We need to get the file path before enabling seccomp. - // For --program-from-tar the path is already validated as absolute above, - // so use it directly instead of resolving against the host CWD. - let prog = if cli_args.program_from_tar { - PathBuf::from(&cli_args.program_and_arguments[0]) - } else { - std::path::absolute(Path::new(&cli_args.program_and_arguments[0])).unwrap() - }; let prog_path = prog.to_str().ok_or_else(|| { anyhow!( "Could not convert program path {:?} to a string", @@ -378,8 +207,6 @@ pub fn run(cli_args: CliArgs) -> Result { ) })?; - let initial_file_system = std::sync::Arc::new(initial_file_system); - let shim = shim_builder.build(); let argv = cli_args @@ -403,7 +230,7 @@ pub fn run(cli_args: CliArgs) -> Result { &broker_shutdown_fds, ); - let program = shim.load_program(initial_file_system, task_params, prog_path, argv, envp)?; + let program = shim.load_program(task_params, prog_path, argv, envp)?; #[cfg(feature = "lock_tracing")] litebox::sync::start_recording(); @@ -432,6 +259,195 @@ pub fn run(cli_args: CliArgs) -> Result { Ok(program.process.wait()) } +struct PreparedHostProgram { + path: String, + directories: Vec<(String, Mode, UserInfo)>, + mode: Mode, + owner: UserInfo, + data: Vec, +} + +fn prepare_host_program(cli_args: &CliArgs, program: &Path) -> Result { + if !program.exists() { + let mut message = format!( + "program not found on host filesystem: {}", + program.display() + ); + if cli_args.initial_files.is_some() { + message.push_str( + "\nhint: if the program is inside the tar archive, add --program-from-tar", + ); + } + anyhow::bail!(message); + } + + let ancestors: Vec<_> = program.ancestors().skip(1).collect(); + let mut previous_user = 0; + let mut directories = Vec::new(); + for path in ancestors.into_iter().rev().skip(1) { + let metadata = path + .metadata() + .with_context(|| format!("failed to read metadata for {}", path.display()))?; + directories.push(( + path_to_string(path)?, + Mode::from_u32_bits_truncate(metadata.st_mode()), + guest_owner(previous_user, metadata.st_uid()), + )); + previous_user = metadata.st_uid(); + } + + let mut data = std::fs::read(program) + .with_context(|| format!("failed to read program {}", program.display()))?; + if cli_args.rewrite_syscalls { + #[cfg(target_arch = "aarch64")] + { + data = litebox_syscall_rewriter::hook_syscalls_in_elf_with_options( + &data, + None, + litebox_syscall_rewriter::RewriteOptions::new( + litebox_syscall_rewriter::TargetHost::Linux, + cfg!(feature = "aarch64_virtualize_x18"), + ), + ) + .with_context(|| format!("failed to rewrite {}", program.display()))?; + } + #[cfg(not(target_arch = "aarch64"))] + { + data = litebox_syscall_rewriter::hook_syscalls_in_elf(&data, None) + .with_context(|| format!("failed to rewrite {}", program.display()))?; + } + } + + let metadata = program + .metadata() + .with_context(|| format!("failed to read metadata for {}", program.display()))?; + Ok(PreparedHostProgram { + path: path_to_string(program)?, + directories, + mode: Mode::from_u32_bits_truncate(metadata.st_mode()), + owner: guest_owner(previous_user, metadata.st_uid()), + data, + }) +} + +fn stage_host_program( + litebox: &litebox::LiteBox, + program: PreparedHostProgram, +) -> Result<()> { + let PreparedHostProgram { + path, + directories, + mode, + owner, + data, + } = program; + let mut context = litebox::fs::Context::new(); + context.set_acting_user(UserInfo::ROOT); + + // Keep ancestors root-owned and writable until all descendants have been staged. Final + // metadata is restored from leaf to root so restrictive host modes cannot block setup. + for (path, mode, _) in &directories { + let staging_mode = *mode | Mode::RWXU; + match litebox.mkdir_file(&context, path.as_str(), staging_mode) { + Ok(()) | Err(litebox::fs::errors::MkdirError::AlreadyExists) => {} + Err(error) => { + return Err(error) + .with_context(|| format!("failed to stage program directory {path}")); + } + } + let status = litebox + .path_file_status(&context, path.as_str()) + .with_context(|| format!("failed to inspect program directory {path}"))?; + if status.file_type != FileType::Directory { + anyhow::bail!("program path component is not a directory: {path}"); + } + set_file_metadata(litebox, &context, path, staging_mode, UserInfo::ROOT)?; + } + + match litebox.unlink_file(&context, path.as_str()) { + Ok(()) + | Err(litebox::fs::errors::UnlinkError::PathError( + litebox::fs::errors::PathError::NoSuchFileOrDirectory + | litebox::fs::errors::PathError::MissingComponent, + )) => {} + Err(error) => { + return Err(error).with_context(|| format!("failed to replace host program {path}")); + } + } + let fd = litebox + .open_file( + &context, + path.as_str(), + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + Mode::RWXU, + ) + .with_context(|| format!("failed to stage host program {path}"))?; + let write_result = write_all(litebox, &fd, &path, &data); + let close_result = litebox + .close_file(&fd) + .with_context(|| format!("failed to close staged host program {path}")); + write_result?; + close_result?; + set_file_metadata(litebox, &context, &path, mode, owner)?; + + for (path, mode, owner) in directories.into_iter().rev() { + set_file_metadata(litebox, &context, &path, mode, owner)?; + } + Ok(()) +} + +fn write_all( + litebox: &litebox::LiteBox, + fd: &litebox::fs::FileFd, + path: &str, + data: &[u8], +) -> Result<()> { + let mut offset = 0; + while offset < data.len() { + let written = litebox + .write_file(fd, &data[offset..], Some(offset)) + .with_context(|| format!("failed to write staged host program {path}"))?; + if written == 0 { + anyhow::bail!("failed to write staged host program {path}: write returned zero"); + } + offset += written; + } + Ok(()) +} + +fn set_file_metadata( + litebox: &litebox::LiteBox, + context: &litebox::fs::Context, + path: &str, + mode: Mode, + owner: UserInfo, +) -> Result<()> { + litebox + .chown_file(context, path, Some(owner.user), Some(owner.group)) + .with_context(|| format!("failed to set owner for staged path {path}"))?; + litebox + .chmod_file(context, path, mode) + .with_context(|| format!("failed to set mode for staged path {path}")) +} + +fn guest_owner(previous_user: u32, user: u32) -> UserInfo { + if previous_user == 0 && user == 0 { + UserInfo::ROOT + } else { + UserInfo { + user: DEFAULT_GUEST_UID, + group: DEFAULT_GUEST_GID, + } + } +} + +fn path_to_string(path: &Path) -> Result { + path.to_str() + .map(str::to_owned) + .ok_or_else(|| anyhow!("file path is not UTF-8: {}", path.display())) +} + fn apply_broker_proxy_environment(environment: &mut Vec, proxy_url: Option<&str>) { environment.retain(|entry| { let key = entry diff --git a/litebox_runner_linux_userland/tests/common/mod.rs b/litebox_runner_linux_userland/tests/common/mod.rs index bddad0925..34eea4aba 100644 --- a/litebox_runner_linux_userland/tests/common/mod.rs +++ b/litebox_runner_linux_userland/tests/common/mod.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; #[cfg(target_os = "linux")] pub mod pty; +pub(crate) mod runner; #[cfg(target_arch = "x86_64")] const MULTIARCH: &str = "x86_64-linux-gnu"; diff --git a/litebox_runner_linux_userland/tests/common/runner.rs b/litebox_runner_linux_userland/tests/common/runner.rs new file mode 100644 index 000000000..f75732845 --- /dev/null +++ b/litebox_runner_linux_userland/tests/common/runner.rs @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::{ + ffi::{OsStr, OsString}, + path::{Path, PathBuf}, +}; + +#[cfg(target_arch = "x86_64")] +const MULTIARCH_LIB_DIR: &str = "lib/x86_64-linux-gnu"; +#[cfg(target_arch = "aarch64")] +const MULTIARCH_LIB_DIR: &str = "lib/aarch64-linux-gnu"; + +#[must_use] +pub(crate) struct Runner { + command: std::process::Command, + dir_path: PathBuf, + tar_dir: PathBuf, + unique_name: String, + cmd_path: PathBuf, + cmd_args: Vec, + #[cfg(target_os = "linux")] + managed_proxy_hosts: Vec, + #[cfg(target_os = "linux")] + use_userland_broker: bool, + #[cfg(target_os = "linux")] + in_process_mode: bool, + has_run: bool, +} + +#[allow( + dead_code, + reason = "loader.rs and run.rs use different parts of this shared test helper" +)] +impl Runner { + pub(crate) fn new(target: &Path, unique_name: &str) -> Self { + Self::new_inner(target, unique_name, true) + } + + pub(crate) fn new_pre_rewritten(target: &Path, unique_name: &str) -> Self { + Self::new_inner(target, unique_name, false) + } + + fn new_inner(target: &Path, unique_name: &str, rewrite_target: bool) -> Self { + let dir_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); + + let tar_dir = dir_path.join(format!("tar_files_{unique_name}")); + let dirs_to_create = ["lib64", MULTIARCH_LIB_DIR, "lib32"]; + for dir in dirs_to_create { + std::fs::create_dir_all(tar_dir.join(dir)).unwrap(); + } + std::fs::create_dir_all(tar_dir.join("out")).unwrap(); + + let target_guest_path = std::path::absolute(target).unwrap(); + let target_dest_path = tar_dir.join(target_guest_path.strip_prefix("/").unwrap()); + if rewrite_target { + let success = super::rewrite_with_cache(target, &target_dest_path, &[]); + assert!(success, "failed to run litebox_syscall_rewriter"); + } else { + std::fs::create_dir_all(target_dest_path.parent().unwrap()).unwrap(); + std::fs::copy(target, &target_dest_path).unwrap(); + } + + let libs = super::find_dependencies(target.to_str().unwrap()); + for file in &libs { + let file_path = Path::new(file.as_str()); + let dest_path = tar_dir.join(&file[1..]); + let success = super::rewrite_with_cache(file_path, &dest_path, &[]); + assert!( + success, + "failed to run litebox_syscall_rewriter for {}", + file_path.to_str().unwrap() + ); + } + + let binary_path = std::env::var("NEXTEST_BIN_EXE_litebox_runner_linux_userland") + .unwrap_or_else(|_| env!("CARGO_BIN_EXE_litebox_runner_linux_userland").to_string()); + + let mut command = std::process::Command::new(binary_path); + command.args([ + "--unstable", + "--env", + "LD_LIBRARY_PATH=/lib64:/lib32:/lib", + "--env", + "HOME=/", + "--program-from-tar", + ]); + + Self { + command, + dir_path, + tar_dir, + cmd_path: target_guest_path, + cmd_args: Vec::new(), + #[cfg(target_os = "linux")] + managed_proxy_hosts: Vec::new(), + #[cfg(target_os = "linux")] + use_userland_broker: true, + #[cfg(target_os = "linux")] + in_process_mode: false, + has_run: false, + unique_name: unique_name.to_owned(), + } + } + + pub(crate) fn tar_dir(&self) -> &Path { + &self.tar_dir + } + + pub(crate) fn env(&mut self, env: impl AsRef) -> &mut Self { + self.command.arg("--env").arg(env); + self + } + + pub(crate) fn envs(&mut self, envs: impl IntoIterator>) -> &mut Self { + for env in envs { + self.env(env); + } + self + } + + pub(crate) fn arg(&mut self, arg: impl AsRef) -> &mut Self { + self.cmd_args.push(arg.as_ref().to_os_string()); + self + } + + pub(crate) fn args(&mut self, args: impl IntoIterator>) -> &mut Self { + for arg in args { + self.arg(arg); + } + self + } + + pub(crate) fn guest_program_path(&mut self, guest_path: &str) -> &mut Self { + self.cmd_path = PathBuf::from(guest_path); + self + } + + #[cfg(all(target_arch = "x86_64", target_os = "linux"))] + pub(crate) fn broker_socket(&mut self, control_socket_path: &Path) -> &mut Self { + self.use_userland_broker = false; + self.command + .arg("--broker-control-channel") + .arg(control_socket_path); + self + } + + #[cfg(all(target_arch = "x86_64", target_os = "linux"))] + pub(crate) fn use_in_process_runner(&mut self) -> &mut Self { + self.use_userland_broker = true; + self.in_process_mode = true; + self + } + + #[cfg(target_os = "linux")] + pub(crate) fn use_userland_broker(&mut self) -> &mut Self { + self.use_userland_broker = true; + self + } + + #[cfg(target_os = "linux")] + pub(crate) fn allow_proxy_host(&mut self, host: impl AsRef) -> &mut Self { + self.managed_proxy_hosts.push(host.as_ref().to_os_string()); + self + } + + pub(crate) fn with_fs_path(&mut self, f: impl FnOnce(&Path)) -> &mut Self { + f(&self.tar_dir); + self + } + + pub(crate) fn run(&mut self) { + self.run_inner(false); + } + + #[must_use] + pub(crate) fn output(&mut self) -> Vec { + self.run_inner(true) + } + + fn prepare_command(&mut self) { + assert!(!self.has_run); + self.has_run = true; + let tar_file = self + .dir_path + .join(format!("rootfs_{}.tar", self.unique_name)); + let tar_success = super::create_tar_with_cache(&self.tar_dir, &tar_file, &self.unique_name); + assert!(tar_success, "failed to create tar file"); + println!("Tar file ready at: {}", tar_file.to_str().unwrap()); + + self.command + .arg("--initial-files") + .arg(&tar_file) + .arg(&self.cmd_path) + .args(&self.cmd_args); + + #[cfg(target_os = "linux")] + if self.use_userland_broker || !self.managed_proxy_hosts.is_empty() { + let runner = self.command.get_program().to_os_string(); + let runner_arguments = self + .command + .get_args() + .filter(|argument| *argument != "--unstable") + .map(OsStr::to_os_string) + .collect::>(); + let broker = Path::new(&runner).with_file_name("litebox-broker-userland"); + let proxy = Path::new(&runner).with_file_name("litebox_egress_proxy"); + assert!( + broker.is_file(), + "userland broker tests require a workspace build producing {}", + broker.display() + ); + if !self.managed_proxy_hosts.is_empty() { + assert!( + proxy.is_file(), + "managed proxy tests require a workspace build producing {}", + proxy.display() + ); + } + let mut command = std::process::Command::new(broker); + for host in &self.managed_proxy_hosts { + command.arg("--allow-host").arg(host); + } + command.arg("--fs-initial-files").arg(&tar_file); + if self.in_process_mode { + command.args(["--unstable", "--in-process-runner"]); + } else { + command.arg("--runner").arg(runner); + } + command.args(runner_arguments); + self.command = command; + } + } + + fn run_inner(&mut self, capture_stdout: bool) -> Vec { + self.prepare_command(); + self.command.stderr(std::process::Stdio::inherit()); + if !capture_stdout { + self.command.stdout(std::process::Stdio::inherit()); + } + println!("Running `{:?}`", self.command); + let output = self + .command + .output() + .expect("Failed to run litebox_runner_linux_userland"); + assert!( + output.status.success(), + "failed to run litebox_runner_linux_userland: {}", + output.status + ); + output.stdout + } + + #[cfg(target_os = "linux")] + pub(crate) fn spawn_with_stdio( + &mut self, + stdin: std::process::Stdio, + stdout: std::process::Stdio, + stderr: std::process::Stdio, + ) -> std::process::Child { + self.prepare_command(); + self.command.stdin(stdin).stdout(stdout).stderr(stderr); + println!("Running `{:?}`", self.command); + self.command + .spawn() + .expect("Failed to spawn litebox_runner_linux_userland") + } +} diff --git a/litebox_runner_linux_userland/tests/loader.rs b/litebox_runner_linux_userland/tests/loader.rs index 1d9e2ed57..44844e772 100644 --- a/litebox_runner_linux_userland/tests/loader.rs +++ b/litebox_runner_linux_userland/tests/loader.rs @@ -4,148 +4,24 @@ mod cache; mod common; -use std::ffi::CString; - -use litebox::fs::{Mode, OFlags}; -use litebox_platform_linux_userland::LinuxUserland as Platform; - -struct TestLauncher { - platform: &'static Platform, - shim_builder: litebox_shim_linux::LinuxShimBuilder, - fs: litebox_shim_linux::DefaultFS, - context: litebox::fs::resolver::Context, -} - -impl TestLauncher { - fn init_platform(tar_data: &'static [u8], initial_files: &[&str]) -> Self { - let platform = Platform::new(); - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let tar_data = if tar_data.is_empty() { - litebox::fs::tar_ro::EMPTY_TAR_FILE.into() - } else { - tar_data.into() - }; - let fs = shim_builder.default_fs(in_mem, tar_data); - let mut this = Self { - platform, - shim_builder, - fs, - context: litebox::fs::resolver::Context::new(), - }; - - for each in initial_files { - this.install_dir_all(std::path::Path::new(each).parent().unwrap()); - let data = std::fs::read(each).unwrap(); - this.install_file(data, each); - } - - this - } - - fn install_dir_all(&mut self, path: &std::path::Path) { - let mut ancestors: Vec<_> = path - .ancestors() - .filter(|a| *a != std::path::Path::new("/") && !a.as_os_str().is_empty()) - .collect(); - ancestors.reverse(); - for ancestor in ancestors { - if let Err(e) = self.install_dir(ancestor.to_str().unwrap()) { - assert!( - matches!(e, litebox::fs::errors::MkdirError::AlreadyExists), - "Failed to create directory {}: {e}", - ancestor.display() - ); - } - } - } - - fn install_dir(&mut self, path: &str) -> Result<(), litebox::fs::errors::MkdirError> { - self.fs - .mkdir(&self.context, path, Mode::RWXU | Mode::RWXG | Mode::RWXO) - } - - fn install_file(&mut self, contents: Vec, out: &str) { - let fd = self - .fs - .open( - &self.context, - out, - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXG | Mode::RWXO | Mode::RWXU, - ) - .unwrap(); - self.fs.write(&fd, &contents, None).unwrap(); - self.fs.close(&fd).unwrap(); - } - - fn test_load_exec_common(self, executable_path: &str) { - let argv = vec![ - CString::new(executable_path).unwrap(), - CString::new("hello").unwrap(), - ]; - let envp = vec![ - CString::new("PATH=/bin").unwrap(), - CString::new("HOME=/").unwrap(), - ]; - let fs = std::sync::Arc::new(self.fs); - let shim = self.shim_builder.build(); - let program = shim - .load_program(fs, self.platform.init_task(), executable_path, argv, envp) - .unwrap(); - unsafe { - litebox_platform_linux_userland::run_thread( - program.entrypoints, - &mut litebox_common_linux::PtRegs::default(), - ); - } - assert_eq!( - program.process.wait(), - 0, - "process exited with non-zero code" - ); - } -} +use common::runner::Runner; #[test] fn test_load_exec_dynamic() { let path = common::compile("./tests/hello.c", "hello_dylib", false, false); - - let files_to_install = common::find_dependencies(path.to_str().unwrap()); - - let executable_path = "/hello_dylib"; - let executable_data = std::fs::read(path).unwrap(); - - let mut launcher = TestLauncher::init_platform( - &[], - &files_to_install - .iter() - .map(std::string::String::as_str) - .collect::>(), - ); - launcher.install_file(executable_data, executable_path); - launcher.test_load_exec_common(executable_path); + Runner::new(&path, "loader_dynamic") + .env("PATH=/bin") + .arg("hello") + .run(); } #[test] fn test_load_exec_static() { let path = common::compile("./tests/hello.c", "hello_exec", true, false); - - let executable_path = "/hello_exec"; - let executable_data = std::fs::read(path).unwrap(); - - let mut launcher = TestLauncher::init_platform(&[], &[]); - - launcher.install_file(executable_data, executable_path); - - launcher.test_load_exec_common(executable_path); + Runner::new(&path, "loader_static") + .env("PATH=/bin") + .arg("hello") + .run(); } const HELLO_WORLD_NOLIBC: &str = r#" @@ -277,10 +153,8 @@ fn test_syscall_rewriter() { let rewrite_success = common::rewrite_with_cache(&path, &hooked_path, &[]); assert!(rewrite_success, "failed to run syscall rewriter"); - let executable_path = "/hello_exec_nolibc.hooked"; - let executable_data = std::fs::read(hooked_path).unwrap(); - - let mut launcher = TestLauncher::init_platform(&[], &[]); - launcher.install_file(executable_data, executable_path); - launcher.test_load_exec_common(executable_path); + Runner::new_pre_rewritten(&hooked_path, "loader_pre_rewritten") + .env("PATH=/bin") + .arg("hello") + .run(); } diff --git a/litebox_runner_linux_userland/tests/rewritten_guests.rs b/litebox_runner_linux_userland/tests/rewritten_guests.rs index a636f5484..e11f9e72f 100644 --- a/litebox_runner_linux_userland/tests/rewritten_guests.rs +++ b/litebox_runner_linux_userland/tests/rewritten_guests.rs @@ -12,6 +12,10 @@ mod common; fn run_rewritten_fixture(source: &str, unique_name: &str) -> std::process::Output { let target = common::compile(source, unique_name, true, false); + run_rewritten_target(&target) +} + +fn run_rewritten_target(target: &std::path::Path) -> std::process::Output { let binary_path = std::env::var("NEXTEST_BIN_EXE_litebox_runner_linux_userland") .unwrap_or_else(|_| env!("CARGO_BIN_EXE_litebox_runner_linux_userland").to_string()); @@ -48,8 +52,9 @@ fn test_host_program_with_rewrite_syscalls() { assert!( output.status.success(), - "failed to run litebox_runner_linux_userland: {}", - output.status + "failed to run litebox_runner_linux_userland ({}): {}", + output.status, + String::from_utf8_lossy(&output.stderr), ); let stdout = String::from_utf8_lossy(&output.stdout); @@ -154,13 +159,7 @@ fn test_x18_virtualization() { true, true, ); - let binary_path = std::env::var("NEXTEST_BIN_EXE_litebox_runner_linux_userland") - .unwrap_or_else(|_| env!("CARGO_BIN_EXE_litebox_runner_linux_userland").to_string()); - let output = std::process::Command::new(binary_path) - .args(["--unstable", "--rewrite-syscalls"]) - .arg(target) - .output() - .expect("Failed to run litebox_runner_linux_userland"); + let output = run_rewritten_target(&target); assert!( output.status.success(), "x18 fixture failed ({}): {}", diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index b401a9d45..c2c530edb 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -4,10 +4,9 @@ mod cache; mod common; -use std::{ - ffi::OsString, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; + +use common::runner::Runner; #[cfg(target_arch = "x86_64")] const BROKER_HELPER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); @@ -109,246 +108,6 @@ fn gateway_udp_policy() -> litebox_broker_core::SocketPolicy { .unwrap() } -/// Debian multiarch library directory preserved at its guest-relative path. -#[cfg(target_arch = "x86_64")] -const MULTIARCH_LIB_DIR: &str = "lib/x86_64-linux-gnu"; -#[cfg(target_arch = "aarch64")] -const MULTIARCH_LIB_DIR: &str = "lib/aarch64-linux-gnu"; - -#[must_use] -struct Runner { - command: std::process::Command, - dir_path: PathBuf, - tar_dir: PathBuf, - unique_name: String, - cmd_path: PathBuf, - cmd_args: Vec, - #[cfg(target_os = "linux")] - managed_proxy_hosts: Vec, - #[cfg(target_os = "linux")] - use_userland_broker: bool, - #[cfg(target_os = "linux")] - in_process_mode: bool, - has_run: bool, -} - -impl Runner { - fn new(target: &Path, unique_name: &str) -> Self { - let dir_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); - - // create tar file containing the rewritten executable and all dependencies - let tar_dir = dir_path.join(format!("tar_files_{unique_name}")); - let dirs_to_create = ["lib64", MULTIARCH_LIB_DIR, "lib32"]; - for dir in dirs_to_create { - std::fs::create_dir_all(tar_dir.join(dir)).unwrap(); - } - std::fs::create_dir_all(tar_dir.join("out")).unwrap(); - - let target_guest_path = std::path::absolute(target).unwrap(); - let target_dest_path = tar_dir.join(target_guest_path.strip_prefix("/").unwrap()); - let success = common::rewrite_with_cache(target, &target_dest_path, &[]); - assert!(success, "failed to run litebox_syscall_rewriter"); - - let libs = common::find_dependencies(target.to_str().unwrap()); - for file in &libs { - let file_path = std::path::Path::new(file.as_str()); - let dest_path = tar_dir.join(&file[1..]); - let success = common::rewrite_with_cache(file_path, &dest_path, &[]); - assert!( - success, - "failed to run litebox_syscall_rewriter for {}", - file_path.to_str().unwrap() - ); - } - - // Get the path to the litebox_runner_linux_userland binary - let binary_path = std::env::var("NEXTEST_BIN_EXE_litebox_runner_linux_userland") - .unwrap_or_else(|_| env!("CARGO_BIN_EXE_litebox_runner_linux_userland").to_string()); - - // run litebox_runner_linux_userland with the tar file and the compiled executable - let mut command = std::process::Command::new(binary_path); - command.args([ - "--unstable", - // Tell ld where to find the libraries. - // See https://man7.org/linux/man-pages/man8/ld.so.8.html for how ld works. - // Alternatively, we could add a `/etc/ld.so.cache` file to the rootfs. - "--env", - "LD_LIBRARY_PATH=/lib64:/lib32:/lib", - "--env", - "HOME=/", - "--program-from-tar", - ]); - - Self { - command, - dir_path, - tar_dir, - cmd_path: target_guest_path, - cmd_args: Vec::new(), - #[cfg(target_os = "linux")] - managed_proxy_hosts: Vec::new(), - #[cfg(target_os = "linux")] - use_userland_broker: true, - #[cfg(target_os = "linux")] - in_process_mode: false, - has_run: false, - unique_name: unique_name.to_owned(), - } - } - - fn tar_dir(&self) -> &Path { - &self.tar_dir - } - - fn env(&mut self, env: impl AsRef) -> &mut Self { - self.command.arg("--env").arg(env); - self - } - - fn envs(&mut self, envs: impl IntoIterator>) -> &mut Self { - for env in envs { - self.env(env); - } - self - } - - fn arg(&mut self, arg: impl AsRef) -> &mut Self { - self.cmd_args.push(arg.as_ref().to_os_string()); - self - } - - fn args(&mut self, args: impl IntoIterator>) -> &mut Self { - for arg in args { - self.arg(arg); - } - self - } - - fn guest_program_path(&mut self, guest_path: &str) -> &mut Self { - self.cmd_path = PathBuf::from(guest_path); - self - } - - #[cfg(all(target_arch = "x86_64", target_os = "linux"))] - fn broker_socket(&mut self, control_socket_path: &Path) -> &mut Self { - self.use_userland_broker = false; - self.command - .arg("--broker-control-channel") - .arg(control_socket_path); - self - } - - #[cfg(all(target_arch = "x86_64", target_os = "linux"))] - fn use_in_process_runner(&mut self) -> &mut Self { - self.use_userland_broker = true; - self.in_process_mode = true; - self - } - - fn with_fs_path(&mut self, f: impl FnOnce(&Path)) -> &mut Self { - f(&self.tar_dir); - self - } - - fn run(&mut self) { - self.run_inner(false); - } - - #[must_use] - fn output(&mut self) -> Vec { - self.run_inner(true) - } - - fn prepare_command(&mut self) { - assert!(!self.has_run); - self.has_run = true; - // create tar file using `tar` command with caching - let tar_file = self - .dir_path - .join(format!("rootfs_{}.tar", self.unique_name)); - let tar_success = - common::create_tar_with_cache(&self.tar_dir, &tar_file, &self.unique_name); - assert!(tar_success, "failed to create tar file"); - println!("Tar file ready at: {}", tar_file.to_str().unwrap()); - - self.command - .arg("--initial-files") - .arg(tar_file) - .arg(&self.cmd_path) - .args(&self.cmd_args); - - #[cfg(target_os = "linux")] - if self.use_userland_broker || !self.managed_proxy_hosts.is_empty() { - let runner = self.command.get_program().to_os_string(); - let runner_arguments = self - .command - .get_args() - .filter(|argument| *argument != "--unstable") - .map(std::ffi::OsStr::to_os_string) - .collect::>(); - let broker = Path::new(&runner).with_file_name("litebox-broker-userland"); - let proxy = Path::new(&runner).with_file_name("litebox_egress_proxy"); - assert!( - broker.is_file(), - "userland broker tests require a workspace build producing {}", - broker.display() - ); - if !self.managed_proxy_hosts.is_empty() { - assert!( - proxy.is_file(), - "managed proxy tests require a workspace build producing {}", - proxy.display() - ); - } - let mut command = std::process::Command::new(broker); - for host in &self.managed_proxy_hosts { - command.arg("--allow-host").arg(host); - } - if self.in_process_mode { - command.args(["--unstable", "--in-process-runner"]); - } else { - command.arg("--runner").arg(runner); - } - command.args(runner_arguments); - self.command = command; - } - } - - fn run_inner(&mut self, capture_stdout: bool) -> Vec { - self.prepare_command(); - self.command.stderr(std::process::Stdio::inherit()); - if !capture_stdout { - self.command.stdout(std::process::Stdio::inherit()); - } - println!("Running `{:?}`", self.command); - let output = self - .command - .output() - .expect("Failed to run litebox_runner_linux_userland"); - assert!( - output.status.success(), - "failed to run litebox_runner_linux_userland: {}", - output.status - ); - output.stdout - } - - #[cfg(target_os = "linux")] - fn spawn_with_stdio( - &mut self, - stdin: std::process::Stdio, - stdout: std::process::Stdio, - stderr: std::process::Stdio, - ) -> std::process::Child { - self.prepare_command(); - self.command.stdin(stdin).stdout(stdout).stderr(stderr); - println!("Running `{:?}`", self.command); - self.command - .spawn() - .expect("Failed to spawn litebox_runner_linux_userland") - } -} - /// Find all C test files in a directory fn find_c_test_files(dir: &str) -> Vec { let mut files = Vec::new(); @@ -380,8 +139,7 @@ fn has_dedicated_c_test(path: &Path) -> bool { #[cfg(target_os = "linux")] fn configure_pipe_broker(path: &Path, runner: &mut Runner) { if path.file_name().and_then(|name| name.to_str()) == Some("sendfile.c") { - runner.use_userland_broker = true; - runner.env("LITEBOX_PIPE_BROKER=1"); + runner.use_userland_broker().env("LITEBOX_PIPE_BROKER=1"); } } @@ -534,24 +292,119 @@ impl Drop for TestBroker { fn spawn_test_broker( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, + file_roots: &[&Path], connection_count: usize, ) -> TestBroker { - spawn_test_broker_with_mode(control_socket_path, policy, connection_count, false) + spawn_test_broker_with_mode( + control_socket_path, + policy, + file_roots, + connection_count, + false, + ) } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] fn spawn_concurrent_test_broker( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, + file_roots: &[&Path], connection_count: usize, ) -> TestBroker { - spawn_test_broker_with_mode(control_socket_path, policy, connection_count, true) + spawn_test_broker_with_mode( + control_socket_path, + policy, + file_roots, + connection_count, + true, + ) +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +fn test_file_service( + file_roots: &[PathBuf], +) -> std::sync::Arc { + use std::os::unix::fs::PermissionsExt; + + use litebox_broker_core::fs::{ + composer::Composer, + in_mem::{InMem, InitialNode}, + resolver::Resolver, + }; + use litebox_broker_platform_linux_userland::LinuxSyncPrimitivesProvider; + use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; + + let directory_mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; + let mut entries = vec![ + ( + "/tmp".to_owned(), + InitialNode::Directory { + mode: directory_mode, + owner: UserInfo::ROOT, + }, + ), + ( + "/registry".to_owned(), + InitialNode::Directory { + mode: directory_mode, + owner: UserInfo::ROOT, + }, + ), + ]; + + for root in file_roots { + for entry in walkdir::WalkDir::new(root).sort_by_file_name() { + let entry = entry.expect("failed to walk runner test root"); + let relative = entry + .path() + .strip_prefix(root) + .expect("runner test path must be below its root"); + if relative.as_os_str().is_empty() { + continue; + } + + let guest_path = format!( + "/{}", + relative + .to_str() + .expect("runner test paths must contain valid UTF-8") + ); + let metadata = + std::fs::metadata(entry.path()).expect("failed to inspect runner test file"); + let mode = Mode::from_u32_bits_truncate(metadata.permissions().mode()); + let node = if metadata.is_dir() { + InitialNode::Directory { + mode, + owner: UserInfo::ROOT, + } + } else { + InitialNode::File { + mode, + owner: UserInfo::ROOT, + data: std::fs::read(entry.path()) + .expect("failed to read runner test file") + .into(), + } + }; + entries.push((guest_path, node)); + } + } + + let backend = Composer::builder() + .mount("/", |_| { + InMem::::new_initialized(entries) + }) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .unwrap(); + std::sync::Arc::new(Resolver::::new(backend)) } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] fn spawn_test_broker_with_mode( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, + file_roots: &[&Path], connection_count: usize, concurrent: bool, ) -> TestBroker { @@ -563,6 +416,10 @@ fn spawn_test_broker_with_mode( let (stdout_tx, stdout_rx) = std::sync::mpsc::channel(); let server_control_socket_path = control_socket_path.to_path_buf(); let cleanup_control_socket_path = control_socket_path.to_path_buf(); + let file_roots = file_roots + .iter() + .map(|root| root.to_path_buf()) + .collect::>(); let broker_thread = std::thread::spawn(move || { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let control_listener = @@ -581,7 +438,7 @@ fn spawn_test_broker_with_mode( ), std::sync::Arc::new(TestRandomProvider), std::sync::Arc::new(CapturingStdioProvider { stdout_tx }), - std::sync::Arc::new(litebox_broker_core::fs::UnsupportedFileService), + test_file_service(&file_roots), ) .expect("failed to create broker core"); ready_tx.send(()).expect("failed to report broker ready"); @@ -676,20 +533,32 @@ fn run_test_broker_connection( let publisher_readiness = readiness.clone(); let publisher = std::thread::spawn(move || publisher_readiness.run(&mut notifications)); let mut close_object_count = 0; + let mut file_handles = Vec::new(); let termination = loop { match request_source .recv_request() .expect("failed to receive broker test request") { litebox_broker_transport::channel::HostReceive::Message(request) => { - if matches!( - &request.operation, - litebox_broker_protocol::message::BrokerOperation::CloseObject(_) - ) { - close_object_count += 1; + if let litebox_broker_protocol::message::BrokerOperation::CloseObject(handle) = + &request.operation + { + if let Some(index) = file_handles.iter().position(|value| value == handle) { + file_handles.swap_remove(index); + } else { + close_object_count += 1; + } } association - .execute_request(request, |response| response_sink.send_response(response)) + .execute_request(request, |response| { + if let litebox_broker_protocol::message::BrokerResult::File( + litebox_broker_protocol::message::FileResponse::Open(open), + ) = &response.result + { + file_handles.push(open.handle); + } + response_sink.send_response(response) + }) .expect("failed to execute broker test request"); } litebox_broker_transport::channel::HostReceive::PeerClosed => { @@ -740,44 +609,47 @@ console.log(content); false, false, ); + let mut true_runner = Runner::new(&true_path, "broker_true_rewriter"); + let mut eventfd_runner = Runner::new(&target, "broker_eventfd_rewriter"); + let mut pipe_runner = Runner::new(&pipe_target, "broker_pipe_rewriter"); + let mut urandom_runner = Runner::new(&urandom_target, "broker_urandom_rewriter"); + let mut node_runner = Runner::new(&node_path, "hello_node_broker_rewriter"); + node_runner + .arg("/out/hello_world.js") + .with_fs_path(|out_dir| { + std::fs::write(out_dir.join("out/hello_world.js"), HELLO_WORLD_JS).unwrap(); + }); let control_socket_path = unique_test_socket_path("runner-broker-control"); let broker_thread = spawn_test_broker( &control_socket_path, litebox_broker_core::PolicyEngine::with_host_guaranteed_rights( litebox_broker_core::ObjectRights::all(), ), + &[ + true_runner.tar_dir(), + eventfd_runner.tar_dir(), + pipe_runner.tar_dir(), + urandom_runner.tar_dir(), + node_runner.tar_dir(), + ], 5, ); - Runner::new(&true_path, "broker_true_rewriter") - .broker_socket(&control_socket_path) - .run(); + true_runner.broker_socket(&control_socket_path).run(); assert_eq!(broker_thread.next_close_object_count(), 0); - Runner::new(&target, "broker_eventfd_rewriter") - .broker_socket(&control_socket_path) - .run(); + eventfd_runner.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_socket(&control_socket_path) - .run(); + pipe_runner.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(&urandom_target, "broker_urandom_rewriter") - .broker_socket(&control_socket_path) - .run(); + urandom_runner.broker_socket(&control_socket_path).run(); assert_eq!(broker_thread.next_close_object_count(), 0); - Runner::new(&node_path, "hello_node_broker_rewriter") - .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(); - }) - .run(); + node_runner.broker_socket(&control_socket_path).run(); assert!(broker_thread.next_close_object_count() > 0); broker_thread.join(); @@ -894,6 +766,7 @@ fn test_runner_broker_tcp_client_with_rewriter() { let refused_listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap(); let refused_port = refused_listener.local_addr().unwrap().port(); drop(refused_listener); + let mut runner = Runner::new(&target, "broker_tcp_client_rewriter"); let control_socket_path = unique_test_socket_path("runner-broker-tcp-control"); let broker = spawn_test_broker( &control_socket_path, @@ -901,9 +774,10 @@ fn test_runner_broker_tcp_client_with_rewriter() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(gateway_tcp_policy()), + &[runner.tar_dir()], 1, ); - Runner::new(&target, "broker_tcp_client_rewriter") + runner .arg(port.to_string()) .arg(refused_port.to_string()) .broker_socket(&control_socket_path) @@ -990,6 +864,7 @@ fn test_runner_broker_udp_with_rewriter() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(gateway_udp_policy()), + &[runner.tar_dir()], 1, ); runner @@ -1013,6 +888,8 @@ fn test_runner_broker_udp_namespace_delivers_after_sender_close() { false, false, ); + let mut server_runner = Runner::new(&target, "broker_udp_namespace_server_rewriter"); + let mut client_runner = Runner::new(&target, "broker_udp_namespace_client_rewriter"); let control_socket_path = unique_test_socket_path("runner-broker-udp-namespace-control"); let broker = spawn_concurrent_test_broker( &control_socket_path, @@ -1020,9 +897,10 @@ fn test_runner_broker_udp_namespace_delivers_after_sender_close() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(litebox_broker_core::SocketPolicy::guest_network()), + &[server_runner.tar_dir()], 2, ); - let mut server = Runner::new(&target, "broker_udp_namespace_server_rewriter") + let mut server = server_runner .arg("server") .broker_socket(&control_socket_path) .spawn_with_stdio(Stdio::null(), Stdio::null(), Stdio::inherit()); @@ -1034,7 +912,7 @@ fn test_runner_broker_udp_namespace_delivers_after_sender_close() { .unwrap(); assert_ne!(port, 0); - Runner::new(&target, "broker_udp_namespace_client_rewriter") + client_runner .arg("client") .arg(port.to_string()) .broker_socket(&control_socket_path) @@ -1069,6 +947,7 @@ fn test_runner_broker_tcp_server_with_rewriter() { false, false, ); + let mut runner = Runner::new(&target, "broker_tcp_server_rewriter"); let control_socket_path = unique_test_socket_path("runner-broker-tcp-server-control"); let broker = spawn_test_broker( &control_socket_path, @@ -1076,11 +955,14 @@ fn test_runner_broker_tcp_server_with_rewriter() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(litebox_broker_core::SocketPolicy::guest_network()), + &[runner.tar_dir()], 1, ); - let mut child = Runner::new(&target, "broker_tcp_server_rewriter") - .broker_socket(&control_socket_path) - .spawn_with_stdio(Stdio::null(), Stdio::null(), Stdio::inherit()); + let mut child = runner.broker_socket(&control_socket_path).spawn_with_stdio( + Stdio::null(), + Stdio::null(), + Stdio::inherit(), + ); let mut output = String::new(); let mut next_marker = |prefix: &str| loop { let line = broker.next_stdout_line(); @@ -1475,6 +1357,7 @@ fn test_broker_with_curl() { }); let curl_path = run_which("curl"); + let mut runner = Runner::new(&curl_path, "curl_rewriter"); let control_socket_path = unique_test_socket_path("runner-broker-curl-control"); let broker = spawn_test_broker( &control_socket_path, @@ -1482,10 +1365,11 @@ fn test_broker_with_curl() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(gateway_tcp_policy()), + &[runner.tar_dir()], 1, ); let url = format!("http://10.0.2.1:{port}/something"); - Runner::new(&curl_path, "curl_rewriter") + runner .args(["-sS", &url]) .broker_socket(&control_socket_path) .run(); @@ -1520,8 +1404,8 @@ fn test_managed_egress_proxy_with_curl() { let curl_path = run_which("curl"); let mut runner = Runner::new(&curl_path, "managed_egress_proxy_curl"); - runner.managed_proxy_hosts.push("bing.com:443".into()); runner + .allow_proxy_host("bing.com:443") .env("HTTPS_PROXY=http://wrong.example:8080") .env("NO_PROXY=*") .with_fs_path(|root| { @@ -1569,6 +1453,7 @@ fn test_broker_with_iperf3() { const IPERF_TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); let iperf3_path = run_which("iperf3"); + let mut runner = Runner::new(&iperf3_path, "broker_iperf3_client_rewriter"); let control_socket_path = unique_test_socket_path("runner-broker-iperf3-control"); let broker = spawn_test_broker( &control_socket_path, @@ -1576,6 +1461,7 @@ fn test_broker_with_iperf3() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(gateway_tcp_policy()), + &[runner.tar_dir()], 1, ); @@ -1635,7 +1521,6 @@ fn test_broker_with_iperf3() { let (port, mut server, server_output) = started_server .unwrap_or_else(|| panic!("iperf3 server did not start; output:\n{last_server_output}")); - let mut runner = Runner::new(&iperf3_path, "broker_iperf3_client_rewriter"); runner .args([ "-c", diff --git a/litebox_runner_snp/src/main.rs b/litebox_runner_snp/src/main.rs index 01a56d242..93902c3ae 100644 --- a/litebox_runner_snp/src/main.rs +++ b/litebox_runner_snp/src/main.rs @@ -10,8 +10,8 @@ mod globals; extern crate alloc; -use alloc::{borrow::ToOwned, boxed::Box}; -use litebox::utils::{ReinterpretUnsignedExt as _, TruncateExt as _}; +use alloc::boxed::Box; +use litebox::utils::TruncateExt as _; use litebox_platform_linux_kernel::{HostInterface, host::snp::ghcb::ghcb_prints}; /// `log` backend that forwards to the GHCB serial console. @@ -157,114 +157,12 @@ pub extern "C" fn sandbox_process_init( let initialized = SHIM.set(Box::new(shim)).is_ok(); assert!(initialized, "shim initialized more than once"); - let parse_args = - |params: &litebox_platform_linux_kernel::host::snp::snp_impl::vmpl2_boot_params| -> Option<( - alloc::string::String, - alloc::vec::Vec, - alloc::vec::Vec, - )> { - let mut argv = alloc::vec::Vec::new(); - let mut envp = alloc::vec::Vec::new(); - - let argv_len = params.argv_len.reinterpret_as_unsigned() as usize; - let env_len = params.env_len.reinterpret_as_unsigned() as usize; - let total = argv_len + env_len; - - let mut idx = 0; - while idx < total { - let arg = core::ffi::CStr::from_bytes_until_nul(¶ms.argv_and_env[idx..]) - .ok()? - .to_owned(); - let this_len = arg.count_bytes() + 1; - - if idx < argv_len { - argv.push(arg); - } else { - envp.push(arg); - } - idx += this_len; - } - let program = argv.first().cloned()?; - Some((program.to_str().ok()?.to_owned(), argv, envp)) - }; - let Some((program, argv, envp)) = parse_args(boot_params) else { - litebox_platform_linux_kernel::host::snp::snp_impl::HostSnpInterface::terminate( - globals::SM_SEV_TERM_SET, - globals::SM_TERM_INVALID_PARAM, - ); - }; - - #[allow(clippy::missing_panics_doc)] - let shim = SHIM.get().expect("initialized"); - let litebox = shim.litebox(); - - let socket_addr = core::net::SocketAddr::V4(core::net::SocketAddrV4::new( - core::net::Ipv4Addr::new(10, 0, 0, 1), - 8888, - )); - let Ok(transport) = shim.tcp_connection(socket_addr) else { - ghcb_prints("failed to connect to 9p server"); - litebox_platform_linux_kernel::host::snp::snp_impl::HostSnpInterface::terminate( - globals::SM_SEV_TERM_SET, - globals::SM_TERM_GENERAL, - ); - }; - let composer = litebox::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - let Ok(nine_p) = litebox::fs::nine_p::NineP::::new( - transport, - 65536, - "root", - "/tmp", - allocators.next(), - ) else { - ghcb_prints("failed to create 9P filesystem"); - litebox_platform_linux_kernel::host::snp::snp_impl::HostSnpInterface::terminate( - globals::SM_SEV_TERM_SET, - globals::SM_TERM_GENERAL, - ); - }; - litebox::fs::overlay::Overlay::::new( - litebox::fs::in_mem::InMem::::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU - | litebox::fs::Mode::RWXG - | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]), - nine_p, - allocators.next(), - ) - }) - .mount("/dev", litebox::fs::devices::Devices::new) - .build() - .unwrap_or_else( - |(litebox::fs::composer::BuildError::NoMounts - | litebox::fs::composer::BuildError::InvalidMountPath - | litebox::fs::composer::BuildError::DuplicateMountPath)| unreachable!(), - ); - let fs = alloc::sync::Arc::new(litebox::fs::resolver::Resolver::new(litebox, composer)); - - // Loading a program may trigger page faults, so we need to set SHIM before this. - let program = match shim.load_program(fs, platform.init_task(boot_params), &program, argv, envp) - { - Ok(program) => program, - Err(err) => { - litebox_util_log::error!(err:% = err; "failed to load program"); - litebox_platform_linux_kernel::host::snp::snp_impl::HostSnpInterface::terminate( - globals::SM_SEV_TERM_SET, - globals::SM_TERM_GENERAL, - ); - } - }; - unsafe { - litebox_platform_linux_kernel::host::snp::snp_impl::run_thread( - alloc::boxed::Box::new(program.entrypoints), - pt_regs, - ) - }; + let _ = (boot_params, pt_regs); + ghcb_prints("filesystem startup requires a kernel broker platform"); + litebox_platform_linux_kernel::host::snp::snp_impl::HostSnpInterface::terminate( + globals::SM_SEV_TERM_SET, + globals::SM_TERM_GENERAL, + ); } #[unsafe(no_mangle)] diff --git a/litebox_runner_windows_on_linux_userland/Cargo.toml b/litebox_runner_windows_on_linux_userland/Cargo.toml index f200cc6a2..967e714f7 100644 --- a/litebox_runner_windows_on_linux_userland/Cargo.toml +++ b/litebox_runner_windows_on_linux_userland/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.97" clap = { version = "4.5.33", features = ["derive"] } litebox = { version = "0.1.0", path = "../litebox" } +litebox_broker_local_userland = { version = "0.1.0", path = "../litebox_broker_local_userland" } litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } litebox_platform_linux_userland = { version = "0.1.0", path = "../litebox_platform_linux_userland" } litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows" } diff --git a/litebox_runner_windows_on_linux_userland/src/lib.rs b/litebox_runner_windows_on_linux_userland/src/lib.rs index 4d214c1ca..9bf794939 100644 --- a/litebox_runner_windows_on_linux_userland/src/lib.rs +++ b/litebox_runner_windows_on_linux_userland/src/lib.rs @@ -9,6 +9,7 @@ extern crate alloc; use anyhow::{Context as _, Result}; use clap::Parser; +use litebox_broker_local_userland as broker; use litebox_platform_linux_userland::LinuxUserland; use std::path::PathBuf; @@ -32,9 +33,21 @@ pub struct CliArgs { /// Allow using unstable options. #[arg(short = 'Z', long = "unstable")] pub unstable: bool, + /// Broker-supplied Unix socket path for the local control channel. + #[arg( + long = "broker-control-channel", + value_name = "PATH", + value_hint = clap::ValueHint::FilePath, + hide = true, + requires = "unstable", + help_heading = "Unstable Options" + )] + pub broker_control_channel: Option, /// Tar archive containing the program and its runtime files. + /// + /// This may be omitted when the broker was configured with `--fs-initial-files`. #[arg(long = "initial-files", value_name = "PATH_TO_TAR", value_hint = clap::ValueHint::FilePath)] - pub initial_files: PathBuf, + pub initial_files: Option, } /// Run Windows PE programs with LiteBox on unmodified Linux. @@ -60,37 +73,33 @@ pub fn run(cli_args: CliArgs) -> Result<()> { ); } - let tar_file = &cli_args.initial_files; - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - anyhow::bail!("Expected a .tar file, found {}", tar_file.display()); - } - let tar_data = std::fs::read(tar_file) - .with_context(|| format!("Could not read tar file at {}", tar_file.display()))?; - let platform = LinuxUserland::new(); - let shim_builder = litebox_shim_windows::WindowsShimBuilder::new(platform); + let control_socket = cli_args + .broker_control_channel + .as_deref() + .context("file operations require --broker-control-channel")?; + let broker::BrokerConnection { + local, + notifications, + coordinator, + positional_io_fds: _broker_positional_io_fds, + shutdown_fd: _broker_shutdown_fd, + } = broker::connect(control_socket)?; + let litebox = litebox::LiteBox::new_with_broker_local(platform, local); + coordinator.install_dispatch(litebox.broker_failure_dispatcher()); + broker::start_notification_receiver( + notifications, + coordinator, + litebox.broker_notification_dispatcher(), + )?; + let shim_builder = + litebox_shim_windows::WindowsShimBuilder::new_with_litebox(platform, litebox); let (program_path, program_args) = cli_args .program_and_arguments .split_first() .context("program path missing - clap should have required at least one argument")?; - let initial_file_system = { - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo { - user: 1000, - group: 1000, - }, - }, - )]); - - shim_builder.default_fs(in_mem, tar_data.into()) - }; - let initial_file_system = std::sync::Arc::new(initial_file_system); - let shim = shim_builder.build(); let argv = std::iter::once(program_path.as_str()) .chain(program_args.iter().map(String::as_str)) @@ -113,7 +122,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } let program = shim - .load_program(initial_file_system, program_path, argv, envp) + .load_program(program_path, argv, envp) .context("failed to load Windows PE program")?; // SAFETY: `WindowsShimEntrypoints::init` populates `rip`/`rsp`/`eflags` inside // `run_thread` before the initial guest thread executes, so the `PtRegs::default()` diff --git a/litebox_runner_windows_userland/Cargo.toml b/litebox_runner_windows_userland/Cargo.toml index 49dce8f25..05a3e888c 100644 --- a/litebox_runner_windows_userland/Cargo.toml +++ b/litebox_runner_windows_userland/Cargo.toml @@ -12,7 +12,7 @@ litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } litebox_platform_windows_userland = { version = "0.1.0", path = "../litebox_platform_windows_userland" } litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = ["backend_tracing"] } -memmap2 = "0.9.8" + tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } [dev-dependencies] diff --git a/litebox_runner_windows_userland/src/lib.rs b/litebox_runner_windows_userland/src/lib.rs index 09209d71a..53d8b2625 100644 --- a/litebox_runner_windows_userland/src/lib.rs +++ b/litebox_runner_windows_userland/src/lib.rs @@ -11,25 +11,7 @@ use anyhow::{Context as _, Result}; use clap::Parser; use litebox_broker_local_userland as broker; use litebox_platform_windows_userland::WindowsUserland; -use memmap2::Mmap; -use std::path::{Path, PathBuf}; - -fn mmapped_file(path: impl AsRef) -> Result<&'static [u8]> { - let path = path.as_ref(); - let file = std::fs::File::open(path) - .with_context(|| format!("Could not open tar file at {}", path.display()))?; - let data = { - // SAFETY: The runner maps the input read-only and does not modify it. The caller must ensure - // the tar file is not modified externally while the guest is running. - // - // Leak the mapping so the borrowed tar data remains valid for the process-lifetime file - // system. - Box::leak(Box::new(unsafe { Mmap::map(&file) }.with_context( - || format!("Could not map tar file at {}", path.display()), - )?)) - }; - Ok(data) -} +use std::path::PathBuf; /// Runs a Windows PE program with LiteBox on unmodified Windows and returns its exit code. /// @@ -61,8 +43,10 @@ pub struct CliArgs { )] pub broker_control_channel: Option, /// Tar archive containing the program and its runtime files. + /// + /// This may be omitted when the broker was configured with `--fs-initial-files`. #[arg(long = "initial-files", value_name = "PATH_TO_TAR", value_hint = clap::ValueHint::FilePath)] - pub initial_files: PathBuf, + pub initial_files: Option, } /// Run Windows PE programs with LiteBox on unmodified Windows. @@ -82,55 +66,29 @@ pub fn run(cli_args: CliArgs) -> Result { ) .init(); - let tar_file = &cli_args.initial_files; - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - anyhow::bail!("Expected a .tar file, found {}", tar_file.display()); - } - let tar_data = mmapped_file(tar_file)?; - let platform = WindowsUserland::new(); - let broker_connection = cli_args + let control_pipe = cli_args .broker_control_channel .as_deref() - .map(broker::connect) - .transpose()?; - let shim_builder = if let Some(broker_connection) = broker_connection { - let broker::BrokerConnection { - local, - notifications, - } = broker_connection; - let litebox = litebox::LiteBox::new_with_broker_local(platform, local); - broker::start_notification_receiver( - notifications, - litebox.broker_notification_dispatcher(), - litebox.broker_failure_dispatcher(), - )?; - litebox_shim_windows::WindowsShimBuilder::new_with_litebox(platform, litebox) - } else { - litebox_shim_windows::WindowsShimBuilder::new(platform) - }; + .context("file operations require --broker-control-channel")?; + let broker::BrokerConnection { + local, + notifications, + } = broker::connect(control_pipe)?; + let litebox = litebox::LiteBox::new_with_broker_local(platform, local); + broker::start_notification_receiver( + notifications, + litebox.broker_notification_dispatcher(), + litebox.broker_failure_dispatcher(), + )?; + let shim_builder = + litebox_shim_windows::WindowsShimBuilder::new_with_litebox(platform, litebox); let (program_path, program_args) = cli_args .program_and_arguments .split_first() .context("program path missing — clap should have required at least one argument")?; - let initial_file_system = { - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo { - user: 1000, - group: 1000, - }, - }, - )]); - - shim_builder.default_fs(in_mem, tar_data.into()) - }; - let initial_file_system = std::sync::Arc::new(initial_file_system); - let shim = shim_builder.build(); let argv = std::iter::once(program_path.as_str()) .chain(program_args.iter().map(String::as_str)) @@ -153,7 +111,7 @@ pub fn run(cli_args: CliArgs) -> Result { } let program = shim - .load_program(initial_file_system, program_path, argv, envp) + .load_program(program_path, argv, envp) .context("failed to load Windows PE program")?; // SAFETY: `WindowsShimEntrypoints::init` populates `rip`/`rsp`/`eflags` inside // `run_thread` before the initial guest thread executes, so the `PtRegs::default()` diff --git a/litebox_runner_windows_userland/tests/run.rs b/litebox_runner_windows_userland/tests/run.rs index fba709946..cc39bf7d2 100644 --- a/litebox_runner_windows_userland/tests/run.rs +++ b/litebox_runner_windows_userland/tests/run.rs @@ -22,9 +22,16 @@ fn run_hello_world_pe() { let (broker, runner) = build_windows_broker(); let mut separate_process = std::process::Command::new(&broker); - separate_process.arg("--runner").arg(runner); + separate_process + .arg("--fs-initial-files") + .arg(&tar_path) + .arg("--runner") + .arg(runner); let mut in_process = std::process::Command::new(broker); - in_process.args(["--unstable", "--in-process-runner"]); + in_process + .arg("--fs-initial-files") + .arg(&tar_path) + .args(["--unstable", "--in-process-runner"]); for (mode, mut command) in [ ("separate-process", separate_process), @@ -80,7 +87,7 @@ fn run_multithreaded_pe() { std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("kernel32_multithread.tar"); create_tar_with_dir(&test_dir, &tar_path); - let mut command = brokered_windows_runner_command(); + let mut command = brokered_windows_runner_command(&tar_path); command.env("LITEBOX_LOG", "debug"); command.args([ "--initial-files", @@ -131,7 +138,7 @@ fn run_crt_locale_pe() { let tar_path = std::path::PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("crt_locale.tar"); create_tar_with_dir(&test_dir, &tar_path); - let mut command = brokered_windows_runner_command(); + let mut command = brokered_windows_runner_command(&tar_path); command.env("LITEBOX_LOG", "debug"); command.args([ "--initial-files", @@ -185,10 +192,14 @@ fn build_windows_broker() -> (std::path::PathBuf, std::path::PathBuf) { (broker, runner) } -fn brokered_windows_runner_command() -> std::process::Command { +fn brokered_windows_runner_command(initial_files: &std::path::Path) -> std::process::Command { let (broker, runner) = build_windows_broker(); let mut command = std::process::Command::new(broker); - command.arg("--runner").arg(runner); + command + .arg("--fs-initial-files") + .arg(initial_files) + .arg("--runner") + .arg(runner); command } diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index ca277428f..5b9153b65 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -8,6 +8,7 @@ arrayvec = { version = "0.7.6", default-features = false } bitvec = { version = "1.0.1", default-features = false, features = ["alloc"] } bitflags = "2.13.1" litebox = { path = "../litebox/", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } litebox_platform = { path = "../litebox_platform", version = "0.1.0" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } @@ -24,13 +25,9 @@ alarm_fallback = [] aarch64_virtualize_x18 = [] [dev-dependencies] -litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_local = { path = "../litebox_broker_local", 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"] } libc = "0.2.177" -tempfile = "3" # The unit tests need a concrete platform to run against. The platform is selected # by the build target so the tests can run on whichever userland platform matches diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 9de7fc2be..02cf99211 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -14,7 +14,6 @@ extern crate alloc; -use alloc::borrow::Cow; use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; @@ -29,8 +28,11 @@ use litebox::{ sync::futex::FutexManager, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _}, }; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileMode as Mode, FileOpenFlags, FileSeekWhence as SeekWhence, +}; use litebox_common_linux::{ - SyscallRequest, + OFlags, SyscallRequest, errno::Errno, user_pointers::{UserPtr, UserPtrMut}, }; @@ -56,15 +58,9 @@ pub(crate) mod channel; pub mod loader; pub(crate) mod stdio; pub mod syscalls; -pub mod transport; mod wait; -pub type DefaultFS = LinuxFS; - -pub(crate) type LinuxFS = - litebox::fs::resolver::Resolver; - -pub(crate) type FileFd = litebox::fd::TypedFd>; +pub(crate) use litebox::fs::FileFd; /// Aggregate bound capturing everything the shim requires of a platform. /// @@ -224,27 +220,19 @@ impl LinuxShimBuilder { &self.litebox } - /// Create the default file system with the given in-memory layer and tar data. - pub fn default_fs( - &self, - in_mem: litebox::fs::in_mem::InMem, - tar_data: Cow<'static, [u8]>, - ) -> DefaultFS { - default_fs(&self.litebox, in_mem, tar_data) - } - /// Build the shim. pub fn build(self) -> LinuxShim { - let net = Network::new(&self.litebox); + let litebox = Arc::new(self.litebox); + let net = Network::new(&litebox); let global = Arc::new(GlobalState { platform: self.platform, - pm: PageManager::new(&self.litebox), + pm: PageManager::new(&litebox), futex_manager: FutexManager::new(), - pipes: Pipes::new(&self.litebox), + pipes: Pipes::new(&litebox), net: litebox::sync::Mutex::new(net), boot_time: self.platform.now(), next_thread_id: 2.into(), // start from 2, as 1 is used by the main thread - litebox: self.litebox, + litebox, unix_addr_table: litebox::sync::RwLock::new(syscalls::unix::UnixAddrTable::new()), elf_patch_cache: litebox::sync::Mutex::new(alloc::collections::BTreeMap::new()), }); @@ -264,7 +252,6 @@ impl LinuxShim { /// initial register state. pub fn load_program( &self, - fs: alloc::sync::Arc>, task: litebox_common_linux::TaskParams, path: &str, argv: Vec, @@ -279,7 +266,7 @@ impl LinuxShim { egid, } = task; - let files = syscalls::file::FilesState::new(fs); + let files = syscalls::file::FilesState::new(); files.set_max_fd(syscalls::process::RLIMIT_NOFILE_CUR); let files = Arc::new(files); let credentials = Arc::new(syscalls::process::Credentials { @@ -330,19 +317,8 @@ impl LinuxShim { &self.0.pm } - /// Establish a TCP connection to the given address. - /// - /// Returns a [`transport::ShimTransport`] that can be used as a - /// byte-stream transport (e.g., for a 9P filesystem client). - pub fn tcp_connection( - &self, - addr: core::net::SocketAddr, - ) -> Result, Errno> { - transport::ShimTransport::connect(self.0.clone(), addr) - } - pub fn litebox(&self) -> &LiteBox { - &self.0.litebox + self.0.litebox.as_ref() } /// Returns the platform this shim was built with. @@ -372,50 +348,45 @@ impl LinuxShimProcess { } } -/// Create the default file system with the given in-memory layer and tar data. -fn default_fs( - litebox: &LiteBox, - in_mem: litebox::fs::in_mem::InMem, - tar_data: Cow<'static, [u8]>, -) -> LinuxFS { - litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox::fs::overlay::Overlay::::new( - in_mem, - litebox::fs::tar_ro::TarRo::new(tar_data, allocators.next()), - allocators.next(), - ) - }) - .mount("/dev", litebox::fs::devices::Devices::new) - .build() - .unwrap(), - ) -} - // Special override so that `GETFL` can return stdio-specific flags #[derive(Clone)] -pub(crate) struct StdioStatusFlags(litebox::fs::OFlags); +pub(crate) struct StdioStatusFlags(OFlags); impl syscalls::file::FilesState { fn initialize_stdio_in_shared_descriptors_table( &self, global: &GlobalState, - context: &litebox::fs::resolver::Context, + context: &litebox::fs::Context, ) { - use litebox::fs::{Mode, OFlags}; - let stdin = self - .fs - .open(context, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + let stdin = global + .litebox + .open_file( + context, + "/dev/stdin", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); - let stdout = self - .fs - .open(context, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + let stdout = global + .litebox + .open_file( + context, + "/dev/stdout", + FileAccessMode::WriteOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); - let stderr = self - .fs - .open(context, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + let stderr = global + .litebox + .open_file( + context, + "/dev/stderr", + FileAccessMode::WriteOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); let mut dt = global.litebox.descriptor_table_mut(); let mut rds = self.raw_descriptor_store.write(); @@ -471,7 +442,7 @@ impl syscalls::file::FilesState { }; } - resolve_fd!(LinuxFS, Fs); + resolve_fd!(litebox::fs::BrokerFile, Fs); resolve_fd!(Network, Network); resolve_fd!(Pipes, Pipes); resolve_fd!(syscalls::eventfd::EventfdSubsystem, Eventfd); @@ -614,7 +585,7 @@ impl Task { self.do_seek( fd, 0, - litebox::fs::SeekWhence::RelativeToCurrentOffset, + SeekWhence::RelativeToCurrentOffset, ) .inspect_err(|e| { match *e { @@ -642,7 +613,7 @@ impl Task { self.do_seek( fd, (cur_loc + read_total).reinterpret_as_signed(), - litebox::fs::SeekWhence::RelativeToBeginning, + SeekWhence::RelativeToBeginning, ) // Given that previous lseek and pread succeeded, this lseek should also succeed. .expect("lseek failed"); @@ -1184,7 +1155,7 @@ struct GlobalState { /// The platform instance used throughout the shim. platform: &'static Platform, /// The LiteBox instance used throughout the shim. - litebox: litebox::LiteBox, + litebox: Arc>, /// The page manager for managing virtual memory. pm: litebox::mm::PageManager, /// The futex manager for handling futex operations. @@ -1240,14 +1211,11 @@ mod test_utils { impl GlobalState { /// Make a new task with default values for testing. - pub(crate) fn new_test_task( - self: Arc, - fs: alloc::sync::Arc>, - ) -> Task { + pub(crate) fn new_test_task(self: Arc) -> Task { let pid = self .next_thread_id .fetch_add(1, core::sync::atomic::Ordering::Relaxed); - let files = Arc::new(syscalls::file::FilesState::new(fs)); + let files = Arc::new(syscalls::file::FilesState::new()); let credentials = Arc::new(syscalls::process::Credentials { uid: 0, euid: 0, diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index d9fc2e9bf..585309ac3 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -5,11 +5,11 @@ use alloc::{ffi::CString, vec::Vec}; use litebox::{ - fs::{Mode, OFlags}, mm::linux::{CreatePagesFlags, MappingError, PAGE_SIZE}, - utils::{ReinterpretSignedExt, TruncateExt}, + utils::ReinterpretSignedExt, }; -use litebox_common_linux::{MapFlags, errno::Errno, loader::ElfParsedFile}; +use litebox_broker_protocol::fs::FileMode as Mode; +use litebox_common_linux::{MapFlags, OFlags, errno::Errno, loader::ElfParsedFile}; use thiserror::Error; use crate::{ @@ -55,28 +55,23 @@ impl litebox_common_linux::loader::ReadAt for &'_ ElfFil return Ok(()); } // Try to read the remaining bytes - let bytes_read = self.task.sys_read(self.fd, buf, Some(offset.trunc()))?; + let file_offset = usize::try_from(offset).map_err(|_| Errno::EOVERFLOW)?; + let bytes_read = self.task.sys_read(self.fd, buf, Some(file_offset))?; if bytes_read == 0 { // reached the end of the file return Err(Errno::ENODATA); } else { // Successfully read some bytes buf = &mut buf[bytes_read..]; - offset += bytes_read as u64; + offset = offset + .checked_add(bytes_read as u64) + .ok_or(Errno::EOVERFLOW)?; } } } fn size(&mut self) -> Result { - #[cfg(target_arch = "x86_64")] - { - Ok(self.task.sys_fstat(self.fd)?.st_size as u64) - } - #[cfg(target_arch = "aarch64")] - { - // The asm-generic ABI uses signed `st_size`; reject negative sizes. - u64::try_from(self.task.sys_fstat(self.fd)?.st_size).map_err(|_| Errno::EINVAL) - } + Ok(self.task.file_status(self.fd)?.size) } } @@ -148,7 +143,7 @@ impl litebox_common_linux::loader::MapMemory for ElfFile prot.flags(), MapFlags::MAP_PRIVATE | MapFlags::MAP_FIXED, self.fd, - offset.trunc(), + usize::try_from(offset).map_err(|_| Errno::EOVERFLOW)?, )?; Ok(()) } @@ -383,180 +378,32 @@ impl From for litebox_common_linux::errno::Errno { #[cfg(test)] mod tests { - extern crate std; - - use alloc::vec::Vec; - use crate::syscalls::tests::TestPlatform; - use litebox::{ - fs::{Mode, OFlags}, - platform::PageManagementProvider, - }; + use litebox::platform::PageManagementProvider; + use litebox_common_linux::loader::MapMemory as _; 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; - #[cfg(target_arch = "x86_64")] - const EM_HOST: u16 = 62; // EM_X86_64 - #[cfg(target_arch = "aarch64")] - const EM_HOST: u16 = 183; // EM_AARCH64 - 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_HOST); - 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() { + fn interpreter_reservation_is_top_down_above_low_heap() { let task = crate::syscalls::tests::init_platform(); - 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 mut interpreter = ElfFile { + task: &task, + fd: 0, + load_high: true, + }; + let address = interpreter + .reserve(PAGE_SIZE, PAGE_SIZE) + .expect("the interpreter reservation should succeed"); + 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, + address >= addr_max / 2, + "interpreter reserved at {address:#x}, near the low-heap region {:#x} rather than top-down high (>= {:#x})", crate::loader::DEFAULT_LOW_ADDR, addr_max / 2, ); + task.sys_munmap(UserPtrMut::from_usize(address), PAGE_SIZE) + .expect("the test reservation should unmap"); } } diff --git a/litebox_shim_linux/src/stdio.rs b/litebox_shim_linux/src/stdio.rs index ab4c97198..edf55aa9c 100644 --- a/litebox_shim_linux/src/stdio.rs +++ b/litebox_shim_linux/src/stdio.rs @@ -7,14 +7,13 @@ mod tests { use core::ffi::CStr; - use litebox::fs::{Mode, OFlags}; - use litebox_common_linux::{FcntlArg, FileDescriptorFlags, IoctlArg, Termios, errno::Errno}; - - use crate::{ - UserPtrMut, - syscalls::tests::{init_platform, init_platform_with_broker}, + use litebox_broker_protocol::fs::FileMode as Mode; + use litebox_common_linux::{ + FcntlArg, FileDescriptorFlags, IoctlArg, OFlags, Termios, errno::Errno, }; + use crate::{UserPtrMut, syscalls::tests::init_platform}; + fn termios() -> Termios { Termios { c_iflag: 0, @@ -127,20 +126,9 @@ mod tests { ); } - #[test] - fn test_stdio_terminal_query_requires_broker() { - let task = init_platform(); - let mut termios = termios(); - - assert_eq!( - task.sys_ioctl(1, IoctlArg::TCGETS(UserPtrMut::from_ptr(&raw mut termios)),), - Err(Errno::EIO) - ); - } - #[test] fn test_stdio_terminal_query_uses_broker() { - let task = init_platform_with_broker(); + let task = init_platform(); let mut termios = termios(); assert_eq!( diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index aa199b8d3..88d97adc3 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -21,7 +21,7 @@ use litebox::{ use litebox_common_linux::{EpollEvent, EpollOp, errno::Errno}; use super::file::FilesState; -use crate::{GlobalState, LinuxFS, ShimPlatform}; +use crate::{GlobalState, ShimPlatform}; pub(crate) struct EpollSubsystem(core::marker::PhantomData); impl FdEnabledSubsystem for EpollSubsystem { @@ -43,7 +43,7 @@ bitflags::bitflags! { pub(crate) enum EpollDescriptor { Eventfd(Arc>>), Epoll(Arc>>), - File(Arc>), + File(Arc), Socket(Arc>), Pipe(Arc>), Unix(Arc>>), @@ -52,7 +52,7 @@ pub(crate) enum EpollDescriptor { impl EpollDescriptor { pub fn try_from(files: &FilesState, raw_fd: usize) -> Result { let rds = files.raw_descriptor_store.read(); - if let Ok(fd) = rds.fd_from_raw_integer::>(raw_fd) { + if let Ok(fd) = rds.fd_from_raw_integer::(raw_fd) { return Ok(EpollDescriptor::File(fd)); } if let Ok(fd) = rds.fd_from_raw_integer::>(raw_fd) { @@ -81,7 +81,7 @@ impl EpollDescriptor { enum DescriptorRef { Eventfd(Weak>>), Epoll(Weak>>), - File(Weak>), + File(Weak), Socket(Weak>), Pipe(Weak>), Unix(Weak>>), @@ -643,7 +643,7 @@ mod test { } fn setup_epoll() -> (crate::Task, EpollFile) { - let task = crate::syscalls::tests::init_platform_with_broker(); + let task = crate::syscalls::tests::init_platform(); let epoll = EpollFile::new(); (task, epoll) @@ -694,15 +694,15 @@ mod test { #[test] fn test_poll() { - let task = crate::syscalls::tests::init_platform_with_broker(); + let task = crate::syscalls::tests::init_platform(); let mut set = super::PollSet::with_capacity(0); let (rfd_u, wfd_u) = task - .sys_pipe2(litebox::fs::OFlags::empty()) + .sys_pipe2(litebox_common_linux::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 no_fds = FilesState::new(); let fds = task.files.borrow().clone(); set.add_fd(rfd, Events::IN); @@ -750,10 +750,10 @@ mod test { #[test] fn test_pselect() { - let task = crate::syscalls::tests::init_platform_with_broker(); + let task = crate::syscalls::tests::init_platform(); let (rfd_u, wfd_u) = task - .sys_pipe2(litebox::fs::OFlags::empty()) + .sys_pipe2(litebox_common_linux::OFlags::empty()) .expect("pipe2 failed"); let rfd = i32::try_from(rfd_u).unwrap(); let wfd = i32::try_from(wfd_u).unwrap(); @@ -790,10 +790,10 @@ mod test { #[test] fn test_pselect_read_hup() { - let task = crate::syscalls::tests::init_platform_with_broker(); + let task = crate::syscalls::tests::init_platform(); let (rfd_u, wfd_u) = task - .sys_pipe2(litebox::fs::OFlags::empty()) + .sys_pipe2(litebox_common_linux::OFlags::empty()) .expect("pipe2 failed"); let rfd = i32::try_from(rfd_u).unwrap(); let wfd = i32::try_from(wfd_u).unwrap(); diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index a3e39721b..ee969d187 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -13,10 +13,9 @@ use litebox::{ wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, - fs::OFlags, sync::RawSyncPrimitivesProvider, }; -use litebox_common_linux::{EfdFlags, errno::Errno}; +use litebox_common_linux::{EfdFlags, OFlags, errno::Errno}; use litebox_platform::time::TimeProvider; use crate::{GlobalState, ShimPlatform}; @@ -102,15 +101,15 @@ impl GlobalState { #[cfg(test)] mod tests { - use litebox_common_linux::{EfdFlags, errno::Errno}; + use litebox_common_linux::EfdFlags; #[test] - fn test_eventfd_requires_broker_control() { + fn test_eventfd_uses_broker_control() { let task = crate::syscalls::tests::init_platform(); - - assert!(matches!( - task.global.create_linux_eventfd(0, EfdFlags::NONBLOCK), - Err(Errno::EIO) - )); + let event = task + .global + .create_linux_eventfd(0, EfdFlags::NONBLOCK) + .unwrap(); + drop(event); } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index b8d6a76f2..0b873f7ed 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -11,22 +11,24 @@ use alloc::{ use litebox::{ event::{Events, wait::WaitError}, fd::{FdEnabledSubsystem, MetadataError, TypedFd}, - fs::{Mode, OFlags, SeekWhence}, + fs::errors::OpenError, mm::linux::PAGE_SIZE, path, stdio::StdioStream, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileMode as Mode, FileOpenFlags, FileSeekWhence as SeekWhence, FileStatus, + FileType, FileUser, +}; use litebox_common_linux::{ AccessFlags, AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, - InodeType, IoReadVec, IoWriteVec, IoctlArg, Statx, StatxMask, TimeParam, errno::Errno, + InodeType, IoReadVec, IoWriteVec, IoctlArg, OFlags, Statx, StatxMask, TimeParam, errno::Errno, signal::Signal, }; use thiserror::Error; -use crate::{ - FileFd, GlobalState, LinuxFS, ShimPlatform, Task, UserPtr, UserPtrMut, syscalls::signal, -}; +use crate::{FileFd, GlobalState, ShimPlatform, Task, UserPtr, UserPtrMut, syscalls::signal}; use core::sync::atomic::{AtomicUsize, Ordering}; #[derive(Clone, Copy)] @@ -35,8 +37,8 @@ struct AccessUserInfo { group: u32, } -impl From for AccessUserInfo { - fn from(value: litebox::fs::UserInfo) -> Self { +impl From for AccessUserInfo { + fn from(value: FileUser) -> Self { Self { user: u32::from(value.user), group: u32::from(value.group), @@ -49,7 +51,7 @@ pub(crate) struct FsState { umask: core::sync::atomic::AtomicU32, // XXX: the context also stores credentials, might need to reconsider design when implementing // `setuid` and similar. - pub(crate) context: litebox::sync::RwLock, + pub(crate) context: litebox::sync::RwLock, } impl Clone for FsState { @@ -64,7 +66,7 @@ impl Clone for FsState { impl FsState { /// Create the state for a task running as `credentials`. pub fn new(credentials: &super::process::Credentials) -> Self { - let user_info = litebox::fs::UserInfo { + let user_info = FileUser { // XXX: Linux ids are 32-bit, but the core litebox file system uses 16-bit ones, so we // may need to widen `UserInfo`. user: u16::try_from(credentials.euid) @@ -72,23 +74,66 @@ impl FsState { group: u16::try_from(credentials.egid) .unwrap_or_else(|_| unimplemented!("{}", credentials.egid)), }; - let mut context = litebox::fs::resolver::Context::new(); + let mut context = litebox::fs::Context::new(); context.set_acting_user(user_info); Self { - umask: (Mode::WGRP | Mode::WOTH).bits().into(), + umask: u32::from((Mode::WGRP | Mode::WOTH).bits()).into(), context: litebox::sync::RwLock::new(context), } } fn umask(&self) -> Mode { - Mode::from_bits_retain(self.umask.load(Ordering::Relaxed)) + Mode::from_u32_bits_truncate(self.umask.load(Ordering::Relaxed)) } } +/// Translate Linux open flags after descriptor-local `O_CLOEXEC` has been removed. +fn file_open_options(flags: OFlags) -> Result<(FileAccessMode, FileOpenFlags), OpenError> { + const SUPPORTED_FLAGS: OFlags = OFlags::CREAT + .union(OFlags::RDONLY) + .union(OFlags::WRONLY) + .union(OFlags::RDWR) + .union(OFlags::TRUNC) + .union(OFlags::NOCTTY) + .union(OFlags::EXCL) + .union(OFlags::DIRECTORY) + .union(OFlags::NONBLOCK) + .union(OFlags::LARGEFILE) + .union(OFlags::NOFOLLOW) + .union(OFlags::APPEND) + .union(OFlags::PATH); + + if flags.intersects(SUPPORTED_FLAGS.complement()) { + unimplemented!("{flags:?}") + } + let access = match flags.bits() & 3 { + 0 => FileAccessMode::ReadOnly, + 1 => FileAccessMode::WriteOnly, + 2 => FileAccessMode::ReadWrite, + _ => return Err(OpenError::AccessNotAllowed), + }; + let mut output = FileOpenFlags::NONE; + for (guest, broker) in [ + (OFlags::CREAT, FileOpenFlags::CREATE), + (OFlags::TRUNC, FileOpenFlags::TRUNCATE), + (OFlags::NOCTTY, FileOpenFlags::NO_CONTROLLING_TERMINAL), + (OFlags::EXCL, FileOpenFlags::EXCLUSIVE), + (OFlags::DIRECTORY, FileOpenFlags::DIRECTORY), + (OFlags::NONBLOCK, FileOpenFlags::NONBLOCKING), + (OFlags::LARGEFILE, FileOpenFlags::LARGE_FILE), + (OFlags::NOFOLLOW, FileOpenFlags::NO_FOLLOW), + (OFlags::APPEND, FileOpenFlags::APPEND), + (OFlags::PATH, FileOpenFlags::PATH), + ] { + if flags.contains(guest) { + output = output.union(broker); + } + } + Ok((access, output)) +} + /// Task state shared by `CLONE_FILES`. pub(crate) struct FilesState { - /// The filesystem implementation, shared across tasks that share file system. - pub(crate) fs: alloc::sync::Arc>, pub(crate) raw_descriptor_store: litebox::sync::RwLock, /// Exclusive upper bound for raw file descriptor values. @@ -96,9 +141,8 @@ pub(crate) struct FilesState { } impl FilesState { - pub(crate) fn new(fs: alloc::sync::Arc>) -> Self { + pub(crate) fn new() -> Self { Self { - fs, raw_descriptor_store: litebox::sync::RwLock::new( litebox::fd::RawDescriptorStorage::new(), ), @@ -169,7 +213,7 @@ impl FilesState { /// A raw fd resolved once into the subsystem that owns it. pub(crate) enum AnyTypedFd { - Fs(alloc::sync::Arc>), + Fs(alloc::sync::Arc), Network(alloc::sync::Arc>>), Pipes(alloc::sync::Arc>>), Eventfd(alloc::sync::Arc>>), @@ -207,7 +251,7 @@ impl AnyTypedFd { } /// The filesystem fd behind this descriptor, or `None` for every other subsystem. - pub(crate) fn as_fs(&self) -> Option<&FileFd> { + pub(crate) fn as_fs(&self) -> Option<&FileFd> { match self { Self::Fs(fd) => Some(fd), _ => None, @@ -215,14 +259,14 @@ impl AnyTypedFd { } /// Like [`Self::as_fs`], but fails with `otherwise` for non-filesystem descriptors. - pub(crate) fn fs_only(&self, otherwise: Errno) -> Result<&FileFd, Errno> { + pub(crate) fn fs_only(&self, otherwise: Errno) -> Result<&FileFd, Errno> { self.as_fs().ok_or(otherwise) } /// Run the handler matching this fd's subsystem. pub(crate) fn dispatch( &self, - fs: impl FnOnce(&FileFd) -> R, + fs: impl FnOnce(&FileFd) -> R, net: impl FnOnce(&TypedFd>) -> R, pipes: impl FnOnce(&TypedFd>) -> R, eventfd: impl FnOnce(&TypedFd>) -> R, @@ -360,7 +404,7 @@ impl Task { path: impl path::Arg, flags: OFlags, mode: Mode, - ) -> Result, Errno> { + ) -> Result { let mode = mode & !self.get_umask(); // TODO: Have the device backend attach stream identity once backends can set descriptor // metadata for newly opened files. @@ -381,12 +425,16 @@ impl Task { } }); let file = { - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files - .fs - .open(&context, &path, flags - OFlags::CLOEXEC, mode) + let path = path + .as_rust_str() + .map_err(litebox::fs::errors::PathError::from)?; + let (access, open_flags) = + file_open_options(flags - OFlags::CLOEXEC).map_err(Errno::from)?; + self.global + .litebox + .open_file(&context, path, access, open_flags, mode) .map_err(Errno::from) }?; if let Some(stream) = stream { @@ -406,12 +454,12 @@ impl Task { pathname: impl path::Arg, flags: OFlags, mode: Mode, - ) -> Result, Errno> { + ) -> Result { let path = self.resolve_path_at(dirfd, pathname)?; self.do_open(path, flags, mode) } - fn insert_raw_file_fd(&self, file: FileFd, flags: OFlags) -> Result { + fn insert_raw_file_fd(&self, file: FileFd, flags: OFlags) -> Result { if flags.contains(OFlags::CLOEXEC) { let None = self .global @@ -424,7 +472,7 @@ impl Task { } let files = self.files.borrow(); let raw_fd = files.insert_raw_fd(file).map_err(|file| { - files.fs.close(&file).unwrap(); + self.global.litebox.close_file(&file).unwrap(); Errno::EMFILE })?; Ok(u32::try_from(raw_fd).unwrap()) @@ -432,13 +480,14 @@ impl Task { /// Handle syscall `umask` pub(crate) fn sys_umask(&self, new_mask: u32) -> Mode { - let new_mask = Mode::from_bits_truncate(new_mask) & (Mode::RWXU | Mode::RWXG | Mode::RWXO); + let new_mask = + Mode::from_u32_bits_truncate(new_mask) & (Mode::RWXU | Mode::RWXG | Mode::RWXO); let old_mask = self .fs .borrow() .umask - .swap(new_mask.bits(), Ordering::Relaxed); - Mode::from_bits_retain(old_mask) + .swap(new_mask.bits().into(), Ordering::Relaxed); + Mode::from_u32_bits_truncate(old_mask) } /// Handle syscall `open` @@ -465,7 +514,12 @@ impl Task { let files = self.files.borrow(); let fd = files.typed_fd(fd)?; fd.dispatch( - |fd| files.fs.truncate(fd, length, false).map_err(Errno::from), + |fd| { + self.global + .litebox + .truncate_file(fd, length, false) + .map_err(Errno::from) + }, |_fd| todo!("net"), |_fd| todo!("pipes"), |_fd| Err(Errno::EINVAL), @@ -493,15 +547,14 @@ impl Task { }; match file_type { InodeType::File => { - let mode = Mode::from_bits_truncate(mode_and_type & !FILE_TYPE_MASK); + let mode = Mode::from_u32_bits_truncate(mode_and_type & !FILE_TYPE_MASK); let file = self.do_openat( dirfd, pathname, OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, mode, )?; - let files = self.files.borrow(); - let _ = files.fs.close(&file); + let _ = self.global.litebox.close_file(&file); } // TODO: Named pipe, socket, block and char files are not supported InodeType::NamedPipe @@ -526,13 +579,18 @@ impl Task { } let path = self.resolve_path_at(dirfd, pathname)?; - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); if flags.contains(AtFlags::AT_REMOVEDIR) { - files.fs.rmdir(&context, path).map_err(Errno::from) + self.global + .litebox + .rmdir_file(&context, path) + .map_err(Errno::from) } else { - files.fs.unlink(&context, path).map_err(Errno::from) + self.global + .litebox + .unlink_file(&context, path) + .map_err(Errno::from) } } @@ -551,15 +609,14 @@ impl Task { buf: &mut [u8], offset: Option, ) -> Result { - let files = self.files.borrow(); // We need to do this cell dance because otherwise Rust can't recognize that the two // closures are mutually exclusive. let buf: core::cell::RefCell<&mut [u8]> = core::cell::RefCell::new(buf); let result = fd.dispatch( |fd| { - files - .fs - .read(fd, &mut buf.borrow_mut(), offset) + self.global + .litebox + .read_file(fd, &mut buf.borrow_mut(), offset) .map_err(Errno::from) }, |fd| { @@ -636,10 +693,14 @@ impl Task { buf: &[u8], offset: Option, ) -> Result { - let files = self.files.borrow(); let is_inet_datagram = core::cell::Cell::new(false); let result = fd.dispatch( - |fd| files.fs.write(fd, buf, offset).map_err(Errno::from), + |fd| { + self.global + .litebox + .write_file(fd, buf, offset) + .map_err(Errno::from) + }, |fd| { espipe_for_non_seekable_offset(offset)?; is_inet_datagram.set(matches!( @@ -717,10 +778,9 @@ impl Task { let rewind = isize::try_from(unread_n).map_err(|_| Errno::EOVERFLOW)?; let fd = in_fd.fs_only(Errno::EINVAL)?; - let files = self.files.borrow(); - files - .fs - .seek(fd, -rewind, SeekWhence::RelativeToCurrentOffset) + self.global + .litebox + .seek_file(fd, -rewind, SeekWhence::RelativeToCurrentOffset) .map(|_| ()) .map_err(Errno::from) } @@ -746,10 +806,8 @@ impl Task { usize::try_from(off).map_err(|_| Errno::EINVAL) }) .transpose()?; - let mut kernel_buf = vec![0u8; count.min(PAGE_SIZE)]; let mut total: usize = 0; - let files = self.files.borrow(); while total < count { let to_read = (count - total).min(kernel_buf.len()); @@ -762,9 +820,10 @@ impl Task { Errno::EINVAL }; let read_result = match typed_in_fd.as_fs() { - Some(fd) => files - .fs - .read(fd, &mut kernel_buf[..to_read], cur_off) + Some(fd) => self + .global + .litebox + .read_file(fd, &mut kernel_buf[..to_read], cur_off) .map_err(Errno::from), None => Err(non_fs_err), }; @@ -845,8 +904,7 @@ impl Task { whence: SeekWhence, ) -> Result { let fd = fd.fs_only(Errno::ESPIPE)?; - let files = self.files.borrow(); - match files.fs.seek(fd, offset, whence) { + match self.global.litebox.seek_file(fd, offset, whence) { Ok(pos) => Ok(pos), Err(litebox::fs::errors::SeekError::NotAFile) => { let base = match whence { @@ -872,12 +930,11 @@ impl Task { fn do_mkdir(&self, pathname: impl path::Arg, mode: Mode) -> Result<(), Errno> { let mode = mode & !self.get_umask(); - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files - .fs - .mkdir(&context, pathname, mode) + self.global + .litebox + .mkdir_file(&context, pathname, mode) .map_err(Errno::from) } @@ -889,11 +946,11 @@ impl Task { mode: u32, ) -> Result<(), Errno> { let pathname = self.resolve_path_at(dirfd, pathname)?; - self.do_mkdir(pathname, Mode::from_bits_retain(mode)) + self.do_mkdir(pathname, Mode::from_u32_bits_truncate(mode)) } pub(crate) fn do_close(&self, raw_fd: usize) -> Result<(), Errno> { - self.do_close_and_replace::>(raw_fd, None) + self.do_close_and_replace::(raw_fd, None) } pub(super) fn remove_and_drop_descriptor(&self, fd: &TypedFd) { @@ -916,7 +973,7 @@ impl Task { let files = self.files.borrow(); let mut rds = files.raw_descriptor_store.write(); let consumed: AnyTypedFd = match rds - .fd_consume_raw_integer::>(raw_fd) + .fd_consume_raw_integer::(raw_fd) { Ok(fd) => AnyTypedFd::Fs(fd), Err(litebox::fd::ErrRawIntFd::NotFound) => { @@ -968,7 +1025,7 @@ impl Task { if let Ok(raw_fd) = i32::try_from(raw_fd) { self.finalize_elf_patch(raw_fd); } - files.fs.close(&fd).map_err(Errno::from) + self.global.litebox.close_file(&fd).map_err(Errno::from) } AnyTypedFd::Network(fd) => self.global.close_socket(&self.wait_cx(), fd), AnyTypedFd::Pipes(fd) => self.global.close_linux_pipe(&fd), @@ -1401,10 +1458,9 @@ impl Task { caller: AccessUserInfo, ) -> Result<(), Errno> { let status = { - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files.fs.file_status(&context, pathname)? + self.global.litebox.path_file_status(&context, pathname)? }; let owner = status.owner.into(); Self::do_access_mode(status.mode, owner, caller, &mode) @@ -1437,13 +1493,20 @@ impl Task { self.do_access(cwd, mode, caller) } FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { - let stat: FileStat = self.with_typed_fd(fd, |fd| self.do_stat(fd))?; + let files = self.files.borrow(); + let typed_fd = files.typed_fd(fd)?; + if let Some(file) = typed_fd.as_fs() { + let status = self.global.litebox.file_status(file)?; + return Self::do_access_mode(status.mode, status.owner.into(), caller, &mode); + } + drop(files); + let stat: FileStat = self.do_stat(&typed_fd)?; let owner = AccessUserInfo { user: stat.st_uid, group: stat.st_gid, }; Self::do_access_mode( - Mode::from_bits_truncate(stat.st_mode & 0o7777), + Mode::from_u32_bits_truncate(stat.st_mode & 0o7777), owner, caller, &mode, @@ -1526,9 +1589,16 @@ fn set_file_descriptor_flags( } impl Task { + /// Query filesystem metadata without narrowing it to a guest `stat` layout. + pub(crate) fn file_status(&self, fd: i32) -> Result { + let files = self.files.borrow(); + let fd = files.typed_fd(fd)?; + Ok(self.global.litebox.file_status(fd.fs_only(Errno::EBADF)?)?) + } + pub(crate) fn do_stat(&self, fd: &AnyTypedFd) -> Result where - T: From + From, + T: TryFrom + From, { // TODO: give correct values for the synthesized branches. let synthetic = |mode_bits: u32, blksize: usize| FileStat { @@ -1552,17 +1622,10 @@ impl Task { ..Default::default() }; let socket_mode = litebox_common_linux::InodeType::Socket as u32 - | (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits(); - let rw_user_mode = (Mode::RUSR | Mode::WUSR).bits(); - let files = self.files.borrow(); + | u32::from((Mode::RWXU | Mode::RWXG | Mode::RWXO).bits()); + let rw_user_mode = u32::from((Mode::RUSR | Mode::WUSR).bits()); fd.dispatch( - |fd| { - files - .fs - .fd_file_status(fd) - .map(T::from) - .map_err(Errno::from) - }, + |fd| T::try_from(self.global.litebox.file_status(fd)?), |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { Ok(T::from(synthetic( @@ -1579,7 +1642,7 @@ impl Task { /// Get the file status of `pathname`. /// /// The `pathname` must be absolute. - fn do_path_stat>( + fn do_path_stat>( &self, pathname: impl path::Arg, follow_symlink: bool, @@ -1592,12 +1655,11 @@ impl Task { normalized_path }; let status = { - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files.fs.file_status(&context, path)? + self.global.litebox.path_file_status(&context, path)? }; - Ok(T::from(status)) + T::try_from(status) } /// Handle syscall `stat` @@ -1628,7 +1690,7 @@ impl Task { flags: AtFlags, ) -> Result where - T: From + From, + T: TryFrom + From, { let get_cwd = || self.cwd_prefix(); let fs_path = FsPath::new(dirfd, pathname, get_cwd)?; @@ -1640,10 +1702,9 @@ impl Task { // Take the cwd before locking the context: this lock is not recursive, so a // waiting writer would deadlock a nested read. let cwd = get_cwd(); - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - Ok(T::from(files.fs.file_status(&context, cwd)?)) + T::try_from(self.global.litebox.path_file_status(&context, cwd)?) } FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { self.with_typed_fd(fd, |fd| self.do_stat(fd)) @@ -1913,7 +1974,6 @@ impl Task { /// Handle syscall `chdir` pub fn sys_chdir(&self, pathname: impl path::Arg) -> Result<(), Errno> { - use litebox::fs::FileType; use litebox::fs::errors::{FileStatusError, PathError}; let fs = self.fs.borrow(); @@ -1934,9 +1994,12 @@ impl Task { // Verify the path exists and is a directory. { - let files = self.files.borrow(); let context = fs.context.read(); - match files.fs.file_status(&context, target.to_string()) { + match self + .global + .litebox + .path_file_status(&context, target.to_string()) + { Ok(status) => { if status.file_type != FileType::Directory { return Err(Errno::ENOTDIR); @@ -2048,13 +2111,12 @@ impl Task { } } - fn is_stdio(&self, fs: &LinuxFS, fd: &FileFd) -> Result { - match fs.fd_file_status(fd) { + fn is_stdio(&self, fs: &litebox::LiteBox, fd: &FileFd) -> Result { + match fs.file_status(fd) { Ok(status) => { // See https://www.kernel.org/doc/Documentation/admin-guide/devices.txt let major = status.node_info.rdev.map_or(0, |v| v.get() >> 8); - Ok((136..=143).contains(&major) - && status.file_type == litebox::fs::FileType::CharacterDevice) + Ok((136..=143).contains(&major) && status.file_type == FileType::CharacterDevice) } Err(litebox::fs::errors::FileStatusError::ClosedFd) => Err(Errno::EBADF), Err(_) => unimplemented!(), @@ -2147,7 +2209,7 @@ impl Task { | IoctlArg::TIOCGWINSZ(..) => { let fd = files.typed_fd(fd)?; let fd = fd.fs_only(Errno::ENOTTY)?; - if !self.is_stdio(&files.fs, fd)? { + if !self.is_stdio(self.global.litebox.as_ref(), fd)? { return Err(Errno::ENOTTY); } let stream = self @@ -2608,7 +2670,7 @@ impl Task { file.dispatch( |fd| { dup(self, &files, fd, close_on_exec, target, |fd| { - let _ = files.fs.close(&fd); + let _ = self.global.litebox.close_file(&fd); }) }, |fd| { @@ -2733,7 +2795,7 @@ impl Task { let mut dir_off = dir_off.0; let mut nbytes = 0; - let mut entries = files.fs.read_dir(file)?; + let mut entries = self.global.litebox.read_file_directory(file)?; entries.sort_by(|a, b| a.name.cmp(&b.name)); for entry in entries.iter().skip(dir_off) { @@ -2749,10 +2811,10 @@ impl Task { break; } let dirent64 = litebox_common_linux::LinuxDirent64 { - ino: entry.ino_info.as_ref().map_or(0, |node_info| node_info.ino) as u64, + ino: entry.ino_info.as_ref().map_or(0, |node_info| node_info.ino), off: dir_off as u64, - len: len.trunc(), - typ: litebox_common_linux::DirentType::from(entry.file_type.clone()) as u8, + len: u16::try_from(len).map_err(|_| Errno::EOVERFLOW)?, + typ: litebox_common_linux::DirentType::from(entry.file_type) as u8, __name: [0; 0], }; let hdr_ptr = UserPtrMut::from_usize(dirp.as_usize() + nbytes); @@ -2789,7 +2851,6 @@ mod tests { use super::*; use alloc::string::String; use core::cell::Cell; - use litebox::fs::{Mode, OFlags}; extern crate std; @@ -2979,42 +3040,61 @@ mod tests { #[test] fn getcwd_and_chdir() { - let task = crate::syscalls::tests::init_platform(); + use crate::syscalls::test_broker::{FileCall, failed, path_status}; + use crate::syscalls::tests::{ROOT, scripted_task}; + use litebox_broker_protocol::fs::{FileError, FileType}; - // Default CWD is root. + let (files, task) = scripted_task([]); + + // The default CWD is the root, and reporting it takes no broker request. let mut buf = [0u8; 256]; let len = task.sys_getcwd(&mut buf).unwrap(); let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); // strip NUL assert_eq!(cwd, "/"); + assert!(files.take_calls().is_empty()); - // chdir + getcwd round trip. - task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "/test_chdir_dir", 0o777) - .unwrap(); + // `chdir` asks the broker for the target's status, then tracks it locally. + files.script([path_status(FileType::Directory, 0o755)]); task.sys_chdir("/test_chdir_dir").unwrap(); + assert_eq!( + files.take_calls(), + std::vec![FileCall::PathStatus { + path: "/test_chdir_dir".into(), + user: ROOT, + }] + ); let len = task.sys_getcwd(&mut buf).unwrap(); - let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); - assert_eq!(cwd, "/test_chdir_dir"); + assert_eq!( + core::str::from_utf8(&buf[..len - 1]).unwrap(), + "/test_chdir_dir" + ); - // chdir to nonexistent path → ENOENT. + // A missing target leaves the CWD alone. + files.script([failed(FileError::NoSuchFileOrDirectory)]); assert_eq!( task.sys_chdir("/does_not_exist").unwrap_err(), Errno::ENOENT ); + let _ = files.take_calls(); + + // An empty path is rejected before the broker is asked. assert_eq!(task.sys_chdir("").unwrap_err(), Errno::ENOENT); + assert!(files.take_calls().is_empty()); - // chdir to a regular file → ENOTDIR. - let fd = task - .sys_open( - "/test_chdir_file", - litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, - Mode::RUSR | Mode::WUSR, - ) - .unwrap(); - let _ = task.sys_close(i32::try_from(fd).unwrap()); + // A non-directory target is rejected by the shim, which checks the reported type. + files.script([path_status(FileType::RegularFile, 0o644)]); assert_eq!( task.sys_chdir("/test_chdir_file").unwrap_err(), Errno::ENOTDIR ); + let _ = files.take_calls(); + + // The CWD is unchanged by the failed calls above. + let len = task.sys_getcwd(&mut buf).unwrap(); + assert_eq!( + core::str::from_utf8(&buf[..len - 1]).unwrap(), + "/test_chdir_dir" + ); // getcwd with too-small buffer → ERANGE. let mut tiny = [0u8; 1]; @@ -3022,40 +3102,48 @@ mod tests { } #[test] - fn chdir_relative_path() { - let task = crate::syscalls::tests::init_platform(); + fn chdir_normalizes_relative_paths() { + use crate::syscalls::test_broker::path_status; + use crate::syscalls::tests::scripted_task; + use litebox_broker_protocol::fs::FileType; - // Create nested dirs: /rel_parent/rel_child - 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(); + let (files, task) = scripted_task([ + path_status(FileType::Directory, 0o755), + path_status(FileType::Directory, 0o755), + path_status(FileType::Directory, 0o755), + ]); - // chdir to /rel_parent first, then relative chdir into child. task.sys_chdir("/rel_parent").unwrap(); task.sys_chdir("rel_child").unwrap(); - let mut buf = [0u8; 256]; let len = task.sys_getcwd(&mut buf).unwrap(); - let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); - assert_eq!(cwd, "/rel_parent/rel_child"); + assert_eq!( + core::str::from_utf8(&buf[..len - 1]).unwrap(), + "/rel_parent/rel_child" + ); - // chdir("..") should normalize back to /rel_parent. task.sys_chdir("..").unwrap(); let len = task.sys_getcwd(&mut buf).unwrap(); - let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); - assert_eq!(cwd, "/rel_parent"); + assert_eq!( + core::str::from_utf8(&buf[..len - 1]).unwrap(), + "/rel_parent" + ); + + assert_eq!( + files.take_paths(), + std::vec!["/rel_parent", "/rel_parent/rel_child", "/rel_parent"], + "the broker only ever sees normalized absolute paths" + ); } #[test] fn mknodat_regular_file_does_not_consume_fd_limit() { + use crate::syscalls::test_broker::{FileCall, closed, opened}; + use crate::syscalls::tests::{FILE_HANDLE, ROOT, scripted_task}; + use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; use litebox_common_linux::{Rlimit, RlimitResource}; - let task = crate::syscalls::tests::init_platform(); + let (files, task) = scripted_task([opened(FILE_HANDLE), closed()]); let old_limit = task.do_prlimit(RlimitResource::NOFILE, None).unwrap(); task.do_prlimit( RlimitResource::NOFILE, @@ -3065,25 +3153,39 @@ mod tests { }), ) .unwrap(); - let path = "/mknodat_at_fd_limit"; - let result = task.sys_mknodat( - litebox_common_linux::AT_FDCWD, - path, - InodeType::File as u32 | (Mode::RUSR | Mode::WUSR).bits(), - 0, + assert_eq!( + task.sys_mknodat( + litebox_common_linux::AT_FDCWD, + "/mknodat_at_fd_limit", + InodeType::File as u32 | u32::from((Mode::RUSR | Mode::WUSR).bits()), + 0, + ), + Ok(()) ); - - assert!( - task.sys_stat(path).is_ok(), - "mknodat created the file before returning {result:?}" + assert_eq!( + files.take_calls(), + std::vec![ + FileCall::Open { + path: "/mknodat_at_fd_limit".into(), + user: ROOT, + access: FileAccessMode::WriteOnly, + flags: FileOpenFlags::CREATE.union(FileOpenFlags::EXCLUSIVE), + mode: crate::syscalls::tests::mode(0o600), + }, + FileCall::Close(FILE_HANDLE), + ], + "the created file is closed again rather than taking a descriptor" ); - assert_eq!(result, Ok(())); } #[test] fn empty_pathnames_return_enoent() { - let task = crate::syscalls::tests::init_platform(); + use crate::syscalls::tests::scripted_task; + + // Nothing here may reach the broker, so the script is empty: an unexpected file request + // would panic instead of being silently answered. + let (files, task) = scripted_task([]); assert_eq!( task.sys_open("", OFlags::RDONLY, Mode::empty()) @@ -3110,7 +3212,7 @@ mod tests { task.sys_mknodat( litebox_common_linux::AT_FDCWD, "", - InodeType::File as u32 | Mode::RWXU.bits(), + InodeType::File as u32 | u32::from(Mode::RWXU.bits()), 0, ) .unwrap_err(), @@ -3122,37 +3224,46 @@ mod tests { .unwrap_err(), Errno::ENOENT ); + assert!(files.take_calls().is_empty()); } /// Verify every path-taking syscall resolves relative paths after `chdir`. #[test] fn all_path_syscalls_respect_chdir() { + use crate::syscalls::test_broker::{Scripted, closed, opened, path_status}; + use crate::syscalls::tests::{FILE_HANDLE, scripted_task}; + use litebox_broker_protocol::fs::FileType; + use litebox_broker_protocol::message::FileResponse; use litebox_common_linux::{AccessFlags, AtFlags}; - let task = crate::syscalls::tests::init_platform(); + let (files, task) = scripted_task([ + path_status(FileType::Directory, 0o755), + opened(FILE_HANDLE), + closed(), + path_status(FileType::RegularFile, 0o644), + path_status(FileType::RegularFile, 0o644), + path_status(FileType::RegularFile, 0o644), + Scripted::Reply(FileResponse::Mkdir), + opened(FILE_HANDLE), + closed(), + path_status(FileType::RegularFile, 0o644), + Scripted::Reply(FileResponse::Unlink), + Scripted::Reply(FileResponse::Rmdir), + ]); - // Set up: mkdir + chdir into /cwd_test/. - 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 ── let fd = task .sys_open( "file.txt", - litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, + OFlags::CREAT | OFlags::WRONLY, Mode::RUSR | Mode::WUSR, ) .unwrap(); task.sys_close(i32::try_from(fd).unwrap()).unwrap(); - // ── sys_stat: stat the relative file ── task.sys_stat("file.txt").unwrap(); - - // ── sys_lstat: lstat the relative file ── task.sys_lstat("file.txt").unwrap(); - - // ── sys_faccessat: check relative file is accessible ── task.sys_faccessat( litebox_common_linux::AT_FDCWD, "file.txt", @@ -3161,23 +3272,19 @@ mod tests { ) .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 ── let fd = task .sys_openat( litebox_common_linux::AT_FDCWD, "subdir/inner.txt", - litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, + OFlags::CREAT | OFlags::WRONLY, Mode::RUSR | Mode::WUSR, ) .unwrap(); task.sys_close(i32::try_from(fd).unwrap()).unwrap(); - // ── sys_newfstatat (AT_FDCWD + relative) ── task.sys_newfstatat( litebox_common_linux::AT_FDCWD, "subdir/inner.txt", @@ -3185,28 +3292,34 @@ mod tests { ) .unwrap(); - // ── sys_unlinkat: remove a file via relative path ── task.sys_unlinkat( litebox_common_linux::AT_FDCWD, "subdir/inner.txt", AtFlags::empty(), ) .unwrap(); - assert_eq!( - task.sys_stat("/cwd_test/subdir/inner.txt").unwrap_err(), - Errno::ENOENT - ); - - // ── sys_unlinkat (AT_REMOVEDIR): remove directory via relative path ── task.sys_unlinkat( litebox_common_linux::AT_FDCWD, "subdir", AtFlags::AT_REMOVEDIR, ) .unwrap(); + assert_eq!( - task.sys_stat("/cwd_test/subdir").unwrap_err(), - Errno::ENOENT + files.take_paths(), + std::vec![ + "/cwd_test", + "/cwd_test/file.txt", + "/cwd_test/file.txt", + "/cwd_test/file.txt", + "/cwd_test/file.txt", + "/cwd_test/subdir", + "/cwd_test/subdir/inner.txt", + "/cwd_test/subdir/inner.txt", + "/cwd_test/subdir/inner.txt", + "/cwd_test/subdir", + ], + "every relative path is resolved against the CWD before it reaches the broker" ); } } diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 0d563a610..93f44de6d 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -5,13 +5,7 @@ //! Most of these syscalls which are not backed by files are implemented in [`litebox_common_linux::mm`]. use alloc::collections::{BTreeMap, BTreeSet}; -use litebox::{ - mm::linux::{MappingError, PAGE_SIZE, PageRange}, - platform::{ - PageManagementProvider, RawConstPointer, - page_mgmt::{FixedAddressBehavior, MemoryRegionPermissions}, - }, -}; +use litebox::mm::linux::{MappingError, PAGE_SIZE}; use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, errno::Errno}; use crate::ShimPlatform; @@ -24,8 +18,6 @@ use alloc::vec::Vec; use core::ops::Range; #[cfg(target_arch = "aarch64")] use litebox::mm::linux::VmFlags; -#[cfg(target_arch = "aarch64")] -use litebox::utils::ReinterpretUnsignedExt as _; use litebox::utils::TruncateExt as _; use object::elf::{ET_DYN, FileHeader64, PT_LOAD, ProgramHeader64}; use object::endian::LittleEndian; @@ -214,14 +206,8 @@ impl Task { let is_exec = prot.contains(ProtFlags::PROT_EXEC); let typed_fd = self.typed_fd(fd).map_err(|_| MappingError::BadFD(fd))?; - // Perform the normal mmap first (CoW or memcpy fallback). - let result = if let Some(cow_result) = - self.try_cow_mmap_file(suggested_addr, len, &prot, &flags, &typed_fd, offset) - { - cow_result? - } else { - self.do_mmap_file_memcpy(suggested_addr, len, prot, flags, &typed_fd, offset)? - }; + let result = + self.do_mmap_file_memcpy(suggested_addr, len, prot, flags, &typed_fd, offset)?; // Runtime syscall rewriting: patch PROT_EXEC segments in-place. if is_exec { @@ -255,98 +241,7 @@ impl Task { Ok(result) } - /// Attempt to create a CoW mapping for a file with static backing data. - /// - /// Returns `Some(result)` if CoW was attempted (success or failure), - /// `None` if CoW is not applicable (fall back to memcpy). - // TODO(jb): does this need to be Option-Result or can it just be Option? - fn try_cow_mmap_file( - &self, - suggested_addr: Option, - len: usize, - prot: &ProtFlags, - flags: &MapFlags, - fd: &AnyTypedFd, - offset: usize, - ) -> Option, MappingError>> { - if !len.is_multiple_of(PAGE_SIZE) { - return None; - } - - let files = self.files.borrow(); - let static_data = files.fs.get_static_backing_data(fd.as_fs()?)?; - - if offset > static_data.len() { - return None; - } - - let available_len = static_data.len().saturating_sub(offset); - if available_len < len { - // Cannot fill full page - return None; - } - - let fixed_behavior = if flags.contains(MapFlags::MAP_FIXED_NOREPLACE) { - FixedAddressBehavior::NoReplace - } else if flags.contains(MapFlags::MAP_FIXED) { - FixedAddressBehavior::Replace - } else { - FixedAddressBehavior::Hint - }; - - let permissions = { - let mut perms = MemoryRegionPermissions::empty(); - perms.set( - MemoryRegionPermissions::READ, - prot.contains(ProtFlags::PROT_READ), - ); - perms.set( - MemoryRegionPermissions::WRITE, - prot.contains(ProtFlags::PROT_WRITE), - ); - perms.set( - MemoryRegionPermissions::EXEC, - prot.contains(ProtFlags::PROT_EXEC), - ); - perms - }; - - // XXX: `try_allocate_cow_pages` and `register_existing_mapping` are not called under a - // unified lock, so there is a theoretical race if two threads concurrently attempt a - // fixed-address mapping with replacement at the same address. In practice this is benign: - // if a program races like this both threads will register the same mapping anyway. Updating - // to a begin/attempt/commit scheme could close this race window entirely. - match <_ as PageManagementProvider<{ PAGE_SIZE }>>::try_allocate_cow_pages( - self.global.platform, - suggested_addr.unwrap_or(0), - &static_data[offset..offset + len], - permissions, - fixed_behavior, - ) { - Ok(ptr) => { - let range = - PageRange::new(ptr.as_usize(), ptr.as_usize().checked_add(len).unwrap()) - .unwrap(); - // SAFETY: ptr is the freshly CoW-mapped region of exactly `len` bytes with - // `permissions`. - unsafe { - self.global.pm.register_existing_mapping( - range, - permissions, - true, - fixed_behavior == FixedAddressBehavior::Replace, - flags.contains(MapFlags::MAP_SHARED), - ) - } - .unwrap(); - Some(Ok(UserPtrMut::from_platform_ptr::(ptr))) - } - Err(_cow_not_supported) => None, - } - } - - /// Fallback mmap implementation using page-by-page memcpy, for files where the CoW attempt - /// fails (either due to lack of support on platform, or non-static-backed data, etc.) + /// Map a file by reading its contents through the filesystem API into allocated pages. fn do_mmap_file_memcpy( &self, suggested_addr: Option, @@ -807,20 +702,17 @@ impl Task { let code_metadata = if pre_patched { None } else { - self.sys_fstat(fd).ok().and_then(|stat| { - let file_size: usize = stat.st_size.reinterpret_as_unsigned().trunc(); + self.file_status(fd).ok().and_then(|stat| { + let file_size = usize::try_from(stat.size).ok()?; let word_len = file_size.div_ceil(8); let mut words = u64::new_vec_zeroed(word_len).ok()?; let bytes = zerocopy::IntoBytes::as_mut_bytes(words.as_mut_slice()); - match self.sys_read(fd, &mut bytes[..file_size], Some(0)) { - Ok(n) if n == file_size => { - litebox_syscall_rewriter::aarch64::ElfCodeMetadata::parse_aligned_in_place( - &mut words, file_size, - ) - .ok() - } - _ => None, - } + self.read_file_exact_at(fd, &mut bytes[..file_size], 0) + .ok()?; + litebox_syscall_rewriter::aarch64::ElfCodeMetadata::parse_aligned_in_place( + &mut words, file_size, + ) + .ok() }) }; #[cfg(all(target_arch = "aarch64", feature = "aarch64_virtualize_x18"))] @@ -952,25 +844,39 @@ impl Task { true } + fn read_file_exact_at( + &self, + fd: i32, + mut data: &mut [u8], + mut offset: usize, + ) -> Result<(), Errno> { + while !data.is_empty() { + let read = self.sys_read(fd, data, Some(offset))?; + if read == 0 { + return Err(Errno::EIO); + } + offset = offset.checked_add(read).ok_or(Errno::EOVERFLOW)?; + data = &mut data[read..]; + } + Ok(()) + } + /// Check if a file has the LITEBOX trampoline magic at its tail. /// Returns (is_pre_patched, file_offset, vaddr, trampoline_size). fn check_trampoline_magic(&self, fd: i32) -> (bool, u64, u64, u64) { const HEADER_SIZE: usize = 32; // TrampolineHeader64: magic(8) + file_offset(8) + vaddr(8) + size(8) - let Ok(stat) = self.sys_fstat(fd) else { + let Ok(stat) = self.file_status(fd) else { return (false, 0, 0, 0); }; - #[cfg(target_arch = "x86_64")] - let file_size: usize = stat.st_size; - #[cfg(target_arch = "aarch64")] - let file_size: usize = { - // The asm-generic ABI uses signed `st_size`. - stat.st_size.reinterpret_as_unsigned().trunc() + let Some(tail_offset) = stat.size.checked_sub(HEADER_SIZE as u64) else { + return (false, 0, 0, 0); }; - if file_size < HEADER_SIZE { + let Ok(tail_offset) = usize::try_from(tail_offset) else { return (false, 0, 0, 0); - } + }; + let mut tail = [0u8; HEADER_SIZE]; - match self.sys_read(fd, &mut tail, Some(file_size - HEADER_SIZE)) { + match self.sys_read(fd, &mut tail, Some(tail_offset)) { Ok(n) if n == HEADER_SIZE => {} _ => return (false, 0, 0, 0), } @@ -1163,12 +1069,12 @@ impl Task { let mut tramp_data = alloc::vec![0u8; state.trampoline_file_size]; let file_off = state.trampoline_file_offset.trunc(); let tramp_ptr = UserPtrMut::::from_usize(tramp_addr); - match self.sys_read(fd, &mut tramp_data, Some(file_off)) { - Ok(n) if n == tramp_data.len() => {} - _ => { - let _ = self.sys_munmap_raw(tramp_ptr, tramp_len); - return false; - } + if self + .read_file_exact_at(fd, &mut tramp_data, file_off) + .is_err() + { + let _ = self.sys_munmap_raw(tramp_ptr, tramp_len); + return false; } // Write syscall entry point to the first 8 bytes. @@ -1540,14 +1446,77 @@ impl Task { #[cfg(test)] mod tests { - use litebox::{ - fs::{Mode, OFlags}, - platform::PageManagementProvider, + use litebox::{mm::linux::PAGE_SIZE, platform::PageManagementProvider}; + use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags}; + use litebox_common_linux::{MRemapFlags, MapFlags, OFlags, ProtFlags, errno::Errno}; + use object::{elf::FileHeader64, endian::LittleEndian}; + + use crate::syscalls::test_broker::{FileCall, Scripted, ScriptedFiles, closed, opened}; + use crate::syscalls::tests::{ + FILE_HANDLE, ROOT, TestPlatform as Platform, init_platform, mode, scripted_task, }; - use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, errno::Errno}; + use crate::{Task, UserPtrMut}; + + fn open_scripted_mmap_file(files: &ScriptedFiles, task: &Task, path: &str) -> i32 { + files.script([opened(FILE_HANDLE)]); + let fd = i32::try_from( + task.sys_open(path, OFlags::RDONLY, Mode::empty()) + .expect("the scripted open must succeed"), + ) + .unwrap(); + assert_eq!( + files.take_calls(), + alloc::vec![FileCall::Open { + path: path.into(), + user: ROOT, + access: FileAccessMode::ReadOnly, + flags: FileOpenFlags::from_bits(0).unwrap(), + mode: mode(0), + }] + ); + fd + } + + fn script_mmap_bytes(files: &ScriptedFiles, data: &[u8]) { + files.script([ + Scripted::Read(data.to_vec()), + Scripted::Read(alloc::vec![]), + Scripted::Read(alloc::vec![]), + ]); + } - use crate::syscalls::tests::TestPlatform as Platform; - use crate::{UserPtrMut, syscalls::tests::init_platform}; + fn assert_mmap_reads(files: &ScriptedFiles, data_len: usize) { + assert_eq!( + files.take_calls(), + alloc::vec![ + FileCall::Read { + handle: FILE_HANDLE, + length: u32::try_from(PAGE_SIZE).unwrap(), + offset: Some(0), + }, + FileCall::Read { + handle: FILE_HANDLE, + length: u32::try_from(PAGE_SIZE).unwrap(), + offset: Some(u64::try_from(data_len).unwrap()), + }, + FileCall::Read { + handle: FILE_HANDLE, + length: u32::try_from(core::mem::size_of::>()) + .unwrap(), + offset: Some(0), + }, + ] + ); + } + + fn close_scripted_mmap_file(files: &ScriptedFiles, task: &Task, fd: i32) { + files.script([closed()]); + task.sys_close(fd).expect("the scripted close must succeed"); + assert_eq!( + files.take_calls(), + alloc::vec![FileCall::Close(FILE_HANDLE)] + ); + } /// Fail closed: an unpatched placeholder executes silently. #[cfg(target_arch = "aarch64")] @@ -1728,14 +1697,10 @@ mod tests { #[test] fn test_file_backed_mmap() { - let task = init_platform(); - let content = b"Hello, world!"; - let fd = task - .sys_open("test.txt", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) - .unwrap(); - let fd = i32::try_from(fd).unwrap(); - assert_eq!(task.sys_write(fd, content, None).unwrap(), content.len()); + let (files, task) = scripted_task([]); + let fd = open_scripted_mmap_file(&files, &task, "/test.txt"); + script_mmap_bytes(&files, content); let addr = task .sys_mmap( 0, @@ -1752,8 +1717,9 @@ mod tests { .as_ref(), content.as_slice(), ); + assert_mmap_reads(&files, content.len()); task.sys_munmap(addr, 0x1000).unwrap(); - task.sys_close(fd).unwrap(); + close_scripted_mmap_file(&files, &task, fd); } #[test] @@ -2058,14 +2024,10 @@ mod tests { #[test] fn test_map_shared_readonly_file() { - let task = init_platform(); - let content = b"Hello, shared!"; - let fd = task - .sys_open("shared.txt", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) - .unwrap(); - let fd = i32::try_from(fd).unwrap(); - assert_eq!(task.sys_write(fd, content, None).unwrap(), content.len()); + let (files, task) = scripted_task([]); + let fd = open_scripted_mmap_file(&files, &task, "/shared.txt"); + script_mmap_bytes(&files, content); // MAP_SHARED with PROT_READ on a file should succeed let addr = task @@ -2079,6 +2041,7 @@ mod tests { .as_ref(), content.as_slice(), ); + assert_mmap_reads(&files, content.len()); // mprotect to add write permission should fail let err = task @@ -2087,7 +2050,7 @@ mod tests { assert_eq!(err, Errno::EACCES); task.sys_munmap(addr, 0x1000).unwrap(); - task.sys_close(fd).unwrap(); + close_scripted_mmap_file(&files, &task, fd); } #[test] diff --git a/litebox_shim_linux/src/syscalls/mod.rs b/litebox_shim_linux/src/syscalls/mod.rs index 53dd561f4..d6502202f 100644 --- a/litebox_shim_linux/src/syscalls/mod.rs +++ b/litebox_shim_linux/src/syscalls/mod.rs @@ -15,17 +15,21 @@ pub(crate) mod unix; pub(crate) mod signal; #[cfg(test)] +pub(crate) mod test_broker; +#[cfg(test)] pub(crate) mod tests; macro_rules! common_functions_for_file_status { () => { - pub(crate) fn get_status(&self) -> litebox::fs::OFlags { - litebox::fs::OFlags::from_bits(self.status.load(core::sync::atomic::Ordering::Relaxed)) - .unwrap() - & litebox::fs::OFlags::STATUS_FLAGS_MASK + pub(crate) fn get_status(&self) -> litebox_common_linux::OFlags { + litebox_common_linux::OFlags::from_bits( + self.status.load(core::sync::atomic::Ordering::Relaxed), + ) + .unwrap() + & litebox_common_linux::OFlags::STATUS_FLAGS_MASK } - pub(crate) fn set_status(&self, flag: litebox::fs::OFlags, on: bool) { + pub(crate) fn set_status(&self, flag: litebox_common_linux::OFlags, on: bool) { if on { self.status .fetch_or(flag.bits(), core::sync::atomic::Ordering::Relaxed); diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index 0d3c17754..c09625ee5 100644 --- a/litebox_shim_linux/src/syscalls/net.rs +++ b/litebox_shim_linux/src/syscalls/net.rs @@ -18,7 +18,6 @@ use litebox::{ wait::{WaitContext, WaitError}, }, fd::EntryHandle, - fs::OFlags, mm::linux::PAGE_SIZE, net::{ CloseBehavior, SOCKET_RECEIVE_OPERATION_SIZE, TcpOptionData, @@ -30,7 +29,7 @@ use litebox::{ utils::TruncateExt as _, }; use litebox_common_linux::{ - AddressFamily, FileDescriptorFlags, IPProtocol, ReceiveFlags, SendFlags, ShutdownHow, + AddressFamily, FileDescriptorFlags, IPProtocol, OFlags, ReceiveFlags, SendFlags, ShutdownHow, SockFlags, SockType, SocketOption, SocketOptionName, TcpOption, UnixProtocol, UserMmsgHdr, UserMsgHdr, errno::Errno, signal::Signal, }; @@ -1260,12 +1259,12 @@ impl GlobalState { }) } - fn get_status(&self, fd: &SocketFd) -> litebox::fs::OFlags { + fn get_status(&self, fd: &SocketFd) -> OFlags { self.litebox .descriptor_table() .with_metadata(fd, |SocketOFlags(flags)| *flags) .unwrap() - & litebox::fs::OFlags::STATUS_FLAGS_MASK + & OFlags::STATUS_FLAGS_MASK } pub(crate) fn get_proxy( @@ -3740,6 +3739,9 @@ mod unix_tests { use alloc::{string::ToString, vec::Vec}; use litebox::event::Events; + use litebox_broker_protocol::ObjectHandle; + use litebox_broker_protocol::fs::{FileAccessMode, FileError, FileOpenFlags}; + use litebox_broker_protocol::message::FileResponse; use litebox_common_linux::{ AddressFamily, AtFlags, ReceiveFlags, SendFlags, SockFlags, SockType, SocketOption, SocketOptionName, TimeParam, errno::Errno, @@ -3747,7 +3749,12 @@ mod unix_tests { use crate::{ UserPtr, UserPtrMut, - syscalls::{net::SocketAddress, tests::init_platform, unix::UnixSocketAddr}, + syscalls::{ + net::SocketAddress, + test_broker::{FileCall, Scripted, ScriptedFiles, closed, failed, opened}, + tests::{ROOT, init_platform, mode, scripted_task}, + unix::UnixSocketAddr, + }, }; extern crate std; @@ -3758,15 +3765,12 @@ mod unix_tests { fn create_unix_server_socket( task: &TestTask, - addr: &str, + addr: UnixSocketAddr, flags: SockFlags, ) -> Result { let raw_server_fd = create_unix_socket(task, SockType::Stream, flags); let server_fd = typed_socket(task, raw_server_fd); - task.do_bind( - &server_fd, - SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), - )?; + task.do_bind(&server_fd, SocketAddress::Unix(addr))?; task.do_listen(&server_fd, 1)?; Ok(raw_server_fd) } @@ -3776,6 +3780,30 @@ mod unix_tests { .expect("close socket failed"); } + fn file_handle(value: u64) -> ObjectHandle { + ObjectHandle(0x2000 + value) + } + + fn script_path_bind(files: &ScriptedFiles, handle: ObjectHandle) { + files.script([opened(handle)]); + } + + fn script_path_lookup(files: &ScriptedFiles, handle: ObjectHandle) { + files.script([opened(handle), closed()]); + } + + fn script_path_bind_failure(files: &ScriptedFiles, error: FileError) { + files.script([failed(error)]); + } + + fn script_path_close(files: &ScriptedFiles) { + files.script([closed()]); + } + + fn script_path_unlink(files: &ScriptedFiles) { + files.script([Scripted::Reply(FileResponse::Unlink)]); + } + fn typed_socket( task: &TestTask, fd: u32, @@ -3812,14 +3840,14 @@ mod unix_tests { let task = init_platform(); for _ in 0..10 { - let server_path = "/unix_stream_socket_server.sock"; - let client_path = "/unix_stream_socket_client.sock"; let raw_server_fd = create_unix_socket(&task, SockType::Datagram, SockFlags::empty()); let raw_client_fd = create_unix_socket(&task, SockType::Datagram, SockFlags::empty()); let server_fd = typed_socket(&task, raw_server_fd); let client_fd = typed_socket(&task, raw_client_fd); - let server_addr = SocketAddress::Unix(UnixSocketAddr::Path(server_path.to_string())); - let client_addr = SocketAddress::Unix(UnixSocketAddr::Path(client_path.to_string())); + let server_addr = + SocketAddress::Unix(UnixSocketAddr::Abstract(b"datagram-server".to_vec())); + let client_addr = + SocketAddress::Unix(UnixSocketAddr::Abstract(b"datagram-client".to_vec())); task.do_bind(&server_fd, server_addr.clone()) .expect("server bind failed"); task.do_bind(&client_fd, client_addr.clone()) @@ -3879,10 +3907,6 @@ mod unix_tests { close_socket(&task, raw_server_fd); close_socket(&task, raw_client_fd); - task.sys_unlinkat(-1, server_path, AtFlags::empty()) - .unwrap(); - task.sys_unlinkat(-1, client_path, AtFlags::empty()) - .unwrap(); } } @@ -3891,16 +3915,14 @@ mod unix_tests { let task = init_platform(); for _ in 0..10 { - let addr = "/unix_stream_socket.sock"; - let raw_server_fd = create_unix_server_socket(&task, addr, SockFlags::empty()).unwrap(); + let addr = UnixSocketAddr::Abstract(b"stream-socket".to_vec()); + let raw_server_fd = + create_unix_server_socket(&task, addr.clone(), SockFlags::empty()).unwrap(); let raw_client_fd = create_unix_socket(&task, SockType::Stream, SockFlags::empty()); let server_fd = typed_socket(&task, raw_server_fd); let client_fd = typed_socket(&task, raw_client_fd); - task.do_connect( - &client_fd, - SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), - ) - .unwrap(); + task.do_connect(&client_fd, SocketAddress::Unix(addr)) + .unwrap(); let mut peer_addr = SocketAddress::default(); let raw_server_conn = task @@ -3941,13 +3963,17 @@ mod unix_tests { close_socket(&task, raw_server_fd); close_socket(&task, raw_client_fd); - task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); + close_socket(&task, raw_server_conn); } } #[test] fn test_unix_stream_socket_refused() { - let task = init_platform(); + const FIRST_BOUND: ObjectHandle = ObjectHandle(0x2300); + const FIRST_LOOKUP: ObjectHandle = ObjectHandle(0x2301); + const SECOND_BOUND: ObjectHandle = ObjectHandle(0x2302); + + let (files, task) = scripted_task([]); let raw_client_fd = create_unix_socket(&task, SockType::Stream, SockFlags::empty()); let client_fd = typed_socket(&task, raw_client_fd); let addr = "/unix_stream_socket_refused.sock"; @@ -3958,9 +3984,16 @@ mod unix_tests { assert_eq!(result.unwrap_err(), Errno::ECONNREFUSED); close_socket(&task, raw_client_fd); - let raw_server_fd = create_unix_server_socket(&task, addr, SockFlags::empty()).unwrap(); + script_path_bind(&files, FIRST_BOUND); + let raw_server_fd = create_unix_server_socket( + &task, + UnixSocketAddr::Path(addr.to_string()), + SockFlags::empty(), + ) + .unwrap(); let raw_client_fd = create_unix_socket(&task, SockType::Stream, SockFlags::empty()); let client_fd = typed_socket(&task, raw_client_fd); + script_path_lookup(&files, FIRST_LOOKUP); let result = task.do_connect( &client_fd, SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), @@ -3979,31 +4012,82 @@ mod unix_tests { assert_eq!(result.unwrap_err(), Errno::ECONNREFUSED); close_socket(&task, raw_another_client); + script_path_close(&files); close_socket(&task, raw_client_fd); let addr = "/unix_stream_socket_refused2.sock"; - let raw_server_fd = create_unix_server_socket(&task, addr, SockFlags::empty()).unwrap(); + script_path_bind(&files, SECOND_BOUND); + let raw_server_fd = create_unix_server_socket( + &task, + UnixSocketAddr::Path(addr.to_string()), + SockFlags::empty(), + ) + .unwrap(); let raw_client_fd = create_unix_socket(&task, SockType::Stream, SockFlags::empty()); let client_fd = typed_socket(&task, raw_client_fd); // remove the sock file + script_path_unlink(&files); task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); + script_path_bind_failure(&files, FileError::NoSuchFileOrDirectory); let result = task.do_connect( &client_fd, SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), ); assert_eq!(result.unwrap_err(), Errno::ENOENT); + script_path_close(&files); close_socket(&task, raw_server_fd); close_socket(&task, raw_client_fd); + + assert_eq!( + files.take_calls(), + alloc::vec![ + FileCall::Open { + path: "/unix_stream_socket_refused.sock".into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::CREATE.union(FileOpenFlags::EXCLUSIVE), + mode: mode(0o755), + }, + FileCall::Open { + path: "/unix_stream_socket_refused.sock".into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::from_bits(0).unwrap(), + mode: mode(0o755), + }, + FileCall::Close(FIRST_LOOKUP), + FileCall::Close(FIRST_BOUND), + FileCall::Open { + path: "/unix_stream_socket_refused2.sock".into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::CREATE.union(FileOpenFlags::EXCLUSIVE), + mode: mode(0o755), + }, + FileCall::Unlink { + path: "/unix_stream_socket_refused2.sock".into(), + user: ROOT, + }, + FileCall::Open { + path: "/unix_stream_socket_refused2.sock".into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::from_bits(0).unwrap(), + mode: mode(0o755), + }, + FileCall::Close(SECOND_BOUND), + ] + ); } fn test_multiple_unix_stream_connections(is_nonblocking: bool) { let task = init_platform(); - let addr = "/unix_multi_stream_socket.sock"; + let addr = UnixSocketAddr::Abstract(b"multi-stream-socket".to_vec()); let raw_server_fd = create_unix_server_socket( &task, - addr, + addr.clone(), if is_nonblocking { SockFlags::NONBLOCK } else { @@ -4013,6 +4097,7 @@ mod unix_tests { .unwrap(); let server_fd = typed_socket(&task, raw_server_fd); + let client_addr = addr.clone(); let client = task.spawn_clone_for_test(move |task| { let mut client_fds = Vec::new(); for _ in 0..10 { @@ -4029,11 +4114,8 @@ mod unix_tests { if is_nonblocking { ppoll(&task, raw_server_fd, Events::OUT); } - task.do_connect( - &client_fd, - SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), - ) - .unwrap(); + task.do_connect(&client_fd, SocketAddress::Unix(client_addr.clone())) + .unwrap(); client_fds.push((raw_client_fd, client_fd)); } @@ -4102,23 +4184,46 @@ mod unix_tests { #[test] fn test_unix_stream_socket_on_same_addr() { - let task = init_platform(); - for _ in 0..10 { + let (files, task) = scripted_task([]); + for iteration in 0..10 { let addr = "/unix_stream_socket_server.sock"; - let raw_server1_fd = - create_unix_server_socket(&task, addr, SockFlags::NONBLOCK).unwrap(); + let first_bound = file_handle(iteration * 4); + let second_bound = file_handle(iteration * 4 + 1); + let first_lookup = file_handle(iteration * 4 + 2); + let second_lookup = file_handle(iteration * 4 + 3); + + script_path_bind(&files, first_bound); + let raw_server1_fd = create_unix_server_socket( + &task, + UnixSocketAddr::Path(addr.to_string()), + SockFlags::NONBLOCK, + ) + .unwrap(); let server1_fd = typed_socket(&task, raw_server1_fd); - let err = create_unix_server_socket(&task, addr, SockFlags::empty()).unwrap_err(); + script_path_bind_failure(&files, FileError::AlreadyExists); + let err = create_unix_server_socket( + &task, + UnixSocketAddr::Path(addr.to_string()), + SockFlags::empty(), + ) + .unwrap_err(); assert_eq!(err, Errno::EADDRINUSE); // remove the socket file to allow another server to bind to the same address + script_path_unlink(&files); task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); - let raw_server2_fd = - create_unix_server_socket(&task, addr, SockFlags::NONBLOCK).unwrap(); + script_path_bind(&files, second_bound); + let raw_server2_fd = create_unix_server_socket( + &task, + UnixSocketAddr::Path(addr.to_string()), + SockFlags::NONBLOCK, + ) + .unwrap(); let server2_fd = typed_socket(&task, raw_server2_fd); let raw_client1_fd = create_unix_socket(&task, SockType::Stream, SockFlags::empty()); let client1_fd = typed_socket(&task, raw_client1_fd); + script_path_lookup(&files, first_lookup); task.do_connect( &client1_fd, SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), @@ -4138,32 +4243,55 @@ mod unix_tests { close_socket(&task, raw_client1_fd); // close server one and connect again + script_path_close(&files); close_socket(&task, raw_server1_fd); let raw_client2_fd = create_unix_socket(&task, SockType::Stream, SockFlags::empty()); let client2_fd = typed_socket(&task, raw_client2_fd); + script_path_lookup(&files, second_lookup); task.do_connect( &client2_fd, SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), ) .unwrap(); close_socket(&task, raw_client2_fd); + script_path_close(&files); close_socket(&task, raw_server2_fd); // still fail after we close the server - let err = create_unix_server_socket(&task, addr, SockFlags::empty()).unwrap_err(); + script_path_bind_failure(&files, FileError::AlreadyExists); + let err = create_unix_server_socket( + &task, + UnixSocketAddr::Path(addr.to_string()), + SockFlags::empty(), + ) + .unwrap_err(); assert_eq!(err, Errno::EADDRINUSE); + script_path_unlink(&files); task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); } + + assert!( + files + .take_calls() + .iter() + .filter(|call| matches!(call, FileCall::Unlink { .. })) + .count() + == 20, + "each iteration must explicitly unlink both path bindings" + ); } #[test] fn test_unix_datagram_socket_on_same_addr() { - let task = init_platform(); - for _ in 0..10 { + let (files, task) = scripted_task([]); + for iteration in 0..10 { let addr = "/unix_datagram_socket_server.sock"; + let first_bound = file_handle(iteration * 2); + let second_bound = file_handle(iteration * 2 + 1); let raw_server_fd = create_unix_socket(&task, SockType::Datagram, SockFlags::empty()); let server_fd = typed_socket(&task, raw_server_fd); + script_path_bind(&files, first_bound); task.do_bind( &server_fd, SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), @@ -4172,6 +4300,7 @@ mod unix_tests { let raw_server_fd2 = create_unix_socket(&task, SockType::Datagram, SockFlags::empty()); let server_fd2 = typed_socket(&task, raw_server_fd2); + script_path_bind_failure(&files, FileError::AlreadyExists); let err = task .do_bind( &server_fd2, @@ -4180,19 +4309,34 @@ mod unix_tests { .unwrap_err(); assert_eq!(err, Errno::EADDRINUSE); + script_path_unlink(&files); task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); let raw_server_fd2 = create_unix_socket(&task, SockType::Datagram, SockFlags::empty()); let server_fd2 = typed_socket(&task, raw_server_fd2); + script_path_bind(&files, second_bound); task.do_bind( &server_fd2, SocketAddress::Unix(UnixSocketAddr::Path(addr.to_string())), ) .unwrap(); + script_path_close(&files); close_socket(&task, raw_server_fd); + script_path_close(&files); close_socket(&task, raw_server_fd2); + script_path_unlink(&files); task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); } + + assert!( + files + .take_calls() + .iter() + .filter(|call| matches!(call, FileCall::Unlink { .. })) + .count() + == 20, + "each iteration must explicitly unlink both path bindings" + ); } fn unix_socketpair_bidirectional(ty: SockType, is_nonblocking: bool) { @@ -4385,10 +4529,18 @@ mod unix_tests { #[test] fn test_unix_stream_addr() { - let task = init_platform(); + const BOUND: ObjectHandle = ObjectHandle(0x2100); + const LOOKUP: ObjectHandle = ObjectHandle(0x2101); + + let (files, task) = scripted_task([]); let server_path = "/unix_stream_sockname.sock"; - let raw_server_fd = - create_unix_server_socket(&task, server_path, SockFlags::empty()).unwrap(); + script_path_bind(&files, BOUND); + let raw_server_fd = create_unix_server_socket( + &task, + UnixSocketAddr::Path(server_path.to_string()), + SockFlags::empty(), + ) + .unwrap(); let server_fd = typed_socket(&task, raw_server_fd); // Server socket should have its bound address @@ -4410,6 +4562,7 @@ mod unix_tests { )); // Connect client to server + script_path_lookup(&files, LOOKUP); task.do_connect( &client_fd, SocketAddress::Unix(UnixSocketAddr::Path(server_path.to_string())), @@ -4452,14 +4605,46 @@ mod unix_tests { close_socket(&task, raw_client_fd); close_socket(&task, raw_server_conn); + script_path_close(&files); close_socket(&task, raw_server_fd); + script_path_unlink(&files); task.sys_unlinkat(-1, server_path, AtFlags::empty()) .unwrap(); + assert_eq!( + files.take_calls(), + alloc::vec![ + FileCall::Open { + path: server_path.into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::CREATE.union(FileOpenFlags::EXCLUSIVE), + mode: mode(0o755), + }, + FileCall::Open { + path: server_path.into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::from_bits(0).unwrap(), + mode: mode(0o755), + }, + FileCall::Close(LOOKUP), + FileCall::Close(BOUND), + FileCall::Unlink { + path: server_path.into(), + user: ROOT, + }, + ], + "path-based Unix sockets must issue explicit open, lookup, close, and unlink requests" + ); } #[test] fn test_unix_datagram_addr() { - let task = init_platform(); + const SERVER: ObjectHandle = ObjectHandle(0x2200); + const CLIENT: ObjectHandle = ObjectHandle(0x2201); + const LOOKUP: ObjectHandle = ObjectHandle(0x2202); + + let (files, task) = scripted_task([]); let server_path = "/unix_datagram_sockname_server.sock"; let client_path = "/unix_datagram_sockname_client.sock"; @@ -4482,6 +4667,7 @@ mod unix_tests { )); // Bind server + script_path_bind(&files, SERVER); task.do_bind( &server_fd, SocketAddress::Unix(UnixSocketAddr::Path(server_path.to_string())), @@ -4496,6 +4682,7 @@ mod unix_tests { ); // Bind client + script_path_bind(&files, CLIENT); task.do_bind( &client_fd, SocketAddress::Unix(UnixSocketAddr::Path(client_path.to_string())), @@ -4510,6 +4697,7 @@ mod unix_tests { ); // Connect client to server + script_path_lookup(&files, LOOKUP); task.do_connect( &client_fd, SocketAddress::Unix(UnixSocketAddr::Path(server_path.to_string())), @@ -4534,11 +4722,52 @@ mod unix_tests { let server_peer_result = task.do_getpeername(&server_fd); assert_eq!(server_peer_result.unwrap_err(), Errno::ENOTCONN); + script_path_close(&files); close_socket(&task, raw_server_fd); + script_path_close(&files); close_socket(&task, raw_client_fd); + script_path_unlink(&files); task.sys_unlinkat(-1, server_path, AtFlags::empty()) .unwrap(); + script_path_unlink(&files); task.sys_unlinkat(-1, client_path, AtFlags::empty()) .unwrap(); + assert_eq!( + files.take_calls(), + alloc::vec![ + FileCall::Open { + path: server_path.into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::CREATE.union(FileOpenFlags::EXCLUSIVE), + mode: mode(0o755), + }, + FileCall::Open { + path: client_path.into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::CREATE.union(FileOpenFlags::EXCLUSIVE), + mode: mode(0o755), + }, + FileCall::Open { + path: server_path.into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::from_bits(0).unwrap(), + mode: mode(0o755), + }, + FileCall::Close(LOOKUP), + FileCall::Close(SERVER), + FileCall::Close(CLIENT), + FileCall::Unlink { + path: server_path.into(), + user: ROOT, + }, + FileCall::Unlink { + path: client_path.into(), + user: ROOT, + }, + ] + ); } } diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs index 51b1abbfa..e04ae0a52 100644 --- a/litebox_shim_linux/src/syscalls/pipe.rs +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -12,10 +12,10 @@ 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 litebox_broker_protocol::fs::FileMode as Mode; +use litebox_common_linux::{FileDescriptorFlags, InodeType, OFlags, errno::Errno}; use crate::{GlobalState, ShimPlatform}; @@ -145,7 +145,7 @@ impl GlobalState { HalfPipeType::SenderHalf => Mode::WUSR, HalfPipeType::ReceiverHalf => Mode::RUSR, }; - Ok(read_write_mode.bits() | InodeType::NamedPipe as u32) + Ok(u32::from(read_write_mode.bits()) | InodeType::NamedPipe as u32) } pub(crate) fn with_linux_pipe_iopollable( diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index 6d77350a7..82fb5099d 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1402,19 +1402,18 @@ impl Task { let full_path = self.resolve_path(&path)?; let file = self.do_open( full_path, - litebox::fs::OFlags::RDONLY, - litebox::fs::Mode::empty(), + litebox_common_linux::OFlags::RDONLY, + litebox_broker_protocol::fs::FileMode::empty(), )?; let mut header = [0u8; SHEBANG_MAX_LINE]; - let files = self.files.borrow(); - let n = match files.fs.read(&file, &mut header, Some(0)) { + let n = match self.global.litebox.read_file(&file, &mut header, Some(0)) { Ok(n) => n, Err(e) => { - let _ = files.fs.close(&file); + let _ = self.global.litebox.close_file(&file); return Err(Errno::from(e)); } }; - let _ = files.fs.close(&file); + let _ = self.global.litebox.close_file(&file); match parse_shebang(&header[..n]) { Some((interp, opt_arg)) => { diff --git a/litebox_shim_linux/src/syscalls/test_broker.rs b/litebox_shim_linux/src/syscalls/test_broker.rs new file mode 100644 index 000000000..7972e26f6 --- /dev/null +++ b/litebox_shim_linux/src/syscalls/test_broker.rs @@ -0,0 +1,876 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! In-process broker fixtures for the Linux shim's unit tests. +//! +//! The shim owns the guest side of the guest/broker boundary: syscall argument validation, path +//! resolution, flag and mode translation, descriptor bookkeeping, and error translation. Broker +//! authority — policy, filesystem resolution, and backend semantics — belongs to +//! `litebox_broker_core` and is tested there. +//! +//! These fixtures therefore answer the broker protocol directly over a +//! [`LocalCallChannel`], with no broker core, policy engine, readiness sink, or host association +//! involved. [`ScriptedFiles`] automatically serves only the standard streams needed to construct +//! a task. Every other file operation must have an explicit scripted response. + +extern crate std; + +use alloc::collections::{BTreeMap, VecDeque}; +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec; +use alloc::vec::Vec; +use core::num::NonZeroU64; +use core::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; + +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::error::ErrorCode; +use litebox_broker_protocol::event::{ConsumeEventResponse, CreateEventResponse, EventConsumeMode}; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileError, FileMode, FileNodeInfo, FileOpenFlags, FileStatus, FileType, + FileUser, OpenFileResponse, ReadDirectoryResponse, ReadFileResponse, +}; +use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, + BrokerResponse, BrokerResult, EventRequest, EventResponse, FileRequest, FileResponse, + PipeRequest, PipeResponse, StdioRequest, StdioResponse, +}; +use litebox_broker_protocol::pipe::{CreatePipeResponse, ReadPipeResponse, WritePipeResponse}; +use litebox_broker_protocol::readiness::ReadinessFlags; +use litebox_broker_protocol::shared_buffer::{ + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferDescriptor, +}; +use litebox_broker_protocol::stdio::{IsTerminalStdioResponse, StdioStream}; +use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; +use litebox_broker_transport::channel::{LocalCallChannel, LocalSetupChannel}; +use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory, SharedMemoryError}; + +/// Preferred I/O block size the fixture reports for every node. +const DEFAULT_BLOCK_SIZE: u64 = 4096; + +/// Device number the fixture reports for the standard streams. +/// +/// The shim only treats a descriptor as a standard stream when its status reports a character +/// device in the pseudo-terminal major range, so the fixture must report one. +/// See . +const PTS_RDEV: u64 = 136 << 8; + +fn standard_stream(path: &str) -> Option<(u64, FileAccessMode)> { + [ + ("/dev/stdin", FileAccessMode::ReadOnly), + ("/dev/stdout", FileAccessMode::WriteOnly), + ("/dev/stderr", FileAccessMode::WriteOnly), + ] + .into_iter() + .enumerate() + .find_map(|(minor, (stream, access))| { + (stream == path).then_some((u64::try_from(minor).unwrap(), access)) + }) +} + +/// Negotiates a broker connection served in-process by `files` and the fixture's own object table. +pub(crate) fn negotiate(files: Arc) -> BrokerLocal { + let broker = LocalChannel(Arc::new(LocalBroker::new(files))); + let memory = Arc::clone(broker.0.buffers.memory()); + let (local, ()) = BrokerLocal::negotiate(broker, |channel| { + Ok((channel, memory as Arc, ())) + }) + .expect("the local broker fixture must negotiate"); + local +} + +/// Allocates the broker object handles the fixture hands out. +pub(crate) struct Handles(AtomicU64); + +impl Handles { + fn next(&self) -> ObjectHandle { + ObjectHandle(self.0.fetch_add(1, Ordering::Relaxed)) + } +} + +/// The fixture's view of the association shared buffers. +pub(crate) struct SharedBuffers(SharedBufferPool>); + +impl SharedBuffers { + /// Returns the bytes the guest staged in `descriptor`. + pub(crate) fn staged(&self, descriptor: SharedBufferDescriptor) -> Vec { + let mut bytes = vec![0; descriptor.length as usize]; + self.0 + .read(descriptor.slot_index, &mut bytes) + .expect("the guest must stage payloads in a valid shared buffer"); + bytes + } + + /// Returns the UTF-8 path the guest staged in `descriptor`. + pub(crate) fn staged_path(&self, descriptor: SharedBufferDescriptor) -> String { + String::from_utf8(self.staged(descriptor)).expect("the guest must stage a UTF-8 path") + } + + /// Stages `data` for the guest to read back out of `descriptor`. + pub(crate) fn stage(&self, descriptor: SharedBufferDescriptor, data: &[u8]) { + assert!( + data.len() <= descriptor.length as usize, + "the fixture must not overfill a leased shared buffer" + ); + self.0 + .write(descriptor.slot_index, data) + .expect("the guest must lease a valid shared buffer"); + } + + fn memory(&self) -> &Arc { + self.0.memory() + } +} + +/// The local end of the fixture's broker connection. +pub(crate) struct LocalChannel(Arc); + +impl LocalSetupChannel for LocalChannel { + type Error = core::convert::Infallible; + + fn send_handshake_request( + &mut self, + request: &BrokerHandshakeRequest, + ) -> core::result::Result<(), Self::Error> { + assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); + Ok(()) + } + + fn recv_handshake_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + })) + } +} + +impl LocalCallChannel for LocalChannel { + type Error = core::convert::Infallible; + + fn call(&self, request: BrokerRequest) -> core::result::Result { + Ok(BrokerResponse { + request_id: request.request_id, + result: self.0.execute(request.operation), + }) + } +} + +/// Serves the object families the shim's guest code needs, without broker authority. +struct LocalBroker { + buffers: SharedBuffers, + handles: Handles, + objects: Mutex>, + files: Arc, +} + +impl LocalBroker { + fn new(files: Arc) -> Self { + let memory = Arc::new(TestSharedMemory::new()); + Self { + buffers: SharedBuffers( + SharedBufferPool::new(memory, SHARED_BUFFER_LAYOUT) + .expect("the fixture pool must match the protocol layout"), + ), + // Handle 0 is left unused so that a zeroed handle never names an object. + handles: Handles(AtomicU64::new(1)), + objects: Mutex::new(BTreeMap::new()), + files, + } + } + + fn execute(&self, operation: BrokerOperation) -> BrokerResult { + match operation { + BrokerOperation::File(request) => { + BrokerResult::File(self.files.request(request, &self.buffers, &self.handles)) + } + BrokerOperation::CloseObject(handle) => { + let closed = self.objects.lock().unwrap().remove(&handle).is_some(); + if closed || self.files.close(handle) { + BrokerResult::ObjectClosed + } else { + BrokerResult::Error(ErrorCode::UnknownObject) + } + } + BrokerOperation::CheckReadiness(handle) => { + self.with_object(handle, |object| BrokerResult::Readiness(object.readiness())) + } + BrokerOperation::Pipe(request) => self.pipe(request), + BrokerOperation::Event(request) => self.event(request), + BrokerOperation::Stdio(request) => Self::stdio(request), + // No fixture supplies randomness, so guest code must surface the failure. + BrokerOperation::FillRandom(_) => BrokerResult::Error(ErrorCode::UnsupportedOperation), + operation @ BrokerOperation::Socket(_) => { + panic!("unscripted broker operation: {operation:?}") + } + } + } + + fn pipe(&self, request: PipeRequest) -> BrokerResult { + match request { + PipeRequest::Create(request) => { + let Ok(capacity) = usize::try_from(request.capacity) else { + return BrokerResult::Error(ErrorCode::ResourceExhausted); + }; + let Ok(atomic_write_size) = usize::try_from(request.atomic_write_size) else { + return BrokerResult::Error(ErrorCode::ResourceExhausted); + }; + if capacity == 0 || atomic_write_size > capacity { + return BrokerResult::Error(ErrorCode::ResourceExhausted); + } + let state = Arc::new(Mutex::new(PipeState { + data: VecDeque::new(), + capacity, + atomic_write_size, + read_open: true, + write_open: true, + })); + let read_handle = self.handles.next(); + let write_handle = self.handles.next(); + let mut objects = self.objects.lock().unwrap(); + objects.insert( + read_handle, + Object::Pipe(PipeEnd { + state: Arc::clone(&state), + endpoint: PipeEndpoint::Read, + }), + ); + objects.insert( + write_handle, + Object::Pipe(PipeEnd { + state, + endpoint: PipeEndpoint::Write, + }), + ); + BrokerResult::Pipe(PipeResponse::Create(CreatePipeResponse { + read_handle, + write_handle, + })) + } + PipeRequest::Read(request) => self.with_object(request.handle, |object| { + let Object::Pipe(pipe) = object else { + return BrokerResult::Error(ErrorCode::InvalidRights); + }; + match pipe.read(request.buffer.length as usize) { + Ok(data) => { + self.buffers.stage(request.buffer, &data); + BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { + read: u32::try_from(data.len()).unwrap(), + })) + } + Err(error) => BrokerResult::Error(error), + } + }), + PipeRequest::Write(request) => { + let data = self.buffers.staged(request.buffer); + self.with_object(request.handle, |object| { + let Object::Pipe(pipe) = object else { + return BrokerResult::Error(ErrorCode::InvalidRights); + }; + match pipe.write(&data) { + Ok(written) => BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { + written: u32::try_from(written).unwrap(), + })), + Err(error) => BrokerResult::Error(error), + } + }) + } + } + } + + fn event(&self, request: EventRequest) -> BrokerResult { + match request { + EventRequest::Create(request) => { + let handle = self.handles.next(); + self.objects.lock().unwrap().insert( + handle, + Object::Event(EventState { + count: request.initial_count, + }), + ); + BrokerResult::Event(EventResponse::Create(CreateEventResponse { handle })) + } + EventRequest::Add(request) => self.with_object_mut(request.handle, |object| { + let Object::Event(event) = object else { + return BrokerResult::Error(ErrorCode::InvalidRights); + }; + match event.add(request.value) { + Ok(readiness) => BrokerResult::Event(EventResponse::Add( + litebox_broker_protocol::event::AddEventResponse { readiness }, + )), + Err(error) => BrokerResult::Error(error), + } + }), + EventRequest::Consume(request) => self.with_object_mut(request.handle, |object| { + let Object::Event(event) = object else { + return BrokerResult::Error(ErrorCode::InvalidRights); + }; + match event.consume(request.mode) { + Ok(consumption) => BrokerResult::Event(EventResponse::Consume(consumption)), + Err(error) => BrokerResult::Error(error), + } + }), + } + } + + /// Answers standard-stream capability queries; only standard output is a terminal here. + fn stdio(request: StdioRequest) -> BrokerResult { + match request { + StdioRequest::IsTerminal(request) => { + BrokerResult::Stdio(StdioResponse::IsTerminal(IsTerminalStdioResponse { + is_terminal: request.stream == StdioStream::Stdout, + })) + } + request => panic!("unscripted stdio request: {request:?}"), + } + } + + fn with_object( + &self, + handle: ObjectHandle, + f: impl FnOnce(&Object) -> BrokerResult, + ) -> BrokerResult { + let objects = self.objects.lock().unwrap(); + match objects.get(&handle) { + Some(object) => f(object), + None => BrokerResult::Error(ErrorCode::UnknownObject), + } + } + + fn with_object_mut( + &self, + handle: ObjectHandle, + f: impl FnOnce(&mut Object) -> BrokerResult, + ) -> BrokerResult { + let mut objects = self.objects.lock().unwrap(); + match objects.get_mut(&handle) { + Some(object) => f(object), + None => BrokerResult::Error(ErrorCode::UnknownObject), + } + } +} + +/// One broker-owned object the fixture serves. +enum Object { + Pipe(PipeEnd), + Event(EventState), +} + +impl Object { + fn readiness(&self) -> ReadinessFlags { + match self { + Self::Pipe(pipe) => pipe.readiness(), + Self::Event(event) => event.readiness(), + } + } +} + +/// One endpoint of a fixture-owned byte pipe. +struct PipeEnd { + state: Arc>, + endpoint: PipeEndpoint, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum PipeEndpoint { + Read, + Write, +} + +struct PipeState { + data: VecDeque, + capacity: usize, + atomic_write_size: usize, + read_open: bool, + write_open: bool, +} + +impl PipeEnd { + fn read(&self, length: usize) -> core::result::Result, ErrorCode> { + if self.endpoint != PipeEndpoint::Read { + return Err(ErrorCode::InvalidRights); + } + if length == 0 { + return Ok(Vec::new()); + } + let mut state = self.state.lock().unwrap(); + if state.data.is_empty() { + // An empty pipe whose writer is gone reads as end-of-file. + return if state.write_open { + Err(ErrorCode::WouldBlock) + } else { + Ok(Vec::new()) + }; + } + let read = length.min(state.data.len()); + Ok(state.data.drain(..read).collect()) + } + + fn write(&self, data: &[u8]) -> core::result::Result { + if self.endpoint != PipeEndpoint::Write { + return Err(ErrorCode::InvalidRights); + } + if data.is_empty() { + return Ok(0); + } + let mut state = self.state.lock().unwrap(); + if !state.read_open { + return Err(ErrorCode::PeerClosed); + } + let available = state.capacity - state.data.len(); + if available == 0 || (data.len() <= state.atomic_write_size && available < data.len()) { + return Err(ErrorCode::WouldBlock); + } + let written = available.min(data.len()); + state.data.extend(&data[..written]); + Ok(written) + } + + fn readiness(&self) -> ReadinessFlags { + let state = self.state.lock().unwrap(); + let mut readiness = ReadinessFlags::default(); + match self.endpoint { + PipeEndpoint::Read => { + if !state.data.is_empty() { + readiness = readiness | ReadinessFlags::READ; + } + if !state.write_open { + readiness = readiness | ReadinessFlags::HANGUP; + } + } + PipeEndpoint::Write => { + if state.data.len() < state.capacity { + readiness = readiness | ReadinessFlags::WRITE; + } + if !state.read_open { + readiness = readiness | ReadinessFlags::ERROR; + } + } + } + readiness + } +} + +impl Drop for PipeEnd { + fn drop(&mut self) { + let mut state = self.state.lock().unwrap(); + match self.endpoint { + PipeEndpoint::Read => state.read_open = false, + PipeEndpoint::Write => state.write_open = false, + } + } +} + +/// A fixture-owned event counter. +struct EventState { + count: u64, +} + +impl EventState { + /// The largest count an event may hold, mirroring `eventfd` saturation. + const MAX_COUNT: u64 = u64::MAX - 1; + + fn add(&mut self, value: u64) -> core::result::Result { + let Some(count) = self + .count + .checked_add(value) + .filter(|count| *count <= Self::MAX_COUNT) + else { + return Err(ErrorCode::WouldBlock); + }; + self.count = count; + Ok(self.readiness()) + } + + fn consume( + &mut self, + mode: EventConsumeMode, + ) -> core::result::Result { + if self.count == 0 { + return Err(ErrorCode::WouldBlock); + } + let value = match mode { + EventConsumeMode::All => core::mem::take(&mut self.count), + EventConsumeMode::One => { + self.count -= 1; + 1 + } + }; + Ok(ConsumeEventResponse { + value, + readiness: self.readiness(), + }) + } + + fn readiness(&self) -> ReadinessFlags { + let mut readiness = ReadinessFlags::default(); + if self.count > 0 { + readiness = readiness | ReadinessFlags::READ; + } + if self.count < Self::MAX_COUNT { + readiness = readiness | ReadinessFlags::WRITE; + } + readiness + } +} + +/// One file request the shim issued, with any staged payload copied out. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum FileCall { + Open { + path: String, + user: FileUser, + access: FileAccessMode, + flags: FileOpenFlags, + mode: FileMode, + }, + Read { + handle: ObjectHandle, + length: u32, + offset: Option, + }, + Write { + handle: ObjectHandle, + data: Vec, + offset: Option, + }, + ReadDirectory { + handle: ObjectHandle, + start_index: u64, + }, + PathStatus { + path: String, + user: FileUser, + }, + HandleStatus(ObjectHandle), + Unlink { + path: String, + user: FileUser, + }, + Mkdir { + path: String, + user: FileUser, + mode: FileMode, + }, + Rmdir { + path: String, + user: FileUser, + }, + Close(ObjectHandle), +} + +/// One scripted answer for the next file request. +pub(crate) enum Scripted { + /// Reply with this response verbatim. + Reply(FileResponse), + /// Stage `data` in the request's shared buffer and report it as read. + Read(Vec), + /// Stage one explicitly encoded directory page. + Directory { + payload: Vec, + next_index: Option, + }, + /// Acknowledge closing the next non-standard file handle. + Closed, +} + +/// Replies with a successful open that hands out `handle`. +pub(crate) fn opened(handle: ObjectHandle) -> Scripted { + Scripted::Reply(FileResponse::Open(OpenFileResponse { handle })) +} + +/// Replies with a guest-visible file failure. +pub(crate) fn failed(error: FileError) -> Scripted { + Scripted::Reply(FileResponse::Failed(error)) +} + +/// Acknowledges a successful close of a non-standard file handle. +pub(crate) fn closed() -> Scripted { + Scripted::Closed +} + +/// Replies to a path-status request with a node of `file_type` and `mode`. +pub(crate) fn path_status(file_type: FileType, mode: u16) -> Scripted { + Scripted::Reply(FileResponse::PathStatus(status(file_type, mode))) +} + +/// Builds a status for a node of `file_type` and `mode`. +pub(crate) fn status(file_type: FileType, mode: u16) -> FileStatus { + FileStatus { + file_type, + mode: FileMode::from_bits(mode).expect("test modes must be supported"), + size: 0, + owner: FileUser { user: 0, group: 0 }, + node_info: FileNodeInfo { + dev: 1, + ino: 42, + rdev: None, + }, + blksize: DEFAULT_BLOCK_SIZE, + } +} + +/// A file fixture that records the requests the shim issues and answers them from a script. +/// +/// The standard streams every task opens at creation are answered automatically and are not +/// recorded, so a test's recorded calls contain only what the test itself provoked. +pub(crate) struct ScriptedFiles { + calls: Mutex>, + script: Mutex>, + /// Handles opened for the standard streams, with the device number reported for each. + stdio: Mutex>, +} + +impl ScriptedFiles { + /// Returns a fixture that answers file requests from `script`, in order. + pub(crate) fn new(script: impl IntoIterator) -> Arc { + Arc::new(Self { + calls: Mutex::new(Vec::new()), + script: Mutex::new(script.into_iter().collect()), + stdio: Mutex::new(BTreeMap::new()), + }) + } + + /// Appends more scripted answers. + pub(crate) fn script(&self, script: impl IntoIterator) { + self.script.lock().unwrap().extend(script); + } + + /// Removes and returns every request recorded so far, oldest first. + pub(crate) fn take_calls(&self) -> Vec { + core::mem::take(&mut self.calls.lock().unwrap()) + } + + /// Returns the paths of every recorded request that names one, oldest first. + pub(crate) fn take_paths(&self) -> Vec { + self.take_calls() + .into_iter() + .filter_map(|call| match call { + FileCall::Open { path, .. } + | FileCall::PathStatus { path, .. } + | FileCall::Unlink { path, .. } + | FileCall::Mkdir { path, .. } + | FileCall::Rmdir { path, .. } => Some(path), + _ => None, + }) + .collect() + } + + fn record(&self, call: FileCall) { + self.calls.lock().unwrap().push(call); + } + + fn next(&self) -> Scripted { + self.script + .lock() + .unwrap() + .pop_front() + .expect("the test script must answer every file request") + } + + fn reply(&self) -> FileResponse { + match self.next() { + Scripted::Reply(response) => response, + Scripted::Read(_) | Scripted::Directory { .. } | Scripted::Closed => { + panic!("scripted payload answer for a request that carries none") + } + } + } +} + +impl ScriptedFiles { + fn request( + &self, + request: FileRequest, + buffers: &SharedBuffers, + handles: &Handles, + ) -> FileResponse { + match request { + FileRequest::Open(request) => { + let path = buffers.staged_path(request.path); + // Task creation always opens the standard streams; answering them here keeps the + // recorded calls limited to what the test itself asked for. + if let Some((minor, access)) = standard_stream(&path) { + assert_eq!( + (request.user, request.access, request.flags, request.mode), + ( + FileUser { user: 0, group: 0 }, + access, + FileOpenFlags::from_bits(0).unwrap(), + FileMode::from_bits(0).unwrap(), + ), + "standard streams must be opened with their bootstrap options" + ); + let handle = handles.next(); + self.stdio.lock().unwrap().insert(handle, PTS_RDEV | minor); + return FileResponse::Open(OpenFileResponse { handle }); + } + self.record(FileCall::Open { + path, + user: request.user, + access: request.access, + flags: request.flags, + mode: request.mode, + }); + self.reply() + } + FileRequest::Read(request) => { + self.record(FileCall::Read { + handle: request.handle, + length: request.buffer.length, + offset: request.offset, + }); + match self.next() { + Scripted::Reply(response) => response, + Scripted::Read(data) => { + buffers.stage(request.buffer, &data); + FileResponse::Read(ReadFileResponse { + read: u32::try_from(data.len()).unwrap(), + }) + } + Scripted::Directory { .. } | Scripted::Closed => { + panic!("non-read scripted answer for a read") + } + } + } + FileRequest::Write(request) => { + self.record(FileCall::Write { + handle: request.handle, + data: buffers.staged(request.buffer), + offset: request.offset, + }); + self.reply() + } + FileRequest::ReadDirectory(request) => { + self.record(FileCall::ReadDirectory { + handle: request.handle, + start_index: request.start_index, + }); + match self.next() { + Scripted::Reply(response) => response, + Scripted::Read(_) | Scripted::Closed => { + panic!("non-directory scripted answer for a directory read") + } + Scripted::Directory { + payload, + next_index, + } => { + buffers.stage(request.buffer, &payload); + FileResponse::ReadDirectory(ReadDirectoryResponse { + length: u32::try_from(payload.len()).unwrap(), + next_index, + }) + } + } + } + FileRequest::PathStatus(request) => { + let path = buffers.staged_path(request.path); + if let Some((minor, _)) = standard_stream(&path) { + assert_eq!( + request.user, + FileUser { user: 0, group: 0 }, + "standard streams must be queried as the bootstrap user" + ); + let mut status = status(FileType::CharacterDevice, 0o620); + status.node_info.rdev = NonZeroU64::new(PTS_RDEV | minor); + return FileResponse::PathStatus(status); + } + self.record(FileCall::PathStatus { + path, + user: request.user, + }); + self.reply() + } + FileRequest::HandleStatus(request) => { + if let Some(rdev) = self.stdio.lock().unwrap().get(&request.handle).copied() { + let mut status = status(FileType::CharacterDevice, 0o620); + status.node_info.rdev = NonZeroU64::new(rdev); + return FileResponse::HandleStatus(status); + } + self.record(FileCall::HandleStatus(request.handle)); + self.reply() + } + FileRequest::Unlink(request) => { + self.record(FileCall::Unlink { + path: buffers.staged_path(request.path), + user: request.user, + }); + self.reply() + } + FileRequest::Mkdir(request) => { + self.record(FileCall::Mkdir { + path: buffers.staged_path(request.path), + user: request.user, + mode: request.mode, + }); + self.reply() + } + FileRequest::Rmdir(request) => { + self.record(FileCall::Rmdir { + path: buffers.staged_path(request.path), + user: request.user, + }); + self.reply() + } + request => panic!("unscripted file request: {request:?}"), + } + } + + fn close(&self, handle: ObjectHandle) -> bool { + if self.stdio.lock().unwrap().remove(&handle).is_some() { + return true; + } + self.record(FileCall::Close(handle)); + match self.next() { + Scripted::Closed => true, + Scripted::Reply(_) | Scripted::Read(_) | Scripted::Directory { .. } => { + panic!("non-close scripted answer for closing {handle:?}") + } + } + } +} + +/// Shared memory backed by an ordinary allocation, since no peer process observes it. +pub(crate) struct TestSharedMemory(Mutex>); + +impl TestSharedMemory { + fn new() -> Self { + Self(Mutex::new(vec![0; SHARED_BUFFER_POOL_SIZE])) + } +} + +impl SharedMemory for TestSharedMemory { + fn len(&self) -> usize { + SHARED_BUFFER_POOL_SIZE + } + + 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(()) + } +} diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index e70502fdc..895cba1cf 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -1,35 +1,30 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox::fs::{Mode, OFlags}; -use litebox_broker_core::{ - AssociationCancellation, BrokerCore, BrokerError, BrokerSession, CallerCredential, - ObjectRights, PolicyEngine, SessionId, - random::{RandomProvider, RandomProviderError}, - readiness::ReadinessRegistration, - socket::{PlatformSocket, SocketProvider}, - stdio::{StdioProvider, StdioProviderError}, +//! Shared fixtures and cross-cutting unit tests for the Linux shim. +//! +//! Tasks here run against the in-process broker fixtures in [`crate::syscalls::test_broker`], so +//! the shim's unit tests exercise guest and shim code rather than broker authority. Filesystem +//! resolution and backend semantics belong to `litebox_broker_core` and are tested there; what the +//! shim owns, and what these tests cover, is the translation between the Linux ABI and the broker +//! protocol. + +use alloc::sync::Arc; +use alloc::vec; +use alloc::vec::Vec; +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileDirectoryEntry, FileError, FileMode as Mode, FileNodeInfo, FileOpenFlags, + FileType, FileUser, MAX_FILE_TRANSFER_SIZE, encode_directory_entries_chunk, }; -use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::{ - BROKER_PROTOCOL_VERSION, - message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, - BrokerResponse, BrokerResult, PipeRequest, PipeResponse, StdioRequest, StdioResponse, - }, - pipe::{CreatePipeResponse, ReadPipeResponse, WritePipeResponse}, - shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}, - socket::CreateSocketRequest, - stdio::{IsTerminalStdioResponse, StdioOutputStream, StdioStream}, +use litebox_broker_protocol::message::FileResponse; +use litebox_common_linux::{ + AtFlags, DirentType, FcntlArg, FileDescriptorFlags, OFlags, errno::Errno, }; -use litebox_broker_transport::{ - channel::{LocalCallChannel, LocalSetupChannel}, - shared_memory::{SharedBufferPool, SharedMemory, SharedMemoryError}, -}; -use litebox_common_linux::{AtFlags, EfdFlags, FcntlArg, FileDescriptorFlags, errno::Errno}; use zerocopy::FromBytes as _; use crate::UserPtrMut; +use crate::syscalls::test_broker::{FileCall, Scripted, ScriptedFiles, closed, failed, opened}; use litebox::shim::{Exception, ExceptionInfo}; use litebox_common_linux::PtRegs; @@ -39,7 +34,11 @@ use litebox_common_linux::signal::{ILL_ILLOPN, SI_KERNEL, SiginfoData, Signal}; extern crate std; -const TEST_TAR_FILE: &[u8] = include_bytes!("../../../litebox/src/fs/test.tar"); +/// The handle the scripted fixture hands out for a file a test opens. +pub(crate) const FILE_HANDLE: ObjectHandle = ObjectHandle(0x1000); + +/// The user every test task acts as. +pub(crate) const ROOT: FileUser = FileUser { user: 0, group: 0 }; /// The concrete platform used by the shim's unit tests. /// @@ -57,272 +56,50 @@ pub(crate) fn test_platform() -> &'static TestPlatform { PLATFORM.get_or_init(TestPlatform::new) } -fn test_broker() -> &'static BrokerCore { - static BROKER: std::sync::OnceLock = std::sync::OnceLock::new(); - BROKER.get_or_init(|| { - BrokerCore::new( - PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), - alloc::sync::Arc::new(PipeOnlySocketProvider), - alloc::sync::Arc::new(UnusedRandomProvider), - alloc::sync::Arc::new(TestStdioProvider), - alloc::sync::Arc::new(litebox_broker_core::fs::UnsupportedFileService), - ) - .unwrap() - }) -} - -struct TestStdioProvider; - -impl StdioProvider for TestStdioProvider { - fn read( - &self, - _cancellation: &AssociationCancellation, - _output: &mut [u8], - ) -> core::result::Result { - Err(StdioProviderError::Unsupported) - } - - fn write( - &self, - _cancellation: &AssociationCancellation, - _stream: StdioOutputStream, - _input: &[u8], - ) -> core::result::Result { - Err(StdioProviderError::Unsupported) - } - - fn is_terminal(&self, stream: StdioStream) -> core::result::Result { - Ok(stream == StdioStream::Stdout) - } -} - +/// Returns a task whose broker only serves the standard streams used during construction. #[must_use] pub(crate) fn init_platform() -> crate::Task { - let platform = test_platform(); - - init_platform_with_builder(crate::LinuxShimBuilder::new(platform)) + init_platform_with_files(ScriptedFiles::new([])) } +/// Returns a task whose broker answers file requests with `files`. #[must_use] -pub(crate) fn init_platform_with_broker() -> crate::Task { +fn init_platform_with_files(files: Arc) -> crate::Task { let platform = test_platform(); - let setup = TestBrokerSetup::new(); - let (broker_local, ()) = BrokerLocal::negotiate(setup, |setup| { - let memory: alloc::sync::Arc = setup.memory.clone(); - Ok((setup.activate(), memory, ())) - }) - .unwrap(); - let litebox = litebox::LiteBox::new_with_broker_local(platform, broker_local); - init_platform_with_builder(crate::LinuxShimBuilder::new_with_litebox(platform, litebox)) -} - -fn init_platform_with_builder( - shim_builder: crate::LinuxShimBuilder, -) -> crate::Task { - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let fs = alloc::sync::Arc::new(shim_builder.default_fs(in_mem, TEST_TAR_FILE.into())); - shim_builder.build().0.new_test_task(fs) -} - -struct TestBrokerSetup { - memory: alloc::sync::Arc, - session: BrokerSession, -} - -impl TestBrokerSetup { - fn new() -> Self { - Self { - memory: alloc::sync::Arc::new(TestSharedMemory::new()), - session: test_broker() - .create_session(CallerCredential::Unauthenticated) - .unwrap(), - } - } - - fn activate(self) -> TestBrokerChannel { - let shared_buffers = SharedBufferPool::new(self.memory, SHARED_BUFFER_LAYOUT).unwrap(); - TestBrokerChannel { - session: self.session, - shared_buffers, - } - } -} - -impl LocalSetupChannel for TestBrokerSetup { - type Error = core::convert::Infallible; - - fn send_handshake_request( - &mut self, - request: &BrokerHandshakeRequest, - ) -> core::result::Result<(), Self::Error> { - assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); - Ok(()) - } - - fn recv_handshake_response( - &mut self, - ) -> core::result::Result, Self::Error> { - Ok(Some(BrokerHandshakeResponse::Negotiated { - broker_protocol_version: BROKER_PROTOCOL_VERSION, - })) - } -} - -struct TestBrokerChannel { - session: BrokerSession, - shared_buffers: SharedBufferPool>, -} - -impl TestBrokerChannel { - fn execute(&self, operation: BrokerOperation) -> litebox_broker_core::Result { - match operation { - BrokerOperation::CloseObject(handle) => self - .session - .close_object_reference(handle) - .map(|()| BrokerResult::ObjectClosed), - BrokerOperation::CheckReadiness(handle) => self - .session - .check_readiness(handle) - .map(BrokerResult::Readiness), - BrokerOperation::Pipe(PipeRequest::Create(request)) => { - litebox_broker_core::pipe::create( - &self.session, - request.capacity, - request.atomic_write_size, - ) - .map(|(read_handle, write_handle)| { - BrokerResult::Pipe(PipeResponse::Create(CreatePipeResponse { - read_handle, - write_handle, - })) - }) - } - BrokerOperation::Pipe(PipeRequest::Read(request)) => { - let data = litebox_broker_core::pipe::read( - &self.session, - request.handle, - request.buffer.length, - )?; - self.shared_buffers - .write(request.buffer.slot_index, &data) - .expect("pipe broker read must use a valid shared buffer"); - Ok(BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { - read: u32::try_from(data.len()).unwrap(), - }))) - } - BrokerOperation::Pipe(PipeRequest::Write(request)) => { - let mut data = std::vec![0; request.buffer.length as usize]; - self.shared_buffers - .read(request.buffer.slot_index, &mut data) - .expect("pipe broker write must use a valid shared buffer"); - litebox_broker_core::pipe::write(&self.session, request.handle, &data).map( - |written| { - BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { - written: u32::try_from(written).unwrap(), - })) - }, - ) - } - BrokerOperation::Stdio(StdioRequest::IsTerminal(request)) => { - litebox_broker_core::stdio::is_terminal(&self.session, request.stream).map( - |is_terminal| { - BrokerResult::Stdio(StdioResponse::IsTerminal(IsTerminalStdioResponse { - is_terminal, - })) - }, - ) - } - operation => panic!("unexpected pipe test broker operation: {operation:?}"), - } - } -} - -impl LocalCallChannel for TestBrokerChannel { - type Error = core::convert::Infallible; - - fn call(&self, request: BrokerRequest) -> core::result::Result { - let result = self - .execute(request.operation) - .unwrap_or_else(|error| BrokerResult::Error(error.into())); - Ok(BrokerResponse { - request_id: request.request_id, - result, - }) - } -} - -struct TestSharedMemory(std::sync::Mutex>); - -impl TestSharedMemory { - fn new() -> Self { - Self(std::sync::Mutex::new(std::vec![ - 0; - SHARED_BUFFER_POOL_SIZE - ])) - } + let litebox = litebox::LiteBox::new_with_broker_local( + platform, + crate::syscalls::test_broker::negotiate(files), + ); + let shim_builder = crate::LinuxShimBuilder::new_with_litebox(platform, litebox); + shim_builder.build().0.new_test_task() } -impl SharedMemory for TestSharedMemory { - fn len(&self) -> usize { - SHARED_BUFFER_POOL_SIZE - } - - 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(()) - } +/// Returns a task and the scripted file fixture that answers its file requests. +#[must_use] +pub(crate) fn scripted_task( + script: impl IntoIterator, +) -> (Arc, crate::Task) { + let files = ScriptedFiles::new(script); + let task = init_platform_with_files(Arc::clone(&files)); + (files, task) } -struct PipeOnlySocketProvider; - -impl SocketProvider for PipeOnlySocketProvider { - fn create( - &self, - _session_id: SessionId, - _request: CreateSocketRequest, - _readiness: ReadinessRegistration, - ) -> litebox_broker_core::Result> { - Err(BrokerError::UnsupportedOperation) +/// Builds one scripted directory entry. +pub(crate) fn directory_entry(name: &str, file_type: FileType, ino: u64) -> FileDirectoryEntry { + FileDirectoryEntry { + name: alloc::string::String::from(name), + file_type, + ino_info: Some(FileNodeInfo { + dev: 1, + ino, + rdev: None, + }), } - - fn close_session(&self, _session_id: SessionId) {} } -struct UnusedRandomProvider; - -impl RandomProvider for UnusedRandomProvider { - fn fill(&self, _output: &mut [u8]) -> core::result::Result<(), RandomProviderError> { - Err(RandomProviderError) - } +/// Builds the protocol mode a request is expected to carry. +pub(crate) fn mode(bits: u16) -> Mode { + Mode::from_bits(bits).expect("test modes must be supported") } #[cfg(target_arch = "x86_64")] @@ -416,7 +193,7 @@ fn exceptions_queue_their_corresponding_signals() { #[test] fn test_fcntl() { - let task = init_platform_with_broker(); + let task = init_platform(); let check = |fd: i32, flags1: OFlags, flags2: OFlags| { assert_eq!( @@ -444,16 +221,6 @@ fn test_fcntl() { let write_fd = i32::try_from(write_fd).unwrap(); check(write_fd, OFlags::WRONLY | OFlags::NONBLOCK, OFlags::WRONLY); - // Eventfd requires broker control in this shim configuration. - let brokerless_task = init_platform(); - assert_eq!( - brokerless_task.sys_eventfd2( - 0, - EfdFlags::CLOEXEC | EfdFlags::SEMAPHORE | EfdFlags::NONBLOCK, - ), - Err(Errno::EIO) - ); - // Test fcntl with DUPFD let fd = task .sys_open("/dev/stdin", OFlags::RDONLY, Mode::empty()) @@ -475,16 +242,9 @@ fn test_fcntl() { assert_eq!(duplicated, min_fd); } -#[test] -fn test_pipe2_requires_broker() { - let task = init_platform(); - - assert_eq!(task.sys_pipe2(OFlags::empty()), Err(Errno::EIO)); -} - #[test] fn test_pipe2_race_with_concurrent_close() { - let task = init_platform_with_broker(); + let task = init_platform(); task.files.borrow().set_max_fd(4); let stop = alloc::sync::Arc::new(core::sync::atomic::AtomicBool::new(false)); @@ -537,342 +297,332 @@ fn test_dup() { assert_eq!(fd2 + 10, fd4); } -// Note the test was generated by copilot with minor fixes. -#[test] -fn test_getdent64() { - let task = init_platform(); - - // Create test files in root directory for testing - let file1_fd = task - .sys_open( - "/test_file1.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RUSR | Mode::WUSR, - ) - .expect("Failed to create test_file1.txt"); - task.sys_close(file1_fd.try_into().unwrap()) - .expect("Failed to close test_file1.txt"); - - let file2_fd = task - .sys_open( - "/test_file2.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RUSR | Mode::WUSR, - ) - .expect("Failed to create test_file2.txt"); - task.sys_close(file2_fd.try_into().unwrap()) - .expect("Failed to close test_file2.txt"); - - // Open the root directory for testing - let dir_fd = task - .sys_open("/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open root directory"); - let dir_fd = dir_fd.try_into().unwrap(); - - // Test 1: Basic functionality - read directory entries - let mut buffer = alloc::vec![0u8; 4096]; - let bytes_read = task - .sys_getdirent64( - dir_fd, - UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), - buffer.len(), - ) - .expect("Failed to read directory entries"); - - assert!(bytes_read > 0, "Should have read some directory entries"); - assert!( - bytes_read <= buffer.len(), - "Should not read more than buffer size" - ); - - // Parse the returned entries to verify they are correct +/// Parses the `linux_dirent64` entries the shim wrote, as `(name, type, inode, offset)`. +fn parse_dirents(buffer: &[u8]) -> Vec<(alloc::string::String, u8, u64, u64)> { + let mut entries = Vec::new(); let mut offset = 0; - let mut found_entries = alloc::vec::Vec::new(); - - while offset < bytes_read { + while offset < buffer.len() { let (dirent, _) = litebox_common_linux::LinuxDirent64::read_from_prefix(&buffer[offset..]).unwrap(); - - // Validate the entry length - assert!(dirent.len > 0, "Directory entry length must be positive"); + assert!(dirent.len > 0, "directory entry length must be positive"); assert!( - offset + dirent.len as usize <= bytes_read, - "Entry should not exceed buffer" + offset + dirent.len as usize <= buffer.len(), + "an entry must not exceed the reported bytes" ); - let name_bytes = { let start = offset + core::mem::offset_of!(litebox_common_linux::LinuxDirent64, __name); - let end = offset + dirent.len as usize; - &buffer[start..end] + &buffer[start..offset + dirent.len as usize] }; - - // Find the null terminator - let null_pos = name_bytes + let name_len = name_bytes .iter() - .position(|&b| b == 0) + .position(|byte| *byte == 0) .unwrap_or(name_bytes.len()); - let name = - core::str::from_utf8(&name_bytes[..null_pos]).expect("Invalid UTF-8 in filename"); - - found_entries.push((alloc::string::String::from(name), dirent.typ, dirent.ino)); + let name = core::str::from_utf8(&name_bytes[..name_len]).expect("names must be UTF-8"); + entries.push(( + alloc::string::String::from(name), + dirent.typ, + dirent.ino, + dirent.off, + )); offset += dirent.len as usize; } + entries +} - assert!( - !found_entries.is_empty(), - "Should find at least some directory entries" - ); +/// Opens a directory over the scripted fixture and returns its descriptor. +fn scripted_dir_fd(files: &ScriptedFiles, task: &crate::Task, path: &str) -> i32 { + files.script([opened(FILE_HANDLE)]); + let fd = task + .sys_open(path, OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .expect("the scripted open must succeed"); + let _ = files.take_calls(); + i32::try_from(fd).unwrap() +} - // Check that our test files appear in the directory listing - let mut entry_names: alloc::vec::Vec = found_entries - .iter() - .map(|(name, _, _)| name.clone()) - .collect(); - entry_names.sort(); - assert_eq!( - entry_names, - alloc::vec![ - ".", - "..", - "bar", - "dev", - "foo", - "test_file1.txt", - "test_file2.txt" - ] +fn close_scripted_file( + files: &ScriptedFiles, + task: &crate::Task, + fd: i32, + handle: ObjectHandle, +) { + files.script([closed()]); + task.sys_close(fd).expect("the scripted close must succeed"); + assert!( + matches!(files.take_calls().last(), Some(FileCall::Close(actual)) if *actual == handle), + "closing the guest fd must close its broker handle" ); +} - // Verify that our test files have the correct type (regular file) - for (name, typ, _) in &found_entries { - if name == "test_file1.txt" || name == "test_file2.txt" { - assert_eq!( - *typ, - litebox_common_linux::DirentType::Regular as u8, - "Test files should have Regular type" - ); - } +fn directory_pages(entries: &[FileDirectoryEntry], maximum_length: usize) -> Vec { + let mut pages = Vec::new(); + let mut start_index = 0; + loop { + let (payload, next_index) = encode_directory_entries_chunk( + entries, + start_index, + maximum_length.min(MAX_FILE_TRANSFER_SIZE as usize), + ) + .expect("scripted directory entries must encode"); + pages.push(Scripted::Directory { + payload, + next_index, + }); + let Some(next_index) = next_index else { + return pages; + }; + start_index = usize::try_from(next_index).unwrap(); } +} - assert_eq!( - task.sys_getdirent64( - dir_fd, - UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), - buffer.len() - ) - .expect("Failed to read directory entries"), - 0, - "should have read all entries in the previous call" - ); - task.sys_close(dir_fd).expect("Failed to close directory"); - - // Test 2: Small buffer (should handle partial reads gracefully) - let dir_fd = task - .sys_open("/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open root directory"); - let dir_fd = dir_fd.try_into().unwrap(); - let mut small_buffer = [0u8; 64]; - let bytes = task +#[test] +fn getdirent64_encodes_the_entries_the_broker_returns() { + let entries = vec![ + directory_entry(".", FileType::Directory, 1), + directory_entry("..", FileType::Directory, 1), + directory_entry("file.txt", FileType::RegularFile, 7), + directory_entry("sub", FileType::Directory, 9), + ]; + let (files, task) = scripted_task([]); + let dir_fd = scripted_dir_fd(&files, &task, "/dir"); + + files.script(directory_pages(&entries, usize::MAX)); + let mut buffer = vec![0u8; 4096]; + let read = task .sys_getdirent64( dir_fd, - UserPtrMut::from_usize(small_buffer.as_mut_ptr() as usize), - small_buffer.len(), + UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), + buffer.len(), ) - .expect("Failed to read directory entries"); - - // Should either succeed with partial data or return 0 if no entry fits - assert!(bytes <= small_buffer.len(), "Should not exceed buffer size"); - // If bytes > 0, verify the structure is valid - if bytes > 0 { - let (dirent, _) = - litebox_common_linux::LinuxDirent64::read_from_prefix(&small_buffer[..bytes]).unwrap(); - assert!( - dirent.len as usize <= bytes, - "First entry length should fit in returned bytes" - ); - assert!(dirent.len > 0, "Entry length should be positive"); - } + .expect("the directory read must succeed"); - // Test 3: Invalid file descriptor - let result = task.sys_getdirent64( - -1, - UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), - buffer.len(), - ); assert_eq!( - result, - Err(Errno::EBADF), - "Should return EBADF for invalid fd" - ); - - // Test 4: File descriptor pointing to a regular file (not a directory) - let file1_fd = task - .sys_open("/test_file1.txt", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open test_file1.txt"); - let file1_fd = file1_fd.try_into().unwrap(); - - let result = task.sys_getdirent64( - file1_fd, - UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), - buffer.len(), + files.take_calls(), + vec![FileCall::ReadDirectory { + handle: FILE_HANDLE, + start_index: 0, + }] ); assert_eq!( - result, - Err(Errno::ENOTDIR), - "Should return ENOTDIR for non-directory fd" + parse_dirents(&buffer[..read]), + vec![ + (".".into(), DirentType::Directory as u8, 1, 0), + ("..".into(), DirentType::Directory as u8, 1, 1), + ("file.txt".into(), DirentType::Regular as u8, 7, 2), + ("sub".into(), DirentType::Directory as u8, 9, 3), + ], + "entries are reported sorted by name, with their broker type and inode" ); - task.sys_close(file1_fd).expect("Failed to close file"); - // Test 5: Zero-length buffer - let result = task.sys_getdirent64( - dir_fd, - UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), - 0, - ); + // A second read resumes after the entries already reported. + files.script(directory_pages(&entries, usize::MAX)); assert_eq!( - result, - Err(Errno::EINVAL), - "Should return EINVAL for zero-length buffer" + task.sys_getdirent64( + dir_fd, + UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), + buffer.len(), + ), + Ok(0), + "the previous call already reported every entry" ); + close_scripted_file(&files, &task, dir_fd, FILE_HANDLE); +} - task.sys_close(dir_fd).expect("Failed to close directory"); - - // Test 6: Multiple reads (test directory offset tracking) - // Reopen directory to reset position - let dir_fd2 = task - .sys_open("/", OFlags::RDONLY, Mode::empty()) - .expect("Failed to reopen root directory"); - let dir_fd2 = dir_fd2.try_into().unwrap(); - - // Read entries in smaller chunks to test offset tracking - let mut all_entries = alloc::vec::Vec::new(); - +#[test] +fn getdirent64_resumes_across_buffers() { + let entries = vec![ + directory_entry("aaaaaaaaaaaaaaaa", FileType::RegularFile, 1), + directory_entry("bbbbbbbbbbbbbbbb", FileType::RegularFile, 2), + directory_entry("cccccccccccccccc", FileType::RegularFile, 3), + ]; + let (files, task) = scripted_task([]); + let dir_fd = scripted_dir_fd(&files, &task, "/dir"); + + let mut names = Vec::new(); + let mut pages = Vec::new(); + let mut chunk = [0u8; 48]; loop { - let mut chunk_buffer = [0u8; 64]; - let bytes_read = task + files.script(directory_pages(&entries, 64)); + let read = task .sys_getdirent64( - dir_fd2, - UserPtrMut::from_usize(chunk_buffer.as_mut_ptr() as usize), - chunk_buffer.len(), + dir_fd, + UserPtrMut::from_usize(chunk.as_mut_ptr() as usize), + chunk.len(), ) - .expect("Failed to read directory chunk"); - - if bytes_read == 0 { - break; // End of directory + .expect("chunked directory reads must succeed"); + pages.extend(files.take_calls()); + if read == 0 { + break; } + assert!(read <= chunk.len(), "the shim must respect the buffer size"); + names.extend( + parse_dirents(&chunk[..read]) + .into_iter() + .map(|entry| entry.0), + ); + } + assert_eq!( + names, + vec!["aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb", "cccccccccccccccc"] + ); + assert!( + pages.iter().any(|call| matches!( + call, + FileCall::ReadDirectory { start_index, .. } if *start_index > 0 + )), + "the guest must resume from the continuation index the broker reported" + ); + close_scripted_file(&files, &task, dir_fd, FILE_HANDLE); +} - // Parse entries from this chunk - let mut offset = 0; - while offset < bytes_read { - let (dirent, _) = litebox_common_linux::LinuxDirent64::read_from_prefix( - &chunk_buffer[offset..bytes_read], - ) - .unwrap(); +#[test] +fn getdirent64_translates_descriptor_and_broker_errors() { + let (files, task) = scripted_task([]); + let mut buffer = [0u8; 256]; - assert!(dirent.len > 0, "Entry length must be positive"); - assert!( - offset + dirent.len as usize <= bytes_read, - "Entry should fit in chunk" - ); + // An unknown descriptor never reaches the broker. + assert_eq!( + task.sys_getdirent64( + -1, + UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), + buffer.len(), + ), + Err(Errno::EBADF) + ); + assert!(files.take_calls().is_empty()); - let name_bytes = { - let start = - offset + core::mem::offset_of!(litebox_common_linux::LinuxDirent64, __name); - let end = offset + dirent.len as usize; - &chunk_buffer[start..end] - }; - - let null_pos = name_bytes - .iter() - .position(|&b| b == 0) - .unwrap_or(name_bytes.len()); - let name = - core::str::from_utf8(&name_bytes[..null_pos]).expect("Invalid UTF-8 in filename"); - - all_entries.push(alloc::string::String::from(name)); - offset += dirent.len as usize; - } - } + let dir_fd = scripted_dir_fd(&files, &task, "/dir"); + files.script([failed(FileError::NotDirectory)]); + assert_eq!( + task.sys_getdirent64( + dir_fd, + UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), + buffer.len(), + ), + Err(Errno::ENOTDIR), + "a broker not-a-directory failure surfaces as ENOTDIR" + ); - // Verify we still got our expected entries through chunked reading - all_entries.sort(); + // A zero-length buffer cannot hold an entry. + let entries = vec![directory_entry("file", FileType::RegularFile, 1)]; + files.script(directory_pages(&entries, usize::MAX)); assert_eq!( - all_entries, - alloc::vec![ - ".", - "..", - "bar", - "dev", - "foo", - "test_file1.txt", - "test_file2.txt" - ] + task.sys_getdirent64( + dir_fd, + UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), + 0 + ), + Err(Errno::EINVAL) ); + close_scripted_file(&files, &task, dir_fd, FILE_HANDLE); } #[test] -fn test_umask_behavior() { - let task = init_platform(); +fn umask_masks_the_creation_mode_sent_to_the_broker() { + let (files, task) = scripted_task([]); - // 1. Capture original mask without changing final state. - let orig = task.sys_umask(0).bits(); // sets mask to 0, returns previous - let _ = task.sys_umask(orig); // restore original + // The default mask is 022, and `umask` returns the previous mask. + assert_eq!(task.sys_umask(0o077).bits(), 0o022); - // We expect the default (from implementation) to be 0o022. - assert_eq!(orig, 0o022, "Default umask should be 022 (got {orig:03o})"); - - // 2. Set a new umask (e.g., 0o077) and verify file creation honors it. - let prev = task.sys_umask(0o077).bits(); - assert_eq!(prev, orig, "Setting umask should return previous value"); - - // Create a file with mode 0o666; with umask 0o077 it should become 0o600. - let file_mode = Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::WGRP | Mode::ROTH | Mode::WOTH; // 0o666 - let test_file = "/umask_rs_test_file_perm.txt"; + files.script([opened(FILE_HANDLE)]); let fd = task - .sys_open(test_file, OFlags::CREAT | OFlags::WRONLY, file_mode) - .expect("Failed to create test file with O_CREAT"); - // Close it (ignore errors) - let _ = task.sys_close(i32::try_from(fd).unwrap()); + .sys_open( + "/masked_file", + OFlags::CREAT | OFlags::WRONLY, + Mode::from_bits_retain(0o666), + ) + .expect("the scripted create must succeed"); + assert_eq!( + files.take_calls(), + vec![FileCall::Open { + path: "/masked_file".into(), + user: ROOT, + access: FileAccessMode::WriteOnly, + flags: FileOpenFlags::CREATE, + mode: mode(0o600), + }], + "the broker is asked to create the file with 0o666 & !0o077" + ); + close_scripted_file(&files, &task, i32::try_from(fd).unwrap(), FILE_HANDLE); - let stat_file = task.sys_stat(test_file).expect("stat failed on test file"); - let actual_file_perm = stat_file.st_mode & 0o777; + files.script([Scripted::Reply(FileResponse::Mkdir)]); + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "/masked_dir", 0o777) + .expect("the scripted mkdir must succeed"); assert_eq!( - actual_file_perm, 0o600, - "File permission should respect umask (expected 600 got {actual_file_perm:03o})", + files.take_calls(), + vec![FileCall::Mkdir { + path: "/masked_dir".into(), + user: ROOT, + mode: mode(0o700), + }], + "the broker is asked to create the directory with 0o777 & !0o077" ); - // 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_mkdirat(litebox_common_linux::AT_FDCWD, test_dir, dir_mode) - .expect("Failed to create test directory"); + // Only the low nine bits of a new mask are retained. + assert_eq!(task.sys_umask(0o1777).bits(), 0o077); + assert_eq!(task.sys_umask(0o022).bits(), 0o777); +} + +#[test] +fn unlinkat_routes_by_flag_and_translates_broker_failures() { + let (files, task) = scripted_task([]); + + // AT_REMOVEDIR combined with any other flag is rejected before the broker is asked. + assert_eq!( + task.sys_unlinkat( + litebox_common_linux::AT_FDCWD, + "/dir", + AtFlags::AT_REMOVEDIR | AtFlags::AT_SYMLINK_NOFOLLOW, + ), + Err(Errno::EINVAL) + ); + assert!(files.take_calls().is_empty()); - let stat_dir = task - .sys_stat(test_dir) - .expect("stat failed on test directory"); - let actual_dir_perm = stat_dir.st_mode & 0o777; + files.script([Scripted::Reply(FileResponse::Unlink)]); + task.sys_unlinkat(litebox_common_linux::AT_FDCWD, "/file", AtFlags::empty()) + .expect("the scripted unlink must succeed"); assert_eq!( - actual_dir_perm, 0o700, - "Directory permission should respect umask (expected 700 got {actual_dir_perm:03o})", + files.take_calls(), + vec![FileCall::Unlink { + path: "/file".into(), + user: ROOT, + }] ); - // 4. High bits are ignored: set mask with bits beyond 0o777. - // Current mask is 0o077; now set 0o1777 -> stored low 9 bits = 0o777. - let prev2 = task.sys_umask(0o1777).bits(); - assert_eq!(prev2, 0o077, "Returned previous mask should be 077"); - let prev3 = task.sys_umask(0).bits(); // fetch current (0o777) and set to 0 + files.script([Scripted::Reply(FileResponse::Rmdir)]); + task.sys_unlinkat( + litebox_common_linux::AT_FDCWD, + "/dir", + AtFlags::AT_REMOVEDIR, + ) + .expect("the scripted rmdir must succeed"); assert_eq!( - prev3, 0o777, - "Only low 9 bits should be retained (expected 777)" + files.take_calls(), + vec![FileCall::Rmdir { + path: "/dir".into(), + user: ROOT, + }], + "AT_REMOVEDIR is routed to the directory-removal request" ); - // Restore to original - let _ = task.sys_umask(orig); + + for (flags, error, errno) in [ + (AtFlags::empty(), FileError::IsDirectory, Errno::EISDIR), + (AtFlags::AT_REMOVEDIR, FileError::NotEmpty, Errno::ENOTEMPTY), + ] { + files.script([failed(error)]); + assert_eq!( + task.sys_unlinkat(litebox_common_linux::AT_FDCWD, "/target", flags), + Err(errno), + "{error:?} must surface as {errno:?}" + ); + let _ = files.take_calls(); + } } #[test] fn test_rlimit_nofile() { use litebox_common_linux::{Rlimit, RlimitResource, errno::Errno}; - let task = crate::syscalls::tests::init_platform(); + let (files, task) = scripted_task([opened(FILE_HANDLE), closed()]); // 1. Get the current NOFILE limit. let cur_lim = task @@ -918,91 +668,19 @@ fn test_rlimit_nofile() { .expect_err("open should fail due to new cur limit"), Errno::EMFILE, ); -} - -#[test] -fn test_unlinkat() { - let task = init_platform(); - - // 1. Create a regular file and unlink it. - let file_path = "/unlink_test_file.txt"; - let fd = task - .sys_open( - file_path, - OFlags::CREAT | OFlags::WRONLY, - Mode::RUSR | Mode::WUSR, - ) - .expect("Failed to create test file for unlink"); - task.sys_close(i32::try_from(fd).unwrap()) - .expect("Failed to close test file"); - task.sys_unlinkat(0, file_path, AtFlags::empty()) - .expect("unlinkat should succeed on regular file"); - assert_eq!( - task.sys_stat(file_path), - Err(Errno::ENOENT), - "File should no longer exist after unlink" - ); - - // 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_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()), - Err(Errno::EISDIR), - "Unlinking a directory without AT_REMOVEDIR should return EISDIR" - ); - - // 3. Create a non-empty directory and remove with AT_REMOVEDIR -> ENOTEMPTY. - let nonempty_dir = "/unlink_dir_nonempty"; - 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( - "/unlink_dir_nonempty/inner.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RUSR | Mode::WUSR, - ) - .expect("Failed to create inner file"); - task.sys_close(i32::try_from(inner_file_fd).unwrap()) - .expect("Failed to close inner file"); - assert_eq!( - task.sys_unlinkat(0, nonempty_dir, AtFlags::AT_REMOVEDIR), - Err(Errno::ENOTEMPTY), - "Removing a non-empty directory with AT_REMOVEDIR should return ENOTEMPTY" - ); - - // 4. Invalid flag combination: AT_REMOVEDIR | (any other flag) -> EINVAL. - assert_eq!( - task.sys_unlinkat( - 0, - dir_path, - AtFlags::AT_REMOVEDIR | AtFlags::AT_SYMLINK_NOFOLLOW - ), - Err(Errno::EINVAL), - "Invalid extra flags with AT_REMOVEDIR should return EINVAL" - ); - - // 5. Successfully remove previously created empty directory with AT_REMOVEDIR. - task.sys_unlinkat(0, dir_path, AtFlags::AT_REMOVEDIR) - .expect("Should remove empty directory with AT_REMOVEDIR"); - assert_eq!( - task.sys_stat(dir_path), - Err(Errno::ENOENT), - "Directory should no longer exist after removal" - ); - - // 6. Create and remove another empty directory to ensure repeatability. - let empty_dir2 = "/unlink_empty_dir"; - 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"); - assert_eq!( - task.sys_stat(empty_dir2), - Err(Errno::ENOENT), - "Second directory should no longer exist after removal" + files.take_calls(), + vec![ + FileCall::Open { + path: "/prlimit_file".into(), + user: ROOT, + access: FileAccessMode::ReadOnly, + flags: FileOpenFlags::CREATE, + mode: mode(0o700), + }, + FileCall::Close(FILE_HANDLE), + ], + "an open that cannot acquire a guest fd must close the broker handle" ); } diff --git a/litebox_shim_linux/src/syscalls/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index ca4200cb1..74ef37c27 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -21,17 +21,18 @@ use litebox::{ wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, - fs::{Mode, OFlags, errors::OpenError}, + fs::errors::OpenError, sync::{Mutex, RwLock}, utils::TruncateExt as _, }; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags}; use litebox_common_linux::{ - IpOption, ReceiveFlags, SendFlags, ShutdownHow, SockFlags, SockType, SocketOption, + IpOption, OFlags, ReceiveFlags, SendFlags, ShutdownHow, SockFlags, SockType, SocketOption, SocketOptionName, errno::Errno, }; use crate::{ - FileFd, GlobalState, LinuxFS, ShimPlatform, Task, UserPtr, UserPtrMut, + FileFd, GlobalState, ShimPlatform, Task, UserPtr, UserPtrMut, channel::{Channel, ReadEnd, WriteEnd}, syscalls::net::{SocketOptionValue, SocketOptions}, }; @@ -70,7 +71,7 @@ pub(crate) enum UnixSocketAddr { /// the socket file remains accessible. The file is automatically closed /// when this structure is dropped. enum UnixBoundSocketAddr { - Path((String, FileFd, Arc>)), + Path((String, FileFd, Arc>)), Abstract(Vec), } @@ -112,20 +113,20 @@ impl UnixSocketAddr { let flags = if is_server { // create the socket file if not exists; // use O_EXCL to ensure exclusive creation - OFlags::CREAT | OFlags::EXCL | OFlags::RDWR + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE } else { - OFlags::RDWR + FileOpenFlags::NONE }; // TODO: extend fs to support creating sock file (i.e., with type `InodeType::Socket`) let file = { - let files = task.files.borrow(); let fs = task.fs.borrow(); let context = fs.context.read(); - files - .fs - .open( + task.global + .litebox + .open_file( &context, path.as_str(), + FileAccessMode::ReadWrite, flags, Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, ) @@ -137,7 +138,7 @@ impl UnixSocketAddr { Ok(UnixBoundSocketAddr::Path(( path, file, - task.files.borrow().fs.clone(), + Arc::clone(&task.global.litebox), ))) } UnixSocketAddr::Abstract(data) => { @@ -174,7 +175,7 @@ impl Drop for UnixBoundSocketAddr { fn drop(&mut self) { match self { Self::Path((_, file, fs)) => { - let _ = fs.close(file); + let _ = fs.close_file(file); } Self::Abstract(_) => {} } diff --git a/litebox_shim_linux/src/transport.rs b/litebox_shim_linux/src/transport.rs deleted file mode 100644 index 45c7fe351..000000000 --- a/litebox_shim_linux/src/transport.rs +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Spin-polling TCP transport over the shim's internal network stack. - -use alloc::boxed::Box; -use alloc::sync::Arc; - -use litebox::fs::nine_p::transport; -use litebox::net::socket_channel::{ChannelReadError, ChannelWriteError, NetworkProxy}; -use litebox::net::{ReceiveFlags, SendFlags}; -use litebox_common_linux::{SockFlags, SockType, errno::Errno}; - -use crate::syscalls::net::SocketFd; -use crate::{GlobalState, ShimPlatform}; - -/// Handles socket cleanup on drop without exposing the concrete socket/global-state types. -/// -/// This is stored as `Box` inside [`ShimTransport`] so that the -/// transport itself does not need to name them. -// XXX: this erasure only existed to hide the old `FS` generic. Now that `SocketDropGuard`'s fields -// are nameable from `Platform` alone, we could inline them into [`ShimTransport`] and drop this -// trait. However, this `DropGuard` _may_ be worth keeping if a future non-socket backing (shared -// memory, ...) needs to share `ShimTransport`. -trait DropGuard: Send + Sync { - fn close(&mut self); -} - -/// Concrete, generic implementation of [`DropGuard`]. -struct SocketDropGuard { - global: Arc>, - sockfd: SocketFd, -} - -impl DropGuard for SocketDropGuard { - fn close(&mut self) { - let _ = self - .global - .net - .lock() - .close(&self.sockfd, litebox::net::CloseBehavior::Immediate); - } -} - -/// A spin-polling TCP transport backed by a raw `SocketFd` and its [`NetworkProxy`]. -/// -/// The socket lives in the litebox descriptor table (for metadata / proxy) but is -/// **not** registered in the guest's file-descriptor table, keeping it invisible -/// to the guest program. -/// -/// All I/O goes through the non-blocking [`NetworkProxy`] methods directly -/// (`try_read` / `try_write`), with spin-polling when data is not yet available. -/// This avoids the need for a `WaitState` or any association with a particular -/// guest `Task`. -pub struct ShimTransport { - drop_guard: Box, - proxy: Arc>, -} - -impl ShimTransport { - /// Create a TCP socket, connect it to `addr`, and return a transport. - /// - /// The socket is created via [`litebox::net::Network::socket`] and initialised - /// with [`GlobalState::initialize_socket`] so that the channel-based proxy is - /// set up, but the socket is **not** assigned a guest fd number. - /// - /// Connection and all subsequent I/O use the [`NetworkProxy`] directly, - /// spin-polling when the operation cannot complete immediately. - pub(crate) fn connect( - global: Arc>, - addr: core::net::SocketAddr, - ) -> Result { - // 1. Create the raw socket. - let sockfd = global - .net - .lock() - .socket(litebox::net::Protocol::Tcp) - .map_err(Errno::from)?; - - // 2. Initialise metadata / proxy in the litebox descriptor table. - let proxy = global.initialize_socket(&sockfd, SockType::Stream, SockFlags::empty()); - - // 3. Initiate the TCP connection. - let mut check_progress = false; - loop { - match global.net.lock().connect(&sockfd, &addr, check_progress) { - Ok(()) => break, - Err(litebox::net::errors::ConnectError::InProgress) => { - core::hint::spin_loop(); - check_progress = true; - } - Err(e) => return Err(Errno::from(e)), - } - } - - let drop_guard = Box::new(SocketDropGuard { global, sockfd }); - - Ok(Self { drop_guard, proxy }) - } -} - -impl Drop for ShimTransport { - fn drop(&mut self) { - self.drop_guard.close(); - } -} - -impl transport::Read for ShimTransport { - fn read(&mut self, buf: &mut [u8]) -> Result { - loop { - match self.proxy.try_read(buf, ReceiveFlags::empty(), None) { - Err(ChannelReadError::WouldBlock) => { - // No data yet — spin until something arrives. - core::hint::spin_loop(); - } - Ok(n) => return Ok(n), - Err(_) => return Err(transport::ReadError), - } - } - } -} - -impl transport::Write for ShimTransport { - fn write(&mut self, buf: &[u8]) -> Result { - loop { - match self.proxy.try_write(buf, SendFlags::empty(), None) { - Ok(n) => return Ok(n), - Err(ChannelWriteError::BufferFull) => { - // TX ring full — spin until space opens up. - core::hint::spin_loop(); - } - Err(_) => return Err(transport::WriteError), - } - } - } -} - -// require network support -#[cfg(target_os = "linux")] -#[cfg(test)] -mod tests { - extern crate std; - - use core::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; - use std::net::TcpListener; - use std::path::Path; - - use litebox::fs::nine_p::NineP; - use litebox::fs::resolver::Resolver; - use litebox::fs::{Mode, OFlags}; - - use crate::syscalls::tests::init_platform; - - use super::*; - - fn find_free_port() -> u16 { - let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind to port 0"); - listener.local_addr().unwrap().port() - } - - struct DiodServer { - child: std::process::Child, - port: u16, - _export_dir: tempfile::TempDir, - export_path: std::path::PathBuf, - } - - impl DiodServer { - const MAX_START_ATTEMPTS: usize = 5; - - fn start() -> Self { - let export_dir = tempfile::tempdir().expect("failed to create temp dir"); - let export_path = export_dir.path().to_path_buf(); - - for attempt in 0..Self::MAX_START_ATTEMPTS { - let port = find_free_port(); - - let mut child = std::process::Command::new("diod") - .args([ - "--foreground", - "--no-auth", - "--export", - export_dir.path().to_str().unwrap(), - "--listen", - &std::format!("0.0.0.0:{port}"), - "--nwthreads", - "1", - ]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::piped()) - .spawn() - .expect("failed to start diod – is it installed? (`apt install diod`)"); - - if Self::wait_until_ready(&mut child, port) { - return Self { - child, - port, - _export_dir: export_dir, - export_path, - }; - } - - let _ = child.kill(); - let _ = child.wait(); - if attempt + 1 < Self::MAX_START_ATTEMPTS { - std::eprintln!( - "diod failed to bind to port {port}, retrying ({}/{})…", - attempt + 1, - Self::MAX_START_ATTEMPTS, - ); - } - } - - panic!( - "failed to start diod after {} attempts", - Self::MAX_START_ATTEMPTS, - ); - } - - fn wait_until_ready(child: &mut std::process::Child, port: u16) -> bool { - use std::net::TcpStream; - let addr = std::format!("127.0.0.1:{port}"); - for _ in 0..50 { - if let Some(_status) = child.try_wait().ok().flatten() { - return false; - } - if TcpStream::connect(&addr).is_ok() { - return true; - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - false - } - - fn export_path(&self) -> &Path { - &self.export_path - } - } - - impl Drop for DiodServer { - fn drop(&mut self) { - let _ = self.child.kill(); - let _ = self.child.wait(); - if let Some(mut stderr) = self.child.stderr.take() { - use std::io::Read as _; - let mut output = std::string::String::new(); - let _ = stderr.read_to_string(&mut output); - if !output.is_empty() { - std::eprintln!("--- diod stderr ---\n{output}\n--- end diod stderr ---"); - } - } - } - } - - /// Helper to create a `SocketAddr` for connection. - fn socket_addr(ip: [u8; 4], port: u16) -> SocketAddr { - SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), - port, - )) - } - - fn connect_9p( - task: &crate::Task, - server: &DiodServer, - ) -> Resolver { - let addr = socket_addr([127, 0, 0, 1], server.port); - let transport = ShimTransport::connect(task.global.clone(), addr) - .expect("failed to connect to 9P server via shim network"); - - let aname = server.export_path().to_str().unwrap(); - let username = std::env::var("USER") - .or_else(|_| std::env::var("LOGNAME")) - .unwrap_or_else(|_| std::string::String::from("nobody")); - - let composer = litebox::fs::composer::Composer::builder() - .mount("/", |allocator| { - NineP::::new( - transport, 65536, &username, aname, allocator, - ) - .expect("failed to create 9P filesystem") - }) - .build() - .expect("a single mount at `/`"); - Resolver::new(&task.global.litebox, composer) - } - - // ----------------------------------------------------------------------- - // Tests (require broker-backed socket setup + diod) - // ----------------------------------------------------------------------- - - #[test] - #[ignore = "requires broker-backed socket test setup"] - fn test_nine_p_create_and_read_file() { - let ctx = litebox::fs::resolver::Context::new(); - let task = init_platform(); - - let server = DiodServer::start(); - let fs = connect_9p(&task, &server); - - // Create a file and write to it. - let fd = fs - .open( - &ctx, - "/hello.txt", - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXU, - ) - .expect("failed to create file via 9P"); - - let data = b"Hello from litebox shim 9P!"; - let written = fs.write(&fd, data, None).expect("failed to write via 9P"); - assert_eq!(written, data.len()); - fs.close(&fd).expect("failed to close file"); - - // Verify on host. - let host_path = server.export_path().join("hello.txt"); - assert!(host_path.exists(), "file should exist on host"); - let host_content = std::fs::read_to_string(&host_path).unwrap(); - assert_eq!(host_content, "Hello from litebox shim 9P!"); - - // Read back through 9P. - let fd = fs - .open(&ctx, "/hello.txt", OFlags::RDONLY, Mode::empty()) - .expect("failed to open file for reading"); - - let mut buf = alloc::vec![0u8; 256]; - let n = fs.read(&fd, &mut buf, None).expect("failed to read via 9P"); - assert_eq!(&buf[..n], data); - fs.close(&fd).expect("failed to close file"); - } - - #[test] - #[ignore = "requires broker-backed socket test setup"] - fn test_nine_p_host_files_visible() { - let ctx = litebox::fs::resolver::Context::new(); - let task = init_platform(); - - let server = DiodServer::start(); - - // Pre-populate files on the host side. - std::fs::write(server.export_path().join("host_file.txt"), "from host").unwrap(); - std::fs::create_dir(server.export_path().join("host_dir")).unwrap(); - std::fs::write( - server.export_path().join("host_dir/inner.txt"), - "inner content", - ) - .unwrap(); - - let fs = connect_9p(&task, &server); - - // Read file created on the host through 9P. - let fd = fs - .open(&ctx, "/host_file.txt", OFlags::RDONLY, Mode::empty()) - .expect("failed to open host file via 9P"); - let mut buf = alloc::vec![0u8; 256]; - let n = fs.read(&fd, &mut buf, None).unwrap(); - assert_eq!(&buf[..n], b"from host"); - fs.close(&fd).unwrap(); - - // List host directory through 9P. - let fd = fs - .open( - &ctx, - "/host_dir", - OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), - ) - .expect("failed to open host dir via 9P"); - let entries = fs.read_dir(&fd).unwrap(); - fs.close(&fd).unwrap(); - - let names: alloc::vec::Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect(); - assert!( - names.contains(&"inner.txt"), - "host_dir should contain 'inner.txt', got: {names:?}" - ); - } -} diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml index 7075fb888..f6a9e3695 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 bitflags = { version = "2.9.0", default-features = false } int-enum = "1.2.0" litebox = { path = "../litebox/", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol/", 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 = { path = "../litebox_platform", version = "0.1.0" } @@ -20,9 +21,17 @@ thiserror = { version = "2.0.6", default-features = false } zerocopy = { version = "0.8", default-features = false, features = ["derive"] } [target.'cfg(target_os = "linux")'.dev-dependencies] +litebox_broker_core = { path = "../litebox_broker_core/", version = "0.1.0" } +litebox_broker_host = { path = "../litebox_broker_host/", version = "0.1.0" } +litebox_broker_local = { path = "../litebox_broker_local/", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport/", version = "0.1.0" } litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland/", version = "0.1.0" } [target.'cfg(target_os = "windows")'.dev-dependencies] +litebox_broker_core = { path = "../litebox_broker_core/", version = "0.1.0" } +litebox_broker_host = { path = "../litebox_broker_host/", version = "0.1.0" } +litebox_broker_local = { path = "../litebox_broker_local/", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport/", version = "0.1.0" } litebox_platform_windows_userland = { path = "../litebox_platform_windows_userland/", version = "0.1.0" } [lints] diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index d03096081..cfe4d20ca 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -11,7 +11,6 @@ extern crate alloc; -use alloc::borrow::Cow; use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::vec::Vec; @@ -33,16 +32,14 @@ use litebox_common_windows::{NtSysno, Win32Sysno}; use litebox_platform::time::TimeProvider; use crate::syscalls::event::{EventHandleObject, EventSubsystem}; -use crate::syscalls::file::{FileObject, FileObjectSubsystem}; +use crate::syscalls::file::FileObject; use crate::syscalls::iocp::{IoCompletionHandleObject, IoCompletionSubsystem}; use crate::syscalls::lpc::{LpcPortHandleObject, LpcPortSubsystem}; use crate::syscalls::mutant::{MutantHandleObject, MutantSubsystem}; use crate::syscalls::object_manager::{ DirectoryHandleObject, DirectoryObjectSubsystem, ObjectManager, }; -use crate::syscalls::registry::{ - NtNotifyChangeKeyRequest, RegistryKeyObject, RegistryKeySubsystem, -}; +use crate::syscalls::registry::{NtNotifyChangeKeyRequest, RegistryKeyObject}; use crate::syscalls::section::{ MapViewOfSectionParameters, SectionHandleObject, SectionObject, SectionSubsystem, }; @@ -65,6 +62,8 @@ mod nt_types; mod syscalls; mod wait; +#[cfg(test)] +mod test_broker; #[cfg(test)] mod tests; @@ -215,11 +214,6 @@ impl Clone for WindowsSectionView { } } -pub type DefaultFS = WindowsFS; - -pub type WindowsFS = - litebox::fs::resolver::Resolver; - fn write_value(address: usize, value: T) -> Option<()> where Platform: RawPointerProvider, @@ -402,11 +396,6 @@ pub struct WindowsShimBuilder { } impl WindowsShimBuilder { - #[must_use] - pub fn new(platform: &'static Platform) -> Self { - Self::new_with_litebox(platform, LiteBox::new(platform)) - } - /// Creates a builder backed by an existing LiteBox instance. #[must_use] pub fn new_with_litebox(platform: &'static Platform, litebox: LiteBox) -> Self { @@ -418,28 +407,19 @@ impl WindowsShimBuilder { &self.litebox } - /// Build the default file system with the given in-memory layer and tar data. - #[must_use] - pub fn default_fs( - &self, - in_mem: litebox::fs::in_mem::InMem, - tar_data: Cow<'static, [u8]>, - ) -> DefaultFS { - default_fs(&self.litebox, in_mem, tar_data) - } - #[must_use] pub fn build(self) -> WindowsShim { + let litebox = Arc::new(self.litebox); let global = Arc::new(GlobalState { platform: self.platform, - page_manager: PageManager::new(&self.litebox), - registry: syscalls::registry::RegistryStore::new(&self.litebox), + page_manager: PageManager::new(&litebox), + registry: syscalls::registry::RegistryStore::new(Arc::clone(&litebox)), wnf_states: syscalls::wnf::WnfStateStore::new( syscalls::wnf::WnfStateStoreData::default(), ), mui_generation: AtomicU32::new(1), qpc_boot_instant: TimeProvider::now(self.platform), - litebox: self.litebox, + litebox, }); WindowsShim(global) } @@ -506,7 +486,6 @@ impl WindowsShim { /// Loads the program at `path` as the shim's initial task. pub fn load_program( &self, - fs: Arc>, path: &str, argv: Vec, envp: Vec, @@ -515,6 +494,7 @@ impl WindowsShim { #[cfg(not(target_os = "windows"))] let _ = map_windows_user_shared_data::(&self.0.page_manager) .ok_or(loader::WindowsLoadError::MapSharedMemory)?; + let fs = Arc::clone(&self.0.litebox); 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. @@ -538,7 +518,7 @@ impl WindowsShim { global: self.0.clone(), process: process.clone(), fs, - fs_context: litebox::fs::resolver::Context::new(), + fs_context: litebox::fs::Context::new(), wait_state: wait::WaitState::new(self.0.platform), io_completion_worker: Mutex::new(syscalls::iocp::IoCompletionWorkerState::new()), entry_point: load_info.entry_point, @@ -561,7 +541,7 @@ struct GlobalState { wnf_states: syscalls::wnf::WnfStateStore, mui_generation: AtomicU32, qpc_boot_instant: ::Instant, - litebox: LiteBox, + litebox: Arc>, } /// Per-process Windows state shared by every thread in the process. @@ -727,8 +707,8 @@ impl Process { struct Task { global: Arc>, process: Arc>, - fs: Arc>, - fs_context: litebox::fs::resolver::Context, + fs: Arc>, + fs_context: litebox::fs::Context, wait_state: wait::WaitState, io_completion_worker: Mutex>, entry_point: usize, @@ -2514,8 +2494,8 @@ impl Task { }; } - try_metadata!(FileObjectSubsystem); - try_metadata!(RegistryKeySubsystem); + try_metadata!(FileObject); + try_metadata!(RegistryKeyObject); try_metadata!(EventSubsystem); try_metadata!(MutantSubsystem); try_metadata!(SemaphoreSubsystem); @@ -2564,8 +2544,8 @@ impl Task { }; } - try_set_attributes!(FileObjectSubsystem); - try_set_attributes!(RegistryKeySubsystem); + try_set_attributes!(FileObject); + try_set_attributes!(RegistryKeyObject); try_set_attributes!(EventSubsystem); try_set_attributes!(MutantSubsystem); try_set_attributes!(SemaphoreSubsystem); @@ -2736,8 +2716,8 @@ impl Task { }; } - try_duplicate!(FileObjectSubsystem); - try_duplicate!(RegistryKeySubsystem); + try_duplicate!(FileObject); + try_duplicate!(RegistryKeyObject); try_duplicate!(EventSubsystem); try_duplicate!(MutantSubsystem); try_duplicate!(SemaphoreSubsystem); @@ -2847,8 +2827,8 @@ impl Task { }; } - try_close!(FileObjectSubsystem, file); - try_close!(RegistryKeySubsystem, registry_key); + try_close!(FileObject, file); + try_close!(RegistryKeyObject, registry_key); try_close!(EventSubsystem, event); try_close!(MutantSubsystem, mutant); try_close!(SemaphoreSubsystem, semaphore); @@ -3013,9 +2993,9 @@ fn is_api_set_contract(dll_name: &str) -> bool { } trait RawHandleVisitor { - fn file(&self, file: FileObject); + fn file(&self, file: FileObject); - fn registry_key(&self, key: RegistryKeyObject); + fn registry_key(&self, key: RegistryKeyObject); fn event(&self, event: EventHandleObject); @@ -3052,11 +3032,11 @@ struct CloseRawHandleVisitor<'task, Platform: ShimPlatform> { } impl RawHandleVisitor for CloseRawHandleVisitor<'_, Platform> { - fn file(&self, file: FileObject) { + fn file(&self, file: FileObject) { self.task.close_file(file); } - fn registry_key(&self, key: RegistryKeyObject) { + fn registry_key(&self, key: RegistryKeyObject) { self.task.close_registry_key(key); } @@ -3183,27 +3163,3 @@ pub struct LoadedProgram { /// Handle used to wait for the loaded program to exit. pub process: Arc>, } - -fn default_fs( - litebox: &LiteBox, - in_mem: litebox::fs::in_mem::InMem, - tar_data: Cow<'static, [u8]>, -) -> WindowsFS -where - Platform: ShimPlatform, -{ - litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox::fs::overlay::Overlay::::new( - in_mem, - litebox::fs::tar_ro::TarRo::new(tar_data, allocators.next()), - allocators.next(), - ) - }) - .mount("/dev", litebox::fs::devices::Devices::new) - .build() - .unwrap(), - ) -} diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 779083de7..a33a9d1ce 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -10,12 +10,12 @@ use core::{ use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt as _; use litebox::{ - fs::{Mode, OFlags}, mm::linux::{ CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, VmemProtectError, }, platform::RawPointerProvider, }; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags}; use litebox_common_windows::loader::{ AccessMemory, Fault, KiUserInvertedFunctionTableEntry, KiUserInvertedFunctionTableHeader, MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, MapMemory, MappingInfo, PAGE_SIZE, PeExportError, @@ -102,14 +102,14 @@ pub(crate) struct WindowsThreadEnvironment { pub(crate) struct PeLoader<'a, Platform: crate::ShimPlatform> { platform: &'static Platform, - fs: Arc>, + fs: Arc>, page_manager: &'a crate::WindowsPageManager, } impl<'a, Platform: crate::ShimPlatform> PeLoader<'a, Platform> { pub(crate) fn new( platform: &'static Platform, - fs: Arc>, + fs: Arc>, page_manager: &'a crate::WindowsPageManager, ) -> Self { Self { @@ -139,13 +139,16 @@ impl<'a, Platform: crate::ShimPlatform> PeLoader<'a, Platform> { application_entry_point }; - let environment = self.create_process_environment(ProcessEnvironmentInput { - image: &image.parsed, - image_base_address: image.mapping.base_addr, - image_path: path, - argv, - envp, - })?; + let environment = create_process_environment( + self.page_manager, + ProcessEnvironmentInput { + image: &image.parsed, + image_base_address: image.mapping.base_addr, + image_path: path, + argv, + envp, + }, + )?; if let Some(ntdll) = &ntdll { let context = X64Context::initial_thread_context( ntdll.exports.rtl_user_thread_start, @@ -213,184 +216,180 @@ impl<'a, Platform: crate::ShimPlatform> PeLoader<'a, Platform> { Ok(()) } +} - fn create_process_environment( - &self, - input: ProcessEnvironmentInput<'_>, - ) -> Result { - let create_pages = |size: usize| -> Result { - let aligned_length = size.next_multiple_of(PAGE_SIZE); - let length = - NonZeroPageSize::new(aligned_length).ok_or(PeImageAccessError::AddressOverflow)?; - // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` leaves address - // selection to the page manager, so this cannot replace an existing mapping. - let ptr = unsafe { - self.page_manager.create_writable_pages( - None, - length, - CreatePagesFlags::empty(), - |_| Ok(0), - ) - }?; - Ok(ptr.as_usize()) - }; - let peb_ptr = create_pages(size_of::())?; - let api_set_map = API_SET_NAMESPACE; - let api_set_map_ptr = create_pages(api_set_map.len())?; - write_guest_slice::(api_set_map_ptr, api_set_map)?; - let win32_image_path = win32_image_path(input.image_path); - let dos_image_path = dos_image_path(input.image_path); - let current_directory_path = Utf16StringBuffer::new(r"C:\")?; - let dll_path = Utf16StringBuffer::new(r"C:\Windows\System32;C:\")?; - let image_path_name = Utf16StringBuffer::new(&dos_image_path)?; - let command_line = - Utf16StringBuffer::new(&windows_command_line(&win32_image_path, input.argv))?; - let window_title = Utf16StringBuffer::new(&dos_image_path)?; - let desktop_info = Utf16StringBuffer::new("")?; - let shell_info = Utf16StringBuffer::new("")?; - let runtime_data = Utf16StringBuffer::new("")?; - let redirection_dll_name = Utf16StringBuffer::new("")?; - let environment_block = windows_environment_block(input.envp); - let environment_size = checked_mul(environment_block.len(), size_of::())?; - let environment_ptr = create_pages(environment_size)?; - write_guest_slice::(environment_ptr, &environment_block)?; - let process_parameter_strings = [ - ¤t_directory_path, - &dll_path, - &image_path_name, - &command_line, - &window_title, - &desktop_info, - &shell_info, - &runtime_data, - &redirection_dll_name, - ]; - let process_parameters_length = process_parameter_strings.iter().try_fold( - size_of::(), - |length, string| { - length - .checked_add(usize::from(string.maximum_length)) - .ok_or(PeImageAccessError::AddressOverflow) - }, - )?; - let process_parameters_allocation_length = - process_parameters_length.next_multiple_of(PAGE_SIZE); - let process_parameters_ptr = create_pages(process_parameters_length)?; - - let mut process_parameters = RtlUserProcessParameters::new_zeroed(); - process_parameters.maximum_length = to_u32(process_parameters_allocation_length)?; - process_parameters.length = to_u32(process_parameters_length)?; - process_parameters.flags = RtlUserProcFlags::NORMALIZED.bits(); - process_parameters.environment = environment_ptr; - process_parameters.environment_size = - u64::try_from(environment_size).map_err(|_| PeImageAccessError::AddressOverflow)?; - let mut process_parameters_allocation = - GuestMemoryAllocator::new(process_parameters_ptr, process_parameters_length)?; - let guest_process_parameters = - process_parameters_allocation.allocate::()?; - process_parameters.current_directory.dos_path = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - ¤t_directory_path, - )?; - process_parameters.dll_path = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &dll_path, - )?; - process_parameters.image_path_name = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &image_path_name, - )?; - process_parameters.command_line = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &command_line, - )?; - process_parameters.window_title = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &window_title, - )?; - process_parameters.desktop_info = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &desktop_info, - )?; - process_parameters.shell_info = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &shell_info, - )?; - process_parameters.runtime_data = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &runtime_data, - )?; - process_parameters.redirection_dll_name = allocate_guest_unicode_string::( - &mut process_parameters_allocation, - &redirection_dll_name, - )?; - guest_process_parameters - .write_at_offset(0, process_parameters) - .ok_or(PeImageAccessError::MemoryAccess)?; - - let read_only_shared_memory_base = create_pages(WINDOWS_SHARED_SECTION_SIZE)?; - let mut shared_heap = - GuestMemoryAllocator::new(read_only_shared_memory_base, WINDOWS_SHARED_SECTION_SIZE)?; - let read_only_static_server_data = - initialize_windows_static_server_data::(&mut shared_heap)?; - let mut peb = ProcessEnvironmentBlock::new_zeroed(); - peb.image_base_address = input.image_base_address; - if input.image_base_address != input.image.image_base() || input.image.has_dynamic_base() { - peb.bit_field = PebBitField::IS_IMAGE_DYNAMICALLY_RELOCATED.bits(); - } - let process_heaps = initial_process_heaps_array(peb_ptr)?; - let fast_peb_lock = create_pages(size_of::())?; - write_guest_value::(fast_peb_lock, RtlCriticalSection::initialized(0))?; - let loader_lock = create_pages(size_of::())?; - write_guest_value::(loader_lock, RtlCriticalSection::initialized(0))?; - - peb.api_set_map = api_set_map_ptr; - peb.process_parameters = process_parameters_ptr; - peb.fast_peb_lock = fast_peb_lock; - peb.shared_data = read_only_shared_memory_base; - peb.number_of_processors = 1; - peb.critical_section_timeout = WINDOWS_CRITICAL_SECTION_TIMEOUT_100NS; - peb.heap_segment_reserve = WINDOWS_HEAP_SEGMENT_RESERVE; - peb.heap_segment_commit = WINDOWS_HEAP_SEGMENT_COMMIT; - peb.heap_de_commit_total_free_threshold = WINDOWS_HEAP_DECOMMIT_TOTAL_FREE_THRESHOLD; - peb.heap_de_commit_free_block_threshold = WINDOWS_HEAP_DECOMMIT_FREE_BLOCK_THRESHOLD; - peb.maximum_number_of_heaps = process_heaps.maximum_number_of_heaps; - peb.process_heaps = process_heaps.address; - peb.loader_lock = loader_lock; - peb.active_process_affinity_mask = 1; - peb.os_major_version = u32::from(crate::syscalls::sysinfo::WINDOWS_OS_MAJOR_VERSION); - peb.os_minor_version = u32::from(crate::syscalls::sysinfo::WINDOWS_OS_MINOR_VERSION); - peb.os_build_number = crate::syscalls::sysinfo::WINDOWS_OS_BUILD_NUMBER; - peb.os_platform_id = crate::syscalls::sysinfo::WINDOWS_OS_PLATFORM_WIN32_NT; - peb.image_subsystem = u32::from(input.image.subsystem()); - peb.image_subsystem_major_version = u32::from(input.image.major_subsystem_version()); - peb.image_subsystem_minor_version = u32::from(input.image.minor_subsystem_version()); - peb.read_only_shared_memory_base = read_only_shared_memory_base; - peb.read_only_static_server_data = read_only_static_server_data; - // TODO(csr-shared-section): model shared backing with distinct client and CSRSS - // virtual addresses instead of aliasing both PEB bases to this single mapping. - peb.csr_server_read_only_shared_memory_base = read_only_shared_memory_base as u64; - - write_guest_value::(peb_ptr, peb)?; - - let thread = create_thread_environment( - self.page_manager, - INITIAL_STACK_SIZE, - peb_ptr, - ClientId { - unique_process: INITIAL_PROCESS_ID, - unique_thread: INITIAL_THREAD_ID, - }, - true, - )?; - Ok(WindowsProcessEnvironment { - peb: peb_ptr, - teb: thread.teb, - context: thread.context, - stack_top: thread.stack_top, - windows_shared_section: read_only_shared_memory_base, - }) - } +/// Builds the synthetic Windows process environment (PEB, TEB, process parameters, and +/// initial thread state) for an image that has already been mapped. +/// +/// This only writes guest memory through `page_manager`, so it is independent of how the +/// image was obtained and needs no file system. +fn create_process_environment( + page_manager: &crate::WindowsPageManager, + input: ProcessEnvironmentInput<'_>, +) -> Result { + let create_pages = |size: usize| -> Result { + let aligned_length = size.next_multiple_of(PAGE_SIZE); + let length = + NonZeroPageSize::new(aligned_length).ok_or(PeImageAccessError::AddressOverflow)?; + // SAFETY: `suggested_address` is `None` and `CreatePagesFlags::empty()` leaves address + // selection to the page manager, so this cannot replace an existing mapping. + let ptr = unsafe { + page_manager.create_writable_pages(None, length, CreatePagesFlags::empty(), |_| Ok(0)) + }?; + Ok(ptr.as_usize()) + }; + let peb_ptr = create_pages(size_of::())?; + let api_set_map = API_SET_NAMESPACE; + let api_set_map_ptr = create_pages(api_set_map.len())?; + write_guest_slice::(api_set_map_ptr, api_set_map)?; + let win32_image_path = win32_image_path(input.image_path); + let dos_image_path = dos_image_path(input.image_path); + let current_directory_path = Utf16StringBuffer::new(r"C:\")?; + let dll_path = Utf16StringBuffer::new(r"C:\Windows\System32;C:\")?; + let image_path_name = Utf16StringBuffer::new(&dos_image_path)?; + let command_line = + Utf16StringBuffer::new(&windows_command_line(&win32_image_path, input.argv))?; + let window_title = Utf16StringBuffer::new(&dos_image_path)?; + let desktop_info = Utf16StringBuffer::new("")?; + let shell_info = Utf16StringBuffer::new("")?; + let runtime_data = Utf16StringBuffer::new("")?; + let redirection_dll_name = Utf16StringBuffer::new("")?; + let environment_block = windows_environment_block(input.envp); + let environment_size = checked_mul(environment_block.len(), size_of::())?; + let environment_ptr = create_pages(environment_size)?; + write_guest_slice::(environment_ptr, &environment_block)?; + let process_parameter_strings = [ + ¤t_directory_path, + &dll_path, + &image_path_name, + &command_line, + &window_title, + &desktop_info, + &shell_info, + &runtime_data, + &redirection_dll_name, + ]; + let process_parameters_length = process_parameter_strings.iter().try_fold( + size_of::(), + |length, string| { + length + .checked_add(usize::from(string.maximum_length)) + .ok_or(PeImageAccessError::AddressOverflow) + }, + )?; + let process_parameters_allocation_length = + process_parameters_length.next_multiple_of(PAGE_SIZE); + let process_parameters_ptr = create_pages(process_parameters_length)?; + + let mut process_parameters = RtlUserProcessParameters::new_zeroed(); + process_parameters.maximum_length = to_u32(process_parameters_allocation_length)?; + process_parameters.length = to_u32(process_parameters_length)?; + process_parameters.flags = RtlUserProcFlags::NORMALIZED.bits(); + process_parameters.environment = environment_ptr; + process_parameters.environment_size = + u64::try_from(environment_size).map_err(|_| PeImageAccessError::AddressOverflow)?; + let mut process_parameters_allocation = + GuestMemoryAllocator::new(process_parameters_ptr, process_parameters_length)?; + let guest_process_parameters = + process_parameters_allocation.allocate::()?; + process_parameters.current_directory.dos_path = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + ¤t_directory_path, + )?; + process_parameters.dll_path = + allocate_guest_unicode_string::(&mut process_parameters_allocation, &dll_path)?; + process_parameters.image_path_name = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &image_path_name, + )?; + process_parameters.command_line = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &command_line, + )?; + process_parameters.window_title = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &window_title, + )?; + process_parameters.desktop_info = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &desktop_info, + )?; + process_parameters.shell_info = + allocate_guest_unicode_string::(&mut process_parameters_allocation, &shell_info)?; + process_parameters.runtime_data = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &runtime_data, + )?; + process_parameters.redirection_dll_name = allocate_guest_unicode_string::( + &mut process_parameters_allocation, + &redirection_dll_name, + )?; + guest_process_parameters + .write_at_offset(0, process_parameters) + .ok_or(PeImageAccessError::MemoryAccess)?; + + let read_only_shared_memory_base = create_pages(WINDOWS_SHARED_SECTION_SIZE)?; + let mut shared_heap = + GuestMemoryAllocator::new(read_only_shared_memory_base, WINDOWS_SHARED_SECTION_SIZE)?; + let read_only_static_server_data = + initialize_windows_static_server_data::(&mut shared_heap)?; + let mut peb = ProcessEnvironmentBlock::new_zeroed(); + peb.image_base_address = input.image_base_address; + if input.image_base_address != input.image.image_base() || input.image.has_dynamic_base() { + peb.bit_field = PebBitField::IS_IMAGE_DYNAMICALLY_RELOCATED.bits(); + } + let process_heaps = initial_process_heaps_array(peb_ptr)?; + let fast_peb_lock = create_pages(size_of::())?; + write_guest_value::(fast_peb_lock, RtlCriticalSection::initialized(0))?; + let loader_lock = create_pages(size_of::())?; + write_guest_value::(loader_lock, RtlCriticalSection::initialized(0))?; + + peb.api_set_map = api_set_map_ptr; + peb.process_parameters = process_parameters_ptr; + peb.fast_peb_lock = fast_peb_lock; + peb.shared_data = read_only_shared_memory_base; + peb.number_of_processors = 1; + peb.critical_section_timeout = WINDOWS_CRITICAL_SECTION_TIMEOUT_100NS; + peb.heap_segment_reserve = WINDOWS_HEAP_SEGMENT_RESERVE; + peb.heap_segment_commit = WINDOWS_HEAP_SEGMENT_COMMIT; + peb.heap_de_commit_total_free_threshold = WINDOWS_HEAP_DECOMMIT_TOTAL_FREE_THRESHOLD; + peb.heap_de_commit_free_block_threshold = WINDOWS_HEAP_DECOMMIT_FREE_BLOCK_THRESHOLD; + peb.maximum_number_of_heaps = process_heaps.maximum_number_of_heaps; + peb.process_heaps = process_heaps.address; + peb.loader_lock = loader_lock; + peb.active_process_affinity_mask = 1; + peb.os_major_version = u32::from(crate::syscalls::sysinfo::WINDOWS_OS_MAJOR_VERSION); + peb.os_minor_version = u32::from(crate::syscalls::sysinfo::WINDOWS_OS_MINOR_VERSION); + peb.os_build_number = crate::syscalls::sysinfo::WINDOWS_OS_BUILD_NUMBER; + peb.os_platform_id = crate::syscalls::sysinfo::WINDOWS_OS_PLATFORM_WIN32_NT; + peb.image_subsystem = u32::from(input.image.subsystem()); + peb.image_subsystem_major_version = u32::from(input.image.major_subsystem_version()); + peb.image_subsystem_minor_version = u32::from(input.image.minor_subsystem_version()); + peb.read_only_shared_memory_base = read_only_shared_memory_base; + peb.read_only_static_server_data = read_only_static_server_data; + // TODO(csr-shared-section): model shared backing with distinct client and CSRSS + // virtual addresses instead of aliasing both PEB bases to this single mapping. + peb.csr_server_read_only_shared_memory_base = read_only_shared_memory_base as u64; + + write_guest_value::(peb_ptr, peb)?; + + let thread = create_thread_environment( + page_manager, + INITIAL_STACK_SIZE, + peb_ptr, + ClientId { + unique_process: INITIAL_PROCESS_ID, + unique_thread: INITIAL_THREAD_ID, + }, + true, + )?; + Ok(WindowsProcessEnvironment { + peb: peb_ptr, + teb: thread.teb, + context: thread.context, + stack_top: thread.stack_top, + windows_shared_section: read_only_shared_memory_base, + }) } pub(crate) fn create_thread_environment( @@ -912,7 +911,7 @@ struct NtDllExports { fn load_ntdll( platform: &'static Platform, - fs: Arc>, + fs: Arc>, page_manager: &crate::WindowsPageManager, ) -> Result, WindowsLoadError> { match load_image_with_writable_sections( @@ -937,7 +936,7 @@ fn load_ntdll( fn load_image( platform: &'static Platform, - fs: Arc>, + fs: Arc>, path: &str, page_manager: &crate::WindowsPageManager, ) -> Result { @@ -946,7 +945,7 @@ fn load_image( pub(crate) fn load_image_section( platform: &'static Platform, - fs: Arc>, + fs: Arc>, path: &str, page_manager: &crate::WindowsPageManager, virtual_allocations: &crate::WindowsVirtualAllocations, @@ -959,7 +958,7 @@ pub(crate) fn load_image_section( pub(crate) struct ImageSectionMetadata { pub(crate) transfer_address: usize, - pub(crate) file_size: u32, + pub(crate) file_size: u64, pub(crate) subsystem: u32, pub(crate) subsystem_major_version: u16, pub(crate) subsystem_minor_version: u16, @@ -969,18 +968,16 @@ pub(crate) struct ImageSectionMetadata { } pub(crate) fn image_section_metadata( - fs: Arc>, + fs: Arc>, path: &str, ) -> Result { let file = PeImageFile::open(fs, path)?; let parsed = PeParsedFile::parse(&mut &file).map_err(WindowsLoadError::Parse)?; let file_size = file .fs - .fd_file_status(&file.fd) + .file_status(&file.fd) .map_err(PeImageAccessError::FileStatus)? - .size - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow)?; + .size; Ok(ImageSectionMetadata { transfer_address: parsed .image_base() @@ -997,7 +994,7 @@ pub(crate) fn image_section_metadata( } fn load_image_with_writable_sections( - fs: Arc>, + fs: Arc>, path: &str, platform: &'static Platform, page_manager: &crate::WindowsPageManager, @@ -1108,16 +1105,17 @@ fn is_missing_file_error(error: &WindowsLoadError) -> bool { } struct PeImageFile { - fs: Arc>, - fd: litebox::fd::TypedFd>, + fs: Arc>, + fd: litebox::fs::FileFd, } impl PeImageFile { - fn open(fs: Arc>, path: &str) -> Result { - let fd = fs.open( - &litebox::fs::resolver::Context::new(), + fn open(fs: Arc>, path: &str) -> Result { + let fd = fs.open_file( + &litebox::fs::Context::new(), path, - OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), )?; Ok(Self { fs, fd }) @@ -1129,7 +1127,7 @@ impl PeImageFile { mut buf: &mut [u8], ) -> Result<(), PeImageAccessError> { while !buf.is_empty() { - let bytes_read = self.fs.read(&self.fd, buf, Some(offset))?; + let bytes_read = self.fs.read_file(&self.fd, buf, Some(offset))?; if bytes_read == 0 { return Err(PeImageAccessError::ShortRead); } @@ -1144,7 +1142,7 @@ impl PeImageFile { impl Drop for PeImageFile { fn drop(&mut self) { - if let Err(e) = self.fs.close(&self.fd) { + if let Err(e) = self.fs.close_file(&self.fd) { litebox_util_log::warn!(error:? = e; "failed to close PE image file"); } } @@ -1163,11 +1161,7 @@ impl ReadAt for &'_ PeImageFile { } fn size(&mut self) -> Result { - self.fs - .fd_file_status(&self.fd)? - .size - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow) + Ok(self.fs.file_status(&self.fd)?.size) } } @@ -2543,19 +2537,10 @@ mod tests { } fn created_process_environment_snapshot() -> CreatedProcessEnvironmentSnapshot { - let platform = crate::tests::test_platform(); - let litebox = litebox::LiteBox::new(platform); + // Process environment construction only writes guest memory, so this needs no files and + // uses the objectless broker association. + let litebox = crate::test_broker::litebox(crate::tests::test_platform()); let page_manager = crate::WindowsPageManager::::new(&litebox); - let fs = Arc::new(litebox::fs::resolver::Resolver::new( - &litebox, - litebox::fs::composer::Composer::builder() - .mount("/", |allocator| { - litebox::fs::in_mem::InMem::::new(allocator) - }) - .build() - .expect("valid test filesystem"), - )); - let loader = PeLoader::new(platform, fs, &page_manager); let image = loaded_module_image(application_module_base()); let image_base_address = image.mapping.base_addr; @@ -2569,15 +2554,17 @@ mod tests { CString::new("B=two").expect("valid envp[1]"), CString::new("a=one").expect("valid envp[2]"), ]; - let environment = loader - .create_process_environment(ProcessEnvironmentInput { + let environment = create_process_environment( + &page_manager, + ProcessEnvironmentInput { image: &image.parsed, image_base_address, image_path: "test.exe", argv: &argv, envp: &envp, - }) - .expect("failed to create synthetic Windows process environment"); + }, + ) + .expect("failed to create synthetic Windows process environment"); let peb = read_guest_value::(environment.peb); CreatedProcessEnvironmentSnapshot { diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 55e6c288c..bc300b061 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -4,18 +4,20 @@ use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; -use core::marker::PhantomData; use core::mem::{align_of, offset_of, size_of}; use int_enum::IntEnum; -use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, SeekError, WriteError, }; -use litebox::fs::{FileStatus, FileType, Mode, OFlags, SeekWhence}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileMode as Mode, FileOpenFlags, FileSeekWhence as SeekWhence, FileStatus, + FileType, +}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::byteorder::native_endian::U32; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; @@ -48,8 +50,8 @@ const FILE_SHARE_READ: u32 = 0x0000_0001; const FILE_SHARE_WRITE: u32 = 0x0000_0002; const FILE_SHARE_DELETE: u32 = 0x0000_0004; -// Bound guest-controlled file I/O allocations while keeping backend call overhead reasonable. -const FILE_IO_CHUNK_SIZE: usize = 0x80_000; +// Match the broker file payload limit so a successful chunk is not mistaken for a short transfer. +const FILE_IO_CHUNK_SIZE: usize = 0x10_000; /// Append at the current end of file const FILE_WRITE_TO_END_OF_FILE: i64 = -1; @@ -261,7 +263,7 @@ struct FileStatusMetadata { end_of_file: i64, allocation_size: i64, file_attributes: FileAttributes, - file_id: Option, + file_id: u64, file_id_128: [u8; 16], } @@ -281,11 +283,9 @@ impl FileStatusMetadata { .checked_next_multiple_of(status.blksize.max(1)) .and_then(|size| i64::try_from(size).ok()) .unwrap_or(i64::MAX); - let file_id = u64::try_from(status.node_info.ino).ok(); + let file_id = status.node_info.ino; let mut file_id_128 = [0; 16]; - if let Some(file_id) = file_id { - file_id_128[..size_of::()].copy_from_slice(&file_id.to_ne_bytes()); - } + file_id_128[..size_of::()].copy_from_slice(&file_id.to_ne_bytes()); Self { end_of_file, allocation_size, @@ -304,9 +304,7 @@ impl DirectoryEntry { end_of_file: metadata.end_of_file, allocation_size: metadata.allocation_size, file_attributes: metadata.file_attributes, - file_id: metadata - .file_id - .map_or(-1, |file_id| i64::from_ne_bytes(file_id.to_ne_bytes())), + file_id: i64::from_ne_bytes(metadata.file_id.to_ne_bytes()), } } } @@ -448,17 +446,13 @@ struct FileStandardInformation { padding: [u8; 2], } -pub(crate) struct FileObjectSubsystem(PhantomData); - -impl FdEnabledSubsystem for FileObjectSubsystem { - type Entry = FileObject; +impl FdEnabledSubsystem for FileObject { + type Entry = Self; } -impl FdEnabledSubsystemEntry for FileObject {} +impl FdEnabledSubsystemEntry for FileObject {} -impl crate::WindowsHandleSubsystem - for FileObjectSubsystem -{ +impl crate::WindowsHandleSubsystem for FileObject { fn normalize_desired_access(desired_access: u32) -> u32 { FileAccess::from_desired_access(desired_access).bits() } @@ -480,9 +474,9 @@ impl crate::WindowsHandleSubsystem } } -pub(crate) struct FileObject { +pub(crate) struct FileObject { path: String, - backing: FileObjectBacking, + backing: FileObjectBacking, create_time_access: FileAccess, share_access: FileShareAccess, create_options: FileCreateOptions, @@ -497,15 +491,15 @@ struct DirectoryQueryState { entries: Vec, } -enum FileObjectBacking { +enum FileObjectBacking { Filesystem { - fd: TypedFd>, + fd: litebox::fs::FileFd, is_directory: bool, }, CondrvStream { object: CondrvObject, stream_object: Arc, - fd: TypedFd>, + fd: litebox::fs::FileFd, }, CondrvControl(CondrvObject), /// A handle to `\Device\KsecDD`. @@ -529,7 +523,7 @@ enum FileIoOperation { /// with the resolved absolute byte offset (`None` when the current file-pointer /// position should be used). type PreparedFileIo = ( - litebox::fd::EntryHandle>, + litebox::fd::EntryHandle, Option, ); @@ -542,7 +536,7 @@ enum FileSharingIdentity<'a> { } impl FileSharingIdentity<'_> { - fn matches(self, file: &FileObject) -> bool { + fn matches(self, file: &FileObject) -> bool { match self { Self::Path(path) => file.condrv_stream_object_id().is_none() && file.path == path, Self::CondrvObject(object_id) => file.condrv_stream_object_id() == Some(object_id), @@ -550,7 +544,7 @@ impl FileSharingIdentity<'_> { } } -impl FileObject { +impl FileObject { fn condrv_object(&self) -> Option { match self.backing { FileObjectBacking::CondrvStream { object, .. } @@ -679,7 +673,7 @@ impl FileAccess { self, create_disposition: CreateDisposition, create_options: FileCreateOptions, - ) -> OFlags { + ) -> (FileAccessMode, FileOpenFlags) { let wants_read = self.intersects(Self::FS_READ_ACCESS); let wants_write = self.intersects(Self::FS_WRITE_ACCESS) || matches!( @@ -689,32 +683,36 @@ impl FileAccess { | CreateDisposition::OverwriteIf ); - let mut flags = match (wants_read, wants_write) { - (true, true) => OFlags::RDWR, - (false, true) => OFlags::WRONLY, - _ => OFlags::RDONLY, + let access = match (wants_read, wants_write) { + (true, true) => FileAccessMode::ReadWrite, + (false, true) => FileAccessMode::WriteOnly, + _ => FileAccessMode::ReadOnly, }; + // Append-only rights are enforced per NT handle by `prepare_file_io`. + let mut flags = FileOpenFlags::NONE; match create_disposition { CreateDisposition::Overwrite => { - flags.insert(OFlags::TRUNC); + flags = flags.union(FileOpenFlags::TRUNCATE); } CreateDisposition::Supersede | CreateDisposition::OverwriteIf => { - flags.insert(OFlags::CREAT | OFlags::TRUNC); + flags = flags.union(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE); + } + CreateDisposition::Create => { + flags = flags.union(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE); } - CreateDisposition::Create => flags.insert(OFlags::CREAT | OFlags::EXCL), - CreateDisposition::OpenIf => flags.insert(OFlags::CREAT), + CreateDisposition::OpenIf => flags = flags.union(FileOpenFlags::CREATE), CreateDisposition::Open => {} } if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { - flags.insert(OFlags::DIRECTORY); + flags = flags.union(FileOpenFlags::DIRECTORY); } if create_options.contains(FileCreateOptions::NON_DIRECTORY_FILE) { - flags.insert(OFlags::NOFOLLOW); + flags = flags.union(FileOpenFlags::NO_FOLLOW); } - flags + (access, flags) } fn conflicts_with_share(self, share_access: FileShareAccess) -> bool { @@ -823,8 +821,8 @@ impl Task { fn file_entry( &self, handle: Handle, - ) -> Result>, NtStatus> { - raw_handle_entry::>( + ) -> Result, NtStatus> { + raw_handle_entry::( &self.global.litebox, &self.process.handles, handle, @@ -836,33 +834,26 @@ impl Task { &self, handle: Handle, operation: FileIoOperation, - ) -> Result< - ( - litebox::fd::EntryHandle>, - bool, - ), - NtStatus, - > { + ) -> Result<(litebox::fd::EntryHandle, bool), NtStatus> { let mut append_only = false; - let file = self.typed_handle_entry_with_access_check::>( - handle, - |granted_access| match operation { - FileIoOperation::Read => granted_access & FileAccess::READ_DATA.bits() != 0, - FileIoOperation::Write => { - append_only = granted_access & FileAccess::WRITE_DATA.bits() == 0 - && granted_access & FileAccess::APPEND_DATA.bits() != 0; - granted_access & (FileAccess::WRITE_DATA | FileAccess::APPEND_DATA).bits() != 0 + let file = + self.typed_handle_entry_with_access_check::(handle, |granted_access| { + match operation { + FileIoOperation::Read => granted_access & FileAccess::READ_DATA.bits() != 0, + FileIoOperation::Write => { + append_only = granted_access & FileAccess::WRITE_DATA.bits() == 0 + && granted_access & FileAccess::APPEND_DATA.bits() != 0; + granted_access & (FileAccess::WRITE_DATA | FileAccess::APPEND_DATA).bits() + != 0 + } } - }, - )?; + })?; Ok((file, append_only)) } pub(crate) fn image_section_file_path(&self, handle: Handle) -> Result { - let entry = self.typed_handle_entry_with_access::>( - handle, - FileAccess::EXECUTE.bits(), - )?; + let entry = + self.typed_handle_entry_with_access::(handle, FileAccess::EXECUTE.bits())?; entry.with_entry(|file| match &file.backing { FileObjectBacking::Filesystem { is_directory: false, @@ -877,36 +868,36 @@ impl Task { }) } - fn insert_file_handle(&self, file: FileObject) -> Result { + fn insert_file_handle(&self, file: FileObject) -> Result { let granted_access = file.create_time_access.bits(); - self.insert_typed_handle::>(file, granted_access, |file| { + self.insert_typed_handle::(file, granted_access, |file| { self.close_file(file); }) } pub(crate) fn close_file_handle(&self, handle: Handle) { - self.close_typed_handle::>(handle, |file| { + self.close_typed_handle::(handle, |file| { self.close_file(file); }); } - pub(crate) fn close_file(&self, file: FileObject) { + pub(crate) fn close_file(&self, file: FileObject) { match file.backing { FileObjectBacking::Filesystem { fd, is_directory } => { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); if file .create_options .contains(FileCreateOptions::DELETE_ON_CLOSE) { if is_directory { - let _ = self.fs.rmdir(&self.fs_context, &file.path); + let _ = self.fs.rmdir_file(&self.fs_context, &file.path); } else { - let _ = self.fs.unlink(&self.fs_context, &file.path); + let _ = self.fs.unlink_file(&self.fs_context, &file.path); } } } FileObjectBacking::CondrvStream { fd, .. } => { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => {} } @@ -981,11 +972,11 @@ impl Task { } Err(status) => return status, }; - let status = match self.fs.file_status(&self.fs_context, &path) { + let status = match self.fs.path_file_status(&self.fs_context, &path) { Ok(status) => status, Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { let parent = parent_directory_path(&path); - return if self.fs.file_status(&self.fs_context, parent).is_ok() { + return if self.fs.path_file_status(&self.fs_context, parent).is_ok() { NtStatus::OBJECT_NAME_NOT_FOUND } else { NtStatus::OBJECT_PATH_NOT_FOUND @@ -993,23 +984,17 @@ impl Task { } Err(error) => return map_file_status_error(error), }; - let readonly = !status.mode.intersects(Mode::WUSR | Mode::WGRP | Mode::WOTH); - let mut file_attributes = match status.file_type { - FileType::Directory => FileAttributes::DIRECTORY, - FileType::RegularFile => FileAttributes::ARCHIVE, + if !matches!( + status.file_type, + FileType::Directory | FileType::RegularFile + ) { // TODO(chardev-attributes): Probe native attributes for character devices and // future filesystem node types; regular files are host-grounded as ARCHIVE. - file_type => { - litebox_util_log::debug!( - path = path.as_str(), - file_type:? = file_type; - "Using archive attributes for nonstandard filesystem node" - ); - FileAttributes::ARCHIVE - } - }; - if readonly { - file_attributes |= FileAttributes::READONLY; + litebox_util_log::debug!( + path = path.as_str(), + file_type:? = status.file_type; + "Using archive attributes for nonstandard filesystem node" + ); } // TODO(fs-timestamps): Populate timestamps when FileStatus exposes them. litebox_util_log::debug!( @@ -1017,7 +1002,9 @@ impl Task { "Using zero timestamps for file attributes" ); let information = FileBasicInformation { - file_attributes: file_attributes.bits(), + file_attributes: FileStatusMetadata::from_status(&status) + .file_attributes + .bits(), ..FileBasicInformation::default() }; if file_information.write_at_offset(0, information).is_none() { @@ -1083,11 +1070,11 @@ impl Task { } Err(status) => return status, }; - let status = match self.fs.file_status(&self.fs_context, &path) { + let status = match self.fs.path_file_status(&self.fs_context, &path) { Ok(status) => status, Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) => { let parent = parent_directory_path(&path); - return if self.fs.file_status(&self.fs_context, parent).is_ok() { + return if self.fs.path_file_status(&self.fs_context, parent).is_ok() { NtStatus::OBJECT_NAME_NOT_FOUND } else { NtStatus::OBJECT_PATH_NOT_FOUND @@ -1098,9 +1085,7 @@ impl Task { let metadata = FileStatusMetadata::from_status(&status); // TODO(fs-timestamps): Populate timestamps when FileStatus exposes them. let information = FileStatBasicInformation { - file_id: metadata - .file_id - .map_or(0, |file_id| i64::from_ne_bytes(file_id.to_ne_bytes())), + file_id: i64::from_ne_bytes(metadata.file_id.to_ne_bytes()), allocation_size: metadata.allocation_size, end_of_file: metadata.end_of_file, file_attributes: metadata.file_attributes.bits(), @@ -1181,7 +1166,7 @@ impl Task { let status = match file.with_entry(|file| match &file.backing { FileObjectBacking::Filesystem { fd, .. } | FileObjectBacking::CondrvStream { fd, .. } => { - self.fs.fd_file_status(fd).map_err(map_file_status_error) + self.fs.file_status(fd).map_err(map_file_status_error) } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { Err(NtStatus::INVALID_DEVICE_REQUEST) @@ -1228,7 +1213,7 @@ impl Task { { return NtStatus::ACCESS_VIOLATION; } - let file = match self.typed_handle_entry_with_access_check::>( + let file = match self.typed_handle_entry_with_access_check::( file_handle, |granted_access| { granted_access & (FileAccess::READ_DATA | FileAccess::WRITE_DATA).bits() != 0 @@ -1249,10 +1234,12 @@ impl Task { if *is_directory { return Err(NtStatus::INVALID_DEVICE_REQUEST); } - self.fs.seek(fd, 0, SeekWhence::RelativeToCurrentOffset) + self.fs + .seek_file(fd, 0, SeekWhence::RelativeToCurrentOffset) } FileObjectBacking::CondrvStream { fd, .. } => { - self.fs.seek(fd, 0, SeekWhence::RelativeToCurrentOffset) + self.fs + .seek_file(fd, 0, SeekWhence::RelativeToCurrentOffset) } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { return Err(NtStatus::INVALID_DEVICE_REQUEST); @@ -1318,7 +1305,7 @@ impl Task { if position < 0 { return NtStatus::INVALID_PARAMETER; } - let file = match self.typed_handle_entry_with_access_check::>( + let file = match self.typed_handle_entry_with_access_check::( file_handle, |granted_access| { granted_access & (FileAccess::READ_DATA | FileAccess::WRITE_DATA).bits() != 0 @@ -1339,10 +1326,12 @@ impl Task { if *is_directory { return Err(NtStatus::INVALID_DEVICE_REQUEST); } - self.fs.seek(fd, position, SeekWhence::RelativeToBeginning) + self.fs + .seek_file(fd, position, SeekWhence::RelativeToBeginning) } FileObjectBacking::CondrvStream { fd, .. } => { - self.fs.seek(fd, position, SeekWhence::RelativeToBeginning) + self.fs + .seek_file(fd, position, SeekWhence::RelativeToBeginning) } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { return Err(NtStatus::INVALID_DEVICE_REQUEST); @@ -1471,10 +1460,10 @@ impl Task { if *is_directory { return Err(WriteError::NotAFile); } - self.fs.write(fd, &bytes, chunk_offset) + self.fs.write_file(fd, &bytes, chunk_offset) } FileObjectBacking::CondrvStream { fd, .. } => { - self.fs.write(fd, &bytes, chunk_offset) + self.fs.write_file(fd, &bytes, chunk_offset) } FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { Err(WriteError::NotAFile) @@ -1501,7 +1490,7 @@ impl Task { .intersects(FileCreateOptions::SYNCHRONOUS_IO) && input_length != 0 { - let _ = self.fs.seek( + let _ = self.fs.seek_file( fd, (offset + total_written).cast_signed(), SeekWhence::RelativeToBeginning, @@ -1574,14 +1563,16 @@ impl Task { return Err(ReadError::NotAFile); } ( - self.fs.read(fd, &mut bytes[..chunk_length], chunk_offset), + self.fs + .read_file(fd, &mut bytes[..chunk_length], chunk_offset), true, ) } // TODO(condrv-large-read): Continue with per-operation nonblocking reads // after the first chunk once FileSystem can report WouldBlock. FileObjectBacking::CondrvStream { fd, .. } => ( - self.fs.read(fd, &mut bytes[..chunk_length], chunk_offset), + self.fs + .read_file(fd, &mut bytes[..chunk_length], chunk_offset), false, ), FileObjectBacking::CondrvControl(_) | FileObjectBacking::KsecDevice => { @@ -1610,7 +1601,7 @@ impl Task { .intersects(FileCreateOptions::SYNCHRONOUS_IO) && output_length != 0 { - let _ = self.fs.seek( + let _ = self.fs.seek_file( fd, (offset + total_read).cast_signed(), SeekWhence::RelativeToBeginning, @@ -1667,9 +1658,9 @@ impl Task { && operation == FileIoOperation::Write => { let status = file - .with_entry(|file| self.fs.file_status(&self.fs_context, &file.path)) + .with_entry(|file| self.fs.path_file_status(&self.fs_context, &file.path)) .map_err(map_file_status_error)?; - Some(status.size) + Some(usize::try_from(status.size).map_err(|_| NtStatus::INVALID_PARAMETER)?) } Some(FILE_USE_FILE_POINTER_POSITION) | None => None, Some(offset) if offset >= 0 => { @@ -1688,7 +1679,11 @@ impl Task { "Ignoring file I/O byte-range lock key; byte-range locking is not supported yet" ); } - if offset.is_some_and(|offset| offset.checked_add(length).is_none()) { + if offset.is_some_and(|offset| { + offset + .checked_add(length) + .is_none_or(|end| isize::try_from(end).is_err()) + }) { return Err(NtStatus::INVALID_PARAMETER); } if !event.is_null() { @@ -1808,7 +1803,7 @@ impl Task { return NtStatus::INFO_LENGTH_MISMATCH; } - let file = match self.typed_handle_entry_with_access::>( + let file = match self.typed_handle_entry_with_access::( file_handle, FileAccess::LIST_DIRECTORY.bits(), ) { @@ -1870,7 +1865,7 @@ impl Task { fn query_directory( &self, - file: &mut FileObject, + file: &mut FileObject, supplied_pattern: Option, information_class: FileInformationClass, flags: DirectoryQueryFlags, @@ -1959,10 +1954,7 @@ impl Task { Ok((NtStatus::SUCCESS, output)) } - fn read_directory_entries( - &self, - file: &FileObject, - ) -> Result, NtStatus> { + fn read_directory_entries(&self, file: &FileObject) -> Result, NtStatus> { let FileObjectBacking::Filesystem { fd, is_directory } = &file.backing else { return Err(NtStatus::INVALID_PARAMETER); }; @@ -1970,24 +1962,28 @@ impl Task { return Err(NtStatus::INVALID_PARAMETER); } - let current_status = self.fs.fd_file_status(fd).map_err(map_file_status_error)?; + let current_status = self.fs.file_status(fd).map_err(map_file_status_error)?; let parent_path = parent_directory_path(&file.path); let parent_status = self .fs - .file_status(&self.fs_context, parent_path) + .path_file_status(&self.fs_context, parent_path) .map_err(map_file_status_error)?; let mut entries = alloc::vec![ DirectoryEntry::from_status(String::from("."), ¤t_status), DirectoryEntry::from_status(String::from(".."), &parent_status), ]; - for entry in self.fs.read_dir(fd).map_err(map_read_dir_error)? { + for entry in self + .fs + .read_file_directory(fd) + .map_err(map_read_dir_error)? + { if entry.name == "." || entry.name == ".." { continue; } let path = child_path(&file.path, &entry.name); let status = self .fs - .file_status(&self.fs_context, path) + .path_file_status(&self.fs_context, path) .map_err(map_file_status_error)?; entries.push(DirectoryEntry::from_status(entry.name, &status)); } @@ -2194,7 +2190,7 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, file_attributes: u32, - ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { self.check_file_sharing( FileSharingIdentity::Path(&path), desired_access, @@ -2244,7 +2240,7 @@ impl Task { create_options: FileCreateOptions, ea_buffer: Option>, ea_length: u32, - ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { if object == CondrvObject::Connect { condrv::validate_connect_server_ea::(ea_buffer, ea_length)?; } else if ea_buffer.is_some() || ea_length != 0 { @@ -2316,7 +2312,7 @@ impl Task { desired_access: FileAccess, share_access: FileShareAccess, create_options: FileCreateOptions, - ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { return Err(NtStatus::NOT_A_DIRECTORY); } @@ -2340,37 +2336,30 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, mode: Mode, - ) -> Result< - ( - TypedFd>, - bool, - FileCreateInformation, - ), - NtStatus, - > { - let existed_before_open = self.fs.file_status(&self.fs_context, path).is_ok(); + ) -> Result<(litebox::fs::FileFd, bool, FileCreateInformation), NtStatus> { + let existed_before_open = self.fs.path_file_status(&self.fs_context, path).is_ok(); if create_disposition == CreateDisposition::Supersede && existed_before_open && !desired_access.contains(FileAccess::DELETE) { return Err(NtStatus::ACCESS_DENIED); } - let flags = desired_access.open_flags(create_disposition, create_options); + let (access, flags) = desired_access.open_flags(create_disposition, create_options); let fd = self .fs - .open(&self.fs_context, path, flags, mode) + .open_file(&self.fs_context, path, access, flags, mode) .map_err(|error| map_open_error(error, create_disposition))?; - let file_status = match self.fs.fd_file_status(&fd) { + let file_status = match self.fs.file_status(&fd) { Ok(file_status) => file_status, Err(error) => { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(map_file_status_error(error)); } }; if create_options.contains(FileCreateOptions::NON_DIRECTORY_FILE) && file_status.file_type == FileType::Directory { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(NtStatus::OBJECT_TYPE_MISMATCH); } let information = create_disposition.success_information(existed_before_open); @@ -2389,7 +2378,7 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, file_attributes: u32, - ) -> Result<(FileObject, FileCreateInformation), NtStatus> { + ) -> Result<(FileObject, FileCreateInformation), NtStatus> { if matches!( create_disposition, CreateDisposition::Supersede @@ -2399,7 +2388,7 @@ impl Task { return Err(NtStatus::INVALID_PARAMETER); } - let existed_before_open = match self.fs.file_status(&self.fs_context, path) { + let existed_before_open = match self.fs.path_file_status(&self.fs_context, path) { Ok(status) => { if status.file_type != FileType::Directory { return Err(NtStatus::NOT_A_DIRECTORY); @@ -2413,7 +2402,7 @@ impl Task { ) => { self.fs - .mkdir( + .mkdir_file( &self.fs_context, path, create_directory_mode(file_attributes), @@ -2429,10 +2418,10 @@ impl Task { } else { CreateDisposition::Open }; - let flags = desired_access.open_flags(open_disposition, create_options); + let (access, flags) = desired_access.open_flags(open_disposition, create_options); let fd = self .fs - .open(&self.fs_context, path, flags, Mode::empty()) + .open_file(&self.fs_context, path, access, flags, Mode::empty()) .map_err(|error| map_open_error(error, create_disposition))?; let information = create_disposition.success_information(existed_before_open); Ok(( @@ -2488,7 +2477,7 @@ impl Task { let Some(handle) = Handle::from_raw_fd(raw_handle) else { continue; }; - let Some(entry) = raw_handle_entry::>( + let Some(entry) = raw_handle_entry::( &self.global.litebox, &self.process.handles, handle, @@ -2823,15 +2812,16 @@ mod tests { fn create_existing_file(task: &Task, path: &str, data: &[u8]) { let fd = task .fs - .open( + .open_file( &task.fs_context, path, - OFlags::CREAT | OFlags::RDWR, + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, Mode::RUSR | Mode::WUSR, ) .unwrap(); - assert_eq!(task.fs.write(&fd, data, Some(0)).unwrap(), data.len()); - task.fs.close(&fd).unwrap(); + assert_eq!(task.fs.write_file(&fd, data, Some(0)).unwrap(), data.len()); + task.fs.close_file(&fd).unwrap(); } fn create_file( @@ -2900,7 +2890,7 @@ mod tests { fn ksecdd_requires_broker_and_rejects_unknown_controls() { run_with_test_platform_pointers(|| { const UNKNOWN_KSEC_IOCTL: u32 = 0x0039_0000; - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let handle = open_ksecdd(&task, FILE_GENERIC_READ | FILE_GENERIC_WRITE); let mut random = [0xa5; 32]; @@ -3181,10 +3171,10 @@ mod tests { #[test] fn nt_query_attributes_file_reports_file_type_attributes() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/query-attributes.txt", b"data"); task.fs - .mkdir( + .mkdir_file( &task.fs_context, "/tmp/query-attributes-dir", Mode::RUSR | Mode::WUSR | Mode::XUSR, @@ -3222,7 +3212,7 @@ mod tests { #[test] fn nt_duplicate_object_rejects_file_access_escalation() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/duplicate-read-only.txt", b"data"); let (status, source, _) = create_file( &task, @@ -3261,7 +3251,7 @@ mod tests { NtStatus::SUCCESS ); assert_eq!( - task.typed_handle::>(maximum_duplicate) + task.typed_handle::(maximum_duplicate) .and_then(|typed| { task.typed_handle_metadata(&typed) .map(|metadata| metadata.granted_access) @@ -3275,7 +3265,7 @@ mod tests { #[test] fn nt_write_file_forces_append_only_handles_to_end_of_file() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let path = "/tmp/append-only.txt"; create_existing_file(&task, path, b"data"); let (status, handle, _) = create_file( @@ -3308,12 +3298,18 @@ mod tests { let fd = task .fs - .open(&task.fs_context, path, OFlags::RDONLY, Mode::empty()) + .open_file( + &task.fs_context, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); let mut contents = [0; 5]; - assert_eq!(task.fs.read(&fd, &mut contents, Some(0)).unwrap(), 5); + assert_eq!(task.fs.read_file(&fd, &mut contents, Some(0)).unwrap(), 5); assert_eq!(&contents, b"data!"); - task.fs.close(&fd).unwrap(); + task.fs.close_file(&fd).unwrap(); }); } @@ -3336,13 +3332,13 @@ mod tests { #[test] fn nt_query_standard_information_uses_open_file_metadata() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let path = "/tmp/query-standard-open-file.txt"; create_existing_file(&task, path, b"original"); let (status, handle, _) = create_file(&task, path, FILE_GENERIC_READ, FILE_OPEN); assert_eq!(status, NtStatus::SUCCESS); - task.fs.unlink(&task.fs_context, path).unwrap(); + task.fs.unlink_file(&task.fs_context, path).unwrap(); create_existing_file(&task, path, b"replacement is longer"); let mut information = FileStandardInformation::default(); @@ -3366,7 +3362,7 @@ mod tests { #[test] fn nt_set_position_information_updates_synchronous_position() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let path = "/tmp/set-position-sync.txt"; create_existing_file(&task, path, b"0123456789"); let (status, handle, _) = create_file( @@ -3420,7 +3416,7 @@ mod tests { #[test] fn nt_set_position_information_rejects_duplicate_without_data_access() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let path = "/tmp/set-position-no-access.txt"; create_existing_file(&task, path, b"0123456789"); let (status, handle, _) = create_file( @@ -3441,7 +3437,7 @@ mod tests { #[test] fn nt_file_io_transfers_across_multiple_chunks() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (status, handle, _) = create_file( &task, "/tmp/chunked-file-io.txt", @@ -3497,7 +3493,7 @@ mod tests { #[test] fn nt_create_file_follows_condrv_connection_through_standard_streams() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let server_handle = open_condrv_server(&task); let reference_handle = open_condrv_reference(&task, server_handle); let (_connect_path, _connect_name, mut connect_attributes) = @@ -3754,7 +3750,7 @@ mod tests { #[test] fn nt_query_volume_information_file_returns_fs_device_information() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let handle = open_fs_root(&task); let mut io_status = IoStatusBlock::default(); let mut output = FileFsDeviceInformation { @@ -3791,9 +3787,9 @@ mod tests { #[test] fn nt_query_directory_file_ex_tracks_restart_single_and_no_cursor_flags() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); task.fs - .mkdir(&task.fs_context, "/tmp/query-cursor", Mode::RWXU) + .mkdir_file(&task.fs_context, "/tmp/query-cursor", Mode::RWXU) .unwrap(); create_existing_file(&task, "/tmp/query-cursor/alpha", b"a"); create_existing_file(&task, "/tmp/query-cursor/beta", b"b"); @@ -3882,7 +3878,7 @@ mod tests { #[test] fn nt_query_volume_information_file_leaves_iosb_untouched_on_failures() { run_with_test_platform_pointers(|| { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let handle = open_fs_root(&task); let sentinel = IoStatusBlock::new(NtStatus::from_raw(0x1111_1111), 0x2222_2222); let mut io_status = sentinel; @@ -3975,10 +3971,10 @@ mod tests { #[test] fn nt_open_file_opens_existing_absolute_and_relative_files() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/dir-file-root.txt", b"root"); task.fs - .mkdir( + .mkdir_file( &task.fs_context, "/tmp/dir", Mode::RUSR | Mode::WUSR | Mode::XUSR, @@ -4045,7 +4041,7 @@ mod tests { #[test] fn nt_create_file_reports_disposition_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/existing.txt", b"old"); let (status, handle, io_status) = @@ -4121,7 +4117,7 @@ mod tests { #[test] fn nt_create_file_reports_missing_and_collision_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/existing-collision.txt", b"old"); let (status, _handle, io_status) = @@ -4149,7 +4145,7 @@ mod tests { #[test] fn nt_create_file_rejects_invalid_share_access() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/invalid-share.txt", b"old"); let (_path, _name, attributes) = open_object_attributes("/tmp/invalid-share.txt"); let mut io_status = IoStatusBlock::default(); @@ -4173,7 +4169,7 @@ mod tests { #[test] fn nt_create_file_directory_handles_can_root_relative_opens() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (_path, _name, attributes) = open_object_attributes("/tmp/created-dir"); let mut io_status = IoStatusBlock::default(); let directory_handle = task @@ -4212,9 +4208,9 @@ mod tests { #[test] fn nt_create_file_actual_directory_handles_can_root_relative_opens() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); task.fs - .mkdir( + .mkdir_file( &task.fs_context, "/tmp/implicit-dir", Mode::RUSR | Mode::WUSR | Mode::XUSR, @@ -4334,19 +4330,18 @@ mod tests { ), Ok(()) ); - assert!( - generic_read - .open_flags( - CreateDisposition::Open, - FileCreateOptions::NON_DIRECTORY_FILE - ) - .contains(OFlags::NOFOLLOW) + assert_eq!( + generic_read.open_flags( + CreateDisposition::Open, + FileCreateOptions::NON_DIRECTORY_FILE + ), + (FileAccessMode::ReadOnly, FileOpenFlags::NO_FOLLOW) ); } #[test] fn nt_create_file_enforces_share_access() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/shared.txt", b"old"); let (_path, _name, attributes) = open_object_attributes("/tmp/shared.txt"); let mut io_status = IoStatusBlock::default(); @@ -4386,7 +4381,7 @@ mod tests { #[test] fn nt_close_releases_file_handle_and_share_lock() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/close-shared.txt", b"old"); let (_path, _name, attributes) = open_object_attributes("/tmp/close-shared.txt"); let mut io_status = IoStatusBlock::default(); @@ -4445,7 +4440,7 @@ mod tests { #[test] fn nt_close_deletes_delete_on_close_file() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/delete-on-close.txt", b"old"); let (_path, _name, attributes) = open_object_attributes("/tmp/delete-on-close.txt"); let mut io_status = IoStatusBlock::default(); @@ -4467,20 +4462,20 @@ mod tests { assert!( task.fs - .file_status(&task.fs_context, "/tmp/delete-on-close.txt") + .path_file_status(&task.fs_context, "/tmp/delete-on-close.txt") .is_ok() ); assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); assert!(matches!( task.fs - .file_status(&task.fs_context, "/tmp/delete-on-close.txt"), + .path_file_status(&task.fs_context, "/tmp/delete-on-close.txt"), Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) )); } #[test] fn nt_close_deletes_delete_on_close_directory() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (_path, _name, attributes) = open_object_attributes("/tmp/delete-on-close-dir"); let mut io_status = IoStatusBlock::default(); let handle = task @@ -4503,20 +4498,20 @@ mod tests { assert!( task.fs - .file_status(&task.fs_context, "/tmp/delete-on-close-dir") + .path_file_status(&task.fs_context, "/tmp/delete-on-close-dir") .is_ok() ); assert_eq!(task.sys_nt_close(handle), NtStatus::SUCCESS); assert!(matches!( task.fs - .file_status(&task.fs_context, "/tmp/delete-on-close-dir"), + .path_file_status(&task.fs_context, "/tmp/delete-on-close-dir"), Err(FileStatusError::PathError(PathError::NoSuchFileOrDirectory)) )); } #[test] fn write_file_result_clears_handle_output_when_iosb_write_fails() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (_path, _name, attributes) = open_object_attributes("/tmp/iosb-fault.txt"); let mut io_status = IoStatusBlock::default(); let created_handle = task @@ -4815,7 +4810,7 @@ mod tests { .contains(FileDeviceCharacteristics::IS_MOUNTED) ); - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let handle = open_fs_root(&task); let mut output = FileFsDeviceInformation { device_type: 0, @@ -5000,7 +4995,7 @@ mod tests { }; close_host_handle(host_handle); - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); create_existing_file(&task, "/tmp/existing.txt", b"litebox"); let (_path, _name, attributes) = open_object_attributes("/tmp/existing.txt"); let mut litebox_handle = Handle::default(); @@ -5116,7 +5111,7 @@ mod tests { }; close_host_handle(host_handle); - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let (_path, _name, attributes) = open_object_attributes("/tmp/supersede-created.txt"); let mut litebox_handle = Handle::default(); let mut litebox_io_status = IoStatusBlock::default(); diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs index c5338e047..bbe85afae 100644 --- a/litebox_shim_windows/src/syscalls/nls.rs +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -5,12 +5,11 @@ use alloc::format; use alloc::string::String; use alloc::vec::Vec; use core::mem::size_of; -use litebox::fd::TypedFd; use litebox::fs::errors::{FileStatusError, OpenError, PathError, ReadError}; -use litebox::fs::{FileType, Mode, OFlags}; use litebox::mm::linux::{CreatePagesFlags, MappingError, NonZeroPageSize}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags, FileType}; use litebox_common_windows::loader::PAGE_SIZE; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; @@ -455,8 +454,8 @@ struct MappedNlsSection { len: usize, } -struct NlsSectionFile { - fd: TypedFd>, +struct NlsSectionFile { + fd: litebox::fs::FileFd, len: usize, } @@ -733,12 +732,12 @@ impl Task { let alloc_len = match nls_section_alloc_len(section_len) { Ok(alloc_len) => alloc_len, Err(status) => { - let _ = self.fs.close(§ion_file.fd); + let _ = self.fs.close_file(§ion_file.fd); return Err(status); } }; let Some(page_len) = NonZeroPageSize::::new(alloc_len) else { - let _ = self.fs.close(§ion_file.fd); + let _ = self.fs.close_file(§ion_file.fd); return Err(NtStatus::INVALID_PARAMETER); }; @@ -759,7 +758,7 @@ impl Task { }, ) }; - let _ = self.fs.close(§ion_file.fd); + let _ = self.fs.close_file(§ion_file.fd); let mapping = mapping.map_err(|_| copy_status.unwrap_or(NtStatus::NO_MEMORY))?; Ok(MappedNlsSection { address: mapping.as_usize(), @@ -770,43 +769,45 @@ impl Task { fn open_nls_section_file( &self, request: NlsSectionRequest, - ) -> Result, NtStatus> { + ) -> Result { let path = nls_section_file_path(request.section_type, request.section_data)?; let fd = self .fs - .open( + .open_file( &self.fs_context, path.as_str(), - OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), ) .map_err(map_nls_open_error)?; - let status = match self.fs.fd_file_status(&fd) { + let status = match self.fs.file_status(&fd) { Ok(status) => status, Err(error) => { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(map_nls_file_status_error(error)); } }; if status.file_type != FileType::RegularFile { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(NtStatus::OBJECT_TYPE_MISMATCH); } if status.size == 0 { - let _ = self.fs.close(&fd); + let _ = self.fs.close_file(&fd); return Err(NtStatus::OBJECT_NAME_NOT_FOUND); } - Ok(NlsSectionFile { - fd, - len: status.size, - }) + let Ok(len) = usize::try_from(status.size) else { + let _ = self.fs.close_file(&fd); + return Err(NtStatus::SECTION_TOO_BIG); + }; + Ok(NlsSectionFile { fd, len }) } fn copy_nls_section_file( &self, - fd: &TypedFd>, + fd: &litebox::fs::FileFd, section_len: usize, output: MutPtr, ) -> Result { @@ -817,7 +818,7 @@ impl Task { let chunk_len = remaining.min(PAGE_SIZE); let read = self .fs - .read(fd, &mut chunk[..chunk_len], Some(offset)) + .read_file(fd, &mut chunk[..chunk_len], Some(offset)) .map_err(map_nls_read_error)?; if read == 0 { return Err(NtStatus::END_OF_FILE); @@ -1156,7 +1157,7 @@ mod tests { #[test] fn nt_get_nls_section_ptr_maps_file_backed_section() { let section_bytes = vec![1, 2, 3, 4, 5]; - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/c_1252.nls", section_bytes.as_slice(), )]); @@ -1202,7 +1203,7 @@ mod tests { #[test] fn nt_get_nls_section_ptr_matches_host_section_content() { let host_file_bytes = host_system32_file_bytes("c_1252.nls"); - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/c_1252.nls", host_file_bytes.as_slice(), )]); @@ -1254,7 +1255,7 @@ mod tests { #[test] fn nt_get_nls_section_ptr_rejects_invalid_arguments() { let bytes = [0xaa]; - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/c_437.nls", bytes.as_slice(), )]); @@ -1311,7 +1312,7 @@ mod tests { #[test] fn nt_initialize_nls_files_maps_locale_file() { let locale_bytes = vec![0x44; PAGE_SIZE + 1]; - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/locale.nls", locale_bytes.as_slice(), )]); @@ -1343,7 +1344,7 @@ mod tests { #[test] fn nt_initialize_nls_files_matches_host_outputs() { let host_file_bytes = host_system32_file_bytes("locale.nls"); - let task = crate::tests::test_task_with_nls_files(&[( + let task = crate::tests::test_task_with_broker_files(&[( "/Windows/System32/locale.nls", host_file_bytes.as_slice(), )]); diff --git a/litebox_shim_windows/src/syscalls/object_manager.rs b/litebox_shim_windows/src/syscalls/object_manager.rs index a8436c688..dc83dd9cb 100644 --- a/litebox_shim_windows/src/syscalls/object_manager.rs +++ b/litebox_shim_windows/src/syscalls/object_manager.rs @@ -1615,7 +1615,8 @@ mod tests { }; use crate::tests::{ TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, null_mut_ptr, object_attributes, - run_with_test_platform_pointers, test_task, unicode_string, utf16_units, + run_with_test_platform_pointers, test_task, test_task_with_broker_files, unicode_string, + utf16_units, }; const DIRECTORY_QUERY: u32 = 0x0000_0001; @@ -1984,7 +1985,7 @@ mod tests { #[test] fn open_section_rejects_empty_known_dlls_with_zeroed_output() { run_with_test_platform_pointers(|| { - let task = test_task(); + let task = test_task_with_broker_files(&[]); let known_dlls_units = utf16_units(r"\KnownDlls"); let known_dlls_name = unicode_string(&known_dlls_units); let known_dlls_attrs = object_attributes( diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index ee376cfb1..62d27814f 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Windows registry syscalls backed by a private file-system-shaped store (i.e., -//! an overlay file system with in-memory and tar backends). +//! Windows registry syscalls backed by LiteBox's broker-backed file APIs. //! //! Registry keys are represented as directories and values as files under each //! key's `.values` directory: @@ -24,12 +23,12 @@ //! This is only an implementation detail: syscall handlers must expose registry //! object semantics rather than file semantics. -use core::marker::PhantomData; use core::mem::{offset_of, size_of}; use alloc::collections::BTreeMap; use alloc::format; use alloc::string::String; +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; @@ -39,14 +38,14 @@ use litebox::event::{ Events, polling::{Pollee, TryOpError}, }; -use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry, TypedFd}; +use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, WriteError, }; -use litebox::fs::{FileType, Mode, OFlags}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::sync::Mutex; use litebox::utils::TruncateExt; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags, FileType}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -58,33 +57,31 @@ use crate::nt_types::{ read_unicode_string_at, }; -type RegistryFileSystem = - litebox::fs::resolver::Resolver; - -pub(crate) struct RegistryKeySubsystem(PhantomData); - -impl FdEnabledSubsystem for RegistryKeySubsystem { - type Entry = RegistryKeyObject; +impl FdEnabledSubsystem for RegistryKeyObject { + type Entry = Self; } -impl FdEnabledSubsystemEntry for RegistryKeyObject {} +impl FdEnabledSubsystemEntry for RegistryKeyObject {} -impl crate::WindowsHandleSubsystem - for RegistryKeySubsystem -{ +impl crate::WindowsHandleSubsystem for RegistryKeyObject { fn normalize_desired_access(desired_access: u32) -> u32 { RegistryKeyAccess::from_desired_access(desired_access).bits() } } -pub(crate) struct RegistryKeyObject { +pub(crate) struct RegistryKeyObject { path: String, - fd: TypedFd>, + fd: litebox::fs::FileFd, } pub(crate) struct RegistryStore { - fs: RegistryFileSystem, - fs_context: litebox::fs::resolver::Context, + fs: Arc>, + fs_context: litebox::fs::Context, + /// Whether the built-in keys and values have been written to [`Self::fs`]. + /// + /// The defaults are written on first use rather than at construction so that + /// building a shim issues no file requests. + defaults_seeded: Mutex, notification_state: Mutex, notification_pollee: Pollee, } @@ -389,19 +386,17 @@ impl RegistryKeyAccess { normalized }) } -} -impl From for OFlags { - fn from(desired_access: RegistryKeyAccess) -> Self { - let wants_read = desired_access.intersects(RegistryKeyAccess::FS_READ_ACCESS); - let wants_write = desired_access.intersects(RegistryKeyAccess::FS_WRITE_ACCESS); + fn open_flags(self) -> (FileAccessMode, FileOpenFlags) { + let wants_read = self.intersects(Self::FS_READ_ACCESS); + let wants_write = self.intersects(Self::FS_WRITE_ACCESS); let access = match (wants_read, wants_write) { - (true, true) => OFlags::RDWR, - (false, true) => OFlags::WRONLY, - _ => OFlags::RDONLY, + (true, true) => FileAccessMode::ReadWrite, + (false, true) => FileAccessMode::WriteOnly, + _ => FileAccessMode::ReadOnly, }; - access | OFlags::DIRECTORY + (access, FileOpenFlags::DIRECTORY) } } @@ -539,7 +534,7 @@ struct KeySummary { max_name_len: usize, values: usize, max_value_name_len: usize, - max_value_data_len: usize, + max_value_data_len: u64, } /// The `KEY_VALUE_BASIC_INFORMATION` structure defines a subset of the full @@ -602,210 +597,44 @@ struct RegistryValue { } impl RegistryStore { - pub(crate) fn new(litebox: &LiteBox) -> Self { - let in_mem = litebox::fs::in_mem::InMem::::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let fs = litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox::fs::overlay::Overlay::::new( - in_mem, - litebox::fs::tar_ro::TarRo::new( - // TODO: Replace with tar file provided by the user - litebox::fs::tar_ro::EMPTY_TAR_FILE.into(), - allocators.next(), - ), - allocators.next(), - ) - }) - .build() - .unwrap(), - ); - let fs_context = litebox::fs::resolver::Context::new(); - { - let fs = &fs; - for key in [ - DEFAULT_SESSION_MANAGER_KEY, - DEFAULT_SEGMENT_HEAP_KEY, - DEFAULT_IMAGE_FILE_EXECUTION_OPTIONS_KEY, - DEFAULT_WINSOCK_PARAMETERS_KEY, - DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, - DEFAULT_WINSOCK_IPV4_TCP_ENTRY_KEY, - DEFAULT_WINSOCK_IPV4_UDP_ENTRY_KEY, - DEFAULT_WINSOCK_IPV6_TCP_ENTRY_KEY, - DEFAULT_WINSOCK_IPV6_UDP_ENTRY_KEY, - DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - ] { - if let Err(status) = create_key_in_fs(fs, &fs_context, key) { - litebox_util_log::error!(key:% = key, status:? = status; "failed to initialize registry key"); - break; - } - } - for (name, value) in [ - ("ACP", DEFAULT_ACP_VALUE), - ("OEMCP", DEFAULT_OEMCP_VALUE), - ("MACCP", DEFAULT_MACCP_VALUE), - ] { - if let Err(status) = write_value_in_fs( - fs, - &fs_context, - DEFAULT_CODE_PAGE_KEY, - name, - RegistryValueType::Sz, - value, - ) { - litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize registry value"); - break; - } - } - - let mut winsock_values = vec![ - ( - DEFAULT_WINSOCK_PARAMETERS_KEY, - "WinSock_Registry_Version", - RegistryValueType::Sz, - utf16le_nul("2.0"), - ), - ( - DEFAULT_WINSOCK_PARAMETERS_KEY, - "Current_Protocol_Catalog", - RegistryValueType::Sz, - utf16le_nul("Protocol_Catalog9"), - ), - ( - DEFAULT_WINSOCK_PARAMETERS_KEY, - "Current_NameSpace_Catalog", - RegistryValueType::Sz, - utf16le_nul("NameSpace_Catalog5"), - ), - ( - DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, - "Num_Catalog_Entries64", - RegistryValueType::Dword, - WINSOCK_PROTOCOL_CATALOG_ENTRY_COUNT.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, - "Next_Catalog_Entry_ID", - RegistryValueType::Dword, - WINSOCK_NEXT_PROTOCOL_CATALOG_ENTRY_ID - .to_le_bytes() - .to_vec(), - ), - ( - DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, - "Serial_Access_Num", - RegistryValueType::Dword, - WINSOCK_INITIAL_CATALOG_SERIAL.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, - "Num_Catalog_Entries64", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_CATALOG_ENTRY_COUNT.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, - "Serial_Access_Num", - RegistryValueType::Dword, - WINSOCK_INITIAL_CATALOG_SERIAL.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "LibraryPath", - RegistryValueType::Sz, - utf16le_nul("%SystemRoot%\\System32\\mswsock.dll"), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "DisplayString", - RegistryValueType::Sz, - utf16le_nul("@%SystemRoot%\\system32\\wshtcpip.dll,-60103"), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "ProviderId", - RegistryValueType::Binary, - WINSOCK_NAMESPACE_PROVIDER_ID.to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "SupportedNameSpace", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_DNS.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "Enabled", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_PROVIDER_ENABLED.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "Version", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_PROVIDER_VERSION.to_le_bytes().to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "StoresServiceClassInfo", - RegistryValueType::Dword, - WINSOCK_NAMESPACE_STORES_SERVICE_CLASS_INFO - .to_le_bytes() - .to_vec(), - ), - ( - DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, - "ProviderInfo", - RegistryValueType::Binary, - Vec::new(), - ), - ]; - for protocol in &DEFAULT_WINSOCK_PROTOCOLS { - winsock_values.push(( - protocol.entry_key, - "PackedCatalogItem", - RegistryValueType::Binary, - default_winsock_protocol_catalog_item(protocol), - )); - winsock_values.push(( - protocol.entry_key, - "ProtocolName", - RegistryValueType::Sz, - utf16le_nul(protocol.protocol_name), - )); - } - for (key, name, value_type, value) in winsock_values { - if let Err(status) = - write_value_in_fs(fs, &fs_context, key, name, value_type, &value) - { - litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize Winsock registry value"); - break; - } - } - } + /// Creates a registry store over the guest's brokered file system. + /// + /// Construction performs no file operation: the built-in keys and values are + /// seeded lazily by [`Self::fs`] when the guest first uses the registry. + pub(crate) fn new(litebox: Arc>) -> Self { Self { - fs, - fs_context, + fs: litebox, + fs_context: litebox::fs::Context::new(), + defaults_seeded: Mutex::new(false), notification_state: Mutex::new(RegistryNotificationState::default()), notification_pollee: Pollee::new(), } } + /// Returns the backing store, seeding the built-in registry contents on first use. + /// + /// Seeding is attempted exactly once. As at startup, a failure is logged and + /// abandons the rest of the defaults rather than failing the operation that + /// triggered it, so a store that cannot be seeded still answers requests. + fn fs(&self) -> &LiteBox { + { + let mut seeded = self.defaults_seeded.lock(); + if !*seeded { + *seeded = true; + seed_defaults(&self.fs, &self.fs_context); + } + } + self.fs.as_ref() + } + fn open_key( &self, path: &str, desired_access: RegistryKeyAccess, - ) -> Result>, NtStatus> { - self.fs - .open(&self.fs_context, path, desired_access.into(), Mode::empty()) + ) -> Result { + let (access, flags) = desired_access.open_flags(); + self.fs() + .open_file(&self.fs_context, path, access, flags, Mode::empty()) .map_err(map_open_error) } @@ -816,34 +645,34 @@ impl RegistryStore { ) -> Result { let value_path = value_path(key_path, value_name)?; let status = self - .fs - .file_status(&self.fs_context, &*value_path) + .fs() + .path_file_status(&self.fs_context, &*value_path) .map_err(map_file_status_error)?; if status.file_type != FileType::RegularFile { return Err(NtStatus::OBJECT_TYPE_MISMATCH); } - if status.size < REGISTRY_VALUE_TYPE_SIZE { + if status.size < REGISTRY_VALUE_TYPE_SIZE as u64 { return Err(NtStatus::UNSUCCESSFUL); } + let size = usize::try_from(status.size).map_err(|_| NtStatus::NO_MEMORY)?; + let mut data = Vec::new(); + data.try_reserve_exact(size) + .map_err(|_| NtStatus::NO_MEMORY)?; + data.resize(size, 0); let fd = self - .fs - .open( + .fs() + .open_file( &self.fs_context, &*value_path, - OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), ) .map_err(map_open_error)?; - let mut data = vec![0; status.size]; - let read = self - .fs - .read(&fd, &mut data, Some(0)) - .map_err(map_read_error)?; - let _ = self.fs.close(&fd); - if read != data.len() { - return Err(NtStatus::UNSUCCESSFUL); - } + let result = read_exact_at(self.fs(), &fd, &mut data); + let _ = self.fs().close_file(&fd); + result?; let value_type = u32::from_le_bytes( data[..REGISTRY_VALUE_TYPE_SIZE] @@ -863,7 +692,7 @@ impl RegistryStore { value: &[u8], ) -> Result<(), NtStatus> { write_value_at_path( - &self.fs, + self.fs(), &self.fs_context, key_path, value_name, @@ -932,9 +761,13 @@ impl RegistryStore { self.notification_pollee.notify_observers(Events::IN); } - fn key_summary(&self, key: &RegistryKeyObject) -> Result { + fn key_summary(&self, key: &RegistryKeyObject) -> Result { let mut summary = KeySummary::default(); - for entry in self.fs.read_dir(&key.fd).map_err(map_read_dir_error)? { + for entry in self + .fs() + .read_file_directory(&key.fd) + .map_err(map_read_dir_error)? + { if entry.file_type == FileType::Directory && entry.name != "." && entry.name != ".." @@ -949,16 +782,20 @@ impl RegistryStore { let values_path = format!("{}/{}", key.path.trim_end_matches('/'), VALUES_DIR_NAME); let values_fd = self - .fs - .open( + .fs() + .open_file( &self.fs_context, &*values_path, - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; - let values = self.fs.read_dir(&values_fd).map_err(map_read_dir_error); - let _ = self.fs.close(&values_fd); + let values = self + .fs() + .read_file_directory(&values_fd) + .map_err(map_read_dir_error); + let _ = self.fs().close_file(&values_fd); for entry in values? { if entry.file_type != FileType::RegularFile { continue; @@ -969,16 +806,16 @@ impl RegistryStore { .max(entry.name.encode_utf16().count() * size_of::()); let path = format!("{values_path}/{}", entry.name); let size = self - .fs - .file_status(&self.fs_context, &*path) + .fs() + .path_file_status(&self.fs_context, &*path) .map_err(map_file_status_error)? .size; - if size < REGISTRY_VALUE_TYPE_SIZE { + if size < REGISTRY_VALUE_TYPE_SIZE as u64 { return Err(NtStatus::UNSUCCESSFUL); } summary.max_value_data_len = summary .max_value_data_len - .max(size - REGISTRY_VALUE_TYPE_SIZE); + .max(size - REGISTRY_VALUE_TYPE_SIZE as u64); } Ok(summary) } @@ -991,11 +828,15 @@ impl RegistryStore { /// subkey names are sorted before indexing. fn nth_subkey_name( &self, - key: &RegistryKeyObject, + key: &RegistryKeyObject, index: u32, ) -> Result, NtStatus> { let mut names = Vec::new(); - for entry in self.fs.read_dir(&key.fd).map_err(map_read_dir_error)? { + for entry in self + .fs() + .read_file_directory(&key.fd) + .map_err(map_read_dir_error)? + { if entry.file_type == FileType::Directory && entry.name != "." && entry.name != ".." @@ -1009,18 +850,15 @@ impl RegistryStore { } /// Computes a [`KeySummary`] for the named direct subkey of `key`. - fn subkey_summary( - &self, - key: &RegistryKeyObject, - name: &str, - ) -> Result { + fn subkey_summary(&self, key: &RegistryKeyObject, name: &str) -> Result { let child_path = format!("{}/{}", key.path.trim_end_matches('/'), name); let child_fd = self - .fs - .open( + .fs() + .open_file( &self.fs_context, &*child_path, - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; @@ -1029,7 +867,7 @@ impl RegistryStore { fd: child_fd, }; let summary = self.key_summary(&child); - let _ = self.fs.close(&child.fd); + let _ = self.fs().close_file(&child.fd); summary } @@ -1041,21 +879,25 @@ impl RegistryStore { /// files under the key's `.values` directory) are sorted before indexing. fn nth_value_name( &self, - key: &RegistryKeyObject, + key: &RegistryKeyObject, index: u32, ) -> Result, NtStatus> { let values_path = format!("{}/{}", key.path.trim_end_matches('/'), VALUES_DIR_NAME); let values_fd = self - .fs - .open( + .fs() + .open_file( &self.fs_context, &*values_path, - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; - let entries = self.fs.read_dir(&values_fd).map_err(map_read_dir_error); - let _ = self.fs.close(&values_fd); + let entries = self + .fs() + .read_file_directory(&values_fd) + .map_err(map_read_dir_error); + let _ = self.fs().close_file(&values_fd); let mut names = Vec::new(); for entry in entries? { if entry.file_type == FileType::RegularFile { @@ -1067,12 +909,181 @@ impl RegistryStore { } } +/// Writes the built-in registry keys and values into `fs`. +/// +/// Registry startup is best-effort: the first failure is logged and abandons the +/// remaining defaults in that group, matching the behavior guests saw when the +/// defaults were written during shim construction. +fn seed_defaults( + fs: &LiteBox, + fs_context: &litebox::fs::Context, +) { + for key in [ + DEFAULT_SESSION_MANAGER_KEY, + DEFAULT_SEGMENT_HEAP_KEY, + DEFAULT_IMAGE_FILE_EXECUTION_OPTIONS_KEY, + DEFAULT_WINSOCK_PARAMETERS_KEY, + DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, + DEFAULT_WINSOCK_IPV4_TCP_ENTRY_KEY, + DEFAULT_WINSOCK_IPV4_UDP_ENTRY_KEY, + DEFAULT_WINSOCK_IPV6_TCP_ENTRY_KEY, + DEFAULT_WINSOCK_IPV6_UDP_ENTRY_KEY, + DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + ] { + if let Err(status) = create_key_in_fs(fs, fs_context, key) { + litebox_util_log::error!(key:% = key, status:? = status; "failed to initialize registry key"); + break; + } + } + for (name, value) in [ + ("ACP", DEFAULT_ACP_VALUE), + ("OEMCP", DEFAULT_OEMCP_VALUE), + ("MACCP", DEFAULT_MACCP_VALUE), + ] { + if let Err(status) = write_value_in_fs( + fs, + fs_context, + DEFAULT_CODE_PAGE_KEY, + name, + RegistryValueType::Sz, + value, + ) { + litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize registry value"); + break; + } + } + + let mut winsock_values = vec![ + ( + DEFAULT_WINSOCK_PARAMETERS_KEY, + "WinSock_Registry_Version", + RegistryValueType::Sz, + utf16le_nul("2.0"), + ), + ( + DEFAULT_WINSOCK_PARAMETERS_KEY, + "Current_Protocol_Catalog", + RegistryValueType::Sz, + utf16le_nul("Protocol_Catalog9"), + ), + ( + DEFAULT_WINSOCK_PARAMETERS_KEY, + "Current_NameSpace_Catalog", + RegistryValueType::Sz, + utf16le_nul("NameSpace_Catalog5"), + ), + ( + DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, + "Num_Catalog_Entries64", + RegistryValueType::Dword, + WINSOCK_PROTOCOL_CATALOG_ENTRY_COUNT.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, + "Next_Catalog_Entry_ID", + RegistryValueType::Dword, + WINSOCK_NEXT_PROTOCOL_CATALOG_ENTRY_ID + .to_le_bytes() + .to_vec(), + ), + ( + DEFAULT_WINSOCK_PROTOCOL_CATALOG_KEY, + "Serial_Access_Num", + RegistryValueType::Dword, + WINSOCK_INITIAL_CATALOG_SERIAL.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, + "Num_Catalog_Entries64", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_CATALOG_ENTRY_COUNT.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_CATALOG_KEY, + "Serial_Access_Num", + RegistryValueType::Dword, + WINSOCK_INITIAL_CATALOG_SERIAL.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "LibraryPath", + RegistryValueType::Sz, + utf16le_nul("%SystemRoot%\\System32\\mswsock.dll"), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "DisplayString", + RegistryValueType::Sz, + utf16le_nul("@%SystemRoot%\\system32\\wshtcpip.dll,-60103"), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "ProviderId", + RegistryValueType::Binary, + WINSOCK_NAMESPACE_PROVIDER_ID.to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "SupportedNameSpace", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_DNS.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "Enabled", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_PROVIDER_ENABLED.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "Version", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_PROVIDER_VERSION.to_le_bytes().to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "StoresServiceClassInfo", + RegistryValueType::Dword, + WINSOCK_NAMESPACE_STORES_SERVICE_CLASS_INFO + .to_le_bytes() + .to_vec(), + ), + ( + DEFAULT_WINSOCK_NAMESPACE_ENTRY_KEY, + "ProviderInfo", + RegistryValueType::Binary, + Vec::new(), + ), + ]; + for protocol in &DEFAULT_WINSOCK_PROTOCOLS { + winsock_values.push(( + protocol.entry_key, + "PackedCatalogItem", + RegistryValueType::Binary, + default_winsock_protocol_catalog_item(protocol), + )); + winsock_values.push(( + protocol.entry_key, + "ProtocolName", + RegistryValueType::Sz, + utf16le_nul(protocol.protocol_name), + )); + } + for (key, name, value_type, value) in winsock_values { + if let Err(status) = write_value_in_fs(fs, fs_context, key, name, value_type, &value) { + litebox_util_log::error!(name:% = name, status:? = status; "failed to initialize Winsock registry value"); + break; + } + } +} + impl Task { fn registry_key_entry( &self, handle: Handle, - ) -> Result>, NtStatus> { - raw_handle_entry::>( + ) -> Result, NtStatus> { + raw_handle_entry::( &self.global.litebox, &self.process.handles, handle, @@ -1082,26 +1093,22 @@ impl Task { fn insert_registry_key_handle( &self, - key: RegistryKeyObject, + key: RegistryKeyObject, granted_access: RegistryKeyAccess, ) -> Result { - self.insert_typed_handle::>( - key, - granted_access.bits(), - |key| { - self.close_registry_key(key); - }, - ) + self.insert_typed_handle::(key, granted_access.bits(), |key| { + self.close_registry_key(key); + }) } pub(crate) fn close_registry_key_handle(&self, handle: Handle) { - self.close_typed_handle::>(handle, |key| { + self.close_typed_handle::(handle, |key| { self.close_registry_key(key); }); } - pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { - let _ = self.global.registry.fs.close(&key.fd); + pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { + let _ = self.global.registry.fs().close_file(&key.fd); } pub(crate) fn sys_nt_open_key( @@ -1239,8 +1246,8 @@ impl Task { let disposition = match self .global .registry - .fs - .file_status(&self.global.registry.fs_context, &path) + .fs() + .path_file_status(&self.global.registry.fs_context, &path) { Ok(status) if status.file_type == FileType::Directory => { RegistryKeyDisposition::OpenedExistingKey @@ -1250,7 +1257,7 @@ impl Task { PathError::NoSuchFileOrDirectory | PathError::MissingComponent, )) => { for created_path in create_key_path_in_fs( - &self.global.registry.fs, + self.global.registry.fs(), &self.global.registry.fs_context, &path, )? { @@ -1392,7 +1399,7 @@ impl Task { length: u32, result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = self.typed_handle_entry_with_access::>( + let key = self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::QUERY_VALUE.bits(), )?; @@ -1438,7 +1445,7 @@ impl Task { data: Option>, data_size: u32, ) -> NtStatus { - let key = match self.typed_handle_entry_with_access::>( + let key = match self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::SET_VALUE.bits(), ) { @@ -1484,7 +1491,7 @@ impl Task { &self, params: NtNotifyChangeKeyRequest, ) -> NtStatus { - let key = match self.typed_handle_entry_with_access::>( + let key = match self.typed_handle_entry_with_access::( params.key_handle, RegistryKeyAccess::NOTIFY.bits(), ) { @@ -1650,9 +1657,9 @@ impl Task { ) -> Result<(), NtStatus> { let key = if key_information_class == KeyInformationClass::Name { // Windows permits KeyNameInformation with any nonzero granted access. - self.typed_handle_entry_with_any_access::>(key_handle)? + self.typed_handle_entry_with_any_access::(key_handle)? } else { - self.typed_handle_entry_with_access::>( + self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::QUERY_VALUE.bits(), )? @@ -1727,7 +1734,8 @@ impl Task { max_class_len: 0, values: summary.values.trunc(), max_value_name_len: summary.max_value_name_len.trunc(), - max_value_data_len: summary.max_value_data_len.trunc(), + max_value_data_len: u32::try_from(summary.max_value_data_len) + .map_err(|_| NtStatus::UNSUCCESSFUL)?, class: [], }; write_query_information::( @@ -1775,7 +1783,8 @@ impl Task { max_name_len: summary.max_name_len.trunc(), values: summary.values.trunc(), max_value_name_len: summary.max_value_name_len.trunc(), - max_value_data_len: summary.max_value_data_len.trunc(), + max_value_data_len: u32::try_from(summary.max_value_data_len) + .map_err(|_| NtStatus::UNSUCCESSFUL)?, name_length: leaf_name.len().trunc(), padding: [0; 4], }; @@ -1871,7 +1880,7 @@ impl Task { length: u32, result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = self.typed_handle_entry_with_access::>( + let key = self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::ENUMERATE_SUB_KEYS.bits(), )?; @@ -1945,7 +1954,8 @@ impl Task { max_class_len: 0, values: summary.values.trunc(), max_value_name_len: summary.max_value_name_len.trunc(), - max_value_data_len: summary.max_value_data_len.trunc(), + max_value_data_len: u32::try_from(summary.max_value_data_len) + .map_err(|_| NtStatus::UNSUCCESSFUL)?, class: [], }; write_query_information::( @@ -1980,7 +1990,7 @@ impl Task { length: u32, result_length: MutPtr, ) -> Result<(), NtStatus> { - let key = self.typed_handle_entry_with_access::>( + let key = self.typed_handle_entry_with_access::( key_handle, RegistryKeyAccess::QUERY_VALUE.bits(), )?; @@ -2234,8 +2244,8 @@ fn is_valid_key_component(component: &str) -> bool { } fn write_value_in_fs( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &LiteBox, + context: &litebox::fs::Context, key_nt_path: &str, value_name: &str, value_type: RegistryValueType, @@ -2246,8 +2256,8 @@ fn write_value_in_fs( } fn write_value_at_path( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &LiteBox, + context: &litebox::fs::Context, key_path: &str, value_name: &str, value_type: u32, @@ -2255,32 +2265,63 @@ fn write_value_at_path( ) -> Result<(), NtStatus> { let value_path = value_path(key_path, value_name)?; let fd = fs - .open( + .open_file( context, &*value_path, - OFlags::CREAT | OFlags::WRONLY | OFlags::TRUNC, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE, Mode::RUSR | Mode::WUSR | Mode::ROTH | Mode::WOTH, ) .map_err(map_open_error)?; - let written = fs - .write(&fd, &value_type.to_le_bytes(), Some(0)) - .map_err(map_write_error)?; - if written != REGISTRY_VALUE_TYPE_SIZE { - return Err(NtStatus::DISK_FULL); - } - let written = fs - .write(&fd, value, Some(REGISTRY_VALUE_TYPE_SIZE)) - .map_err(map_write_error)?; - if written != value.len() { - return Err(NtStatus::DISK_FULL); - } - let _ = fs.close(&fd); + let result = (|| { + write_all_at(fs, &fd, &value_type.to_le_bytes(), 0)?; + write_all_at(fs, &fd, value, REGISTRY_VALUE_TYPE_SIZE) + })(); + let _ = fs.close_file(&fd); + result +} + +fn read_exact_at( + fs: &LiteBox, + fd: &litebox::fs::FileFd, + mut data: &mut [u8], +) -> Result<(), NtStatus> { + let mut offset = 0; + while !data.is_empty() { + let read = fs + .read_file(fd, data, Some(offset)) + .map_err(map_read_error)?; + if read == 0 { + return Err(NtStatus::UNSUCCESSFUL); + } + offset = offset.checked_add(read).ok_or(NtStatus::UNSUCCESSFUL)?; + data = &mut data[read..]; + } + Ok(()) +} + +fn write_all_at( + fs: &LiteBox, + fd: &litebox::fs::FileFd, + mut data: &[u8], + mut offset: usize, +) -> Result<(), NtStatus> { + while !data.is_empty() { + let written = fs + .write_file(fd, data, Some(offset)) + .map_err(map_write_error)?; + if written == 0 { + return Err(NtStatus::DISK_FULL); + } + offset = offset.checked_add(written).ok_or(NtStatus::DISK_FULL)?; + data = &data[written..]; + } Ok(()) } fn create_key_in_fs( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &LiteBox, + context: &litebox::fs::Context, nt_path: &str, ) -> Result { let path = absolute_nt_key_name_to_fs_path(nt_path)?; @@ -2289,8 +2330,8 @@ fn create_key_in_fs( } fn create_key_path_in_fs( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &LiteBox, + context: &litebox::fs::Context, path: &str, ) -> Result, NtStatus> { let mut current = String::new(); @@ -2314,16 +2355,16 @@ fn create_key_path_in_fs( } fn ensure_directory_in_fs( - fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + fs: &LiteBox, + context: &litebox::fs::Context, path: &str, ) -> Result { - match fs.file_status(context, path) { + match fs.path_file_status(context, path) { Ok(status) if status.file_type == FileType::Directory => Ok(false), Ok(_) => Err(NtStatus::OBJECT_TYPE_MISMATCH), Err(FileStatusError::PathError( PathError::NoSuchFileOrDirectory | PathError::MissingComponent, - )) => match fs.mkdir( + )) => match fs.mkdir_file( context, path, Mode::RUSR | Mode::WUSR | Mode::XUSR | Mode::ROTH | Mode::WOTH | Mode::XOTH, @@ -2431,6 +2472,7 @@ mod tests { use super::*; use core::mem::size_of; use litebox::LiteBox; + use litebox_broker_protocol::fs::{FileMode, FileUser}; extern crate std; @@ -2492,9 +2534,32 @@ mod tests { fn RegDeleteTreeW(hKey: *mut core::ffi::c_void, lpSubKey: *const u16) -> i32; } - fn test_registry() -> (LiteBox, RegistryStore) { - let litebox = LiteBox::new(test_platform()); - let registry = RegistryStore::new(&litebox); + /// Returns a registry store backed by a broker core that owns an empty registry hive. + /// + /// The store's defaults are written on first use, so callers observe them through any + /// registry operation, exactly as a guest does. + fn test_registry() -> (Arc>, RegistryStore) { + let mode = FileMode::RWXU | FileMode::RWXG | FileMode::RWXO; + let litebox = Arc::new(crate::test_broker::litebox_with_broker_files( + test_platform(), + alloc::vec![ + ( + "/".into(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode, + owner: FileUser::ROOT, + }, + ), + ( + "/registry".into(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode, + owner: FileUser::ROOT, + }, + ), + ], + )); + let registry = RegistryStore::new(Arc::clone(&litebox)); (litebox, registry) } @@ -2665,30 +2730,39 @@ mod tests { #[test] fn registry_store_separates_values_from_subkeys() { - let (_litebox, registry) = test_registry(); + let (litebox, registry) = test_registry(); let key_path = absolute_nt_key_name_to_fs_path(DEFAULT_CODE_PAGE_KEY).unwrap(); let value_path = value_path(&key_path, "ACP").unwrap(); assert_eq!( registry - .fs - .file_status(®istry.fs_context, &*value_path) + .fs() + .path_file_status(®istry.fs_context, &*value_path) .unwrap() .file_type, FileType::RegularFile ); assert_eq!( registry - .fs - .file_status(®istry.fs_context, &*value_path) + .fs() + .path_file_status(®istry.fs_context, &*value_path) .unwrap() .size, - REGISTRY_VALUE_TYPE_SIZE + DEFAULT_ACP_VALUE.len() + (REGISTRY_VALUE_TYPE_SIZE + DEFAULT_ACP_VALUE.len()) as u64 ); let value = registry.read_value_at_path(&key_path, "ACP").unwrap(); assert_eq!(value.value_type, u32::from(RegistryValueType::Sz)); assert_eq!(value.data, DEFAULT_ACP_VALUE); + let broker_fs = litebox; + assert_eq!( + broker_fs + .path_file_status(&litebox::fs::Context::new(), value_path.as_str()) + .unwrap() + .file_type, + FileType::RegularFile + ); + let values_dir = absolute_nt_key_name_to_fs_path( "\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nls\\CodePage\\.values", ); @@ -2697,7 +2771,7 @@ mod tests { #[test] fn nt_create_key_reports_disposition_and_created_key_is_queryable() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_name = r"\Registry\Machine\Software\LiteBoxCreatedKey"; let key_name_utf16 = utf16(key_name); let key_name = unicode_string(&key_name_utf16); @@ -2802,7 +2876,7 @@ mod tests { #[test] fn nt_enumerate_key_lists_subkeys_in_stable_sorted_order() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let create_key = |path: &str| -> Handle { let name_utf16 = utf16(path); @@ -2896,7 +2970,7 @@ mod tests { #[test] fn nt_set_value_key_replaces_and_round_trips_raw_types_and_empty_data() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_name_utf16 = utf16(r"\Registry\Machine\Software\LiteBoxSetValueKey"); let key_name = unicode_string(&key_name_utf16); let object_attributes = object_attributes(&key_name, 0); @@ -2995,7 +3069,7 @@ mod tests { #[cfg(all(target_os = "windows", target_arch = "x86_64"))] #[test] fn registry_default_code_page_values_match_host() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); for name in ["ACP", "OEMCP", "MACCP"] { @@ -3030,7 +3104,7 @@ mod tests { #[test] fn nt_open_key_opens_existing_absolute_and_relative_keys() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let nls_name = utf16("\\Registry\\Machine\\System\\CurrentControlSet\\Control\\Nls"); let nls_name = unicode_string(&nls_name); let nls_object_attributes = object_attributes(&nls_name, 0); @@ -3048,7 +3122,7 @@ mod tests { #[test] fn nt_open_key_reports_missing_absolute_key() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let name = utf16("\\Registry\\Machine\\Software\\Missing"); let name = unicode_string(&name); let object_attributes = object_attributes(&name, 0); @@ -3062,7 +3136,7 @@ mod tests { fn synchronous_nt_notify_change_key_completes_after_matching_mutation() { use std::time::Duration; - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_name_utf16 = utf16(r"\Registry\Machine\Software\LiteBoxSynchronousNotify"); let key_name = unicode_string(&key_name_utf16); let object_attributes = object_attributes(&key_name, 0); @@ -3115,7 +3189,7 @@ mod tests { fn synchronous_nt_notify_change_key_completes_after_subkey_creation() { use std::time::Duration; - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_name_utf16 = utf16(r"\Registry\Machine\Software\LiteBoxSynchronousNameNotify"); let key_name = unicode_string(&key_name_utf16); let parent_attributes = object_attributes(&key_name, 0); @@ -3183,18 +3257,18 @@ mod tests { #[test] fn nt_open_key_checks_backing_fs_permissions() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let private_key = "\\Registry\\Machine\\Software\\Private"; let private_path = create_key_in_fs( - &task.global.registry.fs, + task.global.registry.fs(), &task.global.registry.fs_context, private_key, ) .unwrap(); task.global .registry - .fs - .chmod( + .fs() + .chmod_file( &task.global.registry.fs_context, &*private_path, Mode::WUSR | Mode::XUSR, @@ -3220,7 +3294,7 @@ mod tests { #[test] fn nt_close_removes_registry_key_handle() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let value_name = utf16("ACP"); let value_name = unicode_string(&value_name); @@ -3256,7 +3330,7 @@ mod tests { #[test] fn nt_query_value_key_reports_partial_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let value_name = utf16("ACP"); let value_name = unicode_string(&value_name); @@ -3293,7 +3367,7 @@ mod tests { fn nt_query_value_key_without_query_access_matches_host() { assert_eq!(host_query_value_with_set_only_access(), ERROR_ACCESS_DENIED); - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let code_page_name = utf16(DEFAULT_CODE_PAGE_KEY); let code_page_name = unicode_string(&code_page_name); let object_attributes = object_attributes(&code_page_name, 0); @@ -3321,7 +3395,7 @@ mod tests { #[test] fn nt_query_value_key_reports_basic_and_full_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let value_name = utf16("OEMCP"); let value_name = unicode_string(&value_name); @@ -3381,7 +3455,7 @@ mod tests { #[test] fn nt_enumerate_value_key_lists_values_in_stable_sorted_order() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); // The code-page key is seeded with ACP, OEMCP, and MACCP values, which // are stored lower-cased and therefore enumerate as acp, maccp, oemcp. let key_handle = open_code_page_key(&task); @@ -3461,7 +3535,7 @@ mod tests { #[test] fn nt_query_value_key_rejects_invalid_arguments() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let value_name = utf16("ACP"); let value_name = unicode_string(&value_name); @@ -3547,7 +3621,7 @@ mod tests { #[test] fn nt_query_key_reports_full_and_cached_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let mut full_bytes = [0u8; 64]; diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index cfba9d4c7..88be54b7e 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -73,7 +73,7 @@ pub(crate) struct SectionHandleObject { pub(crate) struct SectionObject { fs_path: Option, - size: usize, + size: u64, attributes: SectionAllocationAttributes, protection: PageProtection, backing: SectionBacking, @@ -315,7 +315,7 @@ impl Task { }; let section = Arc::new(SectionObject { fs_path: None, - size, + size: size as u64, attributes, protection, backing: SectionBacking::Pagefile, @@ -361,7 +361,7 @@ impl Task { }; let section = Arc::new(SectionObject { fs_path: Some(fs_path), - size: metadata.file_size as usize, + size: metadata.file_size, attributes: SectionAllocationAttributes::SEC_FILE | SectionAllocationAttributes::SEC_IMAGE, protection: PageProtection::PAGE_EXECUTE_WRITECOPY, @@ -471,7 +471,7 @@ impl Task { fs_path:% = fs_path; "NtOpenSection: creating section for KnownDlls image" ); - let Ok(file_status) = self.fs.file_status(&self.fs_context, &fs_path) else { + let Ok(file_status) = self.fs.path_file_status(&self.fs_context, &fs_path) else { return NtStatus::OBJECT_NAME_NOT_FOUND; }; let section = Arc::new(SectionObject { @@ -723,16 +723,16 @@ impl Task { page_protection: PageProtection, permissions: MemoryRegionPermissions, ) -> Result { - if section_offset > section.size { - return Err(NtStatus::INVALID_VIEW_SIZE); - } - let remaining = section.size - section_offset; + let remaining = section + .size + .checked_sub(section_offset as u64) + .ok_or(NtStatus::INVALID_VIEW_SIZE)?; let view_size = if requested_view_size == 0 { - remaining + usize::try_from(remaining).map_err(|_| NtStatus::INVALID_VIEW_SIZE)? } else { requested_view_size }; - if view_size == 0 || view_size > remaining { + if view_size == 0 || view_size as u64 > remaining { return Err(NtStatus::INVALID_VIEW_SIZE); } let mapped_size = view_size @@ -837,11 +837,14 @@ impl Task { return NtStatus::INVALID_VIEW_SIZE; } let view_size = if requested_view_size == 0 { - section.size + let Ok(size) = usize::try_from(section.size) else { + return NtStatus::INVALID_VIEW_SIZE; + }; + size } else { requested_view_size }; - if view_size == 0 || view_size > section.size { + if view_size == 0 || view_size as u64 > section.size { return NtStatus::INVALID_VIEW_SIZE; } let Some(mapped_size) = view_size.checked_next_multiple_of(PAGE_SIZE) else { @@ -1044,7 +1047,7 @@ pub(crate) fn load_time_windows_shared_section( // pointers instead of exposing a zeroed generic pagefile section. Arc::new(SectionObject { fs_path: None, - size: WINDOWS_SHARED_SECTION_SIZE, + size: WINDOWS_SHARED_SECTION_SIZE as u64, attributes: SectionAllocationAttributes::SEC_COMMIT, protection: PageProtection::PAGE_READWRITE, backing: SectionBacking::CsrSharedSection { base }, @@ -1181,7 +1184,7 @@ fn write_section_basic_information( fn write_section_image_information( section: &SectionObject, - fs: Arc>, + fs: Arc>, section_information: MutPtr, section_information_length: usize, return_length: Option>, @@ -1201,6 +1204,9 @@ fn write_section_image_information( Err(crate::loader::WindowsLoadError::Access(_)) => return NtStatus::OBJECT_NAME_NOT_FOUND, Err(_) => return NtStatus::INVALID_FILE_FOR_SECTION, }; + let Ok(image_file_size) = u32::try_from(metadata.file_size) else { + return NtStatus::SECTION_TOO_BIG; + }; // Host ntdll reports ReturnLength=64 for SectionImageInformation on x64; the public // winternl.h layout ends at CheckSum and has no trailing extension fields. let info = SectionImageInformation { @@ -1219,7 +1225,7 @@ fn write_section_image_information( image_contains_code: 1, image_flags: 0, loader_flags: 0, - image_file_size: metadata.file_size, + image_file_size, checksum: 0, }; let output = @@ -1269,7 +1275,9 @@ mod tests { use super::*; use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::syscalls::event::EventType; - use crate::tests::{TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task}; + use crate::tests::{ + TestPlatform, const_ptr, mut_byte_ptr, mut_ptr, test_task, test_task_with_broker_files, + }; #[cfg(all(target_os = "windows", target_arch = "x86_64"))] const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664; @@ -1484,8 +1492,10 @@ mod tests { #[test] fn nt_query_section_image_information_uses_pe_headers() { let image = host_kernel32_image(); - let task = - crate::tests::test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); + let task = crate::tests::test_task_with_broker_files(&[( + "/Windows/System32/kernel32.dll", + &image, + )]); let name = wide(r"\KnownDlls\kernel32.dll"); let unicode = unicode(&name); let attrs = object_attributes(&unicode); @@ -1540,7 +1550,7 @@ mod tests { #[test] fn nt_create_section_maps_file_backed_image() { let image = host_kernel32_image(); - let task = crate::tests::test_task_with_nls_files(&[("/tmp/kernel32.dll", &image)]); + let task = crate::tests::test_task_with_broker_files(&[("/tmp/kernel32.dll", &image)]); let file_handle = open_image_file(&task, r"\Device\HarddiskVolume1\tmp\kernel32.dll"); let mut section_handle = Handle::default(); @@ -1605,8 +1615,10 @@ mod tests { #[test] fn image_section_rejects_writable_view_protection() { let image = host_kernel32_image(); - let task = - crate::tests::test_task_with_nls_files(&[("/Windows/System32/kernel32.dll", &image)]); + let task = crate::tests::test_task_with_broker_files(&[( + "/Windows/System32/kernel32.dll", + &image, + )]); let name = wide(r"\KnownDlls\kernel32.dll"); let unicode = unicode(&name); let attrs = object_attributes(&unicode); @@ -1664,7 +1676,7 @@ mod tests { #[test] fn section_output_handles_follow_host_probe_contracts() { - let task = test_task(); + let task = test_task_with_broker_files(&[]); let name = wide(r"\KnownDlls\DefinitelyMissingLiteBoxProbe.dll"); let unicode = unicode(&name); let attrs = object_attributes(&unicode); diff --git a/litebox_shim_windows/src/test_broker.rs b/litebox_shim_windows/src/test_broker.rs new file mode 100644 index 000000000..8f8c6932c --- /dev/null +++ b/litebox_shim_windows/src/test_broker.rs @@ -0,0 +1,310 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Broker endpoints for the Windows shim's unit tests. +//! +//! The shim owns the guest side of the guest/broker boundary: NT syscall argument validation, +//! object and handle bookkeeping, path and flag translation, and error translation. Broker +//! authority — policy and filesystem semantics — belongs to `litebox_broker_core` and is tested +//! there. +//! +//! Ordinary shim tests therefore use [`litebox`], whose association negotiates the protocol and +//! owns shared memory but serves no objects: any request it receives is a bug in the test or in +//! the shim, and panics. The few tests that genuinely exercise file-backed behavior (registry +//! persistence and defaults, NLS section mapping, file syscalls, and file-backed sections) use +//! [`litebox_with_broker_files`], which owns a broker core for the test process. + +extern crate std; + +use alloc::{boxed::Box, string::String, sync::Arc, vec, vec::Vec}; + +use litebox_broker_core::{ + BrokerCore, ObjectRights, PolicyEngine, + fs::{in_mem::InitialNode, resolver::Resolver}, + random::{RandomProvider, RandomProviderError}, + readiness::ReadinessSink, + socket::UnsupportedSocketProvider, + stdio::UnsupportedStdioProvider, +}; +use litebox_broker_host::{BrokerHostAssociation, BrokerHostError}; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::{ + BROKER_PROTOCOL_VERSION, ObjectHandle, + message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, + BrokerResponse, + }, + readiness::ReadinessFlags, + shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}, +}; +use litebox_broker_transport::{ + channel::{HostReceive, HostSetupChannel, LocalCallChannel, LocalSetupChannel, PeerCredential}, + shared_memory::{SharedBufferPool, SharedMemory, SharedMemoryError}, +}; + +use crate::tests::TestPlatform; + +/// Returns a LiteBox whose broker association serves no objects. +/// +/// The association negotiates the protocol and owns real shared memory, so the local side of the +/// boundary behaves normally, but every request panics. This keeps tests that are not about +/// broker-backed resources honest about what they exercise. +pub(crate) fn litebox(platform: &'static TestPlatform) -> litebox::LiteBox { + let channel = ObjectlessChannel { + memory: Arc::new(TestSharedMemory::new()), + }; + let (broker_local, ()) = BrokerLocal::negotiate(channel, |channel| { + let memory: Arc = channel.memory.clone(); + Ok((channel, memory, ())) + }) + .expect("the objectless broker fixture must negotiate"); + litebox::LiteBox::new_with_broker_local(platform, broker_local) +} + +/// The local end of an association that owns shared memory but no objects. +struct ObjectlessChannel { + memory: Arc, +} + +impl LocalSetupChannel for ObjectlessChannel { + type Error = core::convert::Infallible; + + fn send_handshake_request( + &mut self, + request: &BrokerHandshakeRequest, + ) -> core::result::Result<(), Self::Error> { + assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); + Ok(()) + } + + fn recv_handshake_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + })) + } +} + +impl LocalCallChannel for ObjectlessChannel { + type Error = core::convert::Infallible; + + fn call(&self, request: BrokerRequest) -> core::result::Result { + match request.operation { + BrokerOperation::File(request) => panic!( + "this task's broker serves no files; tests that need them must build their task \ + with `crate::tests::test_task_with_broker_files`: {request:?}" + ), + operation => panic!("this task's broker serves no objects: {operation:?}"), + } + } +} + +/// Returns a LiteBox associated with a broker core that serves `entries` from memory. +/// +/// # Panics +/// +/// Panics if a broker core already exists in this process. The broker core is a process +/// singleton, so at most one file-backed task may be built per test binary invocation; +/// `cargo nextest`, the supported runner, gives each test its own process. +pub(crate) fn litebox_with_broker_files( + platform: &'static TestPlatform, + entries: Vec<(String, InitialNode)>, +) -> litebox::LiteBox { + let in_mem = litebox_broker_core::fs::in_mem::InMem::::new_initialized(entries); + let fs = litebox_broker_core::fs::composer::Composer::builder() + .mount("/", |_| in_mem) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .unwrap(); + let broker = BrokerCore::new( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), + Arc::new(UnsupportedSocketProvider), + Arc::new(UnusedRandomProvider), + Arc::new(UnsupportedStdioProvider), + Arc::new(Resolver::::new(fs)), + ) + .expect("a test process may build only one broker core"); + + let setup = TestBrokerSetup::new(broker); + let (broker_local, ()) = BrokerLocal::negotiate(setup, |setup| { + let memory: Arc = setup.memory.clone(); + Ok((setup.activate(), memory, ())) + }) + .unwrap(); + litebox::LiteBox::new_with_broker_local(platform, broker_local) +} + +struct TestBrokerSetup { + broker: BrokerCore, + memory: Arc, +} + +impl TestBrokerSetup { + fn new(broker: BrokerCore) -> Self { + Self { + broker, + memory: Arc::new(TestSharedMemory::new()), + } + } + + fn activate(self) -> TestBrokerChannel { + let shared_buffers = Box::leak(Box::new( + SharedBufferPool::new(self.memory, SHARED_BUFFER_LAYOUT).unwrap(), + )); + let mut setup = TestHostSetup; + let association = litebox_broker_host::setup_connection( + &self.broker, + &mut setup, + shared_buffers, + Arc::new(TestReadinessSink), + |_| Ok(()), + ) + .unwrap() + .unwrap(); + TestBrokerChannel { association } + } +} + +impl LocalSetupChannel for TestBrokerSetup { + type Error = BrokerHostError; + + 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, + })) + } +} + +struct TestBrokerChannel { + association: BrokerHostAssociation<'static, Arc>, +} + +impl LocalCallChannel for TestBrokerChannel { + type Error = BrokerHostError; + + fn call(&self, request: BrokerRequest) -> core::result::Result { + let mut response = None; + self.association.execute_request(request, |value| { + response = Some(value.clone()); + Ok(()) + })?; + Ok(response.expect("broker host must publish one response")) + } +} + +struct TestHostSetup; + +impl HostSetupChannel for TestHostSetup { + type Error = core::convert::Infallible; + + fn peer_credential(&self) -> core::result::Result { + Ok(PeerCredential::Unauthenticated) + } + + fn recv_handshake_request( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + })) + } + + fn send_handshake_response( + &mut self, + response: &BrokerHandshakeResponse, + ) -> core::result::Result<(), Self::Error> { + assert!(matches!( + response, + BrokerHandshakeResponse::Negotiated { .. } + )); + Ok(()) + } +} + +struct TestReadinessSink; + +impl ReadinessSink for TestReadinessSink { + fn max_tracked_objects(&self) -> usize { + usize::MAX + } + + fn publish( + &self, + _handle: ObjectHandle, + _readiness: ReadinessFlags, + ) -> litebox_broker_core::Result<()> { + Ok(()) + } + + fn republish( + &self, + _handle: ObjectHandle, + _readiness: ReadinessFlags, + ) -> litebox_broker_core::Result<()> { + Ok(()) + } + + fn retire(&self, _handle: ObjectHandle) {} +} + +struct UnusedRandomProvider; + +impl RandomProvider for UnusedRandomProvider { + fn fill(&self, _output: &mut [u8]) -> core::result::Result<(), RandomProviderError> { + Err(RandomProviderError) + } +} + +/// Shared memory backed by an ordinary allocation, since no peer process observes it. +struct TestSharedMemory(std::sync::Mutex>); + +impl TestSharedMemory { + fn new() -> Self { + Self(std::sync::Mutex::new(vec![0; SHARED_BUFFER_POOL_SIZE])) + } +} + +impl SharedMemory for TestSharedMemory { + fn len(&self) -> usize { + SHARED_BUFFER_POOL_SIZE + } + + 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(()) + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 987915a1a..acb4a756f 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -1,14 +1,22 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +//! Shared fixtures and cross-cutting unit tests for the Windows shim. +//! +//! [`test_task`] builds a task over a broker association that serves no objects, so the shim's +//! unit tests exercise guest and shim code rather than broker authority. Only tests that are +//! genuinely about file-backed behavior use [`test_task_with_broker_files`]; see +//! [`crate::test_broker`]. + extern crate std; use alloc::sync::Arc; use alloc::vec::Vec; use core::mem::size_of; -use litebox::fs::{Mode, OFlags}; use litebox::platform::RawConstPointer as _; use litebox::utils::TruncateExt as _; +use litebox_broker_core::fs::in_mem::InitialNode; +use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::syscalls::Handle; @@ -106,74 +114,67 @@ fn map_csr_server_shared_memory( } pub(crate) fn test_task() -> Task { - test_task_with_nls_files(&[]) + test_task_from_litebox(crate::test_broker::litebox(test_platform())) } -pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task { - let platform = test_platform(); - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let shim_builder = crate::WindowsShimBuilder::::new(platform); - let fs = Arc::new(shim_builder.default_fs(in_mem, litebox::fs::tar_ro::EMPTY_TAR_FILE.into())); - let fs_context = litebox::fs::resolver::Context::new(); - { - let fs = &*fs; - fs.mkdir( - &fs_context, - "/tmp", - litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - ) - .expect("/tmp creation cannot fail on a fresh in-memory file system"); - fs.chown(&fs_context, "/tmp", Some(1000), Some(1000)) - .expect("/tmp chown cannot fail on a fresh in-memory file system"); - - if !nls_files.is_empty() { - fs.mkdir( - &fs_context, - "/Windows", - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("/Windows creation cannot fail on a fresh in-memory file system"); - fs.mkdir( - &fs_context, - "/Windows/System32", - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("/Windows/System32 creation cannot fail on a fresh in-memory file system"); - fs.mkdir( - &fs_context, - "/Windows/Globalization", - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("/Windows/Globalization creation cannot fail on a fresh in-memory file system"); - fs.mkdir( - &fs_context, - "/Windows/Globalization/Sorting", - Mode::RWXU | Mode::RWXG | Mode::RWXO, - ) - .expect("/Windows/Globalization/Sorting creation cannot fail on a fresh in-memory file system"); - } - for (path, bytes) in nls_files { - let fd = fs - .open( - &fs_context, - *path, - OFlags::WRONLY | OFlags::CREAT, - Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::ROTH, - ) - .expect("NLS fixture creation should succeed"); - fs.write(&fd, bytes, Some(0)) - .expect("NLS fixture write should succeed"); - fs.close(&fd).expect("NLS fixture close should succeed"); - } +/// Returns a task whose broker serves `files` from an in-memory filesystem. +/// +/// Reserved for tests that genuinely exercise file-backed behavior. Every other test must use +/// [`test_task`], whose broker serves no files at all. The broker core is a process singleton, so +/// exactly one such task may be built per test process; `cargo nextest` runs each test in its own +/// process. +pub(crate) fn test_task_with_broker_files(files: &[(&str, &[u8])]) -> Task { + let directory = |owner| InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner, + }; + let mut entries = alloc::vec![ + ("/".into(), directory(UserInfo::ROOT)), + ( + "/tmp".into(), + directory(UserInfo { + user: 1000, + group: 1000, + }), + ), + ("/registry".into(), directory(UserInfo::ROOT)), + ]; + if !files.is_empty() { + entries.extend([ + ("/Windows".into(), directory(UserInfo::ROOT)), + ("/Windows/System32".into(), directory(UserInfo::ROOT)), + ("/Windows/Globalization".into(), directory(UserInfo::ROOT)), + ( + "/Windows/Globalization/Sorting".into(), + directory(UserInfo::ROOT), + ), + ]); } + entries.extend(files.iter().map(|(path, bytes)| { + ( + (*path).into(), + InitialNode::File { + mode: Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::ROTH, + owner: UserInfo::ROOT, + data: (*bytes).to_vec().into(), + }, + ) + })); + + test_task_from_litebox(crate::test_broker::litebox_with_broker_files( + test_platform(), + entries, + )) +} + +fn test_task_from_litebox(litebox: litebox::LiteBox) -> Task { + let platform = test_platform(); + let shim_builder = + crate::WindowsShimBuilder::::new_with_litebox(platform, litebox); + let fs_context = litebox::fs::Context::new(); let shim = shim_builder.build(); let WindowsShim(global) = shim; + let fs = Arc::clone(&global.litebox); let windows_shared_section_base = map_csr_server_shared_memory(&global.page_manager) .expect("mapping shared memory should succeed");