//! macOS-specific handling of handling exceptions
//!
//! Unlike other Unix platforms macOS here uses mach ports to handle exceptions
//! instead of signals. While macOS platforms could use signals (and
//! historically they did!) this is incompatible when Wasmtime is linked into a
//! project that is otherwise using mach ports for catching exceptions. This
//! came up #2456 notably when a project like Breakpad is integrated to blanket
//! catch exceptions and report them.
//!
//! Mach ports are somewhat obscure and not really heavily used in a ton of
//! places. Needless to say the original author of this file worked with mach
//! ports for the first time when writing this file. As such the exact specifics
//! here may not be super well documented. This file is 100% lifted from
//! SpiderMonkey and then adapted for Wasmtime's purposes. Credit for almost
//! all of this file goes to SpiderMonkey for figuring out all the fiddly bits.
//! See also
//! <https://searchfox.org/mozilla-central/source/js/src/wasm/WasmSignalHandlers.cpp>
//! for the original code.
//!
//! The high-level overview is that when using mach ports a thread is blocked
//! when it generates an exception and then a message can be read from the
//! port. This means that, unlike signals, threads can't fix their own traps.
//! Instead a helper thread is spun up to service exception messages. This is
//! also in conflict with Wasmtime's exception handling currently which is to
//! use a thread-local to store information about how to unwind. Additionally
//! this requires that the check of whether a pc is a wasm trap or not is a
//! global check rather than a per-thread check. This necessitates the existence
//! of `GlobalModuleRegistry` in the `wasmtime` crate.
//!
//! Otherwise this file heavily uses the `mach` Rust crate for type and
//! function declarations. Many bits and pieces are copied or translated from
//! the SpiderMonkey implementation and it should pass all the tests!

#![allow(non_snake_case)]

use crate::traphandlers::{tls, wasmtime_longjmp};
use mach::exception_types::*;
use mach::kern_return::*;
use mach::mach_init::*;
use mach::mach_port::*;
use mach::message::*;
use mach::port::*;
use mach::thread_act::*;
use mach::traps::*;
use std::mem;
use std::thread;

/// Other `mach` declarations awaiting <https://github.com/fitzgen/mach/pull/64>
/// to be merged.
mod mach_addons {
    #![allow(non_camel_case_types)]
    #![allow(non_upper_case_globals)]
    #![allow(dead_code)]

    use mach::{
        exception_types::*, kern_return::*, mach_types::*, message::*, port::*, thread_status::*,
    };
    use std::mem;

    #[repr(C)]
    #[derive(Copy, Clone, Debug)]
    #[allow(dead_code)]
    pub struct NDR_record_t {
        mig_vers: libc::c_uchar,
        if_vers: libc::c_uchar,
        reserved1: libc::c_uchar,
        mig_encoding: libc::c_uchar,
        int_rep: libc::c_uchar,
        char_rep: libc::c_uchar,
        float_rep: libc::c_uchar,
        reserved32: libc::c_uchar,
    }

    extern "C" {
        pub static NDR_record: NDR_record_t;
    }

    // Note that this is copied from Gecko at
    //
    // https://searchfox.org/mozilla-central/rev/ed93119be4818da1509bbcb7b28e245853eeedd5/js/src/wasm/WasmSignalHandlers.cpp#583-601
    //
    // which distinctly diverges from the actual version of this in the header
    // files provided by macOS, notably in the `code` field which uses `i64`
    // instead of `i32`.
    //
    // Also note the `packed(4)` here which forcibly decreases alignment to 4 to
    // additionally match what mach expects (apparently, I wish I had a better
    // reference for this).
    #[repr(C, packed(4))]
    #[allow(dead_code)]
    #[derive(Copy, Clone, Debug)]
    pub struct __Request__exception_raise_t {
        pub Head: mach_msg_header_t,
        /* start of the kernel processed data */
        pub msgh_body: mach_msg_body_t,
        pub thread: mach_msg_port_descriptor_t,
        pub task: mach_msg_port_descriptor_t,
        /* end of the kernel processed data */
        pub NDR: NDR_record_t,
        pub exception: exception_type_t,
        pub codeCnt: mach_msg_type_number_t,

        // Note that this is a divergence from the C headers which use
        // `integer_t` here for this field which is a `c_int`. That isn't
        // actually reflecting reality apparently though because if `c_int` is
        // used here then the structure is too small to receive a message.
        pub code: [i64; 2],
    }

    #[repr(C)]
    #[allow(dead_code)]
    #[derive(Copy, Clone, Debug)]
    pub struct __Reply__exception_raise_t {
        pub Head: mach_msg_header_t,
        pub NDR: NDR_record_t,
        pub RetCode: kern_return_t,
    }

    #[repr(C)]
    #[derive(Copy, Clone, Debug, Default, Hash, PartialOrd, PartialEq, Eq, Ord)]
    pub struct arm_thread_state64_t {
        pub __x: [u64; 29],
        pub __fp: u64, // frame pointer x29
        pub __lr: u64, // link register x30
        pub __sp: u64, // stack pointer x31
        pub __pc: u64,
        pub __cpsr: u32,
        pub __pad: u32,
    }

    impl arm_thread_state64_t {
        pub fn count() -> mach_msg_type_number_t {
            (mem::size_of::<Self>() / mem::size_of::<u32>()) as mach_msg_type_number_t
        }
    }

    pub static ARM_THREAD_STATE64: thread_state_flavor_t = 6;

    #[cfg(target_arch = "x86_64")]
    pub static THREAD_STATE_NONE: thread_state_flavor_t = 13;
    #[cfg(target_arch = "aarch64")]
    pub static THREAD_STATE_NONE: thread_state_flavor_t = 5;

    extern "C" {
        pub fn thread_set_state(
            target_act: thread_port_t,
            flavor: thread_state_flavor_t,
            new_state: thread_state_t,
            new_stateCnt: mach_msg_type_number_t,
        ) -> kern_return_t;

        pub fn thread_set_exception_ports(
            thread: thread_port_t,
            exception_mask: exception_mask_t,
            new_port: mach_port_t,
            behavior: libc::c_uint,
            new_flavor: thread_state_flavor_t,
        ) -> kern_return_t;
    }
}

use mach_addons::*;

/// Just used below
pub enum Void {}
/// For now this is basically unused, we don't expose this any more for
/// Wasmtime on macOS.
pub type SignalHandler<'a> = dyn Fn(Void) -> bool + Send + Sync + 'a;

/// Process-global port that we use to route thread-level exceptions to.
static mut WASMTIME_PORT: mach_port_name_t = MACH_PORT_NULL;

pub unsafe fn platform_init() {
    // Allocate our WASMTIME_PORT and make sure that it can be sent to so we
    // can receive exceptions.
    let me = mach_task_self();
    let kret = mach_port_allocate(me, MACH_PORT_RIGHT_RECEIVE, &mut WASMTIME_PORT);
    assert_eq!(kret, KERN_SUCCESS, "failed to allocate port");
    let kret = mach_port_insert_right(me, WASMTIME_PORT, WASMTIME_PORT, MACH_MSG_TYPE_MAKE_SEND);
    assert_eq!(kret, KERN_SUCCESS, "failed to insert right");

    // Spin up our handler thread which will solely exist to service exceptions
    // generated by other threads. Note that this is a background thread that
    // we're not very interested in so it's detached here.
    thread::spawn(|| handler_thread());
}

// This is largely just copied from SpiderMonkey.
#[repr(C)]
#[allow(dead_code)]
#[derive(Debug)]
struct ExceptionRequest {
    body: __Request__exception_raise_t,
    trailer: mach_msg_trailer_t,
}

unsafe fn handler_thread() {
    // Taken from mach_exc in /usr/include/mach/mach_exc.defs.
    const EXCEPTION_MSG_ID: mach_msg_id_t = 2405;

    loop {
        // Block this thread reading a message from our port. This will block
        // until some thread throws an exception. Note that messages are all
        // expected to be exceptions here.
        let mut request: ExceptionRequest = mem::zeroed();
        let kret = mach_msg(
            &mut request.body.Head,
            MACH_RCV_MSG,
            0,
            mem::size_of_val(&request) as u32,
            WASMTIME_PORT,
            MACH_MSG_TIMEOUT_NONE,
            MACH_PORT_NULL,
        );
        if kret != KERN_SUCCESS {
            eprintln!("mach_msg failed with {} ({0:x})", kret);
            libc::abort();
        }
        if request.body.Head.msgh_id != EXCEPTION_MSG_ID {
            eprintln!("unexpected msg header id {}", request.body.Head.msgh_id);
            libc::abort();
        }

        // Attempt to handle the exception below which will process the state
        // of the request.
        //
        // We unconditionally need to send a message back on our port after
        // this exception is received, and our reply code here dictates whether
        // the thread crashes or whether we continue execution of the thread.
        let reply_code = if handle_exception(&mut request) {
            KERN_SUCCESS
        } else {
            KERN_FAILURE
        };

        // This magic incantation to send a reply back to the kernel was
        // derived from the exc_server generated by
        // 'mig -v /usr/include/mach/mach_exc.defs'.
        let mut reply: __Reply__exception_raise_t = mem::zeroed();
        reply.Head.msgh_bits =
            MACH_MSGH_BITS(request.body.Head.msgh_bits & MACH_MSGH_BITS_REMOTE_MASK, 0);
        reply.Head.msgh_size = mem::size_of_val(&reply) as u32;
        reply.Head.msgh_remote_port = request.body.Head.msgh_remote_port;
        reply.Head.msgh_local_port = MACH_PORT_NULL;
        reply.Head.msgh_id = request.body.Head.msgh_id + 100;
        reply.NDR = NDR_record;
        reply.RetCode = reply_code;
        mach_msg(
            &mut reply.Head,
            MACH_SEND_MSG,
            mem::size_of_val(&reply) as u32,
            0,
            MACH_PORT_NULL,
            MACH_MSG_TIMEOUT_NONE,
            MACH_PORT_NULL,
        );
    }
}

unsafe fn handle_exception(request: &mut ExceptionRequest) -> bool {
    // First make sure that this exception is one that we actually expect to
    // get raised by wasm code. All other exceptions we safely ignore.
    match request.body.exception as u32 {
        EXC_BAD_ACCESS | EXC_BAD_INSTRUCTION | EXC_ARITHMETIC => {}
        _ => return false,
    }

    // For `EXC_BAD_ACCESS` the faulting address is listed as the "subcode" in
    // the second `code` field. If we're ever interested in it the first code
    // field has a `kern_return_t` describing the kind of failure (e.g. SIGSEGV
    // vs SIGBUS), but we're not interested in that right now.
    let (fault1, fault2) = if request.body.exception as u32 == EXC_BAD_ACCESS {
        (1, request.body.code[1] as usize)
    } else {
        (0, 0)
    };

    // Depending on the current architecture various bits and pieces of this
    // will change. This is expected to get filled out for other macos
    // platforms as necessary.
    //
    // The variables this needs to define are:
    //
    // * `ThreadState` - a structure read via `thread_get_state` to learn about
    //   the register state of the thread that trapped.
    // * `thread_state_flavor` - used to read `ThreadState`
    // * `get_pc` - a function from `&ThreadState` to a pointer to read the
    //   current program counter, used to test if it's an address we're
    //   catching wasm traps for.
    // * `resume` - a function used to modify `ThreadState` to resume in the
    //   target thread in the `unwind` function below, passing the two
    //   parameters as the first two arguments.
    // * `thread_state` - a fresh instance of `ThreadState` to read into
    // * `thread_state_count` - the size to pass to `mach_msg`.
    cfg_if::cfg_if! {
        if #[cfg(target_arch = "x86_64")] {
            use mach::structs::x86_thread_state64_t;
            use mach::thread_status::x86_THREAD_STATE64;

            type ThreadState = x86_thread_state64_t;

            let thread_state_flavor = x86_THREAD_STATE64;

            let get_pc_and_fp = |state: &ThreadState| (
                state.__rip as *const u8,
                state.__rbp as usize,
            );

            let resume = |state: &mut ThreadState, pc: usize, fp: usize, fault1: usize, fault2: usize| {
                // The x86_64 ABI requires a 16-byte stack alignment for
                // functions, so typically we'll be 16-byte aligned. In this
                // case we simulate a `call` instruction by decrementing the
                // stack pointer and pushing the "return" address which in this
                // case is the faulting address. This should help the native
                // unwinder figure out how to find the precisely trapping
                // function.
                //
                // Note, however, that if the stack is not 16-byte aligned then
                // we don't do anything. Currently this only arises due to
                // `ud2` in the prologue of functions when performing the
                // initial stack check. In the old backend 0 stack manipulation
                // happens until after the stack check passes, so if the stack
                // check fails (hence we're running in this handler) then the
                // stack is not 16-byte aligned due to the previous return
                // address pushed by `call`. In this scenario we just blow away
                // the stack frame by overwriting %rip. This technically loses
                // the precise frame that was interrupted, but that's probably
                // not the end of the world anyway.
                if state.__rsp % 16 == 0 {
                    state.__rsp -= 8;
                    *(state.__rsp as *mut u64) = state.__rip;
                }
                state.__rip = unwind as u64;
                state.__rdi = pc as u64;
                state.__rsi = fp as u64;
                state.__rdx = fault1 as u64;
                state.__rcx = fault2 as u64;
            };
            let mut thread_state = ThreadState::new();
        } else if #[cfg(target_arch = "aarch64")] {
            type ThreadState = arm_thread_state64_t;

            let thread_state_flavor = ARM_THREAD_STATE64;

            let get_pc_and_fp = |state: &ThreadState| (
                state.__pc as *const u8,
                state.__fp as usize,
            );

            let resume = |state: &mut ThreadState, pc: usize, fp: usize, fault1: usize, fault2: usize| {
                // Clobber LR with the faulting PC, so unwinding resumes at the
                // faulting instruction. The previous value of LR has been saved
                // by the callee (in Cranelift generated code), so no need to
                // stash it.
                state.__lr = pc as u64;

                // Fill in the argument to unwind here, and set PC to it, so
                // it looks like a call to unwind.
                state.__x[0] = pc as u64;
                state.__x[1] = fp as u64;
                state.__x[2] = fault1 as u64;
                state.__x[3] = fault2 as u64;
                state.__pc = unwind as u64;
            };
            let mut thread_state = mem::zeroed::<ThreadState>();
        } else {
            compile_error!("unsupported target architecture");
        }
    }

    // First up read our origin thread's state into the area defined above.
    let origin_thread = request.body.thread.name;
    let mut thread_state_count = ThreadState::count();
    let kret = thread_get_state(
        origin_thread,
        thread_state_flavor,
        &mut thread_state as *mut ThreadState as *mut u32,
        &mut thread_state_count,
    );
    if kret != KERN_SUCCESS {
        return false;
    }

    // Use our global map to determine if this program counter is indeed a wasm
    // trap, loading the `jmp_buf` to unwind to if it is.
    //
    // Note that this is where things are pretty tricky. We're accessing
    // non-`Send` state (`CallThreadState`) from the exception handling thread.
    // While typically invalid we are guaranteed that the original thread is
    // stopped while we're accessing it here so this should be safe.
    //
    // Note also that we access the `state` outside the lock of `MAP`. This
    // again is safe because if `state` is `Some` then we're guaranteed the
    // thread is stopped and won't be removing or invalidating its state.
    // Finally our indirection with a pointer means that we can read the
    // pointer value and if `MAP` changes happen after we read our entry that's
    // ok since they won't invalidate our entry.
    let (pc, fp) = get_pc_and_fp(&thread_state);
    if !super::IS_WASM_PC(pc as usize) {
        return false;
    }

    // We have determined that this is a wasm trap and we need to actually
    // force the thread itself to trap. The thread's register state is
    // configured to resume in the `unwind` function below, we update the
    // thread's register state, and then we're off to the races.
    resume(&mut thread_state, pc as usize, fp, fault1, fault2);
    let kret = thread_set_state(
        origin_thread,
        thread_state_flavor,
        &mut thread_state as *mut ThreadState as *mut u32,
        thread_state_count,
    );
    kret == KERN_SUCCESS
}

/// This is a "landing pad" which is never called directly but is directly
/// resumed into from wasm-trapped threads.
///
/// This is a small shim which primarily serves the purpose of simply capturing
/// a native backtrace once we've switched back to the thread itself. After
/// the backtrace is captured we can do the usual `longjmp` back to the source
/// of the wasm code.
unsafe extern "C" fn unwind(
    wasm_pc: *const u8,
    wasm_fp: usize,
    has_faulting_addr: usize,
    faulting_addr: usize,
) -> ! {
    let jmp_buf = tls::with(|state| {
        let state = state.unwrap();
        let faulting_addr = if has_faulting_addr != 0 {
            Some(faulting_addr)
        } else {
            None
        };
        state.set_jit_trap(wasm_pc, wasm_fp, faulting_addr);
        state.jmp_buf.get()
    });
    debug_assert!(!jmp_buf.is_null());
    wasmtime_longjmp(jmp_buf);
}

/// Exceptions on macOS can be delivered to either thread-level or task-level
/// exception ports. In wasmtime we choose to send the exceptions to
/// thread-level ports. This means that we need to, for each thread that can
/// generate an exception, register our thread's exception port as
/// `WASMTIME_PORT` above.
///
/// Note that this choice is done because at the current time if we were to
/// implement a task-level (process-wide) port we'd have to figure out how to
/// forward exceptions that we're not interested to the previously registered
/// port. At this time the author isn't sure how to even do that. SpiderMonkey
/// calls this forwarding "dark magic" as well, and since SpiderMonkey chooses
/// thread-level ports then I hope that's good enough for wasmtime.
///
/// Also note that this choice of thread-level ports should be fine in that
/// unhandled thread-level exceptions get automatically forwarded to the
/// task-level port which is where we'd expected things like breakpad/crashpad
/// exception handlers to get registered.
#[cold]
pub fn lazy_per_thread_init() {
    unsafe {
        assert!(WASMTIME_PORT != MACH_PORT_NULL);
        let this_thread = mach_thread_self();
        let kret = thread_set_exception_ports(
            this_thread,
            EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC,
            WASMTIME_PORT,
            EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES,
            mach_addons::THREAD_STATE_NONE,
        );
        mach_port_deallocate(mach_task_self(), this_thread);
        assert_eq!(kret, KERN_SUCCESS, "failed to set thread exception port");
    }
}
