From f7b8acf35981eac97d57aa295fc8811765d3e72e Mon Sep 17 00:00:00 2001 From: aarzilli Date: Wed, 20 Dec 2023 11:14:02 +0100 Subject: [PATCH] proc: adds pointer pinning to call injection This commit adds a new mode to call injection. If the runtime.debugPinner function is available in the target executable it obtains a pinner by calling it and then uses it to pin the pointers in the results of call injection. This allows the code for call injection to be refactored to execute the calls in the normal order, since it doesn't need to be concerned with having space on the target's memory to store intermediate values. Updates #3310 --- pkg/proc/bininfo.go | 14 ++- pkg/proc/dwarf_export_test.go | 11 ++ pkg/proc/eval.go | 125 +++++++++++++++---- pkg/proc/evalop/evalcompile.go | 150 ++++++++++++++++++++--- pkg/proc/evalop/ops.go | 58 ++++++++- pkg/proc/fncall.go | 133 ++++++++++++++++----- pkg/proc/mem.go | 15 ++- pkg/proc/target_exec.go | 2 +- pkg/proc/types.go | 5 +- pkg/proc/variables.go | 6 +- pkg/proc/variables_test.go | 212 ++++++++++++++++++++------------- 11 files changed, 573 insertions(+), 158 deletions(-) diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 3f999946cf..d6eff4ea90 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -34,6 +34,7 @@ import ( "github.com/go-delve/delve/pkg/internal/gosym" "github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/proc/debuginfod" + "github.com/go-delve/delve/pkg/proc/evalop" "github.com/hashicorp/golang-lru/simplelru" ) @@ -110,7 +111,8 @@ type BinaryInfo struct { // Go 1.17 register ABI is enabled. regabi bool - logger logflags.Logger + debugPinnerFn *Function + logger logflags.Logger } var ( @@ -2574,13 +2576,23 @@ func (bi *BinaryInfo) LookupFunc() map[string][]*Function { } func (bi *BinaryInfo) lookupOneFunc(name string) *Function { + if name == evalop.DebugPinnerFunctionName && bi.debugPinnerFn != nil { + return bi.debugPinnerFn + } fns := bi.LookupFunc()[name] if fns == nil { return nil } + if name == evalop.DebugPinnerFunctionName { + bi.debugPinnerFn = fns[0] + } return fns[0] } +func (bi *BinaryInfo) hasDebugPinner() bool { + return bi.lookupOneFunc(evalop.DebugPinnerFunctionName) != nil +} + // loadDebugInfoMapsCompileUnit loads entry from a single compile unit. func (bi *BinaryInfo) loadDebugInfoMapsCompileUnit(ctxt *loadDebugInfoMapsContext, image *Image, reader *reader.Reader, cu *compileUnit) { hasAttrGoPkgName := goversion.ProducerAfterOrEqual(cu.producer, 1, 13) diff --git a/pkg/proc/dwarf_export_test.go b/pkg/proc/dwarf_export_test.go index 7add16c718..3431f93baa 100644 --- a/pkg/proc/dwarf_export_test.go +++ b/pkg/proc/dwarf_export_test.go @@ -30,3 +30,14 @@ func NewCompositeMemory(p *Target, pieces []op.Piece, base uint64) (*compositeMe func IsJNZ(inst archInst) bool { return inst.(*x86Inst).Op == x86asm.JNE } + +// HasDebugPinner returns true if the target has runtime.debugPinner. +func (bi *BinaryInfo) HasDebugPinner() bool { + return bi.hasDebugPinner() +} + +// DebugPinCount returns the number of addresses pinned during the last +// function call injection. +func DebugPinCount() int { + return debugPinCount +} diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index 08133d6547..da4679fee9 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -186,9 +186,17 @@ func GoroutineScope(t *Target, thread Thread) (*EvalScope, error) { return FrameToScope(t, thread.ProcessMemory(), g, threadID, locations...), nil } +func (scope *EvalScope) evalopFlags() evalop.Flags { + flags := evalop.Flags(0) + if scope.BinInfo.hasDebugPinner() { + flags |= evalop.HasDebugPinner + } + return flags +} + // EvalExpression returns the value of the given expression. func (scope *EvalScope) EvalExpression(expr string, cfg LoadConfig) (*Variable, error) { - ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, false) + ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, scope.evalopFlags()) if err != nil { return nil, err } @@ -642,7 +650,7 @@ func (scope *EvalScope) setValue(dstv, srcv *Variable, srcExpr string) error { // SetVariable sets the value of the named variable func (scope *EvalScope) SetVariable(name, value string) error { - ops, err := evalop.CompileSet(scopeToEvalLookup{scope}, name, value) + ops, err := evalop.CompileSet(scopeToEvalLookup{scope}, name, value, scope.evalopFlags()) if err != nil { return err } @@ -828,9 +836,13 @@ type evalStack struct { scope *EvalScope curthread Thread lastRetiredFncall *functionCallState + debugPinner *Variable } func (s *evalStack) push(v *Variable) { + if v == nil { + panic(errors.New("internal debugger error, nil pushed onto variables stack")) + } s.stack = append(s.stack, v) } @@ -944,7 +956,7 @@ func (stack *evalStack) run() { stack.executeOp() // If the instruction we just executed requests the call injection // protocol by setting callInjectionContinue we switch to it. - if stack.callInjectionContinue { + if stack.callInjectionContinue && stack.err == nil { scope.callCtx.injectionThread = nil return } @@ -959,25 +971,35 @@ func (stack *evalStack) run() { // injections before returning. if len(stack.fncalls) > 0 { + fncallLog("undoing calls (%v)", stack.err) fncall := stack.fncallPeek() if fncall == stack.lastRetiredFncall { stack.err = fmt.Errorf("internal debugger error: could not undo injected call during error recovery, original error: %v", stack.err) return } if fncall.undoInjection != nil { - // setTargetExecuted is set if evalop.CallInjectionSetTarget has been - // executed but evalop.CallInjectionComplete hasn't, we must undo the callOP - // call in evalop.CallInjectionSetTarget before continuing. - switch scope.BinInfo.Arch.Name { - case "amd64": - regs, _ := curthread.Registers() - setSP(curthread, regs.SP()+uint64(scope.BinInfo.Arch.PtrSize())) - setPC(curthread, fncall.undoInjection.oldpc) - case "arm64", "ppc64le": - setLR(curthread, fncall.undoInjection.oldlr) - setPC(curthread, fncall.undoInjection.oldpc) - default: - panic("not implemented") + if fncall.undoInjection.doComplete2 { + // doComplete2 is set if CallInjectionComplete{DoPinning: true} has been + // executed but CallInjectionComplete2 hasn't. + regs, err := curthread.Registers() + if err == nil { + callInjectionComplete2(scope, scope.BinInfo, fncall, regs, curthread) + } + } else { + // undoInjection is set if evalop.CallInjectionSetTarget has been + // executed but evalop.CallInjectionComplete hasn't, we must undo the callOP + // call in evalop.CallInjectionSetTarget before continuing. + switch scope.BinInfo.Arch.Name { + case "amd64": + regs, _ := curthread.Registers() + setSP(curthread, regs.SP()+uint64(scope.BinInfo.Arch.PtrSize())) + setPC(curthread, fncall.undoInjection.oldpc) + case "arm64", "ppc64le": + setLR(curthread, fncall.undoInjection.oldlr) + setPC(curthread, fncall.undoInjection.oldpc) + default: + panic("not implemented") + } } } stack.lastRetiredFncall = fncall @@ -1137,6 +1159,11 @@ func (stack *evalStack) executeOp() { case *evalop.Pop: stack.pop() + case *evalop.Roll: + rolled := stack.stack[len(stack.stack)-op.N-1] + copy(stack.stack[len(stack.stack)-op.N-1:], stack.stack[len(stack.stack)-op.N:]) + stack.stack[len(stack.stack)-1] = rolled + case *evalop.BuiltinCall: vars := make([]*Variable, len(op.Args)) for i := len(op.Args) - 1; i >= 0; i-- { @@ -1159,9 +1186,29 @@ func (stack *evalStack) executeOp() { stack.err = funcCallCopyOneArg(scope, fncall, actualArg, &fncall.formalArgs[op.ArgNum], curthread) case *evalop.CallInjectionComplete: - stack.fncallPeek().undoInjection = nil + fncall := stack.fncallPeek() + fncall.doPinning = op.DoPinning + if op.DoPinning { + fncall.undoInjection.doComplete2 = true + } else { + fncall.undoInjection = nil + } stack.callInjectionContinue = true + case *evalop.CallInjectionComplete2: + fncall := stack.fncallPeek() + if len(fncall.addrsToPin) != 0 { + stack.err = fmt.Errorf("internal debugger error: CallInjectionComplete2 called when there still are addresses to pin") + } + fncall.undoInjection = nil + regs, err := curthread.Registers() + if err == nil { + callInjectionComplete2(scope, scope.BinInfo, fncall, regs, curthread) + stack.callInjectionContinue = true + } else { + stack.err = err + } + case *evalop.CallInjectionStartSpecial: stack.callInjectionContinue = scope.callInjectionStartSpecial(stack, op, curthread) @@ -1173,6 +1220,26 @@ func (stack *evalStack) executeOp() { rhv := stack.pop() stack.err = scope.setValue(lhv, rhv, exprToString(op.Rhe)) + case *evalop.PushPinAddress: + debugPinCount++ + fncall := stack.fncallPeek() + addrToPin := fncall.addrsToPin[len(fncall.addrsToPin)-1] + fncall.addrsToPin = fncall.addrsToPin[:len(fncall.addrsToPin)-1] + typ, err := scope.BinInfo.findType("unsafe.Pointer") + if ptyp, ok := typ.(*godwarf.PtrType); err == nil && ok { + v := newVariable("", 0, typ, scope.BinInfo, scope.Mem) + v.Children = []Variable{*(newVariable("", uint64(addrToPin), ptyp.Type, scope.BinInfo, scope.Mem))} + stack.push(v) + } else { + stack.err = fmt.Errorf("can not pin address: %v", err) + } + + case *evalop.SetDebugPinner: + stack.debugPinner = stack.pop() + + case *evalop.PushDebugPinner: + stack.push(stack.debugPinner) + default: stack.err = fmt.Errorf("internal debugger error: unknown eval opcode: %#v", op) } @@ -1273,7 +1340,7 @@ func (stack *evalStack) pushIdent(scope *EvalScope, name string) (found bool) { } func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) { - ops, err := evalop.CompileAST(scopeToEvalLookup{scope}, t) + ops, err := evalop.CompileAST(scopeToEvalLookup{scope}, t, scope.evalopFlags()) if err != nil { return nil, err } @@ -1289,9 +1356,14 @@ func exprToString(t ast.Expr) string { } func (scope *EvalScope) evalJump(op *evalop.Jump, stack *evalStack) { - x := stack.peek() - if op.Pop { - stack.pop() + var x *Variable + + switch op.When { + case evalop.JumpIfTrue, evalop.JumpIfFalse, evalop.JumpIfAllocStringChecksFail: + x = stack.peek() + if op.Pop { + stack.pop() + } } var v bool @@ -1308,6 +1380,17 @@ func (scope *EvalScope) evalJump(op *evalop.Jump, stack *evalStack) { return } return + case evalop.JumpAlways: + stack.opidx = op.Target - 1 + return + case evalop.JumpIfPinningDone: + fncall := stack.fncallPeek() + if len(fncall.addrsToPin) == 0 { + stack.opidx = op.Target - 1 + } + return + default: + panic("internal error, bad jump condition") } if x.Kind != reflect.Bool { diff --git a/pkg/proc/evalop/evalcompile.go b/pkg/proc/evalop/evalcompile.go index 660c787942..14cb53049d 100644 --- a/pkg/proc/evalop/evalcompile.go +++ b/pkg/proc/evalop/evalcompile.go @@ -18,7 +18,8 @@ import ( ) var ( - ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'") + ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'") + DebugPinnerFunctionName = "runtime.debugPinnerV1" ) type compileCtx struct { @@ -26,6 +27,8 @@ type compileCtx struct { ops []Op allowCalls bool curCall int + flags Flags + firstCall bool } type evalLookup interface { @@ -33,14 +36,24 @@ type evalLookup interface { HasBuiltin(string) bool } +// Flags describes flags used to control Compile and CompileAST +type Flags uint8 + +const ( + CanSet Flags = 1 << iota // Assignment is allowed + HasDebugPinner // runtime.debugPinner is available +) + // CompileAST compiles the expression t into a list of instructions. -func CompileAST(lookup evalLookup, t ast.Expr) ([]Op, error) { - ctx := &compileCtx{evalLookup: lookup, allowCalls: true} +func CompileAST(lookup evalLookup, t ast.Expr, flags Flags) ([]Op, error) { + ctx := &compileCtx{evalLookup: lookup, allowCalls: true, flags: flags, firstCall: true} err := ctx.compileAST(t) if err != nil { return nil, err } + ctx.compileDebugUnpin() + err = ctx.depthCheck(1) if err != nil { return ctx.ops, err @@ -50,18 +63,18 @@ func CompileAST(lookup evalLookup, t ast.Expr) ([]Op, error) { // Compile compiles the expression expr into a list of instructions. // If canSet is true expressions like "x = y" are also accepted. -func Compile(lookup evalLookup, expr string, canSet bool) ([]Op, error) { +func Compile(lookup evalLookup, expr string, flags Flags) ([]Op, error) { t, err := parser.ParseExpr(expr) if err != nil { - if canSet { + if flags&CanSet != 0 { eqOff, isAs := isAssignment(err) if isAs { - return CompileSet(lookup, expr[:eqOff], expr[eqOff+1:]) + return CompileSet(lookup, expr[:eqOff], expr[eqOff+1:], flags) } } return nil, err } - return CompileAST(lookup, t) + return CompileAST(lookup, t, flags) } func isAssignment(err error) (int, bool) { @@ -74,7 +87,7 @@ func isAssignment(err error) (int, bool) { // CompileSet compiles the expression setting lhexpr to rhexpr into a list of // instructions. -func CompileSet(lookup evalLookup, lhexpr, rhexpr string) ([]Op, error) { +func CompileSet(lookup evalLookup, lhexpr, rhexpr string, flags Flags) ([]Op, error) { lhe, err := parser.ParseExpr(lhexpr) if err != nil { return nil, err @@ -84,7 +97,7 @@ func CompileSet(lookup evalLookup, lhexpr, rhexpr string) ([]Op, error) { return nil, err } - ctx := &compileCtx{evalLookup: lookup, allowCalls: true} + ctx := &compileCtx{evalLookup: lookup, allowCalls: true, flags: flags, firstCall: true} err = ctx.compileAST(rhe) if err != nil { return nil, err @@ -120,13 +133,17 @@ func (ctx *compileCtx) compileAllocLiteralString() { &PushLen{}, &PushNil{}, &PushConst{constant.MakeBool(false)}, - }) + }, true) ctx.pushOp(&ConvertAllocToString{}) jmp.Target = len(ctx.ops) } -func (ctx *compileCtx) compileSpecialCall(fnname string, argAst []ast.Expr, args []Op) { +func (ctx *compileCtx) compileSpecialCall(fnname string, argAst []ast.Expr, args []Op, doPinning bool) { + if doPinning { + ctx.compileGetDebugPinner() + } + id := ctx.curCall ctx.curCall++ ctx.pushOp(&CallInjectionStartSpecial{ @@ -136,11 +153,40 @@ func (ctx *compileCtx) compileSpecialCall(fnname string, argAst []ast.Expr, args ctx.pushOp(&CallInjectionSetTarget{id: id}) for i := range args { - ctx.pushOp(args[i]) + if args[i] != nil { + ctx.pushOp(args[i]) + } ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i}) } - ctx.pushOp(&CallInjectionComplete{id: id}) + doPinning = doPinning && (ctx.flags&HasDebugPinner != 0) + + ctx.pushOp(&CallInjectionComplete{id: id, DoPinning: doPinning}) + + if doPinning { + ctx.compilePinningLoop(id) + } +} + +func (ctx *compileCtx) compileGetDebugPinner() { + if ctx.firstCall && ctx.flags&HasDebugPinner != 0 { + ctx.compileSpecialCall(DebugPinnerFunctionName, []ast.Expr{}, []Op{}, false) + ctx.pushOp(&SetDebugPinner{}) + ctx.firstCall = false + } +} + +func (ctx *compileCtx) compileDebugUnpin() { + if !ctx.firstCall && ctx.flags&HasDebugPinner != 0 { + ctx.compileSpecialCall("runtime.(*Pinner).Unpin", []ast.Expr{ + &ast.Ident{Name: "debugPinner"}, + }, []Op{ + &PushDebugPinner{}, + }, false) + ctx.pushOp(&Pop{}) + ctx.pushOp(&PushNil{}) + ctx.pushOp(&SetDebugPinner{}) + } } func (ctx *compileCtx) pushOp(op Op) { @@ -172,6 +218,8 @@ func (ctx *compileCtx) depthCheck(endDepth int) error { } } + debugPinnerSeen := false + for i, op := range ctx.ops { npop, npush := op.depthCheck() if depth[i] < npop { @@ -179,8 +227,15 @@ func (ctx *compileCtx) depthCheck(endDepth int) error { } d := depth[i] - npop + npush checkAndSet(i+1, d) - if jmp, _ := op.(*Jump); jmp != nil { - checkAndSet(jmp.Target, d) + switch op := op.(type) { + case *Jump: + checkAndSet(op.Target, d) + case *CallInjectionStartSpecial: + debugPinnerSeen = true + case *CallInjectionComplete: + if op.DoPinning && !debugPinnerSeen { + err = fmt.Errorf("internal debugger error: pinning call injection seen before call to %s at instrution %d", DebugPinnerFunctionName, i) + } } if err != nil { return err @@ -521,6 +576,16 @@ func (ctx *compileCtx) compileFunctionCall(node *ast.CallExpr) error { id := ctx.curCall ctx.curCall++ + if ctx.flags&HasDebugPinner != 0 { + return ctx.compileFunctionCallWithPinning(node, id) + } + + return ctx.compileFunctionCallNoPinning(node, id) +} + +// compileFunctionCallNoPinning compiles a function call when runtime.debugPinner is +// not available in the target. +func (ctx *compileCtx) compileFunctionCallNoPinning(node *ast.CallExpr, id int) error { oldAllowCalls := ctx.allowCalls oldOps := ctx.ops ctx.allowCalls = false @@ -570,6 +635,61 @@ func (ctx *compileCtx) compileFunctionCall(node *ast.CallExpr) error { return nil } +// compileFunctionCallWithPinning compiles a function call when runtime.debugPinner +// is available in the target. +func (ctx *compileCtx) compileFunctionCallWithPinning(node *ast.CallExpr, id int) error { + ctx.compileGetDebugPinner() + + err := ctx.compileAST(node.Fun) + if err != nil { + return err + } + + for i, arg := range node.Args { + err := ctx.compileAST(arg) + if isStringLiteral(arg) { + ctx.compileAllocLiteralString() + } + if err != nil { + return fmt.Errorf("error evaluating %q as argument %d in function %s: %v", exprToString(arg), i+1, exprToString(node.Fun), err) + } + } + + ctx.pushOp(&Roll{len(node.Args)}) + ctx.pushOp(&CallInjectionStart{HasFunc: true, id: id, Node: node}) + ctx.pushOp(&Pop{}) + ctx.pushOp(&CallInjectionSetTarget{id: id}) + + for i := len(node.Args) - 1; i >= 0; i-- { + arg := node.Args[i] + ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i, ArgExpr: arg}) + } + + ctx.pushOp(&CallInjectionComplete{id: id, DoPinning: true}) + + ctx.compilePinningLoop(id) + + return nil +} + +func (ctx *compileCtx) compilePinningLoop(id int) { + loopStart := len(ctx.ops) + jmp := &Jump{When: JumpIfPinningDone} + ctx.pushOp(jmp) + ctx.pushOp(&PushPinAddress{}) + ctx.compileSpecialCall("runtime.(*Pinner).Pin", []ast.Expr{ + &ast.Ident{Name: "debugPinner"}, + &ast.Ident{Name: "pinAddress"}, + }, []Op{ + &PushDebugPinner{}, + nil, + }, false) + ctx.pushOp(&Pop{}) + ctx.pushOp(&Jump{When: JumpAlways, Target: loopStart}) + jmp.Target = len(ctx.ops) + ctx.pushOp(&CallInjectionComplete2{id: id}) +} + func Listing(depth []int, ops []Op) string { if depth == nil { depth = make([]int, len(ops)+1) diff --git a/pkg/proc/evalop/ops.go b/pkg/proc/evalop/ops.go index 43ae96c563..d64c099545 100644 --- a/pkg/proc/evalop/ops.go +++ b/pkg/proc/evalop/ops.go @@ -164,8 +164,13 @@ type Jump struct { } func (jmpif *Jump) depthCheck() (npop, npush int) { - if jmpif.Pop { - return 1, 0 + switch jmpif.When { + case JumpIfTrue, JumpIfFalse, JumpIfAllocStringChecksFail: + if jmpif.Pop { + return 1, 0 + } else { + return 1, 1 + } } return 0, 0 } @@ -177,6 +182,8 @@ const ( JumpIfFalse JumpCond = iota JumpIfTrue JumpIfAllocStringChecksFail + JumpAlways + JumpIfPinningDone ) // Binary pops two variables from the stack, applies the specified binary @@ -200,6 +207,13 @@ type Pop struct { func (*Pop) depthCheck() (npop, npush int) { return 1, 0 } +// Roll removes the n-th element of the stack and pushes it back in at the top +type Roll struct { + N int +} + +func (*Roll) depthCheck() (npop, npush int) { return 1, 1 } + // BuiltinCall pops len(Args) argument from the stack, calls the specified // builtin on them and pushes the result back on the stack. type BuiltinCall struct { @@ -240,11 +254,30 @@ type CallInjectionCopyArg struct { func (*CallInjectionCopyArg) depthCheck() (npop, npush int) { return 1, 0 } // CallInjectionComplete resumes target execution so that the injected call can run. +// If DoPinning is true it stops after the call is completed without undoing +// the call injection frames so that address pinning for the return value +// can be performed, see CallInjectionComplete2. type CallInjectionComplete struct { + id int + DoPinning bool +} + +func (op *CallInjectionComplete) depthCheck() (npop, npush int) { + if op.DoPinning { + return 0, 0 + } else { + return 0, 1 + } +} + +// CallInjectionComplete2 if DoPinning was passed to CallInjectionComplete +// this will finish the call injection protocol and push the evaluation +// result on the stack. +type CallInjectionComplete2 struct { id int } -func (*CallInjectionComplete) depthCheck() (npop, npush int) { return 0, 1 } +func (*CallInjectionComplete2) depthCheck() (npop, npush int) { return 0, 1 } // CallInjectionStartSpecial starts call injection for a function with a // name and arguments known at compile time. @@ -272,3 +305,22 @@ type SetValue struct { } func (*SetValue) depthCheck() (npop, npush int) { return 2, 0 } + +// SetDebugPinner pops one variable from the stack and uses it as the saved debug pinner. +type SetDebugPinner struct { +} + +func (*SetDebugPinner) depthCheck() (npop, npush int) { return 1, 0 } + +// PushDebugPinner pushes the debug pinner on the stack. +type PushDebugPinner struct { +} + +func (*PushDebugPinner) depthCheck() (npop, npush int) { return 0, 1 } + +// PushPinAddress pushes an address to pin on the stack (as an +// unsafe.Pointer) and removes it from the list of addresses to pin. +type PushPinAddress struct { +} + +func (*PushPinAddress) depthCheck() (npop, npush int) { return 0, 1 } diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index f045c3ab1c..02d4803f01 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -8,6 +8,7 @@ import ( "go/ast" "go/constant" "reflect" + "slices" "sort" "strconv" "strings" @@ -42,6 +43,9 @@ import ( // - evalop.CallInjectionSetTarget // - evalCallInjectionCopyArg // - evalCallInjectionComplete +// +// When the target has runtime.debugPinner then evalCallInjectionPinPointer +// must be also called in a loop until it returns false. const ( debugCallFunctionNamePrefix1 = "debugCall" @@ -89,12 +93,20 @@ type functionCallState struct { // it contains information on how to undo a function call injection without running it undoInjection *undoInjection + // hasDebugPinner is true if the target has runtime.debugPinner + hasDebugPinner bool + // doPinning is true if this call injection should pin the results + doPinning bool + // addrsToPin addresses from return variables that should be pinned + addrsToPin []uint64 + protocolReg uint64 debugCallName string } type undoInjection struct { oldpc, oldlr uint64 + doComplete2 bool } type callContext struct { @@ -123,10 +135,14 @@ type callInjection struct { endCallInjection func() } +//lint:ignore U1000 this variable is only used by tests +var debugPinCount int + // EvalExpressionWithCalls is like EvalExpression but allows function calls in 'expr'. // Because this can only be done in the current goroutine, unlike // EvalExpression, EvalExpressionWithCalls is not a method of EvalScope. func EvalExpressionWithCalls(grp *TargetGroup, g *G, expr string, retLoadCfg LoadConfig, checkEscape bool) error { + debugPinCount = 0 t := grp.Selected bi := t.BinInfo() if !t.SupportsFunctionCalls() { @@ -172,7 +188,7 @@ func EvalExpressionWithCalls(grp *TargetGroup, g *G, expr string, retLoadCfg Loa return err } - ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, true) + ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, scope.evalopFlags()|evalop.CanSet) if err != nil { return err } @@ -186,7 +202,7 @@ func EvalExpressionWithCalls(grp *TargetGroup, g *G, expr string, retLoadCfg Loa } stack.eval(scope, ops) - if stack.callInjectionContinue { + if stack.callInjectionContinue && stack.err == nil { return grp.Continue() } @@ -289,11 +305,19 @@ func (scope *EvalScope) evalCallInjectionStart(op *evalop.CallInjectionStart, st return } + for _, v := range stack.stack { + if v.Flags&(VariableFakeAddress|VariableCPURegister|variableSaved) != 0 || v.Unreadable != nil || v.DwarfType == nil || v.RealType == nil || v.Addr == 0 { + continue + } + saveVariable(v) + } + fncall := functionCallState{ - expr: op.Node, - savedRegs: regs, - protocolReg: protocolReg, - debugCallName: dbgcallfn.Name, + expr: op.Node, + savedRegs: regs, + protocolReg: protocolReg, + debugCallName: dbgcallfn.Name, + hasDebugPinner: scope.BinInfo.hasDebugPinner(), } if op.HasFunc { @@ -365,10 +389,18 @@ func (scope *EvalScope) evalCallInjectionStart(op *evalop.CallInjectionStart, st p.fncallForG[scope.g.ID].startThreadID = thread.ThreadID() stack.fncallPush(&fncall) - stack.push(newConstant(constant.MakeBool(fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0), scope.Mem)) + stack.push(newConstant(constant.MakeBool(!fncall.hasDebugPinner && (fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0)), scope.Mem)) stack.callInjectionContinue = true } +func saveVariable(v *Variable) { + v.mem = cacheMemory(v.mem, v.Addr, int(v.RealType.Size())) + v.Flags |= variableSaved + if cachemem, ok := v.mem.(*memCache); ok { + v.Unreadable = cachemem.load() + } +} + func funcCallFinish(scope *EvalScope, stack *evalStack) { fncall := stack.fncallPop() if fncall.err != nil { @@ -697,7 +729,7 @@ func allPointers(v *Variable, name string, f func(addr uint64, name string) erro return fmt.Errorf("escape check for %s failed, variable unreadable: %v", name, v.Unreadable) } switch v.Kind { - case reflect.Ptr: + case reflect.Ptr, reflect.UnsafePointer: var w *Variable if len(v.Children) == 1 { // this branch is here to support pointers constructed with typecasts from ints or the '&' operator @@ -713,6 +745,12 @@ func allPointers(v *Variable, name string, f func(addr uint64, name string) erro sv.RealType = resolveTypedef(&(v.RealType.(*godwarf.MapType).TypedefType)) sv = sv.maybeDereference() return f(sv.Addr, name) + case reflect.Interface: + sv := v.clone() + sv.RealType = resolveTypedef(&(v.RealType.(*godwarf.InterfaceType).TypedefType)) + sv = sv.maybeDereference() + sv.Kind = reflect.Struct + return allPointers(sv, name, f) case reflect.Struct: t := v.RealType.(*godwarf.StructType) for _, field := range t.Field { @@ -732,6 +770,10 @@ func allPointers(v *Variable, name string, f func(addr uint64, name string) erro if err := f(v.funcvalAddr(), name); err != nil { return err } + case reflect.Complex64, reflect.Complex128, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Bool, reflect.Float32, reflect.Float64: + // nothing to do + default: + panic(fmt.Errorf("not implemented: %s", v.Kind)) } return nil @@ -864,29 +906,32 @@ func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool { return (v.Flags & VariableReturnArgument) != 0 }) - loadValues(fncall.retvars, callScope.callCtx.retLoadCfg) + if !fncall.doPinning { + loadValues(fncall.retvars, callScope.callCtx.retLoadCfg) + } for _, v := range fncall.retvars { v.Flags |= VariableFakeAddress } - // Store the stack span of the currently running goroutine (which in Go >= - // 1.15 might be different from the original injection goroutine) so that - // later on we can use it to perform the escapeCheck - if threadg, _ := GetG(thread); threadg != nil { - callScope.callCtx.stacks = append(callScope.callCtx.stacks, threadg.stack) - } - if bi.Arch.Name == "arm64" || bi.Arch.Name == "ppc64le" { - oldlr, err := readUintRaw(thread.ProcessMemory(), regs.SP(), int64(bi.Arch.PtrSize())) - if err != nil { - fncall.err = fmt.Errorf("could not restore LR: %v", err) - break - } - if err = setLR(thread, oldlr); err != nil { - fncall.err = fmt.Errorf("could not restore LR: %v", err) - break + if fncall.doPinning { + stack.callInjectionContinue = false + for _, v := range fncall.retvars { + saveVariable(v) + allPointers(v, "", func(addr uint64, _ string) error { + if addr != 0 && pointerEscapes(addr, callScope.g.stack, callScope.callCtx.stacks) { + fncall.addrsToPin = append(fncall.addrsToPin, addr) + } + return nil + }) } + slices.Sort(fncall.addrsToPin) + fncall.addrsToPin = slices.Compact(fncall.addrsToPin) + + return false // will continue with evalop.CallInjectionComplete2 } + callInjectionComplete2(callScope, bi, fncall, regs, thread) + case debugCallRegReadPanic: // 2 // read panic value from stack stack.callInjectionContinue = true @@ -913,9 +958,29 @@ func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool { return false } +func callInjectionComplete2(callScope *EvalScope, bi *BinaryInfo, fncall *functionCallState, regs Registers, thread Thread) { + // Store the stack span of the currently running goroutine (which in Go >= + // 1.15 might be different from the original injection goroutine) so that + // later on we can use it to perform the escapeCheck + if threadg, _ := GetG(thread); threadg != nil { + callScope.callCtx.stacks = append(callScope.callCtx.stacks, threadg.stack) + } + if bi.Arch.Name == "arm64" || bi.Arch.Name == "ppc64le" { + oldlr, err := readUintRaw(thread.ProcessMemory(), regs.SP(), int64(bi.Arch.PtrSize())) + if err != nil { + fncall.err = fmt.Errorf("could not restore LR: %v", err) + return + } + if err = setLR(thread, oldlr); err != nil { + fncall.err = fmt.Errorf("could not restore LR: %v", err) + return + } + } +} + func (scope *EvalScope) evalCallInjectionSetTarget(op *evalop.CallInjectionSetTarget, stack *evalStack, thread Thread) { fncall := stack.fncallPeek() - if fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0 { + if !fncall.hasDebugPinner && (fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0) { funcCallEvalFuncExpr(scope, stack, fncall) } stack.pop() // target function, consumed by funcCallEvalFuncExpr either above or in evalop.CallInjectionStart @@ -944,7 +1009,7 @@ func (scope *EvalScope) evalCallInjectionSetTarget(op *evalop.CallInjectionSetTa if fncall.receiver != nil { err := funcCallCopyOneArg(scope, fncall, fncall.receiver, &fncall.formalArgs[0], thread) if err != nil { - stack.err = err + stack.err = fmt.Errorf("could not set call receiver: %v", err) return } fncall.formalArgs = fncall.formalArgs[1:] @@ -995,6 +1060,9 @@ func fakeFunctionEntryScope(scope *EvalScope, fn *Function, cfa int64, sp uint64 func (scope *EvalScope) callInjectionStartSpecial(stack *evalStack, op *evalop.CallInjectionStartSpecial, curthread Thread) bool { fnv, err := scope.findGlobalInternal(op.FnName) if fnv == nil { + if err == nil { + err = fmt.Errorf("function %s not found", op.FnName) + } stack.err = err return false } @@ -1013,6 +1081,9 @@ func (scope *EvalScope) callInjectionStartSpecial(stack *evalStack, op *evalop.C func (scope *EvalScope) convertAllocToString(stack *evalStack) { mallocv := stack.pop() v := stack.pop() + + mallocv.loadValue(loadFullValue) + if mallocv.Unreadable != nil { stack.err = mallocv.Unreadable return @@ -1058,7 +1129,7 @@ func isCallInjectionStop(t *Target, thread Thread, loc *Location) bool { // callInjectionProtocol is the function called from Continue to progress // the injection protocol for all threads. // Returns true if a call injection terminated -func callInjectionProtocol(t *Target, threads []Thread) (done bool, err error) { +func callInjectionProtocol(t *Target, trapthread Thread, threads []Thread) (done bool, err error) { if len(t.fncallForG) == 0 { // we aren't injecting any calls, no need to check the threads. return false, nil @@ -1072,6 +1143,9 @@ func callInjectionProtocol(t *Target, threads []Thread) (done bool, err error) { if err != nil { continue } + if (thread.ThreadID() != trapthread.ThreadID()) && !thread.SoftExc() { + continue + } if !isCallInjectionStop(t, thread, loc) { continue } @@ -1180,7 +1254,10 @@ func debugCallProtocolReg(archName string, version int) (uint64, bool) { // runtimeWhitelist is a list of functions in the runtime that we can call // (through call injection) even if they are optimized. var runtimeWhitelist = map[string]bool{ - "runtime.mallocgc": true, + "runtime.mallocgc": true, + evalop.DebugPinnerFunctionName: true, + "runtime.(*Pinner).Unpin": true, + "runtime.(*Pinner).Pin": true, } // runtimeOptimizedWorkaround modifies the input DIE so that arguments and diff --git a/pkg/proc/mem.go b/pkg/proc/mem.go index 5f2ad383a7..530ca5c0f9 100644 --- a/pkg/proc/mem.go +++ b/pkg/proc/mem.go @@ -43,14 +43,25 @@ func (m *memCache) contains(addr uint64, size int) bool { return addr >= m.cacheAddr && end <= m.cacheAddr+uint64(len(m.cache)) } +func (m *memCache) load() error { + if m.loaded { + return nil + } + _, err := m.mem.ReadMemory(m.cache, m.cacheAddr) + if err != nil { + return err + } + m.loaded = true + return nil +} + func (m *memCache) ReadMemory(data []byte, addr uint64) (n int, err error) { if m.contains(addr, len(data)) { if !m.loaded { - _, err := m.mem.ReadMemory(m.cache, m.cacheAddr) + err := m.load() if err != nil { return 0, err } - m.loaded = true } copy(data, m.cache[addr-m.cacheAddr:]) return len(data), nil diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go index cbc9389b1e..b6ee2e2fe5 100644 --- a/pkg/proc/target_exec.go +++ b/pkg/proc/target_exec.go @@ -159,7 +159,7 @@ func (grp *TargetGroup) Continue() error { log.Debugf("\t%d PC=%#x", th.ThreadID(), regs.PC()) } } - callInjectionDoneThis, callErrThis := callInjectionProtocol(dbp, threads) + callInjectionDoneThis, callErrThis := callInjectionProtocol(dbp, trapthread, threads) callInjectionDone = callInjectionDone || callInjectionDoneThis if callInjectionDoneThis { dbp.StopReason = StopCallReturned diff --git a/pkg/proc/types.go b/pkg/proc/types.go index bcfc610773..eaa2b7719f 100644 --- a/pkg/proc/types.go +++ b/pkg/proc/types.go @@ -206,7 +206,10 @@ func dwarfToRuntimeType(bi *BinaryInfo, mem MemoryReadWriter, typ godwarf.Type) if kindv == nil || kindv.Unreadable != nil || kindv.Kind != reflect.Uint { kindv = _type.loadFieldNamed("Kind_") } - if kindv == nil || kindv.Unreadable != nil || kindv.Kind != reflect.Uint { + if kindv == nil { + return 0, 0, false, fmt.Errorf("unreadable interace type (no kind field)") + } + if kindv.Unreadable != nil || kindv.Kind != reflect.Uint { return 0, 0, false, fmt.Errorf("unreadable interface type: %v", kindv.Unreadable) } typeKind, _ = constant.Uint64Val(kindv.Value) diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index 95c1222d31..1c3c19fd6a 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -89,6 +89,8 @@ const ( // variableTrustLen means that when this variable is loaded its length // should be trusted and used instead of MaxArrayValues variableTrustLen + + variableSaved ) // Variable represents a variable. It contains the address, name, @@ -1235,7 +1237,7 @@ func (v *Variable) maybeDereference() *Variable { switch t := v.RealType.(type) { case *godwarf.PtrType: - if v.Addr == 0 && len(v.Children) == 1 && v.loaded { + if (v.Addr == 0 || v.Flags&VariableFakeAddress != 0) && len(v.Children) == 1 && v.loaded { // fake pointer variable constructed by casting an integer to a pointer type return &v.Children[0] } @@ -1469,7 +1471,7 @@ func convertToEface(srcv, dstv *Variable) error { } typeAddr, typeKind, runtimeTypeFound, err := dwarfToRuntimeType(srcv.bi, srcv.mem, srcv.RealType) if err != nil { - return err + return fmt.Errorf("can not convert value of type %s to %s: %v", srcv.DwarfType.String(), dstv.DwarfType.String(), err) } if !runtimeTypeFound || typeKind&kindDirectIface == 0 { return &typeConvErr{srcv.DwarfType, dstv.RealType} diff --git a/pkg/proc/variables_test.go b/pkg/proc/variables_test.go index 0fe452dafc..2e2ad1c2ca 100644 --- a/pkg/proc/variables_test.go +++ b/pkg/proc/variables_test.go @@ -912,14 +912,14 @@ func getEvalExpressionTestCases() []varTest { {"bytearray[0] * bytearray[0]", false, "144", "144", "uint8", nil}, // function call / typecast errors - {"unknownthing(1, 2)", false, "", "", "", errors.New("could not find symbol value for unknownthing")}, - {"(unknownthing)(1, 2)", false, "", "", "", errors.New("could not find symbol value for unknownthing")}, + {"unknownthing(1, 2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for unknownthing")}, + {"(unknownthing)(1, 2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for unknownthing")}, {"afunc(2)", false, "", "", "", errors.New("function calls not allowed without using 'call'")}, {"(afunc)(2)", false, "", "", "", errors.New("function calls not allowed without using 'call'")}, - {"(*afunc)(2)", false, "", "", "", errors.New("expression \"afunc\" (func()) can not be dereferenced")}, - {"unknownthing(2)", false, "", "", "", errors.New("could not find symbol value for unknownthing")}, - {"(*unknownthing)(2)", false, "", "", "", errors.New("could not find symbol value for unknownthing")}, - {"(*strings.Split)(2)", false, "", "", "", errors.New("could not find symbol value for strings")}, + {"(*afunc)(2)", false, "", "", "", errors.New("*")}, + {"unknownthing(2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for unknownthing")}, + {"(*unknownthing)(2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for unknownthing")}, + {"(*strings.Split)(2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for strings")}, // pretty printing special types {"tim1", false, `time.Time(1977-05-25T18:00:00Z)…`, `time.Time(1977-05-25T18:00:00Z)…`, "time.Time", nil}, @@ -977,6 +977,18 @@ func getEvalExpressionTestCases() []varTest { return testcases } +func altErrors(errs ...string) *altError { + return &altError{errs} +} + +type altError struct { + errs []string +} + +func (err *altError) Error() string { + return "[multiple alternatives]" +} + func TestEvalExpression(t *testing.T) { testcases := getEvalExpressionTestCases() protest.AllowRecording(t) @@ -1000,8 +1012,22 @@ func TestEvalExpression(t *testing.T) { if err == nil { t.Fatalf("Expected error %s, got no error (%s)", tc.err.Error(), tc.name) } - if tc.err.Error() != err.Error() { - t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error()) + switch e := tc.err.(type) { + case *altError: + ok := false + for _, tgtErr := range e.errs { + if tgtErr == err.Error() { + ok = true + break + } + } + if !ok { + t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error()) + } + default: + if tc.err.Error() != "*" && tc.err.Error() != err.Error() { + t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error()) + } } } }) @@ -1272,9 +1298,10 @@ func TestIssue1075(t *testing.T) { } type testCaseCallFunction struct { - expr string // call expression to evaluate - outs []string // list of return parameters in this format: :: - err error // if not nil should return an error + expr string // call expression to evaluate + outs []string // list of return parameters in this format: :: + err error // if not nil should return an error + pinCount int // where debugPinner is supported this is the number of pins created during the function call injection } func TestCallFunction(t *testing.T) { @@ -1286,126 +1313,126 @@ func TestCallFunction(t *testing.T) { var testcases = []testCaseCallFunction{ // Basic function call injection tests - {"call1(one, two)", []string{":int:3"}, nil}, - {"call1(one+two, 4)", []string{":int:7"}, nil}, - {"callpanic()", []string{`~panic:interface {}:interface {}(string) "callpanic panicked"`}, nil}, - {`stringsJoin(nil, "")`, []string{`:string:""`}, nil}, - {`stringsJoin(stringslice, comma)`, []string{`:string:"one,two,three"`}, nil}, - {`stringsJoin(stringslice, "~~")`, []string{`:string:"one~~two~~three"`}, nil}, - {`stringsJoin(s1, comma)`, nil, errors.New(`could not find symbol value for s1`)}, - {`stringsJoin(intslice, comma)`, nil, errors.New("can not convert value of type []int to []string")}, - {`noreturncall(2)`, nil, nil}, + {"call1(one, two)", []string{":int:3"}, nil, 0}, + {"call1(one+two, 4)", []string{":int:7"}, nil, 0}, + {"callpanic()", []string{`~panic:interface {}:interface {}(string) "callpanic panicked"`}, nil, 0}, + {`stringsJoin(nil, "")`, []string{`:string:""`}, nil, 0}, + {`stringsJoin(stringslice, comma)`, []string{`:string:"one,two,three"`}, nil, 1}, + {`stringsJoin(stringslice, "~~")`, []string{`:string:"one~~two~~three"`}, nil, 2}, + {`stringsJoin(s1, comma)`, nil, errors.New(`could not find symbol value for s1`), 1}, + {`stringsJoin(intslice, comma)`, nil, errors.New("can not convert value of type []int to []string"), 1}, + {`noreturncall(2)`, nil, nil, 0}, // Expression tests - {`square(2) + 1`, []string{":int:5"}, nil}, - {`intcallpanic(1) + 1`, []string{":int:2"}, nil}, - {`intcallpanic(0) + 1`, []string{`~panic:interface {}:interface {}(string) "panic requested"`}, nil}, - {`onetwothree(5)[1] + 2`, []string{":int:9"}, nil}, + {`square(2) + 1`, []string{":int:5"}, nil, 0}, + {`intcallpanic(1) + 1`, []string{":int:2"}, nil, 0}, + {`intcallpanic(0) + 1`, []string{`~panic:interface {}:interface {}(string) "panic requested"`}, nil, 0}, + {`onetwothree(5)[1] + 2`, []string{":int:9"}, nil, 1}, // Call types tests (methods, function pointers, etc.) // The following set of calls was constructed using https://docs.google.com/document/d/1bMwCey-gmqZVTpRax-ESeVuZGmjwbocYs1iHplK-cjo/pub as a reference - {`a.VRcvr(1)`, []string{`:string:"1 + 3 = 4"`}, nil}, // direct call of a method with value receiver / on a value + {`a.VRcvr(1)`, []string{`:string:"1 + 3 = 4"`}, nil, 1}, // direct call of a method with value receiver / on a value - {`a.PRcvr(2)`, []string{`:string:"2 - 3 = -1"`}, nil}, // direct call of a method with pointer receiver / on a value - {`pa.VRcvr(3)`, []string{`:string:"3 + 6 = 9"`}, nil}, // direct call of a method with value receiver / on a pointer - {`pa.PRcvr(4)`, []string{`:string:"4 - 6 = -2"`}, nil}, // direct call of a method with pointer receiver / on a pointer + {`a.PRcvr(2)`, []string{`:string:"2 - 3 = -1"`}, nil, 1}, // direct call of a method with pointer receiver / on a value + {`pa.VRcvr(3)`, []string{`:string:"3 + 6 = 9"`}, nil, 1}, // direct call of a method with value receiver / on a pointer + {`pa.PRcvr(4)`, []string{`:string:"4 - 6 = -2"`}, nil, 1}, // direct call of a method with pointer receiver / on a pointer - {`vable_pa.VRcvr(6)`, []string{`:string:"6 + 6 = 12"`}, nil}, // indirect call of method on interface / containing value with value method - {`pable_pa.PRcvr(7)`, []string{`:string:"7 - 6 = 1"`}, nil}, // indirect call of method on interface / containing pointer with value method - {`vable_a.VRcvr(5)`, []string{`:string:"5 + 3 = 8"`}, nil}, // indirect call of method on interface / containing pointer with pointer method + {`vable_pa.VRcvr(6)`, []string{`:string:"6 + 6 = 12"`}, nil, 1}, // indirect call of method on interface / containing value with value method + {`pable_pa.PRcvr(7)`, []string{`:string:"7 - 6 = 1"`}, nil, 1}, // indirect call of method on interface / containing pointer with value method + {`vable_a.VRcvr(5)`, []string{`:string:"5 + 3 = 8"`}, nil, 1}, // indirect call of method on interface / containing pointer with pointer method - {`pa.nonexistent()`, nil, errors.New("pa has no member nonexistent")}, - {`a.nonexistent()`, nil, errors.New("a has no member nonexistent")}, - {`vable_pa.nonexistent()`, nil, errors.New("vable_pa has no member nonexistent")}, - {`vable_a.nonexistent()`, nil, errors.New("vable_a has no member nonexistent")}, - {`pable_pa.nonexistent()`, nil, errors.New("pable_pa has no member nonexistent")}, + {`pa.nonexistent()`, nil, errors.New("pa has no member nonexistent"), 0}, + {`a.nonexistent()`, nil, errors.New("a has no member nonexistent"), 0}, + {`vable_pa.nonexistent()`, nil, errors.New("vable_pa has no member nonexistent"), 0}, + {`vable_a.nonexistent()`, nil, errors.New("vable_a has no member nonexistent"), 0}, + {`pable_pa.nonexistent()`, nil, errors.New("pable_pa has no member nonexistent"), 0}, - {`fn2glob(10, 20)`, []string{":int:30"}, nil}, // indirect call of func value / set to top-level func - {`fn2clos(11)`, []string{`:string:"1 + 6 + 11 = 18"`}, nil}, // indirect call of func value / set to func literal - {`fn2clos(12)`, []string{`:string:"2 + 6 + 12 = 20"`}, nil}, - {`fn2valmeth(13)`, []string{`:string:"13 + 6 = 19"`}, nil}, // indirect call of func value / set to value method - {`fn2ptrmeth(14)`, []string{`:string:"14 - 6 = 8"`}, nil}, // indirect call of func value / set to pointer method + {`fn2glob(10, 20)`, []string{":int:30"}, nil, 0}, // indirect call of func value / set to top-level func + {`fn2clos(11)`, []string{`:string:"1 + 6 + 11 = 18"`}, nil, 1}, // indirect call of func value / set to func literal + {`fn2clos(12)`, []string{`:string:"2 + 6 + 12 = 20"`}, nil, 1}, + {`fn2valmeth(13)`, []string{`:string:"13 + 6 = 19"`}, nil, 1}, // indirect call of func value / set to value method + {`fn2ptrmeth(14)`, []string{`:string:"14 - 6 = 8"`}, nil, 1}, // indirect call of func value / set to pointer method - {"fn2nil()", nil, errors.New("nil pointer dereference")}, + {"fn2nil()", nil, errors.New("nil pointer dereference"), 0}, - {"ga.PRcvr(2)", []string{`:string:"2 - 0 = 2"`}, nil}, + {"ga.PRcvr(2)", []string{`:string:"2 - 0 = 2"`}, nil, 1}, - {"x.CallMe()", nil, nil}, - {"x2.CallMe(5)", []string{":int:25"}, nil}, + {"x.CallMe()", nil, nil, 0}, + {"x2.CallMe(5)", []string{":int:25"}, nil, 0}, - {"\"delve\".CallMe()", nil, errors.New("\"delve\" (type string) is not a struct")}, + {"\"delve\".CallMe()", nil, errors.New("\"delve\" (type string) is not a struct"), 0}, // Nested function calls tests - {`onetwothree(intcallpanic(2))`, []string{`:[]int:[]int len: 3, cap: 3, [3,4,5]`}, nil}, - {`onetwothree(intcallpanic(0))`, []string{`~panic:interface {}:interface {}(string) "panic requested"`}, nil}, - {`onetwothree(intcallpanic(2)+1)`, []string{`:[]int:[]int len: 3, cap: 3, [4,5,6]`}, nil}, - {`onetwothree(intcallpanic("not a number"))`, nil, errors.New("can not convert \"not a number\" constant to int")}, + {`onetwothree(intcallpanic(2))`, []string{`:[]int:[]int len: 3, cap: 3, [3,4,5]`}, nil, 1}, + {`onetwothree(intcallpanic(0))`, []string{`~panic:interface {}:interface {}(string) "panic requested"`}, nil, 0}, + {`onetwothree(intcallpanic(2)+1)`, []string{`:[]int:[]int len: 3, cap: 3, [4,5,6]`}, nil, 1}, + {`onetwothree(intcallpanic("not a number"))`, nil, errors.New("can not convert \"not a number\" constant to int"), 1}, // Variable setting tests - {`pa2 = getAStructPtr(8); pa2`, []string{`pa2:*main.astruct:*main.astruct {X: 8}`}, nil}, + {`pa2 = getAStructPtr(8); pa2`, []string{`pa2:*main.astruct:*main.astruct {X: 8}`}, nil, 1}, // Escape tests - {"escapeArg(&a2)", nil, errors.New("cannot use &a2 as argument pa2 in function main.escapeArg: stack object passed to escaping pointer: pa2")}, + {"escapeArg(&a2)", nil, errors.New("cannot use &a2 as argument pa2 in function main.escapeArg: stack object passed to escaping pointer: pa2"), 0}, // Issue 1577 - {"1+2", []string{`::3`}, nil}, - {`"de"+"mo"`, []string{`::"demo"`}, nil}, + {"1+2", []string{`::3`}, nil, 0}, + {`"de"+"mo"`, []string{`::"demo"`}, nil, 0}, // Issue 3176 - {`ref.String()[0]`, []string{`:byte:98`}, nil}, - {`ref.String()[20]`, nil, errors.New("index out of bounds")}, + {`ref.String()[0]`, []string{`:byte:98`}, nil, 1}, + {`ref.String()[20]`, nil, errors.New("index out of bounds"), 1}, } var testcases112 = []testCaseCallFunction{ // string allocation requires trusted argument order, which we don't have in Go 1.11 - {`stringsJoin(stringslice, ",")`, []string{`:string:"one,two,three"`}, nil}, - {`str = "a new string"; str`, []string{`str:string:"a new string"`}, nil}, + {`stringsJoin(stringslice, ",")`, []string{`:string:"one,two,three"`}, nil, 2}, + {`str = "a new string"; str`, []string{`str:string:"a new string"`}, nil, 1}, // support calling optimized functions - {`strings.Join(nil, "")`, []string{`:string:""`}, nil}, - {`strings.Join(stringslice, comma)`, []string{`:string:"one,two,three"`}, nil}, - {`strings.Join(intslice, comma)`, nil, errors.New("can not convert value of type []int to []string")}, - {`strings.Join(stringslice, ",")`, []string{`:string:"one,two,three"`}, nil}, - {`strings.LastIndexByte(stringslice[1], 'w')`, []string{":int:1"}, nil}, - {`strings.LastIndexByte(stringslice[1], 'o')`, []string{":int:2"}, nil}, - {`d.Base.Method()`, []string{`:int:4`}, nil}, - {`d.Method()`, []string{`:int:4`}, nil}, + {`strings.Join(nil, "")`, []string{`:string:""`}, nil, 0}, + {`strings.Join(stringslice, comma)`, []string{`:string:"one,two,three"`}, nil, 1}, + {`strings.Join(intslice, comma)`, nil, errors.New("can not convert value of type []int to []string"), 1}, + {`strings.Join(stringslice, ",")`, []string{`:string:"one,two,three"`}, nil, 2}, + {`strings.LastIndexByte(stringslice[1], 'w')`, []string{":int:1"}, nil, 0}, + {`strings.LastIndexByte(stringslice[1], 'o')`, []string{":int:2"}, nil, 0}, + {`d.Base.Method()`, []string{`:int:4`}, nil, 0}, + {`d.Method()`, []string{`:int:4`}, nil, 0}, } var testcases113 = []testCaseCallFunction{ - {`curriedAdd(2)(3)`, []string{`:int:5`}, nil}, + {`curriedAdd(2)(3)`, []string{`:int:5`}, nil, 1}, // Method calls on a value returned by a function - {`getAStruct(3).VRcvr(1)`, []string{`:string:"1 + 3 = 4"`}, nil}, // direct call of a method with value receiver / on a value + {`getAStruct(3).VRcvr(1)`, []string{`:string:"1 + 3 = 4"`}, nil, 1}, // direct call of a method with value receiver / on a value - {`getAStruct(3).PRcvr(2)`, nil, errors.New("cannot use getAStruct(3).PRcvr as argument pa in function main.(*astruct).PRcvr: stack object passed to escaping pointer: pa")}, // direct call of a method with pointer receiver / on a value - {`getAStructPtr(6).VRcvr(3)`, []string{`:string:"3 + 6 = 9"`}, nil}, // direct call of a method with value receiver / on a pointer - {`getAStructPtr(6).PRcvr(4)`, []string{`:string:"4 - 6 = -2"`}, nil}, // direct call of a method with pointer receiver / on a pointer + {`getAStruct(3).PRcvr(2)`, nil, errors.New("could not set call receiver: cannot use getAStruct(3).PRcvr as argument pa in function main.(*astruct).PRcvr: stack object passed to escaping pointer: pa"), 0}, // direct call of a method with pointer receiver / on a value + {`getAStructPtr(6).VRcvr(3)`, []string{`:string:"3 + 6 = 9"`}, nil, 2}, // direct call of a method with value receiver / on a pointer + {`getAStructPtr(6).PRcvr(4)`, []string{`:string:"4 - 6 = -2"`}, nil, 2}, // direct call of a method with pointer receiver / on a pointer - {`getVRcvrableFromAStruct(3).VRcvr(6)`, []string{`:string:"6 + 3 = 9"`}, nil}, // indirect call of method on interface / containing value with value method - {`getPRcvrableFromAStructPtr(6).PRcvr(7)`, []string{`:string:"7 - 6 = 1"`}, nil}, // indirect call of method on interface / containing pointer with value method - {`getVRcvrableFromAStructPtr(6).VRcvr(5)`, []string{`:string:"5 + 6 = 11"`}, nil}, // indirect call of method on interface / containing pointer with pointer method + {`getVRcvrableFromAStruct(3).VRcvr(6)`, []string{`:string:"6 + 3 = 9"`}, nil, 3}, // indirect call of method on interface / containing value with value method + {`getPRcvrableFromAStructPtr(6).PRcvr(7)`, []string{`:string:"7 - 6 = 1"`}, nil, 3}, // indirect call of method on interface / containing pointer with value method + {`getVRcvrableFromAStructPtr(6).VRcvr(5)`, []string{`:string:"5 + 6 = 11"`}, nil, 3}, // indirect call of method on interface / containing pointer with pointer method } var testcasesBefore114After112 = []testCaseCallFunction{ - {`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`)}, + {`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`), 1}, } var testcases114 = []testCaseCallFunction{ - {`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`)}, + {`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`), 1}, } var testcases117 = []testCaseCallFunction{ - {`regabistacktest("one", "two", "three", "four", "five", 4)`, []string{`:string:"onetwo"`, `:string:"twothree"`, `:string:"threefour"`, `:string:"fourfive"`, `:string:"fiveone"`, ":uint8:8"}, nil}, - {`regabistacktest2(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`, []string{":int:3", ":int:5", ":int:7", ":int:9", ":int:11", ":int:13", ":int:15", ":int:17", ":int:19", ":int:11"}, nil}, - {`issue2698.String()`, []string{`:string:"1 2 3 4"`}, nil}, - {`issue3364.String()`, []string{`:string:"1 2"`}, nil}, - {`regabistacktest3(rast3, 5)`, []string{`:[10]string:[10]string ["onetwo","twothree","threefour","fourfive","fivesix","sixseven","sevenheight","heightnine","nineten","tenone"]`, ":uint8:15"}, nil}, - {`floatsum(1, 2)`, []string{":float64:3"}, nil}, + {`regabistacktest("one", "two", "three", "four", "five", 4)`, []string{`:string:"onetwo"`, `:string:"twothree"`, `:string:"threefour"`, `:string:"fourfive"`, `:string:"fiveone"`, ":uint8:8"}, nil, 10}, + {`regabistacktest2(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`, []string{":int:3", ":int:5", ":int:7", ":int:9", ":int:11", ":int:13", ":int:15", ":int:17", ":int:19", ":int:11"}, nil, 0}, + {`issue2698.String()`, []string{`:string:"1 2 3 4"`}, nil, 1}, + {`issue3364.String()`, []string{`:string:"1 2"`}, nil, 1}, + {`regabistacktest3(rast3, 5)`, []string{`:[10]string:[10]string ["onetwo","twothree","threefour","fourfive","fivesix","sixseven","sevenheight","heightnine","nineten","tenone"]`, ":uint8:15"}, nil, 10}, + {`floatsum(1, 2)`, []string{":float64:3"}, nil, 0}, } withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { @@ -1447,7 +1474,7 @@ func TestCallFunction(t *testing.T) { } // LEAVE THIS AS THE LAST ITEM, IT BREAKS THE TARGET PROCESS!!! - testCallFunction(t, grp, p, testCaseCallFunction{"-unsafe escapeArg(&a2)", nil, nil}) + testCallFunction(t, grp, p, testCaseCallFunction{"-unsafe escapeArg(&a2)", nil, nil, 0}) }) } @@ -1463,6 +1490,12 @@ func testCallFunctionSetBreakpoint(t *testing.T, p *proc.Target, grp *proc.Targe } func testCallFunction(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc testCaseCallFunction) { + t.Run(tc.expr, func(t *testing.T) { + testCallFunctionIntl(t, grp, p, tc) + }) +} + +func testCallFunctionIntl(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc testCaseCallFunction) { const unsafePrefix = "-unsafe " var callExpr, varExpr string @@ -1493,7 +1526,11 @@ func testCallFunction(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc te } if err != nil { - t.Fatalf("call %q: error %q", tc.expr, err.Error()) + if strings.HasPrefix(err.Error(), "internal debugger error") { + t.Fatalf("call %q: error %s", tc.expr, err.Error()) + } else { + t.Fatalf("call %q: error %q", tc.expr, err.Error()) + } } retvalsVar := p.CurrentThread().Common().ReturnValues(pnormalLoadConfig) @@ -1538,6 +1575,13 @@ func testCallFunction(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc te t.Fatalf("call %q, output parameter %d: expected value %q, got %q", tc.expr, i, tgtValue, cvs) } } + + if p.BinInfo().HasDebugPinner() { + t.Logf("\t(pins = %d)", proc.DebugPinCount()) + if proc.DebugPinCount() != tc.pinCount { + t.Fatalf("call %q, expected pin count %d, got %d", tc.expr, tc.pinCount, proc.DebugPinCount()) + } + } } func TestIssue1531(t *testing.T) {