diff options
| author | Dan Scales <danscales@google.com> | 2019-10-09 12:18:26 -0700 |
|---|---|---|
| committer | Dan Scales <danscales@google.com> | 2019-11-04 16:32:38 +0000 |
| commit | 7dcd343ed641d3b70c09153d3b041ca3fe83b25e (patch) | |
| tree | 78f84bf2886ea89ea9a292630fff54bcefdbd985 /src/runtime/panic.go | |
| parent | a8fc82f77abff99a3f55b015b017cb4342cd9c08 (diff) | |
| download | go-7dcd343ed641d3b70c09153d3b041ca3fe83b25e.tar.xz | |
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 <danscales@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
Diffstat (limited to 'src/runtime/panic.go')
| -rw-r--r-- | src/runtime/panic.go | 79 |
1 files changed, 70 insertions, 9 deletions
diff --git a/src/runtime/panic.go b/src/runtime/panic.go index bdfe117e45..31bf31110f 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -577,8 +577,15 @@ func Goexit() { // This code is similar to gopanic, see that implementation // for detailed comments. gp := getg() - addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp())) + // Create a panic object for Goexit, so we can recognize when it might be + // bypassed by a recover(). + var p _panic + p.goexit = true + p.link = gp._panic + gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) + + addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp())) for { d := gp._defer if d == nil { @@ -597,6 +604,7 @@ func Goexit() { } } d.started = true + d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) if d.openDefer { done := runOpenDeferFrame(gp, d) if !done { @@ -605,9 +613,29 @@ func Goexit() { // defer that can be recovered. throw("unfinished open-coded defers in Goexit") } - addOneOpenDeferFrame(gp, 0, nil) + if p.aborted { + // Since our current defer caused a panic and may + // have been already freed, just restart scanning + // for open-coded defers from this frame again. + addOneOpenDeferFrame(gp, getcallerpc(), unsafe.Pointer(getcallersp())) + } else { + addOneOpenDeferFrame(gp, 0, nil) + } } else { - reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) + + // Save the pc/sp in reflectcallSave(), so we can "recover" back to this + // loop if necessary. + reflectcallSave(&p, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz)) + } + if p.aborted { + // We had a recursive panic in the defer d we started, and + // then did a recover in a defer that was further down the + // defer chain than d. In the case of an outstanding Goexit, + // we force the recover to return back to this loop. d will + // have already been freed if completed, so just continue + // immediately to the next defer on the chain. + p.aborted = false + continue } if gp._defer != d { throw("bad defer entry in Goexit") @@ -645,7 +673,12 @@ func preprintpanics(p *_panic) { func printpanics(p *_panic) { if p.link != nil { printpanics(p.link) - print("\t") + if !p.link.goexit { + print("\t") + } + } + if p.goexit { + return } print("panic: ") printany(p.arg) @@ -810,10 +843,11 @@ func runOpenDeferFrame(gp *g, d *_defer) bool { } deferBits = deferBits &^ (1 << i) *(*uint8)(unsafe.Pointer(d.varp - uintptr(deferBitsOffset))) = deferBits - if d._panic != nil { - d._panic.argp = unsafe.Pointer(getargp(0)) + p := d._panic + reflectcallSave(p, unsafe.Pointer(closure), deferArgs, argWidth) + if p != nil && p.aborted { + break } - reflectcall(nil, unsafe.Pointer(closure), deferArgs, argWidth, argWidth) d.fn = nil // These args are just a copy, so can be cleared immediately memclrNoHeapPointers(deferArgs, uintptr(argWidth)) @@ -826,6 +860,23 @@ func runOpenDeferFrame(gp *g, d *_defer) bool { return done } +// reflectcallSave calls reflectcall after saving the caller's pc and sp in the +// panic record. This allows the runtime to return to the Goexit defer processing +// loop, in the unusual case where the Goexit may be bypassed by a successful +// recover. +func reflectcallSave(p *_panic, fn, arg unsafe.Pointer, argsize uint32) { + if p != nil { + p.argp = unsafe.Pointer(getargp(0)) + p.pc = getcallerpc() + p.sp = unsafe.Pointer(getcallersp()) + } + reflectcall(nil, fn, arg, argsize, argsize) + if p != nil { + p.pc = 0 + p.sp = unsafe.Pointer(nil) + } +} + // The implementation of the predeclared function panic. func gopanic(e interface{}) { gp := getg() @@ -876,7 +927,8 @@ func gopanic(e interface{}) { } // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic), - // take defer off list. The earlier panic or Goexit will not continue running. + // take defer off list. An earlier panic will not continue running, but we will make sure below that an + // earlier Goexit does continue running. if d.started { if d._panic != nil { d._panic.aborted = true @@ -933,6 +985,15 @@ func gopanic(e interface{}) { freedefer(d) } if p.recovered { + gp._panic = p.link + if gp._panic != nil && gp._panic.goexit && gp._panic.aborted { + // A normal recover would bypass/abort the Goexit. Instead, + // we return to the processing loop of the Goexit. + gp.sigcode0 = uintptr(gp._panic.sp) + gp.sigcode1 = uintptr(gp._panic.pc) + mcall(recovery) + throw("bypassed recovery failed") // mcall should not return + } atomic.Xadd(&runningPanicDefers, -1) if done { @@ -1019,7 +1080,7 @@ func gorecover(argp uintptr) interface{} { // If they match, the caller is the one who can recover. gp := getg() p := gp._panic - if p != nil && !p.recovered && argp == uintptr(p.argp) { + if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } |
