proc: prevent internal breakpoint conditions from failing

An internal breakpoint condition shouldn't ever error:
* use a ThreadContext to evaluate conditions if a goroutine isn't
  available
* evaluate runtime.curg to a fake g variable containing only
  `goid == 0` when there is no current goroutine

Fixes #2113
This commit is contained in:
aarzilli 2020-08-08 15:06:14 +02:00 committed by Alessandro Arzilli
parent 5a5d5f9e68
commit f90134eb4d
6 changed files with 139 additions and 5 deletions

35
_fixtures/issue2113.go Normal file

@ -0,0 +1,35 @@
package main
import (
"fmt"
"runtime"
"sync"
)
func coroutine(i int, start, finish *sync.WaitGroup) {
defer finish.Done()
j := i * 2
if i == 99 {
runtime.Breakpoint()
start.Done()
} else {
start.Wait()
}
fmt.Println("hello ", i, j)
fmt.Println("goodbye", i, j)
}
func main() {
i := 0
var start, finish sync.WaitGroup
start.Add(1)
for ; i < 100; i++ {
finish.Add(1)
go coroutine(i, &start, &finish)
}
finish.Wait()
println(i)
}

@ -195,9 +195,12 @@ func evalBreakpointCondition(thread Thread, cond ast.Expr) (bool, error) {
return true, nil
}
scope, err := GoroutineScope(thread)
if err != nil {
scope, err = ThreadScope(thread)
if err != nil {
return true, err
}
}
v, err := scope.evalAST(cond)
if err != nil {
return true, fmt.Errorf("error evaluating expression: %v", err)

@ -729,7 +729,16 @@ func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
if maybePkg, ok := node.X.(*ast.Ident); ok {
if maybePkg.Name == "runtime" && node.Sel.Name == "curg" {
if scope.g == nil {
return nilVariable, nil
typ, err := scope.BinInfo.findType("runtime.g")
if err != nil {
return nil, fmt.Errorf("blah: %v", err)
}
gvar := newVariable("curg", fakeAddress, typ, scope.BinInfo, scope.Mem)
gvar.loaded = true
gvar.Flags = VariableFakeAddress
gvar.Children = append(gvar.Children, *newConstant(constant.MakeInt64(0), scope.Mem))
gvar.Children[0].Name = "goid"
return gvar, nil
}
return scope.g.variable.clone(), nil
} else if maybePkg.Name == "runtime" && node.Sel.Name == "frameoff" {

@ -8,6 +8,7 @@ import (
"go/constant"
"go/token"
"io/ioutil"
"math/rand"
"net"
"net/http"
"os"
@ -1106,7 +1107,7 @@ func TestProcessReceivesSIGCHLD(t *testing.T) {
func TestIssue239(t *testing.T) {
withTestProcess("is sue239", t, func(p *proc.Target, fixture protest.Fixture) {
setFileBreakpoint(p, t, fixture.Source, 17)
assertNoError(p.Continue(), t, fmt.Sprintf("Continue()"))
assertNoError(p.Continue(), t, "Continue()")
})
}
@ -4938,3 +4939,81 @@ func TestRequestManualStopWhileStopped(t *testing.T) {
t.Logf("done")
})
}
func TestStepOutPreservesGoroutine(t *testing.T) {
// Checks that StepOut preserves the currently selected goroutine.
if runtime.GOOS == "freebsd" {
t.Skip("XXX - not working")
}
rand.Seed(time.Now().Unix())
withTestProcess("issue2113", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue()")
logState := func() {
g := p.SelectedGoroutine()
var goid int = -42
if g != nil {
goid = g.ID
}
pc := currentPC(p, t)
f, l, fn := p.BinInfo().PCToLine(pc)
var fnname string = "???"
if fn != nil {
fnname = fn.Name
}
t.Logf("goroutine %d at %s:%d in %s", goid, f, l, fnname)
}
logState()
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
assertNoError(err, t, "GoroutinesInfo")
candg := []*proc.G{}
bestg := []*proc.G{}
for _, g := range gs {
frames, err := g.Stacktrace(20, 0)
assertNoError(err, t, "Stacktrace")
for _, frame := range frames {
if frame.Call.Fn != nil && frame.Call.Fn.Name == "main.coroutine" {
candg = append(candg, g)
if g.Thread != nil && frames[0].Call.Fn != nil && strings.HasPrefix(frames[0].Call.Fn.Name, "runtime.") {
bestg = append(bestg, g)
}
break
}
}
}
var pickg *proc.G
if len(bestg) > 0 {
pickg = bestg[rand.Intn(len(bestg))]
t.Logf("selected goroutine %d (best)\n", pickg.ID)
} else {
pickg = candg[rand.Intn(len(candg))]
t.Logf("selected goroutine %d\n", pickg.ID)
}
goid := pickg.ID
assertNoError(p.SwitchGoroutine(pickg), t, "SwitchGoroutine")
logState()
err = p.StepOut()
if err != nil {
_, isexited := err.(proc.ErrProcessExited)
if !isexited {
assertNoError(err, t, "StepOut()")
} else {
return
}
}
logState()
g2 := p.SelectedGoroutine()
if g2 == nil {
t.Fatalf("no selected goroutine after stepout")
} else if g2.ID != goid {
t.Fatalf("unexpected selected goroutine %d", g2.ID)
}
})
}

@ -377,7 +377,7 @@ func (dbp *Target) StepOut() error {
if topframe.Ret != 0 {
topframe, retframe := skipAutogeneratedWrappersOut(selg, curthread, &topframe, &retframe)
retFrameCond := astutil.And(sameGCond, frameoffCondition(retframe))
bp, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(topframe.Ret, NextBreakpoint, retFrameCond))
bp, err := allowDuplicateBreakpoint(dbp.SetBreakpoint(retframe.Current.PC, NextBreakpoint, retFrameCond))
if err != nil {
return err
}
@ -620,7 +620,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error {
// For inlined functions there is no need to do this, the set of PCs
// returned by the AllPCsBetween call above already cover all instructions
// of the containing function.
bp, err := dbp.SetBreakpoint(topframe.Ret, NextBreakpoint, retFrameCond)
bp, err := dbp.SetBreakpoint(retframe.Current.PC, NextBreakpoint, retFrameCond)
if _, isexists := err.(BreakpointExistsError); isexists {
if bp.Kind == NextBreakpoint {
// If the return address shares the same address with one of the lines

@ -1039,6 +1039,14 @@ func (v *Variable) structMember(memberName string) (*Variable, error) {
return v.clone(), nil
}
vname := v.Name
if v.loaded && (v.Flags&VariableFakeAddress) != 0 {
for i := range v.Children {
if v.Children[i].Name == memberName {
return &v.Children[i], nil
}
}
return nil, fmt.Errorf("%s has no member %s", vname, memberName)
}
switch v.Kind {
case reflect.Chan:
v = v.clone()