delve/pkg/proc/core/core_test.go
Alessandro Arzilli 1b0c4310c4
proc: give unique addresses to registerized variables (#2527)
We told clients that further loading of variables can be done by
specifying a type cast using the address of a variable that we
returned.
This does not work for registerized variables (or, in general,
variables that have a complex location expression) because we don't
give them unique addresses and we throw away the compositeMemory object
we made to read them.

This commit changes proc so that:

1. variables with location expression divided in pieces do get a unique
   memory address
2. the compositeMemory object is saved somewhere
3. when an integer is cast back into a pointer type we look through our
   saved compositeMemory objects to see if there is one that covers the
   specified address and use it.

The unique memory addresses we generate have the MSB set to 1, as
specified by the Intel 86x64 manual addresses in this form are reserved
for kernel memory (which we can not read anyway) so we are guaranteed
to never generate a fake memory address that overlaps a real memory
address of the application.

The unfortunate side effect of this is that it will break clients that
do not deserialize the address to a 64bit integer. This practice is
contrary to how we defined our types and contrary to the specification
of the JSON format, as of json.org, however it is also fairly common,
due to javascript itself having only 53bit integers.

We could come up with a new mechanism but then even more old clients
would have to be changed.
2021-07-02 18:37:55 +02:00

526 lines
14 KiB
Go

package core
import (
"bytes"
"flag"
"fmt"
"go/constant"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/go-delve/delve/pkg/goversion"
"github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/pkg/proc/test"
)
var buildMode string
func TestMain(m *testing.M) {
flag.StringVar(&buildMode, "test-buildmode", "", "selects build mode")
flag.Parse()
if buildMode != "" && buildMode != "pie" {
fmt.Fprintf(os.Stderr, "unknown build mode %q", buildMode)
os.Exit(1)
}
os.Exit(test.RunTestsWithFixtures(m))
}
func assertNoError(err error, t testing.TB, s string) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
fname := filepath.Base(file)
t.Fatalf("failed assertion at %s:%d: %s - %s\n", fname, line, s, err)
}
}
func TestSplicedReader(t *testing.T) {
data := []byte{}
data2 := []byte{}
for i := 0; i < 100; i++ {
data = append(data, byte(i))
data2 = append(data2, byte(i+100))
}
type region struct {
data []byte
off uint64
length uint64
}
tests := []struct {
name string
regions []region
readAddr uint64
readLen int
want []byte
}{
{
"Insert after",
[]region{
{data, 0, 1},
{data2, 1, 1},
},
0,
2,
[]byte{0, 101},
},
{
"Insert before",
[]region{
{data, 1, 1},
{data2, 0, 1},
},
0,
2,
[]byte{100, 1},
},
{
"Completely overwrite",
[]region{
{data, 1, 1},
{data2, 0, 3},
},
0,
3,
[]byte{100, 101, 102},
},
{
"Overwrite end",
[]region{
{data, 0, 2},
{data2, 1, 2},
},
0,
3,
[]byte{0, 101, 102},
},
{
"Overwrite start",
[]region{
{data, 0, 3},
{data2, 0, 2},
},
0,
3,
[]byte{100, 101, 2},
},
{
"Punch hole",
[]region{
{data, 0, 5},
{data2, 1, 3},
},
0,
5,
[]byte{0, 101, 102, 103, 4},
},
{
"Overlap two",
[]region{
{data, 10, 4},
{data, 14, 4},
{data2, 12, 4},
},
10,
8,
[]byte{10, 11, 112, 113, 114, 115, 16, 17},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mem := &splicedMemory{}
for _, region := range test.regions {
r := bytes.NewReader(region.data)
mem.Add(&offsetReaderAt{r, 0}, region.off, region.length)
}
got := make([]byte, test.readLen)
n, err := mem.ReadMemory(got, test.readAddr)
if n != test.readLen || err != nil || !reflect.DeepEqual(got, test.want) {
t.Errorf("ReadAt = %v, %v, %v, want %v, %v, %v", n, err, got, test.readLen, nil, test.want)
}
})
}
// Test some ReadMemory errors
mem := &splicedMemory{}
for _, region := range []region{
{[]byte{0xa1, 0xa2, 0xa3, 0xa4}, 0x1000, 4},
{[]byte{0xb1, 0xb2, 0xb3, 0xb4}, 0x1004, 4},
{[]byte{0xc1, 0xc2, 0xc3, 0xc4}, 0x1010, 4},
} {
r := bytes.NewReader(region.data)
mem.Add(&offsetReaderAt{r, region.off}, region.off, region.length)
}
got := make([]byte, 4)
// Read before the first mapping
_, err := mem.ReadMemory(got, 0x900)
if err == nil || !strings.HasPrefix(err.Error(), "error while reading spliced memory at 0x900") {
t.Errorf("Read before the start of memory didn't fail (or wrong error): %v", err)
}
// Read after the last mapping
_, err = mem.ReadMemory(got, 0x1100)
if err == nil || (err.Error() != "offset 4352 did not match any regions") {
t.Errorf("Read after the end of memory didn't fail (or wrong error): %v", err)
}
// Read at the start of the first entry
_, err = mem.ReadMemory(got, 0x1000)
if err != nil || !bytes.Equal(got, []byte{0xa1, 0xa2, 0xa3, 0xa4}) {
t.Errorf("Reading at the start of the first entry: %v %#x", err, got)
}
// Read at the start of the second entry
_, err = mem.ReadMemory(got, 0x1004)
if err != nil || !bytes.Equal(got, []byte{0xb1, 0xb2, 0xb3, 0xb4}) {
t.Errorf("Reading at the start of the second entry: %v %#x", err, got)
}
// Read straddling entries 1 and 2
_, err = mem.ReadMemory(got, 0x1002)
if err != nil || !bytes.Equal(got, []byte{0xa3, 0xa4, 0xb1, 0xb2}) {
t.Errorf("Straddled read of the second entry: %v %#x", err, got)
}
// Read past the end of the second entry
_, err = mem.ReadMemory(got, 0x1007)
if err == nil || !strings.HasPrefix(err.Error(), "error while reading spliced memory at 0x1008") {
t.Errorf("Read into gap: %v", err)
}
}
func withCoreFile(t *testing.T, name, args string) *proc.Target {
// This is all very fragile and won't work on hosts with non-default core patterns.
// Might be better to check in the binary and core?
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
test.PathsToRemove = append(test.PathsToRemove, tempDir)
var buildFlags test.BuildFlags
if buildMode == "pie" {
buildFlags = test.BuildModePIE
}
fix := test.BuildFixture(name, buildFlags)
bashCmd := fmt.Sprintf("cd %v && ulimit -c unlimited && GOTRACEBACK=crash %v %s", tempDir, fix.Path, args)
exec.Command("bash", "-c", bashCmd).Run()
cores, err := filepath.Glob(path.Join(tempDir, "core*"))
switch {
case err != nil || len(cores) > 1:
t.Fatalf("Got %v, wanted one file named core* in %v", cores, tempDir)
case len(cores) == 0:
t.Skipf("core file was not produced, could not run test")
return nil
}
corePath := cores[0]
p, err := OpenCore(corePath, fix.Path, []string{})
if err != nil {
t.Errorf("OpenCore(%q) failed: %v", corePath, err)
pat, err := ioutil.ReadFile("/proc/sys/kernel/core_pattern")
t.Errorf("read core_pattern: %q, %v", pat, err)
apport, err := ioutil.ReadFile("/var/log/apport.log")
t.Errorf("read apport log: %q, %v", apport, err)
t.Fatalf("previous errors")
}
return p
}
func logRegisters(t *testing.T, regs proc.Registers, arch *proc.Arch) {
dregs := arch.RegistersToDwarfRegisters(0, regs)
dregs.Reg(^uint64(0))
for i := 0; i < dregs.CurrentSize(); i++ {
reg := dregs.Reg(uint64(i))
if reg == nil {
continue
}
name, _, value := arch.DwarfRegisterToString(i, reg)
t.Logf("%s = %s", name, value)
}
}
func TestCore(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
return
}
if runtime.GOOS == "linux" && os.Getenv("CI") == "true" && buildMode == "pie" {
t.Skip("disabled on linux, Github Actions, with PIE buildmode")
}
p := withCoreFile(t, "panic", "")
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 {
t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err)
}
var panicking *proc.G
var panickingStack []proc.Stackframe
for _, g := range gs {
t.Logf("Goroutine %d", g.ID)
stack, err := g.Stacktrace(10, 0)
if err != nil {
t.Errorf("Stacktrace() on goroutine %v = %v", g, err)
}
for _, frame := range stack {
fnname := ""
if frame.Call.Fn != nil {
fnname = frame.Call.Fn.Name
}
t.Logf("\tframe %s:%d in %s %#x (systemstack: %v)", frame.Call.File, frame.Call.Line, fnname, frame.Call.PC, frame.SystemStack)
if frame.Current.Fn != nil && strings.Contains(frame.Current.Fn.Name, "panic") {
panicking = g
panickingStack = stack
}
}
}
if panicking == nil {
t.Fatalf("Didn't find a call to panic in goroutine stacks: %v", gs)
}
var mainFrame *proc.Stackframe
// Walk backward, because the current function seems to be main.main
// in the actual call to panic().
for i := len(panickingStack) - 1; i >= 0; i-- {
if panickingStack[i].Current.Fn != nil && panickingStack[i].Current.Fn.Name == "main.main" {
mainFrame = &panickingStack[i]
}
}
if mainFrame == nil {
t.Fatalf("Couldn't find main in stack %v", panickingStack)
}
msg, err := proc.FrameToScope(p, p.BinInfo(), p.Memory(), nil, *mainFrame).EvalVariable("msg", proc.LoadConfig{MaxStringLen: 64})
if err != nil {
t.Fatalf("Couldn't EvalVariable(msg, ...): %v", err)
}
if constant.StringVal(msg.Value) != "BOOM!" {
t.Errorf("main.msg = %q, want %q", msg.Value, "BOOM!")
}
regs, err := p.CurrentThread().Registers()
if err != nil {
t.Fatalf("Couldn't get current thread registers: %v", err)
}
logRegisters(t, regs, p.BinInfo().Arch)
}
func TestCoreFpRegisters(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
return
}
// in go1.10 the crash is executed on a different thread and registers are
// no longer available in the core dump.
if ver, _ := goversion.Parse(runtime.Version()); ver.Major < 0 || ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 10, Rev: -1}) {
t.Skip("not supported in go1.10 and later")
}
p := withCoreFile(t, "fputest/", "panic")
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 {
t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err)
}
var regs proc.Registers
for _, thread := range p.ThreadList() {
frames, err := proc.ThreadStacktrace(thread, 10)
if err != nil {
t.Errorf("ThreadStacktrace for %x = %v", thread.ThreadID(), err)
continue
}
for i := range frames {
if frames[i].Current.Fn == nil {
continue
}
if frames[i].Current.Fn.Name == "main.main" {
regs, err = thread.Registers()
if err != nil {
t.Fatalf("Could not get registers for thread %x, %v", thread.ThreadID(), err)
}
break
}
}
if regs != nil {
break
}
}
regtests := []struct{ name, value string }{
{"ST(0)", "0x3fffe666660000000000"},
{"ST(1)", "0x3fffd9999a0000000000"},
{"ST(2)", "0x3fffcccccd0000000000"},
{"ST(3)", "0x3fffc000000000000000"},
{"ST(4)", "0x3fffb333333333333000"},
{"ST(5)", "0x3fffa666666666666800"},
{"ST(6)", "0x3fff9999999999999800"},
{"ST(7)", "0x3fff8cccccccccccd000"},
// Unlike TestClientServer_FpRegisters in service/test/integration2_test
// we can not test the value of XMM0, it probably has been reused by
// something between the panic and the time we get the core dump.
{"XMM9", "0x3ff66666666666663ff4cccccccccccd"},
{"XMM10", "0x3fe666663fd9999a3fcccccd3fc00000"},
{"XMM3", "0x3ff199999999999a3ff3333333333333"},
{"XMM4", "0x3ff4cccccccccccd3ff6666666666666"},
{"XMM5", "0x3fcccccd3fc000003fe666663fd9999a"},
{"XMM6", "0x4004cccccccccccc4003333333333334"},
{"XMM7", "0x40026666666666664002666666666666"},
{"XMM8", "0x4059999a404ccccd4059999a404ccccd"},
}
arch := p.BinInfo().Arch
logRegisters(t, regs, arch)
dregs := arch.RegistersToDwarfRegisters(0, regs)
for _, regtest := range regtests {
found := false
dregs.Reg(^uint64(0))
for i := 0; i < dregs.CurrentSize(); i++ {
reg := dregs.Reg(uint64(i))
regname, _, regval := arch.DwarfRegisterToString(i, reg)
if reg != nil && regname == regtest.name {
found = true
if !strings.HasPrefix(regval, regtest.value) {
t.Fatalf("register %s expected %q got %q", regname, regtest.value, regval)
}
}
}
if !found {
t.Fatalf("register %s not found: %v", regtest.name, regs)
}
}
}
func TestCoreWithEmptyString(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
return
}
if runtime.GOOS == "linux" && os.Getenv("CI") == "true" && buildMode == "pie" {
t.Skip("disabled on linux, Github Actions, with PIE buildmode")
}
p := withCoreFile(t, "coreemptystring", "")
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
assertNoError(err, t, "GoroutinesInfo")
var mainFrame *proc.Stackframe
mainSearch:
for _, g := range gs {
stack, err := g.Stacktrace(10, 0)
assertNoError(err, t, "Stacktrace()")
for _, frame := range stack {
if frame.Current.Fn != nil && frame.Current.Fn.Name == "main.main" {
mainFrame = &frame
break mainSearch
}
}
}
if mainFrame == nil {
t.Fatal("could not find main.main frame")
}
scope := proc.FrameToScope(p, p.BinInfo(), p.Memory(), nil, *mainFrame)
loadConfig := proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1}
v1, err := scope.EvalVariable("t", loadConfig)
assertNoError(err, t, "EvalVariable(t)")
assertNoError(v1.Unreadable, t, "unreadable variable 't'")
t.Logf("t = %#v\n", v1)
v2, err := scope.EvalVariable("s", loadConfig)
assertNoError(err, t, "EvalVariable(s)")
assertNoError(v2.Unreadable, t, "unreadable variable 's'")
t.Logf("s = %#v\n", v2)
}
func TestMinidump(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("minidumps can only be produced on windows")
}
var buildFlags test.BuildFlags
if buildMode == "pie" {
buildFlags = test.BuildModePIE
}
fix := test.BuildFixture("sleep", buildFlags)
mdmpPath := procdump(t, fix.Path)
p, err := OpenCore(mdmpPath, fix.Path, []string{})
if err != nil {
t.Fatalf("OpenCore: %v", err)
}
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 {
t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err)
}
t.Logf("%d goroutines", len(gs))
foundMain, foundTime := false, false
for _, g := range gs {
stack, err := g.Stacktrace(10, 0)
if err != nil {
t.Errorf("Stacktrace() on goroutine %v = %v", g, err)
}
t.Logf("goroutine %d", g.ID)
for _, frame := range stack {
name := "?"
if frame.Current.Fn != nil {
name = frame.Current.Fn.Name
}
t.Logf("\t%s:%d in %s %#x", frame.Current.File, frame.Current.Line, name, frame.Current.PC)
if frame.Current.Fn == nil {
continue
}
switch frame.Current.Fn.Name {
case "main.main":
foundMain = true
case "time.Sleep":
foundTime = true
}
}
if foundMain != foundTime {
t.Errorf("found main.main but no time.Sleep (or viceversa) %v %v", foundMain, foundTime)
}
}
if !foundMain {
t.Fatalf("could not find main goroutine")
}
}
func procdump(t *testing.T, exePath string) string {
exeDir := filepath.Dir(exePath)
cmd := exec.Command("procdump64", "-accepteula", "-ma", "-n", "1", "-s", "3", "-x", exeDir, exePath, "quit")
out, err := cmd.CombinedOutput() // procdump exits with non-zero status on success, so we have to ignore the error here
if !strings.Contains(string(out), "Dump count reached.") {
t.Fatalf("possible error running procdump64, output: %q, error: %v", string(out), err)
}
dh, err := os.Open(exeDir)
if err != nil {
t.Fatalf("could not open executable file directory %q: %v", exeDir, err)
}
defer dh.Close()
fis, err := dh.Readdir(-1)
if err != nil {
t.Fatalf("could not read executable file directory %q: %v", exeDir, err)
}
t.Logf("looking for dump file")
exeName := filepath.Base(exePath)
for _, fi := range fis {
name := fi.Name()
t.Logf("\t%s", name)
if strings.HasPrefix(name, exeName) && strings.HasSuffix(name, ".dmp") {
mdmpPath := filepath.Join(exeDir, name)
test.PathsToRemove = append(test.PathsToRemove, mdmpPath)
return mdmpPath
}
}
t.Fatalf("could not find dump file")
return ""
}