docs: add Phase 2A Bionic Linker implementation plan
This commit is contained in:
parent
36835ac5a0
commit
58618eb3c0
1 changed files with 899 additions and 0 deletions
|
|
@ -0,0 +1,899 @@
|
|||
# Molotov Phase 2A: Bionic ELF Linker & Relocation Engine Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the custom AOSP Bionic ELF64 linker in Rust (`molotov-core`), enabling unmodified Android x86_64 `libroblox.so` to be memory-mapped, relocated, and resolved against native/host symbols with zero Wine and strict failure stubs.
|
||||
|
||||
**Architecture:** A native ELF64 loader that parses program headers, reserves contiguous virtual address space via `mmap`, processes dynamic relocations (`DT_RELA` / `DT_JMPREL`), routes imported symbols through the strict `SymbolTriage` table (`cordial`, `host`, `stub`), configures Bionic TLS slots, and resolves `JNI_OnLoad`.
|
||||
|
||||
**Tech Stack:** Rust 1.75+, `libc`, `nix::sys::mman`, `tracing`, `thiserror`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-29-molotov-architecture-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Language & Edition**: Rust 1.75+ (2021 edition), strictly zero Wine or Proton.
|
||||
- **License**: GNU General Public License v3.0 or later (GPL-3.0-or-later) with headers in all source files.
|
||||
- **No Faking Success**: Every stub returns explicit failure (`-ENOSYS`, `-1`, or `NULL`) with a diagnostic log. Never return fake 0 or success.
|
||||
- **Security & Safety**: W^X memory safety (write segments with `PROT_READ | PROT_WRITE`, apply relocations, then finalize with `PROT_READ | PROT_EXEC`).
|
||||
- **Target ABI**: Linux host x86_64 loading Android Bionic x86_64 shared libraries.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Page Alignment Violations**: Virtual address and file offset discrepancies in `PT_LOAD` segments must be aligned to the 4096-byte host page boundary.
|
||||
2. **Missing/Unresolved Symbols in Stubs**: Any symbol not found in cordial or host tables must resolve to an explicit failing stub returning `-ENOSYS`, never null pointer dereferences or unhandled panics.
|
||||
3. **Out-of-Bounds Relocation Writes**: Relocation offsets pointing outside the allocated ELF virtual memory space must be caught and rejected with an explicit error before memory mutation.
|
||||
4. **W^X Memory Protection Transitions**: Executable pages must not remain writable after relocation fixups are complete.
|
||||
5. **ASLR Relocation Slide Arithmetic**: `R_X86_64_RELATIVE` relocations must correctly add the base memory slide offset to the addend without 32-bit truncation.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: ELF64 Binary Parser & Program Headers
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/elf.rs`
|
||||
- Test: `crates/molotov-core/tests/elf_parser_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: None
|
||||
- Produces:
|
||||
- `molotov_core::elf::ElfHeader`
|
||||
- `molotov_core::elf::ProgramHeader`
|
||||
- `molotov_core::elf::ElfBinary::parse(&[u8])`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for ELF64 validation and segment parsing**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/elf_parser_test.rs
|
||||
use molotov_core::elf::ElfBinary;
|
||||
|
||||
#[test]
|
||||
fn test_rejects_non_elf_magic() {
|
||||
let dummy = b"NOT_AN_ELF_BINARY";
|
||||
let res = ElfBinary::parse(dummy);
|
||||
assert!(res.is_err(), "Must reject files without 0x7F 'E' 'L' 'F' magic header");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parses_valid_minimal_elf64() {
|
||||
let mut header = vec![0u8; 64];
|
||||
// e_ident
|
||||
header[0..4].copy_from_slice(b"\x7fELF");
|
||||
header[4] = 2; // ELFCLASS64
|
||||
header[5] = 1; // ELFDATA2LSB (little endian)
|
||||
header[6] = 1; // EV_CURRENT
|
||||
// e_type = ET_DYN (3)
|
||||
header[16] = 3;
|
||||
// e_machine = EM_X86_64 (62)
|
||||
header[18] = 62;
|
||||
// e_phentsize = 56
|
||||
header[54] = 56;
|
||||
|
||||
let res = ElfBinary::parse(&header);
|
||||
assert!(res.is_ok(), "Valid minimal ELF64 dynamic library header must be accepted");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test elf_parser_test`
|
||||
Expected: FAIL with unexported module `elf`.
|
||||
|
||||
- [ ] **Step 3: Implement ElfBinary parser**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/elf.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ElfError {
|
||||
#[error("File is smaller than minimum ELF64 header (64 bytes)")]
|
||||
TooSmall,
|
||||
#[error("Invalid ELF magic header: expected 0x7F 'ELF'")]
|
||||
InvalidMagic,
|
||||
#[error("Unsupported ELF class: expected 64-bit")]
|
||||
UnsupportedClass,
|
||||
#[error("Unsupported endianness: expected Little Endian")]
|
||||
UnsupportedEndianness,
|
||||
#[error("Unsupported architecture: expected x86_64 (62), found {0}")]
|
||||
UnsupportedMachine(u16),
|
||||
#[error("Invalid ELF type: expected ET_DYN (3), found {0}")]
|
||||
UnsupportedType(u16),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProgramHeader {
|
||||
pub p_type: u32,
|
||||
pub p_flags: u32,
|
||||
pub p_offset: u64,
|
||||
pub p_vaddr: u64,
|
||||
pub p_paddr: u64,
|
||||
pub p_filesz: u64,
|
||||
pub p_memsz: u64,
|
||||
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],
|
||||
pub entry_point: u64,
|
||||
pub program_headers: Vec<ProgramHeader>,
|
||||
}
|
||||
|
||||
impl<'a> ElfBinary<'a> {
|
||||
pub fn parse(bytes: &'a [u8]) -> Result<Self, ElfError> {
|
||||
if bytes.len() < 64 {
|
||||
return Err(ElfError::TooSmall);
|
||||
}
|
||||
|
||||
if &bytes[0..4] != b"\x7fELF" {
|
||||
return Err(ElfError::InvalidMagic);
|
||||
}
|
||||
|
||||
if bytes[4] != 2 {
|
||||
return Err(ElfError::UnsupportedClass);
|
||||
}
|
||||
|
||||
if bytes[5] != 1 {
|
||||
return Err(ElfError::UnsupportedEndianness);
|
||||
}
|
||||
|
||||
let e_type = u16::from_le_bytes([bytes[16], bytes[17]]);
|
||||
if e_type != 3 {
|
||||
return Err(ElfError::UnsupportedType(e_type));
|
||||
}
|
||||
|
||||
let e_machine = u16::from_le_bytes([bytes[18], bytes[19]]);
|
||||
if e_machine != 62 {
|
||||
return Err(ElfError::UnsupportedMachine(e_machine));
|
||||
}
|
||||
|
||||
let entry_point = u64::from_le_bytes(bytes[24..32].try_into().unwrap());
|
||||
let phoff = u64::from_le_bytes(bytes[32..40].try_into().unwrap()) as usize;
|
||||
let phentsize = u16::from_le_bytes([bytes[54], bytes[55]]) as usize;
|
||||
let phnum = u16::from_le_bytes([bytes[56], bytes[57]]) as usize;
|
||||
|
||||
let mut program_headers = Vec::with_capacity(phnum);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
raw: bytes,
|
||||
entry_point,
|
||||
program_headers,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_segments(&self) -> impl Iterator<Item = &ProgramHeader> {
|
||||
self.program_headers.iter().filter(|ph| ph.p_type == PT_LOAD)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test elf_parser_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(core): implement ELF64 header and program segment parser"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Virtual Memory Mapper & Segment Protection Manager
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/mmap.rs`
|
||||
- Test: `crates/molotov-core/tests/mmap_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `molotov_core::elf::{ElfBinary, ProgramHeader}`
|
||||
- Produces:
|
||||
- `molotov_core::mmap::MappedImage`
|
||||
- `molotov_core::mmap::MappedImage::allocate_for_elf()`
|
||||
- `molotov_core::mmap::MappedImage::finalize_protections()`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for virtual memory allocation and page alignment**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/mmap_test.rs
|
||||
use molotov_core::mmap::MappedImage;
|
||||
|
||||
#[test]
|
||||
fn test_page_alignment_helper() {
|
||||
assert_eq!(MappedImage::page_align_down(4096), 4096);
|
||||
assert_eq!(MappedImage::page_align_down(4097), 4096);
|
||||
assert_eq!(MappedImage::page_align_up(4095), 4096);
|
||||
assert_eq!(MappedImage::page_align_up(4096), 4096);
|
||||
assert_eq!(MappedImage::page_align_up(4097), 8192);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mmap_reservation_and_drop() {
|
||||
let mapping = MappedImage::allocate_anonymous(65536).expect("Should mmap virtual address space");
|
||||
assert!(!mapping.base_addr().is_null());
|
||||
assert_eq!(mapping.size(), 65536);
|
||||
// Dropping mapping unmaps memory automatically via RAII
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test mmap_test`
|
||||
Expected: FAIL with unexported module `mmap`.
|
||||
|
||||
- [ ] **Step 3: Implement MappedImage with RAII munmap**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/mmap.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use std::ptr;
|
||||
|
||||
pub const PAGE_SIZE: usize = 4096;
|
||||
|
||||
pub struct MappedImage {
|
||||
base: *mut u8,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
impl MappedImage {
|
||||
pub const fn page_align_down(addr: usize) -> usize {
|
||||
addr & !(PAGE_SIZE - 1)
|
||||
}
|
||||
|
||||
pub const fn page_align_up(addr: usize) -> usize {
|
||||
(addr + PAGE_SIZE - 1) & !(PAGE_SIZE - 1)
|
||||
}
|
||||
|
||||
pub fn allocate_anonymous(size: usize) -> Result<Self, String> {
|
||||
let aligned_size = Self::page_align_up(size);
|
||||
let ptr = unsafe {
|
||||
libc::mmap(
|
||||
ptr::null_mut(),
|
||||
aligned_size,
|
||||
libc::PROT_READ | libc::PROT_WRITE,
|
||||
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
|
||||
-1,
|
||||
0,
|
||||
)
|
||||
};
|
||||
|
||||
if ptr == libc::MAP_FAILED {
|
||||
Err("mmap failed to allocate anonymous memory".to_string())
|
||||
} else {
|
||||
Ok(Self {
|
||||
base: ptr as *mut u8,
|
||||
size: aligned_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_addr(&self) -> *mut u8 {
|
||||
self.base
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn protect(&self, offset: usize, len: usize, prot: i32) -> Result<(), String> {
|
||||
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 ret = unsafe {
|
||||
libc::mprotect(
|
||||
self.base.add(page_start) as *mut libc::c_void,
|
||||
aligned_len,
|
||||
prot,
|
||||
)
|
||||
};
|
||||
|
||||
if ret == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("mprotect failed with errno {}", std::io::Error::last_os_error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MappedImage {
|
||||
fn drop(&mut self) {
|
||||
if !self.base.is_null() {
|
||||
unsafe {
|
||||
libc::munmap(self.base as *mut libc::c_void, self.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for MappedImage {}
|
||||
unsafe impl Sync for MappedImage {}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test mmap_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(core): implement RAII virtual memory mapper with page alignment"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Dynamic Section & Symbol Table Parser
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/dynamic.rs`
|
||||
- Test: `crates/molotov-core/tests/dynamic_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `molotov_core::elf::ElfBinary`
|
||||
- Produces:
|
||||
- `molotov_core::dynamic::DynamicSection`
|
||||
- `molotov_core::dynamic::ElfSymbol`
|
||||
- `molotov_core::dynamic::DynamicSection::find_symbol(&self, name: &str)`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for string and symbol table lookup**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/dynamic_test.rs
|
||||
use molotov_core::dynamic::DynamicSection;
|
||||
|
||||
#[test]
|
||||
fn test_string_table_lookup() {
|
||||
let strtab = b"\0JNI_OnLoad\0ANativeActivity_onCreate\0";
|
||||
let dynamic = DynamicSection::new_with_strtab(strtab);
|
||||
|
||||
assert_eq!(dynamic.lookup_string(1), Some("JNI_OnLoad"));
|
||||
assert_eq!(dynamic.lookup_string(12), Some("ANativeActivity_onCreate"));
|
||||
assert_eq!(dynamic.lookup_string(999), None);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test dynamic_test`
|
||||
Expected: FAIL with unexported module `dynamic`.
|
||||
|
||||
- [ ] **Step 3: Implement DynamicSection**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/dynamic.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ElfSymbol {
|
||||
pub name_offset: u32,
|
||||
pub info: u8,
|
||||
pub other: u8,
|
||||
pub shndx: u16,
|
||||
pub value: u64,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DynamicSection<'a> {
|
||||
pub strtab: &'a [u8],
|
||||
pub symbols: Vec<ElfSymbol>,
|
||||
pub name_to_index: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl<'a> DynamicSection<'a> {
|
||||
pub fn new_with_strtab(strtab: &'a [u8]) -> Self {
|
||||
Self {
|
||||
strtab,
|
||||
symbols: Vec::new(),
|
||||
name_to_index: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lookup_string(&self, offset: usize) -> Option<&'a str> {
|
||||
if offset >= self.strtab.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let slice = &self.strtab[offset..];
|
||||
let null_pos = slice.iter().position(|&b| b == 0)?;
|
||||
std::str::from_utf8(&slice[..null_pos]).ok()
|
||||
}
|
||||
|
||||
pub fn register_symbol(&mut self, sym: ElfSymbol) {
|
||||
if let Some(name) = self.lookup_string(sym.name_offset as usize) {
|
||||
self.name_to_index.insert(name.to_string(), self.symbols.len());
|
||||
}
|
||||
self.symbols.push(sym);
|
||||
}
|
||||
|
||||
pub fn find_symbol(&self, name: &str) -> Option<&ElfSymbol> {
|
||||
let idx = self.name_to_index.get(name)?;
|
||||
self.symbols.get(*idx)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test dynamic_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(core): implement dynamic section string table and symbol resolution"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Relocation Processing Engine (`R_X86_64`)
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/reloc.rs`
|
||||
- Test: `crates/molotov-core/tests/reloc_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `molotov_core::mmap::MappedImage`, `molotov_core::symbols::SymbolTriage`
|
||||
- Produces:
|
||||
- `molotov_core::reloc::RelocationEntry`
|
||||
- `molotov_core::reloc::apply_relocation()`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for relative and symbol relocation calculation**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/reloc_test.rs
|
||||
use molotov_core::reloc::{apply_relative_relocation, RelocationEntry, R_X86_64_RELATIVE};
|
||||
|
||||
#[test]
|
||||
fn test_relative_relocation_adds_slide_and_addend() {
|
||||
let mut memory = vec![0u8; 64];
|
||||
let base_slide: u64 = 0x7fff_0000_0000;
|
||||
let addend: i64 = 0x1000;
|
||||
let reloc = RelocationEntry {
|
||||
offset: 8,
|
||||
r_type: R_X86_64_RELATIVE,
|
||||
symbol_index: 0,
|
||||
addend,
|
||||
};
|
||||
|
||||
apply_relative_relocation(&mut memory, base_slide, &reloc).expect("Relocation should apply cleanly");
|
||||
|
||||
let resolved = u64::from_le_bytes(memory[8..16].try_into().unwrap());
|
||||
assert_eq!(resolved, 0x7fff_0000_1000);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test reloc_test`
|
||||
Expected: FAIL with unexported module `reloc`.
|
||||
|
||||
- [ ] **Step 3: Implement RelocationEntry and relocation processor**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/reloc.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
pub const R_X86_64_64: u32 = 1;
|
||||
pub const R_X86_64_GLOB_DAT: u32 = 6;
|
||||
pub const R_X86_64_JUMP_SLOT: u32 = 7;
|
||||
pub const R_X86_64_RELATIVE: u32 = 8;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RelocationEntry {
|
||||
pub offset: u64,
|
||||
pub r_type: u32,
|
||||
pub symbol_index: u32,
|
||||
pub addend: i64,
|
||||
}
|
||||
|
||||
impl RelocationEntry {
|
||||
pub fn parse_rela(bytes: &[u8]) -> Option<Self> {
|
||||
if bytes.len() < 24 {
|
||||
return None;
|
||||
}
|
||||
let offset = u64::from_le_bytes(bytes[0..8].try_into().ok()?);
|
||||
let info = u64::from_le_bytes(bytes[8..16].try_into().ok()?);
|
||||
let addend = i64::from_le_bytes(bytes[16..24].try_into().ok()?);
|
||||
|
||||
let r_type = (info & 0xffff_ffff) as u32;
|
||||
let symbol_index = (info >> 32) as u32;
|
||||
|
||||
Some(Self {
|
||||
offset,
|
||||
r_type,
|
||||
symbol_index,
|
||||
addend,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_relative_relocation(
|
||||
memory: &mut [u8],
|
||||
base_slide: u64,
|
||||
reloc: &RelocationEntry,
|
||||
) -> Result<(), String> {
|
||||
let target_offset = reloc.offset as usize;
|
||||
if target_offset + 8 > memory.len() {
|
||||
return Err(format!("Relocation offset 0x{:x} out of bounds", target_offset));
|
||||
}
|
||||
|
||||
let target_val = (base_slide as i128 + reloc.addend as i128) as u64;
|
||||
memory[target_offset..target_offset + 8].copy_from_slice(&target_val.to_le_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_symbol_relocation(
|
||||
memory: &mut [u8],
|
||||
symbol_address: u64,
|
||||
reloc: &RelocationEntry,
|
||||
) -> Result<(), String> {
|
||||
let target_offset = reloc.offset as usize;
|
||||
if target_offset + 8 > memory.len() {
|
||||
return Err(format!("Symbol relocation offset 0x{:x} out of bounds", target_offset));
|
||||
}
|
||||
|
||||
let target_val = (symbol_address as i128 + reloc.addend as i128) as u64;
|
||||
memory[target_offset..target_offset + 8].copy_from_slice(&target_val.to_le_bytes());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test reloc_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(core): implement x86_64 dynamic relocation calculation engine"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Stub Trampoline Generator & Host Forwarding
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/trampoline.rs`
|
||||
- Test: `crates/molotov-core/tests/trampoline_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `molotov_core::symbols::{SymbolCategory, SymbolTriage}`
|
||||
- Produces:
|
||||
- `molotov_core::trampoline::TrampolinePool`
|
||||
- `molotov_core::trampoline::resolve_symbol_address()`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for stub failure execution**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/trampoline_test.rs
|
||||
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");
|
||||
assert_ne!(addr, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unimplemented_stub_returns_failing_function() {
|
||||
let stub_fn = resolve_symbol_address("unimplemented_android_telemetry")
|
||||
.expect("Should resolve to a failing stub trampoline");
|
||||
let func: extern "C" fn() -> i32 = unsafe { std::mem::transmute(stub_fn) };
|
||||
let ret = func();
|
||||
assert_eq!(ret, -38, "Stub must return -ENOSYS (-38), never fake 0 or success");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test trampoline_test`
|
||||
Expected: FAIL with unexported module `trampoline`.
|
||||
|
||||
- [ ] **Step 3: Implement TrampolinePool and symbol resolver**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/trampoline.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use std::ffi::CString;
|
||||
|
||||
pub extern "C" fn generic_failing_stub() -> i32 {
|
||||
// ENOSYS is errno 38 on Linux x86_64
|
||||
-38
|
||||
}
|
||||
|
||||
pub fn resolve_symbol_address(symbol: &str) -> Result<u64, String> {
|
||||
// 1. 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);
|
||||
Ok(generic_failing_stub as *const () as u64)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test trampoline_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(core): implement failing stub trampoline and host dlsym resolver"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Bionic TLS Allocator & Slot Manager
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/tls.rs`
|
||||
- Test: `crates/molotov-core/tests/tls_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: None
|
||||
- Produces:
|
||||
- `molotov_core::tls::BionicTls`
|
||||
- `molotov_core::tls::BionicTls::allocate()`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for Bionic TLS slot alignment**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/tls_test.rs
|
||||
use molotov_core::tls::BionicTls;
|
||||
|
||||
#[test]
|
||||
fn test_bionic_tls_allocation() {
|
||||
let tls = BionicTls::allocate().expect("Bionic TLS block should allocate cleanly");
|
||||
assert!(!tls.tcb_ptr().is_null());
|
||||
assert_eq!(tls.tcb_ptr() as usize % 64, 0, "TLS pointer must be 64-byte aligned");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test tls_test`
|
||||
Expected: FAIL with unexported module `tls`.
|
||||
|
||||
- [ ] **Step 3: Implement BionicTls allocator**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/tls.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use std::alloc::{alloc_zeroed, dealloc, Layout};
|
||||
|
||||
pub const BIONIC_TLS_SLOTS: usize = 128;
|
||||
|
||||
pub struct BionicTls {
|
||||
ptr: *mut u64,
|
||||
layout: Layout,
|
||||
}
|
||||
|
||||
impl BionicTls {
|
||||
pub fn allocate() -> Result<Self, String> {
|
||||
let layout = Layout::from_size_align(BIONIC_TLS_SLOTS * std::mem::size_of::<u64>(), 64)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let ptr = unsafe { alloc_zeroed(layout) as *mut u64 };
|
||||
if ptr.is_null() {
|
||||
Err("Failed to allocate memory for Bionic TLS".to_string())
|
||||
} else {
|
||||
// Android x86_64: slot 0 points to the thread control block itself
|
||||
unsafe {
|
||||
*ptr = ptr as u64;
|
||||
}
|
||||
Ok(Self { ptr, layout })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tcb_ptr(&self) -> *mut u64 {
|
||||
self.ptr
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BionicTls {
|
||||
fn drop(&mut self) {
|
||||
if !self.ptr.is_null() {
|
||||
unsafe {
|
||||
dealloc(self.ptr as *mut u8, self.layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test tls_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(core): implement Bionic TLS block allocation and TCB self-pointer setup"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: End-to-End Bionic Linker & Entrypoint Resolver
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/linker.rs`
|
||||
- Modify: `crates/molotov-core/src/lib.rs`
|
||||
- Test: `crates/molotov-core/tests/linker_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: All `molotov-core` modules (`elf`, `mmap`, `dynamic`, `reloc`, `trampoline`, `tls`, `symbols`)
|
||||
- Produces:
|
||||
- `molotov_core::linker::BionicLinker`
|
||||
- `molotov_core::linker::BionicLinker::load_library()`
|
||||
- `molotov_core::linker::BionicLinker::resolve_symbol()`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for end-to-end synthetic ELF loading and symbol resolution**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/linker_test.rs
|
||||
use molotov_core::linker::BionicLinker;
|
||||
|
||||
#[test]
|
||||
fn test_linker_rejects_empty_data() {
|
||||
let linker = BionicLinker::new();
|
||||
let res = linker.load_from_bytes(&[]);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test linker_test`
|
||||
Expected: FAIL with unexported module `linker`.
|
||||
|
||||
- [ ] **Step 3: Implement BionicLinker orchestrator**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/linker.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use crate::dynamic::DynamicSection;
|
||||
use crate::elf::{ElfBinary, PT_DYNAMIC, PT_LOAD};
|
||||
use crate::mmap::MappedImage;
|
||||
use crate::reloc::{apply_relative_relocation, apply_symbol_relocation, RelocationEntry, R_X86_64_RELATIVE};
|
||||
use crate::symbols::SymbolTriage;
|
||||
use crate::trampoline::resolve_symbol_address;
|
||||
|
||||
pub struct LoadedLibrary {
|
||||
pub mapping: MappedImage,
|
||||
pub base_addr: u64,
|
||||
}
|
||||
|
||||
pub struct BionicLinker {
|
||||
triage: SymbolTriage,
|
||||
}
|
||||
|
||||
impl Default for BionicLinker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl BionicLinker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
triage: SymbolTriage::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_from_bytes(&self, bytes: &[u8]) -> Result<LoadedLibrary, String> {
|
||||
let elf = ElfBinary::parse(bytes).map_err(|e| e.to_string())?;
|
||||
|
||||
// Calculate total virtual memory span
|
||||
let mut min_vaddr = u64::MAX;
|
||||
let mut max_vaddr = 0u64;
|
||||
|
||||
for seg in elf.load_segments() {
|
||||
if seg.p_vaddr < min_vaddr {
|
||||
min_vaddr = seg.p_vaddr;
|
||||
}
|
||||
let seg_end = seg.p_vaddr + seg.p_memsz;
|
||||
if seg_end > max_vaddr {
|
||||
max_vaddr = seg_end;
|
||||
}
|
||||
}
|
||||
|
||||
if min_vaddr > max_vaddr {
|
||||
return Err("No PT_LOAD segments found in ELF binary".to_string());
|
||||
}
|
||||
|
||||
let total_size = (max_vaddr - min_vaddr) as usize;
|
||||
let mapping = MappedImage::allocate_anonymous(total_size)?;
|
||||
let base_addr = mapping.base_addr() as u64;
|
||||
|
||||
// Copy PT_LOAD segments into virtual memory
|
||||
for seg in elf.load_segments() {
|
||||
let seg_dest_offset = (seg.p_vaddr - 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LoadedLibrary {
|
||||
mapping,
|
||||
base_addr,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `crates/molotov-core/src/lib.rs`:
|
||||
```rust
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pub mod dynamic;
|
||||
pub mod elf;
|
||||
pub mod linker;
|
||||
pub mod mmap;
|
||||
pub mod reloc;
|
||||
pub mod symbols;
|
||||
pub mod tls;
|
||||
pub mod trampoline;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test linker_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(core): implement end-to-end BionicLinker library loader"
|
||||
```
|
||||
|
||||
---
|
||||
Reference in a new issue