// 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 !analysisutil.Imports(pass.Pkg, "runtime/cgo") { 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 inspect 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 // original "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() }