// Copyright 2024 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. package fuzzer import ( "bytes" "context" "fmt" "hash/crc32" "math/rand" "regexp" "runtime" "strings" "sync" "sync/atomic" "testing" "time" "github.com/google/syzkaller/pkg/corpus" "github.com/google/syzkaller/pkg/csource" "github.com/google/syzkaller/pkg/flatrpc" "github.com/google/syzkaller/pkg/fuzzer/queue" "github.com/google/syzkaller/pkg/rpcserver" "github.com/google/syzkaller/pkg/testutil" "github.com/google/syzkaller/pkg/vminfo" "github.com/google/syzkaller/prog" "github.com/google/syzkaller/sys/targets" "github.com/stretchr/testify/assert" ) func TestFuzz(t *testing.T) { defer checkGoroutineLeaks() target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz) if err != nil { t.Fatal(err) } sysTarget := targets.Get(target.OS, target.Arch) if sysTarget.BrokenCompiler != "" { t.Skipf("skipping, broken cross-compiler: %v", sysTarget.BrokenCompiler) } executor := csource.BuildExecutor(t, target, "../..", "-fsanitize-coverage=trace-pc", "-g") ctx, cancel := context.WithCancel(context.Background()) defer cancel() corpusUpdates := make(chan corpus.NewItemEvent) fuzzer := NewFuzzer(ctx, &Config{ Debug: true, Corpus: corpus.NewMonitoredCorpus(ctx, corpusUpdates), Logf: func(level int, msg string, args ...interface{}) { if level > 1 { return } t.Logf(msg, args...) }, Coverage: true, EnabledCalls: map[*prog.Syscall]bool{ target.SyscallMap["syz_test_fuzzer1"]: true, }, }, rand.New(testutil.RandSource(t)), target) go func() { for { select { case <-ctx.Done(): return case u := <-corpusUpdates: t.Logf("new prog:\n%s", u.ProgData) } } }() tf := &testFuzzer{ t: t, target: target, fuzzer: fuzzer, executor: executor, iterLimit: 10000, expectedCrashes: map[string]bool{ "first bug": true, "second bug": true, }, } tf.run() t.Logf("resulting corpus:") for _, p := range fuzzer.Config.Corpus.Programs() { t.Logf("-----") t.Logf("%s", p.Serialize()) } } func BenchmarkFuzzer(b *testing.B) { b.ReportAllocs() target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz) if err != nil { b.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() calls := map[*prog.Syscall]bool{} for _, c := range target.Syscalls { calls[c] = true } fuzzer := NewFuzzer(ctx, &Config{ Corpus: corpus.NewCorpus(ctx), Coverage: true, EnabledCalls: calls, }, rand.New(rand.NewSource(time.Now().UnixNano())), target) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { req := fuzzer.Next() res, _, _ := emulateExec(req) req.Done(res) } }) } // Based on the example from Go documentation. var crc32q = crc32.MakeTable(0xD5828281) func emulateExec(req *queue.Request) (*queue.Result, string, error) { serializedLines := bytes.Split(req.Prog.Serialize(), []byte("\n")) var info flatrpc.ProgInfo for i, call := range req.Prog.Calls { cover := []uint64{uint64(call.Meta.ID*1024) + uint64(crc32.Checksum(serializedLines[i], crc32q)%4)} callInfo := &flatrpc.CallInfo{} if req.ExecOpts.ExecFlags&flatrpc.ExecFlagCollectCover > 0 { callInfo.Cover = cover } if req.ExecOpts.ExecFlags&flatrpc.ExecFlagCollectSignal > 0 { callInfo.Signal = cover } info.Calls = append(info.Calls, callInfo) } return &queue.Result{Info: &info}, "", nil } type testFuzzer struct { t testing.TB target *prog.Target fuzzer *Fuzzer executor string mu sync.Mutex crashes map[string]int expectedCrashes map[string]bool iter int iterLimit int done func() finished atomic.Bool } func (f *testFuzzer) run() { f.crashes = make(map[string]int) ctx, done := context.WithCancel(context.Background()) f.done = done var output bytes.Buffer cfg := &rpcserver.LocalConfig{ Config: rpcserver.Config{ Config: vminfo.Config{ Debug: true, Cover: true, Target: f.target, Features: flatrpc.FeatureSandboxNone | flatrpc.FeatureCoverage, Sandbox: flatrpc.ExecEnvSandboxNone, }, Procs: 4, Slowdown: 1, }, Executor: f.executor, Dir: f.t.TempDir(), OutputWriter: &output, } cfg.MachineChecked = func(features flatrpc.Feature, syscalls map[*prog.Syscall]bool) queue.Source { return f } if err := rpcserver.RunLocal(ctx, cfg); err != nil { f.t.Logf("executor output:\n%s", output.String()) f.t.Fatal(err) } assert.Equal(f.t, len(f.expectedCrashes), len(f.crashes), "not all expected crashes were found") assert.NotEmpty(f.t, f.fuzzer.Config.Corpus.StatProgs.Val(), "must have non-empty corpus") assert.NotEmpty(f.t, f.fuzzer.Config.Corpus.StatSignal.Val(), "must have non-empty signal") } func (f *testFuzzer) Next() *queue.Request { if f.finished.Load() { return nil } req := f.fuzzer.Next() req.ExecOpts.EnvFlags |= flatrpc.ExecEnvSignal | flatrpc.ExecEnvSandboxNone req.ReturnOutput = true req.ReturnError = true req.OnDone(f.OnDone) return req } func (f *testFuzzer) OnDone(req *queue.Request, res *queue.Result) bool { // TODO: support hints emulation. match := crashRe.FindSubmatch(res.Output) f.mu.Lock() defer f.mu.Unlock() if f.finished.Load() { // Don't touch f.crashes in this case b/c it can cause races with the main goroutine, // and logging can cause "Log in goroutine after TestFuzz has completed" panic. return true } if match != nil { crash := string(match[1]) f.t.Logf("CRASH: %s", crash) res.Status = queue.Crashed if !f.expectedCrashes[crash] { f.t.Errorf("unexpected crash: %q", crash) } f.crashes[crash]++ } f.iter++ corpusProgs := f.fuzzer.Config.Corpus.StatProgs.Val() signal := f.fuzzer.Config.Corpus.StatSignal.Val() if f.iter%100 == 0 { f.t.Logf(": corpus %d, signal %d, max signal %d, crash types %d, running jobs %d", f.iter, corpusProgs, signal, len(f.fuzzer.Cover.maxSignal), len(f.crashes), f.fuzzer.statJobs.Val()) } criteriaMet := len(f.crashes) == len(f.expectedCrashes) && corpusProgs > 0 && signal > 0 if f.iter > f.iterLimit || criteriaMet { f.done() f.finished.Store(true) } return true } var crashRe = regexp.MustCompile(`{{CRASH: (.*?)}}`) func checkGoroutineLeaks() { // Inspired by src/net/http/main_test.go. buf := make([]byte, 2<<20) err := "" for i := 0; i < 3; i++ { buf = buf[:runtime.Stack(buf, true)] err = "" for _, g := range strings.Split(string(buf), "\n\n") { if !strings.Contains(g, "pkg/fuzzer/fuzzer.go") { continue } err = fmt.Sprintf("%sLeaked goroutine:\n%s", err, g) } if err == "" { return } // Give ctx.Done() a chance to propagate to all goroutines. time.Sleep(100 * time.Millisecond) } if err != "" { panic(err) } }