From c989ab2d563b0f07638d83e690a09893352f1022 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 8 Sep 2026 14:40:00 -0700 Subject: [PATCH 01/34] Migrate filesystem clients to broker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- Cargo.lock | 27 +- dev_tests/src/ratchet.rs | 6 +- litebox/Cargo.toml | 1 + litebox/src/broker/mod.rs | 254 +++++ litebox/src/fs/mod.rs | 9 +- litebox/src/fs/nine_p/tests.rs | 79 +- litebox/src/fs/resolver.rs | 891 +++++++++--------- litebox/src/fs/tests.rs | 49 +- litebox/src/lib.rs | 2 + litebox/src/test_broker.rs | 228 +++++ litebox_broker_protocol/src/fs.rs | 1 - litebox_broker_userland/Cargo.toml | 6 + litebox_broker_userland/src/builder.rs | 12 +- litebox_broker_userland/src/linux.rs | 255 ++++- litebox_broker_userland/src/main.rs | 17 + litebox_broker_userland/src/sync.rs | 103 ++ litebox_broker_userland/src/windows.rs | 79 ++ .../tests/userland_broker.rs | 9 +- litebox_common_linux/src/errno/mod.rs | 2 + .../Cargo.toml | 5 + .../src/lib.rs | 58 +- .../tests/common/broker.rs | 218 +++++ .../tests/common/mod.rs | 75 +- litebox_runner_linux_userland/Cargo.toml | 2 +- litebox_runner_linux_userland/src/lib.rs | 234 +---- .../tests/common/broker.rs | 218 +++++ litebox_runner_linux_userland/tests/loader.rs | 160 ++-- litebox_runner_linux_userland/tests/run.rs | 199 +++- litebox_runner_snp/src/main.rs | 118 +-- .../Cargo.toml | 1 + .../src/lib.rs | 58 +- litebox_runner_windows_userland/Cargo.toml | 2 +- litebox_runner_windows_userland/src/lib.rs | 77 +- litebox_shim_linux/Cargo.toml | 1 + litebox_shim_linux/src/lib.rs | 37 +- litebox_shim_linux/src/stdio.rs | 20 +- litebox_shim_linux/src/syscalls/eventfd.rs | 14 +- litebox_shim_linux/src/syscalls/mm.rs | 45 +- litebox_shim_linux/src/syscalls/tests.rs | 235 +++-- litebox_shim_linux/src/transport.rs | 262 +---- litebox_shim_windows/Cargo.toml | 10 + litebox_shim_windows/src/lib.rs | 45 +- litebox_shim_windows/src/loader/pe.rs | 23 +- litebox_shim_windows/src/syscalls/file.rs | 4 +- litebox_shim_windows/src/syscalls/registry.rs | 121 ++- litebox_shim_windows/src/test_broker.rs | 234 +++++ litebox_shim_windows/src/tests.rs | 103 +- 47 files changed, 2882 insertions(+), 1727 deletions(-) create mode 100644 litebox/src/test_broker.rs create mode 100644 litebox_broker_userland/src/sync.rs create mode 100644 litebox_runner_linux_on_windows_userland/tests/common/broker.rs create mode 100644 litebox_runner_linux_userland/tests/common/broker.rs create mode 100644 litebox_shim_windows/src/test_broker.rs diff --git a/Cargo.lock b/Cargo.lock index ea8b1557b5..4cc6179817 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1450,6 +1450,7 @@ dependencies = [ "buddy_system_allocator", "hashbrown", "litebox_broker_core", + "litebox_broker_host", "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", @@ -1575,7 +1576,9 @@ dependencies = [ "litebox_platform_linux_userland", "litebox_runner_linux_userland", "litebox_runner_windows_userland", + "litebox_syscall_rewriter", "tempfile", + "windows-sys 0.60.2", ] [[package]] @@ -1763,7 +1766,12 @@ dependencies = [ "anyhow", "clap", "litebox", + "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", "litebox_broker_local_userland", + "litebox_broker_protocol", + "litebox_broker_transport", "litebox_common_linux", "litebox_platform_windows_userland", "litebox_shim_linux", @@ -1784,6 +1792,7 @@ dependencies = [ "litebox", "litebox_broker_core", "litebox_broker_host", + "litebox_broker_local", "litebox_broker_local_userland", "litebox_broker_platform_linux_userland", "litebox_broker_protocol", @@ -1795,7 +1804,6 @@ dependencies = [ "litebox_shim_linux", "litebox_syscall_rewriter", "litebox_util_log", - "memmap2", "sha2", "tracing-subscriber", "walkdir", @@ -1863,6 +1871,7 @@ dependencies = [ "anyhow", "clap", "litebox", + "litebox_broker_local_userland", "litebox_common_linux", "litebox_common_windows", "litebox_platform_linux_userland", @@ -1885,7 +1894,6 @@ dependencies = [ "litebox_shim_windows", "litebox_syscall_rewriter", "litebox_util_log", - "memmap2", "tar", "tracing-subscriber", ] @@ -1926,6 +1934,7 @@ dependencies = [ "libc", "litebox", "litebox_broker_core", + "litebox_broker_host", "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", @@ -1979,6 +1988,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 +2086,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/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index ca27ed956a..d334fa0282 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -37,7 +37,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_broker_core/", 1), ("litebox_broker_transport_linux_userland/", 1), ("litebox_broker_userland/", 1), - ("litebox/", 7), + ("litebox/", 8), ("litebox_platform/", 2), ("litebox_platform_linux_kernel/", 5), ("litebox_platform_linux_userland/", 5), @@ -45,10 +45,12 @@ fn ratchet_globals() -> Result<()> { ("litebox_platform_multiplex/", 1), ("litebox_platform_windows_userland/", 8), ("litebox_runner_lvbs/", 6), + ("litebox_runner_linux_on_windows_userland/", 1), + ("litebox_runner_linux_userland/", 1), ("litebox_runner_snp/", 2), ("litebox_shim_linux/", 2), ("litebox_shim_optee/", 6), - ("litebox_shim_windows/", 1), + ("litebox_shim_windows/", 2), ("litebox_runner_windows_userland/", 2), ], |file| { diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 520ca728e4..a457dea176 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -39,4 +39,5 @@ enforce_singleton_litebox_instance = [] workspace = true [dev-dependencies] +litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } tempfile = "3" diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index e8177f2ac4..11f9957402 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/mod.rs b/litebox/src/fs/mod.rs index c59d72682a..43eef7402e 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -14,25 +14,26 @@ use core::num::NonZeroUsize; 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)] +#[cfg(test)] pub mod backend { pub use litebox_broker_core::fs::backend::*; } #[doc(hidden)] +#[cfg(test)] pub mod composer { pub use litebox_broker_core::fs::composer::*; } #[doc(hidden)] +#[cfg(test)] pub mod devices { pub use litebox_broker_core::fs::devices::*; } #[doc(hidden)] +#[cfg(test)] pub mod in_mem { pub use litebox_broker_core::fs::in_mem::{InMem, InMemDirHandle, InMemFileHandle}; @@ -94,11 +95,13 @@ pub mod nine_p { } #[doc(hidden)] +#[cfg(test)] pub mod overlay { pub use litebox_broker_core::fs::overlay::*; } #[doc(hidden)] +#[cfg(test)] pub mod tar_ro { pub use litebox_broker_core::fs::tar_ro::*; } diff --git a/litebox/src/fs/nine_p/tests.rs b/litebox/src/fs/nine_p/tests.rs index a46c43d14c..bc7b36acbb 100644 --- a/litebox/src/fs/nine_p/tests.rs +++ b/litebox/src/fs/nine_p/tests.rs @@ -13,13 +13,12 @@ use crate::fs::errors::{ 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 super::nine_p::{NineP, transport}; -type NinePFs = Resolver>; +type NinePFs = crate::fs::resolver::Resolver; /// Attach to `server` over `transport`, building the backend the tests resolve paths through. fn attach( @@ -194,12 +193,9 @@ impl Drop for DiodServer { // Helper: create a connected 9P filesystem // --------------------------------------------------------------------------- -fn connect_9p( - litebox: &crate::LiteBox, - server: &DiodServer, -) -> NinePFs { +fn connect_9p(litebox: &crate::LiteBox, server: &DiodServer) -> NinePFs { let transport = TcpTransport::connect(&server.addr()); - Resolver::new(litebox, attach(transport, server)) + crate::test_broker::brokered_fs(litebox, attach(transport, server)) } // --------------------------------------------------------------------------- @@ -564,9 +560,9 @@ fn connect_9p_broken( litebox: &crate::LiteBox, server: &DiodServer, allowed_writes: usize, -) -> NinePFs { +) -> NinePFs { let tcp = TcpTransport::connect(&server.addr()); - Resolver::new( + crate::test_broker::brokered_fs( litebox, attach(BrokenTransport::new(tcp, allowed_writes), server), ) @@ -609,20 +605,7 @@ fn test_nine_p_broken_read() { 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); @@ -694,19 +677,7 @@ fn test_nine_p_broken_unlink() { 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"); @@ -720,11 +691,7 @@ fn test_nine_p_broken_rmdir() { 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"); @@ -750,20 +717,7 @@ fn test_nine_p_broken_truncate() { 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); @@ -782,20 +736,7 @@ fn test_nine_p_broken_seek() { 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); diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/resolver.rs index ff89b754a2..4a4013f1bc 100644 --- a/litebox/src/fs/resolver.rs +++ b/litebox/src/fs/resolver.rs @@ -1,173 +1,84 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Guest filesystem facade backed directly by the broker-core filesystem engine. +//! Guest filesystem facade 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_core::fs as broker_fs; -use litebox_broker_core::fs::backend::DeviceIo; -use litebox_broker_core::fs::resolver::{Resolver as BrokerResolver, ResolverEntry}; +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::error::ErrorCode; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, + FileSeekWhence, FileStatus as BrokerFileStatus, FileType as BrokerFileType, FileUser, +}; use crate::path::Arg; -use crate::{LiteBox, fd::TypedFd, sync}; +use crate::{ + LiteBox, + fd::{EntryHandle, 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}; +use super::{DirEntry, FileStatus, FileType, Mode, OFlags, SeekWhence, UserInfo}; /// The guest-facing filesystem entry point. -pub struct Resolver< - Platform: sync::RawSyncPrimitivesProvider, - Backend: broker_fs::backend::Backend + 'static, -> { +pub struct Resolver { litebox: LiteBox, - engine: BrokerResolver, + authority: ResolverAuthority, } -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), - } - } +struct ResolverAuthority { + broker: Arc, } -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) - } +struct PinnedBrokerFile { + _entry: EntryHandle>, + broker: Arc, + handle: ObjectHandle, } -/// 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. +impl Resolver { + /// Constructs a resolver whose filesystem authority is owned by the negotiated broker. + /// + /// # Panics + /// + /// Panics if `litebox` has no negotiated broker connection. #[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 { + pub fn new_brokered(litebox: &LiteBox) -> Self { Self { - cwd: Arc::new(ResolvedPath { components: vec![] }), - user_info: UserInfo { - user: 1000, - group: 1000, + litebox: litebox.clone(), + authority: ResolverAuthority { + broker: litebox + .broker_control() + .expect("brokered file operations require a broker connection"), }, } } - /// 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() + fn broker_file(&self, fd: &TypedFd) -> Option> { + let entry_handle = self.litebox.descriptor_table().entry_handle(fd)?; + let (broker, handle) = { + let entry = entry_handle.get_entry(); + (Arc::clone(&entry.entry.broker), entry.entry.handle) }; - for component in path.components()? { - match component { - "" | "." => {} - ".." => { - let _ = components.pop(); - } - _ => { - components.push(component.into()); - } - } - } - Ok(ResolvedPath { components }) + Some(PinnedBrokerFile { + _entry: entry_handle, + broker, + handle, + }) } -} -impl Default for Context { - fn default() -> Self { - Self::new() + fn broker_path(context: &Context, path: impl Arg) -> Result { + Ok(context.resolve(path)?.to_string()) } -} -/// 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. @@ -178,17 +89,24 @@ impl Result, OpenError> { - let path = context.resolve(path)?.to_string(); - let entry = self - .engine - .open( - broker_user_info(context.acting_user()), + let path = Self::broker_path(context, path)?; + let (access, flags) = file_open_options(flags)?; + let handle = self + .authority + .broker + .open_file( &path, - broker_open_flags(flags), - broker_mode(mode), + file_user(context.acting_user()), + access, + flags, + file_mode(mode), ) - .map_err(guest_open_error)?; - Ok(self.litebox.descriptor_table_mut().insert(entry)) + .map_err(|_| OpenError::Io)? + .map_err(open_error)?; + Ok(self.litebox.descriptor_table_mut().insert(BrokerFile { + broker: Arc::clone(&self.authority.broker), + handle, + })) } /// Close the file at `fd`. @@ -198,121 +116,95 @@ impl, 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) + 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`. - /// - /// 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. + 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( &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) + 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, file_seek_whence(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. - /// - /// 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) + 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(&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) + let path = Self::broker_path(context, path)?; + self.authority + .broker + .chmod_file(&path, file_user(context.acting_user()), file_mode(mode)) + .map_err(|_| ChmodError::Io)? + .map_err(chmod_error) } /// Change the owner of a file. @@ -323,54 +215,53 @@ impl, 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) + let path = Self::broker_path(context, path)?; + self.authority + .broker + .chown_file(&path, file_user(context.acting_user()), user, group) + .map_err(|_| ChownError::Io)? + .map_err(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) + let path = Self::broker_path(context, path)?; + self.authority + .broker + .unlink_file(&path, file_user(context.acting_user())) + .map_err(|_| UnlinkError::Io)? + .map_err(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) + let path = Self::broker_path(context, path)?; + self.authority + .broker + .mkdir_file(&path, file_user(context.acting_user()), file_mode(mode)) + .map_err(|_| MkdirError::Io)? + .map_err(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) + let path = Self::broker_path(context, path)?; + self.authority + .broker + .rmdir_file(&path, file_user(context.acting_user())) + .map_err(|_| RmdirError::Io)? + .map_err(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) + let file = self.broker_file(fd).ok_or(ReadDirError::ClosedFd)?; + let entries = file + .broker + .read_directory(file.handle) + .map_err(|error| broker_fd_error(error, ReadDirError::ClosedFd, ReadDirError::Io))? + .map_err(read_dir_error)?; + directory_entries(entries) } /// Obtain the status of a path. @@ -379,271 +270,413 @@ impl 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) + let path = Self::broker_path(context, path)?; + let status = self + .authority + .broker + .path_file_status(&path, file_user(context.acting_user())) + .map_err(|_| FileStatusError::Io)? + .map_err(file_status_error)?; + file_status(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) + let file = self.broker_file(fd).ok_or(FileStatusError::ClosedFd)?; + let status = file + .broker + .handle_file_status(file.handle) + .map_err(|error| { + broker_fd_error(error, FileStatusError::ClosedFd, FileStatusError::Io) + })? + .map_err(file_status_error)?; + file_status(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) + let _ = self.broker_file(fd)?; + None } } -// 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()) +/// Per-call resolution context. The user may hold and mutate this as they wish. +#[derive(Clone, Debug)] +pub struct Context { + cwd: Arc, + user_info: UserInfo, } -fn guest_mode(mode: broker_fs::Mode) -> Mode { - Mode::from_bits_retain(mode.bits()) -} +impl Context { + /// The user that operations on this context act as. + #[must_use] + pub fn acting_user(&self) -> UserInfo { + self.user_info + } -fn broker_open_flags(flags: OFlags) -> broker_fs::OFlags { - broker_fs::OFlags::from_bits_retain(flags.bits()) -} + /// Set the user that operations on this context act as. + pub fn set_acting_user(&mut self, user: UserInfo) { + self.user_info = user; + } -fn broker_user_info(user: UserInfo) -> broker_fs::UserInfo { - broker_fs::UserInfo { - user: user.user, - group: user.group, + /// 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 }) } } -fn guest_user_info(user: broker_fs::UserInfo) -> UserInfo { - UserInfo { - user: user.user, - group: user.group, +impl Default for Context { + fn default() -> Self { + Self::new() } } -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, +/// 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(()) } } -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, +struct BrokerFile { + broker: Arc, + handle: ObjectHandle, +} + +impl Drop for BrokerFile { + fn drop(&mut self) { + let _ = self.broker.close_object(self.handle); + } +} + +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)) } -fn guest_node_info(node: broker_fs::NodeInfo) -> NodeInfo { - NodeInfo { - dev: node.dev, - ino: node.ino, - rdev: node.rdev, +fn file_mode(mode: Mode) -> FileMode { + let bits = u16::try_from(mode.bits() & u32::from(FileMode::SUPPORTED.bits())) + .expect("supported file mode bits fit in u16"); + FileMode::from_bits(bits).expect("masked file mode bits are supported") +} + +const fn file_user(user: UserInfo) -> FileUser { + FileUser { + user: user.user, + group: user.group, } } -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, +const fn file_seek_whence(whence: SeekWhence) -> FileSeekWhence { + match whence { + SeekWhence::RelativeToBeginning => FileSeekWhence::Beginning, + SeekWhence::RelativeToCurrentOffset => FileSeekWhence::Current, + SeekWhence::RelativeToEnd => FileSeekWhence::End, } } -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 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, + } } -fn guest_path_error(error: broker_fs::errors::PathError) -> PathError { +fn path_error(error: FileError) -> Option { match error { - broker_fs::errors::PathError::NoSuchFileOrDirectory => PathError::NoSuchFileOrDirectory, - broker_fs::errors::PathError::NoSearchPerms { - #[cfg(debug_assertions)] - dir, - #[cfg(debug_assertions)] - perms, - } => PathError::NoSearchPerms { + FileError::NoSuchFileOrDirectory => Some(PathError::NoSuchFileOrDirectory), + FileError::NoSearchPermissions => Some(PathError::NoSearchPerms { #[cfg(debug_assertions)] - dir, + dir: String::new(), #[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, + perms: Mode::empty(), + }), + FileError::InvalidPathname => Some(PathError::InvalidPathname), + FileError::MissingComponent => Some(PathError::MissingComponent), + FileError::ComponentNotDirectory => Some(PathError::ComponentNotADirectory), + _ => None, } } -fn guest_open_error(error: broker_fs::errors::OpenError) -> OpenError { +fn open_error(error: FileError) -> OpenError { + if let Some(error) = path_error(error) { + return error.into(); + } 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)) - } + 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 guest_read_error(error: broker_fs::errors::ReadError) -> ReadError { +fn read_error(error: FileError) -> 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, + FileError::NotFile => ReadError::NotAFile, + FileError::NotForReading => ReadError::NotForReading, + _ => ReadError::Io, } } -fn guest_write_error(error: broker_fs::errors::WriteError) -> WriteError { +fn write_error(error: FileError) -> 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, + FileError::NotFile => WriteError::NotAFile, + FileError::NotForWriting => WriteError::NotForWriting, + _ => WriteError::Io, } } -fn guest_seek_error(error: broker_fs::errors::SeekError) -> SeekError { +fn seek_error(error: FileError) -> 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, + FileError::NotFile => SeekError::NotAFile, + FileError::InvalidOffset => SeekError::InvalidOffset, + FileError::NonSeekable => SeekError::NonSeekable, + _ => SeekError::Io, } } -fn guest_truncate_error(error: broker_fs::errors::TruncateError) -> TruncateError { +fn truncate_error(error: FileError) -> 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, + FileError::IsDirectory => TruncateError::IsDirectory, + FileError::NotForWriting => TruncateError::NotForWriting, + FileError::IsTerminalDevice => TruncateError::IsTerminalDevice, + _ => TruncateError::Io, } } -fn guest_chmod_error(error: broker_fs::errors::ChmodError) -> ChmodError { +fn chmod_error(error: FileError) -> ChmodError { + if let Some(error) = path_error(error) { + return error.into(); + } 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)) - } + FileError::NotOwner => ChmodError::NotTheOwner, + FileError::ReadOnlyFs => ChmodError::ReadOnlyFileSystem, + _ => ChmodError::Io, } } -fn guest_chown_error(error: broker_fs::errors::ChownError) -> ChownError { +fn chown_error(error: FileError) -> ChownError { + if let Some(error) = path_error(error) { + return error.into(); + } 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)) - } + FileError::NotOwner => ChownError::NotTheOwner, + FileError::ReadOnlyFs => ChownError::ReadOnlyFileSystem, + _ => ChownError::Io, } } -fn guest_unlink_error(error: broker_fs::errors::UnlinkError) -> UnlinkError { +fn unlink_error(error: FileError) -> UnlinkError { + if let Some(error) = path_error(error) { + return error.into(); + } 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)) - } + FileError::NoWritePermissions => UnlinkError::NoWritePerms, + FileError::IsDirectory => UnlinkError::IsADirectory, + FileError::ReadOnlyFs => UnlinkError::ReadOnlyFileSystem, + _ => UnlinkError::Io, } } -fn guest_mkdir_error(error: broker_fs::errors::MkdirError) -> MkdirError { +fn mkdir_error(error: FileError) -> MkdirError { + if let Some(error) = path_error(error) { + return error.into(); + } 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)) - } + FileError::NoWritePermissions => MkdirError::NoWritePerms, + FileError::AlreadyExists => MkdirError::AlreadyExists, + FileError::ReadOnlyFs => MkdirError::ReadOnlyFileSystem, + _ => MkdirError::Io, } } -fn guest_rmdir_error(error: broker_fs::errors::RmdirError) -> RmdirError { +fn rmdir_error(error: FileError) -> RmdirError { + if let Some(error) = path_error(error) { + return error.into(); + } 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)) - } + FileError::NoWritePermissions => RmdirError::NoWritePerms, + FileError::Busy => RmdirError::Busy, + FileError::NotEmpty => RmdirError::NotEmpty, + FileError::NotDirectory => RmdirError::NotADirectory, + FileError::ReadOnlyFs => RmdirError::ReadOnlyFileSystem, + _ => RmdirError::Io, } } -fn guest_read_dir_error(error: broker_fs::errors::ReadDirError) -> ReadDirError { +fn read_dir_error(error: FileError) -> ReadDirError { match error { - broker_fs::errors::ReadDirError::ClosedFd => ReadDirError::ClosedFd, - broker_fs::errors::ReadDirError::NotADirectory => ReadDirError::NotADirectory, - broker_fs::errors::ReadDirError::Io => ReadDirError::Io, + FileError::NotDirectory => ReadDirError::NotADirectory, + _ => 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)) - } +fn file_status_error(error: FileError) -> FileStatusError { + path_error(error).map_or(FileStatusError::Io, Into::into) +} + +fn file_status(status: BrokerFileStatus) -> Result { + Ok(FileStatus { + file_type: file_type(status.file_type).map_err(|()| FileStatusError::Io)?, + mode: Mode::from_bits_retain(u32::from(status.mode.bits())), + size: usize::try_from(status.size).map_err(|_| FileStatusError::Io)?, + owner: UserInfo { + user: status.owner.user, + group: status.owner.group, + }, + node_info: status_node_info(status.node_info)?, + blksize: usize::try_from(status.block_size).map_err(|_| FileStatusError::Io)?, + }) +} + +fn directory_entries(entries: Vec) -> Result, ReadDirError> { + let mut output = Vec::new(); + output + .try_reserve_exact(entries.len()) + .map_err(|_| ReadDirError::Io)?; + for entry in entries { + output.push(DirEntry { + name: entry.name, + file_type: file_type(entry.file_type).map_err(|()| ReadDirError::Io)?, + ino_info: entry.node_info.map(directory_node_info).transpose()?, + }); + } + Ok(output) +} + +fn file_type(file_type: BrokerFileType) -> Result { + match file_type { + BrokerFileType::RegularFile => Ok(FileType::RegularFile), + BrokerFileType::Directory => Ok(FileType::Directory), + BrokerFileType::CharacterDevice => Ok(FileType::CharacterDevice), + _ => Err(()), } } +fn status_node_info(node: FileNodeInfo) -> Result { + Ok(super::NodeInfo { + dev: usize::try_from(node.dev).map_err(|_| FileStatusError::Io)?, + ino: usize::try_from(node.ino).map_err(|_| FileStatusError::Io)?, + rdev: optional_device(node.rdev).map_err(|()| FileStatusError::Io)?, + }) +} + +fn directory_node_info(node: FileNodeInfo) -> Result { + Ok(super::NodeInfo { + dev: usize::try_from(node.dev).map_err(|_| ReadDirError::Io)?, + ino: usize::try_from(node.ino).map_err(|_| ReadDirError::Io)?, + rdev: optional_device(node.rdev).map_err(|()| ReadDirError::Io)?, + }) +} + +fn optional_device(device: Option) -> Result, ()> { + device + .map(|device| { + usize::try_from(device) + .ok() + .and_then(core::num::NonZeroUsize::new) + .ok_or(()) + }) + .transpose() +} + crate::fd::enable_fds_for_subsystem! { - @ Platform: { sync::RawSyncPrimitivesProvider }, Backend: { broker_fs::backend::Backend + 'static }; - Resolver; - @ Backend: { broker_fs::backend::Backend + 'static }; - ResolverEntry; - -> ResolverFd; + @ Platform: { sync::RawSyncPrimitivesProvider }; + Resolver; + BrokerFile; + -> ResolverFd; } diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 542f634bce..9e55921bc9 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -4,8 +4,8 @@ fn tar_ro_fs( litebox: &crate::LiteBox, tar_data: alloc::borrow::Cow<'static, [u8]>, -) -> crate::fs::resolver::Resolver { - crate::fs::resolver::Resolver::new( +) -> crate::fs::resolver::Resolver { + crate::test_broker::brokered_fs( litebox, crate::fs::tar_ro::TarRo::new( tar_data, @@ -14,26 +14,22 @@ fn tar_ro_fs( ) } -type InMemFs = crate::fs::resolver::Resolver< - crate::platform::mock::MockPlatform, - crate::fs::in_mem::InMem, ->; +type InMemFs = crate::fs::resolver::Resolver; fn in_mem_fs(litebox: &crate::LiteBox) -> InMemFs { - crate::fs::resolver::Resolver::new( + crate::test_broker::brokered_fs( litebox, - crate::fs::in_mem::InMem::new(crate::fs::inode_allocator::InodeAllocator::standalone()), + 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, +fn with_root_privileges( + fs: &mut crate::fs::resolver::Resolver, context: &crate::fs::resolver::Context, - f: impl FnOnce(&mut crate::fs::resolver::Resolver, &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); @@ -41,22 +37,19 @@ fn with_root_privileges< /// 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, +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), + 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, ->; +type OverlayFs = crate::fs::resolver::Resolver; /// An overlay of `upper` over a tar-backed lower layer. fn overlay_fs( @@ -64,9 +57,9 @@ fn overlay_fs( upper: crate::fs::in_mem::InMem, tar_data: alloc::borrow::Cow<'static, [u8]>, ) -> OverlayFs { - crate::fs::resolver::Resolver::new( + crate::test_broker::brokered_fs( litebox, - crate::fs::overlay::Overlay::new( + crate::fs::overlay::Overlay::::new( upper, crate::fs::tar_ro::TarRo::new( tar_data, @@ -2123,7 +2116,6 @@ 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; @@ -2134,7 +2126,7 @@ mod stdio { let ctx = crate::fs::resolver::Context::new(); let platform = MockPlatform::new(); let litebox = LiteBox::new(platform); - let fs = Resolver::new( + let fs = crate::test_broker::brokered_fs( &litebox, crate::fs::composer::Composer::builder() .mount("/dev", Devices::new) @@ -2178,7 +2170,7 @@ mod stdio { fn non_dev_path_fails() { let ctx = crate::fs::resolver::Context::new(); let litebox = LiteBox::new(MockPlatform::new()); - let fs = Resolver::new( + let fs = crate::test_broker::brokered_fs( &litebox, crate::fs::composer::Composer::builder() .mount("/dev", Devices::new) @@ -2203,16 +2195,15 @@ mod composed_stdio { 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; + type ComposedFs = crate::fs::resolver::Resolver; fn composed_fs(litebox: &LiteBox) -> ComposedFs { - Resolver::new( + crate::test_broker::brokered_fs( litebox, Composer::builder() .mount("/", |_| { diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index 0d87da5c1b..319837e113 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -33,6 +33,8 @@ pub mod tls; // The core [`LiteBox`] object itself, re-exported here publicly, just to keep management of the // code cleaner. mod litebox; +#[cfg(test)] +mod test_broker; pub use litebox::LiteBox; // Explicitly-private, the utilities are not exposed to users of LiteBox, and are intended entirely diff --git a/litebox/src/test_broker.rs b/litebox/src/test_broker.rs new file mode 100644 index 0000000000..d111375200 --- /dev/null +++ b/litebox/src/test_broker.rs @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +extern crate std; + +use alloc::{boxed::Box, sync::Arc, vec}; + +use litebox_broker_core::{ + BrokerCore, ObjectRights, PolicyEngine, + fs::{backend::Backend, resolver::Resolver as CoreResolver}, + 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, 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::platform::mock::MockPlatform; + +pub(crate) fn brokered_fs( + litebox: &crate::LiteBox, + backend: BackendType, +) -> crate::fs::resolver::Resolver { + static BROKER: std::sync::OnceLock = std::sync::OnceLock::new(); + let broker = BROKER.get_or_init(|| { + BrokerCore::new( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), + Arc::new(UnsupportedSocketProvider), + Arc::new(UnusedRandomProvider), + Arc::new(UnsupportedStdioProvider), + Arc::new(CoreResolver::::new(backend)), + ) + .unwrap() + }); + + let setup = TestBrokerSetup::new(broker); + let (broker_local, ()) = BrokerLocal::negotiate(setup, |setup| { + let memory: Arc = setup.memory.clone(); + Ok((setup.activate(), memory, ())) + }) + .unwrap(); + let litebox = crate::LiteBox::new_with_broker_local(litebox.x.platform, broker_local); + crate::fs::resolver::Resolver::new_brokered(&litebox) +} + +struct TestBrokerSetup { + broker: &'static BrokerCore, + memory: Arc, +} + +impl TestBrokerSetup { + fn new(broker: &'static 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 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(()) + } +} + +struct UnusedRandomProvider; + +impl RandomProvider for UnusedRandomProvider { + fn fill(&self, _output: &mut [u8]) -> core::result::Result<(), RandomProviderError> { + Err(RandomProviderError) + } +} diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index 918e4ba124..d728ec95fa 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -208,7 +208,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 diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index 942a00bbc8..cba76a62e4 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -17,11 +17,16 @@ litebox_broker_platform_linux_userland = { path = "../litebox_broker_platform_li litebox_broker_transport_linux_userland = { path = "../litebox_broker_transport_linux_userland", version = "0.1.0" } litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland", version = "0.1.0" } litebox_runner_linux_userland = { path = "../litebox_runner_linux_userland", version = "0.1.0" } +litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0.1.0" } tempfile = { version = "3", default-features = false } [target.'cfg(all(windows, target_arch = "x86_64"))'.dependencies] 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" } +windows-sys = { version = "0.60.2", features = [ + "Win32_Foundation", + "Win32_System_Threading", +] } [features] lock_tracing = [ @@ -29,6 +34,7 @@ lock_tracing = [ "litebox_broker_platform_linux_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 93a664a744..e4268b4217 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 156e7b5d29..1d1d04ff45 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -5,13 +5,23 @@ use std::error::Error; use std::ffi::OsString; use std::io::{BufRead, BufReader, Error as IoError, ErrorKind, Result as IoResult}; use std::net::{Ipv4Addr, SocketAddrV4}; +use std::os::linux::fs::MetadataExt as _; use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::{Path, PathBuf}; use std::process::{Child, ChildStdout, Command, Stdio}; +use std::sync::Arc; use std::sync::mpsc::{RecvTimeoutError, sync_channel}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; use clap::Parser as _; +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::fs::{Mode, UserInfo}; use litebox_broker_core::socket::HOST_GATEWAY_IPV4_ADDRESS; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; @@ -24,6 +34,8 @@ use litebox_broker_userland::builder::BrokerCoreBuilder; use super::{SETUP_TIMEOUT, configured_socket_policy}; const PROXY_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); +const DEFAULT_GUEST_UID: u16 = 1000; +const DEFAULT_GUEST_GID: u16 = 1000; pub(super) fn run(mut args: super::CliArgs) -> Result<(), Box> { let proxy = if args.allow_host.is_empty() { @@ -50,7 +62,8 @@ 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 = create_file_service(&args)?; + 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 { @@ -79,6 +92,217 @@ pub(super) fn run(mut args: super::CliArgs) -> Result<(), Box> { } } +fn create_file_service(args: &super::CliArgs) -> Result, Box> { + let runner_args = inferred_linux_runner_args(args); + let windows_runner = runner_is_windows_on_linux(args); + let initial_files = args + .fs_initial_files + .clone() + .or_else(|| { + runner_args + .as_ref() + .and_then(|args| args.initial_files.clone()) + }) + .or_else(|| { + windows_runner + .then(|| windows_runner_initial_files(&args.runner_arguments)) + .flatten() + }); + let program_from_tar = runner_args + .as_ref() + .is_some_and(|args| args.program_from_tar) + || windows_runner + || (args.fs_program.is_none() && args.fs_initial_files.is_some()); + if program_from_tar && initial_files.is_none() { + return Err(IoError::new( + ErrorKind::InvalidInput, + "a tar-backed guest program requires --fs-initial-files or runner --initial-files", + ) + .into()); + } + if runner_args.is_none() + && !windows_runner + && args.fs_program.is_none() + && args.fs_initial_files.is_none() + { + return Err(IoError::new( + ErrorKind::InvalidInput, + "could not infer broker filesystem inputs from runner arguments", + ) + .into()); + } + let program = args.fs_program.clone().or_else(|| { + (!program_from_tar) + .then(|| { + runner_args + .as_ref()? + .program_and_arguments + .first() + .map(PathBuf::from) + }) + .flatten() + }); + let rewrite_syscalls = args.fs_rewrite_syscalls + || runner_args + .as_ref() + .is_some_and(|args| args.rewrite_syscalls); + + let mut entries = Vec::new(); + if let Some(program) = program.as_deref() { + let program = std::path::absolute(program)?; + let ancestors: Vec<_> = program.ancestors().skip(1).collect(); + let mut previous_user = 0; + for path in ancestors.into_iter().rev().skip(1) { + let metadata = path.metadata()?; + let owner = guest_owner(previous_user, metadata.st_uid()); + entries.push(( + path_to_string(path)?, + InitialNode::Directory { + mode: Mode::from_bits_retain(metadata.st_mode()), + owner, + }, + )); + previous_user = metadata.st_uid(); + } + let mut program_data = std::fs::read(&program)?; + if rewrite_syscalls { + program_data = litebox_syscall_rewriter::hook_syscalls_in_elf_with_options( + &program_data, + None, + litebox_syscall_rewriter::RewriteOptions::new( + litebox_syscall_rewriter::TargetHost::Linux, + args.fs_virtualize_x18 || cfg!(feature = "aarch64_virtualize_x18"), + ), + )?; + } + let metadata = program.metadata()?; + entries.push(( + path_to_string(&program)?, + InitialNode::File { + mode: Mode::from_bits_retain(metadata.st_mode()), + owner: guest_owner(previous_user, metadata.st_uid()), + data: program_data.into(), + }, + )); + } + + let writable_directory = |owner| InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner, + }; + if let Some((_, InitialNode::Directory { mode, .. })) = + entries.iter_mut().find(|(path, _)| path == "/tmp") + { + *mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; + } else { + entries.push(("/tmp".to_owned(), writable_directory(UserInfo::ROOT))); + } + entries.push(("/registry".to_owned(), writable_directory(UserInfo::ROOT))); + + let tar_data = match initial_files.as_deref() { + 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()), + ) + .into()); + } + std::borrow::Cow::Owned(std::fs::read(path)?) + } + None => std::borrow::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::< + litebox_broker_platform_linux_userland::LinuxSyncPrimitivesProvider, + _, + >::new(backend))) +} + +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 inferred_linux_runner_args( + args: &super::CliArgs, +) -> Option { + if runner_is_windows_on_linux(args) { + return None; + } + litebox_runner_linux_userland::CliArgs::try_parse_from( + std::iter::once(OsString::from("litebox-runner-linux-userland")) + .chain(std::iter::once(OsString::from("--unstable"))) + .chain(args.runner_arguments.iter().cloned()), + ) + .ok() +} + +fn runner_is_windows_on_linux(args: &super::CliArgs) -> bool { + args.runner + .as_deref() + .and_then(Path::file_name) + .and_then(|name| name.to_str()) + .is_some_and(|name| name.contains("windows_on_linux")) +} + +fn windows_runner_initial_files(arguments: &[OsString]) -> Option { + let mut arguments = arguments.iter(); + while let Some(argument) = arguments.next() { + if argument == "--initial-files" { + return arguments.next().map(PathBuf::from); + } + if let Some(path) = argument + .to_str() + .and_then(|argument| argument.strip_prefix("--initial-files=")) + { + return Some(PathBuf::from(path)); + } + let argument = argument.to_str()?; + let option = argument + .split_once('=') + .map_or(argument, |(option, _)| option); + match option { + "--env" | "--broker-control-channel" => { + if !argument.contains('=') { + arguments.next()?; + } + } + "-Z" | "--unstable" | "--forward-env" => {} + "--" => return None, + argument if argument.starts_with('-') => return None, + _ => return None, + } + } + None +} + +fn path_to_string(path: &Path) -> Result { + path.to_str() + .map(str::to_owned) + .ok_or_else(|| IoError::new(ErrorKind::InvalidData, "file path is not UTF-8")) +} + struct ManagedEgressProxy { child: Child, port: u16, @@ -260,3 +484,32 @@ fn serve_control_stream( UnixStreamHostSetupChannel::into_active, ) } + +#[cfg(test)] +mod tests { + use super::windows_runner_initial_files; + use std::ffi::OsString; + use std::path::PathBuf; + + #[test] + fn windows_runner_initial_files_accepts_equals_options() { + let arguments = [ + "--env=NAME=value", + "--initial-files=root.tar", + "/app/program.exe", + ] + .map(OsString::from); + + assert_eq!( + windows_runner_initial_files(&arguments), + Some(PathBuf::from("root.tar")) + ); + } + + #[test] + fn windows_runner_initial_files_does_not_scan_guest_arguments() { + let arguments = ["/app/program.exe", "--initial-files", "guest-value"].map(OsString::from); + + assert_eq!(windows_runner_initial_files(&arguments), None); + } +} diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 7c2c9527e2..96f7a313bc 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -21,6 +21,8 @@ use litebox_broker_protocol::socket::{Ipv4Address, Port}; #[cfg(target_os = "linux")] mod linux; #[cfg(all(windows, target_arch = "x86_64"))] +mod sync; +#[cfg(all(windows, target_arch = "x86_64"))] mod windows; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -70,6 +72,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,6 +108,20 @@ struct CliArgs { conflicts_with = "in_process_runner" )] runner: Option, + /// Host program to populate into the broker-owned file system. + #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::ExecutablePath)] + fs_program: Option, + /// Tar archive to mount as the broker-owned initial file system. + /// + /// When `--fs-program` is omitted, the guest program is expected to be in this archive. + #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] + fs_initial_files: Option, + /// Rewrite the host program before populating the broker-owned file system. + #[arg(long, requires = "fs_program")] + fs_rewrite_syscalls: bool, + /// Declare that rewritten AArch64 binaries use x18 virtualization. + #[arg(long)] + fs_virtualize_x18: bool, /// Arguments to pass to the local runner. #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true, value_hint = clap::ValueHint::CommandWithArguments)] runner_arguments: Vec, diff --git a/litebox_broker_userland/src/sync.rs b/litebox_broker_userland/src/sync.rs new file mode 100644 index 0000000000..b4c51c5d3b --- /dev/null +++ b/litebox_broker_userland/src/sync.rs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#![cfg(all(windows, target_arch = "x86_64"))] + +use core::ffi::c_void; +use core::sync::atomic::AtomicU32; +use core::time::Duration; + +use litebox_platform::sync::{ImmediatelyWokenUp, RawMutex, RawMutexProvider, UnblockedOrTimedOut}; +use windows_sys::Win32::Foundation::{ERROR_TIMEOUT, GetLastError}; +use windows_sys::Win32::System::Threading::{ + INFINITE, WaitOnAddress, WakeByAddressAll, WakeByAddressSingle, +}; + +#[derive(Clone, Copy, Debug, Default)] +pub(super) struct WindowsSyncPrimitivesProvider; + +impl RawMutexProvider for WindowsSyncPrimitivesProvider { + type RawMutex = WindowsRawMutex; +} + +pub(super) 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, + ) -> Result { + 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 { + Ok(UnblockedOrTimedOut::Unblocked) + } else { + // SAFETY: GetLastError has no preconditions. + match unsafe { GetLastError() } { + ERROR_TIMEOUT => Ok(UnblockedOrTimedOut::TimedOut), + error => panic!("WaitOnAddress failed with error {error}"), + } + } + } +} + +impl RawMutex 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) { + Ok(UnblockedOrTimedOut::Unblocked) => Ok(()), + Ok(UnblockedOrTimedOut::TimedOut) => unreachable!(), + Err(error) => Err(error), + } + } + + fn block_or_timeout( + &self, + expected: u32, + timeout: Duration, + ) -> Result { + self.block_or_maybe_timeout(expected, Some(timeout)) + } +} diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index 8f5ef0919b..e936b3baba 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -8,10 +8,18 @@ use std::ffi::OsString; use std::io::Result as IoResult; use std::os::windows::io::AsRawHandle; use std::process::Child; +use std::sync::Arc; use std::thread::JoinHandle; use std::time::Instant; use clap::Parser as _; +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::TarRo; +use litebox_broker_core::fs::{Mode, UserInfo}; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport_windows_userland::named_pipe::{ @@ -31,6 +39,7 @@ pub(super) fn run(args: super::CliArgs) -> Result<(), Box> { configured_socket_policy(&args.allow_tcp_destination, &args.allow_udp_destination)?, ), ) + .with_file_service(create_file_service(&args)?) .build()?; if args.in_process_runner { @@ -44,6 +53,76 @@ pub(super) fn run(args: super::CliArgs) -> Result<(), Box> { } } +fn create_file_service(args: &super::CliArgs) -> Result, Box> { + if args.fs_program.is_some() || args.fs_rewrite_syscalls || args.fs_virtualize_x18 { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Windows broker file systems require the program in the initial tar archive", + ) + .into()); + } + let runner_args = litebox_runner_windows_userland::CliArgs::try_parse_from( + std::iter::once(OsString::from("litebox-runner-windows-userland")) + .chain(std::iter::once(OsString::from("--unstable"))) + .chain(args.runner_arguments.iter().cloned()), + ) + .ok(); + let initial_files = args + .fs_initial_files + .clone() + .or_else(|| runner_args.and_then(|args| args.initial_files)) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Windows broker requires --fs-initial-files or runner --initial-files", + ) + })?; + if initial_files + .extension() + .and_then(|extension| extension.to_str()) + != Some("tar") + { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("expected a .tar file, found {}", initial_files.display()), + ) + .into()); + } + let tar_data = std::borrow::Cow::Owned(std::fs::read(initial_files)?); + let mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; + let in_mem = InMem::::new_initialized([ + ( + "/tmp", + InitialNode::Directory { + mode, + owner: UserInfo::ROOT, + }, + ), + ( + "/registry", + InitialNode::Directory { + mode, + owner: UserInfo::ROOT, + }, + ), + ]); + 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(|_| std::io::Error::other("failed to construct broker file service"))?; + Ok(Arc::new(Resolver::< + super::sync::WindowsSyncPrimitivesProvider, + _, + >::new(backend))) +} + fn run_runner_in_process( args: &super::CliArgs, control_pipe: &std::ffi::OsStr, diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index f2180b31cd..81a278348d 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -42,10 +42,13 @@ 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("--fs-program") + .arg(&test_executable) .arg("--runner") - .arg(std::env::current_exe().unwrap()) + .arg(&test_executable) .arg(RUNNER_ARGUMENT); wait_for_broker(event_command); @@ -68,12 +71,14 @@ fn run_parent_test() { }); let mut network_command = Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")); network_command + .arg("--fs-program") + .arg(&test_executable) .arg("--allow-tcp-destination") .arg(format!("{gateway}/32:{tcp_port}")) .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/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 9d57b82de8..18a7c4ada8 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_runner_linux_on_windows_userland/Cargo.toml b/litebox_runner_linux_on_windows_userland/Cargo.toml index 0621527828..9707fff78c 100644 --- a/litebox_runner_linux_on_windows_userland/Cargo.toml +++ b/litebox_runner_linux_on_windows_userland/Cargo.toml @@ -15,6 +15,11 @@ litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } [dev-dependencies] +litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core" } +litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } +litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } +litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } +litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } litebox_syscall_rewriter = { version = "0.1.0", path = "../litebox_syscall_rewriter", default-features = false, features = ["std"] } tar = "0.4" diff --git a/litebox_runner_linux_on_windows_userland/src/lib.rs b/litebox_runner_linux_on_windows_userland/src/lib.rs index e047d4fb24..0ae3f58d37 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,52 +72,27 @@ 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 = shim_builder.brokered_fs(); let initial_file_system = std::sync::Arc::new(initial_file_system); let shim = shim_builder.build(); diff --git a/litebox_runner_linux_on_windows_userland/tests/common/broker.rs b/litebox_runner_linux_on_windows_userland/tests/common/broker.rs new file mode 100644 index 0000000000..a9eb2dac33 --- /dev/null +++ b/litebox_runner_linux_on_windows_userland/tests/common/broker.rs @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::sync::{Arc, Mutex, OnceLock}; + +use litebox_broker_core::{ + BrokerCore, ObjectRights, PolicyEngine, + fs::{backend::Backend, 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, 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 litebox_platform_windows_userland::WindowsUserland; + +pub fn litebox( + platform: &'static WindowsUserland, + backend: BackendType, +) -> litebox::LiteBox { + static BROKER: OnceLock = OnceLock::new(); + let broker = BROKER.get_or_init(|| { + BrokerCore::new( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), + Arc::new(UnsupportedSocketProvider), + Arc::new(UnusedRandomProvider), + Arc::new(UnsupportedStdioProvider), + Arc::new(Resolver::::new(backend)), + ) + .unwrap() + }); + + 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: &'static BrokerCore, + memory: Arc, +} + +impl TestBrokerSetup { + fn new(broker: &'static 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, + ) -> Result<(), Self::Error> { + assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); + Ok(()) + } + + fn recv_handshake_response(&mut self) -> 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) -> 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) -> Result { + Ok(PeerCredential::Unauthenticated) + } + + fn recv_handshake_request( + &mut self, + ) -> Result, Self::Error> { + Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + })) + } + + fn send_handshake_response( + &mut self, + response: &BrokerHandshakeResponse, + ) -> 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 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]) -> Result<(), SharedMemoryError> { + let memory = self.0.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let source = memory + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let mut memory = self.0.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let destination = memory + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } +} + +struct UnusedRandomProvider; + +impl RandomProvider for UnusedRandomProvider { + fn fill(&self, _output: &mut [u8]) -> Result<(), RandomProviderError> { + Err(RandomProviderError) + } +} diff --git a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs index 07a8ea88d9..37919a4931 100644 --- a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs +++ b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs @@ -8,6 +8,8 @@ use std::ffi::CString; use litebox::fs::{Mode, OFlags}; use litebox_platform_windows_userland::WindowsUserland as Platform; +mod broker; + pub struct TestLauncher { platform: &'static Platform, shim_builder: litebox_shim_linux::LinuxShimBuilder, @@ -22,43 +24,62 @@ impl TestLauncher { 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 core_mode = litebox_broker_core::fs::Mode::RWXU + | litebox_broker_core::fs::Mode::RWXG + | litebox_broker_core::fs::Mode::RWXO; + let mut entries = vec![( + "/".to_string(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode: core_mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, }, - )]); + )]; + entries.extend(initial_dirs.iter().map(|path| { + ( + (*path).to_string(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode: core_mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, + }, + ) + })); + entries.extend(initial_files.iter().map(|path| { + ( + (*path).to_string(), + litebox_broker_core::fs::in_mem::InitialNode::File { + mode: core_mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, + data: std::fs::read(path).unwrap().into(), + }, + ) + })); + let in_mem = litebox_broker_core::fs::in_mem::InMem::::new_initialized(entries); let tar_data = if tar_data.is_empty() { - litebox::fs::tar_ro::EMPTY_TAR_FILE.into() + litebox_broker_core::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 { + let backend = litebox_broker_core::fs::composer::Composer::builder() + .mount_nestable("/", |allocators| { + litebox_broker_core::fs::overlay::Overlay::::new( + in_mem, + litebox_broker_core::fs::tar_ro::TarRo::new(tar_data, allocators.next()), + allocators.next(), + ) + }) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .unwrap(); + let litebox = broker::litebox(platform, backend); + let shim_builder = + litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox); + let fs = shim_builder.brokered_fs(); + 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) { diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 2c44200dad..e06ae13ac3 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"] } @@ -26,6 +25,7 @@ walkdir = "2.0" glob = "0.3" litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core" } litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } +litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } litebox_broker_platform_linux_userland = { version = "0.1.0", path = "../litebox_broker_platform_linux_userland" } litebox_broker_userland = { version = "0.1.0", path = "../litebox_broker_userland" } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 8db2640e76..664decfbf1 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -3,10 +3,7 @@ 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; @@ -74,7 +71,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 +97,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 +137,36 @@ 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) - } 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"), - ), - ) - .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 - }; - // 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); // SAFETY: `gettid` takes no pointer arguments and has no Rust-side aliasing requirements. let tid = unsafe { libc::syscall(libc::SYS_gettid) } .try_into() @@ -286,82 +181,7 @@ 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()) - }; + let initial_file_system = shim_builder.brokered_fs(); // We need to get the file path before enabling seccomp. // For --program-from-tar the path is already validated as absolute above, diff --git a/litebox_runner_linux_userland/tests/common/broker.rs b/litebox_runner_linux_userland/tests/common/broker.rs new file mode 100644 index 0000000000..6ea76650e3 --- /dev/null +++ b/litebox_runner_linux_userland/tests/common/broker.rs @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::sync::{Arc, Mutex, OnceLock}; + +use litebox_broker_core::{ + BrokerCore, ObjectRights, PolicyEngine, + fs::{backend::Backend, 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, 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 litebox_platform_linux_userland::LinuxUserland; + +pub fn litebox( + platform: &'static LinuxUserland, + backend: BackendType, +) -> litebox::LiteBox { + static BROKER: OnceLock = OnceLock::new(); + let broker = BROKER.get_or_init(|| { + BrokerCore::new( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), + Arc::new(UnsupportedSocketProvider), + Arc::new(UnusedRandomProvider), + Arc::new(UnsupportedStdioProvider), + Arc::new(Resolver::::new(backend)), + ) + .unwrap() + }); + + 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: &'static BrokerCore, + memory: Arc, +} + +impl TestBrokerSetup { + fn new(broker: &'static 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, + ) -> Result<(), Self::Error> { + assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); + Ok(()) + } + + fn recv_handshake_response(&mut self) -> 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) -> 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) -> Result { + Ok(PeerCredential::Unauthenticated) + } + + fn recv_handshake_request( + &mut self, + ) -> Result, Self::Error> { + Ok(HostReceive::Message(BrokerHandshakeRequest { + protocol_version: BROKER_PROTOCOL_VERSION, + })) + } + + fn send_handshake_response( + &mut self, + response: &BrokerHandshakeResponse, + ) -> 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 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]) -> Result<(), SharedMemoryError> { + let memory = self.0.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let source = memory + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } + + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let mut memory = self.0.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + let destination = memory + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice(source); + Ok(()) + } +} + +struct UnusedRandomProvider; + +impl RandomProvider for UnusedRandomProvider { + fn fill(&self, _output: &mut [u8]) -> Result<(), RandomProviderError> { + Err(RandomProviderError) + } +} diff --git a/litebox_runner_linux_userland/tests/loader.rs b/litebox_runner_linux_userland/tests/loader.rs index 1d9e2ed577..cd957f9fb2 100644 --- a/litebox_runner_linux_userland/tests/loader.rs +++ b/litebox_runner_linux_userland/tests/loader.rs @@ -3,87 +3,118 @@ mod cache; mod common; +#[path = "common/broker.rs"] +mod test_broker; 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 { + fn init_platform( + tar_data: &'static [u8], + initial_files: &[&str], + installed_files: &[(&str, Vec)], + ) -> 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 mode = litebox_broker_core::fs::Mode::RWXU + | litebox_broker_core::fs::Mode::RWXG + | litebox_broker_core::fs::Mode::RWXO; + let mut directories = std::collections::BTreeSet::new(); + for path in initial_files { + directories.extend( + std::path::Path::new(path) + .parent() + .into_iter() + .flat_map(std::path::Path::ancestors) + .filter(|path| { + *path != std::path::Path::new("/") && !path.as_os_str().is_empty() + }) + .map(std::path::Path::to_path_buf), + ); + } + for (path, _) in installed_files { + directories.extend( + std::path::Path::new(path) + .parent() + .into_iter() + .flat_map(std::path::Path::ancestors) + .filter(|path| { + *path != std::path::Path::new("/") && !path.as_os_str().is_empty() + }) + .map(std::path::Path::to_path_buf), + ); + } + let mut directories: Vec<_> = directories.into_iter().collect(); + directories.sort_by_key(|path| path.components().count()); + let mut entries = vec![( + "/".to_string(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, }, - )]); + )]; + entries.extend(directories.into_iter().map(|path| { + ( + path.to_string_lossy().into_owned(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, + }, + ) + })); + entries.extend(initial_files.iter().map(|path| { + ( + (*path).to_string(), + litebox_broker_core::fs::in_mem::InitialNode::File { + mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, + data: std::fs::read(path).unwrap().into(), + }, + ) + })); + entries.extend(installed_files.iter().map(|(path, data)| { + ( + (*path).to_string(), + litebox_broker_core::fs::in_mem::InitialNode::File { + mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, + data: data.clone().into(), + }, + ) + })); + let in_mem = litebox_broker_core::fs::in_mem::InMem::::new_initialized(entries); let tar_data = if tar_data.is_empty() { - litebox::fs::tar_ro::EMPTY_TAR_FILE.into() + litebox_broker_core::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 { + let backend = litebox_broker_core::fs::composer::Composer::builder() + .mount_nestable("/", |allocators| { + litebox_broker_core::fs::overlay::Overlay::::new( + in_mem, + litebox_broker_core::fs::tar_ro::TarRo::new(tar_data, allocators.next()), + allocators.next(), + ) + }) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .unwrap(); + let litebox = test_broker::litebox(platform, backend); + let shim_builder = + litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox); + let fs = shim_builder.brokered_fs(); + 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) { @@ -123,14 +154,14 @@ fn test_load_exec_dynamic() { let executable_path = "/hello_dylib"; let executable_data = std::fs::read(path).unwrap(); - let mut launcher = TestLauncher::init_platform( + let launcher = TestLauncher::init_platform( &[], &files_to_install .iter() .map(std::string::String::as_str) .collect::>(), + &[(executable_path, executable_data)], ); - launcher.install_file(executable_data, executable_path); launcher.test_load_exec_common(executable_path); } @@ -141,9 +172,7 @@ fn test_load_exec_static() { 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); + let launcher = TestLauncher::init_platform(&[], &[], &[(executable_path, executable_data)]); launcher.test_load_exec_common(executable_path); } @@ -280,7 +309,6 @@ fn test_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); + let launcher = TestLauncher::init_platform(&[], &[], &[(executable_path, executable_data)]); launcher.test_load_exec_common(executable_path); } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index b401a9d45f..dc7a7f869f 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -534,24 +534,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::{ + Mode, UserInfo, + composer::Composer, + in_mem::{InMem, InitialNode}, + resolver::Resolver, + }; + use litebox_broker_platform_linux_userland::LinuxSyncPrimitivesProvider; + + 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_bits_retain(metadata.permissions().mode() & 0o7777); + 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 +658,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 +680,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 +775,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 +851,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 +1008,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 +1016,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 +1106,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 +1130,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 +1139,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 +1154,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 +1189,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 +1197,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 +1599,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 +1607,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(); @@ -1569,6 +1695,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 +1703,7 @@ fn test_broker_with_iperf3() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(gateway_tcp_policy()), + &[runner.tar_dir()], 1, ); @@ -1635,7 +1763,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 01a56d242e..93902c3ae0 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 f200cc6a2f..967e714f70 100644 --- a/litebox_runner_windows_on_linux_userland/Cargo.toml +++ b/litebox_runner_windows_on_linux_userland/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" anyhow = "1.0.97" clap = { version = "4.5.33", features = ["derive"] } litebox = { version = "0.1.0", path = "../litebox" } +litebox_broker_local_userland = { version = "0.1.0", path = "../litebox_broker_local_userland" } litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } litebox_platform_linux_userland = { version = "0.1.0", path = "../litebox_platform_linux_userland" } litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows" } diff --git a/litebox_runner_windows_on_linux_userland/src/lib.rs b/litebox_runner_windows_on_linux_userland/src/lib.rs index 4d214c1ca2..52fe9ce671 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,35 +73,34 @@ 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 = shim_builder.brokered_fs(); let initial_file_system = std::sync::Arc::new(initial_file_system); let shim = shim_builder.build(); diff --git a/litebox_runner_windows_userland/Cargo.toml b/litebox_runner_windows_userland/Cargo.toml index 49dce8f25a..05a3e888ca 100644 --- a/litebox_runner_windows_userland/Cargo.toml +++ b/litebox_runner_windows_userland/Cargo.toml @@ -12,7 +12,7 @@ litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } litebox_platform_windows_userland = { version = "0.1.0", path = "../litebox_platform_windows_userland" } litebox_shim_windows = { version = "0.1.0", path = "../litebox_shim_windows" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = ["backend_tracing"] } -memmap2 = "0.9.8" + tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } [dev-dependencies] diff --git a/litebox_runner_windows_userland/src/lib.rs b/litebox_runner_windows_userland/src/lib.rs index 09209d71a6..5ec99ef557 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,53 +66,30 @@ pub fn run(cli_args: CliArgs) -> Result { ) .init(); - let tar_file = &cli_args.initial_files; - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - anyhow::bail!("Expected a .tar file, found {}", tar_file.display()); - } - let tar_data = mmapped_file(tar_file)?; - let platform = WindowsUserland::new(); - 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 = shim_builder.brokered_fs(); let initial_file_system = std::sync::Arc::new(initial_file_system); let shim = shim_builder.build(); diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index ca277428f5..6352a3bd65 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -25,6 +25,7 @@ aarch64_virtualize_x18 = [] [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_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 9de7fc2be1..4facde6970 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; @@ -61,8 +60,7 @@ mod wait; pub type DefaultFS = LinuxFS; -pub(crate) type LinuxFS = - litebox::fs::resolver::Resolver; +pub(crate) type LinuxFS = litebox::fs::resolver::Resolver; pub(crate) type FileFd = litebox::fd::TypedFd>; @@ -224,13 +222,10 @@ 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) + /// Creates a filesystem facade backed by the negotiated broker. + #[must_use] + pub fn brokered_fs(&self) -> DefaultFS { + litebox::fs::resolver::Resolver::new_brokered(&self.litebox) } /// Build the shim. @@ -372,28 +367,6 @@ 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); diff --git a/litebox_shim_linux/src/stdio.rs b/litebox_shim_linux/src/stdio.rs index ab4c971980..38f4ace2bd 100644 --- a/litebox_shim_linux/src/stdio.rs +++ b/litebox_shim_linux/src/stdio.rs @@ -10,10 +10,7 @@ mod tests { 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 crate::{UserPtrMut, syscalls::tests::init_platform_with_broker}; fn termios() -> Termios { Termios { @@ -28,7 +25,7 @@ mod tests { #[test] fn test_stdio() { - let task = init_platform(); + let task = init_platform_with_broker(); // Check that the stdio streams are in the file table let stdin_stat = task.sys_fstat(0).unwrap(); @@ -74,7 +71,7 @@ mod tests { #[test] fn test_stdio_flags_with_dup() { - let task = init_platform(); + let task = init_platform_with_broker(); let stdin = 0; let flags = task.sys_fcntl(stdin, FcntlArg::GETFL).unwrap(); @@ -127,17 +124,6 @@ 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(); diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index a3e39721b9..b641a4de07 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -102,15 +102,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/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 0d563a6105..f2760ec232 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -812,15 +812,12 @@ impl Task { 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,6 +949,23 @@ 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) { @@ -969,6 +983,7 @@ impl Task { if file_size < HEADER_SIZE { return (false, 0, 0, 0); } + let mut tail = [0u8; HEADER_SIZE]; match self.sys_read(fd, &mut tail, Some(file_size - HEADER_SIZE)) { Ok(n) if n == HEADER_SIZE => {} @@ -1163,12 +1178,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. diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index e70502fdc4..6d08fbe386 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -1,32 +1,30 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use alloc::boxed::Box; use litebox::fs::{Mode, OFlags}; use litebox_broker_core::{ - AssociationCancellation, BrokerCore, BrokerError, BrokerSession, CallerCredential, - ObjectRights, PolicyEngine, SessionId, + AssociationCancellation, BrokerCore, BrokerError, ObjectRights, PolicyEngine, SessionId, random::{RandomProvider, RandomProviderError}, - readiness::ReadinessRegistration, + readiness::{ReadinessRegistration, ReadinessSink}, socket::{PlatformSocket, SocketProvider}, stdio::{StdioProvider, StdioProviderError}, }; +use litebox_broker_host::{BrokerHostAssociation, BrokerHostError}; 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}, + BROKER_PROTOCOL_VERSION, ObjectHandle, + message::{BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse}, + readiness::ReadinessFlags, shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}, socket::CreateSocketRequest, - stdio::{IsTerminalStdioResponse, StdioOutputStream, StdioStream}, + stdio::{StdioOutputStream, StdioStream}, }; use litebox_broker_transport::{ - channel::{LocalCallChannel, LocalSetupChannel}, + channel::{HostReceive, HostSetupChannel, LocalCallChannel, LocalSetupChannel, PeerCredential}, shared_memory::{SharedBufferPool, SharedMemory, SharedMemoryError}, }; -use litebox_common_linux::{AtFlags, EfdFlags, FcntlArg, FileDescriptorFlags, errno::Errno}; +use litebox_common_linux::{AtFlags, FcntlArg, FileDescriptorFlags, errno::Errno}; use zerocopy::FromBytes as _; use crate::UserPtrMut; @@ -60,12 +58,37 @@ pub(crate) fn test_platform() -> &'static TestPlatform { fn test_broker() -> &'static BrokerCore { static BROKER: std::sync::OnceLock = std::sync::OnceLock::new(); BROKER.get_or_init(|| { + let in_mem = litebox_broker_core::fs::in_mem::InMem::::new_initialized([( + "/", + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode: litebox_broker_core::fs::Mode::RWXU + | litebox_broker_core::fs::Mode::RWXG + | litebox_broker_core::fs::Mode::RWXO, + owner: litebox_broker_core::fs::UserInfo::ROOT, + }, + )]); + let backend = litebox_broker_core::fs::composer::Composer::builder() + .mount_nestable("/", |allocators| { + litebox_broker_core::fs::overlay::Overlay::::new( + in_mem, + litebox_broker_core::fs::tar_ro::TarRo::new( + TEST_TAR_FILE.into(), + allocators.next(), + ), + allocators.next(), + ) + }) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .unwrap(); 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), + alloc::sync::Arc::new( + litebox_broker_core::fs::resolver::Resolver::::new(backend), + ), ) .unwrap() }) @@ -98,9 +121,7 @@ impl StdioProvider for TestStdioProvider { #[must_use] pub(crate) fn init_platform() -> crate::Task { - let platform = test_platform(); - - init_platform_with_builder(crate::LinuxShimBuilder::new(platform)) + init_platform_with_broker() } #[must_use] @@ -119,43 +140,41 @@ pub(crate) fn init_platform_with_broker() -> crate::Task { 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())); + let fs = alloc::sync::Arc::new(shim_builder.brokered_fs()); 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, + 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( + test_broker(), + &mut setup, shared_buffers, - } + alloc::sync::Arc::new(TestReadinessSink), + |_| Ok(()), + ) + .unwrap() + .unwrap(); + TestBrokerChannel { association } } } impl LocalSetupChannel for TestBrokerSetup { - type Error = core::convert::Infallible; + type Error = BrokerHostError; fn send_handshake_request( &mut self, @@ -175,86 +194,75 @@ impl LocalSetupChannel for TestBrokerSetup { } struct TestBrokerChannel { - session: BrokerSession, - shared_buffers: SharedBufferPool>, + association: BrokerHostAssociation<'static, alloc::sync::Arc>, } -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 = 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")) } } -impl LocalCallChannel for TestBrokerChannel { +struct TestHostSetup; + +impl HostSetupChannel for TestHostSetup { 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, - }) + 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 TestSharedMemory(std::sync::Mutex>); @@ -444,16 +452,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,13 +473,6 @@ 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(); diff --git a/litebox_shim_linux/src/transport.rs b/litebox_shim_linux/src/transport.rs index 45c7fe3512..243fa62bf3 100644 --- a/litebox_shim_linux/src/transport.rs +++ b/litebox_shim_linux/src/transport.rs @@ -70,17 +70,14 @@ impl ShimTransport { 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) { @@ -89,7 +86,7 @@ impl ShimTransport { core::hint::spin_loop(); check_progress = true; } - Err(e) => return Err(Errno::from(e)), + Err(error) => return Err(Errno::from(error)), } } @@ -109,11 +106,8 @@ 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(ChannelReadError::WouldBlock) => core::hint::spin_loop(), + Ok(read) => return Ok(read), Err(_) => return Err(transport::ReadError), } } @@ -124,256 +118,10 @@ 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(); - } + Ok(written) => return Ok(written), + Err(ChannelWriteError::BufferFull) => 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 7075fb8887..3b4fe97217 100644 --- a/litebox_shim_windows/Cargo.toml +++ b/litebox_shim_windows/Cargo.toml @@ -20,9 +20,19 @@ 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_protocol = { path = "../litebox_broker_protocol/", 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_protocol = { path = "../litebox_broker_protocol/", 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 d03096081d..05c35d9f9d 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; @@ -65,6 +64,8 @@ mod nt_types; mod syscalls; mod wait; +#[cfg(test)] +mod test_broker; #[cfg(test)] mod tests; @@ -217,8 +218,7 @@ impl Clone for WindowsSectionView { pub type DefaultFS = WindowsFS; -pub type WindowsFS = - litebox::fs::resolver::Resolver; +pub type WindowsFS = litebox::fs::resolver::Resolver; fn write_value(address: usize, value: T) -> Option<()> where @@ -402,11 +402,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,14 +413,10 @@ impl WindowsShimBuilder { &self.litebox } - /// Build the default file system with the given in-memory layer and tar data. + /// Creates a filesystem facade backed by the negotiated broker. #[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) + pub fn brokered_fs(&self) -> DefaultFS { + litebox::fs::resolver::Resolver::new_brokered(&self.litebox) } #[must_use] @@ -3183,27 +3174,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 779083de76..b31edc5016 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -2544,17 +2544,20 @@ mod tests { fn created_process_environment_snapshot() -> CreatedProcessEnvironmentSnapshot { let platform = crate::tests::test_platform(); - let litebox = litebox::LiteBox::new(platform); + let litebox = crate::test_broker::litebox( + platform, + alloc::vec![( + "/".into(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode: litebox_broker_core::fs::Mode::RWXU + | litebox_broker_core::fs::Mode::RWXG + | litebox_broker_core::fs::Mode::RWXO, + owner: litebox_broker_core::fs::UserInfo::ROOT, + }, + )], + ); 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 fs = Arc::new(litebox::fs::resolver::Resolver::new_brokered(&litebox)); let loader = PeLoader::new(platform, fs, &page_manager); let image = loaded_module_image(application_module_base()); diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 55e6c288cc..b305acb41d 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -48,8 +48,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; diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index ee376cfb1f..fba9a70f98 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -58,8 +58,7 @@ use crate::nt_types::{ read_unicode_string_at, }; -type RegistryFileSystem = - litebox::fs::resolver::Resolver; +type RegistryFileSystem = litebox::fs::resolver::Resolver; pub(crate) struct RegistryKeySubsystem(PhantomData); @@ -603,30 +602,7 @@ 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 = litebox::fs::resolver::Resolver::new_brokered(litebox); let fs_context = litebox::fs::resolver::Context::new(); { let fs = &fs; @@ -836,14 +812,9 @@ impl RegistryStore { ) .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 result = read_exact_at(&self.fs, &fd, &mut data); let _ = self.fs.close(&fd); - if read != data.len() { - return Err(NtStatus::UNSUCCESSFUL); - } + result?; let value_type = u32::from_le_bytes( data[..REGISTRY_VALUE_TYPE_SIZE] @@ -2262,19 +2233,45 @@ fn write_value_at_path( 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 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(&fd); + result +} + +fn read_exact_at( + fs: &RegistryFileSystem, + fd: &TypedFd>, + mut data: &mut [u8], +) -> Result<(), NtStatus> { + let mut offset = 0; + while !data.is_empty() { + let read = fs.read(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: &RegistryFileSystem, + fd: &TypedFd>, + mut data: &[u8], + mut offset: usize, +) -> Result<(), NtStatus> { + while !data.is_empty() { + let written = fs.write(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(()) } @@ -2493,7 +2490,28 @@ mod tests { } fn test_registry() -> (LiteBox, RegistryStore) { - let litebox = LiteBox::new(test_platform()); + let mode = litebox_broker_core::fs::Mode::RWXU + | litebox_broker_core::fs::Mode::RWXG + | litebox_broker_core::fs::Mode::RWXO; + let litebox = crate::test_broker::litebox( + test_platform(), + alloc::vec![ + ( + "/".into(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, + }, + ), + ( + "/registry".into(), + litebox_broker_core::fs::in_mem::InitialNode::Directory { + mode, + owner: litebox_broker_core::fs::UserInfo::ROOT, + }, + ), + ], + ); let registry = RegistryStore::new(&litebox); (litebox, registry) } @@ -2665,7 +2683,7 @@ 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(); @@ -2689,6 +2707,15 @@ mod tests { assert_eq!(value.value_type, u32::from(RegistryValueType::Sz)); assert_eq!(value.data, DEFAULT_ACP_VALUE); + let broker_fs = litebox::fs::resolver::Resolver::new_brokered(&litebox); + assert_eq!( + broker_fs + .file_status(&litebox::fs::resolver::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", ); diff --git a/litebox_shim_windows/src/test_broker.rs b/litebox_shim_windows/src/test_broker.rs new file mode 100644 index 0000000000..a80fd58abe --- /dev/null +++ b/litebox_shim_windows/src/test_broker.rs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +extern crate std; + +use alloc::{boxed::Box, 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, 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; + +pub(crate) fn litebox( + platform: &'static TestPlatform, + entries: Vec<(alloc::string::String, InitialNode)>, +) -> litebox::LiteBox { + static BROKER: std::sync::OnceLock = std::sync::OnceLock::new(); + let broker = BROKER.get_or_init(|| { + 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(); + BrokerCore::new( + PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), + Arc::new(UnsupportedSocketProvider), + Arc::new(UnusedRandomProvider), + Arc::new(UnsupportedStdioProvider), + Arc::new(Resolver::::new(fs)), + ) + .unwrap() + }); + + 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: &'static BrokerCore, + memory: Arc, +} + +impl TestBrokerSetup { + fn new(broker: &'static 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 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(()) + } +} + +struct UnusedRandomProvider; + +impl RandomProvider for UnusedRandomProvider { + fn fill(&self, _output: &mut [u8]) -> core::result::Result<(), RandomProviderError> { + Err(RandomProviderError) + } +} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 987915a1ac..365b030e75 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -6,9 +6,9 @@ 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::{Mode, UserInfo, in_mem::InitialNode}; use crate::nt_types::{ObjectAttributes, UnicodeString}; use crate::syscalls::Handle; @@ -111,67 +111,48 @@ pub(crate) fn test_task() -> Task { 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"); - } + 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 !nls_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(nls_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(), + }, + ) + })); + + let litebox = crate::test_broker::litebox(platform, entries); + let shim_builder = + crate::WindowsShimBuilder::::new_with_litebox(platform, litebox); + let fs = Arc::new(shim_builder.brokered_fs()); + let fs_context = litebox::fs::resolver::Context::new(); let shim = shim_builder.build(); let WindowsShim(global) = shim; From d4651c1faee1f010e14a71c8d3cc8c957f05a3f5 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 8 Sep 2026 14:49:37 -0700 Subject: [PATCH 02/34] Fix brokered runner CI tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- .../tests/common/mod.rs | 7 ++++++- .../tests/loader.rs | 11 +++-------- .../tests/rewritten_guests.rs | 12 +++++------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs index 37919a4931..c25713c32d 100644 --- a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs +++ b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs @@ -92,7 +92,12 @@ impl TestLauncher { Mode::RWXG | Mode::RWXO | Mode::RWXU, ) .unwrap(); - self.fs.write(&fd, &contents, None).unwrap(); + let mut written = 0; + while written < contents.len() { + let count = self.fs.write(&fd, &contents[written..], None).unwrap(); + assert_ne!(count, 0, "broker made no progress installing {out}"); + written += count; + } self.fs.close(&fd).unwrap(); } diff --git a/litebox_runner_linux_on_windows_userland/tests/loader.rs b/litebox_runner_linux_on_windows_userland/tests/loader.rs index bb79fd60a2..886f75a3ed 100644 --- a/litebox_runner_linux_on_windows_userland/tests/loader.rs +++ b/litebox_runner_linux_on_windows_userland/tests/loader.rs @@ -344,11 +344,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 +359,9 @@ 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("--runner").arg(runner).args(&args); println!("Running `{command:?}`"); let status = command .status() diff --git a/litebox_runner_linux_userland/tests/rewritten_guests.rs b/litebox_runner_linux_userland/tests/rewritten_guests.rs index a636f54843..3d0b5d17ec 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()); @@ -154,13 +158,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 ({}): {}", From 94cf578d76f34094ecbe8a1a3151b610e26b2ab3 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 8 Sep 2026 15:02:10 -0700 Subject: [PATCH 03/34] Enable x18 rewriting in brokered test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_runner_linux_userland/tests/rewritten_guests.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/litebox_runner_linux_userland/tests/rewritten_guests.rs b/litebox_runner_linux_userland/tests/rewritten_guests.rs index 3d0b5d17ec..4953435694 100644 --- a/litebox_runner_linux_userland/tests/rewritten_guests.rs +++ b/litebox_runner_linux_userland/tests/rewritten_guests.rs @@ -12,10 +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) + run_rewritten_target(&target, false) } -fn run_rewritten_target(target: &std::path::Path) -> std::process::Output { +fn run_rewritten_target(target: &std::path::Path, virtualize_x18: bool) -> 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()); @@ -29,6 +29,9 @@ fn run_rewritten_target(target: &std::path::Path) -> std::process::Output { broker_path.display() ); let mut command = std::process::Command::new(broker_path); + if virtualize_x18 { + command.arg("--fs-virtualize-x18"); + } command.arg("--runner").arg(&binary_path); command }; @@ -158,7 +161,7 @@ fn test_x18_virtualization() { true, true, ); - let output = run_rewritten_target(&target); + let output = run_rewritten_target(&target, true); assert!( output.status.success(), "x18 fixture failed ({}): {}", From feb275a6e7e54d1716a959786ce7957dc35c47cc Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 8 Sep 2026 17:26:35 -0700 Subject: [PATCH 04/34] Move filesystem tests to their broker-core owners Filesystem resolution, backends, and the 9P client now live in litebox_broker_core, so their tests move with them and exercise the broker-core Resolver and backends directly. The ported suites no longer build a BrokerCore, a host/local transport pair, shared memory, or guest descriptors, which lets `litebox` drop its test-only broker harness and the compatibility module paths that existed only for these tests. - litebox_broker_core: add `fs::tests` (in-memory, tar, overlay, devices, and composed backends), `fs::nine_p::tests`, and the `fs::test_support` facade that pairs a resolver with session-shaped device I/O; share one `test_platform` between the fs and session tests. - litebox: replace `fs::tests` with focused guest-facade tests that script broker responses over a local channel, covering context path resolution, value conversion, descriptor lifetime, and broker error mapping. - Delete `litebox::test_broker` (and its `OnceLock`), the test-only `fs` re-export modules, and the now-unused dev-dependencies. - Run litebox_broker_core on the AArch64 CI job so the 9P coverage that moved out of `litebox` still runs there. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- .github/workflows/ci.yml | 1 + Cargo.lock | 3 +- dev_tests/src/ratchet.rs | 2 +- litebox/Cargo.toml | 4 - litebox/src/fs/mod.rs | 94 +- litebox/src/fs/tests.rs | 2817 ++++------------- litebox/src/lib.rs | 2 - litebox/src/test_broker.rs | 228 -- litebox_broker_core/Cargo.toml | 4 + litebox_broker_core/src/fs/mod.rs | 4 + litebox_broker_core/src/fs/nine_p/mod.rs | 2 + .../src/fs/nine_p/tests.rs | 399 +-- .../src/fs/test.tar | Bin litebox_broker_core/src/fs/test_support.rs | 239 ++ litebox_broker_core/src/fs/tests.rs | 2039 ++++++++++++ litebox_broker_core/src/lib.rs | 2 + litebox_broker_core/src/session.rs | 80 +- litebox_broker_core/src/test_platform.rs | 76 + litebox_shim_linux/src/syscalls/tests.rs | 2 +- 19 files changed, 3152 insertions(+), 2846 deletions(-) delete mode 100644 litebox/src/test_broker.rs rename {litebox => litebox_broker_core}/src/fs/nine_p/tests.rs (71%) rename {litebox => litebox_broker_core}/src/fs/test.tar (100%) create mode 100644 litebox_broker_core/src/fs/test_support.rs create mode 100644 litebox_broker_core/src/fs/tests.rs create mode 100644 litebox_broker_core/src/test_platform.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 705dc01fd0..e3799535b2 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 diff --git a/Cargo.lock b/Cargo.lock index 4cc6179817..82f163ef0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1450,7 +1450,6 @@ dependencies = [ "buddy_system_allocator", "hashbrown", "litebox_broker_core", - "litebox_broker_host", "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", @@ -1460,7 +1459,6 @@ dependencies = [ "slabmalloc", "smallvec", "spin 0.9.8", - "tempfile", "thiserror", "windows-sys 0.60.2", "zerocopy", @@ -1477,6 +1475,7 @@ dependencies = [ "litebox_util_log", "spin 0.9.8", "tar-no-std", + "tempfile", "thiserror", ] diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index d334fa0282..e2d3bb5c4a 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -37,7 +37,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_broker_core/", 1), ("litebox_broker_transport_linux_userland/", 1), ("litebox_broker_userland/", 1), - ("litebox/", 8), + ("litebox/", 7), ("litebox_platform/", 2), ("litebox_platform_linux_kernel/", 5), ("litebox_platform_linux_userland/", 5), diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index a457dea176..219c915ac0 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -37,7 +37,3 @@ enforce_singleton_litebox_instance = [] [lints] workspace = true - -[dev-dependencies] -litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } -tempfile = "3" diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index 43eef7402e..790c056c13 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -4,7 +4,8 @@ //! Guest-facing filesystem facade. //! //! Filesystem resolution and backend implementations live in `litebox_broker_core`. This module -//! retains LiteBox's guest values, descriptor integration, and compatibility module paths. +//! retains LiteBox's guest values and descriptor integration, plus the 9P transport traits that +//! deployments implement. use bitflags::bitflags; @@ -14,105 +15,14 @@ use core::num::NonZeroUsize; pub mod errors; pub mod resolver; -#[doc(hidden)] -#[cfg(test)] -pub mod backend { - pub use litebox_broker_core::fs::backend::*; -} - -#[doc(hidden)] -#[cfg(test)] -pub mod composer { - pub use litebox_broker_core::fs::composer::*; -} - -#[doc(hidden)] -#[cfg(test)] -pub mod devices { - pub use litebox_broker_core::fs::devices::*; -} - -#[doc(hidden)] -#[cfg(test)] -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)] -#[cfg(test)] -pub mod overlay { - pub use litebox_broker_core::fs::overlay::*; -} - -#[doc(hidden)] -#[cfg(test)] -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)] diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 9e55921bc9..32f4e629f1 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -1,2284 +1,667 @@ // 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::test_broker::brokered_fs( - litebox, - crate::fs::tar_ro::TarRo::new( - tar_data, - crate::fs::inode_allocator::InodeAllocator::standalone(), - ), - ) +//! Guest-facing filesystem facade tests. +//! +//! Filesystem resolution, backend, and 9P semantics belong to `litebox_broker_core` and are tested +//! there. What LiteBox owns is the guest side of the boundary: resolving paths against a +//! [`Context`], converting between guest and protocol values, mapping broker errors onto guest +//! error types, and tying broker-owned files to guest descriptors. These tests script broker +//! responses over a local channel, so no broker core, policy engine, or host transport is +//! involved. + +extern crate std; + +use alloc::collections::VecDeque; +use alloc::string::{String, ToString as _}; +use alloc::sync::Arc; +use alloc::vec; +use alloc::vec::Vec; +use std::sync::Mutex; + +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, + FileSeekWhence, FileStatus as BrokerFileStatus, FileType as BrokerFileType, FileUser, + MAX_FILE_TRANSFER_SIZE, OpenFileResponse, ReadDirectoryResponse, ReadFileResponse, + SeekFileResponse, WriteFileResponse, encode_directory_entries_chunk, +}; +use litebox_broker_protocol::message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, + BrokerResponse, BrokerResult, FileRequest, FileResponse, +}; +use litebox_broker_protocol::shared_buffer::{ + SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferDescriptor, +}; +use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; +use litebox_broker_transport::channel::{LocalCallChannel, LocalSetupChannel}; +use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory, SharedMemoryError}; + +use crate::fs::errors::{ + OpenError, PathError, ReadDirError, ReadError, RmdirError, UnlinkError, WriteError, +}; +use crate::fs::resolver::{Context, Resolver}; +use crate::fs::{FileType, Mode, OFlags, SeekWhence, UserInfo}; +use crate::platform::mock::MockPlatform; + +/// The handle the scripted broker hands out for every successful open. +const FILE_HANDLE: ObjectHandle = ObjectHandle(7); + +/// One broker request the facade issued, with any shared-buffer payload copied out. +#[derive(Debug, PartialEq, Eq)] +enum Call { + 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, + }, + Seek { + handle: ObjectHandle, + offset: i64, + whence: FileSeekWhence, + }, + ReadDirectory { + handle: ObjectHandle, + start_index: u64, + }, + PathStatus { + path: String, + user: FileUser, + }, + Unlink { + path: String, + user: FileUser, + }, + Rmdir { + path: String, + user: FileUser, + }, + Close(ObjectHandle), } -type InMemFs = crate::fs::resolver::Resolver; - -fn in_mem_fs(litebox: &crate::LiteBox) -> InMemFs { - crate::test_broker::brokered_fs( - 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( - 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); +/// One scripted broker answer. +enum Scripted { + /// Reply with this file response verbatim. + Reply(FileResponse), + /// Stage `data` in the request's shared buffer and report it as read. + Read(Vec), + /// Answer directory reads from `entries`, at most `page_bytes` of them per response. + Directory { + entries: Vec, + page_bytes: usize, + }, } -/// 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; - -/// 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::test_broker::brokered_fs( - 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(), - ), - ) +/// A broker that records requests and answers them from a script. +struct ScriptedBroker { + buffers: SharedBufferPool>, + calls: Mutex>, + script: Mutex>, } -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" - ); +impl ScriptedBroker { + fn new(script: impl IntoIterator) -> Arc { + let memory = Arc::new(TestSharedMemory::new()); + Arc::new(Self { + buffers: SharedBufferPool::new(memory, SHARED_BUFFER_LAYOUT).unwrap(), + calls: Mutex::new(Vec::new()), + script: Mutex::new(script.into_iter().collect()), + }) } - #[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"); + /// Every request observed so far, oldest first. + fn calls(&self) -> std::sync::MutexGuard<'_, Vec> { + self.calls.lock().unwrap() } - #[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" - ); + fn record(&self, call: Call) { + self.calls.lock().unwrap().push(call); } - #[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"); - }); + /// The UTF-8 path staged in `descriptor` by the guest. + fn staged_path(&self, descriptor: SharedBufferDescriptor) -> String { + let mut bytes = vec![0; descriptor.length as usize]; + self.buffers + .read(descriptor.slot_index, &mut bytes) + .unwrap(); + String::from_utf8(bytes).expect("guest must stage a UTF-8 path") } - #[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); + fn staged_data(&self, descriptor: SharedBufferDescriptor) -> Vec { + let mut bytes = vec![0; descriptor.length as usize]; + self.buffers + .read(descriptor.slot_index, &mut bytes) + .unwrap(); + bytes + } + + fn next_scripted(&self) -> Scripted { + self.script + .lock() + .unwrap() + .pop_front() + .expect("test script must answer every file request") + } + + fn file_response(&self, request: FileRequest) -> FileResponse { + match request { + FileRequest::Open(request) => { + self.record(Call::Open { + path: self.staged_path(request.path), + user: request.user, + access: request.access, + flags: request.flags, + mode: request.mode, + }); + self.reply() + } + FileRequest::Read(request) => { + self.record(Call::Read { + handle: request.handle, + length: request.buffer.length, + offset: request.offset, + }); + match self.next_scripted() { + Scripted::Reply(response) => response, + Scripted::Read(data) => { + assert!(data.len() <= request.buffer.length as usize); + self.buffers + .write(request.buffer.slot_index, &data) + .unwrap(); + FileResponse::Read(ReadFileResponse { + read: u32::try_from(data.len()).unwrap(), + }) } - _ => 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. + Scripted::Directory { .. } => panic!("scripted directory answer for a read"), } } - - // 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, + FileRequest::Write(request) => { + self.record(Call::Write { + handle: request.handle, + data: self.staged_data(request.buffer), + offset: request.offset, + }); + self.reply() + } + FileRequest::Seek(request) => { + self.record(Call::Seek { + handle: request.handle, + offset: request.offset, + whence: request.whence, + }); + self.reply() + } + FileRequest::ReadDirectory(request) => { + self.record(Call::ReadDirectory { + handle: request.handle, + start_index: request.start_index, + }); + let scripted = self.next_scripted(); + let Scripted::Directory { + entries, + page_bytes, + } = scripted + else { + let Scripted::Reply(response) = scripted else { + panic!("scripted read answer for a directory read") + }; + return response; + }; + let (payload, next_index) = encode_directory_entries_chunk( + &entries, + usize::try_from(request.start_index).unwrap(), + page_bytes.min(request.buffer.length as usize), ) - .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 + .expect("directory entries must encode"); + self.buffers + .write(request.buffer.slot_index, &payload) + .unwrap(); + if next_index.is_some() { + // The guest asks again from the continuation index, so keep answering. + self.script.lock().unwrap().push_front(Scripted::Directory { + entries, + page_bytes, + }); } - Ok(()) => panic!("Non-owner should not be able to chown"), - Err(e) => panic!("Unexpected error: {e:?}"), + FileResponse::ReadDirectory(ReadDirectoryResponse { + length: u32::try_from(payload.len()).unwrap(), + next_index, + }) } - }); - - // 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 + FileRequest::PathStatus(request) => { + self.record(Call::PathStatus { + path: self.staged_path(request.path), + user: request.user, + }); + self.reply() + } + FileRequest::Unlink(request) => { + self.record(Call::Unlink { + path: self.staged_path(request.path), + user: request.user, + }); + self.reply() + } + FileRequest::Rmdir(request) => { + self.record(Call::Rmdir { + path: self.staged_path(request.path), + user: request.user, + }); + self.reply() } - Ok(()) => panic!("Should not be able to chown non-existent file"), - Err(e) => panic!("Unexpected error: {e:?}"), + request => panic!("unscripted file request: {request:?}"), } - - // 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"); + fn reply(&self) -> FileResponse { + match self.next_scripted() { + Scripted::Reply(response) => response, + Scripted::Read(_) | Scripted::Directory { .. } => { + panic!("scripted payload answer for a request that carries none") + } + } } } -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 - )) - )); +/// The local end of the scripted broker connection. +struct ScriptedChannel(Arc); - // 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 - )) - )); +impl LocalSetupChannel for ScriptedChannel { + type Error = core::convert::Infallible; - // 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 - )) - )); + fn send_handshake_request( + &mut self, + request: &BrokerHandshakeRequest, + ) -> Result<(), Self::Error> { + assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); + Ok(()) } - #[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) - )); - } + fn recv_handshake_response(&mut self) -> Result, Self::Error> { + Ok(Some(BrokerHandshakeResponse::Negotiated { + broker_protocol_version: BROKER_PROTOCOL_VERSION, + })) } +} - #[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"]); +impl LocalCallChannel for ScriptedChannel { + type Error = core::convert::Infallible; - // 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. + fn call(&self, request: BrokerRequest) -> Result { + let result = match request.operation { + BrokerOperation::File(file) => BrokerResult::File(self.0.file_response(file)), + BrokerOperation::CloseObject(handle) => { + self.0.record(Call::Close(handle)); + BrokerResult::ObjectClosed } - } - - // 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!( + operation => panic!("unscripted broker operation: {operation:?}"), + }; + Ok(BrokerResponse { + request_id: request.request_id, 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"); +/// Shared memory backed by an ordinary allocation, since no peer process observes it. +struct TestSharedMemory(Mutex>); - fs.close(&fd1).expect("Failed to close file"); - fs.close(&fd2).expect("Failed to close file"); +impl TestSharedMemory { + fn new() -> Self { + Self(Mutex::new(vec![0; SHARED_BUFFER_POOL_SIZE])) } +} - /// 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 - )), - )); +impl SharedMemory for TestSharedMemory { + fn len(&self) -> usize { + SHARED_BUFFER_POOL_SIZE } - #[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""), - }, - ), - ]), + fn read(&self, offset: usize, destination: &mut [u8]) -> Result<(), SharedMemoryError> { + let memory = self.0.lock().unwrap(); + let end = offset + .checked_add(destination.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + destination.copy_from_slice( + memory + .get(offset..end) + .ok_or(SharedMemoryError::InvalidRange)?, ); - - // 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 - )) - )); + Ok(()) } - #[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"); + fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { + let mut memory = self.0.lock().unwrap(); + let end = offset + .checked_add(source.len()) + .ok_or(SharedMemoryError::InvalidRange)?; + memory + .get_mut(offset..end) + .ok_or(SharedMemoryError::InvalidRange)? + .copy_from_slice(source); + Ok(()) } +} - #[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([])); +/// Build a guest filesystem facade whose broker answers from `script`. +fn scripted_fs( + script: impl IntoIterator, +) -> (Arc, Resolver) { + let broker = ScriptedBroker::new(script); + let memory = Arc::clone(broker.buffers.memory()); + let (local, ()) = BrokerLocal::negotiate(ScriptedChannel(Arc::clone(&broker)), |channel| { + Ok((channel, memory as Arc, ())) + }) + .unwrap(); + let litebox = crate::LiteBox::new_with_broker_local(MockPlatform::new(), local); + let fs = Resolver::new_brokered(&litebox); + (broker, fs) +} - // 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"); +fn opened() -> Scripted { + Scripted::Reply(FileResponse::Open(OpenFileResponse { + handle: FILE_HANDLE, + })) +} - // 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" - ); +fn user(context: &Context) -> FileUser { + FileUser { + user: context.acting_user().user, + group: context.acting_user().group, } +} - #[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); +#[test] +fn context_resolves_paths_against_the_cwd() { + let mut context = Context::new(); + assert_eq!(context.cwd().to_string(), "/"); + assert_eq!(context.resolve("a/b/../c").unwrap().to_string(), "/a/c"); + + context.set_cwd(context.resolve("/work/dir").unwrap()); + assert_eq!(context.cwd().to_string(), "/work/dir"); + assert_eq!( + context.resolve("./file").unwrap().to_string(), + "/work/dir/file" + ); + assert_eq!( + context.resolve("../file").unwrap().to_string(), + "/work/file" + ); + assert_eq!( + context.resolve("/etc//passwd").unwrap().to_string(), + "/etc/passwd" + ); +} - let mut names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect(); - names.sort_unstable(); - assert_eq!( - names, - vec![".", "..", "bar", "foo", "upperdir", "upperfile"] - ); +#[test] +fn open_sends_the_resolved_path_and_translated_flags() { + let mut context = Context::new(); + context.set_cwd(context.resolve("/work").unwrap()); + context.set_acting_user(UserInfo { user: 7, group: 9 }); + let (broker, fs) = scripted_fs([opened()]); + + let fd = fs + .open( + &context, + "sub/../file.txt", + OFlags::CREAT | OFlags::WRONLY | OFlags::APPEND, + Mode::RWXU, + ) + .expect("open should succeed"); + + assert_eq!( + *broker.calls(), + vec![Call::Open { + path: String::from("/work/file.txt"), + user: FileUser { user: 7, group: 9 }, + access: FileAccessMode::WriteOnly, + flags: FileOpenFlags::CREATE | FileOpenFlags::APPEND, + mode: FileMode::from_bits(0o700).unwrap(), + }] + ); + fs.close(&fd).expect("close should succeed"); +} - // 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. - } +#[test] +fn read_and_write_transfer_payloads_through_the_broker() { + let context = Context::new(); + let (broker, fs) = scripted_fs([ + opened(), + Scripted::Reply(FileResponse::Write(WriteFileResponse { written: 5 })), + Scripted::Read(b"broker".to_vec()), + Scripted::Reply(FileResponse::Seek(SeekFileResponse { offset: 3 })), + ]); + + let fd = fs + .open(&context, "/file", OFlags::RDWR, Mode::empty()) + .expect("open should succeed"); + + assert_eq!(fs.write(&fd, b"hello", None).unwrap(), 5); + + let mut buffer = vec![0; 16]; + let read = fs.read(&fd, &mut buffer, Some(2)).unwrap(); + assert_eq!(&buffer[..read], b"broker"); + + assert_eq!(fs.seek(&fd, -3, SeekWhence::RelativeToEnd).unwrap(), 3); + fs.close(&fd).expect("close should succeed"); + + let calls = broker.calls(); + assert_eq!( + calls[1], + Call::Write { + handle: FILE_HANDLE, + data: b"hello".to_vec(), + offset: None, } - - // 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"); - } + ); + assert_eq!( + calls[2], + Call::Read { + handle: FILE_HANDLE, + length: 16, + offset: Some(2), + } + ); + assert_eq!( + calls[3], + Call::Seek { + handle: FILE_HANDLE, + offset: -3, + whence: FileSeekWhence::End, + } + ); + assert_eq!(calls[4], Call::Close(FILE_HANDLE)); } -mod stdio { - use crate::LiteBox; - use crate::fs::devices::Devices; - use crate::fs::errors::{ReadError, WriteError}; - 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 = crate::test_broker::brokered_fs( - &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 = crate::test_broker::brokered_fs( - &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 - )) - )); - } +#[test] +fn closing_releases_the_broker_object_and_the_descriptor() { + let context = Context::new(); + let (broker, fs) = scripted_fs([opened()]); + + let fd = fs + .open(&context, "/file", OFlags::RDONLY, Mode::empty()) + .expect("open should succeed"); + fs.close(&fd).expect("close should succeed"); + assert_eq!(broker.calls()[1], Call::Close(FILE_HANDLE)); + + // The descriptor no longer names a broker file, so operations on it report a closed fd + // instead of reaching the broker. + let mut buffer = [0; 4]; + assert!(matches!( + fs.read(&fd, &mut buffer, None), + Err(ReadError::ClosedFd) + )); + assert!(matches!( + fs.write(&fd, b"x", None), + Err(WriteError::ClosedFd) + )); + assert_eq!(broker.calls().len(), 2); } -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::{Mode, OFlags, UserInfo}; - use crate::platform::mock::MockPlatform; - use alloc::vec; - extern crate std; - - type ComposedFs = crate::fs::resolver::Resolver; +#[test] +fn path_status_converts_broker_values() { + let context = Context::new(); + let (broker, fs) = scripted_fs([Scripted::Reply(FileResponse::PathStatus( + BrokerFileStatus { + file_type: BrokerFileType::CharacterDevice, + mode: FileMode::from_bits(0o644).unwrap(), + size: 12, + owner: FileUser { user: 1, group: 2 }, + node_info: FileNodeInfo { + dev: 3, + ino: 4, + rdev: Some(5), + }, + block_size: 4096, + }, + ))]); + + let status = fs + .file_status(&context, "/dev/null") + .expect("status should succeed"); + + assert_eq!(status.file_type, FileType::CharacterDevice); + assert_eq!(status.mode, Mode::from_bits(0o644).unwrap()); + assert_eq!(status.size, 12); + assert_eq!(status.owner.user, 1); + assert_eq!(status.owner.group, 2); + assert_eq!(status.node_info.dev, 3); + assert_eq!(status.node_info.ino, 4); + assert_eq!( + status.node_info.rdev.map(core::num::NonZeroUsize::get), + Some(5) + ); + assert_eq!(status.blksize, 4096); + assert_eq!( + *broker.calls(), + vec![Call::PathStatus { + path: String::from("/dev/null"), + user: user(&context), + }] + ); +} - fn composed_fs(litebox: &LiteBox) -> ComposedFs { - crate::test_broker::brokered_fs( - 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 read_dir_reassembles_paged_broker_entries() { + let context = Context::new(); + let entries = vec![ + FileDirectoryEntry { + name: String::from("one"), + file_type: BrokerFileType::RegularFile, + node_info: Some(FileNodeInfo { + dev: 1, + ino: 2, + rdev: None, + }), + }, + FileDirectoryEntry { + name: String::from("two"), + file_type: BrokerFileType::Directory, + node_info: None, + }, + ]; + // A page that holds one entry, so the guest has to follow the continuation index. + let page_bytes = + encode_directory_entries_chunk(&entries[..1], 0, MAX_FILE_TRANSFER_SIZE as usize) + .unwrap() + .0 + .len(); + let (broker, fs) = scripted_fs([ + opened(), + Scripted::Directory { + entries, + page_bytes, + }, + ]); + + let fd = fs + .open( + &context, + "/dir", + OFlags::RDONLY | OFlags::DIRECTORY, + Mode::empty(), ) - } - - #[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"); + .expect("open should succeed"); + let entries = fs.read_dir(&fd).expect("read_dir should succeed"); + fs.close(&fd).expect("close should succeed"); + + assert_eq!(entries.len(), 2); + assert_eq!(entries[0].name, "one"); + assert_eq!(entries[0].file_type, FileType::RegularFile); + assert_eq!(entries[0].ino_info.as_ref().map(|node| node.ino), Some(2)); + assert_eq!(entries[1].name, "two"); + assert_eq!(entries[1].file_type, FileType::Directory); + assert!(entries[1].ino_info.is_none()); + + let calls = broker.calls(); + assert_eq!( + calls[1], + Call::ReadDirectory { + handle: FILE_HANDLE, + start_index: 0, + } + ); + assert_eq!( + calls[2], + Call::ReadDirectory { + handle: FILE_HANDLE, + start_index: 1, + } + ); +} - // 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 broker_file_errors_map_to_guest_errors() { + let context = Context::new(); + let (broker, fs) = scripted_fs([ + Scripted::Reply(FileResponse::Failed(FileError::NoSuchFileOrDirectory)), + Scripted::Reply(FileResponse::Failed(FileError::AccessNotAllowed)), + Scripted::Reply(FileResponse::Failed(FileError::NoWritePermissions)), + Scripted::Reply(FileResponse::Failed(FileError::NotEmpty)), + opened(), + Scripted::Reply(FileResponse::Failed(FileError::NotForReading)), + Scripted::Reply(FileResponse::Failed(FileError::NotDirectory)), + ]); + + assert!(matches!( + fs.open(&context, "/missing", OFlags::RDONLY, Mode::empty()), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) + )); + assert!(matches!( + fs.open(&context, "/secret", OFlags::RDONLY, Mode::empty()), + Err(OpenError::AccessNotAllowed) + )); + assert!(matches!( + fs.unlink(&context, "/locked/file"), + Err(UnlinkError::NoWritePerms) + )); + assert!(matches!( + fs.rmdir(&context, "/full"), + Err(RmdirError::NotEmpty) + )); + + let fd = fs + .open(&context, "/file", OFlags::WRONLY, Mode::empty()) + .expect("open should succeed"); + let mut buffer = [0; 4]; + assert!(matches!( + fs.read(&fd, &mut buffer, None), + Err(ReadError::NotForReading) + )); + assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); + fs.close(&fd).expect("close should succeed"); + + assert_eq!(broker.calls().len(), 8); } diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index 319837e113..0d87da5c1b 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -33,8 +33,6 @@ pub mod tls; // The core [`LiteBox`] object itself, re-exported here publicly, just to keep management of the // code cleaner. mod litebox; -#[cfg(test)] -mod test_broker; pub use litebox::LiteBox; // Explicitly-private, the utilities are not exposed to users of LiteBox, and are intended entirely diff --git a/litebox/src/test_broker.rs b/litebox/src/test_broker.rs deleted file mode 100644 index d111375200..0000000000 --- a/litebox/src/test_broker.rs +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -extern crate std; - -use alloc::{boxed::Box, sync::Arc, vec}; - -use litebox_broker_core::{ - BrokerCore, ObjectRights, PolicyEngine, - fs::{backend::Backend, resolver::Resolver as CoreResolver}, - 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, 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::platform::mock::MockPlatform; - -pub(crate) fn brokered_fs( - litebox: &crate::LiteBox, - backend: BackendType, -) -> crate::fs::resolver::Resolver { - static BROKER: std::sync::OnceLock = std::sync::OnceLock::new(); - let broker = BROKER.get_or_init(|| { - BrokerCore::new( - PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), - Arc::new(UnsupportedSocketProvider), - Arc::new(UnusedRandomProvider), - Arc::new(UnsupportedStdioProvider), - Arc::new(CoreResolver::::new(backend)), - ) - .unwrap() - }); - - let setup = TestBrokerSetup::new(broker); - let (broker_local, ()) = BrokerLocal::negotiate(setup, |setup| { - let memory: Arc = setup.memory.clone(); - Ok((setup.activate(), memory, ())) - }) - .unwrap(); - let litebox = crate::LiteBox::new_with_broker_local(litebox.x.platform, broker_local); - crate::fs::resolver::Resolver::new_brokered(&litebox) -} - -struct TestBrokerSetup { - broker: &'static BrokerCore, - memory: Arc, -} - -impl TestBrokerSetup { - fn new(broker: &'static 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 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(()) - } -} - -struct UnusedRandomProvider; - -impl RandomProvider for UnusedRandomProvider { - fn fill(&self, _output: &mut [u8]) -> core::result::Result<(), RandomProviderError> { - Err(RandomProviderError) - } -} diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index 178011dd5a..2716e9a3a1 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/mod.rs b/litebox_broker_core/src/fs/mod.rs index 1917f9a0bc..77c78143ef 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -25,6 +25,10 @@ pub mod overlay; pub mod resolver; mod service; pub mod tar_ro; +#[cfg(test)] +mod test_support; +#[cfg(test)] +mod tests; pub(crate) use service::File; pub use service::{ diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index c751fabe3a..6573df56f8 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -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; diff --git a/litebox/src/fs/nine_p/tests.rs b/litebox_broker_core/src/fs/nine_p/tests.rs similarity index 71% rename from litebox/src/fs/nine_p/tests.rs rename to litebox_broker_core/src/fs/nine_p/tests.rs index bc7b36acbb..12b6ea3cc2 100644 --- a/litebox/src/fs/nine_p/tests.rs +++ b/litebox_broker_core/src/fs/nine_p/tests.rs @@ -1,7 +1,11 @@ // 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 _}; @@ -13,18 +17,20 @@ use crate::fs::errors::{ TruncateError, UnlinkError, WriteError, }; use crate::fs::inode_allocator::InodeAllocator; -use crate::fs::{Mode, OFlags}; -use crate::platform::mock::MockPlatform; +use crate::fs::test_support::{Fs, USER}; +use crate::fs::{FileType, Mode, OFlags, SeekWhence}; +use crate::test_platform::TestPlatform; -use super::nine_p::{NineP, transport}; +use super::{NineP, transport}; -type NinePFs = crate::fs::resolver::Resolver; +/// A resolver over a 9P backend reached through `T`. +type NinePFs = Fs>; /// 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")) @@ -39,7 +45,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, } @@ -193,9 +199,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()); - crate::test_broker::brokered_fs(litebox, attach(transport, server)) + Fs::new(attach(transport, server)) } // --------------------------------------------------------------------------- @@ -204,15 +210,13 @@ fn connect_9p(litebox: &crate::LiteBox, server: &DiodServer) -> Ni #[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, @@ -220,10 +224,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(&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"); @@ -232,48 +238,48 @@ 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(&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(&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!( @@ -284,14 +290,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!( @@ -306,36 +312,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 @@ -347,107 +351,101 @@ 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(&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(&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(&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(&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(); @@ -456,8 +454,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 @@ -469,28 +465,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(&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!( @@ -556,16 +552,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()); - crate::test_broker::brokered_fs( - litebox, - attach(BrokenTransport::new(tcp, allowed_writes), server), - ) + Fs::new(attach(BrokenTransport::new(tcp, allowed_writes), server)) } // --------------------------------------------------------------------------- @@ -576,94 +565,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(); 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(&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(&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); @@ -673,78 +650,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(); 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(); 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(); 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(); 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))); } @@ -752,71 +719,61 @@ 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(&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(&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 @@ -831,7 +788,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!( @@ -842,30 +799,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), @@ -874,59 +829,53 @@ 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(&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 @@ -936,9 +885,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, @@ -948,24 +897,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(&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(&mut fd, &mut read_buf[total_read..], None) .expect("read failed"); if n == 0 { break; @@ -975,19 +926,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, @@ -995,19 +944,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(&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(&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(&mut fd, b"C", None).unwrap(); - fs.close(&fd).unwrap(); + drop(fd); // Verify the final file content on host: "AAAAABBBBBC" let host_content = @@ -1015,14 +964,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(&mut fd, &mut buf, Some(5)) .expect("explicit offset read failed"); assert_eq!(n, 5); assert_eq!(&buf[..n], b"BBBBB"); @@ -1030,8 +979,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(&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/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/test_support.rs b/litebox_broker_core/src/fs/test_support.rs new file mode 100644 index 0000000000..8641d92291 --- /dev/null +++ b/litebox_broker_core/src/fs/test_support.rs @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Shared support for the broker-core filesystem tests. +//! +//! The [`Fs`] facade pairs a [`Resolver`] with the device I/O a broker session would supply, so +//! the tests can exercise resolver and backend semantics without a session, transport, or guest +//! descriptor table. Every method delegates directly to the resolver; no behavior is added here. + +use alloc::vec::Vec; + +use litebox_broker_protocol::stdio::StdioOutputStream; + +use super::backend::{Backend, DeviceIo, NoDeviceIo}; +use super::errors::{ + ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, + RmdirError, SeekError, TruncateError, UnlinkError, WriteError, +}; +use super::resolver::{Resolver, ResolverEntry}; +use super::{DirEntry, FileStatus, Mode, OFlags, SeekWhence, UserInfo}; +use crate::test_platform::TestPlatform; + +/// The unprivileged user these tests act as unless they need root. +pub(in crate::fs) const USER: UserInfo = UserInfo { + user: 1000, + group: 1000, +}; + +/// The privileged user, used for test setup that has to bypass permission checks. +pub(in crate::fs) const ROOT: UserInfo = UserInfo::ROOT; + +/// A test-only facade over [`Resolver`]. +/// +/// It supplies the device I/O that a broker session normally provides, and it keeps the ported +/// test bodies readable by not repeating that argument at every call site. Every method delegates +/// directly to the resolver, so no behavior is added here. +pub(in crate::fs) struct Fs { + resolver: Resolver, +} + +/// One open filesystem entry, the resolver's equivalent of an open file description. +pub(in crate::fs) type Entry = ResolverEntry; + +impl Fs { + pub(in crate::fs) fn new(backend: BackendType) -> Self { + Self { + resolver: Resolver::new(backend), + } + } + + pub(in crate::fs) fn open( + &self, + user: UserInfo, + path: &str, + flags: OFlags, + mode: Mode, + ) -> Result, OpenError> { + self.resolver.open(user, path, flags, mode) + } + + pub(in crate::fs) fn read( + &self, + entry: &mut Entry, + buf: &mut [u8], + offset: Option, + ) -> Result { + self.read_with(&NoDeviceIo, entry, buf, offset) + } + + pub(in crate::fs) fn write( + &self, + entry: &mut Entry, + buf: &[u8], + offset: Option, + ) -> Result { + self.write_with(&NoDeviceIo, entry, buf, offset) + } + + /// Like [`Self::read`], but against the device I/O a broker session would supply. + pub(in crate::fs) fn read_with( + &self, + device_io: &dyn DeviceIo, + entry: &mut Entry, + buf: &mut [u8], + offset: Option, + ) -> Result { + self.resolver.read(device_io, entry, buf, offset) + } + + /// Like [`Self::write`], but against the device I/O a broker session would supply. + pub(in crate::fs) fn write_with( + &self, + device_io: &dyn DeviceIo, + entry: &mut Entry, + buf: &[u8], + offset: Option, + ) -> Result { + self.resolver.write(device_io, entry, buf, offset) + } + + pub(in crate::fs) fn seek( + &self, + entry: &mut Entry, + offset: isize, + whence: SeekWhence, + ) -> Result { + self.resolver.seek(entry, offset, whence) + } + + pub(in crate::fs) fn truncate( + &self, + entry: &mut Entry, + length: usize, + reset_offset: bool, + ) -> Result<(), TruncateError> { + self.resolver.truncate(entry, length, reset_offset) + } + + pub(in crate::fs) fn chmod( + &self, + user: UserInfo, + path: &str, + mode: Mode, + ) -> Result<(), ChmodError> { + self.resolver.chmod(user, path, mode) + } + + pub(in crate::fs) fn chown( + &self, + user: UserInfo, + path: &str, + owner: Option, + group: Option, + ) -> Result<(), ChownError> { + self.resolver.chown(user, path, owner, group) + } + + pub(in crate::fs) fn unlink(&self, user: UserInfo, path: &str) -> Result<(), UnlinkError> { + self.resolver.unlink(user, path) + } + + pub(in crate::fs) fn mkdir( + &self, + user: UserInfo, + path: &str, + mode: Mode, + ) -> Result<(), MkdirError> { + self.resolver.mkdir(user, path, mode) + } + + pub(in crate::fs) fn rmdir(&self, user: UserInfo, path: &str) -> Result<(), RmdirError> { + self.resolver.rmdir(user, path) + } + + pub(in crate::fs) fn read_dir( + &self, + entry: &Entry, + ) -> Result, ReadDirError> { + self.resolver.read_dir(entry) + } + + pub(in crate::fs) fn file_status( + &self, + user: UserInfo, + path: &str, + ) -> Result { + self.resolver.file_status(user, path) + } + + pub(in crate::fs) fn handle_status( + &self, + entry: &Entry, + ) -> Result { + self.resolver.handle_status(entry) + } +} + +/// Device I/O for a broker session whose standard-I/O provider cannot service transfers. +/// +/// This mirrors [`crate::stdio`], which completes empty transfers without consulting the provider +/// and reports an I/O error for anything that would need it. +pub(in crate::fs) 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) + } +} + +/// Device I/O that records the standard-I/O transfers the devices backend asks for. +pub(in crate::fs) struct RecordingStdio { + stdin: &'static [u8], + writes: std::sync::Mutex)>>, +} + +impl RecordingStdio { + pub(in crate::fs) fn new(stdin: &'static [u8]) -> Self { + Self { + stdin, + writes: std::sync::Mutex::new(Vec::new()), + } + } + + pub(in crate::fs) fn writes(&self) -> Vec<(StdioOutputStream, Vec)> { + self.writes.lock().unwrap().clone() + } +} + +impl DeviceIo for RecordingStdio { + fn read_stdin(&self, output: &mut [u8]) -> Result { + let read = self.stdin.len().min(output.len()); + output[..read].copy_from_slice(&self.stdin[..read]); + Ok(read) + } + + fn write_stdio(&self, stream: StdioOutputStream, input: &[u8]) -> Result { + self.writes.lock().unwrap().push((stream, input.into())); + Ok(input.len()) + } + + fn fill_random(&self, output: &mut [u8]) -> Result<(), ReadError> { + output.fill(0x5a); + Ok(()) + } +} diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs new file mode 100644 index 0000000000..20b5ca80b9 --- /dev/null +++ b/litebox_broker_core/src/fs/tests.rs @@ -0,0 +1,2039 @@ +// 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, through the [`Fs`] facade from +//! [`super::test_support`]. 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 super::in_mem::InMem; +use super::inode_allocator::InodeAllocator; +use super::overlay::Overlay; +use super::tar_ro::TarRo; +use super::test_support::{Fs, ROOT, RecordingStdio, USER, UnservicedStdio}; +use super::{Mode, OFlags, UserInfo}; +use crate::test_platform::TestPlatform; + +const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); + +fn in_mem_fs() -> Fs> { + Fs::new(InMem::::new(InodeAllocator::standalone())) +} + +fn tar_ro_fs(tar_data: Cow<'static, [u8]>) -> Fs { + Fs::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]>, +) -> Fs> { + Fs::new(Overlay::::new( + upper, + TarRo::new(tar_data, InodeAllocator::standalone()), + InodeAllocator::standalone(), + )) +} + +mod in_mem { + use super::{Fs, InMem, Mode, OFlags, ROOT, TestPlatform, USER, in_mem_fs}; + use crate::fs::errors::{ + ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, + UnlinkError, + }; + use crate::fs::test_support::Entry; + use crate::fs::{FileType, SeekWhence}; + use alloc::vec; + use alloc::vec::Vec; + + type InMemFs = Fs>; + type InMemEntry = Entry>; + + /// 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(&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(&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(&mut fd, b"x", None).expect("Failed to write file"); + + let mut buffer = [0]; + assert!(matches!( + fs.read(&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(&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(&mut fd, data, None) + .expect("Failed to write to file"); + fs.write(&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(&mut fd, &mut buffer, None) + .expect("Failed to read from file"); + let bytes_read2 = fs + .read(&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 = super::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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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 truncate_resets_or_keeps_position() { + let fs = in_mem_fs(); + world_writable_root(&fs); + + let mut fd = fs + .open(USER, "/truncfile", OFlags::CREAT | OFlags::RDWR, Mode::RWXU) + .expect("Failed to create file"); + fs.write(&mut fd, b"0123456789", None) + .expect("Failed to write file"); + + // Truncating without resetting keeps the position, so a read sees nothing. + fs.truncate(&mut fd, 4, false).expect("Failed to truncate"); + let mut buffer = vec![0; 10]; + assert_eq!( + fs.read(&mut fd, &mut buffer, None) + .expect("Failed to read file"), + 0 + ); + + // Truncating with a reset rewinds to the start of the (now shorter) file. + fs.truncate(&mut fd, 4, true).expect("Failed to truncate"); + let bytes_read = fs + .read(&mut fd, &mut buffer, None) + .expect("Failed to read file"); + assert_eq!(&buffer[..bytes_read], b"0123"); + } + + #[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(&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(&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(&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(&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(&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(&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(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(&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(&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(&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(&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(&mut fd, b"New", None) + .expect("Failed to write after truncation"); + fs.write(&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::{Mode, OFlags, TEST_TAR_FILE, USER, tar_ro_fs}; + use crate::fs::FileType; + 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(&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(&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::{Fs, Mode, OFlags, Overlay, TEST_TAR_FILE, TestPlatform, USER, UserInfo}; + use crate::fs::FileType; + use crate::fs::SeekWhence; + 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) -> Fs> { + 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(&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(&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(&mut fd1, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"testfoo\n"); + + fs.write(&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(&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(&mut fd1, &mut buffer, None) + .expect("Failed to read from file"); + assert_eq!(&buffer[..bytes_read], b"test"); + + fs.write(&mut fd2, b"share", None) + .expect("Failed to write to file"); + + let bytes_read = fs + .read(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&mut fd, &mut buffer, None) + .expect("Failed to read file"); + assert_eq!(bytes_read, 0); + + // Write new content + fs.write(&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(&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(&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::{Fs, Mode, OFlags, RecordingStdio, USER, UnservicedStdio}; + use crate::fs::composer::Composer; + use crate::fs::devices::Devices; + use crate::fs::errors::{OpenError, PathError, ReadError, WriteError}; + use alloc::vec; + use litebox_broker_protocol::stdio::StdioOutputStream; + + fn devices_fs() -> Fs { + Fs::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_with(&stdio, &mut fd_stdout, b"", None), + Ok(0) + )); + assert!(matches!( + fs.write_with(&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_with(&stdio, &mut fd_stderr, b"", None), + Ok(0) + )); + assert!(matches!( + fs.write_with(&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_with(&stdio, &mut fd_stdin, &mut [], None), + Ok(0) + )); + let mut buffer = vec![0; 13]; + assert!(matches!( + fs.read_with(&stdio, &mut fd_stdin, &mut buffer, None), + Err(ReadError::Io) + )); + } + + /// Each stdio device routes to its own stream on the session's device I/O. + #[test] + fn stdio_routes_to_the_session_streams() { + let fs = devices_fs(); + let stdio = RecordingStdio::new(b"host input"); + + let mut fd_stdout = fs + .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stdout"); + assert_eq!( + fs.write_with(&stdio, &mut fd_stdout, b"out", None) + .expect("Failed to write /dev/stdout"), + 3 + ); + + let mut fd_stderr = fs + .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .expect("Failed to open /dev/stderr"); + assert_eq!( + fs.write_with(&stdio, &mut fd_stderr, b"err", None) + .expect("Failed to write /dev/stderr"), + 3 + ); + + assert_eq!( + stdio.writes(), + vec![ + (StdioOutputStream::Stdout, b"out".to_vec()), + (StdioOutputStream::Stderr, b"err".to_vec()), + ] + ); + + let mut fd_stdin = fs + .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open /dev/stdin"); + let mut buffer = vec![0; 16]; + let read = fs + .read_with(&stdio, &mut fd_stdin, &mut buffer, None) + .expect("Failed to read /dev/stdin"); + assert_eq!(&buffer[..read], b"host input"); + + // Reading a write-only device and writing a read-only device are rejected. + assert!(matches!( + fs.read_with(&stdio, &mut fd_stdout, &mut buffer, None), + Err(ReadError::NotForReading) + )); + assert!(matches!( + fs.write_with(&stdio, &mut fd_stdin, b"x", None), + Err(WriteError::NotForWriting) + )); + } + + #[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::{Fs, InMem, Mode, OFlags, 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() -> Fs { + Fs::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_with(&stdio, &mut fd_stdout, b"", None), + Ok(0) + )); + assert!(matches!( + fs.write_with(&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_with(&stdio, &mut fd_stderr, b"", None), + Ok(0) + )); + assert!(matches!( + fs.write_with(&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_with(&stdio, &mut fd_stdin, &mut [], None), + Ok(0) + )); + let mut buffer = vec![0; 1024]; + assert!(matches!( + fs.read_with(&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 c9b6962a20..085cb26801 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 77a0a3478e..b397a8ec4e 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); @@ -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 0000000000..cf1a20da9c --- /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_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 6d08fbe386..8e7d7ce979 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -37,7 +37,7 @@ 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"); +const TEST_TAR_FILE: &[u8] = include_bytes!("../../../litebox_broker_core/src/fs/test.tar"); /// The concrete platform used by the shim's unit tests. /// From 1d2879971d2c22c4b6540af0144769edb102e758 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 9 Sep 2026 06:23:54 -0700 Subject: [PATCH 05/34] Decouple guest tests from broker authority Move runner loader coverage onto the production broker launcher and replace shim-wide BrokerCore fixtures with protocol-level guest tests. Windows registry defaults are now seeded lazily so constructing unrelated shim tests performs no file requests, while explicit file integration tests own their broker setup without process-global state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- Cargo.lock | 10 - dev_tests/src/ratchet.rs | 6 +- .../Cargo.toml | 5 - .../tests/common/broker.rs | 218 ---- .../tests/common/mod.rs | 123 -- .../tests/loader.rs | 42 +- litebox_runner_linux_userland/Cargo.toml | 1 - .../tests/common/broker.rs | 218 ---- .../tests/common/mod.rs | 1 + .../tests/common/runner.rs | 267 +++++ litebox_runner_linux_userland/tests/loader.rs | 180 +-- litebox_runner_linux_userland/tests/run.rs | 252 +--- litebox_shim_linux/Cargo.toml | 4 - litebox_shim_linux/src/loader/elf.rs | 180 +-- litebox_shim_linux/src/stdio.rs | 8 +- litebox_shim_linux/src/syscalls/epoll.rs | 8 +- litebox_shim_linux/src/syscalls/file.rs | 194 +-- litebox_shim_linux/src/syscalls/mm.rs | 97 +- litebox_shim_linux/src/syscalls/mod.rs | 2 + litebox_shim_linux/src/syscalls/net.rs | 322 ++++- .../src/syscalls/test_broker.rs | 875 ++++++++++++++ litebox_shim_linux/src/syscalls/tests.rs | 1065 +++++++---------- litebox_shim_windows/src/loader/pe.rs | 395 +++--- litebox_shim_windows/src/syscalls/file.rs | 52 +- litebox_shim_windows/src/syscalls/nls.rs | 10 +- .../src/syscalls/object_manager.rs | 5 +- litebox_shim_windows/src/syscalls/registry.rs | 491 ++++---- litebox_shim_windows/src/syscalls/section.rs | 20 +- litebox_shim_windows/src/test_broker.rs | 144 ++- litebox_shim_windows/src/tests.rs | 40 +- 30 files changed, 2785 insertions(+), 2450 deletions(-) delete mode 100644 litebox_runner_linux_on_windows_userland/tests/common/broker.rs delete mode 100644 litebox_runner_linux_on_windows_userland/tests/common/mod.rs delete mode 100644 litebox_runner_linux_userland/tests/common/broker.rs create mode 100644 litebox_runner_linux_userland/tests/common/runner.rs create mode 100644 litebox_shim_linux/src/syscalls/test_broker.rs diff --git a/Cargo.lock b/Cargo.lock index 82f163ef0c..39852d33cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1765,12 +1765,7 @@ dependencies = [ "anyhow", "clap", "litebox", - "litebox_broker_core", - "litebox_broker_host", - "litebox_broker_local", "litebox_broker_local_userland", - "litebox_broker_protocol", - "litebox_broker_transport", "litebox_common_linux", "litebox_platform_windows_userland", "litebox_shim_linux", @@ -1791,7 +1786,6 @@ dependencies = [ "litebox", "litebox_broker_core", "litebox_broker_host", - "litebox_broker_local", "litebox_broker_local_userland", "litebox_broker_platform_linux_userland", "litebox_broker_protocol", @@ -1932,8 +1926,6 @@ dependencies = [ "bitvec", "libc", "litebox", - "litebox_broker_core", - "litebox_broker_host", "litebox_broker_local", "litebox_broker_protocol", "litebox_broker_transport", @@ -1946,9 +1938,7 @@ dependencies = [ "object", "once_cell", "ringbuf", - "spin 0.9.8", "syscalls", - "tempfile", "thiserror", "zerocopy", ] diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index e2d3bb5c4a..4aabd15ab9 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -45,12 +45,10 @@ fn ratchet_globals() -> Result<()> { ("litebox_platform_multiplex/", 1), ("litebox_platform_windows_userland/", 8), ("litebox_runner_lvbs/", 6), - ("litebox_runner_linux_on_windows_userland/", 1), - ("litebox_runner_linux_userland/", 1), ("litebox_runner_snp/", 2), - ("litebox_shim_linux/", 2), + ("litebox_shim_linux/", 1), ("litebox_shim_optee/", 6), - ("litebox_shim_windows/", 2), + ("litebox_shim_windows/", 1), ("litebox_runner_windows_userland/", 2), ], |file| { diff --git a/litebox_runner_linux_on_windows_userland/Cargo.toml b/litebox_runner_linux_on_windows_userland/Cargo.toml index 9707fff78c..0621527828 100644 --- a/litebox_runner_linux_on_windows_userland/Cargo.toml +++ b/litebox_runner_linux_on_windows_userland/Cargo.toml @@ -15,11 +15,6 @@ litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } [dev-dependencies] -litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core" } -litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } -litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } -litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } -litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } litebox_syscall_rewriter = { version = "0.1.0", path = "../litebox_syscall_rewriter", default-features = false, features = ["std"] } tar = "0.4" diff --git a/litebox_runner_linux_on_windows_userland/tests/common/broker.rs b/litebox_runner_linux_on_windows_userland/tests/common/broker.rs deleted file mode 100644 index a9eb2dac33..0000000000 --- a/litebox_runner_linux_on_windows_userland/tests/common/broker.rs +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use std::sync::{Arc, Mutex, OnceLock}; - -use litebox_broker_core::{ - BrokerCore, ObjectRights, PolicyEngine, - fs::{backend::Backend, 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, 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 litebox_platform_windows_userland::WindowsUserland; - -pub fn litebox( - platform: &'static WindowsUserland, - backend: BackendType, -) -> litebox::LiteBox { - static BROKER: OnceLock = OnceLock::new(); - let broker = BROKER.get_or_init(|| { - BrokerCore::new( - PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), - Arc::new(UnsupportedSocketProvider), - Arc::new(UnusedRandomProvider), - Arc::new(UnsupportedStdioProvider), - Arc::new(Resolver::::new(backend)), - ) - .unwrap() - }); - - 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: &'static BrokerCore, - memory: Arc, -} - -impl TestBrokerSetup { - fn new(broker: &'static 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, - ) -> Result<(), Self::Error> { - assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); - Ok(()) - } - - fn recv_handshake_response(&mut self) -> 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) -> 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) -> Result { - Ok(PeerCredential::Unauthenticated) - } - - fn recv_handshake_request( - &mut self, - ) -> Result, Self::Error> { - Ok(HostReceive::Message(BrokerHandshakeRequest { - protocol_version: BROKER_PROTOCOL_VERSION, - })) - } - - fn send_handshake_response( - &mut self, - response: &BrokerHandshakeResponse, - ) -> 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 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]) -> Result<(), SharedMemoryError> { - let memory = self.0.lock().unwrap(); - let end = offset - .checked_add(destination.len()) - .ok_or(SharedMemoryError::InvalidRange)?; - let source = memory - .get(offset..end) - .ok_or(SharedMemoryError::InvalidRange)?; - destination.copy_from_slice(source); - Ok(()) - } - - fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { - let mut memory = self.0.lock().unwrap(); - let end = offset - .checked_add(source.len()) - .ok_or(SharedMemoryError::InvalidRange)?; - let destination = memory - .get_mut(offset..end) - .ok_or(SharedMemoryError::InvalidRange)?; - destination.copy_from_slice(source); - Ok(()) - } -} - -struct UnusedRandomProvider; - -impl RandomProvider for UnusedRandomProvider { - fn fill(&self, _output: &mut [u8]) -> Result<(), RandomProviderError> { - Err(RandomProviderError) - } -} 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 c25713c32d..0000000000 --- a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs +++ /dev/null @@ -1,123 +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; - -mod broker; - -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 core_mode = litebox_broker_core::fs::Mode::RWXU - | litebox_broker_core::fs::Mode::RWXG - | litebox_broker_core::fs::Mode::RWXO; - let mut entries = vec![( - "/".to_string(), - litebox_broker_core::fs::in_mem::InitialNode::Directory { - mode: core_mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, - }, - )]; - entries.extend(initial_dirs.iter().map(|path| { - ( - (*path).to_string(), - litebox_broker_core::fs::in_mem::InitialNode::Directory { - mode: core_mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, - }, - ) - })); - entries.extend(initial_files.iter().map(|path| { - ( - (*path).to_string(), - litebox_broker_core::fs::in_mem::InitialNode::File { - mode: core_mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, - data: std::fs::read(path).unwrap().into(), - }, - ) - })); - let in_mem = litebox_broker_core::fs::in_mem::InMem::::new_initialized(entries); - let tar_data = if tar_data.is_empty() { - litebox_broker_core::fs::tar_ro::EMPTY_TAR_FILE.into() - } else { - tar_data.into() - }; - let backend = litebox_broker_core::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox_broker_core::fs::overlay::Overlay::::new( - in_mem, - litebox_broker_core::fs::tar_ro::TarRo::new(tar_data, allocators.next()), - allocators.next(), - ) - }) - .mount("/dev", litebox_broker_core::fs::devices::Devices::new) - .build() - .unwrap(); - let litebox = broker::litebox(platform, backend); - let shim_builder = - litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox); - let fs = shim_builder.brokered_fs(); - Self { - platform, - shim_builder, - fs, - context: litebox::fs::resolver::Context::new(), - } - } - - 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(); - let mut written = 0; - while written < contents.len() { - let count = self.fs.write(&fd, &contents[written..], None).unwrap(); - assert_ne!(count, 0, "broker made no progress installing {out}"); - written += count; - } - 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 886f75a3ed..16dbb15988 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, @@ -261,15 +265,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(); } diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index e06ae13ac3..0589bc789e 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -25,7 +25,6 @@ walkdir = "2.0" glob = "0.3" litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core" } litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } -litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } litebox_broker_platform_linux_userland = { version = "0.1.0", path = "../litebox_broker_platform_linux_userland" } litebox_broker_userland = { version = "0.1.0", path = "../litebox_broker_userland" } diff --git a/litebox_runner_linux_userland/tests/common/broker.rs b/litebox_runner_linux_userland/tests/common/broker.rs deleted file mode 100644 index 6ea76650e3..0000000000 --- a/litebox_runner_linux_userland/tests/common/broker.rs +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use std::sync::{Arc, Mutex, OnceLock}; - -use litebox_broker_core::{ - BrokerCore, ObjectRights, PolicyEngine, - fs::{backend::Backend, 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, 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 litebox_platform_linux_userland::LinuxUserland; - -pub fn litebox( - platform: &'static LinuxUserland, - backend: BackendType, -) -> litebox::LiteBox { - static BROKER: OnceLock = OnceLock::new(); - let broker = BROKER.get_or_init(|| { - BrokerCore::new( - PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), - Arc::new(UnsupportedSocketProvider), - Arc::new(UnusedRandomProvider), - Arc::new(UnsupportedStdioProvider), - Arc::new(Resolver::::new(backend)), - ) - .unwrap() - }); - - 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: &'static BrokerCore, - memory: Arc, -} - -impl TestBrokerSetup { - fn new(broker: &'static 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, - ) -> Result<(), Self::Error> { - assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); - Ok(()) - } - - fn recv_handshake_response(&mut self) -> 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) -> 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) -> Result { - Ok(PeerCredential::Unauthenticated) - } - - fn recv_handshake_request( - &mut self, - ) -> Result, Self::Error> { - Ok(HostReceive::Message(BrokerHandshakeRequest { - protocol_version: BROKER_PROTOCOL_VERSION, - })) - } - - fn send_handshake_response( - &mut self, - response: &BrokerHandshakeResponse, - ) -> 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 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]) -> Result<(), SharedMemoryError> { - let memory = self.0.lock().unwrap(); - let end = offset - .checked_add(destination.len()) - .ok_or(SharedMemoryError::InvalidRange)?; - let source = memory - .get(offset..end) - .ok_or(SharedMemoryError::InvalidRange)?; - destination.copy_from_slice(source); - Ok(()) - } - - fn write(&self, offset: usize, source: &[u8]) -> Result<(), SharedMemoryError> { - let mut memory = self.0.lock().unwrap(); - let end = offset - .checked_add(source.len()) - .ok_or(SharedMemoryError::InvalidRange)?; - let destination = memory - .get_mut(offset..end) - .ok_or(SharedMemoryError::InvalidRange)?; - destination.copy_from_slice(source); - Ok(()) - } -} - -struct UnusedRandomProvider; - -impl RandomProvider for UnusedRandomProvider { - fn fill(&self, _output: &mut [u8]) -> Result<(), RandomProviderError> { - Err(RandomProviderError) - } -} diff --git a/litebox_runner_linux_userland/tests/common/mod.rs b/litebox_runner_linux_userland/tests/common/mod.rs index bddad0925f..34eea4abab 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 0000000000..b497008f7e --- /dev/null +++ b/litebox_runner_linux_userland/tests/common/runner.rs @@ -0,0 +1,267 @@ +// 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); + } + 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 cd957f9fb2..44844e7726 100644 --- a/litebox_runner_linux_userland/tests/loader.rs +++ b/litebox_runner_linux_userland/tests/loader.rs @@ -3,178 +3,25 @@ mod cache; mod common; -#[path = "common/broker.rs"] -mod test_broker; -use std::ffi::CString; - -use litebox_platform_linux_userland::LinuxUserland as Platform; - -struct TestLauncher { - platform: &'static Platform, - shim_builder: litebox_shim_linux::LinuxShimBuilder, - fs: litebox_shim_linux::DefaultFS, -} - -impl TestLauncher { - fn init_platform( - tar_data: &'static [u8], - initial_files: &[&str], - installed_files: &[(&str, Vec)], - ) -> Self { - let platform = Platform::new(); - let mode = litebox_broker_core::fs::Mode::RWXU - | litebox_broker_core::fs::Mode::RWXG - | litebox_broker_core::fs::Mode::RWXO; - let mut directories = std::collections::BTreeSet::new(); - for path in initial_files { - directories.extend( - std::path::Path::new(path) - .parent() - .into_iter() - .flat_map(std::path::Path::ancestors) - .filter(|path| { - *path != std::path::Path::new("/") && !path.as_os_str().is_empty() - }) - .map(std::path::Path::to_path_buf), - ); - } - for (path, _) in installed_files { - directories.extend( - std::path::Path::new(path) - .parent() - .into_iter() - .flat_map(std::path::Path::ancestors) - .filter(|path| { - *path != std::path::Path::new("/") && !path.as_os_str().is_empty() - }) - .map(std::path::Path::to_path_buf), - ); - } - let mut directories: Vec<_> = directories.into_iter().collect(); - directories.sort_by_key(|path| path.components().count()); - let mut entries = vec![( - "/".to_string(), - litebox_broker_core::fs::in_mem::InitialNode::Directory { - mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, - }, - )]; - entries.extend(directories.into_iter().map(|path| { - ( - path.to_string_lossy().into_owned(), - litebox_broker_core::fs::in_mem::InitialNode::Directory { - mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, - }, - ) - })); - entries.extend(initial_files.iter().map(|path| { - ( - (*path).to_string(), - litebox_broker_core::fs::in_mem::InitialNode::File { - mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, - data: std::fs::read(path).unwrap().into(), - }, - ) - })); - entries.extend(installed_files.iter().map(|(path, data)| { - ( - (*path).to_string(), - litebox_broker_core::fs::in_mem::InitialNode::File { - mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, - data: data.clone().into(), - }, - ) - })); - let in_mem = litebox_broker_core::fs::in_mem::InMem::::new_initialized(entries); - let tar_data = if tar_data.is_empty() { - litebox_broker_core::fs::tar_ro::EMPTY_TAR_FILE.into() - } else { - tar_data.into() - }; - let backend = litebox_broker_core::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox_broker_core::fs::overlay::Overlay::::new( - in_mem, - litebox_broker_core::fs::tar_ro::TarRo::new(tar_data, allocators.next()), - allocators.next(), - ) - }) - .mount("/dev", litebox_broker_core::fs::devices::Devices::new) - .build() - .unwrap(); - let litebox = test_broker::litebox(platform, backend); - let shim_builder = - litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox); - let fs = shim_builder.brokered_fs(); - Self { - platform, - shim_builder, - fs, - } - } - - 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 launcher = TestLauncher::init_platform( - &[], - &files_to_install - .iter() - .map(std::string::String::as_str) - .collect::>(), - &[(executable_path, executable_data)], - ); - 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 launcher = TestLauncher::init_platform(&[], &[], &[(executable_path, executable_data)]); - - launcher.test_load_exec_common(executable_path); + Runner::new(&path, "loader_static") + .env("PATH=/bin") + .arg("hello") + .run(); } const HELLO_WORLD_NOLIBC: &str = r#" @@ -306,9 +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 launcher = TestLauncher::init_platform(&[], &[], &[(executable_path, executable_data)]); - 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/run.rs b/litebox_runner_linux_userland/tests/run.rs index dc7a7f869f..6d93ce5b19 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"); } } @@ -1646,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| { diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index 6352a3bd65..d1197fee05 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -24,14 +24,10 @@ alarm_fallback = [] aarch64_virtualize_x18 = [] [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_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/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index d9fc2e9bf5..02f30f5deb 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -383,180 +383,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 38f4ace2bd..4d46bf2d88 100644 --- a/litebox_shim_linux/src/stdio.rs +++ b/litebox_shim_linux/src/stdio.rs @@ -10,7 +10,7 @@ mod tests { use litebox::fs::{Mode, OFlags}; use litebox_common_linux::{FcntlArg, FileDescriptorFlags, IoctlArg, Termios, errno::Errno}; - use crate::{UserPtrMut, syscalls::tests::init_platform_with_broker}; + use crate::{UserPtrMut, syscalls::tests::init_platform}; fn termios() -> Termios { Termios { @@ -25,7 +25,7 @@ mod tests { #[test] fn test_stdio() { - let task = init_platform_with_broker(); + let task = init_platform(); // Check that the stdio streams are in the file table let stdin_stat = task.sys_fstat(0).unwrap(); @@ -71,7 +71,7 @@ mod tests { #[test] fn test_stdio_flags_with_dup() { - let task = init_platform_with_broker(); + let task = init_platform(); let stdin = 0; let flags = task.sys_fcntl(stdin, FcntlArg::GETFL).unwrap(); @@ -126,7 +126,7 @@ mod tests { #[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 aa199b8d34..247c70e350 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -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,7 +694,7 @@ 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 @@ -750,7 +750,7 @@ 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()) @@ -790,7 +790,7 @@ 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()) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index b8d6a76f2a..33fd61d5d3 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -2979,42 +2979,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 +3041,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 +3092,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 | (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()) @@ -3122,21 +3163,35 @@ 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", @@ -3146,13 +3201,8 @@ mod tests { .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,12 +3211,9 @@ 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, @@ -3177,7 +3224,6 @@ mod tests { .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 +3231,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 f2760ec232..b807d69617 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -1557,12 +1557,79 @@ impl Task { mod tests { use litebox::{ fs::{Mode, OFlags}, + mm::linux::PAGE_SIZE, platform::PageManagementProvider, }; + use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, errno::Errno}; + use object::{elf::FileHeader64, endian::LittleEndian}; - use crate::syscalls::tests::TestPlatform as Platform; - use crate::{UserPtrMut, syscalls::tests::init_platform}; + 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 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![]), + ]); + } + + 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")] @@ -1743,14 +1810,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, @@ -1767,8 +1830,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] @@ -2073,14 +2137,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 @@ -2094,6 +2154,7 @@ mod tests { .as_ref(), content.as_slice(), ); + assert_mmap_reads(&files, content.len()); // mprotect to add write permission should fail let err = task @@ -2102,7 +2163,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 53dd561f44..b5e00db5e7 100644 --- a/litebox_shim_linux/src/syscalls/mod.rs +++ b/litebox_shim_linux/src/syscalls/mod.rs @@ -15,6 +15,8 @@ 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 { diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index 0d3c17754d..a42bf9e61b 100644 --- a/litebox_shim_linux/src/syscalls/net.rs +++ b/litebox_shim_linux/src/syscalls/net.rs @@ -3740,6 +3740,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 +3750,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 +3766,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 +3781,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 +3841,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 +3908,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 +3916,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 +3964,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 +3985,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 +4013,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 +4098,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 +4115,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 +4185,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 +4244,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 +4301,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 +4310,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 +4530,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 +4563,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 +4606,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 +4668,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 +4683,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 +4698,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 +4723,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/test_broker.rs b/litebox_shim_linux/src/syscalls/test_broker.rs new file mode 100644 index 0000000000..91d4db0feb --- /dev/null +++ b/litebox_shim_linux/src/syscalls/test_broker.rs @@ -0,0 +1,875 @@ +// 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::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, + }, + block_size: 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 = Some(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 = Some(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 8e7d7ce979..8170a5a2c7 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -1,33 +1,32 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use alloc::boxed::Box; +//! 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::fs::{Mode, OFlags}; -use litebox_broker_core::{ - AssociationCancellation, BrokerCore, BrokerError, ObjectRights, PolicyEngine, SessionId, - random::{RandomProvider, RandomProviderError}, - readiness::{ReadinessRegistration, ReadinessSink}, - socket::{PlatformSocket, SocketProvider}, - stdio::{StdioProvider, StdioProviderError}, +use litebox_broker_protocol::ObjectHandle; +use litebox_broker_protocol::fs::WriteFileResponse; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, FileType, + FileUser, MAX_FILE_TRANSFER_SIZE, encode_directory_entries_chunk, }; -use litebox_broker_host::{BrokerHostAssociation, BrokerHostError}; -use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::{ - BROKER_PROTOCOL_VERSION, ObjectHandle, - message::{BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse}, - readiness::ReadinessFlags, - shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}, - socket::CreateSocketRequest, - stdio::{StdioOutputStream, StdioStream}, -}; -use litebox_broker_transport::{ - channel::{HostReceive, HostSetupChannel, LocalCallChannel, LocalSetupChannel, PeerCredential}, - shared_memory::{SharedBufferPool, SharedMemory, SharedMemoryError}, -}; -use litebox_common_linux::{AtFlags, FcntlArg, FileDescriptorFlags, errno::Errno}; +use litebox_broker_protocol::message::FileResponse; +use litebox_common_linux::{AtFlags, DirentType, FcntlArg, FileDescriptorFlags, errno::Errno}; use zerocopy::FromBytes as _; use crate::UserPtrMut; +use crate::syscalls::test_broker::{ + FileCall, Scripted, ScriptedFiles, closed, failed, opened, path_status, +}; use litebox::shim::{Exception, ExceptionInfo}; use litebox_common_linux::PtRegs; @@ -37,7 +36,11 @@ use litebox_common_linux::signal::{ILL_ILLOPN, SI_KERNEL, SiginfoData, Signal}; extern crate std; -const TEST_TAR_FILE: &[u8] = include_bytes!("../../../litebox_broker_core/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. /// @@ -55,282 +58,51 @@ 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(|| { - let in_mem = litebox_broker_core::fs::in_mem::InMem::::new_initialized([( - "/", - litebox_broker_core::fs::in_mem::InitialNode::Directory { - mode: litebox_broker_core::fs::Mode::RWXU - | litebox_broker_core::fs::Mode::RWXG - | litebox_broker_core::fs::Mode::RWXO, - owner: litebox_broker_core::fs::UserInfo::ROOT, - }, - )]); - let backend = litebox_broker_core::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox_broker_core::fs::overlay::Overlay::::new( - in_mem, - litebox_broker_core::fs::tar_ro::TarRo::new( - TEST_TAR_FILE.into(), - allocators.next(), - ), - allocators.next(), - ) - }) - .mount("/dev", litebox_broker_core::fs::devices::Devices::new) - .build() - .unwrap(); - 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::resolver::Resolver::::new(backend), - ), - ) - .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 { - init_platform_with_broker() + 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 fs = alloc::sync::Arc::new(shim_builder.brokered_fs()); + 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); + let fs = Arc::new(shim_builder.brokered_fs()); shim_builder.build().0.new_test_task(fs) } -struct TestBrokerSetup { - memory: alloc::sync::Arc, -} - -impl TestBrokerSetup { - fn new() -> Self { - Self { - memory: alloc::sync::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( - test_broker(), - &mut setup, - shared_buffers, - alloc::sync::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, alloc::sync::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 TestSharedMemory(std::sync::Mutex>); - -impl TestSharedMemory { - fn new() -> Self { - Self(std::sync::Mutex::new(std::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(()) - } +/// 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, + node_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) -> FileMode { + FileMode::from_bits(bits).expect("test modes must be supported") } #[cfg(target_arch = "x86_64")] @@ -424,7 +196,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!( @@ -475,7 +247,7 @@ fn test_fcntl() { #[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)); @@ -528,342 +300,431 @@ 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 +} + +/// 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() +} +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!( - !found_entries.is_empty(), - "Should find at least some directory entries" + matches!(files.take_calls().last(), Some(FileCall::Close(actual)) if *actual == handle), + "closing the guest fd must close its broker handle" ); +} + +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(); + } +} + +#[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(buffer.as_mut_ptr() as usize), + buffer.len(), + ) + .expect("the directory read must succeed"); - // 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" - ] + files.take_calls(), + vec![FileCall::ReadDirectory { + handle: FILE_HANDLE, + start_index: 0, + }] + ); + assert_eq!( + 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" ); - // 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" - ); - } - } - + // A second read resumes after the entries already reported. + files.script(directory_pages(&entries, usize::MAX)); 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" + buffer.len(), + ), + Ok(0), + "the previous call already reported every entry" ); - 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 - .sys_getdirent64( - dir_fd, - UserPtrMut::from_usize(small_buffer.as_mut_ptr() as usize), - small_buffer.len(), - ) - .expect("Failed to read directory entries"); + close_scripted_file(&files, &task, dir_fd, FILE_HANDLE); +} - // 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" +#[test] +fn getdirent64_resumes_across_buffers_and_rejects_undersized_ones() { + 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 { + files.script(directory_pages(&entries, 64)); + let read = task + .sys_getdirent64( + dir_fd, + UserPtrMut::from_usize(chunk.as_mut_ptr() as usize), + chunk.len(), + ) + .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!(dirent.len > 0, "Entry length should be positive"); } - - // Test 3: Invalid file descriptor - let result = task.sys_getdirent64( - -1, - UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), - buffer.len(), + 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); + + // A buffer too small for even one entry is rejected rather than truncating a name. + let fresh_fd = scripted_dir_fd(&files, &task, "/dir"); + files.script(directory_pages(&entries, usize::MAX)); + let mut tiny = [0u8; 8]; assert_eq!( - result, - Err(Errno::EBADF), - "Should return EBADF for invalid fd" + task.sys_getdirent64( + fresh_fd, + UserPtrMut::from_usize(tiny.as_mut_ptr() as usize), + tiny.len(), + ), + Err(Errno::EINVAL) ); + close_scripted_file(&files, &task, fresh_fd, FILE_HANDLE); +} - // 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(); +#[test] +fn getdirent64_translates_descriptor_and_broker_errors() { + let (files, task) = scripted_task([]); + let mut buffer = [0u8; 256]; - let result = task.sys_getdirent64( - file1_fd, - UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), - buffer.len(), + // 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 dir_fd = scripted_dir_fd(&files, &task, "/dir"); + files.script([failed(FileError::NotDirectory)]); assert_eq!( - result, + task.sys_getdirent64( + dir_fd, + UserPtrMut::from_usize(buffer.as_mut_ptr() as usize), + buffer.len(), + ), Err(Errno::ENOTDIR), - "Should return ENOTDIR for non-directory fd" + "a broker not-a-directory failure surfaces as ENOTDIR" ); - 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 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!( - 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), + 0 + ), + Err(Errno::EINVAL) ); + close_scripted_file(&files, &task, dir_fd, FILE_HANDLE); +} - task.sys_close(dir_fd).expect("Failed to close directory"); +#[test] +fn umask_masks_the_creation_mode_sent_to_the_broker() { + let (files, task) = scripted_task([]); - // 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(); + // The default mask is 022, and `umask` returns the previous mask. + assert_eq!(task.sys_umask(0o077).bits(), 0o022); - // Read entries in smaller chunks to test offset tracking - let mut all_entries = alloc::vec::Vec::new(); + files.script([opened(FILE_HANDLE)]); + let fd = task + .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); - loop { - let mut chunk_buffer = [0u8; 64]; - let bytes_read = task - .sys_getdirent64( - dir_fd2, - UserPtrMut::from_usize(chunk_buffer.as_mut_ptr() as usize), - chunk_buffer.len(), - ) - .expect("Failed to read directory chunk"); + 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!( + 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" + ); - if bytes_read == 0 { - break; // End of 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); +} - // 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 unlinkat_routes_by_flag_and_translates_broker_failures() { + let (files, task) = scripted_task([]); - assert!(dirent.len > 0, "Entry length must be positive"); - assert!( - offset + dirent.len as usize <= bytes_read, - "Entry should fit in chunk" - ); + // 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 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; - } - } + 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!( + files.take_calls(), + vec![FileCall::Unlink { + path: "/file".into(), + user: ROOT, + }] + ); - // Verify we still got our expected entries through chunked reading - all_entries.sort(); + 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!( - all_entries, - alloc::vec![ - ".", - "..", - "bar", - "dev", - "foo", - "test_file1.txt", - "test_file2.txt" - ] + files.take_calls(), + vec![FileCall::Rmdir { + path: "/dir".into(), + user: ROOT, + }], + "AT_REMOVEDIR is routed to the directory-removal request" ); + + for (flags, error, errno) in [ + (AtFlags::empty(), FileError::IsDirectory, Errno::EISDIR), + ( + AtFlags::empty(), + FileError::NoSuchFileOrDirectory, + Errno::ENOENT, + ), + (AtFlags::AT_REMOVEDIR, FileError::NotEmpty, Errno::ENOTEMPTY), + ( + AtFlags::AT_REMOVEDIR, + FileError::NotDirectory, + Errno::ENOTDIR, + ), + ] { + 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_umask_behavior() { - let task = init_platform(); - - // 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 - - // 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"; - 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()); +fn read_and_write_carry_lengths_and_offsets_to_the_broker() { + let (files, task) = scripted_task([opened(FILE_HANDLE)]); + let fd = i32::try_from( + task.sys_open("/data", OFlags::RDWR, Mode::empty()) + .expect("the scripted open must succeed"), + ) + .unwrap(); + let _ = files.take_calls(); - let stat_file = task.sys_stat(test_file).expect("stat failed on test file"); - let actual_file_perm = stat_file.st_mode & 0o777; + // A read without an offset uses the broker-owned file position. + files.script([Scripted::Read(b"hello".to_vec())]); + let mut buffer = [0u8; 8]; + assert_eq!(task.sys_read(fd, &mut buffer, None), Ok(5)); + assert_eq!( + &buffer[..5], + b"hello", + "a short read fills only what arrived" + ); assert_eq!( - actual_file_perm, 0o600, - "File permission should respect umask (expected 600 got {actual_file_perm:03o})", + files.take_calls(), + vec![FileCall::Read { + handle: FILE_HANDLE, + length: 8, + offset: None, + }] ); - // 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"); + // `pread` passes its explicit offset through. + files.script([Scripted::Read(b"lo".to_vec())]); + assert_eq!(task.sys_read(fd, &mut buffer[..2], Some(3)), Ok(2)); + assert_eq!( + files.take_calls(), + vec![FileCall::Read { + handle: FILE_HANDLE, + length: 2, + offset: Some(3), + }] + ); - let stat_dir = task - .sys_stat(test_dir) - .expect("stat failed on test directory"); - let actual_dir_perm = stat_dir.st_mode & 0o777; + // `pwrite` stages its bytes and offset for the broker. + files.script([Scripted::Reply(FileResponse::Write(WriteFileResponse { + written: 3, + }))]); + assert_eq!(task.sys_write(fd, b"abc", Some(7)), Ok(3)); assert_eq!( - actual_dir_perm, 0o700, - "Directory permission should respect umask (expected 700 got {actual_dir_perm:03o})", + files.take_calls(), + vec![FileCall::Write { + handle: FILE_HANDLE, + data: b"abc".to_vec(), + offset: Some(7), + }] ); + close_scripted_file(&files, &task, fd, FILE_HANDLE); +} + +#[test] +fn stat_translates_broker_status_and_failures() { + let (files, task) = scripted_task([path_status(FileType::RegularFile, 0o640)]); - // 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 + let stat = task.sys_stat("/status_file").expect("stat must succeed"); + assert_eq!(stat.st_mode & 0o777, 0o640); assert_eq!( - prev3, 0o777, - "Only low 9 bits should be retained (expected 777)" + files.take_calls(), + vec![FileCall::PathStatus { + path: "/status_file".into(), + user: ROOT, + }] ); - // Restore to original - let _ = task.sys_umask(orig); + + files.script([failed(FileError::NoSuchFileOrDirectory)]); + assert_eq!(task.sys_stat("/missing"), Err(Errno::ENOENT)); + 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 @@ -909,91 +770,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_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index b31edc5016..57dbc47c65 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -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( @@ -2543,22 +2542,10 @@ mod tests { } fn created_process_environment_snapshot() -> CreatedProcessEnvironmentSnapshot { - let platform = crate::tests::test_platform(); - let litebox = crate::test_broker::litebox( - platform, - alloc::vec![( - "/".into(), - litebox_broker_core::fs::in_mem::InitialNode::Directory { - mode: litebox_broker_core::fs::Mode::RWXU - | litebox_broker_core::fs::Mode::RWXG - | litebox_broker_core::fs::Mode::RWXO, - owner: litebox_broker_core::fs::UserInfo::ROOT, - }, - )], - ); + // 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_brokered(&litebox)); - let loader = PeLoader::new(platform, fs, &page_manager); let image = loaded_module_image(application_module_base()); let image_base_address = image.mapping.base_addr; @@ -2572,15 +2559,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 b305acb41d..a3588923ae 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -2900,7 +2900,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,7 +3181,7 @@ 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( @@ -3222,7 +3222,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, @@ -3275,7 +3275,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( @@ -3336,7 +3336,7 @@ 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); @@ -3366,7 +3366,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 +3420,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 +3441,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 +3497,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 +3754,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,7 +3791,7 @@ 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) .unwrap(); @@ -3882,7 +3882,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,7 +3975,7 @@ 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( @@ -4045,7 +4045,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 +4121,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 +4149,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 +4173,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,7 +4212,7 @@ 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( &task.fs_context, @@ -4346,7 +4346,7 @@ mod tests { #[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 +4386,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 +4445,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(); @@ -4480,7 +4480,7 @@ mod tests { #[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 @@ -4516,7 +4516,7 @@ mod tests { #[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 +4815,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 +5000,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 +5116,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 c5338e0476..7a6ce56877 100644 --- a/litebox_shim_windows/src/syscalls/nls.rs +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -1156,7 +1156,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 +1202,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 +1254,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 +1311,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 +1343,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 a8436c6886..dc83dd9cbe 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 fba9a70f98..6d2f8061a0 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -84,6 +84,11 @@ pub(crate) struct RegistryKeyObject { pub(crate) struct RegistryStore { fs: RegistryFileSystem, fs_context: litebox::fs::resolver::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, } @@ -601,186 +606,42 @@ struct RegistryValue { } impl RegistryStore { + /// 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: &LiteBox) -> Self { - let fs = litebox::fs::resolver::Resolver::new_brokered(litebox); - 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; - } - } - } Self { - fs, - fs_context, + fs: litebox::fs::resolver::Resolver::new_brokered(litebox), + fs_context: litebox::fs::resolver::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) -> &RegistryFileSystem { + { + let mut seeded = self.defaults_seeded.lock(); + if !*seeded { + *seeded = true; + seed_defaults(&self.fs, &self.fs_context); + } + } + &self.fs + } + fn open_key( &self, path: &str, desired_access: RegistryKeyAccess, ) -> Result>, NtStatus> { - self.fs + self.fs() .open(&self.fs_context, path, desired_access.into(), Mode::empty()) .map_err(map_open_error) } @@ -792,7 +653,7 @@ impl RegistryStore { ) -> Result { let value_path = value_path(key_path, value_name)?; let status = self - .fs + .fs() .file_status(&self.fs_context, &*value_path) .map_err(map_file_status_error)?; if status.file_type != FileType::RegularFile { @@ -803,7 +664,7 @@ impl RegistryStore { } let fd = self - .fs + .fs() .open( &self.fs_context, &*value_path, @@ -812,8 +673,8 @@ impl RegistryStore { ) .map_err(map_open_error)?; let mut data = vec![0; status.size]; - let result = read_exact_at(&self.fs, &fd, &mut data); - let _ = self.fs.close(&fd); + let result = read_exact_at(self.fs(), &fd, &mut data); + let _ = self.fs().close(&fd); result?; let value_type = u32::from_le_bytes( @@ -834,7 +695,7 @@ impl RegistryStore { value: &[u8], ) -> Result<(), NtStatus> { write_value_at_path( - &self.fs, + self.fs(), &self.fs_context, key_path, value_name, @@ -905,7 +766,7 @@ impl RegistryStore { 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_dir(&key.fd).map_err(map_read_dir_error)? { if entry.file_type == FileType::Directory && entry.name != "." && entry.name != ".." @@ -920,7 +781,7 @@ impl RegistryStore { let values_path = format!("{}/{}", key.path.trim_end_matches('/'), VALUES_DIR_NAME); let values_fd = self - .fs + .fs() .open( &self.fs_context, &*values_path, @@ -928,8 +789,8 @@ impl RegistryStore { 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_dir(&values_fd).map_err(map_read_dir_error); + let _ = self.fs().close(&values_fd); for entry in values? { if entry.file_type != FileType::RegularFile { continue; @@ -940,7 +801,7 @@ impl RegistryStore { .max(entry.name.encode_utf16().count() * size_of::()); let path = format!("{values_path}/{}", entry.name); let size = self - .fs + .fs() .file_status(&self.fs_context, &*path) .map_err(map_file_status_error)? .size; @@ -966,7 +827,7 @@ impl RegistryStore { 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_dir(&key.fd).map_err(map_read_dir_error)? { if entry.file_type == FileType::Directory && entry.name != "." && entry.name != ".." @@ -987,7 +848,7 @@ impl RegistryStore { ) -> Result { let child_path = format!("{}/{}", key.path.trim_end_matches('/'), name); let child_fd = self - .fs + .fs() .open( &self.fs_context, &*child_path, @@ -1000,7 +861,7 @@ impl RegistryStore { fd: child_fd, }; let summary = self.key_summary(&child); - let _ = self.fs.close(&child.fd); + let _ = self.fs().close(&child.fd); summary } @@ -1017,7 +878,7 @@ impl RegistryStore { ) -> Result, NtStatus> { let values_path = format!("{}/{}", key.path.trim_end_matches('/'), VALUES_DIR_NAME); let values_fd = self - .fs + .fs() .open( &self.fs_context, &*values_path, @@ -1025,8 +886,8 @@ impl RegistryStore { 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_dir(&values_fd).map_err(map_read_dir_error); + let _ = self.fs().close(&values_fd); let mut names = Vec::new(); for entry in entries? { if entry.file_type == FileType::RegularFile { @@ -1038,6 +899,175 @@ 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: &RegistryFileSystem, + fs_context: &litebox::fs::resolver::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, @@ -1072,7 +1102,7 @@ impl Task { } pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { - let _ = self.global.registry.fs.close(&key.fd); + let _ = self.global.registry.fs().close(&key.fd); } pub(crate) fn sys_nt_open_key( @@ -1210,7 +1240,7 @@ impl Task { let disposition = match self .global .registry - .fs + .fs() .file_status(&self.global.registry.fs_context, &path) { Ok(status) if status.file_type == FileType::Directory => { @@ -1221,7 +1251,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, )? { @@ -2489,11 +2519,15 @@ mod tests { fn RegDeleteTreeW(hKey: *mut core::ffi::c_void, lpSubKey: *const u16) -> i32; } + /// 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() -> (LiteBox, RegistryStore) { let mode = litebox_broker_core::fs::Mode::RWXU | litebox_broker_core::fs::Mode::RWXG | litebox_broker_core::fs::Mode::RWXO; - let litebox = crate::test_broker::litebox( + let litebox = crate::test_broker::litebox_with_broker_files( test_platform(), alloc::vec![ ( @@ -2681,6 +2715,47 @@ mod tests { query_status } + /// Building a store must not touch the file system: the guest may never use the registry, and + /// a shim is constructed before the guest can be asked to pay for registry startup. + #[test] + fn registry_store_construction_issues_no_file_requests() { + // This broker association serves no files at all and panics on any request, so building + // the store at all is the assertion. + let litebox = crate::test_broker::litebox(test_platform()); + let registry = RegistryStore::new(&litebox); + assert!(!*registry.defaults_seeded.lock()); + } + + #[test] + fn registry_defaults_are_seeded_once_on_first_use() { + 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(); + + // The raw store is read directly here so that the read itself does not seed it. + assert!(matches!( + registry.fs.file_status(®istry.fs_context, &*value_path), + Err(FileStatusError::PathError( + PathError::NoSuchFileOrDirectory | PathError::MissingComponent + )) + )); + + let value = registry + .read_value_at_path(&key_path, "ACP") + .expect("the first registry operation must seed the defaults"); + assert_eq!(value.data, DEFAULT_ACP_VALUE); + assert!(*registry.defaults_seeded.lock()); + + // A guest write must survive a later operation, which must not re-seed over it. + registry + .write_value_at_path(&key_path, "ACP", RegistryValueType::Sz.into(), b"9\0") + .unwrap(); + assert_eq!( + registry.read_value_at_path(&key_path, "ACP").unwrap().data, + b"9\0" + ); + } + #[test] fn registry_store_separates_values_from_subkeys() { let (litebox, registry) = test_registry(); @@ -2689,7 +2764,7 @@ mod tests { assert_eq!( registry - .fs + .fs() .file_status(®istry.fs_context, &*value_path) .unwrap() .file_type, @@ -2697,7 +2772,7 @@ mod tests { ); assert_eq!( registry - .fs + .fs() .file_status(®istry.fs_context, &*value_path) .unwrap() .size, @@ -2724,7 +2799,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); @@ -2829,7 +2904,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); @@ -2923,7 +2998,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); @@ -3022,7 +3097,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"] { @@ -3057,7 +3132,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); @@ -3075,7 +3150,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); @@ -3089,7 +3164,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); @@ -3142,7 +3217,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); @@ -3210,17 +3285,17 @@ 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 + .fs() .chmod( &task.global.registry.fs_context, &*private_path, @@ -3247,7 +3322,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); @@ -3283,7 +3358,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); @@ -3320,7 +3395,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); @@ -3348,7 +3423,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); @@ -3408,7 +3483,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); @@ -3488,7 +3563,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); @@ -3574,7 +3649,7 @@ mod tests { #[test] fn nt_query_key_reports_full_and_cached_information() { - let task = crate::tests::test_task(); + let task = crate::tests::test_task_with_broker_files(&[]); let key_handle = open_code_page_key(&task); let mut full_bytes = [0u8; 64]; diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index cfba9d4c7c..6e93c5d8d5 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -1269,7 +1269,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 +1486,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 +1544,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 +1609,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 +1670,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 index a80fd58abe..8f8c6932c2 100644 --- a/litebox_shim_windows/src/test_broker.rs +++ b/litebox_shim_windows/src/test_broker.rs @@ -1,9 +1,22 @@ // 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, sync::Arc, vec, vec::Vec}; +use alloc::{boxed::Box, string::String, sync::Arc, vec, vec::Vec}; use litebox_broker_core::{ BrokerCore, ObjectRights, PolicyEngine, @@ -17,7 +30,10 @@ use litebox_broker_host::{BrokerHostAssociation, BrokerHostError}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::{ BROKER_PROTOCOL_VERSION, ObjectHandle, - message::{BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerRequest, BrokerResponse}, + message::{ + BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, + BrokerResponse, + }, readiness::ReadinessFlags, shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE}, }; @@ -28,28 +44,87 @@ use litebox_broker_transport::{ use crate::tests::TestPlatform; -pub(crate) fn litebox( +/// 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<(alloc::string::String, InitialNode)>, + entries: Vec<(String, InitialNode)>, ) -> litebox::LiteBox { - static BROKER: std::sync::OnceLock = std::sync::OnceLock::new(); - let broker = BROKER.get_or_init(|| { - 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(); - BrokerCore::new( - PolicyEngine::with_unauthenticated_rights(ObjectRights::all()), - Arc::new(UnsupportedSocketProvider), - Arc::new(UnusedRandomProvider), - Arc::new(UnsupportedStdioProvider), - Arc::new(Resolver::::new(fs)), - ) - .unwrap() - }); + 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| { @@ -61,12 +136,12 @@ pub(crate) fn litebox( } struct TestBrokerSetup { - broker: &'static BrokerCore, + broker: BrokerCore, memory: Arc, } impl TestBrokerSetup { - fn new(broker: &'static BrokerCore) -> Self { + fn new(broker: BrokerCore) -> Self { Self { broker, memory: Arc::new(TestSharedMemory::new()), @@ -79,7 +154,7 @@ impl TestBrokerSetup { )); let mut setup = TestHostSetup; let association = litebox_broker_host::setup_connection( - self.broker, + &self.broker, &mut setup, shared_buffers, Arc::new(TestReadinessSink), @@ -183,7 +258,16 @@ impl ReadinessSink for TestReadinessSink { fn retire(&self, _handle: ObjectHandle) {} } -struct TestSharedMemory(std::sync::Mutex>); +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 { @@ -224,11 +308,3 @@ impl SharedMemory for TestSharedMemory { Ok(()) } } - -struct UnusedRandomProvider; - -impl RandomProvider for UnusedRandomProvider { - fn fill(&self, _output: &mut [u8]) -> core::result::Result<(), RandomProviderError> { - Err(RandomProviderError) - } -} diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 365b030e75..463162ec38 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -1,6 +1,13 @@ // 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; @@ -106,11 +113,16 @@ 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(); +/// 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, @@ -126,7 +138,7 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task Task Task) -> Task { + let platform = test_platform(); let shim_builder = crate::WindowsShimBuilder::::new_with_litebox(platform, litebox); let fs = Arc::new(shim_builder.brokered_fs()); @@ -184,6 +203,15 @@ pub(crate) fn test_task_with_nls_files(nls_files: &[(&str, &[u8])]) -> Task Date: Wed, 9 Sep 2026 16:03:15 -0700 Subject: [PATCH 06/34] Move file APIs onto LiteBox Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/errors.rs | 34 ++--- litebox/src/fs/{resolver.rs => file.rs} | 141 ++++++++---------- litebox/src/fs/mod.rs | 12 +- litebox/src/fs/tests.rs | 69 +++++---- litebox/src/litebox.rs | 18 +-- .../src/lib.rs | 11 +- litebox_runner_linux_userland/src/lib.rs | 6 +- .../src/lib.rs | 5 +- litebox_runner_windows_userland/src/lib.rs | 5 +- litebox_shim_linux/src/lib.rs | 32 ++-- litebox_shim_linux/src/syscalls/epoll.rs | 6 +- litebox_shim_linux/src/syscalls/file.rs | 77 +++++----- litebox_shim_linux/src/syscalls/mm.rs | 2 +- litebox_shim_linux/src/syscalls/process.rs | 6 +- litebox_shim_linux/src/syscalls/tests.rs | 3 +- litebox_shim_linux/src/syscalls/unix.rs | 8 +- litebox_shim_windows/src/lib.rs | 18 +-- litebox_shim_windows/src/loader/pe.rs | 32 ++-- litebox_shim_windows/src/syscalls/file.rs | 119 ++++++++------- litebox_shim_windows/src/syscalls/nls.rs | 23 ++- litebox_shim_windows/src/syscalls/registry.rs | 108 ++++++++------ litebox_shim_windows/src/syscalls/section.rs | 4 +- litebox_shim_windows/src/tests.rs | 4 +- 23 files changed, 353 insertions(+), 390 deletions(-) rename litebox/src/fs/{resolver.rs => file.rs} (87%) diff --git a/litebox/src/fs/errors.rs b/litebox/src/fs/errors.rs index 459caaea27..2f74333589 100644 --- a/litebox/src/fs/errors.rs +++ b/litebox/src/fs/errors.rs @@ -1,20 +1,14 @@ // 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 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 +28,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 +47,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 +61,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 +77,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 +92,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 +109,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 +126,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 +142,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 +158,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 +180,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 +192,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 { diff --git a/litebox/src/fs/resolver.rs b/litebox/src/fs/file.rs similarity index 87% rename from litebox/src/fs/resolver.rs rename to litebox/src/fs/file.rs index 4a4013f1bc..8e3fad0c51 100644 --- a/litebox/src/fs/resolver.rs +++ b/litebox/src/fs/file.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Guest filesystem facade backed by broker-owned file objects. +//! Guest file operations backed by broker-owned file objects. use alloc::string::{String, ToString}; use alloc::sync::Arc; @@ -16,11 +16,7 @@ use litebox_broker_protocol::fs::{ }; use crate::path::Arg; -use crate::{ - LiteBox, - fd::{EntryHandle, TypedFd}, - sync, -}; +use crate::{LiteBox, fd::EntryHandle, sync}; use super::errors::{ ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError, @@ -28,42 +24,18 @@ use super::errors::{ }; use super::{DirEntry, FileStatus, FileType, Mode, OFlags, SeekWhence, UserInfo}; -/// The guest-facing filesystem entry point. -pub struct Resolver { - litebox: LiteBox, - authority: ResolverAuthority, -} - -struct ResolverAuthority { - broker: Arc, -} +/// Type marker for file descriptors backed by broker-owned files. +pub struct File(core::marker::PhantomData); struct PinnedBrokerFile { - _entry: EntryHandle>, + _entry: EntryHandle>, broker: Arc, handle: ObjectHandle, } -impl Resolver { - /// Constructs a resolver whose filesystem authority is owned by the negotiated broker. - /// - /// # Panics - /// - /// Panics if `litebox` has no negotiated broker connection. - #[must_use] - pub fn new_brokered(litebox: &LiteBox) -> Self { - Self { - litebox: litebox.clone(), - authority: ResolverAuthority { - broker: litebox - .broker_control() - .expect("brokered file operations require a broker connection"), - }, - } - } - - fn broker_file(&self, fd: &TypedFd) -> Option> { - let entry_handle = self.litebox.descriptor_table().entry_handle(fd)?; +impl LiteBox { + fn broker_file(&self, fd: &FileFd) -> Option> { + let entry_handle = self.descriptor_table().entry_handle(fd)?; let (broker, handle) = { let entry = entry_handle.get_entry(); (Arc::clone(&entry.entry.broker), entry.entry.handle) @@ -82,18 +54,17 @@ impl Resolver { /// Opens a file. /// /// The `mode` is only significant when creating a file. - pub fn open( + pub fn open_file( &self, context: &Context, path: impl Arg, flags: OFlags, mode: Mode, - ) -> Result, OpenError> { + ) -> Result, OpenError> { let path = Self::broker_path(context, path)?; let (access, flags) = file_open_options(flags)?; - let handle = self - .authority - .broker + let broker = self.broker_control().ok_or(OpenError::Io)?; + let handle = broker .open_file( &path, file_user(context.acting_user()), @@ -103,17 +74,16 @@ impl Resolver { ) .map_err(|_| OpenError::Io)? .map_err(open_error)?; - Ok(self.litebox.descriptor_table_mut().insert(BrokerFile { - broker: Arc::clone(&self.authority.broker), - handle, - })) + Ok(self + .descriptor_table_mut() + .insert(BrokerFile { broker, handle })) } /// 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(); + 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); @@ -121,9 +91,9 @@ impl Resolver { } /// Read from a file descriptor at `offset` into a buffer. - pub fn read( + pub fn read_file( &self, - fd: &TypedFd, + fd: &FileFd, buf: &mut [u8], offset: Option, ) -> Result { @@ -142,9 +112,9 @@ impl Resolver { } /// Write from a buffer to a file descriptor at `offset`. - pub fn write( + pub fn write_file( &self, - fd: &TypedFd, + fd: &FileFd, buf: &[u8], offset: Option, ) -> Result { @@ -163,9 +133,9 @@ impl Resolver { } /// Reposition the read/write file offset. - pub fn seek( + pub fn seek_file( &self, - fd: &TypedFd, + fd: &FileFd, offset: isize, whence: SeekWhence, ) -> Result { @@ -180,9 +150,9 @@ impl Resolver { } /// Truncate the file to the specified length. - pub fn truncate( + pub fn truncate_file( &self, - fd: &TypedFd, + fd: &FileFd, length: usize, reset_offset: bool, ) -> Result<(), TruncateError> { @@ -198,17 +168,22 @@ impl Resolver { } /// Change the permissions of a file. - pub fn chmod(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), ChmodError> { + pub fn chmod_file( + &self, + context: &Context, + path: impl Arg, + mode: Mode, + ) -> Result<(), ChmodError> { let path = Self::broker_path(context, path)?; - self.authority - .broker + self.broker_control() + .ok_or(ChmodError::Io)? .chmod_file(&path, file_user(context.acting_user()), file_mode(mode)) .map_err(|_| ChmodError::Io)? .map_err(chmod_error) } /// Change the owner of a file. - pub fn chown( + pub fn chown_file( &self, context: &Context, path: impl Arg, @@ -216,45 +191,53 @@ impl Resolver { group: Option, ) -> Result<(), ChownError> { let path = Self::broker_path(context, path)?; - self.authority - .broker + self.broker_control() + .ok_or(ChownError::Io)? .chown_file(&path, file_user(context.acting_user()), user, group) .map_err(|_| ChownError::Io)? .map_err(chown_error) } /// Unlink a file. - pub fn unlink(&self, context: &Context, path: impl Arg) -> Result<(), UnlinkError> { + pub fn unlink_file(&self, context: &Context, path: impl Arg) -> Result<(), UnlinkError> { let path = Self::broker_path(context, path)?; - self.authority - .broker + self.broker_control() + .ok_or(UnlinkError::Io)? .unlink_file(&path, file_user(context.acting_user())) .map_err(|_| UnlinkError::Io)? .map_err(unlink_error) } /// Create a new directory. - pub fn mkdir(&self, context: &Context, path: impl Arg, mode: Mode) -> Result<(), MkdirError> { + pub fn mkdir_file( + &self, + context: &Context, + path: impl Arg, + mode: Mode, + ) -> Result<(), MkdirError> { let path = Self::broker_path(context, path)?; - self.authority - .broker + self.broker_control() + .ok_or(MkdirError::Io)? .mkdir_file(&path, file_user(context.acting_user()), file_mode(mode)) .map_err(|_| MkdirError::Io)? .map_err(mkdir_error) } /// Remove a directory. - pub fn rmdir(&self, context: &Context, path: impl Arg) -> Result<(), RmdirError> { + pub fn rmdir_file(&self, context: &Context, path: impl Arg) -> Result<(), RmdirError> { let path = Self::broker_path(context, path)?; - self.authority - .broker + self.broker_control() + .ok_or(RmdirError::Io)? .rmdir_file(&path, file_user(context.acting_user())) .map_err(|_| RmdirError::Io)? .map_err(rmdir_error) } /// Read directory entries from a directory file descriptor. - pub fn read_dir(&self, fd: &TypedFd) -> Result, ReadDirError> { + pub fn read_file_directory( + &self, + fd: &FileFd, + ) -> Result, ReadDirError> { let file = self.broker_file(fd).ok_or(ReadDirError::ClosedFd)?; let entries = file .broker @@ -265,23 +248,23 @@ impl Resolver { } /// Obtain the status of a path. - pub fn file_status( + pub fn path_file_status( &self, context: &Context, path: impl Arg, ) -> Result { let path = Self::broker_path(context, path)?; let status = self - .authority - .broker + .broker_control() + .ok_or(FileStatusError::Io)? .path_file_status(&path, file_user(context.acting_user())) .map_err(|_| FileStatusError::Io)? .map_err(file_status_error)?; file_status(status) } - /// Equivalent to [`Self::file_status`], but on an open `fd`. - pub fn fd_file_status(&self, fd: &TypedFd) -> Result { + /// 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)?; let status = file .broker @@ -294,7 +277,7 @@ impl Resolver { } /// Get static backing data for a file, if available and supported. - pub fn get_static_backing_data(&self, fd: &TypedFd) -> Option<&'static [u8]> { + pub fn get_static_file_backing_data(&self, fd: &FileFd) -> Option<&'static [u8]> { let _ = self.broker_file(fd)?; None } @@ -676,7 +659,7 @@ fn optional_device(device: Option) -> Result; + File; BrokerFile; - -> ResolverFd; + -> FileFd; } diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index 790c056c13..276f90ef55 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Guest-facing filesystem facade. +//! Guest-facing file values and operations. //! //! Filesystem resolution and backend implementations live in `litebox_broker_core`. This module //! retains LiteBox's guest values and descriptor integration, plus the 9P transport traits that @@ -13,7 +13,9 @@ use core::ffi::c_uint; use core::num::NonZeroUsize; pub mod errors; -pub mod resolver; +mod file; + +pub use file::{Context, File, FileFd, ResolvedPath}; #[doc(hidden)] pub mod nine_p { @@ -65,7 +67,7 @@ bitflags! { /// Types of files on a file-system. /// -/// See [`resolver::Resolver::file_status`]. +/// See [`crate::LiteBox::path_file_status`]. #[derive(Debug, PartialEq, Eq, Clone)] #[non_exhaustive] pub enum FileType { @@ -160,7 +162,7 @@ bitflags! { } } -/// The `whence` directive to [`resolver::Resolver::seek`] +/// The `whence` directive to [`crate::LiteBox::seek_file`]. #[derive(Copy, Clone)] pub enum SeekWhence { /// The file offset is set to `offset` bytes. @@ -213,7 +215,7 @@ pub struct NodeInfo { pub rdev: Option, } -/// Directory entries returned by [`resolver::Resolver::read_dir`] +/// Directory entries returned by [`crate::LiteBox::read_file_directory`]. #[derive(Debug)] #[non_exhaustive] pub struct DirEntry { diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 32f4e629f1..3f219aac3d 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Guest-facing filesystem facade tests. +//! Guest-facing file API tests. //! //! Filesystem resolution, backend, and 9P semantics belong to `litebox_broker_core` and are tested //! there. What LiteBox owns is the guest side of the boundary: resolving paths against a @@ -40,14 +40,13 @@ use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory, Sh use crate::fs::errors::{ OpenError, PathError, ReadDirError, ReadError, RmdirError, UnlinkError, WriteError, }; -use crate::fs::resolver::{Context, Resolver}; -use crate::fs::{FileType, Mode, OFlags, SeekWhence, UserInfo}; +use crate::fs::{Context, FileType, Mode, OFlags, SeekWhence, UserInfo}; use crate::platform::mock::MockPlatform; /// The handle the scripted broker hands out for every successful open. const FILE_HANDLE: ObjectHandle = ObjectHandle(7); -/// One broker request the facade issued, with any shared-buffer payload copied out. +/// One broker request LiteBox issued, with any shared-buffer payload copied out. #[derive(Debug, PartialEq, Eq)] enum Call { Open { @@ -355,19 +354,20 @@ impl SharedMemory for TestSharedMemory { } } -/// Build a guest filesystem facade whose broker answers from `script`. +/// Build a LiteBox whose broker answers from `script`. fn scripted_fs( script: impl IntoIterator, -) -> (Arc, Resolver) { +) -> (Arc, crate::LiteBox) { let broker = ScriptedBroker::new(script); let memory = Arc::clone(broker.buffers.memory()); let (local, ()) = BrokerLocal::negotiate(ScriptedChannel(Arc::clone(&broker)), |channel| { Ok((channel, memory as Arc, ())) }) .unwrap(); - let litebox = crate::LiteBox::new_with_broker_local(MockPlatform::new(), local); - let fs = Resolver::new_brokered(&litebox); - (broker, fs) + ( + broker, + crate::LiteBox::new_with_broker_local(MockPlatform::new(), local), + ) } fn opened() -> Scripted { @@ -413,7 +413,7 @@ fn open_sends_the_resolved_path_and_translated_flags() { let (broker, fs) = scripted_fs([opened()]); let fd = fs - .open( + .open_file( &context, "sub/../file.txt", OFlags::CREAT | OFlags::WRONLY | OFlags::APPEND, @@ -431,7 +431,7 @@ fn open_sends_the_resolved_path_and_translated_flags() { mode: FileMode::from_bits(0o700).unwrap(), }] ); - fs.close(&fd).expect("close should succeed"); + fs.close_file(&fd).expect("close should succeed"); } #[test] @@ -445,17 +445,17 @@ fn read_and_write_transfer_payloads_through_the_broker() { ]); let fd = fs - .open(&context, "/file", OFlags::RDWR, Mode::empty()) + .open_file(&context, "/file", OFlags::RDWR, Mode::empty()) .expect("open should succeed"); - assert_eq!(fs.write(&fd, b"hello", None).unwrap(), 5); + assert_eq!(fs.write_file(&fd, b"hello", None).unwrap(), 5); let mut buffer = vec![0; 16]; - let read = fs.read(&fd, &mut buffer, Some(2)).unwrap(); + let read = fs.read_file(&fd, &mut buffer, Some(2)).unwrap(); assert_eq!(&buffer[..read], b"broker"); - assert_eq!(fs.seek(&fd, -3, SeekWhence::RelativeToEnd).unwrap(), 3); - fs.close(&fd).expect("close should succeed"); + assert_eq!(fs.seek_file(&fd, -3, SeekWhence::RelativeToEnd).unwrap(), 3); + fs.close_file(&fd).expect("close should succeed"); let calls = broker.calls(); assert_eq!( @@ -491,20 +491,20 @@ fn closing_releases_the_broker_object_and_the_descriptor() { let (broker, fs) = scripted_fs([opened()]); let fd = fs - .open(&context, "/file", OFlags::RDONLY, Mode::empty()) + .open_file(&context, "/file", OFlags::RDONLY, Mode::empty()) .expect("open should succeed"); - fs.close(&fd).expect("close should succeed"); + fs.close_file(&fd).expect("close should succeed"); assert_eq!(broker.calls()[1], Call::Close(FILE_HANDLE)); // The descriptor no longer names a broker file, so operations on it report a closed fd // instead of reaching the broker. let mut buffer = [0; 4]; assert!(matches!( - fs.read(&fd, &mut buffer, None), + fs.read_file(&fd, &mut buffer, None), Err(ReadError::ClosedFd) )); assert!(matches!( - fs.write(&fd, b"x", None), + fs.write_file(&fd, b"x", None), Err(WriteError::ClosedFd) )); assert_eq!(broker.calls().len(), 2); @@ -529,7 +529,7 @@ fn path_status_converts_broker_values() { ))]); let status = fs - .file_status(&context, "/dev/null") + .path_file_status(&context, "/dev/null") .expect("status should succeed"); assert_eq!(status.file_type, FileType::CharacterDevice); @@ -587,15 +587,17 @@ fn read_dir_reassembles_paged_broker_entries() { ]); let fd = fs - .open( + .open_file( &context, "/dir", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .expect("open should succeed"); - let entries = fs.read_dir(&fd).expect("read_dir should succeed"); - fs.close(&fd).expect("close should succeed"); + let entries = fs + .read_file_directory(&fd) + .expect("read_dir should succeed"); + fs.close_file(&fd).expect("close should succeed"); assert_eq!(entries.len(), 2); assert_eq!(entries[0].name, "one"); @@ -636,32 +638,35 @@ fn broker_file_errors_map_to_guest_errors() { ]); assert!(matches!( - fs.open(&context, "/missing", OFlags::RDONLY, Mode::empty()), + fs.open_file(&context, "/missing", OFlags::RDONLY, Mode::empty()), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) )); assert!(matches!( - fs.open(&context, "/secret", OFlags::RDONLY, Mode::empty()), + fs.open_file(&context, "/secret", OFlags::RDONLY, Mode::empty()), Err(OpenError::AccessNotAllowed) )); assert!(matches!( - fs.unlink(&context, "/locked/file"), + fs.unlink_file(&context, "/locked/file"), Err(UnlinkError::NoWritePerms) )); assert!(matches!( - fs.rmdir(&context, "/full"), + fs.rmdir_file(&context, "/full"), Err(RmdirError::NotEmpty) )); let fd = fs - .open(&context, "/file", OFlags::WRONLY, Mode::empty()) + .open_file(&context, "/file", OFlags::WRONLY, Mode::empty()) .expect("open should succeed"); let mut buffer = [0; 4]; assert!(matches!( - fs.read(&fd, &mut buffer, None), + fs.read_file(&fd, &mut buffer, None), Err(ReadError::NotForReading) )); - assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); - fs.close(&fd).expect("close should succeed"); + assert!(matches!( + fs.read_file_directory(&fd), + Err(ReadDirError::NotADirectory) + )); + fs.close_file(&fd).expect("close should succeed"); assert_eq!(broker.calls().len(), 8); } diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 6007e891af..5c38f5e55e 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -28,6 +28,15 @@ pub struct LiteBox { pub(crate) x: Arc>, } +impl Clone for LiteBox { + /// Creates another handle to the same LiteBox system. + fn clone(&self) -> Self { + Self { + x: Arc::clone(&self.x), + } + } +} + impl LiteBox { /// Create a new (empty) [`LiteBox`] instance for the given `platform`. /// @@ -113,15 +122,6 @@ impl LiteBox { } } - /// An explicitly-crate-internal clone method to prevent outside users from cloning the - /// [`LiteBox`] object, which could cause confusion as to the intended use. External users must - /// only create it via [`Self::new`]. - pub(crate) fn clone(&self) -> Self { - Self { - x: Arc::clone(&self.x), - } - } - /// Access to the file descriptor table. /// /// Note: this takes a lock, and thus should ideally not be held on to for too long to prevent diff --git a/litebox_runner_linux_on_windows_userland/src/lib.rs b/litebox_runner_linux_on_windows_userland/src/lib.rs index 0ae3f58d37..50be8fd6cd 100644 --- a/litebox_runner_linux_on_windows_userland/src/lib.rs +++ b/litebox_runner_linux_on_windows_userland/src/lib.rs @@ -92,9 +92,6 @@ pub fn run(cli_args: CliArgs) -> Result<()> { // 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 = shim_builder.brokered_fs(); - let initial_file_system = std::sync::Arc::new(initial_file_system); - let shim = shim_builder.build(); let argv = cli_args .program_and_arguments @@ -118,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_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 664decfbf1..1e55daf341 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -181,8 +181,6 @@ pub fn run(cli_args: CliArgs) -> Result { gid: u32::from(DEFAULT_GUEST_GID), egid: u32::from(DEFAULT_GUEST_GID), }; - let initial_file_system = shim_builder.brokered_fs(); - // 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. @@ -198,8 +196,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 @@ -223,7 +219,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(); diff --git a/litebox_runner_windows_on_linux_userland/src/lib.rs b/litebox_runner_windows_on_linux_userland/src/lib.rs index 52fe9ce671..9bf794939b 100644 --- a/litebox_runner_windows_on_linux_userland/src/lib.rs +++ b/litebox_runner_windows_on_linux_userland/src/lib.rs @@ -100,9 +100,6 @@ pub fn run(cli_args: CliArgs) -> Result<()> { .split_first() .context("program path missing - clap should have required at least one argument")?; - let initial_file_system = shim_builder.brokered_fs(); - 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)) @@ -125,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/src/lib.rs b/litebox_runner_windows_userland/src/lib.rs index 5ec99ef557..53d8b26256 100644 --- a/litebox_runner_windows_userland/src/lib.rs +++ b/litebox_runner_windows_userland/src/lib.rs @@ -89,9 +89,6 @@ pub fn run(cli_args: CliArgs) -> Result { .split_first() .context("program path missing — clap should have required at least one argument")?; - let initial_file_system = shim_builder.brokered_fs(); - 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)) @@ -114,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_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 4facde6970..b6340e1e48 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -58,11 +58,7 @@ 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) type FileFd = litebox::fs::FileFd; /// Aggregate bound capturing everything the shim requires of a platform. /// @@ -222,12 +218,6 @@ impl LinuxShimBuilder { &self.litebox } - /// Creates a filesystem facade backed by the negotiated broker. - #[must_use] - pub fn brokered_fs(&self) -> DefaultFS { - litebox::fs::resolver::Resolver::new_brokered(&self.litebox) - } - /// Build the shim. pub fn build(self) -> LinuxShim { let net = Network::new(&self.litebox); @@ -259,7 +249,6 @@ impl LinuxShim { /// initial register state. pub fn load_program( &self, - fs: alloc::sync::Arc>, task: litebox_common_linux::TaskParams, path: &str, argv: Vec, @@ -274,7 +263,7 @@ impl LinuxShim { egid, } = task; - let files = syscalls::file::FilesState::new(fs); + let files = syscalls::file::FilesState::new(&self.0.litebox); files.set_max_fd(syscalls::process::RLIMIT_NOFILE_CUR); let files = Arc::new(files); let credentials = Arc::new(syscalls::process::Credentials { @@ -375,20 +364,20 @@ 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()) + .open_file(context, "/dev/stdin", OFlags::RDONLY, Mode::empty()) .unwrap(); let stdout = self .fs - .open(context, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .open_file(context, "/dev/stdout", OFlags::WRONLY, Mode::empty()) .unwrap(); let stderr = self .fs - .open(context, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .open_file(context, "/dev/stderr", OFlags::WRONLY, Mode::empty()) .unwrap(); let mut dt = global.litebox.descriptor_table_mut(); let mut rds = self.raw_descriptor_store.write(); @@ -444,7 +433,7 @@ impl syscalls::file::FilesState { }; } - resolve_fd!(LinuxFS, Fs); + resolve_fd!(litebox::fs::File, Fs); resolve_fd!(Network, Network); resolve_fd!(Pipes, Pipes); resolve_fd!(syscalls::eventfd::EventfdSubsystem, Eventfd); @@ -1213,14 +1202,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(&self.litebox)); let credentials = Arc::new(syscalls::process::Credentials { uid: 0, euid: 0, diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 247c70e350..8783581355 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 { @@ -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) { @@ -702,7 +702,7 @@ mod test { .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(&task.files.borrow().fs); let fds = task.files.borrow().clone(); set.add_fd(rfd, Events::IN); diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 33fd61d5d3..15e1d68aa7 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -24,9 +24,7 @@ use litebox_common_linux::{ }; 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)] @@ -49,7 +47,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 { @@ -72,7 +70,7 @@ 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(), @@ -88,7 +86,7 @@ impl FsState { /// 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) fs: litebox::LiteBox, pub(crate) raw_descriptor_store: litebox::sync::RwLock, /// Exclusive upper bound for raw file descriptor values. @@ -96,9 +94,9 @@ pub(crate) struct FilesState { } impl FilesState { - pub(crate) fn new(fs: alloc::sync::Arc>) -> Self { + pub(crate) fn new(fs: &litebox::LiteBox) -> Self { Self { - fs, + fs: fs.clone(), raw_descriptor_store: litebox::sync::RwLock::new( litebox::fd::RawDescriptorStorage::new(), ), @@ -386,7 +384,7 @@ impl Task { let context = fs.context.read(); files .fs - .open(&context, &path, flags - OFlags::CLOEXEC, mode) + .open_file(&context, &path, flags - OFlags::CLOEXEC, mode) .map_err(Errno::from) }?; if let Some(stream) = stream { @@ -424,7 +422,7 @@ impl Task { } let files = self.files.borrow(); let raw_fd = files.insert_raw_fd(file).map_err(|file| { - files.fs.close(&file).unwrap(); + files.fs.close_file(&file).unwrap(); Errno::EMFILE })?; Ok(u32::try_from(raw_fd).unwrap()) @@ -465,7 +463,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| { + files + .fs + .truncate_file(fd, length, false) + .map_err(Errno::from) + }, |_fd| todo!("net"), |_fd| todo!("pipes"), |_fd| Err(Errno::EINVAL), @@ -501,7 +504,7 @@ impl Task { mode, )?; let files = self.files.borrow(); - let _ = files.fs.close(&file); + let _ = files.fs.close_file(&file); } // TODO: Named pipe, socket, block and char files are not supported InodeType::NamedPipe @@ -530,9 +533,9 @@ impl Task { 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) + files.fs.rmdir_file(&context, path).map_err(Errno::from) } else { - files.fs.unlink(&context, path).map_err(Errno::from) + files.fs.unlink_file(&context, path).map_err(Errno::from) } } @@ -559,7 +562,7 @@ impl Task { |fd| { files .fs - .read(fd, &mut buf.borrow_mut(), offset) + .read_file(fd, &mut buf.borrow_mut(), offset) .map_err(Errno::from) }, |fd| { @@ -639,7 +642,7 @@ impl Task { 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| files.fs.write_file(fd, buf, offset).map_err(Errno::from), |fd| { espipe_for_non_seekable_offset(offset)?; is_inet_datagram.set(matches!( @@ -720,7 +723,7 @@ impl Task { let files = self.files.borrow(); files .fs - .seek(fd, -rewind, SeekWhence::RelativeToCurrentOffset) + .seek_file(fd, -rewind, SeekWhence::RelativeToCurrentOffset) .map(|_| ()) .map_err(Errno::from) } @@ -764,7 +767,7 @@ impl Task { let read_result = match typed_in_fd.as_fs() { Some(fd) => files .fs - .read(fd, &mut kernel_buf[..to_read], cur_off) + .read_file(fd, &mut kernel_buf[..to_read], cur_off) .map_err(Errno::from), None => Err(non_fs_err), }; @@ -846,7 +849,7 @@ impl Task { ) -> Result { let fd = fd.fs_only(Errno::ESPIPE)?; let files = self.files.borrow(); - match files.fs.seek(fd, offset, whence) { + match files.fs.seek_file(fd, offset, whence) { Ok(pos) => Ok(pos), Err(litebox::fs::errors::SeekError::NotAFile) => { let base = match whence { @@ -877,7 +880,7 @@ impl Task { let context = fs.context.read(); files .fs - .mkdir(&context, pathname, mode) + .mkdir_file(&context, pathname, mode) .map_err(Errno::from) } @@ -893,7 +896,7 @@ impl Task { } 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 +919,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 +971,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) + files.fs.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), @@ -1404,7 +1407,7 @@ impl Task { let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files.fs.file_status(&context, pathname)? + files.fs.path_file_status(&context, pathname)? }; let owner = status.owner.into(); Self::do_access_mode(status.mode, owner, caller, &mode) @@ -1556,13 +1559,7 @@ impl Task { let rw_user_mode = (Mode::RUSR | Mode::WUSR).bits(); let files = self.files.borrow(); fd.dispatch( - |fd| { - files - .fs - .fd_file_status(fd) - .map(T::from) - .map_err(Errno::from) - }, + |fd| files.fs.file_status(fd).map(T::from).map_err(Errno::from), |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { Ok(T::from(synthetic( @@ -1595,7 +1592,7 @@ impl Task { let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files.fs.file_status(&context, path)? + files.fs.path_file_status(&context, path)? }; Ok(T::from(status)) } @@ -1643,7 +1640,7 @@ impl Task { let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - Ok(T::from(files.fs.file_status(&context, cwd)?)) + Ok(T::from(files.fs.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)) @@ -1936,7 +1933,7 @@ impl Task { { let files = self.files.borrow(); let context = fs.context.read(); - match files.fs.file_status(&context, target.to_string()) { + match files.fs.path_file_status(&context, target.to_string()) { Ok(status) => { if status.file_type != FileType::Directory { return Err(Errno::ENOTDIR); @@ -2048,8 +2045,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); @@ -2608,7 +2609,7 @@ impl Task { file.dispatch( |fd| { dup(self, &files, fd, close_on_exec, target, |fd| { - let _ = files.fs.close(&fd); + let _ = files.fs.close_file(&fd); }) }, |fd| { @@ -2733,7 +2734,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 = files.fs.read_file_directory(file)?; entries.sort_by(|a, b| a.name.cmp(&b.name)); for entry in entries.iter().skip(dir_off) { diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index b807d69617..b20f0511f8 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -274,7 +274,7 @@ impl Task { } let files = self.files.borrow(); - let static_data = files.fs.get_static_backing_data(fd.as_fs()?)?; + let static_data = files.fs.get_static_file_backing_data(fd.as_fs()?)?; if offset > static_data.len() { return None; diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index 6d77350a76..64ab21fdc2 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1407,14 +1407,14 @@ impl Task { )?; 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 files.fs.read_file(&file, &mut header, Some(0)) { Ok(n) => n, Err(e) => { - let _ = files.fs.close(&file); + let _ = files.fs.close_file(&file); return Err(Errno::from(e)); } }; - let _ = files.fs.close(&file); + let _ = files.fs.close_file(&file); match parse_shebang(&header[..n]) { Some((interp, opt_arg)) => { diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 8170a5a2c7..b0e968f556 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -73,8 +73,7 @@ fn init_platform_with_files(files: Arc) -> crate::Task { - Path((String, FileFd, Arc>)), + Path((String, FileFd, litebox::LiteBox)), Abstract(Vec), } @@ -123,7 +123,7 @@ impl UnixSocketAddr { let context = fs.context.read(); files .fs - .open( + .open_file( &context, path.as_str(), flags, @@ -174,7 +174,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_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 05c35d9f9d..2d98d9929b 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -216,10 +216,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, @@ -413,12 +409,6 @@ impl WindowsShimBuilder { &self.litebox } - /// Creates a filesystem facade backed by the negotiated broker. - #[must_use] - pub fn brokered_fs(&self) -> DefaultFS { - litebox::fs::resolver::Resolver::new_brokered(&self.litebox) - } - #[must_use] pub fn build(self) -> WindowsShim { let global = Arc::new(GlobalState { @@ -497,7 +487,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, @@ -506,6 +495,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::new(self.0.litebox.clone()); 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. @@ -529,7 +519,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, @@ -718,8 +708,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, diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 57dbc47c65..f7ddfa2a85 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -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 { @@ -911,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( @@ -936,7 +936,7 @@ fn load_ntdll( fn load_image( platform: &'static Platform, - fs: Arc>, + fs: Arc>, path: &str, page_manager: &crate::WindowsPageManager, ) -> Result { @@ -945,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, @@ -968,14 +968,14 @@ 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() @@ -996,7 +996,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, @@ -1107,14 +1107,14 @@ 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, Mode::empty(), @@ -1128,7 +1128,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); } @@ -1143,7 +1143,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,7 +1163,7 @@ impl ReadAt for &'_ PeImageFile { fn size(&mut self) -> Result { self.fs - .fd_file_status(&self.fd)? + .file_status(&self.fd)? .size .try_into() .map_err(|_| PeImageAccessError::AddressOverflow) diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index a3588923ae..58d6ae2840 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -8,7 +8,7 @@ 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, @@ -499,13 +499,13 @@ struct DirectoryQueryState { 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`. @@ -893,20 +893,20 @@ impl Task { 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 +981,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 @@ -1083,11 +1083,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 @@ -1181,7 +1181,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) @@ -1249,10 +1249,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); @@ -1339,10 +1341,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 +1475,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 +1505,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 +1578,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 +1616,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,7 +1673,7 @@ 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) } @@ -1970,24 +1976,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)); } @@ -2340,15 +2350,8 @@ 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) @@ -2358,19 +2361,19 @@ impl Task { let 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, 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); @@ -2399,7 +2402,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 +2416,7 @@ impl Task { ) => { self.fs - .mkdir( + .mkdir_file( &self.fs_context, path, create_directory_mode(file_attributes), @@ -2432,7 +2435,7 @@ impl Task { let 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, flags, Mode::empty()) .map_err(|error| map_open_error(error, create_disposition))?; let information = create_disposition.success_information(existed_before_open); Ok(( @@ -2823,15 +2826,15 @@ 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, 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( @@ -3184,7 +3187,7 @@ mod tests { 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, @@ -3308,12 +3311,12 @@ mod tests { let fd = task .fs - .open(&task.fs_context, path, OFlags::RDONLY, Mode::empty()) + .open_file(&task.fs_context, path, OFlags::RDONLY, 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(); }); } @@ -3342,7 +3345,7 @@ mod tests { 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(); @@ -3793,7 +3796,7 @@ mod tests { run_with_test_platform_pointers(|| { 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"); @@ -3978,7 +3981,7 @@ mod tests { 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, @@ -4214,7 +4217,7 @@ mod tests { fn nt_create_file_actual_directory_handles_can_root_relative_opens() { 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, @@ -4467,13 +4470,13 @@ 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)) )); } @@ -4503,13 +4506,13 @@ 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)) )); } diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs index 7a6ce56877..7e76613ba5 100644 --- a/litebox_shim_windows/src/syscalls/nls.rs +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -5,7 +5,6 @@ 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}; @@ -456,7 +455,7 @@ struct MappedNlsSection { } struct NlsSectionFile { - fd: TypedFd>, + 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(), @@ -774,7 +773,7 @@ impl Task { 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, @@ -782,19 +781,19 @@ impl Task { ) .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); } @@ -806,7 +805,7 @@ impl Task { fn copy_nls_section_file( &self, - fd: &TypedFd>, + fd: &litebox::fs::FileFd, section_len: usize, output: MutPtr, ) -> Result { @@ -817,7 +816,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); diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 6d2f8061a0..50f8f828a4 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -39,7 +39,7 @@ 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, }; @@ -58,7 +58,7 @@ use crate::nt_types::{ read_unicode_string_at, }; -type RegistryFileSystem = litebox::fs::resolver::Resolver; +type RegistryFileSystem = LiteBox; pub(crate) struct RegistryKeySubsystem(PhantomData); @@ -78,12 +78,12 @@ impl crate::WindowsHandleSubsystem 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_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 @@ -612,8 +612,8 @@ impl RegistryStore { /// seeded lazily by [`Self::fs`] when the guest first uses the registry. pub(crate) fn new(litebox: &LiteBox) -> Self { Self { - fs: litebox::fs::resolver::Resolver::new_brokered(litebox), - fs_context: litebox::fs::resolver::Context::new(), + fs: litebox.clone(), + fs_context: litebox::fs::Context::new(), defaults_seeded: Mutex::new(false), notification_state: Mutex::new(RegistryNotificationState::default()), notification_pollee: Pollee::new(), @@ -640,9 +640,9 @@ impl RegistryStore { &self, path: &str, desired_access: RegistryKeyAccess, - ) -> Result>, NtStatus> { + ) -> Result, NtStatus> { self.fs() - .open(&self.fs_context, path, desired_access.into(), Mode::empty()) + .open_file(&self.fs_context, path, desired_access.into(), Mode::empty()) .map_err(map_open_error) } @@ -654,7 +654,7 @@ impl RegistryStore { let value_path = value_path(key_path, value_name)?; let status = self .fs() - .file_status(&self.fs_context, &*value_path) + .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); @@ -665,7 +665,7 @@ impl RegistryStore { let fd = self .fs() - .open( + .open_file( &self.fs_context, &*value_path, OFlags::RDONLY, @@ -674,7 +674,7 @@ impl RegistryStore { .map_err(map_open_error)?; let mut data = vec![0; status.size]; let result = read_exact_at(self.fs(), &fd, &mut data); - let _ = self.fs().close(&fd); + let _ = self.fs().close_file(&fd); result?; let value_type = u32::from_le_bytes( @@ -766,7 +766,11 @@ impl RegistryStore { 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 != ".." @@ -782,15 +786,18 @@ impl RegistryStore { let values_path = format!("{}/{}", key.path.trim_end_matches('/'), VALUES_DIR_NAME); let values_fd = self .fs() - .open( + .open_file( &self.fs_context, &*values_path, OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; - let values = self.fs().read_dir(&values_fd).map_err(map_read_dir_error); - let _ = self.fs().close(&values_fd); + let 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; @@ -802,7 +809,7 @@ impl RegistryStore { let path = format!("{values_path}/{}", entry.name); let size = self .fs() - .file_status(&self.fs_context, &*path) + .path_file_status(&self.fs_context, &*path) .map_err(map_file_status_error)? .size; if size < REGISTRY_VALUE_TYPE_SIZE { @@ -827,7 +834,11 @@ impl RegistryStore { 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 != ".." @@ -849,7 +860,7 @@ impl RegistryStore { let child_path = format!("{}/{}", key.path.trim_end_matches('/'), name); let child_fd = self .fs() - .open( + .open_file( &self.fs_context, &*child_path, OFlags::RDONLY | OFlags::DIRECTORY, @@ -861,7 +872,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 } @@ -879,15 +890,18 @@ impl RegistryStore { let values_path = format!("{}/{}", key.path.trim_end_matches('/'), VALUES_DIR_NAME); let values_fd = self .fs() - .open( + .open_file( &self.fs_context, &*values_path, OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; - let entries = self.fs().read_dir(&values_fd).map_err(map_read_dir_error); - let _ = self.fs().close(&values_fd); + let 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 { @@ -906,7 +920,7 @@ impl RegistryStore { /// defaults were written during shim construction. fn seed_defaults( fs: &RegistryFileSystem, - fs_context: &litebox::fs::resolver::Context, + fs_context: &litebox::fs::Context, ) { for key in [ DEFAULT_SESSION_MANAGER_KEY, @@ -1102,7 +1116,7 @@ impl Task { } pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { - let _ = self.global.registry.fs().close(&key.fd); + let _ = self.global.registry.fs().close_file(&key.fd); } pub(crate) fn sys_nt_open_key( @@ -1241,7 +1255,7 @@ impl Task { .global .registry .fs() - .file_status(&self.global.registry.fs_context, &path) + .path_file_status(&self.global.registry.fs_context, &path) { Ok(status) if status.file_type == FileType::Directory => { RegistryKeyDisposition::OpenedExistingKey @@ -2236,7 +2250,7 @@ fn is_valid_key_component(component: &str) -> bool { fn write_value_in_fs( fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + context: &litebox::fs::Context, key_nt_path: &str, value_name: &str, value_type: RegistryValueType, @@ -2248,7 +2262,7 @@ fn write_value_in_fs( fn write_value_at_path( fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + context: &litebox::fs::Context, key_path: &str, value_name: &str, value_type: u32, @@ -2256,7 +2270,7 @@ 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, @@ -2267,18 +2281,20 @@ fn write_value_at_path( write_all_at(fs, &fd, &value_type.to_le_bytes(), 0)?; write_all_at(fs, &fd, value, REGISTRY_VALUE_TYPE_SIZE) })(); - let _ = fs.close(&fd); + let _ = fs.close_file(&fd); result } fn read_exact_at( fs: &RegistryFileSystem, - fd: &TypedFd>, + fd: &litebox::fs::FileFd, mut data: &mut [u8], ) -> Result<(), NtStatus> { let mut offset = 0; while !data.is_empty() { - let read = fs.read(fd, data, Some(offset)).map_err(map_read_error)?; + let read = fs + .read_file(fd, data, Some(offset)) + .map_err(map_read_error)?; if read == 0 { return Err(NtStatus::UNSUCCESSFUL); } @@ -2290,12 +2306,14 @@ fn read_exact_at( fn write_all_at( fs: &RegistryFileSystem, - fd: &TypedFd>, + fd: &litebox::fs::FileFd, mut data: &[u8], mut offset: usize, ) -> Result<(), NtStatus> { while !data.is_empty() { - let written = fs.write(fd, data, Some(offset)).map_err(map_write_error)?; + let written = fs + .write_file(fd, data, Some(offset)) + .map_err(map_write_error)?; if written == 0 { return Err(NtStatus::DISK_FULL); } @@ -2307,7 +2325,7 @@ fn write_all_at( fn create_key_in_fs( fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + context: &litebox::fs::Context, nt_path: &str, ) -> Result { let path = absolute_nt_key_name_to_fs_path(nt_path)?; @@ -2317,7 +2335,7 @@ fn create_key_in_fs( fn create_key_path_in_fs( fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + context: &litebox::fs::Context, path: &str, ) -> Result, NtStatus> { let mut current = String::new(); @@ -2342,15 +2360,15 @@ fn create_key_path_in_fs( fn ensure_directory_in_fs( fs: &RegistryFileSystem, - context: &litebox::fs::resolver::Context, + 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, @@ -2734,7 +2752,9 @@ mod tests { // The raw store is read directly here so that the read itself does not seed it. assert!(matches!( - registry.fs.file_status(®istry.fs_context, &*value_path), + registry + .fs + .path_file_status(®istry.fs_context, &*value_path), Err(FileStatusError::PathError( PathError::NoSuchFileOrDirectory | PathError::MissingComponent )) @@ -2765,7 +2785,7 @@ mod tests { assert_eq!( registry .fs() - .file_status(®istry.fs_context, &*value_path) + .path_file_status(®istry.fs_context, &*value_path) .unwrap() .file_type, FileType::RegularFile @@ -2773,7 +2793,7 @@ mod tests { assert_eq!( registry .fs() - .file_status(®istry.fs_context, &*value_path) + .path_file_status(®istry.fs_context, &*value_path) .unwrap() .size, REGISTRY_VALUE_TYPE_SIZE + DEFAULT_ACP_VALUE.len() @@ -2782,10 +2802,10 @@ mod tests { assert_eq!(value.value_type, u32::from(RegistryValueType::Sz)); assert_eq!(value.data, DEFAULT_ACP_VALUE); - let broker_fs = litebox::fs::resolver::Resolver::new_brokered(&litebox); + let broker_fs = litebox; assert_eq!( broker_fs - .file_status(&litebox::fs::resolver::Context::new(), value_path.as_str(),) + .path_file_status(&litebox::fs::Context::new(), value_path.as_str()) .unwrap() .file_type, FileType::RegularFile @@ -3296,7 +3316,7 @@ mod tests { task.global .registry .fs() - .chmod( + .chmod_file( &task.global.registry.fs_context, &*private_path, Mode::WUSR | Mode::XUSR, diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 6e93c5d8d5..512a98dbba 100644 --- a/litebox_shim_windows/src/syscalls/section.rs +++ b/litebox_shim_windows/src/syscalls/section.rs @@ -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 { @@ -1181,7 +1181,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>, diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 463162ec38..c83a80f299 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -170,8 +170,8 @@ fn test_task_from_litebox(litebox: litebox::LiteBox) -> Task::new_with_litebox(platform, litebox); - let fs = Arc::new(shim_builder.brokered_fs()); - let fs_context = litebox::fs::resolver::Context::new(); + let fs = Arc::new(shim_builder.litebox().clone()); + let fs_context = litebox::fs::Context::new(); let shim = shim_builder.build(); let WindowsShim(global) = shim; From 3d8377a6d48a9b3a854f4fb7d5c7c1a74b848965 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 9 Sep 2026 16:08:49 -0700 Subject: [PATCH 07/34] Consolidate guest file module Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/file.rs | 216 ++++++++++++++++++++++++++++++++++++++++- litebox/src/fs/mod.rs | 214 +--------------------------------------- 2 files changed, 215 insertions(+), 215 deletions(-) diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index 8e3fad0c51..ebce16d627 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -7,6 +7,9 @@ use alloc::string::{String, ToString}; use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; +use bitflags::bitflags; +use core::ffi::c_uint; +use core::num::NonZeroUsize; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::error::ErrorCode; @@ -22,7 +25,210 @@ use super::errors::{ ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; -use super::{DirEntry, FileStatus, FileType, Mode, OFlags, SeekWhence, UserInfo}; + +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 [`LiteBox::path_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 + #[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 [`LiteBox::seek_file`]. +#[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 [`LiteBox::read_file_directory`]. +#[derive(Debug)] +#[non_exhaustive] +pub struct DirEntry { + pub name: String, + pub file_type: FileType, + pub ino_info: Option, +} + +impl UserInfo { + /// The root user + pub const ROOT: Self = Self { user: 0, group: 0 }; +} /// Type marker for file descriptors backed by broker-owned files. pub struct File(core::marker::PhantomData); @@ -630,16 +836,16 @@ fn file_type(file_type: BrokerFileType) -> Result { } } -fn status_node_info(node: FileNodeInfo) -> Result { - Ok(super::NodeInfo { +fn status_node_info(node: FileNodeInfo) -> Result { + Ok(NodeInfo { dev: usize::try_from(node.dev).map_err(|_| FileStatusError::Io)?, ino: usize::try_from(node.ino).map_err(|_| FileStatusError::Io)?, rdev: optional_device(node.rdev).map_err(|()| FileStatusError::Io)?, }) } -fn directory_node_info(node: FileNodeInfo) -> Result { - Ok(super::NodeInfo { +fn directory_node_info(node: FileNodeInfo) -> Result { + Ok(NodeInfo { dev: usize::try_from(node.dev).map_err(|_| ReadDirError::Io)?, ino: usize::try_from(node.ino).map_err(|_| ReadDirError::Io)?, rdev: optional_device(node.rdev).map_err(|()| ReadDirError::Io)?, diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index 276f90ef55..ce0afab5a3 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -7,15 +7,13 @@ //! retains LiteBox's guest values and descriptor integration, plus the 9P transport traits that //! deployments implement. -use bitflags::bitflags; - -use core::ffi::c_uint; -use core::num::NonZeroUsize; - pub mod errors; mod file; -pub use file::{Context, File, FileFd, ResolvedPath}; +pub use file::{ + Context, DirEntry, File, FileFd, FileStatus, FileType, Mode, NodeInfo, OFlags, ResolvedPath, + SeekWhence, UserInfo, +}; #[doc(hidden)] pub mod nine_p { @@ -24,207 +22,3 @@ pub mod nine_p { #[cfg(test)] mod 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 [`crate::LiteBox::path_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 [`crate::LiteBox::seek_file`]. -#[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 [`crate::LiteBox::read_file_directory`]. -#[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 }; -} From f4e7b4450483d0bf1759d07fd98a7dc909d24c70 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 9 Sep 2026 16:25:35 -0700 Subject: [PATCH 08/34] Reference broker files during operations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/file.rs | 26 ++++------------ litebox/src/fs/tests.rs | 67 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index ebce16d627..df45f4ae3c 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -19,7 +19,7 @@ use litebox_broker_protocol::fs::{ }; use crate::path::Arg; -use crate::{LiteBox, fd::EntryHandle, sync}; +use crate::{LiteBox, sync}; use super::errors::{ ChmodError, ChownError, CloseError, FileStatusError, MkdirError, OpenError, PathError, @@ -233,24 +233,10 @@ impl UserInfo { /// Type marker for file descriptors backed by broker-owned files. pub struct File(core::marker::PhantomData); -struct PinnedBrokerFile { - _entry: EntryHandle>, - broker: Arc, - handle: ObjectHandle, -} - impl LiteBox { - fn broker_file(&self, fd: &FileFd) -> Option> { - let entry_handle = self.descriptor_table().entry_handle(fd)?; - let (broker, handle) = { - let entry = entry_handle.get_entry(); - (Arc::clone(&entry.entry.broker), entry.entry.handle) - }; - Some(PinnedBrokerFile { - _entry: entry_handle, - broker, - handle, - }) + 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 { @@ -282,7 +268,7 @@ impl LiteBox { .map_err(open_error)?; Ok(self .descriptor_table_mut() - .insert(BrokerFile { broker, handle })) + .insert(Arc::new(BrokerFile { broker, handle }))) } /// Close the file at `fd`. @@ -866,6 +852,6 @@ fn optional_device(device: Option) -> Result; - BrokerFile; + Arc; -> FileFd; } diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 3f219aac3d..82162bd0a1 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -17,7 +17,7 @@ use alloc::string::{String, ToString as _}; use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; -use std::sync::Mutex; +use std::sync::{Barrier, Mutex}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::fs::{ @@ -96,6 +96,12 @@ enum Scripted { Reply(FileResponse), /// Stage `data` in the request's shared buffer and report it as read. Read(Vec), + /// Wait for the test to close the fd before completing this read. + BlockingRead { + data: Vec, + entered: Arc, + release: Arc, + }, /// Answer directory reads from `entries`, at most `page_bytes` of them per response. Directory { entries: Vec, @@ -183,6 +189,21 @@ impl ScriptedBroker { read: u32::try_from(data.len()).unwrap(), }) } + Scripted::BlockingRead { + data, + entered, + release, + } => { + entered.wait(); + release.wait(); + assert!(data.len() <= request.buffer.length as usize); + self.buffers + .write(request.buffer.slot_index, &data) + .unwrap(); + FileResponse::Read(ReadFileResponse { + read: u32::try_from(data.len()).unwrap(), + }) + } Scripted::Directory { .. } => panic!("scripted directory answer for a read"), } } @@ -267,7 +288,7 @@ impl ScriptedBroker { fn reply(&self) -> FileResponse { match self.next_scripted() { Scripted::Reply(response) => response, - Scripted::Read(_) | Scripted::Directory { .. } => { + Scripted::Read(_) | Scripted::BlockingRead { .. } | Scripted::Directory { .. } => { panic!("scripted payload answer for a request that carries none") } } @@ -510,6 +531,48 @@ fn closing_releases_the_broker_object_and_the_descriptor() { assert_eq!(broker.calls().len(), 2); } +#[test] +fn in_flight_read_keeps_the_broker_file_alive_after_close() { + let entered = Arc::new(Barrier::new(2)); + let release = Arc::new(Barrier::new(2)); + let (broker, fs) = scripted_fs([ + opened(), + Scripted::BlockingRead { + data: b"broker".to_vec(), + entered: Arc::clone(&entered), + release: Arc::clone(&release), + }, + ]); + let fd = Arc::new( + fs.open_file(&Context::new(), "/file", OFlags::RDONLY, Mode::empty()) + .expect("open should succeed"), + ); + + let worker = fs.clone(); + let opened_file = Arc::clone(&fd); + let reader = std::thread::spawn(move || { + let mut buffer = [0; 8]; + let read = worker + .read_file(&opened_file, &mut buffer, None) + .expect("the in-flight read should succeed"); + buffer[..read].to_vec() + }); + + entered.wait(); + fs.close_file(&fd).expect("close should succeed"); + assert!( + !broker + .calls() + .iter() + .any(|call| matches!(call, Call::Close(FILE_HANDLE))), + "the broker object must remain open while the read holds a reference" + ); + + release.wait(); + assert_eq!(reader.join().unwrap(), b"broker"); + assert_eq!(broker.calls().last(), Some(&Call::Close(FILE_HANDLE))); +} + #[test] fn path_status_converts_broker_values() { let context = Context::new(); From 4ec7a9e1f3a82a51556ebdfb16162ac2bd2566ab Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 9 Sep 2026 16:40:32 -0700 Subject: [PATCH 09/34] Share protocol file type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/file.rs | 26 +++----------------------- litebox/src/fs/mod.rs | 6 ++++-- litebox/src/fs/tests.rs | 12 ++++++------ litebox_broker_core/src/fs/mod.rs | 11 +---------- litebox_broker_core/src/fs/service.rs | 18 +++++------------- 5 files changed, 19 insertions(+), 54 deletions(-) diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index df45f4ae3c..bf8348faf2 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -15,7 +15,7 @@ use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::fs::{ FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, - FileSeekWhence, FileStatus as BrokerFileStatus, FileType as BrokerFileType, FileUser, + FileSeekWhence, FileStatus as BrokerFileStatus, FileType, FileUser, }; use crate::path::Arg; @@ -66,17 +66,6 @@ bitflags! { } } -/// Types of files on a file-system. -/// -/// See [`LiteBox::path_file_status`]. -#[derive(Debug, PartialEq, Eq, Clone)] -#[non_exhaustive] -pub enum FileType { - RegularFile, - Directory, - CharacterDevice, -} - bitflags! { /// `O_*` constants for use with open, ... #[repr(transparent)] @@ -786,7 +775,7 @@ fn file_status_error(error: FileError) -> FileStatusError { fn file_status(status: BrokerFileStatus) -> Result { Ok(FileStatus { - file_type: file_type(status.file_type).map_err(|()| FileStatusError::Io)?, + file_type: status.file_type, mode: Mode::from_bits_retain(u32::from(status.mode.bits())), size: usize::try_from(status.size).map_err(|_| FileStatusError::Io)?, owner: UserInfo { @@ -806,22 +795,13 @@ fn directory_entries(entries: Vec) -> Result, for entry in entries { output.push(DirEntry { name: entry.name, - file_type: file_type(entry.file_type).map_err(|()| ReadDirError::Io)?, + file_type: entry.file_type, ino_info: entry.node_info.map(directory_node_info).transpose()?, }); } Ok(output) } -fn file_type(file_type: BrokerFileType) -> Result { - match file_type { - BrokerFileType::RegularFile => Ok(FileType::RegularFile), - BrokerFileType::Directory => Ok(FileType::Directory), - BrokerFileType::CharacterDevice => Ok(FileType::CharacterDevice), - _ => Err(()), - } -} - fn status_node_info(node: FileNodeInfo) -> Result { Ok(NodeInfo { dev: usize::try_from(node.dev).map_err(|_| FileStatusError::Io)?, diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index ce0afab5a3..76ac7d1673 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -10,9 +10,11 @@ pub mod errors; mod file; +pub use litebox_broker_protocol::fs::FileType; + pub use file::{ - Context, DirEntry, File, FileFd, FileStatus, FileType, Mode, NodeInfo, OFlags, ResolvedPath, - SeekWhence, UserInfo, + Context, DirEntry, File, FileFd, FileStatus, Mode, NodeInfo, OFlags, ResolvedPath, SeekWhence, + UserInfo, }; #[doc(hidden)] diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 82162bd0a1..8f28c45439 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -22,9 +22,9 @@ use std::sync::{Barrier, Mutex}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::fs::{ FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, - FileSeekWhence, FileStatus as BrokerFileStatus, FileType as BrokerFileType, FileUser, - MAX_FILE_TRANSFER_SIZE, OpenFileResponse, ReadDirectoryResponse, ReadFileResponse, - SeekFileResponse, WriteFileResponse, encode_directory_entries_chunk, + FileSeekWhence, FileStatus as BrokerFileStatus, FileUser, MAX_FILE_TRANSFER_SIZE, + OpenFileResponse, ReadDirectoryResponse, ReadFileResponse, SeekFileResponse, WriteFileResponse, + encode_directory_entries_chunk, }; use litebox_broker_protocol::message::{ BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, @@ -578,7 +578,7 @@ fn path_status_converts_broker_values() { let context = Context::new(); let (broker, fs) = scripted_fs([Scripted::Reply(FileResponse::PathStatus( BrokerFileStatus { - file_type: BrokerFileType::CharacterDevice, + file_type: FileType::CharacterDevice, mode: FileMode::from_bits(0o644).unwrap(), size: 12, owner: FileUser { user: 1, group: 2 }, @@ -622,7 +622,7 @@ fn read_dir_reassembles_paged_broker_entries() { let entries = vec![ FileDirectoryEntry { name: String::from("one"), - file_type: BrokerFileType::RegularFile, + file_type: FileType::RegularFile, node_info: Some(FileNodeInfo { dev: 1, ino: 2, @@ -631,7 +631,7 @@ fn read_dir_reassembles_paged_broker_entries() { }, FileDirectoryEntry { name: String::from("two"), - file_type: BrokerFileType::Directory, + file_type: FileType::Directory, node_info: None, }, ]; diff --git a/litebox_broker_core/src/fs/mod.rs b/litebox_broker_core/src/fs/mod.rs index 77c78143ef..f002fb6716 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -30,6 +30,7 @@ mod test_support; #[cfg(test)] mod tests; +pub use litebox_broker_protocol::fs::FileType; pub(crate) use service::File; pub use service::{ FileResult, FileService, UnsupportedFileService, chmod, chown, handle_status, mkdir, open, @@ -76,16 +77,6 @@ bitflags! { } } -/// 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)] diff --git a/litebox_broker_core/src/fs/service.rs b/litebox_broker_core/src/fs/service.rs index bf251d7637..0350d26409 100644 --- a/litebox_broker_core/src/fs/service.rs +++ b/litebox_broker_core/src/fs/service.rs @@ -9,8 +9,7 @@ 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, + FileSeekWhence, FileStatus as ProtocolFileStatus, FileUser, MAX_FILE_TRANSFER_SIZE, }; use litebox_broker_protocol::stdio::{MAX_STDIO_TRANSFER_SIZE, StdioOutputStream}; use litebox_platform::sync::{RawSyncPrimitivesProvider, RwLock}; @@ -21,7 +20,7 @@ use super::errors::{ ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; use super::resolver::{Resolver, ResolverEntry}; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, SeekWhence, UserInfo}; +use super::{DirEntry, FileStatus, Mode, NodeInfo, OFlags, SeekWhence, UserInfo}; use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result}; @@ -765,7 +764,7 @@ fn file_status(status: FileStatus) -> Result { .map_err(|_| BrokerError::Internal)?; let mode = FileMode::from_bits(mode_bits).ok_or(BrokerError::Internal)?; Ok(ProtocolFileStatus { - file_type: file_type(status.file_type), + file_type: status.file_type, mode, size: u64::try_from(status.size).map_err(|_| BrokerError::Internal)?, owner: FileUser { @@ -780,19 +779,11 @@ fn file_status(status: FileStatus) -> Result { fn directory_entry(entry: DirEntry) -> Result { Ok(FileDirectoryEntry { name: entry.name, - file_type: file_type(entry.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)?, @@ -927,6 +918,7 @@ fn file_status_error(error: FileStatusError) -> FileError { #[cfg(test)] mod tests { use super::*; + use crate::fs::FileType; #[test] fn file_status_excludes_object_type_mode_bits() { From d581cfeca761a9fd86e196fb553e267996048245 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 09:18:57 -0700 Subject: [PATCH 10/34] Use protocol filesystem value types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- Cargo.lock | 2 + litebox/src/fs/errors.rs | 3 +- litebox/src/fs/file.rs | 117 +++--------------- litebox/src/fs/mod.rs | 7 +- litebox/src/fs/tests.rs | 32 ++--- litebox_broker_core/src/fs/backend.rs | 3 +- litebox_broker_core/src/fs/composer.rs | 4 +- litebox_broker_core/src/fs/devices.rs | 3 +- litebox_broker_core/src/fs/errors.rs | 3 +- litebox_broker_core/src/fs/in_mem.rs | 3 +- litebox_broker_core/src/fs/mod.rs | 67 +--------- litebox_broker_core/src/fs/nine_p/mod.rs | 38 +++--- litebox_broker_core/src/fs/nine_p/tests.rs | 4 +- litebox_broker_core/src/fs/overlay.rs | 3 +- litebox_broker_core/src/fs/resolver.rs | 6 +- litebox_broker_core/src/fs/service.rs | 58 +++------ litebox_broker_core/src/fs/tar_ro.rs | 5 +- litebox_broker_core/src/fs/test_support.rs | 5 +- litebox_broker_core/src/fs/tests.rs | 23 ++-- litebox_broker_core/src/session.rs | 4 +- litebox_broker_host/src/lib.rs | 2 +- litebox_broker_local/src/fs.rs | 2 +- litebox_broker_protocol/Cargo.toml | 1 + litebox_broker_protocol/src/fs.rs | 73 +++++++---- litebox_broker_protocol/src/wire.rs | 4 +- litebox_broker_protocol/src/wire/fs.rs | 12 +- litebox_broker_userland/src/linux.rs | 12 +- litebox_broker_userland/src/windows.rs | 2 +- litebox_common_linux/Cargo.toml | 1 + litebox_common_linux/src/lib.rs | 33 ++--- litebox_runner_linux_userland/tests/run.rs | 6 +- litebox_shim_linux/Cargo.toml | 2 +- litebox_shim_linux/src/lib.rs | 7 +- litebox_shim_linux/src/loader/elf.rs | 3 +- litebox_shim_linux/src/stdio.rs | 3 +- litebox_shim_linux/src/syscalls/file.rs | 49 ++++---- litebox_shim_linux/src/syscalls/mm.rs | 8 +- litebox_shim_linux/src/syscalls/pipe.rs | 5 +- litebox_shim_linux/src/syscalls/process.rs | 2 +- litebox_shim_linux/src/syscalls/tests.rs | 10 +- litebox_shim_linux/src/syscalls/unix.rs | 3 +- litebox_shim_windows/Cargo.toml | 3 +- litebox_shim_windows/src/loader/pe.rs | 3 +- litebox_shim_windows/src/syscalls/file.rs | 3 +- litebox_shim_windows/src/syscalls/nls.rs | 3 +- litebox_shim_windows/src/syscalls/registry.rs | 12 +- litebox_shim_windows/src/tests.rs | 3 +- 47 files changed, 273 insertions(+), 384 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39852d33cd..88f98eec6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1527,6 +1527,7 @@ dependencies = [ name = "litebox_broker_protocol" version = "0.1.0" dependencies = [ + "bitflags", "thiserror", ] @@ -1589,6 +1590,7 @@ dependencies = [ "elf", "int-enum", "litebox", + "litebox_broker_protocol", "syscalls", "thiserror", "zerocopy", diff --git a/litebox/src/fs/errors.rs b/litebox/src/fs/errors.rs index 2f74333589..a8de68d92c 100644 --- a/litebox/src/fs/errors.rs +++ b/litebox/src/fs/errors.rs @@ -3,6 +3,7 @@ //! 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 @@ -224,7 +225,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 index bf8348faf2..2edf97b4f4 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -14,8 +14,8 @@ use core::num::NonZeroUsize; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, - FileSeekWhence, FileStatus as BrokerFileStatus, FileType, FileUser, + FileAccessMode, FileDirectoryEntry, FileError, FileMode as Mode, FileNodeInfo, FileOpenFlags, + FileSeekWhence as SeekWhence, FileStatus as BrokerFileStatus, FileType, FileUser as UserInfo, }; use crate::path::Arg; @@ -26,46 +26,6 @@ use super::errors::{ ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; -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; - } -} - bitflags! { /// `O_*` constants for use with open, ... #[repr(transparent)] @@ -152,17 +112,6 @@ bitflags! { } } -/// The `whence` directive to [`LiteBox::seek_file`]. -#[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 @@ -185,15 +134,6 @@ pub struct FileStatus { 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 { @@ -214,11 +154,6 @@ pub struct DirEntry { pub ino_info: Option, } -impl UserInfo { - /// The root user - pub const ROOT: Self = Self { user: 0, group: 0 }; -} - /// Type marker for file descriptors backed by broker-owned files. pub struct File(core::marker::PhantomData); @@ -248,10 +183,10 @@ impl LiteBox { let handle = broker .open_file( &path, - file_user(context.acting_user()), + context.acting_user(), access, flags, - file_mode(mode), + mode & Mode::SUPPORTED, ) .map_err(|_| OpenError::Io)? .map_err(open_error)?; @@ -324,7 +259,7 @@ impl LiteBox { let offset = i64::try_from(offset).map_err(|_| SeekError::InvalidOffset)?; let offset = file .broker - .seek_file(file.handle, offset, file_seek_whence(whence)) + .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) @@ -358,7 +293,7 @@ impl LiteBox { let path = Self::broker_path(context, path)?; self.broker_control() .ok_or(ChmodError::Io)? - .chmod_file(&path, file_user(context.acting_user()), file_mode(mode)) + .chmod_file(&path, context.acting_user(), mode & Mode::SUPPORTED) .map_err(|_| ChmodError::Io)? .map_err(chmod_error) } @@ -374,7 +309,7 @@ impl LiteBox { let path = Self::broker_path(context, path)?; self.broker_control() .ok_or(ChownError::Io)? - .chown_file(&path, file_user(context.acting_user()), user, group) + .chown_file(&path, context.acting_user(), user, group) .map_err(|_| ChownError::Io)? .map_err(chown_error) } @@ -384,7 +319,7 @@ impl LiteBox { let path = Self::broker_path(context, path)?; self.broker_control() .ok_or(UnlinkError::Io)? - .unlink_file(&path, file_user(context.acting_user())) + .unlink_file(&path, context.acting_user()) .map_err(|_| UnlinkError::Io)? .map_err(unlink_error) } @@ -399,7 +334,7 @@ impl LiteBox { let path = Self::broker_path(context, path)?; self.broker_control() .ok_or(MkdirError::Io)? - .mkdir_file(&path, file_user(context.acting_user()), file_mode(mode)) + .mkdir_file(&path, context.acting_user(), mode & Mode::SUPPORTED) .map_err(|_| MkdirError::Io)? .map_err(mkdir_error) } @@ -409,7 +344,7 @@ impl LiteBox { let path = Self::broker_path(context, path)?; self.broker_control() .ok_or(RmdirError::Io)? - .rmdir_file(&path, file_user(context.acting_user())) + .rmdir_file(&path, context.acting_user()) .map_err(|_| RmdirError::Io)? .map_err(rmdir_error) } @@ -438,7 +373,7 @@ impl LiteBox { let status = self .broker_control() .ok_or(FileStatusError::Io)? - .path_file_status(&path, file_user(context.acting_user())) + .path_file_status(&path, context.acting_user()) .map_err(|_| FileStatusError::Io)? .map_err(file_status_error)?; file_status(status) @@ -605,27 +540,6 @@ fn file_open_options(flags: OFlags) -> Result<(FileAccessMode, FileOpenFlags), O Ok((access, output)) } -fn file_mode(mode: Mode) -> FileMode { - let bits = u16::try_from(mode.bits() & u32::from(FileMode::SUPPORTED.bits())) - .expect("supported file mode bits fit in u16"); - FileMode::from_bits(bits).expect("masked file mode bits are supported") -} - -const fn file_user(user: UserInfo) -> FileUser { - FileUser { - user: user.user, - group: user.group, - } -} - -const fn file_seek_whence(whence: SeekWhence) -> FileSeekWhence { - match whence { - SeekWhence::RelativeToBeginning => FileSeekWhence::Beginning, - SeekWhence::RelativeToCurrentOffset => FileSeekWhence::Current, - SeekWhence::RelativeToEnd => FileSeekWhence::End, - } -} - fn broker_fd_error(error: crate::broker::error::BrokerControlError, closed: T, io: T) -> T { match error { crate::broker::error::BrokerControlError::Broker( @@ -636,6 +550,8 @@ fn broker_fd_error(error: crate::broker::error::BrokerControlError, closed: T } } +// 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), @@ -776,12 +692,9 @@ fn file_status_error(error: FileError) -> FileStatusError { fn file_status(status: BrokerFileStatus) -> Result { Ok(FileStatus { file_type: status.file_type, - mode: Mode::from_bits_retain(u32::from(status.mode.bits())), + mode: status.mode, size: usize::try_from(status.size).map_err(|_| FileStatusError::Io)?, - owner: UserInfo { - user: status.owner.user, - group: status.owner.group, - }, + owner: status.owner, node_info: status_node_info(status.node_info)?, blksize: usize::try_from(status.block_size).map_err(|_| FileStatusError::Io)?, }) diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index 76ac7d1673..65c1390d98 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -10,12 +10,7 @@ pub mod errors; mod file; -pub use litebox_broker_protocol::fs::FileType; - -pub use file::{ - Context, DirEntry, File, FileFd, FileStatus, Mode, NodeInfo, OFlags, ResolvedPath, SeekWhence, - UserInfo, -}; +pub use file::{Context, DirEntry, File, FileFd, FileStatus, NodeInfo, OFlags, ResolvedPath}; #[doc(hidden)] pub mod nine_p { diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 8f28c45439..d498ff36da 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -22,7 +22,7 @@ use std::sync::{Barrier, Mutex}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::fs::{ FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, - FileSeekWhence, FileStatus as BrokerFileStatus, FileUser, MAX_FILE_TRANSFER_SIZE, + FileSeekWhence, FileStatus as BrokerFileStatus, FileType, FileUser, MAX_FILE_TRANSFER_SIZE, OpenFileResponse, ReadDirectoryResponse, ReadFileResponse, SeekFileResponse, WriteFileResponse, encode_directory_entries_chunk, }; @@ -40,7 +40,7 @@ use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory, Sh use crate::fs::errors::{ OpenError, PathError, ReadDirError, ReadError, RmdirError, UnlinkError, WriteError, }; -use crate::fs::{Context, FileType, Mode, OFlags, SeekWhence, UserInfo}; +use crate::fs::{Context, OFlags}; use crate::platform::mock::MockPlatform; /// The handle the scripted broker hands out for every successful open. @@ -430,7 +430,7 @@ fn context_resolves_paths_against_the_cwd() { fn open_sends_the_resolved_path_and_translated_flags() { let mut context = Context::new(); context.set_cwd(context.resolve("/work").unwrap()); - context.set_acting_user(UserInfo { user: 7, group: 9 }); + context.set_acting_user(FileUser { user: 7, group: 9 }); let (broker, fs) = scripted_fs([opened()]); let fd = fs @@ -438,7 +438,7 @@ fn open_sends_the_resolved_path_and_translated_flags() { &context, "sub/../file.txt", OFlags::CREAT | OFlags::WRONLY | OFlags::APPEND, - Mode::RWXU, + FileMode::RWXU, ) .expect("open should succeed"); @@ -466,7 +466,7 @@ fn read_and_write_transfer_payloads_through_the_broker() { ]); let fd = fs - .open_file(&context, "/file", OFlags::RDWR, Mode::empty()) + .open_file(&context, "/file", OFlags::RDWR, FileMode::empty()) .expect("open should succeed"); assert_eq!(fs.write_file(&fd, b"hello", None).unwrap(), 5); @@ -475,7 +475,11 @@ fn read_and_write_transfer_payloads_through_the_broker() { let read = fs.read_file(&fd, &mut buffer, Some(2)).unwrap(); assert_eq!(&buffer[..read], b"broker"); - assert_eq!(fs.seek_file(&fd, -3, SeekWhence::RelativeToEnd).unwrap(), 3); + assert_eq!( + fs.seek_file(&fd, -3, FileSeekWhence::RelativeToEnd) + .unwrap(), + 3 + ); fs.close_file(&fd).expect("close should succeed"); let calls = broker.calls(); @@ -500,7 +504,7 @@ fn read_and_write_transfer_payloads_through_the_broker() { Call::Seek { handle: FILE_HANDLE, offset: -3, - whence: FileSeekWhence::End, + whence: FileSeekWhence::RelativeToEnd, } ); assert_eq!(calls[4], Call::Close(FILE_HANDLE)); @@ -512,7 +516,7 @@ fn closing_releases_the_broker_object_and_the_descriptor() { let (broker, fs) = scripted_fs([opened()]); let fd = fs - .open_file(&context, "/file", OFlags::RDONLY, Mode::empty()) + .open_file(&context, "/file", OFlags::RDONLY, FileMode::empty()) .expect("open should succeed"); fs.close_file(&fd).expect("close should succeed"); assert_eq!(broker.calls()[1], Call::Close(FILE_HANDLE)); @@ -544,7 +548,7 @@ fn in_flight_read_keeps_the_broker_file_alive_after_close() { }, ]); let fd = Arc::new( - fs.open_file(&Context::new(), "/file", OFlags::RDONLY, Mode::empty()) + fs.open_file(&Context::new(), "/file", OFlags::RDONLY, FileMode::empty()) .expect("open should succeed"), ); @@ -596,7 +600,7 @@ fn path_status_converts_broker_values() { .expect("status should succeed"); assert_eq!(status.file_type, FileType::CharacterDevice); - assert_eq!(status.mode, Mode::from_bits(0o644).unwrap()); + assert_eq!(status.mode, FileMode::from_bits(0o644).unwrap()); assert_eq!(status.size, 12); assert_eq!(status.owner.user, 1); assert_eq!(status.owner.group, 2); @@ -654,7 +658,7 @@ fn read_dir_reassembles_paged_broker_entries() { &context, "/dir", OFlags::RDONLY | OFlags::DIRECTORY, - Mode::empty(), + FileMode::empty(), ) .expect("open should succeed"); let entries = fs @@ -701,11 +705,11 @@ fn broker_file_errors_map_to_guest_errors() { ]); assert!(matches!( - fs.open_file(&context, "/missing", OFlags::RDONLY, Mode::empty()), + fs.open_file(&context, "/missing", OFlags::RDONLY, FileMode::empty()), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) )); assert!(matches!( - fs.open_file(&context, "/secret", OFlags::RDONLY, Mode::empty()), + fs.open_file(&context, "/secret", OFlags::RDONLY, FileMode::empty()), Err(OpenError::AccessNotAllowed) )); assert!(matches!( @@ -718,7 +722,7 @@ fn broker_file_errors_map_to_guest_errors() { )); let fd = fs - .open_file(&context, "/file", OFlags::WRONLY, Mode::empty()) + .open_file(&context, "/file", OFlags::WRONLY, FileMode::empty()) .expect("open should succeed"); let mut buffer = [0; 4]; assert!(matches!( diff --git a/litebox_broker_core/src/fs/backend.rs b/litebox_broker_core/src/fs/backend.rs index be7dcc3202..e61d265b8a 100644 --- a/litebox_broker_core/src/fs/backend.rs +++ b/litebox_broker_core/src/fs/backend.rs @@ -8,13 +8,14 @@ use alloc::vec::Vec; use core::any::{Any, TypeId}; use core::marker::PhantomData; +use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use litebox_broker_protocol::stdio::StdioOutputStream; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; -use super::{DirEntry, FileStatus, Mode, OFlags, UserInfo}; +use super::{DirEntry, FileStatus, OFlags}; // This duplicates the cloneable type-erasure support from `litebox::utilities::anymap` because // broker core cannot depend on LiteBox. Keep it local unless broader reuse justifies a common home. diff --git a/litebox_broker_core/src/fs/composer.rs b/litebox_broker_core/src/fs/composer.rs index 2baa830ad8..eb73a42825 100644 --- a/litebox_broker_core/src/fs/composer.rs +++ b/litebox_broker_core/src/fs/composer.rs @@ -9,6 +9,8 @@ use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; +use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; + use super::backend::{ Backend, BackendHandles, CreationMetadata, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, @@ -18,7 +20,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::{InodeAllocator, InodeAllocators}; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; +use super::{DirEntry, FileStatus, NodeInfo, OFlags}; use thiserror::Error; // XXX(jayb): consider removing this via a runtime reserved device ID? diff --git a/litebox_broker_core/src/fs/devices.rs b/litebox_broker_core/src/fs/devices.rs index 220ed37247..ef9d889c90 100644 --- a/litebox_broker_core/src/fs/devices.rs +++ b/litebox_broker_core/src/fs/devices.rs @@ -8,6 +8,7 @@ use alloc::string::String; use alloc::vec; use alloc::vec::Vec; +use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; use litebox_broker_protocol::random::MAX_RANDOM_TRANSFER_SIZE; use litebox_broker_protocol::stdio::StdioOutputStream; @@ -20,7 +21,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; +use super::{DirEntry, FileStatus, NodeInfo, OFlags}; /// Block size for stdio devices const STDIO_BLOCK_SIZE: usize = 1024; diff --git a/litebox_broker_core/src/fs/errors.rs b/litebox_broker_core/src/fs/errors.rs index 9b8d4041ca..3f95740235 100644 --- a/litebox_broker_core/src/fs/errors.rs +++ b/litebox_broker_core/src/fs/errors.rs @@ -3,6 +3,7 @@ //! Possible errors from the broker-core filesystem engine. +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 @@ -207,7 +208,7 @@ pub enum PathError { #[cfg(debug_assertions)] dir: alloc::string::String, #[cfg(debug_assertions)] - perms: super::Mode, + perms: FileMode, }, #[error("invalid characters, not permitted by underlying file system")] InvalidPathname, diff --git a/litebox_broker_core/src/fs/in_mem.rs b/litebox_broker_core/src/fs/in_mem.rs index 8287009836..cbe2d2951e 100644 --- a/litebox_broker_core/src/fs/in_mem.rs +++ b/litebox_broker_core/src/fs/in_mem.rs @@ -8,6 +8,7 @@ use alloc::sync::Arc; use alloc::vec::Vec; use hashbrown::HashMap; +use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; use litebox_platform::sync; use super::errors::{ @@ -15,7 +16,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, UserInfo}; +use super::{DirEntry, FileStatus, NodeInfo}; /// A [`super::backend::Backend`] that stores all files in memory. /// diff --git a/litebox_broker_core/src/fs/mod.rs b/litebox_broker_core/src/fs/mod.rs index f002fb6716..79f42d1c85 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -12,6 +12,7 @@ use bitflags::bitflags; use core::ffi::c_uint; use core::num::NonZeroUsize; +use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; pub mod backend; pub mod composer; @@ -30,53 +31,12 @@ mod test_support; #[cfg(test)] mod tests; -pub use litebox_broker_protocol::fs::FileType; 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; - } -} - bitflags! { /// `O_*` constants for use with open, ... #[repr(transparent)] @@ -163,17 +123,6 @@ 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 @@ -196,15 +145,6 @@ pub struct FileStatus { 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 { @@ -225,10 +165,5 @@ pub struct DirEntry { 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; diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index 6573df56f8..4be90ff863 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -14,6 +14,8 @@ use alloc::vec::Vec; use core::num::NonZeroUsize; use core::sync::atomic::{AtomicBool, Ordering}; +use litebox_broker_protocol::fs::{FileMode, FileType, FileUser}; +use litebox_platform::sync; use thiserror::Error; use self::fcall::Rlerror; @@ -26,8 +28,6 @@ use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; -use litebox_platform::sync; - mod client; mod fcall; mod id_pool; @@ -527,7 +527,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 { @@ -546,7 +546,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. @@ -572,13 +572,13 @@ where Ok(self.remove_at(&dir.into_typed::(), name, false)?) } - fn chmod(&self, h: HandleRef<'_>, mode: super::Mode) -> Result<(), ChmodError> { + fn chmod(&self, h: HandleRef<'_>, mode: FileMode) -> Result<(), ChmodError> { let fid = match h { HandleRef::File(h) => &h.get_typed::().fid, HandleRef::Dir(h) => &h.get_typed::().fid, }; let stat = fcall::SetAttr { - mode: mode.bits(), + mode: mode.bits().into(), ..Default::default() }; Ok(self @@ -703,14 +703,20 @@ fn oflags_to_lopen(flags: OFlags) -> fcall::LOpenFlags { } /// Convert a Qid type to our FileType -fn qid_type_to_file_type(qid_type: fcall::QidType) -> super::FileType { +fn qid_type_to_file_type(qid_type: fcall::QidType) -> FileType { if qid_type.contains(fcall::QidType::DIR) { - super::FileType::Directory + FileType::Directory } else { - super::FileType::RegularFile + FileType::RegularFile } } +fn file_mode(mode: u32) -> FileMode { + let bits = u16::try_from(mode & u32::from(FileMode::SUPPORTED.bits())) + .expect("supported file mode bits fit in u16"); + FileMode::from_bits_retain(bits) +} + /// Convert getattr response to FileStatus /// /// Inode numbers come from the server's qids; `device_id` is the device the caller reports this @@ -724,9 +730,9 @@ fn rgetattr_to_file_status( if attr.valid.contains(fcall::GetattrMask::BASIC) { Ok(super::FileStatus { file_type, - mode: super::Mode::from_bits_truncate(attr.stat.mode), + mode: file_mode(attr.stat.mode), size: usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)?, - owner: super::UserInfo { + owner: FileUser { user: u16::try_from(attr.stat.uid).map_err(|_| Error::InvalidResponse)?, group: u16::try_from(attr.stat.gid).map_err(|_| Error::InvalidResponse)?, }, @@ -743,16 +749,16 @@ fn rgetattr_to_file_status( Ok(super::FileStatus { file_type, mode: if attr.valid.contains(fcall::GetattrMask::MODE) { - super::Mode::from_bits_truncate(attr.stat.mode) + file_mode(attr.stat.mode) } else { - super::Mode::empty() + FileMode::empty() }, size: if attr.valid.contains(fcall::GetattrMask::SIZE) { usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)? } else { 0 }, - owner: super::UserInfo { + owner: FileUser { user: if attr.valid.contains(fcall::GetattrMask::UID) { u16::try_from(attr.stat.uid).map_err(|_| Error::InvalidResponse)? } else { @@ -933,7 +939,7 @@ impl From for FileStatusError { #[cfg(debug_assertions)] dir: String::new(), #[cfg(debug_assertions)] - perms: super::Mode::empty(), + perms: FileMode::empty(), }), _ => FileStatusError::Io, }, @@ -1012,7 +1018,7 @@ impl From for WalkError { #[cfg(debug_assertions)] dir: String::new(), #[cfg(debug_assertions)] - perms: super::Mode::empty(), + perms: FileMode::empty(), }), _ => WalkError::Io, }, diff --git a/litebox_broker_core/src/fs/nine_p/tests.rs b/litebox_broker_core/src/fs/nine_p/tests.rs index 12b6ea3cc2..a5fb9f318f 100644 --- a/litebox_broker_core/src/fs/nine_p/tests.rs +++ b/litebox_broker_core/src/fs/nine_p/tests.rs @@ -12,13 +12,15 @@ use std::io::{Read as _, Write as _}; use std::net::{TcpListener, TcpStream}; use std::path::Path; +use litebox_broker_protocol::fs::{FileMode as Mode, FileSeekWhence as SeekWhence, FileType}; + +use crate::fs::OFlags; use crate::fs::errors::{ FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; use crate::fs::inode_allocator::InodeAllocator; use crate::fs::test_support::{Fs, USER}; -use crate::fs::{FileType, Mode, OFlags, SeekWhence}; use crate::test_platform::TestPlatform; use super::{NineP, transport}; diff --git a/litebox_broker_core/src/fs/overlay.rs b/litebox_broker_core/src/fs/overlay.rs index 96ff68f935..ea7f800f33 100644 --- a/litebox_broker_core/src/fs/overlay.rs +++ b/litebox_broker_core/src/fs/overlay.rs @@ -20,6 +20,7 @@ use alloc::vec::Vec; use hashbrown::{HashMap, HashSet}; +use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; use litebox_platform::sync::{Mutex, MutexGuard, RawSyncPrimitivesProvider}; use super::backend::{ @@ -32,7 +33,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; +use super::{DirEntry, FileStatus, NodeInfo, OFlags}; /// The reserved namespace prefix; no overlay-visible name may start with it. const MARKER_PREFIX: &str = ".litebox-overlay-"; diff --git a/litebox_broker_core/src/fs/resolver.rs b/litebox_broker_core/src/fs/resolver.rs index 472746c13c..77a17154cb 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -7,19 +7,21 @@ use alloc::string::String; use alloc::vec; use alloc::vec::Vec; use core::marker::PhantomData; +use litebox_broker_protocol::fs::{ + FileMode as Mode, FileSeekWhence as SeekWhence, FileType, FileUser as UserInfo, +}; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::{ - FileType, Mode, OFlags, + OFlags, backend::{ CreationMetadata, DeviceIo, DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, }, }; -use super::{SeekWhence, UserInfo}; /// The broker-core filesystem resolver, generic over its synchronization platform and /// [`Backend`](super::backend::Backend). diff --git a/litebox_broker_core/src/fs/service.rs b/litebox_broker_core/src/fs/service.rs index 0350d26409..fb389a2780 100644 --- a/litebox_broker_core/src/fs/service.rs +++ b/litebox_broker_core/src/fs/service.rs @@ -20,7 +20,7 @@ use super::errors::{ ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; use super::resolver::{Resolver, ResolverEntry}; -use super::{DirEntry, FileStatus, Mode, NodeInfo, OFlags, SeekWhence, UserInfo}; +use super::{DirEntry, FileStatus, NodeInfo, OFlags}; use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result}; @@ -207,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))), }; @@ -294,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() { @@ -391,7 +390,7 @@ where path: &str, user: FileUser, ) -> ServiceResult { - let status = match Resolver::file_status(self, user_info(user), path) { + let status = match Resolver::file_status(self, user, path) { Ok(status) => status, Err(error) => return Ok(Err(file_status_error(error))), }; @@ -405,7 +404,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( @@ -416,14 +415,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( @@ -433,11 +429,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)) } } @@ -713,13 +709,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, @@ -746,31 +735,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: status.file_type, - mode, + mode: status.mode & FileMode::SUPPORTED, size: u64::try_from(status.size).map_err(|_| BrokerError::Internal)?, - owner: FileUser { - user: status.owner.user, - group: status.owner.group, - }, + owner: status.owner, node_info: node_info(status.node_info)?, block_size: u64::try_from(status.blksize).map_err(|_| BrokerError::Internal)?, }) @@ -795,6 +765,8 @@ fn node_info(node_info: NodeInfo) -> Result { }) } +// 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, @@ -918,15 +890,15 @@ fn file_status_error(error: FileStatusError) -> FileError { #[cfg(test)] mod tests { use super::*; - use crate::fs::FileType; + use litebox_broker_protocol::fs::FileType; #[test] fn file_status_excludes_object_type_mode_bits() { let status = file_status(FileStatus { file_type: FileType::RegularFile, - mode: Mode::from_bits_retain(0o100644), + mode: FileMode::from_bits_retain(0o100644), size: 1, - owner: UserInfo::ROOT, + owner: FileUser::ROOT, node_info: NodeInfo { dev: 2, ino: 3, diff --git a/litebox_broker_core/src/fs/tar_ro.rs b/litebox_broker_core/src/fs/tar_ro.rs index 4450e63b6a..a4c91cb055 100644 --- a/litebox_broker_core/src/fs/tar_ro.rs +++ b/litebox_broker_core/src/fs/tar_ro.rs @@ -28,11 +28,12 @@ use alloc::string::String; use alloc::vec::Vec; use core::ops::Range; use hashbrown::HashMap; +use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; -use super::{DirEntry, FileType}; +use super::DirEntry; use super::{ - Mode, NodeInfo, OFlags, UserInfo, + NodeInfo, OFlags, backend::{CreationMetadata, DirHandle, FileHandle, HandleRef, WalkingDirHandle}, errors::{ ChmodError, ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, diff --git a/litebox_broker_core/src/fs/test_support.rs b/litebox_broker_core/src/fs/test_support.rs index 8641d92291..57e17d90c0 100644 --- a/litebox_broker_core/src/fs/test_support.rs +++ b/litebox_broker_core/src/fs/test_support.rs @@ -9,6 +9,9 @@ use alloc::vec::Vec; +use litebox_broker_protocol::fs::{ + FileMode as Mode, FileSeekWhence as SeekWhence, FileUser as UserInfo, +}; use litebox_broker_protocol::stdio::StdioOutputStream; use super::backend::{Backend, DeviceIo, NoDeviceIo}; @@ -17,7 +20,7 @@ use super::errors::{ RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; use super::resolver::{Resolver, ResolverEntry}; -use super::{DirEntry, FileStatus, Mode, OFlags, SeekWhence, UserInfo}; +use super::{DirEntry, FileStatus, OFlags}; use crate::test_platform::TestPlatform; /// The unprivileged user these tests act as unless they need root. diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs index 20b5ca80b9..28d42de792 100644 --- a/litebox_broker_core/src/fs/tests.rs +++ b/litebox_broker_core/src/fs/tests.rs @@ -8,13 +8,16 @@ //! descriptor table involved: the semantics under test are owned by broker core. use alloc::borrow::Cow; +use litebox_broker_protocol::fs::{ + FileMode as Mode, FileSeekWhence as SeekWhence, FileType, FileUser as UserInfo, +}; +use super::OFlags; use super::in_mem::InMem; use super::inode_allocator::InodeAllocator; use super::overlay::Overlay; use super::tar_ro::TarRo; use super::test_support::{Fs, ROOT, RecordingStdio, USER, UnservicedStdio}; -use super::{Mode, OFlags, UserInfo}; use crate::test_platform::TestPlatform; const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); @@ -40,13 +43,15 @@ fn overlay_fs( } mod in_mem { - use super::{Fs, InMem, Mode, OFlags, ROOT, TestPlatform, USER, in_mem_fs}; + use super::{ + FileType, Fs, InMem, Mode, OFlags, ROOT, SeekWhence, TestPlatform, USER, UserInfo, + in_mem_fs, + }; use crate::fs::errors::{ ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, UnlinkError, }; use crate::fs::test_support::Entry; - use crate::fs::{FileType, SeekWhence}; use alloc::vec; use alloc::vec::Vec; @@ -443,7 +448,7 @@ mod in_mem { .expect("Failed to chown as owner"); // A different user may not chown (should fail) - let other = super::UserInfo { + let other = UserInfo { user: 500, group: 500, }; @@ -944,8 +949,7 @@ mod in_mem { } mod tar_ro { - use super::{Mode, OFlags, TEST_TAR_FILE, USER, tar_ro_fs}; - use crate::fs::FileType; + use super::{FileType, Mode, OFlags, TEST_TAR_FILE, USER, tar_ro_fs}; use crate::fs::errors::{OpenError, PathError, ReadDirError}; use alloc::vec; use alloc::vec::Vec; @@ -1102,9 +1106,10 @@ mod tar_ro { } mod overlay { - use super::{Fs, Mode, OFlags, Overlay, TEST_TAR_FILE, TestPlatform, USER, UserInfo}; - use crate::fs::FileType; - use crate::fs::SeekWhence; + use super::{ + FileType, Fs, Mode, OFlags, Overlay, 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; diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index b397a8ec4e..a3e8be6236 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -746,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)) ); @@ -815,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!( diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 19adbcfea4..7c8e0b59ad 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 d82108c37c..7bcb012870 100644 --- a/litebox_broker_local/src/fs.rs +++ b/litebox_broker_local/src/fs.rs @@ -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_protocol/Cargo.toml b/litebox_broker_protocol/Cargo.toml index 9cfecae7ec..e1c47bbcc0 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 d728ec95fa..e9252c6282 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -6,6 +6,7 @@ use alloc::string::String; use alloc::vec::Vec; +use bitflags::bitflags; use thiserror::Error; use crate::ObjectHandle; @@ -28,6 +29,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] @@ -129,14 +135,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 +156,43 @@ 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 - } - } - - /// Returns the stable protocol bits. - #[must_use] - pub const fn bits(self) -> u16 { - self.0 +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; } } diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 682230de7e..a4d4d1a0fb 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -635,7 +635,7 @@ mod tests { BrokerOperation::File(FileRequest::Seek(SeekFileRequest { handle, offset: -19, - whence: FileSeekWhence::Current, + whence: FileSeekWhence::RelativeToCurrentOffset, })), BrokerOperation::File(FileRequest::Truncate(TruncateFileRequest { handle, @@ -1308,7 +1308,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; diff --git a/litebox_broker_protocol/src/wire/fs.rs b/litebox_broker_protocol/src/wire/fs.rs index aa33dd8f31..b9ce584029 100644 --- a/litebox_broker_protocol/src/wire/fs.rs +++ b/litebox_broker_protocol/src/wire/fs.rs @@ -418,17 +418,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), } } diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index 1d1d04ff45..d3b0c9322e 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -21,9 +21,9 @@ 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::fs::{Mode, UserInfo}; use litebox_broker_core::socket::HOST_GATEWAY_IPV4_ADDRESS; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; +use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; 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::{ @@ -158,7 +158,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo entries.push(( path_to_string(path)?, InitialNode::Directory { - mode: Mode::from_bits_retain(metadata.st_mode()), + mode: file_mode(metadata.st_mode()), owner, }, )); @@ -179,7 +179,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo entries.push(( path_to_string(&program)?, InitialNode::File { - mode: Mode::from_bits_retain(metadata.st_mode()), + mode: file_mode(metadata.st_mode()), owner: guest_owner(previous_user, metadata.st_uid()), data: program_data.into(), }, @@ -244,6 +244,12 @@ fn guest_owner(previous_user: u32, user: u32) -> UserInfo { } } +fn file_mode(mode: u32) -> Mode { + let bits = u16::try_from(mode & u32::from(Mode::SUPPORTED.bits())) + .expect("supported file mode bits fit in u16"); + Mode::from_bits_retain(bits) +} + fn inferred_linux_runner_args( args: &super::CliArgs, ) -> Option { diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index e936b3baba..73465e5625 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -19,8 +19,8 @@ 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::TarRo; -use litebox_broker_core::fs::{Mode, UserInfo}; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; +use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport_windows_userland::named_pipe::{ WindowsNamedPipeHostSetupChannel, WindowsNamedPipeListener, WindowsNamedPipeStream, diff --git a/litebox_common_linux/Cargo.toml b/litebox_common_linux/Cargo.toml index 1606bad73b..bc1208b280 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/lib.rs b/litebox_common_linux/src/lib.rs index c29e43cf85..8c626bf227 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -13,6 +13,7 @@ use litebox::{ fs::OFlags, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; +use litebox_broker_protocol::fs::{FileMode, FileType, FileUser}; use syscalls::Sysno; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -250,12 +251,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 +282,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!(), } } @@ -375,7 +376,7 @@ impl From for FileStat { file_type, mode, size, - owner: litebox::fs::UserInfo { user, group }, + owner: FileUser { user, group }, node_info: litebox::fs::NodeInfo { dev, ino, rdev }, blksize, .. @@ -384,7 +385,7 @@ impl From for FileStat { st_dev: <_>::try_from(dev).unwrap(), st_ino: <_>::try_from(ino).unwrap(), 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 @@ -515,7 +516,7 @@ impl From for Statx { file_type, mode, size, - owner: litebox::fs::UserInfo { user, group }, + owner: FileUser { user, group }, node_info: litebox::fs::NodeInfo { dev, ino, rdev }, blksize, .. @@ -528,7 +529,7 @@ impl From for Statx { 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_mode: (u32::from(mode.bits()) | InodeType::from(file_type) as u32).trunc(), stx_ino: ino as u64, stx_size: size as u64, stx_blocks: 0, @@ -2277,7 +2278,7 @@ pub enum SyscallRequest { dirfd: i32, pathname: UserPtr, flags: litebox::fs::OFlags, - mode: litebox::fs::Mode, + mode: FileMode, }, Ftruncate { fd: i32, @@ -3415,7 +3416,7 @@ reinterpret_truncated_from_usize_for! { MapFlags, MRemapFlags, AccessFlags, - litebox::fs::Mode, + FileMode, litebox::fs::OFlags, AtFlags, SockFlags, diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 6d93ce5b19..29f343f933 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -327,12 +327,12 @@ fn test_file_service( use std::os::unix::fs::PermissionsExt; use litebox_broker_core::fs::{ - Mode, UserInfo, 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![ @@ -371,7 +371,9 @@ fn test_file_service( ); let metadata = std::fs::metadata(entry.path()).expect("failed to inspect runner test file"); - let mode = Mode::from_bits_retain(metadata.permissions().mode() & 0o7777); + let bits = u16::try_from(metadata.permissions().mode() & 0o7777) + .expect("supported file mode bits fit in u16"); + let mode = Mode::from_bits_retain(bits); let node = if metadata.is_dir() { InitialNode::Directory { mode, diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index d1197fee05..5b9153b652 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" } @@ -25,7 +26,6 @@ aarch64_virtualize_x18 = [] [dev-dependencies] 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" } libc = "0.2.177" diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index b6340e1e48..1bc92a97da 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -28,6 +28,7 @@ use litebox::{ sync::futex::FutexManager, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _}, }; +use litebox_broker_protocol::fs::{FileMode as Mode, FileSeekWhence as SeekWhence}; use litebox_common_linux::{ SyscallRequest, errno::Errno, @@ -366,7 +367,7 @@ impl syscalls::file::FilesState { global: &GlobalState, context: &litebox::fs::Context, ) { - use litebox::fs::{Mode, OFlags}; + use litebox::fs::OFlags; let stdin = self .fs .open_file(context, "/dev/stdin", OFlags::RDONLY, Mode::empty()) @@ -576,7 +577,7 @@ impl Task { self.do_seek( fd, 0, - litebox::fs::SeekWhence::RelativeToCurrentOffset, + SeekWhence::RelativeToCurrentOffset, ) .inspect_err(|e| { match *e { @@ -604,7 +605,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"); diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 02f30f5deb..11338e52f7 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -5,10 +5,11 @@ use alloc::{ffi::CString, vec::Vec}; use litebox::{ - fs::{Mode, OFlags}, + fs::OFlags, mm::linux::{CreatePagesFlags, MappingError, PAGE_SIZE}, utils::{ReinterpretSignedExt, TruncateExt}, }; +use litebox_broker_protocol::fs::FileMode as Mode; use litebox_common_linux::{MapFlags, errno::Errno, loader::ElfParsedFile}; use thiserror::Error; diff --git a/litebox_shim_linux/src/stdio.rs b/litebox_shim_linux/src/stdio.rs index 4d46bf2d88..3e118fc85c 100644 --- a/litebox_shim_linux/src/stdio.rs +++ b/litebox_shim_linux/src/stdio.rs @@ -7,7 +7,8 @@ mod tests { use core::ffi::CStr; - use litebox::fs::{Mode, OFlags}; + use litebox::fs::OFlags; + use litebox_broker_protocol::fs::FileMode as Mode; use litebox_common_linux::{FcntlArg, FileDescriptorFlags, IoctlArg, Termios, errno::Errno}; use crate::{UserPtrMut, syscalls::tests::init_platform}; diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 15e1d68aa7..84d7f4e66b 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -11,12 +11,15 @@ use alloc::{ use litebox::{ event::{Events, wait::WaitError}, fd::{FdEnabledSubsystem, MetadataError, TypedFd}, - fs::{Mode, OFlags, SeekWhence}, + fs::OFlags, mm::linux::PAGE_SIZE, path, stdio::StdioStream, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; +use litebox_broker_protocol::fs::{ + FileMode as Mode, FileSeekWhence as SeekWhence, FileType, FileUser, +}; use litebox_common_linux::{ AccessFlags, AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, InodeType, IoReadVec, IoWriteVec, IoctlArg, Statx, StatxMask, TimeParam, errno::Errno, @@ -33,8 +36,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), @@ -62,7 +65,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) @@ -73,16 +76,22 @@ impl FsState { 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)) + file_mode_from_linux(self.umask.load(Ordering::Relaxed)) } } +fn file_mode_from_linux(mode: u32) -> Mode { + let bits = u16::try_from(mode & u32::from(Mode::SUPPORTED.bits())) + .expect("supported file mode bits fit in u16"); + Mode::from_bits_retain(bits) +} + /// Task state shared by `CLONE_FILES`. pub(crate) struct FilesState { /// The filesystem implementation, shared across tasks that share file system. @@ -430,13 +439,13 @@ 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 = file_mode_from_linux(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); + file_mode_from_linux(old_mask) } /// Handle syscall `open` @@ -496,7 +505,7 @@ impl Task { }; match file_type { InodeType::File => { - let mode = Mode::from_bits_truncate(mode_and_type & !FILE_TYPE_MASK); + let mode = file_mode_from_linux(mode_and_type & !FILE_TYPE_MASK); let file = self.do_openat( dirfd, pathname, @@ -892,7 +901,7 @@ 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, file_mode_from_linux(mode)) } pub(crate) fn do_close(&self, raw_fd: usize) -> Result<(), Errno> { @@ -1446,7 +1455,7 @@ impl Task { group: stat.st_gid, }; Self::do_access_mode( - Mode::from_bits_truncate(stat.st_mode & 0o7777), + file_mode_from_linux(stat.st_mode & 0o7777), owner, caller, &mode, @@ -1555,8 +1564,8 @@ 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(); + | u32::from((Mode::RWXU | Mode::RWXG | Mode::RWXO).bits()); + let rw_user_mode = u32::from((Mode::RUSR | Mode::WUSR).bits()); let files = self.files.borrow(); fd.dispatch( |fd| files.fs.file_status(fd).map(T::from).map_err(Errno::from), @@ -1910,7 +1919,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(); @@ -2054,8 +2062,7 @@ impl Task { 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!(), @@ -2753,7 +2760,7 @@ impl Task { ino: entry.ino_info.as_ref().map_or(0, |node_info| node_info.ino) as u64, off: dir_off as u64, len: len.trunc(), - typ: litebox_common_linux::DirentType::from(entry.file_type.clone()) as u8, + typ: litebox_common_linux::DirentType::from(entry.file_type) as u8, __name: [0; 0], }; let hdr_ptr = UserPtrMut::from_usize(dirp.as_usize() + nbytes); @@ -2790,7 +2797,7 @@ mod tests { use super::*; use alloc::string::String; use core::cell::Cell; - use litebox::fs::{Mode, OFlags}; + use litebox::fs::OFlags; extern crate std; @@ -3098,7 +3105,7 @@ mod tests { task.sys_mknodat( litebox_common_linux::AT_FDCWD, "/mknodat_at_fd_limit", - InodeType::File as u32 | (Mode::RUSR | Mode::WUSR).bits(), + InodeType::File as u32 | u32::from((Mode::RUSR | Mode::WUSR).bits()), 0, ), Ok(()) @@ -3152,7 +3159,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(), diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index b20f0511f8..34e2c02037 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -1555,12 +1555,8 @@ impl Task { #[cfg(test)] mod tests { - use litebox::{ - fs::{Mode, OFlags}, - mm::linux::PAGE_SIZE, - platform::PageManagementProvider, - }; - use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; + use litebox::{fs::OFlags, mm::linux::PAGE_SIZE, platform::PageManagementProvider}; + use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags}; use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, errno::Errno}; use object::{elf::FileHeader64, endian::LittleEndian}; diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs index 51b1abbfa7..cc339b0946 100644 --- a/litebox_shim_linux/src/syscalls/pipe.rs +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -12,9 +12,10 @@ use core::num::NonZero; use litebox::{ event::{IOPollable, wait::WaitContext}, fd::MetadataError, - fs::{Mode, OFlags}, + fs::OFlags, pipes::{Flags, HalfPipeType, PipeFd}, }; +use litebox_broker_protocol::fs::FileMode as Mode; use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; use crate::{GlobalState, ShimPlatform}; @@ -145,7 +146,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 64ab21fdc2..ddf3c2733b 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1403,7 +1403,7 @@ impl Task { let file = self.do_open( full_path, litebox::fs::OFlags::RDONLY, - litebox::fs::Mode::empty(), + litebox_broker_protocol::fs::FileMode::empty(), )?; let mut header = [0u8; SHEBANG_MAX_LINE]; let files = self.files.borrow(); diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index b0e968f556..d8787a5c5d 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -12,12 +12,12 @@ use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; -use litebox::fs::{Mode, OFlags}; +use litebox::fs::OFlags; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::fs::WriteFileResponse; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, FileType, - FileUser, MAX_FILE_TRANSFER_SIZE, encode_directory_entries_chunk, + FileAccessMode, FileDirectoryEntry, FileError, FileMode as Mode, FileNodeInfo, FileOpenFlags, + FileType, FileUser, MAX_FILE_TRANSFER_SIZE, encode_directory_entries_chunk, }; use litebox_broker_protocol::message::FileResponse; use litebox_common_linux::{AtFlags, DirentType, FcntlArg, FileDescriptorFlags, errno::Errno}; @@ -100,8 +100,8 @@ pub(crate) fn directory_entry(name: &str, file_type: FileType, ino: u64) -> File } /// Builds the protocol mode a request is expected to carry. -pub(crate) fn mode(bits: u16) -> FileMode { - FileMode::from_bits(bits).expect("test modes must be supported") +pub(crate) fn mode(bits: u16) -> Mode { + Mode::from_bits(bits).expect("test modes must be supported") } #[cfg(target_arch = "x86_64")] diff --git a/litebox_shim_linux/src/syscalls/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index 459a0a25bd..2187908fce 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -21,10 +21,11 @@ use litebox::{ wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, - fs::{Mode, OFlags, errors::OpenError}, + fs::{OFlags, errors::OpenError}, sync::{Mutex, RwLock}, utils::TruncateExt as _, }; +use litebox_broker_protocol::fs::FileMode as Mode; use litebox_common_linux::{ IpOption, ReceiveFlags, SendFlags, ShutdownHow, SockFlags, SockType, SocketOption, SocketOptionName, errno::Errno, diff --git a/litebox_shim_windows/Cargo.toml b/litebox_shim_windows/Cargo.toml index 3b4fe97217..f6a9e36958 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" } @@ -23,7 +24,6 @@ zerocopy = { version = "0.8", default-features = false, features = ["derive"] } litebox_broker_core = { path = "../litebox_broker_core/", version = "0.1.0" } litebox_broker_host = { path = "../litebox_broker_host/", version = "0.1.0" } litebox_broker_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" } litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland/", version = "0.1.0" } @@ -31,7 +31,6 @@ litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland/" 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_protocol = { path = "../litebox_broker_protocol/", 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" } diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index f7ddfa2a85..eb8e323f8b 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -10,12 +10,13 @@ use core::{ use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt as _; use litebox::{ - fs::{Mode, OFlags}, + fs::OFlags, mm::linux::{ CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, VmemProtectError, }, platform::RawPointerProvider, }; +use litebox_broker_protocol::fs::FileMode as Mode; use litebox_common_windows::loader::{ AccessMemory, Fault, KiUserInvertedFunctionTableEntry, KiUserInvertedFunctionTableHeader, MAXIMUM_INVERTED_FUNCTION_TABLE_SIZE, MapMemory, MappingInfo, PAGE_SIZE, PeExportError, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 58d6ae2840..40e25e3ccc 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -13,9 +13,10 @@ use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, SeekError, WriteError, }; -use litebox::fs::{FileStatus, FileType, Mode, OFlags, SeekWhence}; +use litebox::fs::{FileStatus, OFlags}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; +use litebox_broker_protocol::fs::{FileMode as Mode, FileSeekWhence as SeekWhence, FileType}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::byteorder::native_endian::U32; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs index 7e76613ba5..ecb938b7fd 100644 --- a/litebox_shim_windows/src/syscalls/nls.rs +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -5,11 +5,12 @@ use alloc::format; use alloc::string::String; use alloc::vec::Vec; use core::mem::size_of; +use litebox::fs::OFlags; 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::{FileMode as Mode, FileType}; use litebox_common_windows::loader::PAGE_SIZE; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 50f8f828a4..02bfade1e8 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -40,13 +40,14 @@ use litebox::event::{ polling::{Pollee, TryOpError}, }; use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::fs::OFlags; 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::{FileMode as Mode, FileType}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -2476,6 +2477,7 @@ mod tests { use super::*; use core::mem::size_of; use litebox::LiteBox; + use litebox_broker_protocol::fs::{FileMode, FileUser}; extern crate std; @@ -2542,9 +2544,7 @@ mod tests { /// 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() -> (LiteBox, RegistryStore) { - let mode = litebox_broker_core::fs::Mode::RWXU - | litebox_broker_core::fs::Mode::RWXG - | litebox_broker_core::fs::Mode::RWXO; + let mode = FileMode::RWXU | FileMode::RWXG | FileMode::RWXO; let litebox = crate::test_broker::litebox_with_broker_files( test_platform(), alloc::vec![ @@ -2552,14 +2552,14 @@ mod tests { "/".into(), litebox_broker_core::fs::in_mem::InitialNode::Directory { mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, + owner: FileUser::ROOT, }, ), ( "/registry".into(), litebox_broker_core::fs::in_mem::InitialNode::Directory { mode, - owner: litebox_broker_core::fs::UserInfo::ROOT, + owner: FileUser::ROOT, }, ), ], diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index c83a80f299..d85c4cc36f 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -15,7 +15,8 @@ use alloc::vec::Vec; use core::mem::size_of; use litebox::platform::RawConstPointer as _; use litebox::utils::TruncateExt as _; -use litebox_broker_core::fs::{Mode, UserInfo, in_mem::InitialNode}; +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; From 1ff3435e9a11a29266f3f8d527f7979646bb7b99 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 09:59:17 -0700 Subject: [PATCH 11/34] Share protocol filesystem metadata Use protocol file status, directory entries, and node identities directly across LiteBox, broker core, and shims. Preserve nonzero device identities and narrow metadata only at ABI and memory boundaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/file.rs | 120 ++------------- litebox/src/fs/mod.rs | 8 +- litebox/src/fs/tests.rs | 109 ++++++++------ litebox_broker_core/src/fs/backend.rs | 8 +- litebox_broker_core/src/fs/composer.rs | 26 ++-- litebox_broker_core/src/fs/devices.rs | 42 +++--- litebox_broker_core/src/fs/in_mem.rs | 31 ++-- litebox_broker_core/src/fs/inode_allocator.rs | 36 ++++- litebox_broker_core/src/fs/mod.rs | 46 +----- .../src/fs/nine_p/metadata_tests.rs | 115 +++++++++++++++ litebox_broker_core/src/fs/nine_p/mod.rs | 71 +++++---- litebox_broker_core/src/fs/overlay.rs | 53 +++---- litebox_broker_core/src/fs/resolver.rs | 42 +++--- litebox_broker_core/src/fs/service.rs | 94 +++++------- litebox_broker_core/src/fs/tar_ro.rs | 57 ++++---- litebox_broker_core/src/fs/test_support.rs | 7 +- litebox_broker_core/src/fs/tests.rs | 8 +- litebox_broker_protocol/src/fs.rs | 39 ++++- litebox_broker_protocol/src/wire.rs | 36 ++++- litebox_broker_protocol/src/wire/fs.rs | 8 +- litebox_common_linux/src/lib.rs | 138 ++++++++++++++---- litebox_shim_linux/src/loader/elf.rs | 41 ++++-- litebox_shim_linux/src/syscalls/file.rs | 94 ++++++++++-- litebox_shim_linux/src/syscalls/mm.rs | 22 +-- .../src/syscalls/test_broker.rs | 5 +- litebox_shim_linux/src/syscalls/tests.rs | 4 +- litebox_shim_windows/src/loader/pe.rs | 12 +- litebox_shim_windows/src/syscalls/file.rs | 67 +++++++-- litebox_shim_windows/src/syscalls/nls.rs | 9 +- litebox_shim_windows/src/syscalls/registry.rs | 25 ++-- litebox_shim_windows/src/syscalls/section.rs | 32 ++-- 31 files changed, 828 insertions(+), 577 deletions(-) create mode 100644 litebox_broker_core/src/fs/nine_p/metadata_tests.rs diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index 2edf97b4f4..7f87c8bd15 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -9,13 +9,12 @@ use alloc::vec; use alloc::vec::Vec; use bitflags::bitflags; use core::ffi::c_uint; -use core::num::NonZeroUsize; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::error::ErrorCode; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileError, FileMode as Mode, FileNodeInfo, FileOpenFlags, - FileSeekWhence as SeekWhence, FileStatus as BrokerFileStatus, FileType, FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileError, FileMode as Mode, FileOpenFlags, + FileSeekWhence as SeekWhence, FileStatus, FileUser as UserInfo, }; use crate::path::Arg; @@ -112,48 +111,6 @@ bitflags! { } } -/// 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, -} - -/// 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 [`LiteBox::read_file_directory`]. -#[derive(Debug)] -#[non_exhaustive] -pub struct DirEntry { - pub name: String, - pub file_type: FileType, - pub ino_info: Option, -} - /// Type marker for file descriptors backed by broker-owned files. pub struct File(core::marker::PhantomData); @@ -353,14 +310,12 @@ impl LiteBox { pub fn read_file_directory( &self, fd: &FileFd, - ) -> Result, ReadDirError> { + ) -> Result, ReadDirError> { let file = self.broker_file(fd).ok_or(ReadDirError::ClosedFd)?; - let entries = file - .broker + file.broker .read_directory(file.handle) .map_err(|error| broker_fd_error(error, ReadDirError::ClosedFd, ReadDirError::Io))? - .map_err(read_dir_error)?; - directory_entries(entries) + .map_err(read_dir_error) } /// Obtain the status of a path. @@ -370,26 +325,22 @@ impl LiteBox { path: impl Arg, ) -> Result { let path = Self::broker_path(context, path)?; - let status = self - .broker_control() + self.broker_control() .ok_or(FileStatusError::Io)? .path_file_status(&path, context.acting_user()) .map_err(|_| FileStatusError::Io)? - .map_err(file_status_error)?; - file_status(status) + .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)?; - let status = file - .broker + file.broker .handle_file_status(file.handle) .map_err(|error| { broker_fd_error(error, FileStatusError::ClosedFd, FileStatusError::Io) })? - .map_err(file_status_error)?; - file_status(status) + .map_err(file_status_error) } /// Get static backing data for a file, if available and supported. @@ -689,59 +640,6 @@ fn file_status_error(error: FileError) -> FileStatusError { path_error(error).map_or(FileStatusError::Io, Into::into) } -fn file_status(status: BrokerFileStatus) -> Result { - Ok(FileStatus { - file_type: status.file_type, - mode: status.mode, - size: usize::try_from(status.size).map_err(|_| FileStatusError::Io)?, - owner: status.owner, - node_info: status_node_info(status.node_info)?, - blksize: usize::try_from(status.block_size).map_err(|_| FileStatusError::Io)?, - }) -} - -fn directory_entries(entries: Vec) -> Result, ReadDirError> { - let mut output = Vec::new(); - output - .try_reserve_exact(entries.len()) - .map_err(|_| ReadDirError::Io)?; - for entry in entries { - output.push(DirEntry { - name: entry.name, - file_type: entry.file_type, - ino_info: entry.node_info.map(directory_node_info).transpose()?, - }); - } - Ok(output) -} - -fn status_node_info(node: FileNodeInfo) -> Result { - Ok(NodeInfo { - dev: usize::try_from(node.dev).map_err(|_| FileStatusError::Io)?, - ino: usize::try_from(node.ino).map_err(|_| FileStatusError::Io)?, - rdev: optional_device(node.rdev).map_err(|()| FileStatusError::Io)?, - }) -} - -fn directory_node_info(node: FileNodeInfo) -> Result { - Ok(NodeInfo { - dev: usize::try_from(node.dev).map_err(|_| ReadDirError::Io)?, - ino: usize::try_from(node.ino).map_err(|_| ReadDirError::Io)?, - rdev: optional_device(node.rdev).map_err(|()| ReadDirError::Io)?, - }) -} - -fn optional_device(device: Option) -> Result, ()> { - device - .map(|device| { - usize::try_from(device) - .ok() - .and_then(core::num::NonZeroUsize::new) - .ok_or(()) - }) - .transpose() -} - crate::fd::enable_fds_for_subsystem! { @ Platform: { sync::RawSyncPrimitivesProvider }; File; diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index 65c1390d98..1bb78a6574 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Guest-facing file values and operations. +//! 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 and descriptor integration, plus the 9P transport traits that -//! deployments implement. +//! retains caller context and local descriptors, plus the 9P transport traits that deployments +//! implement. Shared file values are defined in [`litebox_broker_protocol::fs`]. pub mod errors; mod file; -pub use file::{Context, DirEntry, File, FileFd, FileStatus, NodeInfo, OFlags, ResolvedPath}; +pub use file::{Context, File, FileFd, OFlags, ResolvedPath}; #[doc(hidden)] pub mod nine_p { diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index d498ff36da..9aa9ad86a6 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -5,7 +5,7 @@ //! //! Filesystem resolution, backend, and 9P semantics belong to `litebox_broker_core` and are tested //! there. What LiteBox owns is the guest side of the boundary: resolving paths against a -//! [`Context`], converting between guest and protocol values, mapping broker errors onto guest +//! [`Context`], passing shared metadata through, mapping broker errors onto guest //! error types, and tying broker-owned files to guest descriptors. These tests script broker //! responses over a local channel, so no broker core, policy engine, or host transport is //! involved. @@ -17,13 +17,14 @@ use alloc::string::{String, ToString as _}; use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; +use core::num::NonZeroU64; use std::sync::{Barrier, Mutex}; use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::fs::{ FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, - FileSeekWhence, FileStatus as BrokerFileStatus, FileType, FileUser, MAX_FILE_TRANSFER_SIZE, - OpenFileResponse, ReadDirectoryResponse, ReadFileResponse, SeekFileResponse, WriteFileResponse, + FileSeekWhence, FileStatus, FileType, FileUser, MAX_FILE_TRANSFER_SIZE, OpenFileResponse, + ReadDirectoryResponse, ReadFileResponse, SeekFileResponse, WriteFileResponse, encode_directory_entries_chunk, }; use litebox_broker_protocol::message::{ @@ -79,6 +80,7 @@ enum Call { path: String, user: FileUser, }, + HandleStatus(ObjectHandle), Unlink { path: String, user: FileUser, @@ -267,6 +269,10 @@ impl ScriptedBroker { }); self.reply() } + FileRequest::HandleStatus(request) => { + self.record(Call::HandleStatus(request.handle)); + self.reply() + } FileRequest::Unlink(request) => { self.record(Call::Unlink { path: self.staged_path(request.path), @@ -578,45 +584,53 @@ fn in_flight_read_keeps_the_broker_file_alive_after_close() { } #[test] -fn path_status_converts_broker_values() { +fn path_and_handle_status_preserve_protocol_metadata() { let context = Context::new(); - let (broker, fs) = scripted_fs([Scripted::Reply(FileResponse::PathStatus( - BrokerFileStatus { - file_type: FileType::CharacterDevice, - mode: FileMode::from_bits(0o644).unwrap(), - size: 12, - owner: FileUser { user: 1, group: 2 }, - node_info: FileNodeInfo { - dev: 3, - ino: 4, - rdev: Some(5), - }, - block_size: 4096, + let expected = FileStatus { + file_type: FileType::CharacterDevice, + mode: FileMode::from_bits(0o644).unwrap(), + size: u64::MAX, + owner: FileUser { user: 1, group: 2 }, + node_info: FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX, + rdev: Some(NonZeroU64::MAX), }, - ))]); + block_size: u64::MAX, + }; + let (broker, fs) = scripted_fs([ + Scripted::Reply(FileResponse::PathStatus(expected)), + opened(), + Scripted::Reply(FileResponse::HandleStatus(expected)), + ]); let status = fs .path_file_status(&context, "/dev/null") .expect("status should succeed"); - assert_eq!(status.file_type, FileType::CharacterDevice); - assert_eq!(status.mode, FileMode::from_bits(0o644).unwrap()); - assert_eq!(status.size, 12); - assert_eq!(status.owner.user, 1); - assert_eq!(status.owner.group, 2); - assert_eq!(status.node_info.dev, 3); - assert_eq!(status.node_info.ino, 4); - assert_eq!( - status.node_info.rdev.map(core::num::NonZeroUsize::get), - Some(5) - ); - assert_eq!(status.blksize, 4096); + assert_eq!(status, expected); + let fd = fs + .open_file(&context, "/dev/null", OFlags::RDONLY, FileMode::empty()) + .unwrap(); + assert_eq!(fs.file_status(&fd).unwrap(), expected); + fs.close_file(&fd).unwrap(); assert_eq!( *broker.calls(), - vec![Call::PathStatus { - path: String::from("/dev/null"), - user: user(&context), - }] + vec![ + Call::PathStatus { + path: String::from("/dev/null"), + user: user(&context), + }, + Call::Open { + path: String::from("/dev/null"), + user: user(&context), + access: FileAccessMode::ReadOnly, + flags: FileOpenFlags::NONE, + mode: FileMode::empty(), + }, + Call::HandleStatus(FILE_HANDLE), + Call::Close(FILE_HANDLE), + ] ); } @@ -628,27 +642,36 @@ fn read_dir_reassembles_paged_broker_entries() { name: String::from("one"), file_type: FileType::RegularFile, node_info: Some(FileNodeInfo { - dev: 1, - ino: 2, + dev: u64::MAX, + ino: u64::MAX, rdev: None, }), }, FileDirectoryEntry { - name: String::from("two"), + name: String::from("directory"), file_type: FileType::Directory, node_info: None, }, + FileDirectoryEntry { + name: String::from("device"), + file_type: FileType::CharacterDevice, + node_info: Some(FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX, + rdev: Some(NonZeroU64::MAX), + }), + }, ]; // A page that holds one entry, so the guest has to follow the continuation index. let page_bytes = - encode_directory_entries_chunk(&entries[..1], 0, MAX_FILE_TRANSFER_SIZE as usize) + encode_directory_entries_chunk(&entries[2..], 0, MAX_FILE_TRANSFER_SIZE as usize) .unwrap() .0 .len(); let (broker, fs) = scripted_fs([ opened(), Scripted::Directory { - entries, + entries: entries.clone(), page_bytes, }, ]); @@ -661,18 +684,12 @@ fn read_dir_reassembles_paged_broker_entries() { FileMode::empty(), ) .expect("open should succeed"); - let entries = fs + let received = fs .read_file_directory(&fd) .expect("read_dir should succeed"); fs.close_file(&fd).expect("close should succeed"); - assert_eq!(entries.len(), 2); - assert_eq!(entries[0].name, "one"); - assert_eq!(entries[0].file_type, FileType::RegularFile); - assert_eq!(entries[0].ino_info.as_ref().map(|node| node.ino), Some(2)); - assert_eq!(entries[1].name, "two"); - assert_eq!(entries[1].file_type, FileType::Directory); - assert!(entries[1].ino_info.is_none()); + assert_eq!(received, entries); let calls = broker.calls(); assert_eq!( diff --git a/litebox_broker_core/src/fs/backend.rs b/litebox_broker_core/src/fs/backend.rs index e61d265b8a..d12f5832bf 100644 --- a/litebox_broker_core/src/fs/backend.rs +++ b/litebox_broker_core/src/fs/backend.rs @@ -8,14 +8,16 @@ use alloc::vec::Vec; use core::any::{Any, TypeId}; use core::marker::PhantomData; -use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; +use litebox_broker_protocol::fs::{ + FileDirectoryEntry, FileMode as Mode, FileStatus, FileUser as UserInfo, +}; use litebox_broker_protocol::stdio::StdioOutputStream; +use super::OFlags; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; -use super::{DirEntry, FileStatus, OFlags}; // This duplicates the cloneable type-erasure support from `litebox::utilities::anymap` because // broker core cannot depend on LiteBox. Keep it local unless broader reuse justifies a common home. @@ -139,7 +141,7 @@ pub trait Backend: private::Sealed + Send + Sync + Any { ) -> Result, OpenError>; /// Read directory entries at `dir`. - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError>; + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError>; /// Read at `offset` into `buf`, returning the number of bytes read. /// diff --git a/litebox_broker_core/src/fs/composer.rs b/litebox_broker_core/src/fs/composer.rs index eb73a42825..a6651d1dbe 100644 --- a/litebox_broker_core/src/fs/composer.rs +++ b/litebox_broker_core/src/fs/composer.rs @@ -9,8 +9,11 @@ use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; -use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; +use litebox_broker_protocol::fs::{ + FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, +}; +use super::OFlags; use super::backend::{ Backend, BackendHandles, CreationMetadata, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, @@ -20,7 +23,6 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::{InodeAllocator, InodeAllocators}; -use super::{DirEntry, FileStatus, NodeInfo, OFlags}; use thiserror::Error; // XXX(jayb): consider removing this via a runtime reserved device ID? @@ -51,7 +53,7 @@ struct Mount { #[derive(Clone)] struct VirtualDir { path: Vec, - node_info: NodeInfo, + node_info: FileNodeInfo, } /// Composer construction errors. @@ -227,19 +229,23 @@ impl Composer { children } - fn list_mount_children(&self, path: &[String]) -> Vec { + fn list_mount_children(&self, path: &[String]) -> Vec { self.immediate_mount_children(path) .into_iter() - .map(|name| DirEntry { + .map(|name| FileDirectoryEntry { name, file_type: FileType::Directory, // TODO(jayb): set up proper inode info for these - ino_info: None, + node_info: None, }) .collect() } - fn merge_mount_children(&self, mut entries: Vec, path: &[String]) -> Vec { + fn merge_mount_children( + &self, + mut entries: Vec, + path: &[String], + ) -> Vec { for child in self.list_mount_children(path) { entries.retain(|entry| entry.name != child.name); entries.push(child); @@ -273,7 +279,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, @@ -282,7 +288,7 @@ impl Composer { size: super::DEFAULT_DIRECTORY_SIZE, owner: UserInfo::ROOT, node_info, - blksize: super::DEFAULT_DIRECTORY_SIZE, + block_size: super::DEFAULT_DIRECTORY_SIZE, } } @@ -623,7 +629,7 @@ impl Backend for Composer { } } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let handle = handle.into_typed::(); match handle.inner { ComposerDirHandleInner::Virtual { path } => Ok(self.list_mount_children(&path)), diff --git a/litebox_broker_core/src/fs/devices.rs b/litebox_broker_core/src/fs/devices.rs index ef9d889c90..4bc2971e21 100644 --- a/litebox_broker_core/src/fs/devices.rs +++ b/litebox_broker_core/src/fs/devices.rs @@ -8,10 +8,13 @@ use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; +use litebox_broker_protocol::fs::{ + FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, +}; use litebox_broker_protocol::random::MAX_RANDOM_TRANSFER_SIZE; use litebox_broker_protocol::stdio::StdioOutputStream; +use super::OFlags; use super::backend::{ Backend, BackendHandles, CreationMetadata, DeviceIo, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, @@ -21,14 +24,13 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{DirEntry, FileStatus, NodeInfo, OFlags}; /// 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 @@ -38,24 +40,24 @@ const URANDOM_BLOCK_SIZE: usize = 0x1000; /// name=/dev/stderr dev=64 ino=9 rdev=34822 /// ``` // XXX(jayb): Should we be pulling the device names and such from the inode allocator? -const STDIO_NODE_INFO: NodeInfo = NodeInfo { +const STDIO_NODE_INFO: FileNodeInfo = FileNodeInfo { 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 { +const NULL_NODE_INFO: FileNodeInfo = FileNodeInfo { 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 { +const URANDOM_NODE_INFO: FileNodeInfo = FileNodeInfo { 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)] @@ -88,7 +90,7 @@ impl Device { size: 0, owner: UserInfo::ROOT, node_info: STDIO_NODE_INFO, - blksize: STDIO_BLOCK_SIZE, + block_size: STDIO_BLOCK_SIZE, }, Device::Null => FileStatus { file_type: FileType::CharacterDevice, @@ -96,7 +98,7 @@ impl Device { size: 0, owner: UserInfo::ROOT, node_info: NULL_NODE_INFO, - blksize: NULL_BLOCK_SIZE, + block_size: NULL_BLOCK_SIZE, }, Device::URandom => FileStatus { file_type: FileType::CharacterDevice, @@ -104,7 +106,7 @@ impl Device { size: 0, owner: UserInfo::ROOT, node_info: URANDOM_NODE_INFO, - blksize: URANDOM_BLOCK_SIZE, + block_size: URANDOM_BLOCK_SIZE, }, } } @@ -113,7 +115,7 @@ impl Device { /// A [`super::backend::Backend`] that supports Unix-y devices. pub struct Devices { /// Stable inode info for this backend's root directory. - root_inode: NodeInfo, + root_inode: FileNodeInfo, _alloc: InodeAllocator, } @@ -232,14 +234,14 @@ impl Backend for Devices { }) } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let _handle = handle.into_typed::(); Ok(Device::ALL .iter() - .map(|(n, d)| DirEntry { + .map(|(n, d)| FileDirectoryEntry { name: String::from(*n), file_type: FileType::CharacterDevice, - ino_info: Some(d.file_status().node_info), + node_info: Some(d.file_status().node_info), }) .collect()) } @@ -317,8 +319,8 @@ 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(), - blksize: super::DEFAULT_DIRECTORY_SIZE, + node_info: self.root_inode, + block_size: 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 cbe2d2951e..55ce54e27a 100644 --- a/litebox_broker_core/src/fs/in_mem.rs +++ b/litebox_broker_core/src/fs/in_mem.rs @@ -8,7 +8,9 @@ use alloc::sync::Arc; use alloc::vec::Vec; use hashbrown::HashMap; -use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; +use litebox_broker_protocol::fs::{ + FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, +}; use litebox_platform::sync; use super::errors::{ @@ -16,7 +18,6 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{DirEntry, FileStatus, NodeInfo}; /// A [`super::backend::Backend`] that stores all files in memory. /// @@ -380,7 +381,7 @@ impl super::backend::Backend for InMe fn list_dir_at( &self, handle: super::backend::DirHandle, - ) -> Result, ReadDirError> { + ) -> Result, ReadDirError> { Ok(handle .into_typed::() .dir @@ -389,13 +390,13 @@ 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 { + FileDirectoryEntry { name: name.clone(), file_type, - ino_info: Some(node_info), + node_info: Some(node_info), } }) .collect()) @@ -467,10 +468,10 @@ 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(), - blksize: BLOCK_SIZE, + node_info: file.node_info, + block_size: BLOCK_SIZE, }) } super::backend::HandleRef::Dir(h) => { @@ -480,8 +481,8 @@ 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(), - blksize: BLOCK_SIZE, + node_info: dir.node_info, + block_size: BLOCK_SIZE, }) } } @@ -650,7 +651,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), @@ -669,14 +670,14 @@ type DirNode = Arc>>; struct DirData { perms: Permissions, children: HashMap>, - node_info: NodeInfo, + node_info: FileNodeInfo, } type FileNode = Arc>; struct FileData { perms: Permissions, data: alloc::borrow::Cow<'static, [u8]>, - node_info: NodeInfo, + node_info: FileNodeInfo, } #[derive(Clone, Debug)] diff --git a/litebox_broker_core/src/fs/inode_allocator.rs b/litebox_broker_core/src/fs/inode_allocator.rs index 9540807be7..0d1ee9ca33 100644 --- a/litebox_broker_core/src/fs/inode_allocator.rs +++ b/litebox_broker_core/src/fs/inode_allocator.rs @@ -3,7 +3,7 @@ use core::sync::atomic::{AtomicU64, Ordering}; -use super::NodeInfo; +use litebox_broker_protocol::fs::FileNodeInfo; /// Hands out [`InodeAllocator`]s, each with its own device id. #[derive(Debug)] @@ -53,20 +53,42 @@ impl InodeAllocator { Self::for_device(STANDALONE_DEVICE_ID) } - /// Allocate a fresh `NodeInfo` for a new entry on this backend. + /// Allocate a fresh [`FileNodeInfo`] for a new entry on this backend. #[must_use] - pub fn next(&self) -> NodeInfo { + pub fn next(&self) -> FileNodeInfo { let ino = self.counter.fetch_add(1, Ordering::Relaxed); - NodeInfo { + FileNodeInfo { 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 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preserves_full_width_device_and_inode_numbers() { + let allocator = InodeAllocator::for_device(u64::MAX); + allocator.counter.store(u64::MAX - 1, Ordering::Relaxed); + + assert_eq!(allocator.device_id(), u64::MAX); + assert_eq!( + allocator.next(), + FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX - 1, + rdev: None, + } + ); + assert_eq!(allocator.next().ino, u64::MAX); } } diff --git a/litebox_broker_core/src/fs/mod.rs b/litebox_broker_core/src/fs/mod.rs index 79f42d1c85..389037d2da 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -11,8 +11,6 @@ use bitflags::bitflags; use core::ffi::c_uint; -use core::num::NonZeroUsize; -use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; pub mod backend; pub mod composer; @@ -123,47 +121,5 @@ bitflags! { } } -/// 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, -} - -/// 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, -} - /// 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/metadata_tests.rs b/litebox_broker_core/src/fs/nine_p/metadata_tests.rs new file mode 100644 index 0000000000..33cad2fbe8 --- /dev/null +++ b/litebox_broker_core/src/fs/nine_p/metadata_tests.rs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::num::NonZeroU64; + +use litebox_broker_protocol::fs::{FileMode, FileNodeInfo, FileStatus, FileType, FileUser}; + +use super::fcall::{GetattrMask, Qid, QidType, Rgetattr, Stat, Time}; +use super::{Error, rgetattr_to_file_status}; + +fn attributes(valid: GetattrMask) -> Rgetattr { + Rgetattr { + valid, + qid: Qid { + typ: QidType::FILE, + version: 0, + path: u64::MAX - 1, + }, + stat: Stat { + mode: 0o100644, + uid: 1, + gid: 2, + nlink: 1, + rdev: u64::MAX, + size: u64::MAX - 2, + blksize: u64::MAX - 3, + blocks: 0, + atime: Time::default(), + mtime: Time::default(), + ctime: Time::default(), + btime: Time::default(), + generation: 0, + data_version: 0, + }, + } +} + +#[test] +fn getattr_preserves_full_width_metadata() { + for valid in [ + GetattrMask::BASIC, + GetattrMask::MODE + | GetattrMask::UID + | GetattrMask::GID + | GetattrMask::SIZE + | GetattrMask::RDEV + | GetattrMask::BLOCKS, + ] { + let status = rgetattr_to_file_status(&attributes(valid), u64::MAX).unwrap(); + + assert_eq!( + status, + FileStatus { + file_type: FileType::RegularFile, + mode: FileMode::from_bits(0o644).unwrap(), + size: u64::MAX - 2, + owner: FileUser { user: 1, group: 2 }, + node_info: FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX - 1, + rdev: NonZeroU64::new(u64::MAX), + }, + block_size: u64::MAX - 3, + } + ); + } +} + +#[test] +fn getattr_zero_device_number_is_absent() { + for valid in [GetattrMask::BASIC, GetattrMask::RDEV] { + let mut attr = attributes(valid); + attr.stat.rdev = 0; + let status = rgetattr_to_file_status(&attr, u64::MAX).unwrap(); + + assert_eq!(status.node_info.rdev, None); + } +} + +#[test] +fn getattr_ignores_unavailable_metadata() { + let status = rgetattr_to_file_status(&attributes(GetattrMask::empty()), u64::MAX).unwrap(); + + assert_eq!(status.mode, FileMode::empty()); + assert_eq!(status.size, 0); + assert_eq!(status.owner, FileUser::ROOT); + assert_eq!(status.block_size, 0); + assert_eq!( + status.node_info, + FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX - 1, + rdev: None, + } + ); +} + +#[test] +fn getattr_rejects_unrepresentable_owners() { + for valid in [GetattrMask::BASIC, GetattrMask::UID | GetattrMask::GID] { + let mut attr = attributes(valid); + attr.stat.uid = u32::from(u16::MAX) + 1; + assert!(matches!( + rgetattr_to_file_status(&attr, 0), + Err(Error::InvalidResponse) + )); + + attr.stat.uid = 1; + attr.stat.gid = u32::from(u16::MAX) + 1; + assert!(matches!( + rgetattr_to_file_status(&attr, 0), + Err(Error::InvalidResponse) + )); + } +} diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index 4be90ff863..0f0a9a919d 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -11,10 +11,12 @@ 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 litebox_broker_protocol::fs::{FileMode, FileType, FileUser}; +use litebox_broker_protocol::fs::{ + FileDirectoryEntry, FileMode, FileNodeInfo, FileStatus, FileType, FileUser, +}; use litebox_platform::sync; use thiserror::Error; @@ -31,6 +33,8 @@ use super::errors::{ mod client; mod fcall; mod id_pool; +#[cfg(test)] +mod metadata_tests; #[cfg(all(test, target_os = "linux"))] mod tests; @@ -53,9 +57,9 @@ pub struct NineP>, - /// Device id reported in every [`NodeInfo`](super::NodeInfo) from this backend; inode numbers + /// Device id reported in every [`FileNodeInfo`] 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, } @@ -430,7 +434,7 @@ where }) } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let handle = handle.into_typed::(); let entries = self.client.readdir_all(&handle.fid.fid)?; Ok(entries @@ -442,18 +446,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| FileDirectoryEntry { + name: String::from_utf8_lossy(&entry.name).into_owned(), + file_type: qid_type_to_file_type(entry.qid.typ), + node_info: Some(FileNodeInfo { + dev: self.device_id, + ino: entry.qid.path, + rdev: None, + }), }) - .collect::>()?) + .collect()) } fn read( @@ -500,7 +502,7 @@ where SeekBehavior::PositionBased } - fn status(&self, h: HandleRef<'_>) -> Result { + fn status(&self, h: HandleRef<'_>) -> Result { let fid = match h { HandleRef::File(h) => &h.get_typed::().fid, HandleRef::Dir(h) => &h.get_typed::().fid, @@ -721,32 +723,27 @@ fn file_mode(mode: u32) -> FileMode { /// /// Inode numbers come from the server's qids; `device_id` is the device the caller reports this /// filesystem as. -fn rgetattr_to_file_status( - attr: &fcall::Rgetattr, - device_id: usize, -) -> Result { +fn rgetattr_to_file_status(attr: &fcall::Rgetattr, 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 { + Ok(FileStatus { file_type, mode: file_mode(attr.stat.mode), - size: usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)?, + size: attr.stat.size, owner: FileUser { 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 { + node_info: FileNodeInfo { 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)?, + block_size: attr.stat.blksize, }) } else { - Ok(super::FileStatus { + Ok(FileStatus { file_type, mode: if attr.valid.contains(fcall::GetattrMask::MODE) { file_mode(attr.stat.mode) @@ -754,7 +751,7 @@ fn rgetattr_to_file_status( FileMode::empty() }, size: if attr.valid.contains(fcall::GetattrMask::SIZE) { - usize::try_from(attr.stat.size).map_err(|_| Error::InvalidResponse)? + attr.stat.size } else { 0 }, @@ -770,19 +767,17 @@ fn rgetattr_to_file_status( 0 }, }, - node_info: super::NodeInfo { + node_info: FileNodeInfo { 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)? + block_size: if attr.valid.contains(fcall::GetattrMask::BLOCKS) { + attr.stat.blksize } else { 0 }, diff --git a/litebox_broker_core/src/fs/overlay.rs b/litebox_broker_core/src/fs/overlay.rs index ea7f800f33..95e0953b5f 100644 --- a/litebox_broker_core/src/fs/overlay.rs +++ b/litebox_broker_core/src/fs/overlay.rs @@ -20,9 +20,12 @@ use alloc::vec::Vec; use hashbrown::{HashMap, HashSet}; -use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; +use litebox_broker_protocol::fs::{ + FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, +}; use litebox_platform::sync::{Mutex, MutexGuard, RawSyncPrimitivesProvider}; +use super::OFlags; use super::backend::{ Backend, BackendHandles, CreationMetadata, DeviceIo, DirHandle, FileHandle, Handle, HandleRef, NoDeviceIo, PermissionCheck, PermissionInfo, Permissioned, SeekBehavior, WalkOutcome, @@ -33,7 +36,6 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{DirEntry, FileStatus, NodeInfo, OFlags}; /// The reserved namespace prefix; no overlay-visible name may start with it. const MARKER_PREFIX: &str = ".litebox-overlay-"; @@ -60,17 +62,17 @@ struct Namespace; struct State { /// Overlay-visible identity assigned to each per-layer node. - ids: HashMap, + ids: HashMap, /// Files that have been copied up, by overlay identity, and their handle in the upper backend. /// A handle opened against a lower backend stays valid, but every operation looks here first. - copied_up: HashMap, + copied_up: HashMap, } /// A node as identified by the layer that owns it; `Lower` carries the lower backend's index. #[derive(Clone, PartialEq, Eq, Hash)] enum LayerNode { - Upper(NodeInfo), - Lower(usize, NodeInfo), + Upper(FileNodeInfo), + Lower(usize, FileNodeInfo), } pub struct OverlayWalkingDir { @@ -103,7 +105,7 @@ enum OverlayFileLayer { layer: usize, handle: FileHandle, /// The overlay identity of the file, under which a later copy-up records its upper handle. - node: NodeInfo, + node: FileNodeInfo, }, } @@ -118,7 +120,7 @@ struct ResolvedDir { /// An overlay-visible directory entry, plus which layers contribute to it. struct ResolvedEntry { /// The entry as reported by the layer that owns it. - entry: DirEntry, + entry: FileDirectoryEntry, upper: bool, /// The highest-precedence lower backend with an entry of this name, if any. lower: Option, @@ -286,7 +288,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), ); @@ -528,17 +530,16 @@ impl Overlay { /// The overlay-visible identity of `node` as owned by `layer`, allocated on first sight. fn map_node( &self, - ids: &mut HashMap, + ids: &mut HashMap, layer: Option, - node: NodeInfo, - ) -> NodeInfo { + node: FileNodeInfo, + ) -> FileNodeInfo { let rdev = node.rdev; - ids.entry(layer_node(layer, node)) - .or_insert_with(|| NodeInfo { + *ids.entry(layer_node(layer, node)) + .or_insert_with(|| FileNodeInfo { rdev, ..self.alloc.next() }) - .clone() } /// `status` as reported by `layer`, with its node identity replaced by the overlay's own. @@ -555,13 +556,13 @@ impl Overlay { fn bind_copy_up( &self, layer: usize, - lower: NodeInfo, - upper: NodeInfo, + lower: FileNodeInfo, + upper: FileNodeInfo, upper_file: Option<&FileHandle>, ) { 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()); } @@ -605,8 +606,8 @@ impl Overlay { if entry.file_type != FileType::Directory { blocked.insert(entry.name.clone()); } - entry.ino_info = entry - .ino_info + entry.node_info = entry + .node_info .take() .map(|node| self.map_node(&mut self.state.lock().ids, None, node)); entries.insert( @@ -634,7 +635,7 @@ impl Overlay { continue; } let directory = lower_entry.file_type == FileType::Directory; - let lower_node = lower_entry.ino_info.take(); + let lower_node = lower_entry.node_info.take(); let entry = entries .entry(name.clone()) .or_insert_with(|| ResolvedEntry { @@ -646,7 +647,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.node_info = lower_node.map(|node| { self.map_node(&mut self.state.lock().ids, Some(layer), node) }); } @@ -657,7 +658,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.node_info) { self.state .lock() .ids @@ -681,7 +682,7 @@ impl Overlay { /// The node `node` as owned by `layer`, which is `None` for the upper backend and `Some(index)` /// for a lower one. -fn layer_node(layer: Option, node: NodeInfo) -> LayerNode { +fn layer_node(layer: Option, node: FileNodeInfo) -> LayerNode { match layer { None => LayerNode::Upper(node), Some(layer) => LayerNode::Lower(layer, node), @@ -889,10 +890,10 @@ impl Backend for Overlay { }) } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let path = handle.into_typed::().path; let resolved = self.resolve_dir(&path).map_err(|_| ReadDirError::Io)?; - let mut entries: Vec = resolved + let mut entries: Vec = resolved .entries .into_values() .map(|entry| entry.entry) diff --git a/litebox_broker_core/src/fs/resolver.rs b/litebox_broker_core/src/fs/resolver.rs index 77a17154cb..91f18b472e 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -8,7 +8,8 @@ use alloc::vec; use alloc::vec::Vec; use core::marker::PhantomData; use litebox_broker_protocol::fs::{ - FileMode as Mode, FileSeekWhence as SeekWhence, FileType, FileUser as UserInfo, + FileDirectoryEntry, FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, FileType, + FileUser as UserInfo, }; use super::errors::{ @@ -625,10 +626,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), }; @@ -700,17 +704,23 @@ impl Resolver 0, - SeekWhence::RelativeToCurrentOffset => entry.position, + SeekWhence::RelativeToCurrentOffset => { + u64::try_from(entry.position).map_err(|_| SeekError::InvalidOffset)? + } SeekWhence::RelativeToEnd => file_len, }; let new_position = base - .checked_add_signed(offset) + .checked_add_signed( + i64::try_from(offset).map_err(|_| SeekError::InvalidOffset)?, + ) .ok_or(SeekError::InvalidOffset)?; // TODO(jayb): Linux allows regular files to seek past EOF, while some backends or // file types may not. Model that distinction instead of using one resolver rule. if new_position > file_len { return Err(SeekError::InvalidOffset); } + let new_position = + usize::try_from(new_position).map_err(|_| SeekError::InvalidOffset)?; entry.position = new_position; Ok(new_position) } @@ -914,7 +924,7 @@ impl Resolver, - ) -> Result, ReadDirError> { + ) -> Result, ReadDirError> { if entry.path_only { // TODO(jayb): Add an error variant for operations not permitted on O_PATH fds. unimplemented!("read_dir on O_PATH fd") @@ -926,26 +936,22 @@ impl Resolver Result { + pub fn file_status(&self, user: UserInfo, path: &str) -> Result { let entry = self.open(user, path, OFlags::PATH, Mode::empty()) .map_err(|error| match error { @@ -964,7 +970,7 @@ impl Resolver, - ) -> Result { + ) -> Result { self.backend.status(entry.handle.as_ref()) } diff --git a/litebox_broker_core/src/fs/service.rs b/litebox_broker_core/src/fs/service.rs index fb389a2780..908a4029f1 100644 --- a/litebox_broker_core/src/fs/service.rs +++ b/litebox_broker_core/src/fs/service.rs @@ -8,19 +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, 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, NodeInfo, OFlags}; use crate::session::{ObjectEntry, ObjectRights}; use crate::{BrokerError, BrokerSession, Result}; @@ -46,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 { @@ -114,7 +114,7 @@ mod private { &self, _session: &BrokerSession, _file: &File, - ) -> ServiceResult { + ) -> ServiceResult { Err(BrokerError::UnsupportedOperation) } @@ -123,7 +123,7 @@ mod private { _session: &BrokerSession, _path: &str, _user: FileUser, - ) -> ServiceResult { + ) -> ServiceResult { Err(BrokerError::UnsupportedOperation) } @@ -362,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( @@ -389,12 +382,13 @@ where _session: &BrokerSession, path: &str, user: FileUser, - ) -> ServiceResult { - let status = match Resolver::file_status(self, 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( @@ -528,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) } @@ -538,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)); @@ -735,34 +729,8 @@ fn open_flags(access: FileAccessMode, flags: FileOpenFlags) -> Result { Ok(output) } -fn file_status(status: FileStatus) -> Result { - Ok(ProtocolFileStatus { - file_type: status.file_type, - mode: status.mode & FileMode::SUPPORTED, - size: u64::try_from(status.size).map_err(|_| BrokerError::Internal)?, - owner: status.owner, - 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: entry.file_type, - node_info: entry.ino_info.map(node_info).transpose()?, - }) -} - -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 @@ -890,24 +858,28 @@ fn file_status_error(error: FileStatusError) -> FileError { #[cfg(test)] mod tests { use super::*; - use litebox_broker_protocol::fs::FileType; + 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: FileMode::from_bits_retain(0o100644), - size: 1, + size: u64::MAX, owner: FileUser::ROOT, - node_info: NodeInfo { - dev: 2, - ino: 3, - rdev: None, + node_info: FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX - 1, + rdev: NonZeroU64::new(u64::MAX), }, - blksize: 4096, - }) - .unwrap(); + block_size: 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 a4c91cb055..1f3fe081cd 100644 --- a/litebox_broker_core/src/fs/tar_ro.rs +++ b/litebox_broker_core/src/fs/tar_ro.rs @@ -28,23 +28,23 @@ use alloc::string::String; use alloc::vec::Vec; use core::ops::Range; use hashbrown::HashMap; -use litebox_broker_protocol::fs::{FileMode as Mode, FileType, FileUser as UserInfo}; - -use super::DirEntry; +use litebox_broker_protocol::fs::{ + FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, +}; use super::{ - NodeInfo, OFlags, + OFlags, backend::{CreationMetadata, DirHandle, FileHandle, HandleRef, WalkingDirHandle}, errors::{ - ChmodError, ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, - RmdirError, TruncateError, UnlinkError, WalkError, WriteError, + ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, + ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }, inode_allocator::InodeAllocator, }; /// 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 { @@ -179,26 +179,24 @@ impl super::backend::Backend for TarRo { }) } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let handle = handle.into_typed::(); Ok(self.tar_index.dirs[handle.idx] .children .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 { + FileDirectoryEntry { name: name.clone(), file_type, - ino_info: Some(node_info), + node_info: Some(node_info), } }) .collect()) @@ -238,31 +236,28 @@ impl super::backend::Backend for TarRo { super::backend::SeekBehavior::PositionBased } - fn status( - &self, - h: HandleRef<'_>, - ) -> Result { + fn status(&self, h: HandleRef<'_>) -> Result { match h { HandleRef::File(h) => { let file = &self.tar_index.files[h.get_typed::().idx]; - Ok(super::FileStatus { + Ok(FileStatus { file_type: FileType::RegularFile, mode: file.mode, - size: file.data_range.len(), + size: u64::try_from(file.data_range.len()).map_err(|_| FileStatusError::Io)?, owner: file.owner, - node_info: file.node_info.clone(), - blksize: BLOCK_SIZE, + node_info: file.node_info, + block_size: BLOCK_SIZE, }) } HandleRef::Dir(h) => { let dir = &self.tar_index.dirs[h.get_typed::().idx]; - Ok(super::FileStatus { + Ok(FileStatus { file_type: FileType::Directory, mode: DEFAULT_DIR_MODE, size: super::DEFAULT_DIRECTORY_SIZE, owner: dir.owner.unwrap_or(DEFAULT_DIRECTORY_OWNER), - node_info: dir.node_info.clone(), - blksize: BLOCK_SIZE, + node_info: dir.node_info, + block_size: BLOCK_SIZE, }) } } @@ -325,12 +320,12 @@ struct IndexedFile { data_range: Range, mode: Mode, owner: UserInfo, - node_info: NodeInfo, + node_info: FileNodeInfo, } struct IndexedDir { owner: Option, - node_info: NodeInfo, + node_info: FileNodeInfo, children: HashMap, } diff --git a/litebox_broker_core/src/fs/test_support.rs b/litebox_broker_core/src/fs/test_support.rs index 57e17d90c0..c179381eda 100644 --- a/litebox_broker_core/src/fs/test_support.rs +++ b/litebox_broker_core/src/fs/test_support.rs @@ -10,17 +10,18 @@ use alloc::vec::Vec; use litebox_broker_protocol::fs::{ - FileMode as Mode, FileSeekWhence as SeekWhence, FileUser as UserInfo, + FileDirectoryEntry, FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, + FileUser as UserInfo, }; use litebox_broker_protocol::stdio::StdioOutputStream; +use super::OFlags; use super::backend::{Backend, DeviceIo, NoDeviceIo}; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; use super::resolver::{Resolver, ResolverEntry}; -use super::{DirEntry, FileStatus, OFlags}; use crate::test_platform::TestPlatform; /// The unprivileged user these tests act as unless they need root. @@ -158,7 +159,7 @@ impl Fs { pub(in crate::fs) fn read_dir( &self, entry: &Entry, - ) -> Result, ReadDirError> { + ) -> Result, ReadDirError> { self.resolver.read_dir(entry) } diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs index 28d42de792..30c0a1c014 100644 --- a/litebox_broker_core/src/fs/tests.rs +++ b/litebox_broker_core/src/fs/tests.rs @@ -319,7 +319,7 @@ mod in_mem { _ => panic!("Unexpected entry: {}", entry.name), } if entry.name != "." && entry.name != ".." { - assert!(entry.ino_info.is_some(), "Inode info should be present"); + assert!(entry.node_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. @@ -1075,7 +1075,7 @@ mod tar_ro { _ => panic!("Unexpected entry: {}", entry.name), } if entry.name != "." && entry.name != ".." { - assert!(entry.ino_info.is_some(), "Inode info should be present"); + assert!(entry.node_info.is_some(), "Inode info should be present"); } else { // TODO(jayb): Re-enable this assertion once Composer handles `.` and `..` inode // information better. @@ -1393,7 +1393,7 @@ mod overlay { assert_eq!(entries[2].name, "baz"); assert_eq!(entries[2].file_type, FileType::RegularFile); assert!( - entries[2].ino_info.is_some(), + entries[2].node_info.is_some(), "Inode info should be present" ); } @@ -1447,7 +1447,7 @@ mod overlay { _ => panic!("Unexpected entry: {}", entry.name), } if entry.name != "." && entry.name != ".." { - assert!(entry.ino_info.is_some(), "Inode info should be present"); + assert!(entry.node_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. diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index e9252c6282..6da30741f3 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -5,6 +5,7 @@ use alloc::string::String; use alloc::vec::Vec; +use core::num::NonZeroU64; use bitflags::bitflags; use thiserror::Error; @@ -47,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. @@ -572,7 +573,9 @@ pub fn try_decode_directory_entries( 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 }) @@ -674,7 +677,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), } @@ -746,7 +749,7 @@ mod tests { node_info: Some(FileNodeInfo { dev: 5, ino: 7, - rdev: Some(11), + rdev: NonZeroU64::new(11), }), }, ]; @@ -763,7 +766,7 @@ mod tests { node_info: Some(FileNodeInfo { dev: 2, ino: 3, - rdev: Some(5), + rdev: NonZeroU64::new(5), }), }]) .unwrap(); @@ -808,6 +811,28 @@ mod tests { assert_eq!(next_index, None); } + #[test] + fn directory_payload_preserves_full_width_node_info_and_rejects_zero_rdev() { + let entries = [FileDirectoryEntry { + name: "device".into(), + file_type: FileType::CharacterDevice, + node_info: Some(FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX, + rdev: Some(NonZeroU64::MAX), + }), + }]; + let mut payload = encode_directory_entries(&entries).unwrap(); + assert_eq!(decode_directory_entries(&payload).unwrap(), entries); + + let rdev_start = payload.len() - size_of::(); + payload[rdev_start..].fill(0); + assert_eq!( + decode_directory_entries(&payload), + Err(DirectoryPayloadError::Malformed) + ); + } + #[test] fn directory_payload_rejects_malformed_data() { assert_eq!( diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index a4d4d1a0fb..0903719301 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); @@ -1096,7 +1097,7 @@ mod tests { node_info: FileNodeInfo { dev: u64::MAX, ino: u64::MAX, - rdev: Some(u64::MAX), + rdev: Some(NonZeroU64::MAX), }, block_size: u64::MAX, })), @@ -1111,7 +1112,7 @@ mod tests { node_info: FileNodeInfo { dev: u64::MAX, ino: u64::MAX, - rdev: Some(u64::MAX), + rdev: Some(NonZeroU64::MAX), }, block_size: u64::MAX, })), @@ -1332,6 +1333,37 @@ mod tests { assert_eq!(decode_request(&invalid_user), Err(WireError::InvalidTag)); } + #[test] + fn decode_rejects_zero_device_numbers_in_fs_status_responses() { + let status = FileStatus { + file_type: FileType::CharacterDevice, + mode: FileMode::from_bits(0o640).unwrap(), + size: 17, + owner: FileUser::ROOT, + node_info: FileNodeInfo { + dev: 5, + ino: 7, + rdev: Some(NonZeroU64::MAX), + }, + block_size: 4096, + }; + for result in [ + FileResponse::PathStatus(status), + FileResponse::HandleStatus(status), + ] { + let response = BrokerResponse { + request_id: TEST_REQUEST_ID, + result: BrokerResult::File(result), + }; + let mut bytes = encode_response(response.clone()); + assert_eq!(decode_response(&bytes), Ok(response)); + // The optional device number precedes the final block-size field. + let rdev_start = bytes.len() - 2 * size_of::(); + bytes[rdev_start..rdev_start + size_of::()].fill(0); + assert_eq!(decode_response(&bytes), Err(WireError::InvalidTag)); + } + } + #[test] fn decode_rejects_malformed_fs_response_frames() { let status = BrokerResponse { diff --git a/litebox_broker_protocol/src/wire/fs.rs b/litebox_broker_protocol/src/wire/fs.rs index b9ce584029..345f9eec40 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, @@ -440,7 +442,7 @@ 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); + encode_optional_u64(encoder, status.node_info.rdev.map(NonZeroU64::get)); encoder.u64(status.block_size); } @@ -451,7 +453,9 @@ 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 rdev = decode_optional_u64(decoder)? + .map(|value| NonZeroU64::new(value).ok_or(WireError::InvalidTag)) + .transpose()?; let block_size = decoder.u64()?; Ok(FileStatus { file_type, diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 8c626bf227..13df35d32e 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -13,7 +13,7 @@ use litebox::{ fs::OFlags, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; -use litebox_broker_protocol::fs::{FileMode, FileType, FileUser}; +use litebox_broker_protocol::fs::{FileMode, FileNodeInfo, FileStatus, FileType, FileUser}; use syscalls::Sysno; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -369,42 +369,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: FileUser { user, group }, - node_info: litebox::fs::NodeInfo { dev, ino, rdev }, - blksize, + node_info: FileNodeInfo { dev, ino, rdev }, + 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: (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() - } + }) } } @@ -510,35 +511,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: FileUser { user, group }, - node_info: litebox::fs::NodeInfo { dev, ino, rdev }, - blksize, + node_info: FileNodeInfo { dev, ino, rdev }, + 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: (u32::from(mode.bits()) | InodeType::from(file_type) as u32).trunc(), - stx_ino: ino as u64, - stx_size: size as u64, + 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() - } + }) } } @@ -3475,3 +3477,75 @@ impl ReinterpretUsizeAsPtr> for Option FileStatus { + FileStatus { + file_type: FileType::CharacterDevice, + mode: FileMode::RUSR | FileMode::WUSR, + size: u64::from(u32::MAX) + 1, + owner: FileUser { + user: 12, + group: 34, + }, + node_info: FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX, + rdev: NonZeroU64::new(u64::MAX), + }, + block_size: 4096, + } + } + + #[test] + fn stat_preserves_full_width_identity_and_representable_sizes() { + let status = status(); + let stat = FileStat::try_from(status).unwrap(); + assert_eq!({ stat.st_dev }, status.node_info.dev); + assert_eq!({ stat.st_ino }, status.node_info.ino); + assert_eq!({ stat.st_rdev }, u64::MAX); + assert_eq!(u64::try_from(stat.st_size).unwrap(), status.size); + assert_eq!({ stat.st_blksize }, 4096); + assert_eq!({ stat.st_uid }, 12); + assert_eq!({ stat.st_gid }, 34); + } + + #[test] + fn stat_rejects_sizes_outside_the_signed_linux_abi() { + let mut status = status(); + status.size = u64::MAX; + assert_eq!(FileStat::try_from(status), Err(errno::Errno::EOVERFLOW)); + status.size = 0; + #[cfg(target_arch = "x86_64")] + { + status.block_size = u64::MAX; + } + #[cfg(target_arch = "aarch64")] + { + status.block_size = u64::from(i32::MAX.cast_unsigned()) + 1; + } + assert_eq!(FileStat::try_from(status), Err(errno::Errno::EOVERFLOW)); + } + + #[test] + fn statx_preserves_u64_sizes_and_checks_block_size_narrowing() { + let mut status = status(); + status.size = u64::MAX; + let stat = Statx::try_from(status).unwrap(); + assert_eq!(stat.stx_size, u64::MAX); + assert_eq!(stat.stx_ino, u64::MAX); + assert_eq!(stat.stx_dev_major, dev_major(u64::MAX)); + assert_eq!(stat.stx_dev_minor, dev_minor(u64::MAX)); + assert_eq!(stat.stx_rdev_major, dev_major(u64::MAX)); + assert_eq!(stat.stx_rdev_minor, dev_minor(u64::MAX)); + status.block_size = u64::from(u32::MAX) + 1; + assert_eq!( + Statx::try_from(status).unwrap_err(), + errno::Errno::EOVERFLOW + ); + } +} diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 11338e52f7..e79b56b603 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -7,7 +7,7 @@ use alloc::{ffi::CString, vec::Vec}; use litebox::{ fs::OFlags, mm::linux::{CreatePagesFlags, MappingError, PAGE_SIZE}, - utils::{ReinterpretSignedExt, TruncateExt}, + utils::ReinterpretSignedExt, }; use litebox_broker_protocol::fs::FileMode as Mode; use litebox_common_linux::{MapFlags, errno::Errno, loader::ElfParsedFile}; @@ -56,28 +56,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) } } @@ -149,7 +144,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(()) } @@ -390,6 +385,26 @@ mod tests { use super::*; + #[test] + fn elf_file_size_retains_protocol_width() { + use crate::syscalls::test_broker::{Scripted, closed, opened, status}; + use crate::syscalls::tests::{FILE_HANDLE, scripted_task}; + use litebox_broker_protocol::fs::{FileStatus, FileType}; + use litebox_broker_protocol::message::FileResponse; + use litebox_common_linux::loader::ReadAt as _; + + let (files, task) = scripted_task([opened(FILE_HANDLE)]); + let file = ElfFile::new(&task, "/large").unwrap(); + files.script([ + Scripted::Reply(FileResponse::HandleStatus(FileStatus { + size: u64::MAX, + ..status(FileType::RegularFile, 0o644) + })), + closed(), + ]); + assert_eq!((&file).size().unwrap(), u64::MAX); + } + #[test] fn interpreter_reservation_is_top_down_above_low_heap() { let task = crate::syscalls::tests::init_platform(); diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 84d7f4e66b..6029dee62a 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -18,7 +18,7 @@ use litebox::{ utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; use litebox_broker_protocol::fs::{ - FileMode as Mode, FileSeekWhence as SeekWhence, FileType, FileUser, + FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, FileType, FileUser, }; use litebox_common_linux::{ AccessFlags, AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, @@ -1449,7 +1449,14 @@ 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 = files.fs.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, @@ -1538,9 +1545,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(files.fs.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 { @@ -1568,7 +1582,7 @@ impl Task { let rw_user_mode = u32::from((Mode::RUSR | Mode::WUSR).bits()); let files = self.files.borrow(); fd.dispatch( - |fd| files.fs.file_status(fd).map(T::from).map_err(Errno::from), + |fd| T::try_from(files.fs.file_status(fd)?), |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { Ok(T::from(synthetic( @@ -1585,7 +1599,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, @@ -1603,7 +1617,7 @@ impl Task { let context = fs.context.read(); files.fs.path_file_status(&context, path)? }; - Ok(T::from(status)) + T::try_from(status) } /// Handle syscall `stat` @@ -1634,7 +1648,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)?; @@ -1649,7 +1663,7 @@ impl Task { let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - Ok(T::from(files.fs.path_file_status(&context, cwd)?)) + T::try_from(files.fs.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)) @@ -2757,9 +2771,12 @@ 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 + .node_info + .as_ref() + .map_or(0, |node_info| node_info.ino), off: dir_off as u64, - len: len.trunc(), + len: u16::try_from(len).map_err(|_| Errno::EOVERFLOW)?, typ: litebox_common_linux::DirentType::from(entry.file_type) as u8, __name: [0; 0], }; @@ -2801,6 +2818,63 @@ mod tests { extern crate std; + #[test] + fn stat_overflow_does_not_narrow_internal_metadata_or_statx() { + use crate::syscalls::test_broker::{Scripted, closed, opened, status}; + use crate::syscalls::tests::{FILE_HANDLE, scripted_task}; + use litebox_broker_protocol::message::FileResponse; + + let metadata = FileStatus { + size: u64::MAX, + ..status(FileType::RegularFile, 0o644) + }; + let (files, task) = scripted_task([opened(FILE_HANDLE)]); + let fd = i32::try_from( + task.sys_open("/large", OFlags::RDONLY, Mode::empty()) + .unwrap(), + ) + .unwrap(); + files.script([ + Scripted::Reply(FileResponse::HandleStatus(metadata)), + Scripted::Reply(FileResponse::HandleStatus(metadata)), + Scripted::Reply(FileResponse::HandleStatus(metadata)), + Scripted::Reply(FileResponse::HandleStatus(metadata)), + Scripted::Reply(FileResponse::PathStatus(metadata)), + Scripted::Reply(FileResponse::PathStatus(metadata)), + Scripted::Reply(FileResponse::PathStatus(metadata)), + closed(), + ]); + assert_eq!(task.file_status(fd).unwrap(), metadata); + assert_eq!(task.sys_fstat(fd), Err(Errno::EOVERFLOW)); + assert_eq!( + task.sys_statx(fd, "", AtFlags::AT_EMPTY_PATH, StatxMask::STATX_SIZE) + .unwrap() + .stx_size, + u64::MAX + ); + assert_eq!( + task.sys_faccessat(fd, "", AccessFlags::F_OK, AtFlags::AT_EMPTY_PATH), + Ok(()) + ); + assert_eq!(task.sys_stat("/large"), Err(Errno::EOVERFLOW)); + assert_eq!( + task.sys_newfstatat(litebox_common_linux::AT_FDCWD, "/large", AtFlags::empty()), + Err(Errno::EOVERFLOW) + ); + assert_eq!( + task.sys_statx( + litebox_common_linux::AT_FDCWD, + "/large", + AtFlags::empty(), + StatxMask::STATX_SIZE, + ) + .unwrap() + .stx_size, + u64::MAX + ); + task.sys_close(fd).unwrap(); + } + #[test] fn write_to_iovec_returns_partial_after_later_error() { let first = b"first"; diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 34e2c02037..e974109df1 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -24,8 +24,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; @@ -807,8 +805,8 @@ 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()); @@ -970,22 +968,18 @@ impl Task { /// 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), } diff --git a/litebox_shim_linux/src/syscalls/test_broker.rs b/litebox_shim_linux/src/syscalls/test_broker.rs index 91d4db0feb..1c9629f1dd 100644 --- a/litebox_shim_linux/src/syscalls/test_broker.rs +++ b/litebox_shim_linux/src/syscalls/test_broker.rs @@ -20,6 +20,7 @@ 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; @@ -772,7 +773,7 @@ impl ScriptedFiles { "standard streams must be queried as the bootstrap user" ); let mut status = status(FileType::CharacterDevice, 0o620); - status.node_info.rdev = Some(PTS_RDEV | minor); + status.node_info.rdev = NonZeroU64::new(PTS_RDEV | minor); return FileResponse::PathStatus(status); } self.record(FileCall::PathStatus { @@ -784,7 +785,7 @@ impl ScriptedFiles { 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 = Some(rdev); + status.node_info.rdev = NonZeroU64::new(rdev); return FileResponse::HandleStatus(status); } self.record(FileCall::HandleStatus(request.handle)); diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index d8787a5c5d..2d71e1aa66 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -381,7 +381,7 @@ 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("file.txt", FileType::RegularFile, u64::MAX), directory_entry("sub", FileType::Directory, 9), ]; let (files, task) = scripted_task([]); @@ -409,7 +409,7 @@ fn getdirent64_encodes_the_entries_the_broker_returns() { vec![ (".".into(), DirentType::Directory as u8, 1, 0), ("..".into(), DirentType::Directory as u8, 1, 1), - ("file.txt".into(), DirentType::Regular as u8, 7, 2), + ("file.txt".into(), DirentType::Regular as u8, u64::MAX, 2), ("sub".into(), DirentType::Directory as u8, 9, 3), ], "entries are reported sorted by name, with their broker type and inode" diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index eb8e323f8b..221b69b1c8 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -959,7 +959,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, @@ -978,9 +978,7 @@ pub(crate) fn image_section_metadata( .fs .file_status(&file.fd) .map_err(PeImageAccessError::FileStatus)? - .size - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow)?; + .size; Ok(ImageSectionMetadata { transfer_address: parsed .image_base() @@ -1163,11 +1161,7 @@ impl ReadAt for &'_ PeImageFile { } fn size(&mut self) -> Result { - self.fs - .file_status(&self.fd)? - .size - .try_into() - .map_err(|_| PeImageAccessError::AddressOverflow) + Ok(self.fs.file_status(&self.fd)?.size) } } diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 40e25e3ccc..70998d78ce 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -9,14 +9,16 @@ use core::mem::{align_of, offset_of, size_of}; use int_enum::IntEnum; use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; +use litebox::fs::OFlags; use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, SeekError, WriteError, }; -use litebox::fs::{FileStatus, OFlags}; use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; -use litebox_broker_protocol::fs::{FileMode as Mode, FileSeekWhence as SeekWhence, FileType}; +use litebox_broker_protocol::fs::{ + FileMode as Mode, 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}; @@ -262,7 +264,7 @@ struct FileStatusMetadata { end_of_file: i64, allocation_size: i64, file_attributes: FileAttributes, - file_id: Option, + file_id: u64, file_id_128: [u8; 16], } @@ -279,14 +281,12 @@ impl FileStatusMetadata { let end_of_file = i64::try_from(status.size).unwrap_or(i64::MAX); let allocation_size = status .size - .checked_next_multiple_of(status.blksize.max(1)) + .checked_next_multiple_of(status.block_size.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, @@ -305,9 +305,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()), } } } @@ -1099,9 +1097,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(), @@ -1676,7 +1672,7 @@ impl Task { let status = file .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 => { @@ -1695,7 +1691,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() { @@ -2811,6 +2811,41 @@ mod tests { const FILE_CREATE: u32 = 2; const FILE_OVERWRITE: u32 = 4; + #[test] + fn status_metadata_preserves_inode_bits_and_saturates_signed_lengths() { + let status = FileStatus { + file_type: FileType::RegularFile, + mode: Mode::RUSR | Mode::WUSR, + size: u64::MAX, + owner: litebox_broker_protocol::fs::FileUser::ROOT, + node_info: litebox_broker_protocol::fs::FileNodeInfo { + dev: u64::MAX, + ino: u64::MAX, + rdev: None, + }, + block_size: 4096, + }; + let metadata = FileStatusMetadata::from_status(&status); + assert_eq!(metadata.end_of_file, i64::MAX); + assert_eq!(metadata.allocation_size, i64::MAX); + assert_eq!(metadata.file_id, u64::MAX); + assert_eq!(&metadata.file_id_128[..8], &u64::MAX.to_ne_bytes()); + assert_eq!(&metadata.file_id_128[8..], &[0; 8]); + assert_eq!( + DirectoryEntry::from_status(String::from("large"), &status).file_id, + -1 + ); + + let status = FileStatus { + size: 1, + block_size: u64::from(u32::MAX) + 1, + ..status + }; + let metadata = FileStatusMetadata::from_status(&status); + assert_eq!(metadata.end_of_file, 1); + assert_eq!(metadata.allocation_size, i64::from(u32::MAX) + 1); + } + fn open_object_attributes( path: &str, ) -> ( diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs index ecb938b7fd..43b83667d5 100644 --- a/litebox_shim_windows/src/syscalls/nls.rs +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -798,10 +798,11 @@ impl Task { 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( diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 02bfade1e8..f1b697c503 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -544,7 +544,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 @@ -660,10 +660,15 @@ impl RegistryStore { 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_file( @@ -673,7 +678,6 @@ impl RegistryStore { Mode::empty(), ) .map_err(map_open_error)?; - let mut data = vec![0; status.size]; let result = read_exact_at(self.fs(), &fd, &mut data); let _ = self.fs().close_file(&fd); result?; @@ -813,12 +817,12 @@ impl RegistryStore { .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) } @@ -1743,7 +1747,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::( @@ -1791,7 +1796,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], }; @@ -1961,7 +1967,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::( @@ -2796,7 +2803,7 @@ mod tests { .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)); diff --git a/litebox_shim_windows/src/syscalls/section.rs b/litebox_shim_windows/src/syscalls/section.rs index 512a98dbba..88be54b7e7 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, @@ -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 }, @@ -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 = From 13ec1b9db498f6faa74f617ff1e07d0236222d35 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 11:12:52 -0700 Subject: [PATCH 12/34] Use BrokerFile directly for file descriptors Remove the separate File marker and make FileFd non-generic. Update Linux and Windows consumers while preserving Arc ownership and descriptor lifetime behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/file.rs | 34 ++++++++-------- litebox/src/fs/mod.rs | 2 +- litebox/src/fs/tests.rs | 31 ++++++++++++++- litebox_shim_linux/src/lib.rs | 4 +- litebox_shim_linux/src/syscalls/epoll.rs | 6 +-- litebox_shim_linux/src/syscalls/file.rs | 24 +++++------- litebox_shim_linux/src/syscalls/unix.rs | 2 +- litebox_shim_windows/src/lib.rs | 8 ++-- litebox_shim_windows/src/loader/pe.rs | 2 +- litebox_shim_windows/src/syscalls/file.rs | 39 +++++++++---------- litebox_shim_windows/src/syscalls/nls.rs | 8 ++-- litebox_shim_windows/src/syscalls/registry.rs | 30 +++++++------- 12 files changed, 104 insertions(+), 86 deletions(-) diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index 7f87c8bd15..991c9c0ee5 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -111,11 +111,8 @@ bitflags! { } } -/// Type marker for file descriptors backed by broker-owned files. -pub struct File(core::marker::PhantomData); - impl LiteBox { - fn broker_file(&self, fd: &FileFd) -> Option> { + fn broker_file(&self, fd: &FileFd) -> Option> { self.descriptor_table() .with_entry(fd, |entry| Arc::clone(&entry.entry)) } @@ -133,7 +130,7 @@ impl LiteBox { path: impl Arg, flags: OFlags, mode: Mode, - ) -> Result, OpenError> { + ) -> Result { let path = Self::broker_path(context, path)?; let (access, flags) = file_open_options(flags)?; let broker = self.broker_control().ok_or(OpenError::Io)?; @@ -155,7 +152,7 @@ impl LiteBox { /// 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> { + pub fn close_file(&self, fd: &FileFd) -> Result<(), CloseError> { let mut descriptors = self.descriptor_table_mut(); let removed = descriptors.remove(fd); drop(descriptors); @@ -166,7 +163,7 @@ impl LiteBox { /// Read from a file descriptor at `offset` into a buffer. pub fn read_file( &self, - fd: &FileFd, + fd: &FileFd, buf: &mut [u8], offset: Option, ) -> Result { @@ -187,7 +184,7 @@ impl LiteBox { /// Write from a buffer to a file descriptor at `offset`. pub fn write_file( &self, - fd: &FileFd, + fd: &FileFd, buf: &[u8], offset: Option, ) -> Result { @@ -208,7 +205,7 @@ impl LiteBox { /// Reposition the read/write file offset. pub fn seek_file( &self, - fd: &FileFd, + fd: &FileFd, offset: isize, whence: SeekWhence, ) -> Result { @@ -225,7 +222,7 @@ impl LiteBox { /// Truncate the file to the specified length. pub fn truncate_file( &self, - fd: &FileFd, + fd: &FileFd, length: usize, reset_offset: bool, ) -> Result<(), TruncateError> { @@ -309,7 +306,7 @@ impl LiteBox { /// Read directory entries from a directory file descriptor. pub fn read_file_directory( &self, - fd: &FileFd, + fd: &FileFd, ) -> Result, ReadDirError> { let file = self.broker_file(fd).ok_or(ReadDirError::ClosedFd)?; file.broker @@ -333,7 +330,7 @@ impl LiteBox { } /// Equivalent to [`Self::path_file_status`], but on an open `fd`. - pub fn file_status(&self, fd: &FileFd) -> Result { + 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) @@ -344,7 +341,7 @@ impl LiteBox { } /// Get static backing data for a file, if available and supported. - pub fn get_static_file_backing_data(&self, fd: &FileFd) -> Option<&'static [u8]> { + pub fn get_static_file_backing_data(&self, fd: &FileFd) -> Option<&'static [u8]> { let _ = self.broker_file(fd)?; None } @@ -436,7 +433,11 @@ impl core::fmt::Display for ResolvedPath { } } -struct BrokerFile { +/// 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, } @@ -641,8 +642,7 @@ fn file_status_error(error: FileError) -> FileStatusError { } crate::fd::enable_fds_for_subsystem! { - @ Platform: { sync::RawSyncPrimitivesProvider }; - File; + BrokerFile; Arc; - -> FileFd; + -> FileFd; } diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index 1bb78a6574..d3e6817b5a 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -10,7 +10,7 @@ pub mod errors; mod file; -pub use file::{Context, File, FileFd, OFlags, ResolvedPath}; +pub use file::{BrokerFile, Context, FileFd, OFlags, ResolvedPath}; #[doc(hidden)] pub mod nine_p { diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 9aa9ad86a6..40a15127e1 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -41,7 +41,7 @@ use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory, Sh use crate::fs::errors::{ OpenError, PathError, ReadDirError, ReadError, RmdirError, UnlinkError, WriteError, }; -use crate::fs::{Context, OFlags}; +use crate::fs::{BrokerFile, Context, FileFd, OFlags}; use crate::platform::mock::MockPlatform; /// The handle the scripted broker hands out for every successful open. @@ -541,6 +541,35 @@ fn closing_releases_the_broker_object_and_the_descriptor() { assert_eq!(broker.calls().len(), 2); } +#[test] +fn broker_file_descriptors_preserve_subsystem_and_duplicate_lifetimes() { + let (broker, fs) = scripted_fs([opened(), Scripted::Read(b"broker".to_vec())]); + let fd: FileFd = fs + .open_file(&Context::new(), "/file", OFlags::RDONLY, FileMode::empty()) + .unwrap(); + let duplicate = fs.descriptor_table_mut().duplicate(&fd).unwrap(); + let mut raw_fds = crate::fd::RawDescriptorStorage::new(); + let raw_fd = raw_fds.fd_into_raw_integer(fd); + + assert!(matches!( + raw_fds.fd_from_raw_integer::>(raw_fd), + Err(crate::fd::ErrRawIntFd::InvalidSubsystem) + )); + let fd = raw_fds + .fd_consume_raw_integer::(raw_fd) + .unwrap(); + fs.close_file(&fd).unwrap(); + assert_eq!(broker.calls().len(), 1); + + let mut buffer = [0; 6]; + assert_eq!(fs.read_file(&duplicate, &mut buffer, None).unwrap(), 6); + assert_eq!(&buffer, b"broker"); + fs.close_file(&duplicate).unwrap(); + let calls = broker.calls(); + assert_eq!(calls.len(), 3); + assert_eq!(calls.last(), Some(&Call::Close(FILE_HANDLE))); +} + #[test] fn in_flight_read_keeps_the_broker_file_alive_after_close() { let entered = Arc::new(Barrier::new(2)); diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 1bc92a97da..c0e53a6d3d 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -59,7 +59,7 @@ pub mod syscalls; pub mod transport; mod wait; -pub(crate) type FileFd = litebox::fs::FileFd; +pub(crate) use litebox::fs::FileFd; /// Aggregate bound capturing everything the shim requires of a platform. /// @@ -434,7 +434,7 @@ impl syscalls::file::FilesState { }; } - resolve_fd!(litebox::fs::File, Fs); + resolve_fd!(litebox::fs::BrokerFile, Fs); resolve_fd!(Network, Network); resolve_fd!(Pipes, Pipes); resolve_fd!(syscalls::eventfd::EventfdSubsystem, Eventfd); diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 8783581355..9a640ccf61 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -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>>), diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 6029dee62a..3490b1c3ff 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -176,7 +176,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>>), @@ -214,7 +214,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, @@ -222,14 +222,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, @@ -367,7 +367,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. @@ -413,12 +413,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 @@ -905,7 +905,7 @@ impl Task { } 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) { @@ -928,7 +928,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) => { @@ -2067,11 +2067,7 @@ impl Task { } } - fn is_stdio( - &self, - fs: &litebox::LiteBox, - fd: &FileFd, - ) -> Result { + 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 diff --git a/litebox_shim_linux/src/syscalls/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index 2187908fce..31338ad012 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -71,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, litebox::LiteBox)), + Path((String, FileFd, litebox::LiteBox)), Abstract(Vec), } diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 2d98d9929b..85c48a0cb0 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -2994,9 +2994,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); @@ -3033,11 +3033,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); } diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 221b69b1c8..4b0f296bd4 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -1107,7 +1107,7 @@ fn is_missing_file_error(error: &WindowsLoadError) -> bool { struct PeImageFile { fs: Arc>, - fd: litebox::fs::FileFd, + fd: litebox::fs::FileFd, } impl PeImageFile { diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 70998d78ce..ac8ef3bd8c 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -450,10 +450,10 @@ struct FileStandardInformation { pub(crate) struct FileObjectSubsystem(PhantomData); impl FdEnabledSubsystem for FileObjectSubsystem { - type Entry = FileObject; + type Entry = FileObject; } -impl FdEnabledSubsystemEntry for FileObject {} +impl FdEnabledSubsystemEntry for FileObject {} impl crate::WindowsHandleSubsystem for FileObjectSubsystem @@ -479,9 +479,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, @@ -496,15 +496,15 @@ struct DirectoryQueryState { entries: Vec, } -enum FileObjectBacking { +enum FileObjectBacking { Filesystem { - fd: litebox::fs::FileFd, + fd: litebox::fs::FileFd, is_directory: bool, }, CondrvStream { object: CondrvObject, stream_object: Arc, - fd: litebox::fs::FileFd, + fd: litebox::fs::FileFd, }, CondrvControl(CondrvObject), /// A handle to `\Device\KsecDD`. @@ -541,7 +541,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), @@ -549,7 +549,7 @@ impl FileSharingIdentity<'_> { } } -impl FileObject { +impl FileObject { fn condrv_object(&self) -> Option { match self.backing { FileObjectBacking::CondrvStream { object, .. } @@ -876,7 +876,7 @@ 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.close_file(file); @@ -889,7 +889,7 @@ impl Task { }); } - 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_file(&fd); @@ -1877,7 +1877,7 @@ impl Task { fn query_directory( &self, - file: &mut FileObject, + file: &mut FileObject, supplied_pattern: Option, information_class: FileInformationClass, flags: DirectoryQueryFlags, @@ -1966,10 +1966,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); }; @@ -2205,7 +2202,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, @@ -2255,7 +2252,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 { @@ -2327,7 +2324,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); } @@ -2351,7 +2348,7 @@ impl Task { create_disposition: CreateDisposition, create_options: FileCreateOptions, mode: Mode, - ) -> Result<(litebox::fs::FileFd, bool, FileCreateInformation), NtStatus> { + ) -> 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 @@ -2393,7 +2390,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 diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs index 43b83667d5..bc62d18320 100644 --- a/litebox_shim_windows/src/syscalls/nls.rs +++ b/litebox_shim_windows/src/syscalls/nls.rs @@ -455,8 +455,8 @@ struct MappedNlsSection { len: usize, } -struct NlsSectionFile { - fd: litebox::fs::FileFd, +struct NlsSectionFile { + fd: litebox::fs::FileFd, len: usize, } @@ -770,7 +770,7 @@ 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 @@ -807,7 +807,7 @@ impl Task { fn copy_nls_section_file( &self, - fd: &litebox::fs::FileFd, + fd: &litebox::fs::FileFd, section_len: usize, output: MutPtr, ) -> Result { diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index f1b697c503..059b4f1152 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -64,10 +64,10 @@ type RegistryFileSystem = LiteBox; pub(crate) struct RegistryKeySubsystem(PhantomData); impl FdEnabledSubsystem for RegistryKeySubsystem { - type Entry = RegistryKeyObject; + type Entry = RegistryKeyObject; } -impl FdEnabledSubsystemEntry for RegistryKeyObject {} +impl FdEnabledSubsystemEntry for RegistryKeyObject {} impl crate::WindowsHandleSubsystem for RegistryKeySubsystem @@ -77,9 +77,9 @@ impl crate::WindowsHandleSubsystem } } -pub(crate) struct RegistryKeyObject { +pub(crate) struct RegistryKeyObject { path: String, - fd: litebox::fs::FileFd, + fd: litebox::fs::FileFd, } pub(crate) struct RegistryStore { @@ -641,7 +641,7 @@ impl RegistryStore { &self, path: &str, desired_access: RegistryKeyAccess, - ) -> Result, NtStatus> { + ) -> Result { self.fs() .open_file(&self.fs_context, path, desired_access.into(), Mode::empty()) .map_err(map_open_error) @@ -769,7 +769,7 @@ 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() @@ -835,7 +835,7 @@ 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(); @@ -857,11 +857,7 @@ 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() @@ -889,7 +885,7 @@ 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); @@ -1102,7 +1098,7 @@ impl Task { fn insert_registry_key_handle( &self, - key: RegistryKeyObject, + key: RegistryKeyObject, granted_access: RegistryKeyAccess, ) -> Result { self.insert_typed_handle::>( @@ -1120,7 +1116,7 @@ impl Task { }); } - pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { + pub(crate) fn close_registry_key(&self, key: RegistryKeyObject) { let _ = self.global.registry.fs().close_file(&key.fd); } @@ -2295,7 +2291,7 @@ fn write_value_at_path( fn read_exact_at( fs: &RegistryFileSystem, - fd: &litebox::fs::FileFd, + fd: &litebox::fs::FileFd, mut data: &mut [u8], ) -> Result<(), NtStatus> { let mut offset = 0; @@ -2314,7 +2310,7 @@ fn read_exact_at( fn write_all_at( fs: &RegistryFileSystem, - fd: &litebox::fs::FileFd, + fd: &litebox::fs::FileFd, mut data: &[u8], mut offset: usize, ) -> Result<(), NtStatus> { From 4d260ef2fe06bdb6de857a83e2c8f3830e7d881a Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 15:44:04 -0700 Subject: [PATCH 13/34] Share protocol open options and normalized paths Keep raw Linux open flags at the ABI boundary and use protocol access modes and flags directly throughout LiteBox and broker backends. Consolidate lexical path normalization in the protocol and remove duplicate definitions and intermediate conversions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/file.rs | 179 +--- litebox/src/fs/mod.rs | 2 +- litebox/src/fs/tests.rs | 92 +- litebox/src/pipes.rs | 85 +- litebox_broker_core/src/fs/backend.rs | 12 +- litebox_broker_core/src/fs/composer.rs | 14 +- litebox_broker_core/src/fs/devices.rs | 23 +- litebox_broker_core/src/fs/in_mem.rs | 56 +- litebox_broker_core/src/fs/mod.rs | 90 -- litebox_broker_core/src/fs/nine_p/mod.rs | 98 +- .../src/fs/nine_p/open_flags_tests.rs | 46 + litebox_broker_core/src/fs/nine_p/tests.rs | 185 +++- litebox_broker_core/src/fs/overlay.rs | 58 +- litebox_broker_core/src/fs/resolver.rs | 168 ++-- litebox_broker_core/src/fs/service.rs | 72 +- litebox_broker_core/src/fs/tar_ro.rs | 30 +- litebox_broker_core/src/fs/test_support.rs | 10 +- litebox_broker_core/src/fs/tests.rs | 941 +++++++++++++++--- litebox_broker_protocol/src/fs.rs | 146 ++- litebox_broker_protocol/src/fs/path.rs | 114 +++ litebox_common_linux/src/lib.rs | 214 +++- litebox_platform_linux_userland/src/lib.rs | 5 +- litebox_shim_linux/src/lib.rs | 33 +- litebox_shim_linux/src/loader/elf.rs | 3 +- litebox_shim_linux/src/stdio.rs | 5 +- litebox_shim_linux/src/syscalls/epoll.rs | 6 +- litebox_shim_linux/src/syscalls/eventfd.rs | 3 +- litebox_shim_linux/src/syscalls/file.rs | 136 ++- litebox_shim_linux/src/syscalls/mm.rs | 4 +- litebox_shim_linux/src/syscalls/mod.rs | 12 +- litebox_shim_linux/src/syscalls/net.rs | 7 +- litebox_shim_linux/src/syscalls/pipe.rs | 3 +- litebox_shim_linux/src/syscalls/process.rs | 2 +- litebox_shim_linux/src/syscalls/tests.rs | 82 +- litebox_shim_linux/src/syscalls/unix.rs | 11 +- litebox_shim_windows/src/loader/pe.rs | 6 +- litebox_shim_windows/src/syscalls/file.rs | 161 ++- litebox_shim_windows/src/syscalls/nls.rs | 6 +- litebox_shim_windows/src/syscalls/registry.rs | 86 +- 39 files changed, 2283 insertions(+), 923 deletions(-) create mode 100644 litebox_broker_core/src/fs/nine_p/open_flags_tests.rs create mode 100644 litebox_broker_protocol/src/fs/path.rs diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index 991c9c0ee5..ceacd3591a 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -5,16 +5,13 @@ use alloc::string::{String, ToString}; use alloc::sync::Arc; -use alloc::vec; use alloc::vec::Vec; -use bitflags::bitflags; -use core::ffi::c_uint; 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, + FileSeekWhence as SeekWhence, FileStatus, FileUser as UserInfo, ResolvedPath, }; use crate::path::Arg; @@ -25,92 +22,6 @@ use super::errors::{ ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; -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 LiteBox { fn broker_file(&self, fd: &FileFd) -> Option> { self.descriptor_table() @@ -123,16 +34,20 @@ impl LiteBox { /// 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, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, mode: Mode, ) -> Result { let path = Self::broker_path(context, path)?; - let (access, flags) = file_open_options(flags)?; + 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( @@ -381,7 +296,7 @@ impl Context { #[must_use] pub fn new() -> Self { Self { - cwd: Arc::new(ResolvedPath { components: vec![] }), + cwd: Arc::new(ResolvedPath::root()), user_info: UserInfo { user: 1000, group: 1000, @@ -391,21 +306,7 @@ impl Context { /// 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 }) + Ok(self.cwd.resolve(path.as_rust_str()?)) } } @@ -415,24 +316,6 @@ impl Default for Context { } } -/// 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 @@ -448,50 +331,6 @@ impl Drop for BrokerFile { } } -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)) -} - fn broker_fd_error(error: crate::broker::error::BrokerControlError, closed: T, io: T) -> T { match error { crate::broker::error::BrokerControlError::Broker( diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index d3e6817b5a..fafe2318f0 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -10,7 +10,7 @@ pub mod errors; mod file; -pub use file::{BrokerFile, Context, FileFd, OFlags, ResolvedPath}; +pub use file::{BrokerFile, Context, FileFd}; #[doc(hidden)] pub mod nine_p { diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs index 40a15127e1..c90a565519 100644 --- a/litebox/src/fs/tests.rs +++ b/litebox/src/fs/tests.rs @@ -41,7 +41,7 @@ use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory, Sh use crate::fs::errors::{ OpenError, PathError, ReadDirError, ReadError, RmdirError, UnlinkError, WriteError, }; -use crate::fs::{BrokerFile, Context, FileFd, OFlags}; +use crate::fs::{BrokerFile, Context, FileFd}; use crate::platform::mock::MockPlatform; /// The handle the scripted broker hands out for every successful open. @@ -433,7 +433,7 @@ fn context_resolves_paths_against_the_cwd() { } #[test] -fn open_sends_the_resolved_path_and_translated_flags() { +fn open_sends_the_resolved_path_and_protocol_options() { let mut context = Context::new(); context.set_cwd(context.resolve("/work").unwrap()); context.set_acting_user(FileUser { user: 7, group: 9 }); @@ -443,7 +443,8 @@ fn open_sends_the_resolved_path_and_translated_flags() { .open_file( &context, "sub/../file.txt", - OFlags::CREAT | OFlags::WRONLY | OFlags::APPEND, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::APPEND, FileMode::RWXU, ) .expect("open should succeed"); @@ -461,6 +462,22 @@ fn open_sends_the_resolved_path_and_translated_flags() { fs.close_file(&fd).expect("close should succeed"); } +#[test] +fn open_rejects_unknown_protocol_flags_before_the_broker() { + let (broker, fs) = scripted_fs([]); + assert!(matches!( + fs.open_file( + &Context::new(), + "/file", + FileAccessMode::ReadOnly, + FileOpenFlags::from_bits_retain(1 << 15), + FileMode::empty(), + ), + Err(OpenError::AccessNotAllowed) + )); + assert!(broker.calls().is_empty()); +} + #[test] fn read_and_write_transfer_payloads_through_the_broker() { let context = Context::new(); @@ -472,7 +489,13 @@ fn read_and_write_transfer_payloads_through_the_broker() { ]); let fd = fs - .open_file(&context, "/file", OFlags::RDWR, FileMode::empty()) + .open_file( + &context, + "/file", + FileAccessMode::ReadWrite, + FileOpenFlags::NONE, + FileMode::empty(), + ) .expect("open should succeed"); assert_eq!(fs.write_file(&fd, b"hello", None).unwrap(), 5); @@ -522,7 +545,13 @@ fn closing_releases_the_broker_object_and_the_descriptor() { let (broker, fs) = scripted_fs([opened()]); let fd = fs - .open_file(&context, "/file", OFlags::RDONLY, FileMode::empty()) + .open_file( + &context, + "/file", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + FileMode::empty(), + ) .expect("open should succeed"); fs.close_file(&fd).expect("close should succeed"); assert_eq!(broker.calls()[1], Call::Close(FILE_HANDLE)); @@ -545,7 +574,13 @@ fn closing_releases_the_broker_object_and_the_descriptor() { fn broker_file_descriptors_preserve_subsystem_and_duplicate_lifetimes() { let (broker, fs) = scripted_fs([opened(), Scripted::Read(b"broker".to_vec())]); let fd: FileFd = fs - .open_file(&Context::new(), "/file", OFlags::RDONLY, FileMode::empty()) + .open_file( + &Context::new(), + "/file", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + FileMode::empty(), + ) .unwrap(); let duplicate = fs.descriptor_table_mut().duplicate(&fd).unwrap(); let mut raw_fds = crate::fd::RawDescriptorStorage::new(); @@ -583,8 +618,14 @@ fn in_flight_read_keeps_the_broker_file_alive_after_close() { }, ]); let fd = Arc::new( - fs.open_file(&Context::new(), "/file", OFlags::RDONLY, FileMode::empty()) - .expect("open should succeed"), + fs.open_file( + &Context::new(), + "/file", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + FileMode::empty(), + ) + .expect("open should succeed"), ); let worker = fs.clone(); @@ -639,7 +680,13 @@ fn path_and_handle_status_preserve_protocol_metadata() { assert_eq!(status, expected); let fd = fs - .open_file(&context, "/dev/null", OFlags::RDONLY, FileMode::empty()) + .open_file( + &context, + "/dev/null", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + FileMode::empty(), + ) .unwrap(); assert_eq!(fs.file_status(&fd).unwrap(), expected); fs.close_file(&fd).unwrap(); @@ -709,7 +756,8 @@ fn read_dir_reassembles_paged_broker_entries() { .open_file( &context, "/dir", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, FileMode::empty(), ) .expect("open should succeed"); @@ -751,11 +799,23 @@ fn broker_file_errors_map_to_guest_errors() { ]); assert!(matches!( - fs.open_file(&context, "/missing", OFlags::RDONLY, FileMode::empty()), + fs.open_file( + &context, + "/missing", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + FileMode::empty(), + ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) )); assert!(matches!( - fs.open_file(&context, "/secret", OFlags::RDONLY, FileMode::empty()), + fs.open_file( + &context, + "/secret", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + FileMode::empty(), + ), Err(OpenError::AccessNotAllowed) )); assert!(matches!( @@ -768,7 +828,13 @@ fn broker_file_errors_map_to_guest_errors() { )); let fd = fs - .open_file(&context, "/file", OFlags::WRONLY, FileMode::empty()) + .open_file( + &context, + "/file", + FileAccessMode::WriteOnly, + FileOpenFlags::NONE, + FileMode::empty(), + ) .expect("open should succeed"); let mut buffer = [0; 4]; assert!(matches!( diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index a9581abab9..673b3bbb88 100644 --- a/litebox/src/pipes.rs +++ b/litebox/src/pipes.rs @@ -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 & Flags::all(), atomic_slice_guarantee_size, )?; let mut dt = self.litebox.descriptor_table_mut(); @@ -160,7 +159,7 @@ impl Pipes { .ok_or(errors::ClosedError::ClosedFd)? .entry .0; - Ok(Flags::from_oflags_truncate(p.get_status())) + Ok(p.get_status()) } /// Update the flags set on the pipe at `fd`. @@ -178,7 +177,7 @@ impl Pipes { .ok_or(errors::ClosedError::ClosedFd)? .entry .0; - p.set_status(OFlags::from(mask), on); + p.set_status(mask & Flags::all(), on); Ok(()) } @@ -218,21 +217,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; @@ -367,7 +351,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 +379,7 @@ fn new_broker_pipe( pollee: Arc::new(Pollee::new()), peer: Weak::new(), endpoint_type: HalfPipeType::SenderHalf, - status: AtomicU32::new((flags | OFlags::WRONLY).bits()), + status: AtomicU32::new(flags.bits()), }); let reader = Arc::new_cyclic(|weak_reader| { Arc::get_mut(&mut writer) @@ -408,7 +392,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()), + status: AtomicU32::new(flags.bits()), } }); @@ -418,11 +402,11 @@ fn new_broker_pipe( } impl BrokerPipeEnd { - fn get_status(&self) -> OFlags { - OFlags::from_bits(self.status.load(Relaxed)).unwrap() & OFlags::STATUS_FLAGS_MASK + fn get_status(&self) -> Flags { + Flags::from_bits(self.status.load(Relaxed)).expect("pipe status contains only pipe flags") } - fn set_status(&self, mask: OFlags, on: bool) { + fn set_status(&self, mask: Flags, on: bool) { if on { self.status.fetch_or(mask.bits(), Relaxed); } else { @@ -442,7 +426,7 @@ impl BrokerPipeEnd self.pollee .wait( cx, - self.get_status().contains(OFlags::NONBLOCK), + self.get_status().contains(Flags::NON_BLOCKING), Events::IN, || { let data = self @@ -468,7 +452,7 @@ impl BrokerPipeEnd if buf.is_empty() { return Ok(0); } - let nonblock = self.get_status().contains(OFlags::NONBLOCK); + let nonblock = self.get_status().contains(Flags::NON_BLOCKING); if nonblock { let data = &buf[..buf.len().min(MAX_PIPE_TRANSFER_SIZE as usize)]; return self @@ -604,6 +588,53 @@ mod tests { extern crate std; + #[test] + fn pipe_flags_are_independent_for_each_endpoint() { + let platform = crate::platform::mock::MockPlatform::new(); + let (local, ()) = BrokerLocal::negotiate( + FailingPipeChannel { + request_count: Arc::new(AtomicUsize::new(0)), + read_failure: ReadFailure::WouldBlock, + force_transport: Arc::new(AtomicBool::new(false)), + }, + |channel| Ok((channel, Arc::new(NoopSharedMemory), ())), + ) + .unwrap(); + let litebox = crate::LiteBox::new_with_broker_local(platform, local); + let pipes = super::Pipes::new(&litebox); + let unknown = super::Flags::from_bits_retain(1 << 31); + let (writer, reader) = pipes + .create_pipe(2, super::Flags::NON_BLOCKING | unknown, None) + .unwrap(); + assert_eq!( + pipes.get_flags(&writer).unwrap(), + super::Flags::NON_BLOCKING + ); + assert_eq!( + pipes.get_flags(&reader).unwrap(), + super::Flags::NON_BLOCKING + ); + pipes + .update_flags(&writer, super::Flags::NON_BLOCKING, false) + .unwrap(); + assert!(pipes.get_flags(&writer).unwrap().is_empty()); + assert_eq!( + pipes.get_flags(&reader).unwrap(), + super::Flags::NON_BLOCKING + ); + pipes.update_flags(&writer, unknown, true).unwrap(); + assert!(pipes.get_flags(&writer).unwrap().is_empty()); + pipes + .update_flags(&writer, super::Flags::NON_BLOCKING, true) + .unwrap(); + assert_eq!( + pipes.get_flags(&writer).unwrap(), + super::Flags::NON_BLOCKING + ); + pipes.close(&writer).unwrap(); + pipes.close(&reader).unwrap(); + } + #[test] fn broker_control_failure_notifies_all_pipe_observers() { let platform = crate::platform::mock::MockPlatform::new(); diff --git a/litebox_broker_core/src/fs/backend.rs b/litebox_broker_core/src/fs/backend.rs index d12f5832bf..34ab751d2a 100644 --- a/litebox_broker_core/src/fs/backend.rs +++ b/litebox_broker_core/src/fs/backend.rs @@ -9,11 +9,11 @@ use core::any::{Any, TypeId}; use core::marker::PhantomData; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileStatus, FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileOpenFlags, FileStatus, + FileUser as UserInfo, }; use litebox_broker_protocol::stdio::StdioOutputStream; -use super::OFlags; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, @@ -114,11 +114,12 @@ pub trait Backend: private::Sealed + Send + Sync + Any { components: &[&str], ) -> Result>, WalkError>; - /// Take an owned handle to a `dir` found via a walk, validating any open `flags`. + /// Take an owned handle to a `dir` found via a walk, validating `access` and open `flags`. fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result; /// Obtain a walking handle to an existing owned dir. @@ -137,7 +138,8 @@ pub trait Backend: private::Sealed + Send + Sync + Any { &self, dir: WalkingDirHandle<'_>, name: &str, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result, OpenError>; /// Read directory entries at `dir`. diff --git a/litebox_broker_core/src/fs/composer.rs b/litebox_broker_core/src/fs/composer.rs index a6651d1dbe..ff9007db43 100644 --- a/litebox_broker_core/src/fs/composer.rs +++ b/litebox_broker_core/src/fs/composer.rs @@ -10,10 +10,10 @@ use alloc::vec; use alloc::vec::Vec; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, + FileType, FileUser as UserInfo, }; -use super::OFlags; use super::backend::{ Backend, BackendHandles, CreationMetadata, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, @@ -546,7 +546,8 @@ impl Backend for Composer { fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result { let dir = dir.into_typed::(); let inner = match dir.inner { @@ -562,7 +563,7 @@ impl Backend for Composer { mount_index, handle: self.mounts[mount_index] .backend - .owned_dir_at(handle, flags)?, + .owned_dir_at(handle, access, flags)?, }, }; Ok(DirHandle::from_typed::(ComposerDirHandle { inner })) @@ -598,7 +599,8 @@ impl Backend for Composer { &self, dir: WalkingDirHandle<'_>, name: &str, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result, OpenError> { let dir = dir.into_typed::(); match dir.inner { @@ -617,7 +619,7 @@ impl Backend for Composer { )?; self.mounts[mount_index] .backend - .open_file_at(handle, name, flags) + .open_file_at(handle, name, access, flags) .map(|file| Permissioned { item: FileHandle::from_typed::(ComposerFileHandle { mount_index, diff --git a/litebox_broker_core/src/fs/devices.rs b/litebox_broker_core/src/fs/devices.rs index 4bc2971e21..bbc3b2a543 100644 --- a/litebox_broker_core/src/fs/devices.rs +++ b/litebox_broker_core/src/fs/devices.rs @@ -9,12 +9,12 @@ use alloc::string::String; use alloc::vec; use alloc::vec::Vec; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, + FileType, FileUser as UserInfo, }; use litebox_broker_protocol::random::MAX_RANDOM_TRANSFER_SIZE; use litebox_broker_protocol::stdio::StdioOutputStream; -use super::OFlags; use super::backend::{ Backend, BackendHandles, CreationMetadata, DeviceIo, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, @@ -183,7 +183,8 @@ impl Backend for Devices { fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - _flags: OFlags, + _access: FileAccessMode, + _flags: FileOpenFlags, ) -> Result { Ok(DirHandle::from_typed::(dir.into_typed::())) } @@ -198,16 +199,17 @@ impl Backend for Devices { &self, dir: WalkingDirHandle<'_>, name: &str, - flags: OFlags, + _access: FileAccessMode, + flags: FileOpenFlags, ) -> Result, OpenError> { let _dir = dir.into_typed::(); let device = Device::from_name(name) .ok_or(OpenError::PathError(PathError::NoSuchFileOrDirectory))?; - if flags.contains(OFlags::DIRECTORY) { + if flags.contains(FileOpenFlags::DIRECTORY) { return Err(OpenError::PathError(PathError::ComponentNotADirectory)); } - if flags.contains(OFlags::NONBLOCK) + if flags.contains(FileOpenFlags::NONBLOCKING) && matches!( device, Device::Stdin | Device::Stdout | Device::Stderr | Device::URandom @@ -216,7 +218,7 @@ impl Backend for Devices { unimplemented!("Non-blocking I/O is not yet supported for {:?}", device); } - if flags.contains(OFlags::TRUNC) { + if flags.contains(FileOpenFlags::TRUNCATE) { // Note: matching Linux behavior, this does not actually perform any truncation, and // instead, it is silently ignored if you attempt to truncate upon opening stdio. debug_assert!(matches!( @@ -375,7 +377,12 @@ mod tests { fn urandom_requires_broker_only_for_nonempty_reads() { let devices = Devices::new(InodeAllocator::standalone()); let urandom = devices - .open_file_at(devices.root(), "urandom", OFlags::RDONLY) + .open_file_at( + devices.root(), + "urandom", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + ) .unwrap() .item; diff --git a/litebox_broker_core/src/fs/in_mem.rs b/litebox_broker_core/src/fs/in_mem.rs index 55ce54e27a..fa3457471f 100644 --- a/litebox_broker_core/src/fs/in_mem.rs +++ b/litebox_broker_core/src/fs/in_mem.rs @@ -9,7 +9,8 @@ use alloc::vec::Vec; use hashbrown::HashMap; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, + FileType, FileUser as UserInfo, }; use litebox_platform::sync; @@ -210,8 +211,8 @@ impl super::backend::private::Sealed pub struct InMemDirHandle { dir: DirNode, /// The flags the directory was opened with; walking handles are not opened for access, and - /// thus use [`super::OFlags::PATH`]. - flags: super::OFlags, + /// thus use [`FileOpenFlags::PATH`]. + flags: FileOpenFlags, } impl Clone for InMemDirHandle { fn clone(&self) -> Self { @@ -244,7 +245,7 @@ impl super::backend::Backend for InMe fn root(&self) -> super::backend::WalkingDirHandle<'_> { super::backend::WalkingDirHandle::from_typed::(InMemDirHandle { dir: self.root.clone(), - flags: super::OFlags::PATH, + flags: FileOpenFlags::PATH, }) } @@ -284,7 +285,7 @@ impl super::backend::Backend for InMe }); current = InMemDirHandle { dir: child, - flags: super::OFlags::PATH, + flags: FileOpenFlags::PATH, }; } Ok(super::backend::WalkOutcome { @@ -297,19 +298,24 @@ impl super::backend::Backend for InMe fn owned_dir_at( &self, dir: super::backend::WalkingDirHandle<'_>, - flags: super::OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result { - assert_supported_oflags(flags); - if flags.intersects(super::OFlags::WRONLY | super::OFlags::RDWR) { + assert_supported_flags(flags); + if matches!( + access, + FileAccessMode::WriteOnly | FileAccessMode::ReadWrite + ) { // XXX(jayb): POSIX requires `EISDIR` when write access is requested on a directory, but // `OpenError` has no such variant yet. Allowing write-mode directory handles here is a - // workaround for the Windows access-to-`OFlags` mapping introduced by PR #894 + // workaround for the Windows access mapping introduced by PR #894 // (afe6cddc): Windows directory modification rights (such as `DELETE`) use write mode // to trigger permission checks even though the handle is not used as a writable byte // stream. A cleaner design would separate permission-check intent from handle I/O mode, // allowing the Windows shim to request modification permission on a non-stream // directory handle while POSIX opens with `WRONLY` or `RDWR` return `EISDIR`. litebox_util_log::debug!( + access:? = access, flags:? = flags; "using writable-directory workaround for permission checks" ); @@ -329,7 +335,7 @@ impl super::backend::Backend for InMe Some(super::backend::WalkingDirHandle::from_typed::( InMemDirHandle { dir: dir.get_typed::().dir.clone(), - flags: super::OFlags::PATH, + flags: FileOpenFlags::PATH, }, )) } @@ -338,9 +344,10 @@ impl super::backend::Backend for InMe &self, dir: super::backend::WalkingDirHandle<'_>, name: &str, - flags: super::OFlags, + _access: FileAccessMode, + flags: FileOpenFlags, ) -> Result, OpenError> { - assert_supported_oflags(flags); + assert_supported_flags(flags); let dir = dir.into_typed::(); let child = dir .dir @@ -352,12 +359,12 @@ impl super::backend::Backend for InMe let Node::File(file) = child else { return Err(PathError::ComponentNotADirectory.into()); }; - if flags.contains(super::OFlags::DIRECTORY) { + if flags.contains(FileOpenFlags::DIRECTORY) { return Err(PathError::ComponentNotADirectory.into()); } let perms = file.read().perms.clone(); let handle = super::backend::FileHandle::from_typed::(InMemFileHandle { file }); - if flags.contains(super::OFlags::TRUNC) && !flags.contains(super::OFlags::PATH) { + if flags.contains(FileOpenFlags::TRUNCATE) && !flags.contains(FileOpenFlags::PATH) { // Linux truncates whenever the open succeeds, regardless of the access mode (an // `O_RDONLY|O_TRUNC` open of a writable file does truncate it); `O_PATH` opens ignore // `O_TRUNC` entirely. @@ -546,7 +553,7 @@ impl super::backend::Backend for InMe InMemDirHandle { dir: child, // TODO(jayb): is this the right set of flags here? - flags: super::OFlags::PATH, + flags: FileOpenFlags::PATH, }, )) } @@ -628,23 +635,8 @@ impl super::backend::Backend for InMe } } -/// Flags this backend knows how to honor when opening files/directories. -const SUPPORTED_OFLAGS: super::OFlags = super::OFlags::CREAT - .union(super::OFlags::RDONLY) - .union(super::OFlags::WRONLY) - .union(super::OFlags::RDWR) - .union(super::OFlags::TRUNC) - .union(super::OFlags::NOCTTY) - .union(super::OFlags::EXCL) - .union(super::OFlags::DIRECTORY) - .union(super::OFlags::NONBLOCK) - .union(super::OFlags::LARGEFILE) - .union(super::OFlags::NOFOLLOW) - .union(super::OFlags::APPEND) - .union(super::OFlags::PATH); - -fn assert_supported_oflags(flags: super::OFlags) { - if flags.intersects(SUPPORTED_OFLAGS.complement()) { +fn assert_supported_flags(flags: FileOpenFlags) { + if !flags.difference(FileOpenFlags::all()).is_empty() { unimplemented!("{flags:?}") } } diff --git a/litebox_broker_core/src/fs/mod.rs b/litebox_broker_core/src/fs/mod.rs index 389037d2da..f81680c278 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -8,10 +8,6 @@ //! via the [`Composer`](composer::Composer), or stacked as a writable upper over immutable lowers //! via the [`Overlay`](overlay::Overlay). -use bitflags::bitflags; - -use core::ffi::c_uint; - pub mod backend; pub mod composer; pub mod devices; @@ -35,91 +31,5 @@ pub use service::{ path_status, read, read_directory, rmdir, seek, truncate, unlink, write, }; -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 size reported as the size of a directory. 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 0f0a9a919d..2acd16190b 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -15,13 +15,13 @@ use core::num::NonZeroU64; use core::sync::atomic::{AtomicBool, Ordering}; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode, FileNodeInfo, FileStatus, FileType, FileUser, + FileAccessMode, FileDirectoryEntry, FileMode, FileNodeInfo, FileOpenFlags, FileStatus, + FileType, FileUser, }; use litebox_platform::sync; use thiserror::Error; use self::fcall::Rlerror; -use super::OFlags; use super::backend::{ DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, @@ -35,6 +35,8 @@ mod fcall; mod id_pool; #[cfg(test)] mod metadata_tests; +#[cfg(test)] +mod open_flags_tests; #[cfg(all(test, target_os = "linux"))] mod tests; @@ -353,16 +355,20 @@ where fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result { - assert_supported_oflags(flags); - if flags.intersects(OFlags::WRONLY | OFlags::RDWR) { + assert_supported_flags(flags); + if matches!( + access, + FileAccessMode::WriteOnly | FileAccessMode::ReadWrite + ) { // TODO(jayb): POSIX requires `EISDIR` when write access is requested on a directory, // but `OpenError` has no such variant yet. unimplemented!() } let (fid, is_backend_root) = dir.into_typed::().into_dir(); - if flags.contains(OFlags::PATH) { + if flags.contains(FileOpenFlags::PATH) { // An `O_PATH` handle is never opened server-side, so the walked fid can be handed over // as-is, even when it is the shared root fid. return Ok(DirHandle::from_typed::(NinePDirHandle { fid })); @@ -397,12 +403,13 @@ where &self, dir: WalkingDirHandle<'_>, name: &str, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result, OpenError> { - assert_supported_oflags(flags); + assert_supported_flags(flags); // TODO: we do not support non-blocking, so ignore that flag instead of returning an error. - let flags = flags - OFlags::NONBLOCK; - if flags.contains(OFlags::DIRECTORY) { + let flags = flags - FileOpenFlags::NONBLOCKING; + if flags.contains(FileOpenFlags::DIRECTORY) { return Err(OpenError::PathError(PathError::ComponentNotADirectory)); } @@ -418,14 +425,17 @@ where } }; - if !flags.contains(OFlags::PATH) { + if !flags.contains(FileOpenFlags::PATH) { // An `O_PATH` handle addresses the file without opening it server-side. // // The file exists (it is what stopped the walk), so the creation flags say nothing // about how to open it; the resolver enforces `O_CREAT | O_EXCL` itself. self.client.open( &fid.fid, - oflags_to_lopen(flags - OFlags::CREAT - OFlags::EXCL), + open_flags_to_lopen( + access, + flags - FileOpenFlags::CREATE - FileOpenFlags::EXCLUSIVE, + ), )?; } Ok(Permissioned { @@ -634,72 +644,42 @@ fn backend_checked_components(count: usize) -> Vec { ] } -/// Flags this backend knows how to honor when opening files/directories. -const SUPPORTED_OFLAGS: 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); - -fn assert_supported_oflags(flags: OFlags) { - if flags.intersects(SUPPORTED_OFLAGS.complement()) { +fn assert_supported_flags(flags: FileOpenFlags) { + if !flags.difference(FileOpenFlags::all()).is_empty() { unimplemented!("{flags:?}") } } -/// Convert [`OFlags`] to 9P `LOpenFlags` -fn oflags_to_lopen(flags: OFlags) -> fcall::LOpenFlags { - let mut lflags = fcall::LOpenFlags::empty(); - - // Access mode (RDONLY is 0, so we only check for WRONLY and RDWR) - if flags.contains(OFlags::RDWR) { - lflags |= fcall::LOpenFlags::O_RDWR; - } else if flags.contains(OFlags::WRONLY) { - lflags |= fcall::LOpenFlags::O_WRONLY; - } - // RDONLY is implicit if neither WRONLY nor RDWR +/// Convert canonical open intent to the 9P2000.L wire flags. +fn open_flags_to_lopen(access: FileAccessMode, flags: FileOpenFlags) -> fcall::LOpenFlags { + let mut lflags = match access { + FileAccessMode::ReadOnly => fcall::LOpenFlags::empty(), + FileAccessMode::WriteOnly => fcall::LOpenFlags::O_WRONLY, + FileAccessMode::ReadWrite => fcall::LOpenFlags::O_RDWR, + _ => unimplemented!("{access:?}"), + }; - if flags.contains(OFlags::CREAT) { + if flags.contains(FileOpenFlags::CREATE) { lflags |= fcall::LOpenFlags::O_CREAT; } - if flags.contains(OFlags::EXCL) { + if flags.contains(FileOpenFlags::EXCLUSIVE) { lflags |= fcall::LOpenFlags::O_EXCL; } - if flags.contains(OFlags::TRUNC) { + if flags.contains(FileOpenFlags::TRUNCATE) { lflags |= fcall::LOpenFlags::O_TRUNC; } - if flags.contains(OFlags::APPEND) { + if flags.contains(FileOpenFlags::APPEND) { lflags |= fcall::LOpenFlags::O_APPEND; } - if flags.contains(OFlags::DIRECTORY) { + if flags.contains(FileOpenFlags::DIRECTORY) { lflags |= fcall::LOpenFlags::O_DIRECTORY; } - if flags.contains(OFlags::NOFOLLOW) { + if flags.contains(FileOpenFlags::NO_FOLLOW) { lflags |= fcall::LOpenFlags::O_NOFOLLOW; } - if flags.contains(OFlags::NONBLOCK) { + if flags.contains(FileOpenFlags::NONBLOCKING) { lflags |= fcall::LOpenFlags::O_NONBLOCK; } - if flags.contains(OFlags::SYNC) { - lflags |= fcall::LOpenFlags::O_SYNC; - } - if flags.contains(OFlags::DSYNC) { - lflags |= fcall::LOpenFlags::O_DSYNC; - } - if flags.contains(OFlags::DIRECT) { - lflags |= fcall::LOpenFlags::O_DIRECT; - } - if flags.contains(OFlags::NOATIME) { - lflags |= fcall::LOpenFlags::O_NOATIME; - } lflags } diff --git a/litebox_broker_core/src/fs/nine_p/open_flags_tests.rs b/litebox_broker_core/src/fs/nine_p/open_flags_tests.rs new file mode 100644 index 0000000000..cd969a71d5 --- /dev/null +++ b/litebox_broker_core/src/fs/nine_p/open_flags_tests.rs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; + +use super::fcall::LOpenFlags; +use super::open_flags_to_lopen; + +#[test] +fn access_modes_are_not_wire_flag_bits() { + for (access, expected) in [ + (FileAccessMode::ReadOnly, LOpenFlags::empty()), + (FileAccessMode::WriteOnly, LOpenFlags::O_WRONLY), + (FileAccessMode::ReadWrite, LOpenFlags::O_RDWR), + ] { + assert_eq!( + open_flags_to_lopen(access, FileOpenFlags::empty()), + expected + ); + assert_eq!( + open_flags_to_lopen(access, FileOpenFlags::CREATE | FileOpenFlags::APPEND), + expected | LOpenFlags::O_CREAT | LOpenFlags::O_APPEND, + ); + } +} + +#[test] +fn canonical_flags_translate_only_at_wire_boundary() { + for (flag, expected) in [ + (FileOpenFlags::CREATE, LOpenFlags::O_CREAT), + (FileOpenFlags::EXCLUSIVE, LOpenFlags::O_EXCL), + (FileOpenFlags::TRUNCATE, LOpenFlags::O_TRUNC), + (FileOpenFlags::APPEND, LOpenFlags::O_APPEND), + (FileOpenFlags::DIRECTORY, LOpenFlags::O_DIRECTORY), + (FileOpenFlags::NO_FOLLOW, LOpenFlags::O_NOFOLLOW), + (FileOpenFlags::NONBLOCKING, LOpenFlags::O_NONBLOCK), + (FileOpenFlags::NO_CONTROLLING_TERMINAL, LOpenFlags::empty()), + (FileOpenFlags::LARGE_FILE, LOpenFlags::empty()), + (FileOpenFlags::PATH, LOpenFlags::empty()), + ] { + assert_eq!( + open_flags_to_lopen(FileAccessMode::ReadOnly, flag), + expected + ); + } +} diff --git a/litebox_broker_core/src/fs/nine_p/tests.rs b/litebox_broker_core/src/fs/nine_p/tests.rs index a5fb9f318f..e32cbdc5d4 100644 --- a/litebox_broker_core/src/fs/nine_p/tests.rs +++ b/litebox_broker_core/src/fs/nine_p/tests.rs @@ -12,9 +12,10 @@ use std::io::{Read as _, Write as _}; use std::net::{TcpListener, TcpStream}; use std::path::Path; -use litebox_broker_protocol::fs::{FileMode as Mode, FileSeekWhence as SeekWhence, FileType}; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileMode as Mode, FileOpenFlags, FileSeekWhence as SeekWhence, FileType, +}; -use crate::fs::OFlags; use crate::fs::errors::{ FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, @@ -220,7 +221,8 @@ fn test_nine_p_create_and_read_file() { .open( USER, "/hello.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file via 9P"); @@ -241,7 +243,13 @@ fn test_nine_p_create_and_read_file() { // Read the file back through 9P let mut fd = fs - .open(USER, "/hello.txt", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/hello.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("failed to open file for reading via 9P"); let mut buf = alloc::vec![0u8; 256]; @@ -269,7 +277,8 @@ fn test_nine_p_mkdir_and_readdir() { .open( USER, "/subdir/file.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file in subdir"); @@ -278,7 +287,13 @@ fn test_nine_p_mkdir_and_readdir() { // Read the root directory let fd = fs - .open(USER, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open( + USER, + "/", + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, + Mode::empty(), + ) .expect("failed to open root dir"); let entries = fs.read_dir(&fd).expect("failed to readdir root"); drop(fd); @@ -294,7 +309,8 @@ fn test_nine_p_mkdir_and_readdir() { .open( USER, "/subdir", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .expect("failed to open subdir"); @@ -322,7 +338,8 @@ fn test_nine_p_unlink_and_rmdir() { .open( USER, "/to_delete.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -333,8 +350,14 @@ fn test_nine_p_unlink_and_rmdir() { // Verify the file is gone assert!( - fs.open(USER, "/to_delete.txt", OFlags::RDONLY, Mode::empty()) - .is_err(), + fs.open( + USER, + "/to_delete.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty() + ) + .is_err(), "file should no longer exist" ); @@ -361,7 +384,8 @@ fn test_nine_p_file_status() { .open( USER, "/status_test.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -402,7 +426,8 @@ fn test_nine_p_seek_and_partial_read() { .open( USER, "/seek_test.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -411,7 +436,13 @@ fn test_nine_p_seek_and_partial_read() { // Open for reading and seek let mut fd = fs - .open(USER, "/seek_test.txt", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/seek_test.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("failed to open file for reading"); // Seek to offset 5 @@ -438,7 +469,8 @@ fn test_nine_p_truncate() { .open( USER, "/trunc_test.txt", - OFlags::CREAT | OFlags::RDWR, + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -471,7 +503,13 @@ fn test_nine_p_host_files_visible() { // Read file created on the host through 9P let mut fd = fs - .open(USER, "/host_file.txt", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/host_file.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("failed to open host file via 9P"); let mut buf = alloc::vec![0u8; 256]; let n = fs.read(&mut fd, &mut buf, None).unwrap(); @@ -483,7 +521,8 @@ fn test_nine_p_host_files_visible() { .open( USER, "/host_dir", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .expect("failed to open host dir via 9P"); @@ -571,7 +610,13 @@ fn test_nine_p_broken_open() { // 2 writes: version + attach. The next write (open's walk) will fail. let fs = connect_9p_broken(&server, 2); - let result = fs.open(USER, "/anything.txt", OFlags::RDONLY, Mode::empty()); + let result = fs.open( + USER, + "/anything.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ); assert!(matches!(result, Err(OpenError::Io))); } @@ -581,7 +626,13 @@ fn test_nine_p_broken_create() { let server = DiodServer::start(); let fs = connect_9p_broken(&server, 2); - let result = fs.open(USER, "/new.txt", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU); + let result = fs.open( + USER, + "/new.txt", + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ); assert!(matches!(result, Err(OpenError::Io))); } @@ -595,7 +646,13 @@ fn test_nine_p_broken_read() { // 4 writes: version + attach + walk + lopen. Then read will fail. let fs = connect_9p_broken(&server, 4); let mut fd = fs - .open(USER, "/read_me.txt", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/read_me.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("open should succeed before break"); let mut buf = alloc::vec![0u8; 64]; @@ -615,7 +672,8 @@ fn test_nine_p_broken_write() { .open( USER, "/write_me.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("create should succeed before break"); @@ -642,7 +700,13 @@ fn test_nine_p_broken_readdir() { // 4 writes: version + attach + walk + lopen for the directory. let fs = connect_9p_broken(&server, 4); let fd = fs - .open(USER, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open( + USER, + "/", + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, + Mode::empty(), + ) .expect("open dir should succeed before break"); let result = fs.read_dir(&fd); @@ -693,7 +757,13 @@ fn test_nine_p_broken_truncate() { // 4 writes: version + attach + walk + lopen. Then truncate will fail. let fs = connect_9p_broken(&server, 4); let mut fd = fs - .open(USER, "/to_trunc.txt", OFlags::RDWR, Mode::empty()) + .open( + USER, + "/to_trunc.txt", + FileAccessMode::ReadWrite, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("open should succeed before break"); let result = fs.truncate(&mut fd, 0, true); @@ -710,7 +780,13 @@ fn test_nine_p_broken_seek() { // 4 writes: version + attach + walk + lopen. Then the getattr for seek will fail. let fs = connect_9p_broken(&server, 4); let mut fd = fs - .open(USER, "/to_seek.txt", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/to_seek.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("open should succeed before break"); let result = fs.seek(&mut fd, -1, SeekWhence::RelativeToEnd); @@ -736,14 +812,26 @@ fn test_nine_p_deep_path_walk() { // Create a file at the bottom let file_path = path.clone() + "/deep_file.txt"; let mut fd = fs - .open(USER, &file_path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + USER, + &file_path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("failed to create file in deep path"); fs.write(&mut fd, b"deep content", None).unwrap(); drop(fd); // Read it back let mut fd = fs - .open(USER, &file_path, OFlags::RDONLY, Mode::empty()) + .open( + USER, + &file_path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("failed to open file in deep path"); let mut buf = alloc::vec![0u8; 64]; let n = fs.read(&mut fd, &mut buf, None).unwrap(); @@ -768,7 +856,8 @@ fn test_nine_p_chmod() { .open( USER, "/chmod_test.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -809,7 +898,8 @@ fn test_nine_p_chown() { .open( USER, "/chown_test.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -847,7 +937,8 @@ fn test_nine_p_handle_status() { .open( USER, "/fd_stat_test.txt", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -856,7 +947,13 @@ fn test_nine_p_handle_status() { // Open the file and check the open-handle status let fd = fs - .open(USER, "/fd_stat_test.txt", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/fd_stat_test.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("failed to open file"); let status = fs.handle_status(&fd).expect("handle status failed"); @@ -867,7 +964,13 @@ fn test_nine_p_handle_status() { drop(fd); let fd = fs - .open(USER, "/", OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty()) + .open( + USER, + "/", + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, + Mode::empty(), + ) .expect("failed to open root dir"); let status = fs.handle_status(&fd).expect("handle status on dir failed"); assert_eq!(status.file_type, FileType::Directory); @@ -891,7 +994,8 @@ fn test_nine_p_large_read_write() { .open( USER, "/large_test.bin", - OFlags::CREAT | OFlags::RDWR, + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -911,7 +1015,13 @@ fn test_nine_p_large_read_write() { // Read it all back let mut fd = fs - .open(USER, "/large_test.bin", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/large_test.bin", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("failed to open file for reading"); let mut read_buf = alloc::vec![0u8; data_size]; @@ -940,7 +1050,8 @@ fn test_nine_p_explicit_offset_read_write() { .open( USER, "/offset_test.txt", - OFlags::CREAT | OFlags::RDWR, + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("failed to create file"); @@ -967,7 +1078,13 @@ fn test_nine_p_explicit_offset_read_write() { // Now test explicit offset reads let mut fd = fs - .open(USER, "/offset_test.txt", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/offset_test.txt", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("failed to open for reading"); // Read 5 bytes at explicit offset 5 → "BBBBB" diff --git a/litebox_broker_core/src/fs/overlay.rs b/litebox_broker_core/src/fs/overlay.rs index 95e0953b5f..9c670d69e3 100644 --- a/litebox_broker_core/src/fs/overlay.rs +++ b/litebox_broker_core/src/fs/overlay.rs @@ -21,11 +21,11 @@ use alloc::vec::Vec; use hashbrown::{HashMap, HashSet}; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, + FileType, FileUser as UserInfo, }; use litebox_platform::sync::{Mutex, MutexGuard, RawSyncPrimitivesProvider}; -use super::OFlags; use super::backend::{ Backend, BackendHandles, CreationMetadata, DeviceIo, DirHandle, FileHandle, Handle, HandleRef, NoDeviceIo, PermissionCheck, PermissionInfo, Permissioned, SeekBehavior, WalkOutcome, @@ -161,11 +161,19 @@ impl Overlay { } fn resolve_root(&self) -> Result { - let upper = self.upper.owned_dir_at(self.upper.root(), OFlags::PATH)?; + let upper = self.upper.owned_dir_at( + self.upper.root(), + FileAccessMode::ReadOnly, + FileOpenFlags::PATH, + )?; let lowers = self .lowers .iter() - .map(|lower| lower.owned_dir_at(lower.root(), OFlags::PATH).map(Some)) + .map(|lower| { + lower + .owned_dir_at(lower.root(), FileAccessMode::ReadOnly, FileOpenFlags::PATH) + .map(Some) + }) .collect::, _>>()?; self.merge(Some(upper), lowers) } @@ -196,7 +204,11 @@ impl Overlay { return Err(PathError::ComponentNotADirectory.into()); } let component = component.clone(); - let owned = backend.owned_dir_at(outcome.last, OFlags::PATH)?; + let owned = backend.owned_dir_at( + outcome.last, + FileAccessMode::ReadOnly, + FileOpenFlags::PATH, + )?; Ok((owned, component)) } @@ -442,7 +454,11 @@ impl Overlay { locked: &NamespaceGuard<'_, Platform>, path: &[String], ) -> Result { - let mut upper = self.upper.owned_dir_at(self.upper.root(), OFlags::PATH)?; + let mut upper = self.upper.owned_dir_at( + self.upper.root(), + FileAccessMode::ReadOnly, + FileOpenFlags::PATH, + )?; for index in 0..path.len() { // Re-resolve after each materialisation, since it changed the upper namespace. let resolved = self.resolve_dir(&path[..=index])?; @@ -776,13 +792,14 @@ impl Backend for Overlay { fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result { let path = dir.into_typed::().path; let resolved = self.resolve_dir(&path)?; let (_, backend, handle) = self.owning_dir(&resolved).ok_or(OpenError::Io)?; let walking = backend.walking_dir_at(handle).ok_or(OpenError::Io)?; - backend.owned_dir_at(walking, flags)?; + backend.owned_dir_at(walking, access, flags)?; Ok(DirHandle::from_typed::(OverlayDir { path })) } @@ -796,12 +813,13 @@ impl Backend for Overlay { &self, dir: WalkingDirHandle<'_>, name: &str, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result, OpenError> { if !valid(name) { return Err(PathError::InvalidPathname.into()); } - if flags.contains(OFlags::DIRECTORY) { + if flags.contains(FileOpenFlags::DIRECTORY) { return Err(PathError::ComponentNotADirectory.into()); } let path = dir.into_typed::().path; @@ -813,14 +831,14 @@ impl Backend for Overlay { .ok_or(OpenError::PathError(PathError::NoSuchFileOrDirectory))?; // The resolver only reaches `create_file_at` once a walk reported the name as missing, so // an existing entry means an exclusive create must fail here. - if flags.contains(OFlags::CREAT | OFlags::EXCL) { + if flags.contains(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE) { return Err(OpenError::AlreadyExists); } let (layer, permissions) = if entry.upper { let upper = resolved.upper.as_ref().ok_or(OpenError::Io)?; let walking = self.upper.walking_dir_at(upper).ok_or(OpenError::Io)?; - let file = self.upper.open_file_at(walking, name, flags)?; + let file = self.upper.open_file_at(walking, name, access, flags)?; (OverlayFileLayer::Upper(file.item), file.permissions) } else { let layer = entry.lower.ok_or(OpenError::Io)?; @@ -834,14 +852,16 @@ impl Backend for Overlay { // XXX(jayb): the resolver authorizes an open only after this returns, so a // writable open can copy up before a later permission denial. A preflight // authorization hook in `Backend` would make copy-up properly two-phase. - let writing = - flags.intersects(OFlags::WRONLY | OFlags::RDWR | OFlags::APPEND | OFlags::TRUNC); - let lower_flags = if writing { - OFlags::RDONLY + let writing = matches!( + access, + FileAccessMode::WriteOnly | FileAccessMode::ReadWrite + ) || flags.intersects(FileOpenFlags::APPEND | FileOpenFlags::TRUNCATE); + let (lower_access, lower_flags) = if writing { + (FileAccessMode::ReadOnly, FileOpenFlags::empty()) } else { - flags.difference(OFlags::CREAT) + (access, flags.difference(FileOpenFlags::CREATE)) }; - let file = self.lowers[layer].open_file_at(walking, name, lower_flags)?; + let file = self.lowers[layer].open_file_at(walking, name, lower_access, lower_flags)?; // The file's own identity, which is also the key a later copy-up records itself under. let status = self.lowers[layer] .status(HandleRef::File(&file.item)) @@ -858,7 +878,7 @@ impl Backend for Overlay { name, (layer, &file.item), &status, - flags.contains(OFlags::TRUNC), + flags.contains(FileOpenFlags::TRUNCATE), )?; // The lower open was substituted with a read-only one, so its `PermissionCheck` // says nothing about the caller's write access; check the file's own mode instead. diff --git a/litebox_broker_core/src/fs/resolver.rs b/litebox_broker_core/src/fs/resolver.rs index 91f18b472e..9054195936 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -4,25 +4,21 @@ //! Path management, permission checks, and open-state operations above [`super::backend`]. use alloc::string::String; -use alloc::vec; use alloc::vec::Vec; use core::marker::PhantomData; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, FileType, - FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileOpenFlags, + FileSeekWhence as SeekWhence, FileStatus, FileType, FileUser as UserInfo, ResolvedPath, }; +use super::backend::{ + CreationMetadata, DeviceIo, DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, + Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, +}; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; -use super::{ - OFlags, - backend::{ - CreationMetadata, DeviceIo, DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, - Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, - }, -}; /// The broker-core filesystem resolver, generic over its synchronization platform and /// [`Backend`](super::backend::Backend). @@ -55,22 +51,6 @@ impl Context { self.user_info } - fn resolve(path: &str) -> ResolvedPath { - let mut components = vec![]; - for component in path.split('/') { - match component { - "" | "." => {} - ".." => { - let _ = components.pop(); - } - _ => { - components.push(component.into()); - } - } - } - ResolvedPath { components } - } - fn can_execute(&self, permissions: &PermissionInfo) -> bool { if self.user_info.user == permissions.owner.user { permissions.mode.contains(Mode::XUSR) @@ -102,17 +82,6 @@ impl Context { } } -struct ResolvedPath { - components: Vec, -} - -impl ResolvedPath { - fn parent_and_name(&self) -> Option<(Vec<&str>, &str)> { - let (name, parent) = self.components.split_last()?; - Some((parent.iter().map(String::as_str).collect(), name.as_str())) - } -} - /// A directory reached by a walk, plus the permission metadata to check against it. struct WalkedDir<'a> { handle: WalkingDirHandle<'a>, @@ -145,6 +114,7 @@ impl Resolver = parent_components.iter().map(String::as_str).collect(); let parent = self.walk_to_directory( context, self.backend.root(), @@ -172,7 +142,7 @@ impl Resolver) -> Result { self.backend - .owned_dir_at(dir, OFlags::PATH) + .owned_dir_at(dir, FileAccessMode::ReadOnly, FileOpenFlags::PATH) .map_err(|error| match error { OpenError::PathError(PathError::NoSuchFileOrDirectory) => { PathError::MissingComponent.into() @@ -185,7 +155,7 @@ impl Resolver Resolver WalkError::PathError(error), _ => WalkError::Io, }; - let components: Vec<_> = path.components.iter().map(String::as_str).collect(); + let components: Vec<_> = path.components().iter().map(String::as_str).collect(); if components.is_empty() { let root = self .backend - .owned_dir_at(self.backend.root(), OFlags::PATH) + .owned_dir_at( + self.backend.root(), + FileAccessMode::ReadOnly, + FileOpenFlags::PATH, + ) .map_err(map_open_error)?; // A backend root reports no permission metadata, so the backend is left to enforce // whatever it wants on it. @@ -227,7 +201,7 @@ impl Resolver Resolver { let file = self .backend - .open_file_at(outcome.last, components[walked], OFlags::PATH) + .open_file_at( + outcome.last, + components[walked], + FileAccessMode::ReadOnly, + FileOpenFlags::PATH, + ) .map_err(map_open_error)?; Ok(Permissioned { item: Handle::File(file.item), @@ -386,44 +365,36 @@ impl Resolver Resolver { /// Opens a file /// - /// The `mode` is only significant when creating a file + /// The `mode` is only significant when creating a file. [`FileOpenFlags::PATH`] ignores + /// `access` and all flags except [`FileOpenFlags::DIRECTORY`] and [`FileOpenFlags::NO_FOLLOW`]. pub fn open( &self, user: UserInfo, path: &str, - mut flags: OFlags, + mut access: FileAccessMode, + mut flags: FileOpenFlags, mode: Mode, ) -> Result, OpenError> { - const CURRENTLY_SUPPORTED_OFLAGS: 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(CURRENTLY_SUPPORTED_OFLAGS.complement()) { + if !flags.difference(FileOpenFlags::all()).is_empty() { unimplemented!("{flags:?}") } - let path_only = flags.contains(OFlags::PATH); + let path_only = flags.contains(FileOpenFlags::PATH); if path_only { // For `PATH`, we restrict what other flags are allowed, so a missing path cannot lead // to a creation, etc. - flags &= OFlags::PATH | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC; + flags &= FileOpenFlags::PATH | FileOpenFlags::DIRECTORY | FileOpenFlags::NO_FOLLOW; + access = FileAccessMode::ReadOnly; } let context = Context::new(user); - let path = Context::resolve(path); - let access_mode = flags & (OFlags::WRONLY | OFlags::RDWR); - let read_allowed = access_mode == OFlags::RDONLY || access_mode == OFlags::RDWR; - let write_allowed = access_mode == OFlags::WRONLY || access_mode == OFlags::RDWR; - let append_mode = flags.contains(OFlags::APPEND); + let path = ResolvedPath::root().resolve(path); + let (read_allowed, write_allowed) = match access { + FileAccessMode::ReadOnly => (true, false), + FileAccessMode::WriteOnly => (false, true), + FileAccessMode::ReadWrite => (true, true), + _ => unimplemented!("{access:?}"), + }; + let append_mode = flags.contains(FileOpenFlags::APPEND); let entry = |handle, seek_behavior| ResolverEntry { handle, _backend: core::marker::PhantomData, @@ -435,17 +406,20 @@ impl Resolver = path.components.iter().map(String::as_str).collect(); + let components: Vec<_> = path.components().iter().map(String::as_str).collect(); let walk = self.walk_path( &context, self.backend.root(), @@ -463,11 +437,11 @@ impl Resolver { - if flags.contains(OFlags::CREAT) && flags.contains(OFlags::EXCL) { + if flags.contains(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE) { return Err(OpenError::AlreadyExists); } Ok(entry( - Handle::Dir(self.backend.owned_dir_at(outcome.last, flags)?), + Handle::Dir(self.backend.owned_dir_at(outcome.last, access, flags)?), SeekBehavior::NonSeekable, )) } @@ -477,8 +451,10 @@ impl Resolver Resolver + if flags.contains(FileOpenFlags::CREATE) => { let Some((parent_components, name)) = path.parent_and_name() else { unreachable!("root path was handled above") }; + let parent_components: Vec<_> = + parent_components.iter().map(String::as_str).collect(); let parent = self .walk_to_directory( &context, @@ -799,7 +777,7 @@ impl Resolver Result<(), ChmodError> { let context = Context::new(user); - let path = Context::resolve(path); + let path = ResolvedPath::root().resolve(path); let handle = self .path_handle(&context, &path) .map_err(|error| match error { @@ -821,7 +799,7 @@ impl Resolver, ) -> Result<(), ChownError> { let context = Context::new(acting_user); - let path = Context::resolve(path); + let path = ResolvedPath::root().resolve(path); let handle = self .path_handle(&context, &path) .map_err(|error| match error { @@ -837,7 +815,7 @@ impl Resolver Result<(), UnlinkError> { let context = Context::new(user); - let path = Context::resolve(path); + let path = ResolvedPath::root().resolve(path); let Some((parent, name)) = self.parent_dir_and_name(&context, &path) .map_err(|error| match error { @@ -862,7 +840,7 @@ impl Resolver Result<(), MkdirError> { let context = Context::new(user); - let path = Context::resolve(path); + let path = ResolvedPath::root().resolve(path); let Some((parent, name)) = self.parent_dir_and_name(&context, &path) .map_err(|error| match error { @@ -896,7 +874,7 @@ impl Resolver Result<(), RmdirError> { let context = Context::new(user); - let path = Context::resolve(path); + let path = ResolvedPath::root().resolve(path); let Some((parent, name)) = self.parent_dir_and_name(&context, &path) .map_err(|error| match error { @@ -952,17 +930,23 @@ impl Resolver Result { - let entry = - self.open(user, path, OFlags::PATH, Mode::empty()) - .map_err(|error| match error { - OpenError::PathError(error) => error.into(), - OpenError::Io - | OpenError::AccessNotAllowed - | OpenError::NoWritePerms - | OpenError::ReadOnlyFileSystem - | OpenError::AlreadyExists - | OpenError::TruncateError(_) => FileStatusError::Io, - })?; + let entry = self + .open( + user, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::PATH, + Mode::empty(), + ) + .map_err(|error| match error { + OpenError::PathError(error) => error.into(), + OpenError::Io + | OpenError::AccessNotAllowed + | OpenError::NoWritePerms + | OpenError::ReadOnlyFileSystem + | OpenError::AlreadyExists + | OpenError::TruncateError(_) => FileStatusError::Io, + })?; self.handle_status(&entry) } diff --git a/litebox_broker_core/src/fs/service.rs b/litebox_broker_core/src/fs/service.rs index 908a4029f1..08525edbb0 100644 --- a/litebox_broker_core/src/fs/service.rs +++ b/litebox_broker_core/src/fs/service.rs @@ -14,7 +14,6 @@ use litebox_broker_protocol::fs::{ 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, @@ -206,11 +205,18 @@ where flags: FileOpenFlags, mode: FileMode, ) -> ServiceResult { - let flags = open_flags(access, flags)?; - let entry = match Resolver::open(self, user, path, flags, mode & FileMode::SUPPORTED) { - Ok(entry) => entry, - Err(error) => return Ok(Err(file_open_error(error))), - }; + if !matches!( + access, + FileAccessMode::ReadOnly | FileAccessMode::WriteOnly | FileAccessMode::ReadWrite + ) || FileOpenFlags::from_bits(flags.bits()).is_none() + { + return Err(BrokerError::UnsupportedOperation); + } + let entry = + match Resolver::open(self, user, path, access, flags, mode & FileMode::SUPPORTED) { + Ok(entry) => entry, + Err(error) => return Ok(Err(file_open_error(error))), + }; Ok(Ok(File(Arc::new(RwLock::::new(entry))))) } @@ -617,6 +623,9 @@ fn authorize(session: &BrokerSession, required: ObjectRights) -> Result Result { + if FileOpenFlags::from_bits(flags.bits()).is_none() { + return Err(BrokerError::UnsupportedOperation); + } if flags.contains(FileOpenFlags::PATH) { return Ok(ObjectRights::WAIT); } @@ -703,32 +712,6 @@ impl DeviceIo for SessionDeviceIo<'_> { } } -fn open_flags(access: FileAccessMode, flags: FileOpenFlags) -> Result { - let mut output = match access { - FileAccessMode::ReadOnly => OFlags::RDONLY, - FileAccessMode::WriteOnly => OFlags::WRONLY, - FileAccessMode::ReadWrite => OFlags::RDWR, - _ => return Err(BrokerError::UnsupportedOperation), - }; - for (file_flag, engine_flag) in [ - (FileOpenFlags::CREATE, OFlags::CREAT), - (FileOpenFlags::TRUNCATE, OFlags::TRUNC), - (FileOpenFlags::NO_CONTROLLING_TERMINAL, OFlags::NOCTTY), - (FileOpenFlags::EXCLUSIVE, OFlags::EXCL), - (FileOpenFlags::DIRECTORY, OFlags::DIRECTORY), - (FileOpenFlags::NONBLOCKING, OFlags::NONBLOCK), - (FileOpenFlags::LARGE_FILE, OFlags::LARGEFILE), - (FileOpenFlags::NO_FOLLOW, OFlags::NOFOLLOW), - (FileOpenFlags::APPEND, OFlags::APPEND), - (FileOpenFlags::PATH, OFlags::PATH), - ] { - if flags.contains(file_flag) { - output |= engine_flag; - } - } - Ok(output) -} - fn mask_status_mode(status: &mut FileStatus) { status.mode &= FileMode::SUPPORTED; } @@ -861,6 +844,31 @@ mod tests { use core::num::NonZeroU64; use litebox_broker_protocol::fs::{FileNodeInfo, FileType}; + #[test] + fn unknown_open_flags_are_rejected_before_path_only_masking() { + let unknown = FileOpenFlags::from_bits_retain(1 << 15); + for access in [ + FileAccessMode::ReadOnly, + FileAccessMode::WriteOnly, + FileAccessMode::ReadWrite, + ] { + for flags in [ + FileOpenFlags::empty(), + FileOpenFlags::CREATE, + FileOpenFlags::PATH, + ] { + assert!(matches!( + open_required_rights(access, flags | unknown), + Err(BrokerError::UnsupportedOperation) + )); + } + assert_eq!( + open_required_rights(access, FileOpenFlags::PATH).unwrap(), + ObjectRights::WAIT + ); + } + } + #[test] fn file_status_excludes_object_type_mode_bits() { let mut status = FileStatus { diff --git a/litebox_broker_core/src/fs/tar_ro.rs b/litebox_broker_core/src/fs/tar_ro.rs index 1f3fe081cd..6d64053ca2 100644 --- a/litebox_broker_core/src/fs/tar_ro.rs +++ b/litebox_broker_core/src/fs/tar_ro.rs @@ -29,11 +29,11 @@ use alloc::vec::Vec; use core::ops::Range; use hashbrown::HashMap; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileStatus, FileType, FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, + FileType, FileUser as UserInfo, }; use super::{ - OFlags, backend::{CreationMetadata, DirHandle, FileHandle, HandleRef, WalkingDirHandle}, errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, @@ -128,9 +128,15 @@ impl super::backend::Backend for TarRo { fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result { - if flags.intersects(OFlags::CREAT | OFlags::TRUNC | OFlags::WRONLY | OFlags::RDWR) { + if flags.intersects(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE) + || matches!( + access, + FileAccessMode::WriteOnly | FileAccessMode::ReadWrite + ) + { return Err(OpenError::ReadOnlyFileSystem); } Ok(DirHandle::from_typed::(dir.into_typed::())) @@ -146,7 +152,8 @@ impl super::backend::Backend for TarRo { &self, dir: WalkingDirHandle<'_>, name: &str, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, ) -> Result, OpenError> { let dir = dir.into_typed::(); let child = self.tar_index.dirs[dir.idx] @@ -156,14 +163,15 @@ impl super::backend::Backend for TarRo { let IndexedChild::File(file_idx) = *child else { return Err(OpenError::PathError(PathError::ComponentNotADirectory)); }; - if flags.contains(OFlags::DIRECTORY) { + if flags.contains(FileOpenFlags::DIRECTORY) { return Err(OpenError::PathError(PathError::ComponentNotADirectory)); } - if !(flags.contains(OFlags::CREAT) && flags.contains(OFlags::EXCL)) - && (flags.contains(OFlags::CREAT) - || flags.contains(OFlags::TRUNC) - || flags.contains(OFlags::WRONLY) - || flags.contains(OFlags::RDWR)) + if !flags.contains(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE) + && (flags.intersects(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE) + || matches!( + access, + FileAccessMode::WriteOnly | FileAccessMode::ReadWrite + )) { return Err(OpenError::ReadOnlyFileSystem); } diff --git a/litebox_broker_core/src/fs/test_support.rs b/litebox_broker_core/src/fs/test_support.rs index c179381eda..4e4711221e 100644 --- a/litebox_broker_core/src/fs/test_support.rs +++ b/litebox_broker_core/src/fs/test_support.rs @@ -10,12 +10,11 @@ use alloc::vec::Vec; use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, - FileUser as UserInfo, + FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileOpenFlags, + FileSeekWhence as SeekWhence, FileStatus, FileUser as UserInfo, }; use litebox_broker_protocol::stdio::StdioOutputStream; -use super::OFlags; use super::backend::{Backend, DeviceIo, NoDeviceIo}; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, @@ -56,10 +55,11 @@ impl Fs { &self, user: UserInfo, path: &str, - flags: OFlags, + access: FileAccessMode, + flags: FileOpenFlags, mode: Mode, ) -> Result, OpenError> { - self.resolver.open(user, path, flags, mode) + self.resolver.open(user, path, access, flags, mode) } pub(in crate::fs) fn read( diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs index 30c0a1c014..a30559181b 100644 --- a/litebox_broker_core/src/fs/tests.rs +++ b/litebox_broker_core/src/fs/tests.rs @@ -12,7 +12,6 @@ use litebox_broker_protocol::fs::{ FileMode as Mode, FileSeekWhence as SeekWhence, FileType, FileUser as UserInfo, }; -use super::OFlags; use super::in_mem::InMem; use super::inode_allocator::InodeAllocator; use super::overlay::Overlay; @@ -43,13 +42,14 @@ fn overlay_fs( } mod in_mem { + use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; + use super::{ - FileType, Fs, InMem, Mode, OFlags, ROOT, SeekWhence, TestPlatform, USER, UserInfo, - in_mem_fs, + FileType, Fs, InMem, Mode, ROOT, SeekWhence, TestPlatform, USER, UserInfo, in_mem_fs, }; use crate::fs::errors::{ ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, - UnlinkError, + TruncateError, UnlinkError, WriteError, }; use crate::fs::test_support::Entry; use alloc::vec; @@ -77,14 +77,27 @@ mod in_mem { // Test file creation let path = "/testfile"; let fd = fs - .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + ROOT, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + 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(), + fs.open( + ROOT, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU + ) + .is_err(), "File should not exist" ); } @@ -96,7 +109,13 @@ mod in_mem { // Create and write to a file let path = "/testfile"; let mut fd = fs - .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + ROOT, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to create file"); let data = b"Hello, world!"; fs.write(&mut fd, data, None) @@ -105,7 +124,13 @@ mod in_mem { // Read from the file let mut fd = fs - .open(ROOT, path, OFlags::RDONLY, Mode::RWXU) + .open( + ROOT, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs @@ -122,7 +147,13 @@ mod in_mem { let path = "/tmp/write_only"; let mut fd = fs - .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::WUSR, + ) .expect("Failed to create write-only file"); fs.write(&mut fd, b"x", None).expect("Failed to write file"); @@ -134,7 +165,13 @@ mod in_mem { drop(fd); assert!(matches!( - fs.open(USER, path, OFlags::RDONLY, Mode::empty()), + fs.open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty() + ), Err(OpenError::AccessNotAllowed) )); } @@ -146,7 +183,13 @@ mod in_mem { let path = "/tmp/zero_mode"; let mut fd = fs - .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::empty(), + ) .expect("Failed to create zero-mode file"); fs.write(&mut fd, b"x", None).expect("Failed to write file"); drop(fd); @@ -154,7 +197,13 @@ mod in_mem { 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()), + fs.open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::empty() + ), Err(OpenError::AccessNotAllowed) )); } @@ -171,7 +220,14 @@ mod in_mem { // Test directory removal fs.rmdir(ROOT, path).expect("Failed to remove directory"); assert!( - fs.open(ROOT, path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open( + ROOT, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU + ) + .is_err(), "Directory should not exist" ); } @@ -184,14 +240,27 @@ mod in_mem { // Test file creation let path = "/tmp/testfile"; let fd = fs - .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + 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(), + fs.open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU + ) + .is_err(), "File should not exist" ); } @@ -204,7 +273,13 @@ mod in_mem { // Create and write to a file let path = "/tmp/testfile"; let mut fd = fs - .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to create file"); let data = b"Hello, world!"; fs.write(&mut fd, data, None) @@ -215,7 +290,13 @@ mod in_mem { // Read from the file let mut fd = fs - .open(USER, path, OFlags::RDONLY, Mode::RWXU) + .open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs @@ -242,7 +323,14 @@ mod in_mem { // Test directory removal fs.rmdir(USER, path).expect("Failed to remove directory"); assert!( - fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), + fs.open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU + ) + .is_err(), "Directory should not exist" ); } @@ -252,7 +340,13 @@ mod in_mem { let fs = in_mem_fs(); let fd = fs - .open(ROOT, "/", OFlags::RDONLY, Mode::empty()) + .open( + ROOT, + "/", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open root directory"); let entries = fs .read_dir(&fd) @@ -278,7 +372,8 @@ mod in_mem { .open( ROOT, "/testfile1", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("Failed to create file1"); @@ -287,7 +382,8 @@ mod in_mem { .open( ROOT, "/testfile2", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("Failed to create file2"); @@ -295,7 +391,13 @@ mod in_mem { // Read root directory let fd = fs - .open(ROOT, "/", OFlags::RDONLY, Mode::empty()) + .open( + ROOT, + "/", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open root directory"); let entries = fs.read_dir(&fd).expect("Failed to read directory"); drop(fd); @@ -328,7 +430,13 @@ mod in_mem { // Read the subdirectory (should be empty) let fd = fs - .open(ROOT, "/testdir", OFlags::RDONLY, Mode::empty()) + .open( + ROOT, + "/testdir", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open subdirectory"); let entries = fs .read_dir(&fd) @@ -348,7 +456,8 @@ mod in_mem { .open( ROOT, "/testfile", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("Failed to create file"); @@ -356,7 +465,13 @@ mod in_mem { // Try to read_dir on the file (should fail) let fd = fs - .open(ROOT, "/testfile", OFlags::RDONLY, Mode::empty()) + .open( + ROOT, + "/testfile", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file"); assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); } @@ -376,7 +491,8 @@ mod in_mem { .open( ROOT, "/rootdir/file", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("Failed to create file"); @@ -392,7 +508,8 @@ mod in_mem { fs.open( USER, "/rootdir/new", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU ), Err(OpenError::NoWritePerms) @@ -415,7 +532,8 @@ mod in_mem { .open( USER, "/opendir/new", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("Failed to create file"); @@ -435,7 +553,13 @@ mod in_mem { // Create a test file as root let path = "/testfile"; let fd = fs - .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + ROOT, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to create file"); drop(fd); @@ -491,7 +615,8 @@ mod in_mem { .open( USER, "/testfile", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("Failed to create file"); @@ -502,7 +627,8 @@ mod in_mem { .open( USER, "/testdir", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .expect("Failed to open directory with O_DIRECTORY"); @@ -513,7 +639,8 @@ mod in_mem { fs.open( USER, "/testfile", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -524,7 +651,8 @@ mod in_mem { fs.open( USER, "/nonexistent", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) @@ -536,7 +664,8 @@ mod in_mem { .open( USER, "/newfile", - OFlags::CREAT | OFlags::WRONLY | OFlags::DIRECTORY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::DIRECTORY, Mode::RWXU, ) .expect("Failed to create file with O_CREAT | O_DIRECTORY"); @@ -563,7 +692,8 @@ mod in_mem { .open( USER, "/newfile", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ) .expect("Failed to create new file with O_CREAT | O_EXCL"); @@ -578,7 +708,8 @@ mod in_mem { fs.open( USER, "/newfile", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -589,7 +720,8 @@ mod in_mem { .open( USER, "/newfile", - OFlags::EXCL | OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::EXCLUSIVE, Mode::empty(), ) .expect("Failed to open existing file with O_EXCL (without O_CREAT)"); @@ -604,7 +736,13 @@ mod in_mem { // Test O_CREAT without O_EXCL on existing file (should succeed) let fd = fs - .open(USER, "/newfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + USER, + "/newfile", + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to open existing file with O_CREAT (without O_EXCL)"); drop(fd); @@ -615,7 +753,8 @@ mod in_mem { fs.open( USER, "/testdir", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -630,7 +769,13 @@ mod in_mem { // Create a file and write some initial content let path = "/testfile"; let mut fd = fs - .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to create file"); let initial_data = b"Hello, world! This is initial content."; fs.write(&mut fd, initial_data, None) @@ -639,7 +784,13 @@ mod in_mem { // Verify initial content was written let mut fd = fs - .open(USER, path, OFlags::RDONLY, Mode::empty()) + .open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file for reading"); let mut buffer = vec![0; initial_data.len()]; let bytes_read = fs @@ -651,7 +802,13 @@ mod in_mem { // Test O_TRUNC with O_WRONLY - should truncate file let mut fd = fs - .open(USER, path, OFlags::WRONLY | OFlags::TRUNC, Mode::empty()) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::TRUNCATE, + Mode::empty(), + ) .expect("Failed to open file with O_TRUNC | O_WRONLY"); // Write new content to the truncated file @@ -662,7 +819,13 @@ mod in_mem { // Verify the file was truncated and contains only new content let mut fd = fs - .open(USER, path, OFlags::RDONLY, Mode::empty()) + .open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file for verification"); let mut buffer = vec![0; initial_data.len()]; let bytes_read = fs @@ -674,14 +837,26 @@ mod in_mem { // Test O_TRUNC with O_RDWR - should also truncate let mut fd = fs - .open(USER, path, OFlags::WRONLY, Mode::empty()) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file for writing"); fs.write(&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()) + .open( + USER, + path, + FileAccessMode::ReadWrite, + FileOpenFlags::TRUNCATE, + Mode::empty(), + ) .expect("Failed to open file with O_TRUNC | O_RDWR"); // File should be empty after truncation @@ -711,7 +886,13 @@ mod in_mem { world_writable_root(&fs); let mut fd = fs - .open(USER, "/truncfile", OFlags::CREAT | OFlags::RDWR, Mode::RWXU) + .open( + USER, + "/truncfile", + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to create file"); fs.write(&mut fd, b"0123456789", None) .expect("Failed to write file"); @@ -743,7 +924,8 @@ mod in_mem { .open( USER, "/posfile", - OFlags::CREAT | OFlags::RDWR, + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, Mode::RWXU | Mode::RWXG | Mode::RWXO, ) .expect("open failed"); @@ -788,7 +970,13 @@ mod in_mem { /// 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) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to create file"); fs.write(&mut fd, data, None) .expect("Failed to write initial content"); @@ -815,7 +1003,13 @@ mod in_mem { // Re-open with O_APPEND and write more data let mut fd = fs - .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::APPEND, + Mode::empty(), + ) .expect("Failed to open file with O_APPEND"); fs.write(&mut fd, b" World", None) .expect("Failed to append data"); @@ -823,7 +1017,13 @@ mod in_mem { // Verify the file contains both pieces of data concatenated let mut fd = fs - .open(USER, path, OFlags::RDONLY, Mode::empty()) + .open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file for reading"); assert_eq!(read_all(&fs, &mut fd), b"Hello World"); } @@ -839,7 +1039,13 @@ mod in_mem { // Re-open with O_APPEND let mut fd = fs - .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::APPEND, + Mode::empty(), + ) .expect("Failed to open file with O_APPEND"); // Seek to beginning - this should succeed but writes should still append @@ -853,7 +1059,13 @@ mod in_mem { // Verify the file content: original data followed by appended data let mut fd = fs - .open(USER, path, OFlags::RDONLY, Mode::empty()) + .open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file for reading"); assert_eq!(read_all(&fs, &mut fd), b"ABCDEF123"); } @@ -869,7 +1081,13 @@ mod in_mem { // Re-open with O_RDWR | O_APPEND let mut fd = fs - .open(USER, path, OFlags::RDWR | OFlags::APPEND, Mode::empty()) + .open( + USER, + path, + FileAccessMode::ReadWrite, + FileOpenFlags::APPEND, + Mode::empty(), + ) .expect("Failed to open file with O_RDWR | O_APPEND"); // Read should work normally from the beginning @@ -900,7 +1118,13 @@ mod in_mem { // Re-open with O_APPEND let mut fd = fs - .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::APPEND, + Mode::empty(), + ) .expect("Failed to open file with O_APPEND"); // pwrite (write with explicit offset) should ignore O_APPEND per POSIX @@ -909,7 +1133,13 @@ mod in_mem { // Verify the file content: XX should be at position 2, not appended let mut fd = fs - .open(USER, path, OFlags::RDONLY, Mode::empty()) + .open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file for reading"); assert_eq!(read_all(&fs, &mut fd), b"ABXXEF"); } @@ -928,7 +1158,8 @@ mod in_mem { .open( USER, path, - OFlags::WRONLY | OFlags::TRUNC | OFlags::APPEND, + FileAccessMode::WriteOnly, + FileOpenFlags::TRUNCATE | FileOpenFlags::APPEND, Mode::empty(), ) .expect("Failed to open file with O_TRUNC | O_APPEND"); @@ -942,14 +1173,164 @@ mod in_mem { // Verify the file content let mut fd = fs - .open(USER, path, OFlags::RDONLY, Mode::empty()) + .open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file for reading"); assert_eq!(read_all(&fs, &mut fd), b"NewContent"); } + + #[test] + fn creation_flags_do_not_change_access_mode() { + for (access, can_read, can_write) in [ + (FileAccessMode::ReadOnly, true, false), + (FileAccessMode::WriteOnly, false, true), + (FileAccessMode::ReadWrite, true, true), + ] { + let fs = in_mem_fs(); + let mut entry = fs + .open( + USER, + "/file", + access, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE | FileOpenFlags::APPEND, + Mode::RWXU, + ) + .unwrap(); + + let read = fs.read(&mut entry, &mut [0], None); + if can_read { + assert_eq!(read.unwrap(), 0); + } else { + assert!(matches!(read, Err(ReadError::NotForReading))); + } + let write = fs.write(&mut entry, b"x", None); + if can_write { + assert_eq!(write.unwrap(), 1); + } else { + assert!(matches!(write, Err(WriteError::NotForWriting))); + } + } + } + + #[test] + fn read_only_open_can_still_truncate() { + let fs = in_mem_fs(); + create_with_content(&fs, "/file", b"contents"); + let mut entry = fs + .open( + USER, + "/file", + FileAccessMode::ReadOnly, + FileOpenFlags::TRUNCATE, + Mode::empty(), + ) + .unwrap(); + assert_eq!(fs.handle_status(&entry).unwrap().size, 0); + assert!(matches!( + fs.write(&mut entry, b"x", None), + Err(WriteError::NotForWriting) + )); + } + + #[test] + fn path_only_ignores_access_and_mutating_flags() { + let fs = in_mem_fs(); + create_with_content(&fs, "/file", b"contents"); + fs.chmod(USER, "/file", Mode::empty()).unwrap(); + let flags = FileOpenFlags::all() - FileOpenFlags::DIRECTORY; + + for access in [ + FileAccessMode::ReadOnly, + FileAccessMode::WriteOnly, + FileAccessMode::ReadWrite, + ] { + let mut entry = fs.open(USER, "/file", access, flags, Mode::RWXU).unwrap(); + assert!(entry.is_path_only()); + assert!(entry.allows_read()); + assert_eq!(fs.handle_status(&entry).unwrap().size, 8); + assert!(matches!( + fs.write(&mut entry, b"x", None), + Err(WriteError::NotForWriting) + )); + assert!(matches!( + fs.truncate(&mut entry, 0, false), + Err(TruncateError::NotForWriting) + )); + assert!(matches!( + fs.open(USER, "/missing", access, flags, Mode::RWXU), + Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) + )); + assert!(matches!( + fs.open( + USER, + "/file", + access, + flags | FileOpenFlags::DIRECTORY, + Mode::empty(), + ), + Err(OpenError::PathError(PathError::ComponentNotADirectory)) + )); + } + } + + #[test] + fn paths_are_normalized_before_lookup_and_mutation() { + let fs = in_mem_fs(); + fs.mkdir(USER, "/dir", Mode::RWXU).unwrap(); + create_with_content(&fs, "/dir/file", b"contents"); + let expected = fs.file_status(USER, "/dir/file").unwrap(); + for path in [ + "/dir//./missing/../file/", + "dir/../../dir/file", + "../../dir/file", + ] { + let entry = fs + .open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) + .unwrap(); + assert_eq!(fs.handle_status(&entry).unwrap(), expected); + } + let root = fs.file_status(USER, "/").unwrap(); + for path in ["", ".", "../..", "/dir/../../.."] { + assert_eq!(fs.file_status(USER, path).unwrap(), root); + } + + fs.mkdir(USER, "/dir/missing/../new", Mode::RWXU).unwrap(); + let entry = fs + .open( + USER, + "dir/new/./missing/../file", + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, + Mode::RWXU, + ) + .unwrap(); + fs.chmod(USER, "dir/new//file", Mode::RUSR).unwrap(); + fs.chown(USER, "dir/new/unused/../file", None, Some(ROOT.group)) + .unwrap(); + let status = fs.handle_status(&entry).unwrap(); + assert_eq!(status.mode, Mode::RUSR); + assert_eq!(status.owner.group, ROOT.group); + fs.unlink(USER, "/dir/new/missing/../file").unwrap(); + fs.rmdir(USER, "//dir/./new/").unwrap(); + assert!(fs.file_status(USER, "/dir/new").is_err()); + } } mod tar_ro { - use super::{FileType, Mode, OFlags, TEST_TAR_FILE, USER, tar_ro_fs}; + use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; + + use super::{FileType, Mode, TEST_TAR_FILE, USER, tar_ro_fs}; use crate::fs::errors::{OpenError, PathError, ReadDirError}; use alloc::vec; use alloc::vec::Vec; @@ -958,7 +1339,13 @@ mod tar_ro { fn file_read() { let fs = tar_ro_fs(TEST_TAR_FILE.into()); let mut fd = fs - .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -968,7 +1355,13 @@ mod tar_ro { drop(fd); let mut fd = fs - .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "bar/baz", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -981,11 +1374,23 @@ mod tar_ro { 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()), + fs.open( + USER, + "bar/ba", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty() + ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); - fs.open(USER, "bar", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open dir"); + fs.open( + USER, + "bar", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) + .expect("Failed to open dir"); } #[test] @@ -996,7 +1401,8 @@ mod tar_ro { fs.open( USER, "bar", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .expect("Failed to open directory with O_DIRECTORY"); @@ -1006,7 +1412,8 @@ mod tar_ro { fs.open( USER, "foo", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1017,7 +1424,8 @@ mod tar_ro { fs.open( USER, "nonexistent", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) @@ -1028,7 +1436,8 @@ mod tar_ro { fs.open( USER, "bar/baz", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1039,21 +1448,56 @@ mod tar_ro { 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] { + for (access, flags) in [ + (FileAccessMode::WriteOnly, FileOpenFlags::empty()), + (FileAccessMode::ReadWrite, FileOpenFlags::empty()), + (FileAccessMode::ReadOnly, FileOpenFlags::TRUNCATE), + ] { assert!(matches!( - fs.open(USER, "bar", flags, Mode::empty()), + fs.open(USER, "bar", access, flags, Mode::empty()), Err(OpenError::ReadOnlyFileSystem) )); } } + #[test] + fn path_only_does_not_request_write_access_on_read_only_backend() { + let fs = tar_ro_fs(TEST_TAR_FILE.into()); + for path in ["/", "bar", "foo"] { + let expected = fs.file_status(USER, path).unwrap(); + for access in [FileAccessMode::WriteOnly, FileAccessMode::ReadWrite] { + let entry = fs + .open( + USER, + path, + access, + FileOpenFlags::PATH + | FileOpenFlags::CREATE + | FileOpenFlags::EXCLUSIVE + | FileOpenFlags::TRUNCATE + | FileOpenFlags::APPEND, + Mode::empty(), + ) + .unwrap(); + assert!(entry.is_path_only()); + assert_eq!(fs.handle_status(&entry).unwrap(), expected); + } + } + } + #[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()) + .open( + USER, + "/", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open root directory"); let entries = fs.read_dir(&fd).expect("Failed to read root directory"); drop(fd); @@ -1084,7 +1528,13 @@ mod tar_ro { // Read `bar` directory let fd = fs - .open(USER, "bar", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "bar", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open bar directory"); let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); @@ -1099,16 +1549,23 @@ mod tar_ro { let fs = tar_ro_fs(TEST_TAR_FILE.into()); let fd = fs - .open(USER, "foo", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open foo file"); assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); } } mod overlay { + use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; + use super::{ - FileType, Fs, Mode, OFlags, Overlay, SeekWhence, TEST_TAR_FILE, TestPlatform, USER, - UserInfo, + FileType, Fs, Mode, Overlay, SeekWhence, TEST_TAR_FILE, TestPlatform, USER, UserInfo, }; use crate::fs::errors::{FileStatusError, OpenError, PathError, RmdirError}; use crate::fs::in_mem::{InMem, InitialNode}; @@ -1148,7 +1605,13 @@ mod overlay { fn file_read_from_lower() { let fs = overlay_fs(upper([])); let mut fd = fs - .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1165,7 +1628,13 @@ mod overlay { assert_eq!(stat.mode, Mode::from_bits(0o777).unwrap()); let mut fd = fs - .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "bar/baz", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1181,11 +1650,23 @@ mod overlay { fn dir_and_nonexist_checks() { let fs = overlay_fs(upper([])); assert!(matches!( - fs.open(USER, "bar/ba", OFlags::RDONLY, Mode::empty()), + fs.open( + USER, + "bar/ba", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty() + ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); - fs.open(USER, "bar", OFlags::RDONLY, Mode::empty()) - .expect("Failed to open dir"); + fs.open( + USER, + "bar", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + 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 @@ -1194,10 +1675,22 @@ mod overlay { fn file_read_write_copy_up() { let fs = overlay_fs(upper([])); let mut fd1 = fs - .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut fd2 = fs - .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; @@ -1224,10 +1717,22 @@ mod overlay { fn file_read_write_copy_up_keeps_position() { let fs = overlay_fs(upper([])); let mut fd1 = fs - .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut fd2 = fs - .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1250,7 +1755,13 @@ mod overlay { fn file_deletion() { let fs = overlay_fs(upper([])); let mut fd = fs - .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1273,7 +1784,13 @@ mod overlay { // 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()), + fs.open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty() + ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); } @@ -1302,7 +1819,8 @@ mod overlay { fs.open( USER, "bar", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .expect("Failed to open lower layer directory with O_DIRECTORY"); @@ -1311,7 +1829,8 @@ mod overlay { fs.open( USER, "/upperdir", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .expect("Failed to open upper layer directory with O_DIRECTORY"); @@ -1321,7 +1840,8 @@ mod overlay { fs.open( USER, "foo", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1332,7 +1852,8 @@ mod overlay { fs.open( USER, "/upperfile", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1343,7 +1864,8 @@ mod overlay { fs.open( USER, "bar/baz", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1354,7 +1876,8 @@ mod overlay { fs.open( USER, "nonexistent", - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) @@ -1367,7 +1890,13 @@ mod overlay { fn file_create_exist_in_lower() { let fs = overlay_fs(upper([])); let mut fd = fs - .open(USER, "foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1384,7 +1913,13 @@ mod overlay { // Read bar subdirectory let fd = fs - .open(USER, "bar", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "bar", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open bar directory"); let entries = fs.read_dir(&fd).expect("Failed to read bar directory"); @@ -1420,7 +1955,13 @@ mod overlay { // Read root directory (should contain entries from both layers) let fd = fs - .open(USER, "/", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open root directory"); let entries = fs.read_dir(&fd).expect("Failed to read root directory"); drop(fd); @@ -1456,7 +1997,13 @@ mod overlay { // Read upperdir directory (should be from upper layer) let fd = fs - .open(USER, "/upperdir", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/upperdir", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open upperdir"); let entries = fs.read_dir(&fd).expect("Failed to read upperdir"); @@ -1474,7 +2021,8 @@ mod overlay { fs.open( USER, "foo", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -1485,7 +2033,8 @@ mod overlay { .open( USER, "/newfile", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ) .expect("Failed to create new file with O_CREAT | O_EXCL"); @@ -1499,7 +2048,8 @@ mod overlay { fs.open( USER, "/newfile", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -1511,7 +2061,8 @@ mod overlay { fs.open( USER, "bar", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -1527,7 +2078,8 @@ mod overlay { .open( USER, "foo", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ) .expect("Failed to create file over tombstone with O_CREAT | O_EXCL"); @@ -1538,7 +2090,13 @@ mod overlay { // Verify the new content let mut fd = fs - .open(USER, "foo", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open recreated file"); let mut buffer = vec![0; 15]; let bytes_read = fs @@ -1553,7 +2111,8 @@ mod overlay { .open( USER, "/upper_only_file", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU, ) .expect("Failed to create upper layer file"); @@ -1566,7 +2125,8 @@ mod overlay { fs.open( USER, "/upper_only_file", - OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -1589,7 +2149,13 @@ mod overlay { // Verify we can open the directory let fd = fs - .open(USER, "/bar/test", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/bar/test", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /bar/test directory"); let entries = fs .read_dir(&fd) @@ -1609,7 +2175,13 @@ mod overlay { // 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) + .open( + USER, + "bar/test", + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + Mode::RWXU, + ) .expect("Failed to open bar/test for writing"); // Write data to the file @@ -1620,7 +2192,13 @@ mod overlay { // Read the file back let mut fd = fs - .open(USER, "bar/test", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "bar/test", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open bar/test for reading"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1643,7 +2221,13 @@ mod overlay { // 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) + .open( + USER, + "bar/baz", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open bar/baz for writing"); // Write new data to the file (overwriting existing content) @@ -1654,7 +2238,13 @@ mod overlay { // Read the file back to verify it was modified let mut fd = fs - .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "bar/baz", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open bar/baz for reading"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1677,7 +2267,13 @@ mod overlay { // 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()) + .open( + USER, + "foo", + FileAccessMode::ReadWrite, + FileOpenFlags::TRUNCATE, + Mode::empty(), + ) .expect("Failed to open file with O_TRUNC"); // File should be truncated (empty) @@ -1694,7 +2290,13 @@ mod overlay { // Verify the content persists let mut fd = fs - .open(USER, "foo", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to reopen file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1740,7 +2342,8 @@ mod overlay { .open( USER, "/upper_dir/file", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU | Mode::RWXG, ) .expect("create file in upper_dir failed"); @@ -1784,7 +2387,8 @@ mod overlay { .open( USER, "/regular_file", - OFlags::CREAT | OFlags::WRONLY, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, Mode::RWXU | Mode::RWXG, ) .expect("create file failed"); @@ -1811,7 +2415,13 @@ mod overlay { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let mut fd = fs - .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) + .open( + USER, + "foo", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::RWXU, + ) .expect("Failed to open file for writing"); fs.write(&mut fd, b"x", None) .expect("Failed to write to file"); @@ -1825,7 +2435,9 @@ mod overlay { } mod devices { - use super::{Fs, Mode, OFlags, RecordingStdio, USER, UnservicedStdio}; + use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; + + use super::{Fs, Mode, RecordingStdio, USER, UnservicedStdio}; use crate::fs::composer::Composer; use crate::fs::devices::Devices; use crate::fs::errors::{OpenError, PathError, ReadError, WriteError}; @@ -1849,7 +2461,13 @@ mod devices { let stdio = UnservicedStdio; let mut fd_stdout = fs - .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .open( + USER, + "/dev/stdout", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stdout"); assert!(matches!( fs.write_with(&stdio, &mut fd_stdout, b"", None), @@ -1862,7 +2480,13 @@ mod devices { drop(fd_stdout); let mut fd_stderr = fs - .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .open( + USER, + "/dev/stderr", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stderr"); assert!(matches!( fs.write_with(&stdio, &mut fd_stderr, b"", None), @@ -1875,7 +2499,13 @@ mod devices { drop(fd_stderr); let mut fd_stdin = fs - .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/dev/stdin", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stdin"); assert!(matches!( fs.read_with(&stdio, &mut fd_stdin, &mut [], None), @@ -1895,7 +2525,13 @@ mod devices { let stdio = RecordingStdio::new(b"host input"); let mut fd_stdout = fs - .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .open( + USER, + "/dev/stdout", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stdout"); assert_eq!( fs.write_with(&stdio, &mut fd_stdout, b"out", None) @@ -1904,7 +2540,13 @@ mod devices { ); let mut fd_stderr = fs - .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .open( + USER, + "/dev/stderr", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stderr"); assert_eq!( fs.write_with(&stdio, &mut fd_stderr, b"err", None) @@ -1921,7 +2563,13 @@ mod devices { ); let mut fd_stdin = fs - .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/dev/stdin", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stdin"); let mut buffer = vec![0; 16]; let read = fs @@ -1946,14 +2594,22 @@ mod devices { // Attempt to open a non-/dev/* path assert!(matches!( - fs.open(USER, "foo", OFlags::RDONLY, Mode::empty()), + fs.open( + USER, + "foo", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty() + ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) )); } } mod composed { - use super::{Fs, InMem, Mode, OFlags, TestPlatform, USER, UnservicedStdio, UserInfo}; + use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; + + use super::{Fs, InMem, Mode, TestPlatform, USER, UnservicedStdio, UserInfo}; use crate::fs::composer::Composer; use crate::fs::devices::Devices; use crate::fs::errors::{ReadError, WriteError}; @@ -1984,7 +2640,13 @@ mod composed { let stdio = UnservicedStdio; let mut fd_stdout = fs - .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .open( + USER, + "/dev/stdout", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stdout"); assert!(matches!( fs.write_with(&stdio, &mut fd_stdout, b"", None), @@ -1997,7 +2659,13 @@ mod composed { drop(fd_stdout); let mut fd_stderr = fs - .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .open( + USER, + "/dev/stderr", + FileAccessMode::WriteOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stderr"); assert!(matches!( fs.write_with(&stdio, &mut fd_stderr, b"", None), @@ -2010,7 +2678,13 @@ mod composed { drop(fd_stderr); let mut fd_stdin = fs - .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .open( + USER, + "/dev/stdin", + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::empty(), + ) .expect("Failed to open /dev/stdin"); assert!(matches!( fs.read_with(&stdio, &mut fd_stdin, &mut [], None), @@ -2030,14 +2704,27 @@ mod composed { // Test file creation let path = "/testfile"; let fd = fs - .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) + .open( + USER, + path, + FileAccessMode::WriteOnly, + FileOpenFlags::CREATE, + 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(), + fs.open( + USER, + path, + FileAccessMode::ReadOnly, + FileOpenFlags::empty(), + Mode::RWXU + ) + .is_err(), "File should not exist" ); } diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index 6da30741f3..05732523ce 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -13,6 +13,9 @@ use thiserror::Error; use crate::ObjectHandle; use crate::shared_buffer::{SHARED_BUFFER_SLOT_SIZE, SharedBufferDescriptor}; +mod path; +pub use path::ResolvedPath; + /// Maximum bytes transferred through one fs shared-buffer request. /// /// This remains independent of slot capacity so increasing the shared-buffer @@ -197,85 +200,46 @@ bitflags! { } } -/// ABI-neutral fs open flags. -/// -/// These values are intentionally independent of target-specific `O_*` bit -/// assignments. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct FileOpenFlags(u16); - -impl FileOpenFlags { - /// No open flags. - pub const NONE: Self = Self(0); - /// Create the object when it does not exist. - pub const CREATE: Self = Self(1 << 0); - /// Truncate an existing regular file. - pub const TRUNCATE: Self = Self(1 << 1); - /// Do not assign a controlling terminal. - pub const NO_CONTROLLING_TERMINAL: Self = Self(1 << 2); - /// Require creation to fail when the object already exists. - pub const EXCLUSIVE: Self = Self(1 << 3); - /// Require the opened object to be a directory. - pub const DIRECTORY: Self = Self(1 << 4); - /// Request nonblocking operation. - pub const NONBLOCKING: Self = Self(1 << 5); - /// Allow large-file operation. - pub const LARGE_FILE: Self = Self(1 << 6); - /// Do not follow the final symbolic link. - pub const NO_FOLLOW: Self = Self(1 << 7); - /// Append writes to the end of the file. - 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 - | Self::TRUNCATE.0 - | Self::NO_CONTROLLING_TERMINAL.0 - | Self::EXCLUSIVE.0 - | Self::DIRECTORY.0 - | Self::NONBLOCKING.0 - | Self::LARGE_FILE.0 - | Self::NO_FOLLOW.0 - | Self::APPEND.0 - | Self::PATH.0, - ); - - /// Creates flags 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 - } - } - - /// Returns the stable protocol bits. - #[must_use] - pub const fn bits(self) -> u16 { - self.0 - } - - /// Returns whether all flags in `other` are present. - #[must_use] - pub const fn contains(self, other: Self) -> bool { - self.0 & other.0 == other.0 - } - - /// Returns the union of two flag sets. - #[must_use] - pub const fn union(self, other: Self) -> Self { - Self(self.0 | other.0) - } -} - -impl core::ops::BitOr for FileOpenFlags { - type Output = Self; - - fn bitor(self, rhs: Self) -> Self::Output { - Self(self.0 | rhs.0) +bitflags! { + /// ABI-neutral fs open flags. + /// + /// These values are intentionally independent of target-specific `O_*` bit assignments. + #[repr(transparent)] + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] + pub struct FileOpenFlags: u16 { + /// No open flags. + const NONE = 0; + /// Create the object when it does not exist. + const CREATE = 1 << 0; + /// Truncate an existing regular file. + const TRUNCATE = 1 << 1; + /// Do not assign a controlling terminal. + const NO_CONTROLLING_TERMINAL = 1 << 2; + /// Require creation to fail when the object already exists. + const EXCLUSIVE = 1 << 3; + /// Require the opened object to be a directory. + const DIRECTORY = 1 << 4; + /// Request nonblocking operation. + const NONBLOCKING = 1 << 5; + /// Allow large-file operation. + const LARGE_FILE = 1 << 6; + /// Do not follow the final symbolic link. + const NO_FOLLOW = 1 << 7; + /// Append writes to the end of the file. + const APPEND = 1 << 8; + /// Open only for path-based operations. + const PATH = 1 << 9; + /// Every open flag this protocol version defines. + const SUPPORTED = Self::CREATE.bits() + | Self::TRUNCATE.bits() + | Self::NO_CONTROLLING_TERMINAL.bits() + | Self::EXCLUSIVE.bits() + | Self::DIRECTORY.bits() + | Self::NONBLOCKING.bits() + | Self::LARGE_FILE.bits() + | Self::NO_FOLLOW.bits() + | Self::APPEND.bits() + | Self::PATH.bits(); } } @@ -726,6 +690,30 @@ mod tests { use super::*; use alloc::vec; + #[test] + fn file_open_flags_preserve_wire_bits_and_reject_unknown_bits() { + for (flag, bits) in [ + (FileOpenFlags::NONE, 0), + (FileOpenFlags::CREATE, 0x001), + (FileOpenFlags::TRUNCATE, 0x002), + (FileOpenFlags::NO_CONTROLLING_TERMINAL, 0x004), + (FileOpenFlags::EXCLUSIVE, 0x008), + (FileOpenFlags::DIRECTORY, 0x010), + (FileOpenFlags::NONBLOCKING, 0x020), + (FileOpenFlags::LARGE_FILE, 0x040), + (FileOpenFlags::NO_FOLLOW, 0x080), + (FileOpenFlags::APPEND, 0x100), + (FileOpenFlags::PATH, 0x200), + (FileOpenFlags::SUPPORTED, 0x3ff), + ] { + assert_eq!(flag.bits(), bits); + assert_eq!(FileOpenFlags::from_bits(bits), Some(flag)); + } + assert_eq!(FileOpenFlags::all(), FileOpenFlags::SUPPORTED); + assert_eq!(FileOpenFlags::from_bits(0x400), None); + assert_eq!(FileOpenFlags::from_bits(u16::MAX), None); + } + #[test] fn directory_payload_round_trips_all_entry_shapes() { let entries = vec![ diff --git a/litebox_broker_protocol/src/fs/path.rs b/litebox_broker_protocol/src/fs/path.rs new file mode 100644 index 0000000000..a5bdcef27d --- /dev/null +++ b/litebox_broker_protocol/src/fs/path.rs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::string::String; +use alloc::vec::Vec; + +/// An absolute, lexically normalized fs path. +/// +/// Normalization removes redundant separators and `.` components, and processes `..` without +/// going above the root. It does not perform filesystem lookup, resolve symbolic links, or +/// establish that the path exists or is accessible. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ResolvedPath { + components: Vec, +} + +impl ResolvedPath { + /// The root path. + #[must_use] + pub const fn root() -> Self { + Self { + components: Vec::new(), + } + } + + /// Normalize `path`, using this path as the base when `path` is relative. + #[must_use] + pub fn resolve(&self, path: &str) -> Self { + let mut components = if path.starts_with('/') { + Vec::new() + } else { + self.components.clone() + }; + for component in path.split('/') { + match component { + "" | "." => {} + ".." => { + let _ = components.pop(); + } + _ => components.push(component.into()), + } + } + Self { components } + } + + /// Normalized components, excluding the root separator. + #[must_use] + pub fn components(&self) -> &[String] { + &self.components + } + + /// The parent components and final name, or `None` for the root. + #[must_use] + pub fn parent_and_name(&self) -> Option<(&[String], &str)> { + let (name, parent) = self.components.split_last()?; + Some((parent, name.as_str())) + } +} + +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(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString as _; + + #[test] + fn paths_are_absolute_and_lexically_normalized() { + let base = ResolvedPath::root().resolve("/work/dir"); + for (path, expected) in [ + ("", "/work/dir"), + (".", "/work/dir"), + ("./file", "/work/dir/file"), + ("../file", "/work/file"), + ("sub/../../file", "/work/file"), + ("../../../../file", "/file"), + ("/etc//passwd/", "/etc/passwd"), + ("/.././", "/"), + ("////", "/"), + ] { + let resolved = base.resolve(path); + assert_eq!(resolved.to_string(), expected); + assert_eq!(base.resolve(&resolved.to_string()), resolved); + } + assert_eq!(base.to_string(), "/work/dir"); + assert_eq!(ResolvedPath::default(), ResolvedPath::root()); + assert_eq!(ResolvedPath::root().to_string(), "/"); + } + + #[test] + fn components_and_parent_preserve_normalized_names() { + let path = ResolvedPath::root().resolve("/a//b/../c"); + assert_eq!(path.components(), ["a", "c"]); + let (parent, name) = path.parent_and_name().unwrap(); + assert_eq!(parent, ["a"]); + assert_eq!(name, "c"); + + let path = ResolvedPath::root().resolve("/file"); + let (parent, name) = path.parent_and_name().unwrap(); + assert!(parent.is_empty()); + assert_eq!(name, "file"); + assert_eq!(ResolvedPath::root().parent_and_name(), None); + } +} diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 13df35d32e..aeb478abaf 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -6,13 +6,10 @@ #![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}; @@ -61,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. @@ -683,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; } @@ -1435,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; } @@ -2118,7 +2212,7 @@ pub enum SyscallRequest { Dup { oldfd: i32, newfd: Option, - flags: Option, + flags: Option, }, Socket { domain: u32, @@ -2279,7 +2373,7 @@ pub enum SyscallRequest { Openat { dirfd: i32, pathname: UserPtr, - flags: litebox::fs::OFlags, + flags: OFlags, mode: FileMode, }, Ftruncate { @@ -2309,7 +2403,7 @@ pub enum SyscallRequest { }, Pipe2 { pipefd: UserPtrMut, - flags: litebox::fs::OFlags, + flags: OFlags, }, Clone { args: CloneArgs, @@ -2624,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 { @@ -2869,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), } } @@ -3419,7 +3511,7 @@ reinterpret_truncated_from_usize_for! { MRemapFlags, AccessFlags, FileMode, - litebox::fs::OFlags, + OFlags, AtFlags, SockFlags, SendFlags, @@ -3478,6 +3570,96 @@ impl ReinterpretUsizeAsPtr> for Option(), + core::mem::size_of::() + ); + for (flag, bits) in [ + (OFlags::RDONLY, 0), + (OFlags::WRONLY, 0x1), + (OFlags::RDWR, 0x2), + (OFlags::CREAT, 0x40), + (OFlags::EXCL, 0x80), + (OFlags::NOCTTY, 0x100), + (OFlags::TRUNC, 0x200), + (OFlags::APPEND, 0x400), + (OFlags::NDELAY, 0x800), + (OFlags::NONBLOCK, 0x800), + (OFlags::DSYNC, 0x1000), + (OFlags::ASYNC, 0x2000), + (OFlags::NOATIME, 0x40000), + (OFlags::CLOEXEC, 0x80000), + (OFlags::SYNC, 0x101000), + (OFlags::PATH, 0x200000), + ] { + assert_eq!(flag.bits(), bits, "{flag:?}"); + } + #[cfg(target_arch = "x86_64")] + { + assert_eq!(OFlags::DIRECT.bits(), 0x4000); + assert_eq!(OFlags::DIRECTORY.bits(), 0x10000); + assert_eq!(OFlags::LARGEFILE.bits(), 0x8000); + assert_eq!(OFlags::NOFOLLOW.bits(), 0x20000); + assert_eq!(OFlags::TMPFILE.bits(), 0x410000); + assert_eq!(OFlags::STATUS_FLAGS_MASK.bits(), 0x34fc03); + } + #[cfg(target_arch = "aarch64")] + { + assert_eq!(OFlags::DIRECT.bits(), 0x10000); + assert_eq!(OFlags::DIRECTORY.bits(), 0x4000); + assert_eq!(OFlags::LARGEFILE.bits(), 0x20000); + assert_eq!(OFlags::NOFOLLOW.bits(), 0x8000); + assert_eq!(OFlags::TMPFILE.bits(), 0x404000); + assert_eq!(OFlags::STATUS_FLAGS_MASK.bits(), 0x373c03); + } + assert_eq!(EfdFlags::CLOEXEC.bits(), OFlags::CLOEXEC.bits()); + assert_eq!(EfdFlags::NONBLOCK.bits(), OFlags::NONBLOCK.bits()); + assert_eq!(SockFlags::CLOEXEC.bits(), OFlags::CLOEXEC.bits()); + assert_eq!(SockFlags::NONBLOCK.bits(), OFlags::NONBLOCK.bits()); + assert_eq!( + EpollCreateFlags::EPOLL_CLOEXEC.bits(), + OFlags::CLOEXEC.bits() + ); + } + + #[test] + fn open_flags_preserve_unknown_bits_in_linux_argument_decoding() { + let bits = 0x8000_0000 | OFlags::NONBLOCK.bits() | 3; + assert_eq!(OFlags::from_bits(bits).unwrap().bits(), bits); + assert_eq!(OFlags::from_bits_truncate(bits).bits(), bits); + assert_eq!( + OFlags::reinterpret_truncated_from_usize(bits as usize).bits(), + bits + ); + let Some(FcntlArg::SETFL(flags)) = FcntlArg::try_from(F_SETFL, bits as usize) else { + panic!("F_SETFL must retain unknown Linux flags"); + }; + assert_eq!(flags.bits(), bits); + } + + #[test] + fn pipe_flags_translate_only_nonblocking_status() { + assert_eq!( + OFlags::from(litebox::pipes::Flags::empty()), + OFlags::empty() + ); + assert_eq!( + OFlags::from(litebox::pipes::Flags::NON_BLOCKING), + OFlags::NONBLOCK + ); + assert_eq!( + OFlags::from(litebox::pipes::Flags::from_bits_retain(u32::MAX)), + OFlags::NONBLOCK + ); + } +} + #[cfg(test)] mod file_status_tests { use super::*; diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 13170228b5..e97c865f95 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_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index c0e53a6d3d..8b74ceb08d 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -28,9 +28,11 @@ use litebox::{ sync::futex::FutexManager, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _}, }; -use litebox_broker_protocol::fs::{FileMode as Mode, FileSeekWhence as SeekWhence}; +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}, }; @@ -359,7 +361,7 @@ impl LinuxShimProcess { // 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( @@ -367,18 +369,35 @@ impl syscalls::file::FilesState { global: &GlobalState, context: &litebox::fs::Context, ) { - use litebox::fs::OFlags; let stdin = self .fs - .open_file(context, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + .open_file( + context, + "/dev/stdin", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); let stdout = self .fs - .open_file(context, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + .open_file( + context, + "/dev/stdout", + FileAccessMode::WriteOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); let stderr = self .fs - .open_file(context, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + .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(); diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index e79b56b603..9060e00cb9 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -5,12 +5,11 @@ use alloc::{ffi::CString, vec::Vec}; use litebox::{ - fs::OFlags, mm::linux::{CreatePagesFlags, MappingError, PAGE_SIZE}, utils::ReinterpretSignedExt, }; use litebox_broker_protocol::fs::FileMode as Mode; -use litebox_common_linux::{MapFlags, errno::Errno, loader::ElfParsedFile}; +use litebox_common_linux::{MapFlags, OFlags, errno::Errno, loader::ElfParsedFile}; use thiserror::Error; use crate::{ diff --git a/litebox_shim_linux/src/stdio.rs b/litebox_shim_linux/src/stdio.rs index 3e118fc85c..edf55aa9c2 100644 --- a/litebox_shim_linux/src/stdio.rs +++ b/litebox_shim_linux/src/stdio.rs @@ -7,9 +7,10 @@ mod tests { use core::ffi::CStr; - use litebox::fs::OFlags; use litebox_broker_protocol::fs::FileMode as Mode; - use litebox_common_linux::{FcntlArg, FileDescriptorFlags, IoctlArg, Termios, errno::Errno}; + use litebox_common_linux::{ + FcntlArg, FileDescriptorFlags, IoctlArg, OFlags, Termios, errno::Errno, + }; use crate::{UserPtrMut, syscalls::tests::init_platform}; diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 9a640ccf61..533b9086f2 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -698,7 +698,7 @@ mod test { 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(); @@ -753,7 +753,7 @@ mod test { 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(); @@ -793,7 +793,7 @@ mod test { 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 b641a4de07..ee969d1870 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}; diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 3490b1c3ff..d66fad383f 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -11,18 +11,19 @@ use alloc::{ use litebox::{ event::{Events, wait::WaitError}, fd::{FdEnabledSubsystem, MetadataError, TypedFd}, - fs::OFlags, + fs::errors::OpenError, mm::linux::PAGE_SIZE, path, stdio::StdioStream, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; use litebox_broker_protocol::fs::{ - FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, FileType, FileUser, + 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; @@ -92,6 +93,51 @@ fn file_mode_from_linux(mode: u32) -> Mode { Mode::from_bits_retain(bits) } +/// 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. @@ -391,9 +437,14 @@ impl Task { let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); + 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)?; files .fs - .open_file(&context, &path, flags - OFlags::CLOEXEC, mode) + .open_file(&context, path, access, open_flags, mode) .map_err(Errno::from) }?; if let Some(stream) = stream { @@ -2810,10 +2861,81 @@ mod tests { use super::*; use alloc::string::String; use core::cell::Cell; - use litebox::fs::OFlags; extern crate std; + #[test] + fn open_flags_decode_access_mode_separately() { + for (flags, access) in [ + (OFlags::RDONLY, FileAccessMode::ReadOnly), + (OFlags::WRONLY, FileAccessMode::WriteOnly), + (OFlags::RDWR, FileAccessMode::ReadWrite), + ] { + assert_eq!( + file_open_options(flags).unwrap(), + (access, FileOpenFlags::NONE) + ); + } + assert!(matches!( + file_open_options(OFlags::from_bits_retain(3)), + Err(OpenError::AccessNotAllowed) + )); + } + + #[test] + fn open_flags_translate_each_supported_flag_and_path_combinations() { + let mut combined = OFlags::RDWR; + for (linux, normalized) 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), + ] { + assert_eq!( + file_open_options(linux | OFlags::WRONLY).unwrap(), + (FileAccessMode::WriteOnly, normalized) + ); + combined |= linux; + } + assert_eq!( + file_open_options(combined).unwrap(), + (FileAccessMode::ReadWrite, FileOpenFlags::SUPPORTED) + ); + assert!(matches!( + file_open_options(OFlags::PATH | OFlags::from_bits_retain(3)), + Err(OpenError::AccessNotAllowed) + )); + } + + #[test] + fn open_flags_keep_unsupported_flags_unimplemented() { + for flags in [ + OFlags::ASYNC, + OFlags::DIRECT, + OFlags::DSYNC, + OFlags::SYNC, + OFlags::NOATIME, + OFlags::TMPFILE, + OFlags::CLOEXEC, + OFlags::from_bits_retain(0x8000_0000), + ] { + assert!( + std::panic::catch_unwind(|| file_open_options(flags)).is_err(), + "{flags:?} must remain unsupported" + ); + assert!( + std::panic::catch_unwind(|| file_open_options(flags | OFlags::PATH)).is_err(), + "O_PATH must not hide unsupported flags" + ); + } + } + #[test] fn stat_overflow_does_not_narrow_internal_metadata_or_statx() { use crate::syscalls::test_broker::{Scripted, closed, opened, status}; @@ -3273,7 +3395,7 @@ mod tests { let fd = task .sys_open( "file.txt", - litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, + OFlags::CREAT | OFlags::WRONLY, Mode::RUSR | Mode::WUSR, ) .unwrap(); @@ -3296,7 +3418,7 @@ mod tests { .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(); diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index e974109df1..7d21454fe9 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -1549,9 +1549,9 @@ impl Task { #[cfg(test)] mod tests { - use litebox::{fs::OFlags, mm::linux::PAGE_SIZE, 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, ProtFlags, errno::Errno}; + 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}; diff --git a/litebox_shim_linux/src/syscalls/mod.rs b/litebox_shim_linux/src/syscalls/mod.rs index b5e00db5e7..d6502202f0 100644 --- a/litebox_shim_linux/src/syscalls/mod.rs +++ b/litebox_shim_linux/src/syscalls/mod.rs @@ -21,13 +21,15 @@ 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 a42bf9e61b..c09625ee57 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( diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs index cc339b0946..e04ae0a523 100644 --- a/litebox_shim_linux/src/syscalls/pipe.rs +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -12,11 +12,10 @@ use core::num::NonZero; use litebox::{ event::{IOPollable, wait::WaitContext}, fd::MetadataError, - fs::OFlags, pipes::{Flags, HalfPipeType, PipeFd}, }; use litebox_broker_protocol::fs::FileMode as Mode; -use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; +use litebox_common_linux::{FileDescriptorFlags, InodeType, OFlags, errno::Errno}; use crate::{GlobalState, ShimPlatform}; diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index ddf3c2733b..e257fa62d3 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1402,7 +1402,7 @@ impl Task { let full_path = self.resolve_path(&path)?; let file = self.do_open( full_path, - litebox::fs::OFlags::RDONLY, + litebox_common_linux::OFlags::RDONLY, litebox_broker_protocol::fs::FileMode::empty(), )?; let mut header = [0u8; SHEBANG_MAX_LINE]; diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 2d71e1aa66..5d3557b66b 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -12,7 +12,6 @@ use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; -use litebox::fs::OFlags; use litebox_broker_protocol::ObjectHandle; use litebox_broker_protocol::fs::WriteFileResponse; use litebox_broker_protocol::fs::{ @@ -20,7 +19,9 @@ use litebox_broker_protocol::fs::{ FileType, FileUser, MAX_FILE_TRANSFER_SIZE, encode_directory_entries_chunk, }; use litebox_broker_protocol::message::FileResponse; -use litebox_common_linux::{AtFlags, DirentType, FcntlArg, FileDescriptorFlags, errno::Errno}; +use litebox_common_linux::{ + AtFlags, DirentType, FcntlArg, FileDescriptorFlags, OFlags, errno::Errno, +}; use zerocopy::FromBytes as _; use crate::UserPtrMut; @@ -532,6 +533,83 @@ fn getdirent64_translates_descriptor_and_broker_errors() { close_scripted_file(&files, &task, dir_fd, FILE_HANDLE); } +#[test] +fn open_flags_keep_cloexec_local_and_send_normalized_path_options() { + let cloexec_handle = ObjectHandle(FILE_HANDLE.0 + 1); + let (files, task) = scripted_task([opened(FILE_HANDLE), opened(cloexec_handle)]); + let flags = + OFlags::RDWR | OFlags::PATH | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::NONBLOCK; + let plain_fd = i32::try_from(task.sys_open("/path", flags, Mode::empty()).unwrap()).unwrap(); + let cloexec_fd = i32::try_from( + task.sys_openat( + litebox_common_linux::AT_FDCWD, + "/path", + flags | OFlags::CLOEXEC, + Mode::empty(), + ) + .unwrap(), + ) + .unwrap(); + let expected = FileCall::Open { + path: "/path".into(), + user: ROOT, + access: FileAccessMode::ReadWrite, + flags: FileOpenFlags::PATH + | FileOpenFlags::DIRECTORY + | FileOpenFlags::NO_FOLLOW + | FileOpenFlags::NONBLOCKING, + mode: Mode::empty(), + }; + let calls = files.take_calls(); + assert_eq!(calls.len(), 2); + for call in calls { + assert_eq!(call, expected); + } + assert_eq!(task.sys_fcntl(plain_fd, FcntlArg::GETFD), Ok(0)); + assert_eq!( + task.sys_fcntl(cloexec_fd, FcntlArg::GETFD), + Ok(FileDescriptorFlags::FD_CLOEXEC.bits()) + ); + assert_eq!( + task.sys_fcntl(cloexec_fd, FcntlArg::GETFL).unwrap() & OFlags::CLOEXEC.bits(), + 0 + ); + + files.script([closed()]); + task.close_on_exec(); + assert_eq!(files.take_calls(), vec![FileCall::Close(cloexec_handle)]); + assert_eq!( + task.sys_fcntl(cloexec_fd, FcntlArg::GETFD), + Err(Errno::EBADF) + ); + assert_eq!(task.sys_fcntl(plain_fd, FcntlArg::GETFD), Ok(0)); + close_scripted_file(&files, &task, plain_fd, FILE_HANDLE); +} + +#[test] +fn open_flags_reject_invalid_access_before_contacting_broker() { + let (files, task) = scripted_task([]); + for flags in [ + OFlags::from_bits_retain(3), + OFlags::from_bits_retain(3) | OFlags::PATH | OFlags::CLOEXEC, + ] { + assert_eq!( + task.sys_open("/invalid_access", flags, Mode::empty()), + Err(Errno::EACCES) + ); + assert_eq!( + task.sys_openat( + litebox_common_linux::AT_FDCWD, + "/invalid_access", + flags, + Mode::empty(), + ), + Err(Errno::EACCES) + ); + } + assert!(files.take_calls().is_empty()); +} + #[test] fn umask_masks_the_creation_mode_sent_to_the_broker() { let (files, task) = scripted_task([]); diff --git a/litebox_shim_linux/src/syscalls/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index 31338ad012..b43f24654d 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -21,13 +21,13 @@ use litebox::{ wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, - fs::{OFlags, errors::OpenError}, + fs::errors::OpenError, sync::{Mutex, RwLock}, utils::TruncateExt as _, }; -use litebox_broker_protocol::fs::FileMode as Mode; +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, }; @@ -113,9 +113,9 @@ 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 = { @@ -127,6 +127,7 @@ impl UnixSocketAddr { .open_file( &context, path.as_str(), + FileAccessMode::ReadWrite, flags, Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, ) diff --git a/litebox_shim_windows/src/loader/pe.rs b/litebox_shim_windows/src/loader/pe.rs index 4b0f296bd4..a33a9d1ce2 100644 --- a/litebox_shim_windows/src/loader/pe.rs +++ b/litebox_shim_windows/src/loader/pe.rs @@ -10,13 +10,12 @@ use core::{ use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::utils::TruncateExt as _; use litebox::{ - fs::OFlags, mm::linux::{ CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, VmemProtectError, }, platform::RawPointerProvider, }; -use litebox_broker_protocol::fs::FileMode as Mode; +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, @@ -1115,7 +1114,8 @@ impl PeImageFile { let fd = fs.open_file( &litebox::fs::Context::new(), path, - OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), )?; Ok(Self { fs, fd }) diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index ac8ef3bd8c..119c78c280 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -9,7 +9,6 @@ use core::mem::{align_of, offset_of, size_of}; use int_enum::IntEnum; use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; -use litebox::fs::OFlags; use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, SeekError, WriteError, @@ -17,7 +16,8 @@ use litebox::fs::errors::{ use litebox::platform::{RawConstPointer as _, RawMutPointer as _, RawPointerProvider}; use litebox::utils::TruncateExt as _; use litebox_broker_protocol::fs::{ - FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, FileType, + FileAccessMode, FileMode as Mode, FileOpenFlags, FileSeekWhence as SeekWhence, FileStatus, + FileType, }; use litebox_common_windows::nt_status::NtStatus; use zerocopy::byteorder::native_endian::U32; @@ -678,7 +678,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!( @@ -688,32 +688,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.insert(FileOpenFlags::TRUNCATE); } CreateDisposition::Supersede | CreateDisposition::OverwriteIf => { - flags.insert(OFlags::CREAT | OFlags::TRUNC); + flags.insert(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE); } - CreateDisposition::Create => flags.insert(OFlags::CREAT | OFlags::EXCL), - CreateDisposition::OpenIf => flags.insert(OFlags::CREAT), + CreateDisposition::Create => { + flags.insert(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE); + } + CreateDisposition::OpenIf => flags.insert(FileOpenFlags::CREATE), CreateDisposition::Open => {} } if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { - flags.insert(OFlags::DIRECTORY); + flags.insert(FileOpenFlags::DIRECTORY); } if create_options.contains(FileCreateOptions::NON_DIRECTORY_FILE) { - flags.insert(OFlags::NOFOLLOW); + flags.insert(FileOpenFlags::NO_FOLLOW); } - flags + (access, flags) } fn conflicts_with_share(self, share_access: FileShareAccess) -> bool { @@ -2356,10 +2360,10 @@ impl Task { { 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_file(&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.file_status(&fd) { Ok(file_status) => file_status, @@ -2430,10 +2434,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_file(&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(( @@ -2808,6 +2812,103 @@ mod tests { const FILE_CREATE: u32 = 2; const FILE_OVERWRITE: u32 = 4; + #[test] + fn file_access_and_options_map_to_protocol_open_parameters() { + for (desired_access, expected_access) in [ + (0, FileAccessMode::ReadOnly), + (FileAccess::SYNCHRONIZE.bits(), FileAccessMode::ReadOnly), + (FileAccess::READ_DATA.bits(), FileAccessMode::ReadOnly), + (FileAccess::READ_ATTRIBUTES.bits(), FileAccessMode::ReadOnly), + (FileAccess::EXECUTE.bits(), FileAccessMode::ReadOnly), + (FileAccess::WRITE_DATA.bits(), FileAccessMode::WriteOnly), + (FileAccess::APPEND_DATA.bits(), FileAccessMode::WriteOnly), + (FileAccess::DELETE.bits(), FileAccessMode::WriteOnly), + ( + (FileAccess::READ_DATA | FileAccess::APPEND_DATA).bits(), + FileAccessMode::ReadWrite, + ), + (AccessMask::GENERIC_READ.bits(), FileAccessMode::ReadOnly), + (AccessMask::GENERIC_WRITE.bits(), FileAccessMode::WriteOnly), + (AccessMask::GENERIC_EXECUTE.bits(), FileAccessMode::ReadOnly), + (AccessMask::GENERIC_ALL.bits(), FileAccessMode::ReadWrite), + ] { + for (options, expected_flags) in [ + (FileCreateOptions::empty(), FileOpenFlags::NONE), + (FileCreateOptions::DIRECTORY_FILE, FileOpenFlags::DIRECTORY), + ( + FileCreateOptions::NON_DIRECTORY_FILE, + FileOpenFlags::NO_FOLLOW, + ), + ( + FileCreateOptions::WRITE_THROUGH + | FileCreateOptions::SYNCHRONOUS_IO_NONALERT + | FileCreateOptions::OPEN_REPARSE_POINT, + FileOpenFlags::NONE, + ), + ] { + assert_eq!( + FileAccess::from_desired_access(desired_access) + .open_flags(CreateDisposition::Open, options), + (expected_access, expected_flags), + "desired_access={desired_access:#x}, options={options:?}", + ); + } + } + } + + #[test] + fn create_dispositions_map_to_protocol_flags_and_truncation_access() { + for (desired_access, open_access, truncate_access) in [ + ( + FileAccess::empty(), + FileAccessMode::ReadOnly, + FileAccessMode::WriteOnly, + ), + ( + FileAccess::READ_DATA, + FileAccessMode::ReadOnly, + FileAccessMode::ReadWrite, + ), + ( + FileAccess::WRITE_DATA, + FileAccessMode::WriteOnly, + FileAccessMode::WriteOnly, + ), + ] { + for (disposition, expected_flags, truncates) in [ + (CreateDisposition::Open, FileOpenFlags::NONE, false), + ( + CreateDisposition::Create, + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + false, + ), + (CreateDisposition::OpenIf, FileOpenFlags::CREATE, false), + (CreateDisposition::Overwrite, FileOpenFlags::TRUNCATE, true), + ( + CreateDisposition::Supersede, + FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE, + true, + ), + ( + CreateDisposition::OverwriteIf, + FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE, + true, + ), + ] { + let expected_access = if truncates { + truncate_access + } else { + open_access + }; + assert_eq!( + desired_access.open_flags(disposition, FileCreateOptions::empty()), + (expected_access, expected_flags), + "desired_access={desired_access:?}, disposition={disposition:?}", + ); + } + } + } + #[test] fn status_metadata_preserves_inode_bits_and_saturates_signed_lengths() { let status = FileStatus { @@ -2862,7 +2963,8 @@ mod tests { .open_file( &task.fs_context, path, - OFlags::CREAT | OFlags::RDWR, + FileAccessMode::ReadWrite, + FileOpenFlags::CREATE, Mode::RUSR | Mode::WUSR, ) .unwrap(); @@ -3344,7 +3446,13 @@ mod tests { let fd = task .fs - .open_file(&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_file(&fd, &mut contents, Some(0)).unwrap(), 5); @@ -4370,13 +4478,12 @@ 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) ); } diff --git a/litebox_shim_windows/src/syscalls/nls.rs b/litebox_shim_windows/src/syscalls/nls.rs index bc62d18320..bbe85afae2 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::fs::OFlags; use litebox::fs::errors::{FileStatusError, OpenError, PathError, ReadError}; 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::{FileMode as Mode, FileType}; +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}; @@ -777,7 +776,8 @@ impl Task { .open_file( &self.fs_context, path.as_str(), - OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), ) .map_err(map_nls_open_error)?; diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 059b4f1152..3b3d69f5f3 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -40,14 +40,13 @@ use litebox::event::{ polling::{Pollee, TryOpError}, }; use litebox::fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}; -use litebox::fs::OFlags; use litebox::fs::errors::{ FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, WriteError, }; use litebox::platform::{RawConstPointer as _, RawMutPointer as _}; use litebox::sync::Mutex; use litebox::utils::TruncateExt; -use litebox_broker_protocol::fs::{FileMode as Mode, FileType}; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags, FileType}; use litebox_common_windows::nt_status::NtStatus; use zerocopy::{FromBytes, Immutable, IntoBytes}; @@ -394,19 +393,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) } } @@ -642,8 +639,9 @@ impl RegistryStore { path: &str, desired_access: RegistryKeyAccess, ) -> Result { + let (access, flags) = desired_access.open_flags(); self.fs() - .open_file(&self.fs_context, path, desired_access.into(), Mode::empty()) + .open_file(&self.fs_context, path, access, flags, Mode::empty()) .map_err(map_open_error) } @@ -674,7 +672,8 @@ impl RegistryStore { .open_file( &self.fs_context, &*value_path, - OFlags::RDONLY, + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, Mode::empty(), ) .map_err(map_open_error)?; @@ -794,7 +793,8 @@ impl RegistryStore { .open_file( &self.fs_context, &*values_path, - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; @@ -864,7 +864,8 @@ impl RegistryStore { .open_file( &self.fs_context, &*child_path, - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; @@ -894,7 +895,8 @@ impl RegistryStore { .open_file( &self.fs_context, &*values_path, - OFlags::RDONLY | OFlags::DIRECTORY, + FileAccessMode::ReadOnly, + FileOpenFlags::DIRECTORY, Mode::empty(), ) .map_err(map_open_error)?; @@ -2277,7 +2279,8 @@ fn write_value_at_path( .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)?; @@ -2500,6 +2503,55 @@ mod tests { const KEY_VALUE_PARTIAL_INFORMATION_DATA_OFFSET: usize = offset_of!(KeyValuePartialInformation, data); + #[test] + fn registry_access_maps_to_protocol_directory_open_parameters() { + for (desired_access, expected_access) in [ + (0, FileAccessMode::ReadOnly), + ( + RegistryKeyAccess::QUERY_VALUE.bits(), + FileAccessMode::ReadOnly, + ), + ( + RegistryKeyAccess::ENUMERATE_SUB_KEYS.bits(), + FileAccessMode::ReadOnly, + ), + (RegistryKeyAccess::NOTIFY.bits(), FileAccessMode::ReadOnly), + ( + RegistryKeyAccess::SET_VALUE.bits(), + FileAccessMode::WriteOnly, + ), + ( + RegistryKeyAccess::CREATE_SUB_KEY.bits(), + FileAccessMode::WriteOnly, + ), + ( + RegistryKeyAccess::CREATE_LINK.bits(), + FileAccessMode::WriteOnly, + ), + ( + (RegistryKeyAccess::QUERY_VALUE | RegistryKeyAccess::SET_VALUE).bits(), + FileAccessMode::ReadWrite, + ), + (AccessMask::DELETE.bits(), FileAccessMode::WriteOnly), + (AccessMask::WRITE_DAC.bits(), FileAccessMode::WriteOnly), + (AccessMask::WRITE_OWNER.bits(), FileAccessMode::WriteOnly), + (AccessMask::GENERIC_READ.bits(), FileAccessMode::ReadOnly), + (AccessMask::GENERIC_WRITE.bits(), FileAccessMode::WriteOnly), + (AccessMask::GENERIC_EXECUTE.bits(), FileAccessMode::ReadOnly), + (AccessMask::GENERIC_ALL.bits(), FileAccessMode::ReadWrite), + ( + AccessMask::MAXIMUM_ALLOWED.bits(), + FileAccessMode::ReadWrite, + ), + ] { + assert_eq!( + RegistryKeyAccess::from_desired_access(desired_access).open_flags(), + (expected_access, FileOpenFlags::DIRECTORY), + "desired_access={desired_access:#x}", + ); + } + } + #[cfg(all(target_os = "windows", target_arch = "x86_64"))] #[allow(non_snake_case)] #[link(name = "advapi32")] From 2f436e24ac99b1fbf06c7b5a8e0e347a6196c36c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 16:07:44 -0700 Subject: [PATCH 14/34] Remove local file backend coupling Remove the obsolete guest 9P transport and broker-core dependency, drop dead file backing APIs, and simplify Windows file and registry integration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- Cargo.lock | 1 - litebox/Cargo.toml | 1 - litebox/src/fs/errors.rs | 10 -- litebox/src/fs/file.rs | 8 +- litebox/src/fs/mod.rs | 9 +- litebox_shim_linux/src/lib.rs | 12 -- litebox_shim_linux/src/syscalls/mm.rs | 111 +-------------- litebox_shim_linux/src/transport.rs | 127 ------------------ litebox_shim_windows/src/lib.rs | 22 ++- litebox_shim_windows/src/syscalls/file.rs | 94 ++++++------- litebox_shim_windows/src/syscalls/registry.rs | 66 ++++----- 11 files changed, 83 insertions(+), 378 deletions(-) delete mode 100644 litebox_shim_linux/src/transport.rs diff --git a/Cargo.lock b/Cargo.lock index 88f98eec6d..971cda4eae 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", diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 219c915ac0..6a0996212f 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" } diff --git a/litebox/src/fs/errors.rs b/litebox/src/fs/errors.rs index a8de68d92c..49184f5c61 100644 --- a/litebox/src/fs/errors.rs +++ b/litebox/src/fs/errors.rs @@ -205,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 { diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index ceacd3591a..8a2bca3a8c 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -254,15 +254,9 @@ impl LiteBox { })? .map_err(file_status_error) } - - /// Get static backing data for a file, if available and supported. - pub fn get_static_file_backing_data(&self, fd: &FileFd) -> Option<&'static [u8]> { - let _ = self.broker_file(fd)?; - None - } } -/// Per-call resolution context. The user may hold and mutate this as they wish. +/// Caller-owned filesystem context containing a working directory and acting user. #[derive(Clone, Debug)] pub struct Context { cwd: Arc, diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index fafe2318f0..f2d25ecee5 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -4,18 +4,13 @@ //! Guest-facing file operations and local descriptor integration. //! //! Filesystem resolution and backend implementations live in `litebox_broker_core`. This module -//! retains caller context and local descriptors, plus the 9P transport traits that deployments -//! implement. Shared file values are defined in [`litebox_broker_protocol::fs`]. +//! retains caller context and local descriptors. Shared file values are defined in +//! [`litebox_broker_protocol::fs`]. pub mod errors; mod file; pub use file::{BrokerFile, Context, FileFd}; -#[doc(hidden)] -pub mod nine_p { - pub use litebox_broker_core::fs::nine_p::*; -} - #[cfg(test)] mod tests; diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 8b74ceb08d..88c3b8d56c 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -58,7 +58,6 @@ pub(crate) mod channel; pub mod loader; pub(crate) mod stdio; pub mod syscalls; -pub mod transport; mod wait; pub(crate) use litebox::fs::FileFd; @@ -317,17 +316,6 @@ 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 } diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 7d21454fe9..93f44de6db 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; @@ -212,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 { @@ -253,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_file_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, diff --git a/litebox_shim_linux/src/transport.rs b/litebox_shim_linux/src/transport.rs deleted file mode 100644 index 243fa62bf3..0000000000 --- a/litebox_shim_linux/src/transport.rs +++ /dev/null @@ -1,127 +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 { - let sockfd = global - .net - .lock() - .socket(litebox::net::Protocol::Tcp) - .map_err(Errno::from)?; - - let proxy = global.initialize_socket(&sockfd, SockType::Stream, SockFlags::empty()); - - 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(error) => return Err(Errno::from(error)), - } - } - - 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) => core::hint::spin_loop(), - Ok(read) => return Ok(read), - 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(written) => return Ok(written), - Err(ChannelWriteError::BufferFull) => core::hint::spin_loop(), - Err(_) => return Err(transport::WriteError), - } - } - } -} diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 85c48a0cb0..1b62cd0c48 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -32,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, }; @@ -2495,8 +2493,8 @@ impl Task { }; } - try_metadata!(FileObjectSubsystem); - try_metadata!(RegistryKeySubsystem); + try_metadata!(FileObject); + try_metadata!(RegistryKeyObject); try_metadata!(EventSubsystem); try_metadata!(MutantSubsystem); try_metadata!(SemaphoreSubsystem); @@ -2545,8 +2543,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); @@ -2717,8 +2715,8 @@ impl Task { }; } - try_duplicate!(FileObjectSubsystem); - try_duplicate!(RegistryKeySubsystem); + try_duplicate!(FileObject); + try_duplicate!(RegistryKeyObject); try_duplicate!(EventSubsystem); try_duplicate!(MutantSubsystem); try_duplicate!(SemaphoreSubsystem); @@ -2828,8 +2826,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); diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 119c78c280..af5597a329 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -4,7 +4,6 @@ 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; @@ -447,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 crate::WindowsHandleSubsystem - for FileObjectSubsystem -{ +impl crate::WindowsHandleSubsystem for FileObject { fn normalize_desired_access(desired_access: u32) -> u32 { FileAccess::from_desired_access(desired_access).bits() } @@ -528,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, ); @@ -826,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, @@ -839,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, @@ -882,13 +870,13 @@ impl Task { 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); }); } @@ -996,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!( @@ -1020,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() { @@ -1229,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 @@ -1321,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 @@ -1819,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(), ) { @@ -2493,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, @@ -3399,7 +3383,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) diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 3b3d69f5f3..5acb1ff88a 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Windows registry syscalls backed by a private file-system-shaped store (i.e., -//! an overlay file system with in-memory and tar backends). +//! Windows registry syscalls backed by LiteBox's broker-backed file APIs. //! //! Registry keys are represented as directories and values as files under each //! key's `.values` directory: @@ -24,7 +23,6 @@ //! This is only an implementation detail: syscall handlers must expose registry //! object semantics rather than file semantics. -use core::marker::PhantomData; use core::mem::{offset_of, size_of}; use alloc::collections::BTreeMap; @@ -58,19 +56,13 @@ use crate::nt_types::{ read_unicode_string_at, }; -type RegistryFileSystem = LiteBox; - -pub(crate) struct RegistryKeySubsystem(PhantomData); - -impl FdEnabledSubsystem for RegistryKeySubsystem { - type Entry = RegistryKeyObject; +impl FdEnabledSubsystem for RegistryKeyObject { + type Entry = Self; } 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() } @@ -82,7 +74,7 @@ pub(crate) struct RegistryKeyObject { } pub(crate) struct RegistryStore { - fs: RegistryFileSystem, + fs: LiteBox, fs_context: litebox::fs::Context, /// Whether the built-in keys and values have been written to [`Self::fs`]. /// @@ -623,7 +615,7 @@ impl RegistryStore { /// 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) -> &RegistryFileSystem { + fn fs(&self) -> &LiteBox { { let mut seeded = self.defaults_seeded.lock(); if !*seeded { @@ -922,7 +914,7 @@ impl RegistryStore { /// remaining defaults in that group, matching the behavior guests saw when the /// defaults were written during shim construction. fn seed_defaults( - fs: &RegistryFileSystem, + fs: &LiteBox, fs_context: &litebox::fs::Context, ) { for key in [ @@ -1089,8 +1081,8 @@ 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, @@ -1103,17 +1095,13 @@ impl Task { 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); }); } @@ -1410,7 +1398,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(), )?; @@ -1456,7 +1444,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(), ) { @@ -1502,7 +1490,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(), ) { @@ -1668,9 +1656,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(), )? @@ -1891,7 +1879,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(), )?; @@ -2001,7 +1989,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(), )?; @@ -2255,7 +2243,7 @@ fn is_valid_key_component(component: &str) -> bool { } fn write_value_in_fs( - fs: &RegistryFileSystem, + fs: &LiteBox, context: &litebox::fs::Context, key_nt_path: &str, value_name: &str, @@ -2267,7 +2255,7 @@ fn write_value_in_fs( } fn write_value_at_path( - fs: &RegistryFileSystem, + fs: &LiteBox, context: &litebox::fs::Context, key_path: &str, value_name: &str, @@ -2293,7 +2281,7 @@ fn write_value_at_path( } fn read_exact_at( - fs: &RegistryFileSystem, + fs: &LiteBox, fd: &litebox::fs::FileFd, mut data: &mut [u8], ) -> Result<(), NtStatus> { @@ -2312,7 +2300,7 @@ fn read_exact_at( } fn write_all_at( - fs: &RegistryFileSystem, + fs: &LiteBox, fd: &litebox::fs::FileFd, mut data: &[u8], mut offset: usize, @@ -2331,7 +2319,7 @@ fn write_all_at( } fn create_key_in_fs( - fs: &RegistryFileSystem, + fs: &LiteBox, context: &litebox::fs::Context, nt_path: &str, ) -> Result { @@ -2341,7 +2329,7 @@ fn create_key_in_fs( } fn create_key_path_in_fs( - fs: &RegistryFileSystem, + fs: &LiteBox, context: &litebox::fs::Context, path: &str, ) -> Result, NtStatus> { @@ -2366,7 +2354,7 @@ fn create_key_path_in_fs( } fn ensure_directory_in_fs( - fs: &RegistryFileSystem, + fs: &LiteBox, context: &litebox::fs::Context, path: &str, ) -> Result { From 78e679cd11d373690fb9fbc0810f3f2254092e8a Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 18:02:35 -0700 Subject: [PATCH 15/34] Remove LiteBox filesystem facade tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/mod.rs | 3 - litebox/src/fs/tests.rs | 851 ---------------------------------------- 2 files changed, 854 deletions(-) delete mode 100644 litebox/src/fs/tests.rs diff --git a/litebox/src/fs/mod.rs b/litebox/src/fs/mod.rs index f2d25ecee5..2b9d40bf7a 100644 --- a/litebox/src/fs/mod.rs +++ b/litebox/src/fs/mod.rs @@ -11,6 +11,3 @@ pub mod errors; mod file; pub use file::{BrokerFile, Context, FileFd}; - -#[cfg(test)] -mod tests; diff --git a/litebox/src/fs/tests.rs b/litebox/src/fs/tests.rs deleted file mode 100644 index c90a565519..0000000000 --- a/litebox/src/fs/tests.rs +++ /dev/null @@ -1,851 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Guest-facing file API tests. -//! -//! Filesystem resolution, backend, and 9P semantics belong to `litebox_broker_core` and are tested -//! there. What LiteBox owns is the guest side of the boundary: resolving paths against a -//! [`Context`], passing shared metadata through, mapping broker errors onto guest -//! error types, and tying broker-owned files to guest descriptors. These tests script broker -//! responses over a local channel, so no broker core, policy engine, or host transport is -//! involved. - -extern crate std; - -use alloc::collections::VecDeque; -use alloc::string::{String, ToString as _}; -use alloc::sync::Arc; -use alloc::vec; -use alloc::vec::Vec; -use core::num::NonZeroU64; -use std::sync::{Barrier, Mutex}; - -use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileError, FileMode, FileNodeInfo, FileOpenFlags, - FileSeekWhence, FileStatus, FileType, FileUser, MAX_FILE_TRANSFER_SIZE, OpenFileResponse, - ReadDirectoryResponse, ReadFileResponse, SeekFileResponse, WriteFileResponse, - encode_directory_entries_chunk, -}; -use litebox_broker_protocol::message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, - BrokerResponse, BrokerResult, FileRequest, FileResponse, -}; -use litebox_broker_protocol::shared_buffer::{ - SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferDescriptor, -}; -use litebox_broker_protocol::{BROKER_PROTOCOL_VERSION, ObjectHandle}; -use litebox_broker_transport::channel::{LocalCallChannel, LocalSetupChannel}; -use litebox_broker_transport::shared_memory::{SharedBufferPool, SharedMemory, SharedMemoryError}; - -use crate::fs::errors::{ - OpenError, PathError, ReadDirError, ReadError, RmdirError, UnlinkError, WriteError, -}; -use crate::fs::{BrokerFile, Context, FileFd}; -use crate::platform::mock::MockPlatform; - -/// The handle the scripted broker hands out for every successful open. -const FILE_HANDLE: ObjectHandle = ObjectHandle(7); - -/// One broker request LiteBox issued, with any shared-buffer payload copied out. -#[derive(Debug, PartialEq, Eq)] -enum Call { - 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, - }, - Seek { - handle: ObjectHandle, - offset: i64, - whence: FileSeekWhence, - }, - ReadDirectory { - handle: ObjectHandle, - start_index: u64, - }, - PathStatus { - path: String, - user: FileUser, - }, - HandleStatus(ObjectHandle), - Unlink { - path: String, - user: FileUser, - }, - Rmdir { - path: String, - user: FileUser, - }, - Close(ObjectHandle), -} - -/// One scripted broker answer. -enum Scripted { - /// Reply with this file response verbatim. - Reply(FileResponse), - /// Stage `data` in the request's shared buffer and report it as read. - Read(Vec), - /// Wait for the test to close the fd before completing this read. - BlockingRead { - data: Vec, - entered: Arc, - release: Arc, - }, - /// Answer directory reads from `entries`, at most `page_bytes` of them per response. - Directory { - entries: Vec, - page_bytes: usize, - }, -} - -/// A broker that records requests and answers them from a script. -struct ScriptedBroker { - buffers: SharedBufferPool>, - calls: Mutex>, - script: Mutex>, -} - -impl ScriptedBroker { - fn new(script: impl IntoIterator) -> Arc { - let memory = Arc::new(TestSharedMemory::new()); - Arc::new(Self { - buffers: SharedBufferPool::new(memory, SHARED_BUFFER_LAYOUT).unwrap(), - calls: Mutex::new(Vec::new()), - script: Mutex::new(script.into_iter().collect()), - }) - } - - /// Every request observed so far, oldest first. - fn calls(&self) -> std::sync::MutexGuard<'_, Vec> { - self.calls.lock().unwrap() - } - - fn record(&self, call: Call) { - self.calls.lock().unwrap().push(call); - } - - /// The UTF-8 path staged in `descriptor` by the guest. - fn staged_path(&self, descriptor: SharedBufferDescriptor) -> String { - let mut bytes = vec![0; descriptor.length as usize]; - self.buffers - .read(descriptor.slot_index, &mut bytes) - .unwrap(); - String::from_utf8(bytes).expect("guest must stage a UTF-8 path") - } - - fn staged_data(&self, descriptor: SharedBufferDescriptor) -> Vec { - let mut bytes = vec![0; descriptor.length as usize]; - self.buffers - .read(descriptor.slot_index, &mut bytes) - .unwrap(); - bytes - } - - fn next_scripted(&self) -> Scripted { - self.script - .lock() - .unwrap() - .pop_front() - .expect("test script must answer every file request") - } - - fn file_response(&self, request: FileRequest) -> FileResponse { - match request { - FileRequest::Open(request) => { - self.record(Call::Open { - path: self.staged_path(request.path), - user: request.user, - access: request.access, - flags: request.flags, - mode: request.mode, - }); - self.reply() - } - FileRequest::Read(request) => { - self.record(Call::Read { - handle: request.handle, - length: request.buffer.length, - offset: request.offset, - }); - match self.next_scripted() { - Scripted::Reply(response) => response, - Scripted::Read(data) => { - assert!(data.len() <= request.buffer.length as usize); - self.buffers - .write(request.buffer.slot_index, &data) - .unwrap(); - FileResponse::Read(ReadFileResponse { - read: u32::try_from(data.len()).unwrap(), - }) - } - Scripted::BlockingRead { - data, - entered, - release, - } => { - entered.wait(); - release.wait(); - assert!(data.len() <= request.buffer.length as usize); - self.buffers - .write(request.buffer.slot_index, &data) - .unwrap(); - FileResponse::Read(ReadFileResponse { - read: u32::try_from(data.len()).unwrap(), - }) - } - Scripted::Directory { .. } => panic!("scripted directory answer for a read"), - } - } - FileRequest::Write(request) => { - self.record(Call::Write { - handle: request.handle, - data: self.staged_data(request.buffer), - offset: request.offset, - }); - self.reply() - } - FileRequest::Seek(request) => { - self.record(Call::Seek { - handle: request.handle, - offset: request.offset, - whence: request.whence, - }); - self.reply() - } - FileRequest::ReadDirectory(request) => { - self.record(Call::ReadDirectory { - handle: request.handle, - start_index: request.start_index, - }); - let scripted = self.next_scripted(); - let Scripted::Directory { - entries, - page_bytes, - } = scripted - else { - let Scripted::Reply(response) = scripted else { - panic!("scripted read answer for a directory read") - }; - return response; - }; - let (payload, next_index) = encode_directory_entries_chunk( - &entries, - usize::try_from(request.start_index).unwrap(), - page_bytes.min(request.buffer.length as usize), - ) - .expect("directory entries must encode"); - self.buffers - .write(request.buffer.slot_index, &payload) - .unwrap(); - if next_index.is_some() { - // The guest asks again from the continuation index, so keep answering. - self.script.lock().unwrap().push_front(Scripted::Directory { - entries, - page_bytes, - }); - } - FileResponse::ReadDirectory(ReadDirectoryResponse { - length: u32::try_from(payload.len()).unwrap(), - next_index, - }) - } - FileRequest::PathStatus(request) => { - self.record(Call::PathStatus { - path: self.staged_path(request.path), - user: request.user, - }); - self.reply() - } - FileRequest::HandleStatus(request) => { - self.record(Call::HandleStatus(request.handle)); - self.reply() - } - FileRequest::Unlink(request) => { - self.record(Call::Unlink { - path: self.staged_path(request.path), - user: request.user, - }); - self.reply() - } - FileRequest::Rmdir(request) => { - self.record(Call::Rmdir { - path: self.staged_path(request.path), - user: request.user, - }); - self.reply() - } - request => panic!("unscripted file request: {request:?}"), - } - } - - fn reply(&self) -> FileResponse { - match self.next_scripted() { - Scripted::Reply(response) => response, - Scripted::Read(_) | Scripted::BlockingRead { .. } | Scripted::Directory { .. } => { - panic!("scripted payload answer for a request that carries none") - } - } - } -} - -/// The local end of the scripted broker connection. -struct ScriptedChannel(Arc); - -impl LocalSetupChannel for ScriptedChannel { - type Error = core::convert::Infallible; - - fn send_handshake_request( - &mut self, - request: &BrokerHandshakeRequest, - ) -> Result<(), Self::Error> { - assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); - Ok(()) - } - - fn recv_handshake_response(&mut self) -> Result, Self::Error> { - Ok(Some(BrokerHandshakeResponse::Negotiated { - broker_protocol_version: BROKER_PROTOCOL_VERSION, - })) - } -} - -impl LocalCallChannel for ScriptedChannel { - type Error = core::convert::Infallible; - - fn call(&self, request: BrokerRequest) -> Result { - let result = match request.operation { - BrokerOperation::File(file) => BrokerResult::File(self.0.file_response(file)), - BrokerOperation::CloseObject(handle) => { - self.0.record(Call::Close(handle)); - BrokerResult::ObjectClosed - } - operation => panic!("unscripted broker operation: {operation:?}"), - }; - Ok(BrokerResponse { - request_id: request.request_id, - result, - }) - } -} - -/// Shared memory backed by an ordinary allocation, since no peer process observes it. -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]) -> 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]) -> 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(()) - } -} - -/// Build a LiteBox whose broker answers from `script`. -fn scripted_fs( - script: impl IntoIterator, -) -> (Arc, crate::LiteBox) { - let broker = ScriptedBroker::new(script); - let memory = Arc::clone(broker.buffers.memory()); - let (local, ()) = BrokerLocal::negotiate(ScriptedChannel(Arc::clone(&broker)), |channel| { - Ok((channel, memory as Arc, ())) - }) - .unwrap(); - ( - broker, - crate::LiteBox::new_with_broker_local(MockPlatform::new(), local), - ) -} - -fn opened() -> Scripted { - Scripted::Reply(FileResponse::Open(OpenFileResponse { - handle: FILE_HANDLE, - })) -} - -fn user(context: &Context) -> FileUser { - FileUser { - user: context.acting_user().user, - group: context.acting_user().group, - } -} - -#[test] -fn context_resolves_paths_against_the_cwd() { - let mut context = Context::new(); - assert_eq!(context.cwd().to_string(), "/"); - assert_eq!(context.resolve("a/b/../c").unwrap().to_string(), "/a/c"); - - context.set_cwd(context.resolve("/work/dir").unwrap()); - assert_eq!(context.cwd().to_string(), "/work/dir"); - assert_eq!( - context.resolve("./file").unwrap().to_string(), - "/work/dir/file" - ); - assert_eq!( - context.resolve("../file").unwrap().to_string(), - "/work/file" - ); - assert_eq!( - context.resolve("/etc//passwd").unwrap().to_string(), - "/etc/passwd" - ); -} - -#[test] -fn open_sends_the_resolved_path_and_protocol_options() { - let mut context = Context::new(); - context.set_cwd(context.resolve("/work").unwrap()); - context.set_acting_user(FileUser { user: 7, group: 9 }); - let (broker, fs) = scripted_fs([opened()]); - - let fd = fs - .open_file( - &context, - "sub/../file.txt", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::APPEND, - FileMode::RWXU, - ) - .expect("open should succeed"); - - assert_eq!( - *broker.calls(), - vec![Call::Open { - path: String::from("/work/file.txt"), - user: FileUser { user: 7, group: 9 }, - access: FileAccessMode::WriteOnly, - flags: FileOpenFlags::CREATE | FileOpenFlags::APPEND, - mode: FileMode::from_bits(0o700).unwrap(), - }] - ); - fs.close_file(&fd).expect("close should succeed"); -} - -#[test] -fn open_rejects_unknown_protocol_flags_before_the_broker() { - let (broker, fs) = scripted_fs([]); - assert!(matches!( - fs.open_file( - &Context::new(), - "/file", - FileAccessMode::ReadOnly, - FileOpenFlags::from_bits_retain(1 << 15), - FileMode::empty(), - ), - Err(OpenError::AccessNotAllowed) - )); - assert!(broker.calls().is_empty()); -} - -#[test] -fn read_and_write_transfer_payloads_through_the_broker() { - let context = Context::new(); - let (broker, fs) = scripted_fs([ - opened(), - Scripted::Reply(FileResponse::Write(WriteFileResponse { written: 5 })), - Scripted::Read(b"broker".to_vec()), - Scripted::Reply(FileResponse::Seek(SeekFileResponse { offset: 3 })), - ]); - - let fd = fs - .open_file( - &context, - "/file", - FileAccessMode::ReadWrite, - FileOpenFlags::NONE, - FileMode::empty(), - ) - .expect("open should succeed"); - - assert_eq!(fs.write_file(&fd, b"hello", None).unwrap(), 5); - - let mut buffer = vec![0; 16]; - let read = fs.read_file(&fd, &mut buffer, Some(2)).unwrap(); - assert_eq!(&buffer[..read], b"broker"); - - assert_eq!( - fs.seek_file(&fd, -3, FileSeekWhence::RelativeToEnd) - .unwrap(), - 3 - ); - fs.close_file(&fd).expect("close should succeed"); - - let calls = broker.calls(); - assert_eq!( - calls[1], - Call::Write { - handle: FILE_HANDLE, - data: b"hello".to_vec(), - offset: None, - } - ); - assert_eq!( - calls[2], - Call::Read { - handle: FILE_HANDLE, - length: 16, - offset: Some(2), - } - ); - assert_eq!( - calls[3], - Call::Seek { - handle: FILE_HANDLE, - offset: -3, - whence: FileSeekWhence::RelativeToEnd, - } - ); - assert_eq!(calls[4], Call::Close(FILE_HANDLE)); -} - -#[test] -fn closing_releases_the_broker_object_and_the_descriptor() { - let context = Context::new(); - let (broker, fs) = scripted_fs([opened()]); - - let fd = fs - .open_file( - &context, - "/file", - FileAccessMode::ReadOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ) - .expect("open should succeed"); - fs.close_file(&fd).expect("close should succeed"); - assert_eq!(broker.calls()[1], Call::Close(FILE_HANDLE)); - - // The descriptor no longer names a broker file, so operations on it report a closed fd - // instead of reaching the broker. - let mut buffer = [0; 4]; - assert!(matches!( - fs.read_file(&fd, &mut buffer, None), - Err(ReadError::ClosedFd) - )); - assert!(matches!( - fs.write_file(&fd, b"x", None), - Err(WriteError::ClosedFd) - )); - assert_eq!(broker.calls().len(), 2); -} - -#[test] -fn broker_file_descriptors_preserve_subsystem_and_duplicate_lifetimes() { - let (broker, fs) = scripted_fs([opened(), Scripted::Read(b"broker".to_vec())]); - let fd: FileFd = fs - .open_file( - &Context::new(), - "/file", - FileAccessMode::ReadOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ) - .unwrap(); - let duplicate = fs.descriptor_table_mut().duplicate(&fd).unwrap(); - let mut raw_fds = crate::fd::RawDescriptorStorage::new(); - let raw_fd = raw_fds.fd_into_raw_integer(fd); - - assert!(matches!( - raw_fds.fd_from_raw_integer::>(raw_fd), - Err(crate::fd::ErrRawIntFd::InvalidSubsystem) - )); - let fd = raw_fds - .fd_consume_raw_integer::(raw_fd) - .unwrap(); - fs.close_file(&fd).unwrap(); - assert_eq!(broker.calls().len(), 1); - - let mut buffer = [0; 6]; - assert_eq!(fs.read_file(&duplicate, &mut buffer, None).unwrap(), 6); - assert_eq!(&buffer, b"broker"); - fs.close_file(&duplicate).unwrap(); - let calls = broker.calls(); - assert_eq!(calls.len(), 3); - assert_eq!(calls.last(), Some(&Call::Close(FILE_HANDLE))); -} - -#[test] -fn in_flight_read_keeps_the_broker_file_alive_after_close() { - let entered = Arc::new(Barrier::new(2)); - let release = Arc::new(Barrier::new(2)); - let (broker, fs) = scripted_fs([ - opened(), - Scripted::BlockingRead { - data: b"broker".to_vec(), - entered: Arc::clone(&entered), - release: Arc::clone(&release), - }, - ]); - let fd = Arc::new( - fs.open_file( - &Context::new(), - "/file", - FileAccessMode::ReadOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ) - .expect("open should succeed"), - ); - - let worker = fs.clone(); - let opened_file = Arc::clone(&fd); - let reader = std::thread::spawn(move || { - let mut buffer = [0; 8]; - let read = worker - .read_file(&opened_file, &mut buffer, None) - .expect("the in-flight read should succeed"); - buffer[..read].to_vec() - }); - - entered.wait(); - fs.close_file(&fd).expect("close should succeed"); - assert!( - !broker - .calls() - .iter() - .any(|call| matches!(call, Call::Close(FILE_HANDLE))), - "the broker object must remain open while the read holds a reference" - ); - - release.wait(); - assert_eq!(reader.join().unwrap(), b"broker"); - assert_eq!(broker.calls().last(), Some(&Call::Close(FILE_HANDLE))); -} - -#[test] -fn path_and_handle_status_preserve_protocol_metadata() { - let context = Context::new(); - let expected = FileStatus { - file_type: FileType::CharacterDevice, - mode: FileMode::from_bits(0o644).unwrap(), - size: u64::MAX, - owner: FileUser { user: 1, group: 2 }, - node_info: FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX, - rdev: Some(NonZeroU64::MAX), - }, - block_size: u64::MAX, - }; - let (broker, fs) = scripted_fs([ - Scripted::Reply(FileResponse::PathStatus(expected)), - opened(), - Scripted::Reply(FileResponse::HandleStatus(expected)), - ]); - - let status = fs - .path_file_status(&context, "/dev/null") - .expect("status should succeed"); - - assert_eq!(status, expected); - let fd = fs - .open_file( - &context, - "/dev/null", - FileAccessMode::ReadOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ) - .unwrap(); - assert_eq!(fs.file_status(&fd).unwrap(), expected); - fs.close_file(&fd).unwrap(); - assert_eq!( - *broker.calls(), - vec![ - Call::PathStatus { - path: String::from("/dev/null"), - user: user(&context), - }, - Call::Open { - path: String::from("/dev/null"), - user: user(&context), - access: FileAccessMode::ReadOnly, - flags: FileOpenFlags::NONE, - mode: FileMode::empty(), - }, - Call::HandleStatus(FILE_HANDLE), - Call::Close(FILE_HANDLE), - ] - ); -} - -#[test] -fn read_dir_reassembles_paged_broker_entries() { - let context = Context::new(); - let entries = vec![ - FileDirectoryEntry { - name: String::from("one"), - file_type: FileType::RegularFile, - node_info: Some(FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX, - rdev: None, - }), - }, - FileDirectoryEntry { - name: String::from("directory"), - file_type: FileType::Directory, - node_info: None, - }, - FileDirectoryEntry { - name: String::from("device"), - file_type: FileType::CharacterDevice, - node_info: Some(FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX, - rdev: Some(NonZeroU64::MAX), - }), - }, - ]; - // A page that holds one entry, so the guest has to follow the continuation index. - let page_bytes = - encode_directory_entries_chunk(&entries[2..], 0, MAX_FILE_TRANSFER_SIZE as usize) - .unwrap() - .0 - .len(); - let (broker, fs) = scripted_fs([ - opened(), - Scripted::Directory { - entries: entries.clone(), - page_bytes, - }, - ]); - - let fd = fs - .open_file( - &context, - "/dir", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, - FileMode::empty(), - ) - .expect("open should succeed"); - let received = fs - .read_file_directory(&fd) - .expect("read_dir should succeed"); - fs.close_file(&fd).expect("close should succeed"); - - assert_eq!(received, entries); - - let calls = broker.calls(); - assert_eq!( - calls[1], - Call::ReadDirectory { - handle: FILE_HANDLE, - start_index: 0, - } - ); - assert_eq!( - calls[2], - Call::ReadDirectory { - handle: FILE_HANDLE, - start_index: 1, - } - ); -} - -#[test] -fn broker_file_errors_map_to_guest_errors() { - let context = Context::new(); - let (broker, fs) = scripted_fs([ - Scripted::Reply(FileResponse::Failed(FileError::NoSuchFileOrDirectory)), - Scripted::Reply(FileResponse::Failed(FileError::AccessNotAllowed)), - Scripted::Reply(FileResponse::Failed(FileError::NoWritePermissions)), - Scripted::Reply(FileResponse::Failed(FileError::NotEmpty)), - opened(), - Scripted::Reply(FileResponse::Failed(FileError::NotForReading)), - Scripted::Reply(FileResponse::Failed(FileError::NotDirectory)), - ]); - - assert!(matches!( - fs.open_file( - &context, - "/missing", - FileAccessMode::ReadOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ), - Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) - )); - assert!(matches!( - fs.open_file( - &context, - "/secret", - FileAccessMode::ReadOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ), - Err(OpenError::AccessNotAllowed) - )); - assert!(matches!( - fs.unlink_file(&context, "/locked/file"), - Err(UnlinkError::NoWritePerms) - )); - assert!(matches!( - fs.rmdir_file(&context, "/full"), - Err(RmdirError::NotEmpty) - )); - - let fd = fs - .open_file( - &context, - "/file", - FileAccessMode::WriteOnly, - FileOpenFlags::NONE, - FileMode::empty(), - ) - .expect("open should succeed"); - let mut buffer = [0; 4]; - assert!(matches!( - fs.read_file(&fd, &mut buffer, None), - Err(ReadError::NotForReading) - )); - assert!(matches!( - fs.read_file_directory(&fd), - Err(ReadDirError::NotADirectory) - )); - fs.close_file(&fd).expect("close should succeed"); - - assert_eq!(broker.calls().len(), 8); -} From cc56269d6cc04534a179cbc492642aa79eef6d7f Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 18:18:40 -0700 Subject: [PATCH 16/34] Keep LiteBox ownership shim-local Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/litebox.rs | 8 +- litebox_shim_linux/src/lib.rs | 29 +++--- litebox_shim_linux/src/syscalls/epoll.rs | 2 +- litebox_shim_linux/src/syscalls/file.rs | 94 +++++++++---------- litebox_shim_linux/src/syscalls/process.rs | 7 +- litebox_shim_linux/src/syscalls/unix.rs | 9 +- litebox_shim_windows/src/lib.rs | 11 ++- litebox_shim_windows/src/syscalls/registry.rs | 21 +++-- litebox_shim_windows/src/tests.rs | 2 +- 9 files changed, 90 insertions(+), 93 deletions(-) diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 5c38f5e55e..ffa334af51 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -28,16 +28,14 @@ pub struct LiteBox { pub(crate) x: Arc>, } -impl Clone for LiteBox { - /// Creates another handle to the same LiteBox system. - fn clone(&self) -> Self { +impl LiteBox { + /// Create another crate-internal handle to this LiteBox system. + pub(crate) fn clone(&self) -> Self { Self { x: Arc::clone(&self.x), } } -} -impl LiteBox { /// Create a new (empty) [`LiteBox`] instance for the given `platform`. /// /// # Panics diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 88c3b8d56c..02cf99211e 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -222,16 +222,17 @@ impl LinuxShimBuilder { /// 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()), }); @@ -265,7 +266,7 @@ impl LinuxShim { egid, } = task; - let files = syscalls::file::FilesState::new(&self.0.litebox); + 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 { @@ -317,7 +318,7 @@ impl LinuxShim { } pub fn litebox(&self) -> &LiteBox { - &self.0.litebox + self.0.litebox.as_ref() } /// Returns the platform this shim was built with. @@ -357,8 +358,8 @@ impl syscalls::file::FilesState { global: &GlobalState, context: &litebox::fs::Context, ) { - let stdin = self - .fs + let stdin = global + .litebox .open_file( context, "/dev/stdin", @@ -367,8 +368,8 @@ impl syscalls::file::FilesState { Mode::empty(), ) .unwrap(); - let stdout = self - .fs + let stdout = global + .litebox .open_file( context, "/dev/stdout", @@ -377,8 +378,8 @@ impl syscalls::file::FilesState { Mode::empty(), ) .unwrap(); - let stderr = self - .fs + let stderr = global + .litebox .open_file( context, "/dev/stderr", @@ -1154,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. @@ -1214,7 +1215,7 @@ mod test_utils { let pid = self .next_thread_id .fetch_add(1, core::sync::atomic::Ordering::Relaxed); - let files = Arc::new(syscalls::file::FilesState::new(&self.litebox)); + 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/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 533b9086f2..88d97adc33 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -702,7 +702,7 @@ mod test { .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); + let no_fds = FilesState::new(); let fds = task.files.borrow().clone(); set.add_fd(rfd, Events::IN); diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index d66fad383f..55b1135c52 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -140,8 +140,6 @@ fn file_open_options(flags: OFlags) -> Result<(FileAccessMode, FileOpenFlags), O /// Task state shared by `CLONE_FILES`. pub(crate) struct FilesState { - /// The filesystem implementation, shared across tasks that share file system. - pub(crate) fs: litebox::LiteBox, pub(crate) raw_descriptor_store: litebox::sync::RwLock, /// Exclusive upper bound for raw file descriptor values. @@ -149,9 +147,8 @@ pub(crate) struct FilesState { } impl FilesState { - pub(crate) fn new(fs: &litebox::LiteBox) -> Self { + pub(crate) fn new() -> Self { Self { - fs: fs.clone(), raw_descriptor_store: litebox::sync::RwLock::new( litebox::fd::RawDescriptorStorage::new(), ), @@ -434,7 +431,6 @@ impl Task { } }); let file = { - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); let path = path @@ -442,8 +438,8 @@ impl Task { .map_err(litebox::fs::errors::PathError::from)?; let (access, open_flags) = file_open_options(flags - OFlags::CLOEXEC).map_err(Errno::from)?; - files - .fs + self.global + .litebox .open_file(&context, path, access, open_flags, mode) .map_err(Errno::from) }?; @@ -482,7 +478,7 @@ impl Task { } let files = self.files.borrow(); let raw_fd = files.insert_raw_fd(file).map_err(|file| { - files.fs.close_file(&file).unwrap(); + self.global.litebox.close_file(&file).unwrap(); Errno::EMFILE })?; Ok(u32::try_from(raw_fd).unwrap()) @@ -524,8 +520,8 @@ impl Task { let fd = files.typed_fd(fd)?; fd.dispatch( |fd| { - files - .fs + self.global + .litebox .truncate_file(fd, length, false) .map_err(Errno::from) }, @@ -563,8 +559,7 @@ impl Task { OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, mode, )?; - let files = self.files.borrow(); - let _ = files.fs.close_file(&file); + let _ = self.global.litebox.close_file(&file); } // TODO: Named pipe, socket, block and char files are not supported InodeType::NamedPipe @@ -589,13 +584,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_file(&context, path).map_err(Errno::from) + self.global + .litebox + .rmdir_file(&context, path) + .map_err(Errno::from) } else { - files.fs.unlink_file(&context, path).map_err(Errno::from) + self.global + .litebox + .unlink_file(&context, path) + .map_err(Errno::from) } } @@ -614,14 +614,13 @@ 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 + self.global + .litebox .read_file(fd, &mut buf.borrow_mut(), offset) .map_err(Errno::from) }, @@ -699,10 +698,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_file(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!( @@ -780,9 +783,8 @@ 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 + self.global + .litebox .seek_file(fd, -rewind, SeekWhence::RelativeToCurrentOffset) .map(|_| ()) .map_err(Errno::from) @@ -809,10 +811,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()); @@ -825,8 +825,9 @@ impl Task { Errno::EINVAL }; let read_result = match typed_in_fd.as_fs() { - Some(fd) => files - .fs + Some(fd) => self + .global + .litebox .read_file(fd, &mut kernel_buf[..to_read], cur_off) .map_err(Errno::from), None => Err(non_fs_err), @@ -908,8 +909,7 @@ impl Task { whence: SeekWhence, ) -> Result { let fd = fd.fs_only(Errno::ESPIPE)?; - let files = self.files.borrow(); - match files.fs.seek_file(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 { @@ -935,11 +935,10 @@ 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 + self.global + .litebox .mkdir_file(&context, pathname, mode) .map_err(Errno::from) } @@ -1031,7 +1030,7 @@ impl Task { if let Ok(raw_fd) = i32::try_from(raw_fd) { self.finalize_elf_patch(raw_fd); } - files.fs.close_file(&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), @@ -1464,10 +1463,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.path_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) @@ -1503,7 +1501,7 @@ impl Task { let files = self.files.borrow(); let typed_fd = files.typed_fd(fd)?; if let Some(file) = typed_fd.as_fs() { - let status = files.fs.file_status(file)?; + let status = self.global.litebox.file_status(file)?; return Self::do_access_mode(status.mode, status.owner.into(), caller, &mode); } drop(files); @@ -1600,7 +1598,7 @@ impl Task { pub(crate) fn file_status(&self, fd: i32) -> Result { let files = self.files.borrow(); let fd = files.typed_fd(fd)?; - Ok(files.fs.file_status(fd.fs_only(Errno::EBADF)?)?) + Ok(self.global.litebox.file_status(fd.fs_only(Errno::EBADF)?)?) } pub(crate) fn do_stat(&self, fd: &AnyTypedFd) -> Result @@ -1631,9 +1629,8 @@ impl Task { let socket_mode = litebox_common_linux::InodeType::Socket as u32 | u32::from((Mode::RWXU | Mode::RWXG | Mode::RWXO).bits()); let rw_user_mode = u32::from((Mode::RUSR | Mode::WUSR).bits()); - let files = self.files.borrow(); fd.dispatch( - |fd| T::try_from(files.fs.file_status(fd)?), + |fd| T::try_from(self.global.litebox.file_status(fd)?), |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { Ok(T::from(synthetic( @@ -1663,10 +1660,9 @@ impl Task { normalized_path }; let status = { - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files.fs.path_file_status(&context, path)? + self.global.litebox.path_file_status(&context, path)? }; T::try_from(status) } @@ -1711,10 +1707,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(); - T::try_from(files.fs.path_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)) @@ -2004,9 +1999,12 @@ impl Task { // Verify the path exists and is a directory. { - let files = self.files.borrow(); let context = fs.context.read(); - match files.fs.path_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); @@ -2216,7 +2214,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 @@ -2677,7 +2675,7 @@ impl Task { file.dispatch( |fd| { dup(self, &files, fd, close_on_exec, target, |fd| { - let _ = files.fs.close_file(&fd); + let _ = self.global.litebox.close_file(&fd); }) }, |fd| { @@ -2802,7 +2800,7 @@ impl Task { let mut dir_off = dir_off.0; let mut nbytes = 0; - let mut entries = files.fs.read_file_directory(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) { diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index e257fa62d3..82fb5099d9 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1406,15 +1406,14 @@ impl Task { 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(&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(&file); + let _ = self.global.litebox.close_file(&file); return Err(Errno::from(e)); } }; - let _ = files.fs.close_file(&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/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index b43f24654d..74ef37c273 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -71,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, litebox::LiteBox)), + Path((String, FileFd, Arc>)), Abstract(Vec), } @@ -119,11 +119,10 @@ impl UnixSocketAddr { }; // 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 + task.global + .litebox .open_file( &context, path.as_str(), @@ -139,7 +138,7 @@ impl UnixSocketAddr { Ok(UnixBoundSocketAddr::Path(( path, file, - task.files.borrow().fs.clone(), + Arc::clone(&task.global.litebox), ))) } UnixSocketAddr::Abstract(data) => { diff --git a/litebox_shim_windows/src/lib.rs b/litebox_shim_windows/src/lib.rs index 1b62cd0c48..cfe4d20ca8 100644 --- a/litebox_shim_windows/src/lib.rs +++ b/litebox_shim_windows/src/lib.rs @@ -409,16 +409,17 @@ impl WindowsShimBuilder { #[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) } @@ -493,7 +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::new(self.0.litebox.clone()); + 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. @@ -540,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. diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index 5acb1ff88a..ee8141ac84 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -28,6 +28,7 @@ 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; @@ -74,7 +75,7 @@ pub(crate) struct RegistryKeyObject { } pub(crate) struct RegistryStore { - fs: LiteBox, + fs: Arc>, fs_context: litebox::fs::Context, /// Whether the built-in keys and values have been written to [`Self::fs`]. /// @@ -600,9 +601,9 @@ impl RegistryStore { /// /// 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: &LiteBox) -> Self { + pub(crate) fn new(litebox: Arc>) -> Self { Self { - fs: litebox.clone(), + fs: litebox, fs_context: litebox::fs::Context::new(), defaults_seeded: Mutex::new(false), notification_state: Mutex::new(RegistryNotificationState::default()), @@ -623,7 +624,7 @@ impl RegistryStore { seed_defaults(&self.fs, &self.fs_context); } } - &self.fs + self.fs.as_ref() } fn open_key( @@ -2586,9 +2587,9 @@ mod tests { /// /// 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() -> (LiteBox, RegistryStore) { + fn test_registry() -> (Arc>, RegistryStore) { let mode = FileMode::RWXU | FileMode::RWXG | FileMode::RWXO; - let litebox = crate::test_broker::litebox_with_broker_files( + let litebox = Arc::new(crate::test_broker::litebox_with_broker_files( test_platform(), alloc::vec![ ( @@ -2606,8 +2607,8 @@ mod tests { }, ), ], - ); - let registry = RegistryStore::new(&litebox); + )); + let registry = RegistryStore::new(Arc::clone(&litebox)); (litebox, registry) } @@ -2782,8 +2783,8 @@ mod tests { fn registry_store_construction_issues_no_file_requests() { // This broker association serves no files at all and panics on any request, so building // the store at all is the assertion. - let litebox = crate::test_broker::litebox(test_platform()); - let registry = RegistryStore::new(&litebox); + let litebox = Arc::new(crate::test_broker::litebox(test_platform())); + let registry = RegistryStore::new(litebox); assert!(!*registry.defaults_seeded.lock()); } diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index d85c4cc36f..49ef5f6c34 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -171,10 +171,10 @@ fn test_task_from_litebox(litebox: litebox::LiteBox) -> Task::new_with_litebox(platform, litebox); - let fs = Arc::new(shim_builder.litebox().clone()); 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"); From b29fd415e04ebdb1055286e98a90a04702ba547f Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 18:26:31 -0700 Subject: [PATCH 17/34] Restore LiteBox clone layout Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/litebox.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index ffa334af51..6007e891af 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -29,13 +29,6 @@ pub struct LiteBox { } impl LiteBox { - /// Create another crate-internal handle to this LiteBox system. - pub(crate) fn clone(&self) -> Self { - Self { - x: Arc::clone(&self.x), - } - } - /// Create a new (empty) [`LiteBox`] instance for the given `platform`. /// /// # Panics @@ -120,6 +113,15 @@ impl LiteBox { } } + /// An explicitly-crate-internal clone method to prevent outside users from cloning the + /// [`LiteBox`] object, which could cause confusion as to the intended use. External users must + /// only create it via [`Self::new`]. + pub(crate) fn clone(&self) -> Self { + Self { + x: Arc::clone(&self.x), + } + } + /// Access to the file descriptor table. /// /// Note: this takes a lock, and thus should ideally not be held on to for too long to prevent From 88c0af85b88e703af520fac55528a8a7120eaa1e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 18:34:06 -0700 Subject: [PATCH 18/34] Simplify pipe nonblocking state Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/pipes.rs | 118 +++++++++++-------------------------------- 1 file changed, 30 insertions(+), 88 deletions(-) diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index 673b3bbb88..072f7cbbb9 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}; @@ -74,7 +74,7 @@ impl Pipes { broker, self.litebox.broker_pollable_registry(), capacity, - flags & Flags::all(), + flags, atomic_slice_guarantee_size, )?; let mut dt = self.litebox.descriptor_table_mut(); @@ -159,7 +159,11 @@ impl Pipes { .ok_or(errors::ClosedError::ClosedFd)? .entry .0; - Ok(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`. @@ -177,7 +181,9 @@ impl Pipes { .ok_or(errors::ClosedError::ClosedFd)? .entry .0; - p.set_status(mask & Flags::all(), on); + if mask.contains(Flags::NON_BLOCKING) { + p.non_blocking.store(on, Relaxed); + } Ok(()) } @@ -340,7 +346,7 @@ struct BrokerPipeEnd { pollee: Arc>, peer: Weak, endpoint_type: HalfPipeType, - status: AtomicU32, + non_blocking: AtomicBool, } #[expect( @@ -379,7 +385,7 @@ fn new_broker_pipe( pollee: Arc::new(Pollee::new()), peer: Weak::new(), endpoint_type: HalfPipeType::SenderHalf, - status: AtomicU32::new(flags.bits()), + non_blocking: AtomicBool::new(flags.contains(Flags::NON_BLOCKING)), }); let reader = Arc::new_cyclic(|weak_reader| { Arc::get_mut(&mut writer) @@ -392,7 +398,7 @@ fn new_broker_pipe( pollee: Arc::new(Pollee::new()), peer: Arc::downgrade(&writer), endpoint_type: HalfPipeType::ReceiverHalf, - status: AtomicU32::new(flags.bits()), + non_blocking: AtomicBool::new(flags.contains(Flags::NON_BLOCKING)), } }); @@ -402,18 +408,6 @@ fn new_broker_pipe( } impl BrokerPipeEnd { - fn get_status(&self) -> Flags { - Flags::from_bits(self.status.load(Relaxed)).expect("pipe status contains only pipe flags") - } - - fn set_status(&self, mask: Flags, 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 { @@ -424,27 +418,22 @@ impl BrokerPipeEnd .expect("pipe transfer limit must fit in u32"); self.pollee - .wait( - cx, - self.get_status().contains(Flags::NON_BLOCKING), - 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) } @@ -452,7 +441,7 @@ impl BrokerPipeEnd if buf.is_empty() { return Ok(0); } - let nonblock = self.get_status().contains(Flags::NON_BLOCKING); + let nonblock = self.non_blocking.load(Relaxed); if nonblock { let data = &buf[..buf.len().min(MAX_PIPE_TRANSFER_SIZE as usize)]; return self @@ -588,53 +577,6 @@ mod tests { extern crate std; - #[test] - fn pipe_flags_are_independent_for_each_endpoint() { - let platform = crate::platform::mock::MockPlatform::new(); - let (local, ()) = BrokerLocal::negotiate( - FailingPipeChannel { - request_count: Arc::new(AtomicUsize::new(0)), - read_failure: ReadFailure::WouldBlock, - force_transport: Arc::new(AtomicBool::new(false)), - }, - |channel| Ok((channel, Arc::new(NoopSharedMemory), ())), - ) - .unwrap(); - let litebox = crate::LiteBox::new_with_broker_local(platform, local); - let pipes = super::Pipes::new(&litebox); - let unknown = super::Flags::from_bits_retain(1 << 31); - let (writer, reader) = pipes - .create_pipe(2, super::Flags::NON_BLOCKING | unknown, None) - .unwrap(); - assert_eq!( - pipes.get_flags(&writer).unwrap(), - super::Flags::NON_BLOCKING - ); - assert_eq!( - pipes.get_flags(&reader).unwrap(), - super::Flags::NON_BLOCKING - ); - pipes - .update_flags(&writer, super::Flags::NON_BLOCKING, false) - .unwrap(); - assert!(pipes.get_flags(&writer).unwrap().is_empty()); - assert_eq!( - pipes.get_flags(&reader).unwrap(), - super::Flags::NON_BLOCKING - ); - pipes.update_flags(&writer, unknown, true).unwrap(); - assert!(pipes.get_flags(&writer).unwrap().is_empty()); - pipes - .update_flags(&writer, super::Flags::NON_BLOCKING, true) - .unwrap(); - assert_eq!( - pipes.get_flags(&writer).unwrap(), - super::Flags::NON_BLOCKING - ); - pipes.close(&writer).unwrap(); - pipes.close(&reader).unwrap(); - } - #[test] fn broker_control_failure_notifies_all_pipe_observers() { let platform = crate::platform::mock::MockPlatform::new(); From 370abd656461ea6558fe5c19259c0143579ad44d Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 10 Sep 2026 19:52:11 -0700 Subject: [PATCH 19/34] Remove refactoring-only filesystem tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/fs/inode_allocator.rs | 22 -- .../src/fs/nine_p/metadata_tests.rs | 115 -------- litebox_broker_core/src/fs/nine_p/mod.rs | 4 - .../src/fs/nine_p/open_flags_tests.rs | 46 --- litebox_broker_core/src/fs/service.rs | 25 -- litebox_broker_core/src/fs/test_support.rs | 42 +-- litebox_broker_core/src/fs/tests.rs | 278 +----------------- litebox_broker_protocol/src/fs.rs | 46 --- litebox_broker_protocol/src/fs/path.rs | 44 --- litebox_broker_protocol/src/wire.rs | 31 -- litebox_broker_userland/src/linux.rs | 29 -- litebox_common_linux/src/lib.rs | 162 ---------- litebox_shim_linux/src/loader/elf.rs | 20 -- litebox_shim_linux/src/syscalls/file.rs | 129 -------- litebox_shim_linux/src/syscalls/tests.rs | 187 +----------- litebox_shim_windows/src/syscalls/file.rs | 132 --------- litebox_shim_windows/src/syscalls/registry.rs | 92 ------ litebox_shim_windows/src/tests.rs | 9 - 18 files changed, 11 insertions(+), 1402 deletions(-) delete mode 100644 litebox_broker_core/src/fs/nine_p/metadata_tests.rs delete mode 100644 litebox_broker_core/src/fs/nine_p/open_flags_tests.rs diff --git a/litebox_broker_core/src/fs/inode_allocator.rs b/litebox_broker_core/src/fs/inode_allocator.rs index 0d1ee9ca33..577d52ec2a 100644 --- a/litebox_broker_core/src/fs/inode_allocator.rs +++ b/litebox_broker_core/src/fs/inode_allocator.rs @@ -70,25 +70,3 @@ impl InodeAllocator { self.device_id } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn preserves_full_width_device_and_inode_numbers() { - let allocator = InodeAllocator::for_device(u64::MAX); - allocator.counter.store(u64::MAX - 1, Ordering::Relaxed); - - assert_eq!(allocator.device_id(), u64::MAX); - assert_eq!( - allocator.next(), - FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX - 1, - rdev: None, - } - ); - assert_eq!(allocator.next().ino, u64::MAX); - } -} diff --git a/litebox_broker_core/src/fs/nine_p/metadata_tests.rs b/litebox_broker_core/src/fs/nine_p/metadata_tests.rs deleted file mode 100644 index 33cad2fbe8..0000000000 --- a/litebox_broker_core/src/fs/nine_p/metadata_tests.rs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use core::num::NonZeroU64; - -use litebox_broker_protocol::fs::{FileMode, FileNodeInfo, FileStatus, FileType, FileUser}; - -use super::fcall::{GetattrMask, Qid, QidType, Rgetattr, Stat, Time}; -use super::{Error, rgetattr_to_file_status}; - -fn attributes(valid: GetattrMask) -> Rgetattr { - Rgetattr { - valid, - qid: Qid { - typ: QidType::FILE, - version: 0, - path: u64::MAX - 1, - }, - stat: Stat { - mode: 0o100644, - uid: 1, - gid: 2, - nlink: 1, - rdev: u64::MAX, - size: u64::MAX - 2, - blksize: u64::MAX - 3, - blocks: 0, - atime: Time::default(), - mtime: Time::default(), - ctime: Time::default(), - btime: Time::default(), - generation: 0, - data_version: 0, - }, - } -} - -#[test] -fn getattr_preserves_full_width_metadata() { - for valid in [ - GetattrMask::BASIC, - GetattrMask::MODE - | GetattrMask::UID - | GetattrMask::GID - | GetattrMask::SIZE - | GetattrMask::RDEV - | GetattrMask::BLOCKS, - ] { - let status = rgetattr_to_file_status(&attributes(valid), u64::MAX).unwrap(); - - assert_eq!( - status, - FileStatus { - file_type: FileType::RegularFile, - mode: FileMode::from_bits(0o644).unwrap(), - size: u64::MAX - 2, - owner: FileUser { user: 1, group: 2 }, - node_info: FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX - 1, - rdev: NonZeroU64::new(u64::MAX), - }, - block_size: u64::MAX - 3, - } - ); - } -} - -#[test] -fn getattr_zero_device_number_is_absent() { - for valid in [GetattrMask::BASIC, GetattrMask::RDEV] { - let mut attr = attributes(valid); - attr.stat.rdev = 0; - let status = rgetattr_to_file_status(&attr, u64::MAX).unwrap(); - - assert_eq!(status.node_info.rdev, None); - } -} - -#[test] -fn getattr_ignores_unavailable_metadata() { - let status = rgetattr_to_file_status(&attributes(GetattrMask::empty()), u64::MAX).unwrap(); - - assert_eq!(status.mode, FileMode::empty()); - assert_eq!(status.size, 0); - assert_eq!(status.owner, FileUser::ROOT); - assert_eq!(status.block_size, 0); - assert_eq!( - status.node_info, - FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX - 1, - rdev: None, - } - ); -} - -#[test] -fn getattr_rejects_unrepresentable_owners() { - for valid in [GetattrMask::BASIC, GetattrMask::UID | GetattrMask::GID] { - let mut attr = attributes(valid); - attr.stat.uid = u32::from(u16::MAX) + 1; - assert!(matches!( - rgetattr_to_file_status(&attr, 0), - Err(Error::InvalidResponse) - )); - - attr.stat.uid = 1; - attr.stat.gid = u32::from(u16::MAX) + 1; - assert!(matches!( - rgetattr_to_file_status(&attr, 0), - Err(Error::InvalidResponse) - )); - } -} diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index 2acd16190b..90e7ff43b8 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -33,10 +33,6 @@ use super::errors::{ mod client; mod fcall; mod id_pool; -#[cfg(test)] -mod metadata_tests; -#[cfg(test)] -mod open_flags_tests; #[cfg(all(test, target_os = "linux"))] mod tests; diff --git a/litebox_broker_core/src/fs/nine_p/open_flags_tests.rs b/litebox_broker_core/src/fs/nine_p/open_flags_tests.rs deleted file mode 100644 index cd969a71d5..0000000000 --- a/litebox_broker_core/src/fs/nine_p/open_flags_tests.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; - -use super::fcall::LOpenFlags; -use super::open_flags_to_lopen; - -#[test] -fn access_modes_are_not_wire_flag_bits() { - for (access, expected) in [ - (FileAccessMode::ReadOnly, LOpenFlags::empty()), - (FileAccessMode::WriteOnly, LOpenFlags::O_WRONLY), - (FileAccessMode::ReadWrite, LOpenFlags::O_RDWR), - ] { - assert_eq!( - open_flags_to_lopen(access, FileOpenFlags::empty()), - expected - ); - assert_eq!( - open_flags_to_lopen(access, FileOpenFlags::CREATE | FileOpenFlags::APPEND), - expected | LOpenFlags::O_CREAT | LOpenFlags::O_APPEND, - ); - } -} - -#[test] -fn canonical_flags_translate_only_at_wire_boundary() { - for (flag, expected) in [ - (FileOpenFlags::CREATE, LOpenFlags::O_CREAT), - (FileOpenFlags::EXCLUSIVE, LOpenFlags::O_EXCL), - (FileOpenFlags::TRUNCATE, LOpenFlags::O_TRUNC), - (FileOpenFlags::APPEND, LOpenFlags::O_APPEND), - (FileOpenFlags::DIRECTORY, LOpenFlags::O_DIRECTORY), - (FileOpenFlags::NO_FOLLOW, LOpenFlags::O_NOFOLLOW), - (FileOpenFlags::NONBLOCKING, LOpenFlags::O_NONBLOCK), - (FileOpenFlags::NO_CONTROLLING_TERMINAL, LOpenFlags::empty()), - (FileOpenFlags::LARGE_FILE, LOpenFlags::empty()), - (FileOpenFlags::PATH, LOpenFlags::empty()), - ] { - assert_eq!( - open_flags_to_lopen(FileAccessMode::ReadOnly, flag), - expected - ); - } -} diff --git a/litebox_broker_core/src/fs/service.rs b/litebox_broker_core/src/fs/service.rs index 08525edbb0..fcf944cbe6 100644 --- a/litebox_broker_core/src/fs/service.rs +++ b/litebox_broker_core/src/fs/service.rs @@ -844,31 +844,6 @@ mod tests { use core::num::NonZeroU64; use litebox_broker_protocol::fs::{FileNodeInfo, FileType}; - #[test] - fn unknown_open_flags_are_rejected_before_path_only_masking() { - let unknown = FileOpenFlags::from_bits_retain(1 << 15); - for access in [ - FileAccessMode::ReadOnly, - FileAccessMode::WriteOnly, - FileAccessMode::ReadWrite, - ] { - for flags in [ - FileOpenFlags::empty(), - FileOpenFlags::CREATE, - FileOpenFlags::PATH, - ] { - assert!(matches!( - open_required_rights(access, flags | unknown), - Err(BrokerError::UnsupportedOperation) - )); - } - assert_eq!( - open_required_rights(access, FileOpenFlags::PATH).unwrap(), - ObjectRights::WAIT - ); - } - } - #[test] fn file_status_excludes_object_type_mode_bits() { let mut status = FileStatus { diff --git a/litebox_broker_core/src/fs/test_support.rs b/litebox_broker_core/src/fs/test_support.rs index 4e4711221e..eb616d5d2e 100644 --- a/litebox_broker_core/src/fs/test_support.rs +++ b/litebox_broker_core/src/fs/test_support.rs @@ -16,9 +16,11 @@ use litebox_broker_protocol::fs::{ use litebox_broker_protocol::stdio::StdioOutputStream; use super::backend::{Backend, DeviceIo, NoDeviceIo}; +#[cfg(target_os = "linux")] +use super::errors::TruncateError; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, - RmdirError, SeekError, TruncateError, UnlinkError, WriteError, + RmdirError, SeekError, UnlinkError, WriteError, }; use super::resolver::{Resolver, ResolverEntry}; use crate::test_platform::TestPlatform; @@ -111,6 +113,7 @@ impl Fs { self.resolver.seek(entry, offset, whence) } + #[cfg(target_os = "linux")] pub(in crate::fs) fn truncate( &self, entry: &mut Entry, @@ -204,40 +207,3 @@ impl DeviceIo for UnservicedStdio { Err(ReadError::Io) } } - -/// Device I/O that records the standard-I/O transfers the devices backend asks for. -pub(in crate::fs) struct RecordingStdio { - stdin: &'static [u8], - writes: std::sync::Mutex)>>, -} - -impl RecordingStdio { - pub(in crate::fs) fn new(stdin: &'static [u8]) -> Self { - Self { - stdin, - writes: std::sync::Mutex::new(Vec::new()), - } - } - - pub(in crate::fs) fn writes(&self) -> Vec<(StdioOutputStream, Vec)> { - self.writes.lock().unwrap().clone() - } -} - -impl DeviceIo for RecordingStdio { - fn read_stdin(&self, output: &mut [u8]) -> Result { - let read = self.stdin.len().min(output.len()); - output[..read].copy_from_slice(&self.stdin[..read]); - Ok(read) - } - - fn write_stdio(&self, stream: StdioOutputStream, input: &[u8]) -> Result { - self.writes.lock().unwrap().push((stream, input.into())); - Ok(input.len()) - } - - fn fill_random(&self, output: &mut [u8]) -> Result<(), ReadError> { - output.fill(0x5a); - Ok(()) - } -} diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs index a30559181b..74f1b9c801 100644 --- a/litebox_broker_core/src/fs/tests.rs +++ b/litebox_broker_core/src/fs/tests.rs @@ -16,7 +16,7 @@ use super::in_mem::InMem; use super::inode_allocator::InodeAllocator; use super::overlay::Overlay; use super::tar_ro::TarRo; -use super::test_support::{Fs, ROOT, RecordingStdio, USER, UnservicedStdio}; +use super::test_support::{Fs, ROOT, USER, UnservicedStdio}; use crate::test_platform::TestPlatform; const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); @@ -49,7 +49,7 @@ mod in_mem { }; use crate::fs::errors::{ ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, - TruncateError, UnlinkError, WriteError, + UnlinkError, }; use crate::fs::test_support::Entry; use alloc::vec; @@ -880,40 +880,6 @@ mod in_mem { assert_eq!(&buffer[..bytes_read], test_data); } - #[test] - fn truncate_resets_or_keeps_position() { - let fs = in_mem_fs(); - world_writable_root(&fs); - - let mut fd = fs - .open( - USER, - "/truncfile", - FileAccessMode::ReadWrite, - FileOpenFlags::CREATE, - Mode::RWXU, - ) - .expect("Failed to create file"); - fs.write(&mut fd, b"0123456789", None) - .expect("Failed to write file"); - - // Truncating without resetting keeps the position, so a read sees nothing. - fs.truncate(&mut fd, 4, false).expect("Failed to truncate"); - let mut buffer = vec![0; 10]; - assert_eq!( - fs.read(&mut fd, &mut buffer, None) - .expect("Failed to read file"), - 0 - ); - - // Truncating with a reset rewinds to the start of the (now shorter) file. - fs.truncate(&mut fd, 4, true).expect("Failed to truncate"); - let bytes_read = fs - .read(&mut fd, &mut buffer, None) - .expect("Failed to read file"); - assert_eq!(&buffer[..bytes_read], b"0123"); - } - #[test] fn write_position_after_seek() { let fs = in_mem_fs(); @@ -1183,148 +1149,6 @@ mod in_mem { .expect("Failed to open file for reading"); assert_eq!(read_all(&fs, &mut fd), b"NewContent"); } - - #[test] - fn creation_flags_do_not_change_access_mode() { - for (access, can_read, can_write) in [ - (FileAccessMode::ReadOnly, true, false), - (FileAccessMode::WriteOnly, false, true), - (FileAccessMode::ReadWrite, true, true), - ] { - let fs = in_mem_fs(); - let mut entry = fs - .open( - USER, - "/file", - access, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE | FileOpenFlags::APPEND, - Mode::RWXU, - ) - .unwrap(); - - let read = fs.read(&mut entry, &mut [0], None); - if can_read { - assert_eq!(read.unwrap(), 0); - } else { - assert!(matches!(read, Err(ReadError::NotForReading))); - } - let write = fs.write(&mut entry, b"x", None); - if can_write { - assert_eq!(write.unwrap(), 1); - } else { - assert!(matches!(write, Err(WriteError::NotForWriting))); - } - } - } - - #[test] - fn read_only_open_can_still_truncate() { - let fs = in_mem_fs(); - create_with_content(&fs, "/file", b"contents"); - let mut entry = fs - .open( - USER, - "/file", - FileAccessMode::ReadOnly, - FileOpenFlags::TRUNCATE, - Mode::empty(), - ) - .unwrap(); - assert_eq!(fs.handle_status(&entry).unwrap().size, 0); - assert!(matches!( - fs.write(&mut entry, b"x", None), - Err(WriteError::NotForWriting) - )); - } - - #[test] - fn path_only_ignores_access_and_mutating_flags() { - let fs = in_mem_fs(); - create_with_content(&fs, "/file", b"contents"); - fs.chmod(USER, "/file", Mode::empty()).unwrap(); - let flags = FileOpenFlags::all() - FileOpenFlags::DIRECTORY; - - for access in [ - FileAccessMode::ReadOnly, - FileAccessMode::WriteOnly, - FileAccessMode::ReadWrite, - ] { - let mut entry = fs.open(USER, "/file", access, flags, Mode::RWXU).unwrap(); - assert!(entry.is_path_only()); - assert!(entry.allows_read()); - assert_eq!(fs.handle_status(&entry).unwrap().size, 8); - assert!(matches!( - fs.write(&mut entry, b"x", None), - Err(WriteError::NotForWriting) - )); - assert!(matches!( - fs.truncate(&mut entry, 0, false), - Err(TruncateError::NotForWriting) - )); - assert!(matches!( - fs.open(USER, "/missing", access, flags, Mode::RWXU), - Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) - )); - assert!(matches!( - fs.open( - USER, - "/file", - access, - flags | FileOpenFlags::DIRECTORY, - Mode::empty(), - ), - Err(OpenError::PathError(PathError::ComponentNotADirectory)) - )); - } - } - - #[test] - fn paths_are_normalized_before_lookup_and_mutation() { - let fs = in_mem_fs(); - fs.mkdir(USER, "/dir", Mode::RWXU).unwrap(); - create_with_content(&fs, "/dir/file", b"contents"); - let expected = fs.file_status(USER, "/dir/file").unwrap(); - for path in [ - "/dir//./missing/../file/", - "dir/../../dir/file", - "../../dir/file", - ] { - let entry = fs - .open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) - .unwrap(); - assert_eq!(fs.handle_status(&entry).unwrap(), expected); - } - let root = fs.file_status(USER, "/").unwrap(); - for path in ["", ".", "../..", "/dir/../../.."] { - assert_eq!(fs.file_status(USER, path).unwrap(), root); - } - - fs.mkdir(USER, "/dir/missing/../new", Mode::RWXU).unwrap(); - let entry = fs - .open( - USER, - "dir/new/./missing/../file", - FileAccessMode::ReadWrite, - FileOpenFlags::CREATE, - Mode::RWXU, - ) - .unwrap(); - fs.chmod(USER, "dir/new//file", Mode::RUSR).unwrap(); - fs.chown(USER, "dir/new/unused/../file", None, Some(ROOT.group)) - .unwrap(); - let status = fs.handle_status(&entry).unwrap(); - assert_eq!(status.mode, Mode::RUSR); - assert_eq!(status.owner.group, ROOT.group); - fs.unlink(USER, "/dir/new/missing/../file").unwrap(); - fs.rmdir(USER, "//dir/./new/").unwrap(); - assert!(fs.file_status(USER, "/dir/new").is_err()); - } } mod tar_ro { @@ -1460,31 +1284,6 @@ mod tar_ro { } } - #[test] - fn path_only_does_not_request_write_access_on_read_only_backend() { - let fs = tar_ro_fs(TEST_TAR_FILE.into()); - for path in ["/", "bar", "foo"] { - let expected = fs.file_status(USER, path).unwrap(); - for access in [FileAccessMode::WriteOnly, FileAccessMode::ReadWrite] { - let entry = fs - .open( - USER, - path, - access, - FileOpenFlags::PATH - | FileOpenFlags::CREATE - | FileOpenFlags::EXCLUSIVE - | FileOpenFlags::TRUNCATE - | FileOpenFlags::APPEND, - Mode::empty(), - ) - .unwrap(); - assert!(entry.is_path_only()); - assert_eq!(fs.handle_status(&entry).unwrap(), expected); - } - } - } - #[test] fn read_dir_subdirectory() { let fs = tar_ro_fs(TEST_TAR_FILE.into()); @@ -2437,12 +2236,11 @@ mod overlay { mod devices { use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; - use super::{Fs, Mode, RecordingStdio, USER, UnservicedStdio}; + use super::{Fs, Mode, USER, UnservicedStdio}; use crate::fs::composer::Composer; use crate::fs::devices::Devices; use crate::fs::errors::{OpenError, PathError, ReadError, WriteError}; use alloc::vec; - use litebox_broker_protocol::stdio::StdioOutputStream; fn devices_fs() -> Fs { Fs::new( @@ -2518,76 +2316,6 @@ mod devices { )); } - /// Each stdio device routes to its own stream on the session's device I/O. - #[test] - fn stdio_routes_to_the_session_streams() { - let fs = devices_fs(); - let stdio = RecordingStdio::new(b"host input"); - - let mut fd_stdout = fs - .open( - USER, - "/dev/stdout", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) - .expect("Failed to open /dev/stdout"); - assert_eq!( - fs.write_with(&stdio, &mut fd_stdout, b"out", None) - .expect("Failed to write /dev/stdout"), - 3 - ); - - let mut fd_stderr = fs - .open( - USER, - "/dev/stderr", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) - .expect("Failed to open /dev/stderr"); - assert_eq!( - fs.write_with(&stdio, &mut fd_stderr, b"err", None) - .expect("Failed to write /dev/stderr"), - 3 - ); - - assert_eq!( - stdio.writes(), - vec![ - (StdioOutputStream::Stdout, b"out".to_vec()), - (StdioOutputStream::Stderr, b"err".to_vec()), - ] - ); - - let mut fd_stdin = fs - .open( - USER, - "/dev/stdin", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) - .expect("Failed to open /dev/stdin"); - let mut buffer = vec![0; 16]; - let read = fs - .read_with(&stdio, &mut fd_stdin, &mut buffer, None) - .expect("Failed to read /dev/stdin"); - assert_eq!(&buffer[..read], b"host input"); - - // Reading a write-only device and writing a read-only device are rejected. - assert!(matches!( - fs.read_with(&stdio, &mut fd_stdout, &mut buffer, None), - Err(ReadError::NotForReading) - )); - assert!(matches!( - fs.write_with(&stdio, &mut fd_stdin, b"x", None), - Err(WriteError::NotForWriting) - )); - } - #[test] fn non_dev_path_fails() { let fs = devices_fs(); diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index 05732523ce..2d4c77bae1 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -690,30 +690,6 @@ mod tests { use super::*; use alloc::vec; - #[test] - fn file_open_flags_preserve_wire_bits_and_reject_unknown_bits() { - for (flag, bits) in [ - (FileOpenFlags::NONE, 0), - (FileOpenFlags::CREATE, 0x001), - (FileOpenFlags::TRUNCATE, 0x002), - (FileOpenFlags::NO_CONTROLLING_TERMINAL, 0x004), - (FileOpenFlags::EXCLUSIVE, 0x008), - (FileOpenFlags::DIRECTORY, 0x010), - (FileOpenFlags::NONBLOCKING, 0x020), - (FileOpenFlags::LARGE_FILE, 0x040), - (FileOpenFlags::NO_FOLLOW, 0x080), - (FileOpenFlags::APPEND, 0x100), - (FileOpenFlags::PATH, 0x200), - (FileOpenFlags::SUPPORTED, 0x3ff), - ] { - assert_eq!(flag.bits(), bits); - assert_eq!(FileOpenFlags::from_bits(bits), Some(flag)); - } - assert_eq!(FileOpenFlags::all(), FileOpenFlags::SUPPORTED); - assert_eq!(FileOpenFlags::from_bits(0x400), None); - assert_eq!(FileOpenFlags::from_bits(u16::MAX), None); - } - #[test] fn directory_payload_round_trips_all_entry_shapes() { let entries = vec![ @@ -799,28 +775,6 @@ mod tests { assert_eq!(next_index, None); } - #[test] - fn directory_payload_preserves_full_width_node_info_and_rejects_zero_rdev() { - let entries = [FileDirectoryEntry { - name: "device".into(), - file_type: FileType::CharacterDevice, - node_info: Some(FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX, - rdev: Some(NonZeroU64::MAX), - }), - }]; - let mut payload = encode_directory_entries(&entries).unwrap(); - assert_eq!(decode_directory_entries(&payload).unwrap(), entries); - - let rdev_start = payload.len() - size_of::(); - payload[rdev_start..].fill(0); - assert_eq!( - decode_directory_entries(&payload), - Err(DirectoryPayloadError::Malformed) - ); - } - #[test] fn directory_payload_rejects_malformed_data() { assert_eq!( diff --git a/litebox_broker_protocol/src/fs/path.rs b/litebox_broker_protocol/src/fs/path.rs index a5bdcef27d..e230505049 100644 --- a/litebox_broker_protocol/src/fs/path.rs +++ b/litebox_broker_protocol/src/fs/path.rs @@ -68,47 +68,3 @@ impl core::fmt::Display for ResolvedPath { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use alloc::string::ToString as _; - - #[test] - fn paths_are_absolute_and_lexically_normalized() { - let base = ResolvedPath::root().resolve("/work/dir"); - for (path, expected) in [ - ("", "/work/dir"), - (".", "/work/dir"), - ("./file", "/work/dir/file"), - ("../file", "/work/file"), - ("sub/../../file", "/work/file"), - ("../../../../file", "/file"), - ("/etc//passwd/", "/etc/passwd"), - ("/.././", "/"), - ("////", "/"), - ] { - let resolved = base.resolve(path); - assert_eq!(resolved.to_string(), expected); - assert_eq!(base.resolve(&resolved.to_string()), resolved); - } - assert_eq!(base.to_string(), "/work/dir"); - assert_eq!(ResolvedPath::default(), ResolvedPath::root()); - assert_eq!(ResolvedPath::root().to_string(), "/"); - } - - #[test] - fn components_and_parent_preserve_normalized_names() { - let path = ResolvedPath::root().resolve("/a//b/../c"); - assert_eq!(path.components(), ["a", "c"]); - let (parent, name) = path.parent_and_name().unwrap(); - assert_eq!(parent, ["a"]); - assert_eq!(name, "c"); - - let path = ResolvedPath::root().resolve("/file"); - let (parent, name) = path.parent_and_name().unwrap(); - assert!(parent.is_empty()); - assert_eq!(name, "file"); - assert_eq!(ResolvedPath::root().parent_and_name(), None); - } -} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index 0903719301..4032ed70fc 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -1333,37 +1333,6 @@ mod tests { assert_eq!(decode_request(&invalid_user), Err(WireError::InvalidTag)); } - #[test] - fn decode_rejects_zero_device_numbers_in_fs_status_responses() { - let status = FileStatus { - file_type: FileType::CharacterDevice, - mode: FileMode::from_bits(0o640).unwrap(), - size: 17, - owner: FileUser::ROOT, - node_info: FileNodeInfo { - dev: 5, - ino: 7, - rdev: Some(NonZeroU64::MAX), - }, - block_size: 4096, - }; - for result in [ - FileResponse::PathStatus(status), - FileResponse::HandleStatus(status), - ] { - let response = BrokerResponse { - request_id: TEST_REQUEST_ID, - result: BrokerResult::File(result), - }; - let mut bytes = encode_response(response.clone()); - assert_eq!(decode_response(&bytes), Ok(response)); - // The optional device number precedes the final block-size field. - let rdev_start = bytes.len() - 2 * size_of::(); - bytes[rdev_start..rdev_start + size_of::()].fill(0); - assert_eq!(decode_response(&bytes), Err(WireError::InvalidTag)); - } - } - #[test] fn decode_rejects_malformed_fs_response_frames() { let status = BrokerResponse { diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index d3b0c9322e..941f4f0893 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -490,32 +490,3 @@ fn serve_control_stream( UnixStreamHostSetupChannel::into_active, ) } - -#[cfg(test)] -mod tests { - use super::windows_runner_initial_files; - use std::ffi::OsString; - use std::path::PathBuf; - - #[test] - fn windows_runner_initial_files_accepts_equals_options() { - let arguments = [ - "--env=NAME=value", - "--initial-files=root.tar", - "/app/program.exe", - ] - .map(OsString::from); - - assert_eq!( - windows_runner_initial_files(&arguments), - Some(PathBuf::from("root.tar")) - ); - } - - #[test] - fn windows_runner_initial_files_does_not_scan_guest_arguments() { - let arguments = ["/app/program.exe", "--initial-files", "guest-value"].map(OsString::from); - - assert_eq!(windows_runner_initial_files(&arguments), None); - } -} diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index aeb478abaf..9f678cda93 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -3569,165 +3569,3 @@ impl ReinterpretUsizeAsPtr> for Option(), - core::mem::size_of::() - ); - for (flag, bits) in [ - (OFlags::RDONLY, 0), - (OFlags::WRONLY, 0x1), - (OFlags::RDWR, 0x2), - (OFlags::CREAT, 0x40), - (OFlags::EXCL, 0x80), - (OFlags::NOCTTY, 0x100), - (OFlags::TRUNC, 0x200), - (OFlags::APPEND, 0x400), - (OFlags::NDELAY, 0x800), - (OFlags::NONBLOCK, 0x800), - (OFlags::DSYNC, 0x1000), - (OFlags::ASYNC, 0x2000), - (OFlags::NOATIME, 0x40000), - (OFlags::CLOEXEC, 0x80000), - (OFlags::SYNC, 0x101000), - (OFlags::PATH, 0x200000), - ] { - assert_eq!(flag.bits(), bits, "{flag:?}"); - } - #[cfg(target_arch = "x86_64")] - { - assert_eq!(OFlags::DIRECT.bits(), 0x4000); - assert_eq!(OFlags::DIRECTORY.bits(), 0x10000); - assert_eq!(OFlags::LARGEFILE.bits(), 0x8000); - assert_eq!(OFlags::NOFOLLOW.bits(), 0x20000); - assert_eq!(OFlags::TMPFILE.bits(), 0x410000); - assert_eq!(OFlags::STATUS_FLAGS_MASK.bits(), 0x34fc03); - } - #[cfg(target_arch = "aarch64")] - { - assert_eq!(OFlags::DIRECT.bits(), 0x10000); - assert_eq!(OFlags::DIRECTORY.bits(), 0x4000); - assert_eq!(OFlags::LARGEFILE.bits(), 0x20000); - assert_eq!(OFlags::NOFOLLOW.bits(), 0x8000); - assert_eq!(OFlags::TMPFILE.bits(), 0x404000); - assert_eq!(OFlags::STATUS_FLAGS_MASK.bits(), 0x373c03); - } - assert_eq!(EfdFlags::CLOEXEC.bits(), OFlags::CLOEXEC.bits()); - assert_eq!(EfdFlags::NONBLOCK.bits(), OFlags::NONBLOCK.bits()); - assert_eq!(SockFlags::CLOEXEC.bits(), OFlags::CLOEXEC.bits()); - assert_eq!(SockFlags::NONBLOCK.bits(), OFlags::NONBLOCK.bits()); - assert_eq!( - EpollCreateFlags::EPOLL_CLOEXEC.bits(), - OFlags::CLOEXEC.bits() - ); - } - - #[test] - fn open_flags_preserve_unknown_bits_in_linux_argument_decoding() { - let bits = 0x8000_0000 | OFlags::NONBLOCK.bits() | 3; - assert_eq!(OFlags::from_bits(bits).unwrap().bits(), bits); - assert_eq!(OFlags::from_bits_truncate(bits).bits(), bits); - assert_eq!( - OFlags::reinterpret_truncated_from_usize(bits as usize).bits(), - bits - ); - let Some(FcntlArg::SETFL(flags)) = FcntlArg::try_from(F_SETFL, bits as usize) else { - panic!("F_SETFL must retain unknown Linux flags"); - }; - assert_eq!(flags.bits(), bits); - } - - #[test] - fn pipe_flags_translate_only_nonblocking_status() { - assert_eq!( - OFlags::from(litebox::pipes::Flags::empty()), - OFlags::empty() - ); - assert_eq!( - OFlags::from(litebox::pipes::Flags::NON_BLOCKING), - OFlags::NONBLOCK - ); - assert_eq!( - OFlags::from(litebox::pipes::Flags::from_bits_retain(u32::MAX)), - OFlags::NONBLOCK - ); - } -} - -#[cfg(test)] -mod file_status_tests { - use super::*; - use core::num::NonZeroU64; - - fn status() -> FileStatus { - FileStatus { - file_type: FileType::CharacterDevice, - mode: FileMode::RUSR | FileMode::WUSR, - size: u64::from(u32::MAX) + 1, - owner: FileUser { - user: 12, - group: 34, - }, - node_info: FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX, - rdev: NonZeroU64::new(u64::MAX), - }, - block_size: 4096, - } - } - - #[test] - fn stat_preserves_full_width_identity_and_representable_sizes() { - let status = status(); - let stat = FileStat::try_from(status).unwrap(); - assert_eq!({ stat.st_dev }, status.node_info.dev); - assert_eq!({ stat.st_ino }, status.node_info.ino); - assert_eq!({ stat.st_rdev }, u64::MAX); - assert_eq!(u64::try_from(stat.st_size).unwrap(), status.size); - assert_eq!({ stat.st_blksize }, 4096); - assert_eq!({ stat.st_uid }, 12); - assert_eq!({ stat.st_gid }, 34); - } - - #[test] - fn stat_rejects_sizes_outside_the_signed_linux_abi() { - let mut status = status(); - status.size = u64::MAX; - assert_eq!(FileStat::try_from(status), Err(errno::Errno::EOVERFLOW)); - status.size = 0; - #[cfg(target_arch = "x86_64")] - { - status.block_size = u64::MAX; - } - #[cfg(target_arch = "aarch64")] - { - status.block_size = u64::from(i32::MAX.cast_unsigned()) + 1; - } - assert_eq!(FileStat::try_from(status), Err(errno::Errno::EOVERFLOW)); - } - - #[test] - fn statx_preserves_u64_sizes_and_checks_block_size_narrowing() { - let mut status = status(); - status.size = u64::MAX; - let stat = Statx::try_from(status).unwrap(); - assert_eq!(stat.stx_size, u64::MAX); - assert_eq!(stat.stx_ino, u64::MAX); - assert_eq!(stat.stx_dev_major, dev_major(u64::MAX)); - assert_eq!(stat.stx_dev_minor, dev_minor(u64::MAX)); - assert_eq!(stat.stx_rdev_major, dev_major(u64::MAX)); - assert_eq!(stat.stx_rdev_minor, dev_minor(u64::MAX)); - status.block_size = u64::from(u32::MAX) + 1; - assert_eq!( - Statx::try_from(status).unwrap_err(), - errno::Errno::EOVERFLOW - ); - } -} diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 9060e00cb9..585309ac3e 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -384,26 +384,6 @@ mod tests { use super::*; - #[test] - fn elf_file_size_retains_protocol_width() { - use crate::syscalls::test_broker::{Scripted, closed, opened, status}; - use crate::syscalls::tests::{FILE_HANDLE, scripted_task}; - use litebox_broker_protocol::fs::{FileStatus, FileType}; - use litebox_broker_protocol::message::FileResponse; - use litebox_common_linux::loader::ReadAt as _; - - let (files, task) = scripted_task([opened(FILE_HANDLE)]); - let file = ElfFile::new(&task, "/large").unwrap(); - files.script([ - Scripted::Reply(FileResponse::HandleStatus(FileStatus { - size: u64::MAX, - ..status(FileType::RegularFile, 0o644) - })), - closed(), - ]); - assert_eq!((&file).size().unwrap(), u64::MAX); - } - #[test] fn interpreter_reservation_is_top_down_above_low_heap() { let task = crate::syscalls::tests::init_platform(); diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 55b1135c52..1ab34c7d3e 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -2862,135 +2862,6 @@ mod tests { extern crate std; - #[test] - fn open_flags_decode_access_mode_separately() { - for (flags, access) in [ - (OFlags::RDONLY, FileAccessMode::ReadOnly), - (OFlags::WRONLY, FileAccessMode::WriteOnly), - (OFlags::RDWR, FileAccessMode::ReadWrite), - ] { - assert_eq!( - file_open_options(flags).unwrap(), - (access, FileOpenFlags::NONE) - ); - } - assert!(matches!( - file_open_options(OFlags::from_bits_retain(3)), - Err(OpenError::AccessNotAllowed) - )); - } - - #[test] - fn open_flags_translate_each_supported_flag_and_path_combinations() { - let mut combined = OFlags::RDWR; - for (linux, normalized) 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), - ] { - assert_eq!( - file_open_options(linux | OFlags::WRONLY).unwrap(), - (FileAccessMode::WriteOnly, normalized) - ); - combined |= linux; - } - assert_eq!( - file_open_options(combined).unwrap(), - (FileAccessMode::ReadWrite, FileOpenFlags::SUPPORTED) - ); - assert!(matches!( - file_open_options(OFlags::PATH | OFlags::from_bits_retain(3)), - Err(OpenError::AccessNotAllowed) - )); - } - - #[test] - fn open_flags_keep_unsupported_flags_unimplemented() { - for flags in [ - OFlags::ASYNC, - OFlags::DIRECT, - OFlags::DSYNC, - OFlags::SYNC, - OFlags::NOATIME, - OFlags::TMPFILE, - OFlags::CLOEXEC, - OFlags::from_bits_retain(0x8000_0000), - ] { - assert!( - std::panic::catch_unwind(|| file_open_options(flags)).is_err(), - "{flags:?} must remain unsupported" - ); - assert!( - std::panic::catch_unwind(|| file_open_options(flags | OFlags::PATH)).is_err(), - "O_PATH must not hide unsupported flags" - ); - } - } - - #[test] - fn stat_overflow_does_not_narrow_internal_metadata_or_statx() { - use crate::syscalls::test_broker::{Scripted, closed, opened, status}; - use crate::syscalls::tests::{FILE_HANDLE, scripted_task}; - use litebox_broker_protocol::message::FileResponse; - - let metadata = FileStatus { - size: u64::MAX, - ..status(FileType::RegularFile, 0o644) - }; - let (files, task) = scripted_task([opened(FILE_HANDLE)]); - let fd = i32::try_from( - task.sys_open("/large", OFlags::RDONLY, Mode::empty()) - .unwrap(), - ) - .unwrap(); - files.script([ - Scripted::Reply(FileResponse::HandleStatus(metadata)), - Scripted::Reply(FileResponse::HandleStatus(metadata)), - Scripted::Reply(FileResponse::HandleStatus(metadata)), - Scripted::Reply(FileResponse::HandleStatus(metadata)), - Scripted::Reply(FileResponse::PathStatus(metadata)), - Scripted::Reply(FileResponse::PathStatus(metadata)), - Scripted::Reply(FileResponse::PathStatus(metadata)), - closed(), - ]); - assert_eq!(task.file_status(fd).unwrap(), metadata); - assert_eq!(task.sys_fstat(fd), Err(Errno::EOVERFLOW)); - assert_eq!( - task.sys_statx(fd, "", AtFlags::AT_EMPTY_PATH, StatxMask::STATX_SIZE) - .unwrap() - .stx_size, - u64::MAX - ); - assert_eq!( - task.sys_faccessat(fd, "", AccessFlags::F_OK, AtFlags::AT_EMPTY_PATH), - Ok(()) - ); - assert_eq!(task.sys_stat("/large"), Err(Errno::EOVERFLOW)); - assert_eq!( - task.sys_newfstatat(litebox_common_linux::AT_FDCWD, "/large", AtFlags::empty()), - Err(Errno::EOVERFLOW) - ); - assert_eq!( - task.sys_statx( - litebox_common_linux::AT_FDCWD, - "/large", - AtFlags::empty(), - StatxMask::STATX_SIZE, - ) - .unwrap() - .stx_size, - u64::MAX - ); - task.sys_close(fd).unwrap(); - } - #[test] fn write_to_iovec_returns_partial_after_later_error() { let first = b"first"; diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 5d3557b66b..a6f504fe24 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -13,7 +13,6 @@ use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; use litebox_broker_protocol::ObjectHandle; -use litebox_broker_protocol::fs::WriteFileResponse; use litebox_broker_protocol::fs::{ FileAccessMode, FileDirectoryEntry, FileError, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileType, FileUser, MAX_FILE_TRANSFER_SIZE, encode_directory_entries_chunk, @@ -25,9 +24,7 @@ use litebox_common_linux::{ use zerocopy::FromBytes as _; use crate::UserPtrMut; -use crate::syscalls::test_broker::{ - FileCall, Scripted, ScriptedFiles, closed, failed, opened, path_status, -}; +use crate::syscalls::test_broker::{FileCall, Scripted, ScriptedFiles, closed, failed, opened}; use litebox::shim::{Exception, ExceptionInfo}; use litebox_common_linux::PtRegs; @@ -382,7 +379,7 @@ 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, u64::MAX), + directory_entry("file.txt", FileType::RegularFile, 7), directory_entry("sub", FileType::Directory, 9), ]; let (files, task) = scripted_task([]); @@ -410,7 +407,7 @@ fn getdirent64_encodes_the_entries_the_broker_returns() { vec![ (".".into(), DirentType::Directory as u8, 1, 0), ("..".into(), DirentType::Directory as u8, 1, 1), - ("file.txt".into(), DirentType::Regular as u8, u64::MAX, 2), + ("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" @@ -431,7 +428,7 @@ fn getdirent64_encodes_the_entries_the_broker_returns() { } #[test] -fn getdirent64_resumes_across_buffers_and_rejects_undersized_ones() { +fn getdirent64_resumes_across_buffers() { let entries = vec![ directory_entry("aaaaaaaaaaaaaaaa", FileType::RegularFile, 1), directory_entry("bbbbbbbbbbbbbbbb", FileType::RegularFile, 2), @@ -475,20 +472,6 @@ fn getdirent64_resumes_across_buffers_and_rejects_undersized_ones() { "the guest must resume from the continuation index the broker reported" ); close_scripted_file(&files, &task, dir_fd, FILE_HANDLE); - - // A buffer too small for even one entry is rejected rather than truncating a name. - let fresh_fd = scripted_dir_fd(&files, &task, "/dir"); - files.script(directory_pages(&entries, usize::MAX)); - let mut tiny = [0u8; 8]; - assert_eq!( - task.sys_getdirent64( - fresh_fd, - UserPtrMut::from_usize(tiny.as_mut_ptr() as usize), - tiny.len(), - ), - Err(Errno::EINVAL) - ); - close_scripted_file(&files, &task, fresh_fd, FILE_HANDLE); } #[test] @@ -533,83 +516,6 @@ fn getdirent64_translates_descriptor_and_broker_errors() { close_scripted_file(&files, &task, dir_fd, FILE_HANDLE); } -#[test] -fn open_flags_keep_cloexec_local_and_send_normalized_path_options() { - let cloexec_handle = ObjectHandle(FILE_HANDLE.0 + 1); - let (files, task) = scripted_task([opened(FILE_HANDLE), opened(cloexec_handle)]); - let flags = - OFlags::RDWR | OFlags::PATH | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::NONBLOCK; - let plain_fd = i32::try_from(task.sys_open("/path", flags, Mode::empty()).unwrap()).unwrap(); - let cloexec_fd = i32::try_from( - task.sys_openat( - litebox_common_linux::AT_FDCWD, - "/path", - flags | OFlags::CLOEXEC, - Mode::empty(), - ) - .unwrap(), - ) - .unwrap(); - let expected = FileCall::Open { - path: "/path".into(), - user: ROOT, - access: FileAccessMode::ReadWrite, - flags: FileOpenFlags::PATH - | FileOpenFlags::DIRECTORY - | FileOpenFlags::NO_FOLLOW - | FileOpenFlags::NONBLOCKING, - mode: Mode::empty(), - }; - let calls = files.take_calls(); - assert_eq!(calls.len(), 2); - for call in calls { - assert_eq!(call, expected); - } - assert_eq!(task.sys_fcntl(plain_fd, FcntlArg::GETFD), Ok(0)); - assert_eq!( - task.sys_fcntl(cloexec_fd, FcntlArg::GETFD), - Ok(FileDescriptorFlags::FD_CLOEXEC.bits()) - ); - assert_eq!( - task.sys_fcntl(cloexec_fd, FcntlArg::GETFL).unwrap() & OFlags::CLOEXEC.bits(), - 0 - ); - - files.script([closed()]); - task.close_on_exec(); - assert_eq!(files.take_calls(), vec![FileCall::Close(cloexec_handle)]); - assert_eq!( - task.sys_fcntl(cloexec_fd, FcntlArg::GETFD), - Err(Errno::EBADF) - ); - assert_eq!(task.sys_fcntl(plain_fd, FcntlArg::GETFD), Ok(0)); - close_scripted_file(&files, &task, plain_fd, FILE_HANDLE); -} - -#[test] -fn open_flags_reject_invalid_access_before_contacting_broker() { - let (files, task) = scripted_task([]); - for flags in [ - OFlags::from_bits_retain(3), - OFlags::from_bits_retain(3) | OFlags::PATH | OFlags::CLOEXEC, - ] { - assert_eq!( - task.sys_open("/invalid_access", flags, Mode::empty()), - Err(Errno::EACCES) - ); - assert_eq!( - task.sys_openat( - litebox_common_linux::AT_FDCWD, - "/invalid_access", - flags, - Mode::empty(), - ), - Err(Errno::EACCES) - ); - } - assert!(files.take_calls().is_empty()); -} - #[test] fn umask_masks_the_creation_mode_sent_to_the_broker() { let (files, task) = scripted_task([]); @@ -700,17 +606,7 @@ fn unlinkat_routes_by_flag_and_translates_broker_failures() { for (flags, error, errno) in [ (AtFlags::empty(), FileError::IsDirectory, Errno::EISDIR), - ( - AtFlags::empty(), - FileError::NoSuchFileOrDirectory, - Errno::ENOENT, - ), (AtFlags::AT_REMOVEDIR, FileError::NotEmpty, Errno::ENOTEMPTY), - ( - AtFlags::AT_REMOVEDIR, - FileError::NotDirectory, - Errno::ENOTDIR, - ), ] { files.script([failed(error)]); assert_eq!( @@ -722,81 +618,6 @@ fn unlinkat_routes_by_flag_and_translates_broker_failures() { } } -#[test] -fn read_and_write_carry_lengths_and_offsets_to_the_broker() { - let (files, task) = scripted_task([opened(FILE_HANDLE)]); - let fd = i32::try_from( - task.sys_open("/data", OFlags::RDWR, Mode::empty()) - .expect("the scripted open must succeed"), - ) - .unwrap(); - let _ = files.take_calls(); - - // A read without an offset uses the broker-owned file position. - files.script([Scripted::Read(b"hello".to_vec())]); - let mut buffer = [0u8; 8]; - assert_eq!(task.sys_read(fd, &mut buffer, None), Ok(5)); - assert_eq!( - &buffer[..5], - b"hello", - "a short read fills only what arrived" - ); - assert_eq!( - files.take_calls(), - vec![FileCall::Read { - handle: FILE_HANDLE, - length: 8, - offset: None, - }] - ); - - // `pread` passes its explicit offset through. - files.script([Scripted::Read(b"lo".to_vec())]); - assert_eq!(task.sys_read(fd, &mut buffer[..2], Some(3)), Ok(2)); - assert_eq!( - files.take_calls(), - vec![FileCall::Read { - handle: FILE_HANDLE, - length: 2, - offset: Some(3), - }] - ); - - // `pwrite` stages its bytes and offset for the broker. - files.script([Scripted::Reply(FileResponse::Write(WriteFileResponse { - written: 3, - }))]); - assert_eq!(task.sys_write(fd, b"abc", Some(7)), Ok(3)); - assert_eq!( - files.take_calls(), - vec![FileCall::Write { - handle: FILE_HANDLE, - data: b"abc".to_vec(), - offset: Some(7), - }] - ); - close_scripted_file(&files, &task, fd, FILE_HANDLE); -} - -#[test] -fn stat_translates_broker_status_and_failures() { - let (files, task) = scripted_task([path_status(FileType::RegularFile, 0o640)]); - - let stat = task.sys_stat("/status_file").expect("stat must succeed"); - assert_eq!(stat.st_mode & 0o777, 0o640); - assert_eq!( - files.take_calls(), - vec![FileCall::PathStatus { - path: "/status_file".into(), - user: ROOT, - }] - ); - - files.script([failed(FileError::NoSuchFileOrDirectory)]); - assert_eq!(task.sys_stat("/missing"), Err(Errno::ENOENT)); - let _ = files.take_calls(); -} - #[test] fn test_rlimit_nofile() { use litebox_common_linux::{Rlimit, RlimitResource, errno::Errno}; diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index af5597a329..2720aea28d 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -2796,138 +2796,6 @@ mod tests { const FILE_CREATE: u32 = 2; const FILE_OVERWRITE: u32 = 4; - #[test] - fn file_access_and_options_map_to_protocol_open_parameters() { - for (desired_access, expected_access) in [ - (0, FileAccessMode::ReadOnly), - (FileAccess::SYNCHRONIZE.bits(), FileAccessMode::ReadOnly), - (FileAccess::READ_DATA.bits(), FileAccessMode::ReadOnly), - (FileAccess::READ_ATTRIBUTES.bits(), FileAccessMode::ReadOnly), - (FileAccess::EXECUTE.bits(), FileAccessMode::ReadOnly), - (FileAccess::WRITE_DATA.bits(), FileAccessMode::WriteOnly), - (FileAccess::APPEND_DATA.bits(), FileAccessMode::WriteOnly), - (FileAccess::DELETE.bits(), FileAccessMode::WriteOnly), - ( - (FileAccess::READ_DATA | FileAccess::APPEND_DATA).bits(), - FileAccessMode::ReadWrite, - ), - (AccessMask::GENERIC_READ.bits(), FileAccessMode::ReadOnly), - (AccessMask::GENERIC_WRITE.bits(), FileAccessMode::WriteOnly), - (AccessMask::GENERIC_EXECUTE.bits(), FileAccessMode::ReadOnly), - (AccessMask::GENERIC_ALL.bits(), FileAccessMode::ReadWrite), - ] { - for (options, expected_flags) in [ - (FileCreateOptions::empty(), FileOpenFlags::NONE), - (FileCreateOptions::DIRECTORY_FILE, FileOpenFlags::DIRECTORY), - ( - FileCreateOptions::NON_DIRECTORY_FILE, - FileOpenFlags::NO_FOLLOW, - ), - ( - FileCreateOptions::WRITE_THROUGH - | FileCreateOptions::SYNCHRONOUS_IO_NONALERT - | FileCreateOptions::OPEN_REPARSE_POINT, - FileOpenFlags::NONE, - ), - ] { - assert_eq!( - FileAccess::from_desired_access(desired_access) - .open_flags(CreateDisposition::Open, options), - (expected_access, expected_flags), - "desired_access={desired_access:#x}, options={options:?}", - ); - } - } - } - - #[test] - fn create_dispositions_map_to_protocol_flags_and_truncation_access() { - for (desired_access, open_access, truncate_access) in [ - ( - FileAccess::empty(), - FileAccessMode::ReadOnly, - FileAccessMode::WriteOnly, - ), - ( - FileAccess::READ_DATA, - FileAccessMode::ReadOnly, - FileAccessMode::ReadWrite, - ), - ( - FileAccess::WRITE_DATA, - FileAccessMode::WriteOnly, - FileAccessMode::WriteOnly, - ), - ] { - for (disposition, expected_flags, truncates) in [ - (CreateDisposition::Open, FileOpenFlags::NONE, false), - ( - CreateDisposition::Create, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, - false, - ), - (CreateDisposition::OpenIf, FileOpenFlags::CREATE, false), - (CreateDisposition::Overwrite, FileOpenFlags::TRUNCATE, true), - ( - CreateDisposition::Supersede, - FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE, - true, - ), - ( - CreateDisposition::OverwriteIf, - FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE, - true, - ), - ] { - let expected_access = if truncates { - truncate_access - } else { - open_access - }; - assert_eq!( - desired_access.open_flags(disposition, FileCreateOptions::empty()), - (expected_access, expected_flags), - "desired_access={desired_access:?}, disposition={disposition:?}", - ); - } - } - } - - #[test] - fn status_metadata_preserves_inode_bits_and_saturates_signed_lengths() { - let status = FileStatus { - file_type: FileType::RegularFile, - mode: Mode::RUSR | Mode::WUSR, - size: u64::MAX, - owner: litebox_broker_protocol::fs::FileUser::ROOT, - node_info: litebox_broker_protocol::fs::FileNodeInfo { - dev: u64::MAX, - ino: u64::MAX, - rdev: None, - }, - block_size: 4096, - }; - let metadata = FileStatusMetadata::from_status(&status); - assert_eq!(metadata.end_of_file, i64::MAX); - assert_eq!(metadata.allocation_size, i64::MAX); - assert_eq!(metadata.file_id, u64::MAX); - assert_eq!(&metadata.file_id_128[..8], &u64::MAX.to_ne_bytes()); - assert_eq!(&metadata.file_id_128[8..], &[0; 8]); - assert_eq!( - DirectoryEntry::from_status(String::from("large"), &status).file_id, - -1 - ); - - let status = FileStatus { - size: 1, - block_size: u64::from(u32::MAX) + 1, - ..status - }; - let metadata = FileStatusMetadata::from_status(&status); - assert_eq!(metadata.end_of_file, 1); - assert_eq!(metadata.allocation_size, i64::from(u32::MAX) + 1); - } - fn open_object_attributes( path: &str, ) -> ( diff --git a/litebox_shim_windows/src/syscalls/registry.rs b/litebox_shim_windows/src/syscalls/registry.rs index ee8141ac84..62d27814f1 100644 --- a/litebox_shim_windows/src/syscalls/registry.rs +++ b/litebox_shim_windows/src/syscalls/registry.rs @@ -2492,55 +2492,6 @@ mod tests { const KEY_VALUE_PARTIAL_INFORMATION_DATA_OFFSET: usize = offset_of!(KeyValuePartialInformation, data); - #[test] - fn registry_access_maps_to_protocol_directory_open_parameters() { - for (desired_access, expected_access) in [ - (0, FileAccessMode::ReadOnly), - ( - RegistryKeyAccess::QUERY_VALUE.bits(), - FileAccessMode::ReadOnly, - ), - ( - RegistryKeyAccess::ENUMERATE_SUB_KEYS.bits(), - FileAccessMode::ReadOnly, - ), - (RegistryKeyAccess::NOTIFY.bits(), FileAccessMode::ReadOnly), - ( - RegistryKeyAccess::SET_VALUE.bits(), - FileAccessMode::WriteOnly, - ), - ( - RegistryKeyAccess::CREATE_SUB_KEY.bits(), - FileAccessMode::WriteOnly, - ), - ( - RegistryKeyAccess::CREATE_LINK.bits(), - FileAccessMode::WriteOnly, - ), - ( - (RegistryKeyAccess::QUERY_VALUE | RegistryKeyAccess::SET_VALUE).bits(), - FileAccessMode::ReadWrite, - ), - (AccessMask::DELETE.bits(), FileAccessMode::WriteOnly), - (AccessMask::WRITE_DAC.bits(), FileAccessMode::WriteOnly), - (AccessMask::WRITE_OWNER.bits(), FileAccessMode::WriteOnly), - (AccessMask::GENERIC_READ.bits(), FileAccessMode::ReadOnly), - (AccessMask::GENERIC_WRITE.bits(), FileAccessMode::WriteOnly), - (AccessMask::GENERIC_EXECUTE.bits(), FileAccessMode::ReadOnly), - (AccessMask::GENERIC_ALL.bits(), FileAccessMode::ReadWrite), - ( - AccessMask::MAXIMUM_ALLOWED.bits(), - FileAccessMode::ReadWrite, - ), - ] { - assert_eq!( - RegistryKeyAccess::from_desired_access(desired_access).open_flags(), - (expected_access, FileOpenFlags::DIRECTORY), - "desired_access={desired_access:#x}", - ); - } - } - #[cfg(all(target_os = "windows", target_arch = "x86_64"))] #[allow(non_snake_case)] #[link(name = "advapi32")] @@ -2777,49 +2728,6 @@ mod tests { query_status } - /// Building a store must not touch the file system: the guest may never use the registry, and - /// a shim is constructed before the guest can be asked to pay for registry startup. - #[test] - fn registry_store_construction_issues_no_file_requests() { - // This broker association serves no files at all and panics on any request, so building - // the store at all is the assertion. - let litebox = Arc::new(crate::test_broker::litebox(test_platform())); - let registry = RegistryStore::new(litebox); - assert!(!*registry.defaults_seeded.lock()); - } - - #[test] - fn registry_defaults_are_seeded_once_on_first_use() { - 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(); - - // The raw store is read directly here so that the read itself does not seed it. - assert!(matches!( - registry - .fs - .path_file_status(®istry.fs_context, &*value_path), - Err(FileStatusError::PathError( - PathError::NoSuchFileOrDirectory | PathError::MissingComponent - )) - )); - - let value = registry - .read_value_at_path(&key_path, "ACP") - .expect("the first registry operation must seed the defaults"); - assert_eq!(value.data, DEFAULT_ACP_VALUE); - assert!(*registry.defaults_seeded.lock()); - - // A guest write must survive a later operation, which must not re-seed over it. - registry - .write_value_at_path(&key_path, "ACP", RegistryValueType::Sz.into(), b"9\0") - .unwrap(); - assert_eq!( - registry.read_value_at_path(&key_path, "ACP").unwrap().data, - b"9\0" - ); - } - #[test] fn registry_store_separates_values_from_subkeys() { let (litebox, registry) = test_registry(); diff --git a/litebox_shim_windows/src/tests.rs b/litebox_shim_windows/src/tests.rs index 49ef5f6c34..acb4a756fe 100644 --- a/litebox_shim_windows/src/tests.rs +++ b/litebox_shim_windows/src/tests.rs @@ -204,15 +204,6 @@ fn test_task_from_litebox(litebox: litebox::LiteBox) -> Task Date: Thu, 10 Sep 2026 20:24:13 -0700 Subject: [PATCH 20/34] Minimize filesystem migration changes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/fs/file.rs | 39 +- litebox_broker_core/src/fs/backend.rs | 12 +- litebox_broker_core/src/fs/composer.rs | 16 +- litebox_broker_core/src/fs/devices.rs | 31 +- litebox_broker_core/src/fs/in_mem.rs | 61 +- litebox_broker_core/src/fs/inode_allocator.rs | 8 +- litebox_broker_core/src/fs/mod.rs | 91 +++ litebox_broker_core/src/fs/nine_p/mod.rs | 106 ++- litebox_broker_core/src/fs/nine_p/tests.rs | 185 +---- litebox_broker_core/src/fs/overlay.rs | 82 +- litebox_broker_core/src/fs/resolver.rs | 168 ++-- litebox_broker_core/src/fs/service.rs | 47 +- litebox_broker_core/src/fs/tar_ro.rs | 34 +- litebox_broker_core/src/fs/test_support.rs | 10 +- litebox_broker_core/src/fs/tests.rs | 740 +++--------------- litebox_broker_protocol/src/fs.rs | 122 ++- litebox_broker_protocol/src/fs/path.rs | 70 -- litebox_shim_windows/src/syscalls/file.rs | 12 +- 18 files changed, 667 insertions(+), 1167 deletions(-) delete mode 100644 litebox_broker_protocol/src/fs/path.rs diff --git a/litebox/src/fs/file.rs b/litebox/src/fs/file.rs index 8a2bca3a8c..5f1d93bb4d 100644 --- a/litebox/src/fs/file.rs +++ b/litebox/src/fs/file.rs @@ -5,13 +5,14 @@ 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, ResolvedPath, + FileSeekWhence as SeekWhence, FileStatus, FileUser as UserInfo, }; use crate::path::Arg; @@ -290,7 +291,7 @@ impl Context { #[must_use] pub fn new() -> Self { Self { - cwd: Arc::new(ResolvedPath::root()), + cwd: Arc::new(ResolvedPath { components: vec![] }), user_info: UserInfo { user: 1000, group: 1000, @@ -300,7 +301,21 @@ impl Context { /// Resolve `path` against the current context. pub fn resolve(&self, path: impl Arg) -> Result { - Ok(self.cwd.resolve(path.as_rust_str()?)) + 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 }) } } @@ -310,6 +325,24 @@ impl Default for Context { } } +/// 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 diff --git a/litebox_broker_core/src/fs/backend.rs b/litebox_broker_core/src/fs/backend.rs index 34ab751d2a..d12f5832bf 100644 --- a/litebox_broker_core/src/fs/backend.rs +++ b/litebox_broker_core/src/fs/backend.rs @@ -9,11 +9,11 @@ use core::any::{Any, TypeId}; use core::marker::PhantomData; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileOpenFlags, FileStatus, - FileUser as UserInfo, + FileDirectoryEntry, FileMode as Mode, FileStatus, FileUser as UserInfo, }; use litebox_broker_protocol::stdio::StdioOutputStream; +use super::OFlags; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, @@ -114,12 +114,11 @@ pub trait Backend: private::Sealed + Send + Sync + Any { components: &[&str], ) -> Result>, WalkError>; - /// Take an owned handle to a `dir` found via a walk, validating `access` and open `flags`. + /// Take an owned handle to a `dir` found via a walk, validating any open `flags`. fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result; /// Obtain a walking handle to an existing owned dir. @@ -138,8 +137,7 @@ pub trait Backend: private::Sealed + Send + Sync + Any { &self, dir: WalkingDirHandle<'_>, name: &str, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result, OpenError>; /// Read directory entries at `dir`. diff --git a/litebox_broker_core/src/fs/composer.rs b/litebox_broker_core/src/fs/composer.rs index ff9007db43..927fbcf393 100644 --- a/litebox_broker_core/src/fs/composer.rs +++ b/litebox_broker_core/src/fs/composer.rs @@ -10,8 +10,7 @@ use alloc::vec; use alloc::vec::Vec; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, - FileType, FileUser as UserInfo, + FileDirectoryEntry, FileMode as Mode, FileStatus, FileType, FileUser as UserInfo, }; use super::backend::{ @@ -23,6 +22,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::{InodeAllocator, InodeAllocators}; +use super::{NodeInfo, OFlags}; use thiserror::Error; // XXX(jayb): consider removing this via a runtime reserved device ID? @@ -53,7 +53,7 @@ struct Mount { #[derive(Clone)] struct VirtualDir { path: Vec, - node_info: FileNodeInfo, + node_info: NodeInfo, } /// Composer construction errors. @@ -546,8 +546,7 @@ impl Backend for Composer { fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result { let dir = dir.into_typed::(); let inner = match dir.inner { @@ -563,7 +562,7 @@ impl Backend for Composer { mount_index, handle: self.mounts[mount_index] .backend - .owned_dir_at(handle, access, flags)?, + .owned_dir_at(handle, flags)?, }, }; Ok(DirHandle::from_typed::(ComposerDirHandle { inner })) @@ -599,8 +598,7 @@ impl Backend for Composer { &self, dir: WalkingDirHandle<'_>, name: &str, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result, OpenError> { let dir = dir.into_typed::(); match dir.inner { @@ -619,7 +617,7 @@ impl Backend for Composer { )?; self.mounts[mount_index] .backend - .open_file_at(handle, name, access, flags) + .open_file_at(handle, name, flags) .map(|file| Permissioned { item: FileHandle::from_typed::(ComposerFileHandle { mount_index, diff --git a/litebox_broker_core/src/fs/devices.rs b/litebox_broker_core/src/fs/devices.rs index bbc3b2a543..3aee0670de 100644 --- a/litebox_broker_core/src/fs/devices.rs +++ b/litebox_broker_core/src/fs/devices.rs @@ -9,8 +9,7 @@ use alloc::string::String; use alloc::vec; use alloc::vec::Vec; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, - FileType, FileUser as UserInfo, + FileDirectoryEntry, FileMode as Mode, FileStatus, FileType, FileUser as UserInfo, }; use litebox_broker_protocol::random::MAX_RANDOM_TRANSFER_SIZE; use litebox_broker_protocol::stdio::StdioOutputStream; @@ -24,6 +23,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::InodeAllocator; +use super::{NodeInfo, OFlags}; /// Block size for stdio devices const STDIO_BLOCK_SIZE: u64 = 1024; @@ -40,20 +40,20 @@ const URANDOM_BLOCK_SIZE: u64 = 0x1000; /// name=/dev/stderr dev=64 ino=9 rdev=34822 /// ``` // XXX(jayb): Should we be pulling the device names and such from the inode allocator? -const STDIO_NODE_INFO: FileNodeInfo = FileNodeInfo { +const STDIO_NODE_INFO: NodeInfo = NodeInfo { dev: 64, ino: 9, rdev: core::num::NonZeroU64::new(34822), }; /// Node info for /dev/null -const NULL_NODE_INFO: FileNodeInfo = FileNodeInfo { +const NULL_NODE_INFO: NodeInfo = NodeInfo { dev: 5, ino: 4, // major=1, minor=3 rdev: core::num::NonZeroU64::new(0x103), }; /// Node info for /dev/urandom -const URANDOM_NODE_INFO: FileNodeInfo = FileNodeInfo { +const URANDOM_NODE_INFO: NodeInfo = NodeInfo { dev: 5, ino: 8, // major=1, minor=9 @@ -115,7 +115,7 @@ impl Device { /// A [`super::backend::Backend`] that supports Unix-y devices. pub struct Devices { /// Stable inode info for this backend's root directory. - root_inode: FileNodeInfo, + root_inode: NodeInfo, _alloc: InodeAllocator, } @@ -183,8 +183,7 @@ impl Backend for Devices { fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - _access: FileAccessMode, - _flags: FileOpenFlags, + _flags: OFlags, ) -> Result { Ok(DirHandle::from_typed::(dir.into_typed::())) } @@ -199,17 +198,16 @@ impl Backend for Devices { &self, dir: WalkingDirHandle<'_>, name: &str, - _access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result, OpenError> { let _dir = dir.into_typed::(); let device = Device::from_name(name) .ok_or(OpenError::PathError(PathError::NoSuchFileOrDirectory))?; - if flags.contains(FileOpenFlags::DIRECTORY) { + if flags.contains(OFlags::DIRECTORY) { return Err(OpenError::PathError(PathError::ComponentNotADirectory)); } - if flags.contains(FileOpenFlags::NONBLOCKING) + if flags.contains(OFlags::NONBLOCK) && matches!( device, Device::Stdin | Device::Stdout | Device::Stderr | Device::URandom @@ -218,7 +216,7 @@ impl Backend for Devices { unimplemented!("Non-blocking I/O is not yet supported for {:?}", device); } - if flags.contains(FileOpenFlags::TRUNCATE) { + if flags.contains(OFlags::TRUNC) { // Note: matching Linux behavior, this does not actually perform any truncation, and // instead, it is silently ignored if you attempt to truncate upon opening stdio. debug_assert!(matches!( @@ -377,12 +375,7 @@ mod tests { fn urandom_requires_broker_only_for_nonempty_reads() { let devices = Devices::new(InodeAllocator::standalone()); let urandom = devices - .open_file_at( - devices.root(), - "urandom", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - ) + .open_file_at(devices.root(), "urandom", OFlags::RDONLY) .unwrap() .item; diff --git a/litebox_broker_core/src/fs/in_mem.rs b/litebox_broker_core/src/fs/in_mem.rs index fa3457471f..434a05aa0c 100644 --- a/litebox_broker_core/src/fs/in_mem.rs +++ b/litebox_broker_core/src/fs/in_mem.rs @@ -9,11 +9,11 @@ use alloc::vec::Vec; use hashbrown::HashMap; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, - FileType, FileUser as UserInfo, + FileDirectoryEntry, FileMode as Mode, FileStatus, FileType, FileUser as UserInfo, }; use litebox_platform::sync; +use super::NodeInfo; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WriteError, @@ -211,8 +211,8 @@ impl super::backend::private::Sealed pub struct InMemDirHandle { dir: DirNode, /// The flags the directory was opened with; walking handles are not opened for access, and - /// thus use [`FileOpenFlags::PATH`]. - flags: FileOpenFlags, + /// thus use [`super::OFlags::PATH`]. + flags: super::OFlags, } impl Clone for InMemDirHandle { fn clone(&self) -> Self { @@ -245,7 +245,7 @@ impl super::backend::Backend for InMe fn root(&self) -> super::backend::WalkingDirHandle<'_> { super::backend::WalkingDirHandle::from_typed::(InMemDirHandle { dir: self.root.clone(), - flags: FileOpenFlags::PATH, + flags: super::OFlags::PATH, }) } @@ -285,7 +285,7 @@ impl super::backend::Backend for InMe }); current = InMemDirHandle { dir: child, - flags: FileOpenFlags::PATH, + flags: super::OFlags::PATH, }; } Ok(super::backend::WalkOutcome { @@ -298,24 +298,19 @@ impl super::backend::Backend for InMe fn owned_dir_at( &self, dir: super::backend::WalkingDirHandle<'_>, - access: FileAccessMode, - flags: FileOpenFlags, + flags: super::OFlags, ) -> Result { - assert_supported_flags(flags); - if matches!( - access, - FileAccessMode::WriteOnly | FileAccessMode::ReadWrite - ) { + assert_supported_oflags(flags); + if flags.intersects(super::OFlags::WRONLY | super::OFlags::RDWR) { // XXX(jayb): POSIX requires `EISDIR` when write access is requested on a directory, but // `OpenError` has no such variant yet. Allowing write-mode directory handles here is a - // workaround for the Windows access mapping introduced by PR #894 + // workaround for the Windows access-to-`OFlags` mapping introduced by PR #894 // (afe6cddc): Windows directory modification rights (such as `DELETE`) use write mode // to trigger permission checks even though the handle is not used as a writable byte // stream. A cleaner design would separate permission-check intent from handle I/O mode, // allowing the Windows shim to request modification permission on a non-stream // directory handle while POSIX opens with `WRONLY` or `RDWR` return `EISDIR`. litebox_util_log::debug!( - access:? = access, flags:? = flags; "using writable-directory workaround for permission checks" ); @@ -335,7 +330,7 @@ impl super::backend::Backend for InMe Some(super::backend::WalkingDirHandle::from_typed::( InMemDirHandle { dir: dir.get_typed::().dir.clone(), - flags: FileOpenFlags::PATH, + flags: super::OFlags::PATH, }, )) } @@ -344,10 +339,9 @@ impl super::backend::Backend for InMe &self, dir: super::backend::WalkingDirHandle<'_>, name: &str, - _access: FileAccessMode, - flags: FileOpenFlags, + flags: super::OFlags, ) -> Result, OpenError> { - assert_supported_flags(flags); + assert_supported_oflags(flags); let dir = dir.into_typed::(); let child = dir .dir @@ -359,12 +353,12 @@ impl super::backend::Backend for InMe let Node::File(file) = child else { return Err(PathError::ComponentNotADirectory.into()); }; - if flags.contains(FileOpenFlags::DIRECTORY) { + if flags.contains(super::OFlags::DIRECTORY) { return Err(PathError::ComponentNotADirectory.into()); } let perms = file.read().perms.clone(); let handle = super::backend::FileHandle::from_typed::(InMemFileHandle { file }); - if flags.contains(FileOpenFlags::TRUNCATE) && !flags.contains(FileOpenFlags::PATH) { + if flags.contains(super::OFlags::TRUNC) && !flags.contains(super::OFlags::PATH) { // Linux truncates whenever the open succeeds, regardless of the access mode (an // `O_RDONLY|O_TRUNC` open of a writable file does truncate it); `O_PATH` opens ignore // `O_TRUNC` entirely. @@ -553,7 +547,7 @@ impl super::backend::Backend for InMe InMemDirHandle { dir: child, // TODO(jayb): is this the right set of flags here? - flags: FileOpenFlags::PATH, + flags: super::OFlags::PATH, }, )) } @@ -635,8 +629,23 @@ impl super::backend::Backend for InMe } } -fn assert_supported_flags(flags: FileOpenFlags) { - if !flags.difference(FileOpenFlags::all()).is_empty() { +/// Flags this backend knows how to honor when opening files/directories. +const SUPPORTED_OFLAGS: super::OFlags = super::OFlags::CREAT + .union(super::OFlags::RDONLY) + .union(super::OFlags::WRONLY) + .union(super::OFlags::RDWR) + .union(super::OFlags::TRUNC) + .union(super::OFlags::NOCTTY) + .union(super::OFlags::EXCL) + .union(super::OFlags::DIRECTORY) + .union(super::OFlags::NONBLOCK) + .union(super::OFlags::LARGEFILE) + .union(super::OFlags::NOFOLLOW) + .union(super::OFlags::APPEND) + .union(super::OFlags::PATH); + +fn assert_supported_oflags(flags: super::OFlags) { + if flags.intersects(SUPPORTED_OFLAGS.complement()) { unimplemented!("{flags:?}") } } @@ -662,14 +671,14 @@ type DirNode = Arc>>; struct DirData { perms: Permissions, children: HashMap>, - node_info: FileNodeInfo, + node_info: NodeInfo, } type FileNode = Arc>; struct FileData { perms: Permissions, data: alloc::borrow::Cow<'static, [u8]>, - node_info: FileNodeInfo, + node_info: NodeInfo, } #[derive(Clone, Debug)] diff --git a/litebox_broker_core/src/fs/inode_allocator.rs b/litebox_broker_core/src/fs/inode_allocator.rs index 577d52ec2a..3e96043abf 100644 --- a/litebox_broker_core/src/fs/inode_allocator.rs +++ b/litebox_broker_core/src/fs/inode_allocator.rs @@ -3,7 +3,7 @@ use core::sync::atomic::{AtomicU64, Ordering}; -use litebox_broker_protocol::fs::FileNodeInfo; +use super::NodeInfo; /// Hands out [`InodeAllocator`]s, each with its own device id. #[derive(Debug)] @@ -53,11 +53,11 @@ impl InodeAllocator { Self::for_device(STANDALONE_DEVICE_ID) } - /// Allocate a fresh [`FileNodeInfo`] for a new entry on this backend. + /// Allocate a fresh [`NodeInfo`] for a new entry on this backend. #[must_use] - pub fn next(&self) -> FileNodeInfo { + pub fn next(&self) -> NodeInfo { let ino = self.counter.fetch_add(1, Ordering::Relaxed); - FileNodeInfo { + NodeInfo { dev: self.device_id(), ino, rdev: None, diff --git a/litebox_broker_core/src/fs/mod.rs b/litebox_broker_core/src/fs/mod.rs index f81680c278..4d977088e3 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -8,6 +8,10 @@ //! via the [`Composer`](composer::Composer), or stacked as a writable upper over immutable lowers //! via the [`Overlay`](overlay::Overlay). +use bitflags::bitflags; + +use core::ffi::c_uint; + pub mod backend; pub mod composer; pub mod devices; @@ -25,11 +29,98 @@ mod test_support; #[cfg(test)] mod tests; +pub use litebox_broker_protocol::fs::FileNodeInfo as NodeInfo; 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! { + /// `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 size reported as the size of a directory. 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 90e7ff43b8..71bbd24276 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -14,10 +14,7 @@ use alloc::vec::Vec; use core::num::NonZeroU64; use core::sync::atomic::{AtomicBool, Ordering}; -use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileMode, FileNodeInfo, FileOpenFlags, FileStatus, - FileType, FileUser, -}; +use litebox_broker_protocol::fs::{FileDirectoryEntry, FileMode, FileStatus, FileType, FileUser}; use litebox_platform::sync; use thiserror::Error; @@ -30,6 +27,7 @@ use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; +use super::{NodeInfo, OFlags}; mod client; mod fcall; mod id_pool; @@ -55,7 +53,7 @@ pub struct NineP>, - /// Device id reported in every [`FileNodeInfo`] from this backend; inode numbers + /// Device id reported in every [`NodeInfo`] from this backend; inode numbers /// come from the server's qids instead. device_id: u64, /// Whether `unlinkat` is supported by the server @@ -351,20 +349,16 @@ where fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result { - assert_supported_flags(flags); - if matches!( - access, - FileAccessMode::WriteOnly | FileAccessMode::ReadWrite - ) { + assert_supported_oflags(flags); + if flags.intersects(OFlags::WRONLY | OFlags::RDWR) { // TODO(jayb): POSIX requires `EISDIR` when write access is requested on a directory, // but `OpenError` has no such variant yet. unimplemented!() } let (fid, is_backend_root) = dir.into_typed::().into_dir(); - if flags.contains(FileOpenFlags::PATH) { + if flags.contains(OFlags::PATH) { // An `O_PATH` handle is never opened server-side, so the walked fid can be handed over // as-is, even when it is the shared root fid. return Ok(DirHandle::from_typed::(NinePDirHandle { fid })); @@ -399,13 +393,12 @@ where &self, dir: WalkingDirHandle<'_>, name: &str, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result, OpenError> { - assert_supported_flags(flags); + assert_supported_oflags(flags); // TODO: we do not support non-blocking, so ignore that flag instead of returning an error. - let flags = flags - FileOpenFlags::NONBLOCKING; - if flags.contains(FileOpenFlags::DIRECTORY) { + let flags = flags - OFlags::NONBLOCK; + if flags.contains(OFlags::DIRECTORY) { return Err(OpenError::PathError(PathError::ComponentNotADirectory)); } @@ -421,17 +414,14 @@ where } }; - if !flags.contains(FileOpenFlags::PATH) { + if !flags.contains(OFlags::PATH) { // An `O_PATH` handle addresses the file without opening it server-side. // // The file exists (it is what stopped the walk), so the creation flags say nothing // about how to open it; the resolver enforces `O_CREAT | O_EXCL` itself. self.client.open( &fid.fid, - open_flags_to_lopen( - access, - flags - FileOpenFlags::CREATE - FileOpenFlags::EXCLUSIVE, - ), + oflags_to_lopen(flags - OFlags::CREAT - OFlags::EXCL), )?; } Ok(Permissioned { @@ -455,7 +445,7 @@ where .map(|entry| FileDirectoryEntry { name: String::from_utf8_lossy(&entry.name).into_owned(), file_type: qid_type_to_file_type(entry.qid.typ), - node_info: Some(FileNodeInfo { + node_info: Some(NodeInfo { dev: self.device_id, ino: entry.qid.path, rdev: None, @@ -640,42 +630,72 @@ fn backend_checked_components(count: usize) -> Vec { ] } -fn assert_supported_flags(flags: FileOpenFlags) { - if !flags.difference(FileOpenFlags::all()).is_empty() { +/// Flags this backend knows how to honor when opening files/directories. +const SUPPORTED_OFLAGS: 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); + +fn assert_supported_oflags(flags: OFlags) { + if flags.intersects(SUPPORTED_OFLAGS.complement()) { unimplemented!("{flags:?}") } } -/// Convert canonical open intent to the 9P2000.L wire flags. -fn open_flags_to_lopen(access: FileAccessMode, flags: FileOpenFlags) -> fcall::LOpenFlags { - let mut lflags = match access { - FileAccessMode::ReadOnly => fcall::LOpenFlags::empty(), - FileAccessMode::WriteOnly => fcall::LOpenFlags::O_WRONLY, - FileAccessMode::ReadWrite => fcall::LOpenFlags::O_RDWR, - _ => unimplemented!("{access:?}"), - }; +/// Convert [`OFlags`] to 9P `LOpenFlags` +fn oflags_to_lopen(flags: OFlags) -> fcall::LOpenFlags { + let mut lflags = fcall::LOpenFlags::empty(); + + // Access mode (RDONLY is 0, so we only check for WRONLY and RDWR) + if flags.contains(OFlags::RDWR) { + lflags |= fcall::LOpenFlags::O_RDWR; + } else if flags.contains(OFlags::WRONLY) { + lflags |= fcall::LOpenFlags::O_WRONLY; + } + // RDONLY is implicit if neither WRONLY nor RDWR - if flags.contains(FileOpenFlags::CREATE) { + if flags.contains(OFlags::CREAT) { lflags |= fcall::LOpenFlags::O_CREAT; } - if flags.contains(FileOpenFlags::EXCLUSIVE) { + if flags.contains(OFlags::EXCL) { lflags |= fcall::LOpenFlags::O_EXCL; } - if flags.contains(FileOpenFlags::TRUNCATE) { + if flags.contains(OFlags::TRUNC) { lflags |= fcall::LOpenFlags::O_TRUNC; } - if flags.contains(FileOpenFlags::APPEND) { + if flags.contains(OFlags::APPEND) { lflags |= fcall::LOpenFlags::O_APPEND; } - if flags.contains(FileOpenFlags::DIRECTORY) { + if flags.contains(OFlags::DIRECTORY) { lflags |= fcall::LOpenFlags::O_DIRECTORY; } - if flags.contains(FileOpenFlags::NO_FOLLOW) { + if flags.contains(OFlags::NOFOLLOW) { lflags |= fcall::LOpenFlags::O_NOFOLLOW; } - if flags.contains(FileOpenFlags::NONBLOCKING) { + if flags.contains(OFlags::NONBLOCK) { lflags |= fcall::LOpenFlags::O_NONBLOCK; } + if flags.contains(OFlags::SYNC) { + lflags |= fcall::LOpenFlags::O_SYNC; + } + if flags.contains(OFlags::DSYNC) { + lflags |= fcall::LOpenFlags::O_DSYNC; + } + if flags.contains(OFlags::DIRECT) { + lflags |= fcall::LOpenFlags::O_DIRECT; + } + if flags.contains(OFlags::NOATIME) { + lflags |= fcall::LOpenFlags::O_NOATIME; + } lflags } @@ -711,7 +731,7 @@ fn rgetattr_to_file_status(attr: &fcall::Rgetattr, device_id: u64) -> Result Result, + ids: HashMap, /// Files that have been copied up, by overlay identity, and their handle in the upper backend. /// A handle opened against a lower backend stays valid, but every operation looks here first. - copied_up: HashMap, + copied_up: HashMap, } /// A node as identified by the layer that owns it; `Lower` carries the lower backend's index. #[derive(Clone, PartialEq, Eq, Hash)] enum LayerNode { - Upper(FileNodeInfo), - Lower(usize, FileNodeInfo), + Upper(NodeInfo), + Lower(usize, NodeInfo), } pub struct OverlayWalkingDir { @@ -105,7 +105,7 @@ enum OverlayFileLayer { layer: usize, handle: FileHandle, /// The overlay identity of the file, under which a later copy-up records its upper handle. - node: FileNodeInfo, + node: NodeInfo, }, } @@ -161,19 +161,11 @@ impl Overlay { } fn resolve_root(&self) -> Result { - let upper = self.upper.owned_dir_at( - self.upper.root(), - FileAccessMode::ReadOnly, - FileOpenFlags::PATH, - )?; + let upper = self.upper.owned_dir_at(self.upper.root(), OFlags::PATH)?; let lowers = self .lowers .iter() - .map(|lower| { - lower - .owned_dir_at(lower.root(), FileAccessMode::ReadOnly, FileOpenFlags::PATH) - .map(Some) - }) + .map(|lower| lower.owned_dir_at(lower.root(), OFlags::PATH).map(Some)) .collect::, _>>()?; self.merge(Some(upper), lowers) } @@ -204,11 +196,7 @@ impl Overlay { return Err(PathError::ComponentNotADirectory.into()); } let component = component.clone(); - let owned = backend.owned_dir_at( - outcome.last, - FileAccessMode::ReadOnly, - FileOpenFlags::PATH, - )?; + let owned = backend.owned_dir_at(outcome.last, OFlags::PATH)?; Ok((owned, component)) } @@ -454,11 +442,7 @@ impl Overlay { locked: &NamespaceGuard<'_, Platform>, path: &[String], ) -> Result { - let mut upper = self.upper.owned_dir_at( - self.upper.root(), - FileAccessMode::ReadOnly, - FileOpenFlags::PATH, - )?; + let mut upper = self.upper.owned_dir_at(self.upper.root(), OFlags::PATH)?; for index in 0..path.len() { // Re-resolve after each materialisation, since it changed the upper namespace. let resolved = self.resolve_dir(&path[..=index])?; @@ -546,13 +530,13 @@ impl Overlay { /// The overlay-visible identity of `node` as owned by `layer`, allocated on first sight. fn map_node( &self, - ids: &mut HashMap, + ids: &mut HashMap, layer: Option, - node: FileNodeInfo, - ) -> FileNodeInfo { + node: NodeInfo, + ) -> NodeInfo { let rdev = node.rdev; *ids.entry(layer_node(layer, node)) - .or_insert_with(|| FileNodeInfo { + .or_insert_with(|| NodeInfo { rdev, ..self.alloc.next() }) @@ -572,8 +556,8 @@ impl Overlay { fn bind_copy_up( &self, layer: usize, - lower: FileNodeInfo, - upper: FileNodeInfo, + lower: NodeInfo, + upper: NodeInfo, upper_file: Option<&FileHandle>, ) { let mut state = self.state.lock(); @@ -698,7 +682,7 @@ impl Overlay { /// The node `node` as owned by `layer`, which is `None` for the upper backend and `Some(index)` /// for a lower one. -fn layer_node(layer: Option, node: FileNodeInfo) -> LayerNode { +fn layer_node(layer: Option, node: NodeInfo) -> LayerNode { match layer { None => LayerNode::Upper(node), Some(layer) => LayerNode::Lower(layer, node), @@ -792,14 +776,13 @@ impl Backend for Overlay { fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result { let path = dir.into_typed::().path; let resolved = self.resolve_dir(&path)?; let (_, backend, handle) = self.owning_dir(&resolved).ok_or(OpenError::Io)?; let walking = backend.walking_dir_at(handle).ok_or(OpenError::Io)?; - backend.owned_dir_at(walking, access, flags)?; + backend.owned_dir_at(walking, flags)?; Ok(DirHandle::from_typed::(OverlayDir { path })) } @@ -813,13 +796,12 @@ impl Backend for Overlay { &self, dir: WalkingDirHandle<'_>, name: &str, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result, OpenError> { if !valid(name) { return Err(PathError::InvalidPathname.into()); } - if flags.contains(FileOpenFlags::DIRECTORY) { + if flags.contains(OFlags::DIRECTORY) { return Err(PathError::ComponentNotADirectory.into()); } let path = dir.into_typed::().path; @@ -831,14 +813,14 @@ impl Backend for Overlay { .ok_or(OpenError::PathError(PathError::NoSuchFileOrDirectory))?; // The resolver only reaches `create_file_at` once a walk reported the name as missing, so // an existing entry means an exclusive create must fail here. - if flags.contains(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE) { + if flags.contains(OFlags::CREAT | OFlags::EXCL) { return Err(OpenError::AlreadyExists); } let (layer, permissions) = if entry.upper { let upper = resolved.upper.as_ref().ok_or(OpenError::Io)?; let walking = self.upper.walking_dir_at(upper).ok_or(OpenError::Io)?; - let file = self.upper.open_file_at(walking, name, access, flags)?; + let file = self.upper.open_file_at(walking, name, flags)?; (OverlayFileLayer::Upper(file.item), file.permissions) } else { let layer = entry.lower.ok_or(OpenError::Io)?; @@ -852,16 +834,14 @@ impl Backend for Overlay { // XXX(jayb): the resolver authorizes an open only after this returns, so a // writable open can copy up before a later permission denial. A preflight // authorization hook in `Backend` would make copy-up properly two-phase. - let writing = matches!( - access, - FileAccessMode::WriteOnly | FileAccessMode::ReadWrite - ) || flags.intersects(FileOpenFlags::APPEND | FileOpenFlags::TRUNCATE); - let (lower_access, lower_flags) = if writing { - (FileAccessMode::ReadOnly, FileOpenFlags::empty()) + let writing = + flags.intersects(OFlags::WRONLY | OFlags::RDWR | OFlags::APPEND | OFlags::TRUNC); + let lower_flags = if writing { + OFlags::RDONLY } else { - (access, flags.difference(FileOpenFlags::CREATE)) + flags.difference(OFlags::CREAT) }; - let file = self.lowers[layer].open_file_at(walking, name, lower_access, lower_flags)?; + let file = self.lowers[layer].open_file_at(walking, name, lower_flags)?; // The file's own identity, which is also the key a later copy-up records itself under. let status = self.lowers[layer] .status(HandleRef::File(&file.item)) @@ -878,7 +858,7 @@ impl Backend for Overlay { name, (layer, &file.item), &status, - flags.contains(FileOpenFlags::TRUNCATE), + flags.contains(OFlags::TRUNC), )?; // The lower open was substituted with a read-only one, so its `PermissionCheck` // says nothing about the caller's write access; check the file's own mode instead. diff --git a/litebox_broker_core/src/fs/resolver.rs b/litebox_broker_core/src/fs/resolver.rs index 9054195936..91f18b472e 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -4,21 +4,25 @@ //! Path management, permission checks, and open-state operations above [`super::backend`]. use alloc::string::String; +use alloc::vec; use alloc::vec::Vec; use core::marker::PhantomData; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileOpenFlags, - FileSeekWhence as SeekWhence, FileStatus, FileType, FileUser as UserInfo, ResolvedPath, + FileDirectoryEntry, FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, FileType, + FileUser as UserInfo, }; -use super::backend::{ - CreationMetadata, DeviceIo, DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, - Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, -}; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; +use super::{ + OFlags, + backend::{ + CreationMetadata, DeviceIo, DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, + Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, + }, +}; /// The broker-core filesystem resolver, generic over its synchronization platform and /// [`Backend`](super::backend::Backend). @@ -51,6 +55,22 @@ impl Context { self.user_info } + fn resolve(path: &str) -> ResolvedPath { + let mut components = vec![]; + for component in path.split('/') { + match component { + "" | "." => {} + ".." => { + let _ = components.pop(); + } + _ => { + components.push(component.into()); + } + } + } + ResolvedPath { components } + } + fn can_execute(&self, permissions: &PermissionInfo) -> bool { if self.user_info.user == permissions.owner.user { permissions.mode.contains(Mode::XUSR) @@ -82,6 +102,17 @@ impl Context { } } +struct ResolvedPath { + components: Vec, +} + +impl ResolvedPath { + fn parent_and_name(&self) -> Option<(Vec<&str>, &str)> { + let (name, parent) = self.components.split_last()?; + Some((parent.iter().map(String::as_str).collect(), name.as_str())) + } +} + /// A directory reached by a walk, plus the permission metadata to check against it. struct WalkedDir<'a> { handle: WalkingDirHandle<'a>, @@ -114,7 +145,6 @@ impl Resolver = parent_components.iter().map(String::as_str).collect(); let parent = self.walk_to_directory( context, self.backend.root(), @@ -142,7 +172,7 @@ impl Resolver) -> Result { self.backend - .owned_dir_at(dir, FileAccessMode::ReadOnly, FileOpenFlags::PATH) + .owned_dir_at(dir, OFlags::PATH) .map_err(|error| match error { OpenError::PathError(PathError::NoSuchFileOrDirectory) => { PathError::MissingComponent.into() @@ -155,7 +185,7 @@ impl Resolver Resolver WalkError::PathError(error), _ => WalkError::Io, }; - let components: Vec<_> = path.components().iter().map(String::as_str).collect(); + let components: Vec<_> = path.components.iter().map(String::as_str).collect(); if components.is_empty() { let root = self .backend - .owned_dir_at( - self.backend.root(), - FileAccessMode::ReadOnly, - FileOpenFlags::PATH, - ) + .owned_dir_at(self.backend.root(), OFlags::PATH) .map_err(map_open_error)?; // A backend root reports no permission metadata, so the backend is left to enforce // whatever it wants on it. @@ -201,7 +227,7 @@ impl Resolver Resolver { let file = self .backend - .open_file_at( - outcome.last, - components[walked], - FileAccessMode::ReadOnly, - FileOpenFlags::PATH, - ) + .open_file_at(outcome.last, components[walked], OFlags::PATH) .map_err(map_open_error)?; Ok(Permissioned { item: Handle::File(file.item), @@ -365,36 +386,44 @@ impl Resolver Resolver { /// Opens a file /// - /// The `mode` is only significant when creating a file. [`FileOpenFlags::PATH`] ignores - /// `access` and all flags except [`FileOpenFlags::DIRECTORY`] and [`FileOpenFlags::NO_FOLLOW`]. + /// The `mode` is only significant when creating a file pub fn open( &self, user: UserInfo, path: &str, - mut access: FileAccessMode, - mut flags: FileOpenFlags, + mut flags: OFlags, mode: Mode, ) -> Result, OpenError> { - if !flags.difference(FileOpenFlags::all()).is_empty() { + const CURRENTLY_SUPPORTED_OFLAGS: 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(CURRENTLY_SUPPORTED_OFLAGS.complement()) { unimplemented!("{flags:?}") } - let path_only = flags.contains(FileOpenFlags::PATH); + let path_only = flags.contains(OFlags::PATH); if path_only { // For `PATH`, we restrict what other flags are allowed, so a missing path cannot lead // to a creation, etc. - flags &= FileOpenFlags::PATH | FileOpenFlags::DIRECTORY | FileOpenFlags::NO_FOLLOW; - access = FileAccessMode::ReadOnly; + flags &= OFlags::PATH | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC; } let context = Context::new(user); - let path = ResolvedPath::root().resolve(path); - let (read_allowed, write_allowed) = match access { - FileAccessMode::ReadOnly => (true, false), - FileAccessMode::WriteOnly => (false, true), - FileAccessMode::ReadWrite => (true, true), - _ => unimplemented!("{access:?}"), - }; - let append_mode = flags.contains(FileOpenFlags::APPEND); + let path = Context::resolve(path); + let access_mode = flags & (OFlags::WRONLY | OFlags::RDWR); + let read_allowed = access_mode == OFlags::RDONLY || access_mode == OFlags::RDWR; + let write_allowed = access_mode == OFlags::WRONLY || access_mode == OFlags::RDWR; + let append_mode = flags.contains(OFlags::APPEND); let entry = |handle, seek_behavior| ResolverEntry { handle, _backend: core::marker::PhantomData, @@ -406,20 +435,17 @@ impl Resolver = path.components().iter().map(String::as_str).collect(); + let components: Vec<_> = path.components.iter().map(String::as_str).collect(); let walk = self.walk_path( &context, self.backend.root(), @@ -437,11 +463,11 @@ impl Resolver { - if flags.contains(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE) { + if flags.contains(OFlags::CREAT) && flags.contains(OFlags::EXCL) { return Err(OpenError::AlreadyExists); } Ok(entry( - Handle::Dir(self.backend.owned_dir_at(outcome.last, access, flags)?), + Handle::Dir(self.backend.owned_dir_at(outcome.last, flags)?), SeekBehavior::NonSeekable, )) } @@ -451,10 +477,8 @@ impl Resolver Resolver + if flags.contains(OFlags::CREAT) => { let Some((parent_components, name)) = path.parent_and_name() else { unreachable!("root path was handled above") }; - let parent_components: Vec<_> = - parent_components.iter().map(String::as_str).collect(); let parent = self .walk_to_directory( &context, @@ -777,7 +799,7 @@ impl Resolver Result<(), ChmodError> { let context = Context::new(user); - let path = ResolvedPath::root().resolve(path); + let path = Context::resolve(path); let handle = self .path_handle(&context, &path) .map_err(|error| match error { @@ -799,7 +821,7 @@ impl Resolver, ) -> Result<(), ChownError> { let context = Context::new(acting_user); - let path = ResolvedPath::root().resolve(path); + let path = Context::resolve(path); let handle = self .path_handle(&context, &path) .map_err(|error| match error { @@ -815,7 +837,7 @@ impl Resolver Result<(), UnlinkError> { let context = Context::new(user); - let path = ResolvedPath::root().resolve(path); + let path = Context::resolve(path); let Some((parent, name)) = self.parent_dir_and_name(&context, &path) .map_err(|error| match error { @@ -840,7 +862,7 @@ impl Resolver Result<(), MkdirError> { let context = Context::new(user); - let path = ResolvedPath::root().resolve(path); + let path = Context::resolve(path); let Some((parent, name)) = self.parent_dir_and_name(&context, &path) .map_err(|error| match error { @@ -874,7 +896,7 @@ impl Resolver Result<(), RmdirError> { let context = Context::new(user); - let path = ResolvedPath::root().resolve(path); + let path = Context::resolve(path); let Some((parent, name)) = self.parent_dir_and_name(&context, &path) .map_err(|error| match error { @@ -930,23 +952,17 @@ impl Resolver Result { - let entry = self - .open( - user, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::PATH, - Mode::empty(), - ) - .map_err(|error| match error { - OpenError::PathError(error) => error.into(), - OpenError::Io - | OpenError::AccessNotAllowed - | OpenError::NoWritePerms - | OpenError::ReadOnlyFileSystem - | OpenError::AlreadyExists - | OpenError::TruncateError(_) => FileStatusError::Io, - })?; + let entry = + self.open(user, path, OFlags::PATH, Mode::empty()) + .map_err(|error| match error { + OpenError::PathError(error) => error.into(), + OpenError::Io + | OpenError::AccessNotAllowed + | OpenError::NoWritePerms + | OpenError::ReadOnlyFileSystem + | OpenError::AlreadyExists + | OpenError::TruncateError(_) => FileStatusError::Io, + })?; self.handle_status(&entry) } diff --git a/litebox_broker_core/src/fs/service.rs b/litebox_broker_core/src/fs/service.rs index fcf944cbe6..908a4029f1 100644 --- a/litebox_broker_core/src/fs/service.rs +++ b/litebox_broker_core/src/fs/service.rs @@ -14,6 +14,7 @@ use litebox_broker_protocol::fs::{ 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, @@ -205,18 +206,11 @@ where flags: FileOpenFlags, mode: FileMode, ) -> ServiceResult { - if !matches!( - access, - FileAccessMode::ReadOnly | FileAccessMode::WriteOnly | FileAccessMode::ReadWrite - ) || FileOpenFlags::from_bits(flags.bits()).is_none() - { - return Err(BrokerError::UnsupportedOperation); - } - let entry = - match Resolver::open(self, user, path, access, flags, mode & FileMode::SUPPORTED) { - Ok(entry) => entry, - Err(error) => return Ok(Err(file_open_error(error))), - }; + let flags = open_flags(access, flags)?; + let entry = match Resolver::open(self, user, path, flags, mode & FileMode::SUPPORTED) { + Ok(entry) => entry, + Err(error) => return Ok(Err(file_open_error(error))), + }; Ok(Ok(File(Arc::new(RwLock::::new(entry))))) } @@ -623,9 +617,6 @@ fn authorize(session: &BrokerSession, required: ObjectRights) -> Result Result { - if FileOpenFlags::from_bits(flags.bits()).is_none() { - return Err(BrokerError::UnsupportedOperation); - } if flags.contains(FileOpenFlags::PATH) { return Ok(ObjectRights::WAIT); } @@ -712,6 +703,32 @@ impl DeviceIo for SessionDeviceIo<'_> { } } +fn open_flags(access: FileAccessMode, flags: FileOpenFlags) -> Result { + let mut output = match access { + FileAccessMode::ReadOnly => OFlags::RDONLY, + FileAccessMode::WriteOnly => OFlags::WRONLY, + FileAccessMode::ReadWrite => OFlags::RDWR, + _ => return Err(BrokerError::UnsupportedOperation), + }; + for (file_flag, engine_flag) in [ + (FileOpenFlags::CREATE, OFlags::CREAT), + (FileOpenFlags::TRUNCATE, OFlags::TRUNC), + (FileOpenFlags::NO_CONTROLLING_TERMINAL, OFlags::NOCTTY), + (FileOpenFlags::EXCLUSIVE, OFlags::EXCL), + (FileOpenFlags::DIRECTORY, OFlags::DIRECTORY), + (FileOpenFlags::NONBLOCKING, OFlags::NONBLOCK), + (FileOpenFlags::LARGE_FILE, OFlags::LARGEFILE), + (FileOpenFlags::NO_FOLLOW, OFlags::NOFOLLOW), + (FileOpenFlags::APPEND, OFlags::APPEND), + (FileOpenFlags::PATH, OFlags::PATH), + ] { + if flags.contains(file_flag) { + output |= engine_flag; + } + } + Ok(output) +} + fn mask_status_mode(status: &mut FileStatus) { status.mode &= FileMode::SUPPORTED; } diff --git a/litebox_broker_core/src/fs/tar_ro.rs b/litebox_broker_core/src/fs/tar_ro.rs index 6d64053ca2..a081c57e5b 100644 --- a/litebox_broker_core/src/fs/tar_ro.rs +++ b/litebox_broker_core/src/fs/tar_ro.rs @@ -29,11 +29,11 @@ use alloc::vec::Vec; use core::ops::Range; use hashbrown::HashMap; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileNodeInfo, FileOpenFlags, FileStatus, - FileType, FileUser as UserInfo, + FileDirectoryEntry, FileMode as Mode, FileStatus, FileType, FileUser as UserInfo, }; use super::{ + NodeInfo, OFlags, backend::{CreationMetadata, DirHandle, FileHandle, HandleRef, WalkingDirHandle}, errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, @@ -128,15 +128,9 @@ impl super::backend::Backend for TarRo { fn owned_dir_at( &self, dir: WalkingDirHandle<'_>, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result { - if flags.intersects(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE) - || matches!( - access, - FileAccessMode::WriteOnly | FileAccessMode::ReadWrite - ) - { + if flags.intersects(OFlags::CREAT | OFlags::TRUNC | OFlags::WRONLY | OFlags::RDWR) { return Err(OpenError::ReadOnlyFileSystem); } Ok(DirHandle::from_typed::(dir.into_typed::())) @@ -152,8 +146,7 @@ impl super::backend::Backend for TarRo { &self, dir: WalkingDirHandle<'_>, name: &str, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, ) -> Result, OpenError> { let dir = dir.into_typed::(); let child = self.tar_index.dirs[dir.idx] @@ -163,15 +156,14 @@ impl super::backend::Backend for TarRo { let IndexedChild::File(file_idx) = *child else { return Err(OpenError::PathError(PathError::ComponentNotADirectory)); }; - if flags.contains(FileOpenFlags::DIRECTORY) { + if flags.contains(OFlags::DIRECTORY) { return Err(OpenError::PathError(PathError::ComponentNotADirectory)); } - if !flags.contains(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE) - && (flags.intersects(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE) - || matches!( - access, - FileAccessMode::WriteOnly | FileAccessMode::ReadWrite - )) + if !(flags.contains(OFlags::CREAT) && flags.contains(OFlags::EXCL)) + && (flags.contains(OFlags::CREAT) + || flags.contains(OFlags::TRUNC) + || flags.contains(OFlags::WRONLY) + || flags.contains(OFlags::RDWR)) { return Err(OpenError::ReadOnlyFileSystem); } @@ -328,12 +320,12 @@ struct IndexedFile { data_range: Range, mode: Mode, owner: UserInfo, - node_info: FileNodeInfo, + node_info: NodeInfo, } struct IndexedDir { owner: Option, - node_info: FileNodeInfo, + node_info: NodeInfo, children: HashMap, } diff --git a/litebox_broker_core/src/fs/test_support.rs b/litebox_broker_core/src/fs/test_support.rs index eb616d5d2e..8de0171763 100644 --- a/litebox_broker_core/src/fs/test_support.rs +++ b/litebox_broker_core/src/fs/test_support.rs @@ -10,11 +10,12 @@ use alloc::vec::Vec; use litebox_broker_protocol::fs::{ - FileAccessMode, FileDirectoryEntry, FileMode as Mode, FileOpenFlags, - FileSeekWhence as SeekWhence, FileStatus, FileUser as UserInfo, + FileDirectoryEntry, FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, + FileUser as UserInfo, }; use litebox_broker_protocol::stdio::StdioOutputStream; +use super::OFlags; use super::backend::{Backend, DeviceIo, NoDeviceIo}; #[cfg(target_os = "linux")] use super::errors::TruncateError; @@ -57,11 +58,10 @@ impl Fs { &self, user: UserInfo, path: &str, - access: FileAccessMode, - flags: FileOpenFlags, + flags: OFlags, mode: Mode, ) -> Result, OpenError> { - self.resolver.open(user, path, access, flags, mode) + self.resolver.open(user, path, flags, mode) } pub(in crate::fs) fn read( diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs index 74f1b9c801..9dfcb1051b 100644 --- a/litebox_broker_core/src/fs/tests.rs +++ b/litebox_broker_core/src/fs/tests.rs @@ -12,6 +12,7 @@ use litebox_broker_protocol::fs::{ FileMode as Mode, FileSeekWhence as SeekWhence, FileType, FileUser as UserInfo, }; +use super::OFlags; use super::in_mem::InMem; use super::inode_allocator::InodeAllocator; use super::overlay::Overlay; @@ -42,10 +43,9 @@ fn overlay_fs( } mod in_mem { - use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; - use super::{ - FileType, Fs, InMem, Mode, ROOT, SeekWhence, TestPlatform, USER, UserInfo, in_mem_fs, + FileType, Fs, InMem, Mode, OFlags, ROOT, SeekWhence, TestPlatform, USER, UserInfo, + in_mem_fs, }; use crate::fs::errors::{ ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, @@ -77,27 +77,14 @@ mod in_mem { // Test file creation let path = "/testfile"; let fd = fs - .open( - ROOT, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .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, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU - ) - .is_err(), + fs.open(ROOT, path, OFlags::RDONLY, Mode::RWXU).is_err(), "File should not exist" ); } @@ -109,13 +96,7 @@ mod in_mem { // Create and write to a file let path = "/testfile"; let mut fd = fs - .open( - ROOT, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let data = b"Hello, world!"; fs.write(&mut fd, data, None) @@ -124,13 +105,7 @@ mod in_mem { // Read from the file let mut fd = fs - .open( - ROOT, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(ROOT, path, OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs @@ -147,13 +122,7 @@ mod in_mem { let path = "/tmp/write_only"; let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::WUSR, - ) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) .expect("Failed to create write-only file"); fs.write(&mut fd, b"x", None).expect("Failed to write file"); @@ -165,13 +134,7 @@ mod in_mem { drop(fd); assert!(matches!( - fs.open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty() - ), + fs.open(USER, path, OFlags::RDONLY, Mode::empty()), Err(OpenError::AccessNotAllowed) )); } @@ -183,13 +146,7 @@ mod in_mem { let path = "/tmp/zero_mode"; let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::empty(), - ) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) .expect("Failed to create zero-mode file"); fs.write(&mut fd, b"x", None).expect("Failed to write file"); drop(fd); @@ -197,13 +154,7 @@ mod in_mem { let status = fs.file_status(USER, path).expect("Failed to stat file"); assert_eq!(status.mode, Mode::empty()); assert!(matches!( - fs.open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::empty() - ), + fs.open(USER, path, OFlags::WRONLY, Mode::empty()), Err(OpenError::AccessNotAllowed) )); } @@ -220,14 +171,7 @@ mod in_mem { // Test directory removal fs.rmdir(ROOT, path).expect("Failed to remove directory"); assert!( - fs.open( - ROOT, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU - ) - .is_err(), + fs.open(ROOT, path, OFlags::RDONLY, Mode::RWXU).is_err(), "Directory should not exist" ); } @@ -240,27 +184,14 @@ mod in_mem { // Test file creation let path = "/tmp/testfile"; let fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .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, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU - ) - .is_err(), + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), "File should not exist" ); } @@ -273,13 +204,7 @@ mod in_mem { // Create and write to a file let path = "/tmp/testfile"; let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let data = b"Hello, world!"; fs.write(&mut fd, data, None) @@ -290,13 +215,7 @@ mod in_mem { // Read from the file let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, path, OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs @@ -323,14 +242,7 @@ mod in_mem { // Test directory removal fs.rmdir(USER, path).expect("Failed to remove directory"); assert!( - fs.open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU - ) - .is_err(), + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), "Directory should not exist" ); } @@ -340,13 +252,7 @@ mod in_mem { let fs = in_mem_fs(); let fd = fs - .open( - ROOT, - "/", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(ROOT, "/", OFlags::RDONLY, Mode::empty()) .expect("Failed to open root directory"); let entries = fs .read_dir(&fd) @@ -372,8 +278,7 @@ mod in_mem { .open( ROOT, "/testfile1", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create file1"); @@ -382,8 +287,7 @@ mod in_mem { .open( ROOT, "/testfile2", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create file2"); @@ -391,13 +295,7 @@ mod in_mem { // Read root directory let fd = fs - .open( - ROOT, - "/", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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); @@ -430,13 +328,7 @@ mod in_mem { // Read the subdirectory (should be empty) let fd = fs - .open( - ROOT, - "/testdir", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(ROOT, "/testdir", OFlags::RDONLY, Mode::empty()) .expect("Failed to open subdirectory"); let entries = fs .read_dir(&fd) @@ -456,8 +348,7 @@ mod in_mem { .open( ROOT, "/testfile", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create file"); @@ -465,13 +356,7 @@ mod in_mem { // Try to read_dir on the file (should fail) let fd = fs - .open( - ROOT, - "/testfile", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(ROOT, "/testfile", OFlags::RDONLY, Mode::empty()) .expect("Failed to open file"); assert!(matches!(fs.read_dir(&fd), Err(ReadDirError::NotADirectory))); } @@ -491,8 +376,7 @@ mod in_mem { .open( ROOT, "/rootdir/file", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create file"); @@ -508,8 +392,7 @@ mod in_mem { fs.open( USER, "/rootdir/new", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU ), Err(OpenError::NoWritePerms) @@ -532,8 +415,7 @@ mod in_mem { .open( USER, "/opendir/new", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create file"); @@ -553,13 +435,7 @@ mod in_mem { // Create a test file as root let path = "/testfile"; let fd = fs - .open( - ROOT, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); drop(fd); @@ -615,8 +491,7 @@ mod in_mem { .open( USER, "/testfile", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create file"); @@ -627,8 +502,7 @@ mod in_mem { .open( USER, "/testdir", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .expect("Failed to open directory with O_DIRECTORY"); @@ -639,8 +513,7 @@ mod in_mem { fs.open( USER, "/testfile", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -651,8 +524,7 @@ mod in_mem { fs.open( USER, "/nonexistent", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) @@ -664,8 +536,7 @@ mod in_mem { .open( USER, "/newfile", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::DIRECTORY, + OFlags::CREAT | OFlags::WRONLY | OFlags::DIRECTORY, Mode::RWXU, ) .expect("Failed to create file with O_CREAT | O_DIRECTORY"); @@ -692,8 +563,7 @@ mod in_mem { .open( USER, "/newfile", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create new file with O_CREAT | O_EXCL"); @@ -708,8 +578,7 @@ mod in_mem { fs.open( USER, "/newfile", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -720,8 +589,7 @@ mod in_mem { .open( USER, "/newfile", - FileAccessMode::ReadOnly, - FileOpenFlags::EXCLUSIVE, + OFlags::EXCL | OFlags::RDONLY, Mode::empty(), ) .expect("Failed to open existing file with O_EXCL (without O_CREAT)"); @@ -736,13 +604,7 @@ mod in_mem { // Test O_CREAT without O_EXCL on existing file (should succeed) let fd = fs - .open( - USER, - "/newfile", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .open(USER, "/newfile", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to open existing file with O_CREAT (without O_EXCL)"); drop(fd); @@ -753,8 +615,7 @@ mod in_mem { fs.open( USER, "/testdir", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -769,13 +630,7 @@ mod in_mem { // Create a file and write some initial content let path = "/testfile"; let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .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(&mut fd, initial_data, None) @@ -784,13 +639,7 @@ mod in_mem { // Verify initial content was written let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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 @@ -802,13 +651,7 @@ mod in_mem { // Test O_TRUNC with O_WRONLY - should truncate file let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::TRUNCATE, - Mode::empty(), - ) + .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 @@ -819,13 +662,7 @@ mod in_mem { // Verify the file was truncated and contains only new content let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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 @@ -837,26 +674,14 @@ mod in_mem { // Test O_TRUNC with O_RDWR - should also truncate let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, path, OFlags::WRONLY, Mode::empty()) .expect("Failed to open file for writing"); fs.write(&mut fd, b"More content to truncate", None) .expect("Failed to write more content"); drop(fd); let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadWrite, - FileOpenFlags::TRUNCATE, - Mode::empty(), - ) + .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 @@ -890,8 +715,7 @@ mod in_mem { .open( USER, "/posfile", - FileAccessMode::ReadWrite, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::RDWR, Mode::RWXU | Mode::RWXG | Mode::RWXO, ) .expect("open failed"); @@ -936,13 +760,7 @@ mod in_mem { /// 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, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); fs.write(&mut fd, data, None) .expect("Failed to write initial content"); @@ -969,13 +787,7 @@ mod in_mem { // Re-open with O_APPEND and write more data let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::APPEND, - Mode::empty(), - ) + .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_APPEND"); fs.write(&mut fd, b" World", None) .expect("Failed to append data"); @@ -983,13 +795,7 @@ mod in_mem { // Verify the file contains both pieces of data concatenated let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); assert_eq!(read_all(&fs, &mut fd), b"Hello World"); } @@ -1005,13 +811,7 @@ mod in_mem { // Re-open with O_APPEND let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::APPEND, - Mode::empty(), - ) + .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 @@ -1025,13 +825,7 @@ mod in_mem { // Verify the file content: original data followed by appended data let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); assert_eq!(read_all(&fs, &mut fd), b"ABCDEF123"); } @@ -1047,13 +841,7 @@ mod in_mem { // Re-open with O_RDWR | O_APPEND let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadWrite, - FileOpenFlags::APPEND, - Mode::empty(), - ) + .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 @@ -1084,13 +872,7 @@ mod in_mem { // Re-open with O_APPEND let mut fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::APPEND, - Mode::empty(), - ) + .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 @@ -1099,13 +881,7 @@ mod in_mem { // Verify the file content: XX should be at position 2, not appended let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, path, OFlags::RDONLY, Mode::empty()) .expect("Failed to open file for reading"); assert_eq!(read_all(&fs, &mut fd), b"ABXXEF"); } @@ -1124,8 +900,7 @@ mod in_mem { .open( USER, path, - FileAccessMode::WriteOnly, - FileOpenFlags::TRUNCATE | FileOpenFlags::APPEND, + OFlags::WRONLY | OFlags::TRUNC | OFlags::APPEND, Mode::empty(), ) .expect("Failed to open file with O_TRUNC | O_APPEND"); @@ -1139,22 +914,14 @@ mod in_mem { // Verify the file content let mut fd = fs - .open( - USER, - path, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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 litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; - - use super::{FileType, Mode, TEST_TAR_FILE, USER, tar_ro_fs}; + use super::{FileType, Mode, OFlags, TEST_TAR_FILE, USER, tar_ro_fs}; use crate::fs::errors::{OpenError, PathError, ReadDirError}; use alloc::vec; use alloc::vec::Vec; @@ -1163,13 +930,7 @@ mod tar_ro { fn file_read() { let fs = tar_ro_fs(TEST_TAR_FILE.into()); let mut fd = fs - .open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1179,13 +940,7 @@ mod tar_ro { drop(fd); let mut fd = fs - .open( - USER, - "bar/baz", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1198,23 +953,11 @@ mod tar_ro { fn dir_and_nonexist_checks() { let fs = tar_ro_fs(TEST_TAR_FILE.into()); assert!(matches!( - fs.open( - USER, - "bar/ba", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty() - ), + fs.open(USER, "bar/ba", OFlags::RDONLY, Mode::empty()), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); - fs.open( - USER, - "bar", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) - .expect("Failed to open dir"); + fs.open(USER, "bar", OFlags::RDONLY, Mode::empty()) + .expect("Failed to open dir"); } #[test] @@ -1225,8 +968,7 @@ mod tar_ro { fs.open( USER, "bar", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .expect("Failed to open directory with O_DIRECTORY"); @@ -1236,8 +978,7 @@ mod tar_ro { fs.open( USER, "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1248,8 +989,7 @@ mod tar_ro { fs.open( USER, "nonexistent", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) @@ -1260,8 +1000,7 @@ mod tar_ro { fs.open( USER, "bar/baz", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1272,13 +1011,9 @@ mod tar_ro { fn write_or_truncate_open_of_directory_fails() { let fs = tar_ro_fs(TEST_TAR_FILE.into()); - for (access, flags) in [ - (FileAccessMode::WriteOnly, FileOpenFlags::empty()), - (FileAccessMode::ReadWrite, FileOpenFlags::empty()), - (FileAccessMode::ReadOnly, FileOpenFlags::TRUNCATE), - ] { + for flags in [OFlags::WRONLY, OFlags::RDWR, OFlags::TRUNC] { assert!(matches!( - fs.open(USER, "bar", access, flags, Mode::empty()), + fs.open(USER, "bar", flags, Mode::empty()), Err(OpenError::ReadOnlyFileSystem) )); } @@ -1290,13 +1025,7 @@ mod tar_ro { // Read root directory let fd = fs - .open( - USER, - "/", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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); @@ -1327,13 +1056,7 @@ mod tar_ro { // Read `bar` directory let fd = fs - .open( - USER, - "bar", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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"); @@ -1348,23 +1071,16 @@ mod tar_ro { let fs = tar_ro_fs(TEST_TAR_FILE.into()); let fd = fs - .open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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 litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; - use super::{ - FileType, Fs, Mode, Overlay, SeekWhence, TEST_TAR_FILE, TestPlatform, USER, UserInfo, + FileType, Fs, Mode, OFlags, Overlay, SeekWhence, TEST_TAR_FILE, TestPlatform, USER, + UserInfo, }; use crate::fs::errors::{FileStatusError, OpenError, PathError, RmdirError}; use crate::fs::in_mem::{InMem, InitialNode}; @@ -1404,13 +1120,7 @@ mod overlay { fn file_read_from_lower() { let fs = overlay_fs(upper([])); let mut fd = fs - .open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1427,13 +1137,7 @@ mod overlay { assert_eq!(stat.mode, Mode::from_bits(0o777).unwrap()); let mut fd = fs - .open( - USER, - "bar/baz", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "bar/baz", OFlags::RDONLY, Mode::empty()) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -1449,23 +1153,11 @@ mod overlay { fn dir_and_nonexist_checks() { let fs = overlay_fs(upper([])); assert!(matches!( - fs.open( - USER, - "bar/ba", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty() - ), + fs.open(USER, "bar/ba", OFlags::RDONLY, Mode::empty()), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); - fs.open( - USER, - "bar", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) - .expect("Failed to open dir"); + 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 @@ -1474,22 +1166,10 @@ mod overlay { fn file_read_write_copy_up() { let fs = overlay_fs(upper([])); let mut fd1 = fs - .open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut fd2 = fs - .open( - USER, - "foo", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 1024]; @@ -1516,22 +1196,10 @@ mod overlay { fn file_read_write_copy_up_keeps_position() { let fs = overlay_fs(upper([])); let mut fd1 = fs - .open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut fd2 = fs - .open( - USER, - "foo", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1554,13 +1222,7 @@ mod overlay { fn file_deletion() { let fs = overlay_fs(upper([])); let mut fd = fs - .open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, "foo", OFlags::RDONLY, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1583,13 +1245,7 @@ mod overlay { // But if we close and attempt to re-open, it should not exist drop(fd); assert!(matches!( - fs.open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty() - ), + fs.open(USER, "foo", OFlags::RDONLY, Mode::empty()), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)), )); } @@ -1618,8 +1274,7 @@ mod overlay { fs.open( USER, "bar", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .expect("Failed to open lower layer directory with O_DIRECTORY"); @@ -1628,8 +1283,7 @@ mod overlay { fs.open( USER, "/upperdir", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty(), ) .expect("Failed to open upper layer directory with O_DIRECTORY"); @@ -1639,8 +1293,7 @@ mod overlay { fs.open( USER, "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1651,8 +1304,7 @@ mod overlay { fs.open( USER, "/upperfile", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1663,8 +1315,7 @@ mod overlay { fs.open( USER, "bar/baz", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::ComponentNotADirectory)) @@ -1675,8 +1326,7 @@ mod overlay { fs.open( USER, "nonexistent", - FileAccessMode::ReadOnly, - FileOpenFlags::DIRECTORY, + OFlags::RDONLY | OFlags::DIRECTORY, Mode::empty() ), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) @@ -1689,13 +1339,7 @@ mod overlay { fn file_create_exist_in_lower() { let fs = overlay_fs(upper([])); let mut fd = fs - .open( - USER, - "foo", - FileAccessMode::ReadWrite, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .open(USER, "foo", OFlags::RDWR | OFlags::CREAT, Mode::RWXU) .expect("Failed to open file"); let mut buffer = vec![0; 4]; @@ -1712,13 +1356,7 @@ mod overlay { // Read bar subdirectory let fd = fs - .open( - USER, - "bar", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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"); @@ -1754,13 +1392,7 @@ mod overlay { // Read root directory (should contain entries from both layers) let fd = fs - .open( - USER, - "/", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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); @@ -1796,13 +1428,7 @@ mod overlay { // Read upperdir directory (should be from upper layer) let fd = fs - .open( - USER, - "/upperdir", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "/upperdir", OFlags::RDONLY, Mode::empty()) .expect("Failed to open upperdir"); let entries = fs.read_dir(&fd).expect("Failed to read upperdir"); @@ -1820,8 +1446,7 @@ mod overlay { fs.open( USER, "foo", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -1832,8 +1457,7 @@ mod overlay { .open( USER, "/newfile", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create new file with O_CREAT | O_EXCL"); @@ -1847,8 +1471,7 @@ mod overlay { fs.open( USER, "/newfile", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -1860,8 +1483,7 @@ mod overlay { fs.open( USER, "bar", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -1877,8 +1499,7 @@ mod overlay { .open( USER, "foo", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create file over tombstone with O_CREAT | O_EXCL"); @@ -1889,13 +1510,7 @@ mod overlay { // Verify the new content let mut fd = fs - .open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "foo", OFlags::RDONLY, Mode::empty()) .expect("Failed to open recreated file"); let mut buffer = vec![0; 15]; let bytes_read = fs @@ -1910,8 +1525,7 @@ mod overlay { .open( USER, "/upper_only_file", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU, ) .expect("Failed to create upper layer file"); @@ -1924,8 +1538,7 @@ mod overlay { fs.open( USER, "/upper_only_file", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, Mode::RWXU, ), Err(OpenError::AlreadyExists) @@ -1948,13 +1561,7 @@ mod overlay { // Verify we can open the directory let fd = fs - .open( - USER, - "/bar/test", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "/bar/test", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /bar/test directory"); let entries = fs .read_dir(&fd) @@ -1974,13 +1581,7 @@ mod overlay { // 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", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .open(USER, "bar/test", OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to open bar/test for writing"); // Write data to the file @@ -1991,13 +1592,7 @@ mod overlay { // Read the file back let mut fd = fs - .open( - USER, - "bar/test", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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 @@ -2020,13 +1615,7 @@ mod overlay { // 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", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .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) @@ -2037,13 +1626,7 @@ mod overlay { // Read the file back to verify it was modified let mut fd = fs - .open( - USER, - "bar/baz", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .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 @@ -2066,13 +1649,7 @@ mod overlay { // Open with O_TRUNC should copy the file up into the upper backend, empty let mut fd = fs - .open( - USER, - "foo", - FileAccessMode::ReadWrite, - FileOpenFlags::TRUNCATE, - Mode::empty(), - ) + .open(USER, "foo", OFlags::RDWR | OFlags::TRUNC, Mode::empty()) .expect("Failed to open file with O_TRUNC"); // File should be truncated (empty) @@ -2089,13 +1666,7 @@ mod overlay { // Verify the content persists let mut fd = fs - .open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "foo", OFlags::RDONLY, Mode::empty()) .expect("Failed to reopen file"); let mut buffer = vec![0; 1024]; let bytes_read = fs @@ -2141,8 +1712,7 @@ mod overlay { .open( USER, "/upper_dir/file", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU | Mode::RWXG, ) .expect("create file in upper_dir failed"); @@ -2186,8 +1756,7 @@ mod overlay { .open( USER, "/regular_file", - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, + OFlags::CREAT | OFlags::WRONLY, Mode::RWXU | Mode::RWXG, ) .expect("create file failed"); @@ -2214,13 +1783,7 @@ mod overlay { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let mut fd = fs - .open( - USER, - "foo", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::RWXU, - ) + .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file for writing"); fs.write(&mut fd, b"x", None) .expect("Failed to write to file"); @@ -2234,9 +1797,7 @@ mod overlay { } mod devices { - use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; - - use super::{Fs, Mode, USER, UnservicedStdio}; + use super::{Fs, Mode, OFlags, USER, UnservicedStdio}; use crate::fs::composer::Composer; use crate::fs::devices::Devices; use crate::fs::errors::{OpenError, PathError, ReadError, WriteError}; @@ -2259,13 +1820,7 @@ mod devices { let stdio = UnservicedStdio; let mut fd_stdout = fs - .open( - USER, - "/dev/stdout", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stdout"); assert!(matches!( fs.write_with(&stdio, &mut fd_stdout, b"", None), @@ -2278,13 +1833,7 @@ mod devices { drop(fd_stdout); let mut fd_stderr = fs - .open( - USER, - "/dev/stderr", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stderr"); assert!(matches!( fs.write_with(&stdio, &mut fd_stderr, b"", None), @@ -2297,13 +1846,7 @@ mod devices { drop(fd_stderr); let mut fd_stdin = fs - .open( - USER, - "/dev/stdin", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /dev/stdin"); assert!(matches!( fs.read_with(&stdio, &mut fd_stdin, &mut [], None), @@ -2322,22 +1865,14 @@ mod devices { // Attempt to open a non-/dev/* path assert!(matches!( - fs.open( - USER, - "foo", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty() - ), + fs.open(USER, "foo", OFlags::RDONLY, Mode::empty()), Err(OpenError::PathError(PathError::NoSuchFileOrDirectory)) )); } } mod composed { - use litebox_broker_protocol::fs::{FileAccessMode, FileOpenFlags}; - - use super::{Fs, InMem, Mode, TestPlatform, USER, UnservicedStdio, UserInfo}; + use super::{Fs, InMem, Mode, OFlags, TestPlatform, USER, UnservicedStdio, UserInfo}; use crate::fs::composer::Composer; use crate::fs::devices::Devices; use crate::fs::errors::{ReadError, WriteError}; @@ -2368,13 +1903,7 @@ mod composed { let stdio = UnservicedStdio; let mut fd_stdout = fs - .open( - USER, - "/dev/stdout", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "/dev/stdout", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stdout"); assert!(matches!( fs.write_with(&stdio, &mut fd_stdout, b"", None), @@ -2387,13 +1916,7 @@ mod composed { drop(fd_stdout); let mut fd_stderr = fs - .open( - USER, - "/dev/stderr", - FileAccessMode::WriteOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "/dev/stderr", OFlags::WRONLY, Mode::empty()) .expect("Failed to open /dev/stderr"); assert!(matches!( fs.write_with(&stdio, &mut fd_stderr, b"", None), @@ -2406,13 +1929,7 @@ mod composed { drop(fd_stderr); let mut fd_stdin = fs - .open( - USER, - "/dev/stdin", - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::empty(), - ) + .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /dev/stdin"); assert!(matches!( fs.read_with(&stdio, &mut fd_stdin, &mut [], None), @@ -2432,27 +1949,14 @@ mod composed { // Test file creation let path = "/testfile"; let fd = fs - .open( - USER, - path, - FileAccessMode::WriteOnly, - FileOpenFlags::CREATE, - Mode::RWXU, - ) + .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, - FileAccessMode::ReadOnly, - FileOpenFlags::empty(), - Mode::RWXU - ) - .is_err(), + fs.open(USER, path, OFlags::RDONLY, Mode::RWXU).is_err(), "File should not exist" ); } diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index 2d4c77bae1..db01f6b0b0 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -13,9 +13,6 @@ use thiserror::Error; use crate::ObjectHandle; use crate::shared_buffer::{SHARED_BUFFER_SLOT_SIZE, SharedBufferDescriptor}; -mod path; -pub use path::ResolvedPath; - /// Maximum bytes transferred through one fs shared-buffer request. /// /// This remains independent of slot capacity so increasing the shared-buffer @@ -200,46 +197,85 @@ bitflags! { } } -bitflags! { - /// ABI-neutral fs open flags. - /// - /// These values are intentionally independent of target-specific `O_*` bit assignments. - #[repr(transparent)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] - pub struct FileOpenFlags: u16 { - /// No open flags. - const NONE = 0; - /// Create the object when it does not exist. - const CREATE = 1 << 0; - /// Truncate an existing regular file. - const TRUNCATE = 1 << 1; - /// Do not assign a controlling terminal. - const NO_CONTROLLING_TERMINAL = 1 << 2; - /// Require creation to fail when the object already exists. - const EXCLUSIVE = 1 << 3; - /// Require the opened object to be a directory. - const DIRECTORY = 1 << 4; - /// Request nonblocking operation. - const NONBLOCKING = 1 << 5; - /// Allow large-file operation. - const LARGE_FILE = 1 << 6; - /// Do not follow the final symbolic link. - const NO_FOLLOW = 1 << 7; - /// Append writes to the end of the file. - const APPEND = 1 << 8; - /// Open only for path-based operations. - const PATH = 1 << 9; - /// Every open flag this protocol version defines. - const SUPPORTED = Self::CREATE.bits() - | Self::TRUNCATE.bits() - | Self::NO_CONTROLLING_TERMINAL.bits() - | Self::EXCLUSIVE.bits() - | Self::DIRECTORY.bits() - | Self::NONBLOCKING.bits() - | Self::LARGE_FILE.bits() - | Self::NO_FOLLOW.bits() - | Self::APPEND.bits() - | Self::PATH.bits(); +/// ABI-neutral fs open flags. +/// +/// These values are intentionally independent of target-specific `O_*` bit +/// assignments. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct FileOpenFlags(u16); + +impl FileOpenFlags { + /// No open flags. + pub const NONE: Self = Self(0); + /// Create the object when it does not exist. + pub const CREATE: Self = Self(1 << 0); + /// Truncate an existing regular file. + pub const TRUNCATE: Self = Self(1 << 1); + /// Do not assign a controlling terminal. + pub const NO_CONTROLLING_TERMINAL: Self = Self(1 << 2); + /// Require creation to fail when the object already exists. + pub const EXCLUSIVE: Self = Self(1 << 3); + /// Require the opened object to be a directory. + pub const DIRECTORY: Self = Self(1 << 4); + /// Request nonblocking operation. + pub const NONBLOCKING: Self = Self(1 << 5); + /// Allow large-file operation. + pub const LARGE_FILE: Self = Self(1 << 6); + /// Do not follow the final symbolic link. + pub const NO_FOLLOW: Self = Self(1 << 7); + /// Append writes to the end of the file. + 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 + | Self::TRUNCATE.0 + | Self::NO_CONTROLLING_TERMINAL.0 + | Self::EXCLUSIVE.0 + | Self::DIRECTORY.0 + | Self::NONBLOCKING.0 + | Self::LARGE_FILE.0 + | Self::NO_FOLLOW.0 + | Self::APPEND.0 + | Self::PATH.0, + ); + + /// Creates flags 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 + } + } + + /// Returns the stable protocol bits. + #[must_use] + pub const fn bits(self) -> u16 { + self.0 + } + + /// Returns whether all flags in `other` are present. + #[must_use] + pub const fn contains(self, other: Self) -> bool { + self.0 & other.0 == other.0 + } + + /// Returns the union of two flag sets. + #[must_use] + pub const fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } +} + +impl core::ops::BitOr for FileOpenFlags { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) } } diff --git a/litebox_broker_protocol/src/fs/path.rs b/litebox_broker_protocol/src/fs/path.rs deleted file mode 100644 index e230505049..0000000000 --- a/litebox_broker_protocol/src/fs/path.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use alloc::string::String; -use alloc::vec::Vec; - -/// An absolute, lexically normalized fs path. -/// -/// Normalization removes redundant separators and `.` components, and processes `..` without -/// going above the root. It does not perform filesystem lookup, resolve symbolic links, or -/// establish that the path exists or is accessible. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct ResolvedPath { - components: Vec, -} - -impl ResolvedPath { - /// The root path. - #[must_use] - pub const fn root() -> Self { - Self { - components: Vec::new(), - } - } - - /// Normalize `path`, using this path as the base when `path` is relative. - #[must_use] - pub fn resolve(&self, path: &str) -> Self { - let mut components = if path.starts_with('/') { - Vec::new() - } else { - self.components.clone() - }; - for component in path.split('/') { - match component { - "" | "." => {} - ".." => { - let _ = components.pop(); - } - _ => components.push(component.into()), - } - } - Self { components } - } - - /// Normalized components, excluding the root separator. - #[must_use] - pub fn components(&self) -> &[String] { - &self.components - } - - /// The parent components and final name, or `None` for the root. - #[must_use] - pub fn parent_and_name(&self) -> Option<(&[String], &str)> { - let (name, parent) = self.components.split_last()?; - Some((parent, name.as_str())) - } -} - -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(()) - } -} diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 2720aea28d..033a733942 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -693,23 +693,23 @@ impl FileAccess { match create_disposition { CreateDisposition::Overwrite => { - flags.insert(FileOpenFlags::TRUNCATE); + flags = flags.union(FileOpenFlags::TRUNCATE); } CreateDisposition::Supersede | CreateDisposition::OverwriteIf => { - flags.insert(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE); + flags = flags.union(FileOpenFlags::CREATE | FileOpenFlags::TRUNCATE); } CreateDisposition::Create => { - flags.insert(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE); + flags = flags.union(FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE); } - CreateDisposition::OpenIf => flags.insert(FileOpenFlags::CREATE), + CreateDisposition::OpenIf => flags = flags.union(FileOpenFlags::CREATE), CreateDisposition::Open => {} } if create_options.contains(FileCreateOptions::DIRECTORY_FILE) { - flags.insert(FileOpenFlags::DIRECTORY); + flags = flags.union(FileOpenFlags::DIRECTORY); } if create_options.contains(FileCreateOptions::NON_DIRECTORY_FILE) { - flags.insert(FileOpenFlags::NO_FOLLOW); + flags = flags.union(FileOpenFlags::NO_FOLLOW); } (access, flags) From 8752589c7c23d69317d4d92f95839ce0b81641eb Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 09:09:46 -0700 Subject: [PATCH 21/34] Restore broker filesystem type names Re-export canonical protocol filesystem types under the existing broker-core names to minimize migration-only churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/fs/backend.rs | 7 ++---- litebox_broker_core/src/fs/composer.rs | 18 +++++----------- litebox_broker_core/src/fs/devices.rs | 9 +++----- litebox_broker_core/src/fs/errors.rs | 5 +++-- litebox_broker_core/src/fs/in_mem.rs | 9 +++----- litebox_broker_core/src/fs/mod.rs | 5 ++++- litebox_broker_core/src/fs/nine_p/mod.rs | 25 +++++++++++----------- litebox_broker_core/src/fs/nine_p/tests.rs | 4 +--- litebox_broker_core/src/fs/overlay.rs | 11 ++++------ litebox_broker_core/src/fs/resolver.rs | 24 +++++++-------------- litebox_broker_core/src/fs/tar_ro.rs | 18 ++++++---------- litebox_broker_core/src/fs/test_support.rs | 8 ++----- litebox_broker_core/src/fs/tests.rs | 5 +---- 13 files changed, 55 insertions(+), 93 deletions(-) diff --git a/litebox_broker_core/src/fs/backend.rs b/litebox_broker_core/src/fs/backend.rs index d12f5832bf..be7dcc3202 100644 --- a/litebox_broker_core/src/fs/backend.rs +++ b/litebox_broker_core/src/fs/backend.rs @@ -8,16 +8,13 @@ use alloc::vec::Vec; use core::any::{Any, TypeId}; use core::marker::PhantomData; -use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileStatus, FileUser as UserInfo, -}; use litebox_broker_protocol::stdio::StdioOutputStream; -use super::OFlags; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; +use super::{DirEntry, FileStatus, Mode, OFlags, UserInfo}; // This duplicates the cloneable type-erasure support from `litebox::utilities::anymap` because // broker core cannot depend on LiteBox. Keep it local unless broader reuse justifies a common home. @@ -141,7 +138,7 @@ pub trait Backend: private::Sealed + Send + Sync + Any { ) -> Result, OpenError>; /// Read directory entries at `dir`. - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError>; + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError>; /// Read at `offset` into `buf`, returning the number of bytes read. /// diff --git a/litebox_broker_core/src/fs/composer.rs b/litebox_broker_core/src/fs/composer.rs index 927fbcf393..3b003a69f3 100644 --- a/litebox_broker_core/src/fs/composer.rs +++ b/litebox_broker_core/src/fs/composer.rs @@ -9,10 +9,6 @@ use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; -use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileStatus, FileType, FileUser as UserInfo, -}; - use super::backend::{ Backend, BackendHandles, CreationMetadata, DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, @@ -22,7 +18,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::{InodeAllocator, InodeAllocators}; -use super::{NodeInfo, OFlags}; +use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; use thiserror::Error; // XXX(jayb): consider removing this via a runtime reserved device ID? @@ -229,10 +225,10 @@ impl Composer { children } - fn list_mount_children(&self, path: &[String]) -> Vec { + fn list_mount_children(&self, path: &[String]) -> Vec { self.immediate_mount_children(path) .into_iter() - .map(|name| FileDirectoryEntry { + .map(|name| DirEntry { name, file_type: FileType::Directory, // TODO(jayb): set up proper inode info for these @@ -241,11 +237,7 @@ impl Composer { .collect() } - fn merge_mount_children( - &self, - mut entries: Vec, - path: &[String], - ) -> Vec { + fn merge_mount_children(&self, mut entries: Vec, path: &[String]) -> Vec { for child in self.list_mount_children(path) { entries.retain(|entry| entry.name != child.name); entries.push(child); @@ -629,7 +621,7 @@ impl Backend for Composer { } } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let handle = handle.into_typed::(); match handle.inner { ComposerDirHandleInner::Virtual { path } => Ok(self.list_mount_children(&path)), diff --git a/litebox_broker_core/src/fs/devices.rs b/litebox_broker_core/src/fs/devices.rs index 3aee0670de..9bfe85002c 100644 --- a/litebox_broker_core/src/fs/devices.rs +++ b/litebox_broker_core/src/fs/devices.rs @@ -8,9 +8,6 @@ use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileStatus, FileType, FileUser as UserInfo, -}; use litebox_broker_protocol::random::MAX_RANDOM_TRANSFER_SIZE; use litebox_broker_protocol::stdio::StdioOutputStream; @@ -23,7 +20,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{NodeInfo, OFlags}; +use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; /// Block size for stdio devices const STDIO_BLOCK_SIZE: u64 = 1024; @@ -234,11 +231,11 @@ impl Backend for Devices { }) } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let _handle = handle.into_typed::(); Ok(Device::ALL .iter() - .map(|(n, d)| FileDirectoryEntry { + .map(|(n, d)| DirEntry { name: String::from(*n), file_type: FileType::CharacterDevice, node_info: Some(d.file_status().node_info), diff --git a/litebox_broker_core/src/fs/errors.rs b/litebox_broker_core/src/fs/errors.rs index 3f95740235..903b8aef71 100644 --- a/litebox_broker_core/src/fs/errors.rs +++ b/litebox_broker_core/src/fs/errors.rs @@ -3,9 +3,10 @@ //! Possible errors from the broker-core filesystem engine. -use litebox_broker_protocol::fs::FileMode; use thiserror::Error; +use super::Mode; + // 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. @@ -208,7 +209,7 @@ pub enum PathError { #[cfg(debug_assertions)] dir: alloc::string::String, #[cfg(debug_assertions)] - perms: FileMode, + perms: Mode, }, #[error("invalid characters, not permitted by underlying file system")] InvalidPathname, diff --git a/litebox_broker_core/src/fs/in_mem.rs b/litebox_broker_core/src/fs/in_mem.rs index 434a05aa0c..27075a5c3b 100644 --- a/litebox_broker_core/src/fs/in_mem.rs +++ b/litebox_broker_core/src/fs/in_mem.rs @@ -8,17 +8,14 @@ use alloc::sync::Arc; use alloc::vec::Vec; use hashbrown::HashMap; -use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileStatus, FileType, FileUser as UserInfo, -}; use litebox_platform::sync; -use super::NodeInfo; use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, TruncateError, UnlinkError, WriteError, }; use super::inode_allocator::InodeAllocator; +use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, UserInfo}; /// A [`super::backend::Backend`] that stores all files in memory. /// @@ -382,7 +379,7 @@ impl super::backend::Backend for InMe fn list_dir_at( &self, handle: super::backend::DirHandle, - ) -> Result, ReadDirError> { + ) -> Result, ReadDirError> { Ok(handle .into_typed::() .dir @@ -394,7 +391,7 @@ impl super::backend::Backend for InMe Node::File(file) => (FileType::RegularFile, file.read().node_info), Node::Dir(dir) => (FileType::Directory, dir.read().node_info), }; - FileDirectoryEntry { + DirEntry { name: name.clone(), file_type, node_info: Some(node_info), diff --git a/litebox_broker_core/src/fs/mod.rs b/litebox_broker_core/src/fs/mod.rs index 4d977088e3..714a4f92ab 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -29,7 +29,10 @@ mod test_support; #[cfg(test)] mod tests; -pub use litebox_broker_protocol::fs::FileNodeInfo as NodeInfo; +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, diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index 71bbd24276..12c85d9500 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -14,7 +14,6 @@ use alloc::vec::Vec; use core::num::NonZeroU64; use core::sync::atomic::{AtomicBool, Ordering}; -use litebox_broker_protocol::fs::{FileDirectoryEntry, FileMode, FileStatus, FileType, FileUser}; use litebox_platform::sync; use thiserror::Error; @@ -27,7 +26,7 @@ use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; -use super::{NodeInfo, OFlags}; +use super::{FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; mod client; mod fcall; mod id_pool; @@ -430,7 +429,7 @@ where }) } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let handle = handle.into_typed::(); let entries = self.client.readdir_all(&handle.fid.fid)?; Ok(entries @@ -442,7 +441,7 @@ where // have the resolver handle only cases where it is not handled by the backend? !matches!(&*entry.name, b"." | b"..") }) - .map(|entry| FileDirectoryEntry { + .map(|entry| super::DirEntry { name: String::from_utf8_lossy(&entry.name).into_owned(), file_type: qid_type_to_file_type(entry.qid.typ), node_info: Some(NodeInfo { @@ -570,7 +569,7 @@ where Ok(self.remove_at(&dir.into_typed::(), name, false)?) } - fn chmod(&self, h: HandleRef<'_>, mode: FileMode) -> Result<(), ChmodError> { + fn chmod(&self, h: HandleRef<'_>, mode: Mode) -> Result<(), ChmodError> { let fid = match h { HandleRef::File(h) => &h.get_typed::().fid, HandleRef::Dir(h) => &h.get_typed::().fid, @@ -709,10 +708,10 @@ fn qid_type_to_file_type(qid_type: fcall::QidType) -> FileType { } } -fn file_mode(mode: u32) -> FileMode { - let bits = u16::try_from(mode & u32::from(FileMode::SUPPORTED.bits())) +fn file_mode(mode: u32) -> Mode { + let bits = u16::try_from(mode & u32::from(Mode::SUPPORTED.bits())) .expect("supported file mode bits fit in u16"); - FileMode::from_bits_retain(bits) + Mode::from_bits_retain(bits) } /// Convert getattr response to FileStatus @@ -727,7 +726,7 @@ fn rgetattr_to_file_status(attr: &fcall::Rgetattr, device_id: u64) -> Result Result for FileStatusError { #[cfg(debug_assertions)] dir: String::new(), #[cfg(debug_assertions)] - perms: FileMode::empty(), + perms: Mode::empty(), }), _ => FileStatusError::Io, }, @@ -1009,7 +1008,7 @@ impl From for WalkError { #[cfg(debug_assertions)] dir: String::new(), #[cfg(debug_assertions)] - perms: FileMode::empty(), + perms: Mode::empty(), }), _ => WalkError::Io, }, diff --git a/litebox_broker_core/src/fs/nine_p/tests.rs b/litebox_broker_core/src/fs/nine_p/tests.rs index a5fb9f318f..12b6ea3cc2 100644 --- a/litebox_broker_core/src/fs/nine_p/tests.rs +++ b/litebox_broker_core/src/fs/nine_p/tests.rs @@ -12,15 +12,13 @@ use std::io::{Read as _, Write as _}; use std::net::{TcpListener, TcpStream}; use std::path::Path; -use litebox_broker_protocol::fs::{FileMode as Mode, FileSeekWhence as SeekWhence, FileType}; - -use crate::fs::OFlags; use crate::fs::errors::{ FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WriteError, }; use crate::fs::inode_allocator::InodeAllocator; use crate::fs::test_support::{Fs, USER}; +use crate::fs::{FileType, Mode, OFlags, SeekWhence}; use crate::test_platform::TestPlatform; use super::{NineP, transport}; diff --git a/litebox_broker_core/src/fs/overlay.rs b/litebox_broker_core/src/fs/overlay.rs index f06358b9ea..f50ce66961 100644 --- a/litebox_broker_core/src/fs/overlay.rs +++ b/litebox_broker_core/src/fs/overlay.rs @@ -20,9 +20,6 @@ use alloc::vec::Vec; use hashbrown::{HashMap, HashSet}; -use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileStatus, FileType, FileUser as UserInfo, -}; use litebox_platform::sync::{Mutex, MutexGuard, RawSyncPrimitivesProvider}; use super::backend::{ @@ -35,7 +32,7 @@ use super::errors::{ ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::inode_allocator::InodeAllocator; -use super::{NodeInfo, OFlags}; +use super::{DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; /// The reserved namespace prefix; no overlay-visible name may start with it. const MARKER_PREFIX: &str = ".litebox-overlay-"; @@ -120,7 +117,7 @@ struct ResolvedDir { /// An overlay-visible directory entry, plus which layers contribute to it. struct ResolvedEntry { /// The entry as reported by the layer that owns it. - entry: FileDirectoryEntry, + entry: DirEntry, upper: bool, /// The highest-precedence lower backend with an entry of this name, if any. lower: Option, @@ -890,10 +887,10 @@ impl Backend for Overlay { }) } - fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let path = handle.into_typed::().path; let resolved = self.resolve_dir(&path).map_err(|_| ReadDirError::Io)?; - let mut entries: Vec = resolved + let mut entries: Vec = resolved .entries .into_values() .map(|entry| entry.entry) diff --git a/litebox_broker_core/src/fs/resolver.rs b/litebox_broker_core/src/fs/resolver.rs index 91f18b472e..50ede11795 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -3,26 +3,21 @@ //! Path management, permission checks, and open-state operations above [`super::backend`]. -use alloc::string::String; -use alloc::vec; -use alloc::vec::Vec; -use core::marker::PhantomData; -use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, FileType, - FileUser as UserInfo, -}; - use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::{ - OFlags, + DirEntry, FileStatus, FileType, Mode, OFlags, SeekWhence, UserInfo, backend::{ CreationMetadata, DeviceIo, DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, }, }; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::marker::PhantomData; /// The broker-core filesystem resolver, generic over its synchronization platform and /// [`Backend`](super::backend::Backend). @@ -921,10 +916,7 @@ impl Resolver, - ) -> Result, ReadDirError> { + pub fn read_dir(&self, entry: &ResolverEntry) -> Result, ReadDirError> { if entry.path_only { // TODO(jayb): Add an error variant for operations not permitted on O_PATH fds. unimplemented!("read_dir on O_PATH fd") @@ -936,12 +928,12 @@ impl Resolver Result, ReadDirError> { + fn list_dir_at(&self, handle: DirHandle) -> Result, ReadDirError> { let handle = handle.into_typed::(); Ok(self.tar_index.dirs[handle.idx] .children @@ -193,7 +189,7 @@ impl super::backend::Backend for TarRo { (FileType::Directory, self.tar_index.dirs[idx].node_info) } }; - FileDirectoryEntry { + DirEntry { name: name.clone(), file_type, node_info: Some(node_info), diff --git a/litebox_broker_core/src/fs/test_support.rs b/litebox_broker_core/src/fs/test_support.rs index 8de0171763..6281f2e8f0 100644 --- a/litebox_broker_core/src/fs/test_support.rs +++ b/litebox_broker_core/src/fs/test_support.rs @@ -9,13 +9,8 @@ use alloc::vec::Vec; -use litebox_broker_protocol::fs::{ - FileDirectoryEntry, FileMode as Mode, FileSeekWhence as SeekWhence, FileStatus, - FileUser as UserInfo, -}; use litebox_broker_protocol::stdio::StdioOutputStream; -use super::OFlags; use super::backend::{Backend, DeviceIo, NoDeviceIo}; #[cfg(target_os = "linux")] use super::errors::TruncateError; @@ -24,6 +19,7 @@ use super::errors::{ RmdirError, SeekError, UnlinkError, WriteError, }; use super::resolver::{Resolver, ResolverEntry}; +use super::{DirEntry, FileStatus, Mode, OFlags, SeekWhence, UserInfo}; use crate::test_platform::TestPlatform; /// The unprivileged user these tests act as unless they need root. @@ -162,7 +158,7 @@ impl Fs { pub(in crate::fs) fn read_dir( &self, entry: &Entry, - ) -> Result, ReadDirError> { + ) -> Result, ReadDirError> { self.resolver.read_dir(entry) } diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs index 9dfcb1051b..b5253fad86 100644 --- a/litebox_broker_core/src/fs/tests.rs +++ b/litebox_broker_core/src/fs/tests.rs @@ -8,16 +8,13 @@ //! descriptor table involved: the semantics under test are owned by broker core. use alloc::borrow::Cow; -use litebox_broker_protocol::fs::{ - FileMode as Mode, FileSeekWhence as SeekWhence, FileType, FileUser as UserInfo, -}; -use super::OFlags; use super::in_mem::InMem; use super::inode_allocator::InodeAllocator; use super::overlay::Overlay; use super::tar_ro::TarRo; use super::test_support::{Fs, ROOT, USER, UnservicedStdio}; +use super::{FileType, Mode, OFlags, SeekWhence, UserInfo}; use crate::test_platform::TestPlatform; const TEST_TAR_FILE: &[u8] = include_bytes!("./test.tar"); From 1897c4cda644b911abc80ec400f7caedaaff77f4 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 13:06:19 -0700 Subject: [PATCH 22/34] Restore filesystem metadata field names Keep canonical filesystem metadata aligned with the existing broker-core field names to avoid migration-only churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/fs/composer.rs | 4 +-- litebox_broker_core/src/fs/devices.rs | 10 +++--- litebox_broker_core/src/fs/in_mem.rs | 6 ++-- litebox_broker_core/src/fs/nine_p/mod.rs | 6 ++-- litebox_broker_core/src/fs/overlay.rs | 10 +++--- litebox_broker_core/src/fs/resolver.rs | 4 +-- litebox_broker_core/src/fs/service.rs | 2 +- litebox_broker_core/src/fs/tar_ro.rs | 6 ++-- litebox_broker_core/src/fs/tests.rs | 8 ++--- litebox_broker_local/src/fs.rs | 4 +-- litebox_broker_protocol/src/fs.rs | 32 +++++++++---------- litebox_broker_protocol/src/wire.rs | 6 ++-- litebox_broker_protocol/src/wire/fs.rs | 6 ++-- litebox_common_linux/src/lib.rs | 4 +-- litebox_shim_linux/src/syscalls/file.rs | 5 +-- .../src/syscalls/test_broker.rs | 2 +- litebox_shim_linux/src/syscalls/tests.rs | 2 +- litebox_shim_windows/src/syscalls/file.rs | 2 +- 18 files changed, 58 insertions(+), 61 deletions(-) diff --git a/litebox_broker_core/src/fs/composer.rs b/litebox_broker_core/src/fs/composer.rs index 3b003a69f3..48a6ef16c4 100644 --- a/litebox_broker_core/src/fs/composer.rs +++ b/litebox_broker_core/src/fs/composer.rs @@ -232,7 +232,7 @@ impl Composer { name, file_type: FileType::Directory, // TODO(jayb): set up proper inode info for these - node_info: None, + ino_info: None, }) .collect() } @@ -280,7 +280,7 @@ impl Composer { size: super::DEFAULT_DIRECTORY_SIZE, owner: UserInfo::ROOT, node_info, - block_size: super::DEFAULT_DIRECTORY_SIZE, + blksize: super::DEFAULT_DIRECTORY_SIZE, } } diff --git a/litebox_broker_core/src/fs/devices.rs b/litebox_broker_core/src/fs/devices.rs index 9bfe85002c..487b630100 100644 --- a/litebox_broker_core/src/fs/devices.rs +++ b/litebox_broker_core/src/fs/devices.rs @@ -87,7 +87,7 @@ impl Device { size: 0, owner: UserInfo::ROOT, node_info: STDIO_NODE_INFO, - block_size: STDIO_BLOCK_SIZE, + blksize: STDIO_BLOCK_SIZE, }, Device::Null => FileStatus { file_type: FileType::CharacterDevice, @@ -95,7 +95,7 @@ impl Device { size: 0, owner: UserInfo::ROOT, node_info: NULL_NODE_INFO, - block_size: NULL_BLOCK_SIZE, + blksize: NULL_BLOCK_SIZE, }, Device::URandom => FileStatus { file_type: FileType::CharacterDevice, @@ -103,7 +103,7 @@ impl Device { size: 0, owner: UserInfo::ROOT, node_info: URANDOM_NODE_INFO, - block_size: URANDOM_BLOCK_SIZE, + blksize: URANDOM_BLOCK_SIZE, }, } } @@ -238,7 +238,7 @@ impl Backend for Devices { .map(|(n, d)| DirEntry { name: String::from(*n), file_type: FileType::CharacterDevice, - node_info: Some(d.file_status().node_info), + ino_info: Some(d.file_status().node_info), }) .collect()) } @@ -317,7 +317,7 @@ impl Backend for Devices { size: super::DEFAULT_DIRECTORY_SIZE, owner: UserInfo::ROOT, node_info: self.root_inode, - block_size: super::DEFAULT_DIRECTORY_SIZE, + 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 27075a5c3b..8922da9895 100644 --- a/litebox_broker_core/src/fs/in_mem.rs +++ b/litebox_broker_core/src/fs/in_mem.rs @@ -394,7 +394,7 @@ impl super::backend::Backend for InMe DirEntry { name: name.clone(), file_type, - node_info: Some(node_info), + ino_info: Some(node_info), } }) .collect()) @@ -469,7 +469,7 @@ impl super::backend::Backend for InMe size: u64::try_from(file.data.len()).map_err(|_| FileStatusError::Io)?, owner: file.perms.userinfo, node_info: file.node_info, - block_size: BLOCK_SIZE, + blksize: BLOCK_SIZE, }) } super::backend::HandleRef::Dir(h) => { @@ -480,7 +480,7 @@ impl super::backend::Backend for InMe size: super::DEFAULT_DIRECTORY_SIZE, owner: dir.perms.userinfo, node_info: dir.node_info, - block_size: BLOCK_SIZE, + blksize: BLOCK_SIZE, }) } } diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index 12c85d9500..dc819cbc26 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -444,7 +444,7 @@ where .map(|entry| super::DirEntry { name: String::from_utf8_lossy(&entry.name).into_owned(), file_type: qid_type_to_file_type(entry.qid.typ), - node_info: Some(NodeInfo { + ino_info: Some(NodeInfo { dev: self.device_id, ino: entry.qid.path, rdev: None, @@ -735,7 +735,7 @@ fn rgetattr_to_file_status(attr: &fcall::Rgetattr, device_id: u64) -> Result Result Overlay { if entry.file_type != FileType::Directory { blocked.insert(entry.name.clone()); } - entry.node_info = entry - .node_info + entry.ino_info = entry + .ino_info .take() .map(|node| self.map_node(&mut self.state.lock().ids, None, node)); entries.insert( @@ -632,7 +632,7 @@ impl Overlay { continue; } let directory = lower_entry.file_type == FileType::Directory; - let lower_node = lower_entry.node_info.take(); + let lower_node = lower_entry.ino_info.take(); let entry = entries .entry(name.clone()) .or_insert_with(|| ResolvedEntry { @@ -644,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.node_info = lower_node.map(|node| { + entry.entry.ino_info = lower_node.map(|node| { self.map_node(&mut self.state.lock().ids, Some(layer), node) }); } @@ -655,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.node_info) { + 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 50ede11795..81b32b7f79 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -931,12 +931,12 @@ impl Resolver { @@ -253,7 +253,7 @@ impl super::backend::Backend for TarRo { size: super::DEFAULT_DIRECTORY_SIZE, owner: dir.owner.unwrap_or(DEFAULT_DIRECTORY_OWNER), node_info: dir.node_info, - block_size: BLOCK_SIZE, + blksize: BLOCK_SIZE, }) } } diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs index b5253fad86..486711171b 100644 --- a/litebox_broker_core/src/fs/tests.rs +++ b/litebox_broker_core/src/fs/tests.rs @@ -316,7 +316,7 @@ mod in_mem { _ => panic!("Unexpected entry: {}", entry.name), } if entry.name != "." && entry.name != ".." { - assert!(entry.node_info.is_some(), "Inode info should be present"); + 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. @@ -1044,7 +1044,7 @@ mod tar_ro { _ => panic!("Unexpected entry: {}", entry.name), } if entry.name != "." && entry.name != ".." { - assert!(entry.node_info.is_some(), "Inode info should be present"); + 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. @@ -1362,7 +1362,7 @@ mod overlay { assert_eq!(entries[2].name, "baz"); assert_eq!(entries[2].file_type, FileType::RegularFile); assert!( - entries[2].node_info.is_some(), + entries[2].ino_info.is_some(), "Inode info should be present" ); } @@ -1416,7 +1416,7 @@ mod overlay { _ => panic!("Unexpected entry: {}", entry.name), } if entry.name != "." && entry.name != ".." { - assert!(entry.node_info.is_some(), "Inode info should be present"); + 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. diff --git a/litebox_broker_local/src/fs.rs b/litebox_broker_local/src/fs.rs index 7bcb012870..1049d5dda5 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([ diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index db01f6b0b0..3dded6d1e5 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -72,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. @@ -83,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. @@ -566,7 +566,7 @@ 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()?; @@ -585,7 +585,7 @@ pub fn try_decode_directory_entries( entries.push(FileDirectoryEntry { name, file_type, - node_info, + ino_info, }); } if decoder.offset != payload.len() { @@ -619,7 +619,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| { @@ -669,7 +669,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()); @@ -732,12 +732,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, @@ -746,7 +746,7 @@ mod tests { FileDirectoryEntry { name: "device".into(), file_type: FileType::CharacterDevice, - node_info: Some(FileNodeInfo { + ino_info: Some(FileNodeInfo { dev: 5, ino: 7, rdev: NonZeroU64::new(11), @@ -763,7 +763,7 @@ 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: NonZeroU64::new(5), @@ -785,17 +785,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(); @@ -821,7 +821,7 @@ mod tests { let valid = encode_directory_entries(&[FileDirectoryEntry { name: "entry".into(), file_type: FileType::RegularFile, - node_info: None, + ino_info: None, }]) .unwrap(); @@ -855,7 +855,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); @@ -864,7 +864,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 4032ed70fc..648b40bebc 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -1099,7 +1099,7 @@ mod tests { ino: u64::MAX, rdev: Some(NonZeroU64::MAX), }, - block_size: u64::MAX, + blksize: u64::MAX, })), BrokerResult::File(FileResponse::HandleStatus(FileStatus { file_type: FileType::CharacterDevice, @@ -1114,7 +1114,7 @@ mod tests { ino: u64::MAX, rdev: Some(NonZeroU64::MAX), }, - block_size: u64::MAX, + blksize: u64::MAX, })), BrokerResult::File(FileResponse::Chmod), BrokerResult::File(FileResponse::Chown), @@ -1347,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 345f9eec40..9c526d2070 100644 --- a/litebox_broker_protocol/src/wire/fs.rs +++ b/litebox_broker_protocol/src/wire/fs.rs @@ -443,7 +443,7 @@ fn encode_status(encoder: &mut Encoder, status: FileStatus) { encoder.u64(status.node_info.dev); encoder.u64(status.node_info.ino); encode_optional_u64(encoder, status.node_info.rdev.map(NonZeroU64::get)); - encoder.u64(status.block_size); + encoder.u64(status.blksize); } fn decode_status(decoder: &mut Decoder<'_>) -> Result { @@ -456,13 +456,13 @@ fn decode_status(decoder: &mut Decoder<'_>) -> Result { let rdev = decode_optional_u64(decoder)? .map(|value| NonZeroU64::new(value).ok_or(WireError::InvalidTag)) .transpose()?; - let block_size = decoder.u64()?; + let blksize = decoder.u64()?; Ok(FileStatus { file_type, mode, size, owner, node_info: FileNodeInfo { dev, ino, rdev }, - block_size, + blksize, }) } diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 9f678cda93..1eaabda6e5 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -474,7 +474,7 @@ impl TryFrom for FileStat { size, owner: FileUser { user, group }, node_info: FileNodeInfo { dev, ino, rdev }, - block_size, + blksize: block_size, .. } = value; // Linux exposes signed sizes even where the Rust ABI struct uses `usize`. @@ -615,7 +615,7 @@ impl TryFrom for Statx { size, owner: FileUser { user, group }, node_info: FileNodeInfo { dev, ino, rdev }, - block_size, + blksize: block_size, .. } = value; let rdev = rdev.map_or(0, core::num::NonZeroU64::get); diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 1ab34c7d3e..de5c4bb475 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -2816,10 +2816,7 @@ impl Task { break; } let dirent64 = litebox_common_linux::LinuxDirent64 { - ino: entry - .node_info - .as_ref() - .map_or(0, |node_info| node_info.ino), + ino: entry.ino_info.as_ref().map_or(0, |node_info| node_info.ino), off: dir_off as u64, len: u16::try_from(len).map_err(|_| Errno::EOVERFLOW)?, typ: litebox_common_linux::DirentType::from(entry.file_type) as u8, diff --git a/litebox_shim_linux/src/syscalls/test_broker.rs b/litebox_shim_linux/src/syscalls/test_broker.rs index 1c9629f1dd..7972e26f6a 100644 --- a/litebox_shim_linux/src/syscalls/test_broker.rs +++ b/litebox_shim_linux/src/syscalls/test_broker.rs @@ -607,7 +607,7 @@ pub(crate) fn status(file_type: FileType, mode: u16) -> FileStatus { ino: 42, rdev: None, }, - block_size: DEFAULT_BLOCK_SIZE, + blksize: DEFAULT_BLOCK_SIZE, } } diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index a6f504fe24..895cba1cfd 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -89,7 +89,7 @@ pub(crate) fn directory_entry(name: &str, file_type: FileType, ino: u64) -> File FileDirectoryEntry { name: alloc::string::String::from(name), file_type, - node_info: Some(FileNodeInfo { + ino_info: Some(FileNodeInfo { dev: 1, ino, rdev: None, diff --git a/litebox_shim_windows/src/syscalls/file.rs b/litebox_shim_windows/src/syscalls/file.rs index 033a733942..bc300b0617 100644 --- a/litebox_shim_windows/src/syscalls/file.rs +++ b/litebox_shim_windows/src/syscalls/file.rs @@ -280,7 +280,7 @@ impl FileStatusMetadata { let end_of_file = i64::try_from(status.size).unwrap_or(i64::MAX); let allocation_size = status .size - .checked_next_multiple_of(status.block_size.max(1)) + .checked_next_multiple_of(status.blksize.max(1)) .and_then(|size| i64::try_from(size).ok()) .unwrap_or(i64::MAX); let file_id = status.node_info.ino; From bc1218b985c4634d2c413d89c5ed78ed875bf0b8 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 13:17:28 -0700 Subject: [PATCH 23/34] Restore filesystem type qualification Keep the existing qualified type references where imports would add migration-only churn. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/fs/errors.rs | 4 +- litebox_broker_core/src/fs/nine_p/mod.rs | 48 +++++++++++++----------- litebox_broker_core/src/fs/resolver.rs | 29 +++++++++----- litebox_broker_core/src/fs/tar_ro.rs | 29 ++++++++------ 4 files changed, 64 insertions(+), 46 deletions(-) diff --git a/litebox_broker_core/src/fs/errors.rs b/litebox_broker_core/src/fs/errors.rs index 903b8aef71..9b8d4041ca 100644 --- a/litebox_broker_core/src/fs/errors.rs +++ b/litebox_broker_core/src/fs/errors.rs @@ -5,8 +5,6 @@ use thiserror::Error; -use super::Mode; - // 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. @@ -209,7 +207,7 @@ pub enum PathError { #[cfg(debug_assertions)] dir: alloc::string::String, #[cfg(debug_assertions)] - perms: Mode, + perms: super::Mode, }, #[error("invalid characters, not permitted by underlying file system")] InvalidPathname, diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index dc819cbc26..10f6c2a103 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -14,10 +14,10 @@ use alloc::vec::Vec; use core::num::NonZeroU64; use core::sync::atomic::{AtomicBool, Ordering}; -use litebox_platform::sync; use thiserror::Error; use self::fcall::Rlerror; +use super::OFlags; use super::backend::{ DirHandle, FileHandle, HandleRef, PermissionCheck, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkedComponent, WalkingDirHandle, @@ -26,7 +26,8 @@ use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; -use super::{FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo}; +use litebox_platform::sync; + mod client; mod fcall; mod id_pool; @@ -52,7 +53,7 @@ pub struct NineP>, - /// Device id reported in every [`NodeInfo`] from this backend; inode numbers + /// Device id reported in every [`NodeInfo`](super::NodeInfo) from this backend; inode numbers /// come from the server's qids instead. device_id: u64, /// Whether `unlinkat` is supported by the server @@ -444,7 +445,7 @@ where .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(NodeInfo { + ino_info: Some(super::NodeInfo { dev: self.device_id, ino: entry.qid.path, rdev: None, @@ -497,7 +498,7 @@ where SeekBehavior::PositionBased } - fn status(&self, h: HandleRef<'_>) -> Result { + fn status(&self, h: HandleRef<'_>) -> Result { let fid = match h { HandleRef::File(h) => &h.get_typed::().fid, HandleRef::Dir(h) => &h.get_typed::().fid, @@ -569,7 +570,7 @@ where Ok(self.remove_at(&dir.into_typed::(), name, false)?) } - fn chmod(&self, h: HandleRef<'_>, mode: Mode) -> Result<(), ChmodError> { + fn chmod(&self, h: HandleRef<'_>, mode: super::Mode) -> Result<(), ChmodError> { let fid = match h { HandleRef::File(h) => &h.get_typed::().fid, HandleRef::Dir(h) => &h.get_typed::().fid, @@ -700,37 +701,40 @@ fn oflags_to_lopen(flags: OFlags) -> fcall::LOpenFlags { } /// Convert a Qid type to our FileType -fn qid_type_to_file_type(qid_type: fcall::QidType) -> FileType { +fn qid_type_to_file_type(qid_type: fcall::QidType) -> super::FileType { if qid_type.contains(fcall::QidType::DIR) { - FileType::Directory + super::FileType::Directory } else { - FileType::RegularFile + super::FileType::RegularFile } } -fn file_mode(mode: u32) -> Mode { - let bits = u16::try_from(mode & u32::from(Mode::SUPPORTED.bits())) +fn file_mode(mode: u32) -> super::Mode { + let bits = u16::try_from(mode & u32::from(super::Mode::SUPPORTED.bits())) .expect("supported file mode bits fit in u16"); - Mode::from_bits_retain(bits) + super::Mode::from_bits_retain(bits) } /// Convert getattr response to FileStatus /// /// Inode numbers come from the server's qids; `device_id` is the device the caller reports this /// filesystem as. -fn rgetattr_to_file_status(attr: &fcall::Rgetattr, device_id: u64) -> Result { +fn rgetattr_to_file_status( + attr: &fcall::Rgetattr, + device_id: u64, +) -> Result { let file_type = qid_type_to_file_type(attr.qid.typ); if attr.valid.contains(fcall::GetattrMask::BASIC) { - Ok(FileStatus { + Ok(super::FileStatus { file_type, mode: file_mode(attr.stat.mode), size: attr.stat.size, - owner: UserInfo { + 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: NodeInfo { + node_info: super::NodeInfo { dev: device_id, ino: attr.qid.path, rdev: NonZeroU64::new(attr.stat.rdev), @@ -738,19 +742,19 @@ fn rgetattr_to_file_status(attr: &fcall::Rgetattr, device_id: u64) -> Result Result for FileStatusError { #[cfg(debug_assertions)] dir: String::new(), #[cfg(debug_assertions)] - perms: Mode::empty(), + perms: super::Mode::empty(), }), _ => FileStatusError::Io, }, @@ -1008,7 +1012,7 @@ impl From for WalkError { #[cfg(debug_assertions)] dir: String::new(), #[cfg(debug_assertions)] - perms: Mode::empty(), + perms: super::Mode::empty(), }), _ => WalkError::Io, }, diff --git a/litebox_broker_core/src/fs/resolver.rs b/litebox_broker_core/src/fs/resolver.rs index 81b32b7f79..9ae4fd7a36 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -3,21 +3,23 @@ //! Path management, permission checks, and open-state operations above [`super::backend`]. +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::marker::PhantomData; + use super::errors::{ ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, ReadError, RmdirError, SeekError, TruncateError, UnlinkError, WalkError, WriteError, }; use super::{ - DirEntry, FileStatus, FileType, Mode, OFlags, SeekWhence, UserInfo, + FileType, Mode, OFlags, backend::{ CreationMetadata, DeviceIo, DirHandle, Handle, HandleRef, PermissionCheck, PermissionInfo, Permissioned, SeekBehavior, WalkOutcome, WalkStopReason, WalkingDirHandle, }, }; -use alloc::string::String; -use alloc::vec; -use alloc::vec::Vec; -use core::marker::PhantomData; +use super::{SeekWhence, UserInfo}; /// The broker-core filesystem resolver, generic over its synchronization platform and /// [`Backend`](super::backend::Backend). @@ -916,7 +918,10 @@ impl Resolver) -> Result, ReadDirError> { + pub fn read_dir( + &self, + entry: &ResolverEntry, + ) -> Result, ReadDirError> { if entry.path_only { // TODO(jayb): Add an error variant for operations not permitted on O_PATH fds. unimplemented!("read_dir on O_PATH fd") @@ -928,12 +933,12 @@ impl Resolver Resolver Result { + pub fn file_status( + &self, + user: UserInfo, + path: &str, + ) -> Result { let entry = self.open(user, path, OFlags::PATH, Mode::empty()) .map_err(|error| match error { @@ -962,7 +971,7 @@ impl Resolver, - ) -> Result { + ) -> Result { self.backend.status(entry.handle.as_ref()) } diff --git a/litebox_broker_core/src/fs/tar_ro.rs b/litebox_broker_core/src/fs/tar_ro.rs index a987889dd8..95aef5ccee 100644 --- a/litebox_broker_core/src/fs/tar_ro.rs +++ b/litebox_broker_core/src/fs/tar_ro.rs @@ -24,19 +24,22 @@ //! Taro Milk Tea, Tapioca Bubbles, 50% Sugar, No Ice. //! ``` +use alloc::string::String; +use alloc::vec::Vec; +use core::ops::Range; +use hashbrown::HashMap; + +use super::{DirEntry, FileType}; + use super::{ - DirEntry, FileStatus, FileType, Mode, NodeInfo, OFlags, UserInfo, + Mode, NodeInfo, OFlags, UserInfo, backend::{CreationMetadata, DirHandle, FileHandle, HandleRef, WalkingDirHandle}, errors::{ - ChmodError, ChownError, FileStatusError, MkdirError, OpenError, PathError, ReadDirError, - ReadError, RmdirError, TruncateError, UnlinkError, WalkError, WriteError, + ChmodError, ChownError, MkdirError, OpenError, PathError, ReadDirError, ReadError, + RmdirError, TruncateError, UnlinkError, WalkError, WriteError, }, inode_allocator::InodeAllocator, }; -use alloc::string::String; -use alloc::vec::Vec; -use core::ops::Range; -use hashbrown::HashMap; /// Block size for file system I/O operations // TODO(jayb): Determine appropriate block size @@ -232,14 +235,18 @@ impl super::backend::Backend for TarRo { super::backend::SeekBehavior::PositionBased } - fn status(&self, h: HandleRef<'_>) -> Result { + fn status( + &self, + h: HandleRef<'_>, + ) -> Result { match h { HandleRef::File(h) => { let file = &self.tar_index.files[h.get_typed::().idx]; - Ok(FileStatus { + Ok(super::FileStatus { file_type: FileType::RegularFile, mode: file.mode, - size: u64::try_from(file.data_range.len()).map_err(|_| FileStatusError::Io)?, + size: u64::try_from(file.data_range.len()) + .map_err(|_| super::errors::FileStatusError::Io)?, owner: file.owner, node_info: file.node_info, blksize: BLOCK_SIZE, @@ -247,7 +254,7 @@ impl super::backend::Backend for TarRo { } HandleRef::Dir(h) => { let dir = &self.tar_index.dirs[h.get_typed::().idx]; - Ok(FileStatus { + Ok(super::FileStatus { file_type: FileType::Directory, mode: DEFAULT_DIR_MODE, size: super::DEFAULT_DIRECTORY_SIZE, From d2bdcd3a3222d567d9038d0492ae763b787437c3 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 13:29:38 -0700 Subject: [PATCH 24/34] Preserve resolver seek arithmetic Convert canonical file size once and retain the existing usize-based resolver offset calculation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/fs/resolver.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/litebox_broker_core/src/fs/resolver.rs b/litebox_broker_core/src/fs/resolver.rs index 9ae4fd7a36..a0f222982e 100644 --- a/litebox_broker_core/src/fs/resolver.rs +++ b/litebox_broker_core/src/fs/resolver.rs @@ -699,25 +699,20 @@ impl Resolver 0, - SeekWhence::RelativeToCurrentOffset => { - u64::try_from(entry.position).map_err(|_| SeekError::InvalidOffset)? - } + SeekWhence::RelativeToCurrentOffset => entry.position, SeekWhence::RelativeToEnd => file_len, }; let new_position = base - .checked_add_signed( - i64::try_from(offset).map_err(|_| SeekError::InvalidOffset)?, - ) + .checked_add_signed(offset) .ok_or(SeekError::InvalidOffset)?; // TODO(jayb): Linux allows regular files to seek past EOF, while some backends or // file types may not. Model that distinction instead of using one resolver rule. if new_position > file_len { return Err(SeekError::InvalidOffset); } - let new_position = - usize::try_from(new_position).map_err(|_| SeekError::InvalidOffset)?; entry.position = new_position; Ok(new_position) } From 0ee8901f3bdad16557a8775d9b6919087f42f992 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 14:13:42 -0700 Subject: [PATCH 25/34] Use resolver directly in filesystem tests Remove the test-only filesystem facade and have inherited filesystem and 9P tests call broker-core Resolver APIs directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/fs/mod.rs | 2 - litebox_broker_core/src/fs/nine_p/tests.rs | 60 +++--- litebox_broker_core/src/fs/test_support.rs | 205 ------------------ litebox_broker_core/src/fs/tests.rs | 238 ++++++++++++--------- 4 files changed, 168 insertions(+), 337 deletions(-) delete mode 100644 litebox_broker_core/src/fs/test_support.rs diff --git a/litebox_broker_core/src/fs/mod.rs b/litebox_broker_core/src/fs/mod.rs index 714a4f92ab..08c2216969 100644 --- a/litebox_broker_core/src/fs/mod.rs +++ b/litebox_broker_core/src/fs/mod.rs @@ -25,8 +25,6 @@ pub mod resolver; mod service; pub mod tar_ro; #[cfg(test)] -mod test_support; -#[cfg(test)] mod tests; pub use litebox_broker_protocol::fs::{ diff --git a/litebox_broker_core/src/fs/nine_p/tests.rs b/litebox_broker_core/src/fs/nine_p/tests.rs index 12b6ea3cc2..3c11b82da3 100644 --- a/litebox_broker_core/src/fs/nine_p/tests.rs +++ b/litebox_broker_core/src/fs/nine_p/tests.rs @@ -12,19 +12,25 @@ 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::test_support::{Fs, USER}; +use crate::fs::resolver::Resolver; use crate::fs::{FileType, Mode, OFlags, SeekWhence}; use crate::test_platform::TestPlatform; use super::{NineP, transport}; /// A resolver over a 9P backend reached through `T`. -type NinePFs = Fs>; +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( @@ -201,7 +207,7 @@ impl Drop for DiodServer { fn connect_9p(server: &DiodServer) -> NinePFs { let transport = TcpTransport::connect(&server.addr()); - Fs::new(attach(transport, server)) + Resolver::new(attach(transport, server)) } // --------------------------------------------------------------------------- @@ -225,7 +231,7 @@ fn test_nine_p_create_and_read_file() { let data = b"Hello from litebox 9P!"; let written = fs - .write(&mut fd, data, None) + .write(&NoDeviceIo, &mut fd, data, None) .expect("failed to write via 9P"); assert_eq!(written, data.len()); @@ -244,7 +250,7 @@ fn test_nine_p_create_and_read_file() { let mut buf = alloc::vec![0u8; 256]; let bytes_read = fs - .read(&mut fd, &mut buf, None) + .read(&NoDeviceIo, &mut fd, &mut buf, None) .expect("failed to read via 9P"); assert_eq!(&buf[..bytes_read], data); @@ -271,7 +277,8 @@ fn test_nine_p_mkdir_and_readdir() { Mode::RWXU, ) .expect("failed to create file in subdir"); - fs.write(&mut fd, b"nested content", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"nested content", None) + .unwrap(); drop(fd); // Read the root directory @@ -364,7 +371,7 @@ fn test_nine_p_file_status() { ) .expect("failed to create file"); let data = b"1234567890"; - fs.write(&mut fd, data, None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, data, None).unwrap(); drop(fd); // Check file_status via path @@ -404,7 +411,7 @@ fn test_nine_p_seek_and_partial_read() { Mode::RWXU, ) .expect("failed to create file"); - fs.write(&mut fd, b"ABCDEFGHIJ", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"ABCDEFGHIJ", None).unwrap(); drop(fd); // Open for reading and seek @@ -420,7 +427,9 @@ fn test_nine_p_seek_and_partial_read() { // Read from offset 5 → should get "FGHIJ" let mut buf = alloc::vec![0u8; 10]; - let n = fs.read(&mut 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"); drop(fd); @@ -440,7 +449,8 @@ fn test_nine_p_truncate() { Mode::RWXU, ) .expect("failed to create file"); - fs.write(&mut fd, b"Hello, World!", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"Hello, World!", None) + .unwrap(); // Truncate to 5 bytes fs.truncate(&mut fd, 5, true) @@ -472,7 +482,7 @@ fn test_nine_p_host_files_visible() { .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(&mut fd, &mut buf, None).unwrap(); + let n = fs.read(&NoDeviceIo, &mut fd, &mut buf, None).unwrap(); assert_eq!(&buf[..n], b"from host"); drop(fd); @@ -554,7 +564,7 @@ impl transport::Write for BrokenTransport { /// additional write. fn connect_9p_broken(server: &DiodServer, allowed_writes: usize) -> NinePFs { let tcp = TcpTransport::connect(&server.addr()); - Fs::new(attach(BrokenTransport::new(tcp, allowed_writes), server)) + Resolver::new(attach(BrokenTransport::new(tcp, allowed_writes), server)) } // --------------------------------------------------------------------------- @@ -597,7 +607,7 @@ fn test_nine_p_broken_read() { .expect("open should succeed before break"); let mut buf = alloc::vec![0u8; 64]; - let result = fs.read(&mut fd, &mut buf, None); + let result = fs.read(&NoDeviceIo, &mut fd, &mut buf, None); assert!(matches!(result, Err(ReadError::Io))); } @@ -618,7 +628,7 @@ fn test_nine_p_broken_write() { ) .expect("create should succeed before break"); - let result = fs.write(&mut fd, b"data", None); + let result = fs.write(&NoDeviceIo, &mut fd, b"data", None); assert!(matches!(result, Err(WriteError::Io))); } @@ -736,7 +746,8 @@ fn test_nine_p_deep_path_walk() { let mut fd = fs .open(USER, &file_path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("failed to create file in deep path"); - fs.write(&mut fd, b"deep content", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"deep content", None) + .unwrap(); drop(fd); // Read it back @@ -744,7 +755,7 @@ fn test_nine_p_deep_path_walk() { .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(&mut fd, &mut buf, None).unwrap(); + let n = fs.read(&NoDeviceIo, &mut fd, &mut buf, None).unwrap(); assert_eq!(&buf[..n], b"deep content"); drop(fd); @@ -849,7 +860,8 @@ fn test_nine_p_handle_status() { Mode::RWXU, ) .expect("failed to create file"); - fs.write(&mut fd, b"hello fd_stat", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"hello fd_stat", None) + .unwrap(); drop(fd); // Open the file and check the open-handle status @@ -898,7 +910,7 @@ fn test_nine_p_large_read_write() { let mut written = 0; while written < data.len() { let n = fs - .write(&mut fd, &data[written..], None) + .write(&NoDeviceIo, &mut fd, &data[written..], None) .expect("write failed"); assert!(n > 0, "write should make progress"); written += n; @@ -916,7 +928,7 @@ fn test_nine_p_large_read_write() { let mut total_read = 0; while total_read < data.len() { let n = fs - .read(&mut fd, &mut read_buf[total_read..], None) + .read(&NoDeviceIo, &mut fd, &mut read_buf[total_read..], None) .expect("read failed"); if n == 0 { break; @@ -944,17 +956,17 @@ fn test_nine_p_explicit_offset_read_write() { .expect("failed to create file"); // Write "AAAAAAAAAA" at offset 0 using implicit offset - fs.write(&mut 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(&mut 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(&mut fd, b"C", None).unwrap(); + fs.write(&NoDeviceIo, &mut fd, b"C", None).unwrap(); drop(fd); @@ -971,7 +983,7 @@ fn test_nine_p_explicit_offset_read_write() { // Read 5 bytes at explicit offset 5 → "BBBBB" let mut buf = alloc::vec![0u8; 5]; let n = fs - .read(&mut 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"); @@ -980,7 +992,7 @@ fn test_nine_p_explicit_offset_read_write() { // Read using implicit offset → should start at 0 let mut buf = alloc::vec![0u8; 11]; let n = fs - .read(&mut fd, &mut buf, None) + .read(&NoDeviceIo, &mut fd, &mut buf, None) .expect("implicit read failed"); assert_eq!(&buf[..n], b"AAAAABBBBBC"); diff --git a/litebox_broker_core/src/fs/test_support.rs b/litebox_broker_core/src/fs/test_support.rs deleted file mode 100644 index 6281f2e8f0..0000000000 --- a/litebox_broker_core/src/fs/test_support.rs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Shared support for the broker-core filesystem tests. -//! -//! The [`Fs`] facade pairs a [`Resolver`] with the device I/O a broker session would supply, so -//! the tests can exercise resolver and backend semantics without a session, transport, or guest -//! descriptor table. Every method delegates directly to the resolver; no behavior is added here. - -use alloc::vec::Vec; - -use litebox_broker_protocol::stdio::StdioOutputStream; - -use super::backend::{Backend, DeviceIo, NoDeviceIo}; -#[cfg(target_os = "linux")] -use super::errors::TruncateError; -use super::errors::{ - ChmodError, ChownError, FileStatusError, MkdirError, OpenError, ReadDirError, ReadError, - RmdirError, SeekError, UnlinkError, WriteError, -}; -use super::resolver::{Resolver, ResolverEntry}; -use super::{DirEntry, FileStatus, Mode, OFlags, SeekWhence, UserInfo}; -use crate::test_platform::TestPlatform; - -/// The unprivileged user these tests act as unless they need root. -pub(in crate::fs) const USER: UserInfo = UserInfo { - user: 1000, - group: 1000, -}; - -/// The privileged user, used for test setup that has to bypass permission checks. -pub(in crate::fs) const ROOT: UserInfo = UserInfo::ROOT; - -/// A test-only facade over [`Resolver`]. -/// -/// It supplies the device I/O that a broker session normally provides, and it keeps the ported -/// test bodies readable by not repeating that argument at every call site. Every method delegates -/// directly to the resolver, so no behavior is added here. -pub(in crate::fs) struct Fs { - resolver: Resolver, -} - -/// One open filesystem entry, the resolver's equivalent of an open file description. -pub(in crate::fs) type Entry = ResolverEntry; - -impl Fs { - pub(in crate::fs) fn new(backend: BackendType) -> Self { - Self { - resolver: Resolver::new(backend), - } - } - - pub(in crate::fs) fn open( - &self, - user: UserInfo, - path: &str, - flags: OFlags, - mode: Mode, - ) -> Result, OpenError> { - self.resolver.open(user, path, flags, mode) - } - - pub(in crate::fs) fn read( - &self, - entry: &mut Entry, - buf: &mut [u8], - offset: Option, - ) -> Result { - self.read_with(&NoDeviceIo, entry, buf, offset) - } - - pub(in crate::fs) fn write( - &self, - entry: &mut Entry, - buf: &[u8], - offset: Option, - ) -> Result { - self.write_with(&NoDeviceIo, entry, buf, offset) - } - - /// Like [`Self::read`], but against the device I/O a broker session would supply. - pub(in crate::fs) fn read_with( - &self, - device_io: &dyn DeviceIo, - entry: &mut Entry, - buf: &mut [u8], - offset: Option, - ) -> Result { - self.resolver.read(device_io, entry, buf, offset) - } - - /// Like [`Self::write`], but against the device I/O a broker session would supply. - pub(in crate::fs) fn write_with( - &self, - device_io: &dyn DeviceIo, - entry: &mut Entry, - buf: &[u8], - offset: Option, - ) -> Result { - self.resolver.write(device_io, entry, buf, offset) - } - - pub(in crate::fs) fn seek( - &self, - entry: &mut Entry, - offset: isize, - whence: SeekWhence, - ) -> Result { - self.resolver.seek(entry, offset, whence) - } - - #[cfg(target_os = "linux")] - pub(in crate::fs) fn truncate( - &self, - entry: &mut Entry, - length: usize, - reset_offset: bool, - ) -> Result<(), TruncateError> { - self.resolver.truncate(entry, length, reset_offset) - } - - pub(in crate::fs) fn chmod( - &self, - user: UserInfo, - path: &str, - mode: Mode, - ) -> Result<(), ChmodError> { - self.resolver.chmod(user, path, mode) - } - - pub(in crate::fs) fn chown( - &self, - user: UserInfo, - path: &str, - owner: Option, - group: Option, - ) -> Result<(), ChownError> { - self.resolver.chown(user, path, owner, group) - } - - pub(in crate::fs) fn unlink(&self, user: UserInfo, path: &str) -> Result<(), UnlinkError> { - self.resolver.unlink(user, path) - } - - pub(in crate::fs) fn mkdir( - &self, - user: UserInfo, - path: &str, - mode: Mode, - ) -> Result<(), MkdirError> { - self.resolver.mkdir(user, path, mode) - } - - pub(in crate::fs) fn rmdir(&self, user: UserInfo, path: &str) -> Result<(), RmdirError> { - self.resolver.rmdir(user, path) - } - - pub(in crate::fs) fn read_dir( - &self, - entry: &Entry, - ) -> Result, ReadDirError> { - self.resolver.read_dir(entry) - } - - pub(in crate::fs) fn file_status( - &self, - user: UserInfo, - path: &str, - ) -> Result { - self.resolver.file_status(user, path) - } - - pub(in crate::fs) fn handle_status( - &self, - entry: &Entry, - ) -> Result { - self.resolver.handle_status(entry) - } -} - -/// Device I/O for a broker session whose standard-I/O provider cannot service transfers. -/// -/// This mirrors [`crate::stdio`], which completes empty transfers without consulting the provider -/// and reports an I/O error for anything that would need it. -pub(in crate::fs) 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) - } -} diff --git a/litebox_broker_core/src/fs/tests.rs b/litebox_broker_core/src/fs/tests.rs index 486711171b..dfdfa14015 100644 --- a/litebox_broker_core/src/fs/tests.rs +++ b/litebox_broker_core/src/fs/tests.rs @@ -3,36 +3,67 @@ //! Filesystem semantics for the broker-core resolver and its backends. //! -//! These tests drive the resolver and the backends directly, through the [`Fs`] facade from -//! [`super::test_support`]. There is deliberately no broker session, transport, or guest -//! descriptor table involved: the semantics under test are owned by broker core. +//! 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::test_support::{Fs, ROOT, USER, UnservicedStdio}; 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 in_mem_fs() -> Fs> { - Fs::new(InMem::::new(InodeAllocator::standalone())) + 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]>) -> Fs { - Fs::new(TarRo::new(tar_data, 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]>, -) -> Fs> { - Fs::new(Overlay::::new( +) -> Resolver> { + Resolver::new(Overlay::::new( upper, TarRo::new(tar_data, InodeAllocator::standalone()), InodeAllocator::standalone(), @@ -41,19 +72,18 @@ fn overlay_fs( mod in_mem { use super::{ - FileType, Fs, InMem, Mode, OFlags, ROOT, SeekWhence, TestPlatform, USER, UserInfo, - in_mem_fs, + 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 crate::fs::test_support::Entry; use alloc::vec; use alloc::vec::Vec; - type InMemFs = Fs>; - type InMemEntry = Entry>; + 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) { @@ -96,7 +126,7 @@ mod in_mem { .open(ROOT, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let data = b"Hello, world!"; - fs.write(&mut fd, data, None) + fs.write(&NoDeviceIo, &mut fd, data, None) .expect("Failed to write to file"); drop(fd); @@ -106,7 +136,7 @@ mod in_mem { .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(bytes_read, data.len()); assert_eq!(&buffer, data); @@ -121,11 +151,12 @@ mod in_mem { let mut fd = fs .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::WUSR) .expect("Failed to create write-only file"); - fs.write(&mut fd, b"x", None).expect("Failed to write file"); + fs.write(&NoDeviceIo, &mut fd, b"x", None) + .expect("Failed to write file"); let mut buffer = [0]; assert!(matches!( - fs.read(&mut fd, &mut buffer, None), + fs.read(&NoDeviceIo, &mut fd, &mut buffer, None), Err(ReadError::NotForReading) )); drop(fd); @@ -145,7 +176,8 @@ mod in_mem { let mut fd = fs .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::empty()) .expect("Failed to create zero-mode file"); - fs.write(&mut fd, b"x", None).expect("Failed to write 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"); @@ -204,9 +236,9 @@ mod in_mem { .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); let data = b"Hello, world!"; - fs.write(&mut fd, data, None) + fs.write(&NoDeviceIo, &mut fd, data, None) .expect("Failed to write to file"); - fs.write(&mut fd, &data[2..], Some(2)) + fs.write(&NoDeviceIo, &mut fd, &data[2..], Some(2)) .expect("Failed to write to file with offset"); drop(fd); @@ -216,10 +248,10 @@ mod in_mem { .expect("Failed to open file"); let mut buffer = vec![0; data.len()]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); let bytes_read2 = fs - .read(&mut fd, &mut buffer[2..], Some(2)) + .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); @@ -566,7 +598,7 @@ mod in_mem { .expect("Failed to create new file with O_CREAT | O_EXCL"); // Write some data to verify file was created - fs.write(&mut fd, b"test data", None) + fs.write(&NoDeviceIo, &mut fd, b"test data", None) .expect("Failed to write to new file"); drop(fd); @@ -594,7 +626,7 @@ mod in_mem { // Verify we can read the data let mut buffer = vec![0; 9]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"test data"); drop(fd); @@ -630,7 +662,7 @@ mod in_mem { .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(&mut fd, initial_data, None) + fs.write(&NoDeviceIo, &mut fd, initial_data, None) .expect("Failed to write initial content"); drop(fd); @@ -640,7 +672,7 @@ mod in_mem { .expect("Failed to open file for reading"); let mut buffer = vec![0; initial_data.len()]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .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); @@ -653,7 +685,7 @@ mod in_mem { // Write new content to the truncated file let new_data = b"New content"; - fs.write(&mut fd, new_data, None) + fs.write(&NoDeviceIo, &mut fd, new_data, None) .expect("Failed to write new content"); drop(fd); @@ -663,7 +695,7 @@ mod in_mem { .expect("Failed to open file for verification"); let mut buffer = vec![0; initial_data.len()]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .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); @@ -673,7 +705,7 @@ mod in_mem { let mut fd = fs .open(USER, path, OFlags::WRONLY, Mode::empty()) .expect("Failed to open file for writing"); - fs.write(&mut fd, b"More content to truncate", None) + fs.write(&NoDeviceIo, &mut fd, b"More content to truncate", None) .expect("Failed to write more content"); drop(fd); @@ -684,19 +716,19 @@ mod in_mem { // File should be empty after truncation let mut buffer = vec![0; 100]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .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(&mut fd, test_data, None) + 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(&mut fd, &mut buffer, None) + .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); @@ -718,7 +750,7 @@ mod in_mem { .expect("open failed"); // 1. First positional write; position should advance by 6. - fs.write(&mut fd, b"abcdef", None) + fs.write(&NoDeviceIo, &mut fd, b"abcdef", None) .expect("first write failed"); // 2. Rewind to beginning. @@ -726,7 +758,8 @@ mod in_mem { .expect("seek failed"); // 3. Another positional write should write from start - fs.write(&mut fd, b"X", None).expect("overwrite failed"); + fs.write(&NoDeviceIo, &mut fd, b"X", None) + .expect("overwrite failed"); // The file offset should now be at 1. assert_eq!( @@ -739,17 +772,21 @@ mod in_mem { fs.seek(&mut fd, 0, SeekWhence::RelativeToBeginning) .expect("seek failed"); let mut buf = [0u8; 16]; - let n = fs.read(&mut fd, &mut buf, None).expect("read failed"); + 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(&mut fd, b"12", None) + 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(&mut fd, &mut buf2, None).expect("read 2 failed"); + let n2 = fs + .read(&NoDeviceIo, &mut fd, &mut buf2, None) + .expect("read 2 failed"); assert_eq!(n2, 8); assert_eq!(&buf2[..n2], b"Xbcdef12"); } @@ -759,7 +796,7 @@ mod in_mem { let mut fd = fs .open(USER, path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) .expect("Failed to create file"); - fs.write(&mut fd, data, None) + fs.write(&NoDeviceIo, &mut fd, data, None) .expect("Failed to write initial content"); } @@ -767,7 +804,7 @@ mod in_mem { fn read_all(fs: &InMemFs, fd: &mut InMemEntry) -> Vec { let mut buffer = vec![0; 64]; let bytes_read = fs - .read(fd, &mut buffer, None) + .read(&NoDeviceIo, fd, &mut buffer, None) .expect("Failed to read from file"); buffer.truncate(bytes_read); buffer @@ -786,7 +823,7 @@ mod in_mem { let mut fd = fs .open(USER, path, OFlags::WRONLY | OFlags::APPEND, Mode::empty()) .expect("Failed to open file with O_APPEND"); - fs.write(&mut fd, b" World", None) + fs.write(&NoDeviceIo, &mut fd, b" World", None) .expect("Failed to append data"); drop(fd); @@ -816,7 +853,7 @@ mod in_mem { .expect("Failed to seek to beginning"); // Write some data - it should go to the end despite the seek - fs.write(&mut fd, b"123", None) + fs.write(&NoDeviceIo, &mut fd, b"123", None) .expect("Failed to write after seek"); drop(fd); @@ -849,7 +886,7 @@ mod in_mem { .expect("Seek failed"); // Write should append to end, ignoring the current position - fs.write(&mut fd, b" World", None) + fs.write(&NoDeviceIo, &mut fd, b" World", None) .expect("Failed to write with append"); // Seek to beginning and read the whole file @@ -873,7 +910,8 @@ mod in_mem { .expect("Failed to open file with O_APPEND"); // pwrite (write with explicit offset) should ignore O_APPEND per POSIX - fs.write(&mut fd, b"XX", Some(2)).expect("Failed to pwrite"); + 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 @@ -903,9 +941,9 @@ mod in_mem { .expect("Failed to open file with O_TRUNC | O_APPEND"); // File should be truncated, then write should append (to empty file) - fs.write(&mut fd, b"New", None) + fs.write(&NoDeviceIo, &mut fd, b"New", None) .expect("Failed to write after truncation"); - fs.write(&mut fd, b"Content", None) + fs.write(&NoDeviceIo, &mut fd, b"Content", None) .expect("Failed to write second chunk"); drop(fd); @@ -918,7 +956,7 @@ mod in_mem { } mod tar_ro { - use super::{FileType, Mode, OFlags, TEST_TAR_FILE, USER, tar_ro_fs}; + 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; @@ -931,7 +969,7 @@ mod tar_ro { .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"testfoo\n"); drop(fd); @@ -941,7 +979,7 @@ mod tar_ro { .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"test bar baz\n"); } @@ -1076,8 +1114,8 @@ mod tar_ro { mod overlay { use super::{ - FileType, Fs, Mode, OFlags, Overlay, SeekWhence, TEST_TAR_FILE, TestPlatform, USER, - UserInfo, + 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}; @@ -1109,7 +1147,7 @@ mod overlay { ) } - fn overlay_fs(upper: InMem) -> Fs> { + fn overlay_fs(upper: InMem) -> Resolver> { super::overlay_fs(upper, TEST_TAR_FILE.into()) } @@ -1121,7 +1159,7 @@ mod overlay { .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .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"); @@ -1138,7 +1176,7 @@ mod overlay { .expect("Failed to open file"); let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .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"); @@ -1172,17 +1210,17 @@ mod overlay { let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd1, &mut buffer, None) + .read(&NoDeviceIo, &mut fd1, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"testfoo\n"); - fs.write(&mut fd2, b"share", None) + 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(&mut fd1, &mut buffer, None) + .read(&NoDeviceIo, &mut fd1, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"shareoo\n"); } @@ -1202,15 +1240,15 @@ mod overlay { let mut buffer = vec![0; 4]; let bytes_read = fs - .read(&mut fd1, &mut buffer, None) + .read(&NoDeviceIo, &mut fd1, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"test"); - fs.write(&mut fd2, b"share", None) + fs.write(&NoDeviceIo, &mut fd2, b"share", None) .expect("Failed to write to file"); let bytes_read = fs - .read(&mut fd1, &mut buffer, None) + .read(&NoDeviceIo, &mut fd1, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"eoo\n"); } @@ -1226,7 +1264,7 @@ mod overlay { // The file exists, and is readable let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"test"); @@ -1235,7 +1273,7 @@ mod overlay { // This should not really impact the readability; file is fine. let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"foo\n"); @@ -1342,7 +1380,7 @@ mod overlay { // The file exists, and is readable let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from file"); assert_eq!(&buffer[..bytes_read], b"test"); } @@ -1459,7 +1497,7 @@ mod overlay { ) .expect("Failed to create new file with O_CREAT | O_EXCL"); - fs.write(&mut fd, b"overlay test", None) + fs.write(&NoDeviceIo, &mut fd, b"overlay test", None) .expect("Failed to write to new file"); drop(fd); @@ -1501,7 +1539,7 @@ mod overlay { ) .expect("Failed to create file over tombstone with O_CREAT | O_EXCL"); - fs.write(&mut fd, b"new foo content", None) + fs.write(&NoDeviceIo, &mut fd, b"new foo content", None) .expect("Failed to write to recreated file"); drop(fd); @@ -1511,7 +1549,7 @@ mod overlay { .expect("Failed to open recreated file"); let mut buffer = vec![0; 15]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .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); @@ -1526,7 +1564,7 @@ mod overlay { Mode::RWXU, ) .expect("Failed to create upper layer file"); - fs.write(&mut fd, b"upper content", None) + fs.write(&NoDeviceIo, &mut fd, b"upper content", None) .expect("Failed to write to upper layer file"); drop(fd); @@ -1583,7 +1621,7 @@ mod overlay { // Write data to the file let data = b"Hello from nested file!"; - fs.write(&mut fd, data, None) + fs.write(&NoDeviceIo, &mut fd, data, None) .expect("Failed to write to bar/test"); drop(fd); @@ -1593,7 +1631,7 @@ mod overlay { .expect("Failed to open bar/test for reading"); let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from bar/test"); assert_eq!(&buffer[..bytes_read], data); drop(fd); @@ -1617,7 +1655,7 @@ mod overlay { // Write new data to the file (overwriting existing content) let data = b"Modified content!"; - fs.write(&mut fd, data, None) + fs.write(&NoDeviceIo, &mut fd, data, None) .expect("Failed to write to bar/baz"); drop(fd); @@ -1627,7 +1665,7 @@ mod overlay { .expect("Failed to open bar/baz for reading"); let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read from bar/baz"); assert_eq!(&buffer[..bytes_read], data); @@ -1652,12 +1690,12 @@ mod overlay { // File should be truncated (empty) let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read file"); assert_eq!(bytes_read, 0); // Write new content - fs.write(&mut fd, b"new content", None) + fs.write(&NoDeviceIo, &mut fd, b"new content", None) .expect("Failed to write to file"); drop(fd); @@ -1667,7 +1705,7 @@ mod overlay { .expect("Failed to reopen file"); let mut buffer = vec![0; 1024]; let bytes_read = fs - .read(&mut fd, &mut buffer, None) + .read(&NoDeviceIo, &mut fd, &mut buffer, None) .expect("Failed to read file"); assert_eq!(&buffer[..bytes_read], b"new content"); } @@ -1782,7 +1820,7 @@ mod overlay { let mut fd = fs .open(USER, "foo", OFlags::WRONLY, Mode::RWXU) .expect("Failed to open file for writing"); - fs.write(&mut fd, b"x", None) + fs.write(&NoDeviceIo, &mut fd, b"x", None) .expect("Failed to write to file"); drop(fd); let _ = tx.send(()); @@ -1794,14 +1832,14 @@ mod overlay { } mod devices { - use super::{Fs, Mode, OFlags, USER, UnservicedStdio}; + 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() -> Fs { - Fs::new( + fn devices_fs() -> Resolver { + Resolver::new( Composer::builder() .mount("/dev", Devices::new) .build() @@ -1819,12 +1857,9 @@ mod devices { 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_with(&stdio, &mut fd_stdout, b"", None), - Ok(0) - )); - assert!(matches!( - fs.write_with(&stdio, &mut fd_stdout, b"Hello, stdout!", None), + fs.write(&stdio, &mut fd_stdout, b"Hello, stdout!", None), Err(WriteError::Io) )); drop(fd_stdout); @@ -1832,12 +1867,9 @@ mod devices { 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_with(&stdio, &mut fd_stderr, b"", None), - Ok(0) - )); - assert!(matches!( - fs.write_with(&stdio, &mut fd_stderr, b"Hello, stderr!", None), + fs.write(&stdio, &mut fd_stderr, b"Hello, stderr!", None), Err(WriteError::Io) )); drop(fd_stderr); @@ -1846,12 +1878,12 @@ mod devices { .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /dev/stdin"); assert!(matches!( - fs.read_with(&stdio, &mut fd_stdin, &mut [], None), + fs.read(&stdio, &mut fd_stdin, &mut [], None), Ok(0) )); let mut buffer = vec![0; 13]; assert!(matches!( - fs.read_with(&stdio, &mut fd_stdin, &mut buffer, None), + fs.read(&stdio, &mut fd_stdin, &mut buffer, None), Err(ReadError::Io) )); } @@ -1869,15 +1901,15 @@ mod devices { } mod composed { - use super::{Fs, InMem, Mode, OFlags, TestPlatform, USER, UnservicedStdio, UserInfo}; + 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() -> Fs { - Fs::new( + fn composed_fs() -> Resolver { + Resolver::new( Composer::builder() .mount("/", |_| { InMem::::new_initialized([( @@ -1902,12 +1934,9 @@ mod composed { 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_with(&stdio, &mut fd_stdout, b"", None), - Ok(0) - )); - assert!(matches!( - fs.write_with(&stdio, &mut fd_stdout, b"Hello, composed stdout!", None), + fs.write(&stdio, &mut fd_stdout, b"Hello, composed stdout!", None), Err(WriteError::Io) )); drop(fd_stdout); @@ -1915,12 +1944,9 @@ mod composed { 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_with(&stdio, &mut fd_stderr, b"", None), - Ok(0) - )); - assert!(matches!( - fs.write_with(&stdio, &mut fd_stderr, b"Hello, composed stderr!", None), + fs.write(&stdio, &mut fd_stderr, b"Hello, composed stderr!", None), Err(WriteError::Io) )); drop(fd_stderr); @@ -1929,12 +1955,12 @@ mod composed { .open(USER, "/dev/stdin", OFlags::RDONLY, Mode::empty()) .expect("Failed to open /dev/stdin"); assert!(matches!( - fs.read_with(&stdio, &mut fd_stdin, &mut [], None), + fs.read(&stdio, &mut fd_stdin, &mut [], None), Ok(0) )); let mut buffer = vec![0; 1024]; assert!(matches!( - fs.read_with(&stdio, &mut fd_stdin, &mut buffer, None), + fs.read(&stdio, &mut fd_stdin, &mut buffer, None), Err(ReadError::Io) )); } From 817c039e74ef6eb699ab220980fd8a6446bde3ac Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 14:38:55 -0700 Subject: [PATCH 26/34] Share filesystem mode conversion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/fs/nine_p/mod.rs | 10 ++-------- litebox_broker_protocol/src/fs.rs | 21 +++++++++++++++++++++ litebox_broker_userland/src/linux.rs | 10 ++-------- litebox_runner_linux_userland/tests/run.rs | 4 +--- litebox_shim_linux/src/syscalls/file.rs | 19 +++++++------------ 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/litebox_broker_core/src/fs/nine_p/mod.rs b/litebox_broker_core/src/fs/nine_p/mod.rs index 10f6c2a103..38834b2fb4 100644 --- a/litebox_broker_core/src/fs/nine_p/mod.rs +++ b/litebox_broker_core/src/fs/nine_p/mod.rs @@ -709,12 +709,6 @@ fn qid_type_to_file_type(qid_type: fcall::QidType) -> super::FileType { } } -fn file_mode(mode: u32) -> super::Mode { - let bits = u16::try_from(mode & u32::from(super::Mode::SUPPORTED.bits())) - .expect("supported file mode bits fit in u16"); - super::Mode::from_bits_retain(bits) -} - /// Convert getattr response to FileStatus /// /// Inode numbers come from the server's qids; `device_id` is the device the caller reports this @@ -728,7 +722,7 @@ fn rgetattr_to_file_status( if attr.valid.contains(fcall::GetattrMask::BASIC) { Ok(super::FileStatus { file_type, - mode: file_mode(attr.stat.mode), + 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)?, @@ -745,7 +739,7 @@ fn rgetattr_to_file_status( Ok(super::FileStatus { file_type, mode: if attr.valid.contains(fcall::GetattrMask::MODE) { - file_mode(attr.stat.mode) + super::Mode::from_u32_bits_truncate(attr.stat.mode) } else { super::Mode::empty() }, diff --git a/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index 3dded6d1e5..ccd5639fdf 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -197,6 +197,19 @@ bitflags! { } } +impl FileMode { + /// Creates a mode from wider mode bits, discarding bits not defined by this protocol. + #[must_use] + #[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) + } +} + /// ABI-neutral fs open flags. /// /// These values are intentionally independent of target-specific `O_*` bit @@ -726,6 +739,14 @@ mod tests { use super::*; use alloc::vec; + #[test] + fn file_mode_truncates_wider_bits() { + assert_eq!( + FileMode::from_u32_bits_truncate(u32::MAX), + FileMode::SUPPORTED + ); + } + #[test] fn directory_payload_round_trips_all_entry_shapes() { let entries = vec![ diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index 941f4f0893..31dd380a5e 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -158,7 +158,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo entries.push(( path_to_string(path)?, InitialNode::Directory { - mode: file_mode(metadata.st_mode()), + mode: Mode::from_u32_bits_truncate(metadata.st_mode()), owner, }, )); @@ -179,7 +179,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo entries.push(( path_to_string(&program)?, InitialNode::File { - mode: file_mode(metadata.st_mode()), + mode: Mode::from_u32_bits_truncate(metadata.st_mode()), owner: guest_owner(previous_user, metadata.st_uid()), data: program_data.into(), }, @@ -244,12 +244,6 @@ fn guest_owner(previous_user: u32, user: u32) -> UserInfo { } } -fn file_mode(mode: u32) -> Mode { - let bits = u16::try_from(mode & u32::from(Mode::SUPPORTED.bits())) - .expect("supported file mode bits fit in u16"); - Mode::from_bits_retain(bits) -} - fn inferred_linux_runner_args( args: &super::CliArgs, ) -> Option { diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 29f343f933..c2c530edbd 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -371,9 +371,7 @@ fn test_file_service( ); let metadata = std::fs::metadata(entry.path()).expect("failed to inspect runner test file"); - let bits = u16::try_from(metadata.permissions().mode() & 0o7777) - .expect("supported file mode bits fit in u16"); - let mode = Mode::from_bits_retain(bits); + let mode = Mode::from_u32_bits_truncate(metadata.permissions().mode()); let node = if metadata.is_dir() { InitialNode::Directory { mode, diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index de5c4bb475..0b873f7ed4 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -83,16 +83,10 @@ impl FsState { } fn umask(&self) -> Mode { - file_mode_from_linux(self.umask.load(Ordering::Relaxed)) + Mode::from_u32_bits_truncate(self.umask.load(Ordering::Relaxed)) } } -fn file_mode_from_linux(mode: u32) -> Mode { - let bits = u16::try_from(mode & u32::from(Mode::SUPPORTED.bits())) - .expect("supported file mode bits fit in u16"); - Mode::from_bits_retain(bits) -} - /// 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 @@ -486,13 +480,14 @@ impl Task { /// Handle syscall `umask` pub(crate) fn sys_umask(&self, new_mask: u32) -> Mode { - let new_mask = file_mode_from_linux(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().into(), Ordering::Relaxed); - file_mode_from_linux(old_mask) + Mode::from_u32_bits_truncate(old_mask) } /// Handle syscall `open` @@ -552,7 +547,7 @@ impl Task { }; match file_type { InodeType::File => { - let mode = file_mode_from_linux(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, @@ -951,7 +946,7 @@ impl Task { mode: u32, ) -> Result<(), Errno> { let pathname = self.resolve_path_at(dirfd, pathname)?; - self.do_mkdir(pathname, file_mode_from_linux(mode)) + self.do_mkdir(pathname, Mode::from_u32_bits_truncate(mode)) } pub(crate) fn do_close(&self, raw_fd: usize) -> Result<(), Errno> { @@ -1511,7 +1506,7 @@ impl Task { group: stat.st_gid, }; Self::do_access_mode( - file_mode_from_linux(stat.st_mode & 0o7777), + Mode::from_u32_bits_truncate(stat.st_mode & 0o7777), owner, caller, &mode, From 75bcb4fa5c68f9ff4f8a5feec0ca079b3a89ac2c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 15:12:37 -0700 Subject: [PATCH 27/34] Decouple broker filesystem from runner CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- dev_bench/unixbench/run_unixbench.py | 2 + litebox_broker_protocol/src/fs.rs | 8 -- litebox_broker_userland/src/linux.rs | 115 +----------------- litebox_broker_userland/src/main.rs | 2 +- litebox_broker_userland/src/windows.rs | 43 +++---- .../tests/userland_broker.rs | 4 - .../tests/loader.rs | 11 +- .../tests/common/runner.rs | 3 +- .../tests/rewritten_guests.rs | 4 + litebox_runner_windows_userland/tests/run.rs | 23 +++- 10 files changed, 53 insertions(+), 162 deletions(-) diff --git a/dev_bench/unixbench/run_unixbench.py b/dev_bench/unixbench/run_unixbench.py index 0fe02d66c4..4c7d8bb48b 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/litebox_broker_protocol/src/fs.rs b/litebox_broker_protocol/src/fs.rs index ccd5639fdf..45c800e634 100644 --- a/litebox_broker_protocol/src/fs.rs +++ b/litebox_broker_protocol/src/fs.rs @@ -739,14 +739,6 @@ mod tests { use super::*; use alloc::vec; - #[test] - fn file_mode_truncates_wider_bits() { - assert_eq!( - FileMode::from_u32_bits_truncate(u32::MAX), - FileMode::SUPPORTED - ); - } - #[test] fn directory_payload_round_trips_all_entry_shapes() { let entries = vec![ diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index 31dd380a5e..005befbf03 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -7,7 +7,7 @@ use std::io::{BufRead, BufReader, Error as IoError, ErrorKind, Result as IoResul use std::net::{Ipv4Addr, SocketAddrV4}; use std::os::linux::fs::MetadataExt as _; use std::os::unix::net::{UnixListener, UnixStream}; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::{Child, ChildStdout, Command, Stdio}; use std::sync::Arc; use std::sync::mpsc::{RecvTimeoutError, sync_channel}; @@ -93,62 +93,8 @@ pub(super) fn run(mut args: super::CliArgs) -> Result<(), Box> { } fn create_file_service(args: &super::CliArgs) -> Result, Box> { - let runner_args = inferred_linux_runner_args(args); - let windows_runner = runner_is_windows_on_linux(args); - let initial_files = args - .fs_initial_files - .clone() - .or_else(|| { - runner_args - .as_ref() - .and_then(|args| args.initial_files.clone()) - }) - .or_else(|| { - windows_runner - .then(|| windows_runner_initial_files(&args.runner_arguments)) - .flatten() - }); - let program_from_tar = runner_args - .as_ref() - .is_some_and(|args| args.program_from_tar) - || windows_runner - || (args.fs_program.is_none() && args.fs_initial_files.is_some()); - if program_from_tar && initial_files.is_none() { - return Err(IoError::new( - ErrorKind::InvalidInput, - "a tar-backed guest program requires --fs-initial-files or runner --initial-files", - ) - .into()); - } - if runner_args.is_none() - && !windows_runner - && args.fs_program.is_none() - && args.fs_initial_files.is_none() - { - return Err(IoError::new( - ErrorKind::InvalidInput, - "could not infer broker filesystem inputs from runner arguments", - ) - .into()); - } - let program = args.fs_program.clone().or_else(|| { - (!program_from_tar) - .then(|| { - runner_args - .as_ref()? - .program_and_arguments - .first() - .map(PathBuf::from) - }) - .flatten() - }); - let rewrite_syscalls = args.fs_rewrite_syscalls - || runner_args - .as_ref() - .is_some_and(|args| args.rewrite_syscalls); - let mut entries = Vec::new(); - if let Some(program) = program.as_deref() { + if let Some(program) = args.fs_program.as_deref() { let program = std::path::absolute(program)?; let ancestors: Vec<_> = program.ancestors().skip(1).collect(); let mut previous_user = 0; @@ -165,7 +111,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo previous_user = metadata.st_uid(); } let mut program_data = std::fs::read(&program)?; - if rewrite_syscalls { + if args.fs_rewrite_syscalls { program_data = litebox_syscall_rewriter::hook_syscalls_in_elf_with_options( &program_data, None, @@ -199,7 +145,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo } entries.push(("/registry".to_owned(), writable_directory(UserInfo::ROOT))); - let tar_data = match initial_files.as_deref() { + let tar_data = match args.fs_initial_files.as_deref() { Some(path) => { if path.extension().and_then(|extension| extension.to_str()) != Some("tar") { return Err(IoError::new( @@ -244,59 +190,6 @@ fn guest_owner(previous_user: u32, user: u32) -> UserInfo { } } -fn inferred_linux_runner_args( - args: &super::CliArgs, -) -> Option { - if runner_is_windows_on_linux(args) { - return None; - } - litebox_runner_linux_userland::CliArgs::try_parse_from( - std::iter::once(OsString::from("litebox-runner-linux-userland")) - .chain(std::iter::once(OsString::from("--unstable"))) - .chain(args.runner_arguments.iter().cloned()), - ) - .ok() -} - -fn runner_is_windows_on_linux(args: &super::CliArgs) -> bool { - args.runner - .as_deref() - .and_then(Path::file_name) - .and_then(|name| name.to_str()) - .is_some_and(|name| name.contains("windows_on_linux")) -} - -fn windows_runner_initial_files(arguments: &[OsString]) -> Option { - let mut arguments = arguments.iter(); - while let Some(argument) = arguments.next() { - if argument == "--initial-files" { - return arguments.next().map(PathBuf::from); - } - if let Some(path) = argument - .to_str() - .and_then(|argument| argument.strip_prefix("--initial-files=")) - { - return Some(PathBuf::from(path)); - } - let argument = argument.to_str()?; - let option = argument - .split_once('=') - .map_or(argument, |(option, _)| option); - match option { - "--env" | "--broker-control-channel" => { - if !argument.contains('=') { - arguments.next()?; - } - } - "-Z" | "--unstable" | "--forward-env" => {} - "--" => return None, - argument if argument.starts_with('-') => return None, - _ => return None, - } - } - None -} - fn path_to_string(path: &Path) -> Result { path.to_str() .map(str::to_owned) diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 96f7a313bc..adefed6212 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -122,7 +122,7 @@ struct CliArgs { /// Declare that rewritten AArch64 binaries use x18 virtualization. #[arg(long)] fs_virtualize_x18: bool, - /// Arguments to pass to the local runner. + /// 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, } diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index 73465e5625..b2bd227259 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -18,7 +18,7 @@ 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::TarRo; +use litebox_broker_core::fs::tar_ro::{EMPTY_TAR_FILE, TarRo}; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; @@ -61,34 +61,19 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo ) .into()); } - let runner_args = litebox_runner_windows_userland::CliArgs::try_parse_from( - std::iter::once(OsString::from("litebox-runner-windows-userland")) - .chain(std::iter::once(OsString::from("--unstable"))) - .chain(args.runner_arguments.iter().cloned()), - ) - .ok(); - let initial_files = args - .fs_initial_files - .clone() - .or_else(|| runner_args.and_then(|args| args.initial_files)) - .ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Windows broker requires --fs-initial-files or runner --initial-files", - ) - })?; - if initial_files - .extension() - .and_then(|extension| extension.to_str()) - != Some("tar") - { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("expected a .tar file, found {}", initial_files.display()), - ) - .into()); - } - let tar_data = std::borrow::Cow::Owned(std::fs::read(initial_files)?); + let tar_data = match args.fs_initial_files.as_deref() { + Some(path) => { + if path.extension().and_then(|extension| extension.to_str()) != Some("tar") { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("expected a .tar file, found {}", path.display()), + ) + .into()); + } + std::borrow::Cow::Owned(std::fs::read(path)?) + } + None => std::borrow::Cow::Borrowed(EMPTY_TAR_FILE), + }; let mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; let in_mem = InMem::::new_initialized([ ( diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 81a278348d..50dd990f8b 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -45,8 +45,6 @@ fn run_parent_test() { let test_executable = std::env::current_exe().unwrap(); let mut event_command = Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")); event_command - .arg("--fs-program") - .arg(&test_executable) .arg("--runner") .arg(&test_executable) .arg(RUNNER_ARGUMENT); @@ -71,8 +69,6 @@ fn run_parent_test() { }); let mut network_command = Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")); network_command - .arg("--fs-program") - .arg(&test_executable) .arg("--allow-tcp-destination") .arg(format!("{gateway}/32:{tcp_port}")) .arg("--allow-udp-destination") diff --git a/litebox_runner_linux_on_windows_userland/tests/loader.rs b/litebox_runner_linux_on_windows_userland/tests/loader.rs index 16dbb15988..ff04c5bd52 100644 --- a/litebox_runner_linux_on_windows_userland/tests/loader.rs +++ b/litebox_runner_linux_on_windows_userland/tests/loader.rs @@ -246,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) @@ -369,7 +371,12 @@ fn run_dynamic_linked_prog_with_rewriter( args.extend_from_slice(cmd_args); let (broker, runner) = build_windows_broker(); let mut command = std::process::Command::new(broker); - command.arg("--runner").arg(runner).args(&args); + 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/tests/common/runner.rs b/litebox_runner_linux_userland/tests/common/runner.rs index b497008f7e..f75732845e 100644 --- a/litebox_runner_linux_userland/tests/common/runner.rs +++ b/litebox_runner_linux_userland/tests/common/runner.rs @@ -190,7 +190,7 @@ impl Runner { self.command .arg("--initial-files") - .arg(tar_file) + .arg(&tar_file) .arg(&self.cmd_path) .args(&self.cmd_args); @@ -221,6 +221,7 @@ impl Runner { 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 { diff --git a/litebox_runner_linux_userland/tests/rewritten_guests.rs b/litebox_runner_linux_userland/tests/rewritten_guests.rs index 4953435694..9bcca0cc2e 100644 --- a/litebox_runner_linux_userland/tests/rewritten_guests.rs +++ b/litebox_runner_linux_userland/tests/rewritten_guests.rs @@ -29,6 +29,10 @@ fn run_rewritten_target(target: &std::path::Path, virtualize_x18: bool) -> std:: broker_path.display() ); let mut command = std::process::Command::new(broker_path); + command + .arg("--fs-program") + .arg(target) + .arg("--fs-rewrite-syscalls"); if virtualize_x18 { command.arg("--fs-virtualize-x18"); } diff --git a/litebox_runner_windows_userland/tests/run.rs b/litebox_runner_windows_userland/tests/run.rs index fba7099469..cc39bf7d2d 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 } From aaf09f986025a55e03c57d1d8d709df14c846b49 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 15:25:59 -0700 Subject: [PATCH 28/34] Add Windows broker platform crate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- Cargo.lock | 10 ++++++- Cargo.toml | 2 ++ .../Cargo.toml | 17 ++++++++++++ .../src/lib.rs | 14 ++++++++++ .../src/sync.rs | 27 ++++++++++--------- litebox_broker_userland/Cargo.toml | 6 ++--- litebox_broker_userland/src/main.rs | 2 -- litebox_broker_userland/src/windows.rs | 12 ++++----- 8 files changed, 65 insertions(+), 25 deletions(-) create mode 100644 litebox_broker_platform_windows_userland/Cargo.toml create mode 100644 litebox_broker_platform_windows_userland/src/lib.rs rename {litebox_broker_userland => litebox_broker_platform_windows_userland}/src/sync.rs (77%) diff --git a/Cargo.lock b/Cargo.lock index 971cda4eae..9ec74add5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1522,6 +1522,14 @@ 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" @@ -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", @@ -1577,7 +1586,6 @@ dependencies = [ "litebox_runner_windows_userland", "litebox_syscall_rewriter", "tempfile", - "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8ad47a0e94..c51df626a9 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/litebox_broker_platform_windows_userland/Cargo.toml b/litebox_broker_platform_windows_userland/Cargo.toml new file mode 100644 index 0000000000..64325a6a56 --- /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 0000000000..aefeaad288 --- /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_userland/src/sync.rs b/litebox_broker_platform_windows_userland/src/sync.rs similarity index 77% rename from litebox_broker_userland/src/sync.rs rename to litebox_broker_platform_windows_userland/src/sync.rs index b4c51c5d3b..95d52219a1 100644 --- a/litebox_broker_userland/src/sync.rs +++ b/litebox_broker_platform_windows_userland/src/sync.rs @@ -1,26 +1,30 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -#![cfg(all(windows, target_arch = "x86_64"))] +//! 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, RawMutexProvider, UnblockedOrTimedOut}; +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(super) struct WindowsSyncPrimitivesProvider; +pub struct WindowsSyncPrimitivesProvider; impl RawMutexProvider for WindowsSyncPrimitivesProvider { type RawMutex = WindowsRawMutex; } -pub(super) struct WindowsRawMutex { +/// Raw blocking mutex used by the Windows-userland broker. +pub struct WindowsRawMutex { state: AtomicU32, } @@ -35,7 +39,7 @@ impl WindowsRawMutex { &self, expected: u32, timeout: Option, - ) -> Result { + ) -> UnblockedOrTimedOut { let timeout_ms = timeout.map_or(INFINITE, |timeout| { u32::try_from(timeout.as_millis().min(u128::from(INFINITE - 1))).unwrap() }); @@ -49,18 +53,18 @@ impl WindowsRawMutex { ) != 0 }; if unblocked { - Ok(UnblockedOrTimedOut::Unblocked) + UnblockedOrTimedOut::Unblocked } else { // SAFETY: GetLastError has no preconditions. match unsafe { GetLastError() } { - ERROR_TIMEOUT => Ok(UnblockedOrTimedOut::TimedOut), + ERROR_TIMEOUT => UnblockedOrTimedOut::TimedOut, error => panic!("WaitOnAddress failed with error {error}"), } } } } -impl RawMutex for WindowsRawMutex { +impl RawMutexTrait for WindowsRawMutex { const INIT: Self = Self::new(); fn underlying_atomic(&self) -> &AtomicU32 { @@ -87,9 +91,8 @@ impl RawMutex for WindowsRawMutex { fn block(&self, expected: u32) -> Result<(), ImmediatelyWokenUp> { match self.block_or_maybe_timeout(expected, None) { - Ok(UnblockedOrTimedOut::Unblocked) => Ok(()), - Ok(UnblockedOrTimedOut::TimedOut) => unreachable!(), - Err(error) => Err(error), + UnblockedOrTimedOut::Unblocked => Ok(()), + UnblockedOrTimedOut::TimedOut => unreachable!(), } } @@ -98,6 +101,6 @@ impl RawMutex for WindowsRawMutex { expected: u32, timeout: Duration, ) -> Result { - self.block_or_maybe_timeout(expected, Some(timeout)) + Ok(self.block_or_maybe_timeout(expected, Some(timeout))) } } diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index cba76a62e4..3837867823 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -21,17 +21,15 @@ litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0. 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" } -windows-sys = { version = "0.60.2", features = [ - "Win32_Foundation", - "Win32_System_Threading", -] } [features] 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"] diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index adefed6212..a6a84c88a2 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -21,8 +21,6 @@ use litebox_broker_protocol::socket::{Ipv4Address, Port}; #[cfg(target_os = "linux")] mod linux; #[cfg(all(windows, target_arch = "x86_64"))] -mod sync; -#[cfg(all(windows, target_arch = "x86_64"))] mod windows; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index b2bd227259..fae0ad53bb 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -20,6 +20,7 @@ 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::{BrokerCore, ObjectRights, PolicyEngine}; +use litebox_broker_platform_windows_userland::WindowsSyncPrimitivesProvider; use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport_windows_userland::named_pipe::{ @@ -75,7 +76,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo None => std::borrow::Cow::Borrowed(EMPTY_TAR_FILE), }; let mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; - let in_mem = InMem::::new_initialized([ + let in_mem = InMem::::new_initialized([ ( "/tmp", InitialNode::Directory { @@ -93,7 +94,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo ]); let backend = Composer::builder() .mount_nestable("/", |allocators| { - Overlay::::new( + Overlay::::new( in_mem, TarRo::new(tar_data, allocators.next()), allocators.next(), @@ -102,10 +103,9 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo .mount("/dev", litebox_broker_core::fs::devices::Devices::new) .build() .map_err(|_| std::io::Error::other("failed to construct broker file service"))?; - Ok(Arc::new(Resolver::< - super::sync::WindowsSyncPrimitivesProvider, - _, - >::new(backend))) + Ok(Arc::new(Resolver::::new( + backend, + ))) } fn run_runner_in_process( From f9bb88f10ba5bcc49fef11b54ff1c86871aa9564 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 15:46:45 -0700 Subject: [PATCH 29/34] Allow Windows broker platform to use std Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3799535b2..9f8e1e0738 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -319,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. # @@ -388,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' \ From 187417d867a5a1c45dc7eade33f571f5aa1675b4 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 16:04:13 -0700 Subject: [PATCH 30/34] Share broker filesystem construction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_userland/src/fs.rs | 65 ++++++++++++++++++++++++++ litebox_broker_userland/src/linux.rs | 52 ++------------------- litebox_broker_userland/src/main.rs | 1 + litebox_broker_userland/src/windows.rs | 53 ++------------------- 4 files changed, 72 insertions(+), 99 deletions(-) create mode 100644 litebox_broker_userland/src/fs.rs diff --git a/litebox_broker_userland/src/fs.rs b/litebox_broker_userland/src/fs.rs new file mode 100644 index 0000000000..a65a9560bf --- /dev/null +++ b/litebox_broker_userland/src/fs.rs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Broker-owned file service construction shared by userland platforms. + +use std::borrow::Cow; +use std::io::{Error as IoError, ErrorKind, Result as IoResult}; +use std::path::Path; +use std::sync::Arc; + +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_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; +use litebox_platform::sync::RawSyncPrimitivesProvider; + +pub(super) fn create_file_service( + mut entries: Vec<(String, InitialNode)>, + initial_files: Option<&Path>, +) -> IoResult> +where + Platform: RawSyncPrimitivesProvider, +{ + let writable_directory = |owner| InitialNode::Directory { + mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, + owner, + }; + if let Some((_, InitialNode::Directory { mode, .. })) = + entries.iter_mut().find(|(path, _)| path == "/tmp") + { + *mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; + } else { + entries.push(("/tmp".to_owned(), writable_directory(UserInfo::ROOT))); + } + entries.push(("/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))) +} diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index 005befbf03..932843779a 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -16,11 +16,7 @@ use std::time::{Duration, Instant}; use clap::Parser as _; 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::fs::in_mem::InitialNode; use litebox_broker_core::socket::HOST_GATEWAY_IPV4_ADDRESS; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; @@ -132,51 +128,9 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo )); } - let writable_directory = |owner| InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner, - }; - if let Some((_, InitialNode::Directory { mode, .. })) = - entries.iter_mut().find(|(path, _)| path == "/tmp") - { - *mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; - } else { - entries.push(("/tmp".to_owned(), writable_directory(UserInfo::ROOT))); - } - entries.push(("/registry".to_owned(), writable_directory(UserInfo::ROOT))); - - let tar_data = match args.fs_initial_files.as_deref() { - 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()), - ) - .into()); - } - std::borrow::Cow::Owned(std::fs::read(path)?) - } - None => std::borrow::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::< + Ok(super::fs::create_file_service::< litebox_broker_platform_linux_userland::LinuxSyncPrimitivesProvider, - _, - >::new(backend))) + >(entries, args.fs_initial_files.as_deref())?) } fn guest_owner(previous_user: u32, user: u32) -> UserInfo { diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index a6a84c88a2..7c8e5222a5 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -18,6 +18,7 @@ use litebox_broker_core::{ }; use litebox_broker_protocol::socket::{Ipv4Address, Port}; +mod fs; #[cfg(target_os = "linux")] mod linux; #[cfg(all(windows, target_arch = "x86_64"))] diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index fae0ad53bb..ba6fc707e1 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -14,14 +14,8 @@ use std::time::Instant; use clap::Parser as _; 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::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_platform_windows_userland::WindowsSyncPrimitivesProvider; -use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport_windows_userland::named_pipe::{ WindowsNamedPipeHostSetupChannel, WindowsNamedPipeListener, WindowsNamedPipeStream, @@ -62,50 +56,9 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo ) .into()); } - let tar_data = match args.fs_initial_files.as_deref() { - Some(path) => { - if path.extension().and_then(|extension| extension.to_str()) != Some("tar") { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("expected a .tar file, found {}", path.display()), - ) - .into()); - } - std::borrow::Cow::Owned(std::fs::read(path)?) - } - None => std::borrow::Cow::Borrowed(EMPTY_TAR_FILE), - }; - let mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; - let in_mem = InMem::::new_initialized([ - ( - "/tmp", - InitialNode::Directory { - mode, - owner: UserInfo::ROOT, - }, - ), - ( - "/registry", - InitialNode::Directory { - mode, - owner: UserInfo::ROOT, - }, - ), - ]); - 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(|_| std::io::Error::other("failed to construct broker file service"))?; - Ok(Arc::new(Resolver::::new( - backend, - ))) + Ok(super::fs::create_file_service::< + WindowsSyncPrimitivesProvider, + >(Vec::new(), args.fs_initial_files.as_deref())?) } fn run_runner_in_process( From f840666c3f818478d8644e48253f0d4ef81df9f3 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 16:23:53 -0700 Subject: [PATCH 31/34] Call shared broker filesystem builder directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_userland/src/linux.rs | 16 +++++++------- litebox_broker_userland/src/windows.rs | 29 +++++++++++++------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index 932843779a..03515b79df 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -9,16 +9,15 @@ use std::os::linux::fs::MetadataExt as _; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::Path; use std::process::{Child, ChildStdout, Command, Stdio}; -use std::sync::Arc; use std::sync::mpsc::{RecvTimeoutError, sync_channel}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; use clap::Parser as _; -use litebox_broker_core::fs::FileService; use litebox_broker_core::fs::in_mem::InitialNode; 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::fs::{FileMode as Mode, FileUser as UserInfo}; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; use litebox_broker_transport_linux_userland::memfd::MemfdSharedMemory; @@ -58,7 +57,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 fs = create_file_service(&args)?; + let fs = super::fs::create_file_service::( + host_program_entries(&args)?, + 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)? @@ -88,7 +90,9 @@ pub(super) fn run(mut args: super::CliArgs) -> Result<(), Box> { } } -fn create_file_service(args: &super::CliArgs) -> Result, Box> { +fn host_program_entries( + args: &super::CliArgs, +) -> Result, Box> { let mut entries = Vec::new(); if let Some(program) = args.fs_program.as_deref() { let program = std::path::absolute(program)?; @@ -128,9 +132,7 @@ fn create_file_service(args: &super::CliArgs) -> Result, Bo )); } - Ok(super::fs::create_file_service::< - litebox_broker_platform_linux_userland::LinuxSyncPrimitivesProvider, - >(entries, args.fs_initial_files.as_deref())?) + Ok(entries) } fn guest_owner(previous_user: u32, user: u32) -> UserInfo { diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index ba6fc707e1..6f2e6620f3 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -8,12 +8,10 @@ use std::ffi::OsString; use std::io::Result as IoResult; use std::os::windows::io::AsRawHandle; use std::process::Child; -use std::sync::Arc; use std::thread::JoinHandle; use std::time::Instant; use clap::Parser as _; -use litebox_broker_core::fs::FileService; use litebox_broker_core::{BrokerCore, ObjectRights, PolicyEngine}; use litebox_broker_platform_windows_userland::WindowsSyncPrimitivesProvider; use litebox_broker_protocol::shared_buffer::SHARED_BUFFER_POOL_SIZE; @@ -29,13 +27,17 @@ 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)?, - ), - ) - .with_file_service(create_file_service(&args)?) - .build()?; + let policy = PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()).with_socket_policy( + configured_socket_policy(&args.allow_tcp_destination, &args.allow_udp_destination)?, + ); + validate_fs_options(&args)?; + let fs = super::fs::create_file_service::( + Vec::new(), + 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); @@ -48,17 +50,14 @@ pub(super) fn run(args: super::CliArgs) -> Result<(), Box> { } } -fn create_file_service(args: &super::CliArgs) -> Result, Box> { +fn validate_fs_options(args: &super::CliArgs) -> IoResult<()> { if args.fs_program.is_some() || args.fs_rewrite_syscalls || args.fs_virtualize_x18 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Windows broker file systems require the program in the initial tar archive", - ) - .into()); + )); } - Ok(super::fs::create_file_service::< - WindowsSyncPrimitivesProvider, - >(Vec::new(), args.fs_initial_files.as_deref())?) + Ok(()) } fn run_runner_in_process( From c56ff8393f711bdb57d18c9b6ae30ba97cb4c351 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 16:31:04 -0700 Subject: [PATCH 32/34] Gate host filesystem options to Linux Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_userland/src/main.rs | 6 +++++- litebox_broker_userland/src/windows.rs | 11 ----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 7c8e5222a5..f217bcc5dc 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -108,17 +108,21 @@ struct CliArgs { )] runner: Option, /// Host program to populate into the broker-owned file system. + #[cfg(target_os = "linux")] #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::ExecutablePath)] fs_program: Option, /// Tar archive to mount as the broker-owned initial file system. /// - /// When `--fs-program` is omitted, the guest program is expected to be in this archive. + /// The guest program is expected to be in this archive unless supplied + /// through the Linux-only `--fs-program` option. #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] fs_initial_files: Option, /// Rewrite the host program before populating the broker-owned file system. + #[cfg(target_os = "linux")] #[arg(long, requires = "fs_program")] fs_rewrite_syscalls: bool, /// Declare that rewritten AArch64 binaries use x18 virtualization. + #[cfg(target_os = "linux")] #[arg(long)] fs_virtualize_x18: bool, /// Opaque arguments to pass to the local runner without interpretation. diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index 6f2e6620f3..c64810ed2d 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -30,7 +30,6 @@ pub(super) fn run(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)?, ); - validate_fs_options(&args)?; let fs = super::fs::create_file_service::( Vec::new(), args.fs_initial_files.as_deref(), @@ -50,16 +49,6 @@ pub(super) fn run(args: super::CliArgs) -> Result<(), Box> { } } -fn validate_fs_options(args: &super::CliArgs) -> IoResult<()> { - if args.fs_program.is_some() || args.fs_rewrite_syscalls || args.fs_virtualize_x18 { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Windows broker file systems require the program in the initial tar archive", - )); - } - Ok(()) -} - fn run_runner_in_process( args: &super::CliArgs, control_pipe: &std::ffi::OsStr, From 980d0c9aa293e67f95392638fafe7f7d9d97f5ff Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 16:48:57 -0700 Subject: [PATCH 33/34] Move syscall rewriting to Linux runner Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- Cargo.lock | 1 - litebox_broker_userland/Cargo.toml | 1 - litebox_broker_userland/src/fs.rs | 13 +- litebox_broker_userland/src/linux.rs | 69 ------ litebox_broker_userland/src/main.rs | 15 -- litebox_broker_userland/src/windows.rs | 1 - litebox_runner_linux_userland/src/lib.rs | 220 +++++++++++++++++- .../tests/rewritten_guests.rs | 18 +- 8 files changed, 220 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ec74add5e..9606139a1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,7 +1584,6 @@ dependencies = [ "litebox_platform_linux_userland", "litebox_runner_linux_userland", "litebox_runner_windows_userland", - "litebox_syscall_rewriter", "tempfile", ] diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index 3837867823..5a3cac7119 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -17,7 +17,6 @@ litebox_broker_platform_linux_userland = { path = "../litebox_broker_platform_li litebox_broker_transport_linux_userland = { path = "../litebox_broker_transport_linux_userland", version = "0.1.0" } litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland", version = "0.1.0" } litebox_runner_linux_userland = { path = "../litebox_runner_linux_userland", version = "0.1.0" } -litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0.1.0" } tempfile = { version = "3", default-features = false } [target.'cfg(all(windows, target_arch = "x86_64"))'.dependencies] diff --git a/litebox_broker_userland/src/fs.rs b/litebox_broker_userland/src/fs.rs index a65a9560bf..af1cf0f50d 100644 --- a/litebox_broker_userland/src/fs.rs +++ b/litebox_broker_userland/src/fs.rs @@ -18,7 +18,6 @@ use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; use litebox_platform::sync::RawSyncPrimitivesProvider; pub(super) fn create_file_service( - mut entries: Vec<(String, InitialNode)>, initial_files: Option<&Path>, ) -> IoResult> where @@ -28,14 +27,10 @@ where mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, owner, }; - if let Some((_, InitialNode::Directory { mode, .. })) = - entries.iter_mut().find(|(path, _)| path == "/tmp") - { - *mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; - } else { - entries.push(("/tmp".to_owned(), writable_directory(UserInfo::ROOT))); - } - entries.push(("/registry".to_owned(), writable_directory(UserInfo::ROOT))); + 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) => { diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index 03515b79df..1df98e132f 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -5,20 +5,16 @@ use std::error::Error; use std::ffi::OsString; use std::io::{BufRead, BufReader, Error as IoError, ErrorKind, Result as IoResult}; use std::net::{Ipv4Addr, SocketAddrV4}; -use std::os::linux::fs::MetadataExt as _; use std::os::unix::net::{UnixListener, UnixStream}; -use std::path::Path; use std::process::{Child, ChildStdout, Command, Stdio}; use std::sync::mpsc::{RecvTimeoutError, sync_channel}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; use clap::Parser as _; -use litebox_broker_core::fs::in_mem::InitialNode; 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::fs::{FileMode as Mode, FileUser as UserInfo}; 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::{ @@ -29,8 +25,6 @@ use litebox_broker_userland::builder::BrokerCoreBuilder; use super::{SETUP_TIMEOUT, configured_socket_policy}; const PROXY_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); -const DEFAULT_GUEST_UID: u16 = 1000; -const DEFAULT_GUEST_GID: u16 = 1000; pub(super) fn run(mut args: super::CliArgs) -> Result<(), Box> { let proxy = if args.allow_host.is_empty() { @@ -58,7 +52,6 @@ pub(super) fn run(mut args: super::CliArgs) -> Result<(), Box> { configured_socket_policy(&args.allow_tcp_destination, &args.allow_udp_destination)?, ); let fs = super::fs::create_file_service::( - host_program_entries(&args)?, args.fs_initial_files.as_deref(), )?; let build_broker = || BrokerCoreBuilder::new(policy).with_file_service(fs).build(); @@ -90,68 +83,6 @@ pub(super) fn run(mut args: super::CliArgs) -> Result<(), Box> { } } -fn host_program_entries( - args: &super::CliArgs, -) -> Result, Box> { - let mut entries = Vec::new(); - if let Some(program) = args.fs_program.as_deref() { - let program = std::path::absolute(program)?; - let ancestors: Vec<_> = program.ancestors().skip(1).collect(); - let mut previous_user = 0; - for path in ancestors.into_iter().rev().skip(1) { - let metadata = path.metadata()?; - let owner = guest_owner(previous_user, metadata.st_uid()); - entries.push(( - path_to_string(path)?, - InitialNode::Directory { - mode: Mode::from_u32_bits_truncate(metadata.st_mode()), - owner, - }, - )); - previous_user = metadata.st_uid(); - } - let mut program_data = std::fs::read(&program)?; - if args.fs_rewrite_syscalls { - program_data = litebox_syscall_rewriter::hook_syscalls_in_elf_with_options( - &program_data, - None, - litebox_syscall_rewriter::RewriteOptions::new( - litebox_syscall_rewriter::TargetHost::Linux, - args.fs_virtualize_x18 || cfg!(feature = "aarch64_virtualize_x18"), - ), - )?; - } - let metadata = program.metadata()?; - entries.push(( - path_to_string(&program)?, - InitialNode::File { - mode: Mode::from_u32_bits_truncate(metadata.st_mode()), - owner: guest_owner(previous_user, metadata.st_uid()), - data: program_data.into(), - }, - )); - } - - Ok(entries) -} - -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(|| IoError::new(ErrorKind::InvalidData, "file path is not UTF-8")) -} - struct ManagedEgressProxy { child: Child, port: u16, diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index f217bcc5dc..608fb51ee8 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -107,24 +107,9 @@ struct CliArgs { conflicts_with = "in_process_runner" )] runner: Option, - /// Host program to populate into the broker-owned file system. - #[cfg(target_os = "linux")] - #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::ExecutablePath)] - fs_program: Option, /// Tar archive to mount as the broker-owned initial file system. - /// - /// The guest program is expected to be in this archive unless supplied - /// through the Linux-only `--fs-program` option. #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] fs_initial_files: Option, - /// Rewrite the host program before populating the broker-owned file system. - #[cfg(target_os = "linux")] - #[arg(long, requires = "fs_program")] - fs_rewrite_syscalls: bool, - /// Declare that rewritten AArch64 binaries use x18 virtualization. - #[cfg(target_os = "linux")] - #[arg(long)] - fs_virtualize_x18: bool, /// 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, diff --git a/litebox_broker_userland/src/windows.rs b/litebox_broker_userland/src/windows.rs index c64810ed2d..84e7989bb5 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -31,7 +31,6 @@ pub(super) fn run(args: super::CliArgs) -> Result<(), Box> { configured_socket_policy(&args.allow_tcp_destination, &args.allow_udp_destination)?, ); let fs = super::fs::create_file_service::( - Vec::new(), args.fs_initial_files.as_deref(), )?; let broker = BrokerCoreBuilder::new(policy) diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 1e55daf341..bfb1658f2e 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -4,11 +4,13 @@ use anyhow::{Context as _, Result, anyhow}; use clap::Parser; use litebox_platform_linux_userland::LinuxUserland as Platform; +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. @@ -137,6 +139,20 @@ pub fn run(cli_args: CliArgs) -> Result { ); } + 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])).with_context(|| { + format!( + "could not resolve program path {}", + cli_args.program_and_arguments[0] + ) + })? + }; + 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(); @@ -167,6 +183,9 @@ pub fn run(cli_args: CliArgs) -> Result { ) })?; 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() @@ -181,14 +200,6 @@ pub fn run(cli_args: CliArgs) -> Result { gid: u32::from(DEFAULT_GUEST_GID), egid: u32::from(DEFAULT_GUEST_GID), }; - // 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", @@ -248,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/rewritten_guests.rs b/litebox_runner_linux_userland/tests/rewritten_guests.rs index 9bcca0cc2e..e11f9e72fa 100644 --- a/litebox_runner_linux_userland/tests/rewritten_guests.rs +++ b/litebox_runner_linux_userland/tests/rewritten_guests.rs @@ -12,10 +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, false) + run_rewritten_target(&target) } -fn run_rewritten_target(target: &std::path::Path, virtualize_x18: bool) -> std::process::Output { +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()); @@ -29,13 +29,6 @@ fn run_rewritten_target(target: &std::path::Path, virtualize_x18: bool) -> std:: broker_path.display() ); let mut command = std::process::Command::new(broker_path); - command - .arg("--fs-program") - .arg(target) - .arg("--fs-rewrite-syscalls"); - if virtualize_x18 { - command.arg("--fs-virtualize-x18"); - } command.arg("--runner").arg(&binary_path); command }; @@ -59,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); @@ -165,7 +159,7 @@ fn test_x18_virtualization() { true, true, ); - let output = run_rewritten_target(&target, true); + let output = run_rewritten_target(&target); assert!( output.status.success(), "x18 fixture failed ({}): {}", From 627c00bdfc011b5443f53012df83373ad15d7ce3 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 11 Sep 2026 17:00:46 -0700 Subject: [PATCH 34/34] Inline broker file service construction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_userland/src/fs.rs | 60 -------------------------- litebox_broker_userland/src/linux.rs | 2 +- litebox_broker_userland/src/main.rs | 53 ++++++++++++++++++++++- litebox_broker_userland/src/windows.rs | 2 +- 4 files changed, 53 insertions(+), 64 deletions(-) delete mode 100644 litebox_broker_userland/src/fs.rs diff --git a/litebox_broker_userland/src/fs.rs b/litebox_broker_userland/src/fs.rs deleted file mode 100644 index af1cf0f50d..0000000000 --- a/litebox_broker_userland/src/fs.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Broker-owned file service construction shared by userland platforms. - -use std::borrow::Cow; -use std::io::{Error as IoError, ErrorKind, Result as IoResult}; -use std::path::Path; -use std::sync::Arc; - -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_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; -use litebox_platform::sync::RawSyncPrimitivesProvider; - -pub(super) 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))) -} diff --git a/litebox_broker_userland/src/linux.rs b/litebox_broker_userland/src/linux.rs index 1df98e132f..b96bdb6cf3 100644 --- a/litebox_broker_userland/src/linux.rs +++ b/litebox_broker_userland/src/linux.rs @@ -51,7 +51,7 @@ 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 fs = super::fs::create_file_service::( + let fs = super::create_file_service::( args.fs_initial_files.as_deref(), )?; let build_broker = || BrokerCoreBuilder::new(policy).with_file_service(fs).build(); diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 608fb51ee8..ac64645a57 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -1,24 +1,33 @@ // 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; -mod fs; #[cfg(target_os = "linux")] mod linux; #[cfg(all(windows, target_arch = "x86_64"))] @@ -115,6 +124,46 @@ struct CliArgs { 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 84e7989bb5..cf8eb18a2b 100644 --- a/litebox_broker_userland/src/windows.rs +++ b/litebox_broker_userland/src/windows.rs @@ -30,7 +30,7 @@ pub(super) fn run(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 fs = super::fs::create_file_service::( + let fs = super::create_file_service::( args.fs_initial_files.as_deref(), )?; let broker = BrokerCoreBuilder::new(policy)