/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ pub mod crash_annotations { include!(concat!(env!("OUT_DIR"), "/crash_annotations.rs")); } use super::{ breakpad_crash_generator::{BreakpadCrashGenerator, BreakpadProcessId}, phc::{self, StackTrace}, }; #[cfg(any(target_os = "android", target_os = "linux"))] mod linux; #[cfg(any(target_os = "android", target_os = "linux"))] pub(crate) use linux::{get_auxv_info, PlatformData}; #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] pub(crate) use windows::PlatformData; #[cfg(any(target_os = "ios", target_os = "macos"))] mod mach; #[cfg(any(target_os = "ios", target_os = "macos"))] pub(crate) use mach::PlatformData; use anyhow::{Context, Result}; use crash_annotations::{ should_include_annotation, type_of_annotation, CrashAnnotation, CrashAnnotationType, }; use crash_helper_common::{BreakpadChar, BreakpadData, BreakpadString, GeckoChildId, Pid}; use mozannotation_server::{AnnotationData, CAnnotation}; use num_traits::FromPrimitive; use once_cell::sync::Lazy; use std::{ collections::HashMap, convert::TryInto, ffi::{c_char, CStr, CString, OsStr, OsString}, fs::File, io::{Seek, SeekFrom, Write}, mem::size_of, path::{Path, PathBuf}, sync::Mutex, }; pub(crate) struct CrashReport { pub(crate) path: OsString, pub(crate) error: Option, } impl CrashReport { fn new(path: &OsStr, error: &Option) -> CrashReport { CrashReport { path: path.to_owned(), error: error.to_owned(), } } } // Table holding all the crash reports we've generated. It's indexed by PID and // new crash reports are insterted in the corresponding vector in order of // arrival. When crashes are retrieved they're similarly pulled out in the // order they've arrived. static CRASH_REPORTS: Lazy>>> = Lazy::new(Default::default); static CRASH_REPORTS_BY_ID: Lazy>> = Lazy::new(Default::default); /****************************************************************************** * Crash generator * ******************************************************************************/ #[derive(PartialEq)] enum MinidumpOrigin { Breakpad, WindowsErrorReporting, } pub(crate) struct CrashGenerator { // This will be used for generating hangs _minidump_path: OsString, breakpad_server: BreakpadCrashGenerator, } impl CrashGenerator { pub(crate) fn new( breakpad_data: BreakpadData, minidump_path: OsString, ) -> Result { let breakpad_server = BreakpadCrashGenerator::new( breakpad_data, minidump_path.clone(), finalize_breakpad_minidump, )?; Ok(CrashGenerator { _minidump_path: minidump_path, breakpad_server, }) } pub(crate) fn set_path(&mut self, path: OsString) { self.breakpad_server.set_path(path); } pub(crate) fn move_report_to_id(&self, pid: Pid, id: GeckoChildId) { let mut map = CRASH_REPORTS.lock().unwrap(); let mut id_map = CRASH_REPORTS_BY_ID.lock().unwrap(); if let Some(mut entry) = map.remove(&pid) { let crash_report = entry.remove(0); if !entry.is_empty() { map.insert(pid, entry); } id_map.insert(id, crash_report); } } pub(crate) fn retrieve_minidump_by_pid(&self, pid: Pid) -> Option { let mut map = CRASH_REPORTS.lock().unwrap(); if let Some(mut entry) = map.remove(&pid) { let crash_report = entry.remove(0); if !entry.is_empty() { map.insert(pid, entry); } return Some(crash_report); } None } pub(crate) fn retrieve_minidump_by_id(&self, id: GeckoChildId) -> Option { let mut map = CRASH_REPORTS_BY_ID.lock().unwrap(); map.remove(&id) } } /****************************************************************************** * Crash annotations * ******************************************************************************/ macro_rules! read_numeric_annotation { ($t:ty,$d:expr) => { if let AnnotationData::ByteBuffer(buff) = $d { if buff.len() == size_of::<$t>() { let value = buff.get(0..size_of::<$t>()).map(|bytes| { let bytes: [u8; size_of::<$t>()] = bytes.try_into().unwrap(); <$t>::from_ne_bytes(bytes) }); value.map(|value| value.to_string().into_bytes()) } else { None } } else { None } }; } fn write_phc_annotations(file: &mut File, buff: &[u8]) -> Result<()> { let addr_info = phc::AddrInfo::from_bytes(buff)?; if addr_info.kind == phc::Kind::Unknown { return Ok(()); } write!( file, "\"PHCKind\":\"{}\",\ \"PHCBaseAddress\":\"{}\",\ \"PHCUsableSize\":\"{}\",", addr_info.kind_as_str(), addr_info.base_addr as usize, addr_info.usable_size, )?; if addr_info.alloc_stack.has_stack != 0 { write!( file, "\"PHCAllocStack\":\"{}\",", serialize_phc_stack(&addr_info.alloc_stack) )?; } if addr_info.free_stack.has_stack != 0 { write!( file, "\"PHCFreeStack\":\"{}\",", serialize_phc_stack(&addr_info.free_stack) )?; } Ok(()) } fn serialize_phc_stack(stack_trace: &StackTrace) -> String { let mut string = String::new(); for i in 0..stack_trace.length { string.push_str(&(stack_trace.pcs[i] as usize).to_string()); string.push(','); } string.pop(); string } /// This reads the crash annotations, writes them to the .extra file and /// finally stores the resulting minidump in the global hash table. extern "C" fn finalize_breakpad_minidump( process_id: BreakpadProcessId, error_ptr: *const c_char, minidump_path_ptr: *const BreakpadChar, ) { let minidump_path = PathBuf::from(unsafe { ::from_ptr(minidump_path_ptr) }); let error = if !error_ptr.is_null() { // SAFETY: The string is a valid C string we passed in ourselves. Some(unsafe { CStr::from_ptr(error_ptr) }.to_owned()) } else { None }; finalize_crash_report(process_id, error, &minidump_path, MinidumpOrigin::Breakpad); } fn finalize_crash_report( process_id: BreakpadProcessId, error: Option, minidump_path: &Path, origin: MinidumpOrigin, ) { let mut extra_path = PathBuf::from(minidump_path); extra_path.set_extension("extra"); let annotations = retrieve_annotations(&process_id, origin); let extra_file_written = annotations .map(|annotations| write_extra_file(&annotations, &extra_path)) .is_ok(); let path = minidump_path.as_os_str(); let error = if !extra_file_written { Some(CString::new("MissingAnnotations").unwrap()) } else { error }; let map = &mut CRASH_REPORTS.lock().unwrap(); let entry = map.entry(process_id.pid); entry .and_modify(|entry| entry.push(CrashReport::new(path, &error))) .or_insert_with(|| vec![CrashReport::new(path, &error)]); } fn retrieve_annotations( process_id: &BreakpadProcessId, origin: MinidumpOrigin, ) -> Result> { let res = mozannotation_server::retrieve_annotations( process_id.get_native(), CrashAnnotation::Count as usize, ); let mut annotations = res?; if origin == MinidumpOrigin::WindowsErrorReporting { annotations.push(CAnnotation { id: CrashAnnotation::WindowsErrorReporting as u32, data: AnnotationData::ByteBuffer(vec![1]), }); } // Add a unique identifier for this crash event. let crash_id = uuid::Uuid::new_v4() .as_hyphenated() .encode_lower(&mut uuid::Uuid::encode_buffer()) .to_string(); annotations.push(CAnnotation { id: CrashAnnotation::CrashID as u32, data: AnnotationData::String(CString::new(crash_id).context("uuid contains nul byte")?), }); Ok(annotations) } fn write_extra_file(annotations: &Vec, path: &Path) -> Result<()> { let mut annotations_written: usize = 0; let mut file = File::create(path)?; write!(&mut file, "{{")?; for annotation in annotations { if let Some(annotation_id) = CrashAnnotation::from_u32(annotation.id) { if annotation_id == CrashAnnotation::PHCBaseAddress { if let AnnotationData::ByteBuffer(buff) = &annotation.data { write_phc_annotations(&mut file, buff)?; } continue; } let value = match type_of_annotation(annotation_id) { CrashAnnotationType::String => match &annotation.data { AnnotationData::String(string) => Some(escape_value(string.as_bytes())), AnnotationData::ByteBuffer(buffer) => Some(escape_value(buffer)), _ => None, }, CrashAnnotationType::Boolean => { if let AnnotationData::ByteBuffer(buff) = &annotation.data { if buff.len() == 1 { Some(vec![if buff[0] != 0 { b'1' } else { b'0' }]) } else { None } } else { None } } CrashAnnotationType::U32 => { read_numeric_annotation!(u32, &annotation.data) } CrashAnnotationType::U64 => { read_numeric_annotation!(u64, &annotation.data) } CrashAnnotationType::USize => { read_numeric_annotation!(usize, &annotation.data) } CrashAnnotationType::Object => None, // This cannot be found in memory }; if let Some(value) = value { if !value.is_empty() && should_include_annotation(annotation_id, &value) { write!(&mut file, "\"{annotation_id:}\":\"")?; file.write_all(&value)?; write!(&mut file, "\",")?; annotations_written += 1; } } } } if annotations_written > 0 { // Drop the last comma file.seek(SeekFrom::Current(-1))?; } writeln!(&mut file, "}}")?; Ok(()) } // Escapes the characters of a crash annotation so that they appear correctly // within the JSON output, escaping non-visible characters and the like. This // does not try to make the output valid UTF-8 because the input might be // corrupted so there's no point in that. fn escape_value(input: &[u8]) -> Vec { let mut escaped = Vec::::with_capacity(input.len() + 2); for &c in input { if c <= 0x1f || c == b'\\' || c == b'"' { escaped.extend(b"\\u00"); escaped.push(hex_digit_as_ascii_char((c & 0x00f0) >> 4)); escaped.push(hex_digit_as_ascii_char(c & 0x000f)); } else { escaped.push(c) } } escaped } fn hex_digit_as_ascii_char(value: u8) -> u8 { if value < 10 { b'0' + value } else { b'a' + (value - 10) } }