diff --git a/examples/synthetic.rs b/examples/synthetic.rs index 58ddc7d2..63681d99 100644 --- a/examples/synthetic.rs +++ b/examples/synthetic.rs @@ -1,11 +1,12 @@ //! Emits default minidump with no streams to specified path -use std::fs::File; - -use minidump_writer::{ - dir_section::DirSection, - mem_writer::{Buffer, MemoryWriter}, - minidump_format::{MDRawHeader, MD_HEADER_SIGNATURE, MD_HEADER_VERSION}, +use { + minidump_writer::{ + dir_section::DirSection, + mem_writer::{Buffer, MemoryWriter}, + minidump_format::{MDRawHeader, MD_HEADER_SIGNATURE, MD_HEADER_VERSION}, + }, + std::fs::File, }; // usage: `cargo run --example synthetic /tmp/micro-minidump.dmp` diff --git a/src/bin/test.rs b/src/bin/test.rs index 9ab42f13..07fa81df 100644 --- a/src/bin/test.rs +++ b/src/bin/test.rs @@ -10,8 +10,8 @@ mod linux { super::*, error_graph::ErrorList, minidump_writer::{ - minidump_writer::STOP_TIMEOUT, module_reader, ptrace_dumper::PtraceDumper, - LINUX_GATE_LIBRARY_NAME, + minidump_writer::{MinidumpWriter, MinidumpWriterConfig}, + module_reader, LINUX_GATE_LIBRARY_NAME, }, nix::{ sys::mman::{mmap_anonymous, MapFlags, ProtFlags}, @@ -40,12 +40,8 @@ mod linux { let ppid = getppid(); fail_on_soft_error!( soft_errors, - PtraceDumper::new_report_soft_errors( - ppid.as_raw(), - STOP_TIMEOUT, - Default::default(), - &mut soft_errors, - )? + MinidumpWriterConfig::new(ppid.as_raw(), ppid.as_raw()) + .build_for_testing(&mut soft_errors)? ); Ok(()) } @@ -54,12 +50,8 @@ mod linux { let ppid = getppid(); let dumper = fail_on_soft_error!( soft_errors, - PtraceDumper::new_report_soft_errors( - ppid.as_raw(), - STOP_TIMEOUT, - Default::default(), - &mut soft_errors, - )? + MinidumpWriterConfig::new(ppid.as_raw(), ppid.as_raw()) + .build_for_testing(&mut soft_errors)? ); test!(!dumper.threads.is_empty(), "No threads"); test!( @@ -87,18 +79,11 @@ mod linux { use minidump_writer::mem_reader::MemReader; let ppid = getppid().as_raw(); - let mut dumper = fail_on_soft_error!( + let dumper = fail_on_soft_error!( soft_errors, - PtraceDumper::new_report_soft_errors( - ppid, - STOP_TIMEOUT, - Default::default(), - &mut soft_errors - )? + MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&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 @@ -141,30 +126,27 @@ mod linux { } let stack_res = - PtraceDumper::copy_from_process(ppid, stack_var, std::mem::size_of::())?; + MinidumpWriter::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::())?; + MinidumpWriter::copy_from_process(ppid, heap_var, std::mem::size_of::())?; test!(heap_res == expected_heap, "heap var not correct"); - fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors)); + drop(dumper); Ok(()) } fn test_find_mappings(addr1: usize, addr2: usize) -> Result<()> { let ppid = getppid(); + let dumper = fail_on_soft_error!( soft_errors, - PtraceDumper::new_report_soft_errors( - ppid.as_raw(), - STOP_TIMEOUT, - Default::default(), - &mut soft_errors, - )? + MinidumpWriterConfig::new(ppid.as_raw(), ppid.as_raw()) + .build_for_testing(&mut soft_errors)? ); dumper .find_mapping(addr1) @@ -185,16 +167,9 @@ mod linux { let mut dumper = fail_on_soft_error!( soft_errors, - PtraceDumper::new_report_soft_errors( - ppid, - STOP_TIMEOUT, - Default::default(), - &mut soft_errors - )? + MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&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) { @@ -205,7 +180,7 @@ mod linux { let idx = found_exe.unwrap(); let module_reader::BuildId(id) = dumper.from_process_memory_for_index(idx)?; - fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors)); + drop(dumper); assert!(!id.is_empty()); assert!(id.iter().any(|&x| x > 0)); @@ -216,12 +191,8 @@ mod linux { // Now check that PtraceDumper interpreted the mappings properly. let dumper = fail_on_soft_error!( soft_errors, - PtraceDumper::new_report_soft_errors( - getppid().as_raw(), - STOP_TIMEOUT, - Default::default(), - &mut soft_errors, - )? + MinidumpWriterConfig::new(getppid().as_raw(), getppid().as_raw()) + .build_for_testing(&mut soft_errors)? ); let mut mapping_count = 0; for map in &dumper.mappings { @@ -244,29 +215,20 @@ mod linux { fn test_linux_gate_mapping_id() -> Result<()> { let ppid = getppid().as_raw(); - let mut dumper = fail_on_soft_error!( + let dumper = fail_on_soft_error!( soft_errors, - PtraceDumper::new_report_soft_errors( - ppid, - STOP_TIMEOUT, - Default::default(), - &mut soft_errors - )? + MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&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; - 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)?; + MinidumpWriter::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"); - - fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors)); - + drop(dumper); break; } } @@ -278,12 +240,7 @@ mod linux { let ppid = getppid().as_raw(); let dumper = fail_on_soft_error!( soft_errors, - PtraceDumper::new_report_soft_errors( - ppid, - STOP_TIMEOUT, - Default::default(), - &mut soft_errors - )? + MinidumpWriterConfig::new(ppid, ppid).build_for_testing(&mut soft_errors)? ); let linux_gate_loc = dumper.auxv.get_linux_gate_address().unwrap(); test!(linux_gate_loc != 0, "linux_gate_loc == 0"); @@ -332,7 +289,7 @@ mod linux { // One less than the requested amount, as the main thread counts as well for id in 1..num { std::thread::Builder::new() - .name(format!("thread_{}", id)) + .name(format!("thread_{id}")) .spawn(|| { println!("1"); loop { diff --git a/src/linux/android.rs b/src/linux/android.rs index 18d35443..7eee7cb5 100644 --- a/src/linux/android.rs +++ b/src/linux/android.rs @@ -1,8 +1,10 @@ -use crate::errors::AndroidError; -use crate::maps_reader::MappingInfo; -use crate::ptrace_dumper::PtraceDumper; -use crate::Pid; -use goblin::elf; +use { + super::{ + maps_reader::MappingInfo, mem_reader::CopyFromProcessError, + minidump_writer::MinidumpWriter, Pid, + }, + goblin::elf, +}; cfg_if::cfg_if! { if #[cfg(target_pointer_width = "32")] { @@ -26,6 +28,20 @@ cfg_if::cfg_if! { type Result = std::result::Result; +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum AndroidError { + #[error("Failed to copy memory from process")] + CopyFromProcessError(#[from] CopyFromProcessError), + #[error("Failed slice conversion")] + TryFromSliceError( + #[from] + #[serde(skip)] + std::array::TryFromSliceError, + ), + #[error("No Android rel found")] + NoRelFound, +} + struct DynVaddresses { min_vaddr: usize, dyn_vaddr: usize, @@ -36,7 +52,7 @@ fn has_android_packed_relocations(pid: Pid, load_bias: usize, vaddrs: DynVaddres let dyn_addr = load_bias + vaddrs.dyn_vaddr; for idx in 0..vaddrs.dyn_count { let addr = dyn_addr + SIZEOF_DYN * idx; - let dyn_data = PtraceDumper::copy_from_process(pid, addr, SIZEOF_DYN)?; + let dyn_data = MinidumpWriter::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; unsafe { @@ -76,7 +92,7 @@ fn parse_loaded_elf_program_headers( let mut dyn_vaddr = 0; let mut dyn_count = 0; - let phdr_opt = PtraceDumper::copy_from_process( + let phdr_opt = MinidumpWriter::copy_from_process( pid, phdr_addr, elf_header::SIZEOF_EHDR * ehdr.e_phnum as usize, @@ -114,7 +130,7 @@ pub fn late_process_mappings(pid: Pid, mappings: &mut [MappingInfo]) -> Result<( .filter(|m| m.is_executable() && m.name_is_path()) { let ehdr_opt = - PtraceDumper::copy_from_process(pid, map.start_address, elf_header::SIZEOF_EHDR) + MinidumpWriter::copy_from_process(pid, map.start_address, elf_header::SIZEOF_EHDR) .ok() .and_then(|x| elf_header::Header::parse(&x).ok()); diff --git a/src/linux/auxv/mod.rs b/src/linux/auxv/mod.rs index 527fc45b..01654798 100644 --- a/src/linux/auxv/mod.rs +++ b/src/linux/auxv/mod.rs @@ -1,6 +1,7 @@ use { self::reader::ProcfsAuxvIter, - crate::{serializers::*, Pid}, + super::Pid, + crate::serializers::*, error_graph::WriteErrorList, failspot::failspot, std::{fs::File, io::BufReader}, diff --git a/src/linux/auxv/reader.rs b/src/linux/auxv/reader.rs index dadb9979..e36ba93a 100644 --- a/src/linux/auxv/reader.rs +++ b/src/linux/auxv/reader.rs @@ -99,6 +99,6 @@ fn read_long(reader: &mut dyn Read) -> std::io::Result { match std::mem::size_of::() { 4 => reader.read_u32::().map(|u| u as AuxvType), 8 => reader.read_u64::().map(|u| u as AuxvType), - x => panic!("Unexpected type width: {}", x), + x => panic!("Unexpected type width: {x}"), } } diff --git a/src/linux/crash_context/aarch64.rs b/src/linux/crash_context/aarch64.rs index c53c3772..f254ca47 100644 --- a/src/linux/crash_context/aarch64.rs +++ b/src/linux/crash_context/aarch64.rs @@ -1,7 +1,9 @@ -use super::CrashContext; -use crate::{ - minidump_cpu::{RawContextCPU, FP_REG_COUNT, GP_REG_COUNT}, - minidump_format::format, +use { + super::CrashContext, + crate::{ + minidump_cpu::{RawContextCPU, FP_REG_COUNT, GP_REG_COUNT}, + minidump_format::format, + }, }; impl CrashContext { diff --git a/src/linux/crash_context/arm.rs b/src/linux/crash_context/arm.rs index e4e40216..7cf830a7 100644 --- a/src/linux/crash_context/arm.rs +++ b/src/linux/crash_context/arm.rs @@ -1,5 +1,4 @@ -use super::CrashContext; -use crate::minidump_cpu::RawContextCPU; +use {super::CrashContext, crate::minidump_cpu::RawContextCPU}; impl CrashContext { pub fn get_instruction_pointer(&self) -> usize { diff --git a/src/linux/crash_context.rs b/src/linux/crash_context/mod.rs similarity index 63% rename from src/linux/crash_context.rs rename to src/linux/crash_context/mod.rs index f7a554d1..185ad769 100644 --- a/src/linux/crash_context.rs +++ b/src/linux/crash_context/mod.rs @@ -6,6 +6,16 @@ pub struct CrashContext { pub inner: crash_context::CrashContext, } +impl std::fmt::Debug for CrashContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CrashContext") + .field("siginfo", &self.inner.siginfo) + .field("pid", &self.inner.pid) + .field("tid", &self.inner.tid) + .finish_non_exhaustive() + } +} + cfg_if::cfg_if! { if #[cfg(target_arch = "x86_64")] { mod x86_64; diff --git a/src/linux/crash_context/x86.rs b/src/linux/crash_context/x86.rs index 5a2e43ee..d8fb3322 100644 --- a/src/linux/crash_context/x86.rs +++ b/src/linux/crash_context/x86.rs @@ -1,8 +1,10 @@ -use super::CrashContext; -use crate::{minidump_cpu::RawContextCPU, minidump_format::format::ContextFlagsX86}; -use libc::{ - REG_CS, REG_DS, REG_EAX, REG_EBP, REG_EBX, REG_ECX, REG_EDI, REG_EDX, REG_EFL, REG_EIP, REG_ES, - REG_ESI, REG_ESP, REG_FS, REG_GS, REG_SS, REG_UESP, +use { + super::CrashContext, + crate::{minidump_cpu::RawContextCPU, minidump_format::format::ContextFlagsX86}, + libc::{ + REG_CS, REG_DS, REG_EAX, REG_EBP, REG_EBX, REG_ECX, REG_EDI, REG_EDX, REG_EFL, REG_EIP, + REG_ES, REG_ESI, REG_ESP, REG_FS, REG_GS, REG_SS, REG_UESP, + }, }; impl CrashContext { pub fn get_instruction_pointer(&self) -> usize { diff --git a/src/linux/crash_context/x86_64.rs b/src/linux/crash_context/x86_64.rs index e5596926..2d5f29c2 100644 --- a/src/linux/crash_context/x86_64.rs +++ b/src/linux/crash_context/x86_64.rs @@ -1,12 +1,12 @@ -use super::CrashContext; -use crate::{ - minidump_cpu::RawContextCPU, minidump_format::format, thread_info::copy_u32_registers, +use { + super::{super::thread_info::copy_u32_registers, CrashContext}, + crate::{minidump_cpu::RawContextCPU, minidump_format::format}, + libc::{ + REG_CSGSFS, REG_EFL, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15, REG_R8, REG_R9, + REG_RAX, REG_RBP, REG_RBX, REG_RCX, REG_RDI, REG_RDX, REG_RIP, REG_RSI, REG_RSP, + }, + scroll::Pwrite, }; -use libc::{ - REG_CSGSFS, REG_EFL, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15, REG_R8, REG_R9, - REG_RAX, REG_RBP, REG_RBX, REG_RCX, REG_RDI, REG_RDX, REG_RIP, REG_RSI, REG_RSP, -}; -use scroll::Pwrite; impl CrashContext { pub fn get_instruction_pointer(&self) -> usize { diff --git a/src/linux/dso_debug.rs b/src/linux/dso_debug.rs index ef27dd5a..67d6e6f8 100644 --- a/src/linux/dso_debug.rs +++ b/src/linux/dso_debug.rs @@ -1,7 +1,14 @@ -use crate::{ - linux::{auxv::AuxvDumpInfo, errors::SectionDsoDebugError, ptrace_dumper::PtraceDumper}, - mem_writer::{write_string_to_location, Buffer, MemoryArrayWriter, MemoryWriter}, - minidump_format::*, +use { + super::{ + auxv::AuxvDumpInfo, mem_reader::CopyFromProcessError, minidump_writer::MinidumpWriter, + serializers::*, + }, + crate::{ + mem_writer::{ + write_string_to_location, Buffer, MemoryArrayWriter, MemoryWriter, MemoryWriterError, + }, + minidump_format::*, + }, }; type Result = std::result::Result; @@ -26,6 +33,22 @@ cfg_if::cfg_if! { } } +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum SectionDsoDebugError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Could not find: {0}")] + CouldNotFind(&'static str), + #[error("Failed to copy memory from process")] + CopyFromProcessError(#[from] CopyFromProcessError), + #[error("Failed to copy memory from process")] + FromUTF8Error( + #[from] + #[serde(serialize_with = "serialize_from_utf8_error")] + std::string::FromUtf8Error, + ), +} + // COPY from #[derive(Debug, Clone, Default)] #[repr(C)] @@ -85,7 +108,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, SIZEOF_PHDR * phnum_max)?; + let ph = MinidumpWriter::copy_from_process(blamed_thread, phdr, SIZEOF_PHDR * phnum_max)?; let program_headers; #[cfg(target_pointer_width = "64")] { @@ -131,7 +154,7 @@ pub fn write_dso_debug_stream( // DSOs loaded into the program. If this information is indeed available, // dump it to a MD_LINUX_DSO_DEBUG stream. loop { - let dyn_data = PtraceDumper::copy_from_process( + let dyn_data = MinidumpWriter::copy_from_process( blamed_thread, dyn_addr as usize + dynamic_length, dyn_size, @@ -160,7 +183,7 @@ pub fn write_dso_debug_stream( // loader communicates with debuggers. let debug_entry_data = - PtraceDumper::copy_from_process(blamed_thread, r_debug, std::mem::size_of::())?; + MinidumpWriter::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::() }; @@ -171,7 +194,7 @@ pub fn write_dso_debug_stream( let mut dso_vec = Vec::new(); let mut curr_map = debug_entry.r_map; while curr_map != 0 { - let link_map_data = PtraceDumper::copy_from_process( + let link_map_data = MinidumpWriter::copy_from_process( blamed_thread, curr_map, std::mem::size_of::(), @@ -198,7 +221,7 @@ pub fn write_dso_debug_stream( let mut filename = String::new(); if map.l_name > 0 { let filename_data = - PtraceDumper::copy_from_process(blamed_thread, map.l_name, 256)?; + MinidumpWriter::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() { @@ -234,7 +257,7 @@ 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 usize, dynamic_length)?; + MinidumpWriter::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/dumper_cpu_info/arm.rs b/src/linux/dumper_cpu_info/arm.rs index 886920f5..712c43c6 100644 --- a/src/linux/dumper_cpu_info/arm.rs +++ b/src/linux/dumper_cpu_info/arm.rs @@ -1,10 +1,13 @@ -use crate::{errors::CpuInfoError, minidump_format::*}; -use scroll::Pwrite; -use std::{ - collections::HashSet, - fs::File, - io::{BufRead, BufReader, Read}, - path, +use { + super::CpuInfoError, + crate::minidump_format::*, + scroll::Pwrite, + std::{ + collections::HashSet, + fs::File, + io::{BufRead, BufReader, Read}, + path, + }, }; type Result = std::result::Result; diff --git a/src/linux/dumper_cpu_info.rs b/src/linux/dumper_cpu_info/mod.rs similarity index 75% rename from src/linux/dumper_cpu_info.rs rename to src/linux/dumper_cpu_info/mod.rs index 72da20fe..54a4f3b7 100644 --- a/src/linux/dumper_cpu_info.rs +++ b/src/linux/dumper_cpu_info/mod.rs @@ -1,3 +1,8 @@ +use { + crate::{minidump_format::PlatformId, serializers::*}, + nix::sys::utsname::uname, +}; + cfg_if::cfg_if! { if #[cfg(any( target_arch = "x86_64", @@ -20,8 +25,25 @@ cfg_if::cfg_if! { pub use imp::write_cpu_information; -use crate::minidump_format::PlatformId; -use nix::sys::utsname::uname; +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum CpuInfoError { + #[error("IO error for file /proc/cpuinfo")] + 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] + #[serde(skip)] + std::num::ParseIntError, + ), + #[error("Couldn't parse cores: {0}")] + UnparsableCores(String), +} /// Retrieves the [`MDOSPlatform`] and synthesized version information pub fn os_information() -> (PlatformId, String) { diff --git a/src/linux/dumper_cpu_info/x86_mips.rs b/src/linux/dumper_cpu_info/x86_mips.rs index 0999065e..ef0659e6 100644 --- a/src/linux/dumper_cpu_info/x86_mips.rs +++ b/src/linux/dumper_cpu_info/x86_mips.rs @@ -1,5 +1,6 @@ use { - crate::{errors::CpuInfoError, minidump_format::*}, + super::CpuInfoError, + crate::minidump_format::*, failspot::failspot, std::{ io::{BufRead, BufReader}, diff --git a/src/linux/errors.rs b/src/linux/errors.rs deleted file mode 100644 index 95322f10..00000000 --- a/src/linux/errors.rs +++ /dev/null @@ -1,454 +0,0 @@ -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, -}; - -#[derive(Error, Debug, serde::Serialize)] -pub enum MapsReaderError { - #[error("Couldn't parse as ELF file")] - ELFParsingFailed( - #[from] - #[serde(serialize_with = "serialize_goblin_error")] - goblin::error::Error, - ), - #[error("No soname found (filename: {})", .0.to_string_lossy())] - NoSoName(OsString, #[source] ModuleReaderError), - - // parse_from_line() - #[error("Map entry malformed: No {0} found")] - MapEntryMalformed(&'static str), - #[error("Couldn't parse address")] - UnparsableInteger( - #[from] - #[serde(skip)] - std::num::ParseIntError, - ), - #[error("Linux gate location doesn't fit in the required integer type")] - 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] - #[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, serde::Serialize)] -pub enum CpuInfoError { - #[error("IO error for file /proc/cpuinfo")] - 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] - #[serde(skip)] - std::num::ParseIntError, - ), - #[error("Couldn't parse cores: {0}")] - UnparsableCores(String), -} - -#[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] - #[serde(serialize_with = "serialize_io_error")] - std::io::Error, - ), - #[error("Couldn't parse address")] - UnparsableInteger( - #[from] - #[serde(skip)] - std::num::ParseIntError, - ), - #[error("nix::ptrace() 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, serde::Serialize)] -pub enum AndroidError { - #[error("Failed to copy memory from process")] - CopyFromProcessError(#[from] DumperError), - #[error("Failed slice conversion")] - TryFromSliceError( - #[from] - #[serde(skip)] - std::array::TryFromSliceError, - ), - #[error("No Android rel found")] - NoRelFound, -} - -#[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, serde::Serialize)] -pub enum DumperError { - #[error("Failed to get PAGE_SIZE from system")] - SysConfError( - #[from] - #[serde(serialize_with = "serialize_nix_error")] - nix::Error, - ), - #[error("wait::waitpid(Pid={0}) failed")] - WaitPidError( - Pid, - #[source] - #[serde(serialize_with = "serialize_nix_error")] - nix::Error, - ), - #[error("nix::ptrace::attach(Pid={0}) failed")] - PtraceAttachError( - Pid, - #[source] - #[serde(serialize_with = "serialize_nix_error")] - nix::Error, - ), - #[error("nix::ptrace::detach(Pid={0}) failed")] - 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 mapping for stack pointer found")] - NoStackPointerMapping, - #[error("Failed slice conversion")] - TryFromSliceError( - #[from] - #[serde(skip)] - std::array::TryFromSliceError, - ), - #[error("Couldn't parse as ELF file")] - 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] - #[serde(skip)] - std::num::TryFromIntError, - ), - #[error("Maps reader error")] - MapsReaderError(#[from] MapsReaderError), -} - -#[derive(Debug, Error, serde::Serialize)] -pub enum SectionAppMemoryError { - #[error("Failed to copy memory from process")] - CopyFromProcessError(#[from] DumperError), - #[error("Failed to write to memory")] - MemoryWriterError(#[from] MemoryWriterError), -} - -#[derive(Debug, Error, serde::Serialize)] -pub enum SectionExceptionStreamError { - #[error("Failed to write to memory")] - MemoryWriterError(#[from] MemoryWriterError), -} - -#[derive(Debug, Error, serde::Serialize)] -pub enum SectionHandleDataStreamError { - #[error("Failed to access file")] - 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] - #[serde(skip)] - std::num::TryFromIntError, - ), -} - -#[derive(Debug, Error, serde::Serialize)] -pub enum SectionMappingsError { - #[error("Failed to write to memory")] - MemoryWriterError(#[from] MemoryWriterError), - #[error("Failed to get effective path of mapping ({0:?})")] - GetEffectivePathError(MappingInfo, #[source] MapsReaderError), -} - -#[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] - #[serde(serialize_with = "serialize_proc_error")] - procfs_core::ProcError, - ), -} - -#[derive(Debug, Error, serde::Serialize)] -pub enum SectionMemListError { - #[error("Failed to write to memory")] - MemoryWriterError(#[from] MemoryWriterError), -} - -#[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, serde::Serialize)] -pub enum SectionThreadListError { - #[error("Failed to write to memory")] - MemoryWriterError(#[from] MemoryWriterError), - #[error("Failed integer conversion")] - 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] - #[serde(serialize_with = "serialize_io_error")] - std::io::Error, - ), -} - -#[derive(Debug, Error, serde::Serialize)] -pub enum SectionThreadNamesError { - #[error("Failed integer conversion")] - 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] - #[serde(serialize_with = "serialize_io_error")] - std::io::Error, - ), -} - -#[derive(Debug, Error, serde::Serialize)] -pub enum SectionDsoDebugError { - #[error("Failed to write to memory")] - MemoryWriterError(#[from] MemoryWriterError), - #[error("Could not find: {0}")] - CouldNotFind(&'static str), - #[error("Failed to copy memory from process")] - CopyFromProcessError(#[from] DumperError), - #[error("Failed to copy memory from process")] - FromUTF8Error( - #[from] - #[serde(serialize_with = "serialize_from_utf8_error")] - std::string::FromUtf8Error, - ), -} - -#[derive(Debug, Error, serde::Serialize)] -pub enum WriterError { - #[error("Error during init phase")] - InitError(#[from] InitError), - #[error(transparent)] - DumperError(#[from] DumperError), - #[error("Failed when writing section AppMemory")] - SectionAppMemoryError(#[from] SectionAppMemoryError), - #[error("Failed when writing section ExceptionStream")] - SectionExceptionStreamError(#[from] SectionExceptionStreamError), - #[error("Failed when writing section HandleDataStream")] - SectionHandleDataStreamError(#[from] SectionHandleDataStreamError), - #[error("Failed when writing section MappingsError")] - SectionMappingsError(#[from] SectionMappingsError), - #[error("Failed when writing section MemList")] - SectionMemListError(#[from] SectionMemListError), - #[error("Failed when writing section SystemInfo")] - SectionSystemInfoError(#[from] SectionSystemInfoError), - #[error("Failed when writing section MemoryInfoList")] - SectionMemoryInfoListError(#[from] SectionMemInfoListError), - #[error("Failed when writing section ThreadList")] - SectionThreadListError(#[from] SectionThreadListError), - #[error("Failed when writing section ThreadNameList")] - SectionThreadNamesError(#[from] SectionThreadNamesError), - #[error("Failed when writing section DsoDebug")] - SectionDsoDebugError(#[from] SectionDsoDebugError), - #[error("Failed to write to memory")] - MemoryWriterError(#[from] MemoryWriterError), - #[error("Failed to write to file")] - FileWriterError(#[from] FileWriterError), - #[error("Failed to get current timestamp when writing header of minidump")] - 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, 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())] - ReadModuleMemory { - offset: u64, - length: u64, - start_address: Option, - #[source] - #[serde(serialize_with = "serialize_nix_error")] - error: nix::Error, - }, - #[error("failed to parse ELF memory: {0}")] - 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")] - NoStrTab, - #[error("no build id note sections")] - NoSectionNote, - #[error("the ELF data contains no program headers")] - NoProgramHeaders, - #[error("the ELF data contains no sections")] - NoSections, - #[error("the ELF data does not have a .text section from which to generate a build id")] - NoTextSection, - #[error( - "failed to calculate build id\n\ - ... from program headers: {program_headers}\n\ - ... from sections: {section}\n\ - ... from the text section: {section}" - )] - NoBuildId { - program_headers: Box, - section: Box, - generated: Box, - }, - #[error("no dynamic string table section")] - NoDynStrSection, - #[error("a string in the strtab did not have a terminating nul byte")] - StrTabNoNulByte, - #[error("no SONAME found in dynamic linking information")] - NoSoNameEntry, - #[error("no dynamic linking information section")] - NoDynamicSection, - #[error( - "failed to retrieve soname\n\ - ... from program headers: {program_headers}\n\ - ... from sections: {section}" - )] - NoSoName { - program_headers: Box, - section: Box, - }, -} diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 3113d35f..7f6e81e2 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -1,5 +1,6 @@ use { - crate::{auxv::AuxvType, errors::MapsReaderError}, + super::{auxv::AuxvType, module_reader::ModuleReaderError, serializers::*}, + crate::serializers::*, byteorder::{NativeEndian, ReadBytesExt}, goblin::elf, memmap2::{Mmap, MmapOptions}, @@ -55,10 +56,46 @@ pub struct MappingEntry { // A list of pub type MappingList = Vec; -#[derive(Debug)] -pub enum MappingInfoParsingResult { - SkipLine, - Success(MappingInfo), +#[derive(thiserror::Error, Debug, serde::Serialize)] +pub enum MapsReaderError { + #[error("Couldn't parse as ELF file")] + ELFParsingFailed( + #[from] + #[serde(serialize_with = "serialize_goblin_error")] + goblin::error::Error, + ), + #[error("No soname found (filename: {})", .0.to_string_lossy())] + NoSoName(OsString, #[source] ModuleReaderError), + + // parse_from_line() + #[error("Map entry malformed: No {0} found")] + MapEntryMalformed(&'static str), + #[error("Couldn't parse address")] + UnparsableInteger( + #[from] + #[serde(skip)] + std::num::ParseIntError, + ), + #[error("Linux gate location doesn't fit in the required integer type")] + 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] + #[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), } fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool { @@ -236,7 +273,7 @@ impl MappingInfo { .as_ref() .read_u64::() .map(|u| u as usize), - x => panic!("Unexpected type width: {}", x), + x => panic!("Unexpected type width: {x}"), }; if let Ok(addr) = addr { if low_addr <= addr && addr <= high_addr { diff --git a/src/linux/mem_reader.rs b/src/linux/mem_reader.rs index f8e4a040..e8d7d651 100644 --- a/src/linux/mem_reader.rs +++ b/src/linux/mem_reader.rs @@ -1,6 +1,6 @@ //! Functionality for reading a remote process's memory -use crate::{errors::CopyFromProcessError, ptrace_dumper::PtraceDumper, Pid}; +use super::{minidump_writer::MinidumpWriter, serializers::*, Pid}; enum Style { /// Uses [`process_vm_readv`](https://linux.die.net/man/2/process_vm_readv) @@ -28,6 +28,17 @@ enum Style { }, } +#[derive(Debug, thiserror::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, +} + pub struct MemReader { /// The pid of the child to read pid: nix::unistd::Pid, @@ -215,7 +226,7 @@ impl MemReader { } } -impl PtraceDumper { +impl MinidumpWriter { /// Copies a block of bytes from the target process, returning the heap /// allocated copy #[inline] @@ -223,21 +234,19 @@ impl PtraceDumper { pid: Pid, src: usize, length: usize, - ) -> Result, crate::errors::DumperError> { - let length = std::num::NonZeroUsize::new(length).ok_or( - crate::errors::DumperError::CopyFromProcessError(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, - }), - )?; + ) -> 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 mut mem = MemReader::new(pid); - Ok(mem.read_to_vec(src, length)?) + mem.read_to_vec(src, length) } } diff --git a/src/linux/minidump_writer.rs b/src/linux/minidump_writer.rs deleted file mode 100644 index 0c6e385a..00000000 --- a/src/linux/minidump_writer.rs +++ /dev/null @@ -1,467 +0,0 @@ -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::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, - }, -}; - -pub enum CrashingThreadContext { - None, - CrashContext(MDLocationDescriptor), - CrashContextPlusAddress((MDLocationDescriptor, usize)), -} - -/// The default timeout after a `SIGSTOP` after which minidump writing proceeds -/// regardless of the process state -pub const STOP_TIMEOUT: Duration = Duration::from_millis(100); - -pub struct MinidumpWriter { - pub process_id: Pid, - pub blamed_thread: Pid, - pub minidump_size_limit: Option, - pub skip_stacks_if_mapping_unreferenced: bool, - pub principal_mapping_address: Option, - pub user_mapping_list: MappingList, - pub app_memory: AppMemoryList, - pub memory_blocks: Vec, - pub principal_mapping: Option, - pub sanitize_stack: bool, - pub crash_context: Option, - pub crashing_thread_context: CrashingThreadContext, - pub stop_timeout: Duration, - pub direct_auxv_dump_info: Option, -} - -// This doesn't work yet: -// https://github.com/rust-lang/rust/issues/43408 -// fn write>(path: P, value: T) -> Result<()> { -// let mut file = std::fs::File::open(path)?; -// let bytes: [u8; size_of::()] = unsafe { transmute(value) }; -// file.write_all(&bytes)?; -// Ok(()) -// } - -type Result = std::result::Result; - -impl MinidumpWriter { - pub fn new(process: Pid, blamed_thread: Pid) -> Self { - Self { - process_id: process, - blamed_thread, - minidump_size_limit: None, - skip_stacks_if_mapping_unreferenced: false, - principal_mapping_address: None, - user_mapping_list: MappingList::new(), - app_memory: AppMemoryList::new(), - memory_blocks: Vec::new(), - principal_mapping: None, - sanitize_stack: false, - crash_context: None, - crashing_thread_context: CrashingThreadContext::None, - stop_timeout: STOP_TIMEOUT, - direct_auxv_dump_info: None, - } - } - - pub fn set_minidump_size_limit(&mut self, limit: u64) -> &mut Self { - self.minidump_size_limit = Some(limit); - self - } - - pub fn set_user_mapping_list(&mut self, user_mapping_list: MappingList) -> &mut Self { - self.user_mapping_list = user_mapping_list; - self - } - - pub fn set_principal_mapping_address(&mut self, principal_mapping_address: usize) -> &mut Self { - self.principal_mapping_address = Some(principal_mapping_address); - self - } - - pub fn set_app_memory(&mut self, app_memory: AppMemoryList) -> &mut Self { - self.app_memory = app_memory; - self - } - - pub fn set_crash_context(&mut self, crash_context: CrashContext) -> &mut Self { - self.crash_context = Some(crash_context); - self - } - - pub fn skip_stacks_if_mapping_unreferenced(&mut self) -> &mut Self { - self.skip_stacks_if_mapping_unreferenced = true; // Off by default - self - } - - pub fn sanitize_stack(&mut self) -> &mut Self { - self.sanitize_stack = true; // Off by default - self - } - - /// Sets the timeout after `SIGSTOP` is sent to the process, if the process - /// has not stopped by the time the timeout has reached, we proceed with - /// minidump generation - pub fn stop_timeout(&mut self, duration: Duration) -> &mut Self { - self.stop_timeout = duration; - self - } - - /// Directly set important Auxv info determined by the crashing process - /// - /// Since `/proc/{pid}/auxv` can sometimes be inaccessible, the calling process should prefer to transfer this - /// information directly using the Linux `getauxval()` call (if possible). - /// - /// Any field that is set to `0` will be considered unset. In that case, minidump-writer might try other techniques - /// to obtain it (like reading `/proc/{pid}/auxv`). - pub fn set_direct_auxv_dump_info( - &mut self, - direct_auxv_dump_info: DirectAuxvDumpInfo, - ) -> &mut Self { - self.direct_auxv_dump_info = Some(direct_auxv_dump_info); - self - } - - /// Generates a minidump and writes to the destination provided. Returns the in-memory - /// version of the minidump as well. - pub fn dump(&mut self, destination: &mut (impl Write + Seek)) -> Result> { - let auxv = self - .direct_auxv_dump_info - .clone() - .map(AuxvDumpInfo::from) - .unwrap_or_default(); - - 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 { - if let Some(address) = self.principal_mapping_address { - self.principal_mapping = dumper.find_mapping_no_bias(address).cloned(); - } - - if !self.crash_thread_references_principal_mapping(&dumper) { - soft_errors.push(WriterError::PrincipalMappingNotReferenced); - } - } - - let mut buffer = Buffer::with_capacity(0); - self.generate_dump(&mut buffer, &mut dumper, soft_errors, destination)?; - - Ok(buffer.into()) - } - - fn crash_thread_references_principal_mapping(&self, dumper: &PtraceDumper) -> bool { - if self.crash_context.is_none() || self.principal_mapping.is_none() { - return false; - } - - let low_addr = self - .principal_mapping - .as_ref() - .unwrap() - .system_mapping_info - .start_address; - let high_addr = self - .principal_mapping - .as_ref() - .unwrap() - .system_mapping_info - .end_address; - - let pc = self - .crash_context - .as_ref() - .unwrap() - .get_instruction_pointer(); - let stack_pointer = self.crash_context.as_ref().unwrap().get_stack_pointer(); - - if pc >= low_addr && pc < high_addr { - return true; - } - - let (valid_stack_pointer, stack_len) = match dumper.get_stack_info(stack_pointer) { - Ok(x) => x, - Err(_) => { - return false; - } - }; - - let stack_copy = match PtraceDumper::copy_from_process( - self.blamed_thread, - valid_stack_pointer, - stack_len, - ) { - Ok(x) => x, - Err(_) => { - return false; - } - }; - - let sp_offset = stack_pointer.saturating_sub(valid_stack_pointer); - self.principal_mapping - .as_ref() - .unwrap() - .stack_has_pointer_to_mapping(&stack_copy, sp_offset) - } - - fn generate_dump( - &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 = 18u32; - - let mut header_section = MemoryWriter::::alloc(buffer)?; - - let mut dir_section = DirSection::new(buffer, num_writers, destination)?; - - let header = MDRawHeader { - signature: MD_HEADER_SIGNATURE, - version: MD_HEADER_VERSION, - stream_count: num_writers, - // header.get()->stream_directory_rva = dir.position(); - stream_directory_rva: dir_section.position(), - checksum: 0, /* Can be 0. In fact, that's all that's - * been found in minidump files. */ - time_date_stamp: std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_secs() as u32, // TODO: This is not Y2038 safe, but thats how its currently defined as - flags: 0, - }; - header_section.set_value(buffer, header)?; - - // Ensure the header gets flushed. If we crash somewhere below, - // we should have a mostly-intact dump - dir_section.write_to_file(buffer, None)?; - - let dirent = thread_list_stream::write(self, buffer, dumper)?; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = mappings::write(self, buffer, dumper)?; - dir_section.write_to_file(buffer, Some(dirent))?; - - app_memory::write(self, buffer)?; - dir_section.write_to_file(buffer, None)?; - - let dirent = memory_list_stream::write(self, buffer)?; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = exception_stream::write(self, buffer)?; - dir_section.write_to_file(buffer, Some(dirent))?; - - 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)?; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = match self.write_file(buffer, "/proc/cpuinfo") { - Ok(location) => MDRawDirectory { - stream_type: MDStreamType::LinuxCpuInfo as u32, - location, - }, - Err(e) => { - soft_errors.push(WriterError::WriteCpuInfoFailed(e)); - Default::default() - } - }; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = match self.write_file(buffer, &format!("/proc/{}/status", self.blamed_thread)) - { - Ok(location) => MDRawDirectory { - stream_type: MDStreamType::LinuxProcStatus as u32, - location, - }, - Err(e) => { - soft_errors.push(WriterError::WriteThreadProcStatusFailed(e)); - Default::default() - } - }; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = match self - .write_file(buffer, "/etc/lsb-release") - .or_else(|_| self.write_file(buffer, "/etc/os-release")) - { - Ok(location) => MDRawDirectory { - stream_type: MDStreamType::LinuxLsbRelease as u32, - location, - }, - Err(e) => { - soft_errors.push(WriterError::WriteOsReleaseInfoFailed(e)); - Default::default() - } - }; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = match self.write_file(buffer, &format!("/proc/{}/cmdline", self.blamed_thread)) - { - Ok(location) => MDRawDirectory { - stream_type: MDStreamType::LinuxCmdLine as u32, - location, - }, - Err(e) => { - soft_errors.push(WriterError::WriteCommandLineFailed(e)); - Default::default() - } - }; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = match self.write_file(buffer, &format!("/proc/{}/environ", self.blamed_thread)) - { - Ok(location) => MDRawDirectory { - stream_type: MDStreamType::LinuxEnviron as u32, - location, - }, - Err(e) => { - soft_errors.push(WriterError::WriteEnvironmentFailed(e)); - Default::default() - } - }; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = match self.write_file(buffer, &format!("/proc/{}/auxv", self.blamed_thread)) { - Ok(location) => MDRawDirectory { - stream_type: MDStreamType::LinuxAuxv as u32, - location, - }, - Err(e) => { - soft_errors.push(WriterError::WriteAuxvFailed(e)); - Default::default() - } - }; - dir_section.write_to_file(buffer, Some(dirent))?; - - let dirent = match self.write_file(buffer, &format!("/proc/{}/maps", self.blamed_thread)) { - Ok(location) => MDRawDirectory { - stream_type: MDStreamType::LinuxMaps as u32, - location, - }, - Err(e) => { - soft_errors.push(WriterError::WriteMapsFailed(e)); - Default::default() - } - }; - dir_section.write_to_file(buffer, Some(dirent))?; - - 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)) - { - Ok(location) => MDRawDirectory { - stream_type: MDStreamType::MozLinuxLimits as u32, - location, - }, - 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))?; - - 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(()) - } - - #[allow(clippy::unused_self)] - fn write_file( - &self, - buffer: &mut DumpBuf, - filename: &str, - ) -> std::result::Result { - let content = std::fs::read(filename)?; - - let section = MemoryArrayWriter::write_bytes(buffer, &content); - 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/minidump_writer/app_memory.rs b/src/linux/minidump_writer/app_memory.rs new file mode 100644 index 00000000..2d65b646 --- /dev/null +++ b/src/linux/minidump_writer/app_memory.rs @@ -0,0 +1,28 @@ +use super::*; + +#[derive(Debug, Error, serde::Serialize)] +pub enum SectionAppMemoryError { + #[error("Failed to copy memory from process")] + CopyFromProcessError(#[from] CopyFromProcessError), + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), +} + +impl MinidumpWriter { + /// Write application-provided memory regions. + pub fn write_app_memory(&mut self, buffer: &mut DumpBuf) -> Result<(), SectionAppMemoryError> { + let blamed_thread = self.blamed_thread; + for app_memory in &self.app_memory { + let data_copy = + Self::copy_from_process(blamed_thread, app_memory.ptr, app_memory.length)?; + + let section = MemoryArrayWriter::write_bytes(buffer, &data_copy); + let desc = MDMemoryDescriptor { + start_of_memory_range: app_memory.ptr as u64, + memory: section.location(), + }; + self.memory_blocks.push(desc); + } + Ok(()) + } +} diff --git a/src/linux/minidump_writer/errors.rs b/src/linux/minidump_writer/errors.rs new file mode 100644 index 00000000..d73a9ce3 --- /dev/null +++ b/src/linux/minidump_writer/errors.rs @@ -0,0 +1,243 @@ +use { + super::super::{ + auxv::AuxvError, + dso_debug::SectionDsoDebugError, + maps_reader::MapsReaderError, + minidump_writer::{ + app_memory::SectionAppMemoryError, exception_stream::SectionExceptionStreamError, + handle_data_stream::SectionHandleDataStreamError, mappings::SectionMappingsError, + memory_info_list_stream::SectionMemInfoListError, + memory_list_stream::SectionMemListError, systeminfo_stream::SectionSystemInfoError, + thread_list_stream::SectionThreadListError, + thread_names_stream::SectionThreadNamesError, + }, + module_reader::ModuleReaderError, + serializers::*, + Pid, + }, + crate::{dir_section::FileWriterError, mem_writer::MemoryWriterError, serializers::*}, + error_graph::ErrorList, + nix::errno::Errno, + procfs_core::ProcError, + std::ffi::OsString, + thiserror::Error, +}; + +#[cfg(target_os = "android")] +use super::super::android::AndroidError; + +#[derive(Debug, Error, serde::Serialize)] +pub enum WriterError { + #[error("Error during init phase")] + InitError(#[from] InitError), + #[error("Failed when writing section AppMemory")] + SectionAppMemoryError(#[from] SectionAppMemoryError), + #[error("Failed when writing section ExceptionStream")] + SectionExceptionStreamError(#[from] SectionExceptionStreamError), + #[error("Failed when writing section HandleDataStream")] + SectionHandleDataStreamError(#[from] SectionHandleDataStreamError), + #[error("Failed when writing section MappingsError")] + SectionMappingsError(#[from] SectionMappingsError), + #[error("Failed when writing section MemList")] + SectionMemListError(#[from] SectionMemListError), + #[error("Failed when writing section SystemInfo")] + SectionSystemInfoError(#[from] SectionSystemInfoError), + #[error("Failed when writing section MemoryInfoList")] + SectionMemoryInfoListError(#[from] SectionMemInfoListError), + #[error("Failed when writing section ThreadList")] + SectionThreadListError(#[from] SectionThreadListError), + #[error("Failed when writing section ThreadNameList")] + SectionThreadNamesError(#[from] SectionThreadNamesError), + #[error("Failed when writing section DsoDebug")] + SectionDsoDebugError(#[from] SectionDsoDebugError), + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed to write to file")] + FileWriterError(#[from] FileWriterError), + #[error("Failed to get current timestamp when writing header of minidump")] + 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 resuming threads")] + ResumeThreadsErrors(#[source] ErrorList), + #[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("Failed to convert soft error list to JSON")] + ConvertToJsonFailed( + #[source] + #[serde(skip)] + serde_json::Error, + ), + #[error("nix::ptrace::attach(Pid={0}) failed")] + PtraceAttachError( + Pid, + #[source] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), + #[error("nix::ptrace::detach(Pid={0}) failed")] + PtraceDetachError( + Pid, + #[source] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), + #[error("wait::waitpid(Pid={0}) failed")] + WaitPidError( + Pid, + #[source] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), + #[error("Skipped thread {0} due to it being part of the seccomp sandbox's trusted code")] + DetachSkippedThread(Pid), + #[error("Maps reader error")] + MapsReaderError(#[from] MapsReaderError), + #[error("Failed to get PAGE_SIZE from system")] + SysConfError( + #[from] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), + + #[error("No mapping for stack pointer found")] + NoStackPointerMapping, + #[error("Failed slice conversion")] + TryFromSliceError( + #[from] + #[serde(skip)] + std::array::TryFromSliceError, + ), + #[error("Couldn't parse as ELF file")] + 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] + #[serde(skip)] + std::num::TryFromIntError, + ), +} + +#[derive(Debug, Error, serde::Serialize)] +pub enum InitError { + #[error("failed to read auxv")] + ReadAuxvFailed(#[source] super::super::auxv::AuxvError), + #[error("IO error for file {0}")] + IOError( + String, + #[source] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), + #[cfg(target_os = "android")] + #[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), + #[error("Errors occurred while suspending threads")] + SuspendThreadsErrors(#[source] ErrorList), + #[error("No threads left to suspend out of {0}")] + SuspendNoThreadsLeft(usize), + #[error("Crash thread does not reference principal mapping")] + PrincipalMappingNotReferenced, +} + +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum StopProcessError { + #[error("Failed to stop the process")] + Stop( + #[from] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), + #[error("Failed to get the process state")] + State( + #[from] + #[serde(serialize_with = "serialize_proc_error")] + ProcError, + ), + #[error("Timeout waiting for process to stop")] + Timeout, +} + +#[derive(Debug, thiserror::Error)] +pub enum ContinueProcessError { + #[error("Failed to continue the process")] + Continue(#[from] Errno), +} diff --git a/src/linux/minidump_writer/exception_stream.rs b/src/linux/minidump_writer/exception_stream.rs new file mode 100644 index 00000000..7bc7e2db --- /dev/null +++ b/src/linux/minidump_writer/exception_stream.rs @@ -0,0 +1,56 @@ +use {super::*, minidump_common::errors::ExceptionCodeLinux}; + +#[derive(Debug, Error, serde::Serialize)] +pub enum SectionExceptionStreamError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), +} + +impl MinidumpWriter { + pub fn write_exception_stream( + &mut self, + buffer: &mut DumpBuf, + ) -> Result { + let exception = if let Some(context) = &self.crash_context { + MDException { + exception_code: context.inner.siginfo.ssi_signo, + exception_flags: context.inner.siginfo.ssi_code as u32, + exception_address: context.inner.siginfo.ssi_addr, + ..Default::default() + } + } else { + let addr = match &self.crashing_thread_context { + CrashingThreadContext::CrashContextPlusAddress((_, addr)) => *addr, + _ => 0, + }; + MDException { + exception_code: ExceptionCodeLinux::DUMP_REQUESTED as u32, + exception_address: addr as u64, + ..Default::default() + } + }; + + let thread_context = match self.crashing_thread_context { + CrashingThreadContext::CrashContextPlusAddress((ctx, _)) + | CrashingThreadContext::CrashContext(ctx) => ctx, + CrashingThreadContext::None => MDLocationDescriptor { + data_size: 0, + rva: 0, + }, + }; + + let stream = MDRawExceptionStream { + thread_id: self.blamed_thread as u32, + exception_record: exception, + __align: 0, + thread_context, + }; + let exc = MemoryWriter::alloc_with_val(buffer, stream)?; + let dirent = MDRawDirectory { + stream_type: MDStreamType::ExceptionStream as u32, + location: exc.location(), + }; + + Ok(dirent) + } +} diff --git a/src/linux/minidump_writer/handle_data_stream.rs b/src/linux/minidump_writer/handle_data_stream.rs new file mode 100644 index 00000000..ea901a3d --- /dev/null +++ b/src/linux/minidump_writer/handle_data_stream.rs @@ -0,0 +1,104 @@ +use { + super::*, + crate::mem_writer::MemoryWriter, + std::{ + ffi::{CString, OsString}, + fs::{self, DirEntry}, + mem::{self}, + os::unix::prelude::OsStrExt, + path::{Path, PathBuf}, + }, +}; + +fn file_stat(path: &Path) -> Option { + let c_path = CString::new(path.as_os_str().as_bytes()).ok()?; + let mut stat = unsafe { std::mem::zeroed::() }; + let result = unsafe { libc::stat(c_path.as_ptr(), &mut stat) }; + + if result == 0 { + Some(stat) + } else { + None + } +} + +fn direntry_to_descriptor(buffer: &mut DumpBuf, entry: &DirEntry) -> Option { + let handle = filename_to_fd(&entry.file_name())?; + let realpath = fs::read_link(entry.path()).ok()?; + let path_rva = write_string_to_location(buffer, realpath.to_string_lossy().as_ref()).ok()?; + let stat = file_stat(&entry.path())?; + + // TODO: We store the contents of `st_mode` into the `attributes` field, but + // we could also store a human-readable string of the file type inside + // `type_name_rva`. We might move this missing information (and + // more) inside a custom `MINIDUMP_HANDLE_OBJECT_INFORMATION_TYPE` blob. + // That would make this conversion loss-less. + Some(MDRawHandleDescriptor { + handle, + type_name_rva: 0, + object_name_rva: path_rva.rva, + attributes: stat.st_mode, + granted_access: 0, + handle_count: 0, + pointer_count: 0, + }) +} + +fn filename_to_fd(filename: &OsString) -> Option { + let filename = filename.to_string_lossy(); + filename.parse::().ok() +} + +#[derive(Debug, Error, serde::Serialize)] +pub enum SectionHandleDataStreamError { + #[error("Failed to access file")] + 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] + #[serde(skip)] + std::num::TryFromIntError, + ), +} + +impl MinidumpWriter { + pub fn write_handle_data_stream( + &mut self, + buffer: &mut DumpBuf, + ) -> Result { + let proc_fd_path = PathBuf::from(format!("/proc/{}/fd", self.process_id)); + let proc_fd_iter = fs::read_dir(proc_fd_path)?; + let descriptors: Vec<_> = proc_fd_iter + .filter_map(|entry| entry.ok()) + .filter_map(|entry| direntry_to_descriptor(buffer, &entry)) + .collect(); + let number_of_descriptors = descriptors.len() as u32; + + let stream_header = MemoryWriter::::alloc_with_val( + buffer, + MDRawHandleDataStream { + size_of_header: mem::size_of::() as u32, + size_of_descriptor: mem::size_of::() as u32, + number_of_descriptors, + reserved: 0, + }, + )?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::HandleDataStream as u32, + location: stream_header.location(), + }; + + let descriptor_list = + MemoryArrayWriter::::alloc_from_iter(buffer, descriptors)?; + + dirent.location.data_size += descriptor_list.location().data_size; + Ok(dirent) + } +} diff --git a/src/linux/minidump_writer/mappings.rs b/src/linux/minidump_writer/mappings.rs new file mode 100644 index 00000000..b12e2511 --- /dev/null +++ b/src/linux/minidump_writer/mappings.rs @@ -0,0 +1,154 @@ +use super::{ + super::{ + maps_reader::MappingInfo, + module_reader::{BuildId, ReadFromModule, SoName}, + }, + *, +}; + +#[derive(Debug, Error, serde::Serialize)] +pub enum SectionMappingsError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed to get effective path of mapping ({0:?})")] + GetEffectivePathError(MappingInfo, #[source] MapsReaderError), +} + +impl MinidumpWriter { + /// Write information about the mappings in effect. Because we are using the + /// minidump format, the information about the mappings is pretty limited. + /// Because of this, we also include the full, unparsed, /proc/$x/maps file in + /// another stream in the file. + pub fn write_mappings( + &mut self, + buffer: &mut DumpBuf, + ) -> Result { + let mut modules = Vec::new(); + + // First write all the mappings from the dumper + for map_idx in 0..self.mappings.len() { + // If the mapping is uninteresting, or if + // there is caller-provided information about this mapping + // in the user_mapping_list list, skip it + + if !self.mappings[map_idx].is_interesting() + || self.mappings[map_idx].is_contained_in(&self.user_mapping_list) + { + continue; + } + log::debug!("retrieving build id for {:?}", &self.mappings[map_idx]); + let BuildId(identifier) = self + .from_process_memory_for_index(map_idx) + .or_else(|e| { + // If the mapping has an associated name that is a file, try to read the build id + // from the file. If there is no note segment with the build id in + // the program headers, we can't get to the note section if the section header + // table isn't loaded. + if let Some(path) = &self.mappings[map_idx].name { + let path = std::path::Path::new(&path); + if path.exists() { + log::debug!("failed to get build id from process memory ({e}), attempting to retrieve from {}", path.display()); + return BuildId::read_from_file(path) + .map_err(errors::WriterError::ModuleReaderError); + } + log::debug!( + "not attempting to get build id from {}: path does not exist", + path.display() + ); + } + Err(e) + }) + .unwrap_or_else(|e| { + log::warn!("failed to get build id for mapping: {e}"); + BuildId(Vec::new()) + }); + + // If the identifier is all 0, its an uninteresting mapping (bmc#1676109) + if identifier.is_empty() || identifier.iter().all(|&x| x == 0) { + continue; + } + + // SONAME should always be accessible through program headers alone, so we don't really + // need to fall back to trying to read from the mapping file. + let soname = self + .from_process_memory_for_index(map_idx) + .ok() + .map(|SoName(n)| n); + + let module = fill_raw_module(buffer, &self.mappings[map_idx], &identifier, soname)?; + modules.push(module); + } + + // Next write all the mappings provided by the caller + for user in &self.user_mapping_list { + // GUID was provided by caller. + let module = fill_raw_module(buffer, &user.mapping, &user.identifier, None)?; + modules.push(module); + } + + let list_header = MemoryWriter::::alloc_with_val(buffer, modules.len() as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ModuleListStream as u32, + location: list_header.location(), + }; + + if !modules.is_empty() { + let mapping_list = MemoryArrayWriter::::alloc_from_iter(buffer, modules)?; + dirent.location.data_size += mapping_list.location().data_size; + } + + Ok(dirent) + } +} +fn fill_raw_module( + buffer: &mut DumpBuf, + mapping: &MappingInfo, + identifier: &[u8], + soname: Option, +) -> Result { + let cv_record = if identifier.is_empty() { + // Just zeroes + Default::default() + } else { + let cv_signature = crate::minidump_format::format::CvSignature::Elf as u32; + let array_size = std::mem::size_of_val(&cv_signature) + identifier.len(); + + let mut sig_section = MemoryArrayWriter::::alloc_array(buffer, array_size)?; + for (index, val) in cv_signature + .to_ne_bytes() + .iter() + .chain(identifier.iter()) + .enumerate() + { + sig_section.set_value_at(buffer, *val, index)?; + } + sig_section.location() + }; + + let (file_path, _, so_version) = mapping + .get_mapping_effective_path_name_and_version(soname) + .map_err(|e| SectionMappingsError::GetEffectivePathError(mapping.clone(), e))?; + let name_header = write_string_to_location(buffer, file_path.to_string_lossy().as_ref())?; + + let version_info = so_version.map_or(Default::default(), |sov| format::VS_FIXEDFILEINFO { + signature: format::VS_FFI_SIGNATURE, + struct_version: format::VS_FFI_STRUCVERSION, + file_version_hi: sov.major, + file_version_lo: sov.minor, + product_version_hi: sov.patch, + product_version_lo: sov.prerelease, + ..Default::default() + }); + + let raw_module = MDRawModule { + base_of_image: mapping.start_address as u64, + size_of_image: mapping.size as u32, + cv_record, + module_name_rva: name_header.rva, + version_info, + ..Default::default() + }; + + Ok(raw_module) +} diff --git a/src/linux/minidump_writer/memory_info_list_stream.rs b/src/linux/minidump_writer/memory_info_list_stream.rs new file mode 100644 index 00000000..89581c37 --- /dev/null +++ b/src/linux/minidump_writer/memory_info_list_stream.rs @@ -0,0 +1,83 @@ +use { + super::*, + minidump_common::format::{MemoryProtection, MemoryState, MemoryType}, + procfs_core::{process::MMPermissions, FromRead}, +}; + +#[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] + #[serde(serialize_with = "serialize_proc_error")] + procfs_core::ProcError, + ), +} + +impl MinidumpWriter { + /// Write a MemoryInfoListStream using information from procfs. + pub fn write_memory_info_list_stream( + &mut self, + buffer: &mut DumpBuf, + ) -> Result { + let maps = procfs_core::process::MemoryMaps::from_file(std::path::PathBuf::from(format!( + "/proc/{}/maps", + self.blamed_thread + )))?; + + let list_header = MemoryWriter::alloc_with_val( + buffer, + MDMemoryInfoList { + size_of_header: std::mem::size_of::() as u32, + size_of_entry: std::mem::size_of::() as u32, + number_of_entries: maps.len() as u64, + }, + )?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::MemoryInfoListStream as u32, + location: list_header.location(), + }; + + let block_list = MemoryArrayWriter::::alloc_from_iter( + buffer, + maps.iter().map(|mm| MDMemoryInfo { + base_address: mm.address.0, + allocation_base: mm.address.0, + allocation_protection: get_memory_protection(mm.perms).bits(), + __alignment1: 0, + region_size: mm.address.1 - mm.address.0, + state: MemoryState::MEM_COMMIT.bits(), + protection: get_memory_protection(mm.perms).bits(), + _type: if mm.perms.contains(MMPermissions::PRIVATE) { + MemoryType::MEM_PRIVATE + } else { + MemoryType::MEM_MAPPED + } + .bits(), + __alignment2: 0, + }), + )?; + + dirent.location.data_size += block_list.location().data_size; + + Ok(dirent) + } +} +fn get_memory_protection(permissions: MMPermissions) -> MemoryProtection { + let read = permissions.contains(MMPermissions::READ); + let write = permissions.contains(MMPermissions::WRITE); + let exec = permissions.contains(MMPermissions::EXECUTE); + match (read, write, exec) { + (false, false, false) => MemoryProtection::PAGE_NOACCESS, + (false, false, true) => MemoryProtection::PAGE_EXECUTE, + (true, false, false) => MemoryProtection::PAGE_READONLY, + (true, false, true) => MemoryProtection::PAGE_EXECUTE_READ, + // No support for write-only + (true | false, true, false) => MemoryProtection::PAGE_READWRITE, + // No support for execute+write-only + (true | false, true, true) => MemoryProtection::PAGE_EXECUTE_READWRITE, + } +} diff --git a/src/linux/minidump_writer/memory_list_stream.rs b/src/linux/minidump_writer/memory_list_stream.rs new file mode 100644 index 00000000..eea8ce8e --- /dev/null +++ b/src/linux/minidump_writer/memory_list_stream.rs @@ -0,0 +1,29 @@ +use super::*; + +#[derive(Debug, Error, serde::Serialize)] +pub enum SectionMemListError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), +} + +impl MinidumpWriter { + pub fn write_memory_list_stream( + &mut self, + buffer: &mut DumpBuf, + ) -> Result { + let list_header = + MemoryWriter::::alloc_with_val(buffer, self.memory_blocks.len() as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::MemoryListStream as u32, + location: list_header.location(), + }; + + let block_list = + MemoryArrayWriter::::alloc_from_array(buffer, &self.memory_blocks)?; + + dirent.location.data_size += block_list.location().data_size; + + Ok(dirent) + } +} diff --git a/src/linux/ptrace_dumper.rs b/src/linux/minidump_writer/mod.rs similarity index 50% rename from src/linux/ptrace_dumper.rs rename to src/linux/minidump_writer/mod.rs index 7b04a2f5..8666e689 100644 --- a/src/linux/ptrace_dumper.rs +++ b/src/linux/minidump_writer/mod.rs @@ -1,21 +1,27 @@ use { super::{ - auxv::AuxvError, - errors::{AndroidError, MapsReaderError}, + app_memory::AppMemoryList, + auxv::AuxvDumpInfo, + crash_context::CrashContext, + dso_debug, + dumper_cpu_info::CpuInfoError, + maps_reader::{MappingInfo, MappingList, MapsReaderError}, + mem_reader::CopyFromProcessError, + module_reader, serializers::*, + thread_info::{ThreadInfo, ThreadInfoError}, + Pid, }, crate::{ - linux::{ - auxv::AuxvDumpInfo, - errors::{DumperError, ThreadInfoError}, - maps_reader::MappingInfo, - module_reader, - thread_info::ThreadInfo, - Pid, + dir_section::{DirSection, DumpBuf}, + mem_writer::{ + write_string_to_location, Buffer, MemoryArrayWriter, MemoryWriter, MemoryWriterError, }, + minidump_format::*, serializers::*, }, error_graph::{ErrorList, WriteErrorList}, + errors::{ContinueProcessError, InitError, StopProcessError, WriterError}, failspot::failspot, nix::{ errno::Errno, @@ -23,190 +29,240 @@ use { }, procfs_core::{ process::{MMPermissions, ProcState, Stat}, - FromRead, ProcError, + FromRead, }, std::{ - ffi::OsString, + io::{Seek, Write}, path, - result::Result, time::{Duration, Instant}, }, thiserror::Error, }; #[cfg(target_os = "android")] -use crate::linux::android::late_process_mappings; +use super::android::late_process_mappings; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] -use crate::thread_info; +use super::thread_info; -#[derive(Debug, Clone)] -pub struct Thread { - pub tid: Pid, - pub name: Option, -} +pub use super::auxv::{AuxvType, DirectAuxvDumpInfo}; -#[derive(Debug)] -pub struct PtraceDumper { - pub pid: Pid, - threads_suspended: bool, - pub threads: Vec, - pub auxv: AuxvDumpInfo, - pub mappings: Vec, - pub page_size: usize, -} +pub mod app_memory; +pub mod errors; +pub mod exception_stream; +pub mod handle_data_stream; +pub mod mappings; +pub mod memory_info_list_stream; +pub mod memory_list_stream; +pub mod systeminfo_stream; +pub mod thread_list_stream; +pub mod thread_names_stream; + +/// The default timeout after a `SIGSTOP` after which minidump writing proceeds +/// regardless of the process state +pub const STOP_TIMEOUT: Duration = Duration::from_millis(100); #[cfg(target_pointer_width = "32")] pub const AT_SYSINFO_EHDR: u32 = 33; #[cfg(target_pointer_width = "64")] 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) - self.resume_threads(error_graph::strategy::DontCare); - // Always allow the process to continue. - let _ = self.continue_process(); - } +#[derive(Debug)] +pub struct MinidumpWriterConfig { + process_id: Pid, + blamed_thread: Pid, + minidump_size_limit: Option, + skip_stacks_if_mapping_unreferenced: bool, + principal_mapping_address: Option, + user_mapping_list: MappingList, + app_memory: AppMemoryList, + memory_blocks: Vec, + principal_mapping: Option, + sanitize_stack: bool, + crash_context: Option, + crashing_thread_context: CrashingThreadContext, + stop_timeout: Duration, + direct_auxv_dump_info: Option, } -#[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)] +pub struct MinidumpWriter { + pub process_id: Pid, + threads_suspended: bool, + pub threads: Vec, + pub auxv: AuxvDumpInfo, + pub mappings: Vec, + pub page_size: usize, + pub sanitize_stack: bool, + pub minidump_size_limit: Option, + pub user_mapping_list: MappingList, + pub crashing_thread_context: CrashingThreadContext, + stop_timeout: Duration, + pub skip_stacks_if_mapping_unreferenced: bool, + principal_mapping_address: Option, + pub principal_mapping: Option, + pub blamed_thread: Pid, + pub crash_context: Option, + pub app_memory: AppMemoryList, + pub memory_blocks: Vec, } -#[derive(Debug, thiserror::Error, serde::Serialize)] -pub enum StopProcessError { - #[error("Failed to stop the process")] - Stop( - #[from] - #[serde(serialize_with = "serialize_nix_error")] - nix::Error, - ), - #[error("Failed to get the process state")] - State( - #[from] - #[serde(serialize_with = "serialize_proc_error")] - ProcError, - ), - #[error("Timeout waiting for process to stop")] - Timeout, +#[derive(Debug, Clone)] +pub struct Thread { + pub tid: Pid, + pub name: Option, } -#[derive(Debug, thiserror::Error)] -pub enum ContinueProcessError { - #[error("Failed to continue the process")] - Continue(#[from] Errno), +#[derive(Debug, Default)] +pub enum CrashingThreadContext { + #[default] + None, + CrashContext(MDLocationDescriptor), + CrashContextPlusAddress((MDLocationDescriptor, usize)), } -/// PTRACE_DETACH the given pid. -/// -/// This handles special errno cases (ESRCH) which we won't consider errors. -fn ptrace_detach(child: Pid) -> Result<(), DumperError> { - let pid = nix::unistd::Pid::from_raw(child); - ptrace::detach(pid, None).or_else(|e| { - // errno is set to ESRCH if the pid no longer exists, but we don't want to error in that - // case. - if e == nix::Error::ESRCH { - Ok(()) - } else { - Err(DumperError::PtraceDetachError(child, e)) +impl MinidumpWriterConfig { + pub fn new(process_id: Pid, blamed_thread: Pid) -> Self { + Self { + process_id, + blamed_thread, + minidump_size_limit: Default::default(), + skip_stacks_if_mapping_unreferenced: Default::default(), + principal_mapping_address: Default::default(), + user_mapping_list: Default::default(), + app_memory: Default::default(), + memory_blocks: Default::default(), + principal_mapping: Default::default(), + sanitize_stack: Default::default(), + crash_context: Default::default(), + crashing_thread_context: Default::default(), + stop_timeout: STOP_TIMEOUT, + direct_auxv_dump_info: Default::default(), } - }) -} + } -impl PtraceDumper { - /// Constructs a dumper for extracting information from the specified process id - 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); - } + pub fn set_minidump_size_limit(&mut self, limit: u64) -> &mut Self { + self.minidump_size_limit = Some(limit); + self + } + + pub fn set_user_mapping_list(&mut self, user_mapping_list: MappingList) -> &mut Self { + self.user_mapping_list = user_mapping_list; + self + } + + pub fn set_principal_mapping_address(&mut self, principal_mapping_address: usize) -> &mut Self { + self.principal_mapping_address = Some(principal_mapping_address); + self + } + + pub fn set_app_memory(&mut self, app_memory: AppMemoryList) -> &mut Self { + self.app_memory = app_memory; + self + } + + pub fn set_crash_context(&mut self, crash_context: CrashContext) -> &mut Self { + self.crash_context = Some(crash_context); + self + } + + pub fn skip_stacks_if_mapping_unreferenced(&mut self) -> &mut Self { + self.skip_stacks_if_mapping_unreferenced = true; // Off by default + self + } + + pub fn sanitize_stack(&mut self) -> &mut Self { + self.sanitize_stack = true; // Off by default + self + } + + /// Sets the timeout after `SIGSTOP` is sent to the process, if the process + /// has not stopped by the time the timeout has reached, we proceed with + /// minidump generation + pub fn stop_timeout(&mut self, duration: Duration) -> &mut Self { + self.stop_timeout = duration; + self + } + + /// Directly set important Auxv info determined by the crashing process + /// + /// Since `/proc/{pid}/auxv` can sometimes be inaccessible, the calling process should prefer to transfer this + /// information directly using the Linux `getauxval()` call (if possible). + /// + /// Any field that is set to `0` will be considered unset. In that case, minidump-writer might try other techniques + /// to obtain it (like reading `/proc/{pid}/auxv`). + pub fn set_direct_auxv_dump_info( + &mut self, + direct_auxv_dump_info: DirectAuxvDumpInfo, + ) -> &mut Self { + self.direct_auxv_dump_info = Some(direct_auxv_dump_info); + self + } + /// Generates a minidump and writes to the destination provided. Returns the in-memory + /// version of the minidump as well. + pub fn write(self, destination: &mut (impl Write + Seek)) -> Result, WriterError> { + let mut soft_errors = ErrorList::default(); + + let mut writer = self.build(); + writer.init(soft_errors.subwriter(WriterError::InitErrors))?; - let mut dumper = Self { - pid, - threads_suspended: false, - threads: Vec::new(), + let mut buffer = Buffer::with_capacity(0); + writer.write_dump(&mut buffer, destination, soft_errors)?; + Ok(buffer.into()) + } + /// Allows testing code to inspect the pre-output state of the MinidumpWriter + pub fn build_for_testing( + self, + soft_errors: impl WriteErrorList, + ) -> Result { + let mut writer = self.build(); + writer.init(soft_errors)?; + Ok(writer) + } + fn build(self) -> MinidumpWriter { + let auxv = self + .direct_auxv_dump_info + .map(AuxvDumpInfo::from) + .unwrap_or_default(); + + MinidumpWriter { + process_id: self.process_id, + threads_suspended: Default::default(), + threads: Default::default(), auxv, - mappings: Vec::new(), - page_size: 0, - }; - dumper.init(stop_timeout, soft_errors)?; - Ok(dumper) + mappings: Default::default(), + page_size: Default::default(), + sanitize_stack: self.sanitize_stack, + minidump_size_limit: self.minidump_size_limit, + user_mapping_list: self.user_mapping_list, + crashing_thread_context: self.crashing_thread_context, + stop_timeout: self.stop_timeout, + skip_stacks_if_mapping_unreferenced: self.skip_stacks_if_mapping_unreferenced, + principal_mapping_address: self.principal_mapping_address, + principal_mapping: self.principal_mapping, + blamed_thread: self.blamed_thread, + crash_context: self.crash_context, + app_memory: self.app_memory, + memory_blocks: self.memory_blocks, + } } +} +impl MinidumpWriter { // TODO: late_init for chromeos and android - pub fn init( - &mut self, - stop_timeout: Duration, - mut soft_errors: impl WriteErrorList, - ) -> Result<(), InitError> { + fn init(&mut self, mut soft_errors: impl WriteErrorList) -> Result<(), InitError> { + if self.process_id == std::process::id() as i32 { + return Err(InitError::CannotPtraceSameProcess); + } + // Stopping the process is best-effort. - if let Err(e) = self.stop_process(stop_timeout) { + if let Err(e) = self.stop_process(self.stop_timeout) { soft_errors.push(InitError::StopProcessFailed(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, + self.process_id, soft_errors.subwriter(InitError::FillMissingAuxvInfoErrors), ) { soft_errors.push(InitError::FillMissingAuxvInfoFailed(e)); @@ -229,21 +285,279 @@ impl PtraceDumper { .expect("page size apparently unlimited: doesn't make sense.") as usize; - Ok(()) - } + let threads_count = self.threads.len(); + + self.suspend_threads(soft_errors.subwriter(InitError::SuspendThreadsErrors)); + + if self.threads.is_empty() { + soft_errors.push(InitError::SuspendNoThreadsLeft(threads_count)); + } - #[cfg_attr(not(target_os = "android"), allow(clippy::unused_self))] - pub fn late_init(&mut self) -> Result<(), InitError> { #[cfg(target_os = "android")] { - late_process_mappings(self.pid, &mut self.mappings)?; + late_process_mappings(self.process_id, &mut self.mappings)?; } + + if self.skip_stacks_if_mapping_unreferenced { + if let Some(address) = self.principal_mapping_address { + self.principal_mapping = self.find_mapping_no_bias(address).cloned(); + } + + if !self.crash_thread_references_principal_mapping() { + soft_errors.push(InitError::PrincipalMappingNotReferenced); + } + } + + Ok(()) + } + /// Generates a minidump and writes to the destination provided. Returns the in-memory + /// version of the minidump as well. + fn write_dump( + &mut self, + buffer: &mut DumpBuf, + destination: &mut (impl Write + Seek), + mut soft_errors: ErrorList, + ) -> Result<(), WriterError> { + // A minidump file contains a number of tagged streams. This is the number + // of streams which we write. + let num_writers = 18u32; + + let mut header_section = MemoryWriter::::alloc(buffer)?; + + let mut dir_section = DirSection::new(buffer, num_writers, destination)?; + + let header = MDRawHeader { + signature: MD_HEADER_SIGNATURE, + version: MD_HEADER_VERSION, + stream_count: num_writers, + // header.get()->stream_directory_rva = dir.position(); + stream_directory_rva: dir_section.position(), + checksum: 0, /* Can be 0. In fact, that's all that's + * been found in minidump files. */ + time_date_stamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs() as u32, // TODO: This is not Y2038 safe, but thats how its currently defined as + flags: 0, + }; + header_section.set_value(buffer, header)?; + + // Ensure the header gets flushed. If we crash somewhere below, + // we should have a mostly-intact dump + dir_section.write_to_file(buffer, None)?; + + let dirent = self.write_thread_list_stream(buffer)?; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = self.write_mappings(buffer)?; + dir_section.write_to_file(buffer, Some(dirent))?; + + self.write_app_memory(buffer)?; + dir_section.write_to_file(buffer, None)?; + + let dirent = self.write_memory_list_stream(buffer)?; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = self.write_exception_stream(buffer)?; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = systeminfo_stream::write( + buffer, + soft_errors.subwriter(WriterError::WriteSystemInfoErrors), + )?; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = self.write_memory_info_list_stream(buffer)?; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match write_file(buffer, "/proc/cpuinfo") { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxCpuInfo as u32, + location, + }, + Err(e) => { + soft_errors.push(WriterError::WriteCpuInfoFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match write_file(buffer, &format!("/proc/{}/status", self.blamed_thread)) { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxProcStatus as u32, + location, + }, + Err(e) => { + soft_errors.push(WriterError::WriteThreadProcStatusFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match write_file(buffer, "/etc/lsb-release") + .or_else(|_| write_file(buffer, "/etc/os-release")) + { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxLsbRelease as u32, + location, + }, + Err(e) => { + soft_errors.push(WriterError::WriteOsReleaseInfoFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match write_file(buffer, &format!("/proc/{}/cmdline", self.blamed_thread)) { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxCmdLine as u32, + location, + }, + Err(e) => { + soft_errors.push(WriterError::WriteCommandLineFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match write_file(buffer, &format!("/proc/{}/environ", self.blamed_thread)) { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxEnviron as u32, + location, + }, + Err(e) => { + soft_errors.push(WriterError::WriteEnvironmentFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match write_file(buffer, &format!("/proc/{}/auxv", self.blamed_thread)) { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxAuxv as u32, + location, + }, + Err(e) => { + soft_errors.push(WriterError::WriteAuxvFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match write_file(buffer, &format!("/proc/{}/maps", self.blamed_thread)) { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::LinuxMaps as u32, + location, + }, + Err(e) => { + soft_errors.push(WriterError::WriteMapsFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match dso_debug::write_dso_debug_stream(buffer, self.process_id, &self.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 write_file(buffer, &format!("/proc/{}/limits", self.blamed_thread)) { + Ok(location) => MDRawDirectory { + stream_type: MDStreamType::MozLinuxLimits as u32, + location, + }, + Err(e) => { + soft_errors.push(WriterError::WriteLimitsFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = self.write_thread_names_stream(buffer)?; + dir_section.write_to_file(buffer, Some(dirent))?; + + let dirent = match self.write_handle_data_stream(buffer) { + Ok(dirent) => dirent, + Err(e) => { + soft_errors.push(WriterError::WriteHandleDataStreamFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + // 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(()) } + fn crash_thread_references_principal_mapping(&self) -> bool { + if self.crash_context.is_none() || self.principal_mapping.is_none() { + return false; + } + + let low_addr = self + .principal_mapping + .as_ref() + .unwrap() + .system_mapping_info + .start_address; + let high_addr = self + .principal_mapping + .as_ref() + .unwrap() + .system_mapping_info + .end_address; + + let pc = self + .crash_context + .as_ref() + .unwrap() + .get_instruction_pointer(); + let stack_pointer = self.crash_context.as_ref().unwrap().get_stack_pointer(); + + if pc >= low_addr && pc < high_addr { + return true; + } + + let (valid_stack_pointer, stack_len) = match self.get_stack_info(stack_pointer) { + Ok(x) => x, + Err(_) => { + return false; + } + }; + + let stack_copy = match MinidumpWriter::copy_from_process( + self.blamed_thread, + valid_stack_pointer, + stack_len, + ) { + Ok(x) => x, + Err(_) => { + return false; + } + }; + + let sp_offset = stack_pointer.saturating_sub(valid_stack_pointer); + self.principal_mapping + .as_ref() + .unwrap() + .stack_has_pointer_to_mapping(&stack_copy, sp_offset) + } + /// Suspends a thread by attaching to it. - pub fn suspend_thread(child: Pid) -> Result<(), DumperError> { - use DumperError::PtraceAttachError as AttachErr; + fn suspend_thread(child: Pid) -> Result<(), WriterError> { + use WriterError::PtraceAttachError as AttachErr; let pid = nix::unistd::Pid::from_raw(child); // This may fail if the thread has just died or debugged. @@ -252,7 +566,7 @@ impl PtraceDumper { match wait::waitpid(pid, Some(wait::WaitPidFlag::__WALL)) { Ok(status) => { let wait::WaitStatus::Stopped(_, status) = status else { - return Err(DumperError::WaitPidError( + return Err(WriterError::WaitPidError( child, nix::errno::Errno::UnknownErrno, )); @@ -268,13 +582,13 @@ impl PtraceDumper { // Signals other than SIGSTOP that are received need to be reinjected, // or they will otherwise get lost. if let Err(err) = ptrace::cont(pid, status) { - return Err(DumperError::WaitPidError(child, err)); + return Err(WriterError::WaitPidError(child, err)); } } Err(Errno::EINTR) => continue, Err(e) => { ptrace_detach(child)?; - return Err(DumperError::WaitPidError(child, e)); + return Err(WriterError::WaitPidError(child, e)); } } } @@ -303,18 +617,18 @@ impl PtraceDumper { } if skip_thread { ptrace_detach(child)?; - return Err(DumperError::DetachSkippedThread(child)); + return Err(WriterError::DetachSkippedThread(child)); } } Ok(()) } /// Resumes a thread by detaching from it. - pub fn resume_thread(child: Pid) -> Result<(), DumperError> { + fn resume_thread(child: Pid) -> Result<(), WriterError> { ptrace_detach(child) } - pub fn suspend_threads(&mut self, mut soft_errors: impl WriteErrorList) { + 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 @@ -329,10 +643,10 @@ impl PtraceDumper { self.threads_suspended = true; - failspot::failspot!(::SuspendThreads soft_errors.push(DumperError::PtraceAttachError(1234, nix::Error::EPERM))) + failspot::failspot!(::SuspendThreads soft_errors.push(WriterError::PtraceAttachError(1234, nix::Error::EPERM))) } - pub fn resume_threads(&mut self, mut soft_errors: impl WriteErrorList) { + 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) { @@ -352,12 +666,15 @@ impl PtraceDumper { 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))?; + signal::kill( + nix::unistd::Pid::from_raw(self.process_id), + Some(signal::SIGSTOP), + )?; // Something like waitpid for non-child processes would be better, but we have no such // tool, so we poll the status. const POLL_INTERVAL: Duration = Duration::from_millis(1); - let proc_file = format!("/proc/{}/stat", self.pid); + let proc_file = format!("/proc/{}/stat", self.process_id); let end = Instant::now() + timeout; loop { @@ -376,7 +693,10 @@ impl PtraceDumper { /// /// Unlike `stop_process`, this function does not wait for the process to continue. fn continue_process(&mut self) -> Result<(), ContinueProcessError> { - signal::kill(nix::unistd::Pid::from_raw(self.pid), Some(signal::SIGCONT))?; + signal::kill( + nix::unistd::Pid::from_raw(self.process_id), + Some(signal::SIGCONT), + )?; Ok(()) } @@ -386,8 +706,8 @@ impl PtraceDumper { &mut self, mut soft_errors: impl WriteErrorList, ) -> Result<(), InitError> { - let pid = self.pid; - let filename = format!("/proc/{}/task", pid); + let pid = self.process_id; + let filename = format!("/proc/{pid}/task"); let task_path = path::PathBuf::from(&filename); if !task_path.is_dir() { return Err(InitError::ProcPidTaskNotDirectory(filename)); @@ -416,7 +736,7 @@ impl PtraceDumper { "testing requested failure reading thread name", )) } else { - std::fs::read_to_string(format!("/proc/{}/task/{}/comm", pid, tid)) + std::fs::read_to_string(format!("/proc/{pid}/task/{tid}/comm")) }); let name = match name_result { @@ -441,7 +761,7 @@ 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 maps_path = format!("/proc/{}/maps", self.pid); + 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))?; @@ -483,14 +803,14 @@ impl PtraceDumper { return Err(ThreadInfoError::IndexOutOfBounds(index, self.threads.len())); } - ThreadInfo::create(self.pid, self.threads[index].tid) + ThreadInfo::create(self.process_id, self.threads[index].tid) } // Returns a valid stack pointer and the mapping that contains the stack. // The stack pointer will usually point within this mapping, but it might // not in case of stack overflows, hence the returned pointer might be // different from the one that was passed in. - pub fn get_stack_info(&self, int_stack_pointer: usize) -> Result<(usize, usize), DumperError> { + pub fn get_stack_info(&self, int_stack_pointer: usize) -> Result<(usize, usize), WriterError> { // Round the stack pointer to the nearest page, this will cause us to // capture data below the stack pointer which might still be relevant. let mut stack_pointer = int_stack_pointer & !(self.page_size - 1); @@ -522,7 +842,7 @@ impl PtraceDumper { let stack_len = mapping.size - (valid_stack_pointer - mapping.start_address); (valid_stack_pointer, stack_len) }) - .ok_or(DumperError::NoStackPointerMapping) + .ok_or(WriterError::NoStackPointerMapping) } fn may_be_stack(mapping: Option<&MappingInfo>) -> bool { @@ -540,7 +860,7 @@ impl PtraceDumper { stack_copy: &mut [u8], stack_pointer: usize, sp_offset: usize, - ) -> Result<(), DumperError> { + ) -> Result<(), WriterError> { // We optimize the search for containing mappings in three ways: // 1) We expect that pointers into the stack mapping will be common, so // we cache that address range. @@ -664,18 +984,63 @@ impl PtraceDumper { pub fn from_process_memory_for_index( &mut self, idx: usize, - ) -> Result { + ) -> Result { assert!(idx < self.mappings.len()); - Self::from_process_memory_for_mapping(&self.mappings[idx], self.pid) + Self::from_process_memory_for_mapping(&self.mappings[idx], self.process_id) } pub fn from_process_memory_for_mapping( mapping: &MappingInfo, pid: Pid, - ) -> Result { + ) -> Result { Ok(T::read_from_module( module_reader::ProcessReader::new(pid, mapping.start_address).into(), )?) } } + +impl Drop for MinidumpWriter { + fn drop(&mut self) { + // Always try to resume all threads (e.g. in case of error) + self.resume_threads(error_graph::strategy::DontCare); + // Always allow the process to continue. + let _ = self.continue_process(); + } +} + +/// PTRACE_DETACH the given pid. +/// +/// This handles special errno cases (ESRCH) which we won't consider errors. +fn ptrace_detach(child: Pid) -> Result<(), WriterError> { + let pid = nix::unistd::Pid::from_raw(child); + ptrace::detach(pid, None).or_else(|e| { + // errno is set to ESRCH if the pid no longer exists, but we don't want to error in that + // case. + if e == nix::Error::ESRCH { + Ok(()) + } else { + Err(WriterError::PtraceDetachError(child, e)) + } + }) +} + +fn write_file( + buffer: &mut DumpBuf, + filename: &str, +) -> std::result::Result { + let content = std::fs::read(filename)?; + + let section = MemoryArrayWriter::write_bytes(buffer, &content); + 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/sections/systeminfo_stream.rs b/src/linux/minidump_writer/systeminfo_stream.rs similarity index 67% rename from src/linux/sections/systeminfo_stream.rs rename to src/linux/minidump_writer/systeminfo_stream.rs index 62ca332c..4e077701 100644 --- a/src/linux/sections/systeminfo_stream.rs +++ b/src/linux/minidump_writer/systeminfo_stream.rs @@ -1,8 +1,18 @@ use { - super::*, crate::linux::dumper_cpu_info as dci, error_graph::WriteErrorList, - errors::SectionSystemInfoError, + super::{super::dumper_cpu_info as dci, *}, + error_graph::WriteErrorList, }; +#[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), +} + pub fn write( buffer: &mut DumpBuf, mut soft_errors: impl WriteErrorList, diff --git a/src/linux/minidump_writer/thread_list_stream.rs b/src/linux/minidump_writer/thread_list_stream.rs new file mode 100644 index 00000000..9fa573aa --- /dev/null +++ b/src/linux/minidump_writer/thread_list_stream.rs @@ -0,0 +1,249 @@ +use {super::*, crate::minidump_cpu::RawContextCPU, std::cmp::min}; + +// The following kLimit* constants are for when minidump_size_limit_ is set +// and the minidump size might exceed it. +// +// Estimate for how big each thread's stack will be (in bytes). +const LIMIT_AVERAGE_THREAD_STACK_LENGTH: usize = 8 * 1024; +// Number of threads whose stack size we don't want to limit. These base +// threads will simply be the first N threads returned by the dumper (although +// the crashing thread will never be limited). Threads beyond this count are +// the extra threads. +const LIMIT_BASE_THREAD_COUNT: usize = 20; +// Maximum stack size to dump for any extra thread (in bytes). +const LIMIT_MAX_EXTRA_THREAD_STACK_LEN: usize = 2 * 1024; +// Make sure this number of additional bytes can fit in the minidump +// (exclude the stack data). +const LIMIT_MINIDUMP_FUDGE_FACTOR: u64 = 64 * 1024; + +#[derive(Debug, Clone, Copy)] +enum MaxStackLen { + None, + Len(usize), +} + +#[derive(Debug, Error, serde::Serialize)] +pub enum SectionThreadListError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed integer conversion")] + TryFromIntError( + #[from] + #[serde(skip)] + std::num::TryFromIntError, + ), + #[error("Failed to copy memory from process")] + CopyFromProcessError(#[from] CopyFromProcessError), + #[error("Failed to get thread info")] + ThreadInfoError(#[from] ThreadInfoError), + #[error("Failed to write to memory buffer")] + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), + #[error("Failed to sanitize stack copy")] + SanitizeStackCopyFailed(#[source] Box), +} + +impl MinidumpWriter { + pub fn write_thread_list_stream( + &mut self, + buffer: &mut DumpBuf, + ) -> Result { + let num_threads = self.threads.len(); + // Memory looks like this: + // ... + + let list_header = MemoryWriter::::alloc_with_val(buffer, num_threads as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ThreadListStream as u32, + location: list_header.location(), + }; + + let mut thread_list = MemoryArrayWriter::::alloc_array(buffer, num_threads)?; + dirent.location.data_size += thread_list.location().data_size; + // If there's a minidump size limit, check if it might be exceeded. Since + // most of the space is filled with stack data, just check against that. + // If this expects to exceed the limit, set extra_thread_stack_len such + // that any thread beyond the first kLimitBaseThreadCount threads will + // have only kLimitMaxExtraThreadStackLen bytes dumped. + let mut extra_thread_stack_len = MaxStackLen::None; // default to no maximum + if let Some(minidump_size_limit) = self.minidump_size_limit { + let estimated_total_stack_size = + (num_threads * LIMIT_AVERAGE_THREAD_STACK_LENGTH) as u64; + let curr_pos = buffer.position(); + let estimated_minidump_size = + curr_pos + estimated_total_stack_size + LIMIT_MINIDUMP_FUDGE_FACTOR; + if estimated_minidump_size > minidump_size_limit { + extra_thread_stack_len = MaxStackLen::Len(LIMIT_MAX_EXTRA_THREAD_STACK_LEN); + } + } + + for (idx, item) in self.threads.clone().iter().enumerate() { + let mut thread = MDRawThread { + thread_id: item.tid.try_into()?, + suspend_count: 0, + priority_class: 0, + priority: 0, + teb: 0, + stack: MDMemoryDescriptor::default(), + thread_context: MDLocationDescriptor::default(), + }; + + // We have a different source of information for the crashing thread. If + // 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(); + let instruction_ptr = crash_context.get_instruction_pointer(); + let stack_pointer = crash_context.get_stack_pointer(); + self.fill_thread_stack( + buffer, + &mut thread, + instruction_ptr, + stack_pointer, + MaxStackLen::None, + )?; + // Copy 256 bytes around crashing instruction pointer to minidump. + let ip_memory_size: usize = 256; + // Bound it to the upper and lower bounds of the memory map + // it's contained within. If it's not in mapped memory, + // don't bother trying to write it. + for mapping in &self.mappings { + if instruction_ptr < mapping.start_address + || instruction_ptr >= mapping.start_address + mapping.size + { + continue; + } + // Try to get 128 bytes before and after the IP, but + // settle for whatever's available. + let mut ip_memory_d = MDMemoryDescriptor { + start_of_memory_range: std::cmp::max( + mapping.start_address, + instruction_ptr - ip_memory_size / 2, + ) as u64, + ..Default::default() + }; + + let end_of_range = std::cmp::min( + mapping.start_address + mapping.size, + instruction_ptr + ip_memory_size / 2, + ) as u64; + ip_memory_d.memory.data_size = + (end_of_range - ip_memory_d.start_of_memory_range) as u32; + + let memory_copy = MinidumpWriter::copy_from_process( + thread.thread_id as i32, + ip_memory_d.start_of_memory_range as _, + ip_memory_d.memory.data_size as usize, + )?; + + let mem_section = MemoryArrayWriter::alloc_from_array(buffer, &memory_copy)?; + ip_memory_d.memory = mem_section.location(); + self.memory_blocks.push(ip_memory_d); + break; + } + // let cpu = MemoryWriter::alloc(buffer, &memory_copy)?; + let mut cpu: RawContextCPU = Default::default(); + let crash_context = self.crash_context.as_ref().unwrap(); + crash_context.fill_cpu_context(&mut cpu); + let cpu_section = MemoryWriter::alloc_with_val(buffer, cpu)?; + thread.thread_context = cpu_section.location(); + + self.crashing_thread_context = + CrashingThreadContext::CrashContext(cpu_section.location()); + } else { + let info = self.get_thread_info_by_index(idx)?; + let max_stack_len = + if self.minidump_size_limit.is_some() && idx >= LIMIT_BASE_THREAD_COUNT { + extra_thread_stack_len + } else { + MaxStackLen::None // default to no maximum for this thread + }; + let instruction_ptr = info.get_instruction_pointer(); + self.fill_thread_stack( + buffer, + &mut thread, + instruction_ptr, + info.stack_pointer, + max_stack_len, + )?; + + let mut cpu = RawContextCPU::default(); + info.fill_cpu_context(&mut cpu); + let cpu_section = MemoryWriter::::alloc_with_val(buffer, cpu)?; + thread.thread_context = cpu_section.location(); + if item.tid == self.blamed_thread { + // This is the crashing thread of a live process, but + // no context was provided, so set the crash address + // while the instruction pointer is already here. + self.crashing_thread_context = CrashingThreadContext::CrashContextPlusAddress( + (cpu_section.location(), instruction_ptr), + ); + } + } + thread_list.set_value_at(buffer, thread, idx)?; + } + Ok(dirent) + } + + fn fill_thread_stack( + &mut self, + buffer: &mut DumpBuf, + thread: &mut MDRawThread, + instruction_ptr: usize, + stack_ptr: usize, + max_stack_len: MaxStackLen, + ) -> Result<(), SectionThreadListError> { + thread.stack.start_of_memory_range = stack_ptr.try_into()?; + thread.stack.memory.data_size = 0; + thread.stack.memory.rva = buffer.position() as u32; + + if let Ok((valid_stack_ptr, stack_len)) = self.get_stack_info(stack_ptr) { + let stack_len = if let MaxStackLen::Len(max_stack_len) = max_stack_len { + min(stack_len, max_stack_len) + } else { + stack_len + }; + + let mut stack_bytes = MinidumpWriter::copy_from_process( + thread.thread_id.try_into()?, + valid_stack_ptr, + stack_len, + )?; + let stack_pointer_offset = stack_ptr.saturating_sub(valid_stack_ptr); + if self.skip_stacks_if_mapping_unreferenced { + if let Some(principal_mapping) = &self.principal_mapping { + let low_addr = principal_mapping.system_mapping_info.start_address; + let high_addr = principal_mapping.system_mapping_info.end_address; + if (instruction_ptr < low_addr || instruction_ptr > high_addr) + && !principal_mapping + .stack_has_pointer_to_mapping(&stack_bytes, stack_pointer_offset) + { + return Ok(()); + } + } else { + return Ok(()); + } + } + + if self.sanitize_stack { + self.sanitize_stack_copy(&mut stack_bytes, stack_ptr, stack_pointer_offset) + .map_err(|e| SectionThreadListError::SanitizeStackCopyFailed(Box::new(e)))?; + } + + let stack_location = MDLocationDescriptor { + data_size: stack_bytes.len() as u32, + rva: buffer.position() as u32, + }; + buffer.write_all(&stack_bytes); + thread.stack.start_of_memory_range = valid_stack_ptr as u64; + thread.stack.memory = stack_location; + self.memory_blocks.push(thread.stack); + } + Ok(()) + } +} diff --git a/src/linux/minidump_writer/thread_names_stream.rs b/src/linux/minidump_writer/thread_names_stream.rs new file mode 100644 index 00000000..2d5bf453 --- /dev/null +++ b/src/linux/minidump_writer/thread_names_stream.rs @@ -0,0 +1,54 @@ +use super::*; + +#[derive(Debug, Error, serde::Serialize)] +pub enum SectionThreadNamesError { + #[error("Failed integer conversion")] + 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] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), +} + +impl MinidumpWriter { + pub fn write_thread_names_stream( + &self, + buffer: &mut DumpBuf, + ) -> Result { + // Only count threads that have a name + let num_threads = self.threads.iter().filter(|t| t.name.is_some()).count(); + // Memory looks like this: + // ... + + let list_header = MemoryWriter::::alloc_with_val(buffer, num_threads as u32)?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::ThreadNamesStream as u32, + location: list_header.location(), + }; + + let mut thread_list = + MemoryArrayWriter::::alloc_array(buffer, num_threads)?; + dirent.location.data_size += thread_list.location().data_size; + + for (idx, item) in self.threads.iter().enumerate() { + if let Some(name) = &item.name { + let pos = write_string_to_location(buffer, name)?; + let thread = MDRawThreadName { + thread_id: item.tid.try_into()?, + thread_name_rva: pos.rva.into(), + }; + thread_list.set_value_at(buffer, thread, idx)?; + } + } + Ok(dirent) + } +} diff --git a/src/linux.rs b/src/linux/mod.rs similarity index 87% rename from src/linux.rs rename to src/linux/mod.rs index dc605084..9e7b46bd 100644 --- a/src/linux.rs +++ b/src/linux/mod.rs @@ -8,13 +8,10 @@ pub(crate) mod auxv; pub mod crash_context; mod dso_debug; mod dumper_cpu_info; -pub mod errors; pub mod maps_reader; pub mod mem_reader; pub mod minidump_writer; pub mod module_reader; -pub mod ptrace_dumper; -pub(crate) mod sections; mod serializers; pub mod thread_info; diff --git a/src/linux/module_reader.rs b/src/linux/module_reader.rs index ff4c6aec..048d0233 100644 --- a/src/linux/module_reader.rs +++ b/src/linux/module_reader.rs @@ -1,13 +1,15 @@ -use crate::errors::ModuleReaderError as Error; -use crate::mem_reader::MemReader; -use crate::minidump_format::GUID; -use goblin::{ - container::{Container, Ctx, Endian}, - elf, +use { + super::{mem_reader::MemReader, serializers::*}, + crate::{minidump_format::GUID, serializers::*}, + goblin::{ + container::{Container, Ctx, Endian}, + elf, + }, + std::{borrow::Cow, ffi::CStr}, }; -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"; @@ -16,6 +18,72 @@ pub struct ProcessReader { start_address: u64, } +#[derive(Debug, thiserror::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())] + ReadModuleMemory { + offset: u64, + length: u64, + start_address: Option, + #[source] + #[serde(serialize_with = "serialize_nix_error")] + error: nix::Error, + }, + #[error("failed to parse ELF memory: {0}")] + 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")] + NoStrTab, + #[error("no build id note sections")] + NoSectionNote, + #[error("the ELF data contains no program headers")] + NoProgramHeaders, + #[error("the ELF data contains no sections")] + NoSections, + #[error("the ELF data does not have a .text section from which to generate a build id")] + NoTextSection, + #[error( + "failed to calculate build id\n\ + ... from program headers: {program_headers}\n\ + ... from sections: {section}\n\ + ... from the text section: {section}" + )] + NoBuildId { + program_headers: Box, + section: Box, + generated: Box, + }, + #[error("no dynamic string table section")] + NoDynStrSection, + #[error("a string in the strtab did not have a terminating nul byte")] + StrTabNoNulByte, + #[error("no SONAME found in dynamic linking information")] + NoSoNameEntry, + #[error("no dynamic linking information section")] + NoDynamicSection, + #[error( + "failed to retrieve soname\n\ + ... from program headers: {program_headers}\n\ + ... from sections: {section}" + )] + NoSoName { + program_headers: Box, + section: Box, + }, +} + impl ProcessReader { pub fn new(pid: i32, start_address: usize) -> Self { Self { @@ -134,7 +202,7 @@ fn section_header_with_name<'sc>( for header in section_headers { let sh_name = header.sh_name as u64; if sh_name >= strtab_section_header.sh_size { - log::warn!("invalid sh_name offset for {:?}", name); + log::warn!("invalid sh_name offset for {name:?}"); continue; } if sh_name + name.len() as u64 >= strtab_section_header.sh_size { diff --git a/src/linux/sections.rs b/src/linux/sections.rs deleted file mode 100644 index 88d19f51..00000000 --- a/src/linux/sections.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod app_memory; -pub mod exception_stream; -pub mod handle_data_stream; -pub mod mappings; -pub mod memory_info_list_stream; -pub mod memory_list_stream; -pub mod systeminfo_stream; -pub mod thread_list_stream; -pub mod thread_names_stream; - -use crate::{ - dir_section::DumpBuf, - errors::{self}, - linux::{ - minidump_writer::{self, MinidumpWriter}, - ptrace_dumper::PtraceDumper, - }, - mem_writer::*, - minidump_format::*, -}; diff --git a/src/linux/sections/app_memory.rs b/src/linux/sections/app_memory.rs deleted file mode 100644 index 9cf16e80..00000000 --- a/src/linux/sections/app_memory.rs +++ /dev/null @@ -1,23 +0,0 @@ -use super::*; - -/// Write application-provided memory regions. -pub fn write( - config: &mut MinidumpWriter, - buffer: &mut DumpBuf, -) -> Result<(), errors::SectionAppMemoryError> { - for app_memory in &config.app_memory { - let data_copy = PtraceDumper::copy_from_process( - config.blamed_thread, - app_memory.ptr, - app_memory.length, - )?; - - let section = MemoryArrayWriter::write_bytes(buffer, &data_copy); - let desc = MDMemoryDescriptor { - start_of_memory_range: app_memory.ptr as u64, - memory: section.location(), - }; - config.memory_blocks.push(desc); - } - Ok(()) -} diff --git a/src/linux/sections/exception_stream.rs b/src/linux/sections/exception_stream.rs deleted file mode 100644 index f7edda8d..00000000 --- a/src/linux/sections/exception_stream.rs +++ /dev/null @@ -1,50 +0,0 @@ -use super::minidump_writer::CrashingThreadContext; -use super::*; -use minidump_common::errors::ExceptionCodeLinux; - -pub fn write( - config: &mut MinidumpWriter, - buffer: &mut DumpBuf, -) -> Result { - let exception = if let Some(context) = &config.crash_context { - MDException { - exception_code: context.inner.siginfo.ssi_signo, - exception_flags: context.inner.siginfo.ssi_code as u32, - exception_address: context.inner.siginfo.ssi_addr, - ..Default::default() - } - } else { - let addr = match &config.crashing_thread_context { - CrashingThreadContext::CrashContextPlusAddress((_, addr)) => *addr, - _ => 0, - }; - MDException { - exception_code: ExceptionCodeLinux::DUMP_REQUESTED as u32, - exception_address: addr as u64, - ..Default::default() - } - }; - - let thread_context = match config.crashing_thread_context { - CrashingThreadContext::CrashContextPlusAddress((ctx, _)) - | CrashingThreadContext::CrashContext(ctx) => ctx, - CrashingThreadContext::None => MDLocationDescriptor { - data_size: 0, - rva: 0, - }, - }; - - let stream = MDRawExceptionStream { - thread_id: config.blamed_thread as u32, - exception_record: exception, - __align: 0, - thread_context, - }; - let exc = MemoryWriter::alloc_with_val(buffer, stream)?; - let dirent = MDRawDirectory { - stream_type: MDStreamType::ExceptionStream as u32, - location: exc.location(), - }; - - Ok(dirent) -} diff --git a/src/linux/sections/handle_data_stream.rs b/src/linux/sections/handle_data_stream.rs deleted file mode 100644 index b41c542d..00000000 --- a/src/linux/sections/handle_data_stream.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::{ - ffi::{CString, OsString}, - fs::{self, DirEntry}, - mem::{self}, - os::unix::prelude::OsStrExt, - path::{Path, PathBuf}, -}; - -use crate::mem_writer::MemoryWriter; - -use super::*; - -fn file_stat(path: &Path) -> Option { - let c_path = CString::new(path.as_os_str().as_bytes()).ok()?; - let mut stat = unsafe { std::mem::zeroed::() }; - let result = unsafe { libc::stat(c_path.as_ptr(), &mut stat) }; - - if result == 0 { - Some(stat) - } else { - None - } -} - -fn direntry_to_descriptor(buffer: &mut DumpBuf, entry: &DirEntry) -> Option { - let handle = filename_to_fd(&entry.file_name())?; - let realpath = fs::read_link(entry.path()).ok()?; - let path_rva = write_string_to_location(buffer, realpath.to_string_lossy().as_ref()).ok()?; - let stat = file_stat(&entry.path())?; - - // TODO: We store the contents of `st_mode` into the `attributes` field, but - // we could also store a human-readable string of the file type inside - // `type_name_rva`. We might move this missing information (and - // more) inside a custom `MINIDUMP_HANDLE_OBJECT_INFORMATION_TYPE` blob. - // That would make this conversion loss-less. - Some(MDRawHandleDescriptor { - handle, - type_name_rva: 0, - object_name_rva: path_rva.rva, - attributes: stat.st_mode, - granted_access: 0, - handle_count: 0, - pointer_count: 0, - }) -} - -fn filename_to_fd(filename: &OsString) -> Option { - let filename = filename.to_string_lossy(); - filename.parse::().ok() -} - -pub fn write( - config: &mut MinidumpWriter, - buffer: &mut DumpBuf, -) -> Result { - let proc_fd_path = PathBuf::from(format!("/proc/{}/fd", config.process_id)); - let proc_fd_iter = fs::read_dir(proc_fd_path)?; - let descriptors: Vec<_> = proc_fd_iter - .filter_map(|entry| entry.ok()) - .filter_map(|entry| direntry_to_descriptor(buffer, &entry)) - .collect(); - let number_of_descriptors = descriptors.len() as u32; - - let stream_header = MemoryWriter::::alloc_with_val( - buffer, - MDRawHandleDataStream { - size_of_header: mem::size_of::() as u32, - size_of_descriptor: mem::size_of::() as u32, - number_of_descriptors, - reserved: 0, - }, - )?; - - let mut dirent = MDRawDirectory { - stream_type: MDStreamType::HandleDataStream as u32, - location: stream_header.location(), - }; - - let descriptor_list = - MemoryArrayWriter::::alloc_from_iter(buffer, descriptors)?; - - dirent.location.data_size += descriptor_list.location().data_size; - Ok(dirent) -} diff --git a/src/linux/sections/mappings.rs b/src/linux/sections/mappings.rs deleted file mode 100644 index 6688387c..00000000 --- a/src/linux/sections/mappings.rs +++ /dev/null @@ -1,142 +0,0 @@ -use super::*; -use crate::linux::maps_reader::MappingInfo; -use crate::linux::module_reader::{BuildId, ReadFromModule, SoName}; - -/// Write information about the mappings in effect. Because we are using the -/// minidump format, the information about the mappings is pretty limited. -/// Because of this, we also include the full, unparsed, /proc/$x/maps file in -/// another stream in the file. -pub fn write( - config: &mut MinidumpWriter, - buffer: &mut DumpBuf, - dumper: &mut PtraceDumper, -) -> Result { - let mut modules = Vec::new(); - - // First write all the mappings from the dumper - for map_idx in 0..dumper.mappings.len() { - // If the mapping is uninteresting, or if - // there is caller-provided information about this mapping - // in the user_mapping_list list, skip it - - if !dumper.mappings[map_idx].is_interesting() - || dumper.mappings[map_idx].is_contained_in(&config.user_mapping_list) - { - continue; - } - log::debug!("retrieving build id for {:?}", &dumper.mappings[map_idx]); - let BuildId(identifier) = dumper - .from_process_memory_for_index(map_idx) - .or_else(|e| { - // If the mapping has an associated name that is a file, try to read the build id - // from the file. If there is no note segment with the build id in - // the program headers, we can't get to the note section if the section header - // table isn't loaded. - if let Some(path) = &dumper.mappings[map_idx].name { - let path = std::path::Path::new(&path); - if path.exists() { - log::debug!("failed to get build id from process memory ({e}), attempting to retrieve from {}", path.display()); - return BuildId::read_from_file(path) - .map_err(errors::DumperError::ModuleReaderError); - } - log::debug!( - "not attempting to get build id from {}: path does not exist", - path.display() - ); - } - Err(e) - }) - .unwrap_or_else(|e| { - log::warn!("failed to get build id for mapping: {e}"); - BuildId(Vec::new()) - }); - - // If the identifier is all 0, its an uninteresting mapping (bmc#1676109) - if identifier.is_empty() || identifier.iter().all(|&x| x == 0) { - continue; - } - - // SONAME should always be accessible through program headers alone, so we don't really - // need to fall back to trying to read from the mapping file. - let soname = dumper - .from_process_memory_for_index(map_idx) - .ok() - .map(|SoName(n)| n); - - let module = fill_raw_module(buffer, &dumper.mappings[map_idx], &identifier, soname)?; - modules.push(module); - } - - // Next write all the mappings provided by the caller - for user in &config.user_mapping_list { - // GUID was provided by caller. - let module = fill_raw_module(buffer, &user.mapping, &user.identifier, None)?; - modules.push(module); - } - - let list_header = MemoryWriter::::alloc_with_val(buffer, modules.len() as u32)?; - - let mut dirent = MDRawDirectory { - stream_type: MDStreamType::ModuleListStream as u32, - location: list_header.location(), - }; - - if !modules.is_empty() { - let mapping_list = MemoryArrayWriter::::alloc_from_iter(buffer, modules)?; - dirent.location.data_size += mapping_list.location().data_size; - } - - Ok(dirent) -} - -fn fill_raw_module( - buffer: &mut DumpBuf, - mapping: &MappingInfo, - identifier: &[u8], - soname: Option, -) -> Result { - let cv_record = if identifier.is_empty() { - // Just zeroes - Default::default() - } else { - let cv_signature = crate::minidump_format::format::CvSignature::Elf as u32; - let array_size = std::mem::size_of_val(&cv_signature) + identifier.len(); - - let mut sig_section = MemoryArrayWriter::::alloc_array(buffer, array_size)?; - for (index, val) in cv_signature - .to_ne_bytes() - .iter() - .chain(identifier.iter()) - .enumerate() - { - sig_section.set_value_at(buffer, *val, index)?; - } - sig_section.location() - }; - - let (file_path, _, so_version) = mapping - .get_mapping_effective_path_name_and_version(soname) - .map_err(|e| errors::SectionMappingsError::GetEffectivePathError(mapping.clone(), e))?; - let name_header = write_string_to_location(buffer, file_path.to_string_lossy().as_ref())?; - - let version_info = so_version.map_or(Default::default(), |sov| format::VS_FIXEDFILEINFO { - signature: format::VS_FFI_SIGNATURE, - struct_version: format::VS_FFI_STRUCVERSION, - file_version_hi: sov.major, - file_version_lo: sov.minor, - product_version_hi: sov.patch, - product_version_lo: sov.prerelease, - ..Default::default() - }); - - let raw_module = MDRawModule { - base_of_image: mapping.start_address as u64, - size_of_image: mapping.size as u32, - cv_record, - module_name_rva: name_header.rva, - version_info, - ..Default::default() - }; - - Ok(raw_module) -} diff --git a/src/linux/sections/memory_info_list_stream.rs b/src/linux/sections/memory_info_list_stream.rs deleted file mode 100644 index c3cd728c..00000000 --- a/src/linux/sections/memory_info_list_stream.rs +++ /dev/null @@ -1,68 +0,0 @@ -use super::*; -use minidump_common::format::{MemoryProtection, MemoryState, MemoryType}; -use procfs_core::{process::MMPermissions, FromRead}; - -/// Write a MemoryInfoListStream using information from procfs. -pub fn write( - config: &mut MinidumpWriter, - buffer: &mut DumpBuf, -) -> Result { - let maps = procfs_core::process::MemoryMaps::from_file(std::path::PathBuf::from(format!( - "/proc/{}/maps", - config.blamed_thread - )))?; - - let list_header = MemoryWriter::alloc_with_val( - buffer, - MDMemoryInfoList { - size_of_header: std::mem::size_of::() as u32, - size_of_entry: std::mem::size_of::() as u32, - number_of_entries: maps.len() as u64, - }, - )?; - - let mut dirent = MDRawDirectory { - stream_type: MDStreamType::MemoryInfoListStream as u32, - location: list_header.location(), - }; - - let block_list = MemoryArrayWriter::::alloc_from_iter( - buffer, - maps.iter().map(|mm| MDMemoryInfo { - base_address: mm.address.0, - allocation_base: mm.address.0, - allocation_protection: get_memory_protection(mm.perms).bits(), - __alignment1: 0, - region_size: mm.address.1 - mm.address.0, - state: MemoryState::MEM_COMMIT.bits(), - protection: get_memory_protection(mm.perms).bits(), - _type: if mm.perms.contains(MMPermissions::PRIVATE) { - MemoryType::MEM_PRIVATE - } else { - MemoryType::MEM_MAPPED - } - .bits(), - __alignment2: 0, - }), - )?; - - dirent.location.data_size += block_list.location().data_size; - - Ok(dirent) -} - -fn get_memory_protection(permissions: MMPermissions) -> MemoryProtection { - let read = permissions.contains(MMPermissions::READ); - let write = permissions.contains(MMPermissions::WRITE); - let exec = permissions.contains(MMPermissions::EXECUTE); - match (read, write, exec) { - (false, false, false) => MemoryProtection::PAGE_NOACCESS, - (false, false, true) => MemoryProtection::PAGE_EXECUTE, - (true, false, false) => MemoryProtection::PAGE_READONLY, - (true, false, true) => MemoryProtection::PAGE_EXECUTE_READ, - // No support for write-only - (true | false, true, false) => MemoryProtection::PAGE_READWRITE, - // No support for execute+write-only - (true | false, true, true) => MemoryProtection::PAGE_EXECUTE_READWRITE, - } -} diff --git a/src/linux/sections/memory_list_stream.rs b/src/linux/sections/memory_list_stream.rs deleted file mode 100644 index 7f497792..00000000 --- a/src/linux/sections/memory_list_stream.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::*; - -pub fn write( - config: &mut MinidumpWriter, - buffer: &mut DumpBuf, -) -> Result { - let list_header = - MemoryWriter::::alloc_with_val(buffer, config.memory_blocks.len() as u32)?; - - let mut dirent = MDRawDirectory { - stream_type: MDStreamType::MemoryListStream as u32, - location: list_header.location(), - }; - - let block_list = - MemoryArrayWriter::::alloc_from_array(buffer, &config.memory_blocks)?; - - dirent.location.data_size += block_list.location().data_size; - - Ok(dirent) -} diff --git a/src/linux/sections/thread_list_stream.rs b/src/linux/sections/thread_list_stream.rs deleted file mode 100644 index a6dc059e..00000000 --- a/src/linux/sections/thread_list_stream.rs +++ /dev/null @@ -1,232 +0,0 @@ -use std::cmp::min; - -use super::*; -use crate::{minidump_cpu::RawContextCPU, minidump_writer::CrashingThreadContext}; - -// The following kLimit* constants are for when minidump_size_limit_ is set -// and the minidump size might exceed it. -// -// Estimate for how big each thread's stack will be (in bytes). -const LIMIT_AVERAGE_THREAD_STACK_LENGTH: usize = 8 * 1024; -// Number of threads whose stack size we don't want to limit. These base -// threads will simply be the first N threads returned by the dumper (although -// the crashing thread will never be limited). Threads beyond this count are -// the extra threads. -const LIMIT_BASE_THREAD_COUNT: usize = 20; -// Maximum stack size to dump for any extra thread (in bytes). -const LIMIT_MAX_EXTRA_THREAD_STACK_LEN: usize = 2 * 1024; -// Make sure this number of additional bytes can fit in the minidump -// (exclude the stack data). -const LIMIT_MINIDUMP_FUDGE_FACTOR: u64 = 64 * 1024; - -#[derive(Debug, Clone, Copy)] -enum MaxStackLen { - None, - Len(usize), -} - -pub fn write( - config: &mut MinidumpWriter, - buffer: &mut DumpBuf, - dumper: &PtraceDumper, -) -> Result { - let num_threads = dumper.threads.len(); - // Memory looks like this: - // ... - - let list_header = MemoryWriter::::alloc_with_val(buffer, num_threads as u32)?; - - let mut dirent = MDRawDirectory { - stream_type: MDStreamType::ThreadListStream as u32, - location: list_header.location(), - }; - - let mut thread_list = MemoryArrayWriter::::alloc_array(buffer, num_threads)?; - dirent.location.data_size += thread_list.location().data_size; - // If there's a minidump size limit, check if it might be exceeded. Since - // most of the space is filled with stack data, just check against that. - // If this expects to exceed the limit, set extra_thread_stack_len such - // that any thread beyond the first kLimitBaseThreadCount threads will - // have only kLimitMaxExtraThreadStackLen bytes dumped. - let mut extra_thread_stack_len = MaxStackLen::None; // default to no maximum - if let Some(minidump_size_limit) = config.minidump_size_limit { - let estimated_total_stack_size = (num_threads * LIMIT_AVERAGE_THREAD_STACK_LENGTH) as u64; - let curr_pos = buffer.position(); - let estimated_minidump_size = - curr_pos + estimated_total_stack_size + LIMIT_MINIDUMP_FUDGE_FACTOR; - if estimated_minidump_size > minidump_size_limit { - extra_thread_stack_len = MaxStackLen::Len(LIMIT_MAX_EXTRA_THREAD_STACK_LEN); - } - } - - for (idx, item) in dumper.threads.clone().iter().enumerate() { - let mut thread = MDRawThread { - thread_id: item.tid.try_into()?, - suspend_count: 0, - priority_class: 0, - priority: 0, - teb: 0, - stack: MDMemoryDescriptor::default(), - thread_context: MDLocationDescriptor::default(), - }; - - // We have a different source of information for the crashing thread. If - // 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 config.crash_context.is_some() && thread.thread_id == config.blamed_thread as u32 { - let crash_context = config.crash_context.as_ref().unwrap(); - let instruction_ptr = crash_context.get_instruction_pointer(); - let stack_pointer = crash_context.get_stack_pointer(); - fill_thread_stack( - config, - buffer, - dumper, - &mut thread, - instruction_ptr, - stack_pointer, - MaxStackLen::None, - )?; - // Copy 256 bytes around crashing instruction pointer to minidump. - let ip_memory_size: usize = 256; - // Bound it to the upper and lower bounds of the memory map - // it's contained within. If it's not in mapped memory, - // don't bother trying to write it. - for mapping in &dumper.mappings { - if instruction_ptr < mapping.start_address - || instruction_ptr >= mapping.start_address + mapping.size - { - continue; - } - // Try to get 128 bytes before and after the IP, but - // settle for whatever's available. - let mut ip_memory_d = MDMemoryDescriptor { - start_of_memory_range: std::cmp::max( - mapping.start_address, - instruction_ptr - ip_memory_size / 2, - ) as u64, - ..Default::default() - }; - - let end_of_range = std::cmp::min( - mapping.start_address + mapping.size, - instruction_ptr + ip_memory_size / 2, - ) as u64; - ip_memory_d.memory.data_size = - (end_of_range - ip_memory_d.start_of_memory_range) as u32; - - let memory_copy = PtraceDumper::copy_from_process( - thread.thread_id as i32, - ip_memory_d.start_of_memory_range as _, - ip_memory_d.memory.data_size as usize, - )?; - - let mem_section = MemoryArrayWriter::alloc_from_array(buffer, &memory_copy)?; - ip_memory_d.memory = mem_section.location(); - config.memory_blocks.push(ip_memory_d); - - break; - } - // let cpu = MemoryWriter::alloc(buffer, &memory_copy)?; - let mut cpu: RawContextCPU = Default::default(); - let crash_context = config.crash_context.as_ref().unwrap(); - crash_context.fill_cpu_context(&mut cpu); - let cpu_section = MemoryWriter::alloc_with_val(buffer, cpu)?; - thread.thread_context = cpu_section.location(); - - config.crashing_thread_context = - CrashingThreadContext::CrashContext(cpu_section.location()); - } else { - let info = dumper.get_thread_info_by_index(idx)?; - let max_stack_len = - if config.minidump_size_limit.is_some() && idx >= LIMIT_BASE_THREAD_COUNT { - extra_thread_stack_len - } else { - MaxStackLen::None // default to no maximum for this thread - }; - let instruction_ptr = info.get_instruction_pointer(); - fill_thread_stack( - config, - buffer, - dumper, - &mut thread, - instruction_ptr, - info.stack_pointer, - max_stack_len, - )?; - - let mut cpu = RawContextCPU::default(); - info.fill_cpu_context(&mut cpu); - let cpu_section = MemoryWriter::::alloc_with_val(buffer, cpu)?; - thread.thread_context = cpu_section.location(); - if item.tid == config.blamed_thread { - // This is the crashing thread of a live process, but - // no context was provided, so set the crash address - // while the instruction pointer is already here. - config.crashing_thread_context = CrashingThreadContext::CrashContextPlusAddress(( - cpu_section.location(), - instruction_ptr, - )); - } - } - thread_list.set_value_at(buffer, thread, idx)?; - } - Ok(dirent) -} - -fn fill_thread_stack( - config: &mut MinidumpWriter, - buffer: &mut DumpBuf, - dumper: &PtraceDumper, - thread: &mut MDRawThread, - instruction_ptr: usize, - stack_ptr: usize, - max_stack_len: MaxStackLen, -) -> Result<(), errors::SectionThreadListError> { - thread.stack.start_of_memory_range = stack_ptr.try_into()?; - thread.stack.memory.data_size = 0; - thread.stack.memory.rva = buffer.position() as u32; - - if let Ok((valid_stack_ptr, stack_len)) = dumper.get_stack_info(stack_ptr) { - let stack_len = if let MaxStackLen::Len(max_stack_len) = max_stack_len { - min(stack_len, max_stack_len) - } else { - stack_len - }; - - let mut stack_bytes = PtraceDumper::copy_from_process( - thread.thread_id.try_into()?, - valid_stack_ptr, - stack_len, - )?; - let stack_pointer_offset = stack_ptr.saturating_sub(valid_stack_ptr); - if config.skip_stacks_if_mapping_unreferenced { - if let Some(principal_mapping) = &config.principal_mapping { - let low_addr = principal_mapping.system_mapping_info.start_address; - let high_addr = principal_mapping.system_mapping_info.end_address; - if (instruction_ptr < low_addr || instruction_ptr > high_addr) - && !principal_mapping - .stack_has_pointer_to_mapping(&stack_bytes, stack_pointer_offset) - { - return Ok(()); - } - } else { - return Ok(()); - } - } - - if config.sanitize_stack { - dumper.sanitize_stack_copy(&mut stack_bytes, stack_ptr, stack_pointer_offset)?; - } - - let stack_location = MDLocationDescriptor { - data_size: stack_bytes.len() as u32, - rva: buffer.position() as u32, - }; - buffer.write_all(&stack_bytes); - thread.stack.start_of_memory_range = valid_stack_ptr as u64; - thread.stack.memory = stack_location; - config.memory_blocks.push(thread.stack); - } - Ok(()) -} diff --git a/src/linux/sections/thread_names_stream.rs b/src/linux/sections/thread_names_stream.rs deleted file mode 100644 index bd8682b2..00000000 --- a/src/linux/sections/thread_names_stream.rs +++ /dev/null @@ -1,33 +0,0 @@ -use super::*; - -pub fn write( - buffer: &mut DumpBuf, - dumper: &PtraceDumper, -) -> Result { - // Only count threads that have a name - let num_threads = dumper.threads.iter().filter(|t| t.name.is_some()).count(); - // Memory looks like this: - // ... - - let list_header = MemoryWriter::::alloc_with_val(buffer, num_threads as u32)?; - - let mut dirent = MDRawDirectory { - stream_type: MDStreamType::ThreadNamesStream as u32, - location: list_header.location(), - }; - - let mut thread_list = MemoryArrayWriter::::alloc_array(buffer, num_threads)?; - dirent.location.data_size += thread_list.location().data_size; - - for (idx, item) in dumper.threads.iter().enumerate() { - if let Some(name) = &item.name { - let pos = write_string_to_location(buffer, name)?; - let thread = MDRawThreadName { - thread_id: item.tid.try_into()?, - thread_name_rva: pos.rva.into(), - }; - thread_list.set_value_at(buffer, thread, idx)?; - } - } - Ok(dirent) -} diff --git a/src/linux/thread_info/aarch64.rs b/src/linux/thread_info/aarch64.rs index d6d39092..4bf4c6f4 100644 --- a/src/linux/thread_info/aarch64.rs +++ b/src/linux/thread_info/aarch64.rs @@ -1,10 +1,8 @@ -use super::{CommonThreadInfo, NT_Elf}; -use crate::{ - errors::ThreadInfoError, - minidump_cpu::{RawContextCPU, FP_REG_COUNT, GP_REG_COUNT}, - Pid, +use { + super::{CommonThreadInfo, NT_Elf, Pid, ThreadInfoError}, + crate::minidump_cpu::{RawContextCPU, FP_REG_COUNT, GP_REG_COUNT}, + nix::sys::ptrace, }; -use nix::sys::ptrace; /// https://github.com/rust-lang/libc/pull/2719 #[derive(Debug)] diff --git a/src/linux/thread_info/arm.rs b/src/linux/thread_info/arm.rs index 4346c2e5..b5f12636 100644 --- a/src/linux/thread_info/arm.rs +++ b/src/linux/thread_info/arm.rs @@ -1,6 +1,8 @@ -use super::{CommonThreadInfo, NT_Elf}; -use crate::{errors::ThreadInfoError, minidump_cpu::RawContextCPU, Pid}; -use nix::sys::ptrace; +use { + super::{CommonThreadInfo, NT_Elf, Pid, ThreadInfoError}, + crate::minidump_cpu::RawContextCPU, + nix::sys::ptrace, +}; type Result = std::result::Result; diff --git a/src/linux/thread_info/mips.rs b/src/linux/thread_info/mips.rs index 7965ec13..2a99f077 100644 --- a/src/linux/thread_info/mips.rs +++ b/src/linux/thread_info/mips.rs @@ -1,5 +1,4 @@ -use crate::{errors::ThreadInfoError, Pid}; -use libc; +use super::{errors::ThreadInfoError, Pid}; type Result = std::result::Result; diff --git a/src/linux/thread_info.rs b/src/linux/thread_info/mod.rs similarity index 80% rename from src/linux/thread_info.rs rename to src/linux/thread_info/mod.rs index adce589b..eed23426 100644 --- a/src/linux/thread_info.rs +++ b/src/linux/thread_info/mod.rs @@ -1,12 +1,43 @@ -use crate::{errors::ThreadInfoError, Pid}; -use nix::{errno::Errno, sys::ptrace, unistd}; -use std::{ - io::{self, BufRead}, - path, +use { + super::{serializers::*, Pid}, + crate::serializers::*, + nix::{errno::Errno, sys::ptrace}, + std::{ + io::{self, BufRead}, + path, + }, }; type Result = std::result::Result; +#[derive(thiserror::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] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), + #[error("Couldn't parse address")] + UnparsableInteger( + #[from] + #[serde(skip)] + std::num::ParseIntError, + ), + #[error("nix::ptrace() error")] + PtraceError( + #[from] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), + #[error("Invalid line in /proc/{0}/status: {1}")] + InvalidProcStatusFile(Pid, String), +} + cfg_if::cfg_if! { if #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { mod x86; @@ -35,6 +66,7 @@ enum NT_Elf { NT_ARM_VFP = 0x400, // ARM VFP/NEON registers } +#[cfg(target_arch = "x86_64")] #[inline] pub fn copy_u32_registers(dst: &mut [u128], src: &[u32]) { // SAFETY: We are copying a block of memory from ptrace as u32s to the u128 @@ -54,7 +86,7 @@ trait CommonThreadInfo { let mut ppid = -1; let mut tgid = -1; - let status_path = path::PathBuf::from(format!("/proc/{}/status", tid)); + let status_path = path::PathBuf::from(format!("/proc/{tid}/status")); let status_file = std::fs::File::open(status_path)?; for line in io::BufReader::new(status_file).lines() { let l = line?; @@ -79,7 +111,7 @@ trait CommonThreadInfo { } if ppid == -1 || tgid == -1 { return Err(ThreadInfoError::InvalidPid( - format!("/proc/{}/status", tid), + format!("/proc/{tid}/status"), ppid, tgid, )); @@ -138,9 +170,10 @@ trait CommonThreadInfo { } /// COPY FROM CRATE nix BECAUSE ITS NOT PUBLIC + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn ptrace_peek( request: ptrace::RequestType, - pid: unistd::Pid, + pid: nix::unistd::Pid, addr: ptrace::AddressType, data: *mut libc::c_void, ) -> nix::Result { diff --git a/src/linux/thread_info/x86.rs b/src/linux/thread_info/x86.rs index 827cbe78..55da247c 100644 --- a/src/linux/thread_info/x86.rs +++ b/src/linux/thread_info/x86.rs @@ -1,12 +1,15 @@ -use super::{CommonThreadInfo, NT_Elf}; -use crate::{errors::ThreadInfoError, minidump_cpu::RawContextCPU, minidump_format::format, Pid}; -use core::mem::size_of_val; +use { + super::{CommonThreadInfo, NT_Elf, Pid, ThreadInfoError}, + crate::{minidump_cpu::RawContextCPU, minidump_format::format}, + core::mem::size_of_val, + nix::sys::ptrace, + scroll::Pwrite, +}; + #[cfg(all(not(target_os = "android"), target_arch = "x86"))] use libc::user_fpxregs_struct; #[cfg(not(all(target_os = "android", target_arch = "x86")))] use libc::{user, user_fpregs_struct, user_regs_struct}; -use nix::sys::ptrace; -use scroll::Pwrite; type Result = std::result::Result; diff --git a/src/mac/errors.rs b/src/mac/errors.rs index 96ddb88c..44d2f9d7 100644 --- a/src/mac/errors.rs +++ b/src/mac/errors.rs @@ -1,6 +1,4 @@ -use thiserror::Error; - -#[derive(Debug, Error)] +#[derive(Debug, thiserror::Error)] pub enum WriterError { #[error(transparent)] TaskDumpError(#[from] crate::mac::task_dumper::TaskDumpError), diff --git a/src/mac/minidump_writer.rs b/src/mac/minidump_writer.rs index b05662fd..b1061a81 100644 --- a/src/mac/minidump_writer.rs +++ b/src/mac/minidump_writer.rs @@ -1,10 +1,12 @@ -use crate::{ - dir_section::{DirSection, DumpBuf}, - mac::{errors::WriterError, task_dumper::TaskDumper}, - mem_writer::*, - minidump_format::{self, MDMemoryDescriptor, MDRawDirectory, MDRawHeader}, +use { + super::{errors::WriterError, task_dumper::TaskDumper}, + crate::{ + dir_section::{DirSection, DumpBuf}, + mem_writer::*, + minidump_format::{self, MDMemoryDescriptor, MDRawDirectory, MDRawHeader}, + }, + std::io::{Seek, Write}, }; -use std::io::{Seek, Write}; pub use mach2::mach_types::{task_t, thread_t}; diff --git a/src/mac.rs b/src/mac/mod.rs similarity index 100% rename from src/mac.rs rename to src/mac/mod.rs diff --git a/src/mac/streams.rs b/src/mac/streams.rs deleted file mode 100644 index bec3b225..00000000 --- a/src/mac/streams.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod breakpad_info; -mod exception; -mod memory_list; -mod misc_info; -mod module_list; -mod system_info; -mod thread_list; -mod thread_names; - -use super::{ - errors::WriterError, - mach, - minidump_writer::MinidumpWriter, - task_dumper::{self, ImageInfo, TaskDumpError, TaskDumper}, -}; -use crate::{dir_section::DumpBuf, mem_writer::*, minidump_format::*}; diff --git a/src/mac/streams/breakpad_info.rs b/src/mac/streams/breakpad_info.rs index 5196a95c..b7025191 100644 --- a/src/mac/streams/breakpad_info.rs +++ b/src/mac/streams/breakpad_info.rs @@ -1,5 +1,7 @@ -use super::*; -use format::{BreakpadInfoValid, MINIDUMP_BREAKPAD_INFO as BreakpadInfo}; +use { + super::*, + format::{BreakpadInfoValid, MINIDUMP_BREAKPAD_INFO as BreakpadInfo}, +}; impl MinidumpWriter { /// Writes the [`BreakpadInfo`] stream. diff --git a/src/mac/streams/exception.rs b/src/mac/streams/exception.rs index 7dd7f8fa..c0494f7f 100644 --- a/src/mac/streams/exception.rs +++ b/src/mac/streams/exception.rs @@ -1,6 +1,4 @@ -use super::*; - -use mach2::exception_types as et; +use {super::*, mach2::exception_types as et}; impl MinidumpWriter { /// Writes the [`minidump_common::format::MINIDUMP_EXCEPTION_STREAM`] stream. diff --git a/src/mac/streams/misc_info.rs b/src/mac/streams/misc_info.rs index 629b94ce..f3860041 100644 --- a/src/mac/streams/misc_info.rs +++ b/src/mac/streams/misc_info.rs @@ -1,6 +1,8 @@ -use super::*; -use format::{MiscInfoFlags, MINIDUMP_MISC_INFO_2 as MDRawMiscInfo}; -use std::time::Duration; +use { + super::*, + format::{MiscInfoFlags, MINIDUMP_MISC_INFO_2 as MDRawMiscInfo}, + std::time::Duration, +}; /// From #[repr(C)] diff --git a/src/mac/streams/mod.rs b/src/mac/streams/mod.rs new file mode 100644 index 00000000..4b1942e0 --- /dev/null +++ b/src/mac/streams/mod.rs @@ -0,0 +1,18 @@ +mod breakpad_info; +mod exception; +mod memory_list; +mod misc_info; +mod module_list; +mod system_info; +mod thread_list; +mod thread_names; + +use { + super::{ + errors::WriterError, + mach, + minidump_writer::MinidumpWriter, + task_dumper::{self, ImageInfo, TaskDumpError, TaskDumper}, + }, + crate::{dir_section::DumpBuf, mem_writer::*, minidump_format::*}, +}; diff --git a/src/mac/streams/system_info.rs b/src/mac/streams/system_info.rs index aac2de57..30f43d06 100644 --- a/src/mac/streams/system_info.rs +++ b/src/mac/streams/system_info.rs @@ -1,5 +1,4 @@ -use super::*; -use crate::minidump_format::*; +use {super::*, crate::minidump_format::*}; /// Retrieve the OS version information. /// diff --git a/src/mac/streams/thread_list.rs b/src/mac/streams/thread_list.rs index 180bb2f6..d5283de8 100644 --- a/src/mac/streams/thread_list.rs +++ b/src/mac/streams/thread_list.rs @@ -1,5 +1,4 @@ -use super::*; -use crate::minidump_cpu::RawContextCPU; +use {super::*, crate::minidump_cpu::RawContextCPU}; impl MinidumpWriter { /// Writes the [`MDStreamType::ThreadListStream`] which is an array of diff --git a/src/mac/task_dumper.rs b/src/mac/task_dumper.rs index eeda3632..e479ee45 100644 --- a/src/mac/task_dumper.rs +++ b/src/mac/task_dumper.rs @@ -1,6 +1,4 @@ -use crate::mac::mach; -use mach2::mach_types as mt; -use thiserror::Error; +use {super::mach, mach2::mach_types as mt, thiserror::Error}; #[derive(Error, Debug)] pub enum TaskDumpError { diff --git a/src/windows/minidump_writer.rs b/src/windows/minidump_writer.rs index a5b7bebd..2576b59c 100644 --- a/src/windows/minidump_writer.rs +++ b/src/windows/minidump_writer.rs @@ -1,17 +1,21 @@ #![allow(unsafe_code)] -use crate::windows::errors::Error; -use crate::windows::ffi::{ - capture_context, CloseHandle, GetCurrentProcess, GetCurrentThreadId, GetThreadContext, - MiniDumpWriteDump, MinidumpType, OpenProcess, OpenThread, ResumeThread, SuspendThread, - EXCEPTION_POINTERS, EXCEPTION_RECORD, FALSE, HANDLE, MINIDUMP_EXCEPTION_INFORMATION, - MINIDUMP_USER_STREAM, MINIDUMP_USER_STREAM_INFORMATION, PROCESS_ALL_ACCESS, - STATUS_NONCONTINUABLE_EXCEPTION, THREAD_GET_CONTEXT, THREAD_QUERY_INFORMATION, - THREAD_SUSPEND_RESUME, +use { + super::{ + errors::Error, + ffi::{ + capture_context, CloseHandle, GetCurrentProcess, GetCurrentThreadId, GetThreadContext, + MiniDumpWriteDump, MinidumpType, OpenProcess, OpenThread, ResumeThread, SuspendThread, + EXCEPTION_POINTERS, EXCEPTION_RECORD, FALSE, HANDLE, MINIDUMP_EXCEPTION_INFORMATION, + MINIDUMP_USER_STREAM, MINIDUMP_USER_STREAM_INFORMATION, PROCESS_ALL_ACCESS, + STATUS_NONCONTINUABLE_EXCEPTION, THREAD_GET_CONTEXT, THREAD_QUERY_INFORMATION, + THREAD_SUSPEND_RESUME, + }, + }, + minidump_common::format::{BreakpadInfoValid, MINIDUMP_BREAKPAD_INFO, MINIDUMP_STREAM_TYPE}, + scroll::Pwrite, + std::os::windows::io::AsRawHandle, }; -use minidump_common::format::{BreakpadInfoValid, MINIDUMP_BREAKPAD_INFO, MINIDUMP_STREAM_TYPE}; -use scroll::Pwrite; -use std::os::windows::io::AsRawHandle; pub struct MinidumpWriter { /// Optional exception information diff --git a/src/windows.rs b/src/windows/mod.rs similarity index 98% rename from src/windows.rs rename to src/windows/mod.rs index 34b72444..aebd6dee 100644 --- a/src/windows.rs +++ b/src/windows/mod.rs @@ -1,5 +1,6 @@ -pub mod errors; +pub use ffi::MinidumpType; + mod ffi; -pub mod minidump_writer; -pub use ffi::MinidumpType; +pub mod errors; +pub mod minidump_writer; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 745e3a9a..232d79b9 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,7 +1,9 @@ -use std::error; -use std::io::{BufRead, BufReader, Write}; -use std::process::{Child, Command, Stdio}; -use std::result; +use std::{ + error, + io::{BufRead, BufReader, Write}, + process::{Child, Command, Stdio}, + result, +}; #[allow(unused)] type Error = Box; @@ -135,6 +137,8 @@ pub fn assert_soft_errors_in_minidump<'a, 'b, T, I>( for expected_error in expected_errors { assert!(actual_errors .iter() - .any(|actual_error| actual_error == expected_error)); + .any(|actual_error| actual_error == expected_error), + "soft error list missing expected error `{expected_error:#?}`\nError_list: {actual_errors:#?}" + ); } } diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs index d62f56ff..0c7a1884 100644 --- a/tests/linux_minidump_writer.rs +++ b/tests/linux_minidump_writer.rs @@ -8,11 +8,9 @@ use { minidump_writer::{ app_memory::AppMemory, crash_context::CrashContext, - errors::*, maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo}, - minidump_writer::MinidumpWriter, + minidump_writer::{errors::WriterError, MinidumpWriter, MinidumpWriterConfig}, module_reader::{BuildId, ReadFromModule}, - ptrace_dumper::PtraceDumper, Pid, }, nix::{errno::Errno, sys::signal::Signal}, @@ -35,8 +33,8 @@ enum Context { } impl Context { - pub fn minidump_writer(&self, pid: Pid) -> MinidumpWriter { - let mut mw = MinidumpWriter::new(pid, pid); + pub fn minidump_writer(&self, pid: Pid) -> MinidumpWriterConfig { + let mut mw = MinidumpWriterConfig::new(pid, pid); #[cfg(not(target_arch = "mips"))] if self == &Context::With { let crash_context = get_crash_context(pid); @@ -109,8 +107,8 @@ contextual_test! { .tempfile() .unwrap(); - let mut tmp = context.minidump_writer(pid); - let in_memory_buffer = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + let tmp = context.minidump_writer(pid); + let in_memory_buffer = tmp.write(&mut tmpfile).expect("Could not write minidump"); child.kill().expect("Failed to kill process"); // Reap child @@ -179,8 +177,9 @@ contextual_test! { let mut tmp = context.minidump_writer(pid); - tmp.set_user_mapping_list(vec![entry]) - .dump(&mut tmpfile) + tmp.set_user_mapping_list(vec![entry]); + tmp + .write(&mut tmpfile) .expect("Could not write minidump"); child.kill().expect("Failed to kill process"); @@ -267,8 +266,9 @@ contextual_test! { let mut tmp = context.minidump_writer(pid); - tmp.set_app_memory(vec![app_memory]) - .dump(&mut tmpfile) + tmp.set_app_memory(vec![app_memory]); + tmp + .write(&mut tmpfile) .expect("Could not write minidump"); child.kill().expect("Failed to kill process"); @@ -302,7 +302,9 @@ contextual_test! { contextual_test! { fn skip_if_requested(context: Context) { let expected_errors = vec![ - json!("PrincipalMappingNotReferenced"), + json!({ + "InitErrors": ["PrincipalMappingNotReferenced"] + }), ]; let num_of_threads = 1; @@ -325,10 +327,11 @@ contextual_test! { { pr_mapping_addr = 0x010203040; }; - let res = tmp + tmp .skip_stacks_if_mapping_unreferenced() - .set_principal_mapping_address(pr_mapping_addr) - .dump(&mut tmpfile); + .set_principal_mapping_address(pr_mapping_addr); + let res = tmp + .write(&mut tmpfile); child.kill().expect("Failed to kill process"); // Reap child @@ -361,8 +364,9 @@ contextual_test! { .unwrap(); let mut tmp = context.minidump_writer(pid); - tmp.sanitize_stack() - .dump(&mut tmpfile) + tmp.sanitize_stack(); + tmp + .write(&mut tmpfile) .expect("Faild to dump minidump"); child.kill().expect("Failed to kill process"); @@ -431,9 +435,10 @@ contextual_test! { }; let mut tmp = context.minidump_writer(pid); + tmp.set_app_memory(vec![app_memory]); // This should fail, because during the dump an error is detected (try_from fails) - match tmp.set_app_memory(vec![app_memory]).dump(&mut tmpfile) { + match tmp.write(&mut tmpfile) { Err(WriterError::SectionAppMemoryError(_)) => (), _ => panic!("Wrong kind of error returned"), } @@ -467,8 +472,8 @@ contextual_test! { .tempfile() .unwrap(); - let mut tmp = context.minidump_writer(pid); - let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + let tmp = context.minidump_writer(pid); + let _ = tmp.write(&mut tmpfile).expect("Could not write minidump"); child.kill().expect("Failed to kill process"); // Reap child @@ -495,7 +500,7 @@ contextual_test! { let mut expected = HashSet::new(); expected.insert("test".to_string()); for id in 1..num_of_threads { - expected.insert(format!("thread_{}", id)); + expected.insert(format!("thread_{id}")); } assert_eq!(expected, names); } @@ -512,8 +517,8 @@ contextual_test! { .tempfile() .unwrap(); - let mut tmp = context.minidump_writer(pid); - let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + let tmp = context.minidump_writer(pid); + let _ = tmp.write(&mut tmpfile).expect("Could not write minidump"); child.kill().expect("Failed to kill process"); // Reap child @@ -567,8 +572,8 @@ fn minidump_size_limit() { .tempfile() .unwrap(); - MinidumpWriter::new(pid, pid) - .dump(&mut tmpfile) + MinidumpWriterConfig::new(pid, pid) + .write(&mut tmpfile) .expect("Could not write minidump"); let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); @@ -599,10 +604,9 @@ fn minidump_size_limit() { .tempfile() .unwrap(); - MinidumpWriter::new(pid, pid) - .set_minidump_size_limit(minidump_size_limit) - .dump(&mut tmpfile) - .expect("Could not write minidump"); + let mut tmp = MinidumpWriterConfig::new(pid, pid); + tmp.set_minidump_size_limit(minidump_size_limit); + tmp.write(&mut tmpfile).expect("Could not write minidump"); let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); @@ -646,10 +650,9 @@ fn minidump_size_limit() { .tempfile() .unwrap(); - MinidumpWriter::new(pid, pid) - .set_minidump_size_limit(minidump_size_limit) - .dump(&mut tmpfile) - .expect("Could not write minidump"); + let mut tmp = MinidumpWriterConfig::new(pid, pid); + tmp.set_minidump_size_limit(minidump_size_limit); + tmp.write(&mut tmpfile).expect("Could not write minidump"); let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); assert!(meta.len() > 0); @@ -733,8 +736,8 @@ fn with_deleted_binary() { .tempfile() .unwrap(); - MinidumpWriter::new(pid, pid) - .dump(&mut tmpfile) + MinidumpWriterConfig::new(pid, pid) + .write(&mut tmpfile) .expect("Could not write minidump"); child.kill().expect("Failed to kill process"); @@ -794,8 +797,8 @@ fn memory_info_list_stream() { .unwrap(); // Write a minidump - MinidumpWriter::new(pid, pid) - .dump(&mut tmpfile) + MinidumpWriterConfig::new(pid, pid) + .write(&mut tmpfile) .expect("cound not write minidump"); child.kill().expect("Failed to kill process"); child.wait().expect("Failed to wait on killed process"); diff --git a/tests/linux_minidump_writer_soft_error.rs b/tests/linux_minidump_writer_soft_error.rs index 5c5df212..736009e1 100644 --- a/tests/linux_minidump_writer_soft_error.rs +++ b/tests/linux_minidump_writer_soft_error.rs @@ -3,7 +3,7 @@ use { common::*, minidump::Minidump, - minidump_writer::{minidump_writer::MinidumpWriter, FailSpotName}, + minidump_writer::{minidump_writer::MinidumpWriterConfig, FailSpotName}, serde_json::json, }; @@ -23,8 +23,8 @@ fn soft_error_stream() { fail_client.set_enabled(FailSpotName::StopProcess, true); // Write a minidump - MinidumpWriter::new(pid, pid) - .dump(&mut tmpfile) + MinidumpWriterConfig::new(pid, pid) + .write(&mut tmpfile) .expect("cound not write minidump"); child.kill().expect("Failed to kill process"); child.wait().expect("Failed to wait on killed process"); @@ -47,9 +47,9 @@ fn soft_error_stream_content() { error: \"testing requested failure reading thread name\",\n\ }" } - ]} + ]}, + {"SuspendThreadsErrors": [{"PtraceAttachError": [1234, "EPERM"]}]} ]}), - json!({"SuspendThreadsErrors": [{"PtraceAttachError": [1234, "EPERM"]}]}), json!({"WriteSystemInfoErrors": [ {"WriteCpuInformationFailed": {"IOError": "\ Custom {\n \ @@ -80,8 +80,8 @@ fn soft_error_stream_content() { } // Write a minidump - MinidumpWriter::new(pid, pid) - .dump(&mut tmpfile) + MinidumpWriterConfig::new(pid, pid) + .write(&mut tmpfile) .expect("cound not write minidump"); child.kill().expect("Failed to kill process"); child.wait().expect("Failed to wait on killed process"); diff --git a/tests/mac_minidump_writer.rs b/tests/mac_minidump_writer.rs index dcc4aa3e..1087951a 100644 --- a/tests/mac_minidump_writer.rs +++ b/tests/mac_minidump_writer.rs @@ -1,13 +1,15 @@ #![cfg(target_os = "macos")] -mod common; -use common::start_child_and_return; - -use minidump::{ - CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpMiscInfo, - MinidumpModuleList, MinidumpSystemInfo, MinidumpThreadList, +use { + common::start_child_and_return, + minidump::{ + CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpMiscInfo, + MinidumpModuleList, MinidumpSystemInfo, MinidumpThreadList, + }, + minidump_writer::minidump_writer::MinidumpWriter, }; -use minidump_writer::minidump_writer::MinidumpWriter; + +mod common; fn get_crash_reason<'a, T: std::ops::Deref + 'a>( md: &Minidump<'a, T>, diff --git a/tests/ptrace_dumper.rs b/tests/ptrace_dumper.rs index b166c256..d0de9eeb 100644 --- a/tests/ptrace_dumper.rs +++ b/tests/ptrace_dumper.rs @@ -2,8 +2,9 @@ #![cfg(any(target_os = "linux", target_os = "android"))] use { + common::*, error_graph::ErrorList, - minidump_writer::ptrace_dumper::PtraceDumper, + minidump_writer::minidump_writer::MinidumpWriterConfig, nix::{ sys::mman::{mmap, MapFlags, ProtFlags}, sys::signal::Signal, @@ -17,7 +18,6 @@ use { }; mod common; -use common::*; /// These tests generally aren't consistent in resource-deprived environments like CI runners and /// android emulators. @@ -106,24 +106,17 @@ fn test_thread_list_from_parent() { let mut child = start_child_and_wait_for_threads(num_of_threads); let pid = child.id() as i32; - let mut dumper = assert_no_soft_errors!( + let dumper = assert_no_soft_errors!( soft_errors, - PtraceDumper::new_report_soft_errors( - pid, - minidump_writer::minidump_writer::STOP_TIMEOUT, - Default::default(), - &mut soft_errors, - ) + MinidumpWriterConfig::new(pid, pid).build_for_testing(&mut soft_errors) ) .expect("Couldn't init dumper"); assert_eq!(dumper.threads.len(), num_of_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() { - println!("curr_thread: {:?}", curr_thread); + println!("curr_thread: {curr_thread:?}"); let info = dumper .get_thread_info_by_index(idx) .expect("Could not get thread info by index"); @@ -167,7 +160,7 @@ fn test_thread_list_from_parent() { 0 }; */ } - assert_no_soft_errors!(soft_errors, dumper.resume_threads(&mut soft_errors)); + drop(dumper); child.kill().expect("Failed to kill process"); // Reap child @@ -293,20 +286,13 @@ 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 = assert_no_soft_errors!( + let dumper = assert_no_soft_errors!( soft_errors, - PtraceDumper::new_report_soft_errors( - pid, - minidump_writer::minidump_writer::STOP_TIMEOUT, - Default::default(), - &mut soft_errors, - ) + MinidumpWriterConfig::new(pid, pid).build_for_testing(&mut soft_errors) ) .expect("Couldn't init dumper"); assert_eq!(dumper.threads.len(), num_of_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"); @@ -401,7 +387,7 @@ fn test_sanitize_stack_copy() { assert_eq!(simulated_stack[0..size_of::()], defaced); - assert_no_soft_errors!(soft_errors, dumper.resume_threads(&mut soft_errors)); + drop(dumper); child.kill().expect("Failed to kill process"); diff --git a/tests/task_dumper.rs b/tests/task_dumper.rs index 1411acc3..146dc7c4 100644 --- a/tests/task_dumper.rs +++ b/tests/task_dumper.rs @@ -1,8 +1,10 @@ //! All of these tests are specific to the MacOS task dumper #![cfg(target_os = "macos")] -use minidump_writer::{mach::LoadCommand, task_dumper::TaskDumper}; -use std::fmt::Write; +use { + minidump_writer::{mach::LoadCommand, task_dumper::TaskDumper}, + std::fmt::Write, +}; fn call_otool(args: &[&str]) -> String { let mut cmd = std::process::Command::new("otool"); diff --git a/tests/windows_minidump_writer.rs b/tests/windows_minidump_writer.rs index 95f08467..531f2ae3 100644 --- a/tests/windows_minidump_writer.rs +++ b/tests/windows_minidump_writer.rs @@ -1,12 +1,15 @@ #![cfg(all(target_os = "windows", target_arch = "x86_64"))] -use minidump::{ - CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpSystemInfo, - MinidumpThreadList, +use { + common::start_child_and_return, + minidump::{ + CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpSystemInfo, + MinidumpThreadList, + }, + minidump_writer::minidump_writer::MinidumpWriter, }; -use minidump_writer::minidump_writer::MinidumpWriter; + mod common; -use common::start_child_and_return; const EXCEPTION_ILLEGAL_INSTRUCTION: i32 = -1073741795; const STATUS_INVALID_PARAMETER: i32 = -1073741811;