diff --git a/src/mac.rs b/src/mac.rs index 4d405999..745a5a1e 100644 --- a/src/mac.rs +++ b/src/mac.rs @@ -3,6 +3,9 @@ #[cfg(target_pointer_width = "32")] compile_error!("Various MacOS FFI bindings assume we are on a 64-bit architechture"); +/// Re-export of the mach2 library for users who want to call mach specific functions +pub use mach2; + pub mod errors; pub mod mach; pub mod minidump_writer; diff --git a/src/mac/errors.rs b/src/mac/errors.rs index ea85162b..96ddb88c 100644 --- a/src/mac/errors.rs +++ b/src/mac/errors.rs @@ -8,4 +8,6 @@ pub enum WriterError { MemoryWriterError(#[from] crate::mem_writer::MemoryWriterError), #[error("Failed to write to file")] FileWriterError(#[from] crate::dir_section::FileWriterError), + #[error("Attempted to write an exception stream with no crash context")] + NoCrashContext, } diff --git a/src/mac/minidump_writer.rs b/src/mac/minidump_writer.rs index aed3dcbe..b05662fd 100644 --- a/src/mac/minidump_writer.rs +++ b/src/mac/minidump_writer.rs @@ -6,25 +6,71 @@ use crate::{ }; use std::io::{Seek, Write}; +pub use mach2::mach_types::{task_t, thread_t}; + type Result = std::result::Result; pub struct MinidumpWriter { /// The crash context as captured by an exception handler - pub(crate) crash_context: crash_context::CrashContext, + pub(crate) crash_context: Option, /// List of raw blocks of memory we've written into the stream. These are /// referenced by other streams (eg thread list) pub(crate) memory_blocks: Vec, + /// The task being dumped + pub(crate) task: task_t, + /// The handler thread, so it can be ignored/deprioritized + pub(crate) handler_thread: thread_t, } impl MinidumpWriter { - /// Creates a minidump writer - pub fn new(crash_context: crash_context::CrashContext) -> Self { + /// Creates a minidump writer for the specified mach task (process) and + /// handler thread. If not specified, defaults to the current task and thread. + /// + /// ``` + /// use minidump_writer::{minidump_writer::MinidumpWriter, mach2}; + /// + /// // Note that this is the same as specifying `None` for both the task and + /// // handler thread, this is just meant to illustrate how you can setup + /// // a MinidumpWriter manually instead of using a `CrashContext` + /// // SAFETY: syscalls + /// let mdw = unsafe { + /// MinidumpWriter::new( + /// Some(mach2::traps::mach_task_self()), + /// Some(mach2::mach_init::mach_thread_self()), + /// ) + /// }; + /// ``` + pub fn new(task: Option, handler_thread: Option) -> Self { + Self { + crash_context: None, + memory_blocks: Vec::new(), + task: task.unwrap_or_else(|| { + // SAFETY: syscall + unsafe { mach2::traps::mach_task_self() } + }), + handler_thread: handler_thread.unwrap_or_else(|| { + // SAFETY: syscall + unsafe { mach2::mach_init::mach_thread_self() } + }), + } + } + + /// Creates a minidump writer with the specified crash context, presumably + /// for another task + pub fn with_crash_context(crash_context: crash_context::CrashContext) -> Self { + let task = crash_context.task; + let handler_thread = crash_context.handler_thread; + Self { - crash_context, + crash_context: Some(crash_context), memory_blocks: Vec::new(), + task, + handler_thread, } } + /// Writes a minidump to the specified destination, returning the raw minidump + /// contents upon success pub fn dump(&mut self, destination: &mut (impl Write + Seek)) -> Result> { let writers = { #[allow(clippy::type_complexity)] @@ -43,7 +89,12 @@ impl MinidumpWriter { // Exception stream needs to be the last entry in this array as it may // be omitted in the case where the minidump is written without an // exception. - if self.crash_context.exception.is_some() { + if self + .crash_context + .as_ref() + .and_then(|cc| cc.exception.as_ref()) + .is_some() + { writers.push(Box::new(|mw, buffer, dumper| { mw.write_exception(buffer, dumper) })); @@ -77,7 +128,7 @@ impl MinidumpWriter { // we should have a mostly-intact dump dir_section.write_to_file(&mut buffer, None)?; - let dumper = super::task_dumper::TaskDumper::new(self.crash_context.task); + let dumper = super::task_dumper::TaskDumper::new(self.task); for mut writer in writers { let dirent = writer(self, &mut buffer, &dumper)?; @@ -93,7 +144,7 @@ impl MinidumpWriter { pub(crate) fn threads(&self, dumper: &TaskDumper) -> ActiveThreads { ActiveThreads { threads: dumper.read_threads().unwrap_or_default(), - handler_thread: self.crash_context.handler_thread, + handler_thread: self.handler_thread, i: 0, } } diff --git a/src/mac/streams/breakpad_info.rs b/src/mac/streams/breakpad_info.rs index d8f1e587..5196a95c 100644 --- a/src/mac/streams/breakpad_info.rs +++ b/src/mac/streams/breakpad_info.rs @@ -20,9 +20,9 @@ impl MinidumpWriter { | BreakpadInfoValid::RequestingThreadId.bits(), // The thread where the exception port handled the exception, might // be useful to ignore/deprioritize when processing the minidump - dump_thread_id: self.crash_context.handler_thread, + dump_thread_id: self.handler_thread, // The actual thread where the exception was thrown - requesting_thread_id: self.crash_context.thread, + requesting_thread_id: self.crash_context.as_ref().map(|cc| cc.thread).unwrap_or(0), }, )?; diff --git a/src/mac/streams/exception.rs b/src/mac/streams/exception.rs index 69aabc1a..6d7c91af 100644 --- a/src/mac/streams/exception.rs +++ b/src/mac/streams/exception.rs @@ -10,7 +10,14 @@ impl MinidumpWriter { buffer: &mut DumpBuf, dumper: &TaskDumper, ) -> Result { - let thread_state = dumper.read_thread_state(self.crash_context.thread).ok(); + // This shouldn't fail since we won't be writing this stream if the crash context is + // not present + let crash_context = self + .crash_context + .as_ref() + .ok_or(WriterError::NoCrashContext)?; + + let thread_state = dumper.read_thread_state(crash_context.thread).ok(); let thread_context = if let Some(ts) = &thread_state { let mut cpu = Default::default(); @@ -22,8 +29,7 @@ impl MinidumpWriter { None }; - let exception_record = self - .crash_context + let exception_record = crash_context .exception .as_ref() .map(|exc| { @@ -46,7 +52,7 @@ impl MinidumpWriter { .unwrap_or_default(); let stream = MDRawExceptionStream { - thread_id: self.crash_context.thread, + thread_id: crash_context.thread, exception_record, thread_context: thread_context.unwrap_or_default(), __align: 0, diff --git a/src/mac/streams/memory_list.rs b/src/mac/streams/memory_list.rs index 9ead2027..47a37fbf 100644 --- a/src/mac/streams/memory_list.rs +++ b/src/mac/streams/memory_list.rs @@ -11,44 +11,47 @@ impl MinidumpWriter { ) -> Result { // Include some memory around the instruction pointer if the crash was // due to an exception - if self.crash_context.exception.is_some() { - const IP_MEM_SIZE: u64 = 256; + if let Some(cc) = &self.crash_context { + if cc.exception.is_some() { + const IP_MEM_SIZE: u64 = 256; - let get_ip_block = |tid| -> Option> { - let thread_state = dumper.read_thread_state(tid).ok()?; + let get_ip_block = |tid| -> Option> { + let thread_state = dumper.read_thread_state(tid).ok()?; - let ip = thread_state.pc(); + let ip = thread_state.pc(); - // Bound it to the upper and lower bounds of the region - // it's contained within. If it's not in a known memory region, - // don't bother trying to write it. - let region = dumper.get_vm_region(ip).ok()?; + // Bound it to the upper and lower bounds of the region + // it's contained within. If it's not in a known memory region, + // don't bother trying to write it. + let region = dumper.get_vm_region(ip).ok()?; - if ip < region.range.start || ip > region.range.end { - return None; - } - - // Try to get IP_MEM_SIZE / 2 bytes before and after the IP, but - // settle for whatever's available. - let start = std::cmp::max(region.range.start, ip - IP_MEM_SIZE / 2); - let end = std::cmp::min(ip + IP_MEM_SIZE / 2, region.range.end); + if ip < region.range.start || ip > region.range.end { + return None; + } - Some(start..end) - }; + // Try to get IP_MEM_SIZE / 2 bytes before and after the IP, but + // settle for whatever's available. + let start = std::cmp::max(region.range.start, ip - IP_MEM_SIZE / 2); + let end = std::cmp::min(ip + IP_MEM_SIZE / 2, region.range.end); - if let Some(ip_range) = get_ip_block(self.crash_context.thread) { - let size = ip_range.end - ip_range.start; - let stack_buffer = dumper.read_task_memory(ip_range.start as _, size as usize)?; - let ip_location = MDLocationDescriptor { - data_size: size as u32, - rva: buffer.position() as u32, + Some(start..end) }; - buffer.write_all(&stack_buffer); - self.memory_blocks.push(MDMemoryDescriptor { - start_of_memory_range: ip_range.start, - memory: ip_location, - }); + if let Some(ip_range) = get_ip_block(cc.thread) { + let size = ip_range.end - ip_range.start; + let stack_buffer = + dumper.read_task_memory(ip_range.start as _, size as usize)?; + let ip_location = MDLocationDescriptor { + data_size: size as u32, + rva: buffer.position() as u32, + }; + buffer.write_all(&stack_buffer); + + self.memory_blocks.push(MDMemoryDescriptor { + start_of_memory_range: ip_range.start, + memory: ip_location, + }); + } } } diff --git a/src/mac/streams/module_list.rs b/src/mac/streams/module_list.rs index 307bc053..9dff73d8 100644 --- a/src/mac/streams/module_list.rs +++ b/src/mac/streams/module_list.rs @@ -1,5 +1,28 @@ use super::*; +struct ImageLoadInfo { + /// The preferred load address of the TEXT segment + vm_addr: u64, + /// The size of the TEXT segment + vm_size: u64, + /// The difference between the images preferred and actual load address + slide: isize, +} + +struct ImageDetails { + /// Unique identifier for the module + uuid: [u8; 16], + /// The load info for the image indicating the range of addresses it covers + load_info: ImageLoadInfo, + /// Path to the module on the local filesystem. Note that as of MacOS 11.0.1 + /// for system libraries, this path won't actually exist on the filesystem. + /// This data is more useful as human readable information in a minidump, + /// but is not required, as the real identifier is the UUID + file_path: Option, + /// Version information, not present for the main executable + version: Option, +} + impl MinidumpWriter { /// Writes the [`MDStreamType::ModuleListStream`] to the minidump, which is /// the last of all loaded modules (images) in the process. @@ -16,7 +39,9 @@ impl MinidumpWriter { // The list of modules is pretty critical information, but there could // still be useful information in the minidump without them if we can't // retrieve them for some reason - let modules = self.read_loaded_modules(buffer, dumper).unwrap_or_default(); + let modules = self + .write_loaded_modules(buffer, dumper) + .unwrap_or_default(); let list_header = MemoryWriter::::alloc_with_val(buffer, modules.len() as u32)?; @@ -33,7 +58,7 @@ impl MinidumpWriter { Ok(dirent) } - fn read_loaded_modules( + fn write_loaded_modules( &self, buf: &mut DumpBuf, dumper: &TaskDumper, @@ -50,17 +75,21 @@ impl MinidumpWriter { let mut has_main_executable = false; for image in images { - if let Ok((module, is_main_executable)) = self.read_module(image, buf, dumper) { - // We want to keep the modules sorted by their load address except - // in the case of the main executable image which we want to put - // first as it is most likely the culprit, or at least generally - // the most interesting module for human and machine inspectors - if is_main_executable { - modules.insert(0, module); - has_main_executable = true; - } else { - modules.push(module) - }; + if let Ok(image_details) = self.read_image(image, dumper) { + let is_main_executable = image_details.version.is_none(); + + if let Ok(module) = self.write_module(image_details, buf) { + // We want to keep the modules sorted by their load address except + // in the case of the main executable image which we want to put + // first, as it is most likely the culprit, or at least generally + // the most interesting module for human and machine inspectors + if is_main_executable { + modules.insert(0, module); + has_main_executable = true; + } else { + modules.push(module) + }; + } } } @@ -71,19 +100,18 @@ impl MinidumpWriter { } } - fn read_module( + /// Obtains important image metadata by traversing the image's load commands + /// + /// # Errors + /// + /// The image's load commands cannot be traversed, or a required load command + /// is missing + fn read_image( &self, image: ImageInfo, - buf: &mut DumpBuf, dumper: &TaskDumper, - ) -> Result<(MDRawModule, bool), WriterError> { - struct ImageSizes { - vm_addr: u64, - vm_size: u64, - slide: isize, - } - - let mut sizes = None; + ) -> Result { + let mut load_info = None; let mut version = None; let mut uuid = None; @@ -92,15 +120,11 @@ impl MinidumpWriter { for lc in load_commands.iter() { match lc { - mach::LoadCommand::Segment(seg) if sizes.is_none() => { + mach::LoadCommand::Segment(seg) if load_info.is_none() => { if &seg.segment_name[..7] == b"__TEXT\0" { - let slide = if seg.file_off == 0 && seg.file_size != 0 { - image.load_address as isize - seg.vm_addr as isize - } else { - 0 - }; + let slide = image.load_address as isize - seg.vm_addr as isize; - sizes = Some(ImageSizes { + load_info = Some(ImageLoadInfo { vm_addr: seg.vm_addr, vm_size: seg.vm_size, slide, @@ -116,13 +140,13 @@ impl MinidumpWriter { _ => {} } - if sizes.is_some() && version.is_some() && uuid.is_some() { + if load_info.is_some() && version.is_some() && uuid.is_some() { break; } } } - let sizes = sizes.ok_or(TaskDumpError::MissingLoadCommand { + let load_info = load_info.ok_or(TaskDumpError::MissingLoadCommand { name: "LC_SEGMENT_64", id: mach::LC_SEGMENT_64, })?; @@ -132,26 +156,37 @@ impl MinidumpWriter { })?; let file_path = if image.file_path != 0 { - dumper - .read_string(image.file_path) - .unwrap_or_default() - .unwrap_or_default() + dumper.read_string(image.file_path).unwrap_or_default() } else { - String::new() + None }; - let module_name = write_string_to_location(buf, &file_path)?; + Ok(ImageDetails { + uuid, + load_info, + file_path, + version, + }) + } + + fn write_module( + &self, + image: ImageDetails, + buf: &mut DumpBuf, + ) -> Result { + let file_path = image.file_path.as_deref().unwrap_or_default(); + let module_name = write_string_to_location(buf, file_path)?; let mut raw_module = MDRawModule { - base_of_image: (sizes.vm_addr as isize + sizes.slide) as u64, - size_of_image: sizes.vm_size as u32, + base_of_image: (image.load_info.vm_addr as isize + image.load_info.slide) as u64, + size_of_image: image.load_info.vm_size as u32, module_name_rva: module_name.rva, ..Default::default() }; // Version info is not available for the main executable image since // it doesn't issue a LC_ID_DYLIB load command - if let Some(version) = &version { + if let Some(version) = image.version { raw_module.version_info.signature = format::VS_FFI_SIGNATURE; raw_module.version_info.struct_version = format::VS_FFI_STRUCVERSION; @@ -166,7 +201,7 @@ impl MinidumpWriter { } else if file_path.is_empty() { "" } else { - &file_path + file_path }; #[derive(scroll::Pwrite, scroll::SizeWith)] @@ -181,7 +216,7 @@ impl MinidumpWriter { CvInfoPdb { cv_signature: format::CvSignature::Pdb70 as u32, age: 0, - signature: uuid.into(), + signature: image.uuid.into(), }, )?; @@ -195,6 +230,89 @@ impl MinidumpWriter { cv_location.data_size += module_name.len() as u32 + 1; raw_module.cv_record = cv_location; - Ok((raw_module, version.is_none())) + Ok(raw_module) + } +} + +#[cfg(test)] +mod test { + use super::*; + + /// This function isn't declared in libc nor mach2. And is also undocumented + /// by apple, I know, SHOCKING + extern "C" { + fn getsegmentdata( + header: *const libc::mach_header, + segname: *const u8, + size: &mut u64, + ) -> *const u8; + } + + /// Tests that the images we write as modules to the minidump are consistent + /// with those reported by the kernel. The kernel function used as the source + /// of truth can only be used to obtain info for the current process, which + /// is why they aren't used in the actual implementation as we want to handle + /// both the local and intra-process scenarios + #[test] + /// The libc functions used here are all marked as deprecated, saying you + /// should use the mach2 crate, however, the mach2 crate does not expose + /// any of these functions so... + #[allow(deprecated)] + fn images_match() { + let mdw = MinidumpWriter::new(None, None); + let td = TaskDumper::new(mdw.task); + + let images = td.read_images().unwrap(); + + let actual_image_count = unsafe { libc::_dyld_image_count() } as u32; + + assert_eq!(actual_image_count, images.len() as u32); + + for index in 0..actual_image_count { + let expected_img_hdr = unsafe { libc::_dyld_get_image_header(index) }; + + let actual_img = &images[index as usize]; + + assert_eq!(actual_img.load_address, expected_img_hdr as u64); + + let mut expect_segment_size = 0; + let expect_segment_data = unsafe { + getsegmentdata( + expected_img_hdr, + b"__TEXT\0".as_ptr(), + &mut expect_segment_size, + ) + }; + + let actual_img_details = mdw + .read_image(actual_img.clone(), &td) + .expect("failed to get image details"); + + let expected_image_name = + unsafe { std::ffi::CStr::from_ptr(libc::_dyld_get_image_name(index)) }; + + let expected_slide = unsafe { libc::_dyld_get_image_vmaddr_slide(index) }; + assert_eq!( + expected_slide, actual_img_details.load_info.slide, + "image {index}({expected_image_name:?}) slide is incorrect" + ); + + // The segment pointer has already been adjusted by the slide + assert_eq!( + expect_segment_data as u64, + (actual_img_details.load_info.vm_addr as isize + actual_img_details.load_info.slide) + as u64, + "image {index}({expected_image_name:?}) TEXT address is incorrect" + ); + assert_eq!( + expect_segment_size, actual_img_details.load_info.vm_size, + "image {index}({expected_image_name:?}) TEXT size is incorrect" + ); + + assert_eq!( + expected_image_name.to_str().unwrap(), + actual_img_details.file_path.unwrap() + ); + } } } diff --git a/tests/mac_minidump_writer.rs b/tests/mac_minidump_writer.rs index 45ad4494..b0985893 100644 --- a/tests/mac_minidump_writer.rs +++ b/tests/mac_minidump_writer.rs @@ -52,7 +52,7 @@ fn capture_minidump(name: &str, exception_kind: u32) -> Captured<'_> { let task = rcc.crash_context.task; let thread = rcc.crash_context.thread; - let mut dumper = MinidumpWriter::new(rcc.crash_context); + let mut dumper = MinidumpWriter::with_crash_context(rcc.crash_context); dumper .dump(tmpfile.as_file_mut())