diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go index 1407627c08..734dbd4159 100644 --- a/pkg/proc/core/core.go +++ b/pkg/proc/core/core.go @@ -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 @@ -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 } @@ -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 } @@ -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 { @@ -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) { diff --git a/pkg/proc/core/core_test.go b/pkg/proc/core/core_test.go index 4daabd9cbf..3444fdc348 100644 --- a/pkg/proc/core/core_test.go +++ b/pkg/proc/core/core_test.go @@ -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) @@ -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}, diff --git a/pkg/proc/core/linux_core.go b/pkg/proc/core/linux_core.go index ab53291dc8..4dd94f648b 100644 --- a/pkg/proc/core/linux_core.go +++ b/pkg/proc/core/linux_core.go @@ -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 { diff --git a/pkg/proc/core/windows_amd64_minidump.go b/pkg/proc/core/windows_amd64_minidump.go index aeadd11aab..0bcb5e4ecd 100644 --- a/pkg/proc/core/windows_amd64_minidump.go +++ b/pkg/proc/core/windows_amd64_minidump.go @@ -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] diff --git a/pkg/proc/variables_fuzz_test.go b/pkg/proc/variables_fuzz_test.go new file mode 100644 index 0000000000..07e28d3736 --- /dev/null +++ b/pkg/proc/variables_fuzz_test.go @@ -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") +} diff --git a/pkg/proc/variables_test.go b/pkg/proc/variables_test.go index 2b7095a91a..77b8677a53 100644 --- a/pkg/proc/variables_test.go +++ b/pkg/proc/variables_test.go @@ -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) @@ -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}, @@ -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")