diff --git a/Cargo.lock b/Cargo.lock
index ba06cd2..b9f31b9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1432,6 +1432,7 @@ dependencies = [
"tempfile",
"thiserror 2.0.18",
"uuid",
+ "windows-sys 0.52.0",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 6976ffc..9a005f0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,6 +13,10 @@ license = "MIT"
bitflags = "2.10"
cfg-if = "1.0"
crash-context = "0.6"
+error-graph = { version = "0.1.1", features = ["serde"] }
+failspot = "0.2.0"
+# goblin >= 0.10 uses scroll 0.13
+goblin = "0.9.2"
log = "0.4"
# Type definitions and utilities for working with the Minidump format
minidump-common = "0.26"
@@ -24,8 +28,6 @@ thiserror = "2.0"
[target.'cfg(unix)'.dependencies]
libc = "0.2"
-# goblin >= 0.10 uses scroll 0.13
-goblin = "0.9"
memmap2 = "0.9"
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
@@ -46,6 +48,11 @@ procfs-core = { version = "0.18", default-features = false, features = ["serde1"
[target.'cfg(target_os = "windows")'.dependencies]
bitflags = "2.4"
+windows-sys = { version = ">=0.52", features = [
+ "Win32_Foundation",
+ "Win32_System_Diagnostics_Debug",
+ "Win32_System_ProcessStatus",
+] }
[target.'cfg(target_os = "macos")'.dependencies]
# Binds some additional mac specifics not in libc, note that other dependents
diff --git a/src/bin/test.rs b/src/bin/test.rs
index c42ff7f..2681343 100644
--- a/src/bin/test.rs
+++ b/src/bin/test.rs
@@ -77,7 +77,7 @@ mod linux {
}
fn test_copy_from_process(stack_var: usize, heap_var: usize) -> Result<()> {
- use minidump_writer::mem_reader::MemReader;
+ use minidump_writer::process_reader::ProcessReader;
let ppid = getppid().as_raw();
let dumper = fail_on_soft_error!(
@@ -91,7 +91,7 @@ mod linux {
let expected_stack = 0x11223344usize.to_ne_bytes();
let expected_heap = 0x55667788usize.to_ne_bytes();
- let validate = |reader: &mut MemReader| -> Result<()> {
+ let validate = |reader: &mut ProcessReader| -> Result<()> {
let mut val = [0u8; std::mem::size_of::()];
let read = reader.read(stack_var, &mut val)?;
assert_eq!(read, val.len());
@@ -106,14 +106,14 @@ mod linux {
// virtual mem
{
- let mut mr = MemReader::for_virtual_mem(ppid);
+ let mut mr = ProcessReader::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)
+ let mut mr = ProcessReader::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}"))?;
@@ -121,7 +121,7 @@ mod linux {
// ptrace
{
- let mut mr = MemReader::for_ptrace(ppid);
+ let mut mr = ProcessReader::for_ptrace(ppid);
validate(&mut mr)
.map_err(|err| format!("failed to validate memory for {mr:?}: {err}"))?;
}
diff --git a/src/lib.rs b/src/lib.rs
index baeb1fc..f941625 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -28,5 +28,7 @@ pub mod dir_section;
pub mod mem_writer;
pub mod minidump_cpu;
pub mod minidump_format;
+pub mod module_reader;
+pub mod process_reader;
mod serializers;
diff --git a/src/linux/android.rs b/src/linux/android.rs
index 94c89ef..799d349 100644
--- a/src/linux/android.rs
+++ b/src/linux/android.rs
@@ -1,7 +1,7 @@
use {
super::{
- Pid, maps_reader::MappingInfo, mem_reader::CopyFromProcessError,
- minidump_writer::MinidumpWriter,
+ Pid, maps_reader::MappingInfo, minidump_writer::MinidumpWriter,
+ process_reader::CopyFromProcessError,
},
goblin::elf,
};
diff --git a/src/linux/dso_debug.rs b/src/linux/dso_debug.rs
index ab84da4..47e530d 100644
--- a/src/linux/dso_debug.rs
+++ b/src/linux/dso_debug.rs
@@ -1,6 +1,6 @@
use {
super::{
- auxv::AuxvDumpInfo, mem_reader::CopyFromProcessError, minidump_writer::MinidumpWriter,
+ auxv::AuxvDumpInfo, minidump_writer::MinidumpWriter, process_reader::CopyFromProcessError,
serializers::*,
},
crate::{
diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs
index d0370c8..af8001f 100644
--- a/src/linux/maps_reader.rs
+++ b/src/linux/maps_reader.rs
@@ -4,7 +4,10 @@ use {
byteorder::{NativeEndian, ReadBytesExt},
goblin::elf,
memmap2::{Mmap, MmapOptions},
- procfs_core::process::{MMPermissions, MMapPath, MemoryMaps},
+ procfs_core::{
+ FromRead,
+ process::{MMPermissions, MMapPath, MemoryMaps},
+ },
std::{
ffi::{OsStr, OsString},
fs::File,
@@ -96,6 +99,14 @@ pub enum MapsReaderError {
MmapSanityCheckFailed,
#[error("Symlink does not match ({0} vs. {1})")]
SymlinkError(std::path::PathBuf, std::path::PathBuf),
+ #[error("Mappings file missing for pid {pid} (is the process still alive?)")]
+ MappingFileMissing { pid: i32 },
+ #[error("Failed to parse memory maps file")]
+ ParsingError(
+ #[from]
+ #[serde(serialize_with = "serialize_proc_error")]
+ procfs_core::ProcError,
+ ),
}
fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool {
@@ -117,6 +128,20 @@ fn sanitize_path(pathname: OsString) -> OsString {
}
impl MappingInfo {
+ /// Get the mappings for the given process.
+ pub fn for_pid(pid: i32, linux_gate_loc: Option) -> Result> {
+ let maps_path = format!("/proc/{}/maps", pid);
+ let maps_file = File::open(&maps_path).map_err(|e| {
+ if e.kind() == std::io::ErrorKind::NotFound {
+ MapsReaderError::MappingFileMissing { pid }
+ } else {
+ e.into()
+ }
+ })?;
+ let maps = MemoryMaps::from_read(maps_file)?;
+ Self::aggregate(maps, linux_gate_loc)
+ }
+
/// Return whether the `name` field is a path (contains a `/`).
pub fn name_is_path(&self) -> bool {
is_mapping_a_path(self.name.as_deref())
diff --git a/src/linux/minidump_writer/errors.rs b/src/linux/minidump_writer/errors.rs
index 6bea5ff..f77f13b 100644
--- a/src/linux/minidump_writer/errors.rs
+++ b/src/linux/minidump_writer/errors.rs
@@ -200,12 +200,6 @@ pub enum InitError {
EnumerateThreadsErrors(#[source] ErrorList),
#[error("Failed to enumerate threads")]
EnumerateThreadsFailed(#[source] Box),
- #[error("Failed to read process map file")]
- ReadProcessMapFileFailed(
- #[source]
- #[serde(serialize_with = "serialize_proc_error")]
- ProcError,
- ),
#[error("Failed to aggregate process mappings")]
AggregateMappingsFailed(#[source] MapsReaderError),
#[error("Failed to enumerate process mappings")]
diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs
index f388b2e..b86593c 100644
--- a/src/linux/minidump_writer/mod.rs
+++ b/src/linux/minidump_writer/mod.rs
@@ -7,8 +7,7 @@ use {
dso_debug,
dumper_cpu_info::CpuInfoError,
maps_reader::{MappingInfo, MappingList, MapsReaderError},
- mem_reader::CopyFromProcessError,
- module_reader,
+ process_reader::{CopyFromProcessError, ProcessReader},
serializers::*,
thread_info::{ThreadInfo, ThreadInfoError},
},
@@ -18,6 +17,7 @@ use {
Buffer, MemoryArrayWriter, MemoryWriter, MemoryWriterError, write_string_to_location,
},
minidump_format::*,
+ module_reader,
serializers::*,
},
error_graph::{ErrorList, WriteErrorList},
@@ -720,14 +720,7 @@ impl MinidumpWriter {
// case its entry when creating the list of mappings.
// See http://www.trilithium.com/johan/2005/08/linux-gate/ for more
// information.
- let maps_path = format!("/proc/{}/maps", self.process_id);
- let maps_file =
- std::fs::File::open(&maps_path).map_err(|e| InitError::IOError(maps_path, e))?;
-
- let maps = procfs_core::process::MemoryMaps::from_read(maps_file)
- .map_err(InitError::ReadProcessMapFileFailed)?;
-
- self.mappings = MappingInfo::aggregate(maps, self.auxv.get_linux_gate_address())
+ self.mappings = MappingInfo::for_pid(self.process_id, self.auxv.get_linux_gate_address())
.map_err(InitError::AggregateMappingsFailed)?;
// Although the initial executable is usually the first mapping, it's not
@@ -953,10 +946,34 @@ impl MinidumpWriter {
mapping: &MappingInfo,
pid: Pid,
) -> Result {
+ let reader = ProcessReader::new(pid);
Ok(T::read_from_module(
- module_reader::ProcessReader::new(pid, mapping.start_address).into(),
+ module_reader::ModuleMemory::from_process(&reader, mapping.start_address),
)?)
}
+
+ /// Copies a block of bytes from the target process, returning the heap
+ /// allocated copy
+ #[inline]
+ pub fn copy_from_process(
+ pid: Pid,
+ src: usize,
+ length: usize,
+ ) -> Result, CopyFromProcessError> {
+ let length = std::num::NonZeroUsize::new(length).ok_or(CopyFromProcessError {
+ src,
+ child: pid,
+ offset: 0,
+ length,
+ // TODO: We should make copy_from_process also take a NonZero,
+ // as EINVAL could also come from the syscalls that actually read
+ // memory as well which could be confusing
+ source: nix::errno::Errno::EINVAL,
+ })?;
+
+ let mem = ProcessReader::new(pid);
+ mem.read_to_vec(src, length)
+ }
}
impl Drop for MinidumpWriter {
diff --git a/src/linux/minidump_writer/thread_list_stream.rs b/src/linux/minidump_writer/thread_list_stream.rs
index 9fa573a..0e1b525 100644
--- a/src/linux/minidump_writer/thread_list_stream.rs
+++ b/src/linux/minidump_writer/thread_list_stream.rs
@@ -96,8 +96,9 @@ impl MinidumpWriter {
// we used the actual state of the thread we would find it running in the
// signal handler with the alternative stack, which would be deeply
// unhelpful.
- if self.crash_context.is_some() && thread.thread_id == self.blamed_thread as u32 {
- let crash_context = self.crash_context.as_ref().unwrap();
+ if let Some(crash_context) = &self.crash_context
+ && thread.thread_id == self.blamed_thread as u32
+ {
let instruction_ptr = crash_context.get_instruction_pointer();
let stack_pointer = crash_context.get_stack_pointer();
self.fill_thread_stack(
diff --git a/src/linux/mod.rs b/src/linux/mod.rs
index 9e7b46b..68b870b 100644
--- a/src/linux/mod.rs
+++ b/src/linux/mod.rs
@@ -9,9 +9,9 @@ pub mod crash_context;
mod dso_debug;
mod dumper_cpu_info;
pub mod maps_reader;
-pub mod mem_reader;
pub mod minidump_writer;
pub mod module_reader;
+pub mod process_reader;
mod serializers;
pub mod thread_info;
diff --git a/src/linux/module_reader.rs b/src/linux/module_reader.rs
index 2eb521e..c0f7aef 100644
--- a/src/linux/module_reader.rs
+++ b/src/linux/module_reader.rs
@@ -1,5 +1,6 @@
use {
- super::{mem_reader::MemReader, serializers::*},
+ super::serializers::*,
+ crate::module_reader::{ModuleMemory, ModuleMemoryReadError},
crate::{minidump_format::GUID, serializers::*},
goblin::{
container::{Container, Ctx, Endian},
@@ -8,16 +9,10 @@ use {
std::{borrow::Cow, ffi::CStr},
};
-type Buf<'buf> = Cow<'buf, [u8]>;
type Error = ModuleReaderError;
const NOTE_SECTION_NAME: &[u8] = b".note.gnu.build-id\0";
-pub struct ProcessReader {
- inner: MemReader,
- start_address: u64,
-}
-
#[derive(Debug, thiserror::Error, serde::Serialize)]
pub enum ModuleReaderError {
#[error("failed to read module file ({path}): {error}")]
@@ -27,15 +22,8 @@ pub enum ModuleReaderError {
#[serde(serialize_with = "serialize_io_error")]
error: std::io::Error,
},
- #[error("failed to read module memory: {length} bytes at {offset}{}: {error}", .start_address.map(|addr| format!(" (start address: {addr})")).unwrap_or_default())]
- ReadModuleMemory {
- offset: u64,
- length: u64,
- start_address: Option,
- #[source]
- #[serde(serialize_with = "serialize_nix_error")]
- error: nix::Error,
- },
+ #[error(transparent)]
+ ReadModuleMemory(#[from] ModuleMemoryReadError),
#[error("failed to parse ELF memory: {0}")]
Parsing(
#[from]
@@ -84,80 +72,9 @@ pub enum ModuleReaderError {
},
}
-impl ProcessReader {
- pub fn new(pid: i32, start_address: usize) -> Self {
- Self {
- inner: MemReader::new(pid),
- start_address: start_address as u64,
- }
- }
-}
-
-pub enum ProcessMemory<'buf> {
- Slice(&'buf [u8]),
- Process(ProcessReader),
-}
-
-impl<'buf> From<&'buf [u8]> for ProcessMemory<'buf> {
- fn from(value: &'buf [u8]) -> Self {
- Self::Slice(value)
- }
-}
-
-impl From for ProcessMemory<'_> {
- fn from(value: ProcessReader) -> Self {
- Self::Process(value)
- }
-}
-
-impl<'buf> ProcessMemory<'buf> {
- #[inline]
- fn read(&mut self, offset: u64, length: u64) -> Result, Error> {
- let error = move |start_address, error| Error::ReadModuleMemory {
- start_address,
- offset,
- length,
- error,
- };
-
- match self {
- Self::Process(pr) => {
- let error = |e| error(Some(pr.start_address), e);
- let len = std::num::NonZeroUsize::new(length as usize)
- .ok_or_else(|| error(nix::Error::EINVAL))?;
- let proc_offset = pr
- .start_address
- .checked_add(offset)
- .ok_or_else(|| error(nix::Error::EOVERFLOW))?;
- pr.inner
- .read_to_vec(proc_offset as _, len)
- .map(Cow::Owned)
- .map_err(|err| error(err.source))
- }
- Self::Slice(s) => {
- let error = |e| error(None, e);
- let end = offset
- .checked_add(length)
- .ok_or_else(|| error(nix::Error::EOVERFLOW))?;
- s.get(offset as usize..end as usize)
- .map(Cow::Borrowed)
- .ok_or_else(|| error(nix::Error::EACCES))
- }
- }
- }
-
- /// Calculates the absolute address of the specified relative address
- #[inline]
- fn absolute(&self, addr: u64) -> u64 {
- let Self::Process(pr) = self else {
- return addr;
- };
- addr.checked_sub(pr.start_address).unwrap_or(addr)
- }
-
- #[inline]
- fn is_process_memory(&self) -> bool {
- matches!(self, Self::Process(_))
+impl crate::module_reader::ModuleMemory<'_> {
+ pub fn read_from_module(self) -> Result {
+ T::read_from_module(self)
}
}
@@ -192,7 +109,7 @@ fn section_header_with_name<'sc>(
section_headers: &'sc elf::SectionHeaders,
strtab_index: usize,
name: &[u8],
- module_memory: &mut ProcessMemory<'_>,
+ module_memory: &ModuleMemory<'_>,
) -> Result