From ed901c00bb42157fa7f5257f490cc9914b943b44 Mon Sep 17 00:00:00 2001 From: Sangho Lee Date: Thu, 17 Sep 2026 21:01:58 -0700 Subject: [PATCH] Add test broker to Linux-on-macOS runner --- .github/workflows/ci.yml | 4 + Cargo.lock | 4 + .../Cargo.toml | 10 +- .../src/lib.rs | 80 +++++++++- .../src/test_broker.rs | 146 ++++++++++++++++++ .../tests/loader.rs | 45 ++++-- .../tests/runner.rs | 30 +++- .../tests/runner/gates.rs | 20 ++- 8 files changed, 314 insertions(+), 25 deletions(-) create mode 100644 litebox_runner_linux_on_macos_userland/src/test_broker.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46631252c1..00609643e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,6 +172,10 @@ jobs: - run: ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features $MACOS_AARCH64_CRATES - run: ./.github/tools/github_actions_run_cargo build $MACOS_AARCH64_CRATES - run: ./.github/tools/github_actions_run_cargo nextest $MACOS_AARCH64_CRATES + - name: Test Linux-on-macOS runner with in-process broker + run: >- + ./.github/tools/github_actions_run_cargo nextest + -p litebox_runner_linux_on_macos_userland --features test-broker - name: Test native macOS runner with in-process broker run: >- ./.github/tools/github_actions_run_cargo nextest diff --git a/Cargo.lock b/Cargo.lock index 0219e06d1d..9f24d188bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1790,6 +1790,10 @@ dependencies = [ "anyhow", "clap", "litebox", + "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", + "litebox_broker_protocol", "litebox_common_linux", "litebox_platform_macos_userland", "litebox_shim_linux", diff --git a/litebox_runner_linux_on_macos_userland/Cargo.toml b/litebox_runner_linux_on_macos_userland/Cargo.toml index 3d33bbbd0c..820412fc6b 100644 --- a/litebox_runner_linux_on_macos_userland/Cargo.toml +++ b/litebox_runner_linux_on_macos_userland/Cargo.toml @@ -7,9 +7,13 @@ edition = "2024" anyhow = "1" clap = { version = "4", features = ["derive"] } litebox = { path = "../litebox", version = "0.1.0" } +litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0", features = ["test-support"], optional = true } +litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0", features = ["test-support"], optional = true } +litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0", optional = true } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0", optional = true } litebox_common_linux = { path = "../litebox_common_linux", version = "0.1.0" } litebox_platform_macos_userland = { path = "../litebox_platform_macos_userland", version = "0.1.0", features = ["subpage_compat"] } -litebox_shim_linux = { path = "../litebox_shim_linux", version = "0.1.0", default-features = false, features = ["alarm_fallback"] } +litebox_shim_linux = { path = "../litebox_shim_linux", version = "0.1.0", default-features = false } litebox_util_log = { path = "../litebox_util_log", version = "0.1.0", features = ["backend_tracing"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } @@ -17,5 +21,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } tempfile = "3" litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0.1.0", default-features = false } +[features] +# Development-only in-process broker fixture. +test-broker = ["dep:litebox_broker_core", "dep:litebox_broker_host", "dep:litebox_broker_local", "dep:litebox_broker_protocol"] + [lints] workspace = true diff --git a/litebox_runner_linux_on_macos_userland/src/lib.rs b/litebox_runner_linux_on_macos_userland/src/lib.rs index 04b7c0e9b6..03fb5a5b92 100644 --- a/litebox_runner_linux_on_macos_userland/src/lib.rs +++ b/litebox_runner_linux_on_macos_userland/src/lib.rs @@ -4,11 +4,20 @@ //! Run AArch64 Linux PIE programs on an AArch64 macOS host. #![cfg(all(target_os = "macos", target_arch = "aarch64"))] +#[cfg(feature = "test-broker")] +use anyhow::Context as _; use anyhow::{Result, bail}; use clap::Parser; +#[cfg(feature = "test-broker")] +use litebox_platform_macos_userland::MacosUserland4K; use litebox_platform_macos_userland::{GuestAbi, set_guest_abi}; +#[cfg(feature = "test-broker")] +use std::ffi::CString; use std::path::PathBuf; +#[cfg(feature = "test-broker")] +mod test_broker; + #[derive(Parser, Debug)] #[command(about = "AArch64 Linux runner for macOS; broker support is required")] pub struct CliArgs { @@ -33,7 +42,6 @@ pub struct CliArgs { pub program_from_tar: bool, } -/// Returns an error until the macOS runner can connect to a broker. pub fn run(cli_args: CliArgs) -> Result { set_guest_abi(GuestAbi::Linux); tracing_subscriber::fmt() @@ -46,6 +54,72 @@ pub fn run(cli_args: CliArgs) -> Result { ) .init(); - let _ = cli_args; - bail!("filesystem startup on macOS requires broker support") + #[cfg(not(feature = "test-broker"))] + { + let _ = cli_args; + bail!("filesystem startup on macOS requires broker support") + } + + #[cfg(feature = "test-broker")] + { + let requested_program = cli_args + .program_and_arguments + .first() + .context("missing program")?; + if cli_args.program_from_tar && !requested_program.starts_with('/') { + bail!("program path in --initial-files must be absolute, got: {requested_program}"); + } + let platform = MacosUserland4K::new(); + let host_program = + (!cli_args.program_from_tar).then(|| std::path::Path::new(requested_program)); + let setup = test_broker::setup(platform, cli_args.initial_files.as_deref(), host_program)?; + let program_path = if cli_args.program_from_tar { + requested_program.as_str() + } else { + setup.program_path.as_str() + }; + let argv = cli_args + .program_and_arguments + .iter() + .map(|value| CString::new(value.as_bytes())) + .collect::, _>>() + .context("NUL in program argument")?; + 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::, _>>() + .context("NUL in environment entry")?; + let shim = setup.builder.build(); + let program = shim + .load_program( + litebox_common_linux::TaskParams { + pid: setup.process_id, + ppid: 0, + uid: 1000, + euid: 1000, + gid: 1000, + egid: 1000, + }, + setup.initial_thread, + program_path, + argv, + envp, + ) + .context("loading Linux program")?; + // SAFETY: the loader owns valid mappings and has finalized Linux gates; + // entrypoints retain those mappings until guest execution stops. + unsafe { + litebox_platform_macos_userland::run_thread( + program.entrypoints, + &mut litebox_common_linux::PtRegs::default(), + ); + } + let exit_code = program.process.wait_for_unix_shell_exit_code(); + test_broker::flush_output(&setup.stdio)?; + Ok(exit_code) + } } diff --git a/litebox_runner_linux_on_macos_userland/src/test_broker.rs b/litebox_runner_linux_on_macos_userland/src/test_broker.rs new file mode 100644 index 0000000000..1ff92598d4 --- /dev/null +++ b/litebox_runner_linux_on_macos_userland/src/test_broker.rs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! In-process broker fixture for the test-broker runner feature. + +use anyhow::{Context as _, Result, anyhow}; +use litebox::LiteBox; +use litebox_broker_core::{ + ObjectRights, PolicyEngine, + fs::{ + composer::Composer, + devices::Devices, + in_mem::{InMem, InitialNode}, + overlay::Overlay, + resolver::Resolver, + tar_ro::{EMPTY_TAR_FILE, TarRo}, + }, + random::{RandomProvider, RandomProviderError}, + test_support::{TestBrokerCoreBuilder, TestStdioProvider}, +}; +use litebox_broker_host::test_support::InProcessBrokerSetup; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::{ + fs::{FileMode, FileUser}, + stdio::StdioOutputStream, +}; +use litebox_platform_macos_userland::MacosUserland4K; +use litebox_shim_linux::LinuxShimBuilder; +use std::{ + borrow::Cow, + io::{Read as _, Write as _}, + path::Path, + sync::Arc, +}; + +const HOST_PROGRAM_PATH: &str = "/.litebox-host-program"; + +struct TestRandomProvider; + +impl RandomProvider for TestRandomProvider { + fn fill(&self, output: &mut [u8]) -> Result<(), RandomProviderError> { + output.fill(0x5a); + Ok(()) + } +} + +pub(crate) struct Setup { + pub(crate) builder: LinuxShimBuilder, + pub(crate) process_id: i32, + pub(crate) initial_thread: litebox::thread::Thread, + pub(crate) program_path: String, + pub(crate) stdio: Arc, +} + +pub(crate) fn setup( + platform: &'static MacosUserland4K, + initial_files: Option<&Path>, + host_program: Option<&Path>, +) -> Result { + let stdio = Arc::new(TestStdioProvider::default()); + let mut input = Vec::new(); + std::io::stdin() + .read_to_end(&mut input) + .context("reading test input")?; + stdio.push_input(&input); + + let directory = || InitialNode::Directory { + mode: FileMode::RWXU | FileMode::RWXG | FileMode::RWXO, + owner: FileUser::ROOT, + }; + let mut entries = vec![("/tmp".to_owned(), directory())]; + if let Some(path) = host_program { + entries.push(( + HOST_PROGRAM_PATH.to_owned(), + InitialNode::File { + mode: FileMode::RUSR + | FileMode::WUSR + | FileMode::XUSR + | FileMode::RGRP + | FileMode::XGRP + | FileMode::ROTH + | FileMode::XOTH, + owner: FileUser::ROOT, + data: std::fs::read(path) + .with_context(|| format!("reading guest program {}", path.display()))? + .into(), + }, + )); + } + let tar_data = match initial_files { + Some(path) => Cow::Owned( + std::fs::read(path) + .with_context(|| format!("reading initial files {}", path.display()))?, + ), + None => Cow::Borrowed(EMPTY_TAR_FILE), + }; + let upper = InMem::::new_initialized(entries); + let fs = Composer::builder() + .mount_nestable("/", |allocators| { + Overlay::::new( + upper, + TarRo::new(tar_data, allocators.next()), + allocators.next(), + ) + }) + .mount("/dev", Devices::new) + .build() + .map_err(|error| anyhow!("test filesystem: {error:?}"))?; + let core = TestBrokerCoreBuilder::new(PolicyEngine::with_unauthenticated_rights( + ObjectRights::all(), + )) + .with_random_provider(Arc::new(TestRandomProvider)) + .with_stdio_provider(stdio.clone()) + .with_file_service(Arc::new(Resolver::::new(fs))) + .build() + .map_err(|error| anyhow!("test broker: {error:?}"))?; + let setup = InProcessBrokerSetup::new(core); + let readiness = setup.readiness_sink(); + let (local, _startup, ()) = BrokerLocal::negotiate(setup, |setup| { + let memory = setup.shared_memory(); + Ok((setup.activate(), memory, ())) + }) + .map_err(|error| anyhow!("test broker negotiation: {error:?}"))?; + let (litebox, process_id, initial_thread) = + LiteBox::new_process_with_broker_local(platform, local); + let process_id = i32::try_from(process_id.0).context("process ID does not fit pid_t")?; + readiness.attach(litebox.broker_notification_dispatcher()); + + Ok(Setup { + builder: LinuxShimBuilder::new_with_litebox(platform, litebox, process_id), + process_id, + initial_thread, + program_path: host_program.map_or_else(String::new, |_| HOST_PROGRAM_PATH.to_owned()), + stdio, + }) +} + +pub(crate) fn flush_output(stdio: &TestStdioProvider) -> Result<()> { + for (stream, bytes) in stdio.writes() { + match stream { + StdioOutputStream::Stdout => std::io::stdout().write_all(&bytes)?, + StdioOutputStream::Stderr => std::io::stderr().write_all(&bytes)?, + } + } + Ok(()) +} diff --git a/litebox_runner_linux_on_macos_userland/tests/loader.rs b/litebox_runner_linux_on_macos_userland/tests/loader.rs index 23e9fe16b7..26e7e1078f 100644 --- a/litebox_runner_linux_on_macos_userland/tests/loader.rs +++ b/litebox_runner_linux_on_macos_userland/tests/loader.rs @@ -53,15 +53,16 @@ fn run_program(name: &str, aot: bool) { .args(["-Z", "--initial-files"]) .arg(&archive) .args(["--env", "LD_LIBRARY_PATH=/lib/aarch64-linux-gnu"]); - if from_tar { - command - .arg("--program-from-tar") - .arg(format!("/bin/{name}")); + let expected_argv0 = if from_tar { + let program = format!("/bin/{name}"); + command.arg("--program-from-tar").arg(&program); + program } else { - command.arg(root.join("bin").join(name)); - } + let program = root.join("bin").join(name); + command.arg(&program); + program.display().to_string() + }; let output = command.output().unwrap(); - // Stdout requires a broker; these tests check successful execution. assert_eq!( output.status.code(), Some(0), @@ -70,29 +71,51 @@ fn run_program(name: &str, aot: bool) { String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); + if name == "hello_world_dyn" { + let stdout = String::from_utf8(output.stdout).unwrap(); + let expected_prefix = format!( + "argv[0] = {expected_argv0}\nenvp[0] = LD_LIBRARY_PATH=/lib/aarch64-linux-gnu\nElapsed time: " + ); + assert!( + stdout.starts_with(&expected_prefix) && stdout.ends_with(" seconds\n"), + "unexpected guest stdout: {stdout:?}" + ); + } } } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + ignore = "macOS runner requires broker support" +)] fn test_load_exec_dynamic() { run_program("hello_world_dyn", false); } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + ignore = "macOS runner requires broker support" +)] fn test_load_exec_dynamic_pthreads() { run_program("hello_thread", false); } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + ignore = "macOS runner requires broker support" +)] fn test_syscall_rewriter() { run_program("hello_world_dyn", true); } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + 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 b54a19ec78..bc37b3d36e 100644 --- a/litebox_runner_linux_on_macos_userland/tests/runner.rs +++ b/litebox_runner_linux_on_macos_userland/tests/runner.rs @@ -107,7 +107,10 @@ fn phdr( const EXIT_42: &[u32] = &[0xd2800540, 0xd2800ba8, 0xd4000001]; // x0=42; x8=exit; svc #0 #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + ignore = "macOS runner requires broker support" +)] fn bad_syscall_pointer_returns_efault_without_host_crash() { let fixture = Fixture::new(); let code = [ @@ -131,7 +134,10 @@ fn bad_syscall_pointer_returns_efault_without_host_crash() { } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + 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] @@ -147,7 +153,10 @@ fn guest_memory_fault_terminates_with_linux_status() { } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + ignore = "macOS runner requires broker support" +)] fn guest_instruction_faults_deliver_sigill() { for (name, code) in [ ("undefined instruction", vec![0]), @@ -166,7 +175,10 @@ fn guest_instruction_faults_deliver_sigill() { } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + ignore = "macOS runner requires broker support" +)] fn fp_registers_survive_syscalls() { let fixture = Fixture::new(); let code = [ @@ -193,7 +205,10 @@ fn fp_registers_survive_syscalls() { } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + ignore = "macOS runner requires broker support" +)] fn rejects_fixed_address_and_incompatible_page_layouts() { let fixture = Fixture::new(); let mut binary = elf(EXIT_42); @@ -222,7 +237,10 @@ fn rejects_fixed_address_and_incompatible_page_layouts() { } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + 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 747d40fd58..29fb676a55 100644 --- a/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs +++ b/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs @@ -14,7 +14,10 @@ const X18: &[u32] = &[ ]; #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + 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. @@ -60,19 +63,28 @@ fn run_x18_fixture(aot: bool) { } #[test] -#[ignore = "macOS runner requires broker support"] +#[cfg_attr( + not(feature = "test-broker"), + 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"] +#[cfg_attr( + not(feature = "test-broker"), + 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"] +#[cfg_attr( + not(feature = "test-broker"), + 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