mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
[dev.fuzz] internal/fuzz: coarsen each coverage counter when taking a snapshot
When taking a snapshot of coverage counters, round each counter down to the nearest power of 2. After coarsening, at most 1 bit per byte will be set. This lets the coordinator use a coverage array as a mask that distinguish between code that's executed many times for a given input and code that's executed once or a few times. For example, if a byte in this array has the value 12, it means the block has been executed at least 4 times and at least 8 times with different inputs. Also change the term "edge" to "bits" or just be more vague about how coverage is represented. Also add more code that may be "interesting" in test_fuzz_cache. Change-Id: I67bf2adb298fb8efd7680b069a476c27e5fdbdae Reviewed-on: https://go-review.googlesource.com/c/go/+/338829 Trust: Jay Conrod <jayconrod@google.com> Run-TryBot: Jay Conrod <jayconrod@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Roland Shoemaker <roland@golang.org>
This commit is contained in:
parent
f89b4c8d82
commit
7b6893d2d2
3 changed files with 96 additions and 49 deletions
|
|
@ -6,6 +6,7 @@ package fuzz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"internal/unsafeheader"
|
"internal/unsafeheader"
|
||||||
|
"math/bits"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -36,26 +37,70 @@ func ResetCoverage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SnapshotCoverage copies the current counter values into coverageSnapshot,
|
// SnapshotCoverage copies the current counter values into coverageSnapshot,
|
||||||
// preserving them for later inspection.
|
// preserving them for later inspection. SnapshotCoverage also rounds each
|
||||||
|
// counter down to the nearest power of two. This lets the coordinator store
|
||||||
|
// multiple values for each counter by OR'ing them together.
|
||||||
func SnapshotCoverage() {
|
func SnapshotCoverage() {
|
||||||
cov := coverage()
|
cov := coverage()
|
||||||
if coverageSnapshot == nil {
|
for i, b := range cov {
|
||||||
coverageSnapshot = make([]byte, len(cov))
|
b |= b >> 1
|
||||||
|
b |= b >> 2
|
||||||
|
b |= b >> 4
|
||||||
|
b -= b >> 1
|
||||||
|
coverageSnapshot[i] = b
|
||||||
}
|
}
|
||||||
copy(coverageSnapshot, cov)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func countEdges(cov []byte) int {
|
// diffCoverage returns a set of bits set in snapshot but not in base.
|
||||||
n := 0
|
// If there are no new bits set, diffCoverage returns nil.
|
||||||
for _, c := range cov {
|
func diffCoverage(base, snapshot []byte) []byte {
|
||||||
if c > 0 {
|
found := false
|
||||||
n++
|
for i := range snapshot {
|
||||||
|
if snapshot[i]&^base[i] != 0 {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
diff := make([]byte, len(snapshot))
|
||||||
|
for i := range diff {
|
||||||
|
diff[i] = snapshot[i] &^ base[i]
|
||||||
|
}
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
// countNewCoverageBits returns the number of bits set in snapshot that are not
|
||||||
|
// set in base.
|
||||||
|
func countNewCoverageBits(base, snapshot []byte) int {
|
||||||
|
n := 0
|
||||||
|
for i := range snapshot {
|
||||||
|
n += bits.OnesCount8(snapshot[i] &^ base[i])
|
||||||
|
}
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
var coverageSnapshot []byte
|
// hasCoverageBit returns true if snapshot has at least one bit set that is
|
||||||
|
// also set in base.
|
||||||
|
func hasCoverageBit(base, snapshot []byte) bool {
|
||||||
|
for i := range snapshot {
|
||||||
|
if snapshot[i]&base[i] != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func countBits(cov []byte) int {
|
||||||
|
n := 0
|
||||||
|
for _, c := range cov {
|
||||||
|
n += bits.OnesCount8(c)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
var coverageSnapshot = make([]byte, len(coverage()))
|
||||||
|
|
||||||
// _counters and _ecounters mark the start and end, respectively, of where
|
// _counters and _ecounters mark the start and end, respectively, of where
|
||||||
// the 8-bit coverage counters reside in memory. They're known to cmd/link,
|
// the 8-bit coverage counters reside in memory. They're known to cmd/link,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/bits"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -235,8 +236,8 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
|
||||||
stop(err)
|
stop(err)
|
||||||
}
|
}
|
||||||
} else if result.coverageData != nil {
|
} else if result.coverageData != nil {
|
||||||
newEdges := c.updateCoverage(result.coverageData)
|
newBitCount := c.updateCoverage(result.coverageData)
|
||||||
if newEdges > 0 && !c.coverageOnlyRun() {
|
if newBitCount > 0 && !c.coverageOnlyRun() {
|
||||||
// Found an interesting value that expanded coverage.
|
// Found an interesting value that expanded coverage.
|
||||||
// This is not a crasher, but we should add it to the
|
// This is not a crasher, but we should add it to the
|
||||||
// on-disk corpus, and prioritize it for future fuzzing.
|
// on-disk corpus, and prioritize it for future fuzzing.
|
||||||
|
|
@ -255,13 +256,13 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
|
||||||
if printDebugInfo() {
|
if printDebugInfo() {
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
c.opts.Log,
|
c.opts.Log,
|
||||||
"DEBUG new interesting input, elapsed: %s, id: %s, parent: %s, gen: %d, new edges: %d, total edges: %d, size: %d, exec time: %s\n",
|
"DEBUG new interesting input, elapsed: %s, id: %s, parent: %s, gen: %d, new bits: %d, total bits: %d, size: %d, exec time: %s\n",
|
||||||
time.Since(c.startTime),
|
time.Since(c.startTime),
|
||||||
result.entry.Name,
|
result.entry.Name,
|
||||||
result.entry.Parent,
|
result.entry.Parent,
|
||||||
result.entry.Generation,
|
result.entry.Generation,
|
||||||
newEdges,
|
newBitCount,
|
||||||
countEdges(c.coverageData),
|
countBits(c.coverageMask),
|
||||||
len(result.entry.Data),
|
len(result.entry.Data),
|
||||||
result.entryDuration,
|
result.entryDuration,
|
||||||
)
|
)
|
||||||
|
|
@ -271,10 +272,10 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
|
||||||
if printDebugInfo() {
|
if printDebugInfo() {
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
c.opts.Log,
|
c.opts.Log,
|
||||||
"DEBUG processed an initial input, elapsed: %s, id: %s, new edges: %d, size: %d, exec time: %s\n",
|
"DEBUG processed an initial input, elapsed: %s, id: %s, new bits: %d, size: %d, exec time: %s\n",
|
||||||
time.Since(c.startTime),
|
time.Since(c.startTime),
|
||||||
result.entry.Parent,
|
result.entry.Parent,
|
||||||
newEdges,
|
newBitCount,
|
||||||
len(result.entry.Data),
|
len(result.entry.Data),
|
||||||
result.entryDuration,
|
result.entryDuration,
|
||||||
)
|
)
|
||||||
|
|
@ -288,10 +289,10 @@ func CoordinateFuzzing(ctx context.Context, opts CoordinateFuzzingOpts) (err err
|
||||||
if printDebugInfo() {
|
if printDebugInfo() {
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
c.opts.Log,
|
c.opts.Log,
|
||||||
"DEBUG finished processing input corpus, elapsed: %s, entries: %d, initial coverage edges: %d\n",
|
"DEBUG finished processing input corpus, elapsed: %s, entries: %d, initial coverage bits: %d\n",
|
||||||
time.Since(c.startTime),
|
time.Since(c.startTime),
|
||||||
len(c.corpus.entries),
|
len(c.corpus.entries),
|
||||||
countEdges(c.coverageData),
|
countBits(c.coverageMask),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -494,7 +495,13 @@ type coordinator struct {
|
||||||
// which corpus value to send next (or generates something new).
|
// which corpus value to send next (or generates something new).
|
||||||
corpusIndex int
|
corpusIndex int
|
||||||
|
|
||||||
coverageData []byte
|
// coverageMask aggregates coverage that was found for all inputs in the
|
||||||
|
// corpus. Each byte represents a single basic execution block. Each set bit
|
||||||
|
// within the byte indicates that an input has triggered that block at least
|
||||||
|
// 1 << n times, where n is the position of the bit in the byte. For example, a
|
||||||
|
// value of 12 indicates that separate inputs have triggered this block
|
||||||
|
// between 4-7 times and 8-15 times.
|
||||||
|
coverageMask []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCoordinator(opts CoordinateFuzzingOpts) (*coordinator, error) {
|
func newCoordinator(opts CoordinateFuzzingOpts) (*coordinator, error) {
|
||||||
|
|
@ -535,7 +542,7 @@ func newCoordinator(opts CoordinateFuzzingOpts) (*coordinator, error) {
|
||||||
c.covOnlyInputs = 0
|
c.covOnlyInputs = 0
|
||||||
} else {
|
} else {
|
||||||
// Set c.coverageData to a clean []byte full of zeros.
|
// Set c.coverageData to a clean []byte full of zeros.
|
||||||
c.coverageData = make([]byte, covSize)
|
c.coverageMask = make([]byte, covSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.covOnlyInputs > 0 {
|
if c.covOnlyInputs > 0 {
|
||||||
|
|
@ -555,8 +562,6 @@ func (c *coordinator) updateStats(result fuzzResult) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *coordinator) logStats() {
|
func (c *coordinator) logStats() {
|
||||||
// TODO(jayconrod,katiehockman): consider printing the amount of coverage
|
|
||||||
// that has been reached so far (perhaps a percentage of edges?)
|
|
||||||
elapsed := time.Since(c.startTime)
|
elapsed := time.Since(c.startTime)
|
||||||
if c.coverageOnlyRun() {
|
if c.coverageOnlyRun() {
|
||||||
fmt.Fprintf(c.opts.Log, "gathering baseline coverage, elapsed: %.1fs, workers: %d, left: %d\n", elapsed.Seconds(), c.opts.Parallel, c.covOnlyInputs)
|
fmt.Fprintf(c.opts.Log, "gathering baseline coverage, elapsed: %.1fs, workers: %d, left: %d\n", elapsed.Seconds(), c.opts.Parallel, c.covOnlyInputs)
|
||||||
|
|
@ -578,8 +583,9 @@ func (c *coordinator) nextInput() (fuzzInput, bool) {
|
||||||
input := fuzzInput{
|
input := fuzzInput{
|
||||||
entry: c.corpus.entries[c.corpusIndex],
|
entry: c.corpus.entries[c.corpusIndex],
|
||||||
interestingCount: c.interestingCount,
|
interestingCount: c.interestingCount,
|
||||||
coverageData: c.coverageData,
|
coverageData: make([]byte, len(c.coverageMask)),
|
||||||
}
|
}
|
||||||
|
copy(input.coverageData, c.coverageMask)
|
||||||
c.corpusIndex = (c.corpusIndex + 1) % (len(c.corpus.entries))
|
c.corpusIndex = (c.corpusIndex + 1) % (len(c.corpus.entries))
|
||||||
|
|
||||||
if c.coverageOnlyRun() {
|
if c.coverageOnlyRun() {
|
||||||
|
|
@ -607,22 +613,20 @@ func (c *coordinator) coverageOnlyRun() bool {
|
||||||
return c.covOnlyInputs > 0
|
return c.covOnlyInputs > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateCoverage updates c.coverageData for all edges that have a higher
|
// updateCoverage sets bits in c.coverageData that are set in newCoverage.
|
||||||
// counter value in newCoverage. It return true if a new edge was hit.
|
// updateCoverage returns the number of newly set bits. See the comment on
|
||||||
|
// coverageMask for the format.
|
||||||
func (c *coordinator) updateCoverage(newCoverage []byte) int {
|
func (c *coordinator) updateCoverage(newCoverage []byte) int {
|
||||||
if len(newCoverage) != len(c.coverageData) {
|
if len(newCoverage) != len(c.coverageMask) {
|
||||||
panic(fmt.Sprintf("num edges changed at runtime: %d, expected %d", len(newCoverage), len(c.coverageData)))
|
panic(fmt.Sprintf("number of coverage counters changed at runtime: %d, expected %d", len(newCoverage), len(c.coverageMask)))
|
||||||
}
|
}
|
||||||
newEdges := 0
|
newBitCount := 0
|
||||||
for i := range newCoverage {
|
for i := range newCoverage {
|
||||||
if newCoverage[i] > c.coverageData[i] {
|
diff := newCoverage[i] &^ c.coverageMask[i]
|
||||||
if c.coverageData[i] == 0 {
|
newBitCount += bits.OnesCount8(diff)
|
||||||
newEdges++
|
c.coverageMask[i] |= newCoverage[i]
|
||||||
}
|
|
||||||
c.coverageData[i] = newCoverage[i]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newEdges
|
return newBitCount
|
||||||
}
|
}
|
||||||
|
|
||||||
// readCache creates a combined corpus from seed values and values in the cache
|
// readCache creates a combined corpus from seed values and values in the cache
|
||||||
|
|
|
||||||
|
|
@ -569,10 +569,10 @@ type workerServer struct {
|
||||||
workerComm
|
workerComm
|
||||||
m *mutator
|
m *mutator
|
||||||
|
|
||||||
// coverageData is the local coverage data for the worker. It is
|
// coverageMask is the local coverage data for the worker. It is
|
||||||
// periodically updated to reflect the data in the coordinator when new
|
// periodically updated to reflect the data in the coordinator when new
|
||||||
// edges are hit.
|
// coverage is found.
|
||||||
coverageData []byte
|
coverageMask []byte
|
||||||
|
|
||||||
// fuzzFn runs the worker's fuzz function on the given input and returns
|
// fuzzFn runs the worker's fuzz function on the given input and returns
|
||||||
// an error if it finds a crasher (the process may also exit or crash).
|
// an error if it finds a crasher (the process may also exit or crash).
|
||||||
|
|
@ -633,7 +633,7 @@ func (ws *workerServer) serve(ctx context.Context) error {
|
||||||
// the crashing input with this information, since the PRNG is deterministic.
|
// the crashing input with this information, since the PRNG is deterministic.
|
||||||
func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzResponse) {
|
func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzResponse) {
|
||||||
if args.CoverageData != nil {
|
if args.CoverageData != nil {
|
||||||
ws.coverageData = args.CoverageData
|
ws.coverageMask = args.CoverageData
|
||||||
}
|
}
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() { resp.TotalDuration = time.Since(start) }()
|
defer func() { resp.TotalDuration = time.Since(start) }()
|
||||||
|
|
@ -666,8 +666,8 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzRespo
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
if cov := coverage(); len(cov) != len(ws.coverageData) {
|
if cov := coverage(); len(cov) != len(ws.coverageMask) {
|
||||||
panic(fmt.Sprintf("num edges changed at runtime: %d, expected %d", len(cov), len(ws.coverageData)))
|
panic(fmt.Sprintf("number of coverage counters changed at runtime: %d, expected %d", len(cov), len(ws.coverageMask)))
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
@ -687,13 +687,11 @@ func (ws *workerServer) fuzz(ctx context.Context, args fuzzArgs) (resp fuzzRespo
|
||||||
}
|
}
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
for i := range coverageSnapshot {
|
if countNewCoverageBits(ws.coverageMask, coverageSnapshot) > 0 {
|
||||||
if ws.coverageData[i] == 0 && coverageSnapshot[i] > ws.coverageData[i] {
|
// TODO(jayconrod,katie): minimize this.
|
||||||
// TODO(jayconrod,katie): minimize this.
|
resp.CoverageData = coverageSnapshot
|
||||||
resp.CoverageData = coverageSnapshot
|
resp.InterestingDuration = fDur
|
||||||
resp.InterestingDuration = fDur
|
return resp
|
||||||
return resp
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if args.Limit > 0 && mem.header().count == args.Limit {
|
if args.Limit > 0 && mem.header().count == args.Limit {
|
||||||
return resp
|
return resp
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue