From 409ee912f2c4f07e3064b4e6f4a83e1f812531d8 Mon Sep 17 00:00:00 2001 From: Aleksandr Nogikh Date: Fri, 15 Mar 2024 19:53:15 +0100 Subject: all: move fuzzer to the host Instead of doing fuzzing in parallel in running VM, make all decisions in the host syz-manager process. Instantiate and keep a fuzzer.Fuzzer object in syz-manager and update the RPC between syz-manager and syz-fuzzer to exchange exact programs to execute and their resulting signal and coverage. To optimize the networking traffic, exchange mostly only the difference between the known max signal and the detected signal. --- pkg/fuzzer/cover.go | 6 ++++ pkg/fuzzer/fuzzer.go | 77 +++++++++++++++++------------------------------ pkg/fuzzer/fuzzer_test.go | 7 ++--- pkg/fuzzer/job.go | 29 +++++++++--------- pkg/fuzzer/stats.go | 26 ++++++++++++++-- 5 files changed, 73 insertions(+), 72 deletions(-) (limited to 'pkg/fuzzer') diff --git a/pkg/fuzzer/cover.go b/pkg/fuzzer/cover.go index 886da004d..5af00f167 100644 --- a/pkg/fuzzer/cover.go +++ b/pkg/fuzzer/cover.go @@ -35,6 +35,12 @@ func (cover *Cover) addRawMaxSignal(signal []uint32, prio uint8) signal.Signal { return diff } +func (cover *Cover) CopyMaxSignal() signal.Signal { + cover.mu.RLock() + defer cover.mu.RUnlock() + return cover.maxSignal.Copy() +} + func (cover *Cover) GrabNewSignal() signal.Signal { cover.mu.Lock() defer cover.mu.Unlock() diff --git a/pkg/fuzzer/fuzzer.go b/pkg/fuzzer/fuzzer.go index 196e59506..14c3f5902 100644 --- a/pkg/fuzzer/fuzzer.go +++ b/pkg/fuzzer/fuzzer.go @@ -13,15 +13,15 @@ import ( "time" "github.com/google/syzkaller/pkg/corpus" - "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" ) type Fuzzer struct { - Config *Config - Cover *Cover - NeedCandidates chan struct{} + Config *Config + Cover *Cover ctx context.Context mu sync.Mutex @@ -39,18 +39,16 @@ type Fuzzer struct { runningJobs 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 + + outOfQueue atomic.Bool + outOfQueueNext atomic.Int64 } func NewFuzzer(ctx context.Context, cfg *Config, rnd *rand.Rand, target *prog.Target) *Fuzzer { f := &Fuzzer{ - Config: cfg, - Cover: &Cover{}, - NeedCandidates: make(chan struct{}, 1), + Config: cfg, + Cover: &Cover{}, ctx: ctx, stats: map[string]uint64{}, @@ -82,18 +80,16 @@ type Config struct { EnabledCalls map[*prog.Syscall]bool NoMutateCalls map[int]bool FetchRawCover bool - // If the number of queued candidates is less than MinCandidates, - // NeedCandidates is triggered. - MinCandidates uint - NewInputs chan corpus.NewInput + NewInputFilter func(input *corpus.NewInput) bool } type Request struct { Prog *prog.Prog NeedCover bool NeedRawCover bool - NeedSignal bool + NeedSignal rpctype.SignalType NeedHints bool + SignalFilter signal.Signal // If specified, the resulting signal MAY be a subset of it. // Fields that are only relevant within pkg/fuzzer. flags ProgTypes stat string @@ -110,7 +106,7 @@ 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 { + if req.NeedSignal != rpctype.NoSignal && res.Info != nil { for call, info := range res.Info.Calls { fuzzer.triageProgCall(req.Prog, &info, call, req.flags) } @@ -166,7 +162,6 @@ func signalPrio(p *prog.Prog, info *ipc.CallInfo, call int) (prio uint8) { type Candidate struct { Prog *prog.Prog - Hash hash.Sig Smashed bool Minimized bool } @@ -178,20 +173,19 @@ func (fuzzer *Fuzzer) NextInput() *Request { 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 + // The fuzzer may get biased to one specific part of the kernel. + // Periodically generate random programs to ensure that the coverage + // is more uniform. + if !fuzzer.outOfQueue.Load() || + fuzzer.outOfQueueNext.Add(1)%400 > 0 { + nextExec := fuzzer.nextExec.tryPop() + if nextExec != nil { + return nextExec.value + } } // Either generate a new input or mutate an existing one. mutateRate := 0.95 @@ -227,16 +221,15 @@ func (fuzzer *Fuzzer) Logf(level int, msg string, args ...interface{}) { 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) EnableOutOfQueue() { + fuzzer.outOfQueue.Store(true) } func (fuzzer *Fuzzer) rand() *rand.Rand { @@ -250,7 +243,7 @@ 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) { + if req.NeedHints && (req.NeedCover || req.NeedSignal != rpctype.NoSignal) { panic("Request.NeedHints is mutually exclusive with other fields") } fuzzer.nextExec.push(&priorityQueueItem[*Request]{ @@ -330,19 +323,3 @@ func (fuzzer *Fuzzer) logCurrentStats() { fuzzer.Logf(0, "%s", str) } } - -type Stats struct { - CoverStats - corpus.Stats - Candidates int - RunningJobs int -} - -func (fuzzer *Fuzzer) Stats() Stats { - return Stats{ - CoverStats: fuzzer.Cover.Stats(), - Stats: fuzzer.Config.Corpus.Stats(), - Candidates: int(fuzzer.queuedCandidates.Load()), - RunningJobs: int(fuzzer.runningJobs.Load()), - } -} diff --git a/pkg/fuzzer/fuzzer_test.go b/pkg/fuzzer/fuzzer_test.go index 4f8cf41c5..bd6d9a8fe 100644 --- a/pkg/fuzzer/fuzzer_test.go +++ b/pkg/fuzzer/fuzzer_test.go @@ -22,6 +22,7 @@ import ( "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" @@ -54,7 +55,6 @@ func TestFuzz(t *testing.T) { EnabledCalls: map[*prog.Syscall]bool{ target.SyscallMap["syz_test_fuzzer1"]: true, }, - NewInputs: make(chan corpus.NewInput), }, rand.New(testutil.RandSource(t)), target) go func() { @@ -129,7 +129,7 @@ func emulateExec(req *Request) (*Result, string, error) { if req.NeedCover { callInfo.Cover = []uint32{cover} } - if req.NeedSignal { + if req.NeedSignal != rpctype.NoSignal { callInfo.Signal = []uint32{cover} } info.Calls = append(info.Calls, callInfo) @@ -205,7 +205,6 @@ func (f *testFuzzer) wait() { 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, @@ -239,7 +238,7 @@ 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 { + if req.NeedSignal != rpctype.NoSignal { execOpts.Flags |= ipc.FlagCollectSignal } if req.NeedCover { diff --git a/pkg/fuzzer/job.go b/pkg/fuzzer/job.go index dfc0b807e..f567fc2cc 100644 --- a/pkg/fuzzer/job.go +++ b/pkg/fuzzer/job.go @@ -10,6 +10,7 @@ import ( "github.com/google/syzkaller/pkg/corpus" "github.com/google/syzkaller/pkg/cover" "github.com/google/syzkaller/pkg/ipc" + "github.com/google/syzkaller/pkg/rpctype" "github.com/google/syzkaller/pkg/signal" "github.com/google/syzkaller/prog" ) @@ -64,7 +65,7 @@ func genProgRequest(fuzzer *Fuzzer, rnd *rand.Rand) *Request { fuzzer.ChoiceTable()) return &Request{ Prog: p, - NeedSignal: true, + NeedSignal: rpctype.NewSignal, stat: statGenerate, } } @@ -83,7 +84,7 @@ func mutateProgRequest(fuzzer *Fuzzer, rnd *rand.Rand) *Request { ) return &Request{ Prog: newP, - NeedSignal: true, + NeedSignal: rpctype.NewSignal, stat: statFuzz, } } @@ -98,7 +99,7 @@ func candidateRequest(input Candidate) *Request { } return &Request{ Prog: input.Prog, - NeedSignal: true, + NeedSignal: rpctype.NewSignal, stat: statCandidate, flags: flags, } @@ -157,13 +158,10 @@ func (job *triageJob) run(fuzzer *Fuzzer) { Cover: info.cover.Serialize(), RawCover: info.rawCover, } - fuzzer.Config.Corpus.Save(input) - if fuzzer.Config.NewInputs != nil { - select { - case <-fuzzer.ctx.Done(): - case fuzzer.Config.NewInputs <- input: - } + if filter := fuzzer.Config.NewInputFilter; filter != nil && !filter(&input) { + return } + fuzzer.Config.Corpus.Save(input) } type deflakedCover struct { @@ -179,7 +177,7 @@ func (job *triageJob) deflake(fuzzer *Fuzzer) (info deflakedCover, stop bool) { for i := 0; i < signalRuns; i++ { result := fuzzer.exec(job, &Request{ Prog: job.p, - NeedSignal: true, + NeedSignal: rpctype.AllSignal, NeedCover: true, NeedRawCover: fuzzer.Config.FetchRawCover, stat: statTriage, @@ -226,9 +224,10 @@ func (job *triageJob) minimize(fuzzer *Fuzzer, newSignal signal.Signal) (stop bo } for i := 0; i < minimizeAttempts; i++ { result := fuzzer.exec(job, &Request{ - Prog: p1, - NeedSignal: true, - stat: statMinimize, + Prog: p1, + NeedSignal: rpctype.AllSignal, + SignalFilter: newSignal, + stat: statMinimize, }) if result.Stop { stop = true @@ -298,7 +297,7 @@ func (job *smashJob) run(fuzzer *Fuzzer) { fuzzer.Config.Corpus.Programs()) result := fuzzer.exec(job, &Request{ Prog: p, - NeedSignal: true, + NeedSignal: rpctype.NewSignal, stat: statSmash, }) if result.Stop { @@ -386,7 +385,7 @@ func (job *hintsJob) run(fuzzer *Fuzzer) { func(p *prog.Prog) bool { result := fuzzer.exec(job, &Request{ Prog: p, - NeedSignal: true, + NeedSignal: rpctype.NewSignal, stat: statHint, }) return !result.Stop diff --git a/pkg/fuzzer/stats.go b/pkg/fuzzer/stats.go index 17bc6131c..044febc64 100644 --- a/pkg/fuzzer/stats.go +++ b/pkg/fuzzer/stats.go @@ -3,6 +3,8 @@ package fuzzer +import "github.com/google/syzkaller/pkg/corpus" + const ( statGenerate = "exec gen" statFuzz = "exec fuzz" @@ -17,10 +19,28 @@ const ( statBufferTooSmall = "buffer too small" ) -func (fuzzer *Fuzzer) GrabStats() map[string]uint64 { +type Stats struct { + CoverStats + corpus.Stats + Candidates int + RunningJobs int + // Let's keep stats in Named as long as the rest of the code does not depend + // on their specific values. + Named map[string]uint64 +} + +func (fuzzer *Fuzzer) Stats() Stats { + ret := Stats{ + CoverStats: fuzzer.Cover.Stats(), + Stats: fuzzer.Config.Corpus.Stats(), + Candidates: int(fuzzer.queuedCandidates.Load()), + RunningJobs: int(fuzzer.runningJobs.Load()), + Named: make(map[string]uint64), + } fuzzer.mu.Lock() defer fuzzer.mu.Unlock() - ret := fuzzer.stats - fuzzer.stats = map[string]uint64{} + for k, v := range fuzzer.stats { + ret.Named[k] = v + } return ret } -- cgit mrf-deployment