Skip to content

Commit

Permalink
pkg/proc: Prefer throw instead of fatalthrow (go-delve#2616)
Browse files Browse the repository at this point in the history
* pkg/proc: Prefer throw instead of fatalthrow

Currently there is a breakpoint set at runtime.fatalthrow to catch any
situation where the runtime crashes (e.g. deadlock).
When we do this, we go up a frame in order to parse the crash reason.
The problem is that there is no guarentee the "s" variable we attempt to
parse will still be considered "live".
Since runtime.fatalthrow is never called directly, set a breakpoint on
runtime.throw instead and prevent having
to search up a stack frame in order to
get the throw reason.

Fixes go-delve#2602

* service/dap: Fix TestFatalThrowBreakpoint

* Reenable TestFatalThrow DAP test

* service/dap: Don't skip test on < 1.17

* service/dap: Update test constraint for 1.16

* pkg/proc: Reinstate runtime.fatalthrow as switchstack exception
  • Loading branch information
derekparker authored Jul 28, 2021
1 parent 26e7f67 commit f6681c6
Show file tree
Hide file tree
Showing 7 changed files with 10 additions and 21 deletions.
4 changes: 2 additions & 2 deletions pkg/proc/amd64_arch.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func amd64SwitchStack(it *stackIterator, _ *op.DwarfRegisters) bool {
return true

default:
if it.systemstack && it.top && it.g != nil && strings.HasPrefix(it.frame.Current.Fn.Name, "runtime.") && it.frame.Current.Fn.Name != "runtime.fatalthrow" {
if it.systemstack && it.top && it.g != nil && strings.HasPrefix(it.frame.Current.Fn.Name, "runtime.") && it.frame.Current.Fn.Name != "runtime.throw" && it.frame.Current.Fn.Name != "runtime.fatalthrow" {
// The runtime switches to the system stack in multiple places.
// This usually happens through a call to runtime.systemstack but there
// are functions that switch to the system stack manually (for example
Expand All @@ -228,7 +228,7 @@ func amd64SwitchStack(it *stackIterator, _ *op.DwarfRegisters) bool {
// calls we switch directly to the goroutine stack if we detect that the
// function at the top of the stack is a runtime function.
//
// The function "runtime.fatalthrow" is deliberately excluded from this
// The function "runtime.throw" is deliberately excluded from this
// because it can end up in the stack during a cgo call and switching to
// the goroutine stack will exclude all the C functions from the stack
// trace.
Expand Down
2 changes: 1 addition & 1 deletion pkg/proc/arm64_arch.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func arm64SwitchStack(it *stackIterator, callFrameRegs *op.DwarfRegisters) bool
it.pc = newlr
return true
default:
if it.systemstack && it.top && it.g != nil && strings.HasPrefix(it.frame.Current.Fn.Name, "runtime.") && it.frame.Current.Fn.Name != "runtime.fatalthrow" {
if it.systemstack && it.top && it.g != nil && strings.HasPrefix(it.frame.Current.Fn.Name, "runtime.") && it.frame.Current.Fn.Name != "runtime.throw" && it.frame.Current.Fn.Name != "runtime.fatalthrow" {
// The runtime switches to the system stack in multiple places.
// This usually happens through a call to runtime.systemstack but there
// are functions that switch to the system stack manually (for example
Expand Down
4 changes: 2 additions & 2 deletions pkg/proc/i386_arch.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func i386SwitchStack(it *stackIterator, _ *op.DwarfRegisters) bool {
return true

default:
if it.systemstack && it.top && it.g != nil && strings.HasPrefix(it.frame.Current.Fn.Name, "runtime.") && it.frame.Current.Fn.Name != "runtime.fatalthrow" {
if it.systemstack && it.top && it.g != nil && strings.HasPrefix(it.frame.Current.Fn.Name, "runtime.") && it.frame.Current.Fn.Name != "runtime.throw" && it.frame.Current.Fn.Name != "runtime.fatalthrow" {
// The runtime switches to the system stack in multiple places.
// This usually happens through a call to runtime.systemstack but there
// are functions that switch to the system stack manually (for example
Expand All @@ -157,7 +157,7 @@ func i386SwitchStack(it *stackIterator, _ *op.DwarfRegisters) bool {
// calls we switch directly to the goroutine stack if we detect that the
// function at the top of the stack is a runtime function.
//
// The function "runtime.fatalthrow" is deliberately excluded from this
// The function "runtime.throw" is deliberately excluded from this
// because it can end up in the stack during a cgo call and switching to
// the goroutine stack will exclude all the C functions from the stack
// trace.
Expand Down
9 changes: 1 addition & 8 deletions pkg/proc/proc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1354,14 +1354,7 @@ func TestThreadFrameEvaluation(t *testing.T) {
scope, err := proc.ConvertEvalScope(p, 0, 0, 0)
assertNoError(err, t, "ConvertEvalScope() on frame 0")
_, err = scope.EvalVariable("s", normalLoadConfig)
if err == nil {
t.Errorf("expected error for EvalVariable(\"s\") on frame 0")
}

scope, err = proc.ConvertEvalScope(p, 0, 1, 0)
assertNoError(err, t, "ConvertEvalScope() on frame 1")
_, err = scope.EvalVariable("s", normalLoadConfig)
assertNoError(err, t, "EvalVariable(\"s\") on frame 1")
assertNoError(err, t, "EvalVariable(\"s\") on frame 0")
})
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/proc/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ func (t *Target) createUnrecoveredPanicBreakpoint() {

// createFatalThrowBreakpoint creates the a breakpoint as runtime.fatalthrow.
func (t *Target) createFatalThrowBreakpoint() {
fatalpcs, err := FindFunctionLocation(t.Process, "runtime.fatalthrow", 0)
fatalpcs, err := FindFunctionLocation(t.Process, "runtime.throw", 0)
if err == nil {
bp, err := t.SetBreakpointWithID(fatalThrowID, fatalpcs[0])
if err == nil {
Expand Down
2 changes: 1 addition & 1 deletion service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2731,7 +2731,7 @@ func (s *Server) onExceptionInfoRequest(request *dap.ExceptionInfoRequest) {
}

func (s *Server) throwReason(goroutineID int) (string, error) {
return s.getExprString("s", goroutineID, 1)
return s.getExprString("s", goroutineID, 0)
}

func (s *Server) panicReason(goroutineID int) (string, error) {
Expand Down
8 changes: 2 additions & 6 deletions service/dap/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3689,10 +3689,6 @@ func TestPanicBreakpointOnNext(t *testing.T) {
}

func TestFatalThrowBreakpoint(t *testing.T) {
// This is not currently flaky for Go 1.17 see https://github.com/golang/go/issues/46425.
if goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) {
t.Skip()
}
runTest(t, "fatalerror", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
Expand Down Expand Up @@ -3750,9 +3746,9 @@ func TestFatalThrowBreakpoint(t *testing.T) {
client.ContinueRequest(1)
client.ExpectContinueResponse(t)

// TODO(suzmue): Enable this test for 1.17 when https://github.com/golang/go/issues/46425 is fixed.
// This does not work for Go 1.16 so skip by detecting versions before or after 1.16.
var text string
if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 16) {
if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 16) || goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) {
text = "\"all goroutines are asleep - deadlock!\""
}
se := client.ExpectStoppedEvent(t)
Expand Down

0 comments on commit f6681c6

Please sign in to comment.