From 31a52149fec9ef37c754544c987ad3a4c17dcfeb Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Tue, 4 Aug 2026 13:32:44 +0000 Subject: [PATCH 1/5] Add fn to hook but exclude self --- libdd-gotter/src/elf.rs | 54 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index deb5f09052..c800586275 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -856,7 +856,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 @@ -882,13 +882,55 @@ pub enum HookError { 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; @@ -911,6 +953,14 @@ pub unsafe fn hook_symbol( 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; + } + } + // SAFETY: `info` points to a valid `dl_phdr_info` provided by // `dl_iterate_phdr`; the library is mapped for the callback's // duration. From 3e90e9c2ba413f9c6f30e6ea88fac7f620fb6414 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Tue, 4 Aug 2026 15:23:06 +0000 Subject: [PATCH 2/5] Test that skip works --- libdd-gotter/src/elf.rs | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index c800586275..dcba913e5c 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -1342,6 +1342,58 @@ mod tests { assert!(!is_got_pointer_reloc(u32::MAX)); } + /// Verify that `hook_symbol_excluding_self` skips the library + /// containing the hook function. We use `dladdr` on the hook to + /// find our own base address, then confirm `hook_symbol_impl` with + /// `skip_base = Some(our_base)` produces fewer patched entries than + /// without skipping. + #[test] + #[cfg_attr(miri, ignore)] + fn test_hook_symbol_excluding_self_skips_own_library() { + // Use a dummy hook function defined in this test binary. + unsafe extern "C" fn dummy_hook() {} + let hook_addr = dummy_hook as *const () as usize; + + // Resolve our own base address + let mut dl_info: libc::Dl_info = unsafe { core::mem::zeroed() }; + let have_self = unsafe { libc::dladdr(hook_addr as *const c_void, &mut dl_info) } != 0; + assert!(have_self, "dladdr should resolve our own hook function"); + let self_base = dl_info.dli_fbase as usize; + + // Count how many libraries would be visited with and without + // the self-skip. + let mut total_libs = 0usize; + let mut libs_excluding_self = 0usize; + + iterate_libraries(|info, _| { + let lib_name = if info.dlpi_name.is_null() { + "" + } else { + unsafe { CStr::from_ptr(info.dlpi_name) } + .to_str() + .unwrap_or("") + }; + if lib_name.contains("linux-vdso") || lib_name.contains("/ld-linux") { + return false; + } + if unsafe { DynamicInfo::from_phdr(info) }.is_none() { + return false; + } + total_libs += 1; + if info.dlpi_addr as usize != self_base { + libs_excluding_self += 1; + } + false + }); + + assert!(total_libs > 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] From ebe10bf04f355be38a5e7f31317e14a12d705621 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Tue, 4 Aug 2026 15:52:49 +0000 Subject: [PATCH 3/5] exclude self while resolving original --- libdd-gotter/src/elf.rs | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index dcba913e5c..a340401d8b 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -785,9 +785,28 @@ 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 any definition from the +/// library at `skip_base` (its `dlpi_addr`). Used by +/// [`hook_symbol_excluding_self`] to avoid resolving the original +/// from the hook's own library. +fn lookup_symbol_excluding_base( + name: &str, + not_this_symbol: usize, + skip_base: usize, +) -> Option { + lookup_symbol_impl(name, not_this_symbol, Some(skip_base)) +} + +fn lookup_symbol_impl( + name: &str, + not_this_symbol: usize, + skip_base: Option, +) -> Option { let needle = name.as_bytes(); let mut found: Option = None; // SAFETY: the callback runs synchronously inside dl_iterate_phdr; @@ -803,9 +822,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 hook's own library during resolution if requested. + if let Some(base) = skip_base { + if info.dlpi_addr as usize == base { + 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() { @@ -962,8 +989,7 @@ unsafe fn hook_symbol_impl( } // SAFETY: `info` points to a valid `dl_phdr_info` provided by - // `dl_iterate_phdr`; the library is mapped for the callback's - // duration. + // `dl_iterate_phdr` let Some(dyn_info) = (unsafe { DynamicInfo::from_phdr(info) }) else { return false; }; From 1fb492bc319db11bd88dd67534ae9afa2a8b7a52 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Wed, 5 Aug 2026 19:35:15 +0000 Subject: [PATCH 4/5] Add some more detail and context on these APIs --- libdd-gotter/src/elf.rs | 42 +++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index a340401d8b..6cc71110e0 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -569,6 +569,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. @@ -791,21 +811,23 @@ pub fn lookup_symbol(name: &str, not_this_symbol: usize) -> Option } /// Like [`lookup_symbol`], but also skips any definition from the -/// library at `skip_base` (its `dlpi_addr`). Used by -/// [`hook_symbol_excluding_self`] to avoid resolving the original -/// from the hook's own library. -fn lookup_symbol_excluding_base( +/// library at `skip_base` (its `dlpi_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_base: usize, + skip_addr: usize, ) -> Option { - lookup_symbol_impl(name, not_this_symbol, Some(skip_base)) + lookup_symbol_impl(name, not_this_symbol, Some(skip_addr)) } fn lookup_symbol_impl( name: &str, not_this_symbol: usize, - skip_base: Option, + skip_addr: Option, ) -> Option { let needle = name.as_bytes(); let mut found: Option = None; @@ -822,9 +844,9 @@ fn lookup_symbol_impl( if lib_name.contains("linux-vdso") || lib_name.contains("/ld-linux") { return false; } - // Skip the hook's own library during resolution if requested. - if let Some(base) = skip_base { - if info.dlpi_addr as usize == base { + // Skip the library containing skip_addr (the hook function). + if let Some(addr) = skip_addr { + if phdr_contains_addr(info, addr) { return false; } } From 45342c8d20cbceb2c1c3206d229b46b628741055 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Wed, 5 Aug 2026 19:45:27 +0000 Subject: [PATCH 5/5] non PIE executable support --- libdd-gotter/src/elf.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index 6cc71110e0..88f2b45f5e 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -810,12 +810,13 @@ 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 any definition from the -/// library at `skip_base` (its `dlpi_addr`). +/// 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. +/// 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, @@ -1391,10 +1392,8 @@ mod tests { } /// Verify that `hook_symbol_excluding_self` skips the library - /// containing the hook function. We use `dladdr` on the hook to - /// find our own base address, then confirm `hook_symbol_impl` with - /// `skip_base = Some(our_base)` produces fewer patched entries than - /// without skipping. + /// containing the hook function. Uses `phdr_contains_addr` to identify + /// the hook's own library. #[test] #[cfg_attr(miri, ignore)] fn test_hook_symbol_excluding_self_skips_own_library() { @@ -1402,12 +1401,6 @@ mod tests { unsafe extern "C" fn dummy_hook() {} let hook_addr = dummy_hook as *const () as usize; - // Resolve our own base address - let mut dl_info: libc::Dl_info = unsafe { core::mem::zeroed() }; - let have_self = unsafe { libc::dladdr(hook_addr as *const c_void, &mut dl_info) } != 0; - assert!(have_self, "dladdr should resolve our own hook function"); - let self_base = dl_info.dli_fbase as usize; - // Count how many libraries would be visited with and without // the self-skip. let mut total_libs = 0usize; @@ -1428,7 +1421,7 @@ mod tests { return false; } total_libs += 1; - if info.dlpi_addr as usize != self_base { + if !unsafe { phdr_contains_addr(info, hook_addr) } { libs_excluding_self += 1; } false