diff --git a/PLAN.md b/PLAN.md index 17d7fb8..9a9ccc5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,7 +1,7 @@ # AArch64 ELF-to-WebAssembly Browser Runtime ## Engineering Build Plan and Agent Handoff -**Status:** Phase 5 complete; Phase 6 in-browser C compilation next +**Status:** Phase 5 complete; Phase 6 in progress (`execve` checkpoint complete) **Primary implementation language:** Rust **Initial browser target:** Google Chrome **Guest architecture:** AArch64, little-endian, Linux userspace @@ -1871,6 +1871,8 @@ The first Phase 5 checkpoint implemented a checked-in multi-file project that im Phase 5 is complete. `guest-tests/cpython/package-packaging.sh` downloads the official pure-Python `packaging` 26.2 wheel into the repository's ignored `.tmp` directory, verifies its pinned SHA-256 digest, preserves its package and distribution metadata, and emits an independent filesystem image. The shared project imports that wheel, checks its version and `METADATA`, and retains the multi-file result plus deliberate traceback assertions. Native and opt-in Chromium regressions install the standard library, project, and dependency as separate ordered overlays and pass under a 120-million-instruction cap. Chromium image operations now expose a pending state, disable conflicting controls, and acknowledge request identities, closing a race discovered by consecutive installs. Project and package files persist through the existing OPFS snapshot path. Guest-side dependency resolution, network indexes, native wheels, and shared extensions remain deferred. Every Phase 5 deliverable and acceptance criterion has an implementation and regression; Phase 6 begins with a packaged AArch64 Clang/LLD/musl toolchain and child-process compiler orchestration. +The first Phase 6 checkpoint implements AArch64 Linux `execve` as atomic process-image replacement from the guest filesystem. The runtime bounds and copies the pathname, argument vector, environment vector, and executable before loading a fresh static ELF image; it preserves credentials, the current directory, the signal mask, resource counters, trace history, and ordinary descriptors while resetting process-image state, signal dispositions, the alternate signal stack, pending input, and anonymous-memory placement. Descriptors carrying `O_CLOEXEC` are closed only after the replacement image loads successfully. A deterministic two-ELF fixture validates reconstructed `argv`/`envp` plus retained and close-on-exec descriptors through the native runtime, CLI, browser-runtime native host, and a real Chromium Worker using the same installable filesystem image. This establishes process replacement, not yet concurrent child processes: the next checkpoint is a minimal packaged Clang/LLD/musl toolchain and the smallest additional orchestration/syscall surface its observed execution trace requires. + Do not begin the full web IDE before item 30 passes. --- diff --git a/README.md b/README.md index dfaa6d6..4f1e55a 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Run an ELF through the native interpreter and propagate its guest exit status: cargo run -p binarrow-cli -- run path/to/aarch64-static.elf [guest arguments...] ``` -The runnable instruction/syscall profile remains fixture-driven. The current static Rust program adds single-thread atomics and barriers, 128-bit vector moves/stores, byte popcount and reduction, multiplication/division, and signed shifts to the earlier libc instruction path. The runtime implements `openat`, `close`, `lseek`, file `read`/`write`, polling, deterministic process/signal setup, anonymous memory management, terminal output, and exit. Invalid guest arguments return Linux errno values; instruction, syscall, committed-memory, ephemeral-filesystem, open-file, and combined-output limits are enforced before host side effects. +The runnable instruction/syscall profile remains fixture-driven. The current static Rust program adds single-thread atomics and barriers, 128-bit vector moves/stores, byte popcount and reduction, multiplication/division, and signed shifts to the earlier libc instruction path. The runtime implements `openat`, `close`, `lseek`, file `read`/`write`, polling, deterministic process/signal setup, static-ELF `execve`, anonymous memory management, terminal output, and exit. Invalid guest arguments return Linux errno values; instruction, syscall, committed-memory, ephemeral-filesystem, open-file, and combined-output limits are enforced before host side effects. The native CPython checkpoint is reproducible without committing its large generated artifacts. `guest-tests/cpython/build.sh` creates a statically linked @@ -77,6 +77,13 @@ The verified multi-file and third-party imports, package-image overlays, traceback behavior, and current limitations are summarized in [`docs/cpython-compatibility.md`](docs/cpython-compatibility.md). +Phase 6 now has its first process-orchestration primitive. The checked-in +`guest-tests/execve-launcher` parent replaces itself with a second static ELF +loaded from an installable project image. Native and Chromium regressions +verify rebuilt arguments and environment plus ordinary and close-on-exec file +descriptors. Concurrent children, pipes, and the Clang/LLD/musl package remain +the next compilation milestones. + The browser build generates its Memory64, JSPI, and P-code `.wasm` probes before starting Vite. Generated artifacts are not committed. Select **Uploaded AArch64 ELF** to run an external static executable with a chosen `argv[0]` and one argument per line; the executable is transferred directly to the runtime Worker. Run the opt-in Phase 4 interpreter/translator benchmark in Chromium with all diff --git a/crates/browser-runtime/src/lib.rs b/crates/browser-runtime/src/lib.rs index 0aad5d0..3e425ff 100644 --- a/crates/browser-runtime/src/lib.rs +++ b/crates/browser-runtime/src/lib.rs @@ -101,6 +101,11 @@ const SYSTEM_SERVICES_ELF: &[u8] = include_bytes!("../../../guest-tests/system-services/system-services.aarch64.elf"); const TERMINAL_INPUT_ELF: &[u8] = include_bytes!("../../../guest-tests/terminal-input/terminal-input.aarch64.elf"); +const EXECVE_LAUNCHER_ELF: &[u8] = + include_bytes!("../../../guest-tests/execve-launcher/execve-launcher.aarch64.elf"); +#[cfg(test)] +const EXECVE_TOOLCHAIN_IMAGE: &[u8] = + include_bytes!("../../../guest-tests/execve-launcher/toolchain.bnfs"); /// Browser-safe result returned after a guest exits or stops diagnostically. #[wasm_bindgen] @@ -1134,6 +1139,7 @@ fn fixture(name: &str) -> Option<(&'static [u8], &'static [u8])> { "vfs-lifecycle" => Some((VFS_LIFECYCLE_ELF, b"/vfs-lifecycle")), "system-services" => Some((SYSTEM_SERVICES_ELF, b"/system-services")), "terminal-input" => Some((TERMINAL_INPUT_ELF, b"/terminal-input")), + "execve-launcher" => Some((EXECVE_LAUNCHER_ELF, b"/execve-launcher")), _ => None, } } @@ -1207,9 +1213,9 @@ mod tests { use binarrow_runtime_core::GuestAddress; use super::{ - BrowserBlockExecutor, COMPILER_HELLO_ELF, Credentials, Interpreter, ProcessConfig, - ProcessParameters, execute_fixture, fixture, load_process, start_fixture, start_program, - translate_fixture_entry_inner, + BrowserBlockExecutor, COMPILER_HELLO_ELF, Credentials, EXECVE_TOOLCHAIN_IMAGE, Interpreter, + ProcessConfig, ProcessParameters, execute_fixture, fixture, load_process, start_fixture, + start_program, translate_fixture_entry_inner, }; const DEFAULT_INSTRUCTIONS: u64 = 10_000_000; @@ -1265,6 +1271,29 @@ mod tests { } } + #[test] + fn replaces_a_browser_guest_from_the_filesystem() { + let result = execute_fixture( + "execve-launcher", + DEFAULT_INSTRUCTIONS, + DEFAULT_SYSCALLS, + DEFAULT_OUTPUT, + DEFAULT_MEMORY, + DEFAULT_FILESYSTEM, + EXECVE_TOOLCHAIN_IMAGE, + ); + + assert_eq!(result.outcome, "exited"); + assert!(result.diagnostic_code.is_empty()); + assert_eq!(result.stdout, "exec child phase6\n"); + assert!(result.stderr.is_empty()); + assert_eq!(result.exit_code, 0); + assert_eq!(result.dispatched_syscalls, 8); + assert!(result.trace.contains("execve(path=")); + assert!(result.trace.contains("fstat(fd=3")); + assert!(result.trace.contains("fstat(fd=4")); + } + #[test] fn reports_instruction_exhaustion_as_structured_diagnostic() { let result = execute_fixture( diff --git a/crates/cli/tests/run.rs b/crates/cli/tests/run.rs index 7647d5b..74afcee 100644 --- a/crates/cli/tests/run.rs +++ b/crates/cli/tests/run.rs @@ -96,6 +96,25 @@ fn run_command_installs_project_images_over_a_base_snapshot() { fs::remove_file(image_path).unwrap(); } +#[test] +fn run_command_replaces_the_guest_from_an_installed_image() { + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../guest-tests/execve-launcher/execve-launcher.aarch64.elf"); + let image = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../guest-tests/execve-launcher/toolchain.bnfs"); + let output = Command::new(env!("CARGO_BIN_EXE_binarrow")) + .arg("run") + .arg("--filesystem-install") + .arg(image) + .arg(fixture) + .output() + .expect("binarrow should start"); + + assert_eq!(output.status.code(), Some(0)); + assert_eq!(output.stdout, b"exec child phase6\n"); + assert!(output.stderr.is_empty()); +} + #[test] fn trace_command_prints_syscalls_after_guest_output() { let fixture = TempFixture::new("trace", &hello_aarch64_elf(0)); diff --git a/crates/linux-abi/src/lib.rs b/crates/linux-abi/src/lib.rs index 4261d37..974b4e7 100644 --- a/crates/linux-abi/src/lib.rs +++ b/crates/linux-abi/src/lib.rs @@ -31,6 +31,7 @@ pub enum Syscall { RtSigprocmask = 135, Gettid = 178, Munmap = 215, + Execve = 221, Mmap = 222, Mprotect = 226, Getrandom = 278, @@ -68,6 +69,7 @@ impl Syscall { 135 => Some(Self::RtSigprocmask), 178 => Some(Self::Gettid), 215 => Some(Self::Munmap), + 221 => Some(Self::Execve), 222 => Some(Self::Mmap), 226 => Some(Self::Mprotect), 278 => Some(Self::Getrandom), @@ -87,6 +89,8 @@ impl Syscall { #[repr(i32)] pub enum Errno { NoEntry = 2, + ArgumentListTooLong = 7, + ExecutableFormat = 8, BadFileDescriptor = 9, OutOfMemory = 12, PermissionDenied = 13, @@ -144,6 +148,7 @@ mod tests { assert_eq!(Syscall::from_number(135), Some(Syscall::RtSigprocmask)); assert_eq!(Syscall::from_number(178), Some(Syscall::Gettid)); assert_eq!(Syscall::from_number(215), Some(Syscall::Munmap)); + assert_eq!(Syscall::from_number(221), Some(Syscall::Execve)); assert_eq!(Syscall::from_number(222), Some(Syscall::Mmap)); assert_eq!(Syscall::from_number(226), Some(Syscall::Mprotect)); assert_eq!(Syscall::from_number(278), Some(Syscall::Getrandom)); diff --git a/crates/linux-runtime/src/lib.rs b/crates/linux-runtime/src/lib.rs index e20b9c8..4042c9f 100644 --- a/crates/linux-runtime/src/lib.rs +++ b/crates/linux-runtime/src/lib.rs @@ -14,7 +14,7 @@ use binarrow_host_api::{ HostTerminal, NullFileSystem, TerminalInputRead, TerminalStream, }; use binarrow_linux_abi::{Errno, Syscall}; -use binarrow_loader::ProcessImage; +use binarrow_loader::{Credentials, ProcessConfig, ProcessImage, ProcessParameters, load_process}; use binarrow_runtime_core::{GuestAddress, ResourceLimit, ResourceLimits, Trap}; const STANDARD_INPUT: u64 = 0; @@ -24,6 +24,8 @@ const MAIN_THREAD_ID: u64 = 1; const FIRST_FILE_DESCRIPTOR: u32 = 3; const AT_FDCWD: u64 = (-100_i64).cast_unsigned(); const MAX_PATH_BYTES: usize = 4096; +const MAX_EXEC_VECTOR_ENTRIES: usize = 4096; +const MAX_EXEC_VECTOR_BYTES: usize = 1024 * 1024; const OPEN_ACCESS_MASK: u64 = 3; const OPEN_CREATE: u64 = 0x40; const OPEN_EXCLUSIVE: u64 = 0x80; @@ -233,6 +235,11 @@ impl fmt::Display for SyscallEvent { )?; } Some(Syscall::Gettid) => write!(formatter, "gettid()")?, + Some(Syscall::Execve) => write!( + formatter, + "execve(path={:#x}, argv={:#x}, envp={:#x})", + self.arguments[0], self.arguments[1], self.arguments[2], + )?, Some(syscall @ (Syscall::ClockGettime | Syscall::Getrandom)) => { format_system_syscall(formatter, syscall, self.arguments)?; } @@ -443,6 +450,8 @@ pub struct Process { descriptor_flags: BTreeMap, current_directory: Vec, executable_path: Vec, + config: ProcessConfig, + credentials: Credentials, trace: Vec, pending_input: Option, } @@ -473,6 +482,8 @@ impl Process { descriptor_flags: BTreeMap::new(), current_directory: INITIAL_CURRENT_DIRECTORY.to_vec(), executable_path: image.executable_path, + config: image.config, + credentials: image.credentials, trace: Vec::new(), pending_input: None, }) @@ -737,6 +748,7 @@ impl Process { self.set_return(MAIN_THREAD_ID); } Some(Syscall::Gettid) => self.set_return(MAIN_THREAD_ID), + Some(Syscall::Execve) => self.dispatch_execve(filesystem, system), Some(Syscall::ClockGettime) => self.dispatch_clock_gettime(system), Some(Syscall::SchedGetaffinity) => self.dispatch_sched_getaffinity(), Some(Syscall::Sigaltstack) => self.dispatch_sigaltstack(), @@ -756,6 +768,175 @@ impl Process { Ok(None) } + fn dispatch_execve( + &mut self, + filesystem: &mut F, + system: &mut S, + ) { + let path = match self.read_guest_path(GuestAddress::new(self.register(0))) { + Ok(path) if !path.is_empty() => self.absolute_guest_path(path), + Ok(_) => { + self.set_return(Errno::NoEntry.return_value()); + return; + } + Err(error) => { + self.set_return(error.return_value()); + return; + } + }; + let argv = match self.read_exec_vector(GuestAddress::new(self.register(1))) { + Ok(argv) if !argv.is_empty() => argv, + Ok(_) => { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + Err(error) => { + self.set_return(error.return_value()); + return; + } + }; + let envp = match self.read_exec_vector(GuestAddress::new(self.register(2))) { + Ok(envp) => envp, + Err(error) => { + self.set_return(error.return_value()); + return; + } + }; + let executable = match self.read_executable(filesystem, &path) { + Ok(executable) => executable, + Err(error) => { + self.set_return(error.return_value()); + return; + } + }; + let mut random_bytes = [0; 16]; + system.fill_random(&mut random_bytes); + let Ok(image) = load_process( + &executable, + &ProcessParameters { + argv, + envp, + random_bytes, + credentials: self.credentials, + }, + self.config, + ) else { + self.set_return(Errno::ExecutableFormat.return_value()); + return; + }; + let interpreter = + Interpreter::new().expect("the AArch64 language was initialized for this process"); + + self.close_on_exec_descriptors(filesystem); + self.state = Aarch64State::new(image.initial_state.pc, image.initial_state.sp); + self.memory = image.memory; + self.interpreter = interpreter; + self.limits = image.limits; + self.clear_child_tid = None; + self.signal_actions = [SignalAction::default(); LINUX_SIGNAL_COUNT]; + self.signal_stack = SignalStack::default(); + self.next_mmap_address = GuestAddress::new(MMAP_ARENA_START); + self.executable_path = image.executable_path; + self.config = image.config; + self.credentials = image.credentials; + self.pending_input = None; + } + + fn read_exec_vector(&self, address: GuestAddress) -> Result>, Errno> { + if address == GuestAddress::NULL { + return Ok(Vec::new()); + } + let mut vector = Vec::new(); + let mut total_bytes = 0_usize; + for index in 0..MAX_EXEC_VECTOR_ENTRIES { + let offset = u64::try_from(index) + .expect("exec vector index fits u64") + .checked_mul(8) + .ok_or(Errno::Fault)?; + let pointer_address = address.checked_add(offset).ok_or(Errno::Fault)?; + let mut pointer_bytes = [0; 8]; + self.memory + .read_exact(pointer_address, &mut pointer_bytes) + .map_err(|_| Errno::Fault)?; + let pointer = u64::from_le_bytes(pointer_bytes); + if pointer == 0 { + return Ok(vector); + } + let value = self + .read_guest_path(GuestAddress::new(pointer)) + .map_err(|error| { + if error == Errno::InvalidArgument { + Errno::ArgumentListTooLong + } else { + error + } + })?; + total_bytes = total_bytes + .checked_add(value.len() + 1) + .filter(|total| *total <= MAX_EXEC_VECTOR_BYTES) + .ok_or(Errno::ArgumentListTooLong)?; + vector.push(value); + } + Err(Errno::ArgumentListTooLong) + } + + fn read_executable( + &self, + filesystem: &mut F, + path: &[u8], + ) -> Result, Errno> { + let metadata = filesystem.metadata(path).map_err(filesystem_error_errno)?; + if metadata.file_type != FileType::Regular { + return Err(Errno::PermissionDenied); + } + if metadata.size > self.limits.max_filesystem_bytes { + return Err(Errno::OutOfMemory); + } + let size = usize::try_from(metadata.size).map_err(|_| Errno::OutOfMemory)?; + let handle = filesystem + .open( + path, + FileOpenOptions { + access: FileAccess::ReadOnly, + flags: FileOpenFlags::NONE, + }, + ) + .map_err(filesystem_error_errno)?; + let result = (|| { + let mut executable = vec![0; size]; + let mut filled = 0; + while filled < executable.len() { + let read = filesystem + .read(handle, &mut executable[filled..]) + .map_err(filesystem_error_errno)?; + if read == 0 { + return Err(Errno::ExecutableFormat); + } + filled += read; + } + Ok(executable) + })(); + let _ = filesystem.close(handle); + result + } + + fn close_on_exec_descriptors(&mut self, filesystem: &mut F) { + let descriptors = self + .descriptor_flags + .iter() + .filter_map(|(descriptor, flags)| { + (flags & DESCRIPTOR_CLOEXEC != 0).then_some(*descriptor) + }) + .collect::>(); + for descriptor in descriptors { + if let Some(handle) = self.file_descriptors.remove(&descriptor) { + let _ = filesystem.close(handle); + } + self.descriptor_paths.remove(&descriptor); + self.descriptor_flags.remove(&descriptor); + } + } + fn dispatch_getcwd(&mut self) { let buffer = GuestAddress::new(self.register(0)); let size = self.register(1); @@ -1894,6 +2075,10 @@ fn linux_terminal_stat_bytes() -> [u8; STAT_SIZE] { } const fn filesystem_error_return(error: FileSystemError) -> u64 { + filesystem_error_errno(error).return_value() +} + +const fn filesystem_error_errno(error: FileSystemError) -> Errno { match error { FileSystemError::NotFound => Errno::NoEntry, FileSystemError::AlreadyExists => Errno::AlreadyExists, @@ -1906,7 +2091,6 @@ const fn filesystem_error_return(error: FileSystemError) -> u64 { FileSystemError::NotEmpty => Errno::DirectoryNotEmpty, FileSystemError::Unsupported => Errno::NoSystemCall, } - .return_value() } #[cfg(test)] @@ -1936,6 +2120,10 @@ mod tests { const COMPILER_MESSAGE_ADDRESS: u64 = 0x100_0158; const COMPILER_HELLO_ELF: &[u8] = include_bytes!("../../../guest-tests/compiler-hello/compiler-hello.aarch64.elf"); + const EXECVE_LAUNCHER_ELF: &[u8] = + include_bytes!("../../../guest-tests/execve-launcher/execve-launcher.aarch64.elf"); + const EXECVE_TOOLCHAIN_IMAGE: &[u8] = + include_bytes!("../../../guest-tests/execve-launcher/toolchain.bnfs"); const LIBC_MESSAGE: &[u8] = b"libc hello\n"; const LIBC_MESSAGE_ADDRESS: u64 = 0x100_0200; const LIBC_CLEAR_CHILD_TID_ADDRESS: u64 = 0x103_0af0; @@ -2055,6 +2243,57 @@ mod tests { ); } + #[test] + fn execve_replaces_the_process_from_the_guest_filesystem() { + let image = load_process( + EXECVE_LAUNCHER_ELF, + &ProcessParameters { + argv: vec![b"/execve-launcher".to_vec()], + envp: Vec::new(), + random_bytes: [0x42; 16], + credentials: Credentials::default(), + }, + ProcessConfig::default(), + ) + .unwrap(); + let mut process = Process::new(image).unwrap(); + let mut terminal = RecordingTerminal::default(); + let mut filesystem = + MemoryFileSystem::from_snapshot(16 * 1024 * 1024, EXECVE_TOOLCHAIN_IMAGE).unwrap(); + + let result = process + .run_with_filesystem(&mut terminal, &mut filesystem) + .unwrap(); + + assert_eq!(result.exit_code, 0); + assert_eq!(result.dispatched_syscalls, 8); + assert_eq!(result.output_bytes, 18); + assert_eq!(terminal.standard_output, b"exec child phase6\n"); + assert!(terminal.standard_error.is_empty()); + assert_eq!(process.executable_path, b"/project/toolchain/execve-child"); + assert!(process.file_descriptors.is_empty()); + assert_eq!( + process + .trace() + .iter() + .map(|event| (event.number, event.outcome)) + .collect::>(), + [ + (56, SyscallOutcome::Returned(3)), + (56, SyscallOutcome::Returned(4)), + (221, SyscallOutcome::Returned(0)), + ( + 80, + SyscallOutcome::Returned(Errno::BadFileDescriptor.return_value()), + ), + (80, SyscallOutcome::Returned(0)), + (57, SyscallOutcome::Returned(0)), + (64, SyscallOutcome::Returned(18)), + (93, SyscallOutcome::Exited(0)), + ] + ); + } + #[test] fn static_musl_elf_writes_hello_world_and_exits() { let image = load_process( diff --git a/crates/loader/src/lib.rs b/crates/loader/src/lib.rs index 3750d97..97ff2aa 100644 --- a/crates/loader/src/lib.rs +++ b/crates/loader/src/lib.rs @@ -118,6 +118,8 @@ pub struct ProcessImage { pub executable_path: Vec, pub load_bias: u64, pub limits: ResourceLimits, + pub config: ProcessConfig, + pub credentials: Credentials, } /// Why a validated ELF could not become a process image. @@ -238,6 +240,8 @@ pub fn load_process( executable_path: parameters.argv[0].clone(), load_bias, limits: config.limits, + config, + credentials: parameters.credentials, }) } diff --git a/docs/architecture.md b/docs/architecture.md index 545800e..bd9a9fd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -72,11 +72,11 @@ Implements the Phase 2 ephemeral filesystem behind `binarrow-host-api`. It norma ### `binarrow-linux-runtime` -Consumes a loaded process image, owns its architectural execution state and descriptor table, and repeatedly runs the interpreter to structured supervisor-call stops. The dispatcher implements the process calls reached by the static Rust fixture plus `openat`, `close`, `lseek`, and regular-file `read`/`write`. File contents remain behind `HostFileSystem`; guest descriptor allocation, guest-memory copying, errno mapping, and open-file limits remain Linux-runtime responsibilities. Unsupported calls return `ENOSYS`, invalid arguments return Linux errno values, and terminal output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. Every completed dispatch also appends a project-owned trace event whose arguments are captured before return-register mutation; an explicit syscall budget bounds trace growth. +Consumes a loaded process image, owns its architectural execution state and descriptor table, and repeatedly runs the interpreter to structured supervisor-call stops. The dispatcher implements the process calls reached by the static Rust fixture plus `openat`, `close`, `lseek`, regular-file `read`/`write`, and static-ELF `execve`. Successful process replacement reads a bounded pathname and argument/environment vectors from the old address space, loads the new executable through `HostFileSystem`, rebuilds the process image, closes `O_CLOEXEC` descriptors, and retains the current directory, credentials, signal mask, ordinary descriptors, resource counters, and trace history. Failed replacement leaves the old process image and descriptors intact. File contents remain behind `HostFileSystem`; guest descriptor allocation, guest-memory copying, errno mapping, and open-file limits remain Linux-runtime responsibilities. Unsupported calls return `ENOSYS`, invalid arguments return Linux errno values, and terminal output is bounded before bytes cross the host trait. Successful termination reports the guest exit code plus instruction, syscall, and output counters. Every completed dispatch also appends a project-owned trace event whose arguments are captured before return-register mutation; an explicit syscall budget bounds trace growth. ### `binarrow-browser-runtime` -Provides the narrow wasm-bindgen boundary used by the Worker. It embeds deterministic C, Rust, and non-terminating fixtures, applies browser-supplied resource limits, invokes the production loader/runtime, captures the terminal host trait, and exposes output, exit status, counters, trace events, translation metrics, and stable structured diagnostics. Its Tier-1 executor compiles supported hot blocks, caches modules by guest address and instruction encodings, installs their exports in a bounded Worker-local WebAssembly function table, exchanges scalar architectural state through the backend ABI, and invokes the selected entry synchronously. The TypeScript controller starts one run at a time and implements unconditional cancellation by terminating and recreating the Worker; no AArch64 or Linux behavior is reimplemented in TypeScript. +Provides the narrow wasm-bindgen boundary used by the Worker. It embeds deterministic C, Rust, process-replacement, and non-terminating fixtures, applies browser-supplied resource limits, invokes the production loader/runtime, captures the terminal host trait, and exposes output, exit status, counters, trace events, translation metrics, and stable structured diagnostics. The process-replacement fixture loads its second ELF from the same imported OPFS-backed snapshot path used by ordinary project files. Its Tier-1 executor compiles supported hot blocks, caches modules by guest address and instruction encodings, installs their exports in a bounded Worker-local WebAssembly function table, exchanges scalar architectural state through the backend ABI, and invokes the selected entry synchronously. The TypeScript controller starts one run at a time and implements unconditional cancellation by terminating and recreating the Worker; no AArch64 or Linux behavior is reimplemented in TypeScript. ### `binarrow-wasm-backend` @@ -103,3 +103,5 @@ The Icicle feasibility spike is isolated under `experiments/icicle`; it is not a Phase 4 is complete for the initial Tier-1 scope: bounded profiling, scalar basic-block lowering, browser-side dynamic compilation, function-table hot dispatch, interpreter fallback, a session-local module cache, code-identity invalidation tests, and observable translation metrics are all integrated. The differential probe compares the complete scalar state ABI and next PC after interpreting and translating the same 17-instruction arithmetic/control block. An opt-in five-sample Chromium benchmark measures warmed interpreter-only and translated sessions; the recorded 278,528-instruction checkpoint is 1.50x faster through translation. Broader lowering and differential coverage remain ongoing work while Phase 5 shifts to the robust CPython environment. Phase 5 is complete. A shared native/browser CPython regression independently installs the standard-library snapshot, a multi-file project image, and a checksum-pinned official `packaging` 26.2 wheel image. The CLI's repeatable image-overlay option mirrors the browser install operation, while the browser disables conflicting controls until each asynchronous image request is acknowledged and persists the result in OPFS. The Linux runtime implements the close-on-exec ioctl requests used by CPython's directory-opening path. Project and third-party filesystem imports, retained wheel metadata, deterministic output, guest-path tracebacks, exception text, and exit status are verified in both hosts. Guest-side package resolution, network indexes, and native wheels remain outside this phase; Phase 6 moves to in-browser C compilation. + +Phase 6 is in progress. Its first checkpoint adds static `execve` replacement with bounded guest-vector ingestion and Linux-compatible close-on-exec handling. A checked-in launcher, child ELF, and installable filesystem image exercise the same path in native unit tests, the CLI, the browser-runtime native host, and Chromium. This supplies the first compiler-orchestration primitive without pretending to provide concurrent processes, pipes, or a complete toolchain yet. diff --git a/guest-tests/execve-launcher/README.md b/guest-tests/execve-launcher/README.md new file mode 100644 index 0000000..a46dcaa --- /dev/null +++ b/guest-tests/execve-launcher/README.md @@ -0,0 +1,18 @@ +# execve process-replacement checkpoint + +This Phase 6 fixture proves that one static AArch64 guest can replace itself +with another ELF loaded from the project filesystem. The launcher invokes +`execve("/project/toolchain/execve-child", argv, envp)`; the child validates +its reconstructed argument and environment vectors, prints +`exec child phase6`, and exits successfully. + +Build the deterministic launcher, child, and installable toolchain image: + +```sh +guest-tests/execve-launcher/build.sh +``` + +Zig and Rust caches stay under the repository's ignored `.tmp` directory. The +small generated ELFs and filesystem image are checked in so native and Chromium +tests execute identical artifacts without requiring a compiler during normal +test runs. diff --git a/guest-tests/execve-launcher/build.sh b/guest-tests/execve-launcher/build.sh new file mode 100755 index 0000000..de46326 --- /dev/null +++ b/guest-tests/execve-launcher/build.sh @@ -0,0 +1,59 @@ +#!/bin/sh +set -eu + +fixture_directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +workspace_directory=$(CDPATH= cd -- "$fixture_directory/../.." && pwd) +cache_directory=$workspace_directory/.tmp/execve-launcher-zig-cache +cargo_home=$workspace_directory/.tmp/cargo-home +cargo_target=$workspace_directory/.tmp/cargo-target +rust_tmp=$workspace_directory/.tmp/rust-tmp +toolchain_root=$fixture_directory/toolchain-root +launcher=$fixture_directory/execve-launcher.aarch64.elf +child=$toolchain_root/execve-child +image=$fixture_directory/toolchain.bnfs +zig_version=$(zig version) + +if [ "$zig_version" != "0.16.0" ]; then + echo "execve-launcher requires Zig 0.16.0; found $zig_version" >&2 + exit 1 +fi + +mkdir -p \ + "$cache_directory/local" \ + "$cache_directory/global" \ + "$cargo_home" \ + "$cargo_target" \ + "$rust_tmp" \ + "$toolchain_root" +export TMPDIR=$rust_tmp + +for source_and_output in "launcher.c:$launcher" "child.c:$child"; do + source=${source_and_output%%:*} + output=${source_and_output#*:} + env \ + ZIG_LOCAL_CACHE_DIR="$cache_directory/local" \ + ZIG_GLOBAL_CACHE_DIR="$cache_directory/global" \ + zig cc \ + -target aarch64-linux-musl \ + -nostdlib \ + -static \ + -fno-stack-protector \ + -O1 \ + -g0 \ + -s \ + -Wl,--build-id=none \ + -Wl,-e,_start \ + "$fixture_directory/$source" \ + -o "$output" +done + +CARGO_HOME="$cargo_home" CARGO_TARGET_DIR="$cargo_target" cargo build \ + --manifest-path "$workspace_directory/Cargo.toml" \ + --release \ + -p binarrow-cli +"$cargo_target/release/binarrow" image pack \ + --guest-root /project/toolchain \ + "$toolchain_root" \ + "$image" + +chmod 0644 "$launcher" "$child" "$image" diff --git a/guest-tests/execve-launcher/child.c b/guest-tests/execve-launcher/child.c new file mode 100644 index 0000000..1e53a04 --- /dev/null +++ b/guest-tests/execve-launcher/child.c @@ -0,0 +1,67 @@ +typedef unsigned long size_t; + +enum { + SYS_CLOSE = 57, + SYS_WRITE = 64, + SYS_FSTAT = 80, + SYS_EXIT = 93, +}; + +static long syscall3(long number, long first, long second, long third) { + register long x0 __asm__("x0") = first; + register long x1 __asm__("x1") = second; + register long x2 __asm__("x2") = third; + register long x8 __asm__("x8") = number; + __asm__ volatile("svc #0" + : "+r"(x0) + : "r"(x1), "r"(x2), "r"(x8) + : "memory"); + return x0; +} + +static int strings_equal(const char *left, const char *right) { + while (*left != 0 && *left == *right) { + ++left; + ++right; + } + return *left == *right; +} + +__attribute__((noreturn)) static void exit_guest(long status) { + (void)syscall3(SYS_EXIT, status, 0, 0); + __builtin_unreachable(); +} + +__attribute__((noreturn)) void child_main(const unsigned long *stack) { + static const char executable[] = "/project/toolchain/execve-child"; + static const char phase[] = "phase6"; + static const char environment[] = "BINARROW_EXEC=1"; + static const char success[] = "exec child phase6\n"; + unsigned char stat_buffer[128]; + size_t argc = stack[0]; + const char *const *argv = (const char *const *)&stack[1]; + const char *const *envp = &argv[argc + 1]; + + if (argc != 2 || !strings_equal(argv[0], executable) || + !strings_equal(argv[1], phase) || argv[2] != 0 || + !strings_equal(envp[0], environment) || envp[1] != 0) { + exit_guest(2); + } + if (syscall3(SYS_FSTAT, 3, (long)stat_buffer, 0) != -9 || + syscall3(SYS_FSTAT, 4, (long)stat_buffer, 0) != 0 || + syscall3(SYS_CLOSE, 4, 0, 0) != 0) { + exit_guest(4); + } + if (syscall3(SYS_WRITE, 1, (long)success, sizeof(success) - 1) != + sizeof(success) - 1) { + exit_guest(3); + } + exit_guest(0); +} + +__asm__( + ".global _start\n" + ".type _start, %function\n" + "_start:\n" + "mov x0, sp\n" + "b child_main\n"); diff --git a/guest-tests/execve-launcher/execve-launcher.aarch64.elf b/guest-tests/execve-launcher/execve-launcher.aarch64.elf new file mode 100644 index 0000000000000000000000000000000000000000..8ea0414f257dc0cd96ca1e8ee9f9e1e181c96eed GIT binary patch literal 1296 zcmb<-^>JfjWMqH=CWh?{ARZ4?&;cqDzzk(DFfceUSTL|MI54m?uraVPFfg!y#USzy z3}EdHFj@j+7y|R3%u7frkks!~mzE#)2hT84%(ycf#lk5N!-F z8m2GcX-Nvmz7tSePB4OvVt~;iP<=2Oq!uI;__QPi3m9`73A>TGMs08yimmYA87 zn!?4vzz_j-X9bjon9jgZkH%+|X0`^o9pnfX21W)Qs6qiK%~%!0z*r;4D9t0!01}g6 zU|n{|TB8*(NeF9mS2S;C&2J= zArnIg$P9+>-vvN&51kn%K4NB=xI!ADzw!V735qss6CD^Df(|h@1cB6Wa6sf9GRv=0 zU}y+}nY)4;A`Vgul0z5w$j?npDoQO^NX|*jOIIjMEh^5;&r>im)HBqxVDQPz%T6t_ zQh=-S@o|C4G3XWLrzDmnGU%md#HSS{=BCDHq!b}|40$pA_5F#WLf4NLc+Gy+o#qG9PA z#0Ft;H2n{t>Ffd2eptE!sRdzl`xT-3VR;6YFJbu^kuO2IA!P@I1tHa;`oRejA_OMy hKs{sy<{=2Evmlw7fq?;}52>hvD-?qY;?fUO2mmKIqj3NL literal 0 HcmV?d00001 diff --git a/guest-tests/execve-launcher/launcher.c b/guest-tests/execve-launcher/launcher.c new file mode 100644 index 0000000..f3cac19 --- /dev/null +++ b/guest-tests/execve-launcher/launcher.c @@ -0,0 +1,52 @@ +enum { + SYS_OPENAT = 56, + SYS_WRITE = 64, + SYS_EXIT = 93, + SYS_EXECVE = 221, + AT_FDCWD = -100, + O_CLOEXEC = 0x80000, +}; + +static long syscall4( + long number, + long first, + long second, + long third, + long fourth) { + register long x0 __asm__("x0") = first; + register long x1 __asm__("x1") = second; + register long x2 __asm__("x2") = third; + register long x3 __asm__("x3") = fourth; + register long x8 __asm__("x8") = number; + __asm__ volatile("svc #0" + : "+r"(x0) + : "r"(x1), "r"(x2), "r"(x3), "r"(x8) + : "memory"); + return x0; +} + +static long syscall3(long number, long first, long second, long third) { + return syscall4(number, first, second, third, 0); +} + +__attribute__((noreturn)) static void exit_guest(long status) { + (void)syscall3(SYS_EXIT, status, 0, 0); + __builtin_unreachable(); +} + +__attribute__((noreturn)) void _start(void) { + static const char executable[] = "/project/toolchain/execve-child"; + static const char phase[] = "phase6"; + static const char environment[] = "BINARROW_EXEC=1"; + static const char failure[] = "execve failed\n"; + const char *argv[] = {executable, phase, 0}; + const char *envp[] = {environment, 0}; + + if (syscall4(SYS_OPENAT, AT_FDCWD, (long)executable, O_CLOEXEC, 0) != 3 || + syscall4(SYS_OPENAT, AT_FDCWD, (long)executable, 0, 0) != 4) { + exit_guest(2); + } + (void)syscall3(SYS_EXECVE, (long)executable, (long)argv, (long)envp); + (void)syscall3(SYS_WRITE, 2, (long)failure, sizeof(failure) - 1); + exit_guest(1); +} diff --git a/guest-tests/execve-launcher/toolchain-root/execve-child b/guest-tests/execve-launcher/toolchain-root/execve-child new file mode 100644 index 0000000000000000000000000000000000000000..9816c096f2d5a3d6c4b864bd5bcbc98623e29996 GIT binary patch literal 1504 zcmb<-^>JfjWMqH=CWh?{Al@5Bh@b;hLV*R!U|?WyV6b3dWpH3%XJBJsV_;waNx{@P zFo3l)z-S4OVGIlmFq#8u3`iKH56T5mAR##Y2FiocP-DS{vobI+z{FwhgwY3}`d~Cn zU%=Cn6p(#Opz=!?!A3E_Xc3Uj3=9k~8l)B^6!^3x1>{E%n*fHI3{j7#JBe7#J8pe2`6yRY44lHFAv7Jn{?@pm1hjU{GLSWY__6 zjSF7`hytkt;RnnN6B!v8ME)}~U3|gJu!^(Q;fFmZ!^cK;h7fK>ho4!D4whMr3?@0O z3=`!Qn6JM6`CTBF(ZMpFkvIDx14BrcsDni|E5pRB|Nl*y85l%BYEJ+7E|3jY!=3HO z$`HcMfoe|o|Nj%RAF@uwXAVpa%p68$h7c|uho7gI7`Afoa&~jEI{frxbg;~1WH9k& zWtiCY|NjK9zpN8|7#%F_8F|2N%M^96@M2|{mRjwjhhc%vNA%2m*x_PD~9!92^k!@*WHy4>B->JYbez1v3k#2jU)7 zH+tmfrY04omMbLZB<7_nl%*CGXXfWA7#Zpr>RB-OWaee37Fj94Rr&b1z~mV8it5(d5EjN+1_lEfrXvY`lr(jN#L zK~0H(=DP@xAOiz~7gQWZL4{#l22No-U|;}YMj-|S&@U*;&q_@$(J#r*&q>Zm%*zzR`f^@5$OusjHhwz;i1&sOBIp2>QDA{G7#J8F7%UiA85|hc8Q2)u7#J8p zQZRK63}EdHFj@j+7y|NC-~9f%0H9)L5|LAfLg+VeW*{2cY_3 zG)!N>(~=aBeM_M7OBlgMF~DdMkj)GX3@{p`79YINF4}2U}l)e$iN`-pPA|63ucB@ zoUIN&>^T`eHnKB>a5Fmm%wlw~%wl9P$zf%fD6hbL_4Uv10=bM1mhp_d*$){QLb^m9 zEV5Y{CT9KrZ_3QTAOccz`p0*HY_J;cY)4jx5N-}sbGrZkpOF2Kbs|1WhNtoi8m|5#IFDUCwTp3o#?~pU}?|D19n@csDp(U zE5pQ0sM}y_yufO>vJ)8?LO3`bex6`p*y6y@5cH5)e$^vp`Bf{p8-f@a7_KCM#TX`n z#5lMhV)7~s8yguJLK+wvf{?`@FfeQdiNVa!V27wbz`(EtSsZ4z0z*R(D6AMielKuh zY6#-sfT)-EVEA~Dfg$7pv-~QUSui~i_n^AbBR@AasVKEvAvq^8FI}N5wWv5VKTpBP zP|r}$g25*+5ua9+n421(ky3=VFS lifecycle +