Skip to content

Commit 7dcd343

Browse files
committed
runtime: ensure that Goexit cannot be aborted by a recursive panic/recover
When we do a successful recover of a panic, we resume normal execution by returning from the frame that had the deferred call that did the recover (after executing any remaining deferred calls in that frame). However, suppose we have called runtime.Goexit and there is a panic during one of the deferred calls run by the Goexit. Further assume that there is a deferred call in the frame of the Goexit or a parent frame that does a recover. Then the recovery process will actually resume normal execution above the Goexit frame and hence abort the Goexit. We will not terminate the thread as expected, but continue running in the frame above the Goexit. To fix this, we explicitly create a _panic object for a Goexit call. We then change the "abort" behavior for Goexits, but not panics. After a recovery, if the top-level panic is actually a Goexit that is marked to be aborted, then we return to the Goexit defer-processing loop, so that the Goexit is not actually aborted. Actual code changes are just panic.go, runtime2.go, and funcid.go. Adjusted the test related to the new Goexit behavior (TestRecoverBeforePanicAfterGoexit) and added several new tests of aborted panics (whose behavior has not changed). Fixes #29226 Change-Id: Ib13cb0074f5acc2567a28db7ca6912cfc47eecb5 Reviewed-on: https://go-review.googlesource.com/c/go/+/200081 Run-TryBot: Dan Scales <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent a8fc82f commit 7dcd343

File tree

8 files changed

+232
-34
lines changed

8 files changed

+232
-34
lines changed

src/cmd/internal/objabi/funcid.go

+3
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ func GetFuncID(name, file string) FuncID {
9494
case "runtime.runOpenDeferFrame":
9595
// Don't show in the call stack (used when invoking defer functions)
9696
return FuncID_wrapper
97+
case "runtime.reflectcallSave":
98+
// Don't show in the call stack (used when invoking defer functions)
99+
return FuncID_wrapper
97100
}
98101
if file == "<autogenerated>" {
99102
return FuncID_wrapper

src/runtime/callers_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,68 @@ func TestCallersAfterRecovery(t *testing.T) {
147147
panic(1)
148148
}
149149

150+
func TestCallersAbortedPanic(t *testing.T) {
151+
want := []string{"runtime.Callers", "runtime_test.TestCallersAbortedPanic.func2", "runtime_test.TestCallersAbortedPanic"}
152+
153+
defer func() {
154+
r := recover()
155+
if r != nil {
156+
t.Fatalf("should be no panic remaining to recover")
157+
}
158+
}()
159+
160+
defer func() {
161+
// panic2 was aborted/replaced by panic1, so when panic2 was
162+
// recovered, there is no remaining panic on the stack.
163+
pcs := make([]uintptr, 20)
164+
pcs = pcs[:runtime.Callers(0, pcs)]
165+
testCallersEqual(t, pcs, want)
166+
}()
167+
defer func() {
168+
r := recover()
169+
if r != "panic2" {
170+
t.Fatalf("got %v, wanted %v", r, "panic2")
171+
}
172+
}()
173+
defer func() {
174+
// panic2 aborts/replaces panic1, because it is a recursive panic
175+
// that is not recovered within the defer function called by
176+
// panic1 panicking sequence
177+
panic("panic2")
178+
}()
179+
panic("panic1")
180+
}
181+
182+
func TestCallersAbortedPanic2(t *testing.T) {
183+
want := []string{"runtime.Callers", "runtime_test.TestCallersAbortedPanic2.func2", "runtime_test.TestCallersAbortedPanic2"}
184+
defer func() {
185+
r := recover()
186+
if r != nil {
187+
t.Fatalf("should be no panic remaining to recover")
188+
}
189+
}()
190+
defer func() {
191+
pcs := make([]uintptr, 20)
192+
pcs = pcs[:runtime.Callers(0, pcs)]
193+
testCallersEqual(t, pcs, want)
194+
}()
195+
func() {
196+
defer func() {
197+
r := recover()
198+
if r != "panic2" {
199+
t.Fatalf("got %v, wanted %v", r, "panic2")
200+
}
201+
}()
202+
func() {
203+
defer func() {
204+
// Again, panic2 aborts/replaces panic1
205+
panic("panic2")
206+
}()
207+
panic("panic1")
208+
}()
209+
}()
210+
}
211+
150212
func TestCallersNilPointerPanic(t *testing.T) {
151213
// Make sure we don't have any extra frames on the stack (due to
152214
// open-coded defer processing)

src/runtime/crash_test.go

+26-17
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,17 @@ func TestRecursivePanic3(t *testing.T) {
284284

285285
}
286286

287+
func TestRecursivePanic4(t *testing.T) {
288+
output := runTestProg(t, "testprog", "RecursivePanic4")
289+
want := `panic: first panic [recovered]
290+
panic: second panic
291+
`
292+
if !strings.HasPrefix(output, want) {
293+
t.Fatalf("output does not start with %q:\n%s", want, output)
294+
}
295+
296+
}
297+
287298
func TestGoexitCrash(t *testing.T) {
288299
output := runTestProg(t, "testprog", "GoexitExit")
289300
want := "no goroutines (main called runtime.Goexit) - deadlock!"
@@ -415,23 +426,21 @@ func TestRecoveredPanicAfterGoexit(t *testing.T) {
415426
}
416427

417428
func TestRecoverBeforePanicAfterGoexit(t *testing.T) {
418-
// 1. defer a function that recovers
419-
// 2. defer a function that panics
420-
// 3. call goexit
421-
// Goexit should run the #2 defer. Its panic
422-
// should be caught by the #1 defer, and execution
423-
// should resume in the caller. Like the Goexit
424-
// never happened!
425-
defer func() {
426-
r := recover()
427-
if r == nil {
428-
panic("bad recover")
429-
}
430-
}()
431-
defer func() {
432-
panic("hello")
433-
}()
434-
runtime.Goexit()
429+
t.Parallel()
430+
output := runTestProg(t, "testprog", "RecoverBeforePanicAfterGoexit")
431+
want := "fatal error: no goroutines (main called runtime.Goexit) - deadlock!"
432+
if !strings.HasPrefix(output, want) {
433+
t.Fatalf("output does not start with %q:\n%s", want, output)
434+
}
435+
}
436+
437+
func TestRecoverBeforePanicAfterGoexit2(t *testing.T) {
438+
t.Parallel()
439+
output := runTestProg(t, "testprog", "RecoverBeforePanicAfterGoexit2")
440+
want := "fatal error: no goroutines (main called runtime.Goexit) - deadlock!"
441+
if !strings.HasPrefix(output, want) {
442+
t.Fatalf("output does not start with %q:\n%s", want, output)
443+
}
435444
}
436445

437446
func TestNetpollDeadlock(t *testing.T) {

src/runtime/defer_test.go

+6-8
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,16 @@ func TestDisappearingDefer(t *testing.T) {
121121
}
122122

123123
// This tests an extra recursive panic behavior that is only specified in the
124-
// code. Suppose a first panic P1 happens and starts processing defer calls. If
125-
// a second panic P2 happens while processing defer call D in frame F, then defer
124+
// code. Suppose a first panic P1 happens and starts processing defer calls. If a
125+
// second panic P2 happens while processing defer call D in frame F, then defer
126126
// call processing is restarted (with some potentially new defer calls created by
127-
// D or its callees). If the defer processing reaches the started defer call D
127+
// D or its callees). If the defer processing reaches the started defer call D
128128
// again in the defer stack, then the original panic P1 is aborted and cannot
129-
// continue panic processing or be recovered. If the panic P2 does a recover at
130-
// some point, it will naturally the original panic P1 from the stack, since the
131-
// original panic had to be in frame F or a descendant of F.
129+
// continue panic processing or be recovered. If the panic P2 does a recover at
130+
// some point, it will naturally remove the original panic P1 from the stack
131+
// (since the original panic had to be in frame F or a descendant of F).
132132
func TestAbortedPanic(t *testing.T) {
133133
defer func() {
134-
// The first panic should have been "aborted", so there is
135-
// no other panic to recover
136134
r := recover()
137135
if r != nil {
138136
t.Fatal(fmt.Sprintf("wanted nil recover, got %v", r))

src/runtime/panic.go

+70-9
Original file line numberDiff line numberDiff line change
@@ -577,8 +577,15 @@ func Goexit() {
577577
// This code is similar to gopanic, see that implementation
578578
// for detailed comments.
579579
gp := getg()
580-
addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))
581580

581+
// Create a panic object for Goexit, so we can recognize when it might be
582+
// bypassed by a recover().
583+
var p _panic
584+
p.goexit = true
585+
p.link = gp._panic
586+
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
587+
588+
addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))
582589
for {
583590
d := gp._defer
584591
if d == nil {
@@ -597,6 +604,7 @@ func Goexit() {
597604
}
598605
}
599606
d.started = true
607+
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
600608
if d.openDefer {
601609
done := runOpenDeferFrame(gp, d)
602610
if !done {
@@ -605,9 +613,29 @@ func Goexit() {
605613
// defer that can be recovered.
606614
throw("unfinished open-coded defers in Goexit")
607615
}
608-
addOneOpenDeferFrame(gp, 0, nil)
616+
if p.aborted {
617+
// Since our current defer caused a panic and may
618+
// have been already freed, just restart scanning
619+
// for open-coded defers from this frame again.
620+
addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp()))
621+
} else {
622+
addOneOpenDeferFrame(gp, 0, nil)
623+
}
609624
} else {
610-
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
625+
626+
// Save the pc/sp in reflectcallSave(), so we can "recover" back to this
627+
// loop if necessary.
628+
reflectcallSave(&p, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz))
629+
}
630+
if p.aborted {
631+
// We had a recursive panic in the defer d we started, and
632+
// then did a recover in a defer that was further down the
633+
// defer chain than d. In the case of an outstanding Goexit,
634+
// we force the recover to return back to this loop. d will
635+
// have already been freed if completed, so just continue
636+
// immediately to the next defer on the chain.
637+
p.aborted = false
638+
continue
611639
}
612640
if gp._defer != d {
613641
throw("bad defer entry in Goexit")
@@ -645,7 +673,12 @@ func preprintpanics(p *_panic) {
645673
func printpanics(p *_panic) {
646674
if p.link != nil {
647675
printpanics(p.link)
648-
print("\t")
676+
if !p.link.goexit {
677+
print("\t")
678+
}
679+
}
680+
if p.goexit {
681+
return
649682
}
650683
print("panic: ")
651684
printany(p.arg)
@@ -810,10 +843,11 @@ func runOpenDeferFrame(gp *g, d *_defer) bool {
810843
}
811844
deferBits = deferBits &^ (1 << i)
812845
*(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset))) = deferBits
813-
if d._panic != nil {
814-
d._panic.argp = unsafe.Pointer(getargp(0))
846+
p := d._panic
847+
reflectcallSave(p, unsafe.Pointer(closure), deferArgs, argWidth)
848+
if p != nil && p.aborted {
849+
break
815850
}
816-
reflectcall(nil, unsafe.Pointer(closure), deferArgs, argWidth, argWidth)
817851
d.fn = nil
818852
// These args are just a copy, so can be cleared immediately
819853
memclrNoHeapPointers(deferArgs, uintptr(argWidth))
@@ -826,6 +860,23 @@ func runOpenDeferFrame(gp *g, d *_defer) bool {
826860
return done
827861
}
828862

863+
// reflectcallSave calls reflectcall after saving the caller's pc and sp in the
864+
// panic record. This allows the runtime to return to the Goexit defer processing
865+
// loop, in the unusual case where the Goexit may be bypassed by a successful
866+
// recover.
867+
func reflectcallSave(p *_panic, fn, arg unsafe.Pointer, argsize uint32) {
868+
if p != nil {
869+
p.argp = unsafe.Pointer(getargp(0))
870+
p.pc = getcallerpc()
871+
p.sp = unsafe.Pointer(getcallersp())
872+
}
873+
reflectcall(nil, fn, arg, argsize, argsize)
874+
if p != nil {
875+
p.pc = 0
876+
p.sp = unsafe.Pointer(nil)
877+
}
878+
}
879+
829880
// The implementation of the predeclared function panic.
830881
func gopanic(e interface{}) {
831882
gp := getg()
@@ -876,7 +927,8 @@ func gopanic(e interface{}) {
876927
}
877928

878929
// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
879-
// take defer off list. The earlier panic or Goexit will not continue running.
930+
// take defer off list. An earlier panic will not continue running, but we will make sure below that an
931+
// earlier Goexit does continue running.
880932
if d.started {
881933
if d._panic != nil {
882934
d._panic.aborted = true
@@ -933,6 +985,15 @@ func gopanic(e interface{}) {
933985
freedefer(d)
934986
}
935987
if p.recovered {
988+
gp._panic = p.link
989+
if gp._panic != nil && gp._panic.goexit && gp._panic.aborted {
990+
// A normal recover would bypass/abort the Goexit. Instead,
991+
// we return to the processing loop of the Goexit.
992+
gp.sigcode0 = uintptr(gp._panic.sp)
993+
gp.sigcode1 = uintptr(gp._panic.pc)
994+
mcall(recovery)
995+
throw("bypassed recovery failed") // mcall should not return
996+
}
936997
atomic.Xadd(&runningPanicDefers, -1)
937998

938999
if done {
@@ -1019,7 +1080,7 @@ func gorecover(argp uintptr) interface{} {
10191080
// If they match, the caller is the one who can recover.
10201081
gp := getg()
10211082
p := gp._panic
1022-
if p != nil && !p.recovered && argp == uintptr(p.argp) {
1083+
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
10231084
p.recovered = true
10241085
return p.arg
10251086
}

src/runtime/runtime2.go

+3
Original file line numberDiff line numberDiff line change
@@ -872,8 +872,11 @@ type _panic struct {
872872
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
873873
arg interface{} // argument to panic
874874
link *_panic // link to earlier panic
875+
pc uintptr // where to return to in runtime if this panic is bypassed
876+
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
875877
recovered bool // whether this panic is over
876878
aborted bool // the panic was aborted
879+
goexit bool
877880
}
878881

879882
// stack traces

0 commit comments

Comments
 (0)