
* 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
251 lines
6.8 KiB
Go
251 lines
6.8 KiB
Go
package proc_test
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/gob"
|
|
"flag"
|
|
"os"
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-delve/delve/pkg/dwarf/op"
|
|
"github.com/go-delve/delve/pkg/proc"
|
|
"github.com/go-delve/delve/pkg/proc/core"
|
|
|
|
protest "github.com/go-delve/delve/pkg/proc/test"
|
|
)
|
|
|
|
var fuzzEvalExpressionSetup = flag.Bool("fuzzevalexpressionsetup", false, "Performs setup for FuzzEvalExpression")
|
|
|
|
const (
|
|
fuzzExecutable = "testdata/fuzzexe"
|
|
fuzzCoredump = "testdata/fuzzcoredump"
|
|
fuzzInfoPath = "testdata/fuzzinfo"
|
|
)
|
|
|
|
type fuzzInfo struct {
|
|
Loc *proc.Location
|
|
Memchunks []memchunk
|
|
Regs op.DwarfRegisters
|
|
Fuzzbuf []byte
|
|
}
|
|
|
|
// FuzzEvalExpression fuzzes the variables loader and expression evaluator of Delve.
|
|
// To run it, execute the setup first:
|
|
//
|
|
// go test -run FuzzEvalExpression -fuzzevalexpressionsetup
|
|
//
|
|
// this will create some required files in testdata, the fuzzer can then be run with:
|
|
//
|
|
// go test -run NONE -fuzz FuzzEvalExpression -v -fuzzminimizetime=0
|
|
func FuzzEvalExpression(f *testing.F) {
|
|
if *fuzzEvalExpressionSetup {
|
|
doFuzzEvalExpressionSetup(f)
|
|
}
|
|
_, err := os.Stat(fuzzExecutable)
|
|
if os.IsNotExist(err) {
|
|
f.Skip("not setup")
|
|
}
|
|
bi := proc.NewBinaryInfo("linux", "amd64")
|
|
assertNoError(bi.LoadBinaryInfo(fuzzExecutable, 0, nil), f, "LoadBinaryInfo")
|
|
fh, err := os.Open(fuzzInfoPath)
|
|
assertNoError(err, f, "Open fuzzInfoPath")
|
|
defer fh.Close()
|
|
var fi fuzzInfo
|
|
gob.NewDecoder(fh).Decode(&fi)
|
|
fi.Regs.ByteOrder = binary.LittleEndian
|
|
fns, err := bi.FindFunction("main.main")
|
|
assertNoError(err, f, "FindFunction main.main")
|
|
fi.Loc.Fn = fns[0]
|
|
f.Add(fi.Fuzzbuf)
|
|
f.Fuzz(func(t *testing.T, fuzzbuf []byte) {
|
|
t.Log("fuzzbuf len", len(fuzzbuf))
|
|
mem := &core.SplicedMemory{}
|
|
|
|
// can't work with shrunk input fuzzbufs provided by the fuzzer, resize it
|
|
// so it is always at least the size we want.
|
|
lastMemchunk := fi.Memchunks[len(fi.Memchunks)-1]
|
|
fuzzbufsz := lastMemchunk.Idx + int(lastMemchunk.Sz)
|
|
if fuzzbufsz > len(fuzzbuf) {
|
|
newfuzzbuf := make([]byte, fuzzbufsz)
|
|
copy(newfuzzbuf, fuzzbuf)
|
|
fuzzbuf = newfuzzbuf
|
|
}
|
|
|
|
end := uint64(0)
|
|
|
|
for _, memchunk := range fi.Memchunks {
|
|
if end != memchunk.Addr {
|
|
mem.Add(&zeroReader{}, end, memchunk.Addr-end)
|
|
}
|
|
mem.Add(&offsetReader{fuzzbuf[memchunk.Idx:][:memchunk.Sz], memchunk.Addr}, memchunk.Addr, memchunk.Sz)
|
|
end = memchunk.Addr + memchunk.Sz
|
|
}
|
|
|
|
scope := &proc.EvalScope{Location: *fi.Loc, Regs: fi.Regs, Mem: memoryReaderWithFailingWrites{mem}, BinInfo: bi}
|
|
for _, tc := range getEvalExpressionTestCases() {
|
|
_, err := scope.EvalExpression(tc.name, pnormalLoadConfig)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "internal debugger error") {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func doFuzzEvalExpressionSetup(f *testing.F) {
|
|
os.Mkdir("testdata", 0700)
|
|
withTestProcess("testvariables2", f, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
|
|
exePath := fixture.Path
|
|
assertNoError(grp.Continue(), f, "Continue")
|
|
fh, err := os.Create(fuzzCoredump)
|
|
assertNoError(err, f, "Creating coredump")
|
|
var state proc.DumpState
|
|
p.Dump(fh, 0, &state)
|
|
assertNoError(state.Err, f, "Dump()")
|
|
out, err := exec.Command("cp", exePath, fuzzExecutable).CombinedOutput()
|
|
f.Log(string(out))
|
|
assertNoError(err, f, "cp")
|
|
})
|
|
|
|
// 2. Open the core file and search for the correct goroutine
|
|
|
|
cgrp, err := core.OpenCore(fuzzCoredump, fuzzExecutable, nil)
|
|
c := cgrp.Selected
|
|
assertNoError(err, f, "OpenCore")
|
|
gs, _, err := proc.GoroutinesInfo(c, 0, 0)
|
|
assertNoError(err, f, "GoroutinesInfo")
|
|
found := false
|
|
for _, g := range gs {
|
|
if strings.Contains(g.UserCurrent().File, "testvariables2") {
|
|
c.SwitchGoroutine(g)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
f.Errorf("could not find testvariables2 goroutine")
|
|
}
|
|
|
|
// 3. Run all the test cases on the core file, register which memory addresses are read
|
|
|
|
frames, err := proc.GoroutineStacktrace(c, c.SelectedGoroutine(), 2, 0)
|
|
assertNoError(err, f, "Stacktrace")
|
|
|
|
mem := c.Memory()
|
|
loc, _ := c.CurrentThread().Location()
|
|
tmem := &tracingMem{make(map[uint64]int), mem}
|
|
|
|
scope := &proc.EvalScope{Location: *loc, Regs: frames[0].Regs, Mem: tmem, BinInfo: c.BinInfo()}
|
|
|
|
for _, tc := range getEvalExpressionTestCases() {
|
|
scope.EvalExpression(tc.name, pnormalLoadConfig)
|
|
}
|
|
|
|
// 4. Copy all the memory we read into a buffer to use as fuzz example (if
|
|
// we try to use the whole core dump as fuzz example the Go fuzzer crashes)
|
|
|
|
memchunks, fuzzbuf := tmem.memoryReadsCondensed()
|
|
|
|
fh, err := os.Create(fuzzInfoPath)
|
|
assertNoError(err, f, "os.Create")
|
|
frames[0].Regs.ByteOrder = nil
|
|
loc.Fn = nil
|
|
assertNoError(gob.NewEncoder(fh).Encode(fuzzInfo{
|
|
Loc: loc,
|
|
Memchunks: memchunks,
|
|
Regs: frames[0].Regs,
|
|
Fuzzbuf: fuzzbuf,
|
|
}), f, "Encode")
|
|
assertNoError(fh.Close(), f, "Close")
|
|
}
|
|
|
|
type tracingMem struct {
|
|
read map[uint64]int
|
|
mem proc.MemoryReadWriter
|
|
}
|
|
|
|
func (tmem *tracingMem) ReadMemory(b []byte, n uint64) (int, error) {
|
|
if len(b) > tmem.read[n] {
|
|
tmem.read[n] = len(b)
|
|
}
|
|
return tmem.mem.ReadMemory(b, n)
|
|
}
|
|
|
|
func (tmem *tracingMem) WriteMemory(uint64, []byte) (int, error) {
|
|
panic("should not be called")
|
|
}
|
|
|
|
type memchunk struct {
|
|
Addr, Sz uint64
|
|
Idx int
|
|
}
|
|
|
|
func (tmem *tracingMem) memoryReadsCondensed() ([]memchunk, []byte) {
|
|
memoryReads := make([]memchunk, 0, len(tmem.read))
|
|
for addr, sz := range tmem.read {
|
|
memoryReads = append(memoryReads, memchunk{addr, uint64(sz), 0})
|
|
}
|
|
sort.Slice(memoryReads, func(i, j int) bool { return memoryReads[i].Addr < memoryReads[j].Addr })
|
|
|
|
memoryReadsCondensed := make([]memchunk, 0, len(memoryReads))
|
|
for _, v := range memoryReads {
|
|
if len(memoryReadsCondensed) != 0 {
|
|
last := &memoryReadsCondensed[len(memoryReadsCondensed)-1]
|
|
if last.Addr+last.Sz >= v.Addr {
|
|
end := v.Addr + v.Sz
|
|
sz2 := end - last.Addr
|
|
if sz2 > last.Sz {
|
|
last.Sz = sz2
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
memoryReadsCondensed = append(memoryReadsCondensed, v)
|
|
}
|
|
|
|
fuzzbuf := []byte{}
|
|
for i := range memoryReadsCondensed {
|
|
buf := make([]byte, memoryReadsCondensed[i].Sz)
|
|
tmem.mem.ReadMemory(buf, memoryReadsCondensed[i].Addr)
|
|
memoryReadsCondensed[i].Idx = len(fuzzbuf)
|
|
fuzzbuf = append(fuzzbuf, buf...)
|
|
}
|
|
|
|
return memoryReadsCondensed, fuzzbuf
|
|
}
|
|
|
|
type offsetReader struct {
|
|
buf []byte
|
|
addr uint64
|
|
}
|
|
|
|
func (or *offsetReader) ReadMemory(buf []byte, addr uint64) (int, error) {
|
|
copy(buf, or.buf[addr-or.addr:][:len(buf)])
|
|
return len(buf), nil
|
|
}
|
|
|
|
type memoryReaderWithFailingWrites struct {
|
|
proc.MemoryReader
|
|
}
|
|
|
|
func (w memoryReaderWithFailingWrites) WriteMemory(uint64, []byte) (int, error) {
|
|
panic("should not be called")
|
|
}
|
|
|
|
type zeroReader struct{}
|
|
|
|
func (*zeroReader) ReadMemory(b []byte, addr uint64) (int, error) {
|
|
for i := range b {
|
|
b[i] = 0
|
|
}
|
|
return len(b), nil
|
|
}
|
|
|
|
func (*zeroReader) WriteMemory(b []byte, addr uint64) (int, error) {
|
|
panic("should not be called")
|
|
}
|