diff --git a/_fixtures/fncall.go b/_fixtures/fncall.go index c837f919..4c39d276 100644 --- a/_fixtures/fncall.go +++ b/_fixtures/fncall.go @@ -76,6 +76,21 @@ func escapeArg(pa2 *a2struct) { globalPA2 = pa2 } +func square(x int) int { + return x * x +} + +func intcallpanic(a int) int { + if a == 0 { + panic("panic requested") + } + return a +} + +func onetwothree(n int) []int { + return []int{n + 1, n + 2, n + 3} +} + func main() { one, two := 1, 2 intslice := []int{1, 2, 3} @@ -98,5 +113,5 @@ func main() { runtime.Breakpoint() call1(one, two) fn2clos(2) - fmt.Println(one, two, zero, callpanic, callstacktrace, stringsJoin, intslice, stringslice, comma, a.VRcvr, a.PRcvr, pa, vable_a, vable_pa, pable_pa, fn2clos, fn2glob, fn2valmeth, fn2ptrmeth, fn2nil, ga, escapeArg, a2) + fmt.Println(one, two, zero, callpanic, callstacktrace, stringsJoin, intslice, stringslice, comma, a.VRcvr, a.PRcvr, pa, vable_a, vable_pa, pable_pa, fn2clos, fn2glob, fn2valmeth, fn2ptrmeth, fn2nil, ga, escapeArg, a2, square, intcallpanic, onetwothree) } diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index 1020c6c6..9d815178 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -23,8 +23,13 @@ var errOperationOnSpecialFloat = errors.New("operations on non-finite floats not // EvalExpression returns the value of the given expression. func (scope *EvalScope) EvalExpression(expr string, cfg LoadConfig) (*Variable, error) { + if scope.callCtx != nil { + // makes sure that the other goroutine won't wait forever if we make a mistake + defer close(scope.callCtx.continueRequest) + } t, err := parser.ParseExpr(expr) if err != nil { + scope.callCtx.doReturn(nil, err) return nil, err } @@ -33,12 +38,14 @@ func (scope *EvalScope) EvalExpression(expr string, cfg LoadConfig) (*Variable, ev, err = scope.evalAST(t) } if err != nil { + scope.callCtx.doReturn(nil, err) return nil, err } ev.loadValue(cfg) if ev.Name == "" { ev.Name = expr } + scope.callCtx.doReturn(ev, nil) return ev, nil } @@ -174,20 +181,11 @@ func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) { case *ast.CallExpr: if len(node.Args) == 1 { v, err := scope.evalTypeCast(node) - if err == nil { - return v, nil - } - _, isident := node.Fun.(*ast.Ident) - // we don't support function calls at the moment except for a few - // builtin functions so just return the type error here if the function - // isn't an identifier. - // More sophisticated logic will be required when function calls - // are implemented. - if err != reader.TypeNotFoundErr || !isident { + if err == nil || err != reader.TypeNotFoundErr { return v, err } } - return scope.evalBuiltinCall(node) + return scope.evalFunctionCall(node) case *ast.Ident: return scope.evalIdent(node) @@ -395,7 +393,7 @@ func convertInt(n uint64, signed bool, size int64) uint64 { func (scope *EvalScope) evalBuiltinCall(node *ast.CallExpr) (*Variable, error) { fnnode, ok := node.Fun.(*ast.Ident) if !ok { - return nil, fmt.Errorf("function calls are not supported") + return nil, nil } args := make([]*Variable, len(node.Args)) @@ -421,7 +419,7 @@ func (scope *EvalScope) evalBuiltinCall(node *ast.CallExpr) (*Variable, error) { return realBuiltin(args, node.Args) } - return nil, fmt.Errorf("function calls are not supported") + return nil, nil } func capBuiltin(args []*Variable, nodeargs []ast.Expr) (*Variable, error) { diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index ef0d3404..16d70de2 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -7,7 +7,6 @@ import ( "fmt" "go/ast" "go/constant" - "go/parser" "reflect" "sort" @@ -23,13 +22,18 @@ import ( // The protocol is described in $GOROOT/src/runtime/asm_amd64.s in the // comments for function runtime·debugCallV1. // -// There are two main entry points here. The first one is CallFunction which -// evaluates a function call expression, sets up the function call on the -// selected goroutine and resumes execution of the process. +// The main entry point is EvalExpressionWithCalls which will start a goroutine to +// evaluate the provided expression. +// This goroutine can either return immediately, if no function calls were +// needed, or write a continue request to the scope.callCtx.continueRequest +// channel. When this happens EvalExpressionWithCalls will call Continue and +// return. // -// The second one is (*FunctionCallState).step() which is called every time -// the process stops at a breakpoint inside one of the debug injcetion -// functions. +// The Continue loop will write to scope.callCtx.continueCompleted when it +// hits a breakpoint in the call injection protocol. +// +// The work of setting up the function call and executing the protocol is +// done by evalFunctionCall and funcCallStep. const ( debugCallFunctionNamePrefix1 = "debugCall" @@ -49,17 +53,12 @@ var ( errNotEnoughArguments = errors.New("not enough arguments") errNoAddrUnsupported = errors.New("arguments to a function call must have an address") errNotAGoFunction = errors.New("not a Go function") + errFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'") ) type functionCallState struct { - // inProgress is true if a function call is in progress - inProgress bool - // finished is true if the function call terminated - finished bool // savedRegs contains the saved registers savedRegs Registers - // expr contains an expression describing the current function call - expr string // err contains a saved error err error // fn is the function that is being called @@ -77,21 +76,56 @@ type functionCallState struct { panicvar *Variable } -// CallFunction starts a debugger injected function call on the current thread of p. -// See runtime.debugCallV1 in $GOROOT/src/runtime/asm_amd64.s for a -// description of the protocol. -func CallFunction(p Process, expr string, retLoadCfg *LoadConfig, checkEscape bool) error { +type callContext struct { + p Process + + // checkEscape is true if the escape check should be performed. + // See service/api.DebuggerCommand.UnsafeCall in service/api/types.go. + checkEscape bool + + // retLoadCfg is the load configuration used to load return values + retLoadCfg LoadConfig + + // Write to continueRequest to request a call to Continue from the + // debugger's main goroutine. + // Read from continueCompleted to wait for the target process to stop at + // one of the interaction point of the function call protocol. + // To signal that evaluation is completed a value will be written to + // continueRequest having cont == false and the return values in ret. + continueRequest chan<- continueRequest + continueCompleted <-chan struct{} +} + +type continueRequest struct { + cont bool + err error + ret *Variable +} + +func (callCtx *callContext) doContinue() { + callCtx.continueRequest <- continueRequest{cont: true} + <-callCtx.continueCompleted +} + +func (callCtx *callContext) doReturn(ret *Variable, err error) { + if callCtx == nil { + return + } + callCtx.continueRequest <- continueRequest{cont: false, ret: ret, err: err} +} + +// 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(p Process, expr string, retLoadCfg LoadConfig, checkEscape bool) error { bi := p.BinInfo() if !p.Common().fncallEnabled { return errFuncCallUnsupportedBackend } - fncall := &p.Common().fncallState - if fncall.inProgress { + if p.Common().continueCompleted != nil { return errFuncCallInProgress } - *fncall = functionCallState{} - dbgcallfn := bi.LookupFunc[debugCallFunctionName] if dbgcallfn == nil { return errFuncCallUnsupported @@ -106,49 +140,217 @@ func CallFunction(p Process, expr string, retLoadCfg *LoadConfig, checkEscape bo return errGoroutineNotRunning } + scope, err := GoroutineScope(p.CurrentThread()) + if err != nil { + return err + } + + continueRequest := make(chan continueRequest) + continueCompleted := make(chan struct{}) + + scope.callCtx = &callContext{ + p: p, + checkEscape: checkEscape, + retLoadCfg: retLoadCfg, + continueRequest: continueRequest, + continueCompleted: continueCompleted, + } + + p.Common().continueRequest = continueRequest + p.Common().continueCompleted = continueCompleted + + go scope.EvalExpression(expr, retLoadCfg) + + contReq, ok := <-continueRequest + if contReq.cont { + return Continue(p) + } + + return finishEvalExpressionWithCalls(p, contReq, ok) +} + +func finishEvalExpressionWithCalls(p Process, contReq continueRequest, ok bool) error { + var err error + if !ok { + err = errors.New("internal error EvalExpressionWithCalls didn't return anything") + } else if contReq.err != nil { + if fpe, ispanic := contReq.err.(fncallPanicErr); ispanic { + p.CurrentThread().Common().returnValues = []*Variable{fpe.panicVar} + } else { + err = contReq.err + } + } else if contReq.ret.Addr == 0 && contReq.ret.DwarfType == nil { + // this is a variable returned by a function call with multiple return values + r := make([]*Variable, len(contReq.ret.Children)) + for i := range contReq.ret.Children { + r[i] = &contReq.ret.Children[i] + } + p.CurrentThread().Common().returnValues = r + } else { + p.CurrentThread().Common().returnValues = []*Variable{contReq.ret} + } + + p.Common().continueRequest = nil + close(p.Common().continueCompleted) + p.Common().continueCompleted = nil + return err +} + +// evalFunctionCall evaluates a function call. +// If this is a built-in function it's evaluated directly. +// Otherwise this will start the function call injection protocol and +// request that the target process resumes. +// See the comment describing the field EvalScope.callCtx for a description +// of the preconditions that make starting the function call protocol +// possible. +// See runtime.debugCallV1 in $GOROOT/src/runtime/asm_amd64.s for a +// description of the protocol. +func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error) { + r, err := scope.evalBuiltinCall(node) + if r != nil || err != nil { + // it was a builtin call + return r, err + } + if scope.callCtx == nil { + return nil, errFuncCallNotAllowed + } + + p := scope.callCtx.p + bi := scope.BinInfo + if !p.Common().fncallEnabled { + return nil, errFuncCallUnsupportedBackend + } + if p.Common().callInProgress { + return nil, errFuncCallInProgress + } + + p.Common().callInProgress = true + defer func() { + p.Common().callInProgress = false + }() + + dbgcallfn := bi.LookupFunc[debugCallFunctionName] + if dbgcallfn == nil { + return nil, errFuncCallUnsupported + } + + // check that the selected goroutine is running + g := p.SelectedGoroutine() + if g == nil { + return nil, errNoGoroutine + } + if g.Status != Grunning || g.Thread == nil { + return nil, errGoroutineNotRunning + } + // check that there are at least 256 bytes free on the stack regs, err := g.Thread.Registers(true) if err != nil { - return err + return nil, err } regs = regs.Copy() if regs.SP()-256 <= g.stacklo { - return errNotEnoughStack + return nil, errNotEnoughStack } _, err = regs.Get(int(x86asm.RAX)) if err != nil { - return errFuncCallUnsupportedBackend + return nil, errFuncCallUnsupportedBackend } - fn, closureAddr, argvars, err := funcCallEvalExpr(p, expr) + fn, closureAddr, argvars, err := scope.funcCallEvalExpr(node) if err != nil { - return err + return nil, err } - argmem, err := funcCallArgFrame(fn, argvars, g, bi, checkEscape) + argmem, err := funcCallArgFrame(fn, argvars, g, bi, scope.callCtx.checkEscape) if err != nil { - return err + return nil, err } if err := callOP(bi, g.Thread, regs, dbgcallfn.Entry); err != nil { - return err + return nil, err } // write the desired argument frame size at SP-(2*pointer_size) (the extra pointer is the saved PC) if err := writePointer(bi, g.Thread, regs.SP()-3*uint64(bi.Arch.PtrSize()), uint64(len(argmem))); err != nil { - return err + return nil, err } - fncall.inProgress = true - fncall.savedRegs = regs - fncall.expr = expr - fncall.fn = fn - fncall.closureAddr = closureAddr - fncall.argmem = argmem - fncall.retLoadCfg = retLoadCfg + fncall := functionCallState{ + savedRegs: regs, + fn: fn, + closureAddr: closureAddr, + argmem: argmem, + retLoadCfg: &scope.callCtx.retLoadCfg, + } fncallLog("function call initiated %v frame size %d\n", fn, len(argmem)) - return Continue(p) + spoff := int64(scope.Regs.Uint64Val(scope.Regs.SPRegNum)) - int64(g.stackhi) + bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(g.stackhi) + fboff := scope.Regs.FrameBase - int64(g.stackhi) + + for { + scope.callCtx.doContinue() + + g = p.SelectedGoroutine() + if g != nil { + // adjust the value of registers inside scope + for regnum := range scope.Regs.Regs { + switch uint64(regnum) { + case scope.Regs.PCRegNum, scope.Regs.SPRegNum, scope.Regs.BPRegNum: + // leave these alone + default: + // every other register is dirty and unrecoverable + scope.Regs.Regs[regnum] = nil + } + } + + scope.Regs.Regs[scope.Regs.SPRegNum].Uint64Val = uint64(spoff + int64(g.stackhi)) + scope.Regs.Regs[scope.Regs.BPRegNum].Uint64Val = uint64(bpoff + int64(g.stackhi)) + scope.Regs.FrameBase = fboff + int64(g.stackhi) + scope.Regs.CFA = scope.frameOffset + int64(g.stackhi) + } + + finished := funcCallStep(scope, &fncall) + if finished { + break + } + } + + if fncall.err != nil { + return nil, fncall.err + } + + if fncall.panicvar != nil { + return nil, fncallPanicErr{fncall.panicvar} + } + switch len(fncall.retvars) { + case 0: + r := scope.newVariable("", 0, nil, nil) + r.loaded = true + r.Unreadable = errors.New("no return values") + return r, nil + case 1: + return fncall.retvars[0], nil + default: + // create a fake variable without address or type to return multiple values + r := scope.newVariable("", 0, nil, nil) + r.loaded = true + r.Children = make([]Variable, len(fncall.retvars)) + for i := range fncall.retvars { + r.Children[i] = *fncall.retvars[i] + } + return r, nil + } +} + +// fncallPanicErr is the error returned if a called function panics +type fncallPanicErr struct { + panicVar *Variable +} + +func (err fncallPanicErr) Error() string { + return fmt.Sprintf("panic calling a function") } func fncallLog(fmtstr string, args ...interface{}) { @@ -191,21 +393,8 @@ func callOP(bi *BinaryInfo, thread Thread, regs Registers, callAddr uint64) erro // funcCallEvalExpr evaluates expr, which must be a function call, returns // the function being called and its arguments. -func funcCallEvalExpr(p Process, expr string) (fn *Function, closureAddr uint64, argvars []*Variable, err error) { - bi := p.BinInfo() - scope, err := GoroutineScope(p.CurrentThread()) - if err != nil { - return nil, 0, nil, err - } - - t, err := parser.ParseExpr(expr) - if err != nil { - return nil, 0, nil, err - } - callexpr, iscall := t.(*ast.CallExpr) - if !iscall { - return nil, 0, nil, errNotACallExpr - } +func (scope *EvalScope) funcCallEvalExpr(callexpr *ast.CallExpr) (fn *Function, closureAddr uint64, argvars []*Variable, err error) { + bi := scope.BinInfo fnvar, err := scope.evalAST(callexpr.Fun) if err != nil { @@ -395,16 +584,16 @@ const ( debugCallAXRestoreRegisters = 16 ) -func (fncall *functionCallState) step(p Process) { +// funcCallStep executes one step of the function call injection protocol. +func funcCallStep(scope *EvalScope, fncall *functionCallState) bool { + p := scope.callCtx.p bi := p.BinInfo() thread := p.CurrentThread() regs, err := thread.Registers(false) if err != nil { fncall.err = err - fncall.finished = true - fncall.inProgress = false - return + return true } regs = regs.Copy() @@ -453,7 +642,6 @@ func (fncall *functionCallState) step(p Process) { case debugCallAXRestoreRegisters: // runtime requests that we restore the registers (all except pc and sp), // this is also the last step of the function call protocol. - fncall.finished = true pc, sp := regs.PC(), regs.SP() if err := thread.RestoreRegisters(fncall.savedRegs); err != nil { fncall.err = fmt.Errorf("could not restore registers: %v", err) @@ -467,6 +655,7 @@ func (fncall *functionCallState) step(p Process) { if err := stepInstructionOut(p, thread, debugCallFunctionName, debugCallFunctionName); err != nil { fncall.err = fmt.Errorf("could not step out of %s: %v", debugCallFunctionName, err) } + return true case debugCallAXReadReturn: // read return arguments from stack @@ -496,7 +685,7 @@ func (fncall *functionCallState) step(p Process) { case debugCallAXReadPanic: // read panic value from stack if fncall.retLoadCfg == nil { - return + return false } fncall.panicvar, err = readTopstackVariable(thread, regs, "interface {}", *fncall.retLoadCfg) if err != nil { @@ -515,6 +704,8 @@ func (fncall *functionCallState) step(p Process) { // possible is to ignore it and hope it didn't matter. fncallLog("unknown value of AX %#x", rax) } + + return false } func readTopstackVariable(thread Thread, regs Registers, typename string, loadCfg LoadConfig) (*Variable, error) { diff --git a/pkg/proc/interface.go b/pkg/proc/interface.go index 9a4245a5..6362837b 100644 --- a/pkg/proc/interface.go +++ b/pkg/proc/interface.go @@ -115,8 +115,19 @@ type BreakpointManipulation interface { // implementations of the Process interface. type CommonProcess struct { allGCache []*G - fncallState functionCallState fncallEnabled bool + + // if continueCompleted is not nil it means we are in the process of + // executing an injected function call, see comments throughout + // pkg/proc/fncall.go for a description of how this works. + continueCompleted chan<- struct{} + continueRequest <-chan continueRequest + + // callInProgress is true when a function call is being injected in the + // target process. + // This is only used to prevent nested function calls, it should be removed + // when we add support for them. + callInProgress bool } // NewCommonProcess returns a struct with fields common across diff --git a/pkg/proc/proc.go b/pkg/proc/proc.go index 85ef473f..1685b7f6 100644 --- a/pkg/proc/proc.go +++ b/pkg/proc/proc.go @@ -228,18 +228,18 @@ func Continue(dbp Process) error { } return conditionErrors(threads) case strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) || strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2): - fncall := &dbp.Common().fncallState - if !fncall.inProgress { + continueCompleted := dbp.Common().continueCompleted + if continueCompleted == nil { return conditionErrors(threads) } - fncall.step(dbp) - // only stop execution if the function call finished - if fncall.finished { - fncall.inProgress = false - if fncall.err != nil { - return fncall.err + continueCompleted <- struct{}{} + contReq, ok := <-dbp.Common().continueRequest + if !contReq.cont { + // only stop execution if the expression evaluation with calls finished + err := finishEvalExpressionWithCalls(dbp, contReq, ok) + if err != nil { + return err } - curthread.Common().returnValues = fncall.returnValues() return conditionErrors(threads) } default: diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 5ef5a06c..2a73f8fe 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -4123,7 +4123,7 @@ func TestIssue1374(t *testing.T) { setFileBreakpoint(p, t, fixture, 7) assertNoError(proc.Continue(p), t, "First Continue") assertLineNumber(p, t, 7, "Did not continue to correct location (first continue),") - assertNoError(proc.CallFunction(p, "getNum()", &normalLoadConfig, true), t, "Call") + assertNoError(proc.EvalExpressionWithCalls(p, "getNum()", normalLoadConfig, true), t, "Call") err := proc.Continue(p) if _, isexited := err.(proc.ErrProcessExited); !isexited { regs, _ := p.CurrentThread().Registers(false) @@ -4328,7 +4328,7 @@ func TestCallConcurrent(t *testing.T) { gid1 := p.SelectedGoroutine().ID t.Logf("starting injection in %d / %d", p.SelectedGoroutine().ID, p.CurrentThread().ThreadID()) - assertNoError(proc.CallFunction(p, "Foo(10, 1)", &normalLoadConfig, false), t, "EvalExpressionWithCalls()") + assertNoError(proc.EvalExpressionWithCalls(p, "Foo(10, 1)", normalLoadConfig, false), t, "EvalExpressionWithCalls()") returned := testCallConcurrentCheckReturns(p, t, gid1) diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index 3e83fa7f..ba143318 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -221,6 +221,20 @@ type EvalScope struct { frameOffset int64 aordr *dwarf.Reader // extra reader to load DW_AT_abstract_origin entries, do not initialize + + // When the following pointer is not nil this EvalScope was created + // by CallFunction and the expression evaluation is executing on a + // different goroutine from the debugger's main goroutine. + // Under this circumstance the expression evaluator can make function + // calls by setting up the runtime.debugCallV1 call and then writing a + // value to the continueRequest channel. + // When a value is written to continueRequest the debugger's main goroutine + // will call Continue, when the runtime in the target process sends us a + // request in the function call protocol the debugger's main goroutine will + // write a value to the continueCompleted channel. + // The goroutine executing the expression evaluation shall signal that the + // evaluation is complete by closing the continueRequest channel. + callCtx *callContext } // IsNilErr is returned when a variable is nil. diff --git a/service/api/types.go b/service/api/types.go index 0ac4f3d6..2bd6cf6f 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -311,7 +311,20 @@ type DebuggerCommand struct { ReturnInfoLoadConfig *LoadConfig // Expr is the expression argument for a Call command Expr string `json:"expr,omitempty"` - // UnsafeCall disabled parameter escape checking for function calls + + // UnsafeCall disables parameter escape checking for function calls. + // Go objects can be allocated on the stack or on the heap. Heap objects + // can be used by any goroutine; stack objects can only be used by the + // goroutine that owns the stack they are allocated on and can not surivive + // the stack frame of allocation. + // The Go compiler will use escape analysis to determine whether to + // allocate an object on the stack or the heap. + // When injecting a function call Delve will check that no address of a + // stack allocated object is passed to the called function: this ensures + // the rules for stack objects will not be violated. + // If you are absolutely sure that the function you are calling will not + // violate the rules about stack objects you can disable this safety check + // by setting UnsafeCall to true. UnsafeCall bool `json:"unsafeCall,omitempty"` } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 74f05782..33020237 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -599,7 +599,10 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er err = proc.Continue(d.target) case api.Call: d.log.Debugf("function call %s", command.Expr) - err = proc.CallFunction(d.target, command.Expr, api.LoadConfigToProc(command.ReturnInfoLoadConfig), !command.UnsafeCall) + if command.ReturnInfoLoadConfig == nil { + return nil, errors.New("can not call function with nil ReturnInfoLoadConfig") + } + err = proc.EvalExpressionWithCalls(d.target, command.Expr, *api.LoadConfigToProc(command.ReturnInfoLoadConfig), !command.UnsafeCall) case api.Rewind: d.log.Debug("rewinding") if err := d.target.Direction(proc.Backward); err != nil { diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index f00ce682..252bb399 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -1584,6 +1584,7 @@ func TestClientServerFunctionCallBadPos(t *testing.T) { state = <-c.Continue() assertNoError(state.Err, t, "Continue()") + c.SetReturnValuesLoadConfig(&normalLoadConfig) state, err = c.Call("main.call1(main.zero, main.zero)", false) if err == nil || err.Error() != "call not at safe point" { t.Fatalf("wrong error or no error: %v", err) diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 421773ea..6c7c67d6 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -737,7 +737,7 @@ func TestEvalExpression(t *testing.T) { {"i2 << i3", false, "", "", "int", fmt.Errorf("shift count type int, must be unsigned integer")}, {"*(i2 + i3)", false, "", "", "", fmt.Errorf("expression \"(i2 + i3)\" (int) can not be dereferenced")}, {"i2.member", false, "", "", "", fmt.Errorf("i2 (type int) is not a struct")}, - {"fmt.Println(\"hello\")", false, "", "", "", fmt.Errorf("no type entry found, use 'types' for a list of valid types")}, + {"fmt.Println(\"hello\")", false, "", "", "", fmt.Errorf("function calls not allowed without using 'call'")}, {"*nil", false, "", "", "", fmt.Errorf("nil can not be dereferenced")}, {"!nil", false, "", "", "", fmt.Errorf("operator ! can not be applied to \"nil\"")}, {"&nil", false, "", "", "", fmt.Errorf("can not take address of \"nil\"")}, @@ -1093,6 +1093,8 @@ func TestCallFunction(t *testing.T) { outs []string // list of return parameters in this format: :: err error // if not nil should return an error }{ + // 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}, @@ -1102,6 +1104,13 @@ func TestCallFunction(t *testing.T) { {`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")}, + // 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}, + + // 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 @@ -1130,6 +1139,8 @@ func TestCallFunction(t *testing.T) { {"ga.PRcvr(2)", []string{`:string:"2 - 0 = 2"`}, nil}, + // Escape tests + {"escapeArg(&a2)", nil, errors.New("cannot use &a2 as argument pa2 in function main.escapeArg: stack object passed to escaping pointer: pa2")}, {"-unsafe escapeArg(&a2)", nil, nil}, // LEAVE THIS AS THE LAST ITEM, IT BREAKS THE TARGET PROCESS!!! @@ -1151,9 +1162,9 @@ func TestCallFunction(t *testing.T) { checkEscape = false } t.Logf("call %q", tc.expr) - err := proc.CallFunction(p, expr, &pnormalLoadConfig, checkEscape) + err := proc.EvalExpressionWithCalls(p, expr, pnormalLoadConfig, checkEscape) if tc.err != nil { - + t.Logf("\terr = %v\n", err) if err == nil { t.Fatalf("call %q: expected error %q, got no error", tc.expr, tc.err.Error()) }