diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index 5fefd8cc4b..19c12363a5 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -135,13 +135,8 @@ impl<'a> DynamicInfo<'a> { // On 64-bit (this crate's cfg gate), Elf64_Addr (u64) -> usize is lossless. let base = u64_to_usize(info.dlpi_addr); let dyn_begin = (base + u64_to_usize(dyn_phdr.p_vaddr)) as *const Elf64_Dyn; - let containing_load_segment_end = |addr: usize| -> Option { - phdrs.iter().filter(|p| p.p_type == PT_LOAD).find_map(|p| { - let start = base.checked_add(u64_to_usize(p.p_vaddr))?; - let end = start.checked_add(u64_to_usize(p.p_memsz))?; - (addr >= start && addr < end).then_some(end) - }) - }; + let containing_load_segment_end = + |addr: usize| -> Option { find_containing_load_segment(phdrs, base, addr) }; let correct = |a: u64| -> usize { let a = u64_to_usize(a); if a > base { @@ -587,6 +582,38 @@ pub fn check_sym(sym: &Elf64_Sym) -> bool { matches!(stt, 0 | 1 | 2 | 10) } +/// Find the `PT_LOAD` segment in `phdrs` that contains `addr` (using +/// `base` as the ELF object's load bias) and return the segment's end +/// address. +/// +/// on non-PIE executables `base` is 0 but the segments still have +/// the correct absolute virtual addresses once `base` is added. +fn find_containing_load_segment( + phdrs: &[libc::Elf64_Phdr], + base: usize, + addr: usize, +) -> Option { + phdrs.iter().filter(|p| p.p_type == PT_LOAD).find_map(|p| { + let start = base.checked_add(u64_to_usize(p.p_vaddr))?; + let end = start.checked_add(u64_to_usize(p.p_memsz))?; + (addr >= start && addr < end).then_some(end) + }) +} + +/// Check whether `addr` falls within any of a loaded ELF object's +/// `PT_LOAD` segments. +/// +/// # Safety +/// `info` must point to a valid `dl_phdr_info` from `dl_iterate_phdr`. +unsafe fn phdr_contains_addr(info: &dl_phdr_info, addr: usize) -> bool { + // SAFETY: caller guarantees `info` is a valid `dl_phdr_info` for a + // currently-loaded ELF object. `dlpi_phnum` is u16, so the + // conversion to usize is lossless. + let phdrs = slice::from_raw_parts(info.dlpi_phdr, info.dlpi_phnum as usize); + let base = u64_to_usize(info.dlpi_addr); + find_containing_load_segment(phdrs, base, addr).is_some() +} + /// Visit each loaded ELF object once. `is_exe` is true only on the /// first callback (the main executable). The callback returns `true` to /// stop iteration. @@ -803,9 +830,30 @@ pub fn is_got_pointer_reloc(r_type: u32) -> bool { /// Null-sized symbols are ignored so hooks resolve to callable definitions. /// /// Uses `gnu_hash_lookup` for objects with `DT_GNU_HASH`, and falls -/// back to a bounded linear dynsym scan for objects that only have -/// `DT_HASH` (sysv). +/// back to `sysv_hash_lookup` for objects that only have `DT_HASH`. pub fn lookup_symbol(name: &str, not_this_symbol: usize) -> Option { + lookup_symbol_impl(name, not_this_symbol, None) +} + +/// Like [`lookup_symbol`], but also skips the library whose `PT_LOAD` +/// segments contain `skip_addr`. +/// +/// Used by [`hook_symbol_excluding_self`] to ensure `orig_out` resolves +/// to the external definition rather than a same-name export from the +/// hook's own library. +fn lookup_symbol_excluding_addr( + name: &str, + not_this_symbol: usize, + skip_addr: usize, +) -> Option { + lookup_symbol_impl(name, not_this_symbol, Some(skip_addr)) +} + +fn lookup_symbol_impl( + name: &str, + not_this_symbol: usize, + skip_addr: Option, +) -> Option { let needle = name.as_bytes(); let mut found: Option = None; // SAFETY: iterate_libraries calls dl_iterate_phdr which guarantees @@ -822,9 +870,17 @@ pub fn lookup_symbol(name: &str, not_this_symbol: usize) -> Option if lib_name.contains("linux-vdso") || lib_name.contains("/ld-linux") { return false; } + // Skip the library containing skip_addr (the hook function). + if let Some(addr) = skip_addr { + if phdr_contains_addr(info, addr) { + return false; + } + } let Some(dyn_info) = DynamicInfo::from_phdr(info) else { return false; }; + // Try GNU hash, then fall back to sysv hash + // for objects that only have DT_HASH. let sym = if dyn_info.has_gnu_hash() { gnu_hash_lookup(&dyn_info, needle) } else if dyn_info.has_sysv_hash() { @@ -875,7 +931,7 @@ pub enum HookError { } /// Hook a single symbol across all loaded ELF objects by patching their -/// GOT entries. +/// GOT entries, including the library that contains the hook function. /// /// This is a one-shot API: it patches every library that is loaded at /// call time. Libraries `dlopen`'d after this call will **not** be @@ -899,12 +955,54 @@ pub enum HookError { /// `hook_fn` must point to a function with the same calling convention /// and signature as the symbol being hooked. The patching is permanent. pub unsafe fn hook_symbol(symbol_name: &CStr, hook_fn: usize) -> Result { + hook_symbol_impl(symbol_name, hook_fn, None) +} + +/// Like [`hook_symbol`], but skips the library that contains `hook_fn`. +/// +/// Use this when the hook function forwards calls to the original via +/// normal linkage (e.g. `libc::read(fd, buf, len)`) rather than through +/// the stored `orig_addr`. By skipping the hook's own library during +/// patching, its GOT entries remain pointed at the real symbol, so +/// normal calls from within the hook don't recurse. +/// +/// This also excludes the hook's library during symbol resolution, so +/// even if it exports the hooked symbol under the same name, `orig_addr` +/// will point to the external definition rather than the hook library's +/// own export. +/// +/// # Safety +/// Same as [`hook_symbol`]. +pub unsafe fn hook_symbol_excluding_self( + symbol_name: &CStr, + hook_fn: usize, +) -> Result { + hook_symbol_impl(symbol_name, hook_fn, Some(hook_fn)) +} + +/// `skip_addr`: if `Some(addr)`, skip the library whose PT_LOAD segments +/// contain `addr`. Used by `hook_symbol_excluding_self` to identify the +/// hook's own library regardless of PIE vs non-PIE (where `dlpi_addr` +/// may be 0 for the main executable). +unsafe fn hook_symbol_impl( + symbol_name: &CStr, + hook_fn: usize, + skip_addr: Option, +) -> Result { let symbol_name_bytes = symbol_name.to_bytes(); let name_str = symbol_name .to_str() .map_err(|_| HookError::InvalidSymbolName)?; - let result = lookup_symbol(name_str, hook_fn).ok_or(HookError::SymbolNotFound)?; + // Resolve the original symbol, excluding both the hook_fn address + // and (if excluding self) the entire hook library so we don't + // accidentally resolve to a different export from the same object. + let result = if let Some(addr) = skip_addr { + lookup_symbol_excluding_addr(name_str, hook_fn, addr) + } else { + lookup_symbol(name_str, hook_fn) + } + .ok_or(HookError::SymbolNotFound)?; let mut entries_patched: usize = 0; let mut entries_failed: usize = 0; @@ -927,9 +1025,16 @@ pub unsafe fn hook_symbol(symbol_name: &CStr, hook_fn: usize) -> Result 0, "should find at least one library"); + assert!( + libs_excluding_self < total_libs, + "excluding self should skip at least one library \ + (total={total_libs}, excluding_self={libs_excluding_self})" + ); + } + /// Sanity check against real loaded libraries: the filter should /// accept some relocations (GOT entries exist) and reject some #[test] @@ -1342,4 +1493,66 @@ mod tests { "expected at least one non-GOT relocation to be filtered out" ); } + + /// Helper to build a minimal `Elf64_Phdr` for testing. + fn make_load_phdr(vaddr: u64, memsz: u64) -> libc::Elf64_Phdr { + libc::Elf64_Phdr { + p_type: PT_LOAD, + p_flags: 0, + p_offset: 0, + p_vaddr: vaddr, + p_paddr: 0, + p_filesz: 0, + p_memsz: memsz, + p_align: 0, + } + } + + #[test] + fn find_containing_load_segment_hit() { + let phdrs = [ + make_load_phdr(0x1000, 0x2000), // [0x1000, 0x3000) + make_load_phdr(0x5000, 0x1000), // [0x5000, 0x6000) + ]; + // Address inside the first segment returns its end. + assert_eq!( + find_containing_load_segment(&phdrs, 0, 0x2000), + Some(0x3000) + ); + // Address inside the second segment. + assert_eq!( + find_containing_load_segment(&phdrs, 0, 0x5000), + Some(0x6000) + ); + } + + #[test] + fn find_containing_load_segment_miss() { + let phdrs = [make_load_phdr(0x1000, 0x2000)]; + // Before the segment. + assert_eq!(find_containing_load_segment(&phdrs, 0, 0x0FFF), None); + // At the end boundary (exclusive). + assert_eq!(find_containing_load_segment(&phdrs, 0, 0x3000), None); + } + + #[test] + fn find_containing_load_segment_with_base() { + let phdrs = [make_load_phdr(0x1000, 0x2000)]; + let base = 0x4000_0000; + // Segment maps to [base + 0x1000, base + 0x3000). + assert_eq!( + find_containing_load_segment(&phdrs, base, base + 0x1000), + Some(base + 0x3000) + ); + // Without base offset, miss. + assert_eq!(find_containing_load_segment(&phdrs, base, 0x1000), None); + } + + #[test] + fn find_containing_load_segment_skips_non_load() { + let mut phdr = make_load_phdr(0x1000, 0x2000); + phdr.p_type = PT_DYNAMIC; // not PT_LOAD + let phdrs = [phdr]; + assert_eq!(find_containing_load_segment(&phdrs, 0, 0x1500), None); + } }