Skip to content

Commit

Permalink
proc: fuzzing expression evaluator and variable loader (go-delve#3293)
Browse files Browse the repository at this point in the history
Add code to fuzz the expression evaluator and variable loader, see
comment to FuzzEvalExpression for how to use this.
  • Loading branch information
aarzilli committed Mar 16, 2023
1 parent 9faf66b commit 1522382
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 11 deletions.
10 changes: 5 additions & 5 deletions pkg/proc/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
// ErrNoThreads core file did not contain any threads.
var ErrNoThreads = errors.New("no threads found in core file")

// A splicedMemory represents a memory space formed from multiple regions,
// A SplicedMemory represents a memory space formed from multiple regions,
// each of which may override previously regions. For example, in the following
// core, the program text was loaded at 0x400000:
// Start End Page Offset
Expand All @@ -29,7 +29,7 @@ var ErrNoThreads = errors.New("no threads found in core file")
//
// This can be represented in a SplicedMemory by adding the original region,
// then putting the RW mapping on top of it.
type splicedMemory struct {
type SplicedMemory struct {
readers []readerEntry
}

Expand All @@ -40,7 +40,7 @@ type readerEntry struct {
}

// Add adds a new region to the SplicedMemory, which may override existing regions.
func (r *splicedMemory) Add(reader proc.MemoryReader, off, length uint64) {
func (r *SplicedMemory) Add(reader proc.MemoryReader, off, length uint64) {
if length == 0 {
return
}
Expand Down Expand Up @@ -100,7 +100,7 @@ func (r *splicedMemory) Add(reader proc.MemoryReader, off, length uint64) {
}

// ReadMemory implements MemoryReader.ReadMemory.
func (r *splicedMemory) ReadMemory(buf []byte, addr uint64) (n int, err error) {
func (r *SplicedMemory) ReadMemory(buf []byte, addr uint64) (n int, err error) {
started := false
for _, entry := range r.readers {
if entry.offset+entry.length <= addr {
Expand Down Expand Up @@ -201,7 +201,7 @@ var openFns = []openFn{readLinuxOrPlatformIndependentCore, readAMD64Minidump}
// any of the supported formats.
var ErrUnrecognizedFormat = errors.New("unrecognized core format")

// OpenCore will open the core file and return a Process struct.
// OpenCore will open the core file and return a *proc.TargetGroup.
// If the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in.
func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.TargetGroup, error) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/proc/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func TestSplicedReader(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mem := &splicedMemory{}
mem := &SplicedMemory{}
for _, region := range test.regions {
r := bytes.NewReader(region.data)
mem.Add(&offsetReaderAt{r, 0}, region.off, region.length)
Expand All @@ -149,7 +149,7 @@ func TestSplicedReader(t *testing.T) {

// Test some ReadMemory errors

mem := &splicedMemory{}
mem := &SplicedMemory{}
for _, region := range []region{
{[]byte{0xa1, 0xa2, 0xa3, 0xa4}, 0x1000, 4},
{[]byte{0xb1, 0xb2, 0xb3, 0xb4}, 0x1004, 4},
Expand Down
2 changes: 1 addition & 1 deletion pkg/proc/core/linux_core.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ func skipPadding(r io.ReadSeeker, pad int64) error {
}

func buildMemory(core, exeELF *elf.File, exe io.ReaderAt, notes []*note) proc.MemoryReader {
memory := &splicedMemory{}
memory := &SplicedMemory{}

// For now, assume all file mappings are to the exe.
for _, note := range notes {
Expand Down
2 changes: 1 addition & 1 deletion pkg/proc/core/windows_amd64_minidump.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func readAMD64Minidump(minidumpPath, exePath string) (*process, proc.Thread, err
return nil, nil, err
}

memory := &splicedMemory{}
memory := &SplicedMemory{}

for i := range mdmp.MemoryRanges {
m := &mdmp.MemoryRanges[i]
Expand Down
245 changes: 245 additions & 0 deletions pkg/proc/variables_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
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() {
scope.EvalExpression(tc.name, pnormalLoadConfig)
}
})
}

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 := c.SelectedGoroutine().Stacktrace(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")
}
9 changes: 7 additions & 2 deletions pkg/proc/variables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func matchStringOrPrefix(output, target string) bool {
}
}

func assertVariable(t *testing.T, variable *proc.Variable, expected varTest) {
func assertVariable(t testing.TB, variable *proc.Variable, expected varTest) {
if expected.preserveName {
if variable.Name != expected.name {
t.Fatalf("Expected %s got %s\n", expected.name, variable.Name)
Expand Down Expand Up @@ -511,7 +511,7 @@ func TestComplexSetting(t *testing.T) {
})
}

func TestEvalExpression(t *testing.T) {
func getEvalExpressionTestCases() []varTest {
testcases := []varTest{
// slice/array/string subscript
{"s1[0]", false, "\"one\"", "\"one\"", "string", nil},
Expand Down Expand Up @@ -845,6 +845,11 @@ func TestEvalExpression(t *testing.T) {
}
}

return testcases
}

func TestEvalExpression(t *testing.T) {
testcases := getEvalExpressionTestCases()
protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue() returned an error")
Expand Down

0 comments on commit 1522382

Please sign in to comment.