gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/tools/checkescape/checkescape.go (about) 1 // Copyright 2020 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package checkescape allows recursive escape analysis for hot paths. 16 // 17 // The analysis tracks multiple types of escapes, in two categories. First, 18 // 'hard' escapes are explicit allocations. Second, 'soft' escapes are 19 // interface dispatches or dynamic function dispatches; these don't necessarily 20 // escape but they *may* escape. The analysis is capable of making assertions 21 // recursively: soft escapes cannot be analyzed in this way, and therefore 22 // count as escapes for recursive purposes. 23 // 24 // The different types of escapes are as follows, with the category in 25 // parentheses: 26 // 27 // heap: A direct allocation is made on the heap (hard). 28 // builtin: A call is made to a built-in allocation function (hard). 29 // stack: A stack split as part of a function preamble (soft). 30 // interface: A call is made via an interface which *may* escape (soft). 31 // dynamic: A dynamic function is dispatched which *may* escape (soft). 32 // 33 // To the use the package, annotate a function-level comment with either the 34 // line "// +checkescape" or "// +checkescape:OPTION[,OPTION]". In the second 35 // case, the OPTION field is either a type above, or one of: 36 // 37 // local: Escape analysis is limited to local hard escapes only. 38 // all: All the escapes are included. 39 // hard: All hard escapes are included. 40 // 41 // If the "// +checkescape" annotation is provided, this is equivalent to 42 // provided the local and hard options. 43 // 44 // Some examples of this syntax are: 45 // 46 // +checkescape:all - Analyzes for all escapes in this function and all calls. 47 // +checkescape:local - Analyzes only for default local hard escapes. 48 // +checkescape:heap - Only analyzes for heap escapes. 49 // +checkescape:interface,dynamic - Only checks for dynamic calls and interface calls. 50 // +checkescape - Does the same as +checkescape:local,hard. 51 // 52 // Note that all of the above can be inverted by using +mustescape. The 53 // +checkescape keyword will ensure failure if the class of escape occurs, 54 // whereas +mustescape will fail if the given class of escape does not occur. 55 // 56 // Local exemptions can be made by a comment of the form "// escapes: reason." 57 // This must appear on the line of the escape and will also apply to callers of 58 // the function as well (for non-local escape analysis). 59 package checkescape 60 61 import ( 62 "bufio" 63 "bytes" 64 "fmt" 65 "go/ast" 66 "go/token" 67 "go/types" 68 "io" 69 "io/ioutil" 70 "os" 71 "os/exec" 72 "path/filepath" 73 "strings" 74 "sync" 75 76 "golang.org/x/tools/go/analysis" 77 "golang.org/x/tools/go/analysis/passes/buildssa" 78 "golang.org/x/tools/go/ssa" 79 "gvisor.dev/gvisor/tools/nogo/flags" 80 ) 81 82 const ( 83 // magic is the magic annotation. 84 magic = "// +checkescape" 85 86 // Bad versions of `magic` observed in the wilderness of the codebase. 87 badMagicNoSpace = "//+checkescape" 88 badMagicPlural = "// +checkescapes" 89 90 // magicParams is the magic annotation with specific parameters. 91 magicParams = magic + ":" 92 93 // testMagic is the test magic annotation (parameters required). 94 testMagic = "// +mustescape:" 95 96 // exempt is the exemption annotation. 97 exempt = "// escapes" 98 ) 99 100 // EscapeReason is an escape reason. 101 // 102 // This is a simple enum. 103 type EscapeReason int 104 105 const ( 106 allocation EscapeReason = iota 107 builtin 108 interfaceInvoke 109 dynamicCall 110 stackSplit 111 unknownPackage 112 reasonCount // Count for below. 113 ) 114 115 // String returns the string for the EscapeReason. 116 // 117 // Note that this also implicitly defines the reverse string -> EscapeReason 118 // mapping, which is the word before the colon (computed below). 119 func (e EscapeReason) String() string { 120 switch e { 121 case interfaceInvoke: 122 return "interface: call to potentially allocating function" 123 case unknownPackage: 124 return "unknown: no package information available" 125 case allocation: 126 return "heap: explicit allocation" 127 case builtin: 128 return "builtin: call to potentially allocating builtin" 129 case dynamicCall: 130 return "dynamic: call to potentially allocating function" 131 case stackSplit: 132 return "stack: possible split on function entry" 133 default: 134 panic(fmt.Sprintf("unknown reason: %d", e)) 135 } 136 } 137 138 var hardReasons = []EscapeReason{ 139 allocation, 140 builtin, 141 } 142 143 var softReasons = []EscapeReason{ 144 interfaceInvoke, 145 unknownPackage, 146 dynamicCall, 147 stackSplit, 148 } 149 150 var allReasons = append(hardReasons, softReasons...) 151 152 var escapeTypes = func() map[string]EscapeReason { 153 result := make(map[string]EscapeReason) 154 for _, r := range allReasons { 155 parts := strings.Split(r.String(), ":") 156 result[parts[0]] = r // Key before ':'. 157 } 158 return result 159 }() 160 161 // escapingBuiltins are builtins known to escape. 162 // 163 // These are lowered at an earlier stage of compilation to explicit function 164 // calls, but are not available for recursive analysis. 165 var escapingBuiltins = []string{ 166 "append", 167 "makemap", 168 "newobject", 169 "mallocgc", 170 } 171 172 // objdumpAnalyzer accepts the objdump parameter. 173 type objdumpAnalyzer struct { 174 analysis.Analyzer 175 } 176 177 // Run implements nogo.binaryAnalyzer.Run. 178 func (ob *objdumpAnalyzer) Run(pass *analysis.Pass, binary io.Reader) (any, error) { 179 return run(pass, binary) 180 } 181 182 // Legacy implements nogo.analyzer.Legacy. 183 func (ob *objdumpAnalyzer) Legacy() *analysis.Analyzer { 184 return &ob.Analyzer 185 } 186 187 // Analyzer includes specific results. 188 var Analyzer = &objdumpAnalyzer{ 189 Analyzer: analysis.Analyzer{ 190 Name: "checkescape", 191 Doc: "escape analysis checks based on +checkescape annotations", 192 Run: nil, // Must be invoked via Run above. 193 Requires: []*analysis.Analyzer{buildssa.Analyzer}, 194 FactTypes: []analysis.Fact{(*Escapes)(nil)}, 195 }, 196 } 197 198 // LinePosition is a low-resolution token.Position. 199 // 200 // This is used to match against possible exemptions placed in the source. 201 type LinePosition struct { 202 Filename string 203 Line int 204 } 205 206 // String implements fmt.Stringer.String. 207 func (e LinePosition) String() string { 208 return fmt.Sprintf("%s:%d", e.Filename, e.Line) 209 } 210 211 // Simplified returns the simplified name. 212 func (e LinePosition) Simplified() string { 213 return fmt.Sprintf("%s:%d", filepath.Base(e.Filename), e.Line) 214 } 215 216 // CallSite is a single call site. 217 // 218 // These can be chained. 219 type CallSite struct { 220 LocalPos token.Pos 221 Resolved LinePosition 222 } 223 224 // IsValid indicates whether the CallSite is valid or not. 225 func (cs *CallSite) IsValid() bool { 226 return cs.LocalPos.IsValid() 227 } 228 229 // Escapes is a collection of escapes. 230 // 231 // We record at most one escape for each reason, but record the number of 232 // escapes that were omitted. 233 // 234 // This object should be used to summarize all escapes for a single line (local 235 // analysis) or a single function (package facts). 236 // 237 // All fields are exported for gob. 238 type Escapes struct { 239 CallSites [reasonCount][]CallSite 240 Details [reasonCount]string 241 Omitted [reasonCount]int 242 } 243 244 // AFact implements analysis.Fact.AFact. 245 func (*Escapes) AFact() {} 246 247 // add is called by Add and Merge. 248 func (es *Escapes) add(r EscapeReason, detail string, omitted int, callSites ...CallSite) { 249 if es.CallSites[r] != nil { 250 // We will either be replacing the current escape or dropping 251 // the added one. Either way, we increment omitted by the 252 // appropriate amount. 253 es.Omitted[r]++ 254 // If the callSites in the other is only a single element, then 255 // we will universally favor this. This provides the cleanest 256 // set of escapes to summarize, and more importantly: if there 257 if len(es.CallSites) == 1 || len(callSites) != 1 { 258 return 259 } 260 } 261 es.Details[r] = detail 262 es.CallSites[r] = callSites 263 es.Omitted[r] += omitted 264 } 265 266 // Add adds a single escape. 267 func (es *Escapes) Add(r EscapeReason, detail string, callSites ...CallSite) { 268 es.add(r, detail, 0, callSites...) 269 } 270 271 // IsEmpty returns true iff this Escapes is empty. 272 func (es *Escapes) IsEmpty() bool { 273 for _, cs := range es.CallSites { 274 if cs != nil { 275 return false 276 } 277 } 278 return true 279 } 280 281 // Filter filters out all escapes except those matches the given reasons. 282 // 283 // If local is set, then non-local escapes will also be filtered. 284 func (es *Escapes) Filter(reasons []EscapeReason, local bool) { 285 FilterReasons: 286 for r := EscapeReason(0); r < reasonCount; r++ { 287 for i := 0; i < len(reasons); i++ { 288 if r == reasons[i] { 289 continue FilterReasons 290 } 291 } 292 // Zap this reason. 293 es.CallSites[r] = nil 294 es.Details[r] = "" 295 es.Omitted[r] = 0 296 } 297 if !local { 298 return 299 } 300 for r := EscapeReason(0); r < reasonCount; r++ { 301 // Is does meet our local requirement? 302 if len(es.CallSites[r]) > 1 { 303 es.CallSites[r] = nil 304 es.Details[r] = "" 305 es.Omitted[r] = 0 306 } 307 } 308 } 309 310 // MergeWithCall merges these escapes with another. 311 // 312 // If callSite is nil, no call is added. 313 func (es *Escapes) MergeWithCall(other Escapes, callSite CallSite) { 314 for r := EscapeReason(0); r < reasonCount; r++ { 315 if other.CallSites[r] != nil { 316 // Construct our new call chain. 317 newCallSites := other.CallSites[r] 318 if callSite.IsValid() { 319 newCallSites = append([]CallSite{callSite}, newCallSites...) 320 } 321 // Add (potentially replacing) the underlying escape. 322 es.add(r, other.Details[r], other.Omitted[r], newCallSites...) 323 } 324 } 325 } 326 327 // Reportf will call Reportf for each class of escapes. 328 func (es *Escapes) Reportf(pass *analysis.Pass) { 329 var b bytes.Buffer // Reused for all escapes. 330 for r := EscapeReason(0); r < reasonCount; r++ { 331 if es.CallSites[r] == nil { 332 continue 333 } 334 b.Reset() 335 fmt.Fprintf(&b, "%s ", r.String()) 336 if es.Omitted[r] > 0 { 337 fmt.Fprintf(&b, "(%d omitted) ", es.Omitted[r]) 338 } 339 for _, cs := range es.CallSites[r][1:] { 340 fmt.Fprintf(&b, "→ %s ", cs.Resolved.String()) 341 } 342 fmt.Fprintf(&b, "→ %s", es.Details[r]) 343 pass.Reportf(es.CallSites[r][0].LocalPos, b.String()) 344 } 345 } 346 347 // MergeAll merges a sequence of escapes. 348 func MergeAll(others []Escapes) (es Escapes) { 349 for _, other := range others { 350 es.MergeWithCall(other, CallSite{}) 351 } 352 return 353 } 354 355 // loadObjdump reads the objdump output. 356 // 357 // This records if there is a call any function for every source line. It is 358 // used only to remove false positives for escape analysis. The call will be 359 // elided if escape analysis is able to put the object on the heap exclusively. 360 // 361 // Note that the map uses <basename.go>:<line> because that is all that is 362 // provided in the objdump format. Since this is all local, it is sufficient. 363 func loadObjdump(binary io.Reader) (finalResults map[string][]string, finalErr error) { 364 // Do we have a binary? If it's missing, then the nil will simply be 365 // plumbed all the way down here. 366 if binary == nil { 367 return nil, fmt.Errorf("no binary provided") 368 } 369 370 // Construct & start our command. The 'go tool objdump' command 371 // requires a seekable input passed on the command line. Therefore, we 372 // may need to generate a temporary file here. 373 input, ok := binary.(*os.File) 374 if ok { 375 // Ensure that the file is seekable and that the offset is 376 // zero, since we can't control that. 377 if offset, err := input.Seek(0, os.SEEK_CUR); err != nil || offset != 0 { 378 ok = false // Not usable. 379 } 380 } 381 if !ok { 382 // Copy to a temporary path. 383 f, err := ioutil.TempFile("", "") 384 if err != nil { 385 return nil, fmt.Errorf("unable to create temp file: %w", err) 386 } 387 // Ensure the file is deleted. 388 defer os.Remove(f.Name()) 389 // Populate the file contents. 390 if _, err := io.Copy(f, binary); err != nil { 391 return nil, fmt.Errorf("unable to populate temp file: %w", err) 392 } 393 // Seek to the beginning. 394 if _, err := f.Seek(0, os.SEEK_SET); err != nil { 395 return nil, fmt.Errorf("unable to seek in temp file: %w", err) 396 } 397 input = f 398 } 399 400 // Execute go tool objdump ggiven the input. 401 cmd := exec.Command(flags.Go, "tool", "objdump", input.Name()) 402 pipeOut, err := cmd.StdoutPipe() 403 if err != nil { 404 return nil, fmt.Errorf("unable to load objdump: %w", err) 405 } 406 defer pipeOut.Close() 407 pipeErr, err := cmd.StderrPipe() 408 if err != nil { 409 return nil, fmt.Errorf("unable to load objdump: %w", err) 410 } 411 defer pipeErr.Close() 412 if startErr := cmd.Start(); startErr != nil { 413 return nil, fmt.Errorf("unable to start objdump: %w", startErr) 414 } 415 416 // Ensure that the command has finished successfully. Note that even if 417 // we parse the first few lines correctly, and early exit could 418 // indicate that the dump was incomplete and we could be missed some 419 // escapes that would have appeared. We need to force failure. 420 defer func() { 421 var ( 422 wg sync.WaitGroup 423 buf bytes.Buffer 424 ) 425 wg.Add(1) 426 go func() { 427 defer wg.Done() 428 io.Copy(&buf, pipeErr) 429 }() 430 waitErr := cmd.Wait() 431 wg.Wait() 432 if finalErr == nil && waitErr != nil { 433 // Override the function's return value in this case. 434 finalErr = fmt.Errorf("error running objdump %s: %v (%s)", input.Name(), waitErr, buf.Bytes()) 435 } 436 }() 437 438 // Identify calls by address or name. Note that the list of allowed addresses 439 // -- not the list of allowed function names -- is also constructed 440 // dynamically below, as we encounter the addresses. This is because some of 441 // the functions (duffzero) may have jump targets in the middle of the 442 // function itself. 443 funcsAllowed := map[string]struct{}{ 444 "runtime.duffzero": {}, 445 "runtime.duffcopy": {}, 446 "runtime.racefuncenter": {}, 447 "runtime.gcWriteBarrier": {}, 448 "runtime.retpolineAX": {}, 449 "runtime.retpolineBP": {}, 450 "runtime.retpolineBX": {}, 451 "runtime.retpolineCX": {}, 452 "runtime.retpolineDI": {}, 453 "runtime.retpolineDX": {}, 454 "runtime.retpolineR10": {}, 455 "runtime.retpolineR11": {}, 456 "runtime.retpolineR12": {}, 457 "runtime.retpolineR13": {}, 458 "runtime.retpolineR14": {}, 459 "runtime.retpolineR15": {}, 460 "runtime.retpolineR8": {}, 461 "runtime.retpolineR9": {}, 462 "runtime.retpolineSI": {}, 463 "runtime.stackcheck": {}, 464 "runtime.settls": {}, 465 } 466 // addrsAllowed lists every address that can be jumped to within the 467 // funcsAllowed functions. 468 addrsAllowed := make(map[string]struct{}) 469 470 // Build the map. 471 nextFunc := "" // For funcsAllowed. 472 m := make(map[string][]string) 473 r := bufio.NewReader(pipeOut) 474 NextLine: 475 for { 476 line, err := r.ReadString('\n') 477 if err != nil && err != io.EOF { 478 return nil, err 479 } 480 fields := strings.Fields(line) 481 482 // Is this an "allowed" function definition? If so, record every address of 483 // the function body. 484 if len(fields) >= 2 && fields[0] == "TEXT" { 485 nextFunc = strings.TrimSuffix(fields[1], "(SB)") 486 if _, ok := funcsAllowed[nextFunc]; !ok { 487 nextFunc = "" // Don't record addresses. 488 } 489 } 490 if nextFunc != "" && len(fields) > 2 { 491 // We're inside an allowed function. Save the given address (in hex form, 492 // as it appears). 493 addrsAllowed[fields[1]] = struct{}{} 494 } 495 496 // We recognize lines corresponding to actual code (not the 497 // symbol name or other metadata) and annotate them if they 498 // correspond to an explicit CALL instruction. We assume that 499 // the lack of a CALL for a given line is evidence that escape 500 // analysis has eliminated an allocation. 501 // 502 // Lines look like this (including the first space): 503 // gohacks_unsafe.go:33 0xa39 488b442408 MOVQ 0x8(SP), AX 504 if len(fields) >= 5 && line[0] == ' ' { 505 if !strings.Contains(fields[3], "CALL") { 506 continue 507 } 508 site := fields[0] 509 target := strings.TrimSuffix(fields[4], "(SB)") 510 target, err := fixOffset(fields, target) 511 if err != nil { 512 return nil, err 513 } 514 515 // Ignore strings containing allowed functions. 516 if _, ok := funcsAllowed[target]; ok { 517 continue 518 } 519 if _, ok := addrsAllowed[target]; ok { 520 continue 521 } 522 if len(fields) > 5 { 523 // This may be a future relocation. Some 524 // objdump versions describe this differently. 525 // If it contains any of the functions allowed 526 // above as a string, we let it go. 527 softTarget := strings.Join(fields[5:], " ") 528 for name := range funcsAllowed { 529 if strings.Contains(softTarget, name) { 530 continue NextLine 531 } 532 } 533 } 534 535 // Does this exist already? 536 existing, ok := m[site] 537 if !ok { 538 existing = make([]string, 0, 1) 539 } 540 for _, other := range existing { 541 if target == other { 542 continue NextLine 543 } 544 } 545 existing = append(existing, target) 546 m[site] = existing // Update. 547 } 548 if err == io.EOF { 549 break 550 } 551 } 552 553 // Zap any accidental false positives. 554 final := make(map[string][]string) 555 for site, calls := range m { 556 filteredCalls := make([]string, 0, len(calls)) 557 for _, call := range calls { 558 if _, ok := addrsAllowed[call]; ok { 559 continue // Omit this call. 560 } 561 filteredCalls = append(filteredCalls, call) 562 } 563 final[site] = filteredCalls 564 } 565 566 return final, nil 567 } 568 569 // poser is a type that implements Pos. 570 type poser interface { 571 Pos() token.Pos 572 } 573 574 // findReasons extracts reasons from the function. 575 func findReasons(pass *analysis.Pass, fdecl *ast.FuncDecl) ([]EscapeReason, bool, map[EscapeReason]bool) { 576 // Is there a comment? 577 if fdecl.Doc == nil { 578 return nil, false, nil 579 } 580 var ( 581 reasons []EscapeReason 582 local bool 583 testReasons = make(map[EscapeReason]bool) // reason -> local? 584 ) 585 // Scan all lines. 586 found := false 587 for _, c := range fdecl.Doc.List { 588 if strings.HasPrefix(c.Text, badMagicNoSpace) || strings.HasPrefix(c.Text, badMagicPlural) { 589 pass.Reportf(fdecl.Pos(), "misspelled checkescape prefix: please use %q instead", magic) 590 continue 591 } 592 // Does the comment contain a +checkescape line? 593 if !strings.HasPrefix(c.Text, magic) && !strings.HasPrefix(c.Text, testMagic) { 594 continue 595 } 596 if c.Text == magic { 597 // Default: hard reasons, local only. 598 reasons = hardReasons 599 local = true 600 } else if strings.HasPrefix(c.Text, magicParams) { 601 // Extract specific reasons. 602 types := strings.Split(c.Text[len(magicParams):], ",") 603 found = true // For below. 604 for i := 0; i < len(types); i++ { 605 if types[i] == "local" { 606 // Limit search to local escapes. 607 local = true 608 } else if types[i] == "all" { 609 // Append all reasons. 610 reasons = append(reasons, allReasons...) 611 } else if types[i] == "hard" { 612 // Append all hard reasons. 613 reasons = append(reasons, hardReasons...) 614 } else { 615 r, ok := escapeTypes[types[i]] 616 if !ok { 617 // This is not a valid escape reason. 618 pass.Reportf(fdecl.Pos(), "unknown reason: %v", types[i]) 619 continue 620 } 621 reasons = append(reasons, r) 622 } 623 } 624 } else if strings.HasPrefix(c.Text, testMagic) { 625 types := strings.Split(c.Text[len(testMagic):], ",") 626 local := false 627 for i := 0; i < len(types); i++ { 628 if types[i] == "local" { 629 local = true 630 } else { 631 r, ok := escapeTypes[types[i]] 632 if !ok { 633 // This is not a valid escape reason. 634 pass.Reportf(fdecl.Pos(), "unknown reason: %v", types[i]) 635 continue 636 } 637 if v, ok := testReasons[r]; ok && v { 638 // Already registered as local. 639 continue 640 } 641 testReasons[r] = local 642 } 643 } 644 } 645 } 646 if len(reasons) == 0 && found { 647 // A magic annotation was provided, but no reasons. 648 pass.Reportf(fdecl.Pos(), "no reasons provided") 649 } 650 return reasons, local, testReasons 651 } 652 653 // run performs the analysis. 654 func run(pass *analysis.Pass, binary io.Reader) (any, error) { 655 // Note that if this analysis fails, then we don't actually 656 // fail the analyzer itself. We simply report every possible 657 // escape. In most cases this will work just fine. 658 calls, callsErr := loadObjdump(binary) 659 allEscapes := make(map[string][]Escapes) 660 mergedEscapes := make(map[string]Escapes) 661 linePosition := func(inst, parent poser) LinePosition { 662 p := pass.Fset.Position(inst.Pos()) 663 if (p.Filename == "" || p.Line == 0) && parent != nil { 664 p = pass.Fset.Position(parent.Pos()) 665 } 666 return LinePosition{ 667 Filename: p.Filename, 668 Line: p.Line, 669 } 670 } 671 callSite := func(inst ssa.Instruction) CallSite { 672 return CallSite{ 673 LocalPos: inst.Pos(), 674 Resolved: linePosition(inst, inst.Parent()), 675 } 676 } 677 hasCall := func(inst poser) (string, bool) { 678 p := linePosition(inst, nil) 679 if callsErr != nil { 680 // See above: we don't have access to the binary 681 // itself, so need to include every possible call. 682 return fmt.Sprintf("(possible, unable to load objdump: %v)", callsErr), true 683 } 684 s, ok := calls[p.Simplified()] 685 if !ok { 686 return "", false 687 } 688 // Join all calls together. 689 return strings.Join(s, " or "), true 690 } 691 state := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) 692 693 // Build the exception list. 694 exemptions := make(map[LinePosition]string) 695 for _, f := range pass.Files { 696 for _, cg := range f.Comments { 697 for _, c := range cg.List { 698 p := pass.Fset.Position(c.Slash) 699 if strings.HasPrefix(strings.ToLower(c.Text), exempt) { 700 exemptions[LinePosition{ 701 Filename: p.Filename, 702 Line: p.Line, 703 }] = c.Text[len(exempt):] 704 } 705 } 706 } 707 } 708 709 var loadFunc func(*ssa.Function) Escapes // Used below. 710 analyzeInstruction := func(inst ssa.Instruction) (es Escapes) { 711 cs := callSite(inst) 712 if _, ok := exemptions[cs.Resolved]; ok { 713 return // No escape. 714 } 715 switch x := inst.(type) { 716 case *ssa.Call: 717 if x.Call.IsInvoke() { 718 // This is an interface dispatch. There is no 719 // way to know if this is actually escaping or 720 // not, since we don't know the underlying 721 // type. 722 call, _ := hasCall(inst) 723 es.Add(interfaceInvoke, call, cs) 724 return 725 } 726 switch x := x.Call.Value.(type) { 727 case *ssa.Function: 728 // Is this a local function? If yes, call the 729 // function to load the local function. The 730 // local escapes are the escapes found in the 731 // local function. 732 if x.Pkg != nil && x.Pkg.Pkg == pass.Pkg { 733 es.MergeWithCall(loadFunc(x), cs) 734 return 735 } 736 737 // If this package is the atomic package, the implementation 738 // may be replaced by instrinsics that don't have analysis. 739 if x.Pkg != nil && x.Pkg.Pkg.Path() == "sync/atomic" { 740 return 741 } 742 743 // Recursively collect information. 744 var funcEscapes Escapes 745 if !pass.ImportObjectFact(x.Object(), &funcEscapes) { 746 // If this is the unix or syscall 747 // package, and the function is 748 // RawSyscall, we can also ignore this 749 // case. 750 pkgIsUnixOrSyscall := x.Pkg != nil && (x.Pkg.Pkg.Name() == "unix" || x.Pkg.Pkg.Name() == "syscall") 751 methodIsRawSyscall := x.Name() == "RawSyscall" || x.Name() == "RawSyscall6" 752 if pkgIsUnixOrSyscall && methodIsRawSyscall { 753 return 754 } 755 756 // Unable to import the dependency; we must 757 // declare these as escaping. 758 message := fmt.Sprintf("no analysis for %q", x.Object().String()) 759 es.Add(unknownPackage, message, cs) 760 return 761 } 762 763 // The escapes of this instruction are the 764 // escapes of the called function directly. 765 // Note that this may record many escapes. 766 es.MergeWithCall(funcEscapes, cs) 767 return 768 case *ssa.Builtin: 769 // Ignore elided escapes. 770 if _, has := hasCall(inst); !has { 771 return 772 } 773 774 // Check if the builtin is escaping. 775 for _, name := range escapingBuiltins { 776 if x.Name() == name { 777 es.Add(builtin, name, cs) 778 return 779 } 780 } 781 default: 782 // All dynamic calls are counted as soft 783 // escapes. They are similar to interface 784 // dispatches. We cannot actually look up what 785 // this refers to using static analysis alone. 786 call, _ := hasCall(inst) 787 es.Add(dynamicCall, call, cs) 788 } 789 case *ssa.Alloc: 790 // Ignore non-heap allocations. 791 if !x.Heap { 792 return 793 } 794 795 // Ignore elided escapes. 796 call, has := hasCall(inst) 797 if !has { 798 return 799 } 800 801 // This is a real heap allocation. 802 es.Add(allocation, call, cs) 803 case *ssa.MakeMap: 804 es.Add(builtin, "makemap", cs) 805 case *ssa.MakeSlice: 806 es.Add(builtin, "makeslice", cs) 807 case *ssa.MakeClosure: 808 es.Add(builtin, "makeclosure", cs) 809 case *ssa.MakeChan: 810 es.Add(builtin, "makechan", cs) 811 } 812 return 813 } 814 815 var analyzeBasicBlock func(*ssa.BasicBlock) []Escapes // Recursive. 816 analyzeBasicBlock = func(block *ssa.BasicBlock) (rval []Escapes) { 817 for _, inst := range block.Instrs { 818 if es := analyzeInstruction(inst); !es.IsEmpty() { 819 rval = append(rval, es) 820 } 821 } 822 return 823 } 824 825 loadFunc = func(fn *ssa.Function) Escapes { 826 // Is this already available? 827 name := fn.RelString(pass.Pkg) 828 if es, ok := mergedEscapes[name]; ok { 829 return es 830 } 831 832 // In the case of a true cycle, we assume that the current 833 // function itself has no escapes. 834 // 835 // When evaluating the function again, the proper escapes will 836 // be filled in here. 837 allEscapes[name] = nil 838 mergedEscapes[name] = Escapes{} 839 840 // Perform the basic analysis. 841 var es []Escapes 842 if fn.Recover != nil { 843 es = append(es, analyzeBasicBlock(fn.Recover)...) 844 } 845 for _, block := range fn.Blocks { 846 es = append(es, analyzeBasicBlock(block)...) 847 } 848 849 // Check for a stack split. 850 if call, has := hasCall(fn); has { 851 var ss Escapes 852 ss.Add(stackSplit, call, CallSite{ 853 LocalPos: fn.Pos(), 854 Resolved: linePosition(fn, fn.Parent()), 855 }) 856 es = append(es, ss) 857 } 858 859 // Save the result and return. 860 // 861 // Note that we merge the result when saving to the facts. It 862 // doesn't really matter the specific escapes, as long as we 863 // have recorded all the appropriate classes of escapes. 864 summary := MergeAll(es) 865 allEscapes[name] = es 866 mergedEscapes[name] = summary 867 return summary 868 } 869 870 // Complete all local functions. 871 for _, fn := range state.SrcFuncs { 872 funcEscapes := loadFunc(fn) 873 if obj := fn.Object(); obj != nil { 874 pass.ExportObjectFact(obj, &funcEscapes) 875 } 876 } 877 878 // Scan all functions for violations. 879 for _, f := range pass.Files { 880 // Scan all declarations. 881 for _, decl := range f.Decls { 882 // Function declaration? 883 fdecl, ok := decl.(*ast.FuncDecl) 884 if !ok { 885 continue 886 } 887 // Find all declared reasons. 888 reasons, local, testReasons := findReasons(pass, fdecl) 889 890 // Scan for matches. 891 fn := pass.TypesInfo.Defs[fdecl.Name].(*types.Func) 892 fv := state.Pkg.Prog.FuncValue(fn) 893 if fv == nil { 894 continue 895 } 896 name := fv.RelString(pass.Pkg) 897 all, allOk := allEscapes[name] 898 merged, mergedOk := mergedEscapes[name] 899 if !allOk || !mergedOk { 900 pass.Reportf(fdecl.Pos(), "internal error: function %s not found.", name) 901 continue 902 } 903 904 // Filter reasons and report. 905 // 906 // For the findings, we use all escapes. 907 for _, es := range all { 908 es.Filter(reasons, local) 909 es.Reportf(pass) 910 } 911 912 // Scan for test (required) matches. 913 // 914 // For tests we need only the merged escapes. 915 testReasonsFound := make(map[EscapeReason]bool) 916 for r := EscapeReason(0); r < reasonCount; r++ { 917 if merged.CallSites[r] == nil { 918 continue 919 } 920 // Is this local? 921 wantLocal, ok := testReasons[r] 922 isLocal := len(merged.CallSites[r]) == 1 923 testReasonsFound[r] = isLocal 924 if !ok { 925 continue 926 } 927 if isLocal == wantLocal { 928 delete(testReasons, r) 929 } 930 } 931 for reason, local := range testReasons { 932 // We didn't find the escapes we wanted. 933 pass.Reportf(fdecl.Pos(), fmt.Sprintf("testescapes not found: reason=%s, local=%t", reason, local)) 934 } 935 if len(testReasons) > 0 { 936 // Report for debugging. 937 merged.Reportf(pass) 938 } 939 } 940 } 941 942 return nil, nil 943 }