Skip to content

Commit e7d9093

Browse files
avagingvisor-bot
authored andcommitted
pkg/coverage: adopt new coverage interface for kcov
This change updates the kcov implementation to use the new Go coverage interface provided by the `internal/coverage` package. The previous implementation relied on `coverdata.Blocks`, which is part of the older coverage tooling. The new implementation uses `runtime/coverage.WriteCounters` to get the raw coverage data and then decodes it using `decodecounter.NewDecoder`. This approach is aligned with the modern Go coverage infrastructure. This change requires a patched Go compiler that allows importing internal packages. This is a temporary measure until the golang team provides a public API. More details can be found here: golang/go#51430 PiperOrigin-RevId: 825700145
1 parent fec04fd commit e7d9093

File tree

10 files changed

+509
-264
lines changed

10 files changed

+509
-264
lines changed

BUILD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ build_test(
123123
go_path(
124124
name = "gopath",
125125
mode = "archive",
126+
visibility = ["//:sandbox"],
126127
deps = [
127128
# Main binaries.
128129
#
@@ -184,3 +185,8 @@ toolchain(
184185
# To update the WORKSPACE from go.mod, use:
185186
# bazel run //:gazelle -- update-repos -from_file=go.mod
186187
gazelle(name = "gazelle")
188+
189+
exports_files([
190+
"go.sum",
191+
"go.mod",
192+
])

pkg/coverage/BUILD

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ package(
77

88
go_library(
99
name = "coverage",
10-
srcs = ["coverage.go"],
10+
srcs = [
11+
"coverage.go",
12+
"coverage_unsafe.go",
13+
],
1114
visibility = ["//:sandbox"],
1215
deps = [
1316
"//pkg/hostarch",
17+
"//pkg/log",
1418
"//pkg/sync",
1519
"@io_bazel_rules_go//go/tools/coverdata",
1620
],

pkg/coverage/coverage.go

Lines changed: 18 additions & 261 deletions
Original file line numberDiff line numberDiff line change
@@ -12,297 +12,54 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
//go:build !false
16-
// +build !false
15+
//go:build !kcov && !false
16+
// +build !kcov,!false
1717

1818
// Package coverage provides an interface through which Go coverage data can
1919
// 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
2920
package coverage
3021

3122
import (
32-
"fmt"
3323
"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
5324
)
5425

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-
6526
// Available returns whether any coverage data is available.
6627
func Available() bool {
67-
return len(coverdata.Blocks) > 0
28+
return false
6829
}
6930

7031
// EnableReport sets up coverage reporting.
7132
func EnableReport(w io.WriteCloser) {
72-
reportOutputMu.Lock()
73-
defer reportOutputMu.Unlock()
74-
reportOutput = w
33+
}
34+
35+
// Report writes out a coverage report with all blocks that have been covered.
36+
func Report() error {
37+
return nil
7538
}
7639

7740
// 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.
8141
func KcovSupported() bool {
82-
return (reportOutput == nil) && Available()
42+
return false
8343
}
8444

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-
}
45+
// InitCoverageData initializes global kcov-related data structures.
46+
func InitCoverageData() {}
9747

9848
// 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-
}
49+
func ClearCoverageData() {}
11250

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
51+
// ConsumeCoverageData builds the collection of covered PCs.
15052
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
53+
return 0
23954
}
24055

241-
// Symbolize prints information about the block corresponding to pc.
56+
// Symbolize writes out information about the block corresponding to pc.
24257
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)
58+
return nil
25359
}
25460

255-
// WriteAllBlocks prints all information about all blocks along with their
256-
// corresponding synthetic PCs.
61+
// WriteAllBlocks writes out all PCs along with their corresponding position in the
62+
// source code.
25763
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-
}
26564
return nil
26665
}
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-
}

0 commit comments

Comments
 (0)