Skip to content

Commit 8fa5789

Browse files
committed
internal/vulncheck: add binary abstraction data structure
This is minimal information needed for govulncheck to analyze binaries. This will be jsonized into the follow-up CLs. Change-Id: I52f71ad66ea7e5004d4ced388c488eff5e686fd6 Reviewed-on: https://go-review.googlesource.com/c/vuln/+/540235 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Cottrell <iancottrell@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com>
1 parent fcf7dff commit 8fa5789

File tree

5 files changed

+112
-51
lines changed

5 files changed

+112
-51
lines changed

internal/vulncheck/binary.go

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import (
1212
"fmt"
1313
"io"
1414
"runtime/debug"
15+
"sort"
1516

17+
"golang.org/x/tools/go/packages"
1618
"golang.org/x/vuln/internal"
1719
"golang.org/x/vuln/internal/client"
1820
"golang.org/x/vuln/internal/govulncheck"
@@ -23,24 +25,50 @@ import (
2325
// Binary detects presence of vulnerable symbols in exe and
2426
// emits findings to exe.
2527
func Binary(ctx context.Context, handler govulncheck.Handler, exe io.ReaderAt, cfg *govulncheck.Config, client *client.Client) error {
26-
vr, err := binary(ctx, handler, exe, cfg, client)
28+
bin, err := createBin(exe)
29+
if err != nil {
30+
return err
31+
}
32+
33+
vr, err := binary(ctx, handler, bin, cfg, client)
2734
if err != nil {
2835
return err
2936
}
3037
return emitBinaryResult(handler, vr, binaryCallstacks(vr))
3138
}
3239

33-
// binary detects presence of vulnerable symbols in exe.
34-
// The Calls, Imports, and Requires fields on Result will be empty.
35-
func binary(ctx context.Context, handler govulncheck.Handler, exe io.ReaderAt, cfg *govulncheck.Config, client *client.Client) (*Result, error) {
40+
// bin is an abstraction of Go binary containing
41+
// minimal information needed by govulncheck.
42+
type bin struct {
43+
Modules []*packages.Module `json:"modules,omitempty"`
44+
PkgSymbols []buildinfo.Symbol `json:"pkgSymbols,omitempty"`
45+
GoVersion string `json:"goVersion,omitempty"`
46+
GOOS string `json:"goos,omitempty"`
47+
GOARCH string `json:"goarch,omitempty"`
48+
}
49+
50+
func createBin(exe io.ReaderAt) (*bin, error) {
3651
mods, packageSymbols, bi, err := buildinfo.ExtractPackagesAndSymbols(exe)
3752
if err != nil {
3853
return nil, fmt.Errorf("could not parse provided binary: %v", err)
3954
}
4055

41-
graph := NewPackageGraph(bi.GoVersion)
42-
graph.AddModules(mods...)
43-
mods = append(mods, graph.GetModule(internal.GoStdModulePath))
56+
return &bin{
57+
Modules: mods,
58+
PkgSymbols: packageSymbols,
59+
GoVersion: bi.GoVersion,
60+
GOOS: findSetting("GOOS", bi),
61+
GOARCH: findSetting("GOARCH", bi),
62+
}, nil
63+
}
64+
65+
// binary detects presence of vulnerable symbols in bin.
66+
// It does not compute call graphs so the corresponding
67+
// info in Result will be empty.
68+
func binary(ctx context.Context, handler govulncheck.Handler, bin *bin, cfg *govulncheck.Config, client *client.Client) (*Result, error) {
69+
graph := NewPackageGraph(bin.GoVersion)
70+
graph.AddModules(bin.Modules...)
71+
mods := append(bin.Modules, graph.GetModule(internal.GoStdModulePath))
4472

4573
mv, err := FetchVulnerabilities(ctx, client, mods)
4674
if err != nil {
@@ -52,21 +80,27 @@ func binary(ctx context.Context, handler govulncheck.Handler, exe io.ReaderAt, c
5280
return nil, err
5381
}
5482

55-
goos := findSetting("GOOS", bi)
56-
goarch := findSetting("GOARCH", bi)
57-
if goos == "" || goarch == "" {
58-
fmt.Printf("warning: failed to extract build system specification GOOS: %s GOARCH: %s\n", goos, goarch)
83+
if bin.GOOS == "" || bin.GOARCH == "" {
84+
fmt.Printf("warning: failed to extract build system specification GOOS: %s GOARCH: %s\n", bin.GOOS, bin.GOARCH)
5985
}
60-
affVulns := affectingVulnerabilities(mv, goos, goarch)
86+
affVulns := affectingVulnerabilities(mv, bin.GOOS, bin.GOARCH)
6187

6288
result := &Result{}
63-
if packageSymbols == nil {
89+
if len(bin.PkgSymbols) == 0 {
6490
// The binary exe is stripped. We currently cannot detect inlined
6591
// symbols for stripped binaries (see #57764), so we report
6692
// vulnerabilities at the go.mod-level precision.
6793
addRequiresOnlyVulns(result, graph, affVulns)
6894
} else {
69-
for pkg, symbols := range packageSymbols {
95+
// Group symbols per package to avoid querying vulns all over again.
96+
pkgSymbols := make(map[string][]string)
97+
for _, sym := range bin.PkgSymbols {
98+
pkgSymbols[sym.Pkg] = append(pkgSymbols[sym.Pkg], sym.Name)
99+
}
100+
101+
for pkg, symbols := range pkgSymbols {
102+
// sort symbols for deterministic results
103+
sort.SliceStable(symbols, func(i, j int) bool { return symbols[i] < symbols[j] })
70104
if !cfg.ScanLevel.WantSymbols() {
71105
addImportsOnlyVulns(result, graph, pkg, symbols, affVulns)
72106
} else {

internal/vulncheck/binary_test.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package vulncheck
1010
import (
1111
"context"
1212
"fmt"
13-
"io"
1413
"os"
1514
"os/exec"
1615
"path/filepath"
@@ -22,7 +21,6 @@ import (
2221
"golang.org/x/vuln/internal/semver"
2322
"golang.org/x/vuln/internal/test"
2423
"golang.org/x/vuln/internal/testenv"
25-
"golang.org/x/vuln/internal/vulncheck/internal/buildinfo"
2624
)
2725

2826
// TODO: we build binary programatically, so what if the underlying tool chain changes?
@@ -107,11 +105,15 @@ func TestBinary(t *testing.T) {
107105
t.Fatalf("failed to build the binary %v %v", err, string(out))
108106
}
109107

110-
bin, err := os.Open(filepath.Join(e.Config.Dir, "entry"))
108+
exe, err := os.Open(filepath.Join(e.Config.Dir, "entry"))
111109
if err != nil {
112-
t.Fatalf("failed to access the binary %v", err)
110+
t.Fatal(err)
111+
}
112+
defer exe.Close()
113+
bin, err := createBin(exe)
114+
if err != nil {
115+
t.Fatal(err)
113116
}
114-
defer bin.Close()
115117

116118
c, err := newTestClient()
117119
if err != nil {
@@ -125,7 +127,7 @@ func TestBinary(t *testing.T) {
125127
t.Fatal(err)
126128
}
127129

128-
goversion := getGoVersion(bin)
130+
goversion := goVersion(bin)
129131
// In importsOnly mode, vulnerable symbols
130132
// {avuln.VulnData.Vuln1, avuln.VulnData.Vuln2, bvuln.Vuln}
131133
// should be detected.
@@ -161,9 +163,8 @@ func TestBinary(t *testing.T) {
161163
compareVulns(t, wantVulns, res)
162164
}
163165

164-
func getGoVersion(exe io.ReaderAt) string {
165-
_, _, bi, _ := buildinfo.ExtractPackagesAndSymbols(exe)
166-
return semver.GoTagToSemver(bi.GoVersion)
166+
func goVersion(bin *bin) string {
167+
return semver.GoTagToSemver(bin.GoVersion)
167168
}
168169

169170
// Test58509 is supposed to test issue #58509 where a whole
@@ -226,11 +227,15 @@ func Vuln() {
226227
t.Fatalf("failed to build the binary %v %v", err, string(out))
227228
}
228229

229-
bin, err := os.Open(filepath.Join(e.Config.Dir, "entry"))
230+
exe, err := os.Open(filepath.Join(e.Config.Dir, "entry"))
230231
if err != nil {
231-
t.Fatalf("failed to access the binary %v", err)
232+
t.Fatal(err)
233+
}
234+
defer exe.Close()
235+
bin, err := createBin(exe)
236+
if err != nil {
237+
t.Fatal(err)
232238
}
233-
defer bin.Close()
234239

235240
c, err := newTestClient()
236241
if err != nil {

internal/vulncheck/internal/buildinfo/additions_scan.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"io"
1818
"net/url"
1919
"runtime/debug"
20-
"sort"
2120
"strings"
2221

2322
"golang.org/x/tools/go/packages"
@@ -41,12 +40,17 @@ func debugModulesToPackagesModules(debugModules []*debug.Module) []*packages.Mod
4140
return packagesModules
4241
}
4342

43+
type Symbol struct {
44+
Pkg string `json:"pkg,omitempty"`
45+
Name string `json:"name,omitempty"`
46+
}
47+
4448
// ExtractPackagesAndSymbols extracts symbols, packages, modules from
4549
// bin as well as bin's metadata.
4650
//
4751
// If the symbol table is not available, such as in the case of stripped
4852
// binaries, returns module and binary info but without the symbol info.
49-
func ExtractPackagesAndSymbols(bin io.ReaderAt) ([]*packages.Module, map[string][]string, *debug.BuildInfo, error) {
53+
func ExtractPackagesAndSymbols(bin io.ReaderAt) ([]*packages.Module, []Symbol, *debug.BuildInfo, error) {
5054
bi, err := buildinfo.Read(bin)
5155
if err != nil {
5256
return nil, nil, nil, err
@@ -87,11 +91,7 @@ func ExtractPackagesAndSymbols(bin io.ReaderAt) ([]*packages.Module, map[string]
8791
return nil, nil, nil, err
8892
}
8993

90-
type pkgSymbol struct {
91-
pkg string
92-
sym string
93-
}
94-
pkgSyms := make(map[pkgSymbol]bool)
94+
pkgSyms := make(map[Symbol]bool)
9595
for _, f := range tab.Funcs {
9696
if f.Func == nil {
9797
continue
@@ -100,7 +100,7 @@ func ExtractPackagesAndSymbols(bin io.ReaderAt) ([]*packages.Module, map[string]
100100
if err != nil {
101101
return nil, nil, nil, err
102102
}
103-
pkgSyms[pkgSymbol{pkgName, symName}] = true
103+
pkgSyms[Symbol{pkgName, symName}] = true
104104

105105
// Collect symbols that were inlined in f.
106106
it, err := lineTab.InlineTree(&f, value, base, r)
@@ -112,20 +112,16 @@ func ExtractPackagesAndSymbols(bin io.ReaderAt) ([]*packages.Module, map[string]
112112
if err != nil {
113113
return nil, nil, nil, err
114114
}
115-
pkgSyms[pkgSymbol{pkgName, symName}] = true
115+
pkgSyms[Symbol{pkgName, symName}] = true
116116
}
117117
}
118118

119-
packageSymbols := make(map[string][]string)
120-
for p := range pkgSyms {
121-
packageSymbols[p.pkg] = append(packageSymbols[p.pkg], p.sym)
122-
}
123-
// Sort symbols per pkg for deterministic results.
124-
for _, syms := range packageSymbols {
125-
sort.Strings(syms)
119+
var syms []Symbol
120+
for ps := range pkgSyms {
121+
syms = append(syms, ps)
126122
}
127123

128-
return debugModulesToPackagesModules(bi.Deps), packageSymbols, bi, nil
124+
return debugModulesToPackagesModules(bi.Deps), syms, bi, nil
129125
}
130126

131127
func parseName(s *gosym.Sym) (pkg, sym string, err error) {

internal/vulncheck/internal/buildinfo/additions_scan_test.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package buildinfo
99

1010
import (
1111
"os"
12+
"sort"
1213
"testing"
1314

1415
"github.com/google/go-cmp/cmp"
@@ -56,10 +57,29 @@ func TestExtractPackagesAndSymbols(t *testing.T) {
5657
if err != nil {
5758
t.Fatal(err)
5859
}
59-
got := syms["main"]
60-
want := []string{"f", "g", "main"}
61-
if !cmp.Equal(got, want) {
62-
t.Errorf("\ngot %q\nwant %q", got, want)
60+
61+
got := mainSortedSymbols(syms)
62+
want := []Symbol{
63+
{"main", "f"},
64+
{"main", "g"},
65+
{"main", "main"},
66+
}
67+
68+
if diff := cmp.Diff(want, got); diff != "" {
69+
t.Errorf("(-want,+got):%s", diff)
6370
}
6471
})
6572
}
73+
74+
// mainSortedSymbols gets symbols for "main" package and
75+
// sorts them for testing purposes.
76+
func mainSortedSymbols(syms []Symbol) []Symbol {
77+
var s []Symbol
78+
for _, ps := range syms {
79+
if ps.Pkg == "main" {
80+
s = append(s, ps)
81+
}
82+
}
83+
sort.SliceStable(s, func(i, j int) bool { return s[i].Pkg+"."+s[i].Name < s[j].Pkg+"."+s[j].Name })
84+
return s
85+
}

internal/vulncheck/internal/buildinfo/additions_stripped_test.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,16 @@ func TestStrippedDarwin(t *testing.T) {
5959
if err != nil {
6060
t.Fatal(err)
6161
}
62-
got := syms["main"]
63-
want := []string{"f", "g", "main"}
64-
if !cmp.Equal(got, want) {
65-
t.Errorf("\ngot %q\nwant %q", got, want)
62+
63+
got := mainSortedSymbols(syms)
64+
want := []Symbol{
65+
{"main", "f"},
66+
{"main", "g"},
67+
{"main", "main"},
68+
}
69+
70+
if diff := cmp.Diff(want, got); diff != "" {
71+
t.Errorf("(-want,+got):%s", diff)
6672
}
6773
})
6874
}

0 commit comments

Comments
 (0)