Revision 6113e1c338b23c16424b8f077b8f3b31f828067b authored by TEC on 29 April 2023, 09:53:44 UTC, committed by TEC on 20 October 2023, 18:14:11 UTC
These new types allow for arbitrary properties to be attached to regions
of an AbstractString or AbstractChar.

The most common expected use of this is for styled content, where the
styling is attached as special properties. This has the major benefit of
separating styling from content, allowing both to be treated better —
functions that operate on the content won't need variants that work
around styling, and operations that interact with the styling will have
many less edge cases (e.g. printing a substring and having to work
around unterminated ANSI styling codes).

Other use cases are also enabled by this, such as text links and the
preserving of line information in string processing.
1 parent 7bf226b
Raw File
signals-mach.c
// This file is a part of Julia. License is MIT: https://julialang.org/license

// Note that this file is `#include`d by "signals-unix.c"

#include <mach/clock.h>
#include <mach/clock_types.h>
#include <mach/clock_reply.h>
#include <mach/mach_traps.h>
#include <mach/task.h>
#include <mach/mig_errors.h>
#include <AvailabilityMacros.h>
#include "mach_excServer.c"

#ifdef MAC_OS_X_VERSION_10_9
#include <sys/_types/_ucontext64.h>
#else
#define __need_ucontext64_t
#include <sys/_structs.h>
#endif

#include "julia_assert.h"

// private keymgr stuff
#define KEYMGR_GCC3_DW2_OBJ_LIST 302
enum {
  NM_ALLOW_RECURSION = 1,
  NM_RECURSION_ILLEGAL = 2
};
extern void _keymgr_set_and_unlock_processwide_ptr(unsigned int key, void *ptr);
extern int _keymgr_unlock_processwide_ptr(unsigned int key);
extern void *_keymgr_get_and_lock_processwide_ptr(unsigned int key);
extern int _keymgr_get_and_lock_processwide_ptr_2(unsigned int key, void **result);
extern int _keymgr_set_lockmode_processwide_ptr(unsigned int key, unsigned int mode);

// private dyld3/dyld4 stuff
extern void _dyld_atfork_prepare(void) __attribute__((weak_import));
extern void _dyld_atfork_parent(void) __attribute__((weak_import));
//extern void _dyld_fork_child(void) __attribute__((weak_import));
extern void _dyld_dlopen_atfork_prepare(void) __attribute__((weak_import));
extern void _dyld_dlopen_atfork_parent(void) __attribute__((weak_import));
//extern void _dyld_dlopen_atfork_child(void) __attribute__((weak_import));

static void attach_exception_port(thread_port_t thread, int segv_only);

// low 16 bits are the thread id, the next 8 bits are the original gc_state
static arraylist_t suspended_threads;
extern uv_mutex_t safepoint_lock;

// see jl_safepoint_wait_thread_resume
void jl_safepoint_resume_thread_mach(jl_ptls_t ptls2, int16_t tid2)
{
    // must be called with uv_mutex_lock(&safepoint_lock) and uv_mutex_lock(&ptls2->sleep_lock) held (in that order)
    for (size_t i = 0; i < suspended_threads.len; i++) {
        uintptr_t item = (uintptr_t)suspended_threads.items[i];
        int16_t tid = (int16_t)item;
        int8_t gc_state = (int8_t)(item >> 8);
        if (tid != tid2)
            continue;
        jl_atomic_store_release(&ptls2->gc_state, gc_state);
        thread_resume(pthread_mach_thread_np(ptls2->system_id));
        suspended_threads.items[i] = suspended_threads.items[--suspended_threads.len];
        break;
    }
    // thread hadn't actually reached a jl_mach_gc_wait call where we suspended it
}

void jl_mach_gc_end(void)
{
    // must be called with uv_mutex_lock(&safepoint_lock) held
    size_t j = 0;
    for (size_t i = 0; i < suspended_threads.len; i++) {
        uintptr_t item = (uintptr_t)suspended_threads.items[i];
        int16_t tid = (int16_t)item;
        int8_t gc_state = (int8_t)(item >> 8);
        jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid];
        uv_mutex_lock(&ptls2->sleep_lock);
        if (jl_atomic_load_relaxed(&ptls2->suspend_count) == 0) {
            jl_atomic_store_release(&ptls2->gc_state, gc_state);
            thread_resume(pthread_mach_thread_np(ptls2->system_id));
        }
        else {
            // this is the check for jl_safepoint_wait_thread_resume
            suspended_threads.items[j++] = (void*)item;
        }
        uv_mutex_unlock(&ptls2->sleep_lock);
    }
    suspended_threads.len = j;
}

// implement jl_set_gc_and_wait from a different thread
static void jl_mach_gc_wait(jl_ptls_t ptls2, mach_port_t thread, int16_t tid)
{
    // relaxed, since we don't mind missing one--we will hit another soon (immediately probably)
    uv_mutex_lock(&safepoint_lock);
    // Since this gets set to zero only while the safepoint_lock was held this
    // means we can tell for sure if GC is done before we got the message or
    // the safepoint was enabled for SIGINT instead.
    int doing_gc = jl_atomic_load_relaxed(&jl_gc_running);
    int do_suspend = doing_gc;
    int relaxed_suspend_count = !doing_gc && jl_atomic_load_relaxed(&ptls2->suspend_count) != 0;
    if (relaxed_suspend_count) {
        uv_mutex_lock(&ptls2->sleep_lock);
        do_suspend = jl_atomic_load_relaxed(&ptls2->suspend_count) != 0;
        // only do_suspend while holding the sleep_lock, otherwise we might miss a resume
    }
    if (do_suspend) {
        // Set the gc state of the thread, suspend and record it
        //
        // TODO: TSAN will complain that it never saw the faulting task do an
        // atomic release (it was in the kernel). And our attempt here does
        // nothing, since we are a different thread, and it is not transitive).
        //
        // This also means we are not making this thread available for GC work.
        // Eventually, we should probably release this signal to the original
        // thread, (return KERN_FAILURE instead of KERN_SUCCESS) so that it
        // triggers a SIGSEGV and gets handled by the usual codepath for unix.
        int8_t gc_state = ptls2->gc_state;
        jl_atomic_store_release(&ptls2->gc_state, JL_GC_STATE_WAITING);
        uintptr_t item = tid | (((uintptr_t)gc_state) << 16);
        arraylist_push(&suspended_threads, (void*)item);
        thread_suspend(thread);
    }
    if (relaxed_suspend_count)
        uv_mutex_unlock(&ptls2->sleep_lock);
    uv_mutex_unlock(&safepoint_lock);
}

static mach_port_t segv_port = 0;

#define STR(x) #x
#define XSTR(x) STR(x)
#define HANDLE_MACH_ERROR(msg, retval) \
    if (retval != KERN_SUCCESS) { mach_error(msg XSTR(: __FILE__:__LINE__:), (retval)); abort(); }

void *mach_segv_listener(void *arg)
{
    (void)arg;
    int ret = mach_msg_server(mach_exc_server, 2048, segv_port, MACH_MSG_TIMEOUT_NONE);
    mach_error("mach_msg_server" XSTR(: __FILE__:__LINE__:), ret);
    abort();
}


static void allocate_mach_handler(void)
{
    // ensure KEYMGR_GCC3_DW2_OBJ_LIST is initialized, as this requires malloc
    // and thus can deadlock when used without first initializing it.
    // Apple caused this problem in their libunwind in 10.9 (circa keymgr-28)
    // when they removed this part of the code from keymgr.
    // Much thanks to Apple for providing source code, or this would probably
    // have simply remained unsolved forever on their platform.
    // This is similar to just calling checkKeyMgrRegisteredFDEs
    // (this is quite thread-unsafe)
    if (_keymgr_set_lockmode_processwide_ptr(KEYMGR_GCC3_DW2_OBJ_LIST, NM_ALLOW_RECURSION))
        jl_error("_keymgr_set_lockmode_processwide_ptr failed");

    int16_t nthreads = jl_atomic_load_acquire(&jl_n_threads);
    arraylist_new(&suspended_threads, nthreads); // we will resize later (inside safepoint_lock), if needed
    pthread_t thread;
    pthread_attr_t attr;
    kern_return_t ret;
    mach_port_t self = mach_task_self();
    ret = mach_port_allocate(self, MACH_PORT_RIGHT_RECEIVE, &segv_port);
    HANDLE_MACH_ERROR("mach_port_allocate",ret);
    ret = mach_port_insert_right(self, segv_port, segv_port, MACH_MSG_TYPE_MAKE_SEND);
    HANDLE_MACH_ERROR("mach_port_insert_right",ret);
    // Alright, create a thread to serve as the listener for exceptions
    if (pthread_attr_init(&attr) != 0) {
        jl_error("pthread_attr_init failed");
    }
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (pthread_create(&thread, &attr, mach_segv_listener, NULL) != 0) {
        jl_error("pthread_create failed");
    }
    pthread_attr_destroy(&attr);
}

#ifdef LLVMLIBUNWIND
volatile mach_port_t mach_profiler_thread = 0;
static kern_return_t profiler_segv_handler(
    mach_port_t exception_port,
    mach_port_t thread,
    mach_port_t task,
    exception_type_t exception,
    mach_exception_data_t code,
    mach_msg_type_number_t codeCnt);
#endif

#if defined(_CPU_X86_64_)
typedef x86_thread_state64_t host_thread_state_t;
typedef x86_exception_state64_t host_exception_state_t;
#define MACH_THREAD_STATE x86_THREAD_STATE64
#define MACH_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT
#define HOST_EXCEPTION_STATE x86_EXCEPTION_STATE64
#define HOST_EXCEPTION_STATE_COUNT x86_EXCEPTION_STATE64_COUNT

#elif defined(_CPU_AARCH64_)
typedef arm_thread_state64_t host_thread_state_t;
typedef arm_exception_state64_t host_exception_state_t;
#define MACH_THREAD_STATE ARM_THREAD_STATE64
#define MACH_THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT
#define HOST_EXCEPTION_STATE ARM_EXCEPTION_STATE64
#define HOST_EXCEPTION_STATE_COUNT ARM_EXCEPTION_STATE64_COUNT
#endif

static void jl_call_in_state(jl_ptls_t ptls2, host_thread_state_t *state,
                             void (*fptr)(void))
{
#ifdef _CPU_X86_64_
    uintptr_t rsp = state->__rsp;
#elif defined(_CPU_AARCH64_)
    uintptr_t rsp = state->__sp;
#else
#error "julia: throw-in-context not supported on this platform"
#endif
    if (ptls2 == NULL || ptls2->signal_stack == NULL || is_addr_on_sigstack(ptls2, (void*)rsp)) {
        rsp = (rsp - 256) & ~(uintptr_t)15; // redzone and re-alignment
    }
    else {
        rsp = (uintptr_t)ptls2->signal_stack + sig_stack_size;
    }
    assert(rsp % 16 == 0);

#ifdef _CPU_X86_64_
    rsp -= sizeof(void*);
    state->__rsp = rsp; // set stack pointer
    state->__rip = (uint64_t)fptr; // "call" the function
#elif defined(_CPU_AARCH64_)
    state->__sp = rsp;
    state->__pc = (uint64_t)fptr;
    state->__lr = 0;
#else
#error "julia: throw-in-context not supported on this platform"
#endif
}

#ifdef _CPU_X86_64_
int is_write_fault(host_exception_state_t exc_state) {
    return exc_reg_is_write_fault(exc_state.__err);
}
#elif defined(_CPU_AARCH64_)
int is_write_fault(host_exception_state_t exc_state) {
    return exc_reg_is_write_fault(exc_state.__esr);
}
#else
#warning Implement this query for consistent PROT_NONE handling
int is_write_fault(host_exception_state_t exc_state) {
    return 0;
}
#endif

static void jl_throw_in_thread(jl_ptls_t ptls2, mach_port_t thread, jl_value_t *exception)
{
    unsigned int count = MACH_THREAD_STATE_COUNT;
    host_thread_state_t state;
    kern_return_t ret = thread_get_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, &count);
    HANDLE_MACH_ERROR("thread_get_state", ret);
    if (1) { // XXX: !jl_has_safe_restore(ptls2)
        assert(exception);
        ptls2->bt_size =
            rec_backtrace_ctx(ptls2->bt_data, JL_MAX_BT_SIZE, (bt_context_t *)&state,
                              NULL /*current_task?*/);
        ptls2->sig_exception = exception;
    }
    jl_call_in_state(ptls2, &state, &jl_sig_throw);
    ret = thread_set_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, count);
    HANDLE_MACH_ERROR("thread_set_state", ret);
}

static void segv_handler(int sig, siginfo_t *info, void *context)
{
    assert(sig == SIGSEGV || sig == SIGBUS);
    if (jl_get_safe_restore()) { // restarting jl_ or jl_unwind_stepn
        jl_task_t *ct = jl_get_current_task();
        jl_ptls_t ptls = ct == NULL ? NULL : ct->ptls;
        jl_call_in_state(ptls, (host_thread_state_t*)jl_to_bt_context(context), &jl_sig_throw);
    }
    else {
        sigdie_handler(sig, info, context);
    }
}

// n.b. mach_exc_server expects us to define this symbol locally
/* The documentation for catch_exception_raise says: A return value of
 * KERN_SUCCESS indicates that the thread is to continue from the point of
 * exception. A return value of MIG_NO_REPLY indicates that the exception was
 * handled directly and the thread was restarted or terminated by the exception
 * handler. A return value of MIG_DESTROY_REQUEST causes the kernel to try
 * another exception handler (or terminate the thread). Any other value will
 * cause mach_msg_server to remove the task and thread port references.
 *
 * However MIG_DESTROY_REQUEST does not exist, not does it appear the source
 * code for mach_msg_server ever destroy those references (only the message
 * itself).
 */
kern_return_t catch_mach_exception_raise(
    mach_port_t exception_port,
    mach_port_t thread,
    mach_port_t task,
    exception_type_t exception,
    mach_exception_data_t code,
    mach_msg_type_number_t codeCnt)
{
    unsigned int exc_count = HOST_EXCEPTION_STATE_COUNT;
    host_exception_state_t exc_state;
#ifdef LLVMLIBUNWIND
    if (thread == mach_profiler_thread) {
        return profiler_segv_handler(exception_port, thread, task, exception, code, codeCnt);
    }
#endif
    int16_t tid;
    jl_ptls_t ptls2 = NULL;
    int nthreads = jl_atomic_load_acquire(&jl_n_threads);
    for (tid = 0; tid < nthreads; tid++) {
        jl_ptls_t _ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid];
        if (jl_atomic_load_relaxed(&_ptls2->current_task) == NULL) {
            // this thread is dead
            continue;
        }
        if (pthread_mach_thread_np(_ptls2->system_id) == thread) {
            ptls2 = _ptls2;
            break;
        }
    }
    if (!ptls2) {
        // We don't know about this thread, let the kernel try another handler
        // instead. This shouldn't actually happen since we only register the
        // handler for the threads we know about.
        jl_safe_printf("ERROR: Exception handler triggered on unmanaged thread.\n");
        return KERN_INVALID_ARGUMENT;
    }
    // XXX: jl_throw_in_thread or segv_handler will eventually check this, but
    //      we would like to avoid some of this work if we could detect this earlier
    // if (jl_has_safe_restore(ptls2)) {
    //     jl_throw_in_thread(ptls2, thread, jl_stackovf_exception);
    //     return KERN_SUCCESS;
    // }
    if (ptls2->gc_state == JL_GC_STATE_WAITING)
        return KERN_FAILURE;
    if (exception == EXC_ARITHMETIC) {
        jl_throw_in_thread(ptls2, thread, jl_diverror_exception);
        return KERN_SUCCESS;
    }
    assert(exception == EXC_BAD_ACCESS); // SIGSEGV or SIGBUS
    if (codeCnt < 2 || code[0] != KERN_PROTECTION_FAILURE) // SEGV_ACCERR or BUS_ADRERR or BUS_ADRALN
        return KERN_FAILURE;
    uint64_t fault_addr = code[1];
    kern_return_t ret = thread_get_state(thread, HOST_EXCEPTION_STATE, (thread_state_t)&exc_state, &exc_count);
    HANDLE_MACH_ERROR("thread_get_state", ret);
    if (jl_addr_is_safepoint(fault_addr) && !is_write_fault(exc_state)) {
        jl_mach_gc_wait(ptls2, thread, tid);
        if (ptls2->tid != 0)
            return KERN_SUCCESS;
        if (ptls2->defer_signal) {
            jl_safepoint_defer_sigint();
        }
        else if (jl_safepoint_consume_sigint()) {
            jl_clear_force_sigint();
            jl_throw_in_thread(ptls2, thread, jl_interrupt_exception);
        }
        return KERN_SUCCESS;
    }
    if (ptls2->current_task->eh == NULL)
        return KERN_FAILURE;
    jl_value_t *excpt;
    if (is_addr_on_stack(jl_atomic_load_relaxed(&ptls2->current_task), (void*)fault_addr)) {
        excpt = jl_stackovf_exception;
    }
    else if (is_write_fault(exc_state)) // false for alignment errors
        excpt = jl_readonlymemory_exception;
    else
        return KERN_FAILURE;
    jl_throw_in_thread(ptls2, thread, excpt);
    return KERN_SUCCESS;
}

//mach_exc_server expects us to define this symbol locally
kern_return_t catch_mach_exception_raise_state(
    mach_port_t exception_port,
    exception_type_t exception,
    const mach_exception_data_t code,
    mach_msg_type_number_t codeCnt,
    int *flavor,
    const thread_state_t old_state,
    mach_msg_type_number_t old_stateCnt,
    thread_state_t new_state,
    mach_msg_type_number_t *new_stateCnt)
{
    return KERN_INVALID_ARGUMENT; // we only use EXCEPTION_DEFAULT
}

//mach_exc_server expects us to define this symbol locally
kern_return_t catch_mach_exception_raise_state_identity(
    mach_port_t exception_port,
    mach_port_t thread,
    mach_port_t task,
    exception_type_t exception,
    mach_exception_data_t code,
    mach_msg_type_number_t codeCnt,
    int *flavor,
    thread_state_t old_state,
    mach_msg_type_number_t old_stateCnt,
    thread_state_t new_state,
    mach_msg_type_number_t *new_stateCnt)
{
    return KERN_INVALID_ARGUMENT; // we only use EXCEPTION_DEFAULT
}

static void attach_exception_port(thread_port_t thread, int segv_only)
{
    kern_return_t ret;
    // http://www.opensource.apple.com/source/xnu/xnu-2782.1.97/osfmk/man/thread_set_exception_ports.html
    exception_mask_t mask = EXC_MASK_BAD_ACCESS;
    if (!segv_only)
        mask |= EXC_MASK_ARITHMETIC;
    ret = thread_set_exception_ports(thread, mask, segv_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, MACH_THREAD_STATE);
    HANDLE_MACH_ERROR("thread_set_exception_ports", ret);
}

static int jl_thread_suspend_and_get_state2(int tid, host_thread_state_t *ctx) JL_NOTSAFEPOINT
{
    jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid];
    if (ptls2 == NULL) // this thread is not alive
        return 0;
    jl_task_t *ct2 = jl_atomic_load_relaxed(&ptls2->current_task);
    if (ct2 == NULL) // this thread is already dead
        return 0;

    mach_port_t thread = pthread_mach_thread_np(ptls2->system_id);

    kern_return_t ret = thread_suspend(thread);
    HANDLE_MACH_ERROR("thread_suspend", ret);

    // Do the actual sampling
    unsigned int count = MACH_THREAD_STATE_COUNT;
    memset(ctx, 0, sizeof(*ctx));

    // Get the state of the suspended thread
    ret = thread_get_state(thread, MACH_THREAD_STATE, (thread_state_t)ctx, &count);
    return 1;
}

int jl_thread_suspend_and_get_state(int tid, int timeout, bt_context_t *ctx)
{
    (void)timeout;
    host_thread_state_t state;
    if (!jl_thread_suspend_and_get_state2(tid, &state)) {
        return 0;
    }
    *ctx = *(unw_context_t*)&state;
    return 1;
}

void jl_thread_resume(int tid)
{
    jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid];
    mach_port_t thread = pthread_mach_thread_np(ptls2->system_id);
    kern_return_t ret = thread_resume(thread);
    HANDLE_MACH_ERROR("thread_resume", ret);
}

// Throw jl_interrupt_exception if the master thread is in a signal async region
// or if SIGINT happens too often.
static void jl_try_deliver_sigint(void)
{
    jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[0];
    mach_port_t thread = pthread_mach_thread_np(ptls2->system_id);

    kern_return_t ret = thread_suspend(thread);
    HANDLE_MACH_ERROR("thread_suspend", ret);

    // This aborts `sleep` and other syscalls.
    ret = thread_abort(thread);
    HANDLE_MACH_ERROR("thread_abort", ret);

    jl_safepoint_enable_sigint();
    int force = jl_check_force_sigint();
    if (force || (!ptls2->defer_signal && ptls2->io_wait)) {
        jl_safepoint_consume_sigint();
        if (force)
            jl_safe_printf("WARNING: Force throwing a SIGINT\n");
        jl_clear_force_sigint();
        jl_throw_in_thread(ptls2, thread, jl_interrupt_exception);
    }
    else {
        jl_wake_libuv();
    }

    ret = thread_resume(thread);
    HANDLE_MACH_ERROR("thread_resume", ret);
}

static void JL_NORETURN jl_exit_thread0_cb(int signo)
{
CFI_NORETURN
    jl_critical_error(signo, 0, NULL, jl_current_task);
    jl_atexit_hook(128);
    jl_raise(signo);
}

static void jl_exit_thread0(int signo, jl_bt_element_t *bt_data, size_t bt_size)
{
    jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[0];
    mach_port_t thread = pthread_mach_thread_np(ptls2->system_id);

    host_thread_state_t state;
    if (!jl_thread_suspend_and_get_state2(0, &state)) {
        // thread 0 is gone? just do the signal ourself
        jl_raise(signo);
    }

    // This aborts `sleep` and other syscalls.
    kern_return_t ret = thread_abort(thread);
    HANDLE_MACH_ERROR("thread_abort", ret);

    ptls2->bt_size = bt_size; // <= JL_MAX_BT_SIZE
    memcpy(ptls2->bt_data, bt_data, ptls2->bt_size * sizeof(bt_data[0]));

#ifdef _CPU_X86_64_
    // First integer argument. Not portable but good enough =)
    state.__rdi = signo;
#elif defined(_CPU_AARCH64_)
    state.__x[0] = signo;
#else
#error Fill in first integer argument here
#endif
    jl_call_in_state(ptls2, &state, (void (*)(void))&jl_exit_thread0_cb);
    unsigned int count = MACH_THREAD_STATE_COUNT;
    ret = thread_set_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, count);
    HANDLE_MACH_ERROR("thread_set_state", ret);

    ret = thread_resume(thread);
    HANDLE_MACH_ERROR("thread_resume", ret);
}

static int profile_started = 0;
mach_timespec_t timerprof;
static pthread_t profiler_thread;
clock_serv_t clk;
static mach_port_t profile_port = 0;

#ifdef LLVMLIBUNWIND
volatile static int forceDwarf = -2;
static unw_context_t profiler_uc;

static kern_return_t profiler_segv_handler(
    mach_port_t exception_port,
    mach_port_t thread,
    mach_port_t task,
    exception_type_t exception,
    mach_exception_data_t code,
    mach_msg_type_number_t codeCnt)
{
    assert(thread == mach_profiler_thread);
    host_thread_state_t state;

    // Not currently unwinding. Raise regular segfault
    if (forceDwarf == -2)
        return KERN_FAILURE;

    if (forceDwarf == 0)
        forceDwarf = 1;
    else
        forceDwarf = -1;

    unsigned int count = MACH_THREAD_STATE_COUNT;

    thread_get_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, &count);

#ifdef _CPU_X86_64_
    // don't change cs fs gs rflags
    uint64_t cs = state.__cs;
    uint64_t fs = state.__fs;
    uint64_t gs = state.__gs;
    uint64_t rflags = state.__rflags;
#elif defined(_CPU_AARCH64_)
    uint64_t cpsr = state.__cpsr;
#else
#error Unknown CPU
#endif

    memcpy(&state, &profiler_uc, sizeof(state));

#ifdef _CPU_X86_64_
    state.__cs = cs;
    state.__fs = fs;
    state.__gs = gs;
    state.__rflags = rflags;
#else
    state.__cpsr = cpsr;
#endif

    kern_return_t ret = thread_set_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, count);
    HANDLE_MACH_ERROR("thread_set_state", ret);

    return KERN_SUCCESS;
}
#endif

// WARNING: we are unable to handle sigsegv while the dlsymlock is held
static int jl_lock_profile_mach(int dlsymlock)
{
    jl_lock_profile();
    // workaround for old keymgr bugs
    void *unused = NULL;
    int keymgr_locked = _keymgr_get_and_lock_processwide_ptr_2(KEYMGR_GCC3_DW2_OBJ_LIST, &unused) == 0;
    // workaround for new dlsym4 bugs in the workaround for dlsym bugs: _dyld_atfork_prepare
    // acquires its locks in the wrong order, but fortunately we happen to able to guard it
    // with this call to force it to prevent that TSAN violation from causing a deadlock
    if (dlsymlock && _dyld_dlopen_atfork_prepare != NULL && _dyld_dlopen_atfork_parent != NULL)
        _dyld_dlopen_atfork_prepare();
    // workaround for new dlsym4 bugs (API and bugs introduced circa macOS 12.1)
    if (dlsymlock && _dyld_atfork_prepare != NULL && _dyld_atfork_parent != NULL)
        _dyld_atfork_prepare();
    return keymgr_locked;
}

static void jl_unlock_profile_mach(int dlsymlock, int keymgr_locked)
{
    if (dlsymlock && _dyld_atfork_prepare != NULL && _dyld_atfork_parent != NULL)
        _dyld_atfork_parent();
    if (dlsymlock && _dyld_dlopen_atfork_prepare != NULL && _dyld_dlopen_atfork_parent != NULL)
        _dyld_dlopen_atfork_parent();
    if (keymgr_locked)
        _keymgr_unlock_processwide_ptr(KEYMGR_GCC3_DW2_OBJ_LIST);
    jl_unlock_profile();
}

int jl_lock_stackwalk(void)
{
    return jl_lock_profile_mach(1);
}

void jl_unlock_stackwalk(int lockret)
{
    jl_unlock_profile_mach(1, lockret);
}

void *mach_profile_listener(void *arg)
{
    (void)arg;
    const int max_size = 512;
    attach_exception_port(mach_thread_self(), 1);
#ifdef LLVMLIBUNWIND
    mach_profiler_thread = mach_thread_self();
#endif
    mig_reply_error_t *bufRequest = (mig_reply_error_t*)malloc_s(max_size);
    while (1) {
        kern_return_t ret = mach_msg(&bufRequest->Head, MACH_RCV_MSG,
                                     0, max_size, profile_port,
                                     MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
        HANDLE_MACH_ERROR("mach_msg", ret);
        // sample each thread, round-robin style in reverse order
        // (so that thread zero gets notified last)
        int keymgr_locked = jl_lock_profile_mach(0);

        int nthreads = jl_atomic_load_acquire(&jl_n_threads);
        int *randperm = profile_get_randperm(nthreads);
        for (int idx = nthreads; idx-- > 0; ) {
            // Stop the threads in the random or reverse round-robin order.
            int i = randperm[idx];
            // if there is no space left, break early
            if (jl_profile_is_buffer_full()) {
                jl_profile_stop_timer();
                break;
            }

            if (_dyld_dlopen_atfork_prepare != NULL && _dyld_dlopen_atfork_parent != NULL)
                _dyld_dlopen_atfork_prepare();
            if (_dyld_atfork_prepare != NULL && _dyld_atfork_parent != NULL)
                _dyld_atfork_prepare(); // briefly acquire the dlsym lock
            host_thread_state_t state;
            int valid_thread = jl_thread_suspend_and_get_state2(i, &state);
            unw_context_t *uc = (unw_context_t*)&state;
            if (_dyld_atfork_prepare != NULL && _dyld_atfork_parent != NULL)
                _dyld_atfork_parent(); // quickly release the dlsym lock
            if (_dyld_dlopen_atfork_prepare != NULL && _dyld_dlopen_atfork_parent != NULL)
                _dyld_dlopen_atfork_parent();
            if (!valid_thread)
                continue;
            if (running) {
#ifdef LLVMLIBUNWIND
                /*
                 *  Unfortunately compact unwind info is incorrectly generated for quite a number of
                 *  libraries by quite a large number of compilers. We can fall back to DWARF unwind info
                 *  in some cases, but in quite a number of cases (especially libraries not compiled in debug
                 *  mode, only the compact unwind info may be available). Even more unfortunately, there is no
                 *  way to detect such bogus compact unwind info (other than noticing the resulting segfault).
                 *  What we do here is ugly, but necessary until the compact unwind info situation improves.
                 *  We try to use the compact unwind info and if that results in a segfault, we retry with DWARF info.
                 *  Note that in a small number of cases this may result in bogus stack traces, but at least the topmost
                 *  entry will always be correct, and the number of cases in which this is an issue is rather small.
                 *  Other than that, this implementation is not incorrect as the other thread is paused while we are profiling
                 *  and during stack unwinding we only ever read memory, but never write it.
                 */

                forceDwarf = 0;
                unw_getcontext(&profiler_uc); // will resume from this point if the next lines segfault at any point

                if (forceDwarf == 0) {
                    // Save the backtrace
                    bt_size_cur += rec_backtrace_ctx((jl_bt_element_t*)bt_data_prof + bt_size_cur, bt_size_max - bt_size_cur - 1, uc, NULL);
                }
                else if (forceDwarf == 1) {
                    bt_size_cur += rec_backtrace_ctx_dwarf((jl_bt_element_t*)bt_data_prof + bt_size_cur, bt_size_max - bt_size_cur - 1, uc, NULL);
                }
                else if (forceDwarf == -1) {
                    jl_safe_printf("WARNING: profiler attempt to access an invalid memory location\n");
                }

                forceDwarf = -2;
#else
                bt_size_cur += rec_backtrace_ctx((jl_bt_element_t*)bt_data_prof + bt_size_cur, bt_size_max - bt_size_cur - 1, uc, NULL);
#endif
                jl_ptls_t ptls = jl_atomic_load_relaxed(&jl_all_tls_states)[i];

                // store threadid but add 1 as 0 is preserved to indicate end of block
                bt_data_prof[bt_size_cur++].uintptr = ptls->tid + 1;

                // store task id (never null)
                bt_data_prof[bt_size_cur++].jlvalue = (jl_value_t*)jl_atomic_load_relaxed(&ptls->current_task);

                // store cpu cycle clock
                bt_data_prof[bt_size_cur++].uintptr = cycleclock();

                // store whether thread is sleeping but add 1 as 0 is preserved to indicate end of block
                bt_data_prof[bt_size_cur++].uintptr = jl_atomic_load_relaxed(&ptls->sleep_check_state) + 1;

                // Mark the end of this block with two 0's
                bt_data_prof[bt_size_cur++].uintptr = 0;
                bt_data_prof[bt_size_cur++].uintptr = 0;
            }
            // We're done! Resume the thread.
            jl_thread_resume(i);
        }
        jl_unlock_profile_mach(0, keymgr_locked);
        if (running) {
            jl_check_profile_autostop();
            // Reset the alarm
            kern_return_t ret = clock_alarm(clk, TIME_RELATIVE, timerprof, profile_port);
            HANDLE_MACH_ERROR("clock_alarm", ret)
        }
    }
}

JL_DLLEXPORT int jl_profile_start_timer(void)
{
    kern_return_t ret;
    if (!profile_started) {
        mach_port_t self = mach_task_self();

        ret = host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, (clock_serv_t *)&clk);
        HANDLE_MACH_ERROR("host_get_clock_service", ret);

        ret = mach_port_allocate(self, MACH_PORT_RIGHT_RECEIVE, &profile_port);
        HANDLE_MACH_ERROR("mach_port_allocate", ret);

        // Alright, create a thread to serve as the listener for exceptions
        pthread_attr_t attr;
        if (pthread_attr_init(&attr) != 0) {
            jl_error("pthread_attr_init failed");
        }
        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
        if (pthread_create(&profiler_thread, &attr, mach_profile_listener, NULL) != 0) {
            jl_error("pthread_create failed");
        }
        pthread_attr_destroy(&attr);

        profile_started = 1;
    }

    timerprof.tv_sec = nsecprof/GIGA;
    timerprof.tv_nsec = nsecprof%GIGA;

    running = 1;
    // ensure the alarm is running
    ret = clock_alarm(clk, TIME_RELATIVE, timerprof, profile_port);
    HANDLE_MACH_ERROR("clock_alarm", ret);

    return 0;
}

JL_DLLEXPORT void jl_profile_stop_timer(void)
{
    running = 0;
}
back to top