diff --git a/CHANGELOG.md b/CHANGELOG.md index 672ce3b8..3a024f9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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. diff --git a/Cargo.toml b/Cargo.toml index 5e4212bd..648f4e49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ nix = { version = "0.28", default-features = false, features = [ "process", "ptrace", "signal", + "uio", "user", ] } # Used for parsing procfs info. diff --git a/src/bin/test.rs b/src/bin/test.rs index 82c31d31..bb064554 100644 --- a/src/bin/test.rs +++ b/src/bin/test.rs @@ -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::()]; + 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::())?; + + test!(stack_res == expected_stack, "stack var not correct")?; + + let heap_res = + PtraceDumper::copy_from_process(ppid, heap_var, std::mem::size_of::())?; + + 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(()) } @@ -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()?; diff --git a/src/linux.rs b/src/linux.rs index d1f666d9..febdeabe 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -10,6 +10,7 @@ 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; @@ -17,3 +18,4 @@ pub(crate) mod sections; pub mod thread_info; pub use maps_reader::LINUX_GATE_LIBRARY_NAME; +pub type Pid = i32; diff --git a/src/linux/android.rs b/src/linux/android.rs index 05e7d4ac..18d35443 100644 --- a/src/linux/android.rs +++ b/src/linux/android.rs @@ -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 = std::result::Result; +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 = std::result::Result; struct DynVaddresses { min_vaddr: usize, @@ -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; @@ -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 { @@ -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 { diff --git a/src/linux/auxv/mod.rs b/src/linux/auxv/mod.rs index c8ee248a..403ab114 100644 --- a/src/linux/auxv/mod.rs +++ b/src/linux/auxv/mod.rs @@ -1,6 +1,6 @@ pub use reader::ProcfsAuxvIter; use { - crate::linux::thread_info::Pid, + crate::Pid, std::{fs::File, io::BufReader}, thiserror::Error, }; diff --git a/src/linux/dso_debug.rs b/src/linux/dso_debug.rs index c2f873b0..ef27dd5a 100644 --- a/src/linux/dso_debug.rs +++ b/src/linux/dso_debug.rs @@ -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")] { @@ -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; @@ -163,11 +159,8 @@ pub fn write_dso_debug_stream( // See 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::(), - )?; + let debug_entry_data = + PtraceDumper::copy_from_process(blamed_thread, r_debug, std::mem::size_of::())?; // goblin::elf::Dyn doesn't have padding bytes let (head, body, _tail) = unsafe { debug_entry_data.align_to::() }; @@ -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::(), )?; @@ -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() { @@ -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) diff --git a/src/linux/errors.rs b/src/linux/errors.rs index e94c0cce..f8a19cc8 100644 --- a/src/linux/errors.rs +++ b/src/linux/errors.rs @@ -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; @@ -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")] @@ -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)] @@ -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")] @@ -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}")] @@ -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), diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 8b89e873..e023a21a 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -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()) diff --git a/src/linux/mem_reader.rs b/src/linux/mem_reader.rs new file mode 100644 index 00000000..1e9fc87e --- /dev/null +++ b/src/linux/mem_reader.rs @@ -0,0 +1,277 @@ +//! Functionality for reading a remote process's memory + +use crate::{errors::CopyFromProcessError, ptrace_dumper::PtraceDumper, Pid}; + +enum Style { + /// Uses [`process_vm_readv`](https://linux.die.net/man/2/process_vm_readv) + /// to read the memory. + /// + /// This is not available on old <3.2 (really, ancient) kernels, and requires + /// the same permissions as ptrace + VirtualMem, + /// Reads the memory from `/proc//mem` + /// + /// Available on basically all versions of Linux, but could fail if the process + /// has insufficient privileges, ie ptrace + File(std::fs::File), + /// Reads the memory with [ptrace (`PTRACE_PEEKDATA`)](https://man7.org/linux/man-pages/man2/ptrace.2.html) + /// + /// Reads data one word at a time, so slow, but fairly reliable, as long as + /// the process can be ptraced + Ptrace, + /// No methods succeeded, generally there isn't a case where failing a syscall + /// will work if called again + Unavailable { + vmem: nix::Error, + file: nix::Error, + ptrace: nix::Error, + }, +} + +pub struct MemReader { + /// The pid of the child to read + pid: nix::unistd::Pid, + style: Option