| // 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) |
| } |
| } |
| } |