Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

function call injection has problems when the function call involves passing variables stored in registers #3310

Open
Codelax opened this issue Mar 20, 2023 · 0 comments

Comments

@Codelax
Copy link

Codelax commented Mar 20, 2023

  1. What version of Delve are you using (dlv version)?
    dlv 1.20.1
  2. What version of Go are you using? (go version)?
    go 1.20.1
  3. What operating system and processor architecture are you using?
    Linux fedora 6.1.18-200.fc37.x86_64
  4. What did you do?

I debugged my program and when trying a call to get some info, delve took all my ram and my computer crashed.
When limiting with ulimit I get fatal error: out of memory (full stacktrace)

I reproduced the issue with a minimal go program

func reflectFunc(value reflect.Value) {
	fmt.Printf("%s\n", value.Type().Name())
}

func main() {
	i := 2
	val := reflect.ValueOf(i)
	reflectFunc(val)
}

I just put a breakpoint on main.reflectFunc, did a single step.

     4:		"fmt"
     5:		"reflect"
     6:	)
     7:	
     8:	func reflectFunc(value reflect.Value) {
=>   9:		fmt.Printf("%s\n", value.Type().Name())
    10:	}
    11:	
    12:	func main() {
    13:		i := 2
    14:		val := reflect.ValueOf(i)

Then I used call value.Type()

  1. What did you expect to see?
    I expected an error or my call to work

  2. What did you see instead?

My computer crashed :'(

aarzilli added a commit to aarzilli/delve that referenced this issue Mar 24, 2023
deref'd

Fix infinite recursion if escapeCheck, at some point during its
recursion, creates an unreadable variable.

The deeper reason for this is that we evaluate function calls in a very
weird order so that we can always have stack space to store
intermediate evaluation results.
The variable 'value' happens to be stored in a register when we try to
make the call and because of our weird evaluation strategy registers
are no longer available to us when we evaluate 'value'.

This is not a complete fix for the issue, the real fix would be to
evaluate everything in its natural order, storing intermediate values
in Delve's memory instead of the target's stack. To do this we need a
mechanism to pin heap allocated objects, which at the moment does not
exist.

Updates go-delve#3310
derekparker pushed a commit that referenced this issue Mar 27, 2023
deref'd

Fix infinite recursion if escapeCheck, at some point during its
recursion, creates an unreadable variable.

The deeper reason for this is that we evaluate function calls in a very
weird order so that we can always have stack space to store
intermediate evaluation results.
The variable 'value' happens to be stored in a register when we try to
make the call and because of our weird evaluation strategy registers
are no longer available to us when we evaluate 'value'.

This is not a complete fix for the issue, the real fix would be to
evaluate everything in its natural order, storing intermediate values
in Delve's memory instead of the target's stack. To do this we need a
mechanism to pin heap allocated objects, which at the moment does not
exist.

Updates #3310
@aarzilli aarzilli changed the title reflect.Value call Type() out of memory function call injection has problems when the function call involves passing variables stored in registers Apr 16, 2023
aarzilli added a commit to aarzilli/delve that referenced this issue Sep 19, 2023
This commit splits expression evaluation into two parts. The first part (in
pkg/proc/evalop/evalcompile.go) "compiles" as ast.Expr into a list of
instructions (defined in pkg/proc/evalop/ops.go) for a stack machine
(defined by `proc.(*evalStack)`).
The second part is a stack machine (implemented by `proc.(*EvalScope).eval`
and `proc.(*EvalScope).evalOne`) that has two modes of operation: in the
main mode it executes inteructions from the list (by calling `evalOne`), in
the second mode it executes the call injection protocol by calling
`funcCallStep` repeatedly until it either the protocol finishes, needs more
input from the stack machine (to set call arguments) or fails.

This approach has several benefits:

- it is now possible to remove the goroutine we use to evaluate expression
  and the channel used to communicate with the Continue loop.
- every time we resume the target to execute the call injection protocol we
  need to update several local variables to match the changed state of the
  target, this is now done at the top level of the evaluation loop instead of
  being hidden inside a recurisive evaluator
- using runtime.Pin to pin addresses returned by an injected call would
  allow us to use a more natural evaluation order for function calls, which
  would solve some bugs go-delve#3310, allow users to inspect values returned by a
  call injection go-delve#1599 and allow implementing some other features go-delve#1465. Doing
  this with the recursive evaluator, while keeping backwards compatibility
  with versions of Go that do not have runtime.Pin is very hard. However after
  this change we can simply conditionally change how compileFunctionCall works
  and add some opcodes.
aarzilli added a commit to aarzilli/delve that referenced this issue Sep 22, 2023
This commit splits expression evaluation into two parts. The first part (in
pkg/proc/evalop/evalcompile.go) "compiles" as ast.Expr into a list of
instructions (defined in pkg/proc/evalop/ops.go) for a stack machine
(defined by `proc.(*evalStack)`).
The second part is a stack machine (implemented by `proc.(*EvalScope).eval`
and `proc.(*EvalScope).evalOne`) that has two modes of operation: in the
main mode it executes inteructions from the list (by calling `evalOne`), in
the second mode it executes the call injection protocol by calling
`funcCallStep` repeatedly until it either the protocol finishes, needs more
input from the stack machine (to set call arguments) or fails.

This approach has several benefits:

- it is now possible to remove the goroutine we use to evaluate expression
  and the channel used to communicate with the Continue loop.
- every time we resume the target to execute the call injection protocol we
  need to update several local variables to match the changed state of the
  target, this is now done at the top level of the evaluation loop instead of
  being hidden inside a recurisive evaluator
- using runtime.Pin to pin addresses returned by an injected call would
  allow us to use a more natural evaluation order for function calls, which
  would solve some bugs go-delve#3310, allow users to inspect values returned by a
  call injection go-delve#1599 and allow implementing some other features go-delve#1465. Doing
  this with the recursive evaluator, while keeping backwards compatibility
  with versions of Go that do not have runtime.Pin is very hard. However after
  this change we can simply conditionally change how compileFunctionCall works
  and add some opcodes.
aarzilli added a commit to aarzilli/delve that referenced this issue Oct 9, 2023
This commit splits expression evaluation into two parts. The first part (in
pkg/proc/evalop/evalcompile.go) "compiles" as ast.Expr into a list of
instructions (defined in pkg/proc/evalop/ops.go) for a stack machine
(defined by `proc.(*evalStack)`).
The second part is a stack machine (implemented by `proc.(*EvalScope).eval`
and `proc.(*EvalScope).evalOne`) that has two modes of operation: in the
main mode it executes inteructions from the list (by calling `evalOne`), in
the second mode it executes the call injection protocol by calling
`funcCallStep` repeatedly until it either the protocol finishes, needs more
input from the stack machine (to set call arguments) or fails.

This approach has several benefits:

- it is now possible to remove the goroutine we use to evaluate expression
  and the channel used to communicate with the Continue loop.
- every time we resume the target to execute the call injection protocol we
  need to update several local variables to match the changed state of the
  target, this is now done at the top level of the evaluation loop instead of
  being hidden inside a recurisive evaluator
- using runtime.Pin to pin addresses returned by an injected call would
  allow us to use a more natural evaluation order for function calls, which
  would solve some bugs go-delve#3310, allow users to inspect values returned by a
  call injection go-delve#1599 and allow implementing some other features go-delve#1465. Doing
  this with the recursive evaluator, while keeping backwards compatibility
  with versions of Go that do not have runtime.Pin is very hard. However after
  this change we can simply conditionally change how compileFunctionCall works
  and add some opcodes.
derekparker pushed a commit that referenced this issue Oct 17, 2023
* proc: use stack machine to evaluate expressions

This commit splits expression evaluation into two parts. The first part (in
pkg/proc/evalop/evalcompile.go) "compiles" as ast.Expr into a list of
instructions (defined in pkg/proc/evalop/ops.go) for a stack machine
(defined by `proc.(*evalStack)`).
The second part is a stack machine (implemented by `proc.(*EvalScope).eval`
and `proc.(*EvalScope).evalOne`) that has two modes of operation: in the
main mode it executes inteructions from the list (by calling `evalOne`), in
the second mode it executes the call injection protocol by calling
`funcCallStep` repeatedly until it either the protocol finishes, needs more
input from the stack machine (to set call arguments) or fails.

This approach has several benefits:

- it is now possible to remove the goroutine we use to evaluate expression
  and the channel used to communicate with the Continue loop.
- every time we resume the target to execute the call injection protocol we
  need to update several local variables to match the changed state of the
  target, this is now done at the top level of the evaluation loop instead of
  being hidden inside a recurisive evaluator
- using runtime.Pin to pin addresses returned by an injected call would
  allow us to use a more natural evaluation order for function calls, which
  would solve some bugs #3310, allow users to inspect values returned by a
  call injection #1599 and allow implementing some other features #1465. Doing
  this with the recursive evaluator, while keeping backwards compatibility
  with versions of Go that do not have runtime.Pin is very hard. However after
  this change we can simply conditionally change how compileFunctionCall works
  and add some opcodes.

* review round 1

* review round 2
aarzilli added a commit to aarzilli/delve that referenced this issue Jan 23, 2024
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 go-delve#3310
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants