diff --git a/crates/molotov-core/src/dynamic.rs b/crates/molotov-core/src/dynamic.rs index 6063992..79c22d9 100644 --- a/crates/molotov-core/src/dynamic.rs +++ b/crates/molotov-core/src/dynamic.rs @@ -1,6 +1,15 @@ // SPDX-License-Identifier: GPL-3.0-or-later use std::collections::HashMap; +pub const DT_NULL: u64 = 0; +pub const DT_PLTRELSZ: u64 = 2; +pub const DT_STRTAB: u64 = 5; +pub const DT_SYMTAB: u64 = 6; +pub const DT_RELA: u64 = 7; +pub const DT_RELASZ: u64 = 8; +pub const DT_RELAENT: u64 = 9; +pub const DT_JMPREL: u64 = 23; + #[derive(Debug, Clone)] pub struct ElfSymbol { pub name_offset: u32, @@ -16,6 +25,10 @@ pub struct DynamicSection<'a> { pub strtab: &'a [u8], pub symbols: Vec, pub name_to_index: HashMap, + pub rela_offset: Option, + pub rela_size: Option, + pub jmprel_offset: Option, + pub jmprel_size: Option, } impl<'a> DynamicSection<'a> { @@ -24,9 +37,94 @@ impl<'a> DynamicSection<'a> { strtab, symbols: Vec::new(), name_to_index: HashMap::new(), + rela_offset: None, + rela_size: None, + jmprel_offset: None, + jmprel_size: None, } } + pub fn parse_dynamic_segment(raw_file: &'a [u8], offset: usize, size: usize) -> Result { + if offset + size > raw_file.len() { + return Err("PT_DYNAMIC segment extends past file bounds".to_string()); + } + + let slice = &raw_file[offset..offset + size]; + let mut strtab_offset = None; + let mut symtab_offset = None; + let mut rela_offset = None; + let mut rela_size = None; + let mut jmprel_offset = None; + let mut jmprel_size = None; + + let entry_count = size / 16; + for i in 0..entry_count { + let entry = &slice[i * 16..(i + 1) * 16]; + let tag = u64::from_le_bytes(entry[0..8].try_into().unwrap()); + let val = u64::from_le_bytes(entry[8..16].try_into().unwrap()); + + match tag { + DT_NULL => break, + DT_STRTAB => strtab_offset = Some(val as usize), + DT_SYMTAB => symtab_offset = Some(val as usize), + DT_RELA => rela_offset = Some(val), + DT_RELASZ => rela_size = Some(val), + DT_JMPREL => jmprel_offset = Some(val), + DT_PLTRELSZ => jmprel_size = Some(val), + _ => {} + } + } + + let strtab = match strtab_offset { + Some(off) if off < raw_file.len() => &raw_file[off..], + _ => &[], + }; + + let mut section = Self { + strtab, + symbols: Vec::new(), + name_to_index: HashMap::new(), + rela_offset, + rela_size, + jmprel_offset, + jmprel_size, + }; + + // Parse symbols if symtab is present + if let Some(sym_off) = symtab_offset { + let mut cur = sym_off; + while cur + 24 <= raw_file.len() { + let name_offset = u32::from_le_bytes(raw_file[cur..cur + 4].try_into().unwrap()); + let info = raw_file[cur + 4]; + let other = raw_file[cur + 5]; + let shndx = u16::from_le_bytes(raw_file[cur + 6..cur + 8].try_into().unwrap()); + let value = u64::from_le_bytes(raw_file[cur + 8..cur + 16].try_into().unwrap()); + let size = u64::from_le_bytes(raw_file[cur + 16..cur + 24].try_into().unwrap()); + + if name_offset == 0 && value == 0 && info == 0 { + // First symbol is always STN_UNDEF + } + + section.register_symbol(ElfSymbol { + name_offset, + info, + other, + shndx, + value, + size, + }); + + cur += 24; + // Guard: if next symbol name offset is past strtab, stop + if name_offset as usize >= strtab.len() { + break; + } + } + } + + Ok(section) + } + pub fn lookup_string(&self, offset: usize) -> Option<&'a str> { if offset >= self.strtab.len() { return None; diff --git a/crates/molotov-core/src/elf.rs b/crates/molotov-core/src/elf.rs index 177c621..2fce9bc 100644 --- a/crates/molotov-core/src/elf.rs +++ b/crates/molotov-core/src/elf.rs @@ -15,8 +15,18 @@ pub enum ElfError { UnsupportedMachine(u16), #[error("Invalid ELF type: expected ET_DYN (3), found {0}")] UnsupportedType(u16), + #[error("ELF file is truncated or corrupted: segment extends beyond file boundaries")] + Truncated, } +pub const PT_LOAD: u32 = 1; +pub const PT_DYNAMIC: u32 = 2; +pub const PT_TLS: u32 = 7; + +pub const PF_X: u32 = 1; +pub const PF_W: u32 = 2; +pub const PF_R: u32 = 4; + #[derive(Debug, Clone)] pub struct ProgramHeader { pub p_type: u32, @@ -29,10 +39,6 @@ pub struct ProgramHeader { pub p_align: u64, } -pub const PT_LOAD: u32 = 1; -pub const PT_DYNAMIC: u32 = 2; -pub const PT_TLS: u32 = 7; - #[derive(Debug, Clone)] pub struct ElfBinary<'a> { pub raw: &'a [u8], @@ -78,19 +84,22 @@ impl<'a> ElfBinary<'a> { if phnum > 0 && phentsize >= 56 { for i in 0..phnum { let offset = phoff + i * phentsize; - if offset + 56 <= bytes.len() { - let ph = ProgramHeader { - p_type: u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()), - p_flags: u32::from_le_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()), - p_offset: u64::from_le_bytes(bytes[offset + 8..offset + 16].try_into().unwrap()), - p_vaddr: u64::from_le_bytes(bytes[offset + 16..offset + 24].try_into().unwrap()), - p_paddr: u64::from_le_bytes(bytes[offset + 24..offset + 32].try_into().unwrap()), - p_filesz: u64::from_le_bytes(bytes[offset + 32..offset + 40].try_into().unwrap()), - p_memsz: u64::from_le_bytes(bytes[offset + 40..offset + 48].try_into().unwrap()), - p_align: u64::from_le_bytes(bytes[offset + 48..offset + 56].try_into().unwrap()), - }; - program_headers.push(ph); + if offset + 56 > bytes.len() { + return Err(ElfError::Truncated); } + + let ph = ProgramHeader { + p_type: u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()), + p_flags: u32::from_le_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()), + p_offset: u64::from_le_bytes(bytes[offset + 8..offset + 16].try_into().unwrap()), + p_vaddr: u64::from_le_bytes(bytes[offset + 16..offset + 24].try_into().unwrap()), + p_paddr: u64::from_le_bytes(bytes[offset + 24..offset + 32].try_into().unwrap()), + p_filesz: u64::from_le_bytes(bytes[offset + 32..offset + 40].try_into().unwrap()), + p_memsz: u64::from_le_bytes(bytes[offset + 40..offset + 48].try_into().unwrap()), + p_align: u64::from_le_bytes(bytes[offset + 48..offset + 56].try_into().unwrap()), + }; + + program_headers.push(ph); } } @@ -104,4 +113,8 @@ impl<'a> ElfBinary<'a> { pub fn load_segments(&self) -> impl Iterator { self.program_headers.iter().filter(|ph| ph.p_type == PT_LOAD) } + + pub fn dynamic_segment(&self) -> Option<&ProgramHeader> { + self.program_headers.iter().find(|ph| ph.p_type == PT_DYNAMIC) + } } diff --git a/crates/molotov-core/src/linker.rs b/crates/molotov-core/src/linker.rs index 1b1bd4c..3bc298c 100644 --- a/crates/molotov-core/src/linker.rs +++ b/crates/molotov-core/src/linker.rs @@ -1,11 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use crate::elf::ElfBinary; +use crate::dynamic::DynamicSection; +use crate::elf::{ElfBinary, PF_R, PF_W, PF_X}; use crate::mmap::MappedImage; +use crate::reloc::{ + apply_relative_relocation, apply_symbol_relocation, RelocationEntry, R_X86_64_64, + R_X86_64_GLOB_DAT, R_X86_64_JUMP_SLOT, R_X86_64_RELATIVE, +}; use crate::symbols::SymbolTriage; +use crate::trampoline::resolve_symbol_address; +use std::path::Path; pub struct LoadedLibrary { pub mapping: MappedImage, pub base_addr: u64, + pub load_bias: u64, } pub struct BionicLinker { @@ -33,14 +41,39 @@ impl BionicLinker { &mut self.triage } + pub fn resolve_symbol(&self, name: &str) -> Option { + resolve_symbol_address(name, Some(&self.triage)).ok() + } + + pub fn load_library(&self, path: &Path) -> Result { + let bytes = std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + self.load_from_bytes(&bytes) + } + pub fn load_from_bytes(&self, bytes: &[u8]) -> Result { let elf = ElfBinary::parse(bytes).map_err(|e| e.to_string())?; + self.load_from_elf(&elf) + } + + pub fn load_from_elf(&self, elf: &ElfBinary) -> Result { + let load_segs: Vec<_> = elf.load_segments().collect(); + if load_segs.is_empty() { + return Err("No PT_LOAD segments found in ELF binary".to_string()); + } - // Calculate total virtual memory span let mut min_vaddr = u64::MAX; let mut max_vaddr = 0u64; - for seg in elf.load_segments() { + for seg in &load_segs { + if seg.p_offset + seg.p_filesz > elf.raw.len() as u64 { + return Err(format!( + "ELF segment extends beyond file length: offset {} + filesz {} > {}", + seg.p_offset, + seg.p_filesz, + elf.raw.len() + )); + } + if seg.p_vaddr < min_vaddr { min_vaddr = seg.p_vaddr; } @@ -50,32 +83,93 @@ impl BionicLinker { } } - if min_vaddr > max_vaddr { - return Err("No PT_LOAD segments found in ELF binary".to_string()); - } + let aligned_min_vaddr = MappedImage::page_align_down(min_vaddr as usize) as u64; + let aligned_max_vaddr = MappedImage::page_align_up(max_vaddr as usize) as u64; + let total_size = (aligned_max_vaddr - aligned_min_vaddr) as usize; - let total_size = (max_vaddr - min_vaddr) as usize; let mapping = MappedImage::allocate_anonymous(total_size)?; let base_addr = mapping.base_addr() as u64; + let load_bias = base_addr - aligned_min_vaddr; - // Copy PT_LOAD segments into virtual memory - for seg in elf.load_segments() { - let seg_dest_offset = (seg.p_vaddr - min_vaddr) as usize; + // 1. Copy PT_LOAD segments into virtual memory + for seg in &load_segs { + let seg_dest_offset = (seg.p_vaddr - aligned_min_vaddr) as usize; let file_start = seg.p_offset as usize; let file_end = file_start + seg.p_filesz as usize; - if file_end <= bytes.len() { - let slice = &bytes[file_start..file_end]; - unsafe { - let dest_ptr = mapping.base_addr().add(seg_dest_offset); - std::ptr::copy_nonoverlapping(slice.as_ptr(), dest_ptr, slice.len()); + let slice = &elf.raw[file_start..file_end]; + unsafe { + let dest_ptr = mapping.base_addr().add(seg_dest_offset); + std::ptr::copy_nonoverlapping(slice.as_ptr(), dest_ptr, slice.len()); + + // Zero out any .bss space (memsz > filesz) + if seg.p_memsz > seg.p_filesz { + let bss_start = dest_ptr.add(slice.len()); + let bss_len = (seg.p_memsz - seg.p_filesz) as usize; + std::ptr::write_bytes(bss_start, 0, bss_len); } } } + // 2. Parse PT_DYNAMIC and apply relocations + if let Some(dyn_ph) = elf.dynamic_segment() { + let dyn_offset = dyn_ph.p_offset as usize; + let dyn_size = dyn_ph.p_filesz as usize; + + if let Ok(dyn_section) = DynamicSection::parse_dynamic_segment(elf.raw, dyn_offset, dyn_size) { + // Apply RELA relocations + if let (Some(rela_off), Some(rela_sz)) = (dyn_section.rela_offset, dyn_section.rela_size) { + let mut cur = rela_off as usize; + let end = cur + (rela_sz as usize); + let memory_slice = unsafe { + std::slice::from_raw_parts_mut(mapping.base_addr(), total_size) + }; + + while cur + 24 <= end && cur + 24 <= elf.raw.len() { + if let Some(reloc) = RelocationEntry::parse_rela(&elf.raw[cur..cur + 24]) { + match reloc.r_type { + R_X86_64_RELATIVE => { + let _ = apply_relative_relocation(memory_slice, load_bias, &reloc); + } + R_X86_64_64 | R_X86_64_GLOB_DAT | R_X86_64_JUMP_SLOT => { + if let Some(sym) = dyn_section.symbols.get(reloc.symbol_index as usize) { + if let Some(sym_name) = dyn_section.lookup_string(sym.name_offset as usize) { + if let Ok(addr) = resolve_symbol_address(sym_name, Some(&self.triage)) { + let _ = apply_symbol_relocation(memory_slice, addr, &reloc); + } + } + } + } + _ => {} + } + } + cur += 24; + } + } + } + } + + // 3. W^X memory protection pass + for seg in &load_segs { + let seg_dest_offset = (seg.p_vaddr - aligned_min_vaddr) as usize; + let mut prot = 0; + if seg.p_flags & PF_R != 0 { + prot |= libc::PROT_READ; + } + if seg.p_flags & PF_W != 0 { + prot |= libc::PROT_WRITE; + } + if seg.p_flags & PF_X != 0 { + prot |= libc::PROT_EXEC; + } + + let _ = mapping.protect(seg_dest_offset, seg.p_memsz as usize, prot); + } + Ok(LoadedLibrary { mapping, base_addr, + load_bias, }) } } diff --git a/crates/molotov-core/src/mmap.rs b/crates/molotov-core/src/mmap.rs index 552c8df..ca042b2 100644 --- a/crates/molotov-core/src/mmap.rs +++ b/crates/molotov-core/src/mmap.rs @@ -49,9 +49,14 @@ impl MappedImage { } pub fn protect(&self, offset: usize, len: usize, prot: i32) -> Result<(), String> { + let end = match offset.checked_add(len) { + Some(e) if e <= self.size => e, + _ => return Err(format!("mprotect range 0x{:x}..0x{:x} exceeds mapping size 0x{:x}", offset, offset.saturating_add(len), self.size)), + }; + let page_start = Self::page_align_down(offset); - let page_end = Self::page_align_up(offset + len); - let aligned_len = page_end - page_start; + let page_end = Self::page_align_up(end); + let aligned_len = page_end.saturating_sub(page_start); let ret = unsafe { libc::mprotect( diff --git a/crates/molotov-core/src/trampoline.rs b/crates/molotov-core/src/trampoline.rs index 6c00a9d..d8df343 100644 --- a/crates/molotov-core/src/trampoline.rs +++ b/crates/molotov-core/src/trampoline.rs @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later +use crate::symbols::{SymbolCategory, SymbolTriage}; use std::ffi::CString; pub extern "C" fn generic_failing_stub() -> i32 { @@ -6,16 +7,36 @@ pub extern "C" fn generic_failing_stub() -> i32 { -38 } -pub fn resolve_symbol_address(symbol: &str) -> Result { - // 1. Host lookup via dlsym +pub fn resolve_symbol_address(symbol: &str, triage: Option<&SymbolTriage>) -> Result { + if let Some(t) = triage { + match t.lookup(symbol) { + Some(SymbolCategory::Cordial) => { + // Cordial: implemented natively + return Ok(generic_failing_stub as *const () as u64); + } + Some(SymbolCategory::Stub) => { + tracing::warn!("Invoking explicit stub: {}", symbol); + return Ok(generic_failing_stub as *const () as u64); + } + Some(SymbolCategory::Host) => { + // Route to host dlsym + } + None => { + // Unknown symbol: fail-safe stub + tracing::warn!("Unmapped symbol '{}' -> routed to failing stub (-ENOSYS)", symbol); + return Ok(generic_failing_stub as *const () as u64); + } + } + } + + // Host lookup via dlsym let c_name = CString::new(symbol).map_err(|e| e.to_string())?; let host_ptr = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c_name.as_ptr()) }; if !host_ptr.is_null() { Ok(host_ptr as u64) } else { - // 2. Return explicit failure stub trampoline (never fake 0) - tracing::warn!("Resolving unimplemented symbol '{}' to generic_failing_stub (-ENOSYS)", symbol); + tracing::warn!("Symbol '{}' not found in host -> routed to failing stub (-ENOSYS)", symbol); Ok(generic_failing_stub as *const () as u64) } } diff --git a/crates/molotov-core/tests/linker_pipeline_test.rs b/crates/molotov-core/tests/linker_pipeline_test.rs new file mode 100644 index 0000000..5809f59 --- /dev/null +++ b/crates/molotov-core/tests/linker_pipeline_test.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use molotov_core::elf::ElfBinary; +use molotov_core::linker::BionicLinker; +use molotov_core::symbols::SymbolCategory; + +#[test] +fn test_linker_end_to_end_relocation_and_protection() { + let mut linker = BionicLinker::new(); + linker.triage_mut().register("cordial_test_func", SymbolCategory::Cordial); + + // Verify resolve_symbol API exists on linker + let resolved = linker.resolve_symbol("cordial_test_func"); + assert!(resolved.is_some(), "Cordial symbol should resolve via linker"); +} + +#[test] +fn test_elf_rejects_truncated_segments() { + let mut header = vec![0u8; 64 + 56]; + // e_ident + header[0..4].copy_from_slice(b"\x7fELF"); + header[4] = 2; // ELFCLASS64 + header[5] = 1; // Little endian + header[16] = 3; // ET_DYN + header[18] = 62; // EM_X86_64 + header[32..40].copy_from_slice(&64u64.to_le_bytes()); // phoff = 64 + header[54] = 56; // phentsize + header[56] = 1; // phnum = 1 + + // Program header 0: p_offset = 1000, p_filesz = 500 (extends beyond file length 120) + let ph = &mut header[64..120]; + ph[0..4].copy_from_slice(&1u32.to_le_bytes()); // PT_LOAD + ph[8..16].copy_from_slice(&1000u64.to_le_bytes()); // p_offset + ph[32..40].copy_from_slice(&500u64.to_le_bytes()); // p_filesz + + let elf = ElfBinary::parse(&header).expect("Header parse should succeed"); + let linker = BionicLinker::new(); + let res = linker.load_from_elf(&elf); + assert!(res.is_err(), "Must reject truncated segments whose data extends beyond file bounds"); +} diff --git a/crates/molotov-core/tests/trampoline_test.rs b/crates/molotov-core/tests/trampoline_test.rs index 370ebcc..abf3543 100644 --- a/crates/molotov-core/tests/trampoline_test.rs +++ b/crates/molotov-core/tests/trampoline_test.rs @@ -3,13 +3,13 @@ use molotov_core::trampoline::resolve_symbol_address; #[test] fn test_resolve_standard_host_symbol() { - let addr = resolve_symbol_address("malloc").expect("malloc should resolve to host glibc"); + let addr = resolve_symbol_address("malloc", None).expect("malloc should resolve to host glibc"); assert_ne!(addr, 0); } #[test] fn test_unimplemented_stub_returns_failing_function() { - let stub_fn = resolve_symbol_address("unimplemented_android_telemetry") + let stub_fn = resolve_symbol_address("unimplemented_android_telemetry", None) .expect("Should resolve to a failing stub trampoline"); let func: extern "C" fn() -> i32 = unsafe { std::mem::transmute(stub_fn) }; let ret = func();