Skip to content

Commit 78561c4

Browse files
committed
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Alex Brainman <[email protected]>
1 parent d6b56bb commit 78561c4

File tree

5 files changed

+78
-2
lines changed

5 files changed

+78
-2
lines changed

src/runtime/crash_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -687,3 +687,32 @@ func TestRuntimePanic(t *testing.T) {
687687
t.Errorf("output did not contain expected string %q", want)
688688
}
689689
}
690+
691+
// Test that g0 stack overflows are handled gracefully.
692+
func TestG0StackOverflow(t *testing.T) {
693+
testenv.MustHaveExec(t)
694+
695+
switch runtime.GOOS {
696+
case "darwin", "dragonfly", "freebsd", "linux", "netbsd", "openbsd":
697+
t.Skipf("g0 stack is wrong on pthread platforms (see golang.org/issue/26061)")
698+
}
699+
700+
if os.Getenv("TEST_G0_STACK_OVERFLOW") != "1" {
701+
cmd := testenv.CleanCmdEnv(exec.Command(os.Args[0], "-test.run=TestG0StackOverflow", "-test.v"))
702+
cmd.Env = append(cmd.Env, "TEST_G0_STACK_OVERFLOW=1")
703+
out, err := cmd.CombinedOutput()
704+
// Don't check err since it's expected to crash.
705+
if n := strings.Count(string(out), "morestack on g0\n"); n != 1 {
706+
t.Fatalf("%s\n(exit status %v)", out, err)
707+
}
708+
// Check that it's a signal-style traceback.
709+
if runtime.GOOS != "windows" {
710+
if want := "PC="; !strings.Contains(string(out), want) {
711+
t.Errorf("output does not contain %q:\n%s", want, out)
712+
}
713+
}
714+
return
715+
}
716+
717+
runtime.G0StackOverflow()
718+
}

src/runtime/export_test.go

+11
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,14 @@ func PanicForTesting(b []byte, i int) byte {
461461
func unexportedPanicForTesting(b []byte, i int) byte {
462462
return b[i]
463463
}
464+
465+
func G0StackOverflow() {
466+
systemstack(func() {
467+
stackOverflow(nil)
468+
})
469+
}
470+
471+
func stackOverflow(x *byte) {
472+
var buf [256]byte
473+
stackOverflow(&buf[0])
474+
}

src/runtime/os_windows.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -701,8 +701,9 @@ func minit() {
701701
// The system leaves an 8K PAGE_GUARD region at the bottom of
702702
// the stack (in theory VirtualQuery isn't supposed to include
703703
// that, but it does). Add an additional 8K of slop for
704-
// calling C functions that don't have stack checks. We
705-
// shouldn't be anywhere near this bound anyway.
704+
// calling C functions that don't have stack checks and for
705+
// lastcontinuehandler. We shouldn't be anywhere near this
706+
// bound anyway.
706707
base := mbi.allocationBase + 16<<10
707708
// Sanity check the stack bounds.
708709
g0 := getg()

src/runtime/panic.go

+5
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,11 @@ func shouldPushSigpanic(gp *g, pc, lr uintptr) bool {
889889

890890
// isAbortPC returns true if pc is the program counter at which
891891
// runtime.abort raises a signal.
892+
//
893+
// It is nosplit because it's part of the isgoexception
894+
// implementation.
895+
//
896+
//go:nosplit
892897
func isAbortPC(pc uintptr) bool {
893898
return pc == funcPC(abort) || ((GOARCH == "arm" || GOARCH == "arm64") && pc == funcPC(abort)+sys.PCQuantum)
894899
}

src/runtime/signal_windows.go

+30
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ func initExceptionHandler() {
3838
}
3939
}
4040

41+
// isgoexception returns true if this exception should be translated
42+
// into a Go panic.
43+
//
44+
// It is nosplit to avoid growing the stack in case we're aborting
45+
// because of a stack overflow.
46+
//
47+
//go:nosplit
4148
func isgoexception(info *exceptionrecord, r *context) bool {
4249
// Only handle exception if executing instructions in Go binary
4350
// (not Windows library code).
@@ -73,11 +80,19 @@ func isgoexception(info *exceptionrecord, r *context) bool {
7380
// Called by sigtramp from Windows VEH handler.
7481
// Return value signals whether the exception has been handled (EXCEPTION_CONTINUE_EXECUTION)
7582
// or should be made available to other handlers in the chain (EXCEPTION_CONTINUE_SEARCH).
83+
//
84+
// This is the first entry into Go code for exception handling. This
85+
// is nosplit to avoid growing the stack until we've checked for
86+
// _EXCEPTION_BREAKPOINT, which is raised if we overflow the g0 stack,
87+
//
88+
//go:nosplit
7689
func exceptionhandler(info *exceptionrecord, r *context, gp *g) int32 {
7790
if !isgoexception(info, r) {
7891
return _EXCEPTION_CONTINUE_SEARCH
7992
}
8093

94+
// After this point, it is safe to grow the stack.
95+
8196
if gp.throwsplit {
8297
// We can't safely sigpanic because it may grow the
8398
// stack. Let it fall through.
@@ -113,6 +128,10 @@ func exceptionhandler(info *exceptionrecord, r *context, gp *g) int32 {
113128
// if ExceptionHandler returns EXCEPTION_CONTINUE_EXECUTION.
114129
// firstcontinuehandler will stop that search,
115130
// if exceptionhandler did the same earlier.
131+
//
132+
// It is nosplit for the same reason as exceptionhandler.
133+
//
134+
//go:nosplit
116135
func firstcontinuehandler(info *exceptionrecord, r *context, gp *g) int32 {
117136
if !isgoexception(info, r) {
118137
return _EXCEPTION_CONTINUE_SEARCH
@@ -124,6 +143,10 @@ var testingWER bool
124143

125144
// lastcontinuehandler is reached, because runtime cannot handle
126145
// current exception. lastcontinuehandler will print crash info and exit.
146+
//
147+
// It is nosplit for the same reason as exceptionhandler.
148+
//
149+
//go:nosplit
127150
func lastcontinuehandler(info *exceptionrecord, r *context, gp *g) int32 {
128151
if testingWER {
129152
return _EXCEPTION_CONTINUE_SEARCH
@@ -136,6 +159,13 @@ func lastcontinuehandler(info *exceptionrecord, r *context, gp *g) int32 {
136159
}
137160
panicking = 1
138161

162+
// In case we're handling a g0 stack overflow, blow away the
163+
// g0 stack bounds so we have room to print the traceback. If
164+
// this somehow overflows the stack, the OS will trap it.
165+
_g_.stack.lo = 0
166+
_g_.stackguard0 = _g_.stack.lo + _StackGuard
167+
_g_.stackguard1 = _g_.stackguard0
168+
139169
print("Exception ", hex(info.exceptioncode), " ", hex(info.exceptioninformation[0]), " ", hex(info.exceptioninformation[1]), " ", hex(r.ip()), "\n")
140170

141171
print("PC=", hex(r.ip()), "\n")

0 commit comments

Comments
 (0)