Skip to content

Commit

Permalink
Support faster methods of reading process memory (#118)
Browse files Browse the repository at this point in the history
This adds 2 new methods for reading external process memory, in addition to the existing `PTRACE_PEEKDATA` approach currently used.

1. [`process_vm_readv`](https://linux.die.net/man/2/process_vm_readv) - Reads contiguous blocks of a specified size, available since Linux 3.2, so realistically available in every reasonable environment...
2. `/proc/{pid}/mem` - As a fallback in case running in an _ancient_ Linux, we can read from this procfs file, which also allows easy reading of blocks of memory.
3. `PTRACE_PEEKDATA` - The reliable but extremely slow fallback

These 3 methods are probed in the order above until one succeeds, as they all require the same permissions.

Resolves: #72
  • Loading branch information
Jake-Shadle committed Aug 17, 2024
1 parent 6f93cb2 commit d5726d3
Show file tree
Hide file tree
Showing 22 changed files with 564 additions and 302 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- next-header -->
## [Unreleased] - ReleaseDate
### Changed
- [PR#118](https://github.com/rust-minidump/minidump-writer/pull/118) resolved [#72](https://github.com/rust-minidump/minidump-writer/issues/72) by adding support for reading process memory via `process_vm_readv` and `/proc/{pid}/mem`, in addition to the original `PTRACE_PEEKDATA`. This gives significant performance benefits as memory can now be read in blocks of arbitrary size instead of word-by-word with ptrace.

## [0.9.0] - 2024-07-20
### Fixed
- [PR#117](https://github.com/rust-minidump/minidump-writer/pull/117) resolved [#79](https://github.com/rust-minidump/minidump-writer/issues/79) by enabling reading of a module's build id and soname directly from the mapped process rather than relying on file reading, though that is still used as a fallback.
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ nix = { version = "0.28", default-features = false, features = [
"process",
"ptrace",
"signal",
"uio",
"user",
] }
# Used for parsing procfs info.
Expand Down
66 changes: 53 additions & 13 deletions src/bin/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,63 @@ mod linux {
}

fn test_copy_from_process(stack_var: usize, heap_var: usize) -> Result<()> {
use minidump_writer::mem_reader::MemReader;

let ppid = getppid().as_raw();
let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?;
dumper.suspend_threads()?;
let stack_res = PtraceDumper::copy_from_process(ppid, stack_var as *mut libc::c_void, 1)?;

let expected_stack: libc::c_long = 0x11223344;
test!(
stack_res == expected_stack.to_ne_bytes(),
"stack var not correct"
)?;
// We support 3 different methods of reading memory from another
// process, ensure they all function and give the same results

let expected_stack = 0x11223344usize.to_ne_bytes();
let expected_heap = 0x55667788usize.to_ne_bytes();

let validate = |reader: &mut MemReader| -> Result<()> {
let mut val = [0u8; std::mem::size_of::<usize>()];
let read = reader.read(stack_var, &mut val)?;
assert_eq!(read, val.len());
test!(val == expected_stack, "stack var not correct")?;

let read = reader.read(heap_var, &mut val)?;
assert_eq!(read, val.len());
test!(val == expected_heap, "heap var not correct")?;

Ok(())
};

// virtual mem
{
let mut mr = MemReader::for_virtual_mem(ppid);
validate(&mut mr)
.map_err(|err| format!("failed to validate memory for {mr:?}: {err}"))?;
}

// file
{
let mut mr = MemReader::for_file(ppid)
.map_err(|err| format!("failed to open `/proc/{ppid}/mem`: {err}"))?;
validate(&mut mr)
.map_err(|err| format!("failed to validate memory for {mr:?}: {err}"))?;
}

// ptrace
{
let mut mr = MemReader::for_ptrace(ppid);
validate(&mut mr)
.map_err(|err| format!("failed to validate memory for {mr:?}: {err}"))?;
}

let stack_res =
PtraceDumper::copy_from_process(ppid, stack_var, std::mem::size_of::<usize>())?;

test!(stack_res == expected_stack, "stack var not correct")?;

let heap_res =
PtraceDumper::copy_from_process(ppid, heap_var, std::mem::size_of::<usize>())?;

test!(heap_res == expected_heap, "heap var not correct")?;

let heap_res = PtraceDumper::copy_from_process(ppid, heap_var as *mut libc::c_void, 1)?;
let expected_heap: libc::c_long = 0x55667788;
test!(
heap_res == expected_heap.to_ne_bytes(),
"heap var not correct"
)?;
dumper.resume_threads()?;
Ok(())
}
Expand Down Expand Up @@ -137,7 +177,7 @@ mod linux {
found_linux_gate = true;
dumper.suspend_threads()?;
let module_reader::BuildId(id) =
dumper.from_process_memory_for_mapping(&mapping)?;
PtraceDumper::from_process_memory_for_mapping(&mapping, ppid)?;
test!(!id.is_empty(), "id-vec is empty")?;
test!(id.iter().any(|&x| x > 0), "all id elements are 0")?;
dumper.resume_threads()?;
Expand Down
2 changes: 2 additions & 0 deletions src/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ mod dso_debug;
mod dumper_cpu_info;
pub mod errors;
pub mod maps_reader;
pub mod mem_reader;
pub mod minidump_writer;
pub mod module_reader;
pub mod ptrace_dumper;
pub(crate) mod sections;
pub mod thread_info;

pub use maps_reader::LINUX_GATE_LIBRARY_NAME;
pub type Pid = i32;
64 changes: 27 additions & 37 deletions src/linux/android.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
use crate::errors::AndroidError;
use crate::maps_reader::MappingInfo;
use crate::ptrace_dumper::PtraceDumper;
use crate::thread_info::Pid;
use crate::Pid;
use goblin::elf;
#[cfg(target_pointer_width = "32")]
use goblin::elf::dynamic::dyn32::{Dyn, SIZEOF_DYN};
#[cfg(target_pointer_width = "64")]
use goblin::elf::dynamic::dyn64::{Dyn, SIZEOF_DYN};
#[cfg(target_pointer_width = "32")]
use goblin::elf::header::header32 as elf_header;
#[cfg(target_pointer_width = "64")]
use goblin::elf::header::header64 as elf_header;
#[cfg(target_pointer_width = "32")]
use goblin::elf::program_header::program_header32::ProgramHeader;
#[cfg(target_pointer_width = "64")]
use goblin::elf::program_header::program_header64::ProgramHeader;
use std::ffi::c_void;

type Result<T> = std::result::Result<T, AndroidError>;
cfg_if::cfg_if! {
if #[cfg(target_pointer_width = "32")] {
use elf::dynamic::dyn32::{Dyn, SIZEOF_DYN};
use elf::header::header32 as elf_header;
use elf::program_header::program_header32::ProgramHeader;

const DT_ANDROID_REL: u32 = (elf::dynamic::DT_LOOS + 2) as u32;
const DT_ANDROID_RELA: u32 = (elf::dynamic::DT_LOOS + 4) as u32;
} else if #[cfg(target_pointer_width = "64")] {
use elf::dynamic::dyn64::{Dyn, SIZEOF_DYN};
use elf::header::header64 as elf_header;
use elf::program_header::program_header64::ProgramHeader;

const DT_ANDROID_REL: u64 = elf::dynamic::DT_LOOS + 2;
const DT_ANDROID_RELA: u64 = elf::dynamic::DT_LOOS + 4;
} else {
compile_error!("invalid pointer width");
}
}

// From /usr/include/elf.h of the android SDK
// #define DT_ANDROID_REL (DT_LOOS + 2)
// #define DT_ANDROID_RELSZ (DT_LOOS + 3)
// #define DT_ANDROID_RELA (DT_LOOS + 4)
// #define DT_ANDROID_RELASZ (DT_LOOS + 5)
#[cfg(target_pointer_width = "64")]
const DT_ANDROID_REL: u64 = elf::dynamic::DT_LOOS + 2;
#[cfg(target_pointer_width = "64")]
const DT_ANDROID_RELA: u64 = elf::dynamic::DT_LOOS + 4;
#[cfg(target_pointer_width = "32")]
const DT_ANDROID_REL: u32 = (elf::dynamic::DT_LOOS + 2) as u32;
#[cfg(target_pointer_width = "32")]
const DT_ANDROID_RELA: u32 = (elf::dynamic::DT_LOOS + 4) as u32;
type Result<T> = std::result::Result<T, AndroidError>;

struct DynVaddresses {
min_vaddr: usize,
Expand All @@ -42,7 +35,7 @@ struct DynVaddresses {
fn has_android_packed_relocations(pid: Pid, load_bias: usize, vaddrs: DynVaddresses) -> Result<()> {
let dyn_addr = load_bias + vaddrs.dyn_vaddr;
for idx in 0..vaddrs.dyn_count {
let addr = (dyn_addr + SIZEOF_DYN * idx) as *mut c_void;
let addr = dyn_addr + SIZEOF_DYN * idx;
let dyn_data = PtraceDumper::copy_from_process(pid, addr, SIZEOF_DYN)?;
// TODO: Couldn't find a nice way to use goblin for that, to avoid the unsafe-block
let dyn_obj: Dyn;
Expand Down Expand Up @@ -85,7 +78,7 @@ fn parse_loaded_elf_program_headers(

let phdr_opt = PtraceDumper::copy_from_process(
pid,
phdr_addr as *mut c_void,
phdr_addr,
elf_header::SIZEOF_EHDR * ehdr.e_phnum as usize,
);
if let Ok(ph_data) = phdr_opt {
Expand Down Expand Up @@ -120,13 +113,10 @@ pub fn late_process_mappings(pid: Pid, mappings: &mut [MappingInfo]) -> Result<(
.iter_mut()
.filter(|m| m.is_executable() && m.name_is_path())
{
let ehdr_opt = PtraceDumper::copy_from_process(
pid,
map.start_address as *mut c_void,
elf_header::SIZEOF_EHDR,
)
.ok()
.and_then(|x| elf_header::Header::parse(&x).ok());
let ehdr_opt =
PtraceDumper::copy_from_process(pid, map.start_address, elf_header::SIZEOF_EHDR)
.ok()
.and_then(|x| elf_header::Header::parse(&x).ok());

if let Some(ehdr) = ehdr_opt {
if ehdr.e_type == elf_header::ET_DYN {
Expand Down
2 changes: 1 addition & 1 deletion src/linux/auxv/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub use reader::ProcfsAuxvIter;
use {
crate::linux::thread_info::Pid,
crate::Pid,
std::{fs::File, io::BufReader},
thiserror::Error,
};
Expand Down
31 changes: 9 additions & 22 deletions src/linux/dso_debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,7 @@ pub fn write_dso_debug_stream(
.get_program_header_address()
.ok_or(SectionDsoDebugError::CouldNotFind("AT_PHDR in auxv"))? as usize;

let ph = PtraceDumper::copy_from_process(
blamed_thread,
phdr as *mut libc::c_void,
SIZEOF_PHDR * phnum_max,
)?;
let ph = PtraceDumper::copy_from_process(blamed_thread, phdr, SIZEOF_PHDR * phnum_max)?;
let program_headers;
#[cfg(target_pointer_width = "64")]
{
Expand Down Expand Up @@ -137,7 +133,7 @@ pub fn write_dso_debug_stream(
loop {
let dyn_data = PtraceDumper::copy_from_process(
blamed_thread,
(dyn_addr as usize + dynamic_length) as *mut libc::c_void,
dyn_addr as usize + dynamic_length,
dyn_size,
)?;
dynamic_length += dyn_size;
Expand All @@ -163,11 +159,8 @@ pub fn write_dso_debug_stream(
// See <link.h> for a more detailed discussion of the how the dynamic
// loader communicates with debuggers.

let debug_entry_data = PtraceDumper::copy_from_process(
blamed_thread,
r_debug as *mut libc::c_void,
std::mem::size_of::<RDebug>(),
)?;
let debug_entry_data =
PtraceDumper::copy_from_process(blamed_thread, r_debug, std::mem::size_of::<RDebug>())?;

// goblin::elf::Dyn doesn't have padding bytes
let (head, body, _tail) = unsafe { debug_entry_data.align_to::<RDebug>() };
Expand All @@ -180,7 +173,7 @@ pub fn write_dso_debug_stream(
while curr_map != 0 {
let link_map_data = PtraceDumper::copy_from_process(
blamed_thread,
curr_map as *mut libc::c_void,
curr_map,
std::mem::size_of::<LinkMap>(),
)?;

Expand All @@ -204,11 +197,8 @@ pub fn write_dso_debug_stream(
for (idx, map) in dso_vec.iter().enumerate() {
let mut filename = String::new();
if map.l_name > 0 {
let filename_data = PtraceDumper::copy_from_process(
blamed_thread,
map.l_name as *mut libc::c_void,
256,
)?;
let filename_data =
PtraceDumper::copy_from_process(blamed_thread, map.l_name, 256)?;

// C - string is NULL-terminated
if let Some(name) = filename_data.splitn(2, |x| *x == b'\0').next() {
Expand Down Expand Up @@ -243,11 +233,8 @@ pub fn write_dso_debug_stream(
};

dirent.location.data_size += dynamic_length as u32;
let dso_debug_data = PtraceDumper::copy_from_process(
blamed_thread,
dyn_addr as *mut libc::c_void,
dynamic_length,
)?;
let dso_debug_data =
PtraceDumper::copy_from_process(blamed_thread, dyn_addr as usize, dynamic_length)?;
MemoryArrayWriter::write_bytes(buffer, &dso_debug_data);

Ok(dirent)
Expand Down
28 changes: 19 additions & 9 deletions src/linux/errors.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use crate::auxv::AuxvError;
use crate::dir_section::FileWriterError;
use crate::maps_reader::MappingInfo;
use crate::mem_writer::MemoryWriterError;
use crate::thread_info::Pid;
use crate::{
dir_section::FileWriterError, maps_reader::MappingInfo, mem_writer::MemoryWriterError, Pid,
};
use goblin;
use nix::errno::Errno;
use std::ffi::OsString;
Expand All @@ -11,7 +9,7 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub enum InitError {
#[error("failed to read auxv")]
ReadAuxvFailed(AuxvError),
ReadAuxvFailed(crate::auxv::AuxvError),
#[error("IO error for file {0}")]
IOError(String, #[source] std::io::Error),
#[error("crash thread does not reference principal mapping")]
Expand All @@ -20,6 +18,8 @@ pub enum InitError {
AndroidLateInitError(#[from] AndroidError),
#[error("Failed to read the page size")]
PageSizeError(#[from] Errno),
#[error("Ptrace does not function within the same process")]
CannotPtraceSameProcess,
}

#[derive(Error, Debug)]
Expand Down Expand Up @@ -86,6 +86,16 @@ pub enum AndroidError {
NoRelFound,
}

#[derive(Debug, Error)]
#[error("Copy from process {child} failed (source {src}, offset: {offset}, length: {length})")]
pub struct CopyFromProcessError {
pub child: Pid,
pub src: usize,
pub offset: usize,
pub length: usize,
pub source: nix::Error,
}

#[derive(Debug, Error)]
pub enum DumperError {
#[error("Failed to get PAGE_SIZE from system")]
Expand All @@ -96,8 +106,8 @@ pub enum DumperError {
PtraceAttachError(Pid, #[source] nix::Error),
#[error("nix::ptrace::detach(Pid={0}) failed")]
PtraceDetachError(Pid, #[source] nix::Error),
#[error("Copy from process {0} failed (source {1}, offset: {2}, length: {3})")]
CopyFromProcessError(Pid, usize, usize, usize, #[source] nix::Error),
#[error(transparent)]
CopyFromProcessError(#[from] CopyFromProcessError),
#[error("Skipped thread {0} due to it being part of the seccomp sandbox's trusted code")]
DetachSkippedThread(Pid),
#[error("No threads left to suspend out of {0}")]
Expand Down Expand Up @@ -249,7 +259,7 @@ pub enum ModuleReaderError {
offset: u64,
length: u64,
#[source]
error: std::io::Error,
error: nix::Error,
},
#[error("failed to parse ELF memory: {0}")]
Parsing(#[from] goblin::error::Error),
Expand Down
2 changes: 1 addition & 1 deletion src/linux/maps_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ impl MappingInfo {
use super::module_reader::{ReadFromModule, SoName};

let mapped_file = MappingInfo::get_mmap(&self.name, self.offset)?;
Ok(SoName::read_from_module(&*mapped_file)
Ok(SoName::read_from_module((&*mapped_file).into())
.map_err(|e| MapsReaderError::NoSoName(self.name.clone().unwrap_or_default(), e))?
.0
.to_string())
Expand Down
Loading

0 comments on commit d5726d3

Please sign in to comment.