Files
visor/tools/checkconst/checkconst.go
T
Michael PrattandgVisor bot 418052deb7 Remove last remaining !go1.22 build tag
The last remaining !go1.22 build is protecting the definition of
pkg/sync.maptype, which is a copy of runtime.maptype. We need to ensure these
definitions match so we can safely access the hasher field.

At its core, this CL achieves this check by ensuring that
unsafe.Offsetof(maptype{}.Hasher) matches the offset in the runtime version of
the type.

Several things happen along the way to achieve this:

* As of May 2023, runtime.maptype is actually a type alias for
internal/abi.MapType. checkoffset was failing to record the offsets because it
skipped type aliases for no good reason. Simply removing the type alias check
is sufficient to make type aliases work. (This part of the CL is technically
unnecessary because this CL ultimately references internal/abi.MapType
directly in anticipation of removal of the type alias. But there is no reason
not to allow type aliases).

* The checkconst / checkoffset regexp unintentionally does not allow / in
package paths, even though the rest of the package supports /. Fix this.

* checkconst was comparing the literal AST expression string against the
runtime value (i.e., "unsafe.Offsetof(maptype{}.Hasher)" vs "72", which fails
comparison. Switch to getting the resolved constant value from the type
checker.

* nogo/check.importer only loads package facts on direct import (stored in
importer.cache). If a package is not directly imported ImportPackageFact will
not find the facts. Typically packages need to ensure they directly depend on
packages they want facts from (e.g., pkg/sync has a dummy import of runtime in
runtime.go). This doesn't work for internal/abi because we cannot directly
import an internal package. Work around this as a hack by unconditionally
"importing" internal/abi when analyzing any package.

With regard to the last point, not that the nogo/defs.bzl nogo integration only
provides facts from the direct dependencies and the entire stdlib (since the
stdlib is analyzed as one bundle). So this trick only works for a stdlib
package. A bazel package indirect dependency would be missing facts altogether.

PiperOrigin-RevId: 549999084
2023-07-21 11:22:36 -07:00

387 lines
11 KiB
Go

// Copyright 2021 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 checkconst checks constant values.
//
// This analyzer supports multiple annotations: checkconst, checkoffset, checksize and checkalign.
// Each of these essentially checks the value of the declared constant (or the #define'ed value in
// the case of an assembly file) against the value seen during analysis. If this does not match,
// an error is emitted with the appropriate value for that constant/offset/size/alignment.
package checkconst
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"io/ioutil"
"regexp"
"strconv"
"strings"
"golang.org/x/tools/go/analysis"
)
var (
checkconstMagic = "\\+check(const|align|offset|size)"
checkconstRegexp = regexp.MustCompile(checkconstMagic)
constRegexp = regexp.MustCompile("//\\s+" + checkconstMagic + "\\s+([A-Za-z0-9_\\./]+)\\s+([A-Za-z0-9_\\.]+)")
defineRegexp = regexp.MustCompile("#define\\s+[A-Za-z0-9_]+\\s+([A-Za-z0-9_]+\\s*\\+\\s*)*([x0-9]+)\\s+//\\s+" + checkconstMagic + "\\s+([A-Za-z0-9_\\./]+)\\s+([A-Za-z0-9_\\.]+)")
)
// Analyzer defines the entrypoint.
var Analyzer = &analysis.Analyzer{
Name: "checkconst",
Doc: "validates basic constants",
Run: run,
FactTypes: []analysis.Fact{
(*Constants)(nil),
},
}
// Constants contains all constant values.
type Constants struct {
Alignments map[string]int64
Offsets map[string]int64
Sizes map[string]int64
Values map[string]string
}
// AFact implements analysis.Fact.AFact.
func (*Constants) AFact() {}
// walkObject walks a local object hierarchy.
func (c *Constants) walkObject(pass *analysis.Pass, parents []string, obj types.Object) {
switch x := obj.(type) {
case *types.Const:
name := strings.Join(parents, ".")
c.Values[name] = x.Val().ExactString()
case *types.PkgName:
// Don't walk to other packages.
case *types.Var:
// Add information as a field.
bestEffort(func() {
name := strings.Join(parents, ".")
c.Alignments[name] = pass.TypesSizes.Alignof(x.Type())
c.Sizes[name] = pass.TypesSizes.Sizeof(x.Type())
})
case *types.TypeName:
// Skip if just an alias, or if not underlying type, or if a
// type parameter. If it is not an alias, then it must be
// package-local.
typ := x.Type()
if typ == nil || typ.Underlying() == nil {
break
}
if _, ok := typ.(*types.TypeParam); ok {
break
}
// Add basic information.
bestEffort(func() {
name := strings.Join(parents, ".")
c.Alignments[name] = pass.TypesSizes.Alignof(typ)
c.Sizes[name] = pass.TypesSizes.Sizeof(typ)
})
// Recurse to fields if this is a definition.
if structType, ok := typ.Underlying().(*types.Struct); ok {
fields := make([]*types.Var, 0, structType.NumFields())
for i := 0; i < structType.NumFields(); i++ {
fieldObj := structType.Field(i)
fields = append(fields, fieldObj)
c.walkObject(pass, append(parents, fieldObj.Name()), fieldObj)
}
bestEffort(func() {
offsets := pass.TypesSizes.Offsetsof(fields)
for i, field := range fields {
fieldName := strings.Join(append(parents, field.Name()), ".")
c.Offsets[fieldName] = offsets[i]
}
})
}
}
}
// bestEffort is a panic/recover wrapper. This is used because the tools
// library occasionally panics due to some type parameter use, and there is
// simple or obvious way to detect these conditions. This should only be used
// when absolutely necessary.
func bestEffort(fn func()) {
defer func() {
recover()
}()
fn()
}
// walkScope recursively resolves a scope.
func (c *Constants) walkScope(pass *analysis.Pass, parents []string, scope *types.Scope) {
for _, name := range scope.Names() {
c.walkObject(pass, append(parents, name), scope.Lookup(name))
}
}
// extractFacts finds all local facts.
func extractFacts(pass *analysis.Pass) {
c := Constants{
Alignments: make(map[string]int64),
Offsets: make(map[string]int64),
Sizes: make(map[string]int64),
Values: make(map[string]string),
}
// Accumulate all facts.
c.walkScope(pass, make([]string, 0, 128), pass.Pkg.Scope())
pass.ExportPackageFact(&c)
}
// findPackage finds the package by name.
func findPackage(pkg *types.Package, pkgName string) (*types.Package, error) {
if pkgName == "." || pkgName == "" {
return pkg, nil
}
// Attempt to resolve with the full path.
for _, importedPkg := range pkg.Imports() {
if importedPkg.Path() == pkgName {
return importedPkg, nil
}
}
// Attempt to resolve using the short name.
for _, importedPkg := range pkg.Imports() {
if importedPkg.Name() == pkgName {
return importedPkg, nil
}
}
// Attempt to resolve with the full path from transitive dependencies.
//
// This is needed for referencing internal/ packages which we cannot
// directly import, but can be reached indirectly (e.g., internal/abi
// is reachable from runtime).
//
// N.B. nogo/check.importer only loads facts on direct import, so
// ImportPackageFact may fail without an explicit import. See hack in
// nogo/check.Package.
visited := map[*types.Package]struct{}{}
var visit func(pkg *types.Package) *types.Package
visit = func(pkg *types.Package) *types.Package {
if _, ok := visited[pkg]; ok {
return nil
}
visited[pkg] = struct{}{}
if pkg.Path() == pkgName {
return pkg
}
for _, importedPkg := range pkg.Imports() {
if found := visit(importedPkg); found != nil {
return found
}
}
return nil
}
for _, importedPkg := range pkg.Imports() {
if found := visit(importedPkg); found != nil {
return found, nil
}
}
return nil, fmt.Errorf("unable to locate package %q (saw %v)", pkgName, visited)
}
// matchRegexp performs a regexp match with a sanity check.
func matchRegexp(pass *analysis.Pass, pos func() token.Pos, re *regexp.Regexp, text string) ([]string, bool) {
m := re.FindStringSubmatch(text)
if m == nil && checkconstRegexp.FindString(text) != "" {
pass.Reportf(pos(), "potentially misformed checkconst directives")
}
return m, m != nil
}
// buildExpected builds the expected value.
func buildExpected(pass *analysis.Pass, pos func() token.Pos, factName, pkgName, objName string) (string, bool) {
// First, resolve the package.
pkg, err := findPackage(pass.Pkg, pkgName)
if err != nil {
pass.Reportf(pos(), "unable to resolve package %q: %v", pkgName, err)
return "", false
}
// Next, read the appropriate facts.
var (
c Constants
s string
ok bool
)
if !pass.ImportPackageFact(pkg, &c) {
pass.Reportf(pos(), "constant package facts for %q are unavailable", pkg.Path())
return "", false
}
// Finally, format appropriately.
switch factName {
case "const":
s, ok = c.Values[objName]
case "align":
if v, vOk := c.Alignments[objName]; vOk {
s, ok = fmt.Sprintf("%d", v), true
}
case "offset":
if v, vOk := c.Offsets[objName]; vOk {
s, ok = fmt.Sprintf("%d", v), true
}
case "size":
if v, vOk := c.Sizes[objName]; vOk {
s, ok = fmt.Sprintf("%d", v), true
}
}
if !ok {
pass.Reportf(pos(), "fact of type %s unavailable for %q", factName, objName)
}
return s, ok
}
// checkAssembly checks assembly annotations.
func checkAssembly(pass *analysis.Pass) error {
for _, filename := range pass.OtherFiles {
if !strings.HasSuffix(filename, ".s") {
continue
}
content, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("unable to read assembly file: %w", err)
}
// This uses the technique to report issues for assembly files
// as described by the Go documentation:
// https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Pass
tf := pass.Fset.AddFile(filename, -1, len(content))
tf.SetLinesForContent(content)
lines := strings.Split(string(content), "\n")
for lineNumber, lineContent := range lines {
// N.B. This is not evaluated except lazily, since it
// will generate errors to attempt to grab the position
// at the end of input. Just avoid it.
pos := func() token.Pos {
return tf.LineStart(lineNumber + 1)
}
m, ok := matchRegexp(pass, pos, defineRegexp, lineContent)
if !ok {
continue // Already reported, if needed.
}
newValue, ok := buildExpected(pass, pos, m[3], m[4], m[5])
if !ok {
continue // Already reported.
}
// Convert our internal string to the given value. This essentially
// canonicalises the literal string provided in the assembly.
v, err := strconv.ParseInt(m[2], 10, 64)
if err == nil && fmt.Sprintf("%v", v) != newValue {
pass.Reportf(pos(), "got value %v, wanted %q", v, newValue)
continue
} else if err != nil && m[2] != newValue {
pass.Reportf(pos(), "got value %q, wanted %q", m[2], newValue)
continue
}
}
}
return nil
}
// checkConsts walks all package-level const objects.
func checkConsts(pass *analysis.Pass) error {
for _, f := range pass.Files {
for _, decl := range f.Decls {
d, ok := decl.(*ast.GenDecl)
if !ok || d.Tok != token.CONST {
continue
}
findComments := func(vs *ast.ValueSpec) []*ast.Comment {
comments := make([]*ast.Comment, 0)
if d.Doc != nil {
// Include any formally associated doc from the block.
comments = append(comments, d.Doc.List...)
}
if vs.Doc != nil {
// Include any formally associated comments from the value.
comments = append(comments, vs.Doc.List...)
}
for _, cg := range f.Comments {
for _, c := range cg.List {
// Include any comments that appear on the same line
// as the value spec itself, which are not doc comments.
specPosition := pass.Fset.Position(vs.Pos())
commentPosition := pass.Fset.Position(c.Pos())
if specPosition.Line == commentPosition.Line && specPosition.Column < commentPosition.Column {
comments = append(comments, c)
}
}
}
return comments
}
for _, spec := range d.Specs {
vs := spec.(*ast.ValueSpec)
var (
expectedValue string
expectedSet bool
)
for _, l := range findComments(vs) {
m, ok := matchRegexp(pass, l.Pos, constRegexp, l.Text)
if !ok {
continue // Already reported, if needed.
}
newValue, ok := buildExpected(pass, l.Pos, m[1], m[2], m[3])
if ok {
if expectedSet && newValue != expectedValue {
pass.Reportf(l.Pos(), "multiple conflicting values")
continue
}
expectedValue = newValue
expectedSet = true
}
}
if !expectedSet {
continue // Nothing was set.
}
// Format the expression.
for _, valueExpr := range vs.Values {
val := pass.TypesInfo.Types[valueExpr].Value
s := fmt.Sprint(val)
if s != expectedValue {
pass.Reportf(valueExpr.Pos(), "got value %q, wanted %q", s, expectedValue)
continue
}
}
}
}
}
return nil
}
func run(pass *analysis.Pass) (any, error) {
// Extract all local facts. This is done against the compiled objects,
// rather than the source-level analysis, which is done below.
extractFacts(pass)
// Check the local package.
if err := checkConsts(pass); err != nil {
return nil, err
}
if err := checkAssembly(pass); err != nil {
return nil, err
}
return nil, nil
}