/
githubmirror
/
julia
Обзор
Документация
Войти
/
githubmirror
/
julia
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/signals-mach.c
1 168 строк
48 KB
Keno Fischer
cancellation: Hook up ^C (#62655)
08 авг 2026, 03:45
Не верифицирован
08 авг 2026, 03:45
37ef9ad
Код
Авторство
О чём код?
// 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/thread_state.h> #include <mach/mach_traps.h> #include <mach/task.h> #include <mach/mig_errors.h> #include <AvailabilityMacros.h> #include <stdint.h> #include "mig/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" static struct { thread_state_flavor_t flavor; mach_msg_type_number_t count; // in units of natural_t } jl_mach_float_state_info = {0, 0}; // 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); static mach_port_t segv_port = 0; // Dedicated PROT_NONE page for the kernel-assisted restore trigger. void *jl_mach_restore_page = NULL; // Maximum float state count (in natural_t units) across all supported flavors. #if defined(_CPU_X86_64_) #define JL_MACH_FLOAT_STATE_MAX_COUNT x86_AVX512_STATE64_COUNT #elif defined(_CPU_AARCH64_) #define JL_MACH_FLOAT_STATE_MAX_COUNT ARM_NEON_STATE64_COUNT #endif static inline int jl_addr_is_restore_trigger(uintptr_t addr) { uintptr_t page_addr = (uintptr_t)jl_mach_restore_page; return addr >= page_addr && addr < page_addr + jl_page_size; } #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 init_mach_restore_trigger(void) { // Allocate a PROT_NONE page for the kernel-assisted restore trigger. // Reading from this page causes EXC_BAD_ACCESS, which the exception // handler intercepts to restore full GP state via the kernel. jl_mach_restore_page = mmap(NULL, jl_getpagesize(), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (jl_mach_restore_page == MAP_FAILED) { perror("mmap(jl_mach_restore_page)"); abort(); } // Initialize jl_mach_float_state_info #if defined(_CPU_X86_64_) mach_port_t self = pthread_mach_thread_np(pthread_self()); mach_msg_type_number_t count; kern_return_t ret; count = x86_AVX512_STATE64_COUNT; natural_t buf[x86_AVX512_STATE64_COUNT]; ret = thread_get_state(self, x86_AVX512_STATE64, (thread_state_t)buf, &count); if (ret == KERN_SUCCESS) { jl_mach_float_state_info.flavor = x86_AVX512_STATE64; jl_mach_float_state_info.count = x86_AVX512_STATE64_COUNT; } else { count = x86_AVX_STATE64_COUNT; ret = thread_get_state(self, x86_AVX_STATE64, (thread_state_t)buf, &count); if (ret == KERN_SUCCESS) { jl_mach_float_state_info.flavor = x86_AVX_STATE64; jl_mach_float_state_info.count = x86_AVX_STATE64_COUNT; } else { jl_mach_float_state_info.flavor = x86_FLOAT_STATE64; jl_mach_float_state_info.count = x86_FLOAT_STATE64_COUNT; return; } } #elif defined(_CPU_AARCH64_) jl_mach_float_state_info.flavor = ARM_NEON_STATE64; jl_mach_float_state_info.count = ARM_NEON_STATE64_COUNT; #endif } 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"); // setup fault page and register save / restore info for exiting GC safepoint init_mach_restore_trigger(); 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); } #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 #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, host_thread_state_t *state, mach_msg_type_number_t *new_stateCnt); #endif // Create a fake function that describes the register manipulations in jl_noreturn_call_in_state // The callee-saved registers still may get smashed (by the cdecl fptr), since we didn't explicitly copy all of the // state to the stack (to build a real sigreturn frame). __attribute__((naked, used)) void jl_fake_signal_return(void) { #if defined(_CPU_X86_64_) __asm__( " .cfi_signal_frame\n" " .cfi_def_cfa %rsp, 0\n" // CFA here uses %rsp directly " .cfi_offset %rip, 0\n" // previous value of %rip at CFA " .cfi_offset %rsp, 8\n" // previous value of %rsp at CFA " ud2\n" " ud2\n" ); #elif defined(_CPU_AARCH64_) __asm__( " .cfi_signal_frame\n" " .cfi_def_cfa sp, 0\n" // use sp as fp here // This is not quite valid, since the AArch64 DWARF spec lacks the ability to define how to restore the LR register correctly, // so normally libunwind implementations on linux detect this function specially and hack around the invalid info: // https://github.com/llvm/llvm-project/commit/c82deed6764cbc63966374baf9721331901ca958 " .cfi_offset lr, 0\n" " .cfi_offset sp, 8\n" " brk #1\n" " brk #1\n" ); #endif } // Optimized version of `jl_call_in_state` that avoids saving most CPU registers. static void jl_noreturn_call_in_state(host_thread_state_t *state, void (*fptr)(void), uintptr_t arg0, uintptr_t arg1) { #ifdef _CPU_X86_64_ uintptr_t sp = state->__rsp; #elif defined(_CPU_AARCH64_) uintptr_t sp = state->__sp; #endif sp = (sp - 256) & ~(uintptr_t)15; // redzone and re-alignment assert(sp % 16 == 0); #ifdef _CPU_X86_64_ // push {%rsp, %rip} sp -= sizeof(void*); *(uintptr_t*)sp = state->__rsp; sp -= sizeof(void*); *(uintptr_t*)sp = state->__rip; // pushq .jl_fake_signal_return + 1; aka call from jl_fake_signal_return sp -= sizeof(void*); *(uintptr_t*)sp = (uintptr_t)&jl_fake_signal_return + 1; state->__rsp = sp; // set stack pointer state->__rip = (uint64_t)fptr; // "call" the function state->__rdi = arg0; state->__rsi = arg1; #elif defined(_CPU_AARCH64_) // push {%pc, %sp} sp -= sizeof(void*); *(uintptr_t*)sp = state->__sp; sp -= sizeof(void*); *(uintptr_t*)sp = (uintptr_t)state->__pc; state->__sp = sp; // x31 state->__pc = (uint64_t)fptr; // pc state->__lr = (uintptr_t)&jl_fake_signal_return + 4; // x30 state->__x[0] = arg0; state->__x[1] = arg1; #else #error "julia: throw-in-context not supported on this platform" #endif } static void jl_longjmp_in_state(host_thread_state_t *state, jl_jmp_buf jmpbuf, int val) { if (!jl_simulate_longjmp(jmpbuf, (bt_context_t*)state, val)) { // for sanitizer builds, fallback to calling longjmp on the original stack // (this will fail for stack overflow, but that is hardly sanitizer-legal anyways) jl_noreturn_call_in_state(state, (void (*)(void))longjmp, (uintptr_t)jmpbuf, val); } } #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_state(jl_ptls_t ptls2, host_thread_state_t *state, jl_value_t *exception) { if (ptls2->safe_restore) { jl_longjmp_in_state(state, *ptls2->safe_restore, 1); } else { 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; ptls2->io_wait = 0; jl_task_t *ct = jl_atomic_load_relaxed(&ptls2->current_task); // This redirect abandons every frame between the interrupted context // and the handler; clear a published reset context so no // cancellation sender consumes a buffer in the abandoned region // (jl_eh_restore_state republishes the outer one at the catch). The // same applies to a foreign-call cancellation-handler guard // published in an abandoned frame. jl_atomic_store_release(&ct->reset_ctx, NULL); jl_atomic_store_release(&ct->cancel_handler_ctx, NULL); jl_handler_t *eh = ct->eh; if (eh != NULL) { asan_unpoison_task_stack(ct, &eh->eh_ctx); jl_longjmp_in_state(state, eh->eh_ctx, 1); } else { jl_no_exc_handler(exception, ct); } } } // Trampoline that runs on the faulting thread after being hijacked by the // Mach exception handler for a safepoint hit. This uses the same codepath // as the Unix signal handler (jl_set_gc_and_wait), so the faulting thread // participates in GC synchronization directly. static void mach_safepoint_trampoline(jl_ptls_t ptls) { // note: jl_current_task cannot be used here, since __thread variables // may already be destroyed during thread teardown jl_task_t *ct = jl_atomic_load_relaxed(&ptls->current_task); if (ct == NULL) return; // thread is dead, just resume jl_set_gc_and_wait(ct); // (The sigint force-throw that lived here is gone: SIGINT is delivered // through the cancellation system - see jl_sigint_request_cancellation - // and nothing arms the sigint page anymore.) } #if defined(_CPU_AARCH64_) __attribute__((naked, used)) static void jl_mach_restore_trigger(void) { __asm__( " adrp x16, _jl_mach_restore_page@PAGE\n" " ldr x16, [x16, _jl_mach_restore_page@PAGEOFF]\n" " ldr x17, [x16]\n" // EXC_BAD_ACCESS here " brk #1\n" // should never reach here ); } #elif defined(_CPU_X86_64_) __attribute__((naked, used)) static void jl_mach_restore_trigger(void) { __asm__( " movq _jl_mach_restore_page(%rip), %r11\n" " movq (%r11), %r11\n" // EXC_BAD_ACCESS here " ud2\n" // should never reach here ); } #endif // Set up state to call fptr(arg0) on the target thread. static void jl_call_in_state(host_thread_state_t *state, void (*fptr)(void), uintptr_t arg0, const void *float_state) { size_t float_size = jl_mach_float_state_info.count * sizeof(natural_t); size_t total_size = sizeof(host_thread_state_t) + float_size; // When fptr returns, the thread hits jl_mach_restore_trigger and faults // (again), so that we can request that the kernel restore all relevant // processor state. #ifdef _CPU_X86_64_ uintptr_t sp = state->__rsp; sp = (sp - 256) & ~(uintptr_t)15; // redzone and re-alignment sp -= total_size; sp &= ~(uintptr_t)15; memcpy((void*)sp, state, sizeof(host_thread_state_t)); memcpy((void*)(sp + sizeof(host_thread_state_t)), float_state, float_size); // Push the return address for the restore trigger sp -= sizeof(void*); *(uintptr_t*)sp = (uintptr_t)&jl_mach_restore_trigger; state->__rsp = sp; state->__rip = (uint64_t)fptr; state->__rdi = arg0; #elif defined(_CPU_AARCH64_) uintptr_t sp = state->__sp; sp = (sp - 256) & ~(uintptr_t)15; sp -= total_size; sp &= ~(uintptr_t)15; memcpy((void*)sp, state, sizeof(host_thread_state_t)); memcpy((void*)(sp + sizeof(host_thread_state_t)), float_state, float_size); state->__sp = sp; state->__pc = (uint64_t)fptr; state->__lr = (uintptr_t)&jl_mach_restore_trigger; state->__x[0] = arg0; #else #error "julia: call-in-state not supported on this platform" #endif } static void segv_handler(int sig, siginfo_t *info, void *context) { assert(sig == SIGSEGV || sig == SIGBUS); jl_jmp_buf *saferestore = jl_get_safe_restore(); if (saferestore) { // restarting jl_ or jl_unwind_stepn jl_longjmp_in_state((host_thread_state_t*)jl_to_bt_context(context), *saferestore, 1); return; } jl_task_t *ct = jl_get_current_task(); if ((sig != SIGBUS || info->si_code == BUS_ADRERR) && !(ct == NULL || ct->ptls == NULL || jl_atomic_load_relaxed(&ct->ptls->gc_state) == JL_GC_STATE_WAITING || ct->eh == NULL) && is_addr_on_stack(ct, info->si_addr)) { // stack overflow and not a BUS_ADRALN (alignment error) stack_overflow_warning(); } 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, nor 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_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) { host_thread_state_t *state = (host_thread_state_t*)new_state; assert(old_stateCnt >= MACH_THREAD_STATE_COUNT); // Copy old state to new state — we'll modify new_state in place memcpy(new_state, old_state, old_stateCnt * sizeof(natural_t)); *new_stateCnt = old_stateCnt; #ifdef LLVMLIBUNWIND if (thread == mach_profiler_thread) { return profiler_segv_handler(exception_port, thread, task, exception, code, codeCnt, state, new_stateCnt); } #endif jl_ptls_t ptls2 = NULL; int nthreads = jl_atomic_load_acquire(&jl_n_threads); for (int16_t tid = 0; tid < nthreads; tid++) { jl_ptls_t _ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid]; if (_ptls2 == NULL) 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; } if (ptls2->safe_restore) { jl_throw_in_state(ptls2, state, NULL); return KERN_SUCCESS; } if (jl_atomic_load_acquire(&ptls2->gc_state) == JL_GC_STATE_WAITING) return KERN_FAILURE; if (exception == EXC_ARITHMETIC) { jl_throw_in_state(ptls2, state, 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]; unsigned int exc_count = HOST_EXCEPTION_STATE_COUNT; host_exception_state_t exc_state; 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)) { // Save FP/SIMD state from the faulting thread mach_msg_type_number_t float_count = jl_mach_float_state_info.count; natural_t float_state[JL_MACH_FLOAT_STATE_MAX_COUNT]; kern_return_t ret = thread_get_state(thread, jl_mach_float_state_info.flavor, (thread_state_t)float_state, &float_count); HANDLE_MACH_ERROR("thread_get_state", ret); assert(float_count == jl_mach_float_state_info.count); // Hijack the faulting thread to handle the safepoint on its own // stack, using the same jl_set_gc_and_wait() codepath as Unix signals. jl_call_in_state(state, (void (*)(void))&mach_safepoint_trampoline, (uintptr_t)ptls2, float_state); return KERN_SUCCESS; } else if (jl_addr_is_restore_trigger(fault_addr)) { // This is a deliberate fault from jl_mach_restore_trigger, we're // returning from a `jl_call_in_state` (probably the one above). #if defined(_CPU_X86_64_) old_state = (thread_state_t)state->__rsp; #elif defined(_CPU_AARCH64_) old_state = (thread_state_t)state->__sp; #endif // Restore all registers and return KERN_SUCCESS to resume the thread. memcpy(state, old_state, sizeof(host_thread_state_t)); thread_state_t old_fp = (thread_state_t)((char*)old_state + sizeof(host_thread_state_t)); kern_return_t ret = thread_set_state(thread, jl_mach_float_state_info.flavor, old_fp, jl_mach_float_state_info.count); HANDLE_MACH_ERROR("thread_set_state", ret); // A completed cancellation-handler delivery is consumed here (no-op // for other jl_call_in_state users); further deliveries may fire // again once the thread resumes. ptls2->cancel_handler_armed = 0; return KERN_SUCCESS; } if (jl_atomic_load_relaxed(&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)) { stack_overflow_warning(); 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_state(ptls2, state, 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_STATE_IDENTITY } //mach_exc_server expects us to define this symbol locally 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) { return KERN_INVALID_ARGUMENT; // we only use EXCEPTION_STATE_IDENTITY } static void attach_exception_port(thread_port_t thread, int segv_only) { kern_return_t ret; // https://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_STATE_IDENTITY | 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 { if (tid < 0 || tid >= jl_atomic_load_acquire(&jl_n_threads)) return 0; 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; } static 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); } // Serializes every path that suspends a thread and rewrites its context // (jl_send_cancellation_signal on any thread, and jl_send_abandon_signal) for // its complete suspend/rewrite/resume sequence: two rewriters working from // the same suspended snapshot would install conflicting continuations and // task chains. (The profiler does not need it: it only reads contexts, and // suspend counts nest.) It also prevents two threads delivering // cancellations at each other from freezing both: a rewriter always takes // this lock before suspending and never blocks while holding a suspension, // so a suspended thread can never hold it. static pthread_mutex_t ctx_rewrite_lock = PTHREAD_MUTEX_INITIALIZER; // Runs on the interrupted thread, hijacked by jl_send_reset_signal via // jl_call_in_state with the interrupted state saved on the stack below: // invoke the registered cancellation handler with its arguments from the // per-thread save area. Returning runs into jl_mach_restore_trigger, whose // exception-handler branch restores the interrupted state (and disarms). static void jl_mach_cancel_handler_trampoline(jl_ptls_t ptls) { jl_cancel_handler_save_t *save = &ptls->cancel_handler_save; save->fn(save->state, save->sev); } // Suspend-based shootdown delivery (see jl_send_reset_signal in // signals-win.c for the shared shape and locking rationale). Rather than // simulating the longjmp into a GP-only thread state - which cannot carry // the callee-saved SIMD registers the setjmp ABI requires - the frozen // thread is redirected to call the real longjmp on its own stack. A // foreign-call cancellation handler is delivered by hijacking the frozen // thread to run fn(state, sev) via the resumable jl_call_in_state // machinery, whose restore-trigger return path restores the complete // interrupted state through the kernel. Best-effort: any failed check // simply drops the request. static void jl_send_reset_signal(int16_t tid, int reset_code) JL_NOTSAFEPOINT { jl_value_t *bound; int bound_cancelled; jl_cancel_handler_ctx_t *hctx; if (tid < 0 || tid >= jl_atomic_load_acquire(&jl_n_threads)) return; jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid]; if (ptls2 == NULL) return; jl_task_t *ct2 = jl_atomic_load_relaxed(&ptls2->current_task); if (ct2 == NULL) return; // Only proceed if the task has an interruptible-region context // published - a purely polling victim between cancellation points never // has one. Never self-suspend: a thread's own reset region is always // cleared here (the ccall reaching this function is itself an unsafe // point), but a protected runtime span (e.g. the GMP allocation hooks) // can reach this sender - through a finalizer running cancel! - with // its own handler context still published. if (pthread_equal(ptls2->system_id, pthread_self())) return; if (jl_atomic_load_relaxed(&ct2->reset_ctx) == NULL && jl_atomic_load_relaxed(&ct2->cancel_handler_ctx) == NULL) return; pthread_mutex_lock(&ctx_rewrite_lock); // Thread teardown clears current_task while holding the profile write // lock (see jl_free_thread_gc_state): hold the read lock across the // liveness re-check and the suspension, so the thread cannot exit (and // its Mach port cannot die) in between. Suspension failure is still // treated as a best-effort delivery failure, never a fatal error. jl_lock_profile(); ct2 = jl_atomic_load_relaxed(&ptls2->current_task); if (ct2 == NULL) { jl_unlock_profile(); pthread_mutex_unlock(&ctx_rewrite_lock); return; } mach_port_t thread = pthread_mach_thread_np(ptls2->system_id); kern_return_t ret = thread_suspend(thread); jl_unlock_profile(); if (ret != KERN_SUCCESS) { pthread_mutex_unlock(&ctx_rewrite_lock); return; } host_thread_state_t state; unsigned int count = MACH_THREAD_STATE_COUNT; memset(&state, 0, sizeof(state)); if (thread_get_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, &count) != KERN_SUCCESS) goto resume; // Re-check now that the thread cannot run: the current task may have // switched before the freeze. Delivery is gated on an actual // cancellation of the task's bound token source, kept coherent with the // published regions by the exception-handler and finalizer save/restore // discipline. ct2 = jl_atomic_load_relaxed(&ptls2->current_task); bound = ct2 == NULL ? NULL : jl_atomic_load_relaxed(&ct2->bound_cancel_token); bound_cancelled = bound != NULL && bound != jl_nothing && jl_atomic_load_relaxed(&((jl_cancel_source_t*)bound)->state) != 0; hctx = ct2 == NULL ? NULL : jl_atomic_load_acquire(&ct2->cancel_handler_ctx); if (hctx != NULL) { // Handler flavor: a published foreign-call cancellation-handler // guard takes priority over (and suppresses) the reset - its span // (e.g. a protected allocator) is exactly where a longjmp must not // land, and the handler can defer the cancellation and chain into // the reset on region exit. Hijack the frozen thread to run // fn(state, sev) on its own stack via the resumable // jl_call_in_state machinery - at most one delivery at a time per // thread (the save area holds one; skips recover level-triggered). if (!ptls2->cancel_handler_armed && bound_cancelled) { mach_msg_type_number_t float_count = jl_mach_float_state_info.count; natural_t float_state[JL_MACH_FLOAT_STATE_MAX_COUNT]; if (thread_get_state(thread, jl_mach_float_state_info.flavor, (thread_state_t)float_state, &float_count) != KERN_SUCCESS) goto resume; jl_cancel_handler_save_t *save = &ptls2->cancel_handler_save; save->fn = hctx->fn; save->state = hctx->state; save->sev = jl_atomic_load_relaxed(&((jl_cancel_source_t*)bound)->state); ptls2->cancel_handler_armed = 1; jl_call_in_state(&state, (void (*)(void))&jl_mach_cancel_handler_trampoline, (uintptr_t)ptls2, float_state); if (thread_set_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, MACH_THREAD_STATE_COUNT) != KERN_SUCCESS) ptls2->cancel_handler_armed = 0; } } else if (ct2 != NULL && // Reset flavor, additionally gated on the thread running Julia code // (gc_state == 0): a thread inside a GC-safe region may be raced by // a concurrent stop-the-world, and a redirect back into Julia code // would break that protocol. jl_atomic_load_relaxed(&ptls2->gc_state) == JL_GC_STATE_UNSAFE) { jl_reset_ctx_t *reset_ctx = jl_atomic_load_acquire(&ct2->reset_ctx); if (reset_ctx != NULL && reset_ctx->sp != 0 && (reset_code == JL_RESET_CODE_PREEMPT || bound_cancelled)) { // Consume the reset point with an exchange (off-thread senders // may race each other, unlike the Unix in-handler consumer). reset_ctx = jl_atomic_exchange(&ct2->reset_ctx, NULL); if (reset_ctx != NULL && reset_ctx->sp != 0) { // Redirect the frozen thread to call longjmp on its own // stack (restoring callee-saved GP and SIMD state // natively), and rewind the task's gcstack/eh chains only // once the redirect is committed, so that any failure // resumes the thread exactly as it was, with the // (unconsumed) region republished. jl_noreturn_call_in_state(&state, (void (*)(void))longjmp, (uintptr_t)reset_ctx->mctx, reset_code); if (thread_set_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, MACH_THREAD_STATE_COUNT) == KERN_SUCCESS) { ct2->gcstack = reset_ctx->gcstack; ct2->eh = reset_ctx->eh; } else { jl_atomic_store_release(&ct2->reset_ctx, reset_ctx); } } } } resume: if (thread_resume(thread) != KERN_SUCCESS) jl_safe_printf("error: thread_resume failed in cancellation delivery\n"); pthread_mutex_unlock(&ctx_rewrite_lock); } // Switch the target thread's current (already committed) task to // ptls->abandon_to (see jl_abandon_task_request): suspend the thread, validate the // pending request against its frozen state, and on commit redirect it into // the abandon callback. Holds the rewrite lock like every other // suspend-and-rewrite path (see its definition above), and the profile read // lock across the liveness re-check and suspension so the thread cannot // exit in between. void jl_send_abandon_signal(int16_t tid) JL_NOTSAFEPOINT { jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid]; if (ptls2 == NULL) return; pthread_mutex_lock(&ctx_rewrite_lock); jl_lock_profile(); jl_task_t *ct2 = jl_atomic_load_relaxed(&ptls2->current_task); if (ct2 == NULL) { jl_unlock_profile(); pthread_mutex_unlock(&ctx_rewrite_lock); return; } mach_port_t thread = pthread_mach_thread_np(ptls2->system_id); kern_return_t ret = thread_suspend(thread); jl_unlock_profile(); if (ret != KERN_SUCCESS) { pthread_mutex_unlock(&ctx_rewrite_lock); return; } // The victim thread is suspended: fetch its context *before* deciding // anything, validate the pending request against its frozen state, and // only then redirect it into the abandon callback (which never // returns). A commit is rolled back to a refusal if the redirect // cannot be completed - the callback is what publishes the abandoned // task state, so an unredirected victim resumes untouched. On refusal // the requester observes the verdict and withdraws. host_thread_state_t state; unsigned int count = MACH_THREAD_STATE_COUNT; memset(&state, 0, sizeof(state)); if (thread_get_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, &count) != KERN_SUCCESS) { // cannot redirect; leave the request pending for a retry/withdraw } else if (jl_abandon_try_commit(ptls2)) { jl_noreturn_call_in_state(&state, (void (*)(void))&jl_abandon_task_cb, 0, 0); if (thread_set_state(thread, MACH_THREAD_STATE, (thread_state_t)&state, count) != KERN_SUCCESS) { // Roll the commit back: nothing observable was published yet // (the task state is written by the callback). jl_atomic_store_release(&ptls2->abandon_state, JL_ABANDON_REFUSED); } } if (thread_resume(thread) != KERN_SUCCESS) jl_safe_printf("error: thread_resume failed in task abandonment\n"); pthread_mutex_unlock(&ctx_rewrite_lock); } static void jl_exit_thread0_cb(int signo) { jl_fprint_critical_error(ios_safe_stderr, 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])); jl_noreturn_call_in_state(&state, (void (*)(void))&jl_exit_thread0_cb, signo, 0); 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, host_thread_state_t *state, mach_msg_type_number_t *new_stateCnt) { assert(thread == mach_profiler_thread); // Not currently unwinding. Raise regular segfault if (forceDwarf == -2) return KERN_FAILURE; if (forceDwarf == 0) forceDwarf = 1; else forceDwarf = -1; #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 *new_stateCnt = MACH_THREAD_STATE_COUNT; 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_thread_suspend(int16_t tid, bt_context_t *ctx) { int lockret = jl_lock_profile_mach(1); int success = jl_thread_suspend_and_get_state(tid, 1, ctx); jl_unlock_profile_mach(1, lockret); return success; } void jl_with_stackwalk_lock(void (*f)(void*), void *ctx) { int lockret = jl_lock_profile_mach(1); f(ctx); jl_unlock_profile_mach(1, lockret); } // assumes holding `jl_lock_profile_mach` void jl_profile_thread_mach(int tid) { // if there is no space left, return early if (jl_profile_is_buffer_full()) { jl_profile_stop_timer(); return; } 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(tid, &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) return; if (profile_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 profile_bt_size_cur += rec_backtrace_ctx((jl_bt_element_t*)profile_bt_data_prof + profile_bt_size_cur, profile_bt_size_max - profile_bt_size_cur - 1, uc, NULL); } else if (forceDwarf == 1) { profile_bt_size_cur += rec_backtrace_ctx_dwarf((jl_bt_element_t*)profile_bt_data_prof + profile_bt_size_cur, profile_bt_size_max - profile_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 profile_bt_size_cur += rec_backtrace_ctx((jl_bt_element_t*)profile_bt_data_prof + profile_bt_size_cur, profile_bt_size_max - profile_bt_size_cur - 1, uc, NULL); #endif jl_ptls_t ptls = jl_atomic_load_relaxed(&jl_all_tls_states)[tid]; // store threadid but add 1 as 0 is preserved to indicate end of block profile_bt_data_prof[profile_bt_size_cur++].uintptr = ptls->tid + 1; // store task id (never null) profile_bt_data_prof[profile_bt_size_cur++].jlvalue = (jl_value_t*)jl_atomic_load_relaxed(&ptls->current_task); // store cpu cycle clock profile_bt_data_prof[profile_bt_size_cur++].uintptr = cycleclock(); // store whether thread is sleeping (don't ever encode a state as `0` since it is preserved to indicate end of block) int state = jl_atomic_load_relaxed(&ptls->sleep_check_state) == 0 ? PROFILE_STATE_THREAD_NOT_SLEEPING : PROFILE_STATE_THREAD_SLEEPING; profile_bt_data_prof[profile_bt_size_cur++].uintptr = state; // Mark the end of this block with two 0's profile_bt_data_prof[profile_bt_size_cur++].uintptr = 0; profile_bt_data_prof[profile_bt_size_cur++].uintptr = 0; } // We're done! Resume the thread. jl_thread_resume(tid); } 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); if (profile_all_tasks) { // Don't take the stackwalk lock here since it's already taken in `jl_rec_backtrace` jl_profile_task(); } else { int *randperm = profile_get_randperm(nthreads); for (int idx = nthreads; idx-- > 0; ) { // Stop the threads in random order. int i = randperm[idx]; jl_profile_thread_mach(i); } } jl_unlock_profile_mach(0, keymgr_locked); if (profile_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(uint8_t all_tasks) { 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; // hold the lock so that `jl_profile_init` cannot free the buffer while we transition to running uv_mutex_lock(&bt_data_prof_lock); profile_running = 1; profile_all_tasks = all_tasks; uv_mutex_unlock(&bt_data_prof_lock); // 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) { uv_mutex_lock(&bt_data_prof_lock); profile_running = 0; profile_all_tasks = 0; uv_mutex_unlock(&bt_data_prof_lock); } // The mprotect implementation in signals-unix.c does not work on macOS/aarch64, as mentioned. // This implementation comes from dotnet, but is similarly dependent on undocumented behavior of the OS. // Copyright (c) .NET Foundation and Contributors // MIT LICENSE JL_DLLEXPORT void jl_membarrier(void) JL_NOTSAFEPOINT { uintptr_t sp; uintptr_t registerValues[128]; kern_return_t machret; // Iterate through each of the threads in the list. int nthreads = jl_atomic_load_acquire(&jl_n_threads); for (int tid = 0; tid < nthreads; tid++) { jl_ptls_t ptls2 = jl_atomic_load_relaxed(&jl_all_tls_states)[tid]; thread_act_t thread = pthread_mach_thread_np(ptls2->system_id); if (__builtin_available (macOS 10.14, iOS 12, tvOS 9, *)) { // Request the threads pointer values to force the thread to emit a memory barrier size_t registers = 128; machret = thread_get_register_pointer_values(thread, &sp, ®isters, registerValues); } else { // fallback implementation for older OS versions #if defined(_CPU_X86_64_) x86_thread_state64_t threadState; mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT; machret = thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&threadState, &count); #elif defined(_CPU_AARCH64_) arm_thread_state64_t threadState; mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT; machret = thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&threadState, &count); #else #error Unexpected architecture #endif } if (machret == KERN_INSUFFICIENT_BUFFER_SIZE) { HANDLE_MACH_ERROR("thread_get_register_pointer_values()", machret); } } }