Skip to content

Commit 8fb35e0

Browse files
committed
internal/scan: add binary extract mode
The extract mode spits out a json blob representing the minimal representation of a Go binary needed for govulncheck vulnerability detection. binary mode accepts both a Go binary and this representation as an input. The contents of extract should be regarded as a blob. The users of this flag should not rely on its representation. It might change in the future. Change-Id: I81027062d34609fed7541ad2092d4cbe5df0d118 Reviewed-on: https://go-review.googlesource.com/c/vuln/+/542035 Run-TryBot: Zvonimir Pavlinovic <zpavlinovic@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Cottrell <iancottrell@google.com> Reviewed-by: Maceo Thompson <maceothompson@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
1 parent 3072335 commit 8fb35e0

20 files changed

+285
-23
lines changed

cmd/govulncheck/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ with the -mode=binary flag:
5252
Govulncheck uses the binary's symbol information to find mentions of vulnerable
5353
functions. Its output omits call stacks, which require source code analysis.
5454
55+
Govulncheck also supports -mode=extract on a Go binary for extraction of minimal
56+
information needed to analyze the binary. This will produce a blob, typically much
57+
smaller than the binary, that can also be passed to govulncheck as an argument with
58+
-mode=binary. The users should not rely on the contents or representation of the blob.
59+
5560
Govulncheck exits successfully (exit code 0) if there are no vulnerabilities,
5661
and exits unsuccessfully if there are. It also exits successfully if the -json flag
5762
is provided, regardless of the number of detected vulnerabilities.

cmd/govulncheck/main_test.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"unsafe"
2525

2626
"github.com/google/go-cmdtest"
27+
"github.com/google/go-cmp/cmp"
2728
"golang.org/x/vuln/internal/govulncheck"
2829
"golang.org/x/vuln/internal/test"
2930
"golang.org/x/vuln/internal/web"
@@ -153,7 +154,10 @@ func TestCommand(t *testing.T) {
153154
varName := filepath.Base(md) + "_binary"
154155
os.Setenv(varName, binary)
155156
}
156-
runTestSuite(t, filepath.Join(testDir, "testdata", "testfiles"), govulndbURI.String(), *update)
157+
testFilesDir := filepath.Join(testDir, "testdata", "testfiles")
158+
os.Setenv("testdir", testFilesDir)
159+
160+
runTestSuite(t, testFilesDir, govulndbURI.String(), *update)
157161
if runtime.GOOS != "darwin" {
158162
// Binaries are not stripped on darwin with go1.21 and earlier. See #61051.
159163
runTestSuite(t, filepath.Join(testDir, "testdata", "strip"), govulndbURI.String(), *update)
@@ -196,7 +200,7 @@ func runTestSuite(t *testing.T, dir string, govulndb string, update bool) {
196200
}
197201
ts.DisableLogging = true
198202

199-
ts.Commands["govulncheck"] = func(args []string, inputFile string) ([]byte, error) {
203+
govulncheckCmd := func(args []string, inputFile string) ([]byte, error) {
200204
parallelLimiter <- struct{}{}
201205
defer func() { <-parallelLimiter }()
202206

@@ -250,6 +254,37 @@ func runTestSuite(t *testing.T, dir string, govulndb string, update bool) {
250254
}
251255
return out, err
252256
}
257+
ts.Commands["govulncheck"] = govulncheckCmd
258+
259+
// govulncheck-cmp is like govulncheck except that the last argument is a file
260+
// whose contents are compared to the output of govulncheck. This command does
261+
// not output anything.
262+
ts.Commands["govulncheck-cmp"] = func(args []string, inputFile string) ([]byte, error) {
263+
l := len(args)
264+
if l == 0 {
265+
return nil, nil
266+
}
267+
cmpArg := args[l-1]
268+
gArgs := args[:l-1]
269+
270+
out, err := govulncheckCmd(gArgs, inputFile)
271+
if err != nil {
272+
return nil, &cmdtest.ExitCodeErr{Msg: err.Error(), Code: 1}
273+
}
274+
got := string(out)
275+
276+
file, err := os.ReadFile(cmpArg)
277+
if err != nil {
278+
return nil, &cmdtest.ExitCodeErr{Msg: err.Error(), Code: 1}
279+
}
280+
want := string(file)
281+
282+
if diff := cmp.Diff(want, got); diff != "" {
283+
return nil, &cmdtest.ExitCodeErr{Msg: "govulncheck output not matching the file contents:\n" + diff, Code: 1}
284+
}
285+
return nil, nil
286+
}
287+
253288
if update {
254289
ts.Run(t, true)
255290
return
Binary file not shown.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#####
2+
# Test binary mode using the extracted binary blob.
3+
$ govulncheck -mode=binary ${testdir}/extract/vuln.blob --> FAIL 3
4+
Scanning your binary for known vulnerabilities...
5+
6+
Vulnerability #1: GO-2021-0265
7+
A maliciously crafted path can cause Get and other query functions to
8+
consume excessive amounts of CPU and time.
9+
More info: https://pkg.go.dev/vuln/GO-2021-0265
10+
Module: github.com/tidwall/gjson
11+
Found in: github.com/tidwall/gjson@v1.6.5
12+
Fixed in: github.com/tidwall/gjson@v1.9.3
13+
Example traces found:
14+
#1: gjson.Get
15+
#2: gjson.Result.Get
16+
17+
Vulnerability #2: GO-2021-0113
18+
Due to improper index calculation, an incorrectly formatted language tag can
19+
cause Parse to panic via an out of bounds read. If Parse is used to process
20+
untrusted user inputs, this may be used as a vector for a denial of service
21+
attack.
22+
More info: https://pkg.go.dev/vuln/GO-2021-0113
23+
Module: golang.org/x/text
24+
Found in: golang.org/x/text@v0.3.0
25+
Fixed in: golang.org/x/text@v0.3.7
26+
Example traces found:
27+
#1: language.Parse
28+
29+
Vulnerability #3: GO-2021-0054
30+
Due to improper bounds checking, maliciously crafted JSON objects can cause
31+
an out-of-bounds panic. If parsing user input, this may be used as a denial
32+
of service vector.
33+
More info: https://pkg.go.dev/vuln/GO-2021-0054
34+
Module: github.com/tidwall/gjson
35+
Found in: github.com/tidwall/gjson@v1.6.5
36+
Fixed in: github.com/tidwall/gjson@v1.6.6
37+
Example traces found:
38+
#1: gjson.Result.ForEach
39+
40+
Your code is affected by 3 vulnerabilities from 2 modules.
41+
42+
Share feedback at https://go.dev/s/govulncheck-feedback.
43+
44+
# Test extract mode. Due to the size of the blob even for smallest programs, we
45+
# directly compare its output to a target vuln_blob.json file.
46+
$ govulncheck-cmp -mode=extract ${moddir}/vuln/vuln_dont_run_me ${testdir}/extract/vuln.blob

cmd/govulncheck/testdata/testfiles/extract/vuln.blob

Lines changed: 2 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"govulncheck-extract","version":"0.1.0"}{"modules":[]}{"name":"govulncheck-extract","version":"0.1.0"}

cmd/govulncheck/testdata/testfiles/failures/binary_fail.ct

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,54 @@ $ govulncheck -mode=binary notafile --> FAIL 2
44
"notafile" is not a file
55

66
#####
7-
# Test of passing a non-binary file to -mode=binary
7+
# Test of passing a non-binary and non-blob file to -mode=binary
88
$ govulncheck -mode=binary ${moddir}/vuln/go.mod --> FAIL 1
9-
govulncheck: could not parse provided binary: unrecognized file format
9+
govulncheck: unrecognized binary format
10+
11+
#####
12+
# Test of passing a blob with invalid header id
13+
$ govulncheck -mode=binary ${testdir}/failures/invalid_header_name.blob --> FAIL 1
14+
govulncheck: unrecognized binary format
15+
16+
#####
17+
# Test of passing a blob with invalid header version
18+
$ govulncheck -mode=binary ${testdir}/failures/invalid_header_version.blob --> FAIL 1
19+
govulncheck: unrecognized binary format
20+
21+
#####
22+
# Test of passing a blob with no header
23+
$ govulncheck -mode=binary ${testdir}/failures/no_header.blob --> FAIL 1
24+
govulncheck: unrecognized binary format
25+
26+
#####
27+
# Test of passing a blob with invalid header, i.e., no header
28+
$ govulncheck -mode=binary ${testdir}/failures/no_header.blob --> FAIL 1
29+
govulncheck: unrecognized binary format
30+
31+
#####
32+
# Test of passing a blob with no body
33+
$ govulncheck -mode=binary ${testdir}/failures/no_body.blob --> FAIL 1
34+
govulncheck: unrecognized binary format
35+
36+
#####
37+
# Test of passing an empty blob/file
38+
$ govulncheck -mode=binary ${testdir}/failures/empty.blob --> FAIL 1
39+
govulncheck: unrecognized binary format
40+
41+
#####
42+
# Test of passing an empty blob message
43+
$ govulncheck -mode=binary ${testdir}/failures/empty_message.blob --> FAIL 1
44+
govulncheck: unrecognized binary format
45+
46+
#####
47+
# Test of passing blob message with multiple headers
48+
$ govulncheck -mode=binary ${testdir}/failures/multi_header.blob --> FAIL 1
49+
govulncheck: unrecognized binary format
50+
51+
#####
52+
# Test of passing blob message with something after the body
53+
$ govulncheck -mode=binary ${testdir}/failures/multi_header.blob --> FAIL 1
54+
govulncheck: unrecognized binary format
1055

1156
#####
1257
# Test of trying to analyze multiple binaries

cmd/govulncheck/testdata/testfiles/failures/empty.blob

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#####
2+
# Test extraction of an unsupported file format
3+
$ govulncheck -mode=extract ${moddir}/vuln/go.mod --> FAIL 1
4+
govulncheck: unrecognized binary format
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"id":"invalid-name","protocol":"0.1.0"}{"modules":[{"Path":"github.com/tidwall/gjson","Version":"v1.6.5","Replace":null,"Time":null,"Main":false,"Indirect":false,"Dir":"","GoMod":"","GoVersion":"","Error":null}]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"invalid-name","version":"0.1.0"}{"modules":[{"Path":"github.com/tidwall/gjson","Version":"v1.6.5","Replace":null,"Time":null,"Main":false,"Indirect":false,"Dir":"","GoMod":"","GoVersion":"","Error":null}]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"govulncheck-extract","version":"8.8.8"}{"modules":[{"Path":"github.com/tidwall/gjson","Version":"v1.6.5","Replace":null,"Time":null,"Main":false,"Indirect":false,"Dir":"","GoMod":"","GoVersion":"","Error":null}]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"govulncheck-extract","version":"0.1.0"}{"name":"govulncheck-extract","version":"0.1.0"}{"modules":[]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"govulncheck-extract","version":"0.1.0"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"modules":[{"Path":"github.com/tidwall/gjson","Version":"v1.6.5","Replace":null,"Time":null,"Main":false,"Indirect":false,"Dir":"","GoMod":"","GoVersion":"","Error":null}]}

internal/scan/binary.go

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ package scan
99

1010
import (
1111
"context"
12-
"fmt"
12+
"encoding/json"
13+
"errors"
1314
"io"
1415
"os"
1516
"runtime/debug"
@@ -21,17 +22,11 @@ import (
2122
"golang.org/x/vuln/internal/vulncheck"
2223
)
2324

24-
// runBinary detects presence of vulnerable symbols in an executable.
25+
// runBinary detects presence of vulnerable symbols in an executable or its minimal blob representation.
2526
func runBinary(ctx context.Context, handler govulncheck.Handler, cfg *config, client *client.Client) (err error) {
2627
defer derrors.Wrap(&err, "govulncheck")
2728

28-
exe, err := os.Open(cfg.patterns[0])
29-
if err != nil {
30-
return err
31-
}
32-
defer exe.Close()
33-
34-
bin, err := createBin(exe)
29+
bin, err := createBin(cfg.patterns[0])
3530
if err != nil {
3631
return err
3732
}
@@ -43,18 +38,57 @@ func runBinary(ctx context.Context, handler govulncheck.Handler, cfg *config, cl
4338
return vulncheck.Binary(ctx, handler, bin, &cfg.Config, client)
4439
}
4540

46-
func createBin(exe io.ReaderAt) (*vulncheck.Bin, error) {
47-
mods, packageSymbols, bi, err := buildinfo.ExtractPackagesAndSymbols(exe)
41+
func createBin(path string) (*vulncheck.Bin, error) {
42+
f, err := os.Open(path)
4843
if err != nil {
49-
return nil, fmt.Errorf("could not parse provided binary: %v", err)
44+
return nil, err
45+
}
46+
defer f.Close()
47+
48+
// First check if the path points to a Go binary. Otherwise, blob
49+
// parsing might json decode a Go binary which takes time.
50+
//
51+
// TODO(#64716): use fingerprinting to make this precise, clean, and fast.
52+
mods, packageSymbols, bi, err := buildinfo.ExtractPackagesAndSymbols(f)
53+
if err == nil {
54+
return &vulncheck.Bin{
55+
Modules: mods,
56+
PkgSymbols: packageSymbols,
57+
GoVersion: bi.GoVersion,
58+
GOOS: findSetting("GOOS", bi),
59+
GOARCH: findSetting("GOARCH", bi),
60+
}, nil
61+
}
62+
63+
// Otherwise, see if the path points to a valid blob.
64+
bin := parseBlob(f)
65+
if bin != nil {
66+
return bin, nil
67+
}
68+
69+
return nil, errors.New("unrecognized binary format")
70+
}
71+
72+
// parseBlob extracts vulncheck.Bin from a valid blob. If it
73+
// cannot recognize a valid blob, returns nil.
74+
func parseBlob(from io.Reader) *vulncheck.Bin {
75+
dec := json.NewDecoder(from)
76+
77+
var h header
78+
if err := dec.Decode(&h); err != nil {
79+
return nil // no header
80+
} else if h.Name != extractModeID || h.Version != extractModeVersion {
81+
return nil // invalid header
82+
}
83+
84+
var b vulncheck.Bin
85+
if err := dec.Decode(&b); err != nil {
86+
return nil // no body
87+
}
88+
if dec.More() {
89+
return nil // we want just header and body, nothing else
5090
}
51-
return &vulncheck.Bin{
52-
Modules: mods,
53-
PkgSymbols: packageSymbols,
54-
GoVersion: bi.GoVersion,
55-
GOOS: findSetting("GOOS", bi),
56-
GOARCH: findSetting("GOARCH", bi),
57-
}, nil
91+
return &b
5892
}
5993

6094
// findSetting returns value of setting from bi if present.

internal/scan/extract.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build go1.18
6+
// +build go1.18
7+
8+
package scan
9+
10+
import (
11+
"encoding/json"
12+
"fmt"
13+
"io"
14+
"sort"
15+
16+
"golang.org/x/vuln/internal/derrors"
17+
"golang.org/x/vuln/internal/vulncheck"
18+
)
19+
20+
const (
21+
// extractModeID is the unique name of the extract mode protocol
22+
extractModeID = "govulncheck-extract"
23+
extractModeVersion = "0.1.0"
24+
)
25+
26+
// header information for the blob output.
27+
type header struct {
28+
Name string `json:"name"`
29+
Version string `json:"version"`
30+
}
31+
32+
// runExtract dumps the extracted abstraction of binary at cfg.patterns to out.
33+
// It prints out exactly two blob messages, one with the header and one with
34+
// the vulncheck.Bin as the body.
35+
func runExtract(cfg *config, out io.Writer) (err error) {
36+
defer derrors.Wrap(&err, "govulncheck")
37+
38+
bin, err := createBin(cfg.patterns[0])
39+
if err != nil {
40+
return err
41+
}
42+
sortBin(bin) // sort for easier testing and validation
43+
header := header{
44+
Name: extractModeID,
45+
Version: extractModeVersion,
46+
}
47+
48+
enc := json.NewEncoder(out)
49+
50+
if err := enc.Encode(header); err != nil {
51+
return fmt.Errorf("marshaling blob header: %v", err)
52+
}
53+
if err := enc.Encode(bin); err != nil {
54+
return fmt.Errorf("marshaling blob body: %v", err)
55+
}
56+
return nil
57+
}
58+
59+
func sortBin(bin *vulncheck.Bin) {
60+
sort.SliceStable(bin.PkgSymbols, func(i, j int) bool {
61+
return bin.PkgSymbols[i].Pkg+"."+bin.PkgSymbols[i].Name < bin.PkgSymbols[j].Pkg+"."+bin.PkgSymbols[j].Name
62+
})
63+
}

0 commit comments

Comments
 (0)