gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/coverage/coverage.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  //go:build !false
    16  // +build !false
    17  
    18  // Package coverage provides an interface through which Go coverage data can
    19  // be collected, converted to kcov format, and exposed to userspace.
    20  //
    21  // Coverage can be enabled by calling bazel {build,test} with
    22  // --collect_coverage_data and --instrumentation_filter with the desired
    23  // coverage surface. This causes bazel to use the Go cover tool manually to
    24  // generate instrumented files. It injects a hook that registers all coverage
    25  // data with the coverdata package.
    26  //
    27  // Using coverdata.Counters requires sync/atomic integers.
    28  // +checkalignedignore
    29  package coverage
    30  
    31  import (
    32  	"fmt"
    33  	"io"
    34  	"sort"
    35  	"sync/atomic"
    36  	"testing"
    37  
    38  	"gvisor.dev/gvisor/pkg/hostarch"
    39  	"gvisor.dev/gvisor/pkg/sync"
    40  
    41  	"github.com/bazelbuild/rules_go/go/tools/coverdata"
    42  )
    43  
    44  var (
    45  	// coverageMu must be held while accessing coverdata.*. This prevents
    46  	// concurrent reads/writes from multiple threads collecting coverage data.
    47  	coverageMu sync.RWMutex
    48  
    49  	// reportOutput is the place to write out a coverage report. It should be
    50  	// closed after the report is written. It is protected by reportOutputMu.
    51  	reportOutput   io.WriteCloser
    52  	reportOutputMu sync.Mutex
    53  )
    54  
    55  // blockBitLength is the number of bits used to represent coverage block index
    56  // in a synthetic PC (the rest are used to represent the file index). Even
    57  // though a PC has 64 bits, we only use the lower 32 bits because some users
    58  // (e.g., syzkaller) may truncate that address to a 32-bit value.
    59  //
    60  // As of this writing, there are ~1200 files that can be instrumented and at
    61  // most ~1200 blocks per file, so 16 bits is more than enough to represent every
    62  // file and every block.
    63  const blockBitLength = 16
    64  
    65  // Available returns whether any coverage data is available.
    66  func Available() bool {
    67  	return len(coverdata.Blocks) > 0
    68  }
    69  
    70  // EnableReport sets up coverage reporting.
    71  func EnableReport(w io.WriteCloser) {
    72  	reportOutputMu.Lock()
    73  	defer reportOutputMu.Unlock()
    74  	reportOutput = w
    75  }
    76  
    77  // KcovSupported returns whether the kcov interface should be made available.
    78  //
    79  // If coverage reporting is on, do not turn on kcov, which will consume
    80  // coverage data.
    81  func KcovSupported() bool {
    82  	return (reportOutput == nil) && Available()
    83  }
    84  
    85  var globalData struct {
    86  	// files is the set of covered files sorted by filename. It is calculated at
    87  	// startup.
    88  	files []string
    89  
    90  	// syntheticPCs are a set of PCs calculated at startup, where the PC
    91  	// at syntheticPCs[i][j] corresponds to file i, block j.
    92  	syntheticPCs [][]uint64
    93  
    94  	// once ensures that globalData is only initialized once.
    95  	once sync.Once
    96  }
    97  
    98  // ClearCoverageData clears existing coverage data.
    99  //
   100  //go:norace
   101  func ClearCoverageData() {
   102  	coverageMu.Lock()
   103  	defer coverageMu.Unlock()
   104  
   105  	// We do not use atomic operations while reading/writing to the counters,
   106  	// which would drastically degrade performance. Slight discrepancies due to
   107  	// racing is okay for the purposes of kcov.
   108  	for _, counters := range coverdata.Counters {
   109  		clear(counters)
   110  	}
   111  }
   112  
   113  var coveragePool = sync.Pool{
   114  	New: func() any {
   115  		return make([]byte, 0)
   116  	},
   117  }
   118  
   119  // ConsumeCoverageData builds and writes the collection of covered PCs. It
   120  // returns the number of bytes written.
   121  //
   122  // In Linux, a kernel configuration is set that compiles the kernel with a
   123  // custom function that is called at the beginning of every basic block, which
   124  // updates the memory-mapped coverage information. The Go coverage tool does not
   125  // allow us to inject arbitrary instructions into basic blocks, but it does
   126  // provide data that we can convert to a kcov-like format and transfer them to
   127  // userspace through a memory mapping.
   128  //
   129  // Note that this is not a strict implementation of kcov, which is especially
   130  // tricky to do because we do not have the same coverage tools available in Go
   131  // that that are available for the actual Linux kernel. In Linux, a kernel
   132  // configuration is set that compiles the kernel with a custom function that is
   133  // called at the beginning of every basic block to write program counters to the
   134  // kcov memory mapping. In Go, however, coverage tools only give us a count of
   135  // basic blocks as they are executed. Every time we return to userspace, we
   136  // collect the coverage information and write out PCs for each block that was
   137  // executed, providing userspace with the illusion that the kcov data is always
   138  // up to date. For convenience, we also generate a unique synthetic PC for each
   139  // block instead of using actual PCs. Finally, we do not provide thread-specific
   140  // coverage data (each kcov instance only contains PCs executed by the thread
   141  // owning it); instead, we will supply data for any file specified by --
   142  // instrumentation_filter.
   143  //
   144  // Note that we "consume", i.e. clear, coverdata when this function is run, to
   145  // ensure that each event is only reported once. Due to the limitations of Go
   146  // coverage tools, we reset the global coverage data every time this function is
   147  // run.
   148  //
   149  //go:norace
   150  func ConsumeCoverageData(w io.Writer) int {
   151  	InitCoverageData()
   152  
   153  	coverageMu.Lock()
   154  	defer coverageMu.Unlock()
   155  
   156  	total := 0
   157  	var pcBuffer [8]byte
   158  	for fileNum, file := range globalData.files {
   159  		counters := coverdata.Counters[file]
   160  		for index := 0; index < len(counters); index++ {
   161  			// We do not use atomic operations while reading/writing to the counters,
   162  			// which would drastically degrade performance. Slight discrepancies due to
   163  			// racing is okay for the purposes of kcov.
   164  			if counters[index] == 0 {
   165  				continue
   166  			}
   167  			// Non-zero coverage data found; consume it and report as a PC.
   168  			counters[index] = 0
   169  			pc := globalData.syntheticPCs[fileNum][index]
   170  			hostarch.ByteOrder.PutUint64(pcBuffer[:], pc)
   171  			n, err := w.Write(pcBuffer[:])
   172  			if err != nil {
   173  				if err == io.EOF {
   174  					// Simply stop writing if we encounter EOF; it's ok if we attempted to
   175  					// write more than we can hold.
   176  					return total + n
   177  				}
   178  				panic(fmt.Sprintf("Internal error writing PCs to kcov area: %v", err))
   179  			}
   180  			total += n
   181  		}
   182  	}
   183  
   184  	return total
   185  }
   186  
   187  // InitCoverageData initializes globalData. It should be called before any kcov
   188  // data is written.
   189  func InitCoverageData() {
   190  	globalData.once.Do(func() {
   191  		// First, order all files. Then calculate synthetic PCs for every block
   192  		// (using the well-defined ordering for files as well).
   193  		for file := range coverdata.Blocks {
   194  			globalData.files = append(globalData.files, file)
   195  		}
   196  		sort.Strings(globalData.files)
   197  
   198  		for fileNum, file := range globalData.files {
   199  			blocks := coverdata.Blocks[file]
   200  			pcs := make([]uint64, 0, len(blocks))
   201  			for blockNum := range blocks {
   202  				pcs = append(pcs, calculateSyntheticPC(fileNum, blockNum))
   203  			}
   204  			globalData.syntheticPCs = append(globalData.syntheticPCs, pcs)
   205  		}
   206  	})
   207  }
   208  
   209  // reportOnce ensures that a coverage report is written at most once. For a
   210  // complete coverage report, Report should be called during the sandbox teardown
   211  // process. Report is called from multiple places (which may overlap) so that a
   212  // coverage report is written in different sandbox exit scenarios.
   213  var reportOnce sync.Once
   214  
   215  // Report writes out a coverage report with all blocks that have been covered.
   216  //
   217  // TODO(b/144576401): Decide whether this should actually be in LCOV format
   218  func Report() error {
   219  	if reportOutput == nil {
   220  		return nil
   221  	}
   222  
   223  	var err error
   224  	reportOnce.Do(func() {
   225  		for file, counters := range coverdata.Counters {
   226  			blocks := coverdata.Blocks[file]
   227  			for i := 0; i < len(counters); i++ {
   228  				if atomic.LoadUint32(&counters[i]) > 0 {
   229  					err = writeBlock(reportOutput, file, blocks[i])
   230  					if err != nil {
   231  						return
   232  					}
   233  				}
   234  			}
   235  		}
   236  		reportOutput.Close()
   237  	})
   238  	return err
   239  }
   240  
   241  // Symbolize prints information about the block corresponding to pc.
   242  func Symbolize(out io.Writer, pc uint64) error {
   243  	fileNum, blockNum := syntheticPCToIndexes(pc)
   244  	file, err := fileFromIndex(fileNum)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	block, err := blockFromIndex(file, blockNum)
   249  	if err != nil {
   250  		return err
   251  	}
   252  	return writeBlockWithPC(out, pc, file, block)
   253  }
   254  
   255  // WriteAllBlocks prints all information about all blocks along with their
   256  // corresponding synthetic PCs.
   257  func WriteAllBlocks(out io.Writer) error {
   258  	for fileNum, file := range globalData.files {
   259  		for blockNum, block := range coverdata.Blocks[file] {
   260  			if err := writeBlockWithPC(out, calculateSyntheticPC(fileNum, blockNum), file, block); err != nil {
   261  				return err
   262  			}
   263  		}
   264  	}
   265  	return nil
   266  }
   267  
   268  func writeBlockWithPC(out io.Writer, pc uint64, file string, block testing.CoverBlock) error {
   269  	if _, err := io.WriteString(out, fmt.Sprintf("%#x\n", pc)); err != nil {
   270  		return err
   271  	}
   272  	return writeBlock(out, file, block)
   273  }
   274  
   275  func writeBlock(out io.Writer, file string, block testing.CoverBlock) error {
   276  	_, err := io.WriteString(out, fmt.Sprintf("%s:%d.%d,%d.%d\n", file, block.Line0, block.Col0, block.Line1, block.Col1))
   277  	return err
   278  }
   279  
   280  func calculateSyntheticPC(fileNum int, blockNum int) uint64 {
   281  	return (uint64(fileNum) << blockBitLength) + uint64(blockNum)
   282  }
   283  
   284  func syntheticPCToIndexes(pc uint64) (fileNum int, blockNum int) {
   285  	return int(pc >> blockBitLength), int(pc & ((1 << blockBitLength) - 1))
   286  }
   287  
   288  // fileFromIndex returns the name of the file in the sorted list of instrumented files.
   289  func fileFromIndex(i int) (string, error) {
   290  	total := len(globalData.files)
   291  	if i < 0 || i >= total {
   292  		return "", fmt.Errorf("file index out of range: [%d] with length %d", i, total)
   293  	}
   294  	return globalData.files[i], nil
   295  }
   296  
   297  // blockFromIndex returns the i-th block in the given file.
   298  func blockFromIndex(file string, i int) (testing.CoverBlock, error) {
   299  	blocks, ok := coverdata.Blocks[file]
   300  	if !ok {
   301  		return testing.CoverBlock{}, fmt.Errorf("instrumented file %s does not exist", file)
   302  	}
   303  	total := len(blocks)
   304  	if i < 0 || i >= total {
   305  		return testing.CoverBlock{}, fmt.Errorf("block index out of range: [%d] with length %d", i, total)
   306  	}
   307  	return blocks[i], nil
   308  }