go/analysis/passes/cgocall: split out of vet
This checker needed some reworking because whereas vet sees
unprocessed cgo source files (with partial type information), the
analysis API sees cgo-processed files with complete type information.
However, that means the checker must effectively undo some of the
transformations done by cgo, making it more fragile during changes to
cgo.
Change-Id: I3a243260f59b16e2e546e8f3e4585b93d3731192
Reviewed-on: https://go-review.googlesource.com/c/141157
Reviewed-by: Michael Matloob <matloob@golang.org>
diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go
index 83e1a6a..25062c6 100644
--- a/go/analysis/analysistest/analysistest.go
+++ b/go/analysis/analysistest/analysistest.go
@@ -184,7 +184,6 @@
// Extract 'want' comments from Go files.
for _, f := range pass.Files {
- filename := sanitize(gopath, pass.Fset.File(f.Pos()).Name())
for _, cgroup := range f.Comments {
for _, c := range cgroup.List {
@@ -201,13 +200,19 @@
text = text[i+len("// "):]
}
- linenum := pass.Fset.Position(c.Pos()).Line
- processComment(filename, linenum, text)
+ // It's tempting to compute the filename
+ // once outside the loop, but it's
+ // incorrect because it can change due
+ // to //line directives.
+ posn := pass.Fset.Position(c.Pos())
+ filename := sanitize(gopath, posn.Filename)
+ processComment(filename, posn.Line, text)
}
}
}
// Extract 'want' comments from non-Go files.
+ // TODO(adonovan): we may need to handle //line directives.
for _, filename := range pass.OtherFiles {
data, err := ioutil.ReadFile(filename)
if err != nil {
@@ -285,6 +290,12 @@
}
// Reject surplus expectations.
+ //
+ // Sometimes an Analyzer reports two similar diagnostics on a
+ // line with only one expectation. The reader may be confused by
+ // the error message.
+ // TODO(adonovan): print a better error:
+ // "got 2 diagnostics here; each one needs its own expectation".
var surplus []string
for key, expects := range want {
for _, exp := range expects {
diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go
new file mode 100644
index 0000000..daf65b9
--- /dev/null
+++ b/go/analysis/passes/cgocall/cgocall.go
@@ -0,0 +1,222 @@
+// Copyright 2015 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.
+
+package cgocall
+
+import (
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "log"
+ "strings"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/inspector"
+)
+
+var Analyzer = &analysis.Analyzer{
+ Name: "cgocall",
+ Doc: `detect some violations of the cgo pointer passing rules
+
+Check for invalid cgo pointer passing.
+This looks for code that uses cgo to call C code passing values
+whose types are almost always invalid according to the cgo pointer
+sharing rules.
+Specifically, it warns about attempts to pass a Go chan, map, func,
+or slice to C, either directly, or via a pointer, array, or struct.`,
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+ RunDespiteErrors: true,
+ Run: run,
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+
+ nodeFilter := []ast.Node{
+ (*ast.CallExpr)(nil),
+ }
+ inspect.WithStack(nodeFilter, func(n ast.Node, push bool, stack []ast.Node) bool {
+ if !push {
+ return true
+ }
+ call, name := findCall(pass.Fset, stack)
+ if call == nil {
+ return true // not a call we need to check
+ }
+
+ // A call to C.CBytes passes a pointer but is always safe.
+ if name == "CBytes" {
+ return true
+ }
+
+ if false {
+ fmt.Printf("%s: inner call to C.%s\n", pass.Fset.Position(n.Pos()), name)
+ fmt.Printf("%s: outer call to C.%s\n", pass.Fset.Position(call.Lparen), name)
+ }
+
+ for _, arg := range call.Args {
+ if !typeOKForCgoCall(cgoBaseType(pass.TypesInfo, arg), make(map[types.Type]bool)) {
+ pass.Reportf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
+ break
+ }
+
+ // Check for passing the address of a bad type.
+ if conv, ok := arg.(*ast.CallExpr); ok && len(conv.Args) == 1 &&
+ isUnsafePointer(pass.TypesInfo, conv.Fun) {
+ arg = conv.Args[0]
+ }
+ if u, ok := arg.(*ast.UnaryExpr); ok && u.Op == token.AND {
+ if !typeOKForCgoCall(cgoBaseType(pass.TypesInfo, u.X), make(map[types.Type]bool)) {
+ pass.Reportf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
+ break
+ }
+ }
+ }
+ return true
+ })
+ return nil, nil
+}
+
+// findCall returns the CallExpr that we need to check, which may not be
+// the same as the one we're currently visiting, due to code generation.
+// It also returns the name of the function, such as "f" for C.f(...).
+//
+// This checker was initially written in vet to inpect unprocessed cgo
+// source files using partial type information. However, Analyzers in
+// the new analysis API are presented with the type-checked, processed
+// Go ASTs resulting from cgo processing files, so we must choose
+// between:
+//
+// a) locating the cgo file (e.g. from //line directives)
+// and working with that, or
+// b) working with the file generated by cgo.
+//
+// We cannot use (a) because it does not provide type information, which
+// the analyzer needs, and it is infeasible for the analyzer to run the
+// type checker on this file. Thus we choose (b), which is fragile,
+// because the checker may need to change each time the cgo processor
+// changes.
+//
+// Consider a cgo source file containing this header:
+//
+// /* void f(void *x, *y); */
+// import "C"
+//
+// The cgo tool expands a call such as:
+//
+// C.f(x, y)
+//
+// to this:
+//
+// 1 func(param0, param1 unsafe.Pointer) {
+// 2 ... various checks on params ...
+// 3 (_Cfunc_f)(param0, param1)
+// 4 }(x, y)
+//
+// We first locate the _Cfunc_f call on line 3, then
+// walk up the stack of enclosing nodes until we find
+// the call on line 4.
+//
+func findCall(fset *token.FileSet, stack []ast.Node) (*ast.CallExpr, string) {
+ last := len(stack) - 1
+ call := stack[last].(*ast.CallExpr)
+ if id, ok := analysisutil.Unparen(call.Fun).(*ast.Ident); ok {
+ if name := strings.TrimPrefix(id.Name, "_Cfunc_"); name != id.Name {
+ // Find the outer call with the arguments (x, y) we want to check.
+ for i := last - 1; i >= 0; i-- {
+ if outer, ok := stack[i].(*ast.CallExpr); ok {
+ return outer, name
+ }
+ }
+ // This shouldn't happen.
+ // Perhaps the code generator has changed?
+ log.Printf("%s: can't find outer call for C.%s(...)",
+ fset.Position(call.Lparen), name)
+ }
+ }
+ return nil, ""
+}
+
+// cgoBaseType tries to look through type conversions involving
+// unsafe.Pointer to find the real type. It converts:
+// unsafe.Pointer(x) => x
+// *(*unsafe.Pointer)(unsafe.Pointer(&x)) => x
+func cgoBaseType(info *types.Info, arg ast.Expr) types.Type {
+ switch arg := arg.(type) {
+ case *ast.CallExpr:
+ if len(arg.Args) == 1 && isUnsafePointer(info, arg.Fun) {
+ return cgoBaseType(info, arg.Args[0])
+ }
+ case *ast.StarExpr:
+ call, ok := arg.X.(*ast.CallExpr)
+ if !ok || len(call.Args) != 1 {
+ break
+ }
+ // Here arg is *f(v).
+ t := info.Types[call.Fun].Type
+ if t == nil {
+ break
+ }
+ ptr, ok := t.Underlying().(*types.Pointer)
+ if !ok {
+ break
+ }
+ // Here arg is *(*p)(v)
+ elem, ok := ptr.Elem().Underlying().(*types.Basic)
+ if !ok || elem.Kind() != types.UnsafePointer {
+ break
+ }
+ // Here arg is *(*unsafe.Pointer)(v)
+ call, ok = call.Args[0].(*ast.CallExpr)
+ if !ok || len(call.Args) != 1 {
+ break
+ }
+ // Here arg is *(*unsafe.Pointer)(f(v))
+ if !isUnsafePointer(info, call.Fun) {
+ break
+ }
+ // Here arg is *(*unsafe.Pointer)(unsafe.Pointer(v))
+ u, ok := call.Args[0].(*ast.UnaryExpr)
+ if !ok || u.Op != token.AND {
+ break
+ }
+ // Here arg is *(*unsafe.Pointer)(unsafe.Pointer(&v))
+ return cgoBaseType(info, u.X)
+ }
+
+ return info.Types[arg].Type
+}
+
+// typeOKForCgoCall reports whether the type of arg is OK to pass to a
+// C function using cgo. This is not true for Go types with embedded
+// pointers. m is used to avoid infinite recursion on recursive types.
+func typeOKForCgoCall(t types.Type, m map[types.Type]bool) bool {
+ if t == nil || m[t] {
+ return true
+ }
+ m[t] = true
+ switch t := t.Underlying().(type) {
+ case *types.Chan, *types.Map, *types.Signature, *types.Slice:
+ return false
+ case *types.Pointer:
+ return typeOKForCgoCall(t.Elem(), m)
+ case *types.Array:
+ return typeOKForCgoCall(t.Elem(), m)
+ case *types.Struct:
+ for i := 0; i < t.NumFields(); i++ {
+ if !typeOKForCgoCall(t.Field(i).Type(), m) {
+ return false
+ }
+ }
+ }
+ return true
+}
+
+func isUnsafePointer(info *types.Info, e ast.Expr) bool {
+ t := info.Types[e].Type
+ return t != nil && t.Underlying() == types.Typ[types.UnsafePointer]
+}
diff --git a/go/analysis/passes/cgocall/cgocall_test.go b/go/analysis/passes/cgocall/cgocall_test.go
new file mode 100644
index 0000000..db22818
--- /dev/null
+++ b/go/analysis/passes/cgocall/cgocall_test.go
@@ -0,0 +1,13 @@
+package cgocall_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+ "golang.org/x/tools/go/analysis/passes/cgocall"
+)
+
+func TestFromFileSystem(t *testing.T) {
+ testdata := analysistest.TestData()
+ analysistest.Run(t, testdata, cgocall.Analyzer, "a", "b")
+}
diff --git a/go/analysis/passes/cgocall/testdata/src/a/cgo.go b/go/analysis/passes/cgocall/testdata/src/a/cgo.go
new file mode 100644
index 0000000..39ad897
--- /dev/null
+++ b/go/analysis/passes/cgocall/testdata/src/a/cgo.go
@@ -0,0 +1,59 @@
+// Copyright 2015 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.
+
+// This file contains tests for the cgo checker.
+
+package a
+
+// void f(void *ptr) {}
+import "C"
+
+import "unsafe"
+
+func CgoTests() {
+ var c chan bool
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&c))) // want "embedded pointer"
+ C.f(unsafe.Pointer(&c)) // want "embedded pointer"
+
+ var m map[string]string
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&m))) // want "embedded pointer"
+ C.f(unsafe.Pointer(&m)) // want "embedded pointer"
+
+ var f func()
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&f))) // want "embedded pointer"
+ C.f(unsafe.Pointer(&f)) // want "embedded pointer"
+
+ var s []int
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&s))) // want "embedded pointer"
+ C.f(unsafe.Pointer(&s)) // want "embedded pointer"
+
+ var a [1][]int
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&a))) // want "embedded pointer"
+ C.f(unsafe.Pointer(&a)) // want "embedded pointer"
+
+ var st struct{ f []int }
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&st))) // want "embedded pointer"
+ C.f(unsafe.Pointer(&st)) // want "embedded pointer"
+
+ // The following cases are OK.
+ var i int
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&i)))
+ C.f(unsafe.Pointer(&i))
+
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&s[0])))
+ C.f(unsafe.Pointer(&s[0]))
+
+ var a2 [1]int
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&a2)))
+ C.f(unsafe.Pointer(&a2))
+
+ var st2 struct{ i int }
+ C.f(*(*unsafe.Pointer)(unsafe.Pointer(&st2)))
+ C.f(unsafe.Pointer(&st2))
+
+ type cgoStruct struct{ p *cgoStruct }
+ C.f(unsafe.Pointer(&cgoStruct{}))
+
+ C.CBytes([]byte("hello"))
+}
diff --git a/go/analysis/passes/vet/testdata/cgo/cgo2.go b/go/analysis/passes/cgocall/testdata/src/a/cgo2.go
similarity index 71%
rename from go/analysis/passes/vet/testdata/cgo/cgo2.go
rename to go/analysis/passes/cgocall/testdata/src/a/cgo2.go
index 4f27116..a624afb 100644
--- a/go/analysis/passes/vet/testdata/cgo/cgo2.go
+++ b/go/analysis/passes/cgocall/testdata/src/a/cgo2.go
@@ -4,9 +4,11 @@
// Test the cgo checker on a file that doesn't use cgo.
-package testdata
+package a
-var _ = C.f(*p(**p))
+import "unsafe"
// Passing a pointer (via the slice), but C isn't cgo.
-var _ = C.f([]int{3})
+var _ = C.f(unsafe.Pointer(new([]int)))
+
+var C struct{ f func(interface{}) int }
diff --git a/go/analysis/passes/vet/testdata/cgo/cgo3.go b/go/analysis/passes/cgocall/testdata/src/a/cgo3.go
similarity index 63%
rename from go/analysis/passes/vet/testdata/cgo/cgo3.go
rename to go/analysis/passes/cgocall/testdata/src/a/cgo3.go
index 0b1518e..fd09516 100644
--- a/go/analysis/passes/vet/testdata/cgo/cgo3.go
+++ b/go/analysis/passes/cgocall/testdata/src/a/cgo3.go
@@ -2,10 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Used by TestVetVerbose to test that vet -v doesn't fail because it
-// can't find "C".
-
-package testdata
+package a
import "C"
diff --git a/go/analysis/passes/vet/testdata/cgo/cgo4.go b/go/analysis/passes/cgocall/testdata/src/a/cgo4.go
similarity index 95%
rename from go/analysis/passes/vet/testdata/cgo/cgo4.go
rename to go/analysis/passes/cgocall/testdata/src/a/cgo4.go
index 67b5450..fc1d1e5 100644
--- a/go/analysis/passes/vet/testdata/cgo/cgo4.go
+++ b/go/analysis/passes/cgocall/testdata/src/a/cgo4.go
@@ -5,7 +5,7 @@
// Test the cgo checker on a file that doesn't use cgo, but has an
// import named "C".
-package testdata
+package a
import C "fmt"
diff --git a/go/analysis/passes/vet/testdata/cgo/cgo4.go b/go/analysis/passes/cgocall/testdata/src/b/b.go
similarity index 95%
copy from go/analysis/passes/vet/testdata/cgo/cgo4.go
copy to go/analysis/passes/cgocall/testdata/src/b/b.go
index 67b5450..342b14b 100644
--- a/go/analysis/passes/vet/testdata/cgo/cgo4.go
+++ b/go/analysis/passes/cgocall/testdata/src/b/b.go
@@ -5,7 +5,7 @@
// Test the cgo checker on a file that doesn't use cgo, but has an
// import named "C".
-package testdata
+package b
import C "fmt"
diff --git a/go/analysis/passes/vet/cgo.go b/go/analysis/passes/vet/cgo.go
deleted file mode 100644
index 693dac9..0000000
--- a/go/analysis/passes/vet/cgo.go
+++ /dev/null
@@ -1,143 +0,0 @@
-// +build ignore
-
-// Copyright 2015 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.
-
-// Check for invalid cgo pointer passing.
-// This looks for code that uses cgo to call C code passing values
-// whose types are almost always invalid according to the cgo pointer
-// sharing rules.
-// Specifically, it warns about attempts to pass a Go chan, map, func,
-// or slice to C, either directly, or via a pointer, array, or struct.
-
-package main
-
-import (
- "go/ast"
- "go/token"
- "go/types"
-)
-
-func init() {
- register("cgocall",
- "check for types that may not be passed to cgo calls",
- checkCgoCall,
- callExpr)
-}
-
-func checkCgoCall(f *File, node ast.Node) {
- x := node.(*ast.CallExpr)
-
- // We are only looking for calls to functions imported from
- // the "C" package.
- sel, ok := x.Fun.(*ast.SelectorExpr)
- if !ok {
- return
- }
- id, ok := sel.X.(*ast.Ident)
- if !ok {
- return
- }
-
- pkgname, ok := f.pkg.uses[id].(*types.PkgName)
- if !ok || pkgname.Imported().Path() != "C" {
- return
- }
-
- // A call to C.CBytes passes a pointer but is always safe.
- if sel.Sel.Name == "CBytes" {
- return
- }
-
- for _, arg := range x.Args {
- if !typeOKForCgoCall(cgoBaseType(f, arg), make(map[types.Type]bool)) {
- f.Badf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
- }
-
- // Check for passing the address of a bad type.
- if conv, ok := arg.(*ast.CallExpr); ok && len(conv.Args) == 1 && f.hasBasicType(conv.Fun, types.UnsafePointer) {
- arg = conv.Args[0]
- }
- if u, ok := arg.(*ast.UnaryExpr); ok && u.Op == token.AND {
- if !typeOKForCgoCall(cgoBaseType(f, u.X), make(map[types.Type]bool)) {
- f.Badf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
- }
- }
- }
-}
-
-// cgoBaseType tries to look through type conversions involving
-// unsafe.Pointer to find the real type. It converts:
-// unsafe.Pointer(x) => x
-// *(*unsafe.Pointer)(unsafe.Pointer(&x)) => x
-func cgoBaseType(f *File, arg ast.Expr) types.Type {
- switch arg := arg.(type) {
- case *ast.CallExpr:
- if len(arg.Args) == 1 && f.hasBasicType(arg.Fun, types.UnsafePointer) {
- return cgoBaseType(f, arg.Args[0])
- }
- case *ast.StarExpr:
- call, ok := arg.X.(*ast.CallExpr)
- if !ok || len(call.Args) != 1 {
- break
- }
- // Here arg is *f(v).
- t := f.pkg.types[call.Fun].Type
- if t == nil {
- break
- }
- ptr, ok := t.Underlying().(*types.Pointer)
- if !ok {
- break
- }
- // Here arg is *(*p)(v)
- elem, ok := ptr.Elem().Underlying().(*types.Basic)
- if !ok || elem.Kind() != types.UnsafePointer {
- break
- }
- // Here arg is *(*unsafe.Pointer)(v)
- call, ok = call.Args[0].(*ast.CallExpr)
- if !ok || len(call.Args) != 1 {
- break
- }
- // Here arg is *(*unsafe.Pointer)(f(v))
- if !f.hasBasicType(call.Fun, types.UnsafePointer) {
- break
- }
- // Here arg is *(*unsafe.Pointer)(unsafe.Pointer(v))
- u, ok := call.Args[0].(*ast.UnaryExpr)
- if !ok || u.Op != token.AND {
- break
- }
- // Here arg is *(*unsafe.Pointer)(unsafe.Pointer(&v))
- return cgoBaseType(f, u.X)
- }
-
- return f.pkg.types[arg].Type
-}
-
-// typeOKForCgoCall reports whether the type of arg is OK to pass to a
-// C function using cgo. This is not true for Go types with embedded
-// pointers. m is used to avoid infinite recursion on recursive types.
-func typeOKForCgoCall(t types.Type, m map[types.Type]bool) bool {
- if t == nil || m[t] {
- return true
- }
- m[t] = true
- switch t := t.Underlying().(type) {
- case *types.Chan, *types.Map, *types.Signature, *types.Slice:
- return false
- case *types.Pointer:
- return typeOKForCgoCall(t.Elem(), m)
- case *types.Array:
- return typeOKForCgoCall(t.Elem(), m)
- case *types.Struct:
- for i := 0; i < t.NumFields(); i++ {
- if !typeOKForCgoCall(t.Field(i).Type(), m) {
- return false
- }
- }
- }
- return true
-}
diff --git a/go/analysis/passes/vet/testdata/cgo/cgo.go b/go/analysis/passes/vet/testdata/cgo/cgo.go
deleted file mode 100644
index d0df7cf..0000000
--- a/go/analysis/passes/vet/testdata/cgo/cgo.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2015 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.
-
-// This file contains tests for the cgo checker.
-
-package testdata
-
-// void f(void *);
-import "C"
-
-import "unsafe"
-
-func CgoTests() {
- var c chan bool
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&c))) // ERROR "embedded pointer"
- C.f(unsafe.Pointer(&c)) // ERROR "embedded pointer"
-
- var m map[string]string
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&m))) // ERROR "embedded pointer"
- C.f(unsafe.Pointer(&m)) // ERROR "embedded pointer"
-
- var f func()
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&f))) // ERROR "embedded pointer"
- C.f(unsafe.Pointer(&f)) // ERROR "embedded pointer"
-
- var s []int
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&s))) // ERROR "embedded pointer"
- C.f(unsafe.Pointer(&s)) // ERROR "embedded pointer"
-
- var a [1][]int
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&a))) // ERROR "embedded pointer"
- C.f(unsafe.Pointer(&a)) // ERROR "embedded pointer"
-
- var st struct{ f []int }
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&st))) // ERROR "embedded pointer"
- C.f(unsafe.Pointer(&st)) // ERROR "embedded pointer"
-
- // The following cases are OK.
- var i int
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&i)))
- C.f(unsafe.Pointer(&i))
-
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&s[0])))
- C.f(unsafe.Pointer(&s[0]))
-
- var a2 [1]int
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&a2)))
- C.f(unsafe.Pointer(&a2))
-
- var st2 struct{ i int }
- C.f(*(*unsafe.Pointer)(unsafe.Pointer(&st2)))
- C.f(unsafe.Pointer(&st2))
-
- type cgoStruct struct{ p *cgoStruct }
- C.f(unsafe.Pointer(&cgoStruct{}))
-
- C.CBytes([]byte("hello"))
-}