diff options
Diffstat (limited to 'src/debug/buildinfo/buildinfo_test.go')
-rw-r--r-- | src/debug/buildinfo/buildinfo_test.go | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/src/debug/buildinfo/buildinfo_test.go b/src/debug/buildinfo/buildinfo_test.go new file mode 100644 index 0000000..290e370 --- /dev/null +++ b/src/debug/buildinfo/buildinfo_test.go @@ -0,0 +1,329 @@ +// Copyright 2021 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. + +package buildinfo_test + +import ( + "bytes" + "debug/buildinfo" + "debug/pe" + "encoding/binary" + "flag" + "fmt" + "internal/testenv" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "runtime" + "strings" + "testing" +) + +var flagAll = flag.Bool("all", false, "test all supported GOOS/GOARCH platforms, instead of only the current platform") + +// TestReadFile confirms that ReadFile can read build information from binaries +// on supported target platforms. It builds a trivial binary on the current +// platforms (or all platforms if -all is set) in various configurations and +// checks that build information can or cannot be read. +func TestReadFile(t *testing.T) { + if testing.Short() { + t.Skip("test requires compiling and linking, which may be slow") + } + testenv.MustHaveGoBuild(t) + + type platform struct{ goos, goarch string } + platforms := []platform{ + {"aix", "ppc64"}, + {"darwin", "amd64"}, + {"darwin", "arm64"}, + {"linux", "386"}, + {"linux", "amd64"}, + {"windows", "386"}, + {"windows", "amd64"}, + } + runtimePlatform := platform{runtime.GOOS, runtime.GOARCH} + haveRuntimePlatform := false + for _, p := range platforms { + if p == runtimePlatform { + haveRuntimePlatform = true + break + } + } + if !haveRuntimePlatform { + platforms = append(platforms, runtimePlatform) + } + + buildModes := []string{"pie", "exe"} + if testenv.HasCGO() { + buildModes = append(buildModes, "c-shared") + } + + // Keep in sync with src/cmd/go/internal/work/init.go:buildModeInit. + badmode := func(goos, goarch, buildmode string) string { + return fmt.Sprintf("-buildmode=%s not supported on %s/%s", buildmode, goos, goarch) + } + + buildWithModules := func(t *testing.T, goos, goarch, buildmode string) string { + dir := t.TempDir() + gomodPath := filepath.Join(dir, "go.mod") + gomodData := []byte("module example.com/m\ngo 1.18\n") + if err := os.WriteFile(gomodPath, gomodData, 0666); err != nil { + t.Fatal(err) + } + helloPath := filepath.Join(dir, "hello.go") + helloData := []byte("package main\nfunc main() {}\n") + if err := os.WriteFile(helloPath, helloData, 0666); err != nil { + t.Fatal(err) + } + outPath := filepath.Join(dir, path.Base(t.Name())) + cmd := exec.Command(testenv.GoToolPath(t), "build", "-o="+outPath, "-buildmode="+buildmode) + cmd.Dir = dir + cmd.Env = append(os.Environ(), "GO111MODULE=on", "GOOS="+goos, "GOARCH="+goarch) + stderr := &strings.Builder{} + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + if badmodeMsg := badmode(goos, goarch, buildmode); strings.Contains(stderr.String(), badmodeMsg) { + t.Skip(badmodeMsg) + } + t.Fatalf("failed building test file: %v\n%s", err, stderr.String()) + } + return outPath + } + + buildWithGOPATH := func(t *testing.T, goos, goarch, buildmode string) string { + gopathDir := t.TempDir() + pkgDir := filepath.Join(gopathDir, "src/example.com/m") + if err := os.MkdirAll(pkgDir, 0777); err != nil { + t.Fatal(err) + } + helloPath := filepath.Join(pkgDir, "hello.go") + helloData := []byte("package main\nfunc main() {}\n") + if err := os.WriteFile(helloPath, helloData, 0666); err != nil { + t.Fatal(err) + } + outPath := filepath.Join(gopathDir, path.Base(t.Name())) + cmd := exec.Command(testenv.GoToolPath(t), "build", "-o="+outPath, "-buildmode="+buildmode) + cmd.Dir = pkgDir + cmd.Env = append(os.Environ(), "GO111MODULE=off", "GOPATH="+gopathDir, "GOOS="+goos, "GOARCH="+goarch) + stderr := &strings.Builder{} + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + if badmodeMsg := badmode(goos, goarch, buildmode); strings.Contains(stderr.String(), badmodeMsg) { + t.Skip(badmodeMsg) + } + t.Fatalf("failed building test file: %v\n%s", err, stderr.String()) + } + return outPath + } + + damageBuildInfo := func(t *testing.T, name string) { + data, err := os.ReadFile(name) + if err != nil { + t.Fatal(err) + } + i := bytes.Index(data, []byte("\xff Go buildinf:")) + if i < 0 { + t.Fatal("Go buildinf not found") + } + data[i+2] = 'N' + if err := os.WriteFile(name, data, 0666); err != nil { + t.Fatal(err) + } + } + + goVersionRe := regexp.MustCompile("(?m)^go\t.*\n") + buildRe := regexp.MustCompile("(?m)^build\t.*\n") + cleanOutputForComparison := func(got string) string { + // Remove or replace anything that might depend on the test's environment + // so we can check the output afterward with a string comparison. + // We'll remove all build lines except the compiler, just to make sure + // build lines are included. + got = goVersionRe.ReplaceAllString(got, "go\tGOVERSION\n") + got = buildRe.ReplaceAllStringFunc(got, func(match string) string { + if strings.HasPrefix(match, "build\t-compiler=") { + return match + } + return "" + }) + return got + } + + cases := []struct { + name string + build func(t *testing.T, goos, goarch, buildmode string) string + want string + wantErr string + }{ + { + name: "doesnotexist", + build: func(t *testing.T, goos, goarch, buildmode string) string { + return "doesnotexist.txt" + }, + wantErr: "doesnotexist", + }, + { + name: "empty", + build: func(t *testing.T, _, _, _ string) string { + dir := t.TempDir() + name := filepath.Join(dir, "empty") + if err := os.WriteFile(name, nil, 0666); err != nil { + t.Fatal(err) + } + return name + }, + wantErr: "unrecognized file format", + }, + { + name: "valid_modules", + build: buildWithModules, + want: "go\tGOVERSION\n" + + "path\texample.com/m\n" + + "mod\texample.com/m\t(devel)\t\n" + + "build\t-compiler=gc\n", + }, + { + name: "invalid_modules", + build: func(t *testing.T, goos, goarch, buildmode string) string { + name := buildWithModules(t, goos, goarch, buildmode) + damageBuildInfo(t, name) + return name + }, + wantErr: "not a Go executable", + }, + { + name: "valid_gopath", + build: buildWithGOPATH, + want: "go\tGOVERSION\n" + + "path\texample.com/m\n" + + "build\t-compiler=gc\n", + }, + { + name: "invalid_gopath", + build: func(t *testing.T, goos, goarch, buildmode string) string { + name := buildWithGOPATH(t, goos, goarch, buildmode) + damageBuildInfo(t, name) + return name + }, + wantErr: "not a Go executable", + }, + } + + for _, p := range platforms { + p := p + t.Run(p.goos+"_"+p.goarch, func(t *testing.T) { + if p != runtimePlatform && !*flagAll { + t.Skipf("skipping platforms other than %s_%s because -all was not set", runtimePlatform.goos, runtimePlatform.goarch) + } + for _, mode := range buildModes { + mode := mode + t.Run(mode, func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + name := tc.build(t, p.goos, p.goarch, mode) + if info, err := buildinfo.ReadFile(name); err != nil { + if tc.wantErr == "" { + t.Fatalf("unexpected error: %v", err) + } else if errMsg := err.Error(); !strings.Contains(errMsg, tc.wantErr) { + t.Fatalf("got error %q; want error containing %q", errMsg, tc.wantErr) + } + } else { + if tc.wantErr != "" { + t.Fatalf("unexpected success; want error containing %q", tc.wantErr) + } + got := info.String() + if clean := cleanOutputForComparison(string(got)); got != tc.want && clean != tc.want { + t.Fatalf("got:\n%s\nwant:\n%s", got, tc.want) + } + } + }) + } + }) + } + }) + } +} + +// FuzzIssue57002 is a regression test for golang.org/issue/57002. +// +// The cause of issue 57002 is when pointerSize is not being checked, +// the read can panic with slice bounds out of range +func FuzzIssue57002(f *testing.F) { + // input from issue + f.Add([]byte{0x4d, 0x5a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x50, 0x45, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x20, 0x20, 0x20, 0x20, 0x0, 0x0, 0x0, 0x0, 0x20, 0x3f, 0x0, 0x20, 0x0, 0x0, 0x20, 0x20, 0x20, 0x20, 0x20, 0xff, 0x20, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xb, 0x20, 0x20, 0x20, 0xfc, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x9, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, 0x20, 0x20, 0x20, 0x20, 0x20, 0xef, 0x20, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf, 0x0, 0x2, 0x0, 0x20, 0x0, 0x0, 0x9, 0x0, 0x4, 0x0, 0x20, 0xf6, 0x0, 0xd3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x1, 0x0, 0x0, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xa, 0x20, 0xa, 0x20, 0x20, 0x20, 0xff, 0x20, 0x20, 0xff, 0x20, 0x47, 0x6f, 0x20, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x69, 0x6e, 0x66, 0x3a, 0xde, 0xb5, 0xdf, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x6, 0x7f, 0x7f, 0x7f, 0x20, 0xf4, 0xb2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0xb, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x20, 0x0, 0x0, 0x0, 0x0, 0x5, 0x0, 0x20, 0x20, 0x20, 0x20, 0x0, 0x0, 0x0, 0x0, 0x20, 0x3f, 0x27, 0x20, 0x0, 0xd, 0x0, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0xff, 0x20, 0x20, 0xff, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x0, 0x20, 0x20, 0x0, 0x0, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5c, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20}) + f.Fuzz(func(t *testing.T, input []byte) { + buildinfo.Read(bytes.NewReader(input)) + }) +} + +// TestIssue54968 is a regression test for golang.org/issue/54968. +// +// The cause of issue 54968 is when the first buildInfoMagic is invalid, it +// enters an infinite loop. +func TestIssue54968(t *testing.T) { + t.Parallel() + + const ( + paddingSize = 200 + buildInfoAlign = 16 + ) + buildInfoMagic := []byte("\xff Go buildinf:") + + // Construct a valid PE header. + var buf bytes.Buffer + + buf.Write([]byte{'M', 'Z'}) + buf.Write(bytes.Repeat([]byte{0}, 0x3c-2)) + // At location 0x3c, the stub has the file offset to the PE signature. + binary.Write(&buf, binary.LittleEndian, int32(0x3c+4)) + + buf.Write([]byte{'P', 'E', 0, 0}) + + binary.Write(&buf, binary.LittleEndian, pe.FileHeader{NumberOfSections: 1}) + + sh := pe.SectionHeader32{ + Name: [8]uint8{'t', 0}, + SizeOfRawData: uint32(paddingSize + len(buildInfoMagic)), + PointerToRawData: uint32(buf.Len()), + } + sh.PointerToRawData = uint32(buf.Len() + binary.Size(sh)) + + binary.Write(&buf, binary.LittleEndian, sh) + + start := buf.Len() + buf.Write(bytes.Repeat([]byte{0}, paddingSize+len(buildInfoMagic))) + data := buf.Bytes() + + if _, err := pe.NewFile(bytes.NewReader(data)); err != nil { + t.Fatalf("need a valid PE header for the misaligned buildInfoMagic test: %s", err) + } + + // Place buildInfoMagic after the header. + for i := 1; i < paddingSize-len(buildInfoMagic); i++ { + // Test only misaligned buildInfoMagic. + if i%buildInfoAlign == 0 { + continue + } + + t.Run(fmt.Sprintf("start_at_%d", i), func(t *testing.T) { + d := data[:start] + // Construct intentionally-misaligned buildInfoMagic. + d = append(d, bytes.Repeat([]byte{0}, i)...) + d = append(d, buildInfoMagic...) + d = append(d, bytes.Repeat([]byte{0}, paddingSize-i)...) + + _, err := buildinfo.Read(bytes.NewReader(d)) + + wantErr := "not a Go executable" + if err == nil { + t.Errorf("got error nil; want error containing %q", wantErr) + } else if errMsg := err.Error(); !strings.Contains(errMsg, wantErr) { + t.Errorf("got error %q; want error containing %q", errMsg, wantErr) + } + }) + } +} |