diff options
| -rw-r--r-- | executor/common_test.h | 20 | ||||
| -rw-r--r-- | pkg/csource/generated.go | 19 | ||||
| -rw-r--r-- | pkg/fuzzer/corpus.go | 114 | ||||
| -rw-r--r-- | pkg/fuzzer/corpus_test.go (renamed from syz-fuzzer/fuzzer_test.go) | 22 | ||||
| -rw-r--r-- | pkg/fuzzer/fuzzer.go | 350 | ||||
| -rw-r--r-- | pkg/fuzzer/fuzzer_test.go | 290 | ||||
| -rw-r--r-- | pkg/fuzzer/job.go | 401 | ||||
| -rw-r--r-- | pkg/fuzzer/prio_queue.go | 100 | ||||
| -rw-r--r-- | pkg/fuzzer/prio_queue_test.go | 44 | ||||
| -rw-r--r-- | pkg/fuzzer/stats.go | 26 | ||||
| -rw-r--r-- | sys/targets/targets.go | 13 | ||||
| -rw-r--r-- | sys/test/expressions.txt.const | 2 | ||||
| -rw-r--r-- | sys/test/fuzzer.txt | 7 | ||||
| -rw-r--r-- | syz-fuzzer/fuzzer.go | 448 | ||||
| -rw-r--r-- | syz-fuzzer/proc.go | 345 | ||||
| -rw-r--r-- | syz-fuzzer/workqueue.go | 131 |
16 files changed, 1586 insertions, 746 deletions
diff --git a/executor/common_test.h b/executor/common_test.h index 6ef6ed82d..971108df8 100644 --- a/executor/common_test.h +++ b/executor/common_test.h @@ -133,3 +133,23 @@ static int do_sandbox_none(void) return 0; } #endif + +#if SYZ_EXECUTOR || __NR_syz_test_fuzzer1 + +static void fake_crash(const char* name) +{ + failmsg("crash", "{{CRASH: %s}}", name); + doexit(1); +} + +static long syz_test_fuzzer1(volatile long a, volatile long b, volatile long c) +{ + // We probably want something more interesting here. + if (a == 1 && b == 1 && c == 1) + fake_crash("first bug"); + if (a == 1 && b == 2 && c == 3) + fake_crash("second bug"); + return 0; +} + +#endif diff --git a/pkg/csource/generated.go b/pkg/csource/generated.go index 7c01243ce..2c33e7a29 100644 --- a/pkg/csource/generated.go +++ b/pkg/csource/generated.go @@ -12330,6 +12330,25 @@ static int do_sandbox_none(void) } #endif +#if SYZ_EXECUTOR || __NR_syz_test_fuzzer1 + +static void fake_crash(const char* name) +{ + failmsg("crash", "{{CRASH: %s}}", name); + doexit(1); +} + +static long syz_test_fuzzer1(volatile long a, volatile long b, volatile long c) +{ + if (a == 1 && b == 1 && c == 1) + fake_crash("first bug"); + if (a == 1 && b == 2 && c == 3) + fake_crash("second bug"); + return 0; +} + +#endif + #elif GOOS_windows #include <direct.h> diff --git a/pkg/fuzzer/corpus.go b/pkg/fuzzer/corpus.go new file mode 100644 index 000000000..b92ab1c64 --- /dev/null +++ b/pkg/fuzzer/corpus.go @@ -0,0 +1,114 @@ +// 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 ( + "math/rand" + "sort" + "sync" + + "github.com/google/syzkaller/pkg/hash" + "github.com/google/syzkaller/pkg/signal" + "github.com/google/syzkaller/prog" +) + +type Corpus struct { + mu sync.RWMutex + progs []*prog.Prog + hashes map[hash.Sig]struct{} + sumPrios int64 + accPrios []int64 + signal signal.Signal // signal of inputs in corpus + maxSignal signal.Signal // max signal ever observed (including flakes) + newSignal signal.Signal +} + +// CorpusStat is a snapshot of the relevant current state figures. +type CorpusStat struct { + Progs int + Signal int + MaxSignal int +} + +func newCorpus() *Corpus { + return &Corpus{ + hashes: make(map[hash.Sig]struct{}), + } +} + +// TODO: maybe we want to treat progs from other fuzzers exactly like +// candidates? And even triage them? +func (corpus *Corpus) Save(p *prog.Prog, signal signal.Signal, sig hash.Sig) { + corpus.mu.Lock() + defer corpus.mu.Unlock() + if _, ok := corpus.hashes[sig]; !ok { + corpus.progs = append(corpus.progs, p) + corpus.hashes[sig] = struct{}{} + prio := int64(len(signal)) + if prio == 0 { + prio = 1 + } + corpus.sumPrios += prio + corpus.accPrios = append(corpus.accPrios, corpus.sumPrios) + } + corpus.signal.Merge(signal) + corpus.maxSignal.Merge(signal) +} + +// Signal that should no longer be chased after. +func (corpus *Corpus) AddMaxSignal(sign signal.Signal) { + // TODO: how do we ensure occasional drop of this max cover? + corpus.mu.Lock() + defer corpus.mu.Unlock() + corpus.maxSignal.Merge(sign) +} + +func (corpus *Corpus) AddRawMaxSignal(signal []uint32, prio uint8) signal.Signal { + corpus.mu.Lock() + defer corpus.mu.Unlock() + diff := corpus.maxSignal.DiffRaw(signal, prio) + if diff.Empty() { + return diff + } + corpus.maxSignal.Merge(diff) + corpus.newSignal.Merge(diff) + return diff +} + +func (corpus *Corpus) chooseProgram(r *rand.Rand) *prog.Prog { + corpus.mu.RLock() + defer corpus.mu.RUnlock() + if len(corpus.progs) == 0 { + return nil + } + randVal := r.Int63n(corpus.sumPrios + 1) + idx := sort.Search(len(corpus.accPrios), func(i int) bool { + return corpus.accPrios[i] >= randVal + }) + return corpus.progs[idx] +} + +func (corpus *Corpus) Programs() []*prog.Prog { + corpus.mu.RLock() + defer corpus.mu.RUnlock() + return corpus.progs +} + +func (corpus *Corpus) GrabNewSignal() signal.Signal { + corpus.mu.Lock() + defer corpus.mu.Unlock() + sign := corpus.newSignal + corpus.newSignal = nil + return sign +} + +func (corpus *Corpus) Stat() CorpusStat { + corpus.mu.RLock() + defer corpus.mu.RUnlock() + return CorpusStat{ + Progs: len(corpus.progs), + Signal: len(corpus.signal), + MaxSignal: len(corpus.maxSignal), + } +} diff --git a/syz-fuzzer/fuzzer_test.go b/pkg/fuzzer/corpus_test.go index dae8329f7..0b62d8c5a 100644 --- a/syz-fuzzer/fuzzer_test.go +++ b/pkg/fuzzer/corpus_test.go @@ -1,7 +1,7 @@ -// Copyright 2019 syzkaller project authors. All rights reserved. +// 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 main +package fuzzer import ( "math" @@ -24,7 +24,7 @@ func TestChooseProgram(t *testing.T) { rs := rand.NewSource(0) r := rand.New(rs) target := getTarget(t, targets.TestOS, targets.TestArch64) - fuzzer := &Fuzzer{corpusHashes: make(map[hash.Sig]struct{})} + corpus := newCorpus() const ( maxIters = 1000 @@ -39,16 +39,15 @@ func TestChooseProgram(t *testing.T) { sizeSig = 0 } inp := generateInput(target, rs, 10, sizeSig) - fuzzer.addInputToCorpus(inp.p, inp.sign, inp.sig) + corpus.Save(inp.p, inp.sign, inp.sig) priorities[inp.p] = int64(len(inp.sign)) } - snapshot := fuzzer.snapshot() counters := make(map[*prog.Prog]int) for it := 0; it < maxIters; it++ { - counters[snapshot.chooseProgram(r)]++ + counters[corpus.chooseProgram(r)]++ } for p, prio := range priorities { - prob := float64(prio) / float64(fuzzer.sumPrios) + prob := float64(prio) / float64(corpus.sumPrios) diff := math.Abs(prob*maxIters - float64(counters[p])) if diff > eps*maxIters { t.Fatalf("the difference (%f) is higher than %f%%", diff, eps*100) @@ -56,9 +55,9 @@ func TestChooseProgram(t *testing.T) { } } -func TestAddInputConcurrency(t *testing.T) { +func TestCorpusSaveConcurrency(t *testing.T) { target := getTarget(t, targets.TestOS, targets.TestArch64) - fuzzer := &Fuzzer{corpusHashes: make(map[hash.Sig]struct{})} + corpus := newCorpus() const ( routines = 10 @@ -71,9 +70,8 @@ func TestAddInputConcurrency(t *testing.T) { r := rand.New(rs) for it := 0; it < iters; it++ { inp := generateInput(target, rs, 10, it) - fuzzer.addInputToCorpus(inp.p, inp.sign, inp.sig) - snapshot := fuzzer.snapshot() - snapshot.chooseProgram(r).Clone() + corpus.Save(inp.p, inp.sign, inp.sig) + corpus.chooseProgram(r).Clone() } }() } diff --git a/pkg/fuzzer/fuzzer.go b/pkg/fuzzer/fuzzer.go new file mode 100644 index 000000000..64be8325c --- /dev/null +++ b/pkg/fuzzer/fuzzer.go @@ -0,0 +1,350 @@ +// 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 ( + "context" + "fmt" + "math/rand" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/google/syzkaller/pkg/hash" + "github.com/google/syzkaller/pkg/ipc" + "github.com/google/syzkaller/pkg/rpctype" + "github.com/google/syzkaller/prog" +) + +type Fuzzer struct { + Config *Config + Corpus *Corpus + NeedCandidates chan struct{} + + ctx context.Context + mu sync.Mutex + stats map[string]uint64 + rnd *rand.Rand + target *prog.Target + + ct *prog.ChoiceTable + ctProgs int + ctMu sync.Mutex // TODO: use RWLock. + ctRegenerate chan struct{} + + nextExec *priorityQueue[*Request] + runningExecs map[*Request]time.Time + nextJobID atomic.Int64 + + queuedCandidates atomic.Int64 + // If the source of candidates runs out of them, we risk + // generating too many needCandidate requests (one for + // each Config.MinCandidates). We prevent this with candidatesRequested. + candidatesRequested atomic.Bool +} + +func NewFuzzer(ctx context.Context, cfg *Config, rnd *rand.Rand, + target *prog.Target) *Fuzzer { + f := &Fuzzer{ + Config: cfg, + Corpus: newCorpus(), + NeedCandidates: make(chan struct{}, 1), + + ctx: ctx, + stats: map[string]uint64{}, + rnd: rnd, + target: target, + + // We're okay to lose some of the messages -- if we are already + // regenerating the table, we don't want to repeat it right away. + ctRegenerate: make(chan struct{}), + + nextExec: makePriorityQueue[*Request](), + runningExecs: map[*Request]time.Time{}, + } + f.updateChoiceTable(nil) + go f.choiceTableUpdater() + if cfg.Debug { + go f.leakDetector() + go f.logCurrentStats() + } + return f +} + +type Config struct { + Debug bool + Logf func(level int, msg string, args ...interface{}) + Coverage bool + FaultInjection bool + Comparisons bool + Collide bool + EnabledCalls map[*prog.Syscall]bool + NoMutateCalls map[int]bool + LeakChecking bool + FetchRawCover bool + // If the number of queued candidates is less than MinCandidates, + // NeedCandidates is triggered. + MinCandidates uint + NewInputs chan rpctype.Input +} + +type Request struct { + Prog *prog.Prog + NeedCover bool + NeedRawCover bool + NeedSignal bool + NeedHints bool + // Fields that are only relevant within pkg/fuzzer. + flags ProgTypes + stat string + result *Result + resultC chan *Result +} + +type Result struct { + Info *ipc.ProgInfo + Stop bool +} + +func (fuzzer *Fuzzer) Done(req *Request, res *Result) { + // Triage individual calls. + // We do it before unblocking the waiting threads because + // it may result it concurrent modification of req.Prog. + if req.NeedSignal && res.Info != nil { + for call, info := range res.Info.Calls { + fuzzer.triageProgCall(req.Prog, &info, call, req.flags) + } + fuzzer.triageProgCall(req.Prog, &res.Info.Extra, -1, req.flags) + } + // Unblock threads that wait for the result. + req.result = res + if req.resultC != nil { + req.resultC <- res + } + // Update stats. + fuzzer.mu.Lock() + fuzzer.stats[req.stat]++ + delete(fuzzer.runningExecs, req) + fuzzer.mu.Unlock() +} + +func (fuzzer *Fuzzer) triageProgCall(p *prog.Prog, info *ipc.CallInfo, call int, + flags ProgTypes) { + prio := signalPrio(p, info, call) + newMaxSignal := fuzzer.Corpus.AddRawMaxSignal(info.Signal, prio) + if newMaxSignal.Empty() { + return + } + fuzzer.Logf(2, "found new signal in call %d in %s", call, p) + fuzzer.startJob(&triageJob{ + p: p.Clone(), + call: call, + info: *info, + newSignal: newMaxSignal, + flags: flags, + jobPriority: triageJobPrio(flags), + }) +} + +func signalPrio(p *prog.Prog, info *ipc.CallInfo, call int) (prio uint8) { + if call == -1 { + return 0 + } + if info.Errno == 0 { + prio |= 1 << 1 + } + if !p.Target.CallContainsAny(p.Calls[call]) { + prio |= 1 << 0 + } + return +} + +type Candidate struct { + Prog *prog.Prog + Hash hash.Sig + Smashed bool + Minimized bool +} + +func (fuzzer *Fuzzer) NextInput() *Request { + req := fuzzer.nextInput() + fuzzer.mu.Lock() + fuzzer.runningExecs[req] = time.Now() + fuzzer.mu.Unlock() + if req.stat == statCandidate { + if fuzzer.queuedCandidates.Add(-1) < 0 { + panic("queuedCandidates is out of sync") + } + } + if fuzzer.NeedCandidatesNow() && + !fuzzer.candidatesRequested.CompareAndSwap(false, true) { + select { + case fuzzer.NeedCandidates <- struct{}{}: + default: + } + } + return req +} + +func (fuzzer *Fuzzer) nextInput() *Request { + nextExec := fuzzer.nextExec.tryPop() + if nextExec != nil { + return nextExec.value + } + // Either generate a new input or mutate an existing one. + mutateRate := 0.95 + if !fuzzer.Config.Coverage { + // If we don't have real coverage signal, generate programs + // more frequently because fallback signal is weak. + mutateRate = 0.5 + } + rnd := fuzzer.rand() + if rnd.Float64() < mutateRate { + req := mutateProgRequest(fuzzer, rnd) + if req != nil { + return req + } + } + return genProgRequest(fuzzer, rnd) +} + +func (fuzzer *Fuzzer) startJob(newJob job) { + fuzzer.Logf(2, "started %T", newJob) + newJob.saveID(-fuzzer.nextJobID.Add(1)) + go newJob.run(fuzzer) +} + +func (fuzzer *Fuzzer) Logf(level int, msg string, args ...interface{}) { + if fuzzer.Config.Logf == nil { + return + } + fuzzer.Config.Logf(level, msg, args...) +} + +func (fuzzer *Fuzzer) NeedCandidatesNow() bool { + return fuzzer.queuedCandidates.Load() < int64(fuzzer.Config.MinCandidates) +} + +func (fuzzer *Fuzzer) AddCandidates(candidates []Candidate) { + fuzzer.queuedCandidates.Add(int64(len(candidates))) + for _, candidate := range candidates { + fuzzer.pushExec(candidateRequest(candidate), priority{candidatePrio}) + } + fuzzer.candidatesRequested.Store(false) +} + +func (fuzzer *Fuzzer) rand() *rand.Rand { + fuzzer.mu.Lock() + seed := fuzzer.rnd.Int63() + fuzzer.mu.Unlock() + return rand.New(rand.NewSource(seed)) +} + +func (fuzzer *Fuzzer) pushExec(req *Request, prio priority) { + if req.stat == "" { + panic("Request.Stat field must be set") + } + if req.NeedHints && (req.NeedCover || req.NeedSignal) { + panic("Request.NeedHints is mutually exclusive with other fields") + } + fuzzer.nextExec.push(&priorityQueueItem[*Request]{ + value: req, prio: prio, + }) +} + +func (fuzzer *Fuzzer) exec(job job, req *Request) *Result { + req.resultC = make(chan *Result, 1) + fuzzer.pushExec(req, job.priority()) + select { + case <-fuzzer.ctx.Done(): + return &Result{Stop: true} + case res := <-req.resultC: + close(req.resultC) + return res + } +} + +func (fuzzer *Fuzzer) leakDetector() { + const timeout = 20 * time.Minute + ticket := time.NewTicker(timeout) + defer ticket.Stop() + for { + select { + case now := <-ticket.C: + fuzzer.mu.Lock() + for req, startedTime := range fuzzer.runningExecs { + if now.Sub(startedTime) > timeout { + panic(fmt.Sprintf("execution timed out: %v", req)) + } + } + fuzzer.mu.Unlock() + case <-fuzzer.ctx.Done(): + return + } + } +} + +func (fuzzer *Fuzzer) updateChoiceTable(programs []*prog.Prog) { + newCt := fuzzer.target.BuildChoiceTable(programs, fuzzer.Config.EnabledCalls) + + fuzzer.ctMu.Lock() + defer fuzzer.ctMu.Unlock() + if len(programs) >= fuzzer.ctProgs { + fuzzer.ctProgs = len(programs) + fuzzer.ct = newCt + } +} + +func (fuzzer *Fuzzer) choiceTableUpdater() { + for { + select { + case <-fuzzer.ctx.Done(): + return + case <-fuzzer.ctRegenerate: + } + fuzzer.updateChoiceTable(fuzzer.Corpus.Programs()) + } +} + +func (fuzzer *Fuzzer) ChoiceTable() *prog.ChoiceTable { + progs := fuzzer.Corpus.Programs() + + fuzzer.ctMu.Lock() + defer fuzzer.ctMu.Unlock() + + // There were no deep ideas nor any calculations behind these numbers. + regenerateEveryProgs := 333 + if len(progs) < 100 { + regenerateEveryProgs = 33 + } + if fuzzer.ctProgs+regenerateEveryProgs < len(progs) { + select { + case fuzzer.ctRegenerate <- struct{}{}: + default: + // We're okay to lose the message. + // It means that we're already regenerating the table. + } + } + return fuzzer.ct +} + +func (fuzzer *Fuzzer) logCurrentStats() { + for { + select { + case <-time.After(time.Minute): + case <-fuzzer.ctx.Done(): + return + } + + var m runtime.MemStats + runtime.ReadMemStats(&m) + + fuzzer.mu.Lock() + str := fmt.Sprintf("exec queue size: %d, running execs: %d, heap (MB): %d", + fuzzer.nextExec.Len(), len(fuzzer.runningExecs), m.Alloc/1000/1000) + fuzzer.mu.Unlock() + fuzzer.Logf(0, "%s", str) + } +} diff --git a/pkg/fuzzer/fuzzer_test.go b/pkg/fuzzer/fuzzer_test.go new file mode 100644 index 000000000..1896b2b84 --- /dev/null +++ b/pkg/fuzzer/fuzzer_test.go @@ -0,0 +1,290 @@ +// 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" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + "sync" + "testing" + "time" + + "github.com/google/syzkaller/pkg/csource" + "github.com/google/syzkaller/pkg/ipc" + "github.com/google/syzkaller/pkg/ipc/ipcconfig" + "github.com/google/syzkaller/pkg/rpctype" + "github.com/google/syzkaller/pkg/testutil" + "github.com/google/syzkaller/prog" + "github.com/google/syzkaller/sys/targets" + "github.com/stretchr/testify/assert" + "golang.org/x/sync/errgroup" +) + +func TestFuzz(t *testing.T) { + defer checkGoroutineLeaks() + + target, err := prog.GetTarget(targets.TestOS, targets.TestArch64Fuzz) + if err != nil { + t.Fatal(err) + } + executor := buildExecutor(t, target) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + fuzzer := NewFuzzer(ctx, &Config{ + Debug: true, + 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, + }, + NewInputs: make(chan rpctype.Input), + }, rand.New(testutil.RandSource(t)), target) + + go func() { + for { + select { + case <-ctx.Done(): + return + case c := <-fuzzer.Config.NewInputs: + t.Logf("new prog:\n%s", c.Prog) + } + } + }() + + tf := newTestFuzzer(t, fuzzer, map[string]bool{ + "first bug": true, + "second bug": true, + }, 10000) + + for i := 0; i < 2; i++ { + tf.registerExecutor(newProc(t, target, executor)) + } + tf.wait() + + t.Logf("resulting corpus:") + for _, p := range fuzzer.Corpus.Programs() { + t.Logf("-----") + t.Logf("%s", p.Serialize()) + } + + assert.Equal(t, len(tf.expectedCrashes), len(tf.crashes), + "not all expected crashes were found") +} + +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{ + 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.NextInput() + res, _, _ := emulateExec(req) + fuzzer.Done(req, res) + } + }) +} + +// Based on the example from Go documentation. +var crc32q = crc32.MakeTable(0xD5828281) + +func emulateExec(req *Request) (*Result, string, error) { + serializedLines := bytes.Split(req.Prog.Serialize(), []byte("\n")) + var info ipc.ProgInfo + for i, call := range req.Prog.Calls { + cover := uint32(call.Meta.ID*1024) + + crc32.Checksum(serializedLines[i], crc32q)%4 + callInfo := ipc.CallInfo{} + if req.NeedCover { + callInfo.Cover = []uint32{cover} + } + if req.NeedSignal { + callInfo.Signal = []uint32{cover} + } + info.Calls = append(info.Calls, callInfo) + } + return &Result{Info: &info}, "", nil +} + +type testFuzzer struct { + t testing.TB + eg errgroup.Group + fuzzer *Fuzzer + mu sync.Mutex + crashes map[string]int + expectedCrashes map[string]bool + iter int + iterLimit int +} + +func newTestFuzzer(t testing.TB, fuzzer *Fuzzer, expectedCrashes map[string]bool, iterLimit int) *testFuzzer { + return &testFuzzer{ + t: t, + fuzzer: fuzzer, + expectedCrashes: expectedCrashes, + crashes: map[string]int{}, + iterLimit: iterLimit, + } +} + +func (f *testFuzzer) oneMore() bool { + f.mu.Lock() + defer f.mu.Unlock() + f.iter++ + if f.iter%100 == 0 { + stat := f.fuzzer.Corpus.Stat() + f.t.Logf("<iter %d>: corpus %d, signal %d, max signal %d, crash types %d", + f.iter, stat.Progs, stat.Signal, stat.MaxSignal, len(f.crashes)) + } + return f.iter < f.iterLimit && + (f.expectedCrashes == nil || len(f.crashes) != len(f.expectedCrashes)) +} + +func (f *testFuzzer) registerExecutor(proc *executorProc) { + f.eg.Go(func() error { + for f.oneMore() { + req := f.fuzzer.NextInput() + res, crash, err := proc.execute(req) + if err != nil { + return err + } + if crash != "" { + res = &Result{Stop: true} + if !f.expectedCrashes[crash] { + return fmt.Errorf("unexpected crash: %q", crash) + } + f.mu.Lock() + f.t.Logf("CRASH: %s", crash) + f.crashes[crash]++ + f.mu.Unlock() + } + f.fuzzer.Done(req, res) + } + return nil + }) +} + +func (f *testFuzzer) wait() { + t := f.t + err := f.eg.Wait() + if err != nil { + t.Fatal(err) + } + t.Logf("crashes:") + for title, cnt := range f.crashes { + t.Logf("%s: %d", title, cnt) + } + t.Logf("stats:\n%v", f.fuzzer.GrabStats()) +} + +// TODO: it's already implemented in syz-fuzzer/proc.go, +// pkg/runtest and tools/syz-execprog. +// Looks like it's time to factor out this functionality. +type executorProc struct { + env *ipc.Env + execOpts ipc.ExecOpts +} + +func newProc(t *testing.T, target *prog.Target, executor string) *executorProc { + config, execOpts, err := ipcconfig.Default(target) + if err != nil { + t.Fatal(err) + } + config.Executor = executor + config.Flags |= ipc.FlagSignal + env, err := ipc.MakeEnv(config, 0) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { env.Close() }) + return &executorProc{ + env: env, + execOpts: *execOpts, + } +} + +var crashRe = regexp.MustCompile(`{{CRASH: (.*?)}}`) + +func (proc *executorProc) execute(req *Request) (*Result, string, error) { + execOpts := proc.execOpts + // TODO: it's duplicated from fuzzer.go. + if req.NeedSignal { + execOpts.Flags |= ipc.FlagCollectSignal + } + if req.NeedCover { + execOpts.Flags |= ipc.FlagCollectCover + } + // TODO: support req.NeedHints. + output, info, _, err := proc.env.Exec(&execOpts, req.Prog) + ret := crashRe.FindStringSubmatch(string(output)) + if ret != nil { + return nil, ret[1], nil + } else if err != nil { + return nil, "", err + } + return &Result{Info: info}, "", nil +} + +func buildExecutor(t *testing.T, target *prog.Target) string { + executor, err := csource.BuildFile(target, + filepath.FromSlash("../../executor/executor.cc"), + "-fsanitize-coverage=trace-pc", "-g", + ) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { os.Remove(executor) }) + return executor +} + +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) + } +} diff --git a/pkg/fuzzer/job.go b/pkg/fuzzer/job.go new file mode 100644 index 000000000..5a8f610b0 --- /dev/null +++ b/pkg/fuzzer/job.go @@ -0,0 +1,401 @@ +// 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 ( + "fmt" + "math/rand" + + "github.com/google/syzkaller/pkg/cover" + "github.com/google/syzkaller/pkg/hash" + "github.com/google/syzkaller/pkg/ipc" + "github.com/google/syzkaller/pkg/rpctype" + "github.com/google/syzkaller/pkg/signal" + "github.com/google/syzkaller/prog" +) + +const ( + smashPrio int64 = iota + 1 + genPrio + triagePrio + candidatePrio + candidateTriagePrio +) + +type job interface { + run(fuzzer *Fuzzer) + saveID(id int64) + priority() priority +} + +type ProgTypes int + +const ( + ProgCandidate ProgTypes = 1 << iota + ProgMinimized + ProgSmashed + ProgNormal ProgTypes = 0 +) + +type jobPriority struct { + prio priority +} + +func newJobPriority(base int64) jobPriority { + prio := append(make(priority, 0, 2), base) + return jobPriority{prio} +} + +func (jp jobPriority) priority() priority { + return jp.prio +} + +// If we prioritize execution requests only by the base priorities of their origin +// jobs, we risk letting 1000s of simultaneous jobs slowly progress in parallel. +// It's better to let same-prio jobs that were started earlier finish first. +// saveID() allows Fuzzer to attach this sub-prio at the moment of job creation. +func (jp *jobPriority) saveID(id int64) { + jp.prio = append(jp.prio, id) +} + +func genProgRequest(fuzzer *Fuzzer, rnd *rand.Rand) *Request { + p := fuzzer.target.Generate(rnd, + prog.RecommendedCalls, + fuzzer.ChoiceTable()) + return &Request{ + Prog: p, + NeedSignal: true, + stat: statGenerate, + } +} + +func mutateProgRequest(fuzzer *Fuzzer, rnd *rand.Rand) *Request { + p := fuzzer.Corpus.chooseProgram(rnd) + if p == nil { + return nil + } + newP := p.Clone() + newP.Mutate(rnd, + prog.RecommendedCalls, + fuzzer.ChoiceTable(), + fuzzer.Config.NoMutateCalls, + fuzzer.Corpus.Programs(), + ) + return &Request{ + Prog: newP, + NeedSignal: true, + stat: statFuzz, + } +} + +func candidateRequest(input Candidate) *Request { + flags := ProgCandidate + if input.Minimized { + flags |= ProgMinimized + } + if input.Smashed { + flags |= ProgSmashed + } + return &Request{ + Prog: input.Prog, + NeedSignal: true, + stat: statCandidate, + flags: flags, + } +} + +// triageJob are programs for which we noticed potential new coverage during +// first execution. But we are not sure yet if the coverage is real or not. +// During triage we understand if these programs in fact give new coverage, +// and if yes, minimize them and add to corpus. +type triageJob struct { + p *prog.Prog + call int + info ipc.CallInfo + newSignal signal.Signal + flags ProgTypes + jobPriority +} + +func triageJobPrio(flags ProgTypes) jobPriority { + if flags&ProgCandidate > 0 { + return newJobPriority(candidateTriagePrio) + } + return newJobPriority(triagePrio) +} + +func (job *triageJob) run(fuzzer *Fuzzer) { + callName := ".extra" + logCallName := "extra" + if job.call != -1 { + callName = job.p.Calls[job.call].Meta.Name + logCallName = fmt.Sprintf("call #%v %v", job.call, callName) + } + fuzzer.Logf(3, "triaging input for %v (new signal=%v)", logCallName, job.newSignal.Len()) + // Compute input coverage and non-flaky signal for minimization. + info, stop := job.deflake(fuzzer) + if stop || info.newStableSignal.Empty() { + return + } + if job.flags&ProgMinimized == 0 { + stop = job.minimize(fuzzer, info.newStableSignal) + if stop { + return + } + } + data := job.p.Serialize() + fuzzer.Logf(2, "added new input for %q to the corpus:\n%s", + logCallName, string(data)) + if job.flags&ProgSmashed == 0 { + fuzzer.startJob(&smashJob{ + p: job.p.Clone(), + call: job.call, + jobPriority: newJobPriority(smashPrio), + }) + } + fuzzer.Corpus.Save(job.p, info.stableSignal, hash.Hash(data)) + if fuzzer.Config.NewInputs != nil { + select { + case <-fuzzer.ctx.Done(): + case fuzzer.Config.NewInputs <- rpctype.Input{ + Call: callName, + CallID: job.call, + Prog: data, + Signal: info.stableSignal.Serialize(), + Cover: info.cover.Serialize(), + RawCover: info.rawCover, + }: + } + } +} + +type deflakedCover struct { + stableSignal signal.Signal + newStableSignal signal.Signal + cover cover.Cover + rawCover []uint32 +} + +func (job *triageJob) deflake(fuzzer *Fuzzer) (info deflakedCover, stop bool) { + const signalRuns = 3 + var notExecuted int + for i := 0; i < signalRuns; i++ { + result := fuzzer.exec(job, &Request{ + Prog: job.p, + NeedSignal: true, + NeedCover: true, + NeedRawCover: fuzzer.Config.FetchRawCover, + stat: statTriage, + }) + if result.Stop { + stop = true + return + } + if !reexecutionSuccess(result.Info, &job.info, job.call) { + // The call was not executed or failed. + notExecuted++ + if notExecuted >= signalRuns/2+1 { + stop = true + return // if happens too often, give up + } + continue + } + thisSignal, thisCover := getSignalAndCover(job.p, result.Info, job.call) + if len(info.rawCover) == 0 && fuzzer.Config.FetchRawCover { + info.rawCover = thisCover + } + if i == 0 { + info.stableSignal = thisSignal + info.newStableSignal = job.newSignal.Intersection(thisSignal) + } else { + info.stableSignal = info.stableSignal.Intersection(thisSignal) + info.newStableSignal = info.newStableSignal.Intersection(thisSignal) + } + if info.newStableSignal.Empty() { + return + } + info.cover.Merge(thisCover) + } + return +} + +func (job *triageJob) minimize(fuzzer *Fuzzer, newSignal signal.Signal) (stop bool) { + const minimizeAttempts = 3 + job.p, job.call = prog.Minimize(job.p, job.call, false, + func(p1 *prog.Prog, call1 int) bool { + if stop { + return false + } + for i := 0; i < minimizeAttempts; i++ { + result := fuzzer.exec(job, &Request{ + Prog: p1, + NeedSignal: true, + stat: statMinimize, + }) + if result.Stop { + stop = true + return false + } + info := result.Info + if !reexecutionSuccess(info, &job.info, call1) { + // The call was not executed or failed. + continue + } + thisSignal, _ := getSignalAndCover(p1, info, call1) + if newSignal.Intersection(thisSignal).Len() == newSignal.Len() { + return true + } + } + return false + }) + return stop +} + +func reexecutionSuccess(info *ipc.ProgInfo, oldInfo *ipc.CallInfo, call int) bool { + if info == nil || len(info.Calls) == 0 { + return false + } + if call != -1 { + // Don't minimize calls from successful to unsuccessful. + // Successful calls are much more valuable. + if oldInfo.Errno == 0 && info.Calls[call].Errno != 0 { + return false + } + return len(info.Calls[call].Signal) != 0 + } + return len(info.Extra.Signal) != 0 +} + +func getSignalAndCover(p *prog.Prog, info *ipc.ProgInfo, call int) (signal.Signal, []uint32) { + inf := &info.Extra + if call != -1 { + inf = &info.Calls[call] + } + return signal.FromRaw(inf.Signal, signalPrio(p, inf, call)), inf.Cover +} + +type smashJob struct { + p *prog.Prog + call int + jobPriority +} + +func (job *smashJob) run(fuzzer *Fuzzer) { + fuzzer.Logf(2, "smashing the program %s (call=%d):", job.p, job.call) + if fuzzer.Config.Comparisons && job.call >= 0 { + fuzzer.startJob(&hintsJob{ + p: job.p.Clone(), + call: job.call, + jobPriority: newJobPriority(smashPrio), + }) + } + + const iters = 100 + rnd := fuzzer.rand() + for i := 0; i < iters; i++ { + p := job.p.Clone() + p.Mutate(rnd, prog.RecommendedCalls, + fuzzer.ChoiceTable(), + fuzzer.Config.NoMutateCalls, + fuzzer.Corpus.Programs()) + result := fuzzer.exec(job, &Request{ + Prog: p, + NeedSignal: true, + stat: statSmash, + }) + if result.Stop { + return + } + if fuzzer.Config.Collide { + result := fuzzer.exec(job, &Request{ + Prog: randomCollide(p, rnd), + stat: statCollide, + }) + if result.Stop { + return + } + } + } + if fuzzer.Config.FaultInjection && job.call >= 0 { + job.faultInjection(fuzzer) + } +} + +func randomCollide(origP *prog.Prog, rnd *rand.Rand) *prog.Prog { + if rnd.Intn(5) == 0 { + // Old-style collide with a 20% probability. + p, err := prog.DoubleExecCollide(origP, rnd) + if err == nil { + return p + } + } + if rnd.Intn(4) == 0 { + // Duplicate random calls with a 20% probability (25% * 80%). + p, err := prog.DupCallCollide(origP, rnd) + if err == nil { + return p + } + } + p := prog.AssignRandomAsync(origP, rnd) + if rnd.Intn(2) != 0 { + prog.AssignRandomRerun(p, rnd) + } + return p +} + +func (job *smashJob) faultInjection(fuzzer *Fuzzer) { + for nth := 1; nth <= 100; nth++ { + fuzzer.Logf(2, "injecting fault into call %v, step %v", + job.call, nth) + newProg := job.p.Clone() + newProg.Calls[job.call].Props.FailNth = nth + result := fuzzer.exec(job, &Request{ + Prog: job.p, + stat: statSmash, + }) + if result.Stop { + return + } + info := result.Info + if info != nil && len(info.Calls) > job.call && + info.Calls[job.call].Flags&ipc.CallFaultInjected == 0 { + break + } + } +} + +type hintsJob struct { + p *prog.Prog + call int + jobPriority +} + +func (job *hintsJob) run(fuzzer *Fuzzer) { + // First execute the original program to dump comparisons from KCOV. + p := job.p + result := fuzzer.exec(job, &Request{ + Prog: p, + NeedHints: true, + stat: statSeed, + }) + if result.Stop || result.Info == nil { + return + } + // Then mutate the initial program for every match between + // a syscall argument and a comparison operand. + // Execute each of such mutants to check if it gives new coverage. + var stop bool + p.MutateWithHints(job.call, result.Info.Calls[job.call].Comps, + func(p *prog.Prog) { + if stop { + return + } + result := fuzzer.exec(job, &Request{ + Prog: p, + NeedSignal: true, + stat: statHint, + }) + stop = stop || result.Stop + }) +} diff --git a/pkg/fuzzer/prio_queue.go b/pkg/fuzzer/prio_queue.go new file mode 100644 index 000000000..ea8f448c9 --- /dev/null +++ b/pkg/fuzzer/prio_queue.go @@ -0,0 +1,100 @@ +// 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 ( + "container/heap" + "sync" +) + +type priority []int64 + +func (p priority) greaterThan(other priority) bool { + for i := range p { + if i >= len(other) || p[i] > other[i] { + return true + } + if p[i] < other[i] { + return false + } + } + return false +} + +type priorityQueue[T any] struct { + impl priorityQueueImpl[T] + c *sync.Cond +} + +func makePriorityQueue[T any]() *priorityQueue[T] { + return &priorityQueue[T]{ + c: sync.NewCond(&sync.Mutex{}), + } +} + +func (pq *priorityQueue[T]) Len() int { + pq.c.L.Lock() + defer pq.c.L.Unlock() + return pq.impl.Len() +} + +func (pq *priorityQueue[T]) push(item *priorityQueueItem[T]) { + pq.c.L.Lock() + defer pq.c.L.Unlock() + heap.Push(&pq.impl, item) + pq.c.Signal() +} + +// pop() blocks until there's input. +func (pq *priorityQueue[T]) pop() *priorityQueueItem[T] { + pq.c.L.Lock() + defer pq.c.L.Unlock() + for pq.impl.Len() == 0 { + pq.c.Wait() + } + return heap.Pop(&pq.impl).(*priorityQueueItem[T]) +} + +func (pq *priorityQueue[T]) tryPop() *priorityQueueItem[T] { + pq.c.L.Lock() + defer pq.c.L.Unlock() + if len(pq.impl) == 0 { + return nil + } + return heap.Pop(&pq.impl).(*priorityQueueItem[T]) +} + +// The implementation below is based on the example provided +// by https://pkg.go.dev/container/heap. + +type priorityQueueItem[T any] struct { + value T + prio priority +} + +type priorityQueueImpl[T any] []*priorityQueueItem[T] + +func (pq priorityQueueImpl[T]) Len() int { return len(pq) } + +func (pq priorityQueueImpl[T]) Less(i, j int) bool { + // We want Pop to give us the highest, not lowest, + // priority so we use greater than here. + return pq[i].prio.greaterThan(pq[j].prio) +} + +func (pq priorityQueueImpl[T]) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] +} + +func (pq *priorityQueueImpl[T]) Push(x any) { + *pq = append(*pq, x.(*priorityQueueItem[T])) +} + +func (pq *priorityQueueImpl[T]) Pop() any { + n := len(*pq) + item := (*pq)[n-1] + (*pq)[n-1] = nil + *pq = (*pq)[:n-1] + return item +} diff --git a/pkg/fuzzer/prio_queue_test.go b/pkg/fuzzer/prio_queue_test.go new file mode 100644 index 000000000..b2abdb01b --- /dev/null +++ b/pkg/fuzzer/prio_queue_test.go @@ -0,0 +1,44 @@ +// 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 ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPriority(t *testing.T) { + assert.True(t, priority{1, 2}.greaterThan(priority{1, 1})) + assert.True(t, priority{3, 2}.greaterThan(priority{2, 3})) + assert.True(t, priority{1, -5}.greaterThan(priority{1, -10})) +} + +func TestPrioQueueOrder(t *testing.T) { + pq := makePriorityQueue[int]() + pq.push(&priorityQueueItem[int]{value: 1, prio: priority{1}}) + pq.push(&priorityQueueItem[int]{value: 3, prio: priority{3}}) + pq.push(&priorityQueueItem[int]{value: 2, prio: priority{2}}) + + assert.Equal(t, 3, pq.pop().value) + assert.Equal(t, 2, pq.pop().value) + assert.Equal(t, 1, pq.pop().value) + assert.Nil(t, pq.tryPop()) +} + +func TestPrioQueueWait(t *testing.T) { + var wg sync.WaitGroup + pq := makePriorityQueue[int]() + assert.Nil(t, pq.tryPop()) + + wg.Add(1) + go func() { + assert.Equal(t, 10, pq.pop().value) + wg.Done() + }() + + pq.push(&priorityQueueItem[int]{value: 10, prio: priority{1}}) + wg.Wait() +} diff --git a/pkg/fuzzer/stats.go b/pkg/fuzzer/stats.go new file mode 100644 index 000000000..17bc6131c --- /dev/null +++ b/pkg/fuzzer/stats.go @@ -0,0 +1,26 @@ +// 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 + +const ( + statGenerate = "exec gen" + statFuzz = "exec fuzz" + statCandidate = "exec candidate" + statTriage = "exec triage" + statMinimize = "exec minimize" + statSmash = "exec smash" + statHint = "exec hints" + statSeed = "exec seeds" + statCollide = "exec collide" + statExecTotal = "exec total" + statBufferTooSmall = "buffer too small" +) + +func (fuzzer *Fuzzer) GrabStats() map[string]uint64 { + fuzzer.mu.Lock() + defer fuzzer.mu.Unlock() + ret := fuzzer.stats + fuzzer.stats = map[string]uint64{} + return ret +} diff --git a/sys/targets/targets.go b/sys/targets/targets.go index 40fc40991..840c1febf 100644 --- a/sys/targets/targets.go +++ b/sys/targets/targets.go @@ -141,6 +141,7 @@ const ( S390x = "s390x" RiscV64 = "riscv64" TestArch64 = "64" + TestArch64Fuzz = "64_fuzz" TestArch64Fork = "64_fork" TestArch32Shmem = "32_shmem" TestArch32ForkShmem = "32_fork_shmem" @@ -189,6 +190,18 @@ var List = map[string]map[string]*Target{ ExecutorUsesForkServer: false, }, }, + TestArch64Fuzz: { + PtrSize: 8, + PageSize: 8 << 10, + // -fsanitize=address causes SIGSEGV. + CFlags: []string{"-no-pie"}, + osCommon: osCommon{ + SyscallNumbers: true, + SyscallPrefix: "SYS_", + ExecutorUsesShmem: true, + ExecutorUsesForkServer: true, + }, + }, TestArch64Fork: { PtrSize: 8, PageSize: 8 << 10, diff --git a/sys/test/expressions.txt.const b/sys/test/expressions.txt.const index 8b3a2dae5..77e181281 100644 --- a/sys/test/expressions.txt.const +++ b/sys/test/expressions.txt.const @@ -1,3 +1,3 @@ -arches = 32_fork_shmem, 32_shmem, 64, 64_fork +arches = 32_fork_shmem, 32_shmem, 64, 64_fork, 64_fuzz FIELD_FLAG1 = 2 FIELD_FLAG2 = 4
\ No newline at end of file diff --git a/sys/test/fuzzer.txt b/sys/test/fuzzer.txt new file mode 100644 index 000000000..11e91c992 --- /dev/null +++ b/sys/test/fuzzer.txt @@ -0,0 +1,7 @@ +# 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. + +# These definitions are used for pkg/fuzzer tests. +# They must be in close sync with common_test.h. + +syz_test_fuzzer1(a int64[0:16], b int64[0:16], c int64[0:16]) diff --git a/syz-fuzzer/fuzzer.go b/syz-fuzzer/fuzzer.go index 2d9ab7cba..1adec40d4 100644 --- a/syz-fuzzer/fuzzer.go +++ b/syz-fuzzer/fuzzer.go @@ -4,6 +4,7 @@ package main import ( + "context" "flag" "fmt" "math/rand" @@ -12,12 +13,12 @@ import ( "os" "runtime" "runtime/debug" - "sort" "sync" "sync/atomic" "time" "github.com/google/syzkaller/pkg/csource" + "github.com/google/syzkaller/pkg/fuzzer" "github.com/google/syzkaller/pkg/hash" "github.com/google/syzkaller/pkg/host" "github.com/google/syzkaller/pkg/ipc" @@ -25,90 +26,29 @@ import ( "github.com/google/syzkaller/pkg/log" "github.com/google/syzkaller/pkg/osutil" "github.com/google/syzkaller/pkg/rpctype" - "github.com/google/syzkaller/pkg/signal" "github.com/google/syzkaller/pkg/tool" "github.com/google/syzkaller/prog" _ "github.com/google/syzkaller/sys" "github.com/google/syzkaller/sys/targets" ) -type Fuzzer struct { - name string - outputType OutputType - config *ipc.Config - execOpts *ipc.ExecOpts - procs []*Proc - gate *ipc.Gate - workQueue *WorkQueue - needPoll chan struct{} - choiceTable *prog.ChoiceTable - noMutate map[int]bool - // The stats field cannot unfortunately be just an uint64 array, because it - // results in "unaligned 64-bit atomic operation" errors on 32-bit platforms. - stats []uint64 +type FuzzerTool struct { + name string + outputType OutputType + config *ipc.Config + fuzzer *fuzzer.Fuzzer + procs []*Proc + gate *ipc.Gate manager *rpctype.RPCClient target *prog.Target triagedCandidates uint32 timeouts targets.Timeouts - faultInjectionEnabled bool - comparisonTracingEnabled bool - fetchRawCover bool - - corpusMu sync.RWMutex - corpus []*prog.Prog - corpusHashes map[hash.Sig]struct{} - corpusPrios []int64 - sumPrios int64 - - signalMu sync.RWMutex - corpusSignal signal.Signal // signal of inputs in corpus - maxSignal signal.Signal // max signal ever observed including flakes - newSignal signal.Signal // diff of maxSignal since last sync with master - checkResult *rpctype.CheckArgs logMu sync.Mutex - // Let's limit the number of concurrent NewInput requests. - parallelNewInputs chan struct{} - - // Experimental flags. - resetAccState bool -} - -type FuzzerSnapshot struct { - corpus []*prog.Prog - corpusPrios []int64 - sumPrios int64 -} - -type Stat int - -const ( - StatGenerate Stat = iota - StatFuzz - StatCandidate - StatTriage - StatMinimize - StatSmash - StatHint - StatSeed - StatCollide - StatBufferTooSmall - StatCount -) - -var statNames = [StatCount]string{ - StatGenerate: "exec gen", - StatFuzz: "exec fuzz", - StatCandidate: "exec candidate", - StatTriage: "exec triage", - StatMinimize: "exec minimize", - StatSmash: "exec smash", - StatHint: "exec hints", - StatSeed: "exec seeds", - StatCollide: "exec collide", - StatBufferTooSmall: "buffer too small", + bufferTooSmall uint64 + resetAccState bool } type OutputType int @@ -157,7 +97,8 @@ func createIPCConfig(features *host.Features, config *ipc.Config) { // It coincides with prog.MaxPids. const gateSize = prog.MaxPids -// nolint: funlen +// TODO: split into smaller methods. +// nolint: funlen, gocyclo func main() { debug.SetGCPercent(50) @@ -283,60 +224,74 @@ func main() { runTest(target, manager, *flagName, config.Executor) return } - - needPoll := make(chan struct{}, 1) - needPoll <- struct{}{} - fuzzer := &Fuzzer{ - name: *flagName, - outputType: outputType, - config: config, - execOpts: execOpts, - workQueue: newWorkQueue(*flagProcs, needPoll), - needPoll: needPoll, - manager: manager, - target: target, - timeouts: timeouts, - faultInjectionEnabled: r.CheckResult.Features[host.FeatureFault].Enabled, - comparisonTracingEnabled: r.CheckResult.Features[host.FeatureComparisons].Enabled, - corpusHashes: make(map[hash.Sig]struct{}), - checkResult: r.CheckResult, - fetchRawCover: *flagRawCover, - noMutate: r.NoMutateCalls, - stats: make([]uint64, StatCount), - // Queue no more than ~3 new inputs / proc. - parallelNewInputs: make(chan struct{}, int64(3**flagProcs)), - resetAccState: *flagResetAccState, - } - gateCallback := fuzzer.useBugFrames(r, *flagProcs) - fuzzer.gate = ipc.NewGate(gateSize, gateCallback) - - for needCandidates, more := true, true; more; needCandidates = false { - more = fuzzer.poll(needCandidates, nil) - // This loop lead to "no output" in qemu emulation, tell manager we are not dead. - log.Logf(0, "fetching corpus: %v, signal %v/%v (executing program)", - len(fuzzer.corpus), len(fuzzer.corpusSignal), len(fuzzer.maxSignal)) - } + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) calls := make(map[*prog.Syscall]bool) for _, id := range r.CheckResult.EnabledCalls[sandbox] { calls[target.Syscalls[id]] = true } - fuzzer.choiceTable = target.BuildChoiceTable(fuzzer.corpus, calls) - + fuzzerObj := fuzzer.NewFuzzer(context.Background(), &fuzzer.Config{ + Coverage: config.Flags&ipc.FlagSignal > 0, + FaultInjection: r.CheckResult.Features[host.FeatureFault].Enabled, + Comparisons: r.CheckResult.Features[host.FeatureComparisons].Enabled, + Collide: execOpts.Flags&ipc.FlagThreaded > 0, + EnabledCalls: calls, + NoMutateCalls: r.NoMutateCalls, + LeakChecking: r.CheckResult.Features[host.FeatureLeak].Enabled, + FetchRawCover: *flagRawCover, + MinCandidates: uint(*flagProcs * 2), + NewInputs: make(chan rpctype.Input), + }, rnd, target) + + fuzzerTool := &FuzzerTool{ + fuzzer: fuzzerObj, + name: *flagName, + outputType: outputType, + manager: manager, + target: target, + timeouts: timeouts, + config: config, + checkResult: r.CheckResult, + resetAccState: *flagResetAccState, + } + fuzzerObj.Config.Logf = func(level int, msg string, args ...interface{}) { + // Log 0 messages are most important: send them directly to syz-manager. + if level == 0 { + fuzzerTool.Logf(level, msg, args...) + } + // Dump log level 0 and 1 messages into syz-fuzzer output. + if level <= 1 { + fuzzerTool.logMu.Lock() + defer fuzzerTool.logMu.Unlock() + log.Logf(0, "fuzzer: "+msg, args...) + } + } + fuzzerTool.gate = ipc.NewGate(gateSize, + fuzzerTool.useBugFrames(r, *flagProcs)) + for needCandidates, more := true, true; more; needCandidates = false { + more = fuzzerTool.poll(needCandidates, nil) + // This loop lead to "no output" in qemu emulation, tell manager we are not dead. + stat := fuzzerObj.Corpus.Stat() + log.Logf(0, "fetching corpus: %v, signal %v/%v (executing program)", + stat.Progs, stat.Signal, stat.MaxSignal) + } if r.CoverFilterBitmap != nil { - fuzzer.execOpts.Flags |= ipc.FlagEnableCoverageFilter + execOpts.Flags |= ipc.FlagEnableCoverageFilter } log.Logf(0, "starting %v fuzzer processes", *flagProcs) for pid := 0; pid < *flagProcs; pid++ { - proc, err := newProc(fuzzer, pid) + proc, err := newProc(fuzzerTool, execOpts, pid) if err != nil { log.SyzFatalf("failed to create proc: %v", err) } - fuzzer.procs = append(fuzzer.procs, proc) + fuzzerTool.procs = append(fuzzerTool.procs, proc) go proc.loop() } - - fuzzer.pollLoop() + // Start send input workers. + for i := 0; i < *flagProcs*2; i++ { + go fuzzerTool.sendInputsWorker() + } + fuzzerTool.pollLoop() } func collectMachineInfos(target *prog.Target) ([]byte, []host.KernelModule) { @@ -352,33 +307,33 @@ func collectMachineInfos(target *prog.Target) ([]byte, []host.KernelModule) { } // Returns gateCallback for leak checking if enabled. -func (fuzzer *Fuzzer) useBugFrames(r *rpctype.ConnectRes, flagProcs int) func() { +func (tool *FuzzerTool) useBugFrames(r *rpctype.ConnectRes, flagProcs int) func() { var gateCallback func() if r.CheckResult.Features[host.FeatureLeak].Enabled { - gateCallback = func() { fuzzer.gateCallback(r.MemoryLeakFrames) } + gateCallback = func() { tool.gateCallback(r.MemoryLeakFrames) } } if r.CheckResult.Features[host.FeatureKCSAN].Enabled && len(r.DataRaceFrames) != 0 { - fuzzer.filterDataRaceFrames(r.DataRaceFrames) + tool.filterDataRaceFrames(r.DataRaceFrames) } return gateCallback } -func (fuzzer *Fuzzer) gateCallback(leakFrames []string) { +func (tool *FuzzerTool) gateCallback(leakFrames []string) { // Leak checking is very slow so we don't do it while triaging the corpus // (otherwise it takes infinity). When we have presumably triaged the corpus // (triagedCandidates == 1), we run leak checking bug ignore the result // to flush any previous leaks. After that (triagedCandidates == 2) // we do actual leak checking and report leaks. - triagedCandidates := atomic.LoadUint32(&fuzzer.triagedCandidates) + triagedCandidates := atomic.LoadUint32(&tool.triagedCandidates) if triagedCandidates == 0 { return } args := append([]string{"leak"}, leakFrames...) - timeout := fuzzer.timeouts.NoOutput * 9 / 10 - output, err := osutil.RunCmd(timeout, "", fuzzer.config.Executor, args...) + timeout := tool.timeouts.NoOutput * 9 / 10 + output, err := osutil.RunCmd(timeout, "", tool.config.Executor, args...) if err != nil && triagedCandidates == 2 { // If we exit right away, dying executors will dump lots of garbage to console. os.Stdout.Write(output) @@ -387,261 +342,165 @@ func (fuzzer *Fuzzer) gateCallback(leakFrames []string) { os.Exit(1) } if triagedCandidates == 1 { - atomic.StoreUint32(&fuzzer.triagedCandidates, 2) + atomic.StoreUint32(&tool.triagedCandidates, 2) } } -func (fuzzer *Fuzzer) filterDataRaceFrames(frames []string) { +func (tool *FuzzerTool) filterDataRaceFrames(frames []string) { args := append([]string{"setup_kcsan_filterlist"}, frames...) - timeout := time.Minute * fuzzer.timeouts.Scale - output, err := osutil.RunCmd(timeout, "", fuzzer.config.Executor, args...) + timeout := time.Minute * tool.timeouts.Scale + output, err := osutil.RunCmd(timeout, "", tool.config.Executor, args...) if err != nil { log.SyzFatalf("failed to set KCSAN filterlist: %v", err) } log.Logf(0, "%s", output) } -func (fuzzer *Fuzzer) pollLoop() { +func (tool *FuzzerTool) pollLoop() { var execTotal uint64 var lastPoll time.Time var lastPrint time.Time - ticker := time.NewTicker(3 * time.Second * fuzzer.timeouts.Scale).C + ticker := time.NewTicker(3 * time.Second * tool.timeouts.Scale).C for { - poll := false + needCandidates := false select { case <-ticker: - case <-fuzzer.needPoll: - poll = true + case <-tool.fuzzer.NeedCandidates: + needCandidates = true } - if fuzzer.outputType != OutputStdout && time.Since(lastPrint) > 10*time.Second*fuzzer.timeouts.Scale { + if tool.outputType != OutputStdout && time.Since(lastPrint) > 10*time.Second*tool.timeouts.Scale { // Keep-alive for manager. log.Logf(0, "alive, executed %v", execTotal) lastPrint = time.Now() } - if poll || time.Since(lastPoll) > 10*time.Second*fuzzer.timeouts.Scale { - needCandidates := fuzzer.workQueue.wantCandidates() - if poll && !needCandidates { - continue - } - stats := make(map[string]uint64) - for _, proc := range fuzzer.procs { - stats["exec total"] += atomic.SwapUint64(&proc.env.StatExecs, 0) - stats["executor restarts"] += atomic.SwapUint64(&proc.env.StatRestarts, 0) - } - for stat := Stat(0); stat < StatCount; stat++ { - v := atomic.SwapUint64(&fuzzer.stats[stat], 0) - stats[statNames[stat]] = v - execTotal += v - } - if !fuzzer.poll(needCandidates, stats) { + needCandidates = tool.fuzzer.NeedCandidatesNow() + if needCandidates || time.Since(lastPoll) > 10*time.Second*tool.timeouts.Scale { + more := tool.poll(needCandidates, tool.grabStats()) + if !more { lastPoll = time.Now() } } } } -func (fuzzer *Fuzzer) poll(needCandidates bool, stats map[string]uint64) bool { +func (tool *FuzzerTool) poll(needCandidates bool, stats map[string]uint64) bool { + fuzzer := tool.fuzzer a := &rpctype.PollArgs{ - Name: fuzzer.name, + Name: tool.name, NeedCandidates: needCandidates, - MaxSignal: fuzzer.grabNewSignal().Serialize(), + MaxSignal: fuzzer.Corpus.GrabNewSignal().Serialize(), Stats: stats, } r := &rpctype.PollRes{} - if err := fuzzer.manager.Call("Manager.Poll", a, r); err != nil { + if err := tool.manager.Call("Manager.Poll", a, r); err != nil { log.SyzFatalf("Manager.Poll call failed: %v", err) } maxSignal := r.MaxSignal.Deserialize() log.Logf(1, "poll: candidates=%v inputs=%v signal=%v", len(r.Candidates), len(r.NewInputs), maxSignal.Len()) - fuzzer.addMaxSignal(maxSignal) + fuzzer.Corpus.AddMaxSignal(maxSignal) for _, inp := range r.NewInputs { - fuzzer.addInputFromAnotherFuzzer(inp) - } - for _, candidate := range r.Candidates { - fuzzer.addCandidateInput(candidate) + tool.inputFromOtherFuzzer(inp) } - if needCandidates && len(r.Candidates) == 0 && atomic.LoadUint32(&fuzzer.triagedCandidates) == 0 { - atomic.StoreUint32(&fuzzer.triagedCandidates, 1) + tool.addCandidates(r.Candidates) + if needCandidates && len(r.Candidates) == 0 && atomic.LoadUint32(&tool.triagedCandidates) == 0 { + atomic.StoreUint32(&tool.triagedCandidates, 1) } return len(r.NewInputs) != 0 || len(r.Candidates) != 0 || maxSignal.Len() != 0 } -func (fuzzer *Fuzzer) sendInputToManager(inp rpctype.Input) { - fuzzer.parallelNewInputs <- struct{}{} - go func() { - defer func() { <-fuzzer.parallelNewInputs }() +func (tool *FuzzerTool) sendInputsWorker() { + for inp := range tool.fuzzer.Config.NewInputs { a := &rpctype.NewInputArgs{ - Name: fuzzer.name, + Name: tool.name, Input: inp, } - if err := fuzzer.manager.Call("Manager.NewInput", a, nil); err != nil { + if err := tool.manager.Call("Manager.NewInput", a, nil); err != nil { log.SyzFatalf("Manager.NewInput call failed: %v", err) } - }() + } } -func (fuzzer *Fuzzer) addInputFromAnotherFuzzer(inp rpctype.Input) { - p := fuzzer.deserializeInput(inp.Prog) - if p == nil { - return +func (tool *FuzzerTool) grabStats() map[string]uint64 { + stats := tool.fuzzer.GrabStats() + for _, proc := range tool.procs { + stats["exec total"] += atomic.SwapUint64(&proc.env.StatExecs, 0) + stats["executor restarts"] += atomic.SwapUint64(&proc.env.StatRestarts, 0) } - sig := hash.Hash(inp.Prog) - sign := inp.Signal.Deserialize() - fuzzer.addInputToCorpus(p, sign, sig) + stats["buffer too small"] = atomic.SwapUint64(&tool.bufferTooSmall, 0) + return stats } -func (fuzzer *Fuzzer) addCandidateInput(candidate rpctype.Candidate) { - p := fuzzer.deserializeInput(candidate.Prog) - if p == nil { - return +func (tool *FuzzerTool) addCandidates(candidates []rpctype.Candidate) { + var inputs []fuzzer.Candidate + for _, candidate := range candidates { + p := tool.deserializeInput(candidate.Prog) + if p == nil { + continue + } + inputs = append(inputs, fuzzer.Candidate{ + Prog: p, + Smashed: candidate.Smashed, + Minimized: candidate.Minimized, + }) } - flags := ProgCandidate - if candidate.Minimized { - flags |= ProgMinimized + if len(inputs) > 0 { + tool.fuzzer.AddCandidates(inputs) } - if candidate.Smashed { - flags |= ProgSmashed +} + +func (tool *FuzzerTool) inputFromOtherFuzzer(inp rpctype.Input) { + p := tool.deserializeInput(inp.Prog) + if p == nil { + return } - fuzzer.workQueue.enqueue(&WorkCandidate{ - p: p, - flags: flags, - }) + tool.fuzzer.Corpus.Save(p, + inp.Signal.Deserialize(), + hash.Hash(inp.Prog)) } -func (fuzzer *Fuzzer) deserializeInput(inp []byte) *prog.Prog { - p, err := fuzzer.target.Deserialize(inp, prog.NonStrict) +func (tool *FuzzerTool) deserializeInput(inp []byte) *prog.Prog { + p, err := tool.target.Deserialize(inp, prog.NonStrict) if err != nil { log.SyzFatalf("failed to deserialize prog: %v\n%s", err, inp) } - // We build choice table only after we received the initial corpus, - // so we don't check the initial corpus here, we check it later in BuildChoiceTable. - if fuzzer.choiceTable != nil { - fuzzer.checkDisabledCalls(p) - } + tool.checkDisabledCalls(p) if len(p.Calls) > prog.MaxCalls { return nil } return p } -func (fuzzer *Fuzzer) checkDisabledCalls(p *prog.Prog) { +func (tool *FuzzerTool) checkDisabledCalls(p *prog.Prog) { + ct := tool.fuzzer.ChoiceTable() for _, call := range p.Calls { - if !fuzzer.choiceTable.Enabled(call.Meta.ID) { + if !ct.Enabled(call.Meta.ID) { fmt.Printf("executing disabled syscall %v [%v]\n", call.Meta.Name, call.Meta.ID) - sandbox := ipc.FlagsToSandbox(fuzzer.config.Flags) + sandbox := ipc.FlagsToSandbox(tool.config.Flags) fmt.Printf("check result for sandbox=%v:\n", sandbox) - for _, id := range fuzzer.checkResult.EnabledCalls[sandbox] { - meta := fuzzer.target.Syscalls[id] + for _, id := range tool.checkResult.EnabledCalls[sandbox] { + meta := tool.target.Syscalls[id] fmt.Printf(" %v [%v]\n", meta.Name, meta.ID) } fmt.Printf("choice table:\n") - for i, meta := range fuzzer.target.Syscalls { - fmt.Printf(" #%v: %v [%v]: enabled=%v\n", i, meta.Name, meta.ID, fuzzer.choiceTable.Enabled(meta.ID)) + for i, meta := range tool.target.Syscalls { + fmt.Printf(" #%v: %v [%v]: enabled=%v\n", i, meta.Name, meta.ID, ct.Enabled(meta.ID)) } panic("disabled syscall") } } } -func (fuzzer *FuzzerSnapshot) chooseProgram(r *rand.Rand) *prog.Prog { - randVal := r.Int63n(fuzzer.sumPrios + 1) - idx := sort.Search(len(fuzzer.corpusPrios), func(i int) bool { - return fuzzer.corpusPrios[i] >= randVal - }) - return fuzzer.corpus[idx] -} - -func (fuzzer *Fuzzer) addInputToCorpus(p *prog.Prog, sign signal.Signal, sig hash.Sig) { - fuzzer.corpusMu.Lock() - if _, ok := fuzzer.corpusHashes[sig]; !ok { - fuzzer.corpus = append(fuzzer.corpus, p) - fuzzer.corpusHashes[sig] = struct{}{} - prio := int64(len(sign)) - if sign.Empty() { - prio = 1 - } - fuzzer.sumPrios += prio - fuzzer.corpusPrios = append(fuzzer.corpusPrios, fuzzer.sumPrios) - } - fuzzer.corpusMu.Unlock() - - if !sign.Empty() { - fuzzer.signalMu.Lock() - fuzzer.corpusSignal.Merge(sign) - fuzzer.maxSignal.Merge(sign) - fuzzer.signalMu.Unlock() - } -} - -func (fuzzer *Fuzzer) snapshot() FuzzerSnapshot { - fuzzer.corpusMu.RLock() - defer fuzzer.corpusMu.RUnlock() - return FuzzerSnapshot{fuzzer.corpus, fuzzer.corpusPrios, fuzzer.sumPrios} -} - -func (fuzzer *Fuzzer) addMaxSignal(sign signal.Signal) { - if sign.Len() == 0 { - return - } - fuzzer.signalMu.Lock() - defer fuzzer.signalMu.Unlock() - fuzzer.maxSignal.Merge(sign) -} - -func (fuzzer *Fuzzer) grabNewSignal() signal.Signal { - fuzzer.signalMu.Lock() - defer fuzzer.signalMu.Unlock() - sign := fuzzer.newSignal - if sign.Empty() { - return nil - } - fuzzer.newSignal = nil - return sign -} - -func (fuzzer *Fuzzer) corpusSignalDiff(sign signal.Signal) signal.Signal { - fuzzer.signalMu.RLock() - defer fuzzer.signalMu.RUnlock() - return fuzzer.corpusSignal.Diff(sign) -} - -func (fuzzer *Fuzzer) checkNewSignal(p *prog.Prog, info *ipc.ProgInfo) (calls []int, extra bool) { - fuzzer.signalMu.RLock() - defer fuzzer.signalMu.RUnlock() - for i, inf := range info.Calls { - if fuzzer.checkNewCallSignal(p, &inf, i) { - calls = append(calls, i) - } - } - extra = fuzzer.checkNewCallSignal(p, &info.Extra, -1) - return -} - -func (fuzzer *Fuzzer) checkNewCallSignal(p *prog.Prog, info *ipc.CallInfo, call int) bool { - diff := fuzzer.maxSignal.DiffRaw(info.Signal, signalPrio(p, info, call)) - if diff.Empty() { - return false - } - fuzzer.signalMu.RUnlock() - fuzzer.signalMu.Lock() - fuzzer.maxSignal.Merge(diff) - fuzzer.newSignal.Merge(diff) - fuzzer.signalMu.Unlock() - fuzzer.signalMu.RLock() - return true -} - // nolint: unused // It's only needed for debugging. -func (fuzzer *Fuzzer) Logf(level int, msg string, args ...interface{}) { +func (tool *FuzzerTool) Logf(level int, msg string, args ...interface{}) { go func() { a := &rpctype.LogMessageReq{ Level: level, - Name: fuzzer.name, + Name: tool.name, Message: fmt.Sprintf(msg, args...), } - if err := fuzzer.manager.Call("Manager.LogMessage", a, nil); err != nil { + if err := tool.manager.Call("Manager.LogMessage", a, nil); err != nil { log.SyzFatalf("Manager.LogMessage call failed: %v", err) } }() @@ -657,19 +516,6 @@ func setupPprofHandler(port int) { }() } -func signalPrio(p *prog.Prog, info *ipc.CallInfo, call int) (prio uint8) { - if call == -1 { - return 0 - } - if info.Errno == 0 { - prio |= 1 << 1 - } - if !p.Target.CallContainsAny(p.Calls[call]) { - prio |= 1 << 0 - } - return -} - func parseOutputType(str string) OutputType { switch str { case "none": diff --git a/syz-fuzzer/proc.go b/syz-fuzzer/proc.go index 89c12a1a7..369ec5735 100644 --- a/syz-fuzzer/proc.go +++ b/syz-fuzzer/proc.go @@ -13,316 +13,67 @@ import ( "syscall" "time" - "github.com/google/syzkaller/pkg/cover" - "github.com/google/syzkaller/pkg/hash" + "github.com/google/syzkaller/pkg/fuzzer" "github.com/google/syzkaller/pkg/ipc" "github.com/google/syzkaller/pkg/log" - "github.com/google/syzkaller/pkg/rpctype" - "github.com/google/syzkaller/pkg/signal" "github.com/google/syzkaller/prog" ) // Proc represents a single fuzzing process (executor). type Proc struct { - fuzzer *Fuzzer - pid int - env *ipc.Env - rnd *rand.Rand - execOpts *ipc.ExecOpts - execOptsCollide *ipc.ExecOpts - execOptsCover *ipc.ExecOpts - execOptsComps *ipc.ExecOpts + tool *FuzzerTool + pid int + env *ipc.Env + execOpts *ipc.ExecOpts } -func newProc(fuzzer *Fuzzer, pid int) (*Proc, error) { - env, err := ipc.MakeEnv(fuzzer.config, pid) +func newProc(tool *FuzzerTool, execOpts *ipc.ExecOpts, pid int) (*Proc, error) { + env, err := ipc.MakeEnv(tool.config, pid) if err != nil { return nil, err } - rnd := rand.New(rand.NewSource(time.Now().UnixNano() + int64(pid)*1e12)) - execOptsCollide := *fuzzer.execOpts - execOptsCollide.Flags &= ^ipc.FlagCollectSignal - execOptsCover := *fuzzer.execOpts - execOptsCover.Flags |= ipc.FlagCollectCover - execOptsComps := *fuzzer.execOpts - execOptsComps.Flags |= ipc.FlagCollectComps proc := &Proc{ - fuzzer: fuzzer, - pid: pid, - env: env, - rnd: rnd, - execOpts: fuzzer.execOpts, - execOptsCollide: &execOptsCollide, - execOptsCover: &execOptsCover, - execOptsComps: &execOptsComps, + tool: tool, + pid: pid, + env: env, + execOpts: execOpts, } return proc, nil } func (proc *Proc) loop() { - generatePeriod := 100 - if proc.fuzzer.config.Flags&ipc.FlagSignal == 0 { - // If we don't have real coverage signal, generate programs more frequently - // because fallback signal is weak. - generatePeriod = 2 - } - for i := 0; ; i++ { - item := proc.fuzzer.workQueue.dequeue() - if item != nil { - switch item := item.(type) { - case *WorkTriage: - proc.triageInput(item) - case *WorkCandidate: - proc.execute(proc.execOpts, item.p, item.flags, StatCandidate, false) - case *WorkSmash: - proc.smashInput(item) - default: - log.SyzFatalf("unknown work type: %#v", item) - } - continue - } - - ct := proc.fuzzer.choiceTable - fuzzerSnapshot := proc.fuzzer.snapshot() - if len(fuzzerSnapshot.corpus) == 0 || i%generatePeriod == 0 { - // Generate a new prog. - p := proc.fuzzer.target.Generate(proc.rnd, prog.RecommendedCalls, ct) - log.Logf(1, "#%v: generated", proc.pid) - proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatGenerate) - } else { - // Mutate an existing prog. - p := fuzzerSnapshot.chooseProgram(proc.rnd).Clone() - p.Mutate(proc.rnd, prog.RecommendedCalls, ct, proc.fuzzer.noMutate, fuzzerSnapshot.corpus) - log.Logf(1, "#%v: mutated", proc.pid) - proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatFuzz) - } - } -} - -func (proc *Proc) triageInput(item *WorkTriage) { - log.Logf(1, "#%v: triaging type=%x", proc.pid, item.flags) - - prio := signalPrio(item.p, &item.info, item.call) - inputSignal := signal.FromRaw(item.info.Signal, prio) - newSignal := proc.fuzzer.corpusSignalDiff(inputSignal) - if newSignal.Empty() { - return - } - callName := ".extra" - logCallName := "extra" - if item.call != -1 { - callName = item.p.Calls[item.call].Meta.Name - logCallName = fmt.Sprintf("call #%v %v", item.call, callName) - } - log.Logf(3, "triaging input for %v (new signal=%v)", logCallName, newSignal.Len()) - var inputCover cover.Cover - const ( - signalRuns = 3 - minimizeAttempts = 3 - ) - // Compute input coverage and non-flaky signal for minimization. - notexecuted := 0 - rawCover := []uint32{} - for i := 0; i < signalRuns; i++ { - proc.resetAccState() - info := proc.executeRaw(proc.execOptsCover, item.p, StatTriage) - if !reexecutionSuccess(info, &item.info, item.call) { - // The call was not executed or failed. - notexecuted++ - if notexecuted > signalRuns/2+1 { - return // if happens too often, give up - } - continue - } - thisSignal, thisCover := getSignalAndCover(item.p, info, item.call) - if len(rawCover) == 0 && proc.fuzzer.fetchRawCover { - rawCover = append([]uint32{}, thisCover...) - } - newSignal = newSignal.Intersection(thisSignal) - // Without !minimized check manager starts losing some considerable amount - // of coverage after each restart. Mechanics of this are not completely clear. - if newSignal.Empty() && item.flags&ProgMinimized == 0 { - return + rnd := rand.New(rand.NewSource(time.Now().UnixNano() + int64(proc.pid))) + for { + req := proc.tool.fuzzer.NextInput() + opts := *proc.execOpts + if !req.NeedSignal { + opts.Flags &= ^ipc.FlagCollectSignal } - inputCover.Merge(thisCover) - } - if item.flags&ProgMinimized == 0 { - item.p, item.call = prog.Minimize(item.p, item.call, false, - func(p1 *prog.Prog, call1 int) bool { - for i := 0; i < minimizeAttempts; i++ { - info := proc.execute(proc.execOpts, p1, ProgNormal, - StatMinimize, i == 0) - if !reexecutionSuccess(info, &item.info, call1) { - // The call was not executed or failed. - continue - } - thisSignal, _ := getSignalAndCover(p1, info, call1) - if newSignal.Intersection(thisSignal).Len() == newSignal.Len() { - return true - } - } - return false - }) - } - - data := item.p.Serialize() - sig := hash.Hash(data) - - log.Logf(2, "added new input for %v to corpus:\n%s", logCallName, data) - proc.fuzzer.sendInputToManager(rpctype.Input{ - Call: callName, - CallID: item.call, - Prog: data, - Signal: inputSignal.Serialize(), - Cover: inputCover.Serialize(), - RawCover: rawCover, - }) - - proc.fuzzer.addInputToCorpus(item.p, inputSignal, sig) - - if item.flags&ProgSmashed == 0 { - proc.fuzzer.workQueue.enqueue(&WorkSmash{item.p, item.call}) - } -} - -func reexecutionSuccess(info *ipc.ProgInfo, oldInfo *ipc.CallInfo, call int) bool { - if info == nil || len(info.Calls) == 0 { - return false - } - if call != -1 { - // Don't minimize calls from successful to unsuccessful. - // Successful calls are much more valuable. - if oldInfo.Errno == 0 && info.Calls[call].Errno != 0 { - return false + if req.NeedCover { + opts.Flags |= ipc.FlagCollectCover } - return len(info.Calls[call].Signal) != 0 - } - return len(info.Extra.Signal) != 0 -} - -func getSignalAndCover(p *prog.Prog, info *ipc.ProgInfo, call int) (signal.Signal, []uint32) { - inf := &info.Extra - if call != -1 { - inf = &info.Calls[call] - } - return signal.FromRaw(inf.Signal, signalPrio(p, inf, call)), inf.Cover -} - -func (proc *Proc) smashInput(item *WorkSmash) { - if proc.fuzzer.faultInjectionEnabled && item.call != -1 { - proc.failCall(item.p, item.call) - } - if proc.fuzzer.comparisonTracingEnabled && item.call != -1 { - proc.executeHintSeed(item.p, item.call) - } - fuzzerSnapshot := proc.fuzzer.snapshot() - for i := 0; i < 100; i++ { - p := item.p.Clone() - p.Mutate(proc.rnd, prog.RecommendedCalls, proc.fuzzer.choiceTable, proc.fuzzer.noMutate, fuzzerSnapshot.corpus) - log.Logf(1, "#%v: smash mutated", proc.pid) - proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatSmash) - } -} - -func (proc *Proc) failCall(p *prog.Prog, call int) { - for nth := 1; nth <= 100; nth++ { - log.Logf(1, "#%v: injecting fault into call %v/%v", proc.pid, call, nth) - newProg := p.Clone() - newProg.Calls[call].Props.FailNth = nth - info := proc.executeRaw(proc.execOpts, newProg, StatSmash) - if info != nil && len(info.Calls) > call && info.Calls[call].Flags&ipc.CallFaultInjected == 0 { - break + if req.NeedHints { + opts.Flags |= ipc.FlagCollectComps } - } -} - -func (proc *Proc) executeHintSeed(p *prog.Prog, call int) { - log.Logf(1, "#%v: collecting comparisons", proc.pid) - // First execute the original program to dump comparisons from KCOV. - info := proc.execute(proc.execOptsComps, p, ProgNormal, StatSeed, true) - if info == nil { - return - } - - // Then mutate the initial program for every match between - // a syscall argument and a comparison operand. - // Execute each of such mutants to check if it gives new coverage. - p.MutateWithHints(call, info.Calls[call].Comps, func(p *prog.Prog) { - log.Logf(1, "#%v: executing comparison hint", proc.pid) - proc.execute(proc.execOpts, p, ProgNormal, StatHint, false) - }) -} - -func (proc *Proc) execute(execOpts *ipc.ExecOpts, p *prog.Prog, flags ProgTypes, stat Stat, - resetState bool) *ipc.ProgInfo { - if resetState { - proc.resetAccState() - } - info := proc.executeRaw(execOpts, p, stat) - if info == nil { - return nil - } - calls, extra := proc.fuzzer.checkNewSignal(p, info) - for _, callIndex := range calls { - proc.enqueueCallTriage(p, flags, callIndex, info.Calls[callIndex]) - } - if extra { - proc.enqueueCallTriage(p, flags, -1, info.Extra) - } - return info -} - -func (proc *Proc) enqueueCallTriage(p *prog.Prog, flags ProgTypes, callIndex int, info ipc.CallInfo) { - // info.Signal points to the output shmem region, detach it before queueing. - info.Signal = append([]uint32{}, info.Signal...) - // None of the caller use Cover, so just nil it instead of detaching. - // Note: triage input uses executeRaw to get coverage. - info.Cover = nil - proc.fuzzer.workQueue.enqueue(&WorkTriage{ - p: p.Clone(), - call: callIndex, - info: info, - flags: flags, - }) -} - -func (proc *Proc) executeAndCollide(execOpts *ipc.ExecOpts, p *prog.Prog, flags ProgTypes, stat Stat) { - proc.execute(execOpts, p, flags, stat, true) - - if proc.execOptsCollide.Flags&ipc.FlagThreaded == 0 { - // We cannot collide syscalls without being in the threaded mode. - return - } - const collideIterations = 2 - for i := 0; i < collideIterations; i++ { - proc.executeRaw(proc.execOptsCollide, proc.randomCollide(p), StatCollide) - } -} - -func (proc *Proc) randomCollide(origP *prog.Prog) *prog.Prog { - if proc.rnd.Intn(5) == 0 { - // Old-style collide with a 20% probability. - p, err := prog.DoubleExecCollide(origP, proc.rnd) - if err == nil { - return p + if req.NeedRawCover { + opts.Flags &= ^ipc.FlagDedupCover } - } - if proc.rnd.Intn(4) == 0 { - // Duplicate random calls with a 20% probability (25% * 80%). - p, err := prog.DupCallCollide(origP, proc.rnd) - if err == nil { - return p + // Do not let too much state accumulate. + const restartIn = 600 + restart := rnd.Intn(restartIn) == 0 + if (restart || proc.tool.resetAccState) && + (req.NeedCover || req.NeedSignal || req.NeedHints) { + proc.env.ForceRestart() } + info := proc.executeRaw(&opts, req.Prog) + proc.tool.fuzzer.Done(req, &fuzzer.Result{ + Info: info, + }) } - p := prog.AssignRandomAsync(origP, proc.rnd) - if proc.rnd.Intn(2) != 0 { - prog.AssignRandomRerun(p, proc.rnd) - } - return p } -func (proc *Proc) executeRaw(opts *ipc.ExecOpts, p *prog.Prog, stat Stat) *ipc.ProgInfo { - proc.fuzzer.checkDisabledCalls(p) +func (proc *Proc) executeRaw(opts *ipc.ExecOpts, p *prog.Prog) *ipc.ProgInfo { + proc.tool.checkDisabledCalls(p) for try := 0; ; try++ { var output []byte var info *ipc.ProgInfo @@ -332,18 +83,17 @@ func (proc *Proc) executeRaw(opts *ipc.ExecOpts, p *prog.Prog, stat Stat) *ipc.P err := proc.env.RestartIfNeeded(p.Target) if err == nil { // Limit concurrency. - ticket := proc.fuzzer.gate.Enter() + ticket := proc.tool.gate.Enter() proc.logProgram(opts, p) - atomic.AddUint64(&proc.fuzzer.stats[stat], 1) output, info, hanged, err = proc.env.Exec(opts, p) - proc.fuzzer.gate.Leave(ticket) + proc.tool.gate.Leave(ticket) } if err != nil { if err == prog.ErrExecBufferTooSmall { // It's bad if we systematically fail to serialize programs, // but so far we don't have a better handling than counting this. // This error is observed a lot on the seeded syz_mount_image calls. - atomic.AddUint64(&proc.fuzzer.stats[StatBufferTooSmall], 1) + atomic.AddUint64(&proc.tool.bufferTooSmall, 1) return nil } if try > 10 { @@ -360,7 +110,7 @@ func (proc *Proc) executeRaw(opts *ipc.ExecOpts, p *prog.Prog, stat Stat) *ipc.P } func (proc *Proc) logProgram(opts *ipc.ExecOpts, p *prog.Prog) { - if proc.fuzzer.outputType == OutputNone { + if proc.tool.outputType == OutputNone { return } @@ -368,14 +118,14 @@ func (proc *Proc) logProgram(opts *ipc.ExecOpts, p *prog.Prog) { // The following output helps to understand what program crashed kernel. // It must not be intermixed. - switch proc.fuzzer.outputType { + switch proc.tool.outputType { case OutputStdout: now := time.Now() - proc.fuzzer.logMu.Lock() + proc.tool.logMu.Lock() fmt.Printf("%02v:%02v:%02v executing program %v:\n%s\n", now.Hour(), now.Minute(), now.Second(), proc.pid, data) - proc.fuzzer.logMu.Unlock() + proc.tool.logMu.Unlock() case OutputDmesg: fd, err := syscall.Open("/dev/kmsg", syscall.O_WRONLY, 0) if err == nil { @@ -386,19 +136,12 @@ func (proc *Proc) logProgram(opts *ipc.ExecOpts, p *prog.Prog) { syscall.Close(fd) } case OutputFile: - f, err := os.Create(fmt.Sprintf("%v-%v.prog", proc.fuzzer.name, proc.pid)) + f, err := os.Create(fmt.Sprintf("%v-%v.prog", proc.tool.name, proc.pid)) if err == nil { f.Write(data) f.Close() } default: - log.SyzFatalf("unknown output type: %v", proc.fuzzer.outputType) - } -} - -func (proc *Proc) resetAccState() { - if !proc.fuzzer.resetAccState { - return + log.SyzFatalf("unknown output type: %v", proc.tool.outputType) } - proc.env.ForceRestart() } diff --git a/syz-fuzzer/workqueue.go b/syz-fuzzer/workqueue.go deleted file mode 100644 index 62648336c..000000000 --- a/syz-fuzzer/workqueue.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2017 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 main - -import ( - "sync" - - "github.com/google/syzkaller/pkg/ipc" - "github.com/google/syzkaller/prog" -) - -// WorkQueue holds global non-fuzzing work items (see the Work* structs below). -// WorkQueue also does prioritization among work items, for example, we want -// to triage and send to manager new inputs before we smash programs -// in order to not permanently lose interesting programs in case of VM crash. -type WorkQueue struct { - mu sync.RWMutex - triageCandidate []*WorkTriage - candidate []*WorkCandidate - triage []*WorkTriage - smash []*WorkSmash - - procs int - needCandidates chan struct{} -} - -type ProgTypes int - -const ( - ProgCandidate ProgTypes = 1 << iota - ProgMinimized - ProgSmashed - ProgNormal ProgTypes = 0 -) - -// WorkTriage are programs for which we noticed potential new coverage during -// first execution. But we are not sure yet if the coverage is real or not. -// During triage we understand if these programs in fact give new coverage, -// and if yes, minimize them and add to corpus. -type WorkTriage struct { - p *prog.Prog - call int - info ipc.CallInfo - flags ProgTypes -} - -// WorkCandidate are programs from hub. -// We don't know yet if they are useful for this fuzzer or not. -// A proc handles them the same way as locally generated/mutated programs. -type WorkCandidate struct { - p *prog.Prog - flags ProgTypes -} - -// WorkSmash are programs just added to corpus. -// During smashing these programs receive a one-time special attention -// (emit faults, collect comparison hints, etc). -type WorkSmash struct { - p *prog.Prog - call int -} - -func newWorkQueue(procs int, needCandidates chan struct{}) *WorkQueue { - return &WorkQueue{ - procs: procs, - needCandidates: needCandidates, - } -} - -func (wq *WorkQueue) enqueue(item interface{}) { - wq.mu.Lock() - defer wq.mu.Unlock() - switch item := item.(type) { - case *WorkTriage: - if item.flags&ProgCandidate != 0 { - wq.triageCandidate = append(wq.triageCandidate, item) - } else { - wq.triage = append(wq.triage, item) - } - case *WorkCandidate: - wq.candidate = append(wq.candidate, item) - case *WorkSmash: - wq.smash = append(wq.smash, item) - default: - panic("unknown work type") - } -} - -func (wq *WorkQueue) dequeue() (item interface{}) { - wq.mu.RLock() - if len(wq.triageCandidate)+len(wq.candidate)+len(wq.triage)+len(wq.smash) == 0 { - wq.mu.RUnlock() - return nil - } - wq.mu.RUnlock() - wq.mu.Lock() - wantCandidates := false - if len(wq.triageCandidate) != 0 { - last := len(wq.triageCandidate) - 1 - item = wq.triageCandidate[last] - wq.triageCandidate = wq.triageCandidate[:last] - } else if len(wq.candidate) != 0 { - last := len(wq.candidate) - 1 - item = wq.candidate[last] - wq.candidate = wq.candidate[:last] - wantCandidates = len(wq.candidate) < wq.procs - } else if len(wq.triage) != 0 { - last := len(wq.triage) - 1 - item = wq.triage[last] - wq.triage = wq.triage[:last] - } else if len(wq.smash) != 0 { - last := len(wq.smash) - 1 - item = wq.smash[last] - wq.smash = wq.smash[:last] - } - wq.mu.Unlock() - if wantCandidates { - select { - case wq.needCandidates <- struct{}{}: - default: - } - } - return item -} - -func (wq *WorkQueue) wantCandidates() bool { - wq.mu.RLock() - defer wq.mu.RUnlock() - return len(wq.candidate) < wq.procs -} |
