func TestMinimizeInput(t *testing.T) {
type testcase struct {
+ name string
fn func(CorpusEntry) error
input []interface{}
expected []interface{}
}
cases := []testcase{
{
+ name: "ones_byte",
fn: func(e CorpusEntry) error {
b := e.Values[0].([]byte)
ones := 0
expected: []interface{}{[]byte{1, 1, 1}},
},
{
+ name: "ones_string",
fn: func(e CorpusEntry) error {
b := e.Values[0].(string)
ones := 0
expected: []interface{}{"111"},
},
{
+ name: "int",
fn: func(e CorpusEntry) error {
i := e.Values[0].(int)
if i > 100 {
expected: []interface{}{123},
},
{
+ name: "int8",
fn: func(e CorpusEntry) error {
i := e.Values[0].(int8)
if i > 10 {
expected: []interface{}{int8(12)},
},
{
+ name: "int16",
fn: func(e CorpusEntry) error {
i := e.Values[0].(int16)
if i > 10 {
expected: []interface{}{int32(21)},
},
{
+ name: "int32",
fn: func(e CorpusEntry) error {
i := e.Values[0].(uint)
if i > 10 {
expected: []interface{}{uint(12)},
},
{
+ name: "uint8",
fn: func(e CorpusEntry) error {
i := e.Values[0].(uint8)
if i > 10 {
expected: []interface{}{uint8(25)},
},
{
+ name: "uint16",
fn: func(e CorpusEntry) error {
i := e.Values[0].(uint16)
if i > 10 {
expected: []interface{}{uint16(65)},
},
{
+ name: "uint32",
fn: func(e CorpusEntry) error {
i := e.Values[0].(uint32)
if i > 10 {
expected: []interface{}{uint32(42)},
},
{
+ name: "float32",
fn: func(e CorpusEntry) error {
if i := e.Values[0].(float32); i == 1.23 {
return nil
expected: []interface{}{float32(1.2)},
},
{
+ name: "float64",
fn: func(e CorpusEntry) error {
if i := e.Values[0].(float64); i == 1.23 {
return nil
// If we are on a 64 bit platform add int64 and uint64 tests
if v := int64(1<<63 - 1); int64(int(v)) == v {
cases = append(cases, testcase{
+ name: "int64",
fn: func(e CorpusEntry) error {
i := e.Values[0].(int64)
if i > 10 {
input: []interface{}{int64(1<<63 - 1)},
expected: []interface{}{int64(92)},
}, testcase{
+ name: "uint64",
fn: func(e CorpusEntry) error {
i := e.Values[0].(uint64)
if i > 10 {
}
for _, tc := range cases {
- ws := &workerServer{
- fuzzFn: tc.fn,
- }
- count := int64(0)
- vals := tc.input
- err := ws.minimizeInput(context.Background(), vals, &count, 0)
- if err == nil {
- t.Error("minimizeInput didn't fail")
- }
- if expected := fmt.Sprintf("bad %v", tc.input[0]); err.Error() != expected {
- t.Errorf("unexpected error: got %s, want %s", err, expected)
- }
- if !reflect.DeepEqual(vals, tc.expected) {
- t.Errorf("unexpected results: got %v, want %v", vals, tc.expected)
- }
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ ws := &workerServer{
+ fuzzFn: tc.fn,
+ }
+ count := int64(0)
+ vals := tc.input
+ success, err := ws.minimizeInput(context.Background(), vals, &count, 0)
+ if !success {
+ t.Errorf("minimizeInput did not succeed")
+ }
+ if err == nil {
+ t.Error("minimizeInput didn't fail")
+ }
+ if expected := fmt.Sprintf("bad %v", tc.input[0]); err.Error() != expected {
+ t.Errorf("unexpected error: got %s, want %s", err, expected)
+ }
+ if !reflect.DeepEqual(vals, tc.expected) {
+ t.Errorf("unexpected results: got %v, want %v", vals, tc.expected)
+ }
+ })
}
}
totalDuration: resp.TotalDuration,
entryDuration: resp.InterestingDuration,
entry: entry,
- }
- if resp.Err != "" {
- result.crasherMsg = resp.Err
- } else if resp.CoverageData != nil {
- result.coverageData = resp.CoverageData
+ crasherMsg: resp.Err,
+ coverageData: resp.CoverageData,
}
w.coordinator.resultC <- result
// Received input to minimize from coordinator.
result, err := w.minimize(ctx, input)
if err != nil {
- // Failed to minimize. Send back the original crash.
- fmt.Fprintln(w.coordinator.opts.Log, err)
+ // Error minimizing. Send back the original input. If it didn't cause
+ // an error before, report it as causing an error now.
+ // TODO(fuzz): double-check this is handled correctly when
+ // implementing -keepfuzzing.
result = fuzzResult{
entry: input.entry,
crasherMsg: input.crasherMsg,
minimizeAttempted: true,
limit: input.limit,
}
+ if result.crasherMsg == "" {
+ result.crasherMsg = err.Error()
+ }
}
w.coordinator.resultC <- result
}
}
// minimize tells a worker process to attempt to find a smaller value that
-// causes an error. minimize may restart the worker repeatedly if the error
-// causes (or already caused) the worker process to terminate.
-//
-// TODO: support minimizing inputs that expand coverage in a specific way,
-// for example, by ensuring that an input activates a specific set of counters.
+// either causes an error (if we started minimizing because we found an input
+// that causes an error) or preserves new coverage (if we started minimizing
+// because we found an input that expands coverage).
func (w *worker) minimize(ctx context.Context, input fuzzMinimizeInput) (min fuzzResult, err error) {
if w.coordinator.opts.MinimizeTimeout != 0 {
var cancel func()
return fuzzResult{}, fmt.Errorf("fuzzing process terminated unexpectedly while minimizing: %w", w.waitErr)
}
- if resp.Err == "" {
- // Minimization did not find a smaller input that caused a crash.
- return min, nil
+ if input.crasherMsg != "" && resp.Err == "" && !resp.Success {
+ return fuzzResult{}, fmt.Errorf("attempted to minimize but could not reproduce")
}
+
min.crasherMsg = resp.Err
min.count = resp.Count
min.totalDuration = resp.Duration
// minimizeResponse contains results from workerServer.minimize.
type minimizeResponse struct {
- // Err is the error string caused by the value in shared memory.
- // If Err is empty, minimize was unable to find any shorter values that
- // caused errors, and the value in shared memory is the original value.
+ // Success is true if the worker found a smaller input, stored in shared
+ // memory, that was "interesting" for the same reason as the original input.
+ Success bool
+
+ // Err is the error string caused by the value in shared memory, if any.
Err string
// Duration is the time spent minimizing, not including starting or cleaning up.
// Minimize the values in vals, then write to shared memory. We only write
// to shared memory after completing minimization. If the worker terminates
// unexpectedly before then, the coordinator will use the original input.
- err = ws.minimizeInput(ctx, vals, &mem.header().count, args.Limit)
+ resp.Success, err = ws.minimizeInput(ctx, vals, &mem.header().count, args.Limit)
writeToMem(vals, mem)
if err != nil {
resp.Err = err.Error()
// mem just in case an unrecoverable error occurs. It uses the context to
// determine how long to run, stopping once closed. It returns the last error it
// found.
-func (ws *workerServer) minimizeInput(ctx context.Context, vals []interface{}, count *int64, limit int64) error {
+func (ws *workerServer) minimizeInput(ctx context.Context, vals []interface{}, count *int64, limit int64) (success bool, retErr error) {
shouldStop := func() bool {
return ctx.Err() != nil || (limit > 0 && *count >= limit)
}
if shouldStop() {
- return nil
+ return false, nil
}
var valI int
- var retErr error
tryMinimized := func(candidate interface{}) bool {
prev := vals[valI]
// Set vals[valI] to the candidate after it has been
for valI = range vals {
if shouldStop() {
- return retErr
+ break
}
switch v := vals[valI].(type) {
case bool:
panic("unreachable")
}
}
- return retErr
+ return retErr != nil, retErr
}
func writeToMem(vals []interface{}, mem *sharedMem) {