vulncheck/internal/binscan: use debug/buildinfo to read module data

Also adds unit tests for Binary.

Cherry-picked: https://go-review.googlesource.com/c/exp/+/388814

Change-Id: I9fc53e3121b29b53950177852014e15818f783d8
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/395061
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/vulncheck/binary_test.go b/vulncheck/binary_test.go
index ffcce1d..3ff589f 100644
--- a/vulncheck/binary_test.go
+++ b/vulncheck/binary_test.go
@@ -4,4 +4,130 @@
 
 package vulncheck
 
-// TODO(zpavlinovic): add tests.
+import (
+	"context"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	"golang.org/x/tools/go/packages/packagestest"
+)
+
+// TODO: we build binary programatically, so what if the underlying tool chain changes?
+func TestBinary(t *testing.T) {
+	e := packagestest.Export(t, packagestest.Modules, []packagestest.Module{
+		{
+			Name: "golang.org/entry",
+			Files: map[string]interface{}{
+				"main.go": `
+			package main
+
+			import (
+				"golang.org/cmod/c"
+				"golang.org/bmod/bvuln"
+			)
+
+			func main() {
+				c.C()
+				bvuln.NoVuln() // no vuln use
+				print("done")
+			}
+			`,
+			}},
+		{
+			Name: "golang.org/cmod@v1.1.3",
+			Files: map[string]interface{}{"c/c.go": `
+			package c
+
+			import (
+				"golang.org/amod/avuln"
+			)
+
+			//go:noinline
+			func C() {
+				v := avuln.VulnData{}
+				v.Vuln1() // vuln use
+			}
+			`},
+		},
+		{
+			Name: "golang.org/amod@v1.1.3",
+			Files: map[string]interface{}{"avuln/avuln.go": `
+			package avuln
+
+			type VulnData struct {}
+
+			//go:noinline
+			func (v VulnData) Vuln1() {}
+
+			//go:noinline
+			func (v VulnData) Vuln2() {}
+			`},
+		},
+		{
+			Name: "golang.org/bmod@v0.5.0",
+			Files: map[string]interface{}{"bvuln/bvuln.go": `
+			package bvuln
+
+			//go:noinline
+			func Vuln() {}
+
+			//go:noinline
+			func NoVuln() {}
+			`},
+		},
+	})
+	defer e.Cleanup()
+
+	// Make sure local vulns can be loaded.
+	fetchingInTesting = true
+
+	cmd := exec.Command("go", "build")
+	cmd.Dir = e.Config.Dir
+	cmd.Env = e.Config.Env
+	out, err := cmd.CombinedOutput()
+	if err != nil || len(out) > 0 {
+		t.Fatalf("failed to build the binary %v %v", err, string(out))
+	}
+
+	binExt := ""
+	// TODO: is there a better way to do this?
+	if runtime.GOOS == "windows" {
+		binExt = ".exe"
+	}
+
+	bin, err := os.Open(filepath.Join(e.Config.Dir, "entry"+binExt))
+	if err != nil {
+		t.Fatalf("failed to access the binary %v", err)
+	}
+	defer bin.Close()
+
+	// Test imports only mode
+	cfg := &Config{
+		Client:      testClient,
+		ImportsOnly: true,
+	}
+	res, err := Binary(context.Background(), bin, cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// In importsOnly mode, all three vulnerable symbols
+	// {avuln.VulnData.Vuln1, avuln.VulnData.Vuln2, bvuln.Vuln}
+	// should be detected.
+	if len(res.Vulns) != 3 {
+		t.Errorf("expected 3 vuln symbols; got %d", len(res.Vulns))
+	}
+
+	// Test the symbols (non-import mode)
+	cfg = &Config{Client: testClient}
+	res, err = Binary(context.Background(), bin, cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+	// In non-importsOnly mode, only one symbol avuln.VulnData.Vuln1 should be detected.
+	if len(res.Vulns) != 1 {
+		t.Errorf("expected 1 vuln symbols got %d", len(res.Vulns))
+	}
+}
diff --git a/vulncheck/internal/binscan/scan.go b/vulncheck/internal/binscan/scan.go
index 750079a..35d0fcd 100644
--- a/vulncheck/internal/binscan/scan.go
+++ b/vulncheck/internal/binscan/scan.go
@@ -10,9 +10,8 @@
 // and cmd/go/internal/version/exe.go.
 
 import (
-	"bytes"
+	"debug/buildinfo"
 	"debug/gosym"
-	"encoding/binary"
 	"errors"
 	"fmt"
 	"io"
@@ -23,153 +22,6 @@
 	"golang.org/x/tools/go/packages"
 )
 
-// buildInfoMagic, findVers, and readString are copied from
-// cmd/go/internal/version
-
-// 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).
-var buildInfoMagic = []byte("\xff Go buildinf:")
-
-// findVers finds and returns the Go version and module version information
-// in the executable x.
-func findVers(x exe) string {
-	// Read the first 64kB of text to find the build info blob.
-	text := x.DataStart()
-	data, err := x.ReadData(text, 64*1024)
-	if err != nil {
-		return ""
-	}
-	for ; !bytes.HasPrefix(data, buildInfoMagic); data = data[32:] {
-		if len(data) < 32 {
-			return ""
-		}
-	}
-
-	// Decode the blob.
-	ptrSize := int(data[14])
-	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 {
-		readPtr = bo.Uint64
-	}
-	vers := readString(x, ptrSize, readPtr, readPtr(data[16:]))
-	if vers == "" {
-		return ""
-	}
-	mod := readString(x, ptrSize, readPtr, readPtr(data[16+ptrSize:]))
-	if len(mod) >= 33 && mod[len(mod)-17] == '\n' {
-		// Strip module framing.
-		mod = mod[16 : len(mod)-16]
-	} else {
-		mod = ""
-	}
-	return mod
-}
-
-// 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)
-}
-
-// readBuildInfo is copied from runtime/debug
-func readBuildInfo(data string) (*debug.BuildInfo, bool) {
-	if len(data) == 0 {
-		return nil, false
-	}
-
-	const (
-		pathLine = "path\t"
-		modLine  = "mod\t"
-		depLine  = "dep\t"
-		repLine  = "=>\t"
-	)
-
-	readEntryFirstLine := func(elem []string) (debug.Module, bool) {
-		if len(elem) != 2 && len(elem) != 3 {
-			return debug.Module{}, false
-		}
-		sum := ""
-		if len(elem) == 3 {
-			sum = elem[2]
-		}
-		return debug.Module{
-			Path:    elem[0],
-			Version: elem[1],
-			Sum:     sum,
-		}, true
-	}
-
-	var (
-		info = &debug.BuildInfo{}
-		last *debug.Module
-		line string
-		ok   bool
-	)
-	// Reverse of cmd/go/internal/modload.PackageBuildInfo
-	for len(data) > 0 {
-		i := strings.IndexByte(data, '\n')
-		if i < 0 {
-			break
-		}
-		line, data = data[:i], data[i+1:]
-		switch {
-		case strings.HasPrefix(line, pathLine):
-			elem := line[len(pathLine):]
-			info.Path = elem
-		case strings.HasPrefix(line, modLine):
-			elem := strings.Split(line[len(modLine):], "\t")
-			last = &info.Main
-			*last, ok = readEntryFirstLine(elem)
-			if !ok {
-				return nil, false
-			}
-		case strings.HasPrefix(line, depLine):
-			elem := strings.Split(line[len(depLine):], "\t")
-			last = new(debug.Module)
-			info.Deps = append(info.Deps, last)
-			*last, ok = readEntryFirstLine(elem)
-			if !ok {
-				return nil, false
-			}
-		case strings.HasPrefix(line, repLine):
-			elem := strings.Split(line[len(repLine):], "\t")
-			if len(elem) != 3 {
-				return nil, false
-			}
-			if last == nil {
-				return nil, false
-			}
-			last.Replace = &debug.Module{
-				Path:    elem[0],
-				Version: elem[1],
-				Sum:     elem[2],
-			}
-			last = nil
-		}
-	}
-	return info, true
-}
-
 func debugModulesToPackagesModules(debugModules []*debug.Module) []*packages.Module {
 	packagesModules := make([]*packages.Module, len(debugModules))
 	for i, mod := range debugModules {
@@ -189,7 +41,14 @@
 
 // ExtractPackagesAndSymbols extracts the symbols, packages, and their associated module versions
 // from a Go binary. Stripped binaries are not supported.
+//
+// TODO(#51412): detect inlined symbols too
 func ExtractPackagesAndSymbols(bin io.ReaderAt) ([]*packages.Module, map[string][]string, error) {
+	bi, err := buildinfo.Read(bin)
+	if err != nil {
+		return nil, nil, err
+	}
+
 	x, err := openExe(bin)
 	if err != nil {
 		return nil, nil, err
@@ -235,10 +94,5 @@
 		packageSymbols[pkgName] = append(packageSymbols[pkgName], symName)
 	}
 
-	bi, ok := readBuildInfo(findVers(x))
-	if !ok {
-		return nil, nil, err
-	}
-
 	return debugModulesToPackagesModules(bi.Deps), packageSymbols, nil
 }