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  }