| // Copyright 2021 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 vulncheck |
| |
| import ( |
| "bytes" |
| "context" |
| "go/token" |
| "go/types" |
| "sort" |
| "strings" |
| |
| "golang.org/x/tools/go/callgraph" |
| "golang.org/x/tools/go/callgraph/cha" |
| "golang.org/x/tools/go/callgraph/vta" |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/go/types/typeutil" |
| "golang.org/x/vuln/internal" |
| "golang.org/x/vuln/internal/osv" |
| "golang.org/x/vuln/internal/semver" |
| |
| "golang.org/x/tools/go/ssa" |
| ) |
| |
| // buildSSA creates an ssa representation for pkgs. Returns |
| // the ssa program encapsulating the packages and top level |
| // ssa packages corresponding to pkgs. |
| func buildSSA(pkgs []*packages.Package, fset *token.FileSet) (*ssa.Program, []*ssa.Package) { |
| prog := ssa.NewProgram(fset, ssa.InstantiateGenerics) |
| |
| imports := make(map[*packages.Package]*ssa.Package) |
| var createImports func(map[string]*packages.Package) |
| createImports = func(pkgs map[string]*packages.Package) { |
| for _, p := range pkgs { |
| if _, ok := imports[p]; !ok { |
| i := prog.CreatePackage(p.Types, p.Syntax, p.TypesInfo, true) |
| imports[p] = i |
| createImports(p.Imports) |
| } |
| } |
| } |
| |
| for _, tp := range pkgs { |
| createImports(tp.Imports) |
| } |
| |
| var ssaPkgs []*ssa.Package |
| for _, tp := range pkgs { |
| if sp, ok := imports[tp]; ok { |
| ssaPkgs = append(ssaPkgs, sp) |
| } else { |
| sp := prog.CreatePackage(tp.Types, tp.Syntax, tp.TypesInfo, false) |
| ssaPkgs = append(ssaPkgs, sp) |
| } |
| } |
| prog.Build() |
| return prog, ssaPkgs |
| } |
| |
| // callGraph builds a call graph of prog based on VTA analysis. |
| func callGraph(ctx context.Context, prog *ssa.Program, entries []*ssa.Function) (*callgraph.Graph, error) { |
| entrySlice := make(map[*ssa.Function]bool) |
| for _, e := range entries { |
| entrySlice[e] = true |
| } |
| |
| if err := ctx.Err(); err != nil { // cancelled? |
| return nil, err |
| } |
| initial := cha.CallGraph(prog) |
| |
| fslice := forwardSlice(entrySlice, initial) |
| if err := ctx.Err(); err != nil { // cancelled? |
| return nil, err |
| } |
| vtaCg := vta.CallGraph(fslice, initial) |
| |
| // Repeat the process once more, this time using |
| // the produced VTA call graph as the base graph. |
| fslice = forwardSlice(entrySlice, vtaCg) |
| if err := ctx.Err(); err != nil { // cancelled? |
| return nil, err |
| } |
| cg := vta.CallGraph(fslice, vtaCg) |
| cg.DeleteSyntheticNodes() |
| return cg, nil |
| } |
| |
| // dbTypeFormat formats the name of t according how types |
| // are encoded in vulnerability database: |
| // - pointer designation * is skipped |
| // - full path prefix is skipped as well |
| func dbTypeFormat(t types.Type) string { |
| switch tt := t.(type) { |
| case *types.Pointer: |
| return dbTypeFormat(tt.Elem()) |
| case *types.Named: |
| return tt.Obj().Name() |
| default: |
| return types.TypeString(t, func(p *types.Package) string { return "" }) |
| } |
| } |
| |
| // dbFuncName computes a function name consistent with the namings used in vulnerability |
| // databases. Effectively, a qualified name of a function local to its enclosing package. |
| // If a receiver is a pointer, this information is not encoded in the resulting name. If |
| // a function has type argument/parameter, this information is omitted. The name of |
| // anonymous functions is simply "". The function names are unique subject to the enclosing |
| // package, but not globally. |
| // |
| // Examples: |
| // |
| // func (a A) foo (...) {...} -> A.foo |
| // func foo(...) {...} -> foo |
| // func (b *B) bar (...) {...} -> B.bar |
| // func (c C[T]) do(...) {...} -> C.do |
| func dbFuncName(f *ssa.Function) string { |
| selectBound := func(f *ssa.Function) types.Type { |
| // If f is a "bound" function introduced by ssa for a given type, return the type. |
| // When "f" is a "bound" function, it will have 1 free variable of that type within |
| // the function. This is subject to change when ssa changes. |
| if len(f.FreeVars) == 1 && strings.HasPrefix(f.Synthetic, "bound ") { |
| return f.FreeVars[0].Type() |
| } |
| return nil |
| } |
| selectThunk := func(f *ssa.Function) types.Type { |
| // If f is a "thunk" function introduced by ssa for a given type, return the type. |
| // When "f" is a "thunk" function, the first parameter will have that type within |
| // the function. This is subject to change when ssa changes. |
| params := f.Signature.Params() // params.Len() == 1 then params != nil. |
| if strings.HasPrefix(f.Synthetic, "thunk ") && params.Len() >= 1 { |
| if first := params.At(0); first != nil { |
| return first.Type() |
| } |
| } |
| return nil |
| } |
| var qprefix string |
| if recv := f.Signature.Recv(); recv != nil { |
| qprefix = dbTypeFormat(recv.Type()) |
| } else if btype := selectBound(f); btype != nil { |
| qprefix = dbTypeFormat(btype) |
| } else if ttype := selectThunk(f); ttype != nil { |
| qprefix = dbTypeFormat(ttype) |
| } |
| |
| if qprefix == "" { |
| return funcName(f) |
| } |
| return qprefix + "." + funcName(f) |
| } |
| |
| // funcName returns the name of the ssa function f. |
| // It is f.Name() without additional type argument |
| // information in case of generics. |
| func funcName(f *ssa.Function) string { |
| n, _, _ := strings.Cut(f.Name(), "[") |
| return n |
| } |
| |
| // memberFuncs returns functions associated with the `member`: |
| // 1) `member` itself if `member` is a function |
| // 2) `member` methods if `member` is a type |
| // 3) empty list otherwise |
| func memberFuncs(member ssa.Member, prog *ssa.Program) []*ssa.Function { |
| switch t := member.(type) { |
| case *ssa.Type: |
| methods := typeutil.IntuitiveMethodSet(t.Type(), &prog.MethodSets) |
| var funcs []*ssa.Function |
| for _, m := range methods { |
| if f := prog.MethodValue(m); f != nil { |
| funcs = append(funcs, f) |
| } |
| } |
| return funcs |
| case *ssa.Function: |
| return []*ssa.Function{t} |
| default: |
| return nil |
| } |
| } |
| |
| // funcPosition gives the position of `f`. Returns empty token.Position |
| // if no file information on `f` is available. |
| func funcPosition(f *ssa.Function) *token.Position { |
| pos := f.Prog.Fset.Position(f.Pos()) |
| return &pos |
| } |
| |
| // instrPosition gives the position of `instr`. Returns empty token.Position |
| // if no file information on `instr` is available. |
| func instrPosition(instr ssa.Instruction) *token.Position { |
| pos := instr.Parent().Prog.Fset.Position(instr.Pos()) |
| return &pos |
| } |
| |
| func resolved(call ssa.CallInstruction) bool { |
| if call == nil { |
| return true |
| } |
| return call.Common().StaticCallee() != nil |
| } |
| |
| func callRecvType(call ssa.CallInstruction) string { |
| if !call.Common().IsInvoke() { |
| return "" |
| } |
| buf := new(bytes.Buffer) |
| types.WriteType(buf, call.Common().Value.Type(), nil) |
| return buf.String() |
| } |
| |
| func funcRecvType(f *ssa.Function) string { |
| v := f.Signature.Recv() |
| if v == nil { |
| return "" |
| } |
| buf := new(bytes.Buffer) |
| types.WriteType(buf, v.Type(), nil) |
| return buf.String() |
| } |
| |
| func FixedVersion(modulePath, version string, affected []osv.Affected) string { |
| fixed := earliestValidFix(modulePath, version, affected) |
| // Add "v" prefix if one does not exist. moduleVersionString |
| // will later on replace it with "go" if needed. |
| if fixed != "" && !strings.HasPrefix(fixed, "v") { |
| fixed = "v" + fixed |
| } |
| return fixed |
| } |
| |
| // earliestValidFix returns the earliest fix for version of modulePath that |
| // itself is not vulnerable in affected. |
| // |
| // Suppose we have a version "v1.0.0" and we use {...} to denote different |
| // affected regions. Assume for simplicity that all affected apply to the |
| // same input modulePath. |
| // |
| // {[v0.1.0, v0.1.9), [v1.0.0, v2.0.0)} -> v2.0.0 |
| // {[v1.0.0, v1.5.0), [v2.0.0, v2.1.0}, {[v1.4.0, v1.6.0)} -> v2.1.0 |
| func earliestValidFix(modulePath, version string, affected []osv.Affected) string { |
| var moduleAffected []osv.Affected |
| for _, a := range affected { |
| if a.Module.Path == modulePath { |
| moduleAffected = append(moduleAffected, a) |
| } |
| } |
| |
| vFixes := validFixes(version, moduleAffected) |
| for _, fix := range vFixes { |
| if !fixNegated(fix, moduleAffected) { |
| return fix |
| } |
| } |
| return "" |
| |
| } |
| |
| // validFixes computes all fixes for version in affected and |
| // returns them sorted increasingly. Assumes that all affected |
| // apply to the same module. |
| func validFixes(version string, affected []osv.Affected) []string { |
| var fixes []string |
| for _, a := range affected { |
| for _, r := range a.Ranges { |
| if r.Type != osv.RangeTypeSemver { |
| continue |
| } |
| for _, e := range r.Events { |
| fix := e.Fixed |
| if fix != "" && semver.Less(version, fix) { |
| fixes = append(fixes, fix) |
| } |
| } |
| } |
| } |
| sort.SliceStable(fixes, func(i, j int) bool { return semver.Less(fixes[i], fixes[j]) }) |
| return fixes |
| } |
| |
| // fixNegated checks if fix is negated to by a re-introduction |
| // of a vulnerability in affected. Assumes that all affected apply |
| // to the same module. |
| func fixNegated(fix string, affected []osv.Affected) bool { |
| for _, a := range affected { |
| for _, r := range a.Ranges { |
| if semver.ContainsSemver(r, fix) { |
| return true |
| } |
| } |
| } |
| return false |
| } |
| |
| func modPath(mod *packages.Module) string { |
| if mod.Replace != nil { |
| return mod.Replace.Path |
| } |
| return mod.Path |
| } |
| |
| func modVersion(mod *packages.Module) string { |
| if mod.Replace != nil { |
| return mod.Replace.Version |
| } |
| return mod.Version |
| } |
| |
| // pkgPath returns the path of the f's enclosing package, if any. |
| // Otherwise, returns internal.UnknownPackagePath. |
| func pkgPath(f *ssa.Function) string { |
| g := f |
| if f.Origin() != nil { |
| // Instantiations of generics do not have |
| // an associated package. We hence look up |
| // the original function for the package. |
| g = f.Origin() |
| } |
| if g.Package() != nil && g.Package().Pkg != nil { |
| return g.Package().Pkg.Path() |
| } |
| return internal.UnknownPackagePath |
| } |
| |
| func pkgModPath(pkg *packages.Package) string { |
| if pkg != nil && pkg.Module != nil { |
| return pkg.Module.Path |
| } |
| return internal.UnknownModulePath |
| } |
| |
| func IsStdPackage(pkg string) bool { |
| if pkg == "" || pkg == internal.UnknownPackagePath { |
| return false |
| } |
| // std packages do not have a "." in their path. For instance, see |
| // Contains in pkgsite/+/refs/heads/master/internal/stdlbib/stdlib.go. |
| if i := strings.IndexByte(pkg, '/'); i != -1 { |
| pkg = pkg[:i] |
| } |
| return !strings.Contains(pkg, ".") |
| } |