From 29b4c6723f1d5e07804b48ad45be97daa24dfed9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 10 May 2026 15:56:07 -0700 Subject: [PATCH] runtime: make divide-by-zero and nil dereference panics recoverable Modify the Unix signal handler to redirect execution to a Go sigpanic function instead of printing an error and re-raising the signal. The C signal handler modifies the ucontext to make the faulting instruction appear to have called tinygo_sigpanic, which then calls runtimePanic with the appropriate message. Supported on all architectures TinyGo targets on Linux and Darwin: x86_64, i386, aarch64, ARM, and MIPS. Also registers SIGFPE, which was previously not handled at all. --- src/runtime/arch_386.go | 1 + src/runtime/arch_amd64.go | 1 + src/runtime/arch_arm.go | 1 + src/runtime/arch_arm64.go | 1 + src/runtime/arch_mips.go | 1 + src/runtime/arch_mipsle.go | 1 + src/runtime/os_darwin.go | 1 + src/runtime/os_linux.go | 1 + src/runtime/runtime_unix.c | 91 +++++++++++++++++++++++++++++++++++++ src/runtime/runtime_unix.go | 20 ++++++++ testdata/recover.go | 15 ++++++ testdata/recover.txt | 4 ++ 12 files changed, 138 insertions(+) diff --git a/src/runtime/arch_386.go b/src/runtime/arch_386.go index 90ec8e8ba..43130a8fe 100644 --- a/src/runtime/arch_386.go +++ b/src/runtime/arch_386.go @@ -12,6 +12,7 @@ const callInstSize = 5 // "call someFunction" is 5 bytes const ( linux_MAP_ANONYMOUS = 0x20 linux_SIGBUS = 7 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_amd64.go b/src/runtime/arch_amd64.go index 436d6e384..09985cf51 100644 --- a/src/runtime/arch_amd64.go +++ b/src/runtime/arch_amd64.go @@ -12,6 +12,7 @@ const callInstSize = 5 // "call someFunction" is 5 bytes const ( linux_MAP_ANONYMOUS = 0x20 linux_SIGBUS = 7 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_arm.go b/src/runtime/arch_arm.go index ea6b540d2..65e0502f7 100644 --- a/src/runtime/arch_arm.go +++ b/src/runtime/arch_arm.go @@ -14,6 +14,7 @@ const callInstSize = 4 // "bl someFunction" is 4 bytes const ( linux_MAP_ANONYMOUS = 0x20 linux_SIGBUS = 7 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_arm64.go b/src/runtime/arch_arm64.go index 6d3c856cf..3da65dc6d 100644 --- a/src/runtime/arch_arm64.go +++ b/src/runtime/arch_arm64.go @@ -12,6 +12,7 @@ const callInstSize = 4 // "bl someFunction" is 4 bytes const ( linux_MAP_ANONYMOUS = 0x20 linux_SIGBUS = 7 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_mips.go b/src/runtime/arch_mips.go index 5a7d05c89..e83e8c197 100644 --- a/src/runtime/arch_mips.go +++ b/src/runtime/arch_mips.go @@ -12,6 +12,7 @@ const callInstSize = 8 // "jal someFunc" is 4 bytes, plus a MIPS delay slot const ( linux_MAP_ANONYMOUS = 0x800 linux_SIGBUS = 10 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/arch_mipsle.go b/src/runtime/arch_mipsle.go index 498cf862b..e391b3669 100644 --- a/src/runtime/arch_mipsle.go +++ b/src/runtime/arch_mipsle.go @@ -12,6 +12,7 @@ const callInstSize = 8 // "jal someFunc" is 4 bytes, plus a MIPS delay slot const ( linux_MAP_ANONYMOUS = 0x800 linux_SIGBUS = 10 + linux_SIGFPE = 8 linux_SIGILL = 4 linux_SIGSEGV = 11 ) diff --git a/src/runtime/os_darwin.go b/src/runtime/os_darwin.go index 7197c4397..b6e56863b 100644 --- a/src/runtime/os_darwin.go +++ b/src/runtime/os_darwin.go @@ -24,6 +24,7 @@ const ( // https://opensource.apple.com/source/xnu/xnu-7195.141.2/bsd/sys/signal.h.auto.html const ( sig_SIGBUS = 10 + sig_SIGFPE = 8 sig_SIGILL = 4 sig_SIGSEGV = 11 ) diff --git a/src/runtime/os_linux.go b/src/runtime/os_linux.go index 0ae105c5f..e76fc27f6 100644 --- a/src/runtime/os_linux.go +++ b/src/runtime/os_linux.go @@ -27,6 +27,7 @@ const ( const ( sig_SIGBUS = linux_SIGBUS + sig_SIGFPE = linux_SIGFPE sig_SIGILL = linux_SIGILL sig_SIGSEGV = linux_SIGSEGV ) diff --git a/src/runtime/runtime_unix.c b/src/runtime/runtime_unix.c index 79dd7ce91..f6ed030e4 100644 --- a/src/runtime/runtime_unix.c +++ b/src/runtime/runtime_unix.c @@ -12,8 +12,98 @@ void tinygo_handle_fatal_signal(int sig, uintptr_t addr); +// tinygo_sigpanic is defined in Go. It turns a signal into a Go panic +// that can be recovered with recover(). The signal number is passed via +// the tinygo_caught_signal global. +void tinygo_sigpanic(void); + +// Set by the signal handler before redirecting to tinygo_sigpanic. +int tinygo_caught_signal; + +// Whether sigpanic-based recovery is supported for the current +// architecture. Set to 0 on architectures where we can't reliably +// modify the ucontext to redirect execution. +static int can_sigpanic(void) { +#if defined(__x86_64__) || defined(__i386__) || defined(__aarch64__) || defined(__arm64__) || defined(__arm__) || defined(__mips__) + return 1; +#else + return 0; +#endif +} + +// Try to redirect execution from the signal handler to tinygo_sigpanic. +// Returns 1 on success, 0 if the architecture doesn't support it. +static int redirect_to_sigpanic(int sig, ucontext_t *uctx) { + if (!can_sigpanic()) { + return 0; + } + tinygo_caught_signal = sig; + +#if __APPLE__ + #if __arm64__ + // ARM64: set LR to the faulting PC (as return address), set PC to sigpanic + uctx->uc_mcontext->__ss.__lr = uctx->uc_mcontext->__ss.__pc; + uctx->uc_mcontext->__ss.__pc = (uint64_t)&tinygo_sigpanic; + #elif __x86_64__ + // x86_64: push the faulting PC onto the stack, set RIP to sigpanic + uintptr_t sp = uctx->uc_mcontext->__ss.__rsp; + sp -= sizeof(uintptr_t); + *(uintptr_t *)sp = uctx->uc_mcontext->__ss.__rip; + uctx->uc_mcontext->__ss.__rsp = sp; + uctx->uc_mcontext->__ss.__rip = (uint64_t)&tinygo_sigpanic; + #else + return 0; + #endif +#elif __linux__ + #if __x86_64__ + uintptr_t sp = uctx->uc_mcontext.gregs[REG_RSP]; + sp -= sizeof(uintptr_t); + *(uintptr_t *)sp = uctx->uc_mcontext.gregs[REG_RIP]; + uctx->uc_mcontext.gregs[REG_RSP] = sp; + uctx->uc_mcontext.gregs[REG_RIP] = (uintptr_t)&tinygo_sigpanic; + #elif __i386__ + uintptr_t sp = uctx->uc_mcontext.gregs[REG_ESP]; + sp -= sizeof(uintptr_t); + *(uintptr_t *)sp = uctx->uc_mcontext.gregs[REG_EIP]; + uctx->uc_mcontext.gregs[REG_ESP] = sp; + uctx->uc_mcontext.gregs[REG_EIP] = (uintptr_t)&tinygo_sigpanic; + #elif __aarch64__ + uctx->uc_mcontext.regs[30] = uctx->uc_mcontext.pc; // LR = faulting PC + uctx->uc_mcontext.pc = (uintptr_t)&tinygo_sigpanic; + #elif __arm__ + uctx->uc_mcontext.arm_lr = uctx->uc_mcontext.arm_pc; + uctx->uc_mcontext.arm_pc = (uintptr_t)&tinygo_sigpanic; + #elif defined(__mips__) + // MIPS: set RA (gregs[31]) to the faulting PC, set PC to sigpanic. + uctx->uc_mcontext.gregs[31] = uctx->uc_mcontext.pc; + uctx->uc_mcontext.pc = (uintptr_t)&tinygo_sigpanic; + #else + return 0; + #endif +#else + return 0; +#endif + + return 1; +} + static void signal_handler(int sig, siginfo_t *info, void *context) { ucontext_t* uctx = context; + + // Try to redirect to sigpanic for a recoverable panic. + if (redirect_to_sigpanic(sig, uctx)) { + // Re-register the signal handler since SA_RESETHAND cleared it. + // We need it active in case the sigpanic itself faults (e.g., + // stack overflow during panic). + struct sigaction act; + memset(&act, 0, sizeof(act)); + act.sa_flags = SA_SIGINFO | SA_RESETHAND; + act.sa_sigaction = &signal_handler; + sigaction(sig, &act, NULL); + return; // return from signal handler; execution resumes at sigpanic + } + + // Fallback: extract the faulting address and call the fatal handler. uintptr_t addr = 0; #if __APPLE__ #if __arm64__ @@ -51,6 +141,7 @@ void tinygo_register_fatal_signals(void) { // Register the signal handler for common issues. There are more signals, // which can be added if needed. sigaction(SIGBUS, &act, NULL); + sigaction(SIGFPE, &act, NULL); sigaction(SIGILL, &act, NULL); sigaction(SIGSEGV, &act, NULL); } diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index 99f28411f..0e2654941 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -139,6 +139,26 @@ func runMain() { //export tinygo_register_fatal_signals func tinygo_register_fatal_signals() +//go:extern tinygo_caught_signal +var tinygo_caught_signal int32 + +// tinygo_sigpanic is called when a signal (SIGSEGV, SIGFPE, etc.) is caught +// and the signal handler has redirected execution here. It turns the signal +// into a Go panic that can be recovered with recover(). +// +//export tinygo_sigpanic +func tinygo_sigpanic() { + sig := tinygo_caught_signal + switch sig { + case sig_SIGSEGV, sig_SIGBUS: + runtimePanic("nil pointer dereference") + case sig_SIGFPE: + runtimePanic("divide by zero") + default: + runtimePanic("signal") + } +} + // Print fatal errors when they happen, including the instruction location. // With the particular formatting below, `tinygo run` can extract the location // where the signal happened and try to show the source location based on DWARF diff --git a/testdata/recover.go b/testdata/recover.go index 1b7791d8b..619bfa01a 100644 --- a/testdata/recover.go +++ b/testdata/recover.go @@ -44,6 +44,9 @@ func main() { println("\n# recover from nil map and closed channel") recoverNilMapAndChan() + + println("\n# recover from hardware signals") + recoverSignals() } func recoverSimple() { @@ -261,3 +264,15 @@ func recoverNilMapAndChan() { close(ch) }) } + +// Test recovering from hardware signals (SIGFPE, SIGSEGV). +func recoverSignals() { + recoverMustPanic("divide by zero", func() { + var x int + println(1 / x) + }) + recoverMustPanic("nil pointer dereference", func() { + var p *int + println(*p) + }) +} diff --git a/testdata/recover.txt b/testdata/recover.txt index 18ae7bad9..693ddf10a 100644 --- a/testdata/recover.txt +++ b/testdata/recover.txt @@ -49,3 +49,7 @@ outer recovered: repanic value recovered: nil map recovered: closed chan recovered: close nil chan + +# recover from hardware signals + recovered: divide by zero + recovered: nil pointer dereference