diff --git a/runtime/internal/lib/runtime/_wrap/dynunwind.c b/runtime/internal/lib/runtime/_wrap/dynunwind.c new file mode 100644 index 0000000000..6a9b3ba8a5 --- /dev/null +++ b/runtime/internal/lib/runtime/_wrap/dynunwind.c @@ -0,0 +1,382 @@ +/* Dynamically-loaded libunwind for fault-site C stacks. + * + * The FP-walk in unwind_llgo.go breaks on C frames compiled without frame + * pointers, and on linux dladdr cannot name non-dynamic C symbols. This + * code resolves libunwind AT RUNTIME (dlopen/dlsym at handler-install + * time — never in the signal handler itself; no link-time -lunwind) and + * unwinds the fault ucontext with DWARF/compact unwind info, resuming with + * the FP chain where unwind info runs out. + * + * Flavors probed, in order: + * darwin: unw_* live in libSystem — dlsym(RTLD_DEFAULT), no dlopen. + * unw_context_t is a register buffer (LLVM layout); we translate + * the mcontext thread state into it. + * linux: 1) nongnu libunwind (libunwind.so.8): symbols are arch-prefixed + * (_ULx86_64_* / _ULaarch64_*); unw_init_local accepts the + * signal ucontext_t directly, and unw_get_proc_name reads + * .symtab so static C symbols get named. + * 2) LLVM libunwind (libunwind.so.1): plain unw_* symbols; + * context translated like darwin. + * + * Async-signal-safety: dlopen/dlsym/getenv happen once at init. In the + * handler only unw_init_local(2)/unw_step/unw_get_reg run; nongnu libunwind + * documents these as thread-safe and usable from signal handlers for local + * unwinding (remote/global state is only touched on first use — we do a + * throwaway warm-up unwind at init to pre-fault lazy state). The debug + * printing (snprintf/write) is for the experiment only. + */ +#define _XOPEN_SOURCE 700 +#define _DARWIN_C_SOURCE 1 +#if defined(__linux__) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif + +#include +#include + +#if (defined(__APPLE__) || defined(__linux__)) && (defined(__aarch64__) || defined(__x86_64__)) + +#include +#include +#include +#include +#include +#include +#include +#include + +#define DYNUNW_MAX 64 +#define DYNUNW_NAME_LEN 64 + +/* Results of the last fault-time unwind; read by the Go side. */ +static uintptr_t dynunw_pcs[DYNUNW_MAX]; +static int dynunw_count; +static uintptr_t dynunw_end_fp; /* fp at the frame the unwind stopped on */ +static char dynunw_names[DYNUNW_MAX][DYNUNW_NAME_LEN]; +static uintptr_t dynunw_offs[DYNUNW_MAX]; +static long long dynunw_ns; /* handler-path cost of the dynamic unwind */ + +static int dynunw_enabled; /* LLGO_EXP_DYNUNWIND=1 */ +static int dynunw_debug; /* LLGO_EXP_DYNUNWIND_DEBUG=1: print in handler */ + +/* Function-pointer view of whichever libunwind flavor we resolved. + * All flavors share these shapes (unw_word_t == uintptr_t here). */ +typedef int (*unw_init_local_fn)(void *cursor, void *ctx); +typedef int (*unw_init_local2_fn)(void *cursor, void *ctx, int flags); +typedef int (*unw_step_fn)(void *cursor); +typedef int (*unw_get_reg_fn)(void *cursor, int reg, uintptr_t *val); +typedef int (*unw_get_proc_name_fn)(void *cursor, char *buf, size_t len, + uintptr_t *off); + +static unw_init_local_fn p_init_local; +static unw_init_local2_fn p_init_local2; /* preferred: UNW_INIT_SIGNAL_FRAME */ +static unw_step_fn p_step; +static unw_get_reg_fn p_get_reg; +static unw_get_proc_name_fn p_get_proc_name; +/* Whether to call unw_get_proc_name inside the fault handler. Darwin's + * implementation is in-memory (compact unwind / dyld tables); nongnu's + * reads /proc/self/maps and mmaps ELF .symtab — not async-signal-safe, + * so it is off by default on linux (LLGO_DYNUNWIND_NAMES=1 re-enables + * for diagnostics; frame pcs and end-fp are still captured). */ +static int dynunw_capture_names; + +static int dynunw_reg_ip; /* UNW_REG_IP for the resolved flavor */ +static int dynunw_reg_fp; /* frame-pointer regnum for the resolved flavor */ +static int dynunw_ctx_is_ucontext; /* nongnu: pass the ucontext_t through */ + +const char *llgo_dynunwind_flavor = "none"; + +/* Cursor/context buffers. Sized generously: nongnu aarch64's unw_cursor_t is + * 4096 words (33KB); LLVM's is ~1.5KB. Static so the signal stack does not + * pay for them (the fault path is not reentrant: the runtime converts the + * fault to a panic and never returns to the handler). */ +static uint64_t dynunw_cursor[8192]; +static uint64_t dynunw_ctx[1024]; + +#define UNW_INIT_SIGNAL_FRAME 1 + +/* ---- context translation ---------------------------------------------- */ + +/* LLVM libunwind's unw_context_t is the unw_getcontext register dump: + * x86_64: rax rbx rcx rdx rdi rsi rbp rsp r8..r15 rip (17 words) + * aarch64: x0..x28 fp lr sp pc ra_sign_state (34 words) + * Both match (or prefix-match) the platform thread state closely enough to + * fill from the fault mcontext. */ +static void *dynunw_make_ctx(void *uctx) +{ + ucontext_t *uc = (ucontext_t *)uctx; + if (dynunw_ctx_is_ucontext) + return uctx; /* nongnu linux: unw_context_t IS ucontext_t */ +#if defined(__APPLE__) && defined(__aarch64__) + /* __darwin_arm_thread_state64: x[29] fp lr sp pc cpsr(32) pad(32). + * First 33 words line up with LLVM's GPRs; word 33 is ra_sign_state + * there vs cpsr here — zero it (no PAC signing state carried over). */ + memcpy(dynunw_ctx, &uc->uc_mcontext->__ss, 33 * sizeof(uint64_t)); + dynunw_ctx[33] = 0; +#elif defined(__APPLE__) && defined(__x86_64__) + /* __darwin_x86_thread_state64 is exactly LLVM's GPR order for the first + * 17 words (rax..r15, rip). */ + memcpy(dynunw_ctx, &uc->uc_mcontext->__ss, 17 * sizeof(uint64_t)); +#elif defined(__linux__) && defined(__x86_64__) + { + /* linux gregs order: R8 R9 R10 R11 R12 R13 R14 R15 RDI RSI RBP RBX + * RDX RAX RCX RSP RIP ... — permute into LLVM order. */ + greg_t *g = uc->uc_mcontext.gregs; + dynunw_ctx[0] = (uint64_t)g[13]; /* rax */ + dynunw_ctx[1] = (uint64_t)g[11]; /* rbx */ + dynunw_ctx[2] = (uint64_t)g[14]; /* rcx */ + dynunw_ctx[3] = (uint64_t)g[12]; /* rdx */ + dynunw_ctx[4] = (uint64_t)g[8]; /* rdi */ + dynunw_ctx[5] = (uint64_t)g[9]; /* rsi */ + dynunw_ctx[6] = (uint64_t)g[10]; /* rbp */ + dynunw_ctx[7] = (uint64_t)g[15]; /* rsp */ + dynunw_ctx[8] = (uint64_t)g[0]; /* r8 */ + dynunw_ctx[9] = (uint64_t)g[1]; /* r9 */ + dynunw_ctx[10] = (uint64_t)g[2]; /* r10 */ + dynunw_ctx[11] = (uint64_t)g[3]; /* r11 */ + dynunw_ctx[12] = (uint64_t)g[4]; /* r12 */ + dynunw_ctx[13] = (uint64_t)g[5]; /* r13 */ + dynunw_ctx[14] = (uint64_t)g[6]; /* r14 */ + dynunw_ctx[15] = (uint64_t)g[7]; /* r15 */ + dynunw_ctx[16] = (uint64_t)g[16]; /* rip */ + } +#elif defined(__linux__) && defined(__aarch64__) + memcpy(dynunw_ctx, uc->uc_mcontext.regs, 31 * sizeof(uint64_t)); /* x0..x30 */ + dynunw_ctx[31] = (uint64_t)uc->uc_mcontext.sp; + dynunw_ctx[32] = (uint64_t)uc->uc_mcontext.pc; + dynunw_ctx[33] = 0; +#endif + return dynunw_ctx; +} + +/* ---- flavor resolution (init time, NOT in the handler) ----------------- */ + +static void dynunw_use_llvm_regnums(void) +{ + dynunw_reg_ip = -1; /* LLVM UNW_REG_IP */ +#if defined(__aarch64__) + dynunw_reg_fp = 29; /* UNW_ARM64_FP */ +#else + dynunw_reg_fp = 6; /* UNW_X86_64_RBP */ +#endif +} + +static int dynunw_resolve(void) +{ +#if defined(__APPLE__) + /* unw_* are exported by libSystem's libunwind — already loaded. */ + p_init_local = (unw_init_local_fn)dlsym(RTLD_DEFAULT, "unw_init_local"); + p_init_local2 = + (unw_init_local2_fn)dlsym(RTLD_DEFAULT, "unw_init_local2"); + p_step = (unw_step_fn)dlsym(RTLD_DEFAULT, "unw_step"); + p_get_reg = (unw_get_reg_fn)dlsym(RTLD_DEFAULT, "unw_get_reg"); + p_get_proc_name = + (unw_get_proc_name_fn)dlsym(RTLD_DEFAULT, "unw_get_proc_name"); + if (p_init_local && p_step && p_get_reg) { + dynunw_use_llvm_regnums(); + dynunw_ctx_is_ucontext = 0; + dynunw_capture_names = 1; + llgo_dynunwind_flavor = "darwin-libsystem"; + return 1; + } + return 0; +#elif defined(__linux__) +#if defined(__x86_64__) + static const char *pfx = "_ULx86_64_"; + dynunw_reg_ip = 16; /* UNW_X86_64_RIP */ + dynunw_reg_fp = 6; /* UNW_X86_64_RBP */ +#else + static const char *pfx = "_ULaarch64_"; + dynunw_reg_ip = 32; /* UNW_AARCH64_PC */ + dynunw_reg_fp = 29; /* UNW_AARCH64_X29 */ +#endif + /* 1) nongnu libunwind: arch-prefixed local-only symbols; ucontext in; + * .symtab-aware unw_get_proc_name (names static C symbols). */ + { + const char *names[] = {"libunwind.so.8", "libunwind.so", 0}; + int i; + for (i = 0; names[i]; i++) { + void *h = dlopen(names[i], RTLD_NOW | RTLD_GLOBAL); + char sym[64]; + if (!h) + continue; +#define RESOLVE(dst, name, type) \ + do { \ + snprintf(sym, sizeof(sym), "%s%s", pfx, name); \ + dst = (type)dlsym(h, sym); \ + } while (0) + RESOLVE(p_init_local, "init_local", unw_init_local_fn); + RESOLVE(p_init_local2, "init_local2", unw_init_local2_fn); + RESOLVE(p_step, "step", unw_step_fn); + RESOLVE(p_get_reg, "get_reg", unw_get_reg_fn); + RESOLVE(p_get_proc_name, "get_proc_name", unw_get_proc_name_fn); +#undef RESOLVE + if (p_init_local && p_step && p_get_reg) { + dynunw_ctx_is_ucontext = 1; + { + const char *e = getenv("LLGO_DYNUNWIND_NAMES"); + dynunw_capture_names = e && e[0] == '1' && !e[1]; + } + llgo_dynunwind_flavor = "linux-nongnu"; + return 1; + } + } + } + /* 2) LLVM libunwind: plain unw_* names, register-buffer context. */ + { + const char *names[] = {"libunwind.so.1", "libunwind.so", 0}; + int i; + for (i = 0; names[i]; i++) { + void *h = dlopen(names[i], RTLD_NOW | RTLD_GLOBAL); + if (!h) + continue; + p_init_local = (unw_init_local_fn)dlsym(h, "unw_init_local"); + p_init_local2 = (unw_init_local2_fn)dlsym(h, "unw_init_local2"); + p_step = (unw_step_fn)dlsym(h, "unw_step"); + p_get_reg = (unw_get_reg_fn)dlsym(h, "unw_get_reg"); + p_get_proc_name = + (unw_get_proc_name_fn)dlsym(h, "unw_get_proc_name"); + if (p_init_local && p_step && p_get_reg) { + dynunw_use_llvm_regnums(); + dynunw_ctx_is_ucontext = 0; + llgo_dynunwind_flavor = "linux-llvm"; + return 1; + } + } + } + return 0; +#else + return 0; +#endif +} + +/* ---- the fault-time unwind --------------------------------------------- */ + +static int dynunw_run(void *uctx) +{ + void *ctx = dynunw_make_ctx(uctx); + int n = 0, first = 1; + dynunw_end_fp = 0; + if (p_init_local2) { + if (p_init_local2(dynunw_cursor, ctx, UNW_INIT_SIGNAL_FRAME) != 0) + return 0; + } else if (p_init_local(dynunw_cursor, ctx) != 0) { + return 0; + } + while (n < DYNUNW_MAX) { + uintptr_t ip = 0, off = 0; + if (p_get_reg(dynunw_cursor, dynunw_reg_ip, &ip) != 0 || ip == 0) + break; + dynunw_pcs[n] = ip; + dynunw_names[n][0] = 0; + dynunw_offs[n] = 0; + if (dynunw_capture_names && p_get_proc_name && + p_get_proc_name(dynunw_cursor, dynunw_names[n], DYNUNW_NAME_LEN, + &off) == 0) + dynunw_offs[n] = off; + (void)first; + n++; + if (p_get_reg(dynunw_cursor, dynunw_reg_fp, &dynunw_end_fp) != 0) + dynunw_end_fp = 0; + first = 0; + if (p_step(dynunw_cursor) <= 0) + break; + } + return n; +} + +/* Called from llgo_fault_trampoline (fault.c) with the raw ucontext. */ +void llgo_dynunwind_capture(void *uctx) +{ + struct timespec t0, t1; + if (!dynunw_enabled || !p_step) + return; + clock_gettime(CLOCK_MONOTONIC, &t0); + dynunw_count = dynunw_run(uctx); + clock_gettime(CLOCK_MONOTONIC, &t1); + dynunw_ns = (long long)(t1.tv_sec - t0.tv_sec) * 1000000000LL + + (t1.tv_nsec - t0.tv_nsec); + if (dynunw_debug) { + char line[160]; + int i, len; + len = snprintf(line, sizeof(line), + "[dynunwind] flavor=%s frames=%d cost=%lldns\n", + llgo_dynunwind_flavor, dynunw_count, dynunw_ns); + write(2, line, len); + for (i = 0; i < dynunw_count; i++) { + len = snprintf(line, sizeof(line), "[dynunwind] #%02d %p %s+0x%lx\n", + i, (void *)dynunw_pcs[i], + dynunw_names[i][0] ? dynunw_names[i] : "??", + (unsigned long)dynunw_offs[i]); + write(2, line, len); + } + } +} + +/* Called once from llgo_install_fault_handler: resolve + warm up. */ +void llgo_dynunwind_init(void) +{ + /* On by default when a libunwind flavor resolves; LLGO_DYNUNWIND=0 + * forces the plain FP-walk. */ + const char *e = getenv("LLGO_DYNUNWIND"); + dynunw_enabled = !(e && *e == '0'); + e = getenv("LLGO_DYNUNWIND_DEBUG"); + dynunw_debug = e && *e && *e != '0'; + if (!dynunw_enabled) + return; + if (!dynunw_resolve()) { + dynunw_enabled = 0; + if (dynunw_debug) + write(2, "[dynunwind] no libunwind flavor resolved\n", 41); + return; + } + if (dynunw_debug) { + char line[96]; + int len = snprintf(line, sizeof(line), "[dynunwind] resolved flavor=%s\n", + llgo_dynunwind_flavor); + write(2, line, len); + } + /* Warm-up: nongnu libunwind builds lazy per-process state (address-space + * setup, dl_iterate_phdr caches) on first use; do that in normal context + * so the handler-time path avoids first-use allocation. Unwinding our + * own live context here is safe and exercises the whole path. */ +#if defined(__linux__) + { + ucontext_t uc; + if (dynunw_ctx_is_ucontext && getcontext(&uc) == 0) { + dynunw_run(&uc); + dynunw_count = 0; + } + /* LLVM flavor: no ucontext path for the warm-up (unw_getcontext is a + * register dump we cannot fabricate portably here); its first-use + * state is per-cursor anyway. */ + } +#endif /* getcontext is deprecated on darwin; darwin needs no warm-up (all + * state lives in libSystem, already initialized by normal use). */ +} + +/* ---- accessors for the Go side ----------------------------------------- */ + +uintptr_t *llgo_dynunwind_pcbuf(void) { return dynunw_pcs; } +int llgo_dynunwind_pccount(void) { return dynunw_count; } +uintptr_t llgo_dynunwind_endfp(void) { return dynunw_end_fp; } + +/* Symbol name libunwind reported for frame i ("" when unknown). nongnu + * libunwind reads .symtab, naming static C symbols dladdr cannot see. */ +const char *llgo_dynunwind_name(int i) +{ + if (i < 0 || i >= dynunw_count) + return ""; + return dynunw_names[i]; +} + +#else /* unsupported platform */ + +void llgo_dynunwind_init(void) {} +void llgo_dynunwind_capture(void *uctx) { (void)uctx; } +uintptr_t *llgo_dynunwind_pcbuf(void) { return 0; } +int llgo_dynunwind_pccount(void) { return 0; } +uintptr_t llgo_dynunwind_endfp(void) { return 0; } +const char *llgo_dynunwind_name(int i) { (void)i; return ""; } + +#endif diff --git a/runtime/internal/lib/runtime/_wrap/fault.c b/runtime/internal/lib/runtime/_wrap/fault.c new file mode 100644 index 0000000000..641f1a1fe1 --- /dev/null +++ b/runtime/internal/lib/runtime/_wrap/fault.c @@ -0,0 +1,123 @@ +/* Fault handler with fault-site context: see the comment on + * llgo_install_fault_handler below. Separate file because darwin's + * ucontext.h demands _XOPEN_SOURCE before any system header. */ +#define _XOPEN_SOURCE 700 +#define _DARWIN_C_SOURCE 1 +#if defined(__linux__) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif + +/* Fault handler with fault-site context. The plain signal() handler the + * runtime core installs cannot see the interrupted registers, and walking + * from the handler's own frame stops at the signal trampoline — so the + * panic pc snapshot for hardware faults (nil derefs compiled with + * null_pointer_is_valid, faults inside C code, integer division on x86) + * starts from the ucontext's pc/fp instead. */ +#include +#include +#include +#include +#include + +static long llgo_pagesz; /* primed at handler install, out of signal context */ + +#if defined(__APPLE__) || defined(__linux__) +#include + +static void (*llgo_fault_go)(uintptr_t pc, uintptr_t fp, int sig); + +/* Dynamic-libunwind fault unwinding (dynunwind.c); no-ops when disabled + * (LLGO_DYNUNWIND=0) or no libunwind flavor resolved. */ +extern void llgo_dynunwind_init(void); +extern void llgo_dynunwind_capture(void *uctx); + +static volatile int llgo_in_fault; + +static void llgo_fault_trampoline(int sig, siginfo_t *info, void *uctx) +{ + uintptr_t pc = 0, fp = 0; + ucontext_t *uc = (ucontext_t *)uctx; + (void)info; + if (llgo_in_fault) { + signal(sig, SIG_DFL); + raise(sig); + return; + } + llgo_in_fault = 1; +#if defined(__APPLE__) && defined(__aarch64__) + pc = (uintptr_t)uc->uc_mcontext->__ss.__pc; + fp = (uintptr_t)uc->uc_mcontext->__ss.__fp; +#elif defined(__APPLE__) && defined(__x86_64__) + pc = (uintptr_t)uc->uc_mcontext->__ss.__rip; + fp = (uintptr_t)uc->uc_mcontext->__ss.__rbp; +#elif defined(__linux__) && defined(__aarch64__) + pc = (uintptr_t)uc->uc_mcontext.pc; + fp = (uintptr_t)uc->uc_mcontext.regs[29]; +#elif defined(__linux__) && defined(__x86_64__) + pc = (uintptr_t)uc->uc_mcontext.gregs[16 /* REG_RIP */]; + fp = (uintptr_t)uc->uc_mcontext.gregs[10 /* REG_RBP */]; +#endif + llgo_dynunwind_capture(uctx); + llgo_fault_go(pc, fp, sig); +} + +void llgo_fault_capture_done(void) +{ + /* Belt and braces alongside SA_NODEFER: make sure no fault signal + * stays blocked once this fault becomes an ordinary panic. */ + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGSEGV); + sigaddset(&set, SIGBUS); + sigaddset(&set, SIGFPE); + sigprocmask(SIG_UNBLOCK, &set, 0); + llgo_in_fault = 0; +} + +int llgo_mem_readable(void *p); + +void llgo_install_fault_handler(void (*cb)(uintptr_t, uintptr_t, int)) +{ + struct sigaction sa; + int sigs[3] = {SIGSEGV, SIGBUS, SIGFPE}; + int i; + /* Installation runs at startup, before user code. dlopen probes for + * libunwind (and sysconf/sigaction) may set errno; a leaked errno is + * visible to the first two-result cgo call ("v, err := C.f()"), which + * turns it into a spurious non-nil err. */ + int saved_errno = errno; + llgo_fault_go = cb; + llgo_pagesz = sysconf(_SC_PAGESIZE); + llgo_dynunwind_init(); + for (i = 0; i < 3; i++) { + sa.sa_sigaction = llgo_fault_trampoline; + sigemptyset(&sa.sa_mask); + /* SA_NODEFER: the handler converts the fault to a Go panic that + * longjmps out through jmpbufs saved with savemask=0, so an + * auto-blocked signal would stay blocked forever — the next + * genuine fault would then be force-delivered with the default + * action (core) instead of reaching us. Recursion is guarded by + * llgo_in_fault above. */ + sa.sa_flags = SA_SIGINFO | SA_NODEFER; + sigaction(sigs[i], &sa, 0); + } + errno = saved_errno; +} +#endif + +/* Probe whether one byte at p is mapped-readable: msync on the containing + * page returns ENOMEM for unmapped ranges. Used by the fault-context + * walks — an arithmetic-valid frame pointer can still point into a hole, + * and faulting inside the fault path recurses. */ +int llgo_mem_readable(void *p) +{ + int saved_errno = errno; + char *page; + long sz = llgo_pagesz; + if (sz <= 0) + sz = 4096; + page = (char *)((uintptr_t)p & ~(uintptr_t)(sz - 1)); + if (msync(page, 1, MS_ASYNC) == 0) + { int r = 1; errno = saved_errno; return r; } + { int r = errno != ENOMEM; errno = saved_errno; return r; } +} diff --git a/runtime/internal/lib/runtime/fault_unwind_llgo.go b/runtime/internal/lib/runtime/fault_unwind_llgo.go new file mode 100644 index 0000000000..6ee21728d9 --- /dev/null +++ b/runtime/internal/lib/runtime/fault_unwind_llgo.go @@ -0,0 +1,209 @@ +//go:build !baremetal && !wasm + +package runtime + +import ( + "unsafe" + + c "github.com/goplus/llgo/runtime/internal/clite" + rtdebug "github.com/goplus/llgo/runtime/internal/runtime" +) + +// Hardware-fault stacks: a SA_SIGINFO handler captures the interrupted +// context (the handler's own frame-pointer chain dead-ends at the signal +// trampoline), unwinds it — dynamically-resolved libunwind first, which +// survives C frames without frame pointers and (nongnu flavor) names +// static C symbols, then the FP chain — and converts the fault into the +// usual Go panic. The unrecovered traceback prints the fault-site chain: +// C frames down through the Go callers. + +//go:linkname c_installFaultHandler C.llgo_install_fault_handler +func c_installFaultHandler(cb func(uintptr, uintptr, int32)) + +//go:linkname c_dynunwindPCBuf C.llgo_dynunwind_pcbuf +func c_dynunwindPCBuf() unsafe.Pointer + +//go:linkname c_dynunwindPCCount C.llgo_dynunwind_pccount +func c_dynunwindPCCount() int32 + +//go:linkname c_dynunwindEndFP C.llgo_dynunwind_endfp +func c_dynunwindEndFP() uintptr + +//go:linkname c_dynunwindName C.llgo_dynunwind_name +func c_dynunwindName(i int32) *c.Char + +//go:linkname c_memReadable C.llgo_mem_readable +func c_memReadable(p unsafe.Pointer) int32 + +//go:linkname c_faultCaptureDone C.llgo_fault_capture_done +func c_faultCaptureDone() + +func memReadable(addr uintptr) bool { + return c_memReadable(unsafe.Pointer(addr)) != 0 +} + +func init() { + c_installFaultHandler(onFault) +} + +// Fault snapshot: written once in the fault handler, consumed by the +// panic traceback (the process either recovers — dropping the snapshot's +// relevance — or dies printing it; a concurrent fault on another thread +// is a lost race on a doomed process). +var ( + faultPCs [64]uintptr + faultN int32 + // index into the dynunwind name table per pc, -1 when the pc came + // from the FP-chain resume. + faultNameIdx [64]int32 +) + +func onFault(pc, fp uintptr, sig int32) { + if fpUnwindAvailable() { + n := 0 + if dn := int(c_dynunwindPCCount()); dn > 0 { + buf := (*[64]uintptr)(c_dynunwindPCBuf()) + if dn > len(faultPCs) { + dn = len(faultPCs) + } + for i := 0; i < dn; i++ { + faultPCs[i] = buf[i] + faultNameIdx[i] = int32(i) + } + // Frame 0 is the fault pc itself; +1 keeps the pc-1 + // return-address convention landing on the faulting + // instruction (gc's sigpanic does the same). + faultPCs[0]++ + n = dn + // libunwind stops where unwind info runs out; resume with the + // FP chain from its final cursor position. + if efp := c_dynunwindEndFP(); efp != 0 && n < len(faultPCs) { + m := fpWalkFrom(efp, faultPCs[n:]) + if m > 0 && faultPCs[n] == faultPCs[n-1] { + copy(faultPCs[n:], faultPCs[n+1:n+m]) + m-- + } + for i := 0; i < m; i++ { + faultNameIdx[n+i] = -1 + } + n += m + } + } else { + if pc != 0 { + faultPCs[0] = pc + 1 + faultNameIdx[0] = -1 + n = 1 + } + m := fpWalkFrom(fp, faultPCs[n:]) + for i := 0; i < m; i++ { + faultNameIdx[n+i] = -1 + } + n += m + } + faultN = int32(n) + } + // Capture done: re-arm the recursion guard before this fault turns + // into an ordinary (recoverable) panic. + c_faultCaptureDone() + rtdebug.PanicSignal(int(sig)) +} + +// fpWalkFrom walks the frame-pointer chain from an arbitrary frame pointer +// (a fault context's fp). Chain-discipline guards only: it runs in signal +// context where the first-use frame tables cannot be built, so the +// program-text bound is applied at print time. +func fpWalkFrom(fp uintptr, pc []uintptr) int { + n := 0 + const maxFrames = 4096 + wordSize := unsafe.Sizeof(uintptr(0)) + for i := 0; fp != 0 && n < len(pc) && i < maxFrames; i++ { + if fp&(wordSize-1) != 0 || !memReadable(fp) || !memReadable(fp+wordSize) { + break + } + prev := *(*uintptr)(unsafe.Pointer(fp)) + ret := *(*uintptr)(unsafe.Pointer(fp + unsafe.Sizeof(uintptr(0)))) + if ret < minLegalPC { + break + } + pc[n] = ret + n++ + if prev <= fp || prev-fp > maxFPStride || prev&(unsafe.Sizeof(uintptr(0))-1) != 0 { + break + } + fp = prev + } + return n +} + +func stringContainsDot(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] == '.' { + return true + } + } + return false +} + +// faultTraceback prints the gc-style traceback for an unrecovered panic +// that originated in a hardware fault; other panics fall back to the +// clite dump (reports false). +func faultTraceback(skip int) bool { + if faultN == 0 || !fpUnwindAvailable() { + return false + } + initRuntimeFuncPCFrames() + print("goroutine 1 [running]:\n") + printed := 0 + printedInText := 0 + for i := 0; i < int(faultN); i++ { + pc := faultPCs[i] + inText := prebuiltTextContains(pc) + sym := frameSymbol(pc - 1) + name := sym.function + if faultNameIdx[i] >= 0 { + // libunwind's proc name: the nongnu flavor reads .symtab and + // names static C symbols dladdr cannot see. A dot-less name is + // a C symbol — prefer it over the table's nearest-below + // attribution, which cannot see foreign functions linked + // between Go functions and misnames them. + if dyn := c.GoString(c_dynunwindName(faultNameIdx[i])); dyn != "" { + if name == "" || !stringContainsDot(dyn) { + name = dyn + sym.file = "" + sym.line = 0 + sym.entry = 0 + } + } + } + if !inText { + // Frames outside the program's own text: keep leading library + // frames (the fault may sit inside libc/libssl), but once the + // module's frames have been shown the rest is startup plumbing + // below main (__libc_start_main, _start) — cut it. Unnamed + // out-of-text slots are unidentifiable either way. + if printedInText > 0 || name == "" { + break + } + } + if name == "" { + name = unknownFunctionName(pc) + } + print(name, "(...)\n\t") + if sym.file == "" { + // No line info (C frame): print the raw pc — resolvable + // offline (addr2line/atos) — instead of a ???:0 placeholder. + print("pc=0x", string(appendHexUint(nil, pc-1))) + } else { + print(sym.file, ":", sym.line) + if sym.entry != 0 && pc >= sym.entry { + print(" +0x", string(appendHexUint(nil, pc-sym.entry))) + } + } + print("\n") + printed++ + if inText { + printedInText++ + } + } + return printed > 0 +} diff --git a/runtime/internal/lib/runtime/runtime_default.go b/runtime/internal/lib/runtime/runtime_default.go index ae2afb53c8..914a34e5be 100644 --- a/runtime/internal/lib/runtime/runtime_default.go +++ b/runtime/internal/lib/runtime/runtime_default.go @@ -8,7 +8,7 @@ import ( const ( LLGoPackage = "link" - LLGoFiles = "_wrap/runtime.c; _wrap/debugtrap.c" + LLGoFiles = "_wrap/runtime.c; _wrap/debugtrap.c; _wrap/fault.c; _wrap/dynunwind.c" ) //go:linkname c_maxprocs C.llgo_maxprocs diff --git a/runtime/internal/lib/runtime/unwind_llgo.go b/runtime/internal/lib/runtime/unwind_llgo.go index c76e3dec0b..f4811442e0 100644 --- a/runtime/internal/lib/runtime/unwind_llgo.go +++ b/runtime/internal/lib/runtime/unwind_llgo.go @@ -25,6 +25,12 @@ func hasPrefix(s, prefix string) bool { // (caller falls back to the clite dladdr dump) when the FP walk or the // tables are unavailable. func panicTraceback(skip int) bool { + // Hardware-fault panics carry a fault-site pc snapshot; print that + // chain (fault pc through the Go callers) instead of walking the + // live stack, whose walk would start inside the fault plumbing. + if faultTraceback(skip) { + return true + } if !fpUnwindAvailable() { return false } diff --git a/runtime/internal/runtime/z_rt.go b/runtime/internal/runtime/z_rt.go index 93fbed7bd3..f5a9e78ff3 100644 --- a/runtime/internal/runtime/z_rt.go +++ b/runtime/internal/runtime/z_rt.go @@ -86,14 +86,22 @@ func TracePanic(v any) { println("\n") } -// PanicTraceback, when set by the public runtime package, prints a Go-style -// stack trace (function + file:line per frame) for an unrecovered panic and -// reports whether it printed anything. skip counts physical frames above the -// runtime plumbing frame that invokes the hook, matching clite -// debug.PrintStack's convention. When unset or when it reports false, the -// caller falls back to the clite frame dump. +// PanicTraceback, when set by the public runtime package, prints a +// Go-style stack trace for an unrecovered panic and reports whether it +// printed anything; the clite frame dump remains the fallback. var PanicTraceback func(skip int) bool +// PanicSignal converts a hardware signal into the same Go panic the +// legacy signal handler raised. +func PanicSignal(sig int) { + switch sig { + case 8: // SIGFPE + panic(errorString("integer divide by zero")) + default: // SIGSEGV, SIGBUS + panic(errorString("invalid memory address or nil pointer dereference")) + } +} + /* func stringTracef(fp c.FilePtr, format *c.Char, s String) { cs := c.Alloca(uintptr(s.len) + 1) diff --git a/test/go/fault_unwind_test.go b/test/go/fault_unwind_test.go new file mode 100644 index 0000000000..0f6f6a4877 --- /dev/null +++ b/test/go/fault_unwind_test.go @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Hardware faults in C code called from Go convert to recoverable Go +// panics, and the unrecovered traceback shows the fault-site chain — C +// frames down through the Go callers — captured from the signal ucontext +// (dynamically-resolved libunwind first, FP chain otherwise). +package gotest + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" + "testing" +) + +const faultProbeMain = `package main + +import ( + "fmt" + "os" + _ "unsafe" +) + +const ( + LLGoFiles = "wrap/fault.c" +) + +//go:linkname cexcSegv C.cexc_segv +func cexcSegv(depth int32) + +//go:noinline +func viaGo() { cexcSegv(2) } + +func one() { + defer func() { + r := recover() + err, ok := r.(error) + if !ok || err.Error() != "runtime error: invalid memory address or nil pointer dereference" { + panic(r) + } + }() + viaGo() +} + +func main() { + if len(os.Args) > 1 && os.Args[1] == "recover" { + // Three sequential faults: a handler leaving the signal blocked + // after the longjmp escape survives the first and cores on the + // second (the SA_NODEFER regression). + one() + one() + one() + fmt.Println("FAULT_RECOVER_OK") + return + } + viaGo() + fmt.Println("survived") +} +` + +const faultProbeC = `#include + +/* volatile: a bare NULL store is UB and clang would propagate it into + * "unreachable", turning the recursion into an infinite loop. */ +static int32_t *volatile cexc_null; +volatile int32_t cexc_marks; + +void cexc_leaf(void) { *cexc_null = 42; } + +void cexc_mid(int32_t depth) { + if (depth > 0) { + cexc_mid(depth - 1); + cexc_marks++; + return; + } + cexc_leaf(); + cexc_marks++; +} + +void cexc_segv(int32_t depth) { + cexc_mid(depth); + cexc_marks++; +} +` + +var ( + faultLLGoOnce sync.Once + faultLLGoBin string + faultLLGoErr string +) + +func faultLLGo(t *testing.T) string { + t.Helper() + repoRoot := findStringConversionRepoRoot(t) + t.Setenv("LLGO_ROOT", repoRoot) + faultLLGoOnce.Do(func() { + tmp, err := os.MkdirTemp("", "llgo-fault-bin") + if err != nil { + faultLLGoErr = err.Error() + return + } + bin := filepath.Join(tmp, "llgo") + build := exec.Command("go", "build", "-o", bin, "./cmd/llgo") + build.Dir = repoRoot + if out, berr := build.CombinedOutput(); berr != nil { + faultLLGoErr = berr.Error() + "\n" + string(out) + return + } + faultLLGoBin = bin + }) + if faultLLGoErr != "" { + t.Fatalf("building llgo failed: %s", faultLLGoErr) + } + return faultLLGoBin +} + +func writeFaultProbe(t *testing.T) string { + t.Helper() + dir := t.TempDir() + for name, content := range map[string]string{ + "main.go": faultProbeMain, + "go.mod": "module faultprobe\n\ngo 1.21\n", + "wrap/fault.c": faultProbeC, + } { + path := filepath.Join(dir, name) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + return dir +} + +func TestCFaultRecoverable(t *testing.T) { + bin := faultLLGo(t) + dir := writeFaultProbe(t) + cmd := exec.Command(bin, "run", ".", "recover") + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("fault probe died on the signal: %v\n%s", err, out) + } + if !strings.Contains(string(out), "FAULT_RECOVER_OK") { + t.Fatalf("recover marker missing:\n%s", out) + } +} + +func TestCFaultTraceback(t *testing.T) { + bin := faultLLGo(t) + dir := writeFaultProbe(t) + cmd := exec.Command(bin, "run", ".") + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatalf("unrecovered fault probe unexpectedly succeeded:\n%s", out) + } + wants := []string{ + "panic: runtime error: invalid memory address or nil pointer dereference", + "goroutine 1 [running]:", + "main.go:", // the Go caller frames carry file:line + } + if runtime.GOOS == "darwin" { + // C frame names come from dladdr/libunwind; linux runners may + // lack a .symtab-reading libunwind flavor. + wants = append(wants, "cexc_segv") + } + for _, want := range wants { + if !strings.Contains(string(out), want) { + t.Fatalf("fault traceback missing %q:\n%s", want, out) + } + } +}