diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2026-03-06 11:36:44 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2026-03-09 12:29:48 +0000 |
| commit | 176bead5023749b301d573375d79dabee4d0d888 (patch) | |
| tree | d63c6e04860a64a12599c80d50f3b8a5ff27aa4b /pkg/instance | |
| parent | 5cb44a80412f4dcfba9693d7d0e4c2ae352acdaa (diff) | |
pkg/aflow/action/crash: collect test coverage
Collect code coverage for test programs.
This is likley to be needed for #6878 and seed generation workflow.
For now it's not wired into any workflow/tool and is not tested.
But this should provide most of the plumbing to wire it up.
Diffstat (limited to 'pkg/instance')
| -rw-r--r-- | pkg/instance/execprog.go | 63 | ||||
| -rw-r--r-- | pkg/instance/instance.go | 86 | ||||
| -rw-r--r-- | pkg/instance/instance_test.go | 2 |
3 files changed, 105 insertions, 46 deletions
diff --git a/pkg/instance/execprog.go b/pkg/instance/execprog.go index 94a6ddbd5..a1dd9e8a5 100644 --- a/pkg/instance/execprog.go +++ b/pkg/instance/execprog.go @@ -4,9 +4,14 @@ package instance import ( + "bufio" + "bytes" "context" "fmt" "os" + "path/filepath" + "slices" + "strconv" "time" "github.com/google/syzkaller/pkg/csource" @@ -40,6 +45,7 @@ type RunResult struct { Output []byte Report *report.Report Duration time.Duration + Coverage [][]uint64 } const ( @@ -165,8 +171,9 @@ type ExecParams struct { CProg *prog.Prog SyzProg []byte - Opts csource.Options - Duration time.Duration + Opts csource.Options + Duration time.Duration + CollectCoverage bool // If ExitConditions is empty, RunSyzProg() will assume instance.SyzExitConditions. // RunCProg() always runs with binExitConditions. ExitConditions vm.ExitCondition @@ -193,14 +200,41 @@ func (inst *ExecProgInstance) RunCProgRaw(src []byte, target *prog.Target, } func (inst *ExecProgInstance) RunSyzProgFile(progFile string, duration time.Duration, - opts csource.Options, exitCondition vm.ExitCondition) (*RunResult, error) { + opts csource.Options, collectCoverage bool, exitCondition vm.ExitCondition) (*RunResult, error) { + coverFile := "" + if collectCoverage { + coverDir, err := os.MkdirTemp("", "syz-cover") + if err != nil { + return nil, err + } + defer osutil.RemoveAll(coverDir) + coverFile = filepath.Join(coverDir, "cover") + } vmProgFile, err := inst.VMInstance.Copy(progFile) if err != nil { return nil, &TestError{Title: fmt.Sprintf("failed to copy prog to VM: %v", err)} } command := ExecprogCmd(inst.execprogBin, inst.executorBin, inst.mgrCfg.TargetOS, inst.mgrCfg.TargetArch, - inst.mgrCfg.Type, opts, !inst.OldFlagsCompatMode, inst.mgrCfg.Timeouts.Slowdown, vmProgFile) - return inst.runCommand(command, duration, exitCondition) + inst.mgrCfg.Type, opts, !inst.OldFlagsCompatMode, inst.mgrCfg.Timeouts.Slowdown, coverFile, vmProgFile) + res, err := inst.runCommand(command, duration, exitCondition) + if err != nil { + return nil, err + } + if coverFile != "" { + files, err := filepath.Glob(coverFile + "*") + if err != nil { + return nil, fmt.Errorf("failed to glob cover files: %w", err) + } + slices.Sort(files) + for _, f := range files { + cover, err := parseCoverageFile(f) + if err != nil { + return nil, fmt.Errorf("failed to parse cover file: %w", err) + } + res.Coverage = append(res.Coverage, cover) + } + } + return res, nil } func (inst *ExecProgInstance) RunSyzProg(params ExecParams) (*RunResult, error) { @@ -213,5 +247,22 @@ func (inst *ExecProgInstance) RunSyzProg(params ExecParams) (*RunResult, error) if params.ExitConditions == 0 { params.ExitConditions = SyzExitConditions } - return inst.RunSyzProgFile(progFile, params.Duration, params.Opts, params.ExitConditions) + return inst.RunSyzProgFile(progFile, params.Duration, params.Opts, + params.CollectCoverage, params.ExitConditions) +} + +func parseCoverageFile(filename string) ([]uint64, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var res []uint64 + for s := bufio.NewScanner(bytes.NewReader(data)); s.Scan(); { + v, err := strconv.ParseUint(s.Text(), 16, 64) + if err != nil { + return nil, err + } + res = append(res, v) + } + return res, nil } diff --git a/pkg/instance/instance.go b/pkg/instance/instance.go index d8b3a8f71..e63dd7808 100644 --- a/pkg/instance/instance.go +++ b/pkg/instance/instance.go @@ -34,7 +34,7 @@ type Env interface { BuildSyzkaller(string, string) (string, error) CleanKernel(*BuildKernelConfig) error BuildKernel(*BuildKernelConfig) (string, build.ImageDetails, error) - Test(numVMs int, reproSyz, reproOpts, reproC []byte) ([]EnvTestResult, error) + Test(numVMs int, reproSyz, reproOpts, reproC []byte, collectCoverage bool) ([]EnvTestResult, error) } type env struct { @@ -258,7 +258,7 @@ func (err *CrashError) Error() string { // Test boots numVMs VMs, tests basic kernel operation, and optionally tests the provided reproducer. // *TestError is returned if there is a problem with kernel/image (crash, reboot loop, etc). // *CrashError is returned if the reproducer crashes kernel. -func (env *env) Test(numVMs int, reproSyz, reproOpts, reproC []byte) ([]EnvTestResult, error) { +func (env *env) Test(numVMs int, reproSyz, reproOpts, reproC []byte, collectCoverage bool) ([]EnvTestResult, error) { if env.testSem != nil { env.testSem.Wait() defer env.testSem.Signal() @@ -279,14 +279,15 @@ func (env *env) Test(numVMs int, reproSyz, reproOpts, reproC []byte) ([]EnvTestR res := make(chan EnvTestResult, numVMs) for i := 0; i < numVMs; i++ { inst := &inst{ - cfg: env.cfg, - optionalFlags: env.optionalFlags, - reporter: reporter, - vmPool: vmPool, - vmIndex: i, - reproSyz: reproSyz, - reproOpts: reproOpts, - reproC: reproC, + cfg: env.cfg, + optionalFlags: env.optionalFlags, + reporter: reporter, + vmPool: vmPool, + vmIndex: i, + reproSyz: reproSyz, + reproOpts: reproOpts, + reproC: reproC, + collectCoverage: collectCoverage, } go func() { res <- inst.test() }() } @@ -298,20 +299,22 @@ func (env *env) Test(numVMs int, reproSyz, reproOpts, reproC []byte) ([]EnvTestR } type inst struct { - cfg *mgrconfig.Config - optionalFlags bool - reporter *report.Reporter - vmPool *vm.Pool - vm *vm.Instance - vmIndex int - reproSyz []byte - reproOpts []byte - reproC []byte + cfg *mgrconfig.Config + optionalFlags bool + reporter *report.Reporter + vmPool *vm.Pool + vm *vm.Instance + vmIndex int + reproSyz []byte + reproOpts []byte + reproC []byte + collectCoverage bool } type EnvTestResult struct { Error error RawOutput []byte + Coverage [][]uint64 } func (inst *inst) test() EnvTestResult { @@ -362,7 +365,7 @@ func (inst *inst) test() EnvTestResult { return ret } if len(inst.reproSyz) != 0 || len(inst.reproC) != 0 { - ret.RawOutput, ret.Error = inst.testRepro() + ret.RawOutput, ret.Coverage, ret.Error = inst.testRepro() } return ret } @@ -403,44 +406,45 @@ func (inst *inst) testInstance() error { return nil } -func (inst *inst) testRepro() ([]byte, error) { +func (inst *inst) testRepro() ([]byte, [][]uint64, error) { execProg, err := SetupExecProg(inst.vm, inst.cfg, inst.reporter, &OptionalConfig{ OldFlagsCompatMode: !inst.optionalFlags, }) if err != nil { - return nil, err + return nil, nil, err } - transformError := func(res *RunResult, err error) ([]byte, error) { + transformError := func(res *RunResult, err error) ([]byte, [][]uint64, error) { if err != nil { - return nil, err + return nil, nil, err } - if res != nil && res.Report != nil { - return res.Output, &CrashError{Report: res.Report} + if res.Report != nil { + err = &CrashError{Report: res.Report} } - return res.Output, nil + return res.Output, res.Coverage, err } - out := []byte{} + out, coverage := []byte{}, [][]uint64{} if len(inst.reproSyz) > 0 { opts, err := inst.csourceOptions() if err != nil { - return nil, err + return nil, nil, err } - out, err = transformError(execProg.RunSyzProg(ExecParams{ - SyzProg: inst.reproSyz, - Duration: inst.cfg.Timeouts.NoOutputRunningTime, - Opts: opts, + out, coverage, err = transformError(execProg.RunSyzProg(ExecParams{ + SyzProg: inst.reproSyz, + Duration: inst.cfg.Timeouts.NoOutputRunningTime, + Opts: opts, + CollectCoverage: inst.collectCoverage, })) if err != nil { - return out, err + return out, coverage, err } } if len(inst.reproC) > 0 { // We should test for more than full "no output" timeout, but the problem is that C reproducers // don't print anything, so we will get a false "no output" crash. - out, err = transformError(execProg.RunCProgRaw(inst.reproC, inst.cfg.Target, + out, _, err = transformError(execProg.RunCProgRaw(inst.reproC, inst.cfg.Target, inst.cfg.Timeouts.NoOutput/2)) } - return out, err + return out, coverage, err } func (inst *inst) csourceOptions() (csource.Options, error) { @@ -462,7 +466,7 @@ func (inst *inst) csourceOptions() (csource.Options, error) { // nolint:revive func ExecprogCmd(execprog, executor, OS, arch, vmType string, opts csource.Options, - optionalFlags bool, slowdown int, progFile string) string { + optionalFlags bool, slowdown int, coverFile, progFile string) string { repeatCount := 1 if opts.Repeat { repeatCount = 0 @@ -488,11 +492,15 @@ func ExecprogCmd(execprog, executor, OS, arch, vmType string, opts csource.Optio {Name: "type", Value: fmt.Sprint(vmType)}, }) } + coverArg := "" + if coverFile != "" { + coverArg = " -cover=%v -coverfile=" + coverFile + } return fmt.Sprintf("%v -executor=%v -arch=%v%v -sandbox=%v"+ - " -procs=%v -repeat=%v -threaded=%v -collide=%v -cover=0%v %v", + " -procs=%v -repeat=%v -threaded=%v -collide=%v%v%v %v", execprog, executor, arch, osArg, sandbox, opts.Procs, repeatCount, opts.Threaded, opts.Collide, - optionalArg, progFile) + coverArg, optionalArg, progFile) } var MakeBin = func() string { diff --git a/pkg/instance/instance_test.go b/pkg/instance/instance_test.go index cd291233c..ab5e6ffe1 100644 --- a/pkg/instance/instance_test.go +++ b/pkg/instance/instance_test.go @@ -50,7 +50,7 @@ func TestExecprogCmd(t *testing.T) { FaultCall: 2, FaultNth: 3, }, - }, true, 10, "myprog") + }, true, 10, "", "myprog") args := strings.Split(cmdLine, " ")[1:] if err := tool.ParseFlags(flags, args); err != nil { t.Fatal(err) |
