Source file
src/testing/fuzz.go
1
2
3
4
5 package testing
6
7 import (
8 "bytes"
9 "errors"
10 "flag"
11 "fmt"
12 "io"
13 "os"
14 "path/filepath"
15 "reflect"
16 "runtime"
17 "sync/atomic"
18 "time"
19 )
20
21 func initFuzzFlags() {
22 matchFuzz = flag.String("test.fuzz", "", "run the fuzz test matching `regexp`")
23 flag.Var(&fuzzDuration, "test.fuzztime", "time to spend fuzzing; default is to run indefinitely")
24 flag.Var(&minimizeDuration, "test.fuzzminimizetime", "time to spend minimizing a value after finding a failing input")
25
26 fuzzCacheDir = flag.String("test.fuzzcachedir", "", "directory where interesting fuzzing inputs are stored (for use only by cmd/go)")
27 isFuzzWorker = flag.Bool("test.fuzzworker", false, "coordinate with the parent process to fuzz random values (for use only by cmd/go)")
28 }
29
30 var (
31 matchFuzz *string
32 fuzzDuration durationOrCountFlag
33 minimizeDuration = durationOrCountFlag{d: 60 * time.Second, allowZero: true}
34 fuzzCacheDir *string
35 isFuzzWorker *bool
36
37
38
39 corpusDir = "testdata/fuzz"
40 )
41
42
43
44
45 const fuzzWorkerExitCode = 70
46
47
48
49 type InternalFuzzTarget struct {
50 Name string
51 Fn func(f *F)
52 }
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 type F struct {
70 common
71 fuzzContext *fuzzContext
72 testContext *testContext
73
74
75
76 inFuzzFn bool
77
78
79
80 corpus []corpusEntry
81
82 result fuzzResult
83 fuzzCalled bool
84 }
85
86 var _ TB = (*F)(nil)
87
88
89
90
91 type corpusEntry = struct {
92 Parent string
93 Path string
94 Data []byte
95 Values []any
96 Generation int
97 IsSeed bool
98 }
99
100
101
102
103 func (f *F) Helper() {
104 if f.inFuzzFn {
105 panic("testing: f.Helper was called inside the fuzz target, use t.Helper instead")
106 }
107
108
109
110
111 f.mu.Lock()
112 defer f.mu.Unlock()
113 if f.helperPCs == nil {
114 f.helperPCs = make(map[uintptr]struct{})
115 }
116
117 var pc [1]uintptr
118 n := runtime.Callers(2, pc[:])
119 if n == 0 {
120 panic("testing: zero callers found")
121 }
122 if _, found := f.helperPCs[pc[0]]; !found {
123 f.helperPCs[pc[0]] = struct{}{}
124 f.helperNames = nil
125 }
126 }
127
128
129 func (f *F) Fail() {
130
131
132 if f.inFuzzFn {
133 panic("testing: f.Fail was called inside the fuzz target, use t.Fail instead")
134 }
135 f.common.Helper()
136 f.common.Fail()
137 }
138
139
140 func (f *F) Skipped() bool {
141
142
143 if f.inFuzzFn {
144 panic("testing: f.Skipped was called inside the fuzz target, use t.Skipped instead")
145 }
146 f.common.Helper()
147 return f.common.Skipped()
148 }
149
150
151
152
153 func (f *F) Add(args ...any) {
154 var values []any
155 for i := range args {
156 if t := reflect.TypeOf(args[i]); !supportedTypes[t] {
157 panic(fmt.Sprintf("testing: unsupported type to Add %v", t))
158 }
159 values = append(values, args[i])
160 }
161 f.corpus = append(f.corpus, corpusEntry{Values: values, IsSeed: true, Path: fmt.Sprintf("seed#%d", len(f.corpus))})
162 }
163
164
165 var supportedTypes = map[reflect.Type]bool{
166 reflect.TypeOf(([]byte)("")): true,
167 reflect.TypeOf((string)("")): true,
168 reflect.TypeOf((bool)(false)): true,
169 reflect.TypeOf((byte)(0)): true,
170 reflect.TypeOf((rune)(0)): true,
171 reflect.TypeOf((float32)(0)): true,
172 reflect.TypeOf((float64)(0)): true,
173 reflect.TypeOf((int)(0)): true,
174 reflect.TypeOf((int8)(0)): true,
175 reflect.TypeOf((int16)(0)): true,
176 reflect.TypeOf((int32)(0)): true,
177 reflect.TypeOf((int64)(0)): true,
178 reflect.TypeOf((uint)(0)): true,
179 reflect.TypeOf((uint8)(0)): true,
180 reflect.TypeOf((uint16)(0)): true,
181 reflect.TypeOf((uint32)(0)): true,
182 reflect.TypeOf((uint64)(0)): true,
183 }
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211 func (f *F) Fuzz(ff any) {
212 if f.fuzzCalled {
213 panic("testing: F.Fuzz called more than once")
214 }
215 f.fuzzCalled = true
216 if f.failed {
217 return
218 }
219 f.Helper()
220
221
222 fn := reflect.ValueOf(ff)
223 fnType := fn.Type()
224 if fnType.Kind() != reflect.Func {
225 panic("testing: F.Fuzz must receive a function")
226 }
227 if fnType.NumIn() < 2 || fnType.In(0) != reflect.TypeOf((*T)(nil)) {
228 panic("testing: fuzz target must receive at least two arguments, where the first argument is a *T")
229 }
230 if fnType.NumOut() != 0 {
231 panic("testing: fuzz target must not return a value")
232 }
233
234
235 var types []reflect.Type
236 for i := 1; i < fnType.NumIn(); i++ {
237 t := fnType.In(i)
238 if !supportedTypes[t] {
239 panic(fmt.Sprintf("testing: unsupported type for fuzzing %v", t))
240 }
241 types = append(types, t)
242 }
243
244
245
246
247
248 if f.fuzzContext.mode != fuzzWorker {
249 for _, c := range f.corpus {
250 if err := f.fuzzContext.deps.CheckCorpus(c.Values, types); err != nil {
251
252 f.Fatal(err)
253 }
254 }
255
256
257 c, err := f.fuzzContext.deps.ReadCorpus(filepath.Join(corpusDir, f.name), types)
258 if err != nil {
259 f.Fatal(err)
260 }
261 for i := range c {
262 c[i].IsSeed = true
263 if f.fuzzContext.mode == fuzzCoordinator {
264
265
266 c[i].Values = nil
267 }
268 }
269
270 f.corpus = append(f.corpus, c...)
271 }
272
273
274
275
276 run := func(captureOut io.Writer, e corpusEntry) (ok bool) {
277 if e.Values == nil {
278
279
280 panic(fmt.Sprintf("corpus file %q was not unmarshaled", e.Path))
281 }
282 if shouldFailFast() {
283 return true
284 }
285 testName := f.name
286 if e.Path != "" {
287 testName = fmt.Sprintf("%s/%s", testName, filepath.Base(e.Path))
288 }
289 if f.testContext.isFuzzing {
290
291
292
293
294 f.testContext.match.clearSubNames()
295 }
296
297
298
299
300 var pc [maxStackLen]uintptr
301 n := runtime.Callers(2, pc[:])
302 t := &T{
303 common: common{
304 barrier: make(chan bool),
305 signal: make(chan bool),
306 name: testName,
307 parent: &f.common,
308 level: f.level + 1,
309 creator: pc[:n],
310 chatty: f.chatty,
311 },
312 context: f.testContext,
313 }
314 if captureOut != nil {
315
316 t.parent.w = captureOut
317 }
318 t.w = indenter{&t.common}
319 if t.chatty != nil {
320
321 t.chatty.Updatef(t.name, "=== RUN %s\n", t.name)
322 }
323 f.common.inFuzzFn, f.inFuzzFn = true, true
324 go tRunner(t, func(t *T) {
325 args := []reflect.Value{reflect.ValueOf(t)}
326 for _, v := range e.Values {
327 args = append(args, reflect.ValueOf(v))
328 }
329
330
331
332
333 if f.testContext.isFuzzing {
334 defer f.fuzzContext.deps.SnapshotCoverage()
335 f.fuzzContext.deps.ResetCoverage()
336 }
337 fn.Call(args)
338 })
339 <-t.signal
340 f.common.inFuzzFn, f.inFuzzFn = false, false
341 return !t.Failed()
342 }
343
344 switch f.fuzzContext.mode {
345 case fuzzCoordinator:
346
347
348
349 corpusTargetDir := filepath.Join(corpusDir, f.name)
350 cacheTargetDir := filepath.Join(*fuzzCacheDir, f.name)
351 err := f.fuzzContext.deps.CoordinateFuzzing(
352 fuzzDuration.d,
353 int64(fuzzDuration.n),
354 minimizeDuration.d,
355 int64(minimizeDuration.n),
356 *parallel,
357 f.corpus,
358 types,
359 corpusTargetDir,
360 cacheTargetDir)
361 if err != nil {
362 f.result = fuzzResult{Error: err}
363 f.Fail()
364 fmt.Fprintf(f.w, "%v\n", err)
365 if crashErr, ok := err.(fuzzCrashError); ok {
366 crashPath := crashErr.CrashPath()
367 fmt.Fprintf(f.w, "Failing input written to %s\n", crashPath)
368 testName := filepath.Base(crashPath)
369 fmt.Fprintf(f.w, "To re-run:\ngo test -run=%s/%s\n", f.name, testName)
370 }
371 }
372
373
374
375 case fuzzWorker:
376
377
378 if err := f.fuzzContext.deps.RunFuzzWorker(func(e corpusEntry) error {
379
380
381
382
383 var buf bytes.Buffer
384 if ok := run(&buf, e); !ok {
385 return errors.New(buf.String())
386 }
387 return nil
388 }); err != nil {
389
390
391
392 f.Errorf("communicating with fuzzing coordinator: %v", err)
393 }
394
395 default:
396
397
398 for _, e := range f.corpus {
399 name := fmt.Sprintf("%s/%s", f.name, filepath.Base(e.Path))
400 if _, ok, _ := f.testContext.match.fullName(nil, name); ok {
401 run(f.w, e)
402 }
403 }
404 }
405 }
406
407 func (f *F) report() {
408 if *isFuzzWorker || f.parent == nil {
409 return
410 }
411 dstr := fmtDuration(f.duration)
412 format := "--- %s: %s (%s)\n"
413 if f.Failed() {
414 f.flushToParent(f.name, format, "FAIL", f.name, dstr)
415 } else if f.chatty != nil {
416 if f.Skipped() {
417 f.flushToParent(f.name, format, "SKIP", f.name, dstr)
418 } else {
419 f.flushToParent(f.name, format, "PASS", f.name, dstr)
420 }
421 }
422 }
423
424
425 type fuzzResult struct {
426 N int
427 T time.Duration
428 Error error
429 }
430
431 func (r fuzzResult) String() string {
432 if r.Error == nil {
433 return ""
434 }
435 return r.Error.Error()
436 }
437
438
439
440
441
442 type fuzzCrashError interface {
443 error
444 Unwrap() error
445
446
447
448
449
450 CrashPath() string
451 }
452
453
454 type fuzzContext struct {
455 deps testDeps
456 mode fuzzMode
457 }
458
459 type fuzzMode uint8
460
461 const (
462 seedCorpusOnly fuzzMode = iota
463 fuzzCoordinator
464 fuzzWorker
465 )
466
467
468
469
470 func runFuzzTests(deps testDeps, fuzzTests []InternalFuzzTarget, deadline time.Time) (ran, ok bool) {
471 ok = true
472 if len(fuzzTests) == 0 || *isFuzzWorker {
473 return ran, ok
474 }
475 m := newMatcher(deps.MatchString, *match, "-test.run")
476 tctx := newTestContext(*parallel, m)
477 tctx.deadline = deadline
478 var mFuzz *matcher
479 if *matchFuzz != "" {
480 mFuzz = newMatcher(deps.MatchString, *matchFuzz, "-test.fuzz")
481 }
482 fctx := &fuzzContext{deps: deps, mode: seedCorpusOnly}
483 root := common{w: os.Stdout}
484 if Verbose() {
485 root.chatty = newChattyPrinter(root.w)
486 }
487 for _, ft := range fuzzTests {
488 if shouldFailFast() {
489 break
490 }
491 testName, matched, _ := tctx.match.fullName(nil, ft.Name)
492 if !matched {
493 continue
494 }
495 if mFuzz != nil {
496 if _, fuzzMatched, _ := mFuzz.fullName(nil, ft.Name); fuzzMatched {
497
498
499 continue
500 }
501 }
502 f := &F{
503 common: common{
504 signal: make(chan bool),
505 barrier: make(chan bool),
506 name: testName,
507 parent: &root,
508 level: root.level + 1,
509 chatty: root.chatty,
510 },
511 testContext: tctx,
512 fuzzContext: fctx,
513 }
514 f.w = indenter{&f.common}
515 if f.chatty != nil {
516
517 f.chatty.Updatef(f.name, "=== RUN %s\n", f.name)
518 }
519
520 go fRunner(f, ft.Fn)
521 <-f.signal
522 }
523 return root.ran, !root.Failed()
524 }
525
526
527
528
529
530
531
532 func runFuzzing(deps testDeps, fuzzTests []InternalFuzzTarget) (ok bool) {
533 if len(fuzzTests) == 0 || *matchFuzz == "" {
534 return true
535 }
536 m := newMatcher(deps.MatchString, *matchFuzz, "-test.fuzz")
537 tctx := newTestContext(1, m)
538 tctx.isFuzzing = true
539 fctx := &fuzzContext{
540 deps: deps,
541 }
542 root := common{w: os.Stdout}
543 if *isFuzzWorker {
544 root.w = io.Discard
545 fctx.mode = fuzzWorker
546 } else {
547 fctx.mode = fuzzCoordinator
548 }
549 if Verbose() && !*isFuzzWorker {
550 root.chatty = newChattyPrinter(root.w)
551 }
552 var fuzzTest *InternalFuzzTarget
553 var testName string
554 var matched []string
555 for i := range fuzzTests {
556 name, ok, _ := tctx.match.fullName(nil, fuzzTests[i].Name)
557 if !ok {
558 continue
559 }
560 matched = append(matched, name)
561 fuzzTest = &fuzzTests[i]
562 testName = name
563 }
564 if len(matched) == 0 {
565 fmt.Fprintln(os.Stderr, "testing: warning: no fuzz tests to fuzz")
566 return true
567 }
568 if len(matched) > 1 {
569 fmt.Fprintf(os.Stderr, "testing: will not fuzz, -fuzz matches more than one fuzz test: %v\n", matched)
570 return false
571 }
572
573 f := &F{
574 common: common{
575 signal: make(chan bool),
576 barrier: nil,
577 name: testName,
578 parent: &root,
579 level: root.level + 1,
580 chatty: root.chatty,
581 },
582 fuzzContext: fctx,
583 testContext: tctx,
584 }
585 f.w = indenter{&f.common}
586 if f.chatty != nil {
587
588 f.chatty.Updatef(f.name, "=== FUZZ %s\n", f.name)
589 }
590 go fRunner(f, fuzzTest.Fn)
591 <-f.signal
592 return !f.failed
593 }
594
595
596
597
598
599
600
601
602
603
604
605 func fRunner(f *F, fn func(*F)) {
606
607
608
609 defer func() {
610
611
612
613
614
615
616
617 if f.Failed() {
618 atomic.AddUint32(&numFailed, 1)
619 }
620 err := recover()
621 if err == nil {
622 f.mu.RLock()
623 fuzzNotCalled := !f.fuzzCalled && !f.skipped && !f.failed
624 if !f.finished && !f.skipped && !f.failed {
625 err = errNilPanicOrGoexit
626 }
627 f.mu.RUnlock()
628 if fuzzNotCalled && err == nil {
629 f.Error("returned without calling F.Fuzz, F.Fail, or F.Skip")
630 }
631 }
632
633
634
635 didPanic := false
636 defer func() {
637 if !didPanic {
638
639
640
641 f.signal <- true
642 }
643 }()
644
645
646
647 doPanic := func(err any) {
648 f.Fail()
649 if r := f.runCleanup(recoverAndReturnPanic); r != nil {
650 f.Logf("cleanup panicked with %v", r)
651 }
652 for root := &f.common; root.parent != nil; root = root.parent {
653 root.mu.Lock()
654 root.duration += time.Since(root.start)
655 d := root.duration
656 root.mu.Unlock()
657 root.flushToParent(root.name, "--- FAIL: %s (%s)\n", root.name, fmtDuration(d))
658 }
659 didPanic = true
660 panic(err)
661 }
662 if err != nil {
663 doPanic(err)
664 }
665
666
667 f.duration += time.Since(f.start)
668
669 if len(f.sub) > 0 {
670
671
672
673
674 f.testContext.release()
675 close(f.barrier)
676
677 for _, sub := range f.sub {
678 <-sub.signal
679 }
680 cleanupStart := time.Now()
681 err := f.runCleanup(recoverAndReturnPanic)
682 f.duration += time.Since(cleanupStart)
683 if err != nil {
684 doPanic(err)
685 }
686 }
687
688
689 f.report()
690 f.done = true
691 f.setRan()
692 }()
693 defer func() {
694 if len(f.sub) == 0 {
695 f.runCleanup(normalPanic)
696 }
697 }()
698
699 f.start = time.Now()
700 fn(f)
701
702
703
704 f.mu.Lock()
705 f.finished = true
706 f.mu.Unlock()
707 }
708
View as plain text