diff --git a/crates/browser-runtime/src/lib.rs b/crates/browser-runtime/src/lib.rs index 05d082c..65dbbec 100644 --- a/crates/browser-runtime/src/lib.rs +++ b/crates/browser-runtime/src/lib.rs @@ -2,13 +2,32 @@ use core::convert::Infallible; -use binarrow_host_api::{HostTerminal, TerminalStream}; +use binarrow_host_api::{DeterministicSystem, HostTerminal, HostTime, TerminalStream}; use binarrow_linux_runtime::{ExecutionError, Process}; use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; use binarrow_memory_fs::MemoryFileSystem; use binarrow_runtime_core::{MemoryAccess, ResourceLimit, Trap}; use wasm_bindgen::prelude::*; +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen(inline_js = " +export function binarrow_realtime_nanoseconds() { + return BigInt(Date.now()) * 1000000n; +} +export function binarrow_monotonic_nanoseconds() { + return BigInt(Math.floor(performance.now() * 1000000)); +} +export function binarrow_random_seed() { + const bytes = crypto.getRandomValues(new Uint8Array(8)); + return new DataView(bytes.buffer).getBigUint64(0, true); +} +")] +extern "C" { + fn binarrow_realtime_nanoseconds() -> u64; + fn binarrow_monotonic_nanoseconds() -> u64; + fn binarrow_random_seed() -> u64; +} + const COMPILER_HELLO_ELF: &[u8] = include_bytes!("../../../guest-tests/compiler-hello/compiler-hello.aarch64.elf"); const LIBC_HELLO_ELF: &[u8] = @@ -26,6 +45,8 @@ const PROJECT_PERSISTENCE_READ_ELF: &[u8] = include_bytes!("../../../guest-tests/project-persistence/project-persistence-read.aarch64.elf"); const VFS_LIFECYCLE_ELF: &[u8] = include_bytes!("../../../guest-tests/vfs-lifecycle/vfs-lifecycle.aarch64.elf"); +const SYSTEM_SERVICES_ELF: &[u8] = + include_bytes!("../../../guest-tests/system-services/system-services.aarch64.elf"); /// Browser-safe result returned after a guest exits or stops diagnostically. #[wasm_bindgen] @@ -150,6 +171,7 @@ fn execute_fixture( format!("unknown embedded fixture {fixture_name:?}"), ); }; + let mut system = host_system(); let mut config = ProcessConfig::default(); config.limits.instruction_budget = instruction_budget; config.limits.syscall_budget = syscall_budget; @@ -191,7 +213,7 @@ fn execute_fixture( } }; let mut terminal = CapturedTerminal::default(); - let execution = process.run_with_filesystem(&mut terminal, &mut filesystem); + let execution = process.run_with_services(&mut terminal, &mut filesystem, &mut system); let trace = process .trace() .iter() @@ -234,6 +256,31 @@ fn execute_fixture( } } +fn host_time(total_nanoseconds: u64) -> HostTime { + HostTime { + seconds: i64::try_from(total_nanoseconds / 1_000_000_000).unwrap_or(i64::MAX), + nanoseconds: (total_nanoseconds % 1_000_000_000) as u32, + } +} + +#[cfg(target_arch = "wasm32")] +fn host_system() -> DeterministicSystem { + DeterministicSystem::new( + host_time(binarrow_realtime_nanoseconds()), + host_time(binarrow_monotonic_nanoseconds()), + binarrow_random_seed(), + ) +} + +#[cfg(not(target_arch = "wasm32"))] +fn host_system() -> DeterministicSystem { + DeterministicSystem::new( + host_time(1_700_000_000_123_000_000), + host_time(1_234_000_000), + 0x4249_4e41_5252_4f57, + ) +} + impl BrowserExecution { fn diagnostic(code: &str, message: String) -> Self { Self { @@ -266,6 +313,7 @@ fn fixture(name: &str) -> Option<(&'static [u8], &'static [u8])> { Some((PROJECT_PERSISTENCE_READ_ELF, b"/project-persistence-read")) } "vfs-lifecycle" => Some((VFS_LIFECYCLE_ELF, b"/vfs-lifecycle")), + "system-services" => Some((SYSTEM_SERVICES_ELF, b"/system-services")), _ => None, } } @@ -460,4 +508,23 @@ mod tests { assert!(result.trace.contains("renameat(olddirfd=-100")); assert!(result.trace.contains("unlinkat(dirfd=-100")); } + + #[test] + fn executes_the_clock_random_dev_and_proc_fixture() { + let result = execute_fixture( + "system-services", + DEFAULT_INSTRUCTIONS, + DEFAULT_SYSCALLS, + DEFAULT_OUTPUT, + DEFAULT_MEMORY, + DEFAULT_FILESYSTEM, + &[], + ); + + assert_eq!(result.outcome, "exited"); + assert_eq!(result.exit_code, 0); + assert_eq!(result.stdout, "system services ok\n"); + assert!(result.trace.contains("clock_gettime(clock=0")); + assert!(result.trace.contains("getrandom(buffer=")); + } } diff --git a/crates/host-api/src/lib.rs b/crates/host-api/src/lib.rs index 2787331..6b468f2 100644 --- a/crates/host-api/src/lib.rs +++ b/crates/host-api/src/lib.rs @@ -11,6 +11,79 @@ pub enum TerminalStream { StandardError, } +/// Clock sources exposed by Linux `clock_gettime`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum HostClock { + Realtime, + Monotonic, +} + +/// Host-independent timestamp with a normalized nanosecond component. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct HostTime { + pub seconds: i64, + pub nanoseconds: u32, +} + +/// Clock and cryptographic-random services supplied to the guest runtime. +pub trait HostSystem { + fn clock_time(&mut self, clock: HostClock) -> HostTime; + fn fill_random(&mut self, destination: &mut [u8]); +} + +/// Reproducible clock and pseudorandom source for tests and deterministic runs. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DeterministicSystem { + realtime: HostTime, + monotonic: HostTime, + random_state: u64, +} + +impl DeterministicSystem { + #[must_use] + pub const fn new(realtime: HostTime, monotonic: HostTime, random_seed: u64) -> Self { + Self { + realtime, + monotonic, + random_state: random_seed, + } + } +} + +impl Default for DeterministicSystem { + fn default() -> Self { + Self::new( + HostTime { + seconds: 1_700_000_000, + nanoseconds: 0, + }, + HostTime { + seconds: 1, + nanoseconds: 0, + }, + 0x4249_4e41_5252_4f57, + ) + } +} + +impl HostSystem for DeterministicSystem { + fn clock_time(&mut self, clock: HostClock) -> HostTime { + match clock { + HostClock::Realtime => self.realtime, + HostClock::Monotonic => self.monotonic, + } + } + + fn fill_random(&mut self, destination: &mut [u8]) { + for byte in destination { + self.random_state ^= self.random_state << 13; + self.random_state ^= self.random_state >> 7; + self.random_state ^= self.random_state << 17; + *byte = self.random_state.to_le_bytes()[0]; + } + } +} + /// Output service supplied by a native, browser, or deterministic host. pub trait HostTerminal { type Error; diff --git a/crates/linux-abi/src/lib.rs b/crates/linux-abi/src/lib.rs index 22ff0e7..15e0db0 100644 --- a/crates/linux-abi/src/lib.rs +++ b/crates/linux-abi/src/lib.rs @@ -17,6 +17,7 @@ pub enum Syscall { Exit = 93, ExitGroup = 94, SetTidAddress = 96, + ClockGettime = 113, SchedGetaffinity = 123, Sigaltstack = 132, RtSigaction = 134, @@ -24,6 +25,7 @@ pub enum Syscall { Munmap = 215, Mmap = 222, Mprotect = 226, + Getrandom = 278, } impl Syscall { @@ -44,6 +46,7 @@ impl Syscall { 93 => Some(Self::Exit), 94 => Some(Self::ExitGroup), 96 => Some(Self::SetTidAddress), + 113 => Some(Self::ClockGettime), 123 => Some(Self::SchedGetaffinity), 132 => Some(Self::Sigaltstack), 134 => Some(Self::RtSigaction), @@ -51,6 +54,7 @@ impl Syscall { 215 => Some(Self::Munmap), 222 => Some(Self::Mmap), 226 => Some(Self::Mprotect), + 278 => Some(Self::Getrandom), _ => None, } } @@ -108,6 +112,7 @@ mod tests { assert_eq!(Syscall::from_number(93), Some(Syscall::Exit)); assert_eq!(Syscall::from_number(94), Some(Syscall::ExitGroup)); assert_eq!(Syscall::from_number(96), Some(Syscall::SetTidAddress)); + assert_eq!(Syscall::from_number(113), Some(Syscall::ClockGettime)); assert_eq!(Syscall::from_number(123), Some(Syscall::SchedGetaffinity)); assert_eq!(Syscall::from_number(132), Some(Syscall::Sigaltstack)); assert_eq!(Syscall::from_number(134), Some(Syscall::RtSigaction)); @@ -115,6 +120,7 @@ mod tests { assert_eq!(Syscall::from_number(215), Some(Syscall::Munmap)); 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)); assert_eq!(Syscall::from_number(55), None); } diff --git a/crates/linux-runtime/src/lib.rs b/crates/linux-runtime/src/lib.rs index 9b71c10..9492303 100644 --- a/crates/linux-runtime/src/lib.rs +++ b/crates/linux-runtime/src/lib.rs @@ -6,8 +6,8 @@ use std::collections::BTreeMap; use binarrow_aarch64::{Aarch64State, Interpreter, InterpreterInitializationError}; use binarrow_guest_memory::{AddressSpace, Permissions, RegionKind}; use binarrow_host_api::{ - FileAccess, FileOpenFlags, FileOpenOptions, FileSeekFrom, FileSystemError, FileType, - HostFileSystem, HostTerminal, NullFileSystem, TerminalStream, + DeterministicSystem, FileAccess, FileOpenFlags, FileOpenOptions, FileSeekFrom, FileSystemError, + FileType, HostClock, HostFileSystem, HostSystem, HostTerminal, NullFileSystem, TerminalStream, }; use binarrow_linux_abi::{Errno, Syscall}; use binarrow_loader::ProcessImage; @@ -32,6 +32,10 @@ const DIRECTORY_ENTRY_HEADER_SIZE: usize = 19; const DIRECTORY_ENTRY_ALIGNMENT: usize = 8; const DIRECTORY_TYPE: u8 = 4; const REGULAR_FILE_TYPE: u8 = 8; +const CLOCK_REALTIME: u64 = 0; +const CLOCK_MONOTONIC: u64 = 1; +const CLOCK_MONOTONIC_RAW: u64 = 4; +const GETRANDOM_ALLOWED_FLAGS: u64 = 3; const LINUX_SIGNAL_COUNT: usize = 64; const KERNEL_SIGACTION_SIZE: usize = 32; const KERNEL_SIGNAL_SET_SIZE: u64 = 8; @@ -142,6 +146,9 @@ impl fmt::Display for SyscallEvent { self.arguments[0] )?; } + Some(syscall @ (Syscall::ClockGettime | Syscall::Getrandom)) => { + format_system_syscall(formatter, syscall, self.arguments)?; + } Some(Syscall::SchedGetaffinity) => write!( formatter, "sched_getaffinity(pid={}, cpusetsize={}, mask={:#x})", @@ -238,6 +245,26 @@ fn format_vfs_syscall( } } +fn format_system_syscall( + formatter: &mut fmt::Formatter<'_>, + syscall: Syscall, + arguments: [u64; 6], +) -> fmt::Result { + match syscall { + Syscall::ClockGettime => write!( + formatter, + "clock_gettime(clock={}, timespec={:#x})", + arguments[0], arguments[1], + ), + Syscall::Getrandom => write!( + formatter, + "getrandom(buffer={:#x}, count={}, flags={:#x})", + arguments[0], arguments[1], arguments[2], + ), + _ => unreachable!("only host-system calls are delegated to this formatter"), + } +} + /// A failure outside normal Linux syscall return handling. #[derive(Debug)] pub enum ExecutionError { @@ -349,7 +376,11 @@ impl Process { &mut self, terminal: &mut T, ) -> Result> { - self.run_with_filesystem(terminal, &mut NullFileSystem) + self.run_with_services( + terminal, + &mut NullFileSystem, + &mut DeterministicSystem::default(), + ) } /// Run with an explicit synchronous filesystem service. @@ -361,6 +392,20 @@ impl Process { &mut self, terminal: &mut T, filesystem: &mut F, + ) -> Result> { + self.run_with_services(terminal, filesystem, &mut DeterministicSystem::default()) + } + + /// Run with explicit synchronous filesystem, clock, and random services. + /// + /// # Errors + /// + /// Returns the same structured execution failures as [`Self::run`]. + pub fn run_with_services( + &mut self, + terminal: &mut T, + filesystem: &mut F, + system: &mut S, ) -> Result> { loop { let remaining = self @@ -397,16 +442,19 @@ impl Process { let number = stop.supervisor_call.syscall_number; let arguments = self.syscall_arguments(); - if let Some(result) = self.dispatch_syscall(terminal, filesystem, number, arguments)? { + if let Some(result) = + self.dispatch_syscall(terminal, filesystem, system, number, arguments)? + { return Ok(result); } } } - fn dispatch_syscall( + fn dispatch_syscall( &mut self, terminal: &mut T, filesystem: &mut F, + system: &mut S, number: u64, arguments: [u64; 6], ) -> Result, ExecutionError> { @@ -440,6 +488,7 @@ impl Process { self.clear_child_tid = Some(GuestAddress::new(self.register(0))); self.set_return(MAIN_THREAD_ID); } + Some(Syscall::ClockGettime) => self.dispatch_clock_gettime(system), Some(Syscall::SchedGetaffinity) => self.dispatch_sched_getaffinity(), Some(Syscall::Sigaltstack) => self.dispatch_sigaltstack(), Some(Syscall::RtSigaction) => self.dispatch_rt_sigaction(), @@ -447,6 +496,7 @@ impl Process { Some(Syscall::Munmap) => self.dispatch_munmap(), Some(Syscall::Mmap) => self.dispatch_mmap(), Some(Syscall::Mprotect) => self.dispatch_mprotect(), + Some(Syscall::Getrandom) => self.dispatch_getrandom(system), None => self.set_return(Errno::NoSystemCall.return_value()), } self.trace.push(SyscallEvent { @@ -916,6 +966,58 @@ impl Process { self.set_return(CPU_AFFINITY_BYTES); } + fn dispatch_clock_gettime(&mut self, system: &mut S) { + let clock = match self.register(0) { + CLOCK_REALTIME => HostClock::Realtime, + CLOCK_MONOTONIC | CLOCK_MONOTONIC_RAW => HostClock::Monotonic, + _ => { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + }; + let time = system.clock_time(clock); + if time.nanoseconds >= 1_000_000_000 { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + let mut timespec = [0; 16]; + timespec[..8].copy_from_slice(&time.seconds.to_le_bytes()); + timespec[8..].copy_from_slice(&i64::from(time.nanoseconds).to_le_bytes()); + if self + .memory + .write(GuestAddress::new(self.register(1)), ×pec) + .is_err() + { + self.set_return(Errno::Fault.return_value()); + return; + } + self.set_return(0); + } + + fn dispatch_getrandom(&mut self, system: &mut S) { + let count = self.register(1); + if self.register(2) & !GETRANDOM_ALLOWED_FLAGS != 0 || count > self.limits.max_memory_bytes + { + self.set_return(Errno::InvalidArgument.return_value()); + return; + } + let Ok(host_count) = usize::try_from(count) else { + self.set_return(Errno::InvalidArgument.return_value()); + return; + }; + let mut bytes = vec![0; host_count]; + system.fill_random(&mut bytes); + if self + .memory + .write(GuestAddress::new(self.register(0)), &bytes) + .is_err() + { + self.set_return(Errno::Fault.return_value()); + return; + } + self.set_return(count); + } + fn dispatch_rt_sigprocmask(&mut self) { let how = self.register(0); let set_address = GuestAddress::new(self.register(1)); diff --git a/crates/memory-fs/src/lib.rs b/crates/memory-fs/src/lib.rs index 1d77108..20b12fc 100644 --- a/crates/memory-fs/src/lib.rs +++ b/crates/memory-fs/src/lib.rs @@ -11,6 +11,8 @@ use binarrow_host_api::{ const SNAPSHOT_MAGIC: &[u8; 8] = b"BNFS\x02\0\0\0"; const SNAPSHOT_DIRECTORY: u8 = 1; const SNAPSHOT_REGULAR_FILE: u8 = 2; +const PROC_CPUINFO: &[u8] = b"processor\t: 0\nmodel name\t: Binarrow virtual AArch64\n"; +const PROC_SELF_STATUS: &[u8] = b"Name:\tbinarrow-guest\nState:\tR (running)\nThreads:\t1\n"; /// Rejection reason for an imported deterministic filesystem snapshot. #[derive(Clone, Debug, Eq, PartialEq)] @@ -71,7 +73,14 @@ impl MemoryFileSystem { #[must_use] pub fn new(byte_limit: u64) -> Self { Self { - directories: BTreeSet::from([b"/".to_vec(), b"/project".to_vec(), b"/tmp".to_vec()]), + directories: BTreeSet::from([ + b"/".to_vec(), + b"/dev".to_vec(), + b"/proc".to_vec(), + b"/proc/self".to_vec(), + b"/project".to_vec(), + b"/tmp".to_vec(), + ]), files: BTreeMap::new(), open_handles: BTreeMap::new(), next_handle: 1, @@ -233,12 +242,28 @@ impl MemoryFileSystem { return Ok(handle); } if options.flags.contains(FileOpenFlags::DIRECTORY) { - return if self.files.contains_key(&path) { + return if self.files.contains_key(&path) || is_synthetic_file(&path) { Err(FileSystemError::NotDirectory) } else { Err(FileSystemError::NotFound) }; } + if is_synthetic_file(&path) { + if path.starts_with(b"/proc/") && options.access.can_write() { + return Err(FileSystemError::PermissionDenied); + } + let handle = self.allocate_handle()?; + self.open_handles.insert( + handle, + OpenHandle::File { + path, + position: 0, + access: options.access, + append: false, + }, + ); + return Ok(handle); + } let exists = self.files.contains_key(&path); if exists && options.flags.contains(FileOpenFlags::CREATE) @@ -254,6 +279,9 @@ impl MemoryFileSystem { if !self.directories.contains(parent) { return Err(FileSystemError::NotDirectory); } + if !is_mutable_path(&path) { + return Err(FileSystemError::PermissionDenied); + } self.files.insert(path.clone(), Vec::new()); } if options.flags.contains(FileOpenFlags::TRUNCATE) { @@ -309,7 +337,19 @@ impl MemoryFileSystem { if !access.can_read() { return Err(FileSystemError::PermissionDenied); } - let file = self.files.get(path).ok_or(FileSystemError::NotFound)?; + if path == b"/dev/null" { + return Ok(0); + } + if path == b"/dev/zero" { + destination.fill(0); + *position = position.saturating_add(destination.len() as u64); + return Ok(destination.len()); + } + let file = if let Some(contents) = synthetic_file_contents(path) { + contents + } else { + self.files.get(path).ok_or(FileSystemError::NotFound)? + }; let host_position = usize::try_from(*position).map_err(|_| FileSystemError::InvalidInput)?; if host_position >= file.len() { @@ -338,6 +378,10 @@ impl MemoryFileSystem { if !access.can_write() { return Err(FileSystemError::PermissionDenied); } + if matches!(path.as_slice(), b"/dev/null" | b"/dev/zero") { + *position = position.saturating_add(source.len() as u64); + return Ok(source.len()); + } let file = self.files.get_mut(path).ok_or(FileSystemError::NotFound)?; let host_position = if *append { file.len() @@ -389,7 +433,13 @@ impl MemoryFileSystem { let OpenHandle::File { path, position, .. } = open_handle else { unreachable!("all open-handle variants were considered") }; - let file_length = self.files.get(path).ok_or(FileSystemError::NotFound)?.len() as u64; + let file_length = if matches!(path.as_slice(), b"/dev/null" | b"/dev/zero") { + 0 + } else if let Some(contents) = synthetic_file_contents(path) { + contents.len() as u64 + } else { + self.files.get(path).ok_or(FileSystemError::NotFound)?.len() as u64 + }; let base = match from { FileSeekFrom::Start => 0, FileSeekFrom::Current => *position, @@ -448,6 +498,9 @@ impl HostFileSystem for MemoryFileSystem { fn create_directory(&mut self, path: &[u8]) -> Result<(), FileSystemError> { let path = normalize_path(path)?; + if !is_mutable_path(&path) { + return Err(FileSystemError::PermissionDenied); + } if self.directories.contains(&path) || self.files.contains_key(&path) { return Err(FileSystemError::AlreadyExists); } @@ -499,6 +552,7 @@ impl HostFileSystem for MemoryFileSystem { file_type: FileType::Regular, }), ); + entries.extend(synthetic_directory_entries(&path)); entries[2..].sort_by(|left, right| left.name.cmp(&right.name)); let end = position.saturating_add(max_entries).min(entries.len()); let selected = entries.get(position..end).unwrap_or_default().to_vec(); @@ -514,7 +568,11 @@ impl HostFileSystem for MemoryFileSystem { if old_path == new_path { return Ok(()); } - if matches!(old_path.as_slice(), b"/" | b"/project" | b"/tmp") { + if !is_mutable_path(&old_path) + || !is_mutable_path(&new_path) + || is_reserved_path(&old_path) + || is_synthetic_file(&old_path) + { return Err(FileSystemError::PermissionDenied); } let new_parent = parent_path(&new_path).ok_or(FileSystemError::InvalidInput)?; @@ -565,8 +623,11 @@ impl HostFileSystem for MemoryFileSystem { fn remove(&mut self, path: &[u8], directory: bool) -> Result<(), FileSystemError> { let path = normalize_path(path)?; + if !is_mutable_path(&path) { + return Err(FileSystemError::PermissionDenied); + } if directory { - if matches!(path.as_slice(), b"/" | b"/project" | b"/tmp") { + if is_reserved_path(&path) { return Err(FileSystemError::PermissionDenied); } if !self.directories.contains(&path) { @@ -593,6 +654,9 @@ impl HostFileSystem for MemoryFileSystem { if self.directories.contains(&path) { return Err(FileSystemError::IsDirectory); } + if is_synthetic_file(&path) { + return Err(FileSystemError::PermissionDenied); + } let bytes = self.files.remove(&path).ok_or(FileSystemError::NotFound)?; self.stored_bytes -= bytes.len() as u64; Ok(()) @@ -634,6 +698,48 @@ fn parent_path(path: &[u8]) -> Option<&[u8]> { }) } +fn is_mutable_path(path: &[u8]) -> bool { + path.starts_with(b"/project/") || path.starts_with(b"/tmp/") +} + +fn is_reserved_path(path: &[u8]) -> bool { + matches!( + path, + b"/" | b"/dev" | b"/proc" | b"/proc/self" | b"/project" | b"/tmp" + ) +} + +fn is_synthetic_file(path: &[u8]) -> bool { + matches!( + path, + b"/dev/null" | b"/dev/zero" | b"/proc/cpuinfo" | b"/proc/self/status" + ) +} + +fn synthetic_file_contents(path: &[u8]) -> Option<&'static [u8]> { + match path { + b"/proc/cpuinfo" => Some(PROC_CPUINFO), + b"/proc/self/status" => Some(PROC_SELF_STATUS), + _ => None, + } +} + +fn synthetic_directory_entries(path: &[u8]) -> Vec { + let names: &[&[u8]] = match path { + b"/dev" => &[b"null", b"zero"], + b"/proc" => &[b"cpuinfo"], + b"/proc/self" => &[b"status"], + _ => &[], + }; + names + .iter() + .map(|name| DirectoryEntry { + name: name.to_vec(), + file_type: FileType::Regular, + }) + .collect() +} + fn file_name(path: &[u8]) -> &[u8] { parent_path(path) .and_then(|parent| path.get(parent.len() + usize::from(parent != b"/")..)) @@ -858,4 +964,38 @@ mod tests { .unwrap(); assert_eq!(restored.read_directory(handle, 8).unwrap().len(), 2); } + + #[test] + fn exposes_minimal_dev_and_proc_mounts() { + let mut filesystem = MemoryFileSystem::new(64); + let read_only = FileOpenOptions { + access: FileAccess::ReadOnly, + flags: FileOpenFlags::NONE, + }; + let zero = filesystem.open(b"/dev/zero", read_only).unwrap(); + let mut bytes = [0xff; 8]; + assert_eq!(filesystem.read(zero, &mut bytes).unwrap(), bytes.len()); + assert_eq!(bytes, [0; 8]); + + let status = filesystem.open(b"/proc/self/status", read_only).unwrap(); + let mut status_bytes = [0; 128]; + let count = filesystem.read(status, &mut status_bytes).unwrap(); + assert!(status_bytes[..count].starts_with(b"Name:\tbinarrow-guest\n")); + + let null = filesystem + .open( + b"/dev/null", + FileOpenOptions { + access: FileAccess::WriteOnly, + flags: FileOpenFlags::NONE, + }, + ) + .unwrap(); + assert_eq!(filesystem.write(null, b"discarded").unwrap(), 9); + assert_eq!(filesystem.stored_bytes(), 0); + assert_eq!( + filesystem.create_directory(b"/dev/forbidden"), + Err(FileSystemError::PermissionDenied) + ); + } } diff --git a/guest-tests/system-services/README.md b/guest-tests/system-services/README.md new file mode 100644 index 0000000..ce3c607 --- /dev/null +++ b/guest-tests/system-services/README.md @@ -0,0 +1,11 @@ +# System services AArch64 fixture + +This freestanding static AArch64 Linux program validates realtime and +monotonic clocks, successive random byte reads, `/dev/zero`, and the minimal +`/proc/self/status` view. + +Build the deterministic fixture with Zig 0.16.0: + +```sh +guest-tests/system-services/build.sh +``` diff --git a/guest-tests/system-services/build.sh b/guest-tests/system-services/build.sh new file mode 100755 index 0000000..3bee751 --- /dev/null +++ b/guest-tests/system-services/build.sh @@ -0,0 +1,32 @@ +#!/bin/sh +set -eu + +fixture_directory=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +cache_directory=${TMPDIR:-/tmp}/binarrow-zig-cache +output=$fixture_directory/system-services.aarch64.elf +zig_version=$(zig version) + +if [ "$zig_version" != "0.16.0" ]; then + echo "system-services requires Zig 0.16.0; found $zig_version" >&2 + exit 1 +fi + +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 \ + -fno-vectorize \ + -fno-slp-vectorize \ + -O1 \ + -g0 \ + -s \ + -Wl,--build-id=none \ + -Wl,-e,_start \ + "$fixture_directory/main.c" \ + -o "$output" + +chmod 0644 "$output" diff --git a/guest-tests/system-services/main.c b/guest-tests/system-services/main.c new file mode 100644 index 0000000..02ec72d --- /dev/null +++ b/guest-tests/system-services/main.c @@ -0,0 +1,91 @@ +enum { + SYS_OPENAT = 56, + SYS_CLOSE = 57, + SYS_READ = 63, + SYS_WRITE = 64, + SYS_EXIT = 93, + SYS_CLOCK_GETTIME = 113, + SYS_GETRANDOM = 278, + AT_FDCWD = -100, + O_RDONLY = 0, + CLOCK_REALTIME = 0, + CLOCK_MONOTONIC = 1, +}; + +struct timespec { + long seconds; + long nanoseconds; +}; + +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; +} + +__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 zero_path[] = "/dev/zero"; + static const char status_path[] = "/proc/self/status"; + static const char message[] = "system services ok\n"; + struct timespec realtime; + struct timespec monotonic; + unsigned char first[16]; + unsigned char second[16]; + unsigned char zeros[16]; + char status[32]; + + if (syscall3(SYS_CLOCK_GETTIME, CLOCK_REALTIME, (long)&realtime, 0) != 0 || + syscall3(SYS_CLOCK_GETTIME, CLOCK_MONOTONIC, (long)&monotonic, 0) != 0 || + realtime.seconds <= 0 || monotonic.seconds < 0 || + realtime.nanoseconds < 0 || realtime.nanoseconds >= 1000000000 || + monotonic.nanoseconds < 0 || monotonic.nanoseconds >= 1000000000) { + exit_guest(1); + } + if (syscall3(SYS_GETRANDOM, (long)first, sizeof(first), 0) != sizeof(first) || + syscall3(SYS_GETRANDOM, (long)second, sizeof(second), 0) != sizeof(second)) { + exit_guest(2); + } + int different = 0; + for (unsigned long index = 0; index < sizeof(first); ++index) { + different |= first[index] != second[index]; + } + if (!different) { + exit_guest(3); + } + + long descriptor = syscall3(SYS_OPENAT, AT_FDCWD, (long)zero_path, O_RDONLY); + if (descriptor < 0 || + syscall3(SYS_READ, descriptor, (long)zeros, sizeof(zeros)) != sizeof(zeros)) { + exit_guest(4); + } + for (unsigned long index = 0; index < sizeof(zeros); ++index) { + if (zeros[index] != 0) { + exit_guest(5); + } + } + if (syscall3(SYS_CLOSE, descriptor, 0, 0) != 0) { + exit_guest(6); + } + + descriptor = syscall3(SYS_OPENAT, AT_FDCWD, (long)status_path, O_RDONLY); + if (descriptor < 0 || syscall3(SYS_READ, descriptor, (long)status, sizeof(status)) <= 0 || + status[0] != 'N' || status[1] != 'a' || status[2] != 'm' || status[3] != 'e' || + syscall3(SYS_CLOSE, descriptor, 0, 0) != 0) { + exit_guest(7); + } + if (syscall3(SYS_WRITE, 1, (long)message, sizeof(message) - 1) != sizeof(message) - 1) { + exit_guest(8); + } + exit_guest(0); +} diff --git a/guest-tests/system-services/system-services.aarch64.elf b/guest-tests/system-services/system-services.aarch64.elf new file mode 100644 index 0000000000000000000000000000000000000000..da0778a6a15b2d64ea65edf206b58fbc7c03b104 GIT binary patch literal 1992 zcmb<-^>JfjWMqH=CWh?{Al?~9h@b;hf`bjpU|?WyV6b3dWpH3%XJBIh3A2F3Ao30j zVC@VrS^{Jk0|Nt$=71;zD+1|*azPYSC0L5#43r0>p~iwGSs56Rd z8T1Q^@{{$8Q*+Yvi%SwqN{bnaD~n4~a}|nHi^?*SQ;QYyv$+@;rJ1ckc7qIYVPIs? zff&Oe0HqnLf*2TUI-|iO+$eH~R#AkQ3e0dzgPXzI~9W7s;hVUPPxq@r4~NUtWgrAF?roybyP^e0d$hPXzI$9W7tphVUPPxq@r@lVUw(%0 zA2Kn7yfAmPeEA*1PXzI;9W7t}hVUOUGK9RaceH%@AHq*$WC%&X5-$!|#1eQKCdn%> zUv+%@T>uou9PE%VPGI}7{UtNQs)x-C6PcJAf?hzyL1Etr@?W!q3ZLA2Kk6U~@YtFFnBGCQx1iyXgY!#3ZJM zAPx>l{K)$-d^`v?OCBB$u>8lsh?yrn@^e#@ic-rJl5-OC(iO^5i;6Sz^AwB>^$hha z7<@AGvQvw!6yU0Sd|Y6140=WRDTyVC40@><@o7bgxvB9PDMbh#gI-B$MG1pma(-@Z zYF-J0UU5coNl{5+5~%2)7=y|!5N3j!6ag(?BS3--3=9TPaTo;^hH)7<86YJ!Oh2q# zhLzW#vJtMBfq?;5KEuSs(ey8X+P?s55v)9gsfX1=Fuo#GKdh{W)hDp}2T`9ubfDX> p4%LtD&k3N)ih+T_3Ze~8LY)PvgrMOAt-9c9K#Wra^n=uZFaRcSrgs1U literal 0 HcmV?d00001 diff --git a/web/index.html b/web/index.html index c158b21..efaab55 100644 --- a/web/index.html +++ b/web/index.html @@ -33,6 +33,7 @@ + diff --git a/web/src/probe.ts b/web/src/probe.ts index 5370750..24347ec 100644 --- a/web/src/probe.ts +++ b/web/src/probe.ts @@ -20,7 +20,8 @@ export type FixtureName = | "file-roundtrip" | "project-persistence-write" | "project-persistence-read" - | "vfs-lifecycle"; + | "vfs-lifecycle" + | "system-services"; export interface FeatureResult { name: FeatureName; diff --git a/web/tests/probe.spec.ts b/web/tests/probe.spec.ts index a6cd44c..89ac90d 100644 --- a/web/tests/probe.spec.ts +++ b/web/tests/probe.spec.ts @@ -130,6 +130,24 @@ test("creates, modifies, lists, renames, and deletes guest files", async ({ ); }); +test("provides clocks, randomness, and minimal dev and proc mounts", async ({ + page, +}) => { + await page.getByLabel("Fixture").selectOption("system-services"); + await page.getByRole("button", { name: "Start" }).click(); + + await expect(page.getByRole("status")).toHaveText("Guest exited"); + await expect(page.getByLabel("Guest terminal output")).toHaveText( + "system services ok", + ); + await expect(page.getByLabel("System call trace")).toContainText( + "clock_gettime(clock=0", + ); + await expect(page.getByLabel("System call trace")).toContainText( + "getrandom(buffer=", + ); +}); + test("terminates and restarts a Worker running an infinite guest", async ({ page }) => { await page.getByLabel("Fixture").selectOption("infinite-loop"); await page