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 }