aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/fuzzer/fuzzer.go
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2024-05-03 13:12:00 +0200
committerDmitry Vyukov <dvyukov@google.com>2024-05-16 15:38:27 +0000
commit03820adaef911ce08278d95f034f134c3c0c852e (patch)
tree57f87ce0f3dedda459fb1771d3b79ff96e0853bb /pkg/fuzzer/fuzzer.go
parentef5d53ed7e3c7d30481a88301f680e37a5cc4775 (diff)
pkg/fuzzer: use queue layers
Instead of relying on a fuzzer-internal priority queue, utilize stackable layers of request-generating steps. Move the functionality to a separate pkg/fuzzer/queue package. The pkg/fuzzer/queue package can be reused to add extra processing layers on top of the fuzzing and to combine machine checking and fuzzing execution pipelines.
Diffstat (limited to 'pkg/fuzzer/fuzzer.go')
-rw-r--r--pkg/fuzzer/fuzzer.go243
1 files changed, 118 insertions, 125 deletions
diff --git a/pkg/fuzzer/fuzzer.go b/pkg/fuzzer/fuzzer.go
index 7c8b63bab..9d8957922 100644
--- a/pkg/fuzzer/fuzzer.go
+++ b/pkg/fuzzer/fuzzer.go
@@ -9,12 +9,11 @@ import (
"math/rand"
"runtime"
"sync"
- "sync/atomic"
"time"
"github.com/google/syzkaller/pkg/corpus"
+ "github.com/google/syzkaller/pkg/fuzzer/queue"
"github.com/google/syzkaller/pkg/ipc"
- "github.com/google/syzkaller/pkg/signal"
"github.com/google/syzkaller/pkg/stats"
"github.com/google/syzkaller/prog"
)
@@ -34,8 +33,7 @@ type Fuzzer struct {
ctMu sync.Mutex // TODO: use RWLock.
ctRegenerate chan struct{}
- nextExec *priorityQueue[*Request]
- nextJobID atomic.Int64
+ execQueues
}
func NewFuzzer(ctx context.Context, cfg *Config, rnd *rand.Rand,
@@ -57,9 +55,8 @@ func NewFuzzer(ctx context.Context, cfg *Config, rnd *rand.Rand,
// 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](),
}
+ f.execQueues = newExecQueues(f)
f.updateChoiceTable(nil)
go f.choiceTableUpdater()
if cfg.Debug {
@@ -68,67 +65,105 @@ func NewFuzzer(ctx context.Context, cfg *Config, rnd *rand.Rand,
return f
}
-type Config struct {
- Debug bool
- Corpus *corpus.Corpus
- 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
- FetchRawCover bool
- NewInputFilter func(call string) bool
+type execQueues struct {
+ smashQueue *queue.PlainQueue
+ triageQueue *queue.PriorityQueue
+ candidateQueue *queue.PlainQueue
+ triageCandidateQueue *queue.PriorityQueue
+ source queue.Source
}
-type Request struct {
- Prog *prog.Prog
- NeedSignal SignalType
- NeedCover bool
- NeedHints bool
- // If specified, the resulting signal for call SignalFilterCall
- // will include subset of it even if it's not new.
- SignalFilter signal.Signal
- SignalFilterCall int
- // Fields that are only relevant within pkg/fuzzer.
- flags ProgTypes
- stat *stats.Val
- resultC chan *Result
+func newExecQueues(fuzzer *Fuzzer) execQueues {
+ ret := execQueues{
+ triageCandidateQueue: queue.Priority(),
+ candidateQueue: queue.PlainWithStat(fuzzer.StatCandidates),
+ triageQueue: queue.Priority(),
+ smashQueue: queue.Plain(),
+ }
+ // Sources are listed in the order, in which they will be polled.
+ ret.source = queue.Order(
+ ret.triageCandidateQueue,
+ ret.candidateQueue,
+ ret.triageQueue,
+ // Alternate smash jobs with exec/fuzz once in 3 times.
+ queue.Alternate(ret.smashQueue, 3),
+ queue.Callback(fuzzer.genFuzz),
+ )
+ return ret
}
-type SignalType int
+type execOpt any
+type dontTriage struct{}
+type progFlags ProgTypes
-const (
- NoSignal SignalType = iota // we don't need any signal
- NewSignal // we need the newly seen signal
- AllSignal // we need all signal
-)
+func (fuzzer *Fuzzer) validateRequest(req *queue.Request) {
+ if req.NeedHints && (req.NeedCover || req.NeedSignal != queue.NoSignal) {
+ panic("Request.NeedHints is mutually exclusive with other fields")
+ }
+ if req.SignalFilter != nil && req.NeedSignal != queue.NewSignal {
+ panic("SignalFilter must be used with NewSignal")
+ }
+}
-type Result struct {
- Info *ipc.ProgInfo
- Stop bool
+func (fuzzer *Fuzzer) execute(executor queue.Executor, req *queue.Request, opts ...execOpt) *queue.Result {
+ fuzzer.validateRequest(req)
+ executor.Submit(req)
+ res := req.Wait(fuzzer.ctx)
+ fuzzer.processResult(req, res, opts...)
+ return res
}
-func (fuzzer *Fuzzer) Done(req *Request, res *Result) {
+func (fuzzer *Fuzzer) prepare(req *queue.Request, opts ...execOpt) {
+ fuzzer.validateRequest(req)
+ req.OnDone(func(req *queue.Request, res *queue.Result) bool {
+ fuzzer.processResult(req, res, opts...)
+ return true
+ })
+}
+
+func (fuzzer *Fuzzer) enqueue(executor queue.Executor, req *queue.Request, opts ...execOpt) {
+ fuzzer.prepare(req, opts...)
+ executor.Submit(req)
+}
+
+func (fuzzer *Fuzzer) processResult(req *queue.Request, res *queue.Result, opts ...execOpt) {
+ var flags ProgTypes
+ var noTriage bool
+ for _, opt := range opts {
+ switch v := opt.(type) {
+ case progFlags:
+ flags = ProgTypes(v)
+ case dontTriage:
+ noTriage = true
+ }
+ }
// Triage individual calls.
// We do it before unblocking the waiting threads because
// it may result it concurrent modification of req.Prog.
// If we are already triaging this exact prog, this is flaky coverage.
- if req.NeedSignal != NoSignal && res.Info != nil && req.flags&progInTriage == 0 {
+ if req.NeedSignal != queue.NoSignal && res.Info != nil && !noTriage {
for call, info := range res.Info.Calls {
- fuzzer.triageProgCall(req.Prog, &info, call, req.flags)
+ fuzzer.triageProgCall(req.Prog, &info, call, flags)
}
- fuzzer.triageProgCall(req.Prog, &res.Info.Extra, -1, req.flags)
- }
- // Unblock threads that wait for the result.
- if req.resultC != nil {
- req.resultC <- res
+ fuzzer.triageProgCall(req.Prog, &res.Info.Extra, -1, flags)
}
if res.Info != nil {
fuzzer.statExecTime.Add(int(res.Info.Elapsed.Milliseconds()))
}
- req.stat.Add(1)
+}
+
+type Config struct {
+ Debug bool
+ Corpus *corpus.Corpus
+ 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
+ FetchRawCover bool
+ NewInputFilter func(call string) bool
}
func (fuzzer *Fuzzer) triageProgCall(p *prog.Prog, info *ipc.CallInfo, call int, flags ProgTypes) {
@@ -141,13 +176,18 @@ func (fuzzer *Fuzzer) triageProgCall(p *prog.Prog, info *ipc.CallInfo, call int,
return
}
fuzzer.Logf(2, "found new signal in call %d in %s", call, p)
+
+ queue := fuzzer.triageQueue
+ if flags&progCandidate > 0 {
+ queue = fuzzer.triageCandidateQueue
+ }
fuzzer.startJob(fuzzer.statJobsTriage, &triageJob{
- p: p.Clone(),
- call: call,
- info: *info,
- newSignal: newMaxSignal,
- flags: flags,
- jobPriority: triageJobPrio(flags),
+ p: p.Clone(),
+ call: call,
+ info: *info,
+ newSignal: newMaxSignal,
+ flags: flags,
+ queue: queue.AppendQueue(),
})
}
@@ -164,33 +204,7 @@ func signalPrio(p *prog.Prog, info *ipc.CallInfo, call int) (prio uint8) {
return
}
-type Candidate struct {
- Prog *prog.Prog
- Smashed bool
- Minimized bool
-}
-
-func (fuzzer *Fuzzer) NextInput() *Request {
- req := fuzzer.nextInput()
- if req.stat == fuzzer.statExecCandidate {
- fuzzer.StatCandidates.Add(-1)
- }
- return req
-}
-
-func (fuzzer *Fuzzer) nextInput() *Request {
- nextExec := fuzzer.nextExec.tryPop()
-
- // The fuzzer may become too interested in potentially very long hint and smash jobs.
- // Let's leave more space for new input space exploration.
- if nextExec != nil {
- if nextExec.prio.greaterThan(priority{smashPrio}) || fuzzer.nextRand()%3 != 0 {
- return nextExec.value
- } else {
- fuzzer.nextExec.push(nextExec)
- }
- }
-
+func (fuzzer *Fuzzer) genFuzz() *queue.Request {
// Either generate a new input or mutate an existing one.
mutateRate := 0.95
if !fuzzer.Config.Coverage {
@@ -198,23 +212,20 @@ func (fuzzer *Fuzzer) nextInput() *Request {
// more frequently because fallback signal is weak.
mutateRate = 0.5
}
+ var req *queue.Request
rnd := fuzzer.rand()
if rnd.Float64() < mutateRate {
- req := mutateProgRequest(fuzzer, rnd)
- if req != nil {
- return req
- }
+ req = mutateProgRequest(fuzzer, rnd)
+ }
+ if req == nil {
+ req = genProgRequest(fuzzer, rnd)
}
- return genProgRequest(fuzzer, rnd)
+ fuzzer.prepare(req)
+ return req
}
func (fuzzer *Fuzzer) startJob(stat *stats.Val, newJob job) {
fuzzer.Logf(2, "started %T", newJob)
- if impl, ok := newJob.(jobSaveID); ok {
- // E.g. for big and slow hint jobs, we would prefer not to serialize them,
- // but rather to start them all in parallel.
- impl.saveID(-fuzzer.nextJobID.Add(1))
- }
go func() {
stat.Add(1)
fuzzer.statJobs.Add(1)
@@ -224,6 +235,10 @@ func (fuzzer *Fuzzer) startJob(stat *stats.Val, newJob job) {
}()
}
+func (fuzzer *Fuzzer) Next() *queue.Request {
+ return fuzzer.source.Next()
+}
+
func (fuzzer *Fuzzer) Logf(level int, msg string, args ...interface{}) {
if fuzzer.Config.Logf == nil {
return
@@ -231,45 +246,23 @@ func (fuzzer *Fuzzer) Logf(level int, msg string, args ...interface{}) {
fuzzer.Config.Logf(level, msg, args...)
}
+type Candidate struct {
+ Prog *prog.Prog
+ Smashed bool
+ Minimized bool
+}
+
func (fuzzer *Fuzzer) AddCandidates(candidates []Candidate) {
- fuzzer.StatCandidates.Add(len(candidates))
for _, candidate := range candidates {
- fuzzer.pushExec(candidateRequest(fuzzer, candidate), priority{candidatePrio})
+ req, flags := candidateRequest(fuzzer, candidate)
+ fuzzer.enqueue(fuzzer.candidateQueue, req, progFlags(flags))
}
}
func (fuzzer *Fuzzer) rand() *rand.Rand {
- return rand.New(rand.NewSource(fuzzer.nextRand()))
-}
-
-func (fuzzer *Fuzzer) nextRand() int64 {
fuzzer.mu.Lock()
defer fuzzer.mu.Unlock()
- return fuzzer.rnd.Int63()
-}
-
-func (fuzzer *Fuzzer) pushExec(req *Request, prio priority) {
- if req.NeedHints && (req.NeedCover || req.NeedSignal != NoSignal) {
- panic("Request.NeedHints is mutually exclusive with other fields")
- }
- if req.SignalFilter != nil && req.NeedSignal != NewSignal {
- panic("SignalFilter must be used with NewSignal")
- }
- 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
- }
+ return rand.New(rand.NewSource(fuzzer.rnd.Int63()))
}
func (fuzzer *Fuzzer) updateChoiceTable(programs []*prog.Prog) {
@@ -327,8 +320,8 @@ func (fuzzer *Fuzzer) logCurrentStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
- str := fmt.Sprintf("exec queue size: %d, running jobs: %d, heap (MB): %d",
- fuzzer.nextExec.Len(), fuzzer.statJobs.Val(), m.Alloc/1000/1000)
+ str := fmt.Sprintf("running jobs: %d, heap (MB): %d",
+ fuzzer.statJobs.Val(), m.Alloc/1000/1000)
fuzzer.Logf(0, "%s", str)
}
}