github.com/nicocha30/gvisor-ligolo@v0.0.0-20230726075806-989fa2c0a413/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/nicocha30/gvisor-ligolo/pkg/hostarch" 39 "github.com/nicocha30/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 }