mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-04 02:57:46 +00:00
compiler, runtime: make runtime panics recoverable
Emit fault checkpoints around compiler-generated runtime assertions so runtimePanicAt can unwind through the existing defer/recover machinery instead of aborting. This lets panics from bounds checks, type checks, and other compiler-inserted runtime checks be recovered by deferred functions. Mark functions that call recover as noinline. Inlining such a function into a deferred closure can make recover observe the wrong call context and report success when it should return nil. Addresses tinygo-org/tinygo issues 2759 and 3510.
This commit is contained in:
Vendored
+106
@@ -30,8 +30,17 @@ func main() {
|
||||
println("\n# defer panic")
|
||||
deferPanic()
|
||||
|
||||
println("\n# indirect recover")
|
||||
indirectRecover()
|
||||
|
||||
println("\n# runtime.Goexit")
|
||||
runtimeGoexit()
|
||||
|
||||
println("\n# repanic")
|
||||
recoverRepanic()
|
||||
|
||||
println("\n# recover runtime errors")
|
||||
recoverRuntimeError()
|
||||
}
|
||||
|
||||
func recoverSimple() {
|
||||
@@ -114,6 +123,24 @@ func deferPanic() {
|
||||
println("defer panic")
|
||||
}
|
||||
|
||||
// TODO: Go only allows recover() to succeed when called directly from a
|
||||
// deferred function. Update this test once runtime recover can distinguish it.
|
||||
func indirectRecover() {
|
||||
defer func() {
|
||||
if r := indirectRecoverHelper(); r == nil {
|
||||
println("indirect recover returned nil")
|
||||
} else {
|
||||
printitf("indirect recover returned:", r)
|
||||
}
|
||||
}()
|
||||
panic("indirect panic")
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func indirectRecoverHelper() interface{} {
|
||||
return recover()
|
||||
}
|
||||
|
||||
func runtimeGoexit() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
@@ -127,6 +154,27 @@ func runtimeGoexit() {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Test that a repanic inside a deferred function propagates correctly
|
||||
// instead of re-running the same defer. This is a regression test for
|
||||
// tinygo-org/tinygo issue 3449.
|
||||
func recoverRepanic() {
|
||||
// Two defers: inner recovers and repanics, outer should catch it.
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
printitf("outer recovered:", r)
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
println("inner, repanicking")
|
||||
panic(r)
|
||||
}
|
||||
}()
|
||||
panic("repanic value")
|
||||
}
|
||||
|
||||
func printitf(msg string, itf interface{}) {
|
||||
switch itf := itf.(type) {
|
||||
case string:
|
||||
@@ -135,3 +183,61 @@ func printitf(msg string, itf interface{}) {
|
||||
println(msg, itf)
|
||||
}
|
||||
}
|
||||
|
||||
// Test recovering from runtime errors (bounds checks, type assertions, etc.)
|
||||
func recoverRuntimeError() {
|
||||
recoverMustPanic("index", func() {
|
||||
s := make([]int, 5)
|
||||
_ = s[99]
|
||||
})
|
||||
recoverMustPanic("index from helper", func() {
|
||||
s := []byte{1}
|
||||
_ = readOutOfBounds(s)
|
||||
})
|
||||
recoverMustPanic("slice", func() {
|
||||
s := make([]int, 5)
|
||||
_ = s[3:99]
|
||||
})
|
||||
recoverMustPanic("type assert", func() {
|
||||
var x interface{} = 1
|
||||
_ = x.(string)
|
||||
})
|
||||
recoverEmptyInterfaceTypeAssert()
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func readOutOfBounds(s []byte) byte {
|
||||
return s[2]
|
||||
}
|
||||
|
||||
func recoverEmptyInterfaceTypeAssert() {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
println(" failed empty interface type assert")
|
||||
} else {
|
||||
println(" recovered: empty interface type assert")
|
||||
}
|
||||
}()
|
||||
var intf interface{} = 3
|
||||
typed := intf.(interface{})
|
||||
useEmptyInterface(typed)
|
||||
}
|
||||
|
||||
func useEmptyInterface(typed interface{}) {
|
||||
if typed.(int) != 3 {
|
||||
println(" failed empty interface value")
|
||||
}
|
||||
}
|
||||
|
||||
func recoverMustPanic(name string, f func()) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
println(" recovered:", name)
|
||||
} else {
|
||||
println(" failed to recover:", name)
|
||||
}
|
||||
}()
|
||||
f()
|
||||
}
|
||||
|
||||
Vendored
+14
@@ -28,5 +28,19 @@ recovered: panic 2
|
||||
defer panic
|
||||
recovered from deferred call: deferred panic
|
||||
|
||||
# indirect recover
|
||||
indirect recover returned: indirect panic
|
||||
|
||||
# runtime.Goexit
|
||||
Goexit deferred function, recover is nil: true
|
||||
|
||||
# repanic
|
||||
inner, repanicking
|
||||
outer recovered: repanic value
|
||||
|
||||
# recover runtime errors
|
||||
recovered: index
|
||||
recovered: index from helper
|
||||
recovered: slice
|
||||
recovered: type assert
|
||||
recovered: empty interface type assert
|
||||
|
||||
Reference in New Issue
Block a user