src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

witness.go (12786B)


      1 // Copyright 2021 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package vulncheck
      6 
      7 import (
      8 	"container/list"
      9 	"fmt"
     10 	"go/ast"
     11 	"go/token"
     12 	"sort"
     13 	"strconv"
     14 	"strings"
     15 	"sync"
     16 	"unicode"
     17 
     18 	"golang.org/x/tools/go/packages"
     19 )
     20 
     21 // CallStack is a call stack starting with a client
     22 // function or method and ending with a call to a
     23 // vulnerable symbol.
     24 type CallStack []StackEntry
     25 
     26 // StackEntry is an element of a call stack.
     27 type StackEntry struct {
     28 	// Function whose frame is on the stack.
     29 	Function *FuncNode
     30 
     31 	// Call is the call site inducing the next stack frame.
     32 	// nil when the frame represents the last frame in the stack.
     33 	Call *CallSite
     34 }
     35 
     36 // sourceCallstacks returns representative call stacks for each
     37 // vulnerability in res. The returned call stacks are heuristically
     38 // ordered by how seemingly easy is to understand them: shorter
     39 // call stacks with less dynamic call sites appear earlier in the
     40 // returned slices.
     41 //
     42 // sourceCallstacks performs a breadth-first search of res.CallGraph
     43 // starting at the vulnerable symbol and going up until reaching an entry
     44 // function or method in res.CallGraph.Entries. During this search,
     45 // each function is visited at most once to avoid potential
     46 // exponential explosion. Hence, not all call stacks are analyzed.
     47 func sourceCallstacks(res *Result) map[*Vuln]CallStack {
     48 	var (
     49 		wg sync.WaitGroup
     50 		mu sync.Mutex
     51 	)
     52 	stackPerVuln := make(map[*Vuln]CallStack)
     53 	for _, vuln := range res.Vulns {
     54 		vuln := vuln
     55 		wg.Add(1)
     56 		go func() {
     57 			cs := sourceCallstack(vuln, res)
     58 			mu.Lock()
     59 			stackPerVuln[vuln] = cs
     60 			mu.Unlock()
     61 			wg.Done()
     62 		}()
     63 	}
     64 	wg.Wait()
     65 
     66 	updateInitPositions(stackPerVuln)
     67 	return stackPerVuln
     68 }
     69 
     70 // sourceCallstack finds a representative call stack for vuln.
     71 // This is a shortest unique call stack with the least
     72 // number of dynamic call sites.
     73 func sourceCallstack(vuln *Vuln, res *Result) CallStack {
     74 	vulnSink := vuln.CallSink
     75 	if vulnSink == nil {
     76 		return nil
     77 	}
     78 
     79 	entries := make(map[*FuncNode]bool)
     80 	for _, e := range res.EntryFunctions {
     81 		entries[e] = true
     82 	}
     83 
     84 	seen := make(map[*FuncNode]bool)
     85 
     86 	// Do a BFS from the vuln sink to the entry points
     87 	// and find the representative call stack. This is
     88 	// the shortest call stack that goes through the
     89 	// least number of dynamic call sites. We first
     90 	// collect all candidate call stacks of the shortest
     91 	// length and then pick the best one accordingly.
     92 	var candidates []CallStack
     93 	candDepth := 0
     94 	queue := list.New()
     95 	queue.PushBack(&callChain{f: vulnSink})
     96 
     97 	// We want to avoid call stacks that go through
     98 	// other vulnerable symbols of the same package
     99 	// for the same vulnerability. In other words,
    100 	// we want unique call stacks.
    101 	skipSymbols := make(map[*FuncNode]bool)
    102 	for _, v := range res.Vulns {
    103 		if v.CallSink != nil && v != vuln &&
    104 			v.OSV == vuln.OSV && v.Package == vuln.Package {
    105 			skipSymbols[v.CallSink] = true
    106 		}
    107 	}
    108 
    109 	for queue.Len() > 0 {
    110 		front := queue.Front()
    111 		c := front.Value.(*callChain)
    112 		queue.Remove(front)
    113 
    114 		f := c.f
    115 		if seen[f] {
    116 			continue
    117 		}
    118 		seen[f] = true
    119 
    120 		// Pick a single call site for each function in determinstic order.
    121 		// A single call site is sufficient as we visit a function only once.
    122 		for _, cs := range callsites(f.CallSites, seen) {
    123 			nStack := &callChain{f: cs.Parent, call: cs, child: c}
    124 			if !skipSymbols[cs.Parent] {
    125 				queue.PushBack(nStack)
    126 			}
    127 
    128 			if entries[cs.Parent] {
    129 				ns := nStack.CallStack()
    130 				if len(candidates) == 0 || len(ns) == candDepth {
    131 					// The case where we either have not identified
    132 					// any call stacks or just found one of the same
    133 					// length as the previous ones.
    134 					candidates = append(candidates, ns)
    135 					candDepth = len(ns)
    136 				} else {
    137 					// We just found a candidate call stack whose
    138 					// length is greater than what we previously
    139 					// found. We can thus safely disregard this
    140 					// call stack and stop searching since we won't
    141 					// be able to find any better candidates.
    142 					queue.Init() // clear the list, effectively exiting the outer loop
    143 				}
    144 			}
    145 		}
    146 	}
    147 
    148 	// Sort candidate call stacks by their number of dynamic call
    149 	// sites and return the first one.
    150 	sort.SliceStable(candidates, func(i int, j int) bool {
    151 		s1, s2 := candidates[i], candidates[j]
    152 		if w1, w2 := weight(s1), weight(s2); w1 != w2 {
    153 			return w1 < w2
    154 		}
    155 
    156 		// At this point, the stableness/determinism of
    157 		// sorting is guaranteed by the determinism of
    158 		// the underlying call graph and the call stack
    159 		// search algorithm.
    160 		return true
    161 	})
    162 	if len(candidates) == 0 {
    163 		return nil
    164 	}
    165 	return candidates[0]
    166 }
    167 
    168 // callsites picks a call site from sites for each non-visited function.
    169 // For each such function, the smallest (posLess) call site is chosen. The
    170 // returned slice is sorted by caller functions (funcLess). Assumes callee
    171 // of each call site is the same.
    172 func callsites(sites []*CallSite, visited map[*FuncNode]bool) []*CallSite {
    173 	minCs := make(map[*FuncNode]*CallSite)
    174 	for _, cs := range sites {
    175 		if visited[cs.Parent] {
    176 			continue
    177 		}
    178 		if csLess(cs, minCs[cs.Parent]) {
    179 			minCs[cs.Parent] = cs
    180 		}
    181 	}
    182 
    183 	var fs []*FuncNode
    184 	for _, cs := range minCs {
    185 		fs = append(fs, cs.Parent)
    186 	}
    187 	sort.SliceStable(fs, func(i, j int) bool { return funcLess(fs[i], fs[j]) })
    188 
    189 	var css []*CallSite
    190 	for _, f := range fs {
    191 		css = append(css, minCs[f])
    192 	}
    193 	return css
    194 }
    195 
    196 // callChain models a chain of function calls.
    197 type callChain struct {
    198 	call  *CallSite // nil for entry points
    199 	f     *FuncNode
    200 	child *callChain
    201 }
    202 
    203 // CallStack converts callChain to CallStack type.
    204 func (c *callChain) CallStack() CallStack {
    205 	if c == nil {
    206 		return nil
    207 	}
    208 	return append(CallStack{StackEntry{Function: c.f, Call: c.call}}, c.child.CallStack()...)
    209 }
    210 
    211 // weight computes an approximate measure of how easy is to understand the call
    212 // stack when presented to the client as a witness. The smaller the value, the more
    213 // understandable the stack is. Currently defined as the number of unresolved
    214 // call sites in the stack.
    215 func weight(stack CallStack) int {
    216 	w := 0
    217 	for _, e := range stack {
    218 		if e.Call != nil && !e.Call.Resolved {
    219 			w += 1
    220 		}
    221 	}
    222 	return w
    223 }
    224 
    225 // csLess compares two call sites by their locations and, if needed,
    226 // their string representation.
    227 func csLess(cs1, cs2 *CallSite) bool {
    228 	if cs2 == nil {
    229 		return true
    230 	}
    231 
    232 	// fast code path
    233 	if p1, p2 := cs1.Pos, cs2.Pos; p1 != nil && p2 != nil {
    234 		if posLess(*p1, *p2) {
    235 			return true
    236 		}
    237 		if posLess(*p2, *p1) {
    238 			return false
    239 		}
    240 		// for sanity, should not occur in practice
    241 		return fmt.Sprintf("%v.%v", cs1.RecvType, cs2.Name) < fmt.Sprintf("%v.%v", cs2.RecvType, cs2.Name)
    242 	}
    243 
    244 	// code path rarely exercised
    245 	if cs2.Pos == nil {
    246 		return true
    247 	}
    248 	if cs1.Pos == nil {
    249 		return false
    250 	}
    251 	// should very rarely occur in practice
    252 	return fmt.Sprintf("%v.%v", cs1.RecvType, cs2.Name) < fmt.Sprintf("%v.%v", cs2.RecvType, cs2.Name)
    253 }
    254 
    255 // posLess compares two positions by their line and column number,
    256 // and filename if needed.
    257 func posLess(p1, p2 token.Position) bool {
    258 	if p1.Line < p2.Line {
    259 		return true
    260 	}
    261 	if p2.Line < p1.Line {
    262 		return false
    263 	}
    264 
    265 	if p1.Column < p2.Column {
    266 		return true
    267 	}
    268 	if p2.Column < p1.Column {
    269 		return false
    270 	}
    271 
    272 	return strings.Compare(p1.Filename, p2.Filename) == -1
    273 }
    274 
    275 // funcLess compares two function nodes by locations of
    276 // corresponding functions and, if needed, their string representation.
    277 func funcLess(f1, f2 *FuncNode) bool {
    278 	if p1, p2 := f1.Pos, f2.Pos; p1 != nil && p2 != nil {
    279 		if posLess(*p1, *p2) {
    280 			return true
    281 		}
    282 		if posLess(*p2, *p1) {
    283 			return false
    284 		}
    285 		// for sanity, should not occur in practice
    286 		return f1.String() < f2.String()
    287 	}
    288 
    289 	if f2.Pos == nil {
    290 		return true
    291 	}
    292 	if f1.Pos == nil {
    293 		return false
    294 	}
    295 	// should happen only for inits
    296 	return f1.String() < f2.String()
    297 }
    298 
    299 // updateInitPositions populates non-existing positions of init functions
    300 // and their respective calls in callStacks (see #51575).
    301 func updateInitPositions(callStacks map[*Vuln]CallStack) {
    302 	for _, cs := range callStacks {
    303 		for i := range cs {
    304 			updateInitPosition(&cs[i])
    305 			if i != len(cs)-1 {
    306 				updateInitCallPosition(&cs[i], cs[i+1])
    307 			}
    308 		}
    309 	}
    310 }
    311 
    312 // updateInitCallPosition updates the position of a call to init in a stack frame, if
    313 // one already does not exist:
    314 //
    315 //	P1.init -> P2.init: position of call to P2.init is the position of "import P2"
    316 //	statement in P1
    317 //
    318 //	P.init -> P.init#d: P.init is an implicit init. We say it calls the explicit
    319 //	P.init#d at the place of "package P" statement.
    320 func updateInitCallPosition(curr *StackEntry, next StackEntry) {
    321 	call := curr.Call
    322 	if !isInit(next.Function) || (call.Pos != nil && call.Pos.IsValid()) {
    323 		// Skip non-init functions and inits whose call site position is available.
    324 		return
    325 	}
    326 
    327 	var pos token.Position
    328 	if curr.Function.Name == "init" && curr.Function.Package == next.Function.Package {
    329 		// We have implicit P.init calling P.init#d. Set the call position to
    330 		// be at "package P" statement position.
    331 		pos = packageStatementPos(curr.Function.Package)
    332 	} else {
    333 		// Choose the beginning of the import statement as the position.
    334 		pos = importStatementPos(curr.Function.Package, next.Function.Package.PkgPath)
    335 	}
    336 
    337 	call.Pos = &pos
    338 }
    339 
    340 func importStatementPos(pkg *packages.Package, importPath string) token.Position {
    341 	var importSpec *ast.ImportSpec
    342 spec:
    343 	for _, f := range pkg.Syntax {
    344 		for _, impSpec := range f.Imports {
    345 			// Import spec paths have quotation marks.
    346 			impSpecPath, err := strconv.Unquote(impSpec.Path.Value)
    347 			if err != nil {
    348 				panic(fmt.Sprintf("import specification: package path has no quotation marks: %v", err))
    349 			}
    350 			if impSpecPath == importPath {
    351 				importSpec = impSpec
    352 				break spec
    353 			}
    354 		}
    355 	}
    356 
    357 	if importSpec == nil {
    358 		// for sanity, in case of a wild call graph imprecision
    359 		return token.Position{}
    360 	}
    361 
    362 	// Choose the beginning of the import statement as the position.
    363 	return pkg.Fset.Position(importSpec.Pos())
    364 }
    365 
    366 func packageStatementPos(pkg *packages.Package) token.Position {
    367 	if len(pkg.Syntax) == 0 {
    368 		return token.Position{}
    369 	}
    370 	// Choose beginning of the package statement as the position. Pick
    371 	// the first file since it is as good as any.
    372 	return pkg.Fset.Position(pkg.Syntax[0].Package)
    373 }
    374 
    375 // updateInitPosition updates the position of P.init function in a stack frame if one
    376 // is not available. The new position is the position of the "package P" statement.
    377 func updateInitPosition(se *StackEntry) {
    378 	fun := se.Function
    379 	if !isInit(fun) || (fun.Pos != nil && fun.Pos.IsValid()) {
    380 		// Skip non-init functions and inits whose position is available.
    381 		return
    382 	}
    383 
    384 	pos := packageStatementPos(fun.Package)
    385 	fun.Pos = &pos
    386 }
    387 
    388 func isInit(f *FuncNode) bool {
    389 	// A source init function, or anonymous functions used in inits, will
    390 	// be named "init#x" by vulncheck (more precisely, ssa), where x is a
    391 	// positive integer. Implicit inits are named simply "init".
    392 	return f.Name == "init" || strings.HasPrefix(f.Name, "init#")
    393 }
    394 
    395 // binaryCallstacks computes representative call stacks for binary results.
    396 func binaryCallstacks(vr *Result) map[*Vuln]CallStack {
    397 	callstacks := map[*Vuln]CallStack{}
    398 	for _, vv := range uniqueVulns(vr.Vulns) {
    399 		f := &FuncNode{Package: vv.Package, Name: vv.Symbol}
    400 		parts := strings.Split(vv.Symbol, ".")
    401 		if len(parts) != 1 {
    402 			f.RecvType = parts[0]
    403 			f.Name = parts[1]
    404 		}
    405 		callstacks[vv] = CallStack{StackEntry{Function: f}}
    406 	}
    407 	return callstacks
    408 }
    409 
    410 // uniqueVulns does for binary mode what sourceCallstacks does for source mode.
    411 // It tries not to report redundant symbols. Since there are no call stacks in
    412 // binary mode, the following approximate approach is used. Do not report unexported
    413 // symbols for a <vulnID, pkg, module> triple if there are some exported symbols.
    414 // Otherwise, report all unexported symbols to avoid not reporting anything.
    415 func uniqueVulns(vulns []*Vuln) []*Vuln {
    416 	type key struct {
    417 		id  string
    418 		pkg string
    419 		mod string
    420 	}
    421 	hasExported := make(map[key]bool)
    422 	for _, v := range vulns {
    423 		if isExported(v.Symbol) {
    424 			k := key{id: v.OSV.ID, pkg: v.Package.PkgPath, mod: v.Package.Module.Path}
    425 			hasExported[k] = true
    426 		}
    427 	}
    428 
    429 	var uniques []*Vuln
    430 	for _, v := range vulns {
    431 		k := key{id: v.OSV.ID, pkg: v.Package.PkgPath, mod: v.Package.Module.Path}
    432 		if isExported(v.Symbol) || !hasExported[k] {
    433 			uniques = append(uniques, v)
    434 		}
    435 	}
    436 	return uniques
    437 }
    438 
    439 // isExported checks if the symbol is exported. Assumes that the
    440 // symbol is of the form "identifier", "identifier1.identifier2",
    441 // or "identifier.".
    442 func isExported(symbol string) bool {
    443 	parts := strings.Split(symbol, ".")
    444 	last := parts[len(parts)-1]
    445 	if last == "" { // case for "identifier."
    446 		return false
    447 	}
    448 	return unicode.IsUpper(rune(last[0]))
    449 }