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())