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
}