Files
tinygo/src/runtime/panic.go
T
Jake Bailey 16fc1ea2bb runtime: make fatal failures unrecoverable
Match the Go runtime by terminating for deadlocks, stack overflows,
runtime and GC invariants, invalid lock operations, and platform
initialization failures instead of routing them through panic/recover.

Keep language-level runtime errors and unsupported user operations
recoverable. Add crash coverage that verifies fatal errors bypass deferred
recover calls.
2026-07-25 07:14:00 +02:00

277 lines
8.9 KiB
Go

package runtime
import (
"internal/task"
"runtime/interrupt"
"tinygo"
"unsafe"
)
// trap is a compiler hint that this function cannot be executed. It is
// translated into either a trap instruction or a call to abort().
//
//export llvm.trap
func trap()
// Inline assembly stub. It is essentially C longjmp but modified a bit for the
// purposes of TinyGo. It restores the stack pointer and jumps to the given pc.
//
//export tinygo_longjmp
func tinygo_longjmp(frame *deferFrame)
// Compiler intrinsic.
// Returns whether recover is supported on the current architecture.
func supportsRecover() bool
// Compile intrinsic.
// Returns which strategy is used. This is usually "print" but can be changed
// using the -panic= compiler flag.
func panicStrategy() uint8
// DeferFrame is a stack allocated object that stores information for the
// current "defer frame", which is used in functions that use the `defer`
// keyword.
// The compiler knows about the JumpPC struct offset and the DeferPtr field
// index, so they should not be moved without also updating compiler/defer.go.
type deferFrame struct {
JumpSP unsafe.Pointer // stack pointer to return to
JumpPC unsafe.Pointer // pc to return to
ExtraRegs [deferExtraRegs]unsafe.Pointer // extra registers (depending on the architecture)
Previous *deferFrame // previous recover buffer pointer
Panicking panicState // panic/Goexit state
PanicValue interface{} // panic value, might be nil for panic(nil) for example
DeferPtr unsafe.Pointer // head of the stack-allocated defer list
}
type panicState uint8
const (
panicTrue panicState = 1 << iota
panicGoexit
)
// Builtin function panic(msg), used as a compiler intrinsic.
func _panic(message interface{}) {
panicOrGoexit(message, panicTrue)
}
func panicOrGoexit(message interface{}, panicking panicState) {
if panicking != panicGoexit && panicStrategy() == tinygo.PanicStrategyTrap {
trap()
}
// Note: recover is not supported inside interrupts.
// (This could be supported, like defer, but we currently don't).
if supportsRecover() && !interrupt.In() {
frame := (*deferFrame)(task.Current().DeferFrame)
if frame != nil {
frame.PanicValue = message
if panicking&panicTrue != 0 {
panicking |= frame.Panicking & panicGoexit
}
frame.Panicking = panicking
tinygo_longjmp(frame)
// unreachable
}
}
if panicking == panicGoexit {
// Call to Goexit() instead of a panic.
// Exit the goroutine instead of printing a panic message.
goexit()
}
printstring("panic: ")
printitf(message)
printnl()
abort()
}
// Cause a runtime panic, which is (currently) always a string.
func runtimePanic(msg string) {
// As long as this function is inined, llvm.returnaddress(0) will return
// something sensible.
runtimePanicAt(returnAddress(0), msg)
}
// runtimeFatal terminates for runtime failures that cannot safely be
// recovered, such as exhausting the heap.
func runtimeFatal(msg string) {
if panicStrategy() == tinygo.PanicStrategyTrap {
trap()
}
printstring("fatal error: ")
printstring(msg)
printnl()
abort()
}
func runtimePanicAt(addr unsafe.Pointer, msg string) {
if panicStrategy() == tinygo.PanicStrategyTrap {
trap()
}
if supportsRecover() && !interrupt.In() {
frame := (*deferFrame)(task.Current().DeferFrame)
if frame != nil {
// Use the normal panic mechanism so that this runtime error
// can be recovered with recover().
frame.PanicValue = plainError(msg)
frame.Panicking = panicTrue | (frame.Panicking & panicGoexit)
tinygo_longjmp(frame)
// unreachable
}
}
if hasReturnAddr {
// Note: the string "panic: runtime error at " is also used in
// runtime_cortexm_hardfault.go. It is kept the same so that the string
// can be deduplicated by the compiler.
printstring("panic: runtime error at ")
printptr(uintptr(addr) - callInstSize)
printstring(": ")
} else {
printstring("panic: runtime error: ")
}
printstring(msg)
printnl()
abort()
}
// Called at the start of a function that includes a deferred call.
// It gets passed in the stack-allocated defer frame and configures it.
// Note that the frame is not zeroed yet, so we need to initialize all values
// that will be used.
//
//go:inline
//go:nobounds
func setupDeferFrame(frame *deferFrame, jumpSP unsafe.Pointer) {
if interrupt.In() {
// Defer is not currently allowed in interrupts.
// We could add support for this, but since defer might also allocate
// (especially in loops) it might not be a good idea anyway.
runtimeFatal("defer in interrupt")
}
currentTask := task.Current()
frame.Previous = (*deferFrame)(currentTask.DeferFrame)
frame.JumpSP = jumpSP
frame.Panicking = 0
frame.DeferPtr = nil
currentTask.DeferFrame = unsafe.Pointer(frame)
}
// Called right before the return instruction. It pops the defer frame from the
// linked list of defer frames. It also re-raises a panic if the goroutine is
// still panicking.
//
//go:inline
//go:nobounds
func destroyDeferFrame(frame *deferFrame) {
task.Current().DeferFrame = unsafe.Pointer(frame.Previous)
if frame.Panicking&panicTrue != 0 {
// We're still panicking!
// Re-raise the panic now.
panicOrGoexit(frame.PanicValue, panicTrue)
}
if frame.Panicking&panicGoexit != 0 {
// A deferred function panicked during Goexit, and that panic was
// recovered. Continue the original Goexit instead of returning.
panicOrGoexit(nil, panicGoexit)
}
}
// _recover is the built-in recover() function. It tries to recover a currently
// panicking goroutine.
// useParentFrame is set when the caller of runtime._recover has a defer frame
// itself. In that case, recover() shouldn't check that frame but one frame up.
//
// TODO: Go only allows recover() to succeed when called directly from a
// deferred function, not from a sub-call (e.g. defer func() { sub() }() where
// sub() calls recover()). The Go compiler enforces this by walking the stack
// to count frames between gorecover and gopanic. TinyGo currently does not
// have a stack unwinder, so this restriction is not enforced at runtime.
// Functions calling recover() are marked noinline to prevent the most common
// case (inlined sub-call), but non-inlined sub-calls can still incorrectly
// recover. Fixing this properly requires either frame pointer support or a
// lightweight stack unwinder.
func _recover(useParentFrame bool) interface{} {
if !supportsRecover() || interrupt.In() {
// Either we're compiling without stack unwinding support, or we're
// inside an interrupt where panic/recover is not supported. Either way,
// make this a no-op since panic() won't do any long jumps to a deferred
// function.
return nil
}
frame := (*deferFrame)(task.Current().DeferFrame)
if useParentFrame {
// Don't recover panic from the current frame (which can't be panicking
// already), but instead from the previous frame.
frame = frame.Previous
}
if frame != nil && frame.Panicking != 0 {
if frame.Panicking&panicTrue == 0 {
// Special value that indicates we're exiting the goroutine using
// Goexit(). Therefore, make this recover call a no-op.
return nil
}
// Only the first call to recover returns the panic value. It also stops
// the panicking sequence, hence setting panicking to false.
frame.Panicking &^= panicTrue
return frame.PanicValue
}
// Not panicking, so return a nil interface.
return nil
}
// Panic when trying to dereference a nil pointer.
func nilPanic() {
runtimePanicAt(returnAddress(0), "nil pointer dereference")
}
// Panic when trying to add an entry to a nil map
func nilMapPanic() {
runtimePanicAt(returnAddress(0), "assignment to entry in nil map")
}
// Panic when trying to access an array or slice out of bounds.
func lookupPanic() {
runtimePanicAt(returnAddress(0), "index out of range")
}
// Panic when trying to slice a slice out of bounds.
func slicePanic() {
runtimePanicAt(returnAddress(0), "slice out of range")
}
// Panic when trying to convert a slice to an array pointer (Go 1.17+) and the
// slice is shorter than the array.
func sliceToArrayPointerPanic() {
runtimePanicAt(returnAddress(0), "slice smaller than array")
}
// Panic when calling unsafe.Slice() (Go 1.17+) or unsafe.String() (Go 1.20+)
// with a len that's too large (which includes if the ptr is nil and len is
// nonzero).
func unsafeSlicePanic() {
runtimePanicAt(returnAddress(0), "unsafe.Slice/String: len out of range")
}
// Panic when trying to create a new channel that is too big.
func chanMakePanic() {
runtimePanicAt(returnAddress(0), "new channel is too big")
}
// Panic when a shift value is negative.
func negativeShiftPanic() {
runtimePanicAt(returnAddress(0), "negative shift")
}
// Panic when there is a divide by zero.
func divideByZeroPanic() {
runtimePanicAt(returnAddress(0), "divide by zero")
}
func blockingPanic() {
runtimePanicAt(returnAddress(0), "trying to do blocking operation in exported function")
}
//go:linkname fips_fatal crypto/internal/fips140.fatal
func fips_fatal(msg string) {
runtimeFatal(msg)
}