diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index 1ce78482b538c996a2b7c87adaeae7d96bd54129..c9e0526351b26b680329b6b48384cd23b95e1042 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -73,6 +73,8 @@ Aliases: bp ## call Resumes process, injecting a function call (EXPERIMENTAL!!!) + call [-unsafe] + Current limitations: - only pointers to stack-allocated objects can be passed as argument. - only some automatic type conversions are supported. @@ -174,8 +176,8 @@ Aliases: disass ## down Move the current frame down. - down [] - down [] + down [] + down [] Move the current frame down by . The second form runs the command on the given frame. @@ -201,8 +203,8 @@ Aliases: quit q ## frame Set the current frame, or execute command on a different frame. - frame - frame + frame + frame The first form sets frame used by subsequent commands such as "print" or "set". The second form runs the command on the given frame. @@ -394,8 +396,8 @@ If regex is specified only the types matching it will be returned. ## up Move the current frame up. - up [] - up [] + up [] + up [] Move the current frame up by . The second form runs the command on the given frame. diff --git a/_fixtures/fncall.go b/_fixtures/fncall.go index 1100b542a3e9396a849ca1a36263085dd21bb17d..c837f919fd8fce1ccd4e9aad4dc099760353a85b 100644 --- a/_fixtures/fncall.go +++ b/_fixtures/fncall.go @@ -39,6 +39,10 @@ type astruct struct { X int } +type a2struct struct { + Y int +} + func (a astruct) VRcvr(x int) string { return fmt.Sprintf("%d + %d = %d", x, a.X, x+a.X) } @@ -66,6 +70,11 @@ func makeclos(pa *astruct) func(int) string { } var ga astruct +var globalPA2 *a2struct + +func escapeArg(pa2 *a2struct) { + globalPA2 = pa2 +} func main() { one, two := 1, 2 @@ -74,6 +83,7 @@ func main() { comma := "," a := astruct{X: 3} pa := &astruct{X: 6} + a2 := a2struct{Y: 7} var vable_a VRcvrable = a var vable_pa VRcvrable = pa @@ -88,5 +98,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) + 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) } diff --git a/pkg/dwarf/godwarf/type.go b/pkg/dwarf/godwarf/type.go index f46d7ba145797917b4b29d70e385baa064e71d63..e5a0f91d6078e80e3fe6a336e260ecb81a5ea591 100644 --- a/pkg/dwarf/godwarf/type.go +++ b/pkg/dwarf/godwarf/type.go @@ -876,3 +876,13 @@ func zeroArray(t Type) { t = at.Type } } + +func resolveTypedef(typ Type) Type { + for { + if tt, ok := typ.(*TypedefType); ok { + typ = tt.Type + } else { + return typ + } + } +} diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index e731d65c3af7512eaed784635f168e4f0a3634fa..e00cca4c497802aa71a226bc4d0ee0b0f92cdff1 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -1208,7 +1208,7 @@ func (err *typeConvErr) Error() string { func (v *Variable) isType(typ godwarf.Type, kind reflect.Kind) error { if v.DwarfType != nil { - if typ != nil && typ.String() != v.RealType.String() { + if typ == nil || !sameType(typ, v.RealType) { return &typeConvErr{v.DwarfType, typ} } return nil @@ -1265,6 +1265,34 @@ func (v *Variable) isType(typ godwarf.Type, kind reflect.Kind) error { return nil } +func sameType(t1, t2 godwarf.Type) bool { + // Because of a bug in the go linker a type that refers to another type + // (for example a pointer type) will usually use the typedef but rarely use + // the non-typedef entry directly. + // For types that we read directly from go this is fine because it's + // consistent, however we also synthesize some types ourselves + // (specifically pointers and slices) and we always use a reference through + // a typedef. + t1 = resolveTypedef(t1) + t2 = resolveTypedef(t2) + + if tt1, isptr1 := t1.(*godwarf.PtrType); isptr1 { + tt2, isptr2 := t2.(*godwarf.PtrType) + if !isptr2 { + return false + } + return sameType(tt1.Type, tt2.Type) + } + if tt1, isslice1 := t1.(*godwarf.SliceType); isslice1 { + tt2, isslice2 := t2.(*godwarf.SliceType) + if !isslice2 { + return false + } + return sameType(tt1.ElemType, tt2.ElemType) + } + return t1.String() == t2.String() +} + func (v *Variable) sliceAccess(idx int) (*Variable, error) { if idx < 0 || int64(idx) >= v.Len { return nil, fmt.Errorf("index out of bounds") diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index b7239869ad5c311e068b270dbb38864cb329eea1..271b3ce2e3c7716069e5ac6293142b1df9c818e7 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -81,7 +81,7 @@ type functionCallState struct { // 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) error { +func CallFunction(p Process, expr string, retLoadCfg *LoadConfig, checkEscape bool) error { bi := p.BinInfo() if !p.Common().fncallEnabled { return errFuncCallUnsupportedBackend @@ -126,7 +126,7 @@ func CallFunction(p Process, expr string, retLoadCfg *LoadConfig) error { return err } - argmem, err := funcCallArgFrame(fn, argvars, g, bi) + argmem, err := funcCallArgFrame(fn, argvars, g, bi, checkEscape) if err != nil { return err } @@ -259,7 +259,7 @@ type funcCallArg struct { // funcCallArgFrame checks type and pointer escaping for the arguments and // returns the argument frame. -func funcCallArgFrame(fn *Function, actualArgs []*Variable, g *G, bi *BinaryInfo) (argmem []byte, err error) { +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 @@ -278,9 +278,11 @@ func funcCallArgFrame(fn *Function, actualArgs []*Variable, g *G, bi *BinaryInfo formalArg := &formalArgs[i] actualArg := actualArgs[i] - //TODO(aarzilli): only apply the escapeCheck to leaking parameters. - if err := escapeCheck(actualArg, formalArg.name, g); err != nil { - return nil, fmt.Errorf("can not pass %s to %s: %v", actualArg.Name, formalArg.name, err) + 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) + } } //TODO(aarzilli): autmoatic wrapping in interfaces for cases not handled @@ -343,7 +345,13 @@ func funcCallArgs(fn *Function, bi *BinaryInfo, includeRet bool) (argFrameSize i func escapeCheck(v *Variable, name string, g *G) error { switch v.Kind { case reflect.Ptr: - w := v.maybeDereference() + var w *Variable + if len(v.Children) == 1 { + // this branch is here to support pointers constructed with typecasts from ints or the '&' operator + w = &v.Children[0] + } else { + w = v.maybeDereference() + } return escapeCheckPointer(w.Addr, name, g) case reflect.Chan, reflect.String, reflect.Slice: return escapeCheckPointer(v.Base, name, g) diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 4dab5237f78105c6c109cec760058520a43c2c98..02a2a8842d9cc3429cdd6cb372cf310de05e4a7e 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -125,12 +125,12 @@ A tracepoint is a breakpoint that does not stop the execution of the program, in See also: "help on", "help cond" and "help clear"`}, {aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: `Restart process. - restart [checkpoint] - restart [-noargs] newargv... + restart [checkpoint] + restart [-noargs] newargv... - For recorded processes restarts from the start or from the specified - checkpoint. For normal processes restarts the process, optionally changing - the arguments. With -noargs, the process starts with an empty commandline. +For recorded processes restarts from the start or from the specified +checkpoint. For normal processes restarts the process, optionally changing +the arguments. With -noargs, the process starts with an empty commandline. `}, {aliases: []string{"continue", "c"}, cmdFn: c.cont, helpMsg: "Run until breakpoint or program termination."}, {aliases: []string{"step", "s"}, cmdFn: c.step, helpMsg: "Single step through program."}, @@ -139,6 +139,8 @@ See also: "help on", "help cond" and "help clear"`}, {aliases: []string{"stepout"}, cmdFn: c.stepout, helpMsg: "Step out of the current function."}, {aliases: []string{"call"}, cmdFn: c.call, helpMsg: `Resumes process, injecting a function call (EXPERIMENTAL!!!) + call [-unsafe] + Current limitations: - only pointers to stack-allocated objects can be passed as argument. - only some automatic type conversions are supported. @@ -259,8 +261,8 @@ Show source around current point or provided linespec.`}, }, helpMsg: `Set the current frame, or execute command on a different frame. - frame - frame + frame + frame The first form sets frame used by subsequent commands such as "print" or "set". The second form runs the command on the given frame.`}, @@ -270,8 +272,8 @@ The second form runs the command on the given frame.`}, }, helpMsg: `Move the current frame up. - up [] - up [] + up [] + up [] Move the current frame up by . The second form runs the command on the given frame.`}, {aliases: []string{"down"}, @@ -280,8 +282,8 @@ Move the current frame up by . The second form runs the command on the given }, helpMsg: `Move the current frame down. - down [] - down [] + down [] + down [] Move the current frame down by . The second form runs the command on the given frame.`}, {aliases: []string{"source"}, cmdFn: c.sourceCommand, helpMsg: `Executes a file containing a list of delve commands @@ -980,7 +982,13 @@ func (c *Commands) call(t *Term, ctx callContext, args string) error { if err := scopePrefixSwitch(t, ctx); err != nil { return err } - state, err := exitedToError(t.client.Call(args)) + const unsafePrefix = "-unsafe " + unsafe := false + if strings.HasPrefix(args, unsafePrefix) { + unsafe = true + args = args[len(unsafePrefix):] + } + state, err := exitedToError(t.client.Call(args, unsafe)) c.frame = 0 if err != nil { printcontextNoState(t) diff --git a/service/api/types.go b/service/api/types.go index 373cea9a1b3943ade2d74f03ef2b96826503d578..5c692ffbf47c9a4d0ea9a4765ee72631f20f27c6 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -308,6 +308,8 @@ 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 bool `json:"unsafeCall,omitempty"` } // BreakpointInfo contains informations about the current breakpoint diff --git a/service/client.go b/service/client.go index b2ff046d32119d384b2389df43c6e9f31920870a..f4211c3aec167e4ad07ee20c06c176ca3bda0423 100644 --- a/service/client.go +++ b/service/client.go @@ -39,7 +39,7 @@ type Client interface { // StepOut continues to the return address of the current function StepOut() (*api.DebuggerState, error) // Call resumes process execution while making a function call. - Call(expr string) (*api.DebuggerState, error) + Call(expr string, unsafe bool) (*api.DebuggerState, error) // SingleStep will step a single cpu instruction. StepInstruction() (*api.DebuggerState, error) diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 746684f02ff3025361b237181d5ee028136494ed..9a8920c7830876bed1a1dcf766b54a9e20004364 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -565,7 +565,7 @@ 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)) + err = proc.CallFunction(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/rpc2/client.go b/service/rpc2/client.go index bb22cd2e11af123c00db8cc19ec8f984aef28a19..2915a3612af9e60b7f112d329daa7d8542429a1e 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -148,9 +148,9 @@ func (c *RPCClient) StepOut() (*api.DebuggerState, error) { return &out.State, err } -func (c *RPCClient) Call(expr string) (*api.DebuggerState, error) { +func (c *RPCClient) Call(expr string, unsafe bool) (*api.DebuggerState, error) { var out CommandOut - err := c.call("Command", &api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr}, &out) + err := c.call("Command", &api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe}, &out) return &out.State, err } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index e6c07b723d32d4e413db793346d42e35e7143f76..86af8486c30495dcfcde46eaca48dda76575b316 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -1525,7 +1525,7 @@ func TestClientServerFunctionCall(t *testing.T) { state := <-c.Continue() assertNoError(state.Err, t, "Continue()") beforeCallFn := state.CurrentThread.Function.Name() - state, err := c.Call("call1(one, two)") + state, err := c.Call("call1(one, two)", false) assertNoError(err, t, "Call()") t.Logf("returned to %q", state.CurrentThread.Function.Name()) if state.CurrentThread.Function.Name() != beforeCallFn { @@ -1564,7 +1564,7 @@ func TestClientServerFunctionCallBadPos(t *testing.T) { state = <-c.Continue() assertNoError(state.Err, t, "Continue()") - state, err = c.Call("main.call1(main.zero, main.zero)") + 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) } @@ -1578,7 +1578,7 @@ func TestClientServerFunctionCallPanic(t *testing.T) { c.SetReturnValuesLoadConfig(&normalLoadConfig) state := <-c.Continue() assertNoError(state.Err, t, "Continue()") - state, err := c.Call("callpanic()") + state, err := c.Call("callpanic()", false) assertNoError(err, t, "Call()") t.Logf("at: %s:%d", state.CurrentThread.File, state.CurrentThread.Line) if state.CurrentThread.ReturnValues == nil { @@ -1604,7 +1604,7 @@ func TestClientServerFunctionCallStacktrace(t *testing.T) { c.SetReturnValuesLoadConfig(&api.LoadConfig{false, 0, 2048, 0, 0}) state := <-c.Continue() assertNoError(state.Err, t, "Continue()") - state, err := c.Call("callstacktrace()") + state, err := c.Call("callstacktrace()", false) assertNoError(err, t, "Call()") t.Logf("at: %s:%d", state.CurrentThread.File, state.CurrentThread.Line) if state.CurrentThread.ReturnValues == nil { diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 60461b86e169475b5d577cba90485d0d11967af0..83fb6e7af9f5c998f7b90d3bf073840e76aa5bc0 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -1101,7 +1101,8 @@ func TestCallFunction(t *testing.T) { // 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}, // 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 @@ -1125,8 +1126,14 @@ func TestCallFunction(t *testing.T) { {"fn2nil()", nil, errors.New("nil pointer dereference")}, {"ga.PRcvr(2)", []string{`:string:"2 - 0 = 2"`}, nil}, + + {"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 " + withTestProcess("fncall", t, func(p proc.Process, fixture protest.Fixture) { _, err := proc.FindFunctionLocation(p, "runtime.debugCallV1", true, 0) if err != nil { @@ -1134,8 +1141,16 @@ func TestCallFunction(t *testing.T) { } assertNoError(proc.Continue(p), t, "Continue()") for _, tc := range testcases { - err := proc.CallFunction(p, tc.expr, &pnormalLoadConfig) + expr := tc.expr + checkEscape := true + if strings.HasPrefix(expr, unsafePrefix) { + expr = expr[len(unsafePrefix):] + checkEscape = false + } + t.Logf("call %q", tc.expr) + err := proc.CallFunction(p, expr, &pnormalLoadConfig, checkEscape) if tc.err != nil { + if err == nil { t.Fatalf("call %q: expected error %q, got no error", tc.expr, tc.err.Error()) } @@ -1156,7 +1171,6 @@ func TestCallFunction(t *testing.T) { retvals[i] = api.ConvertVar(retvalsVar[i]) } - t.Logf("call %q", tc.expr) for i := range retvals { t.Logf("\t%s = %s", retvals[i].Name, retvals[i].SinglelineString()) }