From 68911a35911bfdeb62bef87d9e844b1c19f86580 Mon Sep 17 00:00:00 2001 From: Dmitry Vyukov Date: Mon, 15 Apr 2024 14:55:02 +0200 Subject: syz-manager, syz-fuzzer: send exec encoded programs to fuzzer Don't send text program to the fuzzer, instead send exec encoding directly. It's more compact now and does not need complex deserialization. --- syz-fuzzer/fuzzer.go | 50 ++++++++++---------------------------------------- syz-fuzzer/proc.go | 41 +++++++++++++++-------------------------- syz-manager/rpc.go | 49 ++++++++++++++++++++++++++++++++++--------------- syz-manager/stats.go | 1 - 4 files changed, 59 insertions(+), 82 deletions(-) diff --git a/syz-fuzzer/fuzzer.go b/syz-fuzzer/fuzzer.go index 7b4fa4217..7bcefa1ff 100644 --- a/syz-fuzzer/fuzzer.go +++ b/syz-fuzzer/fuzzer.go @@ -39,12 +39,11 @@ type FuzzerTool struct { triagedCandidates uint32 timeouts targets.Timeouts - bufferTooSmall atomic.Uint64 noExecRequests atomic.Uint64 noExecDuration atomic.Uint64 resetAccState bool - inputs chan executionRequest + requests chan rpctype.ExecutionRequest results chan executionResult signalMu sync.RWMutex maxSignal signal.Signal @@ -59,12 +58,6 @@ type executionResult struct { info *ipc.ProgInfo } -// executionRequest offloads prog deseralization to another thread. -type executionRequest struct { - rpctype.ExecutionRequest - prog *prog.Prog -} - func createIPCConfig(features *host.Features, config *ipc.Config) { if features[host.FeatureExtraCoverage].Enabled { config.Flags |= ipc.FlagExtraCover @@ -165,7 +158,7 @@ func main() { return } - machineInfo, modules := collectMachineInfos(target) + machineInfo, modules := collectMachineInfos() log.Logf(0, "dialing manager at %v", *flagManager) manager, err := rpctype.NewRPCClient(*flagManager, timeouts.Scale, false, *flagNetCompression) @@ -236,8 +229,8 @@ func main() { config: config, resetAccState: *flagResetAccState, - inputs: make(chan executionRequest, inputsCount), - results: make(chan executionResult, inputsCount), + requests: make(chan rpctype.ExecutionRequest, inputsCount), + results: make(chan executionResult, inputsCount), } fuzzerTool.gate = ipc.NewGate(gateSize, fuzzerTool.useBugFrames(r, *flagProcs)) @@ -259,7 +252,7 @@ func main() { fuzzerTool.exchangeDataWorker() } -func collectMachineInfos(target *prog.Target) ([]byte, []host.KernelModule) { +func collectMachineInfos() ([]byte, []host.KernelModule) { machineInfo, err := host.CollectMachineInfo() if err != nil { log.SyzFatalf("failed to collect machine information: %v", err) @@ -330,17 +323,15 @@ func (tool *FuzzerTool) startExecutingCall(progID int64, pid, try int) { }) } -func (tool *FuzzerTool) exchangeDataCall(needProgs int, results []executionResult, +func (tool *FuzzerTool) exchangeDataCall(needProgs int, results []rpctype.ExecutionResult, latency time.Duration) time.Duration { a := &rpctype.ExchangeInfoRequest{ Name: tool.name, NeedProgs: needProgs, + Results: results, StatsDelta: tool.grabStats(), Latency: latency, } - for _, result := range results { - a.Results = append(a.Results, tool.convertExecutionResult(result)) - } r := &rpctype.ExchangeInfoReply{} start := osutil.MonotonicNano() if err := tool.manager.Call("Manager.ExchangeInfo", a, r); err != nil { @@ -352,14 +343,7 @@ func (tool *FuzzerTool) exchangeDataCall(needProgs int, results []executionResul } tool.updateMaxSignal(r.NewMaxSignal, r.DropMaxSignal) for _, req := range r.Requests { - p := tool.deserializeInput(req.ProgData) - if p == nil { - log.SyzFatalf("failed to deserialize input: %s", req.ProgData) - } - tool.inputs <- executionRequest{ - ExecutionRequest: req, - prog: p, - } + tool.requests <- req } return latency } @@ -367,15 +351,13 @@ func (tool *FuzzerTool) exchangeDataCall(needProgs int, results []executionResul func (tool *FuzzerTool) exchangeDataWorker() { var latency time.Duration for result := range tool.results { - results := []executionResult{ - result, - } + results := []rpctype.ExecutionResult{tool.convertExecutionResult(result)} // Grab other finished calls, just in case there are any. loop: for { select { case res := <-tool.results: - results = append(results, res) + results = append(results, tool.convertExecutionResult(res)) default: break loop } @@ -405,23 +387,11 @@ func (tool *FuzzerTool) grabStats() map[string]uint64 { for _, proc := range tool.procs { stats["executor restarts"] += atomic.SwapUint64(&proc.env.StatRestarts, 0) } - stats["buffer too small"] = tool.bufferTooSmall.Swap(0) stats["no exec requests"] = tool.noExecRequests.Swap(0) stats["no exec duration"] = tool.noExecDuration.Swap(0) return stats } -func (tool *FuzzerTool) deserializeInput(inp []byte) *prog.Prog { - p, err := tool.target.Deserialize(inp, prog.NonStrict) - if err != nil { - log.SyzFatalf("failed to deserialize prog: %v\n%s", err, inp) - } - if len(p.Calls) > prog.MaxCalls { - return nil - } - return p -} - func (tool *FuzzerTool) diffMaxSignal(info *ipc.ProgInfo, mask signal.Signal, maskCall int) { tool.signalMu.RLock() defer tool.signalMu.RUnlock() diff --git a/syz-fuzzer/proc.go b/syz-fuzzer/proc.go index 072a4b66f..be1c82ab5 100644 --- a/syz-fuzzer/proc.go +++ b/syz-fuzzer/proc.go @@ -11,7 +11,6 @@ import ( "github.com/google/syzkaller/pkg/log" "github.com/google/syzkaller/pkg/osutil" "github.com/google/syzkaller/pkg/rpctype" - "github.com/google/syzkaller/prog" ) // Proc represents a single fuzzing process (executor). @@ -60,11 +59,11 @@ func (proc *Proc) loop() { (req.NeedCover || req.NeedSignal != rpctype.NoSignal || req.NeedHints) { proc.env.ForceRestart() } - info, try := proc.execute(&opts, req.ID, req.prog) + info, try := proc.execute(&opts, req.ID, req.ProgData) // Let's perform signal filtering in a separate thread to get the most // exec/sec out of a syz-executor instance. proc.tool.results <- executionResult{ - ExecutionRequest: req.ExecutionRequest, + ExecutionRequest: req, procID: proc.pid, try: try, info: info, @@ -72,30 +71,22 @@ func (proc *Proc) loop() { } } -func (proc *Proc) nextRequest() executionRequest { +func (proc *Proc) nextRequest() rpctype.ExecutionRequest { select { - case req := <-proc.tool.inputs: + case req := <-proc.tool.requests: return req default: } // Not having enough inputs to execute is a sign of RPC communication problems. // Let's count and report such situations. start := osutil.MonotonicNano() - req := <-proc.tool.inputs + req := <-proc.tool.requests proc.tool.noExecDuration.Add(uint64(osutil.MonotonicNano() - start)) proc.tool.noExecRequests.Add(1) return req } -func (proc *Proc) execute(opts *ipc.ExecOpts, progID int64, p *prog.Prog) (*ipc.ProgInfo, int) { - progData, err := p.SerializeForExec() - if err != nil { - // It's bad if we systematically fail to serialize programs, - // but so far we don't have a better handling than counting this. - // This error is observed a lot on the seeded syz_mount_image calls. - proc.tool.bufferTooSmall.Add(1) - return nil, 0 - } +func (proc *Proc) execute(opts *ipc.ExecOpts, progID int64, progData []byte) (*ipc.ProgInfo, int) { for try := 0; ; try++ { var output []byte var info *ipc.ProgInfo @@ -109,18 +100,16 @@ func (proc *Proc) execute(opts *ipc.ExecOpts, progID int64, p *prog.Prog) (*ipc. proc.tool.startExecutingCall(progID, proc.pid, try) output, info, hanged, err = proc.env.ExecProg(opts, progData) proc.tool.gate.Leave(ticket) - } - if err != nil { - if try > 10 { - log.SyzFatalf("executor %v failed %v times: %v\n%s", proc.pid, try, err, output) + if err == nil { + log.Logf(2, "result hanged=%v: %s", hanged, output) + return info, try } - log.Logf(4, "fuzzer detected executor failure='%v', retrying #%d", err, try+1) - if try > 3 { - time.Sleep(100 * time.Millisecond) - } - continue } - log.Logf(2, "result hanged=%v: %s", hanged, output) - return info, try + log.Logf(4, "fuzzer detected executor failure='%v', retrying #%d", err, try+1) + if try > 10 { + log.SyzFatalf("executor %v failed %v times: %v\n%s", proc.pid, try, err, output) + } else if try > 3 { + time.Sleep(100 * time.Millisecond) + } } } diff --git a/syz-manager/rpc.go b/syz-manager/rpc.go index 4a260738e..94f902085 100644 --- a/syz-manager/rpc.go +++ b/syz-manager/rpc.go @@ -45,6 +45,7 @@ type RPCServer struct { statExecs *stats.Val statExecRetries *stats.Val + statExecBufferTooSmall *stats.Val statVMRestarts *stats.Val statExchangeCalls *stats.Val statExchangeProgs *stats.Val @@ -95,6 +96,8 @@ func startRPCServer(mgr *Manager) (*RPCServer, error) { statExecRetries: stats.Create("exec retries", "Number of times a test program was restarted because the first run failed", stats.Rate{}, stats.Graph("executor")), + statExecBufferTooSmall: stats.Create("buffer too small", + "Program serialization overflowed exec buffer", stats.NoGraph), statVMRestarts: stats.Create("vm restarts", "Total number of VM starts", stats.Rate{}, stats.NoGraph), statExchangeCalls: stats.Create("exchange calls", "Number of RPC Exchange calls", @@ -243,20 +246,32 @@ func (serv *RPCServer) ExchangeInfo(a *rpctype.ExchangeInfoRequest, r *rpctype.E return nil } - fuzzer := serv.mgr.getFuzzer() - if fuzzer == nil { + fuzzerObj := serv.mgr.getFuzzer() + if fuzzerObj == nil { // ExchangeInfo calls follow MachineCheck, so the fuzzer must have been initialized. panic("exchange info call with nil fuzzer") } + appendRequest := func(inp *fuzzer.Request) { + if req, ok := runner.newRequest(inp); ok { + r.Requests = append(r.Requests, req) + } else { + // It's bad if we systematically fail to serialize programs, + // but so far we don't have a better handling than counting this. + // This error is observed a lot on the seeded syz_mount_image calls. + serv.statExecBufferTooSmall.Add(1) + fuzzerObj.Done(inp, &fuzzer.Result{Stop: true}) + } + } + // Try to collect some of the postponed requests. if serv.mu.TryLock() { - for i := len(serv.rescuedInputs) - 1; i >= 0 && a.NeedProgs > 0; i-- { - inp := serv.rescuedInputs[i] - serv.rescuedInputs[i] = nil - serv.rescuedInputs = serv.rescuedInputs[:i] - r.Requests = append(r.Requests, runner.newRequest(inp)) - a.NeedProgs-- + for len(serv.rescuedInputs) != 0 && len(r.Requests) < a.NeedProgs { + last := len(serv.rescuedInputs) - 1 + inp := serv.rescuedInputs[last] + serv.rescuedInputs[last] = nil + serv.rescuedInputs = serv.rescuedInputs[:last] + appendRequest(inp) } serv.mu.Unlock() } @@ -264,13 +279,12 @@ func (serv *RPCServer) ExchangeInfo(a *rpctype.ExchangeInfoRequest, r *rpctype.E // First query new inputs and only then post results. // It should foster a more even distribution of executions // across all VMs. - for i := 0; i < a.NeedProgs; i++ { - inp := fuzzer.NextInput() - r.Requests = append(r.Requests, runner.newRequest(inp)) + for len(r.Requests) < a.NeedProgs { + appendRequest(fuzzerObj.NextInput()) } for _, result := range a.Results { - runner.doneRequest(result, fuzzer, serv.cfg.Cover) + runner.doneRequest(result, fuzzerObj, serv.cfg.Cover) } stats.Import(a.StatsDelta) @@ -407,7 +421,12 @@ func (runner *Runner) doneRequest(resp rpctype.ExecutionResult, fuzzerObj *fuzze fuzzerObj.Done(req.req, &fuzzer.Result{Info: info}) } -func (runner *Runner) newRequest(req *fuzzer.Request) rpctype.ExecutionRequest { +func (runner *Runner) newRequest(req *fuzzer.Request) (rpctype.ExecutionRequest, bool) { + progData, err := req.Prog.SerializeForExec() + if err != nil { + return rpctype.ExecutionRequest{}, false + } + var signalFilter signal.Signal if req.SignalFilter != nil { newRawSignal := runner.instModules.Decanonicalize(req.SignalFilter.ToRaw()) @@ -425,13 +444,13 @@ func (runner *Runner) newRequest(req *fuzzer.Request) rpctype.ExecutionRequest { runner.mu.Unlock() return rpctype.ExecutionRequest{ ID: id, - ProgData: req.Prog.Serialize(), + ProgData: progData, NeedCover: req.NeedCover, NeedSignal: req.NeedSignal, SignalFilter: signalFilter, SignalFilterCall: req.SignalFilterCall, NeedHints: req.NeedHints, - } + }, true } func (runner *Runner) logProgram(procID int, p *prog.Prog) { diff --git a/syz-manager/stats.go b/syz-manager/stats.go index 53373cf7e..a0719bc45 100644 --- a/syz-manager/stats.go +++ b/syz-manager/stats.go @@ -78,7 +78,6 @@ func (mgr *Manager) initStats() { // Stats imported from the fuzzer (names must match the the fuzzer names). stats.Create("executor restarts", "Number of times executor process was restarted", stats.Rate{}, stats.Graph("executor")) - stats.Create("buffer too small", "Program serialization overflowed exec buffer", stats.NoGraph) stats.Create("no exec requests", "Number of times fuzzer was stalled with no exec requests", stats.Rate{}) stats.Create("no exec duration", "Total duration fuzzer was stalled with no exec requests (ns/sec)", stats.Rate{}) } -- cgit mrf-deployment