Add runsc symbolize command.

This command takes instruction pointers from stdin and converts them into their
corresponding file names and line/column numbers in the runsc source code. The
inputs are not interpreted as actual addresses, but as synthetic values that are
exposed through /sys/kernel/debug/kcov. One can extract coverage information
from kcov and translate those values into locations in the source code by
running symbolize on the same runsc binary.

This will allow us to generate syzkaller coverage reports.

PiperOrigin-RevId: 347089624
This commit is contained in:
Dean Deng 2020-12-11 15:40:39 -08:00 committed by gVisor bot
parent d45420b152
commit 80379894d3
4 changed files with 192 additions and 34 deletions

View File

@ -27,6 +27,7 @@ import (
"io"
"sort"
"sync/atomic"
"testing"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/usermem"
@ -34,12 +35,6 @@ import (
"github.com/bazelbuild/rules_go/go/tools/coverdata"
)
// KcovAvailable returns whether the kcov coverage interface is available. It is
// available as long as coverage is enabled for some files.
func KcovAvailable() bool {
return len(coverdata.Cover.Blocks) > 0
}
// coverageMu must be held while accessing coverdata.Cover. This prevents
// concurrent reads/writes from multiple threads collecting coverage data.
var coverageMu sync.RWMutex
@ -47,6 +42,22 @@ var coverageMu sync.RWMutex
// once ensures that globalData is only initialized once.
var once sync.Once
// 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
// KcovAvailable returns whether the kcov coverage interface is available. It is
// available as long as coverage is enabled for some files.
func KcovAvailable() bool {
return len(coverdata.Cover.Blocks) > 0
}
var globalData struct {
// files is the set of covered files sorted by filename. It is calculated at
// startup.
@ -104,14 +115,14 @@ var coveragePool = sync.Pool{
// coverage tools, we reset the global coverage data every time this function is
// run.
func ConsumeCoverageData(w io.Writer) int {
once.Do(initCoverageData)
InitCoverageData()
coverageMu.Lock()
defer coverageMu.Unlock()
total := 0
var pcBuffer [8]byte
for fileIndex, file := range globalData.files {
for fileNum, file := range globalData.files {
counters := coverdata.Cover.Counters[file]
for index := 0; index < len(counters); index++ {
if atomic.LoadUint32(&counters[index]) == 0 {
@ -119,7 +130,7 @@ func ConsumeCoverageData(w io.Writer) int {
}
// Non-zero coverage data found; consume it and report as a PC.
atomic.StoreUint32(&counters[index], 0)
pc := globalData.syntheticPCs[fileIndex][index]
pc := globalData.syntheticPCs[fileNum][index]
usermem.ByteOrder.PutUint64(pcBuffer[:], pc)
n, err := w.Write(pcBuffer[:])
if err != nil {
@ -142,31 +153,84 @@ func ConsumeCoverageData(w io.Writer) int {
return total
}
// initCoverageData initializes globalData. It should only be called once,
// before any kcov data is written.
func initCoverageData() {
// First, order all files. Then calculate synthetic PCs for every block
// (using the well-defined ordering for files as well).
for file := range coverdata.Cover.Blocks {
globalData.files = append(globalData.files, file)
}
sort.Strings(globalData.files)
// nextSyntheticPC is the first PC that we generate for a block.
//
// This uses a standard-looking kernel range for simplicity.
//
// FIXME(b/160639712): This is only necessary because syzkaller requires
// addresses in the kernel range. If we can remove this constraint, then we
// should be able to use the actual addresses.
var nextSyntheticPC uint64 = 0xffffffff80000000
for _, file := range globalData.files {
blocks := coverdata.Cover.Blocks[file]
thisFile := make([]uint64, 0, len(blocks))
for range blocks {
thisFile = append(thisFile, nextSyntheticPC)
nextSyntheticPC++ // Advance.
// InitCoverageData initializes globalData. It should be called before any kcov
// data is written.
func InitCoverageData() {
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.Cover.Blocks {
globalData.files = append(globalData.files, file)
}
sort.Strings(globalData.files)
for fileNum, file := range globalData.files {
blocks := coverdata.Cover.Blocks[file]
pcs := make([]uint64, 0, len(blocks))
for blockNum := range blocks {
pcs = append(pcs, calculateSyntheticPC(fileNum, blockNum))
}
globalData.syntheticPCs = append(globalData.syntheticPCs, pcs)
}
})
}
// Symbolize prints 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
}
writeBlock(out, pc, file, block)
return nil
}
// WriteAllBlocks prints all information about all blocks along with their
// corresponding synthetic PCs.
func WriteAllBlocks(out io.Writer) {
for fileNum, file := range globalData.files {
for blockNum, block := range coverdata.Cover.Blocks[file] {
writeBlock(out, calculateSyntheticPC(fileNum, blockNum), file, block)
}
globalData.syntheticPCs = append(globalData.syntheticPCs, thisFile)
}
}
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.Cover.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
}
func writeBlock(out io.Writer, pc uint64, file string, block testing.CoverBlock) {
io.WriteString(out, fmt.Sprintf("%#x\n", pc))
io.WriteString(out, fmt.Sprintf("%s:%d.%d,%d.%d\n", file, block.Line0, block.Col0, block.Line1, block.Col1))
}

View File

@ -83,6 +83,7 @@ func Main(version string) {
subcommands.Register(new(cmd.Spec), "")
subcommands.Register(new(cmd.State), "")
subcommands.Register(new(cmd.Start), "")
subcommands.Register(new(cmd.Symbolize), "")
subcommands.Register(new(cmd.Wait), "")
// Register internal commands with the internal group name. This causes

View File

@ -32,6 +32,7 @@ go_library(
"start.go",
"state.go",
"statefile.go",
"symbolize.go",
"syscalls.go",
"wait.go",
],
@ -39,6 +40,7 @@ go_library(
"//runsc:__subpackages__",
],
deps = [
"//pkg/coverage",
"//pkg/log",
"//pkg/p9",
"//pkg/sentry/control",

91
runsc/cmd/symbolize.go Normal file
View File

@ -0,0 +1,91 @@
// 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.
package cmd
import (
"bufio"
"context"
"os"
"strconv"
"strings"
"github.com/google/subcommands"
"gvisor.dev/gvisor/pkg/coverage"
"gvisor.dev/gvisor/runsc/flag"
)
// Symbolize implements subcommands.Command for the "symbolize" command.
type Symbolize struct {
dumpAll bool
}
// Name implements subcommands.Command.Name.
func (*Symbolize) Name() string {
return "symbolize"
}
// Synopsis implements subcommands.Command.Synopsis.
func (*Symbolize) Synopsis() string {
return "Convert synthetic instruction pointers from kcov into positions in the runsc source code. Only used when Go coverage is enabled."
}
// Usage implements subcommands.Command.Usage.
func (*Symbolize) Usage() string {
return `symbolize - converts synthetic instruction pointers into positions in the runsc source code.
This command takes instruction pointers from stdin and converts them into their
corresponding file names and line/column numbers in the runsc source code. The
inputs are not interpreted as actual addresses, but as synthetic values that are
exposed through /sys/kernel/debug/kcov. One can extract coverage information
from kcov and translate those values into locations in the source code by
running symbolize on the same runsc binary.
`
}
// SetFlags implements subcommands.Command.SetFlags.
func (c *Symbolize) SetFlags(f *flag.FlagSet) {
f.BoolVar(&c.dumpAll, "all", false, "dump information on all coverage blocks along with their synthetic PCs")
}
// Execute implements subcommands.Command.Execute.
func (c *Symbolize) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
if f.NArg() != 0 {
f.Usage()
return subcommands.ExitUsageError
}
if !coverage.KcovAvailable() {
return Errorf("symbolize can only be used when coverage is available.")
}
coverage.InitCoverageData()
if c.dumpAll {
coverage.WriteAllBlocks(os.Stdout)
return subcommands.ExitSuccess
}
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
// Input is always base 16, but may or may not have a leading "0x".
str := strings.TrimPrefix(scanner.Text(), "0x")
pc, err := strconv.ParseUint(str, 16 /* base */, 64 /* bitSize */)
if err != nil {
return Errorf("Failed to symbolize \"%s\": %v", scanner.Text(), err)
}
if err := coverage.Symbolize(os.Stdout, pc); err != nil {
return Errorf("Failed to symbolize \"%s\": %v", scanner.Text(), err)
}
}
return subcommands.ExitSuccess
}