internal/gocore: add support and test for tip

The new test generates a core file on-the-fly using the test GOROOT,
which enables immediate test coverage of changes to the toolchain.

Internals of the runtime may change during the development cycle. The
intention is for gocore to only support the behavior of the final
release. Thus no backwards compatibilty need be maintained during
development.

Change-Id: I95b09130da58467e8a8691eb0054880f12edf353
Reviewed-on: https://go-review.googlesource.com/c/debug/+/426014
Auto-Submit: Michael Pratt <mpratt@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
Run-TryBot: Michael Pratt <mpratt@google.com>
Reviewed-by: Keith Randall <khr@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/gocore/gocore_test.go b/internal/gocore/gocore_test.go
index f72c5fd..e9a41b8 100644
--- a/internal/gocore/gocore_test.go
+++ b/internal/gocore/gocore_test.go
@@ -9,10 +9,12 @@
 
 import (
 	"archive/zip"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
+	"os/exec"
 	"path"
 	"path/filepath"
 	"reflect"
@@ -21,6 +23,7 @@
 	"testing"
 
 	"golang.org/x/debug/internal/core"
+	"golang.org/x/debug/internal/testenv"
 )
 
 // loadTest loads a simple core file which resulted from running the
@@ -85,6 +88,139 @@
 	return p
 }
 
+// loadExampleGenerated generates a core from a binary built with
+// runtime.GOROOT().
+func loadExampleGenerated(t *testing.T) *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()
+
+	dir := t.TempDir()
+	file, output, err := generateCore(dir)
+	t.Logf("crasher output: %s", output)
+	if err != nil {
+		t.Fatalf("generateCore() got err %v want nil", err)
+	}
+	c, err := core.Core(file, "", "")
+	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
+}
+
+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 generateCore(dir 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)
+	cmd := exec.Command(goTool, "build", "-o", "test.exe", srcPath)
+	cmd.Dir = dir
+
+	b, err := cmd.CombinedOutput()
+	if err != nil {
+		return "", nil, fmt.Errorf("error building crasher: %w\n%s", err, string(b))
+	}
+
+	cmd = exec.Command("./test.exe")
+	cmd.Env = append(os.Environ(), "GOTRACEBACK=crash")
+	cmd.Dir = dir
+
+	b, err = cmd.CombinedOutput()
+	// We expect a crash.
+	var ee *exec.ExitError
+	if !errors.As(err, &ee) {
+		return "", b, fmt.Errorf("crasher did not crash, got err %T %w", err, 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()
@@ -268,16 +404,27 @@
 }
 
 func TestVersions(t *testing.T) {
-	loadExampleVersion(t, "1.10")
-	loadExampleVersion(t, "1.11")
-	loadExampleVersion(t, "1.12.zip")
-	loadExampleVersion(t, "1.13.zip")
-	loadExampleVersion(t, "1.13.3.zip")
-	loadExampleVersion(t, "1.14.zip")
-	loadExampleVersion(t, "1.16.zip")
-	loadExampleVersion(t, "1.17.zip")
-	loadExampleVersion(t, "1.18.zip")
-	loadExampleVersion(t, "1.19.zip")
+	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) {
+			loadExampleVersion(t, ver)
+		})
+	}
+
+	t.Run("goroot", func(t *testing.T) {
+		loadExampleGenerated(t)
+	})
 }
 
 func loadZipCore(t *testing.T, name string) *Process {
diff --git a/internal/gocore/module.go b/internal/gocore/module.go
index 5c22b63..4f443c7 100644
--- a/internal/gocore/module.go
+++ b/internal/gocore/module.go
@@ -82,13 +82,22 @@
 // pcln must have type []byte and represent the module's pcln table region.
 func (m *module) readFunc(r region, pctab region, funcnametab region) *Func {
 	f := &Func{module: m, r: r}
-	if r.HasField("entryoff") {
+	var nameOff int32
+	switch {
+	case r.HasField("entryOff"):
+		f.entry = m.textAddr(r.Field("entryOff").Uint32())
+		nameOff = r.Field("nameOff").Int32()
+	case r.HasField("entryoff"):
+		// Prior to 1.20, entryOff and nameOff were named entryoff and
+		// nameoff, respectively.
 		f.entry = m.textAddr(r.Field("entryoff").Uint32())
-	} else {
+		nameOff = r.Field("nameoff").Int32()
+	default:
 		// Prior to 1.18, _func.entry directly referenced the entries.
 		f.entry = core.Address(r.Field("entry").Uintptr())
+		nameOff = r.Field("nameoff").Int32()
 	}
-	f.name = r.p.proc.ReadCString(funcnametab.SliceIndex(int64(r.Field("nameoff").Int32())).a)
+	f.name = r.p.proc.ReadCString(funcnametab.SliceIndex(int64(nameOff)).a)
 	pcsp := r.Field("pcsp")
 	var pcspIdx int64
 	if pcsp.typ.Kind == KindUint {
diff --git a/internal/gocore/process.go b/internal/gocore/process.go
index 6cb8554..4df8008 100644
--- a/internal/gocore/process.go
+++ b/internal/gocore/process.go
@@ -118,7 +118,7 @@
 func Core(proc *core.Process) (p *Process, err error) {
 	// Make sure we have DWARF info.
 	if _, err := proc.DWARF(); err != nil {
-		return nil, err
+		return nil, fmt.Errorf("error reading dwarf: %w", err)
 	}
 
 	// Guard against failures of proc.Read* routines.
@@ -259,6 +259,7 @@
 				min := core.Address(arenaSize*(level2+level1*level2size) - arenaBaseOffset)
 				max := min.Add(arenaSize)
 				bitmap := a.Field("bitmap")
+				oneBitBitmap := a.HasField("noMorePtrs") // Starting in 1.20.
 				spans := a.Field("spans")
 
 				arenas = append(arenas, arena{
@@ -273,16 +274,28 @@
 				// Copy out ptr/nonptr bits
 				n := bitmap.ArrayLen()
 				for i := int64(0); i < n; i++ {
-					// The nth byte is composed of 4 object bits and 4 live/dead
-					// bits. We ignore the 4 live/dead bits, which are on the
-					// high order side of the byte.
-					//
-					// See mbitmap.go for more information on the format of
-					// the bitmap field of heapArena.
-					m := bitmap.ArrayIndex(i).Uint8()
-					for j := int64(0); j < 4; j++ {
-						if m>>uint(j)&1 != 0 {
-							p.setHeapPtr(min.Add((i*4 + j) * ptrSize))
+					if oneBitBitmap {
+						// The array uses 1 bit per word of heap. See mbitmap.go for
+						// more information.
+						m := bitmap.ArrayIndex(i).Uintptr()
+						bits := 8 * ptrSize
+						for j := int64(0); j < bits; j++ {
+							if m>>uint(j)&1 != 0 {
+								p.setHeapPtr(min.Add((i*bits + j) * ptrSize))
+							}
+						}
+					} else {
+						// The nth byte is composed of 4 object bits and 4 live/dead
+						// bits. We ignore the 4 live/dead bits, which are on the
+						// high order side of the byte.
+						//
+						// See mbitmap.go for more information on the format of
+						// the bitmap field of heapArena.
+						m := bitmap.ArrayIndex(i).Uint8()
+						for j := int64(0); j < 4; j++ {
+							if m>>uint(j)&1 != 0 {
+								p.setHeapPtr(min.Add((i*4 + j) * ptrSize))
+							}
 						}
 					}
 				}
@@ -392,6 +405,9 @@
 		if st.IsStruct() && st.HasField("s") { // go1.14+
 			st = st.Field("s")
 		}
+		if st.IsStruct() && st.HasField("value") { // go1.20+
+			st = st.Field("value")
+		}
 		switch st.Uint8() {
 		case spanInUse:
 			inUseSpanSize += spanSize
@@ -596,7 +612,11 @@
 			}
 		}
 	}
-	status := r.Field("atomicstatus").Uint32()
+	st := r.Field("atomicstatus")
+	if st.IsStruct() && st.HasField("value") { // go1.20+
+		st = st.Field("value")
+	}
+	status := st.Uint32()
 	status &^= uint32(p.rtConstants["_Gscan"])
 	var sp, pc core.Address
 	switch status {
diff --git a/internal/gocore/testdata/README b/internal/gocore/testdata/README
index fe2de04..7fa4d21 100644
--- a/internal/gocore/testdata/README
+++ b/internal/gocore/testdata/README
@@ -20,11 +20,10 @@
 steps for subsequent versions:
 
 mkdir /tmp/coretest
+rm -fr /tmp/coretest/*   # in case there's old junk there
+cp coretest/test.go /tmp/coretest
 cd /tmp/coretest
-rm -fr *   // in case there's old junk there
-cat the above program into test.go
 go build test.go
-ulimit -c unlimited
 GOTRACEBACK=crash ./test
 zip 1.12.zip /tmp/coretest/core /tmp/coretest/test  // use your version number
 
diff --git a/internal/gocore/testdata/coretest/test.go b/internal/gocore/testdata/coretest/test.go
new file mode 100644
index 0000000..7f3755c
--- /dev/null
+++ b/internal/gocore/testdata/coretest/test.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+	"fmt"
+	"syscall"
+)
+
+func main() {
+	inf := int64(syscall.RLIM_INFINITY)
+	lim := syscall.Rlimit{
+		Cur: uint64(inf),
+		Max: uint64(inf),
+	}
+	if err := syscall.Setrlimit(syscall.RLIMIT_CORE, &lim); err != nil {
+		panic(fmt.Sprintf("error setting rlimit: %v", err))
+	}
+
+	_ = *(*int)(nil)
+}