//! 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(
    // FFI bindings here for C/etc don't follow Rust's naming conventions.
    non_snake_case,
    // Platform-specific code has a lot of false positives with these lints so
    // like Unix disable the lints for this module.
    clippy::cast_sign_loss,
    clippy::cast_possible_truncation
)]

use crate::runtime::module::lookup_code;
use crate::runtime::vm::sys::traphandlers::wasmtime_longjmp;
use crate::runtime::vm::traphandlers::{tls, TrapRegisters};
use mach2::exc::*;
use mach2::exception_types::*;
use mach2::kern_return::*;
use mach2::mach_init::*;
use mach2::mach_port::*;
use mach2::message::*;
use mach2::ndr::*;
use mach2::port::*;
use mach2::thread_act::*;
use mach2::thread_status::*;
use mach2::traps::*;
use std::io;
use std::mem;
use std::ptr::addr_of_mut;
use std::thread;
use wasmtime_environ::Trap;

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

static mut CHILD_OF_FORKED_PROCESS: bool = false;

pub struct TrapHandler {
    thread: Option<thread::JoinHandle<()>>,
}

static mut PREV_SIGBUS: libc::sigaction = unsafe { mem::zeroed() };

impl TrapHandler {
    pub unsafe fn new() -> TrapHandler {
        // Mach ports do not currently work across forks, so configure Wasmtime to
        // panic in `lazy_per_thread_init` if the child attempts to invoke
        // WebAssembly.
        unsafe extern "C" fn child() {
            CHILD_OF_FORKED_PROCESS = true;
        }
        let rc = libc::pthread_atfork(None, None, Some(child));
        assert_eq!(rc, 0, "failed to configure `pthread_atfork` handler");

        // 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, addr_of_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.
        let thread = thread::spawn(|| handler_thread());

        // Setup a SIGBUS handler which is used for printing that the stack was
        // overflowed when a host overflows its fiber stack.
        unsafe {
            let mut handler: libc::sigaction = mem::zeroed();
            handler.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK;
            handler.sa_sigaction = sigbus_handler as usize;
            libc::sigemptyset(&mut handler.sa_mask);
            if libc::sigaction(libc::SIGBUS, &handler, addr_of_mut!(PREV_SIGBUS)) != 0 {
                panic!(
                    "unable to install signal handler: {}",
                    io::Error::last_os_error(),
                );
            }
        }

        TrapHandler {
            thread: Some(thread),
        }
    }
}

impl Drop for TrapHandler {
    fn drop(&mut self) {
        unsafe {
            let kret = mach_port_destroy(mach_task_self(), WASMTIME_PORT);
            assert_eq!(kret, KERN_SUCCESS, "failed to destroy port");
            self.thread.take().unwrap().join().unwrap();
        }
    }
}

unsafe extern "C" fn sigbus_handler(
    signum: libc::c_int,
    siginfo: *mut libc::siginfo_t,
    context: *mut libc::c_void,
) {
    // If this is a faulting address within the async guard range listed within
    // our tls storage then print a helpful message about it and abort.
    // Otherwise forward to the previous SIGBUS handler, if any.
    tls::with(|info| {
        let info = match info {
            Some(info) => info,
            None => return,
        };
        let faulting_addr = (*siginfo).si_addr() as usize;
        let start = info.async_guard_range.start;
        let end = info.async_guard_range.end;
        if start as usize <= faulting_addr && faulting_addr < end as usize {
            super::signals::abort_stack_overflow();
        }
    });

    super::signals::delegate_signal_to_previous_handler(
        addr_of_mut!(PREV_SIGBUS),
        signum,
        siginfo,
        context,
    )
}

// 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)]
struct __Request__exception_raise_t {
    Head: mach_msg_header_t,
    /* start of the kernel processed data */
    msgh_body: mach_msg_body_t,
    thread: mach_msg_port_descriptor_t,
    task: mach_msg_port_descriptor_t,
    /* end of the kernel processed data */
    NDR: NDR_record_t,
    exception: exception_type_t,
    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.
    code: [i64; 2],
}

// 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 | MACH_RCV_INTERRUPT,
            0,
            mem::size_of_val(&request) as u32,
            WASMTIME_PORT,
            MACH_MSG_TIMEOUT_NONE,
            MACH_PORT_NULL,
        );
        if kret == MACH_RCV_PORT_CHANGED {
            // this port has been closed;
            break;
        }
        if kret != KERN_SUCCESS {
            eprintln!("mach_msg failed with {kret} ({kret:x})");
            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 faulting_addr = if request.body.exception as u32 == EXC_BAD_ACCESS {
        Some(request.body.code[1] as usize)
    } else {
        None
    };

    // 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 mach2::structs::x86_thread_state64_t;
            use mach2::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, trap: Trap| {
                // 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;
                state.__r8 = trap as u64;
            };
            let mut thread_state = ThreadState::new();
        } else if #[cfg(target_arch = "aarch64")] {
            type ThreadState = mach2::structs::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, trap: Trap| {
                // 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.__x[4] = trap 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);
    let Some((code, text_offset)) = lookup_code(pc as usize) else {
        return false;
    };

    let Some(trap) = code.lookup_trap_code(text_offset) else {
        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.
    let (fault1, fault2) = match faulting_addr {
        None => (0, 0),
        Some(addr) => (1, addr),
    };
    resume(&mut thread_state, pc as usize, fp, fault1, fault2, trap);
    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(pc: usize, fp: usize, fault1: usize, fault2: usize, trap: u8) -> ! {
    let jmp_buf = tls::with(|state| {
        let state = state.unwrap();
        let regs = TrapRegisters { pc, fp };
        let faulting_addr = match fault1 {
            0 => None,
            _ => Some(fault2),
        };
        let trap = Trap::from_u8(trap).unwrap();
        state.set_jit_trap(regs, faulting_addr, trap);
        state.take_jmp_buf()
    });
    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!(
            !CHILD_OF_FORKED_PROCESS,
            "cannot use Wasmtime in a forked process when mach ports are \
             configured, see `Config::macos_use_mach_ports`"
        );
        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) as exception_behavior_t,
            THREAD_STATE_NONE,
        );
        mach_port_deallocate(mach_task_self(), this_thread);
        assert_eq!(kret, KERN_SUCCESS, "failed to set thread exception port");
    }
}
