syzkaller/syz-fuzzer/fuzzer.go
Dmitry Vyukov e726bdf922 syz-manager: make rpc communication finer grained
RPC package does excessive caching per connection,
so if a larger object is ever sent in any direction,
rpc connection consumes large amount of memory persistently.
This makes manager consume gigs of memory with large
number of VMs and larger corpus/coverage.

Make all communication done in very limited batches.
2018-06-26 13:59:47 +02:00

419 lines
11 KiB
Go

// Copyright 2015 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 (
"flag"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"runtime/debug"
"sync"
"sync/atomic"
"time"
"github.com/google/syzkaller/pkg/hash"
"github.com/google/syzkaller/pkg/host"
"github.com/google/syzkaller/pkg/ipc"
"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/prog"
_ "github.com/google/syzkaller/sys"
)
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
stats [StatCount]uint64
manager *rpctype.RPCClient
target *prog.Target
faultInjectionEnabled bool
comparisonTracingEnabled bool
corpusMu sync.RWMutex
corpus []*prog.Prog
corpusHashes map[hash.Sig]struct{}
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
logMu sync.Mutex
}
type Stat int
const (
StatGenerate Stat = iota
StatFuzz
StatCandidate
StatTriage
StatMinimize
StatSmash
StatHint
StatSeed
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",
}
type OutputType int
const (
OutputNone OutputType = iota
OutputStdout
OutputDmesg
OutputFile
)
func main() {
debug.SetGCPercent(50)
var (
flagName = flag.String("name", "test", "unique name for manager")
flagArch = flag.String("arch", runtime.GOARCH, "target arch")
flagManager = flag.String("manager", "", "manager rpc address")
flagProcs = flag.Int("procs", 1, "number of parallel test processes")
flagOutput = flag.String("output", "stdout", "write programs to none/stdout/dmesg/file")
flagPprof = flag.String("pprof", "", "address to serve pprof profiles")
flagTest = flag.Bool("test", false, "enable image testing mode") // used by syz-ci
)
flag.Parse()
var outputType OutputType
switch *flagOutput {
case "none":
outputType = OutputNone
case "stdout":
outputType = OutputStdout
case "dmesg":
outputType = OutputDmesg
case "file":
outputType = OutputFile
default:
fmt.Fprintf(os.Stderr, "-output flag must be one of none/stdout/dmesg/file\n")
os.Exit(1)
}
log.Logf(0, "fuzzer started")
target, err := prog.GetTarget(runtime.GOOS, *flagArch)
if err != nil {
log.Fatalf("%v", err)
}
config, execOpts, err := ipc.DefaultConfig()
if err != nil {
log.Fatalf("failed to create default ipc config: %v", err)
}
sandbox := "none"
if config.Flags&ipc.FlagSandboxSetuid != 0 {
sandbox = "setuid"
} else if config.Flags&ipc.FlagSandboxNamespace != 0 {
sandbox = "namespace"
}
shutdown := make(chan struct{})
osutil.HandleInterrupts(shutdown)
go func() {
// Handles graceful preemption on GCE.
<-shutdown
log.Logf(0, "SYZ-FUZZER: PREEMPTED")
os.Exit(1)
}()
checkArgs := &checkArgs{
target: target,
sandbox: sandbox,
ipcConfig: config,
ipcExecOpts: execOpts,
}
if *flagTest {
testImage(*flagManager, checkArgs)
return
}
if *flagPprof != "" {
go func() {
err := http.ListenAndServe(*flagPprof, nil)
log.Fatalf("failed to serve pprof profiles: %v", err)
}()
} else {
runtime.MemProfileRate = 0
}
log.Logf(0, "dialing manager at %v", *flagManager)
manager, err := rpctype.NewRPCClient(*flagManager)
if err != nil {
log.Fatalf("failed to connect to manager: %v ", err)
}
a := &rpctype.ConnectArgs{Name: *flagName}
r := &rpctype.ConnectRes{}
if err := manager.Call("Manager.Connect", a, r); err != nil {
log.Fatalf("failed to connect to manager: %v ", err)
}
if r.CheckResult == nil {
checkArgs.gitRevision = r.GitRevision
checkArgs.targetRevision = r.TargetRevision
checkArgs.enabledCalls = r.EnabledCalls
r.CheckResult, err = checkMachine(checkArgs)
if err != nil {
r.CheckResult = &rpctype.CheckArgs{
Error: err.Error(),
}
}
r.CheckResult.Name = *flagName
if err := manager.Call("Manager.Check", r.CheckResult, nil); err != nil {
log.Fatalf("Manager.Check call failed: %v", err)
}
if r.CheckResult.Error != "" {
log.Fatalf("%v", r.CheckResult.Error)
}
}
log.Logf(0, "syscalls: %v", len(r.CheckResult.EnabledCalls))
for _, feat := range r.CheckResult.Features {
log.Logf(0, "%v: %v", feat.Name, feat.Reason)
}
periodicCallback, err := host.Setup(r.CheckResult.Features)
if err != nil {
log.Fatalf("BUG: %v", err)
}
if r.CheckResult.Features[host.FeatureNetworkInjection].Enabled {
config.Flags |= ipc.FlagEnableTun
}
if r.CheckResult.Features[host.FeatureFaultInjection].Enabled {
config.Flags |= ipc.FlagEnableFault
}
needPoll := make(chan struct{}, 1)
needPoll <- struct{}{}
fuzzer := &Fuzzer{
name: *flagName,
outputType: outputType,
config: config,
execOpts: execOpts,
gate: ipc.NewGate(2**flagProcs, periodicCallback),
workQueue: newWorkQueue(*flagProcs, needPoll),
needPoll: needPoll,
manager: manager,
target: target,
faultInjectionEnabled: r.CheckResult.Features[host.FeatureFaultInjection].Enabled,
comparisonTracingEnabled: r.CheckResult.Features[host.FeatureComparisons].Enabled,
corpusHashes: make(map[hash.Sig]struct{}),
}
for i := 0; fuzzer.poll(i == 0, nil); i++ {
}
calls := make(map[*prog.Syscall]bool)
for _, id := range r.CheckResult.EnabledCalls {
calls[target.Syscalls[id]] = true
}
prios := target.CalculatePriorities(fuzzer.corpus)
fuzzer.choiceTable = target.BuildChoiceTable(prios, calls)
for pid := 0; pid < *flagProcs; pid++ {
proc, err := newProc(fuzzer, pid)
if err != nil {
log.Fatalf("failed to create proc: %v", err)
}
fuzzer.procs = append(fuzzer.procs, proc)
go proc.loop()
}
fuzzer.pollLoop()
}
func (fuzzer *Fuzzer) pollLoop() {
var execTotal uint64
var lastPoll time.Time
var lastPrint time.Time
ticker := time.NewTicker(3 * time.Second).C
for {
poll := false
select {
case <-ticker:
case <-fuzzer.needPoll:
poll = true
}
if fuzzer.outputType != OutputStdout && time.Since(lastPrint) > 10*time.Second {
// Keep-alive for manager.
log.Logf(0, "alive, executed %v", execTotal)
lastPrint = time.Now()
}
if poll || time.Since(lastPoll) > 10*time.Second {
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) {
lastPoll = time.Now()
}
}
}
}
func (fuzzer *Fuzzer) poll(needCandidates bool, stats map[string]uint64) bool {
a := &rpctype.PollArgs{
Name: fuzzer.name,
NeedCandidates: needCandidates,
MaxSignal: fuzzer.grabNewSignal().Serialize(),
Stats: stats,
}
r := &rpctype.PollRes{}
if err := fuzzer.manager.Call("Manager.Poll", a, r); err != nil {
log.Fatalf("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)
for _, inp := range r.NewInputs {
fuzzer.addInputFromAnotherFuzzer(inp)
}
for _, candidate := range r.Candidates {
p, err := fuzzer.target.Deserialize(candidate.Prog)
if err != nil {
log.Fatalf("failed to parse program from manager: %v", err)
}
flags := ProgCandidate
if candidate.Minimized {
flags |= ProgMinimized
}
if candidate.Smashed {
flags |= ProgSmashed
}
fuzzer.workQueue.enqueue(&WorkCandidate{
p: p,
flags: flags,
})
}
return len(r.NewInputs) != 0 || len(r.Candidates) != 0 || maxSignal.Len() != 0
}
func (fuzzer *Fuzzer) sendInputToManager(inp rpctype.RPCInput) {
a := &rpctype.NewInputArgs{
Name: fuzzer.name,
RPCInput: inp,
}
if err := fuzzer.manager.Call("Manager.NewInput", a, nil); err != nil {
log.Fatalf("Manager.NewInput call failed: %v", err)
}
}
func (fuzzer *Fuzzer) addInputFromAnotherFuzzer(inp rpctype.RPCInput) {
p, err := fuzzer.target.Deserialize(inp.Prog)
if err != nil {
log.Fatalf("failed to deserialize prog from another fuzzer: %v", err)
}
sig := hash.Hash(inp.Prog)
sign := inp.Signal.Deserialize()
fuzzer.addInputToCorpus(p, sign, sig)
}
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{}{}
}
fuzzer.corpusMu.Unlock()
if !sign.Empty() {
fuzzer.signalMu.Lock()
fuzzer.corpusSignal.Merge(sign)
fuzzer.maxSignal.Merge(sign)
fuzzer.signalMu.Unlock()
}
}
func (fuzzer *Fuzzer) corpusSnapshot() []*prog.Prog {
fuzzer.corpusMu.RLock()
defer fuzzer.corpusMu.RUnlock()
return fuzzer.corpus
}
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.CallInfo) (calls []int) {
fuzzer.signalMu.RLock()
defer fuzzer.signalMu.RUnlock()
for i, inf := range info {
diff := fuzzer.maxSignal.DiffRaw(inf.Signal, signalPrio(p.Target, p.Calls[i], &inf))
if diff.Empty() {
continue
}
calls = append(calls, i)
fuzzer.signalMu.RUnlock()
fuzzer.signalMu.Lock()
fuzzer.maxSignal.Merge(diff)
fuzzer.newSignal.Merge(diff)
fuzzer.signalMu.Unlock()
fuzzer.signalMu.RLock()
}
return
}
func signalPrio(target *prog.Target, c *prog.Call, ci *ipc.CallInfo) (prio uint8) {
if ci.Errno == 0 {
prio |= 1 << 1
}
if !target.CallContainsAny(c) {
prio |= 1 << 0
}
return
}