317 lines
8.7 KiB
Go
317 lines
8.7 KiB
Go
// Copyright 2019 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package nogo implements binary analysis similar to bazel's nogo,
|
|
// or the unitchecker package. It exists in order to provide additional
|
|
// facilities for analysis, namely plumbing through the output from
|
|
// dumping the generated binary (to analyze actual produced code).
|
|
package nogo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/analysis/internal/facts"
|
|
"golang.org/x/tools/go/gcexportdata"
|
|
"gvisor.dev/gvisor/tools/nogo/data"
|
|
)
|
|
|
|
// pkgConfig is serialized as the configuration.
|
|
//
|
|
// This contains everything required for the analysis.
|
|
type pkgConfig struct {
|
|
ImportPath string
|
|
GoFiles []string
|
|
NonGoFiles []string
|
|
Tags []string
|
|
GOOS string
|
|
GOARCH string
|
|
ImportMap map[string]string
|
|
FactMap map[string]string
|
|
FactOutput string
|
|
Objdump string
|
|
}
|
|
|
|
// loadFacts finds and loads facts per FactMap.
|
|
func (c *pkgConfig) loadFacts(path string) ([]byte, error) {
|
|
realPath, ok := c.FactMap[path]
|
|
if !ok {
|
|
return nil, nil // No facts available.
|
|
}
|
|
|
|
// Read the files file.
|
|
data, err := ioutil.ReadFile(realPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// shouldInclude indicates whether the file should be included.
|
|
//
|
|
// NOTE: This does only basic parsing of tags.
|
|
func (c *pkgConfig) shouldInclude(path string) (bool, error) {
|
|
ctx := build.Default
|
|
ctx.GOOS = c.GOOS
|
|
ctx.GOARCH = c.GOARCH
|
|
ctx.BuildTags = c.Tags
|
|
return ctx.MatchFile(filepath.Dir(path), filepath.Base(path))
|
|
}
|
|
|
|
// importer is an implementation of go/types.Importer.
|
|
//
|
|
// This wraps a configuration, which provides the map of package names to
|
|
// files, and the facts. Note that this importer implementation will always
|
|
// pass when a given package is not available.
|
|
type importer struct {
|
|
pkgConfig
|
|
fset *token.FileSet
|
|
cache map[string]*types.Package
|
|
}
|
|
|
|
// Import implements types.Importer.Import.
|
|
func (i *importer) Import(path string) (*types.Package, error) {
|
|
if path == "unsafe" {
|
|
// Special case: go/types has pre-defined type information for
|
|
// unsafe. We ensure that this package is correct, in case any
|
|
// analyzers are specifically looking for this.
|
|
return types.Unsafe, nil
|
|
}
|
|
realPath, ok := i.ImportMap[path]
|
|
var (
|
|
rc io.ReadCloser
|
|
err error
|
|
)
|
|
if !ok {
|
|
// Not found in the import path. Attempt to find the package
|
|
// via the standard library.
|
|
rc, err = findStdPkg(path, i.GOOS, i.GOARCH)
|
|
} else {
|
|
// Open the file.
|
|
rc, err = os.Open(realPath)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rc.Close()
|
|
|
|
// Load all exported data.
|
|
r, err := gcexportdata.NewReader(rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return gcexportdata.Read(r, i.fset, i.cache, path)
|
|
}
|
|
|
|
// checkPackage runs all analyzers.
|
|
//
|
|
// The implementation was adapted from [1], which was in turn adpated from [2].
|
|
// This returns a list of matching analysis issues, or an error if the analysis
|
|
// could not be completed.
|
|
//
|
|
// [1] bazelbuid/rules_go/tools/builders/nogo_main.go
|
|
// [2] golang.org/x/tools/go/checker/internal/checker
|
|
func checkPackage(config pkgConfig) ([]string, error) {
|
|
imp := &importer{
|
|
pkgConfig: config,
|
|
fset: token.NewFileSet(),
|
|
cache: make(map[string]*types.Package),
|
|
}
|
|
|
|
// Load all source files.
|
|
var syntax []*ast.File
|
|
for _, file := range config.GoFiles {
|
|
include, err := config.shouldInclude(file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error evaluating file %q: %v", file, err)
|
|
}
|
|
if !include {
|
|
continue
|
|
}
|
|
s, err := parser.ParseFile(imp.fset, file, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing file %q: %v", file, err)
|
|
}
|
|
syntax = append(syntax, s)
|
|
}
|
|
|
|
// Check type information.
|
|
typesSizes := types.SizesFor("gc", config.GOARCH)
|
|
typeConfig := types.Config{Importer: imp}
|
|
typesInfo := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
}
|
|
types, err := typeConfig.Check(config.ImportPath, imp.fset, syntax, typesInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error checking types: %v", err)
|
|
}
|
|
|
|
// Load all package facts.
|
|
facts, err := facts.Decode(types, config.loadFacts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decoding facts: %v", err)
|
|
}
|
|
|
|
// Set the binary global for use.
|
|
data.Objdump = config.Objdump
|
|
|
|
// Register fact types and establish dependencies between analyzers.
|
|
// The visit closure will execute recursively, and populate results
|
|
// will all required analysis results.
|
|
diagnostics := make(map[*analysis.Analyzer][]analysis.Diagnostic)
|
|
results := make(map[*analysis.Analyzer]interface{})
|
|
var visit func(*analysis.Analyzer) error // For recursion.
|
|
visit = func(a *analysis.Analyzer) error {
|
|
if _, ok := results[a]; ok {
|
|
return nil
|
|
}
|
|
|
|
// Run recursively for all dependencies.
|
|
for _, req := range a.Requires {
|
|
if err := visit(req); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Prepare the matcher.
|
|
m := analyzerConfig[a]
|
|
report := func(d analysis.Diagnostic) {
|
|
if m.ShouldReport(d, imp.fset) {
|
|
diagnostics[a] = append(diagnostics[a], d)
|
|
}
|
|
}
|
|
|
|
// Run the analysis.
|
|
factFilter := make(map[reflect.Type]bool)
|
|
for _, f := range a.FactTypes {
|
|
factFilter[reflect.TypeOf(f)] = true
|
|
}
|
|
p := &analysis.Pass{
|
|
Analyzer: a,
|
|
Fset: imp.fset,
|
|
Files: syntax,
|
|
Pkg: types,
|
|
TypesInfo: typesInfo,
|
|
ResultOf: results, // All results.
|
|
Report: report,
|
|
ImportPackageFact: facts.ImportPackageFact,
|
|
ExportPackageFact: facts.ExportPackageFact,
|
|
ImportObjectFact: facts.ImportObjectFact,
|
|
ExportObjectFact: facts.ExportObjectFact,
|
|
AllPackageFacts: func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) },
|
|
AllObjectFacts: func() []analysis.ObjectFact { return facts.AllObjectFacts(factFilter) },
|
|
TypesSizes: typesSizes,
|
|
}
|
|
result, err := a.Run(p)
|
|
if err != nil {
|
|
return fmt.Errorf("error running analysis %s: %v", a, err)
|
|
}
|
|
|
|
// Sanity check & save the result.
|
|
if got, want := reflect.TypeOf(result), a.ResultType; got != want {
|
|
return fmt.Errorf("error: analyzer %s returned a result of type %v, but declared ResultType %v", a, got, want)
|
|
}
|
|
results[a] = result
|
|
return nil // Success.
|
|
}
|
|
|
|
// Visit all analysis recursively.
|
|
for a, _ := range analyzerConfig {
|
|
if err := visit(a); err != nil {
|
|
return nil, err // Already has context.
|
|
}
|
|
}
|
|
|
|
// Write the output file.
|
|
if config.FactOutput != "" {
|
|
factData := facts.Encode()
|
|
if err := ioutil.WriteFile(config.FactOutput, factData, 0644); err != nil {
|
|
return nil, fmt.Errorf("error: unable to open facts output %q: %v", config.FactOutput, err)
|
|
}
|
|
}
|
|
|
|
// Convert all diagnostics to strings.
|
|
findings := make([]string, 0, len(diagnostics))
|
|
for a, ds := range diagnostics {
|
|
for _, d := range ds {
|
|
// Include the anlyzer name for debugability and configuration.
|
|
findings = append(findings, fmt.Sprintf("%s: %s: %s", a.Name, imp.fset.Position(d.Pos), d.Message))
|
|
}
|
|
}
|
|
|
|
// Return all findings.
|
|
return findings, nil
|
|
}
|
|
|
|
var (
|
|
configFile = flag.String("config", "", "configuration file (in JSON format)")
|
|
)
|
|
|
|
// Main is the entrypoint; it should be called directly from main.
|
|
//
|
|
// N.B. This package registers it's own flags.
|
|
func Main() {
|
|
// Parse all flags.
|
|
flag.Parse()
|
|
|
|
// Load the configuration.
|
|
f, err := os.Open(*configFile)
|
|
if err != nil {
|
|
log.Fatalf("unable to open configuration %q: %v", *configFile, err)
|
|
}
|
|
defer f.Close()
|
|
config := new(pkgConfig)
|
|
dec := json.NewDecoder(f)
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(config); err != nil {
|
|
log.Fatalf("unable to decode configuration: %v", err)
|
|
}
|
|
|
|
// Process the package.
|
|
findings, err := checkPackage(*config)
|
|
if err != nil {
|
|
log.Fatalf("error checking package: %v", err)
|
|
}
|
|
|
|
// No findings?
|
|
if len(findings) == 0 {
|
|
os.Exit(0)
|
|
}
|
|
|
|
// Print findings and exit with non-zero code.
|
|
for _, finding := range findings {
|
|
fmt.Fprintf(os.Stdout, "%s\n", finding)
|
|
}
|
|
os.Exit(1)
|
|
}
|