diff --git a/docs/superpowers/plans/2026-09-29-molotov-phase2-bionic-linker.md b/docs/superpowers/plans/2026-09-29-molotov-phase2-bionic-linker.md new file mode 100644 index 0000000..636f8ef --- /dev/null +++ b/docs/superpowers/plans/2026-09-29-molotov-phase2-bionic-linker.md @@ -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, +} + +impl<'a> ElfBinary<'a> { + pub fn parse(bytes: &'a [u8]) -> Result { + 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 { + 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 { + 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, + pub name_to_index: HashMap, +} + +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 { + 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 { + // 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 { + let layout = Layout::from_size_align(BIONIC_TLS_SLOTS * std::mem::size_of::(), 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 { + 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" +``` + +---