diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index 5fefd8cc4b..6050beb7ca 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -587,6 +587,26 @@ pub fn check_sym(sym: &Elf64_Sym) -> bool { matches!(stt, 0 | 1 | 2 | 10) } +/// Check whether `addr` falls within any of a loaded ELF object's +/// `PT_LOAD` segments. Works regardless of PIE vs non-PIE: on non-PIE +/// executables `dlpi_addr` is 0 but the segments still have the correct +/// absolute virtual addresses once `dlpi_addr` is added. +/// +/// # 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 { + let phdrs = slice::from_raw_parts(info.dlpi_phdr, info.dlpi_phnum as usize); + let base = info.dlpi_addr as usize; + phdrs.iter().any(|p| { + if p.p_type != PT_LOAD { + return false; + } + let start = base + p.p_vaddr as usize; + let end = start + p.p_memsz as usize; + addr >= start && addr < end + }) +} + /// 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 +823,31 @@ 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. Uses segment containment rather than `dlpi_addr` +/// comparison so it works for non-PIE executables (where `dlpi_addr` is 0). +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 +864,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 +925,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 +949,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 +1019,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]