aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/fuzzer
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2024-06-04 12:55:41 +0200
committerDmitry Vyukov <dvyukov@google.com>2024-06-24 09:57:34 +0000
commite16e2c9a4cb6937323e861b646792a6c4c978a3c (patch)
tree6c513e98e5f465b44a98546d8984485d2c128582 /pkg/fuzzer
parent90d67044dab68568e8f35bc14b68055dbd166eff (diff)
executor: add runner mode
Move all syz-fuzzer logic into syz-executor and remove syz-fuzzer. Also restore syz-runtest functionality in the manager. Update #4917 (sets most signal handlers to SIG_IGN)
Diffstat (limited to 'pkg/fuzzer')
-rw-r--r--pkg/fuzzer/fuzzer.go2
-rw-r--r--pkg/fuzzer/fuzzer_test.go176
-rw-r--r--pkg/fuzzer/job.go23
-rw-r--r--pkg/fuzzer/queue/queue.go27
4 files changed, 113 insertions, 115 deletions
diff --git a/pkg/fuzzer/fuzzer.go b/pkg/fuzzer/fuzzer.go
index 92d8b0f8d..5b95f4eec 100644
--- a/pkg/fuzzer/fuzzer.go
+++ b/pkg/fuzzer/fuzzer.go
@@ -166,7 +166,6 @@ func (fuzzer *Fuzzer) processResult(req *queue.Request, res *queue.Result, flags
type Config struct {
Debug bool
Corpus *corpus.Corpus
- BaseOpts flatrpc.ExecOpts // Fuzzer will use BaseOpts as a base for all requests.
Logf func(level int, msg string, args ...interface{})
Coverage bool
FaultInjection bool
@@ -251,7 +250,6 @@ func (fuzzer *Fuzzer) Next() *queue.Request {
// The fuzzer is not supposed to issue nil requests.
panic("nil request from the fuzzer")
}
- req.ExecOpts = fuzzer.Config.BaseOpts.MergeFlags(req.ExecOpts)
return req
}
diff --git a/pkg/fuzzer/fuzzer_test.go b/pkg/fuzzer/fuzzer_test.go
index 206469fda..55ec09666 100644
--- a/pkg/fuzzer/fuzzer_test.go
+++ b/pkg/fuzzer/fuzzer_test.go
@@ -13,6 +13,7 @@ import (
"runtime"
"strings"
"sync"
+ "sync/atomic"
"testing"
"time"
@@ -20,14 +21,13 @@ import (
"github.com/google/syzkaller/pkg/csource"
"github.com/google/syzkaller/pkg/flatrpc"
"github.com/google/syzkaller/pkg/fuzzer/queue"
- "github.com/google/syzkaller/pkg/ipc"
- "github.com/google/syzkaller/pkg/ipc/ipcconfig"
+ "github.com/google/syzkaller/pkg/rpcserver"
"github.com/google/syzkaller/pkg/signal"
"github.com/google/syzkaller/pkg/testutil"
+ "github.com/google/syzkaller/pkg/vminfo"
"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) {
@@ -42,15 +42,14 @@ func TestFuzz(t *testing.T) {
t.Skipf("skipping, broken cross-compiler: %v", sysTarget.BrokenCompiler)
}
executor := csource.BuildExecutor(t, target, "../..", "-fsanitize-coverage=trace-pc", "-g")
+
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- _, opts, _ := ipcconfig.Default(target)
corpusUpdates := make(chan corpus.NewItemEvent)
fuzzer := NewFuzzer(ctx, &Config{
- Debug: true,
- BaseOpts: *opts,
- Corpus: corpus.NewMonitoredCorpus(ctx, corpusUpdates),
+ Debug: true,
+ Corpus: corpus.NewMonitoredCorpus(ctx, corpusUpdates),
Logf: func(level int, msg string, args ...interface{}) {
if level > 1 {
return
@@ -74,24 +73,24 @@ func TestFuzz(t *testing.T) {
}
}()
- 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 := &testFuzzer{
+ t: t,
+ target: target,
+ fuzzer: fuzzer,
+ executor: executor,
+ iterLimit: 10000,
+ expectedCrashes: map[string]bool{
+ "first bug": true,
+ "second bug": true,
+ },
}
- tf.wait()
+ tf.run()
t.Logf("resulting corpus:")
for _, p := range fuzzer.Config.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) {
@@ -204,114 +203,87 @@ func emulateExec(req *queue.Request) (*queue.Result, string, error) {
type testFuzzer struct {
t testing.TB
- eg errgroup.Group
+ target *prog.Target
fuzzer *Fuzzer
+ executor string
mu sync.Mutex
crashes map[string]int
expectedCrashes map[string]bool
iter int
iterLimit int
+ done func()
+ finished atomic.Bool
+}
+
+func (f *testFuzzer) run() {
+ f.crashes = make(map[string]int)
+ ctx, done := context.WithCancel(context.Background())
+ f.done = done
+ cfg := &rpcserver.LocalConfig{
+ Config: rpcserver.Config{
+ Config: vminfo.Config{
+ Target: f.target,
+ Features: flatrpc.FeatureSandboxNone,
+ Sandbox: flatrpc.ExecEnvSandboxNone,
+ },
+ Procs: 4,
+ Slowdown: 1,
+ },
+ Executor: f.executor,
+ Dir: f.t.TempDir(),
+ Context: ctx,
+ }
+ cfg.MachineChecked = func(features flatrpc.Feature, syscalls map[*prog.Syscall]bool) queue.Source {
+ cfg.Cover = true
+ return f
+ }
+ if err := rpcserver.RunLocal(cfg); err != nil {
+ f.t.Fatal(err)
+ }
+ assert.Equal(f.t, len(f.expectedCrashes), len(f.crashes), "not all expected crashes were found")
}
-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) Next() *queue.Request {
+ if f.finished.Load() {
+ return nil
}
+ req := f.fuzzer.Next()
+ req.ExecOpts.EnvFlags |= flatrpc.ExecEnvSignal | flatrpc.ExecEnvSandboxNone
+ req.ReturnOutput = true
+ req.ReturnError = true
+ req.OnDone(f.OnDone)
+ return req
}
-func (f *testFuzzer) oneMore() bool {
+func (f *testFuzzer) OnDone(req *queue.Request, res *queue.Result) bool {
+ // TODO: support hints emulation.
+ match := crashRe.FindSubmatch(res.Output)
f.mu.Lock()
defer f.mu.Unlock()
+ if match != nil {
+ crash := string(match[1])
+ f.t.Logf("CRASH: %s", crash)
+ res.Status = queue.Crashed
+ if !f.expectedCrashes[crash] {
+ f.t.Errorf("unexpected crash: %q", crash)
+ }
+ f.crashes[crash]++
+ }
f.iter++
if f.iter%100 == 0 {
f.t.Logf("<iter %d>: corpus %d, signal %d, max signal %d, crash types %d, running jobs %d",
f.iter, f.fuzzer.Config.Corpus.StatProgs.Val(), f.fuzzer.Config.Corpus.StatSignal.Val(),
len(f.fuzzer.Cover.maxSignal), len(f.crashes), f.fuzzer.statJobs.Val())
}
- 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.Next()
- res, crash, err := proc.execute(req)
- if err != nil {
- return err
- }
- if crash != "" {
- res = &queue.Result{Status: queue.Crashed}
- 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()
- }
- req.Done(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)
- }
-}
-
-// 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 flatrpc.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
- execOpts.EnvFlags |= flatrpc.ExecEnvSignal
- env, err := ipc.MakeEnv(config, 0)
- if err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() { env.Close() })
- return &executorProc{
- env: env,
- execOpts: *execOpts,
+ if !f.finished.Load() && (f.iter > f.iterLimit || len(f.crashes) == len(f.expectedCrashes)) {
+ f.done()
+ f.finished.Store(true)
}
+ return true
}
var crashRe = regexp.MustCompile(`{{CRASH: (.*?)}}`)
-func (proc *executorProc) execute(req *queue.Request) (*queue.Result, string, error) {
- // TODO: support hints emulation.
- output, info, _, err := proc.env.Exec(&req.ExecOpts, req.Prog)
- ret := crashRe.FindStringSubmatch(string(output))
- if ret != nil {
- return nil, ret[1], nil
- } else if err != nil {
- return nil, "", err
- }
- return &queue.Result{Info: info}, "", nil
-}
-
func checkGoroutineLeaks() {
// Inspired by src/net/http/main_test.go.
buf := make([]byte, 2<<20)
diff --git a/pkg/fuzzer/job.go b/pkg/fuzzer/job.go
index 0f6e0309c..0268172a9 100644
--- a/pkg/fuzzer/job.go
+++ b/pkg/fuzzer/job.go
@@ -121,6 +121,7 @@ func (job *triageJob) handleCall(call int, info *triageCall) {
}
if job.flags&ProgSmashed == 0 {
job.fuzzer.startJob(job.fuzzer.statJobsSmash, &smashJob{
+ exec: job.fuzzer.smashQueue,
p: p.Clone(),
call: call,
})
@@ -240,11 +241,10 @@ func (job *triageJob) minimize(call int, info *triageCall) (*prog.Prog, int) {
}
for i := 0; i < minimizeAttempts; i++ {
result := job.execute(&queue.Request{
- Prog: p1,
- ExecOpts: setFlags(flatrpc.ExecFlagCollectSignal),
- SignalFilter: info.newStableSignal,
- SignalFilterCall: call1,
- Stat: job.fuzzer.statExecMinimize,
+ Prog: p1,
+ ExecOpts: setFlags(flatrpc.ExecFlagCollectSignal),
+ ReturnAllSignal: []int{call1},
+ Stat: job.fuzzer.statExecMinimize,
}, 0)
if result.Stop() {
stop = true
@@ -294,6 +294,7 @@ func getSignalAndCover(p *prog.Prog, info *flatrpc.ProgInfo, call int) signal.Si
}
type smashJob struct {
+ exec queue.Executor
p *prog.Prog
call int
}
@@ -302,6 +303,7 @@ 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(fuzzer.statJobsHints, &hintsJob{
+ exec: fuzzer.smashQueue,
p: job.p.Clone(),
call: job.call,
})
@@ -315,7 +317,7 @@ func (job *smashJob) run(fuzzer *Fuzzer) {
fuzzer.ChoiceTable(),
fuzzer.Config.NoMutateCalls,
fuzzer.Config.Corpus.Programs())
- result := fuzzer.execute(fuzzer.smashQueue, &queue.Request{
+ result := fuzzer.execute(job.exec, &queue.Request{
Prog: p,
ExecOpts: setFlags(flatrpc.ExecFlagCollectSignal),
Stat: fuzzer.statExecSmash,
@@ -324,7 +326,7 @@ func (job *smashJob) run(fuzzer *Fuzzer) {
return
}
if fuzzer.Config.Collide {
- result := fuzzer.execute(fuzzer.smashQueue, &queue.Request{
+ result := fuzzer.execute(job.exec, &queue.Request{
Prog: randomCollide(p, rnd),
Stat: fuzzer.statExecCollide,
})
@@ -366,7 +368,7 @@ func (job *smashJob) faultInjection(fuzzer *Fuzzer) {
job.call, nth)
newProg := job.p.Clone()
newProg.Calls[job.call].Props.FailNth = nth
- result := fuzzer.execute(fuzzer.smashQueue, &queue.Request{
+ result := fuzzer.execute(job.exec, &queue.Request{
Prog: newProg,
Stat: fuzzer.statExecFaultInject,
})
@@ -382,6 +384,7 @@ func (job *smashJob) faultInjection(fuzzer *Fuzzer) {
}
type hintsJob struct {
+ exec queue.Executor
p *prog.Prog
call int
}
@@ -393,7 +396,7 @@ func (job *hintsJob) run(fuzzer *Fuzzer) {
var comps prog.CompMap
for i := 0; i < 2; i++ {
- result := fuzzer.execute(fuzzer.smashQueue, &queue.Request{
+ result := fuzzer.execute(job.exec, &queue.Request{
Prog: p,
ExecOpts: setFlags(flatrpc.ExecFlagCollectComps),
Stat: fuzzer.statExecSeed,
@@ -420,7 +423,7 @@ func (job *hintsJob) run(fuzzer *Fuzzer) {
// Execute each of such mutants to check if it gives new coverage.
p.MutateWithHints(job.call, comps,
func(p *prog.Prog) bool {
- result := fuzzer.execute(fuzzer.smashQueue, &queue.Request{
+ result := fuzzer.execute(job.exec, &queue.Request{
Prog: p,
ExecOpts: setFlags(flatrpc.ExecFlagCollectSignal),
Stat: fuzzer.statExecHint,
diff --git a/pkg/fuzzer/queue/queue.go b/pkg/fuzzer/queue/queue.go
index 46df9d234..051f7205e 100644
--- a/pkg/fuzzer/queue/queue.go
+++ b/pkg/fuzzer/queue/queue.go
@@ -37,7 +37,6 @@ type Request struct {
// Options needed by runtest.
BinaryFile string // If set, it's executed instead of Prog.
- Repeat int // Repeats in addition to the first run.
// Important requests will be retried even from crashed VMs.
Important bool
@@ -113,6 +112,11 @@ func (r *Request) Validate() error {
if (collectComps) && (collectSignal || collectCover) {
return fmt.Errorf("hint collection is mutually exclusive with signal/coverage")
}
+ sandboxes := flatrpc.ExecEnvSandboxNone | flatrpc.ExecEnvSandboxSetuid |
+ flatrpc.ExecEnvSandboxNamespace | flatrpc.ExecEnvSandboxAndroid
+ if r.BinaryFile == "" && r.ExecOpts.EnvFlags&sandboxes == 0 {
+ return fmt.Errorf("no sandboxes set")
+ }
return nil
}
@@ -415,3 +419,24 @@ func (d *Deduplicator) onDone(req *Request, res *Result) bool {
}
return true
}
+
+// DefaultOpts applies opts to all requests in source.
+func DefaultOpts(source Source, opts flatrpc.ExecOpts) Source {
+ return &defaultOpts{source, opts}
+}
+
+type defaultOpts struct {
+ source Source
+ opts flatrpc.ExecOpts
+}
+
+func (do *defaultOpts) Next() *Request {
+ req := do.source.Next()
+ if req == nil {
+ return nil
+ }
+ req.ExecOpts.ExecFlags |= do.opts.ExecFlags
+ req.ExecOpts.EnvFlags |= do.opts.EnvFlags
+ req.ExecOpts.SandboxArg = do.opts.SandboxArg
+ return req
+}