From cdcc8b96dd9215a47ce5ce1074567a1be93eda5f Mon Sep 17 00:00:00 2001 From: Aleksandr Nogikh Date: Wed, 23 Mar 2022 13:50:36 +0000 Subject: all: collect raw coverage Raw coverage might be important when e.g. analysing the origins of out-of-place coverage in coverage reports or understanding why the fuzzer could not reach deeper code. If "raw_cover" is set to true, syzkaller will remember unsorted and unduplicated coverage (PCs) for each its corpus program. --- pkg/instance/instance.go | 14 ++++++++++---- pkg/ipc/ipc.go | 22 +++++++++++++++------- pkg/mgrconfig/config.go | 5 +++++ pkg/rpctype/rpctype.go | 10 ++++++---- syz-fuzzer/fuzzer.go | 22 ++++++++++++++-------- syz-fuzzer/proc.go | 17 ++++++++++------- syz-manager/manager.go | 33 +++++++++++++++++++++++++-------- syz-manager/rpc.go | 1 + 8 files changed, 86 insertions(+), 38 deletions(-) diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index 665281a47..c225f9696 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -452,6 +452,7 @@ type FuzzerCmdArgs struct { Test bool Runtest bool Slowdown int + RawCover bool } func FuzzerCmd(args *FuzzerCmdArgs) string { @@ -470,11 +471,16 @@ func FuzzerCmd(args *FuzzerCmdArgs) string { if args.Verbosity != 0 { verbosityArg = fmt.Sprintf(" -vv=%v", args.Verbosity) } - optionalArg := "" + flags := []tool.Flag{} if args.Slowdown > 0 { - optionalArg = " " + tool.OptionalFlags([]tool.Flag{ - {Name: "slowdown", Value: fmt.Sprint(args.Slowdown)}, - }) + flags = append(flags, tool.Flag{Name: "slowdown", Value: fmt.Sprint(args.Slowdown)}) + } + if args.RawCover { + flags = append(flags, tool.Flag{Name: "raw_cover", Value: "true"}) + } + optionalArg := "" + if len(flags) > 0 { + optionalArg += " " + tool.OptionalFlags(flags) } return fmt.Sprintf("%v -executor=%v -name=%v -arch=%v%v -manager=%v -sandbox=%v"+ " -procs=%v -cover=%v -debug=%v -test=%v%v%v%v", diff --git a/pkg/ipc/ipc.go b/pkg/ipc/ipc.go index 5bf4738ca..6b25d4af4 100644 --- a/pkg/ipc/ipc.go +++ b/pkg/ipc/ipc.go @@ -289,7 +289,7 @@ func (env *Env) Exec(opts *ExecOpts, p *prog.Prog) (output []byte, info *ProgInf return } - info, err0 = env.parseOutput(p) + info, err0 = env.parseOutput(p, opts) if info != nil && env.config.Flags&FlagSignal == 0 { addFallbackSignal(p, info) } @@ -323,7 +323,7 @@ func addFallbackSignal(p *prog.Prog, info *ProgInfo) { } } -func (env *Env) parseOutput(p *prog.Prog) (*ProgInfo, error) { +func (env *Env) parseOutput(p *prog.Prog, opts *ExecOpts) (*ProgInfo, error) { out := env.out ncmd, ok := readUint32(&out) if !ok { @@ -372,19 +372,27 @@ func (env *Env) parseOutput(p *prog.Prog) (*ProgInfo, error) { if len(extraParts) == 0 { return info, nil } - info.Extra = convertExtra(extraParts) + info.Extra = convertExtra(extraParts, opts.Flags&FlagDedupCover > 0) return info, nil } -func convertExtra(extraParts []CallInfo) CallInfo { +func convertExtra(extraParts []CallInfo, dedupCover bool) CallInfo { var extra CallInfo - extraCover := make(cover.Cover) + if dedupCover { + extraCover := make(cover.Cover) + for _, part := range extraParts { + extraCover.Merge(part.Cover) + } + extra.Cover = extraCover.Serialize() + } else { + for _, part := range extraParts { + extra.Cover = append(extra.Cover, part.Cover...) + } + } extraSignal := make(signal.Signal) for _, part := range extraParts { - extraCover.Merge(part.Cover) extraSignal.Merge(signal.FromRaw(part.Signal, 0)) } - extra.Cover = extraCover.Serialize() extra.Signal = make([]uint32, len(extraSignal)) i := 0 for s := range extraSignal { diff --git a/pkg/mgrconfig/config.go b/pkg/mgrconfig/config.go index 5d4a9f8f5..c13e6f452 100644 --- a/pkg/mgrconfig/config.go +++ b/pkg/mgrconfig/config.go @@ -132,6 +132,11 @@ type Config struct { // eg. "0xffffffff81000000:0x10\n" CovFilter covFilterCfg `json:"cover_filter,omitempty"` + // For each prog in the corpus, remember the raw array of PCs obtained from the kernel. + // It can be useful for debugging syzkaller descriptions and syzkaller itself. + // Disabled by default as it slows down fuzzing. + RawCover bool `json:"raw_cover"` + // Reproduce, localize and minimize crashers (default: true). Reproduce bool `json:"reproduce"` diff --git a/pkg/rpctype/rpctype.go b/pkg/rpctype/rpctype.go index cb2f43e76..f1d889a20 100644 --- a/pkg/rpctype/rpctype.go +++ b/pkg/rpctype/rpctype.go @@ -14,10 +14,12 @@ import ( ) type Input struct { - Call string - Prog []byte - Signal signal.Serial - Cover []uint32 + Call string + Prog []byte + Signal signal.Serial + Cover []uint32 + CallID int // seq number of call in the prog to which the item is related (-1 for extra) + RawCover []uint32 } type Candidate struct { diff --git a/syz-fuzzer/fuzzer.go b/syz-fuzzer/fuzzer.go index 8e916f034..f87c7e740 100644 --- a/syz-fuzzer/fuzzer.go +++ b/syz-fuzzer/fuzzer.go @@ -48,6 +48,7 @@ type Fuzzer struct { faultInjectionEnabled bool comparisonTracingEnabled bool + fetchRawCover bool corpusMu sync.RWMutex corpus []*prog.Prog @@ -138,14 +139,15 @@ func main() { debug.SetGCPercent(50) var ( - flagName = flag.String("name", "test", "unique name for manager") - flagOS = flag.String("os", runtime.GOOS, "target OS") - 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") - flagTest = flag.Bool("test", false, "enable image testing mode") // used by syz-ci - flagRunTest = flag.Bool("runtest", false, "enable program testing mode") // used by pkg/runtest + flagName = flag.String("name", "test", "unique name for manager") + flagOS = flag.String("os", runtime.GOOS, "target OS") + 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") + flagTest = flag.Bool("test", false, "enable image testing mode") // used by syz-ci + flagRunTest = flag.Bool("runtest", false, "enable program testing mode") // used by pkg/runtest + flagRawCover = flag.Bool("raw_cover", false, "fetch raw coverage") ) defer tool.Init()() outputType := parseOutputType(*flagOutput) @@ -160,6 +162,9 @@ func main() { if err != nil { log.Fatalf("failed to create default ipc config: %v", err) } + if *flagRawCover { + execOpts.Flags &^= ipc.FlagDedupCover + } timeouts := config.Timeouts sandbox := ipc.FlagsToSandbox(config.Flags) shutdown := make(chan struct{}) @@ -264,6 +269,7 @@ func main() { comparisonTracingEnabled: r.CheckResult.Features[host.FeatureComparisons].Enabled, corpusHashes: make(map[hash.Sig]struct{}), checkResult: r.CheckResult, + fetchRawCover: *flagRawCover, } gateCallback := fuzzer.useBugFrames(r, *flagProcs) fuzzer.gate = ipc.NewGate(2**flagProcs, gateCallback) diff --git a/syz-fuzzer/proc.go b/syz-fuzzer/proc.go index b15560b6a..d46df123b 100644 --- a/syz-fuzzer/proc.go +++ b/syz-fuzzer/proc.go @@ -122,6 +122,7 @@ func (proc *Proc) triageInput(item *WorkTriage) { ) // Compute input coverage and non-flaky signal for minimization. notexecuted := 0 + rawCover := []uint32{} for i := 0; i < signalRuns; i++ { info := proc.executeRaw(proc.execOptsCover, item.p, StatTriage) if !reexecutionSuccess(info, &item.info, item.call) { @@ -133,6 +134,9 @@ func (proc *Proc) triageInput(item *WorkTriage) { continue } thisSignal, thisCover := getSignalAndCover(item.p, info, item.call) + if len(rawCover) == 0 && proc.fuzzer.fetchRawCover { + rawCover = append([]uint32{}, thisCover...) + } 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. @@ -164,10 +168,12 @@ func (proc *Proc) triageInput(item *WorkTriage) { log.Logf(2, "added new input for %v to corpus:\n%s", logCallName, data) proc.fuzzer.sendInputToManager(rpctype.Input{ - Call: callName, - Prog: data, - Signal: inputSignal.Serialize(), - Cover: inputCover.Serialize(), + Call: callName, + CallID: item.call, + Prog: data, + Signal: inputSignal.Serialize(), + Cover: inputCover.Serialize(), + RawCover: rawCover, }) proc.fuzzer.addInputToCorpus(item.p, inputSignal, sig) @@ -303,9 +309,6 @@ func (proc *Proc) randomCollide(origP *prog.Prog) *prog.Prog { } 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") - } proc.fuzzer.checkDisabledCalls(p) // Limit concurrency window and do leak checking once in a while. diff --git a/syz-manager/manager.go b/syz-manager/manager.go index 18ccef497..9d5662188 100644 --- a/syz-manager/manager.go +++ b/syz-manager/manager.go @@ -94,11 +94,17 @@ type Manager struct { modulesInitialized bool } +type CorpusItemUpdate struct { + CallID int + RawCover []uint32 +} + type CorpusItem struct { - Call string - Prog []byte - Signal signal.Serial - Cover []uint32 + Call string + Prog []byte + Signal signal.Serial + Cover []uint32 + Updates []CorpusItemUpdate } func (item *CorpusItem) RPCInput() rpctype.Input { @@ -689,6 +695,7 @@ func (mgr *Manager) runInstanceInner(index int, instanceName string) (*report.Re Test: false, Runtest: false, Slowdown: mgr.cfg.Timeouts.Slowdown, + RawCover: mgr.cfg.RawCover, } cmd := instance.FuzzerCmd(args) outc, errc, err := inst.Run(mgr.cfg.Timeouts.VMRunningTime, mgr.vmStop, cmd) @@ -1164,6 +1171,10 @@ func (mgr *Manager) newInput(inp rpctype.Input, sign signal.Signal) bool { if mgr.saturatedCalls[inp.Call] { return false } + update := CorpusItemUpdate{ + CallID: inp.CallID, + RawCover: inp.RawCover, + } sig := hash.String(inp.Prog) if old, ok := mgr.corpus[sig]; ok { // The input is already present, but possibly with diffent signal/coverage/call. @@ -1173,13 +1184,19 @@ func (mgr *Manager) newInput(inp rpctype.Input, sign signal.Signal) bool { cov.Merge(old.Cover) cov.Merge(inp.Cover) old.Cover = cov.Serialize() + const maxUpdates = 32 + old.Updates = append(old.Updates, update) + if len(old.Updates) > maxUpdates { + old.Updates = old.Updates[:maxUpdates] + } mgr.corpus[sig] = old } else { mgr.corpus[sig] = CorpusItem{ - Call: inp.Call, - Prog: inp.Prog, - Signal: inp.Signal, - Cover: inp.Cover, + Call: inp.Call, + Prog: inp.Prog, + Signal: inp.Signal, + Cover: inp.Cover, + Updates: []CorpusItemUpdate{update}, } mgr.corpusDB.Save(sig, inp.Prog, 0) if err := mgr.corpusDB.Flush(); err != nil { diff --git a/syz-manager/rpc.go b/syz-manager/rpc.go index 8f9eb98b8..397484783 100644 --- a/syz-manager/rpc.go +++ b/syz-manager/rpc.go @@ -302,6 +302,7 @@ func (serv *RPCServer) NewInput(a *rpctype.NewInputArgs, r *int) error { serv.stats.corpusSignal.set(serv.corpusSignal.Len()) a.Input.Cover = nil // Don't send coverage back to all fuzzers. + a.Input.RawCover = nil for _, other := range serv.fuzzers { if other == f || other.rotated { continue -- cgit mrf-deployment