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