Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 92 additions & 27 deletions libdd-gotter/src/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,45 @@ impl<'a> DynamicInfo<'a> {
jmprels_size = 0;
}

// Validate every raw pointer read from PT_DYNAMIC actually falls within one of this
// object's own PT_LOAD segments before trusting it for dereferencing.
// This is a sanity check to avoid crashing due to odd/corrupted dynamic entries.
let in_bounds = |addr: *const c_void, len: usize| -> bool {
let addr = addr as usize;
match containing_load_segment_end(addr) {
Some(end) => addr.checked_add(len).is_some_and(|limit| limit <= end),
None => false,
}
};
if strtab.is_null() || !in_bounds(strtab as *const c_void, strtab_size) {
strtab = core::ptr::null();
}
if symtab.is_null()
|| !in_bounds(symtab as *const c_void, core::mem::size_of::<Elf64_Sym>())
{
symtab = core::ptr::null();
}
if rels.is_null() || !in_bounds(rels as *const c_void, rels_size) {
rels = core::ptr::null();
rels_size = 0;
}
if relas.is_null() || !in_bounds(relas as *const c_void, relas_size) {
relas = core::ptr::null();
relas_size = 0;
}
if jmprels.is_null() || !in_bounds(jmprels as *const c_void, jmprels_size) {
jmprels = core::ptr::null();
jmprels_size = 0;
}
const GNU_HASH_MIN_BYTES: usize = 4 * core::mem::size_of::<u32>();
if gnu_hash.is_null() || !in_bounds(gnu_hash as *const c_void, GNU_HASH_MIN_BYTES) {
gnu_hash = core::ptr::null();
}
const SYSV_HASH_MIN_BYTES: usize = 2 * core::mem::size_of::<u32>();
if sysv_hash.is_null() || !in_bounds(sysv_hash as *const c_void, SYSV_HASH_MIN_BYTES) {
sysv_hash = core::ptr::null();
}

// Need at minimum strtab + symtab to resolve relocation symbol names.
if strtab.is_null() || symtab.is_null() {
return None;
Expand Down Expand Up @@ -226,6 +265,11 @@ impl<'a> DynamicInfo<'a> {
(sym_count_fallback(symtab, strtab, sysv_hash), 0)
};

let max_syms = containing_load_segment_end(symtab as usize)
.map(|end| end.saturating_sub(symtab as usize) / core::mem::size_of::<Elf64_Sym>())
.unwrap_or(0);
let sym_count = sym_count.min(u32::try_from(max_syms).unwrap_or(u32::MAX));

// SAFETY (applies to all `try_as_slice` calls below):
//
// Each pointer was read from the PT_DYNAMIC segment and
Expand Down Expand Up @@ -614,6 +658,43 @@ unsafe fn phdr_contains_addr(info: &dl_phdr_info, addr: usize) -> bool {
find_containing_load_segment(phdrs, base, addr).is_some()
}

/// Whether a `dl_iterate_phdr` entry is a pseudo-object that has no
/// backing file and isn't relocated by ld.so the way a normal shared
/// library is, so it shouldn't be handed to [`DynamicInfo::from_phdr`] or
/// patched: the vdso (kernel-mapped directly at process creation) or
/// glibc's separate dynamic-linker object.
///
/// The vdso's name varies by libc: glibc reports it as
/// `"linux-vdso.so.1"`, but musl can report an empty name, indistinguishable
/// from `dlpi_name` simply being absent. Checking also for `is_exe` here,
/// as the main executable can also have an empty `dlpi_name`.
///
/// `lib_name` must be `None` only when the name is genuinely absent (null
/// or zero-length), as produced by [`dlpi_name`].
pub fn is_vdso_or_dynamic_linker(lib_name: Option<&str>, is_exe: bool) -> bool {
match lib_name {
None => !is_exe,
Some(name) => name.contains("linux-vdso") || name.contains("/ld-linux"),
}
}

/// Convert a `dl_phdr_info::dlpi_name` to `None` if the pointer is null or the name is empty.
/// This helps to distinguish invalid UTF-8 from empty.
///
/// # Safety
/// `ptr` must be null or point to a valid NUL-terminated C string, valid
/// for the lifetime `'a` the caller assigns to the result.
pub unsafe fn dlpi_name<'a>(ptr: *const c_char) -> Option<std::borrow::Cow<'a, str>> {
if ptr.is_null() {
return None;
}
let cstr = CStr::from_ptr(ptr);
if cstr.to_bytes().is_empty() {
return None;
}
Some(cstr.to_string_lossy())
}

/// 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.
Expand Down Expand Up @@ -861,13 +942,9 @@ fn lookup_symbol_impl(
// mapped library. from_phdr's safety precondition (mapped for 'a)
// is satisfied because the callback runs synchronously under the
// loader lock.
iterate_libraries(|info, _is_exe| unsafe {
let lib_name = if info.dlpi_name.is_null() {
""
} else {
CStr::from_ptr(info.dlpi_name).to_str().unwrap_or("")
};
if lib_name.contains("linux-vdso") || lib_name.contains("/ld-linux") {
iterate_libraries(|info, is_exe| unsafe {
let lib_name = dlpi_name(info.dlpi_name);
if is_vdso_or_dynamic_linker(lib_name.as_deref(), is_exe) {
return false;
}
// Skip the library containing skip_addr (the hook function).
Expand Down Expand Up @@ -1012,17 +1089,11 @@ unsafe fn hook_symbol_impl(
let patched_ptr = &mut entries_patched as *mut usize;
let failed_ptr = &mut entries_failed as *mut usize;

iterate_libraries(|info, _is_exe| {
let lib_name = if info.dlpi_name.is_null() {
""
} else {
// SAFETY: dl_iterate_phdr guarantees dlpi_name is a valid
// NUL-terminated C string for the callback's duration.
unsafe { CStr::from_ptr(info.dlpi_name) }
.to_str()
.unwrap_or("")
};
if lib_name.contains("linux-vdso") || lib_name.contains("/ld-linux") {
iterate_libraries(|info, is_exe| {
// SAFETY: dl_iterate_phdr guarantees dlpi_name is a valid
// NUL-terminated C string for the callback's duration.
let lib_name = unsafe { dlpi_name(info.dlpi_name) };
if is_vdso_or_dynamic_linker(lib_name.as_deref(), is_exe) {
return false;
}

Expand Down Expand Up @@ -1430,15 +1501,9 @@ mod tests {
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") {
iterate_libraries(|info, is_exe| {
let lib_name = unsafe { dlpi_name(info.dlpi_name) };
if is_vdso_or_dynamic_linker(lib_name.as_deref(), is_exe) {
return false;
}
if unsafe { DynamicInfo::from_phdr(info) }.is_none() {
Expand Down
19 changes: 7 additions & 12 deletions libdd-profiling-heap-gotter/src/elf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
//! per-library dedup and dlopen rescan support on top.

use std::collections::HashMap;
use std::ffi::CStr;
use std::sync::atomic::{AtomicUsize, Ordering};

pub use libdd_gotter::lookup_symbol;
use libdd_gotter::{
elf64_r_sym, elf64_r_type, is_got_pointer_reloc, iterate_libraries, DynamicInfo, PageProtGuard,
dlpi_name, elf64_r_sym, elf64_r_type, is_got_pointer_reloc, is_vdso_or_dynamic_linker,
iterate_libraries, DynamicInfo, PageProtGuard,
};

/// Per-library bookkeeping for the GOT re-scan. We never un-patch (see
Expand Down Expand Up @@ -152,21 +152,16 @@ impl SymbolOverrides {
// SAFETY: closure runs synchronously inside dl_iterate_phdr.
let self_ptr = self as *mut Self as usize;
let guard_ptr = &mut guard as *mut PageProtGuard as usize;
iterate_libraries(move |info, _is_exe| unsafe {
iterate_libraries(move |info, is_exe| unsafe {
let this = &mut *(self_ptr as *mut Self);
let g = &mut *(guard_ptr as *mut PageProtGuard);
let lib_name = if info.dlpi_name.is_null() {
String::new()
} else {
CStr::from_ptr(info.dlpi_name)
.to_string_lossy()
.into_owned()
};
if lib_name.contains("linux-vdso") || lib_name.contains("/ld-linux") {
let lib_name = dlpi_name(info.dlpi_name);
if is_vdso_or_dynamic_linker(lib_name.as_deref(), is_exe) {
return false;
}
if let Some(dyn_info) = DynamicInfo::from_phdr(info) {
this.apply_to_library(&dyn_info, lib_name, g);
let owned_name = lib_name.map(|c| c.into_owned()).unwrap_or_default();
this.apply_to_library(&dyn_info, owned_name, g);
}
false
});
Expand Down
Loading