From 24ae796f838da07796baa613f42e1372cd5ac62e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sat, 12 Sep 2026 17:42:48 -0700 Subject: [PATCH 1/8] Migrate Linux file clients to the broker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- .github/workflows/ci.yml | 9 +- Cargo.lock | 6 +- dev_bench/src/main.rs | 26 +- dev_bench/unixbench/README.md | 5 +- dev_bench/unixbench/run_unixbench.py | 31 +- litebox/src/pipes.rs | 89 ++-- litebox_broker_userland/Cargo.toml | 1 + litebox_broker_userland/src/main.rs | 1 + .../tests/userland_broker.rs | 5 +- litebox_packager/src/lib.rs | 2 +- litebox_platform_linux_userland/src/lib.rs | 5 +- .../src/lib.rs | 81 +--- .../tests/common/mod.rs | 97 ---- .../tests/loader.rs | 72 +-- litebox_runner_linux_userland/Cargo.toml | 9 +- litebox_runner_linux_userland/src/lib.rs | 310 ++---------- .../tests/common/mod.rs | 1 + .../tests/common/runner.rs | 263 ++++++++++ litebox_runner_linux_userland/tests/loader.rs | 152 +----- .../tests/rewritten_guests.rs | 113 +---- litebox_runner_linux_userland/tests/run.rs | 450 +++++++----------- litebox_runner_snp/src/main.rs | 118 +---- litebox_shim_linux/Cargo.toml | 6 +- litebox_shim_linux/src/lib.rs | 151 +++--- litebox_shim_linux/src/loader/elf.rs | 196 +------- litebox_shim_linux/src/stdio.rs | 26 +- litebox_shim_linux/src/syscalls/epoll.rs | 24 +- litebox_shim_linux/src/syscalls/eventfd.rs | 17 +- litebox_shim_linux/src/syscalls/file.rs | 422 ++++++++-------- litebox_shim_linux/src/syscalls/mm.rs | 228 +++------ litebox_shim_linux/src/syscalls/mod.rs | 14 +- litebox_shim_linux/src/syscalls/net.rs | 125 +++-- litebox_shim_linux/src/syscalls/pipe.rs | 6 +- litebox_shim_linux/src/syscalls/process.rs | 11 +- .../src/syscalls/test_broker.rs | 74 +++ litebox_shim_linux/src/syscalls/tests.rs | 403 +++------------- litebox_shim_linux/src/syscalls/unix.rs | 25 +- 37 files changed, 1285 insertions(+), 2289 deletions(-) delete mode 100644 litebox_runner_linux_on_windows_userland/tests/common/mod.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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9143bbea97..9e95d9fd7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -248,7 +248,6 @@ jobs: -p litebox_broker_local -p litebox_broker_host -p litebox_broker_transport_windows_userland - -p litebox_broker_platform_windows_userland -p litebox_platform_windows_userland -p litebox_shim_linux -p litebox_shim_windows @@ -375,16 +374,16 @@ jobs: # access since it loads files and runs LiteBox on a hosted platform. # # - `litebox_runner_linux_on_windows_userland` is allowed to have `std` - # access since it needs to actually access the file-system, pull in - # relevant files, and then actually trigger LiteBox itself. + # access since it is a hosted executable that connects to the broker + # and launches the LiteBox guest. # # - `litebox_runner_windows_on_linux_userland` is allowed to have `std` # access since it needs to actually access the file-system, pull in # relevant files, and then actually trigger LiteBox itself. # # - `litebox_runner_linux_userland` is allowed to have `std` access - # since it needs to actually access the file-system, pull in - # relevant files, and then actually trigger LiteBox itself. + # since it is a hosted executable that connects to the broker and + # launches the LiteBox guest. # # - `litebox_runner_windows_userland` is allowed to have `std` access # since it needs to actually access the file-system, pull in diff --git a/Cargo.lock b/Cargo.lock index 787a172a85..df449d5580 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1822,9 +1822,7 @@ dependencies = [ "litebox_common_linux", "litebox_platform_linux_userland", "litebox_shim_linux", - "litebox_syscall_rewriter", "litebox_util_log", - "memmap2", "sha2", "tracing-subscriber", "walkdir", @@ -1953,9 +1951,9 @@ dependencies = [ "libc", "litebox", "litebox_broker_core", + "litebox_broker_host", "litebox_broker_local", "litebox_broker_protocol", - "litebox_broker_transport", "litebox_common_linux", "litebox_platform", "litebox_platform_linux_userland", @@ -1966,9 +1964,7 @@ dependencies = [ "object", "once_cell", "ringbuf", - "spin 0.9.8", "syscalls", - "tempfile", "thiserror", "zerocopy", ] diff --git a/dev_bench/src/main.rs b/dev_bench/src/main.rs index c23813401b..a6e9ddce5b 100644 --- a/dev_bench/src/main.rs +++ b/dev_bench/src/main.rs @@ -450,9 +450,16 @@ fn run_rewritten_hello_static(ctx: BenchCtx<'_>) -> Result<()> { is_init, lock_tracing, } = ctx; + let tar_file = sh.current_dir().join("hello_static_rootfs.tar"); if is_init { rewriter_hello_static(ctx.with_init(true))?; rewriter_hello_static(ctx.with_init(false))?; + sh.remove_path(&tar_file)?; + cmd!( + sh, + "tar --format=ustar -cf {tar_file} hello_static_rewritten" + ) + .run()?; let features: &[&str] = if lock_tracing { &["--features", "lock_tracing"] } else { @@ -463,11 +470,15 @@ fn run_rewritten_hello_static(ctx: BenchCtx<'_>) -> Result<()> { "cargo build -p litebox_runner_linux_userland --release {features...}" ) .run()?; + cmd!(sh, "cargo build -p litebox_broker_userland --release").run()?; } else { + let broker = project_root.join("target/release/litebox-broker-userland"); + let runner = project_root.join("target/release/litebox_runner_linux_userland"); cmd!( sh, - "{project_root}/target/release/litebox_runner_linux_userland --unstable hello_static_rewritten" - ).run()?; + "{broker} --fs-initial-files {tar_file} --runner {runner} /hello_static_rewritten" + ) + .run()?; } Ok(()) } @@ -586,9 +597,13 @@ fn run_rewritten_node(ctx: BenchCtx<'_>) -> Result<()> { rewriter_node(ctx.with_init(false))?; let tar_base_dir = sh.current_dir().join("node_tar_base"); + sh.create_dir(&tar_base_dir)?; sh.write_file(tar_base_dir.join("hello_world.js"), HELLO_WORLD_JS)?; + std::fs::copy( + sh.current_dir().join("node_rewritten"), + tar_base_dir.join("node_rewritten"), + )?; let libs = find_dependencies(sh, "node")?; - sh.create_dir(&tar_base_dir)?; for lib in libs { let dest_path = tar_base_dir .join(lib.strip_prefix("/").unwrap_or_else(|_| { @@ -618,11 +633,14 @@ fn run_rewritten_node(ctx: BenchCtx<'_>) -> Result<()> { "cargo build -p litebox_runner_linux_userland {release...} {features...}" ) .run()?; + cmd!(sh, "cargo build -p litebox_broker_userland {release...}").run()?; } else { let mode = if release_mode { "release" } else { "debug" }; + let broker = project_root.join(format!("target/{mode}/litebox-broker-userland")); + let runner = project_root.join(format!("target/{mode}/litebox_runner_linux_userland")); cmd!( sh, - "{project_root}/target/{mode}/litebox_runner_linux_userland --unstable --env HOME=/ --initial-files {tar_file} node_rewritten hello_world.js" + "{broker} --fs-initial-files {tar_file} --runner {runner} --env HOME=/ /node_rewritten hello_world.js" ).run()?; } Ok(()) diff --git a/dev_bench/unixbench/README.md b/dev_bench/unixbench/README.md index f5b89d0a2a..412c816062 100644 --- a/dev_bench/unixbench/README.md +++ b/dev_bench/unixbench/README.md @@ -7,11 +7,11 @@ Run [byte-unixbench](https://github.com/kdlucas/byte-unixbench) benchmarks nativ - The UnixBench source tree at `byte-unixbench-6.0.0/UnixBench/` (extracted from `v6.0.0.zip`). - `gcc`, `make`, `ldd`, `tar` on the host. - Pre-built LiteBox binaries (`litebox-broker-userland`, - `litebox_runner_linux_userland`, and `litebox_syscall_rewriter`). + `litebox_runner_linux_userland`, and `litebox_packager`). Build LiteBox (from workspace root): ```bash -cargo build --release -p litebox_broker_userland -p litebox_runner_linux_userland -p litebox_syscall_rewriter +cargo build --release -p litebox_broker_userland -p litebox_runner_linux_userland -p litebox_packager ``` ## Quick Start @@ -144,5 +144,4 @@ python run_unixbench.py --mode litebox --windows --prepared-dir ./prepared --run ``` **Key differences from Linux mode:** -- No `--rewrite-syscalls` needed — binaries are already pre-rewritten - No native baseline (Linux binaries can't run natively on Windows) diff --git a/dev_bench/unixbench/run_unixbench.py b/dev_bench/unixbench/run_unixbench.py index 0fe02d66c4..057e6815ca 100644 --- a/dev_bench/unixbench/run_unixbench.py +++ b/dev_bench/unixbench/run_unixbench.py @@ -296,12 +296,11 @@ def prepare_litebox_rootfs( The packager discovers shared-library dependencies via ldd, rewrites all ELF files with the syscall rewriter, and produces a tar suitable for - ``--initial-files``. The rewritten main binary is then extracted from - the tar so it can be passed to the runner as the program to execute. + broker ``--fs-initial-files``. - Returns (tar_path, rewritten_binary_path) or None on failure. + Returns (tar_path, guest_program_path) or None on failure. """ - binary = pgms_dir / bench.binary + binary = (pgms_dir / bench.binary).resolve() if not binary.exists(): print(f" [SKIP] {bench.name}: binary not found at {binary}") return None @@ -323,19 +322,17 @@ def prepare_litebox_rootfs( print(f" Error: packager failed for {bench.name}: {stderr[:500]}") return None - # Extract the rewritten main binary from the tar - rewritten = work_dir / f"{bench.binary}.hooked" - try: - extract_rewritten_binary(tar_path, binary, rewritten) - except RuntimeError as e: - print(f" Error: {e}") - return None - # For execl: add the rewritten binary at /pgms/execl in the tar if bench.name == "execl": + rewritten = work_dir / f"{bench.binary}.hooked" + try: + extract_rewritten_binary(tar_path, binary, rewritten) + except RuntimeError as e: + print(f" Error: {e}") + return None add_execl_to_tar(tar_path, rewritten) - return tar_path, rewritten + return tar_path, binary def _run_litebox_cmd( @@ -433,11 +430,12 @@ def run_litebox( if prepared is None: return None - tar_path, rewritten = prepared + tar_path, guest_program = prepared broker_path = runner_path.with_name("litebox-broker-userland") cmd = [ str(broker_path), + "--fs-initial-files", str(tar_path), "--runner", str(runner_path), "--env", "LD_LIBRARY_PATH=/lib64:/lib32:/lib", "--env", "HOME=/", @@ -447,8 +445,7 @@ def run_litebox( if bench.name == "execl": cmd += ["--env", "UB_BINDIR=/pgms"] - cmd += ["--initial-files", str(tar_path)] - cmd += [str(rewritten)] + cmd += [str(guest_program)] return _run_litebox_cmd(bench, duration, cmd) @@ -491,6 +488,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=/", @@ -500,7 +498,6 @@ def run_litebox_windows( if bench.name == "execl": cmd += ["--env", "UB_BINDIR=/pgms"] - cmd += ["--initial-files", str(tar_path)] cmd += [tar_program_path] return _run_litebox_cmd(bench, duration, cmd) diff --git a/litebox/src/pipes.rs b/litebox/src/pipes.rs index a9581abab9..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}; @@ -28,7 +28,6 @@ use crate::{ polling::{Pollee, TryOpError}, wait::{WaitContext, WaitError}, }, - fs::OFlags, sync::RawSyncPrimitivesProvider, }; @@ -75,7 +74,7 @@ impl Pipes { broker, self.litebox.broker_pollable_registry(), capacity, - OFlags::from(flags), + flags, atomic_slice_guarantee_size, )?; let mut dt = self.litebox.descriptor_table_mut(); @@ -160,7 +159,11 @@ impl Pipes { .ok_or(errors::ClosedError::ClosedFd)? .entry .0; - Ok(Flags::from_oflags_truncate(p.get_status())) + Ok(if p.non_blocking.load(Relaxed) { + Flags::NON_BLOCKING + } else { + Flags::empty() + }) } /// Update the flags set on the pipe at `fd`. @@ -178,7 +181,9 @@ impl Pipes { .ok_or(errors::ClosedError::ClosedFd)? .entry .0; - p.set_status(OFlags::from(mask), on); + if mask.contains(Flags::NON_BLOCKING) { + p.non_blocking.store(on, Relaxed); + } Ok(()) } @@ -218,21 +223,6 @@ bitflags::bitflags! { } } -impl Flags { - fn from_oflags_truncate(oflags: OFlags) -> Self { - let mut flags = Flags::empty(); - flags.set(Flags::NON_BLOCKING, oflags.contains(OFlags::NONBLOCK)); - flags - } -} -impl From for OFlags { - fn from(flags: Flags) -> Self { - let mut oflags = OFlags::empty(); - oflags.set(OFlags::NONBLOCK, flags.contains(Flags::NON_BLOCKING)); - oflags - } -} - pub mod errors { use crate::event::wait::WaitError; @@ -356,7 +346,7 @@ struct BrokerPipeEnd { pollee: Arc>, peer: Weak, endpoint_type: HalfPipeType, - status: AtomicU32, + non_blocking: AtomicBool, } #[expect( @@ -367,7 +357,7 @@ fn new_broker_pipe( broker: Arc, pollable_registry: Arc>, capacity: usize, - flags: OFlags, + flags: Flags, atomic_slice_guarantee_size: Option, ) -> Result<(Arc>, Arc>), errors::CreateError> { let atomic_write_size = atomic_slice_guarantee_size @@ -395,7 +385,7 @@ fn new_broker_pipe( pollee: Arc::new(Pollee::new()), peer: Weak::new(), endpoint_type: HalfPipeType::SenderHalf, - status: AtomicU32::new((flags | OFlags::WRONLY).bits()), + non_blocking: AtomicBool::new(flags.contains(Flags::NON_BLOCKING)), }); let reader = Arc::new_cyclic(|weak_reader| { Arc::get_mut(&mut writer) @@ -408,7 +398,7 @@ fn new_broker_pipe( pollee: Arc::new(Pollee::new()), peer: Arc::downgrade(&writer), endpoint_type: HalfPipeType::ReceiverHalf, - status: AtomicU32::new((flags | OFlags::RDONLY).bits()), + non_blocking: AtomicBool::new(flags.contains(Flags::NON_BLOCKING)), } }); @@ -418,18 +408,6 @@ fn new_broker_pipe( } impl BrokerPipeEnd { - fn get_status(&self) -> OFlags { - OFlags::from_bits(self.status.load(Relaxed)).unwrap() & OFlags::STATUS_FLAGS_MASK - } - - fn set_status(&self, mask: OFlags, on: bool) { - if on { - self.status.fetch_or(mask.bits(), Relaxed); - } else { - self.status.fetch_and(mask.complement().bits(), Relaxed); - } - } - fn read(&self, cx: &WaitContext<'_, Platform>, buf: &mut [u8]) -> Result { let length = buf.len().min(MAX_PIPE_TRANSFER_SIZE as usize); if length == 0 { @@ -440,27 +418,22 @@ impl BrokerPipeEnd .expect("pipe transfer limit must fit in u32"); self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::IN, - || { - let data = self - .broker - .read_pipe(self.handle, request_length) - .map_err(|error| self.broker_request_error(error))?; - if data.len() > length { - return Err(TryOpError::Other(PipeError::Io)); - } - buf[..data.len()].copy_from_slice(&data); - if !data.is_empty() - && let Some(peer) = self.peer.upgrade() - { - peer.pollee.notify_observers(Events::OUT); - } - Ok(data.len()) - }, - ) + .wait(cx, self.non_blocking.load(Relaxed), Events::IN, || { + let data = self + .broker + .read_pipe(self.handle, request_length) + .map_err(|error| self.broker_request_error(error))?; + if data.len() > length { + return Err(TryOpError::Other(PipeError::Io)); + } + buf[..data.len()].copy_from_slice(&data); + if !data.is_empty() + && let Some(peer) = self.peer.upgrade() + { + peer.pollee.notify_observers(Events::OUT); + } + Ok(data.len()) + }) .map_err(PipeError::from) } @@ -468,7 +441,7 @@ impl BrokerPipeEnd if buf.is_empty() { return Ok(0); } - let nonblock = self.get_status().contains(OFlags::NONBLOCK); + let nonblock = self.non_blocking.load(Relaxed); if nonblock { let data = &buf[..buf.len().min(MAX_PIPE_TRANSFER_SIZE as usize)]; return self diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index 7ca6ceaeed..6855ddec83 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -31,6 +31,7 @@ lock_tracing = [ "litebox_broker_platform_windows_userland/lock_tracing", "litebox_runner_linux_userland/lock_tracing", ] +aarch64_virtualize_x18 = ["litebox_runner_linux_userland/aarch64_virtualize_x18"] [[bin]] name = "litebox-broker-userland" diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index c3ec48f419..ac64645a57 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -80,6 +80,7 @@ impl FromStr for AllowedDestination { } #[derive(Parser, Debug)] +#[allow(clippy::struct_excessive_bools)] struct CliArgs { /// Permit HTTP and HTTPS proxy requests to a hostname and destination ports. #[cfg(target_os = "linux")] diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index d839cfb4a5..3c4a438367 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -42,10 +42,11 @@ fn run_parent_test() { // the fake runner finishes its broker requests, it terminates the broker // parent process; this lets the test exercise the long-running broker // without a test-only shutdown path. + let test_executable = std::env::current_exe().unwrap(); let mut event_command = Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")); event_command .arg("--runner") - .arg(std::env::current_exe().unwrap()) + .arg(&test_executable) .arg(RUNNER_ARGUMENT); wait_for_broker(event_command); @@ -73,7 +74,7 @@ fn run_parent_test() { .arg("--allow-udp-destination") .arg(format!("{gateway}/32:{udp_port}")) .arg("--runner") - .arg(std::env::current_exe().unwrap()) + .arg(test_executable) .arg(NETWORK_RUNNER_ARGUMENT) .arg(tcp_port.to_string()) .arg(udp_port.to_string()); diff --git a/litebox_packager/src/lib.rs b/litebox_packager/src/lib.rs index edb99e669a..479e05f4a4 100644 --- a/litebox_packager/src/lib.rs +++ b/litebox_packager/src/lib.rs @@ -16,7 +16,7 @@ use tar::{Builder, Header}; /// /// Discovers shared library dependencies, rewrites all ELF files using the /// syscall rewriter, and produces a .tar suitable for use with -/// `litebox-runner-linux-userland --initial-files`. +/// `litebox-broker-userland --fs-initial-files`. /// /// Supports two modes: /// - **Host mode** (default): Takes local ELF files, discovers dependencies via diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index 8c0a58b43b..c443bfa423 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, @@ -2864,7 +2863,7 @@ mod tests { use std::os::unix::net::UnixStream; use std::thread::sleep; - use litebox::fs::OFlags; + use litebox_common_linux::OFlags; use litebox_platform::sync::RawMutex; use crate::LinuxUserland; diff --git a/litebox_runner_linux_on_windows_userland/src/lib.rs b/litebox_runner_linux_on_windows_userland/src/lib.rs index e047d4fb24..ca0b57df37 100644 --- a/litebox_runner_linux_on_windows_userland/src/lib.rs +++ b/litebox_runner_linux_on_windows_userland/src/lib.rs @@ -7,24 +7,21 @@ 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; -use std::path::PathBuf; /// Run Linux programs with LiteBox on unmodified Windows. /// -/// The program binary and all its dependencies must be provided inside a tar -/// archive via `--initial-files`. The program path refers to a path inside the -/// tar archive. +/// The program binary and all its dependencies must be available in the +/// broker-owned file system. #[derive(Parser, Debug)] pub struct CliArgs { /// The program and arguments passed to it (e.g., `/bin/ls --color`). /// - /// The program path refers to a path inside the tar archive provided via - /// `--initial-files`. All binaries must be pre-rewritten with the syscall - /// rewriter. + /// The program path refers to a path inside the broker-owned file system. + /// All binaries must be pre-rewritten with the syscall rewriter. #[arg(required = true, trailing_var_arg = true, value_hint = clap::ValueHint::CommandWithArguments)] pub program_and_arguments: Vec, /// Environment variables passed to the program (`K=V` pairs; can be invoked multiple times) @@ -45,12 +42,6 @@ pub struct CliArgs { help_heading = "Unstable Options" )] pub broker_control_channel: Option, - /// Tar archive containing the program and its shared libraries. - /// - /// All ELF binaries should be pre-rewritten with the syscall rewriter - /// (e.g., via `litebox-packager`). - #[arg(long = "initial-files", value_name = "PATH_TO_TAR", value_hint = clap::ValueHint::FilePath)] - pub initial_files: PathBuf, } /// Run Linux programs with LiteBox on unmodified Windows @@ -71,54 +62,26 @@ pub fn run(cli_args: CliArgs) -> Result<()> { ) .init(); - let tar_file = &cli_args.initial_files; - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - anyhow::bail!("Expected a .tar file, found {}", tar_file.display()); - } - let tar_data = std::fs::read(tar_file) - .map_err(|e| anyhow!("Could not read tar file at {}: {}", tar_file.display(), e))?; - let platform = Platform::new(); - let broker_connection = cli_args + let control_pipe = cli_args .broker_control_channel .as_deref() - .map(broker::connect) - .transpose()?; - let shim_builder = if let Some(broker_connection) = broker_connection { - let broker::BrokerConnection { - local, - notifications, - } = broker_connection; - let litebox = litebox::LiteBox::new_with_broker_local(platform, local); - broker::start_notification_receiver( - notifications, - litebox.broker_notification_dispatcher(), - litebox.broker_failure_dispatcher(), - )?; - litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox) - } else { - litebox_shim_linux::LinuxShimBuilder::new(platform) - }; + .context("file operations require --broker-control-channel")?; + let broker::BrokerConnection { + local, + notifications, + } = broker::connect(control_pipe)?; + let litebox = litebox::LiteBox::new_with_broker_local(platform, local); + broker::start_notification_receiver( + notifications, + litebox.broker_notification_dispatcher(), + litebox.broker_failure_dispatcher(), + )?; + let shim_builder = litebox_shim_linux::LinuxShimBuilder::new_with_litebox(platform, litebox); // The program path is a Unix-style path inside the tar archive. let prog_path = &cli_args.program_and_arguments[0]; - let initial_file_system = { - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/tmp", - litebox::fs::in_mem::InitialNode::Directory { - mode: litebox::fs::Mode::RWXU | litebox::fs::Mode::RWXG | litebox::fs::Mode::RWXO, - owner: litebox::fs::UserInfo { - user: 1000, - group: 1000, - }, - }, - )]); - - shim_builder.default_fs(in_mem, tar_data.into()) - }; - let initial_file_system = std::sync::Arc::new(initial_file_system); - let shim = shim_builder.build(); let argv = cli_args .program_and_arguments @@ -142,13 +105,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { }; let program = shim - .load_program( - initial_file_system, - platform.init_task(), - prog_path, - argv, - envp, - ) + .load_program(platform.init_task(), prog_path, argv, envp) .unwrap(); unsafe { litebox_platform_windows_userland::run_thread( diff --git a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs b/litebox_runner_linux_on_windows_userland/tests/common/mod.rs deleted file mode 100644 index 07a8ea88d9..0000000000 --- a/litebox_runner_linux_on_windows_userland/tests/common/mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#![cfg(all(target_os = "windows", target_arch = "x86_64"))] - -use std::ffi::CString; - -use litebox::fs::{Mode, OFlags}; -use litebox_platform_windows_userland::WindowsUserland as Platform; - -pub struct TestLauncher { - platform: &'static Platform, - shim_builder: litebox_shim_linux::LinuxShimBuilder, - fs: litebox_shim_linux::DefaultFS, - context: litebox::fs::resolver::Context, -} - -impl TestLauncher { - pub fn init_platform( - tar_data: &'static [u8], - initial_dirs: &[&str], - initial_files: &[&str], - ) -> Self { - let platform = Platform::new(); - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let tar_data = if tar_data.is_empty() { - litebox::fs::tar_ro::EMPTY_TAR_FILE.into() - } else { - tar_data.into() - }; - let fs = shim_builder.default_fs(in_mem, tar_data); - let mut this = Self { - platform, - shim_builder, - fs, - context: litebox::fs::resolver::Context::new(), - }; - - for each in initial_dirs { - this.install_dir(each); - } - for each in initial_files { - let data = std::fs::read(each).unwrap(); - this.install_file(data, each); - } - - this - } - - pub fn install_dir(&mut self, path: &str) { - self.fs - .mkdir(&self.context, path, Mode::RWXU | Mode::RWXG | Mode::RWXO) - .expect("Failed to create directory"); - } - - pub fn install_file(&mut self, contents: Vec, out: &str) { - let fd = self - .fs - .open( - &self.context, - out, - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXG | Mode::RWXO | Mode::RWXU, - ) - .unwrap(); - self.fs.write(&fd, &contents, None).unwrap(); - self.fs.close(&fd).unwrap(); - } - - pub fn test_load_exec_common(self, executable_path: &str) { - let fs = std::sync::Arc::new(self.fs); - let argv = vec![ - CString::new(executable_path).unwrap(), - CString::new("hello").unwrap(), - ]; - let envp = vec![CString::new("PATH=/bin").unwrap()]; - let shim = self.shim_builder.build(); - let program = shim - .load_program(fs, self.platform.init_task(), executable_path, argv, envp) - .unwrap(); - unsafe { - litebox_platform_windows_userland::run_thread( - program.entrypoints, - &mut litebox_common_linux::PtRegs::default(), - ); - } - assert_eq!(program.process.wait(), 0); - } -} diff --git a/litebox_runner_linux_on_windows_userland/tests/loader.rs b/litebox_runner_linux_on_windows_userland/tests/loader.rs index bb79fd60a2..08728a87c5 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, @@ -236,16 +240,14 @@ fn run_prog_with_windows_broker( tar.finish().unwrap(); drop(tar); - let mut arguments = Vec::new(); + let mut arguments: Vec = Vec::new(); if !libs.is_empty() { arguments.extend(["--env".into(), "LD_LIBRARY_PATH=/lib64:/lib32:/lib".into()]); } - arguments.extend([ - "--initial-files".into(), - tar_path.into_os_string(), - format!("/{exec_path}").into(), - ]); + arguments.push(format!("/{exec_path}").into()); let status = std::process::Command::new(broker) + .arg("--fs-initial-files") + .arg(&tar_path) .arg("--runner") .arg(runner) .args(arguments) @@ -261,15 +263,19 @@ fn append_rewritten_file( ) { let rewritten = litebox_syscall_rewriter::rewrite_binary(&std::fs::read(source).unwrap(), None).unwrap(); + append_file(tar, &rewritten, archive_path); +} + +fn append_file(tar: &mut tar::Builder, contents: &[u8], archive_path: &str) { let mut header = tar::Header::new_ustar(); - header.set_size(rewritten.len() as u64); + header.set_size(contents.len() as u64); header.set_mode(0o755); header.set_uid(0); header.set_gid(0); header.set_mtime(0); header.set_entry_type(tar::EntryType::Regular); header.set_cksum(); - tar.append_data(&mut header, archive_path, rewritten.as_slice()) + tar.append_data(&mut header, archive_path, contents) .unwrap(); } @@ -344,29 +350,27 @@ 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}"); - // Run litebox_runner_linux_on_windows_userland with the tar file + // Run litebox_runner_linux_on_windows_userland with the broker-owned file system. let mut args = vec![ // 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", - "--initial-files", - tar_target_file.to_str().unwrap(), ]; args.push(&prog_tar_path); args.extend_from_slice(cmd_args); - - let mut command = std::process::Command::new(&binary_path); - command.args(&args); + let (broker, runner) = build_windows_broker(); + let mut command = std::process::Command::new(broker); + command + .arg("--fs-initial-files") + .arg(&tar_target_file) + .arg("--runner") + .arg(runner) + .args(&args); println!("Running `{command:?}`"); let status = command .status() diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 2affcdc8f9..9f3696370e 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -9,14 +9,11 @@ clap = { version = "4.5.33", features = ["derive"] } libc = { version = "0.2.169", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } litebox_broker_local_userland = { version = "0.1.0", path = "../litebox_broker_local_userland" } -litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } litebox_broker_transport_linux_userland = { version = "0.1.0", path = "../litebox_broker_transport_linux_userland" } litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } litebox_platform_linux_userland = { version = "0.1.0", path = "../litebox_platform_linux_userland" } litebox_shim_linux = { version = "0.1.0", path = "../litebox_shim_linux" } -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"] } @@ -27,13 +24,13 @@ glob = "0.3" litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core", features = ["test-support"] } litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } litebox_broker_platform_linux_userland = { version = "0.1.0", path = "../litebox_broker_platform_linux_userland" } +litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } litebox_broker_userland = { version = "0.1.0", path = "../litebox_broker_userland" } [features] lock_tracing = ["litebox/lock_tracing"] -# Keep gate rewriting and platform register/signal virtualization in lockstep. -# Enabling only the shim feature would split logical x18 between its TLS slot -# and the physical register exposed by the platform. +# Pre-rewritten guests that virtualize x18 require matching shim state and +# platform register/signal handling. aarch64_virtualize_x18 = [ "litebox_platform_linux_userland/aarch64_virtualize_x18", "litebox_shim_linux/aarch64_virtualize_x18", diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 8ce8e8a355..b8343fa455 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -3,18 +3,13 @@ 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 std::path::PathBuf; use litebox_broker_local_userland as broker; -extern crate alloc; - // Use a stable non-root guest identity instead of mirroring the host user. This keeps shim -// credentials aligned with the in-memory filesystem default user and avoids truncating high host IDs. +// credentials aligned with packaged guest files and avoids truncating high host IDs. const DEFAULT_GUEST_UID: u16 = 1000; const DEFAULT_GUEST_GID: u16 = 1000; const MANAGED_PROXY_ENV_KEYS: [&str; 5] = [ @@ -31,12 +26,10 @@ const MANAGED_PROXY_ENV_KEYS: [&str; 5] = [ /// - `LITEBOX_LOG=debug` to show debug and higher level logs /// - `LITEBOX_LOG=litebox=debug,litebox::fs=trace` for multiple filters at different levels #[derive(Parser, Debug)] -#[allow(clippy::struct_excessive_bools)] pub struct CliArgs { - /// The program and arguments passed to it (e.g., `python3 --version`). + /// The program and arguments passed to it (e.g., `/usr/bin/python3 --version`). /// - /// By default this is a path on the host filesystem. When --program-from-tar - /// is set, it refers to a path inside the tar archive instead. + /// The program path must be absolute and refer to a file in the broker-owned file system. #[arg(required = true, trailing_var_arg = true, value_hint = clap::ValueHint::CommandWithArguments)] pub program_and_arguments: Vec, /// Environment variables passed to the program (`K=V` pairs; can be invoked multiple times) @@ -48,37 +41,6 @@ pub struct CliArgs { /// Allow using unstable options #[arg(short = 'Z', long = "unstable")] pub unstable: bool, - /// Pre-fill files into the initial file system state - // TODO: Might want to extend this to support full directories at some point? - #[arg(long = "insert-file", value_hint = clap::ValueHint::FilePath, - requires = "unstable", help_heading = "Unstable Options")] - pub insert_files: Vec, - /// Pre-fill the files in this tar file into the initial file system state - #[arg(long = "initial-files", value_name = "PATH_TO_TAR", value_hint = clap::ValueHint::FilePath, - requires = "unstable", help_heading = "Unstable Options")] - pub initial_files: Option, - /// Apply syscall-rewriter to the ELF file before running it - /// - /// This is meant as a convenience feature; real deployments would likely prefer ahead-of-time - /// rewrite things to amortize costs. - #[arg( - long = "rewrite-syscalls", - requires = "unstable", - help_heading = "Unstable Options" - )] - pub rewrite_syscalls: bool, - /// Load the program binary from the tar file instead of from the host filesystem. - /// - /// When set, the program path refers to a path inside the tar filesystem. - /// The binary must already be rewritten (incompatible with --rewrite-syscalls). - /// This is used by `litebox-packager` to create fully self-contained tar bundles. - #[arg( - long = "program-from-tar", - requires_all = ["unstable", "initial_files"], - conflicts_with = "rewrite_syscalls", - help_heading = "Unstable Options" - )] - pub program_from_tar: bool, /// Broker-supplied Unix socket path for the local control channel. #[arg( long = "broker-control-channel", @@ -100,30 +62,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 @@ -148,130 +86,41 @@ pub fn run(cli_args: CliArgs) -> Result { ) .init(); - if !cli_args.insert_files.is_empty() { - unimplemented!( - "this should (hopefully soon) have a nicer interface to support loading in files" - ) - } - - // When loading from tar, the program path is a guest-internal path and must - // be absolute — LiteBox does not resolve programs via PATH. - if cli_args.program_from_tar && !cli_args.program_and_arguments[0].starts_with('/') { - anyhow::bail!( - "--program-from-tar requires an absolute path (e.g., /usr/bin/ls), \ - got: {}", - cli_args.program_and_arguments[0] - ); + let prog_path = &cli_args.program_and_arguments[0]; + if !prog_path.starts_with('/') { + anyhow::bail!("program path must be absolute (e.g., /usr/bin/ls), got: {prog_path}"); } - 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,99 +135,6 @@ pub fn run(cli_args: CliArgs) -> Result { gid: u32::from(DEFAULT_GUEST_GID), egid: u32::from(DEFAULT_GUEST_GID), }; - let initial_file_system = { - // The in-memory layer is pre-populated at construction, which lets us set up root-owned - // directories and files without ever acting as root at runtime. - // - // A host uid of 0 anywhere along the path means the entry stays root-owned; as soon as a - // path component belongs to a non-root host user, that component and everything below it - // is owned by the guest user. - let owner_of = |parent_host_user: u32, host_user: u32| { - if parent_host_user == 0 && host_user == 0 { - litebox::fs::UserInfo::ROOT - } else { - litebox::fs::UserInfo { - user: DEFAULT_GUEST_UID, - group: DEFAULT_GUEST_GID, - } - } - }; - let mut entries: Vec<(String, litebox::fs::in_mem::InitialNode)> = Vec::new(); - - // When loading the program from the tar, we don't need to create ancestor - // directories or write the program binary into the in-memory FS -- the program - // is already in the tar layer. - if let Some(prog_data) = prog_data { - let prog = std::path::absolute(Path::new(&cli_args.program_and_arguments[0])).unwrap(); - let ancestors: Vec<_> = prog.ancestors().collect(); - let mut prev_user = 0; - for (path, &mode_and_user) in ancestors - .into_iter() - .skip(1) - .rev() - .skip(1) - .zip(&ancestor_modes_and_users) - { - entries.push(( - path.to_str().unwrap().to_owned(), - litebox::fs::in_mem::InitialNode::Directory { - mode: mode_and_user.0, - owner: owner_of(prev_user, mode_and_user.1), - }, - )); - prev_user = mode_and_user.1; - } - let last = ancestor_modes_and_users.last().ok_or_else(|| { - anyhow!("program path has no ancestor directories (is it the root path?)") - })?; - entries.push(( - prog.to_str().unwrap().to_owned(), - litebox::fs::in_mem::InitialNode::File { - mode: last.0, - owner: owner_of(prev_user, last.1), - data: prog_data, - }, - )); - } - - let tmp_mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; - if let Some((_, node)) = entries.iter_mut().find(|(path, _)| path == "/tmp") { - // `/tmp` is an ancestor of the program, so it keeps the owner derived above and only - // has its mode widened. - let litebox::fs::in_mem::InitialNode::Directory { mode, .. } = node else { - unreachable!("ancestors are always directories") - }; - *mode = tmp_mode; - } else { - entries.push(( - "/tmp".to_owned(), - litebox::fs::in_mem::InitialNode::Directory { - mode: tmp_mode, - owner: litebox::fs::UserInfo::ROOT, - }, - )); - } - - let in_mem = litebox::fs::in_mem::InMem::new_initialized(entries); - shim_builder.default_fs(in_mem, tar_data.into()) - }; - - // We need to get the file path before enabling seccomp. - // For --program-from-tar the path is already validated as absolute above, - // so use it directly instead of resolving against the host CWD. - let prog = if cli_args.program_from_tar { - PathBuf::from(&cli_args.program_and_arguments[0]) - } else { - std::path::absolute(Path::new(&cli_args.program_and_arguments[0])).unwrap() - }; - let prog_path = prog.to_str().ok_or_else(|| { - anyhow!( - "Could not convert program path {:?} to a string", - cli_args.program_and_arguments[0] - ) - })?; - - let initial_file_system = std::sync::Arc::new(initial_file_system); let shim = shim_builder.build(); @@ -403,7 +159,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_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..587ba8c50d --- /dev/null +++ b/litebox_runner_linux_userland/tests/common/runner.rs @@ -0,0 +1,263 @@ +// 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=/", + ]); + + 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(&self.cmd_path).args(&self.cmd_args); + + #[cfg(target_os = "linux")] + if self.use_userland_broker || !self.managed_proxy_hosts.is_empty() { + let runner = self.command.get_program().to_os_string(); + let runner_arguments = self + .command + .get_args() + .filter(|argument| *argument != "--unstable") + .map(OsStr::to_os_string) + .collect::>(); + let broker = Path::new(&runner).with_file_name("litebox-broker-userland"); + let proxy = Path::new(&runner).with_file_name("litebox_egress_proxy"); + assert!( + broker.is_file(), + "userland broker tests require a workspace build producing {}", + broker.display() + ); + if !self.managed_proxy_hosts.is_empty() { + assert!( + proxy.is_file(), + "managed proxy tests require a workspace build producing {}", + proxy.display() + ); + } + let mut command = std::process::Command::new(broker); + for host in &self.managed_proxy_hosts { + command.arg("--allow-host").arg(host); + } + command.arg("--fs-initial-files").arg(&tar_file); + if self.in_process_mode { + command.args(["--unstable", "--in-process-runner"]); + } else { + command.arg("--runner").arg(runner); + } + command.args(runner_arguments); + self.command = command; + } + } + + fn run_inner(&mut self, capture_stdout: bool) -> Vec { + self.prepare_command(); + self.command.stderr(std::process::Stdio::inherit()); + if !capture_stdout { + self.command.stdout(std::process::Stdio::inherit()); + } + println!("Running `{:?}`", self.command); + let output = self + .command + .output() + .expect("Failed to run litebox_runner_linux_userland"); + assert!( + output.status.success(), + "failed to run litebox_runner_linux_userland: {}", + output.status + ); + output.stdout + } + + #[cfg(target_os = "linux")] + pub(crate) fn spawn_with_stdio( + &mut self, + stdin: std::process::Stdio, + stdout: std::process::Stdio, + stderr: std::process::Stdio, + ) -> std::process::Child { + self.prepare_command(); + self.command.stdin(stdin).stdout(stdout).stderr(stderr); + println!("Running `{:?}`", self.command); + self.command + .spawn() + .expect("Failed to spawn litebox_runner_linux_userland") + } +} diff --git a/litebox_runner_linux_userland/tests/loader.rs b/litebox_runner_linux_userland/tests/loader.rs index 1d9e2ed577..44844e7726 100644 --- a/litebox_runner_linux_userland/tests/loader.rs +++ b/litebox_runner_linux_userland/tests/loader.rs @@ -4,148 +4,24 @@ mod cache; mod common; -use std::ffi::CString; - -use litebox::fs::{Mode, OFlags}; -use litebox_platform_linux_userland::LinuxUserland as Platform; - -struct TestLauncher { - platform: &'static Platform, - shim_builder: litebox_shim_linux::LinuxShimBuilder, - fs: litebox_shim_linux::DefaultFS, - context: litebox::fs::resolver::Context, -} - -impl TestLauncher { - fn init_platform(tar_data: &'static [u8], initial_files: &[&str]) -> Self { - let platform = Platform::new(); - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let tar_data = if tar_data.is_empty() { - litebox::fs::tar_ro::EMPTY_TAR_FILE.into() - } else { - tar_data.into() - }; - let fs = shim_builder.default_fs(in_mem, tar_data); - let mut this = Self { - platform, - shim_builder, - fs, - context: litebox::fs::resolver::Context::new(), - }; - - for each in initial_files { - this.install_dir_all(std::path::Path::new(each).parent().unwrap()); - let data = std::fs::read(each).unwrap(); - this.install_file(data, each); - } - - this - } - - fn install_dir_all(&mut self, path: &std::path::Path) { - let mut ancestors: Vec<_> = path - .ancestors() - .filter(|a| *a != std::path::Path::new("/") && !a.as_os_str().is_empty()) - .collect(); - ancestors.reverse(); - for ancestor in ancestors { - if let Err(e) = self.install_dir(ancestor.to_str().unwrap()) { - assert!( - matches!(e, litebox::fs::errors::MkdirError::AlreadyExists), - "Failed to create directory {}: {e}", - ancestor.display() - ); - } - } - } - - fn install_dir(&mut self, path: &str) -> Result<(), litebox::fs::errors::MkdirError> { - self.fs - .mkdir(&self.context, path, Mode::RWXU | Mode::RWXG | Mode::RWXO) - } - - fn install_file(&mut self, contents: Vec, out: &str) { - let fd = self - .fs - .open( - &self.context, - out, - OFlags::CREAT | OFlags::WRONLY, - Mode::RWXG | Mode::RWXO | Mode::RWXU, - ) - .unwrap(); - self.fs.write(&fd, &contents, None).unwrap(); - self.fs.close(&fd).unwrap(); - } - - fn test_load_exec_common(self, executable_path: &str) { - let argv = vec![ - CString::new(executable_path).unwrap(), - CString::new("hello").unwrap(), - ]; - let envp = vec![ - CString::new("PATH=/bin").unwrap(), - CString::new("HOME=/").unwrap(), - ]; - let fs = std::sync::Arc::new(self.fs); - let shim = self.shim_builder.build(); - let program = shim - .load_program(fs, self.platform.init_task(), executable_path, argv, envp) - .unwrap(); - unsafe { - litebox_platform_linux_userland::run_thread( - program.entrypoints, - &mut litebox_common_linux::PtRegs::default(), - ); - } - assert_eq!( - program.process.wait(), - 0, - "process exited with non-zero code" - ); - } -} +use common::runner::Runner; #[test] fn test_load_exec_dynamic() { let path = common::compile("./tests/hello.c", "hello_dylib", false, false); - - let files_to_install = common::find_dependencies(path.to_str().unwrap()); - - let executable_path = "/hello_dylib"; - let executable_data = std::fs::read(path).unwrap(); - - let mut launcher = TestLauncher::init_platform( - &[], - &files_to_install - .iter() - .map(std::string::String::as_str) - .collect::>(), - ); - launcher.install_file(executable_data, executable_path); - launcher.test_load_exec_common(executable_path); + Runner::new(&path, "loader_dynamic") + .env("PATH=/bin") + .arg("hello") + .run(); } #[test] fn test_load_exec_static() { let path = common::compile("./tests/hello.c", "hello_exec", true, false); - - let executable_path = "/hello_exec"; - let executable_data = std::fs::read(path).unwrap(); - - let mut launcher = TestLauncher::init_platform(&[], &[]); - - launcher.install_file(executable_data, executable_path); - - launcher.test_load_exec_common(executable_path); + Runner::new(&path, "loader_static") + .env("PATH=/bin") + .arg("hello") + .run(); } const HELLO_WORLD_NOLIBC: &str = r#" @@ -277,10 +153,8 @@ fn test_syscall_rewriter() { let rewrite_success = common::rewrite_with_cache(&path, &hooked_path, &[]); assert!(rewrite_success, "failed to run syscall rewriter"); - let executable_path = "/hello_exec_nolibc.hooked"; - let executable_data = std::fs::read(hooked_path).unwrap(); - - let mut launcher = TestLauncher::init_platform(&[], &[]); - launcher.install_file(executable_data, executable_path); - launcher.test_load_exec_common(executable_path); + Runner::new_pre_rewritten(&hooked_path, "loader_pre_rewritten") + .env("PATH=/bin") + .arg("hello") + .run(); } diff --git a/litebox_runner_linux_userland/tests/rewritten_guests.rs b/litebox_runner_linux_userland/tests/rewritten_guests.rs index a636f54843..db4e3b7efd 100644 --- a/litebox_runner_linux_userland/tests/rewritten_guests.rs +++ b/litebox_runner_linux_userland/tests/rewritten_guests.rs @@ -3,56 +3,24 @@ //! Tests for guests whose syscall sites the rewriter redirected. //! -//! Guests run under `--rewrite-syscalls` with no tar rootfs. +//! Guests are rewritten before being loaded into the broker-owned file system. #[allow(dead_code, reason = "shared with the other test binaries")] mod cache; #[allow(dead_code, reason = "shared with the other test binaries")] mod common; -fn run_rewritten_fixture(source: &str, unique_name: &str) -> std::process::Output { - let target = common::compile(source, unique_name, true, false); - 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()); - - #[cfg(target_os = "linux")] - let mut command = { - let broker_path = - std::path::Path::new(&binary_path).with_file_name("litebox-broker-userland"); - assert!( - broker_path.is_file(), - "brokered runner tests require a workspace build producing {}", - broker_path.display() - ); - let mut command = std::process::Command::new(broker_path); - command.arg("--runner").arg(&binary_path); - command - }; - #[cfg(not(target_os = "linux"))] - let mut command = { - let mut command = std::process::Command::new(binary_path); - command.arg("--unstable"); - command - }; +use common::runner::Runner; - command - .args(["--rewrite-syscalls"]) - .arg(target) - .output() - .expect("Failed to run litebox_runner_linux_userland") +fn run_rewritten_fixture(source: &str, unique_name: &str) -> Vec { + let target = common::compile(source, unique_name, true, false); + Runner::new(&target, unique_name).output() } #[test] -fn test_host_program_with_rewrite_syscalls() { - let output = run_rewritten_fixture("./tests/hello.c", "host_program_rewriter"); - - assert!( - output.status.success(), - "failed to run litebox_runner_linux_userland: {}", - output.status - ); - - let stdout = String::from_utf8_lossy(&output.stdout); +fn test_rewritten_program() { + let output = run_rewritten_fixture("./tests/hello.c", "rewritten_program"); + let stdout = String::from_utf8_lossy(&output); println!("{stdout}"); assert!(stdout.contains("argv[0] = "), "unexpected stdout: {stdout}"); } @@ -61,14 +29,7 @@ fn test_host_program_with_rewrite_syscalls() { /// On non-AArch64 the fixture is a no-op. #[test] fn test_svc_scratch_registers_survive_rewritten_syscall() { - let output = run_rewritten_fixture("./tests/svc_scratch_regs.c", "svc_scratch_regs_rewriter"); - - assert!( - output.status.success(), - "guest scratch registers did not survive a rewritten syscall ({}): {}", - output.status, - String::from_utf8_lossy(&output.stderr), - ); + run_rewritten_fixture("./tests/svc_scratch_regs.c", "svc_scratch_regs_rewriter"); } /// The synthetic AArch64 restorer must invoke rt_sigreturn, which restores @@ -77,17 +38,10 @@ fn test_svc_scratch_registers_survive_rewritten_syscall() { #[test] fn test_signal_handler_returns_through_sigreturn() { let output = run_rewritten_fixture("./tests/sigreturn.c", "sigreturn_rewriter"); - - assert!( - output.status.success(), - "signal handler did not return cleanly through rt_sigreturn ({}): {}", - output.status, - String::from_utf8_lossy(&output.stderr), - ); assert!( - String::from_utf8_lossy(&output.stdout).contains("sigreturn ok"), + String::from_utf8_lossy(&output).contains("sigreturn ok"), "guest did not reach the post-sigreturn write: {}", - String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output), ); } @@ -98,31 +52,17 @@ fn test_signal_handler_returns_through_sigreturn() { #[test] fn test_guest_x16_survives_asynchronous_resume() { let output = run_rewritten_fixture("./tests/async_x16.c", "async_x16_rewriter"); - - assert!( - output.status.success(), - "guest x16 did not survive an asynchronous resume ({}): {}", - output.status, - String::from_utf8_lossy(&output.stderr), - ); assert!( - String::from_utf8_lossy(&output.stdout).contains("async x16 ok"), + String::from_utf8_lossy(&output).contains("async x16 ok"), "guest did not resume after the interruption: {}", - String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output), ); } #[test] #[cfg(target_arch = "aarch64")] fn test_guest_simd_survives_signal_delivery() { - let output = run_rewritten_fixture("./tests/sigreturn_simd.c", "sigreturn_simd_rewriter"); - - assert!( - output.status.success(), - "guest SIMD state did not survive signal delivery ({}): {}", - output.status, - String::from_utf8_lossy(&output.stderr), - ); + run_rewritten_fixture("./tests/sigreturn_simd.c", "sigreturn_simd_rewriter"); } /// Semantic stress only: sampling cannot prove a signal PC landed inside a @@ -131,17 +71,10 @@ fn test_guest_simd_survives_signal_delivery() { #[cfg(target_arch = "aarch64")] fn test_signals_while_exercising_each_aarch64_gate_kind() { let output = run_rewritten_fixture("./tests/gate_signals.c", "gate_signals_rewriter"); - - assert!( - output.status.success(), - "AArch64 gate semantics changed while signals were active ({}): {}", - output.status, - String::from_utf8_lossy(&output.stderr), - ); assert!( - String::from_utf8_lossy(&output.stdout).contains("gate signals ok"), + String::from_utf8_lossy(&output).contains("gate signals ok"), "guest did not finish all gate loops: {}", - String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output), ); } @@ -154,17 +87,5 @@ 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"); - assert!( - output.status.success(), - "x18 fixture failed ({}): {}", - output.status, - String::from_utf8_lossy(&output.stderr), - ); + Runner::new(&target, "x18_virtualization_rewriter").run(); } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 83b55f6706..f2324b4b92 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -4,10 +4,9 @@ mod cache; mod common; -use std::{ - ffi::OsString, - path::{Path, PathBuf}, -}; +use std::path::{Path, PathBuf}; + +use common::runner::Runner; #[cfg(target_arch = "x86_64")] const BROKER_HELPER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); @@ -109,246 +108,6 @@ fn gateway_udp_policy() -> litebox_broker_core::SocketPolicy { .unwrap() } -/// Debian multiarch library directory preserved at its guest-relative path. -#[cfg(target_arch = "x86_64")] -const MULTIARCH_LIB_DIR: &str = "lib/x86_64-linux-gnu"; -#[cfg(target_arch = "aarch64")] -const MULTIARCH_LIB_DIR: &str = "lib/aarch64-linux-gnu"; - -#[must_use] -struct Runner { - command: std::process::Command, - dir_path: PathBuf, - tar_dir: PathBuf, - unique_name: String, - cmd_path: PathBuf, - cmd_args: Vec, - #[cfg(target_os = "linux")] - managed_proxy_hosts: Vec, - #[cfg(target_os = "linux")] - use_userland_broker: bool, - #[cfg(target_os = "linux")] - in_process_mode: bool, - has_run: bool, -} - -impl Runner { - fn new(target: &Path, unique_name: &str) -> Self { - let dir_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")); - - // create tar file containing the rewritten executable and all dependencies - let tar_dir = dir_path.join(format!("tar_files_{unique_name}")); - let dirs_to_create = ["lib64", MULTIARCH_LIB_DIR, "lib32"]; - for dir in dirs_to_create { - std::fs::create_dir_all(tar_dir.join(dir)).unwrap(); - } - std::fs::create_dir_all(tar_dir.join("out")).unwrap(); - - let target_guest_path = std::path::absolute(target).unwrap(); - let target_dest_path = tar_dir.join(target_guest_path.strip_prefix("/").unwrap()); - let success = common::rewrite_with_cache(target, &target_dest_path, &[]); - assert!(success, "failed to run litebox_syscall_rewriter"); - - let libs = common::find_dependencies(target.to_str().unwrap()); - for file in &libs { - let file_path = std::path::Path::new(file.as_str()); - let dest_path = tar_dir.join(&file[1..]); - let success = common::rewrite_with_cache(file_path, &dest_path, &[]); - assert!( - success, - "failed to run litebox_syscall_rewriter for {}", - file_path.to_str().unwrap() - ); - } - - // Get the path to the litebox_runner_linux_userland binary - let binary_path = std::env::var("NEXTEST_BIN_EXE_litebox_runner_linux_userland") - .unwrap_or_else(|_| env!("CARGO_BIN_EXE_litebox_runner_linux_userland").to_string()); - - // run litebox_runner_linux_userland with the tar file and the compiled executable - let mut command = std::process::Command::new(binary_path); - command.args([ - "--unstable", - // Tell ld where to find the libraries. - // See https://man7.org/linux/man-pages/man8/ld.so.8.html for how ld works. - // Alternatively, we could add a `/etc/ld.so.cache` file to the rootfs. - "--env", - "LD_LIBRARY_PATH=/lib64:/lib32:/lib", - "--env", - "HOME=/", - "--program-from-tar", - ]); - - Self { - command, - dir_path, - tar_dir, - cmd_path: target_guest_path, - cmd_args: Vec::new(), - #[cfg(target_os = "linux")] - managed_proxy_hosts: Vec::new(), - #[cfg(target_os = "linux")] - use_userland_broker: true, - #[cfg(target_os = "linux")] - in_process_mode: false, - has_run: false, - unique_name: unique_name.to_owned(), - } - } - - fn tar_dir(&self) -> &Path { - &self.tar_dir - } - - fn env(&mut self, env: impl AsRef) -> &mut Self { - self.command.arg("--env").arg(env); - self - } - - fn envs(&mut self, envs: impl IntoIterator>) -> &mut Self { - for env in envs { - self.env(env); - } - self - } - - fn arg(&mut self, arg: impl AsRef) -> &mut Self { - self.cmd_args.push(arg.as_ref().to_os_string()); - self - } - - fn args(&mut self, args: impl IntoIterator>) -> &mut Self { - for arg in args { - self.arg(arg); - } - self - } - - fn guest_program_path(&mut self, guest_path: &str) -> &mut Self { - self.cmd_path = PathBuf::from(guest_path); - self - } - - #[cfg(all(target_arch = "x86_64", target_os = "linux"))] - fn broker_socket(&mut self, control_socket_path: &Path) -> &mut Self { - self.use_userland_broker = false; - self.command - .arg("--broker-control-channel") - .arg(control_socket_path); - self - } - - #[cfg(all(target_arch = "x86_64", target_os = "linux"))] - fn use_in_process_runner(&mut self) -> &mut Self { - self.use_userland_broker = true; - self.in_process_mode = true; - self - } - - fn with_fs_path(&mut self, f: impl FnOnce(&Path)) -> &mut Self { - f(&self.tar_dir); - self - } - - fn run(&mut self) { - self.run_inner(false); - } - - #[must_use] - fn output(&mut self) -> Vec { - self.run_inner(true) - } - - fn prepare_command(&mut self) { - assert!(!self.has_run); - self.has_run = true; - // create tar file using `tar` command with caching - let tar_file = self - .dir_path - .join(format!("rootfs_{}.tar", self.unique_name)); - let tar_success = - common::create_tar_with_cache(&self.tar_dir, &tar_file, &self.unique_name); - assert!(tar_success, "failed to create tar file"); - println!("Tar file ready at: {}", tar_file.to_str().unwrap()); - - self.command - .arg("--initial-files") - .arg(tar_file) - .arg(&self.cmd_path) - .args(&self.cmd_args); - - #[cfg(target_os = "linux")] - if self.use_userland_broker || !self.managed_proxy_hosts.is_empty() { - let runner = self.command.get_program().to_os_string(); - let runner_arguments = self - .command - .get_args() - .filter(|argument| *argument != "--unstable") - .map(std::ffi::OsStr::to_os_string) - .collect::>(); - let broker = Path::new(&runner).with_file_name("litebox-broker-userland"); - let proxy = Path::new(&runner).with_file_name("litebox_egress_proxy"); - assert!( - broker.is_file(), - "userland broker tests require a workspace build producing {}", - broker.display() - ); - if !self.managed_proxy_hosts.is_empty() { - assert!( - proxy.is_file(), - "managed proxy tests require a workspace build producing {}", - proxy.display() - ); - } - let mut command = std::process::Command::new(broker); - for host in &self.managed_proxy_hosts { - command.arg("--allow-host").arg(host); - } - if self.in_process_mode { - command.args(["--unstable", "--in-process-runner"]); - } else { - command.arg("--runner").arg(runner); - } - command.args(runner_arguments); - self.command = command; - } - } - - fn run_inner(&mut self, capture_stdout: bool) -> Vec { - self.prepare_command(); - self.command.stderr(std::process::Stdio::inherit()); - if !capture_stdout { - self.command.stdout(std::process::Stdio::inherit()); - } - println!("Running `{:?}`", self.command); - let output = self - .command - .output() - .expect("Failed to run litebox_runner_linux_userland"); - assert!( - output.status.success(), - "failed to run litebox_runner_linux_userland: {}", - output.status - ); - output.stdout - } - - #[cfg(target_os = "linux")] - fn spawn_with_stdio( - &mut self, - stdin: std::process::Stdio, - stdout: std::process::Stdio, - stderr: std::process::Stdio, - ) -> std::process::Child { - self.prepare_command(); - self.command.stdin(stdin).stdout(stdout).stderr(stderr); - println!("Running `{:?}`", self.command); - self.command - .spawn() - .expect("Failed to spawn litebox_runner_linux_userland") - } -} - /// Find all C test files in a directory fn find_c_test_files(dir: &str) -> Vec { let mut files = Vec::new(); @@ -380,8 +139,7 @@ fn has_dedicated_c_test(path: &Path) -> bool { #[cfg(target_os = "linux")] fn configure_pipe_broker(path: &Path, runner: &mut Runner) { if path.file_name().and_then(|name| name.to_str()) == Some("sendfile.c") { - runner.use_userland_broker = true; - runner.env("LITEBOX_PIPE_BROKER=1"); + runner.use_userland_broker().env("LITEBOX_PIPE_BROKER=1"); } } @@ -534,24 +292,119 @@ impl Drop for TestBroker { fn spawn_test_broker( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, + file_roots: &[&Path], connection_count: usize, ) -> TestBroker { - spawn_test_broker_with_mode(control_socket_path, policy, connection_count, false) + spawn_test_broker_with_mode( + control_socket_path, + policy, + file_roots, + connection_count, + false, + ) } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] fn spawn_concurrent_test_broker( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, + file_roots: &[&Path], connection_count: usize, ) -> TestBroker { - spawn_test_broker_with_mode(control_socket_path, policy, connection_count, true) + spawn_test_broker_with_mode( + control_socket_path, + policy, + file_roots, + connection_count, + true, + ) +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +fn test_file_service( + file_roots: &[PathBuf], +) -> std::sync::Arc { + use std::os::unix::fs::PermissionsExt; + + use litebox_broker_core::fs::{ + composer::Composer, + in_mem::{InMem, InitialNode}, + resolver::Resolver, + }; + use litebox_broker_platform_linux_userland::LinuxSyncPrimitivesProvider; + use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; + + let directory_mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; + let mut entries = vec![ + ( + "/tmp".to_owned(), + InitialNode::Directory { + mode: directory_mode, + owner: UserInfo::ROOT, + }, + ), + ( + "/registry".to_owned(), + InitialNode::Directory { + mode: directory_mode, + owner: UserInfo::ROOT, + }, + ), + ]; + + for root in file_roots { + for entry in walkdir::WalkDir::new(root).sort_by_file_name() { + let entry = entry.expect("failed to walk runner test root"); + let relative = entry + .path() + .strip_prefix(root) + .expect("runner test path must be below its root"); + if relative.as_os_str().is_empty() { + continue; + } + + let guest_path = format!( + "/{}", + relative + .to_str() + .expect("runner test paths must contain valid UTF-8") + ); + let metadata = + std::fs::metadata(entry.path()).expect("failed to inspect runner test file"); + let mode = Mode::from_u32_bits_truncate(metadata.permissions().mode()); + let node = if metadata.is_dir() { + InitialNode::Directory { + mode, + owner: UserInfo::ROOT, + } + } else { + InitialNode::File { + mode, + owner: UserInfo::ROOT, + data: std::fs::read(entry.path()) + .expect("failed to read runner test file") + .into(), + } + }; + entries.push((guest_path, node)); + } + } + + let backend = Composer::builder() + .mount("/", |_| { + InMem::::new_initialized(entries) + }) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .unwrap(); + std::sync::Arc::new(Resolver::::new(backend)) } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] fn spawn_test_broker_with_mode( control_socket_path: &Path, policy: litebox_broker_core::PolicyEngine, + file_roots: &[&Path], connection_count: usize, concurrent: bool, ) -> TestBroker { @@ -563,6 +416,10 @@ fn spawn_test_broker_with_mode( let (stdout_tx, stdout_rx) = std::sync::mpsc::channel(); let server_control_socket_path = control_socket_path.to_path_buf(); let cleanup_control_socket_path = control_socket_path.to_path_buf(); + let file_roots = file_roots + .iter() + .map(|root| root.to_path_buf()) + .collect::>(); let broker_thread = std::thread::spawn(move || { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let control_listener = @@ -580,6 +437,7 @@ fn spawn_test_broker_with_mode( )) .with_random_provider(std::sync::Arc::new(TestRandomProvider)) .with_stdio_provider(std::sync::Arc::new(CapturingStdioProvider { stdout_tx })) + .with_file_service(test_file_service(&file_roots)) .build() .expect("failed to create broker core"); ready_tx.send(()).expect("failed to report broker ready"); @@ -674,20 +532,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 => { @@ -738,44 +608,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(); @@ -892,6 +765,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, @@ -899,9 +773,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) @@ -988,6 +863,7 @@ fn test_runner_broker_udp_with_rewriter() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(gateway_udp_policy()), + &[runner.tar_dir()], 1, ); runner @@ -1011,6 +887,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, @@ -1018,9 +896,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()); @@ -1032,7 +911,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) @@ -1067,6 +946,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, @@ -1074,11 +954,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(); @@ -1473,6 +1356,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, @@ -1480,10 +1364,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(); @@ -1518,8 +1403,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| { @@ -1567,6 +1452,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, @@ -1574,6 +1460,7 @@ fn test_broker_with_iperf3() { litebox_broker_core::ObjectRights::all(), ) .with_socket_policy(gateway_tcp_policy()), + &[runner.tar_dir()], 1, ); @@ -1633,7 +1520,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_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index d7c809f83f..2135aa0634 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -25,12 +25,10 @@ alarm_fallback = [] aarch64_virtualize_x18 = [] [dev-dependencies] -litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0", features = ["test-support"] } +litebox_broker_core = { path = "../litebox_broker_core/", version = "0.1.0", features = ["test-support"] } +litebox_broker_host = { path = "../litebox_broker_host/", version = "0.1.0", features = ["test-support"] } litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } -spin = { version = "0.9.8", default-features = false, features = ["spin_mutex"] } libc = "0.2.177" -tempfile = "3" # The unit tests need a concrete platform to run against. The platform is selected # by the build target so the tests can run on whichever userland platform matches diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index faa6c28fe5..bce1f96d23 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -14,7 +14,6 @@ extern crate alloc; -use alloc::borrow::Cow; use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; @@ -29,21 +28,16 @@ use litebox::{ sync::futex::FutexManager, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _}, }; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileMode as Mode, FileOpenFlags, FileSeekWhence as SeekWhence, +}; use litebox_common_linux::{ - SyscallRequest, + OFlags, SyscallRequest, errno::Errno, user_pointers::{UserPtr, UserPtrMut}, }; use litebox_platform::time::TimeProvider; -fn legacy_o_flags(flags: litebox_common_linux::OFlags) -> litebox::fs::OFlags { - litebox::fs::OFlags::from_bits_retain(flags.bits()) -} - -fn legacy_file_mode(mode: litebox_broker_protocol::fs::FileMode) -> litebox::fs::Mode { - litebox::fs::Mode::from_bits_retain(u32::from(mode.bits())) -} - #[cfg(target_arch = "aarch64")] const fn aarch64_rewrite_options() -> litebox_syscall_rewriter::RewriteOptions { #[cfg(target_os = "macos")] @@ -65,15 +59,9 @@ pub(crate) mod channel; pub mod loader; pub(crate) mod stdio; pub mod syscalls; -pub mod transport; mod wait; -pub type DefaultFS = LinuxFS; - -pub(crate) type LinuxFS = - litebox::fs::resolver::Resolver; - -pub(crate) type FileFd = litebox::fd::TypedFd>; +pub(crate) use litebox::fs::FileFd; /// Aggregate bound capturing everything the shim requires of a platform. /// @@ -233,27 +221,19 @@ impl LinuxShimBuilder { &self.litebox } - /// Create the default file system with the given in-memory layer and tar data. - pub fn default_fs( - &self, - in_mem: litebox::fs::in_mem::InMem, - tar_data: Cow<'static, [u8]>, - ) -> DefaultFS { - default_fs(&self.litebox, in_mem, tar_data) - } - /// Build the shim. pub fn build(self) -> LinuxShim { - let net = Network::new(&self.litebox); + let litebox = Arc::new(self.litebox); + let net = Network::new(&litebox); let global = Arc::new(GlobalState { platform: self.platform, - pm: PageManager::new(&self.litebox), + pm: PageManager::new(&litebox), futex_manager: FutexManager::new(), - pipes: Pipes::new(&self.litebox), + pipes: Pipes::new(&litebox), net: litebox::sync::Mutex::new(net), boot_time: self.platform.now(), next_thread_id: 2.into(), // start from 2, as 1 is used by the main thread - litebox: self.litebox, + litebox, unix_addr_table: litebox::sync::RwLock::new(syscalls::unix::UnixAddrTable::new()), elf_patch_cache: litebox::sync::Mutex::new(alloc::collections::BTreeMap::new()), }); @@ -273,7 +253,6 @@ impl LinuxShim { /// initial register state. pub fn load_program( &self, - fs: alloc::sync::Arc>, task: litebox_common_linux::TaskParams, path: &str, argv: Vec, @@ -288,7 +267,7 @@ impl LinuxShim { egid, } = task; - let files = syscalls::file::FilesState::new(fs); + let files = syscalls::file::FilesState::new(); files.set_max_fd(syscalls::process::RLIMIT_NOFILE_CUR); let files = Arc::new(files); let credentials = Arc::new(syscalls::process::Credentials { @@ -339,19 +318,8 @@ impl LinuxShim { &self.0.pm } - /// Establish a TCP connection to the given address. - /// - /// Returns a [`transport::ShimTransport`] that can be used as a - /// byte-stream transport (e.g., for a 9P filesystem client). - pub fn tcp_connection( - &self, - addr: core::net::SocketAddr, - ) -> Result, Errno> { - transport::ShimTransport::connect(self.0.clone(), addr) - } - pub fn litebox(&self) -> &LiteBox { - &self.0.litebox + self.0.litebox.as_ref() } /// Returns the platform this shim was built with. @@ -389,50 +357,45 @@ impl LinuxShimProcess { } } -/// Create the default file system with the given in-memory layer and tar data. -fn default_fs( - litebox: &LiteBox, - in_mem: litebox::fs::in_mem::InMem, - tar_data: Cow<'static, [u8]>, -) -> LinuxFS { - litebox::fs::resolver::Resolver::new( - litebox, - litebox::fs::composer::Composer::builder() - .mount_nestable("/", |allocators| { - litebox::fs::overlay::Overlay::::new( - in_mem, - litebox::fs::tar_ro::TarRo::new(tar_data, allocators.next()), - allocators.next(), - ) - }) - .mount("/dev", litebox::fs::devices::Devices::new) - .build() - .unwrap(), - ) -} - // Special override so that `GETFL` can return stdio-specific flags #[derive(Clone)] -pub(crate) struct StdioStatusFlags(litebox::fs::OFlags); +pub(crate) struct StdioStatusFlags(OFlags); impl syscalls::file::FilesState { fn initialize_stdio_in_shared_descriptors_table( &self, global: &GlobalState, - context: &litebox::fs::resolver::Context, + context: &litebox::fs::Context, ) { - use litebox::fs::{Mode, OFlags}; - let stdin = self - .fs - .open(context, "/dev/stdin", OFlags::RDONLY, Mode::empty()) + let stdin = global + .litebox + .open_file( + context, + "/dev/stdin", + FileAccessMode::ReadOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); - let stdout = self - .fs - .open(context, "/dev/stdout", OFlags::WRONLY, Mode::empty()) + let stdout = global + .litebox + .open_file( + context, + "/dev/stdout", + FileAccessMode::WriteOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); - let stderr = self - .fs - .open(context, "/dev/stderr", OFlags::WRONLY, Mode::empty()) + let stderr = global + .litebox + .open_file( + context, + "/dev/stderr", + FileAccessMode::WriteOnly, + FileOpenFlags::NONE, + Mode::empty(), + ) .unwrap(); let mut dt = global.litebox.descriptor_table_mut(); let mut rds = self.raw_descriptor_store.write(); @@ -488,7 +451,7 @@ impl syscalls::file::FilesState { }; } - resolve_fd!(LinuxFS, Fs); + resolve_fd!(litebox::fs::BrokerFile, Fs); resolve_fd!(Network, Network); resolve_fd!(Pipes, Pipes); resolve_fd!(syscalls::eventfd::EventfdSubsystem, Eventfd); @@ -631,7 +594,7 @@ impl Task { self.do_seek( fd, 0, - litebox::fs::SeekWhence::RelativeToCurrentOffset, + SeekWhence::RelativeToCurrentOffset, ) .inspect_err(|e| { match *e { @@ -659,7 +622,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"); @@ -786,7 +749,7 @@ impl Task { oldfd, newfd, flags, - } => syscall!(sys_dup(oldfd, newfd, flags.map(legacy_o_flags))), + } => syscall!(sys_dup(oldfd, newfd, flags)), SyscallRequest::Socket { domain, type_and_flags, @@ -990,15 +953,11 @@ impl Task { pathname, flags, mode, - } => { - let flags = legacy_o_flags(flags); - let mode = legacy_file_mode(mode); - pathname - .to_cstring::() - .map_or(Err(Errno::EFAULT), |path| { - syscall!(sys_openat(dirfd, path, flags, mode)) - }) - } + } => pathname + .to_cstring::() + .map_or(Err(Errno::EFAULT), |path| { + syscall!(sys_openat(dirfd, path, flags, mode)) + }), SyscallRequest::Ftruncate { fd, length } => syscall!(sys_ftruncate(fd, length)), SyscallRequest::Mknodat { dirfd, @@ -1088,7 +1047,6 @@ impl Task { syscall!(sys_eventfd2(initval, flags)) } SyscallRequest::Pipe2 { pipefd, flags } => { - let flags = legacy_o_flags(flags); self.sys_pipe2(flags).and_then(|(read_fd, write_fd)| { pipefd .write_at_offset::(0, read_fd) @@ -1206,7 +1164,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. @@ -1262,14 +1220,11 @@ mod test_utils { impl GlobalState { /// Make a new task with default values for testing. - pub(crate) fn new_test_task( - self: Arc, - fs: alloc::sync::Arc>, - ) -> Task { + pub(crate) fn new_test_task(self: Arc) -> Task { let pid = self .next_thread_id .fetch_add(1, core::sync::atomic::Ordering::Relaxed); - let files = Arc::new(syscalls::file::FilesState::new(fs)); + let files = Arc::new(syscalls::file::FilesState::new()); let credentials = Arc::new(syscalls::process::Credentials { uid: 0, euid: 0, diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index d138f6d6f6..585309ac3e 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -5,11 +5,11 @@ use alloc::{ffi::CString, vec::Vec}; use litebox::{ - fs::{Mode, OFlags}, mm::linux::{CreatePagesFlags, MappingError, PAGE_SIZE}, - utils::{ReinterpretSignedExt, TruncateExt}, + utils::ReinterpretSignedExt, }; -use litebox_common_linux::{MapFlags, errno::Errno, loader::ElfParsedFile}; +use litebox_broker_protocol::fs::FileMode as Mode; +use litebox_common_linux::{MapFlags, OFlags, errno::Errno, loader::ElfParsedFile}; use thiserror::Error; use crate::{ @@ -55,14 +55,17 @@ 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)?; } } } @@ -140,7 +143,7 @@ impl litebox_common_linux::loader::MapMemory for ElfFile prot.flags(), MapFlags::MAP_PRIVATE | MapFlags::MAP_FIXED, self.fd, - offset.trunc(), + usize::try_from(offset).map_err(|_| Errno::EOVERFLOW)?, )?; Ok(()) } @@ -375,181 +378,32 @@ impl From for litebox_common_linux::errno::Errno { #[cfg(test)] mod tests { - extern crate std; - - use alloc::vec::Vec; - use crate::syscalls::tests::TestPlatform; - use litebox::{ - fs::{Mode, OFlags}, - platform::PageManagementProvider, - }; + use litebox::platform::PageManagementProvider; + use litebox_common_linux::loader::MapMemory as _; use super::*; - const ELF_HEADER_SIZE: usize = 64; - const ELF_HEADER_SIZE_U16: u16 = 64; - const PROGRAM_HEADER_SIZE_U16: u16 = 56; - const ET_EXEC: u16 = 2; - const ET_DYN: u16 = 3; - #[cfg(target_arch = "x86_64")] - const EM_HOST: u16 = 62; // EM_X86_64 - #[cfg(target_arch = "aarch64")] - const EM_HOST: u16 = 183; // EM_AARCH64 - const PT_LOAD: u32 = 1; - const PT_INTERP: u32 = 3; - const PF_X: u32 = 1; - const PF_R: u32 = 4; - const EXEC_LOAD_ADDR: u64 = 0x400000; - const INTERP_PATH_OFFSET: usize = 0x200; - const INTERP_PATH: &[u8] = b"/ld.so\0"; - - #[derive(Clone, Copy)] - struct ProgramHeader { - typ: u32, - flags: u32, - offset: u64, - vaddr: u64, - filesz: u64, - memsz: u64, - align: u64, - } - - fn push_u16(buf: &mut Vec, value: u16) { - buf.extend_from_slice(&value.to_le_bytes()); - } - - fn push_u32(buf: &mut Vec, value: u32) { - buf.extend_from_slice(&value.to_le_bytes()); - } - - fn push_u64(buf: &mut Vec, value: u64) { - buf.extend_from_slice(&value.to_le_bytes()); - } - - fn append_elf_header(buf: &mut Vec, elf_type: u16, entry: u64, phnum: u16) { - buf.extend_from_slice(b"\x7fELF"); - buf.extend_from_slice(&[2, 1, 1, 0]); - buf.extend_from_slice(&[0; 8]); - push_u16(buf, elf_type); - push_u16(buf, EM_HOST); - push_u32(buf, 1); - push_u64(buf, entry); - push_u64(buf, u64::from(ELF_HEADER_SIZE_U16)); - push_u64(buf, 0); - push_u32(buf, 0); - push_u16(buf, ELF_HEADER_SIZE_U16); - push_u16(buf, PROGRAM_HEADER_SIZE_U16); - push_u16(buf, phnum); - push_u16(buf, 0); - push_u16(buf, 0); - push_u16(buf, 0); - assert_eq!(buf.len(), ELF_HEADER_SIZE); - } - - fn append_program_header(buf: &mut Vec, ph: ProgramHeader) { - push_u32(buf, ph.typ); - push_u32(buf, ph.flags); - push_u64(buf, ph.offset); - push_u64(buf, ph.vaddr); - push_u64(buf, ph.vaddr); - push_u64(buf, ph.filesz); - push_u64(buf, ph.memsz); - push_u64(buf, ph.align); - } - - fn minimal_elf(elf_type: u16, interp: Option<&[u8]>) -> Vec { - let phnum = if interp.is_some() { 2 } else { 1 }; - let page_size = u64::try_from(PAGE_SIZE).expect("PAGE_SIZE fits u64"); - let entry = if elf_type == ET_EXEC { - EXEC_LOAD_ADDR - } else { - 0 - }; - let mut buf = Vec::new(); - append_elf_header(&mut buf, elf_type, entry, phnum); - append_program_header( - &mut buf, - ProgramHeader { - typ: PT_LOAD, - flags: PF_R | PF_X, - offset: 0, - vaddr: if elf_type == ET_EXEC { - EXEC_LOAD_ADDR - } else { - 0 - }, - filesz: page_size, - memsz: page_size, - align: page_size, - }, - ); - if let Some(interp) = interp { - append_program_header( - &mut buf, - ProgramHeader { - typ: PT_INTERP, - flags: PF_R, - offset: u64::try_from(INTERP_PATH_OFFSET).expect("offset fits u64"), - vaddr: 0, - filesz: u64::try_from(interp.len()).expect("interpreter path length fits u64"), - memsz: u64::try_from(interp.len()).expect("interpreter path length fits u64"), - align: 1, - }, - ); - } - buf.resize(PAGE_SIZE, 0); - if let Some(interp) = interp { - buf[INTERP_PATH_OFFSET..INTERP_PATH_OFFSET + interp.len()].copy_from_slice(interp); - } - buf - } - - fn write_file(task: &Task, path: &str, data: &[u8]) { - let fd = task - .sys_open(path, OFlags::CREAT | OFlags::WRONLY, Mode::RWXU) - .expect("failed to create test ELF"); - let fd = i32::try_from(fd).expect("fd fits i32"); - task.sys_write(fd, data, None) - .expect("failed to write test ELF"); - task.sys_close(fd).expect("failed to close test ELF"); - } - #[test] - #[cfg_attr(target_os = "macos", ignore = "macOS runner supports PIE guests only")] - 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 eb496cc73c..edf55aa9c2 100644 --- a/litebox_shim_linux/src/stdio.rs +++ b/litebox_shim_linux/src/stdio.rs @@ -7,14 +7,13 @@ mod tests { use core::ffi::CStr; - use litebox::fs::{Mode, OFlags}; - use litebox_common_linux::{FcntlArg, FileDescriptorFlags, IoctlArg, Termios, errno::Errno}; - - use crate::{ - UserPtrMut, - syscalls::tests::{init_platform, init_platform_with_broker}, + use litebox_broker_protocol::fs::FileMode as Mode; + use litebox_common_linux::{ + FcntlArg, FileDescriptorFlags, IoctlArg, OFlags, Termios, errno::Errno, }; + use crate::{UserPtrMut, syscalls::tests::init_platform}; + fn termios() -> Termios { Termios { c_iflag: 0, @@ -99,7 +98,7 @@ mod tests { let new_flags = flags | OFlags::NONBLOCK.bits(); task.sys_fcntl( stdin2, - FcntlArg::SETFL(litebox_common_linux::OFlags::from_bits(new_flags).unwrap()), + FcntlArg::SETFL(OFlags::from_bits(new_flags).unwrap()), ) .expect("Failed to set flags"); assert_eq!(new_flags, task.sys_fcntl(stdin2, FcntlArg::GETFL).unwrap()); @@ -127,20 +126,9 @@ mod tests { ); } - #[test] - fn test_stdio_terminal_query_requires_broker() { - let task = init_platform(); - let mut termios = termios(); - - assert_eq!( - task.sys_ioctl(1, IoctlArg::TCGETS(UserPtrMut::from_ptr(&raw mut termios)),), - Err(Errno::EIO) - ); - } - #[test] fn test_stdio_terminal_query_uses_broker() { - let task = init_platform_with_broker(); + let task = init_platform(); let mut termios = termios(); assert_eq!( diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index aa199b8d34..88d97adc33 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -21,7 +21,7 @@ use litebox::{ use litebox_common_linux::{EpollEvent, EpollOp, errno::Errno}; use super::file::FilesState; -use crate::{GlobalState, LinuxFS, ShimPlatform}; +use crate::{GlobalState, ShimPlatform}; pub(crate) struct EpollSubsystem(core::marker::PhantomData); impl FdEnabledSubsystem for EpollSubsystem { @@ -43,7 +43,7 @@ bitflags::bitflags! { pub(crate) enum EpollDescriptor { Eventfd(Arc>>), Epoll(Arc>>), - File(Arc>), + File(Arc), Socket(Arc>), Pipe(Arc>), Unix(Arc>>), @@ -52,7 +52,7 @@ pub(crate) enum EpollDescriptor { impl EpollDescriptor { pub fn try_from(files: &FilesState, raw_fd: usize) -> Result { let rds = files.raw_descriptor_store.read(); - if let Ok(fd) = rds.fd_from_raw_integer::>(raw_fd) { + if let Ok(fd) = rds.fd_from_raw_integer::(raw_fd) { return Ok(EpollDescriptor::File(fd)); } if let Ok(fd) = rds.fd_from_raw_integer::>(raw_fd) { @@ -81,7 +81,7 @@ impl EpollDescriptor { enum DescriptorRef { Eventfd(Weak>>), Epoll(Weak>>), - File(Weak>), + File(Weak), Socket(Weak>), Pipe(Weak>), Unix(Weak>>), @@ -643,7 +643,7 @@ mod test { } fn setup_epoll() -> (crate::Task, EpollFile) { - let task = crate::syscalls::tests::init_platform_with_broker(); + let task = crate::syscalls::tests::init_platform(); let epoll = EpollFile::new(); (task, epoll) @@ -694,15 +694,15 @@ mod test { #[test] fn test_poll() { - let task = crate::syscalls::tests::init_platform_with_broker(); + let task = crate::syscalls::tests::init_platform(); let mut set = super::PollSet::with_capacity(0); let (rfd_u, wfd_u) = task - .sys_pipe2(litebox::fs::OFlags::empty()) + .sys_pipe2(litebox_common_linux::OFlags::empty()) .expect("pipe2 failed"); let rfd = i32::try_from(rfd_u).unwrap(); let wfd = i32::try_from(wfd_u).unwrap(); - let no_fds = FilesState::new(task.files.borrow().fs.clone()); + let no_fds = FilesState::new(); let fds = task.files.borrow().clone(); set.add_fd(rfd, Events::IN); @@ -750,10 +750,10 @@ mod test { #[test] fn test_pselect() { - let task = crate::syscalls::tests::init_platform_with_broker(); + let task = crate::syscalls::tests::init_platform(); let (rfd_u, wfd_u) = task - .sys_pipe2(litebox::fs::OFlags::empty()) + .sys_pipe2(litebox_common_linux::OFlags::empty()) .expect("pipe2 failed"); let rfd = i32::try_from(rfd_u).unwrap(); let wfd = i32::try_from(wfd_u).unwrap(); @@ -790,10 +790,10 @@ mod test { #[test] fn test_pselect_read_hup() { - let task = crate::syscalls::tests::init_platform_with_broker(); + let task = crate::syscalls::tests::init_platform(); let (rfd_u, wfd_u) = task - .sys_pipe2(litebox::fs::OFlags::empty()) + .sys_pipe2(litebox_common_linux::OFlags::empty()) .expect("pipe2 failed"); let rfd = i32::try_from(rfd_u).unwrap(); let wfd = i32::try_from(wfd_u).unwrap(); diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index a3e39721b9..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}; @@ -102,15 +101,15 @@ impl GlobalState { #[cfg(test)] mod tests { - use litebox_common_linux::{EfdFlags, errno::Errno}; + use litebox_common_linux::EfdFlags; #[test] - fn test_eventfd_requires_broker_control() { + fn test_eventfd_uses_broker_control() { let task = crate::syscalls::tests::init_platform(); - - assert!(matches!( - task.global.create_linux_eventfd(0, EfdFlags::NONBLOCK), - Err(Errno::EIO) - )); + let event = task + .global + .create_linux_eventfd(0, EfdFlags::NONBLOCK) + .unwrap(); + drop(event); } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 82c8321e64..ed7198ab1f 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -11,24 +11,24 @@ use alloc::{ use litebox::{ event::{Events, wait::WaitError}, fd::{FdEnabledSubsystem, MetadataError, TypedFd}, - fs::{Mode, OFlags, SeekWhence}, + fs::errors::OpenError, mm::linux::PAGE_SIZE, path, stdio::StdioStream, utils::{ReinterpretSignedExt as _, ReinterpretUnsignedExt as _, TruncateExt as _}, }; -use litebox_broker_protocol::fs::{FileMode, FileNodeInfo, FileStatus, FileUser}; +use litebox_broker_protocol::fs::{ + FileAccessMode, FileMode as Mode, FileOpenFlags, FileSeekWhence as SeekWhence, FileStatus, + FileType, FileUser, +}; use litebox_common_linux::{ AccessFlags, AtFlags, EfdFlags, EpollCreateFlags, FcntlArg, FileDescriptorFlags, FileStat, - InodeType, IoReadVec, IoWriteVec, IoctlArg, Statx, StatxMask, TimeParam, errno::Errno, + InodeType, IoReadVec, IoWriteVec, IoctlArg, OFlags, Statx, StatxMask, TimeParam, errno::Errno, signal::Signal, }; use thiserror::Error; -use crate::{ - FileFd, GlobalState, LinuxFS, ShimPlatform, Task, UserPtr, UserPtrMut, legacy_o_flags, - syscalls::signal, -}; +use crate::{FileFd, GlobalState, ShimPlatform, Task, UserPtr, UserPtrMut, syscalls::signal}; use core::sync::atomic::{AtomicUsize, Ordering}; #[derive(Clone, Copy)] @@ -37,8 +37,8 @@ struct AccessUserInfo { group: u32, } -impl From for AccessUserInfo { - fn from(value: litebox::fs::UserInfo) -> Self { +impl From for AccessUserInfo { + fn from(value: FileUser) -> Self { Self { user: u32::from(value.user), group: u32::from(value.group), @@ -46,48 +46,12 @@ impl From for AccessUserInfo { } } -// Temporary adapters while the shim still executes filesystem operations through `litebox::fs`. -fn protocol_file_status(status: litebox::fs::FileStatus) -> Result { - let litebox::fs::FileStatus { - file_type, - mode, - size, - owner, - node_info, - blksize, - .. - } = status; - Ok(FileStatus { - file_type, - mode: FileMode::from_u32_bits_truncate(mode.bits()), - size: u64::try_from(size).map_err(|_| Errno::EOVERFLOW)?, - owner: FileUser { - user: owner.user, - group: owner.group, - }, - node_info: FileNodeInfo { - dev: u64::try_from(node_info.dev).map_err(|_| Errno::EOVERFLOW)?, - ino: u64::try_from(node_info.ino).map_err(|_| Errno::EOVERFLOW)?, - rdev: node_info - .rdev - .map(|rdev| { - core::num::NonZeroU64::new( - u64::try_from(rdev.get()).map_err(|_| Errno::EOVERFLOW)?, - ) - .ok_or(Errno::EOVERFLOW) - }) - .transpose()?, - }, - blksize: u64::try_from(blksize).map_err(|_| Errno::EOVERFLOW)?, - }) -} - /// Task state shared by `CLONE_FS`. 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 { @@ -102,7 +66,7 @@ impl Clone for FsState { impl FsState { /// Create the state for a task running as `credentials`. pub fn new(credentials: &super::process::Credentials) -> Self { - let user_info = litebox::fs::UserInfo { + let user_info = FileUser { // XXX: Linux ids are 32-bit, but the core litebox file system uses 16-bit ones, so we // may need to widen `UserInfo`. user: u16::try_from(credentials.euid) @@ -110,23 +74,66 @@ impl FsState { group: u16::try_from(credentials.egid) .unwrap_or_else(|_| unimplemented!("{}", credentials.egid)), }; - let mut context = litebox::fs::resolver::Context::new(); + let mut context = litebox::fs::Context::new(); context.set_acting_user(user_info); Self { - umask: (Mode::WGRP | Mode::WOTH).bits().into(), + umask: u32::from((Mode::WGRP | Mode::WOTH).bits()).into(), context: litebox::sync::RwLock::new(context), } } fn umask(&self) -> Mode { - Mode::from_bits_retain(self.umask.load(Ordering::Relaxed)) + Mode::from_u32_bits_truncate(self.umask.load(Ordering::Relaxed)) } } +/// Translate Linux open flags after descriptor-local `O_CLOEXEC` has been removed. +fn file_open_options(flags: OFlags) -> Result<(FileAccessMode, FileOpenFlags), OpenError> { + const SUPPORTED_FLAGS: OFlags = OFlags::CREAT + .union(OFlags::RDONLY) + .union(OFlags::WRONLY) + .union(OFlags::RDWR) + .union(OFlags::TRUNC) + .union(OFlags::NOCTTY) + .union(OFlags::EXCL) + .union(OFlags::DIRECTORY) + .union(OFlags::NONBLOCK) + .union(OFlags::LARGEFILE) + .union(OFlags::NOFOLLOW) + .union(OFlags::APPEND) + .union(OFlags::PATH); + + if flags.intersects(SUPPORTED_FLAGS.complement()) { + unimplemented!("{flags:?}") + } + let access = match flags.bits() & 3 { + 0 => FileAccessMode::ReadOnly, + 1 => FileAccessMode::WriteOnly, + 2 => FileAccessMode::ReadWrite, + _ => return Err(OpenError::AccessNotAllowed), + }; + let mut output = FileOpenFlags::NONE; + for (guest, broker) in [ + (OFlags::CREAT, FileOpenFlags::CREATE), + (OFlags::TRUNC, FileOpenFlags::TRUNCATE), + (OFlags::NOCTTY, FileOpenFlags::NO_CONTROLLING_TERMINAL), + (OFlags::EXCL, FileOpenFlags::EXCLUSIVE), + (OFlags::DIRECTORY, FileOpenFlags::DIRECTORY), + (OFlags::NONBLOCK, FileOpenFlags::NONBLOCKING), + (OFlags::LARGEFILE, FileOpenFlags::LARGE_FILE), + (OFlags::NOFOLLOW, FileOpenFlags::NO_FOLLOW), + (OFlags::APPEND, FileOpenFlags::APPEND), + (OFlags::PATH, FileOpenFlags::PATH), + ] { + if flags.contains(guest) { + output = output.union(broker); + } + } + Ok((access, output)) +} + /// Task state shared by `CLONE_FILES`. pub(crate) struct FilesState { - /// The filesystem implementation, shared across tasks that share file system. - pub(crate) fs: alloc::sync::Arc>, pub(crate) raw_descriptor_store: litebox::sync::RwLock, /// Exclusive upper bound for raw file descriptor values. @@ -134,9 +141,8 @@ pub(crate) struct FilesState { } impl FilesState { - pub(crate) fn new(fs: alloc::sync::Arc>) -> Self { + pub(crate) fn new() -> Self { Self { - fs, raw_descriptor_store: litebox::sync::RwLock::new( litebox::fd::RawDescriptorStorage::new(), ), @@ -207,7 +213,7 @@ impl FilesState { /// A raw fd resolved once into the subsystem that owns it. pub(crate) enum AnyTypedFd { - Fs(alloc::sync::Arc>), + Fs(alloc::sync::Arc), Network(alloc::sync::Arc>>), Pipes(alloc::sync::Arc>>), Eventfd(alloc::sync::Arc>>), @@ -245,7 +251,7 @@ impl AnyTypedFd { } /// The filesystem fd behind this descriptor, or `None` for every other subsystem. - pub(crate) fn as_fs(&self) -> Option<&FileFd> { + pub(crate) fn as_fs(&self) -> Option<&FileFd> { match self { Self::Fs(fd) => Some(fd), _ => None, @@ -253,14 +259,14 @@ impl AnyTypedFd { } /// Like [`Self::as_fs`], but fails with `otherwise` for non-filesystem descriptors. - pub(crate) fn fs_only(&self, otherwise: Errno) -> Result<&FileFd, Errno> { + pub(crate) fn fs_only(&self, otherwise: Errno) -> Result<&FileFd, Errno> { self.as_fs().ok_or(otherwise) } /// Run the handler matching this fd's subsystem. pub(crate) fn dispatch( &self, - fs: impl FnOnce(&FileFd) -> R, + fs: impl FnOnce(&FileFd) -> R, net: impl FnOnce(&TypedFd>) -> R, pipes: impl FnOnce(&TypedFd>) -> R, eventfd: impl FnOnce(&TypedFd>) -> R, @@ -398,7 +404,7 @@ impl Task { path: impl path::Arg, flags: OFlags, mode: Mode, - ) -> Result, Errno> { + ) -> Result { let mode = mode & !self.get_umask(); // TODO: Have the device backend attach stream identity once backends can set descriptor // metadata for newly opened files. @@ -419,12 +425,16 @@ impl Task { } }); let file = { - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files - .fs - .open(&context, &path, flags - OFlags::CLOEXEC, mode) + let path = path + .as_rust_str() + .map_err(litebox::fs::errors::PathError::from)?; + let (access, open_flags) = + file_open_options(flags - OFlags::CLOEXEC).map_err(Errno::from)?; + self.global + .litebox + .open_file(&context, path, access, open_flags, mode) .map_err(Errno::from) }?; if let Some(stream) = stream { @@ -444,12 +454,12 @@ impl Task { pathname: impl path::Arg, flags: OFlags, mode: Mode, - ) -> Result, Errno> { + ) -> Result { let path = self.resolve_path_at(dirfd, pathname)?; self.do_open(path, flags, mode) } - fn insert_raw_file_fd(&self, file: FileFd, flags: OFlags) -> Result { + fn insert_raw_file_fd(&self, file: FileFd, flags: OFlags) -> Result { if flags.contains(OFlags::CLOEXEC) { let None = self .global @@ -462,7 +472,7 @@ impl Task { } let files = self.files.borrow(); let raw_fd = files.insert_raw_fd(file).map_err(|file| { - files.fs.close(&file).unwrap(); + self.global.litebox.close_file(&file).unwrap(); Errno::EMFILE })?; Ok(u32::try_from(raw_fd).unwrap()) @@ -470,13 +480,14 @@ impl Task { /// Handle syscall `umask` pub(crate) fn sys_umask(&self, new_mask: u32) -> Mode { - let new_mask = Mode::from_bits_truncate(new_mask) & (Mode::RWXU | Mode::RWXG | Mode::RWXO); + let new_mask = + Mode::from_u32_bits_truncate(new_mask) & (Mode::RWXU | Mode::RWXG | Mode::RWXO); let old_mask = self .fs .borrow() .umask - .swap(new_mask.bits(), Ordering::Relaxed); - Mode::from_bits_retain(old_mask) + .swap(new_mask.bits().into(), Ordering::Relaxed); + Mode::from_u32_bits_truncate(old_mask) } /// Handle syscall `open` @@ -503,7 +514,12 @@ impl Task { let files = self.files.borrow(); let fd = files.typed_fd(fd)?; fd.dispatch( - |fd| files.fs.truncate(fd, length, false).map_err(Errno::from), + |fd| { + self.global + .litebox + .truncate_file(fd, length, false) + .map_err(Errno::from) + }, |_fd| todo!("net"), |_fd| todo!("pipes"), |_fd| Err(Errno::EINVAL), @@ -531,15 +547,14 @@ impl Task { }; match file_type { InodeType::File => { - let mode = Mode::from_bits_truncate(mode_and_type & !FILE_TYPE_MASK); + let mode = Mode::from_u32_bits_truncate(mode_and_type & !FILE_TYPE_MASK); let file = self.do_openat( dirfd, pathname, OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, mode, )?; - let files = self.files.borrow(); - let _ = files.fs.close(&file); + let _ = self.global.litebox.close_file(&file); } // TODO: Named pipe, socket, block and char files are not supported InodeType::NamedPipe @@ -564,13 +579,18 @@ impl Task { } let path = self.resolve_path_at(dirfd, pathname)?; - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); if flags.contains(AtFlags::AT_REMOVEDIR) { - files.fs.rmdir(&context, path).map_err(Errno::from) + self.global + .litebox + .rmdir_file(&context, path) + .map_err(Errno::from) } else { - files.fs.unlink(&context, path).map_err(Errno::from) + self.global + .litebox + .unlink_file(&context, path) + .map_err(Errno::from) } } @@ -589,15 +609,14 @@ impl Task { buf: &mut [u8], offset: Option, ) -> Result { - let files = self.files.borrow(); // We need to do this cell dance because otherwise Rust can't recognize that the two // closures are mutually exclusive. let buf: core::cell::RefCell<&mut [u8]> = core::cell::RefCell::new(buf); let result = fd.dispatch( |fd| { - files - .fs - .read(fd, &mut buf.borrow_mut(), offset) + self.global + .litebox + .read_file(fd, &mut buf.borrow_mut(), offset) .map_err(Errno::from) }, |fd| { @@ -674,10 +693,14 @@ impl Task { buf: &[u8], offset: Option, ) -> Result { - let files = self.files.borrow(); let is_inet_datagram = core::cell::Cell::new(false); let result = fd.dispatch( - |fd| files.fs.write(fd, buf, offset).map_err(Errno::from), + |fd| { + self.global + .litebox + .write_file(fd, buf, offset) + .map_err(Errno::from) + }, |fd| { espipe_for_non_seekable_offset(offset)?; is_inet_datagram.set(matches!( @@ -755,10 +778,9 @@ impl Task { let rewind = isize::try_from(unread_n).map_err(|_| Errno::EOVERFLOW)?; let fd = in_fd.fs_only(Errno::EINVAL)?; - let files = self.files.borrow(); - files - .fs - .seek(fd, -rewind, SeekWhence::RelativeToCurrentOffset) + self.global + .litebox + .seek_file(fd, -rewind, SeekWhence::RelativeToCurrentOffset) .map(|_| ()) .map_err(Errno::from) } @@ -784,10 +806,8 @@ impl Task { usize::try_from(off).map_err(|_| Errno::EINVAL) }) .transpose()?; - let mut kernel_buf = vec![0u8; count.min(PAGE_SIZE)]; let mut total: usize = 0; - let files = self.files.borrow(); while total < count { let to_read = (count - total).min(kernel_buf.len()); @@ -800,9 +820,10 @@ impl Task { Errno::EINVAL }; let read_result = match typed_in_fd.as_fs() { - Some(fd) => files - .fs - .read(fd, &mut kernel_buf[..to_read], cur_off) + Some(fd) => self + .global + .litebox + .read_file(fd, &mut kernel_buf[..to_read], cur_off) .map_err(Errno::from), None => Err(non_fs_err), }; @@ -883,8 +904,7 @@ impl Task { whence: SeekWhence, ) -> Result { let fd = fd.fs_only(Errno::ESPIPE)?; - let files = self.files.borrow(); - match files.fs.seek(fd, offset, whence) { + match self.global.litebox.seek_file(fd, offset, whence) { Ok(pos) => Ok(pos), Err(litebox::fs::errors::SeekError::NotAFile) => { let base = match whence { @@ -910,12 +930,11 @@ impl Task { fn do_mkdir(&self, pathname: impl path::Arg, mode: Mode) -> Result<(), Errno> { let mode = mode & !self.get_umask(); - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files - .fs - .mkdir(&context, pathname, mode) + self.global + .litebox + .mkdir_file(&context, pathname, mode) .map_err(Errno::from) } @@ -927,11 +946,11 @@ impl Task { mode: u32, ) -> Result<(), Errno> { let pathname = self.resolve_path_at(dirfd, pathname)?; - self.do_mkdir(pathname, Mode::from_bits_retain(mode)) + self.do_mkdir(pathname, Mode::from_u32_bits_truncate(mode)) } pub(crate) fn do_close(&self, raw_fd: usize) -> Result<(), Errno> { - self.do_close_and_replace::>(raw_fd, None) + self.do_close_and_replace::(raw_fd, None) } pub(super) fn remove_and_drop_descriptor(&self, fd: &TypedFd) { @@ -954,7 +973,7 @@ impl Task { let files = self.files.borrow(); let mut rds = files.raw_descriptor_store.write(); let consumed: AnyTypedFd = match rds - .fd_consume_raw_integer::>(raw_fd) + .fd_consume_raw_integer::(raw_fd) { Ok(fd) => AnyTypedFd::Fs(fd), Err(litebox::fd::ErrRawIntFd::NotFound) => { @@ -1006,7 +1025,7 @@ impl Task { if let Ok(raw_fd) = i32::try_from(raw_fd) { self.finalize_elf_patch(raw_fd); } - files.fs.close(&fd).map_err(Errno::from) + self.global.litebox.close_file(&fd).map_err(Errno::from) } AnyTypedFd::Network(fd) => self.global.close_socket(&self.wait_cx(), fd), AnyTypedFd::Pipes(fd) => self.global.close_linux_pipe(&fd), @@ -1439,10 +1458,9 @@ impl Task { caller: AccessUserInfo, ) -> Result<(), Errno> { let status = { - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files.fs.file_status(&context, pathname)? + self.global.litebox.path_file_status(&context, pathname)? }; let owner = status.owner.into(); Self::do_access_mode(status.mode, owner, caller, &mode) @@ -1475,13 +1493,20 @@ impl Task { self.do_access(cwd, mode, caller) } FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { - let stat: FileStat = self.with_typed_fd(fd, |fd| self.do_stat(fd))?; + let files = self.files.borrow(); + let typed_fd = files.typed_fd(fd)?; + if let Some(file) = typed_fd.as_fs() { + let status = self.global.litebox.file_status(file)?; + return Self::do_access_mode(status.mode, status.owner.into(), caller, &mode); + } + drop(files); + let stat: FileStat = self.do_stat(&typed_fd)?; let owner = AccessUserInfo { user: stat.st_uid, group: stat.st_gid, }; Self::do_access_mode( - Mode::from_bits_truncate(stat.st_mode & 0o7777), + Mode::from_u32_bits_truncate(stat.st_mode & 0o7777), owner, caller, &mode, @@ -1568,7 +1593,7 @@ impl Task { pub(crate) fn file_status(&self, fd: i32) -> Result { let files = self.files.borrow(); let fd = files.typed_fd(fd)?; - protocol_file_status(files.fs.fd_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 @@ -1597,14 +1622,10 @@ impl Task { ..Default::default() }; let socket_mode = litebox_common_linux::InodeType::Socket as u32 - | (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits(); - let rw_user_mode = (Mode::RUSR | Mode::WUSR).bits(); - let files = self.files.borrow(); + | u32::from((Mode::RWXU | Mode::RWXG | Mode::RWXO).bits()); + let rw_user_mode = u32::from((Mode::RUSR | Mode::WUSR).bits()); fd.dispatch( - |fd| { - let status = files.fs.fd_file_status(fd)?; - T::try_from(protocol_file_status(status)?) - }, + |fd| T::try_from(self.global.litebox.file_status(fd)?), |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { Ok(T::from(synthetic( @@ -1634,12 +1655,11 @@ impl Task { normalized_path }; let status = { - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - files.fs.file_status(&context, path)? + self.global.litebox.path_file_status(&context, path)? }; - T::try_from(protocol_file_status(status)?) + T::try_from(status) } /// Handle syscall `stat` @@ -1682,10 +1702,9 @@ impl Task { // Take the cwd before locking the context: this lock is not recursive, so a // waiting writer would deadlock a nested read. let cwd = get_cwd(); - let files = self.files.borrow(); let fs = self.fs.borrow(); let context = fs.context.read(); - T::try_from(protocol_file_status(files.fs.file_status(&context, cwd)?)?) + T::try_from(self.global.litebox.path_file_status(&context, cwd)?) } FsPath::Fd(fd) if flags.contains(AtFlags::AT_EMPTY_PATH) => { self.with_typed_fd(fd, |fd| self.do_stat(fd)) @@ -1794,7 +1813,6 @@ impl Task { .bits()) } FcntlArg::SETFL(flags) => { - let flags = legacy_o_flags(flags); let setfl_mask = OFlags::APPEND | OFlags::NONBLOCK | OFlags::NDELAY @@ -1956,7 +1974,6 @@ impl Task { /// Handle syscall `chdir` pub fn sys_chdir(&self, pathname: impl path::Arg) -> Result<(), Errno> { - use litebox::fs::FileType; use litebox::fs::errors::{FileStatusError, PathError}; let fs = self.fs.borrow(); @@ -1977,9 +1994,12 @@ impl Task { // Verify the path exists and is a directory. { - let files = self.files.borrow(); let context = fs.context.read(); - match files.fs.file_status(&context, target.to_string()) { + match self + .global + .litebox + .path_file_status(&context, target.to_string()) + { Ok(status) => { if status.file_type != FileType::Directory { return Err(Errno::ENOTDIR); @@ -2091,13 +2111,12 @@ impl Task { } } - fn is_stdio(&self, fs: &LinuxFS, fd: &FileFd) -> Result { - match fs.fd_file_status(fd) { + fn is_stdio(&self, fs: &litebox::LiteBox, fd: &FileFd) -> Result { + match fs.file_status(fd) { Ok(status) => { // See https://www.kernel.org/doc/Documentation/admin-guide/devices.txt let major = status.node_info.rdev.map_or(0, |v| v.get() >> 8); - Ok((136..=143).contains(&major) - && status.file_type == litebox::fs::FileType::CharacterDevice) + Ok((136..=143).contains(&major) && status.file_type == FileType::CharacterDevice) } Err(litebox::fs::errors::FileStatusError::ClosedFd) => Err(Errno::EBADF), Err(_) => unimplemented!(), @@ -2190,7 +2209,7 @@ impl Task { | IoctlArg::TIOCGWINSZ(..) => { let fd = files.typed_fd(fd)?; let fd = fd.fs_only(Errno::ENOTTY)?; - if !self.is_stdio(&files.fs, fd)? { + if !self.is_stdio(self.global.litebox.as_ref(), fd)? { return Err(Errno::ENOTTY); } let stream = self @@ -2651,7 +2670,7 @@ impl Task { file.dispatch( |fd| { dup(self, &files, fd, close_on_exec, target, |fd| { - let _ = files.fs.close(&fd); + let _ = self.global.litebox.close_file(&fd); }) }, |fd| { @@ -2776,7 +2795,7 @@ impl Task { let mut dir_off = dir_off.0; let mut nbytes = 0; - let mut entries = files.fs.read_dir(file)?; + let mut entries = self.global.litebox.read_file_directory(file)?; entries.sort_by(|a, b| a.name.cmp(&b.name)); for entry in entries.iter().skip(dir_off) { @@ -2792,9 +2811,9 @@ impl Task { break; } let dirent64 = litebox_common_linux::LinuxDirent64 { - ino: entry.ino_info.as_ref().map_or(0, |node_info| node_info.ino) as u64, + ino: entry.ino_info.as_ref().map_or(0, |node_info| node_info.ino), off: dir_off as u64, - len: len.trunc(), + len: u16::try_from(len).map_err(|_| Errno::EOVERFLOW)?, typ: litebox_common_linux::DirentType::from(entry.file_type) as u8, __name: [0; 0], }; @@ -2832,7 +2851,6 @@ mod tests { use super::*; use alloc::string::String; use core::cell::Cell; - use litebox::fs::{Mode, OFlags}; extern crate std; @@ -3022,83 +3040,83 @@ mod tests { #[test] fn getcwd_and_chdir() { - let task = crate::syscalls::tests::init_platform(); + use crate::syscalls::tests::{create_directory, create_file, init_platform}; + + let task = init_platform(); + create_directory(&task, "/test_chdir_dir"); + create_file(&task, "/test_chdir_file", &[]); - // Default CWD is root. + // The default CWD is the root. 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, "/"); - // chdir + getcwd round trip. - task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "/test_chdir_dir", 0o777) - .unwrap(); task.sys_chdir("/test_chdir_dir").unwrap(); let len = task.sys_getcwd(&mut buf).unwrap(); - let cwd = core::str::from_utf8(&buf[..len - 1]).unwrap(); - 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. assert_eq!( task.sys_chdir("/does_not_exist").unwrap_err(), Errno::ENOENT ); + + // An empty path is rejected. assert_eq!(task.sys_chdir("").unwrap_err(), Errno::ENOENT); - // 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. assert_eq!( task.sys_chdir("/test_chdir_file").unwrap_err(), Errno::ENOTDIR ); + // 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]; assert_eq!(task.sys_getcwd(&mut tiny).unwrap_err(), Errno::ERANGE); } #[test] - fn chdir_relative_path() { - let task = crate::syscalls::tests::init_platform(); + fn chdir_normalizes_relative_paths() { + use crate::syscalls::tests::{create_directory, init_platform}; - // 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 task = init_platform(); + create_directory(&task, "/rel_parent"); + create_directory(&task, "/rel_parent/rel_child"); - // 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" + ); } #[test] fn mknodat_regular_file_does_not_consume_fd_limit() { + use crate::syscalls::tests::init_platform; use litebox_common_linux::{Rlimit, RlimitResource}; - let task = crate::syscalls::tests::init_platform(); + let task = init_platform(); let old_limit = task.do_prlimit(RlimitResource::NOFILE, None).unwrap(); task.do_prlimit( RlimitResource::NOFILE, @@ -3108,25 +3126,28 @@ 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!( - task.sys_stat(path).is_ok(), - "mknodat created the file before returning {result:?}" - ); - assert_eq!(result, Ok(())); + for index in 0..32 { + let path = alloc::format!("/mknodat_at_fd_limit_{index}"); + assert_eq!( + task.sys_mknodat( + litebox_common_linux::AT_FDCWD, + &path, + InodeType::File as u32 | u32::from((Mode::RUSR | Mode::WUSR).bits()), + 0, + ), + Ok(()), + "transient broker handles must be closed after creating {path}" + ); + assert!(task.sys_stat(&path).is_ok()); + } } #[test] fn empty_pathnames_return_enoent() { - let task = crate::syscalls::tests::init_platform(); + use crate::syscalls::tests::init_platform; + + let task = init_platform(); assert_eq!( task.sys_open("", OFlags::RDONLY, Mode::empty()) @@ -3153,7 +3174,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(), @@ -3170,32 +3191,25 @@ mod tests { /// Verify every path-taking syscall resolves relative paths after `chdir`. #[test] fn all_path_syscalls_respect_chdir() { + use crate::syscalls::tests::{create_directory, init_platform}; use litebox_common_linux::{AccessFlags, AtFlags}; - let task = crate::syscalls::tests::init_platform(); + let task = init_platform(); + create_directory(&task, "/cwd_test"); - // Set up: mkdir + chdir into /cwd_test/. - task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "/cwd_test", 0o777) - .unwrap(); task.sys_chdir("/cwd_test").unwrap(); - // ── sys_open: create a file via relative path ── let fd = task .sys_open( "file.txt", - litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, + OFlags::CREAT | OFlags::WRONLY, Mode::RUSR | Mode::WUSR, ) .unwrap(); task.sys_close(i32::try_from(fd).unwrap()).unwrap(); - // ── sys_stat: stat the relative file ── task.sys_stat("file.txt").unwrap(); - - // ── sys_lstat: lstat the relative file ── task.sys_lstat("file.txt").unwrap(); - - // ── sys_faccessat: check relative file is accessible ── task.sys_faccessat( litebox_common_linux::AT_FDCWD, "file.txt", @@ -3204,23 +3218,19 @@ mod tests { ) .unwrap(); - // ── create a subdirectory via relative path ── task.sys_mkdirat(litebox_common_linux::AT_FDCWD, "subdir", 0o777) .unwrap(); - task.sys_stat("/cwd_test/subdir").unwrap(); // verify via absolute - // ── sys_openat (AT_FDCWD + relative): open inside the new subdir ── let fd = task .sys_openat( litebox_common_linux::AT_FDCWD, "subdir/inner.txt", - litebox::fs::OFlags::CREAT | litebox::fs::OFlags::WRONLY, + OFlags::CREAT | OFlags::WRONLY, Mode::RUSR | Mode::WUSR, ) .unwrap(); task.sys_close(i32::try_from(fd).unwrap()).unwrap(); - // ── sys_newfstatat (AT_FDCWD + relative) ── task.sys_newfstatat( litebox_common_linux::AT_FDCWD, "subdir/inner.txt", @@ -3228,28 +3238,20 @@ 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 - ); + + assert!(task.sys_stat("/cwd_test/file.txt").is_ok()); + assert_eq!(task.sys_stat("/cwd_test/subdir"), Err(Errno::ENOENT)); } } diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 6f51dab673..ca77a479b2 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -5,13 +5,7 @@ //! Most of these syscalls which are not backed by files are implemented in [`litebox_common_linux::mm`]. use alloc::collections::{BTreeMap, BTreeSet}; -use litebox::{ - mm::linux::{MappingError, PAGE_SIZE, PageRange}, - platform::{ - PageManagementProvider, RawConstPointer, - page_mgmt::{FixedAddressBehavior, MemoryRegionPermissions}, - }, -}; +use litebox::mm::linux::{MappingError, PAGE_SIZE}; use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, errno::Errno}; use crate::ShimPlatform; @@ -24,8 +18,6 @@ use alloc::vec::Vec; use core::ops::Range; #[cfg(target_arch = "aarch64")] use litebox::mm::linux::VmFlags; -#[cfg(target_arch = "aarch64")] -use litebox::utils::ReinterpretUnsignedExt as _; use litebox::utils::TruncateExt as _; use object::elf::{ET_DYN, FileHeader64, PT_LOAD, ProgramHeader64}; use object::endian::LittleEndian; @@ -240,14 +232,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 { @@ -281,98 +267,7 @@ impl Task { Ok(result) } - /// Attempt to create a CoW mapping for a file with static backing data. - /// - /// Returns `Some(result)` if CoW was attempted (success or failure), - /// `None` if CoW is not applicable (fall back to memcpy). - // TODO(jb): does this need to be Option-Result or can it just be Option? - fn try_cow_mmap_file( - &self, - suggested_addr: Option, - len: usize, - prot: &ProtFlags, - flags: &MapFlags, - fd: &AnyTypedFd, - offset: usize, - ) -> Option, MappingError>> { - if !len.is_multiple_of(PAGE_SIZE) { - return None; - } - - let files = self.files.borrow(); - let static_data = files.fs.get_static_backing_data(fd.as_fs()?)?; - - if offset > static_data.len() { - return None; - } - - let available_len = static_data.len().saturating_sub(offset); - if available_len < len { - // Cannot fill full page - return None; - } - - let fixed_behavior = if flags.contains(MapFlags::MAP_FIXED_NOREPLACE) { - FixedAddressBehavior::NoReplace - } else if flags.contains(MapFlags::MAP_FIXED) { - FixedAddressBehavior::Replace - } else { - FixedAddressBehavior::Hint - }; - - let permissions = { - let mut perms = MemoryRegionPermissions::empty(); - perms.set( - MemoryRegionPermissions::READ, - prot.contains(ProtFlags::PROT_READ), - ); - perms.set( - MemoryRegionPermissions::WRITE, - prot.contains(ProtFlags::PROT_WRITE), - ); - perms.set( - MemoryRegionPermissions::EXEC, - prot.contains(ProtFlags::PROT_EXEC), - ); - perms - }; - - // XXX: `try_allocate_cow_pages` and `register_existing_mapping` are not called under a - // unified lock, so there is a theoretical race if two threads concurrently attempt a - // fixed-address mapping with replacement at the same address. In practice this is benign: - // if a program races like this both threads will register the same mapping anyway. Updating - // to a begin/attempt/commit scheme could close this race window entirely. - match <_ as PageManagementProvider<{ PAGE_SIZE }>>::try_allocate_cow_pages( - self.global.platform, - suggested_addr.unwrap_or(0), - &static_data[offset..offset + len], - permissions, - fixed_behavior, - ) { - Ok(ptr) => { - let range = - PageRange::new(ptr.as_usize(), ptr.as_usize().checked_add(len).unwrap()) - .unwrap(); - // SAFETY: ptr is the freshly CoW-mapped region of exactly `len` bytes with - // `permissions`. - unsafe { - self.global.pm.register_existing_mapping( - range, - permissions, - true, - fixed_behavior == FixedAddressBehavior::Replace, - flags.contains(MapFlags::MAP_SHARED), - ) - } - .unwrap(); - Some(Ok(UserPtrMut::from_platform_ptr::(ptr))) - } - Err(_cow_not_supported) => None, - } - } - - /// Fallback mmap implementation using page-by-page memcpy, for files where the CoW attempt - /// fails (either due to lack of support on platform, or non-static-backed data, etc.) + /// Map a file by reading its contents through the filesystem API into allocated pages. fn do_mmap_file_memcpy( &self, suggested_addr: Option, @@ -849,26 +744,25 @@ impl Task { let (code_metadata, trampoline_capacity) = if pre_patched { (None, 0) } else { - let scanned = self.sys_fstat(fd).ok().and_then(|stat| { - let file_size: usize = stat.st_size.reinterpret_as_unsigned().trunc(); + let scanned = self.file_status(fd).ok().and_then(|stat| { + let file_size = usize::try_from(stat.size).ok()?; let word_len = file_size.div_ceil(8); let mut words = u64::new_vec_zeroed(word_len).ok()?; let bytes = zerocopy::IntoBytes::as_mut_bytes(words.as_mut_slice()); - match self.sys_read(fd, &mut bytes[..file_size], Some(0)) { - Ok(n) if n == file_size => { - let metadata = litebox_syscall_rewriter::aarch64::ElfCodeMetadata::parse_aligned_in_place( - &mut words, file_size, - ).ok()?; - let upper_bound = metadata - .trampoline_size_upper_bound( - &zerocopy::IntoBytes::as_bytes(words.as_slice())[..file_size], - crate::aarch64_rewrite_options(), - ) - .ok(); - Some((metadata, upper_bound)) - } - _ => None, - } + self.read_file_exact_at(fd, &mut bytes[..file_size], 0) + .ok()?; + let metadata = + litebox_syscall_rewriter::aarch64::ElfCodeMetadata::parse_aligned_in_place( + &mut words, file_size, + ) + .ok()?; + let upper_bound = metadata + .trampoline_size_upper_bound( + &zerocopy::IntoBytes::as_bytes(words.as_slice())[..file_size], + crate::aarch64_rewrite_options(), + ) + .ok(); + Some((metadata, upper_bound)) }); if let Some((metadata, upper_bound)) = scanned { let (executable_bytes, identified_bytes) = metadata.coverage_bytes(); @@ -1012,25 +906,39 @@ impl Task { true } + fn read_file_exact_at( + &self, + fd: i32, + mut data: &mut [u8], + mut offset: usize, + ) -> Result<(), Errno> { + while !data.is_empty() { + let read = self.sys_read(fd, data, Some(offset))?; + if read == 0 { + return Err(Errno::EIO); + } + offset = offset.checked_add(read).ok_or(Errno::EOVERFLOW)?; + data = &mut data[read..]; + } + Ok(()) + } + /// Check if a file has the LITEBOX trampoline magic at its tail. /// Returns (is_pre_patched, file_offset, vaddr, trampoline_size). fn check_trampoline_magic(&self, fd: i32) -> (bool, u64, u64, u64) { const HEADER_SIZE: usize = 32; // TrampolineHeader64: magic(8) + file_offset(8) + vaddr(8) + size(8) - let Ok(stat) = self.sys_fstat(fd) else { + let Ok(stat) = self.file_status(fd) else { return (false, 0, 0, 0); }; - #[cfg(target_arch = "x86_64")] - let file_size: usize = stat.st_size; - #[cfg(target_arch = "aarch64")] - let file_size: usize = { - // The asm-generic ABI uses signed `st_size`. - stat.st_size.reinterpret_as_unsigned().trunc() + let Some(tail_offset) = stat.size.checked_sub(HEADER_SIZE as u64) else { + return (false, 0, 0, 0); }; - if file_size < HEADER_SIZE { + let Ok(tail_offset) = usize::try_from(tail_offset) else { return (false, 0, 0, 0); - } + }; + let mut tail = [0u8; HEADER_SIZE]; - match self.sys_read(fd, &mut tail, Some(file_size - HEADER_SIZE)) { + match self.sys_read(fd, &mut tail, Some(tail_offset)) { Ok(n) if n == HEADER_SIZE => {} _ => return (false, 0, 0, 0), } @@ -1223,12 +1131,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. @@ -1641,15 +1549,15 @@ impl Task { #[cfg(test)] mod tests { use super::PAGE_SIZE; - use litebox::fs::{Mode, OFlags}; #[cfg(any(target_os = "linux", target_os = "windows"))] use litebox::platform::PageManagementProvider; + use litebox_broker_protocol::fs::FileMode as Mode; #[cfg(any(target_os = "linux", target_os = "windows"))] use litebox_common_linux::MRemapFlags; - use litebox_common_linux::{MapFlags, ProtFlags, errno::Errno}; + use litebox_common_linux::{MapFlags, OFlags, ProtFlags, errno::Errno}; - use crate::syscalls::tests::TestPlatform as Platform; - use crate::{UserPtrMut, syscalls::tests::init_platform}; + use crate::UserPtrMut; + use crate::syscalls::tests::{TestPlatform as Platform, create_file, init_platform}; #[test] fn full_capacity_anywhere_precedes_preferred_one_page() { @@ -1854,14 +1762,14 @@ 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 task = init_platform(); + create_file(&task, "/test.txt", content); + let fd = i32::try_from( + task.sys_open("/test.txt", OFlags::RDONLY, Mode::empty()) + .unwrap(), + ) + .unwrap(); let addr = task .sys_mmap( 0, @@ -2188,14 +2096,14 @@ mod tests { #[test] #[cfg_attr(target_os = "macos", ignore = "assumes 4 KiB host pages")] 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 task = init_platform(); + create_file(&task, "/shared.txt", content); + let fd = i32::try_from( + task.sys_open("/shared.txt", OFlags::RDONLY, Mode::empty()) + .unwrap(), + ) + .unwrap(); // MAP_SHARED with PROT_READ on a file should succeed let addr = task diff --git a/litebox_shim_linux/src/syscalls/mod.rs b/litebox_shim_linux/src/syscalls/mod.rs index 53dd561f44..d6502202f0 100644 --- a/litebox_shim_linux/src/syscalls/mod.rs +++ b/litebox_shim_linux/src/syscalls/mod.rs @@ -15,17 +15,21 @@ pub(crate) mod unix; pub(crate) mod signal; #[cfg(test)] +pub(crate) mod test_broker; +#[cfg(test)] pub(crate) mod tests; macro_rules! common_functions_for_file_status { () => { - pub(crate) fn get_status(&self) -> litebox::fs::OFlags { - litebox::fs::OFlags::from_bits(self.status.load(core::sync::atomic::Ordering::Relaxed)) - .unwrap() - & litebox::fs::OFlags::STATUS_FLAGS_MASK + pub(crate) fn get_status(&self) -> litebox_common_linux::OFlags { + litebox_common_linux::OFlags::from_bits( + self.status.load(core::sync::atomic::Ordering::Relaxed), + ) + .unwrap() + & litebox_common_linux::OFlags::STATUS_FLAGS_MASK } - pub(crate) fn set_status(&self, flag: litebox::fs::OFlags, on: bool) { + pub(crate) fn set_status(&self, flag: litebox_common_linux::OFlags, on: bool) { if on { self.status .fetch_or(flag.bits(), core::sync::atomic::Ordering::Relaxed); diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index 0d3c17754d..57996fe681 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( @@ -3758,16 +3757,19 @@ 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_listen(&server_fd, 1)?; + if let Err(error) = task.do_bind(&server_fd, SocketAddress::Unix(addr)) { + close_socket(task, raw_server_fd); + return Err(error); + } + if let Err(error) = task.do_listen(&server_fd, 1) { + close_socket(task, raw_server_fd); + return Err(error); + } Ok(raw_server_fd) } @@ -3812,14 +3814,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 +3881,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 +3889,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,7 +3937,7 @@ 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); } } @@ -3958,7 +3954,12 @@ 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(); + 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); let result = task.do_connect( @@ -3982,7 +3983,12 @@ mod unix_tests { 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(); + 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); @@ -3996,14 +4002,16 @@ mod unix_tests { close_socket(&task, raw_server_fd); close_socket(&task, raw_client_fd); + task.sys_unlinkat(-1, "/unix_stream_socket_refused.sock", AtFlags::empty()) + .unwrap(); } 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 +4021,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 +4038,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)); } @@ -4105,18 +4111,31 @@ mod unix_tests { let task = init_platform(); for _ in 0..10 { let addr = "/unix_stream_socket_server.sock"; - let raw_server1_fd = - create_unix_server_socket(&task, addr, SockFlags::NONBLOCK).unwrap(); + + 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(); + 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 task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); - let raw_server2_fd = - create_unix_server_socket(&task, addr, SockFlags::NONBLOCK).unwrap(); + 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); task.do_connect( @@ -4150,7 +4169,12 @@ mod unix_tests { 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(); + let err = create_unix_server_socket( + &task, + UnixSocketAddr::Path(addr.to_string()), + SockFlags::empty(), + ) + .unwrap_err(); assert_eq!(err, Errno::EADDRINUSE); task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); @@ -4179,6 +4203,7 @@ mod unix_tests { ) .unwrap_err(); assert_eq!(err, Errno::EADDRINUSE); + close_socket(&task, raw_server_fd2); task.sys_unlinkat(-1, addr, AtFlags::empty()).unwrap(); let raw_server_fd2 = create_unix_socket(&task, SockType::Datagram, SockFlags::empty()); @@ -4387,8 +4412,12 @@ mod unix_tests { fn test_unix_stream_addr() { let task = init_platform(); let server_path = "/unix_stream_sockname.sock"; - let raw_server_fd = - create_unix_server_socket(&task, server_path, SockFlags::empty()).unwrap(); + 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 @@ -4408,7 +4437,7 @@ mod unix_tests { client_addr, SocketAddress::Unix(UnixSocketAddr::Unnamed) )); - + // Connect client to server // Connect client to server task.do_connect( &client_fd, @@ -4480,7 +4509,7 @@ mod unix_tests { client_addr, SocketAddress::Unix(UnixSocketAddr::Unnamed) )); - + // Bind server // Bind server task.do_bind( &server_fd, @@ -4494,7 +4523,7 @@ mod unix_tests { server_local, SocketAddress::Unix(UnixSocketAddr::Path(server_path.to_string())) ); - + // Bind client // Bind client task.do_bind( &client_fd, @@ -4508,7 +4537,7 @@ mod unix_tests { client_local, SocketAddress::Unix(UnixSocketAddr::Path(client_path.to_string())) ); - + // Connect client to server // Connect client to server task.do_connect( &client_fd, diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs index 51b1abbfa7..e04ae0a523 100644 --- a/litebox_shim_linux/src/syscalls/pipe.rs +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -12,10 +12,10 @@ use core::num::NonZero; use litebox::{ event::{IOPollable, wait::WaitContext}, fd::MetadataError, - fs::{Mode, OFlags}, pipes::{Flags, HalfPipeType, PipeFd}, }; -use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; +use litebox_broker_protocol::fs::FileMode as Mode; +use litebox_common_linux::{FileDescriptorFlags, InodeType, OFlags, errno::Errno}; use crate::{GlobalState, ShimPlatform}; @@ -145,7 +145,7 @@ impl GlobalState { HalfPipeType::SenderHalf => Mode::WUSR, HalfPipeType::ReceiverHalf => Mode::RUSR, }; - Ok(read_write_mode.bits() | InodeType::NamedPipe as u32) + Ok(u32::from(read_write_mode.bits()) | InodeType::NamedPipe as u32) } pub(crate) fn with_linux_pipe_iopollable( diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index 47c2cd8a48..3c7c48fbe6 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1408,19 +1408,18 @@ impl Task { let full_path = self.resolve_path(&path)?; let file = self.do_open( full_path, - litebox::fs::OFlags::RDONLY, - litebox::fs::Mode::empty(), + litebox_common_linux::OFlags::RDONLY, + litebox_broker_protocol::fs::FileMode::empty(), )?; let mut header = [0u8; SHEBANG_MAX_LINE]; - let files = self.files.borrow(); - let n = match files.fs.read(&file, &mut header, Some(0)) { + let n = match self.global.litebox.read_file(&file, &mut header, Some(0)) { Ok(n) => n, Err(e) => { - let _ = files.fs.close(&file); + let _ = self.global.litebox.close_file(&file); return Err(Errno::from(e)); } }; - let _ = files.fs.close(&file); + let _ = self.global.litebox.close_file(&file); match parse_shebang(&header[..n]) { Some((interp, opt_arg)) => { diff --git a/litebox_shim_linux/src/syscalls/test_broker.rs b/litebox_shim_linux/src/syscalls/test_broker.rs new file mode 100644 index 0000000000..93df65a578 --- /dev/null +++ b/litebox_shim_linux/src/syscalls/test_broker.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! In-process broker setup for the Linux shim's unit tests. +//! +//! The broker core and its mutable in-memory fs are process-wide. The repository's supported +//! `cargo nextest` runner isolates each test in its own process. + +extern crate std; + +use alloc::{sync::Arc, vec}; +use std::sync::OnceLock; + +use litebox_broker_core::{ + BrokerCore, BrokerCoreLimits, ObjectRights, PolicyEngine, + fs::{in_mem::InitialNode, resolver::Resolver}, + test_support::{TerminalOnlyStdioProvider, TestBrokerCoreBuilder}, +}; +use litebox_broker_host::test_support::InProcessBrokerSetup; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::{ + fs::{FileMode, FileUser}, + stdio::StdioStream, +}; + +use crate::syscalls::tests::TestPlatform; + +const MAX_TEST_BROKER_REFERENCES: usize = 16; + +/// Returns a LiteBox connected to the process-wide test broker. +pub(crate) fn litebox(platform: &'static TestPlatform) -> litebox::LiteBox { + let setup = InProcessBrokerSetup::new(test_broker().clone()); + let readiness = setup.readiness_sink(); + let (broker_local, ()) = BrokerLocal::negotiate(setup, |setup| { + let memory = setup.shared_memory(); + Ok((setup.activate(), memory, ())) + }) + .expect("the test broker must negotiate"); + let litebox = litebox::LiteBox::new_with_broker_local(platform, broker_local); + readiness.attach(litebox.broker_notification_dispatcher()); + litebox +} + +fn test_broker() -> &'static BrokerCore { + static BROKER: OnceLock = OnceLock::new(); + BROKER.get_or_init(|| { + let root = InitialNode::Directory { + mode: FileMode::RWXU | FileMode::RWXG | FileMode::RWXO, + owner: FileUser::ROOT, + }; + let in_mem = + litebox_broker_core::fs::in_mem::InMem::::new_initialized(vec![( + "/", root, + )]); + let fs = litebox_broker_core::fs::composer::Composer::builder() + .mount("/", |_| in_mem) + .mount("/dev", litebox_broker_core::fs::devices::Devices::new) + .build() + .expect("the test filesystem must be valid"); + TestBrokerCoreBuilder::new(PolicyEngine::with_unauthenticated_rights( + ObjectRights::all(), + )) + .with_limits(BrokerCoreLimits::new( + MAX_TEST_BROKER_REFERENCES, + BrokerCoreLimits::DEFAULT.max_total_pipe_capacity, + )) + .with_stdio_provider(Arc::new( + TerminalOnlyStdioProvider::default().with_terminal(StdioStream::Stdout), + )) + .with_file_service(Arc::new(Resolver::::new(fs))) + .build() + .expect("a test process may build only one broker core") + }) +} diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 72f417fc7d..669d2a4a57 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -1,27 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox::fs::{Mode, OFlags}; -use litebox_broker_core::{ - BrokerCore, BrokerSession, CallerCredential, ObjectRights, PolicyEngine, - test_support::{TerminalOnlyStdioProvider, TestBrokerCoreBuilder}, -}; -use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::{ - BROKER_PROTOCOL_VERSION, - message::{ - BrokerHandshakeRequest, BrokerHandshakeResponse, BrokerOperation, BrokerRequest, - BrokerResponse, BrokerResult, PipeRequest, PipeResponse, StdioRequest, StdioResponse, - }, - pipe::{CreatePipeResponse, ReadPipeResponse, WritePipeResponse}, - shared_buffer::{SHARED_BUFFER_LAYOUT, SHARED_BUFFER_POOL_SIZE, SharedBufferSequence}, - stdio::{IsTerminalStdioResponse, StdioStream}, -}; -use litebox_broker_transport::{ - channel::{LocalCallChannel, LocalSetupChannel}, - shared_memory::{SharedBufferPool, SharedMemory, SharedMemoryError}, -}; -use litebox_common_linux::{AtFlags, EfdFlags, FcntlArg, FileDescriptorFlags, errno::Errno}; +use litebox_broker_protocol::fs::FileMode as Mode; +use litebox_common_linux::{AtFlags, FcntlArg, FileDescriptorFlags, OFlags, errno::Errno}; use zerocopy::FromBytes as _; use crate::UserPtrMut; @@ -34,8 +15,6 @@ 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 concrete platform used by the shim's unit tests. /// /// This is selected by the build target so the tests can run against whichever @@ -54,248 +33,33 @@ 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(|| { - TestBrokerCoreBuilder::new(PolicyEngine::with_unauthenticated_rights( - ObjectRights::all(), - )) - .with_stdio_provider(alloc::sync::Arc::new( - TerminalOnlyStdioProvider::default().with_terminal(StdioStream::Stdout), - )) - .build() - .unwrap() - }) -} - +/// Returns a task connected to the process-wide in-memory test broker. #[must_use] pub(crate) fn init_platform() -> crate::Task { let platform = test_platform(); - - init_platform_with_builder(crate::LinuxShimBuilder::new(platform)) -} - -#[must_use] -pub(crate) fn init_platform_with_broker() -> crate::Task { - let platform = test_platform(); - let setup = TestBrokerSetup::new(); - let (broker_local, ()) = BrokerLocal::negotiate(setup, |setup| { - let memory: alloc::sync::Arc = setup.memory.clone(); - Ok((setup.activate(), memory, ())) - }) - .unwrap(); - let litebox = litebox::LiteBox::new_with_broker_local(platform, broker_local); - init_platform_with_builder(crate::LinuxShimBuilder::new_with_litebox(platform, litebox)) -} - -fn init_platform_with_builder( - shim_builder: crate::LinuxShimBuilder, -) -> crate::Task { - let in_mem = litebox::fs::in_mem::InMem::new_initialized([( - "/", - litebox::fs::in_mem::InitialNode::Directory { - mode: Mode::RWXU | Mode::RWXG | Mode::RWXO, - owner: litebox::fs::UserInfo::ROOT, - }, - )]); - let fs = alloc::sync::Arc::new(shim_builder.default_fs(in_mem, TEST_TAR_FILE.into())); - shim_builder.build().0.new_test_task(fs) -} - -struct TestBrokerSetup { - memory: alloc::sync::Arc, - session: BrokerSession, -} - -impl TestBrokerSetup { - fn new() -> Self { - Self { - memory: alloc::sync::Arc::new(TestSharedMemory::new()), - session: test_broker() - .create_session(CallerCredential::Unauthenticated) - .unwrap(), - } - } - - fn activate(self) -> TestBrokerChannel { - let shared_buffers = SharedBufferPool::new(self.memory, SHARED_BUFFER_LAYOUT).unwrap(); - TestBrokerChannel { - session: self.session, - shared_buffers, - } - } -} - -impl LocalSetupChannel for TestBrokerSetup { - type Error = core::convert::Infallible; - - fn send_handshake_request( - &mut self, - request: &BrokerHandshakeRequest, - ) -> core::result::Result<(), Self::Error> { - assert_eq!(request.protocol_version, BROKER_PROTOCOL_VERSION); - Ok(()) - } - - fn recv_handshake_response( - &mut self, - ) -> core::result::Result, Self::Error> { - Ok(Some(BrokerHandshakeResponse::Negotiated { - broker_protocol_version: BROKER_PROTOCOL_VERSION, - })) - } -} - -struct TestBrokerChannel { - session: BrokerSession, - shared_buffers: SharedBufferPool>, -} - -impl TestBrokerChannel { - fn write_shared_buffer(&self, buffer: SharedBufferSequence, data: &[u8]) { - let mut offset = 0; - for descriptor in buffer.descriptors(SHARED_BUFFER_LAYOUT).unwrap() { - if offset == data.len() { - break; - } - let length = (data.len() - offset).min(descriptor.length as usize); - let end = offset + length; - self.shared_buffers - .write(descriptor.slot_index, &data[offset..end]) - .expect("broker write must use a valid shared buffer"); - offset = end; - } - assert_eq!(offset, data.len()); - } - - fn read_shared_buffer(&self, buffer: SharedBufferSequence) -> std::vec::Vec { - let mut data = std::vec![0; buffer.length() as usize]; - let mut offset = 0; - for descriptor in buffer.descriptors(SHARED_BUFFER_LAYOUT).unwrap() { - let end = offset + descriptor.length as usize; - self.shared_buffers - .read(descriptor.slot_index, &mut data[offset..end]) - .expect("broker read must use a valid shared buffer"); - offset = end; - } - data - } - - 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.write_shared_buffer(request.buffer, &data); - Ok(BrokerResult::Pipe(PipeResponse::Read(ReadPipeResponse { - read: u32::try_from(data.len()).unwrap(), - }))) - } - BrokerOperation::Pipe(PipeRequest::Write(request)) => { - let data = self.read_shared_buffer(request.buffer); - litebox_broker_core::pipe::write(&self.session, request.handle, &data).map( - |written| { - BrokerResult::Pipe(PipeResponse::Write(WritePipeResponse { - written: u32::try_from(written).unwrap(), - })) - }, - ) - } - BrokerOperation::Stdio(StdioRequest::IsTerminal(request)) => { - litebox_broker_core::stdio::is_terminal(&self.session, request.stream).map( - |is_terminal| { - BrokerResult::Stdio(StdioResponse::IsTerminal(IsTerminalStdioResponse { - is_terminal, - })) - }, - ) - } - operation => panic!("unexpected pipe test broker operation: {operation:?}"), - } - } -} - -impl LocalCallChannel for TestBrokerChannel { - type Error = core::convert::Infallible; - - fn call(&self, request: BrokerRequest) -> core::result::Result { - let result = self - .execute(request.operation) - .unwrap_or_else(|error| BrokerResult::Error(error.into())); - Ok(BrokerResponse { - request_id: request.request_id, - result, - }) - } + let litebox = crate::syscalls::test_broker::litebox(platform); + let shim_builder = crate::LinuxShimBuilder::new_with_litebox(platform, litebox); + shim_builder.build().0.new_test_task() } -struct TestSharedMemory(std::sync::Mutex>); - -impl TestSharedMemory { - fn new() -> Self { - Self(std::sync::Mutex::new(std::vec![ - 0; - SHARED_BUFFER_POOL_SIZE - ])) - } +pub(crate) fn create_directory(task: &crate::Task, path: &str) { + task.sys_mkdirat(litebox_common_linux::AT_FDCWD, path, 0o777) + .expect("the test directory must be created"); } -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(()) +pub(crate) fn create_file(task: &crate::Task, path: &str, data: &[u8]) { + let fd = task + .sys_open( + path, + OFlags::CREAT | OFlags::EXCL | OFlags::WRONLY, + Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::ROTH, + ) + .expect("the test file must be created"); + let fd = i32::try_from(fd).unwrap(); + if !data.is_empty() { + assert_eq!(task.sys_write(fd, data, None), Ok(data.len())); } + task.sys_close(fd).expect("the test file must close"); } #[cfg(target_arch = "x86_64")] @@ -389,7 +153,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!( @@ -404,8 +168,7 @@ fn test_fcntl() { assert_eq!(task.sys_fcntl(fd, FcntlArg::GETFD).unwrap(), 0); // OFlags::RDWR should be ignored - task.sys_fcntl(fd, FcntlArg::SETFL(litebox_common_linux::OFlags::RDWR)) - .unwrap(); + task.sys_fcntl(fd, FcntlArg::SETFL(OFlags::RDWR)).unwrap(); assert_eq!(task.sys_fcntl(fd, FcntlArg::GETFL).unwrap(), flags2.bits()); }; @@ -418,16 +181,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()) @@ -449,16 +202,9 @@ fn test_fcntl() { assert_eq!(duplicated, min_fd); } -#[test] -fn test_pipe2_requires_broker() { - let task = init_platform(); - - assert_eq!(task.sys_pipe2(OFlags::empty()), Err(Errno::EIO)); -} - #[test] fn test_pipe2_race_with_concurrent_close() { - let task = init_platform_with_broker(); + let task = init_platform(); task.files.borrow().set_max_fd(4); let stop = alloc::sync::Arc::new(core::sync::atomic::AtomicBool::new(false)); @@ -517,25 +263,8 @@ 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"); + create_file(&task, "/test_file1.txt", &[]); + create_file(&task, "/test_file2.txt", &[]); // Open the root directory for testing let dir_fd = task @@ -605,15 +334,7 @@ fn test_getdent64() { entry_names.sort(); assert_eq!( entry_names, - alloc::vec![ - ".", - "..", - "bar", - "dev", - "foo", - "test_file1.txt", - "test_file2.txt" - ] + alloc::vec![".", "..", "dev", "test_file1.txt", "test_file2.txt"] ); // Verify that our test files have the correct type (regular file) @@ -771,15 +492,7 @@ fn test_getdent64() { all_entries.sort(); assert_eq!( all_entries, - alloc::vec![ - ".", - "..", - "bar", - "dev", - "foo", - "test_file1.txt", - "test_file2.txt" - ] + alloc::vec![".", "..", "dev", "test_file1.txt", "test_file2.txt"] ); } @@ -789,7 +502,7 @@ fn test_umask_behavior() { // 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 + let _ = task.sys_umask(u32::from(orig)); // restore original // We expect the default (from implementation) to be 0o022. assert_eq!(orig, 0o022, "Default umask should be 022 (got {orig:03o})"); @@ -817,8 +530,12 @@ fn test_umask_behavior() { // 3. Create a directory with mode 0o777; with umask 0o077 should become 0o700. let dir_mode = (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits(); let test_dir = "/umask_rs_test_dir"; - task.sys_mkdirat(litebox_common_linux::AT_FDCWD, test_dir, dir_mode) - .expect("Failed to create test directory"); + task.sys_mkdirat( + litebox_common_linux::AT_FDCWD, + test_dir, + u32::from(dir_mode), + ) + .expect("Failed to create directory"); let stat_dir = task .sys_stat(test_dir) @@ -839,14 +556,14 @@ fn test_umask_behavior() { "Only low 9 bits should be retained (expected 777)" ); // Restore to original - let _ = task.sys_umask(orig); + let _ = task.sys_umask(u32::from(orig)); } #[test] fn test_rlimit_nofile() { use litebox_common_linux::{Rlimit, RlimitResource, errno::Errno}; - let task = crate::syscalls::tests::init_platform(); + let task = init_platform(); // 1. Get the current NOFILE limit. let cur_lim = task @@ -887,11 +604,23 @@ fn test_rlimit_nofile() { .expect_err("dup should fail due to new cur limit"), Errno::EMFILE, ); - assert_eq!( - task.sys_open("/prlimit_file", OFlags::CREAT | OFlags::RDONLY, Mode::RWXU) - .expect_err("open should fail due to new cur limit"), - Errno::EMFILE, - ); + for _ in 0..32 { + assert_eq!( + task.sys_open("/prlimit_file", OFlags::CREAT | OFlags::RDONLY, Mode::RWXU) + .expect_err("open should fail due to new cur limit"), + Errno::EMFILE, + "a failed guest-fd allocation must close its transient broker handle" + ); + } + task.do_prlimit(RlimitResource::NOFILE, Some(cur_lim)) + .expect("restoring the NOFILE limit must succeed"); + task.sys_close(i32::try_from(probe_fd).unwrap()).unwrap(); + task.sys_unlinkat( + litebox_common_linux::AT_FDCWD, + "/prlimit_file", + AtFlags::empty(), + ) + .expect("the file created before descriptor allocation failed must remain removable"); } #[test] @@ -920,8 +649,12 @@ fn test_unlinkat() { // 2. Create a directory and attempt to unlink without AT_REMOVEDIR -> EISDIR. let dir_path = "/unlink_dir"; let dir_mode = (Mode::RWXU | Mode::RWXG | Mode::RWXO).bits(); - task.sys_mkdirat(litebox_common_linux::AT_FDCWD, dir_path, dir_mode) - .expect("Failed to create directory"); + task.sys_mkdirat( + litebox_common_linux::AT_FDCWD, + dir_path, + u32::from(dir_mode), + ) + .expect("Failed to create directory"); assert_eq!( task.sys_unlinkat(0, dir_path, AtFlags::empty()), Err(Errno::EISDIR), @@ -930,8 +663,12 @@ fn test_unlinkat() { // 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"); + task.sys_mkdirat( + litebox_common_linux::AT_FDCWD, + nonempty_dir, + u32::from(dir_mode), + ) + .expect("Failed to create non-empty directory"); let inner_file_fd = task .sys_open( "/unlink_dir_nonempty/inner.txt", @@ -969,8 +706,12 @@ fn test_unlinkat() { // 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_mkdirat( + litebox_common_linux::AT_FDCWD, + empty_dir2, + u32::from(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!( diff --git a/litebox_shim_linux/src/syscalls/unix.rs b/litebox_shim_linux/src/syscalls/unix.rs index ca4200cb15..74ef37c273 100644 --- a/litebox_shim_linux/src/syscalls/unix.rs +++ b/litebox_shim_linux/src/syscalls/unix.rs @@ -21,17 +21,18 @@ use litebox::{ wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, - fs::{Mode, OFlags, errors::OpenError}, + fs::errors::OpenError, sync::{Mutex, RwLock}, utils::TruncateExt as _, }; +use litebox_broker_protocol::fs::{FileAccessMode, FileMode as Mode, FileOpenFlags}; use litebox_common_linux::{ - IpOption, ReceiveFlags, SendFlags, ShutdownHow, SockFlags, SockType, SocketOption, + IpOption, OFlags, ReceiveFlags, SendFlags, ShutdownHow, SockFlags, SockType, SocketOption, SocketOptionName, errno::Errno, }; use crate::{ - FileFd, GlobalState, LinuxFS, ShimPlatform, Task, UserPtr, UserPtrMut, + FileFd, GlobalState, ShimPlatform, Task, UserPtr, UserPtrMut, channel::{Channel, ReadEnd, WriteEnd}, syscalls::net::{SocketOptionValue, SocketOptions}, }; @@ -70,7 +71,7 @@ pub(crate) enum UnixSocketAddr { /// the socket file remains accessible. The file is automatically closed /// when this structure is dropped. enum UnixBoundSocketAddr { - Path((String, FileFd, Arc>)), + Path((String, FileFd, Arc>)), Abstract(Vec), } @@ -112,20 +113,20 @@ impl UnixSocketAddr { let flags = if is_server { // create the socket file if not exists; // use O_EXCL to ensure exclusive creation - OFlags::CREAT | OFlags::EXCL | OFlags::RDWR + FileOpenFlags::CREATE | FileOpenFlags::EXCLUSIVE } else { - OFlags::RDWR + FileOpenFlags::NONE }; // TODO: extend fs to support creating sock file (i.e., with type `InodeType::Socket`) let file = { - let files = task.files.borrow(); let fs = task.fs.borrow(); let context = fs.context.read(); - files - .fs - .open( + task.global + .litebox + .open_file( &context, path.as_str(), + FileAccessMode::ReadWrite, flags, Mode::RWXU | Mode::RGRP | Mode::XGRP | Mode::ROTH | Mode::XOTH, ) @@ -137,7 +138,7 @@ impl UnixSocketAddr { Ok(UnixBoundSocketAddr::Path(( path, file, - task.files.borrow().fs.clone(), + Arc::clone(&task.global.litebox), ))) } UnixSocketAddr::Abstract(data) => { @@ -174,7 +175,7 @@ impl Drop for UnixBoundSocketAddr { fn drop(&mut self) { match self { Self::Path((_, file, fs)) => { - let _ = fs.close(file); + let _ = fs.close_file(file); } Self::Abstract(_) => {} } From c4bba66f6926080edf99ec3731f9029bdd45a315 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Sun, 13 Sep 2026 06:30:06 -0700 Subject: [PATCH 2/8] Restore Windows broker platform CI coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e95d9fd7e..ba1a3594ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -248,6 +248,7 @@ jobs: -p litebox_broker_local -p litebox_broker_host -p litebox_broker_transport_windows_userland + -p litebox_broker_platform_windows_userland -p litebox_platform_windows_userland -p litebox_shim_linux -p litebox_shim_windows From 13f0a187899baf47af2ba42c56d0f6553988707b Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 14 Sep 2026 11:43:28 -0700 Subject: [PATCH 3/8] Fix Linux file migration regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/mm/linux.rs | 2 + litebox/src/mm/mod.rs | 40 ++++- litebox_common_linux/src/errno/mod.rs | 1 + litebox_common_linux/src/mm.rs | 42 ++--- .../src/lib.rs | 165 +----------------- .../tests/loader.rs | 4 + .../tests/runner.rs | 17 ++ .../tests/runner/gates.rs | 4 + litebox_shim_linux/src/lib.rs | 47 ++--- litebox_shim_linux/src/loader/elf.rs | 161 +++++++++++++++-- litebox_shim_linux/src/syscalls/file.rs | 8 +- litebox_shim_linux/src/syscalls/mm.rs | 77 +++++++- 12 files changed, 325 insertions(+), 243 deletions(-) diff --git a/litebox/src/mm/linux.rs b/litebox/src/mm/linux.rs index 0599f7995b..68101d8ef4 100644 --- a/litebox/src/mm/linux.rs +++ b/litebox/src/mm/linux.rs @@ -1036,6 +1036,8 @@ pub enum MappingError { #[error("mapping failed: {0}")] MapError(#[from] crate::platform::page_mgmt::AllocationError), + #[error("failed to apply mapping permissions: {0}")] + ProtectError(#[from] VmemProtectError), } /// Enable [`super::PageManager`] to handle page faults if its platform implements this trait diff --git a/litebox/src/mm/mod.rs b/litebox/src/mm/mod.rs index 119e6743a7..a8bee74399 100644 --- a/litebox/src/mm/mod.rs +++ b/litebox/src/mm/mod.rs @@ -98,13 +98,49 @@ where if before_perms != after_perms { let range = PageRange::new(addr.as_usize(), addr.as_usize() + length.as_usize()).unwrap(); - // `protect` should succeed, as we just created the mapping. let mut vmem = self.vmem.write(); - unsafe { vmem.protect_mapping(range, after_perms) }.expect("failed to protect mapping"); + if let Err(error) = unsafe { vmem.protect_mapping(range, after_perms) } { + unsafe { vmem.remove_mapping(range) } + .expect("failed to remove mapping after permission update failure"); + return Err(MappingError::ProtectError(error)); + } } Ok(addr) } + /// Create pages with the requested final permissions. + /// + /// The pages are temporarily readable and writable while `op` initializes + /// them, then changed to `permissions` before this function returns. + /// + /// # Safety + /// + /// If the suggested start address is given and [`CreatePagesFlags::FIXED_ADDR`] is set, + /// the kernel uses it directly without checking if it is available, causing overlapping + /// mappings to be unmapped. Caller must ensure any overlapping mappings are not used by any other. + pub unsafe fn create_pages_with_permissions( + &self, + suggested_address: Option>, + length: NonZeroPageSize, + flags: CreatePagesFlags, + permissions: MemoryRegionPermissions, + op: F, + ) -> Result, MappingError> + where + F: FnOnce(Platform::RawMutPointer) -> Result, + { + unsafe { + self.create_pages( + suggested_address, + length, + flags, + MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE, + permissions, + op, + ) + } + } + /// Create readable and executable pages. /// /// `suggested_address` is the hint address for where to create the pages if it is not `None`. diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index c9e662a6b5..fe1e6367c9 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -288,6 +288,7 @@ impl From for Errno { litebox::mm::linux::MappingError::NotAFile => Errno::EISDIR, litebox::mm::linux::MappingError::NotForReading => Errno::EACCES, litebox::mm::linux::MappingError::MapError(e) => e.into(), + litebox::mm::linux::MappingError::ProtectError(e) => e.into(), _ => unimplemented!(), } } diff --git a/litebox_common_linux/src/mm.rs b/litebox_common_linux/src/mm.rs index 9fe3c332d1..3e24e81d22 100644 --- a/litebox_common_linux/src/mm.rs +++ b/litebox_common_linux/src/mm.rs @@ -7,7 +7,7 @@ use litebox::{ mm::linux::{ CreatePagesFlags, MappingError, NonZeroAddress, NonZeroPageSize, PAGE_SIZE, VmemUnmapError, }, - platform::page_mgmt::DeallocationError, + platform::page_mgmt::{DeallocationError, MemoryRegionPermissions}, }; use crate::{MRemapFlags, MapFlags, ProtFlags, UserPtrMut, errno::Errno}; @@ -59,31 +59,21 @@ pub fn do_mmap< None => None, }; let length = NonZeroPageSize::new(len).ok_or(MappingError::UnAligned)?; - match prot { - ProtFlags::PROT_READ_EXEC => unsafe { - pm.create_executable_pages(suggested_addr, length, flags, op) - }, - ProtFlags::PROT_READ_WRITE => unsafe { - pm.create_writable_pages(suggested_addr, length, flags, op) - }, - ProtFlags::PROT_READ => unsafe { - pm.create_readable_pages(suggested_addr, length, flags, op) - }, - ProtFlags::PROT_NONE => unsafe { - pm.create_inaccessible_pages(suggested_addr, length, flags, op) - }, - _ => { - #[cfg(debug_assertions)] - todo!("Unsupported prot flags {:?}", prot); - // TODO: create inaccessible pages for now. Creating mapping - // for both executable and writable might be needed for JIT. - #[cfg(not(debug_assertions))] - unsafe { - pm.create_inaccessible_pages(suggested_addr, length, flags, op) - } - } - } - .map(UserPtrMut::from_platform_ptr::) + let mut permissions = MemoryRegionPermissions::empty(); + permissions.set( + MemoryRegionPermissions::READ, + prot.contains(ProtFlags::PROT_READ), + ); + permissions.set( + MemoryRegionPermissions::WRITE, + prot.contains(ProtFlags::PROT_WRITE), + ); + permissions.set( + MemoryRegionPermissions::EXEC, + prot.contains(ProtFlags::PROT_EXEC), + ); + unsafe { pm.create_pages_with_permissions(suggested_addr, length, flags, permissions, op) } + .map(UserPtrMut::from_platform_ptr::) } /// Handle syscall `munmap` diff --git a/litebox_runner_linux_on_macos_userland/src/lib.rs b/litebox_runner_linux_on_macos_userland/src/lib.rs index 9f84262355..91119bd627 100644 --- a/litebox_runner_linux_on_macos_userland/src/lib.rs +++ b/litebox_runner_linux_on_macos_userland/src/lib.rs @@ -4,23 +4,12 @@ //! Run AArch64 Linux PIE programs on an AArch64 macOS host. #![cfg(all(target_os = "macos", target_arch = "aarch64"))] -use anyhow::{Context as _, Result, bail}; +use anyhow::{Result, bail}; use clap::Parser; -use litebox::fs::{ - Mode, UserInfo, - in_mem::{InMem, InitialNode}, -}; -use litebox_platform_macos_userland::MacosUserland as Platform; -use std::ffi::CString; -use std::os::unix::fs::MetadataExt as _; use std::path::PathBuf; -use std::sync::Arc; - -const DEFAULT_GUEST_UID: u16 = 1000; -const DEFAULT_GUEST_GID: u16 = 1000; #[derive(Parser, Debug)] -#[command(about = "Run AArch64 Linux PIE programs on an AArch64 macOS host")] +#[command(about = "AArch64 Linux runner for macOS; broker support is required")] pub struct CliArgs { /// Program and its arguments; host path unless --program-from-tar is set. #[arg(required = true, trailing_var_arg = true, value_hint = clap::ValueHint::CommandWithArguments)] @@ -43,10 +32,7 @@ pub struct CliArgs { pub program_from_tar: bool, } -/// Load and run a Linux program. -/// -/// # Panics -/// Unsupported guest operations may still panic in the Linux shim. +/// Returns an error until the macOS runner can connect to a broker. pub fn run(cli_args: CliArgs) -> Result { tracing_subscriber::fmt() .with_timer(tracing_subscriber::fmt::time::uptime()) @@ -58,147 +44,6 @@ pub fn run(cli_args: CliArgs) -> Result { ) .init(); - let program = cli_args - .program_and_arguments - .first() - .context("missing program path")?; - let prog = if cli_args.program_from_tar { - if !program.starts_with('/') { - bail!("--program-from-tar requires an absolute guest path"); - } - PathBuf::from(program) - } else { - std::path::absolute(program)? - }; - let prog_path = prog.to_str().context("program path must be UTF-8")?; - - let (ancestor_modes_and_users, prog_data) = if cli_args.program_from_tar { - (Vec::new(), None) - } else { - let modes = prog - .ancestors() - .collect::>() - .into_iter() - .rev() - .skip(1) - .map(|path| { - let metadata = path - .metadata() - .with_context(|| format!("reading metadata for {}", path.display()))?; - Ok(( - Mode::from_bits(metadata.mode()).context("unsupported file mode")?, - metadata.uid(), - )) - }) - .collect::>>()?; - let data = std::fs::read(&prog).with_context(|| format!("reading {}", prog.display()))?; - (modes, Some(data)) - }; - let tar_data = if let Some(tar_file) = &cli_args.initial_files { - if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { - bail!("Expected a .tar file, found {}", tar_file.display()); - } - std::fs::read(tar_file).with_context(|| format!("reading {}", tar_file.display()))? - } else { - litebox::fs::tar_ro::EMPTY_TAR_FILE.to_vec() - }; - - let platform = Platform::new(); - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); - let task_params = litebox_common_linux::TaskParams { - pid: 1, - ppid: 0, - uid: u32::from(DEFAULT_GUEST_UID), - euid: u32::from(DEFAULT_GUEST_UID), - gid: u32::from(DEFAULT_GUEST_GID), - egid: u32::from(DEFAULT_GUEST_GID), - }; - let initial_file_system = { - let owner_of = |parent_host_user: u32, host_user: u32| { - if parent_host_user == 0 && host_user == 0 { - UserInfo::ROOT - } else { - UserInfo { - user: DEFAULT_GUEST_UID, - group: DEFAULT_GUEST_GID, - } - } - }; - let mut entries = Vec::new(); - if let Some(prog_data) = prog_data { - let mut prev_user = 0; - for (path, &(mode, user)) in prog - .ancestors() - .skip(1) - .collect::>() - .into_iter() - .rev() - .skip(1) - .zip(&ancestor_modes_and_users) - { - entries.push(( - path.to_str().context("non-UTF-8 ancestor")?.to_owned(), - InitialNode::Directory { - mode, - owner: owner_of(prev_user, user), - }, - )); - prev_user = user; - } - let &(mode, user) = ancestor_modes_and_users - .last() - .context("program path has no ancestors")?; - entries.push(( - prog_path.to_owned(), - InitialNode::File { - mode, - owner: owner_of(prev_user, user), - data: prog_data.into(), - }, - )); - } - let tmp_mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; - if let Some((_, node)) = entries.iter_mut().find(|(path, _)| path == "/tmp") { - let InitialNode::Directory { mode, .. } = node else { - unreachable!() - }; - *mode = tmp_mode; - } else { - entries.push(( - "/tmp".to_owned(), - InitialNode::Directory { - mode: tmp_mode, - owner: UserInfo::ROOT, - }, - )); - } - shim_builder.default_fs(InMem::new_initialized(entries), tar_data.into()) - }; - let initial_file_system = Arc::new(initial_file_system); - let shim = shim_builder.build(); - - let argv = cli_args - .program_and_arguments - .iter() - .map(|value| CString::new(value.as_bytes())) - .collect::, _>>()?; - let mut environment = cli_args.environment_variables; - if cli_args.forward_environment_variables { - environment.extend(std::env::vars().map(|(key, value)| format!("{key}={value}"))); - } - let envp = environment - .iter() - .map(|value| CString::new(value.as_bytes())) - .collect::, _>>()?; - let program = shim - .load_program(initial_file_system, task_params, prog_path, argv, envp) - .context("loading Linux ELF (requires a PIE and 16 KiB-compatible LOAD segments)")?; - // SAFETY: the shim loader supplies valid initial guest code and stack mappings. - unsafe { - litebox_platform_macos_userland::run_thread( - program.entrypoints, - &mut litebox_common_linux::PtRegs::default(), - ); - } - Ok(program.process.wait_for_unix_shell_exit_code()) + let _ = cli_args; + bail!("filesystem startup on macOS requires broker support") } diff --git a/litebox_runner_linux_on_macos_userland/tests/loader.rs b/litebox_runner_linux_on_macos_userland/tests/loader.rs index 2cd571a223..23e9fe16b7 100644 --- a/litebox_runner_linux_on_macos_userland/tests/loader.rs +++ b/litebox_runner_linux_on_macos_userland/tests/loader.rs @@ -74,21 +74,25 @@ fn run_program(name: &str, aot: bool) { } #[test] +#[ignore = "macOS runner requires broker support"] fn test_load_exec_dynamic() { run_program("hello_world_dyn", false); } #[test] +#[ignore = "macOS runner requires broker support"] fn test_load_exec_dynamic_pthreads() { run_program("hello_thread", false); } #[test] +#[ignore = "macOS runner requires broker support"] fn test_syscall_rewriter() { run_program("hello_world_dyn", true); } #[test] +#[ignore = "macOS runner requires broker support"] fn test_syscall_rewriter_pthreads() { run_program("hello_thread", true); } diff --git a/litebox_runner_linux_on_macos_userland/tests/runner.rs b/litebox_runner_linux_on_macos_userland/tests/runner.rs index f1e4a135bb..04b82ad2bc 100644 --- a/litebox_runner_linux_on_macos_userland/tests/runner.rs +++ b/litebox_runner_linux_on_macos_userland/tests/runner.rs @@ -107,6 +107,18 @@ fn phdr( const EXIT_42: &[u32] = &[0xd2800540, 0xd2800ba8, 0xd4000001]; // x0=42; x8=exit; svc #0 #[test] +fn filesystem_startup_fails_closed_without_broker_support() { + let fixture = Fixture::new(); + let output = fixture.run(&[]); + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr) + .contains("filesystem startup on macOS requires broker support") + ); +} + +#[test] +#[ignore = "macOS runner requires broker support"] fn bad_syscall_pointer_returns_efault_without_host_crash() { let fixture = Fixture::new(); let code = [ @@ -130,6 +142,7 @@ fn bad_syscall_pointer_returns_efault_without_host_crash() { } #[test] +#[ignore = "macOS runner requires broker support"] fn guest_memory_fault_terminates_with_linux_status() { let fixture = Fixture::new(); let code = [0xd2800000, 0xf9400000]; // mov x0, #0; ldr x0, [x0] @@ -145,6 +158,7 @@ fn guest_memory_fault_terminates_with_linux_status() { } #[test] +#[ignore = "macOS runner requires broker support"] fn guest_instruction_faults_deliver_sigill() { for (name, code) in [ ("undefined instruction", vec![0]), @@ -163,6 +177,7 @@ fn guest_instruction_faults_deliver_sigill() { } #[test] +#[ignore = "macOS runner requires broker support"] fn fp_registers_survive_syscalls() { let fixture = Fixture::new(); let code = [ @@ -189,6 +204,7 @@ fn fp_registers_survive_syscalls() { } #[test] +#[ignore = "macOS runner requires broker support"] fn rejects_fixed_address_and_incompatible_page_layouts() { let fixture = Fixture::new(); let mut binary = elf(EXIT_42); @@ -217,6 +233,7 @@ fn rejects_fixed_address_and_incompatible_page_layouts() { } #[test] +#[ignore = "macOS runner requires broker support"] fn preserves_scratch_registers_and_accepts_nonzero_svc_immediates() { let fixture = Fixture::new(); let code = [ diff --git a/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs b/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs index 51d32bcad9..747d40fd58 100644 --- a/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs +++ b/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs @@ -14,6 +14,7 @@ const X18: &[u32] = &[ ]; #[test] +#[ignore = "macOS runner requires broker support"] fn guest_signal_return_restores_x18_and_vector_state() { let fixture = Fixture::new(); // SIGUSR1 handler clobbers x18 and d0; synthetic rt_sigreturn restores them. @@ -59,16 +60,19 @@ fn run_x18_fixture(aot: bool) { } #[test] +#[ignore = "macOS runner requires broker support"] fn runtime_x18_gates_preserve_registers_and_branch_targets() { run_x18_fixture(false); } #[test] +#[ignore = "macOS runner requires broker support"] fn aot_x18_gates_preserve_registers_and_branch_targets() { run_x18_fixture(true); } #[test] +#[ignore = "macOS runner requires broker support"] fn clone_uses_distinct_guest_tls_and_x18_slots() { let fixture = Fixture::new(); // Clone with SETTLS|CHILD_CLEARTID; child changes TP and x18. Parent waits diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index bce1f96d23..2f48f0b89b 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -591,43 +591,20 @@ impl Task { // If the read size is too large, we need to do some extra work to avoid OOMing. // We read data in chunks and update the file offset ourselves only if the read succeeds. self.with_typed_fd(fd, |fd| { + let cur_loc = self.do_seek(fd, 0, SeekWhence::RelativeToCurrentOffset)?; + let read_total = self.do_pread_with_user_buf( + fd, + buf, + count, + i64::try_from(cur_loc).map_err(|_| Errno::EOVERFLOW)?, + )?; + let new_loc = cur_loc.checked_add(read_total).ok_or(Errno::EOVERFLOW)?; self.do_seek( fd, - 0, - SeekWhence::RelativeToCurrentOffset, - ) - .inspect_err(|e| { - match *e { - Errno::EBADF => (), // safe errors to return - Errno::ESPIPE => { - unimplemented!("read on non-seekable fds with large buffers"); - } - Errno::EINVAL => { - unreachable!("seekable file should not return EINVAL when getting current offset"); - } - _ => { - unimplemented!("unexpected error from lseek: {}", e); - } - } - }) - .and_then(|cur_loc| { - self.do_pread_with_user_buf( - fd, - buf, - count, - i64::try_from(cur_loc).unwrap(), - ) - .inspect(|read_total| { - // Update the file offset to reflect the read we just did. - self.do_seek( - fd, - (cur_loc + read_total).reinterpret_as_signed(), - SeekWhence::RelativeToBeginning, - ) - // Given that previous lseek and pread succeeded, this lseek should also succeed. - .expect("lseek failed"); - }) - }) + isize::try_from(new_loc).map_err(|_| Errno::EOVERFLOW)?, + SeekWhence::RelativeToBeginning, + )?; + Ok(read_total) }) } } diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index 585309ac3e..77f2e3fc82 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -378,32 +378,165 @@ 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::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; + #[cfg(target_arch = "aarch64")] + const EM_HOST: u16 = 183; + 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 + } + #[test] - fn interpreter_reservation_is_top_down_above_low_heap() { + #[cfg_attr(target_os = "macos", ignore = "macOS runner supports PIE guests only")] + fn et_exec_interpreter_loads_top_down_above_low_heap() { let task = crate::syscalls::tests::init_platform(); - 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"); + crate::syscalls::tests::create_file( + &task, + "/main", + &minimal_elf(ET_EXEC, Some(INTERP_PATH)), + ); + crate::syscalls::tests::create_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"); let addr_max = >::TASK_ADDR_MAX; assert!( - address >= addr_max / 2, - "interpreter reserved at {address:#x}, near the low-heap region {:#x} rather than top-down high (>= {:#x})", + interp.base_addr >= addr_max / 2, + "ET_EXEC interpreter loaded at {:#x}, near the low-heap region {:#x} rather than top-down high (>= {:#x})", + interp.base_addr, crate::loader::DEFAULT_LOW_ADDR, addr_max / 2, ); - task.sys_munmap(UserPtrMut::from_usize(address), PAGE_SIZE) - .expect("the test reservation should unmap"); } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index ed7198ab1f..37a08b1744 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -2011,9 +2011,8 @@ impl Task { Err(FileStatusError::PathError(_)) => { return Err(Errno::EACCES); } - Err(_) => { - return Err(Errno::ENOENT); - } + Err(FileStatusError::Io) => return Err(Errno::EIO), + Err(_) => return Err(Errno::ENOENT), } } @@ -2118,8 +2117,7 @@ impl Task { let major = status.node_info.rdev.map_or(0, |v| v.get() >> 8); Ok((136..=143).contains(&major) && status.file_type == FileType::CharacterDevice) } - Err(litebox::fs::errors::FileStatusError::ClosedFd) => Err(Errno::EBADF), - Err(_) => unimplemented!(), + Err(error) => Err(error.into()), } } diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index ca77a479b2..2728dedd34 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -1548,9 +1548,13 @@ impl Task { #[cfg(test)] mod tests { - use super::PAGE_SIZE; + use super::{PAGE_SIZE, Task}; #[cfg(any(target_os = "linux", target_os = "windows"))] use litebox::platform::PageManagementProvider; + use litebox::{ + mm::linux::{NonZeroAddress, NonZeroPageSize}, + platform::page_mgmt::MemoryRegionPermissions, + }; use litebox_broker_protocol::fs::FileMode as Mode; #[cfg(any(target_os = "linux", target_os = "windows"))] use litebox_common_linux::MRemapFlags; @@ -1559,6 +1563,77 @@ mod tests { use crate::UserPtrMut; use crate::syscalls::tests::{TestPlatform as Platform, create_file, init_platform}; + fn check_file_mmap_permissions( + task: &Task, + fd: i32, + prot: ProtFlags, + expected: MemoryRegionPermissions, + ) { + let typed_fd = task + .typed_fd(fd) + .expect("test file descriptor should resolve"); + let address = task + .do_mmap_file_memcpy(None, PAGE_SIZE, prot, MapFlags::MAP_PRIVATE, &typed_fd, 0) + .expect("file mapping should succeed"); + let actual = task + .global + .pm + .get_memory_permissions( + NonZeroAddress::new(address.as_usize()).expect("mapping address is aligned"), + NonZeroPageSize::new(PAGE_SIZE).expect("page size is valid"), + ) + .expect("mapping permissions should be tracked"); + assert_eq!(actual, expected); + task.sys_munmap(address, PAGE_SIZE) + .expect("test mapping should unmap"); + } + + #[test] + fn file_mmap_preserves_requested_permissions() { + let task = init_platform(); + create_file(&task, "/mmap-permissions", &[0x5a]); + let fd = i32::try_from( + task.sys_open("/mmap-permissions", OFlags::RDONLY, Mode::empty()) + .expect("test file should open"), + ) + .expect("file descriptor should fit i32"); + + for (prot, permissions) in [ + (ProtFlags::PROT_NONE, MemoryRegionPermissions::empty()), + (ProtFlags::PROT_READ, MemoryRegionPermissions::READ), + (ProtFlags::PROT_WRITE, MemoryRegionPermissions::WRITE), + (ProtFlags::PROT_EXEC, MemoryRegionPermissions::EXEC), + ( + ProtFlags::PROT_READ_WRITE, + MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE, + ), + ( + ProtFlags::PROT_READ_EXEC, + MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC, + ), + ] { + check_file_mmap_permissions(&task, fd, prot, permissions); + } + + #[cfg(target_os = "linux")] + for (prot, permissions) in [ + ( + ProtFlags::PROT_WRITE | ProtFlags::PROT_EXEC, + MemoryRegionPermissions::WRITE | MemoryRegionPermissions::EXEC, + ), + ( + ProtFlags::PROT_READ_WRITE_EXEC, + MemoryRegionPermissions::READ + | MemoryRegionPermissions::WRITE + | MemoryRegionPermissions::EXEC, + ), + ] { + check_file_mmap_permissions(&task, fd, prot, permissions); + } + + task.sys_close(fd).expect("test file should close"); + } + #[test] fn full_capacity_anywhere_precedes_preferred_one_page() { let calls = core::cell::RefCell::new(alloc::vec::Vec::new()); From b0fc8ebf6ae07c8027bcb626e011cc7a1b8e13ff Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 14 Sep 2026 12:28:10 -0700 Subject: [PATCH 4/8] Preserve mmap permissions during runtime patching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/mm/linux.rs | 2 + litebox/src/mm/mod.rs | 15 ++ litebox_common_linux/src/errno/mod.rs | 1 + litebox_common_linux/src/mm.rs | 50 ++-- litebox_shim_linux/src/syscalls/mm.rs | 341 ++++++++++++++++---------- 5 files changed, 259 insertions(+), 150 deletions(-) diff --git a/litebox/src/mm/linux.rs b/litebox/src/mm/linux.rs index 68101d8ef4..185844c3b4 100644 --- a/litebox/src/mm/linux.rs +++ b/litebox/src/mm/linux.rs @@ -1033,6 +1033,8 @@ pub enum MappingError { NotAFile, #[error("file not open for reading")] NotForReading, + #[error("invalid memory permissions")] + InvalidPermissions, #[error("mapping failed: {0}")] MapError(#[from] crate::platform::page_mgmt::AllocationError), diff --git a/litebox/src/mm/mod.rs b/litebox/src/mm/mod.rs index a8bee74399..c24d08de3e 100644 --- a/litebox/src/mm/mod.rs +++ b/litebox/src/mm/mod.rs @@ -538,6 +538,21 @@ where unsafe { vmem.protect_mapping(range, new_permissions) } } + /// Change pages to the requested permissions. + /// + /// # Safety + /// + /// The caller must ensure no accesses conflict with the permission change. + /// Callers should avoid writable executable mappings unless they are strictly required. + pub unsafe fn set_page_permissions( + &self, + ptr: Platform::RawMutPointer, + len: usize, + permissions: MemoryRegionPermissions, + ) -> Result<(), VmemProtectError> { + self.change_page_permissions(ptr, len, permissions) + } + /// Make pages readable and writable. /// /// # Safety diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index fe1e6367c9..274174652d 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -287,6 +287,7 @@ impl From for Errno { litebox::mm::linux::MappingError::BadFD(_) => Errno::EBADF, litebox::mm::linux::MappingError::NotAFile => Errno::EISDIR, litebox::mm::linux::MappingError::NotForReading => Errno::EACCES, + litebox::mm::linux::MappingError::InvalidPermissions => Errno::EINVAL, litebox::mm::linux::MappingError::MapError(e) => e.into(), litebox::mm::linux::MappingError::ProtectError(e) => e.into(), _ => unimplemented!(), diff --git a/litebox_common_linux/src/mm.rs b/litebox_common_linux/src/mm.rs index 3e24e81d22..e272ff3fc6 100644 --- a/litebox_common_linux/src/mm.rs +++ b/litebox_common_linux/src/mm.rs @@ -14,6 +14,26 @@ use crate::{MRemapFlags, MapFlags, ProtFlags, UserPtrMut, errno::Errno}; const PAGE_MASK: usize = !(PAGE_SIZE - 1); +fn memory_region_permissions(prot: &ProtFlags) -> Option { + if prot.bits() & !ProtFlags::PROT_READ_WRITE_EXEC.bits() != 0 { + return None; + } + let mut permissions = MemoryRegionPermissions::empty(); + permissions.set( + MemoryRegionPermissions::READ, + prot.contains(ProtFlags::PROT_READ), + ); + permissions.set( + MemoryRegionPermissions::WRITE, + prot.contains(ProtFlags::PROT_WRITE), + ); + permissions.set( + MemoryRegionPermissions::EXEC, + prot.contains(ProtFlags::PROT_EXEC), + ); + Some(permissions) +} + pub fn do_mmap< Platform: litebox::platform::RawPointerProvider + litebox::sync::RawSyncPrimitivesProvider @@ -59,19 +79,7 @@ pub fn do_mmap< None => None, }; let length = NonZeroPageSize::new(len).ok_or(MappingError::UnAligned)?; - let mut permissions = MemoryRegionPermissions::empty(); - permissions.set( - MemoryRegionPermissions::READ, - prot.contains(ProtFlags::PROT_READ), - ); - permissions.set( - MemoryRegionPermissions::WRITE, - prot.contains(ProtFlags::PROT_WRITE), - ); - permissions.set( - MemoryRegionPermissions::EXEC, - prot.contains(ProtFlags::PROT_EXEC), - ); + let permissions = memory_region_permissions(&prot).ok_or(MappingError::InvalidPermissions)?; unsafe { pm.create_pages_with_permissions(suggested_addr, length, flags, permissions, op) } .map(UserPtrMut::from_platform_ptr::) } @@ -130,20 +138,8 @@ pub fn sys_mprotect< } let addr = addr.to_platform_ptr::(); - match prot { - ProtFlags::PROT_READ_EXEC => unsafe { pm.make_pages_executable(addr, len) }, - ProtFlags::PROT_READ_WRITE => unsafe { pm.make_pages_writable(addr, len) }, - ProtFlags::PROT_READ => unsafe { pm.make_pages_readable(addr, len) }, - ProtFlags::PROT_NONE => unsafe { pm.make_pages_inaccessible(addr, len) }, - ProtFlags::PROT_READ_WRITE_EXEC => unsafe { pm.make_pages_rwx(addr, len) }, - _ => { - #[cfg(debug_assertions)] - todo!("Unsupported prot flags {:?}", prot); - #[cfg(not(debug_assertions))] - return Err(Errno::EINVAL); - } - } - .map_err(Errno::from) + let permissions = memory_region_permissions(&prot).ok_or(Errno::EINVAL)?; + unsafe { pm.set_page_permissions(addr, len, permissions) }.map_err(Errno::from) } /// Handle syscall `mremap` diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index 2728dedd34..b0fa14ad38 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -5,7 +5,10 @@ //! 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}; +use litebox::{ + mm::linux::{MappingError, PAGE_SIZE}, + platform::page_mgmt::MemoryRegionPermissions, +}; use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, errno::Errno}; use crate::ShimPlatform; @@ -70,6 +73,23 @@ fn finalize_trampoline_gates( } } +fn prot_flags_from_permissions(permissions: MemoryRegionPermissions) -> ProtFlags { + let mut prot = ProtFlags::PROT_NONE; + prot.set( + ProtFlags::PROT_READ, + permissions.contains(MemoryRegionPermissions::READ), + ); + prot.set( + ProtFlags::PROT_WRITE, + permissions.contains(MemoryRegionPermissions::WRITE), + ); + prot.set( + ProtFlags::PROT_EXEC, + permissions.contains(MemoryRegionPermissions::EXEC), + ); + prot +} + /// Per-fd state for the shim's runtime ELF syscall rewriter. /// /// Tracks base address and trampoline write cursor for each ELF file that @@ -233,18 +253,18 @@ impl Task { let typed_fd = self.typed_fd(fd).map_err(|_| MappingError::BadFD(fd))?; let result = - self.do_mmap_file_memcpy(suggested_addr, len, prot, flags, &typed_fd, offset)?; + self.do_mmap_file_memcpy(suggested_addr, len, prot.clone(), flags, &typed_fd, offset)?; // Runtime syscall rewriting: patch PROT_EXEC segments in-place. if is_exec { let syscall_entry = self.global.platform.get_syscall_entry_point(); if syscall_entry != 0 - && !self.maybe_patch_exec_segment(result, len, fd, syscall_entry, Some(offset)) + && self + .maybe_patch_exec_segment(result, len, fd, syscall_entry, Some(offset), &prot) + .is_err() { - // Trampoline setup failed for a pre-patched binary whose - // .text already contains JMPs to the trampoline address. - // Continuing would guarantee a SIGSEGV on the first - // rewritten syscall, so fail the mmap instead. + // Runtime patching, trampoline setup, or restoration of the + // requested permissions failed, so fail the mmap. let _ = self.sys_munmap(result, len); return Err(MappingError::OutOfMemory); } @@ -441,16 +461,19 @@ impl Task { len: usize, prot: ProtFlags, ) -> Result<(), Errno> { + if !addr.as_usize().is_multiple_of(PAGE_SIZE) + || !len.is_multiple_of(PAGE_SIZE) + || addr.as_usize().checked_add(len).is_none() + || prot.bits() & !ProtFlags::PROT_READ_WRITE_EXEC.bits() != 0 + { + return Err(Errno::EINVAL); + } + // Intercept transitions to PROT_EXEC: patch unpatched file mappings. if prot.contains(ProtFlags::PROT_EXEC) { let syscall_entry = self.global.platform.get_syscall_entry_point(); if syscall_entry != 0 { - #[cfg(target_arch = "x86_64")] - self.maybe_patch_on_mprotect_exec(addr, len, syscall_entry); - #[cfg(target_arch = "aarch64")] - if !self.maybe_patch_on_mprotect_exec(addr, len, syscall_entry) { - return Err(Errno::ENOMEM); - } + self.maybe_patch_on_mprotect_exec(addr, len, syscall_entry)?; } } // Only AArch64 needs protection from loader reprotection of its holes; @@ -563,14 +586,15 @@ impl Task { addr: UserPtrMut, len: usize, syscall_entry: usize, - ) -> bool { + ) -> Result<(), Errno> { let mprotect_start = addr.as_usize(); let mprotect_end = mprotect_start.saturating_add(len); + let mappings = self.global.pm.mappings(); // Find unpatched file mappings that overlap this mprotect range. - // We collect (fd, vaddr, seg_len, file_offset) to avoid holding - // the lock while patching. - let to_patch: alloc::vec::Vec<(i32, usize, usize)> = { + // Split them at VMA boundaries so each patch operation can restore + // the permissions that existed before this mprotect request. + let to_patch: alloc::vec::Vec<(i32, usize, usize, ProtFlags)> = { let cache = self.global.elf_patch_cache.lock(); let mut result = alloc::vec::Vec::new(); for (&fd, state) in cache.iter() { @@ -582,7 +606,20 @@ impl Task { let seg_end = seg_start.saturating_add(seg_len); // Check overlap with the mprotect range. if seg_start < mprotect_end && seg_end > mprotect_start { - result.push((fd, seg_start, seg_len)); + let patch_start = seg_start.max(mprotect_start); + let patch_end = seg_end.min(mprotect_end); + for (mapping, flags) in &mappings { + let start = patch_start.max(mapping.start); + let end = patch_end.min(mapping.end); + if start < end { + result.push(( + fd, + start, + end - start, + prot_flags_from_permissions((*flags).into()), + )); + } + } } } } @@ -592,7 +629,7 @@ impl Task { // A single mprotect range should only overlap mappings from one fd // (a given vaddr range is backed by at most one file at a time). if to_patch.len() > 1 { - let fds: BTreeSet = to_patch.iter().map(|(fd, _, _)| *fd).collect(); + let fds: BTreeSet = to_patch.iter().map(|(fd, _, _, _)| *fd).collect(); if fds.len() > 1 { litebox_util_log::warn!( addr:? = mprotect_start, len:? = len, fds:? = fds; @@ -601,24 +638,18 @@ impl Task { } } - for (fd, seg_start, seg_len) in to_patch { - // Clamp to the intersection of the tracked mapping and the - // mprotect range — only patch the portion becoming executable. - // Re-running the rewriter on already-patched bytes is safe, - // so we don't need to track sub-range overlaps precisely. - let seg_end = seg_start.saturating_add(seg_len); - let patch_start = seg_start.max(mprotect_start); - let patch_end = seg_end.min(mprotect_end); - let patch_len = patch_end.saturating_sub(patch_start); - if patch_len == 0 { - continue; - } + for (fd, patch_start, patch_len, restore_prot) in to_patch { let mapped_addr = UserPtrMut::::from_usize(patch_start); - if !self.maybe_patch_exec_segment(mapped_addr, patch_len, fd, syscall_entry, None) { - return false; - } + self.maybe_patch_exec_segment( + mapped_addr, + patch_len, + fd, + syscall_entry, + None, + &restore_prot, + )?; } - true + Ok(()) } /// Initialize ELF patch state for an fd on its first mmap. @@ -952,12 +983,13 @@ impl Task { } /// Apply the trap fallback to a mapped code segment: replace every patch - /// site with the rewriter's trap, then restore RX. + /// site with the rewriter's trap, then restore the caller-selected permissions. /// /// If `already_rw` is true, the segment is assumed to already be writable /// and the initial mprotect RW is skipped. /// - /// Panics on infrastructure failures (mprotect/read/write/disassembly). + /// Returns an error if those permissions cannot be restored. + /// Panics on other infrastructure failures (mprotect/read/write/disassembly). #[cfg(target_arch = "aarch64")] fn apply_aarch64_trap_fallback( &self, @@ -965,7 +997,8 @@ impl Task { len: usize, already_rw: bool, ranges: Option<&litebox_syscall_rewriter::aarch64::CodeScanRanges>, - ) { + restore_prot: &ProtFlags, + ) -> Result<(), Errno> { if !already_rw { self.sys_mprotect_raw( mapped_addr, @@ -1011,17 +1044,18 @@ impl Task { "fatal: failed to write trap bytes back to code segment" ); - // Restore RX. - self.sys_mprotect_raw( - mapped_addr, - len, - ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, - ) - .expect("fatal: failed to restore code segment to RX after trap fallback"); + // Restore the caller-selected permissions. + self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()) } #[cfg(target_arch = "x86_64")] - fn apply_trap_fallback(&self, mapped_addr: UserPtrMut, len: usize, already_rw: bool) { + fn apply_trap_fallback( + &self, + mapped_addr: UserPtrMut, + len: usize, + already_rw: bool, + restore_prot: &ProtFlags, + ) -> Result<(), Errno> { if !already_rw { self.sys_mprotect_raw( mapped_addr, @@ -1052,13 +1086,8 @@ impl Task { "fatal: failed to write trap bytes back to code segment" ); - // Restore RX. - self.sys_mprotect_raw( - mapped_addr, - len, - ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, - ) - .expect("fatal: failed to restore code segment to RX after trap fallback"); + // Restore the caller-selected permissions. + self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()) } /// Patch an executable segment in-place after it has been mapped. @@ -1068,10 +1097,8 @@ impl Task { /// For unpatched binaries: calls `patch_code_segment()` to rewrite syscall /// instructions and places the generated stubs in the trampoline region. /// - /// Returns `true` on success or non-fatal skip. Returns `false` when a - /// pre-patched binary's trampoline could not be set up — the caller must - /// fail the mapping because the code already contains JMPs to the - /// trampoline address. + /// Returns an error when a pre-patched binary's trampoline cannot be set + /// up or the caller-selected code permissions cannot be restored. fn maybe_patch_exec_segment( &self, mapped_addr: UserPtrMut, @@ -1079,7 +1106,8 @@ impl Task { fd: i32, syscall_entry: usize, file_offset: Option, - ) -> bool { + restore_prot: &ProtFlags, + ) -> Result<(), Errno> { // Initialize patch state if this is the first mmap for this fd. // Typically the first mapping is at offset 0 (the ELF header), but // some loaders may map an executable segment at a non-zero offset first. @@ -1092,11 +1120,11 @@ impl Task { // linker loads shared libraries sequentially. let mut cache = self.global.elf_patch_cache.lock(); let Some(state) = cache.get_mut(&fd) else { - return true; // No patch state — not an ELF we're tracking + return Ok(()); // No patch state — not an ELF we're tracking }; #[cfg(target_arch = "aarch64")] if state.trampoline_invalidated { - return false; + return Err(Errno::ENOMEM); } if state.pre_patched { @@ -1109,7 +1137,7 @@ impl Task { // object-span reservation, so validate ownership before MAP_FIXED. #[cfg(target_arch = "aarch64")] if !self.trampoline_range_is_safe_to_map(state, tramp_addr, tramp_len) { - return false; + return Err(Errno::ENOMEM); } let alloc_result = self.do_mmap_anonymous( Some(tramp_addr), @@ -1118,13 +1146,13 @@ impl Task { MapFlags::MAP_ANONYMOUS | MapFlags::MAP_PRIVATE | MapFlags::MAP_FIXED, ); let Ok(alloc_ptr) = alloc_result else { - return false; + return Err(Errno::ENOMEM); }; let actual_addr = alloc_ptr.as_usize(); if actual_addr != tramp_addr { let _ = self.sys_munmap_raw(UserPtrMut::::from_usize(actual_addr), tramp_len); - return false; + return Err(Errno::ENOMEM); } // Read trampoline data from the file. @@ -1136,7 +1164,7 @@ impl Task { .is_err() { let _ = self.sys_munmap_raw(tramp_ptr, tramp_len); - return false; + return Err(Errno::ENOMEM); } // Write syscall entry point to the first 8 bytes. @@ -1149,7 +1177,7 @@ impl Task { if let Err(e) = finalize_trampoline_gates(self.global.platform, &mut tramp_data) { litebox_util_log::error!(err:% = e; "refusing to map a trampoline whose guest thread-pointer gates are not patched"); let _ = self.sys_munmap_raw(tramp_ptr, tramp_len); - return false; + return Err(Errno::ENOMEM); } // Write to the mapped region. @@ -1158,7 +1186,7 @@ impl Task { .is_none() { let _ = self.sys_munmap_raw(tramp_ptr, tramp_len); - return false; + return Err(Errno::ENOMEM); } // Protect as RX immediately. @@ -1171,13 +1199,13 @@ impl Task { .is_err() { let _ = self.sys_munmap_raw(tramp_ptr, tramp_len); - return false; + return Err(Errno::ENOMEM); } state.trampoline_mapped = true; state.trampoline_mapped_len = tramp_len; } - return true; + return Ok(()); } // ── Runtime patching path (unpatched binaries) ─────────────── @@ -1191,11 +1219,17 @@ impl Task { }); #[cfg(target_arch = "aarch64")] let apply_trap_fallback = |mapped_addr, len, already_rw| { - self.apply_aarch64_trap_fallback(mapped_addr, len, already_rw, scan_ranges.as_ref()); + self.apply_aarch64_trap_fallback( + mapped_addr, + len, + already_rw, + scan_ranges.as_ref(), + restore_prot, + ) }; #[cfg(target_arch = "x86_64")] let apply_trap_fallback = |mapped_addr, len, already_rw| { - self.apply_trap_fallback(mapped_addr, len, already_rw); + self.apply_trap_fallback(mapped_addr, len, already_rw, restore_prot) }; // Allocate the trampoline region if not yet done. @@ -1267,8 +1301,7 @@ impl Task { } else { litebox_util_log::warn!("failed to allocate trampoline region"); } - apply_trap_fallback(mapped_addr, len, false); - return true; + return apply_trap_fallback(mapped_addr, len, false); }; let actual_addr = actual_addr_ptr.as_usize(); @@ -1281,8 +1314,7 @@ impl Task { ); let _ = self.sys_munmap_raw(UserPtrMut::::from_usize(actual_addr), reservation_len); - apply_trap_fallback(mapped_addr, len, false); - return true; + return apply_trap_fallback(mapped_addr, len, false); } state.trampoline_addr = actual_addr; @@ -1296,8 +1328,7 @@ impl Task { litebox_util_log::warn!("failed to write syscall entry point to trampoline"); let _ = self .sys_munmap_raw(UserPtrMut::::from_usize(actual_addr), reservation_len); - apply_trap_fallback(mapped_addr, len, false); - return true; + return apply_trap_fallback(mapped_addr, len, false); } state.trampoline_cursor = litebox_syscall_rewriter::TRAMPOLINE_ENTRY_POINT_BYTES; } else { @@ -1310,7 +1341,7 @@ impl Task { // Performance guard: skip if this exact range was already patched. let mapping_key = (mapped_addr.as_usize(), len); if state.patched_ranges.contains(&mapping_key) { - return true; + return Ok(()); } state.patched_ranges.insert(mapping_key); @@ -1350,11 +1381,7 @@ impl Task { // Read the mapped code into a buffer, patch it, write back. let Some(code_owned) = mapped_addr.to_owned_slice::(len) else { - let _ = self.sys_mprotect_raw( - mapped_addr, - len, - ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, - ); + let _ = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); restore_trampoline_rx(self, state); panic!("fatal: failed to read code segment for patching"); }; @@ -1420,17 +1447,17 @@ impl Task { let mut stubs = stubs; if let Err(e) = finalize_trampoline_gates(self.global.platform, &mut stubs) { litebox_util_log::error!(err:% = e; "refusing to install runtime gates whose guest thread-pointer is not patched"); - apply_trap_fallback(mapped_addr, len, true); + let restored = apply_trap_fallback(mapped_addr, len, true); restore_trampoline_rx(self, state); - return true; + return restored; } stubs }; let Some(new_cursor) = state.trampoline_cursor.checked_add(stubs.len()) else { litebox_util_log::warn!("trampoline cursor overflow"); - apply_trap_fallback(mapped_addr, len, true); + let restored = apply_trap_fallback(mapped_addr, len, true); restore_trampoline_rx(self, state); - return true; + return restored; }; let tramp_pages_needed = align_up(new_cursor, PAGE_SIZE); if tramp_pages_needed > state.trampoline_mapped_len { @@ -1448,9 +1475,9 @@ impl Task { .is_err() { litebox_util_log::warn!("failed to expand trampoline region"); - apply_trap_fallback(mapped_addr, len, true); + let restored = apply_trap_fallback(mapped_addr, len, true); restore_trampoline_rx(self, state); - return true; + return restored; } state.trampoline_mapped_len = tramp_pages_needed; } @@ -1463,11 +1490,7 @@ impl Task { .copy_from_slice::(0, &stubs) .is_none() { - let _ = self.sys_mprotect_raw( - mapped_addr, - len, - ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, - ); + let _ = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); restore_trampoline_rx(self, state); panic!("fatal: failed to write trampoline stubs"); } @@ -1478,11 +1501,7 @@ impl Task { .is_none() { let _ = mapped_addr.copy_from_slice::(0, &original_code); - let _ = self.sys_mprotect_raw( - mapped_addr, - len, - ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, - ); + let _ = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); restore_trampoline_rx(self, state); panic!("fatal: failed to write patched code back to code segment"); } @@ -1499,26 +1518,23 @@ impl Task { .is_none() { let _ = mapped_addr.copy_from_slice::(0, &original_code); + let _ = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); panic!("fatal: failed to write trap bytes back to code segment"); } - // Fall through to restore RX protections below. + // Fall through to restore the caller-selected protections below. } Err(e) => { litebox_util_log::warn!(err:? = e; "patch_code_segment failed"); - apply_trap_fallback(mapped_addr, len, true); + let restored = apply_trap_fallback(mapped_addr, len, true); restore_trampoline_rx(self, state); - return true; + return restored; } } - // Restore the code segment to RX. - let _ = self.sys_mprotect_raw( - mapped_addr, - len, - ProtFlags::PROT_READ | ProtFlags::PROT_EXEC, - ); + // Restore the caller-selected code-segment permissions. + let restored = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); restore_trampoline_rx(self, state); - true + restored } /// Finalize the ELF patching state for `fd`. @@ -1548,7 +1564,8 @@ impl Task { #[cfg(test)] mod tests { - use super::{PAGE_SIZE, Task}; + use super::{ElfPatchState, PAGE_SIZE, Task}; + use alloc::collections::BTreeSet; #[cfg(any(target_os = "linux", target_os = "windows"))] use litebox::platform::PageManagementProvider; use litebox::{ @@ -1563,27 +1580,29 @@ mod tests { use crate::UserPtrMut; use crate::syscalls::tests::{TestPlatform as Platform, create_file, init_platform}; + fn file_mapping_permissions( + task: &Task, + address: UserPtrMut, + ) -> MemoryRegionPermissions { + task.global + .pm + .get_memory_permissions( + NonZeroAddress::new(address.as_usize()).expect("mapping address is aligned"), + NonZeroPageSize::new(PAGE_SIZE).expect("page size is valid"), + ) + .expect("mapping permissions should be tracked") + } + fn check_file_mmap_permissions( task: &Task, fd: i32, prot: ProtFlags, expected: MemoryRegionPermissions, ) { - let typed_fd = task - .typed_fd(fd) - .expect("test file descriptor should resolve"); let address = task - .do_mmap_file_memcpy(None, PAGE_SIZE, prot, MapFlags::MAP_PRIVATE, &typed_fd, 0) + .do_mmap_file(None, PAGE_SIZE, prot, MapFlags::MAP_PRIVATE, fd, 0) .expect("file mapping should succeed"); - let actual = task - .global - .pm - .get_memory_permissions( - NonZeroAddress::new(address.as_usize()).expect("mapping address is aligned"), - NonZeroPageSize::new(PAGE_SIZE).expect("page size is valid"), - ) - .expect("mapping permissions should be tracked"); - assert_eq!(actual, expected); + assert_eq!(file_mapping_permissions(task, address), expected); task.sys_munmap(address, PAGE_SIZE) .expect("test mapping should unmap"); } @@ -1591,12 +1610,35 @@ mod tests { #[test] fn file_mmap_preserves_requested_permissions() { let task = init_platform(); - create_file(&task, "/mmap-permissions", &[0x5a]); + create_file(&task, "/mmap-permissions", &[0]); let fd = i32::try_from( task.sys_open("/mmap-permissions", OFlags::RDONLY, Mode::empty()) .expect("test file should open"), ) .expect("file descriptor should fit i32"); + task.global.elf_patch_cache.lock().insert( + fd, + ElfPatchState { + pre_patched: false, + trampoline_file_offset: 0, + trampoline_file_size: 0, + trampoline_addr: 0, + #[cfg(target_arch = "aarch64")] + load_span: None, + trampoline_cursor: 0, + trampoline_mapped: true, + trampoline_mapped_len: 0, + runtime_patches_committed: false, + #[cfg(target_arch = "aarch64")] + trampoline_invalidated: false, + #[cfg(target_arch = "aarch64")] + code_metadata: None, + #[cfg(target_arch = "aarch64")] + trampoline_capacity: 0, + file_mappings: BTreeSet::new(), + patched_ranges: BTreeSet::new(), + }, + ); for (prot, permissions) in [ (ProtFlags::PROT_NONE, MemoryRegionPermissions::empty()), @@ -1631,6 +1673,59 @@ mod tests { check_file_mmap_permissions(&task, fd, prot, permissions); } + let address = task + .do_mmap_file( + None, + PAGE_SIZE, + ProtFlags::PROT_READ, + MapFlags::MAP_PRIVATE, + fd, + 0, + ) + .expect("file mapping should succeed"); + assert_eq!( + task.sys_mprotect( + address, + PAGE_SIZE, + ProtFlags::PROT_EXEC | ProtFlags::PROT_GROWSDOWN, + ), + Err(Errno::EINVAL) + ); + assert_eq!( + file_mapping_permissions(&task, address), + MemoryRegionPermissions::READ + ); + task.sys_munmap(address, PAGE_SIZE) + .expect("test mapping should unmap"); + + #[cfg(target_os = "macos")] + { + let address = task + .do_mmap_file( + None, + PAGE_SIZE, + ProtFlags::PROT_READ, + MapFlags::MAP_PRIVATE, + fd, + 0, + ) + .expect("file mapping should succeed"); + assert_eq!( + task.sys_mprotect( + address, + PAGE_SIZE, + ProtFlags::PROT_WRITE | ProtFlags::PROT_EXEC, + ), + Err(Errno::EACCES) + ); + assert_eq!( + file_mapping_permissions(&task, address), + MemoryRegionPermissions::READ + ); + task.sys_munmap(address, PAGE_SIZE) + .expect("test mapping should unmap"); + } + task.sys_close(fd).expect("test file should close"); } From 2fbadbd623fa456095d9b77c990b8c87e0d59f5b Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 14 Sep 2026 12:56:00 -0700 Subject: [PATCH 5/8] Fix follow-up Linux client review findings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_common_linux/src/mm.rs | 10 +- litebox_runner_linux_userland/tests/run.rs | 9 +- litebox_shim_linux/src/syscalls/mm.rs | 288 +++++++++++++++++---- 3 files changed, 256 insertions(+), 51 deletions(-) diff --git a/litebox_common_linux/src/mm.rs b/litebox_common_linux/src/mm.rs index e272ff3fc6..0ae4e83e54 100644 --- a/litebox_common_linux/src/mm.rs +++ b/litebox_common_linux/src/mm.rs @@ -80,8 +80,14 @@ pub fn do_mmap< }; let length = NonZeroPageSize::new(len).ok_or(MappingError::UnAligned)?; let permissions = memory_region_permissions(&prot).ok_or(MappingError::InvalidPermissions)?; - unsafe { pm.create_pages_with_permissions(suggested_addr, length, flags, permissions, op) } - .map(UserPtrMut::from_platform_ptr::) + if flags.contains(CreatePagesFlags::MAP_FILE) || !permissions.is_empty() { + unsafe { pm.create_pages_with_permissions(suggested_addr, length, flags, permissions, op) } + } else { + // Anonymous PROT_NONE mappings are reservations and need no writable + // initialization window. + unsafe { pm.create_inaccessible_pages(suggested_addr, length, flags, op) } + } + .map(UserPtrMut::from_platform_ptr::) } /// Handle syscall `munmap` diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index f2324b4b92..4b9e9646c5 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -334,6 +334,11 @@ fn test_file_service( use litebox_broker_platform_linux_userland::LinuxSyncPrimitivesProvider; use litebox_broker_protocol::fs::{FileMode as Mode, FileUser as UserInfo}; + const GUEST_USER: UserInfo = UserInfo { + user: 1000, + group: 1000, + }; + let directory_mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; let mut entries = vec![ ( @@ -375,12 +380,12 @@ fn test_file_service( let node = if metadata.is_dir() { InitialNode::Directory { mode, - owner: UserInfo::ROOT, + owner: GUEST_USER, } } else { InitialNode::File { mode, - owner: UserInfo::ROOT, + owner: GUEST_USER, data: std::fs::read(entry.path()) .expect("failed to read runner test file") .into(), diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index b0fa14ad38..5cb18d6d05 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -90,6 +90,22 @@ fn prot_flags_from_permissions(permissions: MemoryRegionPermissions) -> ProtFlag prot } +type ProtectionRange = (usize, usize, ProtFlags); +type PatchRange = (i32, usize, usize, alloc::vec::Vec); + +fn push_patch_range( + result: &mut alloc::vec::Vec, + fd: i32, + protections: &mut alloc::vec::Vec, +) { + let (Some(first), Some(last)) = (protections.first(), protections.last()) else { + return; + }; + let start = first.0; + let end = last.0 + last.1; + result.push((fd, start, end - start, core::mem::take(protections))); +} + /// Per-fd state for the shim's runtime ELF syscall rewriter. /// /// Tracks base address and trampoline write cursor for each ELF file that @@ -258,9 +274,17 @@ impl Task { // Runtime syscall rewriting: patch PROT_EXEC segments in-place. if is_exec { let syscall_entry = self.global.platform.get_syscall_entry_point(); + let restore_protections = [(result.as_usize(), len, prot.clone())]; if syscall_entry != 0 && self - .maybe_patch_exec_segment(result, len, fd, syscall_entry, Some(offset), &prot) + .maybe_patch_exec_segment( + result, + len, + fd, + syscall_entry, + Some(offset), + &restore_protections, + ) .is_err() { // Runtime patching, trampoline setup, or restoration of the @@ -540,6 +564,19 @@ impl Task { litebox_common_linux::mm::sys_mprotect(&self.global.pm, addr, len, prot) } + fn restore_page_permissions(&self, protections: &[ProtectionRange]) -> Result<(), Errno> { + let mut first_error = None; + for (start, len, prot) in protections { + if let Err(error) = + self.sys_mprotect_raw(UserPtrMut::::from_usize(*start), *len, prot.clone()) + && first_error.is_none() + { + first_error = Some(error); + } + } + first_error.map_or(Ok(()), Err) + } + #[inline] pub(crate) fn sys_mremap( &self, @@ -592,9 +629,9 @@ impl Task { let mappings = self.global.pm.mappings(); // Find unpatched file mappings that overlap this mprotect range. - // Split them at VMA boundaries so each patch operation can restore - // the permissions that existed before this mprotect request. - let to_patch: alloc::vec::Vec<(i32, usize, usize, ProtFlags)> = { + // Preserve the VMA boundaries for restoration without splitting the + // instruction stream passed to the runtime rewriter. + let to_patch: alloc::vec::Vec = { let cache = self.global.elf_patch_cache.lock(); let mut result = alloc::vec::Vec::new(); for (&fd, state) in cache.iter() { @@ -608,18 +645,27 @@ impl Task { if seg_start < mprotect_end && seg_end > mprotect_start { let patch_start = seg_start.max(mprotect_start); let patch_end = seg_end.min(mprotect_end); + let mut restore_protections: alloc::vec::Vec = + alloc::vec::Vec::new(); for (mapping, flags) in &mappings { let start = patch_start.max(mapping.start); let end = patch_end.min(mapping.end); if start < end { - result.push(( - fd, + if restore_protections.last().is_some_and( + |(last_start, last_len, _)| { + last_start.saturating_add(*last_len) != start + }, + ) { + push_patch_range(&mut result, fd, &mut restore_protections); + } + restore_protections.push(( start, end - start, prot_flags_from_permissions((*flags).into()), )); } } + push_patch_range(&mut result, fd, &mut restore_protections); } } } @@ -638,7 +684,7 @@ impl Task { } } - for (fd, patch_start, patch_len, restore_prot) in to_patch { + for (fd, patch_start, patch_len, restore_protections) in to_patch { let mapped_addr = UserPtrMut::::from_usize(patch_start); self.maybe_patch_exec_segment( mapped_addr, @@ -646,7 +692,7 @@ impl Task { fd, syscall_entry, None, - &restore_prot, + &restore_protections, )?; } Ok(()) @@ -997,7 +1043,7 @@ impl Task { len: usize, already_rw: bool, ranges: Option<&litebox_syscall_rewriter::aarch64::CodeScanRanges>, - restore_prot: &ProtFlags, + restore_protections: &[ProtectionRange], ) -> Result<(), Errno> { if !already_rw { self.sys_mprotect_raw( @@ -1045,7 +1091,7 @@ impl Task { ); // Restore the caller-selected permissions. - self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()) + self.restore_page_permissions(restore_protections) } #[cfg(target_arch = "x86_64")] @@ -1054,7 +1100,7 @@ impl Task { mapped_addr: UserPtrMut, len: usize, already_rw: bool, - restore_prot: &ProtFlags, + restore_protections: &[ProtectionRange], ) -> Result<(), Errno> { if !already_rw { self.sys_mprotect_raw( @@ -1087,7 +1133,7 @@ impl Task { ); // Restore the caller-selected permissions. - self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()) + self.restore_page_permissions(restore_protections) } /// Patch an executable segment in-place after it has been mapped. @@ -1106,7 +1152,7 @@ impl Task { fd: i32, syscall_entry: usize, file_offset: Option, - restore_prot: &ProtFlags, + restore_protections: &[ProtectionRange], ) -> Result<(), Errno> { // Initialize patch state if this is the first mmap for this fd. // Typically the first mapping is at offset 0 (the ELF header), but @@ -1224,12 +1270,12 @@ impl Task { len, already_rw, scan_ranges.as_ref(), - restore_prot, + restore_protections, ) }; #[cfg(target_arch = "x86_64")] let apply_trap_fallback = |mapped_addr, len, already_rw| { - self.apply_trap_fallback(mapped_addr, len, already_rw, restore_prot) + self.apply_trap_fallback(mapped_addr, len, already_rw, restore_protections) }; // Allocate the trampoline region if not yet done. @@ -1381,7 +1427,7 @@ impl Task { // Read the mapped code into a buffer, patch it, write back. let Some(code_owned) = mapped_addr.to_owned_slice::(len) else { - let _ = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); + let _ = self.restore_page_permissions(restore_protections); restore_trampoline_rx(self, state); panic!("fatal: failed to read code segment for patching"); }; @@ -1490,7 +1536,7 @@ impl Task { .copy_from_slice::(0, &stubs) .is_none() { - let _ = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); + let _ = self.restore_page_permissions(restore_protections); restore_trampoline_rx(self, state); panic!("fatal: failed to write trampoline stubs"); } @@ -1501,7 +1547,7 @@ impl Task { .is_none() { let _ = mapped_addr.copy_from_slice::(0, &original_code); - let _ = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); + let _ = self.restore_page_permissions(restore_protections); restore_trampoline_rx(self, state); panic!("fatal: failed to write patched code back to code segment"); } @@ -1518,7 +1564,7 @@ impl Task { .is_none() { let _ = mapped_addr.copy_from_slice::(0, &original_code); - let _ = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); + let _ = self.restore_page_permissions(restore_protections); panic!("fatal: failed to write trap bytes back to code segment"); } // Fall through to restore the caller-selected protections below. @@ -1532,7 +1578,7 @@ impl Task { } // Restore the caller-selected code-segment permissions. - let restored = self.sys_mprotect_raw(mapped_addr, len, restore_prot.clone()); + let restored = self.restore_page_permissions(restore_protections); restore_trampoline_rx(self, state); restored } @@ -1580,7 +1626,34 @@ mod tests { use crate::UserPtrMut; use crate::syscalls::tests::{TestPlatform as Platform, create_file, init_platform}; - fn file_mapping_permissions( + fn runtime_patch_state( + file_mappings: BTreeSet<(usize, usize)>, + trampoline_addr: usize, + trampoline_mapped: bool, + ) -> ElfPatchState { + ElfPatchState { + pre_patched: false, + trampoline_file_offset: 0, + trampoline_file_size: 0, + trampoline_addr, + #[cfg(target_arch = "aarch64")] + load_span: None, + trampoline_cursor: 0, + trampoline_mapped, + trampoline_mapped_len: 0, + runtime_patches_committed: false, + #[cfg(target_arch = "aarch64")] + trampoline_invalidated: false, + #[cfg(target_arch = "aarch64")] + code_metadata: None, + #[cfg(target_arch = "aarch64")] + trampoline_capacity: 0, + file_mappings, + patched_ranges: BTreeSet::new(), + } + } + + fn mapping_permissions( task: &Task, address: UserPtrMut, ) -> MemoryRegionPermissions { @@ -1602,7 +1675,7 @@ mod tests { let address = task .do_mmap_file(None, PAGE_SIZE, prot, MapFlags::MAP_PRIVATE, fd, 0) .expect("file mapping should succeed"); - assert_eq!(file_mapping_permissions(task, address), expected); + assert_eq!(mapping_permissions(task, address), expected); task.sys_munmap(address, PAGE_SIZE) .expect("test mapping should unmap"); } @@ -1616,29 +1689,10 @@ mod tests { .expect("test file should open"), ) .expect("file descriptor should fit i32"); - task.global.elf_patch_cache.lock().insert( - fd, - ElfPatchState { - pre_patched: false, - trampoline_file_offset: 0, - trampoline_file_size: 0, - trampoline_addr: 0, - #[cfg(target_arch = "aarch64")] - load_span: None, - trampoline_cursor: 0, - trampoline_mapped: true, - trampoline_mapped_len: 0, - runtime_patches_committed: false, - #[cfg(target_arch = "aarch64")] - trampoline_invalidated: false, - #[cfg(target_arch = "aarch64")] - code_metadata: None, - #[cfg(target_arch = "aarch64")] - trampoline_capacity: 0, - file_mappings: BTreeSet::new(), - patched_ranges: BTreeSet::new(), - }, - ); + task.global + .elf_patch_cache + .lock() + .insert(fd, runtime_patch_state(BTreeSet::new(), 0, true)); for (prot, permissions) in [ (ProtFlags::PROT_NONE, MemoryRegionPermissions::empty()), @@ -1692,7 +1746,7 @@ mod tests { Err(Errno::EINVAL) ); assert_eq!( - file_mapping_permissions(&task, address), + mapping_permissions(&task, address), MemoryRegionPermissions::READ ); task.sys_munmap(address, PAGE_SIZE) @@ -1719,7 +1773,7 @@ mod tests { Err(Errno::EACCES) ); assert_eq!( - file_mapping_permissions(&task, address), + mapping_permissions(&task, address), MemoryRegionPermissions::READ ); task.sys_munmap(address, PAGE_SIZE) @@ -1729,6 +1783,146 @@ mod tests { task.sys_close(fd).expect("test file should close"); } + #[cfg(target_arch = "x86_64")] + #[test] + fn mprotect_rewrites_across_permission_boundaries() { + let task = init_platform(); + let mut content = alloc::vec![0x90; 2 * PAGE_SIZE]; + content[PAGE_SIZE - 1] = 0x0f; + content[PAGE_SIZE] = 0x05; + create_file(&task, "/mprotect-boundary", &content); + let fd = i32::try_from( + task.sys_open("/mprotect-boundary", OFlags::RDONLY, Mode::empty()) + .expect("test file should open"), + ) + .expect("file descriptor should fit i32"); + let address = task + .do_mmap_file( + None, + 2 * PAGE_SIZE, + ProtFlags::PROT_READ, + MapFlags::MAP_PRIVATE, + fd, + 0, + ) + .expect("file mapping should succeed"); + task.global.elf_patch_cache.lock().insert( + fd, + runtime_patch_state( + BTreeSet::from([(address.as_usize(), 2 * PAGE_SIZE)]), + address.as_usize() + 2 * PAGE_SIZE, + false, + ), + ); + + task.sys_mprotect( + UserPtrMut::from_usize(address.as_usize() + PAGE_SIZE), + PAGE_SIZE, + ProtFlags::PROT_READ_WRITE, + ) + .expect("second page permissions should change"); + task.sys_mprotect(address, 2 * PAGE_SIZE, ProtFlags::PROT_READ_EXEC) + .expect("whole mapping should become executable"); + + let rewritten = UserPtrMut::::from_usize(address.as_usize() + PAGE_SIZE - 1) + .to_owned_slice::(2) + .expect("rewritten bytes should remain readable"); + assert_ne!(rewritten.as_ref(), &[0x0f, 0x05]); + + task.sys_munmap(address, 2 * PAGE_SIZE) + .expect("test mapping should unmap"); + task.sys_close(fd).expect("test file should close"); + } + + #[cfg(all(target_arch = "x86_64", target_os = "linux"))] + #[test] + fn mprotect_patches_only_mapped_runs() { + let task = init_platform(); + let mut content = alloc::vec![0x90; 3 * PAGE_SIZE]; + content[PAGE_SIZE / 2] = 0x0f; + content[PAGE_SIZE / 2 + 1] = 0x05; + create_file(&task, "/mprotect-mapped-run", &content); + let fd = i32::try_from( + task.sys_open("/mprotect-mapped-run", OFlags::RDONLY, Mode::empty()) + .expect("test file should open"), + ) + .expect("file descriptor should fit i32"); + let address = task + .do_mmap_file( + None, + 3 * PAGE_SIZE, + ProtFlags::PROT_READ, + MapFlags::MAP_PRIVATE, + fd, + 0, + ) + .expect("file mapping should succeed"); + task.global.elf_patch_cache.lock().insert( + fd, + runtime_patch_state( + BTreeSet::from([(address.as_usize(), 3 * PAGE_SIZE)]), + address.as_usize() + 3 * PAGE_SIZE, + false, + ), + ); + + assert_eq!( + task.sys_mremap(address, 3 * PAGE_SIZE, PAGE_SIZE, MRemapFlags::empty(), 0,) + .expect("file mapping should shrink") + .as_usize(), + address.as_usize() + ); + task.sys_mprotect(address, 3 * PAGE_SIZE, ProtFlags::PROT_READ_EXEC) + .expect("mapped portion should become executable"); + + let rewritten = UserPtrMut::::from_usize(address.as_usize() + PAGE_SIZE / 2) + .to_owned_slice::(2) + .expect("rewritten bytes should remain readable"); + assert_ne!(rewritten.as_ref(), &[0x0f, 0x05]); + + task.sys_munmap(address, PAGE_SIZE) + .expect("test mapping should unmap"); + task.sys_close(fd).expect("test file should close"); + } + + #[test] + fn permission_restoration_continues_after_error() { + let task = init_platform(); + let missing = task + .do_mmap_anonymous( + None, + PAGE_SIZE, + ProtFlags::PROT_READ_WRITE, + MapFlags::MAP_ANONYMOUS | MapFlags::MAP_PRIVATE, + ) + .expect("first test mapping should succeed"); + let surviving = task + .do_mmap_anonymous( + None, + PAGE_SIZE, + ProtFlags::PROT_READ_WRITE, + MapFlags::MAP_ANONYMOUS | MapFlags::MAP_PRIVATE, + ) + .expect("second test mapping should succeed"); + task.sys_munmap(missing, PAGE_SIZE) + .expect("first test mapping should unmap"); + + assert!( + task.restore_page_permissions(&[ + (missing.as_usize(), PAGE_SIZE, ProtFlags::PROT_READ), + (surviving.as_usize(), PAGE_SIZE, ProtFlags::PROT_READ), + ]) + .is_err() + ); + assert_eq!( + mapping_permissions(&task, surviving), + MemoryRegionPermissions::READ + ); + + task.sys_munmap(surviving, PAGE_SIZE) + .expect("second test mapping should unmap"); + } + #[test] fn full_capacity_anywhere_precedes_preferred_one_page() { let calls = core::cell::RefCell::new(alloc::vec::Vec::new()); From ed1ea17ad42d9401ed0e659bdfa1fa6ef975435d Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 14 Sep 2026 13:39:04 -0700 Subject: [PATCH 6/8] Normalize Cargo lockfile ordering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- Cargo.lock | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df449d5580..fd1ed7ab01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1758,6 +1758,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "litebox_platform_windows_userland" +version = "0.1.0" +dependencies = [ + "litebox", + "litebox_common_linux", + "litebox_platform", + "windows-sys 0.60.2", + "zerocopy", +] + [[package]] name = "litebox_runner_linux_on_macos_userland" version = "0.1.0" @@ -1774,17 +1785,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "litebox_platform_windows_userland" -version = "0.1.0" -dependencies = [ - "litebox", - "litebox_common_linux", - "litebox_platform", - "windows-sys 0.60.2", - "zerocopy", -] - [[package]] name = "litebox_runner_linux_on_windows_userland" version = "0.1.0" From 722a94fb55c6e7c6652d68aba421a5e8389429a7 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 14 Sep 2026 13:49:16 -0700 Subject: [PATCH 7/8] Simplify page permission updates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox/src/mm/mod.rs | 68 ++++++++++++++++------------------ litebox_common_linux/src/mm.rs | 2 +- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/litebox/src/mm/mod.rs b/litebox/src/mm/mod.rs index c24d08de3e..311826d5ba 100644 --- a/litebox/src/mm/mod.rs +++ b/litebox/src/mm/mod.rs @@ -524,33 +524,23 @@ where unsafe { vmem.reset_pages(range, anonymous_only) } } - /// Internal common function used by `make_pages_*` to change page permissions. - fn change_page_permissions( - &self, - ptr: Platform::RawMutPointer, - len: usize, - new_permissions: MemoryRegionPermissions, - ) -> Result<(), VmemProtectError> { - let mut vmem = self.vmem.write(); - let start = ptr.as_usize(); - let range = PageRange::new(start, start + len) - .ok_or(VmemProtectError::InvalidRange(start..start + len))?; - unsafe { vmem.protect_mapping(range, new_permissions) } - } - /// Change pages to the requested permissions. /// /// # Safety /// /// The caller must ensure no accesses conflict with the permission change. /// Callers should avoid writable executable mappings unless they are strictly required. - pub unsafe fn set_page_permissions( + pub unsafe fn change_page_permissions( &self, ptr: Platform::RawMutPointer, len: usize, - permissions: MemoryRegionPermissions, + new_permissions: MemoryRegionPermissions, ) -> Result<(), VmemProtectError> { - self.change_page_permissions(ptr, len, permissions) + let mut vmem = self.vmem.write(); + let start = ptr.as_usize(); + let range = PageRange::new(start, start + len) + .ok_or(VmemProtectError::InvalidRange(start..start + len))?; + unsafe { vmem.protect_mapping(range, new_permissions) } } /// Make pages readable and writable. @@ -563,11 +553,13 @@ where ptr: Platform::RawMutPointer, len: usize, ) -> Result<(), VmemProtectError> { - self.change_page_permissions( - ptr, - len, - MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE, - ) + unsafe { + self.change_page_permissions( + ptr, + len, + MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE, + ) + } } /// Make pages readable and executable. @@ -580,11 +572,13 @@ where ptr: Platform::RawMutPointer, len: usize, ) -> Result<(), VmemProtectError> { - self.change_page_permissions( - ptr, - len, - MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC, - ) + unsafe { + self.change_page_permissions( + ptr, + len, + MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC, + ) + } } /// Make pages readable only. @@ -597,7 +591,7 @@ where ptr: Platform::RawMutPointer, len: usize, ) -> Result<(), VmemProtectError> { - self.change_page_permissions(ptr, len, MemoryRegionPermissions::READ) + unsafe { self.change_page_permissions(ptr, len, MemoryRegionPermissions::READ) } } /// Make pages inaccessible. @@ -610,7 +604,7 @@ where ptr: Platform::RawMutPointer, len: usize, ) -> Result<(), VmemProtectError> { - self.change_page_permissions(ptr, len, MemoryRegionPermissions::empty()) + unsafe { self.change_page_permissions(ptr, len, MemoryRegionPermissions::empty()) } } /// Make pages readable, writable and executable. @@ -634,13 +628,15 @@ where ptr: Platform::RawMutPointer, len: usize, ) -> Result<(), VmemProtectError> { - self.change_page_permissions( - ptr, - len, - MemoryRegionPermissions::READ - | MemoryRegionPermissions::WRITE - | MemoryRegionPermissions::EXEC, - ) + unsafe { + self.change_page_permissions( + ptr, + len, + MemoryRegionPermissions::READ + | MemoryRegionPermissions::WRITE + | MemoryRegionPermissions::EXEC, + ) + } } /// Register an already-allocated memory region in the VMA tracker. diff --git a/litebox_common_linux/src/mm.rs b/litebox_common_linux/src/mm.rs index 0ae4e83e54..8bd59b7060 100644 --- a/litebox_common_linux/src/mm.rs +++ b/litebox_common_linux/src/mm.rs @@ -145,7 +145,7 @@ pub fn sys_mprotect< let addr = addr.to_platform_ptr::(); let permissions = memory_region_permissions(&prot).ok_or(Errno::EINVAL)?; - unsafe { pm.set_page_permissions(addr, len, permissions) }.map_err(Errno::from) + unsafe { pm.change_page_permissions(addr, len, permissions) }.map_err(Errno::from) } /// Handle syscall `mremap` From e04ec7a92f89fad4fe5631af98fb4f52c405dacd Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 14 Sep 2026 14:00:37 -0700 Subject: [PATCH 8/8] Remove redundant macOS startup test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- .../tests/runner.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/litebox_runner_linux_on_macos_userland/tests/runner.rs b/litebox_runner_linux_on_macos_userland/tests/runner.rs index 04b82ad2bc..2449341a64 100644 --- a/litebox_runner_linux_on_macos_userland/tests/runner.rs +++ b/litebox_runner_linux_on_macos_userland/tests/runner.rs @@ -106,17 +106,6 @@ fn phdr( } const EXIT_42: &[u32] = &[0xd2800540, 0xd2800ba8, 0xd4000001]; // x0=42; x8=exit; svc #0 -#[test] -fn filesystem_startup_fails_closed_without_broker_support() { - let fixture = Fixture::new(); - let output = fixture.run(&[]); - assert!(!output.status.success()); - assert!( - String::from_utf8_lossy(&output.stderr) - .contains("filesystem startup on macOS requires broker support") - ); -} - #[test] #[ignore = "macOS runner requires broker support"] fn bad_syscall_pointer_returns_efault_without_host_crash() {