blob: c610b1c6fd069f8fd6e516fbb4287e24df84b782 [file] [log] [blame]
// 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.
//go:build go1.18
// +build go1.18
package vulncheck
import (
"context"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"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) {
// TODO(#52160): investigate why Binary does not process plan9 binaries
if !hasGoBuild() || runtime.GOOS == "plan9" {
t.Skip("fails on android and plan9")
}
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()
cmd := exec.Command("go", "build", "-o", "entry")
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))
}
bin, err := os.Open(filepath.Join(e.Config.Dir, "entry"))
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.
wantVulns := []*Vuln{
{Symbol: "Vuln", PkgPath: "golang.org/bmod/bvuln", ModPath: "golang.org/bmod"},
{Symbol: "VulnData.Vuln1", PkgPath: "golang.org/amod/avuln", ModPath: "golang.org/amod"},
{Symbol: "VulnData.Vuln2", PkgPath: "golang.org/amod/avuln", ModPath: "golang.org/amod"},
}
diff := cmp.Diff(wantVulns, res.Vulns,
cmpopts.IgnoreFields(Vuln{}, "OSV"),
cmpopts.SortSlices(func(v1, v2 *Vuln) bool { return v1.Symbol < v2.Symbol }))
if diff != "" {
t.Errorf("vulns mismatch (-want, +got)\n%s", diff)
}
// 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))
}
// Check that the binary's modules are returned.
// The list does not include the module binary itself.
wantMods := []*Module{
{Path: "golang.org/amod", Version: "v1.1.3"},
{Path: "golang.org/bmod", Version: "v0.5.0"},
{Path: "golang.org/cmod", Version: "v1.1.3"},
}
gotMods := res.Modules
sort.Slice(gotMods, func(i, j int) bool { return gotMods[i].Path < gotMods[j].Path })
if diff := cmp.Diff(wantMods, gotMods, cmpopts.IgnoreFields(Module{}, "Dir")); diff != "" {
t.Errorf("modules mismatch (-want, +got):\n%s", diff)
}
}
// hasGoBuild reports whether the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
//
// Duplicated from std/internal/testenv
func hasGoBuild() bool {
if os.Getenv("GO_GCFLAGS") != "" {
return false
}
switch runtime.GOOS {
case "android", "js", "ios":
return false
}
return true
}