1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
|
// 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 defines an Analyzer that detects some violations of
// the cgo pointer passing rules.
package cgocall
import (
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"go/types"
"log"
"os"
"strconv"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
)
const debug = false
const 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.`
var Analyzer = &analysis.Analyzer{
Name: "cgocall",
Doc: doc,
RunDespiteErrors: true,
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
if imports(pass.Pkg, "runtime/cgo") == nil {
return nil, nil // doesn't use cgo
}
cgofiles, info, err := typeCheckCgoSourceFiles(pass.Fset, pass.Pkg, pass.Files, pass.TypesInfo, pass.TypesSizes)
if err != nil {
return nil, err
}
for _, f := range cgofiles {
checkCgo(pass.Fset, f, info, pass.Reportf)
}
return nil, nil
}
func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(token.Pos, string, ...interface{})) {
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
// Is this a C.f() call?
var name string
if sel, ok := analysisutil.Unparen(call.Fun).(*ast.SelectorExpr); ok {
if id, ok := sel.X.(*ast.Ident); ok && id.Name == "C" {
name = sel.Sel.Name
}
}
if name == "" {
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 debug {
log.Printf("%s: call to C.%s", fset.Position(call.Lparen), name)
}
for _, arg := range call.Args {
if !typeOKForCgoCall(cgoBaseType(info, arg), make(map[types.Type]bool)) {
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(info, conv.Fun) {
arg = conv.Args[0]
}
if u, ok := arg.(*ast.UnaryExpr); ok && u.Op == token.AND {
if !typeOKForCgoCall(cgoBaseType(info, u.X), make(map[types.Type]bool)) {
reportf(arg.Pos(), "possibly passing Go type with embedded pointer to C")
break
}
}
}
return true
})
}
// typeCheckCgoSourceFiles returns type-checked syntax trees for the raw
// cgo files of a package (those that import "C"). Such files are not
// Go, so there may be gaps in type information around C.f references.
//
// This checker was initially written in vet to inpect raw cgo source
// files using partial type information. However, Analyzers in the new
// analysis API are presented with the type-checked, "cooked" Go ASTs
// resulting from cgo-processing files, so we must choose between
// working with the cooked file generated by cgo (which was tried but
// proved fragile) or locating the raw cgo file (e.g. from //line
// directives) and working with that, as we now do.
//
// Specifically, we must type-check the raw cgo source files (or at
// least the subtrees needed for this analyzer) in an environment that
// simulates the rest of the already type-checked package.
//
// For example, for each raw cgo source file in the original package,
// such as this one:
//
// package p
// import "C"
// import "fmt"
// type T int
// const k = 3
// var x, y = fmt.Println()
// func f() { ... }
// func g() { ... C.malloc(k) ... }
// func (T) f(int) string { ... }
//
// we synthesize a new ast.File, shown below, that dot-imports the
// orginal "cooked" package using a special name ("·this·"), so that all
// references to package members resolve correctly. (References to
// unexported names cause an "unexported" error, which we ignore.)
//
// To avoid shadowing names imported from the cooked package,
// package-level declarations in the new source file are modified so
// that they do not declare any names.
// (The cgocall analysis is concerned with uses, not declarations.)
// Specifically, type declarations are discarded;
// all names in each var and const declaration are blanked out;
// each method is turned into a regular function by turning
// the receiver into the first parameter;
// and all functions are renamed to "_".
//
// package p
// import . "·this·" // declares T, k, x, y, f, g, T.f
// import "C"
// import "fmt"
// const _ = 3
// var _, _ = fmt.Println()
// func _() { ... }
// func _() { ... C.malloc(k) ... }
// func _(T, int) string { ... }
//
// In this way, the raw function bodies and const/var initializer
// expressions are preserved but refer to the "cooked" objects imported
// from "·this·", and none of the transformed package-level declarations
// actually declares anything. In the example above, the reference to k
// in the argument of the call to C.malloc resolves to "·this·".k, which
// has an accurate type.
//
// This approach could in principle be generalized to more complex
// analyses on raw cgo files. One could synthesize a "C" package so that
// C.f would resolve to "·this·"._C_func_f, for example. But we have
// limited ourselves here to preserving function bodies and initializer
// expressions since that is all that the cgocall analyzer needs.
//
func typeCheckCgoSourceFiles(fset *token.FileSet, pkg *types.Package, files []*ast.File, info *types.Info, sizes types.Sizes) ([]*ast.File, *types.Info, error) {
const thispkg = "·this·"
// Which files are cgo files?
var cgoFiles []*ast.File
importMap := map[string]*types.Package{thispkg: pkg}
for _, raw := range files {
// If f is a cgo-generated file, Position reports
// the original file, honoring //line directives.
filename := fset.Position(raw.Pos()).Filename
f, err := parser.ParseFile(fset, filename, nil, parser.Mode(0))
if err != nil {
return nil, nil, fmt.Errorf("can't parse raw cgo file: %v", err)
}
found := false
for _, spec := range f.Imports {
if spec.Path.Value == `"C"` {
found = true
break
}
}
if !found {
continue // not a cgo file
}
// Record the original import map.
for _, spec := range raw.Imports {
path, _ := strconv.Unquote(spec.Path.Value)
importMap[path] = imported(info, spec)
}
// Add special dot-import declaration:
// import . "·this·"
var decls []ast.Decl
decls = append(decls, &ast.GenDecl{
Tok: token.IMPORT,
Specs: []ast.Spec{
&ast.ImportSpec{
Name: &ast.Ident{Name: "."},
Path: &ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote(thispkg),
},
},
},
})
// Transform declarations from the raw cgo file.
for _, decl := range f.Decls {
switch decl := decl.(type) {
case *ast.GenDecl:
switch decl.Tok {
case token.TYPE:
// Discard type declarations.
continue
case token.IMPORT:
// Keep imports.
case token.VAR, token.CONST:
// Blank the declared var/const names.
for _, spec := range decl.Specs {
spec := spec.(*ast.ValueSpec)
for i := range spec.Names {
spec.Names[i].Name = "_"
}
}
}
case *ast.FuncDecl:
// Blank the declared func name.
decl.Name.Name = "_"
// Turn a method receiver: func (T) f(P) R {...}
// into regular parameter: func _(T, P) R {...}
if decl.Recv != nil {
var params []*ast.Field
params = append(params, decl.Recv.List...)
params = append(params, decl.Type.Params.List...)
decl.Type.Params.List = params
decl.Recv = nil
}
}
decls = append(decls, decl)
}
f.Decls = decls
if debug {
format.Node(os.Stderr, fset, f) // debugging
}
cgoFiles = append(cgoFiles, f)
}
if cgoFiles == nil {
return nil, nil, nil // nothing to do (can't happen?)
}
// Type-check the synthetic files.
tc := &types.Config{
FakeImportC: true,
Importer: importerFunc(func(path string) (*types.Package, error) {
return importMap[path], nil
}),
Sizes: sizes,
Error: func(error) {}, // ignore errors (e.g. unused import)
}
// It's tempting to record the new types in the
// existing pass.TypesInfo, but we don't own it.
altInfo := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
}
tc.Check(pkg.Path(), fset, cgoFiles, altInfo)
return cgoFiles, altInfo, 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]
}
type importerFunc func(path string) (*types.Package, error)
func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
// TODO(adonovan): make this a library function or method of Info.
func imported(info *types.Info, spec *ast.ImportSpec) *types.Package {
obj, ok := info.Implicits[spec]
if !ok {
obj = info.Defs[spec.Name] // renaming import
}
return obj.(*types.PkgName).Imported()
}
// imports reports whether pkg has path among its direct imports.
// It returns the imported package if so, or nil if not.
// TODO(adonovan): move to analysisutil.
func imports(pkg *types.Package, path string) *types.Package {
for _, imp := range pkg.Imports() {
if imp.Path() == path {
return imp
}
}
return nil
}
|