Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support faster methods of reading process memory #118

Merged
merged 11 commits into from
Aug 17, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
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
Loading