github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/fuzzer/fuzzer_test.go (about)

     1  // Copyright 2024 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package fuzzer
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"hash/crc32"
    11  	"math/rand"
    12  	"regexp"
    13  	"runtime"
    14  	"strings"
    15  	"sync"
    16  	"sync/atomic"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/google/syzkaller/pkg/corpus"
    21  	"github.com/google/syzkaller/pkg/csource"
    22  	"github.com/google/syzkaller/pkg/flatrpc"
    23  	"github.com/google/syzkaller/pkg/fuzzer/queue"
    24  	"github.com/google/syzkaller/pkg/rpcserver"
    25  	"github.com/google/syzkaller/pkg/testutil"
    26  	"github.com/google/syzkaller/pkg/vminfo"
    27  	"github.com/google/syzkaller/prog"
    28  	"github.com/google/syzkaller/sys/targets"
    29  	"github.com/stretchr/testify/assert"
    30  )
    31  
    32  func TestFuzz(t *testing.T) {
    33  	defer checkGoroutineLeaks()
    34  
    35  	target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz)
    36  	if err != nil {
    37  		t.Fatal(err)
    38  	}
    39  	sysTarget := targets.Get(target.OS, target.Arch)
    40  	if sysTarget.BrokenCompiler != "" {
    41  		t.Skipf("skipping, broken cross-compiler: %v", sysTarget.BrokenCompiler)
    42  	}
    43  	executor := csource.BuildExecutor(t, target, "../..", "-fsanitize-coverage=trace-pc", "-g")
    44  
    45  	ctx, cancel := context.WithCancel(context.Background())
    46  	defer cancel()
    47  
    48  	corpusUpdates := make(chan corpus.NewItemEvent)
    49  	fuzzer := NewFuzzer(ctx, &Config{
    50  		Debug:  true,
    51  		Corpus: corpus.NewMonitoredCorpus(ctx, corpusUpdates),
    52  		Logf: func(level int, msg string, args ...interface{}) {
    53  			if level > 1 {
    54  				return
    55  			}
    56  			t.Logf(msg, args...)
    57  		},
    58  		Coverage: true,
    59  		EnabledCalls: map[*prog.Syscall]bool{
    60  			target.SyscallMap["syz_test_fuzzer1"]: true,
    61  		},
    62  	}, rand.New(testutil.RandSource(t)), target)
    63  
    64  	go func() {
    65  		for {
    66  			select {
    67  			case <-ctx.Done():
    68  				return
    69  			case u := <-corpusUpdates:
    70  				t.Logf("new prog:\n%s", u.ProgData)
    71  			}
    72  		}
    73  	}()
    74  
    75  	tf := &testFuzzer{
    76  		t:         t,
    77  		target:    target,
    78  		fuzzer:    fuzzer,
    79  		executor:  executor,
    80  		iterLimit: 10000,
    81  		expectedCrashes: map[string]bool{
    82  			"first bug":  true,
    83  			"second bug": true,
    84  		},
    85  	}
    86  	tf.run()
    87  
    88  	t.Logf("resulting corpus:")
    89  	for _, p := range fuzzer.Config.Corpus.Programs() {
    90  		t.Logf("-----")
    91  		t.Logf("%s", p.Serialize())
    92  	}
    93  }
    94  
    95  func BenchmarkFuzzer(b *testing.B) {
    96  	b.ReportAllocs()
    97  	target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz)
    98  	if err != nil {
    99  		b.Fatal(err)
   100  	}
   101  	ctx, cancel := context.WithCancel(context.Background())
   102  	defer cancel()
   103  	calls := map[*prog.Syscall]bool{}
   104  	for _, c := range target.Syscalls {
   105  		calls[c] = true
   106  	}
   107  	fuzzer := NewFuzzer(ctx, &Config{
   108  		Corpus:       corpus.NewCorpus(ctx),
   109  		Coverage:     true,
   110  		EnabledCalls: calls,
   111  	}, rand.New(rand.NewSource(time.Now().UnixNano())), target)
   112  
   113  	b.ResetTimer()
   114  	b.RunParallel(func(pb *testing.PB) {
   115  		for pb.Next() {
   116  			req := fuzzer.Next()
   117  			res, _, _ := emulateExec(req)
   118  			req.Done(res)
   119  		}
   120  	})
   121  }
   122  
   123  // Based on the example from Go documentation.
   124  var crc32q = crc32.MakeTable(0xD5828281)
   125  
   126  func emulateExec(req *queue.Request) (*queue.Result, string, error) {
   127  	serializedLines := bytes.Split(req.Prog.Serialize(), []byte("\n"))
   128  	var info flatrpc.ProgInfo
   129  	for i, call := range req.Prog.Calls {
   130  		cover := []uint64{uint64(call.Meta.ID*1024) +
   131  			uint64(crc32.Checksum(serializedLines[i], crc32q)%4)}
   132  		callInfo := &flatrpc.CallInfo{}
   133  		if req.ExecOpts.ExecFlags&flatrpc.ExecFlagCollectCover > 0 {
   134  			callInfo.Cover = cover
   135  		}
   136  		if req.ExecOpts.ExecFlags&flatrpc.ExecFlagCollectSignal > 0 {
   137  			callInfo.Signal = cover
   138  		}
   139  		info.Calls = append(info.Calls, callInfo)
   140  	}
   141  	return &queue.Result{Info: &info}, "", nil
   142  }
   143  
   144  type testFuzzer struct {
   145  	t               testing.TB
   146  	target          *prog.Target
   147  	fuzzer          *Fuzzer
   148  	executor        string
   149  	mu              sync.Mutex
   150  	crashes         map[string]int
   151  	expectedCrashes map[string]bool
   152  	iter            int
   153  	iterLimit       int
   154  	done            func()
   155  	finished        atomic.Bool
   156  }
   157  
   158  func (f *testFuzzer) run() {
   159  	f.crashes = make(map[string]int)
   160  	ctx, done := context.WithCancel(context.Background())
   161  	f.done = done
   162  	var output bytes.Buffer
   163  	cfg := &rpcserver.LocalConfig{
   164  		Config: rpcserver.Config{
   165  			Config: vminfo.Config{
   166  				Debug:    true,
   167  				Cover:    true,
   168  				Target:   f.target,
   169  				Features: flatrpc.FeatureSandboxNone | flatrpc.FeatureCoverage,
   170  				Sandbox:  flatrpc.ExecEnvSandboxNone,
   171  			},
   172  			Procs:    4,
   173  			Slowdown: 1,
   174  		},
   175  		Executor:     f.executor,
   176  		Dir:          f.t.TempDir(),
   177  		OutputWriter: &output,
   178  	}
   179  	cfg.MachineChecked = func(features flatrpc.Feature, syscalls map[*prog.Syscall]bool) queue.Source {
   180  		return f
   181  	}
   182  	if err := rpcserver.RunLocal(ctx, cfg); err != nil {
   183  		f.t.Logf("executor output:\n%s", output.String())
   184  		f.t.Fatal(err)
   185  	}
   186  	assert.Equal(f.t, len(f.expectedCrashes), len(f.crashes), "not all expected crashes were found")
   187  	assert.NotEmpty(f.t, f.fuzzer.Config.Corpus.StatProgs.Val(), "must have non-empty corpus")
   188  	assert.NotEmpty(f.t, f.fuzzer.Config.Corpus.StatSignal.Val(), "must have non-empty signal")
   189  }
   190  
   191  func (f *testFuzzer) Next() *queue.Request {
   192  	if f.finished.Load() {
   193  		return nil
   194  	}
   195  	req := f.fuzzer.Next()
   196  	req.ExecOpts.EnvFlags |= flatrpc.ExecEnvSignal | flatrpc.ExecEnvSandboxNone
   197  	req.ReturnOutput = true
   198  	req.ReturnError = true
   199  	req.OnDone(f.OnDone)
   200  	return req
   201  }
   202  
   203  func (f *testFuzzer) OnDone(req *queue.Request, res *queue.Result) bool {
   204  	// TODO: support hints emulation.
   205  	match := crashRe.FindSubmatch(res.Output)
   206  	f.mu.Lock()
   207  	defer f.mu.Unlock()
   208  	if f.finished.Load() {
   209  		// Don't touch f.crashes in this case b/c it can cause races with the main goroutine,
   210  		// and logging can cause "Log in goroutine after TestFuzz has completed" panic.
   211  		return true
   212  	}
   213  	if match != nil {
   214  		crash := string(match[1])
   215  		f.t.Logf("CRASH: %s", crash)
   216  		res.Status = queue.Crashed
   217  		if !f.expectedCrashes[crash] {
   218  			f.t.Errorf("unexpected crash: %q", crash)
   219  		}
   220  		f.crashes[crash]++
   221  	}
   222  	f.iter++
   223  	corpusProgs := f.fuzzer.Config.Corpus.StatProgs.Val()
   224  	signal := f.fuzzer.Config.Corpus.StatSignal.Val()
   225  	if f.iter%100 == 0 {
   226  		f.t.Logf("<iter %d>: corpus %d, signal %d, max signal %d, crash types %d, running jobs %d",
   227  			f.iter, corpusProgs, signal, len(f.fuzzer.Cover.maxSignal),
   228  			len(f.crashes), f.fuzzer.statJobs.Val())
   229  	}
   230  	criteriaMet := len(f.crashes) == len(f.expectedCrashes) &&
   231  		corpusProgs > 0 && signal > 0
   232  	if f.iter > f.iterLimit || criteriaMet {
   233  		f.done()
   234  		f.finished.Store(true)
   235  	}
   236  	return true
   237  }
   238  
   239  var crashRe = regexp.MustCompile(`{{CRASH: (.*?)}}`)
   240  
   241  func checkGoroutineLeaks() {
   242  	// Inspired by src/net/http/main_test.go.
   243  	buf := make([]byte, 2<<20)
   244  	err := ""
   245  	for i := 0; i < 3; i++ {
   246  		buf = buf[:runtime.Stack(buf, true)]
   247  		err = ""
   248  		for _, g := range strings.Split(string(buf), "\n\n") {
   249  			if !strings.Contains(g, "pkg/fuzzer/fuzzer.go") {
   250  				continue
   251  			}
   252  			err = fmt.Sprintf("%sLeaked goroutine:\n%s", err, g)
   253  		}
   254  		if err == "" {
   255  			return
   256  		}
   257  		// Give ctx.Done() a chance to propagate to all goroutines.
   258  		time.Sleep(100 * time.Millisecond)
   259  	}
   260  	if err != "" {
   261  		panic(err)
   262  	}
   263  }