From 5ae1b8e974a2b1d42f981f6954a6a2d43d657097 Mon Sep 17 00:00:00 2001 From: Anhad Singh Date: Thu, 19 Dec 2024 00:29:06 +1100 Subject: [PATCH] feat(ld.so): lazy binding Signed-off-by: Anhad Singh --- src/header/dlfcn/mod.rs | 13 +- src/ld_so/callbacks.rs | 8 +- src/ld_so/dso.rs | 43 ++++- src/ld_so/linker.rs | 338 ++++++++++++++++++++++++++++++++++------ 4 files changed, 341 insertions(+), 61 deletions(-) diff --git a/src/header/dlfcn/mod.rs b/src/header/dlfcn/mod.rs index b12f834b42..c62fcacd0f 100644 --- a/src/header/dlfcn/mod.rs +++ b/src/header/dlfcn/mod.rs @@ -7,7 +7,11 @@ use core::{ sync::atomic::{AtomicUsize, Ordering}, }; -use crate::{c_str::CStr, ld_so::tcb::Tcb, platform::types::*}; +use crate::{ + c_str::CStr, + ld_so::{linker::Resolve, tcb::Tcb}, + platform::types::*, +}; pub const RTLD_LAZY: c_int = 0x0001; pub const RTLD_NOW: c_int = 0x0002; @@ -42,6 +46,11 @@ pub unsafe extern "C" fn dladdr(addr: *mut c_void, info: *mut Dl_info) -> c_int #[no_mangle] pub unsafe extern "C" fn dlopen(cfilename: *const c_char, flags: c_int) -> *mut c_void { //TODO support all sort of flags + let resolve = if flags & RTLD_NOW == RTLD_NOW { + Resolve::Now + } else { + Resolve::Lazy + }; let filename = if cfilename.is_null() { None @@ -69,7 +78,7 @@ pub unsafe extern "C" fn dlopen(cfilename: *const c_char, flags: c_int) -> *mut let cbs_c = linker.cbs.clone(); let cbs = cbs_c.borrow(); - let id = match (cbs.load_library)(&mut linker, filename) { + let id = match (cbs.load_library)(&mut linker, filename, resolve) { Err(err) => { ERROR.store(ERROR_NOT_SUPPORTED.as_ptr() as usize, Ordering::SeqCst); return ptr::null_mut(); diff --git a/src/ld_so/callbacks.rs b/src/ld_so/callbacks.rs index 1d7d7a05f4..f2b5b3e51e 100644 --- a/src/ld_so/callbacks.rs +++ b/src/ld_so/callbacks.rs @@ -1,11 +1,11 @@ -use super::linker::Linker; +use super::linker::{Linker, Resolve}; use crate::platform::types::c_void; use alloc::boxed::Box; use goblin::error::Result; pub struct LinkerCallbacks { pub unload: Box, - pub load_library: Box) -> Result>, + pub load_library: Box, Resolve) -> Result>, pub get_sym: Box Option<*mut c_void>>, } @@ -23,8 +23,8 @@ fn unload(linker: &mut Linker, lib_id: usize) { linker.unload(lib_id) } -fn load_library(linker: &mut Linker, name: Option<&str>) -> Result { - linker.load_library(name) +fn load_library(linker: &mut Linker, name: Option<&str>, resolve: Resolve) -> Result { + linker.load_library(name, resolve) } fn get_sym(linker: &Linker, lib_id: usize, name: &str) -> Option<*mut c_void> { diff --git a/src/ld_so/dso.rs b/src/ld_so/dso.rs index 8f619e3957..56a33d01bc 100644 --- a/src/ld_so/dso.rs +++ b/src/ld_so/dso.rs @@ -1,3 +1,5 @@ +//! See . + use super::{ debug::{RTLDDebug, _r_debug}, linker::Symbol, @@ -33,10 +35,32 @@ use goblin::elf64::{ sym, }; use goblin::{ - elf::Elf, + elf::{ + sym::{STB_GLOBAL, STB_WEAK}, + Dynamic, Elf, + }, error::{Error, Result}, }; +#[derive(Debug, PartialEq)] +#[repr(u8)] +pub enum SymbolBinding { + /// Global symbols are visible to all object files being combined. One + /// file's definition of a global symbol will satisfy another file's + /// undefined reference to the same global symbol. + Global = STB_GLOBAL, + /// Weak symbols resemble global symbols, but their definitions have lower + /// precedence. + Weak = STB_WEAK, +} + +impl SymbolBinding { + #[inline] + pub fn is_global(&self) -> bool { + matches!(self, Self::Global) + } +} + /// Use to represent a library as well as all the symbols that is loaded withen it. #[derive(Default)] pub struct DSO { @@ -57,6 +81,9 @@ pub struct DSO { pub tls_module_id: usize, pub tls_offset: usize, pub use_count: usize, + + pub dynamic: Option, + pub dynsyms: Vec, } impl DSO { @@ -106,15 +133,19 @@ impl DSO { 0 }, tls_offset: tls_offset, + + dynamic: elf.dynamic.map(|dynamic| dynamic), + dynsyms: elf.dynsyms.iter().collect(), }; - return Ok((dso, tcb_master)); + + Ok((dso, tcb_master)) } - pub fn get_sym(&self, name: &str) -> Option<(Symbol, bool)> { + pub fn get_sym(&self, name: &str) -> Option<(Symbol, SymbolBinding)> { if let Some(value) = self.global_syms.get(name) { - Some((*value, true)) + Some((*value, SymbolBinding::Global)) } else if let Some(value) = self.weak_syms.get(name) { - Some((*value, false)) + Some((*value, SymbolBinding::Weak)) } else { None } @@ -404,7 +435,7 @@ impl DSO { .iter() .filter(|s| s.sh_type == SHT_INIT_ARRAY || s.sh_type == SHT_FINI_ARRAY) { - let addr = if is_pie_enabled(&elf) { + let addr = if is_pie_enabled(elf) { mmap_addr + section.vm_range().start } else { section.vm_range().start diff --git a/src/ld_so/linker.rs b/src/ld_so/linker.rs index b8d2156eb5..34711d5450 100644 --- a/src/ld_so/linker.rs +++ b/src/ld_so/linker.rs @@ -6,7 +6,12 @@ use alloc::{ }; use core::{cell::RefCell, mem::transmute, ptr}; use goblin::{ - elf::{program_header, reloc, sym::STT_TLS, Elf}, + elf::{ + dynamic::{DT_PLTGOT, DT_STRTAB}, + program_header, reloc, + sym::STT_TLS, + Elf, + }, error::{Error, Result}, }; @@ -19,8 +24,9 @@ use crate::{ unistd::F_OK, }, io::{self, Read}, + ld_so::dso::SymbolBinding, platform::{ - types::{c_int, c_void}, + types::{c_char, c_int, c_uint, c_void}, Pal, Sys, }, }; @@ -68,6 +74,16 @@ impl Symbol { } } +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub enum Resolve { + /// Resolve all undefined symbols immediately. + Now, + /// Perform lazy binding (i.e. symbols will be resolved when they are first + /// used). + #[default] + Lazy, +} + bitflags::bitflags! { #[derive(Debug, Default)] pub struct DebugFlags: u32 { @@ -141,53 +157,50 @@ impl Linker { } pub fn load_program(&mut self, path: &str, base_addr: Option) -> Result { - self.load_object(path, &None, base_addr, false)?; - return Ok(self.objects.get(&root_id).unwrap().entry_point); + self.load_object(path, &None, base_addr, false, Resolve::default())?; + // TODO(andypython): make self.load_object() return a reference to the + // loaded object, thereby remove the ugly unwrap(). + Ok(self.objects.get(&root_id).unwrap().entry_point) } - pub fn load_library(&mut self, name: Option<&str>) -> Result { + pub fn load_library(&mut self, name: Option<&str>, resolve: Resolve) -> Result { match name { Some(name) => { if let Some(id) = self.name_to_object_id_map.get(name) { let obj = self.objects.get_mut(id).unwrap(); obj.use_count += 1; - return Ok(*id); + Ok(*id) } else { let parent_runpath = &self .objects .get(&root_id) .and_then(|parent| parent.runpath.clone()); let lib_id = self.next_object_id; - self.load_object(name, parent_runpath, None, true)?; + self.load_object(name, parent_runpath, None, true, resolve)?; - return Ok(lib_id); + Ok(lib_id) } } - None => return Ok(root_id), + + None => Ok(root_id), } } pub fn get_sym(&self, lib_id: usize, name: &str) -> Option<*mut c_void> { - match self.objects.get(&lib_id) { - Some(obj) => { - return obj.get_sym(name).map(|(s, strong)| { - if s.sym_type != STT_TLS { - s.as_ptr() - } else { - unsafe { - let mut tls_index = dl_tls_index { - ti_module: obj.tls_module_id as u64, - ti_offset: s.value as u64, - }; - __tls_get_addr(&mut tls_index) - } - } - }); - } - _ => { - return None; - } - } + self.objects.get(&lib_id).and_then(|obj| { + obj.get_sym(name).map(|(s, _binding)| { + if s.sym_type != STT_TLS { + s.as_ptr() + } else { + let mut tls_index = dl_tls_index { + ti_module: obj.tls_module_id as u64, + ti_offset: s.value as u64, + }; + + unsafe { __tls_get_addr(&mut tls_index) } + } + }) + }) } pub fn unload(&mut self, lib_id: usize) { @@ -219,6 +232,7 @@ impl Linker { runpath: &Option, base_addr: Option, dlopened: bool, + resolve: Resolve, ) -> Result<()> { unsafe { _r_debug.state = RTLDState::RT_ADD }; _dl_debug_state(); @@ -335,7 +349,7 @@ impl Linker { } } - self.relocate(&new_objects, &objects_data)?; + self.relocate(&new_objects, &objects_data, resolve)?; self.run_init(&new_objects); for obj in new_objects.into_iter() { @@ -420,7 +434,7 @@ impl Linker { )?; } - return Ok(()); + Ok(()) } fn search_object(&self, name: &str, parent_runpath: &Option) -> Result { @@ -474,7 +488,102 @@ impl Linker { return Ok(data); } - fn relocate(&self, new_objects: &Vec, objects_data: &Vec>) -> Result<()> { + fn resolve_sym<'a>(name: &str, objs: impl Iterator) -> Option { + let mut res = None; + + for dso in objs { + if let Some((sym, binding)) = dso.get_sym(name) { + if binding.is_global() { + return Some(sym); + } + + res = Some(sym); + } + } + + res + } + + /// Perform lazy relocations. + fn lazy_relocate<'a>( + &self, + obj: &DSO, + new_objects: &[DSO], + elf: &Elf, + resolve: Resolve, + ) -> Result<()> { + let object_base_addr = obj.mmap.as_ptr() as u64; + + if let Some(dynamic) = elf.dynamic.as_ref() { + // Global Offset Table + let got = if let Some(ptr) = { + dynamic + .dyns + .iter() + .find(|r#dyn| r#dyn.d_tag == DT_PLTGOT) + .map(|r#dyn| r#dyn.d_val) + } { + (object_base_addr + ptr) as *mut usize + } else { + assert_eq!(dynamic.info.jmprel, 0); + return Ok(()); + }; + + unsafe { + got.add(1).write(obj.id); + got.add(2).write(__plt_resolve_trampoline as usize); + } + + for rel in elf.pltrelocs.iter() { + match (rel.r_type, resolve) { + (reloc::R_X86_64_JUMP_SLOT, Resolve::Lazy) => unsafe { + *((object_base_addr + rel.r_offset) as *mut u64) += object_base_addr; + }, + + (reloc::R_X86_64_JUMP_SLOT, Resolve::Now) => { + let sym = elf.dynsyms.get(rel.r_sym).ok_or(Error::Malformed(format!( + "missing symbol for relocation {:?}", + rel + )))?; + + let name = + elf.dynstrtab + .get_at(sym.st_name) + .ok_or(Error::Malformed(format!( + "missing name for symbol {:?}", + sym + )))?; + + // FIXME(andypython): warn on unresolved symbols? + let resolved = Linker::resolve_sym( + name, + self.objects.values().chain(new_objects.iter()), + ) + .map(|sym| sym.as_ptr()) + .unwrap_or(ptr::null_mut()); + + let addend = rel.r_addend.unwrap_or(0) as u64; + + unsafe { + *((object_base_addr + rel.r_offset) as *mut u64) = + resolved as u64 + addend; + } + } + + _ => todo!("unsupported relocation type {:?}", rel.r_type), + } + } + } + + Ok(()) + } + + fn relocate( + &self, + new_objects: &Vec, + objects_data: &Vec>, + resolve: Resolve, + ) -> Result<()> { let symbols_lookup_objects: Vec<&DSO> = self.objects.values().chain(new_objects.iter()).collect(); @@ -489,12 +598,7 @@ impl Linker { let b = mmap.as_ptr() as usize; // Relocate - for rel in elf - .dynrelas - .iter() - .chain(elf.dynrels.iter()) - .chain(elf.pltrelocs.iter()) - { + for rel in elf.dynrelas.iter().chain(elf.dynrels.iter()) { trace!( " rel {}: {:x?}", reloc::r_to_str(rel.r_type, elf.header.e_machine), @@ -520,21 +624,20 @@ impl Linker { reloc::R_X86_64_COPY => 1, _ => 0, }; - for lookup_id in lookup_start..symbols_lookup_objects.len() { - let lookup_obj = &symbols_lookup_objects[lookup_id]; - if let Some((s, strong)) = lookup_obj.get_sym(name) { + for lookup_obj in symbols_lookup_objects.iter().skip(lookup_start) { + if let Some((s, binding)) = lookup_obj.get_sym(name) { trace!( - "symbol {} from {} found in {} ({})", + "symbol {} from {} found in {} ({:?})", name, obj.name, lookup_obj.name, - if strong { "strong" } else { "weak" } + binding ); symbol = Some(s); t = lookup_obj.tls_offset; found = true; // Stop looking if any strong symbol is found - if strong { + if binding.is_global() { break; } } @@ -607,7 +710,7 @@ impl Linker { let sym = symbol .as_ref() .expect("R_X86_64_COPY called without valid symbol"); - ptr::copy_nonoverlapping(sym.as_ptr() as *const u8, ptr, sym.size as usize); + ptr::copy_nonoverlapping(sym.as_ptr() as *const u8, ptr, sym.size); }, _ => { panic!( @@ -639,7 +742,7 @@ impl Linker { prot |= sys_mman::PROT_WRITE; } - let res = unsafe { + unsafe { let ptr = if is_pie_enabled(&elf) { mmap.as_ptr().add(vaddr) } else { @@ -649,18 +752,22 @@ impl Linker { Sys::mprotect(ptr as *mut c_void, vsize, prot).map_err(|_| { Error::Malformed(format!("failed to mprotect {}", obj.name)) })?; - }; + } } } - return Ok(()); + for (i, obj) in new_objects.iter().enumerate() { + self.lazy_relocate(obj, new_objects, &Elf::parse(&objects_data[i])?, resolve)?; + } + + Ok(()) } fn run_init(&self, objects: &Vec) { use crate::platform::{self, types::*}; for obj in objects.iter().rev() { - if let Some((symbol, true)) = obj.get_sym("__relibc_init_environ") { + if let Some((symbol, SymbolBinding::Global)) = obj.get_sym("__relibc_init_environ") { unsafe { symbol .as_ptr() @@ -673,3 +780,136 @@ impl Linker { } } } + +// GOT[1] = object_id +// GOT[2] = __plt_resolve_trampoline +// +// The stubs in .plt will push the relocation index and the object ID onto the stack and jump to +// [`__plt_resolve_trampoline`]. The trampoline will then call this function to resolve the symbol +// and update the respective GOT entry. The trampoline will then jump to the resolved symbol. +// +// FIXME(andypython): 32-bit +extern "C" fn __plt_resolve_inner(object_id: usize, relocation_index: c_uint) -> *mut c_void { + let tcb = unsafe { Tcb::current() }.unwrap(); + assert!(!tcb.linker_ptr.is_null()); + + let linker = unsafe { &*tcb.linker_ptr }.lock(); + + let obj = linker.objects.get(&object_id).unwrap(); + let obj_base = obj.mmap.as_ptr() as usize; + let dynamic = obj.dynamic.as_ref().unwrap(); + let jmprel = dynamic.info.jmprel as usize; + + let rela = unsafe { + &*((obj_base + jmprel) as *const reloc::reloc64::Rela).add(relocation_index as usize) + }; + + assert_eq!( + reloc::reloc64::r_type(rela.r_info), + reloc::R_X86_64_JUMP_SLOT + ); + + let sym = obj + .dynsyms + .get(reloc::reloc64::r_sym(rela.r_info) as usize) + .expect("symbol not found"); + assert_ne!(sym.st_name, 0); + + let strtab_offset = dynamic + .dyns + .iter() + .find(|r#dyn| r#dyn.d_tag == DT_STRTAB) + .unwrap() + .d_val; + + let name = unsafe { + CStr::from_ptr((strtab_offset + sym.st_name as u64 + obj_base as u64) as *const c_char) + }; + + let resolved = Linker::resolve_sym(name.to_str().unwrap(), linker.objects.values()) + .expect("symbol not found") + .as_ptr(); + + unsafe { + println!( + "@plt: {} -> *mut {:#x}", + name.to_string_lossy(), + resolved as usize + ); + + *((obj_base as u64 + rela.r_offset) as *mut u64) = resolved as u64; + } + + resolved +} + +extern "C" { + fn __plt_resolve_trampoline() -> usize; +} + +#[cfg(target_arch = "x86_64")] +core::arch::global_asm!( + " +.global __plt_resolve_trampoline +.hidden __plt_resolve_trampoline +__plt_resolve_trampoline: + push rsi + push rdi + + mov rdi, qword ptr [rsp + 0x10] + mov rsi, qword ptr [rsp + 0x18] + + // stash the floating point argument registers + sub rsp, 128 + movdqu [rsp + 0x00], xmm0 + movdqu [rsp + 0x10], xmm1 + movdqu [rsp + 0x20], xmm2 + movdqu [rsp + 0x30], xmm3 + movdqu [rsp + 0x40], xmm4 + movdqu [rsp + 0x50], xmm5 + movdqu [rsp + 0x60], xmm6 + movdqu [rsp + 0x70], xmm7 + + push rax + push rcx + push rdx + push r8 + push r9 + push r10 + + push rbp + mov rbp, rsp + and rsp, 0xfffffffffffffff0 + call {__plt_resolve_inner} + mov r11, rax + mov rsp, rbp + pop rbp + + pop r10 + pop r9 + pop r8 + pop rdx + pop rcx + pop rax + + movdqu xmm7, [rsp + 0x70] + movdqu xmm6, [rsp + 0x60] + movdqu xmm5, [rsp + 0x50] + movdqu xmm4, [rsp + 0x40] + movdqu xmm3, [rsp + 0x30] + movdqu xmm2, [rsp + 0x20] + movdqu xmm1, [rsp + 0x10] + movdqu xmm0, [rsp + 0x00] + add rsp, 128 + + pop rdi + pop rsi + + add rsp, 0x10 + jmp r11 + + ud2 +.size __plt_resolve_trampoline, . - __plt_resolve_trampoline +", + __plt_resolve_inner = sym __plt_resolve_inner +);