diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index d4550cb..d3d8c99 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -60,6 +60,7 @@ mod tests { entry_size: 56, entry_count: 1, }, + interpreter: None, load_segments: vec![LoadSegment { index: 0, file_offset: 0, diff --git a/crates/loader/src/lib.rs b/crates/loader/src/lib.rs index 97ff2aa..8634418 100644 --- a/crates/loader/src/lib.rs +++ b/crates/loader/src/lib.rs @@ -8,6 +8,7 @@ use binarrow_runtime_core::{GuestAddress, ResourceLimits}; const DEFAULT_PAGE_SIZE: u64 = 4096; const DEFAULT_PIE_BASE: u64 = 0x0001_0000; +const DEFAULT_INTERPRETER_BASE: u64 = 0x2000_0000; const DEFAULT_STACK_TOP: u64 = 0x7f00_0000; const DEFAULT_STACK_SIZE: u64 = 1024 * 1024; @@ -53,6 +54,7 @@ pub struct AuxEntry { pub struct ProcessConfig { pub page_size: u64, pub pie_base: GuestAddress, + pub interpreter_base: GuestAddress, pub stack_top: GuestAddress, pub stack_size: u64, pub limits: ResourceLimits, @@ -63,6 +65,7 @@ impl Default for ProcessConfig { Self { page_size: DEFAULT_PAGE_SIZE, pie_base: GuestAddress::new(DEFAULT_PIE_BASE), + interpreter_base: GuestAddress::new(DEFAULT_INTERPRETER_BASE), stack_top: GuestAddress::new(DEFAULT_STACK_TOP), stack_size: DEFAULT_STACK_SIZE, limits: ResourceLimits::default(), @@ -116,6 +119,7 @@ pub struct ProcessImage { pub initial_state: InitialState, pub stack: InitialStack, pub executable_path: Vec, + pub interpreter_path: Option>, pub load_bias: u64, pub limits: ResourceLimits, pub config: ProcessConfig, @@ -131,7 +135,12 @@ pub enum LoaderError { MisalignedStackTop(GuestAddress), MisalignedStackSize(u64), MisalignedPieBase(GuestAddress), + MisalignedInterpreterBase(GuestAddress), MisalignedLoadBias { segment: usize, alignment: u64 }, + InterpreterRequired(Vec), + InterpreterNotRequested, + ChainedInterpreter, + UnsupportedInterpreterKind(ImageKind), EmptyArgumentVector, InteriorNul { vector: &'static str, index: usize }, LoadAddressOverflow { segment: usize }, @@ -157,10 +166,27 @@ impl fmt::Display for LoaderError { Self::MisalignedPieBase(base) => { write!(formatter, "PIE base {base} is not page-aligned") } + Self::MisalignedInterpreterBase(base) => { + write!(formatter, "interpreter base {base} is not page-aligned") + } Self::MisalignedLoadBias { segment, alignment } => write!( formatter, - "PIE load bias does not satisfy PT_LOAD segment {segment} alignment {alignment:#x}", + "load bias does not satisfy PT_LOAD segment {segment} alignment {alignment:#x}", + ), + Self::InterpreterRequired(path) => write!( + formatter, + "ELF requires guest interpreter {}", + String::from_utf8_lossy(path) ), + Self::InterpreterNotRequested => { + formatter.write_str("ELF does not request a guest interpreter") + } + Self::ChainedInterpreter => { + formatter.write_str("guest interpreter must not request another interpreter") + } + Self::UnsupportedInterpreterKind(kind) => { + write!(formatter, "guest interpreter is {kind}; expected ET_DYN") + } Self::EmptyArgumentVector => { formatter.write_str("initial argument vector must contain argv[0]") } @@ -203,10 +229,58 @@ pub fn load_process( elf_bytes: &[u8], parameters: &ProcessParameters, config: ProcessConfig, +) -> Result { + let elf = binarrow_elf::inspect(elf_bytes)?; + if let Some(path) = elf.interpreter.clone() { + return Err(LoaderError::InterpreterRequired(path)); + } + load_process_images(elf_bytes, elf, None, parameters, config) +} + +/// Validate and map a dynamically linked executable and its guest interpreter. +/// +/// The interpreter bytes are mapped as guest code. Relocations and dependency +/// loading remain the responsibility of that guest interpreter. +/// +/// # Errors +/// +/// Returns [`LoaderError`] when the main image does not request an interpreter, +/// either ELF is invalid, or the two images cannot share a process address space. +pub fn load_process_with_interpreter( + elf_bytes: &[u8], + interpreter_bytes: &[u8], + parameters: &ProcessParameters, + config: ProcessConfig, +) -> Result { + let elf = binarrow_elf::inspect(elf_bytes)?; + if elf.interpreter.is_none() { + return Err(LoaderError::InterpreterNotRequested); + } + let interpreter = binarrow_elf::inspect(interpreter_bytes)?; + if interpreter.interpreter.is_some() { + return Err(LoaderError::ChainedInterpreter); + } + if interpreter.kind != ImageKind::PositionIndependent { + return Err(LoaderError::UnsupportedInterpreterKind(interpreter.kind)); + } + load_process_images( + elf_bytes, + elf, + Some((interpreter_bytes, interpreter)), + parameters, + config, + ) +} + +fn load_process_images( + elf_bytes: &[u8], + elf: ElfImage, + interpreter: Option<(&[u8], ElfImage)>, + parameters: &ProcessParameters, + config: ProcessConfig, ) -> Result { validate_config(config)?; validate_parameters(parameters)?; - let elf = binarrow_elf::inspect(elf_bytes)?; let load_bias = match elf.kind { ImageKind::Executable => 0, ImageKind::PositionIndependent => config.pie_base.get(), @@ -217,10 +291,25 @@ pub fn load_process( map_load_segment(&mut memory, elf_bytes, segment, load_bias)?; } - let pc = elf + let entry = elf .entry .checked_add(load_bias) .ok_or(LoaderError::EntryAddressOverflow)?; + let (pc, interpreter_base) = if let Some((bytes, image)) = interpreter.as_ref() { + let bias = config.interpreter_base.get(); + for segment in &image.load_segments { + map_load_segment(&mut memory, bytes, segment, bias)?; + } + ( + image + .entry + .checked_add(bias) + .ok_or(LoaderError::EntryAddressOverflow)?, + bias, + ) + } else { + (entry, 0) + }; let program_headers = program_header_address(&elf, load_bias)?; let stack = map_stack(&mut memory, config)?; let (sp, stack) = build_initial_stack( @@ -229,7 +318,8 @@ pub fn load_process( parameters, config, &elf, - pc, + entry, + interpreter_base, program_headers, )?; @@ -238,6 +328,7 @@ pub fn load_process( initial_state: InitialState { pc, sp }, stack, executable_path: parameters.argv[0].clone(), + interpreter_path: elf.interpreter, load_bias, limits: config.limits, config, @@ -258,6 +349,15 @@ fn validate_config(config: ProcessConfig) -> Result<(), LoaderError> { if !config.pie_base.get().is_multiple_of(config.page_size) { return Err(LoaderError::MisalignedPieBase(config.pie_base)); } + if !config + .interpreter_base + .get() + .is_multiple_of(config.page_size) + { + return Err(LoaderError::MisalignedInterpreterBase( + config.interpreter_base, + )); + } Ok(()) } @@ -383,6 +483,7 @@ fn build_initial_stack( config: ProcessConfig, elf: &ElfImage, entry: GuestAddress, + interpreter_base: u64, program_headers: GuestAddress, ) -> Result<(GuestAddress, InitialStack), LoaderError> { let mut cursor = mapping.stack_top; @@ -411,6 +512,7 @@ fn build_initial_stack( config, elf, entry, + interpreter_base, program_headers, random, executable_name, @@ -455,6 +557,7 @@ fn initial_auxv( config: ProcessConfig, elf: &ElfImage, entry: GuestAddress, + interpreter_base: u64, program_headers: GuestAddress, random: GuestAddress, executable_name: GuestAddress, @@ -478,7 +581,7 @@ fn initial_auxv( }, AuxEntry { kind: AuxType::InterpreterBase, - value: 0, + value: interpreter_base, }, AuxEntry { kind: AuxType::Flags, @@ -585,6 +688,7 @@ mod tests { use super::{ AuxType, Credentials, LoaderError, ProcessConfig, ProcessParameters, load_process, + load_process_with_interpreter, }; #[test] @@ -715,6 +819,87 @@ mod tests { assert_eq!(process.stack.auxv.last().unwrap().kind, AuxType::Null); } + #[test] + fn maps_guest_interpreter_and_constructs_dynamic_auxv() { + let main = dynamic_elf(); + let mut interpreter = valid_elf(); + write_u16(&mut interpreter, 16, 3); + let process = load_process_with_interpreter( + &main, + &interpreter, + ¶meters(), + ProcessConfig::default(), + ) + .unwrap(); + + assert_eq!( + process.interpreter_path, + Some(b"/lib/ld-musl-aarch64.so.1".to_vec()) + ); + assert_eq!(process.initial_state.pc, GuestAddress::new(0x2040_0100)); + assert_eq!(process.load_bias, 0); + assert_eq!( + process + .stack + .auxv + .iter() + .find(|entry| entry.kind == AuxType::InterpreterBase) + .unwrap() + .value, + 0x2000_0000 + ); + assert_eq!( + process + .stack + .auxv + .iter() + .find(|entry| entry.kind == AuxType::Entry) + .unwrap() + .value, + 0x0040_0100 + ); + assert_eq!( + process + .memory + .regions() + .iter() + .filter(|region| matches!(region.kind(), RegionKind::LoadSegment { .. })) + .count(), + 4 + ); + } + + #[test] + fn requires_exactly_one_position_independent_guest_interpreter() { + let main = dynamic_elf(); + assert_eq!( + load_process(&main, ¶meters(), ProcessConfig::default()), + Err(LoaderError::InterpreterRequired( + b"/lib/ld-musl-aarch64.so.1".to_vec() + )) + ); + assert_eq!( + load_process_with_interpreter( + &valid_elf(), + &valid_elf(), + ¶meters(), + ProcessConfig::default(), + ), + Err(LoaderError::InterpreterNotRequested) + ); + assert_eq!( + load_process_with_interpreter( + &main, + &valid_elf(), + ¶meters(), + ProcessConfig::default(), + ), + Err(LoaderError::UnsupportedInterpreterKind( + binarrow_elf::ImageKind::Executable + )) + ); + } + #[test] fn applies_deterministic_bias_to_position_independent_image() { let mut bytes = valid_elf(); @@ -811,6 +996,17 @@ mod tests { bytes } + fn dynamic_elf() -> Vec { + let mut bytes = valid_elf(); + write_u16(&mut bytes, 56, 3); + let interpreter_header = 176; + write_u32(&mut bytes, interpreter_header, 3); + write_u64(&mut bytes, interpreter_header + 8, 0x1e0); + write_u64(&mut bytes, interpreter_header + 32, 26); + bytes[0x1e0..0x1fa].copy_from_slice(b"/lib/ld-musl-aarch64.so.1\0"); + bytes + } + fn write_load_header( bytes: &mut [u8], offset: usize,