From 9d3d0af032763909df07494185acdfb8654489e3 Mon Sep 17 00:00:00 2001 From: Andrei Vagin Date: Tue, 4 Nov 2025 14:51:45 -0800 Subject: [PATCH] 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: https://github.com/golang/go/issues/51430 PiperOrigin-RevId: 828145693 --- BUILD | 6 + pkg/coverage/BUILD | 6 +- pkg/coverage/coverage.go | 279 ++--------------------- pkg/coverage/coverage_unsafe.go | 386 ++++++++++++++++++++++++++++++++ runsc/BUILD | 23 +- tools/BUILD | 5 + tools/bazeldefs/go.bzl | 1 + tools/build_cover.sh | 52 +++++ tools/defs.bzl | 3 +- tools/go-allow-internal.patch | 12 + 10 files changed, 509 insertions(+), 264 deletions(-) create mode 100644 pkg/coverage/coverage_unsafe.go create mode 100755 tools/build_cover.sh create mode 100644 tools/go-allow-internal.patch diff --git a/BUILD b/BUILD index 098369f470..37f94e7252 100644 --- a/BUILD +++ b/BUILD @@ -123,6 +123,7 @@ build_test( go_path( name = "gopath", mode = "archive", + visibility = ["//:sandbox"], deps = [ # Main binaries. # @@ -184,3 +185,8 @@ toolchain( # To update the WORKSPACE from go.mod, use: # bazel run //:gazelle -- update-repos -from_file=go.mod gazelle(name = "gazelle") + +exports_files([ + "go.sum", + "go.mod", +]) diff --git a/pkg/coverage/BUILD b/pkg/coverage/BUILD index f6c5ebe097..c8e0c93102 100644 --- a/pkg/coverage/BUILD +++ b/pkg/coverage/BUILD @@ -7,10 +7,14 @@ package( go_library( name = "coverage", - srcs = ["coverage.go"], + srcs = [ + "coverage.go", + "coverage_unsafe.go", + ], visibility = ["//:sandbox"], deps = [ "//pkg/hostarch", + "//pkg/log", "//pkg/sync", "@io_bazel_rules_go//go/tools/coverdata", ], diff --git a/pkg/coverage/coverage.go b/pkg/coverage/coverage.go index 11a42d4153..96ba64cdbf 100644 --- a/pkg/coverage/coverage.go +++ b/pkg/coverage/coverage.go @@ -12,297 +12,54 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !false -// +build !false +//go:build !kcov && !false +// +build !kcov,!false // Package coverage provides an interface through which Go coverage data can // be collected, converted to kcov format, and exposed to userspace. -// -// Coverage can be enabled by calling bazel {build,test} with -// --collect_coverage_data and --instrumentation_filter with the desired -// coverage surface. This causes bazel to use the Go cover tool manually to -// generate instrumented files. It injects a hook that registers all coverage -// data with the coverdata package. -// -// Using coverdata.Counters requires sync/atomic integers. -// +checkalignedignore package coverage import ( - "fmt" "io" - "sort" - "sync/atomic" - "testing" - - "gvisor.dev/gvisor/pkg/hostarch" - "gvisor.dev/gvisor/pkg/sync" - - "github.com/bazelbuild/rules_go/go/tools/coverdata" -) - -var ( - // coverageMu must be held while accessing coverdata.*. This prevents - // concurrent reads/writes from multiple threads collecting coverage data. - coverageMu sync.RWMutex - - // reportOutput is the place to write out a coverage report. It should be - // closed after the report is written. It is protected by reportOutputMu. - reportOutput io.WriteCloser - reportOutputMu sync.Mutex ) -// blockBitLength is the number of bits used to represent coverage block index -// in a synthetic PC (the rest are used to represent the file index). Even -// though a PC has 64 bits, we only use the lower 32 bits because some users -// (e.g., syzkaller) may truncate that address to a 32-bit value. -// -// As of this writing, there are ~1200 files that can be instrumented and at -// most ~1200 blocks per file, so 16 bits is more than enough to represent every -// file and every block. -const blockBitLength = 16 - // Available returns whether any coverage data is available. func Available() bool { - return len(coverdata.Blocks) > 0 + return false } // EnableReport sets up coverage reporting. func EnableReport(w io.WriteCloser) { - reportOutputMu.Lock() - defer reportOutputMu.Unlock() - reportOutput = w +} + +// Report writes out a coverage report with all blocks that have been covered. +func Report() error { + return nil } // KcovSupported returns whether the kcov interface should be made available. -// -// If coverage reporting is on, do not turn on kcov, which will consume -// coverage data. func KcovSupported() bool { - return (reportOutput == nil) && Available() + return false } -var globalData struct { - // files is the set of covered files sorted by filename. It is calculated at - // startup. - files []string - - // syntheticPCs are a set of PCs calculated at startup, where the PC - // at syntheticPCs[i][j] corresponds to file i, block j. - syntheticPCs [][]uint64 - - // once ensures that globalData is only initialized once. - once sync.Once -} +// InitCoverageData initializes global kcov-related data structures. +func InitCoverageData() {} // ClearCoverageData clears existing coverage data. -// -//go:norace -func ClearCoverageData() { - coverageMu.Lock() - defer coverageMu.Unlock() - - // We do not use atomic operations while reading/writing to the counters, - // which would drastically degrade performance. Slight discrepancies due to - // racing is okay for the purposes of kcov. - for _, counters := range coverdata.Counters { - clear(counters) - } -} +func ClearCoverageData() {} -var coveragePool = sync.Pool{ - New: func() any { - return make([]byte, 0) - }, -} - -// ConsumeCoverageData builds and writes the collection of covered PCs. It -// returns the number of bytes written. -// -// In Linux, a kernel configuration is set that compiles the kernel with a -// custom function that is called at the beginning of every basic block, which -// updates the memory-mapped coverage information. The Go coverage tool does not -// allow us to inject arbitrary instructions into basic blocks, but it does -// provide data that we can convert to a kcov-like format and transfer them to -// userspace through a memory mapping. -// -// Note that this is not a strict implementation of kcov, which is especially -// tricky to do because we do not have the same coverage tools available in Go -// that that are available for the actual Linux kernel. In Linux, a kernel -// configuration is set that compiles the kernel with a custom function that is -// called at the beginning of every basic block to write program counters to the -// kcov memory mapping. In Go, however, coverage tools only give us a count of -// basic blocks as they are executed. Every time we return to userspace, we -// collect the coverage information and write out PCs for each block that was -// executed, providing userspace with the illusion that the kcov data is always -// up to date. For convenience, we also generate a unique synthetic PC for each -// block instead of using actual PCs. Finally, we do not provide thread-specific -// coverage data (each kcov instance only contains PCs executed by the thread -// owning it); instead, we will supply data for any file specified by -- -// instrumentation_filter. -// -// Note that we "consume", i.e. clear, coverdata when this function is run, to -// ensure that each event is only reported once. Due to the limitations of Go -// coverage tools, we reset the global coverage data every time this function is -// run. -// -//go:norace +// ConsumeCoverageData builds the collection of covered PCs. func ConsumeCoverageData(w io.Writer) int { - InitCoverageData() - - coverageMu.Lock() - defer coverageMu.Unlock() - - total := 0 - var pcBuffer [8]byte - for fileNum, file := range globalData.files { - counters := coverdata.Counters[file] - for index := 0; index < len(counters); index++ { - // We do not use atomic operations while reading/writing to the counters, - // which would drastically degrade performance. Slight discrepancies due to - // racing is okay for the purposes of kcov. - if counters[index] == 0 { - continue - } - // Non-zero coverage data found; consume it and report as a PC. - counters[index] = 0 - pc := globalData.syntheticPCs[fileNum][index] - hostarch.ByteOrder.PutUint64(pcBuffer[:], pc) - n, err := w.Write(pcBuffer[:]) - if err != nil { - if err == io.EOF { - // Simply stop writing if we encounter EOF; it's ok if we attempted to - // write more than we can hold. - return total + n - } - panic(fmt.Sprintf("Internal error writing PCs to kcov area: %v", err)) - } - total += n - } - } - - return total -} - -// InitCoverageData initializes globalData. It should be called before any kcov -// data is written. -func InitCoverageData() { - globalData.once.Do(func() { - // First, order all files. Then calculate synthetic PCs for every block - // (using the well-defined ordering for files as well). - for file := range coverdata.Blocks { - globalData.files = append(globalData.files, file) - } - sort.Strings(globalData.files) - - for fileNum, file := range globalData.files { - blocks := coverdata.Blocks[file] - pcs := make([]uint64, 0, len(blocks)) - for blockNum := range blocks { - pcs = append(pcs, calculateSyntheticPC(fileNum, blockNum)) - } - globalData.syntheticPCs = append(globalData.syntheticPCs, pcs) - } - }) -} - -// reportOnce ensures that a coverage report is written at most once. For a -// complete coverage report, Report should be called during the sandbox teardown -// process. Report is called from multiple places (which may overlap) so that a -// coverage report is written in different sandbox exit scenarios. -var reportOnce sync.Once - -// Report writes out a coverage report with all blocks that have been covered. -// -// TODO(b/144576401): Decide whether this should actually be in LCOV format -func Report() error { - if reportOutput == nil { - return nil - } - - var err error - reportOnce.Do(func() { - for file, counters := range coverdata.Counters { - blocks := coverdata.Blocks[file] - for i := 0; i < len(counters); i++ { - if atomic.LoadUint32(&counters[i]) > 0 { - err = writeBlock(reportOutput, file, blocks[i]) - if err != nil { - return - } - } - } - } - reportOutput.Close() - }) - return err + return 0 } -// Symbolize prints information about the block corresponding to pc. +// Symbolize writes out information about the block corresponding to pc. func Symbolize(out io.Writer, pc uint64) error { - fileNum, blockNum := syntheticPCToIndexes(pc) - file, err := fileFromIndex(fileNum) - if err != nil { - return err - } - block, err := blockFromIndex(file, blockNum) - if err != nil { - return err - } - return writeBlockWithPC(out, pc, file, block) + return nil } -// WriteAllBlocks prints all information about all blocks along with their -// corresponding synthetic PCs. +// WriteAllBlocks writes out all PCs along with their corresponding position in the +// source code. func WriteAllBlocks(out io.Writer) error { - for fileNum, file := range globalData.files { - for blockNum, block := range coverdata.Blocks[file] { - if err := writeBlockWithPC(out, calculateSyntheticPC(fileNum, blockNum), file, block); err != nil { - return err - } - } - } return nil } - -func writeBlockWithPC(out io.Writer, pc uint64, file string, block testing.CoverBlock) error { - if _, err := io.WriteString(out, fmt.Sprintf("%#x\n", pc)); err != nil { - return err - } - return writeBlock(out, file, block) -} - -func writeBlock(out io.Writer, file string, block testing.CoverBlock) error { - _, err := io.WriteString(out, fmt.Sprintf("%s:%d.%d,%d.%d\n", file, block.Line0, block.Col0, block.Line1, block.Col1)) - return err -} - -func calculateSyntheticPC(fileNum int, blockNum int) uint64 { - return (uint64(fileNum) << blockBitLength) + uint64(blockNum) -} - -func syntheticPCToIndexes(pc uint64) (fileNum int, blockNum int) { - return int(pc >> blockBitLength), int(pc & ((1 << blockBitLength) - 1)) -} - -// fileFromIndex returns the name of the file in the sorted list of instrumented files. -func fileFromIndex(i int) (string, error) { - total := len(globalData.files) - if i < 0 || i >= total { - return "", fmt.Errorf("file index out of range: [%d] with length %d", i, total) - } - return globalData.files[i], nil -} - -// blockFromIndex returns the i-th block in the given file. -func blockFromIndex(file string, i int) (testing.CoverBlock, error) { - blocks, ok := coverdata.Blocks[file] - if !ok { - return testing.CoverBlock{}, fmt.Errorf("instrumented file %s does not exist", file) - } - total := len(blocks) - if i < 0 || i >= total { - return testing.CoverBlock{}, fmt.Errorf("block index out of range: [%d] with length %d", i, total) - } - return blocks[i], nil -} diff --git a/pkg/coverage/coverage_unsafe.go b/pkg/coverage/coverage_unsafe.go new file mode 100644 index 0000000000..084ab1a366 --- /dev/null +++ b/pkg/coverage/coverage_unsafe.go @@ -0,0 +1,386 @@ +// Copyright 2020 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build kcov && opensource +// +build kcov,opensource + +// Package coverage provides an interface through which Go coverage data can +// be collected, converted to kcov format, and exposed to userspace. +// +// Coverage can be enabled by calling bazel {build,test} with +// --collect_coverage_data and --instrumentation_filter with the desired +// coverage surface. This causes bazel to use the Go cover tool manually to +// generate instrumented files. It injects a hook that registers all coverage +// data with the coverdata package. +// +// Using coverdata.Counters requires sync/atomic integers. +// +checkalignedignore +package coverage + +import ( + "bytes" + "fmt" + icov "internal/coverage" + "internal/coverage/decodecounter" + "internal/coverage/decodemeta" + "internal/coverage/rtcov" + "io" + "runtime/coverage" + "unsafe" + + "gvisor.dev/gvisor/pkg/hostarch" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sync" +) + +var ( + // coverageMu must be held while accessing coverdata.*. This prevents + // concurrent reads/writes from multiple threads collecting coverage data. + coverageMu sync.RWMutex + + // reportOutput is the place to write out a coverage report. It should be + // closed after the report is written. It is protected by reportOutputMu. + reportOutput io.WriteCloser + reportOutputMu sync.Mutex +) + +// blockBitLength is the number of bits used to represent coverage block index +// in a synthetic PC (the rest are used to represent the file index). Even +// though a PC has 64 bits, we only use the lower 32 bits because some users +// (e.g., syzkaller) may truncate that address to a 32-bit value. +// +// As of this writing, there are ~1200 files that can be instrumented and at +// most ~1200 blocks per file, so 16 bits is more than enough to represent every +// file and every block. +const blockBitLength = 16 + +// Available returns whether any coverage data is available. +func Available() bool { + InitCoverageData() + return len(globalData.pkgs) > 0 +} + +// EnableReport sets up coverage reporting. +func EnableReport(w io.WriteCloser) { + reportOutputMu.Lock() + defer reportOutputMu.Unlock() + reportOutput = w +} + +// KcovSupported returns whether the kcov interface should be made available. +// +// If coverage reporting is on, do not turn on kcov, which will consume +// coverage data. +func KcovSupported() bool { + return (reportOutput == nil) && Available() +} + +var globalData struct { + pkgs map[uint32]*pkg + + // once ensures that globalData is only initialized once. + once sync.Once +} + +// ClearCoverageData clears existing coverage data. +// +//go:norace +func ClearCoverageData() { + coverageMu.Lock() + defer coverageMu.Unlock() + + coverage.ClearCounters() +} + +var coveragePool = sync.Pool{ + New: func() any { + return make([]byte, 0) + }, +} + +// fileBuffer implements io.ReadWriteSeeker. +type fileBuffer struct { + buffer []byte + offset int64 +} + +// Bytes implements io.ReadWriteSeeker.Bytes. +func (fb *fileBuffer) Bytes() []byte { + return fb.buffer +} + +// Len implements io.ReadWriteSeeker.Len. +func (fb *fileBuffer) Len() int { + return len(fb.buffer) +} + +// Write implements io.ReadWriteSeeker.Write. +func (fb *fileBuffer) Read(b []byte) (int, error) { + available := len(fb.buffer) - int(fb.offset) + if available == 0 { + return 0, io.EOF + } + size := len(b) + if size > available { + size = available + } + copy(b, fb.buffer[fb.offset:fb.offset+int64(size)]) + fb.offset += int64(size) + return size, nil +} + +// Write implements io.ReadWriteSeeker.Write. +func (fb *fileBuffer) Write(b []byte) (int, error) { + copied := copy(fb.buffer[fb.offset:], b) + if copied < len(b) { + fb.buffer = append(fb.buffer, b[copied:]...) + } + fb.offset += int64(len(b)) + return len(b), nil +} + +// Seek implements io.ReadWriteSeeker.Seek. +func (fb *fileBuffer) Seek(offset int64, whence int) (int64, error) { + var newOffset int64 + switch whence { + case io.SeekStart: + newOffset = offset + case io.SeekCurrent: + newOffset = fb.offset + offset + case io.SeekEnd: + newOffset = int64(len(fb.buffer)) + offset + default: + return 0, fmt.Errorf("unknown seek method: %v", whence) + } + if newOffset > int64(len(fb.buffer)) || newOffset < 0 { + return 0, fmt.Errorf("invalid offset %d", offset) + } + fb.offset = newOffset + return newOffset, nil +} + +//go:linkname getCovCounterList +func getCovCounterList() []rtcov.CovCounterBlob + +type pkg struct { + funcs map[uint32]icov.FuncDesc +} + +// ConsumeCoverageData builds and writes the collection of covered PCs. It +// returns the number of bytes written. +// +// In Linux, a kernel configuration is set that compiles the kernel with a +// custom function that is called at the beginning of every basic block, which +// updates the memory-mapped coverage information. The Go coverage tool does not +// allow us to inject arbitrary instructions into basic blocks, but it does +// provide data that we can convert to a kcov-like format and transfer them to +// userspace through a memory mapping. +// +// Note that this is not a strict implementation of kcov, which is especially +// tricky to do because we do not have the same coverage tools available in Go +// that that are available for the actual Linux kernel. In Linux, a kernel +// configuration is set that compiles the kernel with a custom function that is +// called at the beginning of every basic block to write program counters to the +// kcov memory mapping. In Go, however, coverage tools only give us a count of +// basic blocks as they are executed. Every time we return to userspace, we +// collect the coverage information and write out PCs for each block that was +// executed, providing userspace with the illusion that the kcov data is always +// up to date. For convenience, we also generate a unique synthetic PC for each +// block instead of using actual PCs. Finally, we do not provide thread-specific +// coverage data (each kcov instance only contains PCs executed by the thread +// owning it); instead, we will supply data for any file specified by -- +// instrumentation_filter. +// +// Note that we "consume", i.e. clear, coverdata when this function is run, to +// ensure that each event is only reported once. Due to the limitations of Go +// coverage tools, we reset the global coverage data every time this function is +// run. +// +//go:norace +func ConsumeCoverageData(w io.Writer) int { + total := 0 + var pcBuffer [8]byte + + consumeCoverageData(func(pc uint64) bool { + hostarch.ByteOrder.PutUint64(pcBuffer[:], pc) + n, err := w.Write(pcBuffer[:]) + if err != nil { + if err == io.EOF { + // Simply stop writing if we encounter EOF; it's ok if we attempted to + // write more than we can hold. + total += n + return false + } + panic(fmt.Sprintf("Internal error writing PCs to kcov area: %v", err)) + } + total += n + return true + }) + + return total +} + +func consumeCoverageData(handler func(pc uint64) bool) { + InitCoverageData() + + coverageMu.Lock() + defer coverageMu.Unlock() + + var buf bytes.Buffer + var writer io.Writer = &buf + err := coverage.WriteCounters(writer) + if err != nil { + log.Warningf("coverage.WriteCounters failed: %s", err) + return + } + coverage.ClearCounters() + + fb := fileBuffer{buffer: buf.Bytes()} + cdr, err := decodecounter.NewCounterDataReader("cover", &fb) + if err != nil { + log.Warningf("decodecounter.NewCounterDataReader failed: %s", err) + return + } + + var data decodecounter.FuncPayload + for { + ok, err := cdr.NextFunc(&data) + if err != nil { + panic(fmt.Sprintf("CounterDataReader.NextFunc failed: %s", err)) + } + if !ok { + break + } + for i := 0; i < len(data.Counters); i++ { + if data.Counters[i] == 0 { + continue + } + pc := calculateSyntheticPC(data.PkgIdx, data.FuncIdx, i) + if !handler(pc) { + return + } + } + } + return +} + +// InitCoverageData initializes globalData. It should be called before any kcov +// data is written. +func InitCoverageData() { + globalData.once.Do(func() { + globalData.pkgs = make(map[uint32]*pkg) + ml := rtcov.Meta.List + for k, b := range ml { + byteSlice := unsafe.Slice(b.P, b.Len) + p := pkg{} + globalData.pkgs[uint32(k)] = &p + p.funcs = make(map[uint32]icov.FuncDesc) + pd, err := decodemeta.NewCoverageMetaDataDecoder(byteSlice, true) + if err != nil { + panic(fmt.Sprintf("decodemeta.NewCoverageMetaDataDecoder failed: %s", err)) + } + var fd icov.FuncDesc + nf := pd.NumFuncs() + for fidx := uint32(0); fidx < nf; fidx++ { + if err := pd.ReadFunc(fidx, &fd); err != nil { + panic(fmt.Sprintf("reading meta-data file: %s", err)) + } + p.funcs[fidx] = fd + } + + } + }) +} + +// reportOnce ensures that a coverage report is written at most once. For a +// complete coverage report, Report should be called during the sandbox teardown +// process. Report is called from multiple places (which may overlap) so that a +// coverage report is written in different sandbox exit scenarios. +var reportOnce sync.Once + +// Report writes out a coverage report with all blocks that have been covered. +// +// TODO(b/144576401): Decide whether this should actually be in LCOV format +func Report() error { + if reportOutput == nil { + return nil + } + + var err error + reportOnce.Do(func() { + consumeCoverageData(func(pc uint64) bool { + err = symbolize(reportOutput, pc) + return err == nil + }) + reportOutput.Close() + }) + return err +} + +// Symbolize prints information about the block corresponding to pc. +func Symbolize(out io.Writer, pc uint64) error { + if _, err := io.WriteString(out, fmt.Sprintf("%#x\n", pc)); err != nil { + return err + } + return symbolize(out, pc) +} + +func symbolize(out io.Writer, pc uint64) error { + pkgIdx, funcIdx, idx := syntheticPCToIndexes(pc) + p := globalData.pkgs[uint32(pkgIdx)] + fd := p.funcs[uint32(funcIdx)] + u := fd.Units[idx] + _, err := io.WriteString(out, fmt.Sprintf("%s:%d.%d,%d.%d\n", fd.Srcfile, u.StLine, u.StCol, u.EnLine, u.EnCol)) + return err +} + +// WriteAllBlocks prints all information about all blocks along with their +// corresponding synthetic PCs. +func WriteAllBlocks(out io.Writer) error { + for pkgIdx, p := range globalData.pkgs { + for funcIdx, fd := range p.funcs { + for idx := range fd.Units { + pc := calculateSyntheticPC(pkgIdx, funcIdx, idx) + err := Symbolize(out, pc) + if err != nil { + return err + } + + } + } + } + return nil +} + +const ( + blockIdxBits = 8 + funcIdxBits = 12 + pkgIdxShift = funcIdxBits + blockIdxBits + funcIdxShift = blockIdxBits + blockIdxMask = (1 << blockIdxBits) - 1 + funcIdxMask = (1 << funcIdxBits) - 1 +) + +func calculateSyntheticPC(pkgIdx uint32, funcIdx uint32, blockIdx int) uint64 { + pc := uint64(blockIdx) | (uint64(funcIdx) << funcIdxShift) | (uint64(pkgIdx) << pkgIdxShift) + return ^pc +} + +func syntheticPCToIndexes(pc uint64) (pkgIdx uint32, funcIdx uint32, blockIdx int) { + pc = ^pc + blockIdx = int(pc & blockIdxMask) + funcIdx = uint32((pc >> funcIdxShift) & funcIdxMask) + pkgIdx = uint32(pc >> pkgIdxShift) + return +} diff --git a/runsc/BUILD b/runsc/BUILD index d3190e07db..5c60a69a72 100644 --- a/runsc/BUILD +++ b/runsc/BUILD @@ -1,4 +1,4 @@ -load("//tools:defs.bzl", "go_binary") +load("//tools:defs.bzl", "go_binary", "go_cov") package( default_applicable_licenses = ["//:license"], @@ -74,3 +74,24 @@ sh_test( args = ["$(location :runsc)"], data = [":runsc"], ) + +go_cov( + name = "runsc_coverage", + srcs = [ + "main.go", + "//:go.mod", + "//:go.sum", + "//:gopath", + "//tools:build_cover.sh", + "//tools:go-allow-internal.patch", + ], + outs = ["runsc_cov"], + cmd = + "\"$(location //tools:build_cover.sh)\" " + + "\"$(location //runsc:runsc_cov)\" " + + "\"$(location //:gopath)\" " + + "\"$(location //:go.mod)\" " + + "\"$(location //:go.sum)\" " + + "\"$(location main.go)\" " + + "\"$(location //tools:go-allow-internal.patch)\" ", +) diff --git a/tools/BUILD b/tools/BUILD index 0a55c54187..cce2c716ce 100644 --- a/tools/BUILD +++ b/tools/BUILD @@ -45,3 +45,8 @@ bzl_library( "//:sandbox", ], ) + +exports_files([ + "build_cover.sh", + "go-allow-internal.patch", +]) diff --git a/tools/bazeldefs/go.bzl b/tools/bazeldefs/go.bzl index d94e13dfcd..5e5411ce1a 100644 --- a/tools/bazeldefs/go.bzl +++ b/tools/bazeldefs/go.bzl @@ -8,6 +8,7 @@ load("//tools/bazeldefs:defs.bzl", "select_arch", "select_system") gazelle = _gazelle go_path = _go_path +go_cov = native.genrule def _go_proto_or_grpc_library(go_library_func, name, **kwargs): if "importpath" in kwargs: diff --git a/tools/build_cover.sh b/tools/build_cover.sh new file mode 100755 index 0000000000..e521e4c4b3 --- /dev/null +++ b/tools/build_cover.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Copyright 2025 The gVisor Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -x -e -o pipefail + +dst=$(realpath "$1") +gopath_zip="$2" +go_mod="$3" +go_sum="$4" +runsc_main_go="$5" +golang_patch=$(realpath "$6") + +mkdir .gocache +GOMODCACHE="$(pwd)/.gocache" +GOCACHE="$(pwd)/.gocache" +export GOMODCACHE GOCACHE +( + # The gVisor code coverate implementation uses internal packages. More details + # can be found here: https://github.com/golang/go/issues/76098. + curl -L https://go.dev/dl/go1.25.3.src.tar.gz | tar -xz + cd go + patch -p1 < "$golang_patch" + cd src + ./make.bash +) + +goroot_dir="$(pwd)/go" +go_tool="$goroot_dir/bin/go" + +gvisor_gopath="gopath" + +unzip -q "$gopath_zip" -d "$gvisor_gopath" +cp "$go_mod" "$go_sum" "$gvisor_gopath/src/gvisor.dev/gvisor/" +mkdir -p "$gvisor_gopath/src/gvisor.dev/gvisor/runsc" +cp "$runsc_main_go" "$gvisor_gopath/src/gvisor.dev/gvisor/runsc/main.go" +cd "$gvisor_gopath/src/gvisor.dev/gvisor/" +export GOROOT="$goroot_dir" +gopkgs=$("$go_tool" list ./... | grep -v pkg/sentry/platform | grep -v pkg/ring0 | grep -v pkg/coverage | paste -sd,) +"$go_tool" build --tags kcov,opensource -cover -coverpkg="$gopkg" -covermode=atomic -o "$dst" runsc/main.go diff --git a/tools/defs.bzl b/tools/defs.bzl index f4c3d76654..e92f5185cd 100644 --- a/tools/defs.bzl +++ b/tools/defs.bzl @@ -7,7 +7,7 @@ change for Google-internal and bazel-compatible rules. load("//tools/bazeldefs:cc.bzl", _cc_binary = "cc_binary", _cc_flags_supplier = "cc_flags_supplier", _cc_grpc_library = "cc_grpc_library", _cc_library = "cc_library", _cc_proto_library = "cc_proto_library", _cc_test = "cc_test", _cc_toolchain = "cc_toolchain", _gbenchmark = "gbenchmark", _gbenchmark_internal = "gbenchmark_internal", _grpcpp = "grpcpp", _gtest = "gtest", _select_gtest = "select_gtest") load("//tools/bazeldefs:defs.bzl", _BuildSettingInfo = "BuildSettingInfo", _bool_flag = "bool_flag", _bpf_program = "bpf_program", _build_test = "build_test", _bzl_library = "bzl_library", _coreutil = "coreutil", _default_net_util = "default_net_util", _more_shards = "more_shards", _most_shards = "most_shards", _proto_library = "proto_library", _select_system = "select_system", _short_path = "short_path", _version = "version") -load("//tools/bazeldefs:go.bzl", _gazelle = "gazelle", _go_binary = "go_binary", _go_grpc_and_proto_libraries = "go_grpc_and_proto_libraries", _go_library = "go_library", _go_path = "go_path", _go_proto_library = "go_proto_library", _go_test = "go_test", _gotsan_flag_values = "gotsan_flag_values", _gotsan_values = "gotsan_values", _select_goarch = "select_goarch", _select_goos = "select_goos") +load("//tools/bazeldefs:go.bzl", _gazelle = "gazelle", _go_binary = "go_binary", _go_cov = "go_cov", _go_grpc_and_proto_libraries = "go_grpc_and_proto_libraries", _go_library = "go_library", _go_path = "go_path", _go_proto_library = "go_proto_library", _go_test = "go_test", _gotsan_flag_values = "gotsan_flag_values", _gotsan_values = "gotsan_values", _select_goarch = "select_goarch", _select_goos = "select_goos") load("//tools/bazeldefs:pkg.bzl", _pkg_deb = "pkg_deb", _pkg_tar = "pkg_tar") load("//tools/bazeldefs:platforms.bzl", _default_platform = "default_platform", _platform_capabilities = "platform_capabilities", _platforms = "platforms", _save_restore_platforms = "save_restore_platforms") load("//tools/bazeldefs:tags.bzl", _go_suffixes = "go_suffixes", _local_test_tags = "local_test_tags") @@ -44,6 +44,7 @@ select_gtest = _select_gtest # Go rules. gazelle = _gazelle go_path = _go_path +go_cov = _go_cov select_goos = _select_goos select_goarch = _select_goarch go_proto_library = _go_proto_library diff --git a/tools/go-allow-internal.patch b/tools/go-allow-internal.patch new file mode 100644 index 0000000000..7154ab7736 --- /dev/null +++ b/tools/go-allow-internal.patch @@ -0,0 +1,12 @@ +diff --git a/src/cmd/go/internal/load/pkg.go b/src/cmd/go/internal/load/pkg.go +index 1f791546f9..809e5f142b 100644 +--- a/src/cmd/go/internal/load/pkg.go ++++ b/src/cmd/go/internal/load/pkg.go +@@ -1464,6 +1464,7 @@ func reusePackage(p *Package, stk *ImportStack) *Package { + // If the import is allowed, disallowInternal returns the original package p. + // If not, it returns a new package containing just an appropriate error. + func disallowInternal(ctx context.Context, srcDir string, importer *Package, importerPath string, p *Package, stk *ImportStack) *PackageError { ++ return nil + // golang.org/s/go14internal: + // An import of a path containing the element “internal” + // is disallowed if the importing code is outside the tree