Skip to content

Commit cfc1baf

Browse files
committed
Safety comments, in code documentation, test for proc maps parsing
1 parent baeeae6 commit cfc1baf

4 files changed

Lines changed: 65 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 0 additions & 1 deletion
This file was deleted.

libdd-gotter/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,17 @@ let mut orig_addr: usize = 0;
2525
unsafe {
2626
hook_symbol(c"__assert_fail", my_hook as *const () as usize, &mut orig_addr);
2727
}
28+
// Release pairs with the Acquire load in my_hook, ensuring the GOT
29+
// patches from hook_symbol are visible before the hook reads orig_addr.
2830
ORIG_FN.store(orig_addr, Ordering::Release);
2931
```
3032

3133
### Multi-symbol registry (heap profiling hooking malloc/free/calloc/realloc)
3234

3335
See [`libdd-profiling-heap-gotter`](../libdd-profiling-heap-gotter) which builds a `SymbolOverrides` registry on top of the primitives exported by this crate.
3436

35-
## Platform support
36-
37-
- **64-bit Linux (glibc)**: Full support.
38-
- **64-bit Linux (musl)**: Works for dynamically linked symbols. Statically linked symbols have no GOT entries and cannot be patched.
39-
- **Other platforms**: The crate compiles but exports nothing — all types and functions are `cfg`-gated to `target_os = "linux"`.
37+
## Support
38+
This library can be used on ARM64 and AMD64 Linux in processes using glibc or musl runtimes.
39+
Only symbols that have been dynamically linked can be intercept.
40+
For instance, if you want to intercept the `malloc` of your C runtime,
41+
you _cannot_ do so if the application has been statically linked against musl.

libdd-gotter/src/elf.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ impl DynamicInfo {
218218
if self.relas.is_null() || self.relas_count == 0 {
219219
&[]
220220
} else {
221+
// SAFETY: same as rels(); from_phdr set relas/relas_count
222+
// from DT_RELA/DT_RELASZ of a mapped ELF object.
221223
unsafe { core::slice::from_raw_parts(self.relas, self.relas_count) }
222224
}
223225
}
@@ -227,6 +229,8 @@ impl DynamicInfo {
227229
if self.jmprels.is_null() || self.jmprels_count == 0 {
228230
&[]
229231
} else {
232+
// SAFETY: same as rels(); from_phdr set jmprels/jmprels_count
233+
// from DT_JMPREL/DT_PLTRELSZ of a mapped ELF object.
230234
unsafe { core::slice::from_raw_parts(self.jmprels, self.jmprels_count) }
231235
}
232236
}
@@ -309,6 +313,14 @@ pub unsafe fn gnu_hash_lookup(info: &DynamicInfo, name: &[u8]) -> Option<Elf64_S
309313
return None;
310314
}
311315

316+
// offset 0: nbuckets (u32)
317+
// offset 1: symbias (u32) first symbol index covered by the hash
318+
// offset 2: bloom_size (u32) number of u64 bloom filter words
319+
// offset 3: bloom_shift (u32) secondary bloom bit shift
320+
// offset 4: bloom[bloom_size] (u64 each, so bloom_size * 2 u32 words)
321+
// buckets[nbuckets] (u32 each)
322+
// chains[...] (u32 each, one per symbol starting at symbias)
323+
312324
let nbuckets = *hashtab;
313325
let symbias = *hashtab.add(1);
314326
let bloom_size = *hashtab.add(2);
@@ -474,8 +486,9 @@ pub struct PageProtGuard {
474486

475487
impl PageProtGuard {
476488
pub fn new() -> Self {
477-
// sysconf can return -1 on error; fall back to a conservative
478-
// 4 KiB default if the query fails.
489+
// SAFETY: sysconf(_SC_PAGESIZE) is safe to call; it
490+
// reads a cached kernel value with no side effects. Returns -1
491+
// on error; we fall back to 4 KiB in that case.
479492
let raw = unsafe { sysconf(_SC_PAGESIZE) };
480493
let page_size = usize::try_from(raw).unwrap_or(4096);
481494
Self {
@@ -625,4 +638,37 @@ mod tests {
625638
let r = lookup_symbol("definitely_not_a_real_libc_symbol_xyzzy", 0);
626639
assert!(r.is_none());
627640
}
641+
642+
#[test]
643+
#[cfg_attr(miri, ignore)]
644+
fn test_read_proc_maps_returns_entries() {
645+
let maps = read_proc_maps();
646+
// Every running Linux process has at least a few mappings
647+
assert!(!maps.is_empty(), "read_proc_maps should return entries");
648+
649+
for entry in &maps {
650+
// Every mapping has a non-zero size.
651+
assert!(
652+
entry.end > entry.start,
653+
"mapping {:#x}-{:#x} has zero or negative size",
654+
entry.start,
655+
entry.end,
656+
);
657+
// Prot flags should only contain the bits we parse.
658+
let valid_bits = PROT_READ | PROT_WRITE | PROT_EXEC;
659+
assert!(
660+
entry.prot & !valid_bits == 0,
661+
"unexpected prot bits {:#x} in mapping {:#x}-{:#x}",
662+
entry.prot,
663+
entry.start,
664+
entry.end,
665+
);
666+
}
667+
668+
// At least one mapping should be readable (the executable itself).
669+
assert!(
670+
maps.iter().any(|e| e.prot & PROT_READ != 0),
671+
"expected at least one readable mapping"
672+
);
673+
}
628674
}

0 commit comments

Comments
 (0)