minidump-writer 0.12.0

Rust rewrite of Breakpad's minidump_writer
Documentation
/* 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 https://mozilla.org/MPL/2.0/. */

use crate::module_reader::ModuleMemory;
use mach2::{
    kern_return::KERN_SUCCESS,
    task::task_info,
    task_info::{TASK_DYLD_ALL_IMAGE_INFO_64, TASK_DYLD_INFO, task_dyld_info},
    vm::mach_vm_read_overwrite,
};
use std::mem::{MaybeUninit, size_of};

pub type ProcessHandle = mach2::mach_types::task_t;

pub struct ProcessReader {
    process: ProcessHandle,
}

#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct AllImagesInfo {
    // VERSION 1
    pub version: u32,
    /// The number of [`ImageInfo`] structs at that following address
    info_array_count: u32,
    /// The address in the process where the array of [`ImageInfo`] structs is
    info_array_addr: u64,
    /// A function pointer, unused
    _notification: u64,
    /// Unused
    _process_detached_from_shared_region: bool,
    // VERSION 2
    lib_system_initialized: bool,
    // Note that crashpad adds a 32-bit int here to get proper alignment when
    // building on 32-bit targets...but we explicitly don't care about 32-bit
    // targets since Apple doesn't
    pub dyld_image_load_address: u64,
}

/// `dyld_image_info` from <usr/include/mach-o/dyld_images.h>
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct ImageInfo {
    /// The address in the process where the image is loaded
    pub load_address: u64,
    /// The address in the process where the image's file path can be read
    pub file_path: u64,
    /// Timestamp for when the image's file was last modified
    pub file_mod_date: u64,
}

#[derive(Debug, thiserror::Error, serde::Serialize)]
#[error("Copy from process {child} failed (source {src}, length: {length})")]
pub struct CopyFromProcessError {
    pub child: ProcessHandle,
    pub src: usize,
    pub length: usize,
    pub kern_return: mach2::kern_return::kern_return_t,
}

#[derive(Debug, thiserror::Error)]
pub enum FindModuleError {
    #[error("Failed to get task info")]
    TaskInfoError,
    #[error("The image is not a supported format")]
    ImageFormatInvalid,
    #[error("The module was not found")]
    ModuleNotFound,
    #[error("Failed to read data from the module")]
    FailedToReadModule(#[from] CopyFromProcessError),
}

impl ProcessReader {
    pub fn new(process: ProcessHandle) -> ProcessReader {
        ProcessReader { process }
    }

    pub fn read(&self, src: usize, dst: &mut [u8]) -> Result<usize, CopyFromProcessError> {
        let mut size: u64 = 0;
        let res = unsafe {
            mach_vm_read_overwrite(
                self.process,
                src as _,
                dst.len() as _,
                dst.as_mut_ptr() as _,
                &mut size as _,
            )
        };

        if res == KERN_SUCCESS {
            Ok(size as usize)
        } else {
            Err(CopyFromProcessError {
                child: self.process,
                src,
                length: dst.len(),
                kern_return: res,
            })
        }
    }

    pub fn find_module(&self, module_name: &str) -> Result<ModuleMemory<'_>, FindModuleError> {
        let dyld_info = self.task_info()?;
        if (dyld_info.all_image_info_format as u32) != TASK_DYLD_ALL_IMAGE_INFO_64 {
            return Err(FindModuleError::ImageFormatInvalid);
        }

        let all_image_info_size = dyld_info.all_image_info_size;
        let all_image_info_addr = dyld_info.all_image_info_addr;
        if (all_image_info_size as usize) < size_of::<AllImagesInfo>() {
            return Err(FindModuleError::ImageFormatInvalid);
        }

        // SAFETY: The values of AllImagesInfo can be arbitrary; if they are incorrect (e.g. bad
        // addresses), bounds checking will produce an appropriate error.
        let all_images_info =
            unsafe { self.copy_object::<AllImagesInfo>(all_image_info_addr as _) }?;

        // Load the images
        // SAFETY: ImageInfo is allowed to have arbitrary values: if they are not valid, it will be
        // caught later.
        let images = unsafe {
            self.copy_array::<ImageInfo>(
                all_images_info.info_array_addr as _,
                all_images_info.info_array_count as _,
            )
        }?;

        images
            .iter()
            .find(|&image| {
                let image_path = self.copy_nul_terminated_string(image.file_path as usize);

                if let Ok(image_path) = image_path {
                    if let Some(image_name) = image_path.into_bytes().rsplit(|&b| b == b'/').next()
                    {
                        image_name.eq(module_name.as_bytes())
                    } else {
                        false
                    }
                } else {
                    false
                }
            })
            .map(|image| ModuleMemory::from_process(self, image.load_address as usize))
            .ok_or(FindModuleError::ModuleNotFound)
    }

    fn task_info(&self) -> Result<task_dyld_info, FindModuleError> {
        let mut info = MaybeUninit::<task_dyld_info>::uninit();
        let mut count = (size_of::<task_dyld_info>() / size_of::<u32>()) as u32;

        let res = unsafe {
            task_info(
                self.process,
                TASK_DYLD_INFO,
                info.as_mut_ptr().cast(),
                &mut count,
            )
        };

        if res == KERN_SUCCESS {
            // SAFETY: this will be initialized if the call succeeded
            unsafe { Ok(info.assume_init()) }
        } else {
            Err(FindModuleError::TaskInfoError)
        }
    }
}