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.
This commit is contained in:
Jake Bailey
2026-07-24 10:49:50 -07:00
committed by Ron Evans
parent 9105cce340
commit 16fc1ea2bb
41 changed files with 144 additions and 101 deletions
+6
View File
@@ -0,0 +1,6 @@
package task
import _ "unsafe"
//go:linkname runtimeFatal runtime.runtimeFatal
func runtimeFatal(msg string)
+1 -1
View File
@@ -20,7 +20,7 @@ func (m *Mutex) Lock() {
func (m *Mutex) Unlock() {
if !m.locked {
panic("sync: unlock of unlocked Mutex")
runtimeFatal("sync: unlock of unlocked Mutex")
}
// Wake up a blocked task, if applicable.
+1 -1
View File
@@ -49,7 +49,7 @@ func (m *Mutex) Lock() {
func (m *Mutex) Unlock() {
if old := m.futex.Swap(0); old == 0 {
// Mutex wasn't locked before.
panic("sync: unlock of unlocked Mutex")
runtimeFatal("sync: unlock of unlocked Mutex")
} else if old == 2 {
// Mutex was a contended lock, so we need to wake the next waiter.
m.futex.Wake()
+2 -2
View File
@@ -15,7 +15,7 @@ func (q *Queue) Push(t *Task) {
mask := lockAtomics()
if asserts && t.Next != nil {
unlockAtomics(mask)
panic("runtime: pushing a task to a queue with a non-nil Next pointer")
runtimeFatal("runtime: pushing a task to a queue with a non-nil Next pointer")
}
if q.tail != nil {
q.tail.Next = t
@@ -78,7 +78,7 @@ func (s *Stack) Push(t *Task) {
mask := lockAtomics()
if asserts && t.Next != nil {
unlockAtomics(mask)
panic("runtime: pushing a task to a stack with a non-nil Next pointer")
runtimeFatal("runtime: pushing a task to a stack with a non-nil Next pointer")
}
s.top, t.Next = t, s.top
unlockAtomics(mask)
+2 -5
View File
@@ -11,9 +11,6 @@ import (
// otherwise Go wouldn't allow the cast to a smaller integer size.
const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0)))
//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(str string)
// state is a structure which holds a reference to the state of the task.
// When the task is suspended, the stack pointers are saved here.
type state struct {
@@ -95,7 +92,7 @@ func Current() *Task {
// This function may only be called when running on a goroutine stack, not when running on the system stack.
func Pause() {
if *currentTask.state.canaryPtr != stackCanary {
runtimePanic("stack overflow")
runtimeFatal("stack overflow")
}
currentTask.state.unwind()
@@ -124,7 +121,7 @@ func (t *Task) Resume() {
currentTask = prevTask
t.gcData.swap()
if uintptr(t.state.asyncifysp) > uintptr(t.state.csp) {
runtimePanic("stack overflow")
runtimeFatal("stack overflow")
}
}
+3 -3
View File
@@ -28,15 +28,15 @@ func exit(goexit bool) {
if t == mainTask {
if goexit {
if remaining == 0 {
runtimePanic("all goroutines are asleep - deadlock!")
runtimeFatal("all goroutines are asleep - deadlock!")
}
atomic.StoreUint32(&mainExitedByGoexit, 1)
}
} else if atomic.LoadUint32(&mainExitedByGoexit) != 0 && remaining == 0 {
runtimePanic("all goroutines are asleep - deadlock!")
runtimeFatal("all goroutines are asleep - deadlock!")
}
// TODO: explicitly free the stack after switching back to the scheduler.
Pause()
runtimePanic("unreachable")
runtimeFatal("unreachable")
}
-3
View File
@@ -6,9 +6,6 @@ import (
"unsafe"
)
//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(str string)
// Stack canary, to detect a stack overflow. The number is a random number
// generated by random.org. The bit fiddling dance is necessary because
// otherwise Go wouldn't allow the cast to a smaller integer size.
+2 -2
View File
@@ -23,10 +23,10 @@ func PauseLocked() {
// valid. If it is not, a stack overflow has occurred.
current := Current()
if *current.state.canaryPtr != stackCanary {
runtimePanic("goroutine stack overflow")
runtimeFatal("goroutine stack overflow")
}
if interrupt.In() {
runtimePanic("blocked inside interrupt")
runtimeFatal("blocked inside interrupt")
}
if current.RunState == RunStateResuming {
// Another core already marked this goroutine as ready to resume.
+2 -2
View File
@@ -18,10 +18,10 @@ func Pause() {
// Check whether the canary (the lowest address of the stack) is still
// valid. If it is not, a stack overflow has occurred.
if *currentTask.state.canaryPtr != stackCanary {
runtimePanic("goroutine stack overflow")
runtimeFatal("goroutine stack overflow")
}
if interrupt.In() {
runtimePanic("blocked inside interrupt")
runtimeFatal("blocked inside interrupt")
}
currentTask.state.pause()
}
+7 -10
View File
@@ -48,7 +48,7 @@ var activeTaskLock PMutex
var mainExitedByGoexit bool
func OnSystemStack() bool {
runtimePanic("todo: task.OnSystemStack")
runtimeFatal("todo: task.OnSystemStack")
return false
}
@@ -63,7 +63,7 @@ func Init(sp uintptr) {
func Current() *Task {
t := (*Task)(tinygo_task_current())
if t == nil {
runtimePanic("unknown current task")
runtimeFatal("unknown current task")
}
return t
}
@@ -112,7 +112,7 @@ func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
activeTaskLock.Lock()
errCode := tinygo_task_start(fn, args, t, &t.state.thread, &t.state.stackTop, stackSize)
if errCode != 0 {
runtimePanic("could not start thread")
runtimeFatal("could not start thread")
}
t.state.QueueNext = activeTasks
activeTasks = t
@@ -127,7 +127,7 @@ func taskExited(t *Task) {
}
if exit(t) {
runtimePanic("all goroutines are asleep - deadlock!")
runtimeFatal("all goroutines are asleep - deadlock!")
}
}
@@ -149,7 +149,7 @@ func exit(t *Task) bool {
// Sanity check.
if !found {
runtimePanic("taskExited failed")
runtimeFatal("taskExited failed")
}
return deadlocked
}
@@ -173,11 +173,11 @@ func Exit() {
}
activeTaskLock.Unlock()
if noOtherTasks {
runtimePanic("all goroutines are asleep - deadlock!")
runtimeFatal("all goroutines are asleep - deadlock!")
}
}
if exit(t) {
runtimePanic("all goroutines are asleep - deadlock!")
runtimeFatal("all goroutines are asleep - deadlock!")
}
tinygo_task_exit()
}
@@ -325,9 +325,6 @@ func StackTop() uintptr {
return Current().state.stackTop
}
//go:linkname runtimePanic runtime.runtimePanic
func runtimePanic(msg string)
// Using //go:linkname instead of //export so that we don't tell the compiler
// that the 't' parameter won't escape (because it will).
//