From 9d965e607d7e5ba18e6032f4ea5fa6b1a95d341c Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Wed, 29 Jul 2026 12:48:59 +0000 Subject: [PATCH 1/5] refactor: extract shared GOT-patching primitives into libdd-got-hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the ELF parsing, dl_iterate_phdr iteration, PageProtGuard, gnu_hash_symbol_count, gnu_hash_lookup, lookup_symbol, and related utilities out of libdd-profiling-heap-gotter into a new libdd-got-hook crate. libdd-profiling-heap-gotter now depends on libdd-got-hook and keeps only the SymbolOverrides multi-symbol registry and per-library dedup/rescan logic. This is a pure code move — no behavioral changes. --- .github/CODEOWNERS | 5 +- Cargo.lock | 1 + Cargo.toml | 1 + datadog-ffe-test-suite/ffe-system-test-data | 1 + libdd-got-hook/Cargo.toml | 18 + libdd-got-hook/src/elf.rs | 601 ++++++++++++++++++++ libdd-got-hook/src/lib.rs | 20 + libdd-profiling-heap-gotter/Cargo.toml | 1 + libdd-profiling-heap-gotter/src/elf.rs | 574 +------------------ tools/docker/Dockerfile.build | 1 + 10 files changed, 673 insertions(+), 550 deletions(-) create mode 160000 datadog-ffe-test-suite/ffe-system-test-data create mode 100644 libdd-got-hook/Cargo.toml create mode 100644 libdd-got-hook/src/elf.rs create mode 100644 libdd-got-hook/src/lib.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d6c4b61d22..3390783a81 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -43,6 +43,7 @@ deny.toml @DataDog/libdatadog docker-bake.hcl @DataDog/apm-common-components-core docs @DataDog/libdatadog examples @DataDog/libdatadog +libdd-agent-client @DataDog/apm-common-components-core libdd-alloc/ @DataDog/libdatadog-profiling libdd-capabilities*/ @DataDog/apm-common-components-core libdd-common*/ @DataDog/libdatadog @@ -50,14 +51,14 @@ libdd-crashtracker*/ @DataDog/libdatadog-profiling libdd-data-pipeline*/ @DataDog/libdatadog-apm libdd-ddsketch*/ @DataDog/libdatadog-apm @DataDog/apm-common-components-core libdd-dogstatsd-client @DataDog/apm-common-components-core -libdd-profiling-heap-*/ @DataDog/libdatadog-profiling +libdd-got-hook/ @DataDog/libdatadog @DataDog/libdatadog-profiling libdd-http-client @DataDog/apm-common-components-core -libdd-agent-client @DataDog/apm-common-components-core libdd-library-config*/ @DataDog/apm-sdk-capabilities-rust libdd-log*/ @DataDog/apm-common-components-core libdd-otel-thread-ctx/ @DataDog/apm-common-components-core libdd-otel-thread-ctx-ffi/ @DataDog/apm-common-components-core libdd-profiling*/ @DataDog/libdatadog-profiling +libdd-profiling-heap-*/ @DataDog/libdatadog-profiling libdd-sampling/ @DataDog/apm-common-components-core libdd-shared-runtime*/ @DataDog/apm-common-components-core libdd-telemetry*/ @DataDog/apm-common-components-core diff --git a/Cargo.lock b/Cargo.lock index 733d170ecf..bf31960d79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3299,6 +3299,7 @@ name = "libdd-profiling-heap-gotter" version = "1.0.0" dependencies = [ "libc", + "libdd-got-hook", "libdd-profiling-heap-sampler", "serial_test", ] diff --git a/Cargo.toml b/Cargo.toml index fe8c6823f4..181a6160d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "libdd-alloc", "libdd-profiling-heap-sampler", "libdd-profiling-heap-allocator", + "libdd-got-hook", "libdd-profiling-heap-gotter", "libdd-profiling-heap-gotter-ffi", "libdd-crashtracker", diff --git a/datadog-ffe-test-suite/ffe-system-test-data b/datadog-ffe-test-suite/ffe-system-test-data new file mode 160000 index 0000000000..d9d8020045 --- /dev/null +++ b/datadog-ffe-test-suite/ffe-system-test-data @@ -0,0 +1 @@ +Subproject commit d9d8020045c7cf373f37e3e7e0122b34709f1a07 diff --git a/libdd-got-hook/Cargo.toml b/libdd-got-hook/Cargo.toml new file mode 100644 index 0000000000..c72c1e99cc --- /dev/null +++ b/libdd-got-hook/Cargo.toml @@ -0,0 +1,18 @@ +# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "libdd-got-hook" +version = "1.0.0" +description = "ELF GOT-patching primitives for runtime function interposition on 64-bit Linux." +homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-got-hook" +repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-got-hook" +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[lib] +bench = false + +[dependencies] +libc = "0.2" diff --git a/libdd-got-hook/src/elf.rs b/libdd-got-hook/src/elf.rs new file mode 100644 index 0000000000..e5b532be64 --- /dev/null +++ b/libdd-got-hook/src/elf.rs @@ -0,0 +1,601 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! GOT-table interposition primitives. +//! +//! Scope: +//! * 64-bit Linux ELF only (`Elf64_*`). +//! * GNU hash tables only (`DT_GNU_HASH`). `DT_HASH` is not parsed; objects without a GNU hash +//! table are skipped. +//! * REL / RELA / JMPREL relocation arrays. + +use core::ffi::{c_char, c_int, c_void, CStr}; +use std::collections::HashMap; +use std::io::{BufRead, BufReader}; + +use libc::{ + dl_iterate_phdr, dl_phdr_info, mprotect, sysconf, Elf64_Rel, Elf64_Rela, Elf64_Sym, + _SC_PAGESIZE, PROT_EXEC, PROT_READ, PROT_WRITE, PT_DYNAMIC, PT_LOAD, +}; + +// ELF dynamic-section tags. The `libc` crate doesn't export these +// (they're processor-independent ELF spec constants). Values from ``. +#[allow(non_camel_case_types)] +#[repr(C)] +struct Elf64_Dyn { + d_tag: i64, + d_un: u64, // d_val / d_ptr union; we only ever read it as u64 +} +const DT_NULL: i64 = 0; +const DT_STRTAB: i64 = 5; +const DT_SYMTAB: i64 = 6; +const DT_RELA: i64 = 7; +const DT_RELASZ: i64 = 8; +const DT_STRSZ: i64 = 10; +const DT_REL: i64 = 17; +const DT_RELSZ: i64 = 18; +const DT_PLTREL: i64 = 20; +const DT_JMPREL: i64 = 23; +const DT_PLTRELSZ: i64 = 2; +const DT_GNU_HASH: i64 = 0x6fff_fef5; +const STN_UNDEF: u32 = 0; + +/// The subset of an ELF object's `PT_DYNAMIC` entries needed to find +/// and rewrite GOT entries. +pub struct DynamicInfo { + strtab: *const c_char, + strtab_size: usize, + symtab: *const Elf64_Sym, + sym_count: u32, + gnu_hash: *const u32, + gnu_hash_words: usize, + rels: *const Elf64_Rel, + rels_count: usize, + relas: *const Elf64_Rela, + relas_count: usize, + jmprels: *const Elf64_Rela, + jmprels_count: usize, + base_address: usize, +} + +impl DynamicInfo { + /// Read DT_* entries out of a PT_DYNAMIC array. + /// + /// Handles the glibc-vs-musl quirk where glibc stores absolute + /// addresses in DT entries while musl stores load-relative offsets; + /// we use the `addr > base ? addr : base + addr` heuristic. + /// + /// # Safety + /// `info` must point to a valid `dl_phdr_info` from `dl_iterate_phdr`. + pub unsafe fn from_phdr(info: &dl_phdr_info) -> Option { + let phdrs = core::slice::from_raw_parts(info.dlpi_phdr, info.dlpi_phnum as usize); + let dyn_phdr = phdrs.iter().find(|p| p.p_type == PT_DYNAMIC)?; + let dyn_begin = (info.dlpi_addr as usize + dyn_phdr.p_vaddr as usize) as *const Elf64_Dyn; + let base = info.dlpi_addr as usize; + 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(p.p_vaddr as usize)?; + let end = start.checked_add(p.p_memsz as usize)?; + (addr >= start && addr < end).then_some(end) + }) + }; + let correct = |a: u64| -> usize { + let a = a as usize; + if a > base { + a + } else { + base + a + } + }; + + let mut strtab: *const c_char = core::ptr::null(); + let mut strtab_size: usize = 0; + let mut symtab: *const Elf64_Sym = core::ptr::null(); + let mut rels: *const Elf64_Rel = core::ptr::null(); + let mut rels_size: usize = 0; + let mut relas: *const Elf64_Rela = core::ptr::null(); + let mut relas_size: usize = 0; + let mut jmprels: *const Elf64_Rela = core::ptr::null(); + let mut jmprels_size: usize = 0; + let mut gnu_hash: *const u32 = core::ptr::null(); + let mut pltrel_type: i64 = 0; + + let mut it = dyn_begin; + loop { + let d = &*it; + if d.d_tag == DT_NULL { + break; + } + let v = d.d_un; + match d.d_tag { + DT_STRTAB => strtab = correct(v) as *const c_char, + DT_STRSZ => strtab_size = v as usize, + DT_SYMTAB => symtab = correct(v) as *const Elf64_Sym, + DT_GNU_HASH => gnu_hash = correct(v) as *const u32, + DT_REL => rels = correct(v) as *const Elf64_Rel, + DT_RELA => relas = correct(v) as *const Elf64_Rela, + DT_JMPREL => jmprels = correct(v) as *const Elf64_Rela, + DT_RELSZ => rels_size = v as usize, + DT_RELASZ => relas_size = v as usize, + DT_PLTRELSZ => jmprels_size = v as usize, + DT_PLTREL => pltrel_type = v as i64, + _ => {} + } + it = it.add(1); + } + + // JMPREL entries are RELA only if DT_PLTREL says so. + if pltrel_type != DT_RELA { + jmprels = core::ptr::null(); + jmprels_size = 0; + } + + if strtab.is_null() || symtab.is_null() || gnu_hash.is_null() { + return None; + } + + let gnu_hash_addr = gnu_hash as usize; + let end = containing_load_segment_end(gnu_hash_addr)?; + let bytes = end.checked_sub(gnu_hash_addr)?; + let gnu_hash_words = bytes / core::mem::size_of::(); + let sym_count = gnu_hash_symbol_count(gnu_hash, gnu_hash_words).unwrap_or_else(|| { + // Fallback for degenerate .gnu.hash (e.g. executables with only + // undefined imports): estimate dynsym entry count from the common + // .dynsym-before-.dynstr layout. This is a heuristic, not an ELF + // guarantee. If it underestimates we may skip patching some + // relocations; valid relocation indexes should still keep an + // overestimate from faulting on normal loaded objects. + let symtab_addr = symtab as usize; + let strtab_addr = strtab as usize; + if strtab_addr > symtab_addr { + let bytes = strtab_addr - symtab_addr; + (bytes / core::mem::size_of::()) as u32 + } else { + // Can't estimate; allow any index and rely on strtab + // bounds checking in sym_name to catch bad accesses. + u32::MAX + } + }); + + Some(Self { + strtab, + strtab_size, + symtab, + sym_count, + gnu_hash, + gnu_hash_words, + rels, + rels_count: rels_size / core::mem::size_of::(), + relas, + relas_count: relas_size / core::mem::size_of::(), + jmprels, + jmprels_count: jmprels_size / core::mem::size_of::(), + base_address: base, + }) + } + + /// Look up the name of the symbol at index `idx` in the dynamic + /// string table. + /// + /// # Safety + /// The `DynamicInfo` must have been produced by [`DynamicInfo::from_phdr`] + /// for a currently-loaded ELF object whose symtab/strtab are still mapped. + pub unsafe fn sym_name(&self, idx: u32) -> Option<&CStr> { + if (idx as usize) >= self.sym_count as usize { + return None; + } + let sym = &*self.symtab.add(idx as usize); + let off = sym.st_name as usize; + if off >= self.strtab_size { + return None; + } + Some(CStr::from_ptr(self.strtab.add(off))) + } + + /// The base load address of this ELF object. + pub fn base_address(&self) -> usize { + self.base_address + } + + /// Access to REL relocations (pointer, count). + pub fn rels(&self) -> (*const Elf64_Rel, usize) { + (self.rels, self.rels_count) + } + + /// Access to RELA relocations (pointer, count). + pub fn relas(&self) -> (*const Elf64_Rela, usize) { + (self.relas, self.relas_count) + } + + /// Access to JMPREL (PLT) relocations (pointer, count). + pub fn jmprels(&self) -> (*const Elf64_Rela, usize) { + (self.jmprels, self.jmprels_count) + } +} + +/// Compute the GNU symbol hash used by `DT_GNU_HASH` tables. +/// See . +pub fn gnu_hash(name: &[u8]) -> u32 { + let mut h: u32 = 5381; + for c in name { + h = h.wrapping_shl(5).wrapping_add(h).wrapping_add(*c as u32); + } + h +} + +/// Compute the total number of entries in `.dynsym` from the `.gnu.hash` +/// table. +/// +/// Returns `None` when the table is structurally invalid or degenerate +/// (all buckets empty). +/// +/// # Safety +/// `hashtab` must point to a valid `.gnu.hash` section of at least +/// `hashtab_words` u32 entries in mapped memory. +pub unsafe fn gnu_hash_symbol_count(hashtab: *const u32, hashtab_words: usize) -> Option { + if hashtab_words < 4 { + return None; + } + + let nbuckets = *hashtab; + let symbias = *hashtab.add(1); + let bloom_size = *hashtab.add(2); + let bloom_size_words = (bloom_size as usize).checked_mul(2)?; + let buckets_start = 4usize.checked_add(bloom_size_words)?; + let chains_start = buckets_start.checked_add(nbuckets as usize)?; + + if bloom_size == 0 || buckets_start > hashtab_words || chains_start > hashtab_words { + return None; + } + if nbuckets == 0 { + return None; + } + + let buckets = core::slice::from_raw_parts(hashtab.add(buckets_start), nbuckets as usize); + let mut idx = *buckets.iter().max()?; + if idx == STN_UNDEF { + return None; + } + if idx < symbias { + return None; + } + + let chain_count = hashtab_words - chains_start; + loop { + let chain_idx = (idx - symbias) as usize; + if chain_idx >= chain_count { + return None; + } + if *hashtab.add(chains_start + chain_idx) & 1 != 0 { + return idx.checked_add(1); + } + idx = idx.checked_add(1)?; + } +} + +/// Look up a symbol by name in an object's `.gnu.hash` table. +/// +/// Returns the `Elf64_Sym` entry if found and valid (non-zero value, +/// function/object/notype binding). +/// +/// # Safety +/// `info` must have been produced by [`DynamicInfo::from_phdr`] for a +/// currently-loaded ELF object. +pub unsafe fn gnu_hash_lookup(info: &DynamicInfo, name: &[u8]) -> Option { + let hashtab = info.gnu_hash; + if hashtab.is_null() || info.gnu_hash_words < 4 { + return None; + } + + let nbuckets = *hashtab; + let symbias = *hashtab.add(1); + let bloom_size = *hashtab.add(2); + let bloom_shift = *hashtab.add(3); + let bloom_size_words = (bloom_size as usize).checked_mul(2)?; + let buckets_start = 4usize.checked_add(bloom_size_words)?; + let chains_start = buckets_start.checked_add(nbuckets as usize)?; + + if nbuckets == 0 + || bloom_size == 0 + || buckets_start > info.gnu_hash_words + || chains_start > info.gnu_hash_words + { + return None; + } + + let h = gnu_hash(name); + let bloom = hashtab.add(4) as *const u64; + let word = *bloom.add(((h / 64) & (bloom_size - 1)) as usize); + let bit1 = h & 63; + let bit2 = (h >> bloom_shift) & 63; + if ((word >> bit1) & (word >> bit2) & 1) == 0 { + return None; + } + + let buckets = hashtab.add(buckets_start); + let mut symidx = *buckets.add((h % nbuckets) as usize); + if symidx == STN_UNDEF { + return None; + } + if symidx < symbias { + return None; + } + + let chain_count = info.gnu_hash_words - chains_start; + loop { + let chain_idx = (symidx - symbias) as usize; + if chain_idx >= chain_count { + return None; + } + let chain_h = *hashtab.add(chains_start + chain_idx); + if ((chain_h ^ h) >> 1) == 0 { + if let Some(sname) = info.sym_name(symidx) { + let sym = info.symtab.add(symidx as usize); + if sname.to_bytes() == name && check_sym(&*sym) { + return Some(*sym); + } + } + } + if chain_h & 1 != 0 { + break; + } + symidx = symidx.checked_add(1)?; + } + None +} + +/// Return whether this is a defining function/object/notype symbol. +pub fn check_sym(sym: &Elf64_Sym) -> bool { + const SHN_ABS: u16 = 0xfff1; + let stt = sym.st_info & 0xf; + (sym.st_value != 0 || sym.st_shndx == SHN_ABS) && + // STT_NOTYPE(0), STT_OBJECT(1), STT_FUNC(2), STT_GNU_IFUNC(10) + matches!(stt, 0 | 1 | 2 | 10) +} + +/// 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. +pub fn iterate_libraries(mut callback: impl FnMut(&dl_phdr_info, bool) -> bool) { + struct Ctx<'a> { + callback: &'a mut dyn FnMut(&dl_phdr_info, bool) -> bool, + is_first: bool, + } + let mut ctx = Ctx { + callback: &mut callback, + is_first: true, + }; + + unsafe extern "C" fn trampoline( + info: *mut dl_phdr_info, + _size: libc::size_t, + data: *mut c_void, + ) -> c_int { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let ctx = &mut *(data as *mut Ctx); + let is_exe = ctx.is_first; + ctx.is_first = false; + (ctx.callback)(&*info, is_exe) + })); + + // Never unwind a Rust panic through libc's dl_iterate_phdr callback. + result.map(i32::from).unwrap_or(1) + } + + unsafe { + dl_iterate_phdr(Some(trampoline), &mut ctx as *mut _ as *mut c_void); + } +} + +/// A single /proc/self/maps entry: address range + current protection flags. +#[derive(Clone, Copy)] +pub struct MapEntry { + pub start: usize, + pub end: usize, + pub prot: i32, +} + +/// Parse /proc/self/maps into a list of (range, prot) entries. +/// +/// Used to remember each GOT page's original protection so we can restore +/// it after patching, rather than leaving Full-RELRO pages read-write for +/// the lifetime of the process. +pub fn read_proc_maps() -> Vec { + let Ok(f) = std::fs::File::open("/proc/self/maps") else { + return Vec::new(); + }; + let mut out = Vec::new(); + for line in BufReader::new(f).lines().map_while(Result::ok) { + let mut parts = line.split_whitespace(); + let Some(range) = parts.next() else { continue }; + let Some(perms) = parts.next() else { continue }; + let Some(dash) = range.find('-') else { + continue; + }; + let Ok(start) = usize::from_str_radix(&range[..dash], 16) else { + continue; + }; + let Ok(end) = usize::from_str_radix(&range[dash + 1..], 16) else { + continue; + }; + let b = perms.as_bytes(); + let mut prot = 0; + if b.first() == Some(&b'r') { + prot |= PROT_READ; + } + if b.get(1) == Some(&b'w') { + prot |= PROT_WRITE; + } + if b.get(2) == Some(&b'x') { + prot |= PROT_EXEC; + } + out.push(MapEntry { start, end, prot }); + } + out +} + +/// Batched GOT-entry patcher that remembers each touched page's +/// original protection and restores it at the end of a patching pass. +/// +/// On Full-RELRO binaries, GOT pages start read-only. This guard +/// mprotects each unique page once (RW), lets the caller write as +/// many entries as it needs, then mprotects each page back to what +/// `/proc/self/maps` reported at guard-construction time when it is +/// dropped (including on panic or early return). +pub struct PageProtGuard { + page_size: usize, + maps: Vec, + // Aligned page base -> original prot flags read from /proc/self/maps. + touched: HashMap, +} + +impl PageProtGuard { + pub fn new() -> Self { + // sysconf can return -1 on error; fall back to a conservative + // 4 KiB default if the query fails. + let raw = unsafe { sysconf(_SC_PAGESIZE) }; + let page_size = usize::try_from(raw).unwrap_or(4096); + Self { + page_size, + maps: read_proc_maps(), + touched: HashMap::new(), + } + } + + pub fn original_prot(&self, addr: usize) -> Option { + self.maps + .iter() + .find(|m| addr >= m.start && addr < m.end) + .map(|m| m.prot) + } + + /// Make the containing page writable if it isn't already touched, + /// then replace one GOT entry. + /// + /// # Safety + /// `addr` must point to a valid GOT slot in mapped memory. + pub unsafe fn override_entry(&mut self, addr: usize, new_value: usize) -> bool { + let aligned = addr & !(self.page_size - 1); + if !self.touched.contains_key(&aligned) { + // If /proc/self/maps isn't available (or the page isn't in + // it, which shouldn't happen for a mapped GOT page) fall + // back to PROT_READ - the RELRO'd default. That's tighter + // than the previous behavior of leaving pages RW. + let orig = self.original_prot(aligned).unwrap_or(PROT_READ); + if mprotect( + aligned as *mut c_void, + self.page_size, + PROT_READ | PROT_WRITE, + ) != 0 + { + return false; + } + self.touched.insert(aligned, orig); + } + core::ptr::write_unaligned(addr as *mut usize, new_value); + true + } +} + +impl Default for PageProtGuard { + fn default() -> Self { + Self::new() + } +} + +impl Drop for PageProtGuard { + /// Restore every touched page to its original protection. + fn drop(&mut self) { + for (aligned, orig) in self.touched.drain() { + unsafe { mprotect(aligned as *mut c_void, self.page_size, orig) }; + } + } +} + +/// Extract the symbol index from an ELF64 relocation's `r_info` field. +pub fn elf64_r_sym(info: u64) -> u64 { + info >> 32 +} + +/// Result of a symbol lookup. +#[derive(Clone, Copy)] +pub struct LookupResult { + pub address: usize, +} + +/// Look up a symbol across loaded objects, returning the first +/// non-zero-sized definition whose address is not `not_this_symbol`. +/// Null-sized symbols are ignored so hooks resolve to callable definitions. +pub fn lookup_symbol(name: &str, not_this_symbol: usize) -> Option { + let needle = name.as_bytes(); + let mut found: Option = None; + 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") { + return false; + } + let Some(dyn_info) = DynamicInfo::from_phdr(info) else { + return false; + }; + if let Some(sym) = gnu_hash_lookup(&dyn_info, needle) { + if sym.st_size > 0 { + let addr = sym.st_value as usize + dyn_info.base_address(); + if addr != not_this_symbol { + found = Some(LookupResult { address: addr }); + return true; + } + } + } + false + }); + found +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn page_prot_guard_finds_original_mapping_protection() { + let guard = PageProtGuard { + page_size: 4096, + maps: vec![ + MapEntry { + start: 0x1000, + end: 0x2000, + prot: PROT_READ, + }, + MapEntry { + start: 0x2000, + end: 0x3000, + prot: PROT_READ | PROT_EXEC, + }, + ], + touched: HashMap::new(), + }; + + assert_eq!(guard.original_prot(0x1000), Some(PROT_READ)); + assert_eq!(guard.original_prot(0x1fff), Some(PROT_READ)); + assert_eq!(guard.original_prot(0x2000), Some(PROT_READ | PROT_EXEC)); + assert_eq!(guard.original_prot(0x3000), None); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn test_can_lookup_malloc() { + let r = lookup_symbol("malloc", 0); + assert!(r.is_some(), "expected to find malloc in loaded libraries"); + assert!(r.unwrap().address != 0); + } + + #[test] + #[cfg_attr(miri, ignore)] + fn test_unknown_symbol_lookup_returns_none() { + let r = lookup_symbol("definitely_not_a_real_libc_symbol_xyzzy", 0); + assert!(r.is_none()); + } +} diff --git a/libdd-got-hook/src/lib.rs b/libdd-got-hook/src/lib.rs new file mode 100644 index 0000000000..a7e8637260 --- /dev/null +++ b/libdd-got-hook/src/lib.rs @@ -0,0 +1,20 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! ELF GOT-patching primitives for runtime function interposition. +//! +//! Walks each loaded ELF object via `dl_iterate_phdr`, parses its +//! `PT_DYNAMIC` for the symbol/string/hash tables and the relocation +//! arrays, and provides utilities to rewrite GOT entries. +//! +//! Scope: +//! * 64-bit Linux ELF only (`Elf64_*`). Other targets are compile-time gated. +//! * `DT_GNU_HASH` for determining dynsym entry count, with a symtab/strtab distance heuristic +//! fallback. +//! * REL / RELA / JMPREL relocation arrays. + +#[cfg(all(target_os = "linux", target_pointer_width = "64"))] +mod elf; + +#[cfg(all(target_os = "linux", target_pointer_width = "64"))] +pub use elf::*; diff --git a/libdd-profiling-heap-gotter/Cargo.toml b/libdd-profiling-heap-gotter/Cargo.toml index 535566fdd1..d28be1a031 100644 --- a/libdd-profiling-heap-gotter/Cargo.toml +++ b/libdd-profiling-heap-gotter/Cargo.toml @@ -25,6 +25,7 @@ test-support = [] [target.'cfg(target_os = "linux")'.dependencies] libc.workspace = true +libdd-got-hook = { version = "1.0.0", path = "../libdd-got-hook" } libdd-profiling-heap-sampler = { version = "1.0.0", path = "../libdd-profiling-heap-sampler" } [target.'cfg(target_os = "linux")'.dev-dependencies] diff --git a/libdd-profiling-heap-gotter/src/elf.rs b/libdd-profiling-heap-gotter/src/elf.rs index a09aaff407..7f814d42a1 100644 --- a/libdd-profiling-heap-gotter/src/elf.rs +++ b/libdd-profiling-heap-gotter/src/elf.rs @@ -1,525 +1,18 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -//! GOT-table interposition primitives. +//! GOT-table interposition for heap profiling. //! -//! Port of ddprof's `src/lib/elfutils.cc` + the parts of -//! `symbol_overrides.cc` that drive it. Walks each loaded ELF object via -//! `dl_iterate_phdr`, parses its `PT_DYNAMIC` for the symbol/string/hash -//! tables and the relocation arrays, and rewrites GOT entries whose -//! symbol name is in the override map. Records the previous values so -//! the overrides can be reverted. -//! -//! Scope: -//! * 64-bit ELF only (`Elf64_*`). Other targets are gated out at compile time via `#[cfg]` on the -//! parent module. -//! * GNU hash tables only - `DT_HASH` is skipped because it has caused problems on older glibc -//! systems. -//! * REL / RELA / JMPREL relocation arrays. +//! Uses the shared ELF primitives from `libdd-got-hook` for parsing and +//! patching, and adds the multi-symbol `SymbolOverrides` registry with +//! per-library dedup and dlopen rescan support on top. -use core::ffi::{c_char, c_int, c_void}; use std::collections::HashMap; use std::ffi::CStr; - -use libc::{ - dl_iterate_phdr, dl_phdr_info, mprotect, sysconf, Elf64_Rel, Elf64_Rela, Elf64_Sym, - _SC_PAGESIZE, PROT_EXEC, PROT_READ, PROT_WRITE, PT_DYNAMIC, PT_LOAD, -}; -use std::io::{BufRead, BufReader}; use std::sync::atomic::{AtomicUsize, Ordering}; -// ELF dynamic-section tags and friends. The `libc` crate doesn't export -// these (they're processor-independent ELF spec constants), so we name -// them locally. Values come from ``. -#[allow(non_camel_case_types)] -#[repr(C)] -struct Elf64_Dyn { - d_tag: i64, - d_un: u64, // d_val / d_ptr union; we only ever read it as u64 -} -const DT_NULL: i64 = 0; -const DT_STRTAB: i64 = 5; -const DT_SYMTAB: i64 = 6; -const DT_RELA: i64 = 7; -const DT_RELASZ: i64 = 8; -const DT_STRSZ: i64 = 10; -const DT_REL: i64 = 17; -const DT_RELSZ: i64 = 18; -const DT_PLTREL: i64 = 20; -const DT_JMPREL: i64 = 23; -const DT_PLTRELSZ: i64 = 2; -const DT_GNU_HASH: i64 = 0x6fff_fef5; -const STN_UNDEF: u32 = 0; - -/// The subset of an ELF object's `PT_DYNAMIC` entries needed to find and rewrite GOT entries. -struct DynamicInfo { - strtab: *const c_char, - strtab_size: usize, - symtab: *const Elf64_Sym, - sym_count: u32, - rels: *const Elf64_Rel, - rels_count: usize, - relas: *const Elf64_Rela, - relas_count: usize, - jmprels: *const Elf64_Rela, - jmprels_count: usize, - gnu_hash: *const u32, - gnu_hash_words: usize, - base_address: usize, -} - -impl DynamicInfo { - /// Read DT_* entries out of a PT_DYNAMIC array. Handles the - /// glibc-vs-musl quirk where glibc stores absolute addresses in DT - /// entries while musl stores load-relative offsets; we use the - /// `addr > base ? addr : base + addr` heuristic. - unsafe fn from_phdr(info: &dl_phdr_info) -> Option { - let phdrs = std::slice::from_raw_parts(info.dlpi_phdr, info.dlpi_phnum as usize); - let dyn_phdr = phdrs.iter().find(|p| p.p_type == PT_DYNAMIC)?; - let dyn_begin = (info.dlpi_addr as usize + dyn_phdr.p_vaddr as usize) as *const Elf64_Dyn; - let base = info.dlpi_addr as usize; - 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(p.p_vaddr as usize)?; - let end = start.checked_add(p.p_memsz as usize)?; - (addr >= start && addr < end).then_some(end) - }) - }; - let correct = |a: u64| -> usize { - let a = a as usize; - if a > base { - a - } else { - base + a - } - }; - - let mut strtab: *const c_char = std::ptr::null(); - let mut strtab_size: usize = 0; - let mut symtab: *const Elf64_Sym = std::ptr::null(); - let mut rels: *const Elf64_Rel = std::ptr::null(); - let mut rels_size: usize = 0; - let mut relas: *const Elf64_Rela = std::ptr::null(); - let mut relas_size: usize = 0; - let mut jmprels: *const Elf64_Rela = std::ptr::null(); - let mut jmprels_size: usize = 0; - let mut gnu_hash: *const u32 = std::ptr::null(); - let mut pltrel_type: i64 = 0; - - let mut it = dyn_begin; - loop { - let d = &*it; - if d.d_tag == DT_NULL { - break; - } - let v = d.d_un; - match d.d_tag { - DT_STRTAB => strtab = correct(v) as *const c_char, - DT_STRSZ => strtab_size = v as usize, - DT_SYMTAB => symtab = correct(v) as *const Elf64_Sym, - DT_GNU_HASH => gnu_hash = correct(v) as *const u32, - DT_REL => rels = correct(v) as *const Elf64_Rel, - DT_RELA => relas = correct(v) as *const Elf64_Rela, - DT_JMPREL => jmprels = correct(v) as *const Elf64_Rela, - DT_RELSZ => rels_size = v as usize, - DT_RELASZ => relas_size = v as usize, - DT_PLTRELSZ => jmprels_size = v as usize, - DT_PLTREL => pltrel_type = v as i64, - _ => {} - } - it = it.add(1); - } - - // JMPREL entries are RELA only if DT_PLTREL says so. - if pltrel_type != DT_RELA { - jmprels = std::ptr::null(); - jmprels_size = 0; - } - - if strtab.is_null() || symtab.is_null() || gnu_hash.is_null() { - return None; - } - - let gnu_hash_addr = gnu_hash as usize; - let end = containing_load_segment_end(gnu_hash_addr)?; - let bytes = end.checked_sub(gnu_hash_addr)?; - let gnu_hash_words = bytes / core::mem::size_of::(); - let sym_count = gnu_hash_symbol_count(gnu_hash, gnu_hash_words).unwrap_or_else(|| { - // Fallback for degenerate .gnu.hash (e.g. executables with only - // undefined imports): estimate dynsym entry count from the common - // .dynsym-before-.dynstr layout. This is a heuristic, not an ELF - // guarantee. If it underestimates we may skip patching some - // relocations; valid relocation indexes should still keep an - // overestimate from faulting on normal loaded objects. - let symtab_addr = symtab as usize; - let strtab_addr = strtab as usize; - if strtab_addr > symtab_addr { - let bytes = strtab_addr - symtab_addr; - (bytes / core::mem::size_of::()) as u32 - } else { - // Can't estimate; allow any index and rely on strtab - // bounds checking in sym_name to catch bad accesses. - u32::MAX - } - }); - - Some(Self { - strtab, - strtab_size, - symtab, - sym_count, - rels, - rels_count: rels_size / core::mem::size_of::(), - relas, - relas_count: relas_size / core::mem::size_of::(), - jmprels, - jmprels_count: jmprels_size / core::mem::size_of::(), - gnu_hash, - gnu_hash_words, - base_address: base, - }) - } - - unsafe fn sym_name(&self, idx: u32) -> Option<&CStr> { - if (idx as usize) >= self.sym_count as usize { - return None; - } - let sym = &*self.symtab.add(idx as usize); - let off = sym.st_name as usize; - if off >= self.strtab_size { - return None; - } - Some(CStr::from_ptr(self.strtab.add(off))) - } -} - -/// Compute the GNU symbol hash used by `DT_GNU_HASH` tables. -/// See . -fn gnu_hash(name: &[u8]) -> u32 { - let mut h: u32 = 5381; - for c in name { - h = h.wrapping_shl(5).wrapping_add(h).wrapping_add(*c as u32); - } - h -} - -/// Compute the total number of entries in `.dynsym` from the `.gnu.hash` table. -/// -/// Returns `None` only when the table is structurally invalid. When the hash -/// is degenerate (all buckets empty, typical for executables that only import -/// symbols), returns `None` to signal that the caller should use a fallback -/// (e.g. estimate from symtab/strtab distance) since the hash table doesn't -/// tell us how many undefined-import entries precede the hashed region. -unsafe fn gnu_hash_symbol_count(hashtab: *const u32, hashtab_words: usize) -> Option { - if hashtab_words < 4 { - return None; - } - - let nbuckets = *hashtab; - let symbias = *hashtab.add(1); - let bloom_size = *hashtab.add(2); - let bloom_size_words = (bloom_size as usize).checked_mul(2)?; - let buckets_start = 4usize.checked_add(bloom_size_words)?; - let chains_start = buckets_start.checked_add(nbuckets as usize)?; - - if bloom_size == 0 || buckets_start > hashtab_words || chains_start > hashtab_words { - return None; - } - if nbuckets == 0 { - // No buckets at all: can't determine symtab size from the hash. - return None; - } - - let buckets = std::slice::from_raw_parts(hashtab.add(buckets_start), nbuckets as usize); - let mut idx = *buckets.iter().max()?; - if idx == STN_UNDEF { - // All buckets empty: hash covers zero defined symbols, but the - // symtab may still have undefined imports. Signal the caller to - // use a fallback. - return None; - } - if idx < symbias { - return None; - } - - let chain_count = hashtab_words - chains_start; - loop { - let chain_idx = (idx - symbias) as usize; - if chain_idx >= chain_count { - return None; - } - if *hashtab.add(chains_start + chain_idx) & 1 != 0 { - return idx.checked_add(1); - } - idx = idx.checked_add(1)?; - } -} - -unsafe fn gnu_hash_lookup(info: &DynamicInfo, name: &[u8]) -> Option { - let hashtab = info.gnu_hash; - if info.gnu_hash_words < 4 { - return None; - } - - let nbuckets = *hashtab; - let symbias = *hashtab.add(1); - let bloom_size = *hashtab.add(2); - let bloom_shift = *hashtab.add(3); - let bloom_size_words = (bloom_size as usize).checked_mul(2)?; - let buckets_start = 4usize.checked_add(bloom_size_words)?; - let chains_start = buckets_start.checked_add(nbuckets as usize)?; - - if nbuckets == 0 - || bloom_size == 0 - || buckets_start > info.gnu_hash_words - || chains_start > info.gnu_hash_words - { - return None; - } - - let h = gnu_hash(name); - let bloom = hashtab.add(4) as *const u64; - let word = *bloom.add(((h / 64) & (bloom_size - 1)) as usize); - let bit1 = h & 63; - let bit2 = (h >> bloom_shift) & 63; - if ((word >> bit1) & (word >> bit2) & 1) == 0 { - return None; - } - - let buckets = hashtab.add(buckets_start); - let mut symidx = *buckets.add((h % nbuckets) as usize); - if symidx == STN_UNDEF { - return None; - } - if symidx < symbias { - return None; - } - - let chain_count = info.gnu_hash_words - chains_start; - loop { - let chain_idx = (symidx - symbias) as usize; - if chain_idx >= chain_count { - return None; - } - let chain_h = *hashtab.add(chains_start + chain_idx); - if ((chain_h ^ h) >> 1) == 0 { - if let Some(sname) = info.sym_name(symidx) { - let sym = info.symtab.add(symidx as usize); - if sname.to_bytes() == name && check_sym(&*sym) { - return Some(*sym); - } - } - } - if chain_h & 1 != 0 { - break; - } - symidx = symidx.checked_add(1)?; - } - None -} - -/// Return whether this is a defining function/object/notype symbol. -fn check_sym(sym: &Elf64_Sym) -> bool { - const SHN_ABS: u16 = 0xfff1; - let stt = sym.st_info & 0xf; - (sym.st_value != 0 || sym.st_shndx == SHN_ABS) && - // STT_NOTYPE(0), STT_OBJECT(1), STT_FUNC(2), STT_GNU_IFUNC(10) - matches!(stt, 0 | 1 | 2 | 10) -} - -/// 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. -fn iterate_libraries(mut callback: impl FnMut(&dl_phdr_info, bool) -> bool) { - struct Ctx<'a> { - callback: &'a mut dyn FnMut(&dl_phdr_info, bool) -> bool, - is_first: bool, - } - let mut ctx = Ctx { - callback: &mut callback, - is_first: true, - }; - - unsafe extern "C" fn trampoline( - info: *mut dl_phdr_info, - _size: libc::size_t, - data: *mut c_void, - ) -> c_int { - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let ctx = &mut *(data as *mut Ctx); - let is_exe = ctx.is_first; - ctx.is_first = false; - (ctx.callback)(&*info, is_exe) - })); - - // Never unwind a Rust panic through libc's dl_iterate_phdr callback. - // Treat patching as best-effort and stop iteration on panic. - result.map(i32::from).unwrap_or(1) - } - - unsafe { - dl_iterate_phdr(Some(trampoline), &mut ctx as *mut _ as *mut c_void); - } -} - -/// A single /proc/self/maps entry: address range + current protection flags. -#[derive(Clone, Copy)] -struct MapEntry { - start: usize, - end: usize, - prot: i32, -} - -/// Parse /proc/self/maps into a sorted list of (range, prot) entries. -/// -/// Used to remember each GOT page's original protection so we can restore -/// it after patching, rather than leaving Full-RELRO pages read-write for -/// the lifetime of the process. -fn read_proc_maps() -> Vec { - let Ok(f) = std::fs::File::open("/proc/self/maps") else { - return Vec::new(); - }; - let mut out = Vec::new(); - for line in BufReader::new(f).lines().map_while(Result::ok) { - let mut parts = line.split_whitespace(); - let Some(range) = parts.next() else { continue }; - let Some(perms) = parts.next() else { continue }; - let Some(dash) = range.find('-') else { - continue; - }; - let Ok(start) = usize::from_str_radix(&range[..dash], 16) else { - continue; - }; - let Ok(end) = usize::from_str_radix(&range[dash + 1..], 16) else { - continue; - }; - let b = perms.as_bytes(); - let mut prot = 0; - if b.first() == Some(&b'r') { - prot |= PROT_READ; - } - if b.get(1) == Some(&b'w') { - prot |= PROT_WRITE; - } - if b.get(2) == Some(&b'x') { - prot |= PROT_EXEC; - } - out.push(MapEntry { start, end, prot }); - } - out -} - -/// Batched GOT-entry patcher that remembers each touched page's -/// original protection and restores it at the end of a patching pass. -/// -/// On Full-RELRO binaries, GOT pages start read-only. The old -/// `override_entry` helper flipped them to read-write and never -/// restored them, weakening RELRO for the process lifetime. This guard -/// mprotects each unique page once (RW), lets the caller write as -/// many entries as it needs, then mprotects each page back to what -/// `/proc/self/maps` reported at guard-construction time when it is -/// dropped (including on panic or early return). -struct PageProtGuard { - page_size: usize, - maps: Vec, - // Aligned page base -> original prot flags read from /proc/self/maps. - touched: HashMap, -} - -impl PageProtGuard { - fn new() -> Self { - // sysconf can return -1 on error; fall back to a conservative - // 4 KiB default if the query fails. - let raw = unsafe { sysconf(_SC_PAGESIZE) }; - let page_size = usize::try_from(raw).unwrap_or(4096); - Self { - page_size, - maps: read_proc_maps(), - touched: HashMap::new(), - } - } - - fn original_prot(&self, addr: usize) -> Option { - self.maps - .iter() - .find(|m| addr >= m.start && addr < m.end) - .map(|m| m.prot) - } - - /// Make the containing page writable if it isn't already touched, - /// then replace one GOT entry. - unsafe fn override_entry(&mut self, addr: usize, new_value: usize) -> bool { - let aligned = addr & !(self.page_size - 1); - if !self.touched.contains_key(&aligned) { - // If /proc/self/maps isn't available (or the page isn't in - // it, which shouldn't happen for a mapped GOT page) fall - // back to PROT_READ - the RELRO'd default. That's tighter - // than the previous behavior of leaving pages RW. - let orig = self.original_prot(aligned).unwrap_or(PROT_READ); - if mprotect( - aligned as *mut c_void, - self.page_size, - PROT_READ | PROT_WRITE, - ) != 0 - { - return false; - } - self.touched.insert(aligned, orig); - } - std::ptr::write_unaligned(addr as *mut usize, new_value); - true - } -} - -impl Drop for PageProtGuard { - /// Restore every touched page to its original protection. Runs on - /// scope exit - including panic or early return - so page protections - /// are never left weakened even if a patching pass bails out midway. - fn drop(&mut self) { - for (aligned, orig) in self.touched.drain() { - // Best-effort: nothing sensible to do on failure other than - // leave the page RW, which is the pre-fix behavior. - unsafe { mprotect(aligned as *mut c_void, self.page_size, orig) }; - } - } -} - -#[derive(Clone, Copy)] -pub struct LookupResult { - pub address: usize, -} - -/// Look up a symbol across loaded objects, returning the first -/// non-zero-sized definition whose address is not `not_this_symbol`. -/// Null-sized symbols are ignored so hooks resolve to callable definitions. -pub fn lookup_symbol(name: &str, not_this_symbol: usize) -> Option { - let needle = name.as_bytes(); - let mut found: Option = None; - 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") { - return false; - } - let Some(dyn_info) = DynamicInfo::from_phdr(info) else { - return false; - }; - if let Some(sym) = gnu_hash_lookup(&dyn_info, needle) { - if sym.st_size > 0 { - let addr = sym.st_value as usize + dyn_info.base_address; - if addr != not_this_symbol { - found = Some(LookupResult { address: addr }); - return true; // stop - } - } - } - false - }); - found -} +pub use libdd_got_hook::lookup_symbol; +use libdd_got_hook::{elf64_r_sym, iterate_libraries, DynamicInfo, PageProtGuard}; /// Per-library bookkeeping for the GOT re-scan. We never un-patch (see /// the crate docs on why un-installing can't be done safely), so this @@ -702,7 +195,7 @@ impl SymbolOverrides { // the load address, and a later `dlopen` can place a different // library at the same address. If the name differs from what we // recorded, treat this as a fresh library so its GOT gets patched. - let entry_is_new = match self.patched_libraries.entry(dyn_info.base_address) { + let entry_is_new = match self.patched_libraries.entry(dyn_info.base_address()) { std::collections::hash_map::Entry::Vacant(e) => { e.insert(PatchedLibrary { library_name, @@ -729,8 +222,9 @@ impl SymbolOverrides { return; } - if !dyn_info.rels.is_null() { - let relocs = std::slice::from_raw_parts(dyn_info.rels, dyn_info.rels_count); + let (rels_ptr, rels_count) = dyn_info.rels(); + if !rels_ptr.is_null() { + let relocs = core::slice::from_raw_parts(rels_ptr, rels_count); for reloc in relocs { Self::process_relocation( &self.overrides, @@ -741,14 +235,11 @@ impl SymbolOverrides { ); } } - for slice_ptr_and_len in [ - (dyn_info.relas, dyn_info.relas_count), - (dyn_info.jmprels, dyn_info.jmprels_count), - ] { - if slice_ptr_and_len.0.is_null() { + for (ptr, count) in [dyn_info.relas(), dyn_info.jmprels()] { + if ptr.is_null() { continue; } - let relocs = std::slice::from_raw_parts(slice_ptr_and_len.0, slice_ptr_and_len.1); + let relocs = core::slice::from_raw_parts(ptr, count); for reloc in relocs { Self::process_relocation( &self.overrides, @@ -800,7 +291,7 @@ impl SymbolOverrides { return; } - let addr = r_offset + dyn_info.base_address; + let addr = r_offset + dyn_info.base_address(); if addr == ov.do_not_override_this_symbol { return; } @@ -810,36 +301,23 @@ impl SymbolOverrides { } } -fn elf64_r_sym(info: u64) -> u64 { - info >> 32 -} - #[cfg(test)] mod tests { use super::*; #[test] - fn page_prot_guard_finds_original_mapping_protection() { - let guard = PageProtGuard { - page_size: 4096, - maps: vec![ - MapEntry { - start: 0x1000, - end: 0x2000, - prot: PROT_READ, - }, - MapEntry { - start: 0x2000, - end: 0x3000, - prot: PROT_READ | PROT_EXEC, - }, - ], - touched: HashMap::new(), - }; + #[cfg_attr(miri, ignore)] + fn can_lookup_malloc() { + let r = lookup_symbol("malloc", 0); + assert!(r.is_some(), "expected to find malloc in loaded libraries"); + let r = r.unwrap(); + assert!(r.address != 0); + } - assert_eq!(guard.original_prot(0x1000), Some(PROT_READ)); - assert_eq!(guard.original_prot(0x1fff), Some(PROT_READ)); - assert_eq!(guard.original_prot(0x2000), Some(PROT_READ | PROT_EXEC)); - assert_eq!(guard.original_prot(0x3000), None); + #[test] + #[cfg_attr(miri, ignore)] // miri doesn't support dl_iterate_phdr + fn unknown_symbol_lookup_returns_none() { + let r = lookup_symbol("definitely_not_a_real_libc_symbol_xyzzy", 0); + assert!(r.is_none()); } } diff --git a/tools/docker/Dockerfile.build b/tools/docker/Dockerfile.build index 5f38cb0707..810f2bdbc8 100644 --- a/tools/docker/Dockerfile.build +++ b/tools/docker/Dockerfile.build @@ -131,6 +131,7 @@ COPY "libdd-otel-thread-ctx/Cargo.toml" "libdd-otel-thread-ctx/" COPY "libdd-otel-thread-ctx-ffi/Cargo.toml" "libdd-otel-thread-ctx-ffi/" COPY "libdd-profiling-heap-sampler/Cargo.toml" "libdd-profiling-heap-sampler/" COPY "libdd-profiling-heap-allocator/Cargo.toml" "libdd-profiling-heap-allocator/" +COPY "libdd-got-hook/Cargo.toml" "libdd-got-hook/" COPY "libdd-profiling-heap-gotter/Cargo.toml" "libdd-profiling-heap-gotter/" COPY "libdd-profiling-heap-gotter-ffi/Cargo.toml" "libdd-profiling-heap-gotter-ffi/" RUN find -name "Cargo.toml" | sed -e s#Cargo.toml#src/lib.rs#g | xargs -n 1 sh -c 'mkdir -p $(dirname $1); touch $1; echo $1' create_stubs From 0a853a1252822ad9c9d76ce6abf3ce995b5f6bb0 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Fri, 31 Jul 2026 16:49:38 +0000 Subject: [PATCH 2/5] Comment clarity and rename pkg --- Cargo.toml | 2 +- {libdd-got-hook => libdd-gotter}/Cargo.toml | 8 ++--- libdd-gotter/README.md | 39 +++++++++++++++++++++ {libdd-got-hook => libdd-gotter}/src/elf.rs | 18 +++++++--- {libdd-got-hook => libdd-gotter}/src/lib.rs | 0 libdd-profiling-heap-gotter/Cargo.toml | 2 +- libdd-profiling-heap-gotter/src/elf.rs | 25 ++----------- 7 files changed, 60 insertions(+), 34 deletions(-) rename {libdd-got-hook => libdd-gotter}/Cargo.toml (87%) create mode 100644 libdd-gotter/README.md rename {libdd-got-hook => libdd-gotter}/src/elf.rs (95%) rename {libdd-got-hook => libdd-gotter}/src/lib.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 181a6160d9..f46ba4db66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ "libdd-alloc", "libdd-profiling-heap-sampler", "libdd-profiling-heap-allocator", - "libdd-got-hook", + "libdd-gotter", "libdd-profiling-heap-gotter", "libdd-profiling-heap-gotter-ffi", "libdd-crashtracker", diff --git a/libdd-got-hook/Cargo.toml b/libdd-gotter/Cargo.toml similarity index 87% rename from libdd-got-hook/Cargo.toml rename to libdd-gotter/Cargo.toml index c72c1e99cc..563e7c55b0 100644 --- a/libdd-got-hook/Cargo.toml +++ b/libdd-gotter/Cargo.toml @@ -2,11 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 [package] -name = "libdd-got-hook" +name = "libdd-gotter" version = "1.0.0" description = "ELF GOT-patching primitives for runtime function interposition on 64-bit Linux." -homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-got-hook" -repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-got-hook" +homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-gotter" +repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-gotter" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -15,4 +15,4 @@ license.workspace = true bench = false [dependencies] -libc = "0.2" +libc.workspace = true diff --git a/libdd-gotter/README.md b/libdd-gotter/README.md new file mode 100644 index 0000000000..687f87e753 --- /dev/null +++ b/libdd-gotter/README.md @@ -0,0 +1,39 @@ +# libdd-gotter + +> [!WARNING] +> This library does runtime function interposition by patching the Global Offset Table (GOT) of loaded ELF objects. This is a substantial intervention in a running process — it modifies function pointers that the dynamic linker has already resolved, affecting all code that calls through those GOT entries. Incorrect use can cause crashes, infinite recursion, heap corruption, or silent data loss. Understand the ELF dynamic linking model before using this crate. + +## What it does + +When a shared library calls an external function like `malloc`, it jumps through a pointer in its **Global Offset Table** -- a writable table that the dynamic linker fills at load time. This crate walks every loaded ELF object via `dl_iterate_phdr`, parses its `PT_DYNAMIC` segment, and rewrites GOT entries so calls are redirected to a hook function. The original function address is resolved and returned so the hook can forward to it. + +## Usage + +### Single-symbol hook (crashtracker intercepting `__assert_fail`) + +```rust +use libdd_got_hook::hook_symbol; + +static ORIG_FN: AtomicUsize = AtomicUsize::new(0); + +unsafe extern "C" fn my_hook(/* same signature as target */) { + // ... do work ... + // forward to original via ORIG_FN +} + +let mut orig_addr: usize = 0; +unsafe { + hook_symbol(c"__assert_fail", my_hook as *const () as usize, &mut orig_addr); +} +ORIG_FN.store(orig_addr, Ordering::Release); +``` + +### Multi-symbol registry (heap profiling hooking malloc/free/calloc/realloc) + +See [`libdd-profiling-heap-gotter`](../libdd-profiling-heap-gotter) which builds a `SymbolOverrides` registry on top of the primitives exported by this crate. + +## Platform support + +- **64-bit Linux (glibc)**: Full support. +- **64-bit Linux (musl)**: Works for dynamically linked symbols. Statically linked symbols have no GOT entries and cannot be patched. +- **Other platforms**: The crate compiles but exports nothing — all types and functions are `cfg`-gated to `target_os = "linux"`. diff --git a/libdd-got-hook/src/elf.rs b/libdd-gotter/src/elf.rs similarity index 95% rename from libdd-got-hook/src/elf.rs rename to libdd-gotter/src/elf.rs index e5b532be64..b0cf63a46e 100644 --- a/libdd-got-hook/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -253,6 +253,9 @@ pub unsafe fn gnu_hash_symbol_count(hashtab: *const u32, hashtab_words: usize) - let buckets = core::slice::from_raw_parts(hashtab.add(buckets_start), nbuckets as usize); let mut idx = *buckets.iter().max()?; + // All buckets empty: hash covers zero defined symbols, but the + // symtab may still have undefined imports. Signal the caller to + // use a fallback. if idx == STN_UNDEF { return None; } @@ -379,6 +382,7 @@ pub fn iterate_libraries(mut callback: impl FnMut(&dl_phdr_info, bool) -> bool) })); // Never unwind a Rust panic through libc's dl_iterate_phdr callback. + // Treat patching as best-effort and stop iteration on panic. result.map(i32::from).unwrap_or(1) } @@ -504,9 +508,13 @@ impl Default for PageProtGuard { } impl Drop for PageProtGuard { - /// Restore every touched page to its original protection. + // Restore every touched page to its original protection. Runs on + /// scope exit - including panic or early return - so page protections + /// are never left weakened even if a patching pass bails out midway. fn drop(&mut self) { for (aligned, orig) in self.touched.drain() { + // Best-effort: nothing sensible to do on failure other than + // leave the page RW, which is the pre-fix behavior. unsafe { mprotect(aligned as *mut c_void, self.page_size, orig) }; } } @@ -585,15 +593,15 @@ mod tests { } #[test] - #[cfg_attr(miri, ignore)] - fn test_can_lookup_malloc() { - let r = lookup_symbol("malloc", 0); + #[cfg_attr(miri, ignore)] // miri doesn't support dl_iterate_phdr + fn test_can_lookup_known_symbol() { + let r = lookup_symbol("malloc", 0); // malloc is definitely known assert!(r.is_some(), "expected to find malloc in loaded libraries"); assert!(r.unwrap().address != 0); } #[test] - #[cfg_attr(miri, ignore)] + #[cfg_attr(miri, ignore)] // miri doesn't support dl_iterate_phdr fn test_unknown_symbol_lookup_returns_none() { let r = lookup_symbol("definitely_not_a_real_libc_symbol_xyzzy", 0); assert!(r.is_none()); diff --git a/libdd-got-hook/src/lib.rs b/libdd-gotter/src/lib.rs similarity index 100% rename from libdd-got-hook/src/lib.rs rename to libdd-gotter/src/lib.rs diff --git a/libdd-profiling-heap-gotter/Cargo.toml b/libdd-profiling-heap-gotter/Cargo.toml index d28be1a031..3e31221946 100644 --- a/libdd-profiling-heap-gotter/Cargo.toml +++ b/libdd-profiling-heap-gotter/Cargo.toml @@ -25,7 +25,7 @@ test-support = [] [target.'cfg(target_os = "linux")'.dependencies] libc.workspace = true -libdd-got-hook = { version = "1.0.0", path = "../libdd-got-hook" } +libdd-gotter = { version = "1.0.0", path = "../libdd-gotter" } libdd-profiling-heap-sampler = { version = "1.0.0", path = "../libdd-profiling-heap-sampler" } [target.'cfg(target_os = "linux")'.dev-dependencies] diff --git a/libdd-profiling-heap-gotter/src/elf.rs b/libdd-profiling-heap-gotter/src/elf.rs index 7f814d42a1..de66c01dd7 100644 --- a/libdd-profiling-heap-gotter/src/elf.rs +++ b/libdd-profiling-heap-gotter/src/elf.rs @@ -11,8 +11,8 @@ use std::collections::HashMap; use std::ffi::CStr; use std::sync::atomic::{AtomicUsize, Ordering}; -pub use libdd_got_hook::lookup_symbol; -use libdd_got_hook::{elf64_r_sym, iterate_libraries, DynamicInfo, PageProtGuard}; +pub use libdd_gotter::lookup_symbol; +use libdd_gotter::{elf64_r_sym, iterate_libraries, DynamicInfo, PageProtGuard}; /// Per-library bookkeeping for the GOT re-scan. We never un-patch (see /// the crate docs on why un-installing can't be done safely), so this @@ -300,24 +300,3 @@ impl SymbolOverrides { guard.override_entry(addr, ov.new_symbol); } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - #[cfg_attr(miri, ignore)] - fn can_lookup_malloc() { - let r = lookup_symbol("malloc", 0); - assert!(r.is_some(), "expected to find malloc in loaded libraries"); - let r = r.unwrap(); - assert!(r.address != 0); - } - - #[test] - #[cfg_attr(miri, ignore)] // miri doesn't support dl_iterate_phdr - fn unknown_symbol_lookup_returns_none() { - let r = lookup_symbol("definitely_not_a_real_libc_symbol_xyzzy", 0); - assert!(r.is_none()); - } -} From baeeae677f2921c4d33be3d35a36348e03df94f3 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Fri, 31 Jul 2026 17:44:45 +0000 Subject: [PATCH 3/5] slice api --- .github/CODEOWNERS | 2 +- libdd-gotter/src/elf.rs | 37 +++++++++++++++++++------- libdd-profiling-heap-gotter/src/elf.rs | 26 +++++++----------- tools/docker/Dockerfile.build | 2 +- 4 files changed, 39 insertions(+), 28 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3390783a81..0a1fa57c04 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -51,7 +51,7 @@ libdd-crashtracker*/ @DataDog/libdatadog-profiling libdd-data-pipeline*/ @DataDog/libdatadog-apm libdd-ddsketch*/ @DataDog/libdatadog-apm @DataDog/apm-common-components-core libdd-dogstatsd-client @DataDog/apm-common-components-core -libdd-got-hook/ @DataDog/libdatadog @DataDog/libdatadog-profiling +libdd-gotter/ @DataDog/libdatadog @DataDog/libdatadog-profiling libdd-http-client @DataDog/apm-common-components-core libdd-library-config*/ @DataDog/apm-sdk-capabilities-rust libdd-log*/ @DataDog/apm-common-components-core diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index b0cf63a46e..3edf254c47 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -197,19 +197,38 @@ impl DynamicInfo { self.base_address } - /// Access to REL relocations (pointer, count). - pub fn rels(&self) -> (*const Elf64_Rel, usize) { - (self.rels, self.rels_count) + /// REL relocations for this object, or empty if none. + /// + /// Safe because `from_phdr` validated the pointer and count from the + /// `PT_DYNAMIC` segment of a currently-loaded ELF object. + pub fn rels(&self) -> &[Elf64_Rel] { + if self.rels.is_null() || self.rels_count == 0 { + &[] + } else { + // SAFETY: from_phdr set rels/rels_count from the DT_REL/DT_RELSZ + // entries of a mapped ELF object; the array is valid for the + // object's lifetime (held by dl_iterate_phdr's loader lock or by + // the caller's dlopen handle). + unsafe { core::slice::from_raw_parts(self.rels, self.rels_count) } + } } - /// Access to RELA relocations (pointer, count). - pub fn relas(&self) -> (*const Elf64_Rela, usize) { - (self.relas, self.relas_count) + /// RELA relocations for this object, or empty if none. + pub fn relas(&self) -> &[Elf64_Rela] { + if self.relas.is_null() || self.relas_count == 0 { + &[] + } else { + unsafe { core::slice::from_raw_parts(self.relas, self.relas_count) } + } } - /// Access to JMPREL (PLT) relocations (pointer, count). - pub fn jmprels(&self) -> (*const Elf64_Rela, usize) { - (self.jmprels, self.jmprels_count) + /// JMPREL (PLT) relocations for this object, or empty if none. + pub fn jmprels(&self) -> &[Elf64_Rela] { + if self.jmprels.is_null() || self.jmprels_count == 0 { + &[] + } else { + unsafe { core::slice::from_raw_parts(self.jmprels, self.jmprels_count) } + } } } diff --git a/libdd-profiling-heap-gotter/src/elf.rs b/libdd-profiling-heap-gotter/src/elf.rs index de66c01dd7..1e6d9291f9 100644 --- a/libdd-profiling-heap-gotter/src/elf.rs +++ b/libdd-profiling-heap-gotter/src/elf.rs @@ -222,24 +222,16 @@ impl SymbolOverrides { return; } - let (rels_ptr, rels_count) = dyn_info.rels(); - if !rels_ptr.is_null() { - let relocs = core::slice::from_raw_parts(rels_ptr, rels_count); - for reloc in relocs { - Self::process_relocation( - &self.overrides, - dyn_info, - elf64_r_sym(reloc.r_info) as u32, - reloc.r_offset as usize, - guard, - ); - } + for reloc in dyn_info.rels() { + Self::process_relocation( + &self.overrides, + dyn_info, + elf64_r_sym(reloc.r_info) as u32, + reloc.r_offset as usize, + guard, + ); } - for (ptr, count) in [dyn_info.relas(), dyn_info.jmprels()] { - if ptr.is_null() { - continue; - } - let relocs = core::slice::from_raw_parts(ptr, count); + for relocs in [dyn_info.relas(), dyn_info.jmprels()] { for reloc in relocs { Self::process_relocation( &self.overrides, diff --git a/tools/docker/Dockerfile.build b/tools/docker/Dockerfile.build index 810f2bdbc8..6022a185f0 100644 --- a/tools/docker/Dockerfile.build +++ b/tools/docker/Dockerfile.build @@ -131,7 +131,7 @@ COPY "libdd-otel-thread-ctx/Cargo.toml" "libdd-otel-thread-ctx/" COPY "libdd-otel-thread-ctx-ffi/Cargo.toml" "libdd-otel-thread-ctx-ffi/" COPY "libdd-profiling-heap-sampler/Cargo.toml" "libdd-profiling-heap-sampler/" COPY "libdd-profiling-heap-allocator/Cargo.toml" "libdd-profiling-heap-allocator/" -COPY "libdd-got-hook/Cargo.toml" "libdd-got-hook/" +COPY "libdd-gotter/Cargo.toml" "libdd-gotter/" COPY "libdd-profiling-heap-gotter/Cargo.toml" "libdd-profiling-heap-gotter/" COPY "libdd-profiling-heap-gotter-ffi/Cargo.toml" "libdd-profiling-heap-gotter-ffi/" RUN find -name "Cargo.toml" | sed -e s#Cargo.toml#src/lib.rs#g | xargs -n 1 sh -c 'mkdir -p $(dirname $1); touch $1; echo $1' create_stubs From 6966e28f09870ec30a5c8a9c49c7f1a42901756d Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Mon, 3 Aug 2026 17:55:18 +0000 Subject: [PATCH 4/5] Safety comments, in code documentation, test for proc maps parsing --- Cargo.lock | 13 ++++-- LICENSE-3rdparty.csv | 2 +- datadog-ffe-test-suite/ffe-system-test-data | 1 - libdd-gotter/README.md | 12 ++--- libdd-gotter/src/elf.rs | 50 ++++++++++++++++++++- 5 files changed, 66 insertions(+), 12 deletions(-) delete mode 160000 datadog-ffe-test-suite/ffe-system-test-data diff --git a/Cargo.lock b/Cargo.lock index bf31960d79..b1ece847cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2787,9 +2787,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libdd-agent-client" @@ -3111,6 +3111,13 @@ dependencies = [ "serde_json", ] +[[package]] +name = "libdd-gotter" +version = "1.0.0" +dependencies = [ + "libc", +] + [[package]] name = "libdd-http-client" version = "0.1.0" @@ -3299,7 +3306,7 @@ name = "libdd-profiling-heap-gotter" version = "1.0.0" dependencies = [ "libc", - "libdd-got-hook", + "libdd-gotter", "libdd-profiling-heap-sampler", "serial_test", ] diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index ef04646f30..7c69816d4f 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -220,7 +220,7 @@ js-sys,https://github.com/rustwasm/wasm-bindgen/tree/master/crates/js-sys,MIT OR kernel32-sys,https://github.com/retep998/winapi-rs,MIT,Peter Atashian kv-log-macro,https://github.com/yoshuawuyts/kv-log-macro,MIT OR Apache-2.0,Yoshua Wuyts lazy_static,https://github.com/rust-lang-nursery/lazy-static.rs,MIT OR Apache-2.0,Marvin Löbel -libc,https://github.com/rust-lang/libc,MIT OR Apache-2.0,The Rust Project Developers +libc,https://github.com/rust-lang/libc,MIT OR Apache-2.0,The libc Authors libdd-libunwind-sys,https://github.com/DataDog/libdatadog/tree/main/libdd-libunwind-sys,Apache-2.0,The libdd-libunwind-sys Authors libloading,https://github.com/nagisa/rust_libloading,ISC,Simonas Kazlauskas libredox,https://gitlab.redox-os.org/redox-os/libredox,MIT,4lDO2 <4lDO2@protonmail.com> diff --git a/datadog-ffe-test-suite/ffe-system-test-data b/datadog-ffe-test-suite/ffe-system-test-data deleted file mode 160000 index d9d8020045..0000000000 --- a/datadog-ffe-test-suite/ffe-system-test-data +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d9d8020045c7cf373f37e3e7e0122b34709f1a07 diff --git a/libdd-gotter/README.md b/libdd-gotter/README.md index 687f87e753..932bc7272a 100644 --- a/libdd-gotter/README.md +++ b/libdd-gotter/README.md @@ -25,6 +25,8 @@ let mut orig_addr: usize = 0; unsafe { hook_symbol(c"__assert_fail", my_hook as *const () as usize, &mut orig_addr); } +// Release pairs with the Acquire load in my_hook, ensuring the GOT +// patches from hook_symbol are visible before the hook reads orig_addr. ORIG_FN.store(orig_addr, Ordering::Release); ``` @@ -32,8 +34,8 @@ ORIG_FN.store(orig_addr, Ordering::Release); See [`libdd-profiling-heap-gotter`](../libdd-profiling-heap-gotter) which builds a `SymbolOverrides` registry on top of the primitives exported by this crate. -## Platform support - -- **64-bit Linux (glibc)**: Full support. -- **64-bit Linux (musl)**: Works for dynamically linked symbols. Statically linked symbols have no GOT entries and cannot be patched. -- **Other platforms**: The crate compiles but exports nothing — all types and functions are `cfg`-gated to `target_os = "linux"`. +## Support +This library can be used on ARM64 and AMD64 Linux in processes using glibc or musl runtimes. +Only symbols that have been dynamically linked can be intercept. +For instance, if you want to intercept the `malloc` of your C runtime, +you _cannot_ do so if the application has been statically linked against musl. diff --git a/libdd-gotter/src/elf.rs b/libdd-gotter/src/elf.rs index 3edf254c47..c3fb336620 100644 --- a/libdd-gotter/src/elf.rs +++ b/libdd-gotter/src/elf.rs @@ -218,6 +218,8 @@ impl DynamicInfo { if self.relas.is_null() || self.relas_count == 0 { &[] } else { + // SAFETY: same as rels(); from_phdr set relas/relas_count + // from DT_RELA/DT_RELASZ of a mapped ELF object. unsafe { core::slice::from_raw_parts(self.relas, self.relas_count) } } } @@ -227,6 +229,8 @@ impl DynamicInfo { if self.jmprels.is_null() || self.jmprels_count == 0 { &[] } else { + // SAFETY: same as rels(); from_phdr set jmprels/jmprels_count + // from DT_JMPREL/DT_PLTRELSZ of a mapped ELF object. unsafe { core::slice::from_raw_parts(self.jmprels, self.jmprels_count) } } } @@ -309,6 +313,14 @@ pub unsafe fn gnu_hash_lookup(info: &DynamicInfo, name: &[u8]) -> Option Self { - // sysconf can return -1 on error; fall back to a conservative - // 4 KiB default if the query fails. + // SAFETY: sysconf(_SC_PAGESIZE) is safe to call; it + // reads a cached kernel value with no side effects. Returns -1 + // on error; we fall back to 4 KiB in that case. let raw = unsafe { sysconf(_SC_PAGESIZE) }; let page_size = usize::try_from(raw).unwrap_or(4096); Self { @@ -625,4 +638,37 @@ mod tests { let r = lookup_symbol("definitely_not_a_real_libc_symbol_xyzzy", 0); assert!(r.is_none()); } + + #[test] + #[cfg_attr(miri, ignore)] + fn test_read_proc_maps_returns_entries() { + let maps = read_proc_maps(); + // Every running Linux process has at least a few mappings + assert!(!maps.is_empty(), "read_proc_maps should return entries"); + + for entry in &maps { + // Every mapping has a non-zero size. + assert!( + entry.end > entry.start, + "mapping {:#x}-{:#x} has zero or negative size", + entry.start, + entry.end, + ); + // Prot flags should only contain the bits we parse. + let valid_bits = PROT_READ | PROT_WRITE | PROT_EXEC; + assert!( + entry.prot & !valid_bits == 0, + "unexpected prot bits {:#x} in mapping {:#x}-{:#x}", + entry.prot, + entry.start, + entry.end, + ); + } + + // At least one mapping should be readable (the executable itself). + assert!( + maps.iter().any(|e| e.prot & PROT_READ != 0), + "expected at least one readable mapping" + ); + } } From 5509c447bb2537dc31862e287be47c711b092902 Mon Sep 17 00:00:00 2001 From: Gyuheon Oh Date: Tue, 4 Aug 2026 13:16:04 +0000 Subject: [PATCH 5/5] Initial reelase --- Cargo.lock | 2 +- libdd-gotter/Cargo.toml | 2 +- libdd-profiling-heap-gotter/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1ece847cf..9c9dd90cd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3113,7 +3113,7 @@ dependencies = [ [[package]] name = "libdd-gotter" -version = "1.0.0" +version = "0.1.0" dependencies = [ "libc", ] diff --git a/libdd-gotter/Cargo.toml b/libdd-gotter/Cargo.toml index 563e7c55b0..8df14a0352 100644 --- a/libdd-gotter/Cargo.toml +++ b/libdd-gotter/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "libdd-gotter" -version = "1.0.0" +version = "0.1.0" description = "ELF GOT-patching primitives for runtime function interposition on 64-bit Linux." homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-gotter" repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-gotter" diff --git a/libdd-profiling-heap-gotter/Cargo.toml b/libdd-profiling-heap-gotter/Cargo.toml index 3e31221946..083ed71dfc 100644 --- a/libdd-profiling-heap-gotter/Cargo.toml +++ b/libdd-profiling-heap-gotter/Cargo.toml @@ -25,7 +25,7 @@ test-support = [] [target.'cfg(target_os = "linux")'.dependencies] libc.workspace = true -libdd-gotter = { version = "1.0.0", path = "../libdd-gotter" } +libdd-gotter = { version = "0.1.0", path = "../libdd-gotter" } libdd-profiling-heap-sampler = { version = "1.0.0", path = "../libdd-profiling-heap-sampler" } [target.'cfg(target_os = "linux")'.dev-dependencies]