blob: b40e4103f7aa19116238d984ecf17e7e5b9eab5c [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package gocore
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"golang.org/x/debug/internal/core"
"golang.org/x/debug/internal/testenv"
"golang.org/x/sys/unix"
)
func loadCore(t *testing.T, corePath, base, exePath string) *Process {
t.Helper()
c, err := core.Core(corePath, base, exePath)
if err != nil {
t.Fatalf("can't load test core file: %s", err)
}
p, err := Core(c)
if err != nil {
t.Fatalf("can't parse Go core: %s", err)
}
return p
}
// loadExample loads a simple core file which resulted from running the
// following program on linux/amd64 with go 1.9.0 (the earliest supported runtime):
//
// package main
//
// func main() {
// _ = *(*int)(nil)
// }
func loadExample(t *testing.T) *Process {
t.Helper()
if runtime.GOOS == "android" {
t.Skip("skipping test on android")
}
return loadCore(t, "testdata/core", "testdata", "")
}
func loadExampleVersion(t *testing.T, version string) *Process {
t.Helper()
if runtime.GOOS == "android" {
t.Skip("skipping test on android")
}
if version == "1.9" {
version = ""
}
var file string
var base string
if strings.HasSuffix(version, ".zip") {
// Make temporary directory.
dir, err := os.MkdirTemp("", strings.TrimSuffix(version, ".zip")+"_")
if err != nil {
t.Fatalf("can't make temp directory: %s", err)
}
defer os.RemoveAll(dir)
// Unpack test into directory.
unzip(t, filepath.Join("testdata", version), dir)
file = filepath.Join(dir, "tmp", "coretest", "core")
base = dir
} else {
file = fmt.Sprintf("testdata/core%s", version)
base = "testdata"
}
return loadCore(t, file, base, "")
}
// loadExampleGenerated generates a core from a binary built with
// runtime.GOROOT().
func loadExampleGenerated(t *testing.T, buildFlags ...string) *Process {
t.Helper()
testenv.MustHaveGoBuild(t)
switch runtime.GOOS {
case "js", "plan9", "windows":
t.Skipf("skipping: no core files on %s", runtime.GOOS)
}
if runtime.GOARCH != "amd64" {
t.Skipf("skipping: only parsing of amd64 cores is supported")
}
cleanup := setupCorePattern(t)
defer cleanup()
if err := adjustCoreRlimit(t); err != nil {
t.Fatalf("unable to adjust core limit, can't test generated core dump: %v", err)
}
dir := t.TempDir()
file, output, err := generateCore(dir, buildFlags...)
t.Logf("crasher output: %s", output)
if err != nil {
t.Fatalf("generateCore() got err %v want nil", err)
}
return loadCore(t, file, "", "")
}
func setupCorePattern(t *testing.T) func() {
if runtime.GOOS != "linux" {
t.Skip("skipping: core file pattern check implemented only for Linux")
}
const (
corePatternPath = "/proc/sys/kernel/core_pattern"
newPattern = "core"
)
b, err := os.ReadFile(corePatternPath)
if err != nil {
t.Fatalf("unable to read core pattern: %v", err)
}
pattern := string(b)
t.Logf("original core pattern: %s", pattern)
// We want a core file in the working directory containing "core" in
// the name. If the pattern already matches this, there is nothing to
// do. What we don't want:
// - Pipe to another process
// - Path components
if !strings.HasPrefix(pattern, "|") && !strings.Contains(pattern, "/") && strings.Contains(pattern, "core") {
// Pattern is fine as-is, nothing to do.
return func() {}
}
if os.Getenv("GO_BUILDER_NAME") == "" {
// Don't change the core pattern on arbitrary machines, as it
// has global effect.
t.Skipf("skipping: unable to generate core file due to incompatible core pattern %q; set %s to %q", pattern, corePatternPath, newPattern)
}
t.Logf("updating core pattern to %q", newPattern)
err = os.WriteFile(corePatternPath, []byte(newPattern), 0)
if err != nil {
t.Skipf("skipping: unable to write core pattern: %v", err)
}
return func() {
t.Logf("resetting core pattern to %q", pattern)
err := os.WriteFile(corePatternPath, []byte(pattern), 0)
if err != nil {
t.Errorf("unable to write core pattern back to original value: %v", err)
}
}
}
func adjustCoreRlimit(t *testing.T) error {
var limit unix.Rlimit
if err := unix.Getrlimit(unix.RLIMIT_CORE, &limit); err != nil {
return fmt.Errorf("getrlimit(RLIMIT_CORE) error: %v", err)
}
if limit.Max == 0 {
return fmt.Errorf("RLIMIT_CORE maximum is 0, core dumping is not possible")
}
// Increase the core limit to the maximum (hard limit), if the current soft
// limit is lower.
if limit.Cur < limit.Max {
oldLimit := limit
limit.Cur = limit.Max
if err := unix.Setrlimit(unix.RLIMIT_CORE, &limit); err != nil {
return fmt.Errorf("setrlimit(RLIMIT_CORE, %+v) error: %v", limit, err)
}
t.Logf("adjusted RLIMIT_CORE from %+v to %+v", oldLimit, limit)
}
return nil
}
// run spawns the supplied exe with wd as working directory.
//
// - The parent environment is amended with GOTRACEBACK=crash to provoke a
// core dump on (e.g.) segfaults.
// - Thread/process state (like resource limits) are propagated.
//
// If the binary fails to crash, an error is returned.
func run(exe, wd string) (pid int, output []byte, err error) {
cmd := exec.Command(exe)
cmd.Env = append(os.Environ(), "GOTRACEBACK=crash")
cmd.Dir = wd
var b bytes.Buffer
cmd.Stdout = &b
cmd.Stderr = &b
runtime.LockOSThread() // Propagate parent state, see [exec.Cmd.Run].
err = cmd.Run()
runtime.UnlockOSThread()
// We expect a crash.
var ee *exec.ExitError
if !errors.As(err, &ee) {
return cmd.Process.Pid, b.Bytes(), fmt.Errorf("crasher did not crash, got err %T %w", err, err)
}
return cmd.Process.Pid, b.Bytes(), nil
}
func generateCore(dir string, buildFlags ...string) (string, []byte, error) {
goTool, err := testenv.GoTool()
if err != nil {
return "", nil, fmt.Errorf("cannot find go tool: %w", err)
}
const source = "./testdata/coretest/test.go"
cwd, err := os.Getwd()
if err != nil {
return "", nil, fmt.Errorf("erroring getting cwd: %w", err)
}
srcPath := filepath.Join(cwd, source)
argv := []string{"build"}
argv = append(argv, buildFlags...)
argv = append(argv, "-o", "test.exe", srcPath)
cmd := exec.Command(goTool, argv...)
cmd.Dir = dir
b, err := cmd.CombinedOutput()
if err != nil {
return "", nil, fmt.Errorf("error building crasher: %w\n%s", err, string(b))
}
_, b, err = run("./test.exe", dir)
if err != nil {
return "", b, err
}
// Look for any file with "core" in the name.
dd, err := os.ReadDir(dir)
if err != nil {
return "", b, fmt.Errorf("error reading output directory: %w", err)
}
for _, d := range dd {
if strings.Contains(d.Name(), "core") {
return filepath.Join(dir, d.Name()), b, nil
}
}
names := make([]string, 0, len(dd))
for _, d := range dd {
names = append(names, d.Name())
}
return "", b, fmt.Errorf("did not find core file in %+v", names)
}
// unzip unpacks the zip file name into the directory dir.
func unzip(t *testing.T, name, dir string) {
t.Helper()
r, err := zip.OpenReader(name)
if err != nil {
t.Fatalf("can't read zip file %s: %s", name, err)
}
for _, f := range r.File {
rf, err := f.Open()
if err != nil {
t.Fatalf("can't read entry %s: %s", f.Name, err)
}
err = os.MkdirAll(path.Dir(filepath.Join(dir, f.Name)), 0777)
if err != nil {
t.Fatalf("can't make directory: %s", err)
}
wf, err := os.Create(filepath.Join(dir, f.Name))
if err != nil {
t.Fatalf("can't write entry %s: %s", f.Name, err)
}
_, err = io.Copy(wf, rf)
if err != nil {
t.Fatalf("can't copy %s: %s", f.Name, err)
}
err = rf.Close()
if err != nil {
t.Fatalf("can't close reader %s: %s", f.Name, err)
}
err = wf.Close()
if err != nil {
t.Fatalf("can't close writer %s: %s", f.Name, err)
}
}
}
func TestObjects(t *testing.T) {
p := loadExample(t)
n := 0
p.ForEachObject(func(x Object) bool {
n++
return true
})
if n != 104 {
t.Errorf("#objects = %d, want 104", n)
}
}
func TestRoots(t *testing.T) {
p := loadExample(t)
n := 0
p.ForEachRoot(func(r *Root) bool {
n++
return true
})
if n != 257 {
t.Errorf("#roots = %d, want 257", n)
}
}
// TestConfig checks the configuration accessors.
func TestConfig(t *testing.T) {
p := loadExample(t)
if v := p.BuildVersion(); v != "go1.9" {
t.Errorf("version=%s, wanted go1.9", v)
}
if n := p.Stats().Size; n != 2732032 {
t.Errorf("all stats=%d, want 2732032", n)
}
}
func TestFindFunc(t *testing.T) {
p := loadExample(t)
a := core.Address(0x404000)
f := p.FindFunc(a)
if f == nil {
t.Errorf("can't find function at %x", a)
return
}
if n := f.Name(); n != "runtime.recvDirect" {
t.Errorf("funcname(%x)=%s, want runtime.recvDirect", a, n)
}
}
func TestTypes(t *testing.T) {
p := loadExample(t)
// Check the type of a few objects.
for _, s := range [...]struct {
addr core.Address
size int64
kind Kind
name string
repeat int64
}{
{0xc420000480, 384, KindStruct, "runtime.g", 1},
{0xc42000a020, 32, KindPtr, "*runtime.g", 4},
{0xc420082000, 96, KindStruct, "hchan<bool>", 1},
{0xc420062000, 64, KindStruct, "runtime._defer", 1},
} {
x, i := p.FindObject(s.addr)
if x == 0 {
t.Errorf("can't find object at %x", s.addr)
continue
}
if i != 0 {
t.Errorf("offset(%x)=%d, want 0", s.addr, i)
}
if p.Size(x) != s.size {
t.Errorf("size(%x)=%d, want %d", s.addr, p.Size(x), s.size)
}
typ, repeat := p.Type(x)
if typ.Kind != s.kind {
t.Errorf("kind(%x)=%s, want %s", s.addr, typ.Kind, s.kind)
}
if typ.Name != s.name {
t.Errorf("name(%x)=%s, want %s", s.addr, typ.Name, s.name)
}
if repeat != s.repeat {
t.Errorf("repeat(%x)=%d, want %d", s.addr, repeat, s.repeat)
}
y, i := p.FindObject(s.addr + 1)
if y != x {
t.Errorf("can't find object at %x", s.addr+1)
}
if i != 1 {
t.Errorf("offset(%x)=%d, want i", s.addr, i)
}
}
}
func TestReverse(t *testing.T) {
p := loadExample(t)
// Build the pointer map.
// m[x]=y means address x has a pointer to address y.
m1 := map[core.Address]core.Address{}
p.ForEachObject(func(x Object) bool {
p.ForEachPtr(x, func(i int64, y Object, j int64) bool {
m1[p.Addr(x).Add(i)] = p.Addr(y).Add(j)
return true
})
return true
})
p.ForEachRoot(func(r *Root) bool {
p.ForEachRootPtr(r, func(i int64, y Object, j int64) bool {
m1[r.Addr.Add(i)] = p.Addr(y).Add(j)
return true
})
return true
})
// Build the same, with reverse entries.
m2 := map[core.Address]core.Address{}
p.ForEachObject(func(y Object) bool {
p.ForEachReversePtr(y, func(x Object, r *Root, i, j int64) bool {
if r != nil {
m2[r.Addr.Add(i)] = p.Addr(y).Add(j)
} else {
m2[p.Addr(x).Add(i)] = p.Addr(y).Add(j)
}
return true
})
return true
})
if !reflect.DeepEqual(m1, m2) {
t.Errorf("forward and reverse edges don't match")
}
}
func TestDynamicType(t *testing.T) {
p := loadExample(t)
for _, g := range p.Globals() {
if g.Name == "runtime.indexError" {
d := p.DynamicType(g.Type, g.Addr)
if d.Name != "runtime.errorString" {
t.Errorf("dynamic type wrong: got %s want runtime.errorString", d.Name)
}
}
}
}
// getStat returns the first (depth first) stat in the hierarchy which matches
// name, nil otherwise.
func getStat(stat *Stats, name string) *Stats {
if stat.Name == name {
return stat
}
for _, child := range stat.Children {
if found := getStat(child, name); found != nil {
return found
}
}
return nil
}
func checkProcess(t *testing.T, p *Process) {
t.Helper()
if gs := p.Goroutines(); len(gs) == 0 {
t.Error("len(p.Goroutines()) == 0, want >0")
}
const heapName = "heap"
heapStat := getStat(p.Stats(), heapName)
if heapStat == nil || heapStat.Size == 0 {
t.Errorf("stat[%q].Size == 0, want >0", heapName)
}
}
func TestVersions(t *testing.T) {
versions := []string{
"1.10",
"1.11",
"1.12.zip",
"1.13.zip",
"1.13.3.zip",
"1.14.zip",
"1.16.zip",
"1.17.zip",
"1.18.zip",
"1.19.zip",
}
for _, ver := range versions {
t.Run(ver, func(t *testing.T) {
p := loadExampleVersion(t, ver)
checkProcess(t, p)
lt := runLT(p)
if !checkDominator(t, lt) {
t.Errorf("sanityCheckDominator(...) = false, want true")
}
})
}
t.Run("goroot", func(t *testing.T) {
for _, buildFlags := range [][]string{
nil,
{"-buildmode=pie"},
} {
t.Run(strings.Join(buildFlags, ","), func(t *testing.T) {
p := loadExampleGenerated(t, buildFlags...)
checkProcess(t, p)
// TODO(aktau): Move checkDominator into checkProcess once this passes
// for loadExampleGenerated.
t.Skip(`skipping dominator check due to "panic: can't find type runtime.itab"`)
lt := runLT(p)
if !checkDominator(t, lt) {
t.Errorf("checkDominator(...) = false, want true")
}
})
}
})
}
func loadZipCore(t *testing.T, name string) *Process {
t.Helper()
if runtime.GOOS == "android" {
t.Skip("skipping test on android")
}
// Make temporary directory.
dir, err := os.MkdirTemp("", name+"_")
if err != nil {
t.Fatalf("can't make temp directory: %s", err)
}
defer os.RemoveAll(dir)
// Unpack bin file and core file into directory.
unzip(t, filepath.Join("testdata", name+".zip"), dir)
exe := filepath.Join(dir, name)
file := filepath.Join(dir, "core")
return loadCore(t, file, dir, exe)
}
func TestRuntimeTypes(t *testing.T) {
p := loadZipCore(t, "runtimetype")
// Check the type of a few objects.
for _, s := range [...]struct {
addr core.Address
size int64
kind Kind
name string
repeat int64
}{
{0xc00018e000, 16, KindStruct, "example.com/m/path-a/pkg.T1", 1},
{0xc00018e010, 16, KindStruct, "example.com/m/path-a/pkg.T2", 1},
{0xc000190000, 32, KindStruct, "example.com/m/path-b/pkg.T1", 1},
{0xc000190020, 32, KindStruct, "example.com/m/path-b/pkg.T2", 1},
} {
x, i := p.FindObject(s.addr)
if x == 0 {
t.Errorf("can't find object at %x", s.addr)
continue
}
if i != 0 {
t.Errorf("offset(%x)=%d, want 0", s.addr, i)
}
if p.Size(x) != s.size {
t.Errorf("size(%x)=%d, want %d", s.addr, p.Size(x), s.size)
}
typ, repeat := p.Type(x)
if typ.Kind != s.kind {
t.Errorf("kind(%x)=%s, want %s", s.addr, typ.Kind, s.kind)
}
if typ.Name != s.name {
t.Errorf("name(%x)=%s, want %s", s.addr, typ.Name, s.name)
}
if repeat != s.repeat {
t.Errorf("repeat(%x)=%d, want %d", s.addr, repeat, s.repeat)
}
y, i := p.FindObject(s.addr + 1)
if y != x {
t.Errorf("can't find object at %x", s.addr+1)
}
if i != 1 {
t.Errorf("offset(%x)=%d, want i", s.addr, i)
}
}
}