mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
runtime: handle g0 stack overflows gracefully
Currently, if the runtime overflows the g0 stack on Windows, it leads to an infinite recursion: 1. Something overflows the g0 stack bounds and calls morestack. 2. morestack determines it's on the g0 stack and hence cannot grow the stack, so it calls badmorestackg0 (which prints "fatal: morestack on g0") followed by abort. 3. abort performs an INT $3, which turns into a Windows _EXCEPTION_BREAKPOINT exception. 4. This enters the Windows sigtramp, which ensures we're on the g0 stack and calls exceptionhandler. 5. exceptionhandler has a stack check prologue, so it determines that it's out of stack and calls morestack. 6. goto 2 Fix this by making the exception handler avoid stack checks until it has ruled out an abort and by blowing away the stack bounds in lastcontinuehandler before we print the final fatal traceback (which itself involves a lot of stack bounds checks). Fixes #21382. Change-Id: Ie66e91f708e18d131d97f22b43f9ac26f3aece5a Reviewed-on: https://go-review.googlesource.com/120857 Run-TryBot: Austin Clements <austin@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org> Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
This commit is contained in:
parent
d6b56bb301
commit
78561c4ae9
5 changed files with 78 additions and 2 deletions
|
|
@ -687,3 +687,32 @@ func TestRuntimePanic(t *testing.T) {
|
||||||
t.Errorf("output did not contain expected string %q", want)
|
t.Errorf("output did not contain expected string %q", want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that g0 stack overflows are handled gracefully.
|
||||||
|
func TestG0StackOverflow(t *testing.T) {
|
||||||
|
testenv.MustHaveExec(t)
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd":
|
||||||
|
t.Skipf("g0 stack is wrong on pthread platforms (see golang.org/issue/26061)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("TEST_G0_STACK_OVERFLOW") != "1" {
|
||||||
|
cmd := testenv.CleanCmdEnv(exec.Command(os.Args[0], "-test.run=TestG0StackOverflow", "-test.v"))
|
||||||
|
cmd.Env = append(cmd.Env, "TEST_G0_STACK_OVERFLOW=1")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
// Don't check err since it's expected to crash.
|
||||||
|
if n := strings.Count(string(out), "morestack on g0\n"); n != 1 {
|
||||||
|
t.Fatalf("%s\n(exit status %v)", out, err)
|
||||||
|
}
|
||||||
|
// Check that it's a signal-style traceback.
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if want := "PC="; !strings.Contains(string(out), want) {
|
||||||
|
t.Errorf("output does not contain %q:\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime.G0StackOverflow()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -461,3 +461,14 @@ func PanicForTesting(b []byte, i int) byte {
|
||||||
func unexportedPanicForTesting(b []byte, i int) byte {
|
func unexportedPanicForTesting(b []byte, i int) byte {
|
||||||
return b[i]
|
return b[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func G0StackOverflow() {
|
||||||
|
systemstack(func() {
|
||||||
|
stackOverflow(nil)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func stackOverflow(x *byte) {
|
||||||
|
var buf [256]byte
|
||||||
|
stackOverflow(&buf[0])
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -701,8 +701,9 @@ func minit() {
|
||||||
// The system leaves an 8K PAGE_GUARD region at the bottom of
|
// The system leaves an 8K PAGE_GUARD region at the bottom of
|
||||||
// the stack (in theory VirtualQuery isn't supposed to include
|
// the stack (in theory VirtualQuery isn't supposed to include
|
||||||
// that, but it does). Add an additional 8K of slop for
|
// that, but it does). Add an additional 8K of slop for
|
||||||
// calling C functions that don't have stack checks. We
|
// calling C functions that don't have stack checks and for
|
||||||
// shouldn't be anywhere near this bound anyway.
|
// lastcontinuehandler. We shouldn't be anywhere near this
|
||||||
|
// bound anyway.
|
||||||
base := mbi.allocationBase + 16<<10
|
base := mbi.allocationBase + 16<<10
|
||||||
// Sanity check the stack bounds.
|
// Sanity check the stack bounds.
|
||||||
g0 := getg()
|
g0 := getg()
|
||||||
|
|
|
||||||
|
|
@ -889,6 +889,11 @@ func shouldPushSigpanic(gp *g, pc, lr uintptr) bool {
|
||||||
|
|
||||||
// isAbortPC returns true if pc is the program counter at which
|
// isAbortPC returns true if pc is the program counter at which
|
||||||
// runtime.abort raises a signal.
|
// runtime.abort raises a signal.
|
||||||
|
//
|
||||||
|
// It is nosplit because it's part of the isgoexception
|
||||||
|
// implementation.
|
||||||
|
//
|
||||||
|
//go:nosplit
|
||||||
func isAbortPC(pc uintptr) bool {
|
func isAbortPC(pc uintptr) bool {
|
||||||
return pc == funcPC(abort) || ((GOARCH == "arm" || GOARCH == "arm64") && pc == funcPC(abort)+sys.PCQuantum)
|
return pc == funcPC(abort) || ((GOARCH == "arm" || GOARCH == "arm64") && pc == funcPC(abort)+sys.PCQuantum)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,13 @@ func initExceptionHandler() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isgoexception returns true if this exception should be translated
|
||||||
|
// into a Go panic.
|
||||||
|
//
|
||||||
|
// It is nosplit to avoid growing the stack in case we're aborting
|
||||||
|
// because of a stack overflow.
|
||||||
|
//
|
||||||
|
//go:nosplit
|
||||||
func isgoexception(info *exceptionrecord, r *context) bool {
|
func isgoexception(info *exceptionrecord, r *context) bool {
|
||||||
// Only handle exception if executing instructions in Go binary
|
// Only handle exception if executing instructions in Go binary
|
||||||
// (not Windows library code).
|
// (not Windows library code).
|
||||||
|
|
@ -73,11 +80,19 @@ func isgoexception(info *exceptionrecord, r *context) bool {
|
||||||
// Called by sigtramp from Windows VEH handler.
|
// Called by sigtramp from Windows VEH handler.
|
||||||
// Return value signals whether the exception has been handled (EXCEPTION_CONTINUE_EXECUTION)
|
// Return value signals whether the exception has been handled (EXCEPTION_CONTINUE_EXECUTION)
|
||||||
// or should be made available to other handlers in the chain (EXCEPTION_CONTINUE_SEARCH).
|
// or should be made available to other handlers in the chain (EXCEPTION_CONTINUE_SEARCH).
|
||||||
|
//
|
||||||
|
// This is the first entry into Go code for exception handling. This
|
||||||
|
// is nosplit to avoid growing the stack until we've checked for
|
||||||
|
// _EXCEPTION_BREAKPOINT, which is raised if we overflow the g0 stack,
|
||||||
|
//
|
||||||
|
//go:nosplit
|
||||||
func exceptionhandler(info *exceptionrecord, r *context, gp *g) int32 {
|
func exceptionhandler(info *exceptionrecord, r *context, gp *g) int32 {
|
||||||
if !isgoexception(info, r) {
|
if !isgoexception(info, r) {
|
||||||
return _EXCEPTION_CONTINUE_SEARCH
|
return _EXCEPTION_CONTINUE_SEARCH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After this point, it is safe to grow the stack.
|
||||||
|
|
||||||
if gp.throwsplit {
|
if gp.throwsplit {
|
||||||
// We can't safely sigpanic because it may grow the
|
// We can't safely sigpanic because it may grow the
|
||||||
// stack. Let it fall through.
|
// stack. Let it fall through.
|
||||||
|
|
@ -113,6 +128,10 @@ func exceptionhandler(info *exceptionrecord, r *context, gp *g) int32 {
|
||||||
// if ExceptionHandler returns EXCEPTION_CONTINUE_EXECUTION.
|
// if ExceptionHandler returns EXCEPTION_CONTINUE_EXECUTION.
|
||||||
// firstcontinuehandler will stop that search,
|
// firstcontinuehandler will stop that search,
|
||||||
// if exceptionhandler did the same earlier.
|
// if exceptionhandler did the same earlier.
|
||||||
|
//
|
||||||
|
// It is nosplit for the same reason as exceptionhandler.
|
||||||
|
//
|
||||||
|
//go:nosplit
|
||||||
func firstcontinuehandler(info *exceptionrecord, r *context, gp *g) int32 {
|
func firstcontinuehandler(info *exceptionrecord, r *context, gp *g) int32 {
|
||||||
if !isgoexception(info, r) {
|
if !isgoexception(info, r) {
|
||||||
return _EXCEPTION_CONTINUE_SEARCH
|
return _EXCEPTION_CONTINUE_SEARCH
|
||||||
|
|
@ -124,6 +143,10 @@ var testingWER bool
|
||||||
|
|
||||||
// lastcontinuehandler is reached, because runtime cannot handle
|
// lastcontinuehandler is reached, because runtime cannot handle
|
||||||
// current exception. lastcontinuehandler will print crash info and exit.
|
// current exception. lastcontinuehandler will print crash info and exit.
|
||||||
|
//
|
||||||
|
// It is nosplit for the same reason as exceptionhandler.
|
||||||
|
//
|
||||||
|
//go:nosplit
|
||||||
func lastcontinuehandler(info *exceptionrecord, r *context, gp *g) int32 {
|
func lastcontinuehandler(info *exceptionrecord, r *context, gp *g) int32 {
|
||||||
if testingWER {
|
if testingWER {
|
||||||
return _EXCEPTION_CONTINUE_SEARCH
|
return _EXCEPTION_CONTINUE_SEARCH
|
||||||
|
|
@ -136,6 +159,13 @@ func lastcontinuehandler(info *exceptionrecord, r *context, gp *g) int32 {
|
||||||
}
|
}
|
||||||
panicking = 1
|
panicking = 1
|
||||||
|
|
||||||
|
// In case we're handling a g0 stack overflow, blow away the
|
||||||
|
// g0 stack bounds so we have room to print the traceback. If
|
||||||
|
// this somehow overflows the stack, the OS will trap it.
|
||||||
|
_g_.stack.lo = 0
|
||||||
|
_g_.stackguard0 = _g_.stack.lo + _StackGuard
|
||||||
|
_g_.stackguard1 = _g_.stackguard0
|
||||||
|
|
||||||
print("Exception ", hex(info.exceptioncode), " ", hex(info.exceptioninformation[0]), " ", hex(info.exceptioninformation[1]), " ", hex(r.ip()), "\n")
|
print("Exception ", hex(info.exceptioncode), " ", hex(info.exceptioninformation[0]), " ", hex(info.exceptioninformation[1]), " ", hex(r.ip()), "\n")
|
||||||
|
|
||||||
print("PC=", hex(r.ip()), "\n")
|
print("PC=", hex(r.ip()), "\n")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue