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 }