mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-16 02:33:28 +00:00
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.
This commit is contained in:
@@ -12,6 +12,7 @@ const callInstSize = 5 // "call someFunction" is 5 bytes
|
|||||||
const (
|
const (
|
||||||
linux_MAP_ANONYMOUS = 0x20
|
linux_MAP_ANONYMOUS = 0x20
|
||||||
linux_SIGBUS = 7
|
linux_SIGBUS = 7
|
||||||
|
linux_SIGFPE = 8
|
||||||
linux_SIGILL = 4
|
linux_SIGILL = 4
|
||||||
linux_SIGSEGV = 11
|
linux_SIGSEGV = 11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const callInstSize = 5 // "call someFunction" is 5 bytes
|
|||||||
const (
|
const (
|
||||||
linux_MAP_ANONYMOUS = 0x20
|
linux_MAP_ANONYMOUS = 0x20
|
||||||
linux_SIGBUS = 7
|
linux_SIGBUS = 7
|
||||||
|
linux_SIGFPE = 8
|
||||||
linux_SIGILL = 4
|
linux_SIGILL = 4
|
||||||
linux_SIGSEGV = 11
|
linux_SIGSEGV = 11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const callInstSize = 4 // "bl someFunction" is 4 bytes
|
|||||||
const (
|
const (
|
||||||
linux_MAP_ANONYMOUS = 0x20
|
linux_MAP_ANONYMOUS = 0x20
|
||||||
linux_SIGBUS = 7
|
linux_SIGBUS = 7
|
||||||
|
linux_SIGFPE = 8
|
||||||
linux_SIGILL = 4
|
linux_SIGILL = 4
|
||||||
linux_SIGSEGV = 11
|
linux_SIGSEGV = 11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const callInstSize = 4 // "bl someFunction" is 4 bytes
|
|||||||
const (
|
const (
|
||||||
linux_MAP_ANONYMOUS = 0x20
|
linux_MAP_ANONYMOUS = 0x20
|
||||||
linux_SIGBUS = 7
|
linux_SIGBUS = 7
|
||||||
|
linux_SIGFPE = 8
|
||||||
linux_SIGILL = 4
|
linux_SIGILL = 4
|
||||||
linux_SIGSEGV = 11
|
linux_SIGSEGV = 11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const callInstSize = 8 // "jal someFunc" is 4 bytes, plus a MIPS delay slot
|
|||||||
const (
|
const (
|
||||||
linux_MAP_ANONYMOUS = 0x800
|
linux_MAP_ANONYMOUS = 0x800
|
||||||
linux_SIGBUS = 10
|
linux_SIGBUS = 10
|
||||||
|
linux_SIGFPE = 8
|
||||||
linux_SIGILL = 4
|
linux_SIGILL = 4
|
||||||
linux_SIGSEGV = 11
|
linux_SIGSEGV = 11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const callInstSize = 8 // "jal someFunc" is 4 bytes, plus a MIPS delay slot
|
|||||||
const (
|
const (
|
||||||
linux_MAP_ANONYMOUS = 0x800
|
linux_MAP_ANONYMOUS = 0x800
|
||||||
linux_SIGBUS = 10
|
linux_SIGBUS = 10
|
||||||
|
linux_SIGFPE = 8
|
||||||
linux_SIGILL = 4
|
linux_SIGILL = 4
|
||||||
linux_SIGSEGV = 11
|
linux_SIGSEGV = 11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const (
|
|||||||
// https://opensource.apple.com/source/xnu/xnu-7195.141.2/bsd/sys/signal.h.auto.html
|
// https://opensource.apple.com/source/xnu/xnu-7195.141.2/bsd/sys/signal.h.auto.html
|
||||||
const (
|
const (
|
||||||
sig_SIGBUS = 10
|
sig_SIGBUS = 10
|
||||||
|
sig_SIGFPE = 8
|
||||||
sig_SIGILL = 4
|
sig_SIGILL = 4
|
||||||
sig_SIGSEGV = 11
|
sig_SIGSEGV = 11
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
sig_SIGBUS = linux_SIGBUS
|
sig_SIGBUS = linux_SIGBUS
|
||||||
|
sig_SIGFPE = linux_SIGFPE
|
||||||
sig_SIGILL = linux_SIGILL
|
sig_SIGILL = linux_SIGILL
|
||||||
sig_SIGSEGV = linux_SIGSEGV
|
sig_SIGSEGV = linux_SIGSEGV
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,8 +12,98 @@
|
|||||||
|
|
||||||
void tinygo_handle_fatal_signal(int sig, uintptr_t addr);
|
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) {
|
static void signal_handler(int sig, siginfo_t *info, void *context) {
|
||||||
ucontext_t* uctx = 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;
|
uintptr_t addr = 0;
|
||||||
#if __APPLE__
|
#if __APPLE__
|
||||||
#if __arm64__
|
#if __arm64__
|
||||||
@@ -51,6 +141,7 @@ void tinygo_register_fatal_signals(void) {
|
|||||||
// Register the signal handler for common issues. There are more signals,
|
// Register the signal handler for common issues. There are more signals,
|
||||||
// which can be added if needed.
|
// which can be added if needed.
|
||||||
sigaction(SIGBUS, &act, NULL);
|
sigaction(SIGBUS, &act, NULL);
|
||||||
|
sigaction(SIGFPE, &act, NULL);
|
||||||
sigaction(SIGILL, &act, NULL);
|
sigaction(SIGILL, &act, NULL);
|
||||||
sigaction(SIGSEGV, &act, NULL);
|
sigaction(SIGSEGV, &act, NULL);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,26 @@ func runMain() {
|
|||||||
//export tinygo_register_fatal_signals
|
//export tinygo_register_fatal_signals
|
||||||
func 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.
|
// Print fatal errors when they happen, including the instruction location.
|
||||||
// With the particular formatting below, `tinygo run` can extract the 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
|
// where the signal happened and try to show the source location based on DWARF
|
||||||
|
|||||||
Vendored
+15
@@ -44,6 +44,9 @@ func main() {
|
|||||||
|
|
||||||
println("\n# recover from nil map and closed channel")
|
println("\n# recover from nil map and closed channel")
|
||||||
recoverNilMapAndChan()
|
recoverNilMapAndChan()
|
||||||
|
|
||||||
|
println("\n# recover from hardware signals")
|
||||||
|
recoverSignals()
|
||||||
}
|
}
|
||||||
|
|
||||||
func recoverSimple() {
|
func recoverSimple() {
|
||||||
@@ -261,3 +264,15 @@ func recoverNilMapAndChan() {
|
|||||||
close(ch)
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+4
@@ -49,3 +49,7 @@ outer recovered: repanic value
|
|||||||
recovered: nil map
|
recovered: nil map
|
||||||
recovered: closed chan
|
recovered: closed chan
|
||||||
recovered: close nil chan
|
recovered: close nil chan
|
||||||
|
|
||||||
|
# recover from hardware signals
|
||||||
|
recovered: divide by zero
|
||||||
|
recovered: nil pointer dereference
|
||||||
|
|||||||
Reference in New Issue
Block a user