From 54376f55dd767f9cf997998375570d6494a48462 Mon Sep 17 00:00:00 2001 From: Alessandro Arzilli Date: Thu, 30 May 2019 17:08:37 +0200 Subject: [PATCH] More Function Calls, parts 2 (#1504) * proc: support nested function calls Changes the code in fncall.go to support nested function calls. This changes delays argument evaluation until after we have used the call injection protocol to allocate an argument frame. When evaluating the parse tree of an expression we'll initiate each function call we find on the way down and then complete the function call on the way up. For example. in: f(g(x)) we will: 1. initiate the call injection protocol for f(...) 2. progress it until the point where we have space for the arguments of 'f' (i.e. when we receive the debugCallAXCompleteCall message from the target runtime) 3. inititate the call injection protocol for g(...) 4. progress it until the point where we have space for the arguments of 'g' 5. copy the value of x into the argument frame of 'g' 6. finish the call to g(...) 7. copy the return value of g(x) into the argument frame of 'f' 8. finish the call to f(...) Updates #119 * proc: bugfix: closure addr was wrong for non-closure functions --- _fixtures/fncall.go | 28 +++- pkg/proc/fncall.go | 245 ++++++++++++++++++++------------- pkg/proc/interface.go | 6 - pkg/proc/mem.go | 15 -- pkg/proc/variables.go | 9 +- service/test/variables_test.go | 142 ++++++++++++------- 6 files changed, 269 insertions(+), 176 deletions(-) diff --git a/_fixtures/fncall.go b/_fixtures/fncall.go index 4c39d27653..c322759385 100644 --- a/_fixtures/fncall.go +++ b/_fixtures/fncall.go @@ -91,6 +91,32 @@ func onetwothree(n int) []int { return []int{n + 1, n + 2, n + 3} } +func curriedAdd(n int) func(int) int { + return func(m int) int { + return n + m + } +} + +func getAStruct(n int) astruct { + return astruct{X: n} +} + +func getAStructPtr(n int) *astruct { + return &astruct{X: n} +} + +func getVRcvrableFromAStruct(n int) VRcvrable { + return astruct{X: n} +} + +func getPRcvrableFromAStructPtr(n int) PRcvrable { + return &astruct{X: n} +} + +func getVRcvrableFromAStructPtr(n int) VRcvrable { + return &astruct{X: n} +} + func main() { one, two := 1, 2 intslice := []int{1, 2, 3} @@ -113,5 +139,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, square, intcallpanic, onetwothree) + 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, curriedAdd, getAStruct, getAStructPtr, getVRcvrableFromAStruct, getPRcvrableFromAStructPtr, getVRcvrableFromAStructPtr) } diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index 16d70de2ba..30f5088d5a 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -39,6 +39,7 @@ const ( debugCallFunctionNamePrefix1 = "debugCall" debugCallFunctionNamePrefix2 = "runtime.debugCall" debugCallFunctionName = "runtime.debugCallV1" + maxArgFrameSize = 65535 ) var ( @@ -61,19 +62,26 @@ type functionCallState struct { savedRegs Registers // err contains a saved error err error + // expr is the expression being evaluated + expr *ast.CallExpr // fn is the function that is being called fn *Function + // receiver is the receiver argument for the function + receiver *Variable // closureAddr is the address of the closure being called closureAddr uint64 - // argmem contains the argument frame of this function call - argmem []byte + // formalArgs are the formal arguments of fn + formalArgs []funcCallArg + // argFrameSize contains the size of the arguments + argFrameSize int64 // retvars contains the return variables after the function call terminates without panic'ing retvars []*Variable - // retLoadCfg is the load configuration used to load return values - retLoadCfg *LoadConfig // panicvar is a variable used to store the value of the panic, if the // called function panics. panicvar *Variable + // lateCallFailure is set to true if the function call could not be + // completed after we started evaluating the arguments. + lateCallFailure bool } type callContext struct { @@ -220,14 +228,6 @@ func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error) 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 { @@ -257,12 +257,12 @@ func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error) return nil, errFuncCallUnsupportedBackend } - fn, closureAddr, argvars, err := scope.funcCallEvalExpr(node) - if err != nil { - return nil, err + fncall := functionCallState{ + expr: node, + savedRegs: regs, } - argmem, err := funcCallArgFrame(fn, argvars, g, bi, scope.callCtx.checkEscape) + err = funcCallEvalFuncExpr(scope, &fncall, false) if err != nil { return nil, err } @@ -271,19 +271,11 @@ func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error) 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 { + if err := writePointer(bi, g.Thread, regs.SP()-3*uint64(bi.Arch.PtrSize()), uint64(fncall.argFrameSize)); err != nil { return nil, err } - 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)) + fncallLog("function call initiated %v frame size %d\n", fncall.fn, fncall.argFrameSize) spoff := int64(scope.Regs.Uint64Val(scope.Regs.SPRegNum)) - int64(g.stackhi) bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(g.stackhi) @@ -391,48 +383,69 @@ func callOP(bi *BinaryInfo, thread Thread, regs Registers, callAddr uint64) erro return thread.SetPC(callAddr) } -// funcCallEvalExpr evaluates expr, which must be a function call, returns -// the function being called and its arguments. -func (scope *EvalScope) funcCallEvalExpr(callexpr *ast.CallExpr) (fn *Function, closureAddr uint64, argvars []*Variable, err error) { +// funcCallEvalFuncExpr evaluates expr.Fun and returns the function that we're trying to call. +// If allowCalls is false function calls will be disabled even if scope.callCtx != nil +func funcCallEvalFuncExpr(scope *EvalScope, fncall *functionCallState, allowCalls bool) error { bi := scope.BinInfo - fnvar, err := scope.evalAST(callexpr.Fun) - if err != nil { - return nil, 0, nil, err + if !allowCalls { + callCtx := scope.callCtx + scope.callCtx = nil + defer func() { + scope.callCtx = callCtx + }() + } + + fnvar, err := scope.evalAST(fncall.expr.Fun) + if err == errFuncCallNotAllowed { + // we can't determine the frame size because callexpr.Fun can't be + // evaluated without enabling function calls, just set up an argument + // frame for the maximum possible argument size. + fncall.argFrameSize = maxArgFrameSize + return nil + } else if err != nil { + return err } if fnvar.Kind != reflect.Func { - return nil, 0, nil, fmt.Errorf("expression %q is not a function", exprToString(callexpr.Fun)) + return fmt.Errorf("expression %q is not a function", exprToString(fncall.expr.Fun)) } fnvar.loadValue(LoadConfig{false, 0, 0, 0, 0, 0}) if fnvar.Unreadable != nil { - return nil, 0, nil, fnvar.Unreadable + return fnvar.Unreadable } if fnvar.Base == 0 { - return nil, 0, nil, errors.New("nil pointer dereference") + return errors.New("nil pointer dereference") } - fn = bi.PCToFunc(uint64(fnvar.Base)) - if fn == nil { - return nil, 0, nil, fmt.Errorf("could not find DIE for function %q", exprToString(callexpr.Fun)) + fncall.fn = bi.PCToFunc(uint64(fnvar.Base)) + if fncall.fn == nil { + return fmt.Errorf("could not find DIE for function %q", exprToString(fncall.expr.Fun)) } - if !fn.cu.isgo { - return nil, 0, nil, errNotAGoFunction + if !fncall.fn.cu.isgo { + return errNotAGoFunction } + fncall.closureAddr = fnvar.closureAddr + + fncall.argFrameSize, fncall.formalArgs, err = funcCallArgs(fncall.fn, bi, false) + if err != nil { + return err + } + + argnum := len(fncall.expr.Args) - argvars = make([]*Variable, 0, len(callexpr.Args)+1) if len(fnvar.Children) > 0 { - // receiver argument - argvars = append(argvars, &fnvar.Children[0]) + argnum++ + fncall.receiver = &fnvar.Children[0] + fncall.receiver.Name = exprToString(fncall.expr.Fun) } - for i := range callexpr.Args { - argvar, err := scope.evalAST(callexpr.Args[i]) - if err != nil { - return nil, 0, nil, err - } - argvar.Name = exprToString(callexpr.Args[i]) - argvars = append(argvars, argvar) + + if argnum > len(fncall.formalArgs) { + return errTooManyArguments + } + if argnum < len(fncall.formalArgs) { + return errNotEnoughArguments } - return fn, fnvar.funcvalAddr(), argvars, nil + return nil } type funcCallArg struct { @@ -442,44 +455,58 @@ type funcCallArg struct { isret bool } -// funcCallArgFrame checks type and pointer escaping for the arguments and -// returns the argument frame. -func funcCallArgFrame(fn *Function, actualArgs []*Variable, g *G, bi *BinaryInfo, checkEscape bool) (argmem []byte, err error) { - argFrameSize, formalArgs, err := funcCallArgs(fn, bi, false) - if err != nil { - return nil, err - } - if len(actualArgs) > len(formalArgs) { - return nil, errTooManyArguments +// funcCallEvalArgs evaluates the arguments of the function call, copying +// the into the argument frame starting at argFrameAddr. +func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr uint64) error { + g := scope.callCtx.p.SelectedGoroutine() + if g == nil { + // this should never happen + return errNoGoroutine } - if len(actualArgs) < len(formalArgs) { - return nil, errNotEnoughArguments + + if fncall.receiver != nil { + err := funcCallCopyOneArg(g, scope, fncall, fncall.receiver, &fncall.formalArgs[0], argFrameAddr) + if err != nil { + return err + } + fncall.formalArgs = fncall.formalArgs[1:] } - // constructs arguments frame - argmem = make([]byte, argFrameSize) - argmemWriter := &bufferMemoryReadWriter{argmem} - for i := range formalArgs { - formalArg := &formalArgs[i] - actualArg := actualArgs[i] + for i := range fncall.formalArgs { + formalArg := &fncall.formalArgs[i] - if checkEscape { - //TODO(aarzilli): only apply the escapeCheck to leaking parameters. - if err := escapeCheck(actualArg, formalArg.name, g); err != nil { - return nil, fmt.Errorf("cannot use %s as argument %s in function %s: %v", actualArg.Name, formalArg.name, fn.Name, err) - } + actualArg, err := scope.evalAST(fncall.expr.Args[i]) + if err != nil { + return fmt.Errorf("error evaluating %q as argument %s in function %s: %v", exprToString(fncall.expr.Args[i]), formalArg.name, fncall.fn.Name, err) + } + actualArg.Name = exprToString(fncall.expr.Args[i]) + + err = funcCallCopyOneArg(g, scope, fncall, actualArg, formalArg, argFrameAddr) + if err != nil { + return err } + } - //TODO(aarzilli): autmoatic wrapping in interfaces for cases not handled - // by convertToEface. + return nil +} - formalArgVar := newVariable(formalArg.name, uintptr(formalArg.off+fakeAddress), formalArg.typ, bi, argmemWriter) - if err := formalArgVar.setValue(actualArg, actualArg.Name); err != nil { - return nil, err +func funcCallCopyOneArg(g *G, scope *EvalScope, fncall *functionCallState, actualArg *Variable, formalArg *funcCallArg, argFrameAddr uint64) error { + if scope.callCtx.checkEscape { + //TODO(aarzilli): only apply the escapeCheck to leaking parameters. + if err := escapeCheck(actualArg, formalArg.name, g); err != nil { + return fmt.Errorf("cannot use %s as argument %s in function %s: %v", actualArg.Name, formalArg.name, fncall.fn.Name, err) } } - return argmem, nil + //TODO(aarzilli): autmoatic wrapping in interfaces for cases not handled + // by convertToEface. + + formalArgVar := newVariable(formalArg.name, uintptr(formalArg.off+int64(argFrameAddr)), formalArg.typ, scope.BinInfo, scope.Mem) + if err := formalArgVar.setValue(actualArg, actualArg.Name); err != nil { + return err + } + + return nil } func funcCallArgs(fn *Function, bi *BinaryInfo, includeRet bool) (argFrameSize int64, formalArgs []funcCallArg, err error) { @@ -585,8 +612,8 @@ const ( ) // funcCallStep executes one step of the function call injection protocol. -func funcCallStep(scope *EvalScope, fncall *functionCallState) bool { - p := scope.callCtx.p +func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool { + p := callScope.callCtx.p bi := p.BinInfo() thread := p.CurrentThread() @@ -624,14 +651,29 @@ func funcCallStep(scope *EvalScope, fncall *functionCallState) bool { fncall.err = fmt.Errorf("%v", constant.StringVal(errvar.Value)) case debugCallAXCompleteCall: - // write arguments to the stack, call final function - n, err := thread.WriteMemory(uintptr(regs.SP()), fncall.argmem) - if err != nil { - fncall.err = fmt.Errorf("could not write arguments: %v", err) - } - if n != len(fncall.argmem) { - fncall.err = fmt.Errorf("short argument write: %d %d", n, len(fncall.argmem)) + // evaluate arguments of the target function, copy them into its argument frame and call the function + if fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0 { + // if we couldn't figure out which function we are calling before + // (because the function we are calling is the return value of a call to + // another function) now we have to figure it out by recursively + // evaluating the function calls. + // This also needs to be done if the function call has a receiver + // argument or a closure address (because those addresses could be on the stack + // and have changed position between the start of the call and now). + + err := funcCallEvalFuncExpr(callScope, fncall, true) + if err != nil { + fncall.err = err + fncall.lateCallFailure = true + break + } + //TODO: double check that function call size isn't too big } + + // instead of evaluating the arguments we start first by pushing the call + // on the stack, this is the opposite of what would happen normally but + // it's necessary because otherwise the GC wouldn't be able to deal with + // the argument frame. if fncall.closureAddr != 0 { // When calling a function pointer we must set the DX register to the // address of the function pointer itself. @@ -639,6 +681,16 @@ func funcCallStep(scope *EvalScope, fncall *functionCallState) bool { } callOP(bi, thread, regs, fncall.fn.Entry) + err := funcCallEvalArgs(callScope, fncall, regs.SP()) + if err != nil { + // rolling back the call, note: this works because we called regs.Copy() above + thread.SetSP(regs.SP()) + thread.SetPC(regs.PC()) + fncall.err = err + fncall.lateCallFailure = true + break + } + 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. @@ -659,19 +711,19 @@ func funcCallStep(scope *EvalScope, fncall *functionCallState) bool { case debugCallAXReadReturn: // read return arguments from stack - if fncall.retLoadCfg == nil || fncall.panicvar != nil { + if fncall.panicvar != nil || fncall.lateCallFailure { break } - scope, err := ThreadScope(thread) + retScope, err := ThreadScope(thread) if err != nil { fncall.err = fmt.Errorf("could not get return values: %v", err) break } // pretend we are still inside the function we called - fakeFunctionEntryScope(scope, fncall.fn, int64(regs.SP()), regs.SP()-uint64(bi.Arch.PtrSize())) + fakeFunctionEntryScope(retScope, fncall.fn, int64(regs.SP()), regs.SP()-uint64(bi.Arch.PtrSize())) - fncall.retvars, err = scope.Locals() + fncall.retvars, err = retScope.Locals() if err != nil { fncall.err = fmt.Errorf("could not get return values: %v", err) break @@ -680,20 +732,17 @@ func funcCallStep(scope *EvalScope, fncall *functionCallState) bool { return (v.Flags & VariableReturnArgument) != 0 }) - loadValues(fncall.retvars, *fncall.retLoadCfg) + loadValues(fncall.retvars, callScope.callCtx.retLoadCfg) case debugCallAXReadPanic: // read panic value from stack - if fncall.retLoadCfg == nil { - return false - } - fncall.panicvar, err = readTopstackVariable(thread, regs, "interface {}", *fncall.retLoadCfg) + fncall.panicvar, err = readTopstackVariable(thread, regs, "interface {}", callScope.callCtx.retLoadCfg) if err != nil { fncall.err = fmt.Errorf("could not get panic: %v", err) break } fncall.panicvar.Name = "~panic" - fncall.panicvar.loadValue(*fncall.retLoadCfg) + fncall.panicvar.loadValue(callScope.callCtx.retLoadCfg) if fncall.panicvar.Unreadable != nil { fncall.err = fmt.Errorf("could not get panic: %v", fncall.panicvar.Unreadable) break diff --git a/pkg/proc/interface.go b/pkg/proc/interface.go index 6362837bd5..a54b4327bf 100644 --- a/pkg/proc/interface.go +++ b/pkg/proc/interface.go @@ -122,12 +122,6 @@ type CommonProcess struct { // 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/mem.go b/pkg/proc/mem.go index 1ccc024e12..6dee7c95cc 100644 --- a/pkg/proc/mem.go +++ b/pkg/proc/mem.go @@ -143,18 +143,3 @@ func DereferenceMemory(mem MemoryReadWriter) MemoryReadWriter { } return mem } - -// bufferMemoryReadWriter is dummy a MemoryReadWriter backed by a []byte. -type bufferMemoryReadWriter struct { - buf []byte -} - -func (mem *bufferMemoryReadWriter) ReadMemory(buf []byte, addr uintptr) (n int, err error) { - copy(buf, mem.buf[addr-fakeAddress:][:len(buf)]) - return len(buf), nil -} - -func (mem *bufferMemoryReadWriter) WriteMemory(addr uintptr, data []byte) (written int, err error) { - copy(mem.buf[addr-fakeAddress:], data) - return len(data), nil -} diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index 587f95b229..b87bf73e22 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -103,6 +103,9 @@ type Variable struct { stride int64 fieldType godwarf.Type + // closureAddr is the closure address for function variables (0 for non-closures) + closureAddr uint64 + // number of elements to skip when loading a map mapSkip int @@ -1698,18 +1701,18 @@ func (v *Variable) writeCopy(srcv *Variable) error { func (v *Variable) readFunctionPtr() { // dereference pointer to find function pc - fnaddr := v.funcvalAddr() + v.closureAddr = v.funcvalAddr() if v.Unreadable != nil { return } - if fnaddr == 0 { + if v.closureAddr == 0 { v.Base = 0 v.Value = constant.MakeString("") return } val := make([]byte, v.bi.Arch.PtrSize()) - _, err := v.mem.ReadMemory(val, uintptr(fnaddr)) + _, err := v.mem.ReadMemory(val, uintptr(v.closureAddr)) if err != nil { v.Unreadable = err return diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 6c7c67d66d..b11f049364 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -1085,14 +1085,16 @@ 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 +} + func TestCallFunction(t *testing.T) { protest.MustSupportFunctionCalls(t, testBackend) - var testcases = []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 - }{ + var testcases = []testCaseCallFunction{ // Basic function call injection tests {"call1(one, two)", []string{":int:3"}, nil}, @@ -1139,14 +1141,33 @@ func TestCallFunction(t *testing.T) { {"ga.PRcvr(2)", []string{`:string:"2 - 0 = 2"`}, nil}, + // Nested function calls + + {`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")}, + // 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!!! } - const unsafePrefix = "-unsafe " + var testcases113 = []testCaseCallFunction{ + {`curriedAdd(2)(3)`, []string{`:int:5`}, nil}, + + // 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).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 + + {`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 + } withTestProcess("fncall", t, func(p proc.Process, fixture protest.Fixture) { _, err := proc.FindFunctionLocation(p, "runtime.debugCallV1", true, 0) @@ -1155,61 +1176,76 @@ func TestCallFunction(t *testing.T) { } assertNoError(proc.Continue(p), t, "Continue()") for _, tc := range testcases { - expr := tc.expr - checkEscape := true - if strings.HasPrefix(expr, unsafePrefix) { - expr = expr[len(unsafePrefix):] - checkEscape = false - } - t.Logf("call %q", tc.expr) - 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()) - } - if tc.err.Error() != err.Error() { - t.Fatalf("call %q: expected error %q, got %q", tc.expr, tc.err.Error(), err.Error()) - } - continue - } + testCallFunction(t, p, tc) + } - if err != nil { - t.Fatalf("call %q: error %q", tc.expr, err.Error()) + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 13) { + for _, tc := range testcases113 { + testCallFunction(t, p, tc) } + } - retvalsVar := p.CurrentThread().Common().ReturnValues(pnormalLoadConfig) - retvals := make([]*api.Variable, len(retvalsVar)) + // LEAVE THIS AS THE LAST ITEM, IT BREAKS THE TARGET PROCESS!!! + testCallFunction(t, p, testCaseCallFunction{"-unsafe escapeArg(&a2)", nil, nil}) + }) +} - for i := range retvals { - retvals[i] = api.ConvertVar(retvalsVar[i]) - } +func testCallFunction(t *testing.T, p proc.Process, tc testCaseCallFunction) { + const unsafePrefix = "-unsafe " - for i := range retvals { - t.Logf("\t%s = %s", retvals[i].Name, retvals[i].SinglelineString()) - } + expr := tc.expr + checkEscape := true + if strings.HasPrefix(expr, unsafePrefix) { + expr = expr[len(unsafePrefix):] + checkEscape = false + } + t.Logf("call %q", tc.expr) + 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()) + } + if tc.err.Error() != err.Error() { + t.Fatalf("call %q: expected error %q, got %q", tc.expr, tc.err.Error(), err.Error()) + } + return + } - if len(retvals) != len(tc.outs) { - t.Fatalf("call %q: wrong number of return parameters", tc.expr) - } + if err != nil { + t.Fatalf("call %q: error %q", tc.expr, err.Error()) + } - for i := range retvals { - outfields := strings.SplitN(tc.outs[i], ":", 3) - tgtName, tgtType, tgtValue := outfields[0], outfields[1], outfields[2] + retvalsVar := p.CurrentThread().Common().ReturnValues(pnormalLoadConfig) + retvals := make([]*api.Variable, len(retvalsVar)) - if tgtName != "" && tgtName != retvals[i].Name { - t.Fatalf("call %q output parameter %d: expected name %q, got %q", tc.expr, i, tgtName, retvals[i].Name) - } + for i := range retvals { + retvals[i] = api.ConvertVar(retvalsVar[i]) + } - if retvals[i].Type != tgtType { - t.Fatalf("call %q, output parameter %d: expected type %q, got %q", tc.expr, i, tgtType, retvals[i].Type) - } - if cvs := retvals[i].SinglelineString(); cvs != tgtValue { - t.Fatalf("call %q, output parameter %d: expected value %q, got %q", tc.expr, i, tgtValue, cvs) - } - } + for i := range retvals { + t.Logf("\t%s = %s", retvals[i].Name, retvals[i].SinglelineString()) + } + + if len(retvals) != len(tc.outs) { + t.Fatalf("call %q: wrong number of return parameters", tc.expr) + } + + for i := range retvals { + outfields := strings.SplitN(tc.outs[i], ":", 3) + tgtName, tgtType, tgtValue := outfields[0], outfields[1], outfields[2] + + if tgtName != "" && tgtName != retvals[i].Name { + t.Fatalf("call %q output parameter %d: expected name %q, got %q", tc.expr, i, tgtName, retvals[i].Name) } - }) + + if retvals[i].Type != tgtType { + t.Fatalf("call %q, output parameter %d: expected type %q, got %q", tc.expr, i, tgtType, retvals[i].Type) + } + if cvs := retvals[i].SinglelineString(); cvs != tgtValue { + t.Fatalf("call %q, output parameter %d: expected value %q, got %q", tc.expr, i, tgtValue, cvs) + } + } } func TestIssue1531(t *testing.T) {