github.com/ttpreport/gvisor-ligolo@v0.0.0-20240123134145-a858404967ba/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 go1.1
    16  // +build go1.1
    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  	"github.com/ttpreport/gvisor-ligolo/pkg/hostarch"
    39  	"github.com/ttpreport/gvisor-ligolo/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  		for index := 0; index < len(counters); index++ {
   110  			counters[index] = 0
   111  		}
   112  	}
   113  }
   114  
   115  var coveragePool = sync.Pool{
   116  	New: func() any {
   117  		return make([]byte, 0)
   118  	},
   119  }
   120  
   121  // ConsumeCoverageData builds and writes the collection of covered PCs. It
   122  // returns the number of bytes written.
   123  //
   124  // In Linux, a kernel configuration is set that compiles the kernel with a
   125  // custom function that is called at the beginning of every basic block, which
   126  // updates the memory-mapped coverage information. The Go coverage tool does not
   127  // allow us to inject arbitrary instructions into basic blocks, but it does
   128  // provide data that we can convert to a kcov-like format and transfer them to
   129  // userspace through a memory mapping.
   130  //
   131  // Note that this is not a strict implementation of kcov, which is especially
   132  // tricky to do because we do not have the same coverage tools available in Go
   133  // that that are available for the actual Linux kernel. In Linux, a kernel
   134  // configuration is set that compiles the kernel with a custom function that is
   135  // called at the beginning of every basic block to write program counters to the
   136  // kcov memory mapping. In Go, however, coverage tools only give us a count of
   137  // basic blocks as they are executed. Every time we return to userspace, we
   138  // collect the coverage information and write out PCs for each block that was
   139  // executed, providing userspace with the illusion that the kcov data is always
   140  // up to date. For convenience, we also generate a unique synthetic PC for each
   141  // block instead of using actual PCs. Finally, we do not provide thread-specific
   142  // coverage data (each kcov instance only contains PCs executed by the thread
   143  // owning it); instead, we will supply data for any file specified by --
   144  // instrumentation_filter.
   145  //
   146  // Note that we "consume", i.e. clear, coverdata when this function is run, to
   147  // ensure that each event is only reported once. Due to the limitations of Go
   148  // coverage tools, we reset the global coverage data every time this function is
   149  // run.
   150  //
   151  //go:norace
   152  func ConsumeCoverageData(w io.Writer) int {
   153  	InitCoverageData()
   154  
   155  	coverageMu.Lock()
   156  	defer coverageMu.Unlock()
   157  
   158  	total := 0
   159  	var pcBuffer [8]byte
   160  	for fileNum, file := range globalData.files {
   161  		counters := coverdata.Counters[file]
   162  		for index := 0; index < len(counters); index++ {
   163  			// We do not use atomic operations while reading/writing to the counters,
   164  			// which would drastically degrade performance. Slight discrepancies due to
   165  			// racing is okay for the purposes of kcov.
   166  			if counters[index] == 0 {
   167  				continue
   168  			}
   169  			// Non-zero coverage data found; consume it and report as a PC.
   170  			counters[index] = 0
   171  			pc := globalData.syntheticPCs[fileNum][index]
   172  			hostarch.ByteOrder.PutUint64(pcBuffer[:], pc)
   173  			n, err := w.Write(pcBuffer[:])
   174  			if err != nil {
   175  				if err == io.EOF {
   176  					// Simply stop writing if we encounter EOF; it's ok if we attempted to
   177  					// write more than we can hold.
   178  					return total + n
   179  				}
   180  				panic(fmt.Sprintf("Internal error writing PCs to kcov area: %v", err))
   181  			}
   182  			total += n
   183  		}
   184  	}
   185  
   186  	return total
   187  }
   188  
   189  // InitCoverageData initializes globalData. It should be called before any kcov
   190  // data is written.
   191  func InitCoverageData() {
   192  	globalData.once.Do(func() {
   193  		// First, order all files. Then calculate synthetic PCs for every block
   194  		// (using the well-defined ordering for files as well).
   195  		for file := range coverdata.Blocks {
   196  			globalData.files = append(globalData.files, file)
   197  		}
   198  		sort.Strings(globalData.files)
   199  
   200  		for fileNum, file := range globalData.files {
   201  			blocks := coverdata.Blocks[file]
   202  			pcs := make([]uint64, 0, len(blocks))
   203  			for blockNum := range blocks {
   204  				pcs = append(pcs, calculateSyntheticPC(fileNum, blockNum))
   205  			}
   206  			globalData.syntheticPCs = append(globalData.syntheticPCs, pcs)
   207  		}
   208  	})
   209  }
   210  
   211  // reportOnce ensures that a coverage report is written at most once. For a
   212  // complete coverage report, Report should be called during the sandbox teardown
   213  // process. Report is called from multiple places (which may overlap) so that a
   214  // coverage report is written in different sandbox exit scenarios.
   215  var reportOnce sync.Once
   216  
   217  // Report writes out a coverage report with all blocks that have been covered.
   218  //
   219  // TODO(b/144576401): Decide whether this should actually be in LCOV format
   220  func Report() error {
   221  	if reportOutput == nil {
   222  		return nil
   223  	}
   224  
   225  	var err error
   226  	reportOnce.Do(func() {
   227  		for file, counters := range coverdata.Counters {
   228  			blocks := coverdata.Blocks[file]
   229  			for i := 0; i < len(counters); i++ {
   230  				if atomic.LoadUint32(&counters[i]) > 0 {
   231  					err = writeBlock(reportOutput, file, blocks[i])
   232  					if err != nil {
   233  						return
   234  					}
   235  				}
   236  			}
   237  		}
   238  		reportOutput.Close()
   239  	})
   240  	return err
   241  }
   242  
   243  // Symbolize prints information about the block corresponding to pc.
   244  func Symbolize(out io.Writer, pc uint64) error {
   245  	fileNum, blockNum := syntheticPCToIndexes(pc)
   246  	file, err := fileFromIndex(fileNum)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	block, err := blockFromIndex(file, blockNum)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	return writeBlockWithPC(out, pc, file, block)
   255  }
   256  
   257  // WriteAllBlocks prints all information about all blocks along with their
   258  // corresponding synthetic PCs.
   259  func WriteAllBlocks(out io.Writer) error {
   260  	for fileNum, file := range globalData.files {
   261  		for blockNum, block := range coverdata.Blocks[file] {
   262  			if err := writeBlockWithPC(out, calculateSyntheticPC(fileNum, blockNum), file, block); err != nil {
   263  				return err
   264  			}
   265  		}
   266  	}
   267  	return nil
   268  }
   269  
   270  func writeBlockWithPC(out io.Writer, pc uint64, file string, block testing.CoverBlock) error {
   271  	if _, err := io.WriteString(out, fmt.Sprintf("%#x\n", pc)); err != nil {
   272  		return err
   273  	}
   274  	return writeBlock(out, file, block)
   275  }
   276  
   277  func writeBlock(out io.Writer, file string, block testing.CoverBlock) error {
   278  	_, err := io.WriteString(out, fmt.Sprintf("%s:%d.%d,%d.%d\n", file, block.Line0, block.Col0, block.Line1, block.Col1))
   279  	return err
   280  }
   281  
   282  func calculateSyntheticPC(fileNum int, blockNum int) uint64 {
   283  	return (uint64(fileNum) << blockBitLength) + uint64(blockNum)
   284  }
   285  
   286  func syntheticPCToIndexes(pc uint64) (fileNum int, blockNum int) {
   287  	return int(pc >> blockBitLength), int(pc & ((1 << blockBitLength) - 1))
   288  }
   289  
   290  // fileFromIndex returns the name of the file in the sorted list of instrumented files.
   291  func fileFromIndex(i int) (string, error) {
   292  	total := len(globalData.files)
   293  	if i < 0 || i >= total {
   294  		return "", fmt.Errorf("file index out of range: [%d] with length %d", i, total)
   295  	}
   296  	return globalData.files[i], nil
   297  }
   298  
   299  // blockFromIndex returns the i-th block in the given file.
   300  func blockFromIndex(file string, i int) (testing.CoverBlock, error) {
   301  	blocks, ok := coverdata.Blocks[file]
   302  	if !ok {
   303  		return testing.CoverBlock{}, fmt.Errorf("instrumented file %s does not exist", file)
   304  	}
   305  	total := len(blocks)
   306  	if i < 0 || i >= total {
   307  		return testing.CoverBlock{}, fmt.Errorf("block index out of range: [%d] with length %d", i, total)
   308  	}
   309  	return blocks[i], nil
   310  }