Skip to content

Commit 742c156

Browse files
committed
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.
1 parent ffd4720 commit 742c156

40 files changed

Lines changed: 141 additions & 98 deletions

main_test.go

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -988,15 +988,15 @@ func TestGoexitCrash(t *testing.T) {
988988
name string
989989
want string
990990
}{
991-
{"main", "all goroutines are asleep - deadlock!"},
992-
{"deadlock", "all goroutines are asleep - deadlock!"},
993-
{"exit", "all goroutines are asleep - deadlock!"},
994-
{"main-other", "all goroutines are asleep - deadlock!"},
995-
{"in-panic", "all goroutines are asleep - deadlock!"},
991+
{"main", "fatal error: all goroutines are asleep - deadlock!"},
992+
{"deadlock", "fatal error: all goroutines are asleep - deadlock!"},
993+
{"exit", "fatal error: all goroutines are asleep - deadlock!"},
994+
{"main-other", "fatal error: all goroutines are asleep - deadlock!"},
995+
{"in-panic", "fatal error: all goroutines are asleep - deadlock!"},
996996
{"panic", "panic: panic after Goexit"},
997-
{"recovered-panic", "all goroutines are asleep - deadlock!"},
998-
{"recover-before-panic", "all goroutines are asleep - deadlock!"},
999-
{"recover-before-panic-loop", "all goroutines are asleep - deadlock!"},
997+
{"recovered-panic", "fatal error: all goroutines are asleep - deadlock!"},
998+
{"recover-before-panic", "fatal error: all goroutines are asleep - deadlock!"},
999+
{"recover-before-panic-loop", "fatal error: all goroutines are asleep - deadlock!"},
10001000
} {
10011001
t.Run(tc.name, func(t *testing.T) {
10021002
output := &bytes.Buffer{}
@@ -1017,6 +1017,34 @@ func TestGoexitCrash(t *testing.T) {
10171017
}
10181018
}
10191019

1020+
func TestRuntimeFatal(t *testing.T) {
1021+
t.Parallel()
1022+
1023+
options := optionsFromTarget("", sema)
1024+
config, err := builder.NewConfig(&options)
1025+
if err != nil {
1026+
t.Fatal(err)
1027+
}
1028+
1029+
output := &bytes.Buffer{}
1030+
_, err = buildAndRun("testdata/runtimefatal.go", config, output, nil, nil, time.Minute, func(cmd *exec.Cmd, result builder.BuildResult) error {
1031+
cmd.Stdout = nil
1032+
cmd.Stderr = nil
1033+
data, err := cmd.CombinedOutput()
1034+
output.Write(data)
1035+
return err
1036+
})
1037+
if err == nil {
1038+
t.Fatal("program unexpectedly exited successfully")
1039+
}
1040+
if want := "fatal error: sync: unlock of unlocked Mutex"; !strings.Contains(output.String(), want) {
1041+
t.Fatalf("output does not contain %q:\n%s", want, output.String())
1042+
}
1043+
if strings.Contains(output.String(), "recovered:") {
1044+
t.Fatalf("fatal runtime error was recovered:\n%s", output.String())
1045+
}
1046+
}
1047+
10201048
func TestTest(t *testing.T) {
10211049
t.Parallel()
10221050

src/internal/task/fatal.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package task
2+
3+
import _ "unsafe"
4+
5+
//go:linkname runtimeFatal runtime.runtimeFatal
6+
func runtimeFatal(msg string)

src/internal/task/mutex-cooperative.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func (m *Mutex) Lock() {
2020

2121
func (m *Mutex) Unlock() {
2222
if !m.locked {
23-
panic("sync: unlock of unlocked Mutex")
23+
runtimeFatal("sync: unlock of unlocked Mutex")
2424
}
2525

2626
// Wake up a blocked task, if applicable.

src/internal/task/mutex-preemptive.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (m *Mutex) Lock() {
4949
func (m *Mutex) Unlock() {
5050
if old := m.futex.Swap(0); old == 0 {
5151
// Mutex wasn't locked before.
52-
panic("sync: unlock of unlocked Mutex")
52+
runtimeFatal("sync: unlock of unlocked Mutex")
5353
} else if old == 2 {
5454
// Mutex was a contended lock, so we need to wake the next waiter.
5555
m.futex.Wake()

src/internal/task/queue.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func (q *Queue) Push(t *Task) {
1515
mask := lockAtomics()
1616
if asserts && t.Next != nil {
1717
unlockAtomics(mask)
18-
panic("runtime: pushing a task to a queue with a non-nil Next pointer")
18+
runtimeFatal("runtime: pushing a task to a queue with a non-nil Next pointer")
1919
}
2020
if q.tail != nil {
2121
q.tail.Next = t
@@ -78,7 +78,7 @@ func (s *Stack) Push(t *Task) {
7878
mask := lockAtomics()
7979
if asserts && t.Next != nil {
8080
unlockAtomics(mask)
81-
panic("runtime: pushing a task to a stack with a non-nil Next pointer")
81+
runtimeFatal("runtime: pushing a task to a stack with a non-nil Next pointer")
8282
}
8383
s.top, t.Next = t, s.top
8484
unlockAtomics(mask)

src/internal/task/task_asyncify.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ import (
1111
// otherwise Go wouldn't allow the cast to a smaller integer size.
1212
const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0)))
1313

14-
//go:linkname runtimePanic runtime.runtimePanic
15-
func runtimePanic(str string)
16-
1714
// state is a structure which holds a reference to the state of the task.
1815
// When the task is suspended, the stack pointers are saved here.
1916
type state struct {
@@ -95,7 +92,7 @@ func Current() *Task {
9592
// This function may only be called when running on a goroutine stack, not when running on the system stack.
9693
func Pause() {
9794
if *currentTask.state.canaryPtr != stackCanary {
98-
runtimePanic("stack overflow")
95+
runtimeFatal("stack overflow")
9996
}
10097

10198
currentTask.state.unwind()
@@ -124,7 +121,7 @@ func (t *Task) Resume() {
124121
currentTask = prevTask
125122
t.gcData.swap()
126123
if uintptr(t.state.asyncifysp) > uintptr(t.state.csp) {
127-
runtimePanic("stack overflow")
124+
runtimeFatal("stack overflow")
128125
}
129126
}
130127

src/internal/task/task_exit.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ func exit(goexit bool) {
2828
if t == mainTask {
2929
if goexit {
3030
if remaining == 0 {
31-
runtimePanic("all goroutines are asleep - deadlock!")
31+
runtimeFatal("all goroutines are asleep - deadlock!")
3232
}
3333
atomic.StoreUint32(&mainExitedByGoexit, 1)
3434
}
3535
} else if atomic.LoadUint32(&mainExitedByGoexit) != 0 && remaining == 0 {
36-
runtimePanic("all goroutines are asleep - deadlock!")
36+
runtimeFatal("all goroutines are asleep - deadlock!")
3737
}
3838

3939
// TODO: explicitly free the stack after switching back to the scheduler.
4040
Pause()
41-
runtimePanic("unreachable")
41+
runtimeFatal("unreachable")
4242
}

src/internal/task/task_stack.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ import (
66
"unsafe"
77
)
88

9-
//go:linkname runtimePanic runtime.runtimePanic
10-
func runtimePanic(str string)
11-
129
// Stack canary, to detect a stack overflow. The number is a random number
1310
// generated by random.org. The bit fiddling dance is necessary because
1411
// otherwise Go wouldn't allow the cast to a smaller integer size.

src/internal/task/task_stack_multicore.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ func PauseLocked() {
2323
// valid. If it is not, a stack overflow has occurred.
2424
current := Current()
2525
if *current.state.canaryPtr != stackCanary {
26-
runtimePanic("goroutine stack overflow")
26+
runtimeFatal("goroutine stack overflow")
2727
}
2828
if interrupt.In() {
29-
runtimePanic("blocked inside interrupt")
29+
runtimeFatal("blocked inside interrupt")
3030
}
3131
if current.RunState == RunStateResuming {
3232
// Another core already marked this goroutine as ready to resume.

src/internal/task/task_stack_unicore.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ func Pause() {
1818
// Check whether the canary (the lowest address of the stack) is still
1919
// valid. If it is not, a stack overflow has occurred.
2020
if *currentTask.state.canaryPtr != stackCanary {
21-
runtimePanic("goroutine stack overflow")
21+
runtimeFatal("goroutine stack overflow")
2222
}
2323
if interrupt.In() {
24-
runtimePanic("blocked inside interrupt")
24+
runtimeFatal("blocked inside interrupt")
2525
}
2626
currentTask.state.pause()
2727
}

0 commit comments

Comments
 (0)