Skip to content

Commit

Permalink
Add inital MacOS implementation (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jake-Shadle authored Apr 21, 2022
1 parent 556dd4d commit 7049170
Show file tree
Hide file tree
Showing 52 changed files with 2,148 additions and 545 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
debug = 2

[patch.crates-io]
minidump-common = { git = "https://github.com/EmbarkStudios/rust-minidump", branch = "main" }
#minidump-writer = { git = "https://github.com/EmbarkStudios/minidump_writer_linux", branch = "main" }
#minidump-writer = { path = "../mdw" }
crash-context = { path = "crash-context" }
5 changes: 4 additions & 1 deletion crash-context/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ rust-version = "1.59.0" # We use `global_asm!`
[dependencies]
cfg-if = "1.0"

[target.'cfg(unix)'.dependencies]
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
libc = "0.2"

[target.'cfg(target_os = "windows")'.dependencies.windows-sys]
Expand All @@ -29,3 +29,6 @@ features = [
"Win32_System_Diagnostics_Debug",
"Win32_System_Kernel",
]

[target.'cfg(target_os = "macos")'.dependencies]
mach2 = "0.4"
3 changes: 3 additions & 0 deletions crash-context/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,8 @@ cfg_if::cfg_if! {
} else if #[cfg(target_os = "windows")] {
mod windows;
pub use windows::*;
} else if #[cfg(target_os = "macos")] {
mod mac;
pub use mac::*;
}
}
24 changes: 24 additions & 0 deletions crash-context/src/mac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use mach2::{exception_types as et, mach_types as mt};

/// Information on the exception that caused the crash
#[derive(Copy, Clone)]
pub struct ExceptionInfo {
/// The exception kind
pub kind: et::exception_type_t,
/// The exception code
pub code: et::mach_exception_data_type_t,
/// Optional subcode, typically only present for `EXC_BAD_ACCESS` exceptions
pub subcode: Option<et::mach_exception_data_type_t>,
}

/// Mac crash context
pub struct CrashContext {
/// The process which crashed
pub task: mt::task_t,
/// The thread in the process that crashed
pub thread: mt::thread_t,
/// The thread that handled the exception. This may be useful to ignore.
pub handler_thread: mt::thread_t,
/// Optional exception information
pub exception: Option<ExceptionInfo>,
}
4 changes: 2 additions & 2 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ skip = [
# range-map uses an old version, and is unfortunately unmaintained and has
# not seen a release in 5 years
{ name = "num-traits", version = "=0.1.43" },
# parking_lot unfortunatley uses an outdated version
{ name = "windows-sys", version = "=0.32.0" },
# debugid uses the pre-1.0 version
{ name = "uuid", version = "=0.8.2" },
]

[sources]
Expand Down
5 changes: 5 additions & 0 deletions exception-handler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,10 @@ features = [
"Win32_System_Threading",
]

[target.'cfg(target_os = "macos")'.dependencies]
# Bindings to MacOS specific APIs that are used. Note we don't use the `mach`
# crate as it is unmaintained
mach2 = "0.4"

[dev-dependencies]
sadness-generator = { path = "../sadness-generator" }
15 changes: 15 additions & 0 deletions exception-handler/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ pub enum Error {
OutOfMemory,
InvalidArgs,
Format(std::fmt::Error),
/// For simplicity sake, only one `ExceptionHandler` can be registered
/// at any one time.
HandlerAlreadyInstalled,
Io(std::io::Error),
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Format(inner) => Some(inner),
Self::Io(inner) => Some(inner),
_ => None,
}
}
Expand All @@ -22,6 +27,10 @@ impl fmt::Display for Error {
Self::OutOfMemory => f.write_str("unable to allocate memory"),
Self::InvalidArgs => f.write_str("invalid arguments provided"),
Self::Format(e) => write!(f, "{}", e),
Self::HandlerAlreadyInstalled => {
f.write_str("an exception handler is already installed")
}
Self::Io(e) => write!(f, "{}", e),
}
}
}
Expand All @@ -31,3 +40,9 @@ impl From<std::fmt::Error> for Error {
Self::Format(e)
}
}

impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
77 changes: 60 additions & 17 deletions exception-handler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,71 @@ pub fn write_stderr(s: &'static str) {
}

cfg_if::cfg_if! {
if #[cfg(unix)] {
if #[cfg(all(unix, not(target_os = "macos")))] {
/// The sole purpose of the unix module is to hook pthread_create to ensure
/// an alternate stack is installed for every native thread in case of a
/// stack overflow. This doesn't apply to MacOS as it uses exception ports,
/// which are always delivered to a specific thread owned by the exception
/// handler
pub mod unix;
}
}

pub use crash_context::CrashContext;

pub enum CrashEventResult {
/// The event was handled in some way
Handled(bool),
/// The handler wishes to jump somewhere else, presumably to return
/// execution and skip the code that caused the exception
Jump {
/// The location to jump back to, retrieved via sig/setjmp
jmp_buf: *mut jmp::JmpBuf,
/// The value that will be returned from the sig/setjmp call that we
/// jump to. Note that if the value is 0 it will be corrected to 1
value: i32,
},
}

impl From<bool> for CrashEventResult {
fn from(b: bool) -> Self {
Self::Handled(b)
}
}

/// User implemented trait for handling a signal that has ocurred.
///
/// # Safety
///
/// This trait is marked unsafe as care needs to be taken when implementing it
/// due to running in a compromised context. Notably, only a small subset of
/// libc functions are [async signal safe](https://man7.org/linux/man-pages/man7/signal-safety.7.html)
/// This trait is marked unsafe as care needs to be taken when implementing it
/// due to running in a compromised context. In general, it is advised to do as
/// _little_ as possible when handling an exception, with more complicated or
/// dangerous (in a compromised context) code being intialized before the
/// [`ExceptionHandler`] is installed, or hoisted out to another process entirely.
///
/// ## Linux
///
/// Notably, only a small subset of libc functions are
/// [async signal safe](https://man7.org/linux/man-pages/man7/signal-safety.7.html)
/// and calling non-safe ones can have undefined behavior, including such common
/// ones as `malloc` (if using a multi-threaded allocator). In general, it is
/// advised to do as _little_ as possible when handling a signal, with more
/// complicated or dangerous (in a compromised context) code being intialized
/// before the signal handler is installed, or hoisted out to an entirely
/// different sub-process.
/// ones as `malloc` (especially if using a multi-threaded allocator).
///
/// ## Windows
///
/// Windows [structured exceptions](https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling)
/// don't have the a notion similar to signal safety, but it is again recommended
/// to do as little work as possible in response to an exception.
///
/// ## Macos
///
/// Mac uses exception ports (sorry, can't give a good link here since Apple
/// documentation is terrible) which are handled by a thread owned by the
/// exception handler which makes them slightly safer to handle than UNIX signals,
/// but it is again recommended to do as little work as possible.
pub unsafe trait CrashEvent: Send + Sync {
/// Method invoked when a crash occurs. Returning true indicates your handler
/// has processed the crash and that no further handlers should run.
fn on_crash(&self, context: &CrashContext) -> bool;
fn on_crash(&self, context: &CrashContext) -> CrashEventResult;
}

/// Creates a [`CrashEvent`] using the supplied closure as the implementation.
Expand All @@ -147,17 +188,17 @@ pub unsafe trait CrashEvent: Send + Sync {
#[inline]
pub unsafe fn make_crash_event<F>(closure: F) -> Box<dyn CrashEvent>
where
F: Send + Sync + Fn(&CrashContext) -> bool + 'static,
F: Send + Sync + Fn(&CrashContext) -> CrashEventResult + 'static,
{
struct Wrapper<F> {
inner: F,
}

unsafe impl<F> CrashEvent for Wrapper<F>
where
F: Send + Sync + Fn(&CrashContext) -> bool,
F: Send + Sync + Fn(&CrashContext) -> CrashEventResult,
{
fn on_crash(&self, context: &CrashContext) -> bool {
fn on_crash(&self, context: &CrashContext) -> CrashEventResult {
(self.inner)(context)
}
}
Expand All @@ -167,14 +208,16 @@ where

cfg_if::cfg_if! {
if #[cfg(any(target_os = "linux", target_os = "android"))] {
#[macro_use]
pub mod linux;

pub use linux::{ExceptionHandler, Signal};
pub use linux::{ExceptionHandler, Signal, jmp};
} else if #[cfg(target_os = "windows")] {
#[macro_use]
pub mod windows;

pub use windows::{ExceptionHandler, ExceptionCode};
pub use windows::{ExceptionHandler, ExceptionCode, jmp};
} else if #[cfg(target_os = "macos")] {
pub mod mac;

pub use mac::{ExceptionHandler, ExceptionType, jmp};
}
}
3 changes: 2 additions & 1 deletion exception-handler/src/linux.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod jmp;
mod state;

use crate::Error;
Expand Down Expand Up @@ -87,7 +88,7 @@ impl ExceptionHandler {
}

/// Sends the specified user signal.
pub fn simulate_signal(&self, signal: Signal) -> bool {
pub fn simulate_signal(&self, signal: Signal) -> crate::CrashEventResult {
// Normally this would be an unsafe function, since this unsafe encompasses
// the entirety of the body, however the user is really not required to
// uphold any guarantees on their end, so no real need to declare the
Expand Down
31 changes: 31 additions & 0 deletions exception-handler/src/linux/jmp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
cfg_if::cfg_if! {
if #[cfg(target_arch = "x86_64")] {
#[repr(C)]
pub struct __jmp_buf([u64; 8]);
} else if #[cfg(target_arch = "x86")] {
#[repr(C)]
pub struct __jmp_buf([u32; 6]);
} else if #[cfg(target_arch = "arm")] {
#[repr(C)]
pub struct __jmp_buf([u64; 32]);
} else if #[cfg(target_arch = "aarch64")] {
#[repr(C)]
pub struct __jmp_buf([u64; 22]);
}
}

#[repr(C)]
pub struct JmpBuf {
/// CPU context
__jmp_buf: __jmp_buf,
/// Whether the signal mask was saved
__fl: u32,
/// Saved signal mask
__ss: [u32; 32],
}

extern "C" {
#[cfg_attr(target_env = "gnu", link_name = "__sigsetjmp")]
pub fn sigsetjmp(jb: *mut JmpBuf, save_mask: i32) -> i32;
pub fn siglongjmp(jb: *mut JmpBuf, val: i32) -> !;
}
57 changes: 37 additions & 20 deletions exception-handler/src/linux/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,17 +241,23 @@ unsafe extern "C" fn signal_handler(
let info = &mut *info;
let uc = &mut *uc;

{
enum Action {
RestoreDefault,
RestorePrevious,
Jump((*mut super::jmp::JmpBuf, i32)),
}

let action = {
let handlers = HANDLER_STACK.lock();

// Sometimes, Breakpad runs inside a process where some other buggy code
// saves and restores signal handlers temporarily with 'signal'
// instead of 'sigaction'. This loses the SA_SIGINFO flag associated
// with this function. As a consequence, the values of 'info' and 'uc'
// become totally bogus, generally inducing a crash.
// We might run inside a process where some other buggy code saves and
// restores signal handlers temporarily with `signal` instead of `sigaction`.
// This loses the `SA_SIGINFO` flag associated with this function. As a
// consequence, the values of `info` and `uc` become totally bogus,
// generally inducing a crash.
//
// The following code tries to detect this case. When it does, it
// resets the signal handlers with sigaction + SA_SIGINFO and returns.
// resets the signal handlers with `sigaction` & `SA_SIGINFO` and returns.
// This forces the signal to be thrown again, but this time the kernel
// will call the function with the right arguments.
{
Expand All @@ -278,30 +284,41 @@ unsafe extern "C" fn signal_handler(
}
}

let handled = (|| {
(|| {
for handler in handlers.iter() {
if let Some(handler) = handler.upgrade() {
if handler.handle_signal(sig as i32, info, uc) {
return true;
match handler.handle_signal(sig as i32, info, uc) {
crate::CrashEventResult::Handled(true) => return Action::RestoreDefault,
crate::CrashEventResult::Handled(false) => {}
crate::CrashEventResult::Jump { jmp_buf, value } => {
return Action::Jump((jmp_buf, value));
}
}
}
}

false
})();
Action::RestorePrevious
})()
};

// Upon returning from this signal handler, sig will become unmasked and
// then it will be retriggered. If one of the ExceptionHandlers handled
// it successfully, restore the default handler. Otherwise, restore the
// previously installed handler. Then, when the signal is retriggered,
// it will be delivered to the appropriate handler.
if handled {
// Upon returning from this signal handler, sig will become unmasked and
// then it will be retriggered. If one of the ExceptionHandlers handled
// it successfully, restore the default handler. Otherwise, restore the
// previously installed handler. Then, when the signal is retriggered,
// it will be delivered to the appropriate handler.
match action {
Action::RestoreDefault => {
debug_print!("installing default handler");
install_default_handler(sig);
} else {
}
Action::RestorePrevious => {
debug_print!("restoring handlers");
restore_handlers();
}
Action::Jump((jmp_buf, value)) => {
debug_print!("jumping");
super::jmp::siglongjmp(jmp_buf, value);
}
}

debug_print!("finishing signal handler");
Expand Down Expand Up @@ -345,7 +362,7 @@ impl HandlerInner {
_sig: libc::c_int,
info: &mut libc::siginfo_t,
uc: &mut libc::c_void,
) -> bool {
) -> crate::CrashEventResult {
// The siginfo_t in libc is lowest common denominator, but this code is
// specifically targeting linux/android, which contains the si_pid field
// that we require
Expand Down
Loading

0 comments on commit 7049170

Please sign in to comment.