diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 19:23:18 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 19:23:18 +0000 |
commit | 43a123c1ae6613b3efeed291fa552ecd909d3acf (patch) | |
tree | fd92518b7024bc74031f78a1cf9e454b65e73665 /src/debug/buildinfo | |
parent | Initial commit. (diff) | |
download | golang-1.20-43a123c1ae6613b3efeed291fa552ecd909d3acf.tar.xz golang-1.20-43a123c1ae6613b3efeed291fa552ecd909d3acf.zip |
Adding upstream version 1.20.14.upstream/1.20.14upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/debug/buildinfo')
-rw-r--r-- | src/debug/buildinfo/buildinfo.go | 451 | ||||
-rw-r--r-- | src/debug/buildinfo/buildinfo_test.go | 329 |
2 files changed, 780 insertions, 0 deletions
diff --git a/src/debug/buildinfo/buildinfo.go b/src/debug/buildinfo/buildinfo.go new file mode 100644 index 0000000..8bc5753 --- /dev/null +++ b/src/debug/buildinfo/buildinfo.go @@ -0,0 +1,451 @@ +// 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 provides access to information embedded in a Go binary +// about how it was built. This includes the Go toolchain version, and the +// set of modules used (for binaries built in module mode). +// +// Build information is available for the currently running binary in +// runtime/debug.ReadBuildInfo. +package buildinfo + +import ( + "bytes" + "debug/elf" + "debug/macho" + "debug/pe" + "debug/plan9obj" + "encoding/binary" + "errors" + "fmt" + "internal/xcoff" + "io" + "io/fs" + "os" + "runtime/debug" +) + +// Type alias for build info. We cannot move the types here, since +// runtime/debug would need to import this package, which would make it +// a much larger dependency. +type BuildInfo = debug.BuildInfo + +var ( + // errUnrecognizedFormat is returned when a given executable file doesn't + // appear to be in a known format, or it breaks the rules of that format, + // or when there are I/O errors reading the file. + errUnrecognizedFormat = errors.New("unrecognized file format") + + // errNotGoExe is returned when a given executable file is valid but does + // not contain Go build information. + errNotGoExe = errors.New("not a Go executable") + + // The build info blob left by the linker is identified by + // a 16-byte header, consisting of buildInfoMagic (14 bytes), + // the binary's pointer size (1 byte), + // and whether the binary is big endian (1 byte). + buildInfoMagic = []byte("\xff Go buildinf:") +) + +// ReadFile returns build information embedded in a Go binary +// file at the given path. Most information is only available for binaries built +// with module support. +func ReadFile(name string) (info *BuildInfo, err error) { + defer func() { + if pathErr := (*fs.PathError)(nil); errors.As(err, &pathErr) { + err = fmt.Errorf("could not read Go build info: %w", err) + } else if err != nil { + err = fmt.Errorf("could not read Go build info from %s: %w", name, err) + } + }() + + f, err := os.Open(name) + if err != nil { + return nil, err + } + defer f.Close() + return Read(f) +} + +// Read returns build information embedded in a Go binary file +// accessed through the given ReaderAt. Most information is only available for +// binaries built with module support. +func Read(r io.ReaderAt) (*BuildInfo, error) { + vers, mod, err := readRawBuildInfo(r) + if err != nil { + return nil, err + } + bi, err := debug.ParseBuildInfo(mod) + if err != nil { + return nil, err + } + bi.GoVersion = vers + return bi, nil +} + +type exe interface { + // ReadData reads and returns up to size bytes starting at virtual address addr. + ReadData(addr, size uint64) ([]byte, error) + + // DataStart returns the virtual address of the segment or section that + // should contain build information. This is either a specially named section + // or the first writable non-zero data segment. + DataStart() uint64 +} + +// readRawBuildInfo extracts the Go toolchain version and module information +// strings from a Go binary. On success, vers should be non-empty. mod +// is empty if the binary was not built with modules enabled. +func readRawBuildInfo(r io.ReaderAt) (vers, mod string, err error) { + // Read the first bytes of the file to identify the format, then delegate to + // a format-specific function to load segment and section headers. + ident := make([]byte, 16) + if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil { + return "", "", errUnrecognizedFormat + } + + var x exe + switch { + case bytes.HasPrefix(ident, []byte("\x7FELF")): + f, err := elf.NewFile(r) + if err != nil { + return "", "", errUnrecognizedFormat + } + x = &elfExe{f} + case bytes.HasPrefix(ident, []byte("MZ")): + f, err := pe.NewFile(r) + if err != nil { + return "", "", errUnrecognizedFormat + } + x = &peExe{f} + case bytes.HasPrefix(ident, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(ident[1:], []byte("\xFA\xED\xFE")): + f, err := macho.NewFile(r) + if err != nil { + return "", "", errUnrecognizedFormat + } + x = &machoExe{f} + case bytes.HasPrefix(ident, []byte{0x01, 0xDF}) || bytes.HasPrefix(ident, []byte{0x01, 0xF7}): + f, err := xcoff.NewFile(r) + if err != nil { + return "", "", errUnrecognizedFormat + } + x = &xcoffExe{f} + case hasPlan9Magic(ident): + f, err := plan9obj.NewFile(r) + if err != nil { + return "", "", errUnrecognizedFormat + } + x = &plan9objExe{f} + default: + return "", "", errUnrecognizedFormat + } + + // Read the first 64kB of dataAddr to find the build info blob. + // On some platforms, the blob will be in its own section, and DataStart + // returns the address of that section. On others, it's somewhere in the + // data segment; the linker puts it near the beginning. + // See cmd/link/internal/ld.Link.buildinfo. + dataAddr := x.DataStart() + data, err := x.ReadData(dataAddr, 64*1024) + if err != nil { + return "", "", err + } + const ( + buildInfoAlign = 16 + buildInfoSize = 32 + ) + for { + i := bytes.Index(data, buildInfoMagic) + if i < 0 || len(data)-i < buildInfoSize { + return "", "", errNotGoExe + } + if i%buildInfoAlign == 0 && len(data)-i >= buildInfoSize { + data = data[i:] + break + } + data = data[(i+buildInfoAlign-1)&^(buildInfoAlign-1):] + } + + // Decode the blob. + // The first 14 bytes are buildInfoMagic. + // The next two bytes indicate pointer size in bytes (4 or 8) and endianness + // (0 for little, 1 for big). + // Two virtual addresses to Go strings follow that: runtime.buildVersion, + // and runtime.modinfo. + // On 32-bit platforms, the last 8 bytes are unused. + // If the endianness has the 2 bit set, then the pointers are zero + // and the 32-byte header is followed by varint-prefixed string data + // for the two string values we care about. + ptrSize := int(data[14]) + if data[15]&2 != 0 { + vers, data = decodeString(data[32:]) + mod, data = decodeString(data) + } else { + bigEndian := data[15] != 0 + var bo binary.ByteOrder + if bigEndian { + bo = binary.BigEndian + } else { + bo = binary.LittleEndian + } + var readPtr func([]byte) uint64 + if ptrSize == 4 { + readPtr = func(b []byte) uint64 { return uint64(bo.Uint32(b)) } + } else if ptrSize == 8 { + readPtr = bo.Uint64 + } else { + return "", "", errNotGoExe + } + vers = readString(x, ptrSize, readPtr, readPtr(data[16:])) + mod = readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:])) + } + if vers == "" { + return "", "", errNotGoExe + } + if len(mod) >= 33 && mod[len(mod)-17] == '\n' { + // Strip module framing: sentinel strings delimiting the module info. + // These are cmd/go/internal/modload.infoStart and infoEnd. + mod = mod[16 : len(mod)-16] + } else { + mod = "" + } + + return vers, mod, nil +} + +func hasPlan9Magic(magic []byte) bool { + if len(magic) >= 4 { + m := binary.BigEndian.Uint32(magic) + switch m { + case plan9obj.Magic386, plan9obj.MagicAMD64, plan9obj.MagicARM: + return true + } + } + return false +} + +func decodeString(data []byte) (s string, rest []byte) { + u, n := binary.Uvarint(data) + if n <= 0 || u >= uint64(len(data)-n) { + return "", nil + } + return string(data[n : uint64(n)+u]), data[uint64(n)+u:] +} + +// readString returns the string at address addr in the executable x. +func readString(x exe, ptrSize int, readPtr func([]byte) uint64, addr uint64) string { + hdr, err := x.ReadData(addr, uint64(2*ptrSize)) + if err != nil || len(hdr) < 2*ptrSize { + return "" + } + dataAddr := readPtr(hdr) + dataLen := readPtr(hdr[ptrSize:]) + data, err := x.ReadData(dataAddr, dataLen) + if err != nil || uint64(len(data)) < dataLen { + return "" + } + return string(data) +} + +// elfExe is the ELF implementation of the exe interface. +type elfExe struct { + f *elf.File +} + +func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) { + for _, prog := range x.f.Progs { + if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 { + n := prog.Vaddr + prog.Filesz - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := prog.ReadAt(data, int64(addr-prog.Vaddr)) + if err != nil { + return nil, err + } + return data, nil + } + } + return nil, errUnrecognizedFormat +} + +func (x *elfExe) DataStart() uint64 { + for _, s := range x.f.Sections { + if s.Name == ".go.buildinfo" { + return s.Addr + } + } + for _, p := range x.f.Progs { + if p.Type == elf.PT_LOAD && p.Flags&(elf.PF_X|elf.PF_W) == elf.PF_W { + return p.Vaddr + } + } + return 0 +} + +// peExe is the PE (Windows Portable Executable) implementation of the exe interface. +type peExe struct { + f *pe.File +} + +func (x *peExe) imageBase() uint64 { + switch oh := x.f.OptionalHeader.(type) { + case *pe.OptionalHeader32: + return uint64(oh.ImageBase) + case *pe.OptionalHeader64: + return oh.ImageBase + } + return 0 +} + +func (x *peExe) ReadData(addr, size uint64) ([]byte, error) { + addr -= x.imageBase() + for _, sect := range x.f.Sections { + if uint64(sect.VirtualAddress) <= addr && addr <= uint64(sect.VirtualAddress+sect.Size-1) { + n := uint64(sect.VirtualAddress+sect.Size) - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := sect.ReadAt(data, int64(addr-uint64(sect.VirtualAddress))) + if err != nil { + return nil, errUnrecognizedFormat + } + return data, nil + } + } + return nil, errUnrecognizedFormat +} + +func (x *peExe) DataStart() uint64 { + // Assume data is first writable section. + const ( + IMAGE_SCN_CNT_CODE = 0x00000020 + IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040 + IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080 + IMAGE_SCN_MEM_EXECUTE = 0x20000000 + IMAGE_SCN_MEM_READ = 0x40000000 + IMAGE_SCN_MEM_WRITE = 0x80000000 + IMAGE_SCN_MEM_DISCARDABLE = 0x2000000 + IMAGE_SCN_LNK_NRELOC_OVFL = 0x1000000 + IMAGE_SCN_ALIGN_32BYTES = 0x600000 + ) + for _, sect := range x.f.Sections { + if sect.VirtualAddress != 0 && sect.Size != 0 && + sect.Characteristics&^IMAGE_SCN_ALIGN_32BYTES == IMAGE_SCN_CNT_INITIALIZED_DATA|IMAGE_SCN_MEM_READ|IMAGE_SCN_MEM_WRITE { + return uint64(sect.VirtualAddress) + x.imageBase() + } + } + return 0 +} + +// machoExe is the Mach-O (Apple macOS/iOS) implementation of the exe interface. +type machoExe struct { + f *macho.File +} + +func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) { + for _, load := range x.f.Loads { + seg, ok := load.(*macho.Segment) + if !ok { + continue + } + if seg.Addr <= addr && addr <= seg.Addr+seg.Filesz-1 { + if seg.Name == "__PAGEZERO" { + continue + } + n := seg.Addr + seg.Filesz - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := seg.ReadAt(data, int64(addr-seg.Addr)) + if err != nil { + return nil, err + } + return data, nil + } + } + return nil, errUnrecognizedFormat +} + +func (x *machoExe) DataStart() uint64 { + // Look for section named "__go_buildinfo". + for _, sec := range x.f.Sections { + if sec.Name == "__go_buildinfo" { + return sec.Addr + } + } + // Try the first non-empty writable segment. + const RW = 3 + for _, load := range x.f.Loads { + seg, ok := load.(*macho.Segment) + if ok && seg.Addr != 0 && seg.Filesz != 0 && seg.Prot == RW && seg.Maxprot == RW { + return seg.Addr + } + } + return 0 +} + +// xcoffExe is the XCOFF (AIX eXtended COFF) implementation of the exe interface. +type xcoffExe struct { + f *xcoff.File +} + +func (x *xcoffExe) ReadData(addr, size uint64) ([]byte, error) { + for _, sect := range x.f.Sections { + if sect.VirtualAddress <= addr && addr <= sect.VirtualAddress+sect.Size-1 { + n := sect.VirtualAddress + sect.Size - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := sect.ReadAt(data, int64(addr-sect.VirtualAddress)) + if err != nil { + return nil, err + } + return data, nil + } + } + return nil, errors.New("address not mapped") +} + +func (x *xcoffExe) DataStart() uint64 { + if s := x.f.SectionByType(xcoff.STYP_DATA); s != nil { + return s.VirtualAddress + } + return 0 +} + +// plan9objExe is the Plan 9 a.out implementation of the exe interface. +type plan9objExe struct { + f *plan9obj.File +} + +func (x *plan9objExe) DataStart() uint64 { + if s := x.f.Section("data"); s != nil { + return uint64(s.Offset) + } + return 0 +} + +func (x *plan9objExe) ReadData(addr, size uint64) ([]byte, error) { + for _, sect := range x.f.Sections { + if uint64(sect.Offset) <= addr && addr <= uint64(sect.Offset+sect.Size-1) { + n := uint64(sect.Offset+sect.Size) - addr + if n > size { + n = size + } + data := make([]byte, n) + _, err := sect.ReadAt(data, int64(addr-uint64(sect.Offset))) + if err != nil { + return nil, err + } + return data, nil + } + } + return nil, errors.New("address not mapped") + +} 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) + } + }) + } +} |