/
fuzzer.go
203 lines (170 loc) · 5.78 KB
/
fuzzer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// Copyright (c) 2020-2021 C4 Project
//
// This file is part of c4t.
// Licenced under the MIT licence; see `LICENSE`.
// Package fuzzer contains a test-plan batch fuzzer.
// It relies on the existence of a single-file fuzzer such as c4f-fuzz.
package fuzzer
import (
"context"
"fmt"
"math/rand"
"github.com/c4-project/c4t/internal/model/service/fuzzer"
"github.com/c4-project/c4t/internal/machine"
"github.com/c4-project/c4t/internal/quantity"
"github.com/c4-project/c4t/internal/plan/stage"
"github.com/c4-project/c4t/internal/subject/corpus/builder"
"github.com/c4-project/c4t/internal/subject/corpus"
"github.com/c4-project/c4t/internal/subject"
"github.com/c4-project/c4t/internal/helper/iohelp"
"github.com/c4-project/c4t/internal/plan"
)
// DefaultSubjectFuzzes is the default number of fuzz cycles to run per subject.
const DefaultSubjectFuzzes = 10
// SubjectPather is the interface of things that serve file-paths for subject outputs during a fuzz batch.
type SubjectPather interface {
// Prepare sets up the directories ready to serve through SubjectPaths.
Prepare() error
// SubjectLitmus gets the litmus filepath for the subject/cycle pair sc.
SubjectLitmus(sc SubjectCycle) string
// SubjectTrace gets the trace filepath for the subject/cycle pair sc.
SubjectTrace(sc SubjectCycle) string
}
//go:generate mockery --name=SubjectPather
// Fuzzer holds the state required for the fuzzing stage of the tester.
type Fuzzer struct {
// driver holds the fuzzer's low-level implementation structs.
driver Driver
// observers observe the fuzzer's progress across a corpus.
observers []builder.Observer
// paths contains the path set for things generated by this fuzzer.
paths SubjectPather
// quantities sets the quantities for this batch fuzzer run.
quantities quantity.FuzzSet
// config sets various options on the fuzzer itself.
config *fuzzer.Config
}
// New constructs a fuzzer with the config c and plan p.
func New(d Driver, ps SubjectPather, o ...Option) (*Fuzzer, error) {
f := Fuzzer{
driver: d,
paths: ps,
quantities: quantity.FuzzSet{
SubjectCycles: DefaultSubjectFuzzes,
},
}
if err := Options(o...)(&f); err != nil {
return nil, err
}
return &f, f.check()
}
// check makes sure various parts of this fuzzer's config are present.
func (f *Fuzzer) check() error {
if f.driver == nil {
return ErrDriverNil
}
if f.paths == nil {
return iohelp.ErrPathsetNil
}
if f.quantities.SubjectCycles <= 0 {
return fmt.Errorf("%w: non-positive subject cycle amount", corpus.ErrSmall)
}
return nil
}
// Stage gets the appropriate stage information for the fuzzer.
func (*Fuzzer) Stage() stage.Stage {
return stage.Fuzz
}
// Close does nothing.
func (*Fuzzer) Close() error {
return nil
}
// Run runs the fuzzer with context ctx and plan p.
func (f *Fuzzer) Run(ctx context.Context, p *plan.Plan) (*plan.Plan, error) {
if err := f.checkPlan(p); err != nil {
return nil, err
}
if err := f.paths.Prepare(); err != nil {
return nil, err
}
rng := p.Metadata.Rand()
fcs, ferr := f.fuzzCorpus(ctx, rng, p.Corpus, p.Machine.Machine)
if ferr != nil {
return nil, ferr
}
return f.sampleAndUpdatePlan(fcs, rng, *p)
}
// sampleAndUpdatePlan samples fcs, updates the fresh plan copy p with it, and returns a pointer to it.
func (f *Fuzzer) sampleAndUpdatePlan(fcs corpus.Corpus, rng *rand.Rand, p plan.Plan) (*plan.Plan, error) {
// TODO(@MattWindsor91): add some observer calls here?
scs, err := fcs.Sample(rng, f.quantities.CorpusSize)
if err != nil {
return nil, err
}
p.Corpus = scs
// Previously, we reset the plan creation date and seed here. This seems a little arbitrary in hindsight,
// so we no longer do so.
return &p, nil
}
// count counts the number of subjects and individual fuzz runs to expect from this fuzzer.
func (f *Fuzzer) count(c corpus.Corpus) (nsubjects, nruns int) {
nsubjects = len(c)
nruns = f.quantities.SubjectCycles * nsubjects
return nsubjects, nruns
}
// fuzzCorpus actually does the business of fuzzing.
func (f *Fuzzer) fuzzCorpus(ctx context.Context, rng *rand.Rand, c corpus.Corpus, m machine.Machine) (corpus.Corpus, error) {
_, nfuzzes := f.count(c)
seeds := corpusSeeds(rng, c)
mf := builder.Manifest{Name: "fuzz", NReqs: nfuzzes}
bc := builder.Config{Manifest: mf, Observers: f.observers}
return builder.ParBuild(ctx, f.quantities.NWorkers, c, bc, func(ctx context.Context, s subject.Named, ch chan<- builder.Request) error {
return f.makeInstance(s, seeds[s.Name], m, ch).Fuzz(ctx)
})
}
// corpusSeeds generates a seed for each subject in c using rng.
//
// This is necessary to avoid a race on the random number generator inside the parallel builder.
func corpusSeeds(rng *rand.Rand, c corpus.Corpus) map[string]int64 {
seeds := make(map[string]int64)
for n := range c {
seeds[n] = rng.Int63()
}
return seeds
}
func (f *Fuzzer) makeInstance(s subject.Named, seed int64, m machine.Machine, resCh chan<- builder.Request) *Instance {
return &Instance{
Config: f.config,
Driver: f.driver,
Subject: s,
SubjectCycles: f.quantities.SubjectCycles,
Pathset: f.paths,
Rng: rand.New(rand.NewSource(seed)),
ResCh: resCh,
Machine: &m,
}
}
func (f *Fuzzer) checkPlan(p *plan.Plan) error {
if p == nil {
return plan.ErrNil
}
if err := p.Check(); err != nil {
return err
}
if err := p.Metadata.RequireStage(stage.Plan); err != nil {
return err
}
return f.checkCount(p.Corpus)
}
func (f *Fuzzer) checkCount(c corpus.Corpus) error {
nsubjects, nruns := f.count(c)
if nsubjects <= 0 {
return corpus.ErrNone
}
// Note that this inequality 'does the right thing' when corpus size = 0, ie no corpus size requirement.
csize := f.quantities.CorpusSize
if nruns < csize {
return fmt.Errorf("%w: projected corpus size %d, want %d", corpus.ErrSmall, nruns, csize)
}
return nil
}