diff --git a/crates/browser-runtime/src/lib.rs b/crates/browser-runtime/src/lib.rs index 79ea9f9..21ffebd 100644 --- a/crates/browser-runtime/src/lib.rs +++ b/crates/browser-runtime/src/lib.rs @@ -8,7 +8,9 @@ use binarrow_aarch64::{Aarch64State, BasicBlock, BlockExecutor, Interpreter}; use binarrow_host_api::{ DeterministicSystem, HostInput, HostTerminal, HostTime, TerminalInputRead, TerminalStream, }; -use binarrow_linux_runtime::{ExecutionError, ExecutionEvent, Process}; +use binarrow_linux_runtime::{ + ExecutionError, ExecutionEvent, Process, load_process_with_guest_filesystem, +}; use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; use binarrow_memory_fs::MemoryFileSystem; use binarrow_runtime_core::{MemoryAccess, ResourceLimit, Trap}; @@ -921,29 +923,36 @@ fn start_guest_session( } else { MemoryFileSystem::from_snapshot(max_filesystem_bytes, filesystem_snapshot) }; - let process = request - .and_then(|program| { - load_process( - program.elf, - &ProcessParameters { - argv: program.arguments, - envp: program.environment, - random_bytes: [0x42; 16], - credentials: Credentials::default(), - }, - config, - ) - .map_err(|error| BrowserExecution::diagnostic("loader.failed", error.to_string())) - }) - .and_then(|image| { - Process::new(image).map_err(|error| { - BrowserExecution::diagnostic("cpu.initialization_failed", error.to_string()) - }) - }); - let (process, filesystem, startup_failure) = match (process, filesystem) { - (Ok(process), Ok(filesystem)) => (Some(process), filesystem, None), - (Err(failure), Ok(filesystem)) => (None, filesystem, Some(failure)), - (_, Err(error)) => ( + let (process, filesystem, startup_failure) = match filesystem { + Ok(mut filesystem) => { + let process = request + .and_then(|program| { + load_process_with_guest_filesystem( + program.elf, + &ProcessParameters { + argv: program.arguments, + envp: program.environment, + random_bytes: [0x42; 16], + credentials: Credentials::default(), + }, + config, + &mut filesystem, + ) + .map_err(|error| { + BrowserExecution::diagnostic("loader.failed", error.to_string()) + }) + }) + .and_then(|image| { + Process::new(image).map_err(|error| { + BrowserExecution::diagnostic("cpu.initialization_failed", error.to_string()) + }) + }); + match process { + Ok(process) => (Some(process), filesystem, None), + Err(failure) => (None, filesystem, Some(failure)), + } + } + Err(error) => ( None, MemoryFileSystem::new(max_filesystem_bytes), Some(BrowserExecution::diagnostic( @@ -1074,7 +1083,7 @@ fn execute_fixture( } } }; - let image = match load_process( + let image = match load_process_with_guest_filesystem( elf, &ProcessParameters { argv: vec![argv0.to_vec()], @@ -1083,6 +1092,7 @@ fn execute_fixture( credentials: Credentials::default(), }, config, + &mut filesystem, ) { Ok(image) => image, Err(error) => { diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 53ef1cf..64418ee 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -11,8 +11,8 @@ use binarrow_host_api::{ FileAccess, FileOpenFlags, FileOpenOptions, FileSystemError, HostFileSystem, HostTerminal, TerminalStream, }; -use binarrow_linux_runtime::Process; -use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters, load_process}; +use binarrow_linux_runtime::{Process, load_process_with_guest_filesystem}; +use binarrow_loader::{Credentials, ProcessConfig, ProcessParameters}; use binarrow_memory_fs::MemoryFileSystem; const USAGE: &str = "usage: binarrow inspect \n binarrow run [run options] [guest arguments...]\n binarrow trace [run options] [guest arguments...]\n binarrow image pack [--guest-root ] \n\nrun options:\n --instruction-budget \n --filesystem-limit \n --filesystem-snapshot \n --filesystem-output \n --filesystem-install (repeatable)\n --random-seed \n --argv0 \n --env (repeatable)"; @@ -369,7 +369,7 @@ fn run_guest(options: RunOptions, print_trace: bool) -> Result { .install_snapshot(&snapshot) .map_err(|error| format!("{}: {error}", path.display()))?; } - let image = load_process( + let image = load_process_with_guest_filesystem( &bytes, &ProcessParameters { argv, @@ -378,6 +378,7 @@ fn run_guest(options: RunOptions, print_trace: bool) -> Result { credentials: Credentials::default(), }, config, + &mut filesystem, ) .map_err(|error| format!("{}: {error}", options.executable.display()))?; let mut process = Process::new(image).map_err(|error| error.to_string())?; diff --git a/crates/linux-runtime/src/lib.rs b/crates/linux-runtime/src/lib.rs index dabf1c4..81ee81c 100644 --- a/crates/linux-runtime/src/lib.rs +++ b/crates/linux-runtime/src/lib.rs @@ -14,7 +14,10 @@ use binarrow_host_api::{ HostTerminal, NullFileSystem, TerminalInputRead, TerminalStream, }; use binarrow_linux_abi::{Errno, Syscall}; -use binarrow_loader::{Credentials, ProcessConfig, ProcessImage, ProcessParameters, load_process}; +use binarrow_loader::{ + Credentials, LoaderError, ProcessConfig, ProcessImage, ProcessParameters, load_process, + load_process_with_interpreter, +}; use binarrow_runtime_core::{GuestAddress, ResourceLimit, ResourceLimits, Trap}; const STANDARD_INPUT: u64 = 0; @@ -105,6 +108,144 @@ const SIGCHLD: u64 = 17; const SUPPORTED_CLONE_FLAGS: u64 = CLONE_VM | CLONE_VFORK | SIGCHLD; const RUSAGE_SIZE: usize = 144; +/// Failure while resolving an executable's optional guest-side interpreter. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ProcessLoadError { + Loader(LoaderError), + InterpreterFile { + path: Vec, + error: FileSystemError, + }, + InterpreterNotRegular(Vec), + InterpreterTooLarge { + path: Vec, + size: u64, + limit: u64, + }, + InterpreterShortRead(Vec), +} + +impl fmt::Display for ProcessLoadError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Loader(error) => write!(formatter, "{error}"), + Self::InterpreterFile { path, error } => write!( + formatter, + "could not read guest interpreter {}: {error:?}", + String::from_utf8_lossy(path) + ), + Self::InterpreterNotRegular(path) => write!( + formatter, + "guest interpreter {} is not a regular file", + String::from_utf8_lossy(path) + ), + Self::InterpreterTooLarge { path, size, limit } => write!( + formatter, + "guest interpreter {} is {size} bytes, exceeding limit {limit}", + String::from_utf8_lossy(path) + ), + Self::InterpreterShortRead(path) => write!( + formatter, + "guest interpreter {} ended before its declared size", + String::from_utf8_lossy(path) + ), + } + } +} + +impl std::error::Error for ProcessLoadError {} + +impl From for ProcessLoadError { + fn from(error: LoaderError) -> Self { + Self::Loader(error) + } +} + +/// Load an executable, resolving `PT_INTERP` from the supplied guest filesystem. +/// +/// # Errors +/// +/// Returns [`ProcessLoadError`] for invalid ELF/process metadata or when the +/// requested guest interpreter cannot be read within the filesystem limit. +pub fn load_process_with_guest_filesystem( + elf_bytes: &[u8], + parameters: &ProcessParameters, + config: ProcessConfig, + filesystem: &mut F, +) -> Result { + match load_process(elf_bytes, parameters, config) { + Ok(image) => Ok(image), + Err(LoaderError::InterpreterRequired(path)) => { + let interpreter = + read_guest_interpreter(filesystem, &path, config.limits.max_filesystem_bytes)?; + load_process_with_interpreter(elf_bytes, &interpreter, parameters, config) + .map_err(ProcessLoadError::Loader) + } + Err(error) => Err(ProcessLoadError::Loader(error)), + } +} + +fn read_guest_interpreter( + filesystem: &mut F, + path: &[u8], + limit: u64, +) -> Result, ProcessLoadError> { + let metadata = + filesystem + .metadata(path) + .map_err(|error| ProcessLoadError::InterpreterFile { + path: path.to_vec(), + error, + })?; + if metadata.file_type != FileType::Regular { + return Err(ProcessLoadError::InterpreterNotRegular(path.to_vec())); + } + if metadata.size > limit { + return Err(ProcessLoadError::InterpreterTooLarge { + path: path.to_vec(), + size: metadata.size, + limit, + }); + } + let size = + usize::try_from(metadata.size).map_err(|_| ProcessLoadError::InterpreterTooLarge { + path: path.to_vec(), + size: metadata.size, + limit, + })?; + let handle = filesystem + .open( + path, + FileOpenOptions { + access: FileAccess::ReadOnly, + flags: FileOpenFlags::NONE, + }, + ) + .map_err(|error| ProcessLoadError::InterpreterFile { + path: path.to_vec(), + error, + })?; + let result = (|| { + let mut bytes = vec![0; size]; + let mut filled = 0; + while filled < bytes.len() { + let read = filesystem + .read(handle, &mut bytes[filled..]) + .map_err(|error| ProcessLoadError::InterpreterFile { + path: path.to_vec(), + error, + })?; + if read == 0 { + return Err(ProcessLoadError::InterpreterShortRead(path.to_vec())); + } + filled += read; + } + Ok(bytes) + })(); + let _ = filesystem.close(handle); + result +} + #[derive(Clone, Copy, Debug, Default)] struct SignalAction { bytes: [u8; KERNEL_SIGACTION_SIZE], @@ -1054,7 +1195,7 @@ impl Process { }; let mut random_bytes = [0; 16]; system.fill_random(&mut random_bytes); - let Ok(image) = load_process( + let image = match load_process_with_guest_filesystem( &executable, &ProcessParameters { argv, @@ -1063,9 +1204,13 @@ impl Process { credentials: self.credentials, }, self.config, - ) else { - self.set_return(Errno::ExecutableFormat.return_value()); - return; + filesystem, + ) { + Ok(image) => image, + Err(error) => { + self.set_return(process_load_errno(&error).return_value()); + return; + } }; let interpreter = Interpreter::new().expect("the AArch64 language was initialized for this process"); @@ -2728,6 +2873,17 @@ const fn filesystem_error_errno(error: FileSystemError) -> Errno { } } +const fn process_load_errno(error: &ProcessLoadError) -> Errno { + match error { + ProcessLoadError::Loader(_) | ProcessLoadError::InterpreterShortRead(_) => { + Errno::ExecutableFormat + } + ProcessLoadError::InterpreterFile { error, .. } => filesystem_error_errno(*error), + ProcessLoadError::InterpreterNotRegular(_) => Errno::PermissionDenied, + ProcessLoadError::InterpreterTooLarge { .. } => Errno::OutOfMemory, + } +} + #[cfg(test)] mod tests { use core::convert::Infallible; @@ -2748,7 +2904,7 @@ mod tests { OPEN_DIRECTORY, OPEN_NOCTTY, OPEN_NOFOLLOW, OPEN_NONBLOCK, OPEN_PATH, PIPE_CAPACITY_BYTES, PipeEnd, Process, STANDARD_OUTPUT, STAT_CHARACTER_MODE, STAT_FIFO_MODE, STAT_FILE_SIZE_OFFSET, STAT_MODE_OFFSET, STAT_REGULAR_MODE, STAT_SIZE, - SUPPORTED_CLONE_FLAGS, SyscallEvent, SyscallOutcome, + SUPPORTED_CLONE_FLAGS, SyscallEvent, SyscallOutcome, load_process_with_guest_filesystem, }; const MESSAGE: &[u8] = b"hello, world\n"; @@ -3990,6 +4146,45 @@ mod tests { })); } + #[test] + fn resolves_dynamic_interpreter_from_guest_filesystem() { + let main = dynamic_hello_elf(); + let mut interpreter = hello_elf(1, MESSAGE_ADDRESS); + write_u16(&mut interpreter, 16, 3); + let path = b"/lib/ld-musl-aarch64.so.1"; + let mut snapshot = b"BNFS\x02\0\0\0".to_vec(); + snapshot.extend_from_slice(&2_u32.to_le_bytes()); + snapshot.push(1); + snapshot.extend_from_slice(&4_u32.to_le_bytes()); + snapshot.extend_from_slice(&0_u64.to_le_bytes()); + snapshot.extend_from_slice(b"/lib"); + snapshot.push(2); + snapshot.extend_from_slice(&u32::try_from(path.len()).unwrap().to_le_bytes()); + snapshot.extend_from_slice(&u64::try_from(interpreter.len()).unwrap().to_le_bytes()); + snapshot.extend_from_slice(path); + snapshot.extend_from_slice(&interpreter); + let mut filesystem = MemoryFileSystem::from_snapshot(1024 * 1024, &snapshot).unwrap(); + + let image = load_process_with_guest_filesystem( + &main, + &ProcessParameters { + argv: vec![b"/hello".to_vec()], + envp: Vec::new(), + random_bytes: [0x42; 16], + credentials: Credentials::default(), + }, + ProcessConfig::default(), + &mut filesystem, + ) + .unwrap(); + + assert_eq!(image.initial_state.pc, GuestAddress::new(0x2000_1100)); + assert_eq!( + image.interpreter_path, + Some(b"/lib/ld-musl-aarch64.so.1".to_vec()) + ); + } + fn load_hello( config: ProcessConfig, file_descriptor: u16, @@ -4047,6 +4242,20 @@ mod tests { bytes } + fn dynamic_hello_elf() -> Vec { + let mut bytes = hello_elf(1, MESSAGE_ADDRESS); + bytes.resize(0x160, 0); + write_u16(&mut bytes, 56, 2); + write_u64(&mut bytes, 96, 0x160); + write_u64(&mut bytes, 104, 0x160); + let interpreter_header = 120; + write_u32(&mut bytes, interpreter_header, 3); + write_u64(&mut bytes, interpreter_header + 8, 0x140); + write_u64(&mut bytes, interpreter_header + 32, 26); + bytes[0x140..0x15a].copy_from_slice(b"/lib/ld-musl-aarch64.so.1\0"); + bytes + } + const fn move_wide(register: u8, immediate: u16) -> u32 { 0xd280_0000 | ((immediate as u32) << 5) | (register as u32) } diff --git a/crates/memory-fs/src/lib.rs b/crates/memory-fs/src/lib.rs index 17a279c..083e258 100644 --- a/crates/memory-fs/src/lib.rs +++ b/crates/memory-fs/src/lib.rs @@ -1,4 +1,4 @@ -//! Bounded ephemeral regular-file storage for browser and deterministic hosts. +//! Bounded in-memory storage for persistent project and immutable system files. use std::collections::{BTreeMap, BTreeSet}; use std::fmt; @@ -58,7 +58,7 @@ enum OpenHandle { }, } -/// An ephemeral filesystem containing regular files and a small directory set. +/// An in-memory filesystem containing regular files and a small directory set. #[derive(Clone, Debug, Eq, PartialEq)] pub struct MemoryFileSystem { directories: BTreeSet>, @@ -165,8 +165,9 @@ impl MemoryFileSystem { Ok(()) } - /// Restore persistent regular files from a deterministic snapshot. - /// Ephemeral `/tmp` files are never accepted from snapshots. + /// Restore persistent project files and immutable system files from a + /// deterministic snapshot. Ephemeral `/tmp` and synthetic `/dev`/`/proc` + /// files are never accepted from snapshots. /// /// # Errors /// @@ -189,7 +190,7 @@ impl MemoryFileSystem { usize::try_from(file_length).map_err(|_| SnapshotError::HostSizeUnsupported)?; let path = reader.read(path_length)?.to_vec(); let normalized = normalize_path(&path).map_err(|_| SnapshotError::InvalidPath)?; - if normalized != path || !path.starts_with(b"/project/") { + if normalized != path || !is_snapshot_path(&path) { return Err(SnapshotError::InvalidPath); } let bytes = reader.read(file_length)?.to_vec(); @@ -231,7 +232,8 @@ impl MemoryFileSystem { Ok(filesystem) } - /// Export persistent `/project` regular files in a deterministic format. + /// Export persistent project files and immutable system files in a + /// deterministic format. /// /// # Errors /// @@ -241,12 +243,12 @@ impl MemoryFileSystem { let directories = self .directories .iter() - .filter(|path| path.starts_with(b"/project/")) + .filter(|path| is_snapshot_path(path)) .collect::>(); let files = self .files .iter() - .filter(|(path, _)| path.starts_with(b"/project/")) + .filter(|(path, _)| is_snapshot_path(path)) .collect::>(); let mut snapshot = Vec::new(); snapshot.extend_from_slice(SNAPSHOT_MAGIC); @@ -283,9 +285,8 @@ impl MemoryFileSystem { Ok(snapshot) } - /// Atomically merge a project image snapshot into this filesystem. - /// Existing files at the same paths are replaced and unrelated project - /// files remain present. + /// Atomically merge an image snapshot into this filesystem. Existing files + /// at the same paths are replaced and unrelated files remain present. /// /// # Errors /// @@ -297,7 +298,7 @@ impl MemoryFileSystem { for directory in image .directories .iter() - .filter(|path| path.starts_with(b"/project/")) + .filter(|path| is_snapshot_path(path)) { if merged.files.contains_key(directory) { return Err(SnapshotError::DuplicatePath); @@ -366,6 +367,9 @@ impl MemoryFileSystem { return Ok(handle); } let exists = self.files.contains_key(&path); + if exists && options.access.can_write() && !is_mutable_path(&path) { + return Err(FileSystemError::PermissionDenied); + } if exists && options.flags.contains(FileOpenFlags::CREATE) && options.flags.contains(FileOpenFlags::EXCLUSIVE) @@ -868,6 +872,17 @@ fn is_mutable_path(path: &[u8]) -> bool { path.starts_with(b"/project/") || path.starts_with(b"/tmp/") } +fn is_snapshot_path(path: &[u8]) -> bool { + path != b"/" + && !matches!( + path, + b"/dev" | b"/proc" | b"/proc/self" | b"/project" | b"/tmp" + ) + && !path.starts_with(b"/dev/") + && !path.starts_with(b"/proc/") + && !path.starts_with(b"/tmp/") +} + fn is_reserved_path(path: &[u8]) -> bool { matches!( path, @@ -1058,6 +1073,37 @@ mod tests { assert_eq!(snapshot, restored.export_snapshot().unwrap()); } + #[test] + fn snapshots_preserve_read_only_system_library_layouts() { + let path = b"/lib/ld-musl-aarch64.so.1"; + let contents = b"guest linker"; + let mut snapshot = b"BNFS\x02\0\0\0".to_vec(); + snapshot.extend_from_slice(&2_u32.to_le_bytes()); + snapshot.push(1); + snapshot.extend_from_slice(&4_u32.to_le_bytes()); + snapshot.extend_from_slice(&0_u64.to_le_bytes()); + snapshot.extend_from_slice(b"/lib"); + snapshot.push(2); + snapshot.extend_from_slice(&u32::try_from(path.len()).unwrap().to_le_bytes()); + snapshot.extend_from_slice(&u64::try_from(contents.len()).unwrap().to_le_bytes()); + snapshot.extend_from_slice(path); + snapshot.extend_from_slice(contents); + + let mut filesystem = MemoryFileSystem::from_snapshot(64, &snapshot).unwrap(); + assert_eq!(filesystem.read_file(path), Some(&contents[..])); + assert_eq!(filesystem.export_snapshot().unwrap(), snapshot); + assert_eq!( + filesystem.open( + path, + FileOpenOptions { + access: FileAccess::WriteOnly, + flags: FileOpenFlags::NONE, + }, + ), + Err(FileSystemError::PermissionDenied) + ); + } + #[test] fn atomically_replaces_file_contents() { let mut filesystem = MemoryFileSystem::new(12);