diff --git a/Cargo.lock b/Cargo.lock
index 4f8719c7..04757f8f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -160,6 +160,9 @@ name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+dependencies = [
+ "serde",
+]
[[package]]
name = "block-buffer"
@@ -600,6 +603,24 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "error-graph"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b920e777967421aa5f9bf34f842c0ab6ba19b3bdb4a082946093860f5858879"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "failspot"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c942e64b20ecd39933d5ff938ca4fdb6ef0d298cc3855b231179a5ef0b24948d"
+dependencies = [
+ "flagset",
+]
+
[[package]]
name = "fallible-iterator"
version = "0.2.0"
@@ -618,6 +639,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+[[package]]
+name = "flagset"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec"
+
[[package]]
name = "flate2"
version = "1.0.35"
@@ -1335,7 +1362,7 @@ dependencies = [
"memmap2",
"minidump-common",
"num-traits",
- "procfs-core 0.17.0",
+ "procfs-core",
"range-map",
"scroll 0.12.0",
"thiserror 1.0.69",
@@ -1389,6 +1416,8 @@ dependencies = [
"crash-context",
"current_platform",
"dump_syms",
+ "error-graph",
+ "failspot",
"futures",
"goblin",
"libc",
@@ -1400,8 +1429,10 @@ dependencies = [
"minidump-common",
"minidump-unwind",
"nix",
- "procfs-core 0.16.0",
+ "procfs-core",
"scroll 0.12.0",
+ "serde",
+ "serde_json",
"similar-asserts",
"tempfile",
"thiserror 1.0.69",
@@ -1698,16 +1729,6 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "procfs-core"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29"
-dependencies = [
- "bitflags 2.6.0",
- "hex",
-]
-
[[package]]
name = "procfs-core"
version = "0.17.0"
@@ -1716,6 +1737,7 @@ checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
dependencies = [
"bitflags 2.6.0",
"hex",
+ "serde",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index ae3afe34..e87d1caa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,10 +13,14 @@ bitflags = "2.4"
byteorder = "1.4"
cfg-if = "1.0"
crash-context = "0.6"
+error-graph = { version = "0.1.1", features = ["serde"] }
+failspot = "0.2.0"
log = "0.4"
memoffset = "0.9"
minidump-common = "0.24"
scroll = "0.12"
+serde = { version = "1.0.208", features = ["derive"] }
+serde_json = "1.0.116"
tempfile = "3.8"
thiserror = "1.0"
@@ -36,7 +40,7 @@ nix = { version = "0.29", default-features = false, features = [
] }
# Used for parsing procfs info.
# default-features is disabled since it pulls in chrono
-procfs-core = { version = "0.16", default-features = false }
+procfs-core = { version = "0.17", default-features = false, features = ["serde1"] }
[target.'cfg(target_os = "windows")'.dependencies]
bitflags = "2.4"
@@ -49,6 +53,7 @@ mach2 = "0.4"
# We auto-detect what the test binary that is spawned for most tests should be
# compiled for
current_platform = "0.2"
+failspot = { version = "0.2.0", features = ["enabled"] }
# Minidump-processor is async so we need an executor
futures = { version = "0.3", features = ["executor"] }
minidump = "0.24"
diff --git a/src/bin/test.rs b/src/bin/test.rs
index 6713852c..9ab42f13 100644
--- a/src/bin/test.rs
+++ b/src/bin/test.rs
@@ -6,14 +6,17 @@ pub type Result = std::result::Result;
#[cfg(any(target_os = "linux", target_os = "android"))]
mod linux {
- use super::*;
- use minidump_writer::{
- minidump_writer::STOP_TIMEOUT, module_reader, ptrace_dumper::PtraceDumper,
- LINUX_GATE_LIBRARY_NAME,
- };
- use nix::{
- sys::mman::{mmap_anonymous, MapFlags, ProtFlags},
- unistd::getppid,
+ use {
+ super::*,
+ error_graph::ErrorList,
+ minidump_writer::{
+ minidump_writer::STOP_TIMEOUT, module_reader, ptrace_dumper::PtraceDumper,
+ LINUX_GATE_LIBRARY_NAME,
+ },
+ nix::{
+ sys::mman::{mmap_anonymous, MapFlags, ProtFlags},
+ unistd::getppid,
+ },
};
macro_rules! test {
@@ -24,15 +27,40 @@ mod linux {
};
}
+ macro_rules! fail_on_soft_error(($n: ident, $e: expr) => {{
+ let mut $n = ErrorList::default();
+ let __result = $e;
+ if !$n.is_empty() {
+ return Err($n.into());
+ }
+ __result
+ }});
+
fn test_setup() -> Result<()> {
let ppid = getppid();
- PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?;
+ fail_on_soft_error!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ ppid.as_raw(),
+ STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors,
+ )?
+ );
Ok(())
}
fn test_thread_list() -> Result<()> {
let ppid = getppid();
- let dumper = PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?;
+ let dumper = fail_on_soft_error!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ ppid.as_raw(),
+ STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors,
+ )?
+ );
test!(!dumper.threads.is_empty(), "No threads");
test!(
dumper
@@ -59,8 +87,17 @@ mod linux {
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 mut dumper = fail_on_soft_error!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ ppid,
+ STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors
+ )?
+ );
+
+ fail_on_soft_error!(soft_errors, dumper.suspend_threads(&mut soft_errors));
// We support 3 different methods of reading memory from another
// process, ensure they all function and give the same results
@@ -113,13 +150,22 @@ mod linux {
test!(heap_res == expected_heap, "heap var not correct");
- dumper.resume_threads()?;
+ fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors));
+
Ok(())
}
fn test_find_mappings(addr1: usize, addr2: usize) -> Result<()> {
let ppid = getppid();
- let dumper = PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?;
+ let dumper = fail_on_soft_error!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ ppid.as_raw(),
+ STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors,
+ )?
+ );
dumper
.find_mapping(addr1)
.ok_or("No mapping for addr1 found")?;
@@ -136,8 +182,19 @@ mod linux {
let ppid = getppid().as_raw();
let exe_link = format!("/proc/{ppid}/exe");
let exe_name = std::fs::read_link(exe_link)?.into_os_string();
- let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?;
- dumper.suspend_threads()?;
+
+ let mut dumper = fail_on_soft_error!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ ppid,
+ STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors
+ )?
+ );
+
+ fail_on_soft_error!(soft_errors, dumper.suspend_threads(&mut soft_errors));
+
let mut found_exe = None;
for (idx, mapping) in dumper.mappings.iter().enumerate() {
if mapping.name.as_ref().map(|x| x.into()).as_ref() == Some(&exe_name) {
@@ -147,7 +204,9 @@ mod linux {
}
let idx = found_exe.unwrap();
let module_reader::BuildId(id) = dumper.from_process_memory_for_index(idx)?;
- dumper.resume_threads()?;
+
+ fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors));
+
assert!(!id.is_empty());
assert!(id.iter().any(|&x| x > 0));
Ok(())
@@ -155,13 +214,21 @@ mod linux {
fn test_merged_mappings(path: String, mapped_mem: usize, mem_size: usize) -> Result<()> {
// Now check that PtraceDumper interpreted the mappings properly.
- let dumper = PtraceDumper::new(getppid().as_raw(), STOP_TIMEOUT, Default::default())?;
+ let dumper = fail_on_soft_error!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ getppid().as_raw(),
+ STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors,
+ )?
+ );
let mut mapping_count = 0;
for map in &dumper.mappings {
if map
.name
.as_ref()
- .map_or(false, |name| name.to_string_lossy().starts_with(&path))
+ .is_some_and(|name| name.to_string_lossy().starts_with(&path))
{
mapping_count += 1;
// This mapping should encompass the entire original mapped
@@ -177,17 +244,29 @@ mod linux {
fn test_linux_gate_mapping_id() -> Result<()> {
let ppid = getppid().as_raw();
- let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?;
+ let mut dumper = fail_on_soft_error!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ ppid,
+ STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors
+ )?
+ );
let mut found_linux_gate = false;
for mapping in dumper.mappings.clone() {
if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) {
found_linux_gate = true;
- dumper.suspend_threads()?;
+
+ fail_on_soft_error!(soft_errors, dumper.suspend_threads(&mut soft_errors));
+
let module_reader::BuildId(id) =
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()?;
+
+ fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors));
+
break;
}
}
@@ -197,7 +276,15 @@ mod linux {
fn test_mappings_include_linux_gate() -> Result<()> {
let ppid = getppid().as_raw();
- let dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?;
+ let dumper = fail_on_soft_error!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ ppid,
+ STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors
+ )?
+ );
let linux_gate_loc = dumper.auxv.get_linux_gate_address().unwrap();
test!(linux_gate_loc != 0, "linux_gate_loc == 0");
let mut found_linux_gate = false;
@@ -205,7 +292,7 @@ mod linux {
if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) {
found_linux_gate = true;
test!(
- linux_gate_loc == mapping.start_address.try_into()?,
+ usize::try_from(linux_gate_loc)? == mapping.start_address,
"linux_gate_loc != start_address"
);
diff --git a/src/dir_section.rs b/src/dir_section.rs
index ced5a2d1..2aaed65a 100644
--- a/src/dir_section.rs
+++ b/src/dir_section.rs
@@ -1,15 +1,22 @@
-use crate::{
- mem_writer::{Buffer, MemoryArrayWriter, MemoryWriterError},
- minidump_format::MDRawDirectory,
+use {
+ crate::{
+ mem_writer::{Buffer, MemoryArrayWriter, MemoryWriterError},
+ minidump_format::MDRawDirectory,
+ serializers::*,
+ },
+ std::io::{Error, Seek, Write},
};
-use std::io::{Error, Seek, Write};
pub type DumpBuf = Buffer;
-#[derive(Debug, thiserror::Error)]
+#[derive(Debug, thiserror::Error, serde::Serialize)]
pub enum FileWriterError {
#[error("IO error")]
- IOError(#[from] Error),
+ IOError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ Error,
+ ),
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
}
diff --git a/src/lib.rs b/src/lib.rs
index a76291d0..67181b4a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,8 +14,19 @@ cfg_if::cfg_if! {
}
}
+pub mod dir_section;
+pub mod mem_writer;
pub mod minidump_cpu;
pub mod minidump_format;
-pub mod dir_section;
-pub mod mem_writer;
+mod serializers;
+
+failspot::failspot_name! {
+ pub enum FailSpotName {
+ StopProcess,
+ FillMissingAuxvInfo,
+ ThreadName,
+ SuspendThreads,
+ CpuInfoFileOpen,
+ }
+}
diff --git a/src/linux.rs b/src/linux.rs
index febdeabe..dc605084 100644
--- a/src/linux.rs
+++ b/src/linux.rs
@@ -15,6 +15,7 @@ pub mod minidump_writer;
pub mod module_reader;
pub mod ptrace_dumper;
pub(crate) mod sections;
+mod serializers;
pub mod thread_info;
pub use maps_reader::LINUX_GATE_LIBRARY_NAME;
diff --git a/src/linux/auxv/mod.rs b/src/linux/auxv/mod.rs
index 403ab114..527fc45b 100644
--- a/src/linux/auxv/mod.rs
+++ b/src/linux/auxv/mod.rs
@@ -1,6 +1,8 @@
-pub use reader::ProcfsAuxvIter;
use {
- crate::Pid,
+ self::reader::ProcfsAuxvIter,
+ crate::{serializers::*, Pid},
+ error_graph::WriteErrorList,
+ failspot::failspot,
std::{fs::File, io::BufReader},
thiserror::Error,
};
@@ -79,7 +81,11 @@ pub struct AuxvDumpInfo {
}
impl AuxvDumpInfo {
- pub fn try_filling_missing_info(&mut self, pid: Pid) -> Result<(), AuxvError> {
+ pub fn try_filling_missing_info(
+ &mut self,
+ pid: Pid,
+ mut soft_errors: impl WriteErrorList,
+ ) -> Result<(), AuxvError> {
if self.is_complete() {
return Ok(());
}
@@ -87,9 +93,14 @@ impl AuxvDumpInfo {
let auxv_path = format!("/proc/{pid}/auxv");
let auxv_file = File::open(&auxv_path).map_err(|e| AuxvError::OpenError(auxv_path, e))?;
- for AuxvPair { key, value } in
- ProcfsAuxvIter::new(BufReader::new(auxv_file)).filter_map(Result::ok)
- {
+ for pair_result in ProcfsAuxvIter::new(BufReader::new(auxv_file)) {
+ let AuxvPair { key, value } = match pair_result {
+ Ok(pair) => pair,
+ Err(e) => {
+ soft_errors.push(e);
+ continue;
+ }
+ };
let dest_field = match key {
consts::AT_PHNUM => &mut self.program_header_count,
consts::AT_PHDR => &mut self.program_header_address,
@@ -102,6 +113,8 @@ impl AuxvDumpInfo {
}
}
+ failspot!(FillMissingAuxvInfo soft_errors.push(AuxvError::InvalidFormat));
+
Ok(())
}
pub fn get_program_header_count(&self) -> Option {
@@ -124,14 +137,23 @@ impl AuxvDumpInfo {
}
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum AuxvError {
#[error("Failed to open file {0}")]
- OpenError(String, #[source] std::io::Error),
+ OpenError(
+ String,
+ #[source]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
#[error("No auxv entry found for PID {0}")]
NoAuxvEntryFound(Pid),
#[error("Invalid auxv format (should not hit EOF before AT_NULL)")]
InvalidFormat,
#[error("IO Error")]
- IOError(#[from] std::io::Error),
+ IOError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
}
diff --git a/src/linux/dumper_cpu_info/x86_mips.rs b/src/linux/dumper_cpu_info/x86_mips.rs
index 0c03d8e5..025a97be 100644
--- a/src/linux/dumper_cpu_info/x86_mips.rs
+++ b/src/linux/dumper_cpu_info/x86_mips.rs
@@ -1,7 +1,11 @@
-use crate::errors::CpuInfoError;
-use crate::minidump_format::*;
-use std::io::{BufRead, BufReader};
-use std::path;
+use {
+ crate::{errors::CpuInfoError, minidump_format::*},
+ failspot::failspot,
+ std::{
+ io::{BufRead, BufReader},
+ path,
+ },
+};
type Result = std::result::Result;
@@ -44,6 +48,11 @@ pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> {
MDCPUArchitecture::PROCESSOR_ARCHITECTURE_AMD64
} as u16;
+ failspot!(
+ CpuInfoFileOpen
+ bail(std::io::Error::other("test requested cpuinfo file failure"))
+ );
+
let cpuinfo_file = std::fs::File::open(path::PathBuf::from("/proc/cpuinfo"))?;
let mut vendor_id = String::new();
diff --git a/src/linux/errors.rs b/src/linux/errors.rs
index 982445a0..95322f10 100644
--- a/src/linux/errors.rs
+++ b/src/linux/errors.rs
@@ -1,31 +1,22 @@
-use crate::{
- dir_section::FileWriterError, maps_reader::MappingInfo, mem_writer::MemoryWriterError, Pid,
+use {
+ super::{ptrace_dumper::InitError, serializers::*},
+ crate::{
+ dir_section::FileWriterError, maps_reader::MappingInfo, mem_writer::MemoryWriterError,
+ serializers::*, Pid,
+ },
+ error_graph::ErrorList,
+ std::ffi::OsString,
+ thiserror::Error,
};
-use goblin;
-use nix::errno::Errno;
-use std::ffi::OsString;
-use thiserror::Error;
-
-#[derive(Debug, Error)]
-pub enum InitError {
- #[error("failed to read auxv")]
- ReadAuxvFailed(crate::auxv::AuxvError),
- #[error("IO error for file {0}")]
- IOError(String, #[source] std::io::Error),
- #[error("crash thread does not reference principal mapping")]
- PrincipalMappingNotReferenced,
- #[error("Failed Android specific late init")]
- 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)]
+#[derive(Error, Debug, serde::Serialize)]
pub enum MapsReaderError {
#[error("Couldn't parse as ELF file")]
- ELFParsingFailed(#[from] goblin::error::Error),
+ ELFParsingFailed(
+ #[from]
+ #[serde(serialize_with = "serialize_goblin_error")]
+ goblin::error::Error,
+ ),
#[error("No soname found (filename: {})", .0.to_string_lossy())]
NoSoName(OsString, #[source] ModuleReaderError),
@@ -33,102 +24,168 @@ pub enum MapsReaderError {
#[error("Map entry malformed: No {0} found")]
MapEntryMalformed(&'static str),
#[error("Couldn't parse address")]
- UnparsableInteger(#[from] std::num::ParseIntError),
+ UnparsableInteger(
+ #[from]
+ #[serde(skip)]
+ std::num::ParseIntError,
+ ),
#[error("Linux gate location doesn't fit in the required integer type")]
- LinuxGateNotConvertable(#[from] std::num::TryFromIntError),
+ LinuxGateNotConvertable(
+ #[from]
+ #[serde(skip)]
+ std::num::TryFromIntError,
+ ),
// get_mmap()
#[error("Not safe to open mapping {}", .0.to_string_lossy())]
NotSafeToOpenMapping(OsString),
#[error("IO Error")]
- FileError(#[from] std::io::Error),
+ FileError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
#[error("Mmapped file empty or not an ELF file")]
MmapSanityCheckFailed,
#[error("Symlink does not match ({0} vs. {1})")]
SymlinkError(std::path::PathBuf, std::path::PathBuf),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum CpuInfoError {
#[error("IO error for file /proc/cpuinfo")]
- IOError(#[from] std::io::Error),
+ IOError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
#[error("Not all entries of /proc/cpuinfo found!")]
NotAllProcEntriesFound,
#[error("Couldn't parse core from file")]
- UnparsableInteger(#[from] std::num::ParseIntError),
+ UnparsableInteger(
+ #[from]
+ #[serde(skip)]
+ std::num::ParseIntError,
+ ),
#[error("Couldn't parse cores: {0}")]
UnparsableCores(String),
}
-#[derive(Error, Debug)]
+#[derive(Error, Debug, serde::Serialize)]
pub enum ThreadInfoError {
#[error("Index out of bounds: Got {0}, only have {1}")]
IndexOutOfBounds(usize, usize),
#[error("Either ppid ({1}) or tgid ({2}) not found in {0}")]
InvalidPid(String, Pid, Pid),
#[error("IO error")]
- IOError(#[from] std::io::Error),
+ IOError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
#[error("Couldn't parse address")]
- UnparsableInteger(#[from] std::num::ParseIntError),
+ UnparsableInteger(
+ #[from]
+ #[serde(skip)]
+ std::num::ParseIntError,
+ ),
#[error("nix::ptrace() error")]
- PtraceError(#[from] nix::Error),
+ PtraceError(
+ #[from]
+ #[serde(serialize_with = "serialize_nix_error")]
+ nix::Error,
+ ),
#[error("Invalid line in /proc/{0}/status: {1}")]
InvalidProcStatusFile(Pid, String),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum AndroidError {
#[error("Failed to copy memory from process")]
CopyFromProcessError(#[from] DumperError),
#[error("Failed slice conversion")]
- TryFromSliceError(#[from] std::array::TryFromSliceError),
+ TryFromSliceError(
+ #[from]
+ #[serde(skip)]
+ std::array::TryFromSliceError,
+ ),
#[error("No Android rel found")]
NoRelFound,
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
#[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,
+ #[serde(serialize_with = "serialize_nix_error")]
pub source: nix::Error,
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum DumperError {
#[error("Failed to get PAGE_SIZE from system")]
- SysConfError(#[from] nix::Error),
+ SysConfError(
+ #[from]
+ #[serde(serialize_with = "serialize_nix_error")]
+ nix::Error,
+ ),
#[error("wait::waitpid(Pid={0}) failed")]
- WaitPidError(Pid, #[source] nix::Error),
+ WaitPidError(
+ Pid,
+ #[source]
+ #[serde(serialize_with = "serialize_nix_error")]
+ nix::Error,
+ ),
#[error("nix::ptrace::attach(Pid={0}) failed")]
- PtraceAttachError(Pid, #[source] nix::Error),
+ PtraceAttachError(
+ Pid,
+ #[source]
+ #[serde(serialize_with = "serialize_nix_error")]
+ nix::Error,
+ ),
#[error("nix::ptrace::detach(Pid={0}) failed")]
- PtraceDetachError(Pid, #[source] nix::Error),
+ PtraceDetachError(
+ Pid,
+ #[source]
+ #[serde(serialize_with = "serialize_nix_error")]
+ 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}")]
- SuspendNoThreadsLeft(usize),
#[error("No mapping for stack pointer found")]
NoStackPointerMapping,
#[error("Failed slice conversion")]
- TryFromSliceError(#[from] std::array::TryFromSliceError),
+ TryFromSliceError(
+ #[from]
+ #[serde(skip)]
+ std::array::TryFromSliceError,
+ ),
#[error("Couldn't parse as ELF file")]
- ELFParsingFailed(#[from] goblin::error::Error),
+ ELFParsingFailed(
+ #[from]
+ #[serde(serialize_with = "serialize_goblin_error")]
+ goblin::error::Error,
+ ),
#[error("Could not read value from module")]
ModuleReaderError(#[from] ModuleReaderError),
#[error("Not safe to open mapping: {}", .0.to_string_lossy())]
NotSafeToOpenMapping(OsString),
#[error("Failed integer conversion")]
- TryFromIntError(#[from] std::num::TryFromIntError),
+ TryFromIntError(
+ #[from]
+ #[serde(skip)]
+ std::num::TryFromIntError,
+ ),
#[error("Maps reader error")]
MapsReaderError(#[from] MapsReaderError),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionAppMemoryError {
#[error("Failed to copy memory from process")]
CopyFromProcessError(#[from] DumperError),
@@ -136,23 +193,31 @@ pub enum SectionAppMemoryError {
MemoryWriterError(#[from] MemoryWriterError),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionExceptionStreamError {
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionHandleDataStreamError {
#[error("Failed to access file")]
- IOError(#[from] std::io::Error),
+ IOError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
#[error("Failed integer conversion")]
- TryFromIntError(#[from] std::num::TryFromIntError),
+ TryFromIntError(
+ #[from]
+ #[serde(skip)]
+ std::num::TryFromIntError,
+ ),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionMappingsError {
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
@@ -160,53 +225,75 @@ pub enum SectionMappingsError {
GetEffectivePathError(MappingInfo, #[source] MapsReaderError),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionMemInfoListError {
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
#[error("Failed to read from procfs")]
- ProcfsError(#[from] procfs_core::ProcError),
+ ProcfsError(
+ #[from]
+ #[serde(serialize_with = "serialize_proc_error")]
+ procfs_core::ProcError,
+ ),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionMemListError {
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionSystemInfoError {
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
#[error("Failed to get CPU Info")]
CpuInfoError(#[from] CpuInfoError),
+ #[error("Failed trying to write CPU information")]
+ WriteCpuInformationFailed(#[source] CpuInfoError),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionThreadListError {
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
#[error("Failed integer conversion")]
- TryFromIntError(#[from] std::num::TryFromIntError),
+ TryFromIntError(
+ #[from]
+ #[serde(skip)]
+ std::num::TryFromIntError,
+ ),
#[error("Failed to copy memory from process")]
CopyFromProcessError(#[from] DumperError),
#[error("Failed to get thread info")]
ThreadInfoError(#[from] ThreadInfoError),
#[error("Failed to write to memory buffer")]
- IOError(#[from] std::io::Error),
+ IOError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionThreadNamesError {
#[error("Failed integer conversion")]
- TryFromIntError(#[from] std::num::TryFromIntError),
+ TryFromIntError(
+ #[from]
+ #[serde(skip)]
+ std::num::TryFromIntError,
+ ),
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
#[error("Failed to write to memory buffer")]
- IOError(#[from] std::io::Error),
+ IOError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum SectionDsoDebugError {
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
@@ -215,10 +302,14 @@ pub enum SectionDsoDebugError {
#[error("Failed to copy memory from process")]
CopyFromProcessError(#[from] DumperError),
#[error("Failed to copy memory from process")]
- FromUTF8Error(#[from] std::string::FromUtf8Error),
+ FromUTF8Error(
+ #[from]
+ #[serde(serialize_with = "serialize_from_utf8_error")]
+ std::string::FromUtf8Error,
+ ),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum WriterError {
#[error("Error during init phase")]
InitError(#[from] InitError),
@@ -249,15 +340,60 @@ pub enum WriterError {
#[error("Failed to write to file")]
FileWriterError(#[from] FileWriterError),
#[error("Failed to get current timestamp when writing header of minidump")]
- SystemTimeError(#[from] std::time::SystemTimeError),
+ SystemTimeError(
+ #[from]
+ #[serde(serialize_with = "serialize_system_time_error")]
+ std::time::SystemTimeError,
+ ),
+ #[error("Errors occurred while initializing PTraceDumper")]
+ InitErrors(#[source] ErrorList),
+ #[error("Errors occurred while suspending threads")]
+ SuspendThreadsErrors(#[source] ErrorList),
+ #[error("Errors occurred while resuming threads")]
+ ResumeThreadsErrors(#[source] ErrorList),
+ #[error("Crash thread does not reference principal mapping")]
+ PrincipalMappingNotReferenced,
+ #[error("Errors occurred while writing system info")]
+ WriteSystemInfoErrors(#[source] ErrorList),
+ #[error("Failed writing cpuinfo")]
+ WriteCpuInfoFailed(#[source] MemoryWriterError),
+ #[error("Failed writing thread proc status")]
+ WriteThreadProcStatusFailed(#[source] MemoryWriterError),
+ #[error("Failed writing OS Release Information")]
+ WriteOsReleaseInfoFailed(#[source] MemoryWriterError),
+ #[error("Failed writing process command line")]
+ WriteCommandLineFailed(#[source] MemoryWriterError),
+ #[error("Writing process environment failed")]
+ WriteEnvironmentFailed(#[source] MemoryWriterError),
+ #[error("Failed to write auxv file")]
+ WriteAuxvFailed(#[source] MemoryWriterError),
+ #[error("Failed to write maps file")]
+ WriteMapsFailed(#[source] MemoryWriterError),
+ #[error("Failed writing DSO Debug Stream")]
+ WriteDSODebugStreamFailed(#[source] SectionDsoDebugError),
+ #[error("Failed writing limits file")]
+ WriteLimitsFailed(#[source] MemoryWriterError),
+ #[error("Failed writing handle data stream")]
+ WriteHandleDataStreamFailed(#[source] SectionHandleDataStreamError),
+ #[error("Failed writing handle data stream direction entry")]
+ WriteHandleDataStreamDirentFailed(#[source] FileWriterError),
+ #[error("No threads left to suspend out of {0}")]
+ SuspendNoThreadsLeft(usize),
+ #[error("Failed to convert soft error list to JSON")]
+ ConvertToJsonFailed(
+ #[source]
+ #[serde(skip)]
+ serde_json::Error,
+ ),
}
-#[derive(Debug, Error)]
+#[derive(Debug, Error, serde::Serialize)]
pub enum ModuleReaderError {
#[error("failed to read module file ({path}): {error}")]
MapFile {
path: std::path::PathBuf,
#[source]
+ #[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())]
@@ -266,10 +402,15 @@ pub enum ModuleReaderError {
length: u64,
start_address: Option,
#[source]
+ #[serde(serialize_with = "serialize_nix_error")]
error: nix::Error,
},
#[error("failed to parse ELF memory: {0}")]
- Parsing(#[from] goblin::error::Error),
+ Parsing(
+ #[from]
+ #[serde(serialize_with = "serialize_goblin_error")]
+ goblin::error::Error,
+ ),
#[error("no build id notes in program headers")]
NoProgramHeaderNote,
#[error("no string table available to locate note sections")]
diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs
index e023a21a..3113d35f 100644
--- a/src/linux/maps_reader.rs
+++ b/src/linux/maps_reader.rs
@@ -1,19 +1,24 @@
-use crate::auxv::AuxvType;
-use crate::errors::MapsReaderError;
-use byteorder::{NativeEndian, ReadBytesExt};
-use goblin::elf;
-use memmap2::{Mmap, MmapOptions};
-use procfs_core::process::{MMPermissions, MMapPath, MemoryMaps};
-use std::ffi::{OsStr, OsString};
-use std::os::unix::ffi::{OsStrExt, OsStringExt};
-use std::{fs::File, mem::size_of, path::PathBuf};
+use {
+ crate::{auxv::AuxvType, errors::MapsReaderError},
+ byteorder::{NativeEndian, ReadBytesExt},
+ goblin::elf,
+ memmap2::{Mmap, MmapOptions},
+ procfs_core::process::{MMPermissions, MMapPath, MemoryMaps},
+ std::{
+ ffi::{OsStr, OsString},
+ fs::File,
+ mem::size_of,
+ os::unix::ffi::{OsStrExt, OsStringExt},
+ path::PathBuf,
+ },
+};
pub const LINUX_GATE_LIBRARY_NAME: &str = "linux-gate.so";
pub const DELETED_SUFFIX: &[u8] = b" (deleted)";
type Result = std::result::Result;
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)]
pub struct SystemMappingInfo {
pub start_address: usize,
pub end_address: usize,
@@ -21,7 +26,7 @@ pub struct SystemMappingInfo {
// One of these is produced for each mapping in the process (i.e. line in
// /proc/$x/maps).
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)]
pub struct MappingInfo {
// On Android, relocation packing can mean that the reported start
// address of the mapping must be adjusted by a bias in order to
@@ -88,7 +93,10 @@ impl MappingInfo {
self.start_address + self.size
}
- pub fn aggregate(memory_maps: MemoryMaps, linux_gate_loc: AuxvType) -> Result> {
+ pub fn aggregate(
+ memory_maps: MemoryMaps,
+ linux_gate_loc: Option,
+ ) -> Result> {
let mut infos = Vec::::new();
for mm in memory_maps {
@@ -112,9 +120,11 @@ impl MappingInfo {
let is_path = is_mapping_a_path(pathname.as_deref());
- if !is_path && linux_gate_loc != 0 && start_address == linux_gate_loc.try_into()? {
- pathname = Some(LINUX_GATE_LIBRARY_NAME.into());
- offset = 0;
+ if let Some(linux_gate_loc) = linux_gate_loc.map(|u| usize::try_from(u).unwrap()) {
+ if !is_path && start_address == linux_gate_loc {
+ pathname = Some(LINUX_GATE_LIBRARY_NAME.into());
+ offset = 0;
+ }
}
if let Some(prev_module) = infos.last_mut() {
@@ -451,7 +461,7 @@ mod tests {
fn get_mappings_for(map: &str, linux_gate_loc: u64) -> Vec {
MappingInfo::aggregate(
MemoryMaps::from_read(map.as_bytes()).expect("failed to read mapping info"),
- linux_gate_loc,
+ Some(linux_gate_loc),
)
.unwrap_or_default()
}
diff --git a/src/linux/minidump_writer.rs b/src/linux/minidump_writer.rs
index ba6b82ec..0c6e385a 100644
--- a/src/linux/minidump_writer.rs
+++ b/src/linux/minidump_writer.rs
@@ -1,23 +1,26 @@
pub use crate::linux::auxv::{AuxvType, DirectAuxvDumpInfo};
-use crate::{
- auxv::AuxvDumpInfo,
- dir_section::{DirSection, DumpBuf},
- linux::{
- app_memory::AppMemoryList,
- crash_context::CrashContext,
- dso_debug,
- errors::{InitError, WriterError},
- maps_reader::{MappingInfo, MappingList},
- ptrace_dumper::PtraceDumper,
- sections::*,
+use {
+ crate::{
+ auxv::AuxvDumpInfo,
+ dir_section::{DirSection, DumpBuf},
+ linux::{
+ app_memory::AppMemoryList,
+ crash_context::CrashContext,
+ dso_debug,
+ errors::WriterError,
+ maps_reader::{MappingInfo, MappingList},
+ ptrace_dumper::PtraceDumper,
+ sections::*,
+ },
+ mem_writer::{Buffer, MemoryArrayWriter, MemoryWriter, MemoryWriterError},
+ minidump_format::*,
+ Pid,
+ },
+ error_graph::{ErrorList, WriteErrorList},
+ std::{
+ io::{Seek, Write},
+ time::Duration,
},
- mem_writer::{Buffer, MemoryArrayWriter, MemoryWriter, MemoryWriterError},
- minidump_format::*,
- Pid,
-};
-use std::{
- io::{Seek, Write},
- time::Duration,
};
pub enum CrashingThreadContext {
@@ -144,8 +147,24 @@ impl MinidumpWriter {
.clone()
.map(AuxvDumpInfo::from)
.unwrap_or_default();
- let mut dumper = PtraceDumper::new(self.process_id, self.stop_timeout, auxv)?;
- dumper.suspend_threads()?;
+
+ let mut soft_errors = ErrorList::default();
+
+ let mut dumper = PtraceDumper::new_report_soft_errors(
+ self.process_id,
+ self.stop_timeout,
+ auxv,
+ soft_errors.subwriter(WriterError::InitErrors),
+ )?;
+
+ let threads_count = dumper.threads.len();
+
+ dumper.suspend_threads(soft_errors.subwriter(WriterError::SuspendThreadsErrors));
+
+ if dumper.threads.is_empty() {
+ soft_errors.push(WriterError::SuspendNoThreadsLeft(threads_count));
+ }
+
dumper.late_init()?;
if self.skip_stacks_if_mapping_unreferenced {
@@ -154,16 +173,12 @@ impl MinidumpWriter {
}
if !self.crash_thread_references_principal_mapping(&dumper) {
- return Err(InitError::PrincipalMappingNotReferenced.into());
+ soft_errors.push(WriterError::PrincipalMappingNotReferenced);
}
}
let mut buffer = Buffer::with_capacity(0);
- self.generate_dump(&mut buffer, &mut dumper, destination)?;
-
- // dumper would resume threads in drop() automatically,
- // but in case there is an error, we want to catch it
- dumper.resume_threads()?;
+ self.generate_dump(&mut buffer, &mut dumper, soft_errors, destination)?;
Ok(buffer.into())
}
@@ -226,11 +241,12 @@ impl MinidumpWriter {
&mut self,
buffer: &mut DumpBuf,
dumper: &mut PtraceDumper,
+ mut soft_errors: ErrorList,
destination: &mut (impl Write + Seek),
) -> Result<()> {
// A minidump file contains a number of tagged streams. This is the number
// of streams which we write.
- let num_writers = 17u32;
+ let num_writers = 18u32;
let mut header_section = MemoryWriter::::alloc(buffer)?;
@@ -270,7 +286,10 @@ impl MinidumpWriter {
let dirent = exception_stream::write(self, buffer)?;
dir_section.write_to_file(buffer, Some(dirent))?;
- let dirent = systeminfo_stream::write(buffer)?;
+ let dirent = systeminfo_stream::write(
+ buffer,
+ soft_errors.subwriter(WriterError::WriteSystemInfoErrors),
+ )?;
dir_section.write_to_file(buffer, Some(dirent))?;
let dirent = memory_info_list_stream::write(self, buffer)?;
@@ -281,7 +300,10 @@ impl MinidumpWriter {
stream_type: MDStreamType::LinuxCpuInfo as u32,
location,
},
- Err(_) => Default::default(),
+ Err(e) => {
+ soft_errors.push(WriterError::WriteCpuInfoFailed(e));
+ Default::default()
+ }
};
dir_section.write_to_file(buffer, Some(dirent))?;
@@ -291,7 +313,10 @@ impl MinidumpWriter {
stream_type: MDStreamType::LinuxProcStatus as u32,
location,
},
- Err(_) => Default::default(),
+ Err(e) => {
+ soft_errors.push(WriterError::WriteThreadProcStatusFailed(e));
+ Default::default()
+ }
};
dir_section.write_to_file(buffer, Some(dirent))?;
@@ -303,7 +328,10 @@ impl MinidumpWriter {
stream_type: MDStreamType::LinuxLsbRelease as u32,
location,
},
- Err(_) => Default::default(),
+ Err(e) => {
+ soft_errors.push(WriterError::WriteOsReleaseInfoFailed(e));
+ Default::default()
+ }
};
dir_section.write_to_file(buffer, Some(dirent))?;
@@ -313,7 +341,10 @@ impl MinidumpWriter {
stream_type: MDStreamType::LinuxCmdLine as u32,
location,
},
- Err(_) => Default::default(),
+ Err(e) => {
+ soft_errors.push(WriterError::WriteCommandLineFailed(e));
+ Default::default()
+ }
};
dir_section.write_to_file(buffer, Some(dirent))?;
@@ -323,7 +354,10 @@ impl MinidumpWriter {
stream_type: MDStreamType::LinuxEnviron as u32,
location,
},
- Err(_) => Default::default(),
+ Err(e) => {
+ soft_errors.push(WriterError::WriteEnvironmentFailed(e));
+ Default::default()
+ }
};
dir_section.write_to_file(buffer, Some(dirent))?;
@@ -332,7 +366,10 @@ impl MinidumpWriter {
stream_type: MDStreamType::LinuxAuxv as u32,
location,
},
- Err(_) => Default::default(),
+ Err(e) => {
+ soft_errors.push(WriterError::WriteAuxvFailed(e));
+ Default::default()
+ }
};
dir_section.write_to_file(buffer, Some(dirent))?;
@@ -341,12 +378,21 @@ impl MinidumpWriter {
stream_type: MDStreamType::LinuxMaps as u32,
location,
},
- Err(_) => Default::default(),
+ Err(e) => {
+ soft_errors.push(WriterError::WriteMapsFailed(e));
+ Default::default()
+ }
};
dir_section.write_to_file(buffer, Some(dirent))?;
- let dirent = dso_debug::write_dso_debug_stream(buffer, self.process_id, &dumper.auxv)
- .unwrap_or_default();
+ let dirent = match dso_debug::write_dso_debug_stream(buffer, self.process_id, &dumper.auxv)
+ {
+ Ok(dirent) => dirent,
+ Err(e) => {
+ soft_errors.push(WriterError::WriteDSODebugStreamFailed(e));
+ Default::default()
+ }
+ };
dir_section.write_to_file(buffer, Some(dirent))?;
let dirent = match self.write_file(buffer, &format!("/proc/{}/limits", self.blamed_thread))
@@ -355,17 +401,43 @@ impl MinidumpWriter {
stream_type: MDStreamType::MozLinuxLimits as u32,
location,
},
- Err(_) => Default::default(),
+ Err(e) => {
+ soft_errors.push(WriterError::WriteLimitsFailed(e));
+ Default::default()
+ }
};
dir_section.write_to_file(buffer, Some(dirent))?;
let dirent = thread_names_stream::write(buffer, dumper)?;
dir_section.write_to_file(buffer, Some(dirent))?;
- // This section is optional, so we ignore errors when writing it
- if let Ok(dirent) = handle_data_stream::write(self, buffer) {
- let _ = dir_section.write_to_file(buffer, Some(dirent));
- }
+ let dirent = match handle_data_stream::write(self, buffer) {
+ Ok(dirent) => dirent,
+ Err(e) => {
+ soft_errors.push(WriterError::WriteHandleDataStreamFailed(e));
+ Default::default()
+ }
+ };
+ dir_section.write_to_file(buffer, Some(dirent))?;
+
+ // ========================================================================================
+ //
+ // PAST THIS BANNER, THE THREADS ARE RUNNING IN THE TARGET PROCESS AGAIN. IF YOU NEED TO
+ // ADD NEW ENTRIES THAT ACCESS THE TARGET MEMORY, DO IT BEFORE HERE!
+ //
+ // ========================================================================================
+
+ // Collect any last-minute soft errors when trying to restart threads
+ dumper.resume_threads(soft_errors.subwriter(WriterError::ResumeThreadsErrors));
+
+ // If this fails, there's really nothing we can do about that (other than ignore it).
+ let dirent = write_soft_errors(buffer, soft_errors)
+ .map(|location| MDRawDirectory {
+ stream_type: MDStreamType::MozSoftErrors as u32,
+ location,
+ })
+ .unwrap_or_default();
+ dir_section.write_to_file(buffer, Some(dirent))?;
// If you add more directory entries, don't forget to update num_writers, above.
Ok(())
@@ -383,3 +455,13 @@ impl MinidumpWriter {
Ok(section.location())
}
}
+
+fn write_soft_errors(
+ buffer: &mut DumpBuf,
+ soft_errors: ErrorList,
+) -> Result {
+ let soft_errors_json_str =
+ serde_json::to_string_pretty(&soft_errors).map_err(WriterError::ConvertToJsonFailed)?;
+ let section = MemoryArrayWriter::write_bytes(buffer, soft_errors_json_str.as_bytes());
+ Ok(section.location())
+}
diff --git a/src/linux/ptrace_dumper.rs b/src/linux/ptrace_dumper.rs
index 66f36564..7b04a2f5 100644
--- a/src/linux/ptrace_dumper.rs
+++ b/src/linux/ptrace_dumper.rs
@@ -1,28 +1,43 @@
+use {
+ super::{
+ auxv::AuxvError,
+ errors::{AndroidError, MapsReaderError},
+ serializers::*,
+ },
+ crate::{
+ linux::{
+ auxv::AuxvDumpInfo,
+ errors::{DumperError, ThreadInfoError},
+ maps_reader::MappingInfo,
+ module_reader,
+ thread_info::ThreadInfo,
+ Pid,
+ },
+ serializers::*,
+ },
+ error_graph::{ErrorList, WriteErrorList},
+ failspot::failspot,
+ nix::{
+ errno::Errno,
+ sys::{ptrace, signal, wait},
+ },
+ procfs_core::{
+ process::{MMPermissions, ProcState, Stat},
+ FromRead, ProcError,
+ },
+ std::{
+ ffi::OsString,
+ path,
+ result::Result,
+ time::{Duration, Instant},
+ },
+ thiserror::Error,
+};
+
#[cfg(target_os = "android")]
use crate::linux::android::late_process_mappings;
-use crate::linux::{
- auxv::AuxvDumpInfo,
- errors::{DumperError, InitError, ThreadInfoError},
- maps_reader::MappingInfo,
- module_reader,
- thread_info::ThreadInfo,
- Pid,
-};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use crate::thread_info;
-use nix::{
- errno::Errno,
- sys::{ptrace, signal, wait},
-};
-use procfs_core::{
- process::{MMPermissions, ProcState, Stat},
- FromRead, ProcError,
-};
-use std::{
- path,
- result::Result,
- time::{Duration, Instant},
-};
#[derive(Debug, Clone)]
pub struct Thread {
@@ -48,24 +63,91 @@ pub const AT_SYSINFO_EHDR: u64 = 33;
impl Drop for PtraceDumper {
fn drop(&mut self) {
// Always try to resume all threads (e.g. in case of error)
- let _ = self.resume_threads();
+ self.resume_threads(error_graph::strategy::DontCare);
// Always allow the process to continue.
let _ = self.continue_process();
}
}
-#[derive(Debug, thiserror::Error)]
-enum StopProcessError {
+#[derive(Debug, Error, serde::Serialize)]
+pub enum InitError {
+ #[error("failed to read auxv")]
+ ReadAuxvFailed(#[source] crate::auxv::AuxvError),
+ #[error("IO error for file {0}")]
+ IOError(
+ String,
+ #[source]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
+ #[error("Failed Android specific late init")]
+ AndroidLateInitError(#[from] AndroidError),
+ #[error("Failed to read the page size")]
+ PageSizeError(
+ #[from]
+ #[serde(serialize_with = "serialize_nix_error")]
+ nix::Error,
+ ),
+ #[error("Ptrace does not function within the same process")]
+ CannotPtraceSameProcess,
+ #[error("Failed to stop the target process")]
+ StopProcessFailed(#[source] StopProcessError),
+ #[error("Errors occurred while filling missing Auxv info")]
+ FillMissingAuxvInfoErrors(#[source] ErrorList),
+ #[error("Failed filling missing Auxv info")]
+ FillMissingAuxvInfoFailed(#[source] AuxvError),
+ #[error("Failed reading proc/pid/task entry for process")]
+ ReadProcessThreadEntryFailed(
+ #[source]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
+ #[error("Process task entry `{0:?}` could not be parsed as a TID")]
+ ProcessTaskEntryNotTid(OsString),
+ #[error("Failed to read thread name")]
+ ReadThreadNameFailed(
+ #[source]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
+ #[error("Proc task directory `{0:?}` is not a directory")]
+ ProcPidTaskNotDirectory(String),
+ #[error("Errors while enumerating threads")]
+ 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")]
+ EnumerateMappingsFailed(#[source] Box),
+}
+
+#[derive(Debug, thiserror::Error, serde::Serialize)]
+pub enum StopProcessError {
#[error("Failed to stop the process")]
- Stop(#[from] Errno),
+ Stop(
+ #[from]
+ #[serde(serialize_with = "serialize_nix_error")]
+ nix::Error,
+ ),
#[error("Failed to get the process state")]
- State(#[from] ProcError),
+ State(
+ #[from]
+ #[serde(serialize_with = "serialize_proc_error")]
+ ProcError,
+ ),
#[error("Timeout waiting for process to stop")]
Timeout,
}
#[derive(Debug, thiserror::Error)]
-enum ContinueProcessError {
+pub enum ContinueProcessError {
#[error("Failed to continue the process")]
Continue(#[from] Errno),
}
@@ -88,8 +170,13 @@ fn ptrace_detach(child: Pid) -> Result<(), DumperError> {
impl PtraceDumper {
/// Constructs a dumper for extracting information from the specified process id
- pub fn new(pid: Pid, stop_timeout: Duration, auxv: AuxvDumpInfo) -> Result {
- if pid == std::process::id() as _ {
+ pub fn new_report_soft_errors(
+ pid: Pid,
+ stop_timeout: Duration,
+ auxv: AuxvDumpInfo,
+ soft_errors: impl WriteErrorList,
+ ) -> Result {
+ if pid == std::process::id() as i32 {
return Err(InitError::CannotPtraceSameProcess);
}
@@ -101,23 +188,43 @@ impl PtraceDumper {
mappings: Vec::new(),
page_size: 0,
};
- dumper.init(stop_timeout)?;
+ dumper.init(stop_timeout, soft_errors)?;
Ok(dumper)
}
// TODO: late_init for chromeos and android
- pub fn init(&mut self, stop_timeout: Duration) -> Result<(), InitError> {
+ pub fn init(
+ &mut self,
+ stop_timeout: Duration,
+ mut soft_errors: impl WriteErrorList,
+ ) -> Result<(), InitError> {
// Stopping the process is best-effort.
if let Err(e) = self.stop_process(stop_timeout) {
- log::warn!("failed to stop process {}: {e}", self.pid);
+ soft_errors.push(InitError::StopProcessFailed(e));
}
- if let Err(e) = self.auxv.try_filling_missing_info(self.pid) {
- log::warn!("failed trying to fill in missing auxv info: {e}");
+ // Even if we completely fail to fill in any additional Auxv info, we can still press
+ // forward.
+ if let Err(e) = self.auxv.try_filling_missing_info(
+ self.pid,
+ soft_errors.subwriter(InitError::FillMissingAuxvInfoErrors),
+ ) {
+ soft_errors.push(InitError::FillMissingAuxvInfoFailed(e));
+ }
+
+ // If we completely fail to enumerate any threads... Some information is still better than
+ // no information!
+ if let Err(e) =
+ self.enumerate_threads(soft_errors.subwriter(InitError::EnumerateThreadsErrors))
+ {
+ soft_errors.push(InitError::EnumerateThreadsFailed(Box::new(e)));
+ }
+
+ // Same with mappings -- Some information is still better than no information!
+ if let Err(e) = self.enumerate_mappings() {
+ soft_errors.push(InitError::EnumerateMappingsFailed(Box::new(e)));
}
- self.enumerate_threads()?;
- self.enumerate_mappings()?;
self.page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE)?
.expect("page size apparently unlimited: doesn't make sense.")
as usize;
@@ -207,42 +314,44 @@ impl PtraceDumper {
ptrace_detach(child)
}
- pub fn suspend_threads(&mut self) -> Result<(), DumperError> {
- let threads_count = self.threads.len();
+ pub fn suspend_threads(&mut self, mut soft_errors: impl WriteErrorList) {
// Iterate over all threads and try to suspend them.
// If the thread either disappeared before we could attach to it, or if
// it was part of the seccomp sandbox's trusted code, it is OK to
// silently drop it from the minidump.
- self.threads.retain(|x| Self::suspend_thread(x.tid).is_ok());
+ self.threads.retain(|x| match Self::suspend_thread(x.tid) {
+ Ok(()) => true,
+ Err(e) => {
+ soft_errors.push(e);
+ false
+ }
+ });
- if self.threads.is_empty() {
- Err(DumperError::SuspendNoThreadsLeft(threads_count))
- } else {
- self.threads_suspended = true;
- Ok(())
- }
+ self.threads_suspended = true;
+
+ failspot::failspot!(::SuspendThreads soft_errors.push(DumperError::PtraceAttachError(1234, nix::Error::EPERM)))
}
- pub fn resume_threads(&mut self) -> Result<(), DumperError> {
- let mut result = Ok(());
+ pub fn resume_threads(&mut self, mut soft_errors: impl WriteErrorList) {
if self.threads_suspended {
for thread in &self.threads {
match Self::resume_thread(thread.tid) {
- Ok(_) => {}
- x => {
- result = x;
+ Ok(()) => (),
+ Err(e) => {
+ soft_errors.push(e);
}
}
}
}
self.threads_suspended = false;
- result
}
/// Send SIGSTOP to the process so that we can get a consistent state.
///
/// This will block waiting for the process to stop until `timeout` has passed.
fn stop_process(&mut self, timeout: Duration) -> Result<(), StopProcessError> {
+ failspot!(StopProcess bail(nix::Error::EPERM));
+
signal::kill(nix::unistd::Pid::from_raw(self.pid), Some(signal::SIGSTOP))?;
// Something like waitpid for non-child processes would be better, but we have no such
@@ -273,30 +382,54 @@ impl PtraceDumper {
/// Parse /proc/$pid/task to list all the threads of the process identified by
/// pid.
- fn enumerate_threads(&mut self) -> Result<(), InitError> {
+ fn enumerate_threads(
+ &mut self,
+ mut soft_errors: impl WriteErrorList,
+ ) -> Result<(), InitError> {
let pid = self.pid;
let filename = format!("/proc/{}/task", pid);
let task_path = path::PathBuf::from(&filename);
- if task_path.is_dir() {
- std::fs::read_dir(task_path)
- .map_err(|e| InitError::IOError(filename, e))?
- .filter_map(|entry| entry.ok()) // Filter out bad entries
- .filter_map(|entry| {
- entry
- .file_name() // Parse name to Pid, filter out those that are unparsable
- .to_str()
- .and_then(|name| name.parse::().ok())
- })
- .map(|tid| {
- // Read the thread-name (if there is any)
- let name = std::fs::read_to_string(format!("/proc/{}/task/{}/comm", pid, tid))
- // NOTE: This is a bit wasteful as it does two allocations in order to trim, but leaving it for now
- .map(|s| s.trim_end().to_string())
- .ok();
- (tid, name)
- })
- .for_each(|(tid, name)| self.threads.push(Thread { tid, name }));
+ if !task_path.is_dir() {
+ return Err(InitError::ProcPidTaskNotDirectory(filename));
+ }
+
+ for entry in std::fs::read_dir(task_path).map_err(|e| InitError::IOError(filename, e))? {
+ let entry = match entry {
+ Ok(entry) => entry,
+ Err(e) => {
+ soft_errors.push(InitError::ReadProcessThreadEntryFailed(e));
+ continue;
+ }
+ };
+ let file_name = entry.file_name();
+ let tid = match file_name.to_str().and_then(|name| name.parse::().ok()) {
+ Some(tid) => tid,
+ None => {
+ soft_errors.push(InitError::ProcessTaskEntryNotTid(file_name));
+ continue;
+ }
+ };
+
+ // Read the thread-name (if there is any)
+ let name_result = failspot!(if ThreadName {
+ Err(std::io::Error::other(
+ "testing requested failure reading thread name",
+ ))
+ } else {
+ std::fs::read_to_string(format!("/proc/{}/task/{}/comm", pid, tid))
+ });
+
+ let name = match name_result {
+ Ok(name) => Some(name.trim_end().to_string()),
+ Err(e) => {
+ soft_errors.push(InitError::ReadThreadNameFailed(e));
+ None
+ }
+ };
+
+ self.threads.push(Thread { tid, name });
}
+
Ok(())
}
@@ -308,39 +441,34 @@ impl PtraceDumper {
// case its entry when creating the list of mappings.
// See http://www.trilithium.com/johan/2005/08/linux-gate/ for more
// information.
- let linux_gate_loc = self.auxv.get_linux_gate_address().unwrap_or_default();
+ let maps_path = format!("/proc/{}/maps", self.pid);
+ 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())
+ .map_err(InitError::AggregateMappingsFailed)?;
+
// Although the initial executable is usually the first mapping, it's not
// guaranteed (see http://crosbug.com/25355); therefore, try to use the
// actual entry point to find the mapping.
- let entry_point_loc = self.auxv.get_entry_address().unwrap_or_default();
- let filename = format!("/proc/{}/maps", self.pid);
- let errmap = |e| InitError::IOError(filename.clone(), e);
- let maps_path = path::PathBuf::from(&filename);
- let maps_file = std::fs::File::open(maps_path).map_err(errmap)?;
-
- use procfs_core::FromRead;
- self.mappings = procfs_core::process::MemoryMaps::from_read(maps_file)
- .ok()
- .and_then(|maps| MappingInfo::aggregate(maps, linux_gate_loc).ok())
- .unwrap_or_default();
-
- if entry_point_loc != 0 {
- let mut swap_idx = None;
- for (idx, module) in self.mappings.iter().enumerate() {
- // If this module contains the entry-point, and it's not already the first
- // one, then we need to make it be first. This is because the minidump
- // format assumes the first module is the one that corresponds to the main
- // executable (as codified in
- // processor/minidump.cc:MinidumpModuleList::GetMainModule()).
- if entry_point_loc >= module.start_address.try_into().unwrap()
- && entry_point_loc < (module.start_address + module.size).try_into().unwrap()
- {
- swap_idx = Some(idx);
- break;
- }
- }
- if let Some(idx) = swap_idx {
- self.mappings.swap(0, idx);
+ if let Some(entry_point_loc) = self
+ .auxv
+ .get_entry_address()
+ .map(|u| usize::try_from(u).unwrap())
+ {
+ // If this module contains the entry-point, and it's not already the first
+ // one, then we need to make it be first. This is because the minidump
+ // format assumes the first module is the one that corresponds to the main
+ // executable (as codified in
+ // processor/minidump.cc:MinidumpModuleList::GetMainModule()).
+ if let Some(entry_mapping_idx) = self.mappings.iter().position(|mapping| {
+ (mapping.start_address..mapping.start_address + mapping.size)
+ .contains(&entry_point_loc)
+ }) {
+ self.mappings.swap(0, entry_mapping_idx);
}
}
Ok(())
diff --git a/src/linux/sections/systeminfo_stream.rs b/src/linux/sections/systeminfo_stream.rs
index a298c00d..62ca332c 100644
--- a/src/linux/sections/systeminfo_stream.rs
+++ b/src/linux/sections/systeminfo_stream.rs
@@ -1,7 +1,12 @@
-use super::*;
-use crate::linux::dumper_cpu_info as dci;
+use {
+ super::*, crate::linux::dumper_cpu_info as dci, error_graph::WriteErrorList,
+ errors::SectionSystemInfoError,
+};
-pub fn write(buffer: &mut DumpBuf) -> Result {
+pub fn write(
+ buffer: &mut DumpBuf,
+ mut soft_errors: impl WriteErrorList,
+) -> Result {
let mut info_section = MemoryWriter::::alloc(buffer)?;
let dirent = MDRawDirectory {
stream_type: MDStreamType::SystemInfoStream as u32,
@@ -16,7 +21,9 @@ pub fn write(buffer: &mut DumpBuf) -> Result(
+ error: &goblin::error::Error,
+ serializer: S,
+) -> Result {
+ serialize_generic_error(error, serializer)
+}
+/// Serialize [nix::Error]
+pub fn serialize_nix_error(
+ error: &nix::Error,
+ serializer: S,
+) -> Result {
+ serialize_generic_error(error, serializer)
+}
+/// Serialize [procfs_core::ProcError]
+pub fn serialize_proc_error(
+ error: &procfs_core::ProcError,
+ serializer: S,
+) -> Result {
+ serialize_generic_error(error, serializer)
+}
+/// Serialize [std::string::FromUtf8Error]
+pub fn serialize_from_utf8_error(
+ error: &std::string::FromUtf8Error,
+ serializer: S,
+) -> Result {
+ serialize_generic_error(error, serializer)
+}
+/// Serialize [std::time::SystemTimeError]
+pub fn serialize_system_time_error(
+ error: &std::time::SystemTimeError,
+ serializer: S,
+) -> Result {
+ serialize_generic_error(error, serializer)
+}
diff --git a/src/mem_writer.rs b/src/mem_writer.rs
index a703d2b1..ae38c79b 100644
--- a/src/mem_writer.rs
+++ b/src/mem_writer.rs
@@ -1,14 +1,31 @@
-use crate::minidump_format::{MDLocationDescriptor, MDRVA};
-use scroll::ctx::{SizeWith, TryIntoCtx};
-
-#[derive(Debug, thiserror::Error)]
+use {
+ crate::{
+ minidump_format::{MDLocationDescriptor, MDRVA},
+ serializers::*,
+ },
+ scroll::ctx::{SizeWith, TryIntoCtx},
+};
+
+#[derive(Debug, thiserror::Error, serde::Serialize)]
pub enum MemoryWriterError {
#[error("IO error when writing to DumpBuf")]
- IOError(#[from] std::io::Error),
+ IOError(
+ #[from]
+ #[serde(serialize_with = "serialize_io_error")]
+ std::io::Error,
+ ),
#[error("Failed integer conversion")]
- TryFromIntError(#[from] std::num::TryFromIntError),
+ TryFromIntError(
+ #[from]
+ #[serde(skip)]
+ std::num::TryFromIntError,
+ ),
#[error("Failed to write to buffer")]
- Scroll(#[from] scroll::Error),
+ Scroll(
+ #[from]
+ #[serde(serialize_with = "serialize_scroll_error")]
+ scroll::Error,
+ ),
}
type WriteResult = std::result::Result;
diff --git a/src/serializers.rs b/src/serializers.rs
new file mode 100644
index 00000000..a42313da
--- /dev/null
+++ b/src/serializers.rs
@@ -0,0 +1,30 @@
+//! Functions used by Serde to serialize types that we don't own (and thus can't implement
+//! [Serialize] for)
+
+use serde::Serializer;
+/// Useful for types that implement [Error][std::error::Error] and don't need any special
+/// treatment.
+pub fn serialize_generic_error(
+ error: &E,
+ serializer: S,
+) -> Result {
+ // I guess we'll have to see if it's more useful to store the debug representation of a
+ // foreign error type or something else (like maybe iterating its error chain into a
+ // list?)
+ let dbg = format!("{error:#?}");
+ serializer.serialize_str(&dbg)
+}
+/// Serialize [std::io::Error]
+pub fn serialize_io_error(
+ error: &std::io::Error,
+ serializer: S,
+) -> Result {
+ serialize_generic_error(error, serializer)
+}
+/// Serialize [scroll::Error]
+pub fn serialize_scroll_error(
+ error: &scroll::Error,
+ serializer: S,
+) -> Result {
+ serialize_generic_error(error, serializer)
+}
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index cf258819..2b5499ef 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -94,3 +94,38 @@ pub fn start_child_and_return(args: &[&str]) -> Child {
.spawn()
.expect("failed to execute child")
}
+
+#[allow(unused)]
+pub fn read_minidump_soft_errors_or_panic<'a, T>(
+ dump: &minidump::Minidump<'a, T>,
+) -> serde_json::Value
+where
+ T: std::ops::Deref + 'a,
+{
+ let contents = std::str::from_utf8(
+ dump.get_raw_stream(minidump_common::format::MINIDUMP_STREAM_TYPE::MozSoftErrors.into())
+ .expect("missing soft error stream"),
+ )
+ .expect("expected utf-8 stream");
+
+ serde_json::from_str(contents).expect("expected json")
+}
+
+#[allow(unused)]
+pub fn assert_soft_errors_in_minidump<'a, 'b, T, I>(
+ dump: &minidump::Minidump<'a, T>,
+ expected_errors: I,
+) where
+ T: std::ops::Deref + 'a,
+ I: IntoIterator- ,
+{
+ let actual_json = read_minidump_soft_errors_or_panic(dump);
+ let actual_errors = actual_json.as_array().unwrap();
+
+ // Ensure that every error we expect is in the actual list somewhere
+ for expected_error in expected_errors {
+ assert!(actual_errors
+ .iter()
+ .any(|actual_error| actual_error == expected_error));
+ }
+}
diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs
index 77986d34..d62f56ff 100644
--- a/tests/linux_minidump_writer.rs
+++ b/tests/linux_minidump_writer.rs
@@ -1,30 +1,32 @@
#![cfg(any(target_os = "linux", target_os = "android"))]
#![allow(unused_imports, unused_variables)]
-use minidump::*;
-use minidump_common::format::{GUID, MINIDUMP_STREAM_TYPE::*};
-use minidump_writer::{
- app_memory::AppMemory,
- crash_context::CrashContext,
- errors::*,
- maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo},
- minidump_writer::MinidumpWriter,
- module_reader::{BuildId, ReadFromModule},
- ptrace_dumper::PtraceDumper,
- Pid,
-};
-use nix::{errno::Errno, sys::signal::Signal};
-use procfs_core::process::MMPermissions;
-use std::collections::HashSet;
-
-use std::{
- io::{BufRead, BufReader},
- os::unix::process::ExitStatusExt,
- process::{Command, Stdio},
+use {
+ common::*,
+ minidump::*,
+ minidump_common::format::{GUID, MINIDUMP_STREAM_TYPE::*},
+ minidump_writer::{
+ app_memory::AppMemory,
+ crash_context::CrashContext,
+ errors::*,
+ maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo},
+ minidump_writer::MinidumpWriter,
+ module_reader::{BuildId, ReadFromModule},
+ ptrace_dumper::PtraceDumper,
+ Pid,
+ },
+ nix::{errno::Errno, sys::signal::Signal},
+ procfs_core::process::MMPermissions,
+ serde_json::json,
+ std::{
+ collections::HashSet,
+ io::{BufRead, BufReader},
+ os::unix::process::ExitStatusExt,
+ process::{Command, Stdio},
+ },
};
mod common;
-use common::*;
#[derive(Debug, PartialEq)]
enum Context {
@@ -299,6 +301,10 @@ contextual_test! {
contextual_test! {
fn skip_if_requested(context: Context) {
+ let expected_errors = vec![
+ json!("PrincipalMappingNotReferenced"),
+ ];
+
let num_of_threads = 1;
let mut child = start_child_and_wait_for_threads(num_of_threads);
let pid = child.id() as i32;
@@ -331,7 +337,9 @@ contextual_test! {
assert_eq!(waitres.code(), None);
assert_eq!(status, Signal::SIGKILL as i32);
- assert!(res.is_err());
+ // Ensure the MozSoftErrors stream contains the expected errors
+ let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump");
+ assert_soft_errors_in_minidump(&dump, &expected_errors);
}
}
@@ -790,7 +798,7 @@ fn memory_info_list_stream() {
.dump(&mut tmpfile)
.expect("cound not write minidump");
child.kill().expect("Failed to kill process");
- let _ = child.wait();
+ child.wait().expect("Failed to wait on killed process");
// Ensure the minidump has a MemoryInfoListStream present and has at least one entry.
let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump");
diff --git a/tests/linux_minidump_writer_soft_error.rs b/tests/linux_minidump_writer_soft_error.rs
new file mode 100644
index 00000000..5c5df212
--- /dev/null
+++ b/tests/linux_minidump_writer_soft_error.rs
@@ -0,0 +1,92 @@
+#![cfg(any(target_os = "linux", target_os = "android"))]
+
+use {
+ common::*,
+ minidump::Minidump,
+ minidump_writer::{minidump_writer::MinidumpWriter, FailSpotName},
+ serde_json::json,
+};
+
+mod common;
+
+#[test]
+fn soft_error_stream() {
+ let mut child = start_child_and_wait_for_threads(1);
+ let pid = child.id() as i32;
+
+ let mut tmpfile = tempfile::Builder::new()
+ .prefix("soft_error_stream")
+ .tempfile()
+ .unwrap();
+
+ let mut fail_client = FailSpotName::testing_client();
+ fail_client.set_enabled(FailSpotName::StopProcess, true);
+
+ // Write a minidump
+ MinidumpWriter::new(pid, pid)
+ .dump(&mut tmpfile)
+ .expect("cound not write minidump");
+ child.kill().expect("Failed to kill process");
+ child.wait().expect("Failed to wait on killed process");
+
+ // Ensure the minidump has a MozSoftErrors present
+ let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump");
+ read_minidump_soft_errors_or_panic(&dump);
+}
+
+#[test]
+fn soft_error_stream_content() {
+ let expected_errors = vec![
+ json!({"InitErrors": [
+ {"StopProcessFailed": {"Stop": "EPERM"}},
+ {"FillMissingAuxvInfoErrors": ["InvalidFormat"]},
+ {"EnumerateThreadsErrors": [
+ {"ReadThreadNameFailed": "\
+ Custom {\n \
+ kind: Other,\n \
+ error: \"testing requested failure reading thread name\",\n\
+ }"
+ }
+ ]}
+ ]}),
+ json!({"SuspendThreadsErrors": [{"PtraceAttachError": [1234, "EPERM"]}]}),
+ json!({"WriteSystemInfoErrors": [
+ {"WriteCpuInformationFailed": {"IOError": "\
+ Custom {\n \
+ kind: Other,\n \
+ error: \"test requested cpuinfo file failure\",\n\
+ }"
+ }}
+ ]}),
+ ];
+
+ let mut child = start_child_and_wait_for_threads(1);
+ let pid = child.id() as i32;
+
+ let mut tmpfile = tempfile::Builder::new()
+ .prefix("soft_error_stream_content")
+ .tempfile()
+ .unwrap();
+
+ let mut fail_client = FailSpotName::testing_client();
+ for name in [
+ FailSpotName::StopProcess,
+ FailSpotName::FillMissingAuxvInfo,
+ FailSpotName::ThreadName,
+ FailSpotName::SuspendThreads,
+ FailSpotName::CpuInfoFileOpen,
+ ] {
+ fail_client.set_enabled(name, true);
+ }
+
+ // Write a minidump
+ MinidumpWriter::new(pid, pid)
+ .dump(&mut tmpfile)
+ .expect("cound not write minidump");
+ child.kill().expect("Failed to kill process");
+ child.wait().expect("Failed to wait on killed process");
+
+ // Ensure the MozSoftErrors stream contains the expected errors
+ let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump");
+ assert_soft_errors_in_minidump(&dump, &expected_errors);
+}
diff --git a/tests/ptrace_dumper.rs b/tests/ptrace_dumper.rs
index 48c4fa27..b166c256 100644
--- a/tests/ptrace_dumper.rs
+++ b/tests/ptrace_dumper.rs
@@ -1,13 +1,20 @@
//! All of these tests are specific to ptrace
#![cfg(any(target_os = "linux", target_os = "android"))]
-use minidump_writer::ptrace_dumper::PtraceDumper;
-use nix::sys::mman::{mmap, MapFlags, ProtFlags};
-use nix::sys::signal::Signal;
-use std::convert::TryInto;
-use std::io::{BufRead, BufReader};
-use std::mem::size_of;
-use std::os::unix::process::ExitStatusExt;
+use {
+ error_graph::ErrorList,
+ minidump_writer::ptrace_dumper::PtraceDumper,
+ nix::{
+ sys::mman::{mmap, MapFlags, ProtFlags},
+ sys::signal::Signal,
+ },
+ std::{
+ convert::TryInto,
+ io::{BufRead, BufReader},
+ mem::size_of,
+ os::unix::process::ExitStatusExt,
+ },
+};
mod common;
use common::*;
@@ -23,6 +30,13 @@ macro_rules! disabled_on_ci_and_android {
};
}
+macro_rules! assert_no_soft_errors(($n: ident, $e: expr) => {{
+ let mut $n = ErrorList::default();
+ let __result = $e;
+ assert!($n.is_empty(), "{:?}", $n);
+ __result
+}});
+
#[test]
fn test_setup() {
spawn_child("setup", &[]);
@@ -91,14 +105,21 @@ fn test_thread_list_from_parent() {
let num_of_threads = 5;
let mut child = start_child_and_wait_for_threads(num_of_threads);
let pid = child.id() as i32;
- let mut dumper = PtraceDumper::new(
- pid,
- minidump_writer::minidump_writer::STOP_TIMEOUT,
- Default::default(),
+
+ let mut dumper = assert_no_soft_errors!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ pid,
+ minidump_writer::minidump_writer::STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors,
+ )
)
.expect("Couldn't init dumper");
+
assert_eq!(dumper.threads.len(), num_of_threads);
- dumper.suspend_threads().expect("Could not suspend threads");
+
+ assert_no_soft_errors!(soft_errors, dumper.suspend_threads(&mut soft_errors));
// let mut matching_threads = 0;
for (idx, curr_thread) in dumper.threads.iter().enumerate() {
@@ -146,7 +167,7 @@ fn test_thread_list_from_parent() {
0
}; */
}
- dumper.resume_threads().expect("Failed to resume threads");
+ assert_no_soft_errors!(soft_errors, dumper.resume_threads(&mut soft_errors));
child.kill().expect("Failed to kill process");
// Reap child
@@ -272,14 +293,20 @@ fn test_sanitize_stack_copy() {
let heap_addr = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16)
.expect("unable to parse mmap_addr");
- let mut dumper = PtraceDumper::new(
- pid,
- minidump_writer::minidump_writer::STOP_TIMEOUT,
- Default::default(),
+ let mut dumper = assert_no_soft_errors!(
+ soft_errors,
+ PtraceDumper::new_report_soft_errors(
+ pid,
+ minidump_writer::minidump_writer::STOP_TIMEOUT,
+ Default::default(),
+ &mut soft_errors,
+ )
)
.expect("Couldn't init dumper");
assert_eq!(dumper.threads.len(), num_of_threads);
- dumper.suspend_threads().expect("Could not suspend threads");
+
+ assert_no_soft_errors!(soft_errors, dumper.suspend_threads(&mut soft_errors));
+
let thread_info = dumper
.get_thread_info_by_index(0)
.expect("Couldn't find thread_info");
@@ -374,7 +401,8 @@ fn test_sanitize_stack_copy() {
assert_eq!(simulated_stack[0..size_of::()], defaced);
- dumper.resume_threads().expect("Failed to resume threads");
+ assert_no_soft_errors!(soft_errors, dumper.resume_threads(&mut soft_errors));
+
child.kill().expect("Failed to kill process");
// Reap child