github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/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 "testing" 17 "time" 18 19 "github.com/google/syzkaller/pkg/corpus" 20 "github.com/google/syzkaller/pkg/csource" 21 "github.com/google/syzkaller/pkg/flatrpc" 22 "github.com/google/syzkaller/pkg/fuzzer/queue" 23 "github.com/google/syzkaller/pkg/ipc" 24 "github.com/google/syzkaller/pkg/ipc/ipcconfig" 25 "github.com/google/syzkaller/pkg/signal" 26 "github.com/google/syzkaller/pkg/testutil" 27 "github.com/google/syzkaller/prog" 28 "github.com/google/syzkaller/sys/targets" 29 "github.com/stretchr/testify/assert" 30 "golang.org/x/sync/errgroup" 31 ) 32 33 func TestFuzz(t *testing.T) { 34 defer checkGoroutineLeaks() 35 36 target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz) 37 if err != nil { 38 t.Fatal(err) 39 } 40 sysTarget := targets.Get(target.OS, target.Arch) 41 if sysTarget.BrokenCompiler != "" { 42 t.Skipf("skipping, broken cross-compiler: %v", sysTarget.BrokenCompiler) 43 } 44 executor := csource.BuildExecutor(t, target, "../..", "-fsanitize-coverage=trace-pc", "-g") 45 ctx, cancel := context.WithCancel(context.Background()) 46 defer cancel() 47 48 _, opts, _ := ipcconfig.Default(target) 49 corpusUpdates := make(chan corpus.NewItemEvent) 50 fuzzer := NewFuzzer(ctx, &Config{ 51 Debug: true, 52 BaseOpts: *opts, 53 Corpus: corpus.NewMonitoredCorpus(ctx, corpusUpdates), 54 Logf: func(level int, msg string, args ...interface{}) { 55 if level > 1 { 56 return 57 } 58 t.Logf(msg, args...) 59 }, 60 Coverage: true, 61 EnabledCalls: map[*prog.Syscall]bool{ 62 target.SyscallMap["syz_test_fuzzer1"]: true, 63 }, 64 }, rand.New(testutil.RandSource(t)), target) 65 66 go func() { 67 for { 68 select { 69 case <-ctx.Done(): 70 return 71 case u := <-corpusUpdates: 72 t.Logf("new prog:\n%s", u.ProgData) 73 } 74 } 75 }() 76 77 tf := newTestFuzzer(t, fuzzer, map[string]bool{ 78 "first bug": true, 79 "second bug": true, 80 }, 10000) 81 82 for i := 0; i < 2; i++ { 83 tf.registerExecutor(newProc(t, target, executor)) 84 } 85 tf.wait() 86 87 t.Logf("resulting corpus:") 88 for _, p := range fuzzer.Config.Corpus.Programs() { 89 t.Logf("-----") 90 t.Logf("%s", p.Serialize()) 91 } 92 93 assert.Equal(t, len(tf.expectedCrashes), len(tf.crashes), 94 "not all expected crashes were found") 95 } 96 97 func BenchmarkFuzzer(b *testing.B) { 98 b.ReportAllocs() 99 target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz) 100 if err != nil { 101 b.Fatal(err) 102 } 103 ctx, cancel := context.WithCancel(context.Background()) 104 defer cancel() 105 calls := map[*prog.Syscall]bool{} 106 for _, c := range target.Syscalls { 107 calls[c] = true 108 } 109 fuzzer := NewFuzzer(ctx, &Config{ 110 Corpus: corpus.NewCorpus(ctx), 111 Coverage: true, 112 EnabledCalls: calls, 113 }, rand.New(rand.NewSource(time.Now().UnixNano())), target) 114 115 b.ResetTimer() 116 b.RunParallel(func(pb *testing.PB) { 117 for pb.Next() { 118 req := fuzzer.Next() 119 res, _, _ := emulateExec(req) 120 req.Done(res) 121 } 122 }) 123 } 124 125 const anyTestProg = `syz_compare(&AUTO="00000000", 0x4, &AUTO=@conditional={0x0, @void, @void}, AUTO)` 126 127 func TestRotate(t *testing.T) { 128 target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz) 129 if err != nil { 130 t.Fatal(err) 131 } 132 133 ctx, cancel := context.WithCancel(context.Background()) 134 defer cancel() 135 136 corpusObj := corpus.NewCorpus(ctx) 137 fuzzer := NewFuzzer(ctx, &Config{ 138 Debug: true, 139 Corpus: corpusObj, 140 Coverage: true, 141 EnabledCalls: map[*prog.Syscall]bool{ 142 target.SyscallMap["syz_compare"]: true, 143 }, 144 }, rand.New(testutil.RandSource(t)), target) 145 146 fakeSignal := func(size int) signal.Signal { 147 var pc []uint32 148 for i := 0; i < size; i++ { 149 pc = append(pc, uint32(i)) 150 } 151 return signal.FromRaw(pc, 0) 152 } 153 154 prog, err := target.Deserialize([]byte(anyTestProg), prog.NonStrict) 155 assert.NoError(t, err) 156 corpusObj.Save(corpus.NewInput{ 157 Prog: prog, 158 Call: 0, 159 Signal: fakeSignal(100), 160 }) 161 fuzzer.Cover.AddMaxSignal(fakeSignal(1000)) 162 163 assert.Equal(t, 1000, len(fuzzer.Cover.maxSignal)) 164 assert.Equal(t, 100, corpusObj.StatSignal.Val()) 165 166 // Rotate some of the signal. 167 fuzzer.RotateMaxSignal(200) 168 assert.Equal(t, 800, len(fuzzer.Cover.maxSignal)) 169 assert.Equal(t, 100, corpusObj.StatSignal.Val()) 170 171 plus, minus := fuzzer.Cover.GrabSignalDelta() 172 assert.Equal(t, 0, plus.Len()) 173 assert.Equal(t, 200, minus.Len()) 174 175 // Rotate the rest. 176 fuzzer.RotateMaxSignal(1000) 177 assert.Equal(t, 100, len(fuzzer.Cover.maxSignal)) 178 assert.Equal(t, 100, corpusObj.StatSignal.Val()) 179 plus, minus = fuzzer.Cover.GrabSignalDelta() 180 assert.Equal(t, 0, plus.Len()) 181 assert.Equal(t, 700, minus.Len()) 182 } 183 184 // Based on the example from Go documentation. 185 var crc32q = crc32.MakeTable(0xD5828281) 186 187 func emulateExec(req *queue.Request) (*queue.Result, string, error) { 188 serializedLines := bytes.Split(req.Prog.Serialize(), []byte("\n")) 189 var info ipc.ProgInfo 190 for i, call := range req.Prog.Calls { 191 cover := uint32(call.Meta.ID*1024) + 192 crc32.Checksum(serializedLines[i], crc32q)%4 193 callInfo := ipc.CallInfo{} 194 if req.ExecOpts.ExecFlags&flatrpc.ExecFlagCollectCover > 0 { 195 callInfo.Cover = []uint32{cover} 196 } 197 if req.ExecOpts.ExecFlags&flatrpc.ExecFlagCollectSignal > 0 { 198 callInfo.Signal = []uint32{cover} 199 } 200 info.Calls = append(info.Calls, callInfo) 201 } 202 return &queue.Result{Info: &info}, "", nil 203 } 204 205 type testFuzzer struct { 206 t testing.TB 207 eg errgroup.Group 208 fuzzer *Fuzzer 209 mu sync.Mutex 210 crashes map[string]int 211 expectedCrashes map[string]bool 212 iter int 213 iterLimit int 214 } 215 216 func newTestFuzzer(t testing.TB, fuzzer *Fuzzer, expectedCrashes map[string]bool, iterLimit int) *testFuzzer { 217 return &testFuzzer{ 218 t: t, 219 fuzzer: fuzzer, 220 expectedCrashes: expectedCrashes, 221 crashes: map[string]int{}, 222 iterLimit: iterLimit, 223 } 224 } 225 226 func (f *testFuzzer) oneMore() bool { 227 f.mu.Lock() 228 defer f.mu.Unlock() 229 f.iter++ 230 if f.iter%100 == 0 { 231 f.t.Logf("<iter %d>: corpus %d, signal %d, max signal %d, crash types %d, running jobs %d", 232 f.iter, f.fuzzer.Config.Corpus.StatProgs.Val(), f.fuzzer.Config.Corpus.StatSignal.Val(), 233 len(f.fuzzer.Cover.maxSignal), len(f.crashes), f.fuzzer.statJobs.Val()) 234 } 235 return f.iter < f.iterLimit && 236 (f.expectedCrashes == nil || len(f.crashes) != len(f.expectedCrashes)) 237 } 238 239 func (f *testFuzzer) registerExecutor(proc *executorProc) { 240 f.eg.Go(func() error { 241 for f.oneMore() { 242 req := f.fuzzer.Next() 243 res, crash, err := proc.execute(req) 244 if err != nil { 245 return err 246 } 247 if crash != "" { 248 res = &queue.Result{Status: queue.Crashed} 249 if !f.expectedCrashes[crash] { 250 return fmt.Errorf("unexpected crash: %q", crash) 251 } 252 f.mu.Lock() 253 f.t.Logf("CRASH: %s", crash) 254 f.crashes[crash]++ 255 f.mu.Unlock() 256 } 257 req.Done(res) 258 } 259 return nil 260 }) 261 } 262 263 func (f *testFuzzer) wait() { 264 t := f.t 265 err := f.eg.Wait() 266 if err != nil { 267 t.Fatal(err) 268 } 269 t.Logf("crashes:") 270 for title, cnt := range f.crashes { 271 t.Logf("%s: %d", title, cnt) 272 } 273 } 274 275 // TODO: it's already implemented in syz-fuzzer/proc.go, 276 // pkg/runtest and tools/syz-execprog. 277 // Looks like it's time to factor out this functionality. 278 type executorProc struct { 279 env *ipc.Env 280 execOpts ipc.ExecOpts 281 } 282 283 func newProc(t *testing.T, target *prog.Target, executor string) *executorProc { 284 config, execOpts, err := ipcconfig.Default(target) 285 if err != nil { 286 t.Fatal(err) 287 } 288 config.Executor = executor 289 execOpts.EnvFlags |= flatrpc.ExecEnvSignal 290 env, err := ipc.MakeEnv(config, 0) 291 if err != nil { 292 t.Fatal(err) 293 } 294 t.Cleanup(func() { env.Close() }) 295 return &executorProc{ 296 env: env, 297 execOpts: *execOpts, 298 } 299 } 300 301 var crashRe = regexp.MustCompile(`{{CRASH: (.*?)}}`) 302 303 func (proc *executorProc) execute(req *queue.Request) (*queue.Result, string, error) { 304 // TODO: support hints emulation. 305 output, info, _, err := proc.env.Exec(&req.ExecOpts, req.Prog) 306 ret := crashRe.FindStringSubmatch(string(output)) 307 if ret != nil { 308 return nil, ret[1], nil 309 } else if err != nil { 310 return nil, "", err 311 } 312 return &queue.Result{Info: info}, "", nil 313 } 314 315 func checkGoroutineLeaks() { 316 // Inspired by src/net/http/main_test.go. 317 buf := make([]byte, 2<<20) 318 err := "" 319 for i := 0; i < 3; i++ { 320 buf = buf[:runtime.Stack(buf, true)] 321 err = "" 322 for _, g := range strings.Split(string(buf), "\n\n") { 323 if !strings.Contains(g, "pkg/fuzzer/fuzzer.go") { 324 continue 325 } 326 err = fmt.Sprintf("%sLeaked goroutine:\n%s", err, g) 327 } 328 if err == "" { 329 return 330 } 331 // Give ctx.Done() a chance to propagate to all goroutines. 332 time.Sleep(100 * time.Millisecond) 333 } 334 if err != "" { 335 panic(err) 336 } 337 }