mirror of
https://github.com/reactos/syzkaller.git
synced 2024-11-23 11:29:46 +00:00
559fbe2dbe
These checks still fire episodically [on gvisor instance only?]. I've done several attempts to debug this/extend checks. But so far I have no glue and we are still seeing them. They are rare enough to be directly debuggable and to be something trivial. This may be some memory corruption (kernel or our race), or some very episodic condition. They are rare enough to be a problem, so don't include syscall name so that they all go into a single bug bucket.
352 lines
10 KiB
Go
352 lines
10 KiB
Go
// Copyright 2017 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 (
|
|
"bytes"
|
|
"fmt"
|
|
"math/rand"
|
|
"os"
|
|
"runtime/debug"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/google/syzkaller/pkg/cover"
|
|
"github.com/google/syzkaller/pkg/hash"
|
|
"github.com/google/syzkaller/pkg/ipc"
|
|
"github.com/google/syzkaller/pkg/log"
|
|
"github.com/google/syzkaller/pkg/rpctype"
|
|
"github.com/google/syzkaller/pkg/signal"
|
|
"github.com/google/syzkaller/prog"
|
|
)
|
|
|
|
// Proc represents a single fuzzing process (executor).
|
|
type Proc struct {
|
|
fuzzer *Fuzzer
|
|
pid int
|
|
env *ipc.Env
|
|
rnd *rand.Rand
|
|
execOpts *ipc.ExecOpts
|
|
execOptsCover *ipc.ExecOpts
|
|
execOptsComps *ipc.ExecOpts
|
|
execOptsNoCollide *ipc.ExecOpts
|
|
}
|
|
|
|
func newProc(fuzzer *Fuzzer, pid int) (*Proc, error) {
|
|
env, err := ipc.MakeEnv(fuzzer.config, pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rnd := rand.New(rand.NewSource(time.Now().UnixNano() + int64(pid)*1e12))
|
|
execOptsNoCollide := *fuzzer.execOpts
|
|
execOptsNoCollide.Flags &= ^ipc.FlagCollide
|
|
execOptsCover := execOptsNoCollide
|
|
execOptsCover.Flags |= ipc.FlagCollectCover
|
|
execOptsComps := execOptsNoCollide
|
|
execOptsComps.Flags |= ipc.FlagCollectComps
|
|
proc := &Proc{
|
|
fuzzer: fuzzer,
|
|
pid: pid,
|
|
env: env,
|
|
rnd: rnd,
|
|
execOpts: fuzzer.execOpts,
|
|
execOptsCover: &execOptsCover,
|
|
execOptsComps: &execOptsComps,
|
|
execOptsNoCollide: &execOptsNoCollide,
|
|
}
|
|
return proc, nil
|
|
}
|
|
|
|
func (proc *Proc) loop() {
|
|
generatePeriod := 100
|
|
if proc.fuzzer.config.Flags&ipc.FlagSignal == 0 {
|
|
// If we don't have real coverage signal, generate programs more frequently
|
|
// because fallback signal is weak.
|
|
generatePeriod = 2
|
|
}
|
|
for i := 0; ; i++ {
|
|
item := proc.fuzzer.workQueue.dequeue()
|
|
if item != nil {
|
|
switch item := item.(type) {
|
|
case *WorkTriage:
|
|
proc.triageInput(item)
|
|
case *WorkCandidate:
|
|
proc.execute(proc.execOpts, item.p, item.flags, StatCandidate)
|
|
case *WorkSmash:
|
|
proc.smashInput(item)
|
|
default:
|
|
log.Fatalf("unknown work type: %#v", item)
|
|
}
|
|
continue
|
|
}
|
|
|
|
ct := proc.fuzzer.choiceTable
|
|
fuzzerSnapshot := proc.fuzzer.snapshot()
|
|
if len(fuzzerSnapshot.corpus) == 0 || i%generatePeriod == 0 {
|
|
// Generate a new prog.
|
|
p := proc.fuzzer.target.Generate(proc.rnd, prog.RecommendedCalls, ct)
|
|
log.Logf(1, "#%v: generated", proc.pid)
|
|
proc.execute(proc.execOpts, p, ProgNormal, StatGenerate)
|
|
} else {
|
|
// Mutate an existing prog.
|
|
p := fuzzerSnapshot.chooseProgram(proc.rnd).Clone()
|
|
p.Mutate(proc.rnd, prog.RecommendedCalls, ct, fuzzerSnapshot.corpus)
|
|
log.Logf(1, "#%v: mutated", proc.pid)
|
|
proc.execute(proc.execOpts, p, ProgNormal, StatFuzz)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (proc *Proc) triageInput(item *WorkTriage) {
|
|
log.Logf(1, "#%v: triaging type=%x", proc.pid, item.flags)
|
|
|
|
prio := signalPrio(item.p, &item.info, item.call)
|
|
inputSignal := signal.FromRaw(item.info.Signal, prio)
|
|
newSignal := proc.fuzzer.corpusSignalDiff(inputSignal)
|
|
if newSignal.Empty() {
|
|
return
|
|
}
|
|
callName := ".extra"
|
|
logCallName := "extra"
|
|
if item.call != -1 {
|
|
callName = item.p.Calls[item.call].Meta.Name
|
|
logCallName = fmt.Sprintf("call #%v %v", item.call, callName)
|
|
}
|
|
log.Logf(3, "triaging input for %v (new signal=%v)", logCallName, newSignal.Len())
|
|
var inputCover cover.Cover
|
|
const (
|
|
signalRuns = 3
|
|
minimizeAttempts = 3
|
|
)
|
|
// Compute input coverage and non-flaky signal for minimization.
|
|
notexecuted := 0
|
|
for i := 0; i < signalRuns; i++ {
|
|
info := proc.executeRaw(proc.execOptsCover, item.p, StatTriage)
|
|
if !reexecutionSuccess(info, &item.info, item.call) {
|
|
// The call was not executed or failed.
|
|
notexecuted++
|
|
if notexecuted > signalRuns/2+1 {
|
|
return // if happens too often, give up
|
|
}
|
|
continue
|
|
}
|
|
thisSignal, thisCover := getSignalAndCover(item.p, info, item.call)
|
|
newSignal = newSignal.Intersection(thisSignal)
|
|
// Without !minimized check manager starts losing some considerable amount
|
|
// of coverage after each restart. Mechanics of this are not completely clear.
|
|
if newSignal.Empty() && item.flags&ProgMinimized == 0 {
|
|
return
|
|
}
|
|
inputCover.Merge(thisCover)
|
|
}
|
|
if item.flags&ProgMinimized == 0 {
|
|
item.p, item.call = prog.Minimize(item.p, item.call, false,
|
|
func(p1 *prog.Prog, call1 int) bool {
|
|
for i := 0; i < minimizeAttempts; i++ {
|
|
info := proc.execute(proc.execOptsNoCollide, p1, ProgNormal, StatMinimize)
|
|
if !reexecutionSuccess(info, &item.info, call1) {
|
|
// The call was not executed or failed.
|
|
continue
|
|
}
|
|
thisSignal, _ := getSignalAndCover(p1, info, call1)
|
|
if newSignal.Intersection(thisSignal).Len() == newSignal.Len() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
|
|
data := item.p.Serialize()
|
|
sig := hash.Hash(data)
|
|
|
|
log.Logf(2, "added new input for %v to corpus:\n%s", logCallName, data)
|
|
proc.fuzzer.sendInputToManager(rpctype.RPCInput{
|
|
Call: callName,
|
|
Prog: data,
|
|
Signal: inputSignal.Serialize(),
|
|
Cover: inputCover.Serialize(),
|
|
})
|
|
|
|
proc.fuzzer.addInputToCorpus(item.p, inputSignal, sig)
|
|
|
|
if item.flags&ProgSmashed == 0 {
|
|
proc.fuzzer.workQueue.enqueue(&WorkSmash{item.p, item.call})
|
|
}
|
|
}
|
|
|
|
func reexecutionSuccess(info *ipc.ProgInfo, oldInfo *ipc.CallInfo, call int) bool {
|
|
if info == nil || len(info.Calls) == 0 {
|
|
return false
|
|
}
|
|
if call != -1 {
|
|
// Don't minimize calls from successful to unsuccessful.
|
|
// Successful calls are much more valuable.
|
|
if oldInfo.Errno == 0 && info.Calls[call].Errno != 0 {
|
|
return false
|
|
}
|
|
return len(info.Calls[call].Signal) != 0
|
|
}
|
|
return len(info.Extra.Signal) != 0
|
|
}
|
|
|
|
func getSignalAndCover(p *prog.Prog, info *ipc.ProgInfo, call int) (signal.Signal, []uint32) {
|
|
inf := &info.Extra
|
|
if call != -1 {
|
|
inf = &info.Calls[call]
|
|
}
|
|
return signal.FromRaw(inf.Signal, signalPrio(p, inf, call)), inf.Cover
|
|
}
|
|
|
|
func (proc *Proc) smashInput(item *WorkSmash) {
|
|
if proc.fuzzer.faultInjectionEnabled && item.call != -1 {
|
|
proc.failCall(item.p, item.call)
|
|
}
|
|
if proc.fuzzer.comparisonTracingEnabled && item.call != -1 {
|
|
proc.executeHintSeed(item.p, item.call)
|
|
}
|
|
fuzzerSnapshot := proc.fuzzer.snapshot()
|
|
for i := 0; i < 100; i++ {
|
|
p := item.p.Clone()
|
|
p.Mutate(proc.rnd, prog.RecommendedCalls, proc.fuzzer.choiceTable, fuzzerSnapshot.corpus)
|
|
log.Logf(1, "#%v: smash mutated", proc.pid)
|
|
proc.execute(proc.execOpts, p, ProgNormal, StatSmash)
|
|
}
|
|
}
|
|
|
|
func (proc *Proc) failCall(p *prog.Prog, call int) {
|
|
for nth := 0; nth < 100; nth++ {
|
|
log.Logf(1, "#%v: injecting fault into call %v/%v", proc.pid, call, nth)
|
|
opts := *proc.execOpts
|
|
opts.Flags |= ipc.FlagInjectFault
|
|
opts.FaultCall = call
|
|
opts.FaultNth = nth
|
|
info := proc.executeRaw(&opts, p, StatSmash)
|
|
if info != nil && len(info.Calls) > call && info.Calls[call].Flags&ipc.CallFaultInjected == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (proc *Proc) executeHintSeed(p *prog.Prog, call int) {
|
|
log.Logf(1, "#%v: collecting comparisons", proc.pid)
|
|
// First execute the original program to dump comparisons from KCOV.
|
|
info := proc.execute(proc.execOptsComps, p, ProgNormal, StatSeed)
|
|
if info == nil {
|
|
return
|
|
}
|
|
|
|
// Then mutate the initial program for every match between
|
|
// a syscall argument and a comparison operand.
|
|
// Execute each of such mutants to check if it gives new coverage.
|
|
p.MutateWithHints(call, info.Calls[call].Comps, func(p *prog.Prog) {
|
|
log.Logf(1, "#%v: executing comparison hint", proc.pid)
|
|
proc.execute(proc.execOpts, p, ProgNormal, StatHint)
|
|
})
|
|
}
|
|
|
|
func (proc *Proc) execute(execOpts *ipc.ExecOpts, p *prog.Prog, flags ProgTypes, stat Stat) *ipc.ProgInfo {
|
|
info := proc.executeRaw(execOpts, p, stat)
|
|
calls, extra := proc.fuzzer.checkNewSignal(p, info)
|
|
for _, callIndex := range calls {
|
|
proc.enqueueCallTriage(p, flags, callIndex, info.Calls[callIndex])
|
|
}
|
|
if extra {
|
|
proc.enqueueCallTriage(p, flags, -1, info.Extra)
|
|
}
|
|
return info
|
|
}
|
|
|
|
func (proc *Proc) enqueueCallTriage(p *prog.Prog, flags ProgTypes, callIndex int, info ipc.CallInfo) {
|
|
// info.Signal points to the output shmem region, detach it before queueing.
|
|
info.Signal = append([]uint32{}, info.Signal...)
|
|
// None of the caller use Cover, so just nil it instead of detaching.
|
|
// Note: triage input uses executeRaw to get coverage.
|
|
info.Cover = nil
|
|
proc.fuzzer.workQueue.enqueue(&WorkTriage{
|
|
p: p.Clone(),
|
|
call: callIndex,
|
|
info: info,
|
|
flags: flags,
|
|
})
|
|
}
|
|
|
|
func (proc *Proc) executeRaw(opts *ipc.ExecOpts, p *prog.Prog, stat Stat) *ipc.ProgInfo {
|
|
if opts.Flags&ipc.FlagDedupCover == 0 {
|
|
log.Fatalf("dedup cover is not enabled")
|
|
}
|
|
for _, call := range p.Calls {
|
|
if !proc.fuzzer.choiceTable.Enabled(call.Meta.ID) {
|
|
fmt.Printf("executing disabled syscall %v", call.Meta.Name)
|
|
panic("disabled syscall")
|
|
}
|
|
}
|
|
|
|
// Limit concurrency window and do leak checking once in a while.
|
|
ticket := proc.fuzzer.gate.Enter()
|
|
defer proc.fuzzer.gate.Leave(ticket)
|
|
|
|
proc.logProgram(opts, p)
|
|
for try := 0; ; try++ {
|
|
atomic.AddUint64(&proc.fuzzer.stats[stat], 1)
|
|
output, info, hanged, err := proc.env.Exec(opts, p)
|
|
if err != nil {
|
|
if try > 10 {
|
|
log.Fatalf("executor %v failed %v times:\n%v", proc.pid, try, err)
|
|
}
|
|
log.Logf(4, "fuzzer detected executor failure='%v', retrying #%d", err, try+1)
|
|
debug.FreeOSMemory()
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
log.Logf(2, "result hanged=%v: %s", hanged, output)
|
|
return info
|
|
}
|
|
}
|
|
|
|
func (proc *Proc) logProgram(opts *ipc.ExecOpts, p *prog.Prog) {
|
|
if proc.fuzzer.outputType == OutputNone {
|
|
return
|
|
}
|
|
|
|
data := p.Serialize()
|
|
strOpts := ""
|
|
if opts.Flags&ipc.FlagInjectFault != 0 {
|
|
strOpts = fmt.Sprintf(" (fault-call:%v fault-nth:%v)", opts.FaultCall, opts.FaultNth)
|
|
}
|
|
|
|
// The following output helps to understand what program crashed kernel.
|
|
// It must not be intermixed.
|
|
switch proc.fuzzer.outputType {
|
|
case OutputStdout:
|
|
now := time.Now()
|
|
proc.fuzzer.logMu.Lock()
|
|
fmt.Printf("%02v:%02v:%02v executing program %v%v:\n%s\n",
|
|
now.Hour(), now.Minute(), now.Second(),
|
|
proc.pid, strOpts, data)
|
|
proc.fuzzer.logMu.Unlock()
|
|
case OutputDmesg:
|
|
fd, err := syscall.Open("/dev/kmsg", syscall.O_WRONLY, 0)
|
|
if err == nil {
|
|
buf := new(bytes.Buffer)
|
|
fmt.Fprintf(buf, "syzkaller: executing program %v%v:\n%s\n",
|
|
proc.pid, strOpts, data)
|
|
syscall.Write(fd, buf.Bytes())
|
|
syscall.Close(fd)
|
|
}
|
|
case OutputFile:
|
|
f, err := os.Create(fmt.Sprintf("%v-%v.prog", proc.fuzzer.name, proc.pid))
|
|
if err == nil {
|
|
if strOpts != "" {
|
|
fmt.Fprintf(f, "#%v\n", strOpts)
|
|
}
|
|
f.Write(data)
|
|
f.Close()
|
|
}
|
|
default:
|
|
log.Fatalf("unknown output type: %v", proc.fuzzer.outputType)
|
|
}
|
|
}
|